@wrongstack/webui 0.63.4 → 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 +434 -124
- package/dist/server/entry.js.map +1 -1
- package/dist/server/index.d.ts +298 -2
- package/dist/server/index.js +435 -94
- 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 {
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -1885,41 +2131,42 @@ async function startWebUI(opts = {}) {
|
|
|
1885
2131
|
inputCost,
|
|
1886
2132
|
outputCost,
|
|
1887
2133
|
cacheReadCost,
|
|
1888
|
-
projectName:
|
|
2134
|
+
projectName: path4.basename(projectRoot) || projectRoot,
|
|
1889
2135
|
cwd: projectRoot,
|
|
1890
2136
|
mode: modeId,
|
|
1891
2137
|
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
|
|
1892
2138
|
wsToken
|
|
1893
2139
|
};
|
|
1894
2140
|
}
|
|
1895
|
-
const wsToken =
|
|
2141
|
+
const wsToken = generateAuthToken();
|
|
1896
2142
|
console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
|
|
1897
2143
|
const verifyClient2 = (info) => verifyClient({
|
|
1898
2144
|
origin: info.origin,
|
|
1899
2145
|
url: info.req.url ?? "",
|
|
1900
2146
|
hostHeader: info.req.headers.host,
|
|
1901
2147
|
remoteAddress: info.req.socket.remoteAddress,
|
|
1902
|
-
wsHost
|
|
2148
|
+
wsHost,
|
|
1903
2149
|
expectedToken: wsToken
|
|
1904
2150
|
});
|
|
1905
2151
|
const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
|
|
1906
2152
|
const wssPrimary = new WebSocketServer({
|
|
1907
|
-
port:
|
|
1908
|
-
host:
|
|
2153
|
+
port: wsPort,
|
|
2154
|
+
host: wsHost,
|
|
1909
2155
|
verifyClient: verifyClient2,
|
|
1910
2156
|
maxPayload: WS_MAX_PAYLOAD
|
|
1911
2157
|
});
|
|
1912
|
-
const wssSecondary =
|
|
1913
|
-
port:
|
|
2158
|
+
const wssSecondary = wsHost === "127.0.0.1" ? new WebSocketServer({
|
|
2159
|
+
port: wsPort,
|
|
1914
2160
|
host: "::1",
|
|
1915
2161
|
verifyClient: verifyClient2,
|
|
1916
2162
|
maxPayload: WS_MAX_PAYLOAD
|
|
1917
2163
|
}) : null;
|
|
1918
2164
|
const clients = /* @__PURE__ */ new Map();
|
|
1919
|
-
const RATE_LIMIT_MESSAGES =
|
|
2165
|
+
const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
|
|
1920
2166
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1921
2167
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
1922
2168
|
function checkRateLimit(ws, client) {
|
|
2169
|
+
if (RATE_LIMIT_MESSAGES <= 0) return true;
|
|
1923
2170
|
const now = Date.now();
|
|
1924
2171
|
const key = client.sessionId ?? String(ws);
|
|
1925
2172
|
const limit = rateLimits.get(key);
|
|
@@ -1933,30 +2180,30 @@ async function startWebUI(opts = {}) {
|
|
|
1933
2180
|
}
|
|
1934
2181
|
let runLock = null;
|
|
1935
2182
|
console.log(
|
|
1936
|
-
`[WebUI] WebSocket server running on ws://${
|
|
2183
|
+
`[WebUI] WebSocket server running on ws://${wsHost}:${wsPort}` + (wssSecondary ? ` (and ws://[::1]:${wsPort})` : "")
|
|
1937
2184
|
);
|
|
1938
2185
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
1939
2186
|
function setupEvents() {
|
|
1940
2187
|
events.on("iteration.started", (e) => {
|
|
1941
|
-
broadcast({
|
|
2188
|
+
broadcast(clients, {
|
|
1942
2189
|
type: "iteration.started",
|
|
1943
2190
|
payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
|
|
1944
2191
|
});
|
|
1945
2192
|
});
|
|
1946
2193
|
events.on("provider.text_delta", (e) => {
|
|
1947
|
-
broadcast({ type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
|
|
2194
|
+
broadcast(clients, { type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
|
|
1948
2195
|
});
|
|
1949
2196
|
events.on("provider.thinking_delta", (e) => {
|
|
1950
|
-
broadcast({ type: "provider.thinking_delta", payload: { text: e.text } });
|
|
2197
|
+
broadcast(clients, { type: "provider.thinking_delta", payload: { text: e.text } });
|
|
1951
2198
|
});
|
|
1952
2199
|
events.on("tool.started", (e) => {
|
|
1953
|
-
broadcast({
|
|
2200
|
+
broadcast(clients, {
|
|
1954
2201
|
type: "tool.started",
|
|
1955
2202
|
payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
|
|
1956
2203
|
});
|
|
1957
2204
|
});
|
|
1958
2205
|
events.on("tool.progress", (e) => {
|
|
1959
|
-
broadcast({
|
|
2206
|
+
broadcast(clients, {
|
|
1960
2207
|
type: "tool.progress",
|
|
1961
2208
|
payload: {
|
|
1962
2209
|
id: e.id,
|
|
@@ -1967,7 +2214,7 @@ async function startWebUI(opts = {}) {
|
|
|
1967
2214
|
});
|
|
1968
2215
|
});
|
|
1969
2216
|
events.on("tool.executed", (e) => {
|
|
1970
|
-
broadcast({
|
|
2217
|
+
broadcast(clients, {
|
|
1971
2218
|
type: "tool.executed",
|
|
1972
2219
|
payload: {
|
|
1973
2220
|
// Forward the tool_use id so frontend can correlate with the
|
|
@@ -1982,13 +2229,13 @@ async function startWebUI(opts = {}) {
|
|
|
1982
2229
|
output: e.output
|
|
1983
2230
|
}
|
|
1984
2231
|
});
|
|
1985
|
-
broadcast({
|
|
2232
|
+
broadcast(clients, {
|
|
1986
2233
|
type: "todos.updated",
|
|
1987
2234
|
payload: { todos: [...context.todos] }
|
|
1988
2235
|
});
|
|
1989
2236
|
});
|
|
1990
2237
|
events.on("provider.response", (e) => {
|
|
1991
|
-
broadcast({
|
|
2238
|
+
broadcast(clients, {
|
|
1992
2239
|
type: "provider.response",
|
|
1993
2240
|
payload: {
|
|
1994
2241
|
usage: e.usage,
|
|
@@ -1998,7 +2245,7 @@ async function startWebUI(opts = {}) {
|
|
|
1998
2245
|
});
|
|
1999
2246
|
});
|
|
2000
2247
|
events.on("context.repaired", (e) => {
|
|
2001
|
-
broadcast({
|
|
2248
|
+
broadcast(clients, {
|
|
2002
2249
|
type: "context.repaired",
|
|
2003
2250
|
payload: {
|
|
2004
2251
|
removedToolUses: e.removedToolUses,
|
|
@@ -2010,7 +2257,7 @@ async function startWebUI(opts = {}) {
|
|
|
2010
2257
|
events.on("tool.confirm_needed", (e) => {
|
|
2011
2258
|
const id = e.toolUseId ?? `confirm_${Date.now()}`;
|
|
2012
2259
|
pendingConfirms.set(id, e.resolve);
|
|
2013
|
-
broadcast({
|
|
2260
|
+
broadcast(clients, {
|
|
2014
2261
|
type: "tool.confirm_needed",
|
|
2015
2262
|
payload: {
|
|
2016
2263
|
id,
|
|
@@ -2021,7 +2268,7 @@ async function startWebUI(opts = {}) {
|
|
|
2021
2268
|
});
|
|
2022
2269
|
});
|
|
2023
2270
|
events.on("error", (e) => {
|
|
2024
|
-
broadcast({
|
|
2271
|
+
broadcast(clients, {
|
|
2025
2272
|
type: "error",
|
|
2026
2273
|
payload: {
|
|
2027
2274
|
phase: e.phase,
|
|
@@ -2029,19 +2276,71 @@ async function startWebUI(opts = {}) {
|
|
|
2029
2276
|
}
|
|
2030
2277
|
});
|
|
2031
2278
|
});
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
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
|
+
);
|
|
2045
2344
|
}
|
|
2046
2345
|
const handleConnection = (ws) => {
|
|
2047
2346
|
const client = { ws, sessionId: session.id, connectedAt: Date.now() };
|
|
@@ -2102,13 +2401,13 @@ async function startWebUI(opts = {}) {
|
|
|
2102
2401
|
console.log(`[WebUI] Backend ready (${label})`);
|
|
2103
2402
|
setupEvents();
|
|
2104
2403
|
};
|
|
2105
|
-
wssPrimary.on("listening", () => armOnce(`${
|
|
2404
|
+
wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
|
|
2106
2405
|
wssPrimary.on("connection", handleConnection);
|
|
2107
2406
|
wssPrimary.on("error", (err) => {
|
|
2108
|
-
console.error(`[WebUI] Primary WS server error (${
|
|
2407
|
+
console.error(`[WebUI] Primary WS server error (${wsHost}):`, err);
|
|
2109
2408
|
});
|
|
2110
2409
|
if (wssSecondary) {
|
|
2111
|
-
wssSecondary.on("listening", () => armOnce(`::1:${
|
|
2410
|
+
wssSecondary.on("listening", () => armOnce(`::1:${wsPort}`));
|
|
2112
2411
|
wssSecondary.on("connection", handleConnection);
|
|
2113
2412
|
wssSecondary.on("error", (err) => {
|
|
2114
2413
|
if (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL") {
|
|
@@ -2185,7 +2484,7 @@ async function startWebUI(opts = {}) {
|
|
|
2185
2484
|
}
|
|
2186
2485
|
case "abort":
|
|
2187
2486
|
runLock?.abort();
|
|
2188
|
-
broadcast({ type: "error", payload: { phase: "abort", message: "User aborted" } });
|
|
2487
|
+
broadcast(clients, { type: "error", payload: { phase: "abort", message: "User aborted" } });
|
|
2189
2488
|
break;
|
|
2190
2489
|
case "ping":
|
|
2191
2490
|
send(ws, { type: "pong", payload: {} });
|
|
@@ -2204,7 +2503,7 @@ async function startWebUI(opts = {}) {
|
|
|
2204
2503
|
context.fileMtimes.clear();
|
|
2205
2504
|
tokenCounter.reset();
|
|
2206
2505
|
sessionStartedAt = Date.now();
|
|
2207
|
-
broadcast({ type: "session.start", payload: await sessionStartPayload() });
|
|
2506
|
+
broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
|
|
2208
2507
|
break;
|
|
2209
2508
|
}
|
|
2210
2509
|
case "context.clear": {
|
|
@@ -2214,7 +2513,7 @@ async function startWebUI(opts = {}) {
|
|
|
2214
2513
|
context.fileMtimes.clear();
|
|
2215
2514
|
tokenCounter.reset();
|
|
2216
2515
|
sendResult(ws, true, "Context cleared");
|
|
2217
|
-
broadcast({
|
|
2516
|
+
broadcast(clients, {
|
|
2218
2517
|
type: "session.start",
|
|
2219
2518
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
2220
2519
|
});
|
|
@@ -2273,7 +2572,7 @@ async function startWebUI(opts = {}) {
|
|
|
2273
2572
|
beforeMessages,
|
|
2274
2573
|
afterMessages: context.messages.length
|
|
2275
2574
|
};
|
|
2276
|
-
broadcast({ type: "context.repaired", payload });
|
|
2575
|
+
broadcast(clients, { type: "context.repaired", payload });
|
|
2277
2576
|
const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
|
|
2278
2577
|
sendResult(
|
|
2279
2578
|
ws,
|
|
@@ -2311,7 +2610,7 @@ async function startWebUI(opts = {}) {
|
|
|
2311
2610
|
context.meta["contextWindowMode"] = policy.id;
|
|
2312
2611
|
context.meta["contextWindowPolicy"] = policy;
|
|
2313
2612
|
sendResult(ws, true, `Context mode switched to ${policy.id}`);
|
|
2314
|
-
broadcast({
|
|
2613
|
+
broadcast(clients, {
|
|
2315
2614
|
type: "context.mode.changed",
|
|
2316
2615
|
payload: { id: policy.id, name: policy.name, policy }
|
|
2317
2616
|
});
|
|
@@ -2337,7 +2636,7 @@ async function startWebUI(opts = {}) {
|
|
|
2337
2636
|
break;
|
|
2338
2637
|
}
|
|
2339
2638
|
case "providers.saved": {
|
|
2340
|
-
const saved = await
|
|
2639
|
+
const saved = await loadConfigProviders();
|
|
2341
2640
|
send(ws, {
|
|
2342
2641
|
type: "providers.saved",
|
|
2343
2642
|
payload: {
|
|
@@ -2396,11 +2695,11 @@ async function startWebUI(opts = {}) {
|
|
|
2396
2695
|
updateAutoCompactionMaxContext?.(newProv);
|
|
2397
2696
|
try {
|
|
2398
2697
|
configWriteLock = configWriteLock.then(async () => {
|
|
2399
|
-
const raw = await
|
|
2698
|
+
const raw = await fs4.readFile(globalConfigPath, "utf8");
|
|
2400
2699
|
const parsed = JSON.parse(raw);
|
|
2401
2700
|
parsed.provider = newProvider;
|
|
2402
2701
|
parsed.model = newModel;
|
|
2403
|
-
await
|
|
2702
|
+
await atomicWrite3(globalConfigPath, JSON.stringify(parsed, null, 2));
|
|
2404
2703
|
});
|
|
2405
2704
|
await configWriteLock;
|
|
2406
2705
|
} catch (err) {
|
|
@@ -2420,7 +2719,7 @@ async function startWebUI(opts = {}) {
|
|
|
2420
2719
|
});
|
|
2421
2720
|
break;
|
|
2422
2721
|
}
|
|
2423
|
-
broadcast({ type: "session.start", payload: await sessionStartPayload() });
|
|
2722
|
+
broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
|
|
2424
2723
|
break;
|
|
2425
2724
|
}
|
|
2426
2725
|
case "key.add":
|
|
@@ -2509,7 +2808,7 @@ async function startWebUI(opts = {}) {
|
|
|
2509
2808
|
tokenCounter.reset();
|
|
2510
2809
|
tokenCounter.account(resumed.data.usage, config.model);
|
|
2511
2810
|
sessionStartedAt = Date.now();
|
|
2512
|
-
broadcast({
|
|
2811
|
+
broadcast(clients, {
|
|
2513
2812
|
type: "session.start",
|
|
2514
2813
|
payload: {
|
|
2515
2814
|
...await sessionStartPayload(),
|
|
@@ -2649,7 +2948,7 @@ async function startWebUI(opts = {}) {
|
|
|
2649
2948
|
case "todos.clear": {
|
|
2650
2949
|
context.state.replaceTodos([]);
|
|
2651
2950
|
sendResult(ws, true, "Todos cleared");
|
|
2652
|
-
broadcast({ type: "todos.updated", payload: { todos: [] } });
|
|
2951
|
+
broadcast(clients, { type: "todos.updated", payload: { todos: [] } });
|
|
2653
2952
|
break;
|
|
2654
2953
|
}
|
|
2655
2954
|
case "plan.get": {
|
|
@@ -2696,7 +2995,7 @@ async function startWebUI(opts = {}) {
|
|
|
2696
2995
|
}
|
|
2697
2996
|
await savePlan(planPath, plan);
|
|
2698
2997
|
sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
|
|
2699
|
-
broadcast({
|
|
2998
|
+
broadcast(clients, {
|
|
2700
2999
|
type: "plan.updated",
|
|
2701
3000
|
payload: { plan }
|
|
2702
3001
|
});
|
|
@@ -2713,7 +3012,7 @@ async function startWebUI(opts = {}) {
|
|
|
2713
3012
|
if (depth > 8 || results.length >= 600) return;
|
|
2714
3013
|
let entries = [];
|
|
2715
3014
|
try {
|
|
2716
|
-
entries = await
|
|
3015
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
2717
3016
|
} catch {
|
|
2718
3017
|
return;
|
|
2719
3018
|
}
|
|
@@ -2723,7 +3022,7 @@ async function startWebUI(opts = {}) {
|
|
|
2723
3022
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
2724
3023
|
if (e.isDirectory()) {
|
|
2725
3024
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
2726
|
-
await walk(
|
|
3025
|
+
await walk(path4.join(dir, e.name), childRel, depth + 1);
|
|
2727
3026
|
} else if (e.isFile()) {
|
|
2728
3027
|
results.push(childRel);
|
|
2729
3028
|
}
|
|
@@ -2792,7 +3091,7 @@ async function startWebUI(opts = {}) {
|
|
|
2792
3091
|
model: config.model
|
|
2793
3092
|
});
|
|
2794
3093
|
sendResult(ws, true, `Switched to mode "${id}"`);
|
|
2795
|
-
broadcast({
|
|
3094
|
+
broadcast(clients, {
|
|
2796
3095
|
type: "session.start",
|
|
2797
3096
|
payload: { ...await sessionStartPayload() }
|
|
2798
3097
|
});
|
|
@@ -2831,39 +3130,20 @@ async function startWebUI(opts = {}) {
|
|
|
2831
3130
|
}
|
|
2832
3131
|
}
|
|
2833
3132
|
}
|
|
2834
|
-
async function
|
|
2835
|
-
|
|
2836
|
-
const raw = await fs2.readFile(globalConfigPath, "utf8");
|
|
2837
|
-
const parsed = JSON.parse(raw);
|
|
2838
|
-
if (!parsed.providers) return {};
|
|
2839
|
-
return decryptConfigSecrets(parsed.providers, vault);
|
|
2840
|
-
} catch {
|
|
2841
|
-
return {};
|
|
2842
|
-
}
|
|
3133
|
+
async function loadConfigProviders() {
|
|
3134
|
+
return loadSavedProviders(globalConfigPath, vault);
|
|
2843
3135
|
}
|
|
2844
|
-
async function
|
|
2845
|
-
configWriteLock = configWriteLock.then(
|
|
2846
|
-
|
|
2847
|
-
|
|
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
|
-
});
|
|
3136
|
+
async function saveConfigProviders(providers) {
|
|
3137
|
+
configWriteLock = configWriteLock.then(
|
|
3138
|
+
() => saveProviders(globalConfigPath, vault, providers)
|
|
3139
|
+
);
|
|
2857
3140
|
await configWriteLock;
|
|
2858
3141
|
}
|
|
2859
|
-
function sendResult(ws, success, message) {
|
|
2860
|
-
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
2861
|
-
}
|
|
2862
3142
|
async function handleKeyUpsert(ws, providerId, label, apiKey) {
|
|
2863
3143
|
try {
|
|
2864
|
-
const providers = await
|
|
3144
|
+
const providers = await loadConfigProviders();
|
|
2865
3145
|
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
2866
|
-
if (result.ok) await
|
|
3146
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2867
3147
|
sendResult(ws, result.ok, result.message);
|
|
2868
3148
|
} catch (err) {
|
|
2869
3149
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2871,9 +3151,9 @@ async function startWebUI(opts = {}) {
|
|
|
2871
3151
|
}
|
|
2872
3152
|
async function handleKeyDelete(ws, providerId, label) {
|
|
2873
3153
|
try {
|
|
2874
|
-
const providers = await
|
|
3154
|
+
const providers = await loadConfigProviders();
|
|
2875
3155
|
const result = deleteKey(providers, providerId, label);
|
|
2876
|
-
if (result.ok) await
|
|
3156
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2877
3157
|
sendResult(ws, result.ok, result.message);
|
|
2878
3158
|
} catch (err) {
|
|
2879
3159
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2881,9 +3161,9 @@ async function startWebUI(opts = {}) {
|
|
|
2881
3161
|
}
|
|
2882
3162
|
async function handleKeySetActive(ws, providerId, label) {
|
|
2883
3163
|
try {
|
|
2884
|
-
const providers = await
|
|
3164
|
+
const providers = await loadConfigProviders();
|
|
2885
3165
|
const result = setActiveKey(providers, providerId, label);
|
|
2886
|
-
if (result.ok) await
|
|
3166
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2887
3167
|
sendResult(ws, result.ok, result.message);
|
|
2888
3168
|
} catch (err) {
|
|
2889
3169
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2891,9 +3171,9 @@ async function startWebUI(opts = {}) {
|
|
|
2891
3171
|
}
|
|
2892
3172
|
async function handleProviderAdd(ws, payload) {
|
|
2893
3173
|
try {
|
|
2894
|
-
const providers = await
|
|
3174
|
+
const providers = await loadConfigProviders();
|
|
2895
3175
|
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
2896
|
-
if (result.ok) await
|
|
3176
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2897
3177
|
sendResult(ws, result.ok, result.message);
|
|
2898
3178
|
} catch (err) {
|
|
2899
3179
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2901,22 +3181,37 @@ async function startWebUI(opts = {}) {
|
|
|
2901
3181
|
}
|
|
2902
3182
|
async function handleProviderRemove(ws, providerId) {
|
|
2903
3183
|
try {
|
|
2904
|
-
const providers = await
|
|
3184
|
+
const providers = await loadConfigProviders();
|
|
2905
3185
|
const result = removeProvider(providers, providerId);
|
|
2906
|
-
if (result.ok) await
|
|
3186
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2907
3187
|
sendResult(ws, result.ok, result.message);
|
|
2908
3188
|
} catch (err) {
|
|
2909
3189
|
sendResult(ws, false, errMessage(err));
|
|
2910
3190
|
}
|
|
2911
3191
|
}
|
|
2912
3192
|
const httpServer = createHttpServer({
|
|
2913
|
-
host:
|
|
2914
|
-
distDir:
|
|
2915
|
-
wsPort
|
|
3193
|
+
host: wsHost,
|
|
3194
|
+
distDir: path4.resolve(import.meta.dirname, "../../dist"),
|
|
3195
|
+
wsPort
|
|
2916
3196
|
});
|
|
2917
|
-
const
|
|
2918
|
-
httpServer.listen(httpPort,
|
|
2919
|
-
|
|
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)));
|
|
2920
3215
|
});
|
|
2921
3216
|
registerShutdownHandlers({
|
|
2922
3217
|
flushSession: async () => {
|
|
@@ -2928,16 +3223,31 @@ async function startWebUI(opts = {}) {
|
|
|
2928
3223
|
await session.close();
|
|
2929
3224
|
},
|
|
2930
3225
|
clients: () => clients.keys(),
|
|
2931
|
-
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)
|
|
2932
3230
|
});
|
|
2933
3231
|
}
|
|
2934
3232
|
|
|
2935
3233
|
// src/server/entry.ts
|
|
2936
|
-
var
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
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
|
+
}
|
|
2943
3253
|
//# sourceMappingURL=entry.js.map
|