@ai-matrx/coding-sessions 0.1.81 → 0.1.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -45,6 +45,7 @@ function installHistoryGuard() {
45
45
  return originalRm(target, options);
46
46
  });
47
47
  }
48
+ var CODEX_SKIP_GIT_REPO_CHECK = "--skip-git-repo-check";
48
49
  var CODEX_HOME_MODULE = /\/@paperclipai\/adapter-codex-local\/dist\/server\/codex-home\.js$/;
49
50
  var ORIGINAL_SIGNATURE = "export async function writeManagedCodexMcpConfig(input) {";
50
51
  var VENDORED_WRITE_MANAGED_CODEX_MCP_CONFIG = `export async function writeManagedCodexMcpConfig(input) {
@@ -128,8 +129,18 @@ async function run(request) {
128
129
  engine: "cli",
129
130
  dangerouslySkipPermissions: true,
130
131
  // Token-level streaming for the transcript.
131
- extraArgs: ["--include-partial-messages"]
132
- } : { engine: "cli" },
132
+ extraArgs: ["--include-partial-messages", ...request.agentArgs ?? []]
133
+ } : {
134
+ engine: "cli",
135
+ // `codex exec` (start AND `exec … resume`) refuses any folder that
136
+ // is not a git repo or a project trusted in ~/.codex/config.toml:
137
+ // "Not inside a trusted directory and --skip-git-repo-check was not
138
+ // specified." People start chats anywhere (~/code, scratch folders),
139
+ // and we never write trust entries into their config. The driver
140
+ // places extraArgs before `resume <id> -` and dedupes this flag
141
+ // against its own sandbox injection.
142
+ extraArgs: [CODEX_SKIP_GIT_REPO_CHECK, ...request.agentArgs ?? []]
143
+ },
133
144
  ...request.model ? { model: request.model } : {},
134
145
  ...request.effort ? request.provider === "claude" ? { effort: request.effort } : { modelReasoningEffort: request.effort } : {}
135
146
  };
@@ -286,6 +297,7 @@ if (process.env.MATRX_CODING_SESSIONS_WORKER === "1") process.on("message", (raw
286
297
  });
287
298
  });
288
299
  export {
300
+ CODEX_SKIP_GIT_REPO_CHECK,
289
301
  spliceCodexConfigGuard
290
302
  };
291
303
  //# sourceMappingURL=worker.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/engine/worker.ts"],"sourcesContent":["/**\n * THE DRIVER WORKER — one short-lived Node process per run (and per provider\n * probe), forked by the engine with a CLEAN environment.\n *\n * Why a separate process at all: Paperclip's drivers build the CLI's\n * environment as `{ ...process.env, ...theirs }`. Whatever process loads a\n * driver therefore leaks its whole environment into the user's Claude Code or\n * Codex — and the process that hosts this package is very often itself running\n * inside a Claude Code session (`CLAUDE_CODE_*`, `ANTHROPIC_BASE_URL`, …). A\n * CLI that inherits those talks to the wrong endpoint or thinks it is a\n * sub-agent. The spike ran under `env -i` for exactly this reason. Forking the\n * driver into a process whose environment the engine built from an allowlist\n * is the only place that guarantee can live, because the driver reads\n * `process.env` directly.\n *\n * 🚨 SELF-CONTAINED ON PURPOSE. This file imports nothing relative: it runs as\n * `node worker.ts` straight from source under Node's type stripping (tests and\n * workspace use) and as `node worker.js` from `dist/`. Keep it that way.\n *\n * Protocol (IPC, JSON): parent → { kind: \"run\" | \"probe\", ... };\n * worker → { kind: \"log\" | \"meta\" | \"spawn\" | \"refused\" | \"result\" | \"probe-result\" | \"fatal\" }.\n */\n\nimport { execFile } from \"node:child_process\";\nimport { registerHooks } from \"node:module\";\nimport fsp from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\ntype Provider = \"claude\" | \"codex\";\n\ninterface RunRequest {\n kind: \"run\";\n provider: Provider;\n runId: string;\n cwd: string;\n prompt: string;\n model: string | null;\n /** Reasoning effort; null leaves the provider's own default. */\n effort?: string | null;\n resumeSessionId: string | null;\n /** Absolute path of the shim that strips Paperclip's additions. */\n shimPath: string;\n /** The real CLI the shim execs. */\n realCommand: string;\n identity: { companyId: string; agentId: string };\n}\n\ninterface ProbeRequest {\n kind: \"probe\";\n commands: { claude: string | null; codex: string | null };\n /** Ask for usage windows (Codex's usage endpoint, with the user's own login). */\n quota?: boolean;\n}\n\ntype Request = RunRequest | ProbeRequest;\n\nfunction send(message: unknown): Promise<void> {\n return new Promise((resolve) => {\n if (!process.send) {\n resolve();\n return;\n }\n process.send(message, () => resolve());\n });\n}\n\n/**\n * THE HISTORY GUARD. On a \"poisoned\" resume, the Claude driver deletes the\n * user's own session file under `~/.claude/projects` and silently starts a\n * fresh session. A user's chat history is theirs; nothing we drive may delete\n * it. This process is ours alone, so the guard patches the one `fs` object the\n * drivers import and refuses — out loud — any unlink/rm inside the user's\n * Claude or Codex home.\n */\nfunction installHistoryGuard(): void {\n const home = os.homedir();\n const protectedRoots = [\n path.join(home, \".claude\"),\n path.join(home, \".codex\"),\n ];\n const isProtected = (target: unknown): boolean => {\n if (typeof target !== \"string\" && !(target instanceof URL)) return false;\n const resolved = path.resolve(typeof target === \"string\" ? target : target.pathname);\n return protectedRoots.some(\n (root) => resolved === root || resolved.startsWith(`${root}${path.sep}`),\n );\n };\n const originalUnlink = fsp.unlink.bind(fsp);\n const originalRm = fsp.rm.bind(fsp);\n const refuse = async (op: string, target: unknown): Promise<never> => {\n await send({ kind: \"refused\", op, target: String(target) });\n const error = new Error(\n `[coding-sessions] Refused to ${op} ${String(target)}: it is part of your own Claude or Codex history.`,\n ) as Error & { code: string };\n error.code = \"EPERM\";\n throw error;\n };\n (fsp as { unlink: typeof fsp.unlink }).unlink = (async (target: Parameters<typeof fsp.unlink>[0]) => {\n if (isProtected(target)) return refuse(\"delete\", target);\n return originalUnlink(target);\n }) as typeof fsp.unlink;\n (fsp as { rm: typeof fsp.rm }).rm = (async (\n target: Parameters<typeof fsp.rm>[0],\n options?: Parameters<typeof fsp.rm>[1],\n ) => {\n if (isProtected(target)) return refuse(\"remove\", target);\n return originalRm(target, options);\n }) as typeof fsp.rm;\n}\n\n/**\n * THE USER'S CODEX CONFIG IS NEVER REWRITTEN.\n *\n * On every run the Codex driver's `writeManagedCodexMcpConfig` reads\n * `$CODEX_HOME/config.toml`, strips its own managed MCP block, writes the file\n * back and chmods it 0600 — even when it has zero MCP gateways to add. With\n * CODEX_HOME pointed at the person's real `~/.codex`, that is a rewrite of\n * their own config (and a mode change) on every run.\n *\n * The fix is our own copy of that one function, spliced in when the driver's\n * `codex-home.js` loads (a module load hook in this worker process only). Our\n * copy returns without touching the file when there are no gateways — always,\n * for us — and otherwise behaves exactly as Paperclip's.\n *\n * Adapted from @paperclipai/adapter-codex-local (dist/server/codex-home.js),\n * Copyright (c) 2025 Paperclip AI, MIT License — see this package's NOTICE.\n */\nconst CODEX_HOME_MODULE = /\\/@paperclipai\\/adapter-codex-local\\/dist\\/server\\/codex-home\\.js$/;\nconst ORIGINAL_SIGNATURE = \"export async function writeManagedCodexMcpConfig(input) {\";\nconst VENDORED_WRITE_MANAGED_CODEX_MCP_CONFIG = `export async function writeManagedCodexMcpConfig(input) {\n const configPath = path.join(input.codexHome, \"config.toml\");\n // @ai-matrx/coding-sessions: with nothing to add, never touch the file.\n if (!Array.isArray(input.gateways) || input.gateways.length === 0) {\n return { configPath, warnings: [] };\n }\n await fs.mkdir(input.codexHome, { recursive: true });\n const existing = await fs.readFile(configPath, \"utf8\").catch((error) => {\n if (error.code === \"ENOENT\")\n return \"\";\n throw error;\n });\n const unmanagedConfig = stripManagedMcpBlock(existing);\n const { block, warnings } = buildManagedMcpBlock({\n gateways: input.gateways,\n apiBaseUrl: input.apiBaseUrl,\n existingNames: readCodexMcpServerNames(unmanagedConfig),\n });\n const next = \\`\\${unmanagedConfig}\\${unmanagedConfig ? \"\\\\n\\\\n\" : \"\"}\\${block}\\\\n\\`;\n await fs.writeFile(configPath, next, { mode: 0o600 });\n await fs.chmod(configPath, 0o600);\n return { configPath, warnings };\n}`;\n\n/** Replace the driver's function with ours in its module source; null when its shape changed. */\nexport function spliceCodexConfigGuard(source: string): string | null {\n const start = source.indexOf(ORIGINAL_SIGNATURE);\n if (start < 0) return null;\n // The function ends at the first line that is exactly \"}\" after its start.\n const end = source.indexOf(\"\\n}\\n\", start);\n if (end < 0) return null;\n return `${source.slice(0, start)}${VENDORED_WRITE_MANAGED_CODEX_MCP_CONFIG}${source.slice(end + 2)}`;\n}\n\nlet codexConfigGuard: \"installed\" | \"failed\" | \"off\" = \"off\";\n\nfunction installCodexConfigGuard(): void {\n registerHooks({\n load(url, context, nextLoad) {\n const result = nextLoad(url, context);\n if (!CODEX_HOME_MODULE.test(url) || result.source === undefined || result.source === null) return result;\n const patched = spliceCodexConfigGuard(String(result.source));\n if (patched === null) {\n codexConfigGuard = \"failed\";\n return result;\n }\n codexConfigGuard = \"installed\";\n return { ...result, source: patched };\n },\n });\n}\n\nasync function loadDriver(provider: Provider): Promise<{\n execute: (ctx: Record<string, unknown>) => Promise<Record<string, unknown>>;\n}> {\n const mod =\n provider === \"claude\"\n ? await import(\"@paperclipai/adapter-claude-local/server\")\n : await import(\"@paperclipai/adapter-codex-local/server\");\n return mod as unknown as {\n execute: (ctx: Record<string, unknown>) => Promise<Record<string, unknown>>;\n };\n}\n\nasync function run(request: RunRequest): Promise<void> {\n installHistoryGuard();\n if (request.provider === \"codex\") installCodexConfigGuard();\n const driver = await loadDriver(request.provider);\n if (request.provider === \"codex\" && codexConfigGuard !== \"installed\") {\n // Loud, never silent: the run continues (Paperclip's rewrite keeps the\n // content), but the person is told their config file may be rewritten.\n await send({ kind: \"refused\", op: \"config-guard\", target: codexConfigGuard });\n }\n const codexHome = path.join(os.homedir(), \".codex\");\n const resuming = request.resumeSessionId !== null;\n let refuseNextSpawn = false;\n\n const config: Record<string, unknown> = {\n cwd: request.cwd,\n // The user's text, exactly. Without this the driver sends Paperclip's\n // heartbeat instructions.\n promptTemplate: request.prompt,\n // Our shim, which drops `PAPERCLIP_*` and Paperclip's `--add-dir`, then\n // execs the real CLI named in `realCommand`.\n command: request.shimPath,\n // An explicit EMPTY skill set: no Paperclip skill is mounted into the\n // user's ~/.codex/skills, and Claude gets no appended skill-list prompt.\n paperclipRuntimeSkills: [],\n env: {\n MATRX_CODING_SESSIONS_REAL_COMMAND: request.realCommand,\n ...(request.provider === \"codex\" ? { CODEX_HOME: codexHome } : {}),\n },\n ...(request.provider === \"claude\"\n ? {\n // The ACP engine writes Paperclip settings into the user's repo and\n // prepends a skill preamble; the CLI engine does neither.\n engine: \"cli\",\n dangerouslySkipPermissions: true,\n // Token-level streaming for the transcript.\n extraArgs: [\"--include-partial-messages\"],\n }\n : { engine: \"cli\" }),\n ...(request.model ? { model: request.model } : {}),\n ...(request.effort\n ? request.provider === \"claude\"\n ? { effort: request.effort }\n : { modelReasoningEffort: request.effort }\n : {}),\n };\n\n const runtime = resuming\n ? {\n sessionId: request.resumeSessionId,\n sessionParams:\n request.provider === \"claude\"\n ? { sessionId: request.resumeSessionId, cwd: request.cwd }\n : { sessionId: request.resumeSessionId },\n sessionDisplayId: null,\n taskKey: null,\n }\n : { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null };\n\n const result = await driver.execute({\n runId: request.runId,\n agent: {\n id: request.identity.agentId,\n companyId: request.identity.companyId,\n name: \"AI Matrx coding session\",\n adapterType: `${request.provider}_local`,\n adapterConfig: {},\n },\n runtime,\n config,\n context: {},\n onLog: async (stream: \"stdout\" | \"stderr\", chunk: string) => {\n // THE SILENT-FRESH-SESSION GUARD. When a resume fails the driver quietly\n // retries in a brand-new session, which would answer a taken-over chat\n // with no memory of it. Refuse the retry and say why instead.\n if (resuming && /retrying with a fresh session/.test(chunk)) {\n refuseNextSpawn = true;\n await send({ kind: \"refused\", op: \"fresh-session-retry\", target: chunk.trim() });\n }\n await send({ kind: \"log\", stream, chunk });\n },\n onMeta: async (meta: Record<string, unknown>) => {\n await send({\n kind: \"meta\",\n command: meta.command ?? null,\n args: Array.isArray(meta.commandArgs) ? meta.commandArgs : [],\n envKeys:\n meta.env && typeof meta.env === \"object\" ? Object.keys(meta.env as object).sort() : [],\n });\n },\n onSpawn: async (meta: { pid: number; processGroupId: number | null; startedAt: string }) => {\n if (refuseNextSpawn) {\n try {\n process.kill(-(meta.processGroupId ?? meta.pid), \"SIGKILL\");\n } catch {\n try {\n process.kill(meta.pid, \"SIGKILL\");\n } catch {\n /* already gone */\n }\n }\n return;\n }\n await send({ kind: \"spawn\", pid: meta.pid, processGroupId: meta.processGroupId });\n },\n });\n\n await send({\n kind: \"result\",\n refusedFreshSession: refuseNextSpawn,\n result: {\n exitCode: result.exitCode ?? null,\n signal: result.signal ?? null,\n timedOut: result.timedOut === true,\n errorMessage: typeof result.errorMessage === \"string\" ? result.errorMessage : null,\n errorCode: typeof result.errorCode === \"string\" ? result.errorCode : null,\n sessionId: typeof result.sessionId === \"string\" ? result.sessionId : null,\n billingType: typeof result.billingType === \"string\" ? result.billingType : null,\n model: typeof result.model === \"string\" ? result.model : null,\n usage: result.usage ?? null,\n costUsd: typeof result.costUsd === \"number\" ? result.costUsd : null,\n summary: typeof result.summary === \"string\" ? result.summary.slice(0, 2000) : null,\n },\n });\n}\n\nfunction execText(command: string, args: string[], timeoutMs = 8000): Promise<string | null> {\n return new Promise((resolve) => {\n execFile(command, args, { timeout: timeoutMs, env: process.env }, (error, stdout) => {\n resolve(error ? null : String(stdout));\n });\n });\n}\n\nasync function probe(request: ProbeRequest): Promise<void> {\n // Claude: the driver's own `claude auth status` reader.\n const claudeState: Record<string, unknown> = { installed: false };\n if (request.commands.claude) {\n const version = await execText(request.commands.claude, [\"--version\"]);\n claudeState.installed = version !== null;\n claudeState.version = version?.trim().split(/\\s+/)[0] ?? null;\n if (version !== null) {\n const claude = (await import(\"@paperclipai/adapter-claude-local/server\")) as unknown as {\n readClaudeAuthStatus: () => Promise<\n { loggedIn: boolean; authMethod: string | null; subscriptionType: string | null } | null\n >;\n };\n const status = await claude.readClaudeAuthStatus();\n claudeState.auth = status;\n }\n }\n\n // Codex: the driver's own `auth.json` reader, against the user's own home.\n const codexState: Record<string, unknown> = { installed: false };\n if (request.commands.codex) {\n const version = await execText(request.commands.codex, [\"--version\"]);\n codexState.installed = version !== null;\n codexState.version = version?.trim().split(/\\s+/).pop() ?? null;\n if (version !== null) {\n const codex = (await import(\"@paperclipai/adapter-codex-local/server\")) as unknown as {\n readCodexAuthInfo: (home?: string) => Promise<\n { planType: string | null; email: string | null } | null\n >;\n };\n const home = path.join(os.homedir(), \".codex\");\n const info = await codex.readCodexAuthInfo(home);\n let apiKey = false;\n if (!info) {\n try {\n const raw = JSON.parse(await fsp.readFile(path.join(home, \"auth.json\"), \"utf8\")) as {\n OPENAI_API_KEY?: unknown;\n };\n apiKey = typeof raw.OPENAI_API_KEY === \"string\" && raw.OPENAI_API_KEY.length > 0;\n } catch {\n apiKey = false;\n }\n }\n // Usage windows from the driver's own reader. Codex only: Claude's\n // reader falls back to driving an interactive `claude /usage` session,\n // which is not something a status check should do to a person's account.\n if (request.quota && info) {\n const quotaMod = codex as unknown as {\n getQuotaWindows?: () => Promise<{ ok?: boolean; windows?: unknown[] }>;\n };\n const answer = await Promise.race([\n quotaMod.getQuotaWindows?.().catch(() => null) ?? Promise.resolve(null),\n new Promise<null>((resolve) => setTimeout(() => resolve(null), 10000)),\n ]);\n codexState.quota = Array.isArray(answer?.windows)\n ? answer.windows.map((w) => {\n const win = (w ?? {}) as Record<string, unknown>;\n return {\n label: typeof win.label === \"string\" ? win.label : \"Usage\",\n usedPercent: typeof win.usedPercent === \"number\" ? win.usedPercent : null,\n resetsAt: typeof win.resetsAt === \"string\" ? win.resetsAt : null,\n valueLabel: typeof win.valueLabel === \"string\" ? win.valueLabel : null,\n };\n })\n : [];\n }\n // Never forward a token: only the plan and account label leave this process.\n codexState.auth = info\n ? { loggedIn: true, planType: info.planType ?? null, email: info.email ?? null }\n : { loggedIn: apiKey, planType: null, email: null, apiKey };\n }\n }\n // The model lists the drivers ship (constants only; nothing is run).\n for (const [state, spec] of [\n [claudeState, \"@paperclipai/adapter-claude-local\"],\n [codexState, \"@paperclipai/adapter-codex-local\"],\n ] as const) {\n try {\n const mod = (await import(spec)) as { models?: unknown; DEFAULT_CLAUDE_LOCAL_MODEL?: unknown };\n // The model the Claude driver forces when none is passed.\n if (typeof mod.DEFAULT_CLAUDE_LOCAL_MODEL === \"string\") state.driverDefaultModel = mod.DEFAULT_CLAUDE_LOCAL_MODEL;\n state.models = Array.isArray(mod.models)\n ? mod.models.flatMap((m) => {\n const entry = (m ?? {}) as Record<string, unknown>;\n return typeof entry.id === \"string\" && entry.id\n ? [{ id: entry.id, label: typeof entry.label === \"string\" ? entry.label : entry.id }]\n : [];\n })\n : [];\n } catch {\n state.models = [];\n }\n }\n await send({ kind: \"probe-result\", claude: claudeState, codex: codexState });\n}\n\n// Only when forked by the engine: importing this file (a test reading\n// `spliceCodexConfigGuard`) must not turn the importer into a worker.\nif (process.env.MATRX_CODING_SESSIONS_WORKER === \"1\") process.on(\"message\", (raw) => {\n const request = raw as Request;\n const work = request.kind === \"run\" ? run(request) : probe(request);\n work\n .catch(async (error: unknown) => {\n await send({\n kind: \"fatal\",\n message: error instanceof Error ? error.message : String(error),\n });\n })\n .finally(() => {\n process.disconnect?.();\n process.exit(0);\n });\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/engine/worker.ts"],"sourcesContent":["/**\n * THE DRIVER WORKER — one short-lived Node process per run (and per provider\n * probe), forked by the engine with a CLEAN environment.\n *\n * Why a separate process at all: Paperclip's drivers build the CLI's\n * environment as `{ ...process.env, ...theirs }`. Whatever process loads a\n * driver therefore leaks its whole environment into the user's Claude Code or\n * Codex — and the process that hosts this package is very often itself running\n * inside a Claude Code session (`CLAUDE_CODE_*`, `ANTHROPIC_BASE_URL`, …). A\n * CLI that inherits those talks to the wrong endpoint or thinks it is a\n * sub-agent. The spike ran under `env -i` for exactly this reason. Forking the\n * driver into a process whose environment the engine built from an allowlist\n * is the only place that guarantee can live, because the driver reads\n * `process.env` directly.\n *\n * 🚨 SELF-CONTAINED ON PURPOSE. This file imports nothing relative: it runs as\n * `node worker.ts` straight from source under Node's type stripping (tests and\n * workspace use) and as `node worker.js` from `dist/`. Keep it that way.\n *\n * Protocol (IPC, JSON): parent → { kind: \"run\" | \"probe\", ... };\n * worker → { kind: \"log\" | \"meta\" | \"spawn\" | \"refused\" | \"result\" | \"probe-result\" | \"fatal\" }.\n */\n\nimport { execFile } from \"node:child_process\";\nimport { registerHooks } from \"node:module\";\nimport fsp from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\ntype Provider = \"claude\" | \"codex\";\n\ninterface RunRequest {\n kind: \"run\";\n provider: Provider;\n runId: string;\n cwd: string;\n prompt: string;\n model: string | null;\n /** Reasoning effort; null leaves the provider's own default. */\n effort?: string | null;\n resumeSessionId: string | null;\n /** Absolute path of the shim that strips Paperclip's additions. */\n shimPath: string;\n /** The real CLI the shim execs. */\n realCommand: string;\n identity: { companyId: string; agentId: string };\n /** CLI args that load an AI Matrx agent (agent-attach.ts); appended to extraArgs. */\n agentArgs?: string[];\n}\n\ninterface ProbeRequest {\n kind: \"probe\";\n commands: { claude: string | null; codex: string | null };\n /** Ask for usage windows (Codex's usage endpoint, with the user's own login). */\n quota?: boolean;\n}\n\ntype Request = RunRequest | ProbeRequest;\n\nfunction send(message: unknown): Promise<void> {\n return new Promise((resolve) => {\n if (!process.send) {\n resolve();\n return;\n }\n process.send(message, () => resolve());\n });\n}\n\n/**\n * THE HISTORY GUARD. On a \"poisoned\" resume, the Claude driver deletes the\n * user's own session file under `~/.claude/projects` and silently starts a\n * fresh session. A user's chat history is theirs; nothing we drive may delete\n * it. This process is ours alone, so the guard patches the one `fs` object the\n * drivers import and refuses — out loud — any unlink/rm inside the user's\n * Claude or Codex home.\n */\nfunction installHistoryGuard(): void {\n const home = os.homedir();\n const protectedRoots = [\n path.join(home, \".claude\"),\n path.join(home, \".codex\"),\n ];\n const isProtected = (target: unknown): boolean => {\n if (typeof target !== \"string\" && !(target instanceof URL)) return false;\n const resolved = path.resolve(typeof target === \"string\" ? target : target.pathname);\n return protectedRoots.some(\n (root) => resolved === root || resolved.startsWith(`${root}${path.sep}`),\n );\n };\n const originalUnlink = fsp.unlink.bind(fsp);\n const originalRm = fsp.rm.bind(fsp);\n const refuse = async (op: string, target: unknown): Promise<never> => {\n await send({ kind: \"refused\", op, target: String(target) });\n const error = new Error(\n `[coding-sessions] Refused to ${op} ${String(target)}: it is part of your own Claude or Codex history.`,\n ) as Error & { code: string };\n error.code = \"EPERM\";\n throw error;\n };\n (fsp as { unlink: typeof fsp.unlink }).unlink = (async (target: Parameters<typeof fsp.unlink>[0]) => {\n if (isProtected(target)) return refuse(\"delete\", target);\n return originalUnlink(target);\n }) as typeof fsp.unlink;\n (fsp as { rm: typeof fsp.rm }).rm = (async (\n target: Parameters<typeof fsp.rm>[0],\n options?: Parameters<typeof fsp.rm>[1],\n ) => {\n if (isProtected(target)) return refuse(\"remove\", target);\n return originalRm(target, options);\n }) as typeof fsp.rm;\n}\n\n/**\n * THE USER'S CODEX CONFIG IS NEVER REWRITTEN.\n *\n * On every run the Codex driver's `writeManagedCodexMcpConfig` reads\n * `$CODEX_HOME/config.toml`, strips its own managed MCP block, writes the file\n * back and chmods it 0600 — even when it has zero MCP gateways to add. With\n * CODEX_HOME pointed at the person's real `~/.codex`, that is a rewrite of\n * their own config (and a mode change) on every run.\n *\n * The fix is our own copy of that one function, spliced in when the driver's\n * `codex-home.js` loads (a module load hook in this worker process only). Our\n * copy returns without touching the file when there are no gateways — always,\n * for us — and otherwise behaves exactly as Paperclip's.\n *\n * Adapted from @paperclipai/adapter-codex-local (dist/server/codex-home.js),\n * Copyright (c) 2025 Paperclip AI, MIT License — see this package's NOTICE.\n */\n/** Lets every Codex run (start, follow-up, takeover) work in any folder. */\nexport const CODEX_SKIP_GIT_REPO_CHECK = \"--skip-git-repo-check\";\n\nconst CODEX_HOME_MODULE = /\\/@paperclipai\\/adapter-codex-local\\/dist\\/server\\/codex-home\\.js$/;\nconst ORIGINAL_SIGNATURE = \"export async function writeManagedCodexMcpConfig(input) {\";\nconst VENDORED_WRITE_MANAGED_CODEX_MCP_CONFIG = `export async function writeManagedCodexMcpConfig(input) {\n const configPath = path.join(input.codexHome, \"config.toml\");\n // @ai-matrx/coding-sessions: with nothing to add, never touch the file.\n if (!Array.isArray(input.gateways) || input.gateways.length === 0) {\n return { configPath, warnings: [] };\n }\n await fs.mkdir(input.codexHome, { recursive: true });\n const existing = await fs.readFile(configPath, \"utf8\").catch((error) => {\n if (error.code === \"ENOENT\")\n return \"\";\n throw error;\n });\n const unmanagedConfig = stripManagedMcpBlock(existing);\n const { block, warnings } = buildManagedMcpBlock({\n gateways: input.gateways,\n apiBaseUrl: input.apiBaseUrl,\n existingNames: readCodexMcpServerNames(unmanagedConfig),\n });\n const next = \\`\\${unmanagedConfig}\\${unmanagedConfig ? \"\\\\n\\\\n\" : \"\"}\\${block}\\\\n\\`;\n await fs.writeFile(configPath, next, { mode: 0o600 });\n await fs.chmod(configPath, 0o600);\n return { configPath, warnings };\n}`;\n\n/** Replace the driver's function with ours in its module source; null when its shape changed. */\nexport function spliceCodexConfigGuard(source: string): string | null {\n const start = source.indexOf(ORIGINAL_SIGNATURE);\n if (start < 0) return null;\n // The function ends at the first line that is exactly \"}\" after its start.\n const end = source.indexOf(\"\\n}\\n\", start);\n if (end < 0) return null;\n return `${source.slice(0, start)}${VENDORED_WRITE_MANAGED_CODEX_MCP_CONFIG}${source.slice(end + 2)}`;\n}\n\nlet codexConfigGuard: \"installed\" | \"failed\" | \"off\" = \"off\";\n\nfunction installCodexConfigGuard(): void {\n registerHooks({\n load(url, context, nextLoad) {\n const result = nextLoad(url, context);\n if (!CODEX_HOME_MODULE.test(url) || result.source === undefined || result.source === null) return result;\n const patched = spliceCodexConfigGuard(String(result.source));\n if (patched === null) {\n codexConfigGuard = \"failed\";\n return result;\n }\n codexConfigGuard = \"installed\";\n return { ...result, source: patched };\n },\n });\n}\n\nasync function loadDriver(provider: Provider): Promise<{\n execute: (ctx: Record<string, unknown>) => Promise<Record<string, unknown>>;\n}> {\n const mod =\n provider === \"claude\"\n ? await import(\"@paperclipai/adapter-claude-local/server\")\n : await import(\"@paperclipai/adapter-codex-local/server\");\n return mod as unknown as {\n execute: (ctx: Record<string, unknown>) => Promise<Record<string, unknown>>;\n };\n}\n\nasync function run(request: RunRequest): Promise<void> {\n installHistoryGuard();\n if (request.provider === \"codex\") installCodexConfigGuard();\n const driver = await loadDriver(request.provider);\n if (request.provider === \"codex\" && codexConfigGuard !== \"installed\") {\n // Loud, never silent: the run continues (Paperclip's rewrite keeps the\n // content), but the person is told their config file may be rewritten.\n await send({ kind: \"refused\", op: \"config-guard\", target: codexConfigGuard });\n }\n const codexHome = path.join(os.homedir(), \".codex\");\n const resuming = request.resumeSessionId !== null;\n let refuseNextSpawn = false;\n\n const config: Record<string, unknown> = {\n cwd: request.cwd,\n // The user's text, exactly. Without this the driver sends Paperclip's\n // heartbeat instructions.\n promptTemplate: request.prompt,\n // Our shim, which drops `PAPERCLIP_*` and Paperclip's `--add-dir`, then\n // execs the real CLI named in `realCommand`.\n command: request.shimPath,\n // An explicit EMPTY skill set: no Paperclip skill is mounted into the\n // user's ~/.codex/skills, and Claude gets no appended skill-list prompt.\n paperclipRuntimeSkills: [],\n env: {\n MATRX_CODING_SESSIONS_REAL_COMMAND: request.realCommand,\n ...(request.provider === \"codex\" ? { CODEX_HOME: codexHome } : {}),\n },\n ...(request.provider === \"claude\"\n ? {\n // The ACP engine writes Paperclip settings into the user's repo and\n // prepends a skill preamble; the CLI engine does neither.\n engine: \"cli\",\n dangerouslySkipPermissions: true,\n // Token-level streaming for the transcript.\n extraArgs: [\"--include-partial-messages\", ...(request.agentArgs ?? [])],\n }\n : {\n engine: \"cli\",\n // `codex exec` (start AND `exec … resume`) refuses any folder that\n // is not a git repo or a project trusted in ~/.codex/config.toml:\n // \"Not inside a trusted directory and --skip-git-repo-check was not\n // specified.\" People start chats anywhere (~/code, scratch folders),\n // and we never write trust entries into their config. The driver\n // places extraArgs before `resume <id> -` and dedupes this flag\n // against its own sandbox injection.\n extraArgs: [CODEX_SKIP_GIT_REPO_CHECK, ...(request.agentArgs ?? [])],\n }),\n ...(request.model ? { model: request.model } : {}),\n ...(request.effort\n ? request.provider === \"claude\"\n ? { effort: request.effort }\n : { modelReasoningEffort: request.effort }\n : {}),\n };\n\n const runtime = resuming\n ? {\n sessionId: request.resumeSessionId,\n sessionParams:\n request.provider === \"claude\"\n ? { sessionId: request.resumeSessionId, cwd: request.cwd }\n : { sessionId: request.resumeSessionId },\n sessionDisplayId: null,\n taskKey: null,\n }\n : { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null };\n\n const result = await driver.execute({\n runId: request.runId,\n agent: {\n id: request.identity.agentId,\n companyId: request.identity.companyId,\n name: \"AI Matrx coding session\",\n adapterType: `${request.provider}_local`,\n adapterConfig: {},\n },\n runtime,\n config,\n context: {},\n onLog: async (stream: \"stdout\" | \"stderr\", chunk: string) => {\n // THE SILENT-FRESH-SESSION GUARD. When a resume fails the driver quietly\n // retries in a brand-new session, which would answer a taken-over chat\n // with no memory of it. Refuse the retry and say why instead.\n if (resuming && /retrying with a fresh session/.test(chunk)) {\n refuseNextSpawn = true;\n await send({ kind: \"refused\", op: \"fresh-session-retry\", target: chunk.trim() });\n }\n await send({ kind: \"log\", stream, chunk });\n },\n onMeta: async (meta: Record<string, unknown>) => {\n await send({\n kind: \"meta\",\n command: meta.command ?? null,\n args: Array.isArray(meta.commandArgs) ? meta.commandArgs : [],\n envKeys:\n meta.env && typeof meta.env === \"object\" ? Object.keys(meta.env as object).sort() : [],\n });\n },\n onSpawn: async (meta: { pid: number; processGroupId: number | null; startedAt: string }) => {\n if (refuseNextSpawn) {\n try {\n process.kill(-(meta.processGroupId ?? meta.pid), \"SIGKILL\");\n } catch {\n try {\n process.kill(meta.pid, \"SIGKILL\");\n } catch {\n /* already gone */\n }\n }\n return;\n }\n await send({ kind: \"spawn\", pid: meta.pid, processGroupId: meta.processGroupId });\n },\n });\n\n await send({\n kind: \"result\",\n refusedFreshSession: refuseNextSpawn,\n result: {\n exitCode: result.exitCode ?? null,\n signal: result.signal ?? null,\n timedOut: result.timedOut === true,\n errorMessage: typeof result.errorMessage === \"string\" ? result.errorMessage : null,\n errorCode: typeof result.errorCode === \"string\" ? result.errorCode : null,\n sessionId: typeof result.sessionId === \"string\" ? result.sessionId : null,\n billingType: typeof result.billingType === \"string\" ? result.billingType : null,\n model: typeof result.model === \"string\" ? result.model : null,\n usage: result.usage ?? null,\n costUsd: typeof result.costUsd === \"number\" ? result.costUsd : null,\n summary: typeof result.summary === \"string\" ? result.summary.slice(0, 2000) : null,\n },\n });\n}\n\nfunction execText(command: string, args: string[], timeoutMs = 8000): Promise<string | null> {\n return new Promise((resolve) => {\n execFile(command, args, { timeout: timeoutMs, env: process.env }, (error, stdout) => {\n resolve(error ? null : String(stdout));\n });\n });\n}\n\nasync function probe(request: ProbeRequest): Promise<void> {\n // Claude: the driver's own `claude auth status` reader.\n const claudeState: Record<string, unknown> = { installed: false };\n if (request.commands.claude) {\n const version = await execText(request.commands.claude, [\"--version\"]);\n claudeState.installed = version !== null;\n claudeState.version = version?.trim().split(/\\s+/)[0] ?? null;\n if (version !== null) {\n const claude = (await import(\"@paperclipai/adapter-claude-local/server\")) as unknown as {\n readClaudeAuthStatus: () => Promise<\n { loggedIn: boolean; authMethod: string | null; subscriptionType: string | null } | null\n >;\n };\n const status = await claude.readClaudeAuthStatus();\n claudeState.auth = status;\n }\n }\n\n // Codex: the driver's own `auth.json` reader, against the user's own home.\n const codexState: Record<string, unknown> = { installed: false };\n if (request.commands.codex) {\n const version = await execText(request.commands.codex, [\"--version\"]);\n codexState.installed = version !== null;\n codexState.version = version?.trim().split(/\\s+/).pop() ?? null;\n if (version !== null) {\n const codex = (await import(\"@paperclipai/adapter-codex-local/server\")) as unknown as {\n readCodexAuthInfo: (home?: string) => Promise<\n { planType: string | null; email: string | null } | null\n >;\n };\n const home = path.join(os.homedir(), \".codex\");\n const info = await codex.readCodexAuthInfo(home);\n let apiKey = false;\n if (!info) {\n try {\n const raw = JSON.parse(await fsp.readFile(path.join(home, \"auth.json\"), \"utf8\")) as {\n OPENAI_API_KEY?: unknown;\n };\n apiKey = typeof raw.OPENAI_API_KEY === \"string\" && raw.OPENAI_API_KEY.length > 0;\n } catch {\n apiKey = false;\n }\n }\n // Usage windows from the driver's own reader. Codex only: Claude's\n // reader falls back to driving an interactive `claude /usage` session,\n // which is not something a status check should do to a person's account.\n if (request.quota && info) {\n const quotaMod = codex as unknown as {\n getQuotaWindows?: () => Promise<{ ok?: boolean; windows?: unknown[] }>;\n };\n const answer = await Promise.race([\n quotaMod.getQuotaWindows?.().catch(() => null) ?? Promise.resolve(null),\n new Promise<null>((resolve) => setTimeout(() => resolve(null), 10000)),\n ]);\n codexState.quota = Array.isArray(answer?.windows)\n ? answer.windows.map((w) => {\n const win = (w ?? {}) as Record<string, unknown>;\n return {\n label: typeof win.label === \"string\" ? win.label : \"Usage\",\n usedPercent: typeof win.usedPercent === \"number\" ? win.usedPercent : null,\n resetsAt: typeof win.resetsAt === \"string\" ? win.resetsAt : null,\n valueLabel: typeof win.valueLabel === \"string\" ? win.valueLabel : null,\n };\n })\n : [];\n }\n // Never forward a token: only the plan and account label leave this process.\n codexState.auth = info\n ? { loggedIn: true, planType: info.planType ?? null, email: info.email ?? null }\n : { loggedIn: apiKey, planType: null, email: null, apiKey };\n }\n }\n // The model lists the drivers ship (constants only; nothing is run).\n for (const [state, spec] of [\n [claudeState, \"@paperclipai/adapter-claude-local\"],\n [codexState, \"@paperclipai/adapter-codex-local\"],\n ] as const) {\n try {\n const mod = (await import(spec)) as { models?: unknown; DEFAULT_CLAUDE_LOCAL_MODEL?: unknown };\n // The model the Claude driver forces when none is passed.\n if (typeof mod.DEFAULT_CLAUDE_LOCAL_MODEL === \"string\") state.driverDefaultModel = mod.DEFAULT_CLAUDE_LOCAL_MODEL;\n state.models = Array.isArray(mod.models)\n ? mod.models.flatMap((m) => {\n const entry = (m ?? {}) as Record<string, unknown>;\n return typeof entry.id === \"string\" && entry.id\n ? [{ id: entry.id, label: typeof entry.label === \"string\" ? entry.label : entry.id }]\n : [];\n })\n : [];\n } catch {\n state.models = [];\n }\n }\n await send({ kind: \"probe-result\", claude: claudeState, codex: codexState });\n}\n\n// Only when forked by the engine: importing this file (a test reading\n// `spliceCodexConfigGuard`) must not turn the importer into a worker.\nif (process.env.MATRX_CODING_SESSIONS_WORKER === \"1\") process.on(\"message\", (raw) => {\n const request = raw as Request;\n const work = request.kind === \"run\" ? run(request) : probe(request);\n work\n .catch(async (error: unknown) => {\n await send({\n kind: \"fatal\",\n message: error instanceof Error ? error.message : String(error),\n });\n })\n .finally(() => {\n process.disconnect?.();\n process.exit(0);\n });\n});\n"],"mappings":"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","names":[]}
@@ -1,13 +1,34 @@
1
+ import { JsonValue } from '@ai-matrx/agents/portable';
2
+ import { CredentialsPort } from '@ai-matrx/data';
3
+ import { McpHttpReply } from '@ai-matrx/agents/portable/mcp';
4
+
1
5
  /**
2
6
  * The public contract of `@ai-matrx/coding-sessions`, shared by the engine
3
7
  * (Node), the local host (Node) and the UI (browser). Pure types — nothing here
4
8
  * touches Node or the DOM, and every value is JSON-serializable because it
5
9
  * crosses a transport (WebSocket today, desktop IPC later).
6
10
  */
11
+
7
12
  /** The coding agents this package drives. "coding session" is the canonical
8
13
  * platform term (chat.coding_session, the Coding Session Bridge). */
9
14
  type CodingProvider = "claude" | "codex";
10
15
  type Billing = "subscription" | "api" | "unknown";
16
+ /** Where the CLI a provider runs came from. */
17
+ type BinarySource = "desktop-app" | "path" | "npm-global" | "configured";
18
+ /** The exact CLI runs use: the newest copy on this computer (or the host's pick). */
19
+ interface ProviderBinary {
20
+ path: string;
21
+ version: string | null;
22
+ source: BinarySource;
23
+ /** "from ChatGPT app", "from PATH", … */
24
+ sourceLabel: string;
25
+ /** A newer copy than the one in use, when one exists. */
26
+ newer: {
27
+ path: string;
28
+ version: string;
29
+ sourceLabel: string;
30
+ } | null;
31
+ }
11
32
  /** What `providers()` answers for one provider, in plain terms. */
12
33
  interface ProviderStatus {
13
34
  provider: CodingProvider;
@@ -45,6 +66,8 @@ interface ProviderStatus {
45
66
  } | null | undefined;
46
67
  /** The effort a run uses when none is picked (the person's own settings). */
47
68
  defaultEffort?: string | null | undefined;
69
+ /** Which CLI runs use, and its version. */
70
+ binary?: ProviderBinary | null | undefined;
48
71
  }
49
72
  interface ModelOption {
50
73
  id: string;
@@ -171,6 +194,8 @@ interface CodingSession {
171
194
  lastRunStatus: RunStatus | null;
172
195
  /** Prompts waiting for the active run to finish, oldest first. */
173
196
  queued: string[];
197
+ /** The AI Matrx agent loaded into every run of this session, when one was picked. */
198
+ agent?: SessionAgent | null | undefined;
174
199
  }
175
200
  /**
176
201
  * One normalized transcript item — the same shape from Claude and Codex, from
@@ -251,6 +276,82 @@ interface StartInput {
251
276
  prompt: string;
252
277
  model?: string | null | undefined;
253
278
  effort?: string | null | undefined;
279
+ /** An AI Matrx agent to load into the CLI. Needs the host's `agents` credentials. */
280
+ agent?: AgentSelection | null | undefined;
281
+ }
282
+ /** Which AI Matrx agent a new session loads, and the person's variable values. */
283
+ interface AgentSelection {
284
+ agentId: string;
285
+ /** Pin one saved version; omitted = the latest, pinned at start. */
286
+ versionId?: string | null | undefined;
287
+ variables?: Record<string, JsonValue> | undefined;
288
+ }
289
+ /** One part of an agent and whether this session's CLI carries it. */
290
+ interface SessionAgentPart {
291
+ part: "instructions" | "skills" | "tools";
292
+ loaded: number;
293
+ total: number;
294
+ }
295
+ /**
296
+ * The agent a session runs with. Recorded at start and never drifts: every
297
+ * later turn reloads the SAME bundle (same version, same hash).
298
+ */
299
+ interface SessionAgent {
300
+ agentId: string;
301
+ versionId: string | null;
302
+ versionNumber: number | null;
303
+ name: string;
304
+ definitionHash: string;
305
+ loaded: SessionAgentPart[];
306
+ /** What could not be carried, with the reason. */
307
+ unavailable: {
308
+ kind: string;
309
+ name: string;
310
+ reason: string;
311
+ }[];
312
+ }
313
+ /** Whether this host can reach AI Matrx as a signed-in person. */
314
+ interface AgentAccess {
315
+ signedIn: boolean;
316
+ userId: string | null;
317
+ organizationId: string | null;
318
+ /** True when the host was started with no AI Matrx connection at all. */
319
+ configured: boolean;
320
+ }
321
+ /** A catalogue read the picker asks the host to make (PostgREST RPC shape). */
322
+ interface AgentCatalogRpcRequest {
323
+ fn: string;
324
+ args?: Record<string, unknown> | undefined;
325
+ count?: "exact" | "planned" | "estimated" | undefined;
326
+ range?: [number, number] | undefined;
327
+ order?: {
328
+ column: string;
329
+ ascending?: boolean | undefined;
330
+ } | undefined;
331
+ }
332
+ interface AgentCatalogRpcResult {
333
+ data: unknown;
334
+ error: {
335
+ message: string;
336
+ code?: string | undefined;
337
+ details?: string | null | undefined;
338
+ hint?: string | null | undefined;
339
+ } | null;
340
+ count?: number | null;
341
+ }
342
+ /** What the New session form needs about one agent. */
343
+ interface AgentDetails {
344
+ agentId: string;
345
+ name: string;
346
+ /** Variable declarations as authored (camelCase). */
347
+ variables: Record<string, JsonValue>[];
348
+ versions: {
349
+ id: string;
350
+ versionNumber: number;
351
+ changedAt: string | null;
352
+ }[];
353
+ /** The version "Latest" resolves to right now. */
354
+ latestVersionNumber: number | null;
254
355
  }
255
356
  interface TakeoverInput {
256
357
  provider: CodingProvider;
@@ -327,6 +428,12 @@ interface CodingSessionsApi {
327
428
  model?: string | null;
328
429
  effort?: string | null;
329
430
  }): Promise<CodingSession>;
431
+ /** Is this host signed in to AI Matrx (so agents can load)? */
432
+ agentAccess(): Promise<AgentAccess>;
433
+ /** One allowlisted agent-catalogue read, made by the host as the signed-in person. */
434
+ agentCatalogRpc(request: AgentCatalogRpcRequest): Promise<AgentCatalogRpcResult>;
435
+ /** An agent's variables and saved versions, for the New session form. */
436
+ agentDetails(agentId: string): Promise<AgentDetails>;
330
437
  }
331
438
  type RawTranscript = {
332
439
  available: true;
@@ -395,6 +502,36 @@ declare function createAiMatrxRawBackupSink(options: {
395
502
  fetch?: typeof fetch;
396
503
  }): RawBackupSink;
397
504
 
505
+ /**
506
+ * THE AI MATRX CONNECTION — what a host needs to load the person's agents into
507
+ * their coding sessions: who they are (a `CredentialsPort`, read fresh on every
508
+ * call), which organization the work runs in, the agent picker's catalogue
509
+ * reads, the portable bundle, and one MCP endpoint per session for the agent's
510
+ * server tools.
511
+ *
512
+ * The token lives only in this process. The browser page asks the host to make
513
+ * each catalogue read (an allowlist of RPCs); the CLI only ever sees a
514
+ * 127.0.0.1 URL carrying a per-session random secret.
515
+ *
516
+ * A host with no credentials (the standalone launchd page today) is a STATE,
517
+ * not an error: `access()` says signed out, the page shows one line, and plain
518
+ * sessions work exactly as before.
519
+ */
520
+
521
+ /** The host option that connects coding sessions to AI Matrx. */
522
+ interface CodingSessionsAgentsOptions {
523
+ /** The person's AI Matrx sign-in. Absent or `get() → null` = signed out. */
524
+ credentials?: CredentialsPort | null | undefined;
525
+ /** The organization agent work runs in (value or getter, read per call). */
526
+ organizationId?: string | null | (() => string | null | undefined) | undefined;
527
+ /** AI Matrx server. Default `https://server.app.matrxserver.com`. */
528
+ serverUrl?: string | undefined;
529
+ /** The platform database URL. Default `https://db.matrxserver.com`. */
530
+ supabaseUrl?: string | undefined;
531
+ /** The database's publishable key — needed for the agent picker's reads. */
532
+ supabaseKey?: string | undefined;
533
+ }
534
+
398
535
  /**
399
536
  * Reading the providers' OWN files — cheaply, and never copying them.
400
537
  *
@@ -604,12 +741,23 @@ interface CodingSessionsEngineOptions {
604
741
  rawBackup?: RawBackupSink | undefined;
605
742
  /** Receives raw provider stdout per run — used to capture test fixtures. */
606
743
  onRawOutput?: ((runId: string, provider: CodingProvider, chunk: string) => void) | undefined;
744
+ /**
745
+ * The AI Matrx connection that loads the person's agents into sessions:
746
+ * `credentials` (a `CredentialsPort`, read fresh per call) plus the
747
+ * organization. Absent → the agent picker shows signed out; plain sessions
748
+ * are unaffected.
749
+ */
750
+ agents?: CodingSessionsAgentsOptions | undefined;
607
751
  }
608
752
  interface CodingSessionsEngine extends CodingSessionsApi {
609
753
  subscribe(listener: (event: CodingEvent) => void): () => void;
610
754
  /** Stop every live run and wait for the index to be written. */
611
755
  dispose(): Promise<void>;
612
756
  readonly dataDir: string;
757
+ /** The host serves agent tools at `<baseUrl>/mcp/<secret>`; null stops serving. */
758
+ setMcpBaseUrl(baseUrl: string | null): void;
759
+ /** One MCP POST for a session's agent tools; null = no session owns `secret`. */
760
+ handleMcp(secret: string, body: unknown): Promise<McpHttpReply | null>;
613
761
  }
614
762
  /**
615
763
  * The status the screen shows: what the provider's own auth check says, then
@@ -1,13 +1,34 @@
1
+ import { JsonValue } from '@ai-matrx/agents/portable';
2
+ import { CredentialsPort } from '@ai-matrx/data';
3
+ import { McpHttpReply } from '@ai-matrx/agents/portable/mcp';
4
+
1
5
  /**
2
6
  * The public contract of `@ai-matrx/coding-sessions`, shared by the engine
3
7
  * (Node), the local host (Node) and the UI (browser). Pure types — nothing here
4
8
  * touches Node or the DOM, and every value is JSON-serializable because it
5
9
  * crosses a transport (WebSocket today, desktop IPC later).
6
10
  */
11
+
7
12
  /** The coding agents this package drives. "coding session" is the canonical
8
13
  * platform term (chat.coding_session, the Coding Session Bridge). */
9
14
  type CodingProvider = "claude" | "codex";
10
15
  type Billing = "subscription" | "api" | "unknown";
16
+ /** Where the CLI a provider runs came from. */
17
+ type BinarySource = "desktop-app" | "path" | "npm-global" | "configured";
18
+ /** The exact CLI runs use: the newest copy on this computer (or the host's pick). */
19
+ interface ProviderBinary {
20
+ path: string;
21
+ version: string | null;
22
+ source: BinarySource;
23
+ /** "from ChatGPT app", "from PATH", … */
24
+ sourceLabel: string;
25
+ /** A newer copy than the one in use, when one exists. */
26
+ newer: {
27
+ path: string;
28
+ version: string;
29
+ sourceLabel: string;
30
+ } | null;
31
+ }
11
32
  /** What `providers()` answers for one provider, in plain terms. */
12
33
  interface ProviderStatus {
13
34
  provider: CodingProvider;
@@ -45,6 +66,8 @@ interface ProviderStatus {
45
66
  } | null | undefined;
46
67
  /** The effort a run uses when none is picked (the person's own settings). */
47
68
  defaultEffort?: string | null | undefined;
69
+ /** Which CLI runs use, and its version. */
70
+ binary?: ProviderBinary | null | undefined;
48
71
  }
49
72
  interface ModelOption {
50
73
  id: string;
@@ -171,6 +194,8 @@ interface CodingSession {
171
194
  lastRunStatus: RunStatus | null;
172
195
  /** Prompts waiting for the active run to finish, oldest first. */
173
196
  queued: string[];
197
+ /** The AI Matrx agent loaded into every run of this session, when one was picked. */
198
+ agent?: SessionAgent | null | undefined;
174
199
  }
175
200
  /**
176
201
  * One normalized transcript item — the same shape from Claude and Codex, from
@@ -251,6 +276,82 @@ interface StartInput {
251
276
  prompt: string;
252
277
  model?: string | null | undefined;
253
278
  effort?: string | null | undefined;
279
+ /** An AI Matrx agent to load into the CLI. Needs the host's `agents` credentials. */
280
+ agent?: AgentSelection | null | undefined;
281
+ }
282
+ /** Which AI Matrx agent a new session loads, and the person's variable values. */
283
+ interface AgentSelection {
284
+ agentId: string;
285
+ /** Pin one saved version; omitted = the latest, pinned at start. */
286
+ versionId?: string | null | undefined;
287
+ variables?: Record<string, JsonValue> | undefined;
288
+ }
289
+ /** One part of an agent and whether this session's CLI carries it. */
290
+ interface SessionAgentPart {
291
+ part: "instructions" | "skills" | "tools";
292
+ loaded: number;
293
+ total: number;
294
+ }
295
+ /**
296
+ * The agent a session runs with. Recorded at start and never drifts: every
297
+ * later turn reloads the SAME bundle (same version, same hash).
298
+ */
299
+ interface SessionAgent {
300
+ agentId: string;
301
+ versionId: string | null;
302
+ versionNumber: number | null;
303
+ name: string;
304
+ definitionHash: string;
305
+ loaded: SessionAgentPart[];
306
+ /** What could not be carried, with the reason. */
307
+ unavailable: {
308
+ kind: string;
309
+ name: string;
310
+ reason: string;
311
+ }[];
312
+ }
313
+ /** Whether this host can reach AI Matrx as a signed-in person. */
314
+ interface AgentAccess {
315
+ signedIn: boolean;
316
+ userId: string | null;
317
+ organizationId: string | null;
318
+ /** True when the host was started with no AI Matrx connection at all. */
319
+ configured: boolean;
320
+ }
321
+ /** A catalogue read the picker asks the host to make (PostgREST RPC shape). */
322
+ interface AgentCatalogRpcRequest {
323
+ fn: string;
324
+ args?: Record<string, unknown> | undefined;
325
+ count?: "exact" | "planned" | "estimated" | undefined;
326
+ range?: [number, number] | undefined;
327
+ order?: {
328
+ column: string;
329
+ ascending?: boolean | undefined;
330
+ } | undefined;
331
+ }
332
+ interface AgentCatalogRpcResult {
333
+ data: unknown;
334
+ error: {
335
+ message: string;
336
+ code?: string | undefined;
337
+ details?: string | null | undefined;
338
+ hint?: string | null | undefined;
339
+ } | null;
340
+ count?: number | null;
341
+ }
342
+ /** What the New session form needs about one agent. */
343
+ interface AgentDetails {
344
+ agentId: string;
345
+ name: string;
346
+ /** Variable declarations as authored (camelCase). */
347
+ variables: Record<string, JsonValue>[];
348
+ versions: {
349
+ id: string;
350
+ versionNumber: number;
351
+ changedAt: string | null;
352
+ }[];
353
+ /** The version "Latest" resolves to right now. */
354
+ latestVersionNumber: number | null;
254
355
  }
255
356
  interface TakeoverInput {
256
357
  provider: CodingProvider;
@@ -327,6 +428,12 @@ interface CodingSessionsApi {
327
428
  model?: string | null;
328
429
  effort?: string | null;
329
430
  }): Promise<CodingSession>;
431
+ /** Is this host signed in to AI Matrx (so agents can load)? */
432
+ agentAccess(): Promise<AgentAccess>;
433
+ /** One allowlisted agent-catalogue read, made by the host as the signed-in person. */
434
+ agentCatalogRpc(request: AgentCatalogRpcRequest): Promise<AgentCatalogRpcResult>;
435
+ /** An agent's variables and saved versions, for the New session form. */
436
+ agentDetails(agentId: string): Promise<AgentDetails>;
330
437
  }
331
438
  type RawTranscript = {
332
439
  available: true;
@@ -395,6 +502,36 @@ declare function createAiMatrxRawBackupSink(options: {
395
502
  fetch?: typeof fetch;
396
503
  }): RawBackupSink;
397
504
 
505
+ /**
506
+ * THE AI MATRX CONNECTION — what a host needs to load the person's agents into
507
+ * their coding sessions: who they are (a `CredentialsPort`, read fresh on every
508
+ * call), which organization the work runs in, the agent picker's catalogue
509
+ * reads, the portable bundle, and one MCP endpoint per session for the agent's
510
+ * server tools.
511
+ *
512
+ * The token lives only in this process. The browser page asks the host to make
513
+ * each catalogue read (an allowlist of RPCs); the CLI only ever sees a
514
+ * 127.0.0.1 URL carrying a per-session random secret.
515
+ *
516
+ * A host with no credentials (the standalone launchd page today) is a STATE,
517
+ * not an error: `access()` says signed out, the page shows one line, and plain
518
+ * sessions work exactly as before.
519
+ */
520
+
521
+ /** The host option that connects coding sessions to AI Matrx. */
522
+ interface CodingSessionsAgentsOptions {
523
+ /** The person's AI Matrx sign-in. Absent or `get() → null` = signed out. */
524
+ credentials?: CredentialsPort | null | undefined;
525
+ /** The organization agent work runs in (value or getter, read per call). */
526
+ organizationId?: string | null | (() => string | null | undefined) | undefined;
527
+ /** AI Matrx server. Default `https://server.app.matrxserver.com`. */
528
+ serverUrl?: string | undefined;
529
+ /** The platform database URL. Default `https://db.matrxserver.com`. */
530
+ supabaseUrl?: string | undefined;
531
+ /** The database's publishable key — needed for the agent picker's reads. */
532
+ supabaseKey?: string | undefined;
533
+ }
534
+
398
535
  /**
399
536
  * Reading the providers' OWN files — cheaply, and never copying them.
400
537
  *
@@ -604,12 +741,23 @@ interface CodingSessionsEngineOptions {
604
741
  rawBackup?: RawBackupSink | undefined;
605
742
  /** Receives raw provider stdout per run — used to capture test fixtures. */
606
743
  onRawOutput?: ((runId: string, provider: CodingProvider, chunk: string) => void) | undefined;
744
+ /**
745
+ * The AI Matrx connection that loads the person's agents into sessions:
746
+ * `credentials` (a `CredentialsPort`, read fresh per call) plus the
747
+ * organization. Absent → the agent picker shows signed out; plain sessions
748
+ * are unaffected.
749
+ */
750
+ agents?: CodingSessionsAgentsOptions | undefined;
607
751
  }
608
752
  interface CodingSessionsEngine extends CodingSessionsApi {
609
753
  subscribe(listener: (event: CodingEvent) => void): () => void;
610
754
  /** Stop every live run and wait for the index to be written. */
611
755
  dispose(): Promise<void>;
612
756
  readonly dataDir: string;
757
+ /** The host serves agent tools at `<baseUrl>/mcp/<secret>`; null stops serving. */
758
+ setMcpBaseUrl(baseUrl: string | null): void;
759
+ /** One MCP POST for a session's agent tools; null = no session owns `secret`. */
760
+ handleMcp(secret: string, body: unknown): Promise<McpHttpReply | null>;
613
761
  }
614
762
  /**
615
763
  * The status the screen shows: what the provider's own auth check says, then
package/dist/index.cjs CHANGED
@@ -633,6 +633,23 @@ function parseJsonl(text) {
633
633
  // src/transcript.ts
634
634
  var import_chat = require("@ai-matrx/chat");
635
635
 
636
+ // src/provider-health.ts
637
+ var MODEL_UNAVAILABLE = [
638
+ /model is not supported when using codex/i,
639
+ /\bmodel[\s:'"`][^.\n]{0,80}\b(?:not supported|does not exist|not available|not found)\b/i,
640
+ /\bmodel_not_found\b/i,
641
+ /issue with the selected model/i,
642
+ /may not exist or you may not have access to it/i
643
+ ];
644
+ var MODEL_UNAVAILABLE_LABEL = "Model not available on this account";
645
+ function isModelUnavailable(text) {
646
+ const trimmed = (text ?? "").trim();
647
+ return trimmed !== "" && MODEL_UNAVAILABLE.some((re) => re.test(trimmed));
648
+ }
649
+ function runErrorLabel(text) {
650
+ return isModelUnavailable(text) ? MODEL_UNAVAILABLE_LABEL : text;
651
+ }
652
+
636
653
  // src/tool-labels.ts
637
654
  var BUILTIN = {
638
655
  Bash: "Run command",
@@ -887,14 +904,15 @@ function createTranscriptBuilder() {
887
904
  }));
888
905
  return;
889
906
  case "error": {
890
- const said = current?.message.parts.some((p) => p.type === "error" && p.message.trim() === item.text.trim());
907
+ const text = runErrorLabel(item.text);
908
+ const said = current?.message.parts.some((p) => p.type === "error" && p.message.trim() === text.trim());
891
909
  if (said) return;
892
910
  upsertPart(item.id, () => ({
893
911
  type: "error",
894
912
  errorType: "provider",
895
913
  code: null,
896
- message: item.text,
897
- userMessage: item.text,
914
+ message: text,
915
+ userMessage: text,
898
916
  recoverable: true,
899
917
  remedy: null
900
918
  }));