copillm 0.1.0
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 +52 -0
- package/dist/agentconfig/apply.js +53 -0
- package/dist/agentconfig/load.js +163 -0
- package/dist/agentconfig/markerBlock.js +76 -0
- package/dist/agentconfig/render.js +317 -0
- package/dist/agentconfig/schema.js +65 -0
- package/dist/auth/copilotToken.js +122 -0
- package/dist/auth/credentials.js +221 -0
- package/dist/auth/deviceFlow.js +89 -0
- package/dist/auth/ensureAuthenticated.js +55 -0
- package/dist/auth/githubIdentity.js +42 -0
- package/dist/auth/interactivePrompt.js +135 -0
- package/dist/claude/cache.js +20 -0
- package/dist/claude/settingsConflict.js +85 -0
- package/dist/cli/agentEnv.js +56 -0
- package/dist/cli/configCommands.js +149 -0
- package/dist/cli/envBlock.js +43 -0
- package/dist/cli/launchAgent.js +59 -0
- package/dist/cli/resolveAgent.js +361 -0
- package/dist/cli.js +1178 -0
- package/dist/codex/init.js +93 -0
- package/dist/config/config.js +51 -0
- package/dist/config/fsSecurity.js +39 -0
- package/dist/config/home.js +62 -0
- package/dist/config/logging.js +33 -0
- package/dist/config/upstream.js +38 -0
- package/dist/models/anthropicDefaults.js +138 -0
- package/dist/models/discovery.js +208 -0
- package/dist/pi/init.js +174 -0
- package/dist/server/anthropicModelsResponse.js +151 -0
- package/dist/server/codexSchema.js +100 -0
- package/dist/server/debugInfo.js +48 -0
- package/dist/server/lock.js +150 -0
- package/dist/server/proxy.js +715 -0
- package/dist/translation/openaiAnthropic.js +391 -0
- package/dist/translation/streamingOpenAIToAnthropic.js +290 -0
- package/dist/types/index.js +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
|
+
import { getCopillmHome } from "../config/home.js";
|
|
6
|
+
const NPM_PACKAGES = {
|
|
7
|
+
codex: "@openai/codex",
|
|
8
|
+
claude: "@anthropic-ai/claude-code",
|
|
9
|
+
pi: "@earendil-works/pi-coding-agent"
|
|
10
|
+
};
|
|
11
|
+
const BIN_NAMES = {
|
|
12
|
+
codex: "codex",
|
|
13
|
+
claude: "claude",
|
|
14
|
+
pi: "pi"
|
|
15
|
+
};
|
|
16
|
+
export function packageNameFor(agent) {
|
|
17
|
+
return NPM_PACKAGES[agent];
|
|
18
|
+
}
|
|
19
|
+
export function binNameFor(agent) {
|
|
20
|
+
return BIN_NAMES[agent];
|
|
21
|
+
}
|
|
22
|
+
export function parsePinSpec(agent, raw) {
|
|
23
|
+
const trimmed = raw.trim();
|
|
24
|
+
if (trimmed.length === 0) {
|
|
25
|
+
return { packageName: NPM_PACKAGES[agent], version: null };
|
|
26
|
+
}
|
|
27
|
+
// Bare version like "1.4.7" or "^1.0.0"
|
|
28
|
+
if (/^[\d^~><=*]/.test(trimmed)) {
|
|
29
|
+
return { packageName: NPM_PACKAGES[agent], version: trimmed };
|
|
30
|
+
}
|
|
31
|
+
// <pkg>@<version>; tolerate scoped pkgs starting with @
|
|
32
|
+
const isScoped = trimmed.startsWith("@");
|
|
33
|
+
const lastAt = trimmed.lastIndexOf("@");
|
|
34
|
+
if (lastAt > 0 && (!isScoped || lastAt > 0)) {
|
|
35
|
+
const pkg = trimmed.slice(0, lastAt);
|
|
36
|
+
const ver = trimmed.slice(lastAt + 1);
|
|
37
|
+
if (pkg && ver) {
|
|
38
|
+
return { packageName: pkg, version: ver };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { packageName: trimmed, version: null };
|
|
42
|
+
}
|
|
43
|
+
export async function resolveAgent(agent, opts = {}) {
|
|
44
|
+
const cacheRoot = opts.cacheRoot ?? path.join(getCopillmHome(), "bin");
|
|
45
|
+
const npmExe = opts.npmExecutable ?? defaultNpmExecutable();
|
|
46
|
+
const log = opts.log ?? ((line) => process.stderr.write(`${line}\n`));
|
|
47
|
+
const pin = opts.pinnedSpec ? parsePinSpec(agent, opts.pinnedSpec) : { packageName: NPM_PACKAGES[agent], version: null };
|
|
48
|
+
const pkg = pin.packageName;
|
|
49
|
+
const binName = BIN_NAMES[agent];
|
|
50
|
+
const agentRoot = path.join(cacheRoot, agent);
|
|
51
|
+
// 1. PATH lookup (skipped when user pinned a specific version)
|
|
52
|
+
if (!pin.version && opts.preferPath !== false) {
|
|
53
|
+
const found = findOnPath(binName);
|
|
54
|
+
if (found) {
|
|
55
|
+
const v = probeVersion(found) ?? "unknown";
|
|
56
|
+
return {
|
|
57
|
+
source: "path",
|
|
58
|
+
binPath: found,
|
|
59
|
+
version: v,
|
|
60
|
+
packageName: pkg,
|
|
61
|
+
cacheDir: null,
|
|
62
|
+
prunedCount: 0,
|
|
63
|
+
displayLine: `\u2192 ${binName} (system PATH, ${found}${v !== "unknown" ? `, v${v}` : ""})`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 2. Determine target version
|
|
68
|
+
let target = pin.version;
|
|
69
|
+
if (!target && !opts.offline) {
|
|
70
|
+
target = npmViewLatest(npmExe, pkg);
|
|
71
|
+
}
|
|
72
|
+
// 3. Cache lookup
|
|
73
|
+
if (target) {
|
|
74
|
+
const cachedDir = path.join(agentRoot, target);
|
|
75
|
+
const cachedBin = binPathInPrefix(cachedDir, binName);
|
|
76
|
+
if (cachedBin && fs.existsSync(cachedBin)) {
|
|
77
|
+
return {
|
|
78
|
+
source: "cache",
|
|
79
|
+
binPath: cachedBin,
|
|
80
|
+
version: target,
|
|
81
|
+
packageName: pkg,
|
|
82
|
+
cacheDir: cachedDir,
|
|
83
|
+
prunedCount: 0,
|
|
84
|
+
displayLine: `\u2192 ${binName} (cached, ${displayPath(cachedDir)}, v${target})`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const last = pickLastCached(agentRoot, binName);
|
|
90
|
+
if (last) {
|
|
91
|
+
return {
|
|
92
|
+
source: "cache",
|
|
93
|
+
binPath: last.binPath,
|
|
94
|
+
version: last.version,
|
|
95
|
+
packageName: pkg,
|
|
96
|
+
cacheDir: last.dir,
|
|
97
|
+
prunedCount: 0,
|
|
98
|
+
displayLine: `\u2192 ${binName} (cached fallback, ${displayPath(last.dir)}, v${last.version})`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`${binName} not installed and no cache available (offline).`);
|
|
102
|
+
}
|
|
103
|
+
if (opts.offline) {
|
|
104
|
+
throw new Error(`${binName}@${target} not in cache and --offline is set.`);
|
|
105
|
+
}
|
|
106
|
+
// 4. Install
|
|
107
|
+
log(`\u2192 ${binName} (installing ${pkg}@${target} into ${displayPath(agentRoot)} \u2026)`);
|
|
108
|
+
fs.mkdirSync(agentRoot, { recursive: true });
|
|
109
|
+
const lockFile = path.join(agentRoot, ".lock");
|
|
110
|
+
await acquireFileLock(lockFile, 5 * 60 * 1000);
|
|
111
|
+
try {
|
|
112
|
+
// Re-check after acquiring lock — another invocation may have just installed it.
|
|
113
|
+
const finalDir = path.join(agentRoot, target);
|
|
114
|
+
const recheckBin = binPathInPrefix(finalDir, binName);
|
|
115
|
+
if (recheckBin && fs.existsSync(recheckBin)) {
|
|
116
|
+
return {
|
|
117
|
+
source: "cache",
|
|
118
|
+
binPath: recheckBin,
|
|
119
|
+
version: target,
|
|
120
|
+
packageName: pkg,
|
|
121
|
+
cacheDir: finalDir,
|
|
122
|
+
prunedCount: 0,
|
|
123
|
+
displayLine: `\u2192 ${binName} (cached, ${displayPath(finalDir)}, v${target})`
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const stagingDir = path.join(agentRoot, `.staging-${sanitize(target)}-${process.pid}`);
|
|
127
|
+
if (fs.existsSync(stagingDir)) {
|
|
128
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
fs.mkdirSync(stagingDir, { recursive: true });
|
|
131
|
+
const spec = `${pkg}@${target}`;
|
|
132
|
+
const installResult = spawnSync(npmExe, ["install", "--prefix", stagingDir, "--no-audit", "--no-fund", "--omit=dev", spec], {
|
|
133
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
134
|
+
shell: process.platform === "win32"
|
|
135
|
+
});
|
|
136
|
+
if (installResult.status !== 0) {
|
|
137
|
+
const msg = installResult.error ? `: ${installResult.error.message}` : "";
|
|
138
|
+
throw new Error(`npm install ${spec} failed (exit ${installResult.status})${msg}`);
|
|
139
|
+
}
|
|
140
|
+
const stagedBin = binPathInPrefix(stagingDir, binName);
|
|
141
|
+
if (!stagedBin || !fs.existsSync(stagedBin)) {
|
|
142
|
+
throw new Error(`Installed package did not produce a ${binName} bin at ${stagingDir}`);
|
|
143
|
+
}
|
|
144
|
+
if (probeVersion(stagedBin) === null) {
|
|
145
|
+
throw new Error(`Smoke test failed: ${stagedBin} --version did not exit 0`);
|
|
146
|
+
}
|
|
147
|
+
if (fs.existsSync(finalDir)) {
|
|
148
|
+
fs.rmSync(finalDir, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
fs.renameSync(stagingDir, finalDir);
|
|
151
|
+
fs.writeFileSync(path.join(finalDir, "version.txt"), `${target}\n`);
|
|
152
|
+
const pruned = pruneSiblings(agentRoot, target);
|
|
153
|
+
const finalBin = binPathInPrefix(finalDir, binName);
|
|
154
|
+
if (!finalBin) {
|
|
155
|
+
throw new Error(`Final install missing bin at ${finalDir}`);
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
source: "installed",
|
|
159
|
+
binPath: finalBin,
|
|
160
|
+
version: target,
|
|
161
|
+
packageName: pkg,
|
|
162
|
+
cacheDir: finalDir,
|
|
163
|
+
prunedCount: pruned,
|
|
164
|
+
displayLine: `\u2192 ${binName} (installed ${pkg}@${target} \u2192 ${displayPath(finalDir)}${pruned > 0 ? `, pruned ${pruned} older version${pruned === 1 ? "" : "s"}` : ""})`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
releaseFileLock(lockFile);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function defaultNpmExecutable() {
|
|
172
|
+
return process.env.COPILLM_NPM_EXECUTABLE && process.env.COPILLM_NPM_EXECUTABLE.trim().length > 0
|
|
173
|
+
? process.env.COPILLM_NPM_EXECUTABLE
|
|
174
|
+
: "npm";
|
|
175
|
+
}
|
|
176
|
+
function binPathInPrefix(prefix, binName) {
|
|
177
|
+
const candidates = process.platform === "win32"
|
|
178
|
+
? [
|
|
179
|
+
path.join(prefix, "node_modules", ".bin", `${binName}.cmd`),
|
|
180
|
+
path.join(prefix, "node_modules", ".bin", `${binName}.exe`),
|
|
181
|
+
path.join(prefix, "node_modules", ".bin", binName)
|
|
182
|
+
]
|
|
183
|
+
: [path.join(prefix, "node_modules", ".bin", binName)];
|
|
184
|
+
for (const c of candidates) {
|
|
185
|
+
if (fs.existsSync(c))
|
|
186
|
+
return c;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
function findOnPath(name) {
|
|
191
|
+
const PATH = process.env.PATH ?? "";
|
|
192
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
193
|
+
const exts = process.platform === "win32"
|
|
194
|
+
? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.toLowerCase())
|
|
195
|
+
: [""];
|
|
196
|
+
for (const dir of PATH.split(sep)) {
|
|
197
|
+
if (!dir)
|
|
198
|
+
continue;
|
|
199
|
+
for (const ext of exts) {
|
|
200
|
+
const candidate = path.join(dir, `${name}${ext}`);
|
|
201
|
+
if (statIsFile(candidate))
|
|
202
|
+
return candidate;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
function statIsFile(p) {
|
|
208
|
+
try {
|
|
209
|
+
return fs.statSync(p).isFile();
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function probeVersion(binPath) {
|
|
216
|
+
const result = spawnSync(binPath, ["--version"], {
|
|
217
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
218
|
+
timeout: 8_000,
|
|
219
|
+
shell: process.platform === "win32" && /\.(cmd|bat)$/i.test(binPath)
|
|
220
|
+
});
|
|
221
|
+
if (result.status !== 0)
|
|
222
|
+
return null;
|
|
223
|
+
const out = `${result.stdout?.toString() ?? ""}${result.stderr?.toString() ?? ""}`.trim();
|
|
224
|
+
const m = out.match(/(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)/);
|
|
225
|
+
return m ? m[1] : (out.length > 0 ? out.split(/\s+/)[0] : null);
|
|
226
|
+
}
|
|
227
|
+
function npmViewLatest(npmExe, pkg) {
|
|
228
|
+
const result = spawnSync(npmExe, ["view", pkg, "version"], {
|
|
229
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
230
|
+
shell: process.platform === "win32",
|
|
231
|
+
timeout: 30_000
|
|
232
|
+
});
|
|
233
|
+
if (result.status !== 0) {
|
|
234
|
+
const err = result.stderr?.toString() ?? "(no stderr)";
|
|
235
|
+
const errMsg = result.error ? `: ${result.error.message}` : "";
|
|
236
|
+
throw new Error(`Failed to query latest version of ${pkg} via npm view: ${err.trim()}${errMsg}`);
|
|
237
|
+
}
|
|
238
|
+
const v = result.stdout?.toString().trim();
|
|
239
|
+
if (!v)
|
|
240
|
+
throw new Error(`Empty response from \`npm view ${pkg} version\``);
|
|
241
|
+
return v;
|
|
242
|
+
}
|
|
243
|
+
function pickLastCached(agentRoot, binName) {
|
|
244
|
+
if (!fs.existsSync(agentRoot))
|
|
245
|
+
return null;
|
|
246
|
+
const versions = fs
|
|
247
|
+
.readdirSync(agentRoot, { withFileTypes: true })
|
|
248
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
249
|
+
.map((e) => e.name)
|
|
250
|
+
.sort((a, b) => compareVersionsDescending(a, b));
|
|
251
|
+
for (const v of versions) {
|
|
252
|
+
const dir = path.join(agentRoot, v);
|
|
253
|
+
const bin = binPathInPrefix(dir, binName);
|
|
254
|
+
if (bin)
|
|
255
|
+
return { dir, binPath: bin, version: v };
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
function compareVersionsDescending(a, b) {
|
|
260
|
+
const pa = a.split(/[.\-+]/).map((n) => parseInt(n, 10));
|
|
261
|
+
const pb = b.split(/[.\-+]/).map((n) => parseInt(n, 10));
|
|
262
|
+
const len = Math.max(pa.length, pb.length);
|
|
263
|
+
for (let i = 0; i < len; i += 1) {
|
|
264
|
+
const da = Number.isFinite(pa[i]) ? pa[i] : 0;
|
|
265
|
+
const db = Number.isFinite(pb[i]) ? pb[i] : 0;
|
|
266
|
+
if (da !== db)
|
|
267
|
+
return db - da;
|
|
268
|
+
}
|
|
269
|
+
return b.localeCompare(a);
|
|
270
|
+
}
|
|
271
|
+
function pruneSiblings(agentRoot, keepVersion) {
|
|
272
|
+
let pruned = 0;
|
|
273
|
+
const oneHourMs = 60 * 60 * 1000;
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
for (const entry of fs.readdirSync(agentRoot, { withFileTypes: true })) {
|
|
276
|
+
if (!entry.isDirectory())
|
|
277
|
+
continue;
|
|
278
|
+
if (entry.name === keepVersion)
|
|
279
|
+
continue;
|
|
280
|
+
const sub = path.join(agentRoot, entry.name);
|
|
281
|
+
if (entry.name.startsWith(".staging-")) {
|
|
282
|
+
try {
|
|
283
|
+
const mtime = fs.statSync(sub).mtimeMs;
|
|
284
|
+
if (now - mtime > oneHourMs) {
|
|
285
|
+
fs.rmSync(sub, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// best effort
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (entry.name.startsWith("."))
|
|
294
|
+
continue;
|
|
295
|
+
try {
|
|
296
|
+
fs.rmSync(sub, { recursive: true, force: true });
|
|
297
|
+
pruned += 1;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// best effort
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return pruned;
|
|
304
|
+
}
|
|
305
|
+
async function acquireFileLock(file, timeoutMs) {
|
|
306
|
+
const start = Date.now();
|
|
307
|
+
while (true) {
|
|
308
|
+
try {
|
|
309
|
+
const fd = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
|
|
310
|
+
fs.writeSync(fd, String(process.pid));
|
|
311
|
+
fs.closeSync(fd);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
const code = e.code;
|
|
316
|
+
if (code !== "EEXIST")
|
|
317
|
+
throw e;
|
|
318
|
+
try {
|
|
319
|
+
const holder = parseInt(fs.readFileSync(file, "utf8").trim(), 10);
|
|
320
|
+
if (Number.isFinite(holder) && !pidAlive(holder)) {
|
|
321
|
+
fs.unlinkSync(file);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// best effort
|
|
327
|
+
}
|
|
328
|
+
if (Date.now() - start > timeoutMs) {
|
|
329
|
+
throw new Error(`Could not acquire lock at ${file} within ${timeoutMs}ms`);
|
|
330
|
+
}
|
|
331
|
+
await sleep(200);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function releaseFileLock(file) {
|
|
336
|
+
try {
|
|
337
|
+
fs.unlinkSync(file);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// best effort
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function pidAlive(pid) {
|
|
344
|
+
try {
|
|
345
|
+
process.kill(pid, 0);
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function displayPath(p) {
|
|
353
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
354
|
+
if (home && p.startsWith(home)) {
|
|
355
|
+
return p.replace(home, "~");
|
|
356
|
+
}
|
|
357
|
+
return p;
|
|
358
|
+
}
|
|
359
|
+
function sanitize(s) {
|
|
360
|
+
return s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
361
|
+
}
|