@wrongstack/webui 0.273.0 → 0.274.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-CwMm5VgW.js +140 -0
- package/dist/assets/index-DqD59GFW.css +2 -0
- package/dist/assets/{vendor-P9eRrO6V.js → vendor-CzID01pz.js} +254 -254
- package/dist/index.html +3 -3
- package/dist/index.js +2854 -2314
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +769 -365
- package/dist/server/entry.js.map +1 -1
- package/dist/server/index.d.ts +85 -15
- package/dist/server/index.js +692 -361
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +15 -1
- package/package.json +7 -7
- package/dist/assets/index-BGzM4-Zu.css +0 -2
- package/dist/assets/index-CM62rXoC.js +0 -140
package/dist/server/index.js
CHANGED
|
@@ -896,7 +896,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
896
896
|
return;
|
|
897
897
|
}
|
|
898
898
|
try {
|
|
899
|
-
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore:
|
|
899
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
|
|
900
900
|
const registry = new SessionRegistry(globalRoot);
|
|
901
901
|
const entry = await registry.get(sessionId);
|
|
902
902
|
if (!entry) {
|
|
@@ -905,7 +905,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
905
905
|
return;
|
|
906
906
|
}
|
|
907
907
|
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
908
|
-
const store = new
|
|
908
|
+
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
909
909
|
const reader = new DefaultSessionReader2({ store });
|
|
910
910
|
const rawEntries = [];
|
|
911
911
|
for await (const ev of reader.replay(sessionId)) {
|
|
@@ -1166,7 +1166,7 @@ function isTrustedLoopbackOrigin(origin) {
|
|
|
1166
1166
|
try {
|
|
1167
1167
|
const url = new URL(origin);
|
|
1168
1168
|
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
1169
|
-
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
1169
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
|
|
1170
1170
|
} catch {
|
|
1171
1171
|
return false;
|
|
1172
1172
|
}
|
|
@@ -1177,6 +1177,14 @@ function isLoopbackBind(wsHost) {
|
|
|
1177
1177
|
function isWildcardBind(wsHost) {
|
|
1178
1178
|
return wsHost === "0.0.0.0" || wsHost === "::" || wsHost === "[::]";
|
|
1179
1179
|
}
|
|
1180
|
+
function normalizeHostname(hostname) {
|
|
1181
|
+
const h = hostname.trim().toLowerCase();
|
|
1182
|
+
return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
1183
|
+
}
|
|
1184
|
+
function allowedHostname(hostname, allowedHostnames) {
|
|
1185
|
+
const normalized = normalizeHostname(hostname);
|
|
1186
|
+
return (allowedHostnames ?? []).some((candidate) => normalizeHostname(candidate) === normalized);
|
|
1187
|
+
}
|
|
1180
1188
|
function tokenMatches(provided, expected) {
|
|
1181
1189
|
if (!provided) return false;
|
|
1182
1190
|
const a = Buffer.from(provided);
|
|
@@ -1215,28 +1223,37 @@ function hostHeaderOk(input) {
|
|
|
1215
1223
|
} catch {
|
|
1216
1224
|
return false;
|
|
1217
1225
|
}
|
|
1218
|
-
return isLoopbackHostname(hostname);
|
|
1226
|
+
return isLoopbackHostname(hostname) || allowedHostname(hostname, input.allowedHostnames);
|
|
1219
1227
|
}
|
|
1220
1228
|
function verifyClient(input) {
|
|
1221
|
-
const {
|
|
1229
|
+
const {
|
|
1230
|
+
origin,
|
|
1231
|
+
url,
|
|
1232
|
+
hostHeader,
|
|
1233
|
+
remoteAddress,
|
|
1234
|
+
cookieHeader,
|
|
1235
|
+
wsHost,
|
|
1236
|
+
expectedToken,
|
|
1237
|
+
requireToken,
|
|
1238
|
+
allowedHostnames,
|
|
1239
|
+
allowBrowserUrlToken
|
|
1240
|
+
} = input;
|
|
1222
1241
|
const urlTokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
1223
1242
|
const cookieTokenOk = tokenMatches(extractTokenFromCookie(cookieHeader), expectedToken);
|
|
1224
|
-
if (!hostHeaderOk({ hostHeader, wsHost })) return false;
|
|
1243
|
+
if (!hostHeaderOk({ hostHeader, wsHost, allowedHostnames })) return false;
|
|
1225
1244
|
if (!origin) {
|
|
1226
1245
|
const remoteIp = remoteAddress ?? "";
|
|
1227
1246
|
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
1228
1247
|
if (!isRemoteLoopback && isWildcardBind(wsHost)) return false;
|
|
1229
|
-
return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost);
|
|
1248
|
+
return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost) && !requireToken;
|
|
1230
1249
|
}
|
|
1231
1250
|
try {
|
|
1232
|
-
const { hostname } = new URL(origin);
|
|
1233
|
-
if (isLoopbackHostname(
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1237
|
-
return true;
|
|
1251
|
+
const { hostname: originHostname } = new URL(origin);
|
|
1252
|
+
if (isLoopbackHostname(originHostname)) {
|
|
1253
|
+
if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
|
|
1254
|
+
return isTrustedLoopbackOrigin(origin);
|
|
1238
1255
|
}
|
|
1239
|
-
return cookieTokenOk;
|
|
1256
|
+
return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
|
|
1240
1257
|
} catch {
|
|
1241
1258
|
return false;
|
|
1242
1259
|
}
|
|
@@ -1262,8 +1279,69 @@ function injectWsPort(html, wsPort) {
|
|
|
1262
1279
|
return `${tag}
|
|
1263
1280
|
${html}`;
|
|
1264
1281
|
}
|
|
1265
|
-
function
|
|
1266
|
-
return
|
|
1282
|
+
function escapeHtmlAttr(value) {
|
|
1283
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1284
|
+
}
|
|
1285
|
+
function injectWsConfig(html, opts) {
|
|
1286
|
+
let out = injectWsPort(html, opts.wsPort);
|
|
1287
|
+
if (!opts.publicWsUrl || out.includes('name="wrongstack-ws-url"')) return out;
|
|
1288
|
+
const tag = `<meta name="wrongstack-ws-url" content="${escapeHtmlAttr(opts.publicWsUrl)}" />`;
|
|
1289
|
+
if (out.includes("</head>")) {
|
|
1290
|
+
return out.replace("</head>", ` ${tag}
|
|
1291
|
+
</head>`);
|
|
1292
|
+
}
|
|
1293
|
+
return `${tag}
|
|
1294
|
+
${out}`;
|
|
1295
|
+
}
|
|
1296
|
+
function firstHeader(value) {
|
|
1297
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1298
|
+
}
|
|
1299
|
+
function wsTokenCookie(token) {
|
|
1300
|
+
return `ws_token=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600`;
|
|
1301
|
+
}
|
|
1302
|
+
function requestToken(req, url) {
|
|
1303
|
+
return url.searchParams.get("token") ?? firstHeader(req.headers["x-ws-token"]) ?? extractTokenFromCookie(req.headers.cookie);
|
|
1304
|
+
}
|
|
1305
|
+
function requestHostForCsp(hostHeader) {
|
|
1306
|
+
const raw = firstHeader(hostHeader)?.trim();
|
|
1307
|
+
if (!raw) return void 0;
|
|
1308
|
+
try {
|
|
1309
|
+
return new URL(`http://${raw}`).hostname;
|
|
1310
|
+
} catch {
|
|
1311
|
+
return void 0;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
function formatCspHostname(hostname) {
|
|
1315
|
+
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
1316
|
+
}
|
|
1317
|
+
function cspSourceFromUrl(rawUrl) {
|
|
1318
|
+
try {
|
|
1319
|
+
const url = new URL(rawUrl);
|
|
1320
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") return void 0;
|
|
1321
|
+
return `${url.protocol}//${formatCspHostname(url.hostname)}${url.port ? `:${url.port}` : ""}`;
|
|
1322
|
+
} catch {
|
|
1323
|
+
return void 0;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
var ALLOWED_INLINE_SCRIPT_HASHES = [
|
|
1327
|
+
"'sha256-6PXDy0zrpXa6mvYOl11bZ8nubNUL7ushPUhGDZtaexg='",
|
|
1328
|
+
"'sha256-6sIdwbEBx7jj0drqSHHm7MqvmoYD3CQ4lp8Zp8blcb0='"
|
|
1329
|
+
];
|
|
1330
|
+
function buildCspHeader(wsPort, requestHost, publicWsUrl) {
|
|
1331
|
+
const connect = /* @__PURE__ */ new Set([
|
|
1332
|
+
"'self'",
|
|
1333
|
+
`ws://127.0.0.1:${wsPort}`,
|
|
1334
|
+
`wss://127.0.0.1:${wsPort}`
|
|
1335
|
+
]);
|
|
1336
|
+
if (requestHost && requestHost !== "127.0.0.1") {
|
|
1337
|
+
const host = formatCspHostname(requestHost);
|
|
1338
|
+
connect.add(`ws://${host}:${wsPort}`);
|
|
1339
|
+
connect.add(`wss://${host}:${wsPort}`);
|
|
1340
|
+
}
|
|
1341
|
+
const publicWsSource = publicWsUrl ? cspSourceFromUrl(publicWsUrl) : void 0;
|
|
1342
|
+
if (publicWsSource) connect.add(publicWsSource);
|
|
1343
|
+
const scriptSrc = ["'self'", ...ALLOWED_INLINE_SCRIPT_HASHES].join(" ");
|
|
1344
|
+
return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; connect-src ${Array.from(connect).join(" ")}; img-src 'self' data:; font-src 'self' data:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
|
|
1267
1345
|
}
|
|
1268
1346
|
function isInsideDist(candidate, distDir) {
|
|
1269
1347
|
const root = path.resolve(distDir);
|
|
@@ -1281,12 +1359,15 @@ function createHttpServer(opts) {
|
|
|
1281
1359
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
1282
1360
|
const distDir = path.resolve(opts.distDir);
|
|
1283
1361
|
const wsPort = opts.wsPort;
|
|
1284
|
-
const
|
|
1362
|
+
const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
|
|
1285
1363
|
return http.createServer(async (req, res) => {
|
|
1286
1364
|
try {
|
|
1287
1365
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
1366
|
+
const providedAccessToken = requestToken(req, url);
|
|
1367
|
+
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
1368
|
+
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
1288
1369
|
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
1289
|
-
const provided = url
|
|
1370
|
+
const provided = requestToken(req, url);
|
|
1290
1371
|
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
1291
1372
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
1292
1373
|
res.end("Unauthorized");
|
|
@@ -1294,7 +1375,7 @@ function createHttpServer(opts) {
|
|
|
1294
1375
|
}
|
|
1295
1376
|
res.writeHead(200, {
|
|
1296
1377
|
"Content-Type": "text/plain",
|
|
1297
|
-
"Set-Cookie":
|
|
1378
|
+
"Set-Cookie": wsTokenCookie(opts.apiToken),
|
|
1298
1379
|
// Belt-and-braces: tell any caches the cookie response itself
|
|
1299
1380
|
// is sensitive.
|
|
1300
1381
|
"Cache-Control": "no-store"
|
|
@@ -1302,10 +1383,20 @@ function createHttpServer(opts) {
|
|
|
1302
1383
|
res.end("ok");
|
|
1303
1384
|
return;
|
|
1304
1385
|
}
|
|
1386
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1387
|
+
res.writeHead(401, {
|
|
1388
|
+
"Content-Type": "text/plain",
|
|
1389
|
+
"Cache-Control": "no-store"
|
|
1390
|
+
});
|
|
1391
|
+
res.end("Unauthorized");
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (shouldSetAuthCookie && opts.apiToken) {
|
|
1395
|
+
res.setHeader("Set-Cookie", wsTokenCookie(opts.apiToken));
|
|
1396
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1397
|
+
}
|
|
1305
1398
|
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
1306
|
-
|
|
1307
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1308
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1399
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1309
1400
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1310
1401
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1311
1402
|
return;
|
|
@@ -1319,9 +1410,7 @@ function createHttpServer(opts) {
|
|
|
1319
1410
|
return;
|
|
1320
1411
|
}
|
|
1321
1412
|
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
1322
|
-
|
|
1323
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1324
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1413
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1325
1414
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1326
1415
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1327
1416
|
return;
|
|
@@ -1331,9 +1420,7 @@ function createHttpServer(opts) {
|
|
|
1331
1420
|
}
|
|
1332
1421
|
const agentsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/agents$/);
|
|
1333
1422
|
if (agentsMatch && req.method === "GET") {
|
|
1334
|
-
|
|
1335
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1336
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1423
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1337
1424
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1338
1425
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1339
1426
|
return;
|
|
@@ -1343,9 +1430,7 @@ function createHttpServer(opts) {
|
|
|
1343
1430
|
}
|
|
1344
1431
|
const eventsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
|
|
1345
1432
|
if (eventsMatch && req.method === "GET") {
|
|
1346
|
-
|
|
1347
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1348
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1433
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1349
1434
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1350
1435
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1351
1436
|
return;
|
|
@@ -1357,9 +1442,7 @@ function createHttpServer(opts) {
|
|
|
1357
1442
|
}
|
|
1358
1443
|
const msgMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/message$/);
|
|
1359
1444
|
if (msgMatch && req.method === "POST") {
|
|
1360
|
-
|
|
1361
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1362
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1445
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1363
1446
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1364
1447
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1365
1448
|
return;
|
|
@@ -1369,9 +1452,7 @@ function createHttpServer(opts) {
|
|
|
1369
1452
|
}
|
|
1370
1453
|
const mailboxMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/mailbox$/);
|
|
1371
1454
|
if (mailboxMatch && req.method === "GET") {
|
|
1372
|
-
|
|
1373
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1374
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1455
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1375
1456
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1376
1457
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1377
1458
|
return;
|
|
@@ -1381,9 +1462,7 @@ function createHttpServer(opts) {
|
|
|
1381
1462
|
}
|
|
1382
1463
|
const interruptMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/interrupt$/);
|
|
1383
1464
|
if (interruptMatch && req.method === "POST") {
|
|
1384
|
-
|
|
1385
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1386
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1465
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1387
1466
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1388
1467
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1389
1468
|
return;
|
|
@@ -1397,9 +1476,7 @@ function createHttpServer(opts) {
|
|
|
1397
1476
|
return;
|
|
1398
1477
|
}
|
|
1399
1478
|
if (url.pathname === "/api/fleet/broadcast" && req.method === "POST") {
|
|
1400
|
-
|
|
1401
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1402
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1479
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1403
1480
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1404
1481
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1405
1482
|
return;
|
|
@@ -1446,11 +1523,14 @@ function createHttpServer(opts) {
|
|
|
1446
1523
|
res.setHeader("X-Frame-Options", "DENY");
|
|
1447
1524
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1448
1525
|
if (ext === ".html") {
|
|
1449
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
1450
|
-
res.setHeader(
|
|
1526
|
+
if (!shouldSetAuthCookie) res.setHeader("Cache-Control", "no-cache");
|
|
1527
|
+
res.setHeader(
|
|
1528
|
+
"Content-Security-Policy",
|
|
1529
|
+
buildCspHeader(wsPort, requestHostForCsp(req.headers.host), opts.publicWsUrl)
|
|
1530
|
+
);
|
|
1451
1531
|
const html = await fs.readFile(resolvedPath, "utf8");
|
|
1452
1532
|
res.writeHead(200);
|
|
1453
|
-
res.end(
|
|
1533
|
+
res.end(injectWsConfig(html, { wsPort, publicWsUrl: opts.publicWsUrl }));
|
|
1454
1534
|
return;
|
|
1455
1535
|
}
|
|
1456
1536
|
const fileContent = await fs.readFile(resolvedPath);
|
|
@@ -1465,9 +1545,13 @@ function createHttpServer(opts) {
|
|
|
1465
1545
|
"X-Content-Type-Options": "nosniff",
|
|
1466
1546
|
"X-Frame-Options": "DENY",
|
|
1467
1547
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
1468
|
-
"Content-Security-Policy": buildCspHeader(
|
|
1548
|
+
"Content-Security-Policy": buildCspHeader(
|
|
1549
|
+
wsPort,
|
|
1550
|
+
requestHostForCsp(req.headers.host),
|
|
1551
|
+
opts.publicWsUrl
|
|
1552
|
+
)
|
|
1469
1553
|
});
|
|
1470
|
-
res.end(
|
|
1554
|
+
res.end(injectWsConfig(html, { wsPort, publicWsUrl: opts.publicWsUrl }));
|
|
1471
1555
|
} catch {
|
|
1472
1556
|
res.writeHead(404);
|
|
1473
1557
|
res.end("Not found");
|
|
@@ -1591,8 +1675,8 @@ function isInside(root, target) {
|
|
|
1591
1675
|
}
|
|
1592
1676
|
|
|
1593
1677
|
// src/server/file-handlers.ts
|
|
1594
|
-
import * as
|
|
1595
|
-
import * as
|
|
1678
|
+
import * as fs4 from "fs/promises";
|
|
1679
|
+
import * as path4 from "path";
|
|
1596
1680
|
import { atomicWrite } from "@wrongstack/core";
|
|
1597
1681
|
|
|
1598
1682
|
// src/server/file-picker.ts
|
|
@@ -1643,6 +1727,34 @@ function rankFiles(paths, query, limit) {
|
|
|
1643
1727
|
return scored.slice(0, limit).map((s) => s.path);
|
|
1644
1728
|
}
|
|
1645
1729
|
|
|
1730
|
+
// src/server/path-containment.ts
|
|
1731
|
+
import * as fs3 from "fs/promises";
|
|
1732
|
+
import * as path3 from "path";
|
|
1733
|
+
function isPathInside(root, target) {
|
|
1734
|
+
const relative3 = path3.relative(root, target);
|
|
1735
|
+
return relative3 === "" || !relative3.startsWith("..") && !path3.isAbsolute(relative3);
|
|
1736
|
+
}
|
|
1737
|
+
async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
|
|
1738
|
+
const resolved = path3.resolve(projectRoot, inputPath);
|
|
1739
|
+
let stat3;
|
|
1740
|
+
try {
|
|
1741
|
+
stat3 = await fs3.stat(resolved);
|
|
1742
|
+
} catch {
|
|
1743
|
+
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
1744
|
+
}
|
|
1745
|
+
if (!stat3.isDirectory()) {
|
|
1746
|
+
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
1747
|
+
}
|
|
1748
|
+
const [realProjectRoot, realResolved] = await Promise.all([
|
|
1749
|
+
fs3.realpath(projectRoot),
|
|
1750
|
+
fs3.realpath(resolved)
|
|
1751
|
+
]);
|
|
1752
|
+
if (!isPathInside(realProjectRoot, realResolved)) {
|
|
1753
|
+
throw new Error(`Path must stay inside the project root: ${projectRoot}`);
|
|
1754
|
+
}
|
|
1755
|
+
return resolved;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1646
1758
|
// src/server/ws-utils.ts
|
|
1647
1759
|
import { randomBytes } from "crypto";
|
|
1648
1760
|
import { WebSocket } from "ws";
|
|
@@ -1671,25 +1783,106 @@ function errMessage(err) {
|
|
|
1671
1783
|
function generateAuthToken() {
|
|
1672
1784
|
return randomBytes(16).toString("hex");
|
|
1673
1785
|
}
|
|
1786
|
+
function resolveAuthToken(explicit) {
|
|
1787
|
+
const configured = explicit?.trim() || process.env["WEBUI_TOKEN"]?.trim() || process.env["WEBUI_AUTH_TOKEN"]?.trim();
|
|
1788
|
+
return configured || generateAuthToken();
|
|
1789
|
+
}
|
|
1790
|
+
function hostForBrowserUrl(bindHost) {
|
|
1791
|
+
if (bindHost === "0.0.0.0") return "127.0.0.1";
|
|
1792
|
+
if (bindHost === "::" || bindHost === "[::]") return "[::1]";
|
|
1793
|
+
if (bindHost.includes(":") && !bindHost.startsWith("[")) return `[${bindHost}]`;
|
|
1794
|
+
return bindHost;
|
|
1795
|
+
}
|
|
1796
|
+
function buildWebUIAccessUrl(opts) {
|
|
1797
|
+
const protocol = opts.protocol ?? "http";
|
|
1798
|
+
const base = opts.publicUrl?.trim() || `${protocol}://${hostForBrowserUrl(opts.host)}:${opts.port}`;
|
|
1799
|
+
if (!opts.token) return base;
|
|
1800
|
+
try {
|
|
1801
|
+
const url = new URL(base);
|
|
1802
|
+
url.searchParams.set("token", opts.token);
|
|
1803
|
+
const rendered = url.toString();
|
|
1804
|
+
const afterOrigin = base.slice(url.origin.length);
|
|
1805
|
+
if (url.pathname === "/" && !afterOrigin.startsWith("/")) {
|
|
1806
|
+
return `${url.origin}${url.search}${url.hash}`;
|
|
1807
|
+
}
|
|
1808
|
+
return rendered;
|
|
1809
|
+
} catch {
|
|
1810
|
+
return `${base}${base.includes("?") ? "&" : "?"}token=${encodeURIComponent(opts.token)}`;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
function envFlag(name2) {
|
|
1814
|
+
const value = process.env[name2]?.trim().toLowerCase();
|
|
1815
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
1816
|
+
}
|
|
1674
1817
|
|
|
1675
1818
|
// src/server/file-handlers.ts
|
|
1819
|
+
async function resolveFileInsideProject(projectRoot, filePath) {
|
|
1820
|
+
const resolved = path4.resolve(projectRoot, filePath);
|
|
1821
|
+
if (!isPathInside(projectRoot, resolved)) {
|
|
1822
|
+
throw new Error("Path outside project root");
|
|
1823
|
+
}
|
|
1824
|
+
const { parent, base } = splitParentAndBase(resolved);
|
|
1825
|
+
const realProjectRoot = await fs4.realpath(projectRoot);
|
|
1826
|
+
const realParent = await realpathAllowMissing(parent);
|
|
1827
|
+
const realFull = path4.join(realParent, base);
|
|
1828
|
+
if (!isPathInside(realProjectRoot, realFull)) {
|
|
1829
|
+
throw new Error("Path outside project root");
|
|
1830
|
+
}
|
|
1831
|
+
return realFull;
|
|
1832
|
+
}
|
|
1833
|
+
function splitParentAndBase(p) {
|
|
1834
|
+
const base = path4.basename(p);
|
|
1835
|
+
const parent = path4.dirname(p);
|
|
1836
|
+
return { parent, base };
|
|
1837
|
+
}
|
|
1838
|
+
async function realpathAllowMissing(p) {
|
|
1839
|
+
try {
|
|
1840
|
+
return await fs4.realpath(p);
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
if (err.code !== "ENOENT") throw err;
|
|
1843
|
+
}
|
|
1844
|
+
const segments = [];
|
|
1845
|
+
let cursor = p;
|
|
1846
|
+
while (true) {
|
|
1847
|
+
const parent = path4.dirname(cursor);
|
|
1848
|
+
if (parent === cursor) {
|
|
1849
|
+
throw new Error("Path outside project root");
|
|
1850
|
+
}
|
|
1851
|
+
segments.unshift(path4.basename(cursor));
|
|
1852
|
+
try {
|
|
1853
|
+
const realParent = await fs4.realpath(parent);
|
|
1854
|
+
return path4.join(realParent, ...segments);
|
|
1855
|
+
} catch (err) {
|
|
1856
|
+
if (err.code !== "ENOENT") throw err;
|
|
1857
|
+
cursor = parent;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1676
1861
|
async function handleFilesTree(ws, msg, projectRoot) {
|
|
1677
1862
|
const payload = msg.payload;
|
|
1678
1863
|
const rawPath = payload?.path?.trim();
|
|
1679
|
-
|
|
1680
|
-
|
|
1864
|
+
let treeRoot;
|
|
1865
|
+
let realProjectRoot;
|
|
1866
|
+
try {
|
|
1867
|
+
if (rawPath && rawPath !== ".") {
|
|
1868
|
+
treeRoot = await resolveWorkingDirInsideProject(projectRoot, rawPath);
|
|
1869
|
+
} else {
|
|
1870
|
+
treeRoot = projectRoot;
|
|
1871
|
+
}
|
|
1872
|
+
realProjectRoot = await fs4.realpath(projectRoot);
|
|
1873
|
+
} catch {
|
|
1681
1874
|
send(ws, {
|
|
1682
1875
|
type: "files.tree",
|
|
1683
1876
|
payload: { root: projectRoot, tree: [], error: "Path outside project root" }
|
|
1684
1877
|
});
|
|
1685
1878
|
return;
|
|
1686
1879
|
}
|
|
1687
|
-
const pathPrefix = treeRoot === projectRoot ? "" : (
|
|
1880
|
+
const pathPrefix = treeRoot === projectRoot ? "" : (path4.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
|
|
1688
1881
|
async function buildTree(dir, rel, depth) {
|
|
1689
1882
|
if (depth > 10) return [];
|
|
1690
1883
|
let entries = [];
|
|
1691
1884
|
try {
|
|
1692
|
-
entries = await
|
|
1885
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1693
1886
|
} catch {
|
|
1694
1887
|
return [];
|
|
1695
1888
|
}
|
|
@@ -1701,11 +1894,20 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1701
1894
|
for (const e of entries) {
|
|
1702
1895
|
if (isHiddenEntry(e.name)) continue;
|
|
1703
1896
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
1704
|
-
const childAbs =
|
|
1897
|
+
const childAbs = path4.join(dir, e.name);
|
|
1705
1898
|
const childPath = pathPrefix + childRel;
|
|
1706
1899
|
if (e.isDirectory()) {
|
|
1707
1900
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
1708
|
-
|
|
1901
|
+
let realChild;
|
|
1902
|
+
try {
|
|
1903
|
+
realChild = await fs4.realpath(childAbs);
|
|
1904
|
+
} catch {
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
if (!isPathInside(realProjectRoot, realChild)) {
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
const children = await buildTree(realChild, childRel, depth + 1);
|
|
1709
1911
|
nodes.push({ name: e.name, path: childPath, type: "directory", children });
|
|
1710
1912
|
} else if (e.isFile()) {
|
|
1711
1913
|
nodes.push({ name: e.name, path: childPath, type: "file" });
|
|
@@ -1715,10 +1917,10 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1715
1917
|
}
|
|
1716
1918
|
try {
|
|
1717
1919
|
const tree = await buildTree(treeRoot, "", 0);
|
|
1718
|
-
const rootLabel = treeRoot === projectRoot ? projectRoot :
|
|
1920
|
+
const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
|
|
1719
1921
|
send(ws, { type: "files.tree", payload: { root: rootLabel, tree } });
|
|
1720
1922
|
} catch (err) {
|
|
1721
|
-
const rootLabel = treeRoot === projectRoot ? projectRoot :
|
|
1923
|
+
const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
|
|
1722
1924
|
send(ws, {
|
|
1723
1925
|
type: "files.tree",
|
|
1724
1926
|
payload: { root: rootLabel, tree: [], error: errMessage(err) }
|
|
@@ -1727,13 +1929,15 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1727
1929
|
}
|
|
1728
1930
|
async function handleFilesRead(ws, msg, projectRoot) {
|
|
1729
1931
|
const { filePath } = msg.payload;
|
|
1730
|
-
|
|
1731
|
-
|
|
1932
|
+
let realResolved;
|
|
1933
|
+
try {
|
|
1934
|
+
realResolved = await resolveFileInsideProject(projectRoot, filePath);
|
|
1935
|
+
} catch {
|
|
1732
1936
|
send(ws, { type: "files.read", payload: { filePath, content: "", error: "Forbidden" } });
|
|
1733
1937
|
return;
|
|
1734
1938
|
}
|
|
1735
1939
|
try {
|
|
1736
|
-
const content = await
|
|
1940
|
+
const content = await fs4.readFile(realResolved, "utf8");
|
|
1737
1941
|
send(ws, { type: "files.read", payload: { filePath, content } });
|
|
1738
1942
|
} catch (err) {
|
|
1739
1943
|
send(ws, {
|
|
@@ -1744,16 +1948,18 @@ async function handleFilesRead(ws, msg, projectRoot) {
|
|
|
1744
1948
|
}
|
|
1745
1949
|
async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
|
|
1746
1950
|
const { filePath, content } = msg.payload;
|
|
1747
|
-
|
|
1748
|
-
|
|
1951
|
+
let realResolved;
|
|
1952
|
+
try {
|
|
1953
|
+
realResolved = await resolveFileInsideProject(projectRoot, filePath);
|
|
1954
|
+
} catch {
|
|
1749
1955
|
send(ws, { type: "files.written", payload: { filePath, success: false, error: "Forbidden" } });
|
|
1750
1956
|
return;
|
|
1751
1957
|
}
|
|
1752
1958
|
try {
|
|
1753
|
-
await atomicWrite(
|
|
1959
|
+
await atomicWrite(realResolved, content);
|
|
1754
1960
|
send(ws, { type: "files.written", payload: { filePath, success: true } });
|
|
1755
1961
|
if (opts.onWritten) {
|
|
1756
|
-
void Promise.resolve(opts.onWritten(
|
|
1962
|
+
void Promise.resolve(opts.onWritten(realResolved)).catch(() => void 0);
|
|
1757
1963
|
}
|
|
1758
1964
|
} catch (err) {
|
|
1759
1965
|
send(ws, {
|
|
@@ -1765,8 +1971,16 @@ async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
|
|
|
1765
1971
|
async function handleFilesList(ws, msg, projectRoot) {
|
|
1766
1972
|
const payload = msg.payload ?? {};
|
|
1767
1973
|
const limit = payload.limit ?? 50;
|
|
1768
|
-
|
|
1769
|
-
|
|
1974
|
+
let listRoot;
|
|
1975
|
+
let realProjectRoot;
|
|
1976
|
+
try {
|
|
1977
|
+
if (payload.path) {
|
|
1978
|
+
listRoot = await resolveWorkingDirInsideProject(projectRoot, payload.path);
|
|
1979
|
+
} else {
|
|
1980
|
+
listRoot = projectRoot;
|
|
1981
|
+
}
|
|
1982
|
+
realProjectRoot = await fs4.realpath(projectRoot);
|
|
1983
|
+
} catch {
|
|
1770
1984
|
send(ws, { type: "files.list", payload: { files: [] } });
|
|
1771
1985
|
return;
|
|
1772
1986
|
}
|
|
@@ -1775,7 +1989,7 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
1775
1989
|
if (depth > 8 || results.length >= 600) return;
|
|
1776
1990
|
let entries = [];
|
|
1777
1991
|
try {
|
|
1778
|
-
entries = await
|
|
1992
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1779
1993
|
} catch {
|
|
1780
1994
|
return;
|
|
1781
1995
|
}
|
|
@@ -1785,7 +1999,16 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
1785
1999
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
1786
2000
|
if (e.isDirectory()) {
|
|
1787
2001
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
1788
|
-
|
|
2002
|
+
let realChild;
|
|
2003
|
+
try {
|
|
2004
|
+
realChild = await fs4.realpath(path4.join(dir, e.name));
|
|
2005
|
+
} catch {
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
if (!isPathInside(realProjectRoot, realChild)) {
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
await walk(realChild, childRel, depth + 1);
|
|
1789
2012
|
} else if (e.isFile()) {
|
|
1790
2013
|
results.push(childRel);
|
|
1791
2014
|
}
|
|
@@ -1799,7 +2022,7 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
1799
2022
|
}
|
|
1800
2023
|
|
|
1801
2024
|
// src/server/completion-handlers.ts
|
|
1802
|
-
import * as
|
|
2025
|
+
import * as path5 from "path";
|
|
1803
2026
|
import { searchCodebaseIndex } from "@wrongstack/tools/codebase-index/index";
|
|
1804
2027
|
var MAX_PREFIX_CHARS = 12e3;
|
|
1805
2028
|
var MAX_SUFFIX_CHARS = 4e3;
|
|
@@ -1874,8 +2097,8 @@ async function handleCompletionRequest(ws, msg, opts) {
|
|
|
1874
2097
|
return;
|
|
1875
2098
|
}
|
|
1876
2099
|
const payload = parsed.payload;
|
|
1877
|
-
const projectRoot =
|
|
1878
|
-
const resolved =
|
|
2100
|
+
const projectRoot = path5.resolve(opts.projectRoot);
|
|
2101
|
+
const resolved = path5.resolve(projectRoot, payload.filePath);
|
|
1879
2102
|
if (!isInside2(projectRoot, resolved)) {
|
|
1880
2103
|
send(ws, {
|
|
1881
2104
|
type: "completion.result",
|
|
@@ -2274,7 +2497,7 @@ function buildSearchQuery(linePrefix, filePath) {
|
|
|
2274
2497
|
if (memberMatch?.[1]) return memberMatch[1];
|
|
2275
2498
|
const token = linePrefix.match(/([A-Za-z_$][\w$]*)$/)?.[1];
|
|
2276
2499
|
if (token && token.length >= 2) return token;
|
|
2277
|
-
return
|
|
2500
|
+
return path5.basename(filePath, path5.extname(filePath));
|
|
2278
2501
|
}
|
|
2279
2502
|
function currentLinePrefix(prefix) {
|
|
2280
2503
|
const idx = Math.max(prefix.lastIndexOf("\n"), prefix.lastIndexOf("\r"));
|
|
@@ -2304,7 +2527,7 @@ function head(value, max) {
|
|
|
2304
2527
|
return value.length <= max ? value : value.slice(0, max);
|
|
2305
2528
|
}
|
|
2306
2529
|
function isInside2(root, target) {
|
|
2307
|
-
return target === root || target.startsWith(root +
|
|
2530
|
+
return target === root || target.startsWith(root + path5.sep);
|
|
2308
2531
|
}
|
|
2309
2532
|
|
|
2310
2533
|
// src/server/memory-handlers.ts
|
|
@@ -2558,8 +2781,8 @@ async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
2558
2781
|
}
|
|
2559
2782
|
|
|
2560
2783
|
// src/server/skills-handlers.ts
|
|
2561
|
-
import { promises as
|
|
2562
|
-
import
|
|
2784
|
+
import { promises as fs5 } from "fs";
|
|
2785
|
+
import path6 from "path";
|
|
2563
2786
|
import JSZip from "jszip";
|
|
2564
2787
|
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
2565
2788
|
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
@@ -2630,19 +2853,19 @@ async function handleSkillsContent(ws, ctx, msg) {
|
|
|
2630
2853
|
send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
|
|
2631
2854
|
return;
|
|
2632
2855
|
}
|
|
2633
|
-
const body = await
|
|
2634
|
-
const skillDir =
|
|
2856
|
+
const body = await fs5.readFile(entry.path, "utf8");
|
|
2857
|
+
const skillDir = path6.dirname(entry.path);
|
|
2635
2858
|
let relatedFiles = [];
|
|
2636
2859
|
try {
|
|
2637
|
-
const files = await
|
|
2638
|
-
relatedFiles = files.filter((f) => f !==
|
|
2860
|
+
const files = await fs5.readdir(skillDir);
|
|
2861
|
+
relatedFiles = files.filter((f) => f !== path6.basename(entry.path)).map((f) => path6.join(skillDir, f));
|
|
2639
2862
|
} catch {
|
|
2640
2863
|
}
|
|
2641
2864
|
const nameLower = name2.toLowerCase();
|
|
2642
2865
|
const refResults = await Promise.all(
|
|
2643
2866
|
entries.filter((e) => e.name.toLowerCase() !== nameLower).map(async (e) => {
|
|
2644
2867
|
try {
|
|
2645
|
-
const content = await
|
|
2868
|
+
const content = await fs5.readFile(e.path, "utf8");
|
|
2646
2869
|
return [e.name, content.toLowerCase().includes(nameLower)];
|
|
2647
2870
|
} catch {
|
|
2648
2871
|
return [e.name, false];
|
|
@@ -2732,14 +2955,14 @@ async function handleSkillsCreate(ws, ctx, msg) {
|
|
|
2732
2955
|
}
|
|
2733
2956
|
const createPayload = parsed.value;
|
|
2734
2957
|
try {
|
|
2735
|
-
const targetDir = createPayload.scope === "global" ?
|
|
2958
|
+
const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
2736
2959
|
try {
|
|
2737
|
-
await
|
|
2960
|
+
await fs5.access(targetDir);
|
|
2738
2961
|
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
2739
2962
|
return;
|
|
2740
2963
|
} catch {
|
|
2741
2964
|
}
|
|
2742
|
-
await
|
|
2965
|
+
await fs5.mkdir(targetDir, { recursive: true });
|
|
2743
2966
|
const lines = createPayload.description.trim().split("\n");
|
|
2744
2967
|
const firstLine = lines[0].trim();
|
|
2745
2968
|
const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
|
|
@@ -2787,13 +3010,13 @@ ${trigger}
|
|
|
2787
3010
|
"- `bug-hunter` \u2014 for systematic bug detection patterns",
|
|
2788
3011
|
"- `output-standards` \u2014 for standardized `<next_steps>` formatting"
|
|
2789
3012
|
].join("\n");
|
|
2790
|
-
await atomicWrite2(
|
|
3013
|
+
await atomicWrite2(path6.join(targetDir, "SKILL.md"), skillContent);
|
|
2791
3014
|
send(ws, {
|
|
2792
3015
|
type: "skills.created",
|
|
2793
3016
|
payload: {
|
|
2794
3017
|
success: true,
|
|
2795
3018
|
error: null,
|
|
2796
|
-
skill: { name: createPayload.name.trim(), path:
|
|
3019
|
+
skill: { name: createPayload.name.trim(), path: path6.join(targetDir, "SKILL.md"), scope: createPayload.scope }
|
|
2797
3020
|
}
|
|
2798
3021
|
});
|
|
2799
3022
|
} catch (err) {
|
|
@@ -2857,23 +3080,23 @@ import {
|
|
|
2857
3080
|
Agent,
|
|
2858
3081
|
AutoCompactionMiddleware,
|
|
2859
3082
|
Context,
|
|
2860
|
-
DefaultMemoryStore
|
|
2861
|
-
DefaultModeStore
|
|
3083
|
+
DefaultMemoryStore,
|
|
3084
|
+
DefaultModeStore,
|
|
2862
3085
|
DefaultModelsRegistry,
|
|
2863
3086
|
DefaultSessionReader,
|
|
2864
|
-
DefaultSessionStore as
|
|
2865
|
-
DefaultSkillLoader
|
|
2866
|
-
DefaultSystemPromptBuilder as
|
|
2867
|
-
DefaultTokenCounter
|
|
3087
|
+
DefaultSessionStore as DefaultSessionStore2,
|
|
3088
|
+
DefaultSkillLoader,
|
|
3089
|
+
DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
|
|
3090
|
+
DefaultTokenCounter,
|
|
2868
3091
|
AnnotationsStore,
|
|
2869
3092
|
CollaborationBus,
|
|
2870
3093
|
collabPauseMiddleware,
|
|
2871
3094
|
collabInjectMiddleware,
|
|
2872
3095
|
estimateRequestTokensCalibrated,
|
|
2873
3096
|
EventBus,
|
|
2874
|
-
createStrategyCompactor
|
|
3097
|
+
createStrategyCompactor,
|
|
2875
3098
|
ProviderRegistry,
|
|
2876
|
-
TOKENS
|
|
3099
|
+
TOKENS,
|
|
2877
3100
|
ToolRegistry,
|
|
2878
3101
|
atomicWrite as atomicWrite6,
|
|
2879
3102
|
createDefaultPipelines,
|
|
@@ -2892,110 +3115,10 @@ import {
|
|
|
2892
3115
|
import { ToolExecutor } from "@wrongstack/core/execution";
|
|
2893
3116
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
|
|
2894
3117
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
|
|
2895
|
-
import { builtinToolsPack, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
|
|
3118
|
+
import { builtinToolsPack, configureExecPolicy, ensureSessionShell, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
|
|
2896
3119
|
import { MCPRegistry } from "@wrongstack/mcp";
|
|
2897
3120
|
import { WebSocket as WebSocket2, WebSocketServer } from "ws";
|
|
2898
|
-
|
|
2899
|
-
// ../runtime/src/container.ts
|
|
2900
|
-
import {
|
|
2901
|
-
Container,
|
|
2902
|
-
DefaultConfigStore,
|
|
2903
|
-
DefaultErrorHandler,
|
|
2904
|
-
DefaultMemoryStore,
|
|
2905
|
-
DefaultModeStore,
|
|
2906
|
-
DefaultPermissionPolicy,
|
|
2907
|
-
DefaultRetryPolicy,
|
|
2908
|
-
DefaultSecretScrubber,
|
|
2909
|
-
DefaultSessionStore,
|
|
2910
|
-
DefaultSkillLoader,
|
|
2911
|
-
DefaultSystemPromptBuilder,
|
|
2912
|
-
DefaultTokenCounter,
|
|
2913
|
-
createStrategyCompactor,
|
|
2914
|
-
buildRecoveryStrategies,
|
|
2915
|
-
TOKENS
|
|
2916
|
-
} from "@wrongstack/core";
|
|
2917
|
-
function createDefaultContainer(opts) {
|
|
2918
|
-
const { config, wpaths, logger, modelsRegistry } = opts;
|
|
2919
|
-
const container = new Container();
|
|
2920
|
-
const configStore = new DefaultConfigStore(config);
|
|
2921
|
-
container.bind(TOKENS.ConfigStore, () => configStore);
|
|
2922
|
-
container.bind(TOKENS.Logger, () => logger);
|
|
2923
|
-
container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
|
|
2924
|
-
container.bind(TOKENS.RetryPolicy, () => new DefaultRetryPolicy());
|
|
2925
|
-
container.bind(
|
|
2926
|
-
TOKENS.ErrorHandler,
|
|
2927
|
-
() => new DefaultErrorHandler(
|
|
2928
|
-
buildRecoveryStrategies({
|
|
2929
|
-
compactor: container.resolve(TOKENS.Compactor),
|
|
2930
|
-
modelsRegistry,
|
|
2931
|
-
getConfig: () => configStore.get()
|
|
2932
|
-
})
|
|
2933
|
-
)
|
|
2934
|
-
);
|
|
2935
|
-
container.bind(TOKENS.ModelsRegistry, () => modelsRegistry);
|
|
2936
|
-
container.bind(
|
|
2937
|
-
TOKENS.TokenCounter,
|
|
2938
|
-
() => new DefaultTokenCounter({ registry: modelsRegistry, providerId: config.provider })
|
|
2939
|
-
);
|
|
2940
|
-
const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
|
|
2941
|
-
container.bind(TOKENS.ModeStore, () => modeStore);
|
|
2942
|
-
container.bind(
|
|
2943
|
-
TOKENS.SessionStore,
|
|
2944
|
-
() => new DefaultSessionStore({
|
|
2945
|
-
dir: wpaths.projectSessions,
|
|
2946
|
-
// Scrub secrets out of persisted user/model turns (F-06). Tool output
|
|
2947
|
-
// is already scrubbed by the executor.
|
|
2948
|
-
secretScrubber: container.resolve(TOKENS.SecretScrubber)
|
|
2949
|
-
})
|
|
2950
|
-
);
|
|
2951
|
-
const memoryStore = new DefaultMemoryStore({ paths: wpaths, events: opts.events });
|
|
2952
|
-
container.bind(TOKENS.MemoryStore, () => memoryStore);
|
|
2953
|
-
const skillLoader = new DefaultSkillLoader({ paths: wpaths, bundledDir: opts.bundledSkillsDir });
|
|
2954
|
-
container.bind(TOKENS.SkillLoader, () => skillLoader);
|
|
2955
|
-
if (opts.systemPrompt) {
|
|
2956
|
-
container.bind(
|
|
2957
|
-
TOKENS.SystemPromptBuilder,
|
|
2958
|
-
() => new DefaultSystemPromptBuilder(opts.systemPrompt)
|
|
2959
|
-
);
|
|
2960
|
-
}
|
|
2961
|
-
container.bind(
|
|
2962
|
-
TOKENS.PermissionPolicy,
|
|
2963
|
-
() => {
|
|
2964
|
-
const policyOptions = {
|
|
2965
|
-
trustFile: wpaths.projectTrust,
|
|
2966
|
-
yolo: opts.permission?.yolo ?? false,
|
|
2967
|
-
yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
|
|
2968
|
-
confirmDestructive: opts.permission?.confirmDestructive ?? false
|
|
2969
|
-
};
|
|
2970
|
-
if (opts.permission?.promptDelegate !== void 0) {
|
|
2971
|
-
policyOptions.promptDelegate = opts.permission.promptDelegate;
|
|
2972
|
-
}
|
|
2973
|
-
return new DefaultPermissionPolicy(policyOptions);
|
|
2974
|
-
}
|
|
2975
|
-
);
|
|
2976
|
-
container.bind(
|
|
2977
|
-
TOKENS.Compactor,
|
|
2978
|
-
() => (
|
|
2979
|
-
// Strategy comes from config.context.strategy: 'hybrid' (default, lossless
|
|
2980
|
-
// rules, no LLM), 'intelligent' (LLM summarization), or 'selective'
|
|
2981
|
-
// (LLM-driven selection). The LLM strategies resolve their provider from
|
|
2982
|
-
// ctx at compact()-time, so binding here (before context.provider exists)
|
|
2983
|
-
// is safe. preserveK / eliseThreshold are class-level fallbacks; the active
|
|
2984
|
-
// ContextWindowPolicy in ctx.meta normally overrides both at runtime.
|
|
2985
|
-
// eliseThreshold is a TOKEN COUNT — a previous value of 0.7 elided
|
|
2986
|
-
// essentially every tool_result (anything > 1 token).
|
|
2987
|
-
createStrategyCompactor({
|
|
2988
|
-
strategy: config.context?.strategy,
|
|
2989
|
-
preserveK: opts.compactor?.preserveK ?? 10,
|
|
2990
|
-
eliseThreshold: opts.compactor?.eliseThreshold ?? 2e3,
|
|
2991
|
-
smart: true,
|
|
2992
|
-
summarizerModel: config.context?.summarizerModel,
|
|
2993
|
-
llmSelector: config.context?.llmSelector
|
|
2994
|
-
})
|
|
2995
|
-
)
|
|
2996
|
-
);
|
|
2997
|
-
return container;
|
|
2998
|
-
}
|
|
3121
|
+
import { createDefaultContainer, makeLightSubagentFactory } from "@wrongstack/runtime";
|
|
2999
3122
|
|
|
3000
3123
|
// src/server/boot.ts
|
|
3001
3124
|
import {
|
|
@@ -3022,6 +3145,13 @@ import {
|
|
|
3022
3145
|
PhaseStore,
|
|
3023
3146
|
WorktreeManager
|
|
3024
3147
|
} from "@wrongstack/core";
|
|
3148
|
+
function deriveTitle(goal) {
|
|
3149
|
+
const firstLine = goal.split("\n").map((l) => l.trim()).find(Boolean);
|
|
3150
|
+
if (!firstLine) return "AutoPhase";
|
|
3151
|
+
const sentence = firstLine.split(/(?<=[.!?])\s/)[0] ?? firstLine;
|
|
3152
|
+
const trimmed = sentence.length <= 64 ? sentence : `${sentence.slice(0, 63).trimEnd()}\u2026`;
|
|
3153
|
+
return trimmed || "AutoPhase";
|
|
3154
|
+
}
|
|
3025
3155
|
function isGitRepo(cwd) {
|
|
3026
3156
|
try {
|
|
3027
3157
|
const r = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, encoding: "utf8", windowsHide: true });
|
|
@@ -3030,6 +3160,19 @@ function isGitRepo(cwd) {
|
|
|
3030
3160
|
return false;
|
|
3031
3161
|
}
|
|
3032
3162
|
}
|
|
3163
|
+
function commitsSince(cwd, baseSha, branch) {
|
|
3164
|
+
try {
|
|
3165
|
+
const r = spawnSync("git", ["log", "--reverse", "--format=%H", `${baseSha}..${branch}`], {
|
|
3166
|
+
cwd,
|
|
3167
|
+
encoding: "utf8",
|
|
3168
|
+
windowsHide: true
|
|
3169
|
+
});
|
|
3170
|
+
if (r.status !== 0) return [];
|
|
3171
|
+
return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3172
|
+
} catch {
|
|
3173
|
+
return [];
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3033
3176
|
var AutoPhaseWebSocketHandler = class {
|
|
3034
3177
|
constructor(agent, context, logger, storeDir, events, projectRoot) {
|
|
3035
3178
|
this.agent = agent;
|
|
@@ -3049,10 +3192,17 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3049
3192
|
store;
|
|
3050
3193
|
clients = /* @__PURE__ */ new Set();
|
|
3051
3194
|
broadcastInterval = null;
|
|
3052
|
-
/** Aborts in-flight task agents when the run is stopped. */
|
|
3195
|
+
/** Aborts in-flight task agents AND the planning turn when the run is stopped. */
|
|
3053
3196
|
abort = null;
|
|
3197
|
+
/** Set the instant a stop/clear/revert is requested, so a planning turn that
|
|
3198
|
+
* resolves afterwards never launches the orchestrator (the abort alone can't
|
|
3199
|
+
* cover the window between the LLM call resolving and the orchestrator start). */
|
|
3200
|
+
stopping = false;
|
|
3054
3201
|
/** Optional per-phase git-worktree isolation (lazily created at start). */
|
|
3055
3202
|
worktrees = null;
|
|
3203
|
+
/** Base branch + tip SHA captured at run start so a revert can git-revert the
|
|
3204
|
+
* run's squash commits (history-preserving) instead of a destructive reset. */
|
|
3205
|
+
runBase = null;
|
|
3056
3206
|
/** Per-run worker identities so the board can show "who is on what". */
|
|
3057
3207
|
usedNicknames = /* @__PURE__ */ new Set();
|
|
3058
3208
|
addClient(ws) {
|
|
@@ -3076,11 +3226,13 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3076
3226
|
this.broadcast({ type: "autophase.resumed", payload: {} });
|
|
3077
3227
|
break;
|
|
3078
3228
|
case "autophase.stop":
|
|
3079
|
-
this.
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3229
|
+
await this.handleStop();
|
|
3230
|
+
break;
|
|
3231
|
+
case "autophase.clear":
|
|
3232
|
+
await this.handleClear();
|
|
3233
|
+
break;
|
|
3234
|
+
case "autophase.revert":
|
|
3235
|
+
await this.handleRevert();
|
|
3084
3236
|
break;
|
|
3085
3237
|
case "autophase.status":
|
|
3086
3238
|
this.broadcastState();
|
|
@@ -3157,17 +3309,27 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3157
3309
|
}
|
|
3158
3310
|
}
|
|
3159
3311
|
async handleStart(payload) {
|
|
3160
|
-
const
|
|
3312
|
+
const goal = payload?.goal || payload?.title || "Untitled Project";
|
|
3313
|
+
const title = deriveTitle(goal);
|
|
3161
3314
|
const autonomous = payload?.autonomous ?? true;
|
|
3162
|
-
|
|
3315
|
+
this.abort = new AbortController();
|
|
3316
|
+
this.stopping = false;
|
|
3317
|
+
const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, this.abort.signal);
|
|
3318
|
+
if (this.stopping || this.abort.signal.aborted) {
|
|
3319
|
+
this.broadcast({ type: "autophase.stopped", payload: { title } });
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3163
3322
|
this.logger.info(`[AutoPhase] Starting: ${title}`);
|
|
3164
|
-
const graph = await new PhaseGraphBuilder({ title, phases, autonomous }).build();
|
|
3323
|
+
const graph = await new PhaseGraphBuilder({ title, description: goal, phases, autonomous }).build();
|
|
3165
3324
|
this.graph = graph;
|
|
3166
|
-
this.abort = new AbortController();
|
|
3167
3325
|
await this.store.save(graph);
|
|
3168
|
-
|
|
3326
|
+
const useWorktrees = payload?.worktrees ?? process.env["WRONGSTACK_AUTOPHASE_WORKTREES"] !== "0";
|
|
3327
|
+
if (!this.worktrees && this.events && this.projectRoot && useWorktrees && isGitRepo(this.projectRoot)) {
|
|
3169
3328
|
this.worktrees = new WorktreeManager({ projectRoot: this.projectRoot, events: this.events });
|
|
3170
3329
|
}
|
|
3330
|
+
if (this.worktrees) {
|
|
3331
|
+
this.runBase = await this.worktrees.currentBase();
|
|
3332
|
+
}
|
|
3171
3333
|
this.orchestrator = new PhaseOrchestrator({
|
|
3172
3334
|
graph,
|
|
3173
3335
|
ctx: {
|
|
@@ -3214,6 +3376,62 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3214
3376
|
this.broadcast({ type: "autophase.failed", payload: { title, error: String(err) } });
|
|
3215
3377
|
});
|
|
3216
3378
|
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Halt the run NOW — at any phase. Sets `stopping` (so a planning turn that
|
|
3381
|
+
* resolves afterwards bails), aborts in-flight agents, stops the orchestrator
|
|
3382
|
+
* tick, and ends the live broadcast. The board is kept for review; use
|
|
3383
|
+
* `autophase.clear` to reset or `autophase.revert` to undo the changes.
|
|
3384
|
+
*/
|
|
3385
|
+
async handleStop() {
|
|
3386
|
+
this.stopping = true;
|
|
3387
|
+
this.abort?.abort();
|
|
3388
|
+
this.orchestrator?.stop();
|
|
3389
|
+
this.stopBroadcast();
|
|
3390
|
+
if (this.graph) await this.store.save(this.graph).catch(() => void 0);
|
|
3391
|
+
this.broadcast({ type: "autophase.stopped", payload: { title: this.graph?.title } });
|
|
3392
|
+
}
|
|
3393
|
+
/**
|
|
3394
|
+
* Stop + wipe: tear down phase worktrees and reset to an empty board so the UI
|
|
3395
|
+
* returns to the start screen ("new one"). Does NOT touch already-merged commits
|
|
3396
|
+
* on the base branch — that is `autophase.revert`.
|
|
3397
|
+
*/
|
|
3398
|
+
async handleClear() {
|
|
3399
|
+
await this.handleStop();
|
|
3400
|
+
if (this.worktrees) await this.worktrees.cleanupAllManaged().catch(() => void 0);
|
|
3401
|
+
this.orchestrator = null;
|
|
3402
|
+
this.graph = null;
|
|
3403
|
+
this.runBase = null;
|
|
3404
|
+
this.usedNicknames.clear();
|
|
3405
|
+
this.broadcast({ type: "autophase.cleared", payload: {} });
|
|
3406
|
+
this.broadcast({ type: "autophase.state", payload: this.buildState() });
|
|
3407
|
+
}
|
|
3408
|
+
/**
|
|
3409
|
+
* Stop + undo: remove phase worktrees, then history-preservingly `git revert`
|
|
3410
|
+
* every commit this run landed on the base branch (captured `runBase`..HEAD),
|
|
3411
|
+
* then reset to an empty board. Refuses (reports a reason) on a dirty tree or a
|
|
3412
|
+
* conflicting revert rather than leaving the tree half-reverted.
|
|
3413
|
+
*/
|
|
3414
|
+
async handleRevert() {
|
|
3415
|
+
await this.handleStop();
|
|
3416
|
+
if (!this.worktrees || !this.runBase || !this.projectRoot) {
|
|
3417
|
+
this.broadcast({
|
|
3418
|
+
type: "autophase.reverted",
|
|
3419
|
+
payload: { ok: false, reverted: 0, reason: "no git baseline was captured for this run" }
|
|
3420
|
+
});
|
|
3421
|
+
return;
|
|
3422
|
+
}
|
|
3423
|
+
await this.worktrees.cleanupAllManaged().catch(() => void 0);
|
|
3424
|
+
const shas = commitsSince(this.projectRoot, this.runBase.sha, this.runBase.branch);
|
|
3425
|
+
const res = await this.worktrees.revertCommits(this.runBase.branch, shas);
|
|
3426
|
+
this.broadcast({ type: "autophase.reverted", payload: res });
|
|
3427
|
+
if (res.ok) {
|
|
3428
|
+
this.orchestrator = null;
|
|
3429
|
+
this.graph = null;
|
|
3430
|
+
this.runBase = null;
|
|
3431
|
+
this.broadcast({ type: "autophase.cleared", payload: {} });
|
|
3432
|
+
this.broadcast({ type: "autophase.state", payload: this.buildState() });
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3217
3435
|
/** Generic fallback phases when the LLM planner produces nothing usable. */
|
|
3218
3436
|
defaultPhases() {
|
|
3219
3437
|
return [
|
|
@@ -3224,13 +3442,18 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3224
3442
|
{ name: "Deployment", description: "Deploy to production", priority: "medium", estimateHours: 2, parallelizable: false }
|
|
3225
3443
|
];
|
|
3226
3444
|
}
|
|
3227
|
-
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
3228
|
-
|
|
3445
|
+
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
3446
|
+
* The caller passes the run's abort signal so a stop during planning cancels
|
|
3447
|
+
* the LLM turn (the previous fresh, never-aborted controller made planning
|
|
3448
|
+
* uninterruptible). */
|
|
3449
|
+
async planPhases(goal, signal) {
|
|
3229
3450
|
try {
|
|
3230
3451
|
const planner = new AutoPhasePlanner({
|
|
3231
3452
|
goal,
|
|
3232
3453
|
runOnce: async (prompt) => {
|
|
3233
|
-
const result = await this.agent.run(prompt, {
|
|
3454
|
+
const result = await this.agent.run(prompt, {
|
|
3455
|
+
signal: signal ?? new AbortController().signal
|
|
3456
|
+
});
|
|
3234
3457
|
return result.status === "done" ? result.finalText ?? "" : "";
|
|
3235
3458
|
}
|
|
3236
3459
|
});
|
|
@@ -3356,15 +3579,37 @@ Type: ${task.type}`;
|
|
|
3356
3579
|
});
|
|
3357
3580
|
const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map(mapTask) : [];
|
|
3358
3581
|
const completedPhases = phases.filter((p) => p.status === "completed").length;
|
|
3582
|
+
const failedPhases = phases.filter((p) => p.status === "failed").length;
|
|
3583
|
+
const failedTasks = phases.reduce(
|
|
3584
|
+
(sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "failed").length,
|
|
3585
|
+
0
|
|
3586
|
+
);
|
|
3587
|
+
const lastFailed = phases.filter((p) => p.status === "failed").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
|
|
3588
|
+
const lastError = lastFailed ? `${lastFailed.name}: ${lastFailed.metadata?.integrationError ?? "phase failed"}` : null;
|
|
3359
3589
|
return {
|
|
3360
3590
|
title: this.graph.title,
|
|
3591
|
+
// Full operator prompt, shown verbatim in a dedicated goal block (the
|
|
3592
|
+
// title is only a short derived heading). Fall back to the title for
|
|
3593
|
+
// legacy boards saved before the title/goal split.
|
|
3594
|
+
goal: this.graph.description || this.graph.title,
|
|
3361
3595
|
phases: phaseItems,
|
|
3362
3596
|
tasks: taskItems,
|
|
3363
3597
|
activePhaseId: currentActiveId,
|
|
3364
3598
|
overallPercent: phases.length > 0 ? Math.round(completedPhases / phases.length * 100) : 0,
|
|
3365
3599
|
autonomous: this.graph.autonomous,
|
|
3366
3600
|
totalTasks,
|
|
3367
|
-
completedTasks
|
|
3601
|
+
completedTasks,
|
|
3602
|
+
// Structured progress + lastError consumed by the autophase store (were
|
|
3603
|
+
// defined client-side but never sent, so they stayed null on the board).
|
|
3604
|
+
progress: {
|
|
3605
|
+
totalPhases: phases.length,
|
|
3606
|
+
completed: completedPhases,
|
|
3607
|
+
failed: failedPhases,
|
|
3608
|
+
totalTasks,
|
|
3609
|
+
completedTasks,
|
|
3610
|
+
failedTasks
|
|
3611
|
+
},
|
|
3612
|
+
lastError
|
|
3368
3613
|
};
|
|
3369
3614
|
}
|
|
3370
3615
|
sendState(client) {
|
|
@@ -3657,6 +3902,12 @@ var SddBoardWebSocketHandler = class {
|
|
|
3657
3902
|
};
|
|
3658
3903
|
|
|
3659
3904
|
// src/server/sdd-wizard-ws-handler.ts
|
|
3905
|
+
function deriveTitle2(goal) {
|
|
3906
|
+
const firstLine = goal.split("\n").map((l) => l.trim()).find(Boolean);
|
|
3907
|
+
if (!firstLine) return "New SDD Project";
|
|
3908
|
+
const sentence = firstLine.split(/(?<=[.!?])\s/)[0] ?? firstLine;
|
|
3909
|
+
return sentence.length <= 64 ? sentence : `${sentence.slice(0, 63).trimEnd()}\u2026`;
|
|
3910
|
+
}
|
|
3660
3911
|
var SddWizardWebSocketHandler = class {
|
|
3661
3912
|
constructor(deps2) {
|
|
3662
3913
|
this.deps = deps2;
|
|
@@ -3695,7 +3946,8 @@ var SddWizardWebSocketHandler = class {
|
|
|
3695
3946
|
parallelSlots: msg.payload?.parallelSlots,
|
|
3696
3947
|
defaultModel: msg.payload?.model,
|
|
3697
3948
|
defaultProvider: msg.payload?.provider,
|
|
3698
|
-
fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0
|
|
3949
|
+
fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0,
|
|
3950
|
+
worktrees: typeof msg.payload?.worktrees === "boolean" ? msg.payload.worktrees : void 0
|
|
3699
3951
|
});
|
|
3700
3952
|
break;
|
|
3701
3953
|
}
|
|
@@ -3715,7 +3967,7 @@ var SddWizardWebSocketHandler = class {
|
|
|
3715
3967
|
}
|
|
3716
3968
|
if (this.busy) return;
|
|
3717
3969
|
this.driver = this.deps.makeDriver();
|
|
3718
|
-
const prompt = this.driver.start(goal);
|
|
3970
|
+
const prompt = this.driver.start(deriveTitle2(goal), goal);
|
|
3719
3971
|
await this.runTurn(prompt);
|
|
3720
3972
|
}
|
|
3721
3973
|
async onMessage(text) {
|
|
@@ -3786,7 +4038,7 @@ var SddWizardWebSocketHandler = class {
|
|
|
3786
4038
|
};
|
|
3787
4039
|
|
|
3788
4040
|
// src/server/sdd-wizard-wiring.ts
|
|
3789
|
-
import * as
|
|
4041
|
+
import * as path7 from "path";
|
|
3790
4042
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3791
4043
|
import {
|
|
3792
4044
|
makeCommandVerifier,
|
|
@@ -3800,6 +4052,7 @@ import {
|
|
|
3800
4052
|
TaskGraphStore as TaskGraphStore2,
|
|
3801
4053
|
WorktreeManager as WorktreeManager2
|
|
3802
4054
|
} from "@wrongstack/core";
|
|
4055
|
+
var PLANNING_ONLY_GUARD = "SYSTEM: You are running a PLANNING-ONLY specification interview. Do NOT write, create, or edit any files, and do NOT run shell/terminal commands or use any code-editing tools \u2014 they are disabled here and any attempt will fail and waste the turn. Respond with TEXT ONLY: ask your question, or emit the requested spec / plan / task JSON. All code is written later, automatically, once the plan is approved and the multi-agent run starts.\n\n---\n\n";
|
|
3803
4056
|
function buildSddWizardDeps(opts) {
|
|
3804
4057
|
const registry = new SddRunRegistry();
|
|
3805
4058
|
let isolatedSeq = 0;
|
|
@@ -3808,11 +4061,11 @@ function buildSddWizardDeps(opts) {
|
|
|
3808
4061
|
id: `sdd-${name2.toLowerCase().replace(/\s+/g, "-")}-${isolatedSeq++}`,
|
|
3809
4062
|
role: "executor",
|
|
3810
4063
|
name: name2,
|
|
3811
|
-
disabledTools: ["delegate"],
|
|
4064
|
+
disabledTools: ["delegate", "write", "edit", "patch", "bash", "exec"],
|
|
3812
4065
|
allowedCapabilities: ["fs.read", "net.outbound"]
|
|
3813
4066
|
});
|
|
3814
4067
|
try {
|
|
3815
|
-
const res = await result.agent.run([{ type: "text", text: prompt }]);
|
|
4068
|
+
const res = await result.agent.run([{ type: "text", text: PLANNING_ONLY_GUARD + prompt }]);
|
|
3816
4069
|
return res.finalText ?? "";
|
|
3817
4070
|
} finally {
|
|
3818
4071
|
await result.dispose?.();
|
|
@@ -3822,17 +4075,18 @@ function buildSddWizardDeps(opts) {
|
|
|
3822
4075
|
makeDriver: () => new SddInterviewDriver({
|
|
3823
4076
|
specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
|
|
3824
4077
|
graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
|
|
3825
|
-
sessionPath:
|
|
4078
|
+
sessionPath: path7.join(opts.paths.projectDir, "sdd-wizard-session.json")
|
|
3826
4079
|
}),
|
|
3827
4080
|
runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
|
|
3828
|
-
startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
|
|
4081
|
+
startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels, worktrees: useWorktrees }) => {
|
|
3829
4082
|
const graph = driver.getGraph();
|
|
3830
4083
|
const tracker = driver.getTracker();
|
|
3831
4084
|
if (!graph || !tracker) {
|
|
3832
4085
|
throw new Error("No task graph to run \u2014 finish the interview first.");
|
|
3833
4086
|
}
|
|
4087
|
+
const worktreesEnabled = useWorktrees ?? process.env["WRONGSTACK_SDD_WORKTREES"] !== "0";
|
|
3834
4088
|
let worktrees;
|
|
3835
|
-
if (
|
|
4089
|
+
if (worktreesEnabled) {
|
|
3836
4090
|
const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3837
4091
|
cwd: opts.projectRoot,
|
|
3838
4092
|
encoding: "utf8",
|
|
@@ -3891,9 +4145,6 @@ async function handleSddWizardRoute(_ws, msg, handlers) {
|
|
|
3891
4145
|
return true;
|
|
3892
4146
|
}
|
|
3893
4147
|
|
|
3894
|
-
// src/server/index.ts
|
|
3895
|
-
import { makeLightSubagentFactory } from "@wrongstack/runtime";
|
|
3896
|
-
|
|
3897
4148
|
// src/server/collaboration-ws-handler.ts
|
|
3898
4149
|
import { randomUUID } from "crypto";
|
|
3899
4150
|
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
@@ -4620,16 +4871,16 @@ var CollaborationWebSocketHandler = class {
|
|
|
4620
4871
|
};
|
|
4621
4872
|
|
|
4622
4873
|
// src/server/projects-manifest.ts
|
|
4623
|
-
import * as
|
|
4624
|
-
import * as
|
|
4874
|
+
import * as fs6 from "fs/promises";
|
|
4875
|
+
import * as path8 from "path";
|
|
4625
4876
|
import { projectSlug } from "@wrongstack/core";
|
|
4626
4877
|
function projectsJsonPath(globalConfigPath) {
|
|
4627
|
-
const base =
|
|
4628
|
-
return
|
|
4878
|
+
const base = path8.dirname(globalConfigPath);
|
|
4879
|
+
return path8.join(base, "projects.json");
|
|
4629
4880
|
}
|
|
4630
4881
|
async function loadManifest(globalConfigPath) {
|
|
4631
4882
|
try {
|
|
4632
|
-
const raw = await
|
|
4883
|
+
const raw = await fs6.readFile(projectsJsonPath(globalConfigPath), "utf8");
|
|
4633
4884
|
const parsed = JSON.parse(raw);
|
|
4634
4885
|
return { projects: parsed.projects ?? [] };
|
|
4635
4886
|
} catch {
|
|
@@ -4638,16 +4889,16 @@ async function loadManifest(globalConfigPath) {
|
|
|
4638
4889
|
}
|
|
4639
4890
|
async function saveManifest(manifest, globalConfigPath) {
|
|
4640
4891
|
const file = projectsJsonPath(globalConfigPath);
|
|
4641
|
-
await
|
|
4642
|
-
await
|
|
4892
|
+
await fs6.mkdir(path8.dirname(file), { recursive: true });
|
|
4893
|
+
await fs6.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
4643
4894
|
}
|
|
4644
4895
|
function generateProjectSlug(rootPath) {
|
|
4645
4896
|
return projectSlug(rootPath);
|
|
4646
4897
|
}
|
|
4647
4898
|
async function ensureProjectDataDir(slug, globalConfigPath) {
|
|
4648
|
-
const base =
|
|
4649
|
-
const dir =
|
|
4650
|
-
await
|
|
4899
|
+
const base = path8.dirname(globalConfigPath);
|
|
4900
|
+
const dir = path8.join(base, "projects", slug);
|
|
4901
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
4651
4902
|
return dir;
|
|
4652
4903
|
}
|
|
4653
4904
|
|
|
@@ -5073,14 +5324,14 @@ function registerShutdownHandlers(res) {
|
|
|
5073
5324
|
|
|
5074
5325
|
// src/server/instance-registry.ts
|
|
5075
5326
|
import * as os from "os";
|
|
5076
|
-
import * as
|
|
5077
|
-
import * as
|
|
5327
|
+
import * as path9 from "path";
|
|
5328
|
+
import * as fs7 from "fs/promises";
|
|
5078
5329
|
import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
|
|
5079
5330
|
function defaultBaseDir() {
|
|
5080
|
-
return
|
|
5331
|
+
return path9.join(os.homedir(), ".wrongstack");
|
|
5081
5332
|
}
|
|
5082
5333
|
function registryPath(baseDir = defaultBaseDir()) {
|
|
5083
|
-
return
|
|
5334
|
+
return path9.join(baseDir, "webui-instances.json");
|
|
5084
5335
|
}
|
|
5085
5336
|
function isPidAlive(pid) {
|
|
5086
5337
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -5093,7 +5344,7 @@ function isPidAlive(pid) {
|
|
|
5093
5344
|
}
|
|
5094
5345
|
async function load(file) {
|
|
5095
5346
|
try {
|
|
5096
|
-
const raw = await
|
|
5347
|
+
const raw = await fs7.readFile(file, "utf8");
|
|
5097
5348
|
const parsed = JSON.parse(raw);
|
|
5098
5349
|
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
5099
5350
|
return parsed;
|
|
@@ -5238,19 +5489,19 @@ function computeUsageCost(usage, rates) {
|
|
|
5238
5489
|
}
|
|
5239
5490
|
|
|
5240
5491
|
// src/server/provider-handlers.ts
|
|
5241
|
-
import { DefaultSecretScrubber
|
|
5492
|
+
import { DefaultSecretScrubber } from "@wrongstack/core";
|
|
5242
5493
|
import { probeLocalLlm } from "@wrongstack/runtime/probe";
|
|
5243
5494
|
|
|
5244
5495
|
// src/server/provider-config-io.ts
|
|
5245
|
-
import * as
|
|
5246
|
-
import * as
|
|
5496
|
+
import * as fs8 from "fs/promises";
|
|
5497
|
+
import * as path10 from "path";
|
|
5247
5498
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
5248
5499
|
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
5249
5500
|
import { DefaultSecretVault } from "@wrongstack/core";
|
|
5250
5501
|
async function loadSavedProviders(configPath, vault) {
|
|
5251
5502
|
let raw;
|
|
5252
5503
|
try {
|
|
5253
|
-
raw = await
|
|
5504
|
+
raw = await fs8.readFile(configPath, "utf8");
|
|
5254
5505
|
} catch {
|
|
5255
5506
|
return {};
|
|
5256
5507
|
}
|
|
@@ -5267,7 +5518,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
5267
5518
|
let raw;
|
|
5268
5519
|
let fileExists = true;
|
|
5269
5520
|
try {
|
|
5270
|
-
raw = await
|
|
5521
|
+
raw = await fs8.readFile(configPath, "utf8");
|
|
5271
5522
|
} catch (err) {
|
|
5272
5523
|
if (err.code !== "ENOENT") {
|
|
5273
5524
|
throw new Error(
|
|
@@ -5295,7 +5546,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
5295
5546
|
await atomicWrite4(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
5296
5547
|
}
|
|
5297
5548
|
function createProviderConfigIO(configPath) {
|
|
5298
|
-
const keyFile =
|
|
5549
|
+
const keyFile = path10.join(path10.dirname(configPath), ".key");
|
|
5299
5550
|
const vault = new DefaultSecretVault({ keyFile });
|
|
5300
5551
|
return {
|
|
5301
5552
|
load: () => loadSavedProviders(configPath, vault),
|
|
@@ -5424,7 +5675,7 @@ function projectSavedProviders(providers) {
|
|
|
5424
5675
|
return view;
|
|
5425
5676
|
});
|
|
5426
5677
|
}
|
|
5427
|
-
var probeScrubber = new
|
|
5678
|
+
var probeScrubber = new DefaultSecretScrubber();
|
|
5428
5679
|
function createProviderHandlers(deps2) {
|
|
5429
5680
|
const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
|
|
5430
5681
|
let configWriteLock = deps2.getConfigWriteLock();
|
|
@@ -5613,7 +5864,7 @@ function createProviderHandlers(deps2) {
|
|
|
5613
5864
|
|
|
5614
5865
|
// src/server/mode-handlers.ts
|
|
5615
5866
|
import {
|
|
5616
|
-
DefaultSystemPromptBuilder
|
|
5867
|
+
DefaultSystemPromptBuilder
|
|
5617
5868
|
} from "@wrongstack/core";
|
|
5618
5869
|
function createModeHandlers(ctx) {
|
|
5619
5870
|
return {
|
|
@@ -5661,7 +5912,7 @@ function createModeHandlers(ctx) {
|
|
|
5661
5912
|
}
|
|
5662
5913
|
ctx.setModeId(id);
|
|
5663
5914
|
const modePrompt = id === "default" ? "" : (await ctx.modeStore.getMode(id))?.prompt ?? "";
|
|
5664
|
-
const freshBuilder = new
|
|
5915
|
+
const freshBuilder = new DefaultSystemPromptBuilder({
|
|
5665
5916
|
memoryStore: ctx.memoryStore,
|
|
5666
5917
|
skillLoader: ctx.skillLoader,
|
|
5667
5918
|
modeStore: ctx.modeStore,
|
|
@@ -5692,40 +5943,10 @@ function createModeHandlers(ctx) {
|
|
|
5692
5943
|
import * as fs9 from "fs/promises";
|
|
5693
5944
|
import * as path11 from "path";
|
|
5694
5945
|
import {
|
|
5695
|
-
DefaultSessionStore
|
|
5696
|
-
DefaultSystemPromptBuilder as
|
|
5946
|
+
DefaultSessionStore,
|
|
5947
|
+
DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2,
|
|
5697
5948
|
getSessionRegistry
|
|
5698
5949
|
} from "@wrongstack/core";
|
|
5699
|
-
|
|
5700
|
-
// src/server/path-containment.ts
|
|
5701
|
-
import * as fs8 from "fs/promises";
|
|
5702
|
-
import * as path10 from "path";
|
|
5703
|
-
function isPathInside(root, target) {
|
|
5704
|
-
const relative3 = path10.relative(root, target);
|
|
5705
|
-
return relative3 === "" || !relative3.startsWith("..") && !path10.isAbsolute(relative3);
|
|
5706
|
-
}
|
|
5707
|
-
async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
|
|
5708
|
-
const resolved = path10.resolve(projectRoot, inputPath);
|
|
5709
|
-
let stat3;
|
|
5710
|
-
try {
|
|
5711
|
-
stat3 = await fs8.stat(resolved);
|
|
5712
|
-
} catch {
|
|
5713
|
-
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
5714
|
-
}
|
|
5715
|
-
if (!stat3.isDirectory()) {
|
|
5716
|
-
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
5717
|
-
}
|
|
5718
|
-
const [realProjectRoot, realResolved] = await Promise.all([
|
|
5719
|
-
fs8.realpath(projectRoot),
|
|
5720
|
-
fs8.realpath(resolved)
|
|
5721
|
-
]);
|
|
5722
|
-
if (!isPathInside(realProjectRoot, realResolved)) {
|
|
5723
|
-
throw new Error(`Path must stay inside the project root: ${projectRoot}`);
|
|
5724
|
-
}
|
|
5725
|
-
return resolved;
|
|
5726
|
-
}
|
|
5727
|
-
|
|
5728
|
-
// src/server/project-handlers.ts
|
|
5729
5950
|
function createProjectHandlers(ctx) {
|
|
5730
5951
|
return {
|
|
5731
5952
|
listProjects: async (ws) => {
|
|
@@ -5837,7 +6058,7 @@ function createProjectHandlers(ctx) {
|
|
|
5837
6058
|
try {
|
|
5838
6059
|
const modeId = ctx.getModeId();
|
|
5839
6060
|
const switchMode = modeId === "default" ? void 0 : await ctx.modeStore.getMode(modeId);
|
|
5840
|
-
const switchBuilder = new
|
|
6061
|
+
const switchBuilder = new DefaultSystemPromptBuilder2({
|
|
5841
6062
|
memoryStore: ctx.memoryStore,
|
|
5842
6063
|
skillLoader: ctx.skillLoader,
|
|
5843
6064
|
modeStore: ctx.modeStore,
|
|
@@ -5861,7 +6082,7 @@ function createProjectHandlers(ctx) {
|
|
|
5861
6082
|
"sessions"
|
|
5862
6083
|
);
|
|
5863
6084
|
await fs9.mkdir(newSessionsDir, { recursive: true });
|
|
5864
|
-
const newSessionStore = new
|
|
6085
|
+
const newSessionStore = new DefaultSessionStore({ dir: newSessionsDir });
|
|
5865
6086
|
const oldSession = ctx.getSession();
|
|
5866
6087
|
const oldSessionId = oldSession.id;
|
|
5867
6088
|
try {
|
|
@@ -6558,6 +6779,22 @@ async function handleModeRoute(ws, msg, handlers) {
|
|
|
6558
6779
|
}
|
|
6559
6780
|
}
|
|
6560
6781
|
|
|
6782
|
+
// src/server/prefs-routes.ts
|
|
6783
|
+
async function handlePrefsRoute(ws, msg, handlers) {
|
|
6784
|
+
switch (msg.type) {
|
|
6785
|
+
case "prefs.get": {
|
|
6786
|
+
await handlers.getPrefs(ws);
|
|
6787
|
+
return true;
|
|
6788
|
+
}
|
|
6789
|
+
case "prefs.update": {
|
|
6790
|
+
await handlers.updatePrefs(ws, msg.payload ?? {});
|
|
6791
|
+
return true;
|
|
6792
|
+
}
|
|
6793
|
+
default:
|
|
6794
|
+
return false;
|
|
6795
|
+
}
|
|
6796
|
+
}
|
|
6797
|
+
|
|
6561
6798
|
// src/server/shell-git-routes.ts
|
|
6562
6799
|
async function handleShellGitRoute(ws, msg, handlers) {
|
|
6563
6800
|
switch (msg.type) {
|
|
@@ -6598,6 +6835,44 @@ async function handleMailboxRoute(ws, msg, handlers) {
|
|
|
6598
6835
|
}
|
|
6599
6836
|
}
|
|
6600
6837
|
|
|
6838
|
+
// src/server/mcp-routes.ts
|
|
6839
|
+
async function handleMcpRoute(ws, msg, handlers) {
|
|
6840
|
+
switch (msg.type) {
|
|
6841
|
+
case "mcp.list":
|
|
6842
|
+
await handlers.list(ws, msg);
|
|
6843
|
+
return true;
|
|
6844
|
+
case "mcp.add":
|
|
6845
|
+
await handlers.add(ws, msg);
|
|
6846
|
+
return true;
|
|
6847
|
+
case "mcp.update":
|
|
6848
|
+
await handlers.update(ws, msg);
|
|
6849
|
+
return true;
|
|
6850
|
+
case "mcp.remove":
|
|
6851
|
+
await handlers.remove(ws, msg);
|
|
6852
|
+
return true;
|
|
6853
|
+
case "mcp.enable":
|
|
6854
|
+
await handlers.enable(ws, msg);
|
|
6855
|
+
return true;
|
|
6856
|
+
case "mcp.disable":
|
|
6857
|
+
await handlers.disable(ws, msg);
|
|
6858
|
+
return true;
|
|
6859
|
+
case "mcp.sleep":
|
|
6860
|
+
await handlers.sleep(ws, msg);
|
|
6861
|
+
return true;
|
|
6862
|
+
case "mcp.wake":
|
|
6863
|
+
await handlers.wake(ws, msg);
|
|
6864
|
+
return true;
|
|
6865
|
+
case "mcp.restart":
|
|
6866
|
+
await handlers.restart(ws, msg);
|
|
6867
|
+
return true;
|
|
6868
|
+
case "mcp.discover":
|
|
6869
|
+
await handlers.discover(ws, msg);
|
|
6870
|
+
return true;
|
|
6871
|
+
default:
|
|
6872
|
+
return false;
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
|
|
6601
6876
|
// src/server/brain-routes.ts
|
|
6602
6877
|
async function handleBrainRoute(ws, msg, handlers) {
|
|
6603
6878
|
switch (msg.type) {
|
|
@@ -7069,11 +7344,13 @@ function setupEvents(deps2) {
|
|
|
7069
7344
|
events.on("provider.response", (e) => {
|
|
7070
7345
|
if (e.usage?.input != null) {
|
|
7071
7346
|
const maxCtx = context.provider.capabilities.maxContext;
|
|
7072
|
-
const
|
|
7347
|
+
const rawLoad = maxCtx > 0 ? e.usage.input / maxCtx : 0;
|
|
7348
|
+
const load2 = Math.max(0, Math.min(1, rawLoad));
|
|
7073
7349
|
const costUsd = context.tokenCounter.estimateCost().total;
|
|
7074
7350
|
forwardSubagent("ctx_pct", {
|
|
7075
7351
|
subagentId: "leader",
|
|
7076
|
-
load:
|
|
7352
|
+
load: load2,
|
|
7353
|
+
rawLoad,
|
|
7077
7354
|
tokens: e.usage.input,
|
|
7078
7355
|
maxContext: maxCtx,
|
|
7079
7356
|
costUsd
|
|
@@ -7724,9 +8001,13 @@ async function handleGoalGet(projectRoot, broadcast2) {
|
|
|
7724
8001
|
|
|
7725
8002
|
// src/server/index.ts
|
|
7726
8003
|
async function startWebUI(opts = {}) {
|
|
8004
|
+
ensureSessionShell();
|
|
7727
8005
|
const requestedWsPort = opts.wsPort ?? 3457;
|
|
7728
|
-
const wsHost = opts.wsHost ?? "127.0.0.1";
|
|
7729
|
-
const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
8006
|
+
const wsHost = opts.wsHost ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
|
|
8007
|
+
const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? "3456", 10);
|
|
8008
|
+
const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
|
|
8009
|
+
const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
|
|
8010
|
+
const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
|
|
7730
8011
|
const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
|
|
7731
8012
|
let wsPort = requestedWsPort;
|
|
7732
8013
|
let httpPort = requestedHttpPort;
|
|
@@ -7805,7 +8086,7 @@ async function startWebUI(opts = {}) {
|
|
|
7805
8086
|
ttlSeconds: 24 * 3600
|
|
7806
8087
|
});
|
|
7807
8088
|
const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry });
|
|
7808
|
-
const configStore = opts.services?.configStore ?? container.resolve(
|
|
8089
|
+
const configStore = opts.services?.configStore ?? container.resolve(TOKENS.ConfigStore);
|
|
7809
8090
|
const providerRegistry = new ProviderRegistry();
|
|
7810
8091
|
try {
|
|
7811
8092
|
const factories = await buildProviderFactoriesFromRegistry({
|
|
@@ -7827,7 +8108,7 @@ async function startWebUI(opts = {}) {
|
|
|
7827
8108
|
r.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
|
|
7828
8109
|
return r;
|
|
7829
8110
|
})();
|
|
7830
|
-
const memoryStore = new
|
|
8111
|
+
const memoryStore = new DefaultMemoryStore({ paths: wpaths });
|
|
7831
8112
|
if (config.features.memory) {
|
|
7832
8113
|
toolRegistry.register(rememberTool(memoryStore));
|
|
7833
8114
|
toolRegistry.register(forgetTool(memoryStore));
|
|
@@ -7840,6 +8121,7 @@ async function startWebUI(opts = {}) {
|
|
|
7840
8121
|
toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
|
|
7841
8122
|
toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
|
|
7842
8123
|
applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
|
|
8124
|
+
configureExecPolicy(config.tools?.exec ?? {});
|
|
7843
8125
|
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
7844
8126
|
const mcpRegistry = new MCPRegistry({
|
|
7845
8127
|
toolRegistry,
|
|
@@ -7856,7 +8138,7 @@ async function startWebUI(opts = {}) {
|
|
|
7856
8138
|
});
|
|
7857
8139
|
}
|
|
7858
8140
|
}
|
|
7859
|
-
let sessionStore = opts.services?.session ?? new
|
|
8141
|
+
let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
|
|
7860
8142
|
if (!opts.services?.session) {
|
|
7861
8143
|
sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
|
|
7862
8144
|
if (count > 0) logger.info(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
|
|
@@ -7944,11 +8226,11 @@ async function startWebUI(opts = {}) {
|
|
|
7944
8226
|
});
|
|
7945
8227
|
} catch {
|
|
7946
8228
|
}
|
|
7947
|
-
const tokenCounter = new
|
|
8229
|
+
const tokenCounter = new DefaultTokenCounter({
|
|
7948
8230
|
registry: modelsRegistry,
|
|
7949
8231
|
providerId: config.provider
|
|
7950
8232
|
});
|
|
7951
|
-
const modeStore = new
|
|
8233
|
+
const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
|
|
7952
8234
|
const activeMode = await modeStore.getActiveMode();
|
|
7953
8235
|
let modeId = activeMode?.id ?? "default";
|
|
7954
8236
|
const modePrompt = activeMode?.prompt ?? "";
|
|
@@ -7969,7 +8251,7 @@ async function startWebUI(opts = {}) {
|
|
|
7969
8251
|
const modelCapabilitiesRef = {
|
|
7970
8252
|
current: modelCapabilities
|
|
7971
8253
|
};
|
|
7972
|
-
const skillLoader = config.features.skills ? new
|
|
8254
|
+
const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
|
|
7973
8255
|
const skillInstaller = config.features.skills ? new SkillInstaller({
|
|
7974
8256
|
manifestPath: path16.join(wstackGlobalRoot2(), "installed-skills.json"),
|
|
7975
8257
|
projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
|
|
@@ -7977,7 +8259,7 @@ async function startWebUI(opts = {}) {
|
|
|
7977
8259
|
projectHash: projectHash(projectRoot),
|
|
7978
8260
|
skillLoader
|
|
7979
8261
|
}) : void 0;
|
|
7980
|
-
const systemPromptBuilder = new
|
|
8262
|
+
const systemPromptBuilder = new DefaultSystemPromptBuilder3({
|
|
7981
8263
|
memoryStore,
|
|
7982
8264
|
skillLoader,
|
|
7983
8265
|
modeStore,
|
|
@@ -8267,7 +8549,7 @@ async function startWebUI(opts = {}) {
|
|
|
8267
8549
|
projectRoot,
|
|
8268
8550
|
logger
|
|
8269
8551
|
});
|
|
8270
|
-
const compactor =
|
|
8552
|
+
const compactor = createStrategyCompactor({
|
|
8271
8553
|
strategy: config.context?.strategy,
|
|
8272
8554
|
preserveK: config.context?.preserveK ?? 10,
|
|
8273
8555
|
eliseThreshold: config.context?.eliseThreshold ?? 2e3,
|
|
@@ -8343,9 +8625,9 @@ async function startWebUI(opts = {}) {
|
|
|
8343
8625
|
maxContext: newMaxContext
|
|
8344
8626
|
});
|
|
8345
8627
|
}
|
|
8346
|
-
const secretScrubber = container.resolve(
|
|
8347
|
-
const renderer = container.has(
|
|
8348
|
-
const permissionPolicy = container.resolve(
|
|
8628
|
+
const secretScrubber = container.resolve(TOKENS.SecretScrubber);
|
|
8629
|
+
const renderer = container.has(TOKENS.Renderer) ? container.resolve(TOKENS.Renderer) : void 0;
|
|
8630
|
+
const permissionPolicy = container.resolve(TOKENS.PermissionPolicy);
|
|
8349
8631
|
const toolExecutor = new ToolExecutor(toolRegistry, {
|
|
8350
8632
|
permissionPolicy,
|
|
8351
8633
|
secretScrubber,
|
|
@@ -8388,7 +8670,7 @@ async function startWebUI(opts = {}) {
|
|
|
8388
8670
|
}),
|
|
8389
8671
|
events
|
|
8390
8672
|
);
|
|
8391
|
-
container.bind(
|
|
8673
|
+
container.bind(TOKENS.BrainArbiter, () => brain);
|
|
8392
8674
|
const brainMailbox = new GlobalMailbox2(wpaths.projectDir, events);
|
|
8393
8675
|
const brainMonitor = new BrainMonitor({
|
|
8394
8676
|
events,
|
|
@@ -8520,8 +8802,16 @@ async function startWebUI(opts = {}) {
|
|
|
8520
8802
|
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID2)
|
|
8521
8803
|
};
|
|
8522
8804
|
}
|
|
8523
|
-
const wsToken =
|
|
8524
|
-
console.log("[WebUI] WS auth token
|
|
8805
|
+
const wsToken = resolveAuthToken(opts.accessToken);
|
|
8806
|
+
console.log("[WebUI] WS auth token ready");
|
|
8807
|
+
const publicHostnames = [publicUrl, publicWsUrl].map((value) => {
|
|
8808
|
+
if (!value) return void 0;
|
|
8809
|
+
try {
|
|
8810
|
+
return new URL(value).hostname;
|
|
8811
|
+
} catch {
|
|
8812
|
+
return void 0;
|
|
8813
|
+
}
|
|
8814
|
+
}).filter((value) => Boolean(value));
|
|
8525
8815
|
const verifyClient2 = (info) => verifyClient({
|
|
8526
8816
|
origin: info.origin,
|
|
8527
8817
|
url: info.req.url ?? "",
|
|
@@ -8533,7 +8823,10 @@ async function startWebUI(opts = {}) {
|
|
|
8533
8823
|
// exposure class.
|
|
8534
8824
|
cookieHeader: info.req.headers.cookie,
|
|
8535
8825
|
wsHost,
|
|
8536
|
-
expectedToken: wsToken
|
|
8826
|
+
expectedToken: wsToken,
|
|
8827
|
+
requireToken,
|
|
8828
|
+
allowedHostnames: publicHostnames,
|
|
8829
|
+
allowBrowserUrlToken: Boolean(publicWsUrl)
|
|
8537
8830
|
});
|
|
8538
8831
|
const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
|
|
8539
8832
|
const wssPrimary = new WebSocketServer({
|
|
@@ -8768,8 +9061,10 @@ async function startWebUI(opts = {}) {
|
|
|
8768
9061
|
let sessionRoutes;
|
|
8769
9062
|
let projectRoutes;
|
|
8770
9063
|
let modeRoutes;
|
|
9064
|
+
let prefsRoutes;
|
|
8771
9065
|
let shellGitRoutes;
|
|
8772
9066
|
let mailboxRoutes;
|
|
9067
|
+
let mcpRoutes;
|
|
8773
9068
|
let brainRoutes;
|
|
8774
9069
|
let autoPhaseRoutes;
|
|
8775
9070
|
let specsRoutes;
|
|
@@ -8780,8 +9075,10 @@ async function startWebUI(opts = {}) {
|
|
|
8780
9075
|
if (await handleSessionRoute(ws, msg, sessionRoutes)) return;
|
|
8781
9076
|
if (await handleProjectRoute(ws, msg, projectRoutes)) return;
|
|
8782
9077
|
if (await handleModeRoute(ws, msg, modeRoutes)) return;
|
|
9078
|
+
if (await handlePrefsRoute(ws, msg, prefsRoutes)) return;
|
|
8783
9079
|
if (await handleShellGitRoute(ws, msg, shellGitRoutes)) return;
|
|
8784
9080
|
if (await handleMailboxRoute(ws, msg, mailboxRoutes)) return;
|
|
9081
|
+
if (await handleMcpRoute(ws, msg, mcpRoutes)) return;
|
|
8785
9082
|
if (await handleBrainRoute(ws, msg, brainRoutes)) return;
|
|
8786
9083
|
if (await handleAutoPhaseRoute(ws, msg, autoPhaseRoutes)) return;
|
|
8787
9084
|
if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
|
|
@@ -8892,27 +9189,31 @@ async function startWebUI(opts = {}) {
|
|
|
8892
9189
|
case "memory.forget":
|
|
8893
9190
|
return handleMemoryForget(ws, msg, memoryStore);
|
|
8894
9191
|
// ── MCP operations — delegated to shared handlers (mcp-handlers.ts),
|
|
8895
|
-
// backed by the live MCPRegistry constructed above.
|
|
9192
|
+
// backed by the live MCPRegistry constructed above. Routed via
|
|
9193
|
+
// handleMcpRoute (see mcpRoutes = { ... } below). These case arms
|
|
9194
|
+
// are unreachable but left as tripwires for any future regression
|
|
9195
|
+
// where the route chain stops claiming 'mcp.*'. If you see one
|
|
9196
|
+
// fire, fix the dispatch order in the handleMessage chain above.
|
|
8896
9197
|
case "mcp.list":
|
|
8897
|
-
|
|
9198
|
+
throw new Error("handleMcpRoute did not claim mcp.list \u2014 check chain order");
|
|
8898
9199
|
case "mcp.add":
|
|
8899
|
-
|
|
8900
|
-
case "mcp.remove":
|
|
8901
|
-
return handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry);
|
|
9200
|
+
throw new Error("handleMcpRoute did not claim mcp.add \u2014 check chain order");
|
|
8902
9201
|
case "mcp.update":
|
|
8903
|
-
|
|
8904
|
-
case "mcp.
|
|
8905
|
-
|
|
8906
|
-
case "mcp.sleep":
|
|
8907
|
-
return handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry);
|
|
8908
|
-
case "mcp.discover":
|
|
8909
|
-
return handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry);
|
|
9202
|
+
throw new Error("handleMcpRoute did not claim mcp.update \u2014 check chain order");
|
|
9203
|
+
case "mcp.remove":
|
|
9204
|
+
throw new Error("handleMcpRoute did not claim mcp.remove \u2014 check chain order");
|
|
8910
9205
|
case "mcp.enable":
|
|
8911
|
-
|
|
9206
|
+
throw new Error("handleMcpRoute did not claim mcp.enable \u2014 check chain order");
|
|
8912
9207
|
case "mcp.disable":
|
|
8913
|
-
|
|
9208
|
+
throw new Error("handleMcpRoute did not claim mcp.disable \u2014 check chain order");
|
|
9209
|
+
case "mcp.sleep":
|
|
9210
|
+
throw new Error("handleMcpRoute did not claim mcp.sleep \u2014 check chain order");
|
|
9211
|
+
case "mcp.wake":
|
|
9212
|
+
throw new Error("handleMcpRoute did not claim mcp.wake \u2014 check chain order");
|
|
8914
9213
|
case "mcp.restart":
|
|
8915
|
-
|
|
9214
|
+
throw new Error("handleMcpRoute did not claim mcp.restart \u2014 check chain order");
|
|
9215
|
+
case "mcp.discover":
|
|
9216
|
+
throw new Error("handleMcpRoute did not claim mcp.discover \u2014 check chain order");
|
|
8916
9217
|
// Skills — full request→response cycle lives in skills-handlers.ts
|
|
8917
9218
|
// (shared with the CLI's embedded server). skillsCtx is the closed-over
|
|
8918
9219
|
// loader/installer/projectRoot the handlers need.
|
|
@@ -9060,53 +9361,11 @@ async function startWebUI(opts = {}) {
|
|
|
9060
9361
|
break;
|
|
9061
9362
|
}
|
|
9062
9363
|
case "prefs.update": {
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
sendResult2(ws, false, parsed.message);
|
|
9066
|
-
break;
|
|
9067
|
-
}
|
|
9068
|
-
const payload = parsed.value.prefs;
|
|
9069
|
-
for (const [key, val] of Object.entries(payload)) {
|
|
9070
|
-
context.meta[key] = val;
|
|
9071
|
-
}
|
|
9072
|
-
void persistPrefsToConfig(payload);
|
|
9073
|
-
if (typeof payload["yolo"] === "boolean") {
|
|
9074
|
-
permissionPolicy.setYolo?.(payload["yolo"]);
|
|
9075
|
-
}
|
|
9076
|
-
if (typeof payload["featureMcp"] === "boolean")
|
|
9077
|
-
config.features.mcp = payload["featureMcp"];
|
|
9078
|
-
if (typeof payload["featurePlugins"] === "boolean")
|
|
9079
|
-
config.features.plugins = payload["featurePlugins"];
|
|
9080
|
-
if (typeof payload["featureMemory"] === "boolean")
|
|
9081
|
-
config.features.memory = payload["featureMemory"];
|
|
9082
|
-
if (typeof payload["featureSkills"] === "boolean")
|
|
9083
|
-
config.features.skills = payload["featureSkills"];
|
|
9084
|
-
if (typeof payload["featureModelsRegistry"] === "boolean")
|
|
9085
|
-
config.features.modelsRegistry = payload["featureModelsRegistry"];
|
|
9086
|
-
if (Array.isArray(payload["fallbackModels"]))
|
|
9087
|
-
config.fallbackModels = payload["fallbackModels"];
|
|
9088
|
-
if (typeof payload["fallbackAuto"] === "boolean")
|
|
9089
|
-
config.fallbackAuto = payload["fallbackAuto"];
|
|
9090
|
-
if (typeof payload["contextAutoCompact"] === "boolean") {
|
|
9091
|
-
if (payload["contextAutoCompact"] && autoCompactor) {
|
|
9092
|
-
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
9093
|
-
pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
|
|
9094
|
-
} else {
|
|
9095
|
-
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
9096
|
-
}
|
|
9097
|
-
}
|
|
9098
|
-
if (typeof payload["logLevel"] === "string") {
|
|
9099
|
-
const valid = ["debug", "info", "warn", "error"];
|
|
9100
|
-
if (valid.includes(payload["logLevel"])) {
|
|
9101
|
-
logger.level = payload["logLevel"];
|
|
9102
|
-
}
|
|
9103
|
-
}
|
|
9104
|
-
broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
|
|
9105
|
-
break;
|
|
9364
|
+
void ws;
|
|
9365
|
+
throw new Error("handlePrefsRoute did not claim prefs.update \u2014 check chain order");
|
|
9106
9366
|
}
|
|
9107
9367
|
case "prefs.get": {
|
|
9108
|
-
|
|
9109
|
-
break;
|
|
9368
|
+
throw new Error("handlePrefsRoute did not claim prefs.get \u2014 check chain order");
|
|
9110
9369
|
}
|
|
9111
9370
|
default:
|
|
9112
9371
|
send(ws, {
|
|
@@ -9327,6 +9586,55 @@ async function startWebUI(opts = {}) {
|
|
|
9327
9586
|
},
|
|
9328
9587
|
sessionStartPayload
|
|
9329
9588
|
});
|
|
9589
|
+
prefsRoutes = {
|
|
9590
|
+
getPrefs: async (ws) => {
|
|
9591
|
+
send(ws, { type: "prefs.updated", payload: prefSnapshot() });
|
|
9592
|
+
},
|
|
9593
|
+
updatePrefs: async (ws, msgPayload) => {
|
|
9594
|
+
const parsed = validatePrefsUpdatePayload(msgPayload);
|
|
9595
|
+
if (!parsed.ok) {
|
|
9596
|
+
sendResult2(ws, false, parsed.message);
|
|
9597
|
+
return;
|
|
9598
|
+
}
|
|
9599
|
+
const payload = parsed.value.prefs;
|
|
9600
|
+
for (const [key, val] of Object.entries(payload)) {
|
|
9601
|
+
context.meta[key] = val;
|
|
9602
|
+
}
|
|
9603
|
+
void persistPrefsToConfig(payload);
|
|
9604
|
+
if (typeof payload["yolo"] === "boolean") {
|
|
9605
|
+
permissionPolicy.setYolo?.(payload["yolo"]);
|
|
9606
|
+
}
|
|
9607
|
+
if (typeof payload["featureMcp"] === "boolean")
|
|
9608
|
+
config.features.mcp = payload["featureMcp"];
|
|
9609
|
+
if (typeof payload["featurePlugins"] === "boolean")
|
|
9610
|
+
config.features.plugins = payload["featurePlugins"];
|
|
9611
|
+
if (typeof payload["featureMemory"] === "boolean")
|
|
9612
|
+
config.features.memory = payload["featureMemory"];
|
|
9613
|
+
if (typeof payload["featureSkills"] === "boolean")
|
|
9614
|
+
config.features.skills = payload["featureSkills"];
|
|
9615
|
+
if (typeof payload["featureModelsRegistry"] === "boolean")
|
|
9616
|
+
config.features.modelsRegistry = payload["featureModelsRegistry"];
|
|
9617
|
+
if (Array.isArray(payload["fallbackModels"]))
|
|
9618
|
+
config.fallbackModels = payload["fallbackModels"];
|
|
9619
|
+
if (typeof payload["fallbackAuto"] === "boolean")
|
|
9620
|
+
config.fallbackAuto = payload["fallbackAuto"];
|
|
9621
|
+
if (typeof payload["contextAutoCompact"] === "boolean") {
|
|
9622
|
+
if (payload["contextAutoCompact"] && autoCompactor) {
|
|
9623
|
+
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
9624
|
+
pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
|
|
9625
|
+
} else {
|
|
9626
|
+
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
9627
|
+
}
|
|
9628
|
+
}
|
|
9629
|
+
if (typeof payload["logLevel"] === "string") {
|
|
9630
|
+
const valid = ["debug", "info", "warn", "error"];
|
|
9631
|
+
if (valid.includes(payload["logLevel"])) {
|
|
9632
|
+
logger.level = payload["logLevel"];
|
|
9633
|
+
}
|
|
9634
|
+
}
|
|
9635
|
+
broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
|
|
9636
|
+
}
|
|
9637
|
+
};
|
|
9330
9638
|
shellGitRoutes = {
|
|
9331
9639
|
gitInfo: async (ws) => {
|
|
9332
9640
|
await handleGitInfo(ws, projectRoot);
|
|
@@ -9379,6 +9687,18 @@ async function startWebUI(opts = {}) {
|
|
|
9379
9687
|
return handleMailboxPurge(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
|
|
9380
9688
|
}
|
|
9381
9689
|
};
|
|
9690
|
+
mcpRoutes = {
|
|
9691
|
+
list: (ws, msg) => handleMcpList(ws, msg, globalConfigPath, mcpRegistry),
|
|
9692
|
+
add: (ws, msg) => handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry),
|
|
9693
|
+
update: (ws, msg) => handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry),
|
|
9694
|
+
remove: (ws, msg) => handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry),
|
|
9695
|
+
enable: (ws, msg) => handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry),
|
|
9696
|
+
disable: (ws, msg) => handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry),
|
|
9697
|
+
sleep: (ws, msg) => handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry),
|
|
9698
|
+
wake: (ws, msg) => handleMcpWake(ws, msg, globalConfigPath, mcpRegistry),
|
|
9699
|
+
restart: (ws, msg) => handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry),
|
|
9700
|
+
discover: (ws, msg) => handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry)
|
|
9701
|
+
};
|
|
9382
9702
|
brainRoutes = {
|
|
9383
9703
|
status: (ws) => {
|
|
9384
9704
|
send(ws, {
|
|
@@ -9446,8 +9766,10 @@ async function startWebUI(opts = {}) {
|
|
|
9446
9766
|
host: wsHost,
|
|
9447
9767
|
distDir: path16.resolve(import.meta.dirname, "../../dist"),
|
|
9448
9768
|
wsPort,
|
|
9769
|
+
publicWsUrl,
|
|
9449
9770
|
globalRoot: wpaths.globalRoot,
|
|
9450
9771
|
apiToken: wsToken,
|
|
9772
|
+
requireToken,
|
|
9451
9773
|
watcherMetrics,
|
|
9452
9774
|
onFleetPing: () => {
|
|
9453
9775
|
void fleetBroadcast?.();
|
|
@@ -9455,7 +9777,12 @@ async function startWebUI(opts = {}) {
|
|
|
9455
9777
|
});
|
|
9456
9778
|
const registryBaseDir = path16.dirname(globalConfigPath);
|
|
9457
9779
|
httpServer.listen(httpPort, wsHost, () => {
|
|
9458
|
-
const openUrl =
|
|
9780
|
+
const openUrl = buildWebUIAccessUrl({
|
|
9781
|
+
host: wsHost,
|
|
9782
|
+
port: httpPort,
|
|
9783
|
+
token: wsToken,
|
|
9784
|
+
publicUrl
|
|
9785
|
+
});
|
|
9459
9786
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
9460
9787
|
if (opts.open) openBrowser(openUrl);
|
|
9461
9788
|
void registerInstance(
|
|
@@ -9467,7 +9794,7 @@ async function startWebUI(opts = {}) {
|
|
|
9467
9794
|
projectRoot,
|
|
9468
9795
|
projectName: path16.basename(projectRoot) || projectRoot,
|
|
9469
9796
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9470
|
-
url:
|
|
9797
|
+
url: buildWebUIAccessUrl({ host: wsHost, port: httpPort, publicUrl })
|
|
9471
9798
|
},
|
|
9472
9799
|
registryBaseDir
|
|
9473
9800
|
).catch((err) => console.warn(JSON.stringify({
|
|
@@ -9517,6 +9844,7 @@ export {
|
|
|
9517
9844
|
browserOpenCommand,
|
|
9518
9845
|
buildCspHeader,
|
|
9519
9846
|
buildSddWizardDeps,
|
|
9847
|
+
buildWebUIAccessUrl,
|
|
9520
9848
|
createCustomModeStore,
|
|
9521
9849
|
createEternalSubscription,
|
|
9522
9850
|
createHttpServer,
|
|
@@ -9524,6 +9852,7 @@ export {
|
|
|
9524
9852
|
createToolLspCompletionSource,
|
|
9525
9853
|
defaultBaseDir,
|
|
9526
9854
|
deleteKey,
|
|
9855
|
+
envFlag,
|
|
9527
9856
|
errMessage,
|
|
9528
9857
|
estimateTokens,
|
|
9529
9858
|
extractToken,
|
|
@@ -9559,6 +9888,7 @@ export {
|
|
|
9559
9888
|
handleSkillsInstall,
|
|
9560
9889
|
handleSkillsUninstall,
|
|
9561
9890
|
handleSkillsUpdate,
|
|
9891
|
+
hostForBrowserUrl,
|
|
9562
9892
|
hostHeaderOk,
|
|
9563
9893
|
injectWsPort,
|
|
9564
9894
|
isLoopbackBind,
|
|
@@ -9574,6 +9904,7 @@ export {
|
|
|
9574
9904
|
registerInstance,
|
|
9575
9905
|
registryPath,
|
|
9576
9906
|
removeProvider,
|
|
9907
|
+
resolveAuthToken,
|
|
9577
9908
|
saveProviders,
|
|
9578
9909
|
send,
|
|
9579
9910
|
sendResult2 as sendResult,
|