@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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // src/server/index.ts
3
- import * as fs2 from "fs/promises";
4
- import * as path2 from "path";
3
+ import * as fs4 from "fs/promises";
4
+ import * as path4 from "path";
5
5
 
6
6
  // src/server/http-server.ts
7
7
  import * as fs from "fs/promises";
@@ -16,8 +16,18 @@ var MIME_TYPES = {
16
16
  ".png": "image/png",
17
17
  ".ico": "image/x-icon"
18
18
  };
19
- function buildCspHeader(wsPort2) {
20
- return `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://127.0.0.1:${wsPort2} wss://127.0.0.1:${wsPort2} ws://[::1]:${wsPort2} wss://[::1]:${wsPort2}; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
19
+ function injectWsPort(html, wsPort) {
20
+ const tag = `<meta name="wrongstack-ws-port" content="${wsPort}" />`;
21
+ if (html.includes('name="wrongstack-ws-port"')) return html;
22
+ if (html.includes("</head>")) {
23
+ return html.replace("</head>", ` ${tag}
24
+ </head>`);
25
+ }
26
+ return `${tag}
27
+ ${html}`;
28
+ }
29
+ function buildCspHeader(wsPort) {
30
+ 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'`;
21
31
  }
22
32
  function isInsideDist(candidate, distDir) {
23
33
  const root = path.resolve(distDir);
@@ -27,7 +37,7 @@ function isInsideDist(candidate, distDir) {
27
37
  function createHttpServer(opts) {
28
38
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
29
39
  const distDir = path.resolve(opts.distDir);
30
- const wsPort2 = opts.wsPort;
40
+ const wsPort = opts.wsPort;
31
41
  return http.createServer(async (req, res) => {
32
42
  try {
33
43
  const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
@@ -55,7 +65,11 @@ function createHttpServer(opts) {
55
65
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
56
66
  if (ext === ".html") {
57
67
  res.setHeader("Cache-Control", "no-cache");
58
- res.setHeader("Content-Security-Policy", buildCspHeader(wsPort2));
68
+ res.setHeader("Content-Security-Policy", buildCspHeader(wsPort));
69
+ const html = await fs.readFile(resolvedPath, "utf8");
70
+ res.writeHead(200);
71
+ res.end(injectWsPort(html, wsPort));
72
+ return;
59
73
  }
60
74
  const fileContent = await fs.readFile(resolvedPath);
61
75
  res.writeHead(200);
@@ -63,15 +77,15 @@ function createHttpServer(opts) {
63
77
  } catch (err) {
64
78
  if (err.code === "ENOENT") {
65
79
  try {
66
- const fileContent = await fs.readFile(path.join(distDir, "index.html"));
80
+ const html = await fs.readFile(path.join(distDir, "index.html"), "utf8");
67
81
  res.writeHead(200, {
68
82
  "Content-Type": "text/html",
69
83
  "X-Content-Type-Options": "nosniff",
70
84
  "X-Frame-Options": "DENY",
71
85
  "Referrer-Policy": "strict-origin-when-cross-origin",
72
- "Content-Security-Policy": buildCspHeader(wsPort2)
86
+ "Content-Security-Policy": buildCspHeader(wsPort)
73
87
  });
74
- res.end(fileContent);
88
+ res.end(injectWsPort(html, wsPort));
75
89
  } catch {
76
90
  res.writeHead(404);
77
91
  res.end("Not found");
@@ -155,7 +169,7 @@ import {
155
169
  ProviderRegistry,
156
170
  TOKENS as TOKENS2,
157
171
  ToolRegistry,
158
- atomicWrite,
172
+ atomicWrite as atomicWrite3,
159
173
  createDefaultPipelines,
160
174
  DEFAULT_CONTEXT_WINDOW_MODE_ID,
161
175
  DEFAULT_TOOLS_CONFIG,
@@ -164,11 +178,9 @@ import {
164
178
  resolveContextWindowPolicy
165
179
  } from "@wrongstack/core";
166
180
  import { ToolExecutor } from "@wrongstack/core/execution";
167
- import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
168
181
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
169
182
  import { builtinToolsPack, forgetTool, rememberTool } from "@wrongstack/tools";
170
- import { WebSocket, WebSocketServer } from "ws";
171
- import { randomBytes } from "crypto";
183
+ import { WebSocketServer } from "ws";
172
184
 
173
185
  // ../runtime/src/container.ts
174
186
  import {
@@ -228,6 +240,7 @@ function createDefaultContainer(opts) {
228
240
  trustFile: wpaths.projectTrust,
229
241
  yolo: opts.permission?.yolo ?? false,
230
242
  yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
243
+ confirmDestructive: opts.permission?.confirmDestructive ?? false,
231
244
  promptDelegate: opts.permission?.promptDelegate
232
245
  })
233
246
  );
@@ -1386,8 +1399,8 @@ import { timingSafeEqual } from "crypto";
1386
1399
  function isLoopbackHostname(hostname) {
1387
1400
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1388
1401
  }
1389
- function isLoopbackBind(wsHost2) {
1390
- return wsHost2 === "127.0.0.1" || wsHost2 === "::1" || wsHost2 === "localhost";
1402
+ function isLoopbackBind(wsHost) {
1403
+ return wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
1391
1404
  }
1392
1405
  function tokenMatches(provided, expected) {
1393
1406
  if (!provided) return false;
@@ -1413,14 +1426,14 @@ function hostHeaderOk(input) {
1413
1426
  return isLoopbackHostname(hostname);
1414
1427
  }
1415
1428
  function verifyClient(input) {
1416
- const { origin, url, hostHeader, remoteAddress, wsHost: wsHost2, expectedToken } = input;
1429
+ const { origin, url, hostHeader, remoteAddress, wsHost, expectedToken } = input;
1417
1430
  const tokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
1418
- if (!hostHeaderOk({ hostHeader, wsHost: wsHost2 })) return false;
1431
+ if (!hostHeaderOk({ hostHeader, wsHost })) return false;
1419
1432
  if (!origin) {
1420
1433
  const remoteIp = remoteAddress ?? "";
1421
1434
  const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
1422
- if (!isRemoteLoopback && wsHost2 === "0.0.0.0") return false;
1423
- return tokenOk || isLoopbackBind(wsHost2);
1435
+ if (!isRemoteLoopback && wsHost === "0.0.0.0") return false;
1436
+ return tokenOk || isLoopbackBind(wsHost);
1424
1437
  }
1425
1438
  try {
1426
1439
  const { hostname } = new URL(origin);
@@ -1447,6 +1460,13 @@ function createShutdown(res) {
1447
1460
  }
1448
1461
  for (const ws of res.clients()) ws.close();
1449
1462
  for (const server of res.servers) server?.close();
1463
+ if (res.onShutdown) {
1464
+ try {
1465
+ await res.onShutdown();
1466
+ } catch (e) {
1467
+ log(`[WebUI] Error during shutdown cleanup: ${e instanceof Error ? e.message : String(e)}`);
1468
+ }
1469
+ }
1450
1470
  exit(0);
1451
1471
  };
1452
1472
  }
@@ -1460,6 +1480,138 @@ function registerShutdownHandlers(res) {
1460
1480
  };
1461
1481
  }
1462
1482
 
1483
+ // src/server/instance-registry.ts
1484
+ import * as os from "os";
1485
+ import * as path2 from "path";
1486
+ import * as fs2 from "fs/promises";
1487
+ import { atomicWrite } from "@wrongstack/core";
1488
+ function defaultBaseDir() {
1489
+ return path2.join(os.homedir(), ".wrongstack");
1490
+ }
1491
+ function registryPath(baseDir = defaultBaseDir()) {
1492
+ return path2.join(baseDir, "webui-instances.json");
1493
+ }
1494
+ function isPidAlive(pid) {
1495
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1496
+ try {
1497
+ process.kill(pid, 0);
1498
+ return true;
1499
+ } catch (err) {
1500
+ return err.code !== "ESRCH";
1501
+ }
1502
+ }
1503
+ async function load(file) {
1504
+ try {
1505
+ const raw = await fs2.readFile(file, "utf8");
1506
+ const parsed = JSON.parse(raw);
1507
+ if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
1508
+ return parsed;
1509
+ }
1510
+ } catch {
1511
+ }
1512
+ return { version: 1, instances: [] };
1513
+ }
1514
+ async function save(file, instances) {
1515
+ await atomicWrite(file, `${JSON.stringify({ version: 1, instances }, null, 2)}
1516
+ `, {
1517
+ mode: 384
1518
+ });
1519
+ }
1520
+ function prune(instances, excludePid) {
1521
+ return instances.filter((i) => i.pid !== excludePid && isPidAlive(i.pid));
1522
+ }
1523
+ async function registerInstance(record, baseDir = defaultBaseDir()) {
1524
+ const file = registryPath(baseDir);
1525
+ const data = await load(file);
1526
+ const instances = prune(data.instances, record.pid);
1527
+ instances.push(record);
1528
+ await save(file, instances);
1529
+ }
1530
+ async function unregisterInstance(pid, baseDir = defaultBaseDir()) {
1531
+ const file = registryPath(baseDir);
1532
+ const data = await load(file);
1533
+ const instances = prune(data.instances, pid);
1534
+ await save(file, instances);
1535
+ }
1536
+ async function listInstances(baseDir = defaultBaseDir()) {
1537
+ const file = registryPath(baseDir);
1538
+ const data = await load(file);
1539
+ const live = prune(data.instances);
1540
+ if (live.length !== data.instances.length) {
1541
+ await save(file, live).catch(() => {
1542
+ });
1543
+ }
1544
+ return live;
1545
+ }
1546
+ function formatInstances(instances) {
1547
+ if (instances.length === 0) {
1548
+ return "No WebUI instances are currently running.";
1549
+ }
1550
+ const lines = [`Running WebUI instances (${instances.length}):`, ""];
1551
+ for (const i of instances) {
1552
+ lines.push(
1553
+ ` \u2022 ${i.url} \xB7 ws:${i.wsPort} \xB7 pid ${i.pid}`,
1554
+ ` project: ${i.projectName} (${i.projectRoot})`,
1555
+ ` since: ${i.startedAt}`
1556
+ );
1557
+ }
1558
+ return lines.join("\n");
1559
+ }
1560
+
1561
+ // src/server/port-utils.ts
1562
+ import * as net from "net";
1563
+ function isPortFree(host, port) {
1564
+ return new Promise((resolve3) => {
1565
+ const srv = net.createServer();
1566
+ srv.once("error", () => resolve3(false));
1567
+ srv.once("listening", () => {
1568
+ srv.close(() => resolve3(true));
1569
+ });
1570
+ try {
1571
+ srv.listen(port, host);
1572
+ } catch {
1573
+ resolve3(false);
1574
+ }
1575
+ });
1576
+ }
1577
+ async function findFreePort(host, startPort, opts = {}) {
1578
+ const exclude = opts.exclude ?? /* @__PURE__ */ new Set();
1579
+ const maxTries = opts.maxTries ?? 200;
1580
+ let port = startPort;
1581
+ for (let i = 0; i < maxTries; i++) {
1582
+ if (port > 65535) port = 1024 + port % 5e4;
1583
+ if (!exclude.has(port) && await isPortFree(host, port)) {
1584
+ return port;
1585
+ }
1586
+ port++;
1587
+ }
1588
+ throw new Error(
1589
+ `No free port found near ${startPort} on ${host} after ${maxTries} attempts.`
1590
+ );
1591
+ }
1592
+
1593
+ // src/server/open-browser.ts
1594
+ import { spawn } from "child_process";
1595
+ function browserOpenCommand(url, platform = process.platform) {
1596
+ if (platform === "win32") {
1597
+ return { command: "cmd", args: ["/c", "start", "", url] };
1598
+ }
1599
+ if (platform === "darwin") {
1600
+ return { command: "open", args: [url] };
1601
+ }
1602
+ return { command: "xdg-open", args: [url] };
1603
+ }
1604
+ function openBrowser(url, platform = process.platform) {
1605
+ try {
1606
+ const { command, args } = browserOpenCommand(url, platform);
1607
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
1608
+ child.on("error", () => {
1609
+ });
1610
+ child.unref();
1611
+ } catch {
1612
+ }
1613
+ }
1614
+
1463
1615
  // src/server/usage-cost.ts
1464
1616
  function getCostRates(model) {
1465
1617
  const cost = model?.cost;
@@ -1473,6 +1625,60 @@ function computeUsageCost(usage, rates) {
1473
1625
  return (usage.input * rates.input + usage.output * rates.output + (usage.cacheRead ?? 0) * rates.cacheRead) / 1e6;
1474
1626
  }
1475
1627
 
1628
+ // src/server/provider-config-io.ts
1629
+ import * as fs3 from "fs/promises";
1630
+ import * as path3 from "path";
1631
+ import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
1632
+ import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
1633
+ import { DefaultSecretVault } from "@wrongstack/core";
1634
+ async function loadSavedProviders(configPath, vault) {
1635
+ let raw;
1636
+ try {
1637
+ raw = await fs3.readFile(configPath, "utf8");
1638
+ } catch {
1639
+ return {};
1640
+ }
1641
+ let parsed = {};
1642
+ try {
1643
+ parsed = JSON.parse(raw);
1644
+ } catch {
1645
+ return {};
1646
+ }
1647
+ if (!parsed.providers) return {};
1648
+ return decryptConfigSecrets(parsed.providers, vault);
1649
+ }
1650
+ async function saveProviders(configPath, vault, providers) {
1651
+ let raw;
1652
+ let fileExists = true;
1653
+ try {
1654
+ raw = await fs3.readFile(configPath, "utf8");
1655
+ } catch (err) {
1656
+ if (err.code !== "ENOENT") {
1657
+ throw new Error(
1658
+ `Refusing to mutate ${configPath}: ${err.message}`,
1659
+ { cause: err }
1660
+ );
1661
+ }
1662
+ fileExists = false;
1663
+ raw = "{}";
1664
+ }
1665
+ let parsed;
1666
+ try {
1667
+ parsed = JSON.parse(raw);
1668
+ } catch (err) {
1669
+ if (fileExists) {
1670
+ throw new Error(
1671
+ `Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
1672
+ { cause: err }
1673
+ );
1674
+ }
1675
+ parsed = {};
1676
+ }
1677
+ parsed.providers = providers;
1678
+ const encrypted = encryptConfigSecrets(parsed, vault);
1679
+ await atomicWrite2(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
1680
+ }
1681
+
1476
1682
  // src/server/provider-keys.ts
1477
1683
  function normalizeKeys(cfg) {
1478
1684
  if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
@@ -1568,6 +1774,159 @@ function removeProvider(providers, providerId) {
1568
1774
  return { ok: true, message: `Provider "${providerId}" removed` };
1569
1775
  }
1570
1776
 
1777
+ // src/server/ws-utils.ts
1778
+ import { randomBytes } from "crypto";
1779
+ import { WebSocket } from "ws";
1780
+ function send(ws, msg) {
1781
+ if (ws.readyState === WebSocket.OPEN) {
1782
+ ws.send(JSON.stringify(msg));
1783
+ }
1784
+ }
1785
+ function broadcast(clients, msg) {
1786
+ const data = JSON.stringify(msg);
1787
+ for (const [ws] of clients) {
1788
+ if (ws.readyState === WebSocket.OPEN) {
1789
+ try {
1790
+ ws.send(data);
1791
+ } catch {
1792
+ }
1793
+ }
1794
+ }
1795
+ }
1796
+ function sendResult(ws, success, message) {
1797
+ send(ws, { type: "key.operation_result", payload: { success, message } });
1798
+ }
1799
+ function errMessage(err) {
1800
+ return err instanceof Error ? err.message : String(err);
1801
+ }
1802
+ function generateAuthToken() {
1803
+ return randomBytes(16).toString("hex");
1804
+ }
1805
+
1806
+ // src/server/provider-handlers.ts
1807
+ function createProviderHandlers(deps) {
1808
+ const { globalConfigPath, vault } = deps;
1809
+ let configWriteLock = deps.getConfigWriteLock();
1810
+ async function loadConfigProviders() {
1811
+ return loadSavedProviders(globalConfigPath, vault);
1812
+ }
1813
+ async function saveConfigProviders(providers) {
1814
+ const next = configWriteLock.then(() => saveProviders(globalConfigPath, vault, providers));
1815
+ configWriteLock = next;
1816
+ deps.setConfigWriteLock(next);
1817
+ await next;
1818
+ }
1819
+ async function handleKeyUpsert(ws, providerId, label, apiKey) {
1820
+ try {
1821
+ const providers = await loadConfigProviders();
1822
+ const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
1823
+ if (result.ok) await saveConfigProviders(providers);
1824
+ sendResult(ws, result.ok, result.message);
1825
+ } catch (err) {
1826
+ sendResult(ws, false, errMessage(err));
1827
+ }
1828
+ }
1829
+ async function handleKeyDelete(ws, providerId, label) {
1830
+ try {
1831
+ const providers = await loadConfigProviders();
1832
+ const result = deleteKey(providers, providerId, label);
1833
+ if (result.ok) await saveConfigProviders(providers);
1834
+ sendResult(ws, result.ok, result.message);
1835
+ } catch (err) {
1836
+ sendResult(ws, false, errMessage(err));
1837
+ }
1838
+ }
1839
+ async function handleKeySetActive(ws, providerId, label) {
1840
+ try {
1841
+ const providers = await loadConfigProviders();
1842
+ const result = setActiveKey(providers, providerId, label);
1843
+ if (result.ok) await saveConfigProviders(providers);
1844
+ sendResult(ws, result.ok, result.message);
1845
+ } catch (err) {
1846
+ sendResult(ws, false, errMessage(err));
1847
+ }
1848
+ }
1849
+ async function handleProviderAdd(ws, payload) {
1850
+ try {
1851
+ const providers = await loadConfigProviders();
1852
+ const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
1853
+ if (result.ok) await saveConfigProviders(providers);
1854
+ sendResult(ws, result.ok, result.message);
1855
+ } catch (err) {
1856
+ sendResult(ws, false, errMessage(err));
1857
+ }
1858
+ }
1859
+ async function handleProviderRemove(ws, providerId) {
1860
+ try {
1861
+ const providers = await loadConfigProviders();
1862
+ const result = removeProvider(providers, providerId);
1863
+ if (result.ok) await saveConfigProviders(providers);
1864
+ sendResult(ws, result.ok, result.message);
1865
+ } catch (err) {
1866
+ sendResult(ws, false, errMessage(err));
1867
+ }
1868
+ }
1869
+ return { handleKeyUpsert, handleKeyDelete, handleKeySetActive, handleProviderAdd, handleProviderRemove, loadConfigProviders };
1870
+ }
1871
+
1872
+ // src/server/setup-events.ts
1873
+ function setupEvents(deps) {
1874
+ const { events, broadcast: broadcast2, clients, config, context, pendingConfirms } = deps;
1875
+ events.on("iteration.started", (e) => {
1876
+ broadcast2(clients, {
1877
+ type: "iteration.started",
1878
+ payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
1879
+ });
1880
+ });
1881
+ events.on("provider.text_delta", (e) => {
1882
+ broadcast2(clients, { type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
1883
+ });
1884
+ events.on("provider.thinking_delta", (e) => {
1885
+ broadcast2(clients, { type: "provider.thinking_delta", payload: { text: e.text } });
1886
+ });
1887
+ events.on("tool.started", (e) => {
1888
+ broadcast2(clients, {
1889
+ type: "tool.started",
1890
+ payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
1891
+ });
1892
+ });
1893
+ events.on("tool.progress", (e) => {
1894
+ broadcast2(clients, {
1895
+ type: "tool.progress",
1896
+ payload: { id: e.id, name: e.name, eventType: e.event.type, text: e.event.text }
1897
+ });
1898
+ });
1899
+ events.on("tool.executed", (e) => {
1900
+ broadcast2(clients, {
1901
+ type: "tool.executed",
1902
+ payload: { id: e.id, name: e.name, durationMs: e.durationMs, ok: e.ok, input: e.input, output: e.output }
1903
+ });
1904
+ broadcast2(clients, { type: "todos.updated", payload: { todos: [...context.todos] } });
1905
+ });
1906
+ events.on("provider.response", (e) => {
1907
+ broadcast2(clients, { type: "provider.response", payload: { usage: e.usage, stopReason: e.stopReason, messageId: "current" } });
1908
+ });
1909
+ events.on("context.repaired", (e) => {
1910
+ broadcast2(clients, { type: "context.repaired", payload: { removedToolUses: e.removedToolUses, removedToolResults: e.removedToolResults, removedMessages: e.removedMessages } });
1911
+ });
1912
+ events.on("tool.confirm_needed", (e) => {
1913
+ const id = e.toolUseId ?? `confirm_${Date.now()}`;
1914
+ pendingConfirms.set(id, e.resolve);
1915
+ broadcast2(clients, { type: "tool.confirm_needed", payload: { id, toolName: e.tool?.name ?? "unknown", input: e.input, suggestedPattern: e.suggestedPattern } });
1916
+ });
1917
+ events.on("error", (e) => {
1918
+ broadcast2(clients, { type: "error", payload: { phase: e.phase, message: e.err instanceof Error ? e.err.message : String(e.err) } });
1919
+ });
1920
+ const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: { kind, ...payload } });
1921
+ 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 }));
1922
+ events.on("subagent.task_started", (e) => forwardSubagent("task_started", { subagentId: e.subagentId, taskId: e.taskId, description: e.description }));
1923
+ events.on("subagent.tool_executed", (e) => forwardSubagent("tool_executed", { subagentId: e.subagentId, toolName: e.name, durationMs: e.durationMs, ok: e.ok }));
1924
+ events.on("subagent.iteration_summary", (e) => forwardSubagent("iteration_summary", { subagentId: e.subagentId, iteration: e.iteration, toolCalls: e.toolCalls, costUsd: e.costUsd, currentTool: e.currentTool }));
1925
+ events.on("subagent.budget_extended", (e) => forwardSubagent("budget_extended", { subagentId: e.subagentId, totalExtensions: e.totalExtensions }));
1926
+ events.on("subagent.ctx_pct", (e) => forwardSubagent("ctx_pct", { subagentId: e.subagentId, load: e.load, tokens: e.tokens, maxContext: e.maxContext }));
1927
+ 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 }));
1928
+ }
1929
+
1571
1930
  // src/server/token-estimator.ts
1572
1931
  function estimateTokens(s) {
1573
1932
  return Math.ceil(s.length / 4);
@@ -1626,12 +1985,23 @@ function estimateContextBreakdown(input) {
1626
1985
  }
1627
1986
 
1628
1987
  // src/server/index.ts
1629
- function errMessage(err) {
1630
- return err instanceof Error ? err.message : String(err);
1631
- }
1632
1988
  async function startWebUI(opts = {}) {
1633
- const wsPort2 = opts.wsPort ?? 3457;
1634
- const wsHost2 = opts.wsHost ?? "127.0.0.1";
1989
+ const requestedWsPort = opts.wsPort ?? 3457;
1990
+ const wsHost = opts.wsHost ?? "127.0.0.1";
1991
+ const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
1992
+ const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
1993
+ let wsPort = requestedWsPort;
1994
+ let httpPort = requestedHttpPort;
1995
+ if (!strictPort) {
1996
+ httpPort = await findFreePort(wsHost, requestedHttpPort);
1997
+ wsPort = await findFreePort(wsHost, requestedWsPort, { exclude: /* @__PURE__ */ new Set([httpPort]) });
1998
+ if (httpPort !== requestedHttpPort) {
1999
+ console.warn(`[WebUI] HTTP port ${requestedHttpPort} in use \u2192 using ${httpPort}`);
2000
+ }
2001
+ if (wsPort !== requestedWsPort) {
2002
+ console.warn(`[WebUI] WS port ${requestedWsPort} in use \u2192 using ${wsPort}`);
2003
+ }
2004
+ }
1635
2005
  console.log("[WebUI] Starting backend services...");
1636
2006
  const boot = await bootConfig();
1637
2007
  const { config: baseConfig, vault, globalConfigPath, projectRoot, wpaths, logger } = boot;
@@ -1885,41 +2255,42 @@ async function startWebUI(opts = {}) {
1885
2255
  inputCost,
1886
2256
  outputCost,
1887
2257
  cacheReadCost,
1888
- projectName: path2.basename(projectRoot) || projectRoot,
2258
+ projectName: path4.basename(projectRoot) || projectRoot,
1889
2259
  cwd: projectRoot,
1890
2260
  mode: modeId,
1891
2261
  contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
1892
2262
  wsToken
1893
2263
  };
1894
2264
  }
1895
- const wsToken = randomBytes(16).toString("hex");
2265
+ const wsToken = generateAuthToken();
1896
2266
  console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
1897
2267
  const verifyClient2 = (info) => verifyClient({
1898
2268
  origin: info.origin,
1899
2269
  url: info.req.url ?? "",
1900
2270
  hostHeader: info.req.headers.host,
1901
2271
  remoteAddress: info.req.socket.remoteAddress,
1902
- wsHost: wsHost2,
2272
+ wsHost,
1903
2273
  expectedToken: wsToken
1904
2274
  });
1905
2275
  const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
1906
2276
  const wssPrimary = new WebSocketServer({
1907
- port: wsPort2,
1908
- host: wsHost2,
2277
+ port: wsPort,
2278
+ host: wsHost,
1909
2279
  verifyClient: verifyClient2,
1910
2280
  maxPayload: WS_MAX_PAYLOAD
1911
2281
  });
1912
- const wssSecondary = wsHost2 === "127.0.0.1" ? new WebSocketServer({
1913
- port: wsPort2,
2282
+ const wssSecondary = wsHost === "127.0.0.1" ? new WebSocketServer({
2283
+ port: wsPort,
1914
2284
  host: "::1",
1915
2285
  verifyClient: verifyClient2,
1916
2286
  maxPayload: WS_MAX_PAYLOAD
1917
2287
  }) : null;
1918
2288
  const clients = /* @__PURE__ */ new Map();
1919
- const RATE_LIMIT_MESSAGES = 60;
2289
+ const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
1920
2290
  const RATE_LIMIT_WINDOW_MS = 6e4;
1921
2291
  const rateLimits = /* @__PURE__ */ new Map();
1922
2292
  function checkRateLimit(ws, client) {
2293
+ if (RATE_LIMIT_MESSAGES <= 0) return true;
1923
2294
  const now = Date.now();
1924
2295
  const key = client.sessionId ?? String(ws);
1925
2296
  const limit = rateLimits.get(key);
@@ -1933,116 +2304,9 @@ async function startWebUI(opts = {}) {
1933
2304
  }
1934
2305
  let runLock = null;
1935
2306
  console.log(
1936
- `[WebUI] WebSocket server running on ws://${wsHost2}:${wsPort2}` + (wssSecondary ? ` (and ws://[::1]:${wsPort2})` : "")
2307
+ `[WebUI] WebSocket server running on ws://${wsHost}:${wsPort}` + (wssSecondary ? ` (and ws://[::1]:${wsPort})` : "")
1937
2308
  );
1938
2309
  const pendingConfirms = /* @__PURE__ */ new Map();
1939
- function setupEvents() {
1940
- events.on("iteration.started", (e) => {
1941
- broadcast({
1942
- type: "iteration.started",
1943
- payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
1944
- });
1945
- });
1946
- events.on("provider.text_delta", (e) => {
1947
- broadcast({ type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
1948
- });
1949
- events.on("provider.thinking_delta", (e) => {
1950
- broadcast({ type: "provider.thinking_delta", payload: { text: e.text } });
1951
- });
1952
- events.on("tool.started", (e) => {
1953
- broadcast({
1954
- type: "tool.started",
1955
- payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
1956
- });
1957
- });
1958
- events.on("tool.progress", (e) => {
1959
- broadcast({
1960
- type: "tool.progress",
1961
- payload: {
1962
- id: e.id,
1963
- name: e.name,
1964
- eventType: e.event.type,
1965
- text: e.event.text
1966
- }
1967
- });
1968
- });
1969
- events.on("tool.executed", (e) => {
1970
- broadcast({
1971
- type: "tool.executed",
1972
- payload: {
1973
- // Forward the tool_use id so frontend can correlate with the
1974
- // matching tool.started bubble — without this, parallel tool calls
1975
- // all stay stuck on "Running…" because the frontend can't tell
1976
- // which bubble this result belongs to.
1977
- id: e.id,
1978
- name: e.name,
1979
- durationMs: e.durationMs,
1980
- ok: e.ok,
1981
- input: e.input,
1982
- output: e.output
1983
- }
1984
- });
1985
- broadcast({
1986
- type: "todos.updated",
1987
- payload: { todos: [...context.todos] }
1988
- });
1989
- });
1990
- events.on("provider.response", (e) => {
1991
- broadcast({
1992
- type: "provider.response",
1993
- payload: {
1994
- usage: e.usage,
1995
- stopReason: e.stopReason,
1996
- messageId: "current"
1997
- }
1998
- });
1999
- });
2000
- events.on("context.repaired", (e) => {
2001
- broadcast({
2002
- type: "context.repaired",
2003
- payload: {
2004
- removedToolUses: e.removedToolUses,
2005
- removedToolResults: e.removedToolResults,
2006
- removedMessages: e.removedMessages
2007
- }
2008
- });
2009
- });
2010
- events.on("tool.confirm_needed", (e) => {
2011
- const id = e.toolUseId ?? `confirm_${Date.now()}`;
2012
- pendingConfirms.set(id, e.resolve);
2013
- broadcast({
2014
- type: "tool.confirm_needed",
2015
- payload: {
2016
- id,
2017
- toolName: e.tool?.name ?? "unknown",
2018
- input: e.input,
2019
- suggestedPattern: e.suggestedPattern
2020
- }
2021
- });
2022
- });
2023
- events.on("error", (e) => {
2024
- broadcast({
2025
- type: "error",
2026
- payload: {
2027
- phase: e.phase,
2028
- message: e.err instanceof Error ? e.err.message : String(e.err)
2029
- }
2030
- });
2031
- });
2032
- }
2033
- function send(ws, msg) {
2034
- if (ws.readyState === WebSocket.OPEN) {
2035
- ws.send(JSON.stringify(msg));
2036
- }
2037
- }
2038
- function broadcast(msg) {
2039
- const data = JSON.stringify(msg);
2040
- for (const [ws] of clients) {
2041
- if (ws.readyState === WebSocket.OPEN) {
2042
- ws.send(data);
2043
- }
2044
- }
2045
- }
2046
2310
  const handleConnection = (ws) => {
2047
2311
  const client = { ws, sessionId: session.id, connectedAt: Date.now() };
2048
2312
  clients.set(ws, client);
@@ -2100,15 +2364,15 @@ async function startWebUI(opts = {}) {
2100
2364
  if (eventsArmed) return;
2101
2365
  eventsArmed = true;
2102
2366
  console.log(`[WebUI] Backend ready (${label})`);
2103
- setupEvents();
2367
+ setupEvents({ events, broadcast, clients, config, context, pendingConfirms });
2104
2368
  };
2105
- wssPrimary.on("listening", () => armOnce(`${wsHost2}:${wsPort2}`));
2369
+ wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
2106
2370
  wssPrimary.on("connection", handleConnection);
2107
2371
  wssPrimary.on("error", (err) => {
2108
- console.error(`[WebUI] Primary WS server error (${wsHost2}):`, err);
2372
+ console.error(`[WebUI] Primary WS server error (${wsHost}):`, err);
2109
2373
  });
2110
2374
  if (wssSecondary) {
2111
- wssSecondary.on("listening", () => armOnce(`::1:${wsPort2}`));
2375
+ wssSecondary.on("listening", () => armOnce(`::1:${wsPort}`));
2112
2376
  wssSecondary.on("connection", handleConnection);
2113
2377
  wssSecondary.on("error", (err) => {
2114
2378
  if (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL") {
@@ -2185,7 +2449,7 @@ async function startWebUI(opts = {}) {
2185
2449
  }
2186
2450
  case "abort":
2187
2451
  runLock?.abort();
2188
- broadcast({ type: "error", payload: { phase: "abort", message: "User aborted" } });
2452
+ broadcast(clients, { type: "error", payload: { phase: "abort", message: "User aborted" } });
2189
2453
  break;
2190
2454
  case "ping":
2191
2455
  send(ws, { type: "pong", payload: {} });
@@ -2204,7 +2468,7 @@ async function startWebUI(opts = {}) {
2204
2468
  context.fileMtimes.clear();
2205
2469
  tokenCounter.reset();
2206
2470
  sessionStartedAt = Date.now();
2207
- broadcast({ type: "session.start", payload: await sessionStartPayload() });
2471
+ broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
2208
2472
  break;
2209
2473
  }
2210
2474
  case "context.clear": {
@@ -2214,7 +2478,7 @@ async function startWebUI(opts = {}) {
2214
2478
  context.fileMtimes.clear();
2215
2479
  tokenCounter.reset();
2216
2480
  sendResult(ws, true, "Context cleared");
2217
- broadcast({
2481
+ broadcast(clients, {
2218
2482
  type: "session.start",
2219
2483
  payload: { ...await sessionStartPayload(), reset: true }
2220
2484
  });
@@ -2273,7 +2537,7 @@ async function startWebUI(opts = {}) {
2273
2537
  beforeMessages,
2274
2538
  afterMessages: context.messages.length
2275
2539
  };
2276
- broadcast({ type: "context.repaired", payload });
2540
+ broadcast(clients, { type: "context.repaired", payload });
2277
2541
  const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
2278
2542
  sendResult(
2279
2543
  ws,
@@ -2311,7 +2575,7 @@ async function startWebUI(opts = {}) {
2311
2575
  context.meta["contextWindowMode"] = policy.id;
2312
2576
  context.meta["contextWindowPolicy"] = policy;
2313
2577
  sendResult(ws, true, `Context mode switched to ${policy.id}`);
2314
- broadcast({
2578
+ broadcast(clients, {
2315
2579
  type: "context.mode.changed",
2316
2580
  payload: { id: policy.id, name: policy.name, policy }
2317
2581
  });
@@ -2337,7 +2601,7 @@ async function startWebUI(opts = {}) {
2337
2601
  break;
2338
2602
  }
2339
2603
  case "providers.saved": {
2340
- const saved = await loadSavedProviders();
2604
+ const saved = await providerHandlers.loadConfigProviders();
2341
2605
  send(ws, {
2342
2606
  type: "providers.saved",
2343
2607
  payload: {
@@ -2396,11 +2660,11 @@ async function startWebUI(opts = {}) {
2396
2660
  updateAutoCompactionMaxContext?.(newProv);
2397
2661
  try {
2398
2662
  configWriteLock = configWriteLock.then(async () => {
2399
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2663
+ const raw = await fs4.readFile(globalConfigPath, "utf8");
2400
2664
  const parsed = JSON.parse(raw);
2401
2665
  parsed.provider = newProvider;
2402
2666
  parsed.model = newModel;
2403
- await atomicWrite(globalConfigPath, JSON.stringify(parsed, null, 2));
2667
+ await atomicWrite3(globalConfigPath, JSON.stringify(parsed, null, 2));
2404
2668
  });
2405
2669
  await configWriteLock;
2406
2670
  } catch (err) {
@@ -2420,33 +2684,33 @@ async function startWebUI(opts = {}) {
2420
2684
  });
2421
2685
  break;
2422
2686
  }
2423
- broadcast({ type: "session.start", payload: await sessionStartPayload() });
2687
+ broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
2424
2688
  break;
2425
2689
  }
2426
2690
  case "key.add":
2427
2691
  case "key.update": {
2428
2692
  const { providerId, label, apiKey } = msg.payload;
2429
- await handleKeyUpsert(ws, providerId, label, apiKey);
2693
+ await providerHandlers.handleKeyUpsert(ws, providerId, label, apiKey);
2430
2694
  break;
2431
2695
  }
2432
2696
  case "key.delete": {
2433
2697
  const { providerId, label } = msg.payload;
2434
- await handleKeyDelete(ws, providerId, label);
2698
+ await providerHandlers.handleKeyDelete(ws, providerId, label);
2435
2699
  break;
2436
2700
  }
2437
2701
  case "key.set_active": {
2438
2702
  const { providerId, label } = msg.payload;
2439
- await handleKeySetActive(ws, providerId, label);
2703
+ await providerHandlers.handleKeySetActive(ws, providerId, label);
2440
2704
  break;
2441
2705
  }
2442
2706
  case "provider.add": {
2443
2707
  const p = msg.payload;
2444
- await handleProviderAdd(ws, p);
2708
+ await providerHandlers.handleProviderAdd(ws, p);
2445
2709
  break;
2446
2710
  }
2447
2711
  case "provider.remove": {
2448
2712
  const { providerId } = msg.payload;
2449
- await handleProviderRemove(ws, providerId);
2713
+ await providerHandlers.handleProviderRemove(ws, providerId);
2450
2714
  break;
2451
2715
  }
2452
2716
  case "sessions.list": {
@@ -2509,7 +2773,7 @@ async function startWebUI(opts = {}) {
2509
2773
  tokenCounter.reset();
2510
2774
  tokenCounter.account(resumed.data.usage, config.model);
2511
2775
  sessionStartedAt = Date.now();
2512
- broadcast({
2776
+ broadcast(clients, {
2513
2777
  type: "session.start",
2514
2778
  payload: {
2515
2779
  ...await sessionStartPayload(),
@@ -2649,7 +2913,7 @@ async function startWebUI(opts = {}) {
2649
2913
  case "todos.clear": {
2650
2914
  context.state.replaceTodos([]);
2651
2915
  sendResult(ws, true, "Todos cleared");
2652
- broadcast({ type: "todos.updated", payload: { todos: [] } });
2916
+ broadcast(clients, { type: "todos.updated", payload: { todos: [] } });
2653
2917
  break;
2654
2918
  }
2655
2919
  case "plan.get": {
@@ -2696,7 +2960,7 @@ async function startWebUI(opts = {}) {
2696
2960
  }
2697
2961
  await savePlan(planPath, plan);
2698
2962
  sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
2699
- broadcast({
2963
+ broadcast(clients, {
2700
2964
  type: "plan.updated",
2701
2965
  payload: { plan }
2702
2966
  });
@@ -2713,7 +2977,7 @@ async function startWebUI(opts = {}) {
2713
2977
  if (depth > 8 || results.length >= 600) return;
2714
2978
  let entries = [];
2715
2979
  try {
2716
- entries = await fs2.readdir(dir, { withFileTypes: true });
2980
+ entries = await fs4.readdir(dir, { withFileTypes: true });
2717
2981
  } catch {
2718
2982
  return;
2719
2983
  }
@@ -2723,7 +2987,7 @@ async function startWebUI(opts = {}) {
2723
2987
  const childRel = rel ? `${rel}/${e.name}` : e.name;
2724
2988
  if (e.isDirectory()) {
2725
2989
  if (SKIP_DIRS.has(e.name)) continue;
2726
- await walk(path2.join(dir, e.name), childRel, depth + 1);
2990
+ await walk(path4.join(dir, e.name), childRel, depth + 1);
2727
2991
  } else if (e.isFile()) {
2728
2992
  results.push(childRel);
2729
2993
  }
@@ -2792,7 +3056,7 @@ async function startWebUI(opts = {}) {
2792
3056
  model: config.model
2793
3057
  });
2794
3058
  sendResult(ws, true, `Switched to mode "${id}"`);
2795
- broadcast({
3059
+ broadcast(clients, {
2796
3060
  type: "session.start",
2797
3061
  payload: { ...await sessionStartPayload() }
2798
3062
  });
@@ -2831,92 +3095,37 @@ async function startWebUI(opts = {}) {
2831
3095
  }
2832
3096
  }
2833
3097
  }
2834
- async function loadSavedProviders() {
2835
- try {
2836
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2837
- const parsed = JSON.parse(raw);
2838
- if (!parsed.providers) return {};
2839
- return decryptConfigSecrets(parsed.providers, vault);
2840
- } catch {
2841
- return {};
2842
- }
2843
- }
2844
- async function saveProviders(providers) {
2845
- configWriteLock = configWriteLock.then(async () => {
2846
- let parsed;
2847
- try {
2848
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2849
- parsed = JSON.parse(raw);
2850
- } catch {
2851
- parsed = {};
2852
- }
2853
- parsed["providers"] = providers;
2854
- const encrypted = encryptConfigSecrets(parsed, vault);
2855
- await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
2856
- });
2857
- await configWriteLock;
2858
- }
2859
- function sendResult(ws, success, message) {
2860
- send(ws, { type: "key.operation_result", payload: { success, message } });
2861
- }
2862
- async function handleKeyUpsert(ws, providerId, label, apiKey) {
2863
- try {
2864
- const providers = await loadSavedProviders();
2865
- const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
2866
- if (result.ok) await saveProviders(providers);
2867
- sendResult(ws, result.ok, result.message);
2868
- } catch (err) {
2869
- sendResult(ws, false, errMessage(err));
2870
- }
2871
- }
2872
- async function handleKeyDelete(ws, providerId, label) {
2873
- try {
2874
- const providers = await loadSavedProviders();
2875
- const result = deleteKey(providers, providerId, label);
2876
- if (result.ok) await saveProviders(providers);
2877
- sendResult(ws, result.ok, result.message);
2878
- } catch (err) {
2879
- sendResult(ws, false, errMessage(err));
3098
+ const providerHandlers = createProviderHandlers({
3099
+ globalConfigPath,
3100
+ vault,
3101
+ getConfigWriteLock: () => configWriteLock,
3102
+ setConfigWriteLock: (p) => {
3103
+ configWriteLock = p;
2880
3104
  }
2881
- }
2882
- async function handleKeySetActive(ws, providerId, label) {
2883
- try {
2884
- const providers = await loadSavedProviders();
2885
- const result = setActiveKey(providers, providerId, label);
2886
- if (result.ok) await saveProviders(providers);
2887
- sendResult(ws, result.ok, result.message);
2888
- } catch (err) {
2889
- sendResult(ws, false, errMessage(err));
2890
- }
2891
- }
2892
- async function handleProviderAdd(ws, payload) {
2893
- try {
2894
- const providers = await loadSavedProviders();
2895
- const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
2896
- if (result.ok) await saveProviders(providers);
2897
- sendResult(ws, result.ok, result.message);
2898
- } catch (err) {
2899
- sendResult(ws, false, errMessage(err));
2900
- }
2901
- }
2902
- async function handleProviderRemove(ws, providerId) {
2903
- try {
2904
- const providers = await loadSavedProviders();
2905
- const result = removeProvider(providers, providerId);
2906
- if (result.ok) await saveProviders(providers);
2907
- sendResult(ws, result.ok, result.message);
2908
- } catch (err) {
2909
- sendResult(ws, false, errMessage(err));
2910
- }
2911
- }
3105
+ });
2912
3106
  const httpServer = createHttpServer({
2913
- host: wsHost2,
2914
- distDir: path2.resolve(import.meta.dirname, "../../dist"),
2915
- wsPort: wsPort2
3107
+ host: wsHost,
3108
+ distDir: path4.resolve(import.meta.dirname, "../../dist"),
3109
+ wsPort
2916
3110
  });
2917
- const httpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
2918
- httpServer.listen(httpPort, wsHost2, () => {
2919
- console.log(`[WebUI] HTTP server running on http://${wsHost2}:${httpPort}`);
3111
+ const registryBaseDir = path4.dirname(globalConfigPath);
3112
+ httpServer.listen(httpPort, wsHost, () => {
3113
+ const openUrl = `http://${wsHost}:${httpPort}`;
3114
+ console.log(`[WebUI] HTTP server running on ${openUrl}`);
3115
+ if (opts.open) openBrowser(openUrl);
3116
+ void registerInstance(
3117
+ {
3118
+ pid: process.pid,
3119
+ httpPort,
3120
+ wsPort,
3121
+ host: wsHost,
3122
+ projectRoot,
3123
+ projectName: path4.basename(projectRoot) || projectRoot,
3124
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3125
+ url: `http://${wsHost}:${httpPort}`
3126
+ },
3127
+ registryBaseDir
3128
+ ).catch((err) => console.warn("[WebUI] Could not record instance:", errMessage(err)));
2920
3129
  });
2921
3130
  registerShutdownHandlers({
2922
3131
  flushSession: async () => {
@@ -2928,16 +3137,31 @@ async function startWebUI(opts = {}) {
2928
3137
  await session.close();
2929
3138
  },
2930
3139
  clients: () => clients.keys(),
2931
- servers: [httpServer, wssPrimary, wssSecondary]
3140
+ servers: [httpServer, wssPrimary, wssSecondary],
3141
+ // Drop this instance from the registry on a clean exit so the file reflects
3142
+ // reality. Crash exits are healed by the next register()/list() prune pass.
3143
+ onShutdown: () => unregisterInstance(process.pid, registryBaseDir)
2932
3144
  });
2933
3145
  }
2934
3146
 
2935
3147
  // src/server/entry.ts
2936
- var wsPort = Number.parseInt(process.env["WS_PORT"] ?? "3457", 10);
2937
- var wsHost = process.env["WS_HOST"] ?? "127.0.0.1";
2938
- console.log(`[WebUI] Starting standalone server on ${wsHost}:${wsPort}...`);
2939
- startWebUI({ wsPort, wsHost }).catch((err) => {
2940
- console.error("[WebUI] Fatal error:", err);
2941
- process.exit(1);
2942
- });
3148
+ var argv = process.argv.slice(2);
3149
+ if (argv.includes("--list") || argv.includes("-l") || argv[0] === "ls") {
3150
+ listInstances().then((instances) => {
3151
+ console.log(formatInstances(instances));
3152
+ process.exit(0);
3153
+ }).catch((err) => {
3154
+ console.error("[WebUI] Could not read instance registry:", err);
3155
+ process.exit(1);
3156
+ });
3157
+ } else {
3158
+ const wsPort = Number.parseInt(process.env["WS_PORT"] ?? "3457", 10);
3159
+ const wsHost = process.env["WS_HOST"] ?? "127.0.0.1";
3160
+ const open = argv.includes("--open") || argv.includes("-o") || process.env["WEBUI_OPEN"] === "1";
3161
+ console.log(`[WebUI] Starting standalone server on ${wsHost}:${wsPort}...`);
3162
+ startWebUI({ wsPort, wsHost, open }).catch((err) => {
3163
+ console.error("[WebUI] Fatal error:", err);
3164
+ process.exit(1);
3165
+ });
3166
+ }
2943
3167
  //# sourceMappingURL=entry.js.map