@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/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;
|
|
@@ -1567,6 +1719,97 @@ function removeProvider(providers, providerId) {
|
|
|
1567
1719
|
return { ok: true, message: `Provider "${providerId}" removed` };
|
|
1568
1720
|
}
|
|
1569
1721
|
|
|
1722
|
+
// src/server/provider-config-io.ts
|
|
1723
|
+
import * as fs3 from "fs/promises";
|
|
1724
|
+
import * as path3 from "path";
|
|
1725
|
+
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
1726
|
+
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
1727
|
+
import { DefaultSecretVault } from "@wrongstack/core";
|
|
1728
|
+
async function loadSavedProviders(configPath, vault) {
|
|
1729
|
+
let raw;
|
|
1730
|
+
try {
|
|
1731
|
+
raw = await fs3.readFile(configPath, "utf8");
|
|
1732
|
+
} catch {
|
|
1733
|
+
return {};
|
|
1734
|
+
}
|
|
1735
|
+
let parsed = {};
|
|
1736
|
+
try {
|
|
1737
|
+
parsed = JSON.parse(raw);
|
|
1738
|
+
} catch {
|
|
1739
|
+
return {};
|
|
1740
|
+
}
|
|
1741
|
+
if (!parsed.providers) return {};
|
|
1742
|
+
return decryptConfigSecrets(parsed.providers, vault);
|
|
1743
|
+
}
|
|
1744
|
+
async function saveProviders(configPath, vault, providers) {
|
|
1745
|
+
let raw;
|
|
1746
|
+
let fileExists = true;
|
|
1747
|
+
try {
|
|
1748
|
+
raw = await fs3.readFile(configPath, "utf8");
|
|
1749
|
+
} catch (err) {
|
|
1750
|
+
if (err.code !== "ENOENT") {
|
|
1751
|
+
throw new Error(
|
|
1752
|
+
`Refusing to mutate ${configPath}: ${err.message}`,
|
|
1753
|
+
{ cause: err }
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
fileExists = false;
|
|
1757
|
+
raw = "{}";
|
|
1758
|
+
}
|
|
1759
|
+
let parsed;
|
|
1760
|
+
try {
|
|
1761
|
+
parsed = JSON.parse(raw);
|
|
1762
|
+
} catch (err) {
|
|
1763
|
+
if (fileExists) {
|
|
1764
|
+
throw new Error(
|
|
1765
|
+
`Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
|
|
1766
|
+
{ cause: err }
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
parsed = {};
|
|
1770
|
+
}
|
|
1771
|
+
parsed.providers = providers;
|
|
1772
|
+
const encrypted = encryptConfigSecrets(parsed, vault);
|
|
1773
|
+
await atomicWrite2(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
1774
|
+
}
|
|
1775
|
+
function createProviderConfigIO(configPath) {
|
|
1776
|
+
const keyFile = path3.join(path3.dirname(configPath), ".key");
|
|
1777
|
+
const vault = new DefaultSecretVault({ keyFile });
|
|
1778
|
+
return {
|
|
1779
|
+
load: () => loadSavedProviders(configPath, vault),
|
|
1780
|
+
save: (providers) => saveProviders(configPath, vault, providers)
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
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
|
+
|
|
1570
1813
|
// src/server/token-estimator.ts
|
|
1571
1814
|
function estimateTokens(s) {
|
|
1572
1815
|
return Math.ceil(s.length / 4);
|
|
@@ -1625,12 +1868,23 @@ function estimateContextBreakdown(input) {
|
|
|
1625
1868
|
}
|
|
1626
1869
|
|
|
1627
1870
|
// src/server/index.ts
|
|
1628
|
-
function errMessage(err) {
|
|
1629
|
-
return err instanceof Error ? err.message : String(err);
|
|
1630
|
-
}
|
|
1631
1871
|
async function startWebUI(opts = {}) {
|
|
1632
|
-
const
|
|
1872
|
+
const requestedWsPort = opts.wsPort ?? 3457;
|
|
1633
1873
|
const wsHost = opts.wsHost ?? "127.0.0.1";
|
|
1874
|
+
const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
1875
|
+
const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
|
|
1876
|
+
let wsPort = requestedWsPort;
|
|
1877
|
+
let httpPort = requestedHttpPort;
|
|
1878
|
+
if (!strictPort) {
|
|
1879
|
+
httpPort = await findFreePort(wsHost, requestedHttpPort);
|
|
1880
|
+
wsPort = await findFreePort(wsHost, requestedWsPort, { exclude: /* @__PURE__ */ new Set([httpPort]) });
|
|
1881
|
+
if (httpPort !== requestedHttpPort) {
|
|
1882
|
+
console.warn(`[WebUI] HTTP port ${requestedHttpPort} in use \u2192 using ${httpPort}`);
|
|
1883
|
+
}
|
|
1884
|
+
if (wsPort !== requestedWsPort) {
|
|
1885
|
+
console.warn(`[WebUI] WS port ${requestedWsPort} in use \u2192 using ${wsPort}`);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1634
1888
|
console.log("[WebUI] Starting backend services...");
|
|
1635
1889
|
const boot = await bootConfig();
|
|
1636
1890
|
const { config: baseConfig, vault, globalConfigPath, projectRoot, wpaths, logger } = boot;
|
|
@@ -1884,14 +2138,14 @@ async function startWebUI(opts = {}) {
|
|
|
1884
2138
|
inputCost,
|
|
1885
2139
|
outputCost,
|
|
1886
2140
|
cacheReadCost,
|
|
1887
|
-
projectName:
|
|
2141
|
+
projectName: path4.basename(projectRoot) || projectRoot,
|
|
1888
2142
|
cwd: projectRoot,
|
|
1889
2143
|
mode: modeId,
|
|
1890
2144
|
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
|
|
1891
2145
|
wsToken
|
|
1892
2146
|
};
|
|
1893
2147
|
}
|
|
1894
|
-
const wsToken =
|
|
2148
|
+
const wsToken = generateAuthToken();
|
|
1895
2149
|
console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
|
|
1896
2150
|
const verifyClient2 = (info) => verifyClient({
|
|
1897
2151
|
origin: info.origin,
|
|
@@ -1915,10 +2169,11 @@ async function startWebUI(opts = {}) {
|
|
|
1915
2169
|
maxPayload: WS_MAX_PAYLOAD
|
|
1916
2170
|
}) : null;
|
|
1917
2171
|
const clients = /* @__PURE__ */ new Map();
|
|
1918
|
-
const RATE_LIMIT_MESSAGES =
|
|
2172
|
+
const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
|
|
1919
2173
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1920
2174
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
1921
2175
|
function checkRateLimit(ws, client) {
|
|
2176
|
+
if (RATE_LIMIT_MESSAGES <= 0) return true;
|
|
1922
2177
|
const now = Date.now();
|
|
1923
2178
|
const key = client.sessionId ?? String(ws);
|
|
1924
2179
|
const limit = rateLimits.get(key);
|
|
@@ -1937,25 +2192,25 @@ async function startWebUI(opts = {}) {
|
|
|
1937
2192
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
1938
2193
|
function setupEvents() {
|
|
1939
2194
|
events.on("iteration.started", (e) => {
|
|
1940
|
-
broadcast({
|
|
2195
|
+
broadcast(clients, {
|
|
1941
2196
|
type: "iteration.started",
|
|
1942
2197
|
payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
|
|
1943
2198
|
});
|
|
1944
2199
|
});
|
|
1945
2200
|
events.on("provider.text_delta", (e) => {
|
|
1946
|
-
broadcast({ type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
|
|
2201
|
+
broadcast(clients, { type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
|
|
1947
2202
|
});
|
|
1948
2203
|
events.on("provider.thinking_delta", (e) => {
|
|
1949
|
-
broadcast({ type: "provider.thinking_delta", payload: { text: e.text } });
|
|
2204
|
+
broadcast(clients, { type: "provider.thinking_delta", payload: { text: e.text } });
|
|
1950
2205
|
});
|
|
1951
2206
|
events.on("tool.started", (e) => {
|
|
1952
|
-
broadcast({
|
|
2207
|
+
broadcast(clients, {
|
|
1953
2208
|
type: "tool.started",
|
|
1954
2209
|
payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
|
|
1955
2210
|
});
|
|
1956
2211
|
});
|
|
1957
2212
|
events.on("tool.progress", (e) => {
|
|
1958
|
-
broadcast({
|
|
2213
|
+
broadcast(clients, {
|
|
1959
2214
|
type: "tool.progress",
|
|
1960
2215
|
payload: {
|
|
1961
2216
|
id: e.id,
|
|
@@ -1966,7 +2221,7 @@ async function startWebUI(opts = {}) {
|
|
|
1966
2221
|
});
|
|
1967
2222
|
});
|
|
1968
2223
|
events.on("tool.executed", (e) => {
|
|
1969
|
-
broadcast({
|
|
2224
|
+
broadcast(clients, {
|
|
1970
2225
|
type: "tool.executed",
|
|
1971
2226
|
payload: {
|
|
1972
2227
|
// Forward the tool_use id so frontend can correlate with the
|
|
@@ -1981,13 +2236,13 @@ async function startWebUI(opts = {}) {
|
|
|
1981
2236
|
output: e.output
|
|
1982
2237
|
}
|
|
1983
2238
|
});
|
|
1984
|
-
broadcast({
|
|
2239
|
+
broadcast(clients, {
|
|
1985
2240
|
type: "todos.updated",
|
|
1986
2241
|
payload: { todos: [...context.todos] }
|
|
1987
2242
|
});
|
|
1988
2243
|
});
|
|
1989
2244
|
events.on("provider.response", (e) => {
|
|
1990
|
-
broadcast({
|
|
2245
|
+
broadcast(clients, {
|
|
1991
2246
|
type: "provider.response",
|
|
1992
2247
|
payload: {
|
|
1993
2248
|
usage: e.usage,
|
|
@@ -1997,7 +2252,7 @@ async function startWebUI(opts = {}) {
|
|
|
1997
2252
|
});
|
|
1998
2253
|
});
|
|
1999
2254
|
events.on("context.repaired", (e) => {
|
|
2000
|
-
broadcast({
|
|
2255
|
+
broadcast(clients, {
|
|
2001
2256
|
type: "context.repaired",
|
|
2002
2257
|
payload: {
|
|
2003
2258
|
removedToolUses: e.removedToolUses,
|
|
@@ -2009,7 +2264,7 @@ async function startWebUI(opts = {}) {
|
|
|
2009
2264
|
events.on("tool.confirm_needed", (e) => {
|
|
2010
2265
|
const id = e.toolUseId ?? `confirm_${Date.now()}`;
|
|
2011
2266
|
pendingConfirms.set(id, e.resolve);
|
|
2012
|
-
broadcast({
|
|
2267
|
+
broadcast(clients, {
|
|
2013
2268
|
type: "tool.confirm_needed",
|
|
2014
2269
|
payload: {
|
|
2015
2270
|
id,
|
|
@@ -2020,7 +2275,7 @@ async function startWebUI(opts = {}) {
|
|
|
2020
2275
|
});
|
|
2021
2276
|
});
|
|
2022
2277
|
events.on("error", (e) => {
|
|
2023
|
-
broadcast({
|
|
2278
|
+
broadcast(clients, {
|
|
2024
2279
|
type: "error",
|
|
2025
2280
|
payload: {
|
|
2026
2281
|
phase: e.phase,
|
|
@@ -2028,19 +2283,71 @@ async function startWebUI(opts = {}) {
|
|
|
2028
2283
|
}
|
|
2029
2284
|
});
|
|
2030
2285
|
});
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2286
|
+
const forwardSubagent = (kind, payload) => broadcast(clients, { type: "subagent.event", payload: { kind, ...payload } });
|
|
2287
|
+
events.on(
|
|
2288
|
+
"subagent.spawned",
|
|
2289
|
+
(e) => forwardSubagent("spawned", {
|
|
2290
|
+
subagentId: e.subagentId,
|
|
2291
|
+
taskId: e.taskId,
|
|
2292
|
+
name: e.name,
|
|
2293
|
+
provider: e.provider,
|
|
2294
|
+
model: e.model,
|
|
2295
|
+
description: e.description
|
|
2296
|
+
})
|
|
2297
|
+
);
|
|
2298
|
+
events.on(
|
|
2299
|
+
"subagent.task_started",
|
|
2300
|
+
(e) => forwardSubagent("task_started", {
|
|
2301
|
+
subagentId: e.subagentId,
|
|
2302
|
+
taskId: e.taskId,
|
|
2303
|
+
description: e.description
|
|
2304
|
+
})
|
|
2305
|
+
);
|
|
2306
|
+
events.on(
|
|
2307
|
+
"subagent.tool_executed",
|
|
2308
|
+
(e) => forwardSubagent("tool_executed", {
|
|
2309
|
+
subagentId: e.subagentId,
|
|
2310
|
+
toolName: e.name,
|
|
2311
|
+
durationMs: e.durationMs,
|
|
2312
|
+
ok: e.ok
|
|
2313
|
+
})
|
|
2314
|
+
);
|
|
2315
|
+
events.on(
|
|
2316
|
+
"subagent.iteration_summary",
|
|
2317
|
+
(e) => forwardSubagent("iteration_summary", {
|
|
2318
|
+
subagentId: e.subagentId,
|
|
2319
|
+
iteration: e.iteration,
|
|
2320
|
+
toolCalls: e.toolCalls,
|
|
2321
|
+
costUsd: e.costUsd,
|
|
2322
|
+
currentTool: e.currentTool
|
|
2323
|
+
})
|
|
2324
|
+
);
|
|
2325
|
+
events.on(
|
|
2326
|
+
"subagent.budget_extended",
|
|
2327
|
+
(e) => forwardSubagent("budget_extended", {
|
|
2328
|
+
subagentId: e.subagentId,
|
|
2329
|
+
totalExtensions: e.totalExtensions
|
|
2330
|
+
})
|
|
2331
|
+
);
|
|
2332
|
+
events.on(
|
|
2333
|
+
"subagent.ctx_pct",
|
|
2334
|
+
(e) => forwardSubagent("ctx_pct", {
|
|
2335
|
+
subagentId: e.subagentId,
|
|
2336
|
+
load: e.load,
|
|
2337
|
+
tokens: e.tokens,
|
|
2338
|
+
maxContext: e.maxContext
|
|
2339
|
+
})
|
|
2340
|
+
);
|
|
2341
|
+
events.on(
|
|
2342
|
+
"subagent.task_completed",
|
|
2343
|
+
(e) => forwardSubagent("task_completed", {
|
|
2344
|
+
subagentId: e.subagentId,
|
|
2345
|
+
status: e.status,
|
|
2346
|
+
iterations: e.iterations,
|
|
2347
|
+
toolCalls: e.toolCalls,
|
|
2348
|
+
error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0
|
|
2349
|
+
})
|
|
2350
|
+
);
|
|
2044
2351
|
}
|
|
2045
2352
|
const handleConnection = (ws) => {
|
|
2046
2353
|
const client = { ws, sessionId: session.id, connectedAt: Date.now() };
|
|
@@ -2184,7 +2491,7 @@ async function startWebUI(opts = {}) {
|
|
|
2184
2491
|
}
|
|
2185
2492
|
case "abort":
|
|
2186
2493
|
runLock?.abort();
|
|
2187
|
-
broadcast({ type: "error", payload: { phase: "abort", message: "User aborted" } });
|
|
2494
|
+
broadcast(clients, { type: "error", payload: { phase: "abort", message: "User aborted" } });
|
|
2188
2495
|
break;
|
|
2189
2496
|
case "ping":
|
|
2190
2497
|
send(ws, { type: "pong", payload: {} });
|
|
@@ -2203,7 +2510,7 @@ async function startWebUI(opts = {}) {
|
|
|
2203
2510
|
context.fileMtimes.clear();
|
|
2204
2511
|
tokenCounter.reset();
|
|
2205
2512
|
sessionStartedAt = Date.now();
|
|
2206
|
-
broadcast({ type: "session.start", payload: await sessionStartPayload() });
|
|
2513
|
+
broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
|
|
2207
2514
|
break;
|
|
2208
2515
|
}
|
|
2209
2516
|
case "context.clear": {
|
|
@@ -2213,7 +2520,7 @@ async function startWebUI(opts = {}) {
|
|
|
2213
2520
|
context.fileMtimes.clear();
|
|
2214
2521
|
tokenCounter.reset();
|
|
2215
2522
|
sendResult(ws, true, "Context cleared");
|
|
2216
|
-
broadcast({
|
|
2523
|
+
broadcast(clients, {
|
|
2217
2524
|
type: "session.start",
|
|
2218
2525
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
2219
2526
|
});
|
|
@@ -2272,7 +2579,7 @@ async function startWebUI(opts = {}) {
|
|
|
2272
2579
|
beforeMessages,
|
|
2273
2580
|
afterMessages: context.messages.length
|
|
2274
2581
|
};
|
|
2275
|
-
broadcast({ type: "context.repaired", payload });
|
|
2582
|
+
broadcast(clients, { type: "context.repaired", payload });
|
|
2276
2583
|
const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
|
|
2277
2584
|
sendResult(
|
|
2278
2585
|
ws,
|
|
@@ -2310,7 +2617,7 @@ async function startWebUI(opts = {}) {
|
|
|
2310
2617
|
context.meta["contextWindowMode"] = policy.id;
|
|
2311
2618
|
context.meta["contextWindowPolicy"] = policy;
|
|
2312
2619
|
sendResult(ws, true, `Context mode switched to ${policy.id}`);
|
|
2313
|
-
broadcast({
|
|
2620
|
+
broadcast(clients, {
|
|
2314
2621
|
type: "context.mode.changed",
|
|
2315
2622
|
payload: { id: policy.id, name: policy.name, policy }
|
|
2316
2623
|
});
|
|
@@ -2336,7 +2643,7 @@ async function startWebUI(opts = {}) {
|
|
|
2336
2643
|
break;
|
|
2337
2644
|
}
|
|
2338
2645
|
case "providers.saved": {
|
|
2339
|
-
const saved = await
|
|
2646
|
+
const saved = await loadConfigProviders();
|
|
2340
2647
|
send(ws, {
|
|
2341
2648
|
type: "providers.saved",
|
|
2342
2649
|
payload: {
|
|
@@ -2395,11 +2702,11 @@ async function startWebUI(opts = {}) {
|
|
|
2395
2702
|
updateAutoCompactionMaxContext?.(newProv);
|
|
2396
2703
|
try {
|
|
2397
2704
|
configWriteLock = configWriteLock.then(async () => {
|
|
2398
|
-
const raw = await
|
|
2705
|
+
const raw = await fs4.readFile(globalConfigPath, "utf8");
|
|
2399
2706
|
const parsed = JSON.parse(raw);
|
|
2400
2707
|
parsed.provider = newProvider;
|
|
2401
2708
|
parsed.model = newModel;
|
|
2402
|
-
await
|
|
2709
|
+
await atomicWrite3(globalConfigPath, JSON.stringify(parsed, null, 2));
|
|
2403
2710
|
});
|
|
2404
2711
|
await configWriteLock;
|
|
2405
2712
|
} catch (err) {
|
|
@@ -2419,7 +2726,7 @@ async function startWebUI(opts = {}) {
|
|
|
2419
2726
|
});
|
|
2420
2727
|
break;
|
|
2421
2728
|
}
|
|
2422
|
-
broadcast({ type: "session.start", payload: await sessionStartPayload() });
|
|
2729
|
+
broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
|
|
2423
2730
|
break;
|
|
2424
2731
|
}
|
|
2425
2732
|
case "key.add":
|
|
@@ -2508,7 +2815,7 @@ async function startWebUI(opts = {}) {
|
|
|
2508
2815
|
tokenCounter.reset();
|
|
2509
2816
|
tokenCounter.account(resumed.data.usage, config.model);
|
|
2510
2817
|
sessionStartedAt = Date.now();
|
|
2511
|
-
broadcast({
|
|
2818
|
+
broadcast(clients, {
|
|
2512
2819
|
type: "session.start",
|
|
2513
2820
|
payload: {
|
|
2514
2821
|
...await sessionStartPayload(),
|
|
@@ -2648,7 +2955,7 @@ async function startWebUI(opts = {}) {
|
|
|
2648
2955
|
case "todos.clear": {
|
|
2649
2956
|
context.state.replaceTodos([]);
|
|
2650
2957
|
sendResult(ws, true, "Todos cleared");
|
|
2651
|
-
broadcast({ type: "todos.updated", payload: { todos: [] } });
|
|
2958
|
+
broadcast(clients, { type: "todos.updated", payload: { todos: [] } });
|
|
2652
2959
|
break;
|
|
2653
2960
|
}
|
|
2654
2961
|
case "plan.get": {
|
|
@@ -2695,7 +3002,7 @@ async function startWebUI(opts = {}) {
|
|
|
2695
3002
|
}
|
|
2696
3003
|
await savePlan(planPath, plan);
|
|
2697
3004
|
sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
|
|
2698
|
-
broadcast({
|
|
3005
|
+
broadcast(clients, {
|
|
2699
3006
|
type: "plan.updated",
|
|
2700
3007
|
payload: { plan }
|
|
2701
3008
|
});
|
|
@@ -2712,7 +3019,7 @@ async function startWebUI(opts = {}) {
|
|
|
2712
3019
|
if (depth > 8 || results.length >= 600) return;
|
|
2713
3020
|
let entries = [];
|
|
2714
3021
|
try {
|
|
2715
|
-
entries = await
|
|
3022
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
2716
3023
|
} catch {
|
|
2717
3024
|
return;
|
|
2718
3025
|
}
|
|
@@ -2722,7 +3029,7 @@ async function startWebUI(opts = {}) {
|
|
|
2722
3029
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
2723
3030
|
if (e.isDirectory()) {
|
|
2724
3031
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
2725
|
-
await walk(
|
|
3032
|
+
await walk(path4.join(dir, e.name), childRel, depth + 1);
|
|
2726
3033
|
} else if (e.isFile()) {
|
|
2727
3034
|
results.push(childRel);
|
|
2728
3035
|
}
|
|
@@ -2791,7 +3098,7 @@ async function startWebUI(opts = {}) {
|
|
|
2791
3098
|
model: config.model
|
|
2792
3099
|
});
|
|
2793
3100
|
sendResult(ws, true, `Switched to mode "${id}"`);
|
|
2794
|
-
broadcast({
|
|
3101
|
+
broadcast(clients, {
|
|
2795
3102
|
type: "session.start",
|
|
2796
3103
|
payload: { ...await sessionStartPayload() }
|
|
2797
3104
|
});
|
|
@@ -2830,39 +3137,20 @@ async function startWebUI(opts = {}) {
|
|
|
2830
3137
|
}
|
|
2831
3138
|
}
|
|
2832
3139
|
}
|
|
2833
|
-
async function
|
|
2834
|
-
|
|
2835
|
-
const raw = await fs2.readFile(globalConfigPath, "utf8");
|
|
2836
|
-
const parsed = JSON.parse(raw);
|
|
2837
|
-
if (!parsed.providers) return {};
|
|
2838
|
-
return decryptConfigSecrets(parsed.providers, vault);
|
|
2839
|
-
} catch {
|
|
2840
|
-
return {};
|
|
2841
|
-
}
|
|
3140
|
+
async function loadConfigProviders() {
|
|
3141
|
+
return loadSavedProviders(globalConfigPath, vault);
|
|
2842
3142
|
}
|
|
2843
|
-
async function
|
|
2844
|
-
configWriteLock = configWriteLock.then(
|
|
2845
|
-
|
|
2846
|
-
|
|
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
|
-
});
|
|
3143
|
+
async function saveConfigProviders(providers) {
|
|
3144
|
+
configWriteLock = configWriteLock.then(
|
|
3145
|
+
() => saveProviders(globalConfigPath, vault, providers)
|
|
3146
|
+
);
|
|
2856
3147
|
await configWriteLock;
|
|
2857
3148
|
}
|
|
2858
|
-
function sendResult(ws, success, message) {
|
|
2859
|
-
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
2860
|
-
}
|
|
2861
3149
|
async function handleKeyUpsert(ws, providerId, label, apiKey) {
|
|
2862
3150
|
try {
|
|
2863
|
-
const providers = await
|
|
3151
|
+
const providers = await loadConfigProviders();
|
|
2864
3152
|
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
2865
|
-
if (result.ok) await
|
|
3153
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2866
3154
|
sendResult(ws, result.ok, result.message);
|
|
2867
3155
|
} catch (err) {
|
|
2868
3156
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2870,9 +3158,9 @@ async function startWebUI(opts = {}) {
|
|
|
2870
3158
|
}
|
|
2871
3159
|
async function handleKeyDelete(ws, providerId, label) {
|
|
2872
3160
|
try {
|
|
2873
|
-
const providers = await
|
|
3161
|
+
const providers = await loadConfigProviders();
|
|
2874
3162
|
const result = deleteKey(providers, providerId, label);
|
|
2875
|
-
if (result.ok) await
|
|
3163
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2876
3164
|
sendResult(ws, result.ok, result.message);
|
|
2877
3165
|
} catch (err) {
|
|
2878
3166
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2880,9 +3168,9 @@ async function startWebUI(opts = {}) {
|
|
|
2880
3168
|
}
|
|
2881
3169
|
async function handleKeySetActive(ws, providerId, label) {
|
|
2882
3170
|
try {
|
|
2883
|
-
const providers = await
|
|
3171
|
+
const providers = await loadConfigProviders();
|
|
2884
3172
|
const result = setActiveKey(providers, providerId, label);
|
|
2885
|
-
if (result.ok) await
|
|
3173
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2886
3174
|
sendResult(ws, result.ok, result.message);
|
|
2887
3175
|
} catch (err) {
|
|
2888
3176
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2890,9 +3178,9 @@ async function startWebUI(opts = {}) {
|
|
|
2890
3178
|
}
|
|
2891
3179
|
async function handleProviderAdd(ws, payload) {
|
|
2892
3180
|
try {
|
|
2893
|
-
const providers = await
|
|
3181
|
+
const providers = await loadConfigProviders();
|
|
2894
3182
|
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
2895
|
-
if (result.ok) await
|
|
3183
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2896
3184
|
sendResult(ws, result.ok, result.message);
|
|
2897
3185
|
} catch (err) {
|
|
2898
3186
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2900,9 +3188,9 @@ async function startWebUI(opts = {}) {
|
|
|
2900
3188
|
}
|
|
2901
3189
|
async function handleProviderRemove(ws, providerId) {
|
|
2902
3190
|
try {
|
|
2903
|
-
const providers = await
|
|
3191
|
+
const providers = await loadConfigProviders();
|
|
2904
3192
|
const result = removeProvider(providers, providerId);
|
|
2905
|
-
if (result.ok) await
|
|
3193
|
+
if (result.ok) await saveConfigProviders(providers);
|
|
2906
3194
|
sendResult(ws, result.ok, result.message);
|
|
2907
3195
|
} catch (err) {
|
|
2908
3196
|
sendResult(ws, false, errMessage(err));
|
|
@@ -2910,12 +3198,27 @@ async function startWebUI(opts = {}) {
|
|
|
2910
3198
|
}
|
|
2911
3199
|
const httpServer = createHttpServer({
|
|
2912
3200
|
host: wsHost,
|
|
2913
|
-
distDir:
|
|
3201
|
+
distDir: path4.resolve(import.meta.dirname, "../../dist"),
|
|
2914
3202
|
wsPort
|
|
2915
3203
|
});
|
|
2916
|
-
const
|
|
3204
|
+
const registryBaseDir = path4.dirname(globalConfigPath);
|
|
2917
3205
|
httpServer.listen(httpPort, wsHost, () => {
|
|
2918
|
-
|
|
3206
|
+
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
3207
|
+
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
3208
|
+
if (opts.open) openBrowser(openUrl);
|
|
3209
|
+
void registerInstance(
|
|
3210
|
+
{
|
|
3211
|
+
pid: process.pid,
|
|
3212
|
+
httpPort,
|
|
3213
|
+
wsPort,
|
|
3214
|
+
host: wsHost,
|
|
3215
|
+
projectRoot,
|
|
3216
|
+
projectName: path4.basename(projectRoot) || projectRoot,
|
|
3217
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3218
|
+
url: `http://${wsHost}:${httpPort}`
|
|
3219
|
+
},
|
|
3220
|
+
registryBaseDir
|
|
3221
|
+
).catch((err) => console.warn("[WebUI] Could not record instance:", errMessage(err)));
|
|
2919
3222
|
});
|
|
2920
3223
|
registerShutdownHandlers({
|
|
2921
3224
|
flushSession: async () => {
|
|
@@ -2927,10 +3230,48 @@ async function startWebUI(opts = {}) {
|
|
|
2927
3230
|
await session.close();
|
|
2928
3231
|
},
|
|
2929
3232
|
clients: () => clients.keys(),
|
|
2930
|
-
servers: [httpServer, wssPrimary, wssSecondary]
|
|
3233
|
+
servers: [httpServer, wssPrimary, wssSecondary],
|
|
3234
|
+
// Drop this instance from the registry on a clean exit so the file reflects
|
|
3235
|
+
// reality. Crash exits are healed by the next register()/list() prune pass.
|
|
3236
|
+
onShutdown: () => unregisterInstance(process.pid, registryBaseDir)
|
|
2931
3237
|
});
|
|
2932
3238
|
}
|
|
2933
3239
|
export {
|
|
2934
|
-
|
|
3240
|
+
addProvider,
|
|
3241
|
+
broadcast,
|
|
3242
|
+
browserOpenCommand,
|
|
3243
|
+
buildCspHeader,
|
|
3244
|
+
createHttpServer,
|
|
3245
|
+
createProviderConfigIO,
|
|
3246
|
+
defaultBaseDir,
|
|
3247
|
+
deleteKey,
|
|
3248
|
+
errMessage,
|
|
3249
|
+
extractToken,
|
|
3250
|
+
findFreePort,
|
|
3251
|
+
formatInstances,
|
|
3252
|
+
generateAuthToken,
|
|
3253
|
+
hostHeaderOk,
|
|
3254
|
+
injectWsPort,
|
|
3255
|
+
isLoopbackBind,
|
|
3256
|
+
isLoopbackHostname,
|
|
3257
|
+
isPortFree,
|
|
3258
|
+
listInstances,
|
|
3259
|
+
loadSavedProviders,
|
|
3260
|
+
maskedKey,
|
|
3261
|
+
normalizeKeys,
|
|
3262
|
+
openBrowser,
|
|
3263
|
+
registerInstance,
|
|
3264
|
+
registryPath,
|
|
3265
|
+
removeProvider,
|
|
3266
|
+
saveProviders,
|
|
3267
|
+
send,
|
|
3268
|
+
sendResult,
|
|
3269
|
+
setActiveKey,
|
|
3270
|
+
startWebUI,
|
|
3271
|
+
tokenMatches,
|
|
3272
|
+
unregisterInstance,
|
|
3273
|
+
upsertKey,
|
|
3274
|
+
verifyClient,
|
|
3275
|
+
writeKeysBack
|
|
2935
3276
|
};
|
|
2936
3277
|
//# sourceMappingURL=index.js.map
|