@wrongstack/webui 0.250.0 → 0.256.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/index-DHok34Zp.js +164 -0
- package/dist/assets/index-DVRA0JCZ.css +2 -0
- package/dist/assets/{vendor-BtOIO1oa.js → vendor-XlX63RRU.js} +229 -229
- package/dist/index.html +3 -3
- package/dist/index.js +3292 -1327
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +333 -170
- package/dist/server/entry.js.map +1 -1
- package/dist/server/index.d.ts +131 -4
- package/dist/server/index.js +339 -170
- package/dist/server/index.js.map +1 -1
- package/package.json +7 -6
- package/dist/assets/index-Cm_R0cfw.css +0 -2
- package/dist/assets/index-DIwGHhP2.js +0 -163
package/dist/server/index.js
CHANGED
|
@@ -15,13 +15,99 @@ import {
|
|
|
15
15
|
createAutonomyBrain,
|
|
16
16
|
createTieredBrainArbiter
|
|
17
17
|
} from "@wrongstack/core";
|
|
18
|
-
import * as
|
|
19
|
-
import * as
|
|
18
|
+
import * as fs7 from "fs/promises";
|
|
19
|
+
import * as path9 from "path";
|
|
20
20
|
|
|
21
21
|
// src/server/http-server.ts
|
|
22
22
|
import * as fs from "fs/promises";
|
|
23
23
|
import * as http from "http";
|
|
24
24
|
import * as path from "path";
|
|
25
|
+
|
|
26
|
+
// src/server/ws-auth.ts
|
|
27
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
28
|
+
import { timingSafeEqual } from "crypto";
|
|
29
|
+
function isLoopbackHostname(hostname) {
|
|
30
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
31
|
+
}
|
|
32
|
+
function isTrustedLoopbackOrigin(origin) {
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(origin);
|
|
35
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
36
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isLoopbackBind(wsHost) {
|
|
42
|
+
return wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
|
|
43
|
+
}
|
|
44
|
+
function tokenMatches(provided, expected) {
|
|
45
|
+
if (!provided) return false;
|
|
46
|
+
const a = Buffer2.from(provided);
|
|
47
|
+
const b = Buffer2.from(expected);
|
|
48
|
+
if (a.length !== b.length) return false;
|
|
49
|
+
return timingSafeEqual(a, b);
|
|
50
|
+
}
|
|
51
|
+
function extractToken(url) {
|
|
52
|
+
const match = url.match(/[?&]token=([^&]+)/);
|
|
53
|
+
return match ? match[1] : void 0;
|
|
54
|
+
}
|
|
55
|
+
function extractTokenFromCookie(cookieHeader) {
|
|
56
|
+
if (!cookieHeader) return void 0;
|
|
57
|
+
const raw = Array.isArray(cookieHeader) ? cookieHeader.join("; ") : cookieHeader;
|
|
58
|
+
for (const part of raw.split(";")) {
|
|
59
|
+
const eq = part.indexOf("=");
|
|
60
|
+
if (eq < 0) continue;
|
|
61
|
+
const name = part.slice(0, eq).trim();
|
|
62
|
+
if (name === "ws_token") {
|
|
63
|
+
try {
|
|
64
|
+
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
65
|
+
} catch {
|
|
66
|
+
return part.slice(eq + 1).trim();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
function hostHeaderOk(input) {
|
|
73
|
+
if (!isLoopbackBind(input.wsHost)) return true;
|
|
74
|
+
const hostHeader = (input.hostHeader ?? "").trim();
|
|
75
|
+
if (!hostHeader) return false;
|
|
76
|
+
let hostname;
|
|
77
|
+
try {
|
|
78
|
+
hostname = new URL(`http://${hostHeader}`).hostname;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return isLoopbackHostname(hostname);
|
|
83
|
+
}
|
|
84
|
+
function verifyClient(input) {
|
|
85
|
+
const { origin, url, hostHeader, remoteAddress, cookieHeader, wsHost, expectedToken } = input;
|
|
86
|
+
const urlToken = extractToken(url ?? "");
|
|
87
|
+
const cookieToken = extractTokenFromCookie(cookieHeader);
|
|
88
|
+
const tokenOk = tokenMatches(urlToken, expectedToken) || tokenMatches(cookieToken, expectedToken);
|
|
89
|
+
if (!hostHeaderOk({ hostHeader, wsHost })) return false;
|
|
90
|
+
if (!origin) {
|
|
91
|
+
const remoteIp = remoteAddress ?? "";
|
|
92
|
+
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
93
|
+
if (!isRemoteLoopback && wsHost === "0.0.0.0") return false;
|
|
94
|
+
return tokenOk || isLoopbackBind(wsHost);
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const { hostname } = new URL(origin);
|
|
98
|
+
if (isLoopbackHostname(hostname)) {
|
|
99
|
+
if (wsHost === "0.0.0.0" && !isTrustedLoopbackOrigin(origin)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return tokenOk;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/server/http-server.ts
|
|
25
111
|
var MIME_TYPES = {
|
|
26
112
|
".html": "text/html",
|
|
27
113
|
".js": "application/javascript",
|
|
@@ -53,15 +139,47 @@ function createHttpServer(opts) {
|
|
|
53
139
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
54
140
|
const distDir = path.resolve(opts.distDir);
|
|
55
141
|
const wsPort = opts.wsPort;
|
|
142
|
+
const requireApiToken = !isLoopbackBind(opts.host) && Boolean(opts.apiToken);
|
|
56
143
|
return http.createServer(async (req, res) => {
|
|
57
144
|
try {
|
|
58
145
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
146
|
+
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
147
|
+
const provided = url.searchParams.get("token") ?? req.headers["x-ws-token"];
|
|
148
|
+
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
149
|
+
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
150
|
+
res.end("Unauthorized");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
res.writeHead(200, {
|
|
154
|
+
"Content-Type": "text/plain",
|
|
155
|
+
"Set-Cookie": `ws_token=${encodeURIComponent(opts.apiToken)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600`,
|
|
156
|
+
// Belt-and-braces: tell any caches the cookie response itself
|
|
157
|
+
// is sensitive.
|
|
158
|
+
"Cache-Control": "no-store"
|
|
159
|
+
});
|
|
160
|
+
res.end("ok");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
59
163
|
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
164
|
+
const headerToken = req.headers["x-ws-token"];
|
|
165
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
166
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
167
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
168
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
60
171
|
await handleApiSessions(res, opts.globalRoot);
|
|
61
172
|
return;
|
|
62
173
|
}
|
|
63
174
|
const agentsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/agents$/);
|
|
64
175
|
if (agentsMatch && req.method === "GET") {
|
|
176
|
+
const headerToken = req.headers["x-ws-token"];
|
|
177
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
178
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
179
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
180
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
65
183
|
await handleApiSessionAgents(res, opts.globalRoot, agentsMatch[1]);
|
|
66
184
|
return;
|
|
67
185
|
}
|
|
@@ -1799,71 +1917,6 @@ async function handleMailboxClear(ws, deps) {
|
|
|
1799
1917
|
}
|
|
1800
1918
|
}
|
|
1801
1919
|
|
|
1802
|
-
// src/server/ws-auth.ts
|
|
1803
|
-
import { Buffer as Buffer2 } from "buffer";
|
|
1804
|
-
import { timingSafeEqual } from "crypto";
|
|
1805
|
-
function isLoopbackHostname(hostname) {
|
|
1806
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1807
|
-
}
|
|
1808
|
-
function isTrustedLoopbackOrigin(origin) {
|
|
1809
|
-
try {
|
|
1810
|
-
const url = new URL(origin);
|
|
1811
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
1812
|
-
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
1813
|
-
} catch {
|
|
1814
|
-
return false;
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
function isLoopbackBind(wsHost) {
|
|
1818
|
-
return wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
|
|
1819
|
-
}
|
|
1820
|
-
function tokenMatches(provided, expected) {
|
|
1821
|
-
if (!provided) return false;
|
|
1822
|
-
const a = Buffer2.from(provided);
|
|
1823
|
-
const b = Buffer2.from(expected);
|
|
1824
|
-
if (a.length !== b.length) return false;
|
|
1825
|
-
return timingSafeEqual(a, b);
|
|
1826
|
-
}
|
|
1827
|
-
function extractToken(url) {
|
|
1828
|
-
const match = url.match(/[?&]token=([^&]+)/);
|
|
1829
|
-
return match ? match[1] : void 0;
|
|
1830
|
-
}
|
|
1831
|
-
function hostHeaderOk(input) {
|
|
1832
|
-
if (!isLoopbackBind(input.wsHost)) return true;
|
|
1833
|
-
const hostHeader = (input.hostHeader ?? "").trim();
|
|
1834
|
-
if (!hostHeader) return false;
|
|
1835
|
-
let hostname;
|
|
1836
|
-
try {
|
|
1837
|
-
hostname = new URL(`http://${hostHeader}`).hostname;
|
|
1838
|
-
} catch {
|
|
1839
|
-
return false;
|
|
1840
|
-
}
|
|
1841
|
-
return isLoopbackHostname(hostname);
|
|
1842
|
-
}
|
|
1843
|
-
function verifyClient(input) {
|
|
1844
|
-
const { origin, url, hostHeader, remoteAddress, wsHost, expectedToken } = input;
|
|
1845
|
-
const tokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
1846
|
-
if (!hostHeaderOk({ hostHeader, wsHost })) return false;
|
|
1847
|
-
if (!origin) {
|
|
1848
|
-
const remoteIp = remoteAddress ?? "";
|
|
1849
|
-
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
1850
|
-
if (!isRemoteLoopback && wsHost === "0.0.0.0") return false;
|
|
1851
|
-
return tokenOk || isLoopbackBind(wsHost);
|
|
1852
|
-
}
|
|
1853
|
-
try {
|
|
1854
|
-
const { hostname } = new URL(origin);
|
|
1855
|
-
if (isLoopbackHostname(hostname)) {
|
|
1856
|
-
if (wsHost === "0.0.0.0" && !isTrustedLoopbackOrigin(origin)) {
|
|
1857
|
-
return false;
|
|
1858
|
-
}
|
|
1859
|
-
return true;
|
|
1860
|
-
}
|
|
1861
|
-
return tokenOk;
|
|
1862
|
-
} catch {
|
|
1863
|
-
return false;
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
1920
|
// src/server/lifecycle.ts
|
|
1868
1921
|
function createShutdown(res) {
|
|
1869
1922
|
const log = res.log ?? ((m) => console.log(m));
|
|
@@ -1981,16 +2034,16 @@ function formatInstances(instances) {
|
|
|
1981
2034
|
// src/server/port-utils.ts
|
|
1982
2035
|
import * as net from "net";
|
|
1983
2036
|
function isPortFree(host, port) {
|
|
1984
|
-
return new Promise((
|
|
2037
|
+
return new Promise((resolve6) => {
|
|
1985
2038
|
const srv = net.createServer();
|
|
1986
|
-
srv.once("error", () =>
|
|
2039
|
+
srv.once("error", () => resolve6(false));
|
|
1987
2040
|
srv.once("listening", () => {
|
|
1988
|
-
srv.close(() =>
|
|
2041
|
+
srv.close(() => resolve6(true));
|
|
1989
2042
|
});
|
|
1990
2043
|
try {
|
|
1991
2044
|
srv.listen(port, host);
|
|
1992
2045
|
} catch {
|
|
1993
|
-
|
|
2046
|
+
resolve6(false);
|
|
1994
2047
|
}
|
|
1995
2048
|
});
|
|
1996
2049
|
}
|
|
@@ -2729,6 +2782,79 @@ function estimateContextBreakdown(input) {
|
|
|
2729
2782
|
};
|
|
2730
2783
|
}
|
|
2731
2784
|
|
|
2785
|
+
// src/server/eternal-iteration-broadcast.ts
|
|
2786
|
+
function createEternalSubscription(subscribe, broadcast2, clientsRef) {
|
|
2787
|
+
let disposed = false;
|
|
2788
|
+
const dispose = subscribe((entry) => {
|
|
2789
|
+
if (disposed) return;
|
|
2790
|
+
broadcast2(clientsRef(), {
|
|
2791
|
+
type: "eternal.iteration",
|
|
2792
|
+
payload: { entry }
|
|
2793
|
+
});
|
|
2794
|
+
});
|
|
2795
|
+
return {
|
|
2796
|
+
dispose() {
|
|
2797
|
+
if (disposed) return;
|
|
2798
|
+
disposed = true;
|
|
2799
|
+
dispose();
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
// src/server/shell-open.ts
|
|
2805
|
+
import * as fs6 from "fs/promises";
|
|
2806
|
+
import * as path8 from "path";
|
|
2807
|
+
import { spawn as spawn2 } from "child_process";
|
|
2808
|
+
var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
|
|
2809
|
+
async function handleShellOpen(req, logger) {
|
|
2810
|
+
try {
|
|
2811
|
+
const resolved = path8.resolve(req.path);
|
|
2812
|
+
await fs6.access(resolved);
|
|
2813
|
+
if (METACHAR_REGEX.test(resolved)) {
|
|
2814
|
+
return { success: false, message: "Path contains unsupported characters." };
|
|
2815
|
+
}
|
|
2816
|
+
const platform = process.platform;
|
|
2817
|
+
const launch = (cmd, args, onError) => {
|
|
2818
|
+
const child = spawn2(cmd, args, {
|
|
2819
|
+
detached: true,
|
|
2820
|
+
stdio: "ignore",
|
|
2821
|
+
windowsHide: true
|
|
2822
|
+
});
|
|
2823
|
+
child.on("error", (err) => {
|
|
2824
|
+
logger.warn(`shell.open spawn failed: ${err.message}`);
|
|
2825
|
+
onError?.();
|
|
2826
|
+
});
|
|
2827
|
+
child.unref();
|
|
2828
|
+
};
|
|
2829
|
+
if (req.target === "file-manager") {
|
|
2830
|
+
if (platform === "win32") launch("explorer", [resolved]);
|
|
2831
|
+
else if (platform === "darwin") launch("open", [resolved]);
|
|
2832
|
+
else launch("xdg-open", [resolved]);
|
|
2833
|
+
} else if (req.target === "terminal") {
|
|
2834
|
+
if (platform === "win32") {
|
|
2835
|
+
launch("cmd", ["/c", "start", "cmd", "/k", "cd", "/d", resolved]);
|
|
2836
|
+
} else if (platform === "darwin") {
|
|
2837
|
+
launch("open", ["-a", "Terminal", resolved]);
|
|
2838
|
+
} else {
|
|
2839
|
+
launch(
|
|
2840
|
+
"x-terminal-emulator",
|
|
2841
|
+
[`--working-directory=${resolved}`],
|
|
2842
|
+
() => launch(
|
|
2843
|
+
"gnome-terminal",
|
|
2844
|
+
[`--working-directory=${resolved}`],
|
|
2845
|
+
() => launch("xterm", ["-e", `cd '${resolved}' && ${process.env["SHELL"] ?? "sh"}`])
|
|
2846
|
+
)
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
2849
|
+
} else {
|
|
2850
|
+
return { success: false, message: `Unknown shell.open target: ${String(req.target)}` };
|
|
2851
|
+
}
|
|
2852
|
+
return { success: true, message: `Opened ${req.target} at ${resolved}` };
|
|
2853
|
+
} catch (err) {
|
|
2854
|
+
return { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2732
2858
|
// src/server/index.ts
|
|
2733
2859
|
async function startWebUI(opts = {}) {
|
|
2734
2860
|
const requestedWsPort = opts.wsPort ?? 3457;
|
|
@@ -2763,7 +2889,8 @@ async function startWebUI(opts = {}) {
|
|
|
2763
2889
|
}
|
|
2764
2890
|
console.log("[WebUI] Starting backend services...");
|
|
2765
2891
|
const boot = await bootConfig();
|
|
2766
|
-
const { config: baseConfig,
|
|
2892
|
+
const { config: baseConfig, globalConfigPath, wpaths, logger } = boot;
|
|
2893
|
+
const vault = opts.services?.vault ?? boot.vault;
|
|
2767
2894
|
let config = baseConfig;
|
|
2768
2895
|
let projectRoot = boot.projectRoot;
|
|
2769
2896
|
let workingDir = projectRoot;
|
|
@@ -2775,12 +2902,12 @@ async function startWebUI(opts = {}) {
|
|
|
2775
2902
|
console.log("[WebUI] No active provider \u2014 auto-selected:", firstKey);
|
|
2776
2903
|
}
|
|
2777
2904
|
const needsProvider = !config.provider || !config.model;
|
|
2778
|
-
const modelsRegistry = new DefaultModelsRegistry({
|
|
2905
|
+
const modelsRegistry = opts.services?.modelsRegistry ?? new DefaultModelsRegistry({
|
|
2779
2906
|
cacheFile: wpaths.modelsCache,
|
|
2780
2907
|
ttlSeconds: 24 * 3600
|
|
2781
2908
|
});
|
|
2782
2909
|
const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry });
|
|
2783
|
-
const configStore = container.resolve(TOKENS2.ConfigStore);
|
|
2910
|
+
const configStore = opts.services?.configStore ?? container.resolve(TOKENS2.ConfigStore);
|
|
2784
2911
|
const providerRegistry = new ProviderRegistry();
|
|
2785
2912
|
try {
|
|
2786
2913
|
const factories = await buildProviderFactoriesFromRegistry({
|
|
@@ -2797,8 +2924,11 @@ async function startWebUI(opts = {}) {
|
|
|
2797
2924
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2798
2925
|
}));
|
|
2799
2926
|
}
|
|
2800
|
-
const toolRegistry =
|
|
2801
|
-
|
|
2927
|
+
const toolRegistry = opts.services?.toolRegistry ?? (() => {
|
|
2928
|
+
const r = new ToolRegistry();
|
|
2929
|
+
r.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
|
|
2930
|
+
return r;
|
|
2931
|
+
})();
|
|
2802
2932
|
const memoryStore = new DefaultMemoryStore2({ paths: wpaths });
|
|
2803
2933
|
if (config.features.memory) {
|
|
2804
2934
|
toolRegistry.register(rememberTool(memoryStore));
|
|
@@ -2806,16 +2936,18 @@ async function startWebUI(opts = {}) {
|
|
|
2806
2936
|
toolRegistry.register(searchMemoryTool(memoryStore));
|
|
2807
2937
|
toolRegistry.register(relatedMemoryTool(memoryStore));
|
|
2808
2938
|
}
|
|
2809
|
-
const events = new EventBus();
|
|
2939
|
+
const events = opts.services?.events ?? new EventBus();
|
|
2810
2940
|
events.setLogger(logger);
|
|
2811
2941
|
toolRegistry.register(makeMailboxTool({ projectDir: wpaths.projectDir, events }));
|
|
2812
2942
|
toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
|
|
2813
2943
|
toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
|
|
2814
2944
|
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
2815
|
-
let sessionStore = new DefaultSessionStore2({ dir: wpaths.projectSessions });
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2945
|
+
let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
|
|
2946
|
+
if (!opts.services?.session) {
|
|
2947
|
+
sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
|
|
2948
|
+
if (count > 0) logger.info(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
|
|
2949
|
+
}).catch(() => void 0);
|
|
2950
|
+
}
|
|
2819
2951
|
const sessionReader = new DefaultSessionReader({ store: sessionStore });
|
|
2820
2952
|
const annotationsStore = new AnnotationsStore({ dir: wpaths.projectSessions });
|
|
2821
2953
|
let session = await sessionStore.create({
|
|
@@ -2837,7 +2969,7 @@ async function startWebUI(opts = {}) {
|
|
|
2837
2969
|
sessionId: session.id,
|
|
2838
2970
|
projectSlug: wpaths.projectSlug,
|
|
2839
2971
|
projectRoot,
|
|
2840
|
-
projectName:
|
|
2972
|
+
projectName: path9.basename(projectRoot),
|
|
2841
2973
|
workingDir,
|
|
2842
2974
|
pid: process.pid,
|
|
2843
2975
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -3032,7 +3164,7 @@ async function startWebUI(opts = {}) {
|
|
|
3032
3164
|
const write = async () => {
|
|
3033
3165
|
let raw;
|
|
3034
3166
|
try {
|
|
3035
|
-
raw = await
|
|
3167
|
+
raw = await fs7.readFile(globalConfigPath, "utf8");
|
|
3036
3168
|
} catch {
|
|
3037
3169
|
raw = "{}";
|
|
3038
3170
|
}
|
|
@@ -3303,6 +3435,14 @@ async function startWebUI(opts = {}) {
|
|
|
3303
3435
|
try {
|
|
3304
3436
|
const m = await modelsRegistry.getModel(config.provider, config.model);
|
|
3305
3437
|
maxContext = m?.capabilities?.maxContext ?? 0;
|
|
3438
|
+
if (!maxContext) {
|
|
3439
|
+
try {
|
|
3440
|
+
const provider2 = await modelsRegistry.getProvider(config.provider);
|
|
3441
|
+
const rawModel = provider2?.models.find((mod) => mod.id === config.model);
|
|
3442
|
+
maxContext = rawModel?.limit?.context ?? 0;
|
|
3443
|
+
} catch {
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3306
3446
|
const rates = getCostRates(m);
|
|
3307
3447
|
inputCost = rates.input;
|
|
3308
3448
|
outputCost = rates.output;
|
|
@@ -3317,12 +3457,11 @@ async function startWebUI(opts = {}) {
|
|
|
3317
3457
|
inputCost,
|
|
3318
3458
|
outputCost,
|
|
3319
3459
|
cacheReadCost,
|
|
3320
|
-
projectName:
|
|
3460
|
+
projectName: path9.basename(projectRoot) || projectRoot,
|
|
3321
3461
|
projectRoot,
|
|
3322
3462
|
cwd: workingDir,
|
|
3323
3463
|
mode: modeId,
|
|
3324
|
-
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID)
|
|
3325
|
-
wsToken
|
|
3464
|
+
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID)
|
|
3326
3465
|
};
|
|
3327
3466
|
}
|
|
3328
3467
|
const wsToken = generateAuthToken();
|
|
@@ -3332,6 +3471,11 @@ async function startWebUI(opts = {}) {
|
|
|
3332
3471
|
url: info.req.url ?? "",
|
|
3333
3472
|
hostHeader: info.req.headers.host,
|
|
3334
3473
|
remoteAddress: info.req.socket.remoteAddress,
|
|
3474
|
+
// C-2 fix: accept the token via the HttpOnly cookie set by
|
|
3475
|
+
// `/ws-auth` (preferred) OR the URL query param (non-browser
|
|
3476
|
+
// fallback). The cookie path closes the C-598 query-string
|
|
3477
|
+
// exposure class.
|
|
3478
|
+
cookieHeader: info.req.headers.cookie,
|
|
3335
3479
|
wsHost,
|
|
3336
3480
|
expectedToken: wsToken
|
|
3337
3481
|
});
|
|
@@ -3356,6 +3500,14 @@ async function startWebUI(opts = {}) {
|
|
|
3356
3500
|
payload: { cwd: newDir, projectRoot }
|
|
3357
3501
|
});
|
|
3358
3502
|
});
|
|
3503
|
+
let eternalSubscription = null;
|
|
3504
|
+
if (opts.subscribeEternalIteration) {
|
|
3505
|
+
eternalSubscription = createEternalSubscription(
|
|
3506
|
+
opts.subscribeEternalIteration,
|
|
3507
|
+
broadcast,
|
|
3508
|
+
() => clients
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3359
3511
|
const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
|
|
3360
3512
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
3361
3513
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
@@ -3432,8 +3584,8 @@ async function startWebUI(opts = {}) {
|
|
|
3432
3584
|
clients.delete(ws);
|
|
3433
3585
|
rateLimits.delete(String(ws));
|
|
3434
3586
|
if (pendingConfirms.size > 0) {
|
|
3435
|
-
for (const [id,
|
|
3436
|
-
|
|
3587
|
+
for (const [id, resolve6] of pendingConfirms) {
|
|
3588
|
+
resolve6("no");
|
|
3437
3589
|
pendingConfirms.delete(id);
|
|
3438
3590
|
}
|
|
3439
3591
|
}
|
|
@@ -3497,33 +3649,33 @@ async function startWebUI(opts = {}) {
|
|
|
3497
3649
|
});
|
|
3498
3650
|
}
|
|
3499
3651
|
async function touchProjectEntry(root, workDir) {
|
|
3500
|
-
const resolved =
|
|
3652
|
+
const resolved = path9.resolve(root);
|
|
3501
3653
|
const manifest = await loadManifest(globalConfigPath);
|
|
3502
3654
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3503
|
-
const existing = manifest.projects.find((p) =>
|
|
3655
|
+
const existing = manifest.projects.find((p) => path9.resolve(p.root) === resolved);
|
|
3504
3656
|
if (existing) {
|
|
3505
3657
|
existing.lastSeen = now;
|
|
3506
|
-
if (workDir) existing.lastWorkingDir =
|
|
3658
|
+
if (workDir) existing.lastWorkingDir = path9.resolve(workDir);
|
|
3507
3659
|
} else {
|
|
3508
3660
|
manifest.projects.push({
|
|
3509
|
-
name:
|
|
3661
|
+
name: path9.basename(resolved),
|
|
3510
3662
|
root: resolved,
|
|
3511
3663
|
slug: generateProjectSlug(resolved),
|
|
3512
3664
|
createdAt: now,
|
|
3513
3665
|
lastSeen: now,
|
|
3514
|
-
lastWorkingDir: workDir ?
|
|
3666
|
+
lastWorkingDir: workDir ? path9.resolve(workDir) : void 0
|
|
3515
3667
|
});
|
|
3516
3668
|
}
|
|
3517
3669
|
await saveManifest(manifest, globalConfigPath);
|
|
3518
3670
|
await ensureProjectDataDir(generateProjectSlug(resolved), globalConfigPath);
|
|
3519
3671
|
}
|
|
3520
3672
|
function projectsJsonPath(globalConfigPath2) {
|
|
3521
|
-
const base =
|
|
3522
|
-
return
|
|
3673
|
+
const base = path9.dirname(globalConfigPath2);
|
|
3674
|
+
return path9.join(base, "projects.json");
|
|
3523
3675
|
}
|
|
3524
3676
|
async function loadManifest(globalConfigPath2) {
|
|
3525
3677
|
try {
|
|
3526
|
-
const raw = await
|
|
3678
|
+
const raw = await fs7.readFile(projectsJsonPath(globalConfigPath2), "utf8");
|
|
3527
3679
|
const parsed = JSON.parse(raw);
|
|
3528
3680
|
return { projects: parsed.projects ?? [] };
|
|
3529
3681
|
} catch {
|
|
@@ -3532,16 +3684,16 @@ async function startWebUI(opts = {}) {
|
|
|
3532
3684
|
}
|
|
3533
3685
|
async function saveManifest(manifest, globalConfigPath2) {
|
|
3534
3686
|
const file = projectsJsonPath(globalConfigPath2);
|
|
3535
|
-
await
|
|
3536
|
-
await
|
|
3687
|
+
await fs7.mkdir(path9.dirname(file), { recursive: true });
|
|
3688
|
+
await fs7.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
3537
3689
|
}
|
|
3538
3690
|
function generateProjectSlug(rootPath) {
|
|
3539
3691
|
return projectSlug(rootPath);
|
|
3540
3692
|
}
|
|
3541
3693
|
async function ensureProjectDataDir(slug, globalConfigPath2) {
|
|
3542
|
-
const base =
|
|
3543
|
-
const dir =
|
|
3544
|
-
await
|
|
3694
|
+
const base = path9.dirname(globalConfigPath2);
|
|
3695
|
+
const dir = path9.join(base, "projects", slug);
|
|
3696
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
3545
3697
|
return dir;
|
|
3546
3698
|
}
|
|
3547
3699
|
async function handleMessage(ws, _client, msg) {
|
|
@@ -3603,10 +3755,10 @@ async function startWebUI(opts = {}) {
|
|
|
3603
3755
|
}
|
|
3604
3756
|
case "tool.confirm_result": {
|
|
3605
3757
|
const { id, decision } = msg.payload;
|
|
3606
|
-
const
|
|
3607
|
-
if (
|
|
3758
|
+
const resolve6 = pendingConfirms.get(id);
|
|
3759
|
+
if (resolve6) {
|
|
3608
3760
|
pendingConfirms.delete(id);
|
|
3609
|
-
|
|
3761
|
+
resolve6(decision);
|
|
3610
3762
|
}
|
|
3611
3763
|
break;
|
|
3612
3764
|
}
|
|
@@ -3880,7 +4032,7 @@ async function startWebUI(opts = {}) {
|
|
|
3880
4032
|
updateAutoCompactionMaxContext?.(newProv);
|
|
3881
4033
|
try {
|
|
3882
4034
|
configWriteLock = configWriteLock.then(async () => {
|
|
3883
|
-
const raw = await
|
|
4035
|
+
const raw = await fs7.readFile(globalConfigPath, "utf8");
|
|
3884
4036
|
const parsed = JSON.parse(raw);
|
|
3885
4037
|
parsed.provider = newProvider;
|
|
3886
4038
|
parsed.model = newModel;
|
|
@@ -4426,8 +4578,8 @@ async function startWebUI(opts = {}) {
|
|
|
4426
4578
|
}
|
|
4427
4579
|
case "goal.get": {
|
|
4428
4580
|
try {
|
|
4429
|
-
const goalPath =
|
|
4430
|
-
const raw = await
|
|
4581
|
+
const goalPath = path9.join(projectRoot, ".wrongstack", "goal.json");
|
|
4582
|
+
const raw = await fs7.readFile(goalPath, "utf8");
|
|
4431
4583
|
const goal = JSON.parse(raw);
|
|
4432
4584
|
broadcast(clients, { type: "goal.updated", payload: goal });
|
|
4433
4585
|
} catch {
|
|
@@ -4487,7 +4639,7 @@ async function startWebUI(opts = {}) {
|
|
|
4487
4639
|
try {
|
|
4488
4640
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4489
4641
|
const rewinder = new DefaultSessionRewinder(
|
|
4490
|
-
|
|
4642
|
+
path9.join(projectRoot, ".wrongstack", "sessions"),
|
|
4491
4643
|
projectRoot
|
|
4492
4644
|
);
|
|
4493
4645
|
const checkpoints = await rewinder.listCheckpoints(session.id);
|
|
@@ -4508,7 +4660,7 @@ async function startWebUI(opts = {}) {
|
|
|
4508
4660
|
try {
|
|
4509
4661
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4510
4662
|
const rewinder = new DefaultSessionRewinder(
|
|
4511
|
-
|
|
4663
|
+
path9.join(projectRoot, ".wrongstack", "sessions"),
|
|
4512
4664
|
projectRoot
|
|
4513
4665
|
);
|
|
4514
4666
|
await rewinder.rewindToCheckpoint(session.id, checkpointIndex);
|
|
@@ -4542,9 +4694,9 @@ async function startWebUI(opts = {}) {
|
|
|
4542
4694
|
case "projects.add": {
|
|
4543
4695
|
const { root: addRoot, name: displayName } = msg.payload;
|
|
4544
4696
|
try {
|
|
4545
|
-
const resolved =
|
|
4546
|
-
await
|
|
4547
|
-
const stat2 = await
|
|
4697
|
+
const resolved = path9.resolve(addRoot);
|
|
4698
|
+
await fs7.access(resolved);
|
|
4699
|
+
const stat2 = await fs7.stat(resolved);
|
|
4548
4700
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4549
4701
|
const manifest = await loadManifest(globalConfigPath);
|
|
4550
4702
|
const existing = manifest.projects.find((p) => p.root === resolved);
|
|
@@ -4560,7 +4712,7 @@ async function startWebUI(opts = {}) {
|
|
|
4560
4712
|
});
|
|
4561
4713
|
break;
|
|
4562
4714
|
}
|
|
4563
|
-
const name = displayName?.trim() ||
|
|
4715
|
+
const name = displayName?.trim() || path9.basename(resolved);
|
|
4564
4716
|
const slug = generateProjectSlug(resolved);
|
|
4565
4717
|
await ensureProjectDataDir(slug, globalConfigPath);
|
|
4566
4718
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -4579,7 +4731,7 @@ async function startWebUI(opts = {}) {
|
|
|
4579
4731
|
send(ws, {
|
|
4580
4732
|
type: "projects.added",
|
|
4581
4733
|
payload: {
|
|
4582
|
-
name:
|
|
4734
|
+
name: path9.basename(addRoot),
|
|
4583
4735
|
root: addRoot,
|
|
4584
4736
|
slug: "",
|
|
4585
4737
|
message: errMessage(err)
|
|
@@ -4591,17 +4743,17 @@ async function startWebUI(opts = {}) {
|
|
|
4591
4743
|
case "projects.select": {
|
|
4592
4744
|
const { root: selRoot, name: selName } = msg.payload;
|
|
4593
4745
|
try {
|
|
4594
|
-
const resolved =
|
|
4746
|
+
const resolved = path9.resolve(selRoot);
|
|
4595
4747
|
try {
|
|
4596
|
-
await
|
|
4597
|
-
const stat2 = await
|
|
4748
|
+
await fs7.access(resolved);
|
|
4749
|
+
const stat2 = await fs7.stat(resolved);
|
|
4598
4750
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4599
4751
|
} catch (err) {
|
|
4600
4752
|
send(ws, {
|
|
4601
4753
|
type: "projects.selected",
|
|
4602
4754
|
payload: {
|
|
4603
4755
|
root: selRoot,
|
|
4604
|
-
name: selName ||
|
|
4756
|
+
name: selName || path9.basename(selRoot),
|
|
4605
4757
|
message: `Cannot switch: ${errMessage(err)}`
|
|
4606
4758
|
}
|
|
4607
4759
|
});
|
|
@@ -4613,7 +4765,7 @@ async function startWebUI(opts = {}) {
|
|
|
4613
4765
|
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
4614
4766
|
entry.lastWorkingDir = resolved;
|
|
4615
4767
|
} else {
|
|
4616
|
-
const name = selName?.trim() ||
|
|
4768
|
+
const name = selName?.trim() || path9.basename(resolved);
|
|
4617
4769
|
const slug = generateProjectSlug(resolved);
|
|
4618
4770
|
manifest.projects.push({
|
|
4619
4771
|
name,
|
|
@@ -4634,13 +4786,33 @@ async function startWebUI(opts = {}) {
|
|
|
4634
4786
|
workingDir = resolved;
|
|
4635
4787
|
context.cwd = workingDir;
|
|
4636
4788
|
context.projectRoot = projectRoot;
|
|
4637
|
-
const
|
|
4638
|
-
|
|
4789
|
+
const switchSlug = entry?.slug ?? generateProjectSlug(resolved);
|
|
4790
|
+
try {
|
|
4791
|
+
const switchMode = modeId === "default" ? void 0 : await modeStore.getMode(modeId);
|
|
4792
|
+
const switchBuilder = new DefaultSystemPromptBuilder2({
|
|
4793
|
+
memoryStore,
|
|
4794
|
+
skillLoader,
|
|
4795
|
+
modeStore,
|
|
4796
|
+
modeId,
|
|
4797
|
+
modePrompt: switchMode?.prompt ?? "",
|
|
4798
|
+
modelCapabilities
|
|
4799
|
+
});
|
|
4800
|
+
context.systemPrompt = await switchBuilder.build({
|
|
4801
|
+
cwd: workingDir,
|
|
4802
|
+
projectRoot,
|
|
4803
|
+
tools: toolRegistry.list(),
|
|
4804
|
+
provider: config.provider,
|
|
4805
|
+
model: config.model
|
|
4806
|
+
});
|
|
4807
|
+
} catch {
|
|
4808
|
+
}
|
|
4809
|
+
const newSessionsDir = path9.join(
|
|
4810
|
+
path9.dirname(globalConfigPath),
|
|
4639
4811
|
"projects",
|
|
4640
|
-
|
|
4812
|
+
switchSlug,
|
|
4641
4813
|
"sessions"
|
|
4642
4814
|
);
|
|
4643
|
-
await
|
|
4815
|
+
await fs7.mkdir(newSessionsDir, { recursive: true });
|
|
4644
4816
|
const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
|
|
4645
4817
|
const oldSessionId = session.id;
|
|
4646
4818
|
try {
|
|
@@ -4666,12 +4838,25 @@ async function startWebUI(opts = {}) {
|
|
|
4666
4838
|
context.fileMtimes.clear();
|
|
4667
4839
|
tokenCounter.reset();
|
|
4668
4840
|
sessionStartedAt = Date.now();
|
|
4841
|
+
try {
|
|
4842
|
+
const registry = getSessionRegistry(wpaths.globalRoot);
|
|
4843
|
+
await registry.register({
|
|
4844
|
+
sessionId: session.id,
|
|
4845
|
+
projectSlug: switchSlug,
|
|
4846
|
+
projectRoot,
|
|
4847
|
+
projectName: path9.basename(projectRoot),
|
|
4848
|
+
workingDir,
|
|
4849
|
+
pid: process.pid,
|
|
4850
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4851
|
+
});
|
|
4852
|
+
} catch {
|
|
4853
|
+
}
|
|
4669
4854
|
send(ws, {
|
|
4670
4855
|
type: "projects.selected",
|
|
4671
4856
|
payload: {
|
|
4672
4857
|
root: resolved,
|
|
4673
|
-
name: selName ||
|
|
4674
|
-
message: `Switched to ${selName ||
|
|
4858
|
+
name: selName || path9.basename(resolved),
|
|
4859
|
+
message: `Switched to ${selName || path9.basename(resolved)}`
|
|
4675
4860
|
}
|
|
4676
4861
|
});
|
|
4677
4862
|
broadcast(clients, {
|
|
@@ -4694,7 +4879,7 @@ async function startWebUI(opts = {}) {
|
|
|
4694
4879
|
type: "projects.selected",
|
|
4695
4880
|
payload: {
|
|
4696
4881
|
root: selRoot,
|
|
4697
|
-
name: selName ||
|
|
4882
|
+
name: selName || path9.basename(selRoot),
|
|
4698
4883
|
message: errMessage(err)
|
|
4699
4884
|
}
|
|
4700
4885
|
});
|
|
@@ -4705,14 +4890,14 @@ async function startWebUI(opts = {}) {
|
|
|
4705
4890
|
case "working_dir.set": {
|
|
4706
4891
|
const { path: newPath } = msg.payload;
|
|
4707
4892
|
try {
|
|
4708
|
-
const resolved =
|
|
4709
|
-
if (!resolved.startsWith(projectRoot +
|
|
4893
|
+
const resolved = path9.resolve(projectRoot, newPath);
|
|
4894
|
+
if (!resolved.startsWith(projectRoot + path9.sep) && resolved !== projectRoot) {
|
|
4710
4895
|
sendResult(ws, false, `Path must stay inside the project root: ${projectRoot}`);
|
|
4711
4896
|
break;
|
|
4712
4897
|
}
|
|
4713
4898
|
try {
|
|
4714
|
-
await
|
|
4715
|
-
const stat2 = await
|
|
4899
|
+
await fs7.access(resolved);
|
|
4900
|
+
const stat2 = await fs7.stat(resolved);
|
|
4716
4901
|
if (!stat2.isDirectory()) throw new Error("Not a directory");
|
|
4717
4902
|
} catch {
|
|
4718
4903
|
sendResult(ws, false, `Directory not found or not accessible: ${resolved}`);
|
|
@@ -4732,58 +4917,30 @@ async function startWebUI(opts = {}) {
|
|
|
4732
4917
|
}
|
|
4733
4918
|
// ── Shell open — spawn terminal or file manager at a path ─────────
|
|
4734
4919
|
case "shell.open": {
|
|
4735
|
-
const
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
const platform = process.platform;
|
|
4741
|
-
let cmd;
|
|
4742
|
-
if (target === "file-manager") {
|
|
4743
|
-
if (platform === "win32") {
|
|
4744
|
-
cmd = `explorer "${resolved}"`;
|
|
4745
|
-
} else if (platform === "darwin") {
|
|
4746
|
-
cmd = `open "${resolved}"`;
|
|
4747
|
-
} else {
|
|
4748
|
-
cmd = `xdg-open "${resolved}"`;
|
|
4749
|
-
}
|
|
4750
|
-
} else {
|
|
4751
|
-
if (platform === "win32") {
|
|
4752
|
-
cmd = `start cmd /k cd /d "${resolved}"`;
|
|
4753
|
-
} else if (platform === "darwin") {
|
|
4754
|
-
cmd = `open -a Terminal "${resolved}"`;
|
|
4755
|
-
} else {
|
|
4756
|
-
cmd = `x-terminal-emulator --working-directory="${resolved}" 2>/dev/null || gnome-terminal --working-directory="${resolved}" 2>/dev/null || xterm -e "cd '${resolved}' && $SHELL"`;
|
|
4757
|
-
}
|
|
4758
|
-
}
|
|
4759
|
-
exec(cmd, { timeout: 5e3 }, (err) => {
|
|
4760
|
-
if (err) {
|
|
4761
|
-
logger.warn(`shell.open failed: ${err.message}`);
|
|
4762
|
-
}
|
|
4763
|
-
});
|
|
4764
|
-
sendResult(ws, true, `Opened ${target} at ${resolved}`);
|
|
4765
|
-
} catch (err) {
|
|
4766
|
-
sendResult(ws, false, errMessage(err));
|
|
4767
|
-
}
|
|
4920
|
+
const result = await handleShellOpen(
|
|
4921
|
+
msg.payload,
|
|
4922
|
+
logger
|
|
4923
|
+
);
|
|
4924
|
+
sendResult(ws, result.success, result.message);
|
|
4768
4925
|
break;
|
|
4769
4926
|
}
|
|
4770
4927
|
// ── Mailbox operations — project-level inter-agent messaging ────
|
|
4771
4928
|
case "mailbox.messages":
|
|
4772
4929
|
return handleMailboxMessages(
|
|
4773
4930
|
ws,
|
|
4774
|
-
{ projectRoot, globalRoot:
|
|
4931
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
4775
4932
|
msg.payload
|
|
4776
4933
|
);
|
|
4777
4934
|
case "mailbox.agents":
|
|
4778
4935
|
return handleMailboxAgents(
|
|
4779
4936
|
ws,
|
|
4780
|
-
{ projectRoot, globalRoot:
|
|
4937
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
4781
4938
|
msg.payload
|
|
4782
4939
|
);
|
|
4783
4940
|
case "mailbox.clear":
|
|
4784
4941
|
return handleMailboxClear(
|
|
4785
4942
|
ws,
|
|
4786
|
-
{ projectRoot, globalRoot:
|
|
4943
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) }
|
|
4787
4944
|
);
|
|
4788
4945
|
// ── Brain — status, autonomy ceiling, direct decision support ───
|
|
4789
4946
|
case "brain.status":
|
|
@@ -4851,10 +5008,12 @@ async function startWebUI(opts = {}) {
|
|
|
4851
5008
|
});
|
|
4852
5009
|
const httpServer = createHttpServer({
|
|
4853
5010
|
host: wsHost,
|
|
4854
|
-
distDir:
|
|
4855
|
-
wsPort
|
|
5011
|
+
distDir: path9.resolve(import.meta.dirname, "../../dist"),
|
|
5012
|
+
wsPort,
|
|
5013
|
+
globalRoot: wpaths.globalRoot,
|
|
5014
|
+
apiToken: wsToken
|
|
4856
5015
|
});
|
|
4857
|
-
const registryBaseDir =
|
|
5016
|
+
const registryBaseDir = path9.dirname(globalConfigPath);
|
|
4858
5017
|
httpServer.listen(httpPort, wsHost, () => {
|
|
4859
5018
|
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
4860
5019
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
@@ -4866,7 +5025,7 @@ async function startWebUI(opts = {}) {
|
|
|
4866
5025
|
wsPort,
|
|
4867
5026
|
host: wsHost,
|
|
4868
5027
|
projectRoot,
|
|
4869
|
-
projectName:
|
|
5028
|
+
projectName: path9.basename(projectRoot) || projectRoot,
|
|
4870
5029
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4871
5030
|
url: `http://${wsHost}:${httpPort}`
|
|
4872
5031
|
},
|
|
@@ -4893,6 +5052,10 @@ async function startWebUI(opts = {}) {
|
|
|
4893
5052
|
// reality. Crash exits are healed by the next register()/list() prune pass.
|
|
4894
5053
|
onShutdown: () => {
|
|
4895
5054
|
brainMonitor.stop();
|
|
5055
|
+
if (eternalSubscription) {
|
|
5056
|
+
eternalSubscription.dispose();
|
|
5057
|
+
eternalSubscription = null;
|
|
5058
|
+
}
|
|
4896
5059
|
return unregisterInstance(process.pid, registryBaseDir);
|
|
4897
5060
|
}
|
|
4898
5061
|
});
|
|
@@ -4904,11 +5067,13 @@ export {
|
|
|
4904
5067
|
browserOpenCommand,
|
|
4905
5068
|
buildCspHeader,
|
|
4906
5069
|
createCustomModeStore,
|
|
5070
|
+
createEternalSubscription,
|
|
4907
5071
|
createHttpServer,
|
|
4908
5072
|
createProviderConfigIO,
|
|
4909
5073
|
defaultBaseDir,
|
|
4910
5074
|
deleteKey,
|
|
4911
5075
|
errMessage,
|
|
5076
|
+
estimateTokens,
|
|
4912
5077
|
extractToken,
|
|
4913
5078
|
findFreePort,
|
|
4914
5079
|
formatInstances,
|
|
@@ -4920,6 +5085,7 @@ export {
|
|
|
4920
5085
|
handleMemoryForget,
|
|
4921
5086
|
handleMemoryList,
|
|
4922
5087
|
handleMemoryRemember,
|
|
5088
|
+
handleShellOpen,
|
|
4923
5089
|
hostHeaderOk,
|
|
4924
5090
|
injectWsPort,
|
|
4925
5091
|
isLoopbackBind,
|
|
@@ -4928,6 +5094,8 @@ export {
|
|
|
4928
5094
|
listInstances,
|
|
4929
5095
|
loadSavedProviders,
|
|
4930
5096
|
maskedKey,
|
|
5097
|
+
messagePreview,
|
|
5098
|
+
messageTokens,
|
|
4931
5099
|
normalizeKeys,
|
|
4932
5100
|
openBrowser,
|
|
4933
5101
|
registerInstance,
|
|
@@ -4938,6 +5106,7 @@ export {
|
|
|
4938
5106
|
sendResult,
|
|
4939
5107
|
setActiveKey,
|
|
4940
5108
|
startWebUI,
|
|
5109
|
+
stringifyContent,
|
|
4941
5110
|
tokenMatches,
|
|
4942
5111
|
unregisterInstance,
|
|
4943
5112
|
upsertKey,
|