@wrongstack/webui 0.54.1 → 0.66.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-CnURnGh-.css +1 -0
- package/dist/assets/index-aAReViZF.js +94 -0
- package/dist/assets/vendor-DW1jimNH.css +1 -0
- package/dist/index.css +333 -200
- package/dist/index.css.map +1 -1
- package/dist/index.html +4 -3
- package/dist/index.js +984 -641
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +444 -126
- package/dist/server/entry.js.map +1 -1
- package/dist/server/index.d.ts +298 -2
- package/dist/server/index.js +445 -96
- 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/assets/{vendor-oYD55Pw4.js → vendor-wUxgMlp-.js} +0 -0
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 {
|
|
@@ -227,7 +239,8 @@ function createDefaultContainer(opts) {
|
|
|
227
239
|
() => new DefaultPermissionPolicy({
|
|
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;
|
|
@@ -1568,6 +1720,89 @@ function removeProvider(providers, providerId) {
|
|
|
1568
1720
|
return { ok: true, message: `Provider "${providerId}" removed` };
|
|
1569
1721
|
}
|
|
1570
1722
|
|
|
1723
|
+
// src/server/provider-config-io.ts
|
|
1724
|
+
import * as fs3 from "fs/promises";
|
|
1725
|
+
import * as path3 from "path";
|
|
1726
|
+
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
1727
|
+
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
1728
|
+
import { DefaultSecretVault } from "@wrongstack/core";
|
|
1729
|
+
async function loadSavedProviders(configPath, vault) {
|
|
1730
|
+
let raw;
|
|
1731
|
+
try {
|
|
1732
|
+
raw = await fs3.readFile(configPath, "utf8");
|
|
1733
|
+
} catch {
|
|
1734
|
+
return {};
|
|
1735
|
+
}
|
|
1736
|
+
let parsed = {};
|
|
1737
|
+
try {
|
|
1738
|
+
parsed = JSON.parse(raw);
|
|
1739
|
+
} catch {
|
|
1740
|
+
return {};
|
|
1741
|
+
}
|
|
1742
|
+
if (!parsed.providers) return {};
|
|
1743
|
+
return decryptConfigSecrets(parsed.providers, vault);
|
|
1744
|
+
}
|
|
1745
|
+
async function saveProviders(configPath, vault, providers) {
|
|
1746
|
+
let raw;
|
|
1747
|
+
let fileExists = true;
|
|
1748
|
+
try {
|
|
1749
|
+
raw = await fs3.readFile(configPath, "utf8");
|
|
1750
|
+
} catch (err) {
|
|
1751
|
+
if (err.code !== "ENOENT") {
|
|
1752
|
+
throw new Error(
|
|
1753
|
+
`Refusing to mutate ${configPath}: ${err.message}`,
|
|
1754
|
+
{ cause: err }
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
fileExists = false;
|
|
1758
|
+
raw = "{}";
|
|
1759
|
+
}
|
|
1760
|
+
let parsed;
|
|
1761
|
+
try {
|
|
1762
|
+
parsed = JSON.parse(raw);
|
|
1763
|
+
} catch (err) {
|
|
1764
|
+
if (fileExists) {
|
|
1765
|
+
throw new Error(
|
|
1766
|
+
`Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
|
|
1767
|
+
{ cause: err }
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
parsed = {};
|
|
1771
|
+
}
|
|
1772
|
+
parsed.providers = providers;
|
|
1773
|
+
const encrypted = encryptConfigSecrets(parsed, vault);
|
|
1774
|
+
await atomicWrite2(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/server/ws-utils.ts
|
|
1778
|
+
import { randomBytes } from "crypto";
|
|
1779
|
+
import { WebSocket } from "ws";
|
|
1780
|
+
function send(ws, msg) {
|
|
1781
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1782
|
+
ws.send(JSON.stringify(msg));
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function broadcast(clients, msg) {
|
|
1786
|
+
const data = JSON.stringify(msg);
|
|
1787
|
+
for (const [ws] of clients) {
|
|
1788
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1789
|
+
try {
|
|
1790
|
+
ws.send(data);
|
|
1791
|
+
} catch {
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function sendResult(ws, success, message) {
|
|
1797
|
+
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
1798
|
+
}
|
|
1799
|
+
function errMessage(err) {
|
|
1800
|
+
return err instanceof Error ? err.message : String(err);
|
|
1801
|
+
}
|
|
1802
|
+
function generateAuthToken() {
|
|
1803
|
+
return randomBytes(16).toString("hex");
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1571
1806
|
// src/server/token-estimator.ts
|
|
1572
1807
|
function estimateTokens(s) {
|
|
1573
1808
|
return Math.ceil(s.length / 4);
|
|
@@ -1626,12 +1861,23 @@ function estimateContextBreakdown(input) {
|
|
|
1626
1861
|
}
|
|
1627
1862
|
|
|
1628
1863
|
// src/server/index.ts
|
|
1629
|
-
function errMessage(err) {
|
|
1630
|
-
return err instanceof Error ? err.message : String(err);
|
|
1631
|
-
}
|
|
1632
1864
|
async function startWebUI(opts = {}) {
|
|
1633
|
-
const
|
|
1634
|
-
const
|
|
1865
|
+
const requestedWsPort = opts.wsPort ?? 3457;
|
|
1866
|
+
const wsHost = opts.wsHost ?? "127.0.0.1";
|
|
1867
|
+
const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
1868
|
+
const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
|
|
1869
|
+
let wsPort = requestedWsPort;
|
|
1870
|
+
let httpPort = requestedHttpPort;
|
|
1871
|
+
if (!strictPort) {
|
|
1872
|
+
httpPort = await findFreePort(wsHost, requestedHttpPort);
|
|
1873
|
+
wsPort = await findFreePort(wsHost, requestedWsPort, { exclude: /* @__PURE__ */ new Set([httpPort]) });
|
|
1874
|
+
if (httpPort !== requestedHttpPort) {
|
|
1875
|
+
console.warn(`[WebUI] HTTP port ${requestedHttpPort} in use \u2192 using ${httpPort}`);
|
|
1876
|
+
}
|
|
1877
|
+
if (wsPort !== requestedWsPort) {
|
|
1878
|
+
console.warn(`[WebUI] WS port ${requestedWsPort} in use \u2192 using ${wsPort}`);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1635
1881
|
console.log("[WebUI] Starting backend services...");
|
|
1636
1882
|
const boot = await bootConfig();
|
|
1637
1883
|
const { config: baseConfig, vault, globalConfigPath, projectRoot, wpaths, logger } = boot;
|
|
@@ -1781,7 +2027,15 @@ async function startWebUI(opts = {}) {
|
|
|
1781
2027
|
});
|
|
1782
2028
|
let autoCompactor;
|
|
1783
2029
|
if (config.context?.autoCompact !== false) {
|
|
1784
|
-
|
|
2030
|
+
let effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
|
|
2031
|
+
if (!effectiveMaxContext) {
|
|
2032
|
+
try {
|
|
2033
|
+
const m = await modelsRegistry.getModel(provider.id, context.model);
|
|
2034
|
+
effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
|
|
2035
|
+
} catch {
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
|
|
1785
2039
|
autoCompactor = new AutoCompactionMiddleware(
|
|
1786
2040
|
compactor,
|
|
1787
2041
|
effectiveMaxContext,
|
|
@@ -1877,41 +2131,42 @@ async function startWebUI(opts = {}) {
|
|
|
1877
2131
|
inputCost,
|
|
1878
2132
|
outputCost,
|
|
1879
2133
|
cacheReadCost,
|
|
1880
|
-
projectName:
|
|
2134
|
+
projectName: path4.basename(projectRoot) || projectRoot,
|
|
1881
2135
|
cwd: projectRoot,
|
|
1882
2136
|
mode: modeId,
|
|
1883
2137
|
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
|
|
1884
2138
|
wsToken
|
|
1885
2139
|
};
|
|
1886
2140
|
}
|
|
1887
|
-
const wsToken =
|
|
2141
|
+
const wsToken = generateAuthToken();
|
|
1888
2142
|
console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
|
|
1889
2143
|
const verifyClient2 = (info) => verifyClient({
|
|
1890
2144
|
origin: info.origin,
|
|
1891
2145
|
url: info.req.url ?? "",
|
|
1892
2146
|
hostHeader: info.req.headers.host,
|
|
1893
2147
|
remoteAddress: info.req.socket.remoteAddress,
|
|
1894
|
-
wsHost
|
|
2148
|
+
wsHost,
|
|
1895
2149
|
expectedToken: wsToken
|
|
1896
2150
|
});
|
|
1897
2151
|
const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
|
|
1898
2152
|
const wssPrimary = new WebSocketServer({
|
|
1899
|
-
port:
|
|
1900
|
-
host:
|
|
2153
|
+
port: wsPort,
|
|
2154
|
+
host: wsHost,
|
|
1901
2155
|
verifyClient: verifyClient2,
|
|
1902
2156
|
maxPayload: WS_MAX_PAYLOAD
|
|
1903
2157
|
});
|
|
1904
|
-
const wssSecondary =
|
|
1905
|
-
port:
|
|
2158
|
+
const wssSecondary = wsHost === "127.0.0.1" ? new WebSocketServer({
|
|
2159
|
+
port: wsPort,
|
|
1906
2160
|
host: "::1",
|
|
1907
2161
|
verifyClient: verifyClient2,
|
|
1908
2162
|
maxPayload: WS_MAX_PAYLOAD
|
|
1909
2163
|
}) : null;
|
|
1910
2164
|
const clients = /* @__PURE__ */ new Map();
|
|
1911
|
-
const RATE_LIMIT_MESSAGES =
|
|
2165
|
+
const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
|
|
1912
2166
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1913
2167
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
1914
2168
|
function checkRateLimit(ws, client) {
|
|
2169
|
+
if (RATE_LIMIT_MESSAGES <= 0) return true;
|
|
1915
2170
|
const now = Date.now();
|
|
1916
2171
|
const key = client.sessionId ?? String(ws);
|
|
1917
2172
|
const limit = rateLimits.get(key);
|
|
@@ -1925,30 +2180,30 @@ async function startWebUI(opts = {}) {
|
|
|
1925
2180
|
}
|
|
1926
2181
|
let runLock = null;
|
|
1927
2182
|
console.log(
|
|
1928
|
-
`[WebUI] WebSocket server running on ws://${
|
|
2183
|
+
`[WebUI] WebSocket server running on ws://${wsHost}:${wsPort}` + (wssSecondary ? ` (and ws://[::1]:${wsPort})` : "")
|
|
1929
2184
|
);
|
|
1930
2185
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
1931
2186
|
function setupEvents() {
|
|
1932
2187
|
events.on("iteration.started", (e) => {
|
|
1933
|
-
broadcast({
|
|
2188
|
+
broadcast(clients, {
|
|
1934
2189
|
type: "iteration.started",
|
|
1935
2190
|
payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
|
|
1936
2191
|
});
|
|
1937
2192
|
});
|
|
1938
2193
|
events.on("provider.text_delta", (e) => {
|
|
1939
|
-
broadcast({ type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
|
|
2194
|
+
broadcast(clients, { type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
|
|
1940
2195
|
});
|
|
1941
2196
|
events.on("provider.thinking_delta", (e) => {
|
|
1942
|
-
broadcast({ type: "provider.thinking_delta", payload: { text: e.text } });
|
|
2197
|
+
broadcast(clients, { type: "provider.thinking_delta", payload: { text: e.text } });
|
|
1943
2198
|
});
|
|
1944
2199
|
events.on("tool.started", (e) => {
|
|
1945
|
-
broadcast({
|
|
2200
|
+
broadcast(clients, {
|
|
1946
2201
|
type: "tool.started",
|
|
1947
2202
|
payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
|
|
1948
2203
|
});
|
|
1949
2204
|
});
|
|
1950
2205
|
events.on("tool.progress", (e) => {
|
|
1951
|
-
broadcast({
|
|
2206
|
+
broadcast(clients, {
|
|
1952
2207
|
type: "tool.progress",
|
|
1953
2208
|
payload: {
|
|
1954
2209
|
id: e.id,
|
|
@@ -1959,7 +2214,7 @@ async function startWebUI(opts = {}) {
|
|
|
1959
2214
|
});
|
|
1960
2215
|
});
|
|
1961
2216
|
events.on("tool.executed", (e) => {
|
|
1962
|
-
broadcast({
|
|
2217
|
+
broadcast(clients, {
|
|
1963
2218
|
type: "tool.executed",
|
|
1964
2219
|
payload: {
|
|
1965
2220
|
// Forward the tool_use id so frontend can correlate with the
|
|
@@ -1974,13 +2229,13 @@ async function startWebUI(opts = {}) {
|
|
|
1974
2229
|
output: e.output
|
|
1975
2230
|
}
|
|
1976
2231
|
});
|
|
1977
|
-
broadcast({
|
|
2232
|
+
broadcast(clients, {
|
|
1978
2233
|
type: "todos.updated",
|
|
1979
2234
|
payload: { todos: [...context.todos] }
|
|
1980
2235
|
});
|
|
1981
2236
|
});
|
|
1982
2237
|
events.on("provider.response", (e) => {
|
|
1983
|
-
broadcast({
|
|
2238
|
+
broadcast(clients, {
|
|
1984
2239
|
type: "provider.response",
|
|
1985
2240
|
payload: {
|
|
1986
2241
|
usage: e.usage,
|
|
@@ -1990,7 +2245,7 @@ async function startWebUI(opts = {}) {
|
|
|
1990
2245
|
});
|
|
1991
2246
|
});
|
|
1992
2247
|
events.on("context.repaired", (e) => {
|
|
1993
|
-
broadcast({
|
|
2248
|
+
broadcast(clients, {
|
|
1994
2249
|
type: "context.repaired",
|
|
1995
2250
|
payload: {
|
|
1996
2251
|
removedToolUses: e.removedToolUses,
|
|
@@ -2002,7 +2257,7 @@ async function startWebUI(opts = {}) {
|
|
|
2002
2257
|
events.on("tool.confirm_needed", (e) => {
|
|
2003
2258
|
const id = e.toolUseId ?? `confirm_${Date.now()}`;
|
|
2004
2259
|
pendingConfirms.set(id, e.resolve);
|
|
2005
|
-
broadcast({
|
|
2260
|
+
broadcast(clients, {
|
|
2006
2261
|
type: "tool.confirm_needed",
|
|
2007
2262
|
payload: {
|
|
2008
2263
|
id,
|
|
@@ -2013,7 +2268,7 @@ async function startWebUI(opts = {}) {
|
|
|
2013
2268
|
});
|
|
2014
2269
|
});
|
|
2015
2270
|
events.on("error", (e) => {
|
|
2016
|
-
broadcast({
|
|
2271
|
+
broadcast(clients, {
|
|
2017
2272
|
type: "error",
|
|
2018
2273
|
payload: {
|
|
2019
2274
|
phase: e.phase,
|
|
@@ -2021,19 +2276,71 @@ async function startWebUI(opts = {}) {
|
|
|
2021
2276
|
}
|
|
2022
2277
|
});
|
|
2023
2278
|
});
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2279
|
+
const forwardSubagent = (kind, payload) => broadcast(clients, { type: "subagent.event", payload: { kind, ...payload } });
|
|
2280
|
+
events.on(
|
|
2281
|
+
"subagent.spawned",
|
|
2282
|
+
(e) => forwardSubagent("spawned", {
|
|
2283
|
+
subagentId: e.subagentId,
|
|
2284
|
+
taskId: e.taskId,
|
|
2285
|
+
name: e.name,
|
|
2286
|
+
provider: e.provider,
|
|
2287
|
+
model: e.model,
|
|
2288
|
+
description: e.description
|
|
2289
|
+
})
|
|
2290
|
+
);
|
|
2291
|
+
events.on(
|
|
2292
|
+
"subagent.task_started",
|
|
2293
|
+
(e) => forwardSubagent("task_started", {
|
|
2294
|
+
subagentId: e.subagentId,
|
|
2295
|
+
taskId: e.taskId,
|
|
2296
|
+
description: e.description
|
|
2297
|
+
})
|
|
2298
|
+
);
|
|
2299
|
+
events.on(
|
|
2300
|
+
"subagent.tool_executed",
|
|
2301
|
+
(e) => forwardSubagent("tool_executed", {
|
|
2302
|
+
subagentId: e.subagentId,
|
|
2303
|
+
toolName: e.name,
|
|
2304
|
+
durationMs: e.durationMs,
|
|
2305
|
+
ok: e.ok
|
|
2306
|
+
})
|
|
2307
|
+
);
|
|
2308
|
+
events.on(
|
|
2309
|
+
"subagent.iteration_summary",
|
|
2310
|
+
(e) => forwardSubagent("iteration_summary", {
|
|
2311
|
+
subagentId: e.subagentId,
|
|
2312
|
+
iteration: e.iteration,
|
|
2313
|
+
toolCalls: e.toolCalls,
|
|
2314
|
+
costUsd: e.costUsd,
|
|
2315
|
+
currentTool: e.currentTool
|
|
2316
|
+
})
|
|
2317
|
+
);
|
|
2318
|
+
events.on(
|
|
2319
|
+
"subagent.budget_extended",
|
|
2320
|
+
(e) => forwardSubagent("budget_extended", {
|
|
2321
|
+
subagentId: e.subagentId,
|
|
2322
|
+
totalExtensions: e.totalExtensions
|
|
2323
|
+
})
|
|
2324
|
+
);
|
|
2325
|
+
events.on(
|
|
2326
|
+
"subagent.ctx_pct",
|
|
2327
|
+
(e) => forwardSubagent("ctx_pct", {
|
|
2328
|
+
subagentId: e.subagentId,
|
|
2329
|
+
load: e.load,
|
|
2330
|
+
tokens: e.tokens,
|
|
2331
|
+
maxContext: e.maxContext
|
|
2332
|
+
})
|
|
2333
|
+
);
|
|
2334
|
+
events.on(
|
|
2335
|
+
"subagent.task_completed",
|
|
2336
|
+
(e) => forwardSubagent("task_completed", {
|
|
2337
|
+
subagentId: e.subagentId,
|
|
2338
|
+
status: e.status,
|
|
2339
|
+
iterations: e.iterations,
|
|
2340
|
+
toolCalls: e.toolCalls,
|
|
2341
|
+
error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0
|
|
2342
|
+
})
|
|
2343
|
+
);
|
|
2037
2344
|
}
|
|
2038
2345
|
const handleConnection = (ws) => {
|
|
2039
2346
|
const client = { ws, sessionId: session.id, connectedAt: Date.now() };
|
|
@@ -2094,13 +2401,13 @@ async function startWebUI(opts = {}) {
|
|
|
2094
2401
|
console.log(`[WebUI] Backend ready (${label})`);
|
|
2095
2402
|
setupEvents();
|
|
2096
2403
|
};
|
|
2097
|
-
wssPrimary.on("listening", () => armOnce(`${
|
|
2404
|
+
wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
|
|
2098
2405
|
wssPrimary.on("connection", handleConnection);
|
|
2099
2406
|
wssPrimary.on("error", (err) => {
|
|
2100
|
-
console.error(`[WebUI] Primary WS server error (${
|
|
2407
|
+
console.error(`[WebUI] Primary WS server error (${wsHost}):`, err);
|
|
2101
2408
|
});
|
|
2102
2409
|
if (wssSecondary) {
|
|
2103
|
-
wssSecondary.on("listening", () => armOnce(`::1:${
|
|
2410
|
+
wssSecondary.on("listening", () => armOnce(`::1:${wsPort}`));
|
|
2104
2411
|
wssSecondary.on("connection", handleConnection);
|
|
2105
2412
|
wssSecondary.on("error", (err) => {
|
|
2106
2413
|
if (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL") {
|
|
@@ -2177,7 +2484,7 @@ async function startWebUI(opts = {}) {
|
|
|
2177
2484
|
}
|
|
2178
2485
|
case "abort":
|
|
2179
2486
|
runLock?.abort();
|
|
2180
|
-
broadcast({ type: "error", payload: { phase: "abort", message: "User aborted" } });
|
|
2487
|
+
broadcast(clients, { type: "error", payload: { phase: "abort", message: "User aborted" } });
|
|
2181
2488
|
break;
|
|
2182
2489
|
case "ping":
|
|
2183
2490
|
send(ws, { type: "pong", payload: {} });
|
|
@@ -2196,7 +2503,7 @@ async function startWebUI(opts = {}) {
|
|
|
2196
2503
|
context.fileMtimes.clear();
|
|
2197
2504
|
tokenCounter.reset();
|
|
2198
2505
|
sessionStartedAt = Date.now();
|
|
2199
|
-
broadcast({ type: "session.start", payload: await sessionStartPayload() });
|
|
2506
|
+
broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
|
|
2200
2507
|
break;
|
|
2201
2508
|
}
|
|
2202
2509
|
case "context.clear": {
|
|
@@ -2206,7 +2513,7 @@ async function startWebUI(opts = {}) {
|
|
|
2206
2513
|
context.fileMtimes.clear();
|
|
2207
2514
|
tokenCounter.reset();
|
|
2208
2515
|
sendResult(ws, true, "Context cleared");
|
|
2209
|
-
broadcast({
|
|
2516
|
+
broadcast(clients, {
|
|
2210
2517
|
type: "session.start",
|
|
2211
2518
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
2212
2519
|
});
|
|
@@ -2265,7 +2572,7 @@ async function startWebUI(opts = {}) {
|
|
|
2265
2572
|
beforeMessages,
|
|
2266
2573
|
afterMessages: context.messages.length
|
|
2267
2574
|
};
|
|
2268
|
-
broadcast({ type: "context.repaired", payload });
|
|
2575
|
+
broadcast(clients, { type: "context.repaired", payload });
|
|
2269
2576
|
const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
|
|
2270
2577
|
sendResult(
|
|
2271
2578
|
ws,
|
|
@@ -2303,7 +2610,7 @@ async function startWebUI(opts = {}) {
|
|
|
2303
2610
|
context.meta["contextWindowMode"] = policy.id;
|
|
2304
2611
|
context.meta["contextWindowPolicy"] = policy;
|
|
2305
2612
|
sendResult(ws, true, `Context mode switched to ${policy.id}`);
|
|
2306
|
-
broadcast({
|
|
2613
|
+
broadcast(clients, {
|
|
2307
2614
|
type: "context.mode.changed",
|
|
2308
2615
|
payload: { id: policy.id, name: policy.name, policy }
|
|
2309
2616
|
});
|
|
@@ -2329,7 +2636,7 @@ async function startWebUI(opts = {}) {
|
|
|
2329
2636
|
break;
|
|
2330
2637
|
}
|
|
2331
2638
|
case "providers.saved": {
|
|
2332
|
-
const saved = await
|
|
2639
|
+
const saved = await loadConfigProviders();
|
|
2333
2640
|
send(ws, {
|
|
2334
2641
|
type: "providers.saved",
|
|
2335
2642
|
payload: {
|
|
@@ -2388,11 +2695,11 @@ async function startWebUI(opts = {}) {
|
|
|
2388
2695
|
updateAutoCompactionMaxContext?.(newProv);
|
|
2389
2696
|
try {
|
|
2390
2697
|
configWriteLock = configWriteLock.then(async () => {
|
|
2391
|
-
const raw = await
|
|
2698
|
+
const raw = await fs4.readFile(globalConfigPath, "utf8");
|
|
2392
2699
|
const parsed = JSON.parse(raw);
|
|
2393
2700
|
parsed.provider = newProvider;
|
|
2394
2701
|
parsed.model = newModel;
|
|
2395
|
-
await
|
|
2702
|
+
await atomicWrite3(globalConfigPath, JSON.stringify(parsed, null, 2));
|
|
2396
2703
|
});
|
|
2397
2704
|
await configWriteLock;
|
|
2398
2705
|
} catch (err) {
|
|
@@ -2412,7 +2719,7 @@ async function startWebUI(opts = {}) {
|
|
|
2412
2719
|
});
|
|
2413
2720
|
break;
|
|
2414
2721
|
}
|
|
2415
|
-
broadcast({ type: "session.start", payload: await sessionStartPayload() });
|
|
2722
|
+
broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
|
|
2416
2723
|
break;
|
|
2417
2724
|
}
|
|
2418
2725
|
case "key.add":
|
|
@@ -2501,7 +2808,7 @@ async function startWebUI(opts = {}) {
|
|
|
2501
2808
|
tokenCounter.reset();
|
|
2502
2809
|
tokenCounter.account(resumed.data.usage, config.model);
|
|
2503
2810
|
sessionStartedAt = Date.now();
|
|
2504
|
-
broadcast({
|
|
2811
|
+
broadcast(clients, {
|
|
2505
2812
|
type: "session.start",
|
|
2506
2813
|
payload: {
|
|
2507
2814
|
...await sessionStartPayload(),
|
|
@@ -2641,7 +2948,7 @@ async function startWebUI(opts = {}) {
|
|
|
2641
2948
|
case "todos.clear": {
|
|
2642
2949
|
context.state.replaceTodos([]);
|
|
2643
2950
|
sendResult(ws, true, "Todos cleared");
|
|
2644
|
-
broadcast({ type: "todos.updated", payload: { todos: [] } });
|
|
2951
|
+
broadcast(clients, { type: "todos.updated", payload: { todos: [] } });
|
|
2645
2952
|
break;
|
|
2646
2953
|
}
|
|
2647
2954
|
case "plan.get": {
|
|
@@ -2688,7 +2995,7 @@ async function startWebUI(opts = {}) {
|
|
|
2688
2995
|
}
|
|
2689
2996
|
await savePlan(planPath, plan);
|
|
2690
2997
|
sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
|
|
2691
|
-
broadcast({
|
|
2998
|
+
broadcast(clients, {
|
|
2692
2999
|
type: "plan.updated",
|
|
2693
3000
|
payload: { plan }
|
|
2694
3001
|
});
|
|
@@ -2705,7 +3012,7 @@ async function startWebUI(opts = {}) {
|
|
|
2705
3012
|
if (depth > 8 || results.length >= 600) return;
|
|
2706
3013
|
let entries = [];
|
|
2707
3014
|
try {
|
|
2708
|
-
entries = await
|
|
3015
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
2709
3016
|
} catch {
|
|
2710
3017
|
return;
|
|
2711
3018
|
}
|
|
@@ -2715,7 +3022,7 @@ async function startWebUI(opts = {}) {
|
|
|
2715
3022
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
2716
3023
|
if (e.isDirectory()) {
|
|
2717
3024
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
2718
|
-
await walk(
|
|
3025
|
+
await walk(path4.join(dir, e.name), childRel, depth + 1);
|
|
2719
3026
|
} else if (e.isFile()) {
|
|
2720
3027
|
results.push(childRel);
|
|
2721
3028
|
}
|
|
@@ -2784,7 +3091,7 @@ async function startWebUI(opts = {}) {
|
|
|
2784
3091
|
model: config.model
|
|
2785
3092
|
});
|
|
2786
3093
|
sendResult(ws, true, `Switched to mode "${id}"`);
|
|
2787
|
-
broadcast({
|
|
3094
|
+
broadcast(clients, {
|
|
2788
3095
|
type: "session.start",
|
|
2789
3096
|
payload: { ...await sessionStartPayload() }
|
|
2790
3097
|
});
|
|
@@ -2823,39 +3130,20 @@ async function startWebUI(opts = {}) {
|
|
|
2823
3130
|
}
|
|
2824
3131
|
}
|
|
2825
3132
|
}
|
|
2826
|
-
async function
|
|
2827
|
-
|
|
2828
|
-
const raw = await fs2.readFile(globalConfigPath, "utf8");
|
|
2829
|
-
const parsed = JSON.parse(raw);
|
|
2830
|
-
if (!parsed.providers) return {};
|
|
2831
|
-
return decryptConfigSecrets(parsed.providers, vault);
|
|
2832
|
-
} catch {
|
|
2833
|
-
return {};
|
|
2834
|
-
}
|
|
3133
|
+
async function loadConfigProviders() {
|
|
3134
|
+
return loadSavedProviders(globalConfigPath, vault);
|
|
2835
3135
|
}
|
|
2836
|
-
async function
|
|
2837
|
-
configWriteLock = configWriteLock.then(
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
const raw = await fs2.readFile(globalConfigPath, "utf8");
|
|
2841
|
-
parsed = JSON.parse(raw);
|
|
2842
|
-
} catch {
|
|
2843
|
-
parsed = {};
|
|
2844
|
-
}
|
|
2845
|
-
parsed["providers"] = providers;
|
|
2846
|
-
const encrypted = encryptConfigSecrets(parsed, vault);
|
|
2847
|
-
await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
2848
|
-
});
|
|
3136
|
+
async function saveConfigProviders(providers) {
|
|
3137
|
+
configWriteLock = configWriteLock.then(
|
|
3138
|
+
() => saveProviders(globalConfigPath, vault, providers)
|
|
3139
|
+
);
|
|
2849
3140
|
await configWriteLock;
|
|
2850
3141
|
}
|
|
2851
|
-
function sendResult(ws, success, message) {
|
|
2852
|
-
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
2853
|
-
}
|
|
2854
3142
|
async function handleKeyUpsert(ws, providerId, label, apiKey) {
|
|
2855
3143
|
try {
|
|
2856
|
-
const providers = await
|
|
3144
|
+
const providers = await loadConfigProviders();
|
|
2857
3145
|
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
2858
|
-
if (result.ok) await
|
|
3146
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2859
3147
|
sendResult(ws, result.ok, result.message);
|
|
2860
3148
|
} catch (err) {
|
|
2861
3149
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2863,9 +3151,9 @@ async function startWebUI(opts = {}) {
|
|
|
2863
3151
|
}
|
|
2864
3152
|
async function handleKeyDelete(ws, providerId, label) {
|
|
2865
3153
|
try {
|
|
2866
|
-
const providers = await
|
|
3154
|
+
const providers = await loadConfigProviders();
|
|
2867
3155
|
const result = deleteKey(providers, providerId, label);
|
|
2868
|
-
if (result.ok) await
|
|
3156
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2869
3157
|
sendResult(ws, result.ok, result.message);
|
|
2870
3158
|
} catch (err) {
|
|
2871
3159
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2873,9 +3161,9 @@ async function startWebUI(opts = {}) {
|
|
|
2873
3161
|
}
|
|
2874
3162
|
async function handleKeySetActive(ws, providerId, label) {
|
|
2875
3163
|
try {
|
|
2876
|
-
const providers = await
|
|
3164
|
+
const providers = await loadConfigProviders();
|
|
2877
3165
|
const result = setActiveKey(providers, providerId, label);
|
|
2878
|
-
if (result.ok) await
|
|
3166
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2879
3167
|
sendResult(ws, result.ok, result.message);
|
|
2880
3168
|
} catch (err) {
|
|
2881
3169
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2883,9 +3171,9 @@ async function startWebUI(opts = {}) {
|
|
|
2883
3171
|
}
|
|
2884
3172
|
async function handleProviderAdd(ws, payload) {
|
|
2885
3173
|
try {
|
|
2886
|
-
const providers = await
|
|
3174
|
+
const providers = await loadConfigProviders();
|
|
2887
3175
|
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
2888
|
-
if (result.ok) await
|
|
3176
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2889
3177
|
sendResult(ws, result.ok, result.message);
|
|
2890
3178
|
} catch (err) {
|
|
2891
3179
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2893,22 +3181,37 @@ async function startWebUI(opts = {}) {
|
|
|
2893
3181
|
}
|
|
2894
3182
|
async function handleProviderRemove(ws, providerId) {
|
|
2895
3183
|
try {
|
|
2896
|
-
const providers = await
|
|
3184
|
+
const providers = await loadConfigProviders();
|
|
2897
3185
|
const result = removeProvider(providers, providerId);
|
|
2898
|
-
if (result.ok) await
|
|
3186
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2899
3187
|
sendResult(ws, result.ok, result.message);
|
|
2900
3188
|
} catch (err) {
|
|
2901
3189
|
sendResult(ws, false, errMessage(err));
|
|
2902
3190
|
}
|
|
2903
3191
|
}
|
|
2904
3192
|
const httpServer = createHttpServer({
|
|
2905
|
-
host:
|
|
2906
|
-
distDir:
|
|
2907
|
-
wsPort
|
|
3193
|
+
host: wsHost,
|
|
3194
|
+
distDir: path4.resolve(import.meta.dirname, "../../dist"),
|
|
3195
|
+
wsPort
|
|
2908
3196
|
});
|
|
2909
|
-
const
|
|
2910
|
-
httpServer.listen(httpPort,
|
|
2911
|
-
|
|
3197
|
+
const registryBaseDir = path4.dirname(globalConfigPath);
|
|
3198
|
+
httpServer.listen(httpPort, wsHost, () => {
|
|
3199
|
+
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
3200
|
+
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
3201
|
+
if (opts.open) openBrowser(openUrl);
|
|
3202
|
+
void registerInstance(
|
|
3203
|
+
{
|
|
3204
|
+
pid: process.pid,
|
|
3205
|
+
httpPort,
|
|
3206
|
+
wsPort,
|
|
3207
|
+
host: wsHost,
|
|
3208
|
+
projectRoot,
|
|
3209
|
+
projectName: path4.basename(projectRoot) || projectRoot,
|
|
3210
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3211
|
+
url: `http://${wsHost}:${httpPort}`
|
|
3212
|
+
},
|
|
3213
|
+
registryBaseDir
|
|
3214
|
+
).catch((err) => console.warn("[WebUI] Could not record instance:", errMessage(err)));
|
|
2912
3215
|
});
|
|
2913
3216
|
registerShutdownHandlers({
|
|
2914
3217
|
flushSession: async () => {
|
|
@@ -2920,16 +3223,31 @@ async function startWebUI(opts = {}) {
|
|
|
2920
3223
|
await session.close();
|
|
2921
3224
|
},
|
|
2922
3225
|
clients: () => clients.keys(),
|
|
2923
|
-
servers: [httpServer, wssPrimary, wssSecondary]
|
|
3226
|
+
servers: [httpServer, wssPrimary, wssSecondary],
|
|
3227
|
+
// Drop this instance from the registry on a clean exit so the file reflects
|
|
3228
|
+
// reality. Crash exits are healed by the next register()/list() prune pass.
|
|
3229
|
+
onShutdown: () => unregisterInstance(process.pid, registryBaseDir)
|
|
2924
3230
|
});
|
|
2925
3231
|
}
|
|
2926
3232
|
|
|
2927
3233
|
// src/server/entry.ts
|
|
2928
|
-
var
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
3234
|
+
var argv = process.argv.slice(2);
|
|
3235
|
+
if (argv.includes("--list") || argv.includes("-l") || argv[0] === "ls") {
|
|
3236
|
+
listInstances().then((instances) => {
|
|
3237
|
+
console.log(formatInstances(instances));
|
|
3238
|
+
process.exit(0);
|
|
3239
|
+
}).catch((err) => {
|
|
3240
|
+
console.error("[WebUI] Could not read instance registry:", err);
|
|
3241
|
+
process.exit(1);
|
|
3242
|
+
});
|
|
3243
|
+
} else {
|
|
3244
|
+
const wsPort = Number.parseInt(process.env["WS_PORT"] ?? "3457", 10);
|
|
3245
|
+
const wsHost = process.env["WS_HOST"] ?? "127.0.0.1";
|
|
3246
|
+
const open = argv.includes("--open") || argv.includes("-o") || process.env["WEBUI_OPEN"] === "1";
|
|
3247
|
+
console.log(`[WebUI] Starting standalone server on ${wsHost}:${wsPort}...`);
|
|
3248
|
+
startWebUI({ wsPort, wsHost, open }).catch((err) => {
|
|
3249
|
+
console.error("[WebUI] Fatal error:", err);
|
|
3250
|
+
process.exit(1);
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
2935
3253
|
//# sourceMappingURL=entry.js.map
|