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