@mclawnet/agent 0.6.34 → 0.6.35

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.
package/cli.js CHANGED
@@ -39,13 +39,39 @@ function printNeedsInit() {
39
39
  // === init ===
40
40
  program
41
41
  .command("init")
42
- .description("Initialize ~/.clawnet/settings.json (token + name)")
42
+ .description("Initialize ~/.clawnet/settings.json (token + name) and check CLI dependencies")
43
43
  .option("--token <token>", "Agent token (skip interactive prompt)")
44
44
  .option("--name <name>", "Agent name (skip interactive prompt)")
45
45
  .option("--hub-url <url>", "Hub WebSocket URL (defaults to wss://mclaw.work/ws/agent)")
46
+ .option("--skip-deps", "Skip CLI dependency check / install / config patching")
47
+ .option("--yes", "Non-interactive: accept all install prompts")
46
48
  .action(async (opts) => {
47
49
  const { saveConfig, loadConfig } = await import("./dist/index.js");
48
50
 
51
+ if (!opts.skipDeps) {
52
+ const { bootstrapDeps } = await import("./dist/bootstrap-deps.js");
53
+ const isTty = process.stdin.isTTY === true && process.stdout.isTTY === true;
54
+ const interactive = isTty && !opts.yes;
55
+ try {
56
+ const report = await bootstrapDeps({
57
+ homeDir: (await import("os")).homedir(),
58
+ interactive,
59
+ install: true,
60
+ promptFn: prompt,
61
+ });
62
+ if (report.present.length) console.log(` ✓ CLIs present: ${report.present.join(", ")}`);
63
+ if (report.installed.length) console.log(` ✓ Installed: ${report.installed.join(", ")}`);
64
+ if (report.missing.filter((m) => !report.installed.includes(m)).length)
65
+ console.log(` ⚠ Still missing: ${report.missing.filter((m) => !report.installed.includes(m)).join(", ")}`);
66
+ for (const c of report.configChanges) console.log(` • ${c}`);
67
+ for (const h of report.envHints) console.log(` ⚠ ${h}`);
68
+ console.log();
69
+ } catch (err) {
70
+ console.error("[clawnet] bootstrap-deps failed:", err.message || err);
71
+ console.error(" Continuing with token setup. Re-run 'clawnet-agent doctor' to retry.");
72
+ }
73
+ }
74
+
49
75
  const existing = loadConfig();
50
76
 
51
77
  let token = opts.token;
@@ -82,6 +108,48 @@ program
82
108
  console.log(" Run 'clawnet-agent start' to start the agent.\n");
83
109
  });
84
110
 
111
+ // === doctor ===
112
+ program
113
+ .command("doctor")
114
+ .description("Diagnose CLI dependencies and config (read-only by default; --fix to install/patch)")
115
+ .option("--fix", "Install missing CLIs and patch missing config keys")
116
+ .option("--yes", "Non-interactive: accept all install prompts (only with --fix)")
117
+ .action(async (opts) => {
118
+ const { bootstrapDeps } = await import("./dist/bootstrap-deps.js");
119
+ const { homedir } = await import("os");
120
+ const isTty = process.stdin.isTTY === true && process.stdout.isTTY === true;
121
+ const interactive = opts.fix && isTty && !opts.yes;
122
+ const report = await bootstrapDeps({
123
+ homeDir: homedir(),
124
+ interactive,
125
+ install: Boolean(opts.fix),
126
+ promptFn: prompt,
127
+ });
128
+ console.log("ClawNet Agent — Doctor Report\n");
129
+ console.log("CLI dependencies:");
130
+ for (const name of ["claude", "codex"]) {
131
+ const present = report.present.includes(name);
132
+ const installed = report.installed.includes(name);
133
+ const status = installed ? "installed (this run)" : present ? "present" : "MISSING";
134
+ console.log(` ${present || installed ? "✓" : "✗"} ${name}: ${status}`);
135
+ }
136
+ console.log("\nConfig:");
137
+ if (report.configChanges.length === 0) {
138
+ console.log(" ✓ ~/.codex/config.toml and ~/.claude/settings.json look fine");
139
+ } else {
140
+ for (const c of report.configChanges) console.log(` • ${c}`);
141
+ }
142
+ if (report.envHints.length > 0) {
143
+ console.log("\nEnvironment:");
144
+ for (const h of report.envHints) console.log(` ⚠ ${h}`);
145
+ }
146
+ const stillMissing = report.missing.filter((m) => !report.installed.includes(m));
147
+ if (stillMissing.length > 0 && !opts.fix) {
148
+ console.log(`\nRun 'clawnet-agent doctor --fix' to install missing CLIs and patch config.`);
149
+ }
150
+ if (stillMissing.length > 0) process.exit(1);
151
+ });
152
+
85
153
  // === start (default command) ===
86
154
  program
87
155
  .command("start", { isDefault: true })
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bootstrap-deps.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap-deps.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/bootstrap-deps.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=runtime-env-defaults.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-env-defaults.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/runtime-env-defaults.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=session-manager-exit-reason.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-manager-exit-reason.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/session-manager-exit-reason.test.ts"],"names":[],"mappings":""}
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  __resetBackendFactoryCache,
3
3
  createBackendAdapter
4
- } from "./chunk-MFXF77LG.js";
4
+ } from "./chunk-FYM7CXUI.js";
5
5
  export {
6
6
  __resetBackendFactoryCache,
7
7
  createBackendAdapter
8
8
  };
9
- //# sourceMappingURL=backend-factory-RUYUBJVF.js.map
9
+ //# sourceMappingURL=backend-factory-VRPU3534.js.map
@@ -0,0 +1,62 @@
1
+ export interface DetectResult {
2
+ found: boolean;
3
+ path?: string;
4
+ }
5
+ export interface DetectOptions {
6
+ /** Override which/where lookup. Tests inject a fake; runtime defaults to execFileSync. */
7
+ whichFn?: (bin: string) => string;
8
+ }
9
+ export declare function detectCli(bin: string, opts?: DetectOptions): DetectResult;
10
+ export interface PatchResult {
11
+ /** Human-readable change log entries; empty array = no changes. */
12
+ patched: string[];
13
+ }
14
+ export interface PatchOptions {
15
+ homeDir: string;
16
+ }
17
+ /**
18
+ * Merge defaults into ~/.codex/config.toml.
19
+ *
20
+ * Two paths:
21
+ * - File absent → write a fresh stringified TOML with our defaults.
22
+ * - File exists → parse to detect missing keys, then APPEND missing
23
+ * fragments as raw text. We never re-stringify the parsed AST, because
24
+ * smol-toml's stringify drops comments and may reorder keys (verified
25
+ * by regression test). Appending preserves the user's original bytes.
26
+ *
27
+ * Never touches user-set model_provider / model / per-provider fields.
28
+ */
29
+ export declare function patchCodexConfig(opts: PatchOptions): PatchResult;
30
+ /**
31
+ * Ensure ~/.claude/settings.json exists.
32
+ *
33
+ * Currently a thin presence-check: claude-code's own config schema evolves
34
+ * fast and we don't want to fight it. Just create an empty object if absent
35
+ * so downstream tools can rely on the file's existence.
36
+ */
37
+ export declare function patchClaudeConfig(opts: PatchOptions): PatchResult;
38
+ export declare function detectEnvVar(name: string): boolean;
39
+ export interface BootstrapOptions {
40
+ homeDir: string;
41
+ /** When true, prompt user before installing CLIs. */
42
+ interactive: boolean;
43
+ /** When false, never install (doctor mode). */
44
+ install: boolean;
45
+ whichFn?: (bin: string) => string;
46
+ installFn?: (cliName: "claude" | "codex") => Promise<void>;
47
+ promptFn?: (question: string) => Promise<string>;
48
+ }
49
+ export interface BootstrapReport {
50
+ /** CLIs that were already installed. */
51
+ present: string[];
52
+ /** CLIs missing at start. */
53
+ missing: string[];
54
+ /** CLIs installed during this run. */
55
+ installed: string[];
56
+ /** Config patcher change logs (codex + claude). */
57
+ configChanges: string[];
58
+ /** Hint strings to print to user about env vars that need attention. */
59
+ envHints: string[];
60
+ }
61
+ export declare function bootstrapDeps(opts: BootstrapOptions): Promise<BootstrapReport>;
62
+ //# sourceMappingURL=bootstrap-deps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap-deps.d.ts","sourceRoot":"","sources":["../src/bootstrap-deps.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACnC;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,YAAY,CAyB7E;AAED,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CAqEhE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CAejE;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGlD;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,WAAW,EAAE,OAAO,CAAC;IACrB,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAClC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,eAAe;IAC9B,wCAAwC;IACxC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,sCAAsC;IACtC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,mDAAmD;IACnD,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,wEAAwE;IACxE,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAmBD,wBAAsB,aAAa,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAqDpF"}
@@ -0,0 +1,154 @@
1
+ // src/bootstrap-deps.ts
2
+ import { execFileSync } from "child_process";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import { platform } from "os";
5
+ import { join } from "path";
6
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
7
+ var isWin = platform() === "win32";
8
+ function detectCli(bin, opts = {}) {
9
+ const whichFn = opts.whichFn ?? ((b) => {
10
+ const cmd = isWin ? "where" : "which";
11
+ return execFileSync(cmd, [b], {
12
+ encoding: "utf-8",
13
+ timeout: 1500,
14
+ stdio: ["ignore", "pipe", "ignore"]
15
+ }).trim().split(/\r?\n/)[0];
16
+ });
17
+ try {
18
+ const path = whichFn(bin);
19
+ if (path) return { found: true, path };
20
+ return { found: false };
21
+ } catch {
22
+ return { found: false };
23
+ }
24
+ }
25
+ var COPILOT_PROVIDER_DEFAULTS = {
26
+ name: "copilot-api",
27
+ base_url: "http://localhost:4141/v1",
28
+ wire_api: "responses",
29
+ env_key: "COPILOT_API_KEY"
30
+ };
31
+ function patchCodexConfig(opts) {
32
+ const codexDir = join(opts.homeDir, ".codex");
33
+ const cfgPath = join(codexDir, "config.toml");
34
+ if (!existsSync(cfgPath)) {
35
+ const fresh = {
36
+ model_provider: "copilot-api",
37
+ model_providers: { "copilot-api": COPILOT_PROVIDER_DEFAULTS }
38
+ };
39
+ mkdirSync(codexDir, { recursive: true });
40
+ writeFileSync(cfgPath, stringifyToml(fresh) + "\n");
41
+ return { patched: ["created ~/.codex/config.toml"] };
42
+ }
43
+ const originalText = readFileSync(cfgPath, "utf-8");
44
+ let cfg;
45
+ try {
46
+ cfg = parseToml(originalText);
47
+ } catch {
48
+ return { patched: ["~/.codex/config.toml could not be parsed; left untouched"] };
49
+ }
50
+ const mp = cfg.model_providers;
51
+ if (mp !== void 0 && (typeof mp !== "object" || Array.isArray(mp))) {
52
+ return {
53
+ patched: ["~/.codex/config.toml has model_providers that is not a table; left untouched"]
54
+ };
55
+ }
56
+ const needsTopLevel = cfg.model_provider === void 0;
57
+ const providers = mp ?? {};
58
+ const needsProvider = !providers["copilot-api"];
59
+ if (!needsTopLevel && !needsProvider) {
60
+ return { patched: [] };
61
+ }
62
+ const fragments = [];
63
+ const patched = [];
64
+ if (needsTopLevel) {
65
+ fragments.push(`model_provider = "copilot-api"`);
66
+ patched.push("set model_provider=copilot-api");
67
+ }
68
+ let updated = originalText;
69
+ if (fragments.length > 0) {
70
+ updated = fragments.join("\n") + "\n\n" + updated;
71
+ }
72
+ if (needsProvider) {
73
+ const sectionToml = stringifyToml({
74
+ model_providers: { "copilot-api": COPILOT_PROVIDER_DEFAULTS }
75
+ });
76
+ const sep = updated.endsWith("\n") ? "\n" : "\n\n";
77
+ updated = updated + sep + sectionToml + "\n";
78
+ patched.push("added [model_providers.copilot-api]");
79
+ }
80
+ writeFileSync(cfgPath, updated);
81
+ return { patched };
82
+ }
83
+ function patchClaudeConfig(opts) {
84
+ const claudeDir = join(opts.homeDir, ".claude");
85
+ const cfgPath = join(claudeDir, "settings.json");
86
+ if (existsSync(cfgPath)) {
87
+ try {
88
+ JSON.parse(readFileSync(cfgPath, "utf-8"));
89
+ return { patched: [] };
90
+ } catch {
91
+ return { patched: ["~/.claude/settings.json is not valid JSON; left untouched"] };
92
+ }
93
+ }
94
+ mkdirSync(claudeDir, { recursive: true });
95
+ writeFileSync(cfgPath, "{}\n");
96
+ return { patched: ["created ~/.claude/settings.json"] };
97
+ }
98
+ function detectEnvVar(name) {
99
+ const v = process.env[name];
100
+ return typeof v === "string" && v.length > 0;
101
+ }
102
+ async function defaultInstall(name) {
103
+ const pkg = name === "claude" ? "@anthropic-ai/claude-code" : "@openai/codex";
104
+ execFileSync("npm", ["install", "-g", pkg], { stdio: "inherit" });
105
+ }
106
+ async function bootstrapDeps(opts) {
107
+ const installFn = opts.installFn ?? defaultInstall;
108
+ const promptFn = opts.promptFn ?? (async () => "y");
109
+ const report = {
110
+ present: [],
111
+ missing: [],
112
+ installed: [],
113
+ configChanges: [],
114
+ envHints: []
115
+ };
116
+ const cliNames = ["claude", "codex"];
117
+ for (const name of cliNames) {
118
+ const r = detectCli(name, { whichFn: opts.whichFn });
119
+ if (r.found) report.present.push(name);
120
+ else report.missing.push(name);
121
+ }
122
+ if (opts.install && report.missing.length > 0) {
123
+ for (const name of report.missing) {
124
+ let shouldInstall = true;
125
+ if (opts.interactive) {
126
+ const ans = (await promptFn(`Install ${name} CLI via npm? [Y/n]: `)).trim().toLowerCase();
127
+ shouldInstall = ans === "" || ans === "y" || ans === "yes";
128
+ }
129
+ if (shouldInstall) {
130
+ await installFn(name);
131
+ report.installed.push(name);
132
+ }
133
+ }
134
+ }
135
+ const codexR = patchCodexConfig({ homeDir: opts.homeDir });
136
+ report.configChanges.push(...codexR.patched);
137
+ const claudeR = patchClaudeConfig({ homeDir: opts.homeDir });
138
+ report.configChanges.push(...claudeR.patched);
139
+ if (!detectEnvVar("COPILOT_API_KEY")) {
140
+ const shellHint = isWin ? "Set COPILOT_API_KEY in System \u2192 Environment Variables (any non-empty value, e.g. 'dummy')" : "Add to ~/.zshrc (or ~/.bashrc): export COPILOT_API_KEY=dummy";
141
+ report.envHints.push(
142
+ `COPILOT_API_KEY is not set. ${shellHint}. The copilot-api proxy does not validate the value, but codex requires the variable to exist.`
143
+ );
144
+ }
145
+ return report;
146
+ }
147
+ export {
148
+ bootstrapDeps,
149
+ detectCli,
150
+ detectEnvVar,
151
+ patchClaudeConfig,
152
+ patchCodexConfig
153
+ };
154
+ //# sourceMappingURL=bootstrap-deps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bootstrap-deps.ts"],"sourcesContent":["/**\n * bootstrap-deps — `clawnet-agent init` / `doctor` shared logic.\n *\n * Responsibilities (all idempotent, safe to re-run):\n * - detect whether claude / codex CLIs are installed (which / where)\n * - idempotent-merge ~/.codex/config.toml (default to copilot-api provider)\n * - idempotent-merge ~/.claude/settings.json (create if absent, never overwrite)\n * - detect env vars (COPILOT_API_KEY) and return user-facing hints when missing\n *\n * Side-effects deliberately NOT taken:\n * - never modifies ~/.zshrc / ~/.bashrc / Windows env — dotfiles are sacred.\n * Missing env is reported via `envHints[]` so the caller can print them.\n * - never overwrites user-set TOML / JSON values; only adds missing keys.\n * - when patching an EXISTING TOML file we never round-trip through\n * stringify (smol-toml drops comments). We parse only to detect which\n * keys are missing, then append the missing bits as raw text so user\n * comments and key order survive untouched.\n *\n * Network / subprocess side-effects (npm install) are injected via `installFn`\n * so tests can verify call shape without actually spawning npm.\n */\nimport { execFileSync } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { platform } from \"node:os\";\nimport { join } from \"node:path\";\nimport { parse as parseToml, stringify as stringifyToml } from \"smol-toml\";\n\nconst isWin = platform() === \"win32\";\n\nexport interface DetectResult {\n found: boolean;\n path?: string;\n}\n\nexport interface DetectOptions {\n /** Override which/where lookup. Tests inject a fake; runtime defaults to execFileSync. */\n whichFn?: (bin: string) => string;\n}\n\nexport function detectCli(bin: string, opts: DetectOptions = {}): DetectResult {\n const whichFn =\n opts.whichFn ??\n ((b: string) => {\n const cmd = isWin ? \"where\" : \"which\";\n // execFileSync (not execSync) — pass `b` as an arg, never interpolate\n // into a shell string. Today callers pass only literal \"claude\"/\"codex\"\n // but the signature accepts any string; using execFile is defense in\n // depth so a future caller passing user input can't smuggle a shell\n // metacharacter and turn this into a command-injection sink.\n return execFileSync(cmd, [b], {\n encoding: \"utf-8\",\n timeout: 1500,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n })\n .trim()\n .split(/\\r?\\n/)[0];\n });\n try {\n const path = whichFn(bin);\n if (path) return { found: true, path };\n return { found: false };\n } catch {\n return { found: false };\n }\n}\n\nexport interface PatchResult {\n /** Human-readable change log entries; empty array = no changes. */\n patched: string[];\n}\n\nexport interface PatchOptions {\n homeDir: string;\n}\n\nconst COPILOT_PROVIDER_DEFAULTS = {\n name: \"copilot-api\",\n base_url: \"http://localhost:4141/v1\",\n wire_api: \"responses\",\n env_key: \"COPILOT_API_KEY\",\n} as const;\n\n/**\n * Merge defaults into ~/.codex/config.toml.\n *\n * Two paths:\n * - File absent → write a fresh stringified TOML with our defaults.\n * - File exists → parse to detect missing keys, then APPEND missing\n * fragments as raw text. We never re-stringify the parsed AST, because\n * smol-toml's stringify drops comments and may reorder keys (verified\n * by regression test). Appending preserves the user's original bytes.\n *\n * Never touches user-set model_provider / model / per-provider fields.\n */\nexport function patchCodexConfig(opts: PatchOptions): PatchResult {\n const codexDir = join(opts.homeDir, \".codex\");\n const cfgPath = join(codexDir, \"config.toml\");\n\n // Fresh-write path.\n if (!existsSync(cfgPath)) {\n const fresh = {\n model_provider: \"copilot-api\",\n model_providers: { \"copilot-api\": COPILOT_PROVIDER_DEFAULTS },\n };\n mkdirSync(codexDir, { recursive: true });\n writeFileSync(cfgPath, stringifyToml(fresh) + \"\\n\");\n return { patched: [\"created ~/.codex/config.toml\"] };\n }\n\n // Existing-file path: parse-then-append-raw.\n const originalText = readFileSync(cfgPath, \"utf-8\");\n let cfg: Record<string, unknown>;\n try {\n cfg = parseToml(originalText) as Record<string, unknown>;\n } catch {\n return { patched: [\"~/.codex/config.toml could not be parsed; left untouched\"] };\n }\n\n // Type-guard model_providers: smol-toml will faithfully parse a wrong-typed\n // `model_providers = \"string\"` as a string, and our previous .cast-and-poke\n // approach would silently overwrite it. Bail loudly instead.\n const mp = cfg.model_providers;\n if (mp !== undefined && (typeof mp !== \"object\" || Array.isArray(mp))) {\n return {\n patched: [\"~/.codex/config.toml has model_providers that is not a table; left untouched\"],\n };\n }\n\n const needsTopLevel = cfg.model_provider === undefined;\n const providers = (mp ?? {}) as Record<string, unknown>;\n const needsProvider = !providers[\"copilot-api\"];\n\n if (!needsTopLevel && !needsProvider) {\n return { patched: [] };\n }\n\n const fragments: string[] = [];\n const patched: string[] = [];\n\n if (needsTopLevel) {\n // Top-level scalar key must precede any [section] header to remain\n // top-level. The safest place to insert is at the very top of the file.\n // We accumulate scalar additions and prepend them in a single splice.\n fragments.push(`model_provider = \"copilot-api\"`);\n patched.push(\"set model_provider=copilot-api\");\n }\n\n let updated = originalText;\n if (fragments.length > 0) {\n updated = fragments.join(\"\\n\") + \"\\n\\n\" + updated;\n }\n\n if (needsProvider) {\n const sectionToml = stringifyToml({\n model_providers: { \"copilot-api\": COPILOT_PROVIDER_DEFAULTS },\n });\n const sep = updated.endsWith(\"\\n\") ? \"\\n\" : \"\\n\\n\";\n updated = updated + sep + sectionToml + \"\\n\";\n patched.push(\"added [model_providers.copilot-api]\");\n }\n\n writeFileSync(cfgPath, updated);\n return { patched };\n}\n\n/**\n * Ensure ~/.claude/settings.json exists.\n *\n * Currently a thin presence-check: claude-code's own config schema evolves\n * fast and we don't want to fight it. Just create an empty object if absent\n * so downstream tools can rely on the file's existence.\n */\nexport function patchClaudeConfig(opts: PatchOptions): PatchResult {\n const claudeDir = join(opts.homeDir, \".claude\");\n const cfgPath = join(claudeDir, \"settings.json\");\n if (existsSync(cfgPath)) {\n // Validate it parses; if not, surface but don't rewrite.\n try {\n JSON.parse(readFileSync(cfgPath, \"utf-8\"));\n return { patched: [] };\n } catch {\n return { patched: [\"~/.claude/settings.json is not valid JSON; left untouched\"] };\n }\n }\n mkdirSync(claudeDir, { recursive: true });\n writeFileSync(cfgPath, \"{}\\n\");\n return { patched: [\"created ~/.claude/settings.json\"] };\n}\n\nexport function detectEnvVar(name: string): boolean {\n const v = process.env[name];\n return typeof v === \"string\" && v.length > 0;\n}\n\nexport interface BootstrapOptions {\n homeDir: string;\n /** When true, prompt user before installing CLIs. */\n interactive: boolean;\n /** When false, never install (doctor mode). */\n install: boolean;\n whichFn?: (bin: string) => string;\n installFn?: (cliName: \"claude\" | \"codex\") => Promise<void>;\n promptFn?: (question: string) => Promise<string>;\n}\n\nexport interface BootstrapReport {\n /** CLIs that were already installed. */\n present: string[];\n /** CLIs missing at start. */\n missing: string[];\n /** CLIs installed during this run. */\n installed: string[];\n /** Config patcher change logs (codex + claude). */\n configChanges: string[];\n /** Hint strings to print to user about env vars that need attention. */\n envHints: string[];\n}\n\n/**\n * Default npm-based install. Kept here (not in detect.ts) so production\n * callers don't have to wire it; tests inject their own spy.\n *\n * Why npm and not brew/cargo/winget: we picked one source per CLI to keep\n * the matrix small. claude-code is npm-only anyway, and codex npm package\n * works on both macOS and Windows (our codex-adapter already handles the\n * Windows .cmd shim).\n */\nasync function defaultInstall(name: \"claude\" | \"codex\"): Promise<void> {\n const pkg = name === \"claude\" ? \"@anthropic-ai/claude-code\" : \"@openai/codex\";\n // Inherit stdio so the user sees npm's progress; init is interactive.\n // execFileSync with explicit args — never interpolate the package name\n // into a shell string even though it's hardcoded today.\n execFileSync(\"npm\", [\"install\", \"-g\", pkg], { stdio: \"inherit\" });\n}\n\nexport async function bootstrapDeps(opts: BootstrapOptions): Promise<BootstrapReport> {\n const installFn = opts.installFn ?? defaultInstall;\n const promptFn = opts.promptFn ?? (async () => \"y\");\n\n const report: BootstrapReport = {\n present: [],\n missing: [],\n installed: [],\n configChanges: [],\n envHints: [],\n };\n\n // 1. Detect CLIs\n const cliNames: Array<\"claude\" | \"codex\"> = [\"claude\", \"codex\"];\n for (const name of cliNames) {\n const r = detectCli(name, { whichFn: opts.whichFn });\n if (r.found) report.present.push(name);\n else report.missing.push(name);\n }\n\n // 2. Optionally install missing\n if (opts.install && report.missing.length > 0) {\n for (const name of report.missing as Array<\"claude\" | \"codex\">) {\n let shouldInstall = true;\n if (opts.interactive) {\n const ans = (await promptFn(`Install ${name} CLI via npm? [Y/n]: `)).trim().toLowerCase();\n // Default Y (empty input → yes); only explicit \"n\"/\"no\" skips.\n shouldInstall = ans === \"\" || ans === \"y\" || ans === \"yes\";\n }\n if (shouldInstall) {\n await installFn(name);\n report.installed.push(name);\n }\n }\n }\n\n // 3. Patch configs (always — they're idempotent)\n const codexR = patchCodexConfig({ homeDir: opts.homeDir });\n report.configChanges.push(...codexR.patched);\n const claudeR = patchClaudeConfig({ homeDir: opts.homeDir });\n report.configChanges.push(...claudeR.patched);\n\n // 4. Env hints (no dotfile mutation — print only)\n if (!detectEnvVar(\"COPILOT_API_KEY\")) {\n const shellHint = isWin\n ? \"Set COPILOT_API_KEY in System → Environment Variables (any non-empty value, e.g. 'dummy')\"\n : \"Add to ~/.zshrc (or ~/.bashrc): export COPILOT_API_KEY=dummy\";\n report.envHints.push(\n `COPILOT_API_KEY is not set. ${shellHint}. The copilot-api proxy does not validate the value, but codex requires the variable to exist.`,\n );\n }\n\n return report;\n}\n"],"mappings":"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","names":[]}
@@ -2281,6 +2281,12 @@ var SessionManager = class {
2281
2281
  // handler. A leftover entry would only matter if a session id were reused,
2282
2282
  // which createSession already forbids (line 193 throws on duplicate).
2283
2283
  expectedExits = /* @__PURE__ */ new Set();
2284
+ // Most recent adapter-emitted error per session, used to enrich the
2285
+ // otherwise opaque "exit code=N" reason that swarm coordinator's crash
2286
+ // inbox surfaces. Entries are auto-pruned on session exit and ignored if
2287
+ // older than 5s (so a stale entry from a long-lived session doesn't
2288
+ // misattribute a later unrelated exit).
2289
+ lastSessionError = /* @__PURE__ */ new Map();
2284
2290
  idleSweepTimer = null;
2285
2291
  // PR-A: effective sweeper config. Initialized from env at construct time
2286
2292
  // and overridable per-instance via startIdleSweeper(overrides) — the
@@ -2487,6 +2493,7 @@ ${notice.text}`;
2487
2493
  });
2488
2494
  spawnAdapter.onError?.(process2, (error) => {
2489
2495
  this.activelyExecuting.delete(options.sessionId);
2496
+ this.lastSessionError.set(options.sessionId, { message: error.message, ts: Date.now() });
2490
2497
  this.onSessionError(options.sessionId, error.message);
2491
2498
  });
2492
2499
  spawnAdapter.onTokenBudgetWarning?.(process2, (info) => {
@@ -2573,6 +2580,10 @@ ${notice.text}`;
2573
2580
  });
2574
2581
  }
2575
2582
  spawnAdapter.onExit?.(process2, (code) => {
2583
+ const lastErr = this.lastSessionError.get(options.sessionId);
2584
+ this.lastSessionError.delete(options.sessionId);
2585
+ const enriched = lastErr && Date.now() - lastErr.ts < 5e3 ? lastErr.message : void 0;
2586
+ const reasonText = enriched ? `exit code=${code ?? "null"} \u2014 ${enriched}` : `exit code=${code ?? "null"}`;
2576
2587
  if (this.sessions.get(options.sessionId) === process2) {
2577
2588
  const expected = this.expectedExits.delete(options.sessionId);
2578
2589
  this.sessions.delete(options.sessionId);
@@ -2581,7 +2592,7 @@ ${notice.text}`;
2581
2592
  this.conversationBuffer.delete(options.sessionId);
2582
2593
  this.scheduleCheckpoint();
2583
2594
  if (!expected) {
2584
- log5.warn({ sessionId: options.sessionId, exitCode: code }, "backend process exited unexpectedly, evicted from session map");
2595
+ log5.warn({ sessionId: options.sessionId, exitCode: code, reason: enriched }, "backend process exited unexpectedly, evicted from session map");
2585
2596
  this.onSessionError(options.sessionId, `backend process exited (code=${code ?? "null"})`);
2586
2597
  } else {
2587
2598
  log5.debug({ sessionId: options.sessionId, exitCode: code }, "backend process exited as expected");
@@ -2590,7 +2601,7 @@ ${notice.text}`;
2590
2601
  this.onSessionExit?.(options.sessionId, {
2591
2602
  code: code ?? null,
2592
2603
  expected,
2593
- ...expected ? {} : { reason: `exit code=${code ?? "null"}` }
2604
+ ...expected ? {} : { reason: reasonText }
2594
2605
  });
2595
2606
  } catch (err) {
2596
2607
  log5.warn({ err, sessionId: options.sessionId }, "onSessionExit listener threw");
@@ -2965,8 +2976,15 @@ ${content}`;
2965
2976
  }
2966
2977
  };
2967
2978
 
2979
+ // src/runtime-env-defaults.ts
2980
+ function ensureRuntimeEnvDefaults() {
2981
+ if (!process.env.COPILOT_API_KEY) {
2982
+ process.env.COPILOT_API_KEY = "dummy";
2983
+ }
2984
+ }
2985
+
2968
2986
  // src/start.ts
2969
- import { SwarmCoordinator, initRoles } from "@mclawnet/swarm";
2987
+ import { SwarmCoordinator, initRoles, WakeupScheduler, listRecoverableSwarmIds as listRecoverableSwarmIds2 } from "@mclawnet/swarm";
2970
2988
  import { TaskStore as TaskStore2 } from "@mclawnet/task";
2971
2989
 
2972
2990
  // src/brain-bridge.ts
@@ -3485,6 +3503,7 @@ import {
3485
3503
  import { PROJECTS_RPC_CAPABILITY } from "@mclawnet/shared";
3486
3504
  var log10 = createLogger10({ module: "agent" });
3487
3505
  async function startAgent(options) {
3506
+ ensureRuntimeEnvDefaults();
3488
3507
  const config = loadConfig(options.config);
3489
3508
  if (!config.token) {
3490
3509
  log10.error("no token configured \u2014 set CLAWNET_TOKEN or use --token");
@@ -3586,7 +3605,7 @@ async function startAgent(options) {
3586
3605
  const sessionManager = new SessionManager({
3587
3606
  adapter: options.adapter,
3588
3607
  resolveAdapter: async (kind) => {
3589
- const { createBackendAdapter } = await import("./backend-factory-RUYUBJVF.js");
3608
+ const { createBackendAdapter } = await import("./backend-factory-VRPU3534.js");
3590
3609
  return createBackendAdapter(kind);
3591
3610
  },
3592
3611
  onOutput: (sessionId, data) => {
@@ -3667,6 +3686,17 @@ async function startAgent(options) {
3667
3686
  }, process.env.CLAWNET_HOME ?? homedir5());
3668
3687
  hub.setSessionManager(sessionManager);
3669
3688
  hub.setSwarmCoordinator(swarmCoordinator);
3689
+ const wakeupHome = process.env.CLAWNET_HOME ?? homedir5();
3690
+ const wakeupScheduler = new WakeupScheduler(swarmCoordinator.inboxRelay);
3691
+ try {
3692
+ const knownSwarms = listRecoverableSwarmIds2().map(({ swarmId, workDir }) => ({
3693
+ swarmId,
3694
+ workDir
3695
+ }));
3696
+ await wakeupScheduler.restoreFromInbox(wakeupHome, knownSwarms);
3697
+ } catch (err) {
3698
+ log10.warn({ err }, "WakeupScheduler.restoreFromInbox failed (non-fatal)");
3699
+ }
3670
3700
  const scheduleRuntime = new ScheduleRuntime({ hub, sessionManager, swarmCoordinator });
3671
3701
  hub.setScheduleRuntime(scheduleRuntime);
3672
3702
  await scheduleRuntime.start();
@@ -3676,6 +3706,7 @@ async function startAgent(options) {
3676
3706
  sessionManager.startIdleSweeper();
3677
3707
  const shutdown = async () => {
3678
3708
  log10.info("shutting down");
3709
+ wakeupScheduler.dispose();
3679
3710
  await sessionManager.closeAll();
3680
3711
  await scheduleRuntime.stop();
3681
3712
  hub.destroy();
@@ -3701,4 +3732,4 @@ export {
3701
3732
  FsBridge,
3702
3733
  startAgent
3703
3734
  };
3704
- //# sourceMappingURL=chunk-2JDX6XFD.js.map
3735
+ //# sourceMappingURL=chunk-B733MQCA.js.map