copillm 0.2.8 → 0.3.0-beta.1
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 +70 -2
- package/dist/agentconfig/load.js +9 -1
- package/dist/agentconfig/render.js +8 -5
- package/dist/agentconfig/schema.js +6 -0
- package/dist/auth/accountManager.js +118 -0
- package/dist/auth/accounts.js +161 -0
- package/dist/auth/copilotToken.js +92 -23
- package/dist/auth/credentials.js +216 -40
- package/dist/auth/deviceFlow.js +110 -23
- package/dist/auth/githubIdentity.js +14 -10
- package/dist/cli/agentEnv.js +15 -9
- package/dist/cli/auth/runAuth.js +206 -9
- package/dist/cli/commands/agents/claude.js +22 -2
- package/dist/cli/commands/agents/codex.js +22 -2
- package/dist/cli/commands/agents/copilot.js +25 -4
- package/dist/cli/commands/agents/pi.js +22 -2
- package/dist/cli/commands/agents/shared.js +57 -0
- package/dist/cli/commands/auth.js +58 -7
- package/dist/cli/commands/daemon.js +79 -17
- package/dist/cli/commands/models.js +0 -5
- package/dist/cli/copillmFlags.js +8 -0
- package/dist/cli/daemon/lifecycle.js +26 -0
- package/dist/cli/daemon/probes.js +99 -33
- package/dist/cli/daemon/runDaemon.js +21 -2
- package/dist/cli/index.js +12 -0
- package/dist/cli/integrations/claudeExport.js +6 -4
- package/dist/cli/integrations/refreshCodex.js +5 -2
- package/dist/cli/integrations/refreshPi.js +5 -2
- package/dist/cli/packageInfo.js +1 -1
- package/dist/cli/shared/devMode.js +98 -0
- package/dist/config/accountId.js +44 -0
- package/dist/config/config.js +13 -2
- package/dist/config/home.js +69 -0
- package/dist/integrations/claude/cache.js +5 -2
- package/dist/integrations/claude/settingsConflict.js +5 -2
- package/dist/integrations/codex/init.js +31 -10
- package/dist/integrations/pi/init.js +8 -17
- package/dist/models/anthropicDefaults.js +13 -4
- package/dist/models/discovery.js +141 -15
- package/dist/server/accountResolver.js +85 -0
- package/dist/server/debugInfo.js +69 -24
- package/dist/server/errors.js +18 -0
- package/dist/server/proxy.js +40 -8
- package/dist/server/routes/debug.js +11 -1
- package/dist/server/routes/models.js +12 -6
- package/dist/server/routes/proxyForward.js +3 -3
- package/dist/server/routes/shared.js +66 -21
- package/dist/server/upstream/copilotClient.js +1 -30
- package/dist/server/upstream/retryPolicy.js +99 -0
- package/package.json +4 -1
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { inspectStoredCredential } from "../../auth/credentials.js";
|
|
3
3
|
import { loadConfig } from "../../config/config.js";
|
|
4
|
+
import { getCopillmHome } from "../../config/home.js";
|
|
4
5
|
import { clearClaudeGatewayCache } from "../../integrations/claude/cache.js";
|
|
6
|
+
import { resolveStartContext } from "../../integrations/codex/init.js";
|
|
5
7
|
import { inspectLock, releaseLock } from "../../server/lock.js";
|
|
6
8
|
import { buildCodexEnvBundle } from "../agentEnv.js";
|
|
7
9
|
import { ensureAuthenticatedInteractive } from "../auth/ensure.js";
|
|
8
|
-
import { computeUptimeSeconds, stopByPid } from "../daemon/lifecycle.js";
|
|
10
|
+
import { computeUptimeSeconds, formatUptime, stopByPid } from "../daemon/lifecycle.js";
|
|
9
11
|
import { probeHealth, readLiveLock, waitForDaemonReady, warnIfDebugRequestedButInactive } from "../daemon/probes.js";
|
|
10
12
|
import { runDaemon } from "../daemon/runDaemon.js";
|
|
11
13
|
import { daemonSpawnEnv } from "../daemon/spawnEnv.js";
|
|
12
14
|
import { buildClaudeExportCommand } from "../integrations/claudeExport.js";
|
|
13
|
-
import { formatStartBanner, formatStopHumanLine } from "../integrations/banner.js";
|
|
15
|
+
import { formatStartBanner, formatStopHumanLine, displayHomePath } from "../integrations/banner.js";
|
|
14
16
|
import { refreshCodexHome } from "../integrations/refreshCodex.js";
|
|
15
17
|
import { refreshPiHome } from "../integrations/refreshPi.js";
|
|
16
18
|
import { writeAuthStatusLine } from "../shared/backends.js";
|
|
17
19
|
import { currentDebugLogPath, enableRuntimeDebug, getRootLogger, resolveCopillmDebug } from "../shared/debug.js";
|
|
20
|
+
import { isDevModeActive } from "../shared/devMode.js";
|
|
18
21
|
import { writeCommandOutput, writeHealthOutput } from "../shared/output.js";
|
|
19
22
|
import { buildSelfSpawnCommand } from "../daemon/selfSpawn.js";
|
|
20
23
|
export function register(program) {
|
|
@@ -25,11 +28,14 @@ export function register(program) {
|
|
|
25
28
|
.option("--debug", "Enable debug endpoints (e.g. /_debug)")
|
|
26
29
|
.option("--no-codex", "Skip generating ~/.copillm/codex/ for Codex CLI")
|
|
27
30
|
.option("--codex-model <id>", "Default Codex model slug")
|
|
28
|
-
.option("--no-pi", "Skip generating
|
|
31
|
+
.option("--no-pi", "Skip generating the copillm-owned pi models.json for pi coding agent")
|
|
29
32
|
.option("--json", "JSON output")
|
|
30
33
|
.action(async (opts) => {
|
|
31
34
|
const debug = resolveCopillmDebug(opts.debug);
|
|
32
35
|
enableRuntimeDebug(debug);
|
|
36
|
+
if (isDevModeActive()) {
|
|
37
|
+
process.stderr.write(`dev mode: isolated COPILLM_HOME ${displayHomePath(getCopillmHome())}\n`);
|
|
38
|
+
}
|
|
33
39
|
if (opts.detach) {
|
|
34
40
|
// Fail fast on missing credentials rather than letting the detached
|
|
35
41
|
// child die silently and surface as a generic "start timed out" error.
|
|
@@ -40,8 +46,9 @@ export function register(program) {
|
|
|
40
46
|
const existingLock = await readLiveLock();
|
|
41
47
|
if (existingLock) {
|
|
42
48
|
const activeDebug = await warnIfDebugRequestedButInactive(debug, existingLock.port);
|
|
43
|
-
const
|
|
44
|
-
const
|
|
49
|
+
const shared = await loadSharedStartContextIfNeeded(opts);
|
|
50
|
+
const codex = opts.codex === false ? null : await refreshCodexHome(existingLock.port, opts.codexModel ?? null, shared);
|
|
51
|
+
const pi = opts.pi === false ? null : await refreshPiHome(existingLock.port, shared);
|
|
45
52
|
const claude = buildClaudeExportCommand(existingLock.port, null);
|
|
46
53
|
const banner = formatStartBanner({
|
|
47
54
|
port: existingLock.port,
|
|
@@ -86,8 +93,9 @@ export function register(program) {
|
|
|
86
93
|
if (!started) {
|
|
87
94
|
throw new Error("Detached daemon start timed out.");
|
|
88
95
|
}
|
|
89
|
-
const
|
|
90
|
-
const
|
|
96
|
+
const sharedDetached = await loadSharedStartContextIfNeeded(opts);
|
|
97
|
+
const codex = opts.codex === false ? null : await refreshCodexHome(started.port, opts.codexModel ?? null, sharedDetached);
|
|
98
|
+
const pi = opts.pi === false ? null : await refreshPiHome(started.port, sharedDetached);
|
|
91
99
|
const claude = buildClaudeExportCommand(started.port, null);
|
|
92
100
|
const banner = formatStartBanner({
|
|
93
101
|
port: started.port,
|
|
@@ -127,8 +135,9 @@ export function register(program) {
|
|
|
127
135
|
const started = await runDaemon({ debug });
|
|
128
136
|
if (started.kind === "already_running") {
|
|
129
137
|
const activeDebug = await warnIfDebugRequestedButInactive(debug, started.lock.port);
|
|
130
|
-
const
|
|
131
|
-
const
|
|
138
|
+
const sharedAlready = await loadSharedStartContextIfNeeded(opts);
|
|
139
|
+
const codex = opts.codex === false ? null : await refreshCodexHome(started.lock.port, opts.codexModel ?? null, sharedAlready);
|
|
140
|
+
const pi = opts.pi === false ? null : await refreshPiHome(started.lock.port, sharedAlready);
|
|
132
141
|
const claude = buildClaudeExportCommand(started.lock.port, null);
|
|
133
142
|
const banner = formatStartBanner({
|
|
134
143
|
port: started.lock.port,
|
|
@@ -159,8 +168,9 @@ export function register(program) {
|
|
|
159
168
|
});
|
|
160
169
|
return;
|
|
161
170
|
}
|
|
162
|
-
const
|
|
163
|
-
const
|
|
171
|
+
const sharedForeground = await loadSharedStartContextIfNeeded(opts);
|
|
172
|
+
const codex = opts.codex === false ? null : await refreshCodexHome(started.port, opts.codexModel ?? null, sharedForeground);
|
|
173
|
+
const pi = opts.pi === false ? null : await refreshPiHome(started.port, sharedForeground);
|
|
164
174
|
const claude = buildClaudeExportCommand(started.port, started.callerSecret);
|
|
165
175
|
const banner = formatStartBanner({
|
|
166
176
|
port: started.port,
|
|
@@ -271,10 +281,13 @@ export function register(program) {
|
|
|
271
281
|
const status = {
|
|
272
282
|
running: lockState.state === "running",
|
|
273
283
|
stale: lockState.state === "stale",
|
|
284
|
+
copillm_home: getCopillmHome(),
|
|
285
|
+
dev_mode: isDevModeActive(),
|
|
274
286
|
pid: lockState.state === "running" ? lockState.lock.pid : null,
|
|
275
287
|
port: lockState.state === "running" ? lockState.lock.port : null,
|
|
276
288
|
started_at_iso: lockState.state === "running" ? lockState.lock.started_at_iso : null,
|
|
277
289
|
uptime_seconds: uptimeSeconds,
|
|
290
|
+
uptime_human: uptimeSeconds === null ? null : formatUptime(uptimeSeconds),
|
|
278
291
|
url: lockState.state === "running" ? `http://127.0.0.1:${lockState.lock.port}` : null,
|
|
279
292
|
require_caller_secret: config.requireCallerSecret,
|
|
280
293
|
account_type: config.accountType,
|
|
@@ -300,6 +313,7 @@ export function register(program) {
|
|
|
300
313
|
process.stdout.write(JSON.stringify(status, null, 2) + "\n");
|
|
301
314
|
return;
|
|
302
315
|
}
|
|
316
|
+
process.stdout.write(`home: ${displayHomePath(status.copillm_home)}${status.dev_mode ? " (dev)" : ""}\n`);
|
|
303
317
|
if (lockState.state === "running") {
|
|
304
318
|
process.stdout.write(`running (pid ${lockState.lock.pid}, port ${lockState.lock.port})\n`);
|
|
305
319
|
process.stdout.write(`health: ${status.health_status}`);
|
|
@@ -317,7 +331,7 @@ export function register(program) {
|
|
|
317
331
|
process.stdout.write(`bearer_ttl_seconds: ${status.bearer_ttl_seconds}\n`);
|
|
318
332
|
}
|
|
319
333
|
if (status.uptime_seconds !== null) {
|
|
320
|
-
process.stdout.write(`
|
|
334
|
+
process.stdout.write(`uptime: ${formatUptime(status.uptime_seconds)} (${status.uptime_seconds}s)\n`);
|
|
321
335
|
}
|
|
322
336
|
writeAuthStatusLine(authInfo);
|
|
323
337
|
process.stdout.write(`checked_at: ${status.checked_at_iso}\n`);
|
|
@@ -347,12 +361,60 @@ export function register(program) {
|
|
|
347
361
|
process.exitCode = 1;
|
|
348
362
|
return;
|
|
349
363
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if
|
|
364
|
+
// Route the /healthz check through `probeHealth` so we inherit retry
|
|
365
|
+
// on transient transport errors AND a try/catch that turns a transient
|
|
366
|
+
// ECONNRESET into a structured `health_probe_failed` result instead of
|
|
367
|
+
// a raw stack trace. The previous bare `fetch` had no try/catch and
|
|
368
|
+
// would crash `copillm health` if the daemon had just been killed
|
|
369
|
+
// between `inspectLock` and the request.
|
|
370
|
+
const health = await probeHealth(lockState.lock.port);
|
|
371
|
+
const payload = {
|
|
372
|
+
ok: health.ok,
|
|
373
|
+
status_code: health.statusCode
|
|
374
|
+
};
|
|
375
|
+
if (health.status !== null)
|
|
376
|
+
payload.status = health.status;
|
|
377
|
+
if (health.error !== null)
|
|
378
|
+
payload.error = health.error;
|
|
379
|
+
if (health.bearerTtlSeconds !== null)
|
|
380
|
+
payload.bearer_ttl_seconds = health.bearerTtlSeconds;
|
|
381
|
+
writeHealthOutput(opts, payload);
|
|
382
|
+
if (!health.ok) {
|
|
355
383
|
process.exitCode = 1;
|
|
356
384
|
}
|
|
357
385
|
});
|
|
358
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Load the shared credential/config/discovery context for `copillm start`'s
|
|
389
|
+
* codex + pi init steps, ONLY when at least one of them is going to run.
|
|
390
|
+
*
|
|
391
|
+
* Without sharing, each step independently re-reads the OS keychain, re-parses
|
|
392
|
+
* the YAML config, and re-fetches the upstream `/models` catalog. With this
|
|
393
|
+
* helper, the work happens once and both steps see the same snapshot.
|
|
394
|
+
*
|
|
395
|
+
* When both `--no-codex` and `--no-pi` are passed (or both wrappers will skip
|
|
396
|
+
* for some other reason), there's no consumer for the context, so we skip
|
|
397
|
+
* the loads entirely — important for `copillm start --no-codex --no-pi` to
|
|
398
|
+
* stay fast and not surface a credential error if the user genuinely just
|
|
399
|
+
* wants the proxy daemon up.
|
|
400
|
+
*
|
|
401
|
+
* Returning `undefined` (not `null`) so it composes naturally with the
|
|
402
|
+
* `precomputed?: PrecomputedStartContext` optional parameter on both
|
|
403
|
+
* `refreshCodexHome` and `refreshPiHome`.
|
|
404
|
+
*/
|
|
405
|
+
async function loadSharedStartContextIfNeeded(opts) {
|
|
406
|
+
if (opts.codex === false && opts.pi === false) {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
return await resolveStartContext();
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// If the load fails (e.g. no credentials, model discovery down), fall
|
|
414
|
+
// back to per-wrapper loads so each one can fail loudly with its own
|
|
415
|
+
// wrapper-specific warning. The wrappers already have try/catch that
|
|
416
|
+
// emit `warning: failed to generate ...` lines — preserving that
|
|
417
|
+
// surface keeps the user-visible behaviour unchanged from before.
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { loadStoredCredential } from "../../auth/credentials.js";
|
|
2
|
-
import { CopilotTokenManager } from "../../auth/copilotToken.js";
|
|
3
2
|
import { loadConfig, saveConfig } from "../../config/config.js";
|
|
4
3
|
import { listModels, resolveModelSelections } from "../../models/discovery.js";
|
|
5
4
|
import { writeCommandOutput } from "../shared/output.js";
|
|
@@ -15,8 +14,6 @@ export function register(program) {
|
|
|
15
14
|
if (!creds) {
|
|
16
15
|
throw new Error("Not authenticated. Run `copillm login`.");
|
|
17
16
|
}
|
|
18
|
-
const tokenManager = new CopilotTokenManager(creds.token);
|
|
19
|
-
await tokenManager.ensureToken(false);
|
|
20
17
|
const result = await listModels(config.accountType, creds.token);
|
|
21
18
|
if (opts.json) {
|
|
22
19
|
process.stdout.write(JSON.stringify({
|
|
@@ -53,8 +50,6 @@ export function register(program) {
|
|
|
53
50
|
if (!creds) {
|
|
54
51
|
throw new Error("Not authenticated. Run `copillm login`.");
|
|
55
52
|
}
|
|
56
|
-
const tokenManager = new CopilotTokenManager(creds.token);
|
|
57
|
-
await tokenManager.ensureToken(false);
|
|
58
53
|
const discovery = await listModels(config.accountType, creds.token);
|
|
59
54
|
const resolution = resolveModelSelections(requested, discovery.models);
|
|
60
55
|
if (resolution.unresolved.length > 0) {
|
package/dist/cli/copillmFlags.js
CHANGED
|
@@ -40,6 +40,14 @@ export const COPILLM_FLAGS = [
|
|
|
40
40
|
kind: "swallow",
|
|
41
41
|
description: "Override active profile for this launch"
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
flag: "--copillm-account",
|
|
45
|
+
aliases: ["--account"],
|
|
46
|
+
takesValue: true,
|
|
47
|
+
dest: "copillmAccount",
|
|
48
|
+
kind: "swallow",
|
|
49
|
+
description: "Route this launch at a specific copillm account"
|
|
50
|
+
},
|
|
43
51
|
{
|
|
44
52
|
flag: "--copillm-no-config",
|
|
45
53
|
aliases: ["--no-config"],
|
|
@@ -59,3 +59,29 @@ export function computeUptimeSeconds(startedAtIso) {
|
|
|
59
59
|
}
|
|
60
60
|
return Math.max(0, Math.floor((Date.now() - startedMs) / 1000));
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Render an uptime duration (in seconds) as a compact human-readable string
|
|
64
|
+
* broken down into days, hours, minutes, and seconds — e.g. `2d 3h 15m 9s`.
|
|
65
|
+
*
|
|
66
|
+
* Leading zero-value units are dropped so short uptimes stay terse
|
|
67
|
+
* (`45s`, `5m 2s`). Sub-minute and zero durations fall back to a seconds
|
|
68
|
+
* component so the result is never empty (`0s`). Negative or non-finite
|
|
69
|
+
* inputs clamp to `0s`.
|
|
70
|
+
*/
|
|
71
|
+
export function formatUptime(totalSeconds) {
|
|
72
|
+
const seconds = Number.isFinite(totalSeconds) ? Math.max(0, Math.floor(totalSeconds)) : 0;
|
|
73
|
+
const days = Math.floor(seconds / 86_400);
|
|
74
|
+
const hours = Math.floor((seconds % 86_400) / 3_600);
|
|
75
|
+
const minutes = Math.floor((seconds % 3_600) / 60);
|
|
76
|
+
const secs = seconds % 60;
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (days > 0)
|
|
79
|
+
parts.push(`${days}d`);
|
|
80
|
+
if (hours > 0)
|
|
81
|
+
parts.push(`${hours}h`);
|
|
82
|
+
if (minutes > 0)
|
|
83
|
+
parts.push(`${minutes}m`);
|
|
84
|
+
if (secs > 0 || parts.length === 0)
|
|
85
|
+
parts.push(`${secs}s`);
|
|
86
|
+
return parts.join(" ");
|
|
87
|
+
}
|
|
@@ -1,23 +1,77 @@
|
|
|
1
|
-
import { setTimeout as
|
|
1
|
+
import { setTimeout as defaultSleep } from "node:timers/promises";
|
|
2
2
|
import { inspectLock } from "../../server/lock.js";
|
|
3
3
|
import { isPidAlive } from "./lifecycle.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Retry helper for loopback fetches. Localhost RTT is sub-millisecond on a
|
|
6
|
+
* healthy daemon, so a 100ms inter-attempt sleep is enough to give a freshly
|
|
7
|
+
* spawned process time to bind, while still keeping total wall-clock for a
|
|
8
|
+
* "probe + 2 retries" sequence well under a second.
|
|
9
|
+
*
|
|
10
|
+
* Only AbortError + transport-class errors trigger retries — a 4xx/5xx
|
|
11
|
+
* response from the daemon is a real signal (the route exists but
|
|
12
|
+
* disagrees with us), and retrying just delays the answer the caller
|
|
13
|
+
* needs to surface. The set is narrow on purpose: we don't want to retry
|
|
14
|
+
* ECONNREFUSED forever on a daemon that genuinely isn't running.
|
|
15
|
+
*/
|
|
16
|
+
const LOOPBACK_PROBE_BACKOFF_MS = 100;
|
|
17
|
+
async function probeWithRetry(attempt, options = {}) {
|
|
18
|
+
const maxAttempts = Math.max(1, options.attempts ?? 3);
|
|
19
|
+
const sleepImpl = options.sleepImpl ?? ((ms) => defaultSleep(ms));
|
|
20
|
+
const backoffMs = options.backoffMs ?? LOOPBACK_PROBE_BACKOFF_MS;
|
|
21
|
+
let last = { ok: null, failed: true, error: undefined };
|
|
22
|
+
for (let i = 0; i < maxAttempts; i += 1) {
|
|
23
|
+
const result = await attempt();
|
|
24
|
+
if (!result.failed) {
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
last = result;
|
|
28
|
+
if (!isRetryableProbeError(result.error)) {
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
if (i < maxAttempts - 1) {
|
|
32
|
+
await sleepImpl(backoffMs);
|
|
33
|
+
}
|
|
11
34
|
}
|
|
35
|
+
return last;
|
|
12
36
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const response = await fetch(`http://127.0.0.1:${port}/_debug`, { signal: AbortSignal.timeout(1_200) });
|
|
16
|
-
return response.ok;
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
37
|
+
function isRetryableProbeError(error) {
|
|
38
|
+
if (!error || typeof error !== "object")
|
|
19
39
|
return false;
|
|
20
|
-
|
|
40
|
+
const typed = error;
|
|
41
|
+
if (typed.name === "AbortError" || typed.name === "TimeoutError")
|
|
42
|
+
return true;
|
|
43
|
+
const code = typed.code?.toUpperCase() ?? typed.cause?.code?.toUpperCase();
|
|
44
|
+
// ECONNREFUSED *is* retried here even though it usually means "nothing
|
|
45
|
+
// listening": a daemon racing to bind right after spawn returns
|
|
46
|
+
// ECONNREFUSED for a tiny window, and probes do call us from spawn-adjacent
|
|
47
|
+
// paths (`waitForDaemonReady` polls; `acquireLock`'s isRunning callback
|
|
48
|
+
// checks an existing lock). The 100ms inter-attempt sleep is enough for
|
|
49
|
+
// the bind race to settle.
|
|
50
|
+
return code === "ECONNRESET" || code === "ECONNREFUSED" || code === "ETIMEDOUT";
|
|
51
|
+
}
|
|
52
|
+
export async function probeLivez(port, options) {
|
|
53
|
+
const result = await probeWithRetry(async () => {
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(`http://127.0.0.1:${port}/livez`, { signal: AbortSignal.timeout(800) });
|
|
56
|
+
return { ok: response.ok, failed: false };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return { ok: null, failed: true, error };
|
|
60
|
+
}
|
|
61
|
+
}, options);
|
|
62
|
+
return result.failed ? false : result.ok;
|
|
63
|
+
}
|
|
64
|
+
export async function probeDebugEndpoint(port, options) {
|
|
65
|
+
const result = await probeWithRetry(async () => {
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(`http://127.0.0.1:${port}/_debug`, { signal: AbortSignal.timeout(1_200) });
|
|
68
|
+
return { ok: response.ok, failed: false };
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return { ok: null, failed: true, error };
|
|
72
|
+
}
|
|
73
|
+
}, options);
|
|
74
|
+
return result.failed ? false : result.ok;
|
|
21
75
|
}
|
|
22
76
|
export async function warnIfDebugRequestedButInactive(debugRequested, port) {
|
|
23
77
|
if (!debugRequested) {
|
|
@@ -29,21 +83,29 @@ export async function warnIfDebugRequestedButInactive(debugRequested, port) {
|
|
|
29
83
|
}
|
|
30
84
|
return active;
|
|
31
85
|
}
|
|
32
|
-
export async function probeHealth(port) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
86
|
+
export async function probeHealth(port, options) {
|
|
87
|
+
const result = await probeWithRetry(async () => {
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(`http://127.0.0.1:${port}/healthz`, { signal: AbortSignal.timeout(1_500) });
|
|
90
|
+
const payload = (await response.json());
|
|
91
|
+
return {
|
|
92
|
+
ok: {
|
|
93
|
+
ok: response.ok,
|
|
94
|
+
statusCode: response.status,
|
|
95
|
+
status: typeof payload.status === "string" ? payload.status : null,
|
|
96
|
+
error: typeof payload.error === "string" ? payload.error : null,
|
|
97
|
+
bearerTtlSeconds: response.ok && typeof payload.bearer_ttl_seconds === "number" ? payload.bearer_ttl_seconds : null
|
|
98
|
+
},
|
|
99
|
+
failed: false
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return { ok: null, failed: true, error };
|
|
104
|
+
}
|
|
105
|
+
}, options);
|
|
106
|
+
return result.failed
|
|
107
|
+
? { ok: false, bearerTtlSeconds: null, statusCode: null, status: null, error: "health_probe_failed" }
|
|
108
|
+
: result.ok;
|
|
47
109
|
}
|
|
48
110
|
export async function readLiveLock() {
|
|
49
111
|
const lockState = inspectLock();
|
|
@@ -52,17 +114,21 @@ export async function readLiveLock() {
|
|
|
52
114
|
}
|
|
53
115
|
return (await probeLivez(lockState.lock.port)) ? lockState.lock : null;
|
|
54
116
|
}
|
|
55
|
-
export async function waitForDaemonReady(pid, timeoutMs) {
|
|
117
|
+
export async function waitForDaemonReady(pid, timeoutMs, options) {
|
|
118
|
+
const sleepImpl = options?.sleepImpl ?? ((ms) => defaultSleep(ms));
|
|
56
119
|
const startedAt = Date.now();
|
|
57
120
|
while (Date.now() - startedAt <= timeoutMs) {
|
|
58
121
|
const lockState = inspectLock();
|
|
59
|
-
|
|
122
|
+
// Use a single-attempt probe inside this poll loop — the outer 150ms
|
|
123
|
+
// loop already retries naturally. Letting `probeLivez` retry internally
|
|
124
|
+
// here would compound delays.
|
|
125
|
+
if (lockState.state === "running" && (await probeLivez(lockState.lock.port, { attempts: 1 }))) {
|
|
60
126
|
return { pid: lockState.lock.pid, port: lockState.lock.port };
|
|
61
127
|
}
|
|
62
128
|
if (pid !== null && !isPidAlive(pid)) {
|
|
63
129
|
return null;
|
|
64
130
|
}
|
|
65
|
-
await
|
|
131
|
+
await sleepImpl(150);
|
|
66
132
|
}
|
|
67
133
|
return null;
|
|
68
134
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { loadStoredCredential } from "../../auth/credentials.js";
|
|
3
|
+
import { readAccountsIndex } from "../../auth/accounts.js";
|
|
3
4
|
import { CopilotTokenManager } from "../../auth/copilotToken.js";
|
|
4
5
|
import { loadConfig } from "../../config/config.js";
|
|
5
6
|
import { acquireLock, LockAlreadyRunningError, releaseLock } from "../../server/lock.js";
|
|
6
7
|
import { startProxyServer } from "../../server/proxy.js";
|
|
8
|
+
import { DaemonAccountResolver } from "../../server/accountResolver.js";
|
|
7
9
|
import { installProcessSafetyNet } from "../processSafetyNet.js";
|
|
8
10
|
import { getRootLogger } from "../shared/debug.js";
|
|
9
11
|
import { withTimeout } from "./lifecycle.js";
|
|
@@ -30,6 +32,22 @@ export async function runDaemon(options) {
|
|
|
30
32
|
}
|
|
31
33
|
const tokenManager = new CopilotTokenManager(creds.token);
|
|
32
34
|
await tokenManager.ensureToken(false);
|
|
35
|
+
// Build the default account's resolved identity. With no accounts index this
|
|
36
|
+
// is the legacy single account (accountId null, legacy model cache). With an
|
|
37
|
+
// index, it reflects the configured default account's id, plan type, and
|
|
38
|
+
// storage scheme — so model discovery and the cache key stay correct.
|
|
39
|
+
const accountsIndex = readAccountsIndex();
|
|
40
|
+
const defaultRecord = accountsIndex
|
|
41
|
+
? accountsIndex.accounts.find((account) => account.id === accountsIndex.defaultAccount) ?? null
|
|
42
|
+
: null;
|
|
43
|
+
const defaultAccount = {
|
|
44
|
+
accountId: defaultRecord?.id ?? null,
|
|
45
|
+
githubToken: creds.token,
|
|
46
|
+
tokenManager,
|
|
47
|
+
accountType: defaultRecord?.accountType ?? config.accountType,
|
|
48
|
+
cacheId: defaultRecord && defaultRecord.storage === "namespaced" ? defaultRecord.id : undefined
|
|
49
|
+
};
|
|
50
|
+
const accountResolver = new DaemonAccountResolver({ default: defaultAccount });
|
|
33
51
|
const callerSecret = config.requireCallerSecret ? randomUUID() : null;
|
|
34
52
|
if (callerSecret) {
|
|
35
53
|
process.stdout.write(`Caller secret: ${callerSecret}\n`);
|
|
@@ -53,6 +71,7 @@ export async function runDaemon(options) {
|
|
|
53
71
|
port,
|
|
54
72
|
config,
|
|
55
73
|
tokenManager,
|
|
74
|
+
accountResolver,
|
|
56
75
|
callerSecret,
|
|
57
76
|
logger,
|
|
58
77
|
debug: Boolean(options?.debug),
|
|
@@ -70,7 +89,7 @@ export async function runDaemon(options) {
|
|
|
70
89
|
}
|
|
71
90
|
}
|
|
72
91
|
if (!server || selectedPort === null) {
|
|
73
|
-
|
|
92
|
+
accountResolver.clearAll();
|
|
74
93
|
throw new Error(`No available port in configured range (${ports[0]}-${ports[ports.length - 1]}).`);
|
|
75
94
|
}
|
|
76
95
|
installProcessSafetyNet(logger);
|
|
@@ -87,7 +106,7 @@ export async function runDaemon(options) {
|
|
|
87
106
|
logger.warn({ err: error }, "graceful shutdown timed out");
|
|
88
107
|
}
|
|
89
108
|
finally {
|
|
90
|
-
|
|
109
|
+
accountResolver.clearAll();
|
|
91
110
|
releaseLock();
|
|
92
111
|
process.exit(0);
|
|
93
112
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -10,18 +10,30 @@ import * as claudeCmd from "./commands/agents/claude.js";
|
|
|
10
10
|
import * as piCmd from "./commands/agents/pi.js";
|
|
11
11
|
import * as copilotCmd from "./commands/agents/copilot.js";
|
|
12
12
|
import { setRootLogger, setRootProgram } from "./shared/debug.js";
|
|
13
|
+
import { applyDevModeEnv } from "./shared/devMode.js";
|
|
13
14
|
import { getPackageInfo } from "./packageInfo.js";
|
|
14
15
|
import { maybeNotifyAboutUpdate } from "./updateNotifier.js";
|
|
15
16
|
const logger = createLogger();
|
|
16
17
|
const program = new Command();
|
|
17
18
|
setRootProgram(program);
|
|
18
19
|
setRootLogger(logger);
|
|
20
|
+
// Honor COPILLM_DEV before anything reads COPILLM_HOME (e.g. the update
|
|
21
|
+
// notifier). The `--dev` flag form is applied later, in the preAction hook,
|
|
22
|
+
// once commander has parsed global options.
|
|
23
|
+
applyDevModeEnv();
|
|
19
24
|
const pkg = getPackageInfo();
|
|
20
25
|
await maybeNotifyAboutUpdate({ packageInfo: pkg });
|
|
21
26
|
program.name("copillm").description("Local Copilot proxy").version(pkg.version);
|
|
22
27
|
program.enablePositionalOptions();
|
|
23
28
|
program.option("--debug", "Enable copillm debug mode (debug endpoint plus verbose daemon diagnostics)");
|
|
29
|
+
program.option("--dev", "Run against an isolated dev home (COPILLM_HOME=~/.copillm-dev, port 4142) so the dev daemon never conflicts with a production copillm");
|
|
24
30
|
program.option("--no-update-notifier", "Skip the npm registry update check for this run");
|
|
31
|
+
// Apply the `--dev` flag as soon as global options are parsed and before any
|
|
32
|
+
// subcommand action resolves COPILLM_HOME. Idempotent with the env-based call
|
|
33
|
+
// above.
|
|
34
|
+
program.hook("preAction", () => {
|
|
35
|
+
applyDevModeEnv(Boolean(program.opts().dev));
|
|
36
|
+
});
|
|
25
37
|
authCmd.register(program);
|
|
26
38
|
daemonCmd.register(program);
|
|
27
39
|
modelsCmd.register(program);
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { buildClaudeEnvBundle } from "../agentEnv.js";
|
|
2
2
|
import { buildClaudeExportCommand as buildClaudeExport, computeAnthropicDefaults, readModelIdsFromCache } from "../../models/anthropicDefaults.js";
|
|
3
|
-
export function buildClaudeExportCommand(port, callerSecret) {
|
|
4
|
-
const
|
|
3
|
+
export function buildClaudeExportCommand(port, callerSecret, opts) {
|
|
4
|
+
const pathPrefix = opts?.pathPrefix ?? "";
|
|
5
|
+
const modelIds = readModelIdsFromCache(opts?.cacheId);
|
|
5
6
|
const defaults = computeAnthropicDefaults(modelIds);
|
|
6
7
|
const command = buildClaudeExport({
|
|
7
8
|
port,
|
|
8
9
|
callerSecret,
|
|
9
10
|
defaults,
|
|
10
|
-
enableGatewayDiscovery: true
|
|
11
|
+
enableGatewayDiscovery: true,
|
|
12
|
+
pathPrefix
|
|
11
13
|
});
|
|
12
|
-
const bundle = buildClaudeEnvBundle({ port, callerSecret, defaults, enableGatewayDiscovery: true });
|
|
14
|
+
const bundle = buildClaudeEnvBundle({ port, callerSecret, defaults, enableGatewayDiscovery: true, pathPrefix });
|
|
13
15
|
return { command, defaults, bundle };
|
|
14
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getCopillmHome } from "../../config/home.js";
|
|
2
2
|
import { defaultOutputDir, generateCodexHome } from "../../integrations/codex/init.js";
|
|
3
|
-
export async function refreshCodexHome(port, model) {
|
|
3
|
+
export async function refreshCodexHome(port, model, precomputed, opts) {
|
|
4
4
|
try {
|
|
5
5
|
const home = getCopillmHome();
|
|
6
6
|
return await generateCodexHome({
|
|
@@ -8,7 +8,10 @@ export async function refreshCodexHome(port, model) {
|
|
|
8
8
|
model,
|
|
9
9
|
port,
|
|
10
10
|
providerId: "copillm",
|
|
11
|
-
reasoningEffort: null
|
|
11
|
+
reasoningEffort: null,
|
|
12
|
+
precomputed,
|
|
13
|
+
pathPrefix: opts?.pathPrefix,
|
|
14
|
+
account: opts?.account
|
|
12
15
|
});
|
|
13
16
|
}
|
|
14
17
|
catch (error) {
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { getCopillmHome } from "../../config/home.js";
|
|
2
2
|
import { defaultOutputDir as defaultPiOutputDir, generatePiHome } from "../../integrations/pi/init.js";
|
|
3
|
-
export async function refreshPiHome(port) {
|
|
3
|
+
export async function refreshPiHome(port, precomputed, opts) {
|
|
4
4
|
try {
|
|
5
5
|
const home = getCopillmHome();
|
|
6
6
|
return await generatePiHome({
|
|
7
7
|
outDir: defaultPiOutputDir(home),
|
|
8
8
|
port,
|
|
9
|
-
providerId: "copillm"
|
|
9
|
+
providerId: "copillm",
|
|
10
|
+
precomputed,
|
|
11
|
+
pathPrefix: opts?.pathPrefix,
|
|
12
|
+
account: opts?.account
|
|
10
13
|
});
|
|
11
14
|
}
|
|
12
15
|
catch (error) {
|
package/dist/cli/packageInfo.js
CHANGED