codeam-cli 2.73.10 → 2.74.1

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.js +323 -61
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to `codeam-cli` are documented here.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.74.0] — 2026-09-03
8
+
9
+ ### Added
10
+
11
+ - **cli:** Tool router (4 schemas instead of hundreds) + house agent's real context window (#700)
12
+
13
+ ### Tests
14
+
15
+ - **cli:** Two of today's tests were not Windows-portable — main went red (#699)
16
+
17
+ ## [2.73.10] — 2026-09-03
18
+
19
+ ### Fixed
20
+
21
+ - **cli:** Re-resolve the integrations manifest from the backend at session start (#697)
22
+
7
23
  ## [2.73.9] — 2026-09-03
8
24
 
9
25
  ### Tests
package/dist/index.js CHANGED
@@ -271,8 +271,8 @@ var init_stdio_proxy = __esm({
271
271
  }
272
272
  async spawnChild(stdout, preResolved) {
273
273
  const spec = preResolved ?? await this.opts.spawnSpec();
274
- const spawn47 = this.opts.spawnImpl ?? import_node_child_process33.spawn;
275
- const child = spawn47(spec.command, spec.args, {
274
+ const spawn48 = this.opts.spawnImpl ?? import_node_child_process33.spawn;
275
+ const child = spawn48(spec.command, spec.args, {
276
276
  env: { ...process.env, ...spec.env },
277
277
  // env only — never argv
278
278
  stdio: ["pipe", "pipe", "inherit"]
@@ -8136,7 +8136,7 @@ function readAnonId() {
8136
8136
  }
8137
8137
  function superProperties() {
8138
8138
  return {
8139
- cliVersion: true ? "2.73.10" : "0.0.0-dev",
8139
+ cliVersion: true ? "2.74.1" : "0.0.0-dev",
8140
8140
  nodeVersion: process.version,
8141
8141
  platform: process.platform,
8142
8142
  arch: process.arch,
@@ -8378,7 +8378,7 @@ var http = __toESM(require("http"));
8378
8378
  // package.json
8379
8379
  var package_default = {
8380
8380
  name: "codeam-cli",
8381
- version: "2.73.10",
8381
+ version: "2.74.1",
8382
8382
  description: "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device \u2014 async. The terminal companion for CodeAgent Mobile.",
8383
8383
  type: "commonjs",
8384
8384
  main: "dist/index.js",
@@ -9971,7 +9971,7 @@ var CommandRelayService = class _CommandRelayService {
9971
9971
  // fresh + clear the "CLI update available" banner after a self-update
9972
9972
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
9973
9973
  // pair/reconnect). Older backends ignore the extra field.
9974
- ..."2.73.10" ? { ideVersion: "2.73.10" } : {}
9974
+ ..."2.74.1" ? { ideVersion: "2.74.1" } : {}
9975
9975
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
9976
9976
  }
9977
9977
  /**
@@ -22037,11 +22037,35 @@ function readHouseProxyChildEnv() {
22037
22037
  return {};
22038
22038
  }
22039
22039
  }
22040
+ var HOUSE_MODEL_CONTEXT_TOKENS = "512000";
22040
22041
  function buildHouseProxyChildEnv(cfg) {
22041
22042
  const env = {
22042
22043
  ANTHROPIC_BASE_URL: cfg.baseUrl,
22043
22044
  ANTHROPIC_AUTH_TOKEN: cfg.token,
22044
- CLAUDE_CODE_AUTO_COMPACT_WINDOW: "512000",
22045
+ // ⚠️ MAX_CONTEXT_TOKENS, not (only) AUTO_COMPACT_WINDOW — and this is
22046
+ // HOUSE-AGENT ONLY: it lives in this builder precisely so a user's own
22047
+ // Claude Code (a real Anthropic model claude knows) is never touched.
22048
+ //
22049
+ // claude does not know `MiniMax-M3`, so it falls back to its
22050
+ // unknown-model default (200k). `CLAUDE_CODE_AUTO_COMPACT_WINDOW` is then
22051
+ // applied as `Math.min(assumedWindow, env)` — it can only SHRINK the
22052
+ // window, never grow it — so the 512000 we used to set here was inert:
22053
+ // claude kept believing 200k. Meanwhile a session with a dozen linked
22054
+ // integrations carries ~175k tokens of MCP tool schemas on EVERY turn
22055
+ // (measured: 475 tools ≈ 698 KB on a real box), so the context started
22056
+ // ~88% full and a 28-character prompt triggered a compaction, then the
22057
+ // next small tool result triggered another: "Autocompact is thrashing:
22058
+ // the context refilled to the limit within 3 turns", 13 compactions in
22059
+ // one session (rafaelph90.br@gmail.com, 2026-09-03).
22060
+ //
22061
+ // claude's own unknown-model notice names the fix: "set
22062
+ // CLAUDE_CODE_MAX_CONTEXT_TOKENS to its real window" — that variable is
22063
+ // what `wL()` returns for an unrecognised model. MiniMax-M3 is a 1M-context
22064
+ // model with a guaranteed minimum of 512K, so 512000 here is the
22065
+ // conservative truth, not a wish. AUTO_COMPACT_WINDOW stays as the
22066
+ // compaction threshold beneath it.
22067
+ CLAUDE_CODE_MAX_CONTEXT_TOKENS: HOUSE_MODEL_CONTEXT_TOKENS,
22068
+ CLAUDE_CODE_AUTO_COMPACT_WINDOW: HOUSE_MODEL_CONTEXT_TOKENS,
22045
22069
  API_TIMEOUT_MS: "3000000"
22046
22070
  };
22047
22071
  if (cfg.openRouter === true) {
@@ -22066,6 +22090,7 @@ var HOUSE_PROXY_ENV_KEYS = [
22066
22090
  "ANTHROPIC_DEFAULT_OPUS_MODEL",
22067
22091
  "ANTHROPIC_DEFAULT_HAIKU_MODEL",
22068
22092
  "CLAUDE_CODE_AUTO_COMPACT_WINDOW",
22093
+ "CLAUDE_CODE_MAX_CONTEXT_TOKENS",
22069
22094
  "API_TIMEOUT_MS",
22070
22095
  "CLAUDE_CONFIG_DIR"
22071
22096
  ];
@@ -22247,7 +22272,7 @@ async function autoUpgradeBeforeCriticalCommand() {
22247
22272
  if (process.env.NODE_ENV === "test") return;
22248
22273
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
22249
22274
  if (process.env.CI) return;
22250
- const current2 = true ? "2.73.10" : null;
22275
+ const current2 = true ? "2.74.1" : null;
22251
22276
  if (!current2) return;
22252
22277
  const cache = readCache();
22253
22278
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -22264,7 +22289,7 @@ function checkForUpdates() {
22264
22289
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
22265
22290
  if (process.env.CI) return;
22266
22291
  if (!process.stdout.isTTY) return;
22267
- const current2 = true ? "2.73.10" : null;
22292
+ const current2 = true ? "2.74.1" : null;
22268
22293
  if (!current2) return;
22269
22294
  const cache = readCache();
22270
22295
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -22287,7 +22312,7 @@ var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
22287
22312
  var SELF_UPDATE_LS_TIMEOUT_MS = 15e3;
22288
22313
  var SUDO_PREFLIGHT_TIMEOUT_MS = 5e3;
22289
22314
  function currentCliVersion() {
22290
- return true ? "2.73.10" : null;
22315
+ return true ? "2.74.1" : null;
22291
22316
  }
22292
22317
  async function installedVersion(deps) {
22293
22318
  const ls = await deps.run(
@@ -23464,20 +23489,7 @@ var HostAgentSupervisor = class {
23464
23489
  if (payload.houseProxy) {
23465
23490
  const { baseUrl, token, agentKind, openRouter } = payload.houseProxy;
23466
23491
  childEnv = {
23467
- ANTHROPIC_BASE_URL: baseUrl,
23468
- ANTHROPIC_AUTH_TOKEN: token,
23469
- // House pins MiniMax; OpenRouter routes the real Claude model names the
23470
- // agent sends, so it sets NO model overrides — but it DOES need
23471
- // ANTHROPIC_API_KEY empty so a stale key can't override the Bearer auth
23472
- // token (OpenRouter's Claude Code setup requires ANTHROPIC_API_KEY="").
23473
- ...openRouter ? { ANTHROPIC_API_KEY: "" } : {
23474
- ANTHROPIC_MODEL: "MiniMax-M3",
23475
- ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M3",
23476
- ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M3",
23477
- ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M3"
23478
- },
23479
- CLAUDE_CODE_AUTO_COMPACT_WINDOW: "512000",
23480
- API_TIMEOUT_MS: "3000000",
23492
+ ...buildHouseProxyChildEnv({ baseUrl, token, openRouter: openRouter === true }),
23481
23493
  CODEAM_AUTO_TOKEN: payload.autoPairToken
23482
23494
  };
23483
23495
  const houseConfigDir = path52.join(
@@ -36832,12 +36844,12 @@ function buildRelaunchProxyEnv(baseEnv) {
36832
36844
  return env;
36833
36845
  }
36834
36846
  var relaunchProxyWithoutBudget = async () => {
36835
- const { spawn: spawn47 } = await import("child_process");
36847
+ const { spawn: spawn48 } = await import("child_process");
36836
36848
  killHeadroomProxy();
36837
36849
  await new Promise((r) => setTimeout(r, 500));
36838
36850
  const proxyEnv = buildRelaunchProxyEnv(process.env);
36839
36851
  try {
36840
- const proxy = spawn47(
36852
+ const proxy = spawn48(
36841
36853
  "headroom",
36842
36854
  ["proxy", "--port", "8787"],
36843
36855
  { stdio: "ignore", detached: true, env: proxyEnv }
@@ -39580,6 +39592,172 @@ var packStatusH = async (ctx) => {
39580
39592
  await ctx.relay.sendResult(ctx.cmd.id, "completed", { state });
39581
39593
  };
39582
39594
 
39595
+ // src/integrations/mcp-router.ts
39596
+ var MCP_ROUTER_SERVER_NAME = "codeam";
39597
+ var CALL_TIMEOUT_MS = (() => {
39598
+ const raw = Number(process.env.CODEAM_MCP_TOOL_TIMEOUT_MS);
39599
+ return Number.isFinite(raw) && raw > 0 ? raw : 12e4;
39600
+ })();
39601
+ var CHILD_START_TIMEOUT_MS = 9e4;
39602
+ function qualifiedName(integration, tool) {
39603
+ return `${integration}/${tool}`;
39604
+ }
39605
+ function splitQualifiedName(q2) {
39606
+ const i = q2.indexOf("/");
39607
+ if (i <= 0 || i === q2.length - 1) return null;
39608
+ return { integration: q2.slice(0, i), tool: q2.slice(i + 1) };
39609
+ }
39610
+ function searchTools(tools, query, integration) {
39611
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
39612
+ return tools.filter((t2) => !integration || t2.integration === integration).filter((t2) => {
39613
+ if (words.length === 0) return true;
39614
+ const hay = `${t2.integration} ${t2.name} ${t2.description}`.toLowerCase();
39615
+ return words.every((w3) => hay.includes(w3));
39616
+ }).sort((a, b) => {
39617
+ const an = words.some((w3) => a.name.toLowerCase().includes(w3)) ? 0 : 1;
39618
+ const bn = words.some((w3) => b.name.toLowerCase().includes(w3)) ? 0 : 1;
39619
+ return an - bn || a.integration.localeCompare(b.integration) || a.name.localeCompare(b.name);
39620
+ });
39621
+ }
39622
+ function routerToolDefinitions() {
39623
+ return [
39624
+ {
39625
+ name: "list_integrations",
39626
+ description: "List the integrations attached to this session (Jira, Linear, ClickUp, Postman, \u2026) with how many tools each exposes and whether it is available. Call this first when the user asks what you can do with an external service.",
39627
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
39628
+ },
39629
+ {
39630
+ name: "search_tools",
39631
+ description: 'Search the tools of the attached integrations by keywords (e.g. "create task", "list boards", "send email"). Returns each match as `<integration>/<tool>` with a one-line description. Use `describe_tool` for the full argument schema, then `call_tool` to run it.',
39632
+ inputSchema: {
39633
+ type: "object",
39634
+ properties: {
39635
+ query: { type: "string", description: "Keywords to match against tool names and descriptions." },
39636
+ integration: { type: "string", description: 'Optional: restrict to one integration id, e.g. "clickup".' },
39637
+ limit: { type: "integer", minimum: 1, maximum: 50, default: 20 }
39638
+ },
39639
+ required: ["query"],
39640
+ additionalProperties: false
39641
+ }
39642
+ },
39643
+ {
39644
+ name: "describe_tool",
39645
+ description: "Return the full JSON input schema of one tool, given its `<integration>/<tool>` name from `search_tools`.",
39646
+ inputSchema: {
39647
+ type: "object",
39648
+ properties: { tool: { type: "string", description: '`<integration>/<tool>`, e.g. "clickup/create_task".' } },
39649
+ required: ["tool"],
39650
+ additionalProperties: false
39651
+ }
39652
+ },
39653
+ {
39654
+ name: "call_tool",
39655
+ description: "Invoke one integration tool by its `<integration>/<tool>` name with the arguments its schema expects. The result is returned verbatim from the integration.",
39656
+ inputSchema: {
39657
+ type: "object",
39658
+ properties: {
39659
+ tool: { type: "string", description: "`<integration>/<tool>`." },
39660
+ arguments: { type: "object", description: "Arguments matching the schema from `describe_tool`.", additionalProperties: true }
39661
+ },
39662
+ required: ["tool"],
39663
+ additionalProperties: false
39664
+ }
39665
+ }
39666
+ ];
39667
+ }
39668
+ async function startChild(id, env, sdk) {
39669
+ const state = { id, client: null, tools: [], error: null };
39670
+ const timer = new Promise((_2, rej) => {
39671
+ const t2 = setTimeout(
39672
+ () => rej(new Error(`timed out after ${CHILD_START_TIMEOUT_MS / 1e3}s starting`)),
39673
+ CHILD_START_TIMEOUT_MS
39674
+ );
39675
+ t2.unref?.();
39676
+ });
39677
+ try {
39678
+ const transport = new sdk.StdioClientTransport({
39679
+ command: process.execPath,
39680
+ args: [process.argv[1], "mcp-run", id],
39681
+ env: Object.fromEntries(
39682
+ Object.entries(env).filter((e) => typeof e[1] === "string")
39683
+ ),
39684
+ stderr: "inherit"
39685
+ });
39686
+ const client3 = new sdk.Client({ name: "codeam-router", version: "1.0.0" }, { capabilities: {} });
39687
+ await Promise.race([client3.connect(transport), timer]);
39688
+ const listed = await Promise.race([client3.listTools(), timer]);
39689
+ state.client = client3;
39690
+ state.tools = listed.tools.map((t2) => ({
39691
+ integration: id,
39692
+ name: t2.name,
39693
+ description: t2.description ?? "",
39694
+ inputSchema: t2.inputSchema
39695
+ }));
39696
+ log.info("mcpRouter", `${id}: ${state.tools.length} tool(s) ready`);
39697
+ } catch (err) {
39698
+ state.error = err instanceof Error ? err.message : String(err);
39699
+ log.warn("mcpRouter", `${id}: unavailable \u2014 ${state.error}`);
39700
+ }
39701
+ return state;
39702
+ }
39703
+ async function mcpRouter() {
39704
+ const manifest = readIntegrationsManifest();
39705
+ const ids = (manifest?.integrations ?? []).filter((e) => e.delivery?.mcp).map((e) => e.id);
39706
+ const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
39707
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
39708
+ const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
39709
+ const { Client: ClientCtor } = await import("@modelcontextprotocol/sdk/client/index.js");
39710
+ const { StdioClientTransport: TransportCtor } = await import("@modelcontextprotocol/sdk/client/stdio.js");
39711
+ const sdk = { Client: ClientCtor, StdioClientTransport: TransportCtor };
39712
+ const children = await Promise.all(ids.map((id) => startChild(id, process.env, sdk)));
39713
+ const byId = new Map(children.map((c2) => [c2.id, c2]));
39714
+ const allTools = children.flatMap((c2) => c2.tools);
39715
+ log.info("mcpRouter", `fronting ${children.filter((c2) => c2.client).length}/${ids.length} integration server(s), ${allTools.length} tools behind 4`);
39716
+ const server = new Server({ name: MCP_ROUTER_SERVER_NAME, version: "1.0.0" }, { capabilities: { tools: {} } });
39717
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: routerToolDefinitions() }));
39718
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
39719
+ const name = req.params.name;
39720
+ const args2 = req.params.arguments ?? {};
39721
+ const text = (v, isError2 = false) => ({ content: [{ type: "text", text: typeof v === "string" ? v : JSON.stringify(v, null, 2) }], isError: isError2 });
39722
+ if (name === "list_integrations") {
39723
+ return text(children.map((c2) => ({ integration: c2.id, available: c2.client !== null, tools: c2.tools.length, ...c2.error ? { error: c2.error } : {} })));
39724
+ }
39725
+ if (name === "search_tools") {
39726
+ const q2 = typeof args2.query === "string" ? args2.query : "";
39727
+ const integration = typeof args2.integration === "string" ? args2.integration : void 0;
39728
+ const limit = typeof args2.limit === "number" ? Math.max(1, Math.min(50, args2.limit)) : 20;
39729
+ const hits = searchTools(allTools, q2, integration).slice(0, limit);
39730
+ return text(hits.map((t2) => ({ tool: qualifiedName(t2.integration, t2.name), description: t2.description })));
39731
+ }
39732
+ if (name === "describe_tool" || name === "call_tool") {
39733
+ const q2 = typeof args2.tool === "string" ? args2.tool : "";
39734
+ const parts = splitQualifiedName(q2);
39735
+ if (!parts) return text(`Expected "<integration>/<tool>", got "${q2}". Use search_tools to find the name.`, true);
39736
+ const child = byId.get(parts.integration);
39737
+ if (!child) return text(`No integration "${parts.integration}" is attached to this session. Use list_integrations.`, true);
39738
+ if (!child.client) return text(`Integration "${parts.integration}" is unavailable: ${child.error ?? "failed to start"}.`, true);
39739
+ const tool = child.tools.find((t2) => t2.name === parts.tool);
39740
+ if (!tool) return text(`No tool "${parts.tool}" in "${parts.integration}". Use search_tools.`, true);
39741
+ if (name === "describe_tool") return text({ tool: q2, description: tool.description, inputSchema: tool.inputSchema });
39742
+ const timer = new Promise((_2, rej) => setTimeout(() => rej(new Error(`"${q2}" did not answer within ${CALL_TIMEOUT_MS / 1e3}s`)), CALL_TIMEOUT_MS).unref?.());
39743
+ try {
39744
+ const res = await Promise.race([
39745
+ child.client.callTool({ name: parts.tool, arguments: args2.arguments ?? {} }),
39746
+ timer
39747
+ ]);
39748
+ return { content: res.content ?? [], isError: res.isError === true };
39749
+ } catch (err) {
39750
+ return text(`${q2} failed: ${err instanceof Error ? err.message : String(err)}`, true);
39751
+ }
39752
+ }
39753
+ return text(`Unknown tool "${name}".`, true);
39754
+ });
39755
+ await server.connect(new StdioServerTransport());
39756
+ process.stdin.on("close", () => {
39757
+ for (const c2 of children) void c2.client?.close().catch(() => void 0);
39758
+ });
39759
+ }
39760
+
39583
39761
  // src/integrations/provision.ts
39584
39762
  function buildMcpServersForStart(ctx) {
39585
39763
  const manifest = readIntegrationsManifest();
@@ -39608,6 +39786,30 @@ function buildMcpServersForStart(ctx) {
39608
39786
  env
39609
39787
  });
39610
39788
  }
39789
+ if (manifest.toolRouter === true && servers.length > 0) {
39790
+ const routed = servers.map((s) => s.name);
39791
+ log.info(
39792
+ "integrations",
39793
+ `tool router ON \u2014 fronting ${routed.length} MCP server(s) behind one: ${routed.join(", ")}`
39794
+ );
39795
+ return [
39796
+ {
39797
+ name: MCP_ROUTER_SERVER_NAME,
39798
+ command: process.execPath,
39799
+ args: [process.argv[1], "mcp-router"],
39800
+ // The router re-spawns each shim with THIS env: the same session /
39801
+ // plugin id / token / secret the shims got when injected directly.
39802
+ // No per-integration id — each child receives its own via
39803
+ // `mcp-run <id>` argv, which the shim prefers.
39804
+ env: [
39805
+ { name: "CODEAM_MCP_SESSION_ID", value: ctx.sessionId },
39806
+ { name: "CODEAM_MCP_PLUGIN_ID", value: ctx.pluginId },
39807
+ { name: "CODEAM_MCP_PLUGIN_TOKEN", value: ctx.pluginAuthToken },
39808
+ ...ctx.pollSecret ? [{ name: "CODEAM_MCP_POLL_SECRET", value: ctx.pollSecret }] : []
39809
+ ]
39810
+ }
39811
+ ];
39812
+ }
39611
39813
  if (servers.length) {
39612
39814
  log.info(
39613
39815
  "integrations",
@@ -43281,7 +43483,11 @@ async function refreshIntegrationsManifest(ctx, fetchImpl = fetch, timeoutMs = 8
43281
43483
  const resp = await fetchImpl(url2, {
43282
43484
  method: "POST",
43283
43485
  headers: { "Content-Type": "application/json", "X-Plugin-Auth-Token": ctx.pluginAuthToken },
43284
- body: JSON.stringify({ sessionId: ctx.sessionId, pluginId: ctx.pluginId }),
43486
+ body: JSON.stringify({
43487
+ sessionId: ctx.sessionId,
43488
+ pluginId: ctx.pluginId,
43489
+ ...ctx.agent ? { agent: ctx.agent } : {}
43490
+ }),
43285
43491
  signal: ac.signal
43286
43492
  });
43287
43493
  if (!resp.ok) {
@@ -44376,7 +44582,11 @@ async function start(requestedAgent, presetSession) {
44376
44582
  await refreshIntegrationsManifest({
44377
44583
  sessionId: session.id,
44378
44584
  pluginId,
44379
- pluginAuthToken: session.pluginAuthToken
44585
+ pluginAuthToken: session.pluginAuthToken,
44586
+ // The house agent runs the `claude` runtime, so `session.agent` cannot
44587
+ // tell it apart; the managed-proxy env can (`isHouseProxyEnv`). The
44588
+ // backend turns the tool router on for the house agent ONLY.
44589
+ agent: isHouseProxyEnv(process.env) ? HOUSE_AGENT_ID : session.agent
44380
44590
  });
44381
44591
  }
44382
44592
  const mcpServers = mergeWithLocalMcpServers(
@@ -47227,7 +47437,7 @@ function checkChokidar() {
47227
47437
  }
47228
47438
  async function doctor(args2 = []) {
47229
47439
  const json = args2.includes("--json");
47230
- const cliVersion = true ? "2.73.10" : "0.0.0-dev";
47440
+ const cliVersion = true ? "2.74.1" : "0.0.0-dev";
47231
47441
  const apiBase2 = resolveApiBaseUrl();
47232
47442
  const diagnosticId = (0, import_node_crypto13.randomUUID)();
47233
47443
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -47425,6 +47635,15 @@ async function completion(args2) {
47425
47635
 
47426
47636
  // src/integrations/mcp-run.ts
47427
47637
  var import_node_child_process34 = require("child_process");
47638
+
47639
+ // src/integrations/launcher-env.ts
47640
+ var LAUNCHER_ENV = Object.freeze({
47641
+ npm_config_audit: "false",
47642
+ npm_config_fund: "false",
47643
+ npm_config_update_notifier: "false"
47644
+ });
47645
+
47646
+ // src/integrations/mcp-run.ts
47428
47647
  var import_node_fs14 = require("fs");
47429
47648
  var import_node_os14 = __toESM(require("os"));
47430
47649
  var import_node_path13 = __toESM(require("path"));
@@ -47554,6 +47773,14 @@ function ensureCommand(command2) {
47554
47773
  );
47555
47774
  }
47556
47775
  }
47776
+ function childEnvFor(delivery, token) {
47777
+ const env = { ...LAUNCHER_ENV, ...delivery.staticEnv };
47778
+ for (const [envVar, field] of Object.entries(delivery.envMapping)) {
47779
+ const value = token[field];
47780
+ if (typeof value === "string" && value) env[envVar] = value;
47781
+ }
47782
+ return env;
47783
+ }
47557
47784
  async function mcpRun(args2) {
47558
47785
  const id = args2[0] ?? process.env.CODEAM_MCP_INTEGRATION_ID;
47559
47786
  const sessionId = process.env.CODEAM_MCP_SESSION_ID;
@@ -47603,12 +47830,7 @@ async function mcpRun(args2) {
47603
47830
  const proxy = new RestartableStdioProxy({
47604
47831
  spawnSpec: async () => {
47605
47832
  current2 = await client3.getToken(id);
47606
- const env = { ...delivery.staticEnv };
47607
- for (const [envVar, field] of Object.entries(delivery.envMapping)) {
47608
- const value = current2[field];
47609
- if (typeof value === "string" && value) env[envVar] = value;
47610
- }
47611
- return { command: launcher, args: delivery.args, env };
47833
+ return { command: launcher, args: delivery.args, env: childEnvFor(delivery, current2) };
47612
47834
  },
47613
47835
  shouldRestartNow: () => current2 !== null && new Date(current2.expiresAt).getTime() - Date.now() < RESTART_AHEAD_MS
47614
47836
  });
@@ -47655,37 +47877,72 @@ function warmCommand(target) {
47655
47877
  }
47656
47878
  return null;
47657
47879
  }
47658
- async function mcpWarm(argv = []) {
47659
- const timeoutMs = 3e5;
47880
+ var WARM_PACKAGE_TIMEOUT_MS = 12e4;
47881
+ var WARM_TOTAL_BUDGET_MS = 10 * 6e4;
47882
+ var WARM_CONCURRENCY = 4;
47883
+ function runCapturing(command2, args2, timeoutMs) {
47884
+ return new Promise((resolve11) => {
47885
+ const child = (0, import_node_child_process35.spawn)(command2, args2, {
47886
+ env: { ...process.env, ...LAUNCHER_ENV },
47887
+ stdio: ["ignore", "ignore", "pipe"]
47888
+ });
47889
+ let stderr = "";
47890
+ child.stderr?.setEncoding("utf8");
47891
+ child.stderr?.on("data", (c2) => stderr += c2);
47892
+ const timer = setTimeout(() => {
47893
+ child.kill("SIGKILL");
47894
+ resolve11({ status: null, stderr, error: new Error(`spawn ${command2} ETIMEDOUT`) });
47895
+ }, timeoutMs);
47896
+ child.on("error", (error) => {
47897
+ clearTimeout(timer);
47898
+ resolve11({ status: null, stderr, error });
47899
+ });
47900
+ child.on("close", (status2) => {
47901
+ clearTimeout(timer);
47902
+ resolve11({ status: status2, stderr });
47903
+ });
47904
+ });
47905
+ }
47906
+ async function mcpWarm(argv = [], opts = {}) {
47907
+ const packageTimeoutMs = opts.packageTimeoutMs ?? WARM_PACKAGE_TIMEOUT_MS;
47908
+ const totalBudgetMs = opts.totalBudgetMs ?? WARM_TOTAL_BUDGET_MS;
47909
+ const concurrency = opts.concurrency ?? WARM_CONCURRENCY;
47660
47910
  const targets = warmTargets();
47661
47911
  const only = argv.filter((a) => !a.startsWith("-"));
47662
47912
  const selected = only.length > 0 ? targets.filter((t2) => only.includes(t2.id)) : targets;
47663
47913
  const result = { warmed: [], skipped: [], failed: [] };
47664
47914
  console.log(`[codeam mcp-warm] ${selected.length} MCP package(s) to pre-fetch`);
47665
- for (const target of selected) {
47666
- const cmd = warmCommand(target);
47667
- if (!cmd) {
47668
- result.skipped.push(`${target.spec} (no warm recipe for '${target.launcher}')`);
47669
- continue;
47670
- }
47671
- const started = Date.now();
47672
- const run = (0, import_node_child_process35.spawnSync)(cmd.command, cmd.args, {
47673
- stdio: ["ignore", "ignore", "pipe"],
47674
- timeout: timeoutMs,
47675
- encoding: "utf8"
47676
- });
47677
- const secs = ((Date.now() - started) / 1e3).toFixed(1);
47678
- if (run.status === 0) {
47679
- result.warmed.push(target.spec);
47680
- console.log(` OK ${target.spec} (${secs}s)`);
47681
- } else {
47682
- const noise = /^npm (warn|notice)\b/;
47683
- const lines = (run.stderr ?? "").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !noise.test(l));
47684
- const reason = run.error?.message ?? lines[lines.length - 1] ?? `exit ${String(run.status)} (no stderr)`;
47685
- result.failed.push({ spec: target.spec, reason });
47686
- console.log(` MISS ${target.spec} (${secs}s) \u2014 fetched at runtime instead: ${reason}`);
47915
+ const deadline = Date.now() + totalBudgetMs;
47916
+ const queue = [...selected];
47917
+ const worker = async () => {
47918
+ for (let target = queue.shift(); target; target = queue.shift()) {
47919
+ const cmd = warmCommand(target);
47920
+ if (!cmd) {
47921
+ result.skipped.push(`${target.spec} (no warm recipe for '${target.launcher}')`);
47922
+ continue;
47923
+ }
47924
+ const remaining = deadline - Date.now();
47925
+ if (remaining <= 0) {
47926
+ result.failed.push({ spec: target.spec, reason: "warm budget spent" });
47927
+ console.log(` MISS ${target.spec} (0.0s) \u2014 fetched at runtime instead: warm budget spent`);
47928
+ continue;
47929
+ }
47930
+ const started = Date.now();
47931
+ const run = await runCapturing(cmd.command, cmd.args, Math.min(packageTimeoutMs, remaining));
47932
+ const secs = ((Date.now() - started) / 1e3).toFixed(1);
47933
+ if (run.status === 0) {
47934
+ result.warmed.push(target.spec);
47935
+ console.log(` OK ${target.spec} (${secs}s)`);
47936
+ } else {
47937
+ const noise = /^npm (warn|notice)\b/;
47938
+ const lines = run.stderr.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !noise.test(l));
47939
+ const reason = run.error?.message ?? lines[lines.length - 1] ?? `exit ${String(run.status)} (no stderr)`;
47940
+ result.failed.push({ spec: target.spec, reason });
47941
+ console.log(` MISS ${target.spec} (${secs}s) \u2014 fetched at runtime instead: ${reason}`);
47942
+ }
47687
47943
  }
47688
- }
47944
+ };
47945
+ await Promise.all(Array.from({ length: Math.max(1, concurrency) }, worker));
47689
47946
  console.log(
47690
47947
  `[codeam mcp-warm] warmed=${result.warmed.length} failed=${result.failed.length} skipped=${result.skipped.length}`
47691
47948
  );
@@ -47693,12 +47950,13 @@ async function mcpWarm(argv = []) {
47693
47950
  "mcpWarm",
47694
47951
  `warmed=${result.warmed.length} failed=${result.failed.length} skipped=${result.skipped.length}`
47695
47952
  );
47953
+ return result;
47696
47954
  }
47697
47955
 
47698
47956
  // src/commands/version.ts
47699
47957
  var import_picocolors15 = __toESM(require("picocolors"));
47700
47958
  function version2() {
47701
- const v = true ? "2.73.10" : "unknown";
47959
+ const v = true ? "2.74.1" : "unknown";
47702
47960
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
47703
47961
  }
47704
47962
 
@@ -47900,7 +48158,11 @@ async function main() {
47900
48158
  return mcpRun(args);
47901
48159
  }
47902
48160
  if (command === "mcp-warm") {
47903
- return mcpWarm(args);
48161
+ await mcpWarm(args);
48162
+ return;
48163
+ }
48164
+ if (command === "mcp-router") {
48165
+ return mcpRouter();
47904
48166
  }
47905
48167
  if (command === "deploy") {
47906
48168
  if (args[0] === "ls" || args[0] === "list") return deployList();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.73.10",
3
+ "version": "2.74.1",
4
4
  "description": "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device — async. The terminal companion for CodeAgent Mobile.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",