@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/entry.js
CHANGED
|
@@ -897,7 +897,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
897
897
|
return;
|
|
898
898
|
}
|
|
899
899
|
try {
|
|
900
|
-
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore:
|
|
900
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
|
|
901
901
|
const registry = new SessionRegistry(globalRoot);
|
|
902
902
|
const entry = await registry.get(sessionId);
|
|
903
903
|
if (!entry) {
|
|
@@ -906,7 +906,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
906
906
|
return;
|
|
907
907
|
}
|
|
908
908
|
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
909
|
-
const store = new
|
|
909
|
+
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
910
910
|
const reader = new DefaultSessionReader2({ store });
|
|
911
911
|
const rawEntries = [];
|
|
912
912
|
for await (const ev of reader.replay(sessionId)) {
|
|
@@ -1167,7 +1167,7 @@ function isTrustedLoopbackOrigin(origin) {
|
|
|
1167
1167
|
try {
|
|
1168
1168
|
const url = new URL(origin);
|
|
1169
1169
|
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
1170
|
-
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
1170
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
|
|
1171
1171
|
} catch {
|
|
1172
1172
|
return false;
|
|
1173
1173
|
}
|
|
@@ -1178,6 +1178,14 @@ function isLoopbackBind(wsHost) {
|
|
|
1178
1178
|
function isWildcardBind(wsHost) {
|
|
1179
1179
|
return wsHost === "0.0.0.0" || wsHost === "::" || wsHost === "[::]";
|
|
1180
1180
|
}
|
|
1181
|
+
function normalizeHostname(hostname) {
|
|
1182
|
+
const h = hostname.trim().toLowerCase();
|
|
1183
|
+
return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
1184
|
+
}
|
|
1185
|
+
function allowedHostname(hostname, allowedHostnames) {
|
|
1186
|
+
const normalized = normalizeHostname(hostname);
|
|
1187
|
+
return (allowedHostnames ?? []).some((candidate) => normalizeHostname(candidate) === normalized);
|
|
1188
|
+
}
|
|
1181
1189
|
function tokenMatches(provided, expected) {
|
|
1182
1190
|
if (!provided) return false;
|
|
1183
1191
|
const a = Buffer.from(provided);
|
|
@@ -1216,28 +1224,37 @@ function hostHeaderOk(input) {
|
|
|
1216
1224
|
} catch {
|
|
1217
1225
|
return false;
|
|
1218
1226
|
}
|
|
1219
|
-
return isLoopbackHostname(hostname);
|
|
1227
|
+
return isLoopbackHostname(hostname) || allowedHostname(hostname, input.allowedHostnames);
|
|
1220
1228
|
}
|
|
1221
1229
|
function verifyClient(input) {
|
|
1222
|
-
const {
|
|
1230
|
+
const {
|
|
1231
|
+
origin,
|
|
1232
|
+
url,
|
|
1233
|
+
hostHeader,
|
|
1234
|
+
remoteAddress,
|
|
1235
|
+
cookieHeader,
|
|
1236
|
+
wsHost,
|
|
1237
|
+
expectedToken,
|
|
1238
|
+
requireToken,
|
|
1239
|
+
allowedHostnames,
|
|
1240
|
+
allowBrowserUrlToken
|
|
1241
|
+
} = input;
|
|
1223
1242
|
const urlTokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
1224
1243
|
const cookieTokenOk = tokenMatches(extractTokenFromCookie(cookieHeader), expectedToken);
|
|
1225
|
-
if (!hostHeaderOk({ hostHeader, wsHost })) return false;
|
|
1244
|
+
if (!hostHeaderOk({ hostHeader, wsHost, allowedHostnames })) return false;
|
|
1226
1245
|
if (!origin) {
|
|
1227
1246
|
const remoteIp = remoteAddress ?? "";
|
|
1228
1247
|
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
1229
1248
|
if (!isRemoteLoopback && isWildcardBind(wsHost)) return false;
|
|
1230
|
-
return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost);
|
|
1249
|
+
return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost) && !requireToken;
|
|
1231
1250
|
}
|
|
1232
1251
|
try {
|
|
1233
|
-
const { hostname } = new URL(origin);
|
|
1234
|
-
if (isLoopbackHostname(
|
|
1235
|
-
if (
|
|
1236
|
-
|
|
1237
|
-
}
|
|
1238
|
-
return true;
|
|
1252
|
+
const { hostname: originHostname } = new URL(origin);
|
|
1253
|
+
if (isLoopbackHostname(originHostname)) {
|
|
1254
|
+
if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
|
|
1255
|
+
return isTrustedLoopbackOrigin(origin);
|
|
1239
1256
|
}
|
|
1240
|
-
return cookieTokenOk;
|
|
1257
|
+
return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
|
|
1241
1258
|
} catch {
|
|
1242
1259
|
return false;
|
|
1243
1260
|
}
|
|
@@ -1263,8 +1280,69 @@ function injectWsPort(html, wsPort) {
|
|
|
1263
1280
|
return `${tag}
|
|
1264
1281
|
${html}`;
|
|
1265
1282
|
}
|
|
1266
|
-
function
|
|
1267
|
-
return
|
|
1283
|
+
function escapeHtmlAttr(value) {
|
|
1284
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1285
|
+
}
|
|
1286
|
+
function injectWsConfig(html, opts) {
|
|
1287
|
+
let out = injectWsPort(html, opts.wsPort);
|
|
1288
|
+
if (!opts.publicWsUrl || out.includes('name="wrongstack-ws-url"')) return out;
|
|
1289
|
+
const tag = `<meta name="wrongstack-ws-url" content="${escapeHtmlAttr(opts.publicWsUrl)}" />`;
|
|
1290
|
+
if (out.includes("</head>")) {
|
|
1291
|
+
return out.replace("</head>", ` ${tag}
|
|
1292
|
+
</head>`);
|
|
1293
|
+
}
|
|
1294
|
+
return `${tag}
|
|
1295
|
+
${out}`;
|
|
1296
|
+
}
|
|
1297
|
+
function firstHeader(value) {
|
|
1298
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1299
|
+
}
|
|
1300
|
+
function wsTokenCookie(token) {
|
|
1301
|
+
return `ws_token=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600`;
|
|
1302
|
+
}
|
|
1303
|
+
function requestToken(req, url) {
|
|
1304
|
+
return url.searchParams.get("token") ?? firstHeader(req.headers["x-ws-token"]) ?? extractTokenFromCookie(req.headers.cookie);
|
|
1305
|
+
}
|
|
1306
|
+
function requestHostForCsp(hostHeader) {
|
|
1307
|
+
const raw = firstHeader(hostHeader)?.trim();
|
|
1308
|
+
if (!raw) return void 0;
|
|
1309
|
+
try {
|
|
1310
|
+
return new URL(`http://${raw}`).hostname;
|
|
1311
|
+
} catch {
|
|
1312
|
+
return void 0;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
function formatCspHostname(hostname) {
|
|
1316
|
+
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
1317
|
+
}
|
|
1318
|
+
function cspSourceFromUrl(rawUrl) {
|
|
1319
|
+
try {
|
|
1320
|
+
const url = new URL(rawUrl);
|
|
1321
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") return void 0;
|
|
1322
|
+
return `${url.protocol}//${formatCspHostname(url.hostname)}${url.port ? `:${url.port}` : ""}`;
|
|
1323
|
+
} catch {
|
|
1324
|
+
return void 0;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
var ALLOWED_INLINE_SCRIPT_HASHES = [
|
|
1328
|
+
"'sha256-6PXDy0zrpXa6mvYOl11bZ8nubNUL7ushPUhGDZtaexg='",
|
|
1329
|
+
"'sha256-6sIdwbEBx7jj0drqSHHm7MqvmoYD3CQ4lp8Zp8blcb0='"
|
|
1330
|
+
];
|
|
1331
|
+
function buildCspHeader(wsPort, requestHost, publicWsUrl) {
|
|
1332
|
+
const connect = /* @__PURE__ */ new Set([
|
|
1333
|
+
"'self'",
|
|
1334
|
+
`ws://127.0.0.1:${wsPort}`,
|
|
1335
|
+
`wss://127.0.0.1:${wsPort}`
|
|
1336
|
+
]);
|
|
1337
|
+
if (requestHost && requestHost !== "127.0.0.1") {
|
|
1338
|
+
const host = formatCspHostname(requestHost);
|
|
1339
|
+
connect.add(`ws://${host}:${wsPort}`);
|
|
1340
|
+
connect.add(`wss://${host}:${wsPort}`);
|
|
1341
|
+
}
|
|
1342
|
+
const publicWsSource = publicWsUrl ? cspSourceFromUrl(publicWsUrl) : void 0;
|
|
1343
|
+
if (publicWsSource) connect.add(publicWsSource);
|
|
1344
|
+
const scriptSrc = ["'self'", ...ALLOWED_INLINE_SCRIPT_HASHES].join(" ");
|
|
1345
|
+
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'`;
|
|
1268
1346
|
}
|
|
1269
1347
|
function isInsideDist(candidate, distDir) {
|
|
1270
1348
|
const root = path.resolve(distDir);
|
|
@@ -1282,12 +1360,15 @@ function createHttpServer(opts) {
|
|
|
1282
1360
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
1283
1361
|
const distDir = path.resolve(opts.distDir);
|
|
1284
1362
|
const wsPort = opts.wsPort;
|
|
1285
|
-
const
|
|
1363
|
+
const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
|
|
1286
1364
|
return http.createServer(async (req, res) => {
|
|
1287
1365
|
try {
|
|
1288
1366
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
1367
|
+
const providedAccessToken = requestToken(req, url);
|
|
1368
|
+
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
1369
|
+
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
1289
1370
|
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
1290
|
-
const provided = url
|
|
1371
|
+
const provided = requestToken(req, url);
|
|
1291
1372
|
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
1292
1373
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
1293
1374
|
res.end("Unauthorized");
|
|
@@ -1295,7 +1376,7 @@ function createHttpServer(opts) {
|
|
|
1295
1376
|
}
|
|
1296
1377
|
res.writeHead(200, {
|
|
1297
1378
|
"Content-Type": "text/plain",
|
|
1298
|
-
"Set-Cookie":
|
|
1379
|
+
"Set-Cookie": wsTokenCookie(opts.apiToken),
|
|
1299
1380
|
// Belt-and-braces: tell any caches the cookie response itself
|
|
1300
1381
|
// is sensitive.
|
|
1301
1382
|
"Cache-Control": "no-store"
|
|
@@ -1303,10 +1384,20 @@ function createHttpServer(opts) {
|
|
|
1303
1384
|
res.end("ok");
|
|
1304
1385
|
return;
|
|
1305
1386
|
}
|
|
1387
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1388
|
+
res.writeHead(401, {
|
|
1389
|
+
"Content-Type": "text/plain",
|
|
1390
|
+
"Cache-Control": "no-store"
|
|
1391
|
+
});
|
|
1392
|
+
res.end("Unauthorized");
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
if (shouldSetAuthCookie && opts.apiToken) {
|
|
1396
|
+
res.setHeader("Set-Cookie", wsTokenCookie(opts.apiToken));
|
|
1397
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1398
|
+
}
|
|
1306
1399
|
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
1307
|
-
|
|
1308
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1309
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1400
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1310
1401
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1311
1402
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1312
1403
|
return;
|
|
@@ -1320,9 +1411,7 @@ function createHttpServer(opts) {
|
|
|
1320
1411
|
return;
|
|
1321
1412
|
}
|
|
1322
1413
|
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
1323
|
-
|
|
1324
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1325
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1414
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1326
1415
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1327
1416
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1328
1417
|
return;
|
|
@@ -1332,9 +1421,7 @@ function createHttpServer(opts) {
|
|
|
1332
1421
|
}
|
|
1333
1422
|
const agentsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/agents$/);
|
|
1334
1423
|
if (agentsMatch && req.method === "GET") {
|
|
1335
|
-
|
|
1336
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1337
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1424
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1338
1425
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1339
1426
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1340
1427
|
return;
|
|
@@ -1344,9 +1431,7 @@ function createHttpServer(opts) {
|
|
|
1344
1431
|
}
|
|
1345
1432
|
const eventsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
|
|
1346
1433
|
if (eventsMatch && req.method === "GET") {
|
|
1347
|
-
|
|
1348
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1349
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1434
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1350
1435
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1351
1436
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1352
1437
|
return;
|
|
@@ -1358,9 +1443,7 @@ function createHttpServer(opts) {
|
|
|
1358
1443
|
}
|
|
1359
1444
|
const msgMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/message$/);
|
|
1360
1445
|
if (msgMatch && req.method === "POST") {
|
|
1361
|
-
|
|
1362
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1363
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1446
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1364
1447
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1365
1448
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1366
1449
|
return;
|
|
@@ -1370,9 +1453,7 @@ function createHttpServer(opts) {
|
|
|
1370
1453
|
}
|
|
1371
1454
|
const mailboxMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/mailbox$/);
|
|
1372
1455
|
if (mailboxMatch && req.method === "GET") {
|
|
1373
|
-
|
|
1374
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1375
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1456
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1376
1457
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1377
1458
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1378
1459
|
return;
|
|
@@ -1382,9 +1463,7 @@ function createHttpServer(opts) {
|
|
|
1382
1463
|
}
|
|
1383
1464
|
const interruptMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/interrupt$/);
|
|
1384
1465
|
if (interruptMatch && req.method === "POST") {
|
|
1385
|
-
|
|
1386
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1387
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1466
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1388
1467
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1389
1468
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1390
1469
|
return;
|
|
@@ -1398,9 +1477,7 @@ function createHttpServer(opts) {
|
|
|
1398
1477
|
return;
|
|
1399
1478
|
}
|
|
1400
1479
|
if (url.pathname === "/api/fleet/broadcast" && req.method === "POST") {
|
|
1401
|
-
|
|
1402
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1403
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1480
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1404
1481
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1405
1482
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1406
1483
|
return;
|
|
@@ -1447,11 +1524,14 @@ function createHttpServer(opts) {
|
|
|
1447
1524
|
res.setHeader("X-Frame-Options", "DENY");
|
|
1448
1525
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1449
1526
|
if (ext === ".html") {
|
|
1450
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
1451
|
-
res.setHeader(
|
|
1527
|
+
if (!shouldSetAuthCookie) res.setHeader("Cache-Control", "no-cache");
|
|
1528
|
+
res.setHeader(
|
|
1529
|
+
"Content-Security-Policy",
|
|
1530
|
+
buildCspHeader(wsPort, requestHostForCsp(req.headers.host), opts.publicWsUrl)
|
|
1531
|
+
);
|
|
1452
1532
|
const html = await fs.readFile(resolvedPath, "utf8");
|
|
1453
1533
|
res.writeHead(200);
|
|
1454
|
-
res.end(
|
|
1534
|
+
res.end(injectWsConfig(html, { wsPort, publicWsUrl: opts.publicWsUrl }));
|
|
1455
1535
|
return;
|
|
1456
1536
|
}
|
|
1457
1537
|
const fileContent = await fs.readFile(resolvedPath);
|
|
@@ -1466,9 +1546,13 @@ function createHttpServer(opts) {
|
|
|
1466
1546
|
"X-Content-Type-Options": "nosniff",
|
|
1467
1547
|
"X-Frame-Options": "DENY",
|
|
1468
1548
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
1469
|
-
"Content-Security-Policy": buildCspHeader(
|
|
1549
|
+
"Content-Security-Policy": buildCspHeader(
|
|
1550
|
+
wsPort,
|
|
1551
|
+
requestHostForCsp(req.headers.host),
|
|
1552
|
+
opts.publicWsUrl
|
|
1553
|
+
)
|
|
1470
1554
|
});
|
|
1471
|
-
res.end(
|
|
1555
|
+
res.end(injectWsConfig(html, { wsPort, publicWsUrl: opts.publicWsUrl }));
|
|
1472
1556
|
} catch {
|
|
1473
1557
|
res.writeHead(404);
|
|
1474
1558
|
res.end("Not found");
|
|
@@ -1592,8 +1676,8 @@ function isInside(root, target) {
|
|
|
1592
1676
|
}
|
|
1593
1677
|
|
|
1594
1678
|
// src/server/file-handlers.ts
|
|
1595
|
-
import * as
|
|
1596
|
-
import * as
|
|
1679
|
+
import * as fs4 from "fs/promises";
|
|
1680
|
+
import * as path4 from "path";
|
|
1597
1681
|
import { atomicWrite } from "@wrongstack/core";
|
|
1598
1682
|
|
|
1599
1683
|
// src/server/file-picker.ts
|
|
@@ -1644,6 +1728,34 @@ function rankFiles(paths, query, limit) {
|
|
|
1644
1728
|
return scored.slice(0, limit).map((s) => s.path);
|
|
1645
1729
|
}
|
|
1646
1730
|
|
|
1731
|
+
// src/server/path-containment.ts
|
|
1732
|
+
import * as fs3 from "fs/promises";
|
|
1733
|
+
import * as path3 from "path";
|
|
1734
|
+
function isPathInside(root, target) {
|
|
1735
|
+
const relative3 = path3.relative(root, target);
|
|
1736
|
+
return relative3 === "" || !relative3.startsWith("..") && !path3.isAbsolute(relative3);
|
|
1737
|
+
}
|
|
1738
|
+
async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
|
|
1739
|
+
const resolved = path3.resolve(projectRoot, inputPath);
|
|
1740
|
+
let stat3;
|
|
1741
|
+
try {
|
|
1742
|
+
stat3 = await fs3.stat(resolved);
|
|
1743
|
+
} catch {
|
|
1744
|
+
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
1745
|
+
}
|
|
1746
|
+
if (!stat3.isDirectory()) {
|
|
1747
|
+
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
1748
|
+
}
|
|
1749
|
+
const [realProjectRoot, realResolved] = await Promise.all([
|
|
1750
|
+
fs3.realpath(projectRoot),
|
|
1751
|
+
fs3.realpath(resolved)
|
|
1752
|
+
]);
|
|
1753
|
+
if (!isPathInside(realProjectRoot, realResolved)) {
|
|
1754
|
+
throw new Error(`Path must stay inside the project root: ${projectRoot}`);
|
|
1755
|
+
}
|
|
1756
|
+
return resolved;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1647
1759
|
// src/server/ws-utils.ts
|
|
1648
1760
|
import { randomBytes } from "crypto";
|
|
1649
1761
|
import { WebSocket } from "ws";
|
|
@@ -1672,25 +1784,106 @@ function errMessage(err) {
|
|
|
1672
1784
|
function generateAuthToken() {
|
|
1673
1785
|
return randomBytes(16).toString("hex");
|
|
1674
1786
|
}
|
|
1787
|
+
function resolveAuthToken(explicit) {
|
|
1788
|
+
const configured = explicit?.trim() || process.env["WEBUI_TOKEN"]?.trim() || process.env["WEBUI_AUTH_TOKEN"]?.trim();
|
|
1789
|
+
return configured || generateAuthToken();
|
|
1790
|
+
}
|
|
1791
|
+
function hostForBrowserUrl(bindHost) {
|
|
1792
|
+
if (bindHost === "0.0.0.0") return "127.0.0.1";
|
|
1793
|
+
if (bindHost === "::" || bindHost === "[::]") return "[::1]";
|
|
1794
|
+
if (bindHost.includes(":") && !bindHost.startsWith("[")) return `[${bindHost}]`;
|
|
1795
|
+
return bindHost;
|
|
1796
|
+
}
|
|
1797
|
+
function buildWebUIAccessUrl(opts) {
|
|
1798
|
+
const protocol = opts.protocol ?? "http";
|
|
1799
|
+
const base = opts.publicUrl?.trim() || `${protocol}://${hostForBrowserUrl(opts.host)}:${opts.port}`;
|
|
1800
|
+
if (!opts.token) return base;
|
|
1801
|
+
try {
|
|
1802
|
+
const url = new URL(base);
|
|
1803
|
+
url.searchParams.set("token", opts.token);
|
|
1804
|
+
const rendered = url.toString();
|
|
1805
|
+
const afterOrigin = base.slice(url.origin.length);
|
|
1806
|
+
if (url.pathname === "/" && !afterOrigin.startsWith("/")) {
|
|
1807
|
+
return `${url.origin}${url.search}${url.hash}`;
|
|
1808
|
+
}
|
|
1809
|
+
return rendered;
|
|
1810
|
+
} catch {
|
|
1811
|
+
return `${base}${base.includes("?") ? "&" : "?"}token=${encodeURIComponent(opts.token)}`;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
function envFlag(name2) {
|
|
1815
|
+
const value = process.env[name2]?.trim().toLowerCase();
|
|
1816
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
1817
|
+
}
|
|
1675
1818
|
|
|
1676
1819
|
// src/server/file-handlers.ts
|
|
1820
|
+
async function resolveFileInsideProject(projectRoot, filePath) {
|
|
1821
|
+
const resolved = path4.resolve(projectRoot, filePath);
|
|
1822
|
+
if (!isPathInside(projectRoot, resolved)) {
|
|
1823
|
+
throw new Error("Path outside project root");
|
|
1824
|
+
}
|
|
1825
|
+
const { parent, base } = splitParentAndBase(resolved);
|
|
1826
|
+
const realProjectRoot = await fs4.realpath(projectRoot);
|
|
1827
|
+
const realParent = await realpathAllowMissing(parent);
|
|
1828
|
+
const realFull = path4.join(realParent, base);
|
|
1829
|
+
if (!isPathInside(realProjectRoot, realFull)) {
|
|
1830
|
+
throw new Error("Path outside project root");
|
|
1831
|
+
}
|
|
1832
|
+
return realFull;
|
|
1833
|
+
}
|
|
1834
|
+
function splitParentAndBase(p) {
|
|
1835
|
+
const base = path4.basename(p);
|
|
1836
|
+
const parent = path4.dirname(p);
|
|
1837
|
+
return { parent, base };
|
|
1838
|
+
}
|
|
1839
|
+
async function realpathAllowMissing(p) {
|
|
1840
|
+
try {
|
|
1841
|
+
return await fs4.realpath(p);
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
if (err.code !== "ENOENT") throw err;
|
|
1844
|
+
}
|
|
1845
|
+
const segments = [];
|
|
1846
|
+
let cursor = p;
|
|
1847
|
+
while (true) {
|
|
1848
|
+
const parent = path4.dirname(cursor);
|
|
1849
|
+
if (parent === cursor) {
|
|
1850
|
+
throw new Error("Path outside project root");
|
|
1851
|
+
}
|
|
1852
|
+
segments.unshift(path4.basename(cursor));
|
|
1853
|
+
try {
|
|
1854
|
+
const realParent = await fs4.realpath(parent);
|
|
1855
|
+
return path4.join(realParent, ...segments);
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
if (err.code !== "ENOENT") throw err;
|
|
1858
|
+
cursor = parent;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1677
1862
|
async function handleFilesTree(ws, msg, projectRoot) {
|
|
1678
1863
|
const payload = msg.payload;
|
|
1679
1864
|
const rawPath = payload?.path?.trim();
|
|
1680
|
-
|
|
1681
|
-
|
|
1865
|
+
let treeRoot;
|
|
1866
|
+
let realProjectRoot;
|
|
1867
|
+
try {
|
|
1868
|
+
if (rawPath && rawPath !== ".") {
|
|
1869
|
+
treeRoot = await resolveWorkingDirInsideProject(projectRoot, rawPath);
|
|
1870
|
+
} else {
|
|
1871
|
+
treeRoot = projectRoot;
|
|
1872
|
+
}
|
|
1873
|
+
realProjectRoot = await fs4.realpath(projectRoot);
|
|
1874
|
+
} catch {
|
|
1682
1875
|
send(ws, {
|
|
1683
1876
|
type: "files.tree",
|
|
1684
1877
|
payload: { root: projectRoot, tree: [], error: "Path outside project root" }
|
|
1685
1878
|
});
|
|
1686
1879
|
return;
|
|
1687
1880
|
}
|
|
1688
|
-
const pathPrefix = treeRoot === projectRoot ? "" : (
|
|
1881
|
+
const pathPrefix = treeRoot === projectRoot ? "" : (path4.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
|
|
1689
1882
|
async function buildTree(dir, rel, depth) {
|
|
1690
1883
|
if (depth > 10) return [];
|
|
1691
1884
|
let entries = [];
|
|
1692
1885
|
try {
|
|
1693
|
-
entries = await
|
|
1886
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1694
1887
|
} catch {
|
|
1695
1888
|
return [];
|
|
1696
1889
|
}
|
|
@@ -1702,11 +1895,20 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1702
1895
|
for (const e of entries) {
|
|
1703
1896
|
if (isHiddenEntry(e.name)) continue;
|
|
1704
1897
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
1705
|
-
const childAbs =
|
|
1898
|
+
const childAbs = path4.join(dir, e.name);
|
|
1706
1899
|
const childPath = pathPrefix + childRel;
|
|
1707
1900
|
if (e.isDirectory()) {
|
|
1708
1901
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
1709
|
-
|
|
1902
|
+
let realChild;
|
|
1903
|
+
try {
|
|
1904
|
+
realChild = await fs4.realpath(childAbs);
|
|
1905
|
+
} catch {
|
|
1906
|
+
continue;
|
|
1907
|
+
}
|
|
1908
|
+
if (!isPathInside(realProjectRoot, realChild)) {
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
const children = await buildTree(realChild, childRel, depth + 1);
|
|
1710
1912
|
nodes.push({ name: e.name, path: childPath, type: "directory", children });
|
|
1711
1913
|
} else if (e.isFile()) {
|
|
1712
1914
|
nodes.push({ name: e.name, path: childPath, type: "file" });
|
|
@@ -1716,10 +1918,10 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1716
1918
|
}
|
|
1717
1919
|
try {
|
|
1718
1920
|
const tree = await buildTree(treeRoot, "", 0);
|
|
1719
|
-
const rootLabel = treeRoot === projectRoot ? projectRoot :
|
|
1921
|
+
const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
|
|
1720
1922
|
send(ws, { type: "files.tree", payload: { root: rootLabel, tree } });
|
|
1721
1923
|
} catch (err) {
|
|
1722
|
-
const rootLabel = treeRoot === projectRoot ? projectRoot :
|
|
1924
|
+
const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
|
|
1723
1925
|
send(ws, {
|
|
1724
1926
|
type: "files.tree",
|
|
1725
1927
|
payload: { root: rootLabel, tree: [], error: errMessage(err) }
|
|
@@ -1728,13 +1930,15 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1728
1930
|
}
|
|
1729
1931
|
async function handleFilesRead(ws, msg, projectRoot) {
|
|
1730
1932
|
const { filePath } = msg.payload;
|
|
1731
|
-
|
|
1732
|
-
|
|
1933
|
+
let realResolved;
|
|
1934
|
+
try {
|
|
1935
|
+
realResolved = await resolveFileInsideProject(projectRoot, filePath);
|
|
1936
|
+
} catch {
|
|
1733
1937
|
send(ws, { type: "files.read", payload: { filePath, content: "", error: "Forbidden" } });
|
|
1734
1938
|
return;
|
|
1735
1939
|
}
|
|
1736
1940
|
try {
|
|
1737
|
-
const content = await
|
|
1941
|
+
const content = await fs4.readFile(realResolved, "utf8");
|
|
1738
1942
|
send(ws, { type: "files.read", payload: { filePath, content } });
|
|
1739
1943
|
} catch (err) {
|
|
1740
1944
|
send(ws, {
|
|
@@ -1745,16 +1949,18 @@ async function handleFilesRead(ws, msg, projectRoot) {
|
|
|
1745
1949
|
}
|
|
1746
1950
|
async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
|
|
1747
1951
|
const { filePath, content } = msg.payload;
|
|
1748
|
-
|
|
1749
|
-
|
|
1952
|
+
let realResolved;
|
|
1953
|
+
try {
|
|
1954
|
+
realResolved = await resolveFileInsideProject(projectRoot, filePath);
|
|
1955
|
+
} catch {
|
|
1750
1956
|
send(ws, { type: "files.written", payload: { filePath, success: false, error: "Forbidden" } });
|
|
1751
1957
|
return;
|
|
1752
1958
|
}
|
|
1753
1959
|
try {
|
|
1754
|
-
await atomicWrite(
|
|
1960
|
+
await atomicWrite(realResolved, content);
|
|
1755
1961
|
send(ws, { type: "files.written", payload: { filePath, success: true } });
|
|
1756
1962
|
if (opts.onWritten) {
|
|
1757
|
-
void Promise.resolve(opts.onWritten(
|
|
1963
|
+
void Promise.resolve(opts.onWritten(realResolved)).catch(() => void 0);
|
|
1758
1964
|
}
|
|
1759
1965
|
} catch (err) {
|
|
1760
1966
|
send(ws, {
|
|
@@ -1766,8 +1972,16 @@ async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
|
|
|
1766
1972
|
async function handleFilesList(ws, msg, projectRoot) {
|
|
1767
1973
|
const payload = msg.payload ?? {};
|
|
1768
1974
|
const limit = payload.limit ?? 50;
|
|
1769
|
-
|
|
1770
|
-
|
|
1975
|
+
let listRoot;
|
|
1976
|
+
let realProjectRoot;
|
|
1977
|
+
try {
|
|
1978
|
+
if (payload.path) {
|
|
1979
|
+
listRoot = await resolveWorkingDirInsideProject(projectRoot, payload.path);
|
|
1980
|
+
} else {
|
|
1981
|
+
listRoot = projectRoot;
|
|
1982
|
+
}
|
|
1983
|
+
realProjectRoot = await fs4.realpath(projectRoot);
|
|
1984
|
+
} catch {
|
|
1771
1985
|
send(ws, { type: "files.list", payload: { files: [] } });
|
|
1772
1986
|
return;
|
|
1773
1987
|
}
|
|
@@ -1776,7 +1990,7 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
1776
1990
|
if (depth > 8 || results.length >= 600) return;
|
|
1777
1991
|
let entries = [];
|
|
1778
1992
|
try {
|
|
1779
|
-
entries = await
|
|
1993
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1780
1994
|
} catch {
|
|
1781
1995
|
return;
|
|
1782
1996
|
}
|
|
@@ -1786,7 +2000,16 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
1786
2000
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
1787
2001
|
if (e.isDirectory()) {
|
|
1788
2002
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
1789
|
-
|
|
2003
|
+
let realChild;
|
|
2004
|
+
try {
|
|
2005
|
+
realChild = await fs4.realpath(path4.join(dir, e.name));
|
|
2006
|
+
} catch {
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
if (!isPathInside(realProjectRoot, realChild)) {
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
await walk(realChild, childRel, depth + 1);
|
|
1790
2013
|
} else if (e.isFile()) {
|
|
1791
2014
|
results.push(childRel);
|
|
1792
2015
|
}
|
|
@@ -1800,7 +2023,7 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
1800
2023
|
}
|
|
1801
2024
|
|
|
1802
2025
|
// src/server/completion-handlers.ts
|
|
1803
|
-
import * as
|
|
2026
|
+
import * as path5 from "path";
|
|
1804
2027
|
import { searchCodebaseIndex } from "@wrongstack/tools/codebase-index/index";
|
|
1805
2028
|
var MAX_PREFIX_CHARS = 12e3;
|
|
1806
2029
|
var MAX_SUFFIX_CHARS = 4e3;
|
|
@@ -1875,8 +2098,8 @@ async function handleCompletionRequest(ws, msg, opts) {
|
|
|
1875
2098
|
return;
|
|
1876
2099
|
}
|
|
1877
2100
|
const payload = parsed.payload;
|
|
1878
|
-
const projectRoot =
|
|
1879
|
-
const resolved =
|
|
2101
|
+
const projectRoot = path5.resolve(opts.projectRoot);
|
|
2102
|
+
const resolved = path5.resolve(projectRoot, payload.filePath);
|
|
1880
2103
|
if (!isInside2(projectRoot, resolved)) {
|
|
1881
2104
|
send(ws, {
|
|
1882
2105
|
type: "completion.result",
|
|
@@ -2275,7 +2498,7 @@ function buildSearchQuery(linePrefix, filePath) {
|
|
|
2275
2498
|
if (memberMatch?.[1]) return memberMatch[1];
|
|
2276
2499
|
const token = linePrefix.match(/([A-Za-z_$][\w$]*)$/)?.[1];
|
|
2277
2500
|
if (token && token.length >= 2) return token;
|
|
2278
|
-
return
|
|
2501
|
+
return path5.basename(filePath, path5.extname(filePath));
|
|
2279
2502
|
}
|
|
2280
2503
|
function currentLinePrefix(prefix) {
|
|
2281
2504
|
const idx = Math.max(prefix.lastIndexOf("\n"), prefix.lastIndexOf("\r"));
|
|
@@ -2305,7 +2528,7 @@ function head(value, max) {
|
|
|
2305
2528
|
return value.length <= max ? value : value.slice(0, max);
|
|
2306
2529
|
}
|
|
2307
2530
|
function isInside2(root, target) {
|
|
2308
|
-
return target === root || target.startsWith(root +
|
|
2531
|
+
return target === root || target.startsWith(root + path5.sep);
|
|
2309
2532
|
}
|
|
2310
2533
|
|
|
2311
2534
|
// src/server/memory-handlers.ts
|
|
@@ -2559,8 +2782,8 @@ async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
2559
2782
|
}
|
|
2560
2783
|
|
|
2561
2784
|
// src/server/skills-handlers.ts
|
|
2562
|
-
import { promises as
|
|
2563
|
-
import
|
|
2785
|
+
import { promises as fs5 } from "fs";
|
|
2786
|
+
import path6 from "path";
|
|
2564
2787
|
import JSZip from "jszip";
|
|
2565
2788
|
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
2566
2789
|
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
@@ -2631,19 +2854,19 @@ async function handleSkillsContent(ws, ctx, msg) {
|
|
|
2631
2854
|
send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
|
|
2632
2855
|
return;
|
|
2633
2856
|
}
|
|
2634
|
-
const body = await
|
|
2635
|
-
const skillDir =
|
|
2857
|
+
const body = await fs5.readFile(entry.path, "utf8");
|
|
2858
|
+
const skillDir = path6.dirname(entry.path);
|
|
2636
2859
|
let relatedFiles = [];
|
|
2637
2860
|
try {
|
|
2638
|
-
const files = await
|
|
2639
|
-
relatedFiles = files.filter((f) => f !==
|
|
2861
|
+
const files = await fs5.readdir(skillDir);
|
|
2862
|
+
relatedFiles = files.filter((f) => f !== path6.basename(entry.path)).map((f) => path6.join(skillDir, f));
|
|
2640
2863
|
} catch {
|
|
2641
2864
|
}
|
|
2642
2865
|
const nameLower = name2.toLowerCase();
|
|
2643
2866
|
const refResults = await Promise.all(
|
|
2644
2867
|
entries.filter((e) => e.name.toLowerCase() !== nameLower).map(async (e) => {
|
|
2645
2868
|
try {
|
|
2646
|
-
const content = await
|
|
2869
|
+
const content = await fs5.readFile(e.path, "utf8");
|
|
2647
2870
|
return [e.name, content.toLowerCase().includes(nameLower)];
|
|
2648
2871
|
} catch {
|
|
2649
2872
|
return [e.name, false];
|
|
@@ -2733,14 +2956,14 @@ async function handleSkillsCreate(ws, ctx, msg) {
|
|
|
2733
2956
|
}
|
|
2734
2957
|
const createPayload = parsed.value;
|
|
2735
2958
|
try {
|
|
2736
|
-
const targetDir = createPayload.scope === "global" ?
|
|
2959
|
+
const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
2737
2960
|
try {
|
|
2738
|
-
await
|
|
2961
|
+
await fs5.access(targetDir);
|
|
2739
2962
|
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
2740
2963
|
return;
|
|
2741
2964
|
} catch {
|
|
2742
2965
|
}
|
|
2743
|
-
await
|
|
2966
|
+
await fs5.mkdir(targetDir, { recursive: true });
|
|
2744
2967
|
const lines = createPayload.description.trim().split("\n");
|
|
2745
2968
|
const firstLine = lines[0].trim();
|
|
2746
2969
|
const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
|
|
@@ -2788,13 +3011,13 @@ ${trigger}
|
|
|
2788
3011
|
"- `bug-hunter` \u2014 for systematic bug detection patterns",
|
|
2789
3012
|
"- `output-standards` \u2014 for standardized `<next_steps>` formatting"
|
|
2790
3013
|
].join("\n");
|
|
2791
|
-
await atomicWrite2(
|
|
3014
|
+
await atomicWrite2(path6.join(targetDir, "SKILL.md"), skillContent);
|
|
2792
3015
|
send(ws, {
|
|
2793
3016
|
type: "skills.created",
|
|
2794
3017
|
payload: {
|
|
2795
3018
|
success: true,
|
|
2796
3019
|
error: null,
|
|
2797
|
-
skill: { name: createPayload.name.trim(), path:
|
|
3020
|
+
skill: { name: createPayload.name.trim(), path: path6.join(targetDir, "SKILL.md"), scope: createPayload.scope }
|
|
2798
3021
|
}
|
|
2799
3022
|
});
|
|
2800
3023
|
} catch (err) {
|
|
@@ -2858,23 +3081,23 @@ import {
|
|
|
2858
3081
|
Agent,
|
|
2859
3082
|
AutoCompactionMiddleware,
|
|
2860
3083
|
Context,
|
|
2861
|
-
DefaultMemoryStore
|
|
2862
|
-
DefaultModeStore
|
|
3084
|
+
DefaultMemoryStore,
|
|
3085
|
+
DefaultModeStore,
|
|
2863
3086
|
DefaultModelsRegistry,
|
|
2864
3087
|
DefaultSessionReader,
|
|
2865
|
-
DefaultSessionStore as
|
|
2866
|
-
DefaultSkillLoader
|
|
2867
|
-
DefaultSystemPromptBuilder as
|
|
2868
|
-
DefaultTokenCounter
|
|
3088
|
+
DefaultSessionStore as DefaultSessionStore2,
|
|
3089
|
+
DefaultSkillLoader,
|
|
3090
|
+
DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
|
|
3091
|
+
DefaultTokenCounter,
|
|
2869
3092
|
AnnotationsStore,
|
|
2870
3093
|
CollaborationBus,
|
|
2871
3094
|
collabPauseMiddleware,
|
|
2872
3095
|
collabInjectMiddleware,
|
|
2873
3096
|
estimateRequestTokensCalibrated,
|
|
2874
3097
|
EventBus,
|
|
2875
|
-
createStrategyCompactor
|
|
3098
|
+
createStrategyCompactor,
|
|
2876
3099
|
ProviderRegistry,
|
|
2877
|
-
TOKENS
|
|
3100
|
+
TOKENS,
|
|
2878
3101
|
ToolRegistry,
|
|
2879
3102
|
atomicWrite as atomicWrite6,
|
|
2880
3103
|
createDefaultPipelines,
|
|
@@ -2893,110 +3116,10 @@ import {
|
|
|
2893
3116
|
import { ToolExecutor } from "@wrongstack/core/execution";
|
|
2894
3117
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
|
|
2895
3118
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
|
|
2896
|
-
import { builtinToolsPack, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
|
|
3119
|
+
import { builtinToolsPack, configureExecPolicy, ensureSessionShell, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
|
|
2897
3120
|
import { MCPRegistry } from "@wrongstack/mcp";
|
|
2898
3121
|
import { WebSocket as WebSocket2, WebSocketServer } from "ws";
|
|
2899
|
-
|
|
2900
|
-
// ../runtime/src/container.ts
|
|
2901
|
-
import {
|
|
2902
|
-
Container,
|
|
2903
|
-
DefaultConfigStore,
|
|
2904
|
-
DefaultErrorHandler,
|
|
2905
|
-
DefaultMemoryStore,
|
|
2906
|
-
DefaultModeStore,
|
|
2907
|
-
DefaultPermissionPolicy,
|
|
2908
|
-
DefaultRetryPolicy,
|
|
2909
|
-
DefaultSecretScrubber,
|
|
2910
|
-
DefaultSessionStore,
|
|
2911
|
-
DefaultSkillLoader,
|
|
2912
|
-
DefaultSystemPromptBuilder,
|
|
2913
|
-
DefaultTokenCounter,
|
|
2914
|
-
createStrategyCompactor,
|
|
2915
|
-
buildRecoveryStrategies,
|
|
2916
|
-
TOKENS
|
|
2917
|
-
} from "@wrongstack/core";
|
|
2918
|
-
function createDefaultContainer(opts) {
|
|
2919
|
-
const { config, wpaths, logger, modelsRegistry } = opts;
|
|
2920
|
-
const container = new Container();
|
|
2921
|
-
const configStore = new DefaultConfigStore(config);
|
|
2922
|
-
container.bind(TOKENS.ConfigStore, () => configStore);
|
|
2923
|
-
container.bind(TOKENS.Logger, () => logger);
|
|
2924
|
-
container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
|
|
2925
|
-
container.bind(TOKENS.RetryPolicy, () => new DefaultRetryPolicy());
|
|
2926
|
-
container.bind(
|
|
2927
|
-
TOKENS.ErrorHandler,
|
|
2928
|
-
() => new DefaultErrorHandler(
|
|
2929
|
-
buildRecoveryStrategies({
|
|
2930
|
-
compactor: container.resolve(TOKENS.Compactor),
|
|
2931
|
-
modelsRegistry,
|
|
2932
|
-
getConfig: () => configStore.get()
|
|
2933
|
-
})
|
|
2934
|
-
)
|
|
2935
|
-
);
|
|
2936
|
-
container.bind(TOKENS.ModelsRegistry, () => modelsRegistry);
|
|
2937
|
-
container.bind(
|
|
2938
|
-
TOKENS.TokenCounter,
|
|
2939
|
-
() => new DefaultTokenCounter({ registry: modelsRegistry, providerId: config.provider })
|
|
2940
|
-
);
|
|
2941
|
-
const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
|
|
2942
|
-
container.bind(TOKENS.ModeStore, () => modeStore);
|
|
2943
|
-
container.bind(
|
|
2944
|
-
TOKENS.SessionStore,
|
|
2945
|
-
() => new DefaultSessionStore({
|
|
2946
|
-
dir: wpaths.projectSessions,
|
|
2947
|
-
// Scrub secrets out of persisted user/model turns (F-06). Tool output
|
|
2948
|
-
// is already scrubbed by the executor.
|
|
2949
|
-
secretScrubber: container.resolve(TOKENS.SecretScrubber)
|
|
2950
|
-
})
|
|
2951
|
-
);
|
|
2952
|
-
const memoryStore = new DefaultMemoryStore({ paths: wpaths, events: opts.events });
|
|
2953
|
-
container.bind(TOKENS.MemoryStore, () => memoryStore);
|
|
2954
|
-
const skillLoader = new DefaultSkillLoader({ paths: wpaths, bundledDir: opts.bundledSkillsDir });
|
|
2955
|
-
container.bind(TOKENS.SkillLoader, () => skillLoader);
|
|
2956
|
-
if (opts.systemPrompt) {
|
|
2957
|
-
container.bind(
|
|
2958
|
-
TOKENS.SystemPromptBuilder,
|
|
2959
|
-
() => new DefaultSystemPromptBuilder(opts.systemPrompt)
|
|
2960
|
-
);
|
|
2961
|
-
}
|
|
2962
|
-
container.bind(
|
|
2963
|
-
TOKENS.PermissionPolicy,
|
|
2964
|
-
() => {
|
|
2965
|
-
const policyOptions = {
|
|
2966
|
-
trustFile: wpaths.projectTrust,
|
|
2967
|
-
yolo: opts.permission?.yolo ?? false,
|
|
2968
|
-
yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
|
|
2969
|
-
confirmDestructive: opts.permission?.confirmDestructive ?? false
|
|
2970
|
-
};
|
|
2971
|
-
if (opts.permission?.promptDelegate !== void 0) {
|
|
2972
|
-
policyOptions.promptDelegate = opts.permission.promptDelegate;
|
|
2973
|
-
}
|
|
2974
|
-
return new DefaultPermissionPolicy(policyOptions);
|
|
2975
|
-
}
|
|
2976
|
-
);
|
|
2977
|
-
container.bind(
|
|
2978
|
-
TOKENS.Compactor,
|
|
2979
|
-
() => (
|
|
2980
|
-
// Strategy comes from config.context.strategy: 'hybrid' (default, lossless
|
|
2981
|
-
// rules, no LLM), 'intelligent' (LLM summarization), or 'selective'
|
|
2982
|
-
// (LLM-driven selection). The LLM strategies resolve their provider from
|
|
2983
|
-
// ctx at compact()-time, so binding here (before context.provider exists)
|
|
2984
|
-
// is safe. preserveK / eliseThreshold are class-level fallbacks; the active
|
|
2985
|
-
// ContextWindowPolicy in ctx.meta normally overrides both at runtime.
|
|
2986
|
-
// eliseThreshold is a TOKEN COUNT — a previous value of 0.7 elided
|
|
2987
|
-
// essentially every tool_result (anything > 1 token).
|
|
2988
|
-
createStrategyCompactor({
|
|
2989
|
-
strategy: config.context?.strategy,
|
|
2990
|
-
preserveK: opts.compactor?.preserveK ?? 10,
|
|
2991
|
-
eliseThreshold: opts.compactor?.eliseThreshold ?? 2e3,
|
|
2992
|
-
smart: true,
|
|
2993
|
-
summarizerModel: config.context?.summarizerModel,
|
|
2994
|
-
llmSelector: config.context?.llmSelector
|
|
2995
|
-
})
|
|
2996
|
-
)
|
|
2997
|
-
);
|
|
2998
|
-
return container;
|
|
2999
|
-
}
|
|
3122
|
+
import { createDefaultContainer, makeLightSubagentFactory } from "@wrongstack/runtime";
|
|
3000
3123
|
|
|
3001
3124
|
// src/server/boot.ts
|
|
3002
3125
|
import {
|
|
@@ -3023,6 +3146,13 @@ import {
|
|
|
3023
3146
|
PhaseStore,
|
|
3024
3147
|
WorktreeManager
|
|
3025
3148
|
} from "@wrongstack/core";
|
|
3149
|
+
function deriveTitle(goal) {
|
|
3150
|
+
const firstLine = goal.split("\n").map((l) => l.trim()).find(Boolean);
|
|
3151
|
+
if (!firstLine) return "AutoPhase";
|
|
3152
|
+
const sentence = firstLine.split(/(?<=[.!?])\s/)[0] ?? firstLine;
|
|
3153
|
+
const trimmed = sentence.length <= 64 ? sentence : `${sentence.slice(0, 63).trimEnd()}\u2026`;
|
|
3154
|
+
return trimmed || "AutoPhase";
|
|
3155
|
+
}
|
|
3026
3156
|
function isGitRepo(cwd) {
|
|
3027
3157
|
try {
|
|
3028
3158
|
const r = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, encoding: "utf8", windowsHide: true });
|
|
@@ -3031,6 +3161,19 @@ function isGitRepo(cwd) {
|
|
|
3031
3161
|
return false;
|
|
3032
3162
|
}
|
|
3033
3163
|
}
|
|
3164
|
+
function commitsSince(cwd, baseSha, branch) {
|
|
3165
|
+
try {
|
|
3166
|
+
const r = spawnSync("git", ["log", "--reverse", "--format=%H", `${baseSha}..${branch}`], {
|
|
3167
|
+
cwd,
|
|
3168
|
+
encoding: "utf8",
|
|
3169
|
+
windowsHide: true
|
|
3170
|
+
});
|
|
3171
|
+
if (r.status !== 0) return [];
|
|
3172
|
+
return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3173
|
+
} catch {
|
|
3174
|
+
return [];
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3034
3177
|
var AutoPhaseWebSocketHandler = class {
|
|
3035
3178
|
constructor(agent, context, logger, storeDir, events, projectRoot) {
|
|
3036
3179
|
this.agent = agent;
|
|
@@ -3050,10 +3193,17 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3050
3193
|
store;
|
|
3051
3194
|
clients = /* @__PURE__ */ new Set();
|
|
3052
3195
|
broadcastInterval = null;
|
|
3053
|
-
/** Aborts in-flight task agents when the run is stopped. */
|
|
3196
|
+
/** Aborts in-flight task agents AND the planning turn when the run is stopped. */
|
|
3054
3197
|
abort = null;
|
|
3198
|
+
/** Set the instant a stop/clear/revert is requested, so a planning turn that
|
|
3199
|
+
* resolves afterwards never launches the orchestrator (the abort alone can't
|
|
3200
|
+
* cover the window between the LLM call resolving and the orchestrator start). */
|
|
3201
|
+
stopping = false;
|
|
3055
3202
|
/** Optional per-phase git-worktree isolation (lazily created at start). */
|
|
3056
3203
|
worktrees = null;
|
|
3204
|
+
/** Base branch + tip SHA captured at run start so a revert can git-revert the
|
|
3205
|
+
* run's squash commits (history-preserving) instead of a destructive reset. */
|
|
3206
|
+
runBase = null;
|
|
3057
3207
|
/** Per-run worker identities so the board can show "who is on what". */
|
|
3058
3208
|
usedNicknames = /* @__PURE__ */ new Set();
|
|
3059
3209
|
addClient(ws) {
|
|
@@ -3077,11 +3227,13 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3077
3227
|
this.broadcast({ type: "autophase.resumed", payload: {} });
|
|
3078
3228
|
break;
|
|
3079
3229
|
case "autophase.stop":
|
|
3080
|
-
this.
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3230
|
+
await this.handleStop();
|
|
3231
|
+
break;
|
|
3232
|
+
case "autophase.clear":
|
|
3233
|
+
await this.handleClear();
|
|
3234
|
+
break;
|
|
3235
|
+
case "autophase.revert":
|
|
3236
|
+
await this.handleRevert();
|
|
3085
3237
|
break;
|
|
3086
3238
|
case "autophase.status":
|
|
3087
3239
|
this.broadcastState();
|
|
@@ -3158,17 +3310,27 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3158
3310
|
}
|
|
3159
3311
|
}
|
|
3160
3312
|
async handleStart(payload) {
|
|
3161
|
-
const
|
|
3313
|
+
const goal = payload?.goal || payload?.title || "Untitled Project";
|
|
3314
|
+
const title = deriveTitle(goal);
|
|
3162
3315
|
const autonomous = payload?.autonomous ?? true;
|
|
3163
|
-
|
|
3316
|
+
this.abort = new AbortController();
|
|
3317
|
+
this.stopping = false;
|
|
3318
|
+
const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, this.abort.signal);
|
|
3319
|
+
if (this.stopping || this.abort.signal.aborted) {
|
|
3320
|
+
this.broadcast({ type: "autophase.stopped", payload: { title } });
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3164
3323
|
this.logger.info(`[AutoPhase] Starting: ${title}`);
|
|
3165
|
-
const graph = await new PhaseGraphBuilder({ title, phases, autonomous }).build();
|
|
3324
|
+
const graph = await new PhaseGraphBuilder({ title, description: goal, phases, autonomous }).build();
|
|
3166
3325
|
this.graph = graph;
|
|
3167
|
-
this.abort = new AbortController();
|
|
3168
3326
|
await this.store.save(graph);
|
|
3169
|
-
|
|
3327
|
+
const useWorktrees = payload?.worktrees ?? process.env["WRONGSTACK_AUTOPHASE_WORKTREES"] !== "0";
|
|
3328
|
+
if (!this.worktrees && this.events && this.projectRoot && useWorktrees && isGitRepo(this.projectRoot)) {
|
|
3170
3329
|
this.worktrees = new WorktreeManager({ projectRoot: this.projectRoot, events: this.events });
|
|
3171
3330
|
}
|
|
3331
|
+
if (this.worktrees) {
|
|
3332
|
+
this.runBase = await this.worktrees.currentBase();
|
|
3333
|
+
}
|
|
3172
3334
|
this.orchestrator = new PhaseOrchestrator({
|
|
3173
3335
|
graph,
|
|
3174
3336
|
ctx: {
|
|
@@ -3215,6 +3377,62 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3215
3377
|
this.broadcast({ type: "autophase.failed", payload: { title, error: String(err) } });
|
|
3216
3378
|
});
|
|
3217
3379
|
}
|
|
3380
|
+
/**
|
|
3381
|
+
* Halt the run NOW — at any phase. Sets `stopping` (so a planning turn that
|
|
3382
|
+
* resolves afterwards bails), aborts in-flight agents, stops the orchestrator
|
|
3383
|
+
* tick, and ends the live broadcast. The board is kept for review; use
|
|
3384
|
+
* `autophase.clear` to reset or `autophase.revert` to undo the changes.
|
|
3385
|
+
*/
|
|
3386
|
+
async handleStop() {
|
|
3387
|
+
this.stopping = true;
|
|
3388
|
+
this.abort?.abort();
|
|
3389
|
+
this.orchestrator?.stop();
|
|
3390
|
+
this.stopBroadcast();
|
|
3391
|
+
if (this.graph) await this.store.save(this.graph).catch(() => void 0);
|
|
3392
|
+
this.broadcast({ type: "autophase.stopped", payload: { title: this.graph?.title } });
|
|
3393
|
+
}
|
|
3394
|
+
/**
|
|
3395
|
+
* Stop + wipe: tear down phase worktrees and reset to an empty board so the UI
|
|
3396
|
+
* returns to the start screen ("new one"). Does NOT touch already-merged commits
|
|
3397
|
+
* on the base branch — that is `autophase.revert`.
|
|
3398
|
+
*/
|
|
3399
|
+
async handleClear() {
|
|
3400
|
+
await this.handleStop();
|
|
3401
|
+
if (this.worktrees) await this.worktrees.cleanupAllManaged().catch(() => void 0);
|
|
3402
|
+
this.orchestrator = null;
|
|
3403
|
+
this.graph = null;
|
|
3404
|
+
this.runBase = null;
|
|
3405
|
+
this.usedNicknames.clear();
|
|
3406
|
+
this.broadcast({ type: "autophase.cleared", payload: {} });
|
|
3407
|
+
this.broadcast({ type: "autophase.state", payload: this.buildState() });
|
|
3408
|
+
}
|
|
3409
|
+
/**
|
|
3410
|
+
* Stop + undo: remove phase worktrees, then history-preservingly `git revert`
|
|
3411
|
+
* every commit this run landed on the base branch (captured `runBase`..HEAD),
|
|
3412
|
+
* then reset to an empty board. Refuses (reports a reason) on a dirty tree or a
|
|
3413
|
+
* conflicting revert rather than leaving the tree half-reverted.
|
|
3414
|
+
*/
|
|
3415
|
+
async handleRevert() {
|
|
3416
|
+
await this.handleStop();
|
|
3417
|
+
if (!this.worktrees || !this.runBase || !this.projectRoot) {
|
|
3418
|
+
this.broadcast({
|
|
3419
|
+
type: "autophase.reverted",
|
|
3420
|
+
payload: { ok: false, reverted: 0, reason: "no git baseline was captured for this run" }
|
|
3421
|
+
});
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
await this.worktrees.cleanupAllManaged().catch(() => void 0);
|
|
3425
|
+
const shas = commitsSince(this.projectRoot, this.runBase.sha, this.runBase.branch);
|
|
3426
|
+
const res = await this.worktrees.revertCommits(this.runBase.branch, shas);
|
|
3427
|
+
this.broadcast({ type: "autophase.reverted", payload: res });
|
|
3428
|
+
if (res.ok) {
|
|
3429
|
+
this.orchestrator = null;
|
|
3430
|
+
this.graph = null;
|
|
3431
|
+
this.runBase = null;
|
|
3432
|
+
this.broadcast({ type: "autophase.cleared", payload: {} });
|
|
3433
|
+
this.broadcast({ type: "autophase.state", payload: this.buildState() });
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3218
3436
|
/** Generic fallback phases when the LLM planner produces nothing usable. */
|
|
3219
3437
|
defaultPhases() {
|
|
3220
3438
|
return [
|
|
@@ -3225,13 +3443,18 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3225
3443
|
{ name: "Deployment", description: "Deploy to production", priority: "medium", estimateHours: 2, parallelizable: false }
|
|
3226
3444
|
];
|
|
3227
3445
|
}
|
|
3228
|
-
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
3229
|
-
|
|
3446
|
+
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
3447
|
+
* The caller passes the run's abort signal so a stop during planning cancels
|
|
3448
|
+
* the LLM turn (the previous fresh, never-aborted controller made planning
|
|
3449
|
+
* uninterruptible). */
|
|
3450
|
+
async planPhases(goal, signal) {
|
|
3230
3451
|
try {
|
|
3231
3452
|
const planner = new AutoPhasePlanner({
|
|
3232
3453
|
goal,
|
|
3233
3454
|
runOnce: async (prompt) => {
|
|
3234
|
-
const result = await this.agent.run(prompt, {
|
|
3455
|
+
const result = await this.agent.run(prompt, {
|
|
3456
|
+
signal: signal ?? new AbortController().signal
|
|
3457
|
+
});
|
|
3235
3458
|
return result.status === "done" ? result.finalText ?? "" : "";
|
|
3236
3459
|
}
|
|
3237
3460
|
});
|
|
@@ -3357,15 +3580,37 @@ Type: ${task.type}`;
|
|
|
3357
3580
|
});
|
|
3358
3581
|
const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map(mapTask) : [];
|
|
3359
3582
|
const completedPhases = phases.filter((p) => p.status === "completed").length;
|
|
3583
|
+
const failedPhases = phases.filter((p) => p.status === "failed").length;
|
|
3584
|
+
const failedTasks = phases.reduce(
|
|
3585
|
+
(sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "failed").length,
|
|
3586
|
+
0
|
|
3587
|
+
);
|
|
3588
|
+
const lastFailed = phases.filter((p) => p.status === "failed").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
|
|
3589
|
+
const lastError = lastFailed ? `${lastFailed.name}: ${lastFailed.metadata?.integrationError ?? "phase failed"}` : null;
|
|
3360
3590
|
return {
|
|
3361
3591
|
title: this.graph.title,
|
|
3592
|
+
// Full operator prompt, shown verbatim in a dedicated goal block (the
|
|
3593
|
+
// title is only a short derived heading). Fall back to the title for
|
|
3594
|
+
// legacy boards saved before the title/goal split.
|
|
3595
|
+
goal: this.graph.description || this.graph.title,
|
|
3362
3596
|
phases: phaseItems,
|
|
3363
3597
|
tasks: taskItems,
|
|
3364
3598
|
activePhaseId: currentActiveId,
|
|
3365
3599
|
overallPercent: phases.length > 0 ? Math.round(completedPhases / phases.length * 100) : 0,
|
|
3366
3600
|
autonomous: this.graph.autonomous,
|
|
3367
3601
|
totalTasks,
|
|
3368
|
-
completedTasks
|
|
3602
|
+
completedTasks,
|
|
3603
|
+
// Structured progress + lastError consumed by the autophase store (were
|
|
3604
|
+
// defined client-side but never sent, so they stayed null on the board).
|
|
3605
|
+
progress: {
|
|
3606
|
+
totalPhases: phases.length,
|
|
3607
|
+
completed: completedPhases,
|
|
3608
|
+
failed: failedPhases,
|
|
3609
|
+
totalTasks,
|
|
3610
|
+
completedTasks,
|
|
3611
|
+
failedTasks
|
|
3612
|
+
},
|
|
3613
|
+
lastError
|
|
3369
3614
|
};
|
|
3370
3615
|
}
|
|
3371
3616
|
sendState(client) {
|
|
@@ -3658,6 +3903,12 @@ var SddBoardWebSocketHandler = class {
|
|
|
3658
3903
|
};
|
|
3659
3904
|
|
|
3660
3905
|
// src/server/sdd-wizard-ws-handler.ts
|
|
3906
|
+
function deriveTitle2(goal) {
|
|
3907
|
+
const firstLine = goal.split("\n").map((l) => l.trim()).find(Boolean);
|
|
3908
|
+
if (!firstLine) return "New SDD Project";
|
|
3909
|
+
const sentence = firstLine.split(/(?<=[.!?])\s/)[0] ?? firstLine;
|
|
3910
|
+
return sentence.length <= 64 ? sentence : `${sentence.slice(0, 63).trimEnd()}\u2026`;
|
|
3911
|
+
}
|
|
3661
3912
|
var SddWizardWebSocketHandler = class {
|
|
3662
3913
|
constructor(deps2) {
|
|
3663
3914
|
this.deps = deps2;
|
|
@@ -3696,7 +3947,8 @@ var SddWizardWebSocketHandler = class {
|
|
|
3696
3947
|
parallelSlots: msg.payload?.parallelSlots,
|
|
3697
3948
|
defaultModel: msg.payload?.model,
|
|
3698
3949
|
defaultProvider: msg.payload?.provider,
|
|
3699
|
-
fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0
|
|
3950
|
+
fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0,
|
|
3951
|
+
worktrees: typeof msg.payload?.worktrees === "boolean" ? msg.payload.worktrees : void 0
|
|
3700
3952
|
});
|
|
3701
3953
|
break;
|
|
3702
3954
|
}
|
|
@@ -3716,7 +3968,7 @@ var SddWizardWebSocketHandler = class {
|
|
|
3716
3968
|
}
|
|
3717
3969
|
if (this.busy) return;
|
|
3718
3970
|
this.driver = this.deps.makeDriver();
|
|
3719
|
-
const prompt = this.driver.start(goal);
|
|
3971
|
+
const prompt = this.driver.start(deriveTitle2(goal), goal);
|
|
3720
3972
|
await this.runTurn(prompt);
|
|
3721
3973
|
}
|
|
3722
3974
|
async onMessage(text) {
|
|
@@ -3787,7 +4039,7 @@ var SddWizardWebSocketHandler = class {
|
|
|
3787
4039
|
};
|
|
3788
4040
|
|
|
3789
4041
|
// src/server/sdd-wizard-wiring.ts
|
|
3790
|
-
import * as
|
|
4042
|
+
import * as path7 from "path";
|
|
3791
4043
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3792
4044
|
import {
|
|
3793
4045
|
makeCommandVerifier,
|
|
@@ -3801,6 +4053,7 @@ import {
|
|
|
3801
4053
|
TaskGraphStore as TaskGraphStore2,
|
|
3802
4054
|
WorktreeManager as WorktreeManager2
|
|
3803
4055
|
} from "@wrongstack/core";
|
|
4056
|
+
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";
|
|
3804
4057
|
function buildSddWizardDeps(opts) {
|
|
3805
4058
|
const registry = new SddRunRegistry();
|
|
3806
4059
|
let isolatedSeq = 0;
|
|
@@ -3809,11 +4062,11 @@ function buildSddWizardDeps(opts) {
|
|
|
3809
4062
|
id: `sdd-${name2.toLowerCase().replace(/\s+/g, "-")}-${isolatedSeq++}`,
|
|
3810
4063
|
role: "executor",
|
|
3811
4064
|
name: name2,
|
|
3812
|
-
disabledTools: ["delegate"],
|
|
4065
|
+
disabledTools: ["delegate", "write", "edit", "patch", "bash", "exec"],
|
|
3813
4066
|
allowedCapabilities: ["fs.read", "net.outbound"]
|
|
3814
4067
|
});
|
|
3815
4068
|
try {
|
|
3816
|
-
const res = await result.agent.run([{ type: "text", text: prompt }]);
|
|
4069
|
+
const res = await result.agent.run([{ type: "text", text: PLANNING_ONLY_GUARD + prompt }]);
|
|
3817
4070
|
return res.finalText ?? "";
|
|
3818
4071
|
} finally {
|
|
3819
4072
|
await result.dispose?.();
|
|
@@ -3823,17 +4076,18 @@ function buildSddWizardDeps(opts) {
|
|
|
3823
4076
|
makeDriver: () => new SddInterviewDriver({
|
|
3824
4077
|
specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
|
|
3825
4078
|
graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
|
|
3826
|
-
sessionPath:
|
|
4079
|
+
sessionPath: path7.join(opts.paths.projectDir, "sdd-wizard-session.json")
|
|
3827
4080
|
}),
|
|
3828
4081
|
runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
|
|
3829
|
-
startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
|
|
4082
|
+
startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels, worktrees: useWorktrees }) => {
|
|
3830
4083
|
const graph = driver.getGraph();
|
|
3831
4084
|
const tracker = driver.getTracker();
|
|
3832
4085
|
if (!graph || !tracker) {
|
|
3833
4086
|
throw new Error("No task graph to run \u2014 finish the interview first.");
|
|
3834
4087
|
}
|
|
4088
|
+
const worktreesEnabled = useWorktrees ?? process.env["WRONGSTACK_SDD_WORKTREES"] !== "0";
|
|
3835
4089
|
let worktrees;
|
|
3836
|
-
if (
|
|
4090
|
+
if (worktreesEnabled) {
|
|
3837
4091
|
const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3838
4092
|
cwd: opts.projectRoot,
|
|
3839
4093
|
encoding: "utf8",
|
|
@@ -3892,9 +4146,6 @@ async function handleSddWizardRoute(_ws, msg, handlers) {
|
|
|
3892
4146
|
return true;
|
|
3893
4147
|
}
|
|
3894
4148
|
|
|
3895
|
-
// src/server/index.ts
|
|
3896
|
-
import { makeLightSubagentFactory } from "@wrongstack/runtime";
|
|
3897
|
-
|
|
3898
4149
|
// src/server/collaboration-ws-handler.ts
|
|
3899
4150
|
import { randomUUID } from "crypto";
|
|
3900
4151
|
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
@@ -4621,16 +4872,16 @@ var CollaborationWebSocketHandler = class {
|
|
|
4621
4872
|
};
|
|
4622
4873
|
|
|
4623
4874
|
// src/server/projects-manifest.ts
|
|
4624
|
-
import * as
|
|
4625
|
-
import * as
|
|
4875
|
+
import * as fs6 from "fs/promises";
|
|
4876
|
+
import * as path8 from "path";
|
|
4626
4877
|
import { projectSlug } from "@wrongstack/core";
|
|
4627
4878
|
function projectsJsonPath(globalConfigPath) {
|
|
4628
|
-
const base =
|
|
4629
|
-
return
|
|
4879
|
+
const base = path8.dirname(globalConfigPath);
|
|
4880
|
+
return path8.join(base, "projects.json");
|
|
4630
4881
|
}
|
|
4631
4882
|
async function loadManifest(globalConfigPath) {
|
|
4632
4883
|
try {
|
|
4633
|
-
const raw = await
|
|
4884
|
+
const raw = await fs6.readFile(projectsJsonPath(globalConfigPath), "utf8");
|
|
4634
4885
|
const parsed = JSON.parse(raw);
|
|
4635
4886
|
return { projects: parsed.projects ?? [] };
|
|
4636
4887
|
} catch {
|
|
@@ -4639,16 +4890,16 @@ async function loadManifest(globalConfigPath) {
|
|
|
4639
4890
|
}
|
|
4640
4891
|
async function saveManifest(manifest, globalConfigPath) {
|
|
4641
4892
|
const file = projectsJsonPath(globalConfigPath);
|
|
4642
|
-
await
|
|
4643
|
-
await
|
|
4893
|
+
await fs6.mkdir(path8.dirname(file), { recursive: true });
|
|
4894
|
+
await fs6.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
4644
4895
|
}
|
|
4645
4896
|
function generateProjectSlug(rootPath) {
|
|
4646
4897
|
return projectSlug(rootPath);
|
|
4647
4898
|
}
|
|
4648
4899
|
async function ensureProjectDataDir(slug, globalConfigPath) {
|
|
4649
|
-
const base =
|
|
4650
|
-
const dir =
|
|
4651
|
-
await
|
|
4900
|
+
const base = path8.dirname(globalConfigPath);
|
|
4901
|
+
const dir = path8.join(base, "projects", slug);
|
|
4902
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
4652
4903
|
return dir;
|
|
4653
4904
|
}
|
|
4654
4905
|
|
|
@@ -5074,14 +5325,14 @@ function registerShutdownHandlers(res) {
|
|
|
5074
5325
|
|
|
5075
5326
|
// src/server/instance-registry.ts
|
|
5076
5327
|
import * as os from "os";
|
|
5077
|
-
import * as
|
|
5078
|
-
import * as
|
|
5328
|
+
import * as path9 from "path";
|
|
5329
|
+
import * as fs7 from "fs/promises";
|
|
5079
5330
|
import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
|
|
5080
5331
|
function defaultBaseDir() {
|
|
5081
|
-
return
|
|
5332
|
+
return path9.join(os.homedir(), ".wrongstack");
|
|
5082
5333
|
}
|
|
5083
5334
|
function registryPath(baseDir = defaultBaseDir()) {
|
|
5084
|
-
return
|
|
5335
|
+
return path9.join(baseDir, "webui-instances.json");
|
|
5085
5336
|
}
|
|
5086
5337
|
function isPidAlive(pid) {
|
|
5087
5338
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -5094,7 +5345,7 @@ function isPidAlive(pid) {
|
|
|
5094
5345
|
}
|
|
5095
5346
|
async function load(file) {
|
|
5096
5347
|
try {
|
|
5097
|
-
const raw = await
|
|
5348
|
+
const raw = await fs7.readFile(file, "utf8");
|
|
5098
5349
|
const parsed = JSON.parse(raw);
|
|
5099
5350
|
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
5100
5351
|
return parsed;
|
|
@@ -5239,19 +5490,19 @@ function computeUsageCost(usage, rates) {
|
|
|
5239
5490
|
}
|
|
5240
5491
|
|
|
5241
5492
|
// src/server/provider-handlers.ts
|
|
5242
|
-
import { DefaultSecretScrubber
|
|
5493
|
+
import { DefaultSecretScrubber } from "@wrongstack/core";
|
|
5243
5494
|
import { probeLocalLlm } from "@wrongstack/runtime/probe";
|
|
5244
5495
|
|
|
5245
5496
|
// src/server/provider-config-io.ts
|
|
5246
|
-
import * as
|
|
5247
|
-
import * as
|
|
5497
|
+
import * as fs8 from "fs/promises";
|
|
5498
|
+
import * as path10 from "path";
|
|
5248
5499
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
5249
5500
|
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
5250
5501
|
import { DefaultSecretVault } from "@wrongstack/core";
|
|
5251
5502
|
async function loadSavedProviders(configPath, vault) {
|
|
5252
5503
|
let raw;
|
|
5253
5504
|
try {
|
|
5254
|
-
raw = await
|
|
5505
|
+
raw = await fs8.readFile(configPath, "utf8");
|
|
5255
5506
|
} catch {
|
|
5256
5507
|
return {};
|
|
5257
5508
|
}
|
|
@@ -5268,7 +5519,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
5268
5519
|
let raw;
|
|
5269
5520
|
let fileExists = true;
|
|
5270
5521
|
try {
|
|
5271
|
-
raw = await
|
|
5522
|
+
raw = await fs8.readFile(configPath, "utf8");
|
|
5272
5523
|
} catch (err) {
|
|
5273
5524
|
if (err.code !== "ENOENT") {
|
|
5274
5525
|
throw new Error(
|
|
@@ -5417,7 +5668,7 @@ function projectSavedProviders(providers) {
|
|
|
5417
5668
|
return view;
|
|
5418
5669
|
});
|
|
5419
5670
|
}
|
|
5420
|
-
var probeScrubber = new
|
|
5671
|
+
var probeScrubber = new DefaultSecretScrubber();
|
|
5421
5672
|
function createProviderHandlers(deps2) {
|
|
5422
5673
|
const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
|
|
5423
5674
|
let configWriteLock = deps2.getConfigWriteLock();
|
|
@@ -5606,7 +5857,7 @@ function createProviderHandlers(deps2) {
|
|
|
5606
5857
|
|
|
5607
5858
|
// src/server/mode-handlers.ts
|
|
5608
5859
|
import {
|
|
5609
|
-
DefaultSystemPromptBuilder
|
|
5860
|
+
DefaultSystemPromptBuilder
|
|
5610
5861
|
} from "@wrongstack/core";
|
|
5611
5862
|
function createModeHandlers(ctx) {
|
|
5612
5863
|
return {
|
|
@@ -5654,7 +5905,7 @@ function createModeHandlers(ctx) {
|
|
|
5654
5905
|
}
|
|
5655
5906
|
ctx.setModeId(id);
|
|
5656
5907
|
const modePrompt = id === "default" ? "" : (await ctx.modeStore.getMode(id))?.prompt ?? "";
|
|
5657
|
-
const freshBuilder = new
|
|
5908
|
+
const freshBuilder = new DefaultSystemPromptBuilder({
|
|
5658
5909
|
memoryStore: ctx.memoryStore,
|
|
5659
5910
|
skillLoader: ctx.skillLoader,
|
|
5660
5911
|
modeStore: ctx.modeStore,
|
|
@@ -5685,40 +5936,10 @@ function createModeHandlers(ctx) {
|
|
|
5685
5936
|
import * as fs9 from "fs/promises";
|
|
5686
5937
|
import * as path11 from "path";
|
|
5687
5938
|
import {
|
|
5688
|
-
DefaultSessionStore
|
|
5689
|
-
DefaultSystemPromptBuilder as
|
|
5939
|
+
DefaultSessionStore,
|
|
5940
|
+
DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2,
|
|
5690
5941
|
getSessionRegistry
|
|
5691
5942
|
} from "@wrongstack/core";
|
|
5692
|
-
|
|
5693
|
-
// src/server/path-containment.ts
|
|
5694
|
-
import * as fs8 from "fs/promises";
|
|
5695
|
-
import * as path10 from "path";
|
|
5696
|
-
function isPathInside(root, target) {
|
|
5697
|
-
const relative3 = path10.relative(root, target);
|
|
5698
|
-
return relative3 === "" || !relative3.startsWith("..") && !path10.isAbsolute(relative3);
|
|
5699
|
-
}
|
|
5700
|
-
async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
|
|
5701
|
-
const resolved = path10.resolve(projectRoot, inputPath);
|
|
5702
|
-
let stat3;
|
|
5703
|
-
try {
|
|
5704
|
-
stat3 = await fs8.stat(resolved);
|
|
5705
|
-
} catch {
|
|
5706
|
-
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
5707
|
-
}
|
|
5708
|
-
if (!stat3.isDirectory()) {
|
|
5709
|
-
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
5710
|
-
}
|
|
5711
|
-
const [realProjectRoot, realResolved] = await Promise.all([
|
|
5712
|
-
fs8.realpath(projectRoot),
|
|
5713
|
-
fs8.realpath(resolved)
|
|
5714
|
-
]);
|
|
5715
|
-
if (!isPathInside(realProjectRoot, realResolved)) {
|
|
5716
|
-
throw new Error(`Path must stay inside the project root: ${projectRoot}`);
|
|
5717
|
-
}
|
|
5718
|
-
return resolved;
|
|
5719
|
-
}
|
|
5720
|
-
|
|
5721
|
-
// src/server/project-handlers.ts
|
|
5722
5943
|
function createProjectHandlers(ctx) {
|
|
5723
5944
|
return {
|
|
5724
5945
|
listProjects: async (ws) => {
|
|
@@ -5830,7 +6051,7 @@ function createProjectHandlers(ctx) {
|
|
|
5830
6051
|
try {
|
|
5831
6052
|
const modeId = ctx.getModeId();
|
|
5832
6053
|
const switchMode = modeId === "default" ? void 0 : await ctx.modeStore.getMode(modeId);
|
|
5833
|
-
const switchBuilder = new
|
|
6054
|
+
const switchBuilder = new DefaultSystemPromptBuilder2({
|
|
5834
6055
|
memoryStore: ctx.memoryStore,
|
|
5835
6056
|
skillLoader: ctx.skillLoader,
|
|
5836
6057
|
modeStore: ctx.modeStore,
|
|
@@ -5854,7 +6075,7 @@ function createProjectHandlers(ctx) {
|
|
|
5854
6075
|
"sessions"
|
|
5855
6076
|
);
|
|
5856
6077
|
await fs9.mkdir(newSessionsDir, { recursive: true });
|
|
5857
|
-
const newSessionStore = new
|
|
6078
|
+
const newSessionStore = new DefaultSessionStore({ dir: newSessionsDir });
|
|
5858
6079
|
const oldSession = ctx.getSession();
|
|
5859
6080
|
const oldSessionId = oldSession.id;
|
|
5860
6081
|
try {
|
|
@@ -6551,6 +6772,22 @@ async function handleModeRoute(ws, msg, handlers) {
|
|
|
6551
6772
|
}
|
|
6552
6773
|
}
|
|
6553
6774
|
|
|
6775
|
+
// src/server/prefs-routes.ts
|
|
6776
|
+
async function handlePrefsRoute(ws, msg, handlers) {
|
|
6777
|
+
switch (msg.type) {
|
|
6778
|
+
case "prefs.get": {
|
|
6779
|
+
await handlers.getPrefs(ws);
|
|
6780
|
+
return true;
|
|
6781
|
+
}
|
|
6782
|
+
case "prefs.update": {
|
|
6783
|
+
await handlers.updatePrefs(ws, msg.payload ?? {});
|
|
6784
|
+
return true;
|
|
6785
|
+
}
|
|
6786
|
+
default:
|
|
6787
|
+
return false;
|
|
6788
|
+
}
|
|
6789
|
+
}
|
|
6790
|
+
|
|
6554
6791
|
// src/server/shell-git-routes.ts
|
|
6555
6792
|
async function handleShellGitRoute(ws, msg, handlers) {
|
|
6556
6793
|
switch (msg.type) {
|
|
@@ -6591,6 +6828,44 @@ async function handleMailboxRoute(ws, msg, handlers) {
|
|
|
6591
6828
|
}
|
|
6592
6829
|
}
|
|
6593
6830
|
|
|
6831
|
+
// src/server/mcp-routes.ts
|
|
6832
|
+
async function handleMcpRoute(ws, msg, handlers) {
|
|
6833
|
+
switch (msg.type) {
|
|
6834
|
+
case "mcp.list":
|
|
6835
|
+
await handlers.list(ws, msg);
|
|
6836
|
+
return true;
|
|
6837
|
+
case "mcp.add":
|
|
6838
|
+
await handlers.add(ws, msg);
|
|
6839
|
+
return true;
|
|
6840
|
+
case "mcp.update":
|
|
6841
|
+
await handlers.update(ws, msg);
|
|
6842
|
+
return true;
|
|
6843
|
+
case "mcp.remove":
|
|
6844
|
+
await handlers.remove(ws, msg);
|
|
6845
|
+
return true;
|
|
6846
|
+
case "mcp.enable":
|
|
6847
|
+
await handlers.enable(ws, msg);
|
|
6848
|
+
return true;
|
|
6849
|
+
case "mcp.disable":
|
|
6850
|
+
await handlers.disable(ws, msg);
|
|
6851
|
+
return true;
|
|
6852
|
+
case "mcp.sleep":
|
|
6853
|
+
await handlers.sleep(ws, msg);
|
|
6854
|
+
return true;
|
|
6855
|
+
case "mcp.wake":
|
|
6856
|
+
await handlers.wake(ws, msg);
|
|
6857
|
+
return true;
|
|
6858
|
+
case "mcp.restart":
|
|
6859
|
+
await handlers.restart(ws, msg);
|
|
6860
|
+
return true;
|
|
6861
|
+
case "mcp.discover":
|
|
6862
|
+
await handlers.discover(ws, msg);
|
|
6863
|
+
return true;
|
|
6864
|
+
default:
|
|
6865
|
+
return false;
|
|
6866
|
+
}
|
|
6867
|
+
}
|
|
6868
|
+
|
|
6594
6869
|
// src/server/brain-routes.ts
|
|
6595
6870
|
async function handleBrainRoute(ws, msg, handlers) {
|
|
6596
6871
|
switch (msg.type) {
|
|
@@ -7062,11 +7337,13 @@ function setupEvents(deps2) {
|
|
|
7062
7337
|
events.on("provider.response", (e) => {
|
|
7063
7338
|
if (e.usage?.input != null) {
|
|
7064
7339
|
const maxCtx = context.provider.capabilities.maxContext;
|
|
7065
|
-
const
|
|
7340
|
+
const rawLoad = maxCtx > 0 ? e.usage.input / maxCtx : 0;
|
|
7341
|
+
const load2 = Math.max(0, Math.min(1, rawLoad));
|
|
7066
7342
|
const costUsd = context.tokenCounter.estimateCost().total;
|
|
7067
7343
|
forwardSubagent("ctx_pct", {
|
|
7068
7344
|
subagentId: "leader",
|
|
7069
|
-
load:
|
|
7345
|
+
load: load2,
|
|
7346
|
+
rawLoad,
|
|
7070
7347
|
tokens: e.usage.input,
|
|
7071
7348
|
maxContext: maxCtx,
|
|
7072
7349
|
costUsd
|
|
@@ -7717,9 +7994,13 @@ async function handleGoalGet(projectRoot, broadcast2) {
|
|
|
7717
7994
|
|
|
7718
7995
|
// src/server/index.ts
|
|
7719
7996
|
async function startWebUI(opts = {}) {
|
|
7997
|
+
ensureSessionShell();
|
|
7720
7998
|
const requestedWsPort = opts.wsPort ?? 3457;
|
|
7721
|
-
const wsHost = opts.wsHost ?? "127.0.0.1";
|
|
7722
|
-
const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
7999
|
+
const wsHost = opts.wsHost ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
|
|
8000
|
+
const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? "3456", 10);
|
|
8001
|
+
const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
|
|
8002
|
+
const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
|
|
8003
|
+
const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
|
|
7723
8004
|
const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
|
|
7724
8005
|
let wsPort = requestedWsPort;
|
|
7725
8006
|
let httpPort = requestedHttpPort;
|
|
@@ -7798,7 +8079,7 @@ async function startWebUI(opts = {}) {
|
|
|
7798
8079
|
ttlSeconds: 24 * 3600
|
|
7799
8080
|
});
|
|
7800
8081
|
const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry });
|
|
7801
|
-
const configStore = opts.services?.configStore ?? container.resolve(
|
|
8082
|
+
const configStore = opts.services?.configStore ?? container.resolve(TOKENS.ConfigStore);
|
|
7802
8083
|
const providerRegistry = new ProviderRegistry();
|
|
7803
8084
|
try {
|
|
7804
8085
|
const factories = await buildProviderFactoriesFromRegistry({
|
|
@@ -7820,7 +8101,7 @@ async function startWebUI(opts = {}) {
|
|
|
7820
8101
|
r.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
|
|
7821
8102
|
return r;
|
|
7822
8103
|
})();
|
|
7823
|
-
const memoryStore = new
|
|
8104
|
+
const memoryStore = new DefaultMemoryStore({ paths: wpaths });
|
|
7824
8105
|
if (config.features.memory) {
|
|
7825
8106
|
toolRegistry.register(rememberTool(memoryStore));
|
|
7826
8107
|
toolRegistry.register(forgetTool(memoryStore));
|
|
@@ -7833,6 +8114,7 @@ async function startWebUI(opts = {}) {
|
|
|
7833
8114
|
toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
|
|
7834
8115
|
toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
|
|
7835
8116
|
applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
|
|
8117
|
+
configureExecPolicy(config.tools?.exec ?? {});
|
|
7836
8118
|
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
7837
8119
|
const mcpRegistry = new MCPRegistry({
|
|
7838
8120
|
toolRegistry,
|
|
@@ -7849,7 +8131,7 @@ async function startWebUI(opts = {}) {
|
|
|
7849
8131
|
});
|
|
7850
8132
|
}
|
|
7851
8133
|
}
|
|
7852
|
-
let sessionStore = opts.services?.session ?? new
|
|
8134
|
+
let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
|
|
7853
8135
|
if (!opts.services?.session) {
|
|
7854
8136
|
sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
|
|
7855
8137
|
if (count > 0) logger.info(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
|
|
@@ -7937,11 +8219,11 @@ async function startWebUI(opts = {}) {
|
|
|
7937
8219
|
});
|
|
7938
8220
|
} catch {
|
|
7939
8221
|
}
|
|
7940
|
-
const tokenCounter = new
|
|
8222
|
+
const tokenCounter = new DefaultTokenCounter({
|
|
7941
8223
|
registry: modelsRegistry,
|
|
7942
8224
|
providerId: config.provider
|
|
7943
8225
|
});
|
|
7944
|
-
const modeStore = new
|
|
8226
|
+
const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
|
|
7945
8227
|
const activeMode = await modeStore.getActiveMode();
|
|
7946
8228
|
let modeId = activeMode?.id ?? "default";
|
|
7947
8229
|
const modePrompt = activeMode?.prompt ?? "";
|
|
@@ -7962,7 +8244,7 @@ async function startWebUI(opts = {}) {
|
|
|
7962
8244
|
const modelCapabilitiesRef = {
|
|
7963
8245
|
current: modelCapabilities
|
|
7964
8246
|
};
|
|
7965
|
-
const skillLoader = config.features.skills ? new
|
|
8247
|
+
const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
|
|
7966
8248
|
const skillInstaller = config.features.skills ? new SkillInstaller({
|
|
7967
8249
|
manifestPath: path16.join(wstackGlobalRoot2(), "installed-skills.json"),
|
|
7968
8250
|
projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
|
|
@@ -7970,7 +8252,7 @@ async function startWebUI(opts = {}) {
|
|
|
7970
8252
|
projectHash: projectHash(projectRoot),
|
|
7971
8253
|
skillLoader
|
|
7972
8254
|
}) : void 0;
|
|
7973
|
-
const systemPromptBuilder = new
|
|
8255
|
+
const systemPromptBuilder = new DefaultSystemPromptBuilder3({
|
|
7974
8256
|
memoryStore,
|
|
7975
8257
|
skillLoader,
|
|
7976
8258
|
modeStore,
|
|
@@ -8260,7 +8542,7 @@ async function startWebUI(opts = {}) {
|
|
|
8260
8542
|
projectRoot,
|
|
8261
8543
|
logger
|
|
8262
8544
|
});
|
|
8263
|
-
const compactor =
|
|
8545
|
+
const compactor = createStrategyCompactor({
|
|
8264
8546
|
strategy: config.context?.strategy,
|
|
8265
8547
|
preserveK: config.context?.preserveK ?? 10,
|
|
8266
8548
|
eliseThreshold: config.context?.eliseThreshold ?? 2e3,
|
|
@@ -8336,9 +8618,9 @@ async function startWebUI(opts = {}) {
|
|
|
8336
8618
|
maxContext: newMaxContext
|
|
8337
8619
|
});
|
|
8338
8620
|
}
|
|
8339
|
-
const secretScrubber = container.resolve(
|
|
8340
|
-
const renderer = container.has(
|
|
8341
|
-
const permissionPolicy = container.resolve(
|
|
8621
|
+
const secretScrubber = container.resolve(TOKENS.SecretScrubber);
|
|
8622
|
+
const renderer = container.has(TOKENS.Renderer) ? container.resolve(TOKENS.Renderer) : void 0;
|
|
8623
|
+
const permissionPolicy = container.resolve(TOKENS.PermissionPolicy);
|
|
8342
8624
|
const toolExecutor = new ToolExecutor(toolRegistry, {
|
|
8343
8625
|
permissionPolicy,
|
|
8344
8626
|
secretScrubber,
|
|
@@ -8381,7 +8663,7 @@ async function startWebUI(opts = {}) {
|
|
|
8381
8663
|
}),
|
|
8382
8664
|
events
|
|
8383
8665
|
);
|
|
8384
|
-
container.bind(
|
|
8666
|
+
container.bind(TOKENS.BrainArbiter, () => brain);
|
|
8385
8667
|
const brainMailbox = new GlobalMailbox2(wpaths.projectDir, events);
|
|
8386
8668
|
const brainMonitor = new BrainMonitor({
|
|
8387
8669
|
events,
|
|
@@ -8513,8 +8795,16 @@ async function startWebUI(opts = {}) {
|
|
|
8513
8795
|
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID2)
|
|
8514
8796
|
};
|
|
8515
8797
|
}
|
|
8516
|
-
const wsToken =
|
|
8517
|
-
console.log("[WebUI] WS auth token
|
|
8798
|
+
const wsToken = resolveAuthToken(opts.accessToken);
|
|
8799
|
+
console.log("[WebUI] WS auth token ready");
|
|
8800
|
+
const publicHostnames = [publicUrl, publicWsUrl].map((value) => {
|
|
8801
|
+
if (!value) return void 0;
|
|
8802
|
+
try {
|
|
8803
|
+
return new URL(value).hostname;
|
|
8804
|
+
} catch {
|
|
8805
|
+
return void 0;
|
|
8806
|
+
}
|
|
8807
|
+
}).filter((value) => Boolean(value));
|
|
8518
8808
|
const verifyClient2 = (info) => verifyClient({
|
|
8519
8809
|
origin: info.origin,
|
|
8520
8810
|
url: info.req.url ?? "",
|
|
@@ -8526,7 +8816,10 @@ async function startWebUI(opts = {}) {
|
|
|
8526
8816
|
// exposure class.
|
|
8527
8817
|
cookieHeader: info.req.headers.cookie,
|
|
8528
8818
|
wsHost,
|
|
8529
|
-
expectedToken: wsToken
|
|
8819
|
+
expectedToken: wsToken,
|
|
8820
|
+
requireToken,
|
|
8821
|
+
allowedHostnames: publicHostnames,
|
|
8822
|
+
allowBrowserUrlToken: Boolean(publicWsUrl)
|
|
8530
8823
|
});
|
|
8531
8824
|
const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
|
|
8532
8825
|
const wssPrimary = new WebSocketServer({
|
|
@@ -8761,8 +9054,10 @@ async function startWebUI(opts = {}) {
|
|
|
8761
9054
|
let sessionRoutes;
|
|
8762
9055
|
let projectRoutes;
|
|
8763
9056
|
let modeRoutes;
|
|
9057
|
+
let prefsRoutes;
|
|
8764
9058
|
let shellGitRoutes;
|
|
8765
9059
|
let mailboxRoutes;
|
|
9060
|
+
let mcpRoutes;
|
|
8766
9061
|
let brainRoutes;
|
|
8767
9062
|
let autoPhaseRoutes;
|
|
8768
9063
|
let specsRoutes;
|
|
@@ -8773,8 +9068,10 @@ async function startWebUI(opts = {}) {
|
|
|
8773
9068
|
if (await handleSessionRoute(ws, msg, sessionRoutes)) return;
|
|
8774
9069
|
if (await handleProjectRoute(ws, msg, projectRoutes)) return;
|
|
8775
9070
|
if (await handleModeRoute(ws, msg, modeRoutes)) return;
|
|
9071
|
+
if (await handlePrefsRoute(ws, msg, prefsRoutes)) return;
|
|
8776
9072
|
if (await handleShellGitRoute(ws, msg, shellGitRoutes)) return;
|
|
8777
9073
|
if (await handleMailboxRoute(ws, msg, mailboxRoutes)) return;
|
|
9074
|
+
if (await handleMcpRoute(ws, msg, mcpRoutes)) return;
|
|
8778
9075
|
if (await handleBrainRoute(ws, msg, brainRoutes)) return;
|
|
8779
9076
|
if (await handleAutoPhaseRoute(ws, msg, autoPhaseRoutes)) return;
|
|
8780
9077
|
if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
|
|
@@ -8885,27 +9182,31 @@ async function startWebUI(opts = {}) {
|
|
|
8885
9182
|
case "memory.forget":
|
|
8886
9183
|
return handleMemoryForget(ws, msg, memoryStore);
|
|
8887
9184
|
// ── MCP operations — delegated to shared handlers (mcp-handlers.ts),
|
|
8888
|
-
// backed by the live MCPRegistry constructed above.
|
|
9185
|
+
// backed by the live MCPRegistry constructed above. Routed via
|
|
9186
|
+
// handleMcpRoute (see mcpRoutes = { ... } below). These case arms
|
|
9187
|
+
// are unreachable but left as tripwires for any future regression
|
|
9188
|
+
// where the route chain stops claiming 'mcp.*'. If you see one
|
|
9189
|
+
// fire, fix the dispatch order in the handleMessage chain above.
|
|
8889
9190
|
case "mcp.list":
|
|
8890
|
-
|
|
9191
|
+
throw new Error("handleMcpRoute did not claim mcp.list \u2014 check chain order");
|
|
8891
9192
|
case "mcp.add":
|
|
8892
|
-
|
|
8893
|
-
case "mcp.remove":
|
|
8894
|
-
return handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry);
|
|
9193
|
+
throw new Error("handleMcpRoute did not claim mcp.add \u2014 check chain order");
|
|
8895
9194
|
case "mcp.update":
|
|
8896
|
-
|
|
8897
|
-
case "mcp.
|
|
8898
|
-
|
|
8899
|
-
case "mcp.sleep":
|
|
8900
|
-
return handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry);
|
|
8901
|
-
case "mcp.discover":
|
|
8902
|
-
return handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry);
|
|
9195
|
+
throw new Error("handleMcpRoute did not claim mcp.update \u2014 check chain order");
|
|
9196
|
+
case "mcp.remove":
|
|
9197
|
+
throw new Error("handleMcpRoute did not claim mcp.remove \u2014 check chain order");
|
|
8903
9198
|
case "mcp.enable":
|
|
8904
|
-
|
|
9199
|
+
throw new Error("handleMcpRoute did not claim mcp.enable \u2014 check chain order");
|
|
8905
9200
|
case "mcp.disable":
|
|
8906
|
-
|
|
9201
|
+
throw new Error("handleMcpRoute did not claim mcp.disable \u2014 check chain order");
|
|
9202
|
+
case "mcp.sleep":
|
|
9203
|
+
throw new Error("handleMcpRoute did not claim mcp.sleep \u2014 check chain order");
|
|
9204
|
+
case "mcp.wake":
|
|
9205
|
+
throw new Error("handleMcpRoute did not claim mcp.wake \u2014 check chain order");
|
|
8907
9206
|
case "mcp.restart":
|
|
8908
|
-
|
|
9207
|
+
throw new Error("handleMcpRoute did not claim mcp.restart \u2014 check chain order");
|
|
9208
|
+
case "mcp.discover":
|
|
9209
|
+
throw new Error("handleMcpRoute did not claim mcp.discover \u2014 check chain order");
|
|
8909
9210
|
// Skills — full request→response cycle lives in skills-handlers.ts
|
|
8910
9211
|
// (shared with the CLI's embedded server). skillsCtx is the closed-over
|
|
8911
9212
|
// loader/installer/projectRoot the handlers need.
|
|
@@ -9053,53 +9354,11 @@ async function startWebUI(opts = {}) {
|
|
|
9053
9354
|
break;
|
|
9054
9355
|
}
|
|
9055
9356
|
case "prefs.update": {
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
sendResult2(ws, false, parsed.message);
|
|
9059
|
-
break;
|
|
9060
|
-
}
|
|
9061
|
-
const payload = parsed.value.prefs;
|
|
9062
|
-
for (const [key, val] of Object.entries(payload)) {
|
|
9063
|
-
context.meta[key] = val;
|
|
9064
|
-
}
|
|
9065
|
-
void persistPrefsToConfig(payload);
|
|
9066
|
-
if (typeof payload["yolo"] === "boolean") {
|
|
9067
|
-
permissionPolicy.setYolo?.(payload["yolo"]);
|
|
9068
|
-
}
|
|
9069
|
-
if (typeof payload["featureMcp"] === "boolean")
|
|
9070
|
-
config.features.mcp = payload["featureMcp"];
|
|
9071
|
-
if (typeof payload["featurePlugins"] === "boolean")
|
|
9072
|
-
config.features.plugins = payload["featurePlugins"];
|
|
9073
|
-
if (typeof payload["featureMemory"] === "boolean")
|
|
9074
|
-
config.features.memory = payload["featureMemory"];
|
|
9075
|
-
if (typeof payload["featureSkills"] === "boolean")
|
|
9076
|
-
config.features.skills = payload["featureSkills"];
|
|
9077
|
-
if (typeof payload["featureModelsRegistry"] === "boolean")
|
|
9078
|
-
config.features.modelsRegistry = payload["featureModelsRegistry"];
|
|
9079
|
-
if (Array.isArray(payload["fallbackModels"]))
|
|
9080
|
-
config.fallbackModels = payload["fallbackModels"];
|
|
9081
|
-
if (typeof payload["fallbackAuto"] === "boolean")
|
|
9082
|
-
config.fallbackAuto = payload["fallbackAuto"];
|
|
9083
|
-
if (typeof payload["contextAutoCompact"] === "boolean") {
|
|
9084
|
-
if (payload["contextAutoCompact"] && autoCompactor) {
|
|
9085
|
-
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
9086
|
-
pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
|
|
9087
|
-
} else {
|
|
9088
|
-
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
9089
|
-
}
|
|
9090
|
-
}
|
|
9091
|
-
if (typeof payload["logLevel"] === "string") {
|
|
9092
|
-
const valid = ["debug", "info", "warn", "error"];
|
|
9093
|
-
if (valid.includes(payload["logLevel"])) {
|
|
9094
|
-
logger.level = payload["logLevel"];
|
|
9095
|
-
}
|
|
9096
|
-
}
|
|
9097
|
-
broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
|
|
9098
|
-
break;
|
|
9357
|
+
void ws;
|
|
9358
|
+
throw new Error("handlePrefsRoute did not claim prefs.update \u2014 check chain order");
|
|
9099
9359
|
}
|
|
9100
9360
|
case "prefs.get": {
|
|
9101
|
-
|
|
9102
|
-
break;
|
|
9361
|
+
throw new Error("handlePrefsRoute did not claim prefs.get \u2014 check chain order");
|
|
9103
9362
|
}
|
|
9104
9363
|
default:
|
|
9105
9364
|
send(ws, {
|
|
@@ -9320,6 +9579,55 @@ async function startWebUI(opts = {}) {
|
|
|
9320
9579
|
},
|
|
9321
9580
|
sessionStartPayload
|
|
9322
9581
|
});
|
|
9582
|
+
prefsRoutes = {
|
|
9583
|
+
getPrefs: async (ws) => {
|
|
9584
|
+
send(ws, { type: "prefs.updated", payload: prefSnapshot() });
|
|
9585
|
+
},
|
|
9586
|
+
updatePrefs: async (ws, msgPayload) => {
|
|
9587
|
+
const parsed = validatePrefsUpdatePayload(msgPayload);
|
|
9588
|
+
if (!parsed.ok) {
|
|
9589
|
+
sendResult2(ws, false, parsed.message);
|
|
9590
|
+
return;
|
|
9591
|
+
}
|
|
9592
|
+
const payload = parsed.value.prefs;
|
|
9593
|
+
for (const [key, val] of Object.entries(payload)) {
|
|
9594
|
+
context.meta[key] = val;
|
|
9595
|
+
}
|
|
9596
|
+
void persistPrefsToConfig(payload);
|
|
9597
|
+
if (typeof payload["yolo"] === "boolean") {
|
|
9598
|
+
permissionPolicy.setYolo?.(payload["yolo"]);
|
|
9599
|
+
}
|
|
9600
|
+
if (typeof payload["featureMcp"] === "boolean")
|
|
9601
|
+
config.features.mcp = payload["featureMcp"];
|
|
9602
|
+
if (typeof payload["featurePlugins"] === "boolean")
|
|
9603
|
+
config.features.plugins = payload["featurePlugins"];
|
|
9604
|
+
if (typeof payload["featureMemory"] === "boolean")
|
|
9605
|
+
config.features.memory = payload["featureMemory"];
|
|
9606
|
+
if (typeof payload["featureSkills"] === "boolean")
|
|
9607
|
+
config.features.skills = payload["featureSkills"];
|
|
9608
|
+
if (typeof payload["featureModelsRegistry"] === "boolean")
|
|
9609
|
+
config.features.modelsRegistry = payload["featureModelsRegistry"];
|
|
9610
|
+
if (Array.isArray(payload["fallbackModels"]))
|
|
9611
|
+
config.fallbackModels = payload["fallbackModels"];
|
|
9612
|
+
if (typeof payload["fallbackAuto"] === "boolean")
|
|
9613
|
+
config.fallbackAuto = payload["fallbackAuto"];
|
|
9614
|
+
if (typeof payload["contextAutoCompact"] === "boolean") {
|
|
9615
|
+
if (payload["contextAutoCompact"] && autoCompactor) {
|
|
9616
|
+
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
9617
|
+
pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
|
|
9618
|
+
} else {
|
|
9619
|
+
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
9620
|
+
}
|
|
9621
|
+
}
|
|
9622
|
+
if (typeof payload["logLevel"] === "string") {
|
|
9623
|
+
const valid = ["debug", "info", "warn", "error"];
|
|
9624
|
+
if (valid.includes(payload["logLevel"])) {
|
|
9625
|
+
logger.level = payload["logLevel"];
|
|
9626
|
+
}
|
|
9627
|
+
}
|
|
9628
|
+
broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
|
|
9629
|
+
}
|
|
9630
|
+
};
|
|
9323
9631
|
shellGitRoutes = {
|
|
9324
9632
|
gitInfo: async (ws) => {
|
|
9325
9633
|
await handleGitInfo(ws, projectRoot);
|
|
@@ -9372,6 +9680,18 @@ async function startWebUI(opts = {}) {
|
|
|
9372
9680
|
return handleMailboxPurge(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
|
|
9373
9681
|
}
|
|
9374
9682
|
};
|
|
9683
|
+
mcpRoutes = {
|
|
9684
|
+
list: (ws, msg) => handleMcpList(ws, msg, globalConfigPath, mcpRegistry),
|
|
9685
|
+
add: (ws, msg) => handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry),
|
|
9686
|
+
update: (ws, msg) => handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry),
|
|
9687
|
+
remove: (ws, msg) => handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry),
|
|
9688
|
+
enable: (ws, msg) => handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry),
|
|
9689
|
+
disable: (ws, msg) => handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry),
|
|
9690
|
+
sleep: (ws, msg) => handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry),
|
|
9691
|
+
wake: (ws, msg) => handleMcpWake(ws, msg, globalConfigPath, mcpRegistry),
|
|
9692
|
+
restart: (ws, msg) => handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry),
|
|
9693
|
+
discover: (ws, msg) => handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry)
|
|
9694
|
+
};
|
|
9375
9695
|
brainRoutes = {
|
|
9376
9696
|
status: (ws) => {
|
|
9377
9697
|
send(ws, {
|
|
@@ -9439,8 +9759,10 @@ async function startWebUI(opts = {}) {
|
|
|
9439
9759
|
host: wsHost,
|
|
9440
9760
|
distDir: path16.resolve(import.meta.dirname, "../../dist"),
|
|
9441
9761
|
wsPort,
|
|
9762
|
+
publicWsUrl,
|
|
9442
9763
|
globalRoot: wpaths.globalRoot,
|
|
9443
9764
|
apiToken: wsToken,
|
|
9765
|
+
requireToken,
|
|
9444
9766
|
watcherMetrics,
|
|
9445
9767
|
onFleetPing: () => {
|
|
9446
9768
|
void fleetBroadcast?.();
|
|
@@ -9448,7 +9770,12 @@ async function startWebUI(opts = {}) {
|
|
|
9448
9770
|
});
|
|
9449
9771
|
const registryBaseDir = path16.dirname(globalConfigPath);
|
|
9450
9772
|
httpServer.listen(httpPort, wsHost, () => {
|
|
9451
|
-
const openUrl =
|
|
9773
|
+
const openUrl = buildWebUIAccessUrl({
|
|
9774
|
+
host: wsHost,
|
|
9775
|
+
port: httpPort,
|
|
9776
|
+
token: wsToken,
|
|
9777
|
+
publicUrl
|
|
9778
|
+
});
|
|
9452
9779
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
9453
9780
|
if (opts.open) openBrowser(openUrl);
|
|
9454
9781
|
void registerInstance(
|
|
@@ -9460,7 +9787,7 @@ async function startWebUI(opts = {}) {
|
|
|
9460
9787
|
projectRoot,
|
|
9461
9788
|
projectName: path16.basename(projectRoot) || projectRoot,
|
|
9462
9789
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9463
|
-
url:
|
|
9790
|
+
url: buildWebUIAccessUrl({ host: wsHost, port: httpPort, publicUrl })
|
|
9464
9791
|
},
|
|
9465
9792
|
registryBaseDir
|
|
9466
9793
|
).catch((err) => console.warn(JSON.stringify({
|
|
@@ -9502,7 +9829,55 @@ async function startWebUI(opts = {}) {
|
|
|
9502
9829
|
|
|
9503
9830
|
// src/server/entry.ts
|
|
9504
9831
|
var argv = process.argv.slice(2);
|
|
9505
|
-
|
|
9832
|
+
function readArg(names) {
|
|
9833
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9834
|
+
const current = argv[i];
|
|
9835
|
+
if (!current) continue;
|
|
9836
|
+
for (const name2 of names) {
|
|
9837
|
+
if (current === name2) {
|
|
9838
|
+
const next = argv[i + 1];
|
|
9839
|
+
if (!next || next.startsWith("-")) {
|
|
9840
|
+
throw new Error(`${name2} requires a value`);
|
|
9841
|
+
}
|
|
9842
|
+
return next;
|
|
9843
|
+
}
|
|
9844
|
+
if (current.startsWith(`${name2}=`)) return current.slice(name2.length + 1);
|
|
9845
|
+
}
|
|
9846
|
+
}
|
|
9847
|
+
return void 0;
|
|
9848
|
+
}
|
|
9849
|
+
function parsePort(value, fallback, label) {
|
|
9850
|
+
if (value === void 0) return fallback;
|
|
9851
|
+
const parsed = Number.parseInt(value, 10);
|
|
9852
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
9853
|
+
throw new Error(`${label} must be a port between 1 and 65535`);
|
|
9854
|
+
}
|
|
9855
|
+
return parsed;
|
|
9856
|
+
}
|
|
9857
|
+
function envFlag2(name2) {
|
|
9858
|
+
const value = process.env[name2]?.trim().toLowerCase();
|
|
9859
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
9860
|
+
}
|
|
9861
|
+
function printHelp() {
|
|
9862
|
+
console.log(`Usage: wstackui [options]
|
|
9863
|
+
|
|
9864
|
+
Options:
|
|
9865
|
+
--host <host> Bind host/interface (default: 127.0.0.1)
|
|
9866
|
+
--port <port> HTTP frontend port (default: 3456)
|
|
9867
|
+
--ws-port <port> WebSocket backend port (default: 3457)
|
|
9868
|
+
--token <token> Fixed access token/password (default: random per process)
|
|
9869
|
+
--public-url <url> Browser-facing HTTP URL for tunnels/proxies
|
|
9870
|
+
--public-ws-url <url> Browser-facing ws:// or wss:// URL for tunnels/proxies
|
|
9871
|
+
--require-token Require token/password even on loopback binds
|
|
9872
|
+
--open, -o Open the browser after startup
|
|
9873
|
+
--list, -l, ls List running WebUI instances
|
|
9874
|
+
--help, -h Show this help
|
|
9875
|
+
`);
|
|
9876
|
+
}
|
|
9877
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
9878
|
+
printHelp();
|
|
9879
|
+
process.exit(0);
|
|
9880
|
+
} else if (argv.includes("--list") || argv.includes("-l") || argv[0] === "ls") {
|
|
9506
9881
|
listInstances().then((instances) => {
|
|
9507
9882
|
console.log(formatInstances(instances));
|
|
9508
9883
|
process.exit(0);
|
|
@@ -9516,11 +9891,40 @@ if (argv.includes("--list") || argv.includes("-l") || argv[0] === "ls") {
|
|
|
9516
9891
|
process.exit(1);
|
|
9517
9892
|
});
|
|
9518
9893
|
} else {
|
|
9519
|
-
|
|
9520
|
-
|
|
9894
|
+
let wsPort;
|
|
9895
|
+
let httpPort;
|
|
9896
|
+
let wsHost;
|
|
9897
|
+
let accessToken;
|
|
9898
|
+
let publicUrl;
|
|
9899
|
+
let publicWsUrl;
|
|
9900
|
+
try {
|
|
9901
|
+
wsHost = readArg(["--host"]) ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
|
|
9902
|
+
httpPort = parsePort(
|
|
9903
|
+
readArg(["--port", "--http-port"]) ?? process.env["WEBUI_PORT"] ?? process.env["PORT"],
|
|
9904
|
+
3456,
|
|
9905
|
+
"--port"
|
|
9906
|
+
);
|
|
9907
|
+
wsPort = parsePort(readArg(["--ws-port"]) ?? process.env["WS_PORT"], 3457, "--ws-port");
|
|
9908
|
+
accessToken = readArg(["--token", "--auth-token"]) ?? process.env["WEBUI_TOKEN"] ?? process.env["WEBUI_AUTH_TOKEN"];
|
|
9909
|
+
publicUrl = readArg(["--public-url"]) ?? process.env["WEBUI_PUBLIC_URL"];
|
|
9910
|
+
publicWsUrl = readArg(["--public-ws-url"]) ?? process.env["WEBUI_PUBLIC_WS_URL"];
|
|
9911
|
+
} catch (err) {
|
|
9912
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
9913
|
+
process.exit(1);
|
|
9914
|
+
}
|
|
9521
9915
|
const open = argv.includes("--open") || argv.includes("-o") || process.env["WEBUI_OPEN"] === "1";
|
|
9522
|
-
|
|
9523
|
-
|
|
9916
|
+
const requireToken = argv.includes("--require-token") || envFlag2("WEBUI_REQUIRE_TOKEN");
|
|
9917
|
+
console.log(`[WebUI] Starting standalone server on ${wsHost} (http:${httpPort}, ws:${wsPort})...`);
|
|
9918
|
+
startWebUI({
|
|
9919
|
+
wsPort,
|
|
9920
|
+
wsHost,
|
|
9921
|
+
httpPort,
|
|
9922
|
+
accessToken,
|
|
9923
|
+
publicUrl,
|
|
9924
|
+
publicWsUrl,
|
|
9925
|
+
requireToken,
|
|
9926
|
+
open
|
|
9927
|
+
}).catch((err) => {
|
|
9524
9928
|
console.error(JSON.stringify({
|
|
9525
9929
|
level: "fatal",
|
|
9526
9930
|
event: "webui.startup_failed",
|