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,111 @@
1
+ /**
2
+ * Declarative registry of copillm-owned launcher flags + the processor that
3
+ * extracts them from a raw arg tail.
4
+ *
5
+ * Why this exists: commander's `passThroughOptions()` stops parsing at the
6
+ * first unrecognized token and forwards everything after it verbatim. That made
7
+ * copillm-flag recognition order-dependent — `copillm claude
8
+ * --dangerously-skip-permissions --copillm-profile work` leaked
9
+ * `--copillm-profile work` straight to the agent, which then crashed on the
10
+ * unknown option. Because every copillm flag is namespaced (`--copillm-*`) or
11
+ * the single well-known `--yolo`, scanning the entire arg list for them is safe
12
+ * and collision-free. This processor does exactly that, layered on top of
13
+ * commander (which still owns subcommand routing).
14
+ *
15
+ * Adding a new copillm launcher flag should be one row in `COPILLM_FLAGS` plus a
16
+ * field on `CopillmLaunchOpts` — never new parsing logic.
17
+ */
18
+ export const COPILLM_FLAGS = [
19
+ {
20
+ flag: "--copillm-use",
21
+ aliases: ["--use"],
22
+ takesValue: true,
23
+ dest: "copillmUse",
24
+ kind: "swallow",
25
+ description: "Pin agent package version"
26
+ },
27
+ {
28
+ flag: "--copillm-debug",
29
+ aliases: ["--debug"],
30
+ takesValue: false,
31
+ dest: "copillmDebug",
32
+ kind: "swallow",
33
+ description: "Enable debug endpoints when auto-starting daemon"
34
+ },
35
+ {
36
+ flag: "--copillm-profile",
37
+ aliases: ["--profile"],
38
+ takesValue: true,
39
+ dest: "copillmProfile",
40
+ kind: "swallow",
41
+ description: "Override active profile for this launch"
42
+ },
43
+ {
44
+ flag: "--copillm-no-config",
45
+ aliases: ["--no-config"],
46
+ takesValue: false,
47
+ dest: "copillmNoConfig",
48
+ kind: "swallow",
49
+ description: "Skip agent.toml fan-out for this launch"
50
+ },
51
+ {
52
+ flag: "--yolo",
53
+ takesValue: false,
54
+ dest: "yolo",
55
+ kind: "translate",
56
+ description: "Skip approvals (translated per-agent)"
57
+ }
58
+ ];
59
+ const SPEC_BY_FLAG = new Map();
60
+ for (const spec of COPILLM_FLAGS) {
61
+ SPEC_BY_FLAG.set(spec.flag, spec);
62
+ for (const alias of spec.aliases ?? []) {
63
+ SPEC_BY_FLAG.set(alias, spec);
64
+ }
65
+ }
66
+ /**
67
+ * Extract copillm-owned flags from a raw arg tail, returning the parsed opts
68
+ * plus everything else to forward to the agent.
69
+ *
70
+ * Extract-everywhere: copillm flags are pulled out regardless of position,
71
+ * including after a `--` separator (the `--` itself is forwarded). Accepted
72
+ * tradeoff: a literal `--copillm-*`/`--yolo` token cannot be passed through to
73
+ * the agent as data. These tokens have no legitimate meaning to the agents.
74
+ *
75
+ * Pure function, no I/O.
76
+ */
77
+ export function processCopillmArgs(rawArgs) {
78
+ const opts = {};
79
+ const forwarded = [];
80
+ for (let i = 0; i < rawArgs.length; i++) {
81
+ const token = rawArgs[i];
82
+ const eq = token.indexOf("=");
83
+ const name = eq === -1 ? token : token.slice(0, eq);
84
+ const spec = SPEC_BY_FLAG.get(name);
85
+ if (!spec) {
86
+ forwarded.push(token);
87
+ continue;
88
+ }
89
+ if (spec.takesValue) {
90
+ let value;
91
+ if (eq !== -1) {
92
+ value = token.slice(eq + 1);
93
+ }
94
+ else if (i + 1 < rawArgs.length) {
95
+ value = rawArgs[++i];
96
+ }
97
+ if (value === undefined) {
98
+ throw new Error(`${spec.flag} requires a value`);
99
+ }
100
+ setOpt(opts, spec.dest, value);
101
+ }
102
+ else {
103
+ setOpt(opts, spec.dest, true);
104
+ }
105
+ }
106
+ return { opts, forwarded };
107
+ }
108
+ function setOpt(opts, dest, value) {
109
+ // Last-wins on repeats. dest/value pairing is guaranteed by the spec table.
110
+ opts[dest] = value;
111
+ }
@@ -0,0 +1,65 @@
1
+ import { spawn } from "node:child_process";
2
+ import { inspectStoredCredential } from "../../auth/credentials.js";
3
+ import { inspectLock } from "../../server/lock.js";
4
+ import { currentDebugLogPath } from "../shared/debug.js";
5
+ import { displayHomePath } from "../integrations/banner.js";
6
+ import { isPidAlive } from "./lifecycle.js";
7
+ import { readLiveLock, waitForDaemonReady, warnIfDebugRequestedButInactive } from "./probes.js";
8
+ import { daemonSpawnEnv } from "./spawnEnv.js";
9
+ import { buildSelfSpawnCommand } from "./selfSpawn.js";
10
+ export async function ensureDaemonRunningForLauncher(opts) {
11
+ const live = await readLiveLock();
12
+ if (live) {
13
+ await warnIfDebugRequestedButInactive(opts.debug, live.port);
14
+ return live;
15
+ }
16
+ // Fail fast on missing credentials rather than spawning a detached daemon
17
+ // that will die silently and surface as a generic "start timed out" error.
18
+ const authState = await inspectStoredCredential();
19
+ if (!authState.stored) {
20
+ throw new Error("Not authenticated. Run `copillm auth login` first.");
21
+ }
22
+ const debugLog = currentDebugLogPath(opts.debug);
23
+ process.stderr.write(opts.debug && debugLog
24
+ ? `Starting copillm in background with debug logging at ${displayHomePath(debugLog)}...\n`
25
+ : `Starting copillm in background...\n`);
26
+ const daemonCommand = buildSelfSpawnCommand("daemon", opts.debug ? ["--debug"] : []);
27
+ const child = spawn(daemonCommand.command, daemonCommand.args, {
28
+ detached: true,
29
+ stdio: ["ignore", "ignore", "pipe"],
30
+ env: daemonSpawnEnv(opts.debug)
31
+ });
32
+ child.unref();
33
+ const stderrChunks = [];
34
+ let stderrBytes = 0;
35
+ const STDERR_TAIL_LIMIT = 8 * 1024;
36
+ if (child.stderr) {
37
+ child.stderr.on("data", (chunk) => {
38
+ stderrChunks.push(chunk);
39
+ stderrBytes += chunk.length;
40
+ while (stderrBytes > STDERR_TAIL_LIMIT && stderrChunks.length > 1) {
41
+ stderrBytes -= stderrChunks[0].length;
42
+ stderrChunks.shift();
43
+ }
44
+ });
45
+ child.stderr.on("error", () => {
46
+ // Ignore — best-effort capture only.
47
+ });
48
+ }
49
+ const formatStderrTail = () => {
50
+ const tail = Buffer.concat(stderrChunks).toString("utf8").trim();
51
+ return tail ? `\nDaemon stderr (tail):\n${tail}` : "";
52
+ };
53
+ const started = await waitForDaemonReady(child.pid ?? null, 10_000);
54
+ if (!started) {
55
+ if (child.pid !== undefined && !isPidAlive(child.pid)) {
56
+ throw new Error(`copillm daemon exited before becoming ready.${formatStderrTail()}`);
57
+ }
58
+ throw new Error(`Auto-start of copillm daemon timed out.${formatStderrTail()}`);
59
+ }
60
+ const inspection = inspectLock();
61
+ if (inspection.state !== "running") {
62
+ throw new Error(`copillm daemon failed to register a lock after auto-start.${formatStderrTail()}`);
63
+ }
64
+ return inspection.lock;
65
+ }
@@ -0,0 +1,61 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ import { inspectLock } from "../../server/lock.js";
3
+ export function isPidAlive(pid) {
4
+ try {
5
+ process.kill(pid, 0);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ export function sendSignalIfAlive(pid, signal) {
13
+ try {
14
+ process.kill(pid, signal);
15
+ return true;
16
+ }
17
+ catch (error) {
18
+ if (error instanceof Error && "code" in error && error.code === "ESRCH") {
19
+ return false;
20
+ }
21
+ throw error;
22
+ }
23
+ }
24
+ export async function stopByPid(pid) {
25
+ if (!sendSignalIfAlive(pid, "SIGTERM")) {
26
+ return;
27
+ }
28
+ const stopDeadline = Date.now() + 8_000;
29
+ while (Date.now() < stopDeadline) {
30
+ const lockState = inspectLock();
31
+ if (lockState.state !== "running" || lockState.lock.pid !== pid) {
32
+ return;
33
+ }
34
+ await sleep(150);
35
+ }
36
+ if (!sendSignalIfAlive(pid, "SIGKILL")) {
37
+ return;
38
+ }
39
+ const killDeadline = Date.now() + 2_000;
40
+ while (Date.now() < killDeadline) {
41
+ const lockState = inspectLock();
42
+ if (lockState.state !== "running" || lockState.lock.pid !== pid) {
43
+ return;
44
+ }
45
+ await sleep(100);
46
+ }
47
+ throw new Error(`Failed to stop daemon pid ${pid}.`);
48
+ }
49
+ export async function withTimeout(promise, timeoutMs, message) {
50
+ const timeoutPromise = sleep(timeoutMs).then(() => {
51
+ throw new Error(message);
52
+ });
53
+ return Promise.race([promise, timeoutPromise]);
54
+ }
55
+ export function computeUptimeSeconds(startedAtIso) {
56
+ const startedMs = Date.parse(startedAtIso);
57
+ if (!Number.isFinite(startedMs)) {
58
+ return null;
59
+ }
60
+ return Math.max(0, Math.floor((Date.now() - startedMs) / 1000));
61
+ }
@@ -0,0 +1,68 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ import { inspectLock } from "../../server/lock.js";
3
+ import { isPidAlive } from "./lifecycle.js";
4
+ export async function probeLivez(port) {
5
+ try {
6
+ const response = await fetch(`http://127.0.0.1:${port}/livez`, { signal: AbortSignal.timeout(800) });
7
+ return response.ok;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ export async function probeDebugEndpoint(port) {
14
+ try {
15
+ const response = await fetch(`http://127.0.0.1:${port}/_debug`, { signal: AbortSignal.timeout(1_200) });
16
+ return response.ok;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ export async function warnIfDebugRequestedButInactive(debugRequested, port) {
23
+ if (!debugRequested) {
24
+ return false;
25
+ }
26
+ const active = await probeDebugEndpoint(port);
27
+ if (!active) {
28
+ process.stderr.write(`warning: copillm is already running without debug mode; run \`copillm stop\` then \`copillm --debug start --detach\` to enable daemon diagnostics.\n`);
29
+ }
30
+ return active;
31
+ }
32
+ export async function probeHealth(port) {
33
+ try {
34
+ const response = await fetch(`http://127.0.0.1:${port}/healthz`, { signal: AbortSignal.timeout(1_500) });
35
+ const payload = (await response.json());
36
+ return {
37
+ ok: response.ok,
38
+ statusCode: response.status,
39
+ status: typeof payload.status === "string" ? payload.status : null,
40
+ error: typeof payload.error === "string" ? payload.error : null,
41
+ bearerTtlSeconds: response.ok && typeof payload.bearer_ttl_seconds === "number" ? payload.bearer_ttl_seconds : null
42
+ };
43
+ }
44
+ catch {
45
+ return { ok: false, bearerTtlSeconds: null, statusCode: null, status: null, error: "health_probe_failed" };
46
+ }
47
+ }
48
+ export async function readLiveLock() {
49
+ const lockState = inspectLock();
50
+ if (lockState.state !== "running") {
51
+ return null;
52
+ }
53
+ return (await probeLivez(lockState.lock.port)) ? lockState.lock : null;
54
+ }
55
+ export async function waitForDaemonReady(pid, timeoutMs) {
56
+ const startedAt = Date.now();
57
+ while (Date.now() - startedAt <= timeoutMs) {
58
+ const lockState = inspectLock();
59
+ if (lockState.state === "running" && (await probeLivez(lockState.lock.port))) {
60
+ return { pid: lockState.lock.pid, port: lockState.lock.port };
61
+ }
62
+ if (pid !== null && !isPidAlive(pid)) {
63
+ return null;
64
+ }
65
+ await sleep(150);
66
+ }
67
+ return null;
68
+ }
@@ -0,0 +1,102 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { loadStoredCredential } from "../../auth/credentials.js";
3
+ import { CopilotTokenManager } from "../../auth/copilotToken.js";
4
+ import { loadConfig } from "../../config/config.js";
5
+ import { acquireLock, LockAlreadyRunningError, releaseLock } from "../../server/lock.js";
6
+ import { startProxyServer } from "../../server/proxy.js";
7
+ import { installProcessSafetyNet } from "../processSafetyNet.js";
8
+ import { getRootLogger } from "../shared/debug.js";
9
+ import { withTimeout } from "./lifecycle.js";
10
+ import { probeLivez } from "./probes.js";
11
+ export function candidatePorts(preferredPort) {
12
+ const ports = [];
13
+ for (let offset = 0; offset < 10; offset += 1) {
14
+ const port = preferredPort + offset;
15
+ if (port <= 65535) {
16
+ ports.push(port);
17
+ }
18
+ }
19
+ return ports;
20
+ }
21
+ export function isAddrInUse(error) {
22
+ return error instanceof Error && "code" in error && error.code === "EADDRINUSE";
23
+ }
24
+ export async function runDaemon(options) {
25
+ const logger = getRootLogger();
26
+ const config = loadConfig();
27
+ const creds = await loadStoredCredential();
28
+ if (!creds) {
29
+ throw new Error("Not authenticated. Run `copillm login` first.");
30
+ }
31
+ const tokenManager = new CopilotTokenManager(creds.token);
32
+ await tokenManager.ensureToken(false);
33
+ const callerSecret = config.requireCallerSecret ? randomUUID() : null;
34
+ if (callerSecret) {
35
+ process.stdout.write(`Caller secret: ${callerSecret}\n`);
36
+ }
37
+ const ports = candidatePorts(config.preferredPort);
38
+ let server = null;
39
+ let selectedPort = null;
40
+ for (const port of ports) {
41
+ try {
42
+ await acquireLock(port, { isRunning: async (lock) => probeLivez(lock.port) });
43
+ }
44
+ catch (error) {
45
+ if (error instanceof LockAlreadyRunningError) {
46
+ tokenManager.clear();
47
+ return { kind: "already_running", lock: error.lock };
48
+ }
49
+ throw error;
50
+ }
51
+ try {
52
+ server = await startProxyServer({
53
+ port,
54
+ config,
55
+ tokenManager,
56
+ callerSecret,
57
+ logger,
58
+ debug: Boolean(options?.debug),
59
+ githubToken: creds.token
60
+ });
61
+ selectedPort = port;
62
+ break;
63
+ }
64
+ catch (error) {
65
+ releaseLock();
66
+ if (isAddrInUse(error)) {
67
+ continue;
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+ if (!server || selectedPort === null) {
73
+ tokenManager.clear();
74
+ throw new Error(`No available port in configured range (${ports[0]}-${ports[ports.length - 1]}).`);
75
+ }
76
+ installProcessSafetyNet(logger);
77
+ let shuttingDown = false;
78
+ const shutdown = async () => {
79
+ if (shuttingDown) {
80
+ return;
81
+ }
82
+ shuttingDown = true;
83
+ try {
84
+ await withTimeout(server.close(), 5_000, "Timed out while draining requests.");
85
+ }
86
+ catch (error) {
87
+ logger.warn({ err: error }, "graceful shutdown timed out");
88
+ }
89
+ finally {
90
+ tokenManager.clear();
91
+ releaseLock();
92
+ process.exit(0);
93
+ }
94
+ };
95
+ process.once("SIGINT", () => {
96
+ void shutdown();
97
+ });
98
+ process.once("SIGTERM", () => {
99
+ void shutdown();
100
+ });
101
+ return { kind: "started", port: selectedPort, callerSecret };
102
+ }
@@ -0,0 +1,15 @@
1
+ import path from "node:path";
2
+ export function buildSelfSpawnCommand(subcommand, extraArgs = [], runtime = process) {
3
+ const entryPoint = runtime.argv[1];
4
+ if (!entryPoint || sameExecutable(entryPoint, runtime.execPath)) {
5
+ return { command: runtime.execPath, args: [subcommand, ...extraArgs] };
6
+ }
7
+ return { command: runtime.execPath, args: [entryPoint, subcommand, ...extraArgs] };
8
+ }
9
+ function sameExecutable(left, right) {
10
+ const normalizedLeft = path.resolve(left);
11
+ const normalizedRight = path.resolve(right);
12
+ return process.platform === "win32"
13
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
14
+ : normalizedLeft === normalizedRight;
15
+ }
@@ -0,0 +1,12 @@
1
+ import { debugLogPath } from "../../config/home.js";
2
+ import { currentDebugLogPath } from "../shared/debug.js";
3
+ export function daemonSpawnEnv(debug) {
4
+ if (!debug) {
5
+ return process.env;
6
+ }
7
+ return {
8
+ ...process.env,
9
+ COPILLM_LOG_LEVEL: "debug",
10
+ COPILLM_LOG_FILE: currentDebugLogPath(true) ?? debugLogPath()
11
+ };
12
+ }
@@ -0,0 +1,41 @@
1
+ import { Command } from "commander";
2
+ import { createLogger } from "../config/logging.js";
3
+ import { registerConfigCommands } from "./configCommands.js";
4
+ import * as authCmd from "./commands/auth.js";
5
+ import * as daemonCmd from "./commands/daemon.js";
6
+ import * as modelsCmd from "./commands/models.js";
7
+ import * as envCmd from "./commands/env.js";
8
+ import * as codexCmd from "./commands/agents/codex.js";
9
+ import * as claudeCmd from "./commands/agents/claude.js";
10
+ import * as piCmd from "./commands/agents/pi.js";
11
+ import * as copilotCmd from "./commands/agents/copilot.js";
12
+ import { setRootLogger, setRootProgram } from "./shared/debug.js";
13
+ import { getPackageInfo } from "./packageInfo.js";
14
+ import { maybeNotifyAboutUpdate } from "./updateNotifier.js";
15
+ const logger = createLogger();
16
+ const program = new Command();
17
+ setRootProgram(program);
18
+ setRootLogger(logger);
19
+ const pkg = getPackageInfo();
20
+ await maybeNotifyAboutUpdate({ packageInfo: pkg });
21
+ program.name("copillm").description("Local Copilot proxy").version(pkg.version);
22
+ program.enablePositionalOptions();
23
+ program.option("--debug", "Enable copillm debug mode (debug endpoint plus verbose daemon diagnostics)");
24
+ program.option("--no-update-notifier", "Skip the npm registry update check for this run");
25
+ authCmd.register(program);
26
+ daemonCmd.register(program);
27
+ modelsCmd.register(program);
28
+ envCmd.register(program);
29
+ codexCmd.register(program);
30
+ claudeCmd.register(program);
31
+ piCmd.register(program);
32
+ copilotCmd.register(program);
33
+ registerConfigCommands(program);
34
+ program.parseAsync(process.argv).catch((error) => {
35
+ if (error instanceof Error) {
36
+ logger.error({ err: error }, error.message);
37
+ process.stderr.write(`${error.message}\n`);
38
+ process.exit(1);
39
+ }
40
+ throw error;
41
+ });
@@ -0,0 +1,51 @@
1
+ export function displayHomePath(p) {
2
+ const home = process.env.HOME ?? process.env.USERPROFILE;
3
+ if (home && p.startsWith(home)) {
4
+ return p.replace(home, "~");
5
+ }
6
+ return p;
7
+ }
8
+ export function formatStartBanner(input) {
9
+ const verb = input.mode === "foreground" ? "listening on" : "running on";
10
+ const lines = [];
11
+ const debugSuffix = input.debug ? " [debug]" : "";
12
+ const modeSuffix = input.mode === "already_running" ? " (already running)" : "";
13
+ lines.push(`● copillm ${verb} http://127.0.0.1:${input.port} (pid ${input.pid})${debugSuffix}${modeSuffix}`);
14
+ if (input.codex) {
15
+ lines.push(` ${input.codex.modelCount} Copilot models discovered · default: ${input.codex.defaultModel}`);
16
+ }
17
+ if (input.debugLogPath) {
18
+ lines.push(` debug log: ${displayHomePath(input.debugLogPath)}`);
19
+ }
20
+ if (input.pi) {
21
+ 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)})` : ""}`);
22
+ }
23
+ lines.push(``);
24
+ lines.push(`Launch an agent against copillm:`);
25
+ if (input.codex) {
26
+ lines.push(` copillm codex # starts Codex CLI, preconfigured`);
27
+ }
28
+ lines.push(` copillm claude # starts Claude Code, preconfigured`);
29
+ if (input.pi) {
30
+ lines.push(` copillm pi # starts pi coding agent, preconfigured`);
31
+ }
32
+ lines.push(``);
33
+ lines.push(`Or print env vars to use yourself:`);
34
+ if (input.codex) {
35
+ lines.push(` copillm env codex`);
36
+ }
37
+ lines.push(` copillm env claude`);
38
+ if (input.pi) {
39
+ lines.push(` copillm env pi`);
40
+ }
41
+ return lines.join("\n");
42
+ }
43
+ export function formatStopHumanLine(primary, cache) {
44
+ if (cache.cleared) {
45
+ return `${primary} Cleared Claude Code gateway cache.`;
46
+ }
47
+ if (cache.reason === "not_present") {
48
+ return primary;
49
+ }
50
+ return `${primary} Could not clear Claude Code gateway cache: ${cache.reason ?? "unknown error"}.`;
51
+ }
@@ -0,0 +1,14 @@
1
+ import { buildClaudeEnvBundle } from "../agentEnv.js";
2
+ import { buildClaudeExportCommand as buildClaudeExport, computeAnthropicDefaults, readModelIdsFromCache } from "../../models/anthropicDefaults.js";
3
+ export function buildClaudeExportCommand(port, callerSecret) {
4
+ const modelIds = readModelIdsFromCache();
5
+ const defaults = computeAnthropicDefaults(modelIds);
6
+ const command = buildClaudeExport({
7
+ port,
8
+ callerSecret,
9
+ defaults,
10
+ enableGatewayDiscovery: true
11
+ });
12
+ const bundle = buildClaudeEnvBundle({ port, callerSecret, defaults, enableGatewayDiscovery: true });
13
+ return { command, defaults, bundle };
14
+ }
@@ -0,0 +1,19 @@
1
+ import { getCopillmHome } from "../../config/home.js";
2
+ import { defaultOutputDir, generateCodexHome } from "../../integrations/codex/init.js";
3
+ export async function refreshCodexHome(port, model) {
4
+ try {
5
+ const home = getCopillmHome();
6
+ return await generateCodexHome({
7
+ outDir: defaultOutputDir(home),
8
+ model,
9
+ port,
10
+ providerId: "copillm",
11
+ reasoningEffort: null
12
+ });
13
+ }
14
+ catch (error) {
15
+ const message = error instanceof Error ? error.message : "unknown_error";
16
+ process.stderr.write(`warning: failed to generate ~/.copillm/codex/ — ${message}\n`);
17
+ return null;
18
+ }
19
+ }
@@ -0,0 +1,17 @@
1
+ import { getCopillmHome } from "../../config/home.js";
2
+ import { defaultOutputDir as defaultPiOutputDir, generatePiHome } from "../../integrations/pi/init.js";
3
+ export async function refreshPiHome(port) {
4
+ try {
5
+ const home = getCopillmHome();
6
+ return await generatePiHome({
7
+ outDir: defaultPiOutputDir(home),
8
+ port,
9
+ providerId: "copillm"
10
+ });
11
+ }
12
+ catch (error) {
13
+ const message = error instanceof Error ? error.message : "unknown_error";
14
+ process.stderr.write(`warning: failed to generate pi models.json — ${message}\n`);
15
+ return null;
16
+ }
17
+ }
@@ -0,0 +1,29 @@
1
+ import { createRequire } from "node:module";
2
+ const FALLBACK_PACKAGE_INFO = {
3
+ name: "copillm",
4
+ version: "0.2.5"
5
+ };
6
+ export function getPackageInfo() {
7
+ const envName = cleanPackageValue(process.env.COPILLM_PACKAGE_NAME);
8
+ const envVersion = cleanPackageValue(process.env.COPILLM_PACKAGE_VERSION);
9
+ if (envName && envVersion) {
10
+ return { name: envName, version: envVersion };
11
+ }
12
+ try {
13
+ const pkg = createRequire(import.meta.url)("../../package.json");
14
+ if (typeof pkg.name === "string" && pkg.name.length > 0 && typeof pkg.version === "string" && pkg.version.length > 0) {
15
+ return { name: pkg.name, version: pkg.version };
16
+ }
17
+ }
18
+ catch {
19
+ // Standalone bundles may not have package.json on disk.
20
+ }
21
+ return FALLBACK_PACKAGE_INFO;
22
+ }
23
+ export function fallbackPackageInfo() {
24
+ return FALLBACK_PACKAGE_INFO;
25
+ }
26
+ function cleanPackageValue(value) {
27
+ const trimmed = value?.trim();
28
+ return trimmed && trimmed.length > 0 ? trimmed : null;
29
+ }
@@ -0,0 +1,31 @@
1
+ export function describeBackend(backend) {
2
+ switch (backend) {
3
+ case "keyring":
4
+ return "OS keychain";
5
+ case "file":
6
+ return "credentials file";
7
+ case "session":
8
+ return "in-memory (session only)";
9
+ default:
10
+ return "no backend";
11
+ }
12
+ }
13
+ export function formatHumanAuthStatusLine(backend, identity) {
14
+ if (!identity) {
15
+ return `logged in (${describeBackend(backend)})`;
16
+ }
17
+ const nameSuffix = identity.name && identity.name !== identity.login ? ` (${identity.name})` : "";
18
+ return `logged in as @${identity.login}${nameSuffix} (${describeBackend(backend)})`;
19
+ }
20
+ export function writeAuthStatusLine(authInfo) {
21
+ if (authInfo.error) {
22
+ process.stdout.write(`auth: error (${authInfo.error})\n`);
23
+ return;
24
+ }
25
+ if (authInfo.stored) {
26
+ process.stdout.write(`auth: logged in (${describeBackend(authInfo.backend)})\n`);
27
+ }
28
+ else {
29
+ process.stdout.write("auth: not logged in\n");
30
+ }
31
+ }