copillm 0.2.2 → 0.2.4

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