@mclawnet/agent 0.6.34 → 0.6.36

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 (77) hide show
  1. package/cli.js +75 -7
  2. package/dist/__tests__/bootstrap-deps.test.d.ts +2 -0
  3. package/dist/__tests__/bootstrap-deps.test.d.ts.map +1 -0
  4. package/dist/__tests__/collect-manifest.test.d.ts +2 -0
  5. package/dist/__tests__/collect-manifest.test.d.ts.map +1 -0
  6. package/dist/__tests__/hub-connection-on-activity.test.d.ts +2 -0
  7. package/dist/__tests__/hub-connection-on-activity.test.d.ts.map +1 -0
  8. package/dist/__tests__/hub-connection-wake-watch.test.d.ts +2 -0
  9. package/dist/__tests__/hub-connection-wake-watch.test.d.ts.map +1 -0
  10. package/dist/__tests__/ideas-rest-client.test.d.ts +2 -0
  11. package/dist/__tests__/ideas-rest-client.test.d.ts.map +1 -0
  12. package/dist/__tests__/legacy-claude-execute-compat.test.d.ts +2 -0
  13. package/dist/__tests__/legacy-claude-execute-compat.test.d.ts.map +1 -0
  14. package/dist/__tests__/no-adapter-cycle.test.d.ts +2 -0
  15. package/dist/__tests__/no-adapter-cycle.test.d.ts.map +1 -0
  16. package/dist/__tests__/runtime-env-defaults.test.d.ts +2 -0
  17. package/dist/__tests__/runtime-env-defaults.test.d.ts.map +1 -0
  18. package/dist/__tests__/session-manager-exit-reason.test.d.ts +2 -0
  19. package/dist/__tests__/session-manager-exit-reason.test.d.ts.map +1 -0
  20. package/dist/__tests__/session-manager-merge.test.d.ts +2 -0
  21. package/dist/__tests__/session-manager-merge.test.d.ts.map +1 -0
  22. package/dist/__tests__/session-manager-sticky.test.d.ts +2 -0
  23. package/dist/__tests__/session-manager-sticky.test.d.ts.map +1 -0
  24. package/dist/__tests__/session-protocol-dispatch.test.d.ts +2 -0
  25. package/dist/__tests__/session-protocol-dispatch.test.d.ts.map +1 -0
  26. package/dist/__tests__/worktree-bridge.test.d.ts +2 -0
  27. package/dist/__tests__/worktree-bridge.test.d.ts.map +1 -0
  28. package/dist/backend-adapter.d.ts +6 -232
  29. package/dist/backend-adapter.d.ts.map +1 -1
  30. package/dist/backend-factory-AFF6I7YF.js +11 -0
  31. package/dist/backend-factory.d.ts +23 -1
  32. package/dist/backend-factory.d.ts.map +1 -1
  33. package/dist/bootstrap-deps.d.ts +84 -0
  34. package/dist/bootstrap-deps.d.ts.map +1 -0
  35. package/dist/bootstrap-deps.js +173 -0
  36. package/dist/bootstrap-deps.js.map +1 -0
  37. package/dist/{chunk-PJ5M6Q36.js → chunk-376QZ7JB.js} +2 -2
  38. package/dist/chunk-376QZ7JB.js.map +1 -0
  39. package/dist/{chunk-2JDX6XFD.js → chunk-GOCWMRBB.js} +1817 -298
  40. package/dist/chunk-GOCWMRBB.js.map +1 -0
  41. package/dist/{chunk-M2CDVPQF.js → chunk-JH6RGJBQ.js} +2 -2
  42. package/dist/{chunk-MFXF77LG.js → chunk-VAEFJLPL.js} +25 -3
  43. package/dist/chunk-VAEFJLPL.js.map +1 -0
  44. package/dist/{dist-VLBO5CT3.js → dist-NWVHAP5R.js} +330 -23
  45. package/dist/dist-NWVHAP5R.js.map +1 -0
  46. package/dist/errors.d.ts +20 -0
  47. package/dist/errors.d.ts.map +1 -1
  48. package/dist/hub-connection.d.ts +25 -1
  49. package/dist/hub-connection.d.ts.map +1 -1
  50. package/dist/ideas-rest-client.d.ts +25 -0
  51. package/dist/ideas-rest-client.d.ts.map +1 -0
  52. package/dist/index.js +3 -3
  53. package/dist/{linux-IHA4O633.js → linux-MBU6ERXL.js} +3 -3
  54. package/dist/{macos-G4VK2253.js → macos-I2DUWFUH.js} +3 -3
  55. package/dist/projects-handler.d.ts +146 -1
  56. package/dist/projects-handler.d.ts.map +1 -1
  57. package/dist/runtime-env-defaults.d.ts +18 -0
  58. package/dist/runtime-env-defaults.d.ts.map +1 -0
  59. package/dist/service/index.js +5 -5
  60. package/dist/session-manager.d.ts +59 -0
  61. package/dist/session-manager.d.ts.map +1 -1
  62. package/dist/start.d.ts.map +1 -1
  63. package/dist/start.js +3 -2
  64. package/dist/{windows-P6U3JLUZ.js → windows-PEJ3KOLC.js} +3 -3
  65. package/dist/worktree-bridge.d.ts +51 -0
  66. package/dist/worktree-bridge.d.ts.map +1 -0
  67. package/package.json +10 -8
  68. package/dist/backend-factory-RUYUBJVF.js +0 -9
  69. package/dist/chunk-2JDX6XFD.js.map +0 -1
  70. package/dist/chunk-MFXF77LG.js.map +0 -1
  71. package/dist/chunk-PJ5M6Q36.js.map +0 -1
  72. package/dist/dist-VLBO5CT3.js.map +0 -1
  73. /package/dist/{backend-factory-RUYUBJVF.js.map → backend-factory-AFF6I7YF.js.map} +0 -0
  74. /package/dist/{chunk-M2CDVPQF.js.map → chunk-JH6RGJBQ.js.map} +0 -0
  75. /package/dist/{linux-IHA4O633.js.map → linux-MBU6ERXL.js.map} +0 -0
  76. /package/dist/{macos-G4VK2253.js.map → macos-I2DUWFUH.js.map} +0 -0
  77. /package/dist/{windows-P6U3JLUZ.js.map → windows-PEJ3KOLC.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  loadConfig,
3
3
  saveConfig
4
- } from "./chunk-PJ5M6Q36.js";
4
+ } from "./chunk-376QZ7JB.js";
5
5
 
6
6
  // src/service/config.ts
7
7
  import { existsSync, readFileSync } from "fs";
@@ -85,4 +85,4 @@ export {
85
85
  validateServiceConfig,
86
86
  mergeServiceConfig
87
87
  };
88
- //# sourceMappingURL=chunk-M2CDVPQF.js.map
88
+ //# sourceMappingURL=chunk-JH6RGJBQ.js.map
@@ -21,7 +21,7 @@ async function createBackendAdapter(kind, ctx = {}) {
21
21
  }
22
22
  } else if (resolved === "codex") {
23
23
  try {
24
- const mod = await import("./dist-VLBO5CT3.js");
24
+ const mod = await import("./dist-NWVHAP5R.js");
25
25
  adapter = new mod.CodexAdapter();
26
26
  } catch (err) {
27
27
  throw rethrowWithRole(
@@ -41,9 +41,31 @@ function rethrowWithRole(err, roleName, prefix) {
41
41
  const tag = roleName ? ` [role=${roleName}]` : "";
42
42
  return new Error(`${prefix}${tag}: ${base}`);
43
43
  }
44
+ var ALL_KINDS_MAP = { claude: true, codex: true };
45
+ var ALL_KINDS = Object.keys(ALL_KINDS_MAP);
46
+ async function collectAgentManifest() {
47
+ const entries = await Promise.all(
48
+ ALL_KINDS.map(async (kind) => {
49
+ try {
50
+ const adapter = await createBackendAdapter(kind);
51
+ return await adapter.getManifest();
52
+ } catch (err) {
53
+ return {
54
+ kind,
55
+ installed: false,
56
+ unavailableReason: err instanceof Error ? err.message : String(err),
57
+ models: [],
58
+ modes: []
59
+ };
60
+ }
61
+ })
62
+ );
63
+ return { manifestVersion: 1, backends: entries };
64
+ }
44
65
 
45
66
  export {
46
67
  __resetBackendFactoryCache,
47
- createBackendAdapter
68
+ createBackendAdapter,
69
+ collectAgentManifest
48
70
  };
49
- //# sourceMappingURL=chunk-MFXF77LG.js.map
71
+ //# sourceMappingURL=chunk-VAEFJLPL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/backend-factory.ts"],"sourcesContent":["import type { BackendAdapter } from \"./backend-adapter.js\";\nimport type { AgentManifestPayload, BackendKind } from \"@mclawnet/shared\";\n\n// Re-export for back-compat with existing callers that import BackendKind from\n// this module rather than @mclawnet/shared.\nexport type { BackendKind };\n\nconst cache = new Map<BackendKind, BackendAdapter>();\n\nexport function __resetBackendFactoryCache(): void {\n cache.clear();\n}\n\nexport interface BackendFactoryContext {\n /** Role instance / template name — surfaces in error messages so users know\n * which role is misconfigured when its backend package fails to load. */\n roleName?: string;\n}\n\n/**\n * Resolve a BackendAdapter for the given backend kind.\n *\n * - \"claude\" / undefined → @mclawnet/claude-adapter (back-compat default).\n * - \"codex\" → @mclawnet/codex-adapter via dynamic import so the codex bundle\n * isn't pulled into claude-only deployments.\n *\n * Why dynamic import (and why codex-adapter is `optionalDependencies` in\n * agent's package.json, not `dependencies`):\n * - agent owns the runtime relationship \"I load adapters on demand\".\n * - Adapters only depend on @mclawnet/backend-types (the interface package),\n * so there is no build-time cycle.\n * - `optionalDependencies` keeps the \"user installs just @mclawnet/agent\n * and codex backend works out of the box\" UX: npm/pnpm will try to\n * install codex-adapter; if that fails (network, platform issue, user\n * opted out via --no-optional), the dynamic import below throws and\n * the agent surfaces a clear error instead of crashing at startup.\n * - Promoting codex-adapter to `dependencies` would force install, which\n * is fine for monorepo but unfriendly to claude-only deployments.\n *\n * Results are cached per kind; subsequent calls return the same instance.\n */\nexport async function createBackendAdapter(\n kind: BackendKind | undefined,\n ctx: BackendFactoryContext = {},\n): Promise<BackendAdapter> {\n const resolved: BackendKind = kind ?? \"claude\";\n const cached = cache.get(resolved);\n if (cached) return cached;\n\n let adapter: BackendAdapter;\n if (resolved === \"claude\") {\n try {\n const mod = (await import(\"@mclawnet/claude-adapter\" as string)) as {\n ClaudeCodeAdapter: new () => BackendAdapter;\n };\n adapter = new mod.ClaudeCodeAdapter();\n } catch (err) {\n throw rethrowWithRole(\n err,\n ctx.roleName,\n \"failed to load @mclawnet/claude-adapter\",\n );\n }\n } else if (resolved === \"codex\") {\n try {\n const mod = (await import(\"@mclawnet/codex-adapter\" as string)) as {\n CodexAdapter: new () => BackendAdapter;\n };\n adapter = new mod.CodexAdapter();\n } catch (err) {\n throw rethrowWithRole(\n err,\n ctx.roleName,\n \"failed to load @mclawnet/codex-adapter (is codex CLI installed?)\",\n );\n }\n } else {\n throw new Error(`unknown backend kind: ${resolved}`);\n }\n\n cache.set(resolved, adapter);\n return adapter;\n}\n\nfunction rethrowWithRole(err: unknown, roleName: string | undefined, prefix: string): Error {\n const base = err instanceof Error ? err.message : String(err);\n const tag = roleName ? ` [role=${roleName}]` : \"\";\n return new Error(`${prefix}${tag}: ${base}`);\n}\n\n/**\n * All backend kinds the agent should advertise in its manifest. Mirrors the\n * `BackendKind` union exposed by @mclawnet/shared. The `satisfies` clause\n * forces a tsc error if `BackendKind` grows without a matching map entry.\n */\nconst ALL_KINDS_MAP = { claude: true, codex: true } as const satisfies Record<BackendKind, true>;\nconst ALL_KINDS = Object.keys(ALL_KINDS_MAP) as BackendKind[];\n\n/**\n * Probe every known backend adapter and aggregate their `getManifest()` output\n * into a single payload the agent can ship to the hub. Failures (binary not on\n * PATH, adapter package broken, etc.) become `installed: false` entries rather\n * than rejecting the whole collection — the hub still wants to know \"we tried\n * but it isn't available\" for each kind.\n */\nexport async function collectAgentManifest(): Promise<AgentManifestPayload> {\n const entries = await Promise.all(\n ALL_KINDS.map(async (kind) => {\n try {\n const adapter = await createBackendAdapter(kind);\n return await adapter.getManifest();\n } catch (err) {\n return {\n kind,\n installed: false,\n unavailableReason: err instanceof Error ? err.message : String(err),\n models: [],\n modes: [],\n };\n }\n }),\n );\n return { manifestVersion: 1, backends: entries };\n}\n"],"mappings":";AAOA,IAAM,QAAQ,oBAAI,IAAiC;AAE5C,SAAS,6BAAmC;AACjD,QAAM,MAAM;AACd;AA8BA,eAAsB,qBACpB,MACA,MAA6B,CAAC,GACL;AACzB,QAAM,WAAwB,QAAQ;AACtC,QAAM,SAAS,MAAM,IAAI,QAAQ;AACjC,MAAI,OAAQ,QAAO;AAEnB,MAAI;AACJ,MAAI,aAAa,UAAU;AACzB,QAAI;AACF,YAAM,MAAO,MAAM,OAAO,0BAAoC;AAG9D,gBAAU,IAAI,IAAI,kBAAkB;AAAA,IACtC,SAAS,KAAK;AACZ,YAAM;AAAA,QACJ;AAAA,QACA,IAAI;AAAA,QACJ;AAAA,MACF;AAAA,IACF;AAAA,EACF,WAAW,aAAa,SAAS;AAC/B,QAAI;AACF,YAAM,MAAO,MAAM,OAAO,oBAAmC;AAG7D,gBAAU,IAAI,IAAI,aAAa;AAAA,IACjC,SAAS,KAAK;AACZ,YAAM;AAAA,QACJ;AAAA,QACA,IAAI;AAAA,QACJ;AAAA,MACF;AAAA,IACF;AAAA,EACF,OAAO;AACL,UAAM,IAAI,MAAM,yBAAyB,QAAQ,EAAE;AAAA,EACrD;AAEA,QAAM,IAAI,UAAU,OAAO;AAC3B,SAAO;AACT;AAEA,SAAS,gBAAgB,KAAc,UAA8B,QAAuB;AAC1F,QAAM,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC5D,QAAM,MAAM,WAAW,UAAU,QAAQ,MAAM;AAC/C,SAAO,IAAI,MAAM,GAAG,MAAM,GAAG,GAAG,KAAK,IAAI,EAAE;AAC7C;AAOA,IAAM,gBAAgB,EAAE,QAAQ,MAAM,OAAO,KAAK;AAClD,IAAM,YAAY,OAAO,KAAK,aAAa;AAS3C,eAAsB,uBAAsD;AAC1E,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,UAAU,IAAI,OAAO,SAAS;AAC5B,UAAI;AACF,cAAM,UAAU,MAAM,qBAAqB,IAAI;AAC/C,eAAO,MAAM,QAAQ,YAAY;AAAA,MACnC,SAAS,KAAK;AACZ,eAAO;AAAA,UACL;AAAA,UACA,WAAW;AAAA,UACX,mBAAmB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,UAClE,QAAQ,CAAC;AAAA,UACT,OAAO,CAAC;AAAA,QACV;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO,EAAE,iBAAiB,GAAG,UAAU,QAAQ;AACjD;","names":[]}
@@ -1,28 +1,43 @@
1
1
  // ../codex-adapter/dist/codex-adapter.js
2
- import { spawn } from "child_process";
2
+ import { execSync, spawn } from "child_process";
3
3
  import { EventEmitter } from "events";
4
- import { existsSync } from "fs";
4
+ import { existsSync, readFileSync } from "fs";
5
5
  import { createRequire } from "module";
6
- import { homedir } from "os";
7
- import { join as join2 } from "path";
6
+ import { homedir, platform as platform2 } from "os";
7
+ import { dirname, join as join2 } from "path";
8
8
 
9
9
  // ../codex-adapter/dist/codex-spawn-args.js
10
10
  import { writeFileSync, unlinkSync } from "fs";
11
11
  import { join } from "path";
12
12
  import { tmpdir } from "os";
13
13
  import { randomUUID } from "crypto";
14
- import { DEFAULT_SANDBOX } from "@mclawnet/shared";
14
+ import { DEFAULT_SANDBOX, SANDBOX_LEVELS } from "@mclawnet/shared";
15
15
  function escapeToml(value) {
16
16
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
17
17
  }
18
+ var VALID_CODEX_SANDBOX = new Set(SANDBOX_LEVELS);
18
19
  function buildCodexSpawnArgs(options) {
19
20
  const args = ["app-server"];
20
21
  let briefingFile;
21
22
  void options.resumeId;
22
23
  args.push("-c", 'approval_policy="never"');
23
- const sandboxLevel = options.sandbox ?? DEFAULT_SANDBOX;
24
+ let sandboxLevel;
25
+ let modeSource;
26
+ if (options.mode && VALID_CODEX_SANDBOX.has(options.mode)) {
27
+ sandboxLevel = options.mode;
28
+ modeSource = "options.mode";
29
+ } else if (options.sandbox) {
30
+ sandboxLevel = options.sandbox;
31
+ modeSource = "options.sandbox";
32
+ } else {
33
+ sandboxLevel = DEFAULT_SANDBOX;
34
+ modeSource = "default";
35
+ }
24
36
  const codexSandboxMode = sandboxLevel === "read-only" ? "read-only" : sandboxLevel === "full-access" ? "danger-full-access" : "workspace-write";
25
37
  args.push("-c", `sandbox_mode="${codexSandboxMode}"`);
38
+ if (options.model) {
39
+ args.push("-c", `model="${escapeToml(options.model)}"`);
40
+ }
26
41
  if (options.mcpServer) {
27
42
  const { command, args: serverArgs, env } = options.mcpServer;
28
43
  args.push("-c", `mcp_servers.clawnet-mcp.command="${escapeToml(command)}"`);
@@ -38,11 +53,12 @@ function buildCodexSpawnArgs(options) {
38
53
  args.push("-c", 'mcp_servers.clawnet-mcp.default_tools_approval_mode="approve"');
39
54
  }
40
55
  if (options.systemPrompt) {
41
- briefingFile = join(tmpdir(), `clawnet-briefing-${options.sessionId}-${randomUUID().slice(0, 8)}.md`);
56
+ const safeId = options.sessionId.replace(/[\\/:*?"<>|\x00-\x1F]/g, "_").replace(/[. ]+$/g, "_");
57
+ briefingFile = join(tmpdir(), `clawnet-briefing-${safeId}-${randomUUID().slice(0, 8)}.md`);
42
58
  writeFileSync(briefingFile, options.systemPrompt, { encoding: "utf-8", mode: 384 });
43
59
  args.push("-c", `model_instructions_file="${escapeToml(briefingFile)}"`);
44
60
  }
45
- return { args, briefingFile };
61
+ return { args, briefingFile, modeSource };
46
62
  }
47
63
  function cleanupBriefingFile(filePath) {
48
64
  try {
@@ -69,17 +85,38 @@ var JsonRpcClient = class {
69
85
  this.onNotification = opts.onNotification;
70
86
  this.onMalformedLine = opts.onMalformedLine;
71
87
  opts.stdout.on("data", (chunk) => this.feed(chunk.toString("utf-8")));
88
+ this.stdin.on("error", (err) => this.failAllPending(err));
89
+ }
90
+ /**
91
+ * Reject every in-flight request with the given error. Called when stdin
92
+ * dies so callers get a clean rejection instead of hanging until the
93
+ * handshake death-poll catches up.
94
+ */
95
+ failAllPending(err) {
96
+ for (const [, p] of this.pending) {
97
+ try {
98
+ p.reject(err);
99
+ } catch {
100
+ }
101
+ }
102
+ this.pending.clear();
72
103
  }
73
104
  request(method, params) {
74
105
  const id = this.nextId++;
75
106
  const frame = { jsonrpc: "2.0", id, method, params };
76
107
  return new Promise((resolve, reject) => {
77
108
  this.pending.set(id, { resolve, reject });
78
- this.stdin.write(JSON.stringify(frame) + "\n");
109
+ this.stdin.write(JSON.stringify(frame) + "\n", (err) => {
110
+ if (err) {
111
+ this.pending.delete(id);
112
+ reject(err);
113
+ }
114
+ });
79
115
  });
80
116
  }
81
117
  notify(method, params) {
82
- this.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
118
+ this.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n", () => {
119
+ });
83
120
  }
84
121
  feed(s) {
85
122
  this.buffer += s;
@@ -273,9 +310,6 @@ function mapCodexFrame(frame) {
273
310
  }
274
311
  case "item/completed": {
275
312
  const item = params.item;
276
- if (item?.type === "agentMessage" && typeof item.text === "string") {
277
- return { kind: "assistant_text", text: item.text };
278
- }
279
313
  if (item?.type === "commandExecution") {
280
314
  const failed = item.status === "failed" || item.status === "declined";
281
315
  const badExit = typeof item.exitCode === "number" && item.exitCode !== 0;
@@ -354,13 +388,139 @@ function mapCodexFrame(frame) {
354
388
  return { kind: "raw", backend: "codex", payload: frame };
355
389
  }
356
390
 
391
+ // ../codex-adapter/dist/catalog.js
392
+ var CODEX_MODELS = [
393
+ {
394
+ code: "gpt-4o",
395
+ label: "GPT-4o",
396
+ tier: "premium",
397
+ contextWindow: 128e3,
398
+ supportsVision: true,
399
+ supportsToolUse: true,
400
+ default: true
401
+ },
402
+ {
403
+ code: "gpt-4o-mini",
404
+ label: "GPT-4o mini",
405
+ tier: "standard",
406
+ contextWindow: 128e3,
407
+ supportsVision: true,
408
+ supportsToolUse: true
409
+ }
410
+ ];
411
+ var CODEX_MODES = [
412
+ { code: "read-only", label: "Read Only", description: "No FS writes" },
413
+ { code: "workspace-write", label: "Workspace Write", description: "Write inside cwd only", default: true },
414
+ { code: "full-access", label: "Full Access", description: "No sandbox" }
415
+ ];
416
+
417
+ // ../codex-adapter/dist/detect.js
418
+ import { execFile } from "child_process";
419
+ import { promisify } from "util";
420
+ import { platform } from "os";
421
+ var exec = promisify(execFile);
422
+ async function detectCodexInstall() {
423
+ try {
424
+ const isWin2 = platform() === "win32";
425
+ const lookup = isWin2 ? "where" : "which";
426
+ const { stdout: lookupOut } = await exec(lookup, ["codex"], { timeout: 5e3 });
427
+ const binaryPath = lookupOut.trim().split(/\r?\n/)[0]?.trim();
428
+ if (!binaryPath) {
429
+ return { installed: false, reason: `codex CLI not detected: empty ${lookup} output` };
430
+ }
431
+ const { stdout: verOut } = await exec(binaryPath, ["--version"], { timeout: 5e3 });
432
+ return { installed: true, binaryPath, version: verOut.trim() };
433
+ } catch (err) {
434
+ const message = err instanceof Error ? err.message : String(err);
435
+ return { installed: false, reason: `codex CLI not detected: ${message}` };
436
+ }
437
+ }
438
+
357
439
  // ../codex-adapter/dist/codex-adapter.js
358
440
  var log = createLogger({ module: "codex-adapter" });
441
+ var isWin = platform2() === "win32";
442
+ function resolveCodexBin(explicit) {
443
+ if (explicit && explicit !== "codex")
444
+ return explicit;
445
+ const names = isWin ? ["codex.exe", "codex.cmd", "codex.bat"] : ["codex"];
446
+ const whichCmd = isWin ? "where" : "which";
447
+ for (const name of names) {
448
+ try {
449
+ const found = execSync(`${whichCmd} ${name}`, {
450
+ encoding: "utf-8",
451
+ // 1.5s is plenty for `where`/`which` on a healthy machine; the old
452
+ // 5s × 3 worst-case (15s blocking event loop) showed up on slow
453
+ // domain-joined Windows hosts / network-PATH setups.
454
+ timeout: 1500,
455
+ stdio: ["ignore", "pipe", "ignore"]
456
+ }).trim().split(/\r?\n/)[0];
457
+ if (found && existsSync(found))
458
+ return found;
459
+ } catch {
460
+ }
461
+ }
462
+ const home = homedir();
463
+ const fallbacks = isWin ? [
464
+ join2(home, "AppData", "Roaming", "npm", "codex.cmd"),
465
+ join2(home, "AppData", "Roaming", "npm", "codex.exe"),
466
+ join2(home, ".cargo", "bin", "codex.exe"),
467
+ join2(home, ".local", "bin", "codex.exe")
468
+ ] : [
469
+ join2(home, ".cargo", "bin", "codex"),
470
+ join2(home, ".local", "bin", "codex"),
471
+ "/opt/homebrew/bin/codex",
472
+ "/usr/local/bin/codex"
473
+ ];
474
+ for (const p of fallbacks) {
475
+ if (existsSync(p))
476
+ return p;
477
+ }
478
+ return isWin ? "codex.cmd" : "codex";
479
+ }
480
+ var resolvedCodexBinCache = /* @__PURE__ */ new Map();
481
+ var resolveCodexBinCallCount = 0;
482
+ function getResolvedCodexBin(explicit) {
483
+ const key = explicit ?? "";
484
+ const hit = resolvedCodexBinCache.get(key);
485
+ if (hit)
486
+ return hit;
487
+ resolveCodexBinCallCount++;
488
+ const resolved = resolveCodexBin(explicit);
489
+ resolvedCodexBinCache.set(key, resolved);
490
+ return resolved;
491
+ }
492
+ function resolveSpawnTarget(codexBin) {
493
+ const isWindows = platform2() === "win32";
494
+ if (!isWindows || !codexBin.toLowerCase().endsWith(".cmd")) {
495
+ return { command: codexBin, prefixArgs: [] };
496
+ }
497
+ try {
498
+ const cmdContent = readFileSync(codexBin, "utf-8");
499
+ const exeMatch = cmdContent.match(/(?:%~?dp0%?)[\\/](.+?\.exe)/i);
500
+ if (exeMatch) {
501
+ const relParts = exeMatch[1].split(/[\\/]/);
502
+ const exePath = join2(dirname(codexBin), ...relParts);
503
+ if (existsSync(exePath))
504
+ return { command: exePath, prefixArgs: [] };
505
+ }
506
+ const jsMatch = cmdContent.match(/(?:%~?dp0%?)[\\/](.+?\.js)/i);
507
+ if (jsMatch) {
508
+ const relParts = jsMatch[1].split(/[\\/]/);
509
+ const cliJsPath = join2(dirname(codexBin), ...relParts);
510
+ if (existsSync(cliJsPath)) {
511
+ return { command: process.execPath, prefixArgs: [cliJsPath] };
512
+ }
513
+ }
514
+ } catch {
515
+ }
516
+ return { command: codexBin, prefixArgs: [] };
517
+ }
359
518
  var CLAWNET_CLIENT_INFO = {
360
519
  name: "clawnet",
361
520
  version: "0.1.0",
362
521
  title: "ClawNet Agent"
363
522
  };
523
+ var ASSISTANT_TEXT_SEEN_MAX = 64;
364
524
  var CodexProcess = class extends EventEmitter {
365
525
  id;
366
526
  workDir;
@@ -388,6 +548,24 @@ var CodexProcess = class extends EventEmitter {
388
548
  handshakeComplete = false;
389
549
  /** Inputs queued by send() while the handshake is still in flight. */
390
550
  pendingInputs = [];
551
+ /**
552
+ * Per-itemId accumulator of assistant text already emitted via
553
+ * `item/agentMessage/delta` chunks. Consulted on `item/completed` with
554
+ * `item.type === "agentMessage"` to decide what (if anything) of the
555
+ * completed payload still needs to be emitted. Without this, codex
556
+ * delivers the same assistant text twice (deltas + final) and the UI
557
+ * renders "Hello worldHello world".
558
+ *
559
+ * Entries are cleared per item when the matching `item/completed` fires.
560
+ * We do NOT clear the whole map on `turn/completed`: codex's wire
561
+ * doesn't strictly guarantee that every item/completed precedes its
562
+ * parent turn/completed, and a trailing item/completed against an empty
563
+ * accumulator would re-emit the full text (reactivating the bug). To
564
+ * cap unbounded growth from interrupted turns that never fire
565
+ * completed, the map is size-bounded with FIFO eviction; in practice a
566
+ * single turn produces 1-2 agentMessage items so the cap is generous.
567
+ */
568
+ assistantTextSeen = /* @__PURE__ */ new Map();
391
569
  /** Temp file for briefing injection; cleaned up on kill(). */
392
570
  briefingFile;
393
571
  /**
@@ -425,6 +603,7 @@ var CodexProcess = class extends EventEmitter {
425
603
  this.approvalMethods.clear();
426
604
  this.approvalResolvers.clear();
427
605
  this.pendingInputs.length = 0;
606
+ this.assistantTextSeen.clear();
428
607
  if (this.briefingFile) {
429
608
  cleanupBriefingFile(this.briefingFile);
430
609
  this.briefingFile = void 0;
@@ -448,28 +627,39 @@ var CodexAdapter = class {
448
627
  type = "codex";
449
628
  codexBin;
450
629
  handshakeTimeoutMs;
630
+ detect;
451
631
  constructor(options) {
452
- this.codexBin = options?.codexBin ?? "codex";
632
+ this.codexBin = getResolvedCodexBin(options?.codexBin);
453
633
  const envTimeout = Number(process.env.CLAWNET_CODEX_HANDSHAKE_TIMEOUT_MS);
454
634
  this.handshakeTimeoutMs = options?.handshakeTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : 15e3);
635
+ this.detect = options?.detect ?? detectCodexInstall;
455
636
  }
456
637
  async spawn(options) {
457
638
  let cwd = options.workDir || process.cwd();
458
639
  if (!existsSync(cwd))
459
640
  cwd = homedir();
460
641
  const mcpServer = this.buildMcpServerConfig(options);
461
- const { args, briefingFile } = buildCodexSpawnArgs({
642
+ const { args, briefingFile, modeSource } = buildCodexSpawnArgs({
462
643
  sessionId: options.sessionId,
463
644
  resumeId: options.resumeId,
464
645
  systemPrompt: options.systemPrompt,
465
646
  mcpServer,
466
- sandbox: options.sandbox
647
+ sandbox: options.sandbox,
648
+ model: options.model,
649
+ mode: options.mode
467
650
  });
651
+ if (options.mode && modeSource !== "options.mode") {
652
+ log.warn({ sessionId: options.sessionId, requestedMode: options.mode, modeSource }, "codex: invalid options.mode ignored, falling back");
653
+ }
468
654
  log.info({ sessionId: options.sessionId, bin: this.codexBin, args, cwd, resumeId: options.resumeId }, "codex spawn: forking app-server");
469
- const proc = spawn(this.codexBin, args, {
655
+ const { command, prefixArgs } = resolveSpawnTarget(this.codexBin);
656
+ if (command !== this.codexBin) {
657
+ log.info({ sessionId: options.sessionId, bin: this.codexBin, spawnCmd: command, prefixArgs }, "codex spawn: using resolved shim target (windows .cmd bypass)");
658
+ }
659
+ const proc = spawn(command, [...prefixArgs, ...args], {
470
660
  cwd,
471
661
  stdio: ["pipe", "pipe", "pipe"],
472
- env: process.env,
662
+ env: { ...process.env, ...options.env ?? {} },
473
663
  windowsHide: true
474
664
  });
475
665
  const stderrChunks = [];
@@ -481,12 +671,32 @@ var CodexAdapter = class {
481
671
  }
482
672
  });
483
673
  let exitInfo = null;
674
+ let createdCp;
675
+ const emitProcessExit = (cp2, code, signal) => {
676
+ try {
677
+ cp2.emit("exit", code);
678
+ } catch {
679
+ }
680
+ const isClean = code === 0 || signal === "SIGTERM";
681
+ if (isClean)
682
+ return;
683
+ const stderr = stderrChunks.join("\n").trim().slice(0, 800);
684
+ const detail = stderr ? ` \u2014 stderr: ${stderr}` : "";
685
+ try {
686
+ cp2.emit("error", new Error(`codex process exited (code=${code ?? "null"}, signal=${signal ?? "null"})${detail}`));
687
+ } catch {
688
+ }
689
+ };
484
690
  proc.on("exit", (code, signal) => {
485
691
  const isClean = code === 0 || signal === "SIGTERM";
486
692
  const level = isClean ? "debug" : "warn";
487
693
  log[level]({ sessionId: options.sessionId, code, signal }, "codex process exited");
488
694
  if (!exitInfo)
489
695
  exitInfo = { code, signal };
696
+ const cpRef = createdCp;
697
+ if (!cpRef)
698
+ return;
699
+ emitProcessExit(cpRef, code, signal);
490
700
  });
491
701
  await new Promise((resolve, reject) => {
492
702
  const t = setTimeout(() => resolve(), 2e3);
@@ -505,15 +715,39 @@ var CodexAdapter = class {
505
715
  cp.resumeId = options.resumeId;
506
716
  cp.getExitInfo = () => exitInfo;
507
717
  cp.getStderr = () => stderrChunks.join("\n").slice(0, 800);
718
+ createdCp = cp;
719
+ if (exitInfo) {
720
+ const { code, signal } = exitInfo;
721
+ setImmediate(() => emitProcessExit(cp, code, signal));
722
+ }
508
723
  this.wireRpc(cp);
509
724
  return cp;
510
725
  }
511
726
  buildMcpServerConfig(options) {
512
727
  let serverPath;
513
- try {
514
- const require2 = createRequire(import.meta.url);
515
- serverPath = require2.resolve("@mclawnet/mcp-server/dist/server.js");
516
- } catch {
728
+ const require2 = createRequire(import.meta.url);
729
+ const tryResolve = (specifier) => {
730
+ try {
731
+ const p = require2.resolve(specifier);
732
+ return p && existsSync(p) ? p : void 0;
733
+ } catch {
734
+ return void 0;
735
+ }
736
+ };
737
+ serverPath = tryResolve("@mclawnet/mcp-server/server");
738
+ if (!serverPath) {
739
+ try {
740
+ const pkgPath = require2.resolve("@mclawnet/mcp-server/package.json");
741
+ const candidate = join2(dirname(pkgPath), "dist", "server.js");
742
+ if (existsSync(candidate))
743
+ serverPath = candidate;
744
+ } catch {
745
+ }
746
+ }
747
+ if (!serverPath) {
748
+ serverPath = tryResolve("@mclawnet/mcp-server/dist/server.js");
749
+ }
750
+ if (!serverPath) {
517
751
  const devPath = join2(import.meta.dirname ?? __dirname, "../../mcp-server/dist/server.js");
518
752
  if (existsSync(devPath))
519
753
  serverPath = devPath;
@@ -522,6 +756,7 @@ var CodexAdapter = class {
522
756
  log.warn({ pkg: "@mclawnet/mcp-server" }, "codex: clawnet-mcp-server not resolvable, codex role will lack MCP tools");
523
757
  return void 0;
524
758
  }
759
+ log.debug({ serverPath }, "codex: resolved clawnet-mcp-server path");
525
760
  const env = {};
526
761
  if (options.workDir)
527
762
  env.CLAWNET_WORK_DIR = options.workDir;
@@ -646,7 +881,24 @@ var CodexAdapter = class {
646
881
  return;
647
882
  }
648
883
  if (method === "error") {
884
+ const p = params ?? {};
885
+ const message = p.error?.message ?? "codex emitted unspecified error";
649
886
  log.warn({ sessionId: cp.id, params }, "codex error notification");
887
+ cp.emit("error", new Error(`codex: ${message}`));
888
+ if (p.willRetry === false) {
889
+ void cp.kill().catch((err) => {
890
+ log.warn({ err, sessionId: cp.id }, "kill after fatal codex error failed");
891
+ });
892
+ }
893
+ return;
894
+ }
895
+ if (method === "thread/status/changed") {
896
+ const p = params ?? {};
897
+ if (p.status?.type === "systemError") {
898
+ log.warn({ sessionId: cp.id, params }, "codex thread systemError");
899
+ cp.emit("error", new Error("codex: thread entered systemError"));
900
+ }
901
+ return;
650
902
  }
651
903
  if (method === "turn/completed" || method === "thread/turnComplete" || method === "turn/complete") {
652
904
  log.info({ sessionId: cp.id, method }, "codex turn complete");
@@ -659,6 +911,49 @@ var CodexAdapter = class {
659
911
  log.info({ sessionId: cp.id }, "codex turn started");
660
912
  return;
661
913
  }
914
+ if (method === "item/agentMessage/delta") {
915
+ const p = params ?? {};
916
+ if (typeof p.itemId === "string" && typeof p.delta === "string") {
917
+ const prev = cp.assistantTextSeen.get(p.itemId) ?? "";
918
+ if (!cp.assistantTextSeen.has(p.itemId) && cp.assistantTextSeen.size >= ASSISTANT_TEXT_SEEN_MAX) {
919
+ const oldest = cp.assistantTextSeen.keys().next().value;
920
+ if (oldest !== void 0)
921
+ cp.assistantTextSeen.delete(oldest);
922
+ }
923
+ cp.assistantTextSeen.set(p.itemId, prev + p.delta);
924
+ } else {
925
+ log.warn({ sessionId: cp.id, params: p }, "codex agentMessage delta: missing itemId/delta \u2014 dedupe will not apply to this item");
926
+ }
927
+ } else if (method === "item/completed") {
928
+ const p = params ?? {};
929
+ const item = p.item;
930
+ if (item?.type === "agentMessage") {
931
+ if (typeof item.id !== "string" || typeof item.text !== "string") {
932
+ log.warn({ sessionId: cp.id, item }, "codex agentMessage completed: malformed item.id/text \u2014 dropping to avoid duplicate emission");
933
+ return;
934
+ }
935
+ const seen = cp.assistantTextSeen.get(item.id) ?? "";
936
+ cp.assistantTextSeen.delete(item.id);
937
+ const full = item.text;
938
+ let suffix;
939
+ if (full === seen) {
940
+ return;
941
+ } else if (seen.length > 0 && full.startsWith(seen)) {
942
+ suffix = full.slice(seen.length);
943
+ } else {
944
+ suffix = full;
945
+ if (seen.length > 0) {
946
+ log.warn({ sessionId: cp.id, itemId: item.id, seenLen: seen.length, fullLen: full.length }, "codex agentMessage: completed text diverges from accumulated deltas \u2014 emitting full");
947
+ }
948
+ }
949
+ if (suffix.length === 0)
950
+ return;
951
+ const out2 = { kind: "assistant_text", text: suffix };
952
+ this.logBackendOutput(cp.id, method, out2);
953
+ cp.emit("output", out2);
954
+ return;
955
+ }
956
+ }
662
957
  const out = mapCodexFrame({ method, params });
663
958
  this.logBackendOutput(cp.id, method, out);
664
959
  cp.emit("output", out);
@@ -763,6 +1058,18 @@ var CodexAdapter = class {
763
1058
  if (process2 instanceof CodexProcess)
764
1059
  process2.on("exit", handler);
765
1060
  }
1061
+ async getManifest() {
1062
+ const det = await this.detect();
1063
+ return {
1064
+ kind: "codex",
1065
+ installed: det.installed,
1066
+ binaryPath: det.binaryPath,
1067
+ version: det.version,
1068
+ unavailableReason: det.reason,
1069
+ models: det.installed ? CODEX_MODELS : [],
1070
+ modes: det.installed ? CODEX_MODES : []
1071
+ };
1072
+ }
766
1073
  };
767
1074
  export {
768
1075
  CodexAdapter,
@@ -772,4 +1079,4 @@ export {
772
1079
  mapCodexFrame,
773
1080
  parseApprovalRequest
774
1081
  };
775
- //# sourceMappingURL=dist-VLBO5CT3.js.map
1082
+ //# sourceMappingURL=dist-NWVHAP5R.js.map