svamp-cli 0.2.264 → 0.2.266
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/{agentCommands-BZPDL-1B.mjs → agentCommands-aCeb0xu5.mjs} +5 -5
- package/dist/{auth-Dv8z9Dg0.mjs → auth-BZ1HwSaj.mjs} +2 -2
- package/dist/{cli-CNorTj26.mjs → cli-DyIeeAe9.mjs} +63 -63
- package/dist/cli.mjs +3 -3
- package/dist/{commands-DuPRCS2G.mjs → commands-BX6_mFbE.mjs} +1 -1
- package/dist/{commands-Dg_nTA2I.mjs → commands-BgjoBiDv.mjs} +2 -2
- package/dist/{commands-Kk7AH5dR.mjs → commands-Bp7U7HSm.mjs} +2 -2
- package/dist/{commands-BmpHRl5s.mjs → commands-BqYexHaV.mjs} +2 -2
- package/dist/{commands-DNyzbWPw.mjs → commands-CWZSjny_.mjs} +6 -6
- package/dist/{commands-CSNOKRVn.mjs → commands-f4bL9Ar6.mjs} +2 -2
- package/dist/{commands-D_YqjDY4.mjs → commands-mmoabAhq.mjs} +3 -3
- package/dist/{fleet-DkFOlkxS.mjs → fleet-CvycbTRs.mjs} +1 -1
- package/dist/{frpc-DmaWRu0M.mjs → frpc-CxqX4sQ7.mjs} +2 -2
- package/dist/{headlessCli-DFsJaJ2p.mjs → headlessCli-Bf9mDfRN.mjs} +3 -3
- package/dist/index.mjs +2 -2
- package/dist/package-CfLZkKfk.mjs +64 -0
- package/dist/{rpc-DswYjrCh.mjs → rpc-BB9tE2dR.mjs} +2 -2
- package/dist/{rpc-Db3BR76v.mjs → rpc-BXmmzMmw.mjs} +2 -2
- package/dist/{run-C3qTQEDV.mjs → run-Byjz649i.mjs} +1 -1
- package/dist/{run-kkeXP2d_.mjs → run-CetRvMab.mjs} +1103 -378
- package/dist/{scheduler-DhKDP3hC.mjs → scheduler-Xhs6PCCU.mjs} +2 -2
- package/dist/{serveCommands-BVJR3K9P.mjs → serveCommands-DGOH5VP9.mjs} +5 -5
- package/dist/{serveManager-a_dIQuK4.mjs → serveManager-Bk6Fkoha.mjs} +3 -3
- package/dist/{sideband-CIJUNKUA.mjs → sideband-CWu6yjFl.mjs} +2 -2
- package/package.json +2 -2
- package/dist/package-Cz5Z0buw.mjs +0 -64
|
@@ -5,13 +5,13 @@ import path__default, { join as join$1, resolve as resolve$1, dirname as dirname
|
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { execFile, spawn as spawn$1, execSync as execSync$1, exec as exec$1 } from 'child_process';
|
|
7
7
|
import { randomUUID as randomUUID$1 } from 'crypto';
|
|
8
|
-
import { randomBytes, createHash
|
|
9
|
-
import { existsSync, readFileSync,
|
|
10
|
-
import { exec,
|
|
8
|
+
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
|
9
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, chmodSync, rmSync, cpSync, realpathSync, readdirSync, statSync, renameSync, appendFileSync, unlinkSync } from 'node:fs';
|
|
10
|
+
import { exec, spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
|
|
11
11
|
import { promisify } from 'util';
|
|
12
|
-
import { extname, resolve, join, sep, basename, dirname } from 'node:path';
|
|
13
|
-
import { EventEmitter } from 'node:events';
|
|
14
12
|
import os, { homedir, platform } from 'node:os';
|
|
13
|
+
import { join, extname, resolve, sep, basename, dirname } from 'node:path';
|
|
14
|
+
import { EventEmitter } from 'node:events';
|
|
15
15
|
import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
|
|
16
16
|
import { createInterface } from 'node:readline';
|
|
17
17
|
import { mkdir, rm, chmod, access, mkdtemp, copyFile, writeFile, readdir, stat, readFile as readFile$1 } from 'node:fs/promises';
|
|
@@ -1170,52 +1170,862 @@ async function readProcessTable() {
|
|
|
1170
1170
|
const command = parts.slice(5).join(" ");
|
|
1171
1171
|
rows.push({ pid, ppid, etimeSec, cpu, rssKb, command });
|
|
1172
1172
|
}
|
|
1173
|
-
return rows;
|
|
1173
|
+
return rows;
|
|
1174
|
+
}
|
|
1175
|
+
async function detectDescendants(rootPid, excludePids = /* @__PURE__ */ new Set()) {
|
|
1176
|
+
if (!rootPid || rootPid < 1) return [];
|
|
1177
|
+
const table = await readProcessTable();
|
|
1178
|
+
if (table.length === 0) return [];
|
|
1179
|
+
const byPpid = /* @__PURE__ */ new Map();
|
|
1180
|
+
for (const row of table) {
|
|
1181
|
+
const list = byPpid.get(row.ppid);
|
|
1182
|
+
if (list) list.push(row);
|
|
1183
|
+
else byPpid.set(row.ppid, [row]);
|
|
1184
|
+
}
|
|
1185
|
+
const descendants = [];
|
|
1186
|
+
const visited = /* @__PURE__ */ new Set([rootPid]);
|
|
1187
|
+
const queue = [rootPid];
|
|
1188
|
+
const MAX_NODES = 5e3;
|
|
1189
|
+
while (queue.length > 0 && descendants.length < MAX_NODES) {
|
|
1190
|
+
const next = queue.shift();
|
|
1191
|
+
const children = byPpid.get(next);
|
|
1192
|
+
if (!children) continue;
|
|
1193
|
+
for (const child of children) {
|
|
1194
|
+
if (visited.has(child.pid)) continue;
|
|
1195
|
+
visited.add(child.pid);
|
|
1196
|
+
queue.push(child.pid);
|
|
1197
|
+
if (!excludePids.has(child.pid)) {
|
|
1198
|
+
descendants.push(child);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
descendants.sort((a, b) => a.etimeSec - b.etimeSec);
|
|
1203
|
+
return descendants;
|
|
1204
|
+
}
|
|
1205
|
+
async function killDescendant(rootPid, targetPid, signal = "SIGTERM") {
|
|
1206
|
+
if (!rootPid || rootPid < 1) return { killed: false, reason: "no root pid" };
|
|
1207
|
+
if (!targetPid || targetPid < 1) return { killed: false, reason: "invalid target pid" };
|
|
1208
|
+
if (targetPid === rootPid) return { killed: false, reason: "cannot kill session root pid" };
|
|
1209
|
+
const descendants = await detectDescendants(rootPid);
|
|
1210
|
+
if (!descendants.some((d) => d.pid === targetPid)) {
|
|
1211
|
+
return { killed: false, reason: "pid is not a descendant of this session" };
|
|
1212
|
+
}
|
|
1213
|
+
try {
|
|
1214
|
+
process.kill(targetPid, signal);
|
|
1215
|
+
return { killed: true };
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
return { killed: false, reason: err?.message ?? "kill failed" };
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const EXAMPLE_HYPHA_PROXY_URL = "https://proxy.hypha.aicell.io";
|
|
1222
|
+
const MODE_KEY = "SVAMP_CLAUDE_PROXY";
|
|
1223
|
+
const HYPHA_PROXY_URL_KEY = "SVAMP_HYPHA_PROXY_URL";
|
|
1224
|
+
const MANAGED_KEYS = /* @__PURE__ */ new Set([
|
|
1225
|
+
MODE_KEY,
|
|
1226
|
+
HYPHA_PROXY_URL_KEY,
|
|
1227
|
+
"ANTHROPIC_BASE_URL",
|
|
1228
|
+
"ANTHROPIC_API_KEY"
|
|
1229
|
+
]);
|
|
1230
|
+
function normalizeProxyUrl(raw) {
|
|
1231
|
+
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
1232
|
+
if (trimmed.endsWith("/v1")) {
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
"Hypha proxy URL must not end in /v1 \u2014 the SDK appends it, producing double /v1/v1/messages"
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
return trimmed;
|
|
1238
|
+
}
|
|
1239
|
+
function resolveHyphaProxyUrl() {
|
|
1240
|
+
const configured = (process.env[HYPHA_PROXY_URL_KEY] || "").trim();
|
|
1241
|
+
if (!configured) return void 0;
|
|
1242
|
+
try {
|
|
1243
|
+
return normalizeProxyUrl(configured);
|
|
1244
|
+
} catch {
|
|
1245
|
+
return void 0;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
function envFilePath() {
|
|
1249
|
+
const svampHome = process.env.SVAMP_HOME || join(homedir(), ".svamp");
|
|
1250
|
+
return join(svampHome, ".env");
|
|
1251
|
+
}
|
|
1252
|
+
function readEnvLines() {
|
|
1253
|
+
const file = envFilePath();
|
|
1254
|
+
if (!existsSync(file)) return [];
|
|
1255
|
+
return readFileSync(file, "utf-8").split("\n");
|
|
1256
|
+
}
|
|
1257
|
+
function writeEnvLines(lines) {
|
|
1258
|
+
const file = envFilePath();
|
|
1259
|
+
const dir = join(file, "..");
|
|
1260
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1261
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
1262
|
+
lines.pop();
|
|
1263
|
+
}
|
|
1264
|
+
writeFileSync(file, lines.join("\n") + "\n", "utf-8");
|
|
1265
|
+
}
|
|
1266
|
+
function updateEnvFile(updates) {
|
|
1267
|
+
const lines = readEnvLines();
|
|
1268
|
+
const kept = [];
|
|
1269
|
+
const remainingUpdates = new Map(Object.entries(updates));
|
|
1270
|
+
for (const line of lines) {
|
|
1271
|
+
const trimmed = line.trim();
|
|
1272
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
1273
|
+
kept.push(line);
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
const eq = trimmed.indexOf("=");
|
|
1277
|
+
const key = eq === -1 ? trimmed : trimmed.slice(0, eq);
|
|
1278
|
+
if (remainingUpdates.has(key)) {
|
|
1279
|
+
const v = remainingUpdates.get(key);
|
|
1280
|
+
remainingUpdates.delete(key);
|
|
1281
|
+
if (v === void 0) continue;
|
|
1282
|
+
kept.push(`${key}=${v}`);
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
kept.push(line);
|
|
1286
|
+
}
|
|
1287
|
+
for (const [key, v] of remainingUpdates) {
|
|
1288
|
+
if (v === void 0) continue;
|
|
1289
|
+
kept.push(`${key}=${v}`);
|
|
1290
|
+
}
|
|
1291
|
+
writeEnvLines(kept);
|
|
1292
|
+
}
|
|
1293
|
+
function currentMode() {
|
|
1294
|
+
const raw = (process.env[MODE_KEY] || "").trim().toLowerCase();
|
|
1295
|
+
if (raw === "hypha" || raw === "hypha-proxy") return "hypha";
|
|
1296
|
+
if (raw === "custom") return "custom";
|
|
1297
|
+
return "login";
|
|
1298
|
+
}
|
|
1299
|
+
function redactKey(key) {
|
|
1300
|
+
if (!key) return void 0;
|
|
1301
|
+
if (key.length <= 12) return "***";
|
|
1302
|
+
return `${key.slice(0, 8)}\u2026${key.slice(-4)}`;
|
|
1303
|
+
}
|
|
1304
|
+
function getClaudeAuthStatus() {
|
|
1305
|
+
const mode = currentMode();
|
|
1306
|
+
if (mode === "hypha") {
|
|
1307
|
+
const token = process.env.HYPHA_TOKEN;
|
|
1308
|
+
const proxyUrl = resolveHyphaProxyUrl();
|
|
1309
|
+
return {
|
|
1310
|
+
mode,
|
|
1311
|
+
baseUrl: proxyUrl,
|
|
1312
|
+
apiKeyPreview: redactKey(token),
|
|
1313
|
+
hyphaTokenMissing: !token,
|
|
1314
|
+
hyphaProxyUrlMissing: !proxyUrl
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
if (mode === "custom") {
|
|
1318
|
+
return {
|
|
1319
|
+
mode,
|
|
1320
|
+
baseUrl: process.env.ANTHROPIC_BASE_URL,
|
|
1321
|
+
apiKeyPreview: redactKey(process.env.ANTHROPIC_API_KEY)
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
return { mode: "login" };
|
|
1325
|
+
}
|
|
1326
|
+
function setClaudeAuthHyphaProxy(url) {
|
|
1327
|
+
if (!url || !url.trim()) {
|
|
1328
|
+
throw new Error(
|
|
1329
|
+
`A Hypha proxy URL is required (no default). Example: svamp daemon auth use-hypha-proxy ${EXAMPLE_HYPHA_PROXY_URL}`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
updateEnvFile({
|
|
1333
|
+
[MODE_KEY]: "hypha",
|
|
1334
|
+
[HYPHA_PROXY_URL_KEY]: normalizeProxyUrl(url),
|
|
1335
|
+
// Hypha mode resolves token live at spawn — no stored copy.
|
|
1336
|
+
ANTHROPIC_BASE_URL: void 0,
|
|
1337
|
+
ANTHROPIC_API_KEY: void 0
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
function setClaudeAuthLogin() {
|
|
1341
|
+
updateEnvFile({
|
|
1342
|
+
[MODE_KEY]: void 0,
|
|
1343
|
+
[HYPHA_PROXY_URL_KEY]: void 0,
|
|
1344
|
+
ANTHROPIC_BASE_URL: void 0,
|
|
1345
|
+
ANTHROPIC_API_KEY: void 0
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
function setClaudeAuthCustom(baseUrl, apiKey) {
|
|
1349
|
+
if (!baseUrl) throw new Error("ANTHROPIC_BASE_URL is required for custom mode");
|
|
1350
|
+
if (!apiKey) throw new Error("ANTHROPIC_API_KEY is required for custom mode");
|
|
1351
|
+
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
1352
|
+
if (trimmed.endsWith("/v1")) {
|
|
1353
|
+
throw new Error(
|
|
1354
|
+
"ANTHROPIC_BASE_URL must not end in /v1 \u2014 the SDK appends it, producing double /v1/v1/messages"
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
updateEnvFile({
|
|
1358
|
+
[MODE_KEY]: "custom",
|
|
1359
|
+
[HYPHA_PROXY_URL_KEY]: void 0,
|
|
1360
|
+
ANTHROPIC_BASE_URL: trimmed,
|
|
1361
|
+
ANTHROPIC_API_KEY: apiKey
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
function applyClaudeProxyEnv(spawnEnv) {
|
|
1365
|
+
const mode = currentMode();
|
|
1366
|
+
if (mode === "hypha") {
|
|
1367
|
+
const proxyUrl = resolveHyphaProxyUrl();
|
|
1368
|
+
if (!proxyUrl) {
|
|
1369
|
+
delete spawnEnv.ANTHROPIC_BASE_URL;
|
|
1370
|
+
delete spawnEnv.ANTHROPIC_API_KEY;
|
|
1371
|
+
return `hypha mode but ${HYPHA_PROXY_URL_KEY} not configured \u2014 run \`svamp daemon auth use-hypha-proxy <URL>\`; falling back to login`;
|
|
1372
|
+
}
|
|
1373
|
+
const token = process.env.HYPHA_TOKEN;
|
|
1374
|
+
if (!token) {
|
|
1375
|
+
delete spawnEnv.ANTHROPIC_BASE_URL;
|
|
1376
|
+
delete spawnEnv.ANTHROPIC_API_KEY;
|
|
1377
|
+
return "hypha mode but HYPHA_TOKEN missing \u2014 falling back to login";
|
|
1378
|
+
}
|
|
1379
|
+
spawnEnv.ANTHROPIC_BASE_URL = proxyUrl;
|
|
1380
|
+
spawnEnv.ANTHROPIC_API_KEY = token;
|
|
1381
|
+
const cacheOverride = spawnEnv.ENABLE_PROMPT_CACHING_1H ?? process.env.ENABLE_PROMPT_CACHING_1H ?? spawnEnv.FORCE_PROMPT_CACHING_5M ?? process.env.FORCE_PROMPT_CACHING_5M;
|
|
1382
|
+
if (cacheOverride === void 0) spawnEnv.ENABLE_PROMPT_CACHING_1H = "1";
|
|
1383
|
+
return `hypha proxy (${proxyUrl}, 1h cache TTL)`;
|
|
1384
|
+
}
|
|
1385
|
+
if (mode === "custom") {
|
|
1386
|
+
const url = process.env.ANTHROPIC_BASE_URL;
|
|
1387
|
+
const key = process.env.ANTHROPIC_API_KEY;
|
|
1388
|
+
if (!url || !key) {
|
|
1389
|
+
delete spawnEnv.ANTHROPIC_BASE_URL;
|
|
1390
|
+
delete spawnEnv.ANTHROPIC_API_KEY;
|
|
1391
|
+
return "custom mode but ANTHROPIC_BASE_URL/API_KEY missing \u2014 falling back to login";
|
|
1392
|
+
}
|
|
1393
|
+
spawnEnv.ANTHROPIC_BASE_URL = url;
|
|
1394
|
+
spawnEnv.ANTHROPIC_API_KEY = key;
|
|
1395
|
+
return `custom proxy (${url})`;
|
|
1396
|
+
}
|
|
1397
|
+
delete spawnEnv.ANTHROPIC_BASE_URL;
|
|
1398
|
+
delete spawnEnv.ANTHROPIC_API_KEY;
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
const MANAGED_ENV_KEYS = Array.from(MANAGED_KEYS);
|
|
1402
|
+
|
|
1403
|
+
var claudeAuth = /*#__PURE__*/Object.freeze({
|
|
1404
|
+
__proto__: null,
|
|
1405
|
+
EXAMPLE_HYPHA_PROXY_URL: EXAMPLE_HYPHA_PROXY_URL,
|
|
1406
|
+
MANAGED_ENV_KEYS: MANAGED_ENV_KEYS,
|
|
1407
|
+
applyClaudeProxyEnv: applyClaudeProxyEnv,
|
|
1408
|
+
getClaudeAuthStatus: getClaudeAuthStatus,
|
|
1409
|
+
resolveHyphaProxyUrl: resolveHyphaProxyUrl,
|
|
1410
|
+
setClaudeAuthCustom: setClaudeAuthCustom,
|
|
1411
|
+
setClaudeAuthHyphaProxy: setClaudeAuthHyphaProxy,
|
|
1412
|
+
setClaudeAuthLogin: setClaudeAuthLogin,
|
|
1413
|
+
updateEnvFile: updateEnvFile
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
function codexPermissionSettings(mode) {
|
|
1417
|
+
switch (mode) {
|
|
1418
|
+
case "yolo":
|
|
1419
|
+
case "bypassPermissions":
|
|
1420
|
+
case "danger-full-access":
|
|
1421
|
+
return { approvalPolicy: "never", sandbox: "danger-full-access" };
|
|
1422
|
+
case "safe-yolo":
|
|
1423
|
+
case "acceptEdits":
|
|
1424
|
+
return { approvalPolicy: "on-failure", sandbox: "workspace-write" };
|
|
1425
|
+
case "read-only":
|
|
1426
|
+
case "plan":
|
|
1427
|
+
return { approvalPolicy: "on-request", sandbox: "read-only" };
|
|
1428
|
+
case "default":
|
|
1429
|
+
default:
|
|
1430
|
+
return { approvalPolicy: "on-request", sandbox: "workspace-write" };
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
const CODEX_PROVIDER_KEY_ENV = "SVAMP_CODEX_API_KEY";
|
|
1434
|
+
const PROVIDER_NAME = "svamp";
|
|
1435
|
+
function resolveCodexProvider(env = process.env) {
|
|
1436
|
+
return {
|
|
1437
|
+
apiBase: env.SVAMP_CODEX_API_BASE || env.LLM_API_BASE || void 0,
|
|
1438
|
+
apiKey: env.SVAMP_CODEX_API_KEY || env.LLM_API_KEY || void 0,
|
|
1439
|
+
model: env.SVAMP_CODEX_MODEL || env.LLM_MODEL || void 0
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
function hasCustomProvider(cfg) {
|
|
1443
|
+
return !!(cfg.apiBase && cfg.apiKey);
|
|
1444
|
+
}
|
|
1445
|
+
function buildCodexProviderArgs(cfg) {
|
|
1446
|
+
if (!hasCustomProvider(cfg)) {
|
|
1447
|
+
return { configArgs: cfg.model ? ["-c", `model=${JSON.stringify(cfg.model)}`] : [], env: {}, model: cfg.model };
|
|
1448
|
+
}
|
|
1449
|
+
const configArgs = [
|
|
1450
|
+
"-c",
|
|
1451
|
+
`model_provider=${JSON.stringify(PROVIDER_NAME)}`,
|
|
1452
|
+
"-c",
|
|
1453
|
+
`model_providers.${PROVIDER_NAME}.name=${JSON.stringify(PROVIDER_NAME)}`,
|
|
1454
|
+
"-c",
|
|
1455
|
+
`model_providers.${PROVIDER_NAME}.base_url=${JSON.stringify(cfg.apiBase)}`,
|
|
1456
|
+
"-c",
|
|
1457
|
+
`model_providers.${PROVIDER_NAME}.env_key=${JSON.stringify(CODEX_PROVIDER_KEY_ENV)}`,
|
|
1458
|
+
"-c",
|
|
1459
|
+
`model_providers.${PROVIDER_NAME}.wire_api=${JSON.stringify("responses")}`
|
|
1460
|
+
];
|
|
1461
|
+
if (cfg.model) configArgs.push("-c", `model=${JSON.stringify(cfg.model)}`);
|
|
1462
|
+
return { configArgs, env: { [CODEX_PROVIDER_KEY_ENV]: cfg.apiKey }, model: cfg.model };
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
var codexProvider = /*#__PURE__*/Object.freeze({
|
|
1466
|
+
__proto__: null,
|
|
1467
|
+
CODEX_PROVIDER_KEY_ENV: CODEX_PROVIDER_KEY_ENV,
|
|
1468
|
+
buildCodexProviderArgs: buildCodexProviderArgs,
|
|
1469
|
+
codexPermissionSettings: codexPermissionSettings,
|
|
1470
|
+
hasCustomProvider: hasCustomProvider,
|
|
1471
|
+
resolveCodexProvider: resolveCodexProvider
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
const METHODS_BY_VENDOR = {
|
|
1475
|
+
anthropic: ["anthropic-oauth", "anthropic-api-key", "claude-oauth-token", "hypha-proxy", "anthropic-gateway"],
|
|
1476
|
+
openai: ["chatgpt-oauth", "openai-api-key", "openai-gateway"]
|
|
1477
|
+
};
|
|
1478
|
+
const OAUTH_METHODS = ["anthropic-oauth", "chatgpt-oauth"];
|
|
1479
|
+
function isOAuthMethod(method) {
|
|
1480
|
+
return OAUTH_METHODS.includes(method);
|
|
1481
|
+
}
|
|
1482
|
+
function accountsFilePath(svampHome) {
|
|
1483
|
+
const home = svampHome || process.env.SVAMP_HOME || join(homedir(), ".svamp");
|
|
1484
|
+
return join(home, "accounts.json");
|
|
1485
|
+
}
|
|
1486
|
+
function accountCodexHomeDir(accountId, svampHome) {
|
|
1487
|
+
const home = svampHome || process.env.SVAMP_HOME || join(homedir(), ".svamp");
|
|
1488
|
+
return join(home, "codex-accounts", accountId);
|
|
1489
|
+
}
|
|
1490
|
+
function loadAccounts(svampHome) {
|
|
1491
|
+
const file = accountsFilePath(svampHome);
|
|
1492
|
+
if (!existsSync(file)) return [];
|
|
1493
|
+
try {
|
|
1494
|
+
const parsed = JSON.parse(readFileSync(file, "utf-8"));
|
|
1495
|
+
if (!parsed || !Array.isArray(parsed.accounts)) return [];
|
|
1496
|
+
return parsed.accounts;
|
|
1497
|
+
} catch {
|
|
1498
|
+
return [];
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
function saveAccounts(accounts, svampHome) {
|
|
1502
|
+
const file = accountsFilePath(svampHome);
|
|
1503
|
+
const dir = join(file, "..");
|
|
1504
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1505
|
+
const payload = { version: 1, accounts };
|
|
1506
|
+
writeFileSync(file, JSON.stringify(payload, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
1507
|
+
try {
|
|
1508
|
+
chmodSync(file, 384);
|
|
1509
|
+
} catch {
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
function previewSecret(secret) {
|
|
1513
|
+
if (!secret) return void 0;
|
|
1514
|
+
if (secret.length <= 8) return "\u2022\u2022\u2022\u2022";
|
|
1515
|
+
return `${secret.slice(0, 4)}\u2026${secret.slice(-4)}`;
|
|
1516
|
+
}
|
|
1517
|
+
function redactAccount(a) {
|
|
1518
|
+
const { secret, accessToken, refreshToken, ...rest } = a;
|
|
1519
|
+
const oauthConnected = isOAuthMethod(a.method) ? a.method === "anthropic-oauth" ? !!accessToken : true : void 0;
|
|
1520
|
+
return {
|
|
1521
|
+
...rest,
|
|
1522
|
+
secretPreview: previewSecret(secret) ?? (accessToken ? previewSecret(accessToken) : void 0),
|
|
1523
|
+
oauthConnected
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
function redactAccounts(accounts) {
|
|
1527
|
+
return accounts.map(redactAccount);
|
|
1528
|
+
}
|
|
1529
|
+
function defaultAccountIds(accounts) {
|
|
1530
|
+
const out = { anthropic: null, openai: null };
|
|
1531
|
+
for (const a of accounts) {
|
|
1532
|
+
if (a.isDefault) out[a.vendor] = a.id;
|
|
1533
|
+
}
|
|
1534
|
+
return out;
|
|
1535
|
+
}
|
|
1536
|
+
function validateAccountInput(input) {
|
|
1537
|
+
const vendor = input.vendor;
|
|
1538
|
+
const method = input.method;
|
|
1539
|
+
if (vendor !== "anthropic" && vendor !== "openai") {
|
|
1540
|
+
throw new Error(`Invalid account vendor: ${String(vendor)}`);
|
|
1541
|
+
}
|
|
1542
|
+
if (!method || !METHODS_BY_VENDOR[vendor].includes(method)) {
|
|
1543
|
+
throw new Error(`Invalid method "${String(method)}" for vendor "${vendor}"`);
|
|
1544
|
+
}
|
|
1545
|
+
if (!input.label || !input.label.trim()) {
|
|
1546
|
+
throw new Error("Account label is required");
|
|
1547
|
+
}
|
|
1548
|
+
const needsSecret = method !== "hypha-proxy" && !isOAuthMethod(method);
|
|
1549
|
+
if (needsSecret && !input.secret && !input.id) {
|
|
1550
|
+
throw new Error(`A credential is required for method "${method}"`);
|
|
1551
|
+
}
|
|
1552
|
+
if ((method === "anthropic-gateway" || method === "openai-gateway") && !input.baseUrl) {
|
|
1553
|
+
throw new Error(`A base URL is required for a custom gateway`);
|
|
1554
|
+
}
|
|
1555
|
+
return { vendor, method };
|
|
1556
|
+
}
|
|
1557
|
+
function upsertAccount(input, svampHome) {
|
|
1558
|
+
const { vendor, method } = validateAccountInput(input);
|
|
1559
|
+
const accounts = loadAccounts(svampHome);
|
|
1560
|
+
const now = Date.now();
|
|
1561
|
+
const existing = input.id ? accounts.find((a) => a.id === input.id) : void 0;
|
|
1562
|
+
const account = {
|
|
1563
|
+
id: existing?.id || input.id || randomUUID(),
|
|
1564
|
+
label: input.label.trim(),
|
|
1565
|
+
vendor,
|
|
1566
|
+
method,
|
|
1567
|
+
// Keep the old secret if the update left it blank (edit label/model without re-entry).
|
|
1568
|
+
secret: input.secret && input.secret.length > 0 ? input.secret : existing?.secret,
|
|
1569
|
+
// OAuth token bundle (anthropic-oauth) — carried through on refresh/update; kept if unset.
|
|
1570
|
+
accessToken: input.accessToken ?? existing?.accessToken,
|
|
1571
|
+
refreshToken: input.refreshToken ?? existing?.refreshToken,
|
|
1572
|
+
expiresAt: input.expiresAt ?? existing?.expiresAt,
|
|
1573
|
+
baseUrl: input.baseUrl?.trim() || void 0,
|
|
1574
|
+
model: input.model?.trim() || void 0,
|
|
1575
|
+
isDefault: input.isDefault ?? existing?.isDefault ?? false,
|
|
1576
|
+
createdBy: existing?.createdBy || input.createdBy,
|
|
1577
|
+
createdAt: existing?.createdAt || now,
|
|
1578
|
+
updatedAt: now
|
|
1579
|
+
};
|
|
1580
|
+
let next = accounts.filter((a) => a.id !== account.id);
|
|
1581
|
+
if (account.isDefault) {
|
|
1582
|
+
next = next.map((a) => a.vendor === vendor ? { ...a, isDefault: false } : a);
|
|
1583
|
+
}
|
|
1584
|
+
if (!next.some((a) => a.vendor === vendor && a.isDefault) && !account.isDefault) {
|
|
1585
|
+
account.isDefault = true;
|
|
1586
|
+
}
|
|
1587
|
+
next.push(account);
|
|
1588
|
+
saveAccounts(next, svampHome);
|
|
1589
|
+
return account;
|
|
1590
|
+
}
|
|
1591
|
+
function deleteAccount(id, svampHome) {
|
|
1592
|
+
const accounts = loadAccounts(svampHome);
|
|
1593
|
+
const removed = accounts.find((a) => a.id === id);
|
|
1594
|
+
let next = accounts.filter((a) => a.id !== id);
|
|
1595
|
+
if (removed?.isDefault) {
|
|
1596
|
+
const idx = next.findIndex((a) => a.vendor === removed.vendor);
|
|
1597
|
+
if (idx >= 0) next[idx] = { ...next[idx], isDefault: true };
|
|
1598
|
+
}
|
|
1599
|
+
saveAccounts(next, svampHome);
|
|
1600
|
+
}
|
|
1601
|
+
function setDefaultAccount(vendor, id, svampHome) {
|
|
1602
|
+
const accounts = loadAccounts(svampHome);
|
|
1603
|
+
if (!accounts.some((a) => a.id === id && a.vendor === vendor)) {
|
|
1604
|
+
throw new Error(`No ${vendor} account with id ${id}`);
|
|
1605
|
+
}
|
|
1606
|
+
const next = accounts.map(
|
|
1607
|
+
(a) => a.vendor === vendor ? { ...a, isDefault: a.id === id, updatedAt: Date.now() } : a
|
|
1608
|
+
);
|
|
1609
|
+
saveAccounts(next, svampHome);
|
|
1610
|
+
}
|
|
1611
|
+
function getAccount(id, svampHome) {
|
|
1612
|
+
return loadAccounts(svampHome).find((a) => a.id === id);
|
|
1613
|
+
}
|
|
1614
|
+
function stripV1(url) {
|
|
1615
|
+
const trimmed = url.trim().replace(/\/+$/, "");
|
|
1616
|
+
if (trimmed.endsWith("/v1")) {
|
|
1617
|
+
throw new Error("Base URL must not end in /v1 \u2014 the SDK appends it (double /v1/v1)");
|
|
1618
|
+
}
|
|
1619
|
+
return trimmed;
|
|
1620
|
+
}
|
|
1621
|
+
function resolveAccountEnv(account, env = process.env, svampHome) {
|
|
1622
|
+
switch (account.method) {
|
|
1623
|
+
case "anthropic-api-key":
|
|
1624
|
+
return {
|
|
1625
|
+
vendor: "anthropic",
|
|
1626
|
+
claudeEnv: {
|
|
1627
|
+
ANTHROPIC_API_KEY: account.secret,
|
|
1628
|
+
ANTHROPIC_BASE_URL: account.baseUrl ? stripV1(account.baseUrl) : void 0,
|
|
1629
|
+
CLAUDE_CODE_OAUTH_TOKEN: void 0
|
|
1630
|
+
},
|
|
1631
|
+
describe: `anthropic API key (${account.label})`
|
|
1632
|
+
};
|
|
1633
|
+
case "anthropic-gateway": {
|
|
1634
|
+
const url = stripV1(account.baseUrl || "");
|
|
1635
|
+
return {
|
|
1636
|
+
vendor: "anthropic",
|
|
1637
|
+
claudeEnv: {
|
|
1638
|
+
ANTHROPIC_BASE_URL: url,
|
|
1639
|
+
ANTHROPIC_API_KEY: account.secret,
|
|
1640
|
+
CLAUDE_CODE_OAUTH_TOKEN: void 0
|
|
1641
|
+
},
|
|
1642
|
+
describe: `anthropic gateway ${url} (${account.label})`
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
case "hypha-proxy": {
|
|
1646
|
+
const url = account.baseUrl ? stripV1(account.baseUrl) : resolveHyphaProxyUrl();
|
|
1647
|
+
const token = env.HYPHA_TOKEN;
|
|
1648
|
+
if (!url) throw new Error("hypha-proxy account has no URL (set SVAMP_HYPHA_PROXY_URL or an account base URL)");
|
|
1649
|
+
if (!token) throw new Error("hypha-proxy account requires HYPHA_TOKEN on the machine");
|
|
1650
|
+
return {
|
|
1651
|
+
vendor: "anthropic",
|
|
1652
|
+
claudeEnv: {
|
|
1653
|
+
ANTHROPIC_BASE_URL: url,
|
|
1654
|
+
ANTHROPIC_API_KEY: token,
|
|
1655
|
+
CLAUDE_CODE_OAUTH_TOKEN: void 0,
|
|
1656
|
+
// #0203: subscription backend qualifies for the free 1h prompt-cache TTL.
|
|
1657
|
+
ENABLE_PROMPT_CACHING_1H: env.FORCE_PROMPT_CACHING_5M ? void 0 : "1"
|
|
1658
|
+
},
|
|
1659
|
+
describe: `hypha proxy ${url} (${account.label})`
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
case "claude-oauth-token":
|
|
1663
|
+
return {
|
|
1664
|
+
vendor: "anthropic",
|
|
1665
|
+
claudeEnv: {
|
|
1666
|
+
CLAUDE_CODE_OAUTH_TOKEN: account.secret,
|
|
1667
|
+
ANTHROPIC_BASE_URL: void 0,
|
|
1668
|
+
ANTHROPIC_API_KEY: void 0
|
|
1669
|
+
},
|
|
1670
|
+
describe: `claude subscription token (${account.label})`
|
|
1671
|
+
};
|
|
1672
|
+
case "anthropic-oauth":
|
|
1673
|
+
return {
|
|
1674
|
+
vendor: "anthropic",
|
|
1675
|
+
claudeEnv: {
|
|
1676
|
+
CLAUDE_CODE_OAUTH_TOKEN: account.accessToken,
|
|
1677
|
+
ANTHROPIC_BASE_URL: void 0,
|
|
1678
|
+
ANTHROPIC_API_KEY: void 0
|
|
1679
|
+
},
|
|
1680
|
+
describe: `claude subscription oauth (${account.label})`
|
|
1681
|
+
};
|
|
1682
|
+
case "openai-api-key":
|
|
1683
|
+
return {
|
|
1684
|
+
vendor: "openai",
|
|
1685
|
+
codexProvider: { model: account.model },
|
|
1686
|
+
extraEnv: { OPENAI_API_KEY: account.secret || "" },
|
|
1687
|
+
describe: `openai API key (${account.label})`
|
|
1688
|
+
};
|
|
1689
|
+
case "openai-gateway": {
|
|
1690
|
+
const url = stripV1(account.baseUrl || "");
|
|
1691
|
+
const provider = { apiBase: url, apiKey: account.secret, model: account.model };
|
|
1692
|
+
return {
|
|
1693
|
+
vendor: "openai",
|
|
1694
|
+
codexProvider: provider,
|
|
1695
|
+
extraEnv: { [CODEX_PROVIDER_KEY_ENV]: account.secret || "" },
|
|
1696
|
+
describe: `openai gateway ${url} (${account.label})`
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
case "chatgpt-oauth": {
|
|
1700
|
+
const codexHome = accountCodexHomeDir(account.id, svampHome);
|
|
1701
|
+
return {
|
|
1702
|
+
vendor: "openai",
|
|
1703
|
+
codexProvider: { model: account.model },
|
|
1704
|
+
codexHome,
|
|
1705
|
+
extraEnv: { CODEX_HOME: codexHome },
|
|
1706
|
+
describe: `chatgpt subscription (${account.label})`
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
default:
|
|
1710
|
+
throw new Error(`Unknown account method: ${account.method}`);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
function ensureCodexHome(account, svampHome) {
|
|
1714
|
+
const dir = accountCodexHomeDir(account.id, svampHome);
|
|
1715
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1716
|
+
return dir;
|
|
1717
|
+
}
|
|
1718
|
+
async function probeAccount(account, env = process.env) {
|
|
1719
|
+
let resolved;
|
|
1720
|
+
try {
|
|
1721
|
+
resolved = resolveAccountEnv(account, env);
|
|
1722
|
+
} catch (e) {
|
|
1723
|
+
return { ok: false, detail: `Misconfigured: ${e?.message || e}` };
|
|
1724
|
+
}
|
|
1725
|
+
const withTimeout = async (url, init) => {
|
|
1726
|
+
const ctrl = new AbortController();
|
|
1727
|
+
const t = setTimeout(() => ctrl.abort(), 8e3);
|
|
1728
|
+
try {
|
|
1729
|
+
return await fetch(url, { ...init, signal: ctrl.signal });
|
|
1730
|
+
} finally {
|
|
1731
|
+
clearTimeout(t);
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
try {
|
|
1735
|
+
switch (account.method) {
|
|
1736
|
+
case "anthropic-api-key":
|
|
1737
|
+
case "anthropic-gateway":
|
|
1738
|
+
case "hypha-proxy": {
|
|
1739
|
+
const base = resolved.claudeEnv?.ANTHROPIC_BASE_URL || "https://api.anthropic.com";
|
|
1740
|
+
const key = resolved.claudeEnv?.ANTHROPIC_API_KEY;
|
|
1741
|
+
if (!key) return { ok: false, detail: "No credential resolved" };
|
|
1742
|
+
const res = await withTimeout(`${base}/v1/messages`, {
|
|
1743
|
+
method: "POST",
|
|
1744
|
+
headers: {
|
|
1745
|
+
"content-type": "application/json",
|
|
1746
|
+
"x-api-key": key,
|
|
1747
|
+
"authorization": `Bearer ${key}`,
|
|
1748
|
+
"anthropic-version": "2023-06-01"
|
|
1749
|
+
},
|
|
1750
|
+
body: JSON.stringify({ model: "claude-3-5-haiku-20241022", max_tokens: 1, messages: [{ role: "user", content: "hi" }] })
|
|
1751
|
+
});
|
|
1752
|
+
if (res.status === 401 || res.status === 403) return { ok: false, detail: `Auth rejected (HTTP ${res.status})` };
|
|
1753
|
+
if (res.ok || res.status === 400 || res.status === 429) return { ok: true, detail: `Reachable (HTTP ${res.status})` };
|
|
1754
|
+
return { ok: true, detail: `Reachable, unexpected HTTP ${res.status}` };
|
|
1755
|
+
}
|
|
1756
|
+
case "openai-api-key":
|
|
1757
|
+
case "openai-gateway": {
|
|
1758
|
+
const base = account.method === "openai-gateway" ? stripV1(account.baseUrl || "") : "https://api.openai.com/v1";
|
|
1759
|
+
const key = account.secret || "";
|
|
1760
|
+
if (!key) return { ok: false, detail: "No credential resolved" };
|
|
1761
|
+
const res = await withTimeout(`${base}/models`, { headers: { authorization: `Bearer ${key}` } });
|
|
1762
|
+
if (res.status === 401 || res.status === 403) return { ok: false, detail: `Auth rejected (HTTP ${res.status})` };
|
|
1763
|
+
if (res.ok || res.status === 400 || res.status === 429) return { ok: true, detail: `Reachable (HTTP ${res.status})` };
|
|
1764
|
+
return { ok: true, detail: `Reachable, unexpected HTTP ${res.status}` };
|
|
1765
|
+
}
|
|
1766
|
+
case "anthropic-oauth": {
|
|
1767
|
+
const tok = account.accessToken;
|
|
1768
|
+
if (!tok) return { ok: false, detail: "Not signed in \u2014 start the OAuth flow" };
|
|
1769
|
+
const res = await withTimeout("https://api.anthropic.com/v1/messages", {
|
|
1770
|
+
method: "POST",
|
|
1771
|
+
headers: {
|
|
1772
|
+
"content-type": "application/json",
|
|
1773
|
+
"authorization": `Bearer ${tok}`,
|
|
1774
|
+
"anthropic-version": "2023-06-01",
|
|
1775
|
+
"anthropic-beta": "oauth-2025-04-20"
|
|
1776
|
+
},
|
|
1777
|
+
body: JSON.stringify({ model: "claude-3-5-haiku-20241022", max_tokens: 1, messages: [{ role: "user", content: "hi" }] })
|
|
1778
|
+
});
|
|
1779
|
+
if (res.status === 401 || res.status === 403) return { ok: false, detail: `Sign-in expired or rejected (HTTP ${res.status}) \u2014 reconnect` };
|
|
1780
|
+
if (res.ok || res.status === 400 || res.status === 429) return { ok: true, detail: `Connected (HTTP ${res.status})` };
|
|
1781
|
+
return { ok: true, detail: `Connected, unexpected HTTP ${res.status}` };
|
|
1782
|
+
}
|
|
1783
|
+
case "claude-oauth-token":
|
|
1784
|
+
return { ok: !!account.secret, detail: account.secret ? "Token stored \u2014 verified when a session starts (Claude CLI uses CLAUDE_CODE_OAUTH_TOKEN)" : "No token stored" };
|
|
1785
|
+
case "chatgpt-oauth":
|
|
1786
|
+
return { ok: !!account.model || true, detail: "ChatGPT signed in via device auth \u2014 verified when a Codex session starts (per-account CODEX_HOME)" };
|
|
1787
|
+
default:
|
|
1788
|
+
return { ok: false, detail: "Unknown method" };
|
|
1789
|
+
}
|
|
1790
|
+
} catch (e) {
|
|
1791
|
+
return { ok: false, detail: `Unreachable: ${e?.message || e}` };
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
async function refreshDueAnthropicAccounts(refresh, log, svampHome, skewMs = 10 * 60 * 1e3) {
|
|
1795
|
+
const due = loadAccounts(svampHome).filter((a) => a.method === "anthropic-oauth" && a.refreshToken && Date.now() >= (a.expiresAt ?? 0) - skewMs);
|
|
1796
|
+
for (const a of due) {
|
|
1797
|
+
try {
|
|
1798
|
+
const t = await refresh(a.refreshToken);
|
|
1799
|
+
upsertAccount({
|
|
1800
|
+
id: a.id,
|
|
1801
|
+
label: a.label,
|
|
1802
|
+
vendor: a.vendor,
|
|
1803
|
+
method: a.method,
|
|
1804
|
+
accessToken: t.accessToken,
|
|
1805
|
+
refreshToken: t.refreshToken,
|
|
1806
|
+
expiresAt: t.expiresAt
|
|
1807
|
+
}, svampHome);
|
|
1808
|
+
log(`[oauth] refreshed anthropic-oauth account ${a.label}`);
|
|
1809
|
+
} catch (e) {
|
|
1810
|
+
log(`[oauth] refresh failed for account ${a.label}: ${e?.message || e}`);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
const ANTHROPIC_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
1816
|
+
const ANTHROPIC_AUTH_ENDPOINT = "https://claude.ai/oauth/authorize";
|
|
1817
|
+
const ANTHROPIC_TOKEN_ENDPOINT = "https://platform.claude.com/v1/oauth/token";
|
|
1818
|
+
const ANTHROPIC_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback";
|
|
1819
|
+
const ANTHROPIC_SCOPES = "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
1820
|
+
function generatePkce() {
|
|
1821
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
1822
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
1823
|
+
return { verifier, challenge };
|
|
1824
|
+
}
|
|
1825
|
+
function buildAnthropicAuthUrl(challenge, state) {
|
|
1826
|
+
const params = new URLSearchParams({
|
|
1827
|
+
response_type: "code",
|
|
1828
|
+
client_id: ANTHROPIC_CLIENT_ID,
|
|
1829
|
+
redirect_uri: ANTHROPIC_REDIRECT_URI,
|
|
1830
|
+
code_challenge: challenge,
|
|
1831
|
+
code_challenge_method: "S256",
|
|
1832
|
+
state,
|
|
1833
|
+
scope: ANTHROPIC_SCOPES,
|
|
1834
|
+
oauth_beta: "oauth-2025-04-20"
|
|
1835
|
+
});
|
|
1836
|
+
return `${ANTHROPIC_AUTH_ENDPOINT}?${params.toString()}`;
|
|
1837
|
+
}
|
|
1838
|
+
function extractAuthCode(raw) {
|
|
1839
|
+
let s = (raw || "").trim();
|
|
1840
|
+
if (s.startsWith("http")) {
|
|
1841
|
+
try {
|
|
1842
|
+
const u = new URL(s);
|
|
1843
|
+
const c = u.searchParams.get("code");
|
|
1844
|
+
if (c) return c;
|
|
1845
|
+
} catch {
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (s.includes("#")) s = s.split("#")[0];
|
|
1849
|
+
return s.trim();
|
|
1174
1850
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1851
|
+
function tokensFromResponse(j) {
|
|
1852
|
+
const expiresInSec = typeof j.expires_in === "number" ? j.expires_in : 3600;
|
|
1853
|
+
return {
|
|
1854
|
+
accessToken: j.access_token || "",
|
|
1855
|
+
refreshToken: j.refresh_token || "",
|
|
1856
|
+
expiresAt: Date.now() + expiresInSec * 1e3
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
async function exchangeAnthropicCode(code, verifier, state) {
|
|
1860
|
+
const res = await fetch(ANTHROPIC_TOKEN_ENDPOINT, {
|
|
1861
|
+
method: "POST",
|
|
1862
|
+
headers: { "content-type": "application/json" },
|
|
1863
|
+
body: JSON.stringify({
|
|
1864
|
+
grant_type: "authorization_code",
|
|
1865
|
+
code: extractAuthCode(code),
|
|
1866
|
+
redirect_uri: ANTHROPIC_REDIRECT_URI,
|
|
1867
|
+
client_id: ANTHROPIC_CLIENT_ID,
|
|
1868
|
+
code_verifier: verifier,
|
|
1869
|
+
state
|
|
1870
|
+
// Anthropic requires state echoed in the body
|
|
1871
|
+
})
|
|
1872
|
+
});
|
|
1873
|
+
if (!res.ok) {
|
|
1874
|
+
const t = await res.text().catch(() => "");
|
|
1875
|
+
throw new Error(`Token exchange failed (${res.status}): ${t.slice(0, 300)}`);
|
|
1876
|
+
}
|
|
1877
|
+
return tokensFromResponse(await res.json());
|
|
1878
|
+
}
|
|
1879
|
+
async function refreshAnthropicToken(refreshToken) {
|
|
1880
|
+
const res = await fetch(ANTHROPIC_TOKEN_ENDPOINT, {
|
|
1881
|
+
method: "POST",
|
|
1882
|
+
headers: { "content-type": "application/json" },
|
|
1883
|
+
body: JSON.stringify({
|
|
1884
|
+
grant_type: "refresh_token",
|
|
1885
|
+
refresh_token: refreshToken,
|
|
1886
|
+
client_id: ANTHROPIC_CLIENT_ID
|
|
1887
|
+
})
|
|
1888
|
+
});
|
|
1889
|
+
if (!res.ok) {
|
|
1890
|
+
const t = await res.text().catch(() => "");
|
|
1891
|
+
throw new Error(`Token refresh failed (${res.status}): ${t.slice(0, 300)}`);
|
|
1184
1892
|
}
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1893
|
+
const tokens = tokensFromResponse(await res.json());
|
|
1894
|
+
if (!tokens.refreshToken) tokens.refreshToken = refreshToken;
|
|
1895
|
+
return tokens;
|
|
1896
|
+
}
|
|
1897
|
+
function parseDeviceAuthOutput(text) {
|
|
1898
|
+
const out = {};
|
|
1899
|
+
const urlMatch = text.match(/https?:\/\/[^\s"'<>)]+/i);
|
|
1900
|
+
if (urlMatch) out.verificationUrl = urlMatch[0].replace(/[.,]$/, "");
|
|
1901
|
+
const codeMatch = text.match(/\b([A-Z0-9]{3,5}(?:-[A-Z0-9]{3,5})+)\b/);
|
|
1902
|
+
if (codeMatch) out.userCode = codeMatch[1];
|
|
1903
|
+
return out;
|
|
1904
|
+
}
|
|
1905
|
+
const flows = /* @__PURE__ */ new Map();
|
|
1906
|
+
const FLOW_TTL_MS = 15 * 60 * 1e3;
|
|
1907
|
+
function oauthFlowDir(flowId, svampHome) {
|
|
1908
|
+
const home = svampHome || process.env.SVAMP_HOME || join(homedir(), ".svamp");
|
|
1909
|
+
return join(home, "oauth-flows", flowId);
|
|
1910
|
+
}
|
|
1911
|
+
function pruneFlows() {
|
|
1912
|
+
const now = Date.now();
|
|
1913
|
+
for (const [id, f] of flows) {
|
|
1914
|
+
if (now - f.createdAt > FLOW_TTL_MS) {
|
|
1915
|
+
try {
|
|
1916
|
+
f.proc?.kill();
|
|
1917
|
+
} catch {
|
|
1199
1918
|
}
|
|
1200
|
-
|
|
1919
|
+
try {
|
|
1920
|
+
if (f.codexHome) rmSync(join(f.codexHome, ".."), { recursive: true, force: true });
|
|
1921
|
+
} catch {
|
|
1922
|
+
}
|
|
1923
|
+
flows.delete(id);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
function startAnthropicFlow(label) {
|
|
1928
|
+
pruneFlows();
|
|
1929
|
+
const { verifier, challenge } = generatePkce();
|
|
1930
|
+
const state = randomBytes(32).toString("base64url");
|
|
1931
|
+
const flowId = randomUUID();
|
|
1932
|
+
const authorizeUrl = buildAnthropicAuthUrl(challenge, state);
|
|
1933
|
+
flows.set(flowId, { id: flowId, vendor: "anthropic", label, createdAt: Date.now(), status: "pending", state, verifier });
|
|
1934
|
+
return { flowId, authorizeUrl };
|
|
1935
|
+
}
|
|
1936
|
+
function startOpenAiDeviceFlow(label, log, svampHome) {
|
|
1937
|
+
pruneFlows();
|
|
1938
|
+
const flowId = randomUUID();
|
|
1939
|
+
const codexHome = join(oauthFlowDir(flowId, svampHome), "codex");
|
|
1940
|
+
mkdirSync(codexHome, { recursive: true });
|
|
1941
|
+
const flow = { id: flowId, vendor: "openai", label, createdAt: Date.now(), status: "pending", codexHome, outputBuffer: "" };
|
|
1942
|
+
flows.set(flowId, flow);
|
|
1943
|
+
return new Promise((resolve, reject) => {
|
|
1944
|
+
let settled = false;
|
|
1945
|
+
const proc = spawn("codex", ["login", "--device-auth"], {
|
|
1946
|
+
env: { ...process.env, CODEX_HOME: codexHome },
|
|
1947
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1948
|
+
});
|
|
1949
|
+
flow.proc = proc;
|
|
1950
|
+
const onData = (buf) => {
|
|
1951
|
+
const text = buf.toString();
|
|
1952
|
+
flow.outputBuffer = (flow.outputBuffer || "") + text;
|
|
1953
|
+
const parsed = parseDeviceAuthOutput(flow.outputBuffer);
|
|
1954
|
+
if (parsed.verificationUrl) flow.verificationUrl = parsed.verificationUrl;
|
|
1955
|
+
if (parsed.userCode) flow.userCode = parsed.userCode;
|
|
1956
|
+
if (!settled && flow.verificationUrl && flow.userCode) {
|
|
1957
|
+
settled = true;
|
|
1958
|
+
resolve({ flowId, verificationUrl: flow.verificationUrl, userCode: flow.userCode });
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
proc.stdout?.on("data", onData);
|
|
1962
|
+
proc.stderr?.on("data", onData);
|
|
1963
|
+
proc.on("exit", (code) => {
|
|
1964
|
+
if (code === 0 && flow.codexHome && existsSync(join(flow.codexHome, "auth.json"))) {
|
|
1965
|
+
flow.status = "authorized";
|
|
1966
|
+
} else {
|
|
1967
|
+
flow.status = "error";
|
|
1968
|
+
flow.error = flow.error || `codex login exited ${code}` + (/(device code|workspace admin|not.*enabled)/i.test(flow.outputBuffer || "") ? " \u2014 device-code login may be disabled for this ChatGPT workspace (ask an admin to enable it)." : "");
|
|
1969
|
+
if (!settled) {
|
|
1970
|
+
settled = true;
|
|
1971
|
+
reject(new Error(flow.error));
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
});
|
|
1975
|
+
proc.on("error", (err) => {
|
|
1976
|
+
flow.status = "error";
|
|
1977
|
+
flow.error = `Failed to run codex login: ${err.message}. Is the Codex CLI installed on this machine?`;
|
|
1978
|
+
if (!settled) {
|
|
1979
|
+
settled = true;
|
|
1980
|
+
reject(new Error(flow.error));
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
setTimeout(() => {
|
|
1984
|
+
if (!settled) {
|
|
1985
|
+
settled = true;
|
|
1986
|
+
reject(new Error("Timed out waiting for the device code from codex login."));
|
|
1987
|
+
}
|
|
1988
|
+
}, 3e4);
|
|
1989
|
+
log(`[oauth] started codex device-auth flow ${flowId}`);
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
function getFlow(flowId) {
|
|
1993
|
+
return flows.get(flowId);
|
|
1994
|
+
}
|
|
1995
|
+
async function completeAnthropicFlow(flowId, code) {
|
|
1996
|
+
const flow = flows.get(flowId);
|
|
1997
|
+
if (!flow || flow.vendor !== "anthropic" || !flow.verifier || !flow.state) {
|
|
1998
|
+
throw new Error("Invalid or expired OAuth flow");
|
|
1201
1999
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
2000
|
+
const tokens = await exchangeAnthropicCode(code, flow.verifier, flow.state);
|
|
2001
|
+
flows.delete(flowId);
|
|
2002
|
+
return tokens;
|
|
1204
2003
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
if (!
|
|
1208
|
-
if (
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
2004
|
+
function finalizeOpenAiFlow(flowId, accountCodexHome) {
|
|
2005
|
+
const flow = flows.get(flowId);
|
|
2006
|
+
if (!flow || flow.vendor !== "openai") throw new Error("Invalid or expired OAuth flow");
|
|
2007
|
+
if (flow.status !== "authorized" || !flow.codexHome) throw new Error("Device authorization not complete yet");
|
|
2008
|
+
mkdirSync(accountCodexHome, { recursive: true });
|
|
2009
|
+
cpSync(flow.codexHome, accountCodexHome, { recursive: true });
|
|
2010
|
+
try {
|
|
2011
|
+
rmSync(join(flow.codexHome, ".."), { recursive: true, force: true });
|
|
2012
|
+
} catch {
|
|
2013
|
+
}
|
|
2014
|
+
flows.delete(flowId);
|
|
2015
|
+
}
|
|
2016
|
+
function cancelFlow(flowId) {
|
|
2017
|
+
const flow = flows.get(flowId);
|
|
2018
|
+
if (!flow) return;
|
|
2019
|
+
flow.status = "cancelled";
|
|
2020
|
+
try {
|
|
2021
|
+
flow.proc?.kill();
|
|
2022
|
+
} catch {
|
|
1212
2023
|
}
|
|
1213
2024
|
try {
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
} catch (err) {
|
|
1217
|
-
return { killed: false, reason: err?.message ?? "kill failed" };
|
|
2025
|
+
if (flow.codexHome) rmSync(join(flow.codexHome, ".."), { recursive: true, force: true });
|
|
2026
|
+
} catch {
|
|
1218
2027
|
}
|
|
2028
|
+
flows.delete(flowId);
|
|
1219
2029
|
}
|
|
1220
2030
|
|
|
1221
2031
|
const FIELD_RANGES = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
|
|
@@ -2600,6 +3410,110 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
2600
3410
|
});
|
|
2601
3411
|
return { success: true };
|
|
2602
3412
|
},
|
|
3413
|
+
// ── Backend Accounts (per-machine credentials; admin-gated) ──
|
|
3414
|
+
// Secrets live on the machine (~/.svamp/accounts.json, 0600) and are NEVER returned —
|
|
3415
|
+
// only redacted previews. The composer sends an opaque accountId; the daemon resolves it
|
|
3416
|
+
// to concrete auth at spawn (daemon/backendAccounts.ts). See docs/sharing-and-security.md.
|
|
3417
|
+
listBackendAccounts: async (context) => {
|
|
3418
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
3419
|
+
const accounts = loadAccounts(metadata.svampHomeDir);
|
|
3420
|
+
return {
|
|
3421
|
+
accounts: redactAccounts(accounts),
|
|
3422
|
+
defaults: defaultAccountIds(accounts)
|
|
3423
|
+
};
|
|
3424
|
+
},
|
|
3425
|
+
setBackendAccount: async (account, context) => {
|
|
3426
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
3427
|
+
const createdBy = context?.user?.email;
|
|
3428
|
+
const saved = upsertAccount({ ...account, createdBy: account.createdBy || createdBy }, metadata.svampHomeDir);
|
|
3429
|
+
notifyListeners({ type: "update-machine", machineId, backendAccountsChanged: true });
|
|
3430
|
+
return { success: true, account: redactAccount(saved) };
|
|
3431
|
+
},
|
|
3432
|
+
deleteBackendAccount: async (id, context) => {
|
|
3433
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
3434
|
+
deleteAccount(id, metadata.svampHomeDir);
|
|
3435
|
+
notifyListeners({ type: "update-machine", machineId, backendAccountsChanged: true });
|
|
3436
|
+
return { success: true };
|
|
3437
|
+
},
|
|
3438
|
+
setDefaultBackendAccount: async (vendor, id, context) => {
|
|
3439
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
3440
|
+
setDefaultAccount(vendor, id, metadata.svampHomeDir);
|
|
3441
|
+
notifyListeners({ type: "update-machine", machineId, backendAccountsChanged: true });
|
|
3442
|
+
return { success: true };
|
|
3443
|
+
},
|
|
3444
|
+
testBackendAccount: async (id, context) => {
|
|
3445
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
3446
|
+
const acct = getAccount(id, metadata.svampHomeDir);
|
|
3447
|
+
if (!acct) return { ok: false, detail: "Account not found" };
|
|
3448
|
+
return await probeAccount(acct);
|
|
3449
|
+
},
|
|
3450
|
+
// ── Guided OAuth login (both vendors; admin-gated) ──
|
|
3451
|
+
// Anthropic: PKCE authorize URL → user pastes the shown code back.
|
|
3452
|
+
// OpenAI: device-auth → we show a verification URL + user code, then poll to completion.
|
|
3453
|
+
startBackendOAuth: async (vendor, label, context) => {
|
|
3454
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
3455
|
+
const lbl = (label || "").trim() || (vendor === "anthropic" ? "Claude subscription" : "ChatGPT subscription");
|
|
3456
|
+
if (vendor === "anthropic") {
|
|
3457
|
+
const { flowId, authorizeUrl } = startAnthropicFlow(lbl);
|
|
3458
|
+
return { vendor, flowId, mode: "paste-code", authorizeUrl };
|
|
3459
|
+
}
|
|
3460
|
+
if (vendor === "openai") {
|
|
3461
|
+
const { flowId, verificationUrl, userCode } = await startOpenAiDeviceFlow(
|
|
3462
|
+
lbl,
|
|
3463
|
+
(m) => console.log(m),
|
|
3464
|
+
metadata.svampHomeDir
|
|
3465
|
+
);
|
|
3466
|
+
return { vendor, flowId, mode: "device-code", verificationUrl, userCode };
|
|
3467
|
+
}
|
|
3468
|
+
throw new Error(`Unsupported OAuth vendor: ${String(vendor)}`);
|
|
3469
|
+
},
|
|
3470
|
+
completeBackendOAuth: async (flowId, code, context) => {
|
|
3471
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
3472
|
+
const flow = getFlow(flowId);
|
|
3473
|
+
if (!flow) throw new Error("OAuth flow expired \u2014 start again");
|
|
3474
|
+
const createdBy = context?.user?.email;
|
|
3475
|
+
if (flow.vendor === "anthropic") {
|
|
3476
|
+
if (!code || !code.trim()) throw new Error("Paste the authorization code");
|
|
3477
|
+
const tokens = await completeAnthropicFlow(flowId, code);
|
|
3478
|
+
const account = upsertAccount({
|
|
3479
|
+
label: flow.label,
|
|
3480
|
+
vendor: "anthropic",
|
|
3481
|
+
method: "anthropic-oauth",
|
|
3482
|
+
accessToken: tokens.accessToken,
|
|
3483
|
+
refreshToken: tokens.refreshToken,
|
|
3484
|
+
expiresAt: tokens.expiresAt,
|
|
3485
|
+
createdBy
|
|
3486
|
+
}, metadata.svampHomeDir);
|
|
3487
|
+
notifyListeners({ type: "update-machine", machineId, backendAccountsChanged: true });
|
|
3488
|
+
return { done: true, account: redactAccount(account) };
|
|
3489
|
+
}
|
|
3490
|
+
if (flow.status === "pending") {
|
|
3491
|
+
return { done: false, status: "pending", verificationUrl: flow.verificationUrl, userCode: flow.userCode };
|
|
3492
|
+
}
|
|
3493
|
+
if (flow.status === "error") throw new Error(flow.error || "Device authorization failed");
|
|
3494
|
+
if (flow.status === "authorized") {
|
|
3495
|
+
const account = upsertAccount({
|
|
3496
|
+
label: flow.label,
|
|
3497
|
+
vendor: "openai",
|
|
3498
|
+
method: "chatgpt-oauth",
|
|
3499
|
+
createdBy
|
|
3500
|
+
}, metadata.svampHomeDir);
|
|
3501
|
+
try {
|
|
3502
|
+
finalizeOpenAiFlow(flowId, accountCodexHomeDir(account.id, metadata.svampHomeDir));
|
|
3503
|
+
} catch (e) {
|
|
3504
|
+
deleteAccount(account.id, metadata.svampHomeDir);
|
|
3505
|
+
throw e;
|
|
3506
|
+
}
|
|
3507
|
+
notifyListeners({ type: "update-machine", machineId, backendAccountsChanged: true });
|
|
3508
|
+
return { done: true, account: redactAccount(account) };
|
|
3509
|
+
}
|
|
3510
|
+
throw new Error("Device authorization was cancelled");
|
|
3511
|
+
},
|
|
3512
|
+
cancelBackendOAuth: async (flowId, context) => {
|
|
3513
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
3514
|
+
cancelFlow(flowId);
|
|
3515
|
+
return { success: true };
|
|
3516
|
+
},
|
|
2603
3517
|
// Register a listener for real-time updates (app calls this with _rintf callback)
|
|
2604
3518
|
registerListener: async (callback, context) => {
|
|
2605
3519
|
trackInbound();
|
|
@@ -3041,7 +3955,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
3041
3955
|
const tunnels = handlers.tunnels;
|
|
3042
3956
|
if (!tunnels) throw new Error("Tunnel management not available");
|
|
3043
3957
|
if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
|
|
3044
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
3958
|
+
const { FrpcTunnel } = await import('./frpc-CxqX4sQ7.mjs');
|
|
3045
3959
|
const tunnel = new FrpcTunnel({
|
|
3046
3960
|
name: params.name,
|
|
3047
3961
|
ports: params.ports,
|
|
@@ -3508,7 +4422,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
3508
4422
|
}
|
|
3509
4423
|
const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
|
|
3510
4424
|
const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
|
|
3511
|
-
const { toolsForRole } = await import('./sideband-
|
|
4425
|
+
const { toolsForRole } = await import('./sideband-CWu6yjFl.mjs');
|
|
3512
4426
|
const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
|
|
3513
4427
|
return fmt(r2);
|
|
3514
4428
|
}
|
|
@@ -3607,7 +4521,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
3607
4521
|
if (r.error || !r.sender) return { error: r.error || "unauthorized" };
|
|
3608
4522
|
const callId = "call_" + Math.random().toString(16).slice(2, 12);
|
|
3609
4523
|
const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
|
|
3610
|
-
const { queryCore } = await import('./commands-
|
|
4524
|
+
const { queryCore } = await import('./commands-BX6_mFbE.mjs');
|
|
3611
4525
|
const timeout = c.reply?.timeout_sec || 120;
|
|
3612
4526
|
let result;
|
|
3613
4527
|
try {
|
|
@@ -3968,277 +4882,82 @@ function classifyInbound(message, now = Date.now(), opts) {
|
|
|
3968
4882
|
return { action: "queue", reason: "circuit-breaker-open" };
|
|
3969
4883
|
}
|
|
3970
4884
|
if (!takeUrgentToken(senderKey(message), now)) {
|
|
3971
|
-
return { action: "queue", reason: "rate-limited" };
|
|
3972
|
-
}
|
|
3973
|
-
const tripped = recordWake(now);
|
|
3974
|
-
return { action: "wake", reason: "urgent", breakerTripped: tripped };
|
|
3975
|
-
}
|
|
3976
|
-
function applyInboxClear(inbox, opts) {
|
|
3977
|
-
let kept;
|
|
3978
|
-
if (opts?.messageId) kept = inbox.filter((m) => m.messageId !== opts.messageId);
|
|
3979
|
-
else if (opts?.all) kept = [];
|
|
3980
|
-
else kept = inbox.filter((m) => !m.read);
|
|
3981
|
-
return { kept, removed: inbox.length - kept.length };
|
|
3982
|
-
}
|
|
3983
|
-
|
|
3984
|
-
function inboxFilePath(projectDir, sessionId) {
|
|
3985
|
-
return join(projectDir, ".svamp", sessionId, "inbox.json");
|
|
3986
|
-
}
|
|
3987
|
-
function loadInbox(projectDir, sessionId) {
|
|
3988
|
-
try {
|
|
3989
|
-
const p = inboxFilePath(projectDir, sessionId);
|
|
3990
|
-
if (!existsSync(p)) return [];
|
|
3991
|
-
const data = JSON.parse(readFileSync(p, "utf8"));
|
|
3992
|
-
if (Array.isArray(data)) return data;
|
|
3993
|
-
if (data && Array.isArray(data.messages)) return data.messages;
|
|
3994
|
-
return [];
|
|
3995
|
-
} catch {
|
|
3996
|
-
return [];
|
|
3997
|
-
}
|
|
3998
|
-
}
|
|
3999
|
-
function saveInbox(projectDir, sessionId, inbox) {
|
|
4000
|
-
try {
|
|
4001
|
-
const p = inboxFilePath(projectDir, sessionId);
|
|
4002
|
-
mkdirSync(dirname(p), { recursive: true });
|
|
4003
|
-
const tmp = `${p}.tmp-${process.pid}`;
|
|
4004
|
-
writeFileSync(tmp, JSON.stringify(inbox));
|
|
4005
|
-
renameSync(tmp, p);
|
|
4006
|
-
} catch {
|
|
4007
|
-
}
|
|
4008
|
-
}
|
|
4009
|
-
|
|
4010
|
-
function envInt(name, fallback) {
|
|
4011
|
-
const raw = process.env[name];
|
|
4012
|
-
if (raw === void 0 || raw === "") return fallback;
|
|
4013
|
-
const n = parseInt(raw, 10);
|
|
4014
|
-
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
4015
|
-
}
|
|
4016
|
-
function getRateLimitRetryConfig() {
|
|
4017
|
-
return {
|
|
4018
|
-
maxRetries: envInt("SVAMP_RATELIMIT_MAX_RETRIES", 200),
|
|
4019
|
-
baseMs: envInt("SVAMP_RATELIMIT_BASE_MS", 5e3),
|
|
4020
|
-
capMs: envInt("SVAMP_RATELIMIT_CAP_MS", 12e4)
|
|
4021
|
-
};
|
|
4022
|
-
}
|
|
4023
|
-
function computeRetryDelayMs(attempt, cfg = getRateLimitRetryConfig(), rng = Math.random) {
|
|
4024
|
-
const a = Math.max(0, Math.floor(attempt));
|
|
4025
|
-
const expo = Math.min(a, 30);
|
|
4026
|
-
const nominal = Math.min(cfg.capMs, cfg.baseMs * Math.pow(2, expo));
|
|
4027
|
-
const half = nominal / 2;
|
|
4028
|
-
return Math.round(half + rng() * half);
|
|
4029
|
-
}
|
|
4030
|
-
function isRetryableRateLimit(text, apiErrorStatus) {
|
|
4031
|
-
const t = (text || "").toLowerCase();
|
|
4032
|
-
const hardLimit = t.includes("quota") || // The Hypha LLM proxy emits a 429 "Daily spending limit reached … Resets
|
|
4033
|
-
// at 00:00 UTC. Contact the admin to raise your limit." That is a hard cap
|
|
4034
|
-
// that resets at a fixed time / needs human action — NOT a transient
|
|
4035
|
-
// upstream throttle. Retrying it 10× just delays surfacing the real error.
|
|
4036
|
-
t.includes("spending limit") || t.includes("credit") || t.includes("balance") || t.includes("billing") || t.includes("payment") || t.includes("subscription") || t.includes("1m-context") || t.includes("1m context") || t.includes("insufficient") || t.includes("usage limit") && !t.includes("not your usage limit");
|
|
4037
|
-
if (hardLimit) return false;
|
|
4038
|
-
const authIssue = apiErrorStatus === 401 || apiErrorStatus === 403 || t.includes("unauthorized") || t.includes("invalid api key") || t.includes("authentication") || t.includes("forbidden");
|
|
4039
|
-
if (authIssue) return false;
|
|
4040
|
-
const clientError = apiErrorStatus === 400 || apiErrorStatus === 404 || apiErrorStatus === 422 || t.includes("invalid_request") || t.includes("invalid request") || t.includes("bad request");
|
|
4041
|
-
if (clientError) return false;
|
|
4042
|
-
const serverTransient = typeof apiErrorStatus === "number" && apiErrorStatus >= 500 && apiErrorStatus <= 599 || apiErrorStatus === 429 || /\b(429|500|502|503|504|520|521|522|523|524|529)\b/.test(t) || t.includes("temporarily limiting") || t.includes("not your usage limit") || t.includes("overloaded") || t.includes("overload") || t.includes("too many requests") || t.includes("server is busy") || t.includes("internal server error") || t.includes("bad gateway") || t.includes("service unavailable") || t.includes("gateway timeout") || t.includes("rate limit") || t.includes("rate-limit") || t.includes("rate limited");
|
|
4043
|
-
if (serverTransient) return true;
|
|
4044
|
-
const networkTransient = t.includes("socket hang up") || t.includes("closed unexpectedly") || t.includes("connection closed") || t.includes("connection reset") || t.includes("connection error") || t.includes("econnreset") || t.includes("etimedout") || t.includes("econnaborted") || t.includes("epipe") || t.includes("enetunreach") || t.includes("enetreset") || t.includes("ehostunreach") || t.includes("eai_again") || t.includes("network error") || t.includes("fetch failed") || t.includes("request timed out") || t.includes("timed out");
|
|
4045
|
-
return networkTransient;
|
|
4046
|
-
}
|
|
4047
|
-
|
|
4048
|
-
const EXAMPLE_HYPHA_PROXY_URL = "https://proxy.hypha.aicell.io";
|
|
4049
|
-
const MODE_KEY = "SVAMP_CLAUDE_PROXY";
|
|
4050
|
-
const HYPHA_PROXY_URL_KEY = "SVAMP_HYPHA_PROXY_URL";
|
|
4051
|
-
const MANAGED_KEYS = /* @__PURE__ */ new Set([
|
|
4052
|
-
MODE_KEY,
|
|
4053
|
-
HYPHA_PROXY_URL_KEY,
|
|
4054
|
-
"ANTHROPIC_BASE_URL",
|
|
4055
|
-
"ANTHROPIC_API_KEY"
|
|
4056
|
-
]);
|
|
4057
|
-
function normalizeProxyUrl(raw) {
|
|
4058
|
-
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
4059
|
-
if (trimmed.endsWith("/v1")) {
|
|
4060
|
-
throw new Error(
|
|
4061
|
-
"Hypha proxy URL must not end in /v1 \u2014 the SDK appends it, producing double /v1/v1/messages"
|
|
4062
|
-
);
|
|
4063
|
-
}
|
|
4064
|
-
return trimmed;
|
|
4065
|
-
}
|
|
4066
|
-
function resolveHyphaProxyUrl() {
|
|
4067
|
-
const configured = (process.env[HYPHA_PROXY_URL_KEY] || "").trim();
|
|
4068
|
-
if (!configured) return void 0;
|
|
4069
|
-
try {
|
|
4070
|
-
return normalizeProxyUrl(configured);
|
|
4071
|
-
} catch {
|
|
4072
|
-
return void 0;
|
|
4073
|
-
}
|
|
4074
|
-
}
|
|
4075
|
-
function envFilePath() {
|
|
4076
|
-
const svampHome = process.env.SVAMP_HOME || join(homedir(), ".svamp");
|
|
4077
|
-
return join(svampHome, ".env");
|
|
4078
|
-
}
|
|
4079
|
-
function readEnvLines() {
|
|
4080
|
-
const file = envFilePath();
|
|
4081
|
-
if (!existsSync(file)) return [];
|
|
4082
|
-
return readFileSync(file, "utf-8").split("\n");
|
|
4083
|
-
}
|
|
4084
|
-
function writeEnvLines(lines) {
|
|
4085
|
-
const file = envFilePath();
|
|
4086
|
-
const dir = join(file, "..");
|
|
4087
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
4088
|
-
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
4089
|
-
lines.pop();
|
|
4090
|
-
}
|
|
4091
|
-
writeFileSync(file, lines.join("\n") + "\n", "utf-8");
|
|
4092
|
-
}
|
|
4093
|
-
function updateEnvFile(updates) {
|
|
4094
|
-
const lines = readEnvLines();
|
|
4095
|
-
const kept = [];
|
|
4096
|
-
const remainingUpdates = new Map(Object.entries(updates));
|
|
4097
|
-
for (const line of lines) {
|
|
4098
|
-
const trimmed = line.trim();
|
|
4099
|
-
if (!trimmed || trimmed.startsWith("#")) {
|
|
4100
|
-
kept.push(line);
|
|
4101
|
-
continue;
|
|
4102
|
-
}
|
|
4103
|
-
const eq = trimmed.indexOf("=");
|
|
4104
|
-
const key = eq === -1 ? trimmed : trimmed.slice(0, eq);
|
|
4105
|
-
if (remainingUpdates.has(key)) {
|
|
4106
|
-
const v = remainingUpdates.get(key);
|
|
4107
|
-
remainingUpdates.delete(key);
|
|
4108
|
-
if (v === void 0) continue;
|
|
4109
|
-
kept.push(`${key}=${v}`);
|
|
4110
|
-
continue;
|
|
4111
|
-
}
|
|
4112
|
-
kept.push(line);
|
|
4113
|
-
}
|
|
4114
|
-
for (const [key, v] of remainingUpdates) {
|
|
4115
|
-
if (v === void 0) continue;
|
|
4116
|
-
kept.push(`${key}=${v}`);
|
|
4885
|
+
return { action: "queue", reason: "rate-limited" };
|
|
4117
4886
|
}
|
|
4118
|
-
|
|
4887
|
+
const tripped = recordWake(now);
|
|
4888
|
+
return { action: "wake", reason: "urgent", breakerTripped: tripped };
|
|
4119
4889
|
}
|
|
4120
|
-
function
|
|
4121
|
-
|
|
4122
|
-
if (
|
|
4123
|
-
if (
|
|
4124
|
-
|
|
4890
|
+
function applyInboxClear(inbox, opts) {
|
|
4891
|
+
let kept;
|
|
4892
|
+
if (opts?.messageId) kept = inbox.filter((m) => m.messageId !== opts.messageId);
|
|
4893
|
+
else if (opts?.all) kept = [];
|
|
4894
|
+
else kept = inbox.filter((m) => !m.read);
|
|
4895
|
+
return { kept, removed: inbox.length - kept.length };
|
|
4125
4896
|
}
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
return `${key.slice(0, 8)}\u2026${key.slice(-4)}`;
|
|
4897
|
+
|
|
4898
|
+
function inboxFilePath(projectDir, sessionId) {
|
|
4899
|
+
return join(projectDir, ".svamp", sessionId, "inbox.json");
|
|
4130
4900
|
}
|
|
4131
|
-
function
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
const
|
|
4136
|
-
return
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
hyphaProxyUrlMissing: !proxyUrl
|
|
4142
|
-
};
|
|
4143
|
-
}
|
|
4144
|
-
if (mode === "custom") {
|
|
4145
|
-
return {
|
|
4146
|
-
mode,
|
|
4147
|
-
baseUrl: process.env.ANTHROPIC_BASE_URL,
|
|
4148
|
-
apiKeyPreview: redactKey(process.env.ANTHROPIC_API_KEY)
|
|
4149
|
-
};
|
|
4901
|
+
function loadInbox(projectDir, sessionId) {
|
|
4902
|
+
try {
|
|
4903
|
+
const p = inboxFilePath(projectDir, sessionId);
|
|
4904
|
+
if (!existsSync(p)) return [];
|
|
4905
|
+
const data = JSON.parse(readFileSync(p, "utf8"));
|
|
4906
|
+
if (Array.isArray(data)) return data;
|
|
4907
|
+
if (data && Array.isArray(data.messages)) return data.messages;
|
|
4908
|
+
return [];
|
|
4909
|
+
} catch {
|
|
4910
|
+
return [];
|
|
4150
4911
|
}
|
|
4151
|
-
return { mode: "login" };
|
|
4152
4912
|
}
|
|
4153
|
-
function
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4913
|
+
function saveInbox(projectDir, sessionId, inbox) {
|
|
4914
|
+
try {
|
|
4915
|
+
const p = inboxFilePath(projectDir, sessionId);
|
|
4916
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
4917
|
+
const tmp = `${p}.tmp-${process.pid}`;
|
|
4918
|
+
writeFileSync(tmp, JSON.stringify(inbox));
|
|
4919
|
+
renameSync(tmp, p);
|
|
4920
|
+
} catch {
|
|
4158
4921
|
}
|
|
4159
|
-
updateEnvFile({
|
|
4160
|
-
[MODE_KEY]: "hypha",
|
|
4161
|
-
[HYPHA_PROXY_URL_KEY]: normalizeProxyUrl(url),
|
|
4162
|
-
// Hypha mode resolves token live at spawn — no stored copy.
|
|
4163
|
-
ANTHROPIC_BASE_URL: void 0,
|
|
4164
|
-
ANTHROPIC_API_KEY: void 0
|
|
4165
|
-
});
|
|
4166
4922
|
}
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
});
|
|
4923
|
+
|
|
4924
|
+
function envInt(name, fallback) {
|
|
4925
|
+
const raw = process.env[name];
|
|
4926
|
+
if (raw === void 0 || raw === "") return fallback;
|
|
4927
|
+
const n = parseInt(raw, 10);
|
|
4928
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
4174
4929
|
}
|
|
4175
|
-
function
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
"ANTHROPIC_BASE_URL must not end in /v1 \u2014 the SDK appends it, producing double /v1/v1/messages"
|
|
4182
|
-
);
|
|
4183
|
-
}
|
|
4184
|
-
updateEnvFile({
|
|
4185
|
-
[MODE_KEY]: "custom",
|
|
4186
|
-
[HYPHA_PROXY_URL_KEY]: void 0,
|
|
4187
|
-
ANTHROPIC_BASE_URL: trimmed,
|
|
4188
|
-
ANTHROPIC_API_KEY: apiKey
|
|
4189
|
-
});
|
|
4930
|
+
function getRateLimitRetryConfig() {
|
|
4931
|
+
return {
|
|
4932
|
+
maxRetries: envInt("SVAMP_RATELIMIT_MAX_RETRIES", 200),
|
|
4933
|
+
baseMs: envInt("SVAMP_RATELIMIT_BASE_MS", 5e3),
|
|
4934
|
+
capMs: envInt("SVAMP_RATELIMIT_CAP_MS", 12e4)
|
|
4935
|
+
};
|
|
4190
4936
|
}
|
|
4191
|
-
function
|
|
4192
|
-
const
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
const key = process.env.ANTHROPIC_API_KEY;
|
|
4215
|
-
if (!url || !key) {
|
|
4216
|
-
delete spawnEnv.ANTHROPIC_BASE_URL;
|
|
4217
|
-
delete spawnEnv.ANTHROPIC_API_KEY;
|
|
4218
|
-
return "custom mode but ANTHROPIC_BASE_URL/API_KEY missing \u2014 falling back to login";
|
|
4219
|
-
}
|
|
4220
|
-
spawnEnv.ANTHROPIC_BASE_URL = url;
|
|
4221
|
-
spawnEnv.ANTHROPIC_API_KEY = key;
|
|
4222
|
-
return `custom proxy (${url})`;
|
|
4223
|
-
}
|
|
4224
|
-
delete spawnEnv.ANTHROPIC_BASE_URL;
|
|
4225
|
-
delete spawnEnv.ANTHROPIC_API_KEY;
|
|
4226
|
-
return null;
|
|
4937
|
+
function computeRetryDelayMs(attempt, cfg = getRateLimitRetryConfig(), rng = Math.random) {
|
|
4938
|
+
const a = Math.max(0, Math.floor(attempt));
|
|
4939
|
+
const expo = Math.min(a, 30);
|
|
4940
|
+
const nominal = Math.min(cfg.capMs, cfg.baseMs * Math.pow(2, expo));
|
|
4941
|
+
const half = nominal / 2;
|
|
4942
|
+
return Math.round(half + rng() * half);
|
|
4943
|
+
}
|
|
4944
|
+
function isRetryableRateLimit(text, apiErrorStatus) {
|
|
4945
|
+
const t = (text || "").toLowerCase();
|
|
4946
|
+
const hardLimit = t.includes("quota") || // The Hypha LLM proxy emits a 429 "Daily spending limit reached … Resets
|
|
4947
|
+
// at 00:00 UTC. Contact the admin to raise your limit." That is a hard cap
|
|
4948
|
+
// that resets at a fixed time / needs human action — NOT a transient
|
|
4949
|
+
// upstream throttle. Retrying it 10× just delays surfacing the real error.
|
|
4950
|
+
t.includes("spending limit") || t.includes("credit") || t.includes("balance") || t.includes("billing") || t.includes("payment") || t.includes("subscription") || t.includes("1m-context") || t.includes("1m context") || t.includes("insufficient") || t.includes("usage limit") && !t.includes("not your usage limit");
|
|
4951
|
+
if (hardLimit) return false;
|
|
4952
|
+
const authIssue = apiErrorStatus === 401 || apiErrorStatus === 403 || t.includes("unauthorized") || t.includes("invalid api key") || t.includes("authentication") || t.includes("forbidden");
|
|
4953
|
+
if (authIssue) return false;
|
|
4954
|
+
const clientError = apiErrorStatus === 400 || apiErrorStatus === 404 || apiErrorStatus === 422 || t.includes("invalid_request") || t.includes("invalid request") || t.includes("bad request");
|
|
4955
|
+
if (clientError) return false;
|
|
4956
|
+
const serverTransient = typeof apiErrorStatus === "number" && apiErrorStatus >= 500 && apiErrorStatus <= 599 || apiErrorStatus === 429 || /\b(429|500|502|503|504|520|521|522|523|524|529)\b/.test(t) || t.includes("temporarily limiting") || t.includes("not your usage limit") || t.includes("overloaded") || t.includes("overload") || t.includes("too many requests") || t.includes("server is busy") || t.includes("internal server error") || t.includes("bad gateway") || t.includes("service unavailable") || t.includes("gateway timeout") || t.includes("rate limit") || t.includes("rate-limit") || t.includes("rate limited");
|
|
4957
|
+
if (serverTransient) return true;
|
|
4958
|
+
const networkTransient = t.includes("socket hang up") || t.includes("closed unexpectedly") || t.includes("connection closed") || t.includes("connection reset") || t.includes("connection error") || t.includes("econnreset") || t.includes("etimedout") || t.includes("econnaborted") || t.includes("epipe") || t.includes("enetunreach") || t.includes("enetreset") || t.includes("ehostunreach") || t.includes("eai_again") || t.includes("network error") || t.includes("fetch failed") || t.includes("request timed out") || t.includes("timed out");
|
|
4959
|
+
return networkTransient;
|
|
4227
4960
|
}
|
|
4228
|
-
const MANAGED_ENV_KEYS = Array.from(MANAGED_KEYS);
|
|
4229
|
-
|
|
4230
|
-
var claudeAuth = /*#__PURE__*/Object.freeze({
|
|
4231
|
-
__proto__: null,
|
|
4232
|
-
EXAMPLE_HYPHA_PROXY_URL: EXAMPLE_HYPHA_PROXY_URL,
|
|
4233
|
-
MANAGED_ENV_KEYS: MANAGED_ENV_KEYS,
|
|
4234
|
-
applyClaudeProxyEnv: applyClaudeProxyEnv,
|
|
4235
|
-
getClaudeAuthStatus: getClaudeAuthStatus,
|
|
4236
|
-
resolveHyphaProxyUrl: resolveHyphaProxyUrl,
|
|
4237
|
-
setClaudeAuthCustom: setClaudeAuthCustom,
|
|
4238
|
-
setClaudeAuthHyphaProxy: setClaudeAuthHyphaProxy,
|
|
4239
|
-
setClaudeAuthLogin: setClaudeAuthLogin,
|
|
4240
|
-
updateEnvFile: updateEnvFile
|
|
4241
|
-
});
|
|
4242
4961
|
|
|
4243
4962
|
const PARTICIPANTS_CHANNEL_ID = "sys-participants";
|
|
4244
4963
|
function channelPublicView(c) {
|
|
@@ -8720,64 +9439,6 @@ function mapDecisionToWire(d) {
|
|
|
8720
9439
|
}
|
|
8721
9440
|
}
|
|
8722
9441
|
|
|
8723
|
-
function codexPermissionSettings(mode) {
|
|
8724
|
-
switch (mode) {
|
|
8725
|
-
case "yolo":
|
|
8726
|
-
case "bypassPermissions":
|
|
8727
|
-
case "danger-full-access":
|
|
8728
|
-
return { approvalPolicy: "never", sandbox: "danger-full-access" };
|
|
8729
|
-
case "safe-yolo":
|
|
8730
|
-
case "acceptEdits":
|
|
8731
|
-
return { approvalPolicy: "on-failure", sandbox: "workspace-write" };
|
|
8732
|
-
case "read-only":
|
|
8733
|
-
case "plan":
|
|
8734
|
-
return { approvalPolicy: "on-request", sandbox: "read-only" };
|
|
8735
|
-
case "default":
|
|
8736
|
-
default:
|
|
8737
|
-
return { approvalPolicy: "on-request", sandbox: "workspace-write" };
|
|
8738
|
-
}
|
|
8739
|
-
}
|
|
8740
|
-
const CODEX_PROVIDER_KEY_ENV = "SVAMP_CODEX_API_KEY";
|
|
8741
|
-
const PROVIDER_NAME = "svamp";
|
|
8742
|
-
function resolveCodexProvider(env = process.env) {
|
|
8743
|
-
return {
|
|
8744
|
-
apiBase: env.SVAMP_CODEX_API_BASE || env.LLM_API_BASE || void 0,
|
|
8745
|
-
apiKey: env.SVAMP_CODEX_API_KEY || env.LLM_API_KEY || void 0,
|
|
8746
|
-
model: env.SVAMP_CODEX_MODEL || env.LLM_MODEL || void 0
|
|
8747
|
-
};
|
|
8748
|
-
}
|
|
8749
|
-
function hasCustomProvider(cfg) {
|
|
8750
|
-
return !!(cfg.apiBase && cfg.apiKey);
|
|
8751
|
-
}
|
|
8752
|
-
function buildCodexProviderArgs(cfg) {
|
|
8753
|
-
if (!hasCustomProvider(cfg)) {
|
|
8754
|
-
return { configArgs: cfg.model ? ["-c", `model=${JSON.stringify(cfg.model)}`] : [], env: {}, model: cfg.model };
|
|
8755
|
-
}
|
|
8756
|
-
const configArgs = [
|
|
8757
|
-
"-c",
|
|
8758
|
-
`model_provider=${JSON.stringify(PROVIDER_NAME)}`,
|
|
8759
|
-
"-c",
|
|
8760
|
-
`model_providers.${PROVIDER_NAME}.name=${JSON.stringify(PROVIDER_NAME)}`,
|
|
8761
|
-
"-c",
|
|
8762
|
-
`model_providers.${PROVIDER_NAME}.base_url=${JSON.stringify(cfg.apiBase)}`,
|
|
8763
|
-
"-c",
|
|
8764
|
-
`model_providers.${PROVIDER_NAME}.env_key=${JSON.stringify(CODEX_PROVIDER_KEY_ENV)}`,
|
|
8765
|
-
"-c",
|
|
8766
|
-
`model_providers.${PROVIDER_NAME}.wire_api=${JSON.stringify("responses")}`
|
|
8767
|
-
];
|
|
8768
|
-
if (cfg.model) configArgs.push("-c", `model=${JSON.stringify(cfg.model)}`);
|
|
8769
|
-
return { configArgs, env: { [CODEX_PROVIDER_KEY_ENV]: cfg.apiKey }, model: cfg.model };
|
|
8770
|
-
}
|
|
8771
|
-
|
|
8772
|
-
var codexProvider = /*#__PURE__*/Object.freeze({
|
|
8773
|
-
__proto__: null,
|
|
8774
|
-
CODEX_PROVIDER_KEY_ENV: CODEX_PROVIDER_KEY_ENV,
|
|
8775
|
-
buildCodexProviderArgs: buildCodexProviderArgs,
|
|
8776
|
-
codexPermissionSettings: codexPermissionSettings,
|
|
8777
|
-
hasCustomProvider: hasCustomProvider,
|
|
8778
|
-
resolveCodexProvider: resolveCodexProvider
|
|
8779
|
-
});
|
|
8780
|
-
|
|
8781
9442
|
class CodexAppServerBackend {
|
|
8782
9443
|
constructor(opts) {
|
|
8783
9444
|
this.opts = opts;
|
|
@@ -13940,7 +14601,7 @@ async function startDaemon(options) {
|
|
|
13940
14601
|
try {
|
|
13941
14602
|
const dir = loadSessionIndex()[sessionId]?.directory;
|
|
13942
14603
|
if (!dir) return;
|
|
13943
|
-
const { reconcileServiceLinks } = await import('./agentCommands-
|
|
14604
|
+
const { reconcileServiceLinks } = await import('./agentCommands-aCeb0xu5.mjs');
|
|
13944
14605
|
const configPath = getSvampConfigPath(dir, sessionId);
|
|
13945
14606
|
const config = readSvampConfig(configPath);
|
|
13946
14607
|
const entries = Array.from(urls.entries());
|
|
@@ -13958,7 +14619,7 @@ async function startDaemon(options) {
|
|
|
13958
14619
|
}
|
|
13959
14620
|
}
|
|
13960
14621
|
async function createExposedTunnel(spec) {
|
|
13961
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
14622
|
+
const { FrpcTunnel } = await import('./frpc-CxqX4sQ7.mjs');
|
|
13962
14623
|
const tunnel = new FrpcTunnel({
|
|
13963
14624
|
name: spec.name,
|
|
13964
14625
|
ports: spec.ports,
|
|
@@ -13979,7 +14640,7 @@ async function startDaemon(options) {
|
|
|
13979
14640
|
}
|
|
13980
14641
|
const tunnelRecreateState = /* @__PURE__ */ new Map();
|
|
13981
14642
|
const tunnelRecreateInFlight = /* @__PURE__ */ new Set();
|
|
13982
|
-
const { ServeManager } = await import('./serveManager-
|
|
14643
|
+
const { ServeManager } = await import('./serveManager-Bk6Fkoha.mjs');
|
|
13983
14644
|
const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
|
|
13984
14645
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
13985
14646
|
});
|
|
@@ -14416,7 +15077,10 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
|
|
|
14416
15077
|
...sessionMetadata,
|
|
14417
15078
|
// Derive the handle name from the id (new-format ids embed it) so the two
|
|
14418
15079
|
// never diverge; fall back to persisted name (legacy ids) then a fresh one.
|
|
14419
|
-
friendlyName: persisted?.metadata?.friendlyName || friendlyNameFromId(sessionId) || generateFriendlyName(collectKnownFriendlyNames())
|
|
15080
|
+
friendlyName: persisted?.metadata?.friendlyName || friendlyNameFromId(sessionId) || generateFriendlyName(collectKnownFriendlyNames()),
|
|
15081
|
+
// Selected backend account (composer). Sticky across respawns + restart via the
|
|
15082
|
+
// persisted metadata; the daemon resolves it to concrete auth at each Claude spawn.
|
|
15083
|
+
accountId: options2.accountId ?? persisted?.metadata?.accountId ?? void 0
|
|
14420
15084
|
};
|
|
14421
15085
|
let currentPermissionMode = options2.permissionMode || persisted?.permissionMode || "bypassPermissions";
|
|
14422
15086
|
const sessionCreatedAt = persisted?.createdAt || Date.now();
|
|
@@ -14916,6 +15580,28 @@ ${parts.join("\n")}`);
|
|
|
14916
15580
|
if (proxyDesc) {
|
|
14917
15581
|
logger.log(`[Session ${sessionId}] Claude auth: ${proxyDesc}`);
|
|
14918
15582
|
}
|
|
15583
|
+
{
|
|
15584
|
+
const acctId = sessionMetadata.accountId;
|
|
15585
|
+
if (acctId) {
|
|
15586
|
+
try {
|
|
15587
|
+
const acct = getAccount(acctId, SVAMP_HOME);
|
|
15588
|
+
if (!acct) {
|
|
15589
|
+
logger.log(`[Session ${sessionId}] account ${acctId} not found \u2014 using machine default`);
|
|
15590
|
+
} else if (acct.vendor !== "anthropic") {
|
|
15591
|
+
logger.log(`[Session ${sessionId}] account ${acctId} is a ${acct.vendor} account, not usable for Claude \u2014 using machine default`);
|
|
15592
|
+
} else {
|
|
15593
|
+
const resolved = resolveAccountEnv(acct, process.env, SVAMP_HOME);
|
|
15594
|
+
for (const [k, v] of Object.entries(resolved.claudeEnv || {})) {
|
|
15595
|
+
if (v === void 0) delete spawnEnv[k];
|
|
15596
|
+
else spawnEnv[k] = v;
|
|
15597
|
+
}
|
|
15598
|
+
logger.log(`[Session ${sessionId}] Claude auth (account): ${resolved.describe}`);
|
|
15599
|
+
}
|
|
15600
|
+
} catch (e) {
|
|
15601
|
+
logger.log(`[Session ${sessionId}] account ${acctId} resolve failed (${e?.message || e}) \u2014 using machine default`);
|
|
15602
|
+
}
|
|
15603
|
+
}
|
|
15604
|
+
}
|
|
14919
15605
|
if (isoConfig && stagedCredentials) {
|
|
14920
15606
|
Object.assign(spawnEnv, stagedCredentials.env);
|
|
14921
15607
|
const filtered = {};
|
|
@@ -16101,11 +16787,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
16101
16787
|
});
|
|
16102
16788
|
},
|
|
16103
16789
|
onIssue: async (params) => {
|
|
16104
|
-
const { issueRpc } = await import('./rpc-
|
|
16790
|
+
const { issueRpc } = await import('./rpc-BB9tE2dR.mjs');
|
|
16105
16791
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
16106
16792
|
},
|
|
16107
16793
|
onWorkflow: async (params) => {
|
|
16108
|
-
const { workflowRpc } = await import('./rpc-
|
|
16794
|
+
const { workflowRpc } = await import('./rpc-BXmmzMmw.mjs');
|
|
16109
16795
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
16110
16796
|
},
|
|
16111
16797
|
onRipgrep: async (args, cwd) => {
|
|
@@ -16396,7 +17082,9 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
16396
17082
|
sharing: options2.sharing,
|
|
16397
17083
|
securityContext: options2.securityContext,
|
|
16398
17084
|
tags: options2.tags,
|
|
16399
|
-
parentSessionId: options2.parentSessionId
|
|
17085
|
+
parentSessionId: options2.parentSessionId,
|
|
17086
|
+
// Selected backend account — sticky across restart via persisted metadata.
|
|
17087
|
+
accountId: options2.accountId ?? acpPersisted?.metadata?.accountId ?? void 0
|
|
16400
17088
|
};
|
|
16401
17089
|
let currentPermissionMode = options2.permissionMode || "bypassPermissions";
|
|
16402
17090
|
const allowedTools = /* @__PURE__ */ new Set();
|
|
@@ -16663,11 +17351,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
16663
17351
|
});
|
|
16664
17352
|
},
|
|
16665
17353
|
onIssue: async (params) => {
|
|
16666
|
-
const { issueRpc } = await import('./rpc-
|
|
17354
|
+
const { issueRpc } = await import('./rpc-BB9tE2dR.mjs');
|
|
16667
17355
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
16668
17356
|
},
|
|
16669
17357
|
onWorkflow: async (params) => {
|
|
16670
|
-
const { workflowRpc } = await import('./rpc-
|
|
17358
|
+
const { workflowRpc } = await import('./rpc-BXmmzMmw.mjs');
|
|
16671
17359
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
16672
17360
|
},
|
|
16673
17361
|
onRipgrep: async (args, cwd) => {
|
|
@@ -16818,16 +17506,41 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
16818
17506
|
const avail = codexAppServerAvailable();
|
|
16819
17507
|
if (avail === null) throw new Error("Codex CLI not found \u2014 install it (`npm i -g @openai/codex`) and authenticate (`svamp daemon codex-auth`).");
|
|
16820
17508
|
if (avail === false) throw new Error("Installed Codex is too old for app-server \u2014 upgrade to >= 0.100 (`npm i -g @openai/codex@latest`).");
|
|
16821
|
-
|
|
17509
|
+
let provider = resolveCodexProvider();
|
|
17510
|
+
let accountCodexEnv;
|
|
17511
|
+
let accountDesc;
|
|
17512
|
+
{
|
|
17513
|
+
const acctId = sessionMetadata.accountId;
|
|
17514
|
+
if (acctId) {
|
|
17515
|
+
try {
|
|
17516
|
+
const acct = getAccount(acctId, SVAMP_HOME);
|
|
17517
|
+
if (!acct) {
|
|
17518
|
+
logger.log(`[Agent Session ${sessionId}] account ${acctId} not found \u2014 using machine default`);
|
|
17519
|
+
} else if (acct.vendor !== "openai") {
|
|
17520
|
+
logger.log(`[Agent Session ${sessionId}] account ${acctId} is a ${acct.vendor} account, not usable for Codex \u2014 using machine default`);
|
|
17521
|
+
} else {
|
|
17522
|
+
const resolved = resolveAccountEnv(acct, process.env, SVAMP_HOME);
|
|
17523
|
+
if (resolved.codexHome) ensureCodexHome(acct, SVAMP_HOME);
|
|
17524
|
+
provider = resolved.codexProvider || {};
|
|
17525
|
+
accountCodexEnv = resolved.extraEnv;
|
|
17526
|
+
accountDesc = resolved.describe;
|
|
17527
|
+
}
|
|
17528
|
+
} catch (e) {
|
|
17529
|
+
logger.log(`[Agent Session ${sessionId}] account ${acctId} resolve failed (${e?.message || e}) \u2014 using machine default`);
|
|
17530
|
+
}
|
|
17531
|
+
}
|
|
17532
|
+
}
|
|
16822
17533
|
const codexModel = options2.model || agentConfig?.default_model || provider.model || void 0;
|
|
16823
17534
|
const codexPerm = codexPermissionSettings(currentPermissionMode);
|
|
16824
|
-
logger.log(`[Agent Session ${sessionId}] Codex backend: app-server (model=${codexModel ?? "default"}${provider.apiBase ? `, provider=${provider.apiBase}` : ""}, mode=${currentPermissionMode} \u2192 approval=${codexPerm.approvalPolicy}/sandbox=${codexPerm.sandbox})`);
|
|
17535
|
+
logger.log(`[Agent Session ${sessionId}] Codex backend: app-server (model=${codexModel ?? "default"}${provider.apiBase ? `, provider=${provider.apiBase}` : ""}${accountDesc ? `, account=${accountDesc}` : ""}, mode=${currentPermissionMode} \u2192 approval=${codexPerm.approvalPolicy}/sandbox=${codexPerm.sandbox})`);
|
|
16825
17536
|
if (acpResumeThreadId) logger.log(`[Agent Session ${sessionId}] Resuming Codex thread ${acpResumeThreadId} (restart recovery)`);
|
|
16826
17537
|
agentBackend = new CodexAppServerBackend({
|
|
16827
17538
|
cwd: directory,
|
|
16828
17539
|
model: codexModel,
|
|
16829
17540
|
provider,
|
|
16830
|
-
env
|
|
17541
|
+
// Account extra env (OPENAI_API_KEY / SVAMP_CODEX_API_KEY / CODEX_HOME) layered
|
|
17542
|
+
// over any per-session environmentVariables from the composer.
|
|
17543
|
+
env: { ...options2.environmentVariables || {}, ...accountCodexEnv || {} },
|
|
16831
17544
|
log: logger.log,
|
|
16832
17545
|
isolationConfig: agentIsoConfig,
|
|
16833
17546
|
approvalPolicy: codexPerm.approvalPolicy,
|
|
@@ -17678,7 +18391,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
17678
18391
|
const PING_TIMEOUT_MS = 15e3;
|
|
17679
18392
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
17680
18393
|
const RECONNECT_JITTER_MS = 2500;
|
|
17681
|
-
const { WorkflowScheduler } = await import('./scheduler-
|
|
18394
|
+
const { WorkflowScheduler } = await import('./scheduler-Xhs6PCCU.mjs');
|
|
17682
18395
|
const workflowScheduler = new WorkflowScheduler({
|
|
17683
18396
|
projectRoots: () => {
|
|
17684
18397
|
const dirs = /* @__PURE__ */ new Set();
|
|
@@ -17696,6 +18409,18 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
17696
18409
|
logger.log(`[workflow] scheduler tick error: ${e?.message || e}`);
|
|
17697
18410
|
}
|
|
17698
18411
|
}, 2e4);
|
|
18412
|
+
const runOAuthAccountRefresh = () => {
|
|
18413
|
+
refreshDueAnthropicAccounts(
|
|
18414
|
+
async (rt) => {
|
|
18415
|
+
const t = await refreshAnthropicToken(rt);
|
|
18416
|
+
return t;
|
|
18417
|
+
},
|
|
18418
|
+
(m) => logger.log(m),
|
|
18419
|
+
SVAMP_HOME
|
|
18420
|
+
).catch((e) => logger.log(`[oauth] account refresh error: ${e?.message || e}`));
|
|
18421
|
+
};
|
|
18422
|
+
runOAuthAccountRefresh();
|
|
18423
|
+
const backendAccountRefreshInterval = setInterval(runOAuthAccountRefresh, 5 * 6e4);
|
|
17699
18424
|
let heartbeatRunning = false;
|
|
17700
18425
|
let lastReconnectAt = 0;
|
|
17701
18426
|
let heartbeatCycle = 0;
|
|
@@ -18289,4 +19014,4 @@ var run = /*#__PURE__*/Object.freeze({
|
|
|
18289
19014
|
writeStopMarker: writeStopMarker
|
|
18290
19015
|
});
|
|
18291
19016
|
|
|
18292
|
-
export { resolveModel as $, runWorkflow as A, setWorkflowEnabled as B, removeWorkflow as C, saveWorkflow as D, rawWorkflow as E, listWorkflows as F, isWorkflowEnabled as G, workflowCrons as H, cronMatches as I, summarize as J, workflowSteps as K, loadMachineContext as L, buildMachineInstructions as M, machineToolsForRole as N, buildMachineTools as O, parseFrontmatter as P, getSkillsServer as Q, READ_ONLY_TOOLS as R, ServeAuth as S, getSkillsWorkspaceName as T, getSkillsCollectionName as U, fetchWithTimeout as V, searchSkills as W, SKILLS_DIR as X, getSkillInfo as Y, downloadSkillFile as Z, listSkillFiles as _, createSessionStore as a, clearStopMarker as a0, stopMarkerExists as a1, formatHandle as a2, normalizeAllowedUser as a3, loadSecurityContextConfig as a4, resolveSecurityContext as a5, buildSecurityContextFromFlags as a6, mergeSecurityContexts as a7, buildSessionShareUrl as a8, computeOutboundHop as a9, registerAwaitingReply as aa, buildMachineShareUrl as ab, parseHandle as ac, handleMatchesMetadata as ad, describeMisconfiguration as ae, buildMachineDeps as af, applyClaudeProxyEnv as ag, composeSessionId as ah, generateFriendlyName as ai, generateHookSettings as aj, claudeAuth as ak,
|
|
19017
|
+
export { resolveModel as $, runWorkflow as A, setWorkflowEnabled as B, removeWorkflow as C, saveWorkflow as D, rawWorkflow as E, listWorkflows as F, isWorkflowEnabled as G, workflowCrons as H, cronMatches as I, summarize as J, workflowSteps as K, loadMachineContext as L, buildMachineInstructions as M, machineToolsForRole as N, buildMachineTools as O, parseFrontmatter as P, getSkillsServer as Q, READ_ONLY_TOOLS as R, ServeAuth as S, getSkillsWorkspaceName as T, getSkillsCollectionName as U, fetchWithTimeout as V, searchSkills as W, SKILLS_DIR as X, getSkillInfo as Y, downloadSkillFile as Z, listSkillFiles as _, createSessionStore as a, clearStopMarker as a0, stopMarkerExists as a1, formatHandle as a2, normalizeAllowedUser as a3, loadSecurityContextConfig as a4, resolveSecurityContext as a5, buildSecurityContextFromFlags as a6, mergeSecurityContexts as a7, buildSessionShareUrl as a8, computeOutboundHop as a9, registerAwaitingReply as aa, buildMachineShareUrl as ab, parseHandle as ac, handleMatchesMetadata as ad, describeMisconfiguration as ae, buildMachineDeps as af, applyClaudeProxyEnv as ag, composeSessionId as ah, generateFriendlyName as ai, generateHookSettings as aj, claudeAuth as ak, codexProvider as al, projectInfo as am, DefaultTransport$1 as an, acpBackend as ao, acpAgentConfig as ap, codexAppServerBackend as aq, GeminiTransport$1 as ar, instanceConfig as as, api as at, run as au, stopDaemon as b, connectToHypha as c, daemonStatus as d, getFrpsSubdomainHost as e, getFrpsServerPort as f, getHyphaServerUrl$1 as g, getFrpsServerAddr as h, shortId as i, getHyphaServerUrl as j, hasCookieToken as k, resolveProjectRoot as l, getIssue as m, resumeIssue as n, addComment as o, pauseIssue as p, addIssue as q, registerMachineService as r, startDaemon as s, listIssues as t, updateIssue as u, searchIssues as v, isVisibleTo as w, getRun as x, listRuns as y, getWorkflow as z };
|