@agentconnect/cli 0.1.7 → 0.2.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/dist/fs-utils.d.ts +1 -1
- package/dist/index.js +56 -4158
- package/dist/manifest.d.ts +1 -1
- package/dist/registry-validate.d.ts +1 -1
- package/dist/registry.d.ts +1 -1
- package/package.json +3 -5
package/dist/index.js
CHANGED
|
@@ -1,4140 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
5
|
-
import { promises as
|
|
6
|
-
import { createPrivateKey, createPublicKey as createPublicKey2, sign as signData } from "crypto";
|
|
7
|
-
|
|
8
|
-
// src/host.ts
|
|
9
|
-
import http from "http";
|
|
10
|
-
import { WebSocketServer } from "ws";
|
|
11
|
-
import { spawn as spawn5 } from "child_process";
|
|
12
|
-
import fs2 from "fs";
|
|
13
|
-
import { promises as fsp } from "fs";
|
|
14
|
-
import net from "net";
|
|
15
|
-
import path6 from "path";
|
|
16
|
-
|
|
17
|
-
// src/providers/claude.ts
|
|
18
|
-
import { spawn as spawn2 } from "child_process";
|
|
19
|
-
import { access, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
20
|
-
import https from "https";
|
|
21
|
-
import os2 from "os";
|
|
22
|
-
import path2 from "path";
|
|
23
|
-
|
|
24
|
-
// src/providers/utils.ts
|
|
25
|
-
import { spawn } from "child_process";
|
|
26
|
-
import { existsSync, realpathSync } from "fs";
|
|
27
|
-
import os from "os";
|
|
28
|
-
import path from "path";
|
|
29
|
-
var DEBUG_ENABLED = Boolean(process.env.AGENTCONNECT_DEBUG?.trim());
|
|
30
|
-
function debugLog(scope, message, details) {
|
|
31
|
-
if (!DEBUG_ENABLED) return;
|
|
32
|
-
let suffix = "";
|
|
33
|
-
if (details) {
|
|
34
|
-
try {
|
|
35
|
-
suffix = ` ${JSON.stringify(details)}`;
|
|
36
|
-
} catch {
|
|
37
|
-
suffix = "";
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
console.log(`[AgentConnect][${scope}] ${message}${suffix}`);
|
|
41
|
-
}
|
|
42
|
-
function splitCommand(value) {
|
|
43
|
-
if (!value) return { command: "", args: [] };
|
|
44
|
-
if (Array.isArray(value)) return { command: value[0] ?? "", args: value.slice(1) };
|
|
45
|
-
const input = String(value).trim();
|
|
46
|
-
const parts = [];
|
|
47
|
-
let current = "";
|
|
48
|
-
let quote = null;
|
|
49
|
-
for (let i = 0; i < input.length; i += 1) {
|
|
50
|
-
const char = input[i];
|
|
51
|
-
if (quote) {
|
|
52
|
-
if (char === quote) {
|
|
53
|
-
quote = null;
|
|
54
|
-
} else {
|
|
55
|
-
current += char;
|
|
56
|
-
}
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (char === '"' || char === "'") {
|
|
60
|
-
quote = char;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (char === " ") {
|
|
64
|
-
if (current) {
|
|
65
|
-
parts.push(current);
|
|
66
|
-
current = "";
|
|
67
|
-
}
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
current += char;
|
|
71
|
-
}
|
|
72
|
-
if (current) parts.push(current);
|
|
73
|
-
return { command: parts[0] ?? "", args: parts.slice(1) };
|
|
74
|
-
}
|
|
75
|
-
function resolveWindowsCommand(command2) {
|
|
76
|
-
if (process.platform !== "win32") return command2;
|
|
77
|
-
if (!command2) return command2;
|
|
78
|
-
if (command2.endsWith(".cmd") || command2.endsWith(".exe") || command2.includes("\\")) {
|
|
79
|
-
return command2;
|
|
80
|
-
}
|
|
81
|
-
return `${command2}.cmd`;
|
|
82
|
-
}
|
|
83
|
-
function getCommonBinPaths() {
|
|
84
|
-
const home = os.homedir();
|
|
85
|
-
const bunInstall = process.env.BUN_INSTALL || path.join(home, ".bun");
|
|
86
|
-
const pnpmHome = process.env.PNPM_HOME || path.join(home, "Library", "pnpm");
|
|
87
|
-
const npmPrefix = process.env.NPM_CONFIG_PREFIX;
|
|
88
|
-
const npmBin = npmPrefix ? path.join(npmPrefix, "bin") : "";
|
|
89
|
-
const claudeLocal = process.env.CLAUDE_CONFIG_DIR ? path.join(process.env.CLAUDE_CONFIG_DIR, "local") : path.join(home, ".claude", "local");
|
|
90
|
-
return [
|
|
91
|
-
path.join(bunInstall, "bin"),
|
|
92
|
-
pnpmHome,
|
|
93
|
-
path.join(home, ".local", "bin"),
|
|
94
|
-
claudeLocal,
|
|
95
|
-
path.join(home, ".claude", "bin"),
|
|
96
|
-
npmBin,
|
|
97
|
-
"/opt/homebrew/bin",
|
|
98
|
-
"/usr/local/bin",
|
|
99
|
-
"/usr/bin",
|
|
100
|
-
"/bin"
|
|
101
|
-
].filter(Boolean);
|
|
102
|
-
}
|
|
103
|
-
function getCommandCandidates(command2) {
|
|
104
|
-
if (process.platform !== "win32") return [command2];
|
|
105
|
-
if (command2.endsWith(".cmd") || command2.endsWith(".exe") || command2.endsWith(".bat")) {
|
|
106
|
-
return [command2];
|
|
107
|
-
}
|
|
108
|
-
return [command2, `${command2}.cmd`, `${command2}.exe`, `${command2}.bat`];
|
|
109
|
-
}
|
|
110
|
-
function resolveCommandPath(command2) {
|
|
111
|
-
if (!command2) return null;
|
|
112
|
-
if (command2.includes("/") || command2.includes("\\")) {
|
|
113
|
-
return existsSync(command2) ? command2 : null;
|
|
114
|
-
}
|
|
115
|
-
const candidates = getCommandCandidates(command2);
|
|
116
|
-
const searchPaths = /* @__PURE__ */ new Set();
|
|
117
|
-
const pathEntries = process.env.PATH ? process.env.PATH.split(path.delimiter) : [];
|
|
118
|
-
for (const entry of pathEntries) {
|
|
119
|
-
if (entry) searchPaths.add(entry);
|
|
120
|
-
}
|
|
121
|
-
for (const entry of getCommonBinPaths()) {
|
|
122
|
-
if (entry) searchPaths.add(entry);
|
|
123
|
-
}
|
|
124
|
-
for (const dir of searchPaths) {
|
|
125
|
-
for (const candidate of candidates) {
|
|
126
|
-
const fullPath = path.join(dir, candidate);
|
|
127
|
-
if (existsSync(fullPath)) return fullPath;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
function resolveCommandRealPath(command2) {
|
|
133
|
-
const resolved = resolveCommandPath(command2);
|
|
134
|
-
if (!resolved) return null;
|
|
135
|
-
try {
|
|
136
|
-
return realpathSync(resolved);
|
|
137
|
-
} catch {
|
|
138
|
-
return resolved;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
function commandExists(command2) {
|
|
142
|
-
return Boolean(resolveCommandPath(command2));
|
|
143
|
-
}
|
|
144
|
-
function runCommand(command2, args2, options = {}) {
|
|
145
|
-
return new Promise((resolve) => {
|
|
146
|
-
const { input, timeoutMs, ...spawnOptions } = options;
|
|
147
|
-
if (!command2) {
|
|
148
|
-
resolve({ code: -1, stdout: "", stderr: "Command is empty" });
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const resolved = resolveCommandPath(command2);
|
|
152
|
-
if (!resolved) {
|
|
153
|
-
resolve({ code: 127, stdout: "", stderr: `Executable not found in PATH: "${command2}"` });
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const startedAt = Date.now();
|
|
157
|
-
debugLog("Command", "run", {
|
|
158
|
-
command: resolved,
|
|
159
|
-
args: args2,
|
|
160
|
-
startedAt: new Date(startedAt).toISOString()
|
|
161
|
-
});
|
|
162
|
-
const child = spawn(resolved, args2, {
|
|
163
|
-
...spawnOptions,
|
|
164
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
165
|
-
});
|
|
166
|
-
let stdout = "";
|
|
167
|
-
let stderr = "";
|
|
168
|
-
let timeout;
|
|
169
|
-
if (typeof timeoutMs === "number" && timeoutMs > 0) {
|
|
170
|
-
timeout = setTimeout(() => {
|
|
171
|
-
child.kill();
|
|
172
|
-
resolve({ code: -1, stdout, stderr: `${stderr}Command timed out` });
|
|
173
|
-
}, timeoutMs);
|
|
174
|
-
}
|
|
175
|
-
if (input) {
|
|
176
|
-
child.stdin?.write(input);
|
|
177
|
-
}
|
|
178
|
-
child.stdin?.end();
|
|
179
|
-
child.stdout?.on("data", (chunk) => {
|
|
180
|
-
stdout += chunk.toString("utf8");
|
|
181
|
-
});
|
|
182
|
-
child.stderr?.on("data", (chunk) => {
|
|
183
|
-
stderr += chunk.toString("utf8");
|
|
184
|
-
});
|
|
185
|
-
child.on("error", (err) => {
|
|
186
|
-
if (timeout) clearTimeout(timeout);
|
|
187
|
-
debugLog("Command", "result", {
|
|
188
|
-
command: resolved,
|
|
189
|
-
code: -1,
|
|
190
|
-
durationMs: Date.now() - startedAt,
|
|
191
|
-
error: err.message
|
|
192
|
-
});
|
|
193
|
-
resolve({ code: -1, stdout, stderr: `${stderr}${err.message}` });
|
|
194
|
-
});
|
|
195
|
-
child.on("close", (code) => {
|
|
196
|
-
if (timeout) clearTimeout(timeout);
|
|
197
|
-
debugLog("Command", "result", {
|
|
198
|
-
command: resolved,
|
|
199
|
-
code: code ?? 0,
|
|
200
|
-
durationMs: Date.now() - startedAt
|
|
201
|
-
});
|
|
202
|
-
resolve({ code: code ?? 0, stdout, stderr });
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
function createLineParser(onLine) {
|
|
207
|
-
let buffer = "";
|
|
208
|
-
return (chunk) => {
|
|
209
|
-
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
210
|
-
buffer += text;
|
|
211
|
-
let idx;
|
|
212
|
-
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
213
|
-
const line = buffer.slice(0, idx).trim();
|
|
214
|
-
buffer = buffer.slice(idx + 1);
|
|
215
|
-
if (line) onLine(line);
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
async function checkCommandVersion(command2, argsList) {
|
|
220
|
-
for (const args2 of argsList) {
|
|
221
|
-
const result = await runCommand(command2, args2);
|
|
222
|
-
if (result.code === 0) {
|
|
223
|
-
const version = result.stdout.trim().split("\n")[0] ?? "";
|
|
224
|
-
return { ok: true, version };
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return { ok: false, version: "" };
|
|
228
|
-
}
|
|
229
|
-
var packageManagerCache = { detected: null };
|
|
230
|
-
async function isCommandAvailable(command2) {
|
|
231
|
-
const result = await runCommand(command2, ["--version"]);
|
|
232
|
-
return result.code === 0;
|
|
233
|
-
}
|
|
234
|
-
async function detectPackageManager() {
|
|
235
|
-
if (packageManagerCache.detected) {
|
|
236
|
-
return packageManagerCache.detected;
|
|
237
|
-
}
|
|
238
|
-
if (await isCommandAvailable("bun")) {
|
|
239
|
-
packageManagerCache.detected = "bun";
|
|
240
|
-
return "bun";
|
|
241
|
-
}
|
|
242
|
-
if (await isCommandAvailable("pnpm")) {
|
|
243
|
-
packageManagerCache.detected = "pnpm";
|
|
244
|
-
return "pnpm";
|
|
245
|
-
}
|
|
246
|
-
if (await isCommandAvailable("npm")) {
|
|
247
|
-
packageManagerCache.detected = "npm";
|
|
248
|
-
return "npm";
|
|
249
|
-
}
|
|
250
|
-
if (process.platform === "darwin" && await isCommandAvailable("brew")) {
|
|
251
|
-
packageManagerCache.detected = "brew";
|
|
252
|
-
return "brew";
|
|
253
|
-
}
|
|
254
|
-
packageManagerCache.detected = "unknown";
|
|
255
|
-
return "unknown";
|
|
256
|
-
}
|
|
257
|
-
function getInstallCommand(packageManager, packageName) {
|
|
258
|
-
switch (packageManager) {
|
|
259
|
-
case "bun":
|
|
260
|
-
return { command: "bun", args: ["add", "-g", packageName] };
|
|
261
|
-
case "pnpm":
|
|
262
|
-
return { command: "pnpm", args: ["add", "-g", packageName] };
|
|
263
|
-
case "npm":
|
|
264
|
-
return { command: "npm", args: ["install", "-g", packageName] };
|
|
265
|
-
case "brew":
|
|
266
|
-
return { command: "brew", args: ["install", packageName] };
|
|
267
|
-
default:
|
|
268
|
-
return { command: "", args: [] };
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
async function buildInstallCommandAuto(packageName) {
|
|
272
|
-
const pm = await detectPackageManager();
|
|
273
|
-
const cmd = getInstallCommand(pm, packageName);
|
|
274
|
-
return { ...cmd, packageManager: pm };
|
|
275
|
-
}
|
|
276
|
-
function buildInstallCommand(envVar, fallback) {
|
|
277
|
-
const value = process.env[envVar] || fallback;
|
|
278
|
-
return splitCommand(value);
|
|
279
|
-
}
|
|
280
|
-
function buildLoginCommand(envVar, fallback) {
|
|
281
|
-
const value = process.env[envVar] || fallback;
|
|
282
|
-
return splitCommand(value);
|
|
283
|
-
}
|
|
284
|
-
function buildStatusCommand(envVar, fallback) {
|
|
285
|
-
const value = process.env[envVar] || fallback;
|
|
286
|
-
return splitCommand(value);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// src/providers/claude.ts
|
|
290
|
-
var CLAUDE_PACKAGE = "@anthropic-ai/claude-code";
|
|
291
|
-
var INSTALL_UNIX = "curl -fsSL https://claude.ai/install.sh | bash";
|
|
292
|
-
var INSTALL_WINDOWS_PS = "irm https://claude.ai/install.ps1 | iex";
|
|
293
|
-
var INSTALL_WINDOWS_CMD = "curl -fsSL https://claude.ai/install.cmd -o install.cmd && install.cmd && del install.cmd";
|
|
294
|
-
var DEFAULT_LOGIN = "";
|
|
295
|
-
var DEFAULT_STATUS = "";
|
|
296
|
-
var CLAUDE_MODELS_CACHE_TTL_MS = 6e4;
|
|
297
|
-
var CLAUDE_RECENT_MODELS_CACHE_TTL_MS = 6e4;
|
|
298
|
-
var CLAUDE_UPDATE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
299
|
-
var claudeModelsCache = null;
|
|
300
|
-
var claudeModelsCacheAt = 0;
|
|
301
|
-
var claudeRecentModelsCache = null;
|
|
302
|
-
var claudeRecentModelsCacheAt = 0;
|
|
303
|
-
var claudeUpdateCache = null;
|
|
304
|
-
var claudeUpdatePromise = null;
|
|
305
|
-
var CLAUDE_LOGIN_CACHE_TTL_MS = 3e4;
|
|
306
|
-
var claudeLoginCache = null;
|
|
307
|
-
var claudeLoginPromise = null;
|
|
308
|
-
function trimOutput(value, limit = 400) {
|
|
309
|
-
const cleaned = value.trim();
|
|
310
|
-
if (!cleaned) return "";
|
|
311
|
-
if (cleaned.length <= limit) return cleaned;
|
|
312
|
-
return `${cleaned.slice(0, limit)}...`;
|
|
313
|
-
}
|
|
314
|
-
function normalizePath(value) {
|
|
315
|
-
const normalized = value.replace(/\\/g, "/");
|
|
316
|
-
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
317
|
-
}
|
|
318
|
-
function parseSemver(value) {
|
|
319
|
-
if (!value) return null;
|
|
320
|
-
const match = value.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
321
|
-
if (!match) return null;
|
|
322
|
-
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
323
|
-
}
|
|
324
|
-
function compareSemver(a, b) {
|
|
325
|
-
if (a[0] !== b[0]) return a[0] - b[0];
|
|
326
|
-
if (a[1] !== b[1]) return a[1] - b[1];
|
|
327
|
-
return a[2] - b[2];
|
|
328
|
-
}
|
|
329
|
-
async function fetchLatestNpmVersion(pkg) {
|
|
330
|
-
const encoded = encodeURIComponent(pkg);
|
|
331
|
-
const data = await fetchJson(`https://registry.npmjs.org/${encoded}`);
|
|
332
|
-
if (!data || typeof data !== "object") return null;
|
|
333
|
-
const latest = data["dist-tags"]?.latest;
|
|
334
|
-
return typeof latest === "string" ? latest : null;
|
|
335
|
-
}
|
|
336
|
-
async function fetchBrewCaskVersion(cask) {
|
|
337
|
-
if (!commandExists("brew")) return null;
|
|
338
|
-
const result = await runCommand("brew", ["info", "--json=v2", "--cask", cask]);
|
|
339
|
-
if (result.code !== 0) return null;
|
|
340
|
-
try {
|
|
341
|
-
const parsed = JSON.parse(result.stdout);
|
|
342
|
-
const version = parsed?.casks?.[0]?.version;
|
|
343
|
-
return typeof version === "string" ? version : null;
|
|
344
|
-
} catch {
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
function getClaudeUpdateAction(commandPath) {
|
|
349
|
-
if (!commandPath) return null;
|
|
350
|
-
const normalized = normalizePath(commandPath);
|
|
351
|
-
const home = normalizePath(os2.homedir());
|
|
352
|
-
if (normalized.startsWith(`${home}/.bun/bin/`)) {
|
|
353
|
-
return {
|
|
354
|
-
command: "bun",
|
|
355
|
-
args: ["install", "-g", CLAUDE_PACKAGE],
|
|
356
|
-
source: "bun",
|
|
357
|
-
commandLabel: "bun install -g @anthropic-ai/claude-code"
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
if (normalized.includes("/node_modules/.bin/")) {
|
|
361
|
-
return {
|
|
362
|
-
command: "npm",
|
|
363
|
-
args: ["install", "-g", CLAUDE_PACKAGE],
|
|
364
|
-
source: "npm",
|
|
365
|
-
commandLabel: "npm install -g @anthropic-ai/claude-code"
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
if (normalized.includes("/cellar/") || normalized.includes("/caskroom/") || normalized.includes("/homebrew/")) {
|
|
369
|
-
return {
|
|
370
|
-
command: "brew",
|
|
371
|
-
args: ["upgrade", "--cask", "claude-code"],
|
|
372
|
-
source: "brew",
|
|
373
|
-
commandLabel: "brew upgrade --cask claude-code"
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
if (process.platform === "win32" && (normalized.includes("/program files/claudecode") || normalized.includes("/programdata/claudecode"))) {
|
|
377
|
-
return {
|
|
378
|
-
command: "winget",
|
|
379
|
-
args: ["upgrade", "Anthropic.ClaudeCode"],
|
|
380
|
-
source: "winget",
|
|
381
|
-
commandLabel: "winget upgrade Anthropic.ClaudeCode"
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
if (normalized.includes("/.local/bin/") || normalized.includes("/.local/share/claude/versions/") || normalized.includes("/.local/share/claude-code/versions/")) {
|
|
385
|
-
if (process.platform === "win32") {
|
|
386
|
-
return {
|
|
387
|
-
command: "powershell",
|
|
388
|
-
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", INSTALL_WINDOWS_PS],
|
|
389
|
-
source: "script",
|
|
390
|
-
commandLabel: INSTALL_WINDOWS_PS
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
return {
|
|
394
|
-
command: "bash",
|
|
395
|
-
args: ["-lc", INSTALL_UNIX],
|
|
396
|
-
source: "script",
|
|
397
|
-
commandLabel: INSTALL_UNIX
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
402
|
-
var DEFAULT_CLAUDE_MODELS = [
|
|
403
|
-
{
|
|
404
|
-
id: "default",
|
|
405
|
-
displayName: "Default \xB7 Opus 4.5"
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
id: "sonnet",
|
|
409
|
-
displayName: "Sonnet 4.5"
|
|
410
|
-
},
|
|
411
|
-
{
|
|
412
|
-
id: "haiku",
|
|
413
|
-
displayName: "Haiku 4.5"
|
|
414
|
-
},
|
|
415
|
-
{
|
|
416
|
-
id: "opus",
|
|
417
|
-
displayName: "Opus"
|
|
418
|
-
}
|
|
419
|
-
];
|
|
420
|
-
function getClaudeCommand() {
|
|
421
|
-
const override = process.env.AGENTCONNECT_CLAUDE_COMMAND;
|
|
422
|
-
const base = override || "claude";
|
|
423
|
-
const resolved = resolveCommandPath(base);
|
|
424
|
-
return resolved || resolveWindowsCommand(base);
|
|
425
|
-
}
|
|
426
|
-
function getClaudeConfigDir() {
|
|
427
|
-
return process.env.CLAUDE_CONFIG_DIR || path2.join(os2.homedir(), ".claude");
|
|
428
|
-
}
|
|
429
|
-
function fetchJson(url) {
|
|
430
|
-
return new Promise((resolve) => {
|
|
431
|
-
https.get(url, (res) => {
|
|
432
|
-
let data = "";
|
|
433
|
-
res.on("data", (chunk) => {
|
|
434
|
-
data += chunk;
|
|
435
|
-
});
|
|
436
|
-
res.on("end", () => {
|
|
437
|
-
try {
|
|
438
|
-
resolve(JSON.parse(data));
|
|
439
|
-
} catch {
|
|
440
|
-
resolve(null);
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
}).on("error", () => resolve(null));
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
function formatClaudeDisplayName(modelId) {
|
|
447
|
-
const value = modelId.trim();
|
|
448
|
-
if (!value.startsWith("claude-")) return value;
|
|
449
|
-
const parts = value.replace(/^claude-/, "").split("-").filter(Boolean);
|
|
450
|
-
if (!parts.length) return value;
|
|
451
|
-
const family = parts[0];
|
|
452
|
-
const numeric = parts.slice(1).filter((entry) => /^\d+$/.test(entry));
|
|
453
|
-
let version = "";
|
|
454
|
-
if (numeric.length >= 2) {
|
|
455
|
-
version = `${numeric[0]}.${numeric[1]}`;
|
|
456
|
-
} else if (numeric.length === 1) {
|
|
457
|
-
version = numeric[0];
|
|
458
|
-
}
|
|
459
|
-
const familyLabel = family.charAt(0).toUpperCase() + family.slice(1);
|
|
460
|
-
return `Claude ${familyLabel}${version ? ` ${version}` : ""}`;
|
|
461
|
-
}
|
|
462
|
-
async function readClaudeStatsModels() {
|
|
463
|
-
const statsPath = path2.join(getClaudeConfigDir(), "stats-cache.json");
|
|
464
|
-
try {
|
|
465
|
-
const raw = await readFile(statsPath, "utf8");
|
|
466
|
-
const parsed = JSON.parse(raw);
|
|
467
|
-
const usage = parsed?.modelUsage;
|
|
468
|
-
if (!usage || typeof usage !== "object") return [];
|
|
469
|
-
return Object.keys(usage).filter(Boolean);
|
|
470
|
-
} catch {
|
|
471
|
-
return [];
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
async function listClaudeModels() {
|
|
475
|
-
if (claudeModelsCache && Date.now() - claudeModelsCacheAt < CLAUDE_MODELS_CACHE_TTL_MS) {
|
|
476
|
-
return claudeModelsCache;
|
|
477
|
-
}
|
|
478
|
-
const list = DEFAULT_CLAUDE_MODELS.map((entry) => ({
|
|
479
|
-
id: entry.id,
|
|
480
|
-
provider: "claude",
|
|
481
|
-
displayName: entry.displayName
|
|
482
|
-
}));
|
|
483
|
-
claudeModelsCache = list;
|
|
484
|
-
claudeModelsCacheAt = Date.now();
|
|
485
|
-
return list;
|
|
486
|
-
}
|
|
487
|
-
async function listClaudeRecentModels() {
|
|
488
|
-
if (claudeRecentModelsCache && Date.now() - claudeRecentModelsCacheAt < CLAUDE_RECENT_MODELS_CACHE_TTL_MS) {
|
|
489
|
-
return claudeRecentModelsCache;
|
|
490
|
-
}
|
|
491
|
-
const discovered = await readClaudeStatsModels();
|
|
492
|
-
const mapped = [];
|
|
493
|
-
const seen = /* @__PURE__ */ new Set();
|
|
494
|
-
for (const modelId of discovered) {
|
|
495
|
-
const id = modelId.trim();
|
|
496
|
-
if (!id || seen.has(id)) continue;
|
|
497
|
-
seen.add(id);
|
|
498
|
-
mapped.push({
|
|
499
|
-
id,
|
|
500
|
-
provider: "claude",
|
|
501
|
-
displayName: formatClaudeDisplayName(id)
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
claudeRecentModelsCache = mapped;
|
|
505
|
-
claudeRecentModelsCacheAt = Date.now();
|
|
506
|
-
return mapped;
|
|
507
|
-
}
|
|
508
|
-
function getClaudeAuthPaths() {
|
|
509
|
-
const home = os2.homedir();
|
|
510
|
-
return [
|
|
511
|
-
path2.join(home, ".claude.json"),
|
|
512
|
-
path2.join(home, ".claude", "settings.json"),
|
|
513
|
-
path2.join(home, ".config", "claude", "auth.json")
|
|
514
|
-
];
|
|
515
|
-
}
|
|
516
|
-
function resolveClaudeTheme() {
|
|
517
|
-
const raw = process.env.AGENTCONNECT_CLAUDE_THEME;
|
|
518
|
-
return raw && raw.trim() ? raw.trim() : "dark";
|
|
519
|
-
}
|
|
520
|
-
function resolveClaudeLoginMethod(options) {
|
|
521
|
-
const raw = options?.loginMethod ?? process.env.AGENTCONNECT_CLAUDE_LOGIN_METHOD;
|
|
522
|
-
if (!raw) return "claudeai";
|
|
523
|
-
const normalized = String(raw).trim().toLowerCase();
|
|
524
|
-
if (normalized === "console") return "console";
|
|
525
|
-
if (normalized === "claudeai" || normalized === "claude") return "claudeai";
|
|
526
|
-
return "claudeai";
|
|
527
|
-
}
|
|
528
|
-
function resolveClaudeLoginExperience(options) {
|
|
529
|
-
const raw = options?.loginExperience ?? process.env.AGENTCONNECT_CLAUDE_LOGIN_EXPERIENCE ?? process.env.AGENTCONNECT_LOGIN_EXPERIENCE;
|
|
530
|
-
if (raw) {
|
|
531
|
-
const normalized = String(raw).trim().toLowerCase();
|
|
532
|
-
if (normalized === "terminal" || normalized === "manual") return "terminal";
|
|
533
|
-
if (normalized === "embedded" || normalized === "pty") return "embedded";
|
|
534
|
-
}
|
|
535
|
-
if (process.env.AGENTCONNECT_HOST_MODE === "dev") return "terminal";
|
|
536
|
-
return "embedded";
|
|
537
|
-
}
|
|
538
|
-
async function resolveClaudeLoginHint(options) {
|
|
539
|
-
const raw = process.env.AGENTCONNECT_CLAUDE_LOGIN_HINT;
|
|
540
|
-
if (raw) {
|
|
541
|
-
const normalized = String(raw).trim().toLowerCase();
|
|
542
|
-
if (normalized === "setup") return "setup";
|
|
543
|
-
if (normalized === "login") return "login";
|
|
544
|
-
}
|
|
545
|
-
if (options?.loginExperience) {
|
|
546
|
-
}
|
|
547
|
-
const status = await checkClaudeCliStatus();
|
|
548
|
-
return status.loginHint ?? "login";
|
|
549
|
-
}
|
|
550
|
-
async function createClaudeLoginSettingsFile(loginMethod) {
|
|
551
|
-
if (!loginMethod) return null;
|
|
552
|
-
const fileName = `agentconnect-claude-login-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`;
|
|
553
|
-
const filePath = path2.join(os2.tmpdir(), fileName);
|
|
554
|
-
const theme = resolveClaudeTheme();
|
|
555
|
-
const payload = {
|
|
556
|
-
forceLoginMethod: loginMethod,
|
|
557
|
-
theme,
|
|
558
|
-
hasCompletedOnboarding: true
|
|
559
|
-
};
|
|
560
|
-
await writeFile(filePath, JSON.stringify(payload), "utf8");
|
|
561
|
-
return filePath;
|
|
562
|
-
}
|
|
563
|
-
async function ensureClaudeOnboardingSettings() {
|
|
564
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR || path2.join(os2.homedir(), ".claude");
|
|
565
|
-
const settingsPath = path2.join(configDir, "settings.json");
|
|
566
|
-
let settings = {};
|
|
567
|
-
try {
|
|
568
|
-
const raw = await readFile(settingsPath, "utf8");
|
|
569
|
-
try {
|
|
570
|
-
settings = JSON.parse(raw);
|
|
571
|
-
} catch {
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
} catch (err) {
|
|
575
|
-
const code = err?.code;
|
|
576
|
-
if (code && code !== "ENOENT") return;
|
|
577
|
-
}
|
|
578
|
-
let updated = false;
|
|
579
|
-
if (!settings.theme) {
|
|
580
|
-
settings.theme = resolveClaudeTheme();
|
|
581
|
-
updated = true;
|
|
582
|
-
}
|
|
583
|
-
if (settings.hasCompletedOnboarding !== true) {
|
|
584
|
-
settings.hasCompletedOnboarding = true;
|
|
585
|
-
updated = true;
|
|
586
|
-
}
|
|
587
|
-
if (!updated) return;
|
|
588
|
-
await mkdir(configDir, { recursive: true });
|
|
589
|
-
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
590
|
-
`, "utf8");
|
|
591
|
-
}
|
|
592
|
-
async function loadPtyModule() {
|
|
593
|
-
try {
|
|
594
|
-
const mod = await import("node-pty");
|
|
595
|
-
if (typeof mod.spawn === "function") return mod;
|
|
596
|
-
if (mod.default && typeof mod.default.spawn === "function") return mod.default;
|
|
597
|
-
return null;
|
|
598
|
-
} catch {
|
|
599
|
-
return null;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
function shellEscape(value) {
|
|
603
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
604
|
-
}
|
|
605
|
-
function cmdEscape(value) {
|
|
606
|
-
if (!value) return '""';
|
|
607
|
-
const escaped = value.replace(/"/g, '""');
|
|
608
|
-
return `"${escaped}"`;
|
|
609
|
-
}
|
|
610
|
-
function buildClaudeCommand(command2, args2, includeLogin = false) {
|
|
611
|
-
const parts = includeLogin ? [command2, ...args2, "/login"] : [command2, ...args2];
|
|
612
|
-
return parts.map(shellEscape).join(" ");
|
|
613
|
-
}
|
|
614
|
-
function buildClaudeCmd(command2, args2, includeLogin = false) {
|
|
615
|
-
const parts = includeLogin ? [command2, ...args2, "/login"] : [command2, ...args2];
|
|
616
|
-
return parts.map(cmdEscape).join(" ");
|
|
617
|
-
}
|
|
618
|
-
async function openClaudeLoginTerminal(command2, args2, includeLogin = false) {
|
|
619
|
-
const message = "AgentConnect: complete Claude login in this terminal. If login does not start automatically, run /login.";
|
|
620
|
-
if (process.platform === "win32") {
|
|
621
|
-
const cmdLine = `echo ${message} & ${buildClaudeCmd(command2, args2, includeLogin)}`;
|
|
622
|
-
await runCommand("cmd", ["/c", "start", "", "cmd", "/k", cmdLine], { shell: true });
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
const loginCommand = buildClaudeCommand(command2, args2, includeLogin);
|
|
626
|
-
const shellCommand = `printf "%s\\n\\n" ${shellEscape(message)}; ${loginCommand}`;
|
|
627
|
-
if (process.platform === "darwin") {
|
|
628
|
-
const script = `tell application "Terminal"
|
|
629
|
-
activate
|
|
630
|
-
do script "${shellCommand.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"
|
|
631
|
-
end tell`;
|
|
632
|
-
await runCommand("osascript", ["-e", script]);
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
if (commandExists("x-terminal-emulator")) {
|
|
636
|
-
await runCommand("x-terminal-emulator", ["-e", "bash", "-lc", shellCommand]);
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
if (commandExists("gnome-terminal")) {
|
|
640
|
-
await runCommand("gnome-terminal", ["--", "bash", "-lc", shellCommand]);
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
if (commandExists("konsole")) {
|
|
644
|
-
await runCommand("konsole", ["-e", "bash", "-lc", shellCommand]);
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
if (commandExists("xterm")) {
|
|
648
|
-
await runCommand("xterm", ["-e", "bash", "-lc", shellCommand]);
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
throw new Error("No terminal emulator found to launch Claude login.");
|
|
652
|
-
}
|
|
653
|
-
function maybeAdvanceClaudeOnboarding(output, loginMethod, write) {
|
|
654
|
-
const text = output.toLowerCase();
|
|
655
|
-
if (text.includes("select login method") || text.includes("claude account with subscription")) {
|
|
656
|
-
if (loginMethod === "console") {
|
|
657
|
-
write("\x1B[B");
|
|
658
|
-
}
|
|
659
|
-
write("\r");
|
|
660
|
-
return true;
|
|
661
|
-
}
|
|
662
|
-
if (text.includes("choose the text style") || text.includes("text style that looks best")) {
|
|
663
|
-
write("\r");
|
|
664
|
-
return true;
|
|
665
|
-
}
|
|
666
|
-
if (text.includes("press enter") || text.includes("enter to confirm")) {
|
|
667
|
-
write("\r");
|
|
668
|
-
return true;
|
|
669
|
-
}
|
|
670
|
-
return false;
|
|
671
|
-
}
|
|
672
|
-
async function hasClaudeAuth() {
|
|
673
|
-
if (typeof process.env.CLAUDE_CODE_OAUTH_TOKEN === "string") {
|
|
674
|
-
return process.env.CLAUDE_CODE_OAUTH_TOKEN.trim().length > 0;
|
|
675
|
-
}
|
|
676
|
-
for (const filePath of getClaudeAuthPaths()) {
|
|
677
|
-
try {
|
|
678
|
-
await access(filePath);
|
|
679
|
-
const raw = await readFile(filePath, "utf8");
|
|
680
|
-
const parsed = JSON.parse(raw);
|
|
681
|
-
const auth = parsed?.claudeAiOauth;
|
|
682
|
-
if (typeof auth?.accessToken === "string" && auth.accessToken.trim()) {
|
|
683
|
-
return true;
|
|
684
|
-
}
|
|
685
|
-
if (typeof parsed.primaryApiKey === "string" && parsed.primaryApiKey.trim()) {
|
|
686
|
-
return true;
|
|
687
|
-
}
|
|
688
|
-
if (typeof parsed.accessToken === "string" && parsed.accessToken.trim()) {
|
|
689
|
-
return true;
|
|
690
|
-
}
|
|
691
|
-
if (typeof parsed.token === "string" && parsed.token.trim()) {
|
|
692
|
-
return true;
|
|
693
|
-
}
|
|
694
|
-
const oauthAccount = parsed.oauthAccount;
|
|
695
|
-
if (typeof oauthAccount?.emailAddress === "string" && oauthAccount.emailAddress.trim()) {
|
|
696
|
-
return true;
|
|
697
|
-
}
|
|
698
|
-
if (typeof oauthAccount?.accountUuid === "string" && oauthAccount.accountUuid.trim()) {
|
|
699
|
-
return true;
|
|
700
|
-
}
|
|
701
|
-
const oauth = parsed.oauth;
|
|
702
|
-
if (typeof oauth?.access_token === "string" && oauth.access_token.trim()) {
|
|
703
|
-
return true;
|
|
704
|
-
}
|
|
705
|
-
} catch {
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
return false;
|
|
709
|
-
}
|
|
710
|
-
function isClaudeAuthErrorText(value) {
|
|
711
|
-
const text = value.toLowerCase();
|
|
712
|
-
return text.includes("authentication_error") || text.includes("authentication_failed") || text.includes("oauth token has expired") || text.includes("token has expired") || text.includes("please run /login") || text.includes("unauthorized") || text.includes("api error: 401") || text.includes("status 401") || text.includes("invalid api key");
|
|
713
|
-
}
|
|
714
|
-
function extractClaudeMessageText(content) {
|
|
715
|
-
if (Array.isArray(content)) {
|
|
716
|
-
return content.map((part) => part?.text ?? "").join(" ");
|
|
717
|
-
}
|
|
718
|
-
return typeof content === "string" ? content : "";
|
|
719
|
-
}
|
|
720
|
-
function resolveClaudeLoginHintFromSource(apiKeySource) {
|
|
721
|
-
if (apiKeySource && apiKeySource.toLowerCase() === "none") return "setup";
|
|
722
|
-
return "login";
|
|
723
|
-
}
|
|
724
|
-
async function checkClaudeCliStatus() {
|
|
725
|
-
if (claudeLoginCache && Date.now() - claudeLoginCache.checkedAt < CLAUDE_LOGIN_CACHE_TTL_MS) {
|
|
726
|
-
return claudeLoginCache.status;
|
|
727
|
-
}
|
|
728
|
-
if (claudeLoginPromise) return claudeLoginPromise;
|
|
729
|
-
claudeLoginPromise = (async () => {
|
|
730
|
-
const command2 = resolveWindowsCommand(getClaudeCommand());
|
|
731
|
-
const args2 = [
|
|
732
|
-
"--print",
|
|
733
|
-
"--output-format",
|
|
734
|
-
"stream-json",
|
|
735
|
-
"--no-session-persistence",
|
|
736
|
-
"--max-budget-usd",
|
|
737
|
-
"0.01"
|
|
738
|
-
];
|
|
739
|
-
const result = await runCommand(command2, args2, {
|
|
740
|
-
env: { ...process.env, CI: "1" },
|
|
741
|
-
input: "ping\n",
|
|
742
|
-
timeoutMs: 8e3
|
|
743
|
-
});
|
|
744
|
-
const output = `${result.stdout}
|
|
745
|
-
${result.stderr}`.trim();
|
|
746
|
-
if (!output) return { loggedIn: null };
|
|
747
|
-
let apiKeySource;
|
|
748
|
-
let authError = null;
|
|
749
|
-
let sawAssistant = false;
|
|
750
|
-
let sawSuccess = false;
|
|
751
|
-
const lines = output.split("\n");
|
|
752
|
-
for (const line of lines) {
|
|
753
|
-
const parsed = safeJsonParse(line);
|
|
754
|
-
if (!parsed || typeof parsed !== "object") continue;
|
|
755
|
-
const record = parsed;
|
|
756
|
-
const type = typeof record.type === "string" ? record.type : "";
|
|
757
|
-
if (type === "system" && record.subtype === "init") {
|
|
758
|
-
const source = typeof record.apiKeySource === "string" ? record.apiKeySource : void 0;
|
|
759
|
-
if (source) apiKeySource = source;
|
|
760
|
-
}
|
|
761
|
-
if (type === "result") {
|
|
762
|
-
const isError = Boolean(record.is_error);
|
|
763
|
-
const resultText = typeof record.result === "string" ? record.result : typeof record.error === "string" ? record.error : JSON.stringify(record.error || record);
|
|
764
|
-
if (isError && isClaudeAuthErrorText(resultText)) {
|
|
765
|
-
authError = authError ?? resultText;
|
|
766
|
-
}
|
|
767
|
-
if (!isError) sawSuccess = true;
|
|
768
|
-
}
|
|
769
|
-
if (type === "message") {
|
|
770
|
-
const message = record.message;
|
|
771
|
-
const role = typeof message?.role === "string" ? message.role : "";
|
|
772
|
-
if (role === "assistant") {
|
|
773
|
-
const text = extractClaudeMessageText(message?.content);
|
|
774
|
-
const errorText = typeof record.error === "string" ? record.error : typeof message?.error === "string" ? message.error : "";
|
|
775
|
-
if (isClaudeAuthErrorText(text) || errorText && isClaudeAuthErrorText(errorText)) {
|
|
776
|
-
authError = authError ?? (text || errorText);
|
|
777
|
-
} else if (text.trim()) {
|
|
778
|
-
sawAssistant = true;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
if (authError) {
|
|
784
|
-
return {
|
|
785
|
-
loggedIn: false,
|
|
786
|
-
apiKeySource,
|
|
787
|
-
loginHint: resolveClaudeLoginHintFromSource(apiKeySource),
|
|
788
|
-
authError
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
if (sawAssistant || sawSuccess) {
|
|
792
|
-
return { loggedIn: true, apiKeySource };
|
|
793
|
-
}
|
|
794
|
-
if (apiKeySource && apiKeySource.toLowerCase() === "none") {
|
|
795
|
-
return { loggedIn: false, apiKeySource, loginHint: "setup" };
|
|
796
|
-
}
|
|
797
|
-
return { loggedIn: null, apiKeySource };
|
|
798
|
-
})().then((status) => {
|
|
799
|
-
claudeLoginCache = { checkedAt: Date.now(), status };
|
|
800
|
-
return status;
|
|
801
|
-
}).finally(() => {
|
|
802
|
-
claudeLoginPromise = null;
|
|
803
|
-
});
|
|
804
|
-
return claudeLoginPromise;
|
|
805
|
-
}
|
|
806
|
-
function getClaudeUpdateSnapshot(commandPath) {
|
|
807
|
-
if (claudeUpdateCache && Date.now() - claudeUpdateCache.checkedAt < CLAUDE_UPDATE_CACHE_TTL_MS) {
|
|
808
|
-
const action = getClaudeUpdateAction(commandPath);
|
|
809
|
-
return {
|
|
810
|
-
updateAvailable: claudeUpdateCache.updateAvailable,
|
|
811
|
-
latestVersion: claudeUpdateCache.latestVersion,
|
|
812
|
-
updateCheckedAt: claudeUpdateCache.checkedAt,
|
|
813
|
-
updateSource: action?.source ?? "unknown",
|
|
814
|
-
updateCommand: action?.commandLabel,
|
|
815
|
-
updateMessage: claudeUpdateCache.updateMessage
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
return {};
|
|
819
|
-
}
|
|
820
|
-
function ensureClaudeUpdateCheck(currentVersion, commandPath) {
|
|
821
|
-
if (claudeUpdateCache && Date.now() - claudeUpdateCache.checkedAt < CLAUDE_UPDATE_CACHE_TTL_MS) {
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
if (claudeUpdatePromise) return;
|
|
825
|
-
claudeUpdatePromise = (async () => {
|
|
826
|
-
const action = getClaudeUpdateAction(commandPath || null);
|
|
827
|
-
let latest = null;
|
|
828
|
-
let updateAvailable;
|
|
829
|
-
let updateMessage;
|
|
830
|
-
if (action?.source === "npm" || action?.source === "bun") {
|
|
831
|
-
latest = await fetchLatestNpmVersion(CLAUDE_PACKAGE);
|
|
832
|
-
} else if (action?.source === "brew") {
|
|
833
|
-
latest = await fetchBrewCaskVersion("claude-code");
|
|
834
|
-
}
|
|
835
|
-
if (latest && currentVersion) {
|
|
836
|
-
const a = parseSemver(currentVersion);
|
|
837
|
-
const b = parseSemver(latest);
|
|
838
|
-
if (a && b) {
|
|
839
|
-
updateAvailable = compareSemver(a, b) < 0;
|
|
840
|
-
updateMessage = updateAvailable ? `Update available: ${currentVersion} -> ${latest}` : `Up to date (${currentVersion})`;
|
|
841
|
-
}
|
|
842
|
-
} else if (!action) {
|
|
843
|
-
updateMessage = "Update check unavailable";
|
|
844
|
-
}
|
|
845
|
-
debugLog("Claude", "update-check", {
|
|
846
|
-
updateAvailable,
|
|
847
|
-
message: updateMessage
|
|
848
|
-
});
|
|
849
|
-
claudeUpdateCache = {
|
|
850
|
-
checkedAt: Date.now(),
|
|
851
|
-
updateAvailable,
|
|
852
|
-
latestVersion: latest ?? void 0,
|
|
853
|
-
updateMessage
|
|
854
|
-
};
|
|
855
|
-
})().finally(() => {
|
|
856
|
-
claudeUpdatePromise = null;
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
async function ensureClaudeInstalled() {
|
|
860
|
-
const command2 = getClaudeCommand();
|
|
861
|
-
const versionCheck = await checkCommandVersion(command2, [["--version"], ["-V"]]);
|
|
862
|
-
if (versionCheck.ok) {
|
|
863
|
-
return { installed: true, version: versionCheck.version || void 0 };
|
|
864
|
-
}
|
|
865
|
-
if (commandExists(command2)) {
|
|
866
|
-
return { installed: true, version: void 0 };
|
|
867
|
-
}
|
|
868
|
-
const override = buildInstallCommand("AGENTCONNECT_CLAUDE_INSTALL", "");
|
|
869
|
-
let install = override;
|
|
870
|
-
let packageManager = override.command ? "unknown" : "unknown";
|
|
871
|
-
if (!install.command) {
|
|
872
|
-
if (process.platform === "win32") {
|
|
873
|
-
if (commandExists("powershell")) {
|
|
874
|
-
install = {
|
|
875
|
-
command: "powershell",
|
|
876
|
-
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", INSTALL_WINDOWS_PS]
|
|
877
|
-
};
|
|
878
|
-
packageManager = "script";
|
|
879
|
-
} else if (commandExists("pwsh")) {
|
|
880
|
-
install = {
|
|
881
|
-
command: "pwsh",
|
|
882
|
-
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", INSTALL_WINDOWS_PS]
|
|
883
|
-
};
|
|
884
|
-
packageManager = "script";
|
|
885
|
-
} else if (commandExists("cmd") && commandExists("curl")) {
|
|
886
|
-
install = { command: "cmd", args: ["/c", INSTALL_WINDOWS_CMD] };
|
|
887
|
-
packageManager = "script";
|
|
888
|
-
}
|
|
889
|
-
} else if (commandExists("bash") && commandExists("curl")) {
|
|
890
|
-
install = { command: "bash", args: ["-lc", INSTALL_UNIX] };
|
|
891
|
-
packageManager = "script";
|
|
892
|
-
} else {
|
|
893
|
-
const auto = await buildInstallCommandAuto(CLAUDE_PACKAGE);
|
|
894
|
-
install = { command: auto.command, args: auto.args };
|
|
895
|
-
packageManager = auto.packageManager;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
if (!install.command) {
|
|
899
|
-
return { installed: false, version: void 0, packageManager };
|
|
900
|
-
}
|
|
901
|
-
await runCommand(install.command, install.args, { shell: process.platform === "win32" });
|
|
902
|
-
const after = await checkCommandVersion(command2, [["--version"], ["-V"]]);
|
|
903
|
-
return {
|
|
904
|
-
installed: after.ok,
|
|
905
|
-
version: after.version || void 0,
|
|
906
|
-
packageManager
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
async function getClaudeStatus() {
|
|
910
|
-
const command2 = getClaudeCommand();
|
|
911
|
-
const versionCheck = await checkCommandVersion(command2, [["--version"], ["-V"]]);
|
|
912
|
-
const installed = versionCheck.ok || commandExists(command2);
|
|
913
|
-
let loggedIn = false;
|
|
914
|
-
if (installed) {
|
|
915
|
-
const status = buildStatusCommand("AGENTCONNECT_CLAUDE_STATUS", DEFAULT_STATUS);
|
|
916
|
-
if (status.command) {
|
|
917
|
-
const statusCommand = resolveWindowsCommand(status.command);
|
|
918
|
-
const result = await runCommand(statusCommand, status.args);
|
|
919
|
-
loggedIn = result.code === 0;
|
|
920
|
-
} else {
|
|
921
|
-
const cliStatus = await checkClaudeCliStatus();
|
|
922
|
-
if (cliStatus.loggedIn === false) {
|
|
923
|
-
loggedIn = false;
|
|
924
|
-
} else if (cliStatus.loggedIn === true) {
|
|
925
|
-
loggedIn = true;
|
|
926
|
-
} else if (cliStatus.apiKeySource?.toLowerCase() === "none") {
|
|
927
|
-
loggedIn = false;
|
|
928
|
-
} else {
|
|
929
|
-
loggedIn = await hasClaudeAuth();
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
if (installed) {
|
|
934
|
-
const resolved2 = resolveCommandRealPath(command2);
|
|
935
|
-
ensureClaudeUpdateCheck(versionCheck.version, resolved2 || null);
|
|
936
|
-
}
|
|
937
|
-
const resolved = resolveCommandRealPath(command2);
|
|
938
|
-
const updateInfo = installed ? getClaudeUpdateSnapshot(resolved || null) : {};
|
|
939
|
-
return { installed, loggedIn, version: versionCheck.version || void 0, ...updateInfo };
|
|
940
|
-
}
|
|
941
|
-
async function getClaudeFastStatus() {
|
|
942
|
-
const command2 = getClaudeCommand();
|
|
943
|
-
const installed = commandExists(command2);
|
|
944
|
-
const loggedIn = installed ? await hasClaudeAuth() : false;
|
|
945
|
-
return { installed, loggedIn };
|
|
946
|
-
}
|
|
947
|
-
async function updateClaude() {
|
|
948
|
-
const command2 = getClaudeCommand();
|
|
949
|
-
if (!commandExists(command2)) {
|
|
950
|
-
return { installed: false, loggedIn: false };
|
|
951
|
-
}
|
|
952
|
-
const resolved = resolveCommandRealPath(command2);
|
|
953
|
-
const updateOverride = buildStatusCommand("AGENTCONNECT_CLAUDE_UPDATE", "");
|
|
954
|
-
const action = updateOverride.command ? null : getClaudeUpdateAction(resolved || null);
|
|
955
|
-
const updateCommand = updateOverride.command || action?.command || "";
|
|
956
|
-
const updateArgs = updateOverride.command ? updateOverride.args : action?.args || [];
|
|
957
|
-
if (!updateCommand) {
|
|
958
|
-
throw new Error("No update command available. Please update Claude manually.");
|
|
959
|
-
}
|
|
960
|
-
const cmd = resolveWindowsCommand(updateCommand);
|
|
961
|
-
debugLog("Claude", "update-run", { command: cmd, args: updateArgs });
|
|
962
|
-
const result = await runCommand(cmd, updateArgs, { env: { ...process.env, CI: "1" } });
|
|
963
|
-
debugLog("Claude", "update-result", {
|
|
964
|
-
code: result.code,
|
|
965
|
-
stdout: trimOutput(result.stdout),
|
|
966
|
-
stderr: trimOutput(result.stderr)
|
|
967
|
-
});
|
|
968
|
-
if (result.code !== 0 && result.code !== null) {
|
|
969
|
-
const message = trimOutput(`${result.stdout}
|
|
970
|
-
${result.stderr}`, 800) || "Update failed";
|
|
971
|
-
throw new Error(message);
|
|
972
|
-
}
|
|
973
|
-
claudeUpdateCache = null;
|
|
974
|
-
claudeUpdatePromise = null;
|
|
975
|
-
return getClaudeStatus();
|
|
976
|
-
}
|
|
977
|
-
async function loginClaude(options) {
|
|
978
|
-
const login = buildLoginCommand("AGENTCONNECT_CLAUDE_LOGIN", DEFAULT_LOGIN);
|
|
979
|
-
if (login.command) {
|
|
980
|
-
const command2 = resolveWindowsCommand(login.command);
|
|
981
|
-
await runCommand(command2, login.args);
|
|
982
|
-
} else {
|
|
983
|
-
await runClaudeLoginFlow(options);
|
|
984
|
-
}
|
|
985
|
-
const status = await getClaudeStatus();
|
|
986
|
-
return { loggedIn: status.loggedIn };
|
|
987
|
-
}
|
|
988
|
-
async function runClaudeLoginFlow(options) {
|
|
989
|
-
const command2 = resolveWindowsCommand(getClaudeCommand());
|
|
990
|
-
const loginMethod = resolveClaudeLoginMethod(options);
|
|
991
|
-
const loginExperience = resolveClaudeLoginExperience(options);
|
|
992
|
-
const loginHint = await resolveClaudeLoginHint(options);
|
|
993
|
-
await ensureClaudeOnboardingSettings();
|
|
994
|
-
const settingsPath = await createClaudeLoginSettingsFile(loginMethod);
|
|
995
|
-
const loginTimeoutMs = Number(process.env.AGENTCONNECT_CLAUDE_LOGIN_TIMEOUT_MS || 18e4);
|
|
996
|
-
const loginArgs = settingsPath ? ["--settings", settingsPath] : [];
|
|
997
|
-
const includeLogin = loginHint === "login";
|
|
998
|
-
let ptyProcess = null;
|
|
999
|
-
let childExited = false;
|
|
1000
|
-
const cleanup = async () => {
|
|
1001
|
-
if (ptyProcess) {
|
|
1002
|
-
try {
|
|
1003
|
-
ptyProcess.kill();
|
|
1004
|
-
} catch {
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
if (settingsPath && loginExperience !== "terminal") {
|
|
1008
|
-
try {
|
|
1009
|
-
await rm(settingsPath, { force: true });
|
|
1010
|
-
} catch {
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
};
|
|
1014
|
-
try {
|
|
1015
|
-
if (loginExperience === "terminal") {
|
|
1016
|
-
await openClaudeLoginTerminal(command2, loginArgs, includeLogin);
|
|
1017
|
-
if (settingsPath) {
|
|
1018
|
-
setTimeout(() => {
|
|
1019
|
-
rm(settingsPath, { force: true }).catch(() => {
|
|
1020
|
-
});
|
|
1021
|
-
}, loginTimeoutMs);
|
|
1022
|
-
}
|
|
1023
|
-
} else {
|
|
1024
|
-
const ptyModule = await loadPtyModule();
|
|
1025
|
-
if (!ptyModule) {
|
|
1026
|
-
throw new Error(
|
|
1027
|
-
"Claude login requires node-pty. Reinstall AgentConnect or run `claude /login` manually."
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1030
|
-
const spawnArgs = includeLogin ? [...loginArgs, "/login"] : loginArgs;
|
|
1031
|
-
ptyProcess = ptyModule.spawn(command2, spawnArgs, {
|
|
1032
|
-
name: "xterm-256color",
|
|
1033
|
-
cols: 100,
|
|
1034
|
-
rows: 30,
|
|
1035
|
-
cwd: os2.homedir(),
|
|
1036
|
-
env: { ...process.env, TERM: "xterm-256color" }
|
|
1037
|
-
});
|
|
1038
|
-
let outputBuffer = "";
|
|
1039
|
-
ptyProcess.onData((data) => {
|
|
1040
|
-
outputBuffer += data;
|
|
1041
|
-
if (outputBuffer.length > 6e3) {
|
|
1042
|
-
outputBuffer = outputBuffer.slice(-3e3);
|
|
1043
|
-
}
|
|
1044
|
-
const advanced = maybeAdvanceClaudeOnboarding(outputBuffer, loginMethod, (input) => {
|
|
1045
|
-
ptyProcess?.write(input);
|
|
1046
|
-
});
|
|
1047
|
-
if (advanced) outputBuffer = "";
|
|
1048
|
-
});
|
|
1049
|
-
ptyProcess.onExit(() => {
|
|
1050
|
-
childExited = true;
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
const start = Date.now();
|
|
1054
|
-
while (Date.now() - start < loginTimeoutMs) {
|
|
1055
|
-
const authed = await hasClaudeAuth();
|
|
1056
|
-
if (authed) {
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
if (childExited) {
|
|
1060
|
-
break;
|
|
1061
|
-
}
|
|
1062
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1063
|
-
}
|
|
1064
|
-
throw new Error(
|
|
1065
|
-
"Claude login timed out. Finish login in your browser or run `claude` manually to authenticate."
|
|
1066
|
-
);
|
|
1067
|
-
} finally {
|
|
1068
|
-
await cleanup();
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
function safeJsonParse(line) {
|
|
1072
|
-
try {
|
|
1073
|
-
return JSON.parse(line);
|
|
1074
|
-
} catch {
|
|
1075
|
-
return null;
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
function mapClaudeModel(model) {
|
|
1079
|
-
if (!model) return null;
|
|
1080
|
-
const value = String(model).toLowerCase();
|
|
1081
|
-
if (value === "default" || value === "recommended") return null;
|
|
1082
|
-
if (value === "claude-default" || value === "claude-recommended") return null;
|
|
1083
|
-
if (value.includes("opus")) return "opus";
|
|
1084
|
-
if (value.includes("sonnet")) return "sonnet";
|
|
1085
|
-
if (value.includes("haiku")) return "haiku";
|
|
1086
|
-
if (value.startsWith("claude-")) return value.replace("claude-", "");
|
|
1087
|
-
return model;
|
|
1088
|
-
}
|
|
1089
|
-
function extractSessionId(msg) {
|
|
1090
|
-
const direct = msg.session_id ?? msg.sessionId;
|
|
1091
|
-
if (typeof direct === "string" && direct) return direct;
|
|
1092
|
-
const nested = msg.message?.session_id ?? msg.message?.sessionId;
|
|
1093
|
-
return typeof nested === "string" && nested ? nested : null;
|
|
1094
|
-
}
|
|
1095
|
-
function extractAssistantDelta(msg) {
|
|
1096
|
-
const type = String(msg.type ?? "");
|
|
1097
|
-
if (type === "stream_event") {
|
|
1098
|
-
const ev = msg.event ?? {};
|
|
1099
|
-
if (ev.type === "content_block_delta") {
|
|
1100
|
-
const text2 = ev.delta?.text;
|
|
1101
|
-
return typeof text2 === "string" && text2 ? text2 : null;
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
if (type === "content_block_delta") {
|
|
1105
|
-
const text2 = msg.delta?.text;
|
|
1106
|
-
return typeof text2 === "string" && text2 ? text2 : null;
|
|
1107
|
-
}
|
|
1108
|
-
const text = msg.delta?.text;
|
|
1109
|
-
return typeof text === "string" && text ? text : null;
|
|
1110
|
-
}
|
|
1111
|
-
function extractTextFromContent(content) {
|
|
1112
|
-
if (!content) return "";
|
|
1113
|
-
if (typeof content === "string") return content;
|
|
1114
|
-
if (Array.isArray(content)) {
|
|
1115
|
-
return content.map((part) => {
|
|
1116
|
-
if (!part || typeof part !== "object") return "";
|
|
1117
|
-
const text = part.text;
|
|
1118
|
-
if (typeof text === "string") return text;
|
|
1119
|
-
return "";
|
|
1120
|
-
}).filter(Boolean).join("");
|
|
1121
|
-
}
|
|
1122
|
-
return "";
|
|
1123
|
-
}
|
|
1124
|
-
function extractAssistantText(msg) {
|
|
1125
|
-
if (String(msg.type ?? "") !== "assistant") return null;
|
|
1126
|
-
const content = msg.message?.content;
|
|
1127
|
-
if (!Array.isArray(content)) return null;
|
|
1128
|
-
const textBlock = content.find((c) => c && typeof c === "object" && c.type === "text");
|
|
1129
|
-
const text = textBlock?.text;
|
|
1130
|
-
return typeof text === "string" && text ? text : null;
|
|
1131
|
-
}
|
|
1132
|
-
function extractResultText(msg) {
|
|
1133
|
-
if (String(msg.type ?? "") !== "result") return null;
|
|
1134
|
-
const text = msg.result;
|
|
1135
|
-
return typeof text === "string" && text ? text : null;
|
|
1136
|
-
}
|
|
1137
|
-
function runClaudePrompt({
|
|
1138
|
-
prompt,
|
|
1139
|
-
resumeSessionId,
|
|
1140
|
-
model,
|
|
1141
|
-
cwd,
|
|
1142
|
-
providerDetailLevel,
|
|
1143
|
-
onEvent,
|
|
1144
|
-
signal
|
|
1145
|
-
}) {
|
|
1146
|
-
return new Promise((resolve) => {
|
|
1147
|
-
const command2 = getClaudeCommand();
|
|
1148
|
-
const args2 = [
|
|
1149
|
-
"-p",
|
|
1150
|
-
"--output-format=stream-json",
|
|
1151
|
-
"--verbose",
|
|
1152
|
-
"--permission-mode",
|
|
1153
|
-
"bypassPermissions"
|
|
1154
|
-
];
|
|
1155
|
-
const modelValue = mapClaudeModel(model);
|
|
1156
|
-
if (modelValue) {
|
|
1157
|
-
args2.push("--model", modelValue);
|
|
1158
|
-
}
|
|
1159
|
-
if (resumeSessionId) args2.push("--resume", resumeSessionId);
|
|
1160
|
-
args2.push(prompt);
|
|
1161
|
-
const child = spawn2(command2, args2, {
|
|
1162
|
-
cwd,
|
|
1163
|
-
env: { ...process.env },
|
|
1164
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1165
|
-
});
|
|
1166
|
-
if (signal) {
|
|
1167
|
-
signal.addEventListener("abort", () => {
|
|
1168
|
-
child.kill("SIGTERM");
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
let aggregated = "";
|
|
1172
|
-
let finalSessionId = null;
|
|
1173
|
-
let didFinalize = false;
|
|
1174
|
-
let sawError = false;
|
|
1175
|
-
const toolBlocks = /* @__PURE__ */ new Map();
|
|
1176
|
-
const thinkingBlocks = /* @__PURE__ */ new Set();
|
|
1177
|
-
const includeRaw = providerDetailLevel === "raw";
|
|
1178
|
-
const buildProviderDetail = (eventType, data, raw) => {
|
|
1179
|
-
const detail = { eventType };
|
|
1180
|
-
if (data && Object.keys(data).length) detail.data = data;
|
|
1181
|
-
if (includeRaw && raw !== void 0) detail.raw = raw;
|
|
1182
|
-
return detail;
|
|
1183
|
-
};
|
|
1184
|
-
const emit = (event) => {
|
|
1185
|
-
if (finalSessionId) {
|
|
1186
|
-
onEvent({ ...event, providerSessionId: finalSessionId });
|
|
1187
|
-
} else {
|
|
1188
|
-
onEvent(event);
|
|
1189
|
-
}
|
|
1190
|
-
};
|
|
1191
|
-
const emitError = (message) => {
|
|
1192
|
-
if (sawError) return;
|
|
1193
|
-
sawError = true;
|
|
1194
|
-
emit({ type: "error", message });
|
|
1195
|
-
};
|
|
1196
|
-
const emitFinal = (text, providerDetail) => {
|
|
1197
|
-
emit({ type: "final", text, providerDetail });
|
|
1198
|
-
};
|
|
1199
|
-
const handleLine = (line) => {
|
|
1200
|
-
const parsed = safeJsonParse(line);
|
|
1201
|
-
if (!parsed || typeof parsed !== "object") {
|
|
1202
|
-
if (line.trim()) {
|
|
1203
|
-
emit({ type: "raw_line", line });
|
|
1204
|
-
}
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
const msg = parsed;
|
|
1208
|
-
const sid = extractSessionId(msg);
|
|
1209
|
-
if (sid) finalSessionId = sid;
|
|
1210
|
-
const msgType = String(msg.type ?? "");
|
|
1211
|
-
let handled = false;
|
|
1212
|
-
if (msgType === "assistant" || msgType === "user" || msgType === "system") {
|
|
1213
|
-
const role = msg.message?.role === "assistant" || msg.message?.role === "user" || msg.message?.role === "system" ? msg.message.role : msgType;
|
|
1214
|
-
const rawContent = msg.message?.content;
|
|
1215
|
-
const content = extractTextFromContent(rawContent);
|
|
1216
|
-
emit({
|
|
1217
|
-
type: "message",
|
|
1218
|
-
provider: "claude",
|
|
1219
|
-
role,
|
|
1220
|
-
content,
|
|
1221
|
-
contentParts: rawContent ?? null,
|
|
1222
|
-
providerDetail: buildProviderDetail(msgType, {}, msg)
|
|
1223
|
-
});
|
|
1224
|
-
handled = true;
|
|
1225
|
-
}
|
|
1226
|
-
if (msgType === "stream_event" && msg.event) {
|
|
1227
|
-
const evType = String(msg.event.type ?? "");
|
|
1228
|
-
const index = typeof msg.event.index === "number" ? msg.event.index : void 0;
|
|
1229
|
-
const block = msg.event.content_block;
|
|
1230
|
-
if (evType === "content_block_start" && block && typeof index === "number") {
|
|
1231
|
-
if (block.type === "tool_use" || block.type === "server_tool_use" || block.type === "mcp_tool_use") {
|
|
1232
|
-
toolBlocks.set(index, { id: block.id, name: block.name });
|
|
1233
|
-
emit({
|
|
1234
|
-
type: "tool_call",
|
|
1235
|
-
provider: "claude",
|
|
1236
|
-
name: block.name,
|
|
1237
|
-
callId: block.id,
|
|
1238
|
-
input: block.input,
|
|
1239
|
-
phase: "start",
|
|
1240
|
-
providerDetail: buildProviderDetail(
|
|
1241
|
-
"content_block_start",
|
|
1242
|
-
{ blockType: block.type, index, name: block.name, id: block.id },
|
|
1243
|
-
msg
|
|
1244
|
-
)
|
|
1245
|
-
});
|
|
1246
|
-
}
|
|
1247
|
-
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
|
1248
|
-
thinkingBlocks.add(index);
|
|
1249
|
-
emit({
|
|
1250
|
-
type: "thinking",
|
|
1251
|
-
provider: "claude",
|
|
1252
|
-
phase: "start",
|
|
1253
|
-
text: typeof block.thinking === "string" ? block.thinking : void 0,
|
|
1254
|
-
providerDetail: buildProviderDetail(
|
|
1255
|
-
"content_block_start",
|
|
1256
|
-
{ blockType: block.type, index },
|
|
1257
|
-
msg
|
|
1258
|
-
)
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
|
-
handled = true;
|
|
1262
|
-
}
|
|
1263
|
-
if (evType === "content_block_delta") {
|
|
1264
|
-
const delta2 = msg.event.delta ?? {};
|
|
1265
|
-
if (delta2.type === "thinking_delta") {
|
|
1266
|
-
emit({
|
|
1267
|
-
type: "thinking",
|
|
1268
|
-
provider: "claude",
|
|
1269
|
-
phase: "delta",
|
|
1270
|
-
text: typeof delta2.thinking === "string" ? delta2.thinking : void 0,
|
|
1271
|
-
providerDetail: buildProviderDetail(
|
|
1272
|
-
"content_block_delta",
|
|
1273
|
-
{ deltaType: delta2.type, index },
|
|
1274
|
-
msg
|
|
1275
|
-
)
|
|
1276
|
-
});
|
|
1277
|
-
handled = true;
|
|
1278
|
-
}
|
|
1279
|
-
if (delta2.type === "input_json_delta") {
|
|
1280
|
-
const tool = typeof index === "number" ? toolBlocks.get(index) : void 0;
|
|
1281
|
-
emit({
|
|
1282
|
-
type: "tool_call",
|
|
1283
|
-
provider: "claude",
|
|
1284
|
-
name: tool?.name,
|
|
1285
|
-
callId: tool?.id,
|
|
1286
|
-
input: delta2.partial_json,
|
|
1287
|
-
phase: "delta",
|
|
1288
|
-
providerDetail: buildProviderDetail(
|
|
1289
|
-
"content_block_delta",
|
|
1290
|
-
{ deltaType: delta2.type, index, name: tool?.name, id: tool?.id },
|
|
1291
|
-
msg
|
|
1292
|
-
)
|
|
1293
|
-
});
|
|
1294
|
-
handled = true;
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
if (evType === "content_block_stop" && typeof index === "number") {
|
|
1298
|
-
if (toolBlocks.has(index)) {
|
|
1299
|
-
const tool = toolBlocks.get(index);
|
|
1300
|
-
emit({
|
|
1301
|
-
type: "tool_call",
|
|
1302
|
-
provider: "claude",
|
|
1303
|
-
name: tool?.name,
|
|
1304
|
-
callId: tool?.id,
|
|
1305
|
-
phase: "completed",
|
|
1306
|
-
providerDetail: buildProviderDetail(
|
|
1307
|
-
"content_block_stop",
|
|
1308
|
-
{ index, name: tool?.name, id: tool?.id },
|
|
1309
|
-
msg
|
|
1310
|
-
)
|
|
1311
|
-
});
|
|
1312
|
-
toolBlocks.delete(index);
|
|
1313
|
-
}
|
|
1314
|
-
if (thinkingBlocks.has(index)) {
|
|
1315
|
-
emit({
|
|
1316
|
-
type: "thinking",
|
|
1317
|
-
provider: "claude",
|
|
1318
|
-
phase: "completed",
|
|
1319
|
-
providerDetail: buildProviderDetail("content_block_stop", { index }, msg)
|
|
1320
|
-
});
|
|
1321
|
-
thinkingBlocks.delete(index);
|
|
1322
|
-
}
|
|
1323
|
-
handled = true;
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
const delta = extractAssistantDelta(msg);
|
|
1327
|
-
if (delta) {
|
|
1328
|
-
aggregated += delta;
|
|
1329
|
-
emit({
|
|
1330
|
-
type: "delta",
|
|
1331
|
-
text: delta,
|
|
1332
|
-
providerDetail: buildProviderDetail(msgType || "delta", {}, msg)
|
|
1333
|
-
});
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
const assistant = extractAssistantText(msg);
|
|
1337
|
-
if (assistant && !aggregated) {
|
|
1338
|
-
aggregated = assistant;
|
|
1339
|
-
emit({
|
|
1340
|
-
type: "delta",
|
|
1341
|
-
text: assistant,
|
|
1342
|
-
providerDetail: buildProviderDetail(msgType || "assistant", {}, msg)
|
|
1343
|
-
});
|
|
1344
|
-
return;
|
|
1345
|
-
}
|
|
1346
|
-
const result = extractResultText(msg);
|
|
1347
|
-
if (result && !didFinalize && !sawError) {
|
|
1348
|
-
didFinalize = true;
|
|
1349
|
-
emitFinal(aggregated || result, buildProviderDetail("result", {}, msg));
|
|
1350
|
-
handled = true;
|
|
1351
|
-
}
|
|
1352
|
-
if (!handled) {
|
|
1353
|
-
emit({
|
|
1354
|
-
type: "detail",
|
|
1355
|
-
provider: "claude",
|
|
1356
|
-
providerDetail: buildProviderDetail(msgType || "unknown", {}, msg)
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
};
|
|
1360
|
-
const stdoutParser = createLineParser(handleLine);
|
|
1361
|
-
const stderrParser = createLineParser(handleLine);
|
|
1362
|
-
child.stdout?.on("data", stdoutParser);
|
|
1363
|
-
child.stderr?.on("data", stderrParser);
|
|
1364
|
-
child.on("close", (code) => {
|
|
1365
|
-
if (!didFinalize) {
|
|
1366
|
-
if (code && code !== 0) {
|
|
1367
|
-
emitError(`Claude exited with code ${code}`);
|
|
1368
|
-
} else if (!sawError) {
|
|
1369
|
-
emitFinal(aggregated);
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
resolve({ sessionId: finalSessionId });
|
|
1373
|
-
});
|
|
1374
|
-
child.on("error", (err) => {
|
|
1375
|
-
emitError(err?.message ?? "Claude failed to start");
|
|
1376
|
-
resolve({ sessionId: finalSessionId });
|
|
1377
|
-
});
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// src/providers/codex.ts
|
|
1382
|
-
import { spawn as spawn3 } from "child_process";
|
|
1383
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
1384
|
-
import https2 from "https";
|
|
1385
|
-
import os3 from "os";
|
|
1386
|
-
import path3 from "path";
|
|
1387
|
-
var CODEX_PACKAGE = "@openai/codex";
|
|
1388
|
-
var DEFAULT_LOGIN2 = "codex login";
|
|
1389
|
-
var DEFAULT_STATUS2 = "codex login status";
|
|
1390
|
-
var CODEX_MODELS_CACHE_TTL_MS = 6e4;
|
|
1391
|
-
var CODEX_UPDATE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
1392
|
-
var codexModelsCache = null;
|
|
1393
|
-
var codexModelsCacheAt = 0;
|
|
1394
|
-
var codexUpdateCache = null;
|
|
1395
|
-
var codexUpdatePromise = null;
|
|
1396
|
-
function trimOutput2(value, limit = 400) {
|
|
1397
|
-
const cleaned = value.trim();
|
|
1398
|
-
if (!cleaned) return "";
|
|
1399
|
-
if (cleaned.length <= limit) return cleaned;
|
|
1400
|
-
return `${cleaned.slice(0, limit)}...`;
|
|
1401
|
-
}
|
|
1402
|
-
function normalizePath2(value) {
|
|
1403
|
-
const normalized = value.replace(/\\/g, "/");
|
|
1404
|
-
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
1405
|
-
}
|
|
1406
|
-
function getCodexConfigDir() {
|
|
1407
|
-
return process.env.CODEX_CONFIG_DIR || path3.join(os3.homedir(), ".codex");
|
|
1408
|
-
}
|
|
1409
|
-
function fetchJson2(url) {
|
|
1410
|
-
return new Promise((resolve) => {
|
|
1411
|
-
https2.get(url, (res) => {
|
|
1412
|
-
let data = "";
|
|
1413
|
-
res.on("data", (chunk) => {
|
|
1414
|
-
data += chunk;
|
|
1415
|
-
});
|
|
1416
|
-
res.on("end", () => {
|
|
1417
|
-
try {
|
|
1418
|
-
resolve(JSON.parse(data));
|
|
1419
|
-
} catch {
|
|
1420
|
-
resolve(null);
|
|
1421
|
-
}
|
|
1422
|
-
});
|
|
1423
|
-
}).on("error", () => resolve(null));
|
|
1424
|
-
});
|
|
1425
|
-
}
|
|
1426
|
-
function parseSemver2(value) {
|
|
1427
|
-
if (!value) return null;
|
|
1428
|
-
const match = value.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
1429
|
-
if (!match) return null;
|
|
1430
|
-
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
1431
|
-
}
|
|
1432
|
-
function compareSemver2(a, b) {
|
|
1433
|
-
if (a[0] !== b[0]) return a[0] - b[0];
|
|
1434
|
-
if (a[1] !== b[1]) return a[1] - b[1];
|
|
1435
|
-
return a[2] - b[2];
|
|
1436
|
-
}
|
|
1437
|
-
async function fetchLatestNpmVersion2(pkg) {
|
|
1438
|
-
const encoded = encodeURIComponent(pkg);
|
|
1439
|
-
const data = await fetchJson2(`https://registry.npmjs.org/${encoded}`);
|
|
1440
|
-
if (!data || typeof data !== "object") return null;
|
|
1441
|
-
const latest = data["dist-tags"]?.latest;
|
|
1442
|
-
return typeof latest === "string" ? latest : null;
|
|
1443
|
-
}
|
|
1444
|
-
async function fetchBrewFormulaVersion(formula) {
|
|
1445
|
-
if (!commandExists("brew")) return null;
|
|
1446
|
-
const result = await runCommand("brew", ["info", "--json=v2", formula]);
|
|
1447
|
-
if (result.code !== 0) return null;
|
|
1448
|
-
try {
|
|
1449
|
-
const parsed = JSON.parse(result.stdout);
|
|
1450
|
-
const version = parsed?.formulae?.[0]?.versions?.stable;
|
|
1451
|
-
return typeof version === "string" ? version : null;
|
|
1452
|
-
} catch {
|
|
1453
|
-
return null;
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
function getCodexUpdateAction(commandPath) {
|
|
1457
|
-
if (process.env.CODEX_MANAGED_BY_NPM) {
|
|
1458
|
-
return {
|
|
1459
|
-
command: "npm",
|
|
1460
|
-
args: ["install", "-g", CODEX_PACKAGE],
|
|
1461
|
-
source: "npm",
|
|
1462
|
-
commandLabel: "npm install -g @openai/codex"
|
|
1463
|
-
};
|
|
1464
|
-
}
|
|
1465
|
-
if (process.env.CODEX_MANAGED_BY_BUN) {
|
|
1466
|
-
return {
|
|
1467
|
-
command: "bun",
|
|
1468
|
-
args: ["install", "-g", CODEX_PACKAGE],
|
|
1469
|
-
source: "bun",
|
|
1470
|
-
commandLabel: "bun install -g @openai/codex"
|
|
1471
|
-
};
|
|
1472
|
-
}
|
|
1473
|
-
if (commandPath) {
|
|
1474
|
-
const normalized = normalizePath2(commandPath);
|
|
1475
|
-
if (normalized.includes(".bun/install/global")) {
|
|
1476
|
-
return {
|
|
1477
|
-
command: "bun",
|
|
1478
|
-
args: ["install", "-g", CODEX_PACKAGE],
|
|
1479
|
-
source: "bun",
|
|
1480
|
-
commandLabel: "bun install -g @openai/codex"
|
|
1481
|
-
};
|
|
1482
|
-
}
|
|
1483
|
-
if (normalized.includes("/node_modules/.bin/") || normalized.includes("/lib/node_modules/")) {
|
|
1484
|
-
return {
|
|
1485
|
-
command: "npm",
|
|
1486
|
-
args: ["install", "-g", CODEX_PACKAGE],
|
|
1487
|
-
source: "npm",
|
|
1488
|
-
commandLabel: "npm install -g @openai/codex"
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
if (process.platform === "darwin" && commandPath && (commandPath.startsWith("/opt/homebrew") || commandPath.startsWith("/usr/local"))) {
|
|
1493
|
-
return {
|
|
1494
|
-
command: "brew",
|
|
1495
|
-
args: ["upgrade", "codex"],
|
|
1496
|
-
source: "brew",
|
|
1497
|
-
commandLabel: "brew upgrade codex"
|
|
1498
|
-
};
|
|
1499
|
-
}
|
|
1500
|
-
return null;
|
|
1501
|
-
}
|
|
1502
|
-
function getCodexUpdateSnapshot(commandPath) {
|
|
1503
|
-
if (codexUpdateCache && Date.now() - codexUpdateCache.checkedAt < CODEX_UPDATE_CACHE_TTL_MS) {
|
|
1504
|
-
const action = getCodexUpdateAction(commandPath);
|
|
1505
|
-
return {
|
|
1506
|
-
updateAvailable: codexUpdateCache.updateAvailable,
|
|
1507
|
-
latestVersion: codexUpdateCache.latestVersion,
|
|
1508
|
-
updateCheckedAt: codexUpdateCache.checkedAt,
|
|
1509
|
-
updateSource: action?.source ?? "unknown",
|
|
1510
|
-
updateCommand: action?.commandLabel,
|
|
1511
|
-
updateMessage: codexUpdateCache.updateMessage
|
|
1512
|
-
};
|
|
1513
|
-
}
|
|
1514
|
-
return {};
|
|
1515
|
-
}
|
|
1516
|
-
function ensureCodexUpdateCheck(currentVersion, commandPath) {
|
|
1517
|
-
if (codexUpdateCache && Date.now() - codexUpdateCache.checkedAt < CODEX_UPDATE_CACHE_TTL_MS) {
|
|
1518
|
-
return;
|
|
1519
|
-
}
|
|
1520
|
-
if (codexUpdatePromise) return;
|
|
1521
|
-
codexUpdatePromise = (async () => {
|
|
1522
|
-
const action = getCodexUpdateAction(commandPath || null);
|
|
1523
|
-
let latest = null;
|
|
1524
|
-
if (action?.source === "brew") {
|
|
1525
|
-
latest = await fetchBrewFormulaVersion("codex");
|
|
1526
|
-
} else {
|
|
1527
|
-
latest = await fetchLatestNpmVersion2(CODEX_PACKAGE);
|
|
1528
|
-
}
|
|
1529
|
-
let updateAvailable;
|
|
1530
|
-
let updateMessage;
|
|
1531
|
-
if (latest && currentVersion) {
|
|
1532
|
-
const a = parseSemver2(currentVersion);
|
|
1533
|
-
const b = parseSemver2(latest);
|
|
1534
|
-
if (a && b) {
|
|
1535
|
-
updateAvailable = compareSemver2(a, b) < 0;
|
|
1536
|
-
updateMessage = updateAvailable ? `Update available: ${currentVersion} -> ${latest}` : `Up to date (${currentVersion})`;
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
codexUpdateCache = {
|
|
1540
|
-
checkedAt: Date.now(),
|
|
1541
|
-
updateAvailable,
|
|
1542
|
-
latestVersion: latest ?? void 0,
|
|
1543
|
-
updateMessage
|
|
1544
|
-
};
|
|
1545
|
-
debugLog("Codex", "update-check", {
|
|
1546
|
-
currentVersion,
|
|
1547
|
-
latest,
|
|
1548
|
-
updateAvailable,
|
|
1549
|
-
message: updateMessage
|
|
1550
|
-
});
|
|
1551
|
-
})().finally(() => {
|
|
1552
|
-
codexUpdatePromise = null;
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
function hasAuthValue(value) {
|
|
1556
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
1557
|
-
}
|
|
1558
|
-
async function hasCodexAuth() {
|
|
1559
|
-
const home = os3.homedir();
|
|
1560
|
-
const candidates = [
|
|
1561
|
-
path3.join(getCodexConfigDir(), "auth.json"),
|
|
1562
|
-
path3.join(home, ".config", "codex", "auth.json")
|
|
1563
|
-
];
|
|
1564
|
-
for (const filePath of candidates) {
|
|
1565
|
-
try {
|
|
1566
|
-
const raw = await readFile2(filePath, "utf8");
|
|
1567
|
-
const parsed = JSON.parse(raw);
|
|
1568
|
-
if (hasAuthValue(parsed.OPENAI_API_KEY)) return true;
|
|
1569
|
-
if (hasAuthValue(parsed.access_token)) return true;
|
|
1570
|
-
if (hasAuthValue(parsed.token)) return true;
|
|
1571
|
-
const tokens = parsed.tokens;
|
|
1572
|
-
if (tokens) {
|
|
1573
|
-
if (hasAuthValue(tokens.access_token)) return true;
|
|
1574
|
-
if (hasAuthValue(tokens.refresh_token)) return true;
|
|
1575
|
-
if (hasAuthValue(tokens.id_token)) return true;
|
|
1576
|
-
}
|
|
1577
|
-
} catch {
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
return false;
|
|
1581
|
-
}
|
|
1582
|
-
function buildCodexExecArgs(options) {
|
|
1583
|
-
const { prompt, cdTarget, resumeSessionId, model, reasoningEffort, mode } = options;
|
|
1584
|
-
const args2 = ["exec", "--skip-git-repo-check"];
|
|
1585
|
-
if (mode === "legacy") {
|
|
1586
|
-
args2.push("--json", "-C", cdTarget);
|
|
1587
|
-
} else {
|
|
1588
|
-
args2.push("--experimental-json", "--cd", cdTarget);
|
|
1589
|
-
}
|
|
1590
|
-
args2.push("--yolo");
|
|
1591
|
-
if (model) {
|
|
1592
|
-
args2.push("--model", String(model));
|
|
1593
|
-
}
|
|
1594
|
-
if (reasoningEffort) {
|
|
1595
|
-
args2.push("--config", `model_reasoning_effort=${reasoningEffort}`);
|
|
1596
|
-
}
|
|
1597
|
-
if (resumeSessionId) {
|
|
1598
|
-
args2.push("resume", resumeSessionId);
|
|
1599
|
-
}
|
|
1600
|
-
args2.push(prompt);
|
|
1601
|
-
return args2;
|
|
1602
|
-
}
|
|
1603
|
-
function shouldFallbackToLegacy(lines) {
|
|
1604
|
-
const combined = lines.join(" ").toLowerCase();
|
|
1605
|
-
if (!(combined.includes("unknown flag") || combined.includes("unknown option") || combined.includes("unrecognized option") || combined.includes("unknown argument") || combined.includes("unexpected argument") || combined.includes("invalid option") || combined.includes("invalid argument"))) {
|
|
1606
|
-
return false;
|
|
1607
|
-
}
|
|
1608
|
-
return combined.includes("experimental-json") || combined.includes("--cd");
|
|
1609
|
-
}
|
|
1610
|
-
function getCodexCommand() {
|
|
1611
|
-
const override = process.env.AGENTCONNECT_CODEX_COMMAND;
|
|
1612
|
-
const base = override || "codex";
|
|
1613
|
-
const resolved = resolveCommandPath(base);
|
|
1614
|
-
return resolved || resolveWindowsCommand(base);
|
|
1615
|
-
}
|
|
1616
|
-
async function ensureCodexInstalled() {
|
|
1617
|
-
const command2 = getCodexCommand();
|
|
1618
|
-
const versionCheck = await checkCommandVersion(command2, [["--version"], ["-V"]]);
|
|
1619
|
-
if (versionCheck.ok) {
|
|
1620
|
-
return { installed: true, version: versionCheck.version || void 0 };
|
|
1621
|
-
}
|
|
1622
|
-
if (commandExists(command2)) {
|
|
1623
|
-
return { installed: true, version: void 0 };
|
|
1624
|
-
}
|
|
1625
|
-
const install = await buildInstallCommandAuto(CODEX_PACKAGE);
|
|
1626
|
-
if (!install.command) {
|
|
1627
|
-
return { installed: false, version: void 0, packageManager: install.packageManager };
|
|
1628
|
-
}
|
|
1629
|
-
debugLog("Codex", "install", { command: install.command, args: install.args });
|
|
1630
|
-
const installResult = await runCommand(install.command, install.args, {
|
|
1631
|
-
shell: process.platform === "win32"
|
|
1632
|
-
});
|
|
1633
|
-
debugLog("Codex", "install-result", {
|
|
1634
|
-
code: installResult.code,
|
|
1635
|
-
stderr: trimOutput2(installResult.stderr)
|
|
1636
|
-
});
|
|
1637
|
-
const after = await checkCommandVersion(command2, [["--version"], ["-V"]]);
|
|
1638
|
-
codexModelsCache = null;
|
|
1639
|
-
codexModelsCacheAt = 0;
|
|
1640
|
-
return {
|
|
1641
|
-
installed: after.ok,
|
|
1642
|
-
version: after.version || void 0,
|
|
1643
|
-
packageManager: install.packageManager
|
|
1644
|
-
};
|
|
1645
|
-
}
|
|
1646
|
-
async function getCodexStatus() {
|
|
1647
|
-
const command2 = getCodexCommand();
|
|
1648
|
-
const versionCheck = await checkCommandVersion(command2, [["--version"], ["-V"]]);
|
|
1649
|
-
const installed = versionCheck.ok || commandExists(command2);
|
|
1650
|
-
let loggedIn = false;
|
|
1651
|
-
let explicitLoggedOut = false;
|
|
1652
|
-
if (installed) {
|
|
1653
|
-
const status = buildStatusCommand("AGENTCONNECT_CODEX_STATUS", DEFAULT_STATUS2);
|
|
1654
|
-
if (status.command) {
|
|
1655
|
-
const statusCommand = resolveWindowsCommand(status.command);
|
|
1656
|
-
const result = await runCommand(statusCommand, status.args, { env: { ...process.env, CI: "1" } });
|
|
1657
|
-
const output = `${result.stdout}
|
|
1658
|
-
${result.stderr}`.toLowerCase();
|
|
1659
|
-
if (output.includes("not logged in") || output.includes("not logged") || output.includes("login required") || output.includes("please login") || output.includes("run codex login")) {
|
|
1660
|
-
explicitLoggedOut = true;
|
|
1661
|
-
loggedIn = false;
|
|
1662
|
-
} else if (output.includes("logged in") || output.includes("signed in") || output.includes("authenticated")) {
|
|
1663
|
-
loggedIn = true;
|
|
1664
|
-
} else {
|
|
1665
|
-
loggedIn = result.code === 0;
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
if (!loggedIn && !explicitLoggedOut) {
|
|
1669
|
-
loggedIn = await hasCodexAuth();
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
const resolved = resolveCommandRealPath(command2);
|
|
1673
|
-
if (installed) {
|
|
1674
|
-
ensureCodexUpdateCheck(versionCheck.version, resolved || null);
|
|
1675
|
-
}
|
|
1676
|
-
const updateInfo = installed ? getCodexUpdateSnapshot(resolved || null) : {};
|
|
1677
|
-
return { installed, loggedIn, version: versionCheck.version || void 0, ...updateInfo };
|
|
1678
|
-
}
|
|
1679
|
-
async function getCodexFastStatus() {
|
|
1680
|
-
const command2 = getCodexCommand();
|
|
1681
|
-
const loggedIn = await hasCodexAuth();
|
|
1682
|
-
const installed = commandExists(command2) || loggedIn;
|
|
1683
|
-
return { installed, loggedIn };
|
|
1684
|
-
}
|
|
1685
|
-
async function updateCodex() {
|
|
1686
|
-
const command2 = getCodexCommand();
|
|
1687
|
-
if (!commandExists(command2)) {
|
|
1688
|
-
return { installed: false, loggedIn: false };
|
|
1689
|
-
}
|
|
1690
|
-
const resolved = resolveCommandRealPath(command2);
|
|
1691
|
-
const updateOverride = buildStatusCommand("AGENTCONNECT_CODEX_UPDATE", "");
|
|
1692
|
-
const action = updateOverride.command ? null : getCodexUpdateAction(resolved || null);
|
|
1693
|
-
const updateCommand = updateOverride.command || action?.command || "";
|
|
1694
|
-
const updateArgs = updateOverride.command ? updateOverride.args : action?.args || [];
|
|
1695
|
-
if (!updateCommand) {
|
|
1696
|
-
throw new Error("No update command available. Please update Codex manually.");
|
|
1697
|
-
}
|
|
1698
|
-
const cmd = resolveWindowsCommand(updateCommand);
|
|
1699
|
-
debugLog("Codex", "update-run", { command: cmd, args: updateArgs });
|
|
1700
|
-
const result = await runCommand(cmd, updateArgs, {
|
|
1701
|
-
env: { ...process.env, CI: "1" }
|
|
1702
|
-
});
|
|
1703
|
-
debugLog("Codex", "update-result", {
|
|
1704
|
-
code: result.code,
|
|
1705
|
-
stdout: trimOutput2(result.stdout),
|
|
1706
|
-
stderr: trimOutput2(result.stderr)
|
|
1707
|
-
});
|
|
1708
|
-
if (result.code !== 0 && result.code !== null) {
|
|
1709
|
-
const message = trimOutput2(`${result.stdout}
|
|
1710
|
-
${result.stderr}`, 800) || "Update failed";
|
|
1711
|
-
throw new Error(message);
|
|
1712
|
-
}
|
|
1713
|
-
codexUpdateCache = null;
|
|
1714
|
-
codexUpdatePromise = null;
|
|
1715
|
-
return getCodexStatus();
|
|
1716
|
-
}
|
|
1717
|
-
async function loginCodex() {
|
|
1718
|
-
const login = buildLoginCommand("AGENTCONNECT_CODEX_LOGIN", DEFAULT_LOGIN2);
|
|
1719
|
-
if (!login.command) {
|
|
1720
|
-
return { loggedIn: false };
|
|
1721
|
-
}
|
|
1722
|
-
const command2 = resolveWindowsCommand(login.command);
|
|
1723
|
-
debugLog("Codex", "login", { command: command2, args: login.args });
|
|
1724
|
-
const result = await runCommand(command2, login.args, { env: { ...process.env, CI: "1" } });
|
|
1725
|
-
debugLog("Codex", "login-result", { code: result.code, stderr: trimOutput2(result.stderr) });
|
|
1726
|
-
const status = await getCodexStatus();
|
|
1727
|
-
codexModelsCache = null;
|
|
1728
|
-
codexModelsCacheAt = 0;
|
|
1729
|
-
return { loggedIn: status.loggedIn };
|
|
1730
|
-
}
|
|
1731
|
-
function safeJsonParse2(line) {
|
|
1732
|
-
try {
|
|
1733
|
-
return JSON.parse(line);
|
|
1734
|
-
} catch {
|
|
1735
|
-
return null;
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
function extractSessionId2(ev) {
|
|
1739
|
-
const t = String(ev.type ?? "");
|
|
1740
|
-
if (t === "thread.started") {
|
|
1741
|
-
return typeof ev.thread_id === "string" ? ev.thread_id : null;
|
|
1742
|
-
}
|
|
1743
|
-
const id = ev.thread_id ?? ev.threadId ?? ev.session_id ?? ev.sessionId;
|
|
1744
|
-
return typeof id === "string" ? id : null;
|
|
1745
|
-
}
|
|
1746
|
-
function extractUsage(ev) {
|
|
1747
|
-
const usage = ev.usage ?? ev.token_usage ?? ev.tokens ?? ev.tokenUsage;
|
|
1748
|
-
if (!usage || typeof usage !== "object") return null;
|
|
1749
|
-
const toNumber = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
1750
|
-
const input = toNumber(
|
|
1751
|
-
usage.input_tokens ?? usage.prompt_tokens ?? usage.inputTokens ?? usage.promptTokens
|
|
1752
|
-
);
|
|
1753
|
-
const output = toNumber(
|
|
1754
|
-
usage.output_tokens ?? usage.completion_tokens ?? usage.outputTokens ?? usage.completionTokens
|
|
1755
|
-
);
|
|
1756
|
-
const total = toNumber(usage.total_tokens ?? usage.totalTokens);
|
|
1757
|
-
const cached = toNumber(usage.cached_input_tokens ?? usage.cachedInputTokens);
|
|
1758
|
-
const out = {};
|
|
1759
|
-
if (input !== void 0) out.input_tokens = input;
|
|
1760
|
-
if (output !== void 0) out.output_tokens = output;
|
|
1761
|
-
if (total !== void 0) out.total_tokens = total;
|
|
1762
|
-
if (cached !== void 0) out.cached_input_tokens = cached;
|
|
1763
|
-
return Object.keys(out).length ? out : null;
|
|
1764
|
-
}
|
|
1765
|
-
function normalizeItem(raw) {
|
|
1766
|
-
if (!raw || typeof raw !== "object") return raw;
|
|
1767
|
-
const item = raw;
|
|
1768
|
-
const type = item.type;
|
|
1769
|
-
const id = item.id;
|
|
1770
|
-
if (type === "command_execution" && id) {
|
|
1771
|
-
return {
|
|
1772
|
-
id,
|
|
1773
|
-
type,
|
|
1774
|
-
command: item.command || "",
|
|
1775
|
-
aggregated_output: item.aggregated_output,
|
|
1776
|
-
exit_code: item.exit_code,
|
|
1777
|
-
status: item.status
|
|
1778
|
-
};
|
|
1779
|
-
}
|
|
1780
|
-
if (type === "reasoning" && id) {
|
|
1781
|
-
return { id, type, text: item.text || "" };
|
|
1782
|
-
}
|
|
1783
|
-
if (type === "agent_message" && id) {
|
|
1784
|
-
return { id, type, text: item.text || "" };
|
|
1785
|
-
}
|
|
1786
|
-
return raw;
|
|
1787
|
-
}
|
|
1788
|
-
function normalizeEvent(raw) {
|
|
1789
|
-
const type = typeof raw.type === "string" ? raw.type : "unknown";
|
|
1790
|
-
if (type === "item.started" || type === "item.completed") {
|
|
1791
|
-
return { type, item: normalizeItem(raw.item) };
|
|
1792
|
-
}
|
|
1793
|
-
if (type === "agent_message") {
|
|
1794
|
-
return { type, text: raw.text || "" };
|
|
1795
|
-
}
|
|
1796
|
-
if (type === "error") {
|
|
1797
|
-
return { type, message: raw.message || "Unknown error" };
|
|
1798
|
-
}
|
|
1799
|
-
return { ...raw, type };
|
|
1800
|
-
}
|
|
1801
|
-
function isTerminalEvent(ev) {
|
|
1802
|
-
const t = String(ev.type ?? "");
|
|
1803
|
-
return t === "turn.completed" || t === "turn.failed";
|
|
1804
|
-
}
|
|
1805
|
-
function normalizeEffortId(raw) {
|
|
1806
|
-
if (!raw) return null;
|
|
1807
|
-
return String(raw).trim().toLowerCase();
|
|
1808
|
-
}
|
|
1809
|
-
function formatEffortLabel(id) {
|
|
1810
|
-
if (!id) return "";
|
|
1811
|
-
const normalized = String(id).trim().toLowerCase();
|
|
1812
|
-
if (normalized === "xhigh") return "X-High";
|
|
1813
|
-
if (normalized === "none") return "None";
|
|
1814
|
-
if (normalized === "minimal") return "Minimal";
|
|
1815
|
-
if (normalized === "low") return "Low";
|
|
1816
|
-
if (normalized === "medium") return "Medium";
|
|
1817
|
-
if (normalized === "high") return "High";
|
|
1818
|
-
return normalized.toUpperCase();
|
|
1819
|
-
}
|
|
1820
|
-
function normalizeEffortOptions(raw) {
|
|
1821
|
-
if (!Array.isArray(raw)) return [];
|
|
1822
|
-
const options = raw.map((entry) => {
|
|
1823
|
-
if (!entry || typeof entry !== "object") return null;
|
|
1824
|
-
const opt = entry;
|
|
1825
|
-
const id = normalizeEffortId(
|
|
1826
|
-
opt.reasoning_effort ?? opt.reasoningEffort ?? opt.effort ?? opt.level
|
|
1827
|
-
);
|
|
1828
|
-
if (!id) return null;
|
|
1829
|
-
const label = formatEffortLabel(id);
|
|
1830
|
-
return { id, label };
|
|
1831
|
-
}).filter((x) => x !== null);
|
|
1832
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1833
|
-
return options.filter((option) => {
|
|
1834
|
-
if (seen.has(option.id)) return false;
|
|
1835
|
-
seen.add(option.id);
|
|
1836
|
-
return true;
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
function normalizeCodexModels(raw) {
|
|
1840
|
-
const response = raw;
|
|
1841
|
-
const list = Array.isArray(response?.data) ? response.data : Array.isArray(response?.items) ? response.items : [];
|
|
1842
|
-
const mapped = [];
|
|
1843
|
-
for (const item of list) {
|
|
1844
|
-
if (!item || typeof item !== "object") continue;
|
|
1845
|
-
const id = item.id || item.model;
|
|
1846
|
-
if (!id) continue;
|
|
1847
|
-
const displayName = item.displayName || item.display_name || item.name || item.title || String(id);
|
|
1848
|
-
const reasoningEfforts = normalizeEffortOptions(
|
|
1849
|
-
item.supportedReasoningEfforts || item.supported_reasoning_efforts || item.supportedReasoningLevels || item.supported_reasoning_levels
|
|
1850
|
-
);
|
|
1851
|
-
const defaultReasoningEffort = normalizeEffortId(
|
|
1852
|
-
item.defaultReasoningEffort || item.default_reasoning_effort
|
|
1853
|
-
);
|
|
1854
|
-
mapped.push({
|
|
1855
|
-
id: String(id),
|
|
1856
|
-
provider: "codex",
|
|
1857
|
-
displayName: String(displayName),
|
|
1858
|
-
reasoningEfforts: reasoningEfforts.length ? reasoningEfforts : void 0,
|
|
1859
|
-
defaultReasoningEffort: defaultReasoningEffort || void 0
|
|
1860
|
-
});
|
|
1861
|
-
}
|
|
1862
|
-
if (!mapped.length) return [];
|
|
1863
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1864
|
-
return mapped.filter((model) => {
|
|
1865
|
-
const key = model.id;
|
|
1866
|
-
if (seen.has(key)) return false;
|
|
1867
|
-
seen.add(key);
|
|
1868
|
-
return true;
|
|
1869
|
-
});
|
|
1870
|
-
}
|
|
1871
|
-
async function fetchCodexModels(command2) {
|
|
1872
|
-
return new Promise((resolve) => {
|
|
1873
|
-
const child = spawn3(command2, ["app-server"], {
|
|
1874
|
-
env: { ...process.env },
|
|
1875
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1876
|
-
});
|
|
1877
|
-
let settled = false;
|
|
1878
|
-
const initId = 1;
|
|
1879
|
-
const listId = 2;
|
|
1880
|
-
const timeout = setTimeout(() => {
|
|
1881
|
-
finalize(null);
|
|
1882
|
-
}, 8e3);
|
|
1883
|
-
const finalize = (models) => {
|
|
1884
|
-
if (settled) return;
|
|
1885
|
-
settled = true;
|
|
1886
|
-
clearTimeout(timeout);
|
|
1887
|
-
child.kill("SIGTERM");
|
|
1888
|
-
resolve(models);
|
|
1889
|
-
};
|
|
1890
|
-
const handleLine = (line) => {
|
|
1891
|
-
const parsed = safeJsonParse2(line);
|
|
1892
|
-
if (!parsed || typeof parsed !== "object") return;
|
|
1893
|
-
if (parsed.id === listId && parsed.result) {
|
|
1894
|
-
finalize(normalizeCodexModels(parsed.result));
|
|
1895
|
-
}
|
|
1896
|
-
if (parsed.id === listId && parsed.error) {
|
|
1897
|
-
finalize(null);
|
|
1898
|
-
}
|
|
1899
|
-
};
|
|
1900
|
-
const stdoutParser = createLineParser(handleLine);
|
|
1901
|
-
child.stdout?.on("data", stdoutParser);
|
|
1902
|
-
child.stderr?.on("data", () => {
|
|
1903
|
-
});
|
|
1904
|
-
child.on("error", () => finalize(null));
|
|
1905
|
-
child.on("close", () => finalize(null));
|
|
1906
|
-
if (!child.stdin) {
|
|
1907
|
-
finalize(null);
|
|
1908
|
-
return;
|
|
1909
|
-
}
|
|
1910
|
-
const initialize = {
|
|
1911
|
-
method: "initialize",
|
|
1912
|
-
id: initId,
|
|
1913
|
-
params: {
|
|
1914
|
-
clientInfo: { name: "agentconnect", title: "AgentConnect", version: "0.1.0" }
|
|
1915
|
-
}
|
|
1916
|
-
};
|
|
1917
|
-
const initialized = { method: "initialized" };
|
|
1918
|
-
const listRequest = { method: "model/list", id: listId, params: { cursor: null, limit: null } };
|
|
1919
|
-
const payload = `${JSON.stringify(initialize)}
|
|
1920
|
-
${JSON.stringify(initialized)}
|
|
1921
|
-
${JSON.stringify(
|
|
1922
|
-
listRequest
|
|
1923
|
-
)}
|
|
1924
|
-
`;
|
|
1925
|
-
child.stdin.write(payload);
|
|
1926
|
-
});
|
|
1927
|
-
}
|
|
1928
|
-
async function listCodexModels() {
|
|
1929
|
-
if (codexModelsCache && Date.now() - codexModelsCacheAt < CODEX_MODELS_CACHE_TTL_MS) {
|
|
1930
|
-
return codexModelsCache;
|
|
1931
|
-
}
|
|
1932
|
-
const command2 = getCodexCommand();
|
|
1933
|
-
const versionCheck = await checkCommandVersion(command2, [["--version"], ["-V"]]);
|
|
1934
|
-
if (!versionCheck.ok) {
|
|
1935
|
-
return [];
|
|
1936
|
-
}
|
|
1937
|
-
const models = await fetchCodexModels(command2);
|
|
1938
|
-
if (models && models.length) {
|
|
1939
|
-
codexModelsCache = models;
|
|
1940
|
-
codexModelsCacheAt = Date.now();
|
|
1941
|
-
return models;
|
|
1942
|
-
}
|
|
1943
|
-
return [];
|
|
1944
|
-
}
|
|
1945
|
-
function runCodexPrompt({
|
|
1946
|
-
prompt,
|
|
1947
|
-
resumeSessionId,
|
|
1948
|
-
model,
|
|
1949
|
-
reasoningEffort,
|
|
1950
|
-
repoRoot,
|
|
1951
|
-
cwd,
|
|
1952
|
-
providerDetailLevel,
|
|
1953
|
-
onEvent,
|
|
1954
|
-
signal
|
|
1955
|
-
}) {
|
|
1956
|
-
return new Promise((resolve) => {
|
|
1957
|
-
const command2 = getCodexCommand();
|
|
1958
|
-
const resolvedRepoRoot = repoRoot ? path3.resolve(repoRoot) : null;
|
|
1959
|
-
const resolvedCwd = cwd ? path3.resolve(cwd) : null;
|
|
1960
|
-
const runDir = resolvedCwd || resolvedRepoRoot || process.cwd();
|
|
1961
|
-
const cdTarget = resolvedRepoRoot || resolvedCwd || ".";
|
|
1962
|
-
const runAttempt = (mode) => new Promise((attemptResolve) => {
|
|
1963
|
-
const args2 = buildCodexExecArgs({
|
|
1964
|
-
prompt,
|
|
1965
|
-
cdTarget,
|
|
1966
|
-
resumeSessionId,
|
|
1967
|
-
model,
|
|
1968
|
-
reasoningEffort,
|
|
1969
|
-
mode
|
|
1970
|
-
});
|
|
1971
|
-
const argsPreview = [...args2];
|
|
1972
|
-
if (argsPreview.length > 0) {
|
|
1973
|
-
argsPreview[argsPreview.length - 1] = "[prompt]";
|
|
1974
|
-
}
|
|
1975
|
-
debugLog("Codex", "spawn", { command: command2, args: argsPreview, cwd: runDir, mode });
|
|
1976
|
-
const child = spawn3(command2, args2, {
|
|
1977
|
-
cwd: runDir,
|
|
1978
|
-
env: { ...process.env },
|
|
1979
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1980
|
-
});
|
|
1981
|
-
if (signal) {
|
|
1982
|
-
signal.addEventListener("abort", () => {
|
|
1983
|
-
child.kill("SIGTERM");
|
|
1984
|
-
});
|
|
1985
|
-
}
|
|
1986
|
-
let aggregated = "";
|
|
1987
|
-
let finalSessionId = null;
|
|
1988
|
-
let didFinalize = false;
|
|
1989
|
-
let sawError = false;
|
|
1990
|
-
const includeRaw = providerDetailLevel === "raw";
|
|
1991
|
-
const buildProviderDetail = (eventType, data, raw) => {
|
|
1992
|
-
const detail = { eventType };
|
|
1993
|
-
if (data && Object.keys(data).length) detail.data = data;
|
|
1994
|
-
if (includeRaw && raw !== void 0) detail.raw = raw;
|
|
1995
|
-
return detail;
|
|
1996
|
-
};
|
|
1997
|
-
const emit = (event) => {
|
|
1998
|
-
if (finalSessionId) {
|
|
1999
|
-
onEvent({ ...event, providerSessionId: finalSessionId });
|
|
2000
|
-
} else {
|
|
2001
|
-
onEvent(event);
|
|
2002
|
-
}
|
|
2003
|
-
};
|
|
2004
|
-
const emitError = (message, providerDetail) => {
|
|
2005
|
-
if (sawError) return;
|
|
2006
|
-
sawError = true;
|
|
2007
|
-
emit({ type: "error", message, providerDetail });
|
|
2008
|
-
};
|
|
2009
|
-
const emitItemEvent = (item, phase) => {
|
|
2010
|
-
const itemType = typeof item.type === "string" ? item.type : "";
|
|
2011
|
-
if (!itemType) return;
|
|
2012
|
-
const providerDetail = buildProviderDetail(
|
|
2013
|
-
phase === "start" ? "item.started" : "item.completed",
|
|
2014
|
-
{
|
|
2015
|
-
itemType,
|
|
2016
|
-
itemId: item.id,
|
|
2017
|
-
status: item.status
|
|
2018
|
-
},
|
|
2019
|
-
item
|
|
2020
|
-
);
|
|
2021
|
-
if (itemType === "agent_message") {
|
|
2022
|
-
if (phase === "completed" && typeof item.text === "string") {
|
|
2023
|
-
emit({
|
|
2024
|
-
type: "message",
|
|
2025
|
-
provider: "codex",
|
|
2026
|
-
role: "assistant",
|
|
2027
|
-
content: item.text,
|
|
2028
|
-
contentParts: item,
|
|
2029
|
-
providerDetail
|
|
2030
|
-
});
|
|
2031
|
-
}
|
|
2032
|
-
return;
|
|
2033
|
-
}
|
|
2034
|
-
if (itemType === "reasoning") {
|
|
2035
|
-
emit({
|
|
2036
|
-
type: "thinking",
|
|
2037
|
-
provider: "codex",
|
|
2038
|
-
phase,
|
|
2039
|
-
text: typeof item.text === "string" ? item.text : void 0,
|
|
2040
|
-
providerDetail
|
|
2041
|
-
});
|
|
2042
|
-
return;
|
|
2043
|
-
}
|
|
2044
|
-
if (itemType === "command_execution") {
|
|
2045
|
-
const output = phase === "completed" ? {
|
|
2046
|
-
output: item.aggregated_output,
|
|
2047
|
-
exitCode: item.exit_code,
|
|
2048
|
-
status: item.status
|
|
2049
|
-
} : void 0;
|
|
2050
|
-
emit({
|
|
2051
|
-
type: "tool_call",
|
|
2052
|
-
provider: "codex",
|
|
2053
|
-
name: "command_execution",
|
|
2054
|
-
callId: item.id,
|
|
2055
|
-
input: { command: item.command },
|
|
2056
|
-
output,
|
|
2057
|
-
phase,
|
|
2058
|
-
providerDetail
|
|
2059
|
-
});
|
|
2060
|
-
return;
|
|
2061
|
-
}
|
|
2062
|
-
emit({
|
|
2063
|
-
type: "tool_call",
|
|
2064
|
-
provider: "codex",
|
|
2065
|
-
name: itemType,
|
|
2066
|
-
callId: item.id,
|
|
2067
|
-
input: phase === "start" ? item : void 0,
|
|
2068
|
-
output: phase === "completed" ? item : void 0,
|
|
2069
|
-
phase,
|
|
2070
|
-
providerDetail
|
|
2071
|
-
});
|
|
2072
|
-
};
|
|
2073
|
-
let sawJson = false;
|
|
2074
|
-
const stdoutLines = [];
|
|
2075
|
-
const stderrLines = [];
|
|
2076
|
-
const pushLine = (list, line) => {
|
|
2077
|
-
if (!line) return;
|
|
2078
|
-
list.push(line);
|
|
2079
|
-
if (list.length > 12) list.shift();
|
|
2080
|
-
};
|
|
2081
|
-
const emitFinal = (text, providerDetail) => {
|
|
2082
|
-
emit({ type: "final", text, providerDetail });
|
|
2083
|
-
};
|
|
2084
|
-
const handleLine = (line, source) => {
|
|
2085
|
-
const parsed = safeJsonParse2(line);
|
|
2086
|
-
if (!parsed || typeof parsed !== "object") {
|
|
2087
|
-
if (line.trim()) {
|
|
2088
|
-
emit({ type: "raw_line", line });
|
|
2089
|
-
}
|
|
2090
|
-
if (source === "stdout") {
|
|
2091
|
-
pushLine(stdoutLines, line);
|
|
2092
|
-
} else {
|
|
2093
|
-
pushLine(stderrLines, line);
|
|
2094
|
-
}
|
|
2095
|
-
return;
|
|
2096
|
-
}
|
|
2097
|
-
sawJson = true;
|
|
2098
|
-
const ev = parsed;
|
|
2099
|
-
const normalized = normalizeEvent(ev);
|
|
2100
|
-
const sid = extractSessionId2(ev);
|
|
2101
|
-
if (sid) finalSessionId = sid;
|
|
2102
|
-
const eventType = typeof ev.type === "string" ? ev.type : normalized.type;
|
|
2103
|
-
const detailData = {};
|
|
2104
|
-
const threadId = ev.thread_id ?? ev.threadId;
|
|
2105
|
-
if (typeof threadId === "string" && threadId) detailData.threadId = threadId;
|
|
2106
|
-
const providerDetail = buildProviderDetail(eventType || "unknown", detailData, ev);
|
|
2107
|
-
let handled = false;
|
|
2108
|
-
const usage = extractUsage(ev);
|
|
2109
|
-
if (usage) {
|
|
2110
|
-
emit({
|
|
2111
|
-
type: "usage",
|
|
2112
|
-
inputTokens: usage.input_tokens,
|
|
2113
|
-
outputTokens: usage.output_tokens,
|
|
2114
|
-
providerDetail
|
|
2115
|
-
});
|
|
2116
|
-
handled = true;
|
|
2117
|
-
}
|
|
2118
|
-
if (normalized.type === "agent_message") {
|
|
2119
|
-
const text = normalized.text;
|
|
2120
|
-
if (typeof text === "string" && text) {
|
|
2121
|
-
aggregated += text;
|
|
2122
|
-
emit({ type: "delta", text, providerDetail });
|
|
2123
|
-
emit({
|
|
2124
|
-
type: "message",
|
|
2125
|
-
provider: "codex",
|
|
2126
|
-
role: "assistant",
|
|
2127
|
-
content: text,
|
|
2128
|
-
contentParts: ev,
|
|
2129
|
-
providerDetail
|
|
2130
|
-
});
|
|
2131
|
-
handled = true;
|
|
2132
|
-
}
|
|
2133
|
-
} else if (normalized.type === "item.completed") {
|
|
2134
|
-
const item = normalized.item;
|
|
2135
|
-
if (item && typeof item === "object") {
|
|
2136
|
-
const itemDetail = buildProviderDetail("item.completed", {
|
|
2137
|
-
itemType: item.type,
|
|
2138
|
-
itemId: item.id,
|
|
2139
|
-
status: item.status
|
|
2140
|
-
}, item);
|
|
2141
|
-
if (item.type === "command_execution" && typeof item.aggregated_output === "string") {
|
|
2142
|
-
emit({ type: "delta", text: item.aggregated_output, providerDetail: itemDetail });
|
|
2143
|
-
}
|
|
2144
|
-
if (item.type === "agent_message" && typeof item.text === "string") {
|
|
2145
|
-
aggregated += item.text;
|
|
2146
|
-
emit({ type: "delta", text: item.text, providerDetail: itemDetail });
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
if (normalized.type === "item.started" && ev.item && typeof ev.item === "object") {
|
|
2151
|
-
emitItemEvent(ev.item, "start");
|
|
2152
|
-
handled = true;
|
|
2153
|
-
}
|
|
2154
|
-
if (normalized.type === "item.completed" && ev.item && typeof ev.item === "object") {
|
|
2155
|
-
emitItemEvent(ev.item, "completed");
|
|
2156
|
-
handled = true;
|
|
2157
|
-
}
|
|
2158
|
-
if (normalized.type === "error") {
|
|
2159
|
-
emitError(normalized.message || "Codex run failed", providerDetail);
|
|
2160
|
-
handled = true;
|
|
2161
|
-
}
|
|
2162
|
-
if (isTerminalEvent(ev) && !didFinalize) {
|
|
2163
|
-
if (ev.type === "turn.failed") {
|
|
2164
|
-
const message = ev.error?.message;
|
|
2165
|
-
emitError(typeof message === "string" ? message : "Codex run failed", providerDetail);
|
|
2166
|
-
didFinalize = true;
|
|
2167
|
-
handled = true;
|
|
2168
|
-
return;
|
|
2169
|
-
}
|
|
2170
|
-
if (!sawError) {
|
|
2171
|
-
didFinalize = true;
|
|
2172
|
-
emitFinal(aggregated, providerDetail);
|
|
2173
|
-
handled = true;
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
if (!handled) {
|
|
2177
|
-
emit({ type: "detail", provider: "codex", providerDetail });
|
|
2178
|
-
}
|
|
2179
|
-
};
|
|
2180
|
-
const stdoutParser = createLineParser((line) => handleLine(line, "stdout"));
|
|
2181
|
-
const stderrParser = createLineParser((line) => handleLine(line, "stderr"));
|
|
2182
|
-
child.stdout?.on("data", stdoutParser);
|
|
2183
|
-
child.stderr?.on("data", stderrParser);
|
|
2184
|
-
child.on("close", (code) => {
|
|
2185
|
-
if (!didFinalize) {
|
|
2186
|
-
if (code && code !== 0) {
|
|
2187
|
-
const hint = stderrLines.at(-1) || stdoutLines.at(-1) || "";
|
|
2188
|
-
const suffix = hint ? `: ${hint}` : "";
|
|
2189
|
-
const fallback = mode === "modern" && !sawJson && shouldFallbackToLegacy([
|
|
2190
|
-
...stderrLines,
|
|
2191
|
-
...stdoutLines
|
|
2192
|
-
]);
|
|
2193
|
-
debugLog("Codex", "exit", {
|
|
2194
|
-
code,
|
|
2195
|
-
stderr: stderrLines,
|
|
2196
|
-
stdout: stdoutLines,
|
|
2197
|
-
fallback
|
|
2198
|
-
});
|
|
2199
|
-
if (fallback) {
|
|
2200
|
-
attemptResolve({ sessionId: finalSessionId, fallback: true });
|
|
2201
|
-
return;
|
|
2202
|
-
}
|
|
2203
|
-
emitError(`Codex exited with code ${code}${suffix}`);
|
|
2204
|
-
} else if (!sawError) {
|
|
2205
|
-
emitFinal(aggregated);
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
attemptResolve({ sessionId: finalSessionId, fallback: false });
|
|
2209
|
-
});
|
|
2210
|
-
child.on("error", (err) => {
|
|
2211
|
-
debugLog("Codex", "spawn-error", { message: err?.message });
|
|
2212
|
-
emitError(err?.message ?? "Codex failed to start");
|
|
2213
|
-
attemptResolve({ sessionId: finalSessionId, fallback: false });
|
|
2214
|
-
});
|
|
2215
|
-
});
|
|
2216
|
-
void (async () => {
|
|
2217
|
-
const primary = await runAttempt("modern");
|
|
2218
|
-
if (primary.fallback) {
|
|
2219
|
-
debugLog("Codex", "fallback", { from: "modern", to: "legacy" });
|
|
2220
|
-
const legacy = await runAttempt("legacy");
|
|
2221
|
-
resolve({ sessionId: legacy.sessionId });
|
|
2222
|
-
return;
|
|
2223
|
-
}
|
|
2224
|
-
resolve({ sessionId: primary.sessionId });
|
|
2225
|
-
})();
|
|
2226
|
-
});
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
// src/providers/cursor.ts
|
|
2230
|
-
import { spawn as spawn4 } from "child_process";
|
|
2231
|
-
import path4 from "path";
|
|
2232
|
-
var INSTALL_UNIX2 = "curl https://cursor.com/install -fsS | bash";
|
|
2233
|
-
var DEFAULT_LOGIN3 = "cursor-agent login";
|
|
2234
|
-
var DEFAULT_STATUS3 = "cursor-agent status";
|
|
2235
|
-
var CURSOR_MODELS_COMMAND = "cursor-agent models";
|
|
2236
|
-
var CURSOR_MODELS_CACHE_TTL_MS = 6e4;
|
|
2237
|
-
var CURSOR_UPDATE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
2238
|
-
var cursorModelsCache = null;
|
|
2239
|
-
var cursorModelsCacheAt = 0;
|
|
2240
|
-
var cursorUpdateCache = null;
|
|
2241
|
-
var cursorUpdatePromise = null;
|
|
2242
|
-
function trimOutput3(value, limit = 400) {
|
|
2243
|
-
const cleaned = value.trim();
|
|
2244
|
-
if (!cleaned) return "";
|
|
2245
|
-
if (cleaned.length <= limit) return cleaned;
|
|
2246
|
-
return `${cleaned.slice(0, limit)}...`;
|
|
2247
|
-
}
|
|
2248
|
-
function normalizePath3(value) {
|
|
2249
|
-
const normalized = value.replace(/\\/g, "/");
|
|
2250
|
-
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
2251
|
-
}
|
|
2252
|
-
function parseSemver3(value) {
|
|
2253
|
-
if (!value) return null;
|
|
2254
|
-
const match = value.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
2255
|
-
if (!match) return null;
|
|
2256
|
-
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
2257
|
-
}
|
|
2258
|
-
function compareSemver3(a, b) {
|
|
2259
|
-
if (a[0] !== b[0]) return a[0] - b[0];
|
|
2260
|
-
if (a[1] !== b[1]) return a[1] - b[1];
|
|
2261
|
-
return a[2] - b[2];
|
|
2262
|
-
}
|
|
2263
|
-
async function fetchBrewCaskVersion2(cask) {
|
|
2264
|
-
if (!commandExists("brew")) return null;
|
|
2265
|
-
const result = await runCommand("brew", ["info", "--json=v2", "--cask", cask]);
|
|
2266
|
-
if (result.code !== 0) return null;
|
|
2267
|
-
try {
|
|
2268
|
-
const parsed = JSON.parse(result.stdout);
|
|
2269
|
-
const version = parsed?.casks?.[0]?.version;
|
|
2270
|
-
return typeof version === "string" ? version : null;
|
|
2271
|
-
} catch {
|
|
2272
|
-
return null;
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
function getCursorUpdateAction(commandPath) {
|
|
2276
|
-
if (!commandPath) return null;
|
|
2277
|
-
const normalized = normalizePath3(commandPath);
|
|
2278
|
-
if (normalized.includes("/cellar/") || normalized.includes("/caskroom/") || normalized.includes("/homebrew/")) {
|
|
2279
|
-
return {
|
|
2280
|
-
command: "brew",
|
|
2281
|
-
args: ["upgrade", "--cask", "cursor"],
|
|
2282
|
-
source: "brew",
|
|
2283
|
-
commandLabel: "brew upgrade --cask cursor"
|
|
2284
|
-
};
|
|
2285
|
-
}
|
|
2286
|
-
if (normalized.includes("/.local/bin/") || normalized.includes("/.local/share/cursor-agent/versions/")) {
|
|
2287
|
-
return {
|
|
2288
|
-
command: "bash",
|
|
2289
|
-
args: ["-lc", INSTALL_UNIX2],
|
|
2290
|
-
source: "script",
|
|
2291
|
-
commandLabel: INSTALL_UNIX2
|
|
2292
|
-
};
|
|
2293
|
-
}
|
|
2294
|
-
return null;
|
|
2295
|
-
}
|
|
2296
|
-
function getCursorCommand() {
|
|
2297
|
-
const override = process.env.AGENTCONNECT_CURSOR_COMMAND;
|
|
2298
|
-
const base = override || "cursor-agent";
|
|
2299
|
-
const resolved = resolveCommandPath(base);
|
|
2300
|
-
return resolved || resolveWindowsCommand(base);
|
|
2301
|
-
}
|
|
2302
|
-
function getCursorApiKey() {
|
|
2303
|
-
return process.env.CURSOR_API_KEY || process.env.AGENTCONNECT_CURSOR_API_KEY || "";
|
|
2304
|
-
}
|
|
2305
|
-
function getCursorDefaultModel() {
|
|
2306
|
-
return process.env.AGENTCONNECT_CURSOR_MODEL?.trim() || "";
|
|
2307
|
-
}
|
|
2308
|
-
function resolveCursorEndpoint() {
|
|
2309
|
-
return process.env.AGENTCONNECT_CURSOR_ENDPOINT?.trim() || "";
|
|
2310
|
-
}
|
|
2311
|
-
function withCursorEndpoint(args2) {
|
|
2312
|
-
const endpoint = resolveCursorEndpoint();
|
|
2313
|
-
if (!endpoint) return args2;
|
|
2314
|
-
if (args2.includes("--endpoint")) return args2;
|
|
2315
|
-
return [...args2, "--endpoint", endpoint];
|
|
2316
|
-
}
|
|
2317
|
-
function resolveCursorModel(model, fallback) {
|
|
2318
|
-
if (!model) return fallback;
|
|
2319
|
-
const raw = String(model);
|
|
2320
|
-
if (raw === "cursor" || raw === "cursor-default") return fallback;
|
|
2321
|
-
if (raw.startsWith("cursor:")) return raw.slice("cursor:".length);
|
|
2322
|
-
if (raw.startsWith("cursor/")) return raw.slice("cursor/".length);
|
|
2323
|
-
return raw;
|
|
2324
|
-
}
|
|
2325
|
-
function formatCursorDefaultLabel(fallback) {
|
|
2326
|
-
if (!fallback) return "Default";
|
|
2327
|
-
return `Default \xB7 ${fallback}`;
|
|
2328
|
-
}
|
|
2329
|
-
function normalizeCursorModelId(value) {
|
|
2330
|
-
if (value.startsWith("cursor:") || value.startsWith("cursor/")) return value;
|
|
2331
|
-
return `cursor:${value}`;
|
|
2332
|
-
}
|
|
2333
|
-
function normalizeCursorModelDisplay(value) {
|
|
2334
|
-
if (value.startsWith("cursor:")) return value.slice("cursor:".length);
|
|
2335
|
-
if (value.startsWith("cursor/")) return value.slice("cursor/".length);
|
|
2336
|
-
return value;
|
|
2337
|
-
}
|
|
2338
|
-
async function listCursorModelsFromCli() {
|
|
2339
|
-
const command2 = getCursorCommand();
|
|
2340
|
-
if (!commandExists(command2)) return [];
|
|
2341
|
-
const modelsCommand = buildStatusCommand(
|
|
2342
|
-
"AGENTCONNECT_CURSOR_MODELS_COMMAND",
|
|
2343
|
-
CURSOR_MODELS_COMMAND
|
|
2344
|
-
);
|
|
2345
|
-
if (!modelsCommand.command) return [];
|
|
2346
|
-
const resolvedCommand = resolveWindowsCommand(modelsCommand.command);
|
|
2347
|
-
const result = await runCommand(resolvedCommand, withCursorEndpoint(modelsCommand.args), {
|
|
2348
|
-
env: buildCursorEnv()
|
|
2349
|
-
});
|
|
2350
|
-
const output = `${result.stdout}
|
|
2351
|
-
${result.stderr}`.trim();
|
|
2352
|
-
if (!output) return [];
|
|
2353
|
-
const parsed = safeJsonParse3(output);
|
|
2354
|
-
if (Array.isArray(parsed)) {
|
|
2355
|
-
const models = parsed.map((entry) => {
|
|
2356
|
-
if (typeof entry === "string" && entry.trim()) {
|
|
2357
|
-
const value = entry.trim();
|
|
2358
|
-
return {
|
|
2359
|
-
id: normalizeCursorModelId(value),
|
|
2360
|
-
provider: "cursor",
|
|
2361
|
-
displayName: normalizeCursorModelDisplay(value)
|
|
2362
|
-
};
|
|
2363
|
-
}
|
|
2364
|
-
if (entry && typeof entry === "object") {
|
|
2365
|
-
const record = entry;
|
|
2366
|
-
const idRaw = typeof record.id === "string" ? record.id.trim() : "";
|
|
2367
|
-
const nameRaw = typeof record.name === "string" ? record.name.trim() : "";
|
|
2368
|
-
const displayRaw = typeof record.displayName === "string" ? record.displayName.trim() : "";
|
|
2369
|
-
const value = idRaw || nameRaw || displayRaw;
|
|
2370
|
-
if (!value) return null;
|
|
2371
|
-
return {
|
|
2372
|
-
id: normalizeCursorModelId(value),
|
|
2373
|
-
provider: "cursor",
|
|
2374
|
-
displayName: normalizeCursorModelDisplay(displayRaw || nameRaw || value)
|
|
2375
|
-
};
|
|
2376
|
-
}
|
|
2377
|
-
return null;
|
|
2378
|
-
}).filter(Boolean);
|
|
2379
|
-
return models;
|
|
2380
|
-
}
|
|
2381
|
-
const lines = output.split("\n").map((line) => line.trim()).filter(Boolean).filter((line) => !line.toLowerCase().startsWith("model")).filter((line) => !/^[-=]{2,}$/.test(line));
|
|
2382
|
-
return lines.map((line) => {
|
|
2383
|
-
const cleaned = line.replace(/^[-*•]\s*/, "");
|
|
2384
|
-
const value = cleaned.split(/\s+/)[0] || cleaned;
|
|
2385
|
-
return {
|
|
2386
|
-
id: normalizeCursorModelId(value),
|
|
2387
|
-
provider: "cursor",
|
|
2388
|
-
displayName: normalizeCursorModelDisplay(value)
|
|
2389
|
-
};
|
|
2390
|
-
});
|
|
2391
|
-
}
|
|
2392
|
-
async function listCursorModels() {
|
|
2393
|
-
if (cursorModelsCache && Date.now() - cursorModelsCacheAt < CURSOR_MODELS_CACHE_TTL_MS) {
|
|
2394
|
-
return cursorModelsCache;
|
|
2395
|
-
}
|
|
2396
|
-
const fallback = getCursorDefaultModel();
|
|
2397
|
-
const base = [
|
|
2398
|
-
{
|
|
2399
|
-
id: "cursor-default",
|
|
2400
|
-
provider: "cursor",
|
|
2401
|
-
displayName: formatCursorDefaultLabel(fallback)
|
|
2402
|
-
}
|
|
2403
|
-
];
|
|
2404
|
-
const envModels = process.env.AGENTCONNECT_CURSOR_MODELS;
|
|
2405
|
-
if (envModels) {
|
|
2406
|
-
try {
|
|
2407
|
-
const parsed = JSON.parse(envModels);
|
|
2408
|
-
if (Array.isArray(parsed)) {
|
|
2409
|
-
for (const entry of parsed) {
|
|
2410
|
-
if (typeof entry === "string" && entry.trim()) {
|
|
2411
|
-
const trimmed = entry.trim();
|
|
2412
|
-
base.push({
|
|
2413
|
-
id: normalizeCursorModelId(trimmed),
|
|
2414
|
-
provider: "cursor",
|
|
2415
|
-
displayName: normalizeCursorModelDisplay(trimmed)
|
|
2416
|
-
});
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
} catch {
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
const cliModels = await listCursorModelsFromCli();
|
|
2424
|
-
base.push(...cliModels);
|
|
2425
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2426
|
-
const list = base.filter((entry) => {
|
|
2427
|
-
const key = `${entry.provider}:${entry.id}`;
|
|
2428
|
-
if (seen.has(key)) return false;
|
|
2429
|
-
seen.add(key);
|
|
2430
|
-
return true;
|
|
2431
|
-
});
|
|
2432
|
-
cursorModelsCache = list;
|
|
2433
|
-
cursorModelsCacheAt = Date.now();
|
|
2434
|
-
return list;
|
|
2435
|
-
}
|
|
2436
|
-
function normalizeCursorStatusOutput(output) {
|
|
2437
|
-
const text = output.toLowerCase();
|
|
2438
|
-
if (text.includes("not authenticated") || text.includes("not logged in") || text.includes("login required") || text.includes("please login") || text.includes("please log in") || text.includes("unauthorized")) {
|
|
2439
|
-
return false;
|
|
2440
|
-
}
|
|
2441
|
-
if (text.includes("authenticated") || text.includes("logged in") || text.includes("signed in") || text.includes("account")) {
|
|
2442
|
-
return true;
|
|
2443
|
-
}
|
|
2444
|
-
return null;
|
|
2445
|
-
}
|
|
2446
|
-
function getCursorUpdateSnapshot(commandPath) {
|
|
2447
|
-
if (cursorUpdateCache && Date.now() - cursorUpdateCache.checkedAt < CURSOR_UPDATE_CACHE_TTL_MS) {
|
|
2448
|
-
const action = getCursorUpdateAction(commandPath);
|
|
2449
|
-
return {
|
|
2450
|
-
updateAvailable: cursorUpdateCache.updateAvailable,
|
|
2451
|
-
latestVersion: cursorUpdateCache.latestVersion,
|
|
2452
|
-
updateCheckedAt: cursorUpdateCache.checkedAt,
|
|
2453
|
-
updateSource: action?.source ?? "unknown",
|
|
2454
|
-
updateCommand: action?.commandLabel,
|
|
2455
|
-
updateMessage: cursorUpdateCache.updateMessage
|
|
2456
|
-
};
|
|
2457
|
-
}
|
|
2458
|
-
return {};
|
|
2459
|
-
}
|
|
2460
|
-
function ensureCursorUpdateCheck(currentVersion, commandPath) {
|
|
2461
|
-
if (cursorUpdateCache && Date.now() - cursorUpdateCache.checkedAt < CURSOR_UPDATE_CACHE_TTL_MS) {
|
|
2462
|
-
return;
|
|
2463
|
-
}
|
|
2464
|
-
if (cursorUpdatePromise) return;
|
|
2465
|
-
cursorUpdatePromise = (async () => {
|
|
2466
|
-
const action = getCursorUpdateAction(commandPath || null);
|
|
2467
|
-
let latest = null;
|
|
2468
|
-
let updateAvailable;
|
|
2469
|
-
let updateMessage;
|
|
2470
|
-
if (action?.source === "brew") {
|
|
2471
|
-
latest = await fetchBrewCaskVersion2("cursor");
|
|
2472
|
-
}
|
|
2473
|
-
if (latest && currentVersion) {
|
|
2474
|
-
const a = parseSemver3(currentVersion);
|
|
2475
|
-
const b = parseSemver3(latest);
|
|
2476
|
-
if (a && b) {
|
|
2477
|
-
updateAvailable = compareSemver3(a, b) < 0;
|
|
2478
|
-
updateMessage = updateAvailable ? `Update available: ${currentVersion} -> ${latest}` : `Up to date (${currentVersion})`;
|
|
2479
|
-
}
|
|
2480
|
-
} else if (!action) {
|
|
2481
|
-
updateMessage = "Update check unavailable";
|
|
2482
|
-
}
|
|
2483
|
-
debugLog("Cursor", "update-check", {
|
|
2484
|
-
updateAvailable,
|
|
2485
|
-
message: updateMessage
|
|
2486
|
-
});
|
|
2487
|
-
cursorUpdateCache = {
|
|
2488
|
-
checkedAt: Date.now(),
|
|
2489
|
-
updateAvailable,
|
|
2490
|
-
latestVersion: latest ?? void 0,
|
|
2491
|
-
updateMessage
|
|
2492
|
-
};
|
|
2493
|
-
})().finally(() => {
|
|
2494
|
-
cursorUpdatePromise = null;
|
|
2495
|
-
});
|
|
2496
|
-
}
|
|
2497
|
-
async function ensureCursorInstalled() {
|
|
2498
|
-
const command2 = getCursorCommand();
|
|
2499
|
-
const versionCheck = await checkCommandVersion(command2, [["--version"], ["-V"]]);
|
|
2500
|
-
debugLog("Cursor", "install-check", {
|
|
2501
|
-
command: command2,
|
|
2502
|
-
versionOk: versionCheck.ok,
|
|
2503
|
-
version: versionCheck.version
|
|
2504
|
-
});
|
|
2505
|
-
if (versionCheck.ok) {
|
|
2506
|
-
return { installed: true, version: versionCheck.version || void 0 };
|
|
2507
|
-
}
|
|
2508
|
-
if (commandExists(command2)) {
|
|
2509
|
-
return { installed: true, version: void 0 };
|
|
2510
|
-
}
|
|
2511
|
-
const override = buildInstallCommand("AGENTCONNECT_CURSOR_INSTALL", "");
|
|
2512
|
-
let install = override;
|
|
2513
|
-
let packageManager = override.command ? "unknown" : "unknown";
|
|
2514
|
-
if (!install.command) {
|
|
2515
|
-
if (process.platform !== "win32" && commandExists("bash") && commandExists("curl")) {
|
|
2516
|
-
install = { command: "bash", args: ["-lc", INSTALL_UNIX2] };
|
|
2517
|
-
packageManager = "script";
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
if (!install.command) {
|
|
2521
|
-
return { installed: false, version: void 0, packageManager };
|
|
2522
|
-
}
|
|
2523
|
-
await runCommand(install.command, install.args, { shell: process.platform === "win32" });
|
|
2524
|
-
const after = await checkCommandVersion(command2, [["--version"], ["-V"]]);
|
|
2525
|
-
return {
|
|
2526
|
-
installed: after.ok,
|
|
2527
|
-
version: after.version || void 0,
|
|
2528
|
-
packageManager
|
|
2529
|
-
};
|
|
2530
|
-
}
|
|
2531
|
-
async function getCursorStatus() {
|
|
2532
|
-
const command2 = getCursorCommand();
|
|
2533
|
-
const versionCheck = await checkCommandVersion(command2, [["--version"], ["-V"]]);
|
|
2534
|
-
const installed = versionCheck.ok || commandExists(command2);
|
|
2535
|
-
let loggedIn = false;
|
|
2536
|
-
if (installed) {
|
|
2537
|
-
const status = buildStatusCommand("AGENTCONNECT_CURSOR_STATUS", DEFAULT_STATUS3);
|
|
2538
|
-
if (status.command) {
|
|
2539
|
-
const statusCommand = resolveWindowsCommand(status.command);
|
|
2540
|
-
const result = await runCommand(statusCommand, withCursorEndpoint(status.args), {
|
|
2541
|
-
env: buildCursorEnv()
|
|
2542
|
-
});
|
|
2543
|
-
const output = `${result.stdout}
|
|
2544
|
-
${result.stderr}`;
|
|
2545
|
-
const parsed = normalizeCursorStatusOutput(output);
|
|
2546
|
-
loggedIn = parsed ?? result.code === 0;
|
|
2547
|
-
}
|
|
2548
|
-
if (!loggedIn) {
|
|
2549
|
-
loggedIn = Boolean(getCursorApiKey().trim());
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
if (installed) {
|
|
2553
|
-
const resolved2 = resolveCommandRealPath(command2);
|
|
2554
|
-
ensureCursorUpdateCheck(versionCheck.version, resolved2 || null);
|
|
2555
|
-
}
|
|
2556
|
-
const resolved = resolveCommandRealPath(command2);
|
|
2557
|
-
const updateInfo = installed ? getCursorUpdateSnapshot(resolved || null) : {};
|
|
2558
|
-
return { installed, loggedIn, version: versionCheck.version || void 0, ...updateInfo };
|
|
2559
|
-
}
|
|
2560
|
-
async function getCursorFastStatus() {
|
|
2561
|
-
const command2 = getCursorCommand();
|
|
2562
|
-
const installed = commandExists(command2);
|
|
2563
|
-
if (!installed) {
|
|
2564
|
-
return { installed: false, loggedIn: false };
|
|
2565
|
-
}
|
|
2566
|
-
return { installed: true, loggedIn: true };
|
|
2567
|
-
}
|
|
2568
|
-
async function updateCursor() {
|
|
2569
|
-
const command2 = getCursorCommand();
|
|
2570
|
-
if (!commandExists(command2)) {
|
|
2571
|
-
return { installed: false, loggedIn: false };
|
|
2572
|
-
}
|
|
2573
|
-
const resolved = resolveCommandRealPath(command2);
|
|
2574
|
-
const updateOverride = buildStatusCommand("AGENTCONNECT_CURSOR_UPDATE", "");
|
|
2575
|
-
const action = updateOverride.command ? null : getCursorUpdateAction(resolved || null);
|
|
2576
|
-
const updateCommand = updateOverride.command || action?.command || "";
|
|
2577
|
-
const updateArgs = updateOverride.command ? updateOverride.args : action?.args || [];
|
|
2578
|
-
if (!updateCommand) {
|
|
2579
|
-
throw new Error("No update command available. Please update Cursor manually.");
|
|
2580
|
-
}
|
|
2581
|
-
const cmd = resolveWindowsCommand(updateCommand);
|
|
2582
|
-
debugLog("Cursor", "update-run", { command: cmd, args: updateArgs });
|
|
2583
|
-
const result = await runCommand(cmd, updateArgs, { env: buildCursorEnv() });
|
|
2584
|
-
debugLog("Cursor", "update-result", {
|
|
2585
|
-
code: result.code,
|
|
2586
|
-
stdout: trimOutput3(result.stdout),
|
|
2587
|
-
stderr: trimOutput3(result.stderr)
|
|
2588
|
-
});
|
|
2589
|
-
if (result.code !== 0 && result.code !== null) {
|
|
2590
|
-
const message = trimOutput3(`${result.stdout}
|
|
2591
|
-
${result.stderr}`, 800) || "Update failed";
|
|
2592
|
-
throw new Error(message);
|
|
2593
|
-
}
|
|
2594
|
-
cursorUpdateCache = null;
|
|
2595
|
-
cursorUpdatePromise = null;
|
|
2596
|
-
return getCursorStatus();
|
|
2597
|
-
}
|
|
2598
|
-
function buildCursorEnv() {
|
|
2599
|
-
const env = { ...process.env };
|
|
2600
|
-
const apiKey = getCursorApiKey().trim();
|
|
2601
|
-
if (apiKey) {
|
|
2602
|
-
env.CURSOR_API_KEY = apiKey;
|
|
2603
|
-
}
|
|
2604
|
-
return env;
|
|
2605
|
-
}
|
|
2606
|
-
async function loginCursor(options) {
|
|
2607
|
-
if (typeof options?.apiKey === "string") {
|
|
2608
|
-
process.env.CURSOR_API_KEY = options.apiKey;
|
|
2609
|
-
}
|
|
2610
|
-
if (typeof options?.baseUrl === "string") {
|
|
2611
|
-
process.env.AGENTCONNECT_CURSOR_ENDPOINT = options.baseUrl;
|
|
2612
|
-
}
|
|
2613
|
-
if (typeof options?.model === "string") {
|
|
2614
|
-
process.env.AGENTCONNECT_CURSOR_MODEL = options.model;
|
|
2615
|
-
cursorModelsCache = null;
|
|
2616
|
-
cursorModelsCacheAt = 0;
|
|
2617
|
-
}
|
|
2618
|
-
if (Array.isArray(options?.models)) {
|
|
2619
|
-
process.env.AGENTCONNECT_CURSOR_MODELS = JSON.stringify(options.models.filter(Boolean));
|
|
2620
|
-
cursorModelsCache = null;
|
|
2621
|
-
cursorModelsCacheAt = 0;
|
|
2622
|
-
}
|
|
2623
|
-
if (!options?.apiKey) {
|
|
2624
|
-
const login = buildLoginCommand("AGENTCONNECT_CURSOR_LOGIN", DEFAULT_LOGIN3);
|
|
2625
|
-
if (login.command) {
|
|
2626
|
-
const command2 = resolveWindowsCommand(login.command);
|
|
2627
|
-
await runCommand(command2, withCursorEndpoint(login.args), { env: buildCursorEnv() });
|
|
2628
|
-
}
|
|
2629
|
-
}
|
|
2630
|
-
const timeoutMs = Number(process.env.AGENTCONNECT_CURSOR_LOGIN_TIMEOUT_MS || 2e4);
|
|
2631
|
-
const pollIntervalMs = Number(process.env.AGENTCONNECT_CURSOR_LOGIN_POLL_MS || 1e3);
|
|
2632
|
-
const start = Date.now();
|
|
2633
|
-
let status = await getCursorStatus();
|
|
2634
|
-
while (!status.loggedIn && Date.now() - start < timeoutMs) {
|
|
2635
|
-
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
2636
|
-
status = await getCursorStatus();
|
|
2637
|
-
}
|
|
2638
|
-
return { loggedIn: status.loggedIn };
|
|
2639
|
-
}
|
|
2640
|
-
function safeJsonParse3(line) {
|
|
2641
|
-
try {
|
|
2642
|
-
return JSON.parse(line);
|
|
2643
|
-
} catch {
|
|
2644
|
-
return null;
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
function extractSessionId3(ev) {
|
|
2648
|
-
const id = ev.session_id ?? ev.sessionId;
|
|
2649
|
-
return typeof id === "string" ? id : null;
|
|
2650
|
-
}
|
|
2651
|
-
function extractUsage2(ev) {
|
|
2652
|
-
const usage = ev.usage ?? ev.token_usage ?? ev.tokenUsage ?? ev.tokens;
|
|
2653
|
-
if (!usage || typeof usage !== "object") return null;
|
|
2654
|
-
const toNumber = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
2655
|
-
const input = toNumber(
|
|
2656
|
-
usage.input_tokens ?? usage.prompt_tokens ?? usage.inputTokens ?? usage.promptTokens
|
|
2657
|
-
);
|
|
2658
|
-
const output = toNumber(
|
|
2659
|
-
usage.output_tokens ?? usage.completion_tokens ?? usage.outputTokens ?? usage.completionTokens
|
|
2660
|
-
);
|
|
2661
|
-
const total = toNumber(usage.total_tokens ?? usage.totalTokens);
|
|
2662
|
-
const out = {};
|
|
2663
|
-
if (input !== void 0) out.input_tokens = input;
|
|
2664
|
-
if (output !== void 0) out.output_tokens = output;
|
|
2665
|
-
if (total !== void 0) out.total_tokens = total;
|
|
2666
|
-
return Object.keys(out).length ? out : null;
|
|
2667
|
-
}
|
|
2668
|
-
function extractTextFromContent2(content) {
|
|
2669
|
-
if (!content) return "";
|
|
2670
|
-
if (typeof content === "string") return content;
|
|
2671
|
-
if (Array.isArray(content)) {
|
|
2672
|
-
return content.map((part) => {
|
|
2673
|
-
if (!part) return "";
|
|
2674
|
-
if (typeof part === "string") return part;
|
|
2675
|
-
if (typeof part === "object") {
|
|
2676
|
-
const text = part.text;
|
|
2677
|
-
if (typeof text === "string") return text;
|
|
2678
|
-
}
|
|
2679
|
-
return "";
|
|
2680
|
-
}).join("");
|
|
2681
|
-
}
|
|
2682
|
-
if (typeof content === "object") {
|
|
2683
|
-
const text = content.text;
|
|
2684
|
-
if (typeof text === "string") return text;
|
|
2685
|
-
}
|
|
2686
|
-
return "";
|
|
2687
|
-
}
|
|
2688
|
-
function extractToolCall(ev) {
|
|
2689
|
-
if (ev.type !== "tool_call") return null;
|
|
2690
|
-
const toolCall = ev.tool_call && typeof ev.tool_call === "object" ? ev.tool_call : null;
|
|
2691
|
-
if (!toolCall) return null;
|
|
2692
|
-
const keys = Object.keys(toolCall);
|
|
2693
|
-
if (!keys.length) return null;
|
|
2694
|
-
const name = keys[0];
|
|
2695
|
-
const entry = toolCall[name];
|
|
2696
|
-
const record = entry && typeof entry === "object" ? entry : null;
|
|
2697
|
-
const input = record?.args ?? record?.input ?? void 0;
|
|
2698
|
-
const output = record?.output ?? record?.result ?? void 0;
|
|
2699
|
-
const subtype = typeof ev.subtype === "string" ? ev.subtype : "";
|
|
2700
|
-
const phase = subtype === "completed" ? "completed" : subtype === "started" ? "start" : subtype === "error" ? "error" : subtype === "delta" ? "delta" : void 0;
|
|
2701
|
-
return {
|
|
2702
|
-
name,
|
|
2703
|
-
input,
|
|
2704
|
-
output,
|
|
2705
|
-
callId: typeof ev.call_id === "string" ? ev.call_id : void 0,
|
|
2706
|
-
phase
|
|
2707
|
-
};
|
|
2708
|
-
}
|
|
2709
|
-
function extractTextFromMessage(message) {
|
|
2710
|
-
if (!message || typeof message !== "object") return "";
|
|
2711
|
-
const content = message.content;
|
|
2712
|
-
return extractTextFromContent2(content);
|
|
2713
|
-
}
|
|
2714
|
-
function extractAssistantDelta2(ev) {
|
|
2715
|
-
if (ev.type !== "assistant" && ev.message?.role !== "assistant") return null;
|
|
2716
|
-
if (typeof ev.text === "string") return ev.text;
|
|
2717
|
-
if (typeof ev.delta === "string") return ev.delta;
|
|
2718
|
-
if (ev.message) {
|
|
2719
|
-
const text = extractTextFromMessage(ev.message);
|
|
2720
|
-
return text || null;
|
|
2721
|
-
}
|
|
2722
|
-
if (ev.content) {
|
|
2723
|
-
const text = extractTextFromContent2(ev.content);
|
|
2724
|
-
return text || null;
|
|
2725
|
-
}
|
|
2726
|
-
return null;
|
|
2727
|
-
}
|
|
2728
|
-
function extractResultText2(ev) {
|
|
2729
|
-
if (typeof ev.result === "string") return ev.result;
|
|
2730
|
-
if (ev.result && typeof ev.result === "object") {
|
|
2731
|
-
const result = ev.result;
|
|
2732
|
-
if (typeof result.text === "string") return result.text;
|
|
2733
|
-
if (result.message) {
|
|
2734
|
-
const text = extractTextFromMessage(result.message);
|
|
2735
|
-
if (text) return text;
|
|
2736
|
-
}
|
|
2737
|
-
if (result.content) {
|
|
2738
|
-
const text = extractTextFromContent2(result.content);
|
|
2739
|
-
if (text) return text;
|
|
2740
|
-
}
|
|
2741
|
-
}
|
|
2742
|
-
if (ev.message) {
|
|
2743
|
-
const text = extractTextFromMessage(ev.message);
|
|
2744
|
-
if (text) return text;
|
|
2745
|
-
}
|
|
2746
|
-
return null;
|
|
2747
|
-
}
|
|
2748
|
-
function isErrorEvent(ev) {
|
|
2749
|
-
if (ev.type === "error") return true;
|
|
2750
|
-
if (ev.type === "result" && ev.subtype) {
|
|
2751
|
-
const subtype = String(ev.subtype).toLowerCase();
|
|
2752
|
-
if (subtype.includes("error") || subtype.includes("failed")) return true;
|
|
2753
|
-
}
|
|
2754
|
-
return false;
|
|
2755
|
-
}
|
|
2756
|
-
function extractErrorMessage(ev) {
|
|
2757
|
-
if (typeof ev.error === "string") return ev.error;
|
|
2758
|
-
if (ev.error && typeof ev.error === "object" && typeof ev.error.message === "string") {
|
|
2759
|
-
return ev.error.message;
|
|
2760
|
-
}
|
|
2761
|
-
if (typeof ev.text === "string" && ev.type === "error") return ev.text;
|
|
2762
|
-
if (typeof ev.result === "string" && ev.type === "result") return ev.result;
|
|
2763
|
-
return null;
|
|
2764
|
-
}
|
|
2765
|
-
function runCursorPrompt({
|
|
2766
|
-
prompt,
|
|
2767
|
-
resumeSessionId,
|
|
2768
|
-
model,
|
|
2769
|
-
repoRoot,
|
|
2770
|
-
cwd,
|
|
2771
|
-
providerDetailLevel,
|
|
2772
|
-
onEvent,
|
|
2773
|
-
signal
|
|
2774
|
-
}) {
|
|
2775
|
-
return new Promise((resolve) => {
|
|
2776
|
-
const command2 = getCursorCommand();
|
|
2777
|
-
const resolvedRepoRoot = repoRoot ? path4.resolve(repoRoot) : null;
|
|
2778
|
-
const resolvedCwd = cwd ? path4.resolve(cwd) : null;
|
|
2779
|
-
const runDir = resolvedCwd || resolvedRepoRoot || process.cwd();
|
|
2780
|
-
const args2 = ["--print", "--output-format", "stream-json"];
|
|
2781
|
-
if (resumeSessionId) {
|
|
2782
|
-
args2.push("--resume", resumeSessionId);
|
|
2783
|
-
}
|
|
2784
|
-
const fallbackModel = getCursorDefaultModel();
|
|
2785
|
-
const resolvedModel = resolveCursorModel(model, fallbackModel);
|
|
2786
|
-
if (resolvedModel) {
|
|
2787
|
-
args2.push("--model", resolvedModel);
|
|
2788
|
-
}
|
|
2789
|
-
const endpoint = resolveCursorEndpoint();
|
|
2790
|
-
if (endpoint) {
|
|
2791
|
-
args2.push("--endpoint", endpoint);
|
|
2792
|
-
}
|
|
2793
|
-
args2.push(prompt);
|
|
2794
|
-
const argsPreview = [...args2];
|
|
2795
|
-
if (argsPreview.length > 0) {
|
|
2796
|
-
argsPreview[argsPreview.length - 1] = "[prompt]";
|
|
2797
|
-
}
|
|
2798
|
-
debugLog("Cursor", "spawn", {
|
|
2799
|
-
command: command2,
|
|
2800
|
-
args: argsPreview,
|
|
2801
|
-
cwd: runDir,
|
|
2802
|
-
model: resolvedModel || null,
|
|
2803
|
-
endpoint: endpoint || null,
|
|
2804
|
-
resume: resumeSessionId || null,
|
|
2805
|
-
apiKeyConfigured: Boolean(getCursorApiKey().trim()),
|
|
2806
|
-
promptChars: prompt.length
|
|
2807
|
-
});
|
|
2808
|
-
const child = spawn4(command2, args2, {
|
|
2809
|
-
cwd: runDir,
|
|
2810
|
-
env: buildCursorEnv(),
|
|
2811
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2812
|
-
});
|
|
2813
|
-
if (signal) {
|
|
2814
|
-
signal.addEventListener("abort", () => {
|
|
2815
|
-
child.kill("SIGTERM");
|
|
2816
|
-
});
|
|
2817
|
-
}
|
|
2818
|
-
let aggregated = "";
|
|
2819
|
-
let finalSessionId = null;
|
|
2820
|
-
let didFinalize = false;
|
|
2821
|
-
let sawError = false;
|
|
2822
|
-
let sawJson = false;
|
|
2823
|
-
let rawOutput = "";
|
|
2824
|
-
const stdoutLines = [];
|
|
2825
|
-
const stderrLines = [];
|
|
2826
|
-
const includeRaw = providerDetailLevel === "raw";
|
|
2827
|
-
const buildProviderDetail = (eventType, data, raw) => {
|
|
2828
|
-
const detail = { eventType };
|
|
2829
|
-
if (data && Object.keys(data).length) detail.data = data;
|
|
2830
|
-
if (includeRaw && raw !== void 0) detail.raw = raw;
|
|
2831
|
-
return detail;
|
|
2832
|
-
};
|
|
2833
|
-
const emit = (event) => {
|
|
2834
|
-
if (finalSessionId) {
|
|
2835
|
-
onEvent({ ...event, providerSessionId: finalSessionId });
|
|
2836
|
-
} else {
|
|
2837
|
-
onEvent(event);
|
|
2838
|
-
}
|
|
2839
|
-
};
|
|
2840
|
-
const pushLine = (list, line) => {
|
|
2841
|
-
if (!line) return;
|
|
2842
|
-
list.push(line);
|
|
2843
|
-
if (list.length > 12) list.shift();
|
|
2844
|
-
};
|
|
2845
|
-
const emitError = (message, providerDetail) => {
|
|
2846
|
-
if (sawError) return;
|
|
2847
|
-
sawError = true;
|
|
2848
|
-
emit({ type: "error", message, providerDetail });
|
|
2849
|
-
};
|
|
2850
|
-
const emitFinal = (text) => {
|
|
2851
|
-
if (didFinalize) return;
|
|
2852
|
-
didFinalize = true;
|
|
2853
|
-
emit({ type: "final", text });
|
|
2854
|
-
};
|
|
2855
|
-
const handleEvent = (ev) => {
|
|
2856
|
-
if (ev?.type === "system" && ev?.subtype === "init") {
|
|
2857
|
-
debugLog("Cursor", "init", {
|
|
2858
|
-
apiKeySource: ev.apiKeySource || null,
|
|
2859
|
-
cwd: ev.cwd || null,
|
|
2860
|
-
model: ev.model || null,
|
|
2861
|
-
permissionMode: ev.permissionMode || null,
|
|
2862
|
-
sessionId: ev.session_id ?? ev.sessionId ?? null
|
|
2863
|
-
});
|
|
2864
|
-
emit({
|
|
2865
|
-
type: "detail",
|
|
2866
|
-
provider: "cursor",
|
|
2867
|
-
providerDetail: buildProviderDetail(
|
|
2868
|
-
"system.init",
|
|
2869
|
-
{
|
|
2870
|
-
apiKeySource: ev.apiKeySource,
|
|
2871
|
-
cwd: ev.cwd,
|
|
2872
|
-
model: ev.model,
|
|
2873
|
-
permissionMode: ev.permissionMode
|
|
2874
|
-
},
|
|
2875
|
-
ev
|
|
2876
|
-
)
|
|
2877
|
-
});
|
|
2878
|
-
}
|
|
2879
|
-
const sid = extractSessionId3(ev);
|
|
2880
|
-
if (sid) finalSessionId = sid;
|
|
2881
|
-
if (ev?.type === "thinking") {
|
|
2882
|
-
const subtype = typeof ev.subtype === "string" ? ev.subtype : "";
|
|
2883
|
-
const phase = subtype === "completed" ? "completed" : subtype === "started" ? "start" : subtype === "error" ? "error" : "delta";
|
|
2884
|
-
emit({
|
|
2885
|
-
type: "thinking",
|
|
2886
|
-
provider: "cursor",
|
|
2887
|
-
phase,
|
|
2888
|
-
text: typeof ev.text === "string" ? ev.text : "",
|
|
2889
|
-
timestampMs: typeof ev.timestamp_ms === "number" ? ev.timestamp_ms : void 0,
|
|
2890
|
-
providerDetail: buildProviderDetail(
|
|
2891
|
-
subtype ? `thinking.${subtype}` : "thinking",
|
|
2892
|
-
{
|
|
2893
|
-
subtype: subtype || void 0
|
|
2894
|
-
},
|
|
2895
|
-
ev
|
|
2896
|
-
)
|
|
2897
|
-
});
|
|
2898
|
-
}
|
|
2899
|
-
if (ev?.type === "assistant" || ev?.type === "user") {
|
|
2900
|
-
const role = ev.message?.role === "assistant" || ev.message?.role === "user" ? ev.message?.role : ev.type;
|
|
2901
|
-
const rawContent = ev.message?.content ?? ev.content;
|
|
2902
|
-
const content = extractTextFromContent2(rawContent);
|
|
2903
|
-
emit({
|
|
2904
|
-
type: "message",
|
|
2905
|
-
provider: "cursor",
|
|
2906
|
-
role,
|
|
2907
|
-
content,
|
|
2908
|
-
contentParts: rawContent ?? null,
|
|
2909
|
-
providerDetail: buildProviderDetail(ev.type, {}, ev)
|
|
2910
|
-
});
|
|
2911
|
-
}
|
|
2912
|
-
const toolCall = extractToolCall(ev);
|
|
2913
|
-
if (toolCall) {
|
|
2914
|
-
emit({
|
|
2915
|
-
type: "tool_call",
|
|
2916
|
-
provider: "cursor",
|
|
2917
|
-
name: toolCall.name,
|
|
2918
|
-
callId: toolCall.callId,
|
|
2919
|
-
input: toolCall.input,
|
|
2920
|
-
output: toolCall.output,
|
|
2921
|
-
phase: toolCall.phase,
|
|
2922
|
-
providerDetail: buildProviderDetail(
|
|
2923
|
-
ev.subtype ? `tool_call.${ev.subtype}` : "tool_call",
|
|
2924
|
-
{
|
|
2925
|
-
name: toolCall.name,
|
|
2926
|
-
callId: toolCall.callId,
|
|
2927
|
-
subtype: ev.subtype
|
|
2928
|
-
},
|
|
2929
|
-
ev
|
|
2930
|
-
)
|
|
2931
|
-
});
|
|
2932
|
-
}
|
|
2933
|
-
const usage = extractUsage2(ev);
|
|
2934
|
-
if (usage) {
|
|
2935
|
-
emit({
|
|
2936
|
-
type: "usage",
|
|
2937
|
-
inputTokens: usage.input_tokens,
|
|
2938
|
-
outputTokens: usage.output_tokens
|
|
2939
|
-
});
|
|
2940
|
-
}
|
|
2941
|
-
if (isErrorEvent(ev)) {
|
|
2942
|
-
const message = extractErrorMessage(ev) || "Cursor run failed";
|
|
2943
|
-
emitError(message, buildProviderDetail(ev.subtype ? `error.${ev.subtype}` : "error", {}, ev));
|
|
2944
|
-
return;
|
|
2945
|
-
}
|
|
2946
|
-
const delta = extractAssistantDelta2(ev);
|
|
2947
|
-
if (delta) {
|
|
2948
|
-
aggregated += delta;
|
|
2949
|
-
emit({ type: "delta", text: delta, providerDetail: buildProviderDetail("delta", {}, ev) });
|
|
2950
|
-
}
|
|
2951
|
-
if (ev.type === "result") {
|
|
2952
|
-
const resultText = extractResultText2(ev);
|
|
2953
|
-
if (!aggregated && resultText) {
|
|
2954
|
-
aggregated = resultText;
|
|
2955
|
-
}
|
|
2956
|
-
if (!sawError) {
|
|
2957
|
-
emitFinal(aggregated || resultText || "");
|
|
2958
|
-
emit({
|
|
2959
|
-
type: "detail",
|
|
2960
|
-
provider: "cursor",
|
|
2961
|
-
providerDetail: buildProviderDetail(
|
|
2962
|
-
"result",
|
|
2963
|
-
{
|
|
2964
|
-
subtype: ev.subtype,
|
|
2965
|
-
duration_ms: typeof ev.duration_ms === "number" ? ev.duration_ms : void 0,
|
|
2966
|
-
request_id: typeof ev.request_id === "string" ? ev.request_id : void 0,
|
|
2967
|
-
is_error: typeof ev.is_error === "boolean" ? ev.is_error : void 0
|
|
2968
|
-
},
|
|
2969
|
-
ev
|
|
2970
|
-
)
|
|
2971
|
-
});
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
|
-
};
|
|
2975
|
-
const handleLine = (line, source) => {
|
|
2976
|
-
const trimmed = line.trim();
|
|
2977
|
-
if (!trimmed) return;
|
|
2978
|
-
const payload = trimmed.startsWith("data: ") ? trimmed.slice(6).trim() : trimmed;
|
|
2979
|
-
const parsed = safeJsonParse3(payload);
|
|
2980
|
-
if (!parsed || typeof parsed !== "object") {
|
|
2981
|
-
emit({ type: "raw_line", line });
|
|
2982
|
-
if (source === "stdout") {
|
|
2983
|
-
rawOutput += `${line}
|
|
2984
|
-
`;
|
|
2985
|
-
pushLine(stdoutLines, line);
|
|
2986
|
-
} else {
|
|
2987
|
-
pushLine(stderrLines, line);
|
|
2988
|
-
}
|
|
2989
|
-
return;
|
|
2990
|
-
}
|
|
2991
|
-
sawJson = true;
|
|
2992
|
-
handleEvent(parsed);
|
|
2993
|
-
};
|
|
2994
|
-
const stdoutParser = createLineParser((line) => handleLine(line, "stdout"));
|
|
2995
|
-
const stderrParser = createLineParser((line) => handleLine(line, "stderr"));
|
|
2996
|
-
child.stdout?.on("data", stdoutParser);
|
|
2997
|
-
child.stderr?.on("data", stderrParser);
|
|
2998
|
-
child.on("close", (code) => {
|
|
2999
|
-
if (!didFinalize) {
|
|
3000
|
-
if (code && code !== 0) {
|
|
3001
|
-
const hint = stderrLines.at(-1) || stdoutLines.at(-1) || "";
|
|
3002
|
-
const suffix = hint ? `: ${hint}` : "";
|
|
3003
|
-
debugLog("Cursor", "exit", { code, stderr: stderrLines, stdout: stdoutLines });
|
|
3004
|
-
emitError(`Cursor CLI exited with code ${code}${suffix}`);
|
|
3005
|
-
} else if (!sawError) {
|
|
3006
|
-
const fallback = !sawJson ? rawOutput.trim() : "";
|
|
3007
|
-
emitFinal(aggregated || fallback);
|
|
3008
|
-
}
|
|
3009
|
-
}
|
|
3010
|
-
resolve({ sessionId: finalSessionId });
|
|
3011
|
-
});
|
|
3012
|
-
child.on("error", (err) => {
|
|
3013
|
-
debugLog("Cursor", "spawn-error", { message: err?.message });
|
|
3014
|
-
emitError(err?.message ?? "Cursor failed to start");
|
|
3015
|
-
resolve({ sessionId: finalSessionId });
|
|
3016
|
-
});
|
|
3017
|
-
});
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
// src/providers/local.ts
|
|
3021
|
-
function getLocalBaseUrl() {
|
|
3022
|
-
const base = process.env.AGENTCONNECT_LOCAL_BASE_URL || "http://localhost:11434/v1";
|
|
3023
|
-
return base.replace(/\/+$/, "");
|
|
3024
|
-
}
|
|
3025
|
-
function getLocalApiKey() {
|
|
3026
|
-
return process.env.AGENTCONNECT_LOCAL_API_KEY || "";
|
|
3027
|
-
}
|
|
3028
|
-
function resolveLocalModel(model, fallback) {
|
|
3029
|
-
if (!model) return fallback;
|
|
3030
|
-
const raw = String(model);
|
|
3031
|
-
if (raw === "local") return fallback;
|
|
3032
|
-
if (raw.startsWith("local:")) return raw.slice("local:".length);
|
|
3033
|
-
if (raw.startsWith("local/")) return raw.slice("local/".length);
|
|
3034
|
-
return raw;
|
|
3035
|
-
}
|
|
3036
|
-
async function fetchJson3(url, options = {}) {
|
|
3037
|
-
const controller = new AbortController();
|
|
3038
|
-
const timer = setTimeout(() => controller.abort(), 4e3);
|
|
3039
|
-
try {
|
|
3040
|
-
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
3041
|
-
if (!res.ok) {
|
|
3042
|
-
return { ok: false, status: res.status, data: null };
|
|
3043
|
-
}
|
|
3044
|
-
const data = await res.json();
|
|
3045
|
-
return { ok: true, status: res.status, data };
|
|
3046
|
-
} catch {
|
|
3047
|
-
return { ok: false, status: 0, data: null };
|
|
3048
|
-
} finally {
|
|
3049
|
-
clearTimeout(timer);
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
async function ensureLocalInstalled() {
|
|
3053
|
-
const base = getLocalBaseUrl();
|
|
3054
|
-
const res = await fetchJson3(`${base}/models`);
|
|
3055
|
-
return { installed: res.ok };
|
|
3056
|
-
}
|
|
3057
|
-
async function getLocalStatus() {
|
|
3058
|
-
const base = getLocalBaseUrl();
|
|
3059
|
-
const res = await fetchJson3(`${base}/models`);
|
|
3060
|
-
if (!res.ok) return { installed: false, loggedIn: false };
|
|
3061
|
-
return { installed: true, loggedIn: true };
|
|
3062
|
-
}
|
|
3063
|
-
async function updateLocal() {
|
|
3064
|
-
return getLocalStatus();
|
|
3065
|
-
}
|
|
3066
|
-
async function loginLocal(options = {}) {
|
|
3067
|
-
if (typeof options.baseUrl === "string") {
|
|
3068
|
-
process.env.AGENTCONNECT_LOCAL_BASE_URL = options.baseUrl;
|
|
3069
|
-
}
|
|
3070
|
-
if (typeof options.apiKey === "string") {
|
|
3071
|
-
process.env.AGENTCONNECT_LOCAL_API_KEY = options.apiKey;
|
|
3072
|
-
}
|
|
3073
|
-
if (typeof options.model === "string") {
|
|
3074
|
-
process.env.AGENTCONNECT_LOCAL_MODEL = options.model;
|
|
3075
|
-
}
|
|
3076
|
-
if (Array.isArray(options.models)) {
|
|
3077
|
-
process.env.AGENTCONNECT_LOCAL_MODELS = JSON.stringify(options.models.filter(Boolean));
|
|
3078
|
-
}
|
|
3079
|
-
const status = await getLocalStatus();
|
|
3080
|
-
return { loggedIn: status.installed };
|
|
3081
|
-
}
|
|
3082
|
-
async function listLocalModels() {
|
|
3083
|
-
const base = getLocalBaseUrl();
|
|
3084
|
-
const res = await fetchJson3(`${base}/models`);
|
|
3085
|
-
if (!res.ok || !res.data || !Array.isArray(res.data.data)) return [];
|
|
3086
|
-
return res.data.data.map((entry) => ({ id: entry.id, provider: "local", displayName: entry.id })).filter((entry) => entry.id);
|
|
3087
|
-
}
|
|
3088
|
-
async function runLocalPrompt({
|
|
3089
|
-
prompt,
|
|
3090
|
-
model,
|
|
3091
|
-
onEvent
|
|
3092
|
-
}) {
|
|
3093
|
-
const base = getLocalBaseUrl();
|
|
3094
|
-
const fallback = process.env.AGENTCONNECT_LOCAL_MODEL || "";
|
|
3095
|
-
const resolvedModel = resolveLocalModel(model, fallback);
|
|
3096
|
-
if (!resolvedModel) {
|
|
3097
|
-
onEvent({ type: "error", message: "Local provider model is not configured." });
|
|
3098
|
-
return { sessionId: null };
|
|
3099
|
-
}
|
|
3100
|
-
const payload = {
|
|
3101
|
-
model: resolvedModel,
|
|
3102
|
-
messages: [{ role: "user", content: prompt }],
|
|
3103
|
-
stream: false
|
|
3104
|
-
};
|
|
3105
|
-
const headers = { "Content-Type": "application/json" };
|
|
3106
|
-
const apiKey = getLocalApiKey();
|
|
3107
|
-
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
3108
|
-
const res = await fetchJson3(`${base}/chat/completions`, {
|
|
3109
|
-
method: "POST",
|
|
3110
|
-
headers,
|
|
3111
|
-
body: JSON.stringify(payload)
|
|
3112
|
-
});
|
|
3113
|
-
if (!res.ok) {
|
|
3114
|
-
onEvent({ type: "error", message: "Local provider request failed." });
|
|
3115
|
-
return { sessionId: null };
|
|
3116
|
-
}
|
|
3117
|
-
const message = res.data?.choices?.[0]?.message?.content;
|
|
3118
|
-
const text = typeof message === "string" ? message : "";
|
|
3119
|
-
if (text) {
|
|
3120
|
-
onEvent({ type: "delta", text });
|
|
3121
|
-
onEvent({ type: "final", text });
|
|
3122
|
-
} else {
|
|
3123
|
-
onEvent({ type: "error", message: "Local provider returned no content." });
|
|
3124
|
-
}
|
|
3125
|
-
return { sessionId: null };
|
|
3126
|
-
}
|
|
3127
|
-
|
|
3128
|
-
// src/providers/index.ts
|
|
3129
|
-
var providers = {
|
|
3130
|
-
claude: {
|
|
3131
|
-
id: "claude",
|
|
3132
|
-
name: "Claude",
|
|
3133
|
-
ensureInstalled: ensureClaudeInstalled,
|
|
3134
|
-
fastStatus: getClaudeFastStatus,
|
|
3135
|
-
status: getClaudeStatus,
|
|
3136
|
-
update: updateClaude,
|
|
3137
|
-
login: loginClaude,
|
|
3138
|
-
logout: async () => {
|
|
3139
|
-
},
|
|
3140
|
-
runPrompt: runClaudePrompt
|
|
3141
|
-
},
|
|
3142
|
-
codex: {
|
|
3143
|
-
id: "codex",
|
|
3144
|
-
name: "Codex",
|
|
3145
|
-
ensureInstalled: ensureCodexInstalled,
|
|
3146
|
-
fastStatus: getCodexFastStatus,
|
|
3147
|
-
status: getCodexStatus,
|
|
3148
|
-
update: updateCodex,
|
|
3149
|
-
login: loginCodex,
|
|
3150
|
-
logout: async () => {
|
|
3151
|
-
},
|
|
3152
|
-
runPrompt: runCodexPrompt
|
|
3153
|
-
},
|
|
3154
|
-
cursor: {
|
|
3155
|
-
id: "cursor",
|
|
3156
|
-
name: "Cursor",
|
|
3157
|
-
ensureInstalled: ensureCursorInstalled,
|
|
3158
|
-
fastStatus: getCursorFastStatus,
|
|
3159
|
-
status: getCursorStatus,
|
|
3160
|
-
update: updateCursor,
|
|
3161
|
-
login: loginCursor,
|
|
3162
|
-
logout: async () => {
|
|
3163
|
-
},
|
|
3164
|
-
runPrompt: runCursorPrompt
|
|
3165
|
-
},
|
|
3166
|
-
local: {
|
|
3167
|
-
id: "local",
|
|
3168
|
-
name: "Local",
|
|
3169
|
-
ensureInstalled: ensureLocalInstalled,
|
|
3170
|
-
status: getLocalStatus,
|
|
3171
|
-
update: updateLocal,
|
|
3172
|
-
login: loginLocal,
|
|
3173
|
-
logout: async () => {
|
|
3174
|
-
},
|
|
3175
|
-
runPrompt: runLocalPrompt
|
|
3176
|
-
}
|
|
3177
|
-
};
|
|
3178
|
-
async function listModels() {
|
|
3179
|
-
const claudeModels = await listClaudeModels();
|
|
3180
|
-
const codexModels = await listCodexModels();
|
|
3181
|
-
const cursorModels = await listCursorModels();
|
|
3182
|
-
const base = [
|
|
3183
|
-
...claudeModels,
|
|
3184
|
-
...cursorModels,
|
|
3185
|
-
{ id: "local", provider: "local", displayName: "Local Model" }
|
|
3186
|
-
];
|
|
3187
|
-
const envModels = process.env.AGENTCONNECT_LOCAL_MODELS;
|
|
3188
|
-
if (envModels) {
|
|
3189
|
-
try {
|
|
3190
|
-
const parsed = JSON.parse(envModels);
|
|
3191
|
-
if (Array.isArray(parsed)) {
|
|
3192
|
-
for (const entry of parsed) {
|
|
3193
|
-
if (typeof entry === "string" && entry) {
|
|
3194
|
-
base.push({ id: entry, provider: "local", displayName: entry });
|
|
3195
|
-
}
|
|
3196
|
-
}
|
|
3197
|
-
}
|
|
3198
|
-
} catch {
|
|
3199
|
-
}
|
|
3200
|
-
}
|
|
3201
|
-
const discovered = await listLocalModels();
|
|
3202
|
-
const all = [...base, ...codexModels, ...discovered.filter((entry) => entry.id !== "local")];
|
|
3203
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3204
|
-
return all.filter((entry) => {
|
|
3205
|
-
const key = `${entry.provider}:${entry.id}`;
|
|
3206
|
-
if (seen.has(key)) return false;
|
|
3207
|
-
seen.add(key);
|
|
3208
|
-
return true;
|
|
3209
|
-
});
|
|
3210
|
-
}
|
|
3211
|
-
async function listRecentModels(providerId) {
|
|
3212
|
-
if (providerId && providerId !== "claude") return [];
|
|
3213
|
-
const recent = await listClaudeRecentModels();
|
|
3214
|
-
return recent.filter((entry) => entry.provider === "claude");
|
|
3215
|
-
}
|
|
3216
|
-
function resolveProviderForModel(model) {
|
|
3217
|
-
const lower = String(model || "").toLowerCase();
|
|
3218
|
-
if (lower.includes("cursor")) return "cursor";
|
|
3219
|
-
if (lower.includes("codex")) return "codex";
|
|
3220
|
-
if (lower.startsWith("gpt") || lower.startsWith("o1") || lower.startsWith("o3") || lower.startsWith("o4")) {
|
|
3221
|
-
return "codex";
|
|
3222
|
-
}
|
|
3223
|
-
if (lower.includes("local")) return "local";
|
|
3224
|
-
if (lower.includes("opus") || lower.includes("sonnet") || lower.includes("haiku"))
|
|
3225
|
-
return "claude";
|
|
3226
|
-
if (lower.includes("claude")) return "claude";
|
|
3227
|
-
return "claude";
|
|
3228
|
-
}
|
|
3229
|
-
|
|
3230
|
-
// src/observed.ts
|
|
3231
|
-
import fs from "fs";
|
|
3232
|
-
import path5 from "path";
|
|
3233
|
-
function createObservedTracker({
|
|
3234
|
-
basePath,
|
|
3235
|
-
appId,
|
|
3236
|
-
requested = []
|
|
3237
|
-
}) {
|
|
3238
|
-
const requestedList = Array.isArray(requested) ? requested.filter(Boolean) : [];
|
|
3239
|
-
const dirPath = path5.join(basePath, ".agentconnect");
|
|
3240
|
-
const filePath = path5.join(dirPath, "observed-capabilities.json");
|
|
3241
|
-
const observed = /* @__PURE__ */ new Set();
|
|
3242
|
-
let writeTimer = null;
|
|
3243
|
-
function load() {
|
|
3244
|
-
if (!fs.existsSync(filePath)) return;
|
|
3245
|
-
try {
|
|
3246
|
-
const raw = fs.readFileSync(filePath, "utf8");
|
|
3247
|
-
const parsed = JSON.parse(raw);
|
|
3248
|
-
if (Array.isArray(parsed?.observed)) {
|
|
3249
|
-
for (const entry of parsed.observed) {
|
|
3250
|
-
if (typeof entry === "string" && entry) observed.add(entry);
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
} catch {
|
|
3254
|
-
}
|
|
3255
|
-
}
|
|
3256
|
-
function snapshot() {
|
|
3257
|
-
return {
|
|
3258
|
-
appId,
|
|
3259
|
-
requested: requestedList,
|
|
3260
|
-
observed: Array.from(observed).sort(),
|
|
3261
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3262
|
-
};
|
|
3263
|
-
}
|
|
3264
|
-
function flush() {
|
|
3265
|
-
if (writeTimer) {
|
|
3266
|
-
clearTimeout(writeTimer);
|
|
3267
|
-
writeTimer = null;
|
|
3268
|
-
}
|
|
3269
|
-
const payload = JSON.stringify(snapshot(), null, 2);
|
|
3270
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
3271
|
-
fs.writeFileSync(filePath, payload);
|
|
3272
|
-
}
|
|
3273
|
-
function scheduleFlush() {
|
|
3274
|
-
if (writeTimer) return;
|
|
3275
|
-
writeTimer = setTimeout(() => {
|
|
3276
|
-
flush();
|
|
3277
|
-
}, 400);
|
|
3278
|
-
}
|
|
3279
|
-
function record(capability) {
|
|
3280
|
-
const value = typeof capability === "string" ? capability.trim() : "";
|
|
3281
|
-
if (!value) return;
|
|
3282
|
-
if (observed.has(value)) return;
|
|
3283
|
-
observed.add(value);
|
|
3284
|
-
scheduleFlush();
|
|
3285
|
-
}
|
|
3286
|
-
function list() {
|
|
3287
|
-
return Array.from(observed).sort();
|
|
3288
|
-
}
|
|
3289
|
-
load();
|
|
3290
|
-
return {
|
|
3291
|
-
record,
|
|
3292
|
-
list,
|
|
3293
|
-
snapshot,
|
|
3294
|
-
flush
|
|
3295
|
-
};
|
|
3296
|
-
}
|
|
3297
|
-
|
|
3298
|
-
// src/host.ts
|
|
3299
|
-
function send(socket, payload) {
|
|
3300
|
-
socket.send(JSON.stringify(payload));
|
|
3301
|
-
}
|
|
3302
|
-
function reply(socket, id, result) {
|
|
3303
|
-
send(socket, { jsonrpc: "2.0", id, result });
|
|
3304
|
-
}
|
|
3305
|
-
function replyError(socket, id, code, message) {
|
|
3306
|
-
send(socket, {
|
|
3307
|
-
jsonrpc: "2.0",
|
|
3308
|
-
id,
|
|
3309
|
-
error: { code, message }
|
|
3310
|
-
});
|
|
3311
|
-
}
|
|
3312
|
-
function sessionEvent(socket, sessionId, type, data) {
|
|
3313
|
-
if (process.env.AGENTCONNECT_DEBUG?.trim()) {
|
|
3314
|
-
try {
|
|
3315
|
-
console.log(
|
|
3316
|
-
`[AgentConnect][Session ${sessionId}] ${type} ${JSON.stringify(data)}`
|
|
3317
|
-
);
|
|
3318
|
-
} catch {
|
|
3319
|
-
console.log(`[AgentConnect][Session ${sessionId}] ${type}`);
|
|
3320
|
-
}
|
|
3321
|
-
}
|
|
3322
|
-
send(socket, {
|
|
3323
|
-
jsonrpc: "2.0",
|
|
3324
|
-
method: "acp.session.event",
|
|
3325
|
-
params: { sessionId, type, data }
|
|
3326
|
-
});
|
|
3327
|
-
}
|
|
3328
|
-
function buildProviderList(statuses) {
|
|
3329
|
-
return Object.values(providers).map((provider) => {
|
|
3330
|
-
const info = statuses[provider.id] || {};
|
|
3331
|
-
return {
|
|
3332
|
-
id: provider.id,
|
|
3333
|
-
name: provider.name,
|
|
3334
|
-
installed: info.installed ?? false,
|
|
3335
|
-
loggedIn: info.loggedIn ?? false,
|
|
3336
|
-
version: info.version,
|
|
3337
|
-
updateAvailable: info.updateAvailable,
|
|
3338
|
-
latestVersion: info.latestVersion,
|
|
3339
|
-
updateCheckedAt: info.updateCheckedAt,
|
|
3340
|
-
updateSource: info.updateSource,
|
|
3341
|
-
updateCommand: info.updateCommand,
|
|
3342
|
-
updateMessage: info.updateMessage,
|
|
3343
|
-
updateInProgress: info.updateInProgress
|
|
3344
|
-
};
|
|
3345
|
-
});
|
|
3346
|
-
}
|
|
3347
|
-
function startDevHost({
|
|
3348
|
-
host = "127.0.0.1",
|
|
3349
|
-
port = 9630,
|
|
3350
|
-
appPath,
|
|
3351
|
-
uiUrl
|
|
3352
|
-
} = {}) {
|
|
3353
|
-
process.env.AGENTCONNECT_HOST_MODE ||= "dev";
|
|
3354
|
-
const server = http.createServer();
|
|
3355
|
-
const wss = new WebSocketServer({ server });
|
|
3356
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
3357
|
-
const activeRuns = /* @__PURE__ */ new Map();
|
|
3358
|
-
const updatingProviders = /* @__PURE__ */ new Map();
|
|
3359
|
-
const processTable = /* @__PURE__ */ new Map();
|
|
3360
|
-
const backendState = /* @__PURE__ */ new Map();
|
|
3361
|
-
const statusCache = /* @__PURE__ */ new Map();
|
|
3362
|
-
const statusCacheTtlMs = 8e3;
|
|
3363
|
-
const statusInFlight = /* @__PURE__ */ new Map();
|
|
3364
|
-
const basePath = appPath || process.cwd();
|
|
3365
|
-
const manifest = readManifest(basePath);
|
|
3366
|
-
const appId = manifest?.id || "agentconnect-dev-app";
|
|
3367
|
-
const requestedCapabilities = Array.isArray(manifest?.capabilities) ? manifest.capabilities : [];
|
|
3368
|
-
const observedTracker = createObservedTracker({
|
|
3369
|
-
basePath,
|
|
3370
|
-
appId,
|
|
3371
|
-
requested: requestedCapabilities
|
|
3372
|
-
});
|
|
3373
|
-
function resolveAppPathInternal(input) {
|
|
3374
|
-
if (!input) return basePath;
|
|
3375
|
-
const value = String(input);
|
|
3376
|
-
return path6.isAbsolute(value) ? value : path6.resolve(basePath, value);
|
|
3377
|
-
}
|
|
3378
|
-
function mapFileType(stat) {
|
|
3379
|
-
if (stat.isFile()) return "file";
|
|
3380
|
-
if (stat.isDirectory()) return "dir";
|
|
3381
|
-
if (stat.isSymbolicLink()) return "link";
|
|
3382
|
-
return "other";
|
|
3383
|
-
}
|
|
3384
|
-
async function allocatePort() {
|
|
3385
|
-
return new Promise((resolve, reject) => {
|
|
3386
|
-
const socket = net.createServer();
|
|
3387
|
-
socket.listen(0, host, () => {
|
|
3388
|
-
const address = socket.address();
|
|
3389
|
-
if (!address || typeof address === "string") {
|
|
3390
|
-
socket.close();
|
|
3391
|
-
reject(new Error("Failed to allocate port."));
|
|
3392
|
-
return;
|
|
3393
|
-
}
|
|
3394
|
-
const portValue = address.port;
|
|
3395
|
-
socket.close(() => resolve(portValue));
|
|
3396
|
-
});
|
|
3397
|
-
socket.on("error", reject);
|
|
3398
|
-
});
|
|
3399
|
-
}
|
|
3400
|
-
async function waitForHealthcheck(url, timeoutMs = 15e3) {
|
|
3401
|
-
const start = Date.now();
|
|
3402
|
-
while (Date.now() - start < timeoutMs) {
|
|
3403
|
-
try {
|
|
3404
|
-
const res = await fetch(url, { method: "GET" });
|
|
3405
|
-
if (res.ok) return true;
|
|
3406
|
-
} catch {
|
|
3407
|
-
}
|
|
3408
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3409
|
-
}
|
|
3410
|
-
return false;
|
|
3411
|
-
}
|
|
3412
|
-
function readManifest(root) {
|
|
3413
|
-
try {
|
|
3414
|
-
const raw = fs2.readFileSync(path6.join(root, "agentconnect.app.json"), "utf8");
|
|
3415
|
-
return JSON.parse(raw);
|
|
3416
|
-
} catch {
|
|
3417
|
-
return null;
|
|
3418
|
-
}
|
|
3419
|
-
}
|
|
3420
|
-
function recordCapability(capability) {
|
|
3421
|
-
observedTracker.record(capability);
|
|
3422
|
-
}
|
|
3423
|
-
function recordModelCapability(model) {
|
|
3424
|
-
const providerId = resolveProviderForModel(model);
|
|
3425
|
-
if (!providerId) return;
|
|
3426
|
-
recordCapability(`model.${providerId}`);
|
|
3427
|
-
}
|
|
3428
|
-
async function getCachedStatus(provider, options = {}) {
|
|
3429
|
-
const cached = statusCache.get(provider.id);
|
|
3430
|
-
const now = Date.now();
|
|
3431
|
-
if (cached && now - cached.at < statusCacheTtlMs) {
|
|
3432
|
-
return cached.status;
|
|
3433
|
-
}
|
|
3434
|
-
const existing = statusInFlight.get(provider.id);
|
|
3435
|
-
if (existing) return existing;
|
|
3436
|
-
if (options.allowFast && provider.fastStatus) {
|
|
3437
|
-
try {
|
|
3438
|
-
const fast = await provider.fastStatus();
|
|
3439
|
-
const startedAt2 = Date.now();
|
|
3440
|
-
const promise2 = provider.status().then((status) => {
|
|
3441
|
-
debugLog("Providers", "status-check", {
|
|
3442
|
-
providerId: provider.id,
|
|
3443
|
-
durationMs: Date.now() - startedAt2,
|
|
3444
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3445
|
-
});
|
|
3446
|
-
statusCache.set(provider.id, { status, at: Date.now() });
|
|
3447
|
-
return status;
|
|
3448
|
-
}).finally(() => {
|
|
3449
|
-
statusInFlight.delete(provider.id);
|
|
3450
|
-
});
|
|
3451
|
-
statusInFlight.set(provider.id, promise2);
|
|
3452
|
-
return fast;
|
|
3453
|
-
} catch {
|
|
3454
|
-
}
|
|
3455
|
-
}
|
|
3456
|
-
const startedAt = Date.now();
|
|
3457
|
-
const promise = provider.status().then((status) => {
|
|
3458
|
-
debugLog("Providers", "status-check", {
|
|
3459
|
-
providerId: provider.id,
|
|
3460
|
-
durationMs: Date.now() - startedAt,
|
|
3461
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3462
|
-
});
|
|
3463
|
-
statusCache.set(provider.id, { status, at: Date.now() });
|
|
3464
|
-
return status;
|
|
3465
|
-
}).finally(() => {
|
|
3466
|
-
statusInFlight.delete(provider.id);
|
|
3467
|
-
});
|
|
3468
|
-
statusInFlight.set(provider.id, promise);
|
|
3469
|
-
return promise;
|
|
3470
|
-
}
|
|
3471
|
-
function invalidateStatus(providerId) {
|
|
3472
|
-
if (!providerId) return;
|
|
3473
|
-
statusCache.delete(providerId);
|
|
3474
|
-
}
|
|
3475
|
-
wss.on("connection", (socket) => {
|
|
3476
|
-
socket.on("message", async (raw) => {
|
|
3477
|
-
let payload;
|
|
3478
|
-
try {
|
|
3479
|
-
payload = JSON.parse(String(raw));
|
|
3480
|
-
} catch {
|
|
3481
|
-
return;
|
|
3482
|
-
}
|
|
3483
|
-
if (!payload || payload.jsonrpc !== "2.0" || payload.id === void 0) {
|
|
3484
|
-
return;
|
|
3485
|
-
}
|
|
3486
|
-
const id = payload.id;
|
|
3487
|
-
const method = payload.method;
|
|
3488
|
-
const params = payload.params ?? {};
|
|
3489
|
-
if (typeof method === "string" && method.startsWith("acp.")) {
|
|
3490
|
-
recordCapability("agent.connect");
|
|
3491
|
-
}
|
|
3492
|
-
if (method === "acp.hello") {
|
|
3493
|
-
const loginExperience = process.env.AGENTCONNECT_LOGIN_EXPERIENCE || process.env.AGENTCONNECT_CLAUDE_LOGIN_EXPERIENCE || (process.env.AGENTCONNECT_HOST_MODE === "dev" ? "terminal" : "embedded");
|
|
3494
|
-
reply(socket, id, {
|
|
3495
|
-
hostId: "agentconnect-dev",
|
|
3496
|
-
hostName: "AgentConnect Dev Host",
|
|
3497
|
-
hostVersion: "0.1.0",
|
|
3498
|
-
protocolVersion: "0.1",
|
|
3499
|
-
mode: "local",
|
|
3500
|
-
capabilities: [],
|
|
3501
|
-
providers: Object.keys(providers),
|
|
3502
|
-
loginExperience
|
|
3503
|
-
});
|
|
3504
|
-
return;
|
|
3505
|
-
}
|
|
3506
|
-
if (method === "acp.providers.list") {
|
|
3507
|
-
const statusEntries = await Promise.all(
|
|
3508
|
-
Object.values(providers).map(async (provider) => {
|
|
3509
|
-
try {
|
|
3510
|
-
return [provider.id, await getCachedStatus(provider, { allowFast: true })];
|
|
3511
|
-
} catch {
|
|
3512
|
-
return [provider.id, { installed: false, loggedIn: false }];
|
|
3513
|
-
}
|
|
3514
|
-
})
|
|
3515
|
-
);
|
|
3516
|
-
const statuses = Object.fromEntries(statusEntries);
|
|
3517
|
-
const list = buildProviderList(statuses).map((entry) => ({
|
|
3518
|
-
...entry,
|
|
3519
|
-
updateInProgress: updatingProviders.has(entry.id)
|
|
3520
|
-
}));
|
|
3521
|
-
reply(socket, id, { providers: list });
|
|
3522
|
-
return;
|
|
3523
|
-
}
|
|
3524
|
-
if (method === "acp.providers.status") {
|
|
3525
|
-
const providerId = params.provider;
|
|
3526
|
-
const provider = providers[providerId];
|
|
3527
|
-
if (!provider) {
|
|
3528
|
-
replyError(socket, id, "AC_ERR_UNSUPPORTED", "Unknown provider");
|
|
3529
|
-
return;
|
|
3530
|
-
}
|
|
3531
|
-
const status = await getCachedStatus(provider, { allowFast: true });
|
|
3532
|
-
reply(socket, id, {
|
|
3533
|
-
provider: {
|
|
3534
|
-
id: provider.id,
|
|
3535
|
-
name: provider.name,
|
|
3536
|
-
installed: status.installed,
|
|
3537
|
-
loggedIn: status.loggedIn,
|
|
3538
|
-
version: status.version,
|
|
3539
|
-
updateAvailable: status.updateAvailable,
|
|
3540
|
-
latestVersion: status.latestVersion,
|
|
3541
|
-
updateCheckedAt: status.updateCheckedAt,
|
|
3542
|
-
updateSource: status.updateSource,
|
|
3543
|
-
updateCommand: status.updateCommand,
|
|
3544
|
-
updateMessage: status.updateMessage,
|
|
3545
|
-
updateInProgress: updatingProviders.has(provider.id) || status.updateInProgress
|
|
3546
|
-
}
|
|
3547
|
-
});
|
|
3548
|
-
return;
|
|
3549
|
-
}
|
|
3550
|
-
if (method === "acp.providers.update") {
|
|
3551
|
-
const providerId = params.provider;
|
|
3552
|
-
const provider = providers[providerId];
|
|
3553
|
-
if (!provider) {
|
|
3554
|
-
replyError(socket, id, "AC_ERR_UNSUPPORTED", "Unknown provider");
|
|
3555
|
-
return;
|
|
3556
|
-
}
|
|
3557
|
-
debugLog("Providers", "update-start", { providerId });
|
|
3558
|
-
if (!updatingProviders.has(providerId)) {
|
|
3559
|
-
const promise = provider.update().finally(() => {
|
|
3560
|
-
updatingProviders.delete(providerId);
|
|
3561
|
-
invalidateStatus(providerId);
|
|
3562
|
-
});
|
|
3563
|
-
updatingProviders.set(providerId, promise);
|
|
3564
|
-
}
|
|
3565
|
-
try {
|
|
3566
|
-
const status = await updatingProviders.get(providerId);
|
|
3567
|
-
debugLog("Providers", "update-complete", {
|
|
3568
|
-
providerId,
|
|
3569
|
-
updateAvailable: status.updateAvailable,
|
|
3570
|
-
latestVersion: status.latestVersion,
|
|
3571
|
-
updateMessage: status.updateMessage
|
|
3572
|
-
});
|
|
3573
|
-
reply(socket, id, {
|
|
3574
|
-
provider: {
|
|
3575
|
-
id: provider.id,
|
|
3576
|
-
name: provider.name,
|
|
3577
|
-
installed: status.installed,
|
|
3578
|
-
loggedIn: status.loggedIn,
|
|
3579
|
-
version: status.version,
|
|
3580
|
-
updateAvailable: status.updateAvailable,
|
|
3581
|
-
latestVersion: status.latestVersion,
|
|
3582
|
-
updateCheckedAt: status.updateCheckedAt,
|
|
3583
|
-
updateSource: status.updateSource,
|
|
3584
|
-
updateCommand: status.updateCommand,
|
|
3585
|
-
updateMessage: status.updateMessage,
|
|
3586
|
-
updateInProgress: false
|
|
3587
|
-
}
|
|
3588
|
-
});
|
|
3589
|
-
} catch (err) {
|
|
3590
|
-
debugLog("Providers", "update-error", {
|
|
3591
|
-
providerId,
|
|
3592
|
-
message: err instanceof Error ? err.message : String(err)
|
|
3593
|
-
});
|
|
3594
|
-
replyError(socket, id, "AC_ERR_INTERNAL", err?.message || "Update failed");
|
|
3595
|
-
}
|
|
3596
|
-
return;
|
|
3597
|
-
}
|
|
3598
|
-
if (method === "acp.providers.ensureInstalled") {
|
|
3599
|
-
const providerId = params.provider;
|
|
3600
|
-
const provider = providers[providerId];
|
|
3601
|
-
if (!provider) {
|
|
3602
|
-
replyError(socket, id, "AC_ERR_UNSUPPORTED", "Unknown provider");
|
|
3603
|
-
return;
|
|
3604
|
-
}
|
|
3605
|
-
const result = await provider.ensureInstalled();
|
|
3606
|
-
invalidateStatus(provider.id);
|
|
3607
|
-
reply(socket, id, result);
|
|
3608
|
-
return;
|
|
3609
|
-
}
|
|
3610
|
-
if (method === "acp.providers.login") {
|
|
3611
|
-
const providerId = params.provider;
|
|
3612
|
-
const provider = providers[providerId];
|
|
3613
|
-
if (!provider) {
|
|
3614
|
-
replyError(socket, id, "AC_ERR_UNSUPPORTED", "Unknown provider");
|
|
3615
|
-
return;
|
|
3616
|
-
}
|
|
3617
|
-
try {
|
|
3618
|
-
const result = await provider.login(params.options);
|
|
3619
|
-
invalidateStatus(provider.id);
|
|
3620
|
-
reply(socket, id, result);
|
|
3621
|
-
} catch (err) {
|
|
3622
|
-
replyError(
|
|
3623
|
-
socket,
|
|
3624
|
-
id,
|
|
3625
|
-
"AC_ERR_INTERNAL",
|
|
3626
|
-
err?.message || "Provider login failed."
|
|
3627
|
-
);
|
|
3628
|
-
}
|
|
3629
|
-
return;
|
|
3630
|
-
}
|
|
3631
|
-
if (method === "acp.providers.logout") {
|
|
3632
|
-
const providerId = params.provider;
|
|
3633
|
-
const provider = providers[providerId];
|
|
3634
|
-
if (!provider) {
|
|
3635
|
-
replyError(socket, id, "AC_ERR_UNSUPPORTED", "Unknown provider");
|
|
3636
|
-
return;
|
|
3637
|
-
}
|
|
3638
|
-
await provider.logout();
|
|
3639
|
-
invalidateStatus(provider.id);
|
|
3640
|
-
reply(socket, id, {});
|
|
3641
|
-
return;
|
|
3642
|
-
}
|
|
3643
|
-
if (method === "acp.models.list") {
|
|
3644
|
-
const models = await listModels();
|
|
3645
|
-
const providerId = params.provider;
|
|
3646
|
-
if (providerId) {
|
|
3647
|
-
reply(socket, id, { models: models.filter((m) => m.provider === providerId) });
|
|
3648
|
-
} else {
|
|
3649
|
-
reply(socket, id, { models });
|
|
3650
|
-
}
|
|
3651
|
-
return;
|
|
3652
|
-
}
|
|
3653
|
-
if (method === "acp.models.recent") {
|
|
3654
|
-
const providerId = params.provider;
|
|
3655
|
-
const models = await listRecentModels(providerId);
|
|
3656
|
-
reply(socket, id, { models });
|
|
3657
|
-
return;
|
|
3658
|
-
}
|
|
3659
|
-
if (method === "acp.models.info") {
|
|
3660
|
-
const modelId = params.model;
|
|
3661
|
-
const model = (await listModels()).find((m) => m.id === modelId);
|
|
3662
|
-
if (!model) {
|
|
3663
|
-
replyError(socket, id, "AC_ERR_INVALID_ARGS", "Unknown model");
|
|
3664
|
-
return;
|
|
3665
|
-
}
|
|
3666
|
-
reply(socket, id, { model });
|
|
3667
|
-
return;
|
|
3668
|
-
}
|
|
3669
|
-
if (method === "acp.sessions.create") {
|
|
3670
|
-
const sessionId = `sess_${Math.random().toString(36).slice(2, 10)}`;
|
|
3671
|
-
const model = params.model || "claude-opus";
|
|
3672
|
-
const reasoningEffort = params.reasoningEffort || null;
|
|
3673
|
-
const cwd = params.cwd ? resolveAppPathInternal(params.cwd) : void 0;
|
|
3674
|
-
const repoRoot = params.repoRoot ? resolveAppPathInternal(params.repoRoot) : void 0;
|
|
3675
|
-
const providerDetailLevel = params.providerDetailLevel || void 0;
|
|
3676
|
-
const providerId = resolveProviderForModel(model);
|
|
3677
|
-
recordModelCapability(model);
|
|
3678
|
-
sessions.set(sessionId, {
|
|
3679
|
-
id: sessionId,
|
|
3680
|
-
providerId,
|
|
3681
|
-
model,
|
|
3682
|
-
providerSessionId: null,
|
|
3683
|
-
reasoningEffort,
|
|
3684
|
-
cwd,
|
|
3685
|
-
repoRoot,
|
|
3686
|
-
providerDetailLevel: providerDetailLevel === "raw" || providerDetailLevel === "minimal" ? providerDetailLevel : void 0
|
|
3687
|
-
});
|
|
3688
|
-
reply(socket, id, { sessionId });
|
|
3689
|
-
return;
|
|
3690
|
-
}
|
|
3691
|
-
if (method === "acp.sessions.resume") {
|
|
3692
|
-
const sessionId = params.sessionId;
|
|
3693
|
-
const existing = sessions.get(sessionId);
|
|
3694
|
-
if (!existing) {
|
|
3695
|
-
const model = params.model || "claude-opus";
|
|
3696
|
-
const reasoningEffort = params.reasoningEffort || null;
|
|
3697
|
-
const cwd = params.cwd ? resolveAppPathInternal(params.cwd) : void 0;
|
|
3698
|
-
const repoRoot = params.repoRoot ? resolveAppPathInternal(params.repoRoot) : void 0;
|
|
3699
|
-
const providerDetailLevel = params.providerDetailLevel || void 0;
|
|
3700
|
-
recordModelCapability(model);
|
|
3701
|
-
sessions.set(sessionId, {
|
|
3702
|
-
id: sessionId,
|
|
3703
|
-
providerId: resolveProviderForModel(model),
|
|
3704
|
-
model,
|
|
3705
|
-
providerSessionId: params.providerSessionId || null,
|
|
3706
|
-
reasoningEffort,
|
|
3707
|
-
cwd,
|
|
3708
|
-
repoRoot,
|
|
3709
|
-
providerDetailLevel: providerDetailLevel === "raw" || providerDetailLevel === "minimal" ? providerDetailLevel : void 0
|
|
3710
|
-
});
|
|
3711
|
-
} else {
|
|
3712
|
-
if (params.providerSessionId) {
|
|
3713
|
-
existing.providerSessionId = String(params.providerSessionId);
|
|
3714
|
-
}
|
|
3715
|
-
if (params.cwd) {
|
|
3716
|
-
existing.cwd = resolveAppPathInternal(params.cwd);
|
|
3717
|
-
}
|
|
3718
|
-
if (params.repoRoot) {
|
|
3719
|
-
existing.repoRoot = resolveAppPathInternal(params.repoRoot);
|
|
3720
|
-
}
|
|
3721
|
-
if (params.providerDetailLevel) {
|
|
3722
|
-
const level = String(params.providerDetailLevel);
|
|
3723
|
-
if (level === "raw" || level === "minimal") {
|
|
3724
|
-
existing.providerDetailLevel = level;
|
|
3725
|
-
}
|
|
3726
|
-
}
|
|
3727
|
-
recordModelCapability(existing.model);
|
|
3728
|
-
}
|
|
3729
|
-
reply(socket, id, { sessionId });
|
|
3730
|
-
return;
|
|
3731
|
-
}
|
|
3732
|
-
if (method === "acp.sessions.send") {
|
|
3733
|
-
const sessionId = params.sessionId;
|
|
3734
|
-
const message = params.message?.content || "";
|
|
3735
|
-
const session = sessions.get(sessionId);
|
|
3736
|
-
if (!session) {
|
|
3737
|
-
replyError(socket, id, "AC_ERR_INVALID_ARGS", "Unknown session");
|
|
3738
|
-
return;
|
|
3739
|
-
}
|
|
3740
|
-
recordModelCapability(session.model);
|
|
3741
|
-
const provider = providers[session.providerId];
|
|
3742
|
-
if (!provider) {
|
|
3743
|
-
replyError(socket, id, "AC_ERR_UNSUPPORTED", "Unknown provider");
|
|
3744
|
-
return;
|
|
3745
|
-
}
|
|
3746
|
-
if (updatingProviders.has(session.providerId)) {
|
|
3747
|
-
replyError(socket, id, "AC_ERR_BUSY", "Provider update in progress.");
|
|
3748
|
-
return;
|
|
3749
|
-
}
|
|
3750
|
-
const status = await provider.status();
|
|
3751
|
-
if (!status.installed) {
|
|
3752
|
-
const installed = await provider.ensureInstalled();
|
|
3753
|
-
if (!installed.installed) {
|
|
3754
|
-
replyError(socket, id, "AC_ERR_NOT_INSTALLED", "Provider CLI is not installed.");
|
|
3755
|
-
return;
|
|
3756
|
-
}
|
|
3757
|
-
}
|
|
3758
|
-
const controller = new AbortController();
|
|
3759
|
-
const cwd = params.cwd ? resolveAppPathInternal(params.cwd) : session.cwd || basePath;
|
|
3760
|
-
const repoRoot = params.repoRoot ? resolveAppPathInternal(params.repoRoot) : session.repoRoot || basePath;
|
|
3761
|
-
const providerDetailLevel = params.providerDetailLevel === "raw" || params.providerDetailLevel === "minimal" ? params.providerDetailLevel : session.providerDetailLevel || "minimal";
|
|
3762
|
-
activeRuns.set(sessionId, controller);
|
|
3763
|
-
let sawError = false;
|
|
3764
|
-
provider.runPrompt({
|
|
3765
|
-
prompt: message,
|
|
3766
|
-
resumeSessionId: session.providerSessionId,
|
|
3767
|
-
model: session.model,
|
|
3768
|
-
reasoningEffort: session.reasoningEffort,
|
|
3769
|
-
repoRoot,
|
|
3770
|
-
cwd,
|
|
3771
|
-
providerDetailLevel,
|
|
3772
|
-
signal: controller.signal,
|
|
3773
|
-
onEvent: (event) => {
|
|
3774
|
-
if (event.type === "error") {
|
|
3775
|
-
sawError = true;
|
|
3776
|
-
}
|
|
3777
|
-
if (sawError && event.type === "final") {
|
|
3778
|
-
return;
|
|
3779
|
-
}
|
|
3780
|
-
sessionEvent(socket, sessionId, event.type, { ...event });
|
|
3781
|
-
}
|
|
3782
|
-
}).then((result) => {
|
|
3783
|
-
if (result?.sessionId) {
|
|
3784
|
-
session.providerSessionId = result.sessionId;
|
|
3785
|
-
}
|
|
3786
|
-
}).catch((err) => {
|
|
3787
|
-
if (!sawError) {
|
|
3788
|
-
sessionEvent(socket, sessionId, "error", {
|
|
3789
|
-
message: err?.message || "Provider error"
|
|
3790
|
-
});
|
|
3791
|
-
}
|
|
3792
|
-
}).finally(() => {
|
|
3793
|
-
activeRuns.delete(sessionId);
|
|
3794
|
-
});
|
|
3795
|
-
reply(socket, id, { accepted: true });
|
|
3796
|
-
return;
|
|
3797
|
-
}
|
|
3798
|
-
if (method === "acp.sessions.cancel") {
|
|
3799
|
-
const sessionId = params.sessionId;
|
|
3800
|
-
const controller = activeRuns.get(sessionId);
|
|
3801
|
-
if (controller) {
|
|
3802
|
-
controller.abort();
|
|
3803
|
-
activeRuns.delete(sessionId);
|
|
3804
|
-
}
|
|
3805
|
-
reply(socket, id, { cancelled: true });
|
|
3806
|
-
return;
|
|
3807
|
-
}
|
|
3808
|
-
if (method === "acp.sessions.close") {
|
|
3809
|
-
const sessionId = params.sessionId;
|
|
3810
|
-
sessions.delete(sessionId);
|
|
3811
|
-
reply(socket, id, { closed: true });
|
|
3812
|
-
return;
|
|
3813
|
-
}
|
|
3814
|
-
if (method === "acp.fs.read") {
|
|
3815
|
-
recordCapability("fs.read");
|
|
3816
|
-
try {
|
|
3817
|
-
const filePath = resolveAppPathInternal(params.path);
|
|
3818
|
-
const encoding = params.encoding || "utf8";
|
|
3819
|
-
const content = await fsp.readFile(filePath, encoding);
|
|
3820
|
-
reply(socket, id, { content, encoding });
|
|
3821
|
-
} catch (err) {
|
|
3822
|
-
replyError(
|
|
3823
|
-
socket,
|
|
3824
|
-
id,
|
|
3825
|
-
"AC_ERR_FS_READ",
|
|
3826
|
-
err?.message || "Failed to read file."
|
|
3827
|
-
);
|
|
3828
|
-
}
|
|
3829
|
-
return;
|
|
3830
|
-
}
|
|
3831
|
-
if (method === "acp.fs.write") {
|
|
3832
|
-
recordCapability("fs.write");
|
|
3833
|
-
try {
|
|
3834
|
-
const filePath = resolveAppPathInternal(params.path);
|
|
3835
|
-
const encoding = params.encoding || "utf8";
|
|
3836
|
-
const content = params.content ?? "";
|
|
3837
|
-
await fsp.mkdir(path6.dirname(filePath), { recursive: true });
|
|
3838
|
-
await fsp.writeFile(filePath, content, {
|
|
3839
|
-
encoding,
|
|
3840
|
-
mode: params.mode
|
|
3841
|
-
});
|
|
3842
|
-
const bytes = Buffer.byteLength(String(content), encoding);
|
|
3843
|
-
reply(socket, id, { bytes });
|
|
3844
|
-
} catch (err) {
|
|
3845
|
-
replyError(
|
|
3846
|
-
socket,
|
|
3847
|
-
id,
|
|
3848
|
-
"AC_ERR_FS_WRITE",
|
|
3849
|
-
err?.message || "Failed to write file."
|
|
3850
|
-
);
|
|
3851
|
-
}
|
|
3852
|
-
return;
|
|
3853
|
-
}
|
|
3854
|
-
if (method === "acp.fs.list") {
|
|
3855
|
-
recordCapability("fs.read");
|
|
3856
|
-
try {
|
|
3857
|
-
const dirPath = resolveAppPathInternal(params.path);
|
|
3858
|
-
const entries = await fsp.readdir(dirPath, { withFileTypes: true });
|
|
3859
|
-
const results = [];
|
|
3860
|
-
for (const entry of entries) {
|
|
3861
|
-
const entryPath = path6.join(dirPath, entry.name);
|
|
3862
|
-
let size = 0;
|
|
3863
|
-
let type = "other";
|
|
3864
|
-
try {
|
|
3865
|
-
const stat = await fsp.lstat(entryPath);
|
|
3866
|
-
type = mapFileType(stat);
|
|
3867
|
-
if (type === "file") size = stat.size;
|
|
3868
|
-
} catch {
|
|
3869
|
-
type = entry.isDirectory() ? "dir" : entry.isFile() ? "file" : "other";
|
|
3870
|
-
}
|
|
3871
|
-
results.push({
|
|
3872
|
-
name: entry.name,
|
|
3873
|
-
path: entryPath,
|
|
3874
|
-
type,
|
|
3875
|
-
size
|
|
3876
|
-
});
|
|
3877
|
-
}
|
|
3878
|
-
reply(socket, id, { entries: results });
|
|
3879
|
-
} catch (err) {
|
|
3880
|
-
replyError(
|
|
3881
|
-
socket,
|
|
3882
|
-
id,
|
|
3883
|
-
"AC_ERR_FS_LIST",
|
|
3884
|
-
err?.message || "Failed to list directory."
|
|
3885
|
-
);
|
|
3886
|
-
}
|
|
3887
|
-
return;
|
|
3888
|
-
}
|
|
3889
|
-
if (method === "acp.fs.stat") {
|
|
3890
|
-
recordCapability("fs.read");
|
|
3891
|
-
try {
|
|
3892
|
-
const filePath = resolveAppPathInternal(params.path);
|
|
3893
|
-
const stat = await fsp.lstat(filePath);
|
|
3894
|
-
reply(socket, id, {
|
|
3895
|
-
type: mapFileType(stat),
|
|
3896
|
-
size: stat.size,
|
|
3897
|
-
mtime: stat.mtime.toISOString()
|
|
3898
|
-
});
|
|
3899
|
-
} catch (err) {
|
|
3900
|
-
replyError(
|
|
3901
|
-
socket,
|
|
3902
|
-
id,
|
|
3903
|
-
"AC_ERR_FS_STAT",
|
|
3904
|
-
err?.message || "Failed to stat file."
|
|
3905
|
-
);
|
|
3906
|
-
}
|
|
3907
|
-
return;
|
|
3908
|
-
}
|
|
3909
|
-
if (method === "acp.process.spawn") {
|
|
3910
|
-
recordCapability("process.spawn");
|
|
3911
|
-
try {
|
|
3912
|
-
const command2 = String(params.command || "");
|
|
3913
|
-
const args2 = Array.isArray(params.args) ? params.args.map(String) : [];
|
|
3914
|
-
const cwd = params.cwd ? resolveAppPathInternal(params.cwd) : basePath;
|
|
3915
|
-
const env = { ...process.env, ...params.env || {} };
|
|
3916
|
-
const useTty = Boolean(params.tty);
|
|
3917
|
-
const child = spawn5(command2, args2, {
|
|
3918
|
-
cwd,
|
|
3919
|
-
env,
|
|
3920
|
-
stdio: useTty ? "inherit" : ["pipe", "pipe", "pipe"]
|
|
3921
|
-
});
|
|
3922
|
-
if (!useTty) {
|
|
3923
|
-
child.stdout?.on("data", () => void 0);
|
|
3924
|
-
child.stderr?.on("data", () => void 0);
|
|
3925
|
-
}
|
|
3926
|
-
if (typeof params.stdin === "string" && child.stdin) {
|
|
3927
|
-
child.stdin.write(params.stdin);
|
|
3928
|
-
child.stdin.end();
|
|
3929
|
-
}
|
|
3930
|
-
if (child.pid) {
|
|
3931
|
-
processTable.set(child.pid, child);
|
|
3932
|
-
child.on("close", () => {
|
|
3933
|
-
if (child.pid) processTable.delete(child.pid);
|
|
3934
|
-
});
|
|
3935
|
-
}
|
|
3936
|
-
reply(socket, id, { pid: child.pid });
|
|
3937
|
-
} catch (err) {
|
|
3938
|
-
replyError(
|
|
3939
|
-
socket,
|
|
3940
|
-
id,
|
|
3941
|
-
"AC_ERR_PROCESS",
|
|
3942
|
-
err?.message || "Failed to spawn process."
|
|
3943
|
-
);
|
|
3944
|
-
}
|
|
3945
|
-
return;
|
|
3946
|
-
}
|
|
3947
|
-
if (method === "acp.process.kill") {
|
|
3948
|
-
recordCapability("process.kill");
|
|
3949
|
-
const pid = Number(params.pid);
|
|
3950
|
-
const signal = params.signal || "SIGTERM";
|
|
3951
|
-
const child = processTable.get(pid);
|
|
3952
|
-
try {
|
|
3953
|
-
const success = child ? child.kill(signal) : process.kill(pid, signal);
|
|
3954
|
-
reply(socket, id, { success: Boolean(success) });
|
|
3955
|
-
} catch (err) {
|
|
3956
|
-
replyError(
|
|
3957
|
-
socket,
|
|
3958
|
-
id,
|
|
3959
|
-
"AC_ERR_PROCESS",
|
|
3960
|
-
err?.message || "Failed to kill process."
|
|
3961
|
-
);
|
|
3962
|
-
}
|
|
3963
|
-
return;
|
|
3964
|
-
}
|
|
3965
|
-
if (method === "acp.net.request") {
|
|
3966
|
-
recordCapability("network.request");
|
|
3967
|
-
try {
|
|
3968
|
-
if (typeof fetch !== "function") {
|
|
3969
|
-
replyError(socket, id, "AC_ERR_NET", "Fetch is not available.");
|
|
3970
|
-
return;
|
|
3971
|
-
}
|
|
3972
|
-
const controller = new AbortController();
|
|
3973
|
-
const timeout = params.timeoutMs;
|
|
3974
|
-
let timer = null;
|
|
3975
|
-
if (timeout) {
|
|
3976
|
-
timer = setTimeout(() => controller.abort(), Number(timeout));
|
|
3977
|
-
}
|
|
3978
|
-
const res = await fetch(String(params.url), {
|
|
3979
|
-
method: params.method || "GET",
|
|
3980
|
-
headers: params.headers || {},
|
|
3981
|
-
body: params.body,
|
|
3982
|
-
signal: controller.signal
|
|
3983
|
-
});
|
|
3984
|
-
if (timer) clearTimeout(timer);
|
|
3985
|
-
const body = await res.text();
|
|
3986
|
-
const headers = {};
|
|
3987
|
-
res.headers.forEach((value, key) => {
|
|
3988
|
-
headers[key] = value;
|
|
3989
|
-
});
|
|
3990
|
-
reply(socket, id, { status: res.status, headers, body });
|
|
3991
|
-
} catch (err) {
|
|
3992
|
-
replyError(
|
|
3993
|
-
socket,
|
|
3994
|
-
id,
|
|
3995
|
-
"AC_ERR_NET",
|
|
3996
|
-
err?.message || "Network request failed."
|
|
3997
|
-
);
|
|
3998
|
-
}
|
|
3999
|
-
return;
|
|
4000
|
-
}
|
|
4001
|
-
if (method === "acp.backend.start") {
|
|
4002
|
-
recordCapability("backend.run");
|
|
4003
|
-
if (!manifest?.backend) {
|
|
4004
|
-
reply(socket, id, { status: "disabled" });
|
|
4005
|
-
return;
|
|
4006
|
-
}
|
|
4007
|
-
const backendConfig = manifest.backend;
|
|
4008
|
-
const existing = backendState.get(appId);
|
|
4009
|
-
if (existing?.status === "running") {
|
|
4010
|
-
reply(socket, id, { status: "running", url: existing.url });
|
|
4011
|
-
return;
|
|
4012
|
-
}
|
|
4013
|
-
try {
|
|
4014
|
-
let assignedPort = null;
|
|
4015
|
-
const declaredPort = backendConfig.env?.PORT;
|
|
4016
|
-
if (declaredPort === void 0 || String(declaredPort) === "0") {
|
|
4017
|
-
assignedPort = await allocatePort();
|
|
4018
|
-
} else if (!Number.isNaN(Number(declaredPort))) {
|
|
4019
|
-
assignedPort = Number(declaredPort);
|
|
4020
|
-
}
|
|
4021
|
-
const env = {
|
|
4022
|
-
...process.env,
|
|
4023
|
-
...backendConfig.env || {},
|
|
4024
|
-
AGENTCONNECT_HOST: `ws://${host}:${port}`,
|
|
4025
|
-
AGENTCONNECT_APP_ID: appId
|
|
4026
|
-
};
|
|
4027
|
-
if (assignedPort) {
|
|
4028
|
-
env.PORT = String(assignedPort);
|
|
4029
|
-
env.AGENTCONNECT_APP_PORT = String(assignedPort);
|
|
4030
|
-
}
|
|
4031
|
-
const cwd = backendConfig.cwd ? resolveAppPathInternal(backendConfig.cwd) : basePath;
|
|
4032
|
-
const args2 = backendConfig.args || [];
|
|
4033
|
-
const child = spawn5(backendConfig.command, args2, {
|
|
4034
|
-
cwd,
|
|
4035
|
-
env,
|
|
4036
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
4037
|
-
});
|
|
4038
|
-
child.stdout?.on("data", () => void 0);
|
|
4039
|
-
child.stderr?.on("data", () => void 0);
|
|
4040
|
-
const url = assignedPort ? `http://${host}:${assignedPort}` : void 0;
|
|
4041
|
-
const record = { status: "starting", pid: child.pid, url };
|
|
4042
|
-
backendState.set(appId, record);
|
|
4043
|
-
child.on("exit", () => {
|
|
4044
|
-
backendState.set(appId, { status: "stopped" });
|
|
4045
|
-
});
|
|
4046
|
-
if (backendConfig.healthcheck?.type === "http" && assignedPort) {
|
|
4047
|
-
const healthUrl = `http://${host}:${assignedPort}${backendConfig.healthcheck.path}`;
|
|
4048
|
-
const ok = await waitForHealthcheck(healthUrl);
|
|
4049
|
-
if (!ok) {
|
|
4050
|
-
child.kill("SIGTERM");
|
|
4051
|
-
backendState.set(appId, { status: "error" });
|
|
4052
|
-
reply(socket, id, { status: "error" });
|
|
4053
|
-
return;
|
|
4054
|
-
}
|
|
4055
|
-
}
|
|
4056
|
-
backendState.set(appId, { status: "running", pid: child.pid, url });
|
|
4057
|
-
reply(socket, id, { status: "running", url });
|
|
4058
|
-
} catch (err) {
|
|
4059
|
-
replyError(
|
|
4060
|
-
socket,
|
|
4061
|
-
id,
|
|
4062
|
-
"AC_ERR_BACKEND",
|
|
4063
|
-
err?.message || "Failed to start backend."
|
|
4064
|
-
);
|
|
4065
|
-
}
|
|
4066
|
-
return;
|
|
4067
|
-
}
|
|
4068
|
-
if (method === "acp.backend.stop") {
|
|
4069
|
-
recordCapability("backend.run");
|
|
4070
|
-
const current = backendState.get(appId);
|
|
4071
|
-
if (!current?.pid) {
|
|
4072
|
-
backendState.set(appId, { status: "stopped" });
|
|
4073
|
-
reply(socket, id, { status: "stopped" });
|
|
4074
|
-
return;
|
|
4075
|
-
}
|
|
4076
|
-
try {
|
|
4077
|
-
process.kill(current.pid, "SIGTERM");
|
|
4078
|
-
} catch {
|
|
4079
|
-
}
|
|
4080
|
-
backendState.set(appId, { status: "stopped" });
|
|
4081
|
-
reply(socket, id, { status: "stopped" });
|
|
4082
|
-
return;
|
|
4083
|
-
}
|
|
4084
|
-
if (method === "acp.backend.status") {
|
|
4085
|
-
recordCapability("backend.run");
|
|
4086
|
-
const current = backendState.get(appId) || { status: "stopped" };
|
|
4087
|
-
reply(socket, id, { status: current.status, url: current.url });
|
|
4088
|
-
return;
|
|
4089
|
-
}
|
|
4090
|
-
if (method === "acp.capabilities.observed") {
|
|
4091
|
-
reply(socket, id, { ...observedTracker.snapshot() });
|
|
4092
|
-
return;
|
|
4093
|
-
}
|
|
4094
|
-
reply(socket, id, {});
|
|
4095
|
-
});
|
|
4096
|
-
});
|
|
4097
|
-
server.listen(port, host, () => {
|
|
4098
|
-
console.log(`AgentConnect dev host running at ws://${host}:${port}`);
|
|
4099
|
-
if (appPath) console.log(`App path: ${appPath}`);
|
|
4100
|
-
if (uiUrl) console.log(`UI dev server: ${uiUrl}`);
|
|
4101
|
-
});
|
|
4102
|
-
process.on("SIGINT", () => {
|
|
4103
|
-
try {
|
|
4104
|
-
observedTracker.flush();
|
|
4105
|
-
} catch {
|
|
4106
|
-
}
|
|
4107
|
-
server.close(() => process.exit(0));
|
|
4108
|
-
});
|
|
4109
|
-
}
|
|
4
|
+
import path7 from "path";
|
|
5
|
+
import { promises as fs5 } from "fs";
|
|
6
|
+
import { createPrivateKey, createPublicKey as createPublicKey2, sign as signData } from "crypto";
|
|
7
|
+
import { startDevHost } from "@agentconnect/host";
|
|
4110
8
|
|
|
4111
9
|
// src/paths.ts
|
|
4112
|
-
import
|
|
10
|
+
import path from "path";
|
|
4113
11
|
import { fileURLToPath } from "url";
|
|
4114
|
-
import { promises as
|
|
12
|
+
import { promises as fs } from "fs";
|
|
4115
13
|
async function exists(target) {
|
|
4116
14
|
try {
|
|
4117
|
-
await
|
|
15
|
+
await fs.stat(target);
|
|
4118
16
|
return true;
|
|
4119
17
|
} catch {
|
|
4120
18
|
return false;
|
|
4121
19
|
}
|
|
4122
20
|
}
|
|
4123
21
|
function resolveAppPath(input) {
|
|
4124
|
-
const candidate = input ?
|
|
22
|
+
const candidate = input ? path.resolve(input) : process.cwd();
|
|
4125
23
|
return candidate;
|
|
4126
24
|
}
|
|
4127
25
|
async function findSchemaDir() {
|
|
4128
26
|
const envDir = process.env.AGENTCONNECT_SCHEMA_DIR;
|
|
4129
27
|
if (envDir && await exists(envDir)) return envDir;
|
|
4130
|
-
const start =
|
|
28
|
+
const start = path.dirname(fileURLToPath(import.meta.url));
|
|
4131
29
|
const roots = [start, process.cwd()];
|
|
4132
30
|
for (const root of roots) {
|
|
4133
31
|
let current = root;
|
|
4134
32
|
for (let i = 0; i < 8; i += 1) {
|
|
4135
|
-
const candidate =
|
|
33
|
+
const candidate = path.join(current, "schemas");
|
|
4136
34
|
if (await exists(candidate)) return candidate;
|
|
4137
|
-
const parent =
|
|
35
|
+
const parent = path.dirname(current);
|
|
4138
36
|
if (parent === current) break;
|
|
4139
37
|
current = parent;
|
|
4140
38
|
}
|
|
@@ -4143,25 +41,25 @@ async function findSchemaDir() {
|
|
|
4143
41
|
}
|
|
4144
42
|
|
|
4145
43
|
// src/zip.ts
|
|
4146
|
-
import
|
|
4147
|
-
import
|
|
44
|
+
import fs3 from "fs";
|
|
45
|
+
import path3 from "path";
|
|
4148
46
|
import { createHash } from "crypto";
|
|
4149
47
|
import yazl from "yazl";
|
|
4150
48
|
import yauzl from "yauzl";
|
|
4151
49
|
|
|
4152
50
|
// src/fs-utils.ts
|
|
4153
|
-
import
|
|
4154
|
-
import { promises as
|
|
51
|
+
import path2 from "path";
|
|
52
|
+
import { promises as fs2 } from "fs";
|
|
4155
53
|
async function collectFiles(root, options = {}) {
|
|
4156
54
|
const ignoreNames = new Set(options.ignoreNames || []);
|
|
4157
55
|
const ignorePaths = new Set(options.ignorePaths || []);
|
|
4158
56
|
const files = [];
|
|
4159
57
|
async function walk(dir) {
|
|
4160
|
-
const entries = await
|
|
58
|
+
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
4161
59
|
for (const entry of entries) {
|
|
4162
60
|
if (ignoreNames.has(entry.name)) continue;
|
|
4163
|
-
const fullPath =
|
|
4164
|
-
const rel =
|
|
61
|
+
const fullPath = path2.join(dir, entry.name);
|
|
62
|
+
const rel = path2.relative(root, fullPath);
|
|
4165
63
|
if (ignorePaths.has(rel)) continue;
|
|
4166
64
|
if (entry.isDirectory()) {
|
|
4167
65
|
await walk(fullPath);
|
|
@@ -4174,16 +72,16 @@ async function collectFiles(root, options = {}) {
|
|
|
4174
72
|
return files.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
4175
73
|
}
|
|
4176
74
|
async function readJson(filePath) {
|
|
4177
|
-
const raw = await
|
|
75
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
4178
76
|
return JSON.parse(raw);
|
|
4179
77
|
}
|
|
4180
78
|
async function writeJson(filePath, data) {
|
|
4181
|
-
await
|
|
4182
|
-
await
|
|
79
|
+
await fs2.mkdir(path2.dirname(filePath), { recursive: true });
|
|
80
|
+
await fs2.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
4183
81
|
}
|
|
4184
82
|
async function fileExists(filePath) {
|
|
4185
83
|
try {
|
|
4186
|
-
await
|
|
84
|
+
await fs2.stat(filePath);
|
|
4187
85
|
return true;
|
|
4188
86
|
} catch {
|
|
4189
87
|
return false;
|
|
@@ -4194,7 +92,7 @@ async function fileExists(filePath) {
|
|
|
4194
92
|
async function hashFile(filePath) {
|
|
4195
93
|
return new Promise((resolve, reject) => {
|
|
4196
94
|
const hash = createHash("sha256");
|
|
4197
|
-
const stream =
|
|
95
|
+
const stream = fs3.createReadStream(filePath);
|
|
4198
96
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
4199
97
|
stream.on("error", reject);
|
|
4200
98
|
stream.on("end", () => resolve(hash.digest("hex")));
|
|
@@ -4206,14 +104,14 @@ async function zipDirectory({
|
|
|
4206
104
|
ignoreNames = [],
|
|
4207
105
|
ignorePaths = []
|
|
4208
106
|
}) {
|
|
4209
|
-
await
|
|
107
|
+
await fs3.promises.mkdir(path3.dirname(outputPath), { recursive: true });
|
|
4210
108
|
const zipfile = new yazl.ZipFile();
|
|
4211
109
|
const files = await collectFiles(inputDir, { ignoreNames, ignorePaths });
|
|
4212
110
|
for (const file of files) {
|
|
4213
111
|
zipfile.addFile(file.fullPath, file.rel);
|
|
4214
112
|
}
|
|
4215
113
|
return new Promise((resolve, reject) => {
|
|
4216
|
-
const outStream =
|
|
114
|
+
const outStream = fs3.createWriteStream(outputPath);
|
|
4217
115
|
zipfile.outputStream.pipe(outStream);
|
|
4218
116
|
outStream.on("close", () => resolve());
|
|
4219
117
|
outStream.on("error", reject);
|
|
@@ -4262,10 +160,10 @@ async function readZipEntry(zipPath, entryName) {
|
|
|
4262
160
|
}
|
|
4263
161
|
|
|
4264
162
|
// src/manifest.ts
|
|
4265
|
-
import
|
|
163
|
+
import path4 from "path";
|
|
4266
164
|
import Ajv from "ajv";
|
|
4267
165
|
async function readManifestFromDir(appPath) {
|
|
4268
|
-
const manifestPath =
|
|
166
|
+
const manifestPath = path4.join(appPath, "agentconnect.app.json");
|
|
4269
167
|
if (!await fileExists(manifestPath)) {
|
|
4270
168
|
throw new Error("agentconnect.app.json not found in app directory.");
|
|
4271
169
|
}
|
|
@@ -4283,7 +181,7 @@ async function loadManifestSchema() {
|
|
|
4283
181
|
if (!schemaDir) {
|
|
4284
182
|
throw new Error("Unable to locate schemas directory.");
|
|
4285
183
|
}
|
|
4286
|
-
const schemaPath =
|
|
184
|
+
const schemaPath = path4.join(schemaDir, "app-manifest.json");
|
|
4287
185
|
return readJson(schemaPath);
|
|
4288
186
|
}
|
|
4289
187
|
async function validateManifest(manifest) {
|
|
@@ -4295,9 +193,9 @@ async function validateManifest(manifest) {
|
|
|
4295
193
|
}
|
|
4296
194
|
|
|
4297
195
|
// src/registry.ts
|
|
4298
|
-
import
|
|
4299
|
-
import { promises as
|
|
4300
|
-
function
|
|
196
|
+
import path5 from "path";
|
|
197
|
+
import { promises as fs4 } from "fs";
|
|
198
|
+
function compareSemver(a, b) {
|
|
4301
199
|
const parse = (v) => v.split("-")[0].split(".").map((n) => Number(n));
|
|
4302
200
|
const pa = parse(a);
|
|
4303
201
|
const pb = parse(b);
|
|
@@ -4319,26 +217,26 @@ async function publishPackage({
|
|
|
4319
217
|
if (!appId || !version) {
|
|
4320
218
|
throw new Error("Manifest must include id and version.");
|
|
4321
219
|
}
|
|
4322
|
-
const entryDir =
|
|
4323
|
-
await
|
|
4324
|
-
const targetZip =
|
|
4325
|
-
await
|
|
4326
|
-
const manifestPath =
|
|
220
|
+
const entryDir = path5.join(registryPath, "apps", appId, version);
|
|
221
|
+
await fs4.mkdir(entryDir, { recursive: true });
|
|
222
|
+
const targetZip = path5.join(entryDir, "app.zip");
|
|
223
|
+
await fs4.copyFile(zipPath, targetZip);
|
|
224
|
+
const manifestPath = path5.join(entryDir, "manifest.json");
|
|
4327
225
|
await writeJson(manifestPath, resolvedManifest);
|
|
4328
226
|
let signatureOut = null;
|
|
4329
227
|
if (signaturePath) {
|
|
4330
|
-
signatureOut =
|
|
4331
|
-
await
|
|
228
|
+
signatureOut = path5.join(entryDir, "signature.json");
|
|
229
|
+
await fs4.copyFile(signaturePath, signatureOut);
|
|
4332
230
|
}
|
|
4333
231
|
const hash = await hashFile(zipPath);
|
|
4334
|
-
const indexPath =
|
|
232
|
+
const indexPath = path5.join(registryPath, "index.json");
|
|
4335
233
|
const index = await fileExists(indexPath) ? await readJson(indexPath) : { apps: {} };
|
|
4336
234
|
if (!index.apps) index.apps = {};
|
|
4337
235
|
if (!index.apps[appId]) {
|
|
4338
236
|
index.apps[appId] = { latest: version, versions: {} };
|
|
4339
237
|
}
|
|
4340
238
|
index.apps[appId].versions[version] = {
|
|
4341
|
-
path:
|
|
239
|
+
path: path5.relative(registryPath, targetZip).replace(/\\/g, "/"),
|
|
4342
240
|
manifest: resolvedManifest,
|
|
4343
241
|
signature: signatureOut ? {
|
|
4344
242
|
algorithm: "unknown",
|
|
@@ -4348,7 +246,7 @@ async function publishPackage({
|
|
|
4348
246
|
hash
|
|
4349
247
|
};
|
|
4350
248
|
const currentLatest = index.apps[appId].latest;
|
|
4351
|
-
if (!currentLatest ||
|
|
249
|
+
if (!currentLatest || compareSemver(version, currentLatest) > 0) {
|
|
4352
250
|
index.apps[appId].latest = version;
|
|
4353
251
|
}
|
|
4354
252
|
await writeJson(indexPath, index);
|
|
@@ -4356,9 +254,9 @@ async function publishPackage({
|
|
|
4356
254
|
}
|
|
4357
255
|
|
|
4358
256
|
// src/registry-validate.ts
|
|
4359
|
-
import
|
|
257
|
+
import path6 from "path";
|
|
4360
258
|
import { createPublicKey, verify as verifySignature } from "crypto";
|
|
4361
|
-
function
|
|
259
|
+
function compareSemver2(a, b) {
|
|
4362
260
|
const parse = (v) => v.split("-")[0].split(".").map((n) => Number(n));
|
|
4363
261
|
const pa = parse(a);
|
|
4364
262
|
const pb = parse(b);
|
|
@@ -4370,7 +268,7 @@ function compareSemver5(a, b) {
|
|
|
4370
268
|
}
|
|
4371
269
|
function resolveEntry(registryPath, entryPath) {
|
|
4372
270
|
if (!entryPath || typeof entryPath !== "string") return null;
|
|
4373
|
-
return
|
|
271
|
+
return path6.resolve(registryPath, entryPath);
|
|
4374
272
|
}
|
|
4375
273
|
function normalizeSignatureAlg(signatureAlg) {
|
|
4376
274
|
const value = String(signatureAlg || "").toLowerCase();
|
|
@@ -4411,7 +309,7 @@ async function validateRegistry({
|
|
|
4411
309
|
}) {
|
|
4412
310
|
const errors = [];
|
|
4413
311
|
const warnings = [];
|
|
4414
|
-
const indexPath =
|
|
312
|
+
const indexPath = path6.join(registryPath, "index.json");
|
|
4415
313
|
if (!await fileExists(indexPath)) {
|
|
4416
314
|
return {
|
|
4417
315
|
valid: false,
|
|
@@ -4466,9 +364,9 @@ async function validateRegistry({
|
|
|
4466
364
|
message: `App ${appId} latest (${latest}) not found in versions.`
|
|
4467
365
|
});
|
|
4468
366
|
}
|
|
4469
|
-
const sorted = [...versionKeys].sort(
|
|
367
|
+
const sorted = [...versionKeys].sort(compareSemver2);
|
|
4470
368
|
const expectedLatest = sorted[sorted.length - 1];
|
|
4471
|
-
if (latest && expectedLatest &&
|
|
369
|
+
if (latest && expectedLatest && compareSemver2(latest, expectedLatest) !== 0) {
|
|
4472
370
|
warnings.push({
|
|
4473
371
|
path: `apps.${appId}`,
|
|
4474
372
|
message: `App ${appId} latest (${latest}) is not the newest (${expectedLatest}).`
|
|
@@ -4627,7 +525,7 @@ async function main() {
|
|
|
4627
525
|
}
|
|
4628
526
|
if (command === "pack") {
|
|
4629
527
|
const appPath = resolveAppPath(getFlag("--app", "-a") ?? void 0);
|
|
4630
|
-
const outPath = getFlag("--out", "-o") ||
|
|
528
|
+
const outPath = getFlag("--out", "-o") || path7.join(appPath, "dist", "app.zip");
|
|
4631
529
|
const manifest = await readManifestFromDir(appPath);
|
|
4632
530
|
const validation = await validateManifest(manifest);
|
|
4633
531
|
if (!validation.valid) {
|
|
@@ -4637,7 +535,7 @@ async function main() {
|
|
|
4637
535
|
}
|
|
4638
536
|
const ignoreNames = ["node_modules", ".git", ".DS_Store"];
|
|
4639
537
|
const ignorePaths = [];
|
|
4640
|
-
const outputRel =
|
|
538
|
+
const outputRel = path7.relative(appPath, outPath);
|
|
4641
539
|
if (!outputRel.startsWith("..") && outputRel !== "") {
|
|
4642
540
|
ignorePaths.push(outputRel);
|
|
4643
541
|
}
|
|
@@ -4649,7 +547,7 @@ async function main() {
|
|
|
4649
547
|
const appArg = getFlag("--app", "-a");
|
|
4650
548
|
const jsonOut = args.includes("--json");
|
|
4651
549
|
const appPath = resolveAppPath(appArg ?? void 0);
|
|
4652
|
-
const stats = await
|
|
550
|
+
const stats = await fs5.stat(appPath).catch(() => null);
|
|
4653
551
|
if (!stats) {
|
|
4654
552
|
console.error("App path not found.");
|
|
4655
553
|
return 1;
|
|
@@ -4690,16 +588,16 @@ async function main() {
|
|
|
4690
588
|
return 1;
|
|
4691
589
|
}
|
|
4692
590
|
const appPath = resolveAppPath(appArg);
|
|
4693
|
-
const appStats = await
|
|
591
|
+
const appStats = await fs5.stat(appPath).catch(() => null);
|
|
4694
592
|
if (!appStats || !appStats.isFile()) {
|
|
4695
593
|
console.error("Sign requires a zip file path.");
|
|
4696
594
|
return 1;
|
|
4697
595
|
}
|
|
4698
|
-
const outPath = getFlag("--out", "-o") ||
|
|
596
|
+
const outPath = getFlag("--out", "-o") || path7.join(path7.dirname(appPath), "app.sig.json");
|
|
4699
597
|
const manifest = await readManifestFromZip(appPath);
|
|
4700
598
|
const hash = await hashFile(appPath);
|
|
4701
599
|
const hashBuffer = Buffer.from(hash, "hex");
|
|
4702
|
-
const privateKeyPem = await
|
|
600
|
+
const privateKeyPem = await fs5.readFile(keyPath, "utf8");
|
|
4703
601
|
const privateKey = createPrivateKey(privateKeyPem);
|
|
4704
602
|
const keyType = privateKey.asymmetricKeyType;
|
|
4705
603
|
const algorithm = keyType === "ed25519" ? null : "sha256";
|
|
@@ -4719,8 +617,8 @@ async function main() {
|
|
|
4719
617
|
publicKey: publicKeyPem,
|
|
4720
618
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4721
619
|
};
|
|
4722
|
-
await
|
|
4723
|
-
await
|
|
620
|
+
await fs5.mkdir(path7.dirname(outPath), { recursive: true });
|
|
621
|
+
await fs5.writeFile(outPath, JSON.stringify(signaturePayload, null, 2), "utf8");
|
|
4724
622
|
console.log(`Signature written to ${outPath}`);
|
|
4725
623
|
return 0;
|
|
4726
624
|
}
|
|
@@ -4735,7 +633,7 @@ async function main() {
|
|
|
4735
633
|
return 1;
|
|
4736
634
|
}
|
|
4737
635
|
const appPath = resolveAppPath(appArg);
|
|
4738
|
-
const appStats = await
|
|
636
|
+
const appStats = await fs5.stat(appPath).catch(() => null);
|
|
4739
637
|
if (!appStats || !appStats.isFile()) {
|
|
4740
638
|
console.error("Publish requires a zip file path.");
|
|
4741
639
|
return 1;
|