@omnicross/daemon 0.1.0 → 0.1.2

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/cli.cjs CHANGED
@@ -180,23 +180,23 @@ function decodeEnvKey(raw) {
180
180
  }
181
181
  return buf;
182
182
  }
183
- function readKeyFile(path) {
184
- const raw = (0, import_node_fs.readFileSync)(path);
183
+ function readKeyFile(path2) {
184
+ const raw = (0, import_node_fs.readFileSync)(path2);
185
185
  if (raw.length === KEY_BYTES2) return raw;
186
186
  const text = raw.toString("utf8").trim();
187
187
  if (/^[0-9a-fA-F]{64}$/.test(text)) return Buffer.from(text, "hex");
188
188
  const b64 = Buffer.from(text, "base64");
189
189
  if (b64.length === KEY_BYTES2) return b64;
190
190
  throw new Error(
191
- `master key file '${path}' is invalid: expected 32 raw bytes, 64 hex chars, or 32-byte base64`
191
+ `master key file '${path2}' is invalid: expected 32 raw bytes, 64 hex chars, or 32-byte base64`
192
192
  );
193
193
  }
194
- function generateKeyFile(path) {
194
+ function generateKeyFile(path2) {
195
195
  const key = (0, import_node_crypto2.randomBytes)(KEY_BYTES2);
196
- (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
197
- (0, import_node_fs.writeFileSync)(path, key, { mode: 384 });
196
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path2), { recursive: true });
197
+ (0, import_node_fs.writeFileSync)(path2, key, { mode: 384 });
198
198
  try {
199
- (0, import_node_fs.chmodSync)(path, 384);
199
+ (0, import_node_fs.chmodSync)(path2, 384);
200
200
  } catch {
201
201
  }
202
202
  return key;
@@ -575,25 +575,25 @@ var secretBox = null;
575
575
  function setSecretBox(box) {
576
576
  secretBox = box;
577
577
  }
578
- function loadConfig(path) {
578
+ function loadConfig(path2) {
579
579
  let raw;
580
580
  try {
581
- raw = (0, import_node_fs2.readFileSync)(path, "utf8");
581
+ raw = (0, import_node_fs2.readFileSync)(path2, "utf8");
582
582
  } catch {
583
- throw new Error(`config: cannot read file at '${path}'`);
583
+ throw new Error(`config: cannot read file at '${path2}'`);
584
584
  }
585
585
  let parsed;
586
586
  try {
587
587
  parsed = JSON.parse(raw);
588
588
  } catch {
589
- throw new Error(`config: '${path}' is not valid JSON`);
589
+ throw new Error(`config: '${path2}' is not valid JSON`);
590
590
  }
591
591
  const validated = validateConfig(parsed);
592
592
  return secretBox ? decryptConfigSecrets(validated, secretBox) : validated;
593
593
  }
594
- function saveConfig(path, cfg) {
594
+ function saveConfig(path2, cfg) {
595
595
  const toWrite = secretBox ? encryptConfigSecrets(cfg, secretBox) : cfg;
596
- (0, import_node_fs2.writeFileSync)(path, JSON.stringify(toWrite, null, 2) + "\n", "utf8");
596
+ (0, import_node_fs2.writeFileSync)(path2, JSON.stringify(toWrite, null, 2) + "\n", "utf8");
597
597
  }
598
598
 
599
599
  // src/commands/paths.ts
@@ -604,6 +604,12 @@ function defaultKeysPath(configPath) {
604
604
  function defaultTokensPath(configPath) {
605
605
  return (0, import_node_path2.join)((0, import_node_path2.dirname)(configPath), "tokens.json");
606
606
  }
607
+ function defaultPricingPath(configPath) {
608
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(configPath), "pricing.json");
609
+ }
610
+ function defaultUsageEventsPath(configPath) {
611
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(configPath), "usage-events.jsonl");
612
+ }
607
613
  function resolveSecretBox(masterKeyFilePath) {
608
614
  return new SecretBox(() => resolveMasterKey({ keyFilePath: masterKeyFilePath }));
609
615
  }
@@ -781,11 +787,11 @@ async function keysRevoke(db, id) {
781
787
  }
782
788
 
783
789
  // src/commands/launch.ts
784
- var import_node_child_process = require("child_process");
785
- var import_node_fs7 = require("fs");
786
- var import_node_path4 = require("path");
790
+ var import_node_child_process2 = require("child_process");
791
+ var import_node_fs13 = require("fs");
792
+ var import_node_path8 = require("path");
787
793
  var import_node_util3 = require("util");
788
- var import_cli_launcher = require("@omnicross/cli-launcher");
794
+ var import_cli_launcher2 = require("@omnicross/cli-launcher");
789
795
 
790
796
  // src/bootstrap.ts
791
797
  var import_GeminiCodeAssistProjectResolver = require("@omnicross/core/auth/GeminiCodeAssistProjectResolver");
@@ -794,6 +800,7 @@ var import_outbound_api4 = require("@omnicross/core/outbound-api");
794
800
  var import_subscriptionRegistryPort = require("@omnicross/core/outbound-api/subscriptionRegistryPort");
795
801
  var import_gemini_code_assist_resolver = require("@omnicross/core/ports/gemini-code-assist-resolver");
796
802
  var import_provider_proxy = require("@omnicross/core/provider-proxy");
803
+ var import_usage = require("@omnicross/core/usage");
797
804
  var import_subscriptions4 = require("@omnicross/subscriptions");
798
805
 
799
806
  // src/admin/accountsCodexOAuth.ts
@@ -888,7 +895,7 @@ function handleCodexOAuthStatus(sessionId, deps) {
888
895
  }
889
896
 
890
897
  // src/admin/AdminServer.ts
891
- var import_node_crypto6 = require("crypto");
898
+ var import_node_crypto7 = require("crypto");
892
899
  var import_node_http2 = __toESM(require("http"), 1);
893
900
 
894
901
  // src/admin/adminApi.ts
@@ -1158,7 +1165,8 @@ async function handleOAuthComplete(providerId, body, deps) {
1158
1165
  const reason = exchangeError instanceof Error ? exchangeError.message : "token exchange failed";
1159
1166
  return err2(502, `oauth token exchange failed for '${providerId}': ${reason}`);
1160
1167
  }
1161
- await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block);
1168
+ const label = typeof body["label"] === "string" && body["label"].trim() ? body["label"].trim() : void 0;
1169
+ await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
1162
1170
  const status = await statusEntryFor(deps.subscriptionAccounts, providerId);
1163
1171
  return { status: 200, body: status ? { account: status } : { ok: true } };
1164
1172
  }
@@ -1191,8 +1199,202 @@ async function exchangeGemini(code, codeVerifier, exchangeFetch) {
1191
1199
  };
1192
1200
  }
1193
1201
 
1194
- // src/ports/account-multi.ts
1202
+ // src/admin/cliLaunch.ts
1203
+ var import_node_child_process = require("child_process");
1195
1204
  var import_node_crypto4 = require("crypto");
1205
+ var import_node_fs5 = require("fs");
1206
+ var import_node_path3 = require("path");
1207
+ var import_cli_launcher = require("@omnicross/cli-launcher");
1208
+ var LAUNCHABLE_CLIS = [
1209
+ { id: "claude", displayName: "Claude Code", command: "claude" },
1210
+ { id: "codex", displayName: "Codex CLI", command: "codex" },
1211
+ { id: "gemini", displayName: "Gemini CLI", command: "gemini" },
1212
+ { id: "qwen", displayName: "Qwen Code", command: "qwen" },
1213
+ { id: "copilot", displayName: "GitHub Copilot CLI", command: "copilot" },
1214
+ { id: "opencode", displayName: "OpenCode", command: "opencode" }
1215
+ ];
1216
+ var INSTALL_COMMANDS = {
1217
+ claude: "npm install -g @anthropic-ai/claude-code",
1218
+ codex: "npm install -g @openai/codex",
1219
+ gemini: "npm install -g @google/gemini-cli",
1220
+ qwen: "npm install -g @qwen-code/qwen-code",
1221
+ copilot: "npm install -g @github/copilot",
1222
+ opencode: "npm install -g opencode-ai"
1223
+ };
1224
+ var LAUNCHABLE_IDS = new Set(LAUNCHABLE_CLIS.map((c) => c.id));
1225
+ function isLaunchCliId(id) {
1226
+ return id !== void 0 && LAUNCHABLE_IDS.has(id);
1227
+ }
1228
+ function probeDefault(candidate) {
1229
+ const segments = (process.env["PATH"] ?? "").split(import_node_path3.delimiter).filter(Boolean);
1230
+ for (const seg of segments) {
1231
+ const full = (0, import_node_path3.join)(seg, candidate);
1232
+ if ((0, import_node_fs5.existsSync)(full)) return full;
1233
+ }
1234
+ return null;
1235
+ }
1236
+ function isCliInstalled(command, platform = process.platform, probe = probeDefault) {
1237
+ if (platform === "win32") {
1238
+ return Boolean(probe(`${command}.exe`) || probe(`${command}.cmd`) || probe(`${command}.bat`));
1239
+ }
1240
+ return Boolean(probe(command));
1241
+ }
1242
+ function detectClis(platform = process.platform, probe = probeDefault) {
1243
+ return LAUNCHABLE_CLIS.map((c) => ({
1244
+ id: c.id,
1245
+ displayName: c.displayName,
1246
+ command: c.command,
1247
+ installed: isCliInstalled(c.command, platform, probe),
1248
+ installable: Boolean(INSTALL_COMMANDS[c.id])
1249
+ }));
1250
+ }
1251
+ function resolveLaunchTarget(providers, requested) {
1252
+ const pick = (requested?.providerId ? providers.find((p) => p.id === requested.providerId) : void 0) ?? providers.find((p) => p.enabled !== false && firstModel(p)) ?? providers.find((p) => firstModel(p));
1253
+ if (!pick) {
1254
+ throw new Error("no provider with a model is configured \u2014 add one on the Providers page first");
1255
+ }
1256
+ const model = requested?.model || firstModel(pick);
1257
+ if (!model) {
1258
+ throw new Error(`provider "${pick.id}" has no models \u2014 add a model on the Providers page first`);
1259
+ }
1260
+ return { providerId: pick.id, model };
1261
+ }
1262
+ function firstModel(p) {
1263
+ return p.models?.[0] ?? p.modelConfigs?.[0]?.id;
1264
+ }
1265
+ async function buildLaunchEnv(cli, llmConfig, target) {
1266
+ const common = {
1267
+ llmConfig,
1268
+ providerId: target.providerId,
1269
+ model: target.model,
1270
+ sessionId: `dashboard:${cli}`
1271
+ };
1272
+ switch (cli) {
1273
+ case "claude":
1274
+ return (0, import_cli_launcher.buildClaudeCliLaunchConfig)(common);
1275
+ case "codex":
1276
+ return (0, import_cli_launcher.buildCodexLaunchConfig)(common);
1277
+ case "gemini":
1278
+ return (0, import_cli_launcher.buildGeminiCliLaunchConfig)(common);
1279
+ case "qwen":
1280
+ case "copilot":
1281
+ case "opencode":
1282
+ return (0, import_cli_launcher.buildChatCliLaunchConfig)({ backendId: cli, ...common });
1283
+ }
1284
+ }
1285
+ function shq(s) {
1286
+ return `'${s.replace(/'/g, `'\\''`)}'`;
1287
+ }
1288
+ var defaultTerminalOpener = ({ cli, command, extraArgs, env, cwd, platform }) => {
1289
+ const childEnv = { ...process.env, ...env };
1290
+ if (platform === "win32") {
1291
+ const args = ["/c", "start", `"omnicross ${cli}"`];
1292
+ if (cwd) args.push("/D", `"${cwd}"`);
1293
+ args.push("cmd", "/k", command, ...extraArgs);
1294
+ (0, import_node_child_process.spawn)(process.env["ComSpec"] || "cmd.exe", args, {
1295
+ env: childEnv,
1296
+ windowsVerbatimArguments: true,
1297
+ detached: true,
1298
+ stdio: "ignore"
1299
+ }).unref();
1300
+ return;
1301
+ }
1302
+ const exportLine = Object.entries(env).map(([k, v]) => `export ${k}=${shq(v)}`).join("; ");
1303
+ const runLine = [command, ...extraArgs].map(shq).join(" ");
1304
+ const script = `${exportLine}; ${cwd ? `cd ${shq(cwd)}; ` : ""}${runLine}`;
1305
+ if (platform === "darwin") {
1306
+ const osa = `tell application "Terminal" to do script ${JSON.stringify(script)}`;
1307
+ (0, import_node_child_process.spawn)("osascript", ["-e", osa], { detached: true, stdio: "ignore" }).unref();
1308
+ return;
1309
+ }
1310
+ (0, import_node_child_process.spawn)("x-terminal-emulator", ["-e", "bash", "-lc", `${script}; exec bash`], {
1311
+ detached: true,
1312
+ stdio: "ignore"
1313
+ }).unref();
1314
+ };
1315
+ var sessions = /* @__PURE__ */ new Map();
1316
+ function errBody(message) {
1317
+ return { error: { type: "admin_api_error", message } };
1318
+ }
1319
+ var defaultCommandRunner = (command) => new Promise((resolve) => {
1320
+ (0, import_node_child_process.exec)(command, { timeout: 18e4 }, (err5, _stdout, stderr) => {
1321
+ if (err5) resolve({ ok: false, error: stderr.trim() || err5.message });
1322
+ else resolve({ ok: true });
1323
+ });
1324
+ });
1325
+ async function handleCliInstall(cli, runner = defaultCommandRunner) {
1326
+ const cmd = INSTALL_COMMANDS[cli];
1327
+ if (!cmd) {
1328
+ return { status: 400, body: errBody(`no install command for cli '${cli}' (manual install only)`) };
1329
+ }
1330
+ const result = await runner(cmd);
1331
+ if (!result.ok) {
1332
+ return { status: 500, body: errBody(result.error || "install failed") };
1333
+ }
1334
+ return { status: 200, body: { ok: true } };
1335
+ }
1336
+ function handleCliList(platform = process.platform, probe = probeDefault) {
1337
+ return { status: 200, body: { clis: detectClis(platform, probe) } };
1338
+ }
1339
+ function handleCliSessions() {
1340
+ const list = [...sessions.values()].map(({ onSessionEnd: _drop, ...rest }) => rest);
1341
+ return { status: 200, body: { sessions: list } };
1342
+ }
1343
+ function handleCliStop(id) {
1344
+ const s = sessions.get(id);
1345
+ if (!s) return { status: 404, body: errBody(`session '${id}' not found`) };
1346
+ try {
1347
+ s.onSessionEnd();
1348
+ } catch {
1349
+ }
1350
+ sessions.delete(id);
1351
+ return { status: 200, body: { ok: true } };
1352
+ }
1353
+ async function handleCliLaunch(cli, body, ctx) {
1354
+ const platform = ctx.platform ?? process.platform;
1355
+ const probe = ctx.probe ?? probeDefault;
1356
+ const meta = LAUNCHABLE_CLIS.find((c) => c.id === cli);
1357
+ if (!meta) return { status: 404, body: errBody(`unknown cli '${cli}'`) };
1358
+ if (!isCliInstalled(meta.command, platform, probe)) {
1359
+ return { status: 400, body: errBody(`"${meta.command}" is not installed (not found on PATH)`) };
1360
+ }
1361
+ let target;
1362
+ try {
1363
+ target = resolveLaunchTarget(ctx.providers, {
1364
+ providerId: typeof body["providerId"] === "string" ? body["providerId"] : void 0,
1365
+ model: typeof body["model"] === "string" ? body["model"] : void 0
1366
+ });
1367
+ } catch (err5) {
1368
+ return { status: 400, body: errBody(err5 instanceof Error ? err5.message : "no launch target") };
1369
+ }
1370
+ let launch;
1371
+ try {
1372
+ launch = await buildLaunchEnv(cli, ctx.llmConfig, target);
1373
+ } catch (err5) {
1374
+ return { status: 400, body: errBody(err5 instanceof Error ? err5.message : "failed to build launch env") };
1375
+ }
1376
+ const cwd = typeof body["cwd"] === "string" && body["cwd"].trim() ? body["cwd"].trim() : void 0;
1377
+ const opener = ctx.opener ?? defaultTerminalOpener;
1378
+ try {
1379
+ opener({ cli, command: meta.command, extraArgs: launch.extraArgs ?? [], env: launch.env, cwd, platform });
1380
+ } catch (err5) {
1381
+ launch.onSessionEnd();
1382
+ return { status: 500, body: errBody(err5 instanceof Error ? err5.message : "failed to open terminal") };
1383
+ }
1384
+ const id = (0, import_node_crypto4.randomUUID)();
1385
+ sessions.set(id, {
1386
+ id,
1387
+ cli,
1388
+ providerId: target.providerId,
1389
+ model: target.model,
1390
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1391
+ onSessionEnd: launch.onSessionEnd
1392
+ });
1393
+ return { status: 200, body: { sessionId: id, providerId: target.providerId, model: target.model } };
1394
+ }
1395
+
1396
+ // src/ports/account-multi.ts
1397
+ var import_node_crypto5 = require("crypto");
1196
1398
  var PROVIDER_KEYS = {
1197
1399
  claude: { block: "claude", accounts: "claudeAccounts", active: "activeClaudeAccountId" },
1198
1400
  codex: { block: "codex", accounts: "codexAccounts", active: "activeCodexAccountId" },
@@ -1258,7 +1460,7 @@ function migrateLazily(config) {
1258
1460
  }
1259
1461
  function addAccount(config, p, tokens, label) {
1260
1462
  const accounts = [...getAccounts(config, p)];
1261
- const id = (0, import_node_crypto4.randomUUID)();
1463
+ const id = (0, import_node_crypto5.randomUUID)();
1262
1464
  accounts.push({
1263
1465
  id,
1264
1466
  label: label ?? `Account ${accounts.length + 1}`,
@@ -1295,6 +1497,13 @@ function setActiveAccount(config, p, id) {
1295
1497
  deriveMirror(config, p);
1296
1498
  return { ok: true };
1297
1499
  }
1500
+ function listAccounts(config, p) {
1501
+ return getAccounts(config, p);
1502
+ }
1503
+ function getAccountById(config, p, id) {
1504
+ const account = getAccounts(config, p).find((a) => a.id === id);
1505
+ return account ? { id: account.id, tokens: account.tokens } : void 0;
1506
+ }
1298
1507
  function getActiveAccount(config, p) {
1299
1508
  const active = getAccounts(config, p).find((a) => a.id === getActiveId(config, p));
1300
1509
  return active ? { id: active.id, tokens: active.tokens } : void 0;
@@ -1328,12 +1537,27 @@ function sanitizeAccounts(config, p) {
1328
1537
  id: a.id,
1329
1538
  label: a.label,
1330
1539
  status: t.status ?? "unconfigured",
1540
+ authMethod: t.authMethod,
1541
+ subscriptionLevel: t.subscriptionLevel,
1331
1542
  expiresAt: t.expiresAt,
1543
+ lastRefreshedAt: t.lastRefreshedAt,
1544
+ isSetupToken: t.isSetupToken,
1332
1545
  hasAccessToken: !!(t.accessToken || t.apiKey),
1333
- isActive: a.id === activeId
1546
+ isActive: a.id === activeId,
1547
+ syncWarning: t.syncWarning
1334
1548
  };
1335
1549
  });
1336
1550
  }
1551
+ function renameAccount(config, p, id, label) {
1552
+ const accounts = getAccounts(config, p);
1553
+ if (!accounts.some((a) => a.id === id)) return { ok: false };
1554
+ setAccounts(
1555
+ config,
1556
+ p,
1557
+ accounts.map((a) => a.id === id ? { ...a, label } : a)
1558
+ );
1559
+ return { ok: true };
1560
+ }
1337
1561
  function clearProvider(config, p) {
1338
1562
  setBlock(config, p, void 0);
1339
1563
  setAccounts(config, p, void 0);
@@ -1342,7 +1566,7 @@ function clearProvider(config, p) {
1342
1566
  var DAEMON_PROVIDER_KEYS = PROVIDER_KEYS;
1343
1567
 
1344
1568
  // src/migration/packCodec.ts
1345
- var import_node_crypto5 = require("crypto");
1569
+ var import_node_crypto6 = require("crypto");
1346
1570
  var PACK_MAGIC = "OMCXPACK";
1347
1571
  var PACK_VERSION = 1;
1348
1572
  var KDF_ALGORITHM = "scrypt";
@@ -1380,17 +1604,17 @@ function fromB64Url(s) {
1380
1604
  return Buffer.from(s, "base64url").toString("utf8");
1381
1605
  }
1382
1606
  function deriveKey(passphrase, salt, N, r, p) {
1383
- return (0, import_node_crypto5.scryptSync)(passphrase, salt, KEY_BYTES3, { N, r, p, maxmem: SCRYPT_MAXMEM });
1607
+ return (0, import_node_crypto6.scryptSync)(passphrase, salt, KEY_BYTES3, { N, r, p, maxmem: SCRYPT_MAXMEM });
1384
1608
  }
1385
1609
  function aadFor(magic, version, kdf) {
1386
1610
  return Buffer.from(`${magic}|${version}|${kdf}`, "utf8");
1387
1611
  }
1388
1612
  function sealPack(bundleJson, passphrase) {
1389
1613
  assertPassphraseStrength(passphrase);
1390
- const salt = (0, import_node_crypto5.randomBytes)(SCRYPT_SALT_BYTES);
1391
- const iv = (0, import_node_crypto5.randomBytes)(IV_BYTES2);
1614
+ const salt = (0, import_node_crypto6.randomBytes)(SCRYPT_SALT_BYTES);
1615
+ const iv = (0, import_node_crypto6.randomBytes)(IV_BYTES2);
1392
1616
  const key = deriveKey(passphrase, salt, SCRYPT_N, SCRYPT_R, SCRYPT_P);
1393
- const cipher = (0, import_node_crypto5.createCipheriv)("aes-256-gcm", key, iv);
1617
+ const cipher = (0, import_node_crypto6.createCipheriv)("aes-256-gcm", key, iv);
1394
1618
  cipher.setAAD(aadFor(PACK_MAGIC, PACK_VERSION, KDF_ALGORITHM));
1395
1619
  const ciphertext = Buffer.concat([cipher.update(bundleJson, "utf8"), cipher.final()]);
1396
1620
  const tag = cipher.getAuthTag();
@@ -1438,7 +1662,7 @@ function openPack(packString, passphrase) {
1438
1662
  throw new PackAuthError("migration pack is malformed (invalid iv/tag length)");
1439
1663
  }
1440
1664
  const key = deriveKey(passphrase, salt, header.N, header.r, header.p);
1441
- const decipher = (0, import_node_crypto5.createDecipheriv)("aes-256-gcm", key, iv);
1665
+ const decipher = (0, import_node_crypto6.createDecipheriv)("aes-256-gcm", key, iv);
1442
1666
  decipher.setAAD(aadFor(PACK_MAGIC, PACK_VERSION, KDF_ALGORITHM));
1443
1667
  decipher.setAuthTag(tag);
1444
1668
  try {
@@ -1580,6 +1804,169 @@ async function handleImport(body, deps) {
1580
1804
  }
1581
1805
  }
1582
1806
 
1807
+ // src/admin/usagePricing.ts
1808
+ var err4 = (status, message) => ({
1809
+ status,
1810
+ body: { error: { type: "admin_api_error", message } }
1811
+ });
1812
+ function parseFiniteInt(raw) {
1813
+ if (raw === null || raw.trim() === "") return null;
1814
+ const n = Number(raw);
1815
+ return Number.isFinite(n) && Number.isInteger(n) ? n : null;
1816
+ }
1817
+ function parseRange(query) {
1818
+ const startTs = parseFiniteInt(query.get("startTs"));
1819
+ const endTs = parseFiniteInt(query.get("endTs"));
1820
+ if (startTs === null || endTs === null) {
1821
+ return err4(400, "startTs and endTs are required finite-integer unix-millis query params");
1822
+ }
1823
+ return { startTs, endTs };
1824
+ }
1825
+ var isRange = (v) => v.startTs !== void 0 && !("status" in v);
1826
+ async function handleUsageGet(view, query, deps) {
1827
+ const range = parseRange(query);
1828
+ if (!isRange(range)) return range;
1829
+ switch (view) {
1830
+ case "totals":
1831
+ return { status: 200, body: await deps.usageRecorder.getTotals(range) };
1832
+ case "by-model":
1833
+ return { status: 200, body: await deps.usageRecorder.getByModel(range) };
1834
+ case "by-api-key": {
1835
+ const rows = await deps.usageRecorder.getByApiKey(range);
1836
+ const labels = poolKeyLabels(loadConfig(deps.configPath));
1837
+ return {
1838
+ status: 200,
1839
+ body: rows.map((r) => {
1840
+ if (r.apiKeyId === null) {
1841
+ return { ...r, label: "unattributed", providerId: null };
1842
+ }
1843
+ const known = labels.get(r.apiKeyId);
1844
+ return known ? { ...r, label: known.label, providerId: known.providerId } : { ...r, label: r.apiKeyId };
1845
+ })
1846
+ };
1847
+ }
1848
+ default:
1849
+ return err4(404, `unknown usage view '${view ?? ""}'`);
1850
+ }
1851
+ }
1852
+ function poolKeyLabels(cfg) {
1853
+ const out = /* @__PURE__ */ new Map();
1854
+ for (const provider of cfg.providers) {
1855
+ for (const key of provider.apiKeys ?? []) {
1856
+ out.set(key.id, {
1857
+ label: key.label && key.label.length > 0 ? key.label : key.id,
1858
+ providerId: provider.id
1859
+ });
1860
+ }
1861
+ }
1862
+ return out;
1863
+ }
1864
+ var INVALID_PRICE = /* @__PURE__ */ Symbol("invalid-price");
1865
+ function parseOptionalPrice(b, key) {
1866
+ if (!(key in b) || b[key] === null || b[key] === void 0) return null;
1867
+ const v = b[key];
1868
+ return typeof v === "number" && Number.isFinite(v) ? v : INVALID_PRICE;
1869
+ }
1870
+ function parsePricingEntryInput(raw) {
1871
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
1872
+ const b = raw;
1873
+ const providerId = typeof b["providerId"] === "string" && b["providerId"].trim() ? b["providerId"].trim() : "";
1874
+ const modelId = typeof b["modelId"] === "string" && b["modelId"].trim() ? b["modelId"].trim() : "";
1875
+ const inputPrice = b["inputPricePer1m"];
1876
+ const outputPrice = b["outputPricePer1m"];
1877
+ if (!providerId || !modelId) return null;
1878
+ if (typeof inputPrice !== "number" || !Number.isFinite(inputPrice)) return null;
1879
+ if (typeof outputPrice !== "number" || !Number.isFinite(outputPrice)) return null;
1880
+ const cacheRead = parseOptionalPrice(b, "cacheReadPricePer1m");
1881
+ const cacheWrite = parseOptionalPrice(b, "cacheWritePricePer1m");
1882
+ if (cacheRead === INVALID_PRICE || cacheWrite === INVALID_PRICE) return null;
1883
+ return {
1884
+ providerId,
1885
+ modelId,
1886
+ inputPricePer1m: inputPrice,
1887
+ outputPricePer1m: outputPrice,
1888
+ cacheReadPricePer1m: cacheRead,
1889
+ cacheWritePricePer1m: cacheWrite
1890
+ };
1891
+ }
1892
+ async function handlePricingList(deps) {
1893
+ return { status: 200, body: { entries: await deps.pricingEngine.getAll() } };
1894
+ }
1895
+ async function handlePricingUpsert(body, deps) {
1896
+ const input = parsePricingEntryInput(body);
1897
+ if (!input) {
1898
+ return err4(400, "invalid pricing entry (providerId, modelId, finite numeric inputPricePer1m/outputPricePer1m required)");
1899
+ }
1900
+ const entry = await deps.pricingEngine.upsertManual(input);
1901
+ return { status: 200, body: { entry } };
1902
+ }
1903
+ async function handlePricingDelete(query, deps) {
1904
+ const providerId = query.get("providerId")?.trim() ?? "";
1905
+ const modelId = query.get("modelId")?.trim() ?? "";
1906
+ if (!providerId || !modelId) {
1907
+ return err4(400, "delete requires providerId and modelId query params");
1908
+ }
1909
+ const deleted = await deps.pricingStore.delete(providerId, modelId);
1910
+ if (deleted) await deps.pricingEngine.invalidateCache();
1911
+ return { status: 200, body: { deleted } };
1912
+ }
1913
+ async function handlePricingFetchLatest(deps) {
1914
+ try {
1915
+ const result = await deps.pricingEngine.fetchLatestFromSource();
1916
+ return {
1917
+ status: 200,
1918
+ body: {
1919
+ appliedCount: result.applied.length,
1920
+ conflicts: result.conflicts,
1921
+ fetchedAt: result.fetchedAt,
1922
+ sourceUrl: result.sourceUrl
1923
+ }
1924
+ };
1925
+ } catch (e) {
1926
+ return err4(502, `pricing-source fetch failed: ${e instanceof Error ? e.message : String(e)}`);
1927
+ }
1928
+ }
1929
+ async function handlePricingResolveConflicts(body, deps) {
1930
+ const raw = body["resolutions"];
1931
+ if (!Array.isArray(raw)) {
1932
+ return err4(400, "resolve-conflicts requires { resolutions: [...] }");
1933
+ }
1934
+ const currentRows = await deps.pricingStore.getAll();
1935
+ const userEditedKeys = new Set(
1936
+ currentRows.filter((r) => r.userEdited).map((r) => `${r.providerId}::${r.modelId}`)
1937
+ );
1938
+ const decisions = [];
1939
+ const pendingIncoming = /* @__PURE__ */ new Map();
1940
+ let staleCount = 0;
1941
+ for (const item of raw) {
1942
+ if (!item || typeof item !== "object") return err4(400, "invalid resolution entry");
1943
+ const r = item;
1944
+ const action = r["action"];
1945
+ if (action !== "overwrite" && action !== "skip") {
1946
+ return err4(400, "resolution action must be 'overwrite' or 'skip'");
1947
+ }
1948
+ const providerId = typeof r["providerId"] === "string" && r["providerId"].trim() ? r["providerId"].trim() : "";
1949
+ const modelId = typeof r["modelId"] === "string" && r["modelId"].trim() ? r["modelId"].trim() : "";
1950
+ if (!providerId || !modelId) {
1951
+ return err4(400, "each resolution requires top-level providerId and modelId");
1952
+ }
1953
+ const incoming = parsePricingEntryInput(r["incoming"]);
1954
+ if (!incoming) return err4(400, "each resolution must echo a valid incoming pricing entry");
1955
+ if (incoming.providerId !== providerId || incoming.modelId !== modelId) {
1956
+ return err4(400, "resolution providerId/modelId must match the echoed incoming entry");
1957
+ }
1958
+ const key = `${providerId}::${modelId}`;
1959
+ if (action === "overwrite" && !userEditedKeys.has(key)) {
1960
+ staleCount += 1;
1961
+ continue;
1962
+ }
1963
+ decisions.push({ providerId, modelId, action });
1964
+ pendingIncoming.set(key, incoming);
1965
+ }
1966
+ const resolution = await deps.pricingEngine.resolveConflicts(decisions, pendingIncoming);
1967
+ return { status: 200, body: { ...resolution, staleCount } };
1968
+ }
1969
+
1583
1970
  // src/admin/adminApi.ts
1584
1971
  function readBody(req) {
1585
1972
  return new Promise((resolve, reject) => {
@@ -1670,9 +2057,9 @@ function toProviderView(row) {
1670
2057
  selectedApiModeId: row.selectedApiModeId
1671
2058
  };
1672
2059
  }
1673
- async function handleAdminApi(req, res, path, deps) {
2060
+ async function handleAdminApi(req, res, path2, deps) {
1674
2061
  const method = (req.method ?? "GET").toUpperCase();
1675
- const sub = path.slice("/admin/api/".length);
2062
+ const sub = path2.slice("/admin/api/".length);
1676
2063
  const [resource, ...rest] = sub.split("/").filter((s) => s.length > 0);
1677
2064
  try {
1678
2065
  switch (resource) {
@@ -1686,6 +2073,8 @@ async function handleAdminApi(req, res, path, deps) {
1686
2073
  return await handleServer(req, res, method, deps);
1687
2074
  case "accounts":
1688
2075
  return await handleAccounts(req, res, method, rest, deps);
2076
+ case "cli":
2077
+ return await handleCli(req, res, method, rest, deps);
1689
2078
  case "status":
1690
2079
  return await handleStatus(res, method, deps);
1691
2080
  case "playground":
@@ -1694,12 +2083,47 @@ async function handleAdminApi(req, res, path, deps) {
1694
2083
  return await handleMigrationExport(req, res, method, deps);
1695
2084
  case "import":
1696
2085
  return await handleMigrationImport(req, res, method, deps);
2086
+ case "usage":
2087
+ return await handleUsage(req, res, method, rest, deps);
2088
+ case "pricing":
2089
+ return await handlePricing(req, res, method, rest, deps);
1697
2090
  default:
1698
2091
  return writeJsonError(res, 404, `unknown admin resource '${resource}'`);
1699
2092
  }
1700
- } catch (err4) {
1701
- writeJsonError(res, 500, err4 instanceof Error ? err4.message : String(err4));
2093
+ } catch (err5) {
2094
+ writeJsonError(res, 500, err5 instanceof Error ? err5.message : String(err5));
2095
+ }
2096
+ }
2097
+ function requestQuery(req) {
2098
+ const raw = req.url ?? "";
2099
+ const qIdx = raw.indexOf("?");
2100
+ return new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
2101
+ }
2102
+ function writeResult(res, result) {
2103
+ writeJson(res, result.status, result.body);
2104
+ }
2105
+ async function handleUsage(req, res, method, rest, deps) {
2106
+ if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on usage`);
2107
+ return writeResult(res, await handleUsageGet(rest[0], requestQuery(req), deps));
2108
+ }
2109
+ async function handlePricing(req, res, method, rest, deps) {
2110
+ if (rest.length === 0) {
2111
+ if (method === "GET") return writeResult(res, await handlePricingList(deps));
2112
+ if (method === "PUT") {
2113
+ return writeResult(res, await handlePricingUpsert(await readJsonBody(req), deps));
2114
+ }
2115
+ if (method === "DELETE") {
2116
+ return writeResult(res, await handlePricingDelete(requestQuery(req), deps));
2117
+ }
2118
+ return writeJsonError(res, 405, `method ${method} not allowed on pricing`);
2119
+ }
2120
+ if (method === "POST" && rest.length === 1 && rest[0] === "fetch-latest") {
2121
+ return writeResult(res, await handlePricingFetchLatest(deps));
1702
2122
  }
2123
+ if (method === "POST" && rest.length === 1 && rest[0] === "resolve-conflicts") {
2124
+ return writeResult(res, await handlePricingResolveConflicts(await readJsonBody(req), deps));
2125
+ }
2126
+ return writeJsonError(res, 404, `unknown pricing route '${rest.join("/")}'`);
1703
2127
  }
1704
2128
  function migrationDeps(deps) {
1705
2129
  return {
@@ -1747,6 +2171,11 @@ async function handleProviders(req, res, method, rest, deps) {
1747
2171
  if (method === "POST" && rest.length === 2 && rest[1] === "test") {
1748
2172
  return await handleTestModel(req, res, rest[0], cfg);
1749
2173
  }
2174
+ if (method === "GET" && rest.length === 2 && rest[1] === "reveal-key") {
2175
+ const row = cfg.providers.find((p) => p.id === rest[0]);
2176
+ if (!row) return writeJsonError(res, 404, `provider '${rest[0]}' not found`);
2177
+ return writeJson(res, 200, { apiKey: row.apiKey ?? "" });
2178
+ }
1750
2179
  if (method === "GET") {
1751
2180
  return writeJson(res, 200, { providers: cfg.providers.map(toProviderView) });
1752
2181
  }
@@ -1842,8 +2271,8 @@ async function handleDiscoverModels(res, id, cfg) {
1842
2271
  const data = await response.json();
1843
2272
  const models = Array.isArray(data?.data) ? data.data.map((m) => typeof m?.id === "string" ? m.id : "").filter((m) => m.length > 0) : [];
1844
2273
  return writeJson(res, 200, { models });
1845
- } catch (err4) {
1846
- const message = err4 instanceof Error ? err4.message : String(err4);
2274
+ } catch (err5) {
2275
+ const message = err5 instanceof Error ? err5.message : String(err5);
1847
2276
  return writeJson(res, 200, { models: [], error: `discovery failed: ${message}` });
1848
2277
  }
1849
2278
  }
@@ -1902,8 +2331,8 @@ async function handleTestModel(req, res, id, cfg) {
1902
2331
  latencyMs,
1903
2332
  sample: extractSampleText(text, row.apiFormat)
1904
2333
  });
1905
- } catch (err4) {
1906
- const message = err4 instanceof Error ? err4.message : String(err4);
2334
+ } catch (err5) {
2335
+ const message = err5 instanceof Error ? err5.message : String(err5);
1907
2336
  return writeJson(res, 200, { ok: false, latencyMs: Date.now() - startedAt, message });
1908
2337
  }
1909
2338
  }
@@ -2252,7 +2681,8 @@ async function handleAccounts(req, res, method, rest, deps) {
2252
2681
  if (method === "GET" && rest.length === 0) {
2253
2682
  const accounts = await deps.subscriptionAccounts.listAll();
2254
2683
  const providerAccounts = await deps.subscriptionTokenWriter.listSanitizedAccounts();
2255
- return writeJson(res, 200, { accounts, providerAccounts });
2684
+ const externalCli = await deps.subscriptionTokenWriter.listExternalCliAvailability();
2685
+ return writeJson(res, 200, { accounts, providerAccounts, externalCli });
2256
2686
  }
2257
2687
  if (method === "GET" && rest[0] === "codex" && rest[1] === "oauth" && rest[3] === "status") {
2258
2688
  const result = handleCodexOAuthStatus(rest[2], deps);
@@ -2272,6 +2702,47 @@ async function handleAccounts(req, res, method, rest, deps) {
2272
2702
  const result = await handleOAuthComplete(providerId, body2, deps);
2273
2703
  return writeJson(res, result.status, result.body);
2274
2704
  }
2705
+ if (method === "POST" && rest[1] === "accounts") {
2706
+ const body2 = await readJsonBody(req);
2707
+ const block = validateTokenBody(providerId, body2);
2708
+ if (!block) {
2709
+ return writeJsonError(res, 400, `malformed token body for provider '${providerId}'`);
2710
+ }
2711
+ const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
2712
+ await deps.subscriptionAccountAppender.appendProviderAccount(providerId, block, label);
2713
+ const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
2714
+ return writeJson(res, 200, status2 ? { account: status2 } : { ok: true });
2715
+ }
2716
+ if (method === "POST" && rest[1] === "import-external") {
2717
+ if (providerId !== "claude" && providerId !== "codex") {
2718
+ return writeJsonError(res, 400, `provider '${providerId}' has no external CLI store`);
2719
+ }
2720
+ const body2 = await readJsonBody(req);
2721
+ const label = typeof body2["label"] === "string" && body2["label"].trim() ? body2["label"].trim() : void 0;
2722
+ const result = await deps.subscriptionTokenWriter.importExternalCliAccount(providerId, label);
2723
+ if (!result.ok) {
2724
+ return writeJsonError(res, 409, `no usable external ${providerId} CLI credential found`);
2725
+ }
2726
+ const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
2727
+ return writeJson(res, 200, { ok: true, account: status2 ?? void 0 });
2728
+ }
2729
+ if (method === "POST" && rest[1] === "refresh") {
2730
+ if (providerId === "opencodego") {
2731
+ return writeJsonError(res, 400, "opencodego credentials are not refreshable");
2732
+ }
2733
+ const writer = deps.subscriptionTokenWriter;
2734
+ const ok = providerId === "claude" ? await writer.refreshClaudeToken() : providerId === "codex" ? await writer.refreshCodexToken() : await writer.refreshGeminiToken();
2735
+ const status2 = await statusEntryFor(deps.subscriptionAccounts, providerId);
2736
+ return writeJson(res, 200, { ok, account: status2 ?? void 0 });
2737
+ }
2738
+ if (method === "POST" && rest[2] === "label") {
2739
+ const accountId = rest[1];
2740
+ const body2 = await readJsonBody(req);
2741
+ const label = typeof body2["label"] === "string" ? body2["label"].trim() : "";
2742
+ const result = await deps.subscriptionTokenWriter.renameAccount(providerId, accountId, label);
2743
+ if (!result.ok) return writeJsonError(res, 404, `account '${accountId}' not found`);
2744
+ return writeJson(res, 200, { ok: true });
2745
+ }
2275
2746
  if (method === "PUT" && rest[1] === "active") {
2276
2747
  const body2 = await readJsonBody(req);
2277
2748
  const id = typeof body2["id"] === "string" ? body2["id"] : "";
@@ -2301,6 +2772,44 @@ async function handleAccounts(req, res, method, rest, deps) {
2301
2772
  }
2302
2773
  return writeJsonError(res, 405, `method ${method} not allowed on accounts`);
2303
2774
  }
2775
+ async function handleCli(req, res, method, rest, deps) {
2776
+ if (method === "GET" && rest.length === 0) {
2777
+ const result = handleCliList(process.platform, deps.cliPathProbe);
2778
+ return writeJson(res, result.status, result.body);
2779
+ }
2780
+ if (method === "GET" && rest[0] === "sessions") {
2781
+ const result = handleCliSessions();
2782
+ return writeJson(res, result.status, result.body);
2783
+ }
2784
+ if (method === "DELETE" && rest[0] === "sessions" && rest[1]) {
2785
+ const result = handleCliStop(rest[1]);
2786
+ return writeJson(res, result.status, result.body);
2787
+ }
2788
+ if (method === "POST" && rest[1] === "install") {
2789
+ const cli = rest[0];
2790
+ if (!isLaunchCliId(cli)) {
2791
+ return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
2792
+ }
2793
+ const result = await handleCliInstall(cli, deps.cliCommandRunner);
2794
+ return writeJson(res, result.status, result.body);
2795
+ }
2796
+ if (method === "POST" && rest[1] === "launch") {
2797
+ const cli = rest[0];
2798
+ if (!isLaunchCliId(cli)) {
2799
+ return writeJsonError(res, 400, `unknown cli '${cli ?? ""}'`);
2800
+ }
2801
+ const body = await readJsonBody(req);
2802
+ const providers = loadConfig(deps.configPath).providers ?? [];
2803
+ const result = await handleCliLaunch(cli, body, {
2804
+ llmConfig: deps.llmConfig,
2805
+ providers,
2806
+ opener: deps.cliTerminalOpener,
2807
+ probe: deps.cliPathProbe
2808
+ });
2809
+ return writeJson(res, result.status, result.body);
2810
+ }
2811
+ return writeJsonError(res, 405, `method ${method} not allowed on cli`);
2812
+ }
2304
2813
  async function handleStatus(res, method, deps) {
2305
2814
  if (method !== "GET") return writeJsonError(res, 405, `method ${method} not allowed on status`);
2306
2815
  const status = deps.outboundApiServer.getStatus();
@@ -2336,21 +2845,21 @@ async function handlePlayground(req, res, method, deps) {
2336
2845
  const payload = body["body"];
2337
2846
  const status = deps.outboundApiServer.getStatus();
2338
2847
  if (!status.running || !status.port) return writeJsonError(res, 503, "outbound server not running");
2339
- const path = resolvePlaygroundPath(endpoint, isRecord(payload) ? payload : {});
2340
- if (!path) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
2848
+ const path2 = resolvePlaygroundPath(endpoint, isRecord(payload) ? payload : {});
2849
+ if (!path2) return writeJsonError(res, 400, `unknown endpoint '${endpoint}'`);
2341
2850
  const upstreamBody = typeof payload === "string" ? payload : JSON.stringify(payload ?? {});
2342
- await proxyToOutbound(res, status.port, path, key, upstreamBody);
2851
+ await proxyToOutbound(res, status.port, path2, key, upstreamBody);
2343
2852
  }
2344
2853
  function isRecord(v) {
2345
2854
  return !!v && typeof v === "object" && !Array.isArray(v);
2346
2855
  }
2347
- function proxyToOutbound(res, outboundPort, path, key, body) {
2856
+ function proxyToOutbound(res, outboundPort, path2, key, body) {
2348
2857
  return new Promise((resolve) => {
2349
2858
  const upstream = import_node_http.default.request(
2350
2859
  {
2351
2860
  host: "127.0.0.1",
2352
2861
  port: outboundPort,
2353
- path,
2862
+ path: path2,
2354
2863
  method: "POST",
2355
2864
  headers: {
2356
2865
  "Content-Type": "application/json",
@@ -2370,8 +2879,8 @@ function proxyToOutbound(res, outboundPort, path, key, body) {
2370
2879
  });
2371
2880
  }
2372
2881
  );
2373
- upstream.on("error", (err4) => {
2374
- if (!res.headersSent) writeJsonError(res, 502, `playground proxy failed: ${err4.message}`);
2882
+ upstream.on("error", (err5) => {
2883
+ if (!res.headersSent) writeJsonError(res, 502, `playground proxy failed: ${err5.message}`);
2375
2884
  else res.end();
2376
2885
  resolve();
2377
2886
  });
@@ -2380,463 +2889,106 @@ function proxyToOutbound(res, outboundPort, path, key, body) {
2380
2889
  });
2381
2890
  }
2382
2891
 
2383
- // src/admin/client.ts
2384
- var DASHBOARD_JS = String.raw`
2385
- (function () {
2386
- var $ = function (id) { return document.getElementById(id); };
2387
- var authToken = null; // set if a 401 ever comes back (token-gated deploys)
2388
-
2389
- function headers(extra) {
2390
- var h = extra || {};
2391
- if (authToken) h['Authorization'] = 'Bearer ' + authToken;
2392
- return h;
2393
- }
2394
-
2395
- async function api(method, path, body) {
2396
- var opt = { method: method, headers: headers(body ? { 'Content-Type': 'application/json' } : {}) };
2397
- if (body) opt.body = JSON.stringify(body);
2398
- var res = await fetch('/admin/api/' + path, opt);
2399
- if (res.status === 401 && !authToken) {
2400
- var t = window.prompt('Admin token required');
2401
- if (t) { authToken = t; return api(method, path, body); }
2402
- }
2403
- var text = await res.text();
2404
- var json = null;
2405
- try { json = text ? JSON.parse(text) : null; } catch (e) { json = { raw: text }; }
2406
- return { status: res.status, json: json };
2407
- }
2408
-
2409
- function clear(el) { while (el.firstChild) el.removeChild(el.firstChild); }
2410
- function td(text) { var c = document.createElement('td'); c.textContent = text == null ? '' : String(text); return c; }
2411
- function btn(label, cls, fn) { var b = document.createElement('button'); b.textContent = label; if (cls) b.className = cls; b.onclick = fn; return b; }
2412
-
2413
- // ── Status ────────────────────────────────────────────────────────────────
2414
- async function loadStatus() {
2415
- var r = await api('GET', 'status');
2416
- var s = r.json || {};
2417
- $('statusBadge').textContent = s.running ? ('running :' + s.port) : 'stopped';
2418
- var html = '';
2419
- if (s.running) {
2420
- html += 'Outbound server <span class="pill ok">running</span> on port ' + s.port + '<br/>';
2421
- if (s.formats) {
2422
- html += '<div class="mono muted">' +
2423
- 'chat: ' + s.formats.chat + '<br/>responses: ' + s.formats.responses +
2424
- '<br/>messages: ' + s.formats.messages + '<br/>gemini: ' + s.formats.gemini + '</div>';
2425
- }
2426
- } else {
2427
- html += 'Outbound server <span class="pill bad">stopped</span>';
2428
- }
2429
- $('statusBody').innerHTML = html;
2430
- }
2431
-
2432
- // ── Providers ───────────────────────────────────────────────────────────────
2433
- // Curated presets loaded from GET /admin/api/presets. Selecting one prefills
2434
- // the add-form (format/base/models); the WRITE still goes through the existing
2435
- // POST/PUT /admin/api/providers path (no new write endpoint).
2436
- var presetsById = {};
2437
- var presetModels = []; // models staged by the last preset prefill
2438
-
2439
- async function loadPresets() {
2440
- var r = await api('GET', 'presets');
2441
- var sel = $('pPreset');
2442
- // Keep the placeholder; drop any previously appended options.
2443
- while (sel.options.length > 1) sel.remove(1);
2444
- presetsById = {};
2445
- (r.json && r.json.presets || []).forEach(function (p) {
2446
- presetsById[p.id] = p;
2447
- var opt = document.createElement('option');
2448
- opt.value = p.id;
2449
- opt.textContent = p.name + ' (' + p.apiFormat + ')';
2450
- sel.appendChild(opt);
2451
- });
2452
- }
2453
-
2454
- function onPresetChange() {
2455
- var p = presetsById[$('pPreset').value];
2456
- if (!p) { presetModels = []; return; }
2457
- if (!$('pId').value.trim()) $('pId').value = p.id;
2458
- $('pFormat').value = p.apiFormat;
2459
- $('pBase').value = p.baseUrl;
2460
- presetModels = Array.isArray(p.models) ? p.models.slice() : [];
2461
- }
2462
-
2463
- async function loadProviders() {
2464
- var r = await api('GET', 'providers');
2465
- var body = $('providersTable').querySelector('tbody');
2466
- clear(body);
2467
- (r.json && r.json.providers || []).forEach(function (p) {
2468
- var tr = document.createElement('tr');
2469
- tr.appendChild(td(p.id));
2470
- tr.appendChild(td(p.apiFormat));
2471
- tr.appendChild(td(p.baseUrl));
2472
- tr.appendChild(td(p.hasApiKey ? p.apiKeyMasked : '(none)'));
2473
- var act = document.createElement('td');
2474
- act.appendChild(btn('Edit', 'secondary', function () {
2475
- $('pId').value = p.id; $('pFormat').value = p.apiFormat; $('pBase').value = p.baseUrl; $('pKey').value = '';
2476
- }));
2477
- act.appendChild(btn('Pool', 'secondary', function () { loadProviderKeys(p.id); }));
2478
- act.appendChild(btn('Delete', 'danger', async function () {
2479
- if (window.confirm('Delete provider ' + p.id + '?')) { await api('DELETE', 'providers/' + encodeURIComponent(p.id)); loadProviders(); }
2480
- }));
2481
- tr.appendChild(act);
2482
- body.appendChild(tr);
2483
- });
2484
- }
2485
-
2486
- // ── Pool health (read-only; key-pool change) ──────────────────────────────
2487
- // GET /admin/api/providers/:id/keys → masked pool view. Multi-key is
2488
- // cold-standby + observable in v1 (no outbound failover yet — see panel note).
2489
- async function loadProviderKeys(providerId) {
2490
- var r = await api('GET', 'providers/' + encodeURIComponent(providerId) + '/keys');
2491
- var panel = $('poolPanel');
2492
- var body = $('poolTable').querySelector('tbody');
2493
- clear(body);
2494
- $('poolTitle').textContent = 'API key pool — ' + providerId;
2495
- (r.json && r.json.keys || []).forEach(function (k) {
2496
- var tr = document.createElement('tr');
2497
- tr.appendChild(td(k.id));
2498
- tr.appendChild(td(k.label));
2499
- tr.appendChild(td(k.apiKeyMasked));
2500
- tr.appendChild(td(k.enabled ? 'yes' : 'no'));
2501
- tr.appendChild(td(k.weight));
2502
- var h = '';
2503
- if (k.health && k.health.autoDisabled) h += 'auto-disabled (' + k.health.autoDisabled.status + ') ';
2504
- if (k.health && k.health.cooldown) h += 'cooldown until ' + new Date(k.health.cooldown.until).toLocaleTimeString();
2505
- tr.appendChild(td(h || 'ok'));
2506
- body.appendChild(tr);
2507
- });
2508
- panel.style.display = 'block';
2509
- }
2510
-
2511
- async function saveProvider() {
2512
- $('pErr').textContent = '';
2513
- var id = $('pId').value.trim();
2514
- if (!id) { $('pErr').textContent = 'id required'; return; }
2515
- var payload = { id: id, apiFormat: $('pFormat').value, baseUrl: $('pBase').value.trim(), apiKey: $('pKey').value };
2516
- // Carry the preset-prefilled models (existing parseProviderInput accepts them).
2517
- if (presetModels.length) payload.models = presetModels;
2518
- // Try PUT first (edit, blank key keeps existing); fall back to POST (create).
2519
- var r = await api('PUT', 'providers/' + encodeURIComponent(id), payload);
2520
- if (r.status === 404) r = await api('POST', 'providers', payload);
2521
- if (r.status >= 400) { $('pErr').textContent = (r.json && r.json.error && r.json.error.message) || ('error ' + r.status); return; }
2522
- $('pId').value = ''; $('pBase').value = ''; $('pKey').value = '';
2523
- $('pPreset').value = ''; presetModels = []; // reset the picker after a write
2524
- loadProviders();
2525
- }
2526
-
2527
- // ── Keys ────────────────────────────────────────────────────────────────────
2528
- async function loadKeys() {
2529
- var r = await api('GET', 'keys');
2530
- var body = $('keysTable').querySelector('tbody');
2531
- clear(body);
2532
- (r.json && r.json.keys || []).forEach(function (k) {
2533
- var tr = document.createElement('tr');
2534
- tr.appendChild(td(k.name));
2535
- tr.appendChild(td(k.keyPrefix));
2536
- tr.appendChild(td(k.enabled ? 'yes' : 'no'));
2537
- tr.appendChild(td(k.revoked ? 'yes' : 'no'));
2538
- var act = document.createElement('td');
2539
- if (!k.revoked) {
2540
- act.appendChild(btn(k.enabled ? 'Disable' : 'Enable', 'secondary', async function () {
2541
- await api('POST', 'keys/' + encodeURIComponent(k.id) + '/enabled', { enabled: !k.enabled }); loadKeys();
2542
- }));
2543
- act.appendChild(btn('Revoke', 'danger', async function () {
2544
- if (window.confirm('Revoke ' + k.name + '?')) { await api('POST', 'keys/' + encodeURIComponent(k.id) + '/revoke'); loadKeys(); }
2545
- }));
2546
- }
2547
- tr.appendChild(act);
2548
- body.appendChild(tr);
2549
- });
2550
- }
2551
-
2552
- function showKeyModal(plaintext) {
2553
- $('keyPlaintext').textContent = plaintext;
2554
- $('keyModalBg').classList.add('show');
2555
- $('keyCopy').onclick = function () { navigator.clipboard && navigator.clipboard.writeText(plaintext); };
2556
- $('keyClose').onclick = function () {
2557
- $('keyModalBg').classList.remove('show');
2558
- $('keyPlaintext').textContent = ''; // never persist the plaintext
2559
- };
2560
- }
2561
-
2562
- async function createKey() {
2563
- var name = $('kName').value.trim() || 'key';
2564
- var r = await api('POST', 'keys', { name: name });
2565
- if (r.json && r.json.plaintextOnce) { showKeyModal(r.json.plaintextOnce); $('kName').value = ''; loadKeys(); }
2566
- }
2567
-
2568
- // ── Server config ───────────────────────────────────────────────────────────
2569
- async function loadServer() {
2570
- var r = await api('GET', 'server');
2571
- var s = (r.json && r.json.server) || {};
2572
- $('sEnabled').checked = !!s.enabled;
2573
- $('sLan').checked = !!s.networkBinding;
2574
- $('sPort').value = s.port || '';
2575
- var body = $('endpointsTable').querySelector('tbody');
2576
- clear(body);
2577
- (s.endpoints || []).forEach(function (e) {
2578
- var tr = document.createElement('tr');
2579
- tr.appendChild(td(e.endpoint));
2580
- tr.appendChild(td(e.defaultModel));
2581
- tr.appendChild(td(e.useSubscription ? 'yes' : 'no'));
2582
- body.appendChild(tr);
2583
- });
2892
+ // src/admin/uiStatic.ts
2893
+ var import_node_fs6 = require("fs");
2894
+ var import_promises = require("fs/promises");
2895
+ var import_node_module = require("module");
2896
+ var import_node_path4 = __toESM(require("path"), 1);
2897
+ var import_meta = {};
2898
+ var CONTENT_TYPES = {
2899
+ ".html": "text/html; charset=utf-8",
2900
+ ".js": "text/javascript; charset=utf-8",
2901
+ ".mjs": "text/javascript; charset=utf-8",
2902
+ ".css": "text/css; charset=utf-8",
2903
+ ".json": "application/json; charset=utf-8",
2904
+ ".svg": "image/svg+xml",
2905
+ ".png": "image/png",
2906
+ ".ico": "image/x-icon",
2907
+ ".webp": "image/webp",
2908
+ ".woff": "font/woff",
2909
+ ".woff2": "font/woff2",
2910
+ ".ttf": "font/ttf",
2911
+ ".map": "application/json; charset=utf-8",
2912
+ ".txt": "text/plain; charset=utf-8"
2913
+ };
2914
+ function resolveUiDist() {
2915
+ const fromEnv = process.env["OMNICROSS_UI_DIST"];
2916
+ if (fromEnv) {
2917
+ return (0, import_node_fs6.existsSync)(import_node_path4.default.join(fromEnv, "index.html")) ? import_node_path4.default.resolve(fromEnv) : null;
2584
2918
  }
2585
-
2586
- async function saveServer() {
2587
- var patch = { enabled: $('sEnabled').checked, networkBinding: $('sLan').checked };
2588
- var port = parseInt($('sPort').value, 10);
2589
- if (port) patch.port = port;
2590
- await api('PUT', 'server', patch);
2591
- loadServer(); loadStatus();
2919
+ try {
2920
+ const req = (0, import_node_module.createRequire)(typeof __filename !== "undefined" ? __filename : import_meta.url);
2921
+ const pkgJson = req.resolve("@omnicross/ui/package.json");
2922
+ const dist = import_node_path4.default.join(import_node_path4.default.dirname(pkgJson), "dist");
2923
+ return (0, import_node_fs6.existsSync)(import_node_path4.default.join(dist, "index.html")) ? dist : null;
2924
+ } catch {
2925
+ return null;
2592
2926
  }
2593
-
2594
- // ── Accounts ────────────────────────────────────────────────────────────────
2595
- // The GET stays token-free (status only). The Save/Clear actions WRITE tokens
2596
- // (secret IN); the form never renders an existing/stored token (write-only).
2597
- async function loadAccounts() {
2598
- var r = await api('GET', 'accounts');
2599
- var body = $('accountsTable').querySelector('tbody');
2600
- clear(body);
2601
- (r.json && r.json.accounts || []).forEach(function (a) {
2602
- var tr = document.createElement('tr');
2603
- tr.appendChild(td(a.displayName || a.providerId));
2604
- tr.appendChild(td(a.kind));
2605
- var st = document.createElement('td');
2606
- var ok = a.credentialStatus && a.credentialStatus.ok;
2607
- var pill = document.createElement('span');
2608
- pill.className = 'pill ' + (ok ? 'ok' : 'bad');
2609
- pill.textContent = ok ? 'ok' : ((a.credentialStatus && a.credentialStatus.reason) || 'no credential');
2610
- st.appendChild(pill);
2611
- tr.appendChild(st);
2612
- var act = document.createElement('td');
2613
- act.appendChild(btn('Clear', 'danger', async function () {
2614
- if (window.confirm('Clear ' + a.providerId + ' token?')) {
2615
- await api('DELETE', 'accounts/' + encodeURIComponent(a.providerId));
2616
- loadAccounts();
2617
- }
2618
- }));
2619
- tr.appendChild(act);
2620
- body.appendChild(tr);
2621
- });
2622
- renderProviderAccounts(r.json && r.json.providerAccounts || {});
2927
+ }
2928
+ async function handleUiStatic(req, res, urlPath, uiDist) {
2929
+ if (urlPath !== "/ui" && !urlPath.startsWith("/ui/")) return false;
2930
+ if (req.method !== "GET" && req.method !== "HEAD") {
2931
+ res.writeHead(405, { "Content-Type": "application/json" });
2932
+ res.end(JSON.stringify({ error: { type: "method_not_allowed", message: "GET/HEAD only" } }));
2933
+ return true;
2623
2934
  }
2624
-
2625
- // Per-provider sanitized accounts (multi-account). Secrets IN-never-OUT: this
2626
- // view shows id/label/status/active only — set-active + delete are STATUS-ONLY.
2627
- function renderProviderAccounts(byProvider) {
2628
- var body = $('providerAccountsTable').querySelector('tbody');
2629
- clear(body);
2630
- Object.keys(byProvider).forEach(function (provider) {
2631
- (byProvider[provider] || []).forEach(function (acc) {
2632
- var tr = document.createElement('tr');
2633
- tr.appendChild(td(provider));
2634
- tr.appendChild(td(acc.label || acc.id));
2635
- tr.appendChild(td(acc.status));
2636
- tr.appendChild(td(acc.isActive ? 'yes' : ''));
2637
- var act = document.createElement('td');
2638
- if (!acc.isActive) {
2639
- act.appendChild(btn('Set active', '', async function () {
2640
- await api('PUT', 'accounts/' + encodeURIComponent(provider) + '/active', { id: acc.id });
2641
- loadAccounts();
2642
- }));
2935
+ if (!uiDist) {
2936
+ res.writeHead(404, { "Content-Type": "application/json" });
2937
+ res.end(
2938
+ JSON.stringify({
2939
+ error: {
2940
+ type: "ui_not_installed",
2941
+ message: "Control Panel UI not installed (@omnicross/ui has no built dist). Install/build @omnicross/ui or set OMNICROSS_UI_DIST."
2643
2942
  }
2644
- act.appendChild(btn('Delete', 'danger', async function () {
2645
- if (window.confirm('Delete account ' + (acc.label || acc.id) + '?')) {
2646
- await api('DELETE', 'accounts/' + encodeURIComponent(provider) + '/' + encodeURIComponent(acc.id));
2647
- loadAccounts();
2648
- }
2649
- }));
2650
- tr.appendChild(act);
2651
- body.appendChild(tr);
2652
- });
2653
- });
2654
- }
2655
-
2656
- async function saveAccount() {
2657
- $('acErr').textContent = '';
2658
- var provider = $('acProvider').value;
2659
- var raw = $('acBody').value.trim();
2660
- if (!raw) { $('acErr').textContent = 'paste a token JSON'; return; }
2661
- var payload; try { payload = JSON.parse(raw); } catch (e) { $('acErr').textContent = 'invalid JSON'; return; }
2662
- var r = await api('PUT', 'accounts/' + encodeURIComponent(provider), payload);
2663
- if (r.status >= 400) { $('acErr').textContent = (r.json && r.json.error && r.json.error.message) || ('error ' + r.status); return; }
2664
- $('acBody').value = ''; // never persist/echo the just-saved token
2665
- loadAccounts();
2666
- }
2667
-
2668
- // ── Playground ──────────────────────────────────────────────────────────────
2669
- async function sendPlayground() {
2670
- var pre = $('plResponse');
2671
- pre.classList.remove('muted');
2672
- pre.textContent = 'sending…';
2673
- var bodyText = $('plBody').value;
2674
- var parsed; try { parsed = JSON.parse(bodyText); } catch (e) { parsed = bodyText; }
2675
- var r = await fetch('/admin/api/playground', {
2676
- method: 'POST',
2677
- headers: headers({ 'Content-Type': 'application/json' }),
2678
- body: JSON.stringify({ endpoint: $('plEndpoint').value, key: $('plKey').value, body: parsed }),
2679
- });
2680
- var text = await r.text();
2681
- pre.textContent = '[' + r.status + ']\n' + text;
2943
+ })
2944
+ );
2945
+ return true;
2682
2946
  }
2683
-
2684
- function wire() {
2685
- $('pSave').onclick = saveProvider;
2686
- $('pPreset').onchange = onPresetChange;
2687
- $('kCreate').onclick = createKey;
2688
- $('sSave').onclick = saveServer;
2689
- $('acSave').onclick = saveAccount;
2690
- $('plSend').onclick = sendPlayground;
2691
- refresh();
2947
+ if (urlPath === "/ui") {
2948
+ res.writeHead(302, { Location: "/ui/" });
2949
+ res.end();
2950
+ return true;
2692
2951
  }
2693
-
2694
- function refresh() {
2695
- loadStatus(); loadProviders(); loadPresets(); loadKeys(); loadServer(); loadAccounts();
2952
+ let rel;
2953
+ try {
2954
+ rel = decodeURIComponent(urlPath.slice("/ui/".length));
2955
+ } catch {
2956
+ res.writeHead(400, { "Content-Type": "application/json" });
2957
+ res.end(JSON.stringify({ error: { type: "bad_request", message: "malformed path" } }));
2958
+ return true;
2959
+ }
2960
+ if (rel.includes("\\") || rel.includes("\0")) {
2961
+ res.writeHead(400, { "Content-Type": "application/json" });
2962
+ res.end(JSON.stringify({ error: { type: "bad_request", message: "invalid path" } }));
2963
+ return true;
2964
+ }
2965
+ const filePath = import_node_path4.default.resolve(uiDist, rel === "" ? "index.html" : rel);
2966
+ if (filePath !== uiDist && !filePath.startsWith(uiDist + import_node_path4.default.sep)) {
2967
+ res.writeHead(403, { "Content-Type": "application/json" });
2968
+ res.end(JSON.stringify({ error: { type: "forbidden", message: "path outside ui root" } }));
2969
+ return true;
2970
+ }
2971
+ let target = filePath;
2972
+ if (!(0, import_node_fs6.existsSync)(target) || (0, import_node_fs6.statSync)(target).isDirectory()) {
2973
+ if (import_node_path4.default.extname(rel) === "") {
2974
+ target = import_node_path4.default.join(uiDist, "index.html");
2975
+ } else {
2976
+ res.writeHead(404, { "Content-Type": "application/json" });
2977
+ res.end(JSON.stringify({ error: { type: "not_found", message: "no such ui asset" } }));
2978
+ return true;
2979
+ }
2696
2980
  }
2697
-
2698
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire);
2699
- else wire();
2700
- })();
2701
- `;
2702
-
2703
- // src/admin/html.ts
2704
- var STYLE = `
2705
- :root { --bg:#0f1115; --panel:#171a21; --line:#272b35; --fg:#e6e8ec; --muted:#8b91a0; --accent:#5b8cff; --danger:#ff5b6e; --ok:#3ecf8e; }
2706
- * { box-sizing: border-box; }
2707
- body { margin:0; font:14px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif; background:var(--bg); color:var(--fg); }
2708
- header { padding:14px 20px; border-bottom:1px solid var(--line); display:flex; align-items:center; gap:12px; }
2709
- header h1 { font-size:16px; margin:0; font-weight:600; }
2710
- header .badge { font-size:12px; color:var(--muted); }
2711
- main { padding:20px; display:grid; gap:20px; max-width:980px; margin:0 auto; }
2712
- section { background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:16px; }
2713
- section h2 { font-size:14px; margin:0 0 12px; font-weight:600; }
2714
- table { width:100%; border-collapse:collapse; font-size:13px; }
2715
- th, td { text-align:left; padding:6px 8px; border-bottom:1px solid var(--line); }
2716
- th { color:var(--muted); font-weight:500; }
2717
- input, select, textarea { background:var(--bg); color:var(--fg); border:1px solid var(--line); border-radius:6px; padding:6px 8px; font:inherit; }
2718
- textarea { width:100%; min-height:90px; resize:vertical; font-family:ui-monospace,Menlo,monospace; }
2719
- button { background:var(--accent); color:#fff; border:0; border-radius:6px; padding:6px 12px; cursor:pointer; font:inherit; }
2720
- button.secondary { background:#2a2f3a; }
2721
- button.danger { background:var(--danger); }
2722
- .row { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:8px; }
2723
- .muted { color:var(--muted); }
2724
- .mono { font-family:ui-monospace,Menlo,monospace; }
2725
- .pill { padding:1px 8px; border-radius:999px; font-size:12px; }
2726
- .pill.ok { background:rgba(62,207,142,.15); color:var(--ok); }
2727
- .pill.bad { background:rgba(255,91,110,.15); color:var(--danger); }
2728
- pre { background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:10px; overflow:auto; max-height:320px; white-space:pre-wrap; }
2729
- .modal-bg { position:fixed; inset:0; background:rgba(0,0,0,.6); display:none; align-items:center; justify-content:center; }
2730
- .modal-bg.show { display:flex; }
2731
- .modal { background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:20px; max-width:520px; width:90%; }
2732
- .warn { color:var(--danger); font-size:13px; margin:8px 0; }
2733
- .err { color:var(--danger); font-size:13px; }
2734
- `;
2735
- var BODY = `
2736
- <header>
2737
- <h1>omnicross daemon dashboard</h1>
2738
- <span class="badge" id="statusBadge">connecting\u2026</span>
2739
- </header>
2740
- <main>
2741
- <section id="statusSection">
2742
- <h2>Runtime status</h2>
2743
- <div id="statusBody" class="muted">loading\u2026</div>
2744
- </section>
2745
-
2746
- <section>
2747
- <h2>Providers</h2>
2748
- <table id="providersTable"><thead><tr><th>id</th><th>format</th><th>base URL</th><th>key</th><th></th></tr></thead><tbody></tbody></table>
2749
- <div class="row">
2750
- <select id="pPreset"><option value="">-- \u9009\u62E9\u9884\u7F6E --</option></select>
2751
- <input id="pId" placeholder="id" size="10" />
2752
- <select id="pFormat"><option value="openai">openai</option><option value="anthropic">anthropic</option><option value="gemini">gemini</option></select>
2753
- <input id="pBase" placeholder="base URL" size="26" />
2754
- <input id="pKey" placeholder="apiKey (blank = keep on edit)" size="22" />
2755
- <button id="pSave">Save provider</button>
2756
- </div>
2757
- <div class="err" id="pErr"></div>
2758
- <div id="poolPanel" style="display:none; margin-top:12px;">
2759
- <h2 id="poolTitle" style="font-size:13px;">API key pool</h2>
2760
- <p class="muted" style="margin:0 0 8px;">Read-only. Multi-key is <b>cold-standby + observable</b> in v1: outbound failover does not yet rotate keys (null-session boundary \u2014 pending the core seam). Keys are masked; edit the pool via the provider's <span class="mono">apiKeys</span>.</p>
2761
- <table id="poolTable"><thead><tr><th>id</th><th>label</th><th>key</th><th>enabled</th><th>weight</th><th>health</th></tr></thead><tbody></tbody></table>
2762
- </div>
2763
- </section>
2764
-
2765
- <section>
2766
- <h2>Named keys</h2>
2767
- <table id="keysTable"><thead><tr><th>name</th><th>prefix</th><th>enabled</th><th>revoked</th><th></th></tr></thead><tbody></tbody></table>
2768
- <div class="row">
2769
- <input id="kName" placeholder="key name" size="16" />
2770
- <button id="kCreate">Create key</button>
2771
- </div>
2772
- </section>
2773
-
2774
- <section>
2775
- <h2>Server config</h2>
2776
- <div class="row">
2777
- <label><input type="checkbox" id="sEnabled" /> enabled</label>
2778
- <label><input type="checkbox" id="sLan" /> networkBinding (LAN)</label>
2779
- <label>port <input id="sPort" size="6" /></label>
2780
- <button id="sSave">Apply server config</button>
2781
- </div>
2782
- <table id="endpointsTable"><thead><tr><th>endpoint</th><th>defaultModel</th><th>subscription</th></tr></thead><tbody></tbody></table>
2783
- </section>
2784
-
2785
- <section>
2786
- <h2>Accounts <span class="muted">(subscription tokens)</span></h2>
2787
- <table id="accountsTable"><thead><tr><th>provider</th><th>kind</th><th>status</th><th></th></tr></thead><tbody></tbody></table>
2788
- <h3>Per-provider accounts <span class="muted">(multi-account \u2014 sanitized, no tokens)</span></h3>
2789
- <table id="providerAccountsTable"><thead><tr><th>provider</th><th>label</th><th>status</th><th>active</th><th></th></tr></thead><tbody></tbody></table>
2790
- <div class="row">
2791
- <select id="acProvider"><option value="claude">claude</option><option value="codex">codex</option><option value="gemini">gemini</option><option value="opencodego">opencodego</option></select>
2792
- <button id="acSave">Save token</button>
2793
- </div>
2794
- <textarea id="acBody" placeholder='{"authMethod":"oauth","status":"authorized","accessToken":"\u2026","refreshToken":"\u2026"}'></textarea>
2795
- <p class="muted">Write-only: paste a token JSON to authorize this provider. The token is shown only on entry \u2014 it is never read back or displayed. Stored as plain JSON in <span class="mono">tokens.json</span>.</p>
2796
- <div class="err" id="acErr"></div>
2797
- </section>
2798
-
2799
- <section>
2800
- <h2>Playground</h2>
2801
- <div class="row">
2802
- <select id="plEndpoint"><option value="chat">chat</option><option value="responses">responses</option><option value="messages">messages</option><option value="gemini">gemini</option></select>
2803
- <input id="plKey" placeholder="named key (sk-omnicross-\u2026)" size="30" />
2804
- <button id="plSend">Send</button>
2805
- </div>
2806
- <textarea id="plBody">{"model":"","messages":[{"role":"user","content":"ping"}]}</textarea>
2807
- <pre id="plResponse" class="muted">response will appear here</pre>
2808
- </section>
2809
- </main>
2810
-
2811
- <div class="modal-bg" id="keyModalBg">
2812
- <div class="modal">
2813
- <h2>Key created</h2>
2814
- <p class="warn">This secret is shown ONCE. Copy it now \u2014 it cannot be retrieved again.</p>
2815
- <pre id="keyPlaintext" class="mono"></pre>
2816
- <div class="row">
2817
- <button id="keyCopy">Copy</button>
2818
- <button class="secondary" id="keyClose">Close</button>
2819
- </div>
2820
- </div>
2821
- </div>
2822
- `;
2823
- var DASHBOARD_HTML = `<!doctype html>
2824
- <html lang="en">
2825
- <head>
2826
- <meta charset="utf-8" />
2827
- <meta name="viewport" content="width=device-width, initial-scale=1" />
2828
- <title>omnicross daemon dashboard</title>
2829
- <style>${STYLE}</style>
2830
- </head>
2831
- <body>
2832
- ${BODY}
2833
- <script>${DASHBOARD_JS}</script>
2834
- </body>
2835
- </html>`;
2981
+ const body = await (0, import_promises.readFile)(target);
2982
+ const type = CONTENT_TYPES[import_node_path4.default.extname(target).toLowerCase()] ?? "application/octet-stream";
2983
+ res.writeHead(200, { "Content-Type": type, "Content-Length": body.length });
2984
+ res.end(req.method === "HEAD" ? void 0 : body);
2985
+ return true;
2986
+ }
2836
2987
 
2837
2988
  // src/admin/AdminServer.ts
2838
2989
  var LOOPBACK_ADDR = "127.0.0.1";
2839
2990
  var LAN_ADDR = "0.0.0.0";
2991
+ var DAEMON_VERSION = true ? "0.1.2" : "0.0.0-dev";
2840
2992
  var AdminServer = class {
2841
2993
  constructor(deps) {
2842
2994
  this.deps = deps;
@@ -2845,6 +2997,8 @@ var AdminServer = class {
2845
2997
  server = null;
2846
2998
  boundPort = 0;
2847
2999
  boundAddr = LOOPBACK_ADDR;
3000
+ /** Control Panel dist dir (resolved once at first request; null = no UI). */
3001
+ uiDist;
2848
3002
  /**
2849
3003
  * Start the admin listener honoring the resolved admin config. Returns the
2850
3004
  * actual bound port, or `0` when it refuses/declines to bind (disabled or the
@@ -2873,13 +3027,13 @@ var AdminServer = class {
2873
3027
  const server = import_node_http2.default.createServer((req, res) => {
2874
3028
  this.onRequest(req, res);
2875
3029
  });
2876
- const onError = (err4) => {
2877
- if (err4.code === "EADDRINUSE" && port !== 0) {
3030
+ const onError = (err5) => {
3031
+ if (err5.code === "EADDRINUSE" && port !== 0) {
2878
3032
  server.removeListener("error", onError);
2879
3033
  this.listen(bindAddr, 0).then(resolve, reject);
2880
3034
  return;
2881
3035
  }
2882
- reject(err4);
3036
+ reject(err5);
2883
3037
  };
2884
3038
  server.on("error", onError);
2885
3039
  server.listen(port, bindAddr, () => {
@@ -2897,8 +3051,8 @@ var AdminServer = class {
2897
3051
  }
2898
3052
  /** Per-request handler: auth gate (when a token is set) → routing. */
2899
3053
  onRequest(req, res) {
2900
- void this.dispatch(req, res).catch((err4) => {
2901
- const message = err4 instanceof Error ? err4.message : String(err4);
3054
+ void this.dispatch(req, res).catch((err5) => {
3055
+ const message = err5 instanceof Error ? err5.message : String(err5);
2902
3056
  console.error("[AdminServer] unhandled error:", message);
2903
3057
  if (!res.headersSent) {
2904
3058
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -2908,22 +3062,26 @@ var AdminServer = class {
2908
3062
  }
2909
3063
  async dispatch(req, res) {
2910
3064
  const cfg = this.deps.getAdminConfig();
3065
+ res.setHeader("x-omnicross-daemon", DAEMON_VERSION);
3066
+ res.setHeader("x-omnicross-pid", String(process.pid));
2911
3067
  if (cfg.token && !this.isAuthorized(req, cfg.token)) {
2912
3068
  res.writeHead(401, { "Content-Type": "application/json" });
2913
3069
  res.end(JSON.stringify({ error: { type: "unauthorized", message: "admin token required" } }));
2914
3070
  return;
2915
3071
  }
2916
3072
  const url = req.url ?? "/";
2917
- const path = url.split("?")[0];
2918
- if ((req.method === "GET" || req.method === "HEAD") && (path === "/" || path === "/admin")) {
2919
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2920
- res.end(req.method === "HEAD" ? void 0 : DASHBOARD_HTML);
3073
+ const path2 = url.split("?")[0];
3074
+ if ((req.method === "GET" || req.method === "HEAD") && (path2 === "/" || path2 === "/admin")) {
3075
+ res.writeHead(302, { Location: "/ui/" });
3076
+ res.end();
2921
3077
  return;
2922
3078
  }
2923
- if (path.startsWith("/admin/api/")) {
2924
- await handleAdminApi(req, res, path, this.deps);
3079
+ if (path2.startsWith("/admin/api/")) {
3080
+ await handleAdminApi(req, res, path2, this.deps);
2925
3081
  return;
2926
3082
  }
3083
+ if (this.uiDist === void 0) this.uiDist = resolveUiDist();
3084
+ if (await handleUiStatic(req, res, path2, this.uiDist)) return;
2927
3085
  res.writeHead(404, { "Content-Type": "application/json" });
2928
3086
  res.end(JSON.stringify({ error: { type: "not_found", message: "no such admin route" } }));
2929
3087
  }
@@ -2958,11 +3116,11 @@ function constantTimeEquals(a, b) {
2958
3116
  const bufA = Buffer.from(a, "utf8");
2959
3117
  const bufB = Buffer.from(b, "utf8");
2960
3118
  if (bufA.length !== bufB.length) return false;
2961
- return (0, import_node_crypto6.timingSafeEqual)(bufA, bufB);
3119
+ return (0, import_node_crypto7.timingSafeEqual)(bufA, bufB);
2962
3120
  }
2963
3121
 
2964
3122
  // src/admin/oauthSessions.ts
2965
- var import_node_crypto7 = __toESM(require("crypto"), 1);
3123
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
2966
3124
  var DEFAULT_OAUTH_SESSION_TTL_MS = 10 * 60 * 1e3;
2967
3125
  var OAuthSessionStore = class {
2968
3126
  constructor(ttlMs = DEFAULT_OAUTH_SESSION_TTL_MS) {
@@ -2976,7 +3134,7 @@ var OAuthSessionStore = class {
2976
3134
  */
2977
3135
  put(session) {
2978
3136
  this.sweep();
2979
- const sessionId = import_node_crypto7.default.randomBytes(24).toString("base64url");
3137
+ const sessionId = import_node_crypto8.default.randomBytes(24).toString("base64url");
2980
3138
  this.sessions.set(sessionId, { ...session, createdAt: Date.now() });
2981
3139
  return sessionId;
2982
3140
  }
@@ -3046,18 +3204,18 @@ function awaitLoopbackCode(expectedState, timeoutMs = DEFAULT_TIMEOUT_MS) {
3046
3204
  res.end(pageHtml("Login complete."));
3047
3205
  finish(server, () => resolve(code));
3048
3206
  });
3049
- server.on("error", (err4) => {
3207
+ server.on("error", (err5) => {
3050
3208
  if (settled) return;
3051
3209
  settled = true;
3052
3210
  clearTimeout(timer);
3053
- if (err4.code === "EADDRINUSE") {
3211
+ if (err5.code === "EADDRINUSE") {
3054
3212
  reject(
3055
3213
  new Error(
3056
3214
  `login: cannot bind ${LOOPBACK_HOST}:${LOOPBACK_PORT} (address in use) \u2014 another codex login or process is holding the port`
3057
3215
  )
3058
3216
  );
3059
3217
  } else {
3060
- reject(err4);
3218
+ reject(err5);
3061
3219
  }
3062
3220
  });
3063
3221
  const timer = setTimeout(() => {
@@ -3311,7 +3469,7 @@ var ConsoleLogger = class {
3311
3469
  };
3312
3470
 
3313
3471
  // src/ports/JsonApiServerSettingsStore.ts
3314
- var import_node_fs5 = require("fs");
3472
+ var import_node_fs7 = require("fs");
3315
3473
  var import_outbound_api3 = require("@omnicross/core/outbound-api");
3316
3474
  var JsonApiServerSettingsStore = class {
3317
3475
  constructor(configPath) {
@@ -3327,12 +3485,12 @@ var JsonApiServerSettingsStore = class {
3327
3485
  if (key !== import_outbound_api3.OUTBOUND_API_SERVER_CONFIG_KEY) return;
3328
3486
  const file = this.readFile();
3329
3487
  file.server = value;
3330
- (0, import_node_fs5.writeFileSync)(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
3488
+ (0, import_node_fs7.writeFileSync)(this.configPath, JSON.stringify(file, null, 2) + "\n", "utf8");
3331
3489
  }
3332
3490
  /** Read the config.json, tolerating a missing/corrupt file (→ empty shape). */
3333
3491
  readFile() {
3334
3492
  try {
3335
- const raw = (0, import_node_fs5.readFileSync)(this.configPath, "utf8");
3493
+ const raw = (0, import_node_fs7.readFileSync)(this.configPath, "utf8");
3336
3494
  const parsed = JSON.parse(raw);
3337
3495
  if (parsed && typeof parsed === "object") return parsed;
3338
3496
  } catch {
@@ -3341,10 +3499,556 @@ var JsonApiServerSettingsStore = class {
3341
3499
  }
3342
3500
  };
3343
3501
 
3502
+ // src/ports/JsonlUsageEventStore.ts
3503
+ var import_node_crypto9 = require("crypto");
3504
+ var import_node_fs8 = require("fs");
3505
+ var JsonlUsageEventStore = class {
3506
+ constructor(eventsPath, isPriced) {
3507
+ this.eventsPath = eventsPath;
3508
+ this.isPriced = isPriced;
3509
+ }
3510
+ eventsPath;
3511
+ isPriced;
3512
+ /** Persist one event: assign `id`, stamp `ts` when absent, append ONE line. */
3513
+ async insert(input) {
3514
+ const row = {
3515
+ ...input,
3516
+ id: (0, import_node_crypto9.randomUUID)(),
3517
+ ts: input.ts ?? Date.now()
3518
+ };
3519
+ (0, import_node_fs8.appendFileSync)(this.eventsPath, JSON.stringify(row) + "\n", "utf8");
3520
+ return row.id;
3521
+ }
3522
+ async getTotals(range) {
3523
+ const totals = {
3524
+ inputTokens: 0,
3525
+ outputTokens: 0,
3526
+ cacheReadTokens: 0,
3527
+ cacheCreationTokens: 0,
3528
+ reasoningTokens: 0,
3529
+ costUsd: 0,
3530
+ costSavedByCacheUsd: 0,
3531
+ eventCount: 0
3532
+ };
3533
+ for (const row of this.readRows(range)) {
3534
+ totals.inputTokens += row.inputTokens;
3535
+ totals.outputTokens += row.outputTokens;
3536
+ totals.cacheReadTokens += row.cacheReadTokens;
3537
+ totals.cacheCreationTokens += row.cacheCreationTokens;
3538
+ totals.reasoningTokens += row.reasoningTokens;
3539
+ totals.costUsd += row.costUsd;
3540
+ totals.costSavedByCacheUsd += row.costSavedByCacheUsd;
3541
+ totals.eventCount += 1;
3542
+ }
3543
+ return totals;
3544
+ }
3545
+ async getByModel(range) {
3546
+ const groups = /* @__PURE__ */ new Map();
3547
+ for (const row of this.readRows(range)) {
3548
+ const key = `${row.providerId}::${row.model}`;
3549
+ let g = groups.get(key);
3550
+ if (!g) {
3551
+ g = {
3552
+ providerId: row.providerId,
3553
+ model: row.model,
3554
+ eventCount: 0,
3555
+ inputTokens: 0,
3556
+ outputTokens: 0,
3557
+ cacheReadTokens: 0,
3558
+ cacheCreationTokens: 0,
3559
+ costUsd: 0,
3560
+ costSavedByCacheUsd: 0,
3561
+ unpriced: false
3562
+ };
3563
+ groups.set(key, g);
3564
+ }
3565
+ g.eventCount += 1;
3566
+ g.inputTokens += row.inputTokens;
3567
+ g.outputTokens += row.outputTokens;
3568
+ g.cacheReadTokens += row.cacheReadTokens;
3569
+ g.cacheCreationTokens += row.cacheCreationTokens;
3570
+ g.costUsd += row.costUsd;
3571
+ g.costSavedByCacheUsd += row.costSavedByCacheUsd;
3572
+ }
3573
+ const rows = Array.from(groups.values());
3574
+ for (const g of rows) {
3575
+ g.unpriced = !await this.isPriced(g.providerId, g.model);
3576
+ }
3577
+ return rows;
3578
+ }
3579
+ /**
3580
+ * Group by RAW apiKeyId (null forms the unattributed sentinel group). Label
3581
+ * here is the raw id fallback — the admin handler resolves display labels
3582
+ * against the configured pool keys (the store stays config-schema-free).
3583
+ */
3584
+ async getByApiKey(range) {
3585
+ const groups = /* @__PURE__ */ new Map();
3586
+ for (const row of this.readRows(range)) {
3587
+ const key = row.apiKeyId;
3588
+ let g = groups.get(key);
3589
+ if (!g) {
3590
+ g = {
3591
+ apiKeyId: key,
3592
+ label: key ?? "unattributed",
3593
+ providerId: key === null ? null : row.providerId,
3594
+ eventCount: 0,
3595
+ inputTokens: 0,
3596
+ outputTokens: 0,
3597
+ costUsd: 0
3598
+ };
3599
+ groups.set(key, g);
3600
+ }
3601
+ g.eventCount += 1;
3602
+ g.inputTokens += row.inputTokens;
3603
+ g.outputTokens += row.outputTokens;
3604
+ g.costUsd += row.costUsd;
3605
+ }
3606
+ return Array.from(groups.values());
3607
+ }
3608
+ async getMessagesForSession(sessionId) {
3609
+ return this.readAllRows().filter((r) => r.sessionId === sessionId).sort((a, b) => a.ts - b.ts).map((r) => ({
3610
+ id: r.id,
3611
+ ts: r.ts,
3612
+ messageId: r.messageId,
3613
+ parentMessageId: r.parentMessageId,
3614
+ sessionId: r.sessionId,
3615
+ providerId: r.providerId,
3616
+ model: r.model,
3617
+ apiKeyId: r.apiKeyId,
3618
+ engineOrigin: r.engineOrigin,
3619
+ inputTokens: r.inputTokens,
3620
+ outputTokens: r.outputTokens,
3621
+ cacheReadTokens: r.cacheReadTokens,
3622
+ cacheCreationTokens: r.cacheCreationTokens,
3623
+ reasoningTokens: r.reasoningTokens,
3624
+ costUsd: r.costUsd,
3625
+ costSavedByCacheUsd: r.costSavedByCacheUsd
3626
+ }));
3627
+ }
3628
+ async getSessionCacheStats(sessionId) {
3629
+ const stats = {
3630
+ sessionId,
3631
+ inputTokens: 0,
3632
+ cacheReadTokens: 0,
3633
+ cacheCreationTokens: 0,
3634
+ outputTokens: 0,
3635
+ eventCount: 0,
3636
+ hitRate: 0
3637
+ };
3638
+ for (const r of this.readAllRows()) {
3639
+ if (r.sessionId !== sessionId) continue;
3640
+ stats.inputTokens += r.inputTokens;
3641
+ stats.cacheReadTokens += r.cacheReadTokens;
3642
+ stats.cacheCreationTokens += r.cacheCreationTokens;
3643
+ stats.outputTokens += r.outputTokens;
3644
+ stats.eventCount += 1;
3645
+ }
3646
+ const promptSide = stats.inputTokens + stats.cacheReadTokens + stats.cacheCreationTokens;
3647
+ stats.hitRate = promptSide > 0 ? stats.cacheReadTokens / promptSide : 0;
3648
+ return stats;
3649
+ }
3650
+ /** Rows inside `startTs <= ts < endTs` (endTs EXCLUSIVE). */
3651
+ readRows(range) {
3652
+ return this.readAllRows().filter((r) => r.ts >= range.startTs && r.ts < range.endTs);
3653
+ }
3654
+ /** Parse every line, skipping malformed/torn lines defensively. */
3655
+ readAllRows() {
3656
+ if (!(0, import_node_fs8.existsSync)(this.eventsPath)) return [];
3657
+ let raw;
3658
+ try {
3659
+ raw = (0, import_node_fs8.readFileSync)(this.eventsPath, "utf8");
3660
+ } catch {
3661
+ return [];
3662
+ }
3663
+ const rows = [];
3664
+ for (const line of raw.split("\n")) {
3665
+ const trimmed = line.trim();
3666
+ if (!trimmed) continue;
3667
+ try {
3668
+ const parsed = JSON.parse(trimmed);
3669
+ if (isUsageEventRecord(parsed)) rows.push(parsed);
3670
+ } catch {
3671
+ }
3672
+ }
3673
+ return rows;
3674
+ }
3675
+ };
3676
+ var NUMERIC_FIELDS = [
3677
+ "ts",
3678
+ "inputTokens",
3679
+ "outputTokens",
3680
+ "cacheReadTokens",
3681
+ "cacheCreationTokens",
3682
+ "reasoningTokens",
3683
+ "costUsd",
3684
+ "costSavedByCacheUsd"
3685
+ ];
3686
+ var NULLABLE_STRING_FIELDS = ["messageId", "parentMessageId", "sessionId", "apiKeyId"];
3687
+ var isStringOrNull = (v) => v === null || typeof v === "string";
3688
+ function isUsageEventRecord(parsed) {
3689
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
3690
+ const r = parsed;
3691
+ if (typeof r["id"] !== "string") return false;
3692
+ if (typeof r["providerId"] !== "string") return false;
3693
+ if (typeof r["model"] !== "string") return false;
3694
+ if (typeof r["engineOrigin"] !== "string") return false;
3695
+ for (const f of NULLABLE_STRING_FIELDS) {
3696
+ if (!isStringOrNull(r[f])) return false;
3697
+ }
3698
+ for (const f of NUMERIC_FIELDS) {
3699
+ const v = r[f];
3700
+ if (typeof v !== "number" || !Number.isFinite(v)) return false;
3701
+ }
3702
+ return true;
3703
+ }
3704
+
3705
+ // src/ports/JsonPricingStore.ts
3706
+ var import_node_fs9 = require("fs");
3707
+ var JsonPricingStore = class {
3708
+ constructor(pricingPath) {
3709
+ this.pricingPath = pricingPath;
3710
+ }
3711
+ pricingPath;
3712
+ async getAll() {
3713
+ return this.readRows();
3714
+ }
3715
+ /**
3716
+ * Insert or update one row keyed (providerId, modelId). `asUserEdit` stamps
3717
+ * user provenance (source 'user', userEdited, editedAt now) so the row is
3718
+ * protected from auto-overwrite during source refreshes; a non-user upsert
3719
+ * stamps source 'litellm' and clears nothing it should not (a plain source
3720
+ * upsert through this method overwrites the row wholesale).
3721
+ */
3722
+ async upsert(input, asUserEdit) {
3723
+ const rows = this.readRows();
3724
+ const entry = this.applyUpsert(rows, input, asUserEdit);
3725
+ this.writeRows(rows);
3726
+ return entry;
3727
+ }
3728
+ /**
3729
+ * Apply a batch fetched from a pricing source. Rows whose local copy is
3730
+ * user-edited are NOT applied — they come back as `{ current, incoming }`
3731
+ * conflicts; everything else is upserted (source 'litellm'). ONE file write
3732
+ * for the whole batch.
3733
+ */
3734
+ async bulkApplyFromSource(entries) {
3735
+ const rows = this.readRows();
3736
+ const applied = [];
3737
+ const conflicts = [];
3738
+ for (const incoming of entries) {
3739
+ const current = rows.find(
3740
+ (r) => r.providerId === incoming.providerId && r.modelId === incoming.modelId
3741
+ );
3742
+ if (current && current.userEdited) {
3743
+ conflicts.push({ current, incoming });
3744
+ continue;
3745
+ }
3746
+ applied.push(this.applyUpsert(
3747
+ rows,
3748
+ incoming,
3749
+ /* asUserEdit */
3750
+ false
3751
+ ));
3752
+ }
3753
+ if (applied.length > 0) this.writeRows(rows);
3754
+ return { applied, conflicts };
3755
+ }
3756
+ /**
3757
+ * Apply per-row conflict decisions: 'overwrite' replaces the local row with
3758
+ * the incoming values (clearing the user-edited mark), 'skip' counts only.
3759
+ */
3760
+ async applyResolutions(resolutions) {
3761
+ const rows = this.readRows();
3762
+ let overwrittenCount = 0;
3763
+ let skippedCount = 0;
3764
+ for (const r of resolutions) {
3765
+ if (r.action === "skip") {
3766
+ skippedCount += 1;
3767
+ continue;
3768
+ }
3769
+ this.applyUpsert(
3770
+ rows,
3771
+ r.incoming,
3772
+ /* asUserEdit */
3773
+ false
3774
+ );
3775
+ overwrittenCount += 1;
3776
+ }
3777
+ if (overwrittenCount > 0) this.writeRows(rows);
3778
+ return { overwrittenCount, skippedCount };
3779
+ }
3780
+ /**
3781
+ * STORE-LOCAL (not on the core port): remove one row. Returns whether a row
3782
+ * was actually removed. The admin DELETE handler calls this then invalidates
3783
+ * the engine cache.
3784
+ */
3785
+ async delete(providerId, modelId) {
3786
+ const rows = this.readRows();
3787
+ const idx = rows.findIndex((r) => r.providerId === providerId && r.modelId === modelId);
3788
+ if (idx < 0) return false;
3789
+ rows.splice(idx, 1);
3790
+ this.writeRows(rows);
3791
+ return true;
3792
+ }
3793
+ /** Upsert into `rows` IN PLACE (no write) and return the resulting entry. */
3794
+ applyUpsert(rows, input, asUserEdit) {
3795
+ const now = Date.now();
3796
+ const entry = {
3797
+ providerId: input.providerId,
3798
+ modelId: input.modelId,
3799
+ inputPricePer1m: input.inputPricePer1m,
3800
+ outputPricePer1m: input.outputPricePer1m,
3801
+ cacheReadPricePer1m: input.cacheReadPricePer1m ?? null,
3802
+ cacheWritePricePer1m: input.cacheWritePricePer1m ?? null,
3803
+ source: asUserEdit ? "user" : "litellm",
3804
+ userEdited: asUserEdit,
3805
+ editedAt: asUserEdit ? now : null,
3806
+ updatedAt: now
3807
+ };
3808
+ const idx = rows.findIndex(
3809
+ (r) => r.providerId === input.providerId && r.modelId === input.modelId
3810
+ );
3811
+ if (idx >= 0) rows[idx] = entry;
3812
+ else rows.push(entry);
3813
+ return entry;
3814
+ }
3815
+ /** Read the pricing rows, tolerating a missing/corrupt file (→ empty list). */
3816
+ readRows() {
3817
+ if (!(0, import_node_fs9.existsSync)(this.pricingPath)) return [];
3818
+ try {
3819
+ const parsed = JSON.parse((0, import_node_fs9.readFileSync)(this.pricingPath, "utf8"));
3820
+ return Array.isArray(parsed) ? parsed : [];
3821
+ } catch {
3822
+ return [];
3823
+ }
3824
+ }
3825
+ writeRows(rows) {
3826
+ (0, import_node_fs9.writeFileSync)(this.pricingPath, JSON.stringify(rows, null, 2) + "\n", "utf8");
3827
+ }
3828
+ };
3829
+
3344
3830
  // src/ports/JsonSubscriptionCredentialStore.ts
3345
- var import_node_fs6 = require("fs");
3346
- var import_node_path3 = require("path");
3831
+ var import_node_fs12 = require("fs");
3832
+ var import_node_path7 = require("path");
3347
3833
  var import_subscriptions3 = require("@omnicross/subscriptions");
3834
+
3835
+ // src/ports/account-sync.ts
3836
+ var IMPORT_EXPIRY_MARGIN_MS = 6e4;
3837
+ function viewOf(tokens) {
3838
+ return tokens;
3839
+ }
3840
+ function decideExternalImport(captured, external, now = Date.now()) {
3841
+ if (!external?.accessToken) return "no-credential";
3842
+ const capturedRt = viewOf(captured).refreshToken;
3843
+ const hasNewRefresh = Boolean(external.refreshToken && external.refreshToken !== capturedRt);
3844
+ const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > now + IMPORT_EXPIRY_MARGIN_MS : true;
3845
+ return hasNewRefresh || accessStillValid ? "import" : "not-rotated";
3846
+ }
3847
+ function buildImportedTokens(captured, external) {
3848
+ const imported = {
3849
+ ...captured,
3850
+ accessToken: external.accessToken,
3851
+ status: "authorized",
3852
+ errorMessage: void 0,
3853
+ syncWarning: void 0,
3854
+ lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
3855
+ };
3856
+ if (external.refreshToken) imported.refreshToken = external.refreshToken;
3857
+ if (external.expiresAt) imported.expiresAt = external.expiresAt;
3858
+ else delete imported.expiresAt;
3859
+ if (external.idToken) imported.idToken = external.idToken;
3860
+ if (external.scopes) imported.scopes = external.scopes;
3861
+ return imported;
3862
+ }
3863
+ function buildTokensFromExternal(provider, external) {
3864
+ const base = {
3865
+ authMethod: "oauth",
3866
+ status: "authorized",
3867
+ accessToken: external.accessToken,
3868
+ lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
3869
+ };
3870
+ if (provider === "claude") {
3871
+ const tokens2 = { ...base };
3872
+ if (external.refreshToken) tokens2.refreshToken = external.refreshToken;
3873
+ if (external.expiresAt) tokens2.expiresAt = external.expiresAt;
3874
+ if (external.scopes) tokens2.scopes = external.scopes;
3875
+ return tokens2;
3876
+ }
3877
+ const tokens = { ...base };
3878
+ if (external.refreshToken) tokens.refreshToken = external.refreshToken;
3879
+ if (external.expiresAt) tokens.expiresAt = external.expiresAt;
3880
+ if (external.idToken) tokens.idToken = external.idToken;
3881
+ return tokens;
3882
+ }
3883
+ function isExternalDivergent(stored, external) {
3884
+ if (!external?.accessToken || !external.refreshToken) return false;
3885
+ const view = viewOf(stored);
3886
+ if (!view.refreshToken || external.refreshToken === view.refreshToken) return false;
3887
+ const storedExp = view.expiresAt ? Date.parse(view.expiresAt) : NaN;
3888
+ const externalExp = external.expiresAt ? Date.parse(external.expiresAt) : Infinity;
3889
+ return !Number.isFinite(storedExp) || externalExp > storedExp;
3890
+ }
3891
+ function findDuplicateCredentialIds(accounts) {
3892
+ const byCredential = /* @__PURE__ */ new Map();
3893
+ for (const account of accounts) {
3894
+ const view = viewOf(account.tokens);
3895
+ const credential = view.refreshToken ?? view.apiKey ?? view.accessToken;
3896
+ if (!credential) continue;
3897
+ const ids = byCredential.get(credential) ?? [];
3898
+ ids.push(account.id);
3899
+ byCredential.set(credential, ids);
3900
+ }
3901
+ const duplicates = /* @__PURE__ */ new Set();
3902
+ for (const ids of byCredential.values()) {
3903
+ if (ids.length > 1) for (const id of ids) duplicates.add(id);
3904
+ }
3905
+ return duplicates;
3906
+ }
3907
+
3908
+ // src/ports/external-cli-credentials.ts
3909
+ var import_node_fs10 = require("fs");
3910
+ var import_node_os2 = require("os");
3911
+ var import_node_path5 = require("path");
3912
+ function externalStorePath(provider, home = (0, import_node_os2.homedir)()) {
3913
+ return provider === "claude" ? (0, import_node_path5.join)(home, ".claude", ".credentials.json") : (0, import_node_path5.join)(home, ".codex", "auth.json");
3914
+ }
3915
+ function decodeJwtExpiryMs(token) {
3916
+ try {
3917
+ const payload = token.split(".")[1];
3918
+ if (!payload) return void 0;
3919
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
3920
+ if (typeof decoded.exp === "number" && Number.isFinite(decoded.exp)) {
3921
+ return decoded.exp * 1e3;
3922
+ }
3923
+ } catch {
3924
+ }
3925
+ return void 0;
3926
+ }
3927
+ function parseClaudeOAuthEnvelope(raw) {
3928
+ const oauth = raw?.claudeAiOauth;
3929
+ if (!oauth || typeof oauth.accessToken !== "string" || !oauth.accessToken) return null;
3930
+ const parsed = { accessToken: oauth.accessToken };
3931
+ if (typeof oauth.refreshToken === "string" && oauth.refreshToken) {
3932
+ parsed.refreshToken = oauth.refreshToken;
3933
+ }
3934
+ if (typeof oauth.expiresAt === "number" && Number.isFinite(oauth.expiresAt)) {
3935
+ parsed.expiresAt = new Date(oauth.expiresAt).toISOString();
3936
+ }
3937
+ if (Array.isArray(oauth.scopes) && oauth.scopes.every((s) => typeof s === "string")) {
3938
+ parsed.scopes = oauth.scopes;
3939
+ }
3940
+ return parsed;
3941
+ }
3942
+ function parseCodexTokensEnvelope(raw) {
3943
+ const tokens = raw?.tokens;
3944
+ if (!tokens) return null;
3945
+ const accessToken = typeof tokens.access_token === "string" && tokens.access_token ? tokens.access_token : void 0;
3946
+ const idToken = typeof tokens.id_token === "string" && tokens.id_token ? tokens.id_token : void 0;
3947
+ if (!accessToken && !idToken) return null;
3948
+ const parsed = {};
3949
+ if (accessToken) {
3950
+ parsed.accessToken = accessToken;
3951
+ const expMs = decodeJwtExpiryMs(accessToken);
3952
+ if (expMs !== void 0) parsed.expiresAt = new Date(expMs).toISOString();
3953
+ }
3954
+ if (idToken) parsed.idToken = idToken;
3955
+ if (typeof tokens.refresh_token === "string" && tokens.refresh_token) {
3956
+ parsed.refreshToken = tokens.refresh_token;
3957
+ }
3958
+ return parsed;
3959
+ }
3960
+ function readExternalCliCredentials(provider, home = (0, import_node_os2.homedir)()) {
3961
+ const path2 = externalStorePath(provider, home);
3962
+ if (!(0, import_node_fs10.existsSync)(path2)) return null;
3963
+ let raw;
3964
+ try {
3965
+ const parsed = JSON.parse((0, import_node_fs10.readFileSync)(path2, "utf8"));
3966
+ raw = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
3967
+ } catch {
3968
+ return null;
3969
+ }
3970
+ return provider === "claude" ? parseClaudeOAuthEnvelope(raw) : parseCodexTokensEnvelope(raw);
3971
+ }
3972
+
3973
+ // src/ports/external-cli-store.ts
3974
+ var import_node_fs11 = require("fs");
3975
+ var import_node_os3 = require("os");
3976
+ var import_node_path6 = require("path");
3977
+ function markerPath(provider, home) {
3978
+ return `${externalStorePath(provider, home)}.omnicross-managed`;
3979
+ }
3980
+ function backupPath(provider, home) {
3981
+ return `${externalStorePath(provider, home)}.omnicross-backup`;
3982
+ }
3983
+ function buildClaudeOAuthEnvelope(tokens) {
3984
+ if (!tokens.accessToken) return null;
3985
+ const envelope = { accessToken: tokens.accessToken };
3986
+ if (tokens.refreshToken) envelope.refreshToken = tokens.refreshToken;
3987
+ if (tokens.expiresAt) {
3988
+ const ms = Date.parse(tokens.expiresAt);
3989
+ if (Number.isFinite(ms)) envelope.expiresAt = ms;
3990
+ }
3991
+ if (tokens.scopes && tokens.scopes.length > 0) envelope.scopes = tokens.scopes;
3992
+ return envelope;
3993
+ }
3994
+ function buildCodexTokensEnvelope(tokens) {
3995
+ if (!tokens.accessToken && !tokens.idToken) return null;
3996
+ const envelope = { access_token: tokens.accessToken ?? "" };
3997
+ if (tokens.idToken) envelope.id_token = tokens.idToken;
3998
+ if (tokens.refreshToken) envelope.refresh_token = tokens.refreshToken;
3999
+ return envelope;
4000
+ }
4001
+ function readExistingObject(path2) {
4002
+ if (!(0, import_node_fs11.existsSync)(path2)) return {};
4003
+ try {
4004
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
4005
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4006
+ } catch {
4007
+ return {};
4008
+ }
4009
+ }
4010
+ function writeAtomic(path2, content) {
4011
+ (0, import_node_fs11.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
4012
+ const temp = `${path2}.omnicross-tmp`;
4013
+ (0, import_node_fs11.writeFileSync)(temp, content, "utf8");
4014
+ (0, import_node_fs11.renameSync)(temp, path2);
4015
+ }
4016
+ function createExternalCliStore(home = (0, import_node_os3.homedir)()) {
4017
+ return {
4018
+ readMarkerAccountId(provider) {
4019
+ const path2 = markerPath(provider, home);
4020
+ if (!(0, import_node_fs11.existsSync)(path2)) return void 0;
4021
+ try {
4022
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf8"));
4023
+ return typeof parsed.accountId === "string" && parsed.accountId ? parsed.accountId : void 0;
4024
+ } catch {
4025
+ return void 0;
4026
+ }
4027
+ },
4028
+ writeMarker(provider, accountId) {
4029
+ writeAtomic(
4030
+ markerPath(provider, home),
4031
+ JSON.stringify({ accountId, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n"
4032
+ );
4033
+ },
4034
+ writeBack(provider, accountId, tokens) {
4035
+ const owner = this.readMarkerAccountId(provider);
4036
+ if (owner !== accountId) return false;
4037
+ const envelope = provider === "claude" ? buildClaudeOAuthEnvelope(tokens) : buildCodexTokensEnvelope(tokens);
4038
+ if (!envelope) return false;
4039
+ const storePath = externalStorePath(provider, home);
4040
+ if ((0, import_node_fs11.existsSync)(storePath) && !(0, import_node_fs11.existsSync)(backupPath(provider, home))) {
4041
+ (0, import_node_fs11.copyFileSync)(storePath, backupPath(provider, home));
4042
+ }
4043
+ const existing = readExistingObject(storePath);
4044
+ const merged = provider === "claude" ? { ...existing, claudeAiOauth: envelope } : { ...existing, tokens: envelope };
4045
+ writeAtomic(storePath, JSON.stringify(merged, null, 2) + "\n");
4046
+ return true;
4047
+ }
4048
+ };
4049
+ }
4050
+
4051
+ // src/ports/JsonSubscriptionCredentialStore.ts
3348
4052
  var JsonSubscriptionCredentialStore = class {
3349
4053
  /**
3350
4054
  * @param tokensPath on-disk `tokens.json` location.
@@ -3354,14 +4058,33 @@ var JsonSubscriptionCredentialStore = class {
3354
4058
  * is unchanged; tests inject a mock fetch. NOT used by any
3355
4059
  * read/write path — only by `refresh*Token`.
3356
4060
  */
3357
- constructor(tokensPath, box, fetchImpl = (url, init) => fetch(url, init)) {
4061
+ constructor(tokensPath, box, fetchImpl = (url, init) => fetch(url, init), externalCliReader = readExternalCliCredentials, externalCliStore = createExternalCliStore()) {
3358
4062
  this.tokensPath = tokensPath;
3359
4063
  this.box = box;
3360
4064
  this.fetchImpl = fetchImpl;
4065
+ this.externalCliReader = externalCliReader;
4066
+ this.externalCliStore = externalCliStore;
3361
4067
  }
3362
4068
  tokensPath;
3363
4069
  box;
3364
4070
  fetchImpl;
4071
+ externalCliReader;
4072
+ externalCliStore;
4073
+ /**
4074
+ * In-flight refresh coalescing (external-cli-sync). OAuth refresh tokens are
4075
+ * SINGLE-USE: two concurrent refreshes of one account each spend the same
4076
+ * token and the loser bricks a healthy account. Every refresh entry point
4077
+ * (auth-strategy lazy refresh, 401 retry, background scheduler) funnels
4078
+ * through `coalesce`, so overlapping callers share ONE upstream round-trip.
4079
+ */
4080
+ inFlightRefreshes = /* @__PURE__ */ new Map();
4081
+ coalesce(key, task) {
4082
+ const existing = this.inFlightRefreshes.get(key);
4083
+ if (existing) return existing;
4084
+ const run = task().finally(() => this.inFlightRefreshes.delete(key));
4085
+ this.inFlightRefreshes.set(key, run);
4086
+ return run;
4087
+ }
3365
4088
  /** Full parsed account-tokens config (or a minimal `{ updatedAt }` when the
3366
4089
  * file is absent/corrupt). This is the hot read — the codex / gemini auth
3367
4090
  * strategies pull `accessToken` / `expiresAt` / `status` from it. */
@@ -3389,10 +4112,41 @@ var JsonSubscriptionCredentialStore = class {
3389
4112
  const out = {};
3390
4113
  for (const provider of ["claude", "codex", "gemini", "opencodego"]) {
3391
4114
  const sanitized = sanitizeAccounts(config, provider);
3392
- if (sanitized.length > 0) out[provider] = sanitized;
4115
+ if (sanitized.length > 0) out[provider] = this.attachSyncWarnings(config, provider, sanitized);
3393
4116
  }
3394
4117
  return out;
3395
4118
  }
4119
+ /**
4120
+ * List-time credential-conflict warnings (external-cli-sync). Computed, not
4121
+ * persisted: (a) `duplicate-token` when two accounts of one provider share a
4122
+ * credential, (b) `external-divergent` when the external CLI native store has
4123
+ * rotated PAST the ACTIVE account (claude/codex only). A warning persisted by
4124
+ * a failed refresh (`external-not-rotated`) takes precedence — it is the most
4125
+ * actionable state.
4126
+ */
4127
+ attachSyncWarnings(config, provider, sanitized) {
4128
+ const duplicates = findDuplicateCredentialIds(listAccounts(config, provider));
4129
+ let divergentId;
4130
+ if (provider === "claude" || provider === "codex") {
4131
+ const active = getActiveAccount(config, provider);
4132
+ if (active && isExternalDivergent(active.tokens, this.safeReadExternal(provider))) {
4133
+ divergentId = active.id;
4134
+ }
4135
+ }
4136
+ if (duplicates.size === 0 && !divergentId) return sanitized;
4137
+ return sanitized.map((account) => {
4138
+ const computed = account.id === divergentId ? "external-divergent" : duplicates.has(account.id) ? "duplicate-token" : void 0;
4139
+ return { ...account, syncWarning: account.syncWarning ?? computed };
4140
+ });
4141
+ }
4142
+ /** Read the external CLI store, never letting an fs/parse error escape. */
4143
+ safeReadExternal(provider) {
4144
+ try {
4145
+ return this.externalCliReader(provider);
4146
+ } catch {
4147
+ return null;
4148
+ }
4149
+ }
3396
4150
  /**
3397
4151
  * Refresh the Claude OAuth access token (oauth design D4). HONEST `false` when
3398
4152
  * the block has no refresh_token (setup-token / manual) — no upstream call, the
@@ -3402,30 +4156,44 @@ var JsonSubscriptionCredentialStore = class {
3402
4156
  * errorMessage → `false`.
3403
4157
  */
3404
4158
  async refreshClaudeToken() {
3405
- const config = this.readConfig();
3406
- const active = getActiveAccount(config, "claude");
3407
- const claude = active?.tokens;
3408
- if (!active || !claude?.refreshToken) return false;
3409
- const capturedId = active.id;
3410
- this.materializeMigration(config);
3411
- try {
3412
- const result = await import_subscriptions3.claudeOAuth.refreshAccessToken(claude.refreshToken, this.fetchImpl);
3413
- const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
3414
- const next = {
3415
- ...claude,
3416
- accessToken: result.accessToken,
3417
- refreshToken: result.refreshToken,
3418
- expiresAt,
3419
- status: "authorized",
3420
- lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
3421
- errorMessage: void 0
3422
- };
3423
- this.writeBackById("claude", capturedId, next);
3424
- return true;
3425
- } catch (error) {
3426
- this.markExpiredById("claude", capturedId, claude, error);
3427
- return false;
3428
- }
4159
+ return this.coalesce("claude:active", async () => {
4160
+ const config = this.readConfig();
4161
+ const active = getActiveAccount(config, "claude");
4162
+ const claude = active?.tokens;
4163
+ if (!active || !claude?.refreshToken) return false;
4164
+ const capturedId = active.id;
4165
+ this.materializeMigration(config);
4166
+ try {
4167
+ const result = await import_subscriptions3.claudeOAuth.refreshAccessToken(claude.refreshToken, this.fetchImpl);
4168
+ const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
4169
+ const next = {
4170
+ ...claude,
4171
+ accessToken: result.accessToken,
4172
+ refreshToken: result.refreshToken,
4173
+ expiresAt,
4174
+ status: "authorized",
4175
+ lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
4176
+ errorMessage: void 0,
4177
+ syncWarning: void 0
4178
+ };
4179
+ this.writeBackById("claude", capturedId, next);
4180
+ this.resyncExternal("claude", capturedId, next);
4181
+ return true;
4182
+ } catch (error) {
4183
+ if (await this.tryExternalImport("claude", capturedId, claude, async (rt) => {
4184
+ const r = await import_subscriptions3.claudeOAuth.refreshAccessToken(rt, this.fetchImpl);
4185
+ return {
4186
+ accessToken: r.accessToken,
4187
+ refreshToken: r.refreshToken,
4188
+ expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
4189
+ };
4190
+ })) {
4191
+ return true;
4192
+ }
4193
+ this.markExpiredById("claude", capturedId, claude, error);
4194
+ return false;
4195
+ }
4196
+ });
3429
4197
  }
3430
4198
  /**
3431
4199
  * Refresh the Codex (ChatGPT) OAuth access token. Same shape
@@ -3433,31 +4201,46 @@ var JsonSubscriptionCredentialStore = class {
3433
4201
  * HONEST `false` when no refresh_token.
3434
4202
  */
3435
4203
  async refreshCodexToken() {
3436
- const config = this.readConfig();
3437
- const active = getActiveAccount(config, "codex");
3438
- const codex = active?.tokens;
3439
- if (!active || !codex?.refreshToken) return false;
3440
- const capturedId = active.id;
3441
- this.materializeMigration(config);
3442
- try {
3443
- const result = await import_subscriptions3.codexOAuth.refreshAccessToken(codex.refreshToken, this.fetchImpl);
3444
- const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
3445
- const next = {
3446
- ...codex,
3447
- accessToken: result.accessToken,
3448
- refreshToken: result.refreshToken,
3449
- idToken: result.idToken,
3450
- expiresAt,
3451
- status: "authorized",
3452
- lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
3453
- errorMessage: void 0
3454
- };
3455
- this.writeBackById("codex", capturedId, next);
3456
- return true;
3457
- } catch (error) {
3458
- this.markExpiredById("codex", capturedId, codex, error);
3459
- return false;
3460
- }
4204
+ return this.coalesce("codex:active", async () => {
4205
+ const config = this.readConfig();
4206
+ const active = getActiveAccount(config, "codex");
4207
+ const codex = active?.tokens;
4208
+ if (!active || !codex?.refreshToken) return false;
4209
+ const capturedId = active.id;
4210
+ this.materializeMigration(config);
4211
+ try {
4212
+ const result = await import_subscriptions3.codexOAuth.refreshAccessToken(codex.refreshToken, this.fetchImpl);
4213
+ const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
4214
+ const next = {
4215
+ ...codex,
4216
+ accessToken: result.accessToken,
4217
+ refreshToken: result.refreshToken,
4218
+ idToken: result.idToken,
4219
+ expiresAt,
4220
+ status: "authorized",
4221
+ lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
4222
+ errorMessage: void 0,
4223
+ syncWarning: void 0
4224
+ };
4225
+ this.writeBackById("codex", capturedId, next);
4226
+ this.resyncExternal("codex", capturedId, next);
4227
+ return true;
4228
+ } catch (error) {
4229
+ if (await this.tryExternalImport("codex", capturedId, codex, async (rt) => {
4230
+ const r = await import_subscriptions3.codexOAuth.refreshAccessToken(rt, this.fetchImpl);
4231
+ return {
4232
+ accessToken: r.accessToken,
4233
+ refreshToken: r.refreshToken,
4234
+ idToken: r.idToken,
4235
+ expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
4236
+ };
4237
+ })) {
4238
+ return true;
4239
+ }
4240
+ this.markExpiredById("codex", capturedId, codex, error);
4241
+ return false;
4242
+ }
4243
+ });
3461
4244
  }
3462
4245
  /**
3463
4246
  * Refresh the Gemini (Google) OAuth access token. The Google
@@ -3467,30 +4250,175 @@ var JsonSubscriptionCredentialStore = class {
3467
4250
  * destroy the ability to refresh again). HONEST `false` when no refresh_token.
3468
4251
  */
3469
4252
  async refreshGeminiToken() {
3470
- const config = this.readConfig();
3471
- const active = getActiveAccount(config, "gemini");
3472
- const gemini = active?.tokens;
3473
- if (!active || !gemini?.refreshToken) return false;
3474
- const capturedId = active.id;
3475
- this.materializeMigration(config);
3476
- try {
3477
- const result = await import_subscriptions3.geminiOAuth.refreshAccessToken(gemini.refreshToken, this.fetchImpl);
3478
- const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
3479
- const next = {
3480
- ...gemini,
3481
- // KEEP the existing refreshToken (response omits it).
3482
- accessToken: result.accessToken,
3483
- expiresAt,
3484
- status: "authorized",
3485
- lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
3486
- errorMessage: void 0
3487
- };
3488
- this.writeBackById("gemini", capturedId, next);
3489
- return true;
3490
- } catch (error) {
3491
- this.markExpiredById("gemini", capturedId, gemini, error);
4253
+ return this.coalesce("gemini:active", async () => {
4254
+ const config = this.readConfig();
4255
+ const active = getActiveAccount(config, "gemini");
4256
+ const gemini = active?.tokens;
4257
+ if (!active || !gemini?.refreshToken) return false;
4258
+ const capturedId = active.id;
4259
+ this.materializeMigration(config);
4260
+ try {
4261
+ const result = await import_subscriptions3.geminiOAuth.refreshAccessToken(gemini.refreshToken, this.fetchImpl);
4262
+ const expiresAt = new Date(Date.now() + result.expiresIn * 1e3).toISOString();
4263
+ const next = {
4264
+ ...gemini,
4265
+ // KEEP the existing refreshToken (response omits it).
4266
+ accessToken: result.accessToken,
4267
+ expiresAt,
4268
+ status: "authorized",
4269
+ lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
4270
+ errorMessage: void 0
4271
+ };
4272
+ this.writeBackById("gemini", capturedId, next);
4273
+ return true;
4274
+ } catch (error) {
4275
+ this.markExpiredById("gemini", capturedId, gemini, error);
4276
+ return false;
4277
+ }
4278
+ });
4279
+ }
4280
+ /**
4281
+ * Refresh a SPECIFIC account by id (background scheduler sweep,
4282
+ * external-cli-sync). Unlike the active-account refreshers it does NOT
4283
+ * attempt the external-import fallback — the external CLI file's lineage can
4284
+ * only plausibly match the ACTIVE account. Coalesced per `provider:id`; on
4285
+ * failure flags ONLY that account `expired`.
4286
+ */
4287
+ async refreshAccountById(provider, id) {
4288
+ return this.coalesce(`${provider}:${id}`, async () => {
4289
+ const config = this.readConfig();
4290
+ const account = getAccountById(config, provider, id);
4291
+ const captured = account?.tokens;
4292
+ if (!account || !captured?.refreshToken) return false;
4293
+ this.materializeMigration(config);
4294
+ try {
4295
+ const refreshed = await this.refreshUpstream(provider, captured.refreshToken);
4296
+ const next = {
4297
+ ...captured,
4298
+ accessToken: refreshed.accessToken,
4299
+ // Gemini's refresh response omits a new refresh token — keep the captured.
4300
+ refreshToken: refreshed.refreshToken ?? captured.refreshToken,
4301
+ expiresAt: refreshed.expiresAt,
4302
+ status: "authorized",
4303
+ lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
4304
+ errorMessage: void 0,
4305
+ syncWarning: void 0
4306
+ };
4307
+ if (refreshed.idToken) next.idToken = refreshed.idToken;
4308
+ this.writeBackById(provider, id, next);
4309
+ if (provider !== "gemini") this.resyncExternal(provider, id, next);
4310
+ return true;
4311
+ } catch (error) {
4312
+ this.markExpiredById(provider, id, captured, error);
4313
+ return false;
4314
+ }
4315
+ });
4316
+ }
4317
+ /** Dispatch one OAuth refresh round-trip to the provider's shared flow. */
4318
+ async refreshUpstream(provider, refreshToken) {
4319
+ const flow = provider === "claude" ? import_subscriptions3.claudeOAuth : provider === "codex" ? import_subscriptions3.codexOAuth : import_subscriptions3.geminiOAuth;
4320
+ const r = await flow.refreshAccessToken(refreshToken, this.fetchImpl);
4321
+ return {
4322
+ accessToken: r.accessToken,
4323
+ refreshToken: r.refreshToken,
4324
+ idToken: r.idToken,
4325
+ expiresAt: new Date(Date.now() + r.expiresIn * 1e3).toISOString()
4326
+ };
4327
+ }
4328
+ /**
4329
+ * External-import fallback for a FAILED active-account refresh
4330
+ * (external-cli-sync). Reads the CLI native store; imports when the external
4331
+ * lineage ROTATED (different refresh token) or its access token is still
4332
+ * valid. When the imported access token is already expired it refreshes once
4333
+ * with the rotated refresh token. A `not-rotated` outcome persists the
4334
+ * `external-not-rotated` warning on the (about-to-be-expired) account so the
4335
+ * UI can tell "genuine revocation" apart from a plain refresh failure.
4336
+ */
4337
+ async tryExternalImport(provider, capturedId, captured, refreshWithToken) {
4338
+ const markerOwner = this.safeReadMarker(provider);
4339
+ if (markerOwner && markerOwner !== capturedId) return false;
4340
+ const external = this.safeReadExternal(provider);
4341
+ const decision = decideExternalImport(captured, external);
4342
+ if (decision === "not-rotated") {
4343
+ captured.syncWarning = "external-not-rotated";
3492
4344
  return false;
3493
4345
  }
4346
+ if (decision !== "import" || !external) return false;
4347
+ let imported = buildImportedTokens(
4348
+ captured,
4349
+ external
4350
+ );
4351
+ const accessStillValid = external.expiresAt ? Date.parse(external.expiresAt) > Date.now() + 6e4 : true;
4352
+ if (!accessStillValid) {
4353
+ try {
4354
+ const refreshed = await refreshWithToken(external.refreshToken);
4355
+ imported = {
4356
+ ...imported,
4357
+ accessToken: refreshed.accessToken,
4358
+ refreshToken: refreshed.refreshToken ?? imported.refreshToken,
4359
+ expiresAt: refreshed.expiresAt,
4360
+ lastRefreshedAt: (/* @__PURE__ */ new Date()).toISOString()
4361
+ };
4362
+ if (refreshed.idToken) imported.idToken = refreshed.idToken;
4363
+ } catch {
4364
+ return false;
4365
+ }
4366
+ }
4367
+ this.writeBackById(provider, capturedId, imported);
4368
+ this.resyncExternal(provider, capturedId, imported);
4369
+ return true;
4370
+ }
4371
+ /**
4372
+ * Marker-gated external write-back (external-cli-sync). After a successful
4373
+ * refresh of the account that OWNS the provider's native CLI store (imported
4374
+ * via `importExternalCliAccount`), push the rotated credential back into the
4375
+ * file — otherwise the daemon's refresh invalidates the single-use refresh
4376
+ * token and silently logs the bare CLI out. NON-FATAL: the internal store is
4377
+ * already persisted; a failed external write only leaves the file stale,
4378
+ * which the `external-divergent` warning surfaces.
4379
+ */
4380
+ resyncExternal(provider, accountId, tokens) {
4381
+ try {
4382
+ this.externalCliStore.writeBack(provider, accountId, tokens);
4383
+ } catch {
4384
+ }
4385
+ }
4386
+ /** Read the marker's owning account id, never letting an fs error escape. */
4387
+ safeReadMarker(provider) {
4388
+ try {
4389
+ return this.externalCliStore.readMarkerAccountId(provider);
4390
+ } catch {
4391
+ return void 0;
4392
+ }
4393
+ }
4394
+ /**
4395
+ * DAEMON-ONLY (admin import button): which providers have a usable external
4396
+ * CLI credential on THIS machine. Pure detection — reads the native files,
4397
+ * never mutates anything, never returns a token.
4398
+ */
4399
+ async listExternalCliAvailability() {
4400
+ return {
4401
+ claude: Boolean(this.safeReadExternal("claude")?.accessToken),
4402
+ codex: Boolean(this.safeReadExternal("codex")?.accessToken)
4403
+ };
4404
+ }
4405
+ /**
4406
+ * DAEMON-ONLY (admin import button): import the external CLI's current login
4407
+ * as a NEW account (+ activate), and take MANAGED ownership of the native
4408
+ * store (marker) so subsequent refreshes write back — keeping the bare CLI
4409
+ * and the daemon on the same live credential instead of silently killing one
4410
+ * side's single-use refresh token.
4411
+ */
4412
+ async importExternalCliAccount(provider, label) {
4413
+ const external = this.safeReadExternal(provider);
4414
+ if (!external?.accessToken) return { ok: false, reason: "no-credential" };
4415
+ const tokens = buildTokensFromExternal(provider, external);
4416
+ const result = await this.appendProviderAccount(provider, tokens, label);
4417
+ try {
4418
+ this.externalCliStore.writeMarker(provider, result.id);
4419
+ } catch {
4420
+ }
4421
+ return { ok: true, id: result.id };
3494
4422
  }
3495
4423
  /**
3496
4424
  * Materialize a lazily-synthesized account id to disk (design D3). On a legacy
@@ -3574,6 +4502,18 @@ var JsonSubscriptionCredentialStore = class {
3574
4502
  this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
3575
4503
  return result;
3576
4504
  }
4505
+ /**
4506
+ * DAEMON-ONLY per-account rename (NOT on the port). Update one account's label;
4507
+ * rejects an unknown id. Label-only — no token material is read or written
4508
+ * (the secret-free invariant holds).
4509
+ */
4510
+ async renameAccount(providerId, id, label) {
4511
+ const current = this.readConfig();
4512
+ const result = renameAccount(current, providerId, id, label);
4513
+ if (!result.ok) return result;
4514
+ this.persist({ ...current, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
4515
+ return result;
4516
+ }
3577
4517
  /**
3578
4518
  * DAEMON-ONLY CLEAR (design D1/D3, NOT on the port). Remove a single provider's
3579
4519
  * block from `tokens.json` and re-persist (the strategies already tolerate an
@@ -3590,9 +4530,9 @@ var JsonSubscriptionCredentialStore = class {
3590
4530
  * → `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
3591
4531
  * write — incl. child 4's future refresh writes — lands encrypted. */
3592
4532
  persist(config) {
3593
- (0, import_node_fs6.mkdirSync)((0, import_node_path3.dirname)(this.tokensPath), { recursive: true });
4533
+ (0, import_node_fs12.mkdirSync)((0, import_node_path7.dirname)(this.tokensPath), { recursive: true });
3594
4534
  const encrypted = encryptTokens(config, this.box);
3595
- (0, import_node_fs6.writeFileSync)(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
4535
+ (0, import_node_fs12.writeFileSync)(this.tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
3596
4536
  }
3597
4537
  /**
3598
4538
  * Read + parse `tokens.json`, tolerating a missing/corrupt file, then DECRYPT
@@ -3608,10 +4548,10 @@ var JsonSubscriptionCredentialStore = class {
3608
4548
  * `config.ts loadConfig`, which decrypts outside its parse try.
3609
4549
  */
3610
4550
  readConfig() {
3611
- if (!(0, import_node_fs6.existsSync)(this.tokensPath)) return { updatedAt: "" };
4551
+ if (!(0, import_node_fs12.existsSync)(this.tokensPath)) return { updatedAt: "" };
3612
4552
  let parsed;
3613
4553
  try {
3614
- const raw = JSON.parse((0, import_node_fs6.readFileSync)(this.tokensPath, "utf8"));
4554
+ const raw = JSON.parse((0, import_node_fs12.readFileSync)(this.tokensPath, "utf8"));
3615
4555
  parsed = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
3616
4556
  } catch {
3617
4557
  parsed = null;
@@ -3622,6 +4562,95 @@ var JsonSubscriptionCredentialStore = class {
3622
4562
  }
3623
4563
  };
3624
4564
 
4565
+ // src/TokenRefreshScheduler.ts
4566
+ var REFRESH_LEAD_MS = 5 * 6e4;
4567
+ var SWEEP_INTERVAL_MS = 6e4;
4568
+ var OAUTH_PROVIDERS = ["claude", "codex", "gemini"];
4569
+ var TokenRefreshScheduler = class {
4570
+ constructor(store, logger, intervalMs = SWEEP_INTERVAL_MS, leadMs = REFRESH_LEAD_MS) {
4571
+ this.store = store;
4572
+ this.logger = logger;
4573
+ this.intervalMs = intervalMs;
4574
+ this.leadMs = leadMs;
4575
+ }
4576
+ store;
4577
+ logger;
4578
+ intervalMs;
4579
+ leadMs;
4580
+ timer = null;
4581
+ sweeping = false;
4582
+ /** Arm the sweep interval. Idempotent. The timer never holds the loop open. */
4583
+ start() {
4584
+ if (this.timer) return;
4585
+ this.timer = setInterval(() => void this.sweep(), this.intervalMs);
4586
+ this.timer.unref?.();
4587
+ }
4588
+ /** Clear the interval (daemon shutdown / test teardown). Idempotent. */
4589
+ dispose() {
4590
+ if (this.timer) {
4591
+ clearInterval(this.timer);
4592
+ this.timer = null;
4593
+ }
4594
+ }
4595
+ /** One sweep over every account of every OAuth provider. Exposed for tests. */
4596
+ async sweep(now = Date.now()) {
4597
+ if (this.sweeping) return;
4598
+ this.sweeping = true;
4599
+ try {
4600
+ const config = await this.store.getFullConfig();
4601
+ for (const provider of OAUTH_PROVIDERS) {
4602
+ const activeId = getActiveAccount(config, provider)?.id;
4603
+ for (const account of listAccounts(config, provider)) {
4604
+ if (!this.needsRefresh(account.tokens, now)) continue;
4605
+ await this.refreshOne(provider, account.id, account.id === activeId);
4606
+ }
4607
+ }
4608
+ } catch (error) {
4609
+ this.logger.warn("token-refresh sweep failed", {
4610
+ error: error instanceof Error ? error.message : String(error)
4611
+ });
4612
+ } finally {
4613
+ this.sweeping = false;
4614
+ }
4615
+ }
4616
+ /** Expiring within the lead window, refreshable, and not already dead. */
4617
+ needsRefresh(tokens, now) {
4618
+ const t = tokens;
4619
+ if (!t.refreshToken || t.status === "expired" || t.status === "error") return false;
4620
+ if (!t.expiresAt) return false;
4621
+ const expiresAt = Date.parse(t.expiresAt);
4622
+ return Number.isFinite(expiresAt) && now >= expiresAt - this.leadMs;
4623
+ }
4624
+ /** Refresh one account; failures are logged, never thrown (the store has
4625
+ * already flagged the account `expired`). */
4626
+ async refreshOne(provider, id, isActive) {
4627
+ try {
4628
+ const ok = isActive ? await this.refreshActive(provider) : await this.store.refreshAccountById(provider, id);
4629
+ if (!ok) {
4630
+ this.logger.warn("background token refresh failed", { provider, accountId: id });
4631
+ } else {
4632
+ this.logger.info("background token refresh succeeded", { provider, accountId: id });
4633
+ }
4634
+ } catch (error) {
4635
+ this.logger.warn("background token refresh threw", {
4636
+ provider,
4637
+ accountId: id,
4638
+ error: error instanceof Error ? error.message : String(error)
4639
+ });
4640
+ }
4641
+ }
4642
+ refreshActive(provider) {
4643
+ switch (provider) {
4644
+ case "claude":
4645
+ return this.store.refreshClaudeToken();
4646
+ case "codex":
4647
+ return this.store.refreshCodexToken();
4648
+ case "gemini":
4649
+ return this.store.refreshGeminiToken();
4650
+ }
4651
+ }
4652
+ };
4653
+
3625
4654
  // src/bootstrap.ts
3626
4655
  function buildDaemon(config, paths) {
3627
4656
  const logger = new ConsoleLogger();
@@ -3654,7 +4683,14 @@ function buildDaemon(config, paths) {
3654
4683
  autoDisableStore.markAutoDisabled(keyId, status, at);
3655
4684
  }
3656
4685
  );
3657
- const providerProxy = (0, import_provider_proxy.getProviderProxy)({ llmConfig, apiKeyPool });
4686
+ const pricingStore = new JsonPricingStore(defaultPricingPath(paths.configPath));
4687
+ const pricingEngine = new import_usage.PricingEngine(pricingStore, logger);
4688
+ const usageEventStore = new JsonlUsageEventStore(
4689
+ defaultUsageEventsPath(paths.configPath),
4690
+ async (providerId, model) => await pricingEngine.getEntry(providerId, model) !== null
4691
+ );
4692
+ const usageRecorder = new import_usage.UsageRecorder(usageEventStore, pricingEngine, logger);
4693
+ const providerProxy = (0, import_provider_proxy.getProviderProxy)({ llmConfig, apiKeyPool, usageRecorder });
3658
4694
  llmConfig.setReloadHook(() => apiKeyPool.invalidateCache());
3659
4695
  const outboundApiServer = (0, import_outbound_api4.getOutboundApiServer)({
3660
4696
  db: keyDb,
@@ -3700,10 +4736,23 @@ function buildDaemon(config, paths) {
3700
4736
  // the multi-account append (`appendProviderAccount`, import re-encrypts at-
3701
4737
  // rest). Confined to the export/import handlers; never reached by a GET.
3702
4738
  migrationCredentialStore: credentialStore,
4739
+ // Code CLI launch (dashboard parity): the external-terminal opener + PATH probe
4740
+ // default to the real implementations; tests inject spies so no window spawns.
4741
+ cliTerminalOpener: paths.cliTerminalOpener,
4742
+ cliPathProbe: paths.cliPathProbe,
4743
+ cliCommandRunner: paths.cliCommandRunner,
4744
+ // Usage/pricing admin surface (usage-pricing child): stats queries go
4745
+ // through the recorder facade, pricing mutations through the engine, and
4746
+ // the row DELETE through the concrete store (delete is store-local — the
4747
+ // core port stays frozen). None of these can reach key material.
4748
+ usageRecorder,
4749
+ pricingEngine,
4750
+ pricingStore,
3703
4751
  // Use the DECRYPTED config so `admin.token` (if stored as `enc:`) is the
3704
4752
  // plaintext bearer the AdminServer's constant-time compare expects (D4).
3705
4753
  getAdminConfig: () => resolveAdminConfig(decryptedConfig.admin)
3706
4754
  });
4755
+ const tokenRefreshScheduler = new TokenRefreshScheduler(credentialStore, logger);
3707
4756
  return {
3708
4757
  logger,
3709
4758
  llmConfig,
@@ -3716,7 +4765,11 @@ function buildDaemon(config, paths) {
3716
4765
  credentialStore,
3717
4766
  subscriptionRegistry,
3718
4767
  subscriptionAccounts,
3719
- adminServer
4768
+ pricingStore,
4769
+ pricingEngine,
4770
+ usageRecorder,
4771
+ adminServer,
4772
+ tokenRefreshScheduler
3720
4773
  };
3721
4774
  }
3722
4775
 
@@ -3758,10 +4811,10 @@ function buildCliSpawnPlan(opts) {
3758
4811
  };
3759
4812
  }
3760
4813
  function resolveInPathDefault(candidate) {
3761
- const segments = (process.env["PATH"] ?? "").split(import_node_path4.delimiter).filter(Boolean);
4814
+ const segments = (process.env["PATH"] ?? "").split(import_node_path8.delimiter).filter(Boolean);
3762
4815
  for (const seg of segments) {
3763
- const full = (0, import_node_path4.join)(seg, candidate);
3764
- if ((0, import_node_fs7.existsSync)(full)) return full;
4816
+ const full = (0, import_node_path8.join)(seg, candidate);
4817
+ if ((0, import_node_fs13.existsSync)(full)) return full;
3765
4818
  }
3766
4819
  return null;
3767
4820
  }
@@ -3801,9 +4854,10 @@ async function runLaunch(argv, deps) {
3801
4854
  try {
3802
4855
  await daemon.llmConfig.ready();
3803
4856
  await daemon.providerProxy.start();
3804
- } catch (err4) {
4857
+ } catch (err5) {
3805
4858
  daemon.apiKeyPool.dispose();
3806
- throw err4;
4859
+ daemon.tokenRefreshScheduler.dispose();
4860
+ throw err5;
3807
4861
  }
3808
4862
  let launch;
3809
4863
  try {
@@ -3811,10 +4865,11 @@ async function runLaunch(argv, deps) {
3811
4865
  providerId: values.provider,
3812
4866
  model: values.model
3813
4867
  });
3814
- } catch (err4) {
4868
+ } catch (err5) {
3815
4869
  await daemon.providerProxy.stop();
3816
4870
  daemon.apiKeyPool.dispose();
3817
- throw err4;
4871
+ daemon.tokenRefreshScheduler.dispose();
4872
+ throw err5;
3818
4873
  }
3819
4874
  try {
3820
4875
  const plan = buildCliSpawnPlan({
@@ -3835,6 +4890,7 @@ async function runLaunch(argv, deps) {
3835
4890
  launch.onSessionEnd();
3836
4891
  await daemon.providerProxy.stop();
3837
4892
  daemon.apiKeyPool.dispose();
4893
+ daemon.tokenRefreshScheduler.dispose();
3838
4894
  }
3839
4895
  }
3840
4896
  async function buildLaunchConfig(cli, llmConfig, opts) {
@@ -3848,15 +4904,15 @@ async function buildLaunchConfig(cli, llmConfig, opts) {
3848
4904
  };
3849
4905
  switch (cli) {
3850
4906
  case "claude":
3851
- return (0, import_cli_launcher.buildClaudeCliLaunchConfig)(common);
4907
+ return (0, import_cli_launcher2.buildClaudeCliLaunchConfig)(common);
3852
4908
  case "codex":
3853
- return (0, import_cli_launcher.buildCodexLaunchConfig)(common);
4909
+ return (0, import_cli_launcher2.buildCodexLaunchConfig)(common);
3854
4910
  case "gemini":
3855
- return (0, import_cli_launcher.buildGeminiCliLaunchConfig)(common);
4911
+ return (0, import_cli_launcher2.buildGeminiCliLaunchConfig)(common);
3856
4912
  case "qwen":
3857
4913
  case "copilot":
3858
4914
  case "opencode":
3859
- return (0, import_cli_launcher.buildChatCliLaunchConfig)({ backendId: cli, ...common });
4915
+ return (0, import_cli_launcher2.buildChatCliLaunchConfig)({ backendId: cli, ...common });
3860
4916
  default: {
3861
4917
  const _exhaustive = cli;
3862
4918
  throw new Error(`Unsupported launch CLI: ${String(_exhaustive)}`);
@@ -3865,7 +4921,7 @@ async function buildLaunchConfig(cli, llmConfig, opts) {
3865
4921
  }
3866
4922
  function spawnCliInherit(plan) {
3867
4923
  return new Promise((resolve, reject) => {
3868
- const child = (0, import_node_child_process.spawn)(plan.command, plan.args, {
4924
+ const child = (0, import_node_child_process2.spawn)(plan.command, plan.args, {
3869
4925
  stdio: "inherit",
3870
4926
  env: plan.env,
3871
4927
  cwd: plan.cwd,
@@ -3884,9 +4940,9 @@ function spawnCliInherit(plan) {
3884
4940
  process.removeListener("SIGINT", onSignal);
3885
4941
  process.removeListener("SIGTERM", onSignal);
3886
4942
  };
3887
- child.on("error", (err4) => {
4943
+ child.on("error", (err5) => {
3888
4944
  detach();
3889
- if (err4.code === "ENOENT") {
4945
+ if (err5.code === "ENOENT") {
3890
4946
  reject(
3891
4947
  new Error(
3892
4948
  `launch: "${plan.command}" not found on PATH \u2014 install the CLI first.`
@@ -3894,7 +4950,7 @@ function spawnCliInherit(plan) {
3894
4950
  );
3895
4951
  return;
3896
4952
  }
3897
- reject(err4);
4953
+ reject(err5);
3898
4954
  });
3899
4955
  child.on("exit", (code, signal) => {
3900
4956
  detach();
@@ -3904,7 +4960,7 @@ function spawnCliInherit(plan) {
3904
4960
  }
3905
4961
 
3906
4962
  // src/commands/login.ts
3907
- var import_node_child_process2 = require("child_process");
4963
+ var import_node_child_process3 = require("child_process");
3908
4964
  var import_node_readline = require("readline");
3909
4965
  var import_node_util4 = require("util");
3910
4966
  var import_subscriptions5 = require("@omnicross/subscriptions");
@@ -4055,7 +5111,7 @@ function openBrowser(url) {
4055
5111
  return new Promise((resolve) => {
4056
5112
  try {
4057
5113
  const { command, args } = buildOpenBrowserCommand(process.platform, url);
4058
- const child = (0, import_node_child_process2.spawn)(command, args, { stdio: "ignore", detached: true });
5114
+ const child = (0, import_node_child_process3.spawn)(command, args, { stdio: "ignore", detached: true });
4059
5115
  child.on("error", () => resolve(false));
4060
5116
  child.unref();
4061
5117
  resolve(true);
@@ -4075,7 +5131,7 @@ function promptPaste(prompt) {
4075
5131
  }
4076
5132
 
4077
5133
  // src/commands/providers.ts
4078
- var import_node_crypto8 = require("crypto");
5134
+ var import_node_crypto10 = require("crypto");
4079
5135
  var import_node_util5 = require("util");
4080
5136
  async function runProviders(argv) {
4081
5137
  const { values, positionals } = (0, import_node_util5.parseArgs)({
@@ -4197,7 +5253,7 @@ function providersAddKey(configPath, providerId, opts) {
4197
5253
  const cfg = loadConfig(configPath);
4198
5254
  const row = cfg.providers.find((p) => p.id === providerId);
4199
5255
  if (!row) throw new Error(`providers add-key: unknown provider '${providerId}'`);
4200
- const entry = { id: (0, import_node_crypto8.randomUUID)(), apiKey: opts.key };
5256
+ const entry = { id: (0, import_node_crypto10.randomUUID)(), apiKey: opts.key };
4201
5257
  if (opts.label) entry.label = opts.label;
4202
5258
  if (opts.weight !== void 0) {
4203
5259
  const w = Number(opts.weight);
@@ -4225,7 +5281,7 @@ function providersRmKey(configPath, providerId, keyId) {
4225
5281
  }
4226
5282
 
4227
5283
  // src/commands/secrets.ts
4228
- var import_node_fs8 = require("fs");
5284
+ var import_node_fs14 = require("fs");
4229
5285
  var import_node_util6 = require("util");
4230
5286
  async function runSecrets(argv) {
4231
5287
  const { values, positionals } = (0, import_node_util6.parseArgs)({
@@ -4297,7 +5353,7 @@ function secretsStatus(args) {
4297
5353
  reportField("admin.token", cfg.admin.token);
4298
5354
  }
4299
5355
  const tokensPath = defaultTokensPath(args.config);
4300
- if ((0, import_node_fs8.existsSync)(tokensPath)) {
5356
+ if ((0, import_node_fs14.existsSync)(tokensPath)) {
4301
5357
  console.info(`Secret status for ${tokensPath}:`);
4302
5358
  reportTokenFields(tokensPath);
4303
5359
  }
@@ -4337,7 +5393,7 @@ function secretsRotate(args) {
4337
5393
  const tokensPath = defaultTokensPath(args.config);
4338
5394
  try {
4339
5395
  cfg = loadConfig(args.config);
4340
- if ((0, import_node_fs8.existsSync)(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, oldBox);
5396
+ if ((0, import_node_fs14.existsSync)(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, oldBox);
4341
5397
  } finally {
4342
5398
  setSecretBox(null);
4343
5399
  }
@@ -4366,28 +5422,28 @@ function secretsDecrypt(args) {
4366
5422
  let tokensPlain = null;
4367
5423
  try {
4368
5424
  cfg = loadConfig(args.config);
4369
- if ((0, import_node_fs8.existsSync)(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, box);
5425
+ if ((0, import_node_fs14.existsSync)(tokensPath)) tokensPlain = decryptTokensFile(tokensPath, box);
4370
5426
  } finally {
4371
5427
  setSecretBox(null);
4372
5428
  }
4373
5429
  saveConfig(args.config, cfg);
4374
5430
  if (tokensPlain) {
4375
- (0, import_node_fs8.writeFileSync)(tokensPath, JSON.stringify(tokensPlain, null, 2) + "\n", "utf8");
5431
+ (0, import_node_fs14.writeFileSync)(tokensPath, JSON.stringify(tokensPlain, null, 2) + "\n", "utf8");
4376
5432
  }
4377
5433
  console.info(`Decrypted secrets to plaintext in ${args.config}` + tokensSuffix(args.config));
4378
5434
  }
4379
- function readRawConfig(path) {
5435
+ function readRawConfig(path2) {
4380
5436
  let parsed;
4381
5437
  try {
4382
- parsed = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
5438
+ parsed = JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8"));
4383
5439
  } catch {
4384
- throw new Error(`secrets: cannot read or parse '${path}'`);
5440
+ throw new Error(`secrets: cannot read or parse '${path2}'`);
4385
5441
  }
4386
5442
  return validateConfig(parsed);
4387
5443
  }
4388
- function readRawJson(path) {
5444
+ function readRawJson(path2) {
4389
5445
  try {
4390
- const parsed = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
5446
+ const parsed = JSON.parse((0, import_node_fs14.readFileSync)(path2, "utf8"));
4391
5447
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4392
5448
  return parsed;
4393
5449
  }
@@ -4397,7 +5453,7 @@ function readRawJson(path) {
4397
5453
  }
4398
5454
  function encryptTokensFileInPlace(configPath, box) {
4399
5455
  const tokensPath = defaultTokensPath(configPath);
4400
- if (!(0, import_node_fs8.existsSync)(tokensPath)) return;
5456
+ if (!(0, import_node_fs14.existsSync)(tokensPath)) return;
4401
5457
  const plain = decryptTokensFile(tokensPath, box);
4402
5458
  writeTokensEncrypted(tokensPath, plain, box);
4403
5459
  }
@@ -4410,7 +5466,7 @@ function writeTokensEncrypted(tokensPath, plain, box) {
4410
5466
  { updatedAt: "", ...plain },
4411
5467
  box
4412
5468
  );
4413
- (0, import_node_fs8.writeFileSync)(tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
5469
+ (0, import_node_fs14.writeFileSync)(tokensPath, JSON.stringify(encrypted, null, 2) + "\n", "utf8");
4414
5470
  }
4415
5471
  var TOKEN_FIELDS2 = {
4416
5472
  claude: ["accessToken", "refreshToken"],
@@ -4433,7 +5489,7 @@ function walkTokens(raw, fn) {
4433
5489
  return next;
4434
5490
  }
4435
5491
  function tokensSuffix(configPath) {
4436
- return (0, import_node_fs8.existsSync)(defaultTokensPath(configPath)) ? " (+ tokens.json)" : "";
5492
+ return (0, import_node_fs14.existsSync)(defaultTokensPath(configPath)) ? " (+ tokens.json)" : "";
4437
5493
  }
4438
5494
 
4439
5495
  // src/commands/start.ts
@@ -4475,6 +5531,7 @@ async function runStart(argv) {
4475
5531
  await daemon.adminServer.start();
4476
5532
  dashboardUrl = daemon.adminServer.getStatus().url;
4477
5533
  }
5534
+ daemon.tokenRefreshScheduler.start();
4478
5535
  const status = daemon.outboundApiServer.getStatus();
4479
5536
  console.info("omnicross daemon is running.");
4480
5537
  if (dashboardUrl) console.info(` dashboard : ${dashboardUrl}`);
@@ -4493,6 +5550,32 @@ async function runStart(argv) {
4493
5550
  console.info(" dashboard: localhost-default; --no-dashboard to disable; admin.token for a bearer gate");
4494
5551
  }
4495
5552
  console.info("Press Ctrl+C to stop.");
5553
+ return { dashboardUrl };
5554
+ }
5555
+
5556
+ // src/commands/ui.ts
5557
+ async function runUi(argv, deps) {
5558
+ const start = deps?.start ?? runStart;
5559
+ const open = deps?.openBrowser ?? openBrowser;
5560
+ const noOpen = argv.includes("--no-open");
5561
+ const startArgv = argv.filter((a) => a !== "--no-open");
5562
+ if (!resolveUiDist()) {
5563
+ console.warn(
5564
+ "omnicross ui: Control Panel assets not found (@omnicross/ui has no built dist). The daemon will still start, but /ui will 404. Install @omnicross/ui (or build it: npm run build -w @omnicross/ui), or set OMNICROSS_UI_DIST."
5565
+ );
5566
+ }
5567
+ const { dashboardUrl } = await start(startArgv);
5568
+ if (!dashboardUrl) {
5569
+ console.warn("omnicross ui: the admin dashboard is disabled \u2014 no UI to open.");
5570
+ return;
5571
+ }
5572
+ const uiUrl = `${dashboardUrl}/ui/`;
5573
+ console.info(`Control Panel: ${uiUrl}`);
5574
+ if (noOpen) return;
5575
+ const launched = await open(uiUrl).catch(() => false);
5576
+ if (!launched) {
5577
+ console.info("(Could not open a browser automatically \u2014 open the URL above manually.)");
5578
+ }
4496
5579
  }
4497
5580
 
4498
5581
  // src/cli.ts
@@ -4500,6 +5583,8 @@ var USAGE = `omnicross \u2014 standalone @omnicross/core daemon
4500
5583
 
4501
5584
  Usage:
4502
5585
  omnicross start --config <path> Boot the daemon (BYO-key serving).
5586
+ omnicross ui --config <path> [--no-open] Boot the daemon + open the Control Panel
5587
+ (the web UI at <dashboard>/ui/) in a browser.
4503
5588
  omnicross keys add <name> --config <p> Mint a named API key (shown once).
4504
5589
  omnicross keys list --config <p> List stored keys (no secrets).
4505
5590
  omnicross keys revoke <id> --config <p> Revoke a key.
@@ -4525,6 +5610,9 @@ async function main() {
4525
5610
  case "start":
4526
5611
  await runStart(rest);
4527
5612
  return;
5613
+ case "ui":
5614
+ await runUi(rest);
5615
+ return;
4528
5616
  case "keys":
4529
5617
  await runKeys(rest);
4530
5618
  return;
@@ -4556,7 +5644,7 @@ async function main() {
4556
5644
  process.exitCode = 1;
4557
5645
  }
4558
5646
  }
4559
- main().catch((err4) => {
4560
- console.error(err4 instanceof Error ? err4.message : String(err4));
5647
+ main().catch((err5) => {
5648
+ console.error(err5 instanceof Error ? err5.message : String(err5));
4561
5649
  process.exitCode = 1;
4562
5650
  });