@wrongstack/webui 0.264.0 → 0.267.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-UZtAQH-v.css +2 -0
- package/dist/assets/index-q6W9UOEF.js +121 -0
- package/dist/assets/{vendor-CEQg2uSG.css → vendor-B2D6LvU3.css} +1 -1
- package/dist/assets/vendor-HxGAEBey.js +1326 -0
- package/dist/index.html +4 -4
- package/dist/index.js +12048 -6641
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +1772 -371
- package/dist/server/entry.js.map +1 -1
- package/dist/server/handlers.d.ts +45 -0
- package/dist/server/handlers.js +179 -0
- package/dist/server/handlers.js.map +1 -0
- package/dist/server/index.d.ts +173 -6
- package/dist/server/index.js +2042 -372
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +420 -2
- package/package.json +8 -5
- package/dist/assets/index-BBPaC1tO.js +0 -170
- package/dist/assets/index-DJmqJ5Wo.css +0 -2
- package/dist/assets/vendor-pWpGJmMc.js +0 -1303
package/dist/server/entry.js
CHANGED
|
@@ -1,8 +1,180 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// src/server/index.ts
|
|
3
|
-
import { expectDefined as expectDefined2, GlobalMailbox as GlobalMailbox2, projectSlug, getSessionRegistry, AgentStatusTracker } from "@wrongstack/core";
|
|
3
|
+
import { expectDefined as expectDefined2, GlobalMailbox as GlobalMailbox2, projectSlug, getSessionRegistry, AgentStatusTracker, FleetNotifier } from "@wrongstack/core";
|
|
4
|
+
|
|
5
|
+
// src/server/handlers/worklist-handlers.ts
|
|
6
|
+
function sendResult(ws, ctx, ok, message) {
|
|
7
|
+
ctx.send(ws, { type: ok ? "ok" : "error", message });
|
|
8
|
+
}
|
|
9
|
+
function handleTodosGet(ctx, ws) {
|
|
10
|
+
ctx.send(ws, { type: "todos.updated", payload: { todos: ctx.context.todos } });
|
|
11
|
+
}
|
|
12
|
+
function handleTodosClear(ctx, ws) {
|
|
13
|
+
ctx.replaceTodos?.([]);
|
|
14
|
+
ctx.broadcast({ type: "todos.cleared" });
|
|
15
|
+
sendResult(ws, ctx, true, "Todo board cleared.");
|
|
16
|
+
}
|
|
17
|
+
function handleTodosRemove(ctx, ws, payload) {
|
|
18
|
+
if (!payload || payload.id === void 0 && payload.index === void 0) {
|
|
19
|
+
sendResult(ws, ctx, false, "todos.remove requires id or index.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const next = payload.id !== void 0 ? ctx.context.todos.filter((t) => t.id !== payload.id) : ctx.context.todos.filter((_, i) => i !== payload.index);
|
|
23
|
+
ctx.replaceTodos?.(next);
|
|
24
|
+
ctx.broadcast({ type: "todos.updated", payload: { todos: next } });
|
|
25
|
+
sendResult(ws, ctx, true, "Todo item removed.");
|
|
26
|
+
}
|
|
27
|
+
function handleTodoUpdate(ctx, ws, payload) {
|
|
28
|
+
const todo = ctx.context.todos.find((t) => t.id === payload.id);
|
|
29
|
+
if (!todo) {
|
|
30
|
+
sendResult(ws, ctx, false, `No todo with id "${payload.id}".`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const next = ctx.context.todos.map(
|
|
34
|
+
(t) => t.id === payload.id ? { ...t, ...payload.status !== void 0 && { status: payload.status }, ...payload.activeForm !== void 0 && { activeForm: payload.activeForm } } : t
|
|
35
|
+
);
|
|
36
|
+
ctx.replaceTodos?.(next);
|
|
37
|
+
ctx.broadcast({ type: "todos.updated", payload: { todos: next } });
|
|
38
|
+
sendResult(ws, ctx, true, `Todo "${todo.content}" updated.`);
|
|
39
|
+
}
|
|
40
|
+
async function handleTasksGet(ctx, ws) {
|
|
41
|
+
const taskPath = ctx.context.meta["task.path"];
|
|
42
|
+
if (typeof taskPath === "string" && taskPath) {
|
|
43
|
+
try {
|
|
44
|
+
const { loadTasks } = await import("@wrongstack/core");
|
|
45
|
+
const file = await loadTasks(taskPath);
|
|
46
|
+
ctx.send(ws, { type: "tasks.updated", payload: { tasks: file?.tasks ?? [] } });
|
|
47
|
+
} catch {
|
|
48
|
+
ctx.send(ws, { type: "tasks.updated", payload: { tasks: [] } });
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
ctx.send(ws, {
|
|
52
|
+
type: "tasks.updated",
|
|
53
|
+
payload: { tasks: [], error: "Task storage not configured." }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function handleTaskUpdate(ctx, ws, payload) {
|
|
58
|
+
const taskPath = ctx.context.meta["task.path"];
|
|
59
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
60
|
+
sendResult(ws, ctx, false, "Task storage is not configured for this session.");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const { loadTasks, saveTasks } = await import("@wrongstack/core");
|
|
65
|
+
const file = await loadTasks(taskPath);
|
|
66
|
+
if (!file) {
|
|
67
|
+
sendResult(ws, ctx, false, "No task file found.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const idx = file.tasks.findIndex((t) => t.id === payload.id);
|
|
71
|
+
if (idx === -1) {
|
|
72
|
+
sendResult(ws, ctx, false, `Task "${payload.id}" not found.`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
file.tasks[idx] = { ...file.tasks[idx], status: payload.status };
|
|
76
|
+
await saveTasks(taskPath, file);
|
|
77
|
+
ctx.broadcast({ type: "tasks.updated", payload: { tasks: file.tasks } });
|
|
78
|
+
sendResult(ws, ctx, true, `Task "${payload.id}" marked ${payload.status}.`);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
sendResult(ws, ctx, false, String(err));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function handlePlanGet(ctx, ws) {
|
|
84
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
85
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
86
|
+
if (typeof planPath === "string" && planPath) {
|
|
87
|
+
try {
|
|
88
|
+
const { loadPlan } = await import("@wrongstack/core");
|
|
89
|
+
const plan = await loadPlan(planPath);
|
|
90
|
+
ctx.send(ws, {
|
|
91
|
+
type: "plan.updated",
|
|
92
|
+
payload: {
|
|
93
|
+
plan: plan ?? {
|
|
94
|
+
version: 1,
|
|
95
|
+
sessionId,
|
|
96
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
97
|
+
items: []
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
} catch {
|
|
102
|
+
ctx.send(ws, {
|
|
103
|
+
type: "plan.updated",
|
|
104
|
+
payload: {
|
|
105
|
+
plan: {
|
|
106
|
+
version: 1,
|
|
107
|
+
sessionId,
|
|
108
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
109
|
+
items: []
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
ctx.send(ws, {
|
|
116
|
+
type: "plan.updated",
|
|
117
|
+
payload: { plan: null, error: "Plan storage is not configured for this session." }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function handlePlanTemplateUse(ctx, ws, template) {
|
|
122
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
123
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
124
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
125
|
+
sendResult(ws, ctx, false, "Plan storage is not configured for this session.");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem } = await import("@wrongstack/core");
|
|
130
|
+
const tpl = getPlanTemplate(template);
|
|
131
|
+
if (!tpl) {
|
|
132
|
+
sendResult(ws, ctx, false, `Unknown template "${template}".`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
let plan = await loadPlan(planPath) ?? emptyPlan(sessionId);
|
|
136
|
+
for (const item of tpl.items) {
|
|
137
|
+
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
138
|
+
}
|
|
139
|
+
await savePlan(planPath, plan);
|
|
140
|
+
sendResult(ws, ctx, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
|
|
141
|
+
ctx.broadcast({ type: "plan.updated", payload: { plan } });
|
|
142
|
+
} catch (err) {
|
|
143
|
+
sendResult(ws, ctx, false, String(err));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function handlePlanItemUpdate(ctx, ws, payload) {
|
|
147
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
148
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
149
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
150
|
+
sendResult(ws, ctx, false, "Plan storage is not configured for this session.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const { loadPlan, savePlan, mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
|
|
155
|
+
let changed = false;
|
|
156
|
+
const plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
157
|
+
const before = p.updatedAt;
|
|
158
|
+
const updated = setPlanItemStatus(p, payload.target, payload.status);
|
|
159
|
+
changed = updated.updatedAt !== before;
|
|
160
|
+
return updated;
|
|
161
|
+
});
|
|
162
|
+
if (!changed) {
|
|
163
|
+
sendResult(ws, ctx, false, `No plan item matched "${payload.target}".`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
sendResult(ws, ctx, true, `Plan item status updated to "${payload.status}".`);
|
|
167
|
+
ctx.broadcast({ type: "plan.updated", payload: { plan } });
|
|
168
|
+
} catch (err) {
|
|
169
|
+
sendResult(ws, ctx, false, String(err));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/server/index.ts
|
|
4
174
|
import { makeMailboxTool, makeMailSendTool, makeMailInboxTool, mailboxSessionTag } from "@wrongstack/core";
|
|
5
|
-
import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
|
|
175
|
+
import { toErrorMessage as toErrorMessage5, wstackGlobalRoot as wstackGlobalRoot2, projectHash, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
176
|
+
import { SkillInstaller } from "@wrongstack/core/skills";
|
|
177
|
+
import JSZip2 from "jszip";
|
|
6
178
|
import {
|
|
7
179
|
BrainMonitor,
|
|
8
180
|
DefaultBrainArbiter,
|
|
@@ -10,8 +182,8 @@ import {
|
|
|
10
182
|
createAutonomyBrain,
|
|
11
183
|
createTieredBrainArbiter
|
|
12
184
|
} from "@wrongstack/core";
|
|
13
|
-
import * as
|
|
14
|
-
import * as
|
|
185
|
+
import * as fs9 from "fs/promises";
|
|
186
|
+
import * as path9 from "path";
|
|
15
187
|
|
|
16
188
|
// src/server/http-server.ts
|
|
17
189
|
import * as fs from "fs/promises";
|
|
@@ -53,8 +225,8 @@ function extractTokenFromCookie(cookieHeader) {
|
|
|
53
225
|
for (const part of raw.split(";")) {
|
|
54
226
|
const eq = part.indexOf("=");
|
|
55
227
|
if (eq < 0) continue;
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
228
|
+
const name2 = part.slice(0, eq).trim();
|
|
229
|
+
if (name2 === "ws_token") {
|
|
58
230
|
try {
|
|
59
231
|
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
60
232
|
} catch {
|
|
@@ -130,6 +302,13 @@ function isInsideDist(candidate, distDir) {
|
|
|
130
302
|
const resolved = path.resolve(candidate);
|
|
131
303
|
return resolved === root || resolved.startsWith(root + path.sep);
|
|
132
304
|
}
|
|
305
|
+
function decodeSessionId(segment) {
|
|
306
|
+
try {
|
|
307
|
+
return decodeURIComponent(segment);
|
|
308
|
+
} catch {
|
|
309
|
+
return segment;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
133
312
|
function createHttpServer(opts) {
|
|
134
313
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
135
314
|
const distDir = path.resolve(opts.distDir);
|
|
@@ -155,6 +334,22 @@ function createHttpServer(opts) {
|
|
|
155
334
|
res.end("ok");
|
|
156
335
|
return;
|
|
157
336
|
}
|
|
337
|
+
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
338
|
+
const headerToken = req.headers["x-ws-token"];
|
|
339
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
340
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
341
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
342
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
opts.onFleetPing?.();
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
res.writeHead(204);
|
|
350
|
+
res.end();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
158
353
|
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
159
354
|
const headerToken = req.headers["x-ws-token"];
|
|
160
355
|
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
@@ -175,7 +370,89 @@ function createHttpServer(opts) {
|
|
|
175
370
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
176
371
|
return;
|
|
177
372
|
}
|
|
178
|
-
await handleApiSessionAgents(res, opts.globalRoot, agentsMatch[1]);
|
|
373
|
+
await handleApiSessionAgents(res, opts.globalRoot, decodeSessionId(agentsMatch[1]));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const eventsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
|
|
377
|
+
if (eventsMatch && req.method === "GET") {
|
|
378
|
+
const headerToken = req.headers["x-ws-token"];
|
|
379
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
380
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
381
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
382
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
|
|
386
|
+
const limit = Math.min(500, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
|
|
387
|
+
await handleApiSessionEvents(res, opts.globalRoot, decodeSessionId(eventsMatch[1]), limit);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const msgMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/message$/);
|
|
391
|
+
if (msgMatch && req.method === "POST") {
|
|
392
|
+
const headerToken = req.headers["x-ws-token"];
|
|
393
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
394
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
395
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
396
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
await handleApiSessionMessage(res, req, opts.globalRoot, decodeSessionId(msgMatch[1]));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const mailboxMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/mailbox$/);
|
|
403
|
+
if (mailboxMatch && req.method === "GET") {
|
|
404
|
+
const headerToken = req.headers["x-ws-token"];
|
|
405
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
406
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
407
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
408
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
await handleApiSessionMailbox(res, opts.globalRoot, decodeSessionId(mailboxMatch[1]));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const interruptMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/interrupt$/);
|
|
415
|
+
if (interruptMatch && req.method === "POST") {
|
|
416
|
+
const headerToken = req.headers["x-ws-token"];
|
|
417
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
418
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
419
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
420
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
await handleApiSessionInterrupt(
|
|
424
|
+
res,
|
|
425
|
+
req,
|
|
426
|
+
opts.globalRoot,
|
|
427
|
+
decodeSessionId(interruptMatch[1])
|
|
428
|
+
);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (url.pathname === "/api/fleet/broadcast" && req.method === "POST") {
|
|
432
|
+
const headerToken = req.headers["x-ws-token"];
|
|
433
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
434
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
435
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
436
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
await handleApiFleetBroadcast(res, req, opts.globalRoot);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (url.pathname === "/debug/watcher-metrics" && req.method === "GET") {
|
|
443
|
+
if (opts.watcherMetrics) {
|
|
444
|
+
const avgDelay = opts.watcherMetrics.broadcastsSent > 0 ? opts.watcherMetrics.totalDebounceDelayMs / opts.watcherMetrics.broadcastsSent : 0;
|
|
445
|
+
const response = {
|
|
446
|
+
...opts.watcherMetrics,
|
|
447
|
+
averageDebounceDelayMs: avgDelay,
|
|
448
|
+
timestamp: Date.now()
|
|
449
|
+
};
|
|
450
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
451
|
+
res.end(JSON.stringify(response));
|
|
452
|
+
} else {
|
|
453
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
454
|
+
res.end(JSON.stringify({ error: "File watcher metrics not available" }));
|
|
455
|
+
}
|
|
179
456
|
return;
|
|
180
457
|
}
|
|
181
458
|
let filePath;
|
|
@@ -307,6 +584,324 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
|
307
584
|
res.end(JSON.stringify({ error: String(err) }));
|
|
308
585
|
}
|
|
309
586
|
}
|
|
587
|
+
function blocksToText(content) {
|
|
588
|
+
if (typeof content === "string") return content;
|
|
589
|
+
if (Array.isArray(content)) {
|
|
590
|
+
return content.filter(
|
|
591
|
+
(b) => !!b && typeof b === "object" && b.type === "text" && typeof b.text === "string"
|
|
592
|
+
).map((b) => b.text).join("\n");
|
|
593
|
+
}
|
|
594
|
+
return "";
|
|
595
|
+
}
|
|
596
|
+
function clip(s, n = 600) {
|
|
597
|
+
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
598
|
+
}
|
|
599
|
+
function asString(v) {
|
|
600
|
+
if (typeof v === "string") return v;
|
|
601
|
+
try {
|
|
602
|
+
return JSON.stringify(v);
|
|
603
|
+
} catch {
|
|
604
|
+
return String(v);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function mapWatchEntry(ev) {
|
|
608
|
+
const ts = typeof ev["ts"] === "string" ? ev["ts"] : "";
|
|
609
|
+
switch (ev["type"]) {
|
|
610
|
+
case "user_input":
|
|
611
|
+
return { ts, role: "user", text: clip(blocksToText(ev["content"])) };
|
|
612
|
+
case "llm_response": {
|
|
613
|
+
const text = blocksToText(ev["content"]);
|
|
614
|
+
return text.trim() ? { ts, role: "assistant", text: clip(text) } : null;
|
|
615
|
+
}
|
|
616
|
+
case "tool_use":
|
|
617
|
+
case "tool_call_start": {
|
|
618
|
+
const input = ev["input"] ?? ev["args"];
|
|
619
|
+
const preview = input !== void 0 && input !== null ? clip(asString(input), 160) : "";
|
|
620
|
+
return { ts, role: "tool", tool: String(ev["name"] ?? "tool"), text: preview };
|
|
621
|
+
}
|
|
622
|
+
case "tool_result": {
|
|
623
|
+
if (ev["isError"]) return { ts, role: "error", text: clip(asString(ev["content"])) };
|
|
624
|
+
const out = asString(ev["content"]).trim();
|
|
625
|
+
return out ? { ts, role: "tool", tool: "\u21B3 result", text: clip(out, 240) } : null;
|
|
626
|
+
}
|
|
627
|
+
case "error":
|
|
628
|
+
case "provider_error":
|
|
629
|
+
return { ts, role: "error", text: clip(String(ev["message"] ?? "error")) };
|
|
630
|
+
case "agent_spawned":
|
|
631
|
+
return { ts, role: "system", text: `spawned ${String(ev["role"] ?? "agent")}` };
|
|
632
|
+
case "task_completed":
|
|
633
|
+
return { ts, role: "system", text: `task done: ${String(ev["title"] ?? "")}` };
|
|
634
|
+
case "task_failed":
|
|
635
|
+
return { ts, role: "system", text: `task failed: ${String(ev["title"] ?? "")}` };
|
|
636
|
+
default:
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
641
|
+
if (!globalRoot) {
|
|
642
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
643
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
|
|
648
|
+
const registry = new SessionRegistry(globalRoot);
|
|
649
|
+
const entry = await registry.get(sessionId);
|
|
650
|
+
if (!entry) {
|
|
651
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
652
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
656
|
+
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
657
|
+
const reader = new DefaultSessionReader2({ store });
|
|
658
|
+
const all = [];
|
|
659
|
+
for await (const ev of reader.replay(sessionId)) {
|
|
660
|
+
const mapped = mapWatchEntry(ev);
|
|
661
|
+
if (mapped) all.push(mapped);
|
|
662
|
+
}
|
|
663
|
+
const tail = all.slice(-limit);
|
|
664
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
665
|
+
res.end(
|
|
666
|
+
JSON.stringify({
|
|
667
|
+
sessionId,
|
|
668
|
+
status: entry.status,
|
|
669
|
+
clientType: entry.clientType,
|
|
670
|
+
projectName: entry.projectName,
|
|
671
|
+
total: all.length,
|
|
672
|
+
entries: tail
|
|
673
|
+
})
|
|
674
|
+
);
|
|
675
|
+
} catch (err) {
|
|
676
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
677
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function readJsonBody(req) {
|
|
681
|
+
return new Promise((resolve5, reject) => {
|
|
682
|
+
let data = "";
|
|
683
|
+
req.on("data", (chunk) => {
|
|
684
|
+
data += chunk;
|
|
685
|
+
if (data.length > 64e3) {
|
|
686
|
+
reject(new Error("Request body too large"));
|
|
687
|
+
req.destroy();
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
req.on("end", () => {
|
|
691
|
+
try {
|
|
692
|
+
resolve5(data ? JSON.parse(data) : {});
|
|
693
|
+
} catch (err) {
|
|
694
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
req.on("error", reject);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
701
|
+
if (!globalRoot) {
|
|
702
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
703
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
let body;
|
|
707
|
+
try {
|
|
708
|
+
body = await readJsonBody(req);
|
|
709
|
+
} catch {
|
|
710
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
711
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const text = typeof body["text"] === "string" ? body["text"].trim() : "";
|
|
715
|
+
if (!text) {
|
|
716
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
717
|
+
res.end(JSON.stringify({ error: "text is required" }));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
721
|
+
const ALLOWED = /* @__PURE__ */ new Set(["steer", "ask", "assign", "note", "btw"]);
|
|
722
|
+
const rawType = typeof body["type"] === "string" ? body["type"] : "steer";
|
|
723
|
+
const type = ALLOWED.has(rawType) ? rawType : "steer";
|
|
724
|
+
const rawPriority = typeof body["priority"] === "string" ? body["priority"] : "";
|
|
725
|
+
const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
|
|
726
|
+
const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
|
|
727
|
+
try {
|
|
728
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
729
|
+
const registry = new SessionRegistry(globalRoot);
|
|
730
|
+
const entry = await registry.get(sessionId);
|
|
731
|
+
if (!entry) {
|
|
732
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
733
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
737
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
738
|
+
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
739
|
+
const sent = await mailbox.send({ from, to, type, subject, body: text, priority });
|
|
740
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
741
|
+
res.end(JSON.stringify({ ok: true, id: sent.id, to, type, delivered: entry.status }));
|
|
742
|
+
} catch (err) {
|
|
743
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
744
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
748
|
+
if (!globalRoot) {
|
|
749
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
750
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
755
|
+
const registry = new SessionRegistry(globalRoot);
|
|
756
|
+
const entry = await registry.get(sessionId);
|
|
757
|
+
if (!entry) {
|
|
758
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
759
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
763
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
764
|
+
const leaderAddr = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
765
|
+
const [inbound, outbound] = await Promise.all([
|
|
766
|
+
mailbox.query({ to: leaderAddr, limit: 50 }),
|
|
767
|
+
mailbox.query({ from: leaderAddr, limit: 50 })
|
|
768
|
+
]);
|
|
769
|
+
const seen = /* @__PURE__ */ new Set();
|
|
770
|
+
const thread = [...inbound, ...outbound].filter((m) => {
|
|
771
|
+
if (seen.has(m.id)) return false;
|
|
772
|
+
seen.add(m.id);
|
|
773
|
+
return true;
|
|
774
|
+
}).sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)).map((m) => ({
|
|
775
|
+
id: m.id,
|
|
776
|
+
from: m.from,
|
|
777
|
+
to: m.to,
|
|
778
|
+
type: m.type,
|
|
779
|
+
subject: m.subject,
|
|
780
|
+
body: m.body,
|
|
781
|
+
priority: m.priority,
|
|
782
|
+
// Whether the leader has read it, and when.
|
|
783
|
+
readByLeader: m.readBy?.[leaderAddr] ?? null,
|
|
784
|
+
readByCount: Object.keys(m.readBy ?? {}).length,
|
|
785
|
+
completed: m.completed,
|
|
786
|
+
outcome: m.outcome ?? null,
|
|
787
|
+
timestamp: m.timestamp,
|
|
788
|
+
replyTo: m.replyTo ?? null,
|
|
789
|
+
fromLeader: m.from === leaderAddr
|
|
790
|
+
}));
|
|
791
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
792
|
+
res.end(JSON.stringify({ sessionId, leader: leaderAddr, status: entry.status, thread }));
|
|
793
|
+
} catch (err) {
|
|
794
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
795
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
799
|
+
if (!globalRoot) {
|
|
800
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
801
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
let body = {};
|
|
805
|
+
try {
|
|
806
|
+
body = await readJsonBody(req);
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
|
|
810
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
811
|
+
try {
|
|
812
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
813
|
+
const registry = new SessionRegistry(globalRoot);
|
|
814
|
+
const entry = await registry.get(sessionId);
|
|
815
|
+
if (!entry) {
|
|
816
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
817
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
821
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
822
|
+
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
823
|
+
const sent = await mailbox.send({
|
|
824
|
+
from,
|
|
825
|
+
to,
|
|
826
|
+
type: "control",
|
|
827
|
+
subject: "interrupt",
|
|
828
|
+
body: reason,
|
|
829
|
+
priority: "high"
|
|
830
|
+
});
|
|
831
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
832
|
+
res.end(JSON.stringify({ ok: true, id: sent.id, to, delivered: entry.status }));
|
|
833
|
+
} catch (err) {
|
|
834
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
835
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
839
|
+
if (!globalRoot) {
|
|
840
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
841
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
let body;
|
|
845
|
+
try {
|
|
846
|
+
body = await readJsonBody(req);
|
|
847
|
+
} catch {
|
|
848
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
849
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const text = typeof body["text"] === "string" ? body["text"].trim() : "";
|
|
853
|
+
if (!text) {
|
|
854
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
855
|
+
res.end(JSON.stringify({ error: "text is required" }));
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
859
|
+
try {
|
|
860
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
861
|
+
const registry = new SessionRegistry(globalRoot);
|
|
862
|
+
const all = await registry.list();
|
|
863
|
+
const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
|
|
864
|
+
const targets = all.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true);
|
|
865
|
+
if (targets.length === 0) {
|
|
866
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
867
|
+
res.end(JSON.stringify({ ok: true, delivered: 0 }));
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const mbByDir = /* @__PURE__ */ new Map();
|
|
871
|
+
const mailboxFor = (projectRoot) => {
|
|
872
|
+
const dir = resolveWstackPaths2({ projectRoot, globalRoot }).projectDir;
|
|
873
|
+
let mb = mbByDir.get(dir);
|
|
874
|
+
if (!mb) {
|
|
875
|
+
mb = new GlobalMailbox3(dir);
|
|
876
|
+
mbByDir.set(dir, mb);
|
|
877
|
+
}
|
|
878
|
+
return mb;
|
|
879
|
+
};
|
|
880
|
+
let delivered = 0;
|
|
881
|
+
await Promise.all(
|
|
882
|
+
targets.map(async (s) => {
|
|
883
|
+
try {
|
|
884
|
+
const mb = mailboxFor(s.projectRoot);
|
|
885
|
+
await mb.send({
|
|
886
|
+
from,
|
|
887
|
+
to: `leader@${mailboxSessionTag2(s.sessionId)}`,
|
|
888
|
+
type: "steer",
|
|
889
|
+
subject: "Broadcast from Fleet HQ",
|
|
890
|
+
body: text,
|
|
891
|
+
priority: "high"
|
|
892
|
+
});
|
|
893
|
+
delivered++;
|
|
894
|
+
} catch {
|
|
895
|
+
}
|
|
896
|
+
})
|
|
897
|
+
);
|
|
898
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
899
|
+
res.end(JSON.stringify({ ok: true, delivered, targets: targets.length }));
|
|
900
|
+
} catch (err) {
|
|
901
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
902
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
310
905
|
|
|
311
906
|
// src/server/file-handlers.ts
|
|
312
907
|
import * as fs2 from "fs/promises";
|
|
@@ -336,8 +931,8 @@ var KEEP_DOTFILES = /* @__PURE__ */ new Set([
|
|
|
336
931
|
".eslintrc",
|
|
337
932
|
".prettierrc"
|
|
338
933
|
]);
|
|
339
|
-
function isHiddenEntry(
|
|
340
|
-
return
|
|
934
|
+
function isHiddenEntry(name2) {
|
|
935
|
+
return name2.startsWith(".") && !KEEP_DOTFILES.has(name2);
|
|
341
936
|
}
|
|
342
937
|
function rankFiles(paths, query, limit) {
|
|
343
938
|
const q = query.toLowerCase();
|
|
@@ -380,7 +975,7 @@ function broadcast(clients, msg) {
|
|
|
380
975
|
}
|
|
381
976
|
}
|
|
382
977
|
}
|
|
383
|
-
function
|
|
978
|
+
function sendResult2(ws, success, message) {
|
|
384
979
|
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
385
980
|
}
|
|
386
981
|
function errMessage(err) {
|
|
@@ -529,25 +1124,240 @@ async function handleMemoryRemember(ws, msg, memoryStore) {
|
|
|
529
1124
|
const { text, scope } = msg.payload;
|
|
530
1125
|
try {
|
|
531
1126
|
await memoryStore.remember(text, scope ?? "project-memory");
|
|
532
|
-
|
|
1127
|
+
sendResult2(ws, true, "Saved to memory");
|
|
533
1128
|
} catch (err) {
|
|
534
|
-
|
|
1129
|
+
sendResult2(ws, false, errMessage(err));
|
|
535
1130
|
}
|
|
536
1131
|
}
|
|
537
1132
|
async function handleMemoryForget(ws, msg, memoryStore) {
|
|
538
1133
|
const { text, scope } = msg.payload;
|
|
539
1134
|
try {
|
|
540
1135
|
const removed = await memoryStore.forget(text, scope ?? "project-memory");
|
|
541
|
-
|
|
1136
|
+
sendResult2(
|
|
542
1137
|
ws,
|
|
543
1138
|
removed > 0,
|
|
544
1139
|
removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
|
|
545
1140
|
);
|
|
546
1141
|
} catch (err) {
|
|
547
|
-
|
|
1142
|
+
sendResult2(ws, false, errMessage(err));
|
|
548
1143
|
}
|
|
549
1144
|
}
|
|
550
1145
|
|
|
1146
|
+
// src/server/mcp-handlers.ts
|
|
1147
|
+
import { allServers } from "@wrongstack/core";
|
|
1148
|
+
import {
|
|
1149
|
+
addMcp,
|
|
1150
|
+
disableMcp,
|
|
1151
|
+
discoverMcp,
|
|
1152
|
+
enableMcp,
|
|
1153
|
+
listMcp,
|
|
1154
|
+
removeMcp,
|
|
1155
|
+
restartMcp,
|
|
1156
|
+
updateMcp
|
|
1157
|
+
} from "@wrongstack/mcp";
|
|
1158
|
+
function mapStatus(raw) {
|
|
1159
|
+
switch (raw) {
|
|
1160
|
+
case "connected":
|
|
1161
|
+
return "connected";
|
|
1162
|
+
case "connecting":
|
|
1163
|
+
case "reconnecting":
|
|
1164
|
+
return "connecting";
|
|
1165
|
+
case "failed":
|
|
1166
|
+
return "error";
|
|
1167
|
+
case "dormant":
|
|
1168
|
+
return "sleeping";
|
|
1169
|
+
default:
|
|
1170
|
+
return "stopped";
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function toView(info) {
|
|
1174
|
+
const view = {
|
|
1175
|
+
name: info.name,
|
|
1176
|
+
transport: info.transport,
|
|
1177
|
+
// A dormant lazy server is "asleep", not stopped — preserve that even when
|
|
1178
|
+
// it's enabled in config.
|
|
1179
|
+
status: info.status === "dormant" ? "sleeping" : info.enabled === false ? "stopped" : mapStatus(info.status),
|
|
1180
|
+
enabled: info.enabled,
|
|
1181
|
+
tools: info.tools
|
|
1182
|
+
};
|
|
1183
|
+
if (info.description !== void 0) view.description = info.description;
|
|
1184
|
+
if (info.lazy !== void 0) view.lazy = info.lazy;
|
|
1185
|
+
return view;
|
|
1186
|
+
}
|
|
1187
|
+
function deps(ws, globalConfigPath, registry) {
|
|
1188
|
+
if (!registry || !globalConfigPath) {
|
|
1189
|
+
send(ws, {
|
|
1190
|
+
type: "mcp.operation_result",
|
|
1191
|
+
payload: { success: false, message: "MCP registry is not available in this session." }
|
|
1192
|
+
});
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
return { configPath: globalConfigPath, registry, presets: allServers() };
|
|
1196
|
+
}
|
|
1197
|
+
function name(msg) {
|
|
1198
|
+
return msg.payload?.name ?? "";
|
|
1199
|
+
}
|
|
1200
|
+
async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
|
|
1201
|
+
if (!mcpRegistry || !globalConfigPath) {
|
|
1202
|
+
send(ws, { type: "mcp.list", payload: { servers: [] } });
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const servers = await listMcp({
|
|
1206
|
+
configPath: globalConfigPath,
|
|
1207
|
+
registry: mcpRegistry,
|
|
1208
|
+
presets: allServers()
|
|
1209
|
+
});
|
|
1210
|
+
send(ws, { type: "mcp.list", payload: { servers: servers.map(toView) } });
|
|
1211
|
+
}
|
|
1212
|
+
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1213
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1214
|
+
if (!d) return;
|
|
1215
|
+
const result = await addMcp(msg.payload, d);
|
|
1216
|
+
if (result.ok && result.server) {
|
|
1217
|
+
send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
|
|
1218
|
+
if (result.registryError) {
|
|
1219
|
+
send(ws, {
|
|
1220
|
+
type: "mcp.server.error",
|
|
1221
|
+
payload: { name: result.server.name, error: result.registryError }
|
|
1222
|
+
});
|
|
1223
|
+
} else if (result.server.enabled) {
|
|
1224
|
+
send(ws, { type: "mcp.server.connected", payload: { name: result.server.name } });
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
send(ws, {
|
|
1228
|
+
type: "mcp.operation_result",
|
|
1229
|
+
payload: { success: result.ok, message: result.message }
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1233
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1234
|
+
if (!d) return;
|
|
1235
|
+
const result = await updateMcp(msg.payload, d);
|
|
1236
|
+
if (result.ok && result.server) {
|
|
1237
|
+
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
1238
|
+
}
|
|
1239
|
+
send(ws, {
|
|
1240
|
+
type: "mcp.operation_result",
|
|
1241
|
+
payload: { success: result.ok, message: result.message }
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
async function handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1245
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1246
|
+
if (!d) return;
|
|
1247
|
+
const result = await removeMcp(name(msg), d);
|
|
1248
|
+
if (result.ok) {
|
|
1249
|
+
send(ws, { type: "mcp.server.removed", payload: { name: name(msg) } });
|
|
1250
|
+
}
|
|
1251
|
+
send(ws, {
|
|
1252
|
+
type: "mcp.operation_result",
|
|
1253
|
+
payload: { success: result.ok, message: result.message }
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
async function handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1257
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1258
|
+
if (!d) return;
|
|
1259
|
+
const result = await enableMcp(name(msg), d);
|
|
1260
|
+
if (result.ok && result.server) {
|
|
1261
|
+
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
1262
|
+
if (result.registryError) {
|
|
1263
|
+
send(ws, {
|
|
1264
|
+
type: "mcp.server.error",
|
|
1265
|
+
payload: { name: name(msg), error: result.registryError }
|
|
1266
|
+
});
|
|
1267
|
+
} else {
|
|
1268
|
+
send(ws, { type: "mcp.server.connected", payload: { name: name(msg) } });
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
send(ws, {
|
|
1272
|
+
type: "mcp.operation_result",
|
|
1273
|
+
payload: { success: result.ok, message: result.message }
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
async function handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1277
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1278
|
+
if (!d) return;
|
|
1279
|
+
const result = await disableMcp(name(msg), d);
|
|
1280
|
+
if (result.ok) {
|
|
1281
|
+
send(ws, { type: "mcp.server.sleeping", payload: { name: name(msg) } });
|
|
1282
|
+
if (result.server) {
|
|
1283
|
+
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
send(ws, {
|
|
1287
|
+
type: "mcp.operation_result",
|
|
1288
|
+
payload: { success: result.ok, message: result.message }
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
async function handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1292
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1293
|
+
if (!d) return;
|
|
1294
|
+
try {
|
|
1295
|
+
await d.registry.stop(name(msg));
|
|
1296
|
+
send(ws, { type: "mcp.server.sleeping", payload: { name: name(msg) } });
|
|
1297
|
+
send(ws, {
|
|
1298
|
+
type: "mcp.operation_result",
|
|
1299
|
+
payload: { success: true, message: `Server "${name(msg)}" stopped` }
|
|
1300
|
+
});
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
1303
|
+
send(ws, { type: "mcp.server.error", payload: { name: name(msg), error } });
|
|
1304
|
+
send(ws, {
|
|
1305
|
+
type: "mcp.operation_result",
|
|
1306
|
+
payload: { success: false, message: `Failed to stop "${name(msg)}": ${error}` }
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
async function handleMcpWake(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1311
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1312
|
+
if (!d) return;
|
|
1313
|
+
send(ws, { type: "mcp.server.waking", payload: { name: name(msg) } });
|
|
1314
|
+
const result = await restartMcp(name(msg), d);
|
|
1315
|
+
if (result.ok && !result.registryError) {
|
|
1316
|
+
send(ws, { type: "mcp.server.connected", payload: { name: name(msg) } });
|
|
1317
|
+
} else if (result.registryError) {
|
|
1318
|
+
send(ws, {
|
|
1319
|
+
type: "mcp.server.error",
|
|
1320
|
+
payload: { name: name(msg), error: result.registryError }
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
send(ws, {
|
|
1324
|
+
type: "mcp.operation_result",
|
|
1325
|
+
payload: { success: result.ok, message: result.message }
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
async function handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1329
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1330
|
+
if (!d) return;
|
|
1331
|
+
const result = await restartMcp(name(msg), d);
|
|
1332
|
+
if (result.ok && !result.registryError) {
|
|
1333
|
+
send(ws, { type: "mcp.server.connected", payload: { name: name(msg) } });
|
|
1334
|
+
} else if (result.registryError) {
|
|
1335
|
+
send(ws, {
|
|
1336
|
+
type: "mcp.server.error",
|
|
1337
|
+
payload: { name: name(msg), error: result.registryError }
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
send(ws, {
|
|
1341
|
+
type: "mcp.operation_result",
|
|
1342
|
+
payload: { success: result.ok, message: result.message }
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1346
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1347
|
+
if (!d) return;
|
|
1348
|
+
const result = await discoverMcp(name(msg), d);
|
|
1349
|
+
if (result.ok) {
|
|
1350
|
+
send(ws, {
|
|
1351
|
+
type: "mcp.server.discovered",
|
|
1352
|
+
payload: { name: name(msg), tools: result.tools ?? [] }
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
send(ws, {
|
|
1356
|
+
type: "mcp.operation_result",
|
|
1357
|
+
payload: { success: result.ok, message: result.message }
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
551
1361
|
// src/server/index.ts
|
|
552
1362
|
import {
|
|
553
1363
|
Agent,
|
|
@@ -581,12 +1391,14 @@ import {
|
|
|
581
1391
|
repairToolUseAdjacency,
|
|
582
1392
|
resolveContextWindowPolicy,
|
|
583
1393
|
enhanceUserPrompt,
|
|
584
|
-
recentTextTurns
|
|
1394
|
+
recentTextTurns,
|
|
1395
|
+
resolveProviderModelList
|
|
585
1396
|
} from "@wrongstack/core";
|
|
586
1397
|
import { ToolExecutor } from "@wrongstack/core/execution";
|
|
587
1398
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
|
|
588
1399
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
|
|
589
1400
|
import { builtinToolsPack, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
|
|
1401
|
+
import { MCPRegistry } from "@wrongstack/mcp";
|
|
590
1402
|
import { WebSocketServer } from "ws";
|
|
591
1403
|
|
|
592
1404
|
// ../runtime/src/container.ts
|
|
@@ -1833,9 +2645,9 @@ var WorktreeWebSocketHandler = class {
|
|
|
1833
2645
|
|
|
1834
2646
|
// src/server/mailbox-handlers.ts
|
|
1835
2647
|
import { GlobalMailbox, resolveProjectDir } from "@wrongstack/core";
|
|
1836
|
-
async function handleMailboxMessages(ws,
|
|
2648
|
+
async function handleMailboxMessages(ws, deps2, payload) {
|
|
1837
2649
|
try {
|
|
1838
|
-
const dir = resolveProjectDir(
|
|
2650
|
+
const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
|
|
1839
2651
|
const mb = new GlobalMailbox(dir);
|
|
1840
2652
|
const messages = await mb.query({
|
|
1841
2653
|
limit: payload?.limit ?? 30,
|
|
@@ -1869,9 +2681,9 @@ async function handleMailboxMessages(ws, deps, payload) {
|
|
|
1869
2681
|
send(ws, { type: "mailbox.messages", payload: { messages: [], error: errMessage(err) } });
|
|
1870
2682
|
}
|
|
1871
2683
|
}
|
|
1872
|
-
async function handleMailboxAgents(ws,
|
|
2684
|
+
async function handleMailboxAgents(ws, deps2, payload) {
|
|
1873
2685
|
try {
|
|
1874
|
-
const dir = resolveProjectDir(
|
|
2686
|
+
const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
|
|
1875
2687
|
const mb = new GlobalMailbox(dir);
|
|
1876
2688
|
const agents = payload?.onlineOnly ? await mb.getOnlineAgents() : await mb.getAgentStatuses();
|
|
1877
2689
|
send(ws, {
|
|
@@ -1898,9 +2710,9 @@ async function handleMailboxAgents(ws, deps, payload) {
|
|
|
1898
2710
|
send(ws, { type: "mailbox.agents", payload: { agents: [], error: errMessage(err) } });
|
|
1899
2711
|
}
|
|
1900
2712
|
}
|
|
1901
|
-
async function handleMailboxClear(ws,
|
|
2713
|
+
async function handleMailboxClear(ws, deps2) {
|
|
1902
2714
|
try {
|
|
1903
|
-
const dir = resolveProjectDir(
|
|
2715
|
+
const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
|
|
1904
2716
|
const mb = new GlobalMailbox(dir);
|
|
1905
2717
|
await mb.clearAll();
|
|
1906
2718
|
send(ws, { type: "mailbox.cleared", payload: {} });
|
|
@@ -1908,9 +2720,9 @@ async function handleMailboxClear(ws, deps) {
|
|
|
1908
2720
|
send(ws, { type: "mailbox.cleared", payload: { error: errMessage(err) } });
|
|
1909
2721
|
}
|
|
1910
2722
|
}
|
|
1911
|
-
async function handleMailboxPurge(ws,
|
|
2723
|
+
async function handleMailboxPurge(ws, deps2, opts) {
|
|
1912
2724
|
try {
|
|
1913
|
-
const dir = resolveProjectDir(
|
|
2725
|
+
const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
|
|
1914
2726
|
const mb = new GlobalMailbox(dir);
|
|
1915
2727
|
const result = await mb.purgeStale(opts);
|
|
1916
2728
|
send(ws, { type: "mailbox.purged", payload: result });
|
|
@@ -2201,7 +3013,7 @@ function writeKeysBack(cfg, keys) {
|
|
|
2201
3013
|
}
|
|
2202
3014
|
cfg.apiKeys = keys;
|
|
2203
3015
|
const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined(keys[0]);
|
|
2204
|
-
cfg.apiKey
|
|
3016
|
+
delete cfg.apiKey;
|
|
2205
3017
|
if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
|
|
2206
3018
|
cfg.activeKey = active.label;
|
|
2207
3019
|
}
|
|
@@ -2300,9 +3112,9 @@ function projectSavedProviders(providers) {
|
|
|
2300
3112
|
});
|
|
2301
3113
|
}
|
|
2302
3114
|
var probeScrubber = new DefaultSecretScrubber2();
|
|
2303
|
-
function createProviderHandlers(
|
|
2304
|
-
const { globalConfigPath, vault, broadcast: broadcast2, clients } =
|
|
2305
|
-
let configWriteLock =
|
|
3115
|
+
function createProviderHandlers(deps2) {
|
|
3116
|
+
const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
|
|
3117
|
+
let configWriteLock = deps2.getConfigWriteLock();
|
|
2306
3118
|
async function loadConfigProviders() {
|
|
2307
3119
|
return loadSavedProviders(globalConfigPath, vault);
|
|
2308
3120
|
}
|
|
@@ -2317,7 +3129,7 @@ function createProviderHandlers(deps) {
|
|
|
2317
3129
|
}));
|
|
2318
3130
|
});
|
|
2319
3131
|
configWriteLock = next;
|
|
2320
|
-
|
|
3132
|
+
deps2.setConfigWriteLock(next);
|
|
2321
3133
|
await next;
|
|
2322
3134
|
}
|
|
2323
3135
|
async function handleKeyUpsert(ws, providerId, label, apiKey) {
|
|
@@ -2325,9 +3137,9 @@ function createProviderHandlers(deps) {
|
|
|
2325
3137
|
const providers = await loadConfigProviders();
|
|
2326
3138
|
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
2327
3139
|
if (result.ok) await saveConfigProviders(providers);
|
|
2328
|
-
|
|
3140
|
+
sendResult2(ws, result.ok, result.message);
|
|
2329
3141
|
} catch (err) {
|
|
2330
|
-
|
|
3142
|
+
sendResult2(ws, false, errMessage(err));
|
|
2331
3143
|
}
|
|
2332
3144
|
}
|
|
2333
3145
|
async function handleKeyDelete(ws, providerId, label) {
|
|
@@ -2335,9 +3147,9 @@ function createProviderHandlers(deps) {
|
|
|
2335
3147
|
const providers = await loadConfigProviders();
|
|
2336
3148
|
const result = deleteKey(providers, providerId, label);
|
|
2337
3149
|
if (result.ok) await saveConfigProviders(providers);
|
|
2338
|
-
|
|
3150
|
+
sendResult2(ws, result.ok, result.message);
|
|
2339
3151
|
} catch (err) {
|
|
2340
|
-
|
|
3152
|
+
sendResult2(ws, false, errMessage(err));
|
|
2341
3153
|
}
|
|
2342
3154
|
}
|
|
2343
3155
|
async function handleKeySetActive(ws, providerId, label) {
|
|
@@ -2345,9 +3157,9 @@ function createProviderHandlers(deps) {
|
|
|
2345
3157
|
const providers = await loadConfigProviders();
|
|
2346
3158
|
const result = setActiveKey(providers, providerId, label);
|
|
2347
3159
|
if (result.ok) await saveConfigProviders(providers);
|
|
2348
|
-
|
|
3160
|
+
sendResult2(ws, result.ok, result.message);
|
|
2349
3161
|
} catch (err) {
|
|
2350
|
-
|
|
3162
|
+
sendResult2(ws, false, errMessage(err));
|
|
2351
3163
|
}
|
|
2352
3164
|
}
|
|
2353
3165
|
async function handleProviderAdd(ws, payload) {
|
|
@@ -2355,13 +3167,13 @@ function createProviderHandlers(deps) {
|
|
|
2355
3167
|
const providers = await loadConfigProviders();
|
|
2356
3168
|
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
2357
3169
|
if (result.ok) await saveConfigProviders(providers);
|
|
2358
|
-
|
|
3170
|
+
sendResult2(ws, result.ok, result.message);
|
|
2359
3171
|
if (result.ok) {
|
|
2360
3172
|
console.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
2361
3173
|
broadcastSaved(providers);
|
|
2362
3174
|
}
|
|
2363
3175
|
} catch (err) {
|
|
2364
|
-
|
|
3176
|
+
sendResult2(ws, false, errMessage(err));
|
|
2365
3177
|
}
|
|
2366
3178
|
}
|
|
2367
3179
|
async function handleProviderRemove(ws, providerId) {
|
|
@@ -2369,9 +3181,9 @@ function createProviderHandlers(deps) {
|
|
|
2369
3181
|
const providers = await loadConfigProviders();
|
|
2370
3182
|
const result = removeProvider(providers, providerId);
|
|
2371
3183
|
if (result.ok) await saveConfigProviders(providers);
|
|
2372
|
-
|
|
3184
|
+
sendResult2(ws, result.ok, result.message);
|
|
2373
3185
|
} catch (err) {
|
|
2374
|
-
|
|
3186
|
+
sendResult2(ws, false, errMessage(err));
|
|
2375
3187
|
}
|
|
2376
3188
|
}
|
|
2377
3189
|
function broadcastSaved(providers) {
|
|
@@ -2385,15 +3197,15 @@ function createProviderHandlers(deps) {
|
|
|
2385
3197
|
const providers = await loadConfigProviders();
|
|
2386
3198
|
const cfg = providers[providerId];
|
|
2387
3199
|
if (!cfg) {
|
|
2388
|
-
|
|
3200
|
+
sendResult2(ws, false, `Unknown provider "${providerId}"`);
|
|
2389
3201
|
return;
|
|
2390
3202
|
}
|
|
2391
3203
|
delete cfg.models;
|
|
2392
3204
|
await saveConfigProviders(providers);
|
|
2393
|
-
|
|
3205
|
+
sendResult2(ws, true, `Cleared model allowlist for ${providerId}`);
|
|
2394
3206
|
broadcastSaved(providers);
|
|
2395
3207
|
} catch (err) {
|
|
2396
|
-
|
|
3208
|
+
sendResult2(ws, false, errMessage(err));
|
|
2397
3209
|
}
|
|
2398
3210
|
}
|
|
2399
3211
|
async function handleProviderUndoClear(ws, providerId, previousModels) {
|
|
@@ -2401,15 +3213,15 @@ function createProviderHandlers(deps) {
|
|
|
2401
3213
|
const providers = await loadConfigProviders();
|
|
2402
3214
|
const cfg = providers[providerId];
|
|
2403
3215
|
if (!cfg) {
|
|
2404
|
-
|
|
3216
|
+
sendResult2(ws, false, `Unknown provider "${providerId}"`);
|
|
2405
3217
|
return;
|
|
2406
3218
|
}
|
|
2407
3219
|
cfg.models = [...previousModels];
|
|
2408
3220
|
await saveConfigProviders(providers);
|
|
2409
|
-
|
|
3221
|
+
sendResult2(ws, true, `Restored ${previousModels.length} model(s) for ${providerId}`);
|
|
2410
3222
|
broadcastSaved(providers);
|
|
2411
3223
|
} catch (err) {
|
|
2412
|
-
|
|
3224
|
+
sendResult2(ws, false, errMessage(err));
|
|
2413
3225
|
}
|
|
2414
3226
|
}
|
|
2415
3227
|
async function handleProviderUpdate(ws, payload) {
|
|
@@ -2417,7 +3229,7 @@ function createProviderHandlers(deps) {
|
|
|
2417
3229
|
const providers = await loadConfigProviders();
|
|
2418
3230
|
const cfg = providers[payload.id];
|
|
2419
3231
|
if (!cfg) {
|
|
2420
|
-
|
|
3232
|
+
sendResult2(ws, false, `Unknown provider "${payload.id}"`);
|
|
2421
3233
|
return;
|
|
2422
3234
|
}
|
|
2423
3235
|
if (payload.family !== void 0) cfg.family = payload.family;
|
|
@@ -2425,10 +3237,10 @@ function createProviderHandlers(deps) {
|
|
|
2425
3237
|
if (payload.envVars !== void 0) cfg.envVars = payload.envVars;
|
|
2426
3238
|
if (payload.models !== void 0) cfg.models = payload.models;
|
|
2427
3239
|
await saveConfigProviders(providers);
|
|
2428
|
-
|
|
3240
|
+
sendResult2(ws, true, `Updated ${payload.id}`);
|
|
2429
3241
|
broadcastSaved(providers);
|
|
2430
3242
|
} catch (err) {
|
|
2431
|
-
|
|
3243
|
+
sendResult2(ws, false, errMessage(err));
|
|
2432
3244
|
}
|
|
2433
3245
|
}
|
|
2434
3246
|
async function handleProviderProbe(ws, providerId, timeoutMs) {
|
|
@@ -2473,9 +3285,12 @@ function createProviderHandlers(deps) {
|
|
|
2473
3285
|
}
|
|
2474
3286
|
|
|
2475
3287
|
// src/server/setup-events.ts
|
|
3288
|
+
import * as fs5 from "fs/promises";
|
|
3289
|
+
import { watch as fsWatch } from "fs";
|
|
2476
3290
|
import * as path5 from "path";
|
|
2477
|
-
function setupEvents(
|
|
2478
|
-
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge } =
|
|
3291
|
+
function setupEvents(deps2) {
|
|
3292
|
+
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
|
|
3293
|
+
const disposers = [];
|
|
2479
3294
|
events.on("iteration.started", (e) => {
|
|
2480
3295
|
const maxIt = typeof context.meta["maxIterations"] === "number" ? context.meta["maxIterations"] : config.tools?.maxIterations ?? 100;
|
|
2481
3296
|
broadcast2(clients, {
|
|
@@ -2506,7 +3321,11 @@ function setupEvents(deps) {
|
|
|
2506
3321
|
events.on("tool.progress", (e) => {
|
|
2507
3322
|
broadcast2(clients, {
|
|
2508
3323
|
type: "tool.progress",
|
|
2509
|
-
|
|
3324
|
+
// Nested `event` shape — the client handler reads `payload.event?.text`
|
|
3325
|
+
// and early-returns on a falsy text, so a flat { eventType, text } payload
|
|
3326
|
+
// makes live tool progress (bash streaming, partial_output, warnings)
|
|
3327
|
+
// never render. Must match WSToolProgress and the CLI server.
|
|
3328
|
+
payload: { id: e.id, name: e.name, event: { type: e.event.type, text: e.event.text, data: e.event.data } }
|
|
2510
3329
|
});
|
|
2511
3330
|
sessionBridge?.append({
|
|
2512
3331
|
type: "tool_progress",
|
|
@@ -2672,20 +3491,165 @@ function setupEvents(deps) {
|
|
|
2672
3491
|
events.onPattern("brain.*", (eventName, payload) => {
|
|
2673
3492
|
broadcast2(clients, { type: "brain.event", payload: { event: eventName, ...payload } });
|
|
2674
3493
|
});
|
|
3494
|
+
events.on("client.status", async (e) => {
|
|
3495
|
+
broadcast2(clients, { type: "client.status_update", payload: e });
|
|
3496
|
+
if (wpaths?.projectStatus) {
|
|
3497
|
+
try {
|
|
3498
|
+
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
3499
|
+
const dir = path5.dirname(statusFile);
|
|
3500
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
3501
|
+
await fs5.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
|
|
3502
|
+
} catch (err) {
|
|
3503
|
+
console.error("[setup-events] Failed to write status.json:", err);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
});
|
|
3507
|
+
if (wpaths?.projectStatus && wpaths.configDir) {
|
|
3508
|
+
const projectsDir = path5.join(wpaths.configDir, "projects");
|
|
3509
|
+
const knownProjectHashes = /* @__PURE__ */ new Set();
|
|
3510
|
+
const debounceTimers = /* @__PURE__ */ new Map();
|
|
3511
|
+
const DEBOUNCE_MS = 150;
|
|
3512
|
+
const pendingStatuses = /* @__PURE__ */ new Map();
|
|
3513
|
+
if (watcherMetrics) {
|
|
3514
|
+
watcherMetrics.fileChangesDetected = 0;
|
|
3515
|
+
watcherMetrics.filesProcessed = 0;
|
|
3516
|
+
watcherMetrics.broadcastsSent = 0;
|
|
3517
|
+
watcherMetrics.debounceResets = 0;
|
|
3518
|
+
watcherMetrics.totalDebounceDelayMs = 0;
|
|
3519
|
+
watcherMetrics.activeProjects = 0;
|
|
3520
|
+
watcherMetrics.averageDebounceDelayMs = 0;
|
|
3521
|
+
watcherMetrics.watcherActive = true;
|
|
3522
|
+
}
|
|
3523
|
+
const getAverageDebounceDelay = () => {
|
|
3524
|
+
if (!watcherMetrics || watcherMetrics.broadcastsSent === 0) return 0;
|
|
3525
|
+
return watcherMetrics.totalDebounceDelayMs / watcherMetrics.broadcastsSent;
|
|
3526
|
+
};
|
|
3527
|
+
const logWatcherMetrics = () => {
|
|
3528
|
+
if (!watcherMetrics) return;
|
|
3529
|
+
watcherMetrics.averageDebounceDelayMs = getAverageDebounceDelay();
|
|
3530
|
+
console.log(
|
|
3531
|
+
`[setup-events] File watcher stats: ${watcherMetrics.broadcastsSent} broadcasts, ${watcherMetrics.fileChangesDetected} file changes, ${watcherMetrics.debounceResets} debounce resets, avg delay: ${watcherMetrics.averageDebounceDelayMs.toFixed(1)}ms, ${watcherMetrics.activeProjects} active projects`
|
|
3532
|
+
);
|
|
3533
|
+
};
|
|
3534
|
+
const metricsInterval = setInterval(logWatcherMetrics, 6e4);
|
|
3535
|
+
const broadcastStatus = (projectHash2, statusData, actualDelayMs) => {
|
|
3536
|
+
broadcast2(clients, { type: "client.status_update", payload: statusData });
|
|
3537
|
+
if (watcherMetrics) {
|
|
3538
|
+
watcherMetrics.broadcastsSent++;
|
|
3539
|
+
watcherMetrics.totalDebounceDelayMs += actualDelayMs;
|
|
3540
|
+
watcherMetrics.averageDebounceDelayMs = getAverageDebounceDelay();
|
|
3541
|
+
}
|
|
3542
|
+
};
|
|
3543
|
+
const scheduleBroadcast = (projectHash2, statusData) => {
|
|
3544
|
+
const now = Date.now();
|
|
3545
|
+
const existing = pendingStatuses.get(projectHash2);
|
|
3546
|
+
if (existing && watcherMetrics) {
|
|
3547
|
+
watcherMetrics.debounceResets++;
|
|
3548
|
+
}
|
|
3549
|
+
pendingStatuses.set(projectHash2, {
|
|
3550
|
+
data: statusData,
|
|
3551
|
+
firstWriteAt: existing ? existing.firstWriteAt : now
|
|
3552
|
+
});
|
|
3553
|
+
const existingTimer = debounceTimers.get(projectHash2);
|
|
3554
|
+
if (existingTimer) {
|
|
3555
|
+
clearTimeout(existingTimer);
|
|
3556
|
+
}
|
|
3557
|
+
const timer = setTimeout(() => {
|
|
3558
|
+
debounceTimers.delete(projectHash2);
|
|
3559
|
+
const pending = pendingStatuses.get(projectHash2);
|
|
3560
|
+
if (pending) {
|
|
3561
|
+
const actualDelay = Date.now() - pending.firstWriteAt;
|
|
3562
|
+
broadcastStatus(projectHash2, pending.data, actualDelay);
|
|
3563
|
+
pendingStatuses.delete(projectHash2);
|
|
3564
|
+
}
|
|
3565
|
+
}, DEBOUNCE_MS);
|
|
3566
|
+
debounceTimers.set(projectHash2, timer);
|
|
3567
|
+
};
|
|
3568
|
+
let watcher;
|
|
3569
|
+
const startWatcher = async () => {
|
|
3570
|
+
try {
|
|
3571
|
+
await fs5.mkdir(projectsDir, { recursive: true });
|
|
3572
|
+
watcher = fsWatch(projectsDir, { persistent: true, recursive: true }, async (eventType, filename) => {
|
|
3573
|
+
if (eventType === "change") {
|
|
3574
|
+
if (filename == null) return;
|
|
3575
|
+
if (watcherMetrics) watcherMetrics.fileChangesDetected++;
|
|
3576
|
+
const targetFile = path5.join(projectsDir, String(filename));
|
|
3577
|
+
if (targetFile.endsWith("status.json")) {
|
|
3578
|
+
const projectHash2 = path5.basename(path5.dirname(targetFile));
|
|
3579
|
+
if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
if (watcherMetrics) watcherMetrics.filesProcessed++;
|
|
3583
|
+
try {
|
|
3584
|
+
const content = await fs5.readFile(targetFile, "utf-8");
|
|
3585
|
+
const statusData = JSON.parse(content);
|
|
3586
|
+
if (statusData.projectHash) {
|
|
3587
|
+
const hash = String(statusData.projectHash);
|
|
3588
|
+
if (!knownProjectHashes.has(hash)) {
|
|
3589
|
+
knownProjectHashes.add(hash);
|
|
3590
|
+
if (watcherMetrics) watcherMetrics.activeProjects = knownProjectHashes.size;
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
scheduleBroadcast(projectHash2, statusData);
|
|
3594
|
+
} catch {
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
});
|
|
3599
|
+
console.log(`[setup-events] Watching ${projectsDir} for status.json changes (hash-filtered, debounced)`);
|
|
3600
|
+
} catch (err) {
|
|
3601
|
+
console.error("[setup-events] Failed to start status file watcher:", err);
|
|
3602
|
+
}
|
|
3603
|
+
};
|
|
3604
|
+
events.on("client.status", (e) => {
|
|
3605
|
+
if (e.projectHash) {
|
|
3606
|
+
const hash = String(e.projectHash);
|
|
3607
|
+
if (!knownProjectHashes.has(hash)) {
|
|
3608
|
+
knownProjectHashes.add(hash);
|
|
3609
|
+
if (watcherMetrics) watcherMetrics.activeProjects = knownProjectHashes.size;
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
});
|
|
3613
|
+
startWatcher();
|
|
3614
|
+
disposers.push(() => {
|
|
3615
|
+
clearInterval(metricsInterval);
|
|
3616
|
+
logWatcherMetrics();
|
|
3617
|
+
if (watcherMetrics) watcherMetrics.watcherActive = false;
|
|
3618
|
+
for (const [projectHash2, pending] of pendingStatuses) {
|
|
3619
|
+
const timer = debounceTimers.get(projectHash2);
|
|
3620
|
+
if (timer) {
|
|
3621
|
+
clearTimeout(timer);
|
|
3622
|
+
broadcastStatus(projectHash2, pending.data, 0);
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
for (const timer of debounceTimers.values()) {
|
|
3626
|
+
clearTimeout(timer);
|
|
3627
|
+
}
|
|
3628
|
+
debounceTimers.clear();
|
|
3629
|
+
pendingStatuses.clear();
|
|
3630
|
+
if (watcher) {
|
|
3631
|
+
watcher.close();
|
|
3632
|
+
console.log("[setup-events] Closed status file watcher");
|
|
3633
|
+
}
|
|
3634
|
+
});
|
|
3635
|
+
}
|
|
2675
3636
|
const globalRoot = globalConfigPath ? path5.dirname(globalConfigPath) : void 0;
|
|
2676
3637
|
if (globalRoot) {
|
|
2677
|
-
const
|
|
3638
|
+
const broadcastSessions = async () => {
|
|
2678
3639
|
try {
|
|
2679
3640
|
const { SessionRegistry } = await import("@wrongstack/core");
|
|
2680
3641
|
const registry = new SessionRegistry(globalRoot);
|
|
2681
3642
|
const sessions = await registry.list();
|
|
2682
|
-
const
|
|
3643
|
+
const mySlug = sessions.find((s) => s.pid === process.pid)?.projectSlug;
|
|
3644
|
+
const live = sessions.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true).map((s) => ({
|
|
2683
3645
|
sessionId: s.sessionId,
|
|
2684
3646
|
projectName: s.projectName,
|
|
2685
3647
|
projectSlug: s.projectSlug,
|
|
2686
3648
|
projectRoot: s.projectRoot,
|
|
2687
3649
|
workingDir: s.workingDir,
|
|
2688
3650
|
gitBranch: s.gitBranch,
|
|
3651
|
+
// Surface (tui/webui/cli) so Fleet HQ can label each live client node.
|
|
3652
|
+
clientType: s.clientType,
|
|
2689
3653
|
status: s.status,
|
|
2690
3654
|
pid: s.pid,
|
|
2691
3655
|
startedAt: s.startedAt,
|
|
@@ -2697,20 +3661,52 @@ function setupEvents(deps) {
|
|
|
2697
3661
|
currentTool: a.currentTool,
|
|
2698
3662
|
iterations: a.iterations,
|
|
2699
3663
|
toolCalls: a.toolCalls,
|
|
3664
|
+
costUsd: a.costUsd,
|
|
3665
|
+
tokensIn: a.tokensIn,
|
|
3666
|
+
tokensOut: a.tokensOut,
|
|
3667
|
+
ctxPct: a.ctxPct,
|
|
3668
|
+
model: a.model,
|
|
3669
|
+
partialText: a.partialText,
|
|
2700
3670
|
lastActivityAt: a.lastActivityAt
|
|
2701
3671
|
}))
|
|
2702
3672
|
}));
|
|
2703
3673
|
broadcast2(clients, { type: "sessions.status_update", payload: { sessions: live } });
|
|
2704
3674
|
} catch {
|
|
2705
3675
|
}
|
|
2706
|
-
}
|
|
3676
|
+
};
|
|
3677
|
+
onFleetBroadcaster?.(broadcastSessions);
|
|
3678
|
+
const statusInterval = setInterval(() => void broadcastSessions(), 5e3);
|
|
2707
3679
|
if (statusInterval.unref) statusInterval.unref();
|
|
3680
|
+
disposers.push(() => clearInterval(statusInterval));
|
|
3681
|
+
let regDebounce;
|
|
3682
|
+
try {
|
|
3683
|
+
const regWatcher = fsWatch(globalRoot, { persistent: false }, (_event, filename) => {
|
|
3684
|
+
const name2 = filename ? String(filename) : "";
|
|
3685
|
+
if (!name2.startsWith("session-registry.json") || name2.endsWith(".lock")) return;
|
|
3686
|
+
if (regDebounce) clearTimeout(regDebounce);
|
|
3687
|
+
regDebounce = setTimeout(() => void broadcastSessions(), 150);
|
|
3688
|
+
});
|
|
3689
|
+
disposers.push(() => {
|
|
3690
|
+
if (regDebounce) clearTimeout(regDebounce);
|
|
3691
|
+
regWatcher.close();
|
|
3692
|
+
});
|
|
3693
|
+
} catch {
|
|
3694
|
+
}
|
|
3695
|
+
void broadcastSessions();
|
|
2708
3696
|
}
|
|
3697
|
+
return () => {
|
|
3698
|
+
for (const dispose of disposers) {
|
|
3699
|
+
try {
|
|
3700
|
+
dispose();
|
|
3701
|
+
} catch {
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
};
|
|
2709
3705
|
}
|
|
2710
3706
|
|
|
2711
3707
|
// src/server/custom-context-modes.ts
|
|
2712
3708
|
import { listContextWindowModes, atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
2713
|
-
import * as
|
|
3709
|
+
import * as fs6 from "fs/promises";
|
|
2714
3710
|
import * as path6 from "path";
|
|
2715
3711
|
var STORE_FILENAME = "custom-context-modes.json";
|
|
2716
3712
|
function storePath(wrongstackDir) {
|
|
@@ -2722,7 +3718,7 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
2722
3718
|
const load2 = async () => {
|
|
2723
3719
|
modes.clear();
|
|
2724
3720
|
try {
|
|
2725
|
-
const raw = await
|
|
3721
|
+
const raw = await fs6.readFile(storePath(wrongstackDir), "utf8");
|
|
2726
3722
|
const parsed = JSON.parse(raw);
|
|
2727
3723
|
if (Array.isArray(parsed.modes)) {
|
|
2728
3724
|
for (const m of parsed.modes) {
|
|
@@ -2902,14 +3898,14 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
|
|
|
2902
3898
|
}
|
|
2903
3899
|
|
|
2904
3900
|
// src/server/shell-open.ts
|
|
2905
|
-
import * as
|
|
3901
|
+
import * as fs7 from "fs/promises";
|
|
2906
3902
|
import * as path7 from "path";
|
|
2907
3903
|
import { spawn as spawn2 } from "child_process";
|
|
2908
3904
|
var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
|
|
2909
3905
|
async function handleShellOpen(req, logger) {
|
|
2910
3906
|
try {
|
|
2911
3907
|
const resolved = path7.resolve(req.path);
|
|
2912
|
-
await
|
|
3908
|
+
await fs7.access(resolved);
|
|
2913
3909
|
if (METACHAR_REGEX.test(resolved)) {
|
|
2914
3910
|
return { success: false, message: "Path contains unsupported characters." };
|
|
2915
3911
|
}
|
|
@@ -2946,15 +3942,190 @@ async function handleShellOpen(req, logger) {
|
|
|
2946
3942
|
)
|
|
2947
3943
|
);
|
|
2948
3944
|
}
|
|
2949
|
-
} else {
|
|
2950
|
-
return { success: false, message: `Unknown shell.open target: ${String(req.target)}` };
|
|
3945
|
+
} else {
|
|
3946
|
+
return { success: false, message: `Unknown shell.open target: ${String(req.target)}` };
|
|
3947
|
+
}
|
|
3948
|
+
return { success: true, message: `Opened ${req.target} at ${resolved}` };
|
|
3949
|
+
} catch (err) {
|
|
3950
|
+
return { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
// src/server/git-handlers.ts
|
|
3955
|
+
async function handleGitInfo(ws, projectRoot) {
|
|
3956
|
+
const cwd = projectRoot || void 0;
|
|
3957
|
+
try {
|
|
3958
|
+
const { execFile: ef } = await import("child_process");
|
|
3959
|
+
const git = (args) => new Promise((resolve5) => {
|
|
3960
|
+
ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
|
|
3961
|
+
resolve5(err ? "" : stdout.trim());
|
|
3962
|
+
});
|
|
3963
|
+
});
|
|
3964
|
+
const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
|
|
3965
|
+
git(["branch", "--show-current"]),
|
|
3966
|
+
git(["diff", "--stat"]),
|
|
3967
|
+
git(["status", "--porcelain"]),
|
|
3968
|
+
git(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"])
|
|
3969
|
+
]);
|
|
3970
|
+
const branch = branchRaw || "(detached)";
|
|
3971
|
+
const addMatch = /(\d+)\s+insertion/i.exec(diffRaw);
|
|
3972
|
+
const delMatch = /(\d+)\s+deletion/i.exec(diffRaw);
|
|
3973
|
+
const added = addMatch ? Number(addMatch[1]) : 0;
|
|
3974
|
+
const deleted = delMatch ? Number(delMatch[1]) : 0;
|
|
3975
|
+
const untracked = statusRaw.split("\n").filter((l) => l.startsWith("??")).length;
|
|
3976
|
+
const [behindRaw, aheadRaw] = (upstreamRaw || "0 0").split(" ");
|
|
3977
|
+
const behind = Number(behindRaw) || 0;
|
|
3978
|
+
const ahead = Number(aheadRaw) || 0;
|
|
3979
|
+
send(ws, { type: "git.info", payload: { branch, added, deleted, untracked, ahead, behind } });
|
|
3980
|
+
} catch {
|
|
3981
|
+
send(ws, { type: "git.info", payload: { branch: "", added: 0, deleted: 0, untracked: 0, ahead: 0, behind: 0 } });
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
function makeGit(cwd) {
|
|
3985
|
+
return async (args) => {
|
|
3986
|
+
const { execFile: ef } = await import("child_process");
|
|
3987
|
+
return new Promise((resolve5) => {
|
|
3988
|
+
ef(
|
|
3989
|
+
"git",
|
|
3990
|
+
args,
|
|
3991
|
+
{ cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
|
|
3992
|
+
(err, stdout) => resolve5(err ? "" : stdout)
|
|
3993
|
+
);
|
|
3994
|
+
});
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3997
|
+
async function handleGitChanges(ws, projectRoot) {
|
|
3998
|
+
const cwd = projectRoot || void 0;
|
|
3999
|
+
try {
|
|
4000
|
+
const git = makeGit(cwd);
|
|
4001
|
+
const [statusRaw, unstagedNumstat, stagedNumstat] = await Promise.all([
|
|
4002
|
+
git(["status", "--porcelain", "-z"]),
|
|
4003
|
+
git(["diff", "--numstat", "-z"]),
|
|
4004
|
+
git(["diff", "--cached", "--numstat", "-z"])
|
|
4005
|
+
]);
|
|
4006
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4007
|
+
const parseNumstat = (raw) => {
|
|
4008
|
+
const parts = raw.split("\0");
|
|
4009
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4010
|
+
const entry = parts[i];
|
|
4011
|
+
if (!entry) continue;
|
|
4012
|
+
const m = /^(\d+|-)\t(\d+|-)\t(.*)$/.exec(entry);
|
|
4013
|
+
if (!m) continue;
|
|
4014
|
+
const added = m[1] === "-" ? 0 : Number(m[1]);
|
|
4015
|
+
const deleted = m[2] === "-" ? 0 : Number(m[2]);
|
|
4016
|
+
let path10 = m[3] ?? "";
|
|
4017
|
+
if (path10 === "") {
|
|
4018
|
+
i += 1;
|
|
4019
|
+
path10 = parts[i + 1] ?? parts[i] ?? "";
|
|
4020
|
+
i += 1;
|
|
4021
|
+
}
|
|
4022
|
+
if (!path10) continue;
|
|
4023
|
+
const prev = counts.get(path10) ?? { added: 0, deleted: 0 };
|
|
4024
|
+
counts.set(path10, { added: prev.added + added, deleted: prev.deleted + deleted });
|
|
4025
|
+
}
|
|
4026
|
+
};
|
|
4027
|
+
parseNumstat(unstagedNumstat);
|
|
4028
|
+
parseNumstat(stagedNumstat);
|
|
4029
|
+
const records = statusRaw.split("\0").filter((r) => r.length > 0);
|
|
4030
|
+
const files = [];
|
|
4031
|
+
for (let i = 0; i < records.length; i++) {
|
|
4032
|
+
const rec = records[i];
|
|
4033
|
+
if (!rec || rec.length < 3) continue;
|
|
4034
|
+
const x = rec[0] ?? " ";
|
|
4035
|
+
const y = rec[1] ?? " ";
|
|
4036
|
+
const path10 = rec.slice(3);
|
|
4037
|
+
const isRename = x === "R" || x === "C" || y === "R" || y === "C";
|
|
4038
|
+
if (isRename) i += 1;
|
|
4039
|
+
let status;
|
|
4040
|
+
if (x === "?" && y === "?") status = "?";
|
|
4041
|
+
else if (x === "U" || y === "U" || x === "A" && y === "A" || x === "D" && y === "D") status = "U";
|
|
4042
|
+
else if (x === "R" || y === "R") status = "R";
|
|
4043
|
+
else if (x === "C" || y === "C") status = "C";
|
|
4044
|
+
else if (x === "A" || y === "A") status = "A";
|
|
4045
|
+
else if (x === "D" || y === "D") status = "D";
|
|
4046
|
+
else status = "M";
|
|
4047
|
+
const staged = x !== " " && x !== "?";
|
|
4048
|
+
let added = counts.get(path10)?.added ?? 0;
|
|
4049
|
+
let deleted = counts.get(path10)?.deleted ?? 0;
|
|
4050
|
+
if (status === "?") {
|
|
4051
|
+
added = await countUntrackedLines(cwd, path10);
|
|
4052
|
+
deleted = 0;
|
|
4053
|
+
}
|
|
4054
|
+
files.push({ path: path10, status, added, deleted, staged });
|
|
2951
4055
|
}
|
|
2952
|
-
|
|
4056
|
+
send(ws, { type: "git.changes", payload: { files } });
|
|
2953
4057
|
} catch (err) {
|
|
2954
|
-
|
|
4058
|
+
send(ws, {
|
|
4059
|
+
type: "git.changes",
|
|
4060
|
+
payload: { files: [], error: err instanceof Error ? err.message : String(err) }
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
async function countUntrackedLines(cwd, relPath) {
|
|
4065
|
+
try {
|
|
4066
|
+
const { readFile: readFile8 } = await import("fs/promises");
|
|
4067
|
+
const { join: join8 } = await import("path");
|
|
4068
|
+
const abs = cwd ? join8(cwd, relPath) : relPath;
|
|
4069
|
+
const buf = await readFile8(abs);
|
|
4070
|
+
if (buf.includes(0)) return 0;
|
|
4071
|
+
if (buf.length === 0) return 0;
|
|
4072
|
+
let lines = 0;
|
|
4073
|
+
for (let i = 0; i < buf.length; i++) if (buf[i] === 10) lines++;
|
|
4074
|
+
if (buf[buf.length - 1] !== 10) lines++;
|
|
4075
|
+
return lines;
|
|
4076
|
+
} catch {
|
|
4077
|
+
return 0;
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
var MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
4081
|
+
async function handleGitDiff(ws, projectRoot, path10) {
|
|
4082
|
+
const cwd = projectRoot || void 0;
|
|
4083
|
+
const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path10, ...extra } });
|
|
4084
|
+
if (!path10 || path10.includes("\0") || path10.includes("..")) {
|
|
4085
|
+
reply({ oldText: "", newText: "", error: "invalid path" });
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
try {
|
|
4089
|
+
const git = makeGit(cwd);
|
|
4090
|
+
const { readFile: readFile8 } = await import("fs/promises");
|
|
4091
|
+
const { join: join8 } = await import("path");
|
|
4092
|
+
const oldText = await git(["show", `HEAD:${path10}`]);
|
|
4093
|
+
let newText = "";
|
|
4094
|
+
try {
|
|
4095
|
+
const abs = cwd ? join8(cwd, path10) : path10;
|
|
4096
|
+
const buf = await readFile8(abs);
|
|
4097
|
+
if (buf.includes(0)) {
|
|
4098
|
+
reply({ oldText: "", newText: "", binary: true });
|
|
4099
|
+
return;
|
|
4100
|
+
}
|
|
4101
|
+
if (buf.length > MAX_DIFF_BYTES) {
|
|
4102
|
+
reply({ oldText: "", newText: "", tooLarge: true });
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
newText = buf.toString("utf8");
|
|
4106
|
+
} catch {
|
|
4107
|
+
newText = "";
|
|
4108
|
+
}
|
|
4109
|
+
if ((oldText.length || 0) > MAX_DIFF_BYTES) {
|
|
4110
|
+
reply({ oldText: "", newText: "", tooLarge: true });
|
|
4111
|
+
return;
|
|
4112
|
+
}
|
|
4113
|
+
if (oldText.includes("\0")) {
|
|
4114
|
+
reply({ oldText: "", newText: "", binary: true });
|
|
4115
|
+
return;
|
|
4116
|
+
}
|
|
4117
|
+
reply({ oldText, newText });
|
|
4118
|
+
} catch (err) {
|
|
4119
|
+
reply({ oldText: "", newText: "", error: err instanceof Error ? err.message : String(err) });
|
|
2955
4120
|
}
|
|
2956
4121
|
}
|
|
2957
4122
|
|
|
4123
|
+
// src/server/skills-handlers.ts
|
|
4124
|
+
import { promises as fs8 } from "fs";
|
|
4125
|
+
import path8 from "path";
|
|
4126
|
+
import JSZip from "jszip";
|
|
4127
|
+
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
4128
|
+
|
|
2958
4129
|
// src/server/index.ts
|
|
2959
4130
|
async function startWebUI(opts = {}) {
|
|
2960
4131
|
const requestedWsPort = opts.wsPort ?? 3457;
|
|
@@ -3042,6 +4213,21 @@ async function startWebUI(opts = {}) {
|
|
|
3042
4213
|
toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
|
|
3043
4214
|
toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
|
|
3044
4215
|
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
4216
|
+
const mcpRegistry = new MCPRegistry({
|
|
4217
|
+
toolRegistry,
|
|
4218
|
+
events,
|
|
4219
|
+
log: logger,
|
|
4220
|
+
// Lazy-connect (per-server `lazy`) manifest cache + default idle auto-sleep.
|
|
4221
|
+
cacheDir: wpaths.cacheDir
|
|
4222
|
+
});
|
|
4223
|
+
if (config.features.mcp && config.mcpServers) {
|
|
4224
|
+
for (const [name2, cfg] of Object.entries(config.mcpServers)) {
|
|
4225
|
+
if (cfg.enabled === false) continue;
|
|
4226
|
+
void mcpRegistry.start({ ...cfg, name: name2 }).catch((err) => {
|
|
4227
|
+
logger.warn(`MCP server "${name2}" failed to start at boot`, err);
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
3045
4231
|
let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
|
|
3046
4232
|
if (!opts.services?.session) {
|
|
3047
4233
|
sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
|
|
@@ -3069,15 +4255,22 @@ async function startWebUI(opts = {}) {
|
|
|
3069
4255
|
sessionId: session.id,
|
|
3070
4256
|
projectSlug: wpaths.projectSlug,
|
|
3071
4257
|
projectRoot,
|
|
3072
|
-
projectName:
|
|
4258
|
+
projectName: path9.basename(projectRoot),
|
|
3073
4259
|
workingDir,
|
|
4260
|
+
clientType: "webui",
|
|
3074
4261
|
pid: process.pid,
|
|
3075
4262
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3076
4263
|
});
|
|
3077
|
-
|
|
4264
|
+
const fleetNotifier = new FleetNotifier({
|
|
4265
|
+
baseDir: wpaths.globalRoot,
|
|
4266
|
+
projectRoot,
|
|
4267
|
+
selfPid: process.pid
|
|
4268
|
+
});
|
|
4269
|
+
statusTracker = new AgentStatusTracker({ events, registry, onUpdate: () => fleetNotifier.notify() });
|
|
3078
4270
|
statusTracker.start();
|
|
3079
4271
|
const stopTracking = async () => {
|
|
3080
4272
|
try {
|
|
4273
|
+
fleetNotifier.dispose();
|
|
3081
4274
|
await registry.markClosing();
|
|
3082
4275
|
statusTracker?.stop();
|
|
3083
4276
|
} catch {
|
|
@@ -3117,6 +4310,13 @@ async function startWebUI(opts = {}) {
|
|
|
3117
4310
|
supportsReasoning: resolvedModel.capabilities.reasoning
|
|
3118
4311
|
} : void 0;
|
|
3119
4312
|
const skillLoader = config.features.skills ? new DefaultSkillLoader2({ paths: wpaths }) : void 0;
|
|
4313
|
+
const skillInstaller = config.features.skills ? new SkillInstaller({
|
|
4314
|
+
manifestPath: path9.join(wstackGlobalRoot2(), "installed-skills.json"),
|
|
4315
|
+
projectSkillsDir: path9.join(projectRoot, ".wrongstack", "skills"),
|
|
4316
|
+
globalSkillsDir: path9.join(wstackGlobalRoot2(), "skills"),
|
|
4317
|
+
projectHash: projectHash(projectRoot),
|
|
4318
|
+
skillLoader
|
|
4319
|
+
}) : void 0;
|
|
3120
4320
|
const systemPromptBuilder = new DefaultSystemPromptBuilder2({
|
|
3121
4321
|
memoryStore,
|
|
3122
4322
|
skillLoader,
|
|
@@ -3186,7 +4386,7 @@ async function startWebUI(opts = {}) {
|
|
|
3186
4386
|
}
|
|
3187
4387
|
} else {
|
|
3188
4388
|
throw new Error(
|
|
3189
|
-
"No provider configured. Run `wrongstack
|
|
4389
|
+
"No provider configured. Run `wrongstack auth` to set up, or configure via the WebUI."
|
|
3190
4390
|
);
|
|
3191
4391
|
}
|
|
3192
4392
|
}
|
|
@@ -3274,7 +4474,7 @@ async function startWebUI(opts = {}) {
|
|
|
3274
4474
|
const write = async () => {
|
|
3275
4475
|
let raw;
|
|
3276
4476
|
try {
|
|
3277
|
-
raw = await
|
|
4477
|
+
raw = await fs9.readFile(globalConfigPath, "utf8");
|
|
3278
4478
|
} catch {
|
|
3279
4479
|
raw = "{}";
|
|
3280
4480
|
}
|
|
@@ -3583,7 +4783,7 @@ async function startWebUI(opts = {}) {
|
|
|
3583
4783
|
inputCost,
|
|
3584
4784
|
outputCost,
|
|
3585
4785
|
cacheReadCost,
|
|
3586
|
-
projectName:
|
|
4786
|
+
projectName: path9.basename(projectRoot) || projectRoot,
|
|
3587
4787
|
projectRoot,
|
|
3588
4788
|
cwd: workingDir,
|
|
3589
4789
|
mode: modeId,
|
|
@@ -3637,10 +4837,11 @@ async function startWebUI(opts = {}) {
|
|
|
3637
4837
|
const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
|
|
3638
4838
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
3639
4839
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
3640
|
-
|
|
4840
|
+
let connSeq = 0;
|
|
4841
|
+
function checkRateLimit(_ws, client) {
|
|
3641
4842
|
if (RATE_LIMIT_MESSAGES <= 0) return true;
|
|
3642
4843
|
const now = Date.now();
|
|
3643
|
-
const key = client.
|
|
4844
|
+
const key = client.connId;
|
|
3644
4845
|
const limit = rateLimits.get(key);
|
|
3645
4846
|
if (!limit || now > limit.resetAt) {
|
|
3646
4847
|
rateLimits.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
|
|
@@ -3656,7 +4857,12 @@ async function startWebUI(opts = {}) {
|
|
|
3656
4857
|
);
|
|
3657
4858
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
3658
4859
|
const handleConnection = (ws) => {
|
|
3659
|
-
const client = {
|
|
4860
|
+
const client = {
|
|
4861
|
+
ws,
|
|
4862
|
+
sessionId: session.id,
|
|
4863
|
+
connectedAt: Date.now(),
|
|
4864
|
+
connId: `c${++connSeq}`
|
|
4865
|
+
};
|
|
3660
4866
|
clients.set(ws, client);
|
|
3661
4867
|
void sessionStartPayload().then((payload) => {
|
|
3662
4868
|
send(ws, { type: "session.start", payload });
|
|
@@ -3686,7 +4892,7 @@ async function startWebUI(opts = {}) {
|
|
|
3686
4892
|
const rawObj = JSON.parse(data.toString());
|
|
3687
4893
|
if (typeof rawObj === "object" && rawObj !== null) {
|
|
3688
4894
|
const obj = rawObj;
|
|
3689
|
-
if ("__proto__"
|
|
4895
|
+
if (Object.hasOwn(obj, "__proto__") || Object.hasOwn(obj, "constructor") || Object.hasOwn(obj, "prototype")) {
|
|
3690
4896
|
send(ws, {
|
|
3691
4897
|
type: "error",
|
|
3692
4898
|
payload: { phase: "parse", message: "Invalid message object" }
|
|
@@ -3707,8 +4913,9 @@ async function startWebUI(opts = {}) {
|
|
|
3707
4913
|
}
|
|
3708
4914
|
});
|
|
3709
4915
|
ws.on("close", () => {
|
|
4916
|
+
const closing = clients.get(ws);
|
|
3710
4917
|
clients.delete(ws);
|
|
3711
|
-
rateLimits.delete(
|
|
4918
|
+
if (closing) rateLimits.delete(closing.connId);
|
|
3712
4919
|
if (pendingConfirms.size > 0) {
|
|
3713
4920
|
for (const [id, resolve5] of pendingConfirms) {
|
|
3714
4921
|
resolve5("no");
|
|
@@ -3734,11 +4941,27 @@ async function startWebUI(opts = {}) {
|
|
|
3734
4941
|
{ sampling: sessionLogging.sampling }
|
|
3735
4942
|
);
|
|
3736
4943
|
let eventsArmed = false;
|
|
4944
|
+
let disposeEvents = null;
|
|
4945
|
+
let fleetBroadcast = null;
|
|
3737
4946
|
const armOnce = (label) => {
|
|
3738
4947
|
if (eventsArmed) return;
|
|
3739
4948
|
eventsArmed = true;
|
|
3740
4949
|
console.log(`[WebUI] Backend ready (${label})`);
|
|
3741
|
-
setupEvents({
|
|
4950
|
+
disposeEvents = setupEvents({
|
|
4951
|
+
events,
|
|
4952
|
+
broadcast,
|
|
4953
|
+
clients,
|
|
4954
|
+
config,
|
|
4955
|
+
context,
|
|
4956
|
+
pendingConfirms,
|
|
4957
|
+
globalConfigPath,
|
|
4958
|
+
sessionBridge,
|
|
4959
|
+
wpaths,
|
|
4960
|
+
watcherMetrics,
|
|
4961
|
+
onFleetBroadcaster: (fn) => {
|
|
4962
|
+
fleetBroadcast = fn;
|
|
4963
|
+
}
|
|
4964
|
+
});
|
|
3742
4965
|
};
|
|
3743
4966
|
wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
|
|
3744
4967
|
wssPrimary.on("connection", handleConnection);
|
|
@@ -3775,33 +4998,33 @@ async function startWebUI(opts = {}) {
|
|
|
3775
4998
|
});
|
|
3776
4999
|
}
|
|
3777
5000
|
async function touchProjectEntry(root, workDir) {
|
|
3778
|
-
const resolved =
|
|
5001
|
+
const resolved = path9.resolve(root);
|
|
3779
5002
|
const manifest = await loadManifest(globalConfigPath);
|
|
3780
5003
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3781
|
-
const existing = manifest.projects.find((p) =>
|
|
5004
|
+
const existing = manifest.projects.find((p) => path9.resolve(p.root) === resolved);
|
|
3782
5005
|
if (existing) {
|
|
3783
5006
|
existing.lastSeen = now;
|
|
3784
|
-
if (workDir) existing.lastWorkingDir =
|
|
5007
|
+
if (workDir) existing.lastWorkingDir = path9.resolve(workDir);
|
|
3785
5008
|
} else {
|
|
3786
5009
|
manifest.projects.push({
|
|
3787
|
-
name:
|
|
5010
|
+
name: path9.basename(resolved),
|
|
3788
5011
|
root: resolved,
|
|
3789
5012
|
slug: generateProjectSlug(resolved),
|
|
3790
5013
|
createdAt: now,
|
|
3791
5014
|
lastSeen: now,
|
|
3792
|
-
lastWorkingDir: workDir ?
|
|
5015
|
+
lastWorkingDir: workDir ? path9.resolve(workDir) : void 0
|
|
3793
5016
|
});
|
|
3794
5017
|
}
|
|
3795
5018
|
await saveManifest(manifest, globalConfigPath);
|
|
3796
5019
|
await ensureProjectDataDir(generateProjectSlug(resolved), globalConfigPath);
|
|
3797
5020
|
}
|
|
3798
5021
|
function projectsJsonPath(globalConfigPath2) {
|
|
3799
|
-
const base =
|
|
3800
|
-
return
|
|
5022
|
+
const base = path9.dirname(globalConfigPath2);
|
|
5023
|
+
return path9.join(base, "projects.json");
|
|
3801
5024
|
}
|
|
3802
5025
|
async function loadManifest(globalConfigPath2) {
|
|
3803
5026
|
try {
|
|
3804
|
-
const raw = await
|
|
5027
|
+
const raw = await fs9.readFile(projectsJsonPath(globalConfigPath2), "utf8");
|
|
3805
5028
|
const parsed = JSON.parse(raw);
|
|
3806
5029
|
return { projects: parsed.projects ?? [] };
|
|
3807
5030
|
} catch {
|
|
@@ -3810,16 +5033,16 @@ async function startWebUI(opts = {}) {
|
|
|
3810
5033
|
}
|
|
3811
5034
|
async function saveManifest(manifest, globalConfigPath2) {
|
|
3812
5035
|
const file = projectsJsonPath(globalConfigPath2);
|
|
3813
|
-
await
|
|
3814
|
-
await
|
|
5036
|
+
await fs9.mkdir(path9.dirname(file), { recursive: true });
|
|
5037
|
+
await fs9.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
3815
5038
|
}
|
|
3816
5039
|
function generateProjectSlug(rootPath) {
|
|
3817
5040
|
return projectSlug(rootPath);
|
|
3818
5041
|
}
|
|
3819
5042
|
async function ensureProjectDataDir(slug, globalConfigPath2) {
|
|
3820
|
-
const base =
|
|
3821
|
-
const dir =
|
|
3822
|
-
await
|
|
5043
|
+
const base = path9.dirname(globalConfigPath2);
|
|
5044
|
+
const dir = path9.join(base, "projects", slug);
|
|
5045
|
+
await fs9.mkdir(dir, { recursive: true });
|
|
3823
5046
|
return dir;
|
|
3824
5047
|
}
|
|
3825
5048
|
async function handleMessage(ws, _client, msg) {
|
|
@@ -3929,7 +5152,7 @@ async function startWebUI(opts = {}) {
|
|
|
3929
5152
|
context.readFiles.clear();
|
|
3930
5153
|
context.fileMtimes.clear();
|
|
3931
5154
|
tokenCounter.reset();
|
|
3932
|
-
|
|
5155
|
+
sendResult2(ws, true, "Context cleared");
|
|
3933
5156
|
broadcast(clients, {
|
|
3934
5157
|
type: "session.start",
|
|
3935
5158
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
@@ -3966,13 +5189,13 @@ async function startWebUI(opts = {}) {
|
|
|
3966
5189
|
repaired: report.repaired
|
|
3967
5190
|
}
|
|
3968
5191
|
});
|
|
3969
|
-
|
|
5192
|
+
sendResult2(
|
|
3970
5193
|
ws,
|
|
3971
5194
|
true,
|
|
3972
5195
|
`Compacted: ${report.before} \u2192 ${report.after} tokens (saved ~${Math.max(0, report.before - report.after)})`
|
|
3973
5196
|
);
|
|
3974
5197
|
} catch (err) {
|
|
3975
|
-
|
|
5198
|
+
sendResult2(ws, false, errMessage(err));
|
|
3976
5199
|
}
|
|
3977
5200
|
break;
|
|
3978
5201
|
}
|
|
@@ -3991,7 +5214,7 @@ async function startWebUI(opts = {}) {
|
|
|
3991
5214
|
};
|
|
3992
5215
|
broadcast(clients, { type: "context.repaired", payload });
|
|
3993
5216
|
const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
|
|
3994
|
-
|
|
5217
|
+
sendResult2(
|
|
3995
5218
|
ws,
|
|
3996
5219
|
true,
|
|
3997
5220
|
removed > 0 ? `Context repaired: removed ${removed} orphan protocol item(s)` : "Context repair found no orphan protocol blocks"
|
|
@@ -4025,14 +5248,14 @@ async function startWebUI(opts = {}) {
|
|
|
4025
5248
|
);
|
|
4026
5249
|
const custom = customModes.find((m) => m.id === id);
|
|
4027
5250
|
if (!custom) {
|
|
4028
|
-
|
|
5251
|
+
sendResult2(ws, false, `Unknown context mode "${id}"`);
|
|
4029
5252
|
break;
|
|
4030
5253
|
}
|
|
4031
5254
|
policy = custom;
|
|
4032
5255
|
}
|
|
4033
5256
|
context.meta["contextWindowMode"] = policy.id;
|
|
4034
5257
|
context.meta["contextWindowPolicy"] = policy;
|
|
4035
|
-
|
|
5258
|
+
sendResult2(ws, true, `Context mode switched to ${policy.id}`);
|
|
4036
5259
|
broadcast(clients, {
|
|
4037
5260
|
type: "context.mode.changed",
|
|
4038
5261
|
payload: { id: policy.id, name: policy.name, policy }
|
|
@@ -4052,7 +5275,7 @@ async function startWebUI(opts = {}) {
|
|
|
4052
5275
|
aggressiveOn: "soft",
|
|
4053
5276
|
targetLoad: 0.65
|
|
4054
5277
|
});
|
|
4055
|
-
|
|
5278
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${payload.id}" created`);
|
|
4056
5279
|
break;
|
|
4057
5280
|
}
|
|
4058
5281
|
case "context.mode.update": {
|
|
@@ -4068,7 +5291,7 @@ async function startWebUI(opts = {}) {
|
|
|
4068
5291
|
preserveK: payload.preserveK,
|
|
4069
5292
|
eliseThreshold: payload.eliseThreshold
|
|
4070
5293
|
});
|
|
4071
|
-
|
|
5294
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${payload.id}" updated`);
|
|
4072
5295
|
break;
|
|
4073
5296
|
}
|
|
4074
5297
|
case "context.mode.delete": {
|
|
@@ -4078,7 +5301,7 @@ async function startWebUI(opts = {}) {
|
|
|
4078
5301
|
context.meta["contextWindowPolicy"] = resolveContextWindowPolicy({}, DEFAULT_CONTEXT_WINDOW_MODE_ID);
|
|
4079
5302
|
}
|
|
4080
5303
|
const result = customModeStore.remove(id);
|
|
4081
|
-
|
|
5304
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${id}" deleted`);
|
|
4082
5305
|
break;
|
|
4083
5306
|
}
|
|
4084
5307
|
case "providers.list": {
|
|
@@ -4125,27 +5348,17 @@ async function startWebUI(opts = {}) {
|
|
|
4125
5348
|
}
|
|
4126
5349
|
case "provider.models": {
|
|
4127
5350
|
const providerId = msg.payload.providerId;
|
|
4128
|
-
const
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
inputCost: m.cost?.input,
|
|
4140
|
-
outputCost: m.cost?.output,
|
|
4141
|
-
capabilities: [
|
|
4142
|
-
...m.tool_call ? ["tools"] : [],
|
|
4143
|
-
...m.reasoning ? ["reasoning"] : []
|
|
4144
|
-
]
|
|
4145
|
-
}))
|
|
4146
|
-
}
|
|
4147
|
-
});
|
|
4148
|
-
}
|
|
5351
|
+
const saved = await providerHandlers.loadConfigProviders();
|
|
5352
|
+
const cfg = saved[providerId];
|
|
5353
|
+
const catalogId = cfg?.type && cfg.type !== providerId ? cfg.type : providerId;
|
|
5354
|
+
const provider2 = await modelsRegistry.getProvider(catalogId);
|
|
5355
|
+
send(ws, {
|
|
5356
|
+
type: "provider.models",
|
|
5357
|
+
payload: {
|
|
5358
|
+
provider: providerId,
|
|
5359
|
+
models: resolveProviderModelList(cfg?.models, provider2)
|
|
5360
|
+
}
|
|
5361
|
+
});
|
|
4149
5362
|
break;
|
|
4150
5363
|
}
|
|
4151
5364
|
case "model.switch": {
|
|
@@ -4159,14 +5372,15 @@ async function startWebUI(opts = {}) {
|
|
|
4159
5372
|
context.provider = newProv;
|
|
4160
5373
|
updateAutoCompactionMaxContext?.(newProv);
|
|
4161
5374
|
try {
|
|
4162
|
-
|
|
4163
|
-
const raw = await
|
|
5375
|
+
const next = configWriteLock.then(async () => {
|
|
5376
|
+
const raw = await fs9.readFile(globalConfigPath, "utf8");
|
|
4164
5377
|
const parsed = JSON.parse(raw);
|
|
4165
5378
|
parsed.provider = newProvider;
|
|
4166
5379
|
parsed.model = newModel;
|
|
4167
5380
|
await atomicWrite5(globalConfigPath, JSON.stringify(parsed, null, 2));
|
|
4168
5381
|
});
|
|
4169
|
-
|
|
5382
|
+
configWriteLock = next.then(() => void 0, () => void 0);
|
|
5383
|
+
await next;
|
|
4170
5384
|
} catch (err) {
|
|
4171
5385
|
console.warn(JSON.stringify({
|
|
4172
5386
|
level: "warn",
|
|
@@ -4319,13 +5533,13 @@ async function startWebUI(opts = {}) {
|
|
|
4319
5533
|
const { id } = msg.payload;
|
|
4320
5534
|
try {
|
|
4321
5535
|
if (id === session.id) {
|
|
4322
|
-
|
|
5536
|
+
sendResult2(ws, false, "Cannot delete the active session");
|
|
4323
5537
|
break;
|
|
4324
5538
|
}
|
|
4325
5539
|
await sessionStore.delete(id);
|
|
4326
|
-
|
|
5540
|
+
sendResult2(ws, true, `Session ${id} deleted`);
|
|
4327
5541
|
} catch (err) {
|
|
4328
|
-
|
|
5542
|
+
sendResult2(ws, false, errMessage(err));
|
|
4329
5543
|
}
|
|
4330
5544
|
break;
|
|
4331
5545
|
}
|
|
@@ -4333,7 +5547,7 @@ async function startWebUI(opts = {}) {
|
|
|
4333
5547
|
const { id } = msg.payload;
|
|
4334
5548
|
try {
|
|
4335
5549
|
if (id === session.id) {
|
|
4336
|
-
|
|
5550
|
+
sendResult2(ws, false, "Session is already active");
|
|
4337
5551
|
break;
|
|
4338
5552
|
}
|
|
4339
5553
|
const resumed = await sessionStore.resume(id);
|
|
@@ -4363,14 +5577,14 @@ async function startWebUI(opts = {}) {
|
|
|
4363
5577
|
replayUsage: resumed.data.usage
|
|
4364
5578
|
}
|
|
4365
5579
|
});
|
|
4366
|
-
|
|
5580
|
+
sendResult2(ws, true, `Resumed session ${id}`);
|
|
4367
5581
|
} catch (err) {
|
|
4368
|
-
|
|
5582
|
+
sendResult2(ws, false, errMessage(err));
|
|
4369
5583
|
}
|
|
4370
5584
|
break;
|
|
4371
5585
|
}
|
|
4372
5586
|
case "session.save": {
|
|
4373
|
-
|
|
5587
|
+
sendResult2(ws, true, `Session ${session.id} is auto-saved`);
|
|
4374
5588
|
break;
|
|
4375
5589
|
}
|
|
4376
5590
|
case "tools.list": {
|
|
@@ -4393,6 +5607,28 @@ async function startWebUI(opts = {}) {
|
|
|
4393
5607
|
return handleMemoryRemember(ws, msg, memoryStore);
|
|
4394
5608
|
case "memory.forget":
|
|
4395
5609
|
return handleMemoryForget(ws, msg, memoryStore);
|
|
5610
|
+
// ── MCP operations — delegated to shared handlers (mcp-handlers.ts),
|
|
5611
|
+
// backed by the live MCPRegistry constructed above. ──
|
|
5612
|
+
case "mcp.list":
|
|
5613
|
+
return handleMcpList(ws, msg, globalConfigPath, mcpRegistry);
|
|
5614
|
+
case "mcp.add":
|
|
5615
|
+
return handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry);
|
|
5616
|
+
case "mcp.remove":
|
|
5617
|
+
return handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry);
|
|
5618
|
+
case "mcp.update":
|
|
5619
|
+
return handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry);
|
|
5620
|
+
case "mcp.wake":
|
|
5621
|
+
return handleMcpWake(ws, msg, globalConfigPath, mcpRegistry);
|
|
5622
|
+
case "mcp.sleep":
|
|
5623
|
+
return handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry);
|
|
5624
|
+
case "mcp.discover":
|
|
5625
|
+
return handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry);
|
|
5626
|
+
case "mcp.enable":
|
|
5627
|
+
return handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry);
|
|
5628
|
+
case "mcp.disable":
|
|
5629
|
+
return handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry);
|
|
5630
|
+
case "mcp.restart":
|
|
5631
|
+
return handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry);
|
|
4396
5632
|
case "skills.list": {
|
|
4397
5633
|
if (!skillLoader) {
|
|
4398
5634
|
send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
|
|
@@ -4402,6 +5638,18 @@ async function startWebUI(opts = {}) {
|
|
|
4402
5638
|
const manifests = await skillLoader.list();
|
|
4403
5639
|
const entries = await skillLoader.listEntries();
|
|
4404
5640
|
const byName = new Map(entries.map((e) => [e.name, e]));
|
|
5641
|
+
const sourceUrlsByName = /* @__PURE__ */ new Map();
|
|
5642
|
+
const refsByName = /* @__PURE__ */ new Map();
|
|
5643
|
+
if (skillInstaller) {
|
|
5644
|
+
try {
|
|
5645
|
+
const installed = await skillInstaller.listInstalled();
|
|
5646
|
+
for (const entry of installed) {
|
|
5647
|
+
sourceUrlsByName.set(entry.name, entry.source);
|
|
5648
|
+
refsByName.set(entry.name, entry.ref);
|
|
5649
|
+
}
|
|
5650
|
+
} catch {
|
|
5651
|
+
}
|
|
5652
|
+
}
|
|
4405
5653
|
send(ws, {
|
|
4406
5654
|
type: "skills.list",
|
|
4407
5655
|
payload: {
|
|
@@ -4411,6 +5659,8 @@ async function startWebUI(opts = {}) {
|
|
|
4411
5659
|
description: m.description,
|
|
4412
5660
|
version: m.version ?? "",
|
|
4413
5661
|
source: m.source,
|
|
5662
|
+
sourceUrl: sourceUrlsByName.get(m.name) ?? "",
|
|
5663
|
+
ref: refsByName.get(m.name) ?? "",
|
|
4414
5664
|
path: m.path,
|
|
4415
5665
|
trigger: byName.get(m.name)?.trigger ?? "",
|
|
4416
5666
|
scope: byName.get(m.name)?.scope ?? []
|
|
@@ -4429,6 +5679,261 @@ async function startWebUI(opts = {}) {
|
|
|
4429
5679
|
}
|
|
4430
5680
|
break;
|
|
4431
5681
|
}
|
|
5682
|
+
case "skills.content": {
|
|
5683
|
+
if (!skillLoader) {
|
|
5684
|
+
send(ws, { type: "skills.content", payload: { name: "", body: "", path: "", source: "", relatedFiles: [], references: [], error: "Skills not enabled" } });
|
|
5685
|
+
break;
|
|
5686
|
+
}
|
|
5687
|
+
const contentPayload = msg.payload;
|
|
5688
|
+
if (!contentPayload?.name) {
|
|
5689
|
+
send(ws, { type: "skills.content", payload: { name: "", body: "", path: "", source: "", relatedFiles: [], references: [], error: "Skill name is required" } });
|
|
5690
|
+
break;
|
|
5691
|
+
}
|
|
5692
|
+
try {
|
|
5693
|
+
const { name: name2, source } = contentPayload;
|
|
5694
|
+
const entries = await skillLoader.listEntries();
|
|
5695
|
+
const entry = entries.find((e) => e.name.toLowerCase() === name2.toLowerCase());
|
|
5696
|
+
if (!entry) {
|
|
5697
|
+
send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
|
|
5698
|
+
break;
|
|
5699
|
+
}
|
|
5700
|
+
const body = await skillLoader.readBody(name2);
|
|
5701
|
+
const skillDir = path9.dirname(entry.path);
|
|
5702
|
+
let relatedFiles = [];
|
|
5703
|
+
try {
|
|
5704
|
+
const files = await fs9.readdir(skillDir);
|
|
5705
|
+
relatedFiles = files.filter((f) => f !== path9.basename(entry.path)).map((f) => path9.join(skillDir, f));
|
|
5706
|
+
} catch {
|
|
5707
|
+
}
|
|
5708
|
+
const refs = [];
|
|
5709
|
+
for (const e of entries) {
|
|
5710
|
+
if (e.name.toLowerCase() === name2.toLowerCase()) continue;
|
|
5711
|
+
try {
|
|
5712
|
+
const content = await skillLoader.readBody(e.name);
|
|
5713
|
+
if (content.toLowerCase().includes(name2.toLowerCase())) {
|
|
5714
|
+
refs.push(e.name);
|
|
5715
|
+
}
|
|
5716
|
+
} catch {
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
send(ws, { type: "skills.content", payload: { name: name2, body, path: entry.path, source, relatedFiles, references: refs } });
|
|
5720
|
+
} catch (err) {
|
|
5721
|
+
send(ws, { type: "skills.content", payload: { name: contentPayload.name, body: "", path: "", source: contentPayload.source, relatedFiles: [], references: [], error: errMessage(err) } });
|
|
5722
|
+
}
|
|
5723
|
+
break;
|
|
5724
|
+
}
|
|
5725
|
+
case "skills.install": {
|
|
5726
|
+
if (!skillInstaller) {
|
|
5727
|
+
send(ws, { type: "skills.installed", payload: { success: false, error: "Skills not enabled" } });
|
|
5728
|
+
break;
|
|
5729
|
+
}
|
|
5730
|
+
const installPayload = msg.payload;
|
|
5731
|
+
if (!installPayload?.ref?.trim()) {
|
|
5732
|
+
send(ws, { type: "skills.installed", payload: { success: false, error: "Skill reference is required (e.g. owner/repo or https://github.com/owner/repo)" } });
|
|
5733
|
+
break;
|
|
5734
|
+
}
|
|
5735
|
+
try {
|
|
5736
|
+
const results = await skillInstaller.install(installPayload.ref.trim(), { global: installPayload.global });
|
|
5737
|
+
send(ws, {
|
|
5738
|
+
type: "skills.installed",
|
|
5739
|
+
payload: {
|
|
5740
|
+
success: true,
|
|
5741
|
+
results,
|
|
5742
|
+
error: null
|
|
5743
|
+
}
|
|
5744
|
+
});
|
|
5745
|
+
} catch (err) {
|
|
5746
|
+
send(ws, {
|
|
5747
|
+
type: "skills.installed",
|
|
5748
|
+
payload: {
|
|
5749
|
+
success: false,
|
|
5750
|
+
error: errMessage(err)
|
|
5751
|
+
}
|
|
5752
|
+
});
|
|
5753
|
+
}
|
|
5754
|
+
break;
|
|
5755
|
+
}
|
|
5756
|
+
case "skills.uninstall": {
|
|
5757
|
+
if (!skillInstaller) {
|
|
5758
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: "Skills not enabled" } });
|
|
5759
|
+
break;
|
|
5760
|
+
}
|
|
5761
|
+
const uninstallPayload = msg.payload;
|
|
5762
|
+
if (!uninstallPayload?.name?.trim()) {
|
|
5763
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: "Skill name is required" } });
|
|
5764
|
+
break;
|
|
5765
|
+
}
|
|
5766
|
+
try {
|
|
5767
|
+
await skillInstaller.uninstall(uninstallPayload.name.trim(), { global: uninstallPayload.global });
|
|
5768
|
+
send(ws, { type: "skills.uninstalled", payload: { success: true, error: null } });
|
|
5769
|
+
} catch (err) {
|
|
5770
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: errMessage(err) } });
|
|
5771
|
+
}
|
|
5772
|
+
break;
|
|
5773
|
+
}
|
|
5774
|
+
case "skills.update": {
|
|
5775
|
+
if (!skillInstaller) {
|
|
5776
|
+
send(ws, { type: "skills.updated", payload: { success: false, error: "Skills not enabled" } });
|
|
5777
|
+
break;
|
|
5778
|
+
}
|
|
5779
|
+
const updatePayload = msg.payload;
|
|
5780
|
+
try {
|
|
5781
|
+
const result = await skillInstaller.update(updatePayload?.name, { global: updatePayload?.global });
|
|
5782
|
+
send(ws, {
|
|
5783
|
+
type: "skills.updated",
|
|
5784
|
+
payload: {
|
|
5785
|
+
success: true,
|
|
5786
|
+
error: null,
|
|
5787
|
+
updated: result.updated,
|
|
5788
|
+
unchanged: result.unchanged,
|
|
5789
|
+
errors: result.errors
|
|
5790
|
+
}
|
|
5791
|
+
});
|
|
5792
|
+
} catch (err) {
|
|
5793
|
+
send(ws, { type: "skills.updated", payload: { success: false, error: errMessage(err) } });
|
|
5794
|
+
}
|
|
5795
|
+
break;
|
|
5796
|
+
}
|
|
5797
|
+
case "skills.create": {
|
|
5798
|
+
const createPayload = msg.payload;
|
|
5799
|
+
if (!createPayload?.name?.trim()) {
|
|
5800
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Skill name is required" } });
|
|
5801
|
+
break;
|
|
5802
|
+
}
|
|
5803
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(createPayload.name.trim())) {
|
|
5804
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Skill name must be kebab-case (e.g. my-new-skill)" } });
|
|
5805
|
+
break;
|
|
5806
|
+
}
|
|
5807
|
+
if (!createPayload?.description?.trim()) {
|
|
5808
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Description/trigger is required" } });
|
|
5809
|
+
break;
|
|
5810
|
+
}
|
|
5811
|
+
try {
|
|
5812
|
+
const targetDir = createPayload.scope === "global" ? path9.join(wstackGlobalRoot2(), "skills", createPayload.name.trim()) : path9.join(projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
5813
|
+
try {
|
|
5814
|
+
await fs9.access(targetDir);
|
|
5815
|
+
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
5816
|
+
break;
|
|
5817
|
+
} catch {
|
|
5818
|
+
}
|
|
5819
|
+
await fs9.mkdir(targetDir, { recursive: true });
|
|
5820
|
+
const lines = createPayload.description.trim().split("\n");
|
|
5821
|
+
const firstLine = lines[0].trim();
|
|
5822
|
+
const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
|
|
5823
|
+
const descriptionText = firstLine + (bodyLines.length > 0 ? `
|
|
5824
|
+
${bodyLines.join("\n")}` : "");
|
|
5825
|
+
const trigger = bodyLines.find((l) => l.toLowerCase().startsWith("triggers:")) ?? "";
|
|
5826
|
+
const skillContent = [
|
|
5827
|
+
"---",
|
|
5828
|
+
`name: ${createPayload.name.trim()}`,
|
|
5829
|
+
"description: |",
|
|
5830
|
+
` ${descriptionText.replace(/\n/g, "\n ")}`,
|
|
5831
|
+
`version: 1.0.0`,
|
|
5832
|
+
"---",
|
|
5833
|
+
"",
|
|
5834
|
+
`# ${createPayload.name.trim().split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}`,
|
|
5835
|
+
"",
|
|
5836
|
+
"## Overview",
|
|
5837
|
+
"",
|
|
5838
|
+
firstLine,
|
|
5839
|
+
"",
|
|
5840
|
+
...bodyLines.length > 0 ? bodyLines.filter((l) => !l.toLowerCase().startsWith("triggers:")) : [],
|
|
5841
|
+
"",
|
|
5842
|
+
"## Rules",
|
|
5843
|
+
"- TODO: add your first rule",
|
|
5844
|
+
"",
|
|
5845
|
+
"## Patterns",
|
|
5846
|
+
"### Do",
|
|
5847
|
+
"```ts",
|
|
5848
|
+
"// TODO: add a good example",
|
|
5849
|
+
"```",
|
|
5850
|
+
"",
|
|
5851
|
+
"### Don't",
|
|
5852
|
+
"```ts",
|
|
5853
|
+
"// TODO: add a bad example",
|
|
5854
|
+
"```",
|
|
5855
|
+
"",
|
|
5856
|
+
"## Workflow",
|
|
5857
|
+
"1. TODO: describe step one",
|
|
5858
|
+
"2. TODO: describe step two",
|
|
5859
|
+
"",
|
|
5860
|
+
trigger ? `
|
|
5861
|
+
${trigger}
|
|
5862
|
+
` : "",
|
|
5863
|
+
"## Skills in scope",
|
|
5864
|
+
"- `bug-hunter` \u2014 for systematic bug detection patterns",
|
|
5865
|
+
"- `output-standards` \u2014 for standardized `<next_steps>` formatting"
|
|
5866
|
+
].join("\n");
|
|
5867
|
+
await fs9.writeFile(path9.join(targetDir, "SKILL.md"), skillContent, "utf-8");
|
|
5868
|
+
send(ws, {
|
|
5869
|
+
type: "skills.created",
|
|
5870
|
+
payload: {
|
|
5871
|
+
success: true,
|
|
5872
|
+
error: null,
|
|
5873
|
+
skill: { name: createPayload.name.trim(), path: path9.join(targetDir, "SKILL.md"), scope: createPayload.scope }
|
|
5874
|
+
}
|
|
5875
|
+
});
|
|
5876
|
+
} catch (err) {
|
|
5877
|
+
send(ws, { type: "skills.created", payload: { success: false, error: errMessage(err) } });
|
|
5878
|
+
}
|
|
5879
|
+
break;
|
|
5880
|
+
}
|
|
5881
|
+
case "skills.edit": {
|
|
5882
|
+
if (!skillLoader) {
|
|
5883
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skills not enabled" } });
|
|
5884
|
+
break;
|
|
5885
|
+
}
|
|
5886
|
+
const editPayload = msg.payload;
|
|
5887
|
+
if (!editPayload?.name?.trim()) {
|
|
5888
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skill name is required" } });
|
|
5889
|
+
break;
|
|
5890
|
+
}
|
|
5891
|
+
if (!editPayload?.body) {
|
|
5892
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skill body is required" } });
|
|
5893
|
+
break;
|
|
5894
|
+
}
|
|
5895
|
+
try {
|
|
5896
|
+
const entries = await skillLoader.listEntries();
|
|
5897
|
+
const entry = entries.find((e) => e.name.toLowerCase() === editPayload.name.toLowerCase());
|
|
5898
|
+
if (!entry) {
|
|
5899
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: `Skill "${editPayload.name}" not found` } });
|
|
5900
|
+
break;
|
|
5901
|
+
}
|
|
5902
|
+
if (entry.scope.includes("bundled")) {
|
|
5903
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Bundled skills cannot be edited" } });
|
|
5904
|
+
break;
|
|
5905
|
+
}
|
|
5906
|
+
await fs9.writeFile(entry.path, editPayload.body, "utf-8");
|
|
5907
|
+
send(ws, { type: "skills.edited", payload: { success: true, error: null } });
|
|
5908
|
+
} catch (err) {
|
|
5909
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: errMessage(err) } });
|
|
5910
|
+
}
|
|
5911
|
+
break;
|
|
5912
|
+
}
|
|
5913
|
+
case "skills.export": {
|
|
5914
|
+
if (!skillLoader) {
|
|
5915
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: "Skills not enabled" } });
|
|
5916
|
+
break;
|
|
5917
|
+
}
|
|
5918
|
+
try {
|
|
5919
|
+
const entries = await skillLoader.listEntries();
|
|
5920
|
+
const zip = new JSZip2();
|
|
5921
|
+
for (const entry of entries) {
|
|
5922
|
+
try {
|
|
5923
|
+
const body = await skillLoader.readBody(entry.name);
|
|
5924
|
+
const safeName = entry.name.replace(/\//g, "_");
|
|
5925
|
+
zip.file(`${safeName}/SKILL.md`, body);
|
|
5926
|
+
} catch {
|
|
5927
|
+
}
|
|
5928
|
+
}
|
|
5929
|
+
const zipBuffer = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
|
5930
|
+
const zipBase64 = zipBuffer.toString("base64");
|
|
5931
|
+
send(ws, { type: "skills.exported", payload: { zipBase64, skillCount: entries.length, error: void 0 } });
|
|
5932
|
+
} catch (err) {
|
|
5933
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: errMessage(err) } });
|
|
5934
|
+
}
|
|
5935
|
+
break;
|
|
5936
|
+
}
|
|
4432
5937
|
case "diag.get": {
|
|
4433
5938
|
const usage = tokenCounter.total();
|
|
4434
5939
|
send(ws, {
|
|
@@ -4456,194 +5961,84 @@ async function startWebUI(opts = {}) {
|
|
|
4456
5961
|
break;
|
|
4457
5962
|
}
|
|
4458
5963
|
case "todos.get": {
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
5964
|
+
const ctx = {
|
|
5965
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5966
|
+
send: (w, m) => send(w, m),
|
|
5967
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5968
|
+
};
|
|
5969
|
+
handleTodosGet(ctx, ws);
|
|
4463
5970
|
break;
|
|
4464
5971
|
}
|
|
4465
5972
|
case "todos.clear": {
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
5973
|
+
const ctx = {
|
|
5974
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5975
|
+
send: (w, m) => send(w, m),
|
|
5976
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5977
|
+
};
|
|
5978
|
+
handleTodosClear(ctx, ws);
|
|
4469
5979
|
break;
|
|
4470
5980
|
}
|
|
4471
5981
|
case "todos.remove": {
|
|
4472
|
-
const
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
}
|
|
4477
|
-
|
|
4478
|
-
let targetIdx = -1;
|
|
4479
|
-
if (typeof id === "string") {
|
|
4480
|
-
targetIdx = context.todos.findIndex((t) => t.id === id);
|
|
4481
|
-
} else if (typeof index === "number" && index > 0) {
|
|
4482
|
-
targetIdx = index - 1;
|
|
4483
|
-
}
|
|
4484
|
-
if (targetIdx < 0 || !context.todos[targetIdx]) {
|
|
4485
|
-
sendResult(ws, false, "Todo not found");
|
|
4486
|
-
break;
|
|
4487
|
-
}
|
|
4488
|
-
const removed = expectDefined2(context.todos[targetIdx]);
|
|
4489
|
-
const next = [...context.todos.slice(0, targetIdx), ...context.todos.slice(targetIdx + 1)];
|
|
4490
|
-
context.state.replaceTodos(next);
|
|
4491
|
-
sendResult(ws, true, `Removed: ${removed.content}`);
|
|
4492
|
-
broadcast(clients, { type: "todos.updated", payload: { todos: next } });
|
|
5982
|
+
const ctx = {
|
|
5983
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5984
|
+
send: (w, m) => send(w, m),
|
|
5985
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5986
|
+
};
|
|
5987
|
+
handleTodosRemove(ctx, ws, msg.payload);
|
|
4493
5988
|
break;
|
|
4494
5989
|
}
|
|
4495
5990
|
case "tasks.get": {
|
|
4496
|
-
const
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
type: "tasks.updated",
|
|
4503
|
-
payload: { tasks: file?.tasks ?? [] }
|
|
4504
|
-
});
|
|
4505
|
-
} catch {
|
|
4506
|
-
send(ws, { type: "tasks.updated", payload: { tasks: [] } });
|
|
4507
|
-
}
|
|
4508
|
-
} else {
|
|
4509
|
-
send(ws, { type: "tasks.updated", payload: { tasks: [], error: "Task storage not configured." } });
|
|
4510
|
-
}
|
|
5991
|
+
const ctx = {
|
|
5992
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5993
|
+
send: (w, m) => send(w, m),
|
|
5994
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5995
|
+
};
|
|
5996
|
+
await handleTasksGet(ctx, ws);
|
|
4511
5997
|
break;
|
|
4512
5998
|
}
|
|
4513
5999
|
case "plan.get": {
|
|
4514
|
-
const
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
type: "plan.updated",
|
|
4521
|
-
payload: {
|
|
4522
|
-
plan: plan ?? {
|
|
4523
|
-
version: 1,
|
|
4524
|
-
sessionId: session.id,
|
|
4525
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4526
|
-
items: []
|
|
4527
|
-
}
|
|
4528
|
-
}
|
|
4529
|
-
});
|
|
4530
|
-
} catch {
|
|
4531
|
-
send(ws, {
|
|
4532
|
-
type: "plan.updated",
|
|
4533
|
-
payload: {
|
|
4534
|
-
plan: {
|
|
4535
|
-
version: 1,
|
|
4536
|
-
sessionId: session.id,
|
|
4537
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4538
|
-
items: []
|
|
4539
|
-
}
|
|
4540
|
-
}
|
|
4541
|
-
});
|
|
4542
|
-
}
|
|
4543
|
-
} else {
|
|
4544
|
-
send(ws, {
|
|
4545
|
-
type: "plan.updated",
|
|
4546
|
-
payload: { plan: null, error: "Plan storage is not configured for this session." }
|
|
4547
|
-
});
|
|
4548
|
-
}
|
|
6000
|
+
const ctx = {
|
|
6001
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6002
|
+
send: (w, m) => send(w, m),
|
|
6003
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6004
|
+
};
|
|
6005
|
+
await handlePlanGet(ctx, ws);
|
|
4549
6006
|
break;
|
|
4550
6007
|
}
|
|
4551
6008
|
case "plan.template_use": {
|
|
4552
|
-
const
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
try {
|
|
4559
|
-
const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem } = await import("@wrongstack/core");
|
|
4560
|
-
const tpl = getPlanTemplate(template);
|
|
4561
|
-
if (!tpl) {
|
|
4562
|
-
sendResult(ws, false, `Unknown template "${template}".`);
|
|
4563
|
-
break;
|
|
4564
|
-
}
|
|
4565
|
-
let plan = await loadPlan(planPath) ?? emptyPlan(session.id);
|
|
4566
|
-
for (const item of tpl.items) {
|
|
4567
|
-
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
4568
|
-
}
|
|
4569
|
-
await savePlan(planPath, plan);
|
|
4570
|
-
sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
|
|
4571
|
-
broadcast(clients, {
|
|
4572
|
-
type: "plan.updated",
|
|
4573
|
-
payload: { plan }
|
|
4574
|
-
});
|
|
4575
|
-
} catch (err) {
|
|
4576
|
-
sendResult(ws, false, errMessage(err));
|
|
4577
|
-
}
|
|
6009
|
+
const ctx = {
|
|
6010
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6011
|
+
send: (w, m) => send(w, m),
|
|
6012
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6013
|
+
};
|
|
6014
|
+
await handlePlanTemplateUse(ctx, ws, msg.payload.template);
|
|
4578
6015
|
break;
|
|
4579
6016
|
}
|
|
4580
6017
|
case "todo.update": {
|
|
4581
|
-
const
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
break;
|
|
4586
|
-
}
|
|
4587
|
-
const next = [...context.todos];
|
|
4588
|
-
const existing = expectDefined2(next[idx]);
|
|
4589
|
-
next[idx] = {
|
|
4590
|
-
...existing,
|
|
4591
|
-
status: payload.status ?? existing.status,
|
|
4592
|
-
activeForm: payload.activeForm !== void 0 ? payload.activeForm : existing.activeForm
|
|
6018
|
+
const ctx = {
|
|
6019
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6020
|
+
send: (w, m) => send(w, m),
|
|
6021
|
+
broadcast: (m) => broadcast(clients, m)
|
|
4593
6022
|
};
|
|
4594
|
-
|
|
4595
|
-
sendResult(ws, true, `Todo "${existing.content}" updated`);
|
|
4596
|
-
broadcast(clients, { type: "todos.updated", payload: { todos: next } });
|
|
6023
|
+
handleTodoUpdate(ctx, ws, msg.payload);
|
|
4597
6024
|
break;
|
|
4598
6025
|
}
|
|
4599
6026
|
case "task.update": {
|
|
4600
|
-
const
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
try {
|
|
4607
|
-
const { mutateTasks } = await import("@wrongstack/core");
|
|
4608
|
-
const file = await mutateTasks(taskPath, session.id, async (f) => {
|
|
4609
|
-
const task = f.tasks.find((t) => t.id === payload.id);
|
|
4610
|
-
if (!task) return f;
|
|
4611
|
-
task.status = payload.status;
|
|
4612
|
-
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4613
|
-
return f;
|
|
4614
|
-
});
|
|
4615
|
-
sendResult(ws, true, `Task status updated to "${payload.status}".`);
|
|
4616
|
-
broadcast(clients, { type: "tasks.updated", payload: { tasks: file.tasks } });
|
|
4617
|
-
} catch (err) {
|
|
4618
|
-
sendResult(ws, false, errMessage(err));
|
|
4619
|
-
}
|
|
6027
|
+
const ctx = {
|
|
6028
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6029
|
+
send: (w, m) => send(w, m),
|
|
6030
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6031
|
+
};
|
|
6032
|
+
await handleTaskUpdate(ctx, ws, msg.payload);
|
|
4620
6033
|
break;
|
|
4621
6034
|
}
|
|
4622
6035
|
case "plan.item.update": {
|
|
4623
|
-
const
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
try {
|
|
4630
|
-
const { mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
|
|
4631
|
-
let changed = false;
|
|
4632
|
-
const plan = await mutatePlan(planPath, session.id, async (p) => {
|
|
4633
|
-
const before = p.updatedAt;
|
|
4634
|
-
const updated = setPlanItemStatus(p, payload.target, payload.status);
|
|
4635
|
-
changed = updated.updatedAt !== before;
|
|
4636
|
-
return updated;
|
|
4637
|
-
});
|
|
4638
|
-
if (!changed) {
|
|
4639
|
-
sendResult(ws, false, `No plan item matched "${payload.target}".`);
|
|
4640
|
-
break;
|
|
4641
|
-
}
|
|
4642
|
-
sendResult(ws, true, `Plan item status updated to "${payload.status}".`);
|
|
4643
|
-
broadcast(clients, { type: "plan.updated", payload: { plan } });
|
|
4644
|
-
} catch (err) {
|
|
4645
|
-
sendResult(ws, false, errMessage(err));
|
|
4646
|
-
}
|
|
6036
|
+
const ctx = {
|
|
6037
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6038
|
+
send: (w, m) => send(w, m),
|
|
6039
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6040
|
+
};
|
|
6041
|
+
await handlePlanItemUpdate(ctx, ws, msg.payload);
|
|
4647
6042
|
break;
|
|
4648
6043
|
}
|
|
4649
6044
|
// ── File operations — delegated to shared handlers (file-handlers.ts) ──
|
|
@@ -4713,13 +6108,13 @@ async function startWebUI(opts = {}) {
|
|
|
4713
6108
|
provider: config.provider,
|
|
4714
6109
|
model: config.model
|
|
4715
6110
|
});
|
|
4716
|
-
|
|
6111
|
+
sendResult2(ws, true, `Switched to mode "${id}"`);
|
|
4717
6112
|
broadcast(clients, {
|
|
4718
6113
|
type: "session.start",
|
|
4719
6114
|
payload: { ...await sessionStartPayload() }
|
|
4720
6115
|
});
|
|
4721
6116
|
} catch (err) {
|
|
4722
|
-
|
|
6117
|
+
sendResult2(ws, false, errMessage(err));
|
|
4723
6118
|
}
|
|
4724
6119
|
break;
|
|
4725
6120
|
}
|
|
@@ -4773,13 +6168,13 @@ async function startWebUI(opts = {}) {
|
|
|
4773
6168
|
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
4774
6169
|
const proc = getProcessRegistry().get(pid);
|
|
4775
6170
|
if (proc?.protected) {
|
|
4776
|
-
|
|
6171
|
+
sendResult2(ws, false, `Cannot kill protected process (PID ${pid})`);
|
|
4777
6172
|
break;
|
|
4778
6173
|
}
|
|
4779
6174
|
getProcessRegistry().kill(pid);
|
|
4780
|
-
|
|
6175
|
+
sendResult2(ws, true, `Killed PID ${pid}`);
|
|
4781
6176
|
} catch (err) {
|
|
4782
|
-
|
|
6177
|
+
sendResult2(ws, false, errMessage(err));
|
|
4783
6178
|
}
|
|
4784
6179
|
break;
|
|
4785
6180
|
}
|
|
@@ -4787,47 +6182,33 @@ async function startWebUI(opts = {}) {
|
|
|
4787
6182
|
try {
|
|
4788
6183
|
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
4789
6184
|
getProcessRegistry().killAll();
|
|
4790
|
-
|
|
6185
|
+
sendResult2(ws, true, "All processes killed");
|
|
4791
6186
|
} catch (err) {
|
|
4792
|
-
|
|
6187
|
+
sendResult2(ws, false, errMessage(err));
|
|
4793
6188
|
}
|
|
4794
6189
|
break;
|
|
4795
6190
|
}
|
|
4796
6191
|
case "git.info": {
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
const branch = branchRaw || "(detached)";
|
|
4812
|
-
const diffMatch = /\+\s*(\d+)\s*deletion/i.exec(diffRaw);
|
|
4813
|
-
const addMatch = /(\d+)\s*insertion/i.exec(diffRaw) ?? /(\d+)\s*addition/i.exec(diffRaw);
|
|
4814
|
-
const delMatch = /\+\s*(\d+)\s*deletion/i.exec(diffRaw);
|
|
4815
|
-
const added = addMatch ? Number(addMatch[1]) : 0;
|
|
4816
|
-
const deleted = delMatch ? Number(delMatch[1]) : 0;
|
|
4817
|
-
const untracked = statusRaw.split("\n").filter((l) => l.startsWith("??")).length;
|
|
4818
|
-
const [aheadRaw, behindRaw] = (upstreamRaw || "0 0").split(" ");
|
|
4819
|
-
const ahead = Number(aheadRaw) || 0;
|
|
4820
|
-
const behind = Number(behindRaw) || 0;
|
|
4821
|
-
send(ws, {
|
|
4822
|
-
type: "git.info",
|
|
4823
|
-
payload: { branch, added, deleted, untracked, ahead, behind }
|
|
4824
|
-
});
|
|
6192
|
+
await handleGitInfo(ws, projectRoot);
|
|
6193
|
+
break;
|
|
6194
|
+
}
|
|
6195
|
+
case "git.changes": {
|
|
6196
|
+
await handleGitChanges(ws, projectRoot);
|
|
6197
|
+
break;
|
|
6198
|
+
}
|
|
6199
|
+
case "git.diff": {
|
|
6200
|
+
await handleGitDiff(ws, projectRoot, String(msg.payload?.path ?? ""));
|
|
6201
|
+
break;
|
|
6202
|
+
}
|
|
6203
|
+
case "webui.shutdown": {
|
|
6204
|
+
console.log("[WebUI] Shutdown requested from client");
|
|
6205
|
+
process.kill(process.pid, "SIGINT");
|
|
4825
6206
|
break;
|
|
4826
6207
|
}
|
|
4827
6208
|
case "goal.get": {
|
|
4828
6209
|
try {
|
|
4829
|
-
const goalPath =
|
|
4830
|
-
const raw = await
|
|
6210
|
+
const goalPath = resolveWstackPaths({ projectRoot }).projectGoal;
|
|
6211
|
+
const raw = await fs9.readFile(goalPath, "utf8");
|
|
4831
6212
|
const goal = JSON.parse(raw);
|
|
4832
6213
|
broadcast(clients, { type: "goal.updated", payload: goal });
|
|
4833
6214
|
} catch {
|
|
@@ -4838,7 +6219,7 @@ async function startWebUI(opts = {}) {
|
|
|
4838
6219
|
case "autonomy.switch": {
|
|
4839
6220
|
const { mode } = msg.payload;
|
|
4840
6221
|
context.meta["autonomy"] = mode;
|
|
4841
|
-
|
|
6222
|
+
sendResult2(ws, true, `Autonomy mode set to "${mode}"`);
|
|
4842
6223
|
broadcast(clients, { type: "prefs.updated", payload: { autonomy: mode } });
|
|
4843
6224
|
void persistPrefsToConfig({ autonomy: mode });
|
|
4844
6225
|
break;
|
|
@@ -4887,7 +6268,7 @@ async function startWebUI(opts = {}) {
|
|
|
4887
6268
|
try {
|
|
4888
6269
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4889
6270
|
const rewinder = new DefaultSessionRewinder(
|
|
4890
|
-
|
|
6271
|
+
path9.join(projectRoot, ".wrongstack", "sessions"),
|
|
4891
6272
|
projectRoot
|
|
4892
6273
|
);
|
|
4893
6274
|
const checkpoints = await rewinder.listCheckpoints(session.id);
|
|
@@ -4908,18 +6289,18 @@ async function startWebUI(opts = {}) {
|
|
|
4908
6289
|
try {
|
|
4909
6290
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4910
6291
|
const rewinder = new DefaultSessionRewinder(
|
|
4911
|
-
|
|
6292
|
+
path9.join(projectRoot, ".wrongstack", "sessions"),
|
|
4912
6293
|
projectRoot
|
|
4913
6294
|
);
|
|
4914
6295
|
await rewinder.rewindToCheckpoint(session.id, checkpointIndex);
|
|
4915
6296
|
await context.session.truncateToCheckpoint(checkpointIndex);
|
|
4916
|
-
|
|
6297
|
+
sendResult2(ws, true, `Rewound to checkpoint ${checkpointIndex}`);
|
|
4917
6298
|
broadcast(clients, {
|
|
4918
6299
|
type: "session.start",
|
|
4919
6300
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
4920
6301
|
});
|
|
4921
6302
|
} catch (err) {
|
|
4922
|
-
|
|
6303
|
+
sendResult2(ws, false, errMessage(err));
|
|
4923
6304
|
}
|
|
4924
6305
|
break;
|
|
4925
6306
|
}
|
|
@@ -4942,9 +6323,9 @@ async function startWebUI(opts = {}) {
|
|
|
4942
6323
|
case "projects.add": {
|
|
4943
6324
|
const { root: addRoot, name: displayName } = msg.payload;
|
|
4944
6325
|
try {
|
|
4945
|
-
const resolved =
|
|
4946
|
-
await
|
|
4947
|
-
const stat2 = await
|
|
6326
|
+
const resolved = path9.resolve(addRoot);
|
|
6327
|
+
await fs9.access(resolved);
|
|
6328
|
+
const stat2 = await fs9.stat(resolved);
|
|
4948
6329
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4949
6330
|
const manifest = await loadManifest(globalConfigPath);
|
|
4950
6331
|
const existing = manifest.projects.find((p) => p.root === resolved);
|
|
@@ -4960,26 +6341,26 @@ async function startWebUI(opts = {}) {
|
|
|
4960
6341
|
});
|
|
4961
6342
|
break;
|
|
4962
6343
|
}
|
|
4963
|
-
const
|
|
6344
|
+
const name2 = displayName?.trim() || path9.basename(resolved);
|
|
4964
6345
|
const slug = generateProjectSlug(resolved);
|
|
4965
6346
|
await ensureProjectDataDir(slug, globalConfigPath);
|
|
4966
6347
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4967
|
-
manifest.projects.push({ name, root: resolved, slug, lastSeen: now, createdAt: now });
|
|
6348
|
+
manifest.projects.push({ name: name2, root: resolved, slug, lastSeen: now, createdAt: now });
|
|
4968
6349
|
await saveManifest(manifest, globalConfigPath);
|
|
4969
6350
|
send(ws, {
|
|
4970
6351
|
type: "projects.added",
|
|
4971
6352
|
payload: {
|
|
4972
|
-
name,
|
|
6353
|
+
name: name2,
|
|
4973
6354
|
root: resolved,
|
|
4974
6355
|
slug,
|
|
4975
|
-
message: `Registered project "${
|
|
6356
|
+
message: `Registered project "${name2}"`
|
|
4976
6357
|
}
|
|
4977
6358
|
});
|
|
4978
6359
|
} catch (err) {
|
|
4979
6360
|
send(ws, {
|
|
4980
6361
|
type: "projects.added",
|
|
4981
6362
|
payload: {
|
|
4982
|
-
name:
|
|
6363
|
+
name: path9.basename(addRoot),
|
|
4983
6364
|
root: addRoot,
|
|
4984
6365
|
slug: "",
|
|
4985
6366
|
message: errMessage(err)
|
|
@@ -4991,17 +6372,17 @@ async function startWebUI(opts = {}) {
|
|
|
4991
6372
|
case "projects.select": {
|
|
4992
6373
|
const { root: selRoot, name: selName } = msg.payload;
|
|
4993
6374
|
try {
|
|
4994
|
-
const resolved =
|
|
6375
|
+
const resolved = path9.resolve(selRoot);
|
|
4995
6376
|
try {
|
|
4996
|
-
await
|
|
4997
|
-
const stat2 = await
|
|
6377
|
+
await fs9.access(resolved);
|
|
6378
|
+
const stat2 = await fs9.stat(resolved);
|
|
4998
6379
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4999
6380
|
} catch (err) {
|
|
5000
6381
|
send(ws, {
|
|
5001
6382
|
type: "projects.selected",
|
|
5002
6383
|
payload: {
|
|
5003
6384
|
root: selRoot,
|
|
5004
|
-
name: selName ||
|
|
6385
|
+
name: selName || path9.basename(selRoot),
|
|
5005
6386
|
message: `Cannot switch: ${errMessage(err)}`
|
|
5006
6387
|
}
|
|
5007
6388
|
});
|
|
@@ -5013,10 +6394,10 @@ async function startWebUI(opts = {}) {
|
|
|
5013
6394
|
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
5014
6395
|
entry.lastWorkingDir = resolved;
|
|
5015
6396
|
} else {
|
|
5016
|
-
const
|
|
6397
|
+
const name2 = selName?.trim() || path9.basename(resolved);
|
|
5017
6398
|
const slug = generateProjectSlug(resolved);
|
|
5018
6399
|
manifest.projects.push({
|
|
5019
|
-
name,
|
|
6400
|
+
name: name2,
|
|
5020
6401
|
root: resolved,
|
|
5021
6402
|
slug,
|
|
5022
6403
|
lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -5054,13 +6435,13 @@ async function startWebUI(opts = {}) {
|
|
|
5054
6435
|
});
|
|
5055
6436
|
} catch {
|
|
5056
6437
|
}
|
|
5057
|
-
const newSessionsDir =
|
|
5058
|
-
|
|
6438
|
+
const newSessionsDir = path9.join(
|
|
6439
|
+
path9.dirname(globalConfigPath),
|
|
5059
6440
|
"projects",
|
|
5060
6441
|
switchSlug,
|
|
5061
6442
|
"sessions"
|
|
5062
6443
|
);
|
|
5063
|
-
await
|
|
6444
|
+
await fs9.mkdir(newSessionsDir, { recursive: true });
|
|
5064
6445
|
const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
|
|
5065
6446
|
const oldSessionId = session.id;
|
|
5066
6447
|
try {
|
|
@@ -5092,8 +6473,9 @@ async function startWebUI(opts = {}) {
|
|
|
5092
6473
|
sessionId: session.id,
|
|
5093
6474
|
projectSlug: switchSlug,
|
|
5094
6475
|
projectRoot,
|
|
5095
|
-
projectName:
|
|
6476
|
+
projectName: path9.basename(projectRoot),
|
|
5096
6477
|
workingDir,
|
|
6478
|
+
clientType: "webui",
|
|
5097
6479
|
pid: process.pid,
|
|
5098
6480
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5099
6481
|
});
|
|
@@ -5103,8 +6485,8 @@ async function startWebUI(opts = {}) {
|
|
|
5103
6485
|
type: "projects.selected",
|
|
5104
6486
|
payload: {
|
|
5105
6487
|
root: resolved,
|
|
5106
|
-
name: selName ||
|
|
5107
|
-
message: `Switched to ${selName ||
|
|
6488
|
+
name: selName || path9.basename(resolved),
|
|
6489
|
+
message: `Switched to ${selName || path9.basename(resolved)}`
|
|
5108
6490
|
}
|
|
5109
6491
|
});
|
|
5110
6492
|
broadcast(clients, {
|
|
@@ -5127,7 +6509,7 @@ async function startWebUI(opts = {}) {
|
|
|
5127
6509
|
type: "projects.selected",
|
|
5128
6510
|
payload: {
|
|
5129
6511
|
root: selRoot,
|
|
5130
|
-
name: selName ||
|
|
6512
|
+
name: selName || path9.basename(selRoot),
|
|
5131
6513
|
message: errMessage(err)
|
|
5132
6514
|
}
|
|
5133
6515
|
});
|
|
@@ -5138,17 +6520,17 @@ async function startWebUI(opts = {}) {
|
|
|
5138
6520
|
case "working_dir.set": {
|
|
5139
6521
|
const { path: newPath } = msg.payload;
|
|
5140
6522
|
try {
|
|
5141
|
-
const resolved =
|
|
5142
|
-
if (!resolved.startsWith(projectRoot +
|
|
5143
|
-
|
|
6523
|
+
const resolved = path9.resolve(projectRoot, newPath);
|
|
6524
|
+
if (!resolved.startsWith(projectRoot + path9.sep) && resolved !== projectRoot) {
|
|
6525
|
+
sendResult2(ws, false, `Path must stay inside the project root: ${projectRoot}`);
|
|
5144
6526
|
break;
|
|
5145
6527
|
}
|
|
5146
6528
|
try {
|
|
5147
|
-
await
|
|
5148
|
-
const stat2 = await
|
|
6529
|
+
await fs9.access(resolved);
|
|
6530
|
+
const stat2 = await fs9.stat(resolved);
|
|
5149
6531
|
if (!stat2.isDirectory()) throw new Error("Not a directory");
|
|
5150
6532
|
} catch {
|
|
5151
|
-
|
|
6533
|
+
sendResult2(ws, false, `Directory not found or not accessible: ${resolved}`);
|
|
5152
6534
|
break;
|
|
5153
6535
|
}
|
|
5154
6536
|
workingDir = resolved;
|
|
@@ -5157,9 +6539,9 @@ async function startWebUI(opts = {}) {
|
|
|
5157
6539
|
type: "working_dir.changed",
|
|
5158
6540
|
payload: { cwd: resolved, projectRoot }
|
|
5159
6541
|
});
|
|
5160
|
-
|
|
6542
|
+
sendResult2(ws, true, `Working directory set to ${resolved}`);
|
|
5161
6543
|
} catch (err) {
|
|
5162
|
-
|
|
6544
|
+
sendResult2(ws, false, errMessage(err));
|
|
5163
6545
|
}
|
|
5164
6546
|
break;
|
|
5165
6547
|
}
|
|
@@ -5169,31 +6551,31 @@ async function startWebUI(opts = {}) {
|
|
|
5169
6551
|
msg.payload,
|
|
5170
6552
|
logger
|
|
5171
6553
|
);
|
|
5172
|
-
|
|
6554
|
+
sendResult2(ws, result.success, result.message);
|
|
5173
6555
|
break;
|
|
5174
6556
|
}
|
|
5175
6557
|
// ── Mailbox operations — project-level inter-agent messaging ────
|
|
5176
6558
|
case "mailbox.messages":
|
|
5177
6559
|
return handleMailboxMessages(
|
|
5178
6560
|
ws,
|
|
5179
|
-
{ projectRoot, globalRoot:
|
|
6561
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
5180
6562
|
msg.payload
|
|
5181
6563
|
);
|
|
5182
6564
|
case "mailbox.agents":
|
|
5183
6565
|
return handleMailboxAgents(
|
|
5184
6566
|
ws,
|
|
5185
|
-
{ projectRoot, globalRoot:
|
|
6567
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
5186
6568
|
msg.payload
|
|
5187
6569
|
);
|
|
5188
6570
|
case "mailbox.clear":
|
|
5189
6571
|
return handleMailboxClear(
|
|
5190
6572
|
ws,
|
|
5191
|
-
{ projectRoot, globalRoot:
|
|
6573
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) }
|
|
5192
6574
|
);
|
|
5193
6575
|
case "mailbox.purge":
|
|
5194
6576
|
return handleMailboxPurge(
|
|
5195
6577
|
ws,
|
|
5196
|
-
{ projectRoot, globalRoot:
|
|
6578
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
5197
6579
|
msg.payload
|
|
5198
6580
|
);
|
|
5199
6581
|
// ── Brain — status, autonomy ceiling, direct decision support ───
|
|
@@ -5207,7 +6589,7 @@ async function startWebUI(opts = {}) {
|
|
|
5207
6589
|
const level = msg.payload?.level ?? "";
|
|
5208
6590
|
const valid = ["off", "low", "medium", "high", "all"];
|
|
5209
6591
|
if (!valid.includes(level)) {
|
|
5210
|
-
|
|
6592
|
+
sendResult2(ws, false, `Unknown risk level "${level}". Use: ${valid.join(", ")}.`);
|
|
5211
6593
|
break;
|
|
5212
6594
|
}
|
|
5213
6595
|
brainSettings.maxAutoRisk = level;
|
|
@@ -5220,7 +6602,7 @@ async function startWebUI(opts = {}) {
|
|
|
5220
6602
|
case "brain.ask": {
|
|
5221
6603
|
const question = msg.payload?.question?.trim();
|
|
5222
6604
|
if (!question) {
|
|
5223
|
-
|
|
6605
|
+
sendResult2(ws, false, "Usage: /brain ask <question>");
|
|
5224
6606
|
break;
|
|
5225
6607
|
}
|
|
5226
6608
|
try {
|
|
@@ -5233,7 +6615,7 @@ async function startWebUI(opts = {}) {
|
|
|
5233
6615
|
});
|
|
5234
6616
|
send(ws, { type: "brain.answer", payload: { question, decision } });
|
|
5235
6617
|
} catch (err) {
|
|
5236
|
-
|
|
6618
|
+
sendResult2(ws, false, `Brain consultation failed: ${errMessage(err)}`);
|
|
5237
6619
|
}
|
|
5238
6620
|
break;
|
|
5239
6621
|
}
|
|
@@ -5260,14 +6642,28 @@ async function startWebUI(opts = {}) {
|
|
|
5260
6642
|
broadcast,
|
|
5261
6643
|
clients
|
|
5262
6644
|
});
|
|
6645
|
+
const watcherMetrics = {
|
|
6646
|
+
fileChangesDetected: 0,
|
|
6647
|
+
filesProcessed: 0,
|
|
6648
|
+
broadcastsSent: 0,
|
|
6649
|
+
debounceResets: 0,
|
|
6650
|
+
totalDebounceDelayMs: 0,
|
|
6651
|
+
activeProjects: 0,
|
|
6652
|
+
averageDebounceDelayMs: 0,
|
|
6653
|
+
watcherActive: false
|
|
6654
|
+
};
|
|
5263
6655
|
const httpServer = createHttpServer({
|
|
5264
6656
|
host: wsHost,
|
|
5265
|
-
distDir:
|
|
6657
|
+
distDir: path9.resolve(import.meta.dirname, "../../dist"),
|
|
5266
6658
|
wsPort,
|
|
5267
6659
|
globalRoot: wpaths.globalRoot,
|
|
5268
|
-
apiToken: wsToken
|
|
6660
|
+
apiToken: wsToken,
|
|
6661
|
+
watcherMetrics,
|
|
6662
|
+
onFleetPing: () => {
|
|
6663
|
+
void fleetBroadcast?.();
|
|
6664
|
+
}
|
|
5269
6665
|
});
|
|
5270
|
-
const registryBaseDir =
|
|
6666
|
+
const registryBaseDir = path9.dirname(globalConfigPath);
|
|
5271
6667
|
httpServer.listen(httpPort, wsHost, () => {
|
|
5272
6668
|
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
5273
6669
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
@@ -5279,7 +6675,7 @@ async function startWebUI(opts = {}) {
|
|
|
5279
6675
|
wsPort,
|
|
5280
6676
|
host: wsHost,
|
|
5281
6677
|
projectRoot,
|
|
5282
|
-
projectName:
|
|
6678
|
+
projectName: path9.basename(projectRoot) || projectRoot,
|
|
5283
6679
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5284
6680
|
url: `http://${wsHost}:${httpPort}`
|
|
5285
6681
|
},
|
|
@@ -5306,6 +6702,11 @@ async function startWebUI(opts = {}) {
|
|
|
5306
6702
|
// reality. Crash exits are healed by the next register()/list() prune pass.
|
|
5307
6703
|
onShutdown: () => {
|
|
5308
6704
|
brainMonitor.stop();
|
|
6705
|
+
void mcpRegistry.stopAll().catch(() => void 0);
|
|
6706
|
+
if (disposeEvents) {
|
|
6707
|
+
disposeEvents();
|
|
6708
|
+
disposeEvents = null;
|
|
6709
|
+
}
|
|
5309
6710
|
if (eternalSubscription) {
|
|
5310
6711
|
eternalSubscription.dispose();
|
|
5311
6712
|
eternalSubscription = null;
|