copillm 0.2.1 → 0.2.3
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/dist/agents/registry.js +72 -0
- package/dist/auth/credentials.js +80 -29
- package/dist/cli.js +70 -13
- package/package.json +2 -2
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent capability registry. The single source of truth for agent-specific
|
|
3
|
+
* behaviour that copillm needs to know about (yolo mapping, future: model
|
|
4
|
+
* pinning, debug surfaces, etc.). Adding a new agent should be one row here
|
|
5
|
+
* plus wiring in `src/cli.ts` — never a new branch in the action handlers.
|
|
6
|
+
*
|
|
7
|
+
* Note: the `AgentName` union lives in `../integrations/registry.ts` (paired
|
|
8
|
+
* with npm package / bin name metadata). We reuse it here so the two
|
|
9
|
+
* registries can never diverge.
|
|
10
|
+
*/
|
|
11
|
+
export const AGENTS = {
|
|
12
|
+
claude: {
|
|
13
|
+
name: "claude",
|
|
14
|
+
yolo: { mode: "inject", flags: ["--dangerously-skip-permissions"] }
|
|
15
|
+
},
|
|
16
|
+
codex: {
|
|
17
|
+
name: "codex",
|
|
18
|
+
yolo: { mode: "inject", flags: ["--dangerously-bypass-approvals-and-sandbox"] }
|
|
19
|
+
},
|
|
20
|
+
copilot: {
|
|
21
|
+
name: "copilot",
|
|
22
|
+
yolo: { mode: "inject", flags: ["--allow-all"] }
|
|
23
|
+
},
|
|
24
|
+
pi: {
|
|
25
|
+
name: "pi",
|
|
26
|
+
yolo: {
|
|
27
|
+
mode: "unsupported",
|
|
28
|
+
reason: "pi has no blanket-approve flag; use its per-tool approvals instead"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Resolve `--yolo` for a given agent and return the (possibly transformed)
|
|
34
|
+
* argv to forward downstream. Pure function aside from the optional warning
|
|
35
|
+
* sink — easy to unit-test.
|
|
36
|
+
*/
|
|
37
|
+
export function applyYolo(options) {
|
|
38
|
+
const args = [...options.userArgs];
|
|
39
|
+
if (!options.yolo)
|
|
40
|
+
return args;
|
|
41
|
+
const spec = AGENTS[options.agent].yolo;
|
|
42
|
+
switch (spec.mode) {
|
|
43
|
+
case "inject": {
|
|
44
|
+
const alreadyPresent = spec.flags.some((flag) => args.includes(flag));
|
|
45
|
+
if (alreadyPresent)
|
|
46
|
+
return args;
|
|
47
|
+
return [...spec.flags, ...args];
|
|
48
|
+
}
|
|
49
|
+
case "passthrough": {
|
|
50
|
+
if (args.includes("--yolo"))
|
|
51
|
+
return args;
|
|
52
|
+
return ["--yolo", ...args];
|
|
53
|
+
}
|
|
54
|
+
case "unsupported": {
|
|
55
|
+
const warn = options.warn ?? ((line) => process.stderr.write(`${line}\n`));
|
|
56
|
+
warn(`copillm: --yolo ignored for ${options.agent} (${spec.reason})`);
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read the `COPILLM_YOLO` env var as a boolean. Accepts "1", "true", "yes"
|
|
63
|
+
* (case-insensitive) as truthy; everything else (including unset) is false.
|
|
64
|
+
*/
|
|
65
|
+
export function yoloFromEnv(env = process.env) {
|
|
66
|
+
const raw = env.COPILLM_YOLO?.trim().toLowerCase();
|
|
67
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
68
|
+
}
|
|
69
|
+
/** Combine the per-launch flag with the env var fallback. */
|
|
70
|
+
export function resolveYolo(flag, env = process.env) {
|
|
71
|
+
return Boolean(flag) || yoloFromEnv(env);
|
|
72
|
+
}
|
package/dist/auth/credentials.js
CHANGED
|
@@ -18,16 +18,57 @@ let sessionCredential = null;
|
|
|
18
18
|
function forceSessionBackend() {
|
|
19
19
|
return process.env.COPILLM_FORCE_SESSION_BACKEND === "1";
|
|
20
20
|
}
|
|
21
|
-
async function
|
|
21
|
+
async function tryImportKeyring() {
|
|
22
22
|
if (forceSessionBackend()) {
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
try {
|
|
26
|
-
const mod = await import("
|
|
27
|
-
return
|
|
26
|
+
const mod = (await import("@napi-rs/keyring"));
|
|
27
|
+
// Test seam: mocks can return `null` or `{ default: null }` to simulate an
|
|
28
|
+
// unavailable backend without throwing from the vi.mock factory (which
|
|
29
|
+
// confuses vitest's hoisting and our isMissingKeyringError check).
|
|
30
|
+
if (!mod) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const AsyncEntry = mod.AsyncEntry ?? (mod.default && typeof mod.default === "object" ? mod.default.AsyncEntry : undefined);
|
|
34
|
+
if (typeof AsyncEntry !== "function") {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
async getPassword(service, account) {
|
|
39
|
+
const entry = new AsyncEntry(service, account);
|
|
40
|
+
try {
|
|
41
|
+
const value = await entry.getPassword();
|
|
42
|
+
return value ?? null;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (isNoEntryError(error)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async setPassword(service, account, password) {
|
|
52
|
+
const entry = new AsyncEntry(service, account);
|
|
53
|
+
await entry.setPassword(password);
|
|
54
|
+
},
|
|
55
|
+
async deletePassword(service, account) {
|
|
56
|
+
const entry = new AsyncEntry(service, account);
|
|
57
|
+
try {
|
|
58
|
+
await entry.deletePassword();
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (isNoEntryError(error)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
28
69
|
}
|
|
29
70
|
catch (error) {
|
|
30
|
-
if (
|
|
71
|
+
if (isMissingKeyringError(error)) {
|
|
31
72
|
return null;
|
|
32
73
|
}
|
|
33
74
|
if (error instanceof Error) {
|
|
@@ -36,15 +77,25 @@ async function tryImportKeytar() {
|
|
|
36
77
|
throw new Error("Failed to initialize OS keychain backend: unknown error");
|
|
37
78
|
}
|
|
38
79
|
}
|
|
39
|
-
|
|
80
|
+
// keyring-rs returns a "NoEntry" error when an item doesn't exist. Map that to
|
|
81
|
+
// null/false to preserve the keytar-style "missing is not an error" semantics
|
|
82
|
+
// that the rest of credentials.ts is built around.
|
|
83
|
+
function isNoEntryError(error) {
|
|
84
|
+
if (!(error instanceof Error)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const message = error.message.toLowerCase();
|
|
88
|
+
return message.includes("no matching entry") || message.includes("no entry") || message.includes("noentry");
|
|
89
|
+
}
|
|
90
|
+
async function resolveKeyring() {
|
|
40
91
|
if (forceSessionBackend()) {
|
|
41
|
-
return {
|
|
92
|
+
return { keyring: null, reason: "forced_session_backend" };
|
|
42
93
|
}
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
return {
|
|
94
|
+
const keyring = await tryImportKeyring();
|
|
95
|
+
if (keyring) {
|
|
96
|
+
return { keyring, reason: null };
|
|
46
97
|
}
|
|
47
|
-
return {
|
|
98
|
+
return { keyring: null, reason: "keyring module is unavailable on this machine" };
|
|
48
99
|
}
|
|
49
100
|
function parseCredentialFile() {
|
|
50
101
|
const path = credentialsReadPath();
|
|
@@ -86,13 +137,13 @@ function canUsePlaintextFallback() {
|
|
|
86
137
|
}
|
|
87
138
|
return process.stdin.isTTY || process.env.COPILLM_ALLOW_PLAINTEXT_CREDENTIALS === "1";
|
|
88
139
|
}
|
|
89
|
-
function
|
|
140
|
+
function isMissingKeyringError(error) {
|
|
90
141
|
if (!(error instanceof Error)) {
|
|
91
142
|
return false;
|
|
92
143
|
}
|
|
93
144
|
const message = error.message.toLowerCase();
|
|
94
|
-
return (message.includes("cannot find package '
|
|
95
|
-
message.includes("cannot find module '
|
|
145
|
+
return (message.includes("cannot find package '@napi-rs/keyring") ||
|
|
146
|
+
message.includes("cannot find module '@napi-rs/keyring") ||
|
|
96
147
|
message.includes("module not found"));
|
|
97
148
|
}
|
|
98
149
|
/**
|
|
@@ -108,14 +159,14 @@ export async function inspectStoredCredential() {
|
|
|
108
159
|
if (fs.existsSync(credentialsReadPath())) {
|
|
109
160
|
return { stored: true, backend: "file" };
|
|
110
161
|
}
|
|
111
|
-
const {
|
|
112
|
-
if (!
|
|
162
|
+
const { keyring } = await resolveKeyring();
|
|
163
|
+
if (!keyring) {
|
|
113
164
|
return { stored: false, backend: null };
|
|
114
165
|
}
|
|
115
166
|
try {
|
|
116
|
-
const token = await
|
|
167
|
+
const token = await keyring.getPassword(SERVICE, ACCOUNT);
|
|
117
168
|
if (token) {
|
|
118
|
-
return { stored: true, backend: "
|
|
169
|
+
return { stored: true, backend: "keyring" };
|
|
119
170
|
}
|
|
120
171
|
return { stored: false, backend: null };
|
|
121
172
|
}
|
|
@@ -134,13 +185,13 @@ export async function loadStoredCredential() {
|
|
|
134
185
|
const parsed = parseCredentialFile();
|
|
135
186
|
return { token: parsed.token, accountType: parsed.accountType, source: "file" };
|
|
136
187
|
}
|
|
137
|
-
const {
|
|
138
|
-
if (!
|
|
188
|
+
const { keyring } = await resolveKeyring();
|
|
189
|
+
if (!keyring) {
|
|
139
190
|
return null;
|
|
140
191
|
}
|
|
141
192
|
let token;
|
|
142
193
|
try {
|
|
143
|
-
token = await
|
|
194
|
+
token = await keyring.getPassword(SERVICE, ACCOUNT);
|
|
144
195
|
}
|
|
145
196
|
catch (error) {
|
|
146
197
|
if (error instanceof Error) {
|
|
@@ -151,7 +202,7 @@ export async function loadStoredCredential() {
|
|
|
151
202
|
if (!token) {
|
|
152
203
|
return null;
|
|
153
204
|
}
|
|
154
|
-
return { token, accountType: "individual", source: "
|
|
205
|
+
return { token, accountType: "individual", source: "keyring" };
|
|
155
206
|
}
|
|
156
207
|
export async function saveStoredCredential(token, accountType, options = {}) {
|
|
157
208
|
const mode = options.mode ?? "auto";
|
|
@@ -164,11 +215,11 @@ export async function saveStoredCredential(token, accountType, options = {}) {
|
|
|
164
215
|
writeCredentialFile(token, accountType);
|
|
165
216
|
return "file";
|
|
166
217
|
}
|
|
167
|
-
const {
|
|
168
|
-
if (
|
|
218
|
+
const { keyring, reason } = await resolveKeyring();
|
|
219
|
+
if (keyring) {
|
|
169
220
|
try {
|
|
170
|
-
await
|
|
171
|
-
return "
|
|
221
|
+
await keyring.setPassword(SERVICE, ACCOUNT, token);
|
|
222
|
+
return "keyring";
|
|
172
223
|
}
|
|
173
224
|
catch (error) {
|
|
174
225
|
if (error instanceof Error) {
|
|
@@ -196,11 +247,11 @@ export async function clearStoredCredential() {
|
|
|
196
247
|
}
|
|
197
248
|
return { backend: "file", removed: true };
|
|
198
249
|
}
|
|
199
|
-
const {
|
|
200
|
-
if (
|
|
250
|
+
const { keyring, reason } = await resolveKeyring();
|
|
251
|
+
if (keyring) {
|
|
201
252
|
try {
|
|
202
|
-
const removed = await
|
|
203
|
-
return { backend: "
|
|
253
|
+
const removed = await keyring.deletePassword(SERVICE, ACCOUNT);
|
|
254
|
+
return { backend: "keyring", removed: removed || hadSession };
|
|
204
255
|
}
|
|
205
256
|
catch (error) {
|
|
206
257
|
if (error instanceof Error) {
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
6
|
import { Command } from "commander";
|
|
6
7
|
import { clearStoredCredential, inspectStoredCredential, loadStoredCredential, saveStoredCredential } from "./auth/credentials.js";
|
|
@@ -24,11 +25,18 @@ import { isShellSyntax, renderEnvBlock } from "./cli/envBlock.js";
|
|
|
24
25
|
import { buildClaudeEnvBundle, buildCodexEnvBundle, buildPiEnvBundle } from "./cli/agentEnv.js";
|
|
25
26
|
import { launchAgent } from "./cli/launchAgent.js";
|
|
26
27
|
import { applyAgentConfig, formatApplyNotes } from "./agentconfig/apply.js";
|
|
28
|
+
import { applyYolo, resolveYolo } from "./agents/registry.js";
|
|
27
29
|
import { registerConfigCommands } from "./cli/configCommands.js";
|
|
28
30
|
import { installProcessSafetyNet } from "./cli/processSafetyNet.js";
|
|
29
31
|
const logger = createLogger();
|
|
30
32
|
const program = new Command();
|
|
31
|
-
|
|
33
|
+
// Resolve the package version from package.json at runtime so `--version` stays
|
|
34
|
+
// in sync with whatever was published. Using createRequire keeps this working
|
|
35
|
+
// under NodeNext ESM without needing an import-assertion syntax flag, and
|
|
36
|
+
// resolves the same file in both `dist/cli.js` (one level deep) and `src/cli.ts`
|
|
37
|
+
// when invoked via tsx.
|
|
38
|
+
const pkgVersion = createRequire(import.meta.url)("../package.json").version;
|
|
39
|
+
program.name("copillm").description("Local Copilot proxy").version(pkgVersion);
|
|
32
40
|
program.enablePositionalOptions();
|
|
33
41
|
program.option("--debug", "Enable copillm debug mode (debug endpoint plus verbose daemon diagnostics)");
|
|
34
42
|
program
|
|
@@ -304,11 +312,19 @@ program
|
|
|
304
312
|
.action(async (opts) => {
|
|
305
313
|
const debug = resolveCopillmDebug(opts.debug);
|
|
306
314
|
enableRuntimeDebug(debug);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
315
|
+
try {
|
|
316
|
+
const started = await runDaemon({ debug });
|
|
317
|
+
if (started.kind === "already_running") {
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}
|
|
320
|
+
process.stdout.write(`copillm listening on http://127.0.0.1:${started.port}${debug ? " [debug]" : ""}\n`);
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
324
|
+
logger.fatal({ err }, "daemon failed to start");
|
|
325
|
+
process.stderr.write(`copillm daemon: ${message}\n`);
|
|
326
|
+
process.exit(1);
|
|
310
327
|
}
|
|
311
|
-
process.stdout.write(`copillm listening on http://127.0.0.1:${started.port}${debug ? " [debug]" : ""}\n`);
|
|
312
328
|
});
|
|
313
329
|
program
|
|
314
330
|
.command("stop")
|
|
@@ -666,6 +682,7 @@ program
|
|
|
666
682
|
.option("--copillm-debug", "Enable debug endpoints when auto-starting daemon")
|
|
667
683
|
.option("--copillm-profile <name>", "Override active profile from ~/.copillm/agent.toml for this launch")
|
|
668
684
|
.option("--copillm-no-config", "Skip agent.toml fan-out for this launch", false)
|
|
685
|
+
.option("--yolo", "Skip approvals/sandbox (injects --dangerously-bypass-approvals-and-sandbox). Env: COPILLM_YOLO")
|
|
669
686
|
.allowUnknownOption(true)
|
|
670
687
|
.passThroughOptions()
|
|
671
688
|
.helpOption(false)
|
|
@@ -691,9 +708,11 @@ program
|
|
|
691
708
|
process.stderr.write(`${line}\n`);
|
|
692
709
|
}
|
|
693
710
|
const env = { ...bundle.env, ...applyResult.envOverlay };
|
|
711
|
+
const baseArgs = [...(forwardedArgs ?? []), ...applyResult.cliArgs];
|
|
712
|
+
const args = applyYolo({ agent: "codex", userArgs: baseArgs, yolo: resolveYolo(opts.yolo) });
|
|
694
713
|
const exitCode = await launchAgent({
|
|
695
714
|
agent: "codex",
|
|
696
|
-
args
|
|
715
|
+
args,
|
|
697
716
|
env,
|
|
698
717
|
pinnedSpec
|
|
699
718
|
});
|
|
@@ -706,6 +725,7 @@ program
|
|
|
706
725
|
.option("--copillm-debug", "Enable debug endpoints when auto-starting daemon")
|
|
707
726
|
.option("--copillm-profile <name>", "Override active profile from ~/.copillm/agent.toml for this launch")
|
|
708
727
|
.option("--copillm-no-config", "Skip agent.toml fan-out for this launch", false)
|
|
728
|
+
.option("--yolo", "Skip permission prompts (injects --dangerously-skip-permissions). Env: COPILLM_YOLO")
|
|
709
729
|
.allowUnknownOption(true)
|
|
710
730
|
.passThroughOptions()
|
|
711
731
|
.helpOption(false)
|
|
@@ -730,9 +750,11 @@ program
|
|
|
730
750
|
process.stderr.write(`${line}\n`);
|
|
731
751
|
}
|
|
732
752
|
const env = { ...claude.bundle.env, ...applyResult.envOverlay };
|
|
753
|
+
const baseArgs = [...(forwardedArgs ?? []), ...applyResult.cliArgs];
|
|
754
|
+
const args = applyYolo({ agent: "claude", userArgs: baseArgs, yolo: resolveYolo(opts.yolo) });
|
|
733
755
|
const exitCode = await launchAgent({
|
|
734
756
|
agent: "claude",
|
|
735
|
-
args
|
|
757
|
+
args,
|
|
736
758
|
env,
|
|
737
759
|
pinnedSpec
|
|
738
760
|
});
|
|
@@ -745,6 +767,7 @@ program
|
|
|
745
767
|
.option("--copillm-debug", "Enable debug endpoints when auto-starting daemon")
|
|
746
768
|
.option("--copillm-profile <name>", "Override active profile from ~/.copillm/agent.toml for this launch")
|
|
747
769
|
.option("--copillm-no-config", "Skip agent.toml fan-out for this launch", false)
|
|
770
|
+
.option("--yolo", "Skip approvals if supported (pi has no equivalent; emits a warning). Env: COPILLM_YOLO")
|
|
748
771
|
.allowUnknownOption(true)
|
|
749
772
|
.passThroughOptions()
|
|
750
773
|
.helpOption(false)
|
|
@@ -769,9 +792,11 @@ program
|
|
|
769
792
|
process.stderr.write(`${line}\n`);
|
|
770
793
|
}
|
|
771
794
|
const env = { ...bundle.env, ...applyResult.envOverlay };
|
|
795
|
+
const baseArgs = [...(forwardedArgs ?? []), ...applyResult.cliArgs];
|
|
796
|
+
const args = applyYolo({ agent: "pi", userArgs: baseArgs, yolo: resolveYolo(opts.yolo) });
|
|
772
797
|
const exitCode = await launchAgent({
|
|
773
798
|
agent: "pi",
|
|
774
|
-
args
|
|
799
|
+
args,
|
|
775
800
|
env,
|
|
776
801
|
pinnedSpec
|
|
777
802
|
});
|
|
@@ -783,6 +808,7 @@ program
|
|
|
783
808
|
.option("--copillm-use <spec>", "Pin copilot package version (e.g. 1.0.52 or @github/copilot@1.0.52)")
|
|
784
809
|
.option("--copillm-profile <name>", "Override active profile from ~/.copillm/agent.toml for this launch")
|
|
785
810
|
.option("--copillm-no-config", "Skip agent.toml fan-out for this launch", false)
|
|
811
|
+
.option("--yolo", "Allow all tools/paths/URLs (injects --allow-all). Env: COPILLM_YOLO")
|
|
786
812
|
.allowUnknownOption(true)
|
|
787
813
|
.passThroughOptions()
|
|
788
814
|
.helpOption(false)
|
|
@@ -812,9 +838,11 @@ program
|
|
|
812
838
|
...applyResult.envOverlay,
|
|
813
839
|
COPILOT_GITHUB_TOKEN: credential.token
|
|
814
840
|
};
|
|
841
|
+
const baseArgs = [...(forwardedArgs ?? []), ...applyResult.cliArgs];
|
|
842
|
+
const args = applyYolo({ agent: "copilot", userArgs: baseArgs, yolo: resolveYolo(opts.yolo) });
|
|
815
843
|
const exitCode = await launchAgent({
|
|
816
844
|
agent: "copilot",
|
|
817
|
-
args
|
|
845
|
+
args,
|
|
818
846
|
env,
|
|
819
847
|
pinnedSpec
|
|
820
848
|
});
|
|
@@ -919,7 +947,7 @@ function candidatePorts(preferredPort) {
|
|
|
919
947
|
}
|
|
920
948
|
function describeBackend(backend) {
|
|
921
949
|
switch (backend) {
|
|
922
|
-
case "
|
|
950
|
+
case "keyring":
|
|
923
951
|
return "OS keychain";
|
|
924
952
|
case "file":
|
|
925
953
|
return "credentials file";
|
|
@@ -1274,6 +1302,12 @@ async function ensureDaemonRunningForLauncher(opts) {
|
|
|
1274
1302
|
await warnIfDebugRequestedButInactive(opts.debug, live.port);
|
|
1275
1303
|
return live;
|
|
1276
1304
|
}
|
|
1305
|
+
// Fail fast on missing credentials rather than spawning a detached daemon
|
|
1306
|
+
// that will die silently and surface as a generic "start timed out" error.
|
|
1307
|
+
const authState = await inspectStoredCredential();
|
|
1308
|
+
if (!authState.stored) {
|
|
1309
|
+
throw new Error("Not authenticated. Run `copillm auth login` first.");
|
|
1310
|
+
}
|
|
1277
1311
|
const debugLog = currentDebugLogPath(opts.debug);
|
|
1278
1312
|
process.stderr.write(opts.debug && debugLog
|
|
1279
1313
|
? `Starting copillm in background with debug logging at ${displayHomePath(debugLog)}...\n`
|
|
@@ -1283,17 +1317,40 @@ async function ensureDaemonRunningForLauncher(opts) {
|
|
|
1283
1317
|
daemonArgs.push("--debug");
|
|
1284
1318
|
const child = spawn(process.execPath, daemonArgs, {
|
|
1285
1319
|
detached: true,
|
|
1286
|
-
stdio: "ignore",
|
|
1320
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
1287
1321
|
env: daemonSpawnEnv(opts.debug)
|
|
1288
1322
|
});
|
|
1289
1323
|
child.unref();
|
|
1324
|
+
const stderrChunks = [];
|
|
1325
|
+
let stderrBytes = 0;
|
|
1326
|
+
const STDERR_TAIL_LIMIT = 8 * 1024;
|
|
1327
|
+
if (child.stderr) {
|
|
1328
|
+
child.stderr.on("data", (chunk) => {
|
|
1329
|
+
stderrChunks.push(chunk);
|
|
1330
|
+
stderrBytes += chunk.length;
|
|
1331
|
+
while (stderrBytes > STDERR_TAIL_LIMIT && stderrChunks.length > 1) {
|
|
1332
|
+
stderrBytes -= stderrChunks[0].length;
|
|
1333
|
+
stderrChunks.shift();
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
child.stderr.on("error", () => {
|
|
1337
|
+
// Ignore — best-effort capture only.
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
const formatStderrTail = () => {
|
|
1341
|
+
const tail = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
1342
|
+
return tail ? `\nDaemon stderr (tail):\n${tail}` : "";
|
|
1343
|
+
};
|
|
1290
1344
|
const started = await waitForDaemonReady(child.pid ?? null, 10_000);
|
|
1291
1345
|
if (!started) {
|
|
1292
|
-
|
|
1346
|
+
if (child.pid !== undefined && !isPidAlive(child.pid)) {
|
|
1347
|
+
throw new Error(`copillm daemon exited before becoming ready.${formatStderrTail()}`);
|
|
1348
|
+
}
|
|
1349
|
+
throw new Error(`Auto-start of copillm daemon timed out.${formatStderrTail()}`);
|
|
1293
1350
|
}
|
|
1294
1351
|
const inspection = inspectLock();
|
|
1295
1352
|
if (inspection.state !== "running") {
|
|
1296
|
-
throw new Error(
|
|
1353
|
+
throw new Error(`copillm daemon failed to register a lock after auto-start.${formatStderrTail()}`);
|
|
1297
1354
|
}
|
|
1298
1355
|
return inspection.lock;
|
|
1299
1356
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copillm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Local Copilot proxy CLI (OpenAI/Anthropic-compatible)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"prepack": "npm run build"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@napi-rs/keyring": "^1.3.0",
|
|
37
38
|
"commander": "^12.1.0",
|
|
38
|
-
"keytar": "^7.9.0",
|
|
39
39
|
"pino": "^9.4.0",
|
|
40
40
|
"pino-pretty": "^11.2.2",
|
|
41
41
|
"smol-toml": "^1.6.1",
|