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