@proxysoul/soulforge 2.13.1 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -129,6 +129,9 @@ function createWorkerHandler(handlers, onInit, onDispose) {
129
129
  const allHandlers = {
130
130
  ...handlers,
131
131
  __memoryUsage: () => {
132
+ try {
133
+ Bun.gc(true);
134
+ } catch {}
132
135
  const usage = process.memoryUsage();
133
136
  return {
134
137
  heapUsed: usage.heapUsed,
@@ -41832,7 +41835,9 @@ var init_ui = __esm(() => {
41832
41835
  floatingTerminal: false,
41833
41836
  updateModal: false,
41834
41837
  mcpSettings: false,
41835
- tabNamePopup: false
41838
+ hearthSettings: false,
41839
+ tabNamePopup: false,
41840
+ uiDemo: false
41836
41841
  };
41837
41842
  useUIStore = create()(subscribeWithSelector((set2) => ({
41838
41843
  modals: {
@@ -49515,7 +49520,7 @@ var package_default;
49515
49520
  var init_package = __esm(() => {
49516
49521
  package_default = {
49517
49522
  name: "@proxysoul/soulforge",
49518
- version: "2.13.1",
49523
+ version: "2.14.0",
49519
49524
  description: "Graph-powered code intelligence \u2014 multi-agent coding with codebase-aware AI",
49520
49525
  repository: {
49521
49526
  type: "git",
@@ -49532,7 +49537,8 @@ var init_package = __esm(() => {
49532
49537
  author: "proxySoul",
49533
49538
  bin: {
49534
49539
  soulforge: "./dist/bin.sh",
49535
- sf: "./dist/bin.sh"
49540
+ sf: "./dist/bin.sh",
49541
+ "soulforge-remote": "./dist/soulforge-remote.sh"
49536
49542
  },
49537
49543
  engines: {
49538
49544
  bun: ">=1.2.0"
@@ -49573,7 +49579,7 @@ var init_package = __esm(() => {
49573
49579
  "@types/node": "25.6.0",
49574
49580
  "@types/react": "19.2.14",
49575
49581
  "babel-plugin-react-compiler": "1.0.0",
49576
- "bun-types": "1.3.12",
49582
+ "bun-types": "1.3.13",
49577
49583
  typescript: "6.0.3"
49578
49584
  },
49579
49585
  dependencies: {
@@ -49593,20 +49599,21 @@ var init_package = __esm(() => {
49593
49599
  "@modelcontextprotocol/sdk": "^1.29.0",
49594
49600
  "@mozilla/readability": "0.6.0",
49595
49601
  "@openrouter/ai-sdk-provider": "2.8.0",
49596
- "@opentui/react": "0.1.100",
49602
+ "@opentui/react": "0.1.102",
49597
49603
  ai: "6.0.168",
49598
49604
  "ghostty-opentui": "1.4.10",
49599
49605
  isbinaryfile: "6.0.0",
49600
49606
  jsonrepair: "^3.14.0",
49601
49607
  linkedom: "0.18.12",
49602
49608
  "linkify-it": "5.0.0",
49603
- marked: "18.0.1",
49609
+ marked: "18.0.2",
49604
49610
  neovim: "5.4.0",
49605
49611
  react: "19.2.5",
49606
49612
  shiki: "4.0.2",
49607
49613
  "strip-ansi": "7.2.0",
49608
49614
  "tree-sitter-wasms": "0.1.13",
49609
49615
  "ts-morph": "28.0.0",
49616
+ undici: "^8.1.0",
49610
49617
  "vercel-minimax-ai-provider": "^0.0.2",
49611
49618
  "web-tree-sitter": "0.25.10",
49612
49619
  zod: "4.3.6",
@@ -74331,11 +74338,162 @@ var init_openrouter = __esm(() => {
74331
74338
  };
74332
74339
  });
74333
74340
 
74341
+ // src/core/proxy/key-resolver.ts
74342
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
74343
+ import { homedir as homedir5 } from "os";
74344
+ import { join as join7 } from "path";
74345
+ function candidateConfigPaths() {
74346
+ const paths = [process.env.PROXY_CONFIG_PATH, join7(homedir5(), ".soulforge", "proxy", "config.yaml"), "/opt/homebrew/etc/cliproxyapi.conf", "/opt/homebrew/etc/cliproxyapi/config.yaml", "/usr/local/etc/cliproxyapi.conf", "/usr/local/etc/cliproxyapi/config.yaml", "/etc/cliproxyapi.conf", "/etc/cliproxyapi/config.yaml", join7(homedir5(), ".config", "cliproxyapi", "config.yaml"), join7(homedir5(), ".cli-proxy-api", "config.yaml")];
74347
+ return paths.filter((p) => typeof p === "string" && p.length > 0);
74348
+ }
74349
+ function isPlausibleKey(value) {
74350
+ const s = value.trim();
74351
+ if (!s)
74352
+ return false;
74353
+ if (PLACEHOLDER_PATTERNS.some((re) => re.test(s)))
74354
+ return false;
74355
+ if (BCRYPT_RE.test(s))
74356
+ return false;
74357
+ if (SHA256_HEX_RE.test(s))
74358
+ return false;
74359
+ return true;
74360
+ }
74361
+ function parseApiKeys(content) {
74362
+ const lines = content.split(`
74363
+ `);
74364
+ const keys = [];
74365
+ let inList = false;
74366
+ let listIndent = -1;
74367
+ for (const rawLine of lines) {
74368
+ const line = rawLine.replace(/\r$/, "");
74369
+ const stripped = line.replace(/^\s*/, "");
74370
+ if (stripped.startsWith("#"))
74371
+ continue;
74372
+ if (!inList) {
74373
+ if (/^api-keys\s*:\s*$/.test(stripped) && !line.startsWith(" ") && !line.startsWith("\t")) {
74374
+ inList = true;
74375
+ listIndent = -1;
74376
+ }
74377
+ continue;
74378
+ }
74379
+ if (stripped === "")
74380
+ continue;
74381
+ const indentMatch = line.match(/^(\s+)/);
74382
+ const indent = indentMatch?.[1]?.length ?? 0;
74383
+ if (indent === 0)
74384
+ break;
74385
+ if (listIndent === -1)
74386
+ listIndent = indent;
74387
+ if (indent < listIndent)
74388
+ break;
74389
+ const item = line.slice(indent).match(/^-\s+(?:"([^"]*)"|'([^']*)'|(.+?))\s*$/);
74390
+ if (!item)
74391
+ continue;
74392
+ const v = item[1] ?? item[2] ?? item[3] ?? "";
74393
+ keys.push(v);
74394
+ }
74395
+ return keys;
74396
+ }
74397
+ function discoverApiKeys() {
74398
+ const out = [];
74399
+ const seen = new Set;
74400
+ for (const path of candidateConfigPaths()) {
74401
+ try {
74402
+ if (!existsSync2(path))
74403
+ continue;
74404
+ const content = readFileSync2(path, "utf-8");
74405
+ for (const k of parseApiKeys(content)) {
74406
+ if (!isPlausibleKey(k))
74407
+ continue;
74408
+ if (seen.has(k))
74409
+ continue;
74410
+ seen.add(k);
74411
+ out.push({
74412
+ key: k,
74413
+ source: path
74414
+ });
74415
+ }
74416
+ } catch {}
74417
+ }
74418
+ return out;
74419
+ }
74420
+ function primaryConfigPath() {
74421
+ for (const path of candidateConfigPaths()) {
74422
+ try {
74423
+ if (!existsSync2(path))
74424
+ continue;
74425
+ const content = readFileSync2(path, "utf-8");
74426
+ if (/^api-keys\s*:\s*$/m.test(content))
74427
+ return path;
74428
+ } catch {}
74429
+ }
74430
+ return null;
74431
+ }
74432
+ function getActiveProxyApiKey() {
74433
+ return cached2;
74434
+ }
74435
+ function setActiveProxyApiKey(key) {
74436
+ cached2 = key;
74437
+ cachedIsProbed = true;
74438
+ }
74439
+ function candidateApiKeys() {
74440
+ const out = [];
74441
+ const push = (v) => {
74442
+ if (!v)
74443
+ return;
74444
+ const s = v.trim();
74445
+ if (!s)
74446
+ return;
74447
+ if (!out.includes(s))
74448
+ out.push(s);
74449
+ };
74450
+ push(process.env.PROXY_API_KEY);
74451
+ push("soulforge");
74452
+ for (const d of discoverApiKeys())
74453
+ push(d.key);
74454
+ return out;
74455
+ }
74456
+ var PLACEHOLDER_PATTERNS, BCRYPT_RE, SHA256_HEX_RE, cached2, cachedIsProbed = false;
74457
+ var init_key_resolver = __esm(() => {
74458
+ PLACEHOLDER_PATTERNS = [/^your[-_]?(?:api[-_]?)?key/i, /^changeme$/i, /^replace[-_ ]?me/i, /^xxx+$/i];
74459
+ BCRYPT_RE = /^\$2[aby]\$/;
74460
+ SHA256_HEX_RE = /^[0-9a-f]{64}$/i;
74461
+ cached2 = process.env.PROXY_API_KEY?.trim() || "soulforge";
74462
+ });
74463
+
74334
74464
  // src/core/setup/install.ts
74335
74465
  import { execSync } from "child_process";
74336
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, symlinkSync, unlinkSync } from "fs";
74337
- import { homedir as homedir5, platform } from "os";
74338
- import { join as join7 } from "path";
74466
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, symlinkSync, unlinkSync } from "fs";
74467
+ import { homedir as homedir6, platform } from "os";
74468
+ import { join as join8 } from "path";
74469
+ async function fetchLatestProxyVersion(timeoutMs = 5000) {
74470
+ try {
74471
+ const ctl = new AbortController;
74472
+ const t = setTimeout(() => ctl.abort(), timeoutMs);
74473
+ const res = await fetch(PROXY_RELEASES_URL, {
74474
+ signal: ctl.signal,
74475
+ headers: {
74476
+ Accept: "application/vnd.github+json"
74477
+ }
74478
+ });
74479
+ clearTimeout(t);
74480
+ if (!res.ok)
74481
+ return null;
74482
+ const data = await res.json();
74483
+ return data.tag_name?.replace(/^v/, "")?.trim() || null;
74484
+ } catch {
74485
+ return null;
74486
+ }
74487
+ }
74488
+ async function resolveProxyVersion() {
74489
+ const env = process.env.SOULFORGE_PROXY_VERSION?.trim();
74490
+ if (env)
74491
+ return env;
74492
+ const latest = await fetchLatestProxyVersion();
74493
+ if (latest)
74494
+ return latest;
74495
+ return FALLBACK_PROXY_VERSION;
74496
+ }
74339
74497
  function getPlatformKey() {
74340
74498
  const key = `${process.platform}-${process.arch}`;
74341
74499
  if (key !== "darwin-arm64" && key !== "darwin-x64" && key !== "linux-x64" && key !== "linux-arm64") {
@@ -74347,22 +74505,22 @@ async function installBinary(config2) {
74347
74505
  ensureDirs();
74348
74506
  const key = getPlatformKey();
74349
74507
  const asset = config2.getAsset(key);
74350
- const extractDir = join7(INSTALLS_DIR, `${config2.name}-${config2.version}`);
74351
- if (!existsSync2(asset.binPath)) {
74508
+ const extractDir = join8(INSTALLS_DIR, `${config2.name}-${config2.version}`);
74509
+ if (!existsSync3(asset.binPath)) {
74352
74510
  await downloadAndExtract(asset.url, extractDir);
74353
74511
  }
74354
- if (!existsSync2(asset.binPath)) {
74512
+ if (!existsSync3(asset.binPath)) {
74355
74513
  throw new Error(`${config2.name} binary not found after extraction at ${asset.binPath}`);
74356
74514
  }
74357
74515
  execSync(`chmod +x "${asset.binPath}"`, {
74358
74516
  stdio: "ignore"
74359
74517
  });
74360
- createSymlink(asset.binPath, join7(BIN_DIR, config2.binName));
74361
- return join7(BIN_DIR, config2.binName);
74518
+ createSymlink(asset.binPath, join8(BIN_DIR, config2.binName));
74519
+ return join8(BIN_DIR, config2.binName);
74362
74520
  }
74363
74521
  function getVendoredPath(binary) {
74364
- const binLink = join7(BIN_DIR, binary);
74365
- return existsSync2(binLink) ? binLink : null;
74522
+ const binLink = join8(BIN_DIR, binary);
74523
+ return existsSync3(binLink) ? binLink : null;
74366
74524
  }
74367
74525
  function ensureDirs() {
74368
74526
  mkdirSync2(BIN_DIR, {
@@ -74380,7 +74538,7 @@ async function downloadAndExtract(url2, extractDir) {
74380
74538
  if (!response.ok) {
74381
74539
  throw new Error(`Download failed: ${response.status} ${response.statusText} (${url2})`);
74382
74540
  }
74383
- const tmpFile = join7(extractDir, "download.tar.gz");
74541
+ const tmpFile = join8(extractDir, "download.tar.gz");
74384
74542
  const buffer = await response.arrayBuffer();
74385
74543
  await Bun.write(tmpFile, buffer);
74386
74544
  execSync(`tar xzf "${tmpFile}" -C "${extractDir}"`, {
@@ -74389,14 +74547,14 @@ async function downloadAndExtract(url2, extractDir) {
74389
74547
  unlinkSync(tmpFile);
74390
74548
  }
74391
74549
  function createSymlink(target, link) {
74392
- if (existsSync2(link)) {
74550
+ if (existsSync3(link)) {
74393
74551
  unlinkSync(link);
74394
74552
  }
74395
74553
  symlinkSync(target, link);
74396
74554
  }
74397
74555
  async function installProxy(version2) {
74398
- const v = version2 ?? PROXY_VERSION;
74399
- return installBinary({
74556
+ const v = version2 ?? await resolveProxyVersion();
74557
+ const path = await installBinary({
74400
74558
  name: "cliproxyapi",
74401
74559
  binName: "cli-proxy-api",
74402
74560
  version: v,
@@ -74405,17 +74563,21 @@ async function installProxy(version2) {
74405
74563
  const asset = `CLIProxyAPI_${v}_${suffix}.tar.gz`;
74406
74564
  return {
74407
74565
  url: `https://github.com/router-for-me/CLIProxyAPI/releases/download/v${v}/${asset}`,
74408
- binPath: join7(INSTALLS_DIR, `cliproxyapi-${v}`, "cli-proxy-api")
74566
+ binPath: join8(INSTALLS_DIR, `cliproxyapi-${v}`, "cli-proxy-api")
74409
74567
  };
74410
74568
  }
74411
74569
  });
74570
+ return {
74571
+ path,
74572
+ version: v
74573
+ };
74412
74574
  }
74413
- var SOULFORGE_DIR, BIN_DIR, INSTALLS_DIR, FONTS_DIR, PROXY_VERSION = "6.9.28", PROXY_SUFFIXES;
74575
+ var SOULFORGE_DIR, BIN_DIR, INSTALLS_DIR, FONTS_DIR, FALLBACK_PROXY_VERSION = "6.9.29", PROXY_RELEASES_URL = "https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest", PROXY_SUFFIXES;
74414
74576
  var init_install = __esm(() => {
74415
- SOULFORGE_DIR = join7(homedir5(), ".soulforge");
74416
- BIN_DIR = join7(SOULFORGE_DIR, "bin");
74417
- INSTALLS_DIR = join7(SOULFORGE_DIR, "installs");
74418
- FONTS_DIR = join7(SOULFORGE_DIR, "fonts");
74577
+ SOULFORGE_DIR = join8(homedir6(), ".soulforge");
74578
+ BIN_DIR = join8(SOULFORGE_DIR, "bin");
74579
+ INSTALLS_DIR = join8(SOULFORGE_DIR, "installs");
74580
+ FONTS_DIR = join8(SOULFORGE_DIR, "fonts");
74419
74581
  PROXY_SUFFIXES = {
74420
74582
  "darwin-arm64": "darwin_arm64",
74421
74583
  "darwin-x64": "darwin_amd64",
@@ -74426,80 +74588,51 @@ var init_install = __esm(() => {
74426
74588
 
74427
74589
  // src/core/proxy/lifecycle.ts
74428
74590
  import { execFileSync, execSync as execSync2, spawn as spawn4 } from "child_process";
74429
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
74430
- import { homedir as homedir6 } from "os";
74431
- import { join as join8 } from "path";
74591
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
74592
+ import { homedir as homedir7 } from "os";
74593
+ import { join as join9 } from "path";
74432
74594
  function setState(state, error48 = null) {
74433
74595
  currentState = state;
74434
74596
  lastError = error48;
74435
74597
  for (const fn of stateListeners)
74436
74598
  fn(state, error48);
74437
74599
  }
74438
- function getInstalledProxyVersion() {
74439
- try {
74440
- if (existsSync3(VERSION_FILE)) {
74441
- const v = readFileSync2(VERSION_FILE, "utf-8").trim();
74442
- if (v)
74443
- return v;
74444
- }
74445
- } catch {}
74446
- return PROXY_VERSION;
74447
- }
74448
74600
  function saveInstalledProxyVersion(version2) {
74449
74601
  mkdirSync3(PROXY_CONFIG_DIR, {
74450
74602
  recursive: true
74451
74603
  });
74452
74604
  writeFileSync2(VERSION_FILE, version2);
74453
74605
  }
74454
- function hasConflictingKeys(content) {
74455
- for (const line of content.split(`
74456
- `)) {
74457
- if (line.length === 0 || line[0] === "#" || line[0] === " " || line[0] === "\t")
74458
- continue;
74459
- const colon = line.indexOf(":");
74460
- if (colon === -1)
74461
- continue;
74462
- const key = line.slice(0, colon).trim();
74463
- if (PERF_KEYS.includes(key))
74464
- return true;
74465
- }
74466
- return false;
74606
+ function stripLegacyPerfBlock(content) {
74607
+ if (!content.includes(LEGACY_PERF_MARKER_PREFIX))
74608
+ return content;
74609
+ const lines = content.split(`
74610
+ `);
74611
+ const start = lines.findIndex((l) => l.startsWith(LEGACY_PERF_MARKER_PREFIX));
74612
+ if (start === -1)
74613
+ return content;
74614
+ let end = start + 1;
74615
+ while (end < lines.length && lines[end]?.trim() !== "")
74616
+ end++;
74617
+ lines.splice(start, end - start);
74618
+ return lines.join(`
74619
+ `);
74467
74620
  }
74468
74621
  function ensureConfig() {
74469
74622
  mkdirSync3(PROXY_CONFIG_DIR, {
74470
74623
  recursive: true
74471
74624
  });
74472
- if (!existsSync3(PROXY_CONFIG_PATH)) {
74473
- writeFileSync2(PROXY_CONFIG_PATH, ["host: 127.0.0.1", "port: 8317", 'auth-dir: "~/.cli-proxy-api"', "api-keys:", ' - "soulforge"', "", PERF_BLOCK, ""].join(`
74625
+ if (!existsSync4(PROXY_CONFIG_PATH)) {
74626
+ writeFileSync2(PROXY_CONFIG_PATH, ["host: 127.0.0.1", "port: 8317", 'auth-dir: "~/.cli-proxy-api"', "api-keys:", ' - "soulforge"', ""].join(`
74474
74627
  `));
74475
74628
  return;
74476
74629
  }
74477
74630
  try {
74478
- const existing = readFileSync2(PROXY_CONFIG_PATH, "utf-8");
74479
- if (existing.includes(PERF_MARKER))
74480
- return;
74481
- let cleaned = existing;
74482
- if (existing.includes(PERF_MARKER_PREFIX)) {
74483
- const lines = existing.split(`
74484
- `);
74485
- const start = lines.findIndex((l) => l.startsWith(PERF_MARKER_PREFIX));
74486
- if (start !== -1) {
74487
- let end = start + 1;
74488
- while (end < lines.length && lines[end]?.trim() !== "")
74489
- end++;
74490
- lines.splice(start, end - start);
74491
- cleaned = lines.join(`
74492
- `);
74493
- }
74631
+ const existing = readFileSync3(PROXY_CONFIG_PATH, "utf-8");
74632
+ const cleaned = stripLegacyPerfBlock(existing);
74633
+ if (cleaned !== existing) {
74634
+ writeFileSync2(PROXY_CONFIG_PATH, cleaned);
74494
74635
  }
74495
- if (hasConflictingKeys(cleaned))
74496
- return;
74497
- const sep = cleaned.endsWith(`
74498
- `) ? "" : `
74499
- `;
74500
- writeFileSync2(PROXY_CONFIG_PATH, `${cleaned}${sep}
74501
- ${PERF_BLOCK}
74502
- `);
74503
74636
  } catch {}
74504
74637
  }
74505
74638
  function commandExists(cmd) {
@@ -74512,24 +74645,76 @@ function commandExists(cmd) {
74512
74645
  return false;
74513
74646
  }
74514
74647
  }
74648
+ function getBinaryVersion(binary) {
74649
+ const parse5 = (s) => {
74650
+ const m = s.match(/Version:\s*(\d+\.\d+\.\d+)/);
74651
+ return m?.[1] ?? null;
74652
+ };
74653
+ try {
74654
+ const out = execFileSync(binary, ["-help"], {
74655
+ encoding: "utf-8",
74656
+ timeout: 2000,
74657
+ stdio: ["ignore", "pipe", "pipe"]
74658
+ });
74659
+ return parse5(out);
74660
+ } catch (err) {
74661
+ const e = err;
74662
+ const stdout = typeof e.stdout === "string" ? e.stdout : e.stdout?.toString() ?? "";
74663
+ const stderr = typeof e.stderr === "string" ? e.stderr : e.stderr?.toString() ?? "";
74664
+ return parse5(stdout + stderr);
74665
+ }
74666
+ }
74667
+ function compareVersions(a, b) {
74668
+ const ap = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
74669
+ const bp = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
74670
+ const len = Math.max(ap.length, bp.length);
74671
+ for (let i = 0;i < len; i++) {
74672
+ const diff = (ap[i] ?? 0) - (bp[i] ?? 0);
74673
+ if (diff !== 0)
74674
+ return diff;
74675
+ }
74676
+ return 0;
74677
+ }
74515
74678
  function getProxyBinary() {
74679
+ const systemBinary = commandExists("cli-proxy-api") ? "cli-proxy-api" : commandExists("cliproxyapi") ? "cliproxyapi" : null;
74516
74680
  const vendored = getVendoredPath("cli-proxy-api");
74517
- if (vendored)
74518
- return vendored;
74519
- if (commandExists("cli-proxy-api"))
74520
- return "cli-proxy-api";
74521
- if (commandExists("cliproxyapi"))
74522
- return "cliproxyapi";
74523
- return null;
74681
+ if (systemBinary && vendored) {
74682
+ const sysVersion = getBinaryVersion(systemBinary);
74683
+ const vendoredVersion = getBinaryVersion(vendored);
74684
+ if (sysVersion && vendoredVersion) {
74685
+ if (compareVersions(sysVersion, vendoredVersion) >= 0)
74686
+ return systemBinary;
74687
+ logBackgroundError("CLIProxyAPI", `system binary v${sysVersion} is older than vendored v${vendoredVersion} \u2014 using vendored`);
74688
+ return vendored;
74689
+ }
74690
+ return systemBinary;
74691
+ }
74692
+ return systemBinary ?? vendored;
74524
74693
  }
74525
- async function healthCheck() {
74694
+ function portIsOccupied() {
74695
+ const portMatch = PROXY_URL.match(/:([0-9]+)/);
74696
+ if (!portMatch)
74697
+ return false;
74698
+ const port = portMatch[1];
74699
+ try {
74700
+ const out = execFileSync("lsof", ["-ti", `tcp:${port}`], {
74701
+ encoding: "utf-8",
74702
+ timeout: 3000,
74703
+ stdio: ["ignore", "pipe", "ignore"]
74704
+ }).trim();
74705
+ return out.length > 0;
74706
+ } catch {
74707
+ return false;
74708
+ }
74709
+ }
74710
+ async function healthCheck(key) {
74526
74711
  try {
74527
74712
  const controller = new AbortController;
74528
74713
  const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
74529
74714
  const res = await fetch(`${PROXY_URL}/models`, {
74530
74715
  signal: controller.signal,
74531
74716
  headers: {
74532
- Authorization: `Bearer ${PROXY_API_KEY}`
74717
+ Authorization: `Bearer ${key}`
74533
74718
  }
74534
74719
  });
74535
74720
  clearTimeout(timeout);
@@ -74542,6 +74727,33 @@ async function healthCheck() {
74542
74727
  return "unreachable";
74543
74728
  }
74544
74729
  }
74730
+ async function probeForWorkingKey() {
74731
+ let sawAuthRequired = false;
74732
+ let sawUnreachable = false;
74733
+ for (const candidate of candidateApiKeys()) {
74734
+ const r = await healthCheck(candidate);
74735
+ if (r === "ok")
74736
+ return {
74737
+ key: candidate,
74738
+ state: "ok"
74739
+ };
74740
+ if (r === "auth-required")
74741
+ sawAuthRequired = true;
74742
+ if (r === "unreachable")
74743
+ sawUnreachable = true;
74744
+ }
74745
+ if (sawAuthRequired)
74746
+ return {
74747
+ state: "auth-required"
74748
+ };
74749
+ if (sawUnreachable)
74750
+ return {
74751
+ state: "unreachable"
74752
+ };
74753
+ return {
74754
+ state: "unreachable"
74755
+ };
74756
+ }
74545
74757
  async function ensureProxy() {
74546
74758
  if (currentState === "starting") {
74547
74759
  return {
@@ -74549,32 +74761,43 @@ async function ensureProxy() {
74549
74761
  error: "Proxy is already starting"
74550
74762
  };
74551
74763
  }
74552
- const installed = getInstalledProxyVersion();
74553
- const needsUpgrade = installed !== PROXY_VERSION;
74554
- if (needsUpgrade) {
74555
- stopProxy();
74556
- killProxyOnPort();
74557
- }
74558
- const health = await healthCheck();
74559
- if (health === "ok" && !needsUpgrade) {
74764
+ const probe = await probeForWorkingKey();
74765
+ if (probe.state === "ok") {
74766
+ setActiveProxyApiKey(probe.key);
74560
74767
  setState("running");
74561
74768
  return {
74562
74769
  ok: true
74563
74770
  };
74564
74771
  }
74565
- if (health === "auth-required") {
74772
+ if (probe.state === "auth-required") {
74773
+ const cfg = primaryConfigPath();
74774
+ const discovered = discoverApiKeys();
74775
+ if (cfg && discovered.length === 0) {
74776
+ const msg = `Proxy rejected every candidate API key. Edit ${cfg} (replace placeholder in \`api-keys:\`) or set PROXY_API_KEY, then restart the proxy.`;
74777
+ setState("needs-auth", msg);
74778
+ return {
74779
+ ok: false,
74780
+ error: msg
74781
+ };
74782
+ }
74566
74783
  setState("needs-auth", "Authentication required \u2014 run /proxy login");
74567
74784
  return {
74568
74785
  ok: false,
74569
74786
  error: "Authentication required \u2014 run /proxy login"
74570
74787
  };
74571
74788
  }
74789
+ if (portIsOccupied()) {
74790
+ logBackgroundError("CLIProxyAPI", "orphan process on port \u2014 clearing");
74791
+ killProxyOnPort();
74792
+ await new Promise((r) => setTimeout(r, 200));
74793
+ }
74572
74794
  setState("starting");
74573
74795
  let binary = getProxyBinary();
74574
74796
  if (!binary) {
74575
74797
  try {
74576
- binary = await installProxy();
74577
- saveInstalledProxyVersion(PROXY_VERSION);
74798
+ const installed = await installProxy();
74799
+ binary = installed.path;
74800
+ saveInstalledProxyVersion(installed.version);
74578
74801
  } catch (err) {
74579
74802
  const msg = toErrorMessage(err);
74580
74803
  setState("error", `Failed to install CLIProxyAPI: ${msg}`);
@@ -74620,7 +74843,7 @@ async function ensureProxy() {
74620
74843
  }
74621
74844
  for (let i = 0;i < STARTUP_POLL_ATTEMPTS; i++) {
74622
74845
  await new Promise((r) => setTimeout(r, STARTUP_POLL_MS));
74623
- const status = await healthCheck();
74846
+ const status = await healthCheck(getActiveProxyApiKey());
74624
74847
  if (status === "ok") {
74625
74848
  setState("running");
74626
74849
  return {
@@ -74657,7 +74880,7 @@ function stopProxy() {
74657
74880
  killProxyOnPort();
74658
74881
  setState("stopped");
74659
74882
  }
74660
- function killProxyOnPort() {
74883
+ function killProxyOnPort(force = false) {
74661
74884
  const portMatch = PROXY_URL.match(/:([0-9]+)/);
74662
74885
  if (!portMatch)
74663
74886
  return;
@@ -74666,13 +74889,15 @@ function killProxyOnPort() {
74666
74889
  try {
74667
74890
  out = execFileSync("lsof", ["-ti", `tcp:${port}`], {
74668
74891
  encoding: "utf-8",
74669
- timeout: 3000
74892
+ timeout: 3000,
74893
+ stdio: ["ignore", "pipe", "ignore"]
74670
74894
  }).trim();
74671
74895
  } catch {
74672
74896
  try {
74673
74897
  out = execFileSync("fuser", [`${port}/tcp`], {
74674
74898
  encoding: "utf-8",
74675
- timeout: 3000
74899
+ timeout: 3000,
74900
+ stdio: ["ignore", "pipe", "ignore"]
74676
74901
  }).trim();
74677
74902
  } catch {
74678
74903
  return;
@@ -74680,31 +74905,28 @@ function killProxyOnPort() {
74680
74905
  }
74681
74906
  if (!out)
74682
74907
  return;
74908
+ const signal = force ? "SIGKILL" : "SIGTERM";
74683
74909
  for (const token of out.split(/[\s\n]+/)) {
74684
74910
  const pid = Number.parseInt(token.trim(), 10);
74685
74911
  if (pid > 0 && pid !== process.pid) {
74686
74912
  try {
74687
- process.kill(pid, "SIGTERM");
74913
+ process.kill(pid, signal);
74688
74914
  } catch {}
74689
74915
  }
74690
74916
  }
74691
74917
  }
74692
- var proxyProcess = null, PROXY_URL, PROXY_API_KEY, PROXY_CONFIG_DIR, PROXY_CONFIG_PATH, HEALTH_TIMEOUT_MS = 2000, STARTUP_POLL_MS = 500, STARTUP_POLL_ATTEMPTS = 10, currentState = "stopped", lastError = null, stateListeners, VERSION_FILE, PERF_MARKER_PREFIX = "# soulforge-perf-defaults", PERF_MARKER_VERSION = 1, PERF_MARKER, PERF_KEYS, PERF_BLOCK, AUTH_DIR, VERSION_CACHE_TTL;
74918
+ var proxyProcess = null, PROXY_URL, PROXY_CONFIG_DIR, PROXY_CONFIG_PATH, HEALTH_TIMEOUT_MS = 2000, STARTUP_POLL_MS = 500, STARTUP_POLL_ATTEMPTS = 10, currentState = "stopped", lastError = null, stateListeners, VERSION_FILE, LEGACY_PERF_MARKER_PREFIX = "# soulforge-perf-defaults", AUTH_DIR, VERSION_CACHE_TTL;
74693
74919
  var init_lifecycle = __esm(() => {
74694
74920
  init_errors4();
74695
74921
  init_process_tracker();
74696
74922
  init_install();
74923
+ init_key_resolver();
74697
74924
  PROXY_URL = process.env.PROXY_API_URL || "http://127.0.0.1:8317/v1";
74698
- PROXY_API_KEY = process.env.PROXY_API_KEY || "soulforge";
74699
- PROXY_CONFIG_DIR = join8(homedir6(), ".soulforge", "proxy");
74700
- PROXY_CONFIG_PATH = join8(PROXY_CONFIG_DIR, "config.yaml");
74925
+ PROXY_CONFIG_DIR = join9(homedir7(), ".soulforge", "proxy");
74926
+ PROXY_CONFIG_PATH = join9(PROXY_CONFIG_DIR, "config.yaml");
74701
74927
  stateListeners = new Set;
74702
- VERSION_FILE = join8(PROXY_CONFIG_DIR, "version");
74703
- PERF_MARKER = `${PERF_MARKER_PREFIX} v${String(PERF_MARKER_VERSION)}`;
74704
- PERF_KEYS = ["request-retry", "max-retry-interval", "max-retry-credentials", "streaming", "nonstream-keepalive-interval"];
74705
- PERF_BLOCK = [PERF_MARKER, "request-retry: 1", "max-retry-interval: 10", "max-retry-credentials: 2", "streaming:", " keepalive-seconds: 15", " bootstrap-retries: 1", "nonstream-keepalive-interval: 30"].join(`
74706
- `);
74707
- AUTH_DIR = join8(homedir6(), ".cli-proxy-api");
74928
+ VERSION_FILE = join9(PROXY_CONFIG_DIR, "version");
74929
+ AUTH_DIR = join9(homedir7(), ".cli-proxy-api");
74708
74930
  VERSION_CACHE_TTL = 10 * 60 * 1000;
74709
74931
  });
74710
74932
 
@@ -74712,14 +74934,14 @@ var init_lifecycle = __esm(() => {
74712
74934
  function isAnthropicModel2(modelId) {
74713
74935
  return modelId.toLowerCase().startsWith("claude");
74714
74936
  }
74715
- var baseURL, apiKey, proxy;
74937
+ var baseURL, proxy;
74716
74938
  var init_proxy = __esm(() => {
74717
74939
  init_dist6();
74718
74940
  init_dist8();
74941
+ init_key_resolver();
74719
74942
  init_lifecycle();
74720
74943
  init_context_windows();
74721
74944
  baseURL = process.env.PROXY_API_URL || "http://127.0.0.1:8317/v1";
74722
- apiKey = process.env.PROXY_API_KEY || "soulforge";
74723
74945
  proxy = {
74724
74946
  id: "proxy",
74725
74947
  name: "Proxy",
@@ -74728,6 +74950,7 @@ var init_proxy = __esm(() => {
74728
74950
  asciiIcon: "\u26E8",
74729
74951
  grouped: true,
74730
74952
  createModel(modelId) {
74953
+ const apiKey = getActiveProxyApiKey();
74731
74954
  if (isAnthropicModel2(modelId)) {
74732
74955
  return createAnthropic({
74733
74956
  baseURL,
@@ -74838,8 +75061,8 @@ var init_vercel_gateway = __esm(() => {
74838
75061
  description: "Vercel AI Gateway",
74839
75062
  grouped: true,
74840
75063
  createModel(modelId) {
74841
- const apiKey2 = getProviderApiKey("AI_GATEWAY_API_KEY");
74842
- if (!apiKey2) {
75064
+ const apiKey = getProviderApiKey("AI_GATEWAY_API_KEY");
75065
+ if (!apiKey) {
74843
75066
  throw new Error("AI_GATEWAY_API_KEY is not set");
74844
75067
  }
74845
75068
  return gateway(modelId);
@@ -77807,21 +78030,21 @@ var init_xai = __esm(() => {
77807
78030
  asciiIcon: "X",
77808
78031
  description: "Grok models",
77809
78032
  createModel(modelId) {
77810
- const apiKey2 = getProviderApiKey("XAI_API_KEY");
77811
- if (!apiKey2) {
78033
+ const apiKey = getProviderApiKey("XAI_API_KEY");
78034
+ if (!apiKey) {
77812
78035
  throw new Error("XAI_API_KEY is not set");
77813
78036
  }
77814
78037
  return createXai({
77815
- apiKey: apiKey2
78038
+ apiKey
77816
78039
  })(modelId);
77817
78040
  },
77818
78041
  async fetchModels() {
77819
- const apiKey2 = getProviderApiKey("XAI_API_KEY");
77820
- if (!apiKey2)
78042
+ const apiKey = getProviderApiKey("XAI_API_KEY");
78043
+ if (!apiKey)
77821
78044
  return null;
77822
78045
  const res = await fetch("https://api.x.ai/v1/models", {
77823
78046
  headers: {
77824
- Authorization: `Bearer ${apiKey2}`
78047
+ Authorization: `Bearer ${apiKey}`
77825
78048
  }
77826
78049
  });
77827
78050
  if (!res.ok)
@@ -78120,9 +78343,9 @@ var init_summarize = __esm(() => {
78120
78343
  });
78121
78344
 
78122
78345
  // src/core/workers/io.worker.ts
78123
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync3, statSync } from "fs";
78346
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync4, statSync } from "fs";
78124
78347
  import { readFile, rename, writeFile as writeFile2 } from "fs/promises";
78125
- import { extname, join as join9 } from "path";
78348
+ import { extname, join as join10 } from "path";
78126
78349
 
78127
78350
  // node_modules/isbinaryfile/lib/index.js
78128
78351
  import { open, stat } from "fs/promises";
@@ -78496,7 +78719,7 @@ var handlers = {
78496
78719
  const dir = sessionDir;
78497
78720
  const sessionMeta = meta3;
78498
78721
  const entries = tabEntries;
78499
- if (!existsSync4(dir)) {
78722
+ if (!existsSync5(dir)) {
78500
78723
  mkdirSync4(dir, {
78501
78724
  recursive: true,
78502
78725
  mode: 448
@@ -78524,8 +78747,8 @@ var handlers = {
78524
78747
  const metaJson = JSON.stringify(updatedMeta, null, 2);
78525
78748
  const lines = allMessages.map((m) => JSON.stringify(m)).join(`
78526
78749
  `);
78527
- const metaPath = join9(dir, "meta.json");
78528
- const jsonlPath = join9(dir, "messages.jsonl");
78750
+ const metaPath = join10(dir, "meta.json");
78751
+ const jsonlPath = join10(dir, "messages.jsonl");
78529
78752
  const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
78530
78753
  const metaTmp = `${metaPath}.${suffix}.tmp`;
78531
78754
  const jsonlTmp = `${jsonlPath}.${suffix}.tmp`;
@@ -78546,7 +78769,7 @@ var handlers = {
78546
78769
  for (const [tabId, msgs] of cores) {
78547
78770
  coreData[tabId] = msgs;
78548
78771
  }
78549
- const corePath = join9(dir, "core.json");
78772
+ const corePath = join10(dir, "core.json");
78550
78773
  const coreTmp = `${corePath}.${suffix}.tmp`;
78551
78774
  await writeFile2(coreTmp, JSON.stringify(coreData), {
78552
78775
  encoding: "utf-8",
@@ -78557,14 +78780,14 @@ var handlers = {
78557
78780
  },
78558
78781
  loadSession: async (sessionDir) => {
78559
78782
  const dir = sessionDir;
78560
- const metaPath = join9(dir, "meta.json");
78561
- if (!existsSync4(metaPath))
78783
+ const metaPath = join10(dir, "meta.json");
78784
+ if (!existsSync5(metaPath))
78562
78785
  return null;
78563
- const meta3 = JSON.parse(readFileSync3(metaPath, "utf-8"));
78564
- const jsonlPath = join9(dir, "messages.jsonl");
78786
+ const meta3 = JSON.parse(readFileSync4(metaPath, "utf-8"));
78787
+ const jsonlPath = join10(dir, "messages.jsonl");
78565
78788
  const allMessages = [];
78566
- if (existsSync4(jsonlPath)) {
78567
- const content = readFileSync3(jsonlPath, "utf-8").trim();
78789
+ if (existsSync5(jsonlPath)) {
78790
+ const content = readFileSync4(jsonlPath, "utf-8").trim();
78568
78791
  if (content) {
78569
78792
  for (const line of content.split(`
78570
78793
  `)) {
@@ -78586,11 +78809,11 @@ var handlers = {
78586
78809
  } = tab.messageRange;
78587
78810
  tabEntries.push([tab.id, allMessages.slice(startLine, endLine)]);
78588
78811
  }
78589
- const corePath = join9(dir, "core.json");
78812
+ const corePath = join10(dir, "core.json");
78590
78813
  let coreEntries;
78591
- if (existsSync4(corePath)) {
78814
+ if (existsSync5(corePath)) {
78592
78815
  try {
78593
- const coreData = JSON.parse(readFileSync3(corePath, "utf-8"));
78816
+ const coreData = JSON.parse(readFileSync4(corePath, "utf-8"));
78594
78817
  coreEntries = Object.entries(coreData);
78595
78818
  } catch {}
78596
78819
  }
@@ -78602,27 +78825,27 @@ var handlers = {
78602
78825
  },
78603
78826
  listSessions: (sessionsDir) => {
78604
78827
  const dir = sessionsDir;
78605
- if (!existsSync4(dir))
78828
+ if (!existsSync5(dir))
78606
78829
  return [];
78607
78830
  try {
78608
78831
  const entries = readdirSync4(dir);
78609
78832
  const metas = [];
78610
78833
  for (const entry of entries) {
78611
78834
  try {
78612
- const fullPath = join9(dir, entry);
78835
+ const fullPath = join10(dir, entry);
78613
78836
  const s = statSync(fullPath);
78614
78837
  if (!s.isDirectory())
78615
78838
  continue;
78616
- const metaPath = join9(fullPath, "meta.json");
78617
- if (!existsSync4(metaPath))
78839
+ const metaPath = join10(fullPath, "meta.json");
78840
+ if (!existsSync5(metaPath))
78618
78841
  continue;
78619
- const raw = readFileSync3(metaPath, "utf-8");
78842
+ const raw = readFileSync4(metaPath, "utf-8");
78620
78843
  const meta3 = JSON.parse(raw);
78621
78844
  const totalMessages = (meta3.tabs ?? []).reduce((sum, t) => sum + (t.messageRange.endLine - t.messageRange.startLine), 0);
78622
78845
  let sizeBytes = 0;
78623
78846
  for (const file2 of ["meta.json", "messages.jsonl"]) {
78624
78847
  try {
78625
- sizeBytes += statSync(join9(fullPath, file2)).size;
78848
+ sizeBytes += statSync(join10(fullPath, file2)).size;
78626
78849
  } catch {}
78627
78850
  }
78628
78851
  metas.push({