copillm 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +357 -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 +101 -0
- package/dist/cli/daemon/ensureRunning.js +66 -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/spawnEnv.js +12 -0
- package/dist/cli/index.js +43 -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/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.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,101 @@
|
|
|
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
|
+
takesValue: true,
|
|
22
|
+
dest: "copillmUse",
|
|
23
|
+
kind: "swallow",
|
|
24
|
+
description: "Pin agent package version"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
flag: "--copillm-debug",
|
|
28
|
+
takesValue: false,
|
|
29
|
+
dest: "copillmDebug",
|
|
30
|
+
kind: "swallow",
|
|
31
|
+
description: "Enable debug endpoints when auto-starting daemon"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
flag: "--copillm-profile",
|
|
35
|
+
takesValue: true,
|
|
36
|
+
dest: "copillmProfile",
|
|
37
|
+
kind: "swallow",
|
|
38
|
+
description: "Override active profile for this launch"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
flag: "--copillm-no-config",
|
|
42
|
+
takesValue: false,
|
|
43
|
+
dest: "copillmNoConfig",
|
|
44
|
+
kind: "swallow",
|
|
45
|
+
description: "Skip agent.toml fan-out for this launch"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
flag: "--yolo",
|
|
49
|
+
takesValue: false,
|
|
50
|
+
dest: "yolo",
|
|
51
|
+
kind: "translate",
|
|
52
|
+
description: "Skip approvals (translated per-agent)"
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
const SPEC_BY_FLAG = new Map(COPILLM_FLAGS.map((spec) => [spec.flag, spec]));
|
|
56
|
+
/**
|
|
57
|
+
* Extract copillm-owned flags from a raw arg tail, returning the parsed opts
|
|
58
|
+
* plus everything else to forward to the agent.
|
|
59
|
+
*
|
|
60
|
+
* Extract-everywhere: copillm flags are pulled out regardless of position,
|
|
61
|
+
* including after a `--` separator (the `--` itself is forwarded). Accepted
|
|
62
|
+
* tradeoff: a literal `--copillm-*`/`--yolo` token cannot be passed through to
|
|
63
|
+
* the agent as data. These tokens have no legitimate meaning to the agents.
|
|
64
|
+
*
|
|
65
|
+
* Pure function, no I/O.
|
|
66
|
+
*/
|
|
67
|
+
export function processCopillmArgs(rawArgs) {
|
|
68
|
+
const opts = {};
|
|
69
|
+
const forwarded = [];
|
|
70
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
71
|
+
const token = rawArgs[i];
|
|
72
|
+
const eq = token.indexOf("=");
|
|
73
|
+
const name = eq === -1 ? token : token.slice(0, eq);
|
|
74
|
+
const spec = SPEC_BY_FLAG.get(name);
|
|
75
|
+
if (!spec) {
|
|
76
|
+
forwarded.push(token);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (spec.takesValue) {
|
|
80
|
+
let value;
|
|
81
|
+
if (eq !== -1) {
|
|
82
|
+
value = token.slice(eq + 1);
|
|
83
|
+
}
|
|
84
|
+
else if (i + 1 < rawArgs.length) {
|
|
85
|
+
value = rawArgs[++i];
|
|
86
|
+
}
|
|
87
|
+
if (value === undefined) {
|
|
88
|
+
throw new Error(`${spec.flag} requires a value`);
|
|
89
|
+
}
|
|
90
|
+
setOpt(opts, spec.dest, value);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
setOpt(opts, spec.dest, true);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { opts, forwarded };
|
|
97
|
+
}
|
|
98
|
+
function setOpt(opts, dest, value) {
|
|
99
|
+
// Last-wins on repeats. dest/value pairing is guaranteed by the spec table.
|
|
100
|
+
opts[dest] = value;
|
|
101
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
export async function ensureDaemonRunningForLauncher(opts) {
|
|
10
|
+
const live = await readLiveLock();
|
|
11
|
+
if (live) {
|
|
12
|
+
await warnIfDebugRequestedButInactive(opts.debug, live.port);
|
|
13
|
+
return live;
|
|
14
|
+
}
|
|
15
|
+
// Fail fast on missing credentials rather than spawning a detached daemon
|
|
16
|
+
// that will die silently and surface as a generic "start timed out" error.
|
|
17
|
+
const authState = await inspectStoredCredential();
|
|
18
|
+
if (!authState.stored) {
|
|
19
|
+
throw new Error("Not authenticated. Run `copillm auth login` first.");
|
|
20
|
+
}
|
|
21
|
+
const debugLog = currentDebugLogPath(opts.debug);
|
|
22
|
+
process.stderr.write(opts.debug && debugLog
|
|
23
|
+
? `Starting copillm in background with debug logging at ${displayHomePath(debugLog)}...\n`
|
|
24
|
+
: `Starting copillm in background...\n`);
|
|
25
|
+
const daemonArgs = [process.argv[1], "daemon"];
|
|
26
|
+
if (opts.debug)
|
|
27
|
+
daemonArgs.push("--debug");
|
|
28
|
+
const child = spawn(process.execPath, daemonArgs, {
|
|
29
|
+
detached: true,
|
|
30
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
31
|
+
env: daemonSpawnEnv(opts.debug)
|
|
32
|
+
});
|
|
33
|
+
child.unref();
|
|
34
|
+
const stderrChunks = [];
|
|
35
|
+
let stderrBytes = 0;
|
|
36
|
+
const STDERR_TAIL_LIMIT = 8 * 1024;
|
|
37
|
+
if (child.stderr) {
|
|
38
|
+
child.stderr.on("data", (chunk) => {
|
|
39
|
+
stderrChunks.push(chunk);
|
|
40
|
+
stderrBytes += chunk.length;
|
|
41
|
+
while (stderrBytes > STDERR_TAIL_LIMIT && stderrChunks.length > 1) {
|
|
42
|
+
stderrBytes -= stderrChunks[0].length;
|
|
43
|
+
stderrChunks.shift();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
child.stderr.on("error", () => {
|
|
47
|
+
// Ignore — best-effort capture only.
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const formatStderrTail = () => {
|
|
51
|
+
const tail = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
52
|
+
return tail ? `\nDaemon stderr (tail):\n${tail}` : "";
|
|
53
|
+
};
|
|
54
|
+
const started = await waitForDaemonReady(child.pid ?? null, 10_000);
|
|
55
|
+
if (!started) {
|
|
56
|
+
if (child.pid !== undefined && !isPidAlive(child.pid)) {
|
|
57
|
+
throw new Error(`copillm daemon exited before becoming ready.${formatStderrTail()}`);
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Auto-start of copillm daemon timed out.${formatStderrTail()}`);
|
|
60
|
+
}
|
|
61
|
+
const inspection = inspectLock();
|
|
62
|
+
if (inspection.state !== "running") {
|
|
63
|
+
throw new Error(`copillm daemon failed to register a lock after auto-start.${formatStderrTail()}`);
|
|
64
|
+
}
|
|
65
|
+
return inspection.lock;
|
|
66
|
+
}
|
|
@@ -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,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,43 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { createLogger } from "../config/logging.js";
|
|
4
|
+
import { registerConfigCommands } from "./configCommands.js";
|
|
5
|
+
import * as authCmd from "./commands/auth.js";
|
|
6
|
+
import * as daemonCmd from "./commands/daemon.js";
|
|
7
|
+
import * as modelsCmd from "./commands/models.js";
|
|
8
|
+
import * as envCmd from "./commands/env.js";
|
|
9
|
+
import * as codexCmd from "./commands/agents/codex.js";
|
|
10
|
+
import * as claudeCmd from "./commands/agents/claude.js";
|
|
11
|
+
import * as piCmd from "./commands/agents/pi.js";
|
|
12
|
+
import * as copilotCmd from "./commands/agents/copilot.js";
|
|
13
|
+
import { setRootLogger, setRootProgram } from "./shared/debug.js";
|
|
14
|
+
const logger = createLogger();
|
|
15
|
+
const program = new Command();
|
|
16
|
+
setRootProgram(program);
|
|
17
|
+
setRootLogger(logger);
|
|
18
|
+
// Resolve the package version from package.json at runtime so `--version` stays
|
|
19
|
+
// in sync with whatever was published. Using createRequire keeps this working
|
|
20
|
+
// under NodeNext ESM without needing an import-assertion syntax flag, and
|
|
21
|
+
// resolves the same file in both `dist/cli.js` (one level deep) and `src/cli.ts`
|
|
22
|
+
// when invoked via tsx.
|
|
23
|
+
const pkgVersion = createRequire(import.meta.url)("../../package.json").version;
|
|
24
|
+
program.name("copillm").description("Local Copilot proxy").version(pkgVersion);
|
|
25
|
+
program.enablePositionalOptions();
|
|
26
|
+
program.option("--debug", "Enable copillm debug mode (debug endpoint plus verbose daemon diagnostics)");
|
|
27
|
+
authCmd.register(program);
|
|
28
|
+
daemonCmd.register(program);
|
|
29
|
+
modelsCmd.register(program);
|
|
30
|
+
envCmd.register(program);
|
|
31
|
+
codexCmd.register(program);
|
|
32
|
+
claudeCmd.register(program);
|
|
33
|
+
piCmd.register(program);
|
|
34
|
+
copilotCmd.register(program);
|
|
35
|
+
registerConfigCommands(program);
|
|
36
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
37
|
+
if (error instanceof Error) {
|
|
38
|
+
logger.error({ err: error }, error.message);
|
|
39
|
+
process.stderr.write(`${error.message}\n`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
});
|
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { debugLogPath } from "../../config/home.js";
|
|
2
|
+
// The root Command and root logger are constructed in cli/index.ts and
|
|
3
|
+
// registered here so any command module can resolve "is debug globally on?"
|
|
4
|
+
// without taking a Command parameter through every call. This preserves the
|
|
5
|
+
// previous behavior where helpers reached for the module-level `program` and
|
|
6
|
+
// `logger` directly.
|
|
7
|
+
let rootProgram = null;
|
|
8
|
+
let rootLogger = null;
|
|
9
|
+
export function setRootProgram(program) {
|
|
10
|
+
rootProgram = program;
|
|
11
|
+
}
|
|
12
|
+
export function setRootLogger(logger) {
|
|
13
|
+
rootLogger = logger;
|
|
14
|
+
}
|
|
15
|
+
export function getRootLogger() {
|
|
16
|
+
if (!rootLogger) {
|
|
17
|
+
throw new Error("Root logger not initialized — cli/index.ts must call setRootLogger() first.");
|
|
18
|
+
}
|
|
19
|
+
return rootLogger;
|
|
20
|
+
}
|
|
21
|
+
function getGlobalDebug() {
|
|
22
|
+
if (!rootProgram) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return Boolean(rootProgram.opts().debug);
|
|
26
|
+
}
|
|
27
|
+
export function resolveCopillmDebug(commandDebug) {
|
|
28
|
+
return Boolean(commandDebug) || getGlobalDebug();
|
|
29
|
+
}
|
|
30
|
+
export function enableRuntimeDebug(debug) {
|
|
31
|
+
if (!debug) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
process.env.COPILLM_LOG_LEVEL = "debug";
|
|
35
|
+
if (rootLogger) {
|
|
36
|
+
rootLogger.level = "debug";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function currentDebugLogPath(debug) {
|
|
40
|
+
if (!debug) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return process.env.COPILLM_LOG_FILE ?? debugLogPath();
|
|
44
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function emitDeprecation(_opts, oldCmd, newCmd) {
|
|
2
|
+
// JSON consumers still get the deprecation on stderr so stdout stays pure;
|
|
3
|
+
// human consumers also get it on stderr. The branches are intentionally
|
|
4
|
+
// identical (preserving the original code shape) so future divergence is
|
|
5
|
+
// explicit.
|
|
6
|
+
process.stderr.write(`note: \`copillm ${oldCmd}\` is deprecated; use \`copillm ${newCmd}\`\n`);
|
|
7
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Named exit codes used by the CLI. Behavior must match the previous inline
|
|
2
|
+
// `process.exit(...)` literals exactly — these constants are documentation,
|
|
3
|
+
// not policy changes.
|
|
4
|
+
export const EXIT_OK = 0;
|
|
5
|
+
export const EXIT_ERROR = 1;
|
|
6
|
+
// Used for "not running" and "not logged in" — terminal states where the
|
|
7
|
+
// command completed successfully but the asked-about resource is absent.
|
|
8
|
+
export const EXIT_NOT_RUNNING = 2;
|
|
9
|
+
export const EXIT_NOT_LOGGED_IN = 2;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function writeCommandOutput(opts, humanLine, payload) {
|
|
2
|
+
if (opts.json) {
|
|
3
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
process.stdout.write(`${humanLine}\n`);
|
|
7
|
+
}
|
|
8
|
+
export function writeHealthOutput(opts, payload) {
|
|
9
|
+
if (opts.json) {
|
|
10
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
14
|
+
}
|