@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/index.js
CHANGED
|
@@ -170,9 +170,12 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
170
170
|
"reasoningPreserve",
|
|
171
171
|
"hqEnabled",
|
|
172
172
|
"hqRawContent",
|
|
173
|
-
"fallbackAuto"
|
|
173
|
+
"fallbackAuto",
|
|
174
|
+
"favoriteModelsOnly"
|
|
174
175
|
]);
|
|
175
|
-
var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels"]);
|
|
176
|
+
var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels", "favoriteModels"]);
|
|
177
|
+
var STRING_ARRAY_RECORD_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackProfiles"]);
|
|
178
|
+
var MODEL_MATRIX_PREF_KEYS = /* @__PURE__ */ new Set(["modelMatrix"]);
|
|
176
179
|
var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
177
180
|
"autonomyDelayMs",
|
|
178
181
|
"autoProceedMaxIterations",
|
|
@@ -207,6 +210,33 @@ function validatePreferenceValue(key, value) {
|
|
|
207
210
|
if (STRING_ARRAY_PREF_KEYS.has(key)) {
|
|
208
211
|
return Array.isArray(value) && value.every((v) => typeof v === "string") ? null : `prefs.update payload.${key} must be an array of strings`;
|
|
209
212
|
}
|
|
213
|
+
if (STRING_ARRAY_RECORD_PREF_KEYS.has(key)) {
|
|
214
|
+
return isRecord(value) && Object.values(value).every(
|
|
215
|
+
(v) => Array.isArray(v) && v.every((item) => typeof item === "string")
|
|
216
|
+
) ? null : `prefs.update payload.${key} must be an object of string arrays`;
|
|
217
|
+
}
|
|
218
|
+
if (MODEL_MATRIX_PREF_KEYS.has(key)) {
|
|
219
|
+
if (!isRecord(value)) return `prefs.update payload.${key} must be an object`;
|
|
220
|
+
for (const entry of Object.values(value)) {
|
|
221
|
+
if (!isRecord(entry)) return `prefs.update payload.${key} entries must be objects`;
|
|
222
|
+
const provider = entry["provider"];
|
|
223
|
+
const model = entry["model"];
|
|
224
|
+
const fallbackProfile = entry["fallbackProfile"];
|
|
225
|
+
if (provider !== void 0 && typeof provider !== "string") {
|
|
226
|
+
return `prefs.update payload.${key}.provider must be a string when provided`;
|
|
227
|
+
}
|
|
228
|
+
if (model !== void 0 && typeof model !== "string") {
|
|
229
|
+
return `prefs.update payload.${key}.model must be a string when provided`;
|
|
230
|
+
}
|
|
231
|
+
if (fallbackProfile !== void 0 && typeof fallbackProfile !== "string") {
|
|
232
|
+
return `prefs.update payload.${key}.fallbackProfile must be a string when provided`;
|
|
233
|
+
}
|
|
234
|
+
if (model === void 0 && fallbackProfile === void 0) {
|
|
235
|
+
return `prefs.update payload.${key} entries require model or fallbackProfile`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
210
240
|
const allowed = ENUM_PREF_KEYS[key];
|
|
211
241
|
if (allowed) {
|
|
212
242
|
return typeof value === "string" && allowed.has(value) ? null : `prefs.update payload.${key} must be one of: ${Array.from(allowed).join(", ")}`;
|
|
@@ -435,8 +465,8 @@ function validateShellOpenPayload(payload) {
|
|
|
435
465
|
if (!isRecord(payload)) {
|
|
436
466
|
return { ok: false, message: "shell.open payload must be an object with string path" };
|
|
437
467
|
}
|
|
438
|
-
const
|
|
439
|
-
if (typeof
|
|
468
|
+
const path18 = payload["path"];
|
|
469
|
+
if (typeof path18 !== "string" || path18.trim().length === 0) {
|
|
440
470
|
return { ok: false, message: "shell.open payload.path must be a non-empty string" };
|
|
441
471
|
}
|
|
442
472
|
const target = payload["target"];
|
|
@@ -446,20 +476,20 @@ function validateShellOpenPayload(payload) {
|
|
|
446
476
|
message: 'shell.open payload.target must be "file" or "terminal" when provided'
|
|
447
477
|
};
|
|
448
478
|
}
|
|
449
|
-
return { ok: true, value: { path:
|
|
479
|
+
return { ok: true, value: { path: path18, target } };
|
|
450
480
|
}
|
|
451
481
|
function validateGitDiffPayload(payload) {
|
|
452
482
|
if (!isRecord(payload)) {
|
|
453
483
|
return { ok: false, message: "git.diff payload must be an object" };
|
|
454
484
|
}
|
|
455
|
-
const
|
|
456
|
-
if (
|
|
485
|
+
const path18 = payload["path"];
|
|
486
|
+
if (path18 === void 0 || path18 === null) {
|
|
457
487
|
return { ok: true, value: { path: "" } };
|
|
458
488
|
}
|
|
459
|
-
if (typeof
|
|
489
|
+
if (typeof path18 !== "string") {
|
|
460
490
|
return { ok: false, message: "git.diff payload.path must be a string when provided" };
|
|
461
491
|
}
|
|
462
|
-
return { ok: true, value: { path:
|
|
492
|
+
return { ok: true, value: { path: path18 } };
|
|
463
493
|
}
|
|
464
494
|
function validateProjectsAddPayload(payload) {
|
|
465
495
|
if (!isRecord(payload)) {
|
|
@@ -709,8 +739,57 @@ async function handleWorklistMessage(ctx, ws, msg) {
|
|
|
709
739
|
|
|
710
740
|
// src/server/index.ts
|
|
711
741
|
import { makeMailboxTool, makeMailSendTool, makeMailInboxTool, mailboxSessionTag } from "@wrongstack/core";
|
|
712
|
-
import { toErrorMessage as toErrorMessage6, wstackGlobalRoot as
|
|
742
|
+
import { toErrorMessage as toErrorMessage6, wstackGlobalRoot as wstackGlobalRoot3, projectHash } from "@wrongstack/core/utils";
|
|
713
743
|
import { SkillInstaller } from "@wrongstack/core/skills";
|
|
744
|
+
|
|
745
|
+
// src/server/discover-mailbox-bridge.ts
|
|
746
|
+
import { resolveProjectDir, wstackGlobalRoot } from "@wrongstack/core";
|
|
747
|
+
import { readLiveLock } from "@wrongstack/core/coordination";
|
|
748
|
+
async function discoverMailboxBridgeForWebui(params) {
|
|
749
|
+
const mode = params.config?.features?.mailboxBridge ?? "auto";
|
|
750
|
+
if (mode === "off") return;
|
|
751
|
+
const projectDir = resolveProjectDir(params.projectRoot, wstackGlobalRoot());
|
|
752
|
+
const result = await readLiveLock(projectDir);
|
|
753
|
+
switch (result.kind) {
|
|
754
|
+
case "live": {
|
|
755
|
+
params.logger.debug("webui joined existing mailbox bridge", {
|
|
756
|
+
url: result.lock.url,
|
|
757
|
+
lockPath: projectDir
|
|
758
|
+
});
|
|
759
|
+
params.ctx.meta["mailboxBridge"] = {
|
|
760
|
+
url: result.lock.url,
|
|
761
|
+
token: result.lock.token,
|
|
762
|
+
lockPath: projectDir,
|
|
763
|
+
childPid: null,
|
|
764
|
+
source: "joined"
|
|
765
|
+
};
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
case "probe-failed": {
|
|
769
|
+
params.logger.warn(
|
|
770
|
+
"mailbox bridge present but /healthz unreachable; webui will start without external-agent connectivity",
|
|
771
|
+
{ url: result.lock.url, lockPath: projectDir }
|
|
772
|
+
);
|
|
773
|
+
params.ctx.meta["mailboxBridge"] = {
|
|
774
|
+
url: result.lock.url,
|
|
775
|
+
token: result.lock.token,
|
|
776
|
+
lockPath: projectDir,
|
|
777
|
+
childPid: null,
|
|
778
|
+
source: "unhealthy"
|
|
779
|
+
};
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
case "absent": {
|
|
783
|
+
params.logger.info(
|
|
784
|
+
"no mailbox bridge running; webui will start without external-agent connectivity. Run `wstack mailbox serve` or a CLI surface to bring one up.",
|
|
785
|
+
{ projectDir }
|
|
786
|
+
);
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// src/server/index.ts
|
|
714
793
|
import {
|
|
715
794
|
BrainMonitor,
|
|
716
795
|
DefaultBrainArbiter,
|
|
@@ -718,8 +797,9 @@ import {
|
|
|
718
797
|
createAutonomyBrain,
|
|
719
798
|
createTieredBrainArbiter
|
|
720
799
|
} from "@wrongstack/core";
|
|
721
|
-
import * as
|
|
722
|
-
import * as
|
|
800
|
+
import * as fs14 from "fs/promises";
|
|
801
|
+
import * as path17 from "path";
|
|
802
|
+
import { createRequire as createRequire2 } from "module";
|
|
723
803
|
|
|
724
804
|
// src/server/http-server.ts
|
|
725
805
|
import * as fs from "fs/promises";
|
|
@@ -896,7 +976,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
896
976
|
return;
|
|
897
977
|
}
|
|
898
978
|
try {
|
|
899
|
-
const { SessionRegistry, resolveWstackPaths:
|
|
979
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
|
|
900
980
|
const registry = new SessionRegistry(globalRoot);
|
|
901
981
|
const entry = await registry.get(sessionId);
|
|
902
982
|
if (!entry) {
|
|
@@ -904,7 +984,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
904
984
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
905
985
|
return;
|
|
906
986
|
}
|
|
907
|
-
const paths =
|
|
987
|
+
const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
|
|
908
988
|
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
909
989
|
const reader = new DefaultSessionReader2({ store });
|
|
910
990
|
const rawEntries = [];
|
|
@@ -931,7 +1011,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
931
1011
|
}
|
|
932
1012
|
}
|
|
933
1013
|
function readJsonBody(req) {
|
|
934
|
-
return new Promise((
|
|
1014
|
+
return new Promise((resolve10, reject) => {
|
|
935
1015
|
let data = "";
|
|
936
1016
|
req.on("data", (chunk) => {
|
|
937
1017
|
data += chunk;
|
|
@@ -942,7 +1022,7 @@ function readJsonBody(req) {
|
|
|
942
1022
|
});
|
|
943
1023
|
req.on("end", () => {
|
|
944
1024
|
try {
|
|
945
|
-
|
|
1025
|
+
resolve10(data ? JSON.parse(data) : {});
|
|
946
1026
|
} catch (err) {
|
|
947
1027
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
948
1028
|
}
|
|
@@ -978,7 +1058,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
|
978
1058
|
const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
|
|
979
1059
|
const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
|
|
980
1060
|
try {
|
|
981
|
-
const { SessionRegistry, resolveWstackPaths:
|
|
1061
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
982
1062
|
const registry = new SessionRegistry(globalRoot);
|
|
983
1063
|
const entry = await registry.get(sessionId);
|
|
984
1064
|
if (!entry) {
|
|
@@ -986,7 +1066,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
|
986
1066
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
987
1067
|
return;
|
|
988
1068
|
}
|
|
989
|
-
const paths =
|
|
1069
|
+
const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
|
|
990
1070
|
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
991
1071
|
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
992
1072
|
const sent = await mailbox.send({ from, to, type, subject, body: text, priority });
|
|
@@ -1004,7 +1084,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
|
1004
1084
|
return;
|
|
1005
1085
|
}
|
|
1006
1086
|
try {
|
|
1007
|
-
const { SessionRegistry, resolveWstackPaths:
|
|
1087
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
1008
1088
|
const registry = new SessionRegistry(globalRoot);
|
|
1009
1089
|
const entry = await registry.get(sessionId);
|
|
1010
1090
|
if (!entry) {
|
|
@@ -1012,7 +1092,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
|
1012
1092
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
1013
1093
|
return;
|
|
1014
1094
|
}
|
|
1015
|
-
const paths =
|
|
1095
|
+
const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
|
|
1016
1096
|
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
1017
1097
|
const leaderAddr = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
1018
1098
|
const [inbound, outbound] = await Promise.all([
|
|
@@ -1062,7 +1142,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
|
1062
1142
|
const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
|
|
1063
1143
|
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
1064
1144
|
try {
|
|
1065
|
-
const { SessionRegistry, resolveWstackPaths:
|
|
1145
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
1066
1146
|
const registry = new SessionRegistry(globalRoot);
|
|
1067
1147
|
const entry = await registry.get(sessionId);
|
|
1068
1148
|
if (!entry) {
|
|
@@ -1070,7 +1150,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
|
1070
1150
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
1071
1151
|
return;
|
|
1072
1152
|
}
|
|
1073
|
-
const paths =
|
|
1153
|
+
const paths = resolveWstackPaths4({ projectRoot: entry.projectRoot, globalRoot });
|
|
1074
1154
|
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
1075
1155
|
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
1076
1156
|
const sent = await mailbox.send({
|
|
@@ -1110,7 +1190,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
1110
1190
|
}
|
|
1111
1191
|
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
1112
1192
|
try {
|
|
1113
|
-
const { SessionRegistry, resolveWstackPaths:
|
|
1193
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths4, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
1114
1194
|
const registry = new SessionRegistry(globalRoot);
|
|
1115
1195
|
const all = await registry.list();
|
|
1116
1196
|
const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
|
|
@@ -1122,7 +1202,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
1122
1202
|
}
|
|
1123
1203
|
const mbByDir = /* @__PURE__ */ new Map();
|
|
1124
1204
|
const mailboxFor = (projectRoot) => {
|
|
1125
|
-
const dir =
|
|
1205
|
+
const dir = resolveWstackPaths4({ projectRoot, globalRoot }).projectDir;
|
|
1126
1206
|
let mb = mbByDir.get(dir);
|
|
1127
1207
|
if (!mb) {
|
|
1128
1208
|
mb = new GlobalMailbox3(dir);
|
|
@@ -1166,7 +1246,7 @@ function isTrustedLoopbackOrigin(origin) {
|
|
|
1166
1246
|
try {
|
|
1167
1247
|
const url = new URL(origin);
|
|
1168
1248
|
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
1169
|
-
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
1249
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
|
|
1170
1250
|
} catch {
|
|
1171
1251
|
return false;
|
|
1172
1252
|
}
|
|
@@ -1177,6 +1257,14 @@ function isLoopbackBind(wsHost) {
|
|
|
1177
1257
|
function isWildcardBind(wsHost) {
|
|
1178
1258
|
return wsHost === "0.0.0.0" || wsHost === "::" || wsHost === "[::]";
|
|
1179
1259
|
}
|
|
1260
|
+
function normalizeHostname(hostname) {
|
|
1261
|
+
const h = hostname.trim().toLowerCase();
|
|
1262
|
+
return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
1263
|
+
}
|
|
1264
|
+
function allowedHostname(hostname, allowedHostnames) {
|
|
1265
|
+
const normalized = normalizeHostname(hostname);
|
|
1266
|
+
return (allowedHostnames ?? []).some((candidate) => normalizeHostname(candidate) === normalized);
|
|
1267
|
+
}
|
|
1180
1268
|
function tokenMatches(provided, expected) {
|
|
1181
1269
|
if (!provided) return false;
|
|
1182
1270
|
const a = Buffer.from(provided);
|
|
@@ -1215,28 +1303,37 @@ function hostHeaderOk(input) {
|
|
|
1215
1303
|
} catch {
|
|
1216
1304
|
return false;
|
|
1217
1305
|
}
|
|
1218
|
-
return isLoopbackHostname(hostname);
|
|
1306
|
+
return isLoopbackHostname(hostname) || allowedHostname(hostname, input.allowedHostnames);
|
|
1219
1307
|
}
|
|
1220
1308
|
function verifyClient(input) {
|
|
1221
|
-
const {
|
|
1309
|
+
const {
|
|
1310
|
+
origin,
|
|
1311
|
+
url,
|
|
1312
|
+
hostHeader,
|
|
1313
|
+
remoteAddress,
|
|
1314
|
+
cookieHeader,
|
|
1315
|
+
wsHost,
|
|
1316
|
+
expectedToken,
|
|
1317
|
+
requireToken,
|
|
1318
|
+
allowedHostnames,
|
|
1319
|
+
allowBrowserUrlToken
|
|
1320
|
+
} = input;
|
|
1222
1321
|
const urlTokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
1223
1322
|
const cookieTokenOk = tokenMatches(extractTokenFromCookie(cookieHeader), expectedToken);
|
|
1224
|
-
if (!hostHeaderOk({ hostHeader, wsHost })) return false;
|
|
1323
|
+
if (!hostHeaderOk({ hostHeader, wsHost, allowedHostnames })) return false;
|
|
1225
1324
|
if (!origin) {
|
|
1226
1325
|
const remoteIp = remoteAddress ?? "";
|
|
1227
1326
|
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
1228
1327
|
if (!isRemoteLoopback && isWildcardBind(wsHost)) return false;
|
|
1229
|
-
return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost);
|
|
1328
|
+
return urlTokenOk || cookieTokenOk || isLoopbackBind(wsHost) && !requireToken;
|
|
1230
1329
|
}
|
|
1231
1330
|
try {
|
|
1232
|
-
const { hostname } = new URL(origin);
|
|
1233
|
-
if (isLoopbackHostname(
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1237
|
-
return true;
|
|
1331
|
+
const { hostname: originHostname } = new URL(origin);
|
|
1332
|
+
if (isLoopbackHostname(originHostname)) {
|
|
1333
|
+
if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
|
|
1334
|
+
return isTrustedLoopbackOrigin(origin);
|
|
1238
1335
|
}
|
|
1239
|
-
return cookieTokenOk;
|
|
1336
|
+
return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
|
|
1240
1337
|
} catch {
|
|
1241
1338
|
return false;
|
|
1242
1339
|
}
|
|
@@ -1262,8 +1359,69 @@ function injectWsPort(html, wsPort) {
|
|
|
1262
1359
|
return `${tag}
|
|
1263
1360
|
${html}`;
|
|
1264
1361
|
}
|
|
1265
|
-
function
|
|
1266
|
-
return
|
|
1362
|
+
function escapeHtmlAttr(value) {
|
|
1363
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1364
|
+
}
|
|
1365
|
+
function injectWsConfig(html, opts) {
|
|
1366
|
+
let out = injectWsPort(html, opts.wsPort);
|
|
1367
|
+
if (!opts.publicWsUrl || out.includes('name="wrongstack-ws-url"')) return out;
|
|
1368
|
+
const tag = `<meta name="wrongstack-ws-url" content="${escapeHtmlAttr(opts.publicWsUrl)}" />`;
|
|
1369
|
+
if (out.includes("</head>")) {
|
|
1370
|
+
return out.replace("</head>", ` ${tag}
|
|
1371
|
+
</head>`);
|
|
1372
|
+
}
|
|
1373
|
+
return `${tag}
|
|
1374
|
+
${out}`;
|
|
1375
|
+
}
|
|
1376
|
+
function firstHeader(value) {
|
|
1377
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1378
|
+
}
|
|
1379
|
+
function wsTokenCookie(token) {
|
|
1380
|
+
return `ws_token=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600`;
|
|
1381
|
+
}
|
|
1382
|
+
function requestToken(req, url) {
|
|
1383
|
+
return url.searchParams.get("token") ?? firstHeader(req.headers["x-ws-token"]) ?? extractTokenFromCookie(req.headers.cookie);
|
|
1384
|
+
}
|
|
1385
|
+
function requestHostForCsp(hostHeader) {
|
|
1386
|
+
const raw = firstHeader(hostHeader)?.trim();
|
|
1387
|
+
if (!raw) return void 0;
|
|
1388
|
+
try {
|
|
1389
|
+
return new URL(`http://${raw}`).hostname;
|
|
1390
|
+
} catch {
|
|
1391
|
+
return void 0;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
function formatCspHostname(hostname) {
|
|
1395
|
+
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
1396
|
+
}
|
|
1397
|
+
function cspSourceFromUrl(rawUrl) {
|
|
1398
|
+
try {
|
|
1399
|
+
const url = new URL(rawUrl);
|
|
1400
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") return void 0;
|
|
1401
|
+
return `${url.protocol}//${formatCspHostname(url.hostname)}${url.port ? `:${url.port}` : ""}`;
|
|
1402
|
+
} catch {
|
|
1403
|
+
return void 0;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
var ALLOWED_INLINE_SCRIPT_HASHES = [
|
|
1407
|
+
"'sha256-6PXDy0zrpXa6mvYOl11bZ8nubNUL7ushPUhGDZtaexg='",
|
|
1408
|
+
"'sha256-6sIdwbEBx7jj0drqSHHm7MqvmoYD3CQ4lp8Zp8blcb0='"
|
|
1409
|
+
];
|
|
1410
|
+
function buildCspHeader(wsPort, requestHost, publicWsUrl) {
|
|
1411
|
+
const connect = /* @__PURE__ */ new Set([
|
|
1412
|
+
"'self'",
|
|
1413
|
+
`ws://127.0.0.1:${wsPort}`,
|
|
1414
|
+
`wss://127.0.0.1:${wsPort}`
|
|
1415
|
+
]);
|
|
1416
|
+
if (requestHost && requestHost !== "127.0.0.1" && requestHost !== "::1" && requestHost !== "[::1]") {
|
|
1417
|
+
const host = formatCspHostname(requestHost);
|
|
1418
|
+
connect.add(`ws://${host}:${wsPort}`);
|
|
1419
|
+
connect.add(`wss://${host}:${wsPort}`);
|
|
1420
|
+
}
|
|
1421
|
+
const publicWsSource = publicWsUrl ? cspSourceFromUrl(publicWsUrl) : void 0;
|
|
1422
|
+
if (publicWsSource) connect.add(publicWsSource);
|
|
1423
|
+
const scriptSrc = ["'self'", ...ALLOWED_INLINE_SCRIPT_HASHES].join(" ");
|
|
1424
|
+
return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; connect-src ${Array.from(connect).join(" ")}; img-src 'self' data:; font-src 'self' data:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
|
|
1267
1425
|
}
|
|
1268
1426
|
function isInsideDist(candidate, distDir) {
|
|
1269
1427
|
const root = path.resolve(distDir);
|
|
@@ -1281,12 +1439,15 @@ function createHttpServer(opts) {
|
|
|
1281
1439
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
1282
1440
|
const distDir = path.resolve(opts.distDir);
|
|
1283
1441
|
const wsPort = opts.wsPort;
|
|
1284
|
-
const
|
|
1442
|
+
const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
|
|
1285
1443
|
return http.createServer(async (req, res) => {
|
|
1286
1444
|
try {
|
|
1287
1445
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
1446
|
+
const providedAccessToken = requestToken(req, url);
|
|
1447
|
+
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
1448
|
+
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
1288
1449
|
if (url.pathname === "/ws-auth" && req.method === "GET" && (opts.enableWsCookie ?? true)) {
|
|
1289
|
-
const provided = url
|
|
1450
|
+
const provided = requestToken(req, url);
|
|
1290
1451
|
if (!provided || !opts.apiToken || !tokenMatches(provided, opts.apiToken)) {
|
|
1291
1452
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
1292
1453
|
res.end("Unauthorized");
|
|
@@ -1294,7 +1455,7 @@ function createHttpServer(opts) {
|
|
|
1294
1455
|
}
|
|
1295
1456
|
res.writeHead(200, {
|
|
1296
1457
|
"Content-Type": "text/plain",
|
|
1297
|
-
"Set-Cookie":
|
|
1458
|
+
"Set-Cookie": wsTokenCookie(opts.apiToken),
|
|
1298
1459
|
// Belt-and-braces: tell any caches the cookie response itself
|
|
1299
1460
|
// is sensitive.
|
|
1300
1461
|
"Cache-Control": "no-store"
|
|
@@ -1302,10 +1463,20 @@ function createHttpServer(opts) {
|
|
|
1302
1463
|
res.end("ok");
|
|
1303
1464
|
return;
|
|
1304
1465
|
}
|
|
1466
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1467
|
+
res.writeHead(401, {
|
|
1468
|
+
"Content-Type": "text/plain",
|
|
1469
|
+
"Cache-Control": "no-store"
|
|
1470
|
+
});
|
|
1471
|
+
res.end("Unauthorized");
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (shouldSetAuthCookie && opts.apiToken) {
|
|
1475
|
+
res.setHeader("Set-Cookie", wsTokenCookie(opts.apiToken));
|
|
1476
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1477
|
+
}
|
|
1305
1478
|
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
1306
|
-
|
|
1307
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1308
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1479
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1309
1480
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1310
1481
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1311
1482
|
return;
|
|
@@ -1319,9 +1490,7 @@ function createHttpServer(opts) {
|
|
|
1319
1490
|
return;
|
|
1320
1491
|
}
|
|
1321
1492
|
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
1322
|
-
|
|
1323
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1324
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1493
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1325
1494
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1326
1495
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1327
1496
|
return;
|
|
@@ -1331,9 +1500,7 @@ function createHttpServer(opts) {
|
|
|
1331
1500
|
}
|
|
1332
1501
|
const agentsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/agents$/);
|
|
1333
1502
|
if (agentsMatch && req.method === "GET") {
|
|
1334
|
-
|
|
1335
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1336
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1503
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1337
1504
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1338
1505
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1339
1506
|
return;
|
|
@@ -1343,9 +1510,7 @@ function createHttpServer(opts) {
|
|
|
1343
1510
|
}
|
|
1344
1511
|
const eventsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
|
|
1345
1512
|
if (eventsMatch && req.method === "GET") {
|
|
1346
|
-
|
|
1347
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1348
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1513
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1349
1514
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1350
1515
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1351
1516
|
return;
|
|
@@ -1357,9 +1522,7 @@ function createHttpServer(opts) {
|
|
|
1357
1522
|
}
|
|
1358
1523
|
const msgMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/message$/);
|
|
1359
1524
|
if (msgMatch && req.method === "POST") {
|
|
1360
|
-
|
|
1361
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1362
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1525
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1363
1526
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1364
1527
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1365
1528
|
return;
|
|
@@ -1369,9 +1532,7 @@ function createHttpServer(opts) {
|
|
|
1369
1532
|
}
|
|
1370
1533
|
const mailboxMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/mailbox$/);
|
|
1371
1534
|
if (mailboxMatch && req.method === "GET") {
|
|
1372
|
-
|
|
1373
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1374
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1535
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1375
1536
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1376
1537
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1377
1538
|
return;
|
|
@@ -1381,9 +1542,7 @@ function createHttpServer(opts) {
|
|
|
1381
1542
|
}
|
|
1382
1543
|
const interruptMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/interrupt$/);
|
|
1383
1544
|
if (interruptMatch && req.method === "POST") {
|
|
1384
|
-
|
|
1385
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1386
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1545
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1387
1546
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1388
1547
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1389
1548
|
return;
|
|
@@ -1397,9 +1556,7 @@ function createHttpServer(opts) {
|
|
|
1397
1556
|
return;
|
|
1398
1557
|
}
|
|
1399
1558
|
if (url.pathname === "/api/fleet/broadcast" && req.method === "POST") {
|
|
1400
|
-
|
|
1401
|
-
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
1402
|
-
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
1559
|
+
if (requireAccessToken && !accessTokenOk) {
|
|
1403
1560
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1404
1561
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1405
1562
|
return;
|
|
@@ -1446,11 +1603,14 @@ function createHttpServer(opts) {
|
|
|
1446
1603
|
res.setHeader("X-Frame-Options", "DENY");
|
|
1447
1604
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1448
1605
|
if (ext === ".html") {
|
|
1449
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
1450
|
-
res.setHeader(
|
|
1606
|
+
if (!shouldSetAuthCookie) res.setHeader("Cache-Control", "no-cache");
|
|
1607
|
+
res.setHeader(
|
|
1608
|
+
"Content-Security-Policy",
|
|
1609
|
+
buildCspHeader(wsPort, requestHostForCsp(req.headers.host), opts.publicWsUrl)
|
|
1610
|
+
);
|
|
1451
1611
|
const html = await fs.readFile(resolvedPath, "utf8");
|
|
1452
1612
|
res.writeHead(200);
|
|
1453
|
-
res.end(
|
|
1613
|
+
res.end(injectWsConfig(html, { wsPort, publicWsUrl: opts.publicWsUrl }));
|
|
1454
1614
|
return;
|
|
1455
1615
|
}
|
|
1456
1616
|
const fileContent = await fs.readFile(resolvedPath);
|
|
@@ -1465,9 +1625,13 @@ function createHttpServer(opts) {
|
|
|
1465
1625
|
"X-Content-Type-Options": "nosniff",
|
|
1466
1626
|
"X-Frame-Options": "DENY",
|
|
1467
1627
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
1468
|
-
"Content-Security-Policy": buildCspHeader(
|
|
1628
|
+
"Content-Security-Policy": buildCspHeader(
|
|
1629
|
+
wsPort,
|
|
1630
|
+
requestHostForCsp(req.headers.host),
|
|
1631
|
+
opts.publicWsUrl
|
|
1632
|
+
)
|
|
1469
1633
|
});
|
|
1470
|
-
res.end(
|
|
1634
|
+
res.end(injectWsConfig(html, { wsPort, publicWsUrl: opts.publicWsUrl }));
|
|
1471
1635
|
} catch {
|
|
1472
1636
|
res.writeHead(404);
|
|
1473
1637
|
res.end("Not found");
|
|
@@ -1699,6 +1863,37 @@ function errMessage(err) {
|
|
|
1699
1863
|
function generateAuthToken() {
|
|
1700
1864
|
return randomBytes(16).toString("hex");
|
|
1701
1865
|
}
|
|
1866
|
+
function resolveAuthToken(explicit) {
|
|
1867
|
+
const configured = explicit?.trim() || process.env["WEBUI_TOKEN"]?.trim() || process.env["WEBUI_AUTH_TOKEN"]?.trim();
|
|
1868
|
+
return configured || generateAuthToken();
|
|
1869
|
+
}
|
|
1870
|
+
function hostForBrowserUrl(bindHost) {
|
|
1871
|
+
if (bindHost === "0.0.0.0") return "127.0.0.1";
|
|
1872
|
+
if (bindHost === "::" || bindHost === "[::]") return "[::1]";
|
|
1873
|
+
if (bindHost.includes(":") && !bindHost.startsWith("[")) return `[${bindHost}]`;
|
|
1874
|
+
return bindHost;
|
|
1875
|
+
}
|
|
1876
|
+
function buildWebUIAccessUrl(opts) {
|
|
1877
|
+
const protocol = opts.protocol ?? "http";
|
|
1878
|
+
const base = opts.publicUrl?.trim() || `${protocol}://${hostForBrowserUrl(opts.host)}:${opts.port}`;
|
|
1879
|
+
if (!opts.token) return base;
|
|
1880
|
+
try {
|
|
1881
|
+
const url = new URL(base);
|
|
1882
|
+
url.searchParams.set("token", opts.token);
|
|
1883
|
+
const rendered = url.toString();
|
|
1884
|
+
const afterOrigin = base.slice(url.origin.length);
|
|
1885
|
+
if (url.pathname === "/" && !afterOrigin.startsWith("/")) {
|
|
1886
|
+
return `${url.origin}${url.search}${url.hash}`;
|
|
1887
|
+
}
|
|
1888
|
+
return rendered;
|
|
1889
|
+
} catch {
|
|
1890
|
+
return `${base}${base.includes("?") ? "&" : "?"}token=${encodeURIComponent(opts.token)}`;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
function envFlag(name2) {
|
|
1894
|
+
const value = process.env[name2]?.trim().toLowerCase();
|
|
1895
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
1896
|
+
}
|
|
1702
1897
|
|
|
1703
1898
|
// src/server/file-handlers.ts
|
|
1704
1899
|
async function resolveFileInsideProject(projectRoot, filePath) {
|
|
@@ -1743,6 +1938,16 @@ async function realpathAllowMissing(p) {
|
|
|
1743
1938
|
}
|
|
1744
1939
|
}
|
|
1745
1940
|
}
|
|
1941
|
+
function validatedPayload(msg, label) {
|
|
1942
|
+
if (msg == null || typeof msg !== "object") {
|
|
1943
|
+
throw new TypeError(`Expected object for ${label}, got ${msg}`);
|
|
1944
|
+
}
|
|
1945
|
+
const payload = msg.payload;
|
|
1946
|
+
if (payload == null || typeof payload !== "object") {
|
|
1947
|
+
throw new TypeError(`Expected payload object for ${label}, got ${payload}`);
|
|
1948
|
+
}
|
|
1949
|
+
return payload;
|
|
1950
|
+
}
|
|
1746
1951
|
async function handleFilesTree(ws, msg, projectRoot) {
|
|
1747
1952
|
const payload = msg.payload;
|
|
1748
1953
|
const rawPath = payload?.path?.trim();
|
|
@@ -1813,7 +2018,13 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1813
2018
|
}
|
|
1814
2019
|
}
|
|
1815
2020
|
async function handleFilesRead(ws, msg, projectRoot) {
|
|
1816
|
-
|
|
2021
|
+
let filePath;
|
|
2022
|
+
try {
|
|
2023
|
+
({ filePath } = validatedPayload(msg, "files.read"));
|
|
2024
|
+
} catch {
|
|
2025
|
+
send(ws, { type: "files.read", payload: { filePath: "", content: "", error: "Malformed request" } });
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
1817
2028
|
let realResolved;
|
|
1818
2029
|
try {
|
|
1819
2030
|
realResolved = await resolveFileInsideProject(projectRoot, filePath);
|
|
@@ -1832,7 +2043,14 @@ async function handleFilesRead(ws, msg, projectRoot) {
|
|
|
1832
2043
|
}
|
|
1833
2044
|
}
|
|
1834
2045
|
async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
|
|
1835
|
-
|
|
2046
|
+
let filePath;
|
|
2047
|
+
let content;
|
|
2048
|
+
try {
|
|
2049
|
+
({ filePath, content } = validatedPayload(msg, "files.write"));
|
|
2050
|
+
} catch {
|
|
2051
|
+
send(ws, { type: "files.written", payload: { filePath: "", success: false, error: "Malformed request" } });
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
1836
2054
|
let realResolved;
|
|
1837
2055
|
try {
|
|
1838
2056
|
realResolved = await resolveFileInsideProject(projectRoot, filePath);
|
|
@@ -2670,7 +2888,7 @@ import { promises as fs5 } from "fs";
|
|
|
2670
2888
|
import path6 from "path";
|
|
2671
2889
|
import JSZip from "jszip";
|
|
2672
2890
|
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
2673
|
-
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
2891
|
+
import { wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
|
|
2674
2892
|
async function handleSkillsList(ws, ctx) {
|
|
2675
2893
|
if (!ctx.skillLoader) {
|
|
2676
2894
|
send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
|
|
@@ -2840,7 +3058,7 @@ async function handleSkillsCreate(ws, ctx, msg) {
|
|
|
2840
3058
|
}
|
|
2841
3059
|
const createPayload = parsed.value;
|
|
2842
3060
|
try {
|
|
2843
|
-
const targetDir = createPayload.scope === "global" ? path6.join(
|
|
3061
|
+
const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot2(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
2844
3062
|
try {
|
|
2845
3063
|
await fs5.access(targetDir);
|
|
2846
3064
|
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
@@ -2960,6 +3178,416 @@ async function handleSkillsExport(ws, ctx) {
|
|
|
2960
3178
|
}
|
|
2961
3179
|
}
|
|
2962
3180
|
|
|
3181
|
+
// src/server/prompts-handlers.ts
|
|
3182
|
+
function parseVariablesPayload(raw) {
|
|
3183
|
+
if (!Array.isArray(raw)) return void 0;
|
|
3184
|
+
const out = [];
|
|
3185
|
+
for (const item of raw) {
|
|
3186
|
+
if (!item || typeof item !== "object") continue;
|
|
3187
|
+
const o = item;
|
|
3188
|
+
if (typeof o["name"] !== "string" || !o["name"].trim()) continue;
|
|
3189
|
+
const enumVals = Array.isArray(o["enum"]) && o["enum"].every((x) => typeof x === "string") ? o["enum"].map((s) => s.trim()).filter(Boolean) : void 0;
|
|
3190
|
+
const v = { name: o["name"].trim() };
|
|
3191
|
+
if (typeof o["description"] === "string" && o["description"].trim()) {
|
|
3192
|
+
v.description = o["description"].trim();
|
|
3193
|
+
}
|
|
3194
|
+
if (o["required"] === true) v.required = true;
|
|
3195
|
+
if (o["multiline"] === true) v.multiline = true;
|
|
3196
|
+
if (enumVals && enumVals.length > 0) v.enum = enumVals;
|
|
3197
|
+
out.push(v);
|
|
3198
|
+
}
|
|
3199
|
+
return out.length > 0 ? out : void 0;
|
|
3200
|
+
}
|
|
3201
|
+
function toMeta(e) {
|
|
3202
|
+
return {
|
|
3203
|
+
id: e.id,
|
|
3204
|
+
slug: e.slug,
|
|
3205
|
+
title: e.title,
|
|
3206
|
+
description: e.description,
|
|
3207
|
+
category: e.category,
|
|
3208
|
+
tags: e.tags,
|
|
3209
|
+
source: e.source,
|
|
3210
|
+
favorite: e.favorite,
|
|
3211
|
+
variables: e.variables ?? []
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
async function handlePromptsList(ws, ctx) {
|
|
3215
|
+
if (!ctx.promptLoader) {
|
|
3216
|
+
send(ws, { type: "prompts.list", payload: { enabled: false, prompts: [], categories: [] } });
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
try {
|
|
3220
|
+
const [all, categories] = await Promise.all([
|
|
3221
|
+
ctx.promptLoader.list(),
|
|
3222
|
+
ctx.promptLoader.categories()
|
|
3223
|
+
]);
|
|
3224
|
+
send(ws, {
|
|
3225
|
+
type: "prompts.list",
|
|
3226
|
+
payload: { enabled: true, prompts: all.map(toMeta), categories }
|
|
3227
|
+
});
|
|
3228
|
+
} catch (err) {
|
|
3229
|
+
send(ws, {
|
|
3230
|
+
type: "prompts.list",
|
|
3231
|
+
payload: { enabled: true, prompts: [], categories: [], error: errMessage(err) }
|
|
3232
|
+
});
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
async function handlePromptsSearch(ws, ctx, msg) {
|
|
3236
|
+
if (!ctx.promptLoader) {
|
|
3237
|
+
send(ws, { type: "prompts.search", payload: { enabled: false, prompts: [] } });
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
const payload = msg.payload ?? {};
|
|
3241
|
+
try {
|
|
3242
|
+
const results = await ctx.promptLoader.search(payload.query ?? "", {
|
|
3243
|
+
...payload.category ? { category: payload.category } : {},
|
|
3244
|
+
limit: 50
|
|
3245
|
+
});
|
|
3246
|
+
send(ws, { type: "prompts.search", payload: { enabled: true, prompts: results.map(toMeta) } });
|
|
3247
|
+
} catch (err) {
|
|
3248
|
+
send(ws, {
|
|
3249
|
+
type: "prompts.search",
|
|
3250
|
+
payload: { enabled: true, prompts: [], error: errMessage(err) }
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
async function handlePromptsContent(ws, ctx, msg) {
|
|
3255
|
+
const slug = msg.payload?.slug;
|
|
3256
|
+
if (!ctx.promptLoader || !slug) {
|
|
3257
|
+
send(ws, {
|
|
3258
|
+
type: "prompts.content",
|
|
3259
|
+
payload: { slug: slug ?? "", found: false, content: "", variables: [] }
|
|
3260
|
+
});
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
try {
|
|
3264
|
+
const entry = await ctx.promptLoader.find(slug);
|
|
3265
|
+
if (!entry) {
|
|
3266
|
+
send(ws, {
|
|
3267
|
+
type: "prompts.content",
|
|
3268
|
+
payload: { slug, found: false, content: "", variables: [] }
|
|
3269
|
+
});
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
send(ws, {
|
|
3273
|
+
type: "prompts.content",
|
|
3274
|
+
payload: {
|
|
3275
|
+
slug: entry.slug,
|
|
3276
|
+
found: true,
|
|
3277
|
+
title: entry.title,
|
|
3278
|
+
content: entry.content,
|
|
3279
|
+
variables: entry.variables ?? [],
|
|
3280
|
+
category: entry.category,
|
|
3281
|
+
source: entry.source
|
|
3282
|
+
}
|
|
3283
|
+
});
|
|
3284
|
+
} catch (err) {
|
|
3285
|
+
send(ws, {
|
|
3286
|
+
type: "prompts.content",
|
|
3287
|
+
payload: { slug, found: false, content: "", variables: [], error: errMessage(err) }
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
async function handlePromptsFavorite(ws, ctx, msg) {
|
|
3292
|
+
const payload = msg.payload;
|
|
3293
|
+
if (!ctx.promptLoader || !payload?.slug) {
|
|
3294
|
+
send(ws, {
|
|
3295
|
+
type: "prompts.favorite",
|
|
3296
|
+
payload: { success: false, error: "Prompt library unavailable" }
|
|
3297
|
+
});
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
try {
|
|
3301
|
+
const updated = await ctx.promptLoader.setFavorite(payload.slug, payload.favorite !== false);
|
|
3302
|
+
if (!updated) {
|
|
3303
|
+
send(ws, {
|
|
3304
|
+
type: "prompts.favorite",
|
|
3305
|
+
payload: { success: false, error: "Prompt not found" }
|
|
3306
|
+
});
|
|
3307
|
+
return;
|
|
3308
|
+
}
|
|
3309
|
+
send(ws, {
|
|
3310
|
+
type: "prompts.favorite",
|
|
3311
|
+
payload: { success: true, slug: updated.slug, favorite: updated.favorite }
|
|
3312
|
+
});
|
|
3313
|
+
} catch (err) {
|
|
3314
|
+
send(ws, { type: "prompts.favorite", payload: { success: false, error: errMessage(err) } });
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
async function handlePromptsCreate(ws, ctx, msg) {
|
|
3318
|
+
const p = msg.payload;
|
|
3319
|
+
if (!ctx.promptLoader || !p) {
|
|
3320
|
+
send(ws, {
|
|
3321
|
+
type: "prompts.created",
|
|
3322
|
+
payload: { success: false, error: "Prompt library unavailable" }
|
|
3323
|
+
});
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
const title = typeof p["title"] === "string" ? p["title"].trim() : "";
|
|
3327
|
+
const content = typeof p["content"] === "string" ? p["content"] : "";
|
|
3328
|
+
if (!title || !content) {
|
|
3329
|
+
send(ws, {
|
|
3330
|
+
type: "prompts.created",
|
|
3331
|
+
payload: { success: false, error: "Title and content are required" }
|
|
3332
|
+
});
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
try {
|
|
3336
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3337
|
+
const tags = Array.isArray(p["tags"]) ? p["tags"].filter((t) => typeof t === "string") : [];
|
|
3338
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "prompt";
|
|
3339
|
+
const variables = parseVariablesPayload(p["variables"]);
|
|
3340
|
+
const entry = {
|
|
3341
|
+
id: slug,
|
|
3342
|
+
slug,
|
|
3343
|
+
title,
|
|
3344
|
+
description: typeof p["description"] === "string" ? p["description"] : "",
|
|
3345
|
+
content,
|
|
3346
|
+
category: typeof p["category"] === "string" && p["category"] ? p["category"] : "uncategorized",
|
|
3347
|
+
tags,
|
|
3348
|
+
source: "user",
|
|
3349
|
+
favorite: false,
|
|
3350
|
+
...variables ? { variables } : {},
|
|
3351
|
+
createdAt: now,
|
|
3352
|
+
updatedAt: now
|
|
3353
|
+
};
|
|
3354
|
+
await ctx.promptLoader.save(entry);
|
|
3355
|
+
send(ws, { type: "prompts.created", payload: { success: true, slug } });
|
|
3356
|
+
} catch (err) {
|
|
3357
|
+
send(ws, { type: "prompts.created", payload: { success: false, error: errMessage(err) } });
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
async function handlePromptsUsed(ws, ctx, msg) {
|
|
3361
|
+
const slug = msg.payload?.slug;
|
|
3362
|
+
if (!ctx.promptUsage || !slug) {
|
|
3363
|
+
send(ws, { type: "prompts.used", payload: { success: false } });
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
try {
|
|
3367
|
+
await ctx.promptUsage.record(slug);
|
|
3368
|
+
send(ws, { type: "prompts.used", payload: { success: true, slug } });
|
|
3369
|
+
} catch {
|
|
3370
|
+
send(ws, { type: "prompts.used", payload: { success: false } });
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
async function handlePromptsRecent(ws, ctx) {
|
|
3374
|
+
if (!ctx.promptUsage) {
|
|
3375
|
+
send(ws, { type: "prompts.recent", payload: { slugs: [] } });
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3378
|
+
try {
|
|
3379
|
+
const recent = await ctx.promptUsage.recent(50);
|
|
3380
|
+
send(ws, { type: "prompts.recent", payload: { slugs: recent.map((r) => r.slug) } });
|
|
3381
|
+
} catch (err) {
|
|
3382
|
+
send(ws, { type: "prompts.recent", payload: { slugs: [], error: errMessage(err) } });
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
// src/server/design-handlers.ts
|
|
3387
|
+
import * as fs6 from "fs/promises";
|
|
3388
|
+
import * as path7 from "path";
|
|
3389
|
+
import {
|
|
3390
|
+
applyTokenOverrides,
|
|
3391
|
+
getDesignKitLoader,
|
|
3392
|
+
getDesignState,
|
|
3393
|
+
isDesignStack,
|
|
3394
|
+
loadActiveKit,
|
|
3395
|
+
materializeTokens,
|
|
3396
|
+
recordKitChoice,
|
|
3397
|
+
recordOverrides,
|
|
3398
|
+
runDesignVerify,
|
|
3399
|
+
setActiveKit,
|
|
3400
|
+
setDesignOverrides
|
|
3401
|
+
} from "@wrongstack/core";
|
|
3402
|
+
function readOverrides(value) {
|
|
3403
|
+
const out = {};
|
|
3404
|
+
if (value && typeof value === "object") {
|
|
3405
|
+
for (const [k, v] of Object.entries(value)) {
|
|
3406
|
+
if (typeof v === "string") out[k] = v;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
return out;
|
|
3410
|
+
}
|
|
3411
|
+
var FOUNDATIONS_ID = "_foundations";
|
|
3412
|
+
async function buildListPayload(ctx) {
|
|
3413
|
+
const loader = getDesignKitLoader(ctx.projectRoot);
|
|
3414
|
+
const manifests = (await loader.list()).filter((k) => k.id !== FOUNDATIONS_ID);
|
|
3415
|
+
const kits = [];
|
|
3416
|
+
for (const m of manifests) {
|
|
3417
|
+
const tokens = await loader.readTokens(m.id);
|
|
3418
|
+
kits.push({
|
|
3419
|
+
id: m.id,
|
|
3420
|
+
name: m.name,
|
|
3421
|
+
aesthetic: m.aesthetic,
|
|
3422
|
+
bestFor: m.bestFor,
|
|
3423
|
+
stacks: m.stacks,
|
|
3424
|
+
tags: m.tags,
|
|
3425
|
+
light: tokens?.light ?? {},
|
|
3426
|
+
dark: tokens?.dark ?? {}
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
3429
|
+
const state = ctx.agentMeta ? getDesignState(ctx.agentMeta) : void 0;
|
|
3430
|
+
const persisted = await loadActiveKit(ctx.projectRoot).catch(() => void 0);
|
|
3431
|
+
return {
|
|
3432
|
+
kits,
|
|
3433
|
+
activeKit: state?.activeKit ?? persisted?.kit ?? null,
|
|
3434
|
+
stack: state?.stack ?? persisted?.stack ?? null,
|
|
3435
|
+
overrides: state?.overrides ?? persisted?.overrides ?? {}
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
3438
|
+
async function handleDesignList(ws, ctx) {
|
|
3439
|
+
try {
|
|
3440
|
+
send(ws, { type: "design.list", payload: await buildListPayload(ctx) });
|
|
3441
|
+
} catch (err) {
|
|
3442
|
+
send(ws, {
|
|
3443
|
+
type: "design.list",
|
|
3444
|
+
payload: { kits: [], activeKit: null, stack: null, error: String(err) }
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
async function handleDesignState(ws, ctx) {
|
|
3449
|
+
const state = ctx.agentMeta ? getDesignState(ctx.agentMeta) : void 0;
|
|
3450
|
+
send(ws, {
|
|
3451
|
+
type: "design.state",
|
|
3452
|
+
payload: {
|
|
3453
|
+
activeKit: state?.activeKit ?? null,
|
|
3454
|
+
stack: state?.stack ?? null,
|
|
3455
|
+
overrides: state?.overrides ?? {}
|
|
3456
|
+
}
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
async function handleDesignUse(ws, ctx, msg) {
|
|
3460
|
+
const payload = msg.payload ?? {};
|
|
3461
|
+
const kitId = typeof payload.kit === "string" ? payload.kit.trim() : "";
|
|
3462
|
+
if (!kitId) {
|
|
3463
|
+
send(ws, { type: "design.use", payload: { ok: false, error: "No kit id provided" } });
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
try {
|
|
3467
|
+
const loader = getDesignKitLoader(ctx.projectRoot);
|
|
3468
|
+
const kit = await loader.find(kitId);
|
|
3469
|
+
if (!kit) {
|
|
3470
|
+
send(ws, { type: "design.use", payload: { ok: false, kit: kitId, error: "Kit not found" } });
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
const stackArg = typeof payload.stack === "string" ? payload.stack : void 0;
|
|
3474
|
+
const stack = stackArg && isDesignStack(stackArg) ? stackArg : kit.stacks[0] ?? "web";
|
|
3475
|
+
const persisted = await loadActiveKit(ctx.projectRoot).catch(() => void 0);
|
|
3476
|
+
const keep = persisted?.kit === kit.id ? persisted.overrides ?? {} : {};
|
|
3477
|
+
const overrides = { ...keep, ...readOverrides(payload.overrides) };
|
|
3478
|
+
if (ctx.agentMeta) setActiveKit(ctx.agentMeta, kit.id, stack, overrides);
|
|
3479
|
+
await recordKitChoice(
|
|
3480
|
+
ctx.projectRoot,
|
|
3481
|
+
kit.id,
|
|
3482
|
+
stack,
|
|
3483
|
+
"webui",
|
|
3484
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
3485
|
+
Object.keys(overrides).length ? overrides : void 0
|
|
3486
|
+
);
|
|
3487
|
+
const body = await loader.readBody(kit.id, stack);
|
|
3488
|
+
const rawTokens = await loader.readTokens(kit.id);
|
|
3489
|
+
const tokens = rawTokens ? applyTokenOverrides(rawTokens, overrides) : rawTokens;
|
|
3490
|
+
send(ws, {
|
|
3491
|
+
type: "design.use",
|
|
3492
|
+
payload: {
|
|
3493
|
+
ok: true,
|
|
3494
|
+
kit: kit.id,
|
|
3495
|
+
name: kit.name,
|
|
3496
|
+
aesthetic: kit.aesthetic,
|
|
3497
|
+
stack,
|
|
3498
|
+
body,
|
|
3499
|
+
overrides,
|
|
3500
|
+
light: tokens?.light ?? {},
|
|
3501
|
+
dark: tokens?.dark ?? {}
|
|
3502
|
+
}
|
|
3503
|
+
});
|
|
3504
|
+
} catch (err) {
|
|
3505
|
+
send(ws, { type: "design.use", payload: { ok: false, kit: kitId, error: String(err) } });
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
async function handleDesignSet(ws, ctx, msg) {
|
|
3509
|
+
const patch = readOverrides(msg.payload?.overrides);
|
|
3510
|
+
if (Object.keys(patch).length === 0) {
|
|
3511
|
+
send(ws, { type: "design.set", payload: { ok: false, error: "No overrides provided" } });
|
|
3512
|
+
return;
|
|
3513
|
+
}
|
|
3514
|
+
try {
|
|
3515
|
+
const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
|
|
3516
|
+
if (!merged) {
|
|
3517
|
+
send(ws, { type: "design.set", payload: { ok: false, error: "No active kit" } });
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
if (ctx.agentMeta) setDesignOverrides(ctx.agentMeta, merged);
|
|
3521
|
+
send(ws, { type: "design.set", payload: { ok: true, overrides: merged } });
|
|
3522
|
+
} catch (err) {
|
|
3523
|
+
send(ws, { type: "design.set", payload: { ok: false, error: String(err) } });
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
async function handleDesignMaterialize(ws, ctx, msg) {
|
|
3527
|
+
const payload = msg.payload ?? {};
|
|
3528
|
+
try {
|
|
3529
|
+
const active = await loadActiveKit(ctx.projectRoot);
|
|
3530
|
+
if (!active) {
|
|
3531
|
+
send(ws, { type: "design.materialize", payload: { ok: false, error: "No active kit" } });
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
3534
|
+
const loader = getDesignKitLoader(ctx.projectRoot);
|
|
3535
|
+
const stackArg = typeof payload.stack === "string" ? payload.stack : void 0;
|
|
3536
|
+
const stack = stackArg && isDesignStack(stackArg) ? stackArg : active.stack && isDesignStack(active.stack) ? active.stack : "web";
|
|
3537
|
+
const raw = await loader.readTokens(active.kit);
|
|
3538
|
+
if (!raw) {
|
|
3539
|
+
send(ws, { type: "design.materialize", payload: { ok: false, error: "Kit has no tokens" } });
|
|
3540
|
+
return;
|
|
3541
|
+
}
|
|
3542
|
+
const tokens = applyTokenOverrides(raw, active.overrides);
|
|
3543
|
+
const result = materializeTokens({
|
|
3544
|
+
tokens,
|
|
3545
|
+
stack,
|
|
3546
|
+
kitId: active.kit,
|
|
3547
|
+
outPath: typeof payload.out === "string" ? payload.out : void 0
|
|
3548
|
+
});
|
|
3549
|
+
const abs = path7.join(ctx.projectRoot, result.path);
|
|
3550
|
+
await fs6.mkdir(path7.dirname(abs), { recursive: true });
|
|
3551
|
+
await fs6.writeFile(abs, result.content);
|
|
3552
|
+
send(ws, {
|
|
3553
|
+
type: "design.materialize",
|
|
3554
|
+
payload: { ok: true, path: result.path, format: result.format, stack }
|
|
3555
|
+
});
|
|
3556
|
+
} catch (err) {
|
|
3557
|
+
send(ws, { type: "design.materialize", payload: { ok: false, error: String(err) } });
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
async function handleDesignVerify(ws, ctx) {
|
|
3561
|
+
try {
|
|
3562
|
+
const active = await loadActiveKit(ctx.projectRoot);
|
|
3563
|
+
if (!active) {
|
|
3564
|
+
send(ws, { type: "design.verify", payload: { ok: false, error: "No active kit" } });
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
const loader = getDesignKitLoader(ctx.projectRoot);
|
|
3568
|
+
const raw = await loader.readTokens(active.kit);
|
|
3569
|
+
if (!raw) {
|
|
3570
|
+
send(ws, { type: "design.verify", payload: { ok: false, error: "Kit has no tokens" } });
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
const tokens = applyTokenOverrides(raw, active.overrides);
|
|
3574
|
+
const report = await runDesignVerify(ctx.projectRoot, tokens);
|
|
3575
|
+
send(ws, {
|
|
3576
|
+
type: "design.verify",
|
|
3577
|
+
payload: {
|
|
3578
|
+
ok: true,
|
|
3579
|
+
kit: active.kit,
|
|
3580
|
+
filesScanned: report.filesScanned,
|
|
3581
|
+
score: report.score,
|
|
3582
|
+
violations: report.violations.slice(0, 50),
|
|
3583
|
+
violationCount: report.violations.length
|
|
3584
|
+
}
|
|
3585
|
+
});
|
|
3586
|
+
} catch (err) {
|
|
3587
|
+
send(ws, { type: "design.verify", payload: { ok: false, error: String(err) } });
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
|
|
2963
3591
|
// src/server/index.ts
|
|
2964
3592
|
import {
|
|
2965
3593
|
Agent,
|
|
@@ -2971,6 +3599,8 @@ import {
|
|
|
2971
3599
|
DefaultSessionReader,
|
|
2972
3600
|
DefaultSessionStore as DefaultSessionStore2,
|
|
2973
3601
|
DefaultSkillLoader,
|
|
3602
|
+
DefaultPromptLoader,
|
|
3603
|
+
PromptUsageStore,
|
|
2974
3604
|
DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
|
|
2975
3605
|
DefaultTokenCounter,
|
|
2976
3606
|
AnnotationsStore,
|
|
@@ -2985,17 +3615,20 @@ import {
|
|
|
2985
3615
|
ToolRegistry,
|
|
2986
3616
|
atomicWrite as atomicWrite6,
|
|
2987
3617
|
createDefaultPipelines,
|
|
3618
|
+
installDesignStudioMiddleware,
|
|
2988
3619
|
createSessionEventBridge,
|
|
2989
3620
|
resolveSessionLoggingConfig,
|
|
2990
3621
|
DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2,
|
|
2991
3622
|
DEFAULT_SESSION_PRUNE_DAYS,
|
|
2992
3623
|
DEFAULT_TOOLS_CONFIG,
|
|
2993
3624
|
applyToolDescriptionModes,
|
|
3625
|
+
applyToolResultRenderModes,
|
|
2994
3626
|
resolveContextWindowPolicy as resolveContextWindowPolicy2,
|
|
2995
3627
|
enhanceUserPrompt,
|
|
2996
3628
|
gatedEnhancerReasoning,
|
|
2997
3629
|
recentTextTurns,
|
|
2998
|
-
resolveProviderModelList
|
|
3630
|
+
resolveProviderModelList,
|
|
3631
|
+
cleanupStaleSddWorktrees as cleanupStaleSddWorktrees3
|
|
2999
3632
|
} from "@wrongstack/core";
|
|
3000
3633
|
import { ToolExecutor } from "@wrongstack/core/execution";
|
|
3001
3634
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
|
|
@@ -3030,6 +3663,13 @@ import {
|
|
|
3030
3663
|
PhaseStore,
|
|
3031
3664
|
WorktreeManager
|
|
3032
3665
|
} from "@wrongstack/core";
|
|
3666
|
+
function deriveTitle(goal) {
|
|
3667
|
+
const firstLine = goal.split("\n").map((l) => l.trim()).find(Boolean);
|
|
3668
|
+
if (!firstLine) return "AutoPhase";
|
|
3669
|
+
const sentence = firstLine.split(/(?<=[.!?])\s/)[0] ?? firstLine;
|
|
3670
|
+
const trimmed = sentence.length <= 64 ? sentence : `${sentence.slice(0, 63).trimEnd()}\u2026`;
|
|
3671
|
+
return trimmed || "AutoPhase";
|
|
3672
|
+
}
|
|
3033
3673
|
function isGitRepo(cwd) {
|
|
3034
3674
|
try {
|
|
3035
3675
|
const r = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, encoding: "utf8", windowsHide: true });
|
|
@@ -3038,6 +3678,19 @@ function isGitRepo(cwd) {
|
|
|
3038
3678
|
return false;
|
|
3039
3679
|
}
|
|
3040
3680
|
}
|
|
3681
|
+
function commitsSince(cwd, baseSha, branch) {
|
|
3682
|
+
try {
|
|
3683
|
+
const r = spawnSync("git", ["log", "--reverse", "--format=%H", `${baseSha}..${branch}`], {
|
|
3684
|
+
cwd,
|
|
3685
|
+
encoding: "utf8",
|
|
3686
|
+
windowsHide: true
|
|
3687
|
+
});
|
|
3688
|
+
if (r.status !== 0) return [];
|
|
3689
|
+
return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3690
|
+
} catch {
|
|
3691
|
+
return [];
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3041
3694
|
var AutoPhaseWebSocketHandler = class {
|
|
3042
3695
|
constructor(agent, context, logger, storeDir, events, projectRoot) {
|
|
3043
3696
|
this.agent = agent;
|
|
@@ -3057,10 +3710,17 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3057
3710
|
store;
|
|
3058
3711
|
clients = /* @__PURE__ */ new Set();
|
|
3059
3712
|
broadcastInterval = null;
|
|
3060
|
-
/** Aborts in-flight task agents when the run is stopped. */
|
|
3713
|
+
/** Aborts in-flight task agents AND the planning turn when the run is stopped. */
|
|
3061
3714
|
abort = null;
|
|
3715
|
+
/** Set the instant a stop/clear/revert is requested, so a planning turn that
|
|
3716
|
+
* resolves afterwards never launches the orchestrator (the abort alone can't
|
|
3717
|
+
* cover the window between the LLM call resolving and the orchestrator start). */
|
|
3718
|
+
stopping = false;
|
|
3062
3719
|
/** Optional per-phase git-worktree isolation (lazily created at start). */
|
|
3063
3720
|
worktrees = null;
|
|
3721
|
+
/** Base branch + tip SHA captured at run start so a revert can git-revert the
|
|
3722
|
+
* run's squash commits (history-preserving) instead of a destructive reset. */
|
|
3723
|
+
runBase = null;
|
|
3064
3724
|
/** Per-run worker identities so the board can show "who is on what". */
|
|
3065
3725
|
usedNicknames = /* @__PURE__ */ new Set();
|
|
3066
3726
|
addClient(ws) {
|
|
@@ -3084,11 +3744,13 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3084
3744
|
this.broadcast({ type: "autophase.resumed", payload: {} });
|
|
3085
3745
|
break;
|
|
3086
3746
|
case "autophase.stop":
|
|
3087
|
-
this.
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3747
|
+
await this.handleStop();
|
|
3748
|
+
break;
|
|
3749
|
+
case "autophase.clear":
|
|
3750
|
+
await this.handleClear();
|
|
3751
|
+
break;
|
|
3752
|
+
case "autophase.revert":
|
|
3753
|
+
await this.handleRevert();
|
|
3092
3754
|
break;
|
|
3093
3755
|
case "autophase.status":
|
|
3094
3756
|
this.broadcastState();
|
|
@@ -3165,17 +3827,27 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3165
3827
|
}
|
|
3166
3828
|
}
|
|
3167
3829
|
async handleStart(payload) {
|
|
3168
|
-
const
|
|
3830
|
+
const goal = payload?.goal || payload?.title || "Untitled Project";
|
|
3831
|
+
const title = deriveTitle(goal);
|
|
3169
3832
|
const autonomous = payload?.autonomous ?? true;
|
|
3170
|
-
|
|
3833
|
+
this.abort = new AbortController();
|
|
3834
|
+
this.stopping = false;
|
|
3835
|
+
const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, this.abort.signal);
|
|
3836
|
+
if (this.stopping || this.abort.signal.aborted) {
|
|
3837
|
+
this.broadcast({ type: "autophase.stopped", payload: { title } });
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3171
3840
|
this.logger.info(`[AutoPhase] Starting: ${title}`);
|
|
3172
|
-
const graph = await new PhaseGraphBuilder({ title, phases, autonomous }).build();
|
|
3841
|
+
const graph = await new PhaseGraphBuilder({ title, description: goal, phases, autonomous }).build();
|
|
3173
3842
|
this.graph = graph;
|
|
3174
|
-
this.abort = new AbortController();
|
|
3175
3843
|
await this.store.save(graph);
|
|
3176
|
-
|
|
3844
|
+
const useWorktrees = payload?.worktrees ?? process.env["WRONGSTACK_AUTOPHASE_WORKTREES"] !== "0";
|
|
3845
|
+
if (!this.worktrees && this.events && this.projectRoot && useWorktrees && isGitRepo(this.projectRoot)) {
|
|
3177
3846
|
this.worktrees = new WorktreeManager({ projectRoot: this.projectRoot, events: this.events });
|
|
3178
3847
|
}
|
|
3848
|
+
if (this.worktrees) {
|
|
3849
|
+
this.runBase = await this.worktrees.currentBase();
|
|
3850
|
+
}
|
|
3179
3851
|
this.orchestrator = new PhaseOrchestrator({
|
|
3180
3852
|
graph,
|
|
3181
3853
|
ctx: {
|
|
@@ -3222,6 +3894,62 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3222
3894
|
this.broadcast({ type: "autophase.failed", payload: { title, error: String(err) } });
|
|
3223
3895
|
});
|
|
3224
3896
|
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Halt the run NOW — at any phase. Sets `stopping` (so a planning turn that
|
|
3899
|
+
* resolves afterwards bails), aborts in-flight agents, stops the orchestrator
|
|
3900
|
+
* tick, and ends the live broadcast. The board is kept for review; use
|
|
3901
|
+
* `autophase.clear` to reset or `autophase.revert` to undo the changes.
|
|
3902
|
+
*/
|
|
3903
|
+
async handleStop() {
|
|
3904
|
+
this.stopping = true;
|
|
3905
|
+
this.abort?.abort();
|
|
3906
|
+
this.orchestrator?.stop();
|
|
3907
|
+
this.stopBroadcast();
|
|
3908
|
+
if (this.graph) await this.store.save(this.graph).catch(() => void 0);
|
|
3909
|
+
this.broadcast({ type: "autophase.stopped", payload: { title: this.graph?.title } });
|
|
3910
|
+
}
|
|
3911
|
+
/**
|
|
3912
|
+
* Stop + wipe: tear down phase worktrees and reset to an empty board so the UI
|
|
3913
|
+
* returns to the start screen ("new one"). Does NOT touch already-merged commits
|
|
3914
|
+
* on the base branch — that is `autophase.revert`.
|
|
3915
|
+
*/
|
|
3916
|
+
async handleClear() {
|
|
3917
|
+
await this.handleStop();
|
|
3918
|
+
if (this.worktrees) await this.worktrees.cleanupAllManaged().catch(() => void 0);
|
|
3919
|
+
this.orchestrator = null;
|
|
3920
|
+
this.graph = null;
|
|
3921
|
+
this.runBase = null;
|
|
3922
|
+
this.usedNicknames.clear();
|
|
3923
|
+
this.broadcast({ type: "autophase.cleared", payload: {} });
|
|
3924
|
+
this.broadcast({ type: "autophase.state", payload: this.buildState() });
|
|
3925
|
+
}
|
|
3926
|
+
/**
|
|
3927
|
+
* Stop + undo: remove phase worktrees, then history-preservingly `git revert`
|
|
3928
|
+
* every commit this run landed on the base branch (captured `runBase`..HEAD),
|
|
3929
|
+
* then reset to an empty board. Refuses (reports a reason) on a dirty tree or a
|
|
3930
|
+
* conflicting revert rather than leaving the tree half-reverted.
|
|
3931
|
+
*/
|
|
3932
|
+
async handleRevert() {
|
|
3933
|
+
await this.handleStop();
|
|
3934
|
+
if (!this.worktrees || !this.runBase || !this.projectRoot) {
|
|
3935
|
+
this.broadcast({
|
|
3936
|
+
type: "autophase.reverted",
|
|
3937
|
+
payload: { ok: false, reverted: 0, reason: "no git baseline was captured for this run" }
|
|
3938
|
+
});
|
|
3939
|
+
return;
|
|
3940
|
+
}
|
|
3941
|
+
await this.worktrees.cleanupAllManaged().catch(() => void 0);
|
|
3942
|
+
const shas = commitsSince(this.projectRoot, this.runBase.sha, this.runBase.branch);
|
|
3943
|
+
const res = await this.worktrees.revertCommits(this.runBase.branch, shas);
|
|
3944
|
+
this.broadcast({ type: "autophase.reverted", payload: res });
|
|
3945
|
+
if (res.ok) {
|
|
3946
|
+
this.orchestrator = null;
|
|
3947
|
+
this.graph = null;
|
|
3948
|
+
this.runBase = null;
|
|
3949
|
+
this.broadcast({ type: "autophase.cleared", payload: {} });
|
|
3950
|
+
this.broadcast({ type: "autophase.state", payload: this.buildState() });
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3225
3953
|
/** Generic fallback phases when the LLM planner produces nothing usable. */
|
|
3226
3954
|
defaultPhases() {
|
|
3227
3955
|
return [
|
|
@@ -3232,13 +3960,18 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3232
3960
|
{ name: "Deployment", description: "Deploy to production", priority: "medium", estimateHours: 2, parallelizable: false }
|
|
3233
3961
|
];
|
|
3234
3962
|
}
|
|
3235
|
-
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
3236
|
-
|
|
3963
|
+
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
3964
|
+
* The caller passes the run's abort signal so a stop during planning cancels
|
|
3965
|
+
* the LLM turn (the previous fresh, never-aborted controller made planning
|
|
3966
|
+
* uninterruptible). */
|
|
3967
|
+
async planPhases(goal, signal) {
|
|
3237
3968
|
try {
|
|
3238
3969
|
const planner = new AutoPhasePlanner({
|
|
3239
3970
|
goal,
|
|
3240
3971
|
runOnce: async (prompt) => {
|
|
3241
|
-
const result = await this.agent.run(prompt, {
|
|
3972
|
+
const result = await this.agent.run(prompt, {
|
|
3973
|
+
signal: signal ?? new AbortController().signal
|
|
3974
|
+
});
|
|
3242
3975
|
return result.status === "done" ? result.finalText ?? "" : "";
|
|
3243
3976
|
}
|
|
3244
3977
|
});
|
|
@@ -3373,6 +4106,10 @@ Type: ${task.type}`;
|
|
|
3373
4106
|
const lastError = lastFailed ? `${lastFailed.name}: ${lastFailed.metadata?.integrationError ?? "phase failed"}` : null;
|
|
3374
4107
|
return {
|
|
3375
4108
|
title: this.graph.title,
|
|
4109
|
+
// Full operator prompt, shown verbatim in a dedicated goal block (the
|
|
4110
|
+
// title is only a short derived heading). Fall back to the title for
|
|
4111
|
+
// legacy boards saved before the title/goal split.
|
|
4112
|
+
goal: this.graph.description || this.graph.title,
|
|
3376
4113
|
phases: phaseItems,
|
|
3377
4114
|
tasks: taskItems,
|
|
3378
4115
|
activePhaseId: currentActiveId,
|
|
@@ -3574,7 +4311,7 @@ var SpecsWebSocketHandler = class {
|
|
|
3574
4311
|
};
|
|
3575
4312
|
|
|
3576
4313
|
// src/server/sdd-board-ws-handler.ts
|
|
3577
|
-
import { SddBoardStore } from "@wrongstack/core";
|
|
4314
|
+
import { applySddLifecycle, SddBoardStore } from "@wrongstack/core";
|
|
3578
4315
|
var CONTROL_TYPES = /* @__PURE__ */ new Set([
|
|
3579
4316
|
"pause",
|
|
3580
4317
|
"resume",
|
|
@@ -3588,19 +4325,19 @@ var CONTROL_TYPES = /* @__PURE__ */ new Set([
|
|
|
3588
4325
|
"set_task_verification",
|
|
3589
4326
|
"cancel_task",
|
|
3590
4327
|
"delete_task",
|
|
3591
|
-
"split_task"
|
|
3592
|
-
// Lifecycle (pair with a prior `stop`): sweep worktrees / revert merged commits.
|
|
3593
|
-
"cleanup_worktrees",
|
|
3594
|
-
"rollback"
|
|
4328
|
+
"split_task"
|
|
3595
4329
|
]);
|
|
4330
|
+
var LIFECYCLE_TYPES = /* @__PURE__ */ new Set(["cleanup_worktrees", "rollback", "destroy"]);
|
|
3596
4331
|
var SddBoardWebSocketHandler = class {
|
|
3597
4332
|
store;
|
|
3598
4333
|
clients = /* @__PURE__ */ new Set();
|
|
4334
|
+
lifecycle;
|
|
3599
4335
|
latest = null;
|
|
3600
4336
|
poll = null;
|
|
3601
4337
|
unsub = null;
|
|
3602
|
-
constructor(boardsDir, events) {
|
|
4338
|
+
constructor(boardsDir, events, lifecycle) {
|
|
3603
4339
|
this.store = new SddBoardStore({ baseDir: boardsDir });
|
|
4340
|
+
this.lifecycle = lifecycle;
|
|
3604
4341
|
if (events) {
|
|
3605
4342
|
const handler = (e) => {
|
|
3606
4343
|
this.latest = e.snapshot;
|
|
@@ -3629,6 +4366,10 @@ var SddBoardWebSocketHandler = class {
|
|
|
3629
4366
|
return;
|
|
3630
4367
|
}
|
|
3631
4368
|
const action = msg.type.replace(/^sdd\.board\./, "");
|
|
4369
|
+
if (LIFECYCLE_TYPES.has(action)) {
|
|
4370
|
+
await this.applyLifecycle(action, msg.payload);
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
3632
4373
|
if (CONTROL_TYPES.has(action)) {
|
|
3633
4374
|
const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
|
|
3634
4375
|
if (runId) {
|
|
@@ -3640,6 +4381,40 @@ var SddBoardWebSocketHandler = class {
|
|
|
3640
4381
|
}
|
|
3641
4382
|
}
|
|
3642
4383
|
}
|
|
4384
|
+
/**
|
|
4385
|
+
* Apply a cleanup/rollback/destroy from disk and broadcast a structured
|
|
4386
|
+
* `sdd.board.lifecycle_result`. Refuses (no-op) while a run is still active —
|
|
4387
|
+
* the user must stop it first; the UI gates the buttons on `!active` and the
|
|
4388
|
+
* Destroy flow auto-stops then waits before sending `destroy`.
|
|
4389
|
+
*/
|
|
4390
|
+
async applyLifecycle(op, payload) {
|
|
4391
|
+
if (!this.lifecycle) {
|
|
4392
|
+
this.broadcast({
|
|
4393
|
+
type: "sdd.board.lifecycle_result",
|
|
4394
|
+
payload: { op, ok: false, reason: "Lifecycle operations are not available in this session." }
|
|
4395
|
+
});
|
|
4396
|
+
return;
|
|
4397
|
+
}
|
|
4398
|
+
if (this.latest && (this.latest.status === "running" || this.latest.status === "paused")) {
|
|
4399
|
+
this.broadcast({
|
|
4400
|
+
type: "sdd.board.lifecycle_result",
|
|
4401
|
+
payload: { op, ok: false, reason: "Stop the run first, then retry." }
|
|
4402
|
+
});
|
|
4403
|
+
return;
|
|
4404
|
+
}
|
|
4405
|
+
const runId = payload?.runId ?? this.latest?.runId;
|
|
4406
|
+
const result = await applySddLifecycle(op, {
|
|
4407
|
+
projectRoot: this.lifecycle.projectRoot,
|
|
4408
|
+
paths: this.lifecycle.paths,
|
|
4409
|
+
runId,
|
|
4410
|
+
revertMerged: payload?.revertMerged === true
|
|
4411
|
+
});
|
|
4412
|
+
this.broadcast({ type: "sdd.board.lifecycle_result", payload: result });
|
|
4413
|
+
if (op === "destroy" && result.ok) {
|
|
4414
|
+
this.latest = null;
|
|
4415
|
+
this.broadcast({ type: "sdd.board.snapshot", payload: null });
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
3643
4418
|
dispose() {
|
|
3644
4419
|
if (this.poll) clearInterval(this.poll);
|
|
3645
4420
|
this.unsub?.();
|
|
@@ -3683,6 +4458,12 @@ var SddBoardWebSocketHandler = class {
|
|
|
3683
4458
|
};
|
|
3684
4459
|
|
|
3685
4460
|
// src/server/sdd-wizard-ws-handler.ts
|
|
4461
|
+
function deriveTitle2(goal) {
|
|
4462
|
+
const firstLine = goal.split("\n").map((l) => l.trim()).find(Boolean);
|
|
4463
|
+
if (!firstLine) return "New SDD Project";
|
|
4464
|
+
const sentence = firstLine.split(/(?<=[.!?])\s/)[0] ?? firstLine;
|
|
4465
|
+
return sentence.length <= 64 ? sentence : `${sentence.slice(0, 63).trimEnd()}\u2026`;
|
|
4466
|
+
}
|
|
3686
4467
|
var SddWizardWebSocketHandler = class {
|
|
3687
4468
|
constructor(deps2) {
|
|
3688
4469
|
this.deps = deps2;
|
|
@@ -3721,7 +4502,8 @@ var SddWizardWebSocketHandler = class {
|
|
|
3721
4502
|
parallelSlots: msg.payload?.parallelSlots,
|
|
3722
4503
|
defaultModel: msg.payload?.model,
|
|
3723
4504
|
defaultProvider: msg.payload?.provider,
|
|
3724
|
-
fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0
|
|
4505
|
+
fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0,
|
|
4506
|
+
worktrees: typeof msg.payload?.worktrees === "boolean" ? msg.payload.worktrees : void 0
|
|
3725
4507
|
});
|
|
3726
4508
|
break;
|
|
3727
4509
|
}
|
|
@@ -3741,7 +4523,7 @@ var SddWizardWebSocketHandler = class {
|
|
|
3741
4523
|
}
|
|
3742
4524
|
if (this.busy) return;
|
|
3743
4525
|
this.driver = this.deps.makeDriver();
|
|
3744
|
-
const prompt = this.driver.start(goal);
|
|
4526
|
+
const prompt = this.driver.start(deriveTitle2(goal), goal);
|
|
3745
4527
|
await this.runTurn(prompt);
|
|
3746
4528
|
}
|
|
3747
4529
|
async onMessage(text) {
|
|
@@ -3812,9 +4594,10 @@ var SddWizardWebSocketHandler = class {
|
|
|
3812
4594
|
};
|
|
3813
4595
|
|
|
3814
4596
|
// src/server/sdd-wizard-wiring.ts
|
|
3815
|
-
import * as
|
|
4597
|
+
import * as path8 from "path";
|
|
3816
4598
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3817
4599
|
import {
|
|
4600
|
+
cleanupStaleSddWorktrees,
|
|
3818
4601
|
makeCommandVerifier,
|
|
3819
4602
|
makeLlmSubtaskGenerator,
|
|
3820
4603
|
SddBoardStore as SddBoardStore2,
|
|
@@ -3826,6 +4609,7 @@ import {
|
|
|
3826
4609
|
TaskGraphStore as TaskGraphStore2,
|
|
3827
4610
|
WorktreeManager as WorktreeManager2
|
|
3828
4611
|
} from "@wrongstack/core";
|
|
4612
|
+
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";
|
|
3829
4613
|
function buildSddWizardDeps(opts) {
|
|
3830
4614
|
const registry = new SddRunRegistry();
|
|
3831
4615
|
let isolatedSeq = 0;
|
|
@@ -3834,11 +4618,11 @@ function buildSddWizardDeps(opts) {
|
|
|
3834
4618
|
id: `sdd-${name2.toLowerCase().replace(/\s+/g, "-")}-${isolatedSeq++}`,
|
|
3835
4619
|
role: "executor",
|
|
3836
4620
|
name: name2,
|
|
3837
|
-
disabledTools: ["delegate"],
|
|
4621
|
+
disabledTools: ["delegate", "write", "edit", "patch", "bash", "exec"],
|
|
3838
4622
|
allowedCapabilities: ["fs.read", "net.outbound"]
|
|
3839
4623
|
});
|
|
3840
4624
|
try {
|
|
3841
|
-
const res = await result.agent.run([{ type: "text", text: prompt }]);
|
|
4625
|
+
const res = await result.agent.run([{ type: "text", text: PLANNING_ONLY_GUARD + prompt }]);
|
|
3842
4626
|
return res.finalText ?? "";
|
|
3843
4627
|
} finally {
|
|
3844
4628
|
await result.dispose?.();
|
|
@@ -3848,23 +4632,30 @@ function buildSddWizardDeps(opts) {
|
|
|
3848
4632
|
makeDriver: () => new SddInterviewDriver({
|
|
3849
4633
|
specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
|
|
3850
4634
|
graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
|
|
3851
|
-
sessionPath:
|
|
4635
|
+
sessionPath: path8.join(opts.paths.projectDir, "sdd-wizard-session.json")
|
|
3852
4636
|
}),
|
|
3853
4637
|
runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
|
|
3854
|
-
startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
|
|
4638
|
+
startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels, worktrees: useWorktrees }) => {
|
|
3855
4639
|
const graph = driver.getGraph();
|
|
3856
4640
|
const tracker = driver.getTracker();
|
|
3857
4641
|
if (!graph || !tracker) {
|
|
3858
4642
|
throw new Error("No task graph to run \u2014 finish the interview first.");
|
|
3859
4643
|
}
|
|
4644
|
+
const worktreesEnabled = useWorktrees ?? process.env["WRONGSTACK_SDD_WORKTREES"] !== "0";
|
|
3860
4645
|
let worktrees;
|
|
3861
|
-
if (
|
|
4646
|
+
if (worktreesEnabled) {
|
|
3862
4647
|
const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3863
4648
|
cwd: opts.projectRoot,
|
|
3864
4649
|
encoding: "utf8",
|
|
3865
4650
|
windowsHide: true
|
|
3866
4651
|
}).stdout?.trim() === "true";
|
|
3867
|
-
if (inGit)
|
|
4652
|
+
if (inGit) {
|
|
4653
|
+
await cleanupStaleSddWorktrees({
|
|
4654
|
+
projectRoot: opts.projectRoot,
|
|
4655
|
+
boardsDir: opts.paths.projectSddBoards
|
|
4656
|
+
}).catch(() => void 0);
|
|
4657
|
+
worktrees = new WorktreeManager2({ projectRoot: opts.projectRoot, events: opts.events });
|
|
4658
|
+
}
|
|
3868
4659
|
}
|
|
3869
4660
|
const boardStore = new SddBoardStore2({ baseDir: opts.paths.projectSddBoards });
|
|
3870
4661
|
const verifyTask = makeCommandVerifier();
|
|
@@ -4643,16 +5434,16 @@ var CollaborationWebSocketHandler = class {
|
|
|
4643
5434
|
};
|
|
4644
5435
|
|
|
4645
5436
|
// src/server/projects-manifest.ts
|
|
4646
|
-
import * as
|
|
4647
|
-
import * as
|
|
5437
|
+
import * as fs7 from "fs/promises";
|
|
5438
|
+
import * as path9 from "path";
|
|
4648
5439
|
import { projectSlug } from "@wrongstack/core";
|
|
4649
5440
|
function projectsJsonPath(globalConfigPath) {
|
|
4650
|
-
const base =
|
|
4651
|
-
return
|
|
5441
|
+
const base = path9.dirname(globalConfigPath);
|
|
5442
|
+
return path9.join(base, "projects.json");
|
|
4652
5443
|
}
|
|
4653
5444
|
async function loadManifest(globalConfigPath) {
|
|
4654
5445
|
try {
|
|
4655
|
-
const raw = await
|
|
5446
|
+
const raw = await fs7.readFile(projectsJsonPath(globalConfigPath), "utf8");
|
|
4656
5447
|
const parsed = JSON.parse(raw);
|
|
4657
5448
|
return { projects: parsed.projects ?? [] };
|
|
4658
5449
|
} catch {
|
|
@@ -4661,16 +5452,16 @@ async function loadManifest(globalConfigPath) {
|
|
|
4661
5452
|
}
|
|
4662
5453
|
async function saveManifest(manifest, globalConfigPath) {
|
|
4663
5454
|
const file = projectsJsonPath(globalConfigPath);
|
|
4664
|
-
await
|
|
4665
|
-
await
|
|
5455
|
+
await fs7.mkdir(path9.dirname(file), { recursive: true });
|
|
5456
|
+
await fs7.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
4666
5457
|
}
|
|
4667
5458
|
function generateProjectSlug(rootPath) {
|
|
4668
5459
|
return projectSlug(rootPath);
|
|
4669
5460
|
}
|
|
4670
5461
|
async function ensureProjectDataDir(slug, globalConfigPath) {
|
|
4671
|
-
const base =
|
|
4672
|
-
const dir =
|
|
4673
|
-
await
|
|
5462
|
+
const base = path9.dirname(globalConfigPath);
|
|
5463
|
+
const dir = path9.join(base, "projects", slug);
|
|
5464
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
4674
5465
|
return dir;
|
|
4675
5466
|
}
|
|
4676
5467
|
|
|
@@ -4832,16 +5623,22 @@ function clampDim(value, fallback) {
|
|
|
4832
5623
|
}
|
|
4833
5624
|
|
|
4834
5625
|
// src/server/worktree-ws-handler.ts
|
|
5626
|
+
import { join as join6, resolve as resolve6, sep as sep4 } from "path";
|
|
5627
|
+
import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2, WorktreeManager as WorktreeManager3 } from "@wrongstack/core";
|
|
4835
5628
|
import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
|
|
4836
5629
|
var MAX_ACTIVITY = 6;
|
|
5630
|
+
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["allocating", "active", "committing", "merging"]);
|
|
5631
|
+
var MANAGED_BRANCH_RE = /^wstack\/ap\/[A-Za-z0-9._/-]+$/;
|
|
4837
5632
|
var WorktreeWebSocketHandler = class {
|
|
4838
|
-
constructor(events, logger) {
|
|
5633
|
+
constructor(events, logger, management) {
|
|
4839
5634
|
this.events = events;
|
|
4840
5635
|
this.logger = logger;
|
|
5636
|
+
this.management = management;
|
|
4841
5637
|
this.subscribe();
|
|
4842
5638
|
}
|
|
4843
5639
|
events;
|
|
4844
5640
|
logger;
|
|
5641
|
+
management;
|
|
4845
5642
|
clients = /* @__PURE__ */ new Set();
|
|
4846
5643
|
handles = /* @__PURE__ */ new Map();
|
|
4847
5644
|
baseBranch = "";
|
|
@@ -4852,12 +5649,197 @@ var WorktreeWebSocketHandler = class {
|
|
|
4852
5649
|
ws.on("close", () => this.clients.delete(ws));
|
|
4853
5650
|
ws.on("error", () => this.clients.delete(ws));
|
|
4854
5651
|
this.send(ws, this.stateMessage());
|
|
5652
|
+
void this.scanAndBroadcast();
|
|
5653
|
+
}
|
|
5654
|
+
/** Handle worktree-panel control messages (scan / clean / per-row ops). */
|
|
5655
|
+
async handleMessage(msg) {
|
|
5656
|
+
if (msg.type === "worktree.scan") {
|
|
5657
|
+
await this.scanAndBroadcast();
|
|
5658
|
+
return true;
|
|
5659
|
+
}
|
|
5660
|
+
if (msg.type === "worktree.cleanup") {
|
|
5661
|
+
await this.cleanupOrphans();
|
|
5662
|
+
return true;
|
|
5663
|
+
}
|
|
5664
|
+
if (msg.type === "worktree.remove") {
|
|
5665
|
+
await this.removeOne(msg.payload?.["dir"], msg.payload?.["branch"]);
|
|
5666
|
+
return true;
|
|
5667
|
+
}
|
|
5668
|
+
if (msg.type === "worktree.merge") {
|
|
5669
|
+
await this.mergeBranch(msg.payload?.["branch"]);
|
|
5670
|
+
return true;
|
|
5671
|
+
}
|
|
5672
|
+
if (msg.type === "worktree.diff") {
|
|
5673
|
+
await this.diffOne(msg.payload?.["dir"], msg.payload?.["baseBranch"]);
|
|
5674
|
+
return true;
|
|
5675
|
+
}
|
|
5676
|
+
return false;
|
|
4855
5677
|
}
|
|
4856
5678
|
dispose() {
|
|
4857
5679
|
for (const off of this.offs) off();
|
|
4858
5680
|
this.offs.length = 0;
|
|
4859
5681
|
this.stopBroadcast();
|
|
4860
5682
|
}
|
|
5683
|
+
// ── orphan management ─────────────────────────────────────────────────────
|
|
5684
|
+
/** Absolute managed-worktrees root for this project. */
|
|
5685
|
+
worktreesRoot() {
|
|
5686
|
+
return resolve6(join6(this.management.projectRoot, ".wrongstack", "worktrees"));
|
|
5687
|
+
}
|
|
5688
|
+
/** True iff `dir` resolves strictly inside the managed worktrees root. */
|
|
5689
|
+
underRoot(dir) {
|
|
5690
|
+
const abs = resolve6(dir);
|
|
5691
|
+
const root = this.worktreesRoot();
|
|
5692
|
+
return abs !== root && abs.startsWith(root + sep4);
|
|
5693
|
+
}
|
|
5694
|
+
/** Branches of worktrees a live in-session run currently owns. */
|
|
5695
|
+
liveActiveBranches() {
|
|
5696
|
+
const live = /* @__PURE__ */ new Set();
|
|
5697
|
+
for (const h of this.handles.values()) {
|
|
5698
|
+
if (ACTIVE_STATUSES.has(h.status) && h.branch) live.add(h.branch);
|
|
5699
|
+
}
|
|
5700
|
+
return live;
|
|
5701
|
+
}
|
|
5702
|
+
/**
|
|
5703
|
+
* Scan the disk for managed worktrees/branches NOT owned by a live in-session
|
|
5704
|
+
* run and broadcast them as orphans, with whether it is safe to clean now.
|
|
5705
|
+
* No-op (empty inventory) when management deps were not wired.
|
|
5706
|
+
*/
|
|
5707
|
+
async scanAndBroadcast() {
|
|
5708
|
+
if (!this.management) {
|
|
5709
|
+
this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
|
|
5710
|
+
return;
|
|
5711
|
+
}
|
|
5712
|
+
try {
|
|
5713
|
+
const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
|
|
5714
|
+
const { worktrees, branches } = await wt.listManaged();
|
|
5715
|
+
const live = this.liveActiveBranches();
|
|
5716
|
+
const orphans = [];
|
|
5717
|
+
const seenBranches = /* @__PURE__ */ new Set();
|
|
5718
|
+
for (const w of worktrees) {
|
|
5719
|
+
if (w.branch && live.has(w.branch)) continue;
|
|
5720
|
+
if (w.branch) seenBranches.add(w.branch);
|
|
5721
|
+
orphans.push({ kind: "worktree", dir: w.dir, branch: w.branch });
|
|
5722
|
+
}
|
|
5723
|
+
for (const b of branches) {
|
|
5724
|
+
if (live.has(b) || seenBranches.has(b)) continue;
|
|
5725
|
+
orphans.push({ kind: "branch", branch: b });
|
|
5726
|
+
}
|
|
5727
|
+
const canClean = this.liveActiveBranches().size === 0;
|
|
5728
|
+
this.broadcast({
|
|
5729
|
+
type: "worktree.orphans",
|
|
5730
|
+
payload: {
|
|
5731
|
+
orphans,
|
|
5732
|
+
canClean,
|
|
5733
|
+
reason: canClean ? void 0 : "a run is live in this session"
|
|
5734
|
+
}
|
|
5735
|
+
});
|
|
5736
|
+
} catch (err) {
|
|
5737
|
+
this.logger.debug?.(`worktree orphan scan failed: ${toErrorMessage4(err)}`);
|
|
5738
|
+
this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
/**
|
|
5742
|
+
* Force-remove every orphaned worktree + branch. Refused while a run is live —
|
|
5743
|
+
* in this session (active handles) OR another process (the SDD board liveness
|
|
5744
|
+
* guard inside cleanupStaleSddWorktrees). Best-effort; reports the outcome.
|
|
5745
|
+
*/
|
|
5746
|
+
async cleanupOrphans() {
|
|
5747
|
+
if (!this.management) {
|
|
5748
|
+
this.broadcast({
|
|
5749
|
+
type: "worktree.cleanup_result",
|
|
5750
|
+
payload: { ok: false, removed: 0, reason: "cleanup is not available in this session" }
|
|
5751
|
+
});
|
|
5752
|
+
return;
|
|
5753
|
+
}
|
|
5754
|
+
if (this.liveActiveBranches().size > 0) {
|
|
5755
|
+
this.broadcast({
|
|
5756
|
+
type: "worktree.cleanup_result",
|
|
5757
|
+
payload: { ok: false, removed: 0, reason: "a run is live in this session \u2014 stop it first" }
|
|
5758
|
+
});
|
|
5759
|
+
return;
|
|
5760
|
+
}
|
|
5761
|
+
const res = await cleanupStaleSddWorktrees2({
|
|
5762
|
+
projectRoot: this.management.projectRoot,
|
|
5763
|
+
boardsDir: this.management.boardsDir
|
|
5764
|
+
});
|
|
5765
|
+
if (res.skippedReason) {
|
|
5766
|
+
this.broadcast({
|
|
5767
|
+
type: "worktree.cleanup_result",
|
|
5768
|
+
payload: { ok: false, removed: 0, reason: res.skippedReason }
|
|
5769
|
+
});
|
|
5770
|
+
await this.scanAndBroadcast();
|
|
5771
|
+
return;
|
|
5772
|
+
}
|
|
5773
|
+
for (const [id, h] of [...this.handles]) {
|
|
5774
|
+
if (!ACTIVE_STATUSES.has(h.status)) this.handles.delete(id);
|
|
5775
|
+
}
|
|
5776
|
+
this.broadcast({ type: "worktree.cleanup_result", payload: { ok: true, removed: res.removed } });
|
|
5777
|
+
this.broadcastState();
|
|
5778
|
+
await this.scanAndBroadcast();
|
|
5779
|
+
}
|
|
5780
|
+
/** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
|
|
5781
|
+
async removeOne(dir, branch) {
|
|
5782
|
+
if (!this.management || !dir && !branch) {
|
|
5783
|
+
this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "nothing to remove" } });
|
|
5784
|
+
return;
|
|
5785
|
+
}
|
|
5786
|
+
if (branch && !MANAGED_BRANCH_RE.test(branch)) {
|
|
5787
|
+
this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "not a managed worktree branch" } });
|
|
5788
|
+
return;
|
|
5789
|
+
}
|
|
5790
|
+
if (dir && !this.underRoot(dir)) {
|
|
5791
|
+
this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "path is outside the managed worktrees root" } });
|
|
5792
|
+
return;
|
|
5793
|
+
}
|
|
5794
|
+
if (branch && this.liveActiveBranches().has(branch)) {
|
|
5795
|
+
this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "a run is live on this worktree \u2014 stop it first" } });
|
|
5796
|
+
return;
|
|
5797
|
+
}
|
|
5798
|
+
let removed = false;
|
|
5799
|
+
if (dir) {
|
|
5800
|
+
const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
|
|
5801
|
+
({ removed } = await wt.removeOne(dir, branch));
|
|
5802
|
+
}
|
|
5803
|
+
for (const [id, h] of [...this.handles]) {
|
|
5804
|
+
if (branch && h.branch === branch || dir && h.handleId && dir.endsWith(h.handleId)) this.handles.delete(id);
|
|
5805
|
+
}
|
|
5806
|
+
this.broadcast({ type: "worktree.cleanup_result", payload: { ok: removed, removed: removed ? 1 : 0, reason: removed ? void 0 : "remove failed (not a managed worktree?)" } });
|
|
5807
|
+
this.broadcastState();
|
|
5808
|
+
await this.scanAndBroadcast();
|
|
5809
|
+
}
|
|
5810
|
+
/** Squash-merge ONE branch into base. Refused while a live run owns it. */
|
|
5811
|
+
async mergeBranch(branch) {
|
|
5812
|
+
if (!this.management || !branch) {
|
|
5813
|
+
this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch: branch ?? "", reason: "no branch" } });
|
|
5814
|
+
return;
|
|
5815
|
+
}
|
|
5816
|
+
if (!MANAGED_BRANCH_RE.test(branch)) {
|
|
5817
|
+
this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "not a managed worktree branch" } });
|
|
5818
|
+
return;
|
|
5819
|
+
}
|
|
5820
|
+
if (this.liveActiveBranches().has(branch)) {
|
|
5821
|
+
this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "a run is live on this worktree \u2014 stop it first" } });
|
|
5822
|
+
return;
|
|
5823
|
+
}
|
|
5824
|
+
const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
|
|
5825
|
+
const res = await wt.mergeBranch(branch);
|
|
5826
|
+
this.broadcast({
|
|
5827
|
+
type: "worktree.merge_result",
|
|
5828
|
+
payload: { ok: res.ok, branch, conflict: res.conflict, conflictFiles: res.conflictFiles, reason: res.reason }
|
|
5829
|
+
});
|
|
5830
|
+
await this.scanAndBroadcast();
|
|
5831
|
+
}
|
|
5832
|
+
/** Compact change summary for one worktree checkout. */
|
|
5833
|
+
async diffOne(dir, baseBranch) {
|
|
5834
|
+
if (!this.management || !dir || !this.underRoot(dir)) {
|
|
5835
|
+
this.broadcast({ type: "worktree.diff_result", payload: { dir: dir ?? "", summary: null } });
|
|
5836
|
+
return;
|
|
5837
|
+
}
|
|
5838
|
+
const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
|
|
5839
|
+
const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
|
|
5840
|
+
const summary = await wt.diffSummary(resolve6(dir), base);
|
|
5841
|
+
this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
|
|
5842
|
+
}
|
|
4861
5843
|
// ── internals ───────────────────────────────────────────────────────────
|
|
4862
5844
|
subscribe() {
|
|
4863
5845
|
const on = this.events.on.bind(this.events);
|
|
@@ -4869,6 +5851,7 @@ var WorktreeWebSocketHandler = class {
|
|
|
4869
5851
|
handleId: e.handleId,
|
|
4870
5852
|
ownerId: e.ownerId,
|
|
4871
5853
|
ownerLabel: e.ownerLabel,
|
|
5854
|
+
dir: e.dir,
|
|
4872
5855
|
branch: e.branch,
|
|
4873
5856
|
baseBranch: e.baseBranch,
|
|
4874
5857
|
status: "active",
|
|
@@ -4971,10 +5954,10 @@ var WorktreeWebSocketHandler = class {
|
|
|
4971
5954
|
};
|
|
4972
5955
|
|
|
4973
5956
|
// src/server/mailbox-handlers.ts
|
|
4974
|
-
import { GlobalMailbox, resolveProjectDir } from "@wrongstack/core";
|
|
5957
|
+
import { GlobalMailbox, resolveProjectDir as resolveProjectDir2 } from "@wrongstack/core";
|
|
4975
5958
|
async function handleMailboxMessages(ws, deps2, payload) {
|
|
4976
5959
|
try {
|
|
4977
|
-
const dir =
|
|
5960
|
+
const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
|
|
4978
5961
|
const mb = new GlobalMailbox(dir);
|
|
4979
5962
|
const messages = await mb.query({
|
|
4980
5963
|
limit: payload?.limit ?? 30,
|
|
@@ -5010,7 +5993,7 @@ async function handleMailboxMessages(ws, deps2, payload) {
|
|
|
5010
5993
|
}
|
|
5011
5994
|
async function handleMailboxAgents(ws, deps2, payload) {
|
|
5012
5995
|
try {
|
|
5013
|
-
const dir =
|
|
5996
|
+
const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
|
|
5014
5997
|
const mb = new GlobalMailbox(dir);
|
|
5015
5998
|
const agents = payload?.onlineOnly ? await mb.getOnlineAgents() : await mb.getAgentStatuses();
|
|
5016
5999
|
send(ws, {
|
|
@@ -5039,7 +6022,7 @@ async function handleMailboxAgents(ws, deps2, payload) {
|
|
|
5039
6022
|
}
|
|
5040
6023
|
async function handleMailboxClear(ws, deps2) {
|
|
5041
6024
|
try {
|
|
5042
|
-
const dir =
|
|
6025
|
+
const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
|
|
5043
6026
|
const mb = new GlobalMailbox(dir);
|
|
5044
6027
|
await mb.clearAll();
|
|
5045
6028
|
send(ws, { type: "mailbox.cleared", payload: {} });
|
|
@@ -5049,7 +6032,7 @@ async function handleMailboxClear(ws, deps2) {
|
|
|
5049
6032
|
}
|
|
5050
6033
|
async function handleMailboxPurge(ws, deps2, opts) {
|
|
5051
6034
|
try {
|
|
5052
|
-
const dir =
|
|
6035
|
+
const dir = resolveProjectDir2(deps2.projectRoot, deps2.globalRoot);
|
|
5053
6036
|
const mb = new GlobalMailbox(dir);
|
|
5054
6037
|
const result = await mb.purgeStale(opts);
|
|
5055
6038
|
send(ws, { type: "mailbox.purged", payload: result });
|
|
@@ -5096,14 +6079,14 @@ function registerShutdownHandlers(res) {
|
|
|
5096
6079
|
|
|
5097
6080
|
// src/server/instance-registry.ts
|
|
5098
6081
|
import * as os from "os";
|
|
5099
|
-
import * as
|
|
5100
|
-
import * as
|
|
6082
|
+
import * as path10 from "path";
|
|
6083
|
+
import * as fs8 from "fs/promises";
|
|
5101
6084
|
import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
|
|
5102
6085
|
function defaultBaseDir() {
|
|
5103
|
-
return
|
|
6086
|
+
return path10.join(os.homedir(), ".wrongstack");
|
|
5104
6087
|
}
|
|
5105
6088
|
function registryPath(baseDir = defaultBaseDir()) {
|
|
5106
|
-
return
|
|
6089
|
+
return path10.join(baseDir, "webui-instances.json");
|
|
5107
6090
|
}
|
|
5108
6091
|
function isPidAlive(pid) {
|
|
5109
6092
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -5116,7 +6099,7 @@ function isPidAlive(pid) {
|
|
|
5116
6099
|
}
|
|
5117
6100
|
async function load(file) {
|
|
5118
6101
|
try {
|
|
5119
|
-
const raw = await
|
|
6102
|
+
const raw = await fs8.readFile(file, "utf8");
|
|
5120
6103
|
const parsed = JSON.parse(raw);
|
|
5121
6104
|
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
5122
6105
|
return parsed;
|
|
@@ -5175,16 +6158,16 @@ function formatInstances(instances) {
|
|
|
5175
6158
|
// src/server/port-utils.ts
|
|
5176
6159
|
import * as net from "net";
|
|
5177
6160
|
function isPortFree(host, port) {
|
|
5178
|
-
return new Promise((
|
|
6161
|
+
return new Promise((resolve10) => {
|
|
5179
6162
|
const srv = net.createServer();
|
|
5180
|
-
srv.once("error", () =>
|
|
6163
|
+
srv.once("error", () => resolve10(false));
|
|
5181
6164
|
srv.once("listening", () => {
|
|
5182
|
-
srv.close(() =>
|
|
6165
|
+
srv.close(() => resolve10(true));
|
|
5183
6166
|
});
|
|
5184
6167
|
try {
|
|
5185
6168
|
srv.listen(port, host);
|
|
5186
6169
|
} catch {
|
|
5187
|
-
|
|
6170
|
+
resolve10(false);
|
|
5188
6171
|
}
|
|
5189
6172
|
});
|
|
5190
6173
|
}
|
|
@@ -5265,15 +6248,15 @@ import { DefaultSecretScrubber } from "@wrongstack/core";
|
|
|
5265
6248
|
import { probeLocalLlm } from "@wrongstack/runtime/probe";
|
|
5266
6249
|
|
|
5267
6250
|
// src/server/provider-config-io.ts
|
|
5268
|
-
import * as
|
|
5269
|
-
import * as
|
|
6251
|
+
import * as fs9 from "fs/promises";
|
|
6252
|
+
import * as path11 from "path";
|
|
5270
6253
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
5271
6254
|
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
5272
6255
|
import { DefaultSecretVault } from "@wrongstack/core";
|
|
5273
6256
|
async function loadSavedProviders(configPath, vault) {
|
|
5274
6257
|
let raw;
|
|
5275
6258
|
try {
|
|
5276
|
-
raw = await
|
|
6259
|
+
raw = await fs9.readFile(configPath, "utf8");
|
|
5277
6260
|
} catch {
|
|
5278
6261
|
return {};
|
|
5279
6262
|
}
|
|
@@ -5290,7 +6273,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
5290
6273
|
let raw;
|
|
5291
6274
|
let fileExists = true;
|
|
5292
6275
|
try {
|
|
5293
|
-
raw = await
|
|
6276
|
+
raw = await fs9.readFile(configPath, "utf8");
|
|
5294
6277
|
} catch (err) {
|
|
5295
6278
|
if (err.code !== "ENOENT") {
|
|
5296
6279
|
throw new Error(
|
|
@@ -5318,7 +6301,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
5318
6301
|
await atomicWrite4(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
5319
6302
|
}
|
|
5320
6303
|
function createProviderConfigIO(configPath) {
|
|
5321
|
-
const keyFile =
|
|
6304
|
+
const keyFile = path11.join(path11.dirname(configPath), ".key");
|
|
5322
6305
|
const vault = new DefaultSecretVault({ keyFile });
|
|
5323
6306
|
return {
|
|
5324
6307
|
load: () => loadSavedProviders(configPath, vault),
|
|
@@ -5636,7 +6619,8 @@ function createProviderHandlers(deps2) {
|
|
|
5636
6619
|
|
|
5637
6620
|
// src/server/mode-handlers.ts
|
|
5638
6621
|
import {
|
|
5639
|
-
DefaultSystemPromptBuilder
|
|
6622
|
+
DefaultSystemPromptBuilder,
|
|
6623
|
+
resolveWstackPaths
|
|
5640
6624
|
} from "@wrongstack/core";
|
|
5641
6625
|
function createModeHandlers(ctx) {
|
|
5642
6626
|
return {
|
|
@@ -5684,13 +6668,18 @@ function createModeHandlers(ctx) {
|
|
|
5684
6668
|
}
|
|
5685
6669
|
ctx.setModeId(id);
|
|
5686
6670
|
const modePrompt = id === "default" ? "" : (await ctx.modeStore.getMode(id))?.prompt ?? "";
|
|
6671
|
+
const paths = resolveWstackPaths({ projectRoot: ctx.projectRoot, globalRoot: ctx.globalRoot });
|
|
5687
6672
|
const freshBuilder = new DefaultSystemPromptBuilder({
|
|
5688
6673
|
memoryStore: ctx.memoryStore,
|
|
5689
6674
|
skillLoader: ctx.skillLoader,
|
|
5690
6675
|
modeStore: ctx.modeStore,
|
|
5691
6676
|
modeId: id,
|
|
5692
6677
|
modePrompt,
|
|
5693
|
-
modelCapabilities: ctx.modelCapabilities
|
|
6678
|
+
modelCapabilities: ctx.modelCapabilities,
|
|
6679
|
+
instructionPaths: {
|
|
6680
|
+
globalDir: paths.globalInstructions,
|
|
6681
|
+
projectDir: paths.inProjectInstructions
|
|
6682
|
+
}
|
|
5694
6683
|
});
|
|
5695
6684
|
ctx.context.systemPrompt = await freshBuilder.build({
|
|
5696
6685
|
cwd: ctx.projectRoot,
|
|
@@ -5712,12 +6701,13 @@ function createModeHandlers(ctx) {
|
|
|
5712
6701
|
}
|
|
5713
6702
|
|
|
5714
6703
|
// src/server/project-handlers.ts
|
|
5715
|
-
import * as
|
|
5716
|
-
import * as
|
|
6704
|
+
import * as fs10 from "fs/promises";
|
|
6705
|
+
import * as path12 from "path";
|
|
5717
6706
|
import {
|
|
5718
6707
|
DefaultSessionStore,
|
|
5719
6708
|
DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2,
|
|
5720
|
-
getSessionRegistry
|
|
6709
|
+
getSessionRegistry,
|
|
6710
|
+
resolveWstackPaths as resolveWstackPaths2
|
|
5721
6711
|
} from "@wrongstack/core";
|
|
5722
6712
|
function createProjectHandlers(ctx) {
|
|
5723
6713
|
return {
|
|
@@ -5740,9 +6730,9 @@ function createProjectHandlers(ctx) {
|
|
|
5740
6730
|
}
|
|
5741
6731
|
const { root: addRoot, name: displayName } = parsed.value;
|
|
5742
6732
|
try {
|
|
5743
|
-
const resolved =
|
|
5744
|
-
await
|
|
5745
|
-
const stat3 = await
|
|
6733
|
+
const resolved = path12.resolve(addRoot);
|
|
6734
|
+
await fs10.access(resolved);
|
|
6735
|
+
const stat3 = await fs10.stat(resolved);
|
|
5746
6736
|
if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
5747
6737
|
const manifest = await loadManifest(ctx.globalConfigPath);
|
|
5748
6738
|
const existing = manifest.projects.find((p) => p.root === resolved);
|
|
@@ -5758,7 +6748,7 @@ function createProjectHandlers(ctx) {
|
|
|
5758
6748
|
});
|
|
5759
6749
|
return;
|
|
5760
6750
|
}
|
|
5761
|
-
const name2 = displayName?.trim() ||
|
|
6751
|
+
const name2 = displayName?.trim() || path12.basename(resolved);
|
|
5762
6752
|
const slug = generateProjectSlug(resolved);
|
|
5763
6753
|
await ensureProjectDataDir(slug, ctx.globalConfigPath);
|
|
5764
6754
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5771,7 +6761,7 @@ function createProjectHandlers(ctx) {
|
|
|
5771
6761
|
} catch (err) {
|
|
5772
6762
|
send(ws, {
|
|
5773
6763
|
type: "projects.added",
|
|
5774
|
-
payload: { name:
|
|
6764
|
+
payload: { name: path12.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
|
|
5775
6765
|
});
|
|
5776
6766
|
}
|
|
5777
6767
|
},
|
|
@@ -5786,17 +6776,17 @@ function createProjectHandlers(ctx) {
|
|
|
5786
6776
|
}
|
|
5787
6777
|
const { root: selRoot, name: selName } = parsed.value;
|
|
5788
6778
|
try {
|
|
5789
|
-
const resolved =
|
|
6779
|
+
const resolved = path12.resolve(selRoot);
|
|
5790
6780
|
try {
|
|
5791
|
-
await
|
|
5792
|
-
const stat3 = await
|
|
6781
|
+
await fs10.access(resolved);
|
|
6782
|
+
const stat3 = await fs10.stat(resolved);
|
|
5793
6783
|
if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
5794
6784
|
} catch (err) {
|
|
5795
6785
|
send(ws, {
|
|
5796
6786
|
type: "projects.selected",
|
|
5797
6787
|
payload: {
|
|
5798
6788
|
root: selRoot,
|
|
5799
|
-
name: selName ||
|
|
6789
|
+
name: selName || path12.basename(selRoot),
|
|
5800
6790
|
message: `Cannot switch: ${errMessage(err)}`
|
|
5801
6791
|
}
|
|
5802
6792
|
});
|
|
@@ -5808,7 +6798,7 @@ function createProjectHandlers(ctx) {
|
|
|
5808
6798
|
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
5809
6799
|
entry.lastWorkingDir = resolved;
|
|
5810
6800
|
} else {
|
|
5811
|
-
const name2 = selName?.trim() ||
|
|
6801
|
+
const name2 = selName?.trim() || path12.basename(resolved);
|
|
5812
6802
|
const slug = generateProjectSlug(resolved);
|
|
5813
6803
|
manifest.projects.push({
|
|
5814
6804
|
name: name2,
|
|
@@ -5830,13 +6820,21 @@ function createProjectHandlers(ctx) {
|
|
|
5830
6820
|
try {
|
|
5831
6821
|
const modeId = ctx.getModeId();
|
|
5832
6822
|
const switchMode = modeId === "default" ? void 0 : await ctx.modeStore.getMode(modeId);
|
|
6823
|
+
const switchPaths = resolveWstackPaths2({
|
|
6824
|
+
projectRoot: resolved,
|
|
6825
|
+
globalRoot: ctx.wpaths.globalRoot
|
|
6826
|
+
});
|
|
5833
6827
|
const switchBuilder = new DefaultSystemPromptBuilder2({
|
|
5834
6828
|
memoryStore: ctx.memoryStore,
|
|
5835
6829
|
skillLoader: ctx.skillLoader,
|
|
5836
6830
|
modeStore: ctx.modeStore,
|
|
5837
6831
|
modeId,
|
|
5838
6832
|
modePrompt: switchMode?.prompt ?? "",
|
|
5839
|
-
modelCapabilities: ctx.modelCapabilities
|
|
6833
|
+
modelCapabilities: ctx.modelCapabilities,
|
|
6834
|
+
instructionPaths: {
|
|
6835
|
+
globalDir: switchPaths.globalInstructions,
|
|
6836
|
+
projectDir: switchPaths.inProjectInstructions
|
|
6837
|
+
}
|
|
5840
6838
|
});
|
|
5841
6839
|
ctx.context.systemPrompt = await switchBuilder.build({
|
|
5842
6840
|
cwd: resolved,
|
|
@@ -5847,13 +6845,13 @@ function createProjectHandlers(ctx) {
|
|
|
5847
6845
|
});
|
|
5848
6846
|
} catch {
|
|
5849
6847
|
}
|
|
5850
|
-
const newSessionsDir =
|
|
5851
|
-
|
|
6848
|
+
const newSessionsDir = path12.join(
|
|
6849
|
+
path12.dirname(ctx.globalConfigPath),
|
|
5852
6850
|
"projects",
|
|
5853
6851
|
switchSlug,
|
|
5854
6852
|
"sessions"
|
|
5855
6853
|
);
|
|
5856
|
-
await
|
|
6854
|
+
await fs10.mkdir(newSessionsDir, { recursive: true });
|
|
5857
6855
|
const newSessionStore = new DefaultSessionStore({ dir: newSessionsDir });
|
|
5858
6856
|
const oldSession = ctx.getSession();
|
|
5859
6857
|
const oldSessionId = oldSession.id;
|
|
@@ -5887,7 +6885,7 @@ function createProjectHandlers(ctx) {
|
|
|
5887
6885
|
sessionId: newSession.id,
|
|
5888
6886
|
projectSlug: switchSlug,
|
|
5889
6887
|
projectRoot: resolved,
|
|
5890
|
-
projectName:
|
|
6888
|
+
projectName: path12.basename(resolved),
|
|
5891
6889
|
workingDir: resolved,
|
|
5892
6890
|
clientType: "webui",
|
|
5893
6891
|
pid: process.pid,
|
|
@@ -5899,8 +6897,8 @@ function createProjectHandlers(ctx) {
|
|
|
5899
6897
|
type: "projects.selected",
|
|
5900
6898
|
payload: {
|
|
5901
6899
|
root: resolved,
|
|
5902
|
-
name: selName ||
|
|
5903
|
-
message: `Switched to ${selName ||
|
|
6900
|
+
name: selName || path12.basename(resolved),
|
|
6901
|
+
message: `Switched to ${selName || path12.basename(resolved)}`
|
|
5904
6902
|
}
|
|
5905
6903
|
});
|
|
5906
6904
|
broadcast(ctx.clients, {
|
|
@@ -5920,7 +6918,7 @@ function createProjectHandlers(ctx) {
|
|
|
5920
6918
|
type: "projects.selected",
|
|
5921
6919
|
payload: {
|
|
5922
6920
|
root: selRoot,
|
|
5923
|
-
name: selName ||
|
|
6921
|
+
name: selName || path12.basename(selRoot),
|
|
5924
6922
|
message: errMessage(err)
|
|
5925
6923
|
}
|
|
5926
6924
|
});
|
|
@@ -5951,7 +6949,7 @@ function createProjectHandlers(ctx) {
|
|
|
5951
6949
|
}
|
|
5952
6950
|
|
|
5953
6951
|
// src/server/session-handlers.ts
|
|
5954
|
-
import * as
|
|
6952
|
+
import * as path13 from "path";
|
|
5955
6953
|
import {
|
|
5956
6954
|
DEFAULT_CONTEXT_WINDOW_MODE_ID,
|
|
5957
6955
|
repairToolUseAdjacency,
|
|
@@ -6293,7 +7291,7 @@ function createSessionHandlers(ctx) {
|
|
|
6293
7291
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
6294
7292
|
const projectRoot = ctx.getProjectRoot();
|
|
6295
7293
|
const rewinder = new DefaultSessionRewinder(
|
|
6296
|
-
|
|
7294
|
+
path13.join(projectRoot, ".wrongstack", "sessions"),
|
|
6297
7295
|
projectRoot
|
|
6298
7296
|
);
|
|
6299
7297
|
const checkpoints = await rewinder.listCheckpoints(ctx.getSession().id);
|
|
@@ -6308,7 +7306,7 @@ function createSessionHandlers(ctx) {
|
|
|
6308
7306
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
6309
7307
|
const projectRoot = ctx.getProjectRoot();
|
|
6310
7308
|
const rewinder = new DefaultSessionRewinder(
|
|
6311
|
-
|
|
7309
|
+
path13.join(projectRoot, ".wrongstack", "sessions"),
|
|
6312
7310
|
projectRoot
|
|
6313
7311
|
);
|
|
6314
7312
|
await rewinder.rewindToCheckpoint(ctx.getSession().id, checkpointIndex);
|
|
@@ -6684,9 +7682,9 @@ async function handleSddBoardRoute(_ws, msg, handlers) {
|
|
|
6684
7682
|
}
|
|
6685
7683
|
|
|
6686
7684
|
// src/server/setup-events.ts
|
|
6687
|
-
import * as
|
|
7685
|
+
import * as fs11 from "fs/promises";
|
|
6688
7686
|
import { watch as fsWatch } from "fs";
|
|
6689
|
-
import * as
|
|
7687
|
+
import * as path14 from "path";
|
|
6690
7688
|
function setupEvents(deps2) {
|
|
6691
7689
|
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
|
|
6692
7690
|
const disposers = [];
|
|
@@ -6775,6 +7773,22 @@ function setupEvents(deps2) {
|
|
|
6775
7773
|
}).catch(() => {
|
|
6776
7774
|
});
|
|
6777
7775
|
broadcast2(clients, { type: "todos.updated", payload: { todos: [...context.todos] } });
|
|
7776
|
+
const sideEffects = context.sideEffects ?? [];
|
|
7777
|
+
if (sideEffects.length > 0) {
|
|
7778
|
+
broadcast2(clients, {
|
|
7779
|
+
type: "side_effects",
|
|
7780
|
+
payload: {
|
|
7781
|
+
sideEffects: sideEffects.slice(-50).map((se) => ({
|
|
7782
|
+
toolUseId: se.toolUseId,
|
|
7783
|
+
toolName: se.toolName,
|
|
7784
|
+
ts: se.ts,
|
|
7785
|
+
input: se.input,
|
|
7786
|
+
outcome: se.outcome,
|
|
7787
|
+
risk: se.risk
|
|
7788
|
+
}))
|
|
7789
|
+
}
|
|
7790
|
+
});
|
|
7791
|
+
}
|
|
6778
7792
|
if (e.name === "task" || e.name === "plan" || e.name === "todo") {
|
|
6779
7793
|
void (async () => {
|
|
6780
7794
|
try {
|
|
@@ -7153,16 +8167,16 @@ function setupEvents(deps2) {
|
|
|
7153
8167
|
if (wpaths?.projectStatus) {
|
|
7154
8168
|
try {
|
|
7155
8169
|
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
7156
|
-
const dir =
|
|
7157
|
-
await
|
|
7158
|
-
await
|
|
8170
|
+
const dir = path14.dirname(statusFile);
|
|
8171
|
+
await fs11.mkdir(dir, { recursive: true });
|
|
8172
|
+
await fs11.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
|
|
7159
8173
|
} catch (err) {
|
|
7160
8174
|
console.error("[setup-events] Failed to write status.json:", err);
|
|
7161
8175
|
}
|
|
7162
8176
|
}
|
|
7163
8177
|
});
|
|
7164
8178
|
if (wpaths?.projectStatus && wpaths.configDir) {
|
|
7165
|
-
const projectsDir =
|
|
8179
|
+
const projectsDir = path14.join(wpaths.configDir, "projects");
|
|
7166
8180
|
const knownProjectHashes = /* @__PURE__ */ new Set();
|
|
7167
8181
|
const debounceTimers = /* @__PURE__ */ new Map();
|
|
7168
8182
|
const DEBOUNCE_MS = 150;
|
|
@@ -7225,20 +8239,20 @@ function setupEvents(deps2) {
|
|
|
7225
8239
|
let watcher;
|
|
7226
8240
|
const startWatcher = async () => {
|
|
7227
8241
|
try {
|
|
7228
|
-
await
|
|
8242
|
+
await fs11.mkdir(projectsDir, { recursive: true });
|
|
7229
8243
|
watcher = fsWatch(projectsDir, { persistent: true, recursive: true }, async (eventType, filename) => {
|
|
7230
8244
|
if (eventType === "change") {
|
|
7231
8245
|
if (filename == null) return;
|
|
7232
8246
|
if (watcherMetrics) watcherMetrics.fileChangesDetected++;
|
|
7233
|
-
const targetFile =
|
|
8247
|
+
const targetFile = path14.join(projectsDir, String(filename));
|
|
7234
8248
|
if (targetFile.endsWith("status.json")) {
|
|
7235
|
-
const projectHash2 =
|
|
8249
|
+
const projectHash2 = path14.basename(path14.dirname(targetFile));
|
|
7236
8250
|
if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
|
|
7237
8251
|
return;
|
|
7238
8252
|
}
|
|
7239
8253
|
if (watcherMetrics) watcherMetrics.filesProcessed++;
|
|
7240
8254
|
try {
|
|
7241
|
-
const content = await
|
|
8255
|
+
const content = await fs11.readFile(targetFile, "utf-8");
|
|
7242
8256
|
const statusData = JSON.parse(content);
|
|
7243
8257
|
if (statusData.projectHash) {
|
|
7244
8258
|
const hash = String(statusData.projectHash);
|
|
@@ -7290,7 +8304,7 @@ function setupEvents(deps2) {
|
|
|
7290
8304
|
}
|
|
7291
8305
|
});
|
|
7292
8306
|
}
|
|
7293
|
-
const globalRoot = globalConfigPath ?
|
|
8307
|
+
const globalRoot = globalConfigPath ? path14.dirname(globalConfigPath) : void 0;
|
|
7294
8308
|
if (globalRoot) {
|
|
7295
8309
|
const broadcastSessions = async () => {
|
|
7296
8310
|
try {
|
|
@@ -7363,11 +8377,11 @@ function setupEvents(deps2) {
|
|
|
7363
8377
|
|
|
7364
8378
|
// src/server/custom-context-modes.ts
|
|
7365
8379
|
import { listContextWindowModes, atomicWrite as atomicWrite5 } from "@wrongstack/core";
|
|
7366
|
-
import * as
|
|
7367
|
-
import * as
|
|
8380
|
+
import * as fs12 from "fs/promises";
|
|
8381
|
+
import * as path15 from "path";
|
|
7368
8382
|
var STORE_FILENAME = "custom-context-modes.json";
|
|
7369
8383
|
function storePath(wrongstackDir) {
|
|
7370
|
-
return
|
|
8384
|
+
return path15.join(wrongstackDir, STORE_FILENAME);
|
|
7371
8385
|
}
|
|
7372
8386
|
var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
|
|
7373
8387
|
function createCustomModeStore(wrongstackDir) {
|
|
@@ -7375,7 +8389,7 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
7375
8389
|
const load2 = async () => {
|
|
7376
8390
|
modes.clear();
|
|
7377
8391
|
try {
|
|
7378
|
-
const raw = await
|
|
8392
|
+
const raw = await fs12.readFile(storePath(wrongstackDir), "utf8");
|
|
7379
8393
|
const parsed = JSON.parse(raw);
|
|
7380
8394
|
if (Array.isArray(parsed.modes)) {
|
|
7381
8395
|
for (const m of parsed.modes) {
|
|
@@ -7421,7 +8435,8 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
7421
8435
|
custom: true
|
|
7422
8436
|
};
|
|
7423
8437
|
modes.set(mode.id, entry);
|
|
7424
|
-
void save2()
|
|
8438
|
+
void save2().catch(() => {
|
|
8439
|
+
});
|
|
7425
8440
|
return { ok: true };
|
|
7426
8441
|
};
|
|
7427
8442
|
const update = (id, patch) => {
|
|
@@ -7447,7 +8462,8 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
7447
8462
|
if (patch.targetLoad !== void 0) next.targetLoad = patch.targetLoad;
|
|
7448
8463
|
if (patch.aggressiveOn !== void 0) next.aggressiveOn = patch.aggressiveOn;
|
|
7449
8464
|
modes.set(id, next);
|
|
7450
|
-
void save2()
|
|
8465
|
+
void save2().catch(() => {
|
|
8466
|
+
});
|
|
7451
8467
|
return { ok: true };
|
|
7452
8468
|
};
|
|
7453
8469
|
const remove = (id) => {
|
|
@@ -7457,7 +8473,8 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
7457
8473
|
if (!modes.delete(id)) {
|
|
7458
8474
|
return { ok: false, error: `Mode "${id}" not found` };
|
|
7459
8475
|
}
|
|
7460
|
-
void save2()
|
|
8476
|
+
void save2().catch(() => {
|
|
8477
|
+
});
|
|
7461
8478
|
return { ok: true };
|
|
7462
8479
|
};
|
|
7463
8480
|
const list = () => {
|
|
@@ -7498,14 +8515,17 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
|
|
|
7498
8515
|
}
|
|
7499
8516
|
|
|
7500
8517
|
// src/server/shell-open.ts
|
|
7501
|
-
import * as
|
|
7502
|
-
import * as
|
|
8518
|
+
import * as fs13 from "fs/promises";
|
|
8519
|
+
import * as path16 from "path";
|
|
7503
8520
|
import { spawn as spawn2 } from "child_process";
|
|
7504
|
-
var METACHAR_REGEX = /[&|<>^"'
|
|
8521
|
+
var METACHAR_REGEX = /[&|<>^"'`'\n\r]/;
|
|
8522
|
+
function shellQuote(s) {
|
|
8523
|
+
return s.replaceAll("'", `'"'"'`);
|
|
8524
|
+
}
|
|
7505
8525
|
async function handleShellOpen(req, logger) {
|
|
7506
8526
|
try {
|
|
7507
|
-
const resolved =
|
|
7508
|
-
await
|
|
8527
|
+
const resolved = path16.resolve(req.path);
|
|
8528
|
+
await fs13.access(resolved);
|
|
7509
8529
|
if (METACHAR_REGEX.test(resolved)) {
|
|
7510
8530
|
return { success: false, message: "Path contains unsupported characters." };
|
|
7511
8531
|
}
|
|
@@ -7538,7 +8558,11 @@ async function handleShellOpen(req, logger) {
|
|
|
7538
8558
|
() => launch(
|
|
7539
8559
|
"gnome-terminal",
|
|
7540
8560
|
[`--working-directory=${resolved}`],
|
|
7541
|
-
() =>
|
|
8561
|
+
() => (
|
|
8562
|
+
// Pass argv array so sh -c sees a literal string, not an interpolated one.
|
|
8563
|
+
// shellQuote() guards against paths that somehow slipped the METACHAR_REGEX.
|
|
8564
|
+
launch("xterm", ["-e", "sh", "-c", `cd ${shellQuote(resolved)} && ${process.env["SHELL"] ?? "sh"}`])
|
|
8565
|
+
)
|
|
7542
8566
|
)
|
|
7543
8567
|
);
|
|
7544
8568
|
}
|
|
@@ -7557,9 +8581,9 @@ async function handleGitInfo(ws, projectRoot) {
|
|
|
7557
8581
|
const cwd = projectRoot || void 0;
|
|
7558
8582
|
try {
|
|
7559
8583
|
const { execFile: ef } = await import("child_process");
|
|
7560
|
-
const git = (args) => new Promise((
|
|
8584
|
+
const git = (args) => new Promise((resolve10) => {
|
|
7561
8585
|
ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
|
|
7562
|
-
|
|
8586
|
+
resolve10(err ? "" : stdout.trim());
|
|
7563
8587
|
});
|
|
7564
8588
|
});
|
|
7565
8589
|
const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
|
|
@@ -7585,12 +8609,12 @@ async function handleGitInfo(ws, projectRoot) {
|
|
|
7585
8609
|
function makeGit(cwd) {
|
|
7586
8610
|
return async (args) => {
|
|
7587
8611
|
const { execFile: ef } = await import("child_process");
|
|
7588
|
-
return new Promise((
|
|
8612
|
+
return new Promise((resolve10) => {
|
|
7589
8613
|
ef(
|
|
7590
8614
|
"git",
|
|
7591
8615
|
args,
|
|
7592
8616
|
{ cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
|
|
7593
|
-
(err, stdout) =>
|
|
8617
|
+
(err, stdout) => resolve10(err ? "" : stdout)
|
|
7594
8618
|
);
|
|
7595
8619
|
});
|
|
7596
8620
|
};
|
|
@@ -7614,15 +8638,15 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
7614
8638
|
if (!m) continue;
|
|
7615
8639
|
const added = m[1] === "-" ? 0 : Number(m[1]);
|
|
7616
8640
|
const deleted = m[2] === "-" ? 0 : Number(m[2]);
|
|
7617
|
-
let
|
|
7618
|
-
if (
|
|
8641
|
+
let path18 = m[3] ?? "";
|
|
8642
|
+
if (path18 === "") {
|
|
7619
8643
|
i += 1;
|
|
7620
|
-
|
|
8644
|
+
path18 = parts[i + 1] ?? parts[i] ?? "";
|
|
7621
8645
|
i += 1;
|
|
7622
8646
|
}
|
|
7623
|
-
if (!
|
|
7624
|
-
const prev = counts.get(
|
|
7625
|
-
counts.set(
|
|
8647
|
+
if (!path18) continue;
|
|
8648
|
+
const prev = counts.get(path18) ?? { added: 0, deleted: 0 };
|
|
8649
|
+
counts.set(path18, { added: prev.added + added, deleted: prev.deleted + deleted });
|
|
7626
8650
|
}
|
|
7627
8651
|
};
|
|
7628
8652
|
parseNumstat(unstagedNumstat);
|
|
@@ -7634,7 +8658,7 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
7634
8658
|
if (!rec || rec.length < 3) continue;
|
|
7635
8659
|
const x = rec[0] ?? " ";
|
|
7636
8660
|
const y = rec[1] ?? " ";
|
|
7637
|
-
const
|
|
8661
|
+
const path18 = rec.slice(3);
|
|
7638
8662
|
const isRename = x === "R" || x === "C" || y === "R" || y === "C";
|
|
7639
8663
|
if (isRename) i += 1;
|
|
7640
8664
|
let status;
|
|
@@ -7646,13 +8670,13 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
7646
8670
|
else if (x === "D" || y === "D") status = "D";
|
|
7647
8671
|
else status = "M";
|
|
7648
8672
|
const staged = x !== " " && x !== "?";
|
|
7649
|
-
let added = counts.get(
|
|
7650
|
-
let deleted = counts.get(
|
|
8673
|
+
let added = counts.get(path18)?.added ?? 0;
|
|
8674
|
+
let deleted = counts.get(path18)?.deleted ?? 0;
|
|
7651
8675
|
if (status === "?") {
|
|
7652
8676
|
added = 0;
|
|
7653
8677
|
deleted = 0;
|
|
7654
8678
|
}
|
|
7655
|
-
files.push({ path:
|
|
8679
|
+
files.push({ path: path18, status, added, deleted, staged });
|
|
7656
8680
|
}
|
|
7657
8681
|
send(ws, { type: "git.changes", payload: { files } });
|
|
7658
8682
|
} catch (err) {
|
|
@@ -7663,21 +8687,21 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
7663
8687
|
}
|
|
7664
8688
|
}
|
|
7665
8689
|
var MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
7666
|
-
async function handleGitDiff(ws, projectRoot,
|
|
8690
|
+
async function handleGitDiff(ws, projectRoot, path18) {
|
|
7667
8691
|
const cwd = projectRoot || void 0;
|
|
7668
|
-
const reply = (extra) => send(ws, { type: "git.diff", payload: { path:
|
|
7669
|
-
if (!
|
|
8692
|
+
const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path18, ...extra } });
|
|
8693
|
+
if (!path18 || path18.includes("\0") || path18.includes("..") || nodePath.isAbsolute(path18)) {
|
|
7670
8694
|
reply({ oldText: "", newText: "", error: "invalid path" });
|
|
7671
8695
|
return;
|
|
7672
8696
|
}
|
|
7673
8697
|
try {
|
|
7674
8698
|
const git = makeGit(cwd);
|
|
7675
8699
|
const { readFile: readFile9 } = await import("fs/promises");
|
|
7676
|
-
const { join:
|
|
7677
|
-
const oldText = await git(["show", `HEAD:${
|
|
8700
|
+
const { join: join14 } = await import("path");
|
|
8701
|
+
const oldText = await git(["show", `HEAD:${path18}`]);
|
|
7678
8702
|
let newText = "";
|
|
7679
8703
|
try {
|
|
7680
|
-
const abs = cwd ?
|
|
8704
|
+
const abs = cwd ? join14(cwd, path18) : path18;
|
|
7681
8705
|
const buf = await readFile9(abs);
|
|
7682
8706
|
if (buf.includes(0)) {
|
|
7683
8707
|
reply({ oldText: "", newText: "", binary: true });
|
|
@@ -7758,10 +8782,10 @@ async function handleProcessKillAll(ws) {
|
|
|
7758
8782
|
}
|
|
7759
8783
|
|
|
7760
8784
|
// src/server/goal-handlers.ts
|
|
7761
|
-
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
8785
|
+
import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
|
|
7762
8786
|
async function handleGoalGet(projectRoot, broadcast2) {
|
|
7763
8787
|
try {
|
|
7764
|
-
const goalPath =
|
|
8788
|
+
const goalPath = resolveWstackPaths3({ projectRoot }).projectGoal;
|
|
7765
8789
|
const { readFile: readFile9 } = await import("fs/promises");
|
|
7766
8790
|
const raw = await readFile9(goalPath, "utf8");
|
|
7767
8791
|
const goal = JSON.parse(raw);
|
|
@@ -7775,8 +8799,11 @@ async function handleGoalGet(projectRoot, broadcast2) {
|
|
|
7775
8799
|
async function startWebUI(opts = {}) {
|
|
7776
8800
|
ensureSessionShell();
|
|
7777
8801
|
const requestedWsPort = opts.wsPort ?? 3457;
|
|
7778
|
-
const wsHost = opts.wsHost ?? "127.0.0.1";
|
|
7779
|
-
const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
8802
|
+
const wsHost = opts.wsHost ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
|
|
8803
|
+
const requestedHttpPort = opts.httpPort ?? opts.webuiPort ?? opts.port ?? Number.parseInt(process.env["WEBUI_PORT"] ?? process.env["PORT"] ?? "3456", 10);
|
|
8804
|
+
const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
|
|
8805
|
+
const publicWsUrl = opts.publicWsUrl ?? process.env["WEBUI_PUBLIC_WS_URL"];
|
|
8806
|
+
const requireToken = opts.requireToken ?? envFlag("WEBUI_REQUIRE_TOKEN");
|
|
7780
8807
|
const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
|
|
7781
8808
|
let wsPort = requestedWsPort;
|
|
7782
8809
|
let httpPort = requestedHttpPort;
|
|
@@ -7816,7 +8843,7 @@ async function startWebUI(opts = {}) {
|
|
|
7816
8843
|
const write = async () => {
|
|
7817
8844
|
let raw;
|
|
7818
8845
|
try {
|
|
7819
|
-
raw = await
|
|
8846
|
+
raw = await fs14.readFile(globalConfigPath, "utf8");
|
|
7820
8847
|
} catch {
|
|
7821
8848
|
raw = "{}";
|
|
7822
8849
|
}
|
|
@@ -7890,6 +8917,7 @@ async function startWebUI(opts = {}) {
|
|
|
7890
8917
|
toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
|
|
7891
8918
|
toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
|
|
7892
8919
|
applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
|
|
8920
|
+
applyToolResultRenderModes(toolRegistry, config.tools?.resultRenderMode);
|
|
7893
8921
|
configureExecPolicy(config.tools?.exec ?? {});
|
|
7894
8922
|
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
7895
8923
|
const mcpRegistry = new MCPRegistry({
|
|
@@ -7934,7 +8962,7 @@ async function startWebUI(opts = {}) {
|
|
|
7934
8962
|
sessionId: session.id,
|
|
7935
8963
|
projectSlug: wpaths.projectSlug,
|
|
7936
8964
|
projectRoot,
|
|
7937
|
-
projectName:
|
|
8965
|
+
projectName: path17.basename(projectRoot),
|
|
7938
8966
|
workingDir,
|
|
7939
8967
|
clientType: "webui",
|
|
7940
8968
|
pid: process.pid,
|
|
@@ -7954,7 +8982,7 @@ async function startWebUI(opts = {}) {
|
|
|
7954
8982
|
const hqTelemetry = createHqPublisherFromEnv({
|
|
7955
8983
|
clientKind: "webui",
|
|
7956
8984
|
projectRoot,
|
|
7957
|
-
projectName:
|
|
8985
|
+
projectName: path17.basename(projectRoot),
|
|
7958
8986
|
appConfig: config,
|
|
7959
8987
|
socketFactory: (url) => new WebSocket2(url)
|
|
7960
8988
|
});
|
|
@@ -7966,7 +8994,7 @@ async function startWebUI(opts = {}) {
|
|
|
7966
8994
|
events,
|
|
7967
8995
|
sessionId: session.id,
|
|
7968
8996
|
projectRoot,
|
|
7969
|
-
projectName:
|
|
8997
|
+
projectName: path17.basename(projectRoot),
|
|
7970
8998
|
globalRoot: wpaths.globalRoot,
|
|
7971
8999
|
initialAgents: statusTracker?.getAgents(),
|
|
7972
9000
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -8022,19 +9050,39 @@ async function startWebUI(opts = {}) {
|
|
|
8022
9050
|
};
|
|
8023
9051
|
const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
|
|
8024
9052
|
const skillInstaller = config.features.skills ? new SkillInstaller({
|
|
8025
|
-
manifestPath:
|
|
8026
|
-
projectSkillsDir:
|
|
8027
|
-
globalSkillsDir:
|
|
9053
|
+
manifestPath: path17.join(wstackGlobalRoot3(), "installed-skills.json"),
|
|
9054
|
+
projectSkillsDir: path17.join(projectRoot, ".wrongstack", "skills"),
|
|
9055
|
+
globalSkillsDir: path17.join(wstackGlobalRoot3(), "skills"),
|
|
8028
9056
|
projectHash: projectHash(projectRoot),
|
|
8029
9057
|
skillLoader
|
|
8030
9058
|
}) : void 0;
|
|
9059
|
+
const promptsEnabled = config.features.prompts !== false;
|
|
9060
|
+
const bundledPromptsDir = promptsEnabled ? (() => {
|
|
9061
|
+
try {
|
|
9062
|
+
const req = createRequire2(import.meta.url);
|
|
9063
|
+
return path17.join(
|
|
9064
|
+
path17.dirname(req.resolve("@wrongstack/core/package.json")),
|
|
9065
|
+
"data",
|
|
9066
|
+
"prompts"
|
|
9067
|
+
);
|
|
9068
|
+
} catch {
|
|
9069
|
+
return void 0;
|
|
9070
|
+
}
|
|
9071
|
+
})() : void 0;
|
|
9072
|
+
const promptLoader = promptsEnabled ? new DefaultPromptLoader({ paths: wpaths, bundledDir: bundledPromptsDir }) : void 0;
|
|
9073
|
+
const promptUsage = new PromptUsageStore(wpaths.promptUsage);
|
|
9074
|
+
const promptsCtx = { promptLoader, promptUsage };
|
|
8031
9075
|
const systemPromptBuilder = new DefaultSystemPromptBuilder3({
|
|
8032
9076
|
memoryStore,
|
|
8033
9077
|
skillLoader,
|
|
8034
9078
|
modeStore,
|
|
8035
9079
|
modeId,
|
|
8036
9080
|
modePrompt,
|
|
8037
|
-
modelCapabilities: () => modelCapabilitiesRef.current
|
|
9081
|
+
modelCapabilities: () => modelCapabilitiesRef.current,
|
|
9082
|
+
instructionPaths: {
|
|
9083
|
+
globalDir: wpaths.globalInstructions,
|
|
9084
|
+
projectDir: wpaths.inProjectInstructions
|
|
9085
|
+
}
|
|
8038
9086
|
});
|
|
8039
9087
|
let onlineAgents = [];
|
|
8040
9088
|
try {
|
|
@@ -8129,6 +9177,10 @@ async function startWebUI(opts = {}) {
|
|
|
8129
9177
|
context.meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
|
|
8130
9178
|
context.meta["nextPrediction"] = config.nextPrediction ?? false;
|
|
8131
9179
|
context.meta["fallbackModels"] = config.fallbackModels ?? [];
|
|
9180
|
+
context.meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
|
|
9181
|
+
context.meta["favoriteModels"] = config.favoriteModels ?? [];
|
|
9182
|
+
context.meta["favoriteModelsOnly"] = config.favoriteModelsOnly === true;
|
|
9183
|
+
context.meta["modelMatrix"] = config.modelMatrix ?? {};
|
|
8132
9184
|
context.meta["fallbackAuto"] = config.fallbackAuto !== false;
|
|
8133
9185
|
context.meta["featureMcp"] = config.features.mcp !== false;
|
|
8134
9186
|
context.meta["featurePlugins"] = config.features.plugins !== false;
|
|
@@ -8141,6 +9193,20 @@ async function startWebUI(opts = {}) {
|
|
|
8141
9193
|
context.meta["logLevel"] = config.log?.level ?? "info";
|
|
8142
9194
|
context.meta["auditLevel"] = config.session?.auditLevel ?? "standard";
|
|
8143
9195
|
context.meta["maxIterations"] = config.tools?.maxIterations ?? 500;
|
|
9196
|
+
context.meta["contextMode"] = config.context?.mode ?? "balanced";
|
|
9197
|
+
{
|
|
9198
|
+
const tsm = config.features?.tokenSavingMode;
|
|
9199
|
+
context.meta["tokenSavingTier"] = typeof tsm === "string" ? tsm : tsm ? "medium" : "off";
|
|
9200
|
+
}
|
|
9201
|
+
context.meta["maxConcurrent"] = typeof config.maxConcurrent === "number" ? config.maxConcurrent : 10;
|
|
9202
|
+
context.meta["titleAnimation"] = autonomyCfg["terminalTitleAnimation"] !== false;
|
|
9203
|
+
{
|
|
9204
|
+
const mr = config.modelRuntime ?? {};
|
|
9205
|
+
context.meta["reasoningMode"] = mr.reasoning?.mode ?? "auto";
|
|
9206
|
+
context.meta["reasoningEffort"] = mr.reasoning?.effort ?? "high";
|
|
9207
|
+
context.meta["reasoningPreserve"] = mr.reasoning?.preserve === true;
|
|
9208
|
+
context.meta["cacheTtl"] = mr.cache?.ttl ?? "default";
|
|
9209
|
+
}
|
|
8144
9210
|
const hqConfig = config.hq;
|
|
8145
9211
|
context.meta["hqEnabled"] = hqConfig?.enabled === true;
|
|
8146
9212
|
context.meta["hqUrl"] = hqConfig?.url ?? "";
|
|
@@ -8174,6 +9240,10 @@ async function startWebUI(opts = {}) {
|
|
|
8174
9240
|
"indexOnStart",
|
|
8175
9241
|
"contextAutoCompact",
|
|
8176
9242
|
"contextStrategy",
|
|
9243
|
+
"contextMode",
|
|
9244
|
+
"tokenSavingTier",
|
|
9245
|
+
"maxConcurrent",
|
|
9246
|
+
"titleAnimation",
|
|
8177
9247
|
"logLevel",
|
|
8178
9248
|
"auditLevel",
|
|
8179
9249
|
"hqEnabled",
|
|
@@ -8189,6 +9259,10 @@ async function startWebUI(opts = {}) {
|
|
|
8189
9259
|
"reasoningPreserve",
|
|
8190
9260
|
"cacheTtl",
|
|
8191
9261
|
"fallbackModels",
|
|
9262
|
+
"fallbackProfiles",
|
|
9263
|
+
"favoriteModels",
|
|
9264
|
+
"favoriteModelsOnly",
|
|
9265
|
+
"modelMatrix",
|
|
8192
9266
|
"fallbackAuto"
|
|
8193
9267
|
];
|
|
8194
9268
|
const prefSnapshot = () => {
|
|
@@ -8221,6 +9295,15 @@ async function startWebUI(opts = {}) {
|
|
|
8221
9295
|
if (autonomyTouched) decrypted.autonomy = autonomyCfg;
|
|
8222
9296
|
if (typeof payload["nextPrediction"] === "boolean") decrypted.nextPrediction = payload["nextPrediction"];
|
|
8223
9297
|
if (Array.isArray(payload["fallbackModels"])) decrypted.fallbackModels = payload["fallbackModels"];
|
|
9298
|
+
if (payload["fallbackProfiles"] && typeof payload["fallbackProfiles"] === "object" && !Array.isArray(payload["fallbackProfiles"])) {
|
|
9299
|
+
decrypted.fallbackProfiles = payload["fallbackProfiles"];
|
|
9300
|
+
}
|
|
9301
|
+
if (Array.isArray(payload["favoriteModels"])) decrypted.favoriteModels = payload["favoriteModels"];
|
|
9302
|
+
if (typeof payload["favoriteModelsOnly"] === "boolean")
|
|
9303
|
+
decrypted.favoriteModelsOnly = payload["favoriteModelsOnly"];
|
|
9304
|
+
if (payload["modelMatrix"] && typeof payload["modelMatrix"] === "object" && !Array.isArray(payload["modelMatrix"])) {
|
|
9305
|
+
decrypted.modelMatrix = payload["modelMatrix"];
|
|
9306
|
+
}
|
|
8224
9307
|
if (typeof payload["fallbackAuto"] === "boolean") decrypted.fallbackAuto = payload["fallbackAuto"];
|
|
8225
9308
|
const FEATURE_MAP = {
|
|
8226
9309
|
featureMcp: "mcp",
|
|
@@ -8236,12 +9319,26 @@ async function startWebUI(opts = {}) {
|
|
|
8236
9319
|
decrypted.features = feats;
|
|
8237
9320
|
}
|
|
8238
9321
|
}
|
|
8239
|
-
if (typeof payload["contextAutoCompact"] === "boolean" || typeof payload["contextStrategy"] === "string") {
|
|
9322
|
+
if (typeof payload["contextAutoCompact"] === "boolean" || typeof payload["contextStrategy"] === "string" || typeof payload["contextMode"] === "string") {
|
|
8240
9323
|
const ctxCfg = decrypted.context ?? {};
|
|
8241
9324
|
if (typeof payload["contextAutoCompact"] === "boolean") ctxCfg.autoCompact = payload["contextAutoCompact"];
|
|
8242
9325
|
if (typeof payload["contextStrategy"] === "string") ctxCfg.strategy = payload["contextStrategy"];
|
|
9326
|
+
if (typeof payload["contextMode"] === "string") ctxCfg.mode = payload["contextMode"];
|
|
8243
9327
|
decrypted.context = ctxCfg;
|
|
8244
9328
|
}
|
|
9329
|
+
if (typeof payload["tokenSavingTier"] === "string") {
|
|
9330
|
+
const featsCfg = decrypted.features ?? {};
|
|
9331
|
+
featsCfg.tokenSavingMode = payload["tokenSavingTier"];
|
|
9332
|
+
decrypted.features = featsCfg;
|
|
9333
|
+
}
|
|
9334
|
+
if (typeof payload["maxConcurrent"] === "number") {
|
|
9335
|
+
decrypted.maxConcurrent = payload["maxConcurrent"];
|
|
9336
|
+
}
|
|
9337
|
+
if (typeof payload["titleAnimation"] === "boolean") {
|
|
9338
|
+
const autoCfg = decrypted.autonomy ?? {};
|
|
9339
|
+
autoCfg.terminalTitleAnimation = payload["titleAnimation"];
|
|
9340
|
+
decrypted.autonomy = autoCfg;
|
|
9341
|
+
}
|
|
8245
9342
|
if (typeof payload["logLevel"] === "string") {
|
|
8246
9343
|
const logCfg = decrypted.log ?? {};
|
|
8247
9344
|
logCfg.level = payload["logLevel"];
|
|
@@ -8312,6 +9409,7 @@ async function startWebUI(opts = {}) {
|
|
|
8312
9409
|
const collabInject = collabInjectMiddleware(collabBus, { logger });
|
|
8313
9410
|
Object.defineProperty(collabInject, "name", { value: "collab-inject" });
|
|
8314
9411
|
pipelines.toolCall.prepend(collabInject);
|
|
9412
|
+
installDesignStudioMiddleware({ pipelines, ctx: context });
|
|
8315
9413
|
const codebaseIndexing = setupWebUICodebaseIndexing({
|
|
8316
9414
|
config,
|
|
8317
9415
|
context,
|
|
@@ -8407,6 +9505,17 @@ async function startWebUI(opts = {}) {
|
|
|
8407
9505
|
perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
|
|
8408
9506
|
tracer: void 0
|
|
8409
9507
|
});
|
|
9508
|
+
const webuiLogger = container.resolve(TOKENS.Logger);
|
|
9509
|
+
void discoverMailboxBridgeForWebui({
|
|
9510
|
+
projectRoot,
|
|
9511
|
+
config,
|
|
9512
|
+
logger: webuiLogger,
|
|
9513
|
+
ctx: context
|
|
9514
|
+
}).catch((err) => {
|
|
9515
|
+
webuiLogger.warn("mailbox bridge discovery threw on webui boot", {
|
|
9516
|
+
err: err instanceof Error ? err.message : String(err)
|
|
9517
|
+
});
|
|
9518
|
+
});
|
|
8410
9519
|
const agent = new Agent({
|
|
8411
9520
|
container,
|
|
8412
9521
|
tools: toolRegistry,
|
|
@@ -8503,7 +9612,18 @@ async function startWebUI(opts = {}) {
|
|
|
8503
9612
|
projectRoot
|
|
8504
9613
|
);
|
|
8505
9614
|
const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
|
|
8506
|
-
const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards
|
|
9615
|
+
const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards, void 0, {
|
|
9616
|
+
projectRoot,
|
|
9617
|
+
paths: {
|
|
9618
|
+
projectSpecs: wpaths.projectSpecs,
|
|
9619
|
+
projectTaskGraphs: wpaths.projectTaskGraphs,
|
|
9620
|
+
projectSddSession: wpaths.projectSddSession,
|
|
9621
|
+
projectSddBoards: wpaths.projectSddBoards
|
|
9622
|
+
}
|
|
9623
|
+
});
|
|
9624
|
+
void cleanupStaleSddWorktrees3({ projectRoot, boardsDir: wpaths.projectSddBoards }).catch(
|
|
9625
|
+
() => void 0
|
|
9626
|
+
);
|
|
8507
9627
|
const sddWizardHandler = new SddWizardWebSocketHandler(
|
|
8508
9628
|
buildSddWizardDeps({
|
|
8509
9629
|
agent,
|
|
@@ -8525,7 +9645,10 @@ async function startWebUI(opts = {}) {
|
|
|
8525
9645
|
}
|
|
8526
9646
|
})
|
|
8527
9647
|
);
|
|
8528
|
-
const worktreeHandler = new WorktreeWebSocketHandler(events, logger
|
|
9648
|
+
const worktreeHandler = new WorktreeWebSocketHandler(events, logger, {
|
|
9649
|
+
projectRoot,
|
|
9650
|
+
boardsDir: wpaths.projectSddBoards
|
|
9651
|
+
});
|
|
8529
9652
|
const terminalHandler = new TerminalWebSocketHandler(() => workingDir, logger);
|
|
8530
9653
|
const collabHandler = new CollaborationWebSocketHandler(
|
|
8531
9654
|
events,
|
|
@@ -8564,15 +9687,23 @@ async function startWebUI(opts = {}) {
|
|
|
8564
9687
|
inputCost,
|
|
8565
9688
|
outputCost,
|
|
8566
9689
|
cacheReadCost,
|
|
8567
|
-
projectName:
|
|
9690
|
+
projectName: path17.basename(projectRoot) || projectRoot,
|
|
8568
9691
|
projectRoot,
|
|
8569
9692
|
cwd: workingDir,
|
|
8570
9693
|
mode: modeId,
|
|
8571
9694
|
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID2)
|
|
8572
9695
|
};
|
|
8573
9696
|
}
|
|
8574
|
-
const wsToken =
|
|
8575
|
-
console.log("[WebUI] WS auth token
|
|
9697
|
+
const wsToken = resolveAuthToken(opts.accessToken);
|
|
9698
|
+
console.log("[WebUI] WS auth token ready");
|
|
9699
|
+
const publicHostnames = [publicUrl, publicWsUrl].map((value) => {
|
|
9700
|
+
if (!value) return void 0;
|
|
9701
|
+
try {
|
|
9702
|
+
return new URL(value).hostname;
|
|
9703
|
+
} catch {
|
|
9704
|
+
return void 0;
|
|
9705
|
+
}
|
|
9706
|
+
}).filter((value) => Boolean(value));
|
|
8576
9707
|
const verifyClient2 = (info) => verifyClient({
|
|
8577
9708
|
origin: info.origin,
|
|
8578
9709
|
url: info.req.url ?? "",
|
|
@@ -8584,7 +9715,10 @@ async function startWebUI(opts = {}) {
|
|
|
8584
9715
|
// exposure class.
|
|
8585
9716
|
cookieHeader: info.req.headers.cookie,
|
|
8586
9717
|
wsHost,
|
|
8587
|
-
expectedToken: wsToken
|
|
9718
|
+
expectedToken: wsToken,
|
|
9719
|
+
requireToken,
|
|
9720
|
+
allowedHostnames: publicHostnames,
|
|
9721
|
+
allowBrowserUrlToken: Boolean(publicWsUrl)
|
|
8588
9722
|
});
|
|
8589
9723
|
const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
|
|
8590
9724
|
const wssPrimary = new WebSocketServer({
|
|
@@ -8702,8 +9836,8 @@ async function startWebUI(opts = {}) {
|
|
|
8702
9836
|
clients.delete(ws);
|
|
8703
9837
|
if (closing) rateLimits.delete(closing.connId);
|
|
8704
9838
|
if (pendingConfirms.size > 0) {
|
|
8705
|
-
for (const [id,
|
|
8706
|
-
|
|
9839
|
+
for (const [id, resolve10] of pendingConfirms) {
|
|
9840
|
+
resolve10("no");
|
|
8707
9841
|
pendingConfirms.delete(id);
|
|
8708
9842
|
}
|
|
8709
9843
|
}
|
|
@@ -8783,21 +9917,21 @@ async function startWebUI(opts = {}) {
|
|
|
8783
9917
|
});
|
|
8784
9918
|
}
|
|
8785
9919
|
async function touchProjectEntry(root, workDir) {
|
|
8786
|
-
const resolved =
|
|
9920
|
+
const resolved = path17.resolve(root);
|
|
8787
9921
|
const manifest = await loadManifest(globalConfigPath);
|
|
8788
9922
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8789
|
-
const existing = manifest.projects.find((p) =>
|
|
9923
|
+
const existing = manifest.projects.find((p) => path17.resolve(p.root) === resolved);
|
|
8790
9924
|
if (existing) {
|
|
8791
9925
|
existing.lastSeen = now;
|
|
8792
|
-
if (workDir) existing.lastWorkingDir =
|
|
9926
|
+
if (workDir) existing.lastWorkingDir = path17.resolve(workDir);
|
|
8793
9927
|
} else {
|
|
8794
9928
|
manifest.projects.push({
|
|
8795
|
-
name:
|
|
9929
|
+
name: path17.basename(resolved),
|
|
8796
9930
|
root: resolved,
|
|
8797
9931
|
slug: generateProjectSlug(resolved),
|
|
8798
9932
|
createdAt: now,
|
|
8799
9933
|
lastSeen: now,
|
|
8800
|
-
lastWorkingDir: workDir ?
|
|
9934
|
+
lastWorkingDir: workDir ? path17.resolve(workDir) : void 0
|
|
8801
9935
|
});
|
|
8802
9936
|
}
|
|
8803
9937
|
await saveManifest(manifest, globalConfigPath);
|
|
@@ -8842,6 +9976,8 @@ async function startWebUI(opts = {}) {
|
|
|
8842
9976
|
if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
|
|
8843
9977
|
if (await handleSddBoardRoute(ws, msg, sddBoardRoutes)) return;
|
|
8844
9978
|
if (await handleSddWizardRoute(ws, msg, sddWizardRoutes)) return;
|
|
9979
|
+
if (msg.type.startsWith("worktree.") && await worktreeHandler.handleMessage(msg))
|
|
9980
|
+
return;
|
|
8845
9981
|
switch (msg.type) {
|
|
8846
9982
|
// Collaboration messages short-circuit the user/agent flow.
|
|
8847
9983
|
// They don't touch runLock, the agent loop, or the message queue —
|
|
@@ -8912,10 +10048,10 @@ async function startWebUI(opts = {}) {
|
|
|
8912
10048
|
}
|
|
8913
10049
|
case "tool.confirm_result": {
|
|
8914
10050
|
const { id, decision } = msg.payload;
|
|
8915
|
-
const
|
|
8916
|
-
if (
|
|
10051
|
+
const resolve10 = pendingConfirms.get(id);
|
|
10052
|
+
if (resolve10) {
|
|
8917
10053
|
pendingConfirms.delete(id);
|
|
8918
|
-
|
|
10054
|
+
resolve10(decision);
|
|
8919
10055
|
}
|
|
8920
10056
|
break;
|
|
8921
10057
|
}
|
|
@@ -8999,6 +10135,48 @@ async function startWebUI(opts = {}) {
|
|
|
8999
10135
|
case "skills.export":
|
|
9000
10136
|
await handleSkillsExport(ws, { skillLoader, skillInstaller, projectRoot });
|
|
9001
10137
|
break;
|
|
10138
|
+
// Prompt library — shared handlers (prompts-handlers.ts).
|
|
10139
|
+
case "prompts.list":
|
|
10140
|
+
await handlePromptsList(ws, promptsCtx);
|
|
10141
|
+
break;
|
|
10142
|
+
case "prompts.search":
|
|
10143
|
+
await handlePromptsSearch(ws, promptsCtx, msg);
|
|
10144
|
+
break;
|
|
10145
|
+
case "prompts.content":
|
|
10146
|
+
await handlePromptsContent(ws, promptsCtx, msg);
|
|
10147
|
+
break;
|
|
10148
|
+
case "prompts.favorite":
|
|
10149
|
+
await handlePromptsFavorite(ws, promptsCtx, msg);
|
|
10150
|
+
break;
|
|
10151
|
+
case "prompts.create":
|
|
10152
|
+
await handlePromptsCreate(ws, promptsCtx, msg);
|
|
10153
|
+
break;
|
|
10154
|
+
case "prompts.used":
|
|
10155
|
+
await handlePromptsUsed(ws, promptsCtx, msg);
|
|
10156
|
+
break;
|
|
10157
|
+
case "prompts.recent":
|
|
10158
|
+
await handlePromptsRecent(ws, promptsCtx);
|
|
10159
|
+
break;
|
|
10160
|
+
// Design Studio — shared handlers (design-handlers.ts). agentMeta is the
|
|
10161
|
+
// live context so design.use pins the active kit for the next turn.
|
|
10162
|
+
case "design.list":
|
|
10163
|
+
await handleDesignList(ws, { projectRoot, agentMeta: context });
|
|
10164
|
+
break;
|
|
10165
|
+
case "design.use":
|
|
10166
|
+
await handleDesignUse(ws, { projectRoot, agentMeta: context }, msg);
|
|
10167
|
+
break;
|
|
10168
|
+
case "design.state":
|
|
10169
|
+
await handleDesignState(ws, { projectRoot, agentMeta: context });
|
|
10170
|
+
break;
|
|
10171
|
+
case "design.set":
|
|
10172
|
+
await handleDesignSet(ws, { projectRoot, agentMeta: context }, msg);
|
|
10173
|
+
break;
|
|
10174
|
+
case "design.materialize":
|
|
10175
|
+
await handleDesignMaterialize(ws, { projectRoot, agentMeta: context }, msg);
|
|
10176
|
+
break;
|
|
10177
|
+
case "design.verify":
|
|
10178
|
+
await handleDesignVerify(ws, { projectRoot, agentMeta: context });
|
|
10179
|
+
break;
|
|
9002
10180
|
case "diag.get": {
|
|
9003
10181
|
const usage = tokenCounter.total();
|
|
9004
10182
|
send(ws, {
|
|
@@ -9079,11 +10257,29 @@ async function startWebUI(opts = {}) {
|
|
|
9079
10257
|
messages: context.messages.length,
|
|
9080
10258
|
readFiles: context.readFiles.size,
|
|
9081
10259
|
tools: toolRegistry.list().length,
|
|
10260
|
+
sideEffectCount: context.sideEffects?.length ?? 0,
|
|
9082
10261
|
elapsedMs: Date.now() - sessionStartedAt
|
|
9083
10262
|
}
|
|
9084
10263
|
});
|
|
9085
10264
|
break;
|
|
9086
10265
|
}
|
|
10266
|
+
case "side_effects.list": {
|
|
10267
|
+
const sideEffects = context.sideEffects ?? [];
|
|
10268
|
+
send(ws, {
|
|
10269
|
+
type: "side_effects",
|
|
10270
|
+
payload: {
|
|
10271
|
+
sideEffects: sideEffects.slice(-50).map((se) => ({
|
|
10272
|
+
toolUseId: se.toolUseId,
|
|
10273
|
+
toolName: se.toolName,
|
|
10274
|
+
ts: se.ts,
|
|
10275
|
+
input: se.input,
|
|
10276
|
+
outcome: se.outcome,
|
|
10277
|
+
risk: se.risk
|
|
10278
|
+
}))
|
|
10279
|
+
}
|
|
10280
|
+
});
|
|
10281
|
+
break;
|
|
10282
|
+
}
|
|
9087
10283
|
case "process.list": {
|
|
9088
10284
|
await handleProcessList(ws);
|
|
9089
10285
|
break;
|
|
@@ -9338,6 +10534,7 @@ async function startWebUI(opts = {}) {
|
|
|
9338
10534
|
toolRegistry,
|
|
9339
10535
|
config,
|
|
9340
10536
|
projectRoot,
|
|
10537
|
+
globalRoot: wpaths.globalRoot,
|
|
9341
10538
|
clients,
|
|
9342
10539
|
setModeId: (id) => {
|
|
9343
10540
|
modeId = id;
|
|
@@ -9374,6 +10571,16 @@ async function startWebUI(opts = {}) {
|
|
|
9374
10571
|
config.features.modelsRegistry = payload["featureModelsRegistry"];
|
|
9375
10572
|
if (Array.isArray(payload["fallbackModels"]))
|
|
9376
10573
|
config.fallbackModels = payload["fallbackModels"];
|
|
10574
|
+
if (payload["fallbackProfiles"] && typeof payload["fallbackProfiles"] === "object" && !Array.isArray(payload["fallbackProfiles"])) {
|
|
10575
|
+
config.fallbackProfiles = payload["fallbackProfiles"];
|
|
10576
|
+
}
|
|
10577
|
+
if (Array.isArray(payload["favoriteModels"]))
|
|
10578
|
+
config.favoriteModels = payload["favoriteModels"];
|
|
10579
|
+
if (typeof payload["favoriteModelsOnly"] === "boolean")
|
|
10580
|
+
config.favoriteModelsOnly = payload["favoriteModelsOnly"];
|
|
10581
|
+
if (payload["modelMatrix"] && typeof payload["modelMatrix"] === "object" && !Array.isArray(payload["modelMatrix"])) {
|
|
10582
|
+
config.modelMatrix = payload["modelMatrix"];
|
|
10583
|
+
}
|
|
9377
10584
|
if (typeof payload["fallbackAuto"] === "boolean")
|
|
9378
10585
|
config.fallbackAuto = payload["fallbackAuto"];
|
|
9379
10586
|
if (typeof payload["contextAutoCompact"] === "boolean") {
|
|
@@ -9425,7 +10632,7 @@ async function startWebUI(opts = {}) {
|
|
|
9425
10632
|
sendResult2(ws, false, parsed.message);
|
|
9426
10633
|
return;
|
|
9427
10634
|
}
|
|
9428
|
-
return handleMailboxMessages(ws, { projectRoot, globalRoot:
|
|
10635
|
+
return handleMailboxMessages(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }, parsed.value);
|
|
9429
10636
|
},
|
|
9430
10637
|
agents: (ws, msg) => {
|
|
9431
10638
|
const parsed = validateMailboxAgentsPayload(msg.payload);
|
|
@@ -9433,16 +10640,16 @@ async function startWebUI(opts = {}) {
|
|
|
9433
10640
|
sendResult2(ws, false, parsed.message);
|
|
9434
10641
|
return;
|
|
9435
10642
|
}
|
|
9436
|
-
return handleMailboxAgents(ws, { projectRoot, globalRoot:
|
|
10643
|
+
return handleMailboxAgents(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }, parsed.value);
|
|
9437
10644
|
},
|
|
9438
|
-
clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot:
|
|
10645
|
+
clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }),
|
|
9439
10646
|
purge: (ws, msg) => {
|
|
9440
10647
|
const parsed = validateMailboxPurgePayload(msg.payload);
|
|
9441
10648
|
if (!parsed.ok) {
|
|
9442
10649
|
sendResult2(ws, false, parsed.message);
|
|
9443
10650
|
return;
|
|
9444
10651
|
}
|
|
9445
|
-
return handleMailboxPurge(ws, { projectRoot, globalRoot:
|
|
10652
|
+
return handleMailboxPurge(ws, { projectRoot, globalRoot: path17.dirname(globalConfigPath) }, parsed.value);
|
|
9446
10653
|
}
|
|
9447
10654
|
};
|
|
9448
10655
|
mcpRoutes = {
|
|
@@ -9522,18 +10729,25 @@ async function startWebUI(opts = {}) {
|
|
|
9522
10729
|
};
|
|
9523
10730
|
const httpServer = createHttpServer({
|
|
9524
10731
|
host: wsHost,
|
|
9525
|
-
distDir:
|
|
10732
|
+
distDir: path17.resolve(import.meta.dirname, "../../dist"),
|
|
9526
10733
|
wsPort,
|
|
10734
|
+
publicWsUrl,
|
|
9527
10735
|
globalRoot: wpaths.globalRoot,
|
|
9528
10736
|
apiToken: wsToken,
|
|
10737
|
+
requireToken,
|
|
9529
10738
|
watcherMetrics,
|
|
9530
10739
|
onFleetPing: () => {
|
|
9531
10740
|
void fleetBroadcast?.();
|
|
9532
10741
|
}
|
|
9533
10742
|
});
|
|
9534
|
-
const registryBaseDir =
|
|
10743
|
+
const registryBaseDir = path17.dirname(globalConfigPath);
|
|
9535
10744
|
httpServer.listen(httpPort, wsHost, () => {
|
|
9536
|
-
const openUrl =
|
|
10745
|
+
const openUrl = buildWebUIAccessUrl({
|
|
10746
|
+
host: wsHost,
|
|
10747
|
+
port: httpPort,
|
|
10748
|
+
token: wsToken,
|
|
10749
|
+
publicUrl
|
|
10750
|
+
});
|
|
9537
10751
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
9538
10752
|
if (opts.open) openBrowser(openUrl);
|
|
9539
10753
|
void registerInstance(
|
|
@@ -9543,9 +10757,9 @@ async function startWebUI(opts = {}) {
|
|
|
9543
10757
|
wsPort,
|
|
9544
10758
|
host: wsHost,
|
|
9545
10759
|
projectRoot,
|
|
9546
|
-
projectName:
|
|
10760
|
+
projectName: path17.basename(projectRoot) || projectRoot,
|
|
9547
10761
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9548
|
-
url:
|
|
10762
|
+
url: buildWebUIAccessUrl({ host: wsHost, port: httpPort, publicUrl })
|
|
9549
10763
|
},
|
|
9550
10764
|
registryBaseDir
|
|
9551
10765
|
).catch((err) => console.warn(JSON.stringify({
|
|
@@ -9595,6 +10809,7 @@ export {
|
|
|
9595
10809
|
browserOpenCommand,
|
|
9596
10810
|
buildCspHeader,
|
|
9597
10811
|
buildSddWizardDeps,
|
|
10812
|
+
buildWebUIAccessUrl,
|
|
9598
10813
|
createCustomModeStore,
|
|
9599
10814
|
createEternalSubscription,
|
|
9600
10815
|
createHttpServer,
|
|
@@ -9602,6 +10817,7 @@ export {
|
|
|
9602
10817
|
createToolLspCompletionSource,
|
|
9603
10818
|
defaultBaseDir,
|
|
9604
10819
|
deleteKey,
|
|
10820
|
+
envFlag,
|
|
9605
10821
|
errMessage,
|
|
9606
10822
|
estimateTokens,
|
|
9607
10823
|
extractToken,
|
|
@@ -9609,6 +10825,12 @@ export {
|
|
|
9609
10825
|
formatInstances,
|
|
9610
10826
|
generateAuthToken,
|
|
9611
10827
|
handleCompletionRequest,
|
|
10828
|
+
handleDesignList,
|
|
10829
|
+
handleDesignMaterialize,
|
|
10830
|
+
handleDesignSet,
|
|
10831
|
+
handleDesignState,
|
|
10832
|
+
handleDesignUse,
|
|
10833
|
+
handleDesignVerify,
|
|
9612
10834
|
handleFilesList,
|
|
9613
10835
|
handleFilesRead,
|
|
9614
10836
|
handleFilesTree,
|
|
@@ -9629,6 +10851,13 @@ export {
|
|
|
9629
10851
|
handleMemoryForget,
|
|
9630
10852
|
handleMemoryList,
|
|
9631
10853
|
handleMemoryRemember,
|
|
10854
|
+
handlePromptsContent,
|
|
10855
|
+
handlePromptsCreate,
|
|
10856
|
+
handlePromptsFavorite,
|
|
10857
|
+
handlePromptsList,
|
|
10858
|
+
handlePromptsRecent,
|
|
10859
|
+
handlePromptsSearch,
|
|
10860
|
+
handlePromptsUsed,
|
|
9632
10861
|
handleShellOpen,
|
|
9633
10862
|
handleSkillsContent,
|
|
9634
10863
|
handleSkillsCreate,
|
|
@@ -9637,6 +10866,7 @@ export {
|
|
|
9637
10866
|
handleSkillsInstall,
|
|
9638
10867
|
handleSkillsUninstall,
|
|
9639
10868
|
handleSkillsUpdate,
|
|
10869
|
+
hostForBrowserUrl,
|
|
9640
10870
|
hostHeaderOk,
|
|
9641
10871
|
injectWsPort,
|
|
9642
10872
|
isLoopbackBind,
|
|
@@ -9652,6 +10882,7 @@ export {
|
|
|
9652
10882
|
registerInstance,
|
|
9653
10883
|
registryPath,
|
|
9654
10884
|
removeProvider,
|
|
10885
|
+
resolveAuthToken,
|
|
9655
10886
|
saveProviders,
|
|
9656
10887
|
send,
|
|
9657
10888
|
sendResult2 as sendResult,
|