diff --git a/.changeset/extract-colony-process.md b/.changeset/extract-colony-process.md new file mode 100644 index 0000000..68c85ea --- /dev/null +++ b/.changeset/extract-colony-process.md @@ -0,0 +1,17 @@ +--- +'@colony/hooks': patch +'@colony/mcp-server': patch +'@colony/worker': patch +'@imdeadpool/colony': patch +--- + +Extract shared `isMainEntry`, pidfile helpers, `isAlive`, and the +`spawn(process.execPath, …)` wrapper into a new `@colony/process` +package. These utilities had divergent copies in four places +(`apps/cli/src/commands/lifecycle.ts`, `apps/cli/src/commands/worker.ts`, +`apps/mcp-server/src/server.ts`, `apps/worker/src/server.ts`, and +`packages/hooks/src/auto-spawn.ts`). The regex that decides whether +Node should be invoked via `execPath` — the Windows EFTYPE guard — +and the realpath-normalized bin-shim check both now live exactly once. + +No behavior change. Internal helper refactor only. diff --git a/CLAUDE.md b/CLAUDE.md index bcbc3d1..89f7b0c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ Claude Code works the same way Codex does in this repo: isolated `agent/*` branc ## Architectural rules -- Monorepo with pnpm workspaces. Dependency direction is strictly downward: `apps/*` may depend on `packages/*`; `packages/*` may depend on each other only in the order `config → compress → storage → { core, embedding } → hooks → installers`. (`core` and `embedding` are siblings — both consume `config` and `storage`, neither depends on the other.) No upward or sideways imports that break this order. +- Monorepo with pnpm workspaces. Dependency direction is strictly downward: `apps/*` may depend on `packages/*`; `packages/*` may depend on each other only in the order `process → config → compress → storage → { core, embedding } → hooks → installers`. (`core` and `embedding` are siblings — both consume `config` and `storage`, neither depends on the other. `process` has no upstream deps — only `node:` builtins.) No upward or sideways imports that break this order. - All database I/O goes through `@colony/storage`. No other package opens the DB directly. - Settings access goes through `@colony/config`. No direct reads from `~/.colony/settings.json` elsewhere. - All user-visible strings default to the caveman intensity from settings (default `full`). @@ -48,6 +48,7 @@ Claude Code works the same way Codex does in this repo: isolated `agent/*` branc apps/cli user-facing binary apps/worker local HTTP daemon: read-only viewer + embedding backfill loop apps/mcp-server stdio MCP server +packages/process shared pidfile / spawn / isMainEntry helpers (no deps) packages/config settings schema, loader, defaults, settingsDocs() packages/compress compression engine + lexicon packages/storage SQLite + FTS5 + vector adapter diff --git a/apps/cli/package.json b/apps/cli/package.json index 1053d01..88c929a 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -67,6 +67,7 @@ "@colony/hooks": "workspace:*", "@colony/installers": "workspace:*", "@colony/mcp-server": "workspace:*", + "@colony/process": "workspace:*", "@colony/storage": "workspace:*", "@colony/worker": "workspace:*", "tsup": "^8.3.5", diff --git a/apps/cli/src/commands/lifecycle.ts b/apps/cli/src/commands/lifecycle.ts index 5adf861..7ec61c4 100644 --- a/apps/cli/src/commands/lifecycle.ts +++ b/apps/cli/src/commands/lifecycle.ts @@ -1,7 +1,13 @@ import { spawn } from 'node:child_process'; -import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { loadSettings, resolveDataDir } from '@colony/config'; +import { + isAlive, + readPidFile, + removePidFile, + spawnNodeScript, + writePidFile, +} from '@colony/process'; import type { Command } from 'commander'; import kleur from 'kleur'; import { resolveCliPath } from '../util/resolve.js'; @@ -17,23 +23,12 @@ function pidFile(): string { return join(resolveDataDir(loadSettings().dataDir), 'worker.pid'); } -function isAlive(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - async function waitForPidOrPort(timeoutMs = 5000): Promise { const pf = pidFile(); const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { - if (existsSync(pf)) { - const pid = Number(readFileSync(pf, 'utf8')); - if (pid > 0 && isAlive(pid)) return true; - } + const pid = readPidFile(pf); + if (pid !== null && isAlive(pid)) return true; await new Promise((r) => setTimeout(r, 100)); } return false; @@ -41,43 +36,29 @@ async function waitForPidOrPort(timeoutMs = 5000): Promise { function startWorker(silent = false): number | null { const pf = pidFile(); - if (existsSync(pf)) { - const pid = Number(readFileSync(pf, 'utf8')); - if (pid > 0 && isAlive(pid)) { - if (!silent) process.stdout.write(`${kleur.yellow('already running')} (pid ${pid})\n`); - return pid; - } - try { - unlinkSync(pf); - } catch { - // ignore + const existing = readPidFile(pf); + if (existing !== null) { + if (isAlive(existing)) { + if (!silent) process.stdout.write(`${kleur.yellow('already running')} (pid ${existing})\n`); + return existing; } + removePidFile(pf); } - // Spawn `node worker run` so Windows doesn't try to exec a .js directly. - const child = spawn(process.execPath, [resolveCliPath(), 'worker', 'run'], { - detached: true, - stdio: 'ignore', - env: process.env, - }); - child.unref(); - if (child.pid) writeFileSync(pf, String(child.pid)); + const child = spawnNodeScript(resolveCliPath(), ['worker', 'run']); + if (child.pid) writePidFile(pf, child.pid); return child.pid ?? null; } function stopWorker(): boolean { const pf = pidFile(); - if (!existsSync(pf)) return false; - const pid = Number(readFileSync(pf, 'utf8')); + const pid = readPidFile(pf); + if (pid === null) return false; try { process.kill(pid); } catch { // already dead } - try { - unlinkSync(pf); - } catch { - // ignore - } + removePidFile(pf); return true; } diff --git a/apps/cli/src/commands/worker.ts b/apps/cli/src/commands/worker.ts index f6374f7..ef3f007 100644 --- a/apps/cli/src/commands/worker.ts +++ b/apps/cli/src/commands/worker.ts @@ -1,7 +1,12 @@ -import { spawn } from 'node:child_process'; -import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { loadSettings, resolveDataDir } from '@colony/config'; +import { + isAlive, + readPidFile, + removePidFile, + spawnNodeScript, + writePidFile, +} from '@colony/process'; import type { Command } from 'commander'; import kleur from 'kleur'; import { resolveCliPath } from '../util/resolve.js'; @@ -10,15 +15,6 @@ function pidFile(): string { return join(resolveDataDir(loadSettings().dataDir), 'worker.pid'); } -function isAlive(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - export function registerWorkerCommand(program: Command): void { const w = program.command('worker').description('Manage local worker daemon'); @@ -26,24 +22,16 @@ export function registerWorkerCommand(program: Command): void { .description('Start the worker in the background') .action(async () => { const pf = pidFile(); - if (existsSync(pf)) { - const pid = Number(readFileSync(pf, 'utf8')); - if (isAlive(pid)) { - process.stdout.write(`${kleur.yellow('already running')} (pid ${pid})\n`); + const existing = readPidFile(pf); + if (existing !== null) { + if (isAlive(existing)) { + process.stdout.write(`${kleur.yellow('already running')} (pid ${existing})\n`); return; } - unlinkSync(pf); + removePidFile(pf); } - // Spawn `node worker run` — not ` worker run` — because on - // Windows the resolved cliPath is the .js file (npm's bin shim points - // at it), and spawn() can't execute a .js directly → EFTYPE. - const child = spawn(process.execPath, [resolveCliPath(), 'worker', 'run'], { - detached: true, - stdio: 'ignore', - env: process.env, - }); - child.unref(); - writeFileSync(pf, String(child.pid)); + const child = spawnNodeScript(resolveCliPath(), ['worker', 'run']); + if (child.pid) writePidFile(pf, child.pid); process.stdout.write(`${kleur.green('started')} (pid ${child.pid})\n`); }); @@ -58,18 +46,18 @@ export function registerWorkerCommand(program: Command): void { .description('Stop the worker daemon') .action(async () => { const pf = pidFile(); - if (!existsSync(pf)) { + const pid = readPidFile(pf); + if (pid === null) { process.stdout.write(`${kleur.dim('not running')}\n`); return; } - const pid = Number(readFileSync(pf, 'utf8')); try { process.kill(pid); process.stdout.write(`${kleur.green('stopped')} (pid ${pid})\n`); } catch (e) { process.stdout.write(`${kleur.yellow('stale pidfile')} ${String(e)}\n`); } finally { - unlinkSync(pf); + removePidFile(pf); } }); @@ -77,11 +65,11 @@ export function registerWorkerCommand(program: Command): void { .description('Show worker status') .action(async () => { const pf = pidFile(); - if (!existsSync(pf)) { + const pid = readPidFile(pf); + if (pid === null) { process.stdout.write(`${kleur.dim('not running')}\n`); return; } - const pid = Number(readFileSync(pf, 'utf8')); process.stdout.write( `${isAlive(pid) ? kleur.green('running') : kleur.red('dead')} (pid ${pid})\n`, ); diff --git a/apps/mcp-server/package.json b/apps/mcp-server/package.json index b3c076c..eae2bd8 100644 --- a/apps/mcp-server/package.json +++ b/apps/mcp-server/package.json @@ -20,6 +20,7 @@ "@colony/core": "workspace:*", "@colony/embedding": "workspace:*", "@colony/hooks": "workspace:*", + "@colony/process": "workspace:*", "@modelcontextprotocol/sdk": "^1.0.0", "zod": "^3.23.8" }, diff --git a/apps/mcp-server/src/server.ts b/apps/mcp-server/src/server.ts index 7df48d6..d12cc7b 100644 --- a/apps/mcp-server/src/server.ts +++ b/apps/mcp-server/src/server.ts @@ -1,7 +1,5 @@ #!/usr/bin/env node -import { realpathSync } from 'node:fs'; import { join } from 'node:path'; -import { pathToFileURL } from 'node:url'; import { type Settings, loadSettings, resolveDataDir } from '@colony/config'; import { type AgentCapabilities, @@ -22,6 +20,7 @@ import { } from '@colony/core'; import { createEmbedder } from '@colony/embedding'; import { type HookInput, type HookName, upsertActiveSession } from '@colony/hooks'; +import { isMainEntry } from '@colony/process'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; @@ -867,19 +866,9 @@ export async function main(): Promise { await server.connect(transport); } -if (isMainEntry()) { +if (isMainEntry(import.meta.url)) { main().catch((err) => { process.stderr.write(`[colony mcp] fatal: ${String(err)}\n`); process.exit(1); }); } - -function isMainEntry(): boolean { - const argv = process.argv[1]; - if (!argv) return false; - try { - return import.meta.url === pathToFileURL(realpathSync(argv)).href; - } catch { - return import.meta.url === pathToFileURL(argv).href; - } -} diff --git a/apps/worker/package.json b/apps/worker/package.json index ee6844d..5cd4ee9 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -19,6 +19,7 @@ "@colony/config": "workspace:*", "@colony/core": "workspace:*", "@colony/embedding": "workspace:*", + "@colony/process": "workspace:*", "@colony/storage": "workspace:*", "hono": "^4.6.10", "@hono/node-server": "^1.13.7" diff --git a/apps/worker/src/server.ts b/apps/worker/src/server.ts index 2b2e644..96cf790 100644 --- a/apps/worker/src/server.ts +++ b/apps/worker/src/server.ts @@ -1,11 +1,11 @@ #!/usr/bin/env node -import { existsSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs'; +import { writeFileSync } from 'node:fs'; import { join } from 'node:path'; -import { pathToFileURL } from 'node:url'; import { expand } from '@colony/compress'; import { type Settings, loadSettings, resolveDataDir } from '@colony/config'; import { type HivemindOptions, MemoryStore, readHivemind } from '@colony/core'; import { createEmbedder } from '@colony/embedding'; +import { isMainEntry, removePidFile, writePidFile } from '@colony/process'; import { serve } from '@hono/node-server'; import { Hono } from 'hono'; import { type EmbedLoopHandle, startEmbedLoop, stateFilePath } from './embed-loop.js'; @@ -85,30 +85,18 @@ function pidFilePath(settings: Settings): string { return join(resolveDataDir(settings.dataDir), 'worker.pid'); } -function writePidFile(settings: Settings): void { - writeFileSync(pidFilePath(settings), String(process.pid)); -} - -function removePidFile(settings: Settings): void { - try { - unlinkSync(pidFilePath(settings)); - } catch { - // already gone - } -} - export async function start(): Promise { const settings = loadSettings(); const dbPath = join(resolveDataDir(settings.dataDir), 'data.db'); const store = new MemoryStore({ dbPath, settings }); - writePidFile(settings); + writePidFile(pidFilePath(settings)); let loop: EmbedLoopHandle | undefined; const servers: Array> = []; const shutdown = async () => { - removePidFile(settings); + removePidFile(pidFilePath(settings)); if (loop) await loop.stop(); for (const s of servers) s.close(); store.close(); @@ -174,19 +162,9 @@ export async function start(): Promise { ); } -if (isMainEntry()) { +if (isMainEntry(import.meta.url)) { start().catch((err) => { process.stderr.write(`[colony worker] fatal: ${String(err)}\n`); process.exit(1); }); } - -function isMainEntry(): boolean { - const argv = process.argv[1]; - if (!argv) return false; - try { - return import.meta.url === pathToFileURL(realpathSync(argv)).href; - } catch { - return import.meta.url === pathToFileURL(argv).href; - } -} diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 35f8a75..e4569c4 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -22,7 +22,8 @@ }, "dependencies": { "@colony/config": "workspace:*", - "@colony/core": "workspace:*" + "@colony/core": "workspace:*", + "@colony/process": "workspace:*" }, "devDependencies": { "tsup": "^8.3.5", diff --git a/packages/hooks/src/auto-spawn.ts b/packages/hooks/src/auto-spawn.ts index d130c07..c824464 100644 --- a/packages/hooks/src/auto-spawn.ts +++ b/packages/hooks/src/auto-spawn.ts @@ -1,7 +1,6 @@ -import { spawn } from 'node:child_process'; -import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { type Settings, resolveDataDir } from '@colony/config'; +import { isAlive, readPidFile, spawnNodeScript } from '@colony/process'; /** * Ensure the worker daemon is running. Called from the hook runner after @@ -24,42 +23,19 @@ export function ensureWorkerRunning(settings: Settings): void { if (settings.embedding.provider === 'none') return; const pidFile = join(resolveDataDir(settings.dataDir), 'worker.pid'); - if (existsSync(pidFile)) { - try { - const pid = Number(readFileSync(pidFile, 'utf8')); - if (pid > 0 && isAlive(pid)) return; - } catch { - // fall through — stale or unreadable pidfile - } - } + const pid = readPidFile(pidFile); + if (pid !== null && isAlive(pid)) return; const cli = resolveCli(); if (!cli) return; try { - // Spawn `node worker start` — Windows can't exec a raw .js path - // (EFTYPE), and `cli` is the .js entry when the hook runs through the - // colony CLI. - const child = spawn(process.execPath, [cli, 'worker', 'start'], { - detached: true, - stdio: 'ignore', - env: { ...process.env }, - }); - child.unref(); + spawnNodeScript(cli, ['worker', 'start']); } catch { // Best-effort — if spawn fails, the hook still succeeds. Next hook will retry. } } -function isAlive(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - function resolveCli(): string | null { // argv[1] is the CLI binary when the hook handler runs through `colony hook`. const argv1 = process.argv[1]; diff --git a/packages/process/package.json b/packages/process/package.json new file mode 100644 index 0000000..8a92d8f --- /dev/null +++ b/packages/process/package.json @@ -0,0 +1,28 @@ +{ + "name": "@colony/process", + "version": "0.5.0", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup src/index.ts --format esm --dts --clean", + "dev": "tsup src/index.ts --format esm --dts --watch", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "tsup": "^8.3.5", + "typescript": "^5.6.3", + "vitest": "^2.1.5" + } +} diff --git a/packages/process/src/alive.ts b/packages/process/src/alive.ts new file mode 100644 index 0000000..f33340c --- /dev/null +++ b/packages/process/src/alive.ts @@ -0,0 +1,14 @@ +/** + * Best-effort liveness probe for a pid. Uses `process.kill(pid, 0)` which + * sends no signal but throws ESRCH when the pid does not exist. Returns + * false on any error so callers do not need to differentiate. + */ +export function isAlive(pid: number): boolean { + if (!Number.isFinite(pid) || pid <= 0) return false; + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} diff --git a/packages/process/src/index.ts b/packages/process/src/index.ts new file mode 100644 index 0000000..dcb9451 --- /dev/null +++ b/packages/process/src/index.ts @@ -0,0 +1,4 @@ +export { isMainEntry } from './is-main.js'; +export { isAlive } from './alive.js'; +export { readPidFile, writePidFile, removePidFile } from './pidfile.js'; +export { spawnNodeScript } from './spawn.js'; diff --git a/packages/process/src/is-main.ts b/packages/process/src/is-main.ts new file mode 100644 index 0000000..5d24dd9 --- /dev/null +++ b/packages/process/src/is-main.ts @@ -0,0 +1,21 @@ +import { realpathSync } from 'node:fs'; +import { pathToFileURL } from 'node:url'; + +/** + * Returns true when the caller's module is the entrypoint Node was invoked with. + * + * Pass the caller's `import.meta.url`. The comparison normalizes both sides + * through `pathToFileURL(realpathSync(...))` so it works when the binary is + * reached through an npm-installed symlink in `node_modules/.bin` — the + * original motivation for this helper (0.2.0 release notes: "binary works when + * invoked through npm's symlinked `bin/` shim"). + */ +export function isMainEntry(importMetaUrl: string): boolean { + const argv = process.argv[1]; + if (!argv) return false; + try { + return importMetaUrl === pathToFileURL(realpathSync(argv)).href; + } catch { + return importMetaUrl === pathToFileURL(argv).href; + } +} diff --git a/packages/process/src/pidfile.ts b/packages/process/src/pidfile.ts new file mode 100644 index 0000000..d289642 --- /dev/null +++ b/packages/process/src/pidfile.ts @@ -0,0 +1,35 @@ +import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'; + +/** + * Write the current (or caller-provided) pid to a file. Overwrites any stale + * content; callers are responsible for deciding whether it is safe to do so. + */ +export function writePidFile(path: string, pid: number = process.pid): void { + writeFileSync(path, String(pid)); +} + +/** + * Read the pid from a pidfile. Returns null when the file is missing, empty, + * or contains a non-numeric / non-positive value. Does not probe liveness — + * pair with {@link isAlive} when you need to know the pid is still valid. + */ +export function readPidFile(path: string): number | null { + if (!existsSync(path)) return null; + try { + const raw = readFileSync(path, 'utf8').trim(); + if (!raw) return null; + const pid = Number(raw); + return Number.isFinite(pid) && pid > 0 ? pid : null; + } catch { + return null; + } +} + +/** Remove a pidfile silently. No-op if it is already gone. */ +export function removePidFile(path: string): void { + try { + unlinkSync(path); + } catch { + // already gone + } +} diff --git a/packages/process/src/spawn.ts b/packages/process/src/spawn.ts new file mode 100644 index 0000000..5fc028d --- /dev/null +++ b/packages/process/src/spawn.ts @@ -0,0 +1,26 @@ +import { type SpawnOptions, spawn } from 'node:child_process'; + +/** + * Spawn a Node script with `process.execPath` so the OS does not have to + * figure out how to exec a `.js` file. Windows cannot exec `.js` directly + * (EFTYPE); macOS and Linux tolerate it only when the shebang resolves to a + * real node binary. Using `execPath` avoids both failure modes. + * + * Defaults to a fully detached child: `detached: true`, `stdio: 'ignore'`, + * and `child.unref()`. Callers can override any of those via `opts`. + */ +export function spawnNodeScript( + script: string, + args: readonly string[] = [], + opts: SpawnOptions = {}, +): ReturnType { + const merged: SpawnOptions = { + detached: true, + stdio: 'ignore', + env: process.env, + ...opts, + }; + const child = spawn(process.execPath, [script, ...args], merged); + if (merged.detached) child.unref(); + return child; +} diff --git a/packages/process/test/process.test.ts b/packages/process/test/process.test.ts new file mode 100644 index 0000000..2d2c3f5 --- /dev/null +++ b/packages/process/test/process.test.ts @@ -0,0 +1,61 @@ +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { isAlive, readPidFile, removePidFile, writePidFile } from '../src/index.js'; + +let dir: string; +let pf: string; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'colony-process-')); + pf = join(dir, 'worker.pid'); +}); + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }); +}); + +describe('pidfile', () => { + it('round-trips pid via write → read', () => { + writePidFile(pf, 4242); + expect(readFileSync(pf, 'utf8')).toBe('4242'); + expect(readPidFile(pf)).toBe(4242); + }); + + it('defaults to process.pid when pid argument omitted', () => { + writePidFile(pf); + expect(readPidFile(pf)).toBe(process.pid); + }); + + it('returns null for missing, empty, or non-numeric pidfiles', () => { + expect(readPidFile(pf)).toBeNull(); + writePidFile(pf, process.pid); + removePidFile(pf); + expect(readPidFile(pf)).toBeNull(); + }); + + it('removePidFile is idempotent', () => { + expect(() => removePidFile(pf)).not.toThrow(); + writePidFile(pf, 1); + removePidFile(pf); + expect(() => removePidFile(pf)).not.toThrow(); + }); +}); + +describe('isAlive', () => { + it('returns true for the current process', () => { + expect(isAlive(process.pid)).toBe(true); + }); + + it('rejects invalid pids', () => { + expect(isAlive(0)).toBe(false); + expect(isAlive(-1)).toBe(false); + expect(isAlive(Number.NaN)).toBe(false); + }); + + it('returns false for a plausible-but-dead pid', () => { + // 0x7fffffff is above the default pid_max on macOS + Linux + expect(isAlive(0x7fffffff)).toBe(false); + }); +}); diff --git a/packages/process/tsconfig.json b/packages/process/tsconfig.json new file mode 100644 index 0000000..1e20cd9 --- /dev/null +++ b/packages/process/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed9a1a7..8716f08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,6 +73,9 @@ importers: '@colony/mcp-server': specifier: workspace:* version: link:../mcp-server + '@colony/process': + specifier: workspace:* + version: link:../../packages/process '@colony/storage': specifier: workspace:* version: link:../../packages/storage @@ -118,6 +121,9 @@ importers: '@colony/hooks': specifier: workspace:* version: link:../../packages/hooks + '@colony/process': + specifier: workspace:* + version: link:../../packages/process '@modelcontextprotocol/sdk': specifier: ^1.0.0 version: 1.29.0(zod@3.25.76) @@ -149,6 +155,9 @@ importers: '@colony/embedding': specifier: workspace:* version: link:../../packages/embedding + '@colony/process': + specifier: workspace:* + version: link:../../packages/process '@colony/storage': specifier: workspace:* version: link:../../packages/storage @@ -263,6 +272,9 @@ importers: '@colony/core': specifier: workspace:* version: link:../core + '@colony/process': + specifier: workspace:* + version: link:../process devDependencies: tsup: specifier: ^8.3.5 @@ -290,6 +302,18 @@ importers: specifier: ^2.1.5 version: 2.1.9(@types/node@22.19.17) + packages/process: + devDependencies: + tsup: + specifier: ^8.3.5 + version: 8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.6.3 + version: 5.9.3 + vitest: + specifier: ^2.1.5 + version: 2.1.9(@types/node@22.19.17) + packages/storage: dependencies: '@colony/config':