@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/entry.js
CHANGED
|
@@ -16,13 +16,99 @@ import {
|
|
|
16
16
|
createAutonomyBrain,
|
|
17
17
|
createTieredBrainArbiter
|
|
18
18
|
} from "@wrongstack/core";
|
|
19
|
-
import * as
|
|
20
|
-
import * as
|
|
19
|
+
import * as fs7 from "fs/promises";
|
|
20
|
+
import * as path9 from "path";
|
|
21
21
|
|
|
22
22
|
// src/server/http-server.ts
|
|
23
23
|
import * as fs from "fs/promises";
|
|
24
24
|
import * as http from "http";
|
|
25
25
|
import * as path from "path";
|
|
26
|
+
|
|
27
|
+
// src/server/ws-auth.ts
|
|
28
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
29
|
+
import { timingSafeEqual } from "crypto";
|
|
30
|
+
function isLoopbackHostname(hostname) {
|
|
31
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
32
|
+
}
|
|
33
|
+
function isTrustedLoopbackOrigin(origin) {
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(origin);
|
|
36
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
37
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isLoopbackBind(wsHost) {
|
|
43
|
+
return wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
|
|
44
|
+
}
|
|
45
|
+
function tokenMatches(provided, expected) {
|
|
46
|
+
if (!provided) return false;
|
|
47
|
+
const a = Buffer2.from(provided);
|
|
48
|
+
const b = Buffer2.from(expected);
|
|
49
|
+
if (a.length !== b.length) return false;
|
|
50
|
+
return timingSafeEqual(a, b);
|
|
51
|
+
}
|
|
52
|
+
function extractToken(url) {
|
|
53
|
+
const match = url.match(/[?&]token=([^&]+)/);
|
|
54
|
+
return match ? match[1] : void 0;
|
|
55
|
+
}
|
|
56
|
+
function extractTokenFromCookie(cookieHeader) {
|
|
57
|
+
if (!cookieHeader) return void 0;
|
|
58
|
+
const raw = Array.isArray(cookieHeader) ? cookieHeader.join("; ") : cookieHeader;
|
|
59
|
+
for (const part of raw.split(";")) {
|
|
60
|
+
const eq = part.indexOf("=");
|
|
61
|
+
if (eq < 0) continue;
|
|
62
|
+
const name = part.slice(0, eq).trim();
|
|
63
|
+
if (name === "ws_token") {
|
|
64
|
+
try {
|
|
65
|
+
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
66
|
+
} catch {
|
|
67
|
+
return part.slice(eq + 1).trim();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
function hostHeaderOk(input) {
|
|
74
|
+
if (!isLoopbackBind(input.wsHost)) return true;
|
|
75
|
+
const hostHeader = (input.hostHeader ?? "").trim();
|
|
76
|
+
if (!hostHeader) return false;
|
|
77
|
+
let hostname;
|
|
78
|
+
try {
|
|
79
|
+
hostname = new URL(`http://${hostHeader}`).hostname;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return isLoopbackHostname(hostname);
|
|
84
|
+
}
|
|
85
|
+
function verifyClient(input) {
|
|
86
|
+
const { origin, url, hostHeader, remoteAddress, cookieHeader, wsHost, expectedToken } = input;
|
|
87
|
+
const urlToken = extractToken(url ?? "");
|
|
88
|
+
const cookieToken = extractTokenFromCookie(cookieHeader);
|
|
89
|
+
const tokenOk = tokenMatches(urlToken, expectedToken) || tokenMatches(cookieToken, expectedToken);
|
|
90
|
+
if (!hostHeaderOk({ hostHeader, wsHost })) return false;
|
|
91
|
+
if (!origin) {
|
|
92
|
+
const remoteIp = remoteAddress ?? "";
|
|
93
|
+
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
94
|
+
if (!isRemoteLoopback && wsHost === "0.0.0.0") return false;
|
|
95
|
+
return tokenOk || isLoopbackBind(wsHost);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const { hostname } = new URL(origin);
|
|
99
|
+
if (isLoopbackHostname(hostname)) {
|
|
100
|
+
if (wsHost === "0.0.0.0" && !isTrustedLoopbackOrigin(origin)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return tokenOk;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/server/http-server.ts
|
|
26
112
|
var MIME_TYPES = {
|
|
27
113
|
".html": "text/html",
|
|
28
114
|
".js": "application/javascript",
|
|
@@ -54,15 +140,47 @@ function createHttpServer(opts) {
|
|
|
54
140
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
55
141
|
const distDir = path.resolve(opts.distDir);
|
|
56
142
|
const wsPort = opts.wsPort;
|
|
143
|
+
const requireApiToken = !isLoopbackBind(opts.host) && Boolean(opts.apiToken);
|
|
57
144
|
return http.createServer(async (req, res) => {
|
|
58
145
|
try {
|
|
59
146
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
147
|
+
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
148
|
+
const provided = url.searchParams.get("token") ?? req.headers["x-ws-token"];
|
|
149
|
+
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
150
|
+
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
151
|
+
res.end("Unauthorized");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
res.writeHead(200, {
|
|
155
|
+
"Content-Type": "text/plain",
|
|
156
|
+
"Set-Cookie": `ws_token=${encodeURIComponent(opts.apiToken)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600`,
|
|
157
|
+
// Belt-and-braces: tell any caches the cookie response itself
|
|
158
|
+
// is sensitive.
|
|
159
|
+
"Cache-Control": "no-store"
|
|
160
|
+
});
|
|
161
|
+
res.end("ok");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
60
164
|
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
165
|
+
const headerToken = req.headers["x-ws-token"];
|
|
166
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
167
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
168
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
169
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
61
172
|
await handleApiSessions(res, opts.globalRoot);
|
|
62
173
|
return;
|
|
63
174
|
}
|
|
64
175
|
const agentsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/agents$/);
|
|
65
176
|
if (agentsMatch && req.method === "GET") {
|
|
177
|
+
const headerToken = req.headers["x-ws-token"];
|
|
178
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
179
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
180
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
181
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
66
184
|
await handleApiSessionAgents(res, opts.globalRoot, agentsMatch[1]);
|
|
67
185
|
return;
|
|
68
186
|
}
|
|
@@ -1800,71 +1918,6 @@ async function handleMailboxClear(ws, deps) {
|
|
|
1800
1918
|
}
|
|
1801
1919
|
}
|
|
1802
1920
|
|
|
1803
|
-
// src/server/ws-auth.ts
|
|
1804
|
-
import { Buffer as Buffer2 } from "buffer";
|
|
1805
|
-
import { timingSafeEqual } from "crypto";
|
|
1806
|
-
function isLoopbackHostname(hostname) {
|
|
1807
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1808
|
-
}
|
|
1809
|
-
function isTrustedLoopbackOrigin(origin) {
|
|
1810
|
-
try {
|
|
1811
|
-
const url = new URL(origin);
|
|
1812
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
1813
|
-
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
1814
|
-
} catch {
|
|
1815
|
-
return false;
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
function isLoopbackBind(wsHost) {
|
|
1819
|
-
return wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
|
|
1820
|
-
}
|
|
1821
|
-
function tokenMatches(provided, expected) {
|
|
1822
|
-
if (!provided) return false;
|
|
1823
|
-
const a = Buffer2.from(provided);
|
|
1824
|
-
const b = Buffer2.from(expected);
|
|
1825
|
-
if (a.length !== b.length) return false;
|
|
1826
|
-
return timingSafeEqual(a, b);
|
|
1827
|
-
}
|
|
1828
|
-
function extractToken(url) {
|
|
1829
|
-
const match = url.match(/[?&]token=([^&]+)/);
|
|
1830
|
-
return match ? match[1] : void 0;
|
|
1831
|
-
}
|
|
1832
|
-
function hostHeaderOk(input) {
|
|
1833
|
-
if (!isLoopbackBind(input.wsHost)) return true;
|
|
1834
|
-
const hostHeader = (input.hostHeader ?? "").trim();
|
|
1835
|
-
if (!hostHeader) return false;
|
|
1836
|
-
let hostname;
|
|
1837
|
-
try {
|
|
1838
|
-
hostname = new URL(`http://${hostHeader}`).hostname;
|
|
1839
|
-
} catch {
|
|
1840
|
-
return false;
|
|
1841
|
-
}
|
|
1842
|
-
return isLoopbackHostname(hostname);
|
|
1843
|
-
}
|
|
1844
|
-
function verifyClient(input) {
|
|
1845
|
-
const { origin, url, hostHeader, remoteAddress, wsHost, expectedToken } = input;
|
|
1846
|
-
const tokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
1847
|
-
if (!hostHeaderOk({ hostHeader, wsHost })) return false;
|
|
1848
|
-
if (!origin) {
|
|
1849
|
-
const remoteIp = remoteAddress ?? "";
|
|
1850
|
-
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
1851
|
-
if (!isRemoteLoopback && wsHost === "0.0.0.0") return false;
|
|
1852
|
-
return tokenOk || isLoopbackBind(wsHost);
|
|
1853
|
-
}
|
|
1854
|
-
try {
|
|
1855
|
-
const { hostname } = new URL(origin);
|
|
1856
|
-
if (isLoopbackHostname(hostname)) {
|
|
1857
|
-
if (wsHost === "0.0.0.0" && !isTrustedLoopbackOrigin(origin)) {
|
|
1858
|
-
return false;
|
|
1859
|
-
}
|
|
1860
|
-
return true;
|
|
1861
|
-
}
|
|
1862
|
-
return tokenOk;
|
|
1863
|
-
} catch {
|
|
1864
|
-
return false;
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
1921
|
// src/server/lifecycle.ts
|
|
1869
1922
|
function createShutdown(res) {
|
|
1870
1923
|
const log = res.log ?? ((m) => console.log(m));
|
|
@@ -1982,16 +2035,16 @@ function formatInstances(instances) {
|
|
|
1982
2035
|
// src/server/port-utils.ts
|
|
1983
2036
|
import * as net from "net";
|
|
1984
2037
|
function isPortFree(host, port) {
|
|
1985
|
-
return new Promise((
|
|
2038
|
+
return new Promise((resolve6) => {
|
|
1986
2039
|
const srv = net.createServer();
|
|
1987
|
-
srv.once("error", () =>
|
|
2040
|
+
srv.once("error", () => resolve6(false));
|
|
1988
2041
|
srv.once("listening", () => {
|
|
1989
|
-
srv.close(() =>
|
|
2042
|
+
srv.close(() => resolve6(true));
|
|
1990
2043
|
});
|
|
1991
2044
|
try {
|
|
1992
2045
|
srv.listen(port, host);
|
|
1993
2046
|
} catch {
|
|
1994
|
-
|
|
2047
|
+
resolve6(false);
|
|
1995
2048
|
}
|
|
1996
2049
|
});
|
|
1997
2050
|
}
|
|
@@ -2722,6 +2775,79 @@ function estimateContextBreakdown(input) {
|
|
|
2722
2775
|
};
|
|
2723
2776
|
}
|
|
2724
2777
|
|
|
2778
|
+
// src/server/eternal-iteration-broadcast.ts
|
|
2779
|
+
function createEternalSubscription(subscribe, broadcast2, clientsRef) {
|
|
2780
|
+
let disposed = false;
|
|
2781
|
+
const dispose = subscribe((entry) => {
|
|
2782
|
+
if (disposed) return;
|
|
2783
|
+
broadcast2(clientsRef(), {
|
|
2784
|
+
type: "eternal.iteration",
|
|
2785
|
+
payload: { entry }
|
|
2786
|
+
});
|
|
2787
|
+
});
|
|
2788
|
+
return {
|
|
2789
|
+
dispose() {
|
|
2790
|
+
if (disposed) return;
|
|
2791
|
+
disposed = true;
|
|
2792
|
+
dispose();
|
|
2793
|
+
}
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
// src/server/shell-open.ts
|
|
2798
|
+
import * as fs6 from "fs/promises";
|
|
2799
|
+
import * as path8 from "path";
|
|
2800
|
+
import { spawn as spawn2 } from "child_process";
|
|
2801
|
+
var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
|
|
2802
|
+
async function handleShellOpen(req, logger) {
|
|
2803
|
+
try {
|
|
2804
|
+
const resolved = path8.resolve(req.path);
|
|
2805
|
+
await fs6.access(resolved);
|
|
2806
|
+
if (METACHAR_REGEX.test(resolved)) {
|
|
2807
|
+
return { success: false, message: "Path contains unsupported characters." };
|
|
2808
|
+
}
|
|
2809
|
+
const platform = process.platform;
|
|
2810
|
+
const launch = (cmd, args, onError) => {
|
|
2811
|
+
const child = spawn2(cmd, args, {
|
|
2812
|
+
detached: true,
|
|
2813
|
+
stdio: "ignore",
|
|
2814
|
+
windowsHide: true
|
|
2815
|
+
});
|
|
2816
|
+
child.on("error", (err) => {
|
|
2817
|
+
logger.warn(`shell.open spawn failed: ${err.message}`);
|
|
2818
|
+
onError?.();
|
|
2819
|
+
});
|
|
2820
|
+
child.unref();
|
|
2821
|
+
};
|
|
2822
|
+
if (req.target === "file-manager") {
|
|
2823
|
+
if (platform === "win32") launch("explorer", [resolved]);
|
|
2824
|
+
else if (platform === "darwin") launch("open", [resolved]);
|
|
2825
|
+
else launch("xdg-open", [resolved]);
|
|
2826
|
+
} else if (req.target === "terminal") {
|
|
2827
|
+
if (platform === "win32") {
|
|
2828
|
+
launch("cmd", ["/c", "start", "cmd", "/k", "cd", "/d", resolved]);
|
|
2829
|
+
} else if (platform === "darwin") {
|
|
2830
|
+
launch("open", ["-a", "Terminal", resolved]);
|
|
2831
|
+
} else {
|
|
2832
|
+
launch(
|
|
2833
|
+
"x-terminal-emulator",
|
|
2834
|
+
[`--working-directory=${resolved}`],
|
|
2835
|
+
() => launch(
|
|
2836
|
+
"gnome-terminal",
|
|
2837
|
+
[`--working-directory=${resolved}`],
|
|
2838
|
+
() => launch("xterm", ["-e", `cd '${resolved}' && ${process.env["SHELL"] ?? "sh"}`])
|
|
2839
|
+
)
|
|
2840
|
+
);
|
|
2841
|
+
}
|
|
2842
|
+
} else {
|
|
2843
|
+
return { success: false, message: `Unknown shell.open target: ${String(req.target)}` };
|
|
2844
|
+
}
|
|
2845
|
+
return { success: true, message: `Opened ${req.target} at ${resolved}` };
|
|
2846
|
+
} catch (err) {
|
|
2847
|
+
return { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2725
2851
|
// src/server/index.ts
|
|
2726
2852
|
async function startWebUI(opts = {}) {
|
|
2727
2853
|
const requestedWsPort = opts.wsPort ?? 3457;
|
|
@@ -2756,7 +2882,8 @@ async function startWebUI(opts = {}) {
|
|
|
2756
2882
|
}
|
|
2757
2883
|
console.log("[WebUI] Starting backend services...");
|
|
2758
2884
|
const boot = await bootConfig();
|
|
2759
|
-
const { config: baseConfig,
|
|
2885
|
+
const { config: baseConfig, globalConfigPath, wpaths, logger } = boot;
|
|
2886
|
+
const vault = opts.services?.vault ?? boot.vault;
|
|
2760
2887
|
let config = baseConfig;
|
|
2761
2888
|
let projectRoot = boot.projectRoot;
|
|
2762
2889
|
let workingDir = projectRoot;
|
|
@@ -2768,12 +2895,12 @@ async function startWebUI(opts = {}) {
|
|
|
2768
2895
|
console.log("[WebUI] No active provider \u2014 auto-selected:", firstKey);
|
|
2769
2896
|
}
|
|
2770
2897
|
const needsProvider = !config.provider || !config.model;
|
|
2771
|
-
const modelsRegistry = new DefaultModelsRegistry({
|
|
2898
|
+
const modelsRegistry = opts.services?.modelsRegistry ?? new DefaultModelsRegistry({
|
|
2772
2899
|
cacheFile: wpaths.modelsCache,
|
|
2773
2900
|
ttlSeconds: 24 * 3600
|
|
2774
2901
|
});
|
|
2775
2902
|
const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry });
|
|
2776
|
-
const configStore = container.resolve(TOKENS2.ConfigStore);
|
|
2903
|
+
const configStore = opts.services?.configStore ?? container.resolve(TOKENS2.ConfigStore);
|
|
2777
2904
|
const providerRegistry = new ProviderRegistry();
|
|
2778
2905
|
try {
|
|
2779
2906
|
const factories = await buildProviderFactoriesFromRegistry({
|
|
@@ -2790,8 +2917,11 @@ async function startWebUI(opts = {}) {
|
|
|
2790
2917
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2791
2918
|
}));
|
|
2792
2919
|
}
|
|
2793
|
-
const toolRegistry =
|
|
2794
|
-
|
|
2920
|
+
const toolRegistry = opts.services?.toolRegistry ?? (() => {
|
|
2921
|
+
const r = new ToolRegistry();
|
|
2922
|
+
r.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
|
|
2923
|
+
return r;
|
|
2924
|
+
})();
|
|
2795
2925
|
const memoryStore = new DefaultMemoryStore2({ paths: wpaths });
|
|
2796
2926
|
if (config.features.memory) {
|
|
2797
2927
|
toolRegistry.register(rememberTool(memoryStore));
|
|
@@ -2799,16 +2929,18 @@ async function startWebUI(opts = {}) {
|
|
|
2799
2929
|
toolRegistry.register(searchMemoryTool(memoryStore));
|
|
2800
2930
|
toolRegistry.register(relatedMemoryTool(memoryStore));
|
|
2801
2931
|
}
|
|
2802
|
-
const events = new EventBus();
|
|
2932
|
+
const events = opts.services?.events ?? new EventBus();
|
|
2803
2933
|
events.setLogger(logger);
|
|
2804
2934
|
toolRegistry.register(makeMailboxTool({ projectDir: wpaths.projectDir, events }));
|
|
2805
2935
|
toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
|
|
2806
2936
|
toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
|
|
2807
2937
|
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
2808
|
-
let sessionStore = new DefaultSessionStore2({ dir: wpaths.projectSessions });
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2938
|
+
let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
|
|
2939
|
+
if (!opts.services?.session) {
|
|
2940
|
+
sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
|
|
2941
|
+
if (count > 0) logger.info(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
|
|
2942
|
+
}).catch(() => void 0);
|
|
2943
|
+
}
|
|
2812
2944
|
const sessionReader = new DefaultSessionReader({ store: sessionStore });
|
|
2813
2945
|
const annotationsStore = new AnnotationsStore({ dir: wpaths.projectSessions });
|
|
2814
2946
|
let session = await sessionStore.create({
|
|
@@ -2830,7 +2962,7 @@ async function startWebUI(opts = {}) {
|
|
|
2830
2962
|
sessionId: session.id,
|
|
2831
2963
|
projectSlug: wpaths.projectSlug,
|
|
2832
2964
|
projectRoot,
|
|
2833
|
-
projectName:
|
|
2965
|
+
projectName: path9.basename(projectRoot),
|
|
2834
2966
|
workingDir,
|
|
2835
2967
|
pid: process.pid,
|
|
2836
2968
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -3025,7 +3157,7 @@ async function startWebUI(opts = {}) {
|
|
|
3025
3157
|
const write = async () => {
|
|
3026
3158
|
let raw;
|
|
3027
3159
|
try {
|
|
3028
|
-
raw = await
|
|
3160
|
+
raw = await fs7.readFile(globalConfigPath, "utf8");
|
|
3029
3161
|
} catch {
|
|
3030
3162
|
raw = "{}";
|
|
3031
3163
|
}
|
|
@@ -3296,6 +3428,14 @@ async function startWebUI(opts = {}) {
|
|
|
3296
3428
|
try {
|
|
3297
3429
|
const m = await modelsRegistry.getModel(config.provider, config.model);
|
|
3298
3430
|
maxContext = m?.capabilities?.maxContext ?? 0;
|
|
3431
|
+
if (!maxContext) {
|
|
3432
|
+
try {
|
|
3433
|
+
const provider2 = await modelsRegistry.getProvider(config.provider);
|
|
3434
|
+
const rawModel = provider2?.models.find((mod) => mod.id === config.model);
|
|
3435
|
+
maxContext = rawModel?.limit?.context ?? 0;
|
|
3436
|
+
} catch {
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3299
3439
|
const rates = getCostRates(m);
|
|
3300
3440
|
inputCost = rates.input;
|
|
3301
3441
|
outputCost = rates.output;
|
|
@@ -3310,12 +3450,11 @@ async function startWebUI(opts = {}) {
|
|
|
3310
3450
|
inputCost,
|
|
3311
3451
|
outputCost,
|
|
3312
3452
|
cacheReadCost,
|
|
3313
|
-
projectName:
|
|
3453
|
+
projectName: path9.basename(projectRoot) || projectRoot,
|
|
3314
3454
|
projectRoot,
|
|
3315
3455
|
cwd: workingDir,
|
|
3316
3456
|
mode: modeId,
|
|
3317
|
-
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID)
|
|
3318
|
-
wsToken
|
|
3457
|
+
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID)
|
|
3319
3458
|
};
|
|
3320
3459
|
}
|
|
3321
3460
|
const wsToken = generateAuthToken();
|
|
@@ -3325,6 +3464,11 @@ async function startWebUI(opts = {}) {
|
|
|
3325
3464
|
url: info.req.url ?? "",
|
|
3326
3465
|
hostHeader: info.req.headers.host,
|
|
3327
3466
|
remoteAddress: info.req.socket.remoteAddress,
|
|
3467
|
+
// C-2 fix: accept the token via the HttpOnly cookie set by
|
|
3468
|
+
// `/ws-auth` (preferred) OR the URL query param (non-browser
|
|
3469
|
+
// fallback). The cookie path closes the C-598 query-string
|
|
3470
|
+
// exposure class.
|
|
3471
|
+
cookieHeader: info.req.headers.cookie,
|
|
3328
3472
|
wsHost,
|
|
3329
3473
|
expectedToken: wsToken
|
|
3330
3474
|
});
|
|
@@ -3349,6 +3493,14 @@ async function startWebUI(opts = {}) {
|
|
|
3349
3493
|
payload: { cwd: newDir, projectRoot }
|
|
3350
3494
|
});
|
|
3351
3495
|
});
|
|
3496
|
+
let eternalSubscription = null;
|
|
3497
|
+
if (opts.subscribeEternalIteration) {
|
|
3498
|
+
eternalSubscription = createEternalSubscription(
|
|
3499
|
+
opts.subscribeEternalIteration,
|
|
3500
|
+
broadcast,
|
|
3501
|
+
() => clients
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3352
3504
|
const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
|
|
3353
3505
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
3354
3506
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
@@ -3425,8 +3577,8 @@ async function startWebUI(opts = {}) {
|
|
|
3425
3577
|
clients.delete(ws);
|
|
3426
3578
|
rateLimits.delete(String(ws));
|
|
3427
3579
|
if (pendingConfirms.size > 0) {
|
|
3428
|
-
for (const [id,
|
|
3429
|
-
|
|
3580
|
+
for (const [id, resolve6] of pendingConfirms) {
|
|
3581
|
+
resolve6("no");
|
|
3430
3582
|
pendingConfirms.delete(id);
|
|
3431
3583
|
}
|
|
3432
3584
|
}
|
|
@@ -3490,33 +3642,33 @@ async function startWebUI(opts = {}) {
|
|
|
3490
3642
|
});
|
|
3491
3643
|
}
|
|
3492
3644
|
async function touchProjectEntry(root, workDir) {
|
|
3493
|
-
const resolved =
|
|
3645
|
+
const resolved = path9.resolve(root);
|
|
3494
3646
|
const manifest = await loadManifest(globalConfigPath);
|
|
3495
3647
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3496
|
-
const existing = manifest.projects.find((p) =>
|
|
3648
|
+
const existing = manifest.projects.find((p) => path9.resolve(p.root) === resolved);
|
|
3497
3649
|
if (existing) {
|
|
3498
3650
|
existing.lastSeen = now;
|
|
3499
|
-
if (workDir) existing.lastWorkingDir =
|
|
3651
|
+
if (workDir) existing.lastWorkingDir = path9.resolve(workDir);
|
|
3500
3652
|
} else {
|
|
3501
3653
|
manifest.projects.push({
|
|
3502
|
-
name:
|
|
3654
|
+
name: path9.basename(resolved),
|
|
3503
3655
|
root: resolved,
|
|
3504
3656
|
slug: generateProjectSlug(resolved),
|
|
3505
3657
|
createdAt: now,
|
|
3506
3658
|
lastSeen: now,
|
|
3507
|
-
lastWorkingDir: workDir ?
|
|
3659
|
+
lastWorkingDir: workDir ? path9.resolve(workDir) : void 0
|
|
3508
3660
|
});
|
|
3509
3661
|
}
|
|
3510
3662
|
await saveManifest(manifest, globalConfigPath);
|
|
3511
3663
|
await ensureProjectDataDir(generateProjectSlug(resolved), globalConfigPath);
|
|
3512
3664
|
}
|
|
3513
3665
|
function projectsJsonPath(globalConfigPath2) {
|
|
3514
|
-
const base =
|
|
3515
|
-
return
|
|
3666
|
+
const base = path9.dirname(globalConfigPath2);
|
|
3667
|
+
return path9.join(base, "projects.json");
|
|
3516
3668
|
}
|
|
3517
3669
|
async function loadManifest(globalConfigPath2) {
|
|
3518
3670
|
try {
|
|
3519
|
-
const raw = await
|
|
3671
|
+
const raw = await fs7.readFile(projectsJsonPath(globalConfigPath2), "utf8");
|
|
3520
3672
|
const parsed = JSON.parse(raw);
|
|
3521
3673
|
return { projects: parsed.projects ?? [] };
|
|
3522
3674
|
} catch {
|
|
@@ -3525,16 +3677,16 @@ async function startWebUI(opts = {}) {
|
|
|
3525
3677
|
}
|
|
3526
3678
|
async function saveManifest(manifest, globalConfigPath2) {
|
|
3527
3679
|
const file = projectsJsonPath(globalConfigPath2);
|
|
3528
|
-
await
|
|
3529
|
-
await
|
|
3680
|
+
await fs7.mkdir(path9.dirname(file), { recursive: true });
|
|
3681
|
+
await fs7.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
3530
3682
|
}
|
|
3531
3683
|
function generateProjectSlug(rootPath) {
|
|
3532
3684
|
return projectSlug(rootPath);
|
|
3533
3685
|
}
|
|
3534
3686
|
async function ensureProjectDataDir(slug, globalConfigPath2) {
|
|
3535
|
-
const base =
|
|
3536
|
-
const dir =
|
|
3537
|
-
await
|
|
3687
|
+
const base = path9.dirname(globalConfigPath2);
|
|
3688
|
+
const dir = path9.join(base, "projects", slug);
|
|
3689
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
3538
3690
|
return dir;
|
|
3539
3691
|
}
|
|
3540
3692
|
async function handleMessage(ws, _client, msg) {
|
|
@@ -3596,10 +3748,10 @@ async function startWebUI(opts = {}) {
|
|
|
3596
3748
|
}
|
|
3597
3749
|
case "tool.confirm_result": {
|
|
3598
3750
|
const { id, decision } = msg.payload;
|
|
3599
|
-
const
|
|
3600
|
-
if (
|
|
3751
|
+
const resolve6 = pendingConfirms.get(id);
|
|
3752
|
+
if (resolve6) {
|
|
3601
3753
|
pendingConfirms.delete(id);
|
|
3602
|
-
|
|
3754
|
+
resolve6(decision);
|
|
3603
3755
|
}
|
|
3604
3756
|
break;
|
|
3605
3757
|
}
|
|
@@ -3873,7 +4025,7 @@ async function startWebUI(opts = {}) {
|
|
|
3873
4025
|
updateAutoCompactionMaxContext?.(newProv);
|
|
3874
4026
|
try {
|
|
3875
4027
|
configWriteLock = configWriteLock.then(async () => {
|
|
3876
|
-
const raw = await
|
|
4028
|
+
const raw = await fs7.readFile(globalConfigPath, "utf8");
|
|
3877
4029
|
const parsed = JSON.parse(raw);
|
|
3878
4030
|
parsed.provider = newProvider;
|
|
3879
4031
|
parsed.model = newModel;
|
|
@@ -4419,8 +4571,8 @@ async function startWebUI(opts = {}) {
|
|
|
4419
4571
|
}
|
|
4420
4572
|
case "goal.get": {
|
|
4421
4573
|
try {
|
|
4422
|
-
const goalPath =
|
|
4423
|
-
const raw = await
|
|
4574
|
+
const goalPath = path9.join(projectRoot, ".wrongstack", "goal.json");
|
|
4575
|
+
const raw = await fs7.readFile(goalPath, "utf8");
|
|
4424
4576
|
const goal = JSON.parse(raw);
|
|
4425
4577
|
broadcast(clients, { type: "goal.updated", payload: goal });
|
|
4426
4578
|
} catch {
|
|
@@ -4480,7 +4632,7 @@ async function startWebUI(opts = {}) {
|
|
|
4480
4632
|
try {
|
|
4481
4633
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4482
4634
|
const rewinder = new DefaultSessionRewinder(
|
|
4483
|
-
|
|
4635
|
+
path9.join(projectRoot, ".wrongstack", "sessions"),
|
|
4484
4636
|
projectRoot
|
|
4485
4637
|
);
|
|
4486
4638
|
const checkpoints = await rewinder.listCheckpoints(session.id);
|
|
@@ -4501,7 +4653,7 @@ async function startWebUI(opts = {}) {
|
|
|
4501
4653
|
try {
|
|
4502
4654
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4503
4655
|
const rewinder = new DefaultSessionRewinder(
|
|
4504
|
-
|
|
4656
|
+
path9.join(projectRoot, ".wrongstack", "sessions"),
|
|
4505
4657
|
projectRoot
|
|
4506
4658
|
);
|
|
4507
4659
|
await rewinder.rewindToCheckpoint(session.id, checkpointIndex);
|
|
@@ -4535,9 +4687,9 @@ async function startWebUI(opts = {}) {
|
|
|
4535
4687
|
case "projects.add": {
|
|
4536
4688
|
const { root: addRoot, name: displayName } = msg.payload;
|
|
4537
4689
|
try {
|
|
4538
|
-
const resolved =
|
|
4539
|
-
await
|
|
4540
|
-
const stat2 = await
|
|
4690
|
+
const resolved = path9.resolve(addRoot);
|
|
4691
|
+
await fs7.access(resolved);
|
|
4692
|
+
const stat2 = await fs7.stat(resolved);
|
|
4541
4693
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4542
4694
|
const manifest = await loadManifest(globalConfigPath);
|
|
4543
4695
|
const existing = manifest.projects.find((p) => p.root === resolved);
|
|
@@ -4553,7 +4705,7 @@ async function startWebUI(opts = {}) {
|
|
|
4553
4705
|
});
|
|
4554
4706
|
break;
|
|
4555
4707
|
}
|
|
4556
|
-
const name = displayName?.trim() ||
|
|
4708
|
+
const name = displayName?.trim() || path9.basename(resolved);
|
|
4557
4709
|
const slug = generateProjectSlug(resolved);
|
|
4558
4710
|
await ensureProjectDataDir(slug, globalConfigPath);
|
|
4559
4711
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -4572,7 +4724,7 @@ async function startWebUI(opts = {}) {
|
|
|
4572
4724
|
send(ws, {
|
|
4573
4725
|
type: "projects.added",
|
|
4574
4726
|
payload: {
|
|
4575
|
-
name:
|
|
4727
|
+
name: path9.basename(addRoot),
|
|
4576
4728
|
root: addRoot,
|
|
4577
4729
|
slug: "",
|
|
4578
4730
|
message: errMessage(err)
|
|
@@ -4584,17 +4736,17 @@ async function startWebUI(opts = {}) {
|
|
|
4584
4736
|
case "projects.select": {
|
|
4585
4737
|
const { root: selRoot, name: selName } = msg.payload;
|
|
4586
4738
|
try {
|
|
4587
|
-
const resolved =
|
|
4739
|
+
const resolved = path9.resolve(selRoot);
|
|
4588
4740
|
try {
|
|
4589
|
-
await
|
|
4590
|
-
const stat2 = await
|
|
4741
|
+
await fs7.access(resolved);
|
|
4742
|
+
const stat2 = await fs7.stat(resolved);
|
|
4591
4743
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4592
4744
|
} catch (err) {
|
|
4593
4745
|
send(ws, {
|
|
4594
4746
|
type: "projects.selected",
|
|
4595
4747
|
payload: {
|
|
4596
4748
|
root: selRoot,
|
|
4597
|
-
name: selName ||
|
|
4749
|
+
name: selName || path9.basename(selRoot),
|
|
4598
4750
|
message: `Cannot switch: ${errMessage(err)}`
|
|
4599
4751
|
}
|
|
4600
4752
|
});
|
|
@@ -4606,7 +4758,7 @@ async function startWebUI(opts = {}) {
|
|
|
4606
4758
|
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
4607
4759
|
entry.lastWorkingDir = resolved;
|
|
4608
4760
|
} else {
|
|
4609
|
-
const name = selName?.trim() ||
|
|
4761
|
+
const name = selName?.trim() || path9.basename(resolved);
|
|
4610
4762
|
const slug = generateProjectSlug(resolved);
|
|
4611
4763
|
manifest.projects.push({
|
|
4612
4764
|
name,
|
|
@@ -4627,13 +4779,33 @@ async function startWebUI(opts = {}) {
|
|
|
4627
4779
|
workingDir = resolved;
|
|
4628
4780
|
context.cwd = workingDir;
|
|
4629
4781
|
context.projectRoot = projectRoot;
|
|
4630
|
-
const
|
|
4631
|
-
|
|
4782
|
+
const switchSlug = entry?.slug ?? generateProjectSlug(resolved);
|
|
4783
|
+
try {
|
|
4784
|
+
const switchMode = modeId === "default" ? void 0 : await modeStore.getMode(modeId);
|
|
4785
|
+
const switchBuilder = new DefaultSystemPromptBuilder2({
|
|
4786
|
+
memoryStore,
|
|
4787
|
+
skillLoader,
|
|
4788
|
+
modeStore,
|
|
4789
|
+
modeId,
|
|
4790
|
+
modePrompt: switchMode?.prompt ?? "",
|
|
4791
|
+
modelCapabilities
|
|
4792
|
+
});
|
|
4793
|
+
context.systemPrompt = await switchBuilder.build({
|
|
4794
|
+
cwd: workingDir,
|
|
4795
|
+
projectRoot,
|
|
4796
|
+
tools: toolRegistry.list(),
|
|
4797
|
+
provider: config.provider,
|
|
4798
|
+
model: config.model
|
|
4799
|
+
});
|
|
4800
|
+
} catch {
|
|
4801
|
+
}
|
|
4802
|
+
const newSessionsDir = path9.join(
|
|
4803
|
+
path9.dirname(globalConfigPath),
|
|
4632
4804
|
"projects",
|
|
4633
|
-
|
|
4805
|
+
switchSlug,
|
|
4634
4806
|
"sessions"
|
|
4635
4807
|
);
|
|
4636
|
-
await
|
|
4808
|
+
await fs7.mkdir(newSessionsDir, { recursive: true });
|
|
4637
4809
|
const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
|
|
4638
4810
|
const oldSessionId = session.id;
|
|
4639
4811
|
try {
|
|
@@ -4659,12 +4831,25 @@ async function startWebUI(opts = {}) {
|
|
|
4659
4831
|
context.fileMtimes.clear();
|
|
4660
4832
|
tokenCounter.reset();
|
|
4661
4833
|
sessionStartedAt = Date.now();
|
|
4834
|
+
try {
|
|
4835
|
+
const registry = getSessionRegistry(wpaths.globalRoot);
|
|
4836
|
+
await registry.register({
|
|
4837
|
+
sessionId: session.id,
|
|
4838
|
+
projectSlug: switchSlug,
|
|
4839
|
+
projectRoot,
|
|
4840
|
+
projectName: path9.basename(projectRoot),
|
|
4841
|
+
workingDir,
|
|
4842
|
+
pid: process.pid,
|
|
4843
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4844
|
+
});
|
|
4845
|
+
} catch {
|
|
4846
|
+
}
|
|
4662
4847
|
send(ws, {
|
|
4663
4848
|
type: "projects.selected",
|
|
4664
4849
|
payload: {
|
|
4665
4850
|
root: resolved,
|
|
4666
|
-
name: selName ||
|
|
4667
|
-
message: `Switched to ${selName ||
|
|
4851
|
+
name: selName || path9.basename(resolved),
|
|
4852
|
+
message: `Switched to ${selName || path9.basename(resolved)}`
|
|
4668
4853
|
}
|
|
4669
4854
|
});
|
|
4670
4855
|
broadcast(clients, {
|
|
@@ -4687,7 +4872,7 @@ async function startWebUI(opts = {}) {
|
|
|
4687
4872
|
type: "projects.selected",
|
|
4688
4873
|
payload: {
|
|
4689
4874
|
root: selRoot,
|
|
4690
|
-
name: selName ||
|
|
4875
|
+
name: selName || path9.basename(selRoot),
|
|
4691
4876
|
message: errMessage(err)
|
|
4692
4877
|
}
|
|
4693
4878
|
});
|
|
@@ -4698,14 +4883,14 @@ async function startWebUI(opts = {}) {
|
|
|
4698
4883
|
case "working_dir.set": {
|
|
4699
4884
|
const { path: newPath } = msg.payload;
|
|
4700
4885
|
try {
|
|
4701
|
-
const resolved =
|
|
4702
|
-
if (!resolved.startsWith(projectRoot +
|
|
4886
|
+
const resolved = path9.resolve(projectRoot, newPath);
|
|
4887
|
+
if (!resolved.startsWith(projectRoot + path9.sep) && resolved !== projectRoot) {
|
|
4703
4888
|
sendResult(ws, false, `Path must stay inside the project root: ${projectRoot}`);
|
|
4704
4889
|
break;
|
|
4705
4890
|
}
|
|
4706
4891
|
try {
|
|
4707
|
-
await
|
|
4708
|
-
const stat2 = await
|
|
4892
|
+
await fs7.access(resolved);
|
|
4893
|
+
const stat2 = await fs7.stat(resolved);
|
|
4709
4894
|
if (!stat2.isDirectory()) throw new Error("Not a directory");
|
|
4710
4895
|
} catch {
|
|
4711
4896
|
sendResult(ws, false, `Directory not found or not accessible: ${resolved}`);
|
|
@@ -4725,58 +4910,30 @@ async function startWebUI(opts = {}) {
|
|
|
4725
4910
|
}
|
|
4726
4911
|
// ── Shell open — spawn terminal or file manager at a path ─────────
|
|
4727
4912
|
case "shell.open": {
|
|
4728
|
-
const
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
const platform = process.platform;
|
|
4734
|
-
let cmd;
|
|
4735
|
-
if (target === "file-manager") {
|
|
4736
|
-
if (platform === "win32") {
|
|
4737
|
-
cmd = `explorer "${resolved}"`;
|
|
4738
|
-
} else if (platform === "darwin") {
|
|
4739
|
-
cmd = `open "${resolved}"`;
|
|
4740
|
-
} else {
|
|
4741
|
-
cmd = `xdg-open "${resolved}"`;
|
|
4742
|
-
}
|
|
4743
|
-
} else {
|
|
4744
|
-
if (platform === "win32") {
|
|
4745
|
-
cmd = `start cmd /k cd /d "${resolved}"`;
|
|
4746
|
-
} else if (platform === "darwin") {
|
|
4747
|
-
cmd = `open -a Terminal "${resolved}"`;
|
|
4748
|
-
} else {
|
|
4749
|
-
cmd = `x-terminal-emulator --working-directory="${resolved}" 2>/dev/null || gnome-terminal --working-directory="${resolved}" 2>/dev/null || xterm -e "cd '${resolved}' && $SHELL"`;
|
|
4750
|
-
}
|
|
4751
|
-
}
|
|
4752
|
-
exec(cmd, { timeout: 5e3 }, (err) => {
|
|
4753
|
-
if (err) {
|
|
4754
|
-
logger.warn(`shell.open failed: ${err.message}`);
|
|
4755
|
-
}
|
|
4756
|
-
});
|
|
4757
|
-
sendResult(ws, true, `Opened ${target} at ${resolved}`);
|
|
4758
|
-
} catch (err) {
|
|
4759
|
-
sendResult(ws, false, errMessage(err));
|
|
4760
|
-
}
|
|
4913
|
+
const result = await handleShellOpen(
|
|
4914
|
+
msg.payload,
|
|
4915
|
+
logger
|
|
4916
|
+
);
|
|
4917
|
+
sendResult(ws, result.success, result.message);
|
|
4761
4918
|
break;
|
|
4762
4919
|
}
|
|
4763
4920
|
// ── Mailbox operations — project-level inter-agent messaging ────
|
|
4764
4921
|
case "mailbox.messages":
|
|
4765
4922
|
return handleMailboxMessages(
|
|
4766
4923
|
ws,
|
|
4767
|
-
{ projectRoot, globalRoot:
|
|
4924
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
4768
4925
|
msg.payload
|
|
4769
4926
|
);
|
|
4770
4927
|
case "mailbox.agents":
|
|
4771
4928
|
return handleMailboxAgents(
|
|
4772
4929
|
ws,
|
|
4773
|
-
{ projectRoot, globalRoot:
|
|
4930
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
4774
4931
|
msg.payload
|
|
4775
4932
|
);
|
|
4776
4933
|
case "mailbox.clear":
|
|
4777
4934
|
return handleMailboxClear(
|
|
4778
4935
|
ws,
|
|
4779
|
-
{ projectRoot, globalRoot:
|
|
4936
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) }
|
|
4780
4937
|
);
|
|
4781
4938
|
// ── Brain — status, autonomy ceiling, direct decision support ───
|
|
4782
4939
|
case "brain.status":
|
|
@@ -4844,10 +5001,12 @@ async function startWebUI(opts = {}) {
|
|
|
4844
5001
|
});
|
|
4845
5002
|
const httpServer = createHttpServer({
|
|
4846
5003
|
host: wsHost,
|
|
4847
|
-
distDir:
|
|
4848
|
-
wsPort
|
|
5004
|
+
distDir: path9.resolve(import.meta.dirname, "../../dist"),
|
|
5005
|
+
wsPort,
|
|
5006
|
+
globalRoot: wpaths.globalRoot,
|
|
5007
|
+
apiToken: wsToken
|
|
4849
5008
|
});
|
|
4850
|
-
const registryBaseDir =
|
|
5009
|
+
const registryBaseDir = path9.dirname(globalConfigPath);
|
|
4851
5010
|
httpServer.listen(httpPort, wsHost, () => {
|
|
4852
5011
|
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
4853
5012
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
@@ -4859,7 +5018,7 @@ async function startWebUI(opts = {}) {
|
|
|
4859
5018
|
wsPort,
|
|
4860
5019
|
host: wsHost,
|
|
4861
5020
|
projectRoot,
|
|
4862
|
-
projectName:
|
|
5021
|
+
projectName: path9.basename(projectRoot) || projectRoot,
|
|
4863
5022
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4864
5023
|
url: `http://${wsHost}:${httpPort}`
|
|
4865
5024
|
},
|
|
@@ -4886,6 +5045,10 @@ async function startWebUI(opts = {}) {
|
|
|
4886
5045
|
// reality. Crash exits are healed by the next register()/list() prune pass.
|
|
4887
5046
|
onShutdown: () => {
|
|
4888
5047
|
brainMonitor.stop();
|
|
5048
|
+
if (eternalSubscription) {
|
|
5049
|
+
eternalSubscription.dispose();
|
|
5050
|
+
eternalSubscription = null;
|
|
5051
|
+
}
|
|
4889
5052
|
return unregisterInstance(process.pid, registryBaseDir);
|
|
4890
5053
|
}
|
|
4891
5054
|
});
|