@mclawnet/agent 0.6.33 → 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 +69 -1
- package/dist/__tests__/backend-adapter-types.test.d.ts +2 -0
- package/dist/__tests__/backend-adapter-types.test.d.ts.map +1 -0
- package/dist/__tests__/backend-factory.test.d.ts +2 -0
- package/dist/__tests__/backend-factory.test.d.ts.map +1 -0
- package/dist/__tests__/bootstrap-deps.test.d.ts +2 -0
- package/dist/__tests__/bootstrap-deps.test.d.ts.map +1 -0
- package/dist/__tests__/normalize-backend-output.test.d.ts +2 -0
- package/dist/__tests__/normalize-backend-output.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-backend.test.d.ts +2 -0
- package/dist/__tests__/session-manager-backend.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-permission.test.d.ts +2 -0
- package/dist/__tests__/session-manager-permission.test.d.ts.map +1 -0
- package/dist/__tests__/templates-roles-bridge.test.d.ts +2 -0
- package/dist/__tests__/templates-roles-bridge.test.d.ts.map +1 -0
- package/dist/backend-adapter.d.ts +102 -10
- package/dist/backend-adapter.d.ts.map +1 -1
- package/dist/backend-factory-VRPU3534.js +9 -0
- package/dist/backend-factory-VRPU3534.js.map +1 -0
- package/dist/backend-factory.d.ts +19 -0
- package/dist/backend-factory.d.ts.map +1 -0
- package/dist/bootstrap-deps.d.ts +62 -0
- package/dist/bootstrap-deps.d.ts.map +1 -0
- package/dist/bootstrap-deps.js +154 -0
- package/dist/bootstrap-deps.js.map +1 -0
- package/dist/checkpoint.d.ts +1 -1
- package/dist/checkpoint.d.ts.map +1 -1
- package/dist/{chunk-QPLG5WHL.js → chunk-B733MQCA.js} +479 -87
- package/dist/chunk-B733MQCA.js.map +1 -0
- package/dist/chunk-FYM7CXUI.js +49 -0
- package/dist/chunk-FYM7CXUI.js.map +1 -0
- package/dist/dist-EGT2NQEW.js +940 -0
- package/dist/dist-EGT2NQEW.js.map +1 -0
- package/dist/fs-handler.d.ts +1 -1
- package/dist/fs-handler.d.ts.map +1 -1
- package/dist/hub-connection.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/normalize-backend-output.d.ts +5 -0
- package/dist/normalize-backend-output.d.ts.map +1 -0
- package/dist/runtime-env-defaults.d.ts +18 -0
- package/dist/runtime-env-defaults.d.ts.map +1 -0
- package/dist/session-manager.d.ts +36 -4
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/start.d.ts +2 -0
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +1 -1
- package/dist/swarm-control-dispatch.d.ts +33 -1
- package/dist/swarm-control-dispatch.d.ts.map +1 -1
- package/dist/swarm-session-bridge.d.ts +3 -3
- package/dist/swarm-session-bridge.d.ts.map +1 -1
- package/dist/templates-roles-bridge.d.ts +14 -0
- package/dist/templates-roles-bridge.d.ts.map +1 -0
- package/package.json +14 -10
- package/dist/chunk-QPLG5WHL.js.map +0 -1
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 @@
|
|
|
1
|
+
{"version":3,"file":"backend-adapter-types.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/backend-adapter-types.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend-factory.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/backend-factory.test.ts"],"names":[],"mappings":""}
|
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"normalize-backend-output.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/normalize-backend-output.test.ts"],"names":[],"mappings":""}
|
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"session-manager-backend.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/session-manager-backend.test.ts"],"names":[],"mappings":""}
|
|
@@ -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":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-manager-permission.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/session-manager-permission.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates-roles-bridge.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/templates-roles-bridge.test.ts"],"names":[],"mappings":""}
|
|
@@ -4,8 +4,66 @@
|
|
|
4
4
|
* Implementations:
|
|
5
5
|
* - ClaudeCodeAdapter (@mclawnet/claude-adapter): spawn `claude` CLI
|
|
6
6
|
* - OpenClawAdapter: spawn `openclaw gateway` (legacy)
|
|
7
|
-
* - CodexAdapter:
|
|
7
|
+
* - CodexAdapter: M3.S3 — `codex app-server --listen stdio://`
|
|
8
|
+
*
|
|
9
|
+
* M3.S1 introduces:
|
|
10
|
+
* - Typed permission flow (PermissionRequest / PermissionDecision)
|
|
11
|
+
* - Typed BackendOutput envelope replacing the historical `onOutput(msg: unknown)`
|
|
12
|
+
* - `backendSessionId` replaces the previous `claudeSessionId` (see plan D7).
|
|
8
13
|
*/
|
|
14
|
+
export interface PermissionRequest {
|
|
15
|
+
/** Stable identifier for the pending tool call. */
|
|
16
|
+
callId: string;
|
|
17
|
+
/** Tool name as reported by the backend (e.g. 'shell', 'apply_patch', 'Write'). */
|
|
18
|
+
toolName: string;
|
|
19
|
+
/** Raw tool input as the backend supplied it. */
|
|
20
|
+
input: unknown;
|
|
21
|
+
/** Optional backend-supplied context (sandbox, cwd, reason, ...). */
|
|
22
|
+
meta?: {
|
|
23
|
+
backend?: "claude" | "codex" | string;
|
|
24
|
+
reason?: string;
|
|
25
|
+
cwd?: string;
|
|
26
|
+
[k: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* S0 finding: neither claude `--permission-mode default` (no wire channel)
|
|
31
|
+
* nor codex `ReviewDecision` supports "approve with modifiedInput", so the
|
|
32
|
+
* S1 contract is a 4-state enum. `allow_session` may degrade to `allow` for
|
|
33
|
+
* backends that don't track session-scoped approvals.
|
|
34
|
+
*/
|
|
35
|
+
export interface PermissionDecision {
|
|
36
|
+
callId: string;
|
|
37
|
+
decision: "allow" | "allow_session" | "deny" | "abort";
|
|
38
|
+
reason?: string;
|
|
39
|
+
}
|
|
40
|
+
export type BackendOutput = {
|
|
41
|
+
kind: "assistant_text";
|
|
42
|
+
text: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "tool_use";
|
|
45
|
+
toolName: string;
|
|
46
|
+
input: unknown;
|
|
47
|
+
callId: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: "tool_result";
|
|
50
|
+
callId: string;
|
|
51
|
+
output: unknown;
|
|
52
|
+
isError?: boolean;
|
|
53
|
+
} | {
|
|
54
|
+
kind: "system";
|
|
55
|
+
subtype: string;
|
|
56
|
+
data: unknown;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Adapter-internal diagnostic only. Hub and wire layers MUST NOT consume
|
|
60
|
+
* `raw` — promote a new typed variant instead when hub needs to read it.
|
|
61
|
+
*/
|
|
62
|
+
| {
|
|
63
|
+
kind: "raw";
|
|
64
|
+
backend: string;
|
|
65
|
+
payload: unknown;
|
|
66
|
+
};
|
|
9
67
|
export interface BackendProcess {
|
|
10
68
|
/** Unique identifier (e.g., Claude session ID) */
|
|
11
69
|
id: string;
|
|
@@ -70,6 +128,26 @@ export interface SpawnOptions {
|
|
|
70
128
|
* don't honour it lose D-lite escalation but never crash.
|
|
71
129
|
*/
|
|
72
130
|
maxOutputTokens?: number;
|
|
131
|
+
/**
|
|
132
|
+
* Optional backend kind for this spawn. When set, SessionManager resolves
|
|
133
|
+
* an adapter via its `resolveAdapter` hook (typically `createBackendAdapter`)
|
|
134
|
+
* and uses it for this process instead of the default constructor adapter.
|
|
135
|
+
* Omitted = use the default adapter (back-compat).
|
|
136
|
+
*/
|
|
137
|
+
backend?: "claude" | "codex";
|
|
138
|
+
/**
|
|
139
|
+
* Per-role sandbox level. Each adapter maps to its own permission model:
|
|
140
|
+
* - codex-adapter: `-c sandbox_mode="<value>"` (OS-level seatbelt /
|
|
141
|
+
* landlock — strong enforcement)
|
|
142
|
+
* - claude-adapter: `--permission-mode` (LLM-soft approval; UI should
|
|
143
|
+
* tooltip that claude has no real OS sandbox)
|
|
144
|
+
*
|
|
145
|
+
* Values: "read-only" | "workspace-write" | "full-access".
|
|
146
|
+
* Default (undefined): adapter-specific — claude uses constructor
|
|
147
|
+
* permissionMode (typically bypassPermissions), codex uses
|
|
148
|
+
* "workspace-write".
|
|
149
|
+
*/
|
|
150
|
+
sandbox?: "read-only" | "workspace-write" | "full-access";
|
|
73
151
|
}
|
|
74
152
|
export interface BackendAdapter {
|
|
75
153
|
/** Backend type identifier */
|
|
@@ -80,35 +158,49 @@ export interface BackendAdapter {
|
|
|
80
158
|
stop(process: BackendProcess): Promise<void>;
|
|
81
159
|
/** Send user input to the backend process */
|
|
82
160
|
send(process: BackendProcess, input: string): void;
|
|
83
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Register output handler. Handler receives `unknown` today (legacy claude
|
|
163
|
+
* stream-json frames); adapters are migrating to emit `BackendOutput`
|
|
164
|
+
* envelopes. Consumers that need typed output should narrow with the
|
|
165
|
+
* `BackendOutput` discriminator.
|
|
166
|
+
*/
|
|
84
167
|
onOutput(process: BackendProcess, handler: (msg: unknown) => void): void;
|
|
85
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* M3.S1 — Optional permission flow.
|
|
170
|
+
* - codex-adapter (S3): implemented; maps from `ReviewDecision` wire format.
|
|
171
|
+
* - claude-adapter: M3 stays on `bypassPermissions` and does NOT implement
|
|
172
|
+
* these. M4+ will add via an MCP permission-prompt tool.
|
|
173
|
+
*/
|
|
174
|
+
onPermissionRequest?(process: BackendProcess, handler: (req: PermissionRequest) => void): void;
|
|
175
|
+
respondToPermission?(process: BackendProcess, decision: PermissionDecision): Promise<void>;
|
|
176
|
+
/** Turn-complete handler. */
|
|
86
177
|
onTurnComplete?(process: BackendProcess, handler: (info: {
|
|
87
|
-
|
|
178
|
+
backendSessionId?: string;
|
|
88
179
|
cost?: number;
|
|
89
180
|
duration?: number;
|
|
90
181
|
contextUsage?: {
|
|
91
182
|
used: number;
|
|
92
183
|
total: number;
|
|
93
184
|
};
|
|
185
|
+
backendMeta?: Record<string, unknown>;
|
|
94
186
|
}) => void): void;
|
|
95
187
|
/** Register error handler */
|
|
96
188
|
onError?(process: BackendProcess, handler: (error: Error) => void): void;
|
|
97
189
|
/**
|
|
98
190
|
* Register a handler that fires the moment the backend reports its session
|
|
99
|
-
* is initialized (e.g. Claude CLI's `system/init` frame).
|
|
100
|
-
* persist `
|
|
101
|
-
* `turn_complete`, and a mid-turn crash would leave db's
|
|
102
|
-
* NULL despite a valid jsonl file existing on disk.
|
|
191
|
+
* is initialized (e.g. Claude CLI's `system/init` frame, codex `thread_id`).
|
|
192
|
+
* Used by hub to persist `backendSessionId` early — without this we have to
|
|
193
|
+
* wait for `turn_complete`, and a mid-turn crash would leave db's
|
|
194
|
+
* backend_session_id NULL despite a valid jsonl file existing on disk.
|
|
103
195
|
*
|
|
104
196
|
* Optional: adapters that don't implement this lose the early-backfill
|
|
105
|
-
* capability — db's
|
|
197
|
+
* capability — db's backend_session_id will only be populated on
|
|
106
198
|
* `turn_complete`, so any session whose first turn crashes will be
|
|
107
199
|
* unrecoverable via `--resume`. Other backends (codex, openclaw) should
|
|
108
200
|
* implement this if they have an analogous "session ready" signal.
|
|
109
201
|
*/
|
|
110
202
|
onSessionStarted?(process: BackendProcess, handler: (info: {
|
|
111
|
-
|
|
203
|
+
backendSessionId: string;
|
|
112
204
|
}) => void): void;
|
|
113
205
|
/**
|
|
114
206
|
* Register a handler that fires when the underlying process exits on its
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend-adapter.d.ts","sourceRoot":"","sources":["../src/backend-adapter.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"backend-adapter.d.ts","sourceRoot":"","sources":["../src/backend-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BackendAdapter } from "./backend-adapter.js";
|
|
2
|
+
export type BackendKind = "claude" | "codex";
|
|
3
|
+
export declare function __resetBackendFactoryCache(): void;
|
|
4
|
+
export interface BackendFactoryContext {
|
|
5
|
+
/** Role instance / template name — surfaces in error messages so users know
|
|
6
|
+
* which role is misconfigured when its backend package fails to load. */
|
|
7
|
+
roleName?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a BackendAdapter for the given backend kind.
|
|
11
|
+
*
|
|
12
|
+
* - "claude" / undefined → @mclawnet/claude-adapter (back-compat default).
|
|
13
|
+
* - "codex" → @mclawnet/codex-adapter via dynamic import so the codex bundle
|
|
14
|
+
* isn't pulled into claude-only deployments.
|
|
15
|
+
*
|
|
16
|
+
* Results are cached per kind; subsequent calls return the same instance.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createBackendAdapter(kind: BackendKind | undefined, ctx?: BackendFactoryContext): Promise<BackendAdapter>;
|
|
19
|
+
//# sourceMappingURL=backend-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend-factory.d.ts","sourceRoot":"","sources":["../src/backend-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAI7C,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD;AAED,MAAM,WAAW,qBAAqB;IACpC;8EAC0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,GAAG,GAAE,qBAA0B,GAC9B,OAAO,CAAC,cAAc,CAAC,CA0CzB"}
|
|
@@ -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":[]}
|
package/dist/checkpoint.d.ts
CHANGED
package/dist/checkpoint.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkpoint.d.ts","sourceRoot":"","sources":["../src/checkpoint.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,
|
|
1
|
+
{"version":3,"file":"checkpoint.d.ts","sourceRoot":"","sources":["../src/checkpoint.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,CAAC,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,IAAI,CAAC,CAIf;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAShC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAO/C"}
|