@wrongstack/webui 0.63.4 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  2. package/dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  3. package/dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  4. package/dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  5. package/dist/assets/ibm-plex-mono-cyrillic-600-normal-CTOM6hUh.woff2 +0 -0
  6. package/dist/assets/ibm-plex-mono-cyrillic-600-normal-fLZuRloM.woff +0 -0
  7. package/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  8. package/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  9. package/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  10. package/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  11. package/dist/assets/ibm-plex-mono-cyrillic-ext-600-normal-9HEixskS.woff +0 -0
  12. package/dist/assets/ibm-plex-mono-cyrillic-ext-600-normal-V-xxqcpd.woff2 +0 -0
  13. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  14. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  15. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  16. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  17. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  18. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  19. package/dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  20. package/dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  21. package/dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  22. package/dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  23. package/dist/assets/ibm-plex-mono-latin-ext-600-normal-D38SheWl.woff2 +0 -0
  24. package/dist/assets/ibm-plex-mono-latin-ext-600-normal-DmB0ttJJ.woff +0 -0
  25. package/dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  26. package/dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  27. package/dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  28. package/dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  29. package/dist/assets/ibm-plex-mono-vietnamese-600-normal-D2EvbN8M.woff2 +0 -0
  30. package/dist/assets/ibm-plex-mono-vietnamese-600-normal-iLQfcSjf.woff +0 -0
  31. package/dist/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  32. package/dist/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  33. package/dist/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  34. package/dist/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  35. package/dist/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  36. package/dist/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  37. package/dist/assets/index-BeXRAkSS.js +94 -0
  38. package/dist/assets/index-C_0-qbQ-.css +1 -0
  39. package/dist/assets/{vendor-oYD55Pw4.js → vendor-CzdG0ns2.js} +88 -88
  40. package/dist/assets/vendor-DW1jimNH.css +1 -0
  41. package/dist/index.css +333 -214
  42. package/dist/index.css.map +1 -1
  43. package/dist/index.html +4 -3
  44. package/dist/index.js +2769 -2832
  45. package/dist/index.js.map +1 -1
  46. package/dist/server/entry.js +479 -255
  47. package/dist/server/entry.js.map +1 -1
  48. package/dist/server/index.d.ts +298 -2
  49. package/dist/server/index.js +480 -225
  50. package/dist/server/index.js.map +1 -1
  51. package/package.json +9 -6
  52. package/dist/assets/index-5ECutVTP.css +0 -1
  53. package/dist/assets/index-BRHGqfHg.js +0 -94
@@ -1,6 +1,6 @@
1
1
  // src/server/index.ts
2
- import * as fs2 from "fs/promises";
3
- import * as path2 from "path";
2
+ import * as fs4 from "fs/promises";
3
+ import * as path4 from "path";
4
4
 
5
5
  // src/server/http-server.ts
6
6
  import * as fs from "fs/promises";
@@ -15,6 +15,16 @@ var MIME_TYPES = {
15
15
  ".png": "image/png",
16
16
  ".ico": "image/x-icon"
17
17
  };
18
+ function injectWsPort(html, wsPort) {
19
+ const tag = `<meta name="wrongstack-ws-port" content="${wsPort}" />`;
20
+ if (html.includes('name="wrongstack-ws-port"')) return html;
21
+ if (html.includes("</head>")) {
22
+ return html.replace("</head>", ` ${tag}
23
+ </head>`);
24
+ }
25
+ return `${tag}
26
+ ${html}`;
27
+ }
18
28
  function buildCspHeader(wsPort) {
19
29
  return `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://127.0.0.1:${wsPort} wss://127.0.0.1:${wsPort} ws://[::1]:${wsPort} wss://[::1]:${wsPort}; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
20
30
  }
@@ -55,6 +65,10 @@ function createHttpServer(opts) {
55
65
  if (ext === ".html") {
56
66
  res.setHeader("Cache-Control", "no-cache");
57
67
  res.setHeader("Content-Security-Policy", buildCspHeader(wsPort));
68
+ const html = await fs.readFile(resolvedPath, "utf8");
69
+ res.writeHead(200);
70
+ res.end(injectWsPort(html, wsPort));
71
+ return;
58
72
  }
59
73
  const fileContent = await fs.readFile(resolvedPath);
60
74
  res.writeHead(200);
@@ -62,7 +76,7 @@ function createHttpServer(opts) {
62
76
  } catch (err) {
63
77
  if (err.code === "ENOENT") {
64
78
  try {
65
- const fileContent = await fs.readFile(path.join(distDir, "index.html"));
79
+ const html = await fs.readFile(path.join(distDir, "index.html"), "utf8");
66
80
  res.writeHead(200, {
67
81
  "Content-Type": "text/html",
68
82
  "X-Content-Type-Options": "nosniff",
@@ -70,7 +84,7 @@ function createHttpServer(opts) {
70
84
  "Referrer-Policy": "strict-origin-when-cross-origin",
71
85
  "Content-Security-Policy": buildCspHeader(wsPort)
72
86
  });
73
- res.end(fileContent);
87
+ res.end(injectWsPort(html, wsPort));
74
88
  } catch {
75
89
  res.writeHead(404);
76
90
  res.end("Not found");
@@ -154,7 +168,7 @@ import {
154
168
  ProviderRegistry,
155
169
  TOKENS as TOKENS2,
156
170
  ToolRegistry,
157
- atomicWrite,
171
+ atomicWrite as atomicWrite3,
158
172
  createDefaultPipelines,
159
173
  DEFAULT_CONTEXT_WINDOW_MODE_ID,
160
174
  DEFAULT_TOOLS_CONFIG,
@@ -163,11 +177,9 @@ import {
163
177
  resolveContextWindowPolicy
164
178
  } from "@wrongstack/core";
165
179
  import { ToolExecutor } from "@wrongstack/core/execution";
166
- import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
167
180
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
168
181
  import { builtinToolsPack, forgetTool, rememberTool } from "@wrongstack/tools";
169
- import { WebSocket, WebSocketServer } from "ws";
170
- import { randomBytes } from "crypto";
182
+ import { WebSocketServer } from "ws";
171
183
 
172
184
  // ../runtime/src/container.ts
173
185
  import {
@@ -227,6 +239,7 @@ function createDefaultContainer(opts) {
227
239
  trustFile: wpaths.projectTrust,
228
240
  yolo: opts.permission?.yolo ?? false,
229
241
  yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
242
+ confirmDestructive: opts.permission?.confirmDestructive ?? false,
230
243
  promptDelegate: opts.permission?.promptDelegate
231
244
  })
232
245
  );
@@ -1446,6 +1459,13 @@ function createShutdown(res) {
1446
1459
  }
1447
1460
  for (const ws of res.clients()) ws.close();
1448
1461
  for (const server of res.servers) server?.close();
1462
+ if (res.onShutdown) {
1463
+ try {
1464
+ await res.onShutdown();
1465
+ } catch (e) {
1466
+ log(`[WebUI] Error during shutdown cleanup: ${e instanceof Error ? e.message : String(e)}`);
1467
+ }
1468
+ }
1449
1469
  exit(0);
1450
1470
  };
1451
1471
  }
@@ -1459,6 +1479,138 @@ function registerShutdownHandlers(res) {
1459
1479
  };
1460
1480
  }
1461
1481
 
1482
+ // src/server/instance-registry.ts
1483
+ import * as os from "os";
1484
+ import * as path2 from "path";
1485
+ import * as fs2 from "fs/promises";
1486
+ import { atomicWrite } from "@wrongstack/core";
1487
+ function defaultBaseDir() {
1488
+ return path2.join(os.homedir(), ".wrongstack");
1489
+ }
1490
+ function registryPath(baseDir = defaultBaseDir()) {
1491
+ return path2.join(baseDir, "webui-instances.json");
1492
+ }
1493
+ function isPidAlive(pid) {
1494
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1495
+ try {
1496
+ process.kill(pid, 0);
1497
+ return true;
1498
+ } catch (err) {
1499
+ return err.code !== "ESRCH";
1500
+ }
1501
+ }
1502
+ async function load(file) {
1503
+ try {
1504
+ const raw = await fs2.readFile(file, "utf8");
1505
+ const parsed = JSON.parse(raw);
1506
+ if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
1507
+ return parsed;
1508
+ }
1509
+ } catch {
1510
+ }
1511
+ return { version: 1, instances: [] };
1512
+ }
1513
+ async function save(file, instances) {
1514
+ await atomicWrite(file, `${JSON.stringify({ version: 1, instances }, null, 2)}
1515
+ `, {
1516
+ mode: 384
1517
+ });
1518
+ }
1519
+ function prune(instances, excludePid) {
1520
+ return instances.filter((i) => i.pid !== excludePid && isPidAlive(i.pid));
1521
+ }
1522
+ async function registerInstance(record, baseDir = defaultBaseDir()) {
1523
+ const file = registryPath(baseDir);
1524
+ const data = await load(file);
1525
+ const instances = prune(data.instances, record.pid);
1526
+ instances.push(record);
1527
+ await save(file, instances);
1528
+ }
1529
+ async function unregisterInstance(pid, baseDir = defaultBaseDir()) {
1530
+ const file = registryPath(baseDir);
1531
+ const data = await load(file);
1532
+ const instances = prune(data.instances, pid);
1533
+ await save(file, instances);
1534
+ }
1535
+ async function listInstances(baseDir = defaultBaseDir()) {
1536
+ const file = registryPath(baseDir);
1537
+ const data = await load(file);
1538
+ const live = prune(data.instances);
1539
+ if (live.length !== data.instances.length) {
1540
+ await save(file, live).catch(() => {
1541
+ });
1542
+ }
1543
+ return live;
1544
+ }
1545
+ function formatInstances(instances) {
1546
+ if (instances.length === 0) {
1547
+ return "No WebUI instances are currently running.";
1548
+ }
1549
+ const lines = [`Running WebUI instances (${instances.length}):`, ""];
1550
+ for (const i of instances) {
1551
+ lines.push(
1552
+ ` \u2022 ${i.url} \xB7 ws:${i.wsPort} \xB7 pid ${i.pid}`,
1553
+ ` project: ${i.projectName} (${i.projectRoot})`,
1554
+ ` since: ${i.startedAt}`
1555
+ );
1556
+ }
1557
+ return lines.join("\n");
1558
+ }
1559
+
1560
+ // src/server/port-utils.ts
1561
+ import * as net from "net";
1562
+ function isPortFree(host, port) {
1563
+ return new Promise((resolve3) => {
1564
+ const srv = net.createServer();
1565
+ srv.once("error", () => resolve3(false));
1566
+ srv.once("listening", () => {
1567
+ srv.close(() => resolve3(true));
1568
+ });
1569
+ try {
1570
+ srv.listen(port, host);
1571
+ } catch {
1572
+ resolve3(false);
1573
+ }
1574
+ });
1575
+ }
1576
+ async function findFreePort(host, startPort, opts = {}) {
1577
+ const exclude = opts.exclude ?? /* @__PURE__ */ new Set();
1578
+ const maxTries = opts.maxTries ?? 200;
1579
+ let port = startPort;
1580
+ for (let i = 0; i < maxTries; i++) {
1581
+ if (port > 65535) port = 1024 + port % 5e4;
1582
+ if (!exclude.has(port) && await isPortFree(host, port)) {
1583
+ return port;
1584
+ }
1585
+ port++;
1586
+ }
1587
+ throw new Error(
1588
+ `No free port found near ${startPort} on ${host} after ${maxTries} attempts.`
1589
+ );
1590
+ }
1591
+
1592
+ // src/server/open-browser.ts
1593
+ import { spawn } from "child_process";
1594
+ function browserOpenCommand(url, platform = process.platform) {
1595
+ if (platform === "win32") {
1596
+ return { command: "cmd", args: ["/c", "start", "", url] };
1597
+ }
1598
+ if (platform === "darwin") {
1599
+ return { command: "open", args: [url] };
1600
+ }
1601
+ return { command: "xdg-open", args: [url] };
1602
+ }
1603
+ function openBrowser(url, platform = process.platform) {
1604
+ try {
1605
+ const { command, args } = browserOpenCommand(url, platform);
1606
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
1607
+ child.on("error", () => {
1608
+ });
1609
+ child.unref();
1610
+ } catch {
1611
+ }
1612
+ }
1613
+
1462
1614
  // src/server/usage-cost.ts
1463
1615
  function getCostRates(model) {
1464
1616
  const cost = model?.cost;
@@ -1472,6 +1624,68 @@ function computeUsageCost(usage, rates) {
1472
1624
  return (usage.input * rates.input + usage.output * rates.output + (usage.cacheRead ?? 0) * rates.cacheRead) / 1e6;
1473
1625
  }
1474
1626
 
1627
+ // src/server/provider-config-io.ts
1628
+ import * as fs3 from "fs/promises";
1629
+ import * as path3 from "path";
1630
+ import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
1631
+ import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
1632
+ import { DefaultSecretVault } from "@wrongstack/core";
1633
+ async function loadSavedProviders(configPath, vault) {
1634
+ let raw;
1635
+ try {
1636
+ raw = await fs3.readFile(configPath, "utf8");
1637
+ } catch {
1638
+ return {};
1639
+ }
1640
+ let parsed = {};
1641
+ try {
1642
+ parsed = JSON.parse(raw);
1643
+ } catch {
1644
+ return {};
1645
+ }
1646
+ if (!parsed.providers) return {};
1647
+ return decryptConfigSecrets(parsed.providers, vault);
1648
+ }
1649
+ async function saveProviders(configPath, vault, providers) {
1650
+ let raw;
1651
+ let fileExists = true;
1652
+ try {
1653
+ raw = await fs3.readFile(configPath, "utf8");
1654
+ } catch (err) {
1655
+ if (err.code !== "ENOENT") {
1656
+ throw new Error(
1657
+ `Refusing to mutate ${configPath}: ${err.message}`,
1658
+ { cause: err }
1659
+ );
1660
+ }
1661
+ fileExists = false;
1662
+ raw = "{}";
1663
+ }
1664
+ let parsed;
1665
+ try {
1666
+ parsed = JSON.parse(raw);
1667
+ } catch (err) {
1668
+ if (fileExists) {
1669
+ throw new Error(
1670
+ `Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
1671
+ { cause: err }
1672
+ );
1673
+ }
1674
+ parsed = {};
1675
+ }
1676
+ parsed.providers = providers;
1677
+ const encrypted = encryptConfigSecrets(parsed, vault);
1678
+ await atomicWrite2(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
1679
+ }
1680
+ function createProviderConfigIO(configPath) {
1681
+ const keyFile = path3.join(path3.dirname(configPath), ".key");
1682
+ const vault = new DefaultSecretVault({ keyFile });
1683
+ return {
1684
+ load: () => loadSavedProviders(configPath, vault),
1685
+ save: (providers) => saveProviders(configPath, vault, providers)
1686
+ };
1687
+ }
1688
+
1475
1689
  // src/server/provider-keys.ts
1476
1690
  function normalizeKeys(cfg) {
1477
1691
  if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
@@ -1567,6 +1781,159 @@ function removeProvider(providers, providerId) {
1567
1781
  return { ok: true, message: `Provider "${providerId}" removed` };
1568
1782
  }
1569
1783
 
1784
+ // src/server/ws-utils.ts
1785
+ import { randomBytes } from "crypto";
1786
+ import { WebSocket } from "ws";
1787
+ function send(ws, msg) {
1788
+ if (ws.readyState === WebSocket.OPEN) {
1789
+ ws.send(JSON.stringify(msg));
1790
+ }
1791
+ }
1792
+ function broadcast(clients, msg) {
1793
+ const data = JSON.stringify(msg);
1794
+ for (const [ws] of clients) {
1795
+ if (ws.readyState === WebSocket.OPEN) {
1796
+ try {
1797
+ ws.send(data);
1798
+ } catch {
1799
+ }
1800
+ }
1801
+ }
1802
+ }
1803
+ function sendResult(ws, success, message) {
1804
+ send(ws, { type: "key.operation_result", payload: { success, message } });
1805
+ }
1806
+ function errMessage(err) {
1807
+ return err instanceof Error ? err.message : String(err);
1808
+ }
1809
+ function generateAuthToken() {
1810
+ return randomBytes(16).toString("hex");
1811
+ }
1812
+
1813
+ // src/server/provider-handlers.ts
1814
+ function createProviderHandlers(deps) {
1815
+ const { globalConfigPath, vault } = deps;
1816
+ let configWriteLock = deps.getConfigWriteLock();
1817
+ async function loadConfigProviders() {
1818
+ return loadSavedProviders(globalConfigPath, vault);
1819
+ }
1820
+ async function saveConfigProviders(providers) {
1821
+ const next = configWriteLock.then(() => saveProviders(globalConfigPath, vault, providers));
1822
+ configWriteLock = next;
1823
+ deps.setConfigWriteLock(next);
1824
+ await next;
1825
+ }
1826
+ async function handleKeyUpsert(ws, providerId, label, apiKey) {
1827
+ try {
1828
+ const providers = await loadConfigProviders();
1829
+ const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
1830
+ if (result.ok) await saveConfigProviders(providers);
1831
+ sendResult(ws, result.ok, result.message);
1832
+ } catch (err) {
1833
+ sendResult(ws, false, errMessage(err));
1834
+ }
1835
+ }
1836
+ async function handleKeyDelete(ws, providerId, label) {
1837
+ try {
1838
+ const providers = await loadConfigProviders();
1839
+ const result = deleteKey(providers, providerId, label);
1840
+ if (result.ok) await saveConfigProviders(providers);
1841
+ sendResult(ws, result.ok, result.message);
1842
+ } catch (err) {
1843
+ sendResult(ws, false, errMessage(err));
1844
+ }
1845
+ }
1846
+ async function handleKeySetActive(ws, providerId, label) {
1847
+ try {
1848
+ const providers = await loadConfigProviders();
1849
+ const result = setActiveKey(providers, providerId, label);
1850
+ if (result.ok) await saveConfigProviders(providers);
1851
+ sendResult(ws, result.ok, result.message);
1852
+ } catch (err) {
1853
+ sendResult(ws, false, errMessage(err));
1854
+ }
1855
+ }
1856
+ async function handleProviderAdd(ws, payload) {
1857
+ try {
1858
+ const providers = await loadConfigProviders();
1859
+ const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
1860
+ if (result.ok) await saveConfigProviders(providers);
1861
+ sendResult(ws, result.ok, result.message);
1862
+ } catch (err) {
1863
+ sendResult(ws, false, errMessage(err));
1864
+ }
1865
+ }
1866
+ async function handleProviderRemove(ws, providerId) {
1867
+ try {
1868
+ const providers = await loadConfigProviders();
1869
+ const result = removeProvider(providers, providerId);
1870
+ if (result.ok) await saveConfigProviders(providers);
1871
+ sendResult(ws, result.ok, result.message);
1872
+ } catch (err) {
1873
+ sendResult(ws, false, errMessage(err));
1874
+ }
1875
+ }
1876
+ return { handleKeyUpsert, handleKeyDelete, handleKeySetActive, handleProviderAdd, handleProviderRemove, loadConfigProviders };
1877
+ }
1878
+
1879
+ // src/server/setup-events.ts
1880
+ function setupEvents(deps) {
1881
+ const { events, broadcast: broadcast2, clients, config, context, pendingConfirms } = deps;
1882
+ events.on("iteration.started", (e) => {
1883
+ broadcast2(clients, {
1884
+ type: "iteration.started",
1885
+ payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
1886
+ });
1887
+ });
1888
+ events.on("provider.text_delta", (e) => {
1889
+ broadcast2(clients, { type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
1890
+ });
1891
+ events.on("provider.thinking_delta", (e) => {
1892
+ broadcast2(clients, { type: "provider.thinking_delta", payload: { text: e.text } });
1893
+ });
1894
+ events.on("tool.started", (e) => {
1895
+ broadcast2(clients, {
1896
+ type: "tool.started",
1897
+ payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
1898
+ });
1899
+ });
1900
+ events.on("tool.progress", (e) => {
1901
+ broadcast2(clients, {
1902
+ type: "tool.progress",
1903
+ payload: { id: e.id, name: e.name, eventType: e.event.type, text: e.event.text }
1904
+ });
1905
+ });
1906
+ events.on("tool.executed", (e) => {
1907
+ broadcast2(clients, {
1908
+ type: "tool.executed",
1909
+ payload: { id: e.id, name: e.name, durationMs: e.durationMs, ok: e.ok, input: e.input, output: e.output }
1910
+ });
1911
+ broadcast2(clients, { type: "todos.updated", payload: { todos: [...context.todos] } });
1912
+ });
1913
+ events.on("provider.response", (e) => {
1914
+ broadcast2(clients, { type: "provider.response", payload: { usage: e.usage, stopReason: e.stopReason, messageId: "current" } });
1915
+ });
1916
+ events.on("context.repaired", (e) => {
1917
+ broadcast2(clients, { type: "context.repaired", payload: { removedToolUses: e.removedToolUses, removedToolResults: e.removedToolResults, removedMessages: e.removedMessages } });
1918
+ });
1919
+ events.on("tool.confirm_needed", (e) => {
1920
+ const id = e.toolUseId ?? `confirm_${Date.now()}`;
1921
+ pendingConfirms.set(id, e.resolve);
1922
+ broadcast2(clients, { type: "tool.confirm_needed", payload: { id, toolName: e.tool?.name ?? "unknown", input: e.input, suggestedPattern: e.suggestedPattern } });
1923
+ });
1924
+ events.on("error", (e) => {
1925
+ broadcast2(clients, { type: "error", payload: { phase: e.phase, message: e.err instanceof Error ? e.err.message : String(e.err) } });
1926
+ });
1927
+ const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: { kind, ...payload } });
1928
+ events.on("subagent.spawned", (e) => forwardSubagent("spawned", { subagentId: e.subagentId, taskId: e.taskId, name: e.name, provider: e.provider, model: e.model, description: e.description }));
1929
+ events.on("subagent.task_started", (e) => forwardSubagent("task_started", { subagentId: e.subagentId, taskId: e.taskId, description: e.description }));
1930
+ events.on("subagent.tool_executed", (e) => forwardSubagent("tool_executed", { subagentId: e.subagentId, toolName: e.name, durationMs: e.durationMs, ok: e.ok }));
1931
+ events.on("subagent.iteration_summary", (e) => forwardSubagent("iteration_summary", { subagentId: e.subagentId, iteration: e.iteration, toolCalls: e.toolCalls, costUsd: e.costUsd, currentTool: e.currentTool }));
1932
+ events.on("subagent.budget_extended", (e) => forwardSubagent("budget_extended", { subagentId: e.subagentId, totalExtensions: e.totalExtensions }));
1933
+ events.on("subagent.ctx_pct", (e) => forwardSubagent("ctx_pct", { subagentId: e.subagentId, load: e.load, tokens: e.tokens, maxContext: e.maxContext }));
1934
+ events.on("subagent.task_completed", (e) => forwardSubagent("task_completed", { subagentId: e.subagentId, status: e.status, iterations: e.iterations, toolCalls: e.toolCalls, error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0 }));
1935
+ }
1936
+
1570
1937
  // src/server/token-estimator.ts
1571
1938
  function estimateTokens(s) {
1572
1939
  return Math.ceil(s.length / 4);
@@ -1625,12 +1992,23 @@ function estimateContextBreakdown(input) {
1625
1992
  }
1626
1993
 
1627
1994
  // src/server/index.ts
1628
- function errMessage(err) {
1629
- return err instanceof Error ? err.message : String(err);
1630
- }
1631
1995
  async function startWebUI(opts = {}) {
1632
- const wsPort = opts.wsPort ?? 3457;
1996
+ const requestedWsPort = opts.wsPort ?? 3457;
1633
1997
  const wsHost = opts.wsHost ?? "127.0.0.1";
1998
+ const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
1999
+ const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
2000
+ let wsPort = requestedWsPort;
2001
+ let httpPort = requestedHttpPort;
2002
+ if (!strictPort) {
2003
+ httpPort = await findFreePort(wsHost, requestedHttpPort);
2004
+ wsPort = await findFreePort(wsHost, requestedWsPort, { exclude: /* @__PURE__ */ new Set([httpPort]) });
2005
+ if (httpPort !== requestedHttpPort) {
2006
+ console.warn(`[WebUI] HTTP port ${requestedHttpPort} in use \u2192 using ${httpPort}`);
2007
+ }
2008
+ if (wsPort !== requestedWsPort) {
2009
+ console.warn(`[WebUI] WS port ${requestedWsPort} in use \u2192 using ${wsPort}`);
2010
+ }
2011
+ }
1634
2012
  console.log("[WebUI] Starting backend services...");
1635
2013
  const boot = await bootConfig();
1636
2014
  const { config: baseConfig, vault, globalConfigPath, projectRoot, wpaths, logger } = boot;
@@ -1884,14 +2262,14 @@ async function startWebUI(opts = {}) {
1884
2262
  inputCost,
1885
2263
  outputCost,
1886
2264
  cacheReadCost,
1887
- projectName: path2.basename(projectRoot) || projectRoot,
2265
+ projectName: path4.basename(projectRoot) || projectRoot,
1888
2266
  cwd: projectRoot,
1889
2267
  mode: modeId,
1890
2268
  contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
1891
2269
  wsToken
1892
2270
  };
1893
2271
  }
1894
- const wsToken = randomBytes(16).toString("hex");
2272
+ const wsToken = generateAuthToken();
1895
2273
  console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
1896
2274
  const verifyClient2 = (info) => verifyClient({
1897
2275
  origin: info.origin,
@@ -1915,10 +2293,11 @@ async function startWebUI(opts = {}) {
1915
2293
  maxPayload: WS_MAX_PAYLOAD
1916
2294
  }) : null;
1917
2295
  const clients = /* @__PURE__ */ new Map();
1918
- const RATE_LIMIT_MESSAGES = 60;
2296
+ const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
1919
2297
  const RATE_LIMIT_WINDOW_MS = 6e4;
1920
2298
  const rateLimits = /* @__PURE__ */ new Map();
1921
2299
  function checkRateLimit(ws, client) {
2300
+ if (RATE_LIMIT_MESSAGES <= 0) return true;
1922
2301
  const now = Date.now();
1923
2302
  const key = client.sessionId ?? String(ws);
1924
2303
  const limit = rateLimits.get(key);
@@ -1935,113 +2314,6 @@ async function startWebUI(opts = {}) {
1935
2314
  `[WebUI] WebSocket server running on ws://${wsHost}:${wsPort}` + (wssSecondary ? ` (and ws://[::1]:${wsPort})` : "")
1936
2315
  );
1937
2316
  const pendingConfirms = /* @__PURE__ */ new Map();
1938
- function setupEvents() {
1939
- events.on("iteration.started", (e) => {
1940
- broadcast({
1941
- type: "iteration.started",
1942
- payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
1943
- });
1944
- });
1945
- events.on("provider.text_delta", (e) => {
1946
- broadcast({ type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
1947
- });
1948
- events.on("provider.thinking_delta", (e) => {
1949
- broadcast({ type: "provider.thinking_delta", payload: { text: e.text } });
1950
- });
1951
- events.on("tool.started", (e) => {
1952
- broadcast({
1953
- type: "tool.started",
1954
- payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
1955
- });
1956
- });
1957
- events.on("tool.progress", (e) => {
1958
- broadcast({
1959
- type: "tool.progress",
1960
- payload: {
1961
- id: e.id,
1962
- name: e.name,
1963
- eventType: e.event.type,
1964
- text: e.event.text
1965
- }
1966
- });
1967
- });
1968
- events.on("tool.executed", (e) => {
1969
- broadcast({
1970
- type: "tool.executed",
1971
- payload: {
1972
- // Forward the tool_use id so frontend can correlate with the
1973
- // matching tool.started bubble — without this, parallel tool calls
1974
- // all stay stuck on "Running…" because the frontend can't tell
1975
- // which bubble this result belongs to.
1976
- id: e.id,
1977
- name: e.name,
1978
- durationMs: e.durationMs,
1979
- ok: e.ok,
1980
- input: e.input,
1981
- output: e.output
1982
- }
1983
- });
1984
- broadcast({
1985
- type: "todos.updated",
1986
- payload: { todos: [...context.todos] }
1987
- });
1988
- });
1989
- events.on("provider.response", (e) => {
1990
- broadcast({
1991
- type: "provider.response",
1992
- payload: {
1993
- usage: e.usage,
1994
- stopReason: e.stopReason,
1995
- messageId: "current"
1996
- }
1997
- });
1998
- });
1999
- events.on("context.repaired", (e) => {
2000
- broadcast({
2001
- type: "context.repaired",
2002
- payload: {
2003
- removedToolUses: e.removedToolUses,
2004
- removedToolResults: e.removedToolResults,
2005
- removedMessages: e.removedMessages
2006
- }
2007
- });
2008
- });
2009
- events.on("tool.confirm_needed", (e) => {
2010
- const id = e.toolUseId ?? `confirm_${Date.now()}`;
2011
- pendingConfirms.set(id, e.resolve);
2012
- broadcast({
2013
- type: "tool.confirm_needed",
2014
- payload: {
2015
- id,
2016
- toolName: e.tool?.name ?? "unknown",
2017
- input: e.input,
2018
- suggestedPattern: e.suggestedPattern
2019
- }
2020
- });
2021
- });
2022
- events.on("error", (e) => {
2023
- broadcast({
2024
- type: "error",
2025
- payload: {
2026
- phase: e.phase,
2027
- message: e.err instanceof Error ? e.err.message : String(e.err)
2028
- }
2029
- });
2030
- });
2031
- }
2032
- function send(ws, msg) {
2033
- if (ws.readyState === WebSocket.OPEN) {
2034
- ws.send(JSON.stringify(msg));
2035
- }
2036
- }
2037
- function broadcast(msg) {
2038
- const data = JSON.stringify(msg);
2039
- for (const [ws] of clients) {
2040
- if (ws.readyState === WebSocket.OPEN) {
2041
- ws.send(data);
2042
- }
2043
- }
2044
- }
2045
2317
  const handleConnection = (ws) => {
2046
2318
  const client = { ws, sessionId: session.id, connectedAt: Date.now() };
2047
2319
  clients.set(ws, client);
@@ -2099,7 +2371,7 @@ async function startWebUI(opts = {}) {
2099
2371
  if (eventsArmed) return;
2100
2372
  eventsArmed = true;
2101
2373
  console.log(`[WebUI] Backend ready (${label})`);
2102
- setupEvents();
2374
+ setupEvents({ events, broadcast, clients, config, context, pendingConfirms });
2103
2375
  };
2104
2376
  wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
2105
2377
  wssPrimary.on("connection", handleConnection);
@@ -2184,7 +2456,7 @@ async function startWebUI(opts = {}) {
2184
2456
  }
2185
2457
  case "abort":
2186
2458
  runLock?.abort();
2187
- broadcast({ type: "error", payload: { phase: "abort", message: "User aborted" } });
2459
+ broadcast(clients, { type: "error", payload: { phase: "abort", message: "User aborted" } });
2188
2460
  break;
2189
2461
  case "ping":
2190
2462
  send(ws, { type: "pong", payload: {} });
@@ -2203,7 +2475,7 @@ async function startWebUI(opts = {}) {
2203
2475
  context.fileMtimes.clear();
2204
2476
  tokenCounter.reset();
2205
2477
  sessionStartedAt = Date.now();
2206
- broadcast({ type: "session.start", payload: await sessionStartPayload() });
2478
+ broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
2207
2479
  break;
2208
2480
  }
2209
2481
  case "context.clear": {
@@ -2213,7 +2485,7 @@ async function startWebUI(opts = {}) {
2213
2485
  context.fileMtimes.clear();
2214
2486
  tokenCounter.reset();
2215
2487
  sendResult(ws, true, "Context cleared");
2216
- broadcast({
2488
+ broadcast(clients, {
2217
2489
  type: "session.start",
2218
2490
  payload: { ...await sessionStartPayload(), reset: true }
2219
2491
  });
@@ -2272,7 +2544,7 @@ async function startWebUI(opts = {}) {
2272
2544
  beforeMessages,
2273
2545
  afterMessages: context.messages.length
2274
2546
  };
2275
- broadcast({ type: "context.repaired", payload });
2547
+ broadcast(clients, { type: "context.repaired", payload });
2276
2548
  const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
2277
2549
  sendResult(
2278
2550
  ws,
@@ -2310,7 +2582,7 @@ async function startWebUI(opts = {}) {
2310
2582
  context.meta["contextWindowMode"] = policy.id;
2311
2583
  context.meta["contextWindowPolicy"] = policy;
2312
2584
  sendResult(ws, true, `Context mode switched to ${policy.id}`);
2313
- broadcast({
2585
+ broadcast(clients, {
2314
2586
  type: "context.mode.changed",
2315
2587
  payload: { id: policy.id, name: policy.name, policy }
2316
2588
  });
@@ -2336,7 +2608,7 @@ async function startWebUI(opts = {}) {
2336
2608
  break;
2337
2609
  }
2338
2610
  case "providers.saved": {
2339
- const saved = await loadSavedProviders();
2611
+ const saved = await providerHandlers.loadConfigProviders();
2340
2612
  send(ws, {
2341
2613
  type: "providers.saved",
2342
2614
  payload: {
@@ -2395,11 +2667,11 @@ async function startWebUI(opts = {}) {
2395
2667
  updateAutoCompactionMaxContext?.(newProv);
2396
2668
  try {
2397
2669
  configWriteLock = configWriteLock.then(async () => {
2398
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2670
+ const raw = await fs4.readFile(globalConfigPath, "utf8");
2399
2671
  const parsed = JSON.parse(raw);
2400
2672
  parsed.provider = newProvider;
2401
2673
  parsed.model = newModel;
2402
- await atomicWrite(globalConfigPath, JSON.stringify(parsed, null, 2));
2674
+ await atomicWrite3(globalConfigPath, JSON.stringify(parsed, null, 2));
2403
2675
  });
2404
2676
  await configWriteLock;
2405
2677
  } catch (err) {
@@ -2419,33 +2691,33 @@ async function startWebUI(opts = {}) {
2419
2691
  });
2420
2692
  break;
2421
2693
  }
2422
- broadcast({ type: "session.start", payload: await sessionStartPayload() });
2694
+ broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
2423
2695
  break;
2424
2696
  }
2425
2697
  case "key.add":
2426
2698
  case "key.update": {
2427
2699
  const { providerId, label, apiKey } = msg.payload;
2428
- await handleKeyUpsert(ws, providerId, label, apiKey);
2700
+ await providerHandlers.handleKeyUpsert(ws, providerId, label, apiKey);
2429
2701
  break;
2430
2702
  }
2431
2703
  case "key.delete": {
2432
2704
  const { providerId, label } = msg.payload;
2433
- await handleKeyDelete(ws, providerId, label);
2705
+ await providerHandlers.handleKeyDelete(ws, providerId, label);
2434
2706
  break;
2435
2707
  }
2436
2708
  case "key.set_active": {
2437
2709
  const { providerId, label } = msg.payload;
2438
- await handleKeySetActive(ws, providerId, label);
2710
+ await providerHandlers.handleKeySetActive(ws, providerId, label);
2439
2711
  break;
2440
2712
  }
2441
2713
  case "provider.add": {
2442
2714
  const p = msg.payload;
2443
- await handleProviderAdd(ws, p);
2715
+ await providerHandlers.handleProviderAdd(ws, p);
2444
2716
  break;
2445
2717
  }
2446
2718
  case "provider.remove": {
2447
2719
  const { providerId } = msg.payload;
2448
- await handleProviderRemove(ws, providerId);
2720
+ await providerHandlers.handleProviderRemove(ws, providerId);
2449
2721
  break;
2450
2722
  }
2451
2723
  case "sessions.list": {
@@ -2508,7 +2780,7 @@ async function startWebUI(opts = {}) {
2508
2780
  tokenCounter.reset();
2509
2781
  tokenCounter.account(resumed.data.usage, config.model);
2510
2782
  sessionStartedAt = Date.now();
2511
- broadcast({
2783
+ broadcast(clients, {
2512
2784
  type: "session.start",
2513
2785
  payload: {
2514
2786
  ...await sessionStartPayload(),
@@ -2648,7 +2920,7 @@ async function startWebUI(opts = {}) {
2648
2920
  case "todos.clear": {
2649
2921
  context.state.replaceTodos([]);
2650
2922
  sendResult(ws, true, "Todos cleared");
2651
- broadcast({ type: "todos.updated", payload: { todos: [] } });
2923
+ broadcast(clients, { type: "todos.updated", payload: { todos: [] } });
2652
2924
  break;
2653
2925
  }
2654
2926
  case "plan.get": {
@@ -2695,7 +2967,7 @@ async function startWebUI(opts = {}) {
2695
2967
  }
2696
2968
  await savePlan(planPath, plan);
2697
2969
  sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
2698
- broadcast({
2970
+ broadcast(clients, {
2699
2971
  type: "plan.updated",
2700
2972
  payload: { plan }
2701
2973
  });
@@ -2712,7 +2984,7 @@ async function startWebUI(opts = {}) {
2712
2984
  if (depth > 8 || results.length >= 600) return;
2713
2985
  let entries = [];
2714
2986
  try {
2715
- entries = await fs2.readdir(dir, { withFileTypes: true });
2987
+ entries = await fs4.readdir(dir, { withFileTypes: true });
2716
2988
  } catch {
2717
2989
  return;
2718
2990
  }
@@ -2722,7 +2994,7 @@ async function startWebUI(opts = {}) {
2722
2994
  const childRel = rel ? `${rel}/${e.name}` : e.name;
2723
2995
  if (e.isDirectory()) {
2724
2996
  if (SKIP_DIRS.has(e.name)) continue;
2725
- await walk(path2.join(dir, e.name), childRel, depth + 1);
2997
+ await walk(path4.join(dir, e.name), childRel, depth + 1);
2726
2998
  } else if (e.isFile()) {
2727
2999
  results.push(childRel);
2728
3000
  }
@@ -2791,7 +3063,7 @@ async function startWebUI(opts = {}) {
2791
3063
  model: config.model
2792
3064
  });
2793
3065
  sendResult(ws, true, `Switched to mode "${id}"`);
2794
- broadcast({
3066
+ broadcast(clients, {
2795
3067
  type: "session.start",
2796
3068
  payload: { ...await sessionStartPayload() }
2797
3069
  });
@@ -2830,92 +3102,37 @@ async function startWebUI(opts = {}) {
2830
3102
  }
2831
3103
  }
2832
3104
  }
2833
- async function loadSavedProviders() {
2834
- try {
2835
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2836
- const parsed = JSON.parse(raw);
2837
- if (!parsed.providers) return {};
2838
- return decryptConfigSecrets(parsed.providers, vault);
2839
- } catch {
2840
- return {};
2841
- }
2842
- }
2843
- async function saveProviders(providers) {
2844
- configWriteLock = configWriteLock.then(async () => {
2845
- let parsed;
2846
- try {
2847
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2848
- parsed = JSON.parse(raw);
2849
- } catch {
2850
- parsed = {};
2851
- }
2852
- parsed["providers"] = providers;
2853
- const encrypted = encryptConfigSecrets(parsed, vault);
2854
- await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
2855
- });
2856
- await configWriteLock;
2857
- }
2858
- function sendResult(ws, success, message) {
2859
- send(ws, { type: "key.operation_result", payload: { success, message } });
2860
- }
2861
- async function handleKeyUpsert(ws, providerId, label, apiKey) {
2862
- try {
2863
- const providers = await loadSavedProviders();
2864
- const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
2865
- if (result.ok) await saveProviders(providers);
2866
- sendResult(ws, result.ok, result.message);
2867
- } catch (err) {
2868
- sendResult(ws, false, errMessage(err));
3105
+ const providerHandlers = createProviderHandlers({
3106
+ globalConfigPath,
3107
+ vault,
3108
+ getConfigWriteLock: () => configWriteLock,
3109
+ setConfigWriteLock: (p) => {
3110
+ configWriteLock = p;
2869
3111
  }
2870
- }
2871
- async function handleKeyDelete(ws, providerId, label) {
2872
- try {
2873
- const providers = await loadSavedProviders();
2874
- const result = deleteKey(providers, providerId, label);
2875
- if (result.ok) await saveProviders(providers);
2876
- sendResult(ws, result.ok, result.message);
2877
- } catch (err) {
2878
- sendResult(ws, false, errMessage(err));
2879
- }
2880
- }
2881
- async function handleKeySetActive(ws, providerId, label) {
2882
- try {
2883
- const providers = await loadSavedProviders();
2884
- const result = setActiveKey(providers, providerId, label);
2885
- if (result.ok) await saveProviders(providers);
2886
- sendResult(ws, result.ok, result.message);
2887
- } catch (err) {
2888
- sendResult(ws, false, errMessage(err));
2889
- }
2890
- }
2891
- async function handleProviderAdd(ws, payload) {
2892
- try {
2893
- const providers = await loadSavedProviders();
2894
- const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
2895
- if (result.ok) await saveProviders(providers);
2896
- sendResult(ws, result.ok, result.message);
2897
- } catch (err) {
2898
- sendResult(ws, false, errMessage(err));
2899
- }
2900
- }
2901
- async function handleProviderRemove(ws, providerId) {
2902
- try {
2903
- const providers = await loadSavedProviders();
2904
- const result = removeProvider(providers, providerId);
2905
- if (result.ok) await saveProviders(providers);
2906
- sendResult(ws, result.ok, result.message);
2907
- } catch (err) {
2908
- sendResult(ws, false, errMessage(err));
2909
- }
2910
- }
3112
+ });
2911
3113
  const httpServer = createHttpServer({
2912
3114
  host: wsHost,
2913
- distDir: path2.resolve(import.meta.dirname, "../../dist"),
3115
+ distDir: path4.resolve(import.meta.dirname, "../../dist"),
2914
3116
  wsPort
2915
3117
  });
2916
- const httpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
3118
+ const registryBaseDir = path4.dirname(globalConfigPath);
2917
3119
  httpServer.listen(httpPort, wsHost, () => {
2918
- console.log(`[WebUI] HTTP server running on http://${wsHost}:${httpPort}`);
3120
+ const openUrl = `http://${wsHost}:${httpPort}`;
3121
+ console.log(`[WebUI] HTTP server running on ${openUrl}`);
3122
+ if (opts.open) openBrowser(openUrl);
3123
+ void registerInstance(
3124
+ {
3125
+ pid: process.pid,
3126
+ httpPort,
3127
+ wsPort,
3128
+ host: wsHost,
3129
+ projectRoot,
3130
+ projectName: path4.basename(projectRoot) || projectRoot,
3131
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3132
+ url: `http://${wsHost}:${httpPort}`
3133
+ },
3134
+ registryBaseDir
3135
+ ).catch((err) => console.warn("[WebUI] Could not record instance:", errMessage(err)));
2919
3136
  });
2920
3137
  registerShutdownHandlers({
2921
3138
  flushSession: async () => {
@@ -2927,10 +3144,48 @@ async function startWebUI(opts = {}) {
2927
3144
  await session.close();
2928
3145
  },
2929
3146
  clients: () => clients.keys(),
2930
- servers: [httpServer, wssPrimary, wssSecondary]
3147
+ servers: [httpServer, wssPrimary, wssSecondary],
3148
+ // Drop this instance from the registry on a clean exit so the file reflects
3149
+ // reality. Crash exits are healed by the next register()/list() prune pass.
3150
+ onShutdown: () => unregisterInstance(process.pid, registryBaseDir)
2931
3151
  });
2932
3152
  }
2933
3153
  export {
2934
- startWebUI
3154
+ addProvider,
3155
+ broadcast,
3156
+ browserOpenCommand,
3157
+ buildCspHeader,
3158
+ createHttpServer,
3159
+ createProviderConfigIO,
3160
+ defaultBaseDir,
3161
+ deleteKey,
3162
+ errMessage,
3163
+ extractToken,
3164
+ findFreePort,
3165
+ formatInstances,
3166
+ generateAuthToken,
3167
+ hostHeaderOk,
3168
+ injectWsPort,
3169
+ isLoopbackBind,
3170
+ isLoopbackHostname,
3171
+ isPortFree,
3172
+ listInstances,
3173
+ loadSavedProviders,
3174
+ maskedKey,
3175
+ normalizeKeys,
3176
+ openBrowser,
3177
+ registerInstance,
3178
+ registryPath,
3179
+ removeProvider,
3180
+ saveProviders,
3181
+ send,
3182
+ sendResult,
3183
+ setActiveKey,
3184
+ startWebUI,
3185
+ tokenMatches,
3186
+ unregisterInstance,
3187
+ upsertKey,
3188
+ verifyClient,
3189
+ writeKeysBack
2935
3190
  };
2936
3191
  //# sourceMappingURL=index.js.map