@wrongstack/webui 0.63.4 → 0.66.13

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-CnURnGh-.css +1 -0
  38. package/dist/assets/index-aAReViZF.js +94 -0
  39. package/dist/assets/vendor-DW1jimNH.css +1 -0
  40. package/dist/index.css +333 -200
  41. package/dist/index.css.map +1 -1
  42. package/dist/index.html +4 -3
  43. package/dist/index.js +984 -641
  44. package/dist/index.js.map +1 -1
  45. package/dist/server/entry.js +434 -124
  46. package/dist/server/entry.js.map +1 -1
  47. package/dist/server/index.d.ts +298 -2
  48. package/dist/server/index.js +435 -94
  49. package/dist/server/index.js.map +1 -1
  50. package/package.json +9 -6
  51. package/dist/assets/index-5ECutVTP.css +0 -1
  52. package/dist/assets/index-BRHGqfHg.js +0 -94
  53. /package/dist/assets/{vendor-oYD55Pw4.js → vendor-wUxgMlp-.js} +0 -0
@@ -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;
@@ -1568,6 +1720,89 @@ function removeProvider(providers, providerId) {
1568
1720
  return { ok: true, message: `Provider "${providerId}" removed` };
1569
1721
  }
1570
1722
 
1723
+ // src/server/provider-config-io.ts
1724
+ import * as fs3 from "fs/promises";
1725
+ import * as path3 from "path";
1726
+ import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
1727
+ import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
1728
+ import { DefaultSecretVault } from "@wrongstack/core";
1729
+ async function loadSavedProviders(configPath, vault) {
1730
+ let raw;
1731
+ try {
1732
+ raw = await fs3.readFile(configPath, "utf8");
1733
+ } catch {
1734
+ return {};
1735
+ }
1736
+ let parsed = {};
1737
+ try {
1738
+ parsed = JSON.parse(raw);
1739
+ } catch {
1740
+ return {};
1741
+ }
1742
+ if (!parsed.providers) return {};
1743
+ return decryptConfigSecrets(parsed.providers, vault);
1744
+ }
1745
+ async function saveProviders(configPath, vault, providers) {
1746
+ let raw;
1747
+ let fileExists = true;
1748
+ try {
1749
+ raw = await fs3.readFile(configPath, "utf8");
1750
+ } catch (err) {
1751
+ if (err.code !== "ENOENT") {
1752
+ throw new Error(
1753
+ `Refusing to mutate ${configPath}: ${err.message}`,
1754
+ { cause: err }
1755
+ );
1756
+ }
1757
+ fileExists = false;
1758
+ raw = "{}";
1759
+ }
1760
+ let parsed;
1761
+ try {
1762
+ parsed = JSON.parse(raw);
1763
+ } catch (err) {
1764
+ if (fileExists) {
1765
+ throw new Error(
1766
+ `Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
1767
+ { cause: err }
1768
+ );
1769
+ }
1770
+ parsed = {};
1771
+ }
1772
+ parsed.providers = providers;
1773
+ const encrypted = encryptConfigSecrets(parsed, vault);
1774
+ await atomicWrite2(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
1775
+ }
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
+
1571
1806
  // src/server/token-estimator.ts
1572
1807
  function estimateTokens(s) {
1573
1808
  return Math.ceil(s.length / 4);
@@ -1626,12 +1861,23 @@ function estimateContextBreakdown(input) {
1626
1861
  }
1627
1862
 
1628
1863
  // src/server/index.ts
1629
- function errMessage(err) {
1630
- return err instanceof Error ? err.message : String(err);
1631
- }
1632
1864
  async function startWebUI(opts = {}) {
1633
- const wsPort2 = opts.wsPort ?? 3457;
1634
- const wsHost2 = opts.wsHost ?? "127.0.0.1";
1865
+ const requestedWsPort = opts.wsPort ?? 3457;
1866
+ const wsHost = opts.wsHost ?? "127.0.0.1";
1867
+ const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
1868
+ const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
1869
+ let wsPort = requestedWsPort;
1870
+ let httpPort = requestedHttpPort;
1871
+ if (!strictPort) {
1872
+ httpPort = await findFreePort(wsHost, requestedHttpPort);
1873
+ wsPort = await findFreePort(wsHost, requestedWsPort, { exclude: /* @__PURE__ */ new Set([httpPort]) });
1874
+ if (httpPort !== requestedHttpPort) {
1875
+ console.warn(`[WebUI] HTTP port ${requestedHttpPort} in use \u2192 using ${httpPort}`);
1876
+ }
1877
+ if (wsPort !== requestedWsPort) {
1878
+ console.warn(`[WebUI] WS port ${requestedWsPort} in use \u2192 using ${wsPort}`);
1879
+ }
1880
+ }
1635
1881
  console.log("[WebUI] Starting backend services...");
1636
1882
  const boot = await bootConfig();
1637
1883
  const { config: baseConfig, vault, globalConfigPath, projectRoot, wpaths, logger } = boot;
@@ -1885,41 +2131,42 @@ async function startWebUI(opts = {}) {
1885
2131
  inputCost,
1886
2132
  outputCost,
1887
2133
  cacheReadCost,
1888
- projectName: path2.basename(projectRoot) || projectRoot,
2134
+ projectName: path4.basename(projectRoot) || projectRoot,
1889
2135
  cwd: projectRoot,
1890
2136
  mode: modeId,
1891
2137
  contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
1892
2138
  wsToken
1893
2139
  };
1894
2140
  }
1895
- const wsToken = randomBytes(16).toString("hex");
2141
+ const wsToken = generateAuthToken();
1896
2142
  console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
1897
2143
  const verifyClient2 = (info) => verifyClient({
1898
2144
  origin: info.origin,
1899
2145
  url: info.req.url ?? "",
1900
2146
  hostHeader: info.req.headers.host,
1901
2147
  remoteAddress: info.req.socket.remoteAddress,
1902
- wsHost: wsHost2,
2148
+ wsHost,
1903
2149
  expectedToken: wsToken
1904
2150
  });
1905
2151
  const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
1906
2152
  const wssPrimary = new WebSocketServer({
1907
- port: wsPort2,
1908
- host: wsHost2,
2153
+ port: wsPort,
2154
+ host: wsHost,
1909
2155
  verifyClient: verifyClient2,
1910
2156
  maxPayload: WS_MAX_PAYLOAD
1911
2157
  });
1912
- const wssSecondary = wsHost2 === "127.0.0.1" ? new WebSocketServer({
1913
- port: wsPort2,
2158
+ const wssSecondary = wsHost === "127.0.0.1" ? new WebSocketServer({
2159
+ port: wsPort,
1914
2160
  host: "::1",
1915
2161
  verifyClient: verifyClient2,
1916
2162
  maxPayload: WS_MAX_PAYLOAD
1917
2163
  }) : null;
1918
2164
  const clients = /* @__PURE__ */ new Map();
1919
- const RATE_LIMIT_MESSAGES = 60;
2165
+ const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
1920
2166
  const RATE_LIMIT_WINDOW_MS = 6e4;
1921
2167
  const rateLimits = /* @__PURE__ */ new Map();
1922
2168
  function checkRateLimit(ws, client) {
2169
+ if (RATE_LIMIT_MESSAGES <= 0) return true;
1923
2170
  const now = Date.now();
1924
2171
  const key = client.sessionId ?? String(ws);
1925
2172
  const limit = rateLimits.get(key);
@@ -1933,30 +2180,30 @@ async function startWebUI(opts = {}) {
1933
2180
  }
1934
2181
  let runLock = null;
1935
2182
  console.log(
1936
- `[WebUI] WebSocket server running on ws://${wsHost2}:${wsPort2}` + (wssSecondary ? ` (and ws://[::1]:${wsPort2})` : "")
2183
+ `[WebUI] WebSocket server running on ws://${wsHost}:${wsPort}` + (wssSecondary ? ` (and ws://[::1]:${wsPort})` : "")
1937
2184
  );
1938
2185
  const pendingConfirms = /* @__PURE__ */ new Map();
1939
2186
  function setupEvents() {
1940
2187
  events.on("iteration.started", (e) => {
1941
- broadcast({
2188
+ broadcast(clients, {
1942
2189
  type: "iteration.started",
1943
2190
  payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
1944
2191
  });
1945
2192
  });
1946
2193
  events.on("provider.text_delta", (e) => {
1947
- broadcast({ type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
2194
+ broadcast(clients, { type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
1948
2195
  });
1949
2196
  events.on("provider.thinking_delta", (e) => {
1950
- broadcast({ type: "provider.thinking_delta", payload: { text: e.text } });
2197
+ broadcast(clients, { type: "provider.thinking_delta", payload: { text: e.text } });
1951
2198
  });
1952
2199
  events.on("tool.started", (e) => {
1953
- broadcast({
2200
+ broadcast(clients, {
1954
2201
  type: "tool.started",
1955
2202
  payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
1956
2203
  });
1957
2204
  });
1958
2205
  events.on("tool.progress", (e) => {
1959
- broadcast({
2206
+ broadcast(clients, {
1960
2207
  type: "tool.progress",
1961
2208
  payload: {
1962
2209
  id: e.id,
@@ -1967,7 +2214,7 @@ async function startWebUI(opts = {}) {
1967
2214
  });
1968
2215
  });
1969
2216
  events.on("tool.executed", (e) => {
1970
- broadcast({
2217
+ broadcast(clients, {
1971
2218
  type: "tool.executed",
1972
2219
  payload: {
1973
2220
  // Forward the tool_use id so frontend can correlate with the
@@ -1982,13 +2229,13 @@ async function startWebUI(opts = {}) {
1982
2229
  output: e.output
1983
2230
  }
1984
2231
  });
1985
- broadcast({
2232
+ broadcast(clients, {
1986
2233
  type: "todos.updated",
1987
2234
  payload: { todos: [...context.todos] }
1988
2235
  });
1989
2236
  });
1990
2237
  events.on("provider.response", (e) => {
1991
- broadcast({
2238
+ broadcast(clients, {
1992
2239
  type: "provider.response",
1993
2240
  payload: {
1994
2241
  usage: e.usage,
@@ -1998,7 +2245,7 @@ async function startWebUI(opts = {}) {
1998
2245
  });
1999
2246
  });
2000
2247
  events.on("context.repaired", (e) => {
2001
- broadcast({
2248
+ broadcast(clients, {
2002
2249
  type: "context.repaired",
2003
2250
  payload: {
2004
2251
  removedToolUses: e.removedToolUses,
@@ -2010,7 +2257,7 @@ async function startWebUI(opts = {}) {
2010
2257
  events.on("tool.confirm_needed", (e) => {
2011
2258
  const id = e.toolUseId ?? `confirm_${Date.now()}`;
2012
2259
  pendingConfirms.set(id, e.resolve);
2013
- broadcast({
2260
+ broadcast(clients, {
2014
2261
  type: "tool.confirm_needed",
2015
2262
  payload: {
2016
2263
  id,
@@ -2021,7 +2268,7 @@ async function startWebUI(opts = {}) {
2021
2268
  });
2022
2269
  });
2023
2270
  events.on("error", (e) => {
2024
- broadcast({
2271
+ broadcast(clients, {
2025
2272
  type: "error",
2026
2273
  payload: {
2027
2274
  phase: e.phase,
@@ -2029,19 +2276,71 @@ async function startWebUI(opts = {}) {
2029
2276
  }
2030
2277
  });
2031
2278
  });
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
- }
2279
+ const forwardSubagent = (kind, payload) => broadcast(clients, { type: "subagent.event", payload: { kind, ...payload } });
2280
+ events.on(
2281
+ "subagent.spawned",
2282
+ (e) => forwardSubagent("spawned", {
2283
+ subagentId: e.subagentId,
2284
+ taskId: e.taskId,
2285
+ name: e.name,
2286
+ provider: e.provider,
2287
+ model: e.model,
2288
+ description: e.description
2289
+ })
2290
+ );
2291
+ events.on(
2292
+ "subagent.task_started",
2293
+ (e) => forwardSubagent("task_started", {
2294
+ subagentId: e.subagentId,
2295
+ taskId: e.taskId,
2296
+ description: e.description
2297
+ })
2298
+ );
2299
+ events.on(
2300
+ "subagent.tool_executed",
2301
+ (e) => forwardSubagent("tool_executed", {
2302
+ subagentId: e.subagentId,
2303
+ toolName: e.name,
2304
+ durationMs: e.durationMs,
2305
+ ok: e.ok
2306
+ })
2307
+ );
2308
+ events.on(
2309
+ "subagent.iteration_summary",
2310
+ (e) => forwardSubagent("iteration_summary", {
2311
+ subagentId: e.subagentId,
2312
+ iteration: e.iteration,
2313
+ toolCalls: e.toolCalls,
2314
+ costUsd: e.costUsd,
2315
+ currentTool: e.currentTool
2316
+ })
2317
+ );
2318
+ events.on(
2319
+ "subagent.budget_extended",
2320
+ (e) => forwardSubagent("budget_extended", {
2321
+ subagentId: e.subagentId,
2322
+ totalExtensions: e.totalExtensions
2323
+ })
2324
+ );
2325
+ events.on(
2326
+ "subagent.ctx_pct",
2327
+ (e) => forwardSubagent("ctx_pct", {
2328
+ subagentId: e.subagentId,
2329
+ load: e.load,
2330
+ tokens: e.tokens,
2331
+ maxContext: e.maxContext
2332
+ })
2333
+ );
2334
+ events.on(
2335
+ "subagent.task_completed",
2336
+ (e) => forwardSubagent("task_completed", {
2337
+ subagentId: e.subagentId,
2338
+ status: e.status,
2339
+ iterations: e.iterations,
2340
+ toolCalls: e.toolCalls,
2341
+ error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0
2342
+ })
2343
+ );
2045
2344
  }
2046
2345
  const handleConnection = (ws) => {
2047
2346
  const client = { ws, sessionId: session.id, connectedAt: Date.now() };
@@ -2102,13 +2401,13 @@ async function startWebUI(opts = {}) {
2102
2401
  console.log(`[WebUI] Backend ready (${label})`);
2103
2402
  setupEvents();
2104
2403
  };
2105
- wssPrimary.on("listening", () => armOnce(`${wsHost2}:${wsPort2}`));
2404
+ wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
2106
2405
  wssPrimary.on("connection", handleConnection);
2107
2406
  wssPrimary.on("error", (err) => {
2108
- console.error(`[WebUI] Primary WS server error (${wsHost2}):`, err);
2407
+ console.error(`[WebUI] Primary WS server error (${wsHost}):`, err);
2109
2408
  });
2110
2409
  if (wssSecondary) {
2111
- wssSecondary.on("listening", () => armOnce(`::1:${wsPort2}`));
2410
+ wssSecondary.on("listening", () => armOnce(`::1:${wsPort}`));
2112
2411
  wssSecondary.on("connection", handleConnection);
2113
2412
  wssSecondary.on("error", (err) => {
2114
2413
  if (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL") {
@@ -2185,7 +2484,7 @@ async function startWebUI(opts = {}) {
2185
2484
  }
2186
2485
  case "abort":
2187
2486
  runLock?.abort();
2188
- broadcast({ type: "error", payload: { phase: "abort", message: "User aborted" } });
2487
+ broadcast(clients, { type: "error", payload: { phase: "abort", message: "User aborted" } });
2189
2488
  break;
2190
2489
  case "ping":
2191
2490
  send(ws, { type: "pong", payload: {} });
@@ -2204,7 +2503,7 @@ async function startWebUI(opts = {}) {
2204
2503
  context.fileMtimes.clear();
2205
2504
  tokenCounter.reset();
2206
2505
  sessionStartedAt = Date.now();
2207
- broadcast({ type: "session.start", payload: await sessionStartPayload() });
2506
+ broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
2208
2507
  break;
2209
2508
  }
2210
2509
  case "context.clear": {
@@ -2214,7 +2513,7 @@ async function startWebUI(opts = {}) {
2214
2513
  context.fileMtimes.clear();
2215
2514
  tokenCounter.reset();
2216
2515
  sendResult(ws, true, "Context cleared");
2217
- broadcast({
2516
+ broadcast(clients, {
2218
2517
  type: "session.start",
2219
2518
  payload: { ...await sessionStartPayload(), reset: true }
2220
2519
  });
@@ -2273,7 +2572,7 @@ async function startWebUI(opts = {}) {
2273
2572
  beforeMessages,
2274
2573
  afterMessages: context.messages.length
2275
2574
  };
2276
- broadcast({ type: "context.repaired", payload });
2575
+ broadcast(clients, { type: "context.repaired", payload });
2277
2576
  const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
2278
2577
  sendResult(
2279
2578
  ws,
@@ -2311,7 +2610,7 @@ async function startWebUI(opts = {}) {
2311
2610
  context.meta["contextWindowMode"] = policy.id;
2312
2611
  context.meta["contextWindowPolicy"] = policy;
2313
2612
  sendResult(ws, true, `Context mode switched to ${policy.id}`);
2314
- broadcast({
2613
+ broadcast(clients, {
2315
2614
  type: "context.mode.changed",
2316
2615
  payload: { id: policy.id, name: policy.name, policy }
2317
2616
  });
@@ -2337,7 +2636,7 @@ async function startWebUI(opts = {}) {
2337
2636
  break;
2338
2637
  }
2339
2638
  case "providers.saved": {
2340
- const saved = await loadSavedProviders();
2639
+ const saved = await loadConfigProviders();
2341
2640
  send(ws, {
2342
2641
  type: "providers.saved",
2343
2642
  payload: {
@@ -2396,11 +2695,11 @@ async function startWebUI(opts = {}) {
2396
2695
  updateAutoCompactionMaxContext?.(newProv);
2397
2696
  try {
2398
2697
  configWriteLock = configWriteLock.then(async () => {
2399
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2698
+ const raw = await fs4.readFile(globalConfigPath, "utf8");
2400
2699
  const parsed = JSON.parse(raw);
2401
2700
  parsed.provider = newProvider;
2402
2701
  parsed.model = newModel;
2403
- await atomicWrite(globalConfigPath, JSON.stringify(parsed, null, 2));
2702
+ await atomicWrite3(globalConfigPath, JSON.stringify(parsed, null, 2));
2404
2703
  });
2405
2704
  await configWriteLock;
2406
2705
  } catch (err) {
@@ -2420,7 +2719,7 @@ async function startWebUI(opts = {}) {
2420
2719
  });
2421
2720
  break;
2422
2721
  }
2423
- broadcast({ type: "session.start", payload: await sessionStartPayload() });
2722
+ broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
2424
2723
  break;
2425
2724
  }
2426
2725
  case "key.add":
@@ -2509,7 +2808,7 @@ async function startWebUI(opts = {}) {
2509
2808
  tokenCounter.reset();
2510
2809
  tokenCounter.account(resumed.data.usage, config.model);
2511
2810
  sessionStartedAt = Date.now();
2512
- broadcast({
2811
+ broadcast(clients, {
2513
2812
  type: "session.start",
2514
2813
  payload: {
2515
2814
  ...await sessionStartPayload(),
@@ -2649,7 +2948,7 @@ async function startWebUI(opts = {}) {
2649
2948
  case "todos.clear": {
2650
2949
  context.state.replaceTodos([]);
2651
2950
  sendResult(ws, true, "Todos cleared");
2652
- broadcast({ type: "todos.updated", payload: { todos: [] } });
2951
+ broadcast(clients, { type: "todos.updated", payload: { todos: [] } });
2653
2952
  break;
2654
2953
  }
2655
2954
  case "plan.get": {
@@ -2696,7 +2995,7 @@ async function startWebUI(opts = {}) {
2696
2995
  }
2697
2996
  await savePlan(planPath, plan);
2698
2997
  sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
2699
- broadcast({
2998
+ broadcast(clients, {
2700
2999
  type: "plan.updated",
2701
3000
  payload: { plan }
2702
3001
  });
@@ -2713,7 +3012,7 @@ async function startWebUI(opts = {}) {
2713
3012
  if (depth > 8 || results.length >= 600) return;
2714
3013
  let entries = [];
2715
3014
  try {
2716
- entries = await fs2.readdir(dir, { withFileTypes: true });
3015
+ entries = await fs4.readdir(dir, { withFileTypes: true });
2717
3016
  } catch {
2718
3017
  return;
2719
3018
  }
@@ -2723,7 +3022,7 @@ async function startWebUI(opts = {}) {
2723
3022
  const childRel = rel ? `${rel}/${e.name}` : e.name;
2724
3023
  if (e.isDirectory()) {
2725
3024
  if (SKIP_DIRS.has(e.name)) continue;
2726
- await walk(path2.join(dir, e.name), childRel, depth + 1);
3025
+ await walk(path4.join(dir, e.name), childRel, depth + 1);
2727
3026
  } else if (e.isFile()) {
2728
3027
  results.push(childRel);
2729
3028
  }
@@ -2792,7 +3091,7 @@ async function startWebUI(opts = {}) {
2792
3091
  model: config.model
2793
3092
  });
2794
3093
  sendResult(ws, true, `Switched to mode "${id}"`);
2795
- broadcast({
3094
+ broadcast(clients, {
2796
3095
  type: "session.start",
2797
3096
  payload: { ...await sessionStartPayload() }
2798
3097
  });
@@ -2831,39 +3130,20 @@ async function startWebUI(opts = {}) {
2831
3130
  }
2832
3131
  }
2833
3132
  }
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
- }
3133
+ async function loadConfigProviders() {
3134
+ return loadSavedProviders(globalConfigPath, vault);
2843
3135
  }
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
- });
3136
+ async function saveConfigProviders(providers) {
3137
+ configWriteLock = configWriteLock.then(
3138
+ () => saveProviders(globalConfigPath, vault, providers)
3139
+ );
2857
3140
  await configWriteLock;
2858
3141
  }
2859
- function sendResult(ws, success, message) {
2860
- send(ws, { type: "key.operation_result", payload: { success, message } });
2861
- }
2862
3142
  async function handleKeyUpsert(ws, providerId, label, apiKey) {
2863
3143
  try {
2864
- const providers = await loadSavedProviders();
3144
+ const providers = await loadConfigProviders();
2865
3145
  const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
2866
- if (result.ok) await saveProviders(providers);
3146
+ if (result.ok) await saveConfigProviders(providers);
2867
3147
  sendResult(ws, result.ok, result.message);
2868
3148
  } catch (err) {
2869
3149
  sendResult(ws, false, errMessage(err));
@@ -2871,9 +3151,9 @@ async function startWebUI(opts = {}) {
2871
3151
  }
2872
3152
  async function handleKeyDelete(ws, providerId, label) {
2873
3153
  try {
2874
- const providers = await loadSavedProviders();
3154
+ const providers = await loadConfigProviders();
2875
3155
  const result = deleteKey(providers, providerId, label);
2876
- if (result.ok) await saveProviders(providers);
3156
+ if (result.ok) await saveConfigProviders(providers);
2877
3157
  sendResult(ws, result.ok, result.message);
2878
3158
  } catch (err) {
2879
3159
  sendResult(ws, false, errMessage(err));
@@ -2881,9 +3161,9 @@ async function startWebUI(opts = {}) {
2881
3161
  }
2882
3162
  async function handleKeySetActive(ws, providerId, label) {
2883
3163
  try {
2884
- const providers = await loadSavedProviders();
3164
+ const providers = await loadConfigProviders();
2885
3165
  const result = setActiveKey(providers, providerId, label);
2886
- if (result.ok) await saveProviders(providers);
3166
+ if (result.ok) await saveConfigProviders(providers);
2887
3167
  sendResult(ws, result.ok, result.message);
2888
3168
  } catch (err) {
2889
3169
  sendResult(ws, false, errMessage(err));
@@ -2891,9 +3171,9 @@ async function startWebUI(opts = {}) {
2891
3171
  }
2892
3172
  async function handleProviderAdd(ws, payload) {
2893
3173
  try {
2894
- const providers = await loadSavedProviders();
3174
+ const providers = await loadConfigProviders();
2895
3175
  const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
2896
- if (result.ok) await saveProviders(providers);
3176
+ if (result.ok) await saveConfigProviders(providers);
2897
3177
  sendResult(ws, result.ok, result.message);
2898
3178
  } catch (err) {
2899
3179
  sendResult(ws, false, errMessage(err));
@@ -2901,22 +3181,37 @@ async function startWebUI(opts = {}) {
2901
3181
  }
2902
3182
  async function handleProviderRemove(ws, providerId) {
2903
3183
  try {
2904
- const providers = await loadSavedProviders();
3184
+ const providers = await loadConfigProviders();
2905
3185
  const result = removeProvider(providers, providerId);
2906
- if (result.ok) await saveProviders(providers);
3186
+ if (result.ok) await saveConfigProviders(providers);
2907
3187
  sendResult(ws, result.ok, result.message);
2908
3188
  } catch (err) {
2909
3189
  sendResult(ws, false, errMessage(err));
2910
3190
  }
2911
3191
  }
2912
3192
  const httpServer = createHttpServer({
2913
- host: wsHost2,
2914
- distDir: path2.resolve(import.meta.dirname, "../../dist"),
2915
- wsPort: wsPort2
3193
+ host: wsHost,
3194
+ distDir: path4.resolve(import.meta.dirname, "../../dist"),
3195
+ wsPort
2916
3196
  });
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}`);
3197
+ const registryBaseDir = path4.dirname(globalConfigPath);
3198
+ httpServer.listen(httpPort, wsHost, () => {
3199
+ const openUrl = `http://${wsHost}:${httpPort}`;
3200
+ console.log(`[WebUI] HTTP server running on ${openUrl}`);
3201
+ if (opts.open) openBrowser(openUrl);
3202
+ void registerInstance(
3203
+ {
3204
+ pid: process.pid,
3205
+ httpPort,
3206
+ wsPort,
3207
+ host: wsHost,
3208
+ projectRoot,
3209
+ projectName: path4.basename(projectRoot) || projectRoot,
3210
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3211
+ url: `http://${wsHost}:${httpPort}`
3212
+ },
3213
+ registryBaseDir
3214
+ ).catch((err) => console.warn("[WebUI] Could not record instance:", errMessage(err)));
2920
3215
  });
2921
3216
  registerShutdownHandlers({
2922
3217
  flushSession: async () => {
@@ -2928,16 +3223,31 @@ async function startWebUI(opts = {}) {
2928
3223
  await session.close();
2929
3224
  },
2930
3225
  clients: () => clients.keys(),
2931
- servers: [httpServer, wssPrimary, wssSecondary]
3226
+ servers: [httpServer, wssPrimary, wssSecondary],
3227
+ // Drop this instance from the registry on a clean exit so the file reflects
3228
+ // reality. Crash exits are healed by the next register()/list() prune pass.
3229
+ onShutdown: () => unregisterInstance(process.pid, registryBaseDir)
2932
3230
  });
2933
3231
  }
2934
3232
 
2935
3233
  // 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
- });
3234
+ var argv = process.argv.slice(2);
3235
+ if (argv.includes("--list") || argv.includes("-l") || argv[0] === "ls") {
3236
+ listInstances().then((instances) => {
3237
+ console.log(formatInstances(instances));
3238
+ process.exit(0);
3239
+ }).catch((err) => {
3240
+ console.error("[WebUI] Could not read instance registry:", err);
3241
+ process.exit(1);
3242
+ });
3243
+ } else {
3244
+ const wsPort = Number.parseInt(process.env["WS_PORT"] ?? "3457", 10);
3245
+ const wsHost = process.env["WS_HOST"] ?? "127.0.0.1";
3246
+ const open = argv.includes("--open") || argv.includes("-o") || process.env["WEBUI_OPEN"] === "1";
3247
+ console.log(`[WebUI] Starting standalone server on ${wsHost}:${wsPort}...`);
3248
+ startWebUI({ wsPort, wsHost, open }).catch((err) => {
3249
+ console.error("[WebUI] Fatal error:", err);
3250
+ process.exit(1);
3251
+ });
3252
+ }
2943
3253
  //# sourceMappingURL=entry.js.map