@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/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/server/index.ts
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
2
|
+
import * as fs4 from "fs/promises";
|
|
3
|
+
import * as path4 from "path";
|
|
4
4
|
|
|
5
5
|
// src/server/http-server.ts
|
|
6
6
|
import * as fs from "fs/promises";
|
|
@@ -15,6 +15,16 @@ var MIME_TYPES = {
|
|
|
15
15
|
".png": "image/png",
|
|
16
16
|
".ico": "image/x-icon"
|
|
17
17
|
};
|
|
18
|
+
function injectWsPort(html, wsPort) {
|
|
19
|
+
const tag = `<meta name="wrongstack-ws-port" content="${wsPort}" />`;
|
|
20
|
+
if (html.includes('name="wrongstack-ws-port"')) return html;
|
|
21
|
+
if (html.includes("</head>")) {
|
|
22
|
+
return html.replace("</head>", ` ${tag}
|
|
23
|
+
</head>`);
|
|
24
|
+
}
|
|
25
|
+
return `${tag}
|
|
26
|
+
${html}`;
|
|
27
|
+
}
|
|
18
28
|
function buildCspHeader(wsPort) {
|
|
19
29
|
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'`;
|
|
20
30
|
}
|
|
@@ -55,6 +65,10 @@ function createHttpServer(opts) {
|
|
|
55
65
|
if (ext === ".html") {
|
|
56
66
|
res.setHeader("Cache-Control", "no-cache");
|
|
57
67
|
res.setHeader("Content-Security-Policy", buildCspHeader(wsPort));
|
|
68
|
+
const html = await fs.readFile(resolvedPath, "utf8");
|
|
69
|
+
res.writeHead(200);
|
|
70
|
+
res.end(injectWsPort(html, wsPort));
|
|
71
|
+
return;
|
|
58
72
|
}
|
|
59
73
|
const fileContent = await fs.readFile(resolvedPath);
|
|
60
74
|
res.writeHead(200);
|
|
@@ -62,7 +76,7 @@ function createHttpServer(opts) {
|
|
|
62
76
|
} catch (err) {
|
|
63
77
|
if (err.code === "ENOENT") {
|
|
64
78
|
try {
|
|
65
|
-
const
|
|
79
|
+
const html = await fs.readFile(path.join(distDir, "index.html"), "utf8");
|
|
66
80
|
res.writeHead(200, {
|
|
67
81
|
"Content-Type": "text/html",
|
|
68
82
|
"X-Content-Type-Options": "nosniff",
|
|
@@ -70,7 +84,7 @@ function createHttpServer(opts) {
|
|
|
70
84
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
71
85
|
"Content-Security-Policy": buildCspHeader(wsPort)
|
|
72
86
|
});
|
|
73
|
-
res.end(
|
|
87
|
+
res.end(injectWsPort(html, wsPort));
|
|
74
88
|
} catch {
|
|
75
89
|
res.writeHead(404);
|
|
76
90
|
res.end("Not found");
|
|
@@ -154,7 +168,7 @@ import {
|
|
|
154
168
|
ProviderRegistry,
|
|
155
169
|
TOKENS as TOKENS2,
|
|
156
170
|
ToolRegistry,
|
|
157
|
-
atomicWrite,
|
|
171
|
+
atomicWrite as atomicWrite3,
|
|
158
172
|
createDefaultPipelines,
|
|
159
173
|
DEFAULT_CONTEXT_WINDOW_MODE_ID,
|
|
160
174
|
DEFAULT_TOOLS_CONFIG,
|
|
@@ -163,11 +177,9 @@ import {
|
|
|
163
177
|
resolveContextWindowPolicy
|
|
164
178
|
} from "@wrongstack/core";
|
|
165
179
|
import { ToolExecutor } from "@wrongstack/core/execution";
|
|
166
|
-
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
167
180
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
|
|
168
181
|
import { builtinToolsPack, forgetTool, rememberTool } from "@wrongstack/tools";
|
|
169
|
-
import {
|
|
170
|
-
import { randomBytes } from "crypto";
|
|
182
|
+
import { WebSocketServer } from "ws";
|
|
171
183
|
|
|
172
184
|
// ../runtime/src/container.ts
|
|
173
185
|
import {
|
|
@@ -227,6 +239,7 @@ function createDefaultContainer(opts) {
|
|
|
227
239
|
trustFile: wpaths.projectTrust,
|
|
228
240
|
yolo: opts.permission?.yolo ?? false,
|
|
229
241
|
yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
|
|
242
|
+
confirmDestructive: opts.permission?.confirmDestructive ?? false,
|
|
230
243
|
promptDelegate: opts.permission?.promptDelegate
|
|
231
244
|
})
|
|
232
245
|
);
|
|
@@ -1446,6 +1459,13 @@ function createShutdown(res) {
|
|
|
1446
1459
|
}
|
|
1447
1460
|
for (const ws of res.clients()) ws.close();
|
|
1448
1461
|
for (const server of res.servers) server?.close();
|
|
1462
|
+
if (res.onShutdown) {
|
|
1463
|
+
try {
|
|
1464
|
+
await res.onShutdown();
|
|
1465
|
+
} catch (e) {
|
|
1466
|
+
log(`[WebUI] Error during shutdown cleanup: ${e instanceof Error ? e.message : String(e)}`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1449
1469
|
exit(0);
|
|
1450
1470
|
};
|
|
1451
1471
|
}
|
|
@@ -1459,6 +1479,138 @@ function registerShutdownHandlers(res) {
|
|
|
1459
1479
|
};
|
|
1460
1480
|
}
|
|
1461
1481
|
|
|
1482
|
+
// src/server/instance-registry.ts
|
|
1483
|
+
import * as os from "os";
|
|
1484
|
+
import * as path2 from "path";
|
|
1485
|
+
import * as fs2 from "fs/promises";
|
|
1486
|
+
import { atomicWrite } from "@wrongstack/core";
|
|
1487
|
+
function defaultBaseDir() {
|
|
1488
|
+
return path2.join(os.homedir(), ".wrongstack");
|
|
1489
|
+
}
|
|
1490
|
+
function registryPath(baseDir = defaultBaseDir()) {
|
|
1491
|
+
return path2.join(baseDir, "webui-instances.json");
|
|
1492
|
+
}
|
|
1493
|
+
function isPidAlive(pid) {
|
|
1494
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1495
|
+
try {
|
|
1496
|
+
process.kill(pid, 0);
|
|
1497
|
+
return true;
|
|
1498
|
+
} catch (err) {
|
|
1499
|
+
return err.code !== "ESRCH";
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
async function load(file) {
|
|
1503
|
+
try {
|
|
1504
|
+
const raw = await fs2.readFile(file, "utf8");
|
|
1505
|
+
const parsed = JSON.parse(raw);
|
|
1506
|
+
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
1507
|
+
return parsed;
|
|
1508
|
+
}
|
|
1509
|
+
} catch {
|
|
1510
|
+
}
|
|
1511
|
+
return { version: 1, instances: [] };
|
|
1512
|
+
}
|
|
1513
|
+
async function save(file, instances) {
|
|
1514
|
+
await atomicWrite(file, `${JSON.stringify({ version: 1, instances }, null, 2)}
|
|
1515
|
+
`, {
|
|
1516
|
+
mode: 384
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
function prune(instances, excludePid) {
|
|
1520
|
+
return instances.filter((i) => i.pid !== excludePid && isPidAlive(i.pid));
|
|
1521
|
+
}
|
|
1522
|
+
async function registerInstance(record, baseDir = defaultBaseDir()) {
|
|
1523
|
+
const file = registryPath(baseDir);
|
|
1524
|
+
const data = await load(file);
|
|
1525
|
+
const instances = prune(data.instances, record.pid);
|
|
1526
|
+
instances.push(record);
|
|
1527
|
+
await save(file, instances);
|
|
1528
|
+
}
|
|
1529
|
+
async function unregisterInstance(pid, baseDir = defaultBaseDir()) {
|
|
1530
|
+
const file = registryPath(baseDir);
|
|
1531
|
+
const data = await load(file);
|
|
1532
|
+
const instances = prune(data.instances, pid);
|
|
1533
|
+
await save(file, instances);
|
|
1534
|
+
}
|
|
1535
|
+
async function listInstances(baseDir = defaultBaseDir()) {
|
|
1536
|
+
const file = registryPath(baseDir);
|
|
1537
|
+
const data = await load(file);
|
|
1538
|
+
const live = prune(data.instances);
|
|
1539
|
+
if (live.length !== data.instances.length) {
|
|
1540
|
+
await save(file, live).catch(() => {
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
return live;
|
|
1544
|
+
}
|
|
1545
|
+
function formatInstances(instances) {
|
|
1546
|
+
if (instances.length === 0) {
|
|
1547
|
+
return "No WebUI instances are currently running.";
|
|
1548
|
+
}
|
|
1549
|
+
const lines = [`Running WebUI instances (${instances.length}):`, ""];
|
|
1550
|
+
for (const i of instances) {
|
|
1551
|
+
lines.push(
|
|
1552
|
+
` \u2022 ${i.url} \xB7 ws:${i.wsPort} \xB7 pid ${i.pid}`,
|
|
1553
|
+
` project: ${i.projectName} (${i.projectRoot})`,
|
|
1554
|
+
` since: ${i.startedAt}`
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
return lines.join("\n");
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// src/server/port-utils.ts
|
|
1561
|
+
import * as net from "net";
|
|
1562
|
+
function isPortFree(host, port) {
|
|
1563
|
+
return new Promise((resolve3) => {
|
|
1564
|
+
const srv = net.createServer();
|
|
1565
|
+
srv.once("error", () => resolve3(false));
|
|
1566
|
+
srv.once("listening", () => {
|
|
1567
|
+
srv.close(() => resolve3(true));
|
|
1568
|
+
});
|
|
1569
|
+
try {
|
|
1570
|
+
srv.listen(port, host);
|
|
1571
|
+
} catch {
|
|
1572
|
+
resolve3(false);
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
async function findFreePort(host, startPort, opts = {}) {
|
|
1577
|
+
const exclude = opts.exclude ?? /* @__PURE__ */ new Set();
|
|
1578
|
+
const maxTries = opts.maxTries ?? 200;
|
|
1579
|
+
let port = startPort;
|
|
1580
|
+
for (let i = 0; i < maxTries; i++) {
|
|
1581
|
+
if (port > 65535) port = 1024 + port % 5e4;
|
|
1582
|
+
if (!exclude.has(port) && await isPortFree(host, port)) {
|
|
1583
|
+
return port;
|
|
1584
|
+
}
|
|
1585
|
+
port++;
|
|
1586
|
+
}
|
|
1587
|
+
throw new Error(
|
|
1588
|
+
`No free port found near ${startPort} on ${host} after ${maxTries} attempts.`
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/server/open-browser.ts
|
|
1593
|
+
import { spawn } from "child_process";
|
|
1594
|
+
function browserOpenCommand(url, platform = process.platform) {
|
|
1595
|
+
if (platform === "win32") {
|
|
1596
|
+
return { command: "cmd", args: ["/c", "start", "", url] };
|
|
1597
|
+
}
|
|
1598
|
+
if (platform === "darwin") {
|
|
1599
|
+
return { command: "open", args: [url] };
|
|
1600
|
+
}
|
|
1601
|
+
return { command: "xdg-open", args: [url] };
|
|
1602
|
+
}
|
|
1603
|
+
function openBrowser(url, platform = process.platform) {
|
|
1604
|
+
try {
|
|
1605
|
+
const { command, args } = browserOpenCommand(url, platform);
|
|
1606
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
1607
|
+
child.on("error", () => {
|
|
1608
|
+
});
|
|
1609
|
+
child.unref();
|
|
1610
|
+
} catch {
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1462
1614
|
// src/server/usage-cost.ts
|
|
1463
1615
|
function getCostRates(model) {
|
|
1464
1616
|
const cost = model?.cost;
|
|
@@ -1472,6 +1624,68 @@ function computeUsageCost(usage, rates) {
|
|
|
1472
1624
|
return (usage.input * rates.input + usage.output * rates.output + (usage.cacheRead ?? 0) * rates.cacheRead) / 1e6;
|
|
1473
1625
|
}
|
|
1474
1626
|
|
|
1627
|
+
// src/server/provider-config-io.ts
|
|
1628
|
+
import * as fs3 from "fs/promises";
|
|
1629
|
+
import * as path3 from "path";
|
|
1630
|
+
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
1631
|
+
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
1632
|
+
import { DefaultSecretVault } from "@wrongstack/core";
|
|
1633
|
+
async function loadSavedProviders(configPath, vault) {
|
|
1634
|
+
let raw;
|
|
1635
|
+
try {
|
|
1636
|
+
raw = await fs3.readFile(configPath, "utf8");
|
|
1637
|
+
} catch {
|
|
1638
|
+
return {};
|
|
1639
|
+
}
|
|
1640
|
+
let parsed = {};
|
|
1641
|
+
try {
|
|
1642
|
+
parsed = JSON.parse(raw);
|
|
1643
|
+
} catch {
|
|
1644
|
+
return {};
|
|
1645
|
+
}
|
|
1646
|
+
if (!parsed.providers) return {};
|
|
1647
|
+
return decryptConfigSecrets(parsed.providers, vault);
|
|
1648
|
+
}
|
|
1649
|
+
async function saveProviders(configPath, vault, providers) {
|
|
1650
|
+
let raw;
|
|
1651
|
+
let fileExists = true;
|
|
1652
|
+
try {
|
|
1653
|
+
raw = await fs3.readFile(configPath, "utf8");
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
if (err.code !== "ENOENT") {
|
|
1656
|
+
throw new Error(
|
|
1657
|
+
`Refusing to mutate ${configPath}: ${err.message}`,
|
|
1658
|
+
{ cause: err }
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
fileExists = false;
|
|
1662
|
+
raw = "{}";
|
|
1663
|
+
}
|
|
1664
|
+
let parsed;
|
|
1665
|
+
try {
|
|
1666
|
+
parsed = JSON.parse(raw);
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
if (fileExists) {
|
|
1669
|
+
throw new Error(
|
|
1670
|
+
`Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
|
|
1671
|
+
{ cause: err }
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
parsed = {};
|
|
1675
|
+
}
|
|
1676
|
+
parsed.providers = providers;
|
|
1677
|
+
const encrypted = encryptConfigSecrets(parsed, vault);
|
|
1678
|
+
await atomicWrite2(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
1679
|
+
}
|
|
1680
|
+
function createProviderConfigIO(configPath) {
|
|
1681
|
+
const keyFile = path3.join(path3.dirname(configPath), ".key");
|
|
1682
|
+
const vault = new DefaultSecretVault({ keyFile });
|
|
1683
|
+
return {
|
|
1684
|
+
load: () => loadSavedProviders(configPath, vault),
|
|
1685
|
+
save: (providers) => saveProviders(configPath, vault, providers)
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1475
1689
|
// src/server/provider-keys.ts
|
|
1476
1690
|
function normalizeKeys(cfg) {
|
|
1477
1691
|
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
@@ -1567,6 +1781,159 @@ function removeProvider(providers, providerId) {
|
|
|
1567
1781
|
return { ok: true, message: `Provider "${providerId}" removed` };
|
|
1568
1782
|
}
|
|
1569
1783
|
|
|
1784
|
+
// src/server/ws-utils.ts
|
|
1785
|
+
import { randomBytes } from "crypto";
|
|
1786
|
+
import { WebSocket } from "ws";
|
|
1787
|
+
function send(ws, msg) {
|
|
1788
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1789
|
+
ws.send(JSON.stringify(msg));
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function broadcast(clients, msg) {
|
|
1793
|
+
const data = JSON.stringify(msg);
|
|
1794
|
+
for (const [ws] of clients) {
|
|
1795
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1796
|
+
try {
|
|
1797
|
+
ws.send(data);
|
|
1798
|
+
} catch {
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function sendResult(ws, success, message) {
|
|
1804
|
+
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
1805
|
+
}
|
|
1806
|
+
function errMessage(err) {
|
|
1807
|
+
return err instanceof Error ? err.message : String(err);
|
|
1808
|
+
}
|
|
1809
|
+
function generateAuthToken() {
|
|
1810
|
+
return randomBytes(16).toString("hex");
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/server/provider-handlers.ts
|
|
1814
|
+
function createProviderHandlers(deps) {
|
|
1815
|
+
const { globalConfigPath, vault } = deps;
|
|
1816
|
+
let configWriteLock = deps.getConfigWriteLock();
|
|
1817
|
+
async function loadConfigProviders() {
|
|
1818
|
+
return loadSavedProviders(globalConfigPath, vault);
|
|
1819
|
+
}
|
|
1820
|
+
async function saveConfigProviders(providers) {
|
|
1821
|
+
const next = configWriteLock.then(() => saveProviders(globalConfigPath, vault, providers));
|
|
1822
|
+
configWriteLock = next;
|
|
1823
|
+
deps.setConfigWriteLock(next);
|
|
1824
|
+
await next;
|
|
1825
|
+
}
|
|
1826
|
+
async function handleKeyUpsert(ws, providerId, label, apiKey) {
|
|
1827
|
+
try {
|
|
1828
|
+
const providers = await loadConfigProviders();
|
|
1829
|
+
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
1830
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
1831
|
+
sendResult(ws, result.ok, result.message);
|
|
1832
|
+
} catch (err) {
|
|
1833
|
+
sendResult(ws, false, errMessage(err));
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
async function handleKeyDelete(ws, providerId, label) {
|
|
1837
|
+
try {
|
|
1838
|
+
const providers = await loadConfigProviders();
|
|
1839
|
+
const result = deleteKey(providers, providerId, label);
|
|
1840
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
1841
|
+
sendResult(ws, result.ok, result.message);
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
sendResult(ws, false, errMessage(err));
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
async function handleKeySetActive(ws, providerId, label) {
|
|
1847
|
+
try {
|
|
1848
|
+
const providers = await loadConfigProviders();
|
|
1849
|
+
const result = setActiveKey(providers, providerId, label);
|
|
1850
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
1851
|
+
sendResult(ws, result.ok, result.message);
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
sendResult(ws, false, errMessage(err));
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
async function handleProviderAdd(ws, payload) {
|
|
1857
|
+
try {
|
|
1858
|
+
const providers = await loadConfigProviders();
|
|
1859
|
+
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
1860
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
1861
|
+
sendResult(ws, result.ok, result.message);
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
sendResult(ws, false, errMessage(err));
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
async function handleProviderRemove(ws, providerId) {
|
|
1867
|
+
try {
|
|
1868
|
+
const providers = await loadConfigProviders();
|
|
1869
|
+
const result = removeProvider(providers, providerId);
|
|
1870
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
1871
|
+
sendResult(ws, result.ok, result.message);
|
|
1872
|
+
} catch (err) {
|
|
1873
|
+
sendResult(ws, false, errMessage(err));
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
return { handleKeyUpsert, handleKeyDelete, handleKeySetActive, handleProviderAdd, handleProviderRemove, loadConfigProviders };
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// src/server/setup-events.ts
|
|
1880
|
+
function setupEvents(deps) {
|
|
1881
|
+
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms } = deps;
|
|
1882
|
+
events.on("iteration.started", (e) => {
|
|
1883
|
+
broadcast2(clients, {
|
|
1884
|
+
type: "iteration.started",
|
|
1885
|
+
payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
|
|
1886
|
+
});
|
|
1887
|
+
});
|
|
1888
|
+
events.on("provider.text_delta", (e) => {
|
|
1889
|
+
broadcast2(clients, { type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
|
|
1890
|
+
});
|
|
1891
|
+
events.on("provider.thinking_delta", (e) => {
|
|
1892
|
+
broadcast2(clients, { type: "provider.thinking_delta", payload: { text: e.text } });
|
|
1893
|
+
});
|
|
1894
|
+
events.on("tool.started", (e) => {
|
|
1895
|
+
broadcast2(clients, {
|
|
1896
|
+
type: "tool.started",
|
|
1897
|
+
payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
|
|
1898
|
+
});
|
|
1899
|
+
});
|
|
1900
|
+
events.on("tool.progress", (e) => {
|
|
1901
|
+
broadcast2(clients, {
|
|
1902
|
+
type: "tool.progress",
|
|
1903
|
+
payload: { id: e.id, name: e.name, eventType: e.event.type, text: e.event.text }
|
|
1904
|
+
});
|
|
1905
|
+
});
|
|
1906
|
+
events.on("tool.executed", (e) => {
|
|
1907
|
+
broadcast2(clients, {
|
|
1908
|
+
type: "tool.executed",
|
|
1909
|
+
payload: { id: e.id, name: e.name, durationMs: e.durationMs, ok: e.ok, input: e.input, output: e.output }
|
|
1910
|
+
});
|
|
1911
|
+
broadcast2(clients, { type: "todos.updated", payload: { todos: [...context.todos] } });
|
|
1912
|
+
});
|
|
1913
|
+
events.on("provider.response", (e) => {
|
|
1914
|
+
broadcast2(clients, { type: "provider.response", payload: { usage: e.usage, stopReason: e.stopReason, messageId: "current" } });
|
|
1915
|
+
});
|
|
1916
|
+
events.on("context.repaired", (e) => {
|
|
1917
|
+
broadcast2(clients, { type: "context.repaired", payload: { removedToolUses: e.removedToolUses, removedToolResults: e.removedToolResults, removedMessages: e.removedMessages } });
|
|
1918
|
+
});
|
|
1919
|
+
events.on("tool.confirm_needed", (e) => {
|
|
1920
|
+
const id = e.toolUseId ?? `confirm_${Date.now()}`;
|
|
1921
|
+
pendingConfirms.set(id, e.resolve);
|
|
1922
|
+
broadcast2(clients, { type: "tool.confirm_needed", payload: { id, toolName: e.tool?.name ?? "unknown", input: e.input, suggestedPattern: e.suggestedPattern } });
|
|
1923
|
+
});
|
|
1924
|
+
events.on("error", (e) => {
|
|
1925
|
+
broadcast2(clients, { type: "error", payload: { phase: e.phase, message: e.err instanceof Error ? e.err.message : String(e.err) } });
|
|
1926
|
+
});
|
|
1927
|
+
const forwardSubagent = (kind, payload) => broadcast2(clients, { type: "subagent.event", payload: { kind, ...payload } });
|
|
1928
|
+
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 }));
|
|
1929
|
+
events.on("subagent.task_started", (e) => forwardSubagent("task_started", { subagentId: e.subagentId, taskId: e.taskId, description: e.description }));
|
|
1930
|
+
events.on("subagent.tool_executed", (e) => forwardSubagent("tool_executed", { subagentId: e.subagentId, toolName: e.name, durationMs: e.durationMs, ok: e.ok }));
|
|
1931
|
+
events.on("subagent.iteration_summary", (e) => forwardSubagent("iteration_summary", { subagentId: e.subagentId, iteration: e.iteration, toolCalls: e.toolCalls, costUsd: e.costUsd, currentTool: e.currentTool }));
|
|
1932
|
+
events.on("subagent.budget_extended", (e) => forwardSubagent("budget_extended", { subagentId: e.subagentId, totalExtensions: e.totalExtensions }));
|
|
1933
|
+
events.on("subagent.ctx_pct", (e) => forwardSubagent("ctx_pct", { subagentId: e.subagentId, load: e.load, tokens: e.tokens, maxContext: e.maxContext }));
|
|
1934
|
+
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 }));
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1570
1937
|
// src/server/token-estimator.ts
|
|
1571
1938
|
function estimateTokens(s) {
|
|
1572
1939
|
return Math.ceil(s.length / 4);
|
|
@@ -1625,12 +1992,23 @@ function estimateContextBreakdown(input) {
|
|
|
1625
1992
|
}
|
|
1626
1993
|
|
|
1627
1994
|
// src/server/index.ts
|
|
1628
|
-
function errMessage(err) {
|
|
1629
|
-
return err instanceof Error ? err.message : String(err);
|
|
1630
|
-
}
|
|
1631
1995
|
async function startWebUI(opts = {}) {
|
|
1632
|
-
const
|
|
1996
|
+
const requestedWsPort = opts.wsPort ?? 3457;
|
|
1633
1997
|
const wsHost = opts.wsHost ?? "127.0.0.1";
|
|
1998
|
+
const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
1999
|
+
const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
|
|
2000
|
+
let wsPort = requestedWsPort;
|
|
2001
|
+
let httpPort = requestedHttpPort;
|
|
2002
|
+
if (!strictPort) {
|
|
2003
|
+
httpPort = await findFreePort(wsHost, requestedHttpPort);
|
|
2004
|
+
wsPort = await findFreePort(wsHost, requestedWsPort, { exclude: /* @__PURE__ */ new Set([httpPort]) });
|
|
2005
|
+
if (httpPort !== requestedHttpPort) {
|
|
2006
|
+
console.warn(`[WebUI] HTTP port ${requestedHttpPort} in use \u2192 using ${httpPort}`);
|
|
2007
|
+
}
|
|
2008
|
+
if (wsPort !== requestedWsPort) {
|
|
2009
|
+
console.warn(`[WebUI] WS port ${requestedWsPort} in use \u2192 using ${wsPort}`);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1634
2012
|
console.log("[WebUI] Starting backend services...");
|
|
1635
2013
|
const boot = await bootConfig();
|
|
1636
2014
|
const { config: baseConfig, vault, globalConfigPath, projectRoot, wpaths, logger } = boot;
|
|
@@ -1884,14 +2262,14 @@ async function startWebUI(opts = {}) {
|
|
|
1884
2262
|
inputCost,
|
|
1885
2263
|
outputCost,
|
|
1886
2264
|
cacheReadCost,
|
|
1887
|
-
projectName:
|
|
2265
|
+
projectName: path4.basename(projectRoot) || projectRoot,
|
|
1888
2266
|
cwd: projectRoot,
|
|
1889
2267
|
mode: modeId,
|
|
1890
2268
|
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
|
|
1891
2269
|
wsToken
|
|
1892
2270
|
};
|
|
1893
2271
|
}
|
|
1894
|
-
const wsToken =
|
|
2272
|
+
const wsToken = generateAuthToken();
|
|
1895
2273
|
console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
|
|
1896
2274
|
const verifyClient2 = (info) => verifyClient({
|
|
1897
2275
|
origin: info.origin,
|
|
@@ -1915,10 +2293,11 @@ async function startWebUI(opts = {}) {
|
|
|
1915
2293
|
maxPayload: WS_MAX_PAYLOAD
|
|
1916
2294
|
}) : null;
|
|
1917
2295
|
const clients = /* @__PURE__ */ new Map();
|
|
1918
|
-
const RATE_LIMIT_MESSAGES =
|
|
2296
|
+
const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
|
|
1919
2297
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1920
2298
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
1921
2299
|
function checkRateLimit(ws, client) {
|
|
2300
|
+
if (RATE_LIMIT_MESSAGES <= 0) return true;
|
|
1922
2301
|
const now = Date.now();
|
|
1923
2302
|
const key = client.sessionId ?? String(ws);
|
|
1924
2303
|
const limit = rateLimits.get(key);
|
|
@@ -1935,113 +2314,6 @@ async function startWebUI(opts = {}) {
|
|
|
1935
2314
|
`[WebUI] WebSocket server running on ws://${wsHost}:${wsPort}` + (wssSecondary ? ` (and ws://[::1]:${wsPort})` : "")
|
|
1936
2315
|
);
|
|
1937
2316
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
1938
|
-
function setupEvents() {
|
|
1939
|
-
events.on("iteration.started", (e) => {
|
|
1940
|
-
broadcast({
|
|
1941
|
-
type: "iteration.started",
|
|
1942
|
-
payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
|
|
1943
|
-
});
|
|
1944
|
-
});
|
|
1945
|
-
events.on("provider.text_delta", (e) => {
|
|
1946
|
-
broadcast({ type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
|
|
1947
|
-
});
|
|
1948
|
-
events.on("provider.thinking_delta", (e) => {
|
|
1949
|
-
broadcast({ type: "provider.thinking_delta", payload: { text: e.text } });
|
|
1950
|
-
});
|
|
1951
|
-
events.on("tool.started", (e) => {
|
|
1952
|
-
broadcast({
|
|
1953
|
-
type: "tool.started",
|
|
1954
|
-
payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
|
|
1955
|
-
});
|
|
1956
|
-
});
|
|
1957
|
-
events.on("tool.progress", (e) => {
|
|
1958
|
-
broadcast({
|
|
1959
|
-
type: "tool.progress",
|
|
1960
|
-
payload: {
|
|
1961
|
-
id: e.id,
|
|
1962
|
-
name: e.name,
|
|
1963
|
-
eventType: e.event.type,
|
|
1964
|
-
text: e.event.text
|
|
1965
|
-
}
|
|
1966
|
-
});
|
|
1967
|
-
});
|
|
1968
|
-
events.on("tool.executed", (e) => {
|
|
1969
|
-
broadcast({
|
|
1970
|
-
type: "tool.executed",
|
|
1971
|
-
payload: {
|
|
1972
|
-
// Forward the tool_use id so frontend can correlate with the
|
|
1973
|
-
// matching tool.started bubble — without this, parallel tool calls
|
|
1974
|
-
// all stay stuck on "Running…" because the frontend can't tell
|
|
1975
|
-
// which bubble this result belongs to.
|
|
1976
|
-
id: e.id,
|
|
1977
|
-
name: e.name,
|
|
1978
|
-
durationMs: e.durationMs,
|
|
1979
|
-
ok: e.ok,
|
|
1980
|
-
input: e.input,
|
|
1981
|
-
output: e.output
|
|
1982
|
-
}
|
|
1983
|
-
});
|
|
1984
|
-
broadcast({
|
|
1985
|
-
type: "todos.updated",
|
|
1986
|
-
payload: { todos: [...context.todos] }
|
|
1987
|
-
});
|
|
1988
|
-
});
|
|
1989
|
-
events.on("provider.response", (e) => {
|
|
1990
|
-
broadcast({
|
|
1991
|
-
type: "provider.response",
|
|
1992
|
-
payload: {
|
|
1993
|
-
usage: e.usage,
|
|
1994
|
-
stopReason: e.stopReason,
|
|
1995
|
-
messageId: "current"
|
|
1996
|
-
}
|
|
1997
|
-
});
|
|
1998
|
-
});
|
|
1999
|
-
events.on("context.repaired", (e) => {
|
|
2000
|
-
broadcast({
|
|
2001
|
-
type: "context.repaired",
|
|
2002
|
-
payload: {
|
|
2003
|
-
removedToolUses: e.removedToolUses,
|
|
2004
|
-
removedToolResults: e.removedToolResults,
|
|
2005
|
-
removedMessages: e.removedMessages
|
|
2006
|
-
}
|
|
2007
|
-
});
|
|
2008
|
-
});
|
|
2009
|
-
events.on("tool.confirm_needed", (e) => {
|
|
2010
|
-
const id = e.toolUseId ?? `confirm_${Date.now()}`;
|
|
2011
|
-
pendingConfirms.set(id, e.resolve);
|
|
2012
|
-
broadcast({
|
|
2013
|
-
type: "tool.confirm_needed",
|
|
2014
|
-
payload: {
|
|
2015
|
-
id,
|
|
2016
|
-
toolName: e.tool?.name ?? "unknown",
|
|
2017
|
-
input: e.input,
|
|
2018
|
-
suggestedPattern: e.suggestedPattern
|
|
2019
|
-
}
|
|
2020
|
-
});
|
|
2021
|
-
});
|
|
2022
|
-
events.on("error", (e) => {
|
|
2023
|
-
broadcast({
|
|
2024
|
-
type: "error",
|
|
2025
|
-
payload: {
|
|
2026
|
-
phase: e.phase,
|
|
2027
|
-
message: e.err instanceof Error ? e.err.message : String(e.err)
|
|
2028
|
-
}
|
|
2029
|
-
});
|
|
2030
|
-
});
|
|
2031
|
-
}
|
|
2032
|
-
function send(ws, msg) {
|
|
2033
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
2034
|
-
ws.send(JSON.stringify(msg));
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
function broadcast(msg) {
|
|
2038
|
-
const data = JSON.stringify(msg);
|
|
2039
|
-
for (const [ws] of clients) {
|
|
2040
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
2041
|
-
ws.send(data);
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
2317
|
const handleConnection = (ws) => {
|
|
2046
2318
|
const client = { ws, sessionId: session.id, connectedAt: Date.now() };
|
|
2047
2319
|
clients.set(ws, client);
|
|
@@ -2099,7 +2371,7 @@ async function startWebUI(opts = {}) {
|
|
|
2099
2371
|
if (eventsArmed) return;
|
|
2100
2372
|
eventsArmed = true;
|
|
2101
2373
|
console.log(`[WebUI] Backend ready (${label})`);
|
|
2102
|
-
setupEvents();
|
|
2374
|
+
setupEvents({ events, broadcast, clients, config, context, pendingConfirms });
|
|
2103
2375
|
};
|
|
2104
2376
|
wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
|
|
2105
2377
|
wssPrimary.on("connection", handleConnection);
|
|
@@ -2184,7 +2456,7 @@ async function startWebUI(opts = {}) {
|
|
|
2184
2456
|
}
|
|
2185
2457
|
case "abort":
|
|
2186
2458
|
runLock?.abort();
|
|
2187
|
-
broadcast({ type: "error", payload: { phase: "abort", message: "User aborted" } });
|
|
2459
|
+
broadcast(clients, { type: "error", payload: { phase: "abort", message: "User aborted" } });
|
|
2188
2460
|
break;
|
|
2189
2461
|
case "ping":
|
|
2190
2462
|
send(ws, { type: "pong", payload: {} });
|
|
@@ -2203,7 +2475,7 @@ async function startWebUI(opts = {}) {
|
|
|
2203
2475
|
context.fileMtimes.clear();
|
|
2204
2476
|
tokenCounter.reset();
|
|
2205
2477
|
sessionStartedAt = Date.now();
|
|
2206
|
-
broadcast({ type: "session.start", payload: await sessionStartPayload() });
|
|
2478
|
+
broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
|
|
2207
2479
|
break;
|
|
2208
2480
|
}
|
|
2209
2481
|
case "context.clear": {
|
|
@@ -2213,7 +2485,7 @@ async function startWebUI(opts = {}) {
|
|
|
2213
2485
|
context.fileMtimes.clear();
|
|
2214
2486
|
tokenCounter.reset();
|
|
2215
2487
|
sendResult(ws, true, "Context cleared");
|
|
2216
|
-
broadcast({
|
|
2488
|
+
broadcast(clients, {
|
|
2217
2489
|
type: "session.start",
|
|
2218
2490
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
2219
2491
|
});
|
|
@@ -2272,7 +2544,7 @@ async function startWebUI(opts = {}) {
|
|
|
2272
2544
|
beforeMessages,
|
|
2273
2545
|
afterMessages: context.messages.length
|
|
2274
2546
|
};
|
|
2275
|
-
broadcast({ type: "context.repaired", payload });
|
|
2547
|
+
broadcast(clients, { type: "context.repaired", payload });
|
|
2276
2548
|
const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
|
|
2277
2549
|
sendResult(
|
|
2278
2550
|
ws,
|
|
@@ -2310,7 +2582,7 @@ async function startWebUI(opts = {}) {
|
|
|
2310
2582
|
context.meta["contextWindowMode"] = policy.id;
|
|
2311
2583
|
context.meta["contextWindowPolicy"] = policy;
|
|
2312
2584
|
sendResult(ws, true, `Context mode switched to ${policy.id}`);
|
|
2313
|
-
broadcast({
|
|
2585
|
+
broadcast(clients, {
|
|
2314
2586
|
type: "context.mode.changed",
|
|
2315
2587
|
payload: { id: policy.id, name: policy.name, policy }
|
|
2316
2588
|
});
|
|
@@ -2336,7 +2608,7 @@ async function startWebUI(opts = {}) {
|
|
|
2336
2608
|
break;
|
|
2337
2609
|
}
|
|
2338
2610
|
case "providers.saved": {
|
|
2339
|
-
const saved = await
|
|
2611
|
+
const saved = await providerHandlers.loadConfigProviders();
|
|
2340
2612
|
send(ws, {
|
|
2341
2613
|
type: "providers.saved",
|
|
2342
2614
|
payload: {
|
|
@@ -2395,11 +2667,11 @@ async function startWebUI(opts = {}) {
|
|
|
2395
2667
|
updateAutoCompactionMaxContext?.(newProv);
|
|
2396
2668
|
try {
|
|
2397
2669
|
configWriteLock = configWriteLock.then(async () => {
|
|
2398
|
-
const raw = await
|
|
2670
|
+
const raw = await fs4.readFile(globalConfigPath, "utf8");
|
|
2399
2671
|
const parsed = JSON.parse(raw);
|
|
2400
2672
|
parsed.provider = newProvider;
|
|
2401
2673
|
parsed.model = newModel;
|
|
2402
|
-
await
|
|
2674
|
+
await atomicWrite3(globalConfigPath, JSON.stringify(parsed, null, 2));
|
|
2403
2675
|
});
|
|
2404
2676
|
await configWriteLock;
|
|
2405
2677
|
} catch (err) {
|
|
@@ -2419,33 +2691,33 @@ async function startWebUI(opts = {}) {
|
|
|
2419
2691
|
});
|
|
2420
2692
|
break;
|
|
2421
2693
|
}
|
|
2422
|
-
broadcast({ type: "session.start", payload: await sessionStartPayload() });
|
|
2694
|
+
broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
|
|
2423
2695
|
break;
|
|
2424
2696
|
}
|
|
2425
2697
|
case "key.add":
|
|
2426
2698
|
case "key.update": {
|
|
2427
2699
|
const { providerId, label, apiKey } = msg.payload;
|
|
2428
|
-
await handleKeyUpsert(ws, providerId, label, apiKey);
|
|
2700
|
+
await providerHandlers.handleKeyUpsert(ws, providerId, label, apiKey);
|
|
2429
2701
|
break;
|
|
2430
2702
|
}
|
|
2431
2703
|
case "key.delete": {
|
|
2432
2704
|
const { providerId, label } = msg.payload;
|
|
2433
|
-
await handleKeyDelete(ws, providerId, label);
|
|
2705
|
+
await providerHandlers.handleKeyDelete(ws, providerId, label);
|
|
2434
2706
|
break;
|
|
2435
2707
|
}
|
|
2436
2708
|
case "key.set_active": {
|
|
2437
2709
|
const { providerId, label } = msg.payload;
|
|
2438
|
-
await handleKeySetActive(ws, providerId, label);
|
|
2710
|
+
await providerHandlers.handleKeySetActive(ws, providerId, label);
|
|
2439
2711
|
break;
|
|
2440
2712
|
}
|
|
2441
2713
|
case "provider.add": {
|
|
2442
2714
|
const p = msg.payload;
|
|
2443
|
-
await handleProviderAdd(ws, p);
|
|
2715
|
+
await providerHandlers.handleProviderAdd(ws, p);
|
|
2444
2716
|
break;
|
|
2445
2717
|
}
|
|
2446
2718
|
case "provider.remove": {
|
|
2447
2719
|
const { providerId } = msg.payload;
|
|
2448
|
-
await handleProviderRemove(ws, providerId);
|
|
2720
|
+
await providerHandlers.handleProviderRemove(ws, providerId);
|
|
2449
2721
|
break;
|
|
2450
2722
|
}
|
|
2451
2723
|
case "sessions.list": {
|
|
@@ -2508,7 +2780,7 @@ async function startWebUI(opts = {}) {
|
|
|
2508
2780
|
tokenCounter.reset();
|
|
2509
2781
|
tokenCounter.account(resumed.data.usage, config.model);
|
|
2510
2782
|
sessionStartedAt = Date.now();
|
|
2511
|
-
broadcast({
|
|
2783
|
+
broadcast(clients, {
|
|
2512
2784
|
type: "session.start",
|
|
2513
2785
|
payload: {
|
|
2514
2786
|
...await sessionStartPayload(),
|
|
@@ -2648,7 +2920,7 @@ async function startWebUI(opts = {}) {
|
|
|
2648
2920
|
case "todos.clear": {
|
|
2649
2921
|
context.state.replaceTodos([]);
|
|
2650
2922
|
sendResult(ws, true, "Todos cleared");
|
|
2651
|
-
broadcast({ type: "todos.updated", payload: { todos: [] } });
|
|
2923
|
+
broadcast(clients, { type: "todos.updated", payload: { todos: [] } });
|
|
2652
2924
|
break;
|
|
2653
2925
|
}
|
|
2654
2926
|
case "plan.get": {
|
|
@@ -2695,7 +2967,7 @@ async function startWebUI(opts = {}) {
|
|
|
2695
2967
|
}
|
|
2696
2968
|
await savePlan(planPath, plan);
|
|
2697
2969
|
sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
|
|
2698
|
-
broadcast({
|
|
2970
|
+
broadcast(clients, {
|
|
2699
2971
|
type: "plan.updated",
|
|
2700
2972
|
payload: { plan }
|
|
2701
2973
|
});
|
|
@@ -2712,7 +2984,7 @@ async function startWebUI(opts = {}) {
|
|
|
2712
2984
|
if (depth > 8 || results.length >= 600) return;
|
|
2713
2985
|
let entries = [];
|
|
2714
2986
|
try {
|
|
2715
|
-
entries = await
|
|
2987
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
2716
2988
|
} catch {
|
|
2717
2989
|
return;
|
|
2718
2990
|
}
|
|
@@ -2722,7 +2994,7 @@ async function startWebUI(opts = {}) {
|
|
|
2722
2994
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
2723
2995
|
if (e.isDirectory()) {
|
|
2724
2996
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
2725
|
-
await walk(
|
|
2997
|
+
await walk(path4.join(dir, e.name), childRel, depth + 1);
|
|
2726
2998
|
} else if (e.isFile()) {
|
|
2727
2999
|
results.push(childRel);
|
|
2728
3000
|
}
|
|
@@ -2791,7 +3063,7 @@ async function startWebUI(opts = {}) {
|
|
|
2791
3063
|
model: config.model
|
|
2792
3064
|
});
|
|
2793
3065
|
sendResult(ws, true, `Switched to mode "${id}"`);
|
|
2794
|
-
broadcast({
|
|
3066
|
+
broadcast(clients, {
|
|
2795
3067
|
type: "session.start",
|
|
2796
3068
|
payload: { ...await sessionStartPayload() }
|
|
2797
3069
|
});
|
|
@@ -2830,92 +3102,37 @@ async function startWebUI(opts = {}) {
|
|
|
2830
3102
|
}
|
|
2831
3103
|
}
|
|
2832
3104
|
}
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
} catch {
|
|
2840
|
-
return {};
|
|
2841
|
-
}
|
|
2842
|
-
}
|
|
2843
|
-
async function saveProviders(providers) {
|
|
2844
|
-
configWriteLock = configWriteLock.then(async () => {
|
|
2845
|
-
let parsed;
|
|
2846
|
-
try {
|
|
2847
|
-
const raw = await fs2.readFile(globalConfigPath, "utf8");
|
|
2848
|
-
parsed = JSON.parse(raw);
|
|
2849
|
-
} catch {
|
|
2850
|
-
parsed = {};
|
|
2851
|
-
}
|
|
2852
|
-
parsed["providers"] = providers;
|
|
2853
|
-
const encrypted = encryptConfigSecrets(parsed, vault);
|
|
2854
|
-
await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
2855
|
-
});
|
|
2856
|
-
await configWriteLock;
|
|
2857
|
-
}
|
|
2858
|
-
function sendResult(ws, success, message) {
|
|
2859
|
-
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
2860
|
-
}
|
|
2861
|
-
async function handleKeyUpsert(ws, providerId, label, apiKey) {
|
|
2862
|
-
try {
|
|
2863
|
-
const providers = await loadSavedProviders();
|
|
2864
|
-
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
2865
|
-
if (result.ok) await saveProviders(providers);
|
|
2866
|
-
sendResult(ws, result.ok, result.message);
|
|
2867
|
-
} catch (err) {
|
|
2868
|
-
sendResult(ws, false, errMessage(err));
|
|
3105
|
+
const providerHandlers = createProviderHandlers({
|
|
3106
|
+
globalConfigPath,
|
|
3107
|
+
vault,
|
|
3108
|
+
getConfigWriteLock: () => configWriteLock,
|
|
3109
|
+
setConfigWriteLock: (p) => {
|
|
3110
|
+
configWriteLock = p;
|
|
2869
3111
|
}
|
|
2870
|
-
}
|
|
2871
|
-
async function handleKeyDelete(ws, providerId, label) {
|
|
2872
|
-
try {
|
|
2873
|
-
const providers = await loadSavedProviders();
|
|
2874
|
-
const result = deleteKey(providers, providerId, label);
|
|
2875
|
-
if (result.ok) await saveProviders(providers);
|
|
2876
|
-
sendResult(ws, result.ok, result.message);
|
|
2877
|
-
} catch (err) {
|
|
2878
|
-
sendResult(ws, false, errMessage(err));
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
async function handleKeySetActive(ws, providerId, label) {
|
|
2882
|
-
try {
|
|
2883
|
-
const providers = await loadSavedProviders();
|
|
2884
|
-
const result = setActiveKey(providers, providerId, label);
|
|
2885
|
-
if (result.ok) await saveProviders(providers);
|
|
2886
|
-
sendResult(ws, result.ok, result.message);
|
|
2887
|
-
} catch (err) {
|
|
2888
|
-
sendResult(ws, false, errMessage(err));
|
|
2889
|
-
}
|
|
2890
|
-
}
|
|
2891
|
-
async function handleProviderAdd(ws, payload) {
|
|
2892
|
-
try {
|
|
2893
|
-
const providers = await loadSavedProviders();
|
|
2894
|
-
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
2895
|
-
if (result.ok) await saveProviders(providers);
|
|
2896
|
-
sendResult(ws, result.ok, result.message);
|
|
2897
|
-
} catch (err) {
|
|
2898
|
-
sendResult(ws, false, errMessage(err));
|
|
2899
|
-
}
|
|
2900
|
-
}
|
|
2901
|
-
async function handleProviderRemove(ws, providerId) {
|
|
2902
|
-
try {
|
|
2903
|
-
const providers = await loadSavedProviders();
|
|
2904
|
-
const result = removeProvider(providers, providerId);
|
|
2905
|
-
if (result.ok) await saveProviders(providers);
|
|
2906
|
-
sendResult(ws, result.ok, result.message);
|
|
2907
|
-
} catch (err) {
|
|
2908
|
-
sendResult(ws, false, errMessage(err));
|
|
2909
|
-
}
|
|
2910
|
-
}
|
|
3112
|
+
});
|
|
2911
3113
|
const httpServer = createHttpServer({
|
|
2912
3114
|
host: wsHost,
|
|
2913
|
-
distDir:
|
|
3115
|
+
distDir: path4.resolve(import.meta.dirname, "../../dist"),
|
|
2914
3116
|
wsPort
|
|
2915
3117
|
});
|
|
2916
|
-
const
|
|
3118
|
+
const registryBaseDir = path4.dirname(globalConfigPath);
|
|
2917
3119
|
httpServer.listen(httpPort, wsHost, () => {
|
|
2918
|
-
|
|
3120
|
+
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
3121
|
+
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
3122
|
+
if (opts.open) openBrowser(openUrl);
|
|
3123
|
+
void registerInstance(
|
|
3124
|
+
{
|
|
3125
|
+
pid: process.pid,
|
|
3126
|
+
httpPort,
|
|
3127
|
+
wsPort,
|
|
3128
|
+
host: wsHost,
|
|
3129
|
+
projectRoot,
|
|
3130
|
+
projectName: path4.basename(projectRoot) || projectRoot,
|
|
3131
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3132
|
+
url: `http://${wsHost}:${httpPort}`
|
|
3133
|
+
},
|
|
3134
|
+
registryBaseDir
|
|
3135
|
+
).catch((err) => console.warn("[WebUI] Could not record instance:", errMessage(err)));
|
|
2919
3136
|
});
|
|
2920
3137
|
registerShutdownHandlers({
|
|
2921
3138
|
flushSession: async () => {
|
|
@@ -2927,10 +3144,48 @@ async function startWebUI(opts = {}) {
|
|
|
2927
3144
|
await session.close();
|
|
2928
3145
|
},
|
|
2929
3146
|
clients: () => clients.keys(),
|
|
2930
|
-
servers: [httpServer, wssPrimary, wssSecondary]
|
|
3147
|
+
servers: [httpServer, wssPrimary, wssSecondary],
|
|
3148
|
+
// Drop this instance from the registry on a clean exit so the file reflects
|
|
3149
|
+
// reality. Crash exits are healed by the next register()/list() prune pass.
|
|
3150
|
+
onShutdown: () => unregisterInstance(process.pid, registryBaseDir)
|
|
2931
3151
|
});
|
|
2932
3152
|
}
|
|
2933
3153
|
export {
|
|
2934
|
-
|
|
3154
|
+
addProvider,
|
|
3155
|
+
broadcast,
|
|
3156
|
+
browserOpenCommand,
|
|
3157
|
+
buildCspHeader,
|
|
3158
|
+
createHttpServer,
|
|
3159
|
+
createProviderConfigIO,
|
|
3160
|
+
defaultBaseDir,
|
|
3161
|
+
deleteKey,
|
|
3162
|
+
errMessage,
|
|
3163
|
+
extractToken,
|
|
3164
|
+
findFreePort,
|
|
3165
|
+
formatInstances,
|
|
3166
|
+
generateAuthToken,
|
|
3167
|
+
hostHeaderOk,
|
|
3168
|
+
injectWsPort,
|
|
3169
|
+
isLoopbackBind,
|
|
3170
|
+
isLoopbackHostname,
|
|
3171
|
+
isPortFree,
|
|
3172
|
+
listInstances,
|
|
3173
|
+
loadSavedProviders,
|
|
3174
|
+
maskedKey,
|
|
3175
|
+
normalizeKeys,
|
|
3176
|
+
openBrowser,
|
|
3177
|
+
registerInstance,
|
|
3178
|
+
registryPath,
|
|
3179
|
+
removeProvider,
|
|
3180
|
+
saveProviders,
|
|
3181
|
+
send,
|
|
3182
|
+
sendResult,
|
|
3183
|
+
setActiveKey,
|
|
3184
|
+
startWebUI,
|
|
3185
|
+
tokenMatches,
|
|
3186
|
+
unregisterInstance,
|
|
3187
|
+
upsertKey,
|
|
3188
|
+
verifyClient,
|
|
3189
|
+
writeKeysBack
|
|
2935
3190
|
};
|
|
2936
3191
|
//# sourceMappingURL=index.js.map
|