@wrongstack/webui 0.41.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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // src/server/index.ts
3
- import * as fs3 from "fs/promises";
3
+ import * as fs2 from "fs/promises";
4
4
  import * as path2 from "path";
5
5
 
6
6
  // src/server/http-server.ts
@@ -84,6 +84,54 @@ function createHttpServer(opts) {
84
84
  });
85
85
  }
86
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
+
87
135
  // src/server/index.ts
88
136
  import {
89
137
  Agent,
@@ -120,7 +168,7 @@ import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/sec
120
168
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
121
169
  import { builtinToolsPack, forgetTool, rememberTool } from "@wrongstack/tools";
122
170
  import { WebSocket, WebSocketServer } from "ws";
123
- import { randomBytes, timingSafeEqual } from "crypto";
171
+ import { randomBytes } from "crypto";
124
172
 
125
173
  // ../runtime/src/container.ts
126
174
  import {
@@ -194,44 +242,14 @@ function createDefaultContainer(opts) {
194
242
  }
195
243
 
196
244
  // src/server/boot.ts
197
- import * as fs2 from "fs/promises";
198
- import * as os from "os";
199
245
  import {
200
- DefaultConfigLoader,
201
- DefaultLogger,
202
- DefaultPathResolver,
203
- DefaultSecretVault,
204
- migratePlaintextSecrets,
205
- resolveWstackPaths,
206
- writeErr
246
+ bootConfig as coreBootConfig
207
247
  } from "@wrongstack/core";
208
248
  async function bootConfig() {
209
- const cwd = process.cwd();
210
- const pathResolver = new DefaultPathResolver(cwd);
211
- const projectRoot = pathResolver.projectRoot;
212
- const userHome = os.homedir();
213
- const wpaths = resolveWstackPaths({ projectRoot, userHome });
214
- await fs2.mkdir(wpaths.globalRoot, { recursive: true });
215
- await fs2.mkdir(wpaths.projectDir, { recursive: true });
216
- await fs2.mkdir(wpaths.projectSessions, { recursive: true });
217
- const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey });
218
- for (const file of [wpaths.globalConfig, wpaths.projectLocalConfig]) {
219
- try {
220
- const { migrated } = await migratePlaintextSecrets(file, vault);
221
- if (migrated > 0) {
222
- writeErr(`[WebUI] Encrypted ${migrated} plaintext secret(s) in ${file}
223
- `);
224
- }
225
- } catch {
226
- }
227
- }
228
- const configLoader = new DefaultConfigLoader({ paths: wpaths, vault });
229
- const config = await configLoader.load({ cliFlags: {} });
230
- const logger = new DefaultLogger({
231
- level: config.log?.level ?? "info",
232
- file: wpaths.logFile
249
+ const { config, vault, globalConfigPath, projectRoot, wpaths, logger } = await coreBootConfig({
250
+ appLabel: "WebUI"
233
251
  });
234
- return { config, vault, globalConfigPath: wpaths.globalConfig, projectRoot, wpaths, logger };
252
+ return { config, vault, globalConfigPath, projectRoot, wpaths, logger };
235
253
  }
236
254
  function patchConfig(config, updates) {
237
255
  return Object.freeze({ ...config, ...updates });
@@ -1362,7 +1380,255 @@ var WorktreeWebSocketHandler = class {
1362
1380
  }
1363
1381
  };
1364
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
+
1365
1628
  // src/server/index.ts
1629
+ function errMessage(err) {
1630
+ return err instanceof Error ? err.message : String(err);
1631
+ }
1366
1632
  async function startWebUI(opts = {}) {
1367
1633
  const wsPort2 = opts.wsPort ?? 3457;
1368
1634
  const wsHost2 = opts.wsHost ?? "127.0.0.1";
@@ -1597,10 +1863,10 @@ async function startWebUI(opts = {}) {
1597
1863
  try {
1598
1864
  const m = await modelsRegistry.getModel(config.provider, config.model);
1599
1865
  maxContext = m?.capabilities?.maxContext ?? 0;
1600
- const cost = m?.cost;
1601
- inputCost = cost?.input ?? 0;
1602
- outputCost = cost?.output ?? 0;
1603
- cacheReadCost = cost?.cache_read ?? 0;
1866
+ const rates = getCostRates(m);
1867
+ inputCost = rates.input;
1868
+ outputCost = rates.output;
1869
+ cacheReadCost = rates.cacheRead;
1604
1870
  } catch {
1605
1871
  }
1606
1872
  return {
@@ -1620,59 +1886,25 @@ async function startWebUI(opts = {}) {
1620
1886
  }
1621
1887
  const wsToken = randomBytes(16).toString("hex");
1622
1888
  console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
1623
- const isLoopback = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1624
- const tokenMatches = (provided) => {
1625
- if (!provided) return false;
1626
- const a = Buffer.from(provided);
1627
- const b = Buffer.from(wsToken);
1628
- if (a.length !== b.length) return false;
1629
- return timingSafeEqual(a, b);
1630
- };
1631
- const hostHeaderOk = (req) => {
1632
- const boundToLoopback = wsHost2 === "127.0.0.1" || wsHost2 === "::1" || wsHost2 === "localhost";
1633
- if (!boundToLoopback) return true;
1634
- const hostHeader = (req.headers.host ?? "").trim();
1635
- if (!hostHeader) return false;
1636
- let hostname;
1637
- try {
1638
- hostname = new URL(`http://${hostHeader}`).hostname;
1639
- } catch {
1640
- return false;
1641
- }
1642
- return isLoopback(hostname);
1643
- };
1644
- const verifyClient = (info) => {
1645
- const origin = info.origin;
1646
- const url = info.req.url ?? "";
1647
- const tokenMatch = url.match(/[?&]token=([^&]+)/);
1648
- const providedToken = tokenMatch ? tokenMatch[1] : void 0;
1649
- const tokenOk = tokenMatches(providedToken);
1650
- if (!hostHeaderOk(info.req)) return false;
1651
- if (!origin) {
1652
- const remoteIp = info.req.socket.remoteAddress ?? "";
1653
- const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
1654
- if (!isRemoteLoopback && wsHost2 === "0.0.0.0") return false;
1655
- return tokenOk || wsHost2 === "127.0.0.1" || wsHost2 === "::1" || wsHost2 === "localhost";
1656
- }
1657
- try {
1658
- const { hostname } = new URL(origin);
1659
- if (isLoopback(hostname)) return true;
1660
- return tokenOk;
1661
- } catch {
1662
- return false;
1663
- }
1664
- };
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
+ });
1665
1897
  const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
1666
1898
  const wssPrimary = new WebSocketServer({
1667
1899
  port: wsPort2,
1668
1900
  host: wsHost2,
1669
- verifyClient,
1901
+ verifyClient: verifyClient2,
1670
1902
  maxPayload: WS_MAX_PAYLOAD
1671
1903
  });
1672
1904
  const wssSecondary = wsHost2 === "127.0.0.1" ? new WebSocketServer({
1673
1905
  port: wsPort2,
1674
1906
  host: "::1",
1675
- verifyClient,
1907
+ verifyClient: verifyClient2,
1676
1908
  maxPayload: WS_MAX_PAYLOAD
1677
1909
  }) : null;
1678
1910
  const clients = /* @__PURE__ */ new Map();
@@ -1924,7 +2156,7 @@ async function startWebUI(opts = {}) {
1924
2156
  type: "error",
1925
2157
  payload: {
1926
2158
  phase: "agent.run",
1927
- message: err instanceof Error ? err.message : String(err)
2159
+ message: errMessage(err)
1928
2160
  }
1929
2161
  });
1930
2162
  } finally {
@@ -1981,62 +2213,17 @@ async function startWebUI(opts = {}) {
1981
2213
  break;
1982
2214
  }
1983
2215
  case "context.debug": {
1984
- const estimate = (s) => Math.ceil(s.length / 4);
1985
- const stringifyContent = (c) => {
1986
- if (typeof c === "string") return c;
1987
- try {
1988
- return JSON.stringify(c);
1989
- } catch {
1990
- return String(c);
1991
- }
1992
- };
1993
- const sysTokens = context.systemPrompt.reduce((acc, b) => acc + estimate(b.text ?? ""), 0);
1994
- const tools = toolRegistry.list();
1995
- const toolBreakdown = tools.map((t) => {
1996
- const schema = t.inputSchema ?? {};
1997
- const desc = t.description ?? "";
1998
- return {
1999
- name: t.name,
2000
- tokens: estimate(t.name) + estimate(desc) + estimate(stringifyContent(schema))
2001
- };
2216
+ const breakdown = estimateContextBreakdown({
2217
+ systemPrompt: context.systemPrompt,
2218
+ tools: toolRegistry.list(),
2219
+ messages: context.messages
2002
2220
  });
2003
- const toolTokens = toolBreakdown.reduce((a, b) => a + b.tokens, 0);
2004
- const messageBreakdown = context.messages.map((m, i) => {
2005
- let tk = 0;
2006
- if (typeof m.content === "string") {
2007
- tk = estimate(m.content);
2008
- } else if (Array.isArray(m.content)) {
2009
- for (const b of m.content) {
2010
- if (b.type === "text") tk += estimate(b.text ?? "");
2011
- else if (b.type === "tool_use") tk += estimate(stringifyContent(b.input));
2012
- else if (b.type === "tool_result") tk += estimate(stringifyContent(b.content));
2013
- else tk += estimate(stringifyContent(b));
2014
- }
2015
- }
2016
- return {
2017
- index: i,
2018
- role: m.role,
2019
- tokens: tk,
2020
- preview: typeof m.content === "string" ? m.content.slice(0, 60) : Array.isArray(m.content) ? m.content.map(
2021
- (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}]`
2022
- ).join(" ").slice(0, 60) : ""
2023
- };
2024
- });
2025
- const msgTokens = messageBreakdown.reduce((a, b) => a + b.tokens, 0);
2026
- const total = sysTokens + toolTokens + msgTokens;
2027
2221
  send(ws, {
2028
2222
  type: "context.debug",
2029
2223
  payload: {
2030
- total,
2224
+ ...breakdown,
2031
2225
  mode: context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID,
2032
- policy: context.meta["contextWindowPolicy"],
2033
- systemPrompt: sysTokens,
2034
- tools: { total: toolTokens, count: tools.length, breakdown: toolBreakdown },
2035
- messages: {
2036
- total: msgTokens,
2037
- count: context.messages.length,
2038
- breakdown: messageBreakdown
2039
- }
2226
+ policy: context.meta["contextWindowPolicy"]
2040
2227
  }
2041
2228
  });
2042
2229
  break;
@@ -2061,7 +2248,7 @@ async function startWebUI(opts = {}) {
2061
2248
  `Compacted: ${report.before} \u2192 ${report.after} tokens (saved ~${Math.max(0, report.before - report.after)})`
2062
2249
  );
2063
2250
  } catch (err) {
2064
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2251
+ sendResult(ws, false, errMessage(err));
2065
2252
  }
2066
2253
  break;
2067
2254
  }
@@ -2201,7 +2388,7 @@ async function startWebUI(opts = {}) {
2201
2388
  updateAutoCompactionMaxContext?.(newProv);
2202
2389
  try {
2203
2390
  configWriteLock = configWriteLock.then(async () => {
2204
- const raw = await fs3.readFile(globalConfigPath, "utf8");
2391
+ const raw = await fs2.readFile(globalConfigPath, "utf8");
2205
2392
  const parsed = JSON.parse(raw);
2206
2393
  parsed.provider = newProvider;
2207
2394
  parsed.model = newModel;
@@ -2220,7 +2407,7 @@ async function startWebUI(opts = {}) {
2220
2407
  type: "key.operation_result",
2221
2408
  payload: {
2222
2409
  success: false,
2223
- message: `Switch failed: ${err instanceof Error ? err.message : String(err)}`
2410
+ message: `Switch failed: ${errMessage(err)}`
2224
2411
  }
2225
2412
  });
2226
2413
  break;
@@ -2275,7 +2462,7 @@ async function startWebUI(opts = {}) {
2275
2462
  } catch (err) {
2276
2463
  send(ws, {
2277
2464
  type: "sessions.list",
2278
- payload: { sessions: [], error: err instanceof Error ? err.message : String(err) }
2465
+ payload: { sessions: [], error: errMessage(err) }
2279
2466
  });
2280
2467
  }
2281
2468
  break;
@@ -2290,7 +2477,7 @@ async function startWebUI(opts = {}) {
2290
2477
  await sessionStore.delete(id);
2291
2478
  sendResult(ws, true, `Session ${id} deleted`);
2292
2479
  } catch (err) {
2293
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2480
+ sendResult(ws, false, errMessage(err));
2294
2481
  }
2295
2482
  break;
2296
2483
  }
@@ -2325,7 +2512,7 @@ async function startWebUI(opts = {}) {
2325
2512
  });
2326
2513
  sendResult(ws, true, `Resumed session ${id}`);
2327
2514
  } catch (err) {
2328
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2515
+ sendResult(ws, false, errMessage(err));
2329
2516
  }
2330
2517
  break;
2331
2518
  }
@@ -2353,7 +2540,7 @@ async function startWebUI(opts = {}) {
2353
2540
  } catch (err) {
2354
2541
  send(ws, {
2355
2542
  type: "memory.list",
2356
- payload: { text: "", error: err instanceof Error ? err.message : String(err) }
2543
+ payload: { text: "", error: errMessage(err) }
2357
2544
  });
2358
2545
  }
2359
2546
  break;
@@ -2364,7 +2551,7 @@ async function startWebUI(opts = {}) {
2364
2551
  await memoryStore.remember(text, scope ?? "project-memory");
2365
2552
  sendResult(ws, true, "Saved to memory");
2366
2553
  } catch (err) {
2367
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2554
+ sendResult(ws, false, errMessage(err));
2368
2555
  }
2369
2556
  break;
2370
2557
  }
@@ -2378,7 +2565,7 @@ async function startWebUI(opts = {}) {
2378
2565
  removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
2379
2566
  );
2380
2567
  } catch (err) {
2381
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2568
+ sendResult(ws, false, errMessage(err));
2382
2569
  }
2383
2570
  break;
2384
2571
  }
@@ -2412,7 +2599,7 @@ async function startWebUI(opts = {}) {
2412
2599
  payload: {
2413
2600
  skills: [],
2414
2601
  enabled: true,
2415
- error: err instanceof Error ? err.message : String(err)
2602
+ error: errMessage(err)
2416
2603
  }
2417
2604
  });
2418
2605
  }
@@ -2506,44 +2693,25 @@ async function startWebUI(opts = {}) {
2506
2693
  payload: { plan }
2507
2694
  });
2508
2695
  } catch (err) {
2509
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2696
+ sendResult(ws, false, errMessage(err));
2510
2697
  }
2511
2698
  break;
2512
2699
  }
2513
2700
  case "files.list": {
2514
2701
  const payload = msg.payload ?? {};
2515
- const query = (payload.query ?? "").toLowerCase();
2516
2702
  const limit = payload.limit ?? 50;
2517
- const SKIP_DIRS = /* @__PURE__ */ new Set([
2518
- ".git",
2519
- "node_modules",
2520
- "dist",
2521
- "build",
2522
- ".next",
2523
- ".turbo",
2524
- ".cache",
2525
- "target",
2526
- "coverage",
2527
- ".nyc_output",
2528
- "out",
2529
- ".pnpm-store",
2530
- ".parcel-cache"
2531
- ]);
2532
2703
  const results = [];
2533
2704
  async function walk(dir, rel, depth) {
2534
2705
  if (depth > 8 || results.length >= 600) return;
2535
2706
  let entries = [];
2536
2707
  try {
2537
- entries = await fs3.readdir(dir, { withFileTypes: true });
2708
+ entries = await fs2.readdir(dir, { withFileTypes: true });
2538
2709
  } catch {
2539
2710
  return;
2540
2711
  }
2541
2712
  for (const e of entries) {
2542
2713
  if (results.length >= 600) return;
2543
- if (e.name.startsWith(".") && e.name !== ".wrongstack" && e.name !== ".env.example") {
2544
- if (e.name !== ".gitignore" && e.name !== ".eslintrc" && e.name !== ".prettierrc")
2545
- continue;
2546
- }
2714
+ if (isHiddenEntry(e.name)) continue;
2547
2715
  const childRel = rel ? `${rel}/${e.name}` : e.name;
2548
2716
  if (e.isDirectory()) {
2549
2717
  if (SKIP_DIRS.has(e.name)) continue;
@@ -2554,26 +2722,9 @@ async function startWebUI(opts = {}) {
2554
2722
  }
2555
2723
  }
2556
2724
  await walk(projectRoot, "", 0);
2557
- const scored = [];
2558
- for (const p of results) {
2559
- if (!query) {
2560
- scored.push({ path: p, score: 0 });
2561
- continue;
2562
- }
2563
- const lower = p.toLowerCase();
2564
- const base = lower.split("/").pop() ?? lower;
2565
- let score = 0;
2566
- if (base === query) score = 100;
2567
- else if (base.startsWith(query)) score = 60;
2568
- else if (lower.includes(query)) score = 20;
2569
- else continue;
2570
- score -= p.split("/").length;
2571
- scored.push({ path: p, score });
2572
- }
2573
- scored.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
2574
2725
  send(ws, {
2575
2726
  type: "files.list",
2576
- payload: { files: scored.slice(0, limit).map((s) => s.path) }
2727
+ payload: { files: rankFiles(results, payload.query ?? "", limit) }
2577
2728
  });
2578
2729
  break;
2579
2730
  }
@@ -2599,7 +2750,7 @@ async function startWebUI(opts = {}) {
2599
2750
  payload: {
2600
2751
  modes: [],
2601
2752
  activeId: "default",
2602
- error: err instanceof Error ? err.message : String(err)
2753
+ error: errMessage(err)
2603
2754
  }
2604
2755
  });
2605
2756
  }
@@ -2638,7 +2789,7 @@ async function startWebUI(opts = {}) {
2638
2789
  payload: { ...await sessionStartPayload() }
2639
2790
  });
2640
2791
  } catch (err) {
2641
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2792
+ sendResult(ws, false, errMessage(err));
2642
2793
  }
2643
2794
  break;
2644
2795
  }
@@ -2646,10 +2797,7 @@ async function startWebUI(opts = {}) {
2646
2797
  const usage = tokenCounter.total();
2647
2798
  const cacheStats = tokenCounter.cacheStats();
2648
2799
  const m = await modelsRegistry.getModel(config.provider, config.model).catch(() => null);
2649
- const inputCost = m?.cost?.input ?? 0;
2650
- const outputCost = m?.cost?.output ?? 0;
2651
- const cacheReadCost = m?.cost?.cache_read ?? 0;
2652
- const cost = (usage.input * inputCost + usage.output * outputCost + (usage.cacheRead ?? 0) * cacheReadCost) / 1e6;
2800
+ const cost = computeUsageCost(usage, getCostRates(m));
2653
2801
  send(ws, {
2654
2802
  type: "stats.get",
2655
2803
  payload: {
@@ -2677,7 +2825,7 @@ async function startWebUI(opts = {}) {
2677
2825
  }
2678
2826
  async function loadSavedProviders() {
2679
2827
  try {
2680
- const raw = await fs3.readFile(globalConfigPath, "utf8");
2828
+ const raw = await fs2.readFile(globalConfigPath, "utf8");
2681
2829
  const parsed = JSON.parse(raw);
2682
2830
  if (!parsed.providers) return {};
2683
2831
  return decryptConfigSecrets(parsed.providers, vault);
@@ -2689,7 +2837,7 @@ async function startWebUI(opts = {}) {
2689
2837
  configWriteLock = configWriteLock.then(async () => {
2690
2838
  let parsed;
2691
2839
  try {
2692
- const raw = await fs3.readFile(globalConfigPath, "utf8");
2840
+ const raw = await fs2.readFile(globalConfigPath, "utf8");
2693
2841
  parsed = JSON.parse(raw);
2694
2842
  } catch {
2695
2843
  parsed = {};
@@ -2700,134 +2848,57 @@ async function startWebUI(opts = {}) {
2700
2848
  });
2701
2849
  await configWriteLock;
2702
2850
  }
2703
- function normalizeKeys(cfg) {
2704
- if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
2705
- return cfg.apiKeys.map((k) => ({ ...k }));
2706
- }
2707
- if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
2708
- return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
2709
- }
2710
- return [];
2711
- }
2712
- function writeKeysBack(cfg, keys) {
2713
- if (keys.length === 0) {
2714
- delete cfg.apiKeys;
2715
- delete cfg.apiKey;
2716
- delete cfg.activeKey;
2717
- return;
2718
- }
2719
- cfg.apiKeys = keys;
2720
- const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
2721
- cfg.apiKey = active.apiKey;
2722
- if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
2723
- cfg.activeKey = active.label;
2724
- }
2725
- }
2726
- function maskedKey(key) {
2727
- if (!key) return "\u2014";
2728
- if (key.length <= 8) return "\u2022".repeat(key.length);
2729
- return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
2730
- }
2731
2851
  function sendResult(ws, success, message) {
2732
2852
  send(ws, { type: "key.operation_result", payload: { success, message } });
2733
2853
  }
2734
2854
  async function handleKeyUpsert(ws, providerId, label, apiKey) {
2735
2855
  try {
2736
2856
  const providers = await loadSavedProviders();
2737
- const existing = providers[providerId] ?? { type: providerId };
2738
- const keys = normalizeKeys(existing);
2739
- const idx = keys.findIndex((k) => k.label === label);
2740
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2741
- if (idx >= 0) {
2742
- keys[idx] = { ...keys[idx], apiKey, createdAt: nowIso };
2743
- } else {
2744
- keys.push({ label, apiKey, createdAt: nowIso });
2745
- }
2746
- writeKeysBack(existing, keys);
2747
- if (!existing.activeKey) existing.activeKey = label;
2748
- providers[providerId] = existing;
2749
- await saveProviders(providers);
2750
- 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);
2751
2860
  } catch (err) {
2752
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2861
+ sendResult(ws, false, errMessage(err));
2753
2862
  }
2754
2863
  }
2755
2864
  async function handleKeyDelete(ws, providerId, label) {
2756
2865
  try {
2757
2866
  const providers = await loadSavedProviders();
2758
- const existing = providers[providerId];
2759
- if (!existing) {
2760
- sendResult(ws, false, `Provider "${providerId}" not found`);
2761
- return;
2762
- }
2763
- const keys = normalizeKeys(existing).filter((k) => k.label !== label);
2764
- if (keys.length === 0) {
2765
- delete providers[providerId];
2766
- } else {
2767
- writeKeysBack(existing, keys);
2768
- if (existing.activeKey === label) existing.activeKey = keys[0].label;
2769
- providers[providerId] = existing;
2770
- }
2771
- await saveProviders(providers);
2772
- 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);
2773
2870
  } catch (err) {
2774
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2871
+ sendResult(ws, false, errMessage(err));
2775
2872
  }
2776
2873
  }
2777
2874
  async function handleKeySetActive(ws, providerId, label) {
2778
2875
  try {
2779
2876
  const providers = await loadSavedProviders();
2780
- const existing = providers[providerId];
2781
- if (!existing) {
2782
- sendResult(ws, false, `Provider "${providerId}" not found`);
2783
- return;
2784
- }
2785
- existing.activeKey = label;
2786
- writeKeysBack(existing, normalizeKeys(existing));
2787
- providers[providerId] = existing;
2788
- await saveProviders(providers);
2789
- 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);
2790
2880
  } catch (err) {
2791
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2881
+ sendResult(ws, false, errMessage(err));
2792
2882
  }
2793
2883
  }
2794
2884
  async function handleProviderAdd(ws, payload) {
2795
2885
  try {
2796
2886
  const providers = await loadSavedProviders();
2797
- if (providers[payload.id]) {
2798
- sendResult(ws, false, `Provider "${payload.id}" already exists. Use key.add to add a key.`);
2799
- return;
2800
- }
2801
- const newProv = {
2802
- type: payload.id,
2803
- family: payload.family,
2804
- baseUrl: payload.baseUrl
2805
- };
2806
- if (payload.apiKey) {
2807
- newProv.apiKeys = [
2808
- { label: "default", apiKey: payload.apiKey, createdAt: (/* @__PURE__ */ new Date()).toISOString() }
2809
- ];
2810
- newProv.activeKey = "default";
2811
- }
2812
- providers[payload.id] = newProv;
2813
- await saveProviders(providers);
2814
- 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);
2815
2890
  } catch (err) {
2816
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2891
+ sendResult(ws, false, errMessage(err));
2817
2892
  }
2818
2893
  }
2819
2894
  async function handleProviderRemove(ws, providerId) {
2820
2895
  try {
2821
2896
  const providers = await loadSavedProviders();
2822
- if (!providers[providerId]) {
2823
- sendResult(ws, false, `Provider "${providerId}" not found`);
2824
- return;
2825
- }
2826
- delete providers[providerId];
2827
- await saveProviders(providers);
2828
- 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);
2829
2900
  } catch (err) {
2830
- sendResult(ws, false, err instanceof Error ? err.message : String(err));
2901
+ sendResult(ws, false, errMessage(err));
2831
2902
  }
2832
2903
  }
2833
2904
  const httpServer = createHttpServer({
@@ -2839,26 +2910,18 @@ async function startWebUI(opts = {}) {
2839
2910
  httpServer.listen(httpPort, wsHost2, () => {
2840
2911
  console.log(`[WebUI] HTTP server running on http://${wsHost2}:${httpPort}`);
2841
2912
  });
2842
- const shutdown = async () => {
2843
- console.log("[WebUI] Shutting down...");
2844
- try {
2913
+ registerShutdownHandlers({
2914
+ flushSession: async () => {
2845
2915
  await session.append({
2846
2916
  type: "session_end",
2847
2917
  ts: (/* @__PURE__ */ new Date()).toISOString(),
2848
2918
  usage: tokenCounter.total()
2849
2919
  });
2850
2920
  await session.close();
2851
- } catch (e) {
2852
- console.warn("[WebUI] Error closing session:", e);
2853
- }
2854
- for (const [ws] of clients) ws.close();
2855
- httpServer.close();
2856
- wssPrimary.close();
2857
- wssSecondary?.close();
2858
- process.exit(0);
2859
- };
2860
- process.on("SIGINT", shutdown);
2861
- process.on("SIGTERM", shutdown);
2921
+ },
2922
+ clients: () => clients.keys(),
2923
+ servers: [httpServer, wssPrimary, wssSecondary]
2924
+ });
2862
2925
  }
2863
2926
 
2864
2927
  // src/server/entry.ts