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
|
@@ -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) {
|