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.
Files changed (48) 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 +358 -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 +111 -0
  19. package/dist/cli/daemon/ensureRunning.js +65 -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/selfSpawn.js +15 -0
  24. package/dist/cli/daemon/spawnEnv.js +12 -0
  25. package/dist/cli/index.js +41 -0
  26. package/dist/cli/integrations/banner.js +51 -0
  27. package/dist/cli/integrations/claudeExport.js +14 -0
  28. package/dist/cli/integrations/refreshCodex.js +19 -0
  29. package/dist/cli/integrations/refreshPi.js +17 -0
  30. package/dist/cli/packageInfo.js +29 -0
  31. package/dist/cli/shared/backends.js +31 -0
  32. package/dist/cli/shared/debug.js +44 -0
  33. package/dist/cli/shared/deprecation.js +7 -0
  34. package/dist/cli/shared/exitCodes.js +9 -0
  35. package/dist/cli/shared/output.js +14 -0
  36. package/dist/cli/shared/parseAgent.js +6 -0
  37. package/dist/cli/updateNotifier.js +223 -0
  38. package/dist/cli.js +1 -1355
  39. package/dist/server/errors.js +195 -0
  40. package/dist/server/proxy.js +50 -885
  41. package/dist/server/routes/debug.js +65 -0
  42. package/dist/server/routes/health.js +32 -0
  43. package/dist/server/routes/models.js +41 -0
  44. package/dist/server/routes/proxyForward.js +108 -0
  45. package/dist/server/routes/shared.js +161 -0
  46. package/dist/server/upstream/copilotClient.js +137 -0
  47. package/dist/server/upstream/streaming.js +146 -0
  48. package/package.json +7 -2
@@ -0,0 +1,358 @@
1
+ import { spawn } from "node:child_process";
2
+ import { inspectStoredCredential } from "../../auth/credentials.js";
3
+ import { loadConfig } from "../../config/config.js";
4
+ import { clearClaudeGatewayCache } from "../../integrations/claude/cache.js";
5
+ import { inspectLock, releaseLock } from "../../server/lock.js";
6
+ import { buildCodexEnvBundle } from "../agentEnv.js";
7
+ import { ensureAuthenticatedInteractive } from "../auth/ensure.js";
8
+ import { computeUptimeSeconds, stopByPid } from "../daemon/lifecycle.js";
9
+ import { probeHealth, readLiveLock, waitForDaemonReady, warnIfDebugRequestedButInactive } from "../daemon/probes.js";
10
+ import { runDaemon } from "../daemon/runDaemon.js";
11
+ import { daemonSpawnEnv } from "../daemon/spawnEnv.js";
12
+ import { buildClaudeExportCommand } from "../integrations/claudeExport.js";
13
+ import { formatStartBanner, formatStopHumanLine } from "../integrations/banner.js";
14
+ import { refreshCodexHome } from "../integrations/refreshCodex.js";
15
+ import { refreshPiHome } from "../integrations/refreshPi.js";
16
+ import { writeAuthStatusLine } from "../shared/backends.js";
17
+ import { currentDebugLogPath, enableRuntimeDebug, getRootLogger, resolveCopillmDebug } from "../shared/debug.js";
18
+ import { writeCommandOutput, writeHealthOutput } from "../shared/output.js";
19
+ import { buildSelfSpawnCommand } from "../daemon/selfSpawn.js";
20
+ export function register(program) {
21
+ program
22
+ .command("start")
23
+ .description("Start proxy")
24
+ .option("--detach", "Run in detached mode")
25
+ .option("--debug", "Enable debug endpoints (e.g. /_debug)")
26
+ .option("--no-codex", "Skip generating ~/.copillm/codex/ for Codex CLI")
27
+ .option("--codex-model <id>", "Default Codex model slug")
28
+ .option("--no-pi", "Skip generating ~/.pi/agent/models.json for pi coding agent")
29
+ .option("--json", "JSON output")
30
+ .action(async (opts) => {
31
+ const debug = resolveCopillmDebug(opts.debug);
32
+ enableRuntimeDebug(debug);
33
+ if (opts.detach) {
34
+ // Fail fast on missing credentials rather than letting the detached
35
+ // child die silently and surface as a generic "start timed out" error.
36
+ const authState = await inspectStoredCredential();
37
+ if (!authState.stored) {
38
+ throw new Error("Not authenticated. Run `copillm auth login` first, or start without --detach to log in interactively.");
39
+ }
40
+ const existingLock = await readLiveLock();
41
+ if (existingLock) {
42
+ const activeDebug = await warnIfDebugRequestedButInactive(debug, existingLock.port);
43
+ const codex = opts.codex === false ? null : await refreshCodexHome(existingLock.port, opts.codexModel ?? null);
44
+ const pi = opts.pi === false ? null : await refreshPiHome(existingLock.port);
45
+ const claude = buildClaudeExportCommand(existingLock.port, null);
46
+ const banner = formatStartBanner({
47
+ port: existingLock.port,
48
+ pid: existingLock.pid,
49
+ mode: "already_running",
50
+ debug: activeDebug,
51
+ debugLogPath: null,
52
+ codex,
53
+ pi
54
+ });
55
+ writeCommandOutput(opts, banner, {
56
+ status: "already_running",
57
+ pid: existingLock.pid,
58
+ port: existingLock.port,
59
+ debug: activeDebug,
60
+ url: `http://127.0.0.1:${existingLock.port}`,
61
+ codex_home: codex?.outDir ?? null,
62
+ codex_export_command: codex?.exportCommand ?? null,
63
+ codex_env: codex ? buildCodexEnvBundle(codex.outDir).env : null,
64
+ pi_home: pi?.outDir ?? null,
65
+ pi_config_path: pi?.configPath ?? null,
66
+ pi_mirror_path: pi?.mirrorPath ?? null,
67
+ pi_backup_path: pi?.backupPath ?? null,
68
+ pi_model_count: pi?.modelCount ?? null,
69
+ claude_export_command: claude.command,
70
+ claude_env: claude.bundle.env,
71
+ claude_defaults: claude.defaults
72
+ });
73
+ return;
74
+ }
75
+ const daemonCommand = buildSelfSpawnCommand("daemon");
76
+ if (debug) {
77
+ daemonCommand.args.push("--debug");
78
+ }
79
+ const child = spawn(daemonCommand.command, daemonCommand.args, {
80
+ detached: true,
81
+ stdio: "ignore",
82
+ env: daemonSpawnEnv(debug)
83
+ });
84
+ child.unref();
85
+ const started = await waitForDaemonReady(child.pid ?? null, 8_000);
86
+ if (!started) {
87
+ throw new Error("Detached daemon start timed out.");
88
+ }
89
+ const codex = opts.codex === false ? null : await refreshCodexHome(started.port, opts.codexModel ?? null);
90
+ const pi = opts.pi === false ? null : await refreshPiHome(started.port);
91
+ const claude = buildClaudeExportCommand(started.port, null);
92
+ const banner = formatStartBanner({
93
+ port: started.port,
94
+ pid: started.pid,
95
+ mode: "detached",
96
+ debug,
97
+ debugLogPath: currentDebugLogPath(debug),
98
+ codex,
99
+ pi
100
+ });
101
+ writeCommandOutput(opts, banner, {
102
+ status: "ok",
103
+ mode: "detached",
104
+ pid: started.pid,
105
+ port: started.port,
106
+ debug,
107
+ debug_log_path: currentDebugLogPath(debug),
108
+ url: `http://127.0.0.1:${started.port}`,
109
+ codex_home: codex?.outDir ?? null,
110
+ codex_export_command: codex?.exportCommand ?? null,
111
+ codex_env: codex ? buildCodexEnvBundle(codex.outDir).env : null,
112
+ codex_default_model: codex?.defaultModel ?? null,
113
+ codex_model_count: codex?.modelCount ?? null,
114
+ pi_home: pi?.outDir ?? null,
115
+ pi_config_path: pi?.configPath ?? null,
116
+ pi_mirror_path: pi?.mirrorPath ?? null,
117
+ pi_backup_path: pi?.backupPath ?? null,
118
+ pi_model_count: pi?.modelCount ?? null,
119
+ claude_export_command: claude.command,
120
+ claude_env: claude.bundle.env,
121
+ claude_defaults: claude.defaults
122
+ });
123
+ return;
124
+ }
125
+ // Foreground path: interactively prompt for login if needed.
126
+ await ensureAuthenticatedInteractive();
127
+ const started = await runDaemon({ debug });
128
+ if (started.kind === "already_running") {
129
+ const activeDebug = await warnIfDebugRequestedButInactive(debug, started.lock.port);
130
+ const codex = opts.codex === false ? null : await refreshCodexHome(started.lock.port, opts.codexModel ?? null);
131
+ const pi = opts.pi === false ? null : await refreshPiHome(started.lock.port);
132
+ const claude = buildClaudeExportCommand(started.lock.port, null);
133
+ const banner = formatStartBanner({
134
+ port: started.lock.port,
135
+ pid: started.lock.pid,
136
+ mode: "already_running",
137
+ debug: activeDebug,
138
+ debugLogPath: null,
139
+ codex,
140
+ pi
141
+ });
142
+ writeCommandOutput(opts, banner, {
143
+ status: "already_running",
144
+ pid: started.lock.pid,
145
+ port: started.lock.port,
146
+ debug: activeDebug,
147
+ url: `http://127.0.0.1:${started.lock.port}`,
148
+ codex_home: codex?.outDir ?? null,
149
+ codex_export_command: codex?.exportCommand ?? null,
150
+ codex_env: codex ? buildCodexEnvBundle(codex.outDir).env : null,
151
+ pi_home: pi?.outDir ?? null,
152
+ pi_config_path: pi?.configPath ?? null,
153
+ pi_mirror_path: pi?.mirrorPath ?? null,
154
+ pi_backup_path: pi?.backupPath ?? null,
155
+ pi_model_count: pi?.modelCount ?? null,
156
+ claude_export_command: claude.command,
157
+ claude_env: claude.bundle.env,
158
+ claude_defaults: claude.defaults
159
+ });
160
+ return;
161
+ }
162
+ const codex = opts.codex === false ? null : await refreshCodexHome(started.port, opts.codexModel ?? null);
163
+ const pi = opts.pi === false ? null : await refreshPiHome(started.port);
164
+ const claude = buildClaudeExportCommand(started.port, started.callerSecret);
165
+ const banner = formatStartBanner({
166
+ port: started.port,
167
+ pid: process.pid,
168
+ mode: "foreground",
169
+ debug,
170
+ debugLogPath: currentDebugLogPath(debug),
171
+ codex,
172
+ pi
173
+ });
174
+ writeCommandOutput(opts, banner, {
175
+ status: "ok",
176
+ mode: "foreground",
177
+ pid: process.pid,
178
+ port: started.port,
179
+ debug,
180
+ debug_log_path: currentDebugLogPath(debug),
181
+ url: `http://127.0.0.1:${started.port}`,
182
+ caller_secret: started.callerSecret,
183
+ codex_home: codex?.outDir ?? null,
184
+ codex_export_command: codex?.exportCommand ?? null,
185
+ codex_env: codex ? buildCodexEnvBundle(codex.outDir).env : null,
186
+ codex_default_model: codex?.defaultModel ?? null,
187
+ codex_model_count: codex?.modelCount ?? null,
188
+ pi_home: pi?.outDir ?? null,
189
+ pi_config_path: pi?.configPath ?? null,
190
+ pi_mirror_path: pi?.mirrorPath ?? null,
191
+ pi_backup_path: pi?.backupPath ?? null,
192
+ pi_model_count: pi?.modelCount ?? null,
193
+ claude_export_command: claude.command,
194
+ claude_env: claude.bundle.env,
195
+ claude_defaults: claude.defaults
196
+ });
197
+ });
198
+ program
199
+ .command("daemon")
200
+ .description("Internal background command")
201
+ .option("--debug", "Enable debug endpoints")
202
+ .action(async (opts) => {
203
+ const debug = resolveCopillmDebug(opts.debug);
204
+ enableRuntimeDebug(debug);
205
+ try {
206
+ const started = await runDaemon({ debug });
207
+ if (started.kind === "already_running") {
208
+ process.exit(0);
209
+ }
210
+ process.stdout.write(`copillm listening on http://127.0.0.1:${started.port}${debug ? " [debug]" : ""}\n`);
211
+ }
212
+ catch (err) {
213
+ const message = err instanceof Error ? err.message : String(err);
214
+ getRootLogger().fatal({ err }, "daemon failed to start");
215
+ process.stderr.write(`copillm daemon: ${message}\n`);
216
+ process.exit(1);
217
+ }
218
+ });
219
+ program
220
+ .command("stop")
221
+ .description("Stop detached daemon")
222
+ .option("--json", "JSON output")
223
+ .action(async (opts) => {
224
+ const lockState = inspectLock();
225
+ if (lockState.state === "missing") {
226
+ const cache = clearClaudeGatewayCache();
227
+ writeCommandOutput(opts, formatStopHumanLine("Not running.", cache), {
228
+ status: "not_running",
229
+ claude_cache: cache
230
+ });
231
+ return;
232
+ }
233
+ if (lockState.state === "stale") {
234
+ releaseLock();
235
+ const cache = clearClaudeGatewayCache();
236
+ writeCommandOutput(opts, formatStopHumanLine("Removed stale lock.", cache), {
237
+ status: "stale_lock_removed",
238
+ reason: lockState.reason,
239
+ claude_cache: cache
240
+ });
241
+ return;
242
+ }
243
+ await stopByPid(lockState.lock.pid);
244
+ const cache = clearClaudeGatewayCache();
245
+ writeCommandOutput(opts, formatStopHumanLine("Stopped.", cache), {
246
+ status: "ok",
247
+ pid: lockState.lock.pid,
248
+ claude_cache: cache
249
+ });
250
+ });
251
+ program
252
+ .command("status")
253
+ .description("Show daemon status")
254
+ .option("--json", "JSON output")
255
+ .action(async (opts) => {
256
+ const config = loadConfig();
257
+ const lockState = inspectLock();
258
+ const checkedAtIso = new Date().toISOString();
259
+ const uptimeSeconds = lockState.state === "running" ? computeUptimeSeconds(lockState.lock.started_at_iso) : null;
260
+ // inspectStoredCredential never returns the token itself, so it's safe to
261
+ // include the result in the status payload.
262
+ let authInfo;
263
+ try {
264
+ const info = await inspectStoredCredential();
265
+ authInfo = { stored: info.stored, backend: info.backend, error: null };
266
+ }
267
+ catch (error) {
268
+ const message = error instanceof Error ? error.message : "unknown_error";
269
+ authInfo = { stored: false, backend: null, error: message };
270
+ }
271
+ const status = {
272
+ running: lockState.state === "running",
273
+ stale: lockState.state === "stale",
274
+ pid: lockState.state === "running" ? lockState.lock.pid : null,
275
+ port: lockState.state === "running" ? lockState.lock.port : null,
276
+ started_at_iso: lockState.state === "running" ? lockState.lock.started_at_iso : null,
277
+ uptime_seconds: uptimeSeconds,
278
+ url: lockState.state === "running" ? `http://127.0.0.1:${lockState.lock.port}` : null,
279
+ require_caller_secret: config.requireCallerSecret,
280
+ account_type: config.accountType,
281
+ selected_models: config.selectedModels,
282
+ auth: authInfo,
283
+ bearer_ttl_seconds: null,
284
+ health_check_status_code: null,
285
+ health_state: null,
286
+ health_error: null,
287
+ health_status: "unknown",
288
+ checked_at_iso: checkedAtIso,
289
+ stale_reason: lockState.state === "stale" ? lockState.reason : null
290
+ };
291
+ if (lockState.state === "running") {
292
+ const health = await probeHealth(lockState.lock.port);
293
+ status.health_status = health.ok ? "ok" : "degraded";
294
+ status.bearer_ttl_seconds = health.bearerTtlSeconds;
295
+ status.health_check_status_code = health.statusCode;
296
+ status.health_state = health.status;
297
+ status.health_error = health.error;
298
+ }
299
+ if (opts.json) {
300
+ process.stdout.write(JSON.stringify(status, null, 2) + "\n");
301
+ return;
302
+ }
303
+ if (lockState.state === "running") {
304
+ process.stdout.write(`running (pid ${lockState.lock.pid}, port ${lockState.lock.port})\n`);
305
+ process.stdout.write(`health: ${status.health_status}`);
306
+ if (status.health_state) {
307
+ process.stdout.write(` (${status.health_state})`);
308
+ }
309
+ if (status.health_check_status_code !== null) {
310
+ process.stdout.write(` [http ${status.health_check_status_code}]`);
311
+ }
312
+ if (status.health_error) {
313
+ process.stdout.write(` error=${status.health_error}`);
314
+ }
315
+ process.stdout.write("\n");
316
+ if (status.bearer_ttl_seconds !== null) {
317
+ process.stdout.write(`bearer_ttl_seconds: ${status.bearer_ttl_seconds}\n`);
318
+ }
319
+ if (status.uptime_seconds !== null) {
320
+ process.stdout.write(`uptime_seconds: ${status.uptime_seconds}\n`);
321
+ }
322
+ writeAuthStatusLine(authInfo);
323
+ process.stdout.write(`checked_at: ${status.checked_at_iso}\n`);
324
+ return;
325
+ }
326
+ if (lockState.state === "stale") {
327
+ process.stdout.write(`stale lock (${lockState.reason})\n`);
328
+ writeAuthStatusLine(authInfo);
329
+ return;
330
+ }
331
+ process.stdout.write("not running\n");
332
+ writeAuthStatusLine(authInfo);
333
+ });
334
+ program
335
+ .command("health")
336
+ .description("Check health endpoint")
337
+ .option("--json", "JSON output")
338
+ .action(async (opts) => {
339
+ const lockState = inspectLock();
340
+ if (lockState.state !== "running") {
341
+ const payload = {
342
+ ok: false,
343
+ status: lockState.state === "stale" ? "stale_lock" : "not_running",
344
+ detail: lockState.state === "stale" ? lockState.reason : "Daemon is not running."
345
+ };
346
+ writeHealthOutput(opts, payload);
347
+ process.exitCode = 1;
348
+ return;
349
+ }
350
+ const response = await fetch(`http://127.0.0.1:${lockState.lock.port}/healthz`, { signal: AbortSignal.timeout(2_000) });
351
+ const payload = (await response.json());
352
+ const output = { ok: response.ok, status_code: response.status, ...payload };
353
+ writeHealthOutput(opts, output);
354
+ if (!response.ok) {
355
+ process.exitCode = 1;
356
+ }
357
+ });
358
+ }
@@ -0,0 +1,135 @@
1
+ import { detectClaudeSettingsConflicts, formatSettingsConflictWarning } from "../../integrations/claude/settingsConflict.js";
2
+ import { inspectLock } from "../../server/lock.js";
3
+ import { buildCodexEnvBundle, buildPiEnvBundle } from "../agentEnv.js";
4
+ import { isShellSyntax, renderEnvBlock } from "../envBlock.js";
5
+ import { buildClaudeExportCommand } from "../integrations/claudeExport.js";
6
+ import { refreshCodexHome } from "../integrations/refreshCodex.js";
7
+ import { refreshPiHome } from "../integrations/refreshPi.js";
8
+ import { parseAgentName } from "../shared/parseAgent.js";
9
+ export function register(program) {
10
+ program
11
+ .command("env <agent>")
12
+ .description("Print env vars to launch codex, claude, or pi against copillm")
13
+ .option("--shell <shell>", "Shell syntax: sh|fish|powershell", "sh")
14
+ .option("--json", "JSON output")
15
+ .option("--inline", "Single-line legacy export form (claude only)")
16
+ .action(async (agentRaw, opts) => {
17
+ const agent = parseAgentName(agentRaw);
18
+ if (!isShellSyntax(opts.shell)) {
19
+ throw new Error(`Unsupported --shell value: ${opts.shell}. Use sh, fish, or powershell.`);
20
+ }
21
+ const shell = opts.shell;
22
+ const lockState = inspectLock();
23
+ if (lockState.state !== "running") {
24
+ const message = lockState.state === "stale"
25
+ ? `copillm has a stale lock (${lockState.reason}). Run \`copillm stop\` then \`copillm start --detach\`.`
26
+ : "copillm is not running. Run `copillm start --detach` first.";
27
+ if (opts.json) {
28
+ process.stdout.write(JSON.stringify({ status: "not_running", agent, error: message }, null, 2) + "\n");
29
+ }
30
+ else {
31
+ process.stderr.write(`${message}\n`);
32
+ }
33
+ process.exit(2);
34
+ return;
35
+ }
36
+ if (agent === "codex") {
37
+ const codex = await refreshCodexHome(lockState.lock.port, null);
38
+ if (!codex) {
39
+ throw new Error("Failed to prepare Codex home (see warning above).");
40
+ }
41
+ const bundle = buildCodexEnvBundle(codex.outDir);
42
+ const block = renderEnvBlock({
43
+ agent: "codex",
44
+ env: bundle.env,
45
+ shell,
46
+ inlineComments: bundle.inlineComments,
47
+ trailingNotes: bundle.trailingNotes
48
+ });
49
+ if (opts.json) {
50
+ process.stdout.write(JSON.stringify({
51
+ agent: "codex",
52
+ package: "@openai/codex",
53
+ shell,
54
+ env: bundle.env,
55
+ shell_block: block
56
+ }, null, 2) + "\n");
57
+ }
58
+ else {
59
+ process.stdout.write(`${block}\n`);
60
+ }
61
+ process.exit(0);
62
+ }
63
+ if (agent === "pi") {
64
+ const pi = await refreshPiHome(lockState.lock.port);
65
+ if (!pi) {
66
+ throw new Error("Failed to prepare pi models.json (see warning above).");
67
+ }
68
+ const bundle = buildPiEnvBundle(pi.outDir);
69
+ const block = renderEnvBlock({
70
+ agent: "pi",
71
+ env: bundle.env,
72
+ shell,
73
+ inlineComments: bundle.inlineComments,
74
+ trailingNotes: bundle.trailingNotes
75
+ });
76
+ if (opts.json) {
77
+ process.stdout.write(JSON.stringify({
78
+ agent: "pi",
79
+ package: "@earendil-works/pi-coding-agent",
80
+ shell,
81
+ env: bundle.env,
82
+ shell_block: block,
83
+ pi_home: pi.outDir,
84
+ pi_config_path: pi.configPath,
85
+ pi_mirror_path: pi.mirrorPath,
86
+ pi_backup_path: pi.backupPath,
87
+ pi_model_count: pi.modelCount
88
+ }, null, 2) + "\n");
89
+ }
90
+ else {
91
+ process.stdout.write(`${block}\n`);
92
+ }
93
+ process.exit(0);
94
+ }
95
+ const claude = buildClaudeExportCommand(lockState.lock.port, null);
96
+ const settingsConflicts = detectClaudeSettingsConflicts(claude.bundle.env);
97
+ if (opts.inline) {
98
+ if (opts.json) {
99
+ process.stdout.write(JSON.stringify({ agent: "claude", inline: claude.command }, null, 2) + "\n");
100
+ }
101
+ else {
102
+ process.stdout.write(`${claude.command}\n`);
103
+ }
104
+ for (const line of formatSettingsConflictWarning(settingsConflicts)) {
105
+ process.stderr.write(`${line}\n`);
106
+ }
107
+ process.exit(0);
108
+ }
109
+ const block = renderEnvBlock({
110
+ agent: "claude",
111
+ env: claude.bundle.env,
112
+ shell,
113
+ inlineComments: claude.bundle.inlineComments,
114
+ trailingNotes: claude.bundle.trailingNotes
115
+ });
116
+ if (opts.json) {
117
+ process.stdout.write(JSON.stringify({
118
+ agent: "claude",
119
+ package: "@anthropic-ai/claude-code",
120
+ shell,
121
+ env: claude.bundle.env,
122
+ shell_block: block,
123
+ defaults: claude.defaults,
124
+ settings_conflicts: settingsConflicts.conflicts
125
+ }, null, 2) + "\n");
126
+ }
127
+ else {
128
+ process.stdout.write(`${block}\n`);
129
+ }
130
+ for (const line of formatSettingsConflictWarning(settingsConflicts)) {
131
+ process.stderr.write(`${line}\n`);
132
+ }
133
+ process.exit(0);
134
+ });
135
+ }
@@ -0,0 +1,80 @@
1
+ import { loadStoredCredential } from "../../auth/credentials.js";
2
+ import { CopilotTokenManager } from "../../auth/copilotToken.js";
3
+ import { loadConfig, saveConfig } from "../../config/config.js";
4
+ import { listModels, resolveModelSelections } from "../../models/discovery.js";
5
+ import { writeCommandOutput } from "../shared/output.js";
6
+ export function register(program) {
7
+ const models = program.command("models").description("Model commands");
8
+ models
9
+ .command("list")
10
+ .description("List entitled models")
11
+ .option("--json", "JSON output")
12
+ .action(async (opts) => {
13
+ const config = loadConfig();
14
+ const creds = await loadStoredCredential();
15
+ if (!creds) {
16
+ throw new Error("Not authenticated. Run `copillm login`.");
17
+ }
18
+ const tokenManager = new CopilotTokenManager(creds.token);
19
+ await tokenManager.ensureToken(false);
20
+ const result = await listModels(config.accountType, creds.token);
21
+ if (opts.json) {
22
+ process.stdout.write(JSON.stringify({
23
+ models: result.models,
24
+ discovery: {
25
+ source: result.source,
26
+ stale: result.stale,
27
+ cache_age_seconds: result.cacheAgeSeconds,
28
+ warning: result.warning
29
+ }
30
+ }, null, 2) + "\n");
31
+ return;
32
+ }
33
+ process.stdout.write(result.models.map((model) => model.id).join("\n") + "\n");
34
+ if (result.stale) {
35
+ process.stdout.write("⚠ using stale model snapshot (upstream discovery unavailable)\n");
36
+ }
37
+ });
38
+ models
39
+ .command("select")
40
+ .requiredOption("--models <ids>", "Comma-separated model ids")
41
+ .description("Select exposed models")
42
+ .option("--json", "JSON output")
43
+ .action(async (opts) => {
44
+ const config = loadConfig();
45
+ const requested = Array.from(new Set(opts.models
46
+ .split(",")
47
+ .map((value) => value.trim())
48
+ .filter((value) => value.length > 0)));
49
+ if (requested.length === 0) {
50
+ throw new Error("At least one model must be selected.");
51
+ }
52
+ const creds = await loadStoredCredential();
53
+ if (!creds) {
54
+ throw new Error("Not authenticated. Run `copillm login`.");
55
+ }
56
+ const tokenManager = new CopilotTokenManager(creds.token);
57
+ await tokenManager.ensureToken(false);
58
+ const discovery = await listModels(config.accountType, creds.token);
59
+ const resolution = resolveModelSelections(requested, discovery.models);
60
+ if (resolution.unresolved.length > 0) {
61
+ const available = discovery.models.map((model) => model.id).join(", ");
62
+ throw new Error(`Unknown model selection(s): ${resolution.unresolved.join(", ")}. Available models: ${available}`);
63
+ }
64
+ const resolvedSelected = Array.from(new Set(resolution.resolved.map((entry) => entry.resolvedId)));
65
+ saveConfig({ ...config, selectedModels: resolvedSelected });
66
+ const usedAlias = resolution.resolved.some((entry) => entry.input !== entry.resolvedId);
67
+ writeCommandOutput(opts, `Selected ${resolvedSelected.length} model(s)${usedAlias ? " (resolved aliases)." : "."}${discovery.stale ? " Using stale snapshot." : ""}`, {
68
+ status: "ok",
69
+ selected_models: resolvedSelected,
70
+ requested_models: requested,
71
+ resolutions: resolution.resolved,
72
+ discovery: {
73
+ source: discovery.source,
74
+ stale: discovery.stale,
75
+ cache_age_seconds: discovery.cacheAgeSeconds,
76
+ warning: discovery.warning
77
+ }
78
+ });
79
+ });
80
+ }
@@ -23,6 +23,16 @@ body = ""
23
23
  # url = "https://api.githubcopilot.com/mcp/"
24
24
  # headers = { Authorization = "Bearer \${GITHUB_TOKEN}" }
25
25
 
26
+ # Uncomment to control --yolo without typing the flag every launch.
27
+ # Precedence: --yolo flag > COPILLM_YOLO env (1/true/yes or 0/false/no) >
28
+ # profile.yolo.agents.<id> > profile.yolo.enabled > defaults.* > off.
29
+ # [defaults.yolo]
30
+ # enabled = false
31
+ # [defaults.yolo.agents]
32
+ # claude = true # auto-skip permission prompts for claude
33
+ # codex = false # still ask for codex
34
+ # pi = false # pi has no skip-permissions flag; emits a warning if true
35
+
26
36
  [profiles.default]
27
37
  `;
28
38
  export function registerConfigCommands(program) {