@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.
- package/cli.js +75 -7
- package/dist/__tests__/bootstrap-deps.test.d.ts +2 -0
- package/dist/__tests__/bootstrap-deps.test.d.ts.map +1 -0
- package/dist/__tests__/collect-manifest.test.d.ts +2 -0
- package/dist/__tests__/collect-manifest.test.d.ts.map +1 -0
- package/dist/__tests__/hub-connection-on-activity.test.d.ts +2 -0
- package/dist/__tests__/hub-connection-on-activity.test.d.ts.map +1 -0
- package/dist/__tests__/hub-connection-wake-watch.test.d.ts +2 -0
- package/dist/__tests__/hub-connection-wake-watch.test.d.ts.map +1 -0
- package/dist/__tests__/ideas-rest-client.test.d.ts +2 -0
- package/dist/__tests__/ideas-rest-client.test.d.ts.map +1 -0
- package/dist/__tests__/legacy-claude-execute-compat.test.d.ts +2 -0
- package/dist/__tests__/legacy-claude-execute-compat.test.d.ts.map +1 -0
- package/dist/__tests__/no-adapter-cycle.test.d.ts +2 -0
- package/dist/__tests__/no-adapter-cycle.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-env-defaults.test.d.ts +2 -0
- package/dist/__tests__/runtime-env-defaults.test.d.ts.map +1 -0
- package/dist/__tests__/session-manager-exit-reason.test.d.ts +2 -0
- package/dist/__tests__/session-manager-exit-reason.test.d.ts.map +1 -0
- package/dist/__tests__/session-manager-merge.test.d.ts +2 -0
- package/dist/__tests__/session-manager-merge.test.d.ts.map +1 -0
- package/dist/__tests__/session-manager-sticky.test.d.ts +2 -0
- package/dist/__tests__/session-manager-sticky.test.d.ts.map +1 -0
- package/dist/__tests__/session-protocol-dispatch.test.d.ts +2 -0
- package/dist/__tests__/session-protocol-dispatch.test.d.ts.map +1 -0
- package/dist/__tests__/worktree-bridge.test.d.ts +2 -0
- package/dist/__tests__/worktree-bridge.test.d.ts.map +1 -0
- package/dist/backend-adapter.d.ts +6 -232
- package/dist/backend-adapter.d.ts.map +1 -1
- package/dist/backend-factory-AFF6I7YF.js +11 -0
- package/dist/backend-factory.d.ts +23 -1
- package/dist/backend-factory.d.ts.map +1 -1
- package/dist/bootstrap-deps.d.ts +84 -0
- package/dist/bootstrap-deps.d.ts.map +1 -0
- package/dist/bootstrap-deps.js +173 -0
- package/dist/bootstrap-deps.js.map +1 -0
- package/dist/{chunk-PJ5M6Q36.js → chunk-376QZ7JB.js} +2 -2
- package/dist/chunk-376QZ7JB.js.map +1 -0
- package/dist/{chunk-2JDX6XFD.js → chunk-GOCWMRBB.js} +1817 -298
- package/dist/chunk-GOCWMRBB.js.map +1 -0
- package/dist/{chunk-M2CDVPQF.js → chunk-JH6RGJBQ.js} +2 -2
- package/dist/{chunk-MFXF77LG.js → chunk-VAEFJLPL.js} +25 -3
- package/dist/chunk-VAEFJLPL.js.map +1 -0
- package/dist/{dist-VLBO5CT3.js → dist-NWVHAP5R.js} +330 -23
- package/dist/dist-NWVHAP5R.js.map +1 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/hub-connection.d.ts +25 -1
- package/dist/hub-connection.d.ts.map +1 -1
- package/dist/ideas-rest-client.d.ts +25 -0
- package/dist/ideas-rest-client.d.ts.map +1 -0
- package/dist/index.js +3 -3
- package/dist/{linux-IHA4O633.js → linux-MBU6ERXL.js} +3 -3
- package/dist/{macos-G4VK2253.js → macos-I2DUWFUH.js} +3 -3
- package/dist/projects-handler.d.ts +146 -1
- package/dist/projects-handler.d.ts.map +1 -1
- package/dist/runtime-env-defaults.d.ts +18 -0
- package/dist/runtime-env-defaults.d.ts.map +1 -0
- package/dist/service/index.js +5 -5
- package/dist/session-manager.d.ts +59 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +3 -2
- package/dist/{windows-P6U3JLUZ.js → windows-PEJ3KOLC.js} +3 -3
- package/dist/worktree-bridge.d.ts +51 -0
- package/dist/worktree-bridge.d.ts.map +1 -0
- package/package.json +10 -8
- package/dist/backend-factory-RUYUBJVF.js +0 -9
- package/dist/chunk-2JDX6XFD.js.map +0 -1
- package/dist/chunk-MFXF77LG.js.map +0 -1
- package/dist/chunk-PJ5M6Q36.js.map +0 -1
- package/dist/dist-VLBO5CT3.js.map +0 -1
- /package/dist/{backend-factory-RUYUBJVF.js.map → backend-factory-AFF6I7YF.js.map} +0 -0
- /package/dist/{chunk-M2CDVPQF.js.map → chunk-JH6RGJBQ.js.map} +0 -0
- /package/dist/{linux-IHA4O633.js.map → linux-MBU6ERXL.js.map} +0 -0
- /package/dist/{macos-G4VK2253.js.map → macos-I2DUWFUH.js.map} +0 -0
- /package/dist/{windows-P6U3JLUZ.js.map → windows-PEJ3KOLC.js.map} +0 -0
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
function detectActiveCodexProvider(homeDir) {
|
|
103
|
+
const cfgPath = join(homeDir, ".codex", "config.toml");
|
|
104
|
+
if (!existsSync(cfgPath)) return void 0;
|
|
105
|
+
try {
|
|
106
|
+
const cfg = parseToml(readFileSync(cfgPath, "utf-8"));
|
|
107
|
+
const v = cfg.model_provider;
|
|
108
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
109
|
+
} catch {
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function buildCopilotApiEnvHint(opts) {
|
|
114
|
+
if (opts.platform === "win32") {
|
|
115
|
+
return "COPILOT_API_KEY is not set, and your active codex provider is copilot-api.\n Persist it (takes effect in NEW shells):\n setx COPILOT_API_KEY dummy\n Current PowerShell session only:\n $env:COPILOT_API_KEY = 'dummy'\n The copilot-api proxy does not validate the value (any non-empty string works); codex just needs the variable to exist.";
|
|
116
|
+
}
|
|
117
|
+
return "COPILOT_API_KEY is not set, and your active codex provider is copilot-api.\n Add to ~/.zshrc (or ~/.bashrc) and reload:\n export COPILOT_API_KEY=dummy\n Current shell only:\n export COPILOT_API_KEY=dummy\n The copilot-api proxy does not validate the value (any non-empty string works); codex just needs the variable to exist.";
|
|
118
|
+
}
|
|
119
|
+
async function defaultInstall(name) {
|
|
120
|
+
const pkg = name === "claude" ? "@anthropic-ai/claude-code" : "@openai/codex";
|
|
121
|
+
execFileSync("npm", ["install", "-g", pkg], { stdio: "inherit" });
|
|
122
|
+
}
|
|
123
|
+
async function bootstrapDeps(opts) {
|
|
124
|
+
const installFn = opts.installFn ?? defaultInstall;
|
|
125
|
+
const promptFn = opts.promptFn ?? (async () => "y");
|
|
126
|
+
const report = {
|
|
127
|
+
present: [],
|
|
128
|
+
missing: [],
|
|
129
|
+
installed: [],
|
|
130
|
+
configChanges: [],
|
|
131
|
+
envHints: []
|
|
132
|
+
};
|
|
133
|
+
const cliNames = ["claude", "codex"];
|
|
134
|
+
for (const name of cliNames) {
|
|
135
|
+
const r = detectCli(name, { whichFn: opts.whichFn });
|
|
136
|
+
if (r.found) report.present.push(name);
|
|
137
|
+
else report.missing.push(name);
|
|
138
|
+
}
|
|
139
|
+
if (opts.install && report.missing.length > 0) {
|
|
140
|
+
for (const name of report.missing) {
|
|
141
|
+
let shouldInstall = true;
|
|
142
|
+
if (opts.interactive) {
|
|
143
|
+
const ans = (await promptFn(`Install ${name} CLI via npm? [Y/n]: `)).trim().toLowerCase();
|
|
144
|
+
shouldInstall = ans === "" || ans === "y" || ans === "yes";
|
|
145
|
+
}
|
|
146
|
+
if (shouldInstall) {
|
|
147
|
+
await installFn(name);
|
|
148
|
+
report.installed.push(name);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const codexR = patchCodexConfig({ homeDir: opts.homeDir });
|
|
153
|
+
report.configChanges.push(...codexR.patched);
|
|
154
|
+
const claudeR = patchClaudeConfig({ homeDir: opts.homeDir });
|
|
155
|
+
report.configChanges.push(...claudeR.patched);
|
|
156
|
+
if (!detectEnvVar("COPILOT_API_KEY")) {
|
|
157
|
+
const activeProvider = detectActiveCodexProvider(opts.homeDir);
|
|
158
|
+
if (activeProvider === "copilot-api") {
|
|
159
|
+
report.envHints.push(buildCopilotApiEnvHint({ platform: process.platform }));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return report;
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
bootstrapDeps,
|
|
166
|
+
buildCopilotApiEnvHint,
|
|
167
|
+
detectActiveCodexProvider,
|
|
168
|
+
detectCli,
|
|
169
|
+
detectEnvVar,
|
|
170
|
+
patchClaudeConfig,
|
|
171
|
+
patchCodexConfig
|
|
172
|
+
};
|
|
173
|
+
//# 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\n/**\n * Read ~/.codex/config.toml and return the currently-active model_provider\n * string. Returns undefined when the file is absent, malformed, or has no\n * model_provider key set.\n *\n * Used by bootstrapDeps to gate provider-specific env-var hints — we only\n * warn about COPILOT_API_KEY when the user actually picked copilot-api.\n */\nexport function detectActiveCodexProvider(homeDir: string): string | undefined {\n const cfgPath = join(homeDir, \".codex\", \"config.toml\");\n if (!existsSync(cfgPath)) return undefined;\n try {\n const cfg = parseToml(readFileSync(cfgPath, \"utf-8\")) as Record<string, unknown>;\n const v = cfg.model_provider;\n return typeof v === \"string\" && v.length > 0 ? v : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Build a platform-specific user-facing instruction for setting\n * COPILOT_API_KEY. The copilot-api proxy (the localhost:4141 forwarder)\n * does not validate the bearer token — any non-empty string works — but\n * codex's env_key check fails hard if the variable is missing. So the hint\n * has to tell the user exactly which shell command to run.\n *\n * Platform split kept here (not at the call site) so future locales / new\n * platforms can extend the matrix without touching bootstrapDeps logic.\n */\nexport function buildCopilotApiEnvHint(opts: { platform: NodeJS.Platform | string }): string {\n if (opts.platform === \"win32\") {\n return (\n \"COPILOT_API_KEY is not set, and your active codex provider is copilot-api.\\n\" +\n \" Persist it (takes effect in NEW shells):\\n\" +\n \" setx COPILOT_API_KEY dummy\\n\" +\n \" Current PowerShell session only:\\n\" +\n \" $env:COPILOT_API_KEY = 'dummy'\\n\" +\n \" The copilot-api proxy does not validate the value (any non-empty string works); codex just needs the variable to exist.\"\n );\n }\n return (\n \"COPILOT_API_KEY is not set, and your active codex provider is copilot-api.\\n\" +\n \" Add to ~/.zshrc (or ~/.bashrc) and reload:\\n\" +\n \" export COPILOT_API_KEY=dummy\\n\" +\n \" Current shell only:\\n\" +\n \" export COPILOT_API_KEY=dummy\\n\" +\n \" The copilot-api proxy does not validate the value (any non-empty string works); codex just needs the variable to exist.\"\n );\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). Provider-aware: only\n // warn about COPILOT_API_KEY when the user's active codex provider is\n // actually copilot-api. Otherwise the warning is noise (users on\n // openai / claude / etc. would see a misleading hint about a variable\n // they don't need). Read the *post-patch* config so a fresh-write that\n // just defaulted to copilot-api is detected correctly.\n if (!detectEnvVar(\"COPILOT_API_KEY\")) {\n const activeProvider = detectActiveCodexProvider(opts.homeDir);\n if (activeProvider === \"copilot-api\") {\n report.envHints.push(buildCopilotApiEnvHint({ platform: process.platform }));\n }\n }\n\n return report;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -11,7 +11,7 @@ var DEFAULTS = {
|
|
|
11
11
|
hubUrl: DEFAULT_HUB_URL,
|
|
12
12
|
token: "",
|
|
13
13
|
name: hostname(),
|
|
14
|
-
backendType: "claude
|
|
14
|
+
backendType: "claude"
|
|
15
15
|
};
|
|
16
16
|
function normalizeHubUrl(url) {
|
|
17
17
|
url = url.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
|
|
@@ -90,4 +90,4 @@ export {
|
|
|
90
90
|
loadConfig,
|
|
91
91
|
saveConfig
|
|
92
92
|
};
|
|
93
|
-
//# sourceMappingURL=chunk-
|
|
93
|
+
//# sourceMappingURL=chunk-376QZ7JB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir, hostname } from \"node:os\";\nimport type { BackendType } from \"@mclawnet/shared\";\nimport { createLogger } from \"@mclawnet/logger\";\n\nconst log = createLogger({ module: \"config\" });\n\nexport interface EmbeddingConfig {\n /** Embedding provider mode: auto / openai / ollama / hash */\n provider?: string;\n /** OpenAI-compatible API base URL (e.g. http://localhost:4141/v1 for copilot proxy) */\n openaiBaseUrl?: string;\n /** OpenAI API key (use \"dummy\" for copilot proxy) */\n openaiApiKey?: string;\n /** OpenAI embedding model name */\n openaiModel?: string;\n /** Ollama server URL */\n ollamaUrl?: string;\n /** Ollama embedding model name */\n ollamaModel?: string;\n}\n\nexport interface AgentConfig {\n hubUrl: string;\n token: string;\n name: string;\n backendType: BackendType;\n embedding?: EmbeddingConfig;\n}\n\nconst CONFIG_DIR = join(homedir(), \".clawnet\");\nconst SETTINGS_FILE = join(CONFIG_DIR, \"settings.json\");\n\nconst DEFAULT_HUB_URL = process.env.CLAWNET_DEFAULT_HUB_URL || \"wss://mclaw.work/ws/agent\";\n\nconst DEFAULTS: AgentConfig = {\n hubUrl: DEFAULT_HUB_URL,\n token: \"\",\n name: hostname(),\n backendType: \"claude\",\n};\n\n/** Ensure hubUrl uses ws(s):// protocol and ends with /ws/agent */\nfunction normalizeHubUrl(url: string): string {\n // Convert http(s):// to ws(s)://\n url = url.replace(/^https:\\/\\//, \"wss://\").replace(/^http:\\/\\//, \"ws://\");\n // Default to wss:// if no protocol\n if (!/^wss?:\\/\\//.test(url)) url = \"wss://\" + url;\n if (url.endsWith(\"/ws/agent\")) return url;\n return url.replace(/\\/+$/, \"\") + \"/ws/agent\";\n}\n\n/**\n * Map structured embedding config to CLAWNET_* env vars.\n * Only sets vars that are not already defined in process.env.\n */\nfunction applyEmbeddingConfig(embedding: EmbeddingConfig): void {\n const mapping: Array<[keyof EmbeddingConfig, string]> = [\n [\"provider\", \"CLAWNET_EMBEDDING_PROVIDER\"],\n [\"openaiBaseUrl\", \"CLAWNET_OPENAI_BASE_URL\"],\n [\"openaiApiKey\", \"CLAWNET_OPENAI_API_KEY\"],\n [\"openaiModel\", \"CLAWNET_OPENAI_EMBEDDING_MODEL\"],\n [\"ollamaUrl\", \"CLAWNET_OLLAMA_URL\"],\n [\"ollamaModel\", \"CLAWNET_OLLAMA_MODEL\"],\n ];\n\n for (const [configKey, envKey] of mapping) {\n const value = embedding[configKey];\n if (value !== undefined && process.env[envKey] === undefined) {\n process.env[envKey] = value;\n }\n }\n}\n\n/** Load config: CLI opts > env vars > settings file > defaults */\nexport function loadConfig(cliOpts: Partial<AgentConfig> = {}): AgentConfig {\n let fileConfig: Partial<AgentConfig> = {};\n\n log.info({ path: SETTINGS_FILE }, \"loading config\");\n\n if (existsSync(SETTINGS_FILE)) {\n try {\n fileConfig = JSON.parse(readFileSync(SETTINGS_FILE, \"utf-8\"));\n log.info(\n { hubUrl: fileConfig.hubUrl, hasToken: !!fileConfig.token, name: fileConfig.name },\n \"settings.json loaded\"\n );\n } catch (e) {\n log.warn({ err: e }, \"failed to parse settings.json\");\n }\n } else {\n log.warn(\"settings.json not found\");\n }\n\n // Apply structured embedding config to process.env\n if (fileConfig.embedding) {\n applyEmbeddingConfig(fileConfig.embedding);\n }\n\n const hubUrl =\n cliOpts.hubUrl ??\n process.env.CLAWNET_HUB_URL ??\n fileConfig.hubUrl ??\n DEFAULTS.hubUrl;\n\n const resolved = {\n hubUrl: normalizeHubUrl(hubUrl),\n token:\n cliOpts.token ??\n process.env.CLAWNET_TOKEN ??\n fileConfig.token ??\n DEFAULTS.token,\n name:\n cliOpts.name ??\n process.env.CLAWNET_NAME ??\n fileConfig.name ??\n DEFAULTS.name,\n backendType:\n (cliOpts.backendType as BackendType) ??\n (process.env.CLAWNET_BACKEND_TYPE as BackendType) ??\n (fileConfig.backendType as BackendType) ??\n DEFAULTS.backendType,\n embedding: fileConfig.embedding,\n };\n\n log.info(\n {\n hubUrl: resolved.hubUrl,\n hubUrlSource: cliOpts.hubUrl ? \"cli\" : process.env.CLAWNET_HUB_URL ? \"env\" : fileConfig.hubUrl ? \"file\" : \"default\",\n tokenSource: cliOpts.token ? \"cli\" : process.env.CLAWNET_TOKEN ? \"env\" : fileConfig.token ? \"file\" : \"default\",\n hasToken: !!resolved.token,\n },\n \"config resolved\"\n );\n\n return resolved;\n}\n\n/** Save config to ~/.clawnet/settings.json */\nexport function saveConfig(config: Partial<AgentConfig>): void {\n mkdirSync(CONFIG_DIR, { recursive: true });\n\n let existing: Partial<AgentConfig> = {};\n if (existsSync(SETTINGS_FILE)) {\n try {\n existing = JSON.parse(readFileSync(SETTINGS_FILE, \"utf-8\"));\n } catch {\n // ignore\n }\n }\n\n const merged = { ...existing, ...config };\n writeFileSync(SETTINGS_FILE, JSON.stringify(merged, null, 2) + \"\\n\");\n}\n"],"mappings":";AAAA,SAAS,cAAc,eAAe,WAAW,kBAAkB;AACnE,SAAS,YAAY;AACrB,SAAS,SAAS,gBAAgB;AAElC,SAAS,oBAAoB;AAE7B,IAAM,MAAM,aAAa,EAAE,QAAQ,SAAS,CAAC;AAyB7C,IAAM,aAAa,KAAK,QAAQ,GAAG,UAAU;AAC7C,IAAM,gBAAgB,KAAK,YAAY,eAAe;AAEtD,IAAM,kBAAkB,QAAQ,IAAI,2BAA2B;AAE/D,IAAM,WAAwB;AAAA,EAC5B,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM,SAAS;AAAA,EACf,aAAa;AACf;AAGA,SAAS,gBAAgB,KAAqB;AAE5C,QAAM,IAAI,QAAQ,eAAe,QAAQ,EAAE,QAAQ,cAAc,OAAO;AAExE,MAAI,CAAC,aAAa,KAAK,GAAG,EAAG,OAAM,WAAW;AAC9C,MAAI,IAAI,SAAS,WAAW,EAAG,QAAO;AACtC,SAAO,IAAI,QAAQ,QAAQ,EAAE,IAAI;AACnC;AAMA,SAAS,qBAAqB,WAAkC;AAC9D,QAAM,UAAkD;AAAA,IACtD,CAAC,YAAY,4BAA4B;AAAA,IACzC,CAAC,iBAAiB,yBAAyB;AAAA,IAC3C,CAAC,gBAAgB,wBAAwB;AAAA,IACzC,CAAC,eAAe,gCAAgC;AAAA,IAChD,CAAC,aAAa,oBAAoB;AAAA,IAClC,CAAC,eAAe,sBAAsB;AAAA,EACxC;AAEA,aAAW,CAAC,WAAW,MAAM,KAAK,SAAS;AACzC,UAAM,QAAQ,UAAU,SAAS;AACjC,QAAI,UAAU,UAAa,QAAQ,IAAI,MAAM,MAAM,QAAW;AAC5D,cAAQ,IAAI,MAAM,IAAI;AAAA,IACxB;AAAA,EACF;AACF;AAGO,SAAS,WAAW,UAAgC,CAAC,GAAgB;AAC1E,MAAI,aAAmC,CAAC;AAExC,MAAI,KAAK,EAAE,MAAM,cAAc,GAAG,gBAAgB;AAElD,MAAI,WAAW,aAAa,GAAG;AAC7B,QAAI;AACF,mBAAa,KAAK,MAAM,aAAa,eAAe,OAAO,CAAC;AAC5D,UAAI;AAAA,QACF,EAAE,QAAQ,WAAW,QAAQ,UAAU,CAAC,CAAC,WAAW,OAAO,MAAM,WAAW,KAAK;AAAA,QACjF;AAAA,MACF;AAAA,IACF,SAAS,GAAG;AACV,UAAI,KAAK,EAAE,KAAK,EAAE,GAAG,+BAA+B;AAAA,IACtD;AAAA,EACF,OAAO;AACL,QAAI,KAAK,yBAAyB;AAAA,EACpC;AAGA,MAAI,WAAW,WAAW;AACxB,yBAAqB,WAAW,SAAS;AAAA,EAC3C;AAEA,QAAM,SACJ,QAAQ,UACR,QAAQ,IAAI,mBACZ,WAAW,UACX,SAAS;AAEX,QAAM,WAAW;AAAA,IACf,QAAQ,gBAAgB,MAAM;AAAA,IAC9B,OACE,QAAQ,SACR,QAAQ,IAAI,iBACZ,WAAW,SACX,SAAS;AAAA,IACX,MACE,QAAQ,QACR,QAAQ,IAAI,gBACZ,WAAW,QACX,SAAS;AAAA,IACX,aACG,QAAQ,eACR,QAAQ,IAAI,wBACZ,WAAW,eACZ,SAAS;AAAA,IACX,WAAW,WAAW;AAAA,EACxB;AAEA,MAAI;AAAA,IACF;AAAA,MACE,QAAQ,SAAS;AAAA,MACjB,cAAc,QAAQ,SAAS,QAAQ,QAAQ,IAAI,kBAAkB,QAAQ,WAAW,SAAS,SAAS;AAAA,MAC1G,aAAa,QAAQ,QAAQ,QAAQ,QAAQ,IAAI,gBAAgB,QAAQ,WAAW,QAAQ,SAAS;AAAA,MACrG,UAAU,CAAC,CAAC,SAAS;AAAA,IACvB;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,WAAW,QAAoC;AAC7D,YAAU,YAAY,EAAE,WAAW,KAAK,CAAC;AAEzC,MAAI,WAAiC,CAAC;AACtC,MAAI,WAAW,aAAa,GAAG;AAC7B,QAAI;AACF,iBAAW,KAAK,MAAM,aAAa,eAAe,OAAO,CAAC;AAAA,IAC5D,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,SAAS,EAAE,GAAG,UAAU,GAAG,OAAO;AACxC,gBAAc,eAAe,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,IAAI;AACrE;","names":[]}
|