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.
Files changed (38) hide show
  1. package/README.md +52 -0
  2. package/dist/agentconfig/apply.js +53 -0
  3. package/dist/agentconfig/load.js +163 -0
  4. package/dist/agentconfig/markerBlock.js +76 -0
  5. package/dist/agentconfig/render.js +317 -0
  6. package/dist/agentconfig/schema.js +65 -0
  7. package/dist/auth/copilotToken.js +122 -0
  8. package/dist/auth/credentials.js +221 -0
  9. package/dist/auth/deviceFlow.js +89 -0
  10. package/dist/auth/ensureAuthenticated.js +55 -0
  11. package/dist/auth/githubIdentity.js +42 -0
  12. package/dist/auth/interactivePrompt.js +135 -0
  13. package/dist/claude/cache.js +20 -0
  14. package/dist/claude/settingsConflict.js +85 -0
  15. package/dist/cli/agentEnv.js +56 -0
  16. package/dist/cli/configCommands.js +149 -0
  17. package/dist/cli/envBlock.js +43 -0
  18. package/dist/cli/launchAgent.js +59 -0
  19. package/dist/cli/resolveAgent.js +361 -0
  20. package/dist/cli.js +1178 -0
  21. package/dist/codex/init.js +93 -0
  22. package/dist/config/config.js +51 -0
  23. package/dist/config/fsSecurity.js +39 -0
  24. package/dist/config/home.js +62 -0
  25. package/dist/config/logging.js +33 -0
  26. package/dist/config/upstream.js +38 -0
  27. package/dist/models/anthropicDefaults.js +138 -0
  28. package/dist/models/discovery.js +208 -0
  29. package/dist/pi/init.js +174 -0
  30. package/dist/server/anthropicModelsResponse.js +151 -0
  31. package/dist/server/codexSchema.js +100 -0
  32. package/dist/server/debugInfo.js +48 -0
  33. package/dist/server/lock.js +150 -0
  34. package/dist/server/proxy.js +715 -0
  35. package/dist/translation/openaiAnthropic.js +391 -0
  36. package/dist/translation/streamingOpenAIToAnthropic.js +290 -0
  37. package/dist/types/index.js +1 -0
  38. 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
+ }