@wrongstack/webui 0.32.0 → 0.51.3

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.
@@ -1,7 +1,137 @@
1
1
  // src/server/index.ts
2
2
  import * as fs2 from "fs/promises";
3
+ import * as path2 from "path";
4
+
5
+ // src/server/http-server.ts
6
+ import * as fs from "fs/promises";
3
7
  import * as http from "http";
4
8
  import * as path from "path";
9
+ var MIME_TYPES = {
10
+ ".html": "text/html",
11
+ ".js": "application/javascript",
12
+ ".css": "text/css",
13
+ ".json": "application/json",
14
+ ".svg": "image/svg+xml",
15
+ ".png": "image/png",
16
+ ".ico": "image/x-icon"
17
+ };
18
+ function buildCspHeader(wsPort) {
19
+ 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
+ }
21
+ function isInsideDist(candidate, distDir) {
22
+ const root = path.resolve(distDir);
23
+ const resolved = path.resolve(candidate);
24
+ return resolved === root || resolved.startsWith(root + path.sep);
25
+ }
26
+ function createHttpServer(opts) {
27
+ const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
28
+ const distDir = path.resolve(opts.distDir);
29
+ const wsPort = opts.wsPort;
30
+ return http.createServer(async (req, res) => {
31
+ try {
32
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
33
+ let filePath;
34
+ if (url.pathname === "/" || url.pathname === "") {
35
+ filePath = path.join(distDir, "index.html");
36
+ } else if (url.pathname.startsWith("/assets/")) {
37
+ filePath = path.join(distDir, url.pathname);
38
+ } else if (url.pathname.startsWith("/")) {
39
+ filePath = path.join(distDir, url.pathname);
40
+ } else {
41
+ filePath = path.join(distDir, "index.html");
42
+ }
43
+ const resolvedPath = path.resolve(filePath);
44
+ if (!isInsideDist(resolvedPath, distDir)) {
45
+ res.writeHead(403, { "Content-Type": "text/plain" });
46
+ res.end("Forbidden");
47
+ return;
48
+ }
49
+ const ext = path.extname(resolvedPath);
50
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
51
+ res.setHeader("Content-Type", contentType);
52
+ res.setHeader("X-Content-Type-Options", "nosniff");
53
+ res.setHeader("X-Frame-Options", "DENY");
54
+ res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
55
+ if (ext === ".html") {
56
+ res.setHeader("Cache-Control", "no-cache");
57
+ res.setHeader("Content-Security-Policy", buildCspHeader(wsPort));
58
+ }
59
+ const fileContent = await fs.readFile(resolvedPath);
60
+ res.writeHead(200);
61
+ res.end(fileContent);
62
+ } catch (err) {
63
+ if (err.code === "ENOENT") {
64
+ try {
65
+ const fileContent = await fs.readFile(path.join(distDir, "index.html"));
66
+ res.writeHead(200, {
67
+ "Content-Type": "text/html",
68
+ "X-Content-Type-Options": "nosniff",
69
+ "X-Frame-Options": "DENY",
70
+ "Referrer-Policy": "strict-origin-when-cross-origin",
71
+ "Content-Security-Policy": buildCspHeader(wsPort)
72
+ });
73
+ res.end(fileContent);
74
+ } catch {
75
+ res.writeHead(404);
76
+ res.end("Not found");
77
+ }
78
+ } else {
79
+ res.writeHead(500);
80
+ res.end("Server error");
81
+ }
82
+ }
83
+ });
84
+ }
85
+
86
+ // src/server/file-picker.ts
87
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
88
+ ".git",
89
+ "node_modules",
90
+ "dist",
91
+ "build",
92
+ ".next",
93
+ ".turbo",
94
+ ".cache",
95
+ "target",
96
+ "coverage",
97
+ ".nyc_output",
98
+ "out",
99
+ ".pnpm-store",
100
+ ".parcel-cache"
101
+ ]);
102
+ var KEEP_DOTFILES = /* @__PURE__ */ new Set([
103
+ ".wrongstack",
104
+ ".env.example",
105
+ ".gitignore",
106
+ ".eslintrc",
107
+ ".prettierrc"
108
+ ]);
109
+ function isHiddenEntry(name) {
110
+ return name.startsWith(".") && !KEEP_DOTFILES.has(name);
111
+ }
112
+ function rankFiles(paths, query, limit) {
113
+ const q = query.toLowerCase();
114
+ const scored = [];
115
+ for (const p of paths) {
116
+ if (!q) {
117
+ scored.push({ path: p, score: 0 });
118
+ continue;
119
+ }
120
+ const lower = p.toLowerCase();
121
+ const base = lower.split("/").pop() ?? lower;
122
+ let score = 0;
123
+ if (base === q) score = 100;
124
+ else if (base.startsWith(q)) score = 60;
125
+ else if (lower.includes(q)) score = 20;
126
+ else continue;
127
+ score -= p.split("/").length;
128
+ scored.push({ path: p, score });
129
+ }
130
+ scored.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
131
+ return scored.slice(0, limit).map((s) => s.path);
132
+ }
133
+
134
+ // src/server/index.ts
5
135
  import {
6
136
  Agent,
7
137
  AutoCompactionMiddleware,
@@ -37,7 +167,7 @@ import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/sec
37
167
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
38
168
  import { builtinToolsPack, forgetTool, rememberTool } from "@wrongstack/tools";
39
169
  import { WebSocket, WebSocketServer } from "ws";
40
- import { randomBytes, timingSafeEqual } from "crypto";
170
+ import { randomBytes } from "crypto";
41
171
 
42
172
  // ../runtime/src/container.ts
43
173
  import {
@@ -111,43 +241,14 @@ function createDefaultContainer(opts) {
111
241
  }
112
242
 
113
243
  // src/server/boot.ts
114
- import * as fs from "fs/promises";
115
- import * as os from "os";
116
244
  import {
117
- DefaultConfigLoader,
118
- DefaultLogger,
119
- DefaultPathResolver,
120
- DefaultSecretVault,
121
- migratePlaintextSecrets,
122
- resolveWstackPaths
245
+ bootConfig as coreBootConfig
123
246
  } from "@wrongstack/core";
124
247
  async function bootConfig() {
125
- const cwd = process.cwd();
126
- const pathResolver = new DefaultPathResolver(cwd);
127
- const projectRoot = pathResolver.projectRoot;
128
- const userHome = os.homedir();
129
- const wpaths = resolveWstackPaths({ projectRoot, userHome });
130
- await fs.mkdir(wpaths.globalRoot, { recursive: true });
131
- await fs.mkdir(wpaths.projectDir, { recursive: true });
132
- await fs.mkdir(wpaths.projectSessions, { recursive: true });
133
- const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey });
134
- for (const file of [wpaths.globalConfig, wpaths.projectLocalConfig]) {
135
- try {
136
- const { migrated } = await migratePlaintextSecrets(file, vault);
137
- if (migrated > 0) {
138
- process.stderr.write(`[WebUI] Encrypted ${migrated} plaintext secret(s) in ${file}
139
- `);
140
- }
141
- } catch {
142
- }
143
- }
144
- const configLoader = new DefaultConfigLoader({ paths: wpaths, vault });
145
- const config = await configLoader.load({ cliFlags: {} });
146
- const logger = new DefaultLogger({
147
- level: config.log?.level ?? "info",
148
- file: wpaths.logFile
248
+ const { config, vault, globalConfigPath, projectRoot, wpaths, logger } = await coreBootConfig({
249
+ appLabel: "WebUI"
149
250
  });
150
- return { config, vault, globalConfigPath: wpaths.globalConfig, projectRoot, wpaths, logger };
251
+ return { config, vault, globalConfigPath, projectRoot, wpaths, logger };
151
252
  }
152
253
  function patchConfig(config, updates) {
153
254
  return Object.freeze({ ...config, ...updates });
@@ -1278,7 +1379,255 @@ var WorktreeWebSocketHandler = class {
1278
1379
  }
1279
1380
  };
1280
1381
 
1382
+ // src/server/ws-auth.ts
1383
+ import { Buffer } from "buffer";
1384
+ import { timingSafeEqual } from "crypto";
1385
+ function isLoopbackHostname(hostname) {
1386
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1387
+ }
1388
+ function isLoopbackBind(wsHost) {
1389
+ return wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
1390
+ }
1391
+ function tokenMatches(provided, expected) {
1392
+ if (!provided) return false;
1393
+ const a = Buffer.from(provided);
1394
+ const b = Buffer.from(expected);
1395
+ if (a.length !== b.length) return false;
1396
+ return timingSafeEqual(a, b);
1397
+ }
1398
+ function extractToken(url) {
1399
+ const match = url.match(/[?&]token=([^&]+)/);
1400
+ return match ? match[1] : void 0;
1401
+ }
1402
+ function hostHeaderOk(input) {
1403
+ if (!isLoopbackBind(input.wsHost)) return true;
1404
+ const hostHeader = (input.hostHeader ?? "").trim();
1405
+ if (!hostHeader) return false;
1406
+ let hostname;
1407
+ try {
1408
+ hostname = new URL(`http://${hostHeader}`).hostname;
1409
+ } catch {
1410
+ return false;
1411
+ }
1412
+ return isLoopbackHostname(hostname);
1413
+ }
1414
+ function verifyClient(input) {
1415
+ const { origin, url, hostHeader, remoteAddress, wsHost, expectedToken } = input;
1416
+ const tokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
1417
+ if (!hostHeaderOk({ hostHeader, wsHost })) return false;
1418
+ if (!origin) {
1419
+ const remoteIp = remoteAddress ?? "";
1420
+ const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
1421
+ if (!isRemoteLoopback && wsHost === "0.0.0.0") return false;
1422
+ return tokenOk || isLoopbackBind(wsHost);
1423
+ }
1424
+ try {
1425
+ const { hostname } = new URL(origin);
1426
+ if (isLoopbackHostname(hostname)) return true;
1427
+ return tokenOk;
1428
+ } catch {
1429
+ return false;
1430
+ }
1431
+ }
1432
+
1433
+ // src/server/lifecycle.ts
1434
+ function createShutdown(res) {
1435
+ const log = res.log ?? ((m) => console.log(m));
1436
+ const exit = res.exit ?? ((code) => process.exit(code));
1437
+ let shuttingDown = false;
1438
+ return async () => {
1439
+ if (shuttingDown) return;
1440
+ shuttingDown = true;
1441
+ log("[WebUI] Shutting down...");
1442
+ try {
1443
+ await res.flushSession();
1444
+ } catch (e) {
1445
+ log(`[WebUI] Error closing session: ${e instanceof Error ? e.message : String(e)}`);
1446
+ }
1447
+ for (const ws of res.clients()) ws.close();
1448
+ for (const server of res.servers) server?.close();
1449
+ exit(0);
1450
+ };
1451
+ }
1452
+ function registerShutdownHandlers(res) {
1453
+ const shutdown = createShutdown(res);
1454
+ process.on("SIGINT", shutdown);
1455
+ process.on("SIGTERM", shutdown);
1456
+ return () => {
1457
+ process.off("SIGINT", shutdown);
1458
+ process.off("SIGTERM", shutdown);
1459
+ };
1460
+ }
1461
+
1462
+ // src/server/usage-cost.ts
1463
+ function getCostRates(model) {
1464
+ const cost = model?.cost;
1465
+ return {
1466
+ input: cost?.input ?? 0,
1467
+ output: cost?.output ?? 0,
1468
+ cacheRead: cost?.cache_read ?? 0
1469
+ };
1470
+ }
1471
+ function computeUsageCost(usage, rates) {
1472
+ return (usage.input * rates.input + usage.output * rates.output + (usage.cacheRead ?? 0) * rates.cacheRead) / 1e6;
1473
+ }
1474
+
1475
+ // src/server/provider-keys.ts
1476
+ function normalizeKeys(cfg) {
1477
+ if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
1478
+ return cfg.apiKeys.map((k) => ({ ...k }));
1479
+ }
1480
+ if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
1481
+ return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
1482
+ }
1483
+ return [];
1484
+ }
1485
+ function writeKeysBack(cfg, keys) {
1486
+ if (keys.length === 0) {
1487
+ delete cfg.apiKeys;
1488
+ delete cfg.apiKey;
1489
+ delete cfg.activeKey;
1490
+ return;
1491
+ }
1492
+ cfg.apiKeys = keys;
1493
+ const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
1494
+ cfg.apiKey = active.apiKey;
1495
+ if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
1496
+ cfg.activeKey = active.label;
1497
+ }
1498
+ }
1499
+ function maskedKey(key) {
1500
+ if (!key) return "\u2014";
1501
+ if (key.length <= 8) return "\u2022".repeat(key.length);
1502
+ return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
1503
+ }
1504
+ function upsertKey(providers, providerId, label, apiKey, nowIso) {
1505
+ const existing = providers[providerId] ?? { type: providerId };
1506
+ const keys = normalizeKeys(existing);
1507
+ const idx = keys.findIndex((k) => k.label === label);
1508
+ if (idx >= 0) {
1509
+ keys[idx] = { ...keys[idx], apiKey, createdAt: nowIso };
1510
+ } else {
1511
+ keys.push({ label, apiKey, createdAt: nowIso });
1512
+ }
1513
+ writeKeysBack(existing, keys);
1514
+ if (!existing.activeKey) existing.activeKey = label;
1515
+ providers[providerId] = existing;
1516
+ return { ok: true, message: `Key "${label}" saved for ${providerId}` };
1517
+ }
1518
+ function deleteKey(providers, providerId, label) {
1519
+ const existing = providers[providerId];
1520
+ if (!existing) {
1521
+ return { ok: false, message: `Provider "${providerId}" not found` };
1522
+ }
1523
+ const keys = normalizeKeys(existing).filter((k) => k.label !== label);
1524
+ if (keys.length === 0) {
1525
+ delete providers[providerId];
1526
+ } else {
1527
+ writeKeysBack(existing, keys);
1528
+ if (existing.activeKey === label) existing.activeKey = keys[0].label;
1529
+ providers[providerId] = existing;
1530
+ }
1531
+ return { ok: true, message: `Key "${label}" deleted from ${providerId}` };
1532
+ }
1533
+ function setActiveKey(providers, providerId, label) {
1534
+ const existing = providers[providerId];
1535
+ if (!existing) {
1536
+ return { ok: false, message: `Provider "${providerId}" not found` };
1537
+ }
1538
+ existing.activeKey = label;
1539
+ writeKeysBack(existing, normalizeKeys(existing));
1540
+ providers[providerId] = existing;
1541
+ return { ok: true, message: `Active key for ${providerId} set to "${label}"` };
1542
+ }
1543
+ function addProvider(providers, payload, nowIso) {
1544
+ if (providers[payload.id]) {
1545
+ return {
1546
+ ok: false,
1547
+ message: `Provider "${payload.id}" already exists. Use key.add to add a key.`
1548
+ };
1549
+ }
1550
+ const newProv = {
1551
+ type: payload.id,
1552
+ family: payload.family,
1553
+ baseUrl: payload.baseUrl
1554
+ };
1555
+ if (payload.apiKey) {
1556
+ newProv.apiKeys = [{ label: "default", apiKey: payload.apiKey, createdAt: nowIso }];
1557
+ newProv.activeKey = "default";
1558
+ }
1559
+ providers[payload.id] = newProv;
1560
+ return { ok: true, message: `Provider "${payload.id}" added` };
1561
+ }
1562
+ function removeProvider(providers, providerId) {
1563
+ if (!providers[providerId]) {
1564
+ return { ok: false, message: `Provider "${providerId}" not found` };
1565
+ }
1566
+ delete providers[providerId];
1567
+ return { ok: true, message: `Provider "${providerId}" removed` };
1568
+ }
1569
+
1570
+ // src/server/token-estimator.ts
1571
+ function estimateTokens(s) {
1572
+ return Math.ceil(s.length / 4);
1573
+ }
1574
+ function stringifyContent(c) {
1575
+ if (typeof c === "string") return c;
1576
+ try {
1577
+ return JSON.stringify(c);
1578
+ } catch {
1579
+ return String(c);
1580
+ }
1581
+ }
1582
+ function messageTokens(content) {
1583
+ if (typeof content === "string") return estimateTokens(content);
1584
+ if (!Array.isArray(content)) return 0;
1585
+ let tk = 0;
1586
+ for (const b of content) {
1587
+ if (b.type === "text") tk += estimateTokens(b.text ?? "");
1588
+ else if (b.type === "tool_use") tk += estimateTokens(stringifyContent(b.input));
1589
+ else if (b.type === "tool_result") tk += estimateTokens(stringifyContent(b.content));
1590
+ else tk += estimateTokens(stringifyContent(b));
1591
+ }
1592
+ return tk;
1593
+ }
1594
+ function messagePreview(content) {
1595
+ if (typeof content === "string") return content.slice(0, 60);
1596
+ if (!Array.isArray(content)) return "";
1597
+ return content.map(
1598
+ (b) => b.type === "text" ? (b.text ?? "").slice(0, 40) : b.type === "tool_use" ? `[tool_use: ${b.name}]` : b.type === "tool_result" ? "[tool_result]" : `[${b.type}]`
1599
+ ).join(" ").slice(0, 60);
1600
+ }
1601
+ function estimateContextBreakdown(input) {
1602
+ const sysTokens = input.systemPrompt.reduce((acc, b) => acc + estimateTokens(b.text ?? ""), 0);
1603
+ const toolBreakdown = input.tools.map((t) => {
1604
+ const schema = t.inputSchema ?? {};
1605
+ const desc = t.description ?? "";
1606
+ return {
1607
+ name: t.name,
1608
+ tokens: estimateTokens(t.name) + estimateTokens(desc) + estimateTokens(stringifyContent(schema))
1609
+ };
1610
+ });
1611
+ const toolTokens = toolBreakdown.reduce((a, b) => a + b.tokens, 0);
1612
+ const messageBreakdown = input.messages.map((m, i) => ({
1613
+ index: i,
1614
+ role: m.role,
1615
+ tokens: messageTokens(m.content),
1616
+ preview: messagePreview(m.content)
1617
+ }));
1618
+ const msgTokens = messageBreakdown.reduce((a, b) => a + b.tokens, 0);
1619
+ return {
1620
+ total: sysTokens + toolTokens + msgTokens,
1621
+ systemPrompt: sysTokens,
1622
+ tools: { total: toolTokens, count: input.tools.length, breakdown: toolBreakdown },
1623
+ messages: { total: msgTokens, count: input.messages.length, breakdown: messageBreakdown }
1624
+ };
1625
+ }
1626
+
1281
1627
  // src/server/index.ts
1628
+ function errMessage(err) {
1629
+ return err instanceof Error ? err.message : String(err);
1630
+ }
1282
1631
  async function startWebUI(opts = {}) {
1283
1632
  const wsPort = opts.wsPort ?? 3457;
1284
1633
  const wsHost = opts.wsHost ?? "127.0.0.1";
@@ -1513,10 +1862,10 @@ async function startWebUI(opts = {}) {
1513
1862
  try {
1514
1863
  const m = await modelsRegistry.getModel(config.provider, config.model);
1515
1864
  maxContext = m?.capabilities?.maxContext ?? 0;
1516
- const cost = m?.cost;
1517
- inputCost = cost?.input ?? 0;
1518
- outputCost = cost?.output ?? 0;
1519
- cacheReadCost = cost?.cache_read ?? 0;
1865
+ const rates = getCostRates(m);
1866
+ inputCost = rates.input;
1867
+ outputCost = rates.output;
1868
+ cacheReadCost = rates.cacheRead;
1520
1869
  } catch {
1521
1870
  }
1522
1871
  return {
@@ -1527,7 +1876,7 @@ async function startWebUI(opts = {}) {
1527
1876
  inputCost,
1528
1877
  outputCost,
1529
1878
  cacheReadCost,
1530
- projectName: path.basename(projectRoot) || projectRoot,
1879
+ projectName: path2.basename(projectRoot) || projectRoot,
1531
1880
  cwd: projectRoot,
1532
1881
  mode: modeId,
1533
1882
  contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
@@ -1536,59 +1885,25 @@ async function startWebUI(opts = {}) {
1536
1885
  }
1537
1886
  const wsToken = randomBytes(16).toString("hex");
1538
1887
  console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
1539
- const isLoopback = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1540
- const tokenMatches = (provided) => {
1541
- if (!provided) return false;
1542
- const a = Buffer.from(provided);
1543
- const b = Buffer.from(wsToken);
1544
- if (a.length !== b.length) return false;
1545
- return timingSafeEqual(a, b);
1546
- };
1547
- const hostHeaderOk = (req) => {
1548
- const boundToLoopback = wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
1549
- if (!boundToLoopback) return true;
1550
- const hostHeader = (req.headers.host ?? "").trim();
1551
- if (!hostHeader) return false;
1552
- let hostname;
1553
- try {
1554
- hostname = new URL(`http://${hostHeader}`).hostname;
1555
- } catch {
1556
- return false;
1557
- }
1558
- return isLoopback(hostname);
1559
- };
1560
- const verifyClient = (info) => {
1561
- const origin = info.origin;
1562
- const url = info.req.url ?? "";
1563
- const tokenMatch = url.match(/[?&]token=([^&]+)/);
1564
- const providedToken = tokenMatch ? tokenMatch[1] : void 0;
1565
- const tokenOk = tokenMatches(providedToken);
1566
- if (!hostHeaderOk(info.req)) return false;
1567
- if (!origin) {
1568
- const remoteIp = info.req.socket.remoteAddress ?? "";
1569
- const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
1570
- if (!isRemoteLoopback && wsHost === "0.0.0.0") return false;
1571
- return tokenOk || wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
1572
- }
1573
- try {
1574
- const { hostname } = new URL(origin);
1575
- if (isLoopback(hostname)) return true;
1576
- return tokenOk;
1577
- } catch {
1578
- return false;
1579
- }
1580
- };
1888
+ const verifyClient2 = (info) => verifyClient({
1889
+ origin: info.origin,
1890
+ url: info.req.url ?? "",
1891
+ hostHeader: info.req.headers.host,
1892
+ remoteAddress: info.req.socket.remoteAddress,
1893
+ wsHost,
1894
+ expectedToken: wsToken
1895
+ });
1581
1896
  const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
1582
1897
  const wssPrimary = new WebSocketServer({
1583
1898
  port: wsPort,
1584
1899
  host: wsHost,
1585
- verifyClient,
1900
+ verifyClient: verifyClient2,
1586
1901
  maxPayload: WS_MAX_PAYLOAD
1587
1902
  });
1588
1903
  const wssSecondary = wsHost === "127.0.0.1" ? new WebSocketServer({
1589
1904
  port: wsPort,
1590
1905
  host: "::1",
1591
- verifyClient,
1906
+ verifyClient: verifyClient2,
1592
1907
  maxPayload: WS_MAX_PAYLOAD
1593
1908
  }) : null;
1594
1909
  const clients = /* @__PURE__ */ new Map();
@@ -1761,8 +2076,8 @@ async function startWebUI(opts = {}) {
1761
2076
  rateLimits.delete(String(ws));
1762
2077
  console.log("[WebUI] Client disconnected, total:", clients.size);
1763
2078
  if (pendingConfirms.size > 0) {
1764
- for (const [id, resolve2] of pendingConfirms) {
1765
- resolve2("no");
2079
+ for (const [id, resolve3] of pendingConfirms) {
2080
+ resolve3("no");
1766
2081
  pendingConfirms.delete(id);
1767
2082
  }
1768
2083
  }
@@ -1840,7 +2155,7 @@ async function startWebUI(opts = {}) {
1840
2155
  type: "error",
1841
2156
  payload: {
1842
2157
  phase: "agent.run",
1843
- message: err instanceof Error ? err.message : String(err)
2158
+ message: errMessage(err)
1844
2159
  }
1845
2160
  });
1846
2161
  } finally {
@@ -1852,10 +2167,10 @@ async function startWebUI(opts = {}) {
1852
2167
  }
1853
2168
  case "tool.confirm_result": {
1854
2169
  const { id, decision } = msg.payload;
1855
- const resolve2 = pendingConfirms.get(id);
1856
- if (resolve2) {
2170
+ const resolve3 = pendingConfirms.get(id);
2171
+ if (resolve3) {
1857
2172
  pendingConfirms.delete(id);
1858
- resolve2(decision);
2173
+ resolve3(decision);
1859
2174
  }
1860
2175
  break;
1861
2176
  }
@@ -1897,62 +2212,17 @@ async function startWebUI(opts = {}) {
1897
2212
  break;
1898
2213
  }
1899
2214
  case "context.debug": {
1900
- const estimate = (s) => Math.ceil(s.length / 4);
1901
- const stringifyContent = (c) => {
1902
- if (typeof c === "string") return c;
1903
- try {
1904
- return JSON.stringify(c);
1905
- } catch {
1906
- return String(c);
1907
- }
1908
- };
1909
- const sysTokens = context.systemPrompt.reduce((acc, b) => acc + estimate(b.text ?? ""), 0);
1910
- const tools = toolRegistry.list();
1911
- const toolBreakdown = tools.map((t) => {
1912
- const schema = t.inputSchema ?? {};
1913
- const desc = t.description ?? "";
1914
- return {
1915
- name: t.name,
1916
- tokens: estimate(t.name) + estimate(desc) + estimate(stringifyContent(schema))
1917
- };
1918
- });
1919
- const toolTokens = toolBreakdown.reduce((a, b) => a + b.tokens, 0);
1920
- const messageBreakdown = context.messages.map((m, i) => {
1921
- let tk = 0;
1922
- if (typeof m.content === "string") {
1923
- tk = estimate(m.content);
1924
- } else if (Array.isArray(m.content)) {
1925
- for (const b of m.content) {
1926
- if (b.type === "text") tk += estimate(b.text ?? "");
1927
- else if (b.type === "tool_use") tk += estimate(stringifyContent(b.input));
1928
- else if (b.type === "tool_result") tk += estimate(stringifyContent(b.content));
1929
- else tk += estimate(stringifyContent(b));
1930
- }
1931
- }
1932
- return {
1933
- index: i,
1934
- role: m.role,
1935
- tokens: tk,
1936
- preview: typeof m.content === "string" ? m.content.slice(0, 60) : Array.isArray(m.content) ? m.content.map(
1937
- (b) => b.type === "text" ? (b.text ?? "").slice(0, 40) : b.type === "tool_use" ? `[tool_use: ${b.name}]` : b.type === "tool_result" ? `[tool_result]` : `[${b.type}]`
1938
- ).join(" ").slice(0, 60) : ""
1939
- };
2215
+ const breakdown = estimateContextBreakdown({
2216
+ systemPrompt: context.systemPrompt,
2217
+ tools: toolRegistry.list(),
2218
+ messages: context.messages
1940
2219
  });
1941
- const msgTokens = messageBreakdown.reduce((a, b) => a + b.tokens, 0);
1942
- const total = sysTokens + toolTokens + msgTokens;
1943
2220
  send(ws, {
1944
2221
  type: "context.debug",
1945
2222
  payload: {
1946
- total,
2223
+ ...breakdown,
1947
2224
  mode: context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID,
1948
- policy: context.meta["contextWindowPolicy"],
1949
- systemPrompt: sysTokens,
1950
- tools: { total: toolTokens, count: tools.length, breakdown: toolBreakdown },
1951
- messages: {
1952
- total: msgTokens,
1953
- count: context.messages.length,
1954
- breakdown: messageBreakdown
1955
- }
2225
+ policy: context.meta["contextWindowPolicy"]
1956
2226
  }
1957
2227
  });
1958
2228
  break;
@@ -1977,7 +2247,7 @@ async function startWebUI(opts = {}) {
1977
2247
  `Compacted: ${report.before} \u2192 ${report.after} tokens (saved ~${Math.max(0, report.before - report.after)})`
1978
2248
  );
1979
2249
  } catch (err) {
1980
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2250
+ sendResult(ws, false, errMessage(err));
1981
2251
  }
1982
2252
  break;
1983
2253
  }
@@ -2136,7 +2406,7 @@ async function startWebUI(opts = {}) {
2136
2406
  type: "key.operation_result",
2137
2407
  payload: {
2138
2408
  success: false,
2139
- message: `Switch failed: ${err instanceof Error ? err.message : String(err)}`
2409
+ message: `Switch failed: ${errMessage(err)}`
2140
2410
  }
2141
2411
  });
2142
2412
  break;
@@ -2191,7 +2461,7 @@ async function startWebUI(opts = {}) {
2191
2461
  } catch (err) {
2192
2462
  send(ws, {
2193
2463
  type: "sessions.list",
2194
- payload: { sessions: [], error: err instanceof Error ? err.message : String(err) }
2464
+ payload: { sessions: [], error: errMessage(err) }
2195
2465
  });
2196
2466
  }
2197
2467
  break;
@@ -2206,7 +2476,7 @@ async function startWebUI(opts = {}) {
2206
2476
  await sessionStore.delete(id);
2207
2477
  sendResult(ws, true, `Session ${id} deleted`);
2208
2478
  } catch (err) {
2209
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2479
+ sendResult(ws, false, errMessage(err));
2210
2480
  }
2211
2481
  break;
2212
2482
  }
@@ -2241,7 +2511,7 @@ async function startWebUI(opts = {}) {
2241
2511
  });
2242
2512
  sendResult(ws, true, `Resumed session ${id}`);
2243
2513
  } catch (err) {
2244
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2514
+ sendResult(ws, false, errMessage(err));
2245
2515
  }
2246
2516
  break;
2247
2517
  }
@@ -2269,7 +2539,7 @@ async function startWebUI(opts = {}) {
2269
2539
  } catch (err) {
2270
2540
  send(ws, {
2271
2541
  type: "memory.list",
2272
- payload: { text: "", error: err instanceof Error ? err.message : String(err) }
2542
+ payload: { text: "", error: errMessage(err) }
2273
2543
  });
2274
2544
  }
2275
2545
  break;
@@ -2280,7 +2550,7 @@ async function startWebUI(opts = {}) {
2280
2550
  await memoryStore.remember(text, scope ?? "project-memory");
2281
2551
  sendResult(ws, true, "Saved to memory");
2282
2552
  } catch (err) {
2283
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2553
+ sendResult(ws, false, errMessage(err));
2284
2554
  }
2285
2555
  break;
2286
2556
  }
@@ -2294,7 +2564,7 @@ async function startWebUI(opts = {}) {
2294
2564
  removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
2295
2565
  );
2296
2566
  } catch (err) {
2297
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2567
+ sendResult(ws, false, errMessage(err));
2298
2568
  }
2299
2569
  break;
2300
2570
  }
@@ -2328,7 +2598,7 @@ async function startWebUI(opts = {}) {
2328
2598
  payload: {
2329
2599
  skills: [],
2330
2600
  enabled: true,
2331
- error: err instanceof Error ? err.message : String(err)
2601
+ error: errMessage(err)
2332
2602
  }
2333
2603
  });
2334
2604
  }
@@ -2422,29 +2692,13 @@ async function startWebUI(opts = {}) {
2422
2692
  payload: { plan }
2423
2693
  });
2424
2694
  } catch (err) {
2425
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2695
+ sendResult(ws, false, errMessage(err));
2426
2696
  }
2427
2697
  break;
2428
2698
  }
2429
2699
  case "files.list": {
2430
2700
  const payload = msg.payload ?? {};
2431
- const query = (payload.query ?? "").toLowerCase();
2432
2701
  const limit = payload.limit ?? 50;
2433
- const SKIP_DIRS = /* @__PURE__ */ new Set([
2434
- ".git",
2435
- "node_modules",
2436
- "dist",
2437
- "build",
2438
- ".next",
2439
- ".turbo",
2440
- ".cache",
2441
- "target",
2442
- "coverage",
2443
- ".nyc_output",
2444
- "out",
2445
- ".pnpm-store",
2446
- ".parcel-cache"
2447
- ]);
2448
2702
  const results = [];
2449
2703
  async function walk(dir, rel, depth) {
2450
2704
  if (depth > 8 || results.length >= 600) return;
@@ -2456,40 +2710,20 @@ async function startWebUI(opts = {}) {
2456
2710
  }
2457
2711
  for (const e of entries) {
2458
2712
  if (results.length >= 600) return;
2459
- if (e.name.startsWith(".") && e.name !== ".wrongstack" && e.name !== ".env.example") {
2460
- if (e.name !== ".gitignore" && e.name !== ".eslintrc" && e.name !== ".prettierrc")
2461
- continue;
2462
- }
2713
+ if (isHiddenEntry(e.name)) continue;
2463
2714
  const childRel = rel ? `${rel}/${e.name}` : e.name;
2464
2715
  if (e.isDirectory()) {
2465
2716
  if (SKIP_DIRS.has(e.name)) continue;
2466
- await walk(path.join(dir, e.name), childRel, depth + 1);
2717
+ await walk(path2.join(dir, e.name), childRel, depth + 1);
2467
2718
  } else if (e.isFile()) {
2468
2719
  results.push(childRel);
2469
2720
  }
2470
2721
  }
2471
2722
  }
2472
2723
  await walk(projectRoot, "", 0);
2473
- const scored = [];
2474
- for (const p of results) {
2475
- if (!query) {
2476
- scored.push({ path: p, score: 0 });
2477
- continue;
2478
- }
2479
- const lower = p.toLowerCase();
2480
- const base = lower.split("/").pop() ?? lower;
2481
- let score = 0;
2482
- if (base === query) score = 100;
2483
- else if (base.startsWith(query)) score = 60;
2484
- else if (lower.includes(query)) score = 20;
2485
- else continue;
2486
- score -= p.split("/").length;
2487
- scored.push({ path: p, score });
2488
- }
2489
- scored.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
2490
2724
  send(ws, {
2491
2725
  type: "files.list",
2492
- payload: { files: scored.slice(0, limit).map((s) => s.path) }
2726
+ payload: { files: rankFiles(results, payload.query ?? "", limit) }
2493
2727
  });
2494
2728
  break;
2495
2729
  }
@@ -2515,7 +2749,7 @@ async function startWebUI(opts = {}) {
2515
2749
  payload: {
2516
2750
  modes: [],
2517
2751
  activeId: "default",
2518
- error: err instanceof Error ? err.message : String(err)
2752
+ error: errMessage(err)
2519
2753
  }
2520
2754
  });
2521
2755
  }
@@ -2554,7 +2788,7 @@ async function startWebUI(opts = {}) {
2554
2788
  payload: { ...await sessionStartPayload() }
2555
2789
  });
2556
2790
  } catch (err) {
2557
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2791
+ sendResult(ws, false, errMessage(err));
2558
2792
  }
2559
2793
  break;
2560
2794
  }
@@ -2562,10 +2796,7 @@ async function startWebUI(opts = {}) {
2562
2796
  const usage = tokenCounter.total();
2563
2797
  const cacheStats = tokenCounter.cacheStats();
2564
2798
  const m = await modelsRegistry.getModel(config.provider, config.model).catch(() => null);
2565
- const inputCost = m?.cost?.input ?? 0;
2566
- const outputCost = m?.cost?.output ?? 0;
2567
- const cacheReadCost = m?.cost?.cache_read ?? 0;
2568
- const cost = (usage.input * inputCost + usage.output * outputCost + (usage.cacheRead ?? 0) * cacheReadCost) / 1e6;
2799
+ const cost = computeUsageCost(usage, getCostRates(m));
2569
2800
  send(ws, {
2570
2801
  type: "stats.get",
2571
2802
  payload: {
@@ -2616,230 +2847,80 @@ async function startWebUI(opts = {}) {
2616
2847
  });
2617
2848
  await configWriteLock;
2618
2849
  }
2619
- function normalizeKeys(cfg) {
2620
- if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
2621
- return cfg.apiKeys.map((k) => ({ ...k }));
2622
- }
2623
- if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
2624
- return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
2625
- }
2626
- return [];
2627
- }
2628
- function writeKeysBack(cfg, keys) {
2629
- if (keys.length === 0) {
2630
- delete cfg.apiKeys;
2631
- delete cfg.apiKey;
2632
- delete cfg.activeKey;
2633
- return;
2634
- }
2635
- cfg.apiKeys = keys;
2636
- const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
2637
- cfg.apiKey = active.apiKey;
2638
- if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
2639
- cfg.activeKey = active.label;
2640
- }
2641
- }
2642
- function maskedKey(key) {
2643
- if (!key) return "\u2014";
2644
- if (key.length <= 8) return "\u2022".repeat(key.length);
2645
- return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
2646
- }
2647
2850
  function sendResult(ws, success, message) {
2648
2851
  send(ws, { type: "key.operation_result", payload: { success, message } });
2649
2852
  }
2650
2853
  async function handleKeyUpsert(ws, providerId, label, apiKey) {
2651
2854
  try {
2652
2855
  const providers = await loadSavedProviders();
2653
- const existing = providers[providerId] ?? { type: providerId };
2654
- const keys = normalizeKeys(existing);
2655
- const idx = keys.findIndex((k) => k.label === label);
2656
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2657
- if (idx >= 0) {
2658
- keys[idx] = { ...keys[idx], apiKey, createdAt: nowIso };
2659
- } else {
2660
- keys.push({ label, apiKey, createdAt: nowIso });
2661
- }
2662
- writeKeysBack(existing, keys);
2663
- if (!existing.activeKey) existing.activeKey = label;
2664
- providers[providerId] = existing;
2665
- await saveProviders(providers);
2666
- sendResult(ws, true, `Key "${label}" saved for ${providerId}`);
2856
+ const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
2857
+ if (result.ok) await saveProviders(providers);
2858
+ sendResult(ws, result.ok, result.message);
2667
2859
  } catch (err) {
2668
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2860
+ sendResult(ws, false, errMessage(err));
2669
2861
  }
2670
2862
  }
2671
2863
  async function handleKeyDelete(ws, providerId, label) {
2672
2864
  try {
2673
2865
  const providers = await loadSavedProviders();
2674
- const existing = providers[providerId];
2675
- if (!existing) {
2676
- sendResult(ws, false, `Provider "${providerId}" not found`);
2677
- return;
2678
- }
2679
- const keys = normalizeKeys(existing).filter((k) => k.label !== label);
2680
- if (keys.length === 0) {
2681
- delete providers[providerId];
2682
- } else {
2683
- writeKeysBack(existing, keys);
2684
- if (existing.activeKey === label) existing.activeKey = keys[0].label;
2685
- providers[providerId] = existing;
2686
- }
2687
- await saveProviders(providers);
2688
- sendResult(ws, true, `Key "${label}" deleted from ${providerId}`);
2866
+ const result = deleteKey(providers, providerId, label);
2867
+ if (result.ok) await saveProviders(providers);
2868
+ sendResult(ws, result.ok, result.message);
2689
2869
  } catch (err) {
2690
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2870
+ sendResult(ws, false, errMessage(err));
2691
2871
  }
2692
2872
  }
2693
2873
  async function handleKeySetActive(ws, providerId, label) {
2694
2874
  try {
2695
2875
  const providers = await loadSavedProviders();
2696
- const existing = providers[providerId];
2697
- if (!existing) {
2698
- sendResult(ws, false, `Provider "${providerId}" not found`);
2699
- return;
2700
- }
2701
- existing.activeKey = label;
2702
- writeKeysBack(existing, normalizeKeys(existing));
2703
- providers[providerId] = existing;
2704
- await saveProviders(providers);
2705
- sendResult(ws, true, `Active key for ${providerId} set to "${label}"`);
2876
+ const result = setActiveKey(providers, providerId, label);
2877
+ if (result.ok) await saveProviders(providers);
2878
+ sendResult(ws, result.ok, result.message);
2706
2879
  } catch (err) {
2707
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2880
+ sendResult(ws, false, errMessage(err));
2708
2881
  }
2709
2882
  }
2710
2883
  async function handleProviderAdd(ws, payload) {
2711
2884
  try {
2712
2885
  const providers = await loadSavedProviders();
2713
- if (providers[payload.id]) {
2714
- sendResult(ws, false, `Provider "${payload.id}" already exists. Use key.add to add a key.`);
2715
- return;
2716
- }
2717
- const newProv = {
2718
- type: payload.id,
2719
- family: payload.family,
2720
- baseUrl: payload.baseUrl
2721
- };
2722
- if (payload.apiKey) {
2723
- newProv.apiKeys = [
2724
- { label: "default", apiKey: payload.apiKey, createdAt: (/* @__PURE__ */ new Date()).toISOString() }
2725
- ];
2726
- newProv.activeKey = "default";
2727
- }
2728
- providers[payload.id] = newProv;
2729
- await saveProviders(providers);
2730
- sendResult(ws, true, `Provider "${payload.id}" added`);
2886
+ const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
2887
+ if (result.ok) await saveProviders(providers);
2888
+ sendResult(ws, result.ok, result.message);
2731
2889
  } catch (err) {
2732
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2890
+ sendResult(ws, false, errMessage(err));
2733
2891
  }
2734
2892
  }
2735
2893
  async function handleProviderRemove(ws, providerId) {
2736
2894
  try {
2737
2895
  const providers = await loadSavedProviders();
2738
- if (!providers[providerId]) {
2739
- sendResult(ws, false, `Provider "${providerId}" not found`);
2740
- return;
2741
- }
2742
- delete providers[providerId];
2743
- await saveProviders(providers);
2744
- sendResult(ws, true, `Provider "${providerId}" removed`);
2896
+ const result = removeProvider(providers, providerId);
2897
+ if (result.ok) await saveProviders(providers);
2898
+ sendResult(ws, result.ok, result.message);
2745
2899
  } catch (err) {
2746
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2900
+ sendResult(ws, false, errMessage(err));
2747
2901
  }
2748
2902
  }
2749
- const httpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
2750
- const DIST_DIR = path.resolve(import.meta.dirname, "../../dist");
2751
- const mimeTypes = {
2752
- ".html": "text/html",
2753
- ".js": "application/javascript",
2754
- ".css": "text/css",
2755
- ".json": "application/json",
2756
- ".svg": "image/svg+xml",
2757
- ".png": "image/png",
2758
- ".ico": "image/x-icon"
2759
- };
2760
- const httpServer = http.createServer(async (req, res) => {
2761
- try {
2762
- const url = new URL(req.url ?? "/", `http://127.0.0.1:${httpPort}`);
2763
- let filePath;
2764
- if (url.pathname === "/" || url.pathname === "") {
2765
- filePath = path.join(DIST_DIR, "index.html");
2766
- } else if (url.pathname.startsWith("/assets/")) {
2767
- filePath = path.join(DIST_DIR, url.pathname);
2768
- } else if (url.pathname.startsWith("/")) {
2769
- filePath = path.join(DIST_DIR, url.pathname);
2770
- } else {
2771
- filePath = path.join(DIST_DIR, "index.html");
2772
- }
2773
- const resolvedPath = path.resolve(filePath);
2774
- const resolvedRoot = path.resolve(DIST_DIR);
2775
- if (!resolvedPath.startsWith(resolvedRoot + path.sep) && resolvedPath !== resolvedRoot) {
2776
- res.writeHead(403, { "Content-Type": "text/plain" });
2777
- res.end("Forbidden");
2778
- return;
2779
- }
2780
- const ext = path.extname(resolvedPath);
2781
- const contentType = mimeTypes[ext] ?? "application/octet-stream";
2782
- res.setHeader("Content-Type", contentType);
2783
- res.setHeader("X-Content-Type-Options", "nosniff");
2784
- res.setHeader("X-Frame-Options", "DENY");
2785
- res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
2786
- if (ext === ".html") {
2787
- res.setHeader("Cache-Control", "no-cache");
2788
- res.setHeader(
2789
- "Content-Security-Policy",
2790
- `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'`
2791
- );
2792
- }
2793
- const fileContent = await fs2.readFile(resolvedPath);
2794
- res.writeHead(200);
2795
- res.end(fileContent);
2796
- } catch (err) {
2797
- if (err.code === "ENOENT") {
2798
- try {
2799
- const fileContent = await fs2.readFile(path.join(DIST_DIR, "index.html"));
2800
- res.writeHead(200, {
2801
- "Content-Type": "text/html",
2802
- "X-Content-Type-Options": "nosniff",
2803
- "X-Frame-Options": "DENY",
2804
- "Referrer-Policy": "strict-origin-when-cross-origin",
2805
- // SPA fallback previously shipped no CSP — apply the same policy as
2806
- // the direct .html branch so deep-linked routes aren't unprotected.
2807
- "Content-Security-Policy": `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'`
2808
- });
2809
- res.end(fileContent);
2810
- } catch {
2811
- res.writeHead(404);
2812
- res.end("Not found");
2813
- }
2814
- } else {
2815
- res.writeHead(500);
2816
- res.end("Server error");
2817
- }
2818
- }
2903
+ const httpServer = createHttpServer({
2904
+ host: wsHost,
2905
+ distDir: path2.resolve(import.meta.dirname, "../../dist"),
2906
+ wsPort
2819
2907
  });
2908
+ const httpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
2820
2909
  httpServer.listen(httpPort, wsHost, () => {
2821
2910
  console.log(`[WebUI] HTTP server running on http://${wsHost}:${httpPort}`);
2822
2911
  });
2823
- const shutdown = async () => {
2824
- console.log("[WebUI] Shutting down...");
2825
- try {
2912
+ registerShutdownHandlers({
2913
+ flushSession: async () => {
2826
2914
  await session.append({
2827
2915
  type: "session_end",
2828
2916
  ts: (/* @__PURE__ */ new Date()).toISOString(),
2829
2917
  usage: tokenCounter.total()
2830
2918
  });
2831
2919
  await session.close();
2832
- } catch (e) {
2833
- console.warn("[WebUI] Error closing session:", e);
2834
- }
2835
- for (const [ws] of clients) ws.close();
2836
- httpServer.close();
2837
- wssPrimary.close();
2838
- wssSecondary?.close();
2839
- process.exit(0);
2840
- };
2841
- process.on("SIGINT", shutdown);
2842
- process.on("SIGTERM", shutdown);
2920
+ },
2921
+ clients: () => clients.keys(),
2922
+ servers: [httpServer, wssPrimary, wssSecondary]
2923
+ });
2843
2924
  }
2844
2925
  export {
2845
2926
  startWebUI