@wrongstack/webui 0.273.1 → 0.275.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-BG4jUAmc.js +141 -0
- package/dist/assets/index-Cw32ELnp.css +2 -0
- package/dist/assets/{vendor-P9eRrO6V.js → vendor-Cl_sFcw4.js} +251 -251
- package/dist/index.html +3 -3
- package/dist/index.js +9111 -6139
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +1537 -246
- package/dist/server/entry.js.map +1 -1
- package/dist/server/handlers.js.map +1 -1
- package/dist/server/index.d.ts +217 -18
- package/dist/server/index.js +1473 -242
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +275 -5
- package/package.json +7 -7
- package/dist/assets/index-BGzM4-Zu.css +0 -2
- package/dist/assets/index-D0dNaLPf.js +0 -140
package/dist/server/entry.js
CHANGED
|
@@ -171,9 +171,12 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
171
171
|
"reasoningPreserve",
|
|
172
172
|
"hqEnabled",
|
|
173
173
|
"hqRawContent",
|
|
174
|
-
"fallbackAuto"
|
|
174
|
+
"fallbackAuto",
|
|
175
|
+
"favoriteModelsOnly"
|
|
175
176
|
]);
|
|
176
|
-
var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels"]);
|
|
177
|
+
var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels", "favoriteModels"]);
|
|
178
|
+
var STRING_ARRAY_RECORD_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackProfiles"]);
|
|
179
|
+
var MODEL_MATRIX_PREF_KEYS = /* @__PURE__ */ new Set(["modelMatrix"]);
|
|
177
180
|
var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
178
181
|
"autonomyDelayMs",
|
|
179
182
|
"autoProceedMaxIterations",
|
|
@@ -208,6 +211,33 @@ function validatePreferenceValue(key, value) {
|
|
|
208
211
|
if (STRING_ARRAY_PREF_KEYS.has(key)) {
|
|
209
212
|
return Array.isArray(value) && value.every((v) => typeof v === "string") ? null : `prefs.update payload.${key} must be an array of strings`;
|
|
210
213
|
}
|
|
214
|
+
if (STRING_ARRAY_RECORD_PREF_KEYS.has(key)) {
|
|
215
|
+
return isRecord(value) && Object.values(value).every(
|
|
216
|
+
(v) => Array.isArray(v) && v.every((item) => typeof item === "string")
|
|
217
|
+
) ? null : `prefs.update payload.${key} must be an object of string arrays`;
|
|
218
|
+
}
|
|
219
|
+
if (MODEL_MATRIX_PREF_KEYS.has(key)) {
|
|
220
|
+
if (!isRecord(value)) return `prefs.update payload.${key} must be an object`;
|
|
221
|
+
for (const entry of Object.values(value)) {
|
|
222
|
+
if (!isRecord(entry)) return `prefs.update payload.${key} entries must be objects`;
|
|
223
|
+
const provider = entry["provider"];
|
|
224
|
+
const model = entry["model"];
|
|
225
|
+
const fallbackProfile = entry["fallbackProfile"];
|
|
226
|
+
if (provider !== void 0 && typeof provider !== "string") {
|
|
227
|
+
return `prefs.update payload.${key}.provider must be a string when provided`;
|
|
228
|
+
}
|
|
229
|
+
if (model !== void 0 && typeof model !== "string") {
|
|
230
|
+
return `prefs.update payload.${key}.model must be a string when provided`;
|
|
231
|
+
}
|
|
232
|
+
if (fallbackProfile !== void 0 && typeof fallbackProfile !== "string") {
|
|
233
|
+
return `prefs.update payload.${key}.fallbackProfile must be a string when provided`;
|
|
234
|
+
}
|
|
235
|
+
if (model === void 0 && fallbackProfile === void 0) {
|
|
236
|
+
return `prefs.update payload.${key} entries require model or fallbackProfile`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
211
241
|
const allowed = ENUM_PREF_KEYS[key];
|
|
212
242
|
if (allowed) {
|
|
213
243
|
return typeof value === "string" && allowed.has(value) ? null : `prefs.update payload.${key} must be one of: ${Array.from(allowed).join(", ")}`;
|
|
@@ -436,8 +466,8 @@ function validateShellOpenPayload(payload) {
|
|
|
436
466
|
if (!isRecord(payload)) {
|
|
437
467
|
return { ok: false, message: "shell.open payload must be an object with string path" };
|
|
438
468
|
}
|
|
439
|
-
const
|
|
440
|
-
if (typeof
|
|
469
|
+
const path18 = payload["path"];
|
|
470
|
+
if (typeof path18 !== "string" || path18.trim().length === 0) {
|
|
441
471
|
return { ok: false, message: "shell.open payload.path must be a non-empty string" };
|
|
442
472
|
}
|
|
443
473
|
const target = payload["target"];
|
|
@@ -447,20 +477,20 @@ function validateShellOpenPayload(payload) {
|
|
|
447
477
|
message: 'shell.open payload.target must be "file" or "terminal" when provided'
|
|
448
478
|
};
|
|
449
479
|
}
|
|
450
|
-
return { ok: true, value: { path:
|
|
480
|
+
return { ok: true, value: { path: path18, target } };
|
|
451
481
|
}
|
|
452
482
|
function validateGitDiffPayload(payload) {
|
|
453
483
|
if (!isRecord(payload)) {
|
|
454
484
|
return { ok: false, message: "git.diff payload must be an object" };
|
|
455
485
|
}
|
|
456
|
-
const
|
|
457
|
-
if (
|
|
486
|
+
const path18 = payload["path"];
|
|
487
|
+
if (path18 === void 0 || path18 === null) {
|
|
458
488
|
return { ok: true, value: { path: "" } };
|
|
459
489
|
}
|
|
460
|
-
if (typeof
|
|
490
|
+
if (typeof path18 !== "string") {
|
|
461
491
|
return { ok: false, message: "git.diff payload.path must be a string when provided" };
|
|
462
492
|
}
|
|
463
|
-
return { ok: true, value: { path:
|
|
493
|
+
return { ok: true, value: { path: path18 } };
|
|
464
494
|
}
|
|
465
495
|
function validateProjectsAddPayload(payload) {
|
|
466
496
|
if (!isRecord(payload)) {
|
|
@@ -710,8 +740,57 @@ async function handleWorklistMessage(ctx, ws, msg) {
|
|
|
710
740
|
|
|
711
741
|
// src/server/index.ts
|
|
712
742
|
import { makeMailboxTool, makeMailSendTool, makeMailInboxTool, mailboxSessionTag } from "@wrongstack/core";
|
|
713
|
-
import { toErrorMessage as toErrorMessage6, wstackGlobalRoot as
|
|
743
|
+
import { toErrorMessage as toErrorMessage6, wstackGlobalRoot as wstackGlobalRoot3, projectHash } from "@wrongstack/core/utils";
|
|
714
744
|
import { SkillInstaller } from "@wrongstack/core/skills";
|
|
745
|
+
|
|
746
|
+
// src/server/discover-mailbox-bridge.ts
|
|
747
|
+
import { resolveProjectDir, wstackGlobalRoot } from "@wrongstack/core";
|
|
748
|
+
import { readLiveLock } from "@wrongstack/core/coordination";
|
|
749
|
+
async function discoverMailboxBridgeForWebui(params) {
|
|
750
|
+
const mode = params.config?.features?.mailboxBridge ?? "auto";
|
|
751
|
+
if (mode === "off") return;
|
|
752
|
+
const projectDir = resolveProjectDir(params.projectRoot, wstackGlobalRoot());
|
|
753
|
+
const result = await readLiveLock(projectDir);
|
|
754
|
+
switch (result.kind) {
|
|
755
|
+
case "live": {
|
|
756
|
+
params.logger.debug("webui joined existing mailbox bridge", {
|
|
757
|
+
url: result.lock.url,
|
|
758
|
+
lockPath: projectDir
|
|
759
|
+
});
|
|
760
|
+
params.ctx.meta["mailboxBridge"] = {
|
|
761
|
+
url: result.lock.url,
|
|
762
|
+
token: result.lock.token,
|
|
763
|
+
lockPath: projectDir,
|
|
764
|
+
childPid: null,
|
|
765
|
+
source: "joined"
|
|
766
|
+
};
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
case "probe-failed": {
|
|
770
|
+
params.logger.warn(
|
|
771
|
+
"mailbox bridge present but /healthz unreachable; webui will start without external-agent connectivity",
|
|
772
|
+
{ url: result.lock.url, lockPath: projectDir }
|
|
773
|
+
);
|
|
774
|
+
params.ctx.meta["mailboxBridge"] = {
|
|
775
|
+
url: result.lock.url,
|
|
776
|
+
token: result.lock.token,
|
|
777
|
+
lockPath: projectDir,
|
|
778
|
+
childPid: null,
|
|
779
|
+
source: "unhealthy"
|
|
780
|
+
};
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
case "absent": {
|
|
784
|
+
params.logger.info(
|
|
785
|
+
"no mailbox bridge running; webui will start without external-agent connectivity. Run `wstack mailbox serve` or a CLI surface to bring one up.",
|
|
786
|
+
{ projectDir }
|
|
787
|
+
);
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/server/index.ts
|
|
715
794
|
import {
|
|
716
795
|
BrainMonitor,
|
|
717
796
|
DefaultBrainArbiter,
|
|
@@ -719,8 +798,9 @@ import {
|
|
|
719
798
|
createAutonomyBrain,
|
|
720
799
|
createTieredBrainArbiter
|
|
721
800
|
} from "@wrongstack/core";
|
|
722
|
-
import * as
|
|
723
|
-
import * as
|
|
801
|
+
import * as fs14 from "fs/promises";
|
|
802
|
+
import * as path17 from "path";
|
|
803
|
+
import { createRequire as createRequire2 } from "module";
|
|
724
804
|
|
|
725
805
|
// src/server/http-server.ts
|
|
726
806
|
import * as fs from "fs/promises";
|
|
@@ -897,7 +977,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
897
977
|
return;
|
|
898
978
|
}
|
|
899
979
|
try {
|
|
900
|
-
const { SessionRegistry, resolveWstackPaths:
|
|
980
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
|
|
901
981
|
const registry = new SessionRegistry(globalRoot);
|
|
902
982
|
const entry = await registry.get(sessionId);
|
|
903
983
|
if (!entry) {
|
|
@@ -905,7 +985,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
905
985
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
906
986
|
return;
|
|
907
987
|
}
|
|
908
|
-
const paths =
|
|
988
|
+
const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
|
|
909
989
|
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
910
990
|
const reader = new DefaultSessionReader2({ store });
|
|
911
991
|
const rawEntries = [];
|
|
@@ -932,7 +1012,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
932
1012
|
}
|
|
933
1013
|
}
|
|
934
1014
|
function readJsonBody(req) {
|
|
935
|
-
return new Promise((
|
|
1015
|
+
return new Promise((resolve10, reject) => {
|
|
936
1016
|
let data = "";
|
|
937
1017
|
req.on("data", (chunk) => {
|
|
938
1018
|
data += chunk;
|
|
@@ -943,7 +1023,7 @@ function readJsonBody(req) {
|
|
|
943
1023
|
});
|
|
944
1024
|
req.on("end", () => {
|
|
945
1025
|
try {
|
|
946
|
-
|
|
1026
|
+
resolve10(data ? JSON.parse(data) : {});
|
|
947
1027
|
} catch (err) {
|
|
948
1028
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
949
1029
|
}
|
|
@@ -979,7 +1059,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
|
979
1059
|
const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
|
|
980
1060
|
const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
|
|
981
1061
|
try {
|
|
982
|
-
const { SessionRegistry, resolveWstackPaths:
|
|
1062
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
983
1063
|
const registry = new SessionRegistry(globalRoot);
|
|
984
1064
|
const entry = await registry.get(sessionId);
|
|
985
1065
|
if (!entry) {
|
|
@@ -987,7 +1067,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
|
987
1067
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
988
1068
|
return;
|
|
989
1069
|
}
|
|
990
|
-
const paths =
|
|
1070
|
+
const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
|
|
991
1071
|
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
992
1072
|
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
993
1073
|
const sent = await mailbox.send({ from, to, type, subject, body: text, priority });
|
|
@@ -1005,7 +1085,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
|
1005
1085
|
return;
|
|
1006
1086
|
}
|
|
1007
1087
|
try {
|
|
1008
|
-
const { SessionRegistry, resolveWstackPaths:
|
|
1088
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
1009
1089
|
const registry = new SessionRegistry(globalRoot);
|
|
1010
1090
|
const entry = await registry.get(sessionId);
|
|
1011
1091
|
if (!entry) {
|
|
@@ -1013,7 +1093,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
|
1013
1093
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
1014
1094
|
return;
|
|
1015
1095
|
}
|
|
1016
|
-
const paths =
|
|
1096
|
+
const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
|
|
1017
1097
|
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
1018
1098
|
const leaderAddr = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
1019
1099
|
const [inbound, outbound] = await Promise.all([
|
|
@@ -1063,7 +1143,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
|
1063
1143
|
const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
|
|
1064
1144
|
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
1065
1145
|
try {
|
|
1066
|
-
const { SessionRegistry, resolveWstackPaths:
|
|
1146
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
1067
1147
|
const registry = new SessionRegistry(globalRoot);
|
|
1068
1148
|
const entry = await registry.get(sessionId);
|
|
1069
1149
|
if (!entry) {
|
|
@@ -1071,7 +1151,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
|
1071
1151
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
1072
1152
|
return;
|
|
1073
1153
|
}
|
|
1074
|
-
const paths =
|
|
1154
|
+
const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
|
|
1075
1155
|
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
1076
1156
|
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
1077
1157
|
const sent = await mailbox.send({
|
|
@@ -1111,7 +1191,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
1111
1191
|
}
|
|
1112
1192
|
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
1113
1193
|
try {
|
|
1114
|
-
const { SessionRegistry, resolveWstackPaths:
|
|
1194
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
1115
1195
|
const registry = new SessionRegistry(globalRoot);
|
|
1116
1196
|
const all = await registry.list();
|
|
1117
1197
|
const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
|
|
@@ -1123,7 +1203,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
1123
1203
|
}
|
|
1124
1204
|
const mbByDir = /* @__PURE__ */ new Map();
|
|
1125
1205
|
const mailboxFor = (projectRoot) => {
|
|
1126
|
-
const dir =
|
|
1206
|
+
const dir = resolveWstackPaths4({ projectRoot, globalRoot }).projectDir;
|
|
1127
1207
|
let mb = mbByDir.get(dir);
|
|
1128
1208
|
if (!mb) {
|
|
1129
1209
|
mb = new GlobalMailbox3(dir);
|
|
@@ -1167,7 +1247,7 @@ function isTrustedLoopbackOrigin(origin) {
|
|
|
1167
1247
|
try {
|
|
1168
1248
|
const url = new URL(origin);
|
|
1169
1249
|
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
1170
|
-
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
1250
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
|
|
1171
1251
|
} catch {
|
|
1172
1252
|
return false;
|
|
1173
1253
|
}
|
|
@@ -1178,6 +1258,14 @@ function isLoopbackBind(wsHost) {
|
|
|
1178
1258
|
function isWildcardBind(wsHost) {
|
|
1179
1259
|
return wsHost === "0.0.0.0" || wsHost === "::" || wsHost === "[::]";
|
|
1180
1260
|
}
|
|
1261
|
+
function normalizeHostname(hostname) {
|
|
1262
|
+
const h = hostname.trim().toLowerCase();
|
|
1263
|
+
return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
1264
|
+
}
|
|
1265
|
+
function allowedHostname(hostname, allowedHostnames) {
|
|
1266
|
+
const normalized = normalizeHostname(hostname);
|
|
1267
|
+
return (allowedHostnames ?? []).some((candidate) => normalizeHostname(candidate) === normalized);
|
|
1268
|
+
}
|
|
1181
1269
|
function tokenMatches(provided, expected) {
|
|
1182
1270
|
if (!provided) return false;
|
|
1183
1271
|
const a = Buffer.from(provided);
|
|
@@ -1216,28 +1304,37 @@ function hostHeaderOk(input) {
|
|
|
1216
1304
|
} catch {
|
|
1217
1305
|
return false;
|
|
1218
1306
|
}
|
|
1219
|
-
return isLoopbackHostname(hostname);
|
|
1307
|
+
return isLoopbackHostname(hostname) || allowedHostname(hostname, input.allowedHostnames);
|
|
1220
1308
|
}
|
|
1221
1309
|
function verifyClient(input) {
|
|
1222
|
-
const {
|
|
1310
|
+
const {
|
|
1311
|
+
origin,
|
|
1312
|
+
url,
|
|
1313
|
+
hostHeader,
|
|
1314
|
+
remoteAddress,
|
|
1315
|
+
cookieHeader,
|
|
1316
|
+
wsHost,
|
|
1317
|
+
expectedToken,
|
|
1318
|
+
requireToken,
|
|
1319
|
+
allowedHostnames,
|
|
1320
|
+
allowBrowserUrlToken
|
|
1321
|
+
} = input;
|
|
1223
1322
|
const urlTokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
1224
1323
|
const cookieTokenOk = tokenMatches(extractTokenFromCookie(cookieHeader), expectedToken);
|
|
1225
|
-
if (!hostHeaderOk({ hostHeader, wsHost })) return false;
|
|
1324
|
+
if (!hostHeaderOk({ hostHeader, wsHost, allowedHostnames })) return false;
|
|
1226
1325
|
if (!origin) {
|
|
1227
1326
|
const remoteIp = remoteAddress ?? "";
|
|
1228
1327
|
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
1229
1328
|
if (!isRemoteLoopback && isWildcardBind(wsHost)) return false;
|
|
1230
|
-
return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost);
|
|
1329
|
+
return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost) && !requireToken;
|
|
1231
1330
|
}
|
|
1232
1331
|
try {
|
|
1233
|
-
const { hostname } = new URL(origin);
|
|
1234
|
-
if (isLoopbackHostname(
|
|
1235
|
-
if (
|
|
1236
|
-
|
|
1237
|
-
}
|
|
1238
|
-
return true;
|
|
1332
|
+
const { hostname: originHostname } = new URL(origin);
|
|
1333
|
+
if (isLoopbackHostname(originHostname)) {
|
|
1334
|
+
if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
|
|
1335
|
+
return isTrustedLoopbackOrigin(origin);
|
|
1239
1336
|
}
|
|
1240
|
-
return cookieTokenOk;
|
|
1337
|
+
return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
|
|
1241
1338
|
} catch {
|
|
1242
1339
|
return false;
|
|
1243
1340
|
}
|
|
@@ -1263,8 +1360,69 @@ function injectWsPort(html, wsPort) {
|
|
|
1263
1360
|
return `${tag}
|
|
1264
1361
|
${html}`;
|
|
1265
1362
|
}
|
|
1266
|
-
function
|
|
1267
|
-
return
|
|
1363
|
+
function escapeHtmlAttr(value) {
|
|
1364
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1365
|
+
}
|
|
1366
|
+
function injectWsConfig(html, opts) {
|
|
1367
|
+
let out = injectWsPort(html, opts.wsPort);
|
|
1368
|
+
if (!opts.publicWsUrl || out.includes('name="wrongstack-ws-url"')) return out;
|
|
1369
|
+
const tag = `<meta name="wrongstack-ws-url" content="${escapeHtmlAttr(opts.publicWsUrl)}" />`;
|
|
1370
|
+
if (out.includes("</head>")) {
|
|
1371
|
+
return out.replace("</head>", ` ${tag}
|
|
1372
|
+
</head>`);
|
|
1373
|
+
}
|
|
1374
|
+
return `${tag}
|
|
1375
|
+
${out}`;
|
|
1376
|
+
}
|
|
1377
|
+
function firstHeader(value) {
|
|
1378
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1379
|
+
}
|
|
1380
|
+
function wsTokenCookie(token) {
|
|
1381
|
+
return `ws_token=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600`;
|
|
1382
|
+
}
|
|
1383
|
+
function requestToken(req, url) {
|
|
1384
|
+
return url.searchParams.get("token") ?? firstHeader(req.headers["x-ws-token"]) ?? extractTokenFromCookie(req.headers.cookie);
|
|
1385
|
+
}
|
|
1386
|
+
function requestHostForCsp(hostHeader) {
|
|
1387
|
+
const raw = firstHeader(hostHeader)?.trim();
|
|
1388
|
+
if (!raw) return void 0;
|
|
1389
|
+
try {
|
|
1390
|
+
return new URL(`http://${raw}`).hostname;
|
|
1391
|
+
} catch {
|
|
1392
|
+
return void 0;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
function formatCspHostname(hostname) {
|
|
1396
|
+
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
1397
|
+
}
|
|
1398
|
+
function cspSourceFromUrl(rawUrl) {
|
|
1399
|
+
try {
|
|
1400
|
+
const url = new URL(rawUrl);
|
|
1401
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") return void 0;
|
|
1402
|
+
return `${url.protocol}//${formatCspHostname(url.hostname)}${url.port ? `:${url.port}` : ""}`;
|
|
1403
|
+
} catch {
|
|
1404
|
+
return void 0;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
var ALLOWED_INLINE_SCRIPT_HASHES = [
|
|
1408
|
+
"'sha256-6PXDy0zrpXa6mvYOl11bZ8nubNUL7ushPUhGDZtaexg='",
|
|
1409
|
+
"'sha256-6sIdwbEBx7jj0drqSHHm7MqvmoYD3CQ4lp8Zp8blcb0='"
|
|
1410
|
+
];
|
|
1411
|
+
function buildCspHeader(wsPort, requestHost, publicWsUrl) {
|
|
1412
|
+
const connect = /* @__PURE__ */ new Set([
|
|
1413
|
+
"'self'",
|
|
1414
|
+
`ws://127.0.0.1:${wsPort}`,
|
|
1415
|
+
`wss://127.0.0.1:${wsPort}`
|
|
1416
|
+
]);
|
|
1417
|
+
if (requestHost && requestHost !== "127.0.0.1" && requestHost !== "::1" && requestHost !== "[::1]") {
|
|
1418
|
+
const host = formatCspHostname(requestHost);
|
|
1419
|
+
connect.add(`ws://${host}:${wsPort}`);
|
|
1420
|
+
connect.add(`wss://${host}:${wsPort}`);
|
|
1421
|
+
}
|
|
1422
|
+
const publicWsSource = publicWsUrl ? cspSourceFromUrl(publicWsUrl) : void 0;
|
|
1423
|
+
if (publicWsSource) connect.add(publicWsSource);
|
|
1424
|
+
const scriptSrc = ["'self'", ...ALLOWED_INLINE_SCRIPT_HASHES].join(" ");
|
|
1425
|
+
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
1426
|
}
|
|
1269
1427
|
function isInsideDist(candidate, distDir) {
|
|
1270
1428
|
const root = path.resolve(distDir);
|
|
@@ -1282,12 +1440,15 @@ function createHttpServer(opts) {
|
|
|
1282
1440
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
1283
1441
|
const distDir = path.resolve(opts.distDir);
|
|
1284
1442
|
const wsPort = opts.wsPort;
|
|
1285
|
-
const
|
|
1443
|
+
const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
|
|
1286
1444
|
return http.createServer(async (req, res) => {
|
|
1287
1445
|
try {
|
|
1288
1446
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
1447
|
+
const providedAccessToken = requestToken(req, url);
|
|
1448
|
+
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
1449
|
+
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
1289
1450
|
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
1290
|
-
const provided = url
|
|
1451
|
+
const provided = requestToken(req, url);
|
|
1291
1452
|
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
1292
1453
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
1293
1454
|
res.end("Unauthorized");
|
|
@@ -1295,7 +1456,7 @@ function createHttpServer(opts) {
|
|
|
1295
1456
|
}
|
|
1296
1457
|
res.writeHead(200, {
|
|
1297
1458
|
"Content-Type": "text/plain",
|
|
1298
|
-
"Set-Cookie":
|
|
1459
|
+
"Set-Cookie": wsTokenCookie(opts.apiToken),
|
|
1299
1460
|
// Belt-and-braces: tell any caches the cookie response itself
|
|
1300
1461
|
// is sensitive.
|
|
1301
1462
|
"Cache-Control": "no-store"
|
|
@@ -1303,10 +1464,20 @@ function createHttpServer(opts) {
|
|
|
1303
1464
|
res.end("ok");
|
|
1304
1465
|
return;
|
|
1305
1466
|
}
|
|
1467
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1468
|
+
res.writeHead(401, {
|
|
1469
|
+
"Content-Type": "text/plain",
|
|
1470
|
+
"Cache-Control": "no-store"
|
|
1471
|
+
});
|
|
1472
|
+
res.end("Unauthorized");
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (shouldSetAuthCookie && opts.apiToken) {
|
|
1476
|
+
res.setHeader("Set-Cookie", wsTokenCookie(opts.apiToken));
|
|
1477
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1478
|
+
}
|
|
1306
1479
|
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 ?? "")) {
|
|
1480
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1310
1481
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1311
1482
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1312
1483
|
return;
|
|
@@ -1320,9 +1491,7 @@ function createHttpServer(opts) {
|
|
|
1320
1491
|
return;
|
|
1321
1492
|
}
|
|
1322
1493
|
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 ?? "")) {
|
|
1494
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1326
1495
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1327
1496
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1328
1497
|
return;
|
|
@@ -1332,9 +1501,7 @@ function createHttpServer(opts) {
|
|
|
1332
1501
|
}
|
|
1333
1502
|
const agentsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/agents$/);
|
|
1334
1503
|
if (agentsMatch && req.method === "GET") {
|
|
1335
|
-
|
|
1336
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1337
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1504
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1338
1505
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1339
1506
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1340
1507
|
return;
|
|
@@ -1344,9 +1511,7 @@ function createHttpServer(opts) {
|
|
|
1344
1511
|
}
|
|
1345
1512
|
const eventsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
|
|
1346
1513
|
if (eventsMatch && req.method === "GET") {
|
|
1347
|
-
|
|
1348
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1349
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1514
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1350
1515
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1351
1516
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1352
1517
|
return;
|
|
@@ -1358,9 +1523,7 @@ function createHttpServer(opts) {
|
|
|
1358
1523
|
}
|
|
1359
1524
|
const msgMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/message$/);
|
|
1360
1525
|
if (msgMatch && req.method === "POST") {
|
|
1361
|
-
|
|
1362
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1363
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1526
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1364
1527
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1365
1528
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1366
1529
|
return;
|
|
@@ -1370,9 +1533,7 @@ function createHttpServer(opts) {
|
|
|
1370
1533
|
}
|
|
1371
1534
|
const mailboxMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/mailbox$/);
|
|
1372
1535
|
if (mailboxMatch && req.method === "GET") {
|
|
1373
|
-
|
|
1374
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1375
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1536
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1376
1537
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1377
1538
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1378
1539
|
return;
|
|
@@ -1382,9 +1543,7 @@ function createHttpServer(opts) {
|
|
|
1382
1543
|
}
|
|
1383
1544
|
const interruptMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/interrupt$/);
|
|
1384
1545
|
if (interruptMatch && req.method === "POST") {
|
|
1385
|
-
|
|
1386
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1387
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1546
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1388
1547
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1389
1548
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1390
1549
|
return;
|
|
@@ -1398,9 +1557,7 @@ function createHttpServer(opts) {
|
|
|
1398
1557
|
return;
|
|
1399
1558
|
}
|
|
1400
1559
|
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 ?? "")) {
|
|
1560
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1404
1561
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1405
1562
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1406
1563
|
return;
|
|
@@ -1447,11 +1604,14 @@ function createHttpServer(opts) {
|
|
|
1447
1604
|
res.setHeader("X-Frame-Options", "DENY");
|
|
1448
1605
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1449
1606
|
if (ext === ".html") {
|
|
1450
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
1451
|
-
res.setHeader(
|
|
1607
|
+
if (!shouldSetAuthCookie) res.setHeader("Cache-Control", "no-cache");
|
|
1608
|
+
res.setHeader(
|
|
1609
|
+
"Content-Security-Policy",
|
|
1610
|
+
buildCspHeader(wsPort, requestHostForCsp(req.headers.host), opts.publicWsUrl)
|
|
1611
|
+
);
|
|
1452
1612
|
const html = await fs.readFile(resolvedPath, "utf8");
|
|
1453
1613
|
res.writeHead(200);
|
|
1454
|
-
res.end(
|
|
1614
|
+
res.end(injectWsConfig(html, { wsPort, publicWsUrl: opts.publicWsUrl }));
|
|
1455
1615
|
return;
|
|
1456
1616
|
}
|
|
1457
1617
|
const fileContent = await fs.readFile(resolvedPath);
|
|
@@ -1466,9 +1626,13 @@ function createHttpServer(opts) {
|
|
|
1466
1626
|
"X-Content-Type-Options": "nosniff",
|
|
1467
1627
|
"X-Frame-Options": "DENY",
|
|
1468
1628
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
1469
|
-
"Content-Security-Policy": buildCspHeader(
|
|
1629
|
+
"Content-Security-Policy": buildCspHeader(
|
|
1630
|
+
wsPort,
|
|
1631
|
+
requestHostForCsp(req.headers.host),
|
|
1632
|
+
opts.publicWsUrl
|
|
1633
|
+
)
|
|
1470
1634
|
});
|
|
1471
|
-
res.end(
|
|
1635
|
+
res.end(injectWsConfig(html, { wsPort, publicWsUrl: opts.publicWsUrl }));
|
|
1472
1636
|
} catch {
|
|
1473
1637
|
res.writeHead(404);
|
|
1474
1638
|
res.end("Not found");
|
|
@@ -1700,6 +1864,37 @@ function errMessage(err) {
|
|
|
1700
1864
|
function generateAuthToken() {
|
|
1701
1865
|
return randomBytes(16).toString("hex");
|
|
1702
1866
|
}
|
|
1867
|
+
function resolveAuthToken(explicit) {
|
|
1868
|
+
const configured = explicit?.trim() || process.env["WEBUI_TOKEN"]?.trim() || process.env["WEBUI_AUTH_TOKEN"]?.trim();
|
|
1869
|
+
return configured || generateAuthToken();
|
|
1870
|
+
}
|
|
1871
|
+
function hostForBrowserUrl(bindHost) {
|
|
1872
|
+
if (bindHost === "0.0.0.0") return "127.0.0.1";
|
|
1873
|
+
if (bindHost === "::" || bindHost === "[::]") return "[::1]";
|
|
1874
|
+
if (bindHost.includes(":") && !bindHost.startsWith("[")) return `[${bindHost}]`;
|
|
1875
|
+
return bindHost;
|
|
1876
|
+
}
|
|
1877
|
+
function buildWebUIAccessUrl(opts) {
|
|
1878
|
+
const protocol = opts.protocol ?? "http";
|
|
1879
|
+
const base = opts.publicUrl?.trim() || `${protocol}://${hostForBrowserUrl(opts.host)}:${opts.port}`;
|
|
1880
|
+
if (!opts.token) return base;
|
|
1881
|
+
try {
|
|
1882
|
+
const url = new URL(base);
|
|
1883
|
+
url.searchParams.set("token", opts.token);
|
|
1884
|
+
const rendered = url.toString();
|
|
1885
|
+
const afterOrigin = base.slice(url.origin.length);
|
|
1886
|
+
if (url.pathname === "/" && !afterOrigin.startsWith("/")) {
|
|
1887
|
+
return `${url.origin}${url.search}${url.hash}`;
|
|
1888
|
+
}
|
|
1889
|
+
return rendered;
|
|
1890
|
+
} catch {
|
|
1891
|
+
return `${base}${base.includes("?") ? "&" : "?"}token=${encodeURIComponent(opts.token)}`;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
function envFlag(name2) {
|
|
1895
|
+
const value = process.env[name2]?.trim().toLowerCase();
|
|
1896
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
1897
|
+
}
|
|
1703
1898
|
|
|
1704
1899
|
// src/server/file-handlers.ts
|
|
1705
1900
|
async function resolveFileInsideProject(projectRoot, filePath) {
|
|
@@ -1744,6 +1939,16 @@ async function realpathAllowMissing(p) {
|
|
|
1744
1939
|
}
|
|
1745
1940
|
}
|
|
1746
1941
|
}
|
|
1942
|
+
function validatedPayload(msg, label) {
|
|
1943
|
+
if (msg == null || typeof msg !== "object") {
|
|
1944
|
+
throw new TypeError(`Expected object for ${label}, got ${msg}`);
|
|
1945
|
+
}
|
|
1946
|
+
const payload = msg.payload;
|
|
1947
|
+
if (payload == null || typeof payload !== "object") {
|
|
1948
|
+
throw new TypeError(`Expected payload object for ${label}, got ${payload}`);
|
|
1949
|
+
}
|
|
1950
|
+
return payload;
|
|
1951
|
+
}
|
|
1747
1952
|
async function handleFilesTree(ws, msg, projectRoot) {
|
|
1748
1953
|
const payload = msg.payload;
|
|
1749
1954
|
const rawPath = payload?.path?.trim();
|
|
@@ -1814,7 +2019,13 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1814
2019
|
}
|
|
1815
2020
|
}
|
|
1816
2021
|
async function handleFilesRead(ws, msg, projectRoot) {
|
|
1817
|
-
|
|
2022
|
+
let filePath;
|
|
2023
|
+
try {
|
|
2024
|
+
({ filePath } = validatedPayload(msg, "files.read"));
|
|
2025
|
+
} catch {
|
|
2026
|
+
send(ws, { type: "files.read", payload: { filePath: "", content: "", error: "Malformed request" } });
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
1818
2029
|
let realResolved;
|
|
1819
2030
|
try {
|
|
1820
2031
|
realResolved = await resolveFileInsideProject(projectRoot, filePath);
|
|
@@ -1833,7 +2044,14 @@ async function handleFilesRead(ws, msg, projectRoot) {
|
|
|
1833
2044
|
}
|
|
1834
2045
|
}
|
|
1835
2046
|
async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
|
|
1836
|
-
|
|
2047
|
+
let filePath;
|
|
2048
|
+
let content;
|
|
2049
|
+
try {
|
|
2050
|
+
({ filePath, content } = validatedPayload(msg, "files.write"));
|
|
2051
|
+
} catch {
|
|
2052
|
+
send(ws, { type: "files.written", payload: { filePath: "", success: false, error: "Malformed request" } });
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
1837
2055
|
let realResolved;
|
|
1838
2056
|
try {
|
|
1839
2057
|
realResolved = await resolveFileInsideProject(projectRoot, filePath);
|
|
@@ -2671,7 +2889,7 @@ import { promises as fs5 } from "fs";
|
|
|
2671
2889
|
import path6 from "path";
|
|
2672
2890
|
import JSZip from "jszip";
|
|
2673
2891
|
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
2674
|
-
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
2892
|
+
import { wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
|
|
2675
2893
|
async function handleSkillsList(ws, ctx) {
|
|
2676
2894
|
if (!ctx.skillLoader) {
|
|
2677
2895
|
send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
|
|
@@ -2841,7 +3059,7 @@ async function handleSkillsCreate(ws, ctx, msg) {
|
|
|
2841
3059
|
}
|
|
2842
3060
|
const createPayload = parsed.value;
|
|
2843
3061
|
try {
|
|
2844
|
-
const targetDir = createPayload.scope === "global" ? path6.join(
|
|
3062
|
+
const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot2(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
2845
3063
|
try {
|
|
2846
3064
|
await fs5.access(targetDir);
|
|
2847
3065
|
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
@@ -2961,6 +3179,416 @@ async function handleSkillsExport(ws, ctx) {
|
|
|
2961
3179
|
}
|
|
2962
3180
|
}
|
|
2963
3181
|
|
|
3182
|
+
// src/server/prompts-handlers.ts
|
|
3183
|
+
function parseVariablesPayload(raw) {
|
|
3184
|
+
if (!Array.isArray(raw)) return void 0;
|
|
3185
|
+
const out = [];
|
|
3186
|
+
for (const item of raw) {
|
|
3187
|
+
if (!item || typeof item !== "object") continue;
|
|
3188
|
+
const o = item;
|
|
3189
|
+
if (typeof o["name"] !== "string" || !o["name"].trim()) continue;
|
|
3190
|
+
const enumVals = Array.isArray(o["enum"]) && o["enum"].every((x) => typeof x === "string") ? o["enum"].map((s) => s.trim()).filter(Boolean) : void 0;
|
|
3191
|
+
const v = { name: o["name"].trim() };
|
|
3192
|
+
if (typeof o["description"] === "string" && o["description"].trim()) {
|
|
3193
|
+
v.description = o["description"].trim();
|
|
3194
|
+
}
|
|
3195
|
+
if (o["required"] === true) v.required = true;
|
|
3196
|
+
if (o["multiline"] === true) v.multiline = true;
|
|
3197
|
+
if (enumVals && enumVals.length > 0) v.enum = enumVals;
|
|
3198
|
+
out.push(v);
|
|
3199
|
+
}
|
|
3200
|
+
return out.length > 0 ? out : void 0;
|
|
3201
|
+
}
|
|
3202
|
+
function toMeta(e) {
|
|
3203
|
+
return {
|
|
3204
|
+
id: e.id,
|
|
3205
|
+
slug: e.slug,
|
|
3206
|
+
title: e.title,
|
|
3207
|
+
description: e.description,
|
|
3208
|
+
category: e.category,
|
|
3209
|
+
tags: e.tags,
|
|
3210
|
+
source: e.source,
|
|
3211
|
+
favorite: e.favorite,
|
|
3212
|
+
variables: e.variables ?? []
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
3215
|
+
async function handlePromptsList(ws, ctx) {
|
|
3216
|
+
if (!ctx.promptLoader) {
|
|
3217
|
+
send(ws, { type: "prompts.list", payload: { enabled: false, prompts: [], categories: [] } });
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
try {
|
|
3221
|
+
const [all, categories] = await Promise.all([
|
|
3222
|
+
ctx.promptLoader.list(),
|
|
3223
|
+
ctx.promptLoader.categories()
|
|
3224
|
+
]);
|
|
3225
|
+
send(ws, {
|
|
3226
|
+
type: "prompts.list",
|
|
3227
|
+
payload: { enabled: true, prompts: all.map(toMeta), categories }
|
|
3228
|
+
});
|
|
3229
|
+
} catch (err) {
|
|
3230
|
+
send(ws, {
|
|
3231
|
+
type: "prompts.list",
|
|
3232
|
+
payload: { enabled: true, prompts: [], categories: [], error: errMessage(err) }
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
async function handlePromptsSearch(ws, ctx, msg) {
|
|
3237
|
+
if (!ctx.promptLoader) {
|
|
3238
|
+
send(ws, { type: "prompts.search", payload: { enabled: false, prompts: [] } });
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
const payload = msg.payload ?? {};
|
|
3242
|
+
try {
|
|
3243
|
+
const results = await ctx.promptLoader.search(payload.query ?? "", {
|
|
3244
|
+
...payload.category ? { category: payload.category } : {},
|
|
3245
|
+
limit: 50
|
|
3246
|
+
});
|
|
3247
|
+
send(ws, { type: "prompts.search", payload: { enabled: true, prompts: results.map(toMeta) } });
|
|
3248
|
+
} catch (err) {
|
|
3249
|
+
send(ws, {
|
|
3250
|
+
type: "prompts.search",
|
|
3251
|
+
payload: { enabled: true, prompts: [], error: errMessage(err) }
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
async function handlePromptsContent(ws, ctx, msg) {
|
|
3256
|
+
const slug = msg.payload?.slug;
|
|
3257
|
+
if (!ctx.promptLoader || !slug) {
|
|
3258
|
+
send(ws, {
|
|
3259
|
+
type: "prompts.content",
|
|
3260
|
+
payload: { slug: slug ?? "", found: false, content: "", variables: [] }
|
|
3261
|
+
});
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
try {
|
|
3265
|
+
const entry = await ctx.promptLoader.find(slug);
|
|
3266
|
+
if (!entry) {
|
|
3267
|
+
send(ws, {
|
|
3268
|
+
type: "prompts.content",
|
|
3269
|
+
payload: { slug, found: false, content: "", variables: [] }
|
|
3270
|
+
});
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
send(ws, {
|
|
3274
|
+
type: "prompts.content",
|
|
3275
|
+
payload: {
|
|
3276
|
+
slug: entry.slug,
|
|
3277
|
+
found: true,
|
|
3278
|
+
title: entry.title,
|
|
3279
|
+
content: entry.content,
|
|
3280
|
+
variables: entry.variables ?? [],
|
|
3281
|
+
category: entry.category,
|
|
3282
|
+
source: entry.source
|
|
3283
|
+
}
|
|
3284
|
+
});
|
|
3285
|
+
} catch (err) {
|
|
3286
|
+
send(ws, {
|
|
3287
|
+
type: "prompts.content",
|
|
3288
|
+
payload: { slug, found: false, content: "", variables: [], error: errMessage(err) }
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
async function handlePromptsFavorite(ws, ctx, msg) {
|
|
3293
|
+
const payload = msg.payload;
|
|
3294
|
+
if (!ctx.promptLoader || !payload?.slug) {
|
|
3295
|
+
send(ws, {
|
|
3296
|
+
type: "prompts.favorite",
|
|
3297
|
+
payload: { success: false, error: "Prompt library unavailable" }
|
|
3298
|
+
});
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
try {
|
|
3302
|
+
const updated = await ctx.promptLoader.setFavorite(payload.slug, payload.favorite !== false);
|
|
3303
|
+
if (!updated) {
|
|
3304
|
+
send(ws, {
|
|
3305
|
+
type: "prompts.favorite",
|
|
3306
|
+
payload: { success: false, error: "Prompt not found" }
|
|
3307
|
+
});
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
send(ws, {
|
|
3311
|
+
type: "prompts.favorite",
|
|
3312
|
+
payload: { success: true, slug: updated.slug, favorite: updated.favorite }
|
|
3313
|
+
});
|
|
3314
|
+
} catch (err) {
|
|
3315
|
+
send(ws, { type: "prompts.favorite", payload: { success: false, error: errMessage(err) } });
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
async function handlePromptsCreate(ws, ctx, msg) {
|
|
3319
|
+
const p = msg.payload;
|
|
3320
|
+
if (!ctx.promptLoader || !p) {
|
|
3321
|
+
send(ws, {
|
|
3322
|
+
type: "prompts.created",
|
|
3323
|
+
payload: { success: false, error: "Prompt library unavailable" }
|
|
3324
|
+
});
|
|
3325
|
+
return;
|
|
3326
|
+
}
|
|
3327
|
+
const title = typeof p["title"] === "string" ? p["title"].trim() : "";
|
|
3328
|
+
const content = typeof p["content"] === "string" ? p["content"] : "";
|
|
3329
|
+
if (!title || !content) {
|
|
3330
|
+
send(ws, {
|
|
3331
|
+
type: "prompts.created",
|
|
3332
|
+
payload: { success: false, error: "Title and content are required" }
|
|
3333
|
+
});
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
try {
|
|
3337
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3338
|
+
const tags = Array.isArray(p["tags"]) ? p["tags"].filter((t) => typeof t === "string") : [];
|
|
3339
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "prompt";
|
|
3340
|
+
const variables = parseVariablesPayload(p["variables"]);
|
|
3341
|
+
const entry = {
|
|
3342
|
+
id: slug,
|
|
3343
|
+
slug,
|
|
3344
|
+
title,
|
|
3345
|
+
description: typeof p["description"] === "string" ? p["description"] : "",
|
|
3346
|
+
content,
|
|
3347
|
+
category: typeof p["category"] === "string" && p["category"] ? p["category"] : "uncategorized",
|
|
3348
|
+
tags,
|
|
3349
|
+
source: "user",
|
|
3350
|
+
favorite: false,
|
|
3351
|
+
...variables ? { variables } : {},
|
|
3352
|
+
createdAt: now,
|
|
3353
|
+
updatedAt: now
|
|
3354
|
+
};
|
|
3355
|
+
await ctx.promptLoader.save(entry);
|
|
3356
|
+
send(ws, { type: "prompts.created", payload: { success: true, slug } });
|
|
3357
|
+
} catch (err) {
|
|
3358
|
+
send(ws, { type: "prompts.created", payload: { success: false, error: errMessage(err) } });
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
async function handlePromptsUsed(ws, ctx, msg) {
|
|
3362
|
+
const slug = msg.payload?.slug;
|
|
3363
|
+
if (!ctx.promptUsage || !slug) {
|
|
3364
|
+
send(ws, { type: "prompts.used", payload: { success: false } });
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
try {
|
|
3368
|
+
await ctx.promptUsage.record(slug);
|
|
3369
|
+
send(ws, { type: "prompts.used", payload: { success: true, slug } });
|
|
3370
|
+
} catch {
|
|
3371
|
+
send(ws, { type: "prompts.used", payload: { success: false } });
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
async function handlePromptsRecent(ws, ctx) {
|
|
3375
|
+
if (!ctx.promptUsage) {
|
|
3376
|
+
send(ws, { type: "prompts.recent", payload: { slugs: [] } });
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
try {
|
|
3380
|
+
const recent = await ctx.promptUsage.recent(50);
|
|
3381
|
+
send(ws, { type: "prompts.recent", payload: { slugs: recent.map((r) => r.slug) } });
|
|
3382
|
+
} catch (err) {
|
|
3383
|
+
send(ws, { type: "prompts.recent", payload: { slugs: [], error: errMessage(err) } });
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
// src/server/design-handlers.ts
|
|
3388
|
+
import * as fs6 from "fs/promises";
|
|
3389
|
+
import * as path7 from "path";
|
|
3390
|
+
import {
|
|
3391
|
+
applyTokenOverrides,
|
|
3392
|
+
getDesignKitLoader,
|
|
3393
|
+
getDesignState,
|
|
3394
|
+
isDesignStack,
|
|
3395
|
+
loadActiveKit,
|
|
3396
|
+
materializeTokens,
|
|
3397
|
+
recordKitChoice,
|
|
3398
|
+
recordOverrides,
|
|
3399
|
+
runDesignVerify,
|
|
3400
|
+
setActiveKit,
|
|
3401
|
+
setDesignOverrides
|
|
3402
|
+
} from "@wrongstack/core";
|
|
3403
|
+
function readOverrides(value) {
|
|
3404
|
+
const out = {};
|
|
3405
|
+
if (value && typeof value === "object") {
|
|
3406
|
+
for (const [k, v] of Object.entries(value)) {
|
|
3407
|
+
if (typeof v === "string") out[k] = v;
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
return out;
|
|
3411
|
+
}
|
|
3412
|
+
var FOUNDATIONS_ID = "_foundations";
|
|
3413
|
+
async function buildListPayload(ctx) {
|
|
3414
|
+
const loader = getDesignKitLoader(ctx.projectRoot);
|
|
3415
|
+
const manifests = (await loader.list()).filter((k) => k.id !== FOUNDATIONS_ID);
|
|
3416
|
+
const kits = [];
|
|
3417
|
+
for (const m of manifests) {
|
|
3418
|
+
const tokens = await loader.readTokens(m.id);
|
|
3419
|
+
kits.push({
|
|
3420
|
+
id: m.id,
|
|
3421
|
+
name: m.name,
|
|
3422
|
+
aesthetic: m.aesthetic,
|
|
3423
|
+
bestFor: m.bestFor,
|
|
3424
|
+
stacks: m.stacks,
|
|
3425
|
+
tags: m.tags,
|
|
3426
|
+
light: tokens?.light ?? {},
|
|
3427
|
+
dark: tokens?.dark ?? {}
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
const state = ctx.agentMeta ? getDesignState(ctx.agentMeta) : void 0;
|
|
3431
|
+
const persisted = await loadActiveKit(ctx.projectRoot).catch(() => void 0);
|
|
3432
|
+
return {
|
|
3433
|
+
kits,
|
|
3434
|
+
activeKit: state?.activeKit ?? persisted?.kit ?? null,
|
|
3435
|
+
stack: state?.stack ?? persisted?.stack ?? null,
|
|
3436
|
+
overrides: state?.overrides ?? persisted?.overrides ?? {}
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
async function handleDesignList(ws, ctx) {
|
|
3440
|
+
try {
|
|
3441
|
+
send(ws, { type: "design.list", payload: await buildListPayload(ctx) });
|
|
3442
|
+
} catch (err) {
|
|
3443
|
+
send(ws, {
|
|
3444
|
+
type: "design.list",
|
|
3445
|
+
payload: { kits: [], activeKit: null, stack: null, error: String(err) }
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
async function handleDesignState(ws, ctx) {
|
|
3450
|
+
const state = ctx.agentMeta ? getDesignState(ctx.agentMeta) : void 0;
|
|
3451
|
+
send(ws, {
|
|
3452
|
+
type: "design.state",
|
|
3453
|
+
payload: {
|
|
3454
|
+
activeKit: state?.activeKit ?? null,
|
|
3455
|
+
stack: state?.stack ?? null,
|
|
3456
|
+
overrides: state?.overrides ?? {}
|
|
3457
|
+
}
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
async function handleDesignUse(ws, ctx, msg) {
|
|
3461
|
+
const payload = msg.payload ?? {};
|
|
3462
|
+
const kitId = typeof payload.kit === "string" ? payload.kit.trim() : "";
|
|
3463
|
+
if (!kitId) {
|
|
3464
|
+
send(ws, { type: "design.use", payload: { ok: false, error: "No kit id provided" } });
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
try {
|
|
3468
|
+
const loader = getDesignKitLoader(ctx.projectRoot);
|
|
3469
|
+
const kit = await loader.find(kitId);
|
|
3470
|
+
if (!kit) {
|
|
3471
|
+
send(ws, { type: "design.use", payload: { ok: false, kit: kitId, error: "Kit not found" } });
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
const stackArg = typeof payload.stack === "string" ? payload.stack : void 0;
|
|
3475
|
+
const stack = stackArg && isDesignStack(stackArg) ? stackArg : kit.stacks[0] ?? "web";
|
|
3476
|
+
const persisted = await loadActiveKit(ctx.projectRoot).catch(() => void 0);
|
|
3477
|
+
const keep = persisted?.kit === kit.id ? persisted.overrides ?? {} : {};
|
|
3478
|
+
const overrides = { ...keep, ...readOverrides(payload.overrides) };
|
|
3479
|
+
if (ctx.agentMeta) setActiveKit(ctx.agentMeta, kit.id, stack, overrides);
|
|
3480
|
+
await recordKitChoice(
|
|
3481
|
+
ctx.projectRoot,
|
|
3482
|
+
kit.id,
|
|
3483
|
+
stack,
|
|
3484
|
+
"webui",
|
|
3485
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
3486
|
+
Object.keys(overrides).length ? overrides : void 0
|
|
3487
|
+
);
|
|
3488
|
+
const body = await loader.readBody(kit.id, stack);
|
|
3489
|
+
const rawTokens = await loader.readTokens(kit.id);
|
|
3490
|
+
const tokens = rawTokens ? applyTokenOverrides(rawTokens, overrides) : rawTokens;
|
|
3491
|
+
send(ws, {
|
|
3492
|
+
type: "design.use",
|
|
3493
|
+
payload: {
|
|
3494
|
+
ok: true,
|
|
3495
|
+
kit: kit.id,
|
|
3496
|
+
name: kit.name,
|
|
3497
|
+
aesthetic: kit.aesthetic,
|
|
3498
|
+
stack,
|
|
3499
|
+
body,
|
|
3500
|
+
overrides,
|
|
3501
|
+
light: tokens?.light ?? {},
|
|
3502
|
+
dark: tokens?.dark ?? {}
|
|
3503
|
+
}
|
|
3504
|
+
});
|
|
3505
|
+
} catch (err) {
|
|
3506
|
+
send(ws, { type: "design.use", payload: { ok: false, kit: kitId, error: String(err) } });
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
async function handleDesignSet(ws, ctx, msg) {
|
|
3510
|
+
const patch = readOverrides(msg.payload?.overrides);
|
|
3511
|
+
if (Object.keys(patch).length === 0) {
|
|
3512
|
+
send(ws, { type: "design.set", payload: { ok: false, error: "No overrides provided" } });
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
try {
|
|
3516
|
+
const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
|
|
3517
|
+
if (!merged) {
|
|
3518
|
+
send(ws, { type: "design.set", payload: { ok: false, error: "No active kit" } });
|
|
3519
|
+
return;
|
|
3520
|
+
}
|
|
3521
|
+
if (ctx.agentMeta) setDesignOverrides(ctx.agentMeta, merged);
|
|
3522
|
+
send(ws, { type: "design.set", payload: { ok: true, overrides: merged } });
|
|
3523
|
+
} catch (err) {
|
|
3524
|
+
send(ws, { type: "design.set", payload: { ok: false, error: String(err) } });
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
async function handleDesignMaterialize(ws, ctx, msg) {
|
|
3528
|
+
const payload = msg.payload ?? {};
|
|
3529
|
+
try {
|
|
3530
|
+
const active = await loadActiveKit(ctx.projectRoot);
|
|
3531
|
+
if (!active) {
|
|
3532
|
+
send(ws, { type: "design.materialize", payload: { ok: false, error: "No active kit" } });
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
const loader = getDesignKitLoader(ctx.projectRoot);
|
|
3536
|
+
const stackArg = typeof payload.stack === "string" ? payload.stack : void 0;
|
|
3537
|
+
const stack = stackArg && isDesignStack(stackArg) ? stackArg : active.stack && isDesignStack(active.stack) ? active.stack : "web";
|
|
3538
|
+
const raw = await loader.readTokens(active.kit);
|
|
3539
|
+
if (!raw) {
|
|
3540
|
+
send(ws, { type: "design.materialize", payload: { ok: false, error: "Kit has no tokens" } });
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
const tokens = applyTokenOverrides(raw, active.overrides);
|
|
3544
|
+
const result = materializeTokens({
|
|
3545
|
+
tokens,
|
|
3546
|
+
stack,
|
|
3547
|
+
kitId: active.kit,
|
|
3548
|
+
outPath: typeof payload.out === "string" ? payload.out : void 0
|
|
3549
|
+
});
|
|
3550
|
+
const abs = path7.join(ctx.projectRoot, result.path);
|
|
3551
|
+
await fs6.mkdir(path7.dirname(abs), { recursive: true });
|
|
3552
|
+
await fs6.writeFile(abs, result.content);
|
|
3553
|
+
send(ws, {
|
|
3554
|
+
type: "design.materialize",
|
|
3555
|
+
payload: { ok: true, path: result.path, format: result.format, stack }
|
|
3556
|
+
});
|
|
3557
|
+
} catch (err) {
|
|
3558
|
+
send(ws, { type: "design.materialize", payload: { ok: false, error: String(err) } });
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
async function handleDesignVerify(ws, ctx) {
|
|
3562
|
+
try {
|
|
3563
|
+
const active = await loadActiveKit(ctx.projectRoot);
|
|
3564
|
+
if (!active) {
|
|
3565
|
+
send(ws, { type: "design.verify", payload: { ok: false, error: "No active kit" } });
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
const loader = getDesignKitLoader(ctx.projectRoot);
|
|
3569
|
+
const raw = await loader.readTokens(active.kit);
|
|
3570
|
+
if (!raw) {
|
|
3571
|
+
send(ws, { type: "design.verify", payload: { ok: false, error: "Kit has no tokens" } });
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
const tokens = applyTokenOverrides(raw, active.overrides);
|
|
3575
|
+
const report = await runDesignVerify(ctx.projectRoot, tokens);
|
|
3576
|
+
send(ws, {
|
|
3577
|
+
type: "design.verify",
|
|
3578
|
+
payload: {
|
|
3579
|
+
ok: true,
|
|
3580
|
+
kit: active.kit,
|
|
3581
|
+
filesScanned: report.filesScanned,
|
|
3582
|
+
score: report.score,
|
|
3583
|
+
violations: report.violations.slice(0, 50),
|
|
3584
|
+
violationCount: report.violations.length
|
|
3585
|
+
}
|
|
3586
|
+
});
|
|
3587
|
+
} catch (err) {
|
|
3588
|
+
send(ws, { type: "design.verify", payload: { ok: false, error: String(err) } });
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
|
|
2964
3592
|
// src/server/index.ts
|
|
2965
3593
|
import {
|
|
2966
3594
|
Agent,
|
|
@@ -2972,6 +3600,8 @@ import {
|
|
|
2972
3600
|
DefaultSessionReader,
|
|
2973
3601
|
DefaultSessionStore as DefaultSessionStore2,
|
|
2974
3602
|
DefaultSkillLoader,
|
|
3603
|
+
DefaultPromptLoader,
|
|
3604
|
+
PromptUsageStore,
|
|
2975
3605
|
DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
|
|
2976
3606
|
DefaultTokenCounter,
|
|
2977
3607
|
AnnotationsStore,
|
|
@@ -2986,17 +3616,20 @@ import {
|
|
|
2986
3616
|
ToolRegistry,
|
|
2987
3617
|
atomicWrite as atomicWrite6,
|
|
2988
3618
|
createDefaultPipelines,
|
|
3619
|
+
installDesignStudioMiddleware,
|
|
2989
3620
|
createSessionEventBridge,
|
|
2990
3621
|
resolveSessionLoggingConfig,
|
|
2991
3622
|
DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2,
|
|
2992
3623
|
DEFAULT_SESSION_PRUNE_DAYS,
|
|
2993
3624
|
DEFAULT_TOOLS_CONFIG,
|
|
2994
3625
|
applyToolDescriptionModes,
|
|
3626
|
+
applyToolResultRenderModes,
|
|
2995
3627
|
resolveContextWindowPolicy as resolveContextWindowPolicy2,
|
|
2996
3628
|
enhanceUserPrompt,
|
|
2997
3629
|
gatedEnhancerReasoning,
|
|
2998
3630
|
recentTextTurns,
|
|
2999
|
-
resolveProviderModelList
|
|
3631
|
+
resolveProviderModelList,
|
|
3632
|
+
cleanupStaleSddWorktrees as cleanupStaleSddWorktrees3
|
|
3000
3633
|
} from "@wrongstack/core";
|
|
3001
3634
|
import { ToolExecutor } from "@wrongstack/core/execution";
|
|
3002
3635
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
|
|
@@ -3031,6 +3664,13 @@ import {
|
|
|
3031
3664
|
PhaseStore,
|
|
3032
3665
|
WorktreeManager
|
|
3033
3666
|
} from "@wrongstack/core";
|
|
3667
|
+
function deriveTitle(goal) {
|
|
3668
|
+
const firstLine = goal.split("\n").map((l) => l.trim()).find(Boolean);
|
|
3669
|
+
if (!firstLine) return "AutoPhase";
|
|
3670
|
+
const sentence = firstLine.split(/(?<=[.!?])\s/)[0] ?? firstLine;
|
|
3671
|
+
const trimmed = sentence.length <= 64 ? sentence : `${sentence.slice(0, 63).trimEnd()}\u2026`;
|
|
3672
|
+
return trimmed || "AutoPhase";
|
|
3673
|
+
}
|
|
3034
3674
|
function isGitRepo(cwd) {
|
|
3035
3675
|
try {
|
|
3036
3676
|
const r = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, encoding: "utf8", windowsHide: true });
|
|
@@ -3039,6 +3679,19 @@ function isGitRepo(cwd) {
|
|
|
3039
3679
|
return false;
|
|
3040
3680
|
}
|
|
3041
3681
|
}
|
|
3682
|
+
function commitsSince(cwd, baseSha, branch) {
|
|
3683
|
+
try {
|
|
3684
|
+
const r = spawnSync("git", ["log", "--reverse", "--format=%H", `${baseSha}..${branch}`], {
|
|
3685
|
+
cwd,
|
|
3686
|
+
encoding: "utf8",
|
|
3687
|
+
windowsHide: true
|
|
3688
|
+
});
|
|
3689
|
+
if (r.status !== 0) return [];
|
|
3690
|
+
return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3691
|
+
} catch {
|
|
3692
|
+
return [];
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3042
3695
|
var AutoPhaseWebSocketHandler = class {
|
|
3043
3696
|
constructor(agent, context, logger, storeDir, events, projectRoot) {
|
|
3044
3697
|
this.agent = agent;
|
|
@@ -3058,10 +3711,17 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3058
3711
|
store;
|
|
3059
3712
|
clients = /* @__PURE__ */ new Set();
|
|
3060
3713
|
broadcastInterval = null;
|
|
3061
|
-
/** Aborts in-flight task agents when the run is stopped. */
|
|
3714
|
+
/** Aborts in-flight task agents AND the planning turn when the run is stopped. */
|
|
3062
3715
|
abort = null;
|
|
3716
|
+
/** Set the instant a stop/clear/revert is requested, so a planning turn that
|
|
3717
|
+
* resolves afterwards never launches the orchestrator (the abort alone can't
|
|
3718
|
+
* cover the window between the LLM call resolving and the orchestrator start). */
|
|
3719
|
+
stopping = false;
|
|
3063
3720
|
/** Optional per-phase git-worktree isolation (lazily created at start). */
|
|
3064
3721
|
worktrees = null;
|
|
3722
|
+
/** Base branch + tip SHA captured at run start so a revert can git-revert the
|
|
3723
|
+
* run's squash commits (history-preserving) instead of a destructive reset. */
|
|
3724
|
+
runBase = null;
|
|
3065
3725
|
/** Per-run worker identities so the board can show "who is on what". */
|
|
3066
3726
|
usedNicknames = /* @__PURE__ */ new Set();
|
|
3067
3727
|
addClient(ws) {
|
|
@@ -3085,11 +3745,13 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3085
3745
|
this.broadcast({ type: "autophase.resumed", payload: {} });
|
|
3086
3746
|
break;
|
|
3087
3747
|
case "autophase.stop":
|
|
3088
|
-
this.
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3748
|
+
await this.handleStop();
|
|
3749
|
+
break;
|
|
3750
|
+
case "autophase.clear":
|
|
3751
|
+
await this.handleClear();
|
|
3752
|
+
break;
|
|
3753
|
+
case "autophase.revert":
|
|
3754
|
+
await this.handleRevert();
|
|
3093
3755
|
break;
|
|
3094
3756
|
case "autophase.status":
|
|
3095
3757
|
this.broadcastState();
|
|
@@ -3166,17 +3828,27 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3166
3828
|
}
|
|
3167
3829
|
}
|
|
3168
3830
|
async handleStart(payload) {
|
|
3169
|
-
const
|
|
3831
|
+
const goal = payload?.goal || payload?.title || "Untitled Project";
|
|
3832
|
+
const title = deriveTitle(goal);
|
|
3170
3833
|
const autonomous = payload?.autonomous ?? true;
|
|
3171
|
-
|
|
3834
|
+
this.abort = new AbortController();
|
|
3835
|
+
this.stopping = false;
|
|
3836
|
+
const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, this.abort.signal);
|
|
3837
|
+
if (this.stopping || this.abort.signal.aborted) {
|
|
3838
|
+
this.broadcast({ type: "autophase.stopped", payload: { title } });
|
|
3839
|
+
return;
|
|
3840
|
+
}
|
|
3172
3841
|
this.logger.info(`[AutoPhase] Starting: ${title}`);
|
|
3173
|
-
const graph = await new PhaseGraphBuilder({ title, phases, autonomous }).build();
|
|
3842
|
+
const graph = await new PhaseGraphBuilder({ title, description: goal, phases, autonomous }).build();
|
|
3174
3843
|
this.graph = graph;
|
|
3175
|
-
this.abort = new AbortController();
|
|
3176
3844
|
await this.store.save(graph);
|
|
3177
|
-
|
|
3845
|
+
const useWorktrees = payload?.worktrees ?? process.env["WRONGSTACK_AUTOPHASE_WORKTREES"] !== "0";
|
|
3846
|
+
if (!this.worktrees && this.events && this.projectRoot && useWorktrees && isGitRepo(this.projectRoot)) {
|
|
3178
3847
|
this.worktrees = new WorktreeManager({ projectRoot: this.projectRoot, events: this.events });
|
|
3179
3848
|
}
|
|
3849
|
+
if (this.worktrees) {
|
|
3850
|
+
this.runBase = await this.worktrees.currentBase();
|
|
3851
|
+
}
|
|
3180
3852
|
this.orchestrator = new PhaseOrchestrator({
|
|
3181
3853
|
graph,
|
|
3182
3854
|
ctx: {
|
|
@@ -3223,6 +3895,62 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3223
3895
|
this.broadcast({ type: "autophase.failed", payload: { title, error: String(err) } });
|
|
3224
3896
|
});
|
|
3225
3897
|
}
|
|
3898
|
+
/**
|
|
3899
|
+
* Halt the run NOW — at any phase. Sets `stopping` (so a planning turn that
|
|
3900
|
+
* resolves afterwards bails), aborts in-flight agents, stops the orchestrator
|
|
3901
|
+
* tick, and ends the live broadcast. The board is kept for review; use
|
|
3902
|
+
* `autophase.clear` to reset or `autophase.revert` to undo the changes.
|
|
3903
|
+
*/
|
|
3904
|
+
async handleStop() {
|
|
3905
|
+
this.stopping = true;
|
|
3906
|
+
this.abort?.abort();
|
|
3907
|
+
this.orchestrator?.stop();
|
|
3908
|
+
this.stopBroadcast();
|
|
3909
|
+
if (this.graph) await this.store.save(this.graph).catch(() => void 0);
|
|
3910
|
+
this.broadcast({ type: "autophase.stopped", payload: { title: this.graph?.title } });
|
|
3911
|
+
}
|
|
3912
|
+
/**
|
|
3913
|
+
* Stop + wipe: tear down phase worktrees and reset to an empty board so the UI
|
|
3914
|
+
* returns to the start screen ("new one"). Does NOT touch already-merged commits
|
|
3915
|
+
* on the base branch — that is `autophase.revert`.
|
|
3916
|
+
*/
|
|
3917
|
+
async handleClear() {
|
|
3918
|
+
await this.handleStop();
|
|
3919
|
+
if (this.worktrees) await this.worktrees.cleanupAllManaged().catch(() => void 0);
|
|
3920
|
+
this.orchestrator = null;
|
|
3921
|
+
this.graph = null;
|
|
3922
|
+
this.runBase = null;
|
|
3923
|
+
this.usedNicknames.clear();
|
|
3924
|
+
this.broadcast({ type: "autophase.cleared", payload: {} });
|
|
3925
|
+
this.broadcast({ type: "autophase.state", payload: this.buildState() });
|
|
3926
|
+
}
|
|
3927
|
+
/**
|
|
3928
|
+
* Stop + undo: remove phase worktrees, then history-preservingly `git revert`
|
|
3929
|
+
* every commit this run landed on the base branch (captured `runBase`..HEAD),
|
|
3930
|
+
* then reset to an empty board. Refuses (reports a reason) on a dirty tree or a
|
|
3931
|
+
* conflicting revert rather than leaving the tree half-reverted.
|
|
3932
|
+
*/
|
|
3933
|
+
async handleRevert() {
|
|
3934
|
+
await this.handleStop();
|
|
3935
|
+
if (!this.worktrees || !this.runBase || !this.projectRoot) {
|
|
3936
|
+
this.broadcast({
|
|
3937
|
+
type: "autophase.reverted",
|
|
3938
|
+
payload: { ok: false, reverted: 0, reason: "no git baseline was captured for this run" }
|
|
3939
|
+
});
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
await this.worktrees.cleanupAllManaged().catch(() => void 0);
|
|
3943
|
+
const shas = commitsSince(this.projectRoot, this.runBase.sha, this.runBase.branch);
|
|
3944
|
+
const res = await this.worktrees.revertCommits(this.runBase.branch, shas);
|
|
3945
|
+
this.broadcast({ type: "autophase.reverted", payload: res });
|
|
3946
|
+
if (res.ok) {
|
|
3947
|
+
this.orchestrator = null;
|
|
3948
|
+
this.graph = null;
|
|
3949
|
+
this.runBase = null;
|
|
3950
|
+
this.broadcast({ type: "autophase.cleared", payload: {} });
|
|
3951
|
+
this.broadcast({ type: "autophase.state", payload: this.buildState() });
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3226
3954
|
/** Generic fallback phases when the LLM planner produces nothing usable. */
|
|
3227
3955
|
defaultPhases() {
|
|
3228
3956
|
return [
|
|
@@ -3233,13 +3961,18 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3233
3961
|
{ name: "Deployment", description: "Deploy to production", priority: "medium", estimateHours: 2, parallelizable: false }
|
|
3234
3962
|
];
|
|
3235
3963
|
}
|
|
3236
|
-
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
3237
|
-
|
|
3964
|
+
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
3965
|
+
* The caller passes the run's abort signal so a stop during planning cancels
|
|
3966
|
+
* the LLM turn (the previous fresh, never-aborted controller made planning
|
|
3967
|
+
* uninterruptible). */
|
|
3968
|
+
async planPhases(goal, signal) {
|
|
3238
3969
|
try {
|
|
3239
3970
|
const planner = new AutoPhasePlanner({
|
|
3240
3971
|
goal,
|
|
3241
3972
|
runOnce: async (prompt) => {
|
|
3242
|
-
const result = await this.agent.run(prompt, {
|
|
3973
|
+
const result = await this.agent.run(prompt, {
|
|
3974
|
+
signal: signal ?? new AbortController().signal
|
|
3975
|
+
});
|
|
3243
3976
|
return result.status === "done" ? result.finalText ?? "" : "";
|
|
3244
3977
|
}
|
|
3245
3978
|
});
|
|
@@ -3374,6 +4107,10 @@ Type: ${task.type}`;
|
|
|
3374
4107
|
const lastError = lastFailed ? `${lastFailed.name}: ${lastFailed.metadata?.integrationError ?? "phase failed"}` : null;
|
|
3375
4108
|
return {
|
|
3376
4109
|
title: this.graph.title,
|
|
4110
|
+
// Full operator prompt, shown verbatim in a dedicated goal block (the
|
|
4111
|
+
// title is only a short derived heading). Fall back to the title for
|
|
4112
|
+
// legacy boards saved before the title/goal split.
|
|
4113
|
+
goal: this.graph.description || this.graph.title,
|
|
3377
4114
|
phases: phaseItems,
|
|
3378
4115
|
tasks: taskItems,
|
|
3379
4116
|
activePhaseId: currentActiveId,
|
|
@@ -3575,7 +4312,7 @@ var SpecsWebSocketHandler = class {
|
|
|
3575
4312
|
};
|
|
3576
4313
|
|
|
3577
4314
|
// src/server/sdd-board-ws-handler.ts
|
|
3578
|
-
import { SddBoardStore } from "@wrongstack/core";
|
|
4315
|
+
import { applySddLifecycle, SddBoardStore } from "@wrongstack/core";
|
|
3579
4316
|
var CONTROL_TYPES = /* @__PURE__ */ new Set([
|
|
3580
4317
|
"pause",
|
|
3581
4318
|
"resume",
|
|
@@ -3589,19 +4326,19 @@ var CONTROL_TYPES = /* @__PURE__ */ new Set([
|
|
|
3589
4326
|
"set_task_verification",
|
|
3590
4327
|
"cancel_task",
|
|
3591
4328
|
"delete_task",
|
|
3592
|
-
"split_task"
|
|
3593
|
-
// Lifecycle (pair with a prior `stop`): sweep worktrees / revert merged commits.
|
|
3594
|
-
"cleanup_worktrees",
|
|
3595
|
-
"rollback"
|
|
4329
|
+
"split_task"
|
|
3596
4330
|
]);
|
|
4331
|
+
var LIFECYCLE_TYPES = /* @__PURE__ */ new Set(["cleanup_worktrees", "rollback", "destroy"]);
|
|
3597
4332
|
var SddBoardWebSocketHandler = class {
|
|
3598
4333
|
store;
|
|
3599
4334
|
clients = /* @__PURE__ */ new Set();
|
|
4335
|
+
lifecycle;
|
|
3600
4336
|
latest = null;
|
|
3601
4337
|
poll = null;
|
|
3602
4338
|
unsub = null;
|
|
3603
|
-
constructor(boardsDir, events) {
|
|
4339
|
+
constructor(boardsDir, events, lifecycle) {
|
|
3604
4340
|
this.store = new SddBoardStore({ baseDir: boardsDir });
|
|
4341
|
+
this.lifecycle = lifecycle;
|
|
3605
4342
|
if (events) {
|
|
3606
4343
|
const handler = (e) => {
|
|
3607
4344
|
this.latest = e.snapshot;
|
|
@@ -3630,6 +4367,10 @@ var SddBoardWebSocketHandler = class {
|
|
|
3630
4367
|
return;
|
|
3631
4368
|
}
|
|
3632
4369
|
const action = msg.type.replace(/^sdd\.board\./, "");
|
|
4370
|
+
if (LIFECYCLE_TYPES.has(action)) {
|
|
4371
|
+
await this.applyLifecycle(action, msg.payload);
|
|
4372
|
+
return;
|
|
4373
|
+
}
|
|
3633
4374
|
if (CONTROL_TYPES.has(action)) {
|
|
3634
4375
|
const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
|
|
3635
4376
|
if (runId) {
|
|
@@ -3641,6 +4382,40 @@ var SddBoardWebSocketHandler = class {
|
|
|
3641
4382
|
}
|
|
3642
4383
|
}
|
|
3643
4384
|
}
|
|
4385
|
+
/**
|
|
4386
|
+
* Apply a cleanup/rollback/destroy from disk and broadcast a structured
|
|
4387
|
+
* `sdd.board.lifecycle_result`. Refuses (no-op) while a run is still active —
|
|
4388
|
+
* the user must stop it first; the UI gates the buttons on `!active` and the
|
|
4389
|
+
* Destroy flow auto-stops then waits before sending `destroy`.
|
|
4390
|
+
*/
|
|
4391
|
+
async applyLifecycle(op, payload) {
|
|
4392
|
+
if (!this.lifecycle) {
|
|
4393
|
+
this.broadcast({
|
|
4394
|
+
type: "sdd.board.lifecycle_result",
|
|
4395
|
+
payload: { op, ok: false, reason: "Lifecycle operations are not available in this session." }
|
|
4396
|
+
});
|
|
4397
|
+
return;
|
|
4398
|
+
}
|
|
4399
|
+
if (this.latest && (this.latest.status === "running" || this.latest.status === "paused")) {
|
|
4400
|
+
this.broadcast({
|
|
4401
|
+
type: "sdd.board.lifecycle_result",
|
|
4402
|
+
payload: { op, ok: false, reason: "Stop the run first, then retry." }
|
|
4403
|
+
});
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4406
|
+
const runId = payload?.runId ?? this.latest?.runId;
|
|
4407
|
+
const result = await applySddLifecycle(op, {
|
|
4408
|
+
projectRoot: this.lifecycle.projectRoot,
|
|
4409
|
+
paths: this.lifecycle.paths,
|
|
4410
|
+
runId,
|
|
4411
|
+
revertMerged: payload?.revertMerged === true
|
|
4412
|
+
});
|
|
4413
|
+
this.broadcast({ type: "sdd.board.lifecycle_result", payload: result });
|
|
4414
|
+
if (op === "destroy" && result.ok) {
|
|
4415
|
+
this.latest = null;
|
|
4416
|
+
this.broadcast({ type: "sdd.board.snapshot", payload: null });
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
3644
4419
|
dispose() {
|
|
3645
4420
|
if (this.poll) clearInterval(this.poll);
|
|
3646
4421
|
this.unsub?.();
|
|
@@ -3684,6 +4459,12 @@ var SddBoardWebSocketHandler = class {
|
|
|
3684
4459
|
};
|
|
3685
4460
|
|
|
3686
4461
|
// src/server/sdd-wizard-ws-handler.ts
|
|
4462
|
+
function deriveTitle2(goal) {
|
|
4463
|
+
const firstLine = goal.split("\n").map((l) => l.trim()).find(Boolean);
|
|
4464
|
+
if (!firstLine) return "New SDD Project";
|
|
4465
|
+
const sentence = firstLine.split(/(?<=[.!?])\s/)[0] ?? firstLine;
|
|
4466
|
+
return sentence.length <= 64 ? sentence : `${sentence.slice(0, 63).trimEnd()}\u2026`;
|
|
4467
|
+
}
|
|
3687
4468
|
var SddWizardWebSocketHandler = class {
|
|
3688
4469
|
constructor(deps2) {
|
|
3689
4470
|
this.deps = deps2;
|
|
@@ -3722,7 +4503,8 @@ var SddWizardWebSocketHandler = class {
|
|
|
3722
4503
|
parallelSlots: msg.payload?.parallelSlots,
|
|
3723
4504
|
defaultModel: msg.payload?.model,
|
|
3724
4505
|
defaultProvider: msg.payload?.provider,
|
|
3725
|
-
fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0
|
|
4506
|
+
fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0,
|
|
4507
|
+
worktrees: typeof msg.payload?.worktrees === "boolean" ? msg.payload.worktrees : void 0
|
|
3726
4508
|
});
|
|
3727
4509
|
break;
|
|
3728
4510
|
}
|
|
@@ -3742,7 +4524,7 @@ var SddWizardWebSocketHandler = class {
|
|
|
3742
4524
|
}
|
|
3743
4525
|
if (this.busy) return;
|
|
3744
4526
|
this.driver = this.deps.makeDriver();
|
|
3745
|
-
const prompt = this.driver.start(goal);
|
|
4527
|
+
const prompt = this.driver.start(deriveTitle2(goal), goal);
|
|
3746
4528
|
await this.runTurn(prompt);
|
|
3747
4529
|
}
|
|
3748
4530
|
async onMessage(text) {
|
|
@@ -3813,9 +4595,10 @@ var SddWizardWebSocketHandler = class {
|
|
|
3813
4595
|
};
|
|
3814
4596
|
|
|
3815
4597
|
// src/server/sdd-wizard-wiring.ts
|
|
3816
|
-
import * as
|
|
4598
|
+
import * as path8 from "path";
|
|
3817
4599
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3818
4600
|
import {
|
|
4601
|
+
cleanupStaleSddWorktrees,
|
|
3819
4602
|
makeCommandVerifier,
|
|
3820
4603
|
makeLlmSubtaskGenerator,
|
|
3821
4604
|
SddBoardStore as SddBoardStore2,
|
|
@@ -3827,6 +4610,7 @@ import {
|
|
|
3827
4610
|
TaskGraphStore as TaskGraphStore2,
|
|
3828
4611
|
WorktreeManager as WorktreeManager2
|
|
3829
4612
|
} from "@wrongstack/core";
|
|
4613
|
+
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";
|
|
3830
4614
|
function buildSddWizardDeps(opts) {
|
|
3831
4615
|
const registry = new SddRunRegistry();
|
|
3832
4616
|
let isolatedSeq = 0;
|
|
@@ -3835,11 +4619,11 @@ function buildSddWizardDeps(opts) {
|
|
|
3835
4619
|
id: `sdd-${name2.toLowerCase().replace(/\s+/g, "-")}-${isolatedSeq++}`,
|
|
3836
4620
|
role: "executor",
|
|
3837
4621
|
name: name2,
|
|
3838
|
-
disabledTools: ["delegate"],
|
|
4622
|
+
disabledTools: ["delegate", "write", "edit", "patch", "bash", "exec"],
|
|
3839
4623
|
allowedCapabilities: ["fs.read", "net.outbound"]
|
|
3840
4624
|
});
|
|
3841
4625
|
try {
|
|
3842
|
-
const res = await result.agent.run([{ type: "text", text: prompt }]);
|
|
4626
|
+
const res = await result.agent.run([{ type: "text", text: PLANNING_ONLY_GUARD + prompt }]);
|
|
3843
4627
|
return res.finalText ?? "";
|
|
3844
4628
|
} finally {
|
|
3845
4629
|
await result.dispose?.();
|
|
@@ -3849,23 +4633,30 @@ function buildSddWizardDeps(opts) {
|
|
|
3849
4633
|
makeDriver: () => new SddInterviewDriver({
|
|
3850
4634
|
specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
|
|
3851
4635
|
graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
|
|
3852
|
-
sessionPath:
|
|
4636
|
+
sessionPath: path8.join(opts.paths.projectDir, "sdd-wizard-session.json")
|
|
3853
4637
|
}),
|
|
3854
4638
|
runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
|
|
3855
|
-
startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
|
|
4639
|
+
startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels, worktrees: useWorktrees }) => {
|
|
3856
4640
|
const graph = driver.getGraph();
|
|
3857
4641
|
const tracker = driver.getTracker();
|
|
3858
4642
|
if (!graph || !tracker) {
|
|
3859
4643
|
throw new Error("No task graph to run \u2014 finish the interview first.");
|
|
3860
4644
|
}
|
|
4645
|
+
const worktreesEnabled = useWorktrees ?? process.env["WRONGSTACK_SDD_WORKTREES"] !== "0";
|
|
3861
4646
|
let worktrees;
|
|
3862
|
-
if (
|
|
4647
|
+
if (worktreesEnabled) {
|
|
3863
4648
|
const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3864
4649
|
cwd: opts.projectRoot,
|
|
3865
4650
|
encoding: "utf8",
|
|
3866
4651
|
windowsHide: true
|
|
3867
4652
|
}).stdout?.trim() === "true";
|
|
3868
|
-
if (inGit)
|
|
4653
|
+
if (inGit) {
|
|
4654
|
+
await cleanupStaleSddWorktrees({
|
|
4655
|
+
projectRoot: opts.projectRoot,
|
|
4656
|
+
boardsDir: opts.paths.projectSddBoards
|
|
4657
|
+
}).catch(() => void 0);
|
|
4658
|
+
worktrees = new WorktreeManager2({ projectRoot: opts.projectRoot, events: opts.events });
|
|
4659
|
+
}
|
|
3869
4660
|
}
|
|
3870
4661
|
const boardStore = new SddBoardStore2({ baseDir: opts.paths.projectSddBoards });
|
|
3871
4662
|
const verifyTask = makeCommandVerifier();
|
|
@@ -4644,16 +5435,16 @@ var CollaborationWebSocketHandler = class {
|
|
|
4644
5435
|
};
|
|
4645
5436
|
|
|
4646
5437
|
// src/server/projects-manifest.ts
|
|
4647
|
-
import * as
|
|
4648
|
-
import * as
|
|
5438
|
+
import * as fs7 from "fs/promises";
|
|
5439
|
+
import * as path9 from "path";
|
|
4649
5440
|
import { projectSlug } from "@wrongstack/core";
|
|
4650
5441
|
function projectsJsonPath(globalConfigPath) {
|
|
4651
|
-
const base =
|
|
4652
|
-
return
|
|
5442
|
+
const base = path9.dirname(globalConfigPath);
|
|
5443
|
+
return path9.join(base, "projects.json");
|
|
4653
5444
|
}
|
|
4654
5445
|
async function loadManifest(globalConfigPath) {
|
|
4655
5446
|
try {
|
|
4656
|
-
const raw = await
|
|
5447
|
+
const raw = await fs7.readFile(projectsJsonPath(globalConfigPath), "utf8");
|
|
4657
5448
|
const parsed = JSON.parse(raw);
|
|
4658
5449
|
return { projects: parsed.projects ?? [] };
|
|
4659
5450
|
} catch {
|
|
@@ -4662,16 +5453,16 @@ async function loadManifest(globalConfigPath) {
|
|
|
4662
5453
|
}
|
|
4663
5454
|
async function saveManifest(manifest, globalConfigPath) {
|
|
4664
5455
|
const file = projectsJsonPath(globalConfigPath);
|
|
4665
|
-
await
|
|
4666
|
-
await
|
|
5456
|
+
await fs7.mkdir(path9.dirname(file), { recursive: true });
|
|
5457
|
+
await fs7.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
4667
5458
|
}
|
|
4668
5459
|
function generateProjectSlug(rootPath) {
|
|
4669
5460
|
return projectSlug(rootPath);
|
|
4670
5461
|
}
|
|
4671
5462
|
async function ensureProjectDataDir(slug, globalConfigPath) {
|
|
4672
|
-
const base =
|
|
4673
|
-
const dir =
|
|
4674
|
-
await
|
|
5463
|
+
const base = path9.dirname(globalConfigPath);
|
|
5464
|
+
const dir = path9.join(base, "projects", slug);
|
|
5465
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
4675
5466
|
return dir;
|
|
4676
5467
|
}
|
|
4677
5468
|
|
|
@@ -4833,16 +5624,22 @@ function clampDim(value, fallback) {
|
|
|
4833
5624
|
}
|
|
4834
5625
|
|
|
4835
5626
|
// src/server/worktree-ws-handler.ts
|
|
5627
|
+
import { join as join6, resolve as resolve6, sep as sep4 } from "path";
|
|
5628
|
+
import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2, WorktreeManager as WorktreeManager3 } from "@wrongstack/core";
|
|
4836
5629
|
import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
|
|
4837
5630
|
var MAX_ACTIVITY = 6;
|
|
5631
|
+
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["allocating", "active", "committing", "merging"]);
|
|
5632
|
+
var MANAGED_BRANCH_RE = /^wstack\/ap\/[A-Za-z0-9._/-]+$/;
|
|
4838
5633
|
var WorktreeWebSocketHandler = class {
|
|
4839
|
-
constructor(events, logger) {
|
|
5634
|
+
constructor(events, logger, management) {
|
|
4840
5635
|
this.events = events;
|
|
4841
5636
|
this.logger = logger;
|
|
5637
|
+
this.management = management;
|
|
4842
5638
|
this.subscribe();
|
|
4843
5639
|
}
|
|
4844
5640
|
events;
|
|
4845
5641
|
logger;
|
|
5642
|
+
management;
|
|
4846
5643
|
clients = /* @__PURE__ */ new Set();
|
|
4847
5644
|
handles = /* @__PURE__ */ new Map();
|
|
4848
5645
|
baseBranch = "";
|
|
@@ -4853,12 +5650,197 @@ var WorktreeWebSocketHandler = class {
|
|
|
4853
5650
|
ws.on("close", () => this.clients.delete(ws));
|
|
4854
5651
|
ws.on("error", () => this.clients.delete(ws));
|
|
4855
5652
|
this.send(ws, this.stateMessage());
|
|
5653
|
+
void this.scanAndBroadcast();
|
|
5654
|
+
}
|
|
5655
|
+
/** Handle worktree-panel control messages (scan / clean / per-row ops). */
|
|
5656
|
+
async handleMessage(msg) {
|
|
5657
|
+
if (msg.type === "worktree.scan") {
|
|
5658
|
+
await this.scanAndBroadcast();
|
|
5659
|
+
return true;
|
|
5660
|
+
}
|
|
5661
|
+
if (msg.type === "worktree.cleanup") {
|
|
5662
|
+
await this.cleanupOrphans();
|
|
5663
|
+
return true;
|
|
5664
|
+
}
|
|
5665
|
+
if (msg.type === "worktree.remove") {
|
|
5666
|
+
await this.removeOne(msg.payload?.["dir"], msg.payload?.["branch"]);
|
|
5667
|
+
return true;
|
|
5668
|
+
}
|
|
5669
|
+
if (msg.type === "worktree.merge") {
|
|
5670
|
+
await this.mergeBranch(msg.payload?.["branch"]);
|
|
5671
|
+
return true;
|
|
5672
|
+
}
|
|
5673
|
+
if (msg.type === "worktree.diff") {
|
|
5674
|
+
await this.diffOne(msg.payload?.["dir"], msg.payload?.["baseBranch"]);
|
|
5675
|
+
return true;
|
|
5676
|
+
}
|
|
5677
|
+
return false;
|
|
4856
5678
|
}
|
|
4857
5679
|
dispose() {
|
|
4858
5680
|
for (const off of this.offs) off();
|
|
4859
5681
|
this.offs.length = 0;
|
|
4860
5682
|
this.stopBroadcast();
|
|
4861
5683
|
}
|
|
5684
|
+
// ── orphan management ─────────────────────────────────────────────────────
|
|
5685
|
+
/** Absolute managed-worktrees root for this project. */
|
|
5686
|
+
worktreesRoot() {
|
|
5687
|
+
return resolve6(join6(this.management.projectRoot, ".wrongstack", "worktrees"));
|
|
5688
|
+
}
|
|
5689
|
+
/** True iff `dir` resolves strictly inside the managed worktrees root. */
|
|
5690
|
+
underRoot(dir) {
|
|
5691
|
+
const abs = resolve6(dir);
|
|
5692
|
+
const root = this.worktreesRoot();
|
|
5693
|
+
return abs !== root && abs.startsWith(root + sep4);
|
|
5694
|
+
}
|
|
5695
|
+
/** Branches of worktrees a live in-session run currently owns. */
|
|
5696
|
+
liveActiveBranches() {
|
|
5697
|
+
const live = /* @__PURE__ */ new Set();
|
|
5698
|
+
for (const h of this.handles.values()) {
|
|
5699
|
+
if (ACTIVE_STATUSES.has(h.status) && h.branch) live.add(h.branch);
|
|
5700
|
+
}
|
|
5701
|
+
return live;
|
|
5702
|
+
}
|
|
5703
|
+
/**
|
|
5704
|
+
* Scan the disk for managed worktrees/branches NOT owned by a live in-session
|
|
5705
|
+
* run and broadcast them as orphans, with whether it is safe to clean now.
|
|
5706
|
+
* No-op (empty inventory) when management deps were not wired.
|
|
5707
|
+
*/
|
|
5708
|
+
async scanAndBroadcast() {
|
|
5709
|
+
if (!this.management) {
|
|
5710
|
+
this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
|
|
5711
|
+
return;
|
|
5712
|
+
}
|
|
5713
|
+
try {
|
|
5714
|
+
const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
|
|
5715
|
+
const { worktrees, branches } = await wt.listManaged();
|
|
5716
|
+
const live = this.liveActiveBranches();
|
|
5717
|
+
const orphans = [];
|
|
5718
|
+
const seenBranches = /* @__PURE__ */ new Set();
|
|
5719
|
+
for (const w of worktrees) {
|
|
5720
|
+
if (w.branch && live.has(w.branch)) continue;
|
|
5721
|
+
if (w.branch) seenBranches.add(w.branch);
|
|
5722
|
+
orphans.push({ kind: "worktree", dir: w.dir, branch: w.branch });
|
|
5723
|
+
}
|
|
5724
|
+
for (const b of branches) {
|
|
5725
|
+
if (live.has(b) || seenBranches.has(b)) continue;
|
|
5726
|
+
orphans.push({ kind: "branch", branch: b });
|
|
5727
|
+
}
|
|
5728
|
+
const canClean = this.liveActiveBranches().size === 0;
|
|
5729
|
+
this.broadcast({
|
|
5730
|
+
type: "worktree.orphans",
|
|
5731
|
+
payload: {
|
|
5732
|
+
orphans,
|
|
5733
|
+
canClean,
|
|
5734
|
+
reason: canClean ? void 0 : "a run is live in this session"
|
|
5735
|
+
}
|
|
5736
|
+
});
|
|
5737
|
+
} catch (err) {
|
|
5738
|
+
this.logger.debug?.(`worktree orphan scan failed: ${toErrorMessage4(err)}`);
|
|
5739
|
+
this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
|
|
5740
|
+
}
|
|
5741
|
+
}
|
|
5742
|
+
/**
|
|
5743
|
+
* Force-remove every orphaned worktree + branch. Refused while a run is live —
|
|
5744
|
+
* in this session (active handles) OR another process (the SDD board liveness
|
|
5745
|
+
* guard inside cleanupStaleSddWorktrees). Best-effort; reports the outcome.
|
|
5746
|
+
*/
|
|
5747
|
+
async cleanupOrphans() {
|
|
5748
|
+
if (!this.management) {
|
|
5749
|
+
this.broadcast({
|
|
5750
|
+
type: "worktree.cleanup_result",
|
|
5751
|
+
payload: { ok: false, removed: 0, reason: "cleanup is not available in this session" }
|
|
5752
|
+
});
|
|
5753
|
+
return;
|
|
5754
|
+
}
|
|
5755
|
+
if (this.liveActiveBranches().size > 0) {
|
|
5756
|
+
this.broadcast({
|
|
5757
|
+
type: "worktree.cleanup_result",
|
|
5758
|
+
payload: { ok: false, removed: 0, reason: "a run is live in this session \u2014 stop it first" }
|
|
5759
|
+
});
|
|
5760
|
+
return;
|
|
5761
|
+
}
|
|
5762
|
+
const res = await cleanupStaleSddWorktrees2({
|
|
5763
|
+
projectRoot: this.management.projectRoot,
|
|
5764
|
+
boardsDir: this.management.boardsDir
|
|
5765
|
+
});
|
|
5766
|
+
if (res.skippedReason) {
|
|
5767
|
+
this.broadcast({
|
|
5768
|
+
type: "worktree.cleanup_result",
|
|
5769
|
+
payload: { ok: false, removed: 0, reason: res.skippedReason }
|
|
5770
|
+
});
|
|
5771
|
+
await this.scanAndBroadcast();
|
|
5772
|
+
return;
|
|
5773
|
+
}
|
|
5774
|
+
for (const [id, h] of [...this.handles]) {
|
|
5775
|
+
if (!ACTIVE_STATUSES.has(h.status)) this.handles.delete(id);
|
|
5776
|
+
}
|
|
5777
|
+
this.broadcast({ type: "worktree.cleanup_result", payload: { ok: true, removed: res.removed } });
|
|
5778
|
+
this.broadcastState();
|
|
5779
|
+
await this.scanAndBroadcast();
|
|
5780
|
+
}
|
|
5781
|
+
/** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
|
|
5782
|
+
async removeOne(dir, branch) {
|
|
5783
|
+
if (!this.management || !dir && !branch) {
|
|
5784
|
+
this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "nothing to remove" } });
|
|
5785
|
+
return;
|
|
5786
|
+
}
|
|
5787
|
+
if (branch && !MANAGED_BRANCH_RE.test(branch)) {
|
|
5788
|
+
this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "not a managed worktree branch" } });
|
|
5789
|
+
return;
|
|
5790
|
+
}
|
|
5791
|
+
if (dir && !this.underRoot(dir)) {
|
|
5792
|
+
this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "path is outside the managed worktrees root" } });
|
|
5793
|
+
return;
|
|
5794
|
+
}
|
|
5795
|
+
if (branch && this.liveActiveBranches().has(branch)) {
|
|
5796
|
+
this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "a run is live on this worktree \u2014 stop it first" } });
|
|
5797
|
+
return;
|
|
5798
|
+
}
|
|
5799
|
+
let removed = false;
|
|
5800
|
+
if (dir) {
|
|
5801
|
+
const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
|
|
5802
|
+
({ removed } = await wt.removeOne(dir, branch));
|
|
5803
|
+
}
|
|
5804
|
+
for (const [id, h] of [...this.handles]) {
|
|
5805
|
+
if (branch && h.branch === branch || dir && h.handleId && dir.endsWith(h.handleId)) this.handles.delete(id);
|
|
5806
|
+
}
|
|
5807
|
+
this.broadcast({ type: "worktree.cleanup_result", payload: { ok: removed, removed: removed ? 1 : 0, reason: removed ? void 0 : "remove failed (not a managed worktree?)" } });
|
|
5808
|
+
this.broadcastState();
|
|
5809
|
+
await this.scanAndBroadcast();
|
|
5810
|
+
}
|
|
5811
|
+
/** Squash-merge ONE branch into base. Refused while a live run owns it. */
|
|
5812
|
+
async mergeBranch(branch) {
|
|
5813
|
+
if (!this.management || !branch) {
|
|
5814
|
+
this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch: branch ?? "", reason: "no branch" } });
|
|
5815
|
+
return;
|
|
5816
|
+
}
|
|
5817
|
+
if (!MANAGED_BRANCH_RE.test(branch)) {
|
|
5818
|
+
this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "not a managed worktree branch" } });
|
|
5819
|
+
return;
|
|
5820
|
+
}
|
|
5821
|
+
if (this.liveActiveBranches().has(branch)) {
|
|
5822
|
+
this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "a run is live on this worktree \u2014 stop it first" } });
|
|
5823
|
+
return;
|
|
5824
|
+
}
|
|
5825
|
+
const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
|
|
5826
|
+
const res = await wt.mergeBranch(branch);
|
|
5827
|
+
this.broadcast({
|
|
5828
|
+
type: "worktree.merge_result",
|
|
5829
|
+
payload: { ok: res.ok, branch, conflict: res.conflict, conflictFiles: res.conflictFiles, reason: res.reason }
|
|
5830
|
+
});
|
|
5831
|
+
await this.scanAndBroadcast();
|
|
5832
|
+
}
|
|
5833
|
+
/** Compact change summary for one worktree checkout. */
|
|
5834
|
+
async diffOne(dir, baseBranch) {
|
|
5835
|
+
if (!this.management || !dir || !this.underRoot(dir)) {
|
|
5836
|
+
this.broadcast({ type: "worktree.diff_result", payload: { dir: dir ?? "", summary: null } });
|
|
5837
|
+
return;
|
|
5838
|
+
}
|
|
5839
|
+
const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
|
|
5840
|
+
const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
|
|
5841
|
+
const summary = await wt.diffSummary(resolve6(dir), base);
|
|
5842
|
+
this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
|
|
5843
|
+
}
|
|
4862
5844
|
// ── internals ───────────────────────────────────────────────────────────
|
|
4863
5845
|
subscribe() {
|
|
4864
5846
|
const on = this.events.on.bind(this.events);
|
|
@@ -4870,6 +5852,7 @@ var WorktreeWebSocketHandler = class {
|
|
|
4870
5852
|
handleId: e.handleId,
|
|
4871
5853
|
ownerId: e.ownerId,
|
|
4872
5854
|
ownerLabel: e.ownerLabel,
|
|
5855
|
+
dir: e.dir,
|
|
4873
5856
|
branch: e.branch,
|
|
4874
5857
|
baseBranch: e.baseBranch,
|
|
4875
5858
|
status: "active",
|
|
@@ -4972,10 +5955,10 @@ var WorktreeWebSocketHandler = class {
|
|
|
4972
5955
|
};
|
|
4973
5956
|
|
|
4974
5957
|
// src/server/mailbox-handlers.ts
|
|
4975
|
-
import { GlobalMailbox, resolveProjectDir } from "@wrongstack/core";
|
|
5958
|
+
import { GlobalMailbox, resolveProjectDir as resolveProjectDir2 } from "@wrongstack/core";
|
|
4976
5959
|
async function handleMailboxMessages(ws, deps2, payload) {
|
|
4977
5960
|
try {
|
|
4978
|
-
const dir =
|
|
5961
|
+
const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
|
|
4979
5962
|
const mb = new GlobalMailbox(dir);
|
|
4980
5963
|
const messages = await mb.query({
|
|
4981
5964
|
limit: payload?.limit ?? 30,
|
|
@@ -5011,7 +5994,7 @@ async function handleMailboxMessages(ws, deps2, payload) {
|
|
|
5011
5994
|
}
|
|
5012
5995
|
async function handleMailboxAgents(ws, deps2, payload) {
|
|
5013
5996
|
try {
|
|
5014
|
-
const dir =
|
|
5997
|
+
const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
|
|
5015
5998
|
const mb = new GlobalMailbox(dir);
|
|
5016
5999
|
const agents = payload?.onlineOnly ? await mb.getOnlineAgents() : await mb.getAgentStatuses();
|
|
5017
6000
|
send(ws, {
|
|
@@ -5040,7 +6023,7 @@ async function handleMailboxAgents(ws, deps2, payload) {
|
|
|
5040
6023
|
}
|
|
5041
6024
|
async function handleMailboxClear(ws, deps2) {
|
|
5042
6025
|
try {
|
|
5043
|
-
const dir =
|
|
6026
|
+
const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
|
|
5044
6027
|
const mb = new GlobalMailbox(dir);
|
|
5045
6028
|
await mb.clearAll();
|
|
5046
6029
|
send(ws, { type: "mailbox.cleared", payload: {} });
|
|
@@ -5050,7 +6033,7 @@ async function handleMailboxClear(ws, deps2) {
|
|
|
5050
6033
|
}
|
|
5051
6034
|
async function handleMailboxPurge(ws, deps2, opts) {
|
|
5052
6035
|
try {
|
|
5053
|
-
const dir =
|
|
6036
|
+
const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
|
|
5054
6037
|
const mb = new GlobalMailbox(dir);
|
|
5055
6038
|
const result = await mb.purgeStale(opts);
|
|
5056
6039
|
send(ws, { type: "mailbox.purged", payload: result });
|
|
@@ -5097,14 +6080,14 @@ function registerShutdownHandlers(res) {
|
|
|
5097
6080
|
|
|
5098
6081
|
// src/server/instance-registry.ts
|
|
5099
6082
|
import * as os from "os";
|
|
5100
|
-
import * as
|
|
5101
|
-
import * as
|
|
6083
|
+
import * as path10 from "path";
|
|
6084
|
+
import * as fs8 from "fs/promises";
|
|
5102
6085
|
import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
|
|
5103
6086
|
function defaultBaseDir() {
|
|
5104
|
-
return
|
|
6087
|
+
return path10.join(os.homedir(), ".wrongstack");
|
|
5105
6088
|
}
|
|
5106
6089
|
function registryPath(baseDir = defaultBaseDir()) {
|
|
5107
|
-
return
|
|
6090
|
+
return path10.join(baseDir, "webui-instances.json");
|
|
5108
6091
|
}
|
|
5109
6092
|
function isPidAlive(pid) {
|
|
5110
6093
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -5117,7 +6100,7 @@ function isPidAlive(pid) {
|
|
|
5117
6100
|
}
|
|
5118
6101
|
async function load(file) {
|
|
5119
6102
|
try {
|
|
5120
|
-
const raw = await
|
|
6103
|
+
const raw = await fs8.readFile(file, "utf8");
|
|
5121
6104
|
const parsed = JSON.parse(raw);
|
|
5122
6105
|
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
5123
6106
|
return parsed;
|
|
@@ -5176,16 +6159,16 @@ function formatInstances(instances) {
|
|
|
5176
6159
|
// src/server/port-utils.ts
|
|
5177
6160
|
import * as net from "net";
|
|
5178
6161
|
function isPortFree(host, port) {
|
|
5179
|
-
return new Promise((
|
|
6162
|
+
return new Promise((resolve10) => {
|
|
5180
6163
|
const srv = net.createServer();
|
|
5181
|
-
srv.once("error", () =>
|
|
6164
|
+
srv.once("error", () => resolve10(false));
|
|
5182
6165
|
srv.once("listening", () => {
|
|
5183
|
-
srv.close(() =>
|
|
6166
|
+
srv.close(() => resolve10(true));
|
|
5184
6167
|
});
|
|
5185
6168
|
try {
|
|
5186
6169
|
srv.listen(port, host);
|
|
5187
6170
|
} catch {
|
|
5188
|
-
|
|
6171
|
+
resolve10(false);
|
|
5189
6172
|
}
|
|
5190
6173
|
});
|
|
5191
6174
|
}
|
|
@@ -5266,15 +6249,15 @@ import { DefaultSecretScrubber } from "@wrongstack/core";
|
|
|
5266
6249
|
import { probeLocalLlm } from "@wrongstack/runtime/probe";
|
|
5267
6250
|
|
|
5268
6251
|
// src/server/provider-config-io.ts
|
|
5269
|
-
import * as
|
|
5270
|
-
import * as
|
|
6252
|
+
import * as fs9 from "fs/promises";
|
|
6253
|
+
import * as path11 from "path";
|
|
5271
6254
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
5272
6255
|
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
5273
6256
|
import { DefaultSecretVault } from "@wrongstack/core";
|
|
5274
6257
|
async function loadSavedProviders(configPath, vault) {
|
|
5275
6258
|
let raw;
|
|
5276
6259
|
try {
|
|
5277
|
-
raw = await
|
|
6260
|
+
raw = await fs9.readFile(configPath, "utf8");
|
|
5278
6261
|
} catch {
|
|
5279
6262
|
return {};
|
|
5280
6263
|
}
|
|
@@ -5291,7 +6274,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
5291
6274
|
let raw;
|
|
5292
6275
|
let fileExists = true;
|
|
5293
6276
|
try {
|
|
5294
|
-
raw = await
|
|
6277
|
+
raw = await fs9.readFile(configPath, "utf8");
|
|
5295
6278
|
} catch (err) {
|
|
5296
6279
|
if (err.code !== "ENOENT") {
|
|
5297
6280
|
throw new Error(
|
|
@@ -5629,7 +6612,8 @@ function createProviderHandlers(deps2) {
|
|
|
5629
6612
|
|
|
5630
6613
|
// src/server/mode-handlers.ts
|
|
5631
6614
|
import {
|
|
5632
|
-
DefaultSystemPromptBuilder
|
|
6615
|
+
DefaultSystemPromptBuilder,
|
|
6616
|
+
resolveWstackPaths
|
|
5633
6617
|
} from "@wrongstack/core";
|
|
5634
6618
|
function createModeHandlers(ctx) {
|
|
5635
6619
|
return {
|
|
@@ -5677,13 +6661,18 @@ function createModeHandlers(ctx) {
|
|
|
5677
6661
|
}
|
|
5678
6662
|
ctx.setModeId(id);
|
|
5679
6663
|
const modePrompt = id === "default" ? "" : (await ctx.modeStore.getMode(id))?.prompt ?? "";
|
|
6664
|
+
const paths = resolveWstackPaths({ projectRoot: ctx.projectRoot, globalRoot: ctx.globalRoot });
|
|
5680
6665
|
const freshBuilder = new DefaultSystemPromptBuilder({
|
|
5681
6666
|
memoryStore: ctx.memoryStore,
|
|
5682
6667
|
skillLoader: ctx.skillLoader,
|
|
5683
6668
|
modeStore: ctx.modeStore,
|
|
5684
6669
|
modeId: id,
|
|
5685
6670
|
modePrompt,
|
|
5686
|
-
modelCapabilities: ctx.modelCapabilities
|
|
6671
|
+
modelCapabilities: ctx.modelCapabilities,
|
|
6672
|
+
instructionPaths: {
|
|
6673
|
+
globalDir: paths.globalInstructions,
|
|
6674
|
+
projectDir: paths.inProjectInstructions
|
|
6675
|
+
}
|
|
5687
6676
|
});
|
|
5688
6677
|
ctx.context.systemPrompt = await freshBuilder.build({
|
|
5689
6678
|
cwd: ctx.projectRoot,
|
|
@@ -5705,12 +6694,13 @@ function createModeHandlers(ctx) {
|
|
|
5705
6694
|
}
|
|
5706
6695
|
|
|
5707
6696
|
// src/server/project-handlers.ts
|
|
5708
|
-
import * as
|
|
5709
|
-
import * as
|
|
6697
|
+
import * as fs10 from "fs/promises";
|
|
6698
|
+
import * as path12 from "path";
|
|
5710
6699
|
import {
|
|
5711
6700
|
DefaultSessionStore,
|
|
5712
6701
|
DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2,
|
|
5713
|
-
getSessionRegistry
|
|
6702
|
+
getSessionRegistry,
|
|
6703
|
+
resolveWstackPaths as resolveWstackPaths2
|
|
5714
6704
|
} from "@wrongstack/core";
|
|
5715
6705
|
function createProjectHandlers(ctx) {
|
|
5716
6706
|
return {
|
|
@@ -5733,9 +6723,9 @@ function createProjectHandlers(ctx) {
|
|
|
5733
6723
|
}
|
|
5734
6724
|
const { root: addRoot, name: displayName } = parsed.value;
|
|
5735
6725
|
try {
|
|
5736
|
-
const resolved =
|
|
5737
|
-
await
|
|
5738
|
-
const stat3 = await
|
|
6726
|
+
const resolved = path12.resolve(addRoot);
|
|
6727
|
+
await fs10.access(resolved);
|
|
6728
|
+
const stat3 = await fs10.stat(resolved);
|
|
5739
6729
|
if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
5740
6730
|
const manifest = await loadManifest(ctx.globalConfigPath);
|
|
5741
6731
|
const existing = manifest.projects.find((p) => p.root === resolved);
|
|
@@ -5751,7 +6741,7 @@ function createProjectHandlers(ctx) {
|
|
|
5751
6741
|
});
|
|
5752
6742
|
return;
|
|
5753
6743
|
}
|
|
5754
|
-
const name2 = displayName?.trim() ||
|
|
6744
|
+
const name2 = displayName?.trim() || path12.basename(resolved);
|
|
5755
6745
|
const slug = generateProjectSlug(resolved);
|
|
5756
6746
|
await ensureProjectDataDir(slug, ctx.globalConfigPath);
|
|
5757
6747
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5764,7 +6754,7 @@ function createProjectHandlers(ctx) {
|
|
|
5764
6754
|
} catch (err) {
|
|
5765
6755
|
send(ws, {
|
|
5766
6756
|
type: "projects.added",
|
|
5767
|
-
payload: { name:
|
|
6757
|
+
payload: { name: path12.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
|
|
5768
6758
|
});
|
|
5769
6759
|
}
|
|
5770
6760
|
},
|
|
@@ -5779,17 +6769,17 @@ function createProjectHandlers(ctx) {
|
|
|
5779
6769
|
}
|
|
5780
6770
|
const { root: selRoot, name: selName } = parsed.value;
|
|
5781
6771
|
try {
|
|
5782
|
-
const resolved =
|
|
6772
|
+
const resolved = path12.resolve(selRoot);
|
|
5783
6773
|
try {
|
|
5784
|
-
await
|
|
5785
|
-
const stat3 = await
|
|
6774
|
+
await fs10.access(resolved);
|
|
6775
|
+
const stat3 = await fs10.stat(resolved);
|
|
5786
6776
|
if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
5787
6777
|
} catch (err) {
|
|
5788
6778
|
send(ws, {
|
|
5789
6779
|
type: "projects.selected",
|
|
5790
6780
|
payload: {
|
|
5791
6781
|
root: selRoot,
|
|
5792
|
-
name: selName ||
|
|
6782
|
+
name: selName || path12.basename(selRoot),
|
|
5793
6783
|
message: `Cannot switch: ${errMessage(err)}`
|
|
5794
6784
|
}
|
|
5795
6785
|
});
|
|
@@ -5801,7 +6791,7 @@ function createProjectHandlers(ctx) {
|
|
|
5801
6791
|
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
5802
6792
|
entry.lastWorkingDir = resolved;
|
|
5803
6793
|
} else {
|
|
5804
|
-
const name2 = selName?.trim() ||
|
|
6794
|
+
const name2 = selName?.trim() || path12.basename(resolved);
|
|
5805
6795
|
const slug = generateProjectSlug(resolved);
|
|
5806
6796
|
manifest.projects.push({
|
|
5807
6797
|
name: name2,
|
|
@@ -5823,13 +6813,21 @@ function createProjectHandlers(ctx) {
|
|
|
5823
6813
|
try {
|
|
5824
6814
|
const modeId = ctx.getModeId();
|
|
5825
6815
|
const switchMode = modeId === "default" ? void 0 : await ctx.modeStore.getMode(modeId);
|
|
6816
|
+
const switchPaths = resolveWstackPaths2({
|
|
6817
|
+
projectRoot: resolved,
|
|
6818
|
+
globalRoot: ctx.wpaths.globalRoot
|
|
6819
|
+
});
|
|
5826
6820
|
const switchBuilder = new DefaultSystemPromptBuilder2({
|
|
5827
6821
|
memoryStore: ctx.memoryStore,
|
|
5828
6822
|
skillLoader: ctx.skillLoader,
|
|
5829
6823
|
modeStore: ctx.modeStore,
|
|
5830
6824
|
modeId,
|
|
5831
6825
|
modePrompt: switchMode?.prompt ?? "",
|
|
5832
|
-
modelCapabilities: ctx.modelCapabilities
|
|
6826
|
+
modelCapabilities: ctx.modelCapabilities,
|
|
6827
|
+
instructionPaths: {
|
|
6828
|
+
globalDir: switchPaths.globalInstructions,
|
|
6829
|
+
projectDir: switchPaths.inProjectInstructions
|
|
6830
|
+
}
|
|
5833
6831
|
});
|
|
5834
6832
|
ctx.context.systemPrompt = await switchBuilder.build({
|
|
5835
6833
|
cwd: resolved,
|
|
@@ -5840,13 +6838,13 @@ function createProjectHandlers(ctx) {
|
|
|
5840
6838
|
});
|
|
5841
6839
|
} catch {
|
|
5842
6840
|
}
|
|
5843
|
-
const newSessionsDir =
|
|
5844
|
-
|
|
6841
|
+
const newSessionsDir = path12.join(
|
|
6842
|
+
path12.dirname(ctx.globalConfigPath),
|
|
5845
6843
|
"projects",
|
|
5846
6844
|
switchSlug,
|
|
5847
6845
|
"sessions"
|
|
5848
6846
|
);
|
|
5849
|
-
await
|
|
6847
|
+
await fs10.mkdir(newSessionsDir, { recursive: true });
|
|
5850
6848
|
const newSessionStore = new DefaultSessionStore({ dir: newSessionsDir });
|
|
5851
6849
|
const oldSession = ctx.getSession();
|
|
5852
6850
|
const oldSessionId = oldSession.id;
|
|
@@ -5880,7 +6878,7 @@ function createProjectHandlers(ctx) {
|
|
|
5880
6878
|
sessionId: newSession.id,
|
|
5881
6879
|
projectSlug: switchSlug,
|
|
5882
6880
|
projectRoot: resolved,
|
|
5883
|
-
projectName:
|
|
6881
|
+
projectName: path12.basename(resolved),
|
|
5884
6882
|
workingDir: resolved,
|
|
5885
6883
|
clientType: "webui",
|
|
5886
6884
|
pid: process.pid,
|
|
@@ -5892,8 +6890,8 @@ function createProjectHandlers(ctx) {
|
|
|
5892
6890
|
type: "projects.selected",
|
|
5893
6891
|
payload: {
|
|
5894
6892
|
root: resolved,
|
|
5895
|
-
name: selName ||
|
|
5896
|
-
message: `Switched to ${selName ||
|
|
6893
|
+
name: selName || path12.basename(resolved),
|
|
6894
|
+
message: `Switched to ${selName || path12.basename(resolved)}`
|
|
5897
6895
|
}
|
|
5898
6896
|
});
|
|
5899
6897
|
broadcast(ctx.clients, {
|
|
@@ -5913,7 +6911,7 @@ function createProjectHandlers(ctx) {
|
|
|
5913
6911
|
type: "projects.selected",
|
|
5914
6912
|
payload: {
|
|
5915
6913
|
root: selRoot,
|
|
5916
|
-
name: selName ||
|
|
6914
|
+
name: selName || path12.basename(selRoot),
|
|
5917
6915
|
message: errMessage(err)
|
|
5918
6916
|
}
|
|
5919
6917
|
});
|
|
@@ -5944,7 +6942,7 @@ function createProjectHandlers(ctx) {
|
|
|
5944
6942
|
}
|
|
5945
6943
|
|
|
5946
6944
|
// src/server/session-handlers.ts
|
|
5947
|
-
import * as
|
|
6945
|
+
import * as path13 from "path";
|
|
5948
6946
|
import {
|
|
5949
6947
|
DEFAULT_CONTEXT_WINDOW_MODE_ID,
|
|
5950
6948
|
repairToolUseAdjacency,
|
|
@@ -6286,7 +7284,7 @@ function createSessionHandlers(ctx) {
|
|
|
6286
7284
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
6287
7285
|
const projectRoot = ctx.getProjectRoot();
|
|
6288
7286
|
const rewinder = new DefaultSessionRewinder(
|
|
6289
|
-
|
|
7287
|
+
path13.join(projectRoot, ".wrongstack", "sessions"),
|
|
6290
7288
|
projectRoot
|
|
6291
7289
|
);
|
|
6292
7290
|
const checkpoints = await rewinder.listCheckpoints(ctx.getSession().id);
|
|
@@ -6301,7 +7299,7 @@ function createSessionHandlers(ctx) {
|
|
|
6301
7299
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
6302
7300
|
const projectRoot = ctx.getProjectRoot();
|
|
6303
7301
|
const rewinder = new DefaultSessionRewinder(
|
|
6304
|
-
|
|
7302
|
+
path13.join(projectRoot, ".wrongstack", "sessions"),
|
|
6305
7303
|
projectRoot
|
|
6306
7304
|
);
|
|
6307
7305
|
await rewinder.rewindToCheckpoint(ctx.getSession().id, checkpointIndex);
|
|
@@ -6677,9 +7675,9 @@ async function handleSddBoardRoute(_ws, msg, handlers) {
|
|
|
6677
7675
|
}
|
|
6678
7676
|
|
|
6679
7677
|
// src/server/setup-events.ts
|
|
6680
|
-
import * as
|
|
7678
|
+
import * as fs11 from "fs/promises";
|
|
6681
7679
|
import { watch as fsWatch } from "fs";
|
|
6682
|
-
import * as
|
|
7680
|
+
import * as path14 from "path";
|
|
6683
7681
|
function setupEvents(deps2) {
|
|
6684
7682
|
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
|
|
6685
7683
|
const disposers = [];
|
|
@@ -6768,6 +7766,22 @@ function setupEvents(deps2) {
|
|
|
6768
7766
|
}).catch(() => {
|
|
6769
7767
|
});
|
|
6770
7768
|
broadcast2(clients, { type: "todos.updated", payload: { todos: [...context.todos] } });
|
|
7769
|
+
const sideEffects = context.sideEffects ?? [];
|
|
7770
|
+
if (sideEffects.length > 0) {
|
|
7771
|
+
broadcast2(clients, {
|
|
7772
|
+
type: "side_effects",
|
|
7773
|
+
payload: {
|
|
7774
|
+
sideEffects: sideEffects.slice(-50).map((se) => ({
|
|
7775
|
+
toolUseId: se.toolUseId,
|
|
7776
|
+
toolName: se.toolName,
|
|
7777
|
+
ts: se.ts,
|
|
7778
|
+
input: se.input,
|
|
7779
|
+
outcome: se.outcome,
|
|
7780
|
+
risk: se.risk
|
|
7781
|
+
}))
|
|
7782
|
+
}
|
|
7783
|
+
});
|
|
7784
|
+
}
|
|
6771
7785
|
if (e.name === "task" || e.name === "plan" || e.name === "todo") {
|
|
6772
7786
|
void (async () => {
|
|
6773
7787
|
try {
|
|
@@ -7146,16 +8160,16 @@ function setupEvents(deps2) {
|
|
|
7146
8160
|
if (wpaths?.projectStatus) {
|
|
7147
8161
|
try {
|
|
7148
8162
|
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
7149
|
-
const dir =
|
|
7150
|
-
await
|
|
7151
|
-
await
|
|
8163
|
+
const dir = path14.dirname(statusFile);
|
|
8164
|
+
await fs11.mkdir(dir, { recursive: true });
|
|
8165
|
+
await fs11.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
|
|
7152
8166
|
} catch (err) {
|
|
7153
8167
|
console.error("[setup-events] Failed to write status.json:", err);
|
|
7154
8168
|
}
|
|
7155
8169
|
}
|
|
7156
8170
|
});
|
|
7157
8171
|
if (wpaths?.projectStatus && wpaths.configDir) {
|
|
7158
|
-
const projectsDir =
|
|
8172
|
+
const projectsDir = path14.join(wpaths.configDir, "projects");
|
|
7159
8173
|
const knownProjectHashes = /* @__PURE__ */ new Set();
|
|
7160
8174
|
const debounceTimers = /* @__PURE__ */ new Map();
|
|
7161
8175
|
const DEBOUNCE_MS = 150;
|
|
@@ -7218,20 +8232,20 @@ function setupEvents(deps2) {
|
|
|
7218
8232
|
let watcher;
|
|
7219
8233
|
const startWatcher = async () => {
|
|
7220
8234
|
try {
|
|
7221
|
-
await
|
|
8235
|
+
await fs11.mkdir(projectsDir, { recursive: true });
|
|
7222
8236
|
watcher = fsWatch(projectsDir, { persistent: true, recursive: true }, async (eventType, filename) => {
|
|
7223
8237
|
if (eventType === "change") {
|
|
7224
8238
|
if (filename == null) return;
|
|
7225
8239
|
if (watcherMetrics) watcherMetrics.fileChangesDetected++;
|
|
7226
|
-
const targetFile =
|
|
8240
|
+
const targetFile = path14.join(projectsDir, String(filename));
|
|
7227
8241
|
if (targetFile.endsWith("status.json")) {
|
|
7228
|
-
const projectHash2 =
|
|
8242
|
+
const projectHash2 = path14.basename(path14.dirname(targetFile));
|
|
7229
8243
|
if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
|
|
7230
8244
|
return;
|
|
7231
8245
|
}
|
|
7232
8246
|
if (watcherMetrics) watcherMetrics.filesProcessed++;
|
|
7233
8247
|
try {
|
|
7234
|
-
const content = await
|
|
8248
|
+
const content = await fs11.readFile(targetFile, "utf-8");
|
|
7235
8249
|
const statusData = JSON.parse(content);
|
|
7236
8250
|
if (statusData.projectHash) {
|
|
7237
8251
|
const hash = String(statusData.projectHash);
|
|
@@ -7283,7 +8297,7 @@ function setupEvents(deps2) {
|
|
|
7283
8297
|
}
|
|
7284
8298
|
});
|
|
7285
8299
|
}
|
|
7286
|
-
const globalRoot = globalConfigPath ?
|
|
8300
|
+
const globalRoot = globalConfigPath ? path14.dirname(globalConfigPath) : void 0;
|
|
7287
8301
|
if (globalRoot) {
|
|
7288
8302
|
const broadcastSessions = async () => {
|
|
7289
8303
|
try {
|
|
@@ -7356,11 +8370,11 @@ function setupEvents(deps2) {
|
|
|
7356
8370
|
|
|
7357
8371
|
// src/server/custom-context-modes.ts
|
|
7358
8372
|
import { listContextWindowModes, atomicWrite as atomicWrite5 } from "@wrongstack/core";
|
|
7359
|
-
import * as
|
|
7360
|
-
import * as
|
|
8373
|
+
import * as fs12 from "fs/promises";
|
|
8374
|
+
import * as path15 from "path";
|
|
7361
8375
|
var STORE_FILENAME = "custom-context-modes.json";
|
|
7362
8376
|
function storePath(wrongstackDir) {
|
|
7363
|
-
return
|
|
8377
|
+
return path15.join(wrongstackDir, STORE_FILENAME);
|
|
7364
8378
|
}
|
|
7365
8379
|
var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
|
|
7366
8380
|
function createCustomModeStore(wrongstackDir) {
|
|
@@ -7368,7 +8382,7 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
7368
8382
|
const load2 = async () => {
|
|
7369
8383
|
modes.clear();
|
|
7370
8384
|
try {
|
|
7371
|
-
const raw = await
|
|
8385
|
+
const raw = await fs12.readFile(storePath(wrongstackDir), "utf8");
|
|
7372
8386
|
const parsed = JSON.parse(raw);
|
|
7373
8387
|
if (Array.isArray(parsed.modes)) {
|
|
7374
8388
|
for (const m of parsed.modes) {
|
|
@@ -7414,7 +8428,8 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
7414
8428
|
custom: true
|
|
7415
8429
|
};
|
|
7416
8430
|
modes.set(mode.id, entry);
|
|
7417
|
-
void save2()
|
|
8431
|
+
void save2().catch(() => {
|
|
8432
|
+
});
|
|
7418
8433
|
return { ok: true };
|
|
7419
8434
|
};
|
|
7420
8435
|
const update = (id, patch) => {
|
|
@@ -7440,7 +8455,8 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
7440
8455
|
if (patch.targetLoad !== void 0) next.targetLoad = patch.targetLoad;
|
|
7441
8456
|
if (patch.aggressiveOn !== void 0) next.aggressiveOn = patch.aggressiveOn;
|
|
7442
8457
|
modes.set(id, next);
|
|
7443
|
-
void save2()
|
|
8458
|
+
void save2().catch(() => {
|
|
8459
|
+
});
|
|
7444
8460
|
return { ok: true };
|
|
7445
8461
|
};
|
|
7446
8462
|
const remove = (id) => {
|
|
@@ -7450,7 +8466,8 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
7450
8466
|
if (!modes.delete(id)) {
|
|
7451
8467
|
return { ok: false, error: `Mode "${id}" not found` };
|
|
7452
8468
|
}
|
|
7453
|
-
void save2()
|
|
8469
|
+
void save2().catch(() => {
|
|
8470
|
+
});
|
|
7454
8471
|
return { ok: true };
|
|
7455
8472
|
};
|
|
7456
8473
|
const list = () => {
|
|
@@ -7491,14 +8508,17 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
|
|
|
7491
8508
|
}
|
|
7492
8509
|
|
|
7493
8510
|
// src/server/shell-open.ts
|
|
7494
|
-
import * as
|
|
7495
|
-
import * as
|
|
8511
|
+
import * as fs13 from "fs/promises";
|
|
8512
|
+
import * as path16 from "path";
|
|
7496
8513
|
import { spawn as spawn2 } from "child_process";
|
|
7497
|
-
var METACHAR_REGEX = /[&|<>^"'
|
|
8514
|
+
var METACHAR_REGEX = /[&|<>^"'`'\n\r]/;
|
|
8515
|
+
function shellQuote(s) {
|
|
8516
|
+
return s.replaceAll("'", `'"'"'`);
|
|
8517
|
+
}
|
|
7498
8518
|
async function handleShellOpen(req, logger) {
|
|
7499
8519
|
try {
|
|
7500
|
-
const resolved =
|
|
7501
|
-
await
|
|
8520
|
+
const resolved = path16.resolve(req.path);
|
|
8521
|
+
await fs13.access(resolved);
|
|
7502
8522
|
if (METACHAR_REGEX.test(resolved)) {
|
|
7503
8523
|
return { success: false, message: "Path contains unsupported characters." };
|
|
7504
8524
|
}
|
|
@@ -7531,7 +8551,11 @@ async function handleShellOpen(req, logger) {
|
|
|
7531
8551
|
() => launch(
|
|
7532
8552
|
"gnome-terminal",
|
|
7533
8553
|
[`--working-directory=${resolved}`],
|
|
7534
|
-
() =>
|
|
8554
|
+
() => (
|
|
8555
|
+
// Pass argv array so sh -c sees a literal string, not an interpolated one.
|
|
8556
|
+
// shellQuote() guards against paths that somehow slipped the METACHAR_REGEX.
|
|
8557
|
+
launch("xterm", ["-e", "sh", "-c", `cd ${shellQuote(resolved)} && ${process.env["SHELL"] ?? "sh"}`])
|
|
8558
|
+
)
|
|
7535
8559
|
)
|
|
7536
8560
|
);
|
|
7537
8561
|
}
|
|
@@ -7550,9 +8574,9 @@ async function handleGitInfo(ws, projectRoot) {
|
|
|
7550
8574
|
const cwd = projectRoot || void 0;
|
|
7551
8575
|
try {
|
|
7552
8576
|
const { execFile: ef } = await import("child_process");
|
|
7553
|
-
const git = (args) => new Promise((
|
|
8577
|
+
const git = (args) => new Promise((resolve10) => {
|
|
7554
8578
|
ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
|
|
7555
|
-
|
|
8579
|
+
resolve10(err ? "" : stdout.trim());
|
|
7556
8580
|
});
|
|
7557
8581
|
});
|
|
7558
8582
|
const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
|
|
@@ -7578,12 +8602,12 @@ async function handleGitInfo(ws, projectRoot) {
|
|
|
7578
8602
|
function makeGit(cwd) {
|
|
7579
8603
|
return async (args) => {
|
|
7580
8604
|
const { execFile: ef } = await import("child_process");
|
|
7581
|
-
return new Promise((
|
|
8605
|
+
return new Promise((resolve10) => {
|
|
7582
8606
|
ef(
|
|
7583
8607
|
"git",
|
|
7584
8608
|
args,
|
|
7585
8609
|
{ cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
|
|
7586
|
-
(err, stdout) =>
|
|
8610
|
+
(err, stdout) => resolve10(err ? "" : stdout)
|
|
7587
8611
|
);
|
|
7588
8612
|
});
|
|
7589
8613
|
};
|
|
@@ -7607,15 +8631,15 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
7607
8631
|
if (!m) continue;
|
|
7608
8632
|
const added = m[1] === "-" ? 0 : Number(m[1]);
|
|
7609
8633
|
const deleted = m[2] === "-" ? 0 : Number(m[2]);
|
|
7610
|
-
let
|
|
7611
|
-
if (
|
|
8634
|
+
let path18 = m[3] ?? "";
|
|
8635
|
+
if (path18 === "") {
|
|
7612
8636
|
i += 1;
|
|
7613
|
-
|
|
8637
|
+
path18 = parts[i + 1] ?? parts[i] ?? "";
|
|
7614
8638
|
i += 1;
|
|
7615
8639
|
}
|
|
7616
|
-
if (!
|
|
7617
|
-
const prev = counts.get(
|
|
7618
|
-
counts.set(
|
|
8640
|
+
if (!path18) continue;
|
|
8641
|
+
const prev = counts.get(path18) ?? { added: 0, deleted: 0 };
|
|
8642
|
+
counts.set(path18, { added: prev.added + added, deleted: prev.deleted + deleted });
|
|
7619
8643
|
}
|
|
7620
8644
|
};
|
|
7621
8645
|
parseNumstat(unstagedNumstat);
|
|
@@ -7627,7 +8651,7 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
7627
8651
|
if (!rec || rec.length < 3) continue;
|
|
7628
8652
|
const x = rec[0] ?? " ";
|
|
7629
8653
|
const y = rec[1] ?? " ";
|
|
7630
|
-
const
|
|
8654
|
+
const path18 = rec.slice(3);
|
|
7631
8655
|
const isRename = x === "R" || x === "C" || y === "R" || y === "C";
|
|
7632
8656
|
if (isRename) i += 1;
|
|
7633
8657
|
let status;
|
|
@@ -7639,13 +8663,13 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
7639
8663
|
else if (x === "D" || y === "D") status = "D";
|
|
7640
8664
|
else status = "M";
|
|
7641
8665
|
const staged = x !== " " && x !== "?";
|
|
7642
|
-
let added = counts.get(
|
|
7643
|
-
let deleted = counts.get(
|
|
8666
|
+
let added = counts.get(path18)?.added ?? 0;
|
|
8667
|
+
let deleted = counts.get(path18)?.deleted ?? 0;
|
|
7644
8668
|
if (status === "?") {
|
|
7645
8669
|
added = 0;
|
|
7646
8670
|
deleted = 0;
|
|
7647
8671
|
}
|
|
7648
|
-
files.push({ path:
|
|
8672
|
+
files.push({ path: path18, status, added, deleted, staged });
|
|
7649
8673
|
}
|
|
7650
8674
|
send(ws, { type: "git.changes", payload: { files } });
|
|
7651
8675
|
} catch (err) {
|
|
@@ -7656,21 +8680,21 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
7656
8680
|
}
|
|
7657
8681
|
}
|
|
7658
8682
|
var MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
7659
|
-
async function handleGitDiff(ws, projectRoot,
|
|
8683
|
+
async function handleGitDiff(ws, projectRoot, path18) {
|
|
7660
8684
|
const cwd = projectRoot || void 0;
|
|
7661
|
-
const reply = (extra) => send(ws, { type: "git.diff", payload: { path:
|
|
7662
|
-
if (!
|
|
8685
|
+
const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path18, ...extra } });
|
|
8686
|
+
if (!path18 || path18.includes("\0") || path18.includes("..") || nodePath.isAbsolute(path18)) {
|
|
7663
8687
|
reply({ oldText: "", newText: "", error: "invalid path" });
|
|
7664
8688
|
return;
|
|
7665
8689
|
}
|
|
7666
8690
|
try {
|
|
7667
8691
|
const git = makeGit(cwd);
|
|
7668
8692
|
const { readFile: readFile9 } = await import("fs/promises");
|
|
7669
|
-
const { join:
|
|
7670
|
-
const oldText = await git(["show", `HEAD:${
|
|
8693
|
+
const { join: join14 } = await import("path");
|
|
8694
|
+
const oldText = await git(["show", `HEAD:${path18}`]);
|
|
7671
8695
|
let newText = "";
|
|
7672
8696
|
try {
|
|
7673
|
-
const abs = cwd ?
|
|
8697
|
+
const abs = cwd ? join14(cwd, path18) : path18;
|
|
7674
8698
|
const buf = await readFile9(abs);
|
|
7675
8699
|
if (buf.includes(0)) {
|
|
7676
8700
|
reply({ oldText: "", newText: "", binary: true });
|
|
@@ -7751,10 +8775,10 @@ async function handleProcessKillAll(ws) {
|
|
|
7751
8775
|
}
|
|
7752
8776
|
|
|
7753
8777
|
// src/server/goal-handlers.ts
|
|
7754
|
-
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
8778
|
+
import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
|
|
7755
8779
|
async function handleGoalGet(projectRoot, broadcast2) {
|
|
7756
8780
|
try {
|
|
7757
|
-
const goalPath =
|
|
8781
|
+
const goalPath = resolveWstackPaths3({ projectRoot }).projectGoal;
|
|
7758
8782
|
const { readFile: readFile9 } = await import("fs/promises");
|
|
7759
8783
|
const raw = await readFile9(goalPath, "utf8");
|
|
7760
8784
|
const goal = JSON.parse(raw);
|
|
@@ -7768,8 +8792,11 @@ async function handleGoalGet(projectRoot, broadcast2) {
|
|
|
7768
8792
|
async function startWebUI(opts = {}) {
|
|
7769
8793
|
ensureSessionShell();
|
|
7770
8794
|
const requestedWsPort = opts.wsPort ?? 3457;
|
|
7771
|
-
const wsHost = opts.wsHost ?? "127.0.0.1";
|
|
7772
|
-
const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
8795
|
+
const wsHost = opts.wsHost ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
|
|
8796
|
+
const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? "3456", 10);
|
|
8797
|
+
const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
|
|
8798
|
+
const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
|
|
8799
|
+
const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
|
|
7773
8800
|
const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
|
|
7774
8801
|
let wsPort = requestedWsPort;
|
|
7775
8802
|
let httpPort = requestedHttpPort;
|
|
@@ -7809,7 +8836,7 @@ async function startWebUI(opts = {}) {
|
|
|
7809
8836
|
const write = async () => {
|
|
7810
8837
|
let raw;
|
|
7811
8838
|
try {
|
|
7812
|
-
raw = await
|
|
8839
|
+
raw = await fs14.readFile(globalConfigPath, "utf8");
|
|
7813
8840
|
} catch {
|
|
7814
8841
|
raw = "{}";
|
|
7815
8842
|
}
|
|
@@ -7883,6 +8910,7 @@ async function startWebUI(opts = {}) {
|
|
|
7883
8910
|
toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
|
|
7884
8911
|
toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
|
|
7885
8912
|
applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
|
|
8913
|
+
applyToolResultRenderModes(toolRegistry, config.tools?.resultRenderMode);
|
|
7886
8914
|
configureExecPolicy(config.tools?.exec ?? {});
|
|
7887
8915
|
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
7888
8916
|
const mcpRegistry = new MCPRegistry({
|
|
@@ -7927,7 +8955,7 @@ async function startWebUI(opts = {}) {
|
|
|
7927
8955
|
sessionId: session.id,
|
|
7928
8956
|
projectSlug: wpaths.projectSlug,
|
|
7929
8957
|
projectRoot,
|
|
7930
|
-
projectName:
|
|
8958
|
+
projectName: path17.basename(projectRoot),
|
|
7931
8959
|
workingDir,
|
|
7932
8960
|
clientType: "webui",
|
|
7933
8961
|
pid: process.pid,
|
|
@@ -7947,7 +8975,7 @@ async function startWebUI(opts = {}) {
|
|
|
7947
8975
|
const hqTelemetry = createHqPublisherFromEnv({
|
|
7948
8976
|
clientKind: "webui",
|
|
7949
8977
|
projectRoot,
|
|
7950
|
-
projectName:
|
|
8978
|
+
projectName: path17.basename(projectRoot),
|
|
7951
8979
|
appConfig: config,
|
|
7952
8980
|
socketFactory: (url) => new WebSocket2(url)
|
|
7953
8981
|
});
|
|
@@ -7959,7 +8987,7 @@ async function startWebUI(opts = {}) {
|
|
|
7959
8987
|
events,
|
|
7960
8988
|
sessionId: session.id,
|
|
7961
8989
|
projectRoot,
|
|
7962
|
-
projectName:
|
|
8990
|
+
projectName: path17.basename(projectRoot),
|
|
7963
8991
|
globalRoot: wpaths.globalRoot,
|
|
7964
8992
|
initialAgents: statusTracker?.getAgents(),
|
|
7965
8993
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -8015,19 +9043,39 @@ async function startWebUI(opts = {}) {
|
|
|
8015
9043
|
};
|
|
8016
9044
|
const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
|
|
8017
9045
|
const skillInstaller = config.features.skills ? new SkillInstaller({
|
|
8018
|
-
manifestPath:
|
|
8019
|
-
projectSkillsDir:
|
|
8020
|
-
globalSkillsDir:
|
|
9046
|
+
manifestPath: path17.join(wstackGlobalRoot3(), "installed-skills.json"),
|
|
9047
|
+
projectSkillsDir: path17.join(projectRoot, ".wrongstack", "skills"),
|
|
9048
|
+
globalSkillsDir: path17.join(wstackGlobalRoot3(), "skills"),
|
|
8021
9049
|
projectHash: projectHash(projectRoot),
|
|
8022
9050
|
skillLoader
|
|
8023
9051
|
}) : void 0;
|
|
9052
|
+
const promptsEnabled = config.features.prompts !== false;
|
|
9053
|
+
const bundledPromptsDir = promptsEnabled ? (() => {
|
|
9054
|
+
try {
|
|
9055
|
+
const req = createRequire2(import.meta.url);
|
|
9056
|
+
return path17.join(
|
|
9057
|
+
path17.dirname(req.resolve("@wrongstack/core/package.json")),
|
|
9058
|
+
"data",
|
|
9059
|
+
"prompts"
|
|
9060
|
+
);
|
|
9061
|
+
} catch {
|
|
9062
|
+
return void 0;
|
|
9063
|
+
}
|
|
9064
|
+
})() : void 0;
|
|
9065
|
+
const promptLoader = promptsEnabled ? new DefaultPromptLoader({ paths: wpaths, bundledDir: bundledPromptsDir }) : void 0;
|
|
9066
|
+
const promptUsage = new PromptUsageStore(wpaths.promptUsage);
|
|
9067
|
+
const promptsCtx = { promptLoader, promptUsage };
|
|
8024
9068
|
const systemPromptBuilder = new DefaultSystemPromptBuilder3({
|
|
8025
9069
|
memoryStore,
|
|
8026
9070
|
skillLoader,
|
|
8027
9071
|
modeStore,
|
|
8028
9072
|
modeId,
|
|
8029
9073
|
modePrompt,
|
|
8030
|
-
modelCapabilities: () => modelCapabilitiesRef.current
|
|
9074
|
+
modelCapabilities: () => modelCapabilitiesRef.current,
|
|
9075
|
+
instructionPaths: {
|
|
9076
|
+
globalDir: wpaths.globalInstructions,
|
|
9077
|
+
projectDir: wpaths.inProjectInstructions
|
|
9078
|
+
}
|
|
8031
9079
|
});
|
|
8032
9080
|
let onlineAgents = [];
|
|
8033
9081
|
try {
|
|
@@ -8122,6 +9170,10 @@ async function startWebUI(opts = {}) {
|
|
|
8122
9170
|
context.meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
|
|
8123
9171
|
context.meta["nextPrediction"] = config.nextPrediction ?? false;
|
|
8124
9172
|
context.meta["fallbackModels"] = config.fallbackModels ?? [];
|
|
9173
|
+
context.meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
|
|
9174
|
+
context.meta["favoriteModels"] = config.favoriteModels ?? [];
|
|
9175
|
+
context.meta["favoriteModelsOnly"] = config.favoriteModelsOnly === true;
|
|
9176
|
+
context.meta["modelMatrix"] = config.modelMatrix ?? {};
|
|
8125
9177
|
context.meta["fallbackAuto"] = config.fallbackAuto !== false;
|
|
8126
9178
|
context.meta["featureMcp"] = config.features.mcp !== false;
|
|
8127
9179
|
context.meta["featurePlugins"] = config.features.plugins !== false;
|
|
@@ -8134,6 +9186,20 @@ async function startWebUI(opts = {}) {
|
|
|
8134
9186
|
context.meta["logLevel"] = config.log?.level ?? "info";
|
|
8135
9187
|
context.meta["auditLevel"] = config.session?.auditLevel ?? "standard";
|
|
8136
9188
|
context.meta["maxIterations"] = config.tools?.maxIterations ?? 500;
|
|
9189
|
+
context.meta["contextMode"] = config.context?.mode ?? "balanced";
|
|
9190
|
+
{
|
|
9191
|
+
const tsm = config.features?.tokenSavingMode;
|
|
9192
|
+
context.meta["tokenSavingTier"] = typeof tsm === "string" ? tsm : tsm ? "medium" : "off";
|
|
9193
|
+
}
|
|
9194
|
+
context.meta["maxConcurrent"] = typeof config.maxConcurrent === "number" ? config.maxConcurrent : 10;
|
|
9195
|
+
context.meta["titleAnimation"] = autonomyCfg["terminalTitleAnimation"] !== false;
|
|
9196
|
+
{
|
|
9197
|
+
const mr = config.modelRuntime ?? {};
|
|
9198
|
+
context.meta["reasoningMode"] = mr.reasoning?.mode ?? "auto";
|
|
9199
|
+
context.meta["reasoningEffort"] = mr.reasoning?.effort ?? "high";
|
|
9200
|
+
context.meta["reasoningPreserve"] = mr.reasoning?.preserve === true;
|
|
9201
|
+
context.meta["cacheTtl"] = mr.cache?.ttl ?? "default";
|
|
9202
|
+
}
|
|
8137
9203
|
const hqConfig = config.hq;
|
|
8138
9204
|
context.meta["hqEnabled"] = hqConfig?.enabled === true;
|
|
8139
9205
|
context.meta["hqUrl"] = hqConfig?.url ?? "";
|
|
@@ -8167,6 +9233,10 @@ async function startWebUI(opts = {}) {
|
|
|
8167
9233
|
"indexOnStart",
|
|
8168
9234
|
"contextAutoCompact",
|
|
8169
9235
|
"contextStrategy",
|
|
9236
|
+
"contextMode",
|
|
9237
|
+
"tokenSavingTier",
|
|
9238
|
+
"maxConcurrent",
|
|
9239
|
+
"titleAnimation",
|
|
8170
9240
|
"logLevel",
|
|
8171
9241
|
"auditLevel",
|
|
8172
9242
|
"hqEnabled",
|
|
@@ -8182,6 +9252,10 @@ async function startWebUI(opts = {}) {
|
|
|
8182
9252
|
"reasoningPreserve",
|
|
8183
9253
|
"cacheTtl",
|
|
8184
9254
|
"fallbackModels",
|
|
9255
|
+
"fallbackProfiles",
|
|
9256
|
+
"favoriteModels",
|
|
9257
|
+
"favoriteModelsOnly",
|
|
9258
|
+
"modelMatrix",
|
|
8185
9259
|
"fallbackAuto"
|
|
8186
9260
|
];
|
|
8187
9261
|
const prefSnapshot = () => {
|
|
@@ -8214,6 +9288,15 @@ async function startWebUI(opts = {}) {
|
|
|
8214
9288
|
if (autonomyTouched) decrypted.autonomy = autonomyCfg;
|
|
8215
9289
|
if (typeof payload["nextPrediction"] === "boolean") decrypted.nextPrediction = payload["nextPrediction"];
|
|
8216
9290
|
if (Array.isArray(payload["fallbackModels"])) decrypted.fallbackModels = payload["fallbackModels"];
|
|
9291
|
+
if (payload["fallbackProfiles"] && typeof payload["fallbackProfiles"] === "object" && !Array.isArray(payload["fallbackProfiles"])) {
|
|
9292
|
+
decrypted.fallbackProfiles = payload["fallbackProfiles"];
|
|
9293
|
+
}
|
|
9294
|
+
if (Array.isArray(payload["favoriteModels"])) decrypted.favoriteModels = payload["favoriteModels"];
|
|
9295
|
+
if (typeof payload["favoriteModelsOnly"] === "boolean")
|
|
9296
|
+
decrypted.favoriteModelsOnly = payload["favoriteModelsOnly"];
|
|
9297
|
+
if (payload["modelMatrix"] && typeof payload["modelMatrix"] === "object" && !Array.isArray(payload["modelMatrix"])) {
|
|
9298
|
+
decrypted.modelMatrix = payload["modelMatrix"];
|
|
9299
|
+
}
|
|
8217
9300
|
if (typeof payload["fallbackAuto"] === "boolean") decrypted.fallbackAuto = payload["fallbackAuto"];
|
|
8218
9301
|
const FEATURE_MAP = {
|
|
8219
9302
|
featureMcp: "mcp",
|
|
@@ -8229,12 +9312,26 @@ async function startWebUI(opts = {}) {
|
|
|
8229
9312
|
decrypted.features = feats;
|
|
8230
9313
|
}
|
|
8231
9314
|
}
|
|
8232
|
-
if (typeof payload["contextAutoCompact"] === "boolean" || typeof payload["contextStrategy"] === "string") {
|
|
9315
|
+
if (typeof payload["contextAutoCompact"] === "boolean" || typeof payload["contextStrategy"] === "string" || typeof payload["contextMode"] === "string") {
|
|
8233
9316
|
const ctxCfg = decrypted.context ?? {};
|
|
8234
9317
|
if (typeof payload["contextAutoCompact"] === "boolean") ctxCfg.autoCompact = payload["contextAutoCompact"];
|
|
8235
9318
|
if (typeof payload["contextStrategy"] === "string") ctxCfg.strategy = payload["contextStrategy"];
|
|
9319
|
+
if (typeof payload["contextMode"] === "string") ctxCfg.mode = payload["contextMode"];
|
|
8236
9320
|
decrypted.context = ctxCfg;
|
|
8237
9321
|
}
|
|
9322
|
+
if (typeof payload["tokenSavingTier"] === "string") {
|
|
9323
|
+
const featsCfg = decrypted.features ?? {};
|
|
9324
|
+
featsCfg.tokenSavingMode = payload["tokenSavingTier"];
|
|
9325
|
+
decrypted.features = featsCfg;
|
|
9326
|
+
}
|
|
9327
|
+
if (typeof payload["maxConcurrent"] === "number") {
|
|
9328
|
+
decrypted.maxConcurrent = payload["maxConcurrent"];
|
|
9329
|
+
}
|
|
9330
|
+
if (typeof payload["titleAnimation"] === "boolean") {
|
|
9331
|
+
const autoCfg = decrypted.autonomy ?? {};
|
|
9332
|
+
autoCfg.terminalTitleAnimation = payload["titleAnimation"];
|
|
9333
|
+
decrypted.autonomy = autoCfg;
|
|
9334
|
+
}
|
|
8238
9335
|
if (typeof payload["logLevel"] === "string") {
|
|
8239
9336
|
const logCfg = decrypted.log ?? {};
|
|
8240
9337
|
logCfg.level = payload["logLevel"];
|
|
@@ -8305,6 +9402,7 @@ async function startWebUI(opts = {}) {
|
|
|
8305
9402
|
const collabInject = collabInjectMiddleware(collabBus, { logger });
|
|
8306
9403
|
Object.defineProperty(collabInject, "name", { value: "collab-inject" });
|
|
8307
9404
|
pipelines.toolCall.prepend(collabInject);
|
|
9405
|
+
installDesignStudioMiddleware({ pipelines, ctx: context });
|
|
8308
9406
|
const codebaseIndexing = setupWebUICodebaseIndexing({
|
|
8309
9407
|
config,
|
|
8310
9408
|
context,
|
|
@@ -8400,6 +9498,17 @@ async function startWebUI(opts = {}) {
|
|
|
8400
9498
|
perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
|
|
8401
9499
|
tracer: void 0
|
|
8402
9500
|
});
|
|
9501
|
+
const webuiLogger = container.resolve(TOKENS.Logger);
|
|
9502
|
+
void discoverMailboxBridgeForWebui({
|
|
9503
|
+
projectRoot,
|
|
9504
|
+
config,
|
|
9505
|
+
logger: webuiLogger,
|
|
9506
|
+
ctx: context
|
|
9507
|
+
}).catch((err) => {
|
|
9508
|
+
webuiLogger.warn("mailbox bridge discovery threw on webui boot", {
|
|
9509
|
+
err: err instanceof Error ? err.message : String(err)
|
|
9510
|
+
});
|
|
9511
|
+
});
|
|
8403
9512
|
const agent = new Agent({
|
|
8404
9513
|
container,
|
|
8405
9514
|
tools: toolRegistry,
|
|
@@ -8496,7 +9605,18 @@ async function startWebUI(opts = {}) {
|
|
|
8496
9605
|
projectRoot
|
|
8497
9606
|
);
|
|
8498
9607
|
const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
|
|
8499
|
-
const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards
|
|
9608
|
+
const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards, void 0, {
|
|
9609
|
+
projectRoot,
|
|
9610
|
+
paths: {
|
|
9611
|
+
projectSpecs: wpaths.projectSpecs,
|
|
9612
|
+
projectTaskGraphs: wpaths.projectTaskGraphs,
|
|
9613
|
+
projectSddSession: wpaths.projectSddSession,
|
|
9614
|
+
projectSddBoards: wpaths.projectSddBoards
|
|
9615
|
+
}
|
|
9616
|
+
});
|
|
9617
|
+
void cleanupStaleSddWorktrees3({ projectRoot, boardsDir: wpaths.projectSddBoards }).catch(
|
|
9618
|
+
() => void 0
|
|
9619
|
+
);
|
|
8500
9620
|
const sddWizardHandler = new SddWizardWebSocketHandler(
|
|
8501
9621
|
buildSddWizardDeps({
|
|
8502
9622
|
agent,
|
|
@@ -8518,7 +9638,10 @@ async function startWebUI(opts = {}) {
|
|
|
8518
9638
|
}
|
|
8519
9639
|
})
|
|
8520
9640
|
);
|
|
8521
|
-
const worktreeHandler = new WorktreeWebSocketHandler(events, logger
|
|
9641
|
+
const worktreeHandler = new WorktreeWebSocketHandler(events, logger, {
|
|
9642
|
+
projectRoot,
|
|
9643
|
+
boardsDir: wpaths.projectSddBoards
|
|
9644
|
+
});
|
|
8522
9645
|
const terminalHandler = new TerminalWebSocketHandler(() => workingDir, logger);
|
|
8523
9646
|
const collabHandler = new CollaborationWebSocketHandler(
|
|
8524
9647
|
events,
|
|
@@ -8557,15 +9680,23 @@ async function startWebUI(opts = {}) {
|
|
|
8557
9680
|
inputCost,
|
|
8558
9681
|
outputCost,
|
|
8559
9682
|
cacheReadCost,
|
|
8560
|
-
projectName:
|
|
9683
|
+
projectName: path17.basename(projectRoot) || projectRoot,
|
|
8561
9684
|
projectRoot,
|
|
8562
9685
|
cwd: workingDir,
|
|
8563
9686
|
mode: modeId,
|
|
8564
9687
|
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID2)
|
|
8565
9688
|
};
|
|
8566
9689
|
}
|
|
8567
|
-
const wsToken =
|
|
8568
|
-
console.log("[WebUI] WS auth token
|
|
9690
|
+
const wsToken = resolveAuthToken(opts.accessToken);
|
|
9691
|
+
console.log("[WebUI] WS auth token ready");
|
|
9692
|
+
const publicHostnames = [publicUrl, publicWsUrl].map((value) => {
|
|
9693
|
+
if (!value) return void 0;
|
|
9694
|
+
try {
|
|
9695
|
+
return new URL(value).hostname;
|
|
9696
|
+
} catch {
|
|
9697
|
+
return void 0;
|
|
9698
|
+
}
|
|
9699
|
+
}).filter((value) => Boolean(value));
|
|
8569
9700
|
const verifyClient2 = (info) => verifyClient({
|
|
8570
9701
|
origin: info.origin,
|
|
8571
9702
|
url: info.req.url ?? "",
|
|
@@ -8577,7 +9708,10 @@ async function startWebUI(opts = {}) {
|
|
|
8577
9708
|
// exposure class.
|
|
8578
9709
|
cookieHeader: info.req.headers.cookie,
|
|
8579
9710
|
wsHost,
|
|
8580
|
-
expectedToken: wsToken
|
|
9711
|
+
expectedToken: wsToken,
|
|
9712
|
+
requireToken,
|
|
9713
|
+
allowedHostnames: publicHostnames,
|
|
9714
|
+
allowBrowserUrlToken: Boolean(publicWsUrl)
|
|
8581
9715
|
});
|
|
8582
9716
|
const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
|
|
8583
9717
|
const wssPrimary = new WebSocketServer({
|
|
@@ -8695,8 +9829,8 @@ async function startWebUI(opts = {}) {
|
|
|
8695
9829
|
clients.delete(ws);
|
|
8696
9830
|
if (closing) rateLimits.delete(closing.connId);
|
|
8697
9831
|
if (pendingConfirms.size > 0) {
|
|
8698
|
-
for (const [id,
|
|
8699
|
-
|
|
9832
|
+
for (const [id, resolve10] of pendingConfirms) {
|
|
9833
|
+
resolve10("no");
|
|
8700
9834
|
pendingConfirms.delete(id);
|
|
8701
9835
|
}
|
|
8702
9836
|
}
|
|
@@ -8776,21 +9910,21 @@ async function startWebUI(opts = {}) {
|
|
|
8776
9910
|
});
|
|
8777
9911
|
}
|
|
8778
9912
|
async function touchProjectEntry(root, workDir) {
|
|
8779
|
-
const resolved =
|
|
9913
|
+
const resolved = path17.resolve(root);
|
|
8780
9914
|
const manifest = await loadManifest(globalConfigPath);
|
|
8781
9915
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8782
|
-
const existing = manifest.projects.find((p) =>
|
|
9916
|
+
const existing = manifest.projects.find((p) => path17.resolve(p.root) === resolved);
|
|
8783
9917
|
if (existing) {
|
|
8784
9918
|
existing.lastSeen = now;
|
|
8785
|
-
if (workDir) existing.lastWorkingDir =
|
|
9919
|
+
if (workDir) existing.lastWorkingDir = path17.resolve(workDir);
|
|
8786
9920
|
} else {
|
|
8787
9921
|
manifest.projects.push({
|
|
8788
|
-
name:
|
|
9922
|
+
name: path17.basename(resolved),
|
|
8789
9923
|
root: resolved,
|
|
8790
9924
|
slug: generateProjectSlug(resolved),
|
|
8791
9925
|
createdAt: now,
|
|
8792
9926
|
lastSeen: now,
|
|
8793
|
-
lastWorkingDir: workDir ?
|
|
9927
|
+
lastWorkingDir: workDir ? path17.resolve(workDir) : void 0
|
|
8794
9928
|
});
|
|
8795
9929
|
}
|
|
8796
9930
|
await saveManifest(manifest, globalConfigPath);
|
|
@@ -8835,6 +9969,8 @@ async function startWebUI(opts = {}) {
|
|
|
8835
9969
|
if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
|
|
8836
9970
|
if (await handleSddBoardRoute(ws, msg, sddBoardRoutes)) return;
|
|
8837
9971
|
if (await handleSddWizardRoute(ws, msg, sddWizardRoutes)) return;
|
|
9972
|
+
if (msg.type.startsWith("worktree.") && await worktreeHandler.handleMessage(msg))
|
|
9973
|
+
return;
|
|
8838
9974
|
switch (msg.type) {
|
|
8839
9975
|
// Collaboration messages short-circuit the user/agent flow.
|
|
8840
9976
|
// They don't touch runLock, the agent loop, or the message queue —
|
|
@@ -8905,10 +10041,10 @@ async function startWebUI(opts = {}) {
|
|
|
8905
10041
|
}
|
|
8906
10042
|
case "tool.confirm_result": {
|
|
8907
10043
|
const { id, decision } = msg.payload;
|
|
8908
|
-
const
|
|
8909
|
-
if (
|
|
10044
|
+
const resolve10 = pendingConfirms.get(id);
|
|
10045
|
+
if (resolve10) {
|
|
8910
10046
|
pendingConfirms.delete(id);
|
|
8911
|
-
|
|
10047
|
+
resolve10(decision);
|
|
8912
10048
|
}
|
|
8913
10049
|
break;
|
|
8914
10050
|
}
|
|
@@ -8992,6 +10128,48 @@ async function startWebUI(opts = {}) {
|
|
|
8992
10128
|
case "skills.export":
|
|
8993
10129
|
await handleSkillsExport(ws, { skillLoader, skillInstaller, projectRoot });
|
|
8994
10130
|
break;
|
|
10131
|
+
// Prompt library — shared handlers (prompts-handlers.ts).
|
|
10132
|
+
case "prompts.list":
|
|
10133
|
+
await handlePromptsList(ws, promptsCtx);
|
|
10134
|
+
break;
|
|
10135
|
+
case "prompts.search":
|
|
10136
|
+
await handlePromptsSearch(ws, promptsCtx, msg);
|
|
10137
|
+
break;
|
|
10138
|
+
case "prompts.content":
|
|
10139
|
+
await handlePromptsContent(ws, promptsCtx, msg);
|
|
10140
|
+
break;
|
|
10141
|
+
case "prompts.favorite":
|
|
10142
|
+
await handlePromptsFavorite(ws, promptsCtx, msg);
|
|
10143
|
+
break;
|
|
10144
|
+
case "prompts.create":
|
|
10145
|
+
await handlePromptsCreate(ws, promptsCtx, msg);
|
|
10146
|
+
break;
|
|
10147
|
+
case "prompts.used":
|
|
10148
|
+
await handlePromptsUsed(ws, promptsCtx, msg);
|
|
10149
|
+
break;
|
|
10150
|
+
case "prompts.recent":
|
|
10151
|
+
await handlePromptsRecent(ws, promptsCtx);
|
|
10152
|
+
break;
|
|
10153
|
+
// Design Studio — shared handlers (design-handlers.ts). agentMeta is the
|
|
10154
|
+
// live context so design.use pins the active kit for the next turn.
|
|
10155
|
+
case "design.list":
|
|
10156
|
+
await handleDesignList(ws, { projectRoot, agentMeta: context });
|
|
10157
|
+
break;
|
|
10158
|
+
case "design.use":
|
|
10159
|
+
await handleDesignUse(ws, { projectRoot, agentMeta: context }, msg);
|
|
10160
|
+
break;
|
|
10161
|
+
case "design.state":
|
|
10162
|
+
await handleDesignState(ws, { projectRoot, agentMeta: context });
|
|
10163
|
+
break;
|
|
10164
|
+
case "design.set":
|
|
10165
|
+
await handleDesignSet(ws, { projectRoot, agentMeta: context }, msg);
|
|
10166
|
+
break;
|
|
10167
|
+
case "design.materialize":
|
|
10168
|
+
await handleDesignMaterialize(ws, { projectRoot, agentMeta: context }, msg);
|
|
10169
|
+
break;
|
|
10170
|
+
case "design.verify":
|
|
10171
|
+
await handleDesignVerify(ws, { projectRoot, agentMeta: context });
|
|
10172
|
+
break;
|
|
8995
10173
|
case "diag.get": {
|
|
8996
10174
|
const usage = tokenCounter.total();
|
|
8997
10175
|
send(ws, {
|
|
@@ -9072,11 +10250,29 @@ async function startWebUI(opts = {}) {
|
|
|
9072
10250
|
messages: context.messages.length,
|
|
9073
10251
|
readFiles: context.readFiles.size,
|
|
9074
10252
|
tools: toolRegistry.list().length,
|
|
10253
|
+
sideEffectCount: context.sideEffects?.length ?? 0,
|
|
9075
10254
|
elapsedMs: Date.now() - sessionStartedAt
|
|
9076
10255
|
}
|
|
9077
10256
|
});
|
|
9078
10257
|
break;
|
|
9079
10258
|
}
|
|
10259
|
+
case "side_effects.list": {
|
|
10260
|
+
const sideEffects = context.sideEffects ?? [];
|
|
10261
|
+
send(ws, {
|
|
10262
|
+
type: "side_effects",
|
|
10263
|
+
payload: {
|
|
10264
|
+
sideEffects: sideEffects.slice(-50).map((se) => ({
|
|
10265
|
+
toolUseId: se.toolUseId,
|
|
10266
|
+
toolName: se.toolName,
|
|
10267
|
+
ts: se.ts,
|
|
10268
|
+
input: se.input,
|
|
10269
|
+
outcome: se.outcome,
|
|
10270
|
+
risk: se.risk
|
|
10271
|
+
}))
|
|
10272
|
+
}
|
|
10273
|
+
});
|
|
10274
|
+
break;
|
|
10275
|
+
}
|
|
9080
10276
|
case "process.list": {
|
|
9081
10277
|
await handleProcessList(ws);
|
|
9082
10278
|
break;
|
|
@@ -9331,6 +10527,7 @@ async function startWebUI(opts = {}) {
|
|
|
9331
10527
|
toolRegistry,
|
|
9332
10528
|
config,
|
|
9333
10529
|
projectRoot,
|
|
10530
|
+
globalRoot: wpaths.globalRoot,
|
|
9334
10531
|
clients,
|
|
9335
10532
|
setModeId: (id) => {
|
|
9336
10533
|
modeId = id;
|
|
@@ -9367,6 +10564,16 @@ async function startWebUI(opts = {}) {
|
|
|
9367
10564
|
config.features.modelsRegistry = payload["featureModelsRegistry"];
|
|
9368
10565
|
if (Array.isArray(payload["fallbackModels"]))
|
|
9369
10566
|
config.fallbackModels = payload["fallbackModels"];
|
|
10567
|
+
if (payload["fallbackProfiles"] && typeof payload["fallbackProfiles"] === "object" && !Array.isArray(payload["fallbackProfiles"])) {
|
|
10568
|
+
config.fallbackProfiles = payload["fallbackProfiles"];
|
|
10569
|
+
}
|
|
10570
|
+
if (Array.isArray(payload["favoriteModels"]))
|
|
10571
|
+
config.favoriteModels = payload["favoriteModels"];
|
|
10572
|
+
if (typeof payload["favoriteModelsOnly"] === "boolean")
|
|
10573
|
+
config.favoriteModelsOnly = payload["favoriteModelsOnly"];
|
|
10574
|
+
if (payload["modelMatrix"] && typeof payload["modelMatrix"] === "object" && !Array.isArray(payload["modelMatrix"])) {
|
|
10575
|
+
config.modelMatrix = payload["modelMatrix"];
|
|
10576
|
+
}
|
|
9370
10577
|
if (typeof payload["fallbackAuto"] === "boolean")
|
|
9371
10578
|
config.fallbackAuto = payload["fallbackAuto"];
|
|
9372
10579
|
if (typeof payload["contextAutoCompact"] === "boolean") {
|
|
@@ -9418,7 +10625,7 @@ async function startWebUI(opts = {}) {
|
|
|
9418
10625
|
sendResult2(ws, false, parsed.message);
|
|
9419
10626
|
return;
|
|
9420
10627
|
}
|
|
9421
|
-
return handleMailboxMessages(ws, { projectRoot, globalRoot:
|
|
10628
|
+
return handleMailboxMessages(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }, parsed.value);
|
|
9422
10629
|
},
|
|
9423
10630
|
agents: (ws, msg) => {
|
|
9424
10631
|
const parsed = validateMailboxAgentsPayload(msg.payload);
|
|
@@ -9426,16 +10633,16 @@ async function startWebUI(opts = {}) {
|
|
|
9426
10633
|
sendResult2(ws, false, parsed.message);
|
|
9427
10634
|
return;
|
|
9428
10635
|
}
|
|
9429
|
-
return handleMailboxAgents(ws, { projectRoot, globalRoot:
|
|
10636
|
+
return handleMailboxAgents(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }, parsed.value);
|
|
9430
10637
|
},
|
|
9431
|
-
clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot:
|
|
10638
|
+
clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }),
|
|
9432
10639
|
purge: (ws, msg) => {
|
|
9433
10640
|
const parsed = validateMailboxPurgePayload(msg.payload);
|
|
9434
10641
|
if (!parsed.ok) {
|
|
9435
10642
|
sendResult2(ws, false, parsed.message);
|
|
9436
10643
|
return;
|
|
9437
10644
|
}
|
|
9438
|
-
return handleMailboxPurge(ws, { projectRoot, globalRoot:
|
|
10645
|
+
return handleMailboxPurge(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }, parsed.value);
|
|
9439
10646
|
}
|
|
9440
10647
|
};
|
|
9441
10648
|
mcpRoutes = {
|
|
@@ -9515,18 +10722,25 @@ async function startWebUI(opts = {}) {
|
|
|
9515
10722
|
};
|
|
9516
10723
|
const httpServer = createHttpServer({
|
|
9517
10724
|
host: wsHost,
|
|
9518
|
-
distDir:
|
|
10725
|
+
distDir: path17.resolve(import.meta.dirname, "../../dist"),
|
|
9519
10726
|
wsPort,
|
|
10727
|
+
publicWsUrl,
|
|
9520
10728
|
globalRoot: wpaths.globalRoot,
|
|
9521
10729
|
apiToken: wsToken,
|
|
10730
|
+
requireToken,
|
|
9522
10731
|
watcherMetrics,
|
|
9523
10732
|
onFleetPing: () => {
|
|
9524
10733
|
void fleetBroadcast?.();
|
|
9525
10734
|
}
|
|
9526
10735
|
});
|
|
9527
|
-
const registryBaseDir =
|
|
10736
|
+
const registryBaseDir = path17.dirname(globalConfigPath);
|
|
9528
10737
|
httpServer.listen(httpPort, wsHost, () => {
|
|
9529
|
-
const openUrl =
|
|
10738
|
+
const openUrl = buildWebUIAccessUrl({
|
|
10739
|
+
host: wsHost,
|
|
10740
|
+
port: httpPort,
|
|
10741
|
+
token: wsToken,
|
|
10742
|
+
publicUrl
|
|
10743
|
+
});
|
|
9530
10744
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
9531
10745
|
if (opts.open) openBrowser(openUrl);
|
|
9532
10746
|
void registerInstance(
|
|
@@ -9536,9 +10750,9 @@ async function startWebUI(opts = {}) {
|
|
|
9536
10750
|
wsPort,
|
|
9537
10751
|
host: wsHost,
|
|
9538
10752
|
projectRoot,
|
|
9539
|
-
projectName:
|
|
10753
|
+
projectName: path17.basename(projectRoot) || projectRoot,
|
|
9540
10754
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9541
|
-
url:
|
|
10755
|
+
url: buildWebUIAccessUrl({ host: wsHost, port: httpPort, publicUrl })
|
|
9542
10756
|
},
|
|
9543
10757
|
registryBaseDir
|
|
9544
10758
|
).catch((err) => console.warn(JSON.stringify({
|
|
@@ -9580,7 +10794,55 @@ async function startWebUI(opts = {}) {
|
|
|
9580
10794
|
|
|
9581
10795
|
// src/server/entry.ts
|
|
9582
10796
|
var argv = process.argv.slice(2);
|
|
9583
|
-
|
|
10797
|
+
function readArg(names) {
|
|
10798
|
+
for (let i = 0; i < argv.length; i++) {
|
|
10799
|
+
const current = argv[i];
|
|
10800
|
+
if (!current) continue;
|
|
10801
|
+
for (const name2 of names) {
|
|
10802
|
+
if (current === name2) {
|
|
10803
|
+
const next = argv[i + 1];
|
|
10804
|
+
if (!next || next.startsWith("-")) {
|
|
10805
|
+
throw new Error(`${name2} requires a value`);
|
|
10806
|
+
}
|
|
10807
|
+
return next;
|
|
10808
|
+
}
|
|
10809
|
+
if (current.startsWith(`${name2}=`)) return current.slice(name2.length + 1);
|
|
10810
|
+
}
|
|
10811
|
+
}
|
|
10812
|
+
return void 0;
|
|
10813
|
+
}
|
|
10814
|
+
function parsePort(value, fallback, label) {
|
|
10815
|
+
if (value === void 0) return fallback;
|
|
10816
|
+
const parsed = Number.parseInt(value, 10);
|
|
10817
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
10818
|
+
throw new Error(`${label} must be a port between 1 and 65535`);
|
|
10819
|
+
}
|
|
10820
|
+
return parsed;
|
|
10821
|
+
}
|
|
10822
|
+
function envFlag2(name2) {
|
|
10823
|
+
const value = process.env[name2]?.trim().toLowerCase();
|
|
10824
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
10825
|
+
}
|
|
10826
|
+
function printHelp() {
|
|
10827
|
+
console.log(`Usage: wstackui [options]
|
|
10828
|
+
|
|
10829
|
+
Options:
|
|
10830
|
+
--host <host> Bind host/interface (default: 127.0.0.1)
|
|
10831
|
+
--port <port> HTTP frontend port (default: 3456)
|
|
10832
|
+
--ws-port <port> WebSocket backend port (default: 3457)
|
|
10833
|
+
--token <token> Fixed access token/password (default: random per process)
|
|
10834
|
+
--public-url <url> Browser-facing HTTP URL for tunnels/proxies
|
|
10835
|
+
--public-ws-url <url> Browser-facing ws:// or wss:// URL for tunnels/proxies
|
|
10836
|
+
--require-token Require token/password even on loopback binds
|
|
10837
|
+
--open, -o Open the browser after startup
|
|
10838
|
+
--list, -l, ls List running WebUI instances
|
|
10839
|
+
--help, -h Show this help
|
|
10840
|
+
`);
|
|
10841
|
+
}
|
|
10842
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
10843
|
+
printHelp();
|
|
10844
|
+
process.exit(0);
|
|
10845
|
+
} else if (argv.includes("--list") || argv.includes("-l") || argv[0] === "ls") {
|
|
9584
10846
|
listInstances().then((instances) => {
|
|
9585
10847
|
console.log(formatInstances(instances));
|
|
9586
10848
|
process.exit(0);
|
|
@@ -9594,11 +10856,40 @@ if (argv.includes("--list") || argv.includes("-l") || argv[0] === "ls") {
|
|
|
9594
10856
|
process.exit(1);
|
|
9595
10857
|
});
|
|
9596
10858
|
} else {
|
|
9597
|
-
|
|
9598
|
-
|
|
10859
|
+
let wsPort;
|
|
10860
|
+
let httpPort;
|
|
10861
|
+
let wsHost;
|
|
10862
|
+
let accessToken;
|
|
10863
|
+
let publicUrl;
|
|
10864
|
+
let publicWsUrl;
|
|
10865
|
+
try {
|
|
10866
|
+
wsHost = readArg(["--host"]) ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
|
|
10867
|
+
httpPort = parsePort(
|
|
10868
|
+
readArg(["--port", "--http-port"]) ?? process.env["WEBUI_PORT"] ?? process.env["PORT"],
|
|
10869
|
+
3456,
|
|
10870
|
+
"--port"
|
|
10871
|
+
);
|
|
10872
|
+
wsPort = parsePort(readArg(["--ws-port"]) ?? process.env["WS_PORT"], 3457, "--ws-port");
|
|
10873
|
+
accessToken = readArg(["--token", "--auth-token"]) ?? process.env["WEBUI_TOKEN"] ?? process.env["WEBUI_AUTH_TOKEN"];
|
|
10874
|
+
publicUrl = readArg(["--public-url"]) ?? process.env["WEBUI_PUBLIC_URL"];
|
|
10875
|
+
publicWsUrl = readArg(["--public-ws-url"]) ?? process.env["WEBUI_PUBLIC_WS_URL"];
|
|
10876
|
+
} catch (err) {
|
|
10877
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
10878
|
+
process.exit(1);
|
|
10879
|
+
}
|
|
9599
10880
|
const open = argv.includes("--open") || argv.includes("-o") || process.env["WEBUI_OPEN"] === "1";
|
|
9600
|
-
|
|
9601
|
-
|
|
10881
|
+
const requireToken = argv.includes("--require-token") || envFlag2("WEBUI_REQUIRE_TOKEN");
|
|
10882
|
+
console.log(`[WebUI] Starting standalone server on ${wsHost} (http:${httpPort}, ws:${wsPort})...`);
|
|
10883
|
+
startWebUI({
|
|
10884
|
+
wsPort,
|
|
10885
|
+
wsHost,
|
|
10886
|
+
httpPort,
|
|
10887
|
+
accessToken,
|
|
10888
|
+
publicUrl,
|
|
10889
|
+
publicWsUrl,
|
|
10890
|
+
requireToken,
|
|
10891
|
+
open
|
|
10892
|
+
}).catch((err) => {
|
|
9602
10893
|
console.error(JSON.stringify({
|
|
9603
10894
|
level: "fatal",
|
|
9604
10895
|
event: "webui.startup_failed",
|