copillm 0.2.3 → 0.2.5
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/README.md +1 -0
- package/dist/agentconfig/apply.js +4 -3
- package/dist/agentconfig/load.js +34 -1
- package/dist/agentconfig/schema.js +22 -0
- package/dist/agents/registry.js +80 -6
- package/dist/cli/auth/ensure.js +30 -0
- package/dist/cli/auth/runAuth.js +38 -0
- package/dist/cli/commands/agents/claude.js +47 -0
- package/dist/cli/commands/agents/codex.js +48 -0
- package/dist/cli/commands/agents/copilot.js +49 -0
- package/dist/cli/commands/agents/pi.js +47 -0
- package/dist/cli/commands/agents/shared.js +28 -0
- package/dist/cli/commands/auth.js +99 -0
- package/dist/cli/commands/daemon.js +358 -0
- package/dist/cli/commands/env.js +135 -0
- package/dist/cli/commands/models.js +80 -0
- package/dist/cli/configCommands.js +10 -0
- package/dist/cli/copillmFlags.js +111 -0
- package/dist/cli/daemon/ensureRunning.js +65 -0
- package/dist/cli/daemon/lifecycle.js +61 -0
- package/dist/cli/daemon/probes.js +68 -0
- package/dist/cli/daemon/runDaemon.js +102 -0
- package/dist/cli/daemon/selfSpawn.js +15 -0
- package/dist/cli/daemon/spawnEnv.js +12 -0
- package/dist/cli/index.js +41 -0
- package/dist/cli/integrations/banner.js +51 -0
- package/dist/cli/integrations/claudeExport.js +14 -0
- package/dist/cli/integrations/refreshCodex.js +19 -0
- package/dist/cli/integrations/refreshPi.js +17 -0
- package/dist/cli/packageInfo.js +29 -0
- package/dist/cli/shared/backends.js +31 -0
- package/dist/cli/shared/debug.js +44 -0
- package/dist/cli/shared/deprecation.js +7 -0
- package/dist/cli/shared/exitCodes.js +9 -0
- package/dist/cli/shared/output.js +14 -0
- package/dist/cli/shared/parseAgent.js +6 -0
- package/dist/cli/updateNotifier.js +223 -0
- package/dist/cli.js +1 -1355
- package/dist/server/errors.js +195 -0
- package/dist/server/proxy.js +50 -885
- package/dist/server/routes/debug.js +65 -0
- package/dist/server/routes/health.js +32 -0
- package/dist/server/routes/models.js +41 -0
- package/dist/server/routes/proxyForward.js +108 -0
- package/dist/server/routes/shared.js +161 -0
- package/dist/server/upstream/copilotClient.js +137 -0
- package/dist/server/upstream/streaming.js +146 -0
- package/package.json +7 -2
package/dist/cli.js
CHANGED
|
@@ -1,1356 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
6
|
-
import { Command } from "commander";
|
|
7
|
-
import { clearStoredCredential, inspectStoredCredential, loadStoredCredential, saveStoredCredential } from "./auth/credentials.js";
|
|
8
|
-
import { inspectGithubIdentity } from "./auth/githubIdentity.js";
|
|
9
|
-
import { ensureAuthenticatedInteractive as ensureAuthenticatedInteractiveImpl } from "./auth/ensureAuthenticated.js";
|
|
10
|
-
import { loginViaDeviceFlow } from "./auth/deviceFlow.js";
|
|
11
|
-
import { CopilotTokenManager } from "./auth/copilotToken.js";
|
|
12
|
-
import { confirm, choose } from "./auth/interactivePrompt.js";
|
|
13
|
-
import { loadConfig, saveConfig } from "./config/config.js";
|
|
14
|
-
import { createLogger } from "./config/logging.js";
|
|
15
|
-
import { listModels, resolveModelSelections } from "./models/discovery.js";
|
|
16
|
-
import { acquireLock, inspectLock, LockAlreadyRunningError, releaseLock } from "./server/lock.js";
|
|
17
|
-
import { startProxyServer } from "./server/proxy.js";
|
|
18
|
-
import { defaultOutputDir, generateCodexHome } from "./integrations/codex/init.js";
|
|
19
|
-
import { defaultOutputDir as defaultPiOutputDir, generatePiHome } from "./integrations/pi/init.js";
|
|
20
|
-
import { debugLogPath, getCopillmHome } from "./config/home.js";
|
|
21
|
-
import { clearClaudeGatewayCache } from "./integrations/claude/cache.js";
|
|
22
|
-
import { detectClaudeSettingsConflicts, formatSettingsConflictWarning } from "./integrations/claude/settingsConflict.js";
|
|
23
|
-
import { buildClaudeExportCommand as buildClaudeExport, computeAnthropicDefaults, readModelIdsFromCache } from "./models/anthropicDefaults.js";
|
|
24
|
-
import { isShellSyntax, renderEnvBlock } from "./cli/envBlock.js";
|
|
25
|
-
import { buildClaudeEnvBundle, buildCodexEnvBundle, buildPiEnvBundle } from "./cli/agentEnv.js";
|
|
26
|
-
import { launchAgent } from "./cli/launchAgent.js";
|
|
27
|
-
import { applyAgentConfig, formatApplyNotes } from "./agentconfig/apply.js";
|
|
28
|
-
import { applyYolo, resolveYolo } from "./agents/registry.js";
|
|
29
|
-
import { registerConfigCommands } from "./cli/configCommands.js";
|
|
30
|
-
import { installProcessSafetyNet } from "./cli/processSafetyNet.js";
|
|
31
|
-
const logger = createLogger();
|
|
32
|
-
const program = new Command();
|
|
33
|
-
// Resolve the package version from package.json at runtime so `--version` stays
|
|
34
|
-
// in sync with whatever was published. Using createRequire keeps this working
|
|
35
|
-
// under NodeNext ESM without needing an import-assertion syntax flag, and
|
|
36
|
-
// resolves the same file in both `dist/cli.js` (one level deep) and `src/cli.ts`
|
|
37
|
-
// when invoked via tsx.
|
|
38
|
-
const pkgVersion = createRequire(import.meta.url)("../package.json").version;
|
|
39
|
-
program.name("copillm").description("Local Copilot proxy").version(pkgVersion);
|
|
40
|
-
program.enablePositionalOptions();
|
|
41
|
-
program.option("--debug", "Enable copillm debug mode (debug endpoint plus verbose daemon diagnostics)");
|
|
42
|
-
program
|
|
43
|
-
.command("login")
|
|
44
|
-
.description("[deprecated] Use `copillm auth login`")
|
|
45
|
-
.option("--json", "JSON output")
|
|
46
|
-
.action(async (opts) => {
|
|
47
|
-
emitDeprecation(opts, "login", "auth login");
|
|
48
|
-
await runAuthLogin(opts, { forceSession: false });
|
|
49
|
-
});
|
|
50
|
-
program
|
|
51
|
-
.command("logout")
|
|
52
|
-
.description("[deprecated] Use `copillm auth logout`")
|
|
53
|
-
.option("--json", "JSON output")
|
|
54
|
-
.action(async (opts) => {
|
|
55
|
-
emitDeprecation(opts, "logout", "auth logout");
|
|
56
|
-
await runAuthLogout(opts);
|
|
57
|
-
});
|
|
58
|
-
const auth = program.command("auth").description("Authentication commands");
|
|
59
|
-
auth
|
|
60
|
-
.command("login")
|
|
61
|
-
.description("Authenticate with GitHub")
|
|
62
|
-
.option("--json", "JSON output")
|
|
63
|
-
// Undocumented test seam: force the session-only backend regardless of
|
|
64
|
-
// whether the OS keychain is available. Equivalent to setting
|
|
65
|
-
// COPILLM_FORCE_SESSION_BACKEND=1 for the duration of this command.
|
|
66
|
-
.option("--force-session", "(test-only) force the session-only backend", false)
|
|
67
|
-
.action(async (opts) => {
|
|
68
|
-
await runAuthLogin(opts, { forceSession: Boolean(opts.forceSession) });
|
|
69
|
-
});
|
|
70
|
-
auth
|
|
71
|
-
.command("logout")
|
|
72
|
-
.description("Clear credentials and stop running daemon")
|
|
73
|
-
.option("--json", "JSON output")
|
|
74
|
-
.action(async (opts) => {
|
|
75
|
-
await runAuthLogout(opts);
|
|
76
|
-
});
|
|
77
|
-
auth
|
|
78
|
-
.command("status")
|
|
79
|
-
.description("Report whether a credential is stored (token is never printed)")
|
|
80
|
-
.option("--json", "JSON output")
|
|
81
|
-
.option("--no-user", "Skip the GitHub /user lookup that fetches the login name")
|
|
82
|
-
.action(async (opts) => {
|
|
83
|
-
let info;
|
|
84
|
-
try {
|
|
85
|
-
info = await inspectStoredCredential();
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
const message = error instanceof Error ? error.message : "unknown_error";
|
|
89
|
-
if (opts.json) {
|
|
90
|
-
process.stdout.write(JSON.stringify({ status: "error", error: message }, null, 2) + "\n");
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
process.stderr.write(`auth status error: ${message}\n`);
|
|
94
|
-
}
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
// commander's --no-user toggles opts.user to false; when the flag is
|
|
98
|
-
// omitted opts.user is undefined and we treat that as "fetch by default".
|
|
99
|
-
const userLookupEnabled = info.stored && opts.user !== false;
|
|
100
|
-
let identity = null;
|
|
101
|
-
if (userLookupEnabled) {
|
|
102
|
-
// inspectGithubIdentity is designed to return null on any failure, but
|
|
103
|
-
// we wrap defensively at the CLI level too: a regression in the wrapper,
|
|
104
|
-
// or a platform-specific fetch error path (e.g. Node 22 on macOS has
|
|
105
|
-
// surfaced uncaught socket rejections from privileged-port ECONNREFUSED),
|
|
106
|
-
// must never break the auth-status command. Status output should always
|
|
107
|
-
// succeed even when the network is broken.
|
|
108
|
-
try {
|
|
109
|
-
identity = await inspectGithubIdentity();
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
identity = null;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
if (opts.json) {
|
|
116
|
-
process.stdout.write(JSON.stringify({
|
|
117
|
-
status: info.stored ? "logged_in" : "logged_out",
|
|
118
|
-
stored: info.stored,
|
|
119
|
-
backend: info.backend,
|
|
120
|
-
user: identity
|
|
121
|
-
}, null, 2) + "\n");
|
|
122
|
-
}
|
|
123
|
-
else if (info.stored) {
|
|
124
|
-
process.stdout.write(`${formatHumanAuthStatusLine(info.backend, identity)}\n`);
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
process.stdout.write("not logged in\n");
|
|
128
|
-
}
|
|
129
|
-
process.exit(info.stored ? 0 : 2);
|
|
130
|
-
});
|
|
131
|
-
program
|
|
132
|
-
.command("start")
|
|
133
|
-
.description("Start proxy")
|
|
134
|
-
.option("--detach", "Run in detached mode")
|
|
135
|
-
.option("--debug", "Enable debug endpoints (e.g. /_debug)")
|
|
136
|
-
.option("--no-codex", "Skip generating ~/.copillm/codex/ for Codex CLI")
|
|
137
|
-
.option("--codex-model <id>", "Default Codex model slug")
|
|
138
|
-
.option("--no-pi", "Skip generating ~/.pi/agent/models.json for pi coding agent")
|
|
139
|
-
.option("--json", "JSON output")
|
|
140
|
-
.action(async (opts) => {
|
|
141
|
-
const debug = resolveCopillmDebug(opts.debug);
|
|
142
|
-
enableRuntimeDebug(debug);
|
|
143
|
-
if (opts.detach) {
|
|
144
|
-
// Fail fast on missing credentials rather than letting the detached
|
|
145
|
-
// child die silently and surface as a generic "start timed out" error.
|
|
146
|
-
const authState = await inspectStoredCredential();
|
|
147
|
-
if (!authState.stored) {
|
|
148
|
-
throw new Error("Not authenticated. Run `copillm auth login` first, or start without --detach to log in interactively.");
|
|
149
|
-
}
|
|
150
|
-
const existingLock = await readLiveLock();
|
|
151
|
-
if (existingLock) {
|
|
152
|
-
const activeDebug = await warnIfDebugRequestedButInactive(debug, existingLock.port);
|
|
153
|
-
const codex = opts.codex === false ? null : await refreshCodexHome(existingLock.port, opts.codexModel ?? null);
|
|
154
|
-
const pi = opts.pi === false ? null : await refreshPiHome(existingLock.port);
|
|
155
|
-
const claude = buildClaudeExportCommand(existingLock.port, null);
|
|
156
|
-
const banner = formatStartBanner({
|
|
157
|
-
port: existingLock.port,
|
|
158
|
-
pid: existingLock.pid,
|
|
159
|
-
mode: "already_running",
|
|
160
|
-
debug: activeDebug,
|
|
161
|
-
debugLogPath: null,
|
|
162
|
-
codex,
|
|
163
|
-
pi
|
|
164
|
-
});
|
|
165
|
-
writeCommandOutput(opts, banner, {
|
|
166
|
-
status: "already_running",
|
|
167
|
-
pid: existingLock.pid,
|
|
168
|
-
port: existingLock.port,
|
|
169
|
-
debug: activeDebug,
|
|
170
|
-
url: `http://127.0.0.1:${existingLock.port}`,
|
|
171
|
-
codex_home: codex?.outDir ?? null,
|
|
172
|
-
codex_export_command: codex?.exportCommand ?? null,
|
|
173
|
-
codex_env: codex ? buildCodexEnvBundle(codex.outDir).env : null,
|
|
174
|
-
pi_home: pi?.outDir ?? null,
|
|
175
|
-
pi_config_path: pi?.configPath ?? null,
|
|
176
|
-
pi_mirror_path: pi?.mirrorPath ?? null,
|
|
177
|
-
pi_backup_path: pi?.backupPath ?? null,
|
|
178
|
-
pi_model_count: pi?.modelCount ?? null,
|
|
179
|
-
claude_export_command: claude.command,
|
|
180
|
-
claude_env: claude.bundle.env,
|
|
181
|
-
claude_defaults: claude.defaults
|
|
182
|
-
});
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
const daemonArgs = [process.argv[1], "daemon"];
|
|
186
|
-
if (debug) {
|
|
187
|
-
daemonArgs.push("--debug");
|
|
188
|
-
}
|
|
189
|
-
const child = spawn(process.execPath, daemonArgs, {
|
|
190
|
-
detached: true,
|
|
191
|
-
stdio: "ignore",
|
|
192
|
-
env: daemonSpawnEnv(debug)
|
|
193
|
-
});
|
|
194
|
-
child.unref();
|
|
195
|
-
const started = await waitForDaemonReady(child.pid ?? null, 8_000);
|
|
196
|
-
if (!started) {
|
|
197
|
-
throw new Error("Detached daemon start timed out.");
|
|
198
|
-
}
|
|
199
|
-
const codex = opts.codex === false ? null : await refreshCodexHome(started.port, opts.codexModel ?? null);
|
|
200
|
-
const pi = opts.pi === false ? null : await refreshPiHome(started.port);
|
|
201
|
-
const claude = buildClaudeExportCommand(started.port, null);
|
|
202
|
-
const banner = formatStartBanner({
|
|
203
|
-
port: started.port,
|
|
204
|
-
pid: started.pid,
|
|
205
|
-
mode: "detached",
|
|
206
|
-
debug,
|
|
207
|
-
debugLogPath: currentDebugLogPath(debug),
|
|
208
|
-
codex,
|
|
209
|
-
pi
|
|
210
|
-
});
|
|
211
|
-
writeCommandOutput(opts, banner, {
|
|
212
|
-
status: "ok",
|
|
213
|
-
mode: "detached",
|
|
214
|
-
pid: started.pid,
|
|
215
|
-
port: started.port,
|
|
216
|
-
debug,
|
|
217
|
-
debug_log_path: currentDebugLogPath(debug),
|
|
218
|
-
url: `http://127.0.0.1:${started.port}`,
|
|
219
|
-
codex_home: codex?.outDir ?? null,
|
|
220
|
-
codex_export_command: codex?.exportCommand ?? null,
|
|
221
|
-
codex_env: codex ? buildCodexEnvBundle(codex.outDir).env : null,
|
|
222
|
-
codex_default_model: codex?.defaultModel ?? null,
|
|
223
|
-
codex_model_count: codex?.modelCount ?? null,
|
|
224
|
-
pi_home: pi?.outDir ?? null,
|
|
225
|
-
pi_config_path: pi?.configPath ?? null,
|
|
226
|
-
pi_mirror_path: pi?.mirrorPath ?? null,
|
|
227
|
-
pi_backup_path: pi?.backupPath ?? null,
|
|
228
|
-
pi_model_count: pi?.modelCount ?? null,
|
|
229
|
-
claude_export_command: claude.command,
|
|
230
|
-
claude_env: claude.bundle.env,
|
|
231
|
-
claude_defaults: claude.defaults
|
|
232
|
-
});
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
// Foreground path: interactively prompt for login if needed.
|
|
236
|
-
await ensureAuthenticatedInteractive();
|
|
237
|
-
const started = await runDaemon({ debug });
|
|
238
|
-
if (started.kind === "already_running") {
|
|
239
|
-
const activeDebug = await warnIfDebugRequestedButInactive(debug, started.lock.port);
|
|
240
|
-
const codex = opts.codex === false ? null : await refreshCodexHome(started.lock.port, opts.codexModel ?? null);
|
|
241
|
-
const pi = opts.pi === false ? null : await refreshPiHome(started.lock.port);
|
|
242
|
-
const claude = buildClaudeExportCommand(started.lock.port, null);
|
|
243
|
-
const banner = formatStartBanner({
|
|
244
|
-
port: started.lock.port,
|
|
245
|
-
pid: started.lock.pid,
|
|
246
|
-
mode: "already_running",
|
|
247
|
-
debug: activeDebug,
|
|
248
|
-
debugLogPath: null,
|
|
249
|
-
codex,
|
|
250
|
-
pi
|
|
251
|
-
});
|
|
252
|
-
writeCommandOutput(opts, banner, {
|
|
253
|
-
status: "already_running",
|
|
254
|
-
pid: started.lock.pid,
|
|
255
|
-
port: started.lock.port,
|
|
256
|
-
debug: activeDebug,
|
|
257
|
-
url: `http://127.0.0.1:${started.lock.port}`,
|
|
258
|
-
codex_home: codex?.outDir ?? null,
|
|
259
|
-
codex_export_command: codex?.exportCommand ?? null,
|
|
260
|
-
codex_env: codex ? buildCodexEnvBundle(codex.outDir).env : null,
|
|
261
|
-
pi_home: pi?.outDir ?? null,
|
|
262
|
-
pi_config_path: pi?.configPath ?? null,
|
|
263
|
-
pi_mirror_path: pi?.mirrorPath ?? null,
|
|
264
|
-
pi_backup_path: pi?.backupPath ?? null,
|
|
265
|
-
pi_model_count: pi?.modelCount ?? null,
|
|
266
|
-
claude_export_command: claude.command,
|
|
267
|
-
claude_env: claude.bundle.env,
|
|
268
|
-
claude_defaults: claude.defaults
|
|
269
|
-
});
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
const codex = opts.codex === false ? null : await refreshCodexHome(started.port, opts.codexModel ?? null);
|
|
273
|
-
const pi = opts.pi === false ? null : await refreshPiHome(started.port);
|
|
274
|
-
const claude = buildClaudeExportCommand(started.port, started.callerSecret);
|
|
275
|
-
const banner = formatStartBanner({
|
|
276
|
-
port: started.port,
|
|
277
|
-
pid: process.pid,
|
|
278
|
-
mode: "foreground",
|
|
279
|
-
debug,
|
|
280
|
-
debugLogPath: currentDebugLogPath(debug),
|
|
281
|
-
codex,
|
|
282
|
-
pi
|
|
283
|
-
});
|
|
284
|
-
writeCommandOutput(opts, banner, {
|
|
285
|
-
status: "ok",
|
|
286
|
-
mode: "foreground",
|
|
287
|
-
pid: process.pid,
|
|
288
|
-
port: started.port,
|
|
289
|
-
debug,
|
|
290
|
-
debug_log_path: currentDebugLogPath(debug),
|
|
291
|
-
url: `http://127.0.0.1:${started.port}`,
|
|
292
|
-
caller_secret: started.callerSecret,
|
|
293
|
-
codex_home: codex?.outDir ?? null,
|
|
294
|
-
codex_export_command: codex?.exportCommand ?? null,
|
|
295
|
-
codex_env: codex ? buildCodexEnvBundle(codex.outDir).env : null,
|
|
296
|
-
codex_default_model: codex?.defaultModel ?? null,
|
|
297
|
-
codex_model_count: codex?.modelCount ?? null,
|
|
298
|
-
pi_home: pi?.outDir ?? null,
|
|
299
|
-
pi_config_path: pi?.configPath ?? null,
|
|
300
|
-
pi_mirror_path: pi?.mirrorPath ?? null,
|
|
301
|
-
pi_backup_path: pi?.backupPath ?? null,
|
|
302
|
-
pi_model_count: pi?.modelCount ?? null,
|
|
303
|
-
claude_export_command: claude.command,
|
|
304
|
-
claude_env: claude.bundle.env,
|
|
305
|
-
claude_defaults: claude.defaults
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
program
|
|
309
|
-
.command("daemon")
|
|
310
|
-
.description("Internal background command")
|
|
311
|
-
.option("--debug", "Enable debug endpoints")
|
|
312
|
-
.action(async (opts) => {
|
|
313
|
-
const debug = resolveCopillmDebug(opts.debug);
|
|
314
|
-
enableRuntimeDebug(debug);
|
|
315
|
-
try {
|
|
316
|
-
const started = await runDaemon({ debug });
|
|
317
|
-
if (started.kind === "already_running") {
|
|
318
|
-
process.exit(0);
|
|
319
|
-
}
|
|
320
|
-
process.stdout.write(`copillm listening on http://127.0.0.1:${started.port}${debug ? " [debug]" : ""}\n`);
|
|
321
|
-
}
|
|
322
|
-
catch (err) {
|
|
323
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
324
|
-
logger.fatal({ err }, "daemon failed to start");
|
|
325
|
-
process.stderr.write(`copillm daemon: ${message}\n`);
|
|
326
|
-
process.exit(1);
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
program
|
|
330
|
-
.command("stop")
|
|
331
|
-
.description("Stop detached daemon")
|
|
332
|
-
.option("--json", "JSON output")
|
|
333
|
-
.action(async (opts) => {
|
|
334
|
-
const lockState = inspectLock();
|
|
335
|
-
if (lockState.state === "missing") {
|
|
336
|
-
const cache = clearClaudeGatewayCache();
|
|
337
|
-
writeCommandOutput(opts, formatStopHumanLine("Not running.", cache), {
|
|
338
|
-
status: "not_running",
|
|
339
|
-
claude_cache: cache
|
|
340
|
-
});
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
if (lockState.state === "stale") {
|
|
344
|
-
releaseLock();
|
|
345
|
-
const cache = clearClaudeGatewayCache();
|
|
346
|
-
writeCommandOutput(opts, formatStopHumanLine("Removed stale lock.", cache), {
|
|
347
|
-
status: "stale_lock_removed",
|
|
348
|
-
reason: lockState.reason,
|
|
349
|
-
claude_cache: cache
|
|
350
|
-
});
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
await stopByPid(lockState.lock.pid);
|
|
354
|
-
const cache = clearClaudeGatewayCache();
|
|
355
|
-
writeCommandOutput(opts, formatStopHumanLine("Stopped.", cache), {
|
|
356
|
-
status: "ok",
|
|
357
|
-
pid: lockState.lock.pid,
|
|
358
|
-
claude_cache: cache
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
program
|
|
362
|
-
.command("status")
|
|
363
|
-
.description("Show daemon status")
|
|
364
|
-
.option("--json", "JSON output")
|
|
365
|
-
.action(async (opts) => {
|
|
366
|
-
const config = loadConfig();
|
|
367
|
-
const lockState = inspectLock();
|
|
368
|
-
const checkedAtIso = new Date().toISOString();
|
|
369
|
-
const uptimeSeconds = lockState.state === "running" ? computeUptimeSeconds(lockState.lock.started_at_iso) : null;
|
|
370
|
-
// inspectStoredCredential never returns the token itself, so it's safe to
|
|
371
|
-
// include the result in the status payload.
|
|
372
|
-
let authInfo;
|
|
373
|
-
try {
|
|
374
|
-
const info = await inspectStoredCredential();
|
|
375
|
-
authInfo = { stored: info.stored, backend: info.backend, error: null };
|
|
376
|
-
}
|
|
377
|
-
catch (error) {
|
|
378
|
-
const message = error instanceof Error ? error.message : "unknown_error";
|
|
379
|
-
authInfo = { stored: false, backend: null, error: message };
|
|
380
|
-
}
|
|
381
|
-
const status = {
|
|
382
|
-
running: lockState.state === "running",
|
|
383
|
-
stale: lockState.state === "stale",
|
|
384
|
-
pid: lockState.state === "running" ? lockState.lock.pid : null,
|
|
385
|
-
port: lockState.state === "running" ? lockState.lock.port : null,
|
|
386
|
-
started_at_iso: lockState.state === "running" ? lockState.lock.started_at_iso : null,
|
|
387
|
-
uptime_seconds: uptimeSeconds,
|
|
388
|
-
url: lockState.state === "running" ? `http://127.0.0.1:${lockState.lock.port}` : null,
|
|
389
|
-
require_caller_secret: config.requireCallerSecret,
|
|
390
|
-
account_type: config.accountType,
|
|
391
|
-
selected_models: config.selectedModels,
|
|
392
|
-
auth: authInfo,
|
|
393
|
-
bearer_ttl_seconds: null,
|
|
394
|
-
health_check_status_code: null,
|
|
395
|
-
health_state: null,
|
|
396
|
-
health_error: null,
|
|
397
|
-
health_status: "unknown",
|
|
398
|
-
checked_at_iso: checkedAtIso,
|
|
399
|
-
stale_reason: lockState.state === "stale" ? lockState.reason : null
|
|
400
|
-
};
|
|
401
|
-
if (lockState.state === "running") {
|
|
402
|
-
const health = await probeHealth(lockState.lock.port);
|
|
403
|
-
status.health_status = health.ok ? "ok" : "degraded";
|
|
404
|
-
status.bearer_ttl_seconds = health.bearerTtlSeconds;
|
|
405
|
-
status.health_check_status_code = health.statusCode;
|
|
406
|
-
status.health_state = health.status;
|
|
407
|
-
status.health_error = health.error;
|
|
408
|
-
}
|
|
409
|
-
if (opts.json) {
|
|
410
|
-
process.stdout.write(JSON.stringify(status, null, 2) + "\n");
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
if (lockState.state === "running") {
|
|
414
|
-
process.stdout.write(`running (pid ${lockState.lock.pid}, port ${lockState.lock.port})\n`);
|
|
415
|
-
process.stdout.write(`health: ${status.health_status}`);
|
|
416
|
-
if (status.health_state) {
|
|
417
|
-
process.stdout.write(` (${status.health_state})`);
|
|
418
|
-
}
|
|
419
|
-
if (status.health_check_status_code !== null) {
|
|
420
|
-
process.stdout.write(` [http ${status.health_check_status_code}]`);
|
|
421
|
-
}
|
|
422
|
-
if (status.health_error) {
|
|
423
|
-
process.stdout.write(` error=${status.health_error}`);
|
|
424
|
-
}
|
|
425
|
-
process.stdout.write("\n");
|
|
426
|
-
if (status.bearer_ttl_seconds !== null) {
|
|
427
|
-
process.stdout.write(`bearer_ttl_seconds: ${status.bearer_ttl_seconds}\n`);
|
|
428
|
-
}
|
|
429
|
-
if (status.uptime_seconds !== null) {
|
|
430
|
-
process.stdout.write(`uptime_seconds: ${status.uptime_seconds}\n`);
|
|
431
|
-
}
|
|
432
|
-
writeAuthStatusLine(authInfo);
|
|
433
|
-
process.stdout.write(`checked_at: ${status.checked_at_iso}\n`);
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
if (lockState.state === "stale") {
|
|
437
|
-
process.stdout.write(`stale lock (${lockState.reason})\n`);
|
|
438
|
-
writeAuthStatusLine(authInfo);
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
process.stdout.write("not running\n");
|
|
442
|
-
writeAuthStatusLine(authInfo);
|
|
443
|
-
});
|
|
444
|
-
function writeAuthStatusLine(authInfo) {
|
|
445
|
-
if (authInfo.error) {
|
|
446
|
-
process.stdout.write(`auth: error (${authInfo.error})\n`);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
if (authInfo.stored) {
|
|
450
|
-
process.stdout.write(`auth: logged in (${describeBackend(authInfo.backend)})\n`);
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
process.stdout.write("auth: not logged in\n");
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
program
|
|
457
|
-
.command("health")
|
|
458
|
-
.description("Check health endpoint")
|
|
459
|
-
.option("--json", "JSON output")
|
|
460
|
-
.action(async (opts) => {
|
|
461
|
-
const lockState = inspectLock();
|
|
462
|
-
if (lockState.state !== "running") {
|
|
463
|
-
const payload = {
|
|
464
|
-
ok: false,
|
|
465
|
-
status: lockState.state === "stale" ? "stale_lock" : "not_running",
|
|
466
|
-
detail: lockState.state === "stale" ? lockState.reason : "Daemon is not running."
|
|
467
|
-
};
|
|
468
|
-
writeHealthOutput(opts, payload);
|
|
469
|
-
process.exitCode = 1;
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
const response = await fetch(`http://127.0.0.1:${lockState.lock.port}/healthz`, { signal: AbortSignal.timeout(2_000) });
|
|
473
|
-
const payload = (await response.json());
|
|
474
|
-
const output = { ok: response.ok, status_code: response.status, ...payload };
|
|
475
|
-
writeHealthOutput(opts, output);
|
|
476
|
-
if (!response.ok) {
|
|
477
|
-
process.exitCode = 1;
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
const models = program.command("models").description("Model commands");
|
|
481
|
-
models
|
|
482
|
-
.command("list")
|
|
483
|
-
.description("List entitled models")
|
|
484
|
-
.option("--json", "JSON output")
|
|
485
|
-
.action(async (opts) => {
|
|
486
|
-
const config = loadConfig();
|
|
487
|
-
const creds = await loadStoredCredential();
|
|
488
|
-
if (!creds) {
|
|
489
|
-
throw new Error("Not authenticated. Run `copillm login`.");
|
|
490
|
-
}
|
|
491
|
-
const tokenManager = new CopilotTokenManager(creds.token);
|
|
492
|
-
await tokenManager.ensureToken(false);
|
|
493
|
-
const result = await listModels(config.accountType, creds.token);
|
|
494
|
-
if (opts.json) {
|
|
495
|
-
process.stdout.write(JSON.stringify({
|
|
496
|
-
models: result.models,
|
|
497
|
-
discovery: {
|
|
498
|
-
source: result.source,
|
|
499
|
-
stale: result.stale,
|
|
500
|
-
cache_age_seconds: result.cacheAgeSeconds,
|
|
501
|
-
warning: result.warning
|
|
502
|
-
}
|
|
503
|
-
}, null, 2) + "\n");
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
process.stdout.write(result.models.map((model) => model.id).join("\n") + "\n");
|
|
507
|
-
if (result.stale) {
|
|
508
|
-
process.stdout.write("⚠ using stale model snapshot (upstream discovery unavailable)\n");
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
models
|
|
512
|
-
.command("select")
|
|
513
|
-
.requiredOption("--models <ids>", "Comma-separated model ids")
|
|
514
|
-
.description("Select exposed models")
|
|
515
|
-
.option("--json", "JSON output")
|
|
516
|
-
.action(async (opts) => {
|
|
517
|
-
const config = loadConfig();
|
|
518
|
-
const requested = Array.from(new Set(opts.models
|
|
519
|
-
.split(",")
|
|
520
|
-
.map((value) => value.trim())
|
|
521
|
-
.filter((value) => value.length > 0)));
|
|
522
|
-
if (requested.length === 0) {
|
|
523
|
-
throw new Error("At least one model must be selected.");
|
|
524
|
-
}
|
|
525
|
-
const creds = await loadStoredCredential();
|
|
526
|
-
if (!creds) {
|
|
527
|
-
throw new Error("Not authenticated. Run `copillm login`.");
|
|
528
|
-
}
|
|
529
|
-
const tokenManager = new CopilotTokenManager(creds.token);
|
|
530
|
-
await tokenManager.ensureToken(false);
|
|
531
|
-
const discovery = await listModels(config.accountType, creds.token);
|
|
532
|
-
const resolution = resolveModelSelections(requested, discovery.models);
|
|
533
|
-
if (resolution.unresolved.length > 0) {
|
|
534
|
-
const available = discovery.models.map((model) => model.id).join(", ");
|
|
535
|
-
throw new Error(`Unknown model selection(s): ${resolution.unresolved.join(", ")}. Available models: ${available}`);
|
|
536
|
-
}
|
|
537
|
-
const resolvedSelected = Array.from(new Set(resolution.resolved.map((entry) => entry.resolvedId)));
|
|
538
|
-
saveConfig({ ...config, selectedModels: resolvedSelected });
|
|
539
|
-
const usedAlias = resolution.resolved.some((entry) => entry.input !== entry.resolvedId);
|
|
540
|
-
writeCommandOutput(opts, `Selected ${resolvedSelected.length} model(s)${usedAlias ? " (resolved aliases)." : "."}${discovery.stale ? " Using stale snapshot." : ""}`, {
|
|
541
|
-
status: "ok",
|
|
542
|
-
selected_models: resolvedSelected,
|
|
543
|
-
requested_models: requested,
|
|
544
|
-
resolutions: resolution.resolved,
|
|
545
|
-
discovery: {
|
|
546
|
-
source: discovery.source,
|
|
547
|
-
stale: discovery.stale,
|
|
548
|
-
cache_age_seconds: discovery.cacheAgeSeconds,
|
|
549
|
-
warning: discovery.warning
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
program
|
|
554
|
-
.command("env <agent>")
|
|
555
|
-
.description("Print env vars to launch codex, claude, or pi against copillm")
|
|
556
|
-
.option("--shell <shell>", "Shell syntax: sh|fish|powershell", "sh")
|
|
557
|
-
.option("--json", "JSON output")
|
|
558
|
-
.option("--inline", "Single-line legacy export form (claude only)")
|
|
559
|
-
.action(async (agentRaw, opts) => {
|
|
560
|
-
const agent = parseAgentName(agentRaw);
|
|
561
|
-
if (!isShellSyntax(opts.shell)) {
|
|
562
|
-
throw new Error(`Unsupported --shell value: ${opts.shell}. Use sh, fish, or powershell.`);
|
|
563
|
-
}
|
|
564
|
-
const shell = opts.shell;
|
|
565
|
-
const lockState = inspectLock();
|
|
566
|
-
if (lockState.state !== "running") {
|
|
567
|
-
const message = lockState.state === "stale"
|
|
568
|
-
? `copillm has a stale lock (${lockState.reason}). Run \`copillm stop\` then \`copillm start --detach\`.`
|
|
569
|
-
: "copillm is not running. Run `copillm start --detach` first.";
|
|
570
|
-
if (opts.json) {
|
|
571
|
-
process.stdout.write(JSON.stringify({ status: "not_running", agent, error: message }, null, 2) + "\n");
|
|
572
|
-
}
|
|
573
|
-
else {
|
|
574
|
-
process.stderr.write(`${message}\n`);
|
|
575
|
-
}
|
|
576
|
-
process.exit(2);
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
if (agent === "codex") {
|
|
580
|
-
const codex = await refreshCodexHome(lockState.lock.port, null);
|
|
581
|
-
if (!codex) {
|
|
582
|
-
throw new Error("Failed to prepare Codex home (see warning above).");
|
|
583
|
-
}
|
|
584
|
-
const bundle = buildCodexEnvBundle(codex.outDir);
|
|
585
|
-
const block = renderEnvBlock({
|
|
586
|
-
agent: "codex",
|
|
587
|
-
env: bundle.env,
|
|
588
|
-
shell,
|
|
589
|
-
inlineComments: bundle.inlineComments,
|
|
590
|
-
trailingNotes: bundle.trailingNotes
|
|
591
|
-
});
|
|
592
|
-
if (opts.json) {
|
|
593
|
-
process.stdout.write(JSON.stringify({
|
|
594
|
-
agent: "codex",
|
|
595
|
-
package: "@openai/codex",
|
|
596
|
-
shell,
|
|
597
|
-
env: bundle.env,
|
|
598
|
-
shell_block: block
|
|
599
|
-
}, null, 2) + "\n");
|
|
600
|
-
}
|
|
601
|
-
else {
|
|
602
|
-
process.stdout.write(`${block}\n`);
|
|
603
|
-
}
|
|
604
|
-
process.exit(0);
|
|
605
|
-
}
|
|
606
|
-
if (agent === "pi") {
|
|
607
|
-
const pi = await refreshPiHome(lockState.lock.port);
|
|
608
|
-
if (!pi) {
|
|
609
|
-
throw new Error("Failed to prepare pi models.json (see warning above).");
|
|
610
|
-
}
|
|
611
|
-
const bundle = buildPiEnvBundle(pi.outDir);
|
|
612
|
-
const block = renderEnvBlock({
|
|
613
|
-
agent: "pi",
|
|
614
|
-
env: bundle.env,
|
|
615
|
-
shell,
|
|
616
|
-
inlineComments: bundle.inlineComments,
|
|
617
|
-
trailingNotes: bundle.trailingNotes
|
|
618
|
-
});
|
|
619
|
-
if (opts.json) {
|
|
620
|
-
process.stdout.write(JSON.stringify({
|
|
621
|
-
agent: "pi",
|
|
622
|
-
package: "@earendil-works/pi-coding-agent",
|
|
623
|
-
shell,
|
|
624
|
-
env: bundle.env,
|
|
625
|
-
shell_block: block,
|
|
626
|
-
pi_home: pi.outDir,
|
|
627
|
-
pi_config_path: pi.configPath,
|
|
628
|
-
pi_mirror_path: pi.mirrorPath,
|
|
629
|
-
pi_backup_path: pi.backupPath,
|
|
630
|
-
pi_model_count: pi.modelCount
|
|
631
|
-
}, null, 2) + "\n");
|
|
632
|
-
}
|
|
633
|
-
else {
|
|
634
|
-
process.stdout.write(`${block}\n`);
|
|
635
|
-
}
|
|
636
|
-
process.exit(0);
|
|
637
|
-
}
|
|
638
|
-
const claude = buildClaudeExportCommand(lockState.lock.port, null);
|
|
639
|
-
const settingsConflicts = detectClaudeSettingsConflicts(claude.bundle.env);
|
|
640
|
-
if (opts.inline) {
|
|
641
|
-
if (opts.json) {
|
|
642
|
-
process.stdout.write(JSON.stringify({ agent: "claude", inline: claude.command }, null, 2) + "\n");
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
process.stdout.write(`${claude.command}\n`);
|
|
646
|
-
}
|
|
647
|
-
for (const line of formatSettingsConflictWarning(settingsConflicts)) {
|
|
648
|
-
process.stderr.write(`${line}\n`);
|
|
649
|
-
}
|
|
650
|
-
process.exit(0);
|
|
651
|
-
}
|
|
652
|
-
const block = renderEnvBlock({
|
|
653
|
-
agent: "claude",
|
|
654
|
-
env: claude.bundle.env,
|
|
655
|
-
shell,
|
|
656
|
-
inlineComments: claude.bundle.inlineComments,
|
|
657
|
-
trailingNotes: claude.bundle.trailingNotes
|
|
658
|
-
});
|
|
659
|
-
if (opts.json) {
|
|
660
|
-
process.stdout.write(JSON.stringify({
|
|
661
|
-
agent: "claude",
|
|
662
|
-
package: "@anthropic-ai/claude-code",
|
|
663
|
-
shell,
|
|
664
|
-
env: claude.bundle.env,
|
|
665
|
-
shell_block: block,
|
|
666
|
-
defaults: claude.defaults,
|
|
667
|
-
settings_conflicts: settingsConflicts.conflicts
|
|
668
|
-
}, null, 2) + "\n");
|
|
669
|
-
}
|
|
670
|
-
else {
|
|
671
|
-
process.stdout.write(`${block}\n`);
|
|
672
|
-
}
|
|
673
|
-
for (const line of formatSettingsConflictWarning(settingsConflicts)) {
|
|
674
|
-
process.stderr.write(`${line}\n`);
|
|
675
|
-
}
|
|
676
|
-
process.exit(0);
|
|
677
|
-
});
|
|
678
|
-
program
|
|
679
|
-
.command("codex")
|
|
680
|
-
.description("Launch Codex CLI against copillm (auto-starts daemon, downloads codex if missing)")
|
|
681
|
-
.option("--copillm-use <spec>", "Pin codex package version (e.g. 1.4.7 or @openai/codex@1.4.7)")
|
|
682
|
-
.option("--copillm-debug", "Enable debug endpoints when auto-starting daemon")
|
|
683
|
-
.option("--copillm-profile <name>", "Override active profile from ~/.copillm/agent.toml for this launch")
|
|
684
|
-
.option("--copillm-no-config", "Skip agent.toml fan-out for this launch", false)
|
|
685
|
-
.option("--yolo", "Skip approvals/sandbox (injects --dangerously-bypass-approvals-and-sandbox). Env: COPILLM_YOLO")
|
|
686
|
-
.allowUnknownOption(true)
|
|
687
|
-
.passThroughOptions()
|
|
688
|
-
.helpOption(false)
|
|
689
|
-
.argument("[args...]", "Args forwarded to codex")
|
|
690
|
-
.action(async (forwardedArgs, opts) => {
|
|
691
|
-
const debug = resolveCopillmDebug(opts.copillmDebug);
|
|
692
|
-
enableRuntimeDebug(debug);
|
|
693
|
-
const lock = await ensureDaemonRunningForLauncher({ debug });
|
|
694
|
-
const codex = await refreshCodexHome(lock.port, null);
|
|
695
|
-
if (!codex) {
|
|
696
|
-
throw new Error("Failed to prepare Codex home (see warning above).");
|
|
697
|
-
}
|
|
698
|
-
const bundle = buildCodexEnvBundle(codex.outDir);
|
|
699
|
-
const pinnedSpec = opts.copillmUse ?? process.env.COPILLM_CODEX_VERSION ?? undefined;
|
|
700
|
-
const applyResult = applyAgentConfig({
|
|
701
|
-
agent: "codex",
|
|
702
|
-
cwd: process.cwd(),
|
|
703
|
-
codexHomeDir: codex.outDir,
|
|
704
|
-
profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null,
|
|
705
|
-
skip: Boolean(opts.copillmNoConfig)
|
|
706
|
-
});
|
|
707
|
-
for (const line of formatApplyNotes(applyResult, "codex")) {
|
|
708
|
-
process.stderr.write(`${line}\n`);
|
|
709
|
-
}
|
|
710
|
-
const env = { ...bundle.env, ...applyResult.envOverlay };
|
|
711
|
-
const baseArgs = [...(forwardedArgs ?? []), ...applyResult.cliArgs];
|
|
712
|
-
const args = applyYolo({ agent: "codex", userArgs: baseArgs, yolo: resolveYolo(opts.yolo) });
|
|
713
|
-
const exitCode = await launchAgent({
|
|
714
|
-
agent: "codex",
|
|
715
|
-
args,
|
|
716
|
-
env,
|
|
717
|
-
pinnedSpec
|
|
718
|
-
});
|
|
719
|
-
process.exit(exitCode);
|
|
720
|
-
});
|
|
721
|
-
program
|
|
722
|
-
.command("claude")
|
|
723
|
-
.description("Launch Claude Code against copillm (auto-starts daemon, downloads claude if missing)")
|
|
724
|
-
.option("--copillm-use <spec>", "Pin claude package version (e.g. 1.0.0 or @anthropic-ai/claude-code@1.0.0)")
|
|
725
|
-
.option("--copillm-debug", "Enable debug endpoints when auto-starting daemon")
|
|
726
|
-
.option("--copillm-profile <name>", "Override active profile from ~/.copillm/agent.toml for this launch")
|
|
727
|
-
.option("--copillm-no-config", "Skip agent.toml fan-out for this launch", false)
|
|
728
|
-
.option("--yolo", "Skip permission prompts (injects --dangerously-skip-permissions). Env: COPILLM_YOLO")
|
|
729
|
-
.allowUnknownOption(true)
|
|
730
|
-
.passThroughOptions()
|
|
731
|
-
.helpOption(false)
|
|
732
|
-
.argument("[args...]", "Args forwarded to claude")
|
|
733
|
-
.action(async (forwardedArgs, opts) => {
|
|
734
|
-
const debug = resolveCopillmDebug(opts.copillmDebug);
|
|
735
|
-
enableRuntimeDebug(debug);
|
|
736
|
-
const lock = await ensureDaemonRunningForLauncher({ debug });
|
|
737
|
-
const claude = buildClaudeExportCommand(lock.port, null);
|
|
738
|
-
const pinnedSpec = opts.copillmUse ?? process.env.COPILLM_CLAUDE_VERSION ?? undefined;
|
|
739
|
-
const conflicts = detectClaudeSettingsConflicts(claude.bundle.env);
|
|
740
|
-
for (const line of formatSettingsConflictWarning(conflicts)) {
|
|
741
|
-
process.stderr.write(`${line}\n`);
|
|
742
|
-
}
|
|
743
|
-
const applyResult = applyAgentConfig({
|
|
744
|
-
agent: "claude",
|
|
745
|
-
cwd: process.cwd(),
|
|
746
|
-
profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null,
|
|
747
|
-
skip: Boolean(opts.copillmNoConfig)
|
|
748
|
-
});
|
|
749
|
-
for (const line of formatApplyNotes(applyResult, "claude")) {
|
|
750
|
-
process.stderr.write(`${line}\n`);
|
|
751
|
-
}
|
|
752
|
-
const env = { ...claude.bundle.env, ...applyResult.envOverlay };
|
|
753
|
-
const baseArgs = [...(forwardedArgs ?? []), ...applyResult.cliArgs];
|
|
754
|
-
const args = applyYolo({ agent: "claude", userArgs: baseArgs, yolo: resolveYolo(opts.yolo) });
|
|
755
|
-
const exitCode = await launchAgent({
|
|
756
|
-
agent: "claude",
|
|
757
|
-
args,
|
|
758
|
-
env,
|
|
759
|
-
pinnedSpec
|
|
760
|
-
});
|
|
761
|
-
process.exit(exitCode);
|
|
762
|
-
});
|
|
763
|
-
program
|
|
764
|
-
.command("pi")
|
|
765
|
-
.description("Launch pi coding agent against copillm (auto-starts daemon, downloads pi if missing)")
|
|
766
|
-
.option("--copillm-use <spec>", "Pin pi package version (e.g. 0.75.4 or @earendil-works/pi-coding-agent@0.75.4)")
|
|
767
|
-
.option("--copillm-debug", "Enable debug endpoints when auto-starting daemon")
|
|
768
|
-
.option("--copillm-profile <name>", "Override active profile from ~/.copillm/agent.toml for this launch")
|
|
769
|
-
.option("--copillm-no-config", "Skip agent.toml fan-out for this launch", false)
|
|
770
|
-
.option("--yolo", "Skip approvals if supported (pi has no equivalent; emits a warning). Env: COPILLM_YOLO")
|
|
771
|
-
.allowUnknownOption(true)
|
|
772
|
-
.passThroughOptions()
|
|
773
|
-
.helpOption(false)
|
|
774
|
-
.argument("[args...]", "Args forwarded to pi")
|
|
775
|
-
.action(async (forwardedArgs, opts) => {
|
|
776
|
-
const debug = resolveCopillmDebug(opts.copillmDebug);
|
|
777
|
-
enableRuntimeDebug(debug);
|
|
778
|
-
const lock = await ensureDaemonRunningForLauncher({ debug });
|
|
779
|
-
const pi = await refreshPiHome(lock.port);
|
|
780
|
-
if (!pi) {
|
|
781
|
-
throw new Error("Failed to prepare pi models.json (see warning above).");
|
|
782
|
-
}
|
|
783
|
-
const bundle = buildPiEnvBundle(pi.outDir);
|
|
784
|
-
const pinnedSpec = opts.copillmUse ?? process.env.COPILLM_PI_VERSION ?? undefined;
|
|
785
|
-
const applyResult = applyAgentConfig({
|
|
786
|
-
agent: "pi",
|
|
787
|
-
cwd: process.cwd(),
|
|
788
|
-
profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null,
|
|
789
|
-
skip: Boolean(opts.copillmNoConfig)
|
|
790
|
-
});
|
|
791
|
-
for (const line of formatApplyNotes(applyResult, "pi")) {
|
|
792
|
-
process.stderr.write(`${line}\n`);
|
|
793
|
-
}
|
|
794
|
-
const env = { ...bundle.env, ...applyResult.envOverlay };
|
|
795
|
-
const baseArgs = [...(forwardedArgs ?? []), ...applyResult.cliArgs];
|
|
796
|
-
const args = applyYolo({ agent: "pi", userArgs: baseArgs, yolo: resolveYolo(opts.yolo) });
|
|
797
|
-
const exitCode = await launchAgent({
|
|
798
|
-
agent: "pi",
|
|
799
|
-
args,
|
|
800
|
-
env,
|
|
801
|
-
pinnedSpec
|
|
802
|
-
});
|
|
803
|
-
process.exit(exitCode);
|
|
804
|
-
});
|
|
805
|
-
program
|
|
806
|
-
.command("copilot")
|
|
807
|
-
.description("Launch GitHub Copilot CLI reusing copillm's stored GitHub token (no second device flow)")
|
|
808
|
-
.option("--copillm-use <spec>", "Pin copilot package version (e.g. 1.0.52 or @github/copilot@1.0.52)")
|
|
809
|
-
.option("--copillm-profile <name>", "Override active profile from ~/.copillm/agent.toml for this launch")
|
|
810
|
-
.option("--copillm-no-config", "Skip agent.toml fan-out for this launch", false)
|
|
811
|
-
.option("--yolo", "Allow all tools/paths/URLs (injects --allow-all). Env: COPILLM_YOLO")
|
|
812
|
-
.allowUnknownOption(true)
|
|
813
|
-
.passThroughOptions()
|
|
814
|
-
.helpOption(false)
|
|
815
|
-
.argument("[args...]", "Args forwarded to copilot")
|
|
816
|
-
.action(async (forwardedArgs, opts) => {
|
|
817
|
-
const credential = await loadStoredCredential();
|
|
818
|
-
if (!credential) {
|
|
819
|
-
process.stderr.write("copillm: no stored GitHub credential — run `copillm auth login` first.\n");
|
|
820
|
-
process.exit(1);
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
const pinnedSpec = opts.copillmUse ?? process.env.COPILLM_COPILOT_VERSION ?? undefined;
|
|
824
|
-
const applyResult = applyAgentConfig({
|
|
825
|
-
agent: "copilot",
|
|
826
|
-
cwd: process.cwd(),
|
|
827
|
-
profileOverride: opts.copillmProfile ?? process.env.COPILLM_PROFILE ?? null,
|
|
828
|
-
skip: Boolean(opts.copillmNoConfig)
|
|
829
|
-
});
|
|
830
|
-
for (const line of formatApplyNotes(applyResult, "copilot")) {
|
|
831
|
-
process.stderr.write(`${line}\n`);
|
|
832
|
-
}
|
|
833
|
-
// Inject the stored GitHub OAuth token into the child env only — never
|
|
834
|
-
// export to the parent shell and never persist. Copilot CLI honours
|
|
835
|
-
// COPILOT_GITHUB_TOKEN ahead of its own stored credentials, so this
|
|
836
|
-
// short-circuits its device-flow login when copillm already has a token.
|
|
837
|
-
const env = {
|
|
838
|
-
...applyResult.envOverlay,
|
|
839
|
-
COPILOT_GITHUB_TOKEN: credential.token
|
|
840
|
-
};
|
|
841
|
-
const baseArgs = [...(forwardedArgs ?? []), ...applyResult.cliArgs];
|
|
842
|
-
const args = applyYolo({ agent: "copilot", userArgs: baseArgs, yolo: resolveYolo(opts.yolo) });
|
|
843
|
-
const exitCode = await launchAgent({
|
|
844
|
-
agent: "copilot",
|
|
845
|
-
args,
|
|
846
|
-
env,
|
|
847
|
-
pinnedSpec
|
|
848
|
-
});
|
|
849
|
-
process.exit(exitCode);
|
|
850
|
-
});
|
|
851
|
-
registerConfigCommands(program);
|
|
852
|
-
program.parseAsync(process.argv).catch((error) => {
|
|
853
|
-
if (error instanceof Error) {
|
|
854
|
-
logger.error({ err: error }, error.message);
|
|
855
|
-
process.stderr.write(`${error.message}\n`);
|
|
856
|
-
process.exit(1);
|
|
857
|
-
}
|
|
858
|
-
throw error;
|
|
859
|
-
});
|
|
860
|
-
async function runDaemon(options) {
|
|
861
|
-
const config = loadConfig();
|
|
862
|
-
const creds = await loadStoredCredential();
|
|
863
|
-
if (!creds) {
|
|
864
|
-
throw new Error("Not authenticated. Run `copillm login` first.");
|
|
865
|
-
}
|
|
866
|
-
const tokenManager = new CopilotTokenManager(creds.token);
|
|
867
|
-
await tokenManager.ensureToken(false);
|
|
868
|
-
const callerSecret = config.requireCallerSecret ? randomUUID() : null;
|
|
869
|
-
if (callerSecret) {
|
|
870
|
-
process.stdout.write(`Caller secret: ${callerSecret}\n`);
|
|
871
|
-
}
|
|
872
|
-
const ports = candidatePorts(config.preferredPort);
|
|
873
|
-
let server = null;
|
|
874
|
-
let selectedPort = null;
|
|
875
|
-
for (const port of ports) {
|
|
876
|
-
try {
|
|
877
|
-
await acquireLock(port, { isRunning: async (lock) => probeLivez(lock.port) });
|
|
878
|
-
}
|
|
879
|
-
catch (error) {
|
|
880
|
-
if (error instanceof LockAlreadyRunningError) {
|
|
881
|
-
tokenManager.clear();
|
|
882
|
-
return { kind: "already_running", lock: error.lock };
|
|
883
|
-
}
|
|
884
|
-
throw error;
|
|
885
|
-
}
|
|
886
|
-
try {
|
|
887
|
-
server = await startProxyServer({
|
|
888
|
-
port,
|
|
889
|
-
config,
|
|
890
|
-
tokenManager,
|
|
891
|
-
callerSecret,
|
|
892
|
-
logger,
|
|
893
|
-
debug: Boolean(options?.debug),
|
|
894
|
-
githubToken: creds.token
|
|
895
|
-
});
|
|
896
|
-
selectedPort = port;
|
|
897
|
-
break;
|
|
898
|
-
}
|
|
899
|
-
catch (error) {
|
|
900
|
-
releaseLock();
|
|
901
|
-
if (isAddrInUse(error)) {
|
|
902
|
-
continue;
|
|
903
|
-
}
|
|
904
|
-
throw error;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
if (!server || selectedPort === null) {
|
|
908
|
-
tokenManager.clear();
|
|
909
|
-
throw new Error(`No available port in configured range (${ports[0]}-${ports[ports.length - 1]}).`);
|
|
910
|
-
}
|
|
911
|
-
installProcessSafetyNet(logger);
|
|
912
|
-
let shuttingDown = false;
|
|
913
|
-
const shutdown = async () => {
|
|
914
|
-
if (shuttingDown) {
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
shuttingDown = true;
|
|
918
|
-
try {
|
|
919
|
-
await withTimeout(server.close(), 5_000, "Timed out while draining requests.");
|
|
920
|
-
}
|
|
921
|
-
catch (error) {
|
|
922
|
-
logger.warn({ err: error }, "graceful shutdown timed out");
|
|
923
|
-
}
|
|
924
|
-
finally {
|
|
925
|
-
tokenManager.clear();
|
|
926
|
-
releaseLock();
|
|
927
|
-
process.exit(0);
|
|
928
|
-
}
|
|
929
|
-
};
|
|
930
|
-
process.once("SIGINT", () => {
|
|
931
|
-
void shutdown();
|
|
932
|
-
});
|
|
933
|
-
process.once("SIGTERM", () => {
|
|
934
|
-
void shutdown();
|
|
935
|
-
});
|
|
936
|
-
return { kind: "started", port: selectedPort, callerSecret };
|
|
937
|
-
}
|
|
938
|
-
function candidatePorts(preferredPort) {
|
|
939
|
-
const ports = [];
|
|
940
|
-
for (let offset = 0; offset < 10; offset += 1) {
|
|
941
|
-
const port = preferredPort + offset;
|
|
942
|
-
if (port <= 65535) {
|
|
943
|
-
ports.push(port);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
return ports;
|
|
947
|
-
}
|
|
948
|
-
function describeBackend(backend) {
|
|
949
|
-
switch (backend) {
|
|
950
|
-
case "keyring":
|
|
951
|
-
return "OS keychain";
|
|
952
|
-
case "file":
|
|
953
|
-
return "credentials file";
|
|
954
|
-
case "session":
|
|
955
|
-
return "in-memory (session only)";
|
|
956
|
-
default:
|
|
957
|
-
return "no backend";
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
function formatHumanAuthStatusLine(backend, identity) {
|
|
961
|
-
if (!identity) {
|
|
962
|
-
return `logged in (${describeBackend(backend)})`;
|
|
963
|
-
}
|
|
964
|
-
const nameSuffix = identity.name && identity.name !== identity.login ? ` (${identity.name})` : "";
|
|
965
|
-
return `logged in as @${identity.login}${nameSuffix} (${describeBackend(backend)})`;
|
|
966
|
-
}
|
|
967
|
-
function emitDeprecation(opts, oldCmd, newCmd) {
|
|
968
|
-
if (opts.json) {
|
|
969
|
-
// Keep stdout pristine for JSON consumers; deprecation goes to stderr.
|
|
970
|
-
process.stderr.write(`note: \`copillm ${oldCmd}\` is deprecated; use \`copillm ${newCmd}\`\n`);
|
|
971
|
-
}
|
|
972
|
-
else {
|
|
973
|
-
process.stderr.write(`note: \`copillm ${oldCmd}\` is deprecated; use \`copillm ${newCmd}\`\n`);
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
async function runAuthLogin(opts, options) {
|
|
977
|
-
if (options.forceSession) {
|
|
978
|
-
process.env.COPILLM_FORCE_SESSION_BACKEND = "1";
|
|
979
|
-
}
|
|
980
|
-
const config = loadConfig();
|
|
981
|
-
const token = await loginViaDeviceFlow();
|
|
982
|
-
const saveMode = options.forceSession ? "session" : "auto";
|
|
983
|
-
const backend = await saveStoredCredential(token, config.accountType, { mode: saveMode });
|
|
984
|
-
writeCommandOutput(opts, `Login succeeded. Credentials stored via ${describeBackend(backend)}.`, {
|
|
985
|
-
status: "ok",
|
|
986
|
-
action: "login",
|
|
987
|
-
credential_backend: backend
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
async function runAuthLogout(opts) {
|
|
991
|
-
const result = await clearStoredCredential();
|
|
992
|
-
const lockState = inspectLock();
|
|
993
|
-
if (lockState.state === "running") {
|
|
994
|
-
await stopByPid(lockState.lock.pid);
|
|
995
|
-
}
|
|
996
|
-
else if (lockState.state === "stale") {
|
|
997
|
-
releaseLock();
|
|
998
|
-
}
|
|
999
|
-
const credentialStatus = result.removed ? "removed" : "not present";
|
|
1000
|
-
writeCommandOutput(opts, `Logged out. Credentials ${credentialStatus} from ${describeBackend(result.backend)}.`, {
|
|
1001
|
-
status: "ok",
|
|
1002
|
-
action: "logout",
|
|
1003
|
-
credential_backend: result.backend,
|
|
1004
|
-
credential_removed: result.removed
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
|
-
/**
|
|
1008
|
-
* Build the default dependency bundle for ensureAuthenticatedInteractive.
|
|
1009
|
-
* Lives here (rather than inside the auth module) so the auth module stays
|
|
1010
|
-
* UI-framework-agnostic and tests can supply alternative implementations.
|
|
1011
|
-
*/
|
|
1012
|
-
function defaultEnsureAuthDeps() {
|
|
1013
|
-
return {
|
|
1014
|
-
inspectStoredCredential,
|
|
1015
|
-
isTty: () => process.stdin.isTTY === true,
|
|
1016
|
-
confirm,
|
|
1017
|
-
choose,
|
|
1018
|
-
loginViaDeviceFlow,
|
|
1019
|
-
loadAccountType: () => loadConfig().accountType,
|
|
1020
|
-
saveStoredCredential,
|
|
1021
|
-
describeBackend,
|
|
1022
|
-
print: (line) => process.stdout.write(line),
|
|
1023
|
-
setEnv: (key, value) => {
|
|
1024
|
-
process.env[key] = value;
|
|
1025
|
-
}
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1028
|
-
async function ensureAuthenticatedInteractive() {
|
|
1029
|
-
return ensureAuthenticatedInteractiveImpl(defaultEnsureAuthDeps());
|
|
1030
|
-
}
|
|
1031
|
-
function writeCommandOutput(opts, humanLine, payload) {
|
|
1032
|
-
if (opts.json) {
|
|
1033
|
-
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
process.stdout.write(`${humanLine}\n`);
|
|
1037
|
-
}
|
|
1038
|
-
function resolveCopillmDebug(commandDebug) {
|
|
1039
|
-
return Boolean(commandDebug) || Boolean(program.opts().debug);
|
|
1040
|
-
}
|
|
1041
|
-
function enableRuntimeDebug(debug) {
|
|
1042
|
-
if (!debug) {
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
process.env.COPILLM_LOG_LEVEL = "debug";
|
|
1046
|
-
logger.level = "debug";
|
|
1047
|
-
}
|
|
1048
|
-
function currentDebugLogPath(debug) {
|
|
1049
|
-
if (!debug) {
|
|
1050
|
-
return null;
|
|
1051
|
-
}
|
|
1052
|
-
return process.env.COPILLM_LOG_FILE ?? debugLogPath();
|
|
1053
|
-
}
|
|
1054
|
-
function daemonSpawnEnv(debug) {
|
|
1055
|
-
if (!debug) {
|
|
1056
|
-
return process.env;
|
|
1057
|
-
}
|
|
1058
|
-
return {
|
|
1059
|
-
...process.env,
|
|
1060
|
-
COPILLM_LOG_LEVEL: "debug",
|
|
1061
|
-
COPILLM_LOG_FILE: currentDebugLogPath(true) ?? debugLogPath()
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
function formatStopHumanLine(primary, cache) {
|
|
1065
|
-
if (cache.cleared) {
|
|
1066
|
-
return `${primary} Cleared Claude Code gateway cache.`;
|
|
1067
|
-
}
|
|
1068
|
-
if (cache.reason === "not_present") {
|
|
1069
|
-
return primary;
|
|
1070
|
-
}
|
|
1071
|
-
return `${primary} Could not clear Claude Code gateway cache: ${cache.reason ?? "unknown error"}.`;
|
|
1072
|
-
}
|
|
1073
|
-
function writeHealthOutput(opts, payload) {
|
|
1074
|
-
if (opts.json) {
|
|
1075
|
-
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
1079
|
-
}
|
|
1080
|
-
function isAddrInUse(error) {
|
|
1081
|
-
return error instanceof Error && "code" in error && error.code === "EADDRINUSE";
|
|
1082
|
-
}
|
|
1083
|
-
async function refreshCodexHome(port, model) {
|
|
1084
|
-
try {
|
|
1085
|
-
const home = getCopillmHome();
|
|
1086
|
-
return await generateCodexHome({
|
|
1087
|
-
outDir: defaultOutputDir(home),
|
|
1088
|
-
model,
|
|
1089
|
-
port,
|
|
1090
|
-
providerId: "copillm",
|
|
1091
|
-
reasoningEffort: null
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
catch (error) {
|
|
1095
|
-
const message = error instanceof Error ? error.message : "unknown_error";
|
|
1096
|
-
process.stderr.write(`warning: failed to generate ~/.copillm/codex/ — ${message}\n`);
|
|
1097
|
-
return null;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
async function refreshPiHome(port) {
|
|
1101
|
-
try {
|
|
1102
|
-
const home = getCopillmHome();
|
|
1103
|
-
return await generatePiHome({
|
|
1104
|
-
outDir: defaultPiOutputDir(home),
|
|
1105
|
-
port,
|
|
1106
|
-
providerId: "copillm"
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
catch (error) {
|
|
1110
|
-
const message = error instanceof Error ? error.message : "unknown_error";
|
|
1111
|
-
process.stderr.write(`warning: failed to generate pi models.json — ${message}\n`);
|
|
1112
|
-
return null;
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
function buildClaudeExportCommand(port, callerSecret) {
|
|
1116
|
-
const modelIds = readModelIdsFromCache();
|
|
1117
|
-
const defaults = computeAnthropicDefaults(modelIds);
|
|
1118
|
-
const command = buildClaudeExport({
|
|
1119
|
-
port,
|
|
1120
|
-
callerSecret,
|
|
1121
|
-
defaults,
|
|
1122
|
-
enableGatewayDiscovery: true
|
|
1123
|
-
});
|
|
1124
|
-
const bundle = buildClaudeEnvBundle({ port, callerSecret, defaults, enableGatewayDiscovery: true });
|
|
1125
|
-
return { command, defaults, bundle };
|
|
1126
|
-
}
|
|
1127
|
-
function formatStartBanner(input) {
|
|
1128
|
-
const verb = input.mode === "foreground" ? "listening on" : "running on";
|
|
1129
|
-
const lines = [];
|
|
1130
|
-
const debugSuffix = input.debug ? " [debug]" : "";
|
|
1131
|
-
const modeSuffix = input.mode === "already_running" ? " (already running)" : "";
|
|
1132
|
-
lines.push(`\u25CF copillm ${verb} http://127.0.0.1:${input.port} (pid ${input.pid})${debugSuffix}${modeSuffix}`);
|
|
1133
|
-
if (input.codex) {
|
|
1134
|
-
lines.push(` ${input.codex.modelCount} Copilot models discovered \u00B7 default: ${input.codex.defaultModel}`);
|
|
1135
|
-
}
|
|
1136
|
-
if (input.debugLogPath) {
|
|
1137
|
-
lines.push(` debug log: ${displayHomePath(input.debugLogPath)}`);
|
|
1138
|
-
}
|
|
1139
|
-
if (input.pi) {
|
|
1140
|
-
lines.push(` pi: wrote ${input.pi.modelCount} models to ${displayHomePath(input.pi.configPath)}${input.pi.backupPath ? ` (backed up prior config to ${displayHomePath(input.pi.backupPath)})` : ""}`);
|
|
1141
|
-
}
|
|
1142
|
-
lines.push(``);
|
|
1143
|
-
lines.push(`Launch an agent against copillm:`);
|
|
1144
|
-
if (input.codex) {
|
|
1145
|
-
lines.push(` copillm codex # starts Codex CLI, preconfigured`);
|
|
1146
|
-
}
|
|
1147
|
-
lines.push(` copillm claude # starts Claude Code, preconfigured`);
|
|
1148
|
-
if (input.pi) {
|
|
1149
|
-
lines.push(` copillm pi # starts pi coding agent, preconfigured`);
|
|
1150
|
-
}
|
|
1151
|
-
lines.push(``);
|
|
1152
|
-
lines.push(`Or print env vars to use yourself:`);
|
|
1153
|
-
if (input.codex) {
|
|
1154
|
-
lines.push(` copillm env codex`);
|
|
1155
|
-
}
|
|
1156
|
-
lines.push(` copillm env claude`);
|
|
1157
|
-
if (input.pi) {
|
|
1158
|
-
lines.push(` copillm env pi`);
|
|
1159
|
-
}
|
|
1160
|
-
return lines.join("\n");
|
|
1161
|
-
}
|
|
1162
|
-
function displayHomePath(p) {
|
|
1163
|
-
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
1164
|
-
if (home && p.startsWith(home)) {
|
|
1165
|
-
return p.replace(home, "~");
|
|
1166
|
-
}
|
|
1167
|
-
return p;
|
|
1168
|
-
}
|
|
1169
|
-
async function probeLivez(port) {
|
|
1170
|
-
try {
|
|
1171
|
-
const response = await fetch(`http://127.0.0.1:${port}/livez`, { signal: AbortSignal.timeout(800) });
|
|
1172
|
-
return response.ok;
|
|
1173
|
-
}
|
|
1174
|
-
catch {
|
|
1175
|
-
return false;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
async function warnIfDebugRequestedButInactive(debugRequested, port) {
|
|
1179
|
-
if (!debugRequested) {
|
|
1180
|
-
return false;
|
|
1181
|
-
}
|
|
1182
|
-
const active = await probeDebugEndpoint(port);
|
|
1183
|
-
if (!active) {
|
|
1184
|
-
process.stderr.write(`warning: copillm is already running without debug mode; run \`copillm stop\` then \`copillm --debug start --detach\` to enable daemon diagnostics.\n`);
|
|
1185
|
-
}
|
|
1186
|
-
return active;
|
|
1187
|
-
}
|
|
1188
|
-
async function probeDebugEndpoint(port) {
|
|
1189
|
-
try {
|
|
1190
|
-
const response = await fetch(`http://127.0.0.1:${port}/_debug`, { signal: AbortSignal.timeout(1_200) });
|
|
1191
|
-
return response.ok;
|
|
1192
|
-
}
|
|
1193
|
-
catch {
|
|
1194
|
-
return false;
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
async function probeHealth(port) {
|
|
1198
|
-
try {
|
|
1199
|
-
const response = await fetch(`http://127.0.0.1:${port}/healthz`, { signal: AbortSignal.timeout(1_500) });
|
|
1200
|
-
const payload = (await response.json());
|
|
1201
|
-
return {
|
|
1202
|
-
ok: response.ok,
|
|
1203
|
-
statusCode: response.status,
|
|
1204
|
-
status: typeof payload.status === "string" ? payload.status : null,
|
|
1205
|
-
error: typeof payload.error === "string" ? payload.error : null,
|
|
1206
|
-
bearerTtlSeconds: response.ok && typeof payload.bearer_ttl_seconds === "number" ? payload.bearer_ttl_seconds : null
|
|
1207
|
-
};
|
|
1208
|
-
}
|
|
1209
|
-
catch {
|
|
1210
|
-
return { ok: false, bearerTtlSeconds: null, statusCode: null, status: null, error: "health_probe_failed" };
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
async function waitForDaemonReady(pid, timeoutMs) {
|
|
1214
|
-
const startedAt = Date.now();
|
|
1215
|
-
while (Date.now() - startedAt <= timeoutMs) {
|
|
1216
|
-
const lockState = inspectLock();
|
|
1217
|
-
if (lockState.state === "running" && (await probeLivez(lockState.lock.port))) {
|
|
1218
|
-
return { pid: lockState.lock.pid, port: lockState.lock.port };
|
|
1219
|
-
}
|
|
1220
|
-
if (pid !== null && !isPidAlive(pid)) {
|
|
1221
|
-
return null;
|
|
1222
|
-
}
|
|
1223
|
-
await sleep(150);
|
|
1224
|
-
}
|
|
1225
|
-
return null;
|
|
1226
|
-
}
|
|
1227
|
-
function isPidAlive(pid) {
|
|
1228
|
-
try {
|
|
1229
|
-
process.kill(pid, 0);
|
|
1230
|
-
return true;
|
|
1231
|
-
}
|
|
1232
|
-
catch {
|
|
1233
|
-
return false;
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
async function stopByPid(pid) {
|
|
1237
|
-
if (!sendSignalIfAlive(pid, "SIGTERM")) {
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
const stopDeadline = Date.now() + 8_000;
|
|
1241
|
-
while (Date.now() < stopDeadline) {
|
|
1242
|
-
const lockState = inspectLock();
|
|
1243
|
-
if (lockState.state !== "running" || lockState.lock.pid !== pid) {
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1246
|
-
await sleep(150);
|
|
1247
|
-
}
|
|
1248
|
-
if (!sendSignalIfAlive(pid, "SIGKILL")) {
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
const killDeadline = Date.now() + 2_000;
|
|
1252
|
-
while (Date.now() < killDeadline) {
|
|
1253
|
-
const lockState = inspectLock();
|
|
1254
|
-
if (lockState.state !== "running" || lockState.lock.pid !== pid) {
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1257
|
-
await sleep(100);
|
|
1258
|
-
}
|
|
1259
|
-
throw new Error(`Failed to stop daemon pid ${pid}.`);
|
|
1260
|
-
}
|
|
1261
|
-
async function withTimeout(promise, timeoutMs, message) {
|
|
1262
|
-
const timeoutPromise = sleep(timeoutMs).then(() => {
|
|
1263
|
-
throw new Error(message);
|
|
1264
|
-
});
|
|
1265
|
-
return Promise.race([promise, timeoutPromise]);
|
|
1266
|
-
}
|
|
1267
|
-
function sendSignalIfAlive(pid, signal) {
|
|
1268
|
-
try {
|
|
1269
|
-
process.kill(pid, signal);
|
|
1270
|
-
return true;
|
|
1271
|
-
}
|
|
1272
|
-
catch (error) {
|
|
1273
|
-
if (error instanceof Error && "code" in error && error.code === "ESRCH") {
|
|
1274
|
-
return false;
|
|
1275
|
-
}
|
|
1276
|
-
throw error;
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
async function readLiveLock() {
|
|
1280
|
-
const lockState = inspectLock();
|
|
1281
|
-
if (lockState.state !== "running") {
|
|
1282
|
-
return null;
|
|
1283
|
-
}
|
|
1284
|
-
return (await probeLivez(lockState.lock.port)) ? lockState.lock : null;
|
|
1285
|
-
}
|
|
1286
|
-
function computeUptimeSeconds(startedAtIso) {
|
|
1287
|
-
const startedMs = Date.parse(startedAtIso);
|
|
1288
|
-
if (!Number.isFinite(startedMs)) {
|
|
1289
|
-
return null;
|
|
1290
|
-
}
|
|
1291
|
-
return Math.max(0, Math.floor((Date.now() - startedMs) / 1000));
|
|
1292
|
-
}
|
|
1293
|
-
function parseAgentName(raw) {
|
|
1294
|
-
const v = raw.trim().toLowerCase();
|
|
1295
|
-
if (v === "codex" || v === "claude" || v === "pi")
|
|
1296
|
-
return v;
|
|
1297
|
-
throw new Error(`Unknown agent: ${raw}. Expected "codex", "claude", or "pi".`);
|
|
1298
|
-
}
|
|
1299
|
-
async function ensureDaemonRunningForLauncher(opts) {
|
|
1300
|
-
const live = await readLiveLock();
|
|
1301
|
-
if (live) {
|
|
1302
|
-
await warnIfDebugRequestedButInactive(opts.debug, live.port);
|
|
1303
|
-
return live;
|
|
1304
|
-
}
|
|
1305
|
-
// Fail fast on missing credentials rather than spawning a detached daemon
|
|
1306
|
-
// that will die silently and surface as a generic "start timed out" error.
|
|
1307
|
-
const authState = await inspectStoredCredential();
|
|
1308
|
-
if (!authState.stored) {
|
|
1309
|
-
throw new Error("Not authenticated. Run `copillm auth login` first.");
|
|
1310
|
-
}
|
|
1311
|
-
const debugLog = currentDebugLogPath(opts.debug);
|
|
1312
|
-
process.stderr.write(opts.debug && debugLog
|
|
1313
|
-
? `Starting copillm in background with debug logging at ${displayHomePath(debugLog)}...\n`
|
|
1314
|
-
: `Starting copillm in background...\n`);
|
|
1315
|
-
const daemonArgs = [process.argv[1], "daemon"];
|
|
1316
|
-
if (opts.debug)
|
|
1317
|
-
daemonArgs.push("--debug");
|
|
1318
|
-
const child = spawn(process.execPath, daemonArgs, {
|
|
1319
|
-
detached: true,
|
|
1320
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
1321
|
-
env: daemonSpawnEnv(opts.debug)
|
|
1322
|
-
});
|
|
1323
|
-
child.unref();
|
|
1324
|
-
const stderrChunks = [];
|
|
1325
|
-
let stderrBytes = 0;
|
|
1326
|
-
const STDERR_TAIL_LIMIT = 8 * 1024;
|
|
1327
|
-
if (child.stderr) {
|
|
1328
|
-
child.stderr.on("data", (chunk) => {
|
|
1329
|
-
stderrChunks.push(chunk);
|
|
1330
|
-
stderrBytes += chunk.length;
|
|
1331
|
-
while (stderrBytes > STDERR_TAIL_LIMIT && stderrChunks.length > 1) {
|
|
1332
|
-
stderrBytes -= stderrChunks[0].length;
|
|
1333
|
-
stderrChunks.shift();
|
|
1334
|
-
}
|
|
1335
|
-
});
|
|
1336
|
-
child.stderr.on("error", () => {
|
|
1337
|
-
// Ignore — best-effort capture only.
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
const formatStderrTail = () => {
|
|
1341
|
-
const tail = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
1342
|
-
return tail ? `\nDaemon stderr (tail):\n${tail}` : "";
|
|
1343
|
-
};
|
|
1344
|
-
const started = await waitForDaemonReady(child.pid ?? null, 10_000);
|
|
1345
|
-
if (!started) {
|
|
1346
|
-
if (child.pid !== undefined && !isPidAlive(child.pid)) {
|
|
1347
|
-
throw new Error(`copillm daemon exited before becoming ready.${formatStderrTail()}`);
|
|
1348
|
-
}
|
|
1349
|
-
throw new Error(`Auto-start of copillm daemon timed out.${formatStderrTail()}`);
|
|
1350
|
-
}
|
|
1351
|
-
const inspection = inspectLock();
|
|
1352
|
-
if (inspection.state !== "running") {
|
|
1353
|
-
throw new Error(`copillm daemon failed to register a lock after auto-start.${formatStderrTail()}`);
|
|
1354
|
-
}
|
|
1355
|
-
return inspection.lock;
|
|
1356
|
-
}
|
|
2
|
+
import "./cli/index.js";
|