@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/index.js
CHANGED
|
@@ -1,7 +1,179 @@
|
|
|
1
1
|
// src/server/index.ts
|
|
2
|
-
import { expectDefined as expectDefined2, GlobalMailbox as GlobalMailbox2, projectSlug, getSessionRegistry, AgentStatusTracker } from "@wrongstack/core";
|
|
2
|
+
import { expectDefined as expectDefined2, GlobalMailbox as GlobalMailbox2, projectSlug, getSessionRegistry, AgentStatusTracker, FleetNotifier } from "@wrongstack/core";
|
|
3
|
+
|
|
4
|
+
// src/server/handlers/worklist-handlers.ts
|
|
5
|
+
function sendResult(ws, ctx, ok, message) {
|
|
6
|
+
ctx.send(ws, { type: ok ? "ok" : "error", message });
|
|
7
|
+
}
|
|
8
|
+
function handleTodosGet(ctx, ws) {
|
|
9
|
+
ctx.send(ws, { type: "todos.updated", payload: { todos: ctx.context.todos } });
|
|
10
|
+
}
|
|
11
|
+
function handleTodosClear(ctx, ws) {
|
|
12
|
+
ctx.replaceTodos?.([]);
|
|
13
|
+
ctx.broadcast({ type: "todos.cleared" });
|
|
14
|
+
sendResult(ws, ctx, true, "Todo board cleared.");
|
|
15
|
+
}
|
|
16
|
+
function handleTodosRemove(ctx, ws, payload) {
|
|
17
|
+
if (!payload || payload.id === void 0 && payload.index === void 0) {
|
|
18
|
+
sendResult(ws, ctx, false, "todos.remove requires id or index.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const next = payload.id !== void 0 ? ctx.context.todos.filter((t) => t.id !== payload.id) : ctx.context.todos.filter((_, i) => i !== payload.index);
|
|
22
|
+
ctx.replaceTodos?.(next);
|
|
23
|
+
ctx.broadcast({ type: "todos.updated", payload: { todos: next } });
|
|
24
|
+
sendResult(ws, ctx, true, "Todo item removed.");
|
|
25
|
+
}
|
|
26
|
+
function handleTodoUpdate(ctx, ws, payload) {
|
|
27
|
+
const todo = ctx.context.todos.find((t) => t.id === payload.id);
|
|
28
|
+
if (!todo) {
|
|
29
|
+
sendResult(ws, ctx, false, `No todo with id "${payload.id}".`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const next = ctx.context.todos.map(
|
|
33
|
+
(t) => t.id === payload.id ? { ...t, ...payload.status !== void 0 && { status: payload.status }, ...payload.activeForm !== void 0 && { activeForm: payload.activeForm } } : t
|
|
34
|
+
);
|
|
35
|
+
ctx.replaceTodos?.(next);
|
|
36
|
+
ctx.broadcast({ type: "todos.updated", payload: { todos: next } });
|
|
37
|
+
sendResult(ws, ctx, true, `Todo "${todo.content}" updated.`);
|
|
38
|
+
}
|
|
39
|
+
async function handleTasksGet(ctx, ws) {
|
|
40
|
+
const taskPath = ctx.context.meta["task.path"];
|
|
41
|
+
if (typeof taskPath === "string" && taskPath) {
|
|
42
|
+
try {
|
|
43
|
+
const { loadTasks } = await import("@wrongstack/core");
|
|
44
|
+
const file = await loadTasks(taskPath);
|
|
45
|
+
ctx.send(ws, { type: "tasks.updated", payload: { tasks: file?.tasks ?? [] } });
|
|
46
|
+
} catch {
|
|
47
|
+
ctx.send(ws, { type: "tasks.updated", payload: { tasks: [] } });
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
ctx.send(ws, {
|
|
51
|
+
type: "tasks.updated",
|
|
52
|
+
payload: { tasks: [], error: "Task storage not configured." }
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function handleTaskUpdate(ctx, ws, payload) {
|
|
57
|
+
const taskPath = ctx.context.meta["task.path"];
|
|
58
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
59
|
+
sendResult(ws, ctx, false, "Task storage is not configured for this session.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const { loadTasks, saveTasks } = await import("@wrongstack/core");
|
|
64
|
+
const file = await loadTasks(taskPath);
|
|
65
|
+
if (!file) {
|
|
66
|
+
sendResult(ws, ctx, false, "No task file found.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const idx = file.tasks.findIndex((t) => t.id === payload.id);
|
|
70
|
+
if (idx === -1) {
|
|
71
|
+
sendResult(ws, ctx, false, `Task "${payload.id}" not found.`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
file.tasks[idx] = { ...file.tasks[idx], status: payload.status };
|
|
75
|
+
await saveTasks(taskPath, file);
|
|
76
|
+
ctx.broadcast({ type: "tasks.updated", payload: { tasks: file.tasks } });
|
|
77
|
+
sendResult(ws, ctx, true, `Task "${payload.id}" marked ${payload.status}.`);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
sendResult(ws, ctx, false, String(err));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function handlePlanGet(ctx, ws) {
|
|
83
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
84
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
85
|
+
if (typeof planPath === "string" && planPath) {
|
|
86
|
+
try {
|
|
87
|
+
const { loadPlan } = await import("@wrongstack/core");
|
|
88
|
+
const plan = await loadPlan(planPath);
|
|
89
|
+
ctx.send(ws, {
|
|
90
|
+
type: "plan.updated",
|
|
91
|
+
payload: {
|
|
92
|
+
plan: plan ?? {
|
|
93
|
+
version: 1,
|
|
94
|
+
sessionId,
|
|
95
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
96
|
+
items: []
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
ctx.send(ws, {
|
|
102
|
+
type: "plan.updated",
|
|
103
|
+
payload: {
|
|
104
|
+
plan: {
|
|
105
|
+
version: 1,
|
|
106
|
+
sessionId,
|
|
107
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
108
|
+
items: []
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
ctx.send(ws, {
|
|
115
|
+
type: "plan.updated",
|
|
116
|
+
payload: { plan: null, error: "Plan storage is not configured for this session." }
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function handlePlanTemplateUse(ctx, ws, template) {
|
|
121
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
122
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
123
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
124
|
+
sendResult(ws, ctx, false, "Plan storage is not configured for this session.");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem } = await import("@wrongstack/core");
|
|
129
|
+
const tpl = getPlanTemplate(template);
|
|
130
|
+
if (!tpl) {
|
|
131
|
+
sendResult(ws, ctx, false, `Unknown template "${template}".`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let plan = await loadPlan(planPath) ?? emptyPlan(sessionId);
|
|
135
|
+
for (const item of tpl.items) {
|
|
136
|
+
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
137
|
+
}
|
|
138
|
+
await savePlan(planPath, plan);
|
|
139
|
+
sendResult(ws, ctx, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
|
|
140
|
+
ctx.broadcast({ type: "plan.updated", payload: { plan } });
|
|
141
|
+
} catch (err) {
|
|
142
|
+
sendResult(ws, ctx, false, String(err));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function handlePlanItemUpdate(ctx, ws, payload) {
|
|
146
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
147
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
148
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
149
|
+
sendResult(ws, ctx, false, "Plan storage is not configured for this session.");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const { loadPlan, savePlan, mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
|
|
154
|
+
let changed = false;
|
|
155
|
+
const plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
156
|
+
const before = p.updatedAt;
|
|
157
|
+
const updated = setPlanItemStatus(p, payload.target, payload.status);
|
|
158
|
+
changed = updated.updatedAt !== before;
|
|
159
|
+
return updated;
|
|
160
|
+
});
|
|
161
|
+
if (!changed) {
|
|
162
|
+
sendResult(ws, ctx, false, `No plan item matched "${payload.target}".`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
sendResult(ws, ctx, true, `Plan item status updated to "${payload.status}".`);
|
|
166
|
+
ctx.broadcast({ type: "plan.updated", payload: { plan } });
|
|
167
|
+
} catch (err) {
|
|
168
|
+
sendResult(ws, ctx, false, String(err));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/server/index.ts
|
|
3
173
|
import { makeMailboxTool, makeMailSendTool, makeMailInboxTool, mailboxSessionTag } from "@wrongstack/core";
|
|
4
|
-
import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
|
|
174
|
+
import { toErrorMessage as toErrorMessage5, wstackGlobalRoot as wstackGlobalRoot2, projectHash, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
175
|
+
import { SkillInstaller } from "@wrongstack/core/skills";
|
|
176
|
+
import JSZip2 from "jszip";
|
|
5
177
|
import {
|
|
6
178
|
BrainMonitor,
|
|
7
179
|
DefaultBrainArbiter,
|
|
@@ -9,8 +181,8 @@ import {
|
|
|
9
181
|
createAutonomyBrain,
|
|
10
182
|
createTieredBrainArbiter
|
|
11
183
|
} from "@wrongstack/core";
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
184
|
+
import * as fs9 from "fs/promises";
|
|
185
|
+
import * as path9 from "path";
|
|
14
186
|
|
|
15
187
|
// src/server/http-server.ts
|
|
16
188
|
import * as fs from "fs/promises";
|
|
@@ -52,8 +224,8 @@ function extractTokenFromCookie(cookieHeader) {
|
|
|
52
224
|
for (const part of raw.split(";")) {
|
|
53
225
|
const eq = part.indexOf("=");
|
|
54
226
|
if (eq < 0) continue;
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
227
|
+
const name2 = part.slice(0, eq).trim();
|
|
228
|
+
if (name2 === "ws_token") {
|
|
57
229
|
try {
|
|
58
230
|
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
59
231
|
} catch {
|
|
@@ -129,6 +301,13 @@ function isInsideDist(candidate, distDir) {
|
|
|
129
301
|
const resolved = path.resolve(candidate);
|
|
130
302
|
return resolved === root || resolved.startsWith(root + path.sep);
|
|
131
303
|
}
|
|
304
|
+
function decodeSessionId(segment) {
|
|
305
|
+
try {
|
|
306
|
+
return decodeURIComponent(segment);
|
|
307
|
+
} catch {
|
|
308
|
+
return segment;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
132
311
|
function createHttpServer(opts) {
|
|
133
312
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
134
313
|
const distDir = path.resolve(opts.distDir);
|
|
@@ -154,6 +333,22 @@ function createHttpServer(opts) {
|
|
|
154
333
|
res.end("ok");
|
|
155
334
|
return;
|
|
156
335
|
}
|
|
336
|
+
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
337
|
+
const headerToken = req.headers["x-ws-token"];
|
|
338
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
339
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
340
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
341
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
opts.onFleetPing?.();
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
res.writeHead(204);
|
|
349
|
+
res.end();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
157
352
|
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
158
353
|
const headerToken = req.headers["x-ws-token"];
|
|
159
354
|
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
@@ -174,7 +369,89 @@ function createHttpServer(opts) {
|
|
|
174
369
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
175
370
|
return;
|
|
176
371
|
}
|
|
177
|
-
await handleApiSessionAgents(res, opts.globalRoot, agentsMatch[1]);
|
|
372
|
+
await handleApiSessionAgents(res, opts.globalRoot, decodeSessionId(agentsMatch[1]));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const eventsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
|
|
376
|
+
if (eventsMatch && req.method === "GET") {
|
|
377
|
+
const headerToken = req.headers["x-ws-token"];
|
|
378
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
379
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
380
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
381
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
|
|
385
|
+
const limit = Math.min(500, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
|
|
386
|
+
await handleApiSessionEvents(res, opts.globalRoot, decodeSessionId(eventsMatch[1]), limit);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const msgMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/message$/);
|
|
390
|
+
if (msgMatch && req.method === "POST") {
|
|
391
|
+
const headerToken = req.headers["x-ws-token"];
|
|
392
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
393
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
394
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
395
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
await handleApiSessionMessage(res, req, opts.globalRoot, decodeSessionId(msgMatch[1]));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const mailboxMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/mailbox$/);
|
|
402
|
+
if (mailboxMatch && req.method === "GET") {
|
|
403
|
+
const headerToken = req.headers["x-ws-token"];
|
|
404
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
405
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
406
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
407
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
await handleApiSessionMailbox(res, opts.globalRoot, decodeSessionId(mailboxMatch[1]));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const interruptMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/interrupt$/);
|
|
414
|
+
if (interruptMatch && req.method === "POST") {
|
|
415
|
+
const headerToken = req.headers["x-ws-token"];
|
|
416
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
417
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
418
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
419
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
await handleApiSessionInterrupt(
|
|
423
|
+
res,
|
|
424
|
+
req,
|
|
425
|
+
opts.globalRoot,
|
|
426
|
+
decodeSessionId(interruptMatch[1])
|
|
427
|
+
);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (url.pathname === "/api/fleet/broadcast" && req.method === "POST") {
|
|
431
|
+
const headerToken = req.headers["x-ws-token"];
|
|
432
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
433
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
434
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
435
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
await handleApiFleetBroadcast(res, req, opts.globalRoot);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (url.pathname === "/debug/watcher-metrics" && req.method === "GET") {
|
|
442
|
+
if (opts.watcherMetrics) {
|
|
443
|
+
const avgDelay = opts.watcherMetrics.broadcastsSent > 0 ? opts.watcherMetrics.totalDebounceDelayMs / opts.watcherMetrics.broadcastsSent : 0;
|
|
444
|
+
const response = {
|
|
445
|
+
...opts.watcherMetrics,
|
|
446
|
+
averageDebounceDelayMs: avgDelay,
|
|
447
|
+
timestamp: Date.now()
|
|
448
|
+
};
|
|
449
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
450
|
+
res.end(JSON.stringify(response));
|
|
451
|
+
} else {
|
|
452
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
453
|
+
res.end(JSON.stringify({ error: "File watcher metrics not available" }));
|
|
454
|
+
}
|
|
178
455
|
return;
|
|
179
456
|
}
|
|
180
457
|
let filePath;
|
|
@@ -306,6 +583,324 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
|
306
583
|
res.end(JSON.stringify({ error: String(err) }));
|
|
307
584
|
}
|
|
308
585
|
}
|
|
586
|
+
function blocksToText(content) {
|
|
587
|
+
if (typeof content === "string") return content;
|
|
588
|
+
if (Array.isArray(content)) {
|
|
589
|
+
return content.filter(
|
|
590
|
+
(b) => !!b && typeof b === "object" && b.type === "text" && typeof b.text === "string"
|
|
591
|
+
).map((b) => b.text).join("\n");
|
|
592
|
+
}
|
|
593
|
+
return "";
|
|
594
|
+
}
|
|
595
|
+
function clip(s, n = 600) {
|
|
596
|
+
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
597
|
+
}
|
|
598
|
+
function asString(v) {
|
|
599
|
+
if (typeof v === "string") return v;
|
|
600
|
+
try {
|
|
601
|
+
return JSON.stringify(v);
|
|
602
|
+
} catch {
|
|
603
|
+
return String(v);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function mapWatchEntry(ev) {
|
|
607
|
+
const ts = typeof ev["ts"] === "string" ? ev["ts"] : "";
|
|
608
|
+
switch (ev["type"]) {
|
|
609
|
+
case "user_input":
|
|
610
|
+
return { ts, role: "user", text: clip(blocksToText(ev["content"])) };
|
|
611
|
+
case "llm_response": {
|
|
612
|
+
const text = blocksToText(ev["content"]);
|
|
613
|
+
return text.trim() ? { ts, role: "assistant", text: clip(text) } : null;
|
|
614
|
+
}
|
|
615
|
+
case "tool_use":
|
|
616
|
+
case "tool_call_start": {
|
|
617
|
+
const input = ev["input"] ?? ev["args"];
|
|
618
|
+
const preview = input !== void 0 && input !== null ? clip(asString(input), 160) : "";
|
|
619
|
+
return { ts, role: "tool", tool: String(ev["name"] ?? "tool"), text: preview };
|
|
620
|
+
}
|
|
621
|
+
case "tool_result": {
|
|
622
|
+
if (ev["isError"]) return { ts, role: "error", text: clip(asString(ev["content"])) };
|
|
623
|
+
const out = asString(ev["content"]).trim();
|
|
624
|
+
return out ? { ts, role: "tool", tool: "\u21B3 result", text: clip(out, 240) } : null;
|
|
625
|
+
}
|
|
626
|
+
case "error":
|
|
627
|
+
case "provider_error":
|
|
628
|
+
return { ts, role: "error", text: clip(String(ev["message"] ?? "error")) };
|
|
629
|
+
case "agent_spawned":
|
|
630
|
+
return { ts, role: "system", text: `spawned ${String(ev["role"] ?? "agent")}` };
|
|
631
|
+
case "task_completed":
|
|
632
|
+
return { ts, role: "system", text: `task done: ${String(ev["title"] ?? "")}` };
|
|
633
|
+
case "task_failed":
|
|
634
|
+
return { ts, role: "system", text: `task failed: ${String(ev["title"] ?? "")}` };
|
|
635
|
+
default:
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
640
|
+
if (!globalRoot) {
|
|
641
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
642
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
|
|
647
|
+
const registry = new SessionRegistry(globalRoot);
|
|
648
|
+
const entry = await registry.get(sessionId);
|
|
649
|
+
if (!entry) {
|
|
650
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
651
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
655
|
+
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
656
|
+
const reader = new DefaultSessionReader2({ store });
|
|
657
|
+
const all = [];
|
|
658
|
+
for await (const ev of reader.replay(sessionId)) {
|
|
659
|
+
const mapped = mapWatchEntry(ev);
|
|
660
|
+
if (mapped) all.push(mapped);
|
|
661
|
+
}
|
|
662
|
+
const tail = all.slice(-limit);
|
|
663
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
664
|
+
res.end(
|
|
665
|
+
JSON.stringify({
|
|
666
|
+
sessionId,
|
|
667
|
+
status: entry.status,
|
|
668
|
+
clientType: entry.clientType,
|
|
669
|
+
projectName: entry.projectName,
|
|
670
|
+
total: all.length,
|
|
671
|
+
entries: tail
|
|
672
|
+
})
|
|
673
|
+
);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
676
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function readJsonBody(req) {
|
|
680
|
+
return new Promise((resolve5, reject) => {
|
|
681
|
+
let data = "";
|
|
682
|
+
req.on("data", (chunk) => {
|
|
683
|
+
data += chunk;
|
|
684
|
+
if (data.length > 64e3) {
|
|
685
|
+
reject(new Error("Request body too large"));
|
|
686
|
+
req.destroy();
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
req.on("end", () => {
|
|
690
|
+
try {
|
|
691
|
+
resolve5(data ? JSON.parse(data) : {});
|
|
692
|
+
} catch (err) {
|
|
693
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
req.on("error", reject);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
700
|
+
if (!globalRoot) {
|
|
701
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
702
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
let body;
|
|
706
|
+
try {
|
|
707
|
+
body = await readJsonBody(req);
|
|
708
|
+
} catch {
|
|
709
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
710
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const text = typeof body["text"] === "string" ? body["text"].trim() : "";
|
|
714
|
+
if (!text) {
|
|
715
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
716
|
+
res.end(JSON.stringify({ error: "text is required" }));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
720
|
+
const ALLOWED = /* @__PURE__ */ new Set(["steer", "ask", "assign", "note", "btw"]);
|
|
721
|
+
const rawType = typeof body["type"] === "string" ? body["type"] : "steer";
|
|
722
|
+
const type = ALLOWED.has(rawType) ? rawType : "steer";
|
|
723
|
+
const rawPriority = typeof body["priority"] === "string" ? body["priority"] : "";
|
|
724
|
+
const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
|
|
725
|
+
const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
|
|
726
|
+
try {
|
|
727
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
728
|
+
const registry = new SessionRegistry(globalRoot);
|
|
729
|
+
const entry = await registry.get(sessionId);
|
|
730
|
+
if (!entry) {
|
|
731
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
732
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
736
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
737
|
+
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
738
|
+
const sent = await mailbox.send({ from, to, type, subject, body: text, priority });
|
|
739
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
740
|
+
res.end(JSON.stringify({ ok: true, id: sent.id, to, type, delivered: entry.status }));
|
|
741
|
+
} catch (err) {
|
|
742
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
743
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
747
|
+
if (!globalRoot) {
|
|
748
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
749
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
754
|
+
const registry = new SessionRegistry(globalRoot);
|
|
755
|
+
const entry = await registry.get(sessionId);
|
|
756
|
+
if (!entry) {
|
|
757
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
758
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
762
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
763
|
+
const leaderAddr = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
764
|
+
const [inbound, outbound] = await Promise.all([
|
|
765
|
+
mailbox.query({ to: leaderAddr, limit: 50 }),
|
|
766
|
+
mailbox.query({ from: leaderAddr, limit: 50 })
|
|
767
|
+
]);
|
|
768
|
+
const seen = /* @__PURE__ */ new Set();
|
|
769
|
+
const thread = [...inbound, ...outbound].filter((m) => {
|
|
770
|
+
if (seen.has(m.id)) return false;
|
|
771
|
+
seen.add(m.id);
|
|
772
|
+
return true;
|
|
773
|
+
}).sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)).map((m) => ({
|
|
774
|
+
id: m.id,
|
|
775
|
+
from: m.from,
|
|
776
|
+
to: m.to,
|
|
777
|
+
type: m.type,
|
|
778
|
+
subject: m.subject,
|
|
779
|
+
body: m.body,
|
|
780
|
+
priority: m.priority,
|
|
781
|
+
// Whether the leader has read it, and when.
|
|
782
|
+
readByLeader: m.readBy?.[leaderAddr] ?? null,
|
|
783
|
+
readByCount: Object.keys(m.readBy ?? {}).length,
|
|
784
|
+
completed: m.completed,
|
|
785
|
+
outcome: m.outcome ?? null,
|
|
786
|
+
timestamp: m.timestamp,
|
|
787
|
+
replyTo: m.replyTo ?? null,
|
|
788
|
+
fromLeader: m.from === leaderAddr
|
|
789
|
+
}));
|
|
790
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
791
|
+
res.end(JSON.stringify({ sessionId, leader: leaderAddr, status: entry.status, thread }));
|
|
792
|
+
} catch (err) {
|
|
793
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
794
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
798
|
+
if (!globalRoot) {
|
|
799
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
800
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
let body = {};
|
|
804
|
+
try {
|
|
805
|
+
body = await readJsonBody(req);
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
|
|
809
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
810
|
+
try {
|
|
811
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
812
|
+
const registry = new SessionRegistry(globalRoot);
|
|
813
|
+
const entry = await registry.get(sessionId);
|
|
814
|
+
if (!entry) {
|
|
815
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
816
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
820
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
821
|
+
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
822
|
+
const sent = await mailbox.send({
|
|
823
|
+
from,
|
|
824
|
+
to,
|
|
825
|
+
type: "control",
|
|
826
|
+
subject: "interrupt",
|
|
827
|
+
body: reason,
|
|
828
|
+
priority: "high"
|
|
829
|
+
});
|
|
830
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
831
|
+
res.end(JSON.stringify({ ok: true, id: sent.id, to, delivered: entry.status }));
|
|
832
|
+
} catch (err) {
|
|
833
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
834
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
838
|
+
if (!globalRoot) {
|
|
839
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
840
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
let body;
|
|
844
|
+
try {
|
|
845
|
+
body = await readJsonBody(req);
|
|
846
|
+
} catch {
|
|
847
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
848
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const text = typeof body["text"] === "string" ? body["text"].trim() : "";
|
|
852
|
+
if (!text) {
|
|
853
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
854
|
+
res.end(JSON.stringify({ error: "text is required" }));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
858
|
+
try {
|
|
859
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
860
|
+
const registry = new SessionRegistry(globalRoot);
|
|
861
|
+
const all = await registry.list();
|
|
862
|
+
const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
|
|
863
|
+
const targets = all.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true);
|
|
864
|
+
if (targets.length === 0) {
|
|
865
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
866
|
+
res.end(JSON.stringify({ ok: true, delivered: 0 }));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const mbByDir = /* @__PURE__ */ new Map();
|
|
870
|
+
const mailboxFor = (projectRoot) => {
|
|
871
|
+
const dir = resolveWstackPaths2({ projectRoot, globalRoot }).projectDir;
|
|
872
|
+
let mb = mbByDir.get(dir);
|
|
873
|
+
if (!mb) {
|
|
874
|
+
mb = new GlobalMailbox3(dir);
|
|
875
|
+
mbByDir.set(dir, mb);
|
|
876
|
+
}
|
|
877
|
+
return mb;
|
|
878
|
+
};
|
|
879
|
+
let delivered = 0;
|
|
880
|
+
await Promise.all(
|
|
881
|
+
targets.map(async (s) => {
|
|
882
|
+
try {
|
|
883
|
+
const mb = mailboxFor(s.projectRoot);
|
|
884
|
+
await mb.send({
|
|
885
|
+
from,
|
|
886
|
+
to: `leader@${mailboxSessionTag2(s.sessionId)}`,
|
|
887
|
+
type: "steer",
|
|
888
|
+
subject: "Broadcast from Fleet HQ",
|
|
889
|
+
body: text,
|
|
890
|
+
priority: "high"
|
|
891
|
+
});
|
|
892
|
+
delivered++;
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
})
|
|
896
|
+
);
|
|
897
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
898
|
+
res.end(JSON.stringify({ ok: true, delivered, targets: targets.length }));
|
|
899
|
+
} catch (err) {
|
|
900
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
901
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
309
904
|
|
|
310
905
|
// src/server/file-handlers.ts
|
|
311
906
|
import * as fs2 from "fs/promises";
|
|
@@ -335,8 +930,8 @@ var KEEP_DOTFILES = /* @__PURE__ */ new Set([
|
|
|
335
930
|
".eslintrc",
|
|
336
931
|
".prettierrc"
|
|
337
932
|
]);
|
|
338
|
-
function isHiddenEntry(
|
|
339
|
-
return
|
|
933
|
+
function isHiddenEntry(name2) {
|
|
934
|
+
return name2.startsWith(".") && !KEEP_DOTFILES.has(name2);
|
|
340
935
|
}
|
|
341
936
|
function rankFiles(paths, query, limit) {
|
|
342
937
|
const q = query.toLowerCase();
|
|
@@ -379,7 +974,7 @@ function broadcast(clients, msg) {
|
|
|
379
974
|
}
|
|
380
975
|
}
|
|
381
976
|
}
|
|
382
|
-
function
|
|
977
|
+
function sendResult2(ws, success, message) {
|
|
383
978
|
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
384
979
|
}
|
|
385
980
|
function errMessage(err) {
|
|
@@ -528,23 +1123,238 @@ async function handleMemoryRemember(ws, msg, memoryStore) {
|
|
|
528
1123
|
const { text, scope } = msg.payload;
|
|
529
1124
|
try {
|
|
530
1125
|
await memoryStore.remember(text, scope ?? "project-memory");
|
|
531
|
-
|
|
1126
|
+
sendResult2(ws, true, "Saved to memory");
|
|
532
1127
|
} catch (err) {
|
|
533
|
-
|
|
1128
|
+
sendResult2(ws, false, errMessage(err));
|
|
534
1129
|
}
|
|
535
1130
|
}
|
|
536
1131
|
async function handleMemoryForget(ws, msg, memoryStore) {
|
|
537
1132
|
const { text, scope } = msg.payload;
|
|
538
1133
|
try {
|
|
539
1134
|
const removed = await memoryStore.forget(text, scope ?? "project-memory");
|
|
540
|
-
|
|
1135
|
+
sendResult2(
|
|
541
1136
|
ws,
|
|
542
1137
|
removed > 0,
|
|
543
1138
|
removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
|
|
544
1139
|
);
|
|
545
1140
|
} catch (err) {
|
|
546
|
-
|
|
1141
|
+
sendResult2(ws, false, errMessage(err));
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/server/mcp-handlers.ts
|
|
1146
|
+
import { allServers } from "@wrongstack/core";
|
|
1147
|
+
import {
|
|
1148
|
+
addMcp,
|
|
1149
|
+
disableMcp,
|
|
1150
|
+
discoverMcp,
|
|
1151
|
+
enableMcp,
|
|
1152
|
+
listMcp,
|
|
1153
|
+
removeMcp,
|
|
1154
|
+
restartMcp,
|
|
1155
|
+
updateMcp
|
|
1156
|
+
} from "@wrongstack/mcp";
|
|
1157
|
+
function mapStatus(raw) {
|
|
1158
|
+
switch (raw) {
|
|
1159
|
+
case "connected":
|
|
1160
|
+
return "connected";
|
|
1161
|
+
case "connecting":
|
|
1162
|
+
case "reconnecting":
|
|
1163
|
+
return "connecting";
|
|
1164
|
+
case "failed":
|
|
1165
|
+
return "error";
|
|
1166
|
+
case "dormant":
|
|
1167
|
+
return "sleeping";
|
|
1168
|
+
default:
|
|
1169
|
+
return "stopped";
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function toView(info) {
|
|
1173
|
+
const view = {
|
|
1174
|
+
name: info.name,
|
|
1175
|
+
transport: info.transport,
|
|
1176
|
+
// A dormant lazy server is "asleep", not stopped — preserve that even when
|
|
1177
|
+
// it's enabled in config.
|
|
1178
|
+
status: info.status === "dormant" ? "sleeping" : info.enabled === false ? "stopped" : mapStatus(info.status),
|
|
1179
|
+
enabled: info.enabled,
|
|
1180
|
+
tools: info.tools
|
|
1181
|
+
};
|
|
1182
|
+
if (info.description !== void 0) view.description = info.description;
|
|
1183
|
+
if (info.lazy !== void 0) view.lazy = info.lazy;
|
|
1184
|
+
return view;
|
|
1185
|
+
}
|
|
1186
|
+
function deps(ws, globalConfigPath, registry) {
|
|
1187
|
+
if (!registry || !globalConfigPath) {
|
|
1188
|
+
send(ws, {
|
|
1189
|
+
type: "mcp.operation_result",
|
|
1190
|
+
payload: { success: false, message: "MCP registry is not available in this session." }
|
|
1191
|
+
});
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
return { configPath: globalConfigPath, registry, presets: allServers() };
|
|
1195
|
+
}
|
|
1196
|
+
function name(msg) {
|
|
1197
|
+
return msg.payload?.name ?? "";
|
|
1198
|
+
}
|
|
1199
|
+
async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
|
|
1200
|
+
if (!mcpRegistry || !globalConfigPath) {
|
|
1201
|
+
send(ws, { type: "mcp.list", payload: { servers: [] } });
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const servers = await listMcp({
|
|
1205
|
+
configPath: globalConfigPath,
|
|
1206
|
+
registry: mcpRegistry,
|
|
1207
|
+
presets: allServers()
|
|
1208
|
+
});
|
|
1209
|
+
send(ws, { type: "mcp.list", payload: { servers: servers.map(toView) } });
|
|
1210
|
+
}
|
|
1211
|
+
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1212
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1213
|
+
if (!d) return;
|
|
1214
|
+
const result = await addMcp(msg.payload, d);
|
|
1215
|
+
if (result.ok && result.server) {
|
|
1216
|
+
send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
|
|
1217
|
+
if (result.registryError) {
|
|
1218
|
+
send(ws, {
|
|
1219
|
+
type: "mcp.server.error",
|
|
1220
|
+
payload: { name: result.server.name, error: result.registryError }
|
|
1221
|
+
});
|
|
1222
|
+
} else if (result.server.enabled) {
|
|
1223
|
+
send(ws, { type: "mcp.server.connected", payload: { name: result.server.name } });
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
send(ws, {
|
|
1227
|
+
type: "mcp.operation_result",
|
|
1228
|
+
payload: { success: result.ok, message: result.message }
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1232
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1233
|
+
if (!d) return;
|
|
1234
|
+
const result = await updateMcp(msg.payload, d);
|
|
1235
|
+
if (result.ok && result.server) {
|
|
1236
|
+
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
1237
|
+
}
|
|
1238
|
+
send(ws, {
|
|
1239
|
+
type: "mcp.operation_result",
|
|
1240
|
+
payload: { success: result.ok, message: result.message }
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
async function handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1244
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1245
|
+
if (!d) return;
|
|
1246
|
+
const result = await removeMcp(name(msg), d);
|
|
1247
|
+
if (result.ok) {
|
|
1248
|
+
send(ws, { type: "mcp.server.removed", payload: { name: name(msg) } });
|
|
1249
|
+
}
|
|
1250
|
+
send(ws, {
|
|
1251
|
+
type: "mcp.operation_result",
|
|
1252
|
+
payload: { success: result.ok, message: result.message }
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
async function handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1256
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1257
|
+
if (!d) return;
|
|
1258
|
+
const result = await enableMcp(name(msg), d);
|
|
1259
|
+
if (result.ok && result.server) {
|
|
1260
|
+
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
1261
|
+
if (result.registryError) {
|
|
1262
|
+
send(ws, {
|
|
1263
|
+
type: "mcp.server.error",
|
|
1264
|
+
payload: { name: name(msg), error: result.registryError }
|
|
1265
|
+
});
|
|
1266
|
+
} else {
|
|
1267
|
+
send(ws, { type: "mcp.server.connected", payload: { name: name(msg) } });
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
send(ws, {
|
|
1271
|
+
type: "mcp.operation_result",
|
|
1272
|
+
payload: { success: result.ok, message: result.message }
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
async function handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1276
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1277
|
+
if (!d) return;
|
|
1278
|
+
const result = await disableMcp(name(msg), d);
|
|
1279
|
+
if (result.ok) {
|
|
1280
|
+
send(ws, { type: "mcp.server.sleeping", payload: { name: name(msg) } });
|
|
1281
|
+
if (result.server) {
|
|
1282
|
+
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
send(ws, {
|
|
1286
|
+
type: "mcp.operation_result",
|
|
1287
|
+
payload: { success: result.ok, message: result.message }
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
async function handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1291
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1292
|
+
if (!d) return;
|
|
1293
|
+
try {
|
|
1294
|
+
await d.registry.stop(name(msg));
|
|
1295
|
+
send(ws, { type: "mcp.server.sleeping", payload: { name: name(msg) } });
|
|
1296
|
+
send(ws, {
|
|
1297
|
+
type: "mcp.operation_result",
|
|
1298
|
+
payload: { success: true, message: `Server "${name(msg)}" stopped` }
|
|
1299
|
+
});
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
1302
|
+
send(ws, { type: "mcp.server.error", payload: { name: name(msg), error } });
|
|
1303
|
+
send(ws, {
|
|
1304
|
+
type: "mcp.operation_result",
|
|
1305
|
+
payload: { success: false, message: `Failed to stop "${name(msg)}": ${error}` }
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
async function handleMcpWake(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1310
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1311
|
+
if (!d) return;
|
|
1312
|
+
send(ws, { type: "mcp.server.waking", payload: { name: name(msg) } });
|
|
1313
|
+
const result = await restartMcp(name(msg), d);
|
|
1314
|
+
if (result.ok && !result.registryError) {
|
|
1315
|
+
send(ws, { type: "mcp.server.connected", payload: { name: name(msg) } });
|
|
1316
|
+
} else if (result.registryError) {
|
|
1317
|
+
send(ws, {
|
|
1318
|
+
type: "mcp.server.error",
|
|
1319
|
+
payload: { name: name(msg), error: result.registryError }
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
send(ws, {
|
|
1323
|
+
type: "mcp.operation_result",
|
|
1324
|
+
payload: { success: result.ok, message: result.message }
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
async function handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1328
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1329
|
+
if (!d) return;
|
|
1330
|
+
const result = await restartMcp(name(msg), d);
|
|
1331
|
+
if (result.ok && !result.registryError) {
|
|
1332
|
+
send(ws, { type: "mcp.server.connected", payload: { name: name(msg) } });
|
|
1333
|
+
} else if (result.registryError) {
|
|
1334
|
+
send(ws, {
|
|
1335
|
+
type: "mcp.server.error",
|
|
1336
|
+
payload: { name: name(msg), error: result.registryError }
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
send(ws, {
|
|
1340
|
+
type: "mcp.operation_result",
|
|
1341
|
+
payload: { success: result.ok, message: result.message }
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
|
|
1345
|
+
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
1346
|
+
if (!d) return;
|
|
1347
|
+
const result = await discoverMcp(name(msg), d);
|
|
1348
|
+
if (result.ok) {
|
|
1349
|
+
send(ws, {
|
|
1350
|
+
type: "mcp.server.discovered",
|
|
1351
|
+
payload: { name: name(msg), tools: result.tools ?? [] }
|
|
1352
|
+
});
|
|
547
1353
|
}
|
|
1354
|
+
send(ws, {
|
|
1355
|
+
type: "mcp.operation_result",
|
|
1356
|
+
payload: { success: result.ok, message: result.message }
|
|
1357
|
+
});
|
|
548
1358
|
}
|
|
549
1359
|
|
|
550
1360
|
// src/server/index.ts
|
|
@@ -580,12 +1390,14 @@ import {
|
|
|
580
1390
|
repairToolUseAdjacency,
|
|
581
1391
|
resolveContextWindowPolicy,
|
|
582
1392
|
enhanceUserPrompt,
|
|
583
|
-
recentTextTurns
|
|
1393
|
+
recentTextTurns,
|
|
1394
|
+
resolveProviderModelList
|
|
584
1395
|
} from "@wrongstack/core";
|
|
585
1396
|
import { ToolExecutor } from "@wrongstack/core/execution";
|
|
586
1397
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
|
|
587
1398
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
|
|
588
1399
|
import { builtinToolsPack, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
|
|
1400
|
+
import { MCPRegistry } from "@wrongstack/mcp";
|
|
589
1401
|
import { WebSocketServer } from "ws";
|
|
590
1402
|
|
|
591
1403
|
// ../runtime/src/container.ts
|
|
@@ -1832,9 +2644,9 @@ var WorktreeWebSocketHandler = class {
|
|
|
1832
2644
|
|
|
1833
2645
|
// src/server/mailbox-handlers.ts
|
|
1834
2646
|
import { GlobalMailbox, resolveProjectDir } from "@wrongstack/core";
|
|
1835
|
-
async function handleMailboxMessages(ws,
|
|
2647
|
+
async function handleMailboxMessages(ws, deps2, payload) {
|
|
1836
2648
|
try {
|
|
1837
|
-
const dir = resolveProjectDir(
|
|
2649
|
+
const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
|
|
1838
2650
|
const mb = new GlobalMailbox(dir);
|
|
1839
2651
|
const messages = await mb.query({
|
|
1840
2652
|
limit: payload?.limit ?? 30,
|
|
@@ -1868,9 +2680,9 @@ async function handleMailboxMessages(ws, deps, payload) {
|
|
|
1868
2680
|
send(ws, { type: "mailbox.messages", payload: { messages: [], error: errMessage(err) } });
|
|
1869
2681
|
}
|
|
1870
2682
|
}
|
|
1871
|
-
async function handleMailboxAgents(ws,
|
|
2683
|
+
async function handleMailboxAgents(ws, deps2, payload) {
|
|
1872
2684
|
try {
|
|
1873
|
-
const dir = resolveProjectDir(
|
|
2685
|
+
const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
|
|
1874
2686
|
const mb = new GlobalMailbox(dir);
|
|
1875
2687
|
const agents = payload?.onlineOnly ? await mb.getOnlineAgents() : await mb.getAgentStatuses();
|
|
1876
2688
|
send(ws, {
|
|
@@ -1897,9 +2709,9 @@ async function handleMailboxAgents(ws, deps, payload) {
|
|
|
1897
2709
|
send(ws, { type: "mailbox.agents", payload: { agents: [], error: errMessage(err) } });
|
|
1898
2710
|
}
|
|
1899
2711
|
}
|
|
1900
|
-
async function handleMailboxClear(ws,
|
|
2712
|
+
async function handleMailboxClear(ws, deps2) {
|
|
1901
2713
|
try {
|
|
1902
|
-
const dir = resolveProjectDir(
|
|
2714
|
+
const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
|
|
1903
2715
|
const mb = new GlobalMailbox(dir);
|
|
1904
2716
|
await mb.clearAll();
|
|
1905
2717
|
send(ws, { type: "mailbox.cleared", payload: {} });
|
|
@@ -1907,9 +2719,9 @@ async function handleMailboxClear(ws, deps) {
|
|
|
1907
2719
|
send(ws, { type: "mailbox.cleared", payload: { error: errMessage(err) } });
|
|
1908
2720
|
}
|
|
1909
2721
|
}
|
|
1910
|
-
async function handleMailboxPurge(ws,
|
|
2722
|
+
async function handleMailboxPurge(ws, deps2, opts) {
|
|
1911
2723
|
try {
|
|
1912
|
-
const dir = resolveProjectDir(
|
|
2724
|
+
const dir = resolveProjectDir(deps2.projectRoot, deps2.globalRoot);
|
|
1913
2725
|
const mb = new GlobalMailbox(dir);
|
|
1914
2726
|
const result = await mb.purgeStale(opts);
|
|
1915
2727
|
send(ws, { type: "mailbox.purged", payload: result });
|
|
@@ -2208,7 +3020,7 @@ function writeKeysBack(cfg, keys) {
|
|
|
2208
3020
|
}
|
|
2209
3021
|
cfg.apiKeys = keys;
|
|
2210
3022
|
const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined(keys[0]);
|
|
2211
|
-
cfg.apiKey
|
|
3023
|
+
delete cfg.apiKey;
|
|
2212
3024
|
if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
|
|
2213
3025
|
cfg.activeKey = active.label;
|
|
2214
3026
|
}
|
|
@@ -2307,9 +3119,9 @@ function projectSavedProviders(providers) {
|
|
|
2307
3119
|
});
|
|
2308
3120
|
}
|
|
2309
3121
|
var probeScrubber = new DefaultSecretScrubber2();
|
|
2310
|
-
function createProviderHandlers(
|
|
2311
|
-
const { globalConfigPath, vault, broadcast: broadcast2, clients } =
|
|
2312
|
-
let configWriteLock =
|
|
3122
|
+
function createProviderHandlers(deps2) {
|
|
3123
|
+
const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
|
|
3124
|
+
let configWriteLock = deps2.getConfigWriteLock();
|
|
2313
3125
|
async function loadConfigProviders() {
|
|
2314
3126
|
return loadSavedProviders(globalConfigPath, vault);
|
|
2315
3127
|
}
|
|
@@ -2324,7 +3136,7 @@ function createProviderHandlers(deps) {
|
|
|
2324
3136
|
}));
|
|
2325
3137
|
});
|
|
2326
3138
|
configWriteLock = next;
|
|
2327
|
-
|
|
3139
|
+
deps2.setConfigWriteLock(next);
|
|
2328
3140
|
await next;
|
|
2329
3141
|
}
|
|
2330
3142
|
async function handleKeyUpsert(ws, providerId, label, apiKey) {
|
|
@@ -2332,9 +3144,9 @@ function createProviderHandlers(deps) {
|
|
|
2332
3144
|
const providers = await loadConfigProviders();
|
|
2333
3145
|
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
2334
3146
|
if (result.ok) await saveConfigProviders(providers);
|
|
2335
|
-
|
|
3147
|
+
sendResult2(ws, result.ok, result.message);
|
|
2336
3148
|
} catch (err) {
|
|
2337
|
-
|
|
3149
|
+
sendResult2(ws, false, errMessage(err));
|
|
2338
3150
|
}
|
|
2339
3151
|
}
|
|
2340
3152
|
async function handleKeyDelete(ws, providerId, label) {
|
|
@@ -2342,9 +3154,9 @@ function createProviderHandlers(deps) {
|
|
|
2342
3154
|
const providers = await loadConfigProviders();
|
|
2343
3155
|
const result = deleteKey(providers, providerId, label);
|
|
2344
3156
|
if (result.ok) await saveConfigProviders(providers);
|
|
2345
|
-
|
|
3157
|
+
sendResult2(ws, result.ok, result.message);
|
|
2346
3158
|
} catch (err) {
|
|
2347
|
-
|
|
3159
|
+
sendResult2(ws, false, errMessage(err));
|
|
2348
3160
|
}
|
|
2349
3161
|
}
|
|
2350
3162
|
async function handleKeySetActive(ws, providerId, label) {
|
|
@@ -2352,9 +3164,9 @@ function createProviderHandlers(deps) {
|
|
|
2352
3164
|
const providers = await loadConfigProviders();
|
|
2353
3165
|
const result = setActiveKey(providers, providerId, label);
|
|
2354
3166
|
if (result.ok) await saveConfigProviders(providers);
|
|
2355
|
-
|
|
3167
|
+
sendResult2(ws, result.ok, result.message);
|
|
2356
3168
|
} catch (err) {
|
|
2357
|
-
|
|
3169
|
+
sendResult2(ws, false, errMessage(err));
|
|
2358
3170
|
}
|
|
2359
3171
|
}
|
|
2360
3172
|
async function handleProviderAdd(ws, payload) {
|
|
@@ -2362,13 +3174,13 @@ function createProviderHandlers(deps) {
|
|
|
2362
3174
|
const providers = await loadConfigProviders();
|
|
2363
3175
|
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
2364
3176
|
if (result.ok) await saveConfigProviders(providers);
|
|
2365
|
-
|
|
3177
|
+
sendResult2(ws, result.ok, result.message);
|
|
2366
3178
|
if (result.ok) {
|
|
2367
3179
|
console.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
2368
3180
|
broadcastSaved(providers);
|
|
2369
3181
|
}
|
|
2370
3182
|
} catch (err) {
|
|
2371
|
-
|
|
3183
|
+
sendResult2(ws, false, errMessage(err));
|
|
2372
3184
|
}
|
|
2373
3185
|
}
|
|
2374
3186
|
async function handleProviderRemove(ws, providerId) {
|
|
@@ -2376,9 +3188,9 @@ function createProviderHandlers(deps) {
|
|
|
2376
3188
|
const providers = await loadConfigProviders();
|
|
2377
3189
|
const result = removeProvider(providers, providerId);
|
|
2378
3190
|
if (result.ok) await saveConfigProviders(providers);
|
|
2379
|
-
|
|
3191
|
+
sendResult2(ws, result.ok, result.message);
|
|
2380
3192
|
} catch (err) {
|
|
2381
|
-
|
|
3193
|
+
sendResult2(ws, false, errMessage(err));
|
|
2382
3194
|
}
|
|
2383
3195
|
}
|
|
2384
3196
|
function broadcastSaved(providers) {
|
|
@@ -2392,15 +3204,15 @@ function createProviderHandlers(deps) {
|
|
|
2392
3204
|
const providers = await loadConfigProviders();
|
|
2393
3205
|
const cfg = providers[providerId];
|
|
2394
3206
|
if (!cfg) {
|
|
2395
|
-
|
|
3207
|
+
sendResult2(ws, false, `Unknown provider "${providerId}"`);
|
|
2396
3208
|
return;
|
|
2397
3209
|
}
|
|
2398
3210
|
delete cfg.models;
|
|
2399
3211
|
await saveConfigProviders(providers);
|
|
2400
|
-
|
|
3212
|
+
sendResult2(ws, true, `Cleared model allowlist for ${providerId}`);
|
|
2401
3213
|
broadcastSaved(providers);
|
|
2402
3214
|
} catch (err) {
|
|
2403
|
-
|
|
3215
|
+
sendResult2(ws, false, errMessage(err));
|
|
2404
3216
|
}
|
|
2405
3217
|
}
|
|
2406
3218
|
async function handleProviderUndoClear(ws, providerId, previousModels) {
|
|
@@ -2408,15 +3220,15 @@ function createProviderHandlers(deps) {
|
|
|
2408
3220
|
const providers = await loadConfigProviders();
|
|
2409
3221
|
const cfg = providers[providerId];
|
|
2410
3222
|
if (!cfg) {
|
|
2411
|
-
|
|
3223
|
+
sendResult2(ws, false, `Unknown provider "${providerId}"`);
|
|
2412
3224
|
return;
|
|
2413
3225
|
}
|
|
2414
3226
|
cfg.models = [...previousModels];
|
|
2415
3227
|
await saveConfigProviders(providers);
|
|
2416
|
-
|
|
3228
|
+
sendResult2(ws, true, `Restored ${previousModels.length} model(s) for ${providerId}`);
|
|
2417
3229
|
broadcastSaved(providers);
|
|
2418
3230
|
} catch (err) {
|
|
2419
|
-
|
|
3231
|
+
sendResult2(ws, false, errMessage(err));
|
|
2420
3232
|
}
|
|
2421
3233
|
}
|
|
2422
3234
|
async function handleProviderUpdate(ws, payload) {
|
|
@@ -2424,7 +3236,7 @@ function createProviderHandlers(deps) {
|
|
|
2424
3236
|
const providers = await loadConfigProviders();
|
|
2425
3237
|
const cfg = providers[payload.id];
|
|
2426
3238
|
if (!cfg) {
|
|
2427
|
-
|
|
3239
|
+
sendResult2(ws, false, `Unknown provider "${payload.id}"`);
|
|
2428
3240
|
return;
|
|
2429
3241
|
}
|
|
2430
3242
|
if (payload.family !== void 0) cfg.family = payload.family;
|
|
@@ -2432,10 +3244,10 @@ function createProviderHandlers(deps) {
|
|
|
2432
3244
|
if (payload.envVars !== void 0) cfg.envVars = payload.envVars;
|
|
2433
3245
|
if (payload.models !== void 0) cfg.models = payload.models;
|
|
2434
3246
|
await saveConfigProviders(providers);
|
|
2435
|
-
|
|
3247
|
+
sendResult2(ws, true, `Updated ${payload.id}`);
|
|
2436
3248
|
broadcastSaved(providers);
|
|
2437
3249
|
} catch (err) {
|
|
2438
|
-
|
|
3250
|
+
sendResult2(ws, false, errMessage(err));
|
|
2439
3251
|
}
|
|
2440
3252
|
}
|
|
2441
3253
|
async function handleProviderProbe(ws, providerId, timeoutMs) {
|
|
@@ -2480,9 +3292,12 @@ function createProviderHandlers(deps) {
|
|
|
2480
3292
|
}
|
|
2481
3293
|
|
|
2482
3294
|
// src/server/setup-events.ts
|
|
3295
|
+
import * as fs5 from "fs/promises";
|
|
3296
|
+
import { watch as fsWatch } from "fs";
|
|
2483
3297
|
import * as path5 from "path";
|
|
2484
|
-
function setupEvents(
|
|
2485
|
-
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge } =
|
|
3298
|
+
function setupEvents(deps2) {
|
|
3299
|
+
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
|
|
3300
|
+
const disposers = [];
|
|
2486
3301
|
events.on("iteration.started", (e) => {
|
|
2487
3302
|
const maxIt = typeof context.meta["maxIterations"] === "number" ? context.meta["maxIterations"] : config.tools?.maxIterations ?? 100;
|
|
2488
3303
|
broadcast2(clients, {
|
|
@@ -2513,7 +3328,11 @@ function setupEvents(deps) {
|
|
|
2513
3328
|
events.on("tool.progress", (e) => {
|
|
2514
3329
|
broadcast2(clients, {
|
|
2515
3330
|
type: "tool.progress",
|
|
2516
|
-
|
|
3331
|
+
// Nested `event` shape — the client handler reads `payload.event?.text`
|
|
3332
|
+
// and early-returns on a falsy text, so a flat { eventType, text } payload
|
|
3333
|
+
// makes live tool progress (bash streaming, partial_output, warnings)
|
|
3334
|
+
// never render. Must match WSToolProgress and the CLI server.
|
|
3335
|
+
payload: { id: e.id, name: e.name, event: { type: e.event.type, text: e.event.text, data: e.event.data } }
|
|
2517
3336
|
});
|
|
2518
3337
|
sessionBridge?.append({
|
|
2519
3338
|
type: "tool_progress",
|
|
@@ -2679,20 +3498,165 @@ function setupEvents(deps) {
|
|
|
2679
3498
|
events.onPattern("brain.*", (eventName, payload) => {
|
|
2680
3499
|
broadcast2(clients, { type: "brain.event", payload: { event: eventName, ...payload } });
|
|
2681
3500
|
});
|
|
3501
|
+
events.on("client.status", async (e) => {
|
|
3502
|
+
broadcast2(clients, { type: "client.status_update", payload: e });
|
|
3503
|
+
if (wpaths?.projectStatus) {
|
|
3504
|
+
try {
|
|
3505
|
+
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
3506
|
+
const dir = path5.dirname(statusFile);
|
|
3507
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
3508
|
+
await fs5.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
|
|
3509
|
+
} catch (err) {
|
|
3510
|
+
console.error("[setup-events] Failed to write status.json:", err);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
});
|
|
3514
|
+
if (wpaths?.projectStatus && wpaths.configDir) {
|
|
3515
|
+
const projectsDir = path5.join(wpaths.configDir, "projects");
|
|
3516
|
+
const knownProjectHashes = /* @__PURE__ */ new Set();
|
|
3517
|
+
const debounceTimers = /* @__PURE__ */ new Map();
|
|
3518
|
+
const DEBOUNCE_MS = 150;
|
|
3519
|
+
const pendingStatuses = /* @__PURE__ */ new Map();
|
|
3520
|
+
if (watcherMetrics) {
|
|
3521
|
+
watcherMetrics.fileChangesDetected = 0;
|
|
3522
|
+
watcherMetrics.filesProcessed = 0;
|
|
3523
|
+
watcherMetrics.broadcastsSent = 0;
|
|
3524
|
+
watcherMetrics.debounceResets = 0;
|
|
3525
|
+
watcherMetrics.totalDebounceDelayMs = 0;
|
|
3526
|
+
watcherMetrics.activeProjects = 0;
|
|
3527
|
+
watcherMetrics.averageDebounceDelayMs = 0;
|
|
3528
|
+
watcherMetrics.watcherActive = true;
|
|
3529
|
+
}
|
|
3530
|
+
const getAverageDebounceDelay = () => {
|
|
3531
|
+
if (!watcherMetrics || watcherMetrics.broadcastsSent === 0) return 0;
|
|
3532
|
+
return watcherMetrics.totalDebounceDelayMs / watcherMetrics.broadcastsSent;
|
|
3533
|
+
};
|
|
3534
|
+
const logWatcherMetrics = () => {
|
|
3535
|
+
if (!watcherMetrics) return;
|
|
3536
|
+
watcherMetrics.averageDebounceDelayMs = getAverageDebounceDelay();
|
|
3537
|
+
console.log(
|
|
3538
|
+
`[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`
|
|
3539
|
+
);
|
|
3540
|
+
};
|
|
3541
|
+
const metricsInterval = setInterval(logWatcherMetrics, 6e4);
|
|
3542
|
+
const broadcastStatus = (projectHash2, statusData, actualDelayMs) => {
|
|
3543
|
+
broadcast2(clients, { type: "client.status_update", payload: statusData });
|
|
3544
|
+
if (watcherMetrics) {
|
|
3545
|
+
watcherMetrics.broadcastsSent++;
|
|
3546
|
+
watcherMetrics.totalDebounceDelayMs += actualDelayMs;
|
|
3547
|
+
watcherMetrics.averageDebounceDelayMs = getAverageDebounceDelay();
|
|
3548
|
+
}
|
|
3549
|
+
};
|
|
3550
|
+
const scheduleBroadcast = (projectHash2, statusData) => {
|
|
3551
|
+
const now = Date.now();
|
|
3552
|
+
const existing = pendingStatuses.get(projectHash2);
|
|
3553
|
+
if (existing && watcherMetrics) {
|
|
3554
|
+
watcherMetrics.debounceResets++;
|
|
3555
|
+
}
|
|
3556
|
+
pendingStatuses.set(projectHash2, {
|
|
3557
|
+
data: statusData,
|
|
3558
|
+
firstWriteAt: existing ? existing.firstWriteAt : now
|
|
3559
|
+
});
|
|
3560
|
+
const existingTimer = debounceTimers.get(projectHash2);
|
|
3561
|
+
if (existingTimer) {
|
|
3562
|
+
clearTimeout(existingTimer);
|
|
3563
|
+
}
|
|
3564
|
+
const timer = setTimeout(() => {
|
|
3565
|
+
debounceTimers.delete(projectHash2);
|
|
3566
|
+
const pending = pendingStatuses.get(projectHash2);
|
|
3567
|
+
if (pending) {
|
|
3568
|
+
const actualDelay = Date.now() - pending.firstWriteAt;
|
|
3569
|
+
broadcastStatus(projectHash2, pending.data, actualDelay);
|
|
3570
|
+
pendingStatuses.delete(projectHash2);
|
|
3571
|
+
}
|
|
3572
|
+
}, DEBOUNCE_MS);
|
|
3573
|
+
debounceTimers.set(projectHash2, timer);
|
|
3574
|
+
};
|
|
3575
|
+
let watcher;
|
|
3576
|
+
const startWatcher = async () => {
|
|
3577
|
+
try {
|
|
3578
|
+
await fs5.mkdir(projectsDir, { recursive: true });
|
|
3579
|
+
watcher = fsWatch(projectsDir, { persistent: true, recursive: true }, async (eventType, filename) => {
|
|
3580
|
+
if (eventType === "change") {
|
|
3581
|
+
if (filename == null) return;
|
|
3582
|
+
if (watcherMetrics) watcherMetrics.fileChangesDetected++;
|
|
3583
|
+
const targetFile = path5.join(projectsDir, String(filename));
|
|
3584
|
+
if (targetFile.endsWith("status.json")) {
|
|
3585
|
+
const projectHash2 = path5.basename(path5.dirname(targetFile));
|
|
3586
|
+
if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
if (watcherMetrics) watcherMetrics.filesProcessed++;
|
|
3590
|
+
try {
|
|
3591
|
+
const content = await fs5.readFile(targetFile, "utf-8");
|
|
3592
|
+
const statusData = JSON.parse(content);
|
|
3593
|
+
if (statusData.projectHash) {
|
|
3594
|
+
const hash = String(statusData.projectHash);
|
|
3595
|
+
if (!knownProjectHashes.has(hash)) {
|
|
3596
|
+
knownProjectHashes.add(hash);
|
|
3597
|
+
if (watcherMetrics) watcherMetrics.activeProjects = knownProjectHashes.size;
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
scheduleBroadcast(projectHash2, statusData);
|
|
3601
|
+
} catch {
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
});
|
|
3606
|
+
console.log(`[setup-events] Watching ${projectsDir} for status.json changes (hash-filtered, debounced)`);
|
|
3607
|
+
} catch (err) {
|
|
3608
|
+
console.error("[setup-events] Failed to start status file watcher:", err);
|
|
3609
|
+
}
|
|
3610
|
+
};
|
|
3611
|
+
events.on("client.status", (e) => {
|
|
3612
|
+
if (e.projectHash) {
|
|
3613
|
+
const hash = String(e.projectHash);
|
|
3614
|
+
if (!knownProjectHashes.has(hash)) {
|
|
3615
|
+
knownProjectHashes.add(hash);
|
|
3616
|
+
if (watcherMetrics) watcherMetrics.activeProjects = knownProjectHashes.size;
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
});
|
|
3620
|
+
startWatcher();
|
|
3621
|
+
disposers.push(() => {
|
|
3622
|
+
clearInterval(metricsInterval);
|
|
3623
|
+
logWatcherMetrics();
|
|
3624
|
+
if (watcherMetrics) watcherMetrics.watcherActive = false;
|
|
3625
|
+
for (const [projectHash2, pending] of pendingStatuses) {
|
|
3626
|
+
const timer = debounceTimers.get(projectHash2);
|
|
3627
|
+
if (timer) {
|
|
3628
|
+
clearTimeout(timer);
|
|
3629
|
+
broadcastStatus(projectHash2, pending.data, 0);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
for (const timer of debounceTimers.values()) {
|
|
3633
|
+
clearTimeout(timer);
|
|
3634
|
+
}
|
|
3635
|
+
debounceTimers.clear();
|
|
3636
|
+
pendingStatuses.clear();
|
|
3637
|
+
if (watcher) {
|
|
3638
|
+
watcher.close();
|
|
3639
|
+
console.log("[setup-events] Closed status file watcher");
|
|
3640
|
+
}
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
2682
3643
|
const globalRoot = globalConfigPath ? path5.dirname(globalConfigPath) : void 0;
|
|
2683
3644
|
if (globalRoot) {
|
|
2684
|
-
const
|
|
3645
|
+
const broadcastSessions = async () => {
|
|
2685
3646
|
try {
|
|
2686
3647
|
const { SessionRegistry } = await import("@wrongstack/core");
|
|
2687
3648
|
const registry = new SessionRegistry(globalRoot);
|
|
2688
3649
|
const sessions = await registry.list();
|
|
2689
|
-
const
|
|
3650
|
+
const mySlug = sessions.find((s) => s.pid === process.pid)?.projectSlug;
|
|
3651
|
+
const live = sessions.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true).map((s) => ({
|
|
2690
3652
|
sessionId: s.sessionId,
|
|
2691
3653
|
projectName: s.projectName,
|
|
2692
3654
|
projectSlug: s.projectSlug,
|
|
2693
3655
|
projectRoot: s.projectRoot,
|
|
2694
3656
|
workingDir: s.workingDir,
|
|
2695
3657
|
gitBranch: s.gitBranch,
|
|
3658
|
+
// Surface (tui/webui/cli) so Fleet HQ can label each live client node.
|
|
3659
|
+
clientType: s.clientType,
|
|
2696
3660
|
status: s.status,
|
|
2697
3661
|
pid: s.pid,
|
|
2698
3662
|
startedAt: s.startedAt,
|
|
@@ -2704,20 +3668,52 @@ function setupEvents(deps) {
|
|
|
2704
3668
|
currentTool: a.currentTool,
|
|
2705
3669
|
iterations: a.iterations,
|
|
2706
3670
|
toolCalls: a.toolCalls,
|
|
3671
|
+
costUsd: a.costUsd,
|
|
3672
|
+
tokensIn: a.tokensIn,
|
|
3673
|
+
tokensOut: a.tokensOut,
|
|
3674
|
+
ctxPct: a.ctxPct,
|
|
3675
|
+
model: a.model,
|
|
3676
|
+
partialText: a.partialText,
|
|
2707
3677
|
lastActivityAt: a.lastActivityAt
|
|
2708
3678
|
}))
|
|
2709
3679
|
}));
|
|
2710
3680
|
broadcast2(clients, { type: "sessions.status_update", payload: { sessions: live } });
|
|
2711
3681
|
} catch {
|
|
2712
3682
|
}
|
|
2713
|
-
}
|
|
3683
|
+
};
|
|
3684
|
+
onFleetBroadcaster?.(broadcastSessions);
|
|
3685
|
+
const statusInterval = setInterval(() => void broadcastSessions(), 5e3);
|
|
2714
3686
|
if (statusInterval.unref) statusInterval.unref();
|
|
3687
|
+
disposers.push(() => clearInterval(statusInterval));
|
|
3688
|
+
let regDebounce;
|
|
3689
|
+
try {
|
|
3690
|
+
const regWatcher = fsWatch(globalRoot, { persistent: false }, (_event, filename) => {
|
|
3691
|
+
const name2 = filename ? String(filename) : "";
|
|
3692
|
+
if (!name2.startsWith("session-registry.json") || name2.endsWith(".lock")) return;
|
|
3693
|
+
if (regDebounce) clearTimeout(regDebounce);
|
|
3694
|
+
regDebounce = setTimeout(() => void broadcastSessions(), 150);
|
|
3695
|
+
});
|
|
3696
|
+
disposers.push(() => {
|
|
3697
|
+
if (regDebounce) clearTimeout(regDebounce);
|
|
3698
|
+
regWatcher.close();
|
|
3699
|
+
});
|
|
3700
|
+
} catch {
|
|
3701
|
+
}
|
|
3702
|
+
void broadcastSessions();
|
|
2715
3703
|
}
|
|
3704
|
+
return () => {
|
|
3705
|
+
for (const dispose of disposers) {
|
|
3706
|
+
try {
|
|
3707
|
+
dispose();
|
|
3708
|
+
} catch {
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
};
|
|
2716
3712
|
}
|
|
2717
3713
|
|
|
2718
3714
|
// src/server/custom-context-modes.ts
|
|
2719
3715
|
import { listContextWindowModes, atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
2720
|
-
import * as
|
|
3716
|
+
import * as fs6 from "fs/promises";
|
|
2721
3717
|
import * as path6 from "path";
|
|
2722
3718
|
var STORE_FILENAME = "custom-context-modes.json";
|
|
2723
3719
|
function storePath(wrongstackDir) {
|
|
@@ -2729,7 +3725,7 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
2729
3725
|
const load2 = async () => {
|
|
2730
3726
|
modes.clear();
|
|
2731
3727
|
try {
|
|
2732
|
-
const raw = await
|
|
3728
|
+
const raw = await fs6.readFile(storePath(wrongstackDir), "utf8");
|
|
2733
3729
|
const parsed = JSON.parse(raw);
|
|
2734
3730
|
if (Array.isArray(parsed.modes)) {
|
|
2735
3731
|
for (const m of parsed.modes) {
|
|
@@ -2909,14 +3905,14 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
|
|
|
2909
3905
|
}
|
|
2910
3906
|
|
|
2911
3907
|
// src/server/shell-open.ts
|
|
2912
|
-
import * as
|
|
3908
|
+
import * as fs7 from "fs/promises";
|
|
2913
3909
|
import * as path7 from "path";
|
|
2914
3910
|
import { spawn as spawn2 } from "child_process";
|
|
2915
3911
|
var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
|
|
2916
3912
|
async function handleShellOpen(req, logger) {
|
|
2917
3913
|
try {
|
|
2918
3914
|
const resolved = path7.resolve(req.path);
|
|
2919
|
-
await
|
|
3915
|
+
await fs7.access(resolved);
|
|
2920
3916
|
if (METACHAR_REGEX.test(resolved)) {
|
|
2921
3917
|
return { success: false, message: "Path contains unsupported characters." };
|
|
2922
3918
|
}
|
|
@@ -2953,12 +3949,436 @@ async function handleShellOpen(req, logger) {
|
|
|
2953
3949
|
)
|
|
2954
3950
|
);
|
|
2955
3951
|
}
|
|
2956
|
-
} else {
|
|
2957
|
-
return { success: false, message: `Unknown shell.open target: ${String(req.target)}` };
|
|
3952
|
+
} else {
|
|
3953
|
+
return { success: false, message: `Unknown shell.open target: ${String(req.target)}` };
|
|
3954
|
+
}
|
|
3955
|
+
return { success: true, message: `Opened ${req.target} at ${resolved}` };
|
|
3956
|
+
} catch (err) {
|
|
3957
|
+
return { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
// src/server/git-handlers.ts
|
|
3962
|
+
async function handleGitInfo(ws, projectRoot) {
|
|
3963
|
+
const cwd = projectRoot || void 0;
|
|
3964
|
+
try {
|
|
3965
|
+
const { execFile: ef } = await import("child_process");
|
|
3966
|
+
const git = (args) => new Promise((resolve5) => {
|
|
3967
|
+
ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
|
|
3968
|
+
resolve5(err ? "" : stdout.trim());
|
|
3969
|
+
});
|
|
3970
|
+
});
|
|
3971
|
+
const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
|
|
3972
|
+
git(["branch", "--show-current"]),
|
|
3973
|
+
git(["diff", "--stat"]),
|
|
3974
|
+
git(["status", "--porcelain"]),
|
|
3975
|
+
git(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"])
|
|
3976
|
+
]);
|
|
3977
|
+
const branch = branchRaw || "(detached)";
|
|
3978
|
+
const addMatch = /(\d+)\s+insertion/i.exec(diffRaw);
|
|
3979
|
+
const delMatch = /(\d+)\s+deletion/i.exec(diffRaw);
|
|
3980
|
+
const added = addMatch ? Number(addMatch[1]) : 0;
|
|
3981
|
+
const deleted = delMatch ? Number(delMatch[1]) : 0;
|
|
3982
|
+
const untracked = statusRaw.split("\n").filter((l) => l.startsWith("??")).length;
|
|
3983
|
+
const [behindRaw, aheadRaw] = (upstreamRaw || "0 0").split(" ");
|
|
3984
|
+
const behind = Number(behindRaw) || 0;
|
|
3985
|
+
const ahead = Number(aheadRaw) || 0;
|
|
3986
|
+
send(ws, { type: "git.info", payload: { branch, added, deleted, untracked, ahead, behind } });
|
|
3987
|
+
} catch {
|
|
3988
|
+
send(ws, { type: "git.info", payload: { branch: "", added: 0, deleted: 0, untracked: 0, ahead: 0, behind: 0 } });
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
function makeGit(cwd) {
|
|
3992
|
+
return async (args) => {
|
|
3993
|
+
const { execFile: ef } = await import("child_process");
|
|
3994
|
+
return new Promise((resolve5) => {
|
|
3995
|
+
ef(
|
|
3996
|
+
"git",
|
|
3997
|
+
args,
|
|
3998
|
+
{ cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
|
|
3999
|
+
(err, stdout) => resolve5(err ? "" : stdout)
|
|
4000
|
+
);
|
|
4001
|
+
});
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
async function handleGitChanges(ws, projectRoot) {
|
|
4005
|
+
const cwd = projectRoot || void 0;
|
|
4006
|
+
try {
|
|
4007
|
+
const git = makeGit(cwd);
|
|
4008
|
+
const [statusRaw, unstagedNumstat, stagedNumstat] = await Promise.all([
|
|
4009
|
+
git(["status", "--porcelain", "-z"]),
|
|
4010
|
+
git(["diff", "--numstat", "-z"]),
|
|
4011
|
+
git(["diff", "--cached", "--numstat", "-z"])
|
|
4012
|
+
]);
|
|
4013
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4014
|
+
const parseNumstat = (raw) => {
|
|
4015
|
+
const parts = raw.split("\0");
|
|
4016
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4017
|
+
const entry = parts[i];
|
|
4018
|
+
if (!entry) continue;
|
|
4019
|
+
const m = /^(\d+|-)\t(\d+|-)\t(.*)$/.exec(entry);
|
|
4020
|
+
if (!m) continue;
|
|
4021
|
+
const added = m[1] === "-" ? 0 : Number(m[1]);
|
|
4022
|
+
const deleted = m[2] === "-" ? 0 : Number(m[2]);
|
|
4023
|
+
let path10 = m[3] ?? "";
|
|
4024
|
+
if (path10 === "") {
|
|
4025
|
+
i += 1;
|
|
4026
|
+
path10 = parts[i + 1] ?? parts[i] ?? "";
|
|
4027
|
+
i += 1;
|
|
4028
|
+
}
|
|
4029
|
+
if (!path10) continue;
|
|
4030
|
+
const prev = counts.get(path10) ?? { added: 0, deleted: 0 };
|
|
4031
|
+
counts.set(path10, { added: prev.added + added, deleted: prev.deleted + deleted });
|
|
4032
|
+
}
|
|
4033
|
+
};
|
|
4034
|
+
parseNumstat(unstagedNumstat);
|
|
4035
|
+
parseNumstat(stagedNumstat);
|
|
4036
|
+
const records = statusRaw.split("\0").filter((r) => r.length > 0);
|
|
4037
|
+
const files = [];
|
|
4038
|
+
for (let i = 0; i < records.length; i++) {
|
|
4039
|
+
const rec = records[i];
|
|
4040
|
+
if (!rec || rec.length < 3) continue;
|
|
4041
|
+
const x = rec[0] ?? " ";
|
|
4042
|
+
const y = rec[1] ?? " ";
|
|
4043
|
+
const path10 = rec.slice(3);
|
|
4044
|
+
const isRename = x === "R" || x === "C" || y === "R" || y === "C";
|
|
4045
|
+
if (isRename) i += 1;
|
|
4046
|
+
let status;
|
|
4047
|
+
if (x === "?" && y === "?") status = "?";
|
|
4048
|
+
else if (x === "U" || y === "U" || x === "A" && y === "A" || x === "D" && y === "D") status = "U";
|
|
4049
|
+
else if (x === "R" || y === "R") status = "R";
|
|
4050
|
+
else if (x === "C" || y === "C") status = "C";
|
|
4051
|
+
else if (x === "A" || y === "A") status = "A";
|
|
4052
|
+
else if (x === "D" || y === "D") status = "D";
|
|
4053
|
+
else status = "M";
|
|
4054
|
+
const staged = x !== " " && x !== "?";
|
|
4055
|
+
let added = counts.get(path10)?.added ?? 0;
|
|
4056
|
+
let deleted = counts.get(path10)?.deleted ?? 0;
|
|
4057
|
+
if (status === "?") {
|
|
4058
|
+
added = await countUntrackedLines(cwd, path10);
|
|
4059
|
+
deleted = 0;
|
|
4060
|
+
}
|
|
4061
|
+
files.push({ path: path10, status, added, deleted, staged });
|
|
4062
|
+
}
|
|
4063
|
+
send(ws, { type: "git.changes", payload: { files } });
|
|
4064
|
+
} catch (err) {
|
|
4065
|
+
send(ws, {
|
|
4066
|
+
type: "git.changes",
|
|
4067
|
+
payload: { files: [], error: err instanceof Error ? err.message : String(err) }
|
|
4068
|
+
});
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
async function countUntrackedLines(cwd, relPath) {
|
|
4072
|
+
try {
|
|
4073
|
+
const { readFile: readFile8 } = await import("fs/promises");
|
|
4074
|
+
const { join: join8 } = await import("path");
|
|
4075
|
+
const abs = cwd ? join8(cwd, relPath) : relPath;
|
|
4076
|
+
const buf = await readFile8(abs);
|
|
4077
|
+
if (buf.includes(0)) return 0;
|
|
4078
|
+
if (buf.length === 0) return 0;
|
|
4079
|
+
let lines = 0;
|
|
4080
|
+
for (let i = 0; i < buf.length; i++) if (buf[i] === 10) lines++;
|
|
4081
|
+
if (buf[buf.length - 1] !== 10) lines++;
|
|
4082
|
+
return lines;
|
|
4083
|
+
} catch {
|
|
4084
|
+
return 0;
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
var MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
4088
|
+
async function handleGitDiff(ws, projectRoot, path10) {
|
|
4089
|
+
const cwd = projectRoot || void 0;
|
|
4090
|
+
const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path10, ...extra } });
|
|
4091
|
+
if (!path10 || path10.includes("\0") || path10.includes("..")) {
|
|
4092
|
+
reply({ oldText: "", newText: "", error: "invalid path" });
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
try {
|
|
4096
|
+
const git = makeGit(cwd);
|
|
4097
|
+
const { readFile: readFile8 } = await import("fs/promises");
|
|
4098
|
+
const { join: join8 } = await import("path");
|
|
4099
|
+
const oldText = await git(["show", `HEAD:${path10}`]);
|
|
4100
|
+
let newText = "";
|
|
4101
|
+
try {
|
|
4102
|
+
const abs = cwd ? join8(cwd, path10) : path10;
|
|
4103
|
+
const buf = await readFile8(abs);
|
|
4104
|
+
if (buf.includes(0)) {
|
|
4105
|
+
reply({ oldText: "", newText: "", binary: true });
|
|
4106
|
+
return;
|
|
4107
|
+
}
|
|
4108
|
+
if (buf.length > MAX_DIFF_BYTES) {
|
|
4109
|
+
reply({ oldText: "", newText: "", tooLarge: true });
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
4112
|
+
newText = buf.toString("utf8");
|
|
4113
|
+
} catch {
|
|
4114
|
+
newText = "";
|
|
4115
|
+
}
|
|
4116
|
+
if ((oldText.length || 0) > MAX_DIFF_BYTES) {
|
|
4117
|
+
reply({ oldText: "", newText: "", tooLarge: true });
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
if (oldText.includes("\0")) {
|
|
4121
|
+
reply({ oldText: "", newText: "", binary: true });
|
|
4122
|
+
return;
|
|
4123
|
+
}
|
|
4124
|
+
reply({ oldText, newText });
|
|
4125
|
+
} catch (err) {
|
|
4126
|
+
reply({ oldText: "", newText: "", error: err instanceof Error ? err.message : String(err) });
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
// src/server/skills-handlers.ts
|
|
4131
|
+
import { promises as fs8 } from "fs";
|
|
4132
|
+
import path8 from "path";
|
|
4133
|
+
import JSZip from "jszip";
|
|
4134
|
+
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
4135
|
+
async function handleSkillsContent(ws, ctx, msg) {
|
|
4136
|
+
if (!ctx.skillLoader) {
|
|
4137
|
+
send(ws, { type: "skills.content", payload: { name: "", body: "", path: "", source: "", relatedFiles: [], references: [], error: "Skills not enabled" } });
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
const contentPayload = msg.payload;
|
|
4141
|
+
if (!contentPayload?.name) {
|
|
4142
|
+
send(ws, { type: "skills.content", payload: { name: "", body: "", path: "", source: "", relatedFiles: [], references: [], error: "Skill name is required" } });
|
|
4143
|
+
return;
|
|
4144
|
+
}
|
|
4145
|
+
try {
|
|
4146
|
+
const { name: name2, source } = contentPayload;
|
|
4147
|
+
const entries = await ctx.skillLoader.listEntries();
|
|
4148
|
+
const entry = entries.find((e) => e.name.toLowerCase() === name2.toLowerCase());
|
|
4149
|
+
if (!entry) {
|
|
4150
|
+
send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
const body = await fs8.readFile(entry.path, "utf8");
|
|
4154
|
+
const skillDir = path8.dirname(entry.path);
|
|
4155
|
+
let relatedFiles = [];
|
|
4156
|
+
try {
|
|
4157
|
+
const files = await fs8.readdir(skillDir);
|
|
4158
|
+
relatedFiles = files.filter((f) => f !== path8.basename(entry.path)).map((f) => path8.join(skillDir, f));
|
|
4159
|
+
} catch {
|
|
4160
|
+
}
|
|
4161
|
+
const nameLower = name2.toLowerCase();
|
|
4162
|
+
const refResults = await Promise.all(
|
|
4163
|
+
entries.filter((e) => e.name.toLowerCase() !== nameLower).map(async (e) => {
|
|
4164
|
+
try {
|
|
4165
|
+
const content = await fs8.readFile(e.path, "utf8");
|
|
4166
|
+
return [e.name, content.toLowerCase().includes(nameLower)];
|
|
4167
|
+
} catch {
|
|
4168
|
+
return [e.name, false];
|
|
4169
|
+
}
|
|
4170
|
+
})
|
|
4171
|
+
);
|
|
4172
|
+
const refs = refResults.filter(([, hasRef]) => hasRef).map(([n]) => n);
|
|
4173
|
+
send(ws, { type: "skills.content", payload: { name: name2, body, path: entry.path, source, relatedFiles, references: refs } });
|
|
4174
|
+
} catch (err) {
|
|
4175
|
+
send(ws, { type: "skills.content", payload: { name: contentPayload.name, body: "", path: "", source: contentPayload.source, relatedFiles: [], references: [], error: errMessage(err) } });
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
async function handleSkillsInstall(ws, ctx, msg) {
|
|
4179
|
+
if (!ctx.skillInstaller) {
|
|
4180
|
+
send(ws, { type: "skills.installed", payload: { success: false, error: "Skills not enabled" } });
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4183
|
+
const installPayload = msg.payload;
|
|
4184
|
+
if (!installPayload?.ref?.trim()) {
|
|
4185
|
+
send(ws, { type: "skills.installed", payload: { success: false, error: "Skill reference is required (e.g. owner/repo or https://github.com/owner/repo)" } });
|
|
4186
|
+
return;
|
|
4187
|
+
}
|
|
4188
|
+
try {
|
|
4189
|
+
const results = await ctx.skillInstaller.install(installPayload.ref.trim(), { global: installPayload.global });
|
|
4190
|
+
send(ws, {
|
|
4191
|
+
type: "skills.installed",
|
|
4192
|
+
payload: {
|
|
4193
|
+
success: true,
|
|
4194
|
+
results,
|
|
4195
|
+
error: null
|
|
4196
|
+
}
|
|
4197
|
+
});
|
|
4198
|
+
} catch (err) {
|
|
4199
|
+
send(ws, {
|
|
4200
|
+
type: "skills.installed",
|
|
4201
|
+
payload: {
|
|
4202
|
+
success: false,
|
|
4203
|
+
error: errMessage(err)
|
|
4204
|
+
}
|
|
4205
|
+
});
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
async function handleSkillsUninstall(ws, ctx, msg) {
|
|
4209
|
+
if (!ctx.skillInstaller) {
|
|
4210
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: "Skills not enabled" } });
|
|
4211
|
+
return;
|
|
4212
|
+
}
|
|
4213
|
+
const uninstallPayload = msg.payload;
|
|
4214
|
+
if (!uninstallPayload?.name?.trim()) {
|
|
4215
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: "Skill name is required" } });
|
|
4216
|
+
return;
|
|
4217
|
+
}
|
|
4218
|
+
try {
|
|
4219
|
+
await ctx.skillInstaller.uninstall(uninstallPayload.name.trim(), { global: uninstallPayload.global });
|
|
4220
|
+
send(ws, { type: "skills.uninstalled", payload: { success: true, error: null } });
|
|
4221
|
+
} catch (err) {
|
|
4222
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: errMessage(err) } });
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
async function handleSkillsUpdate(ws, ctx, msg) {
|
|
4226
|
+
if (!ctx.skillInstaller) {
|
|
4227
|
+
send(ws, { type: "skills.updated", payload: { success: false, error: "Skills not enabled" } });
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
4230
|
+
const updatePayload = msg.payload;
|
|
4231
|
+
try {
|
|
4232
|
+
const result = await ctx.skillInstaller.update(updatePayload?.name, { global: updatePayload?.global });
|
|
4233
|
+
send(ws, {
|
|
4234
|
+
type: "skills.updated",
|
|
4235
|
+
payload: {
|
|
4236
|
+
success: true,
|
|
4237
|
+
error: null,
|
|
4238
|
+
updated: result.updated,
|
|
4239
|
+
unchanged: result.unchanged,
|
|
4240
|
+
errors: result.errors
|
|
4241
|
+
}
|
|
4242
|
+
});
|
|
4243
|
+
} catch (err) {
|
|
4244
|
+
send(ws, { type: "skills.updated", payload: { success: false, error: errMessage(err) } });
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
async function handleSkillsCreate(ws, ctx, msg) {
|
|
4248
|
+
const createPayload = msg.payload;
|
|
4249
|
+
if (!createPayload?.name?.trim()) {
|
|
4250
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Skill name is required" } });
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
4253
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(createPayload.name.trim())) {
|
|
4254
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Skill name must be kebab-case (e.g. my-new-skill)" } });
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
if (!createPayload?.description?.trim()) {
|
|
4258
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Description/trigger is required" } });
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
try {
|
|
4262
|
+
const targetDir = createPayload.scope === "global" ? path8.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path8.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
4263
|
+
try {
|
|
4264
|
+
await fs8.access(targetDir);
|
|
4265
|
+
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
4266
|
+
return;
|
|
4267
|
+
} catch {
|
|
4268
|
+
}
|
|
4269
|
+
await fs8.mkdir(targetDir, { recursive: true });
|
|
4270
|
+
const lines = createPayload.description.trim().split("\n");
|
|
4271
|
+
const firstLine = lines[0].trim();
|
|
4272
|
+
const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
|
|
4273
|
+
const descriptionText = firstLine + (bodyLines.length > 0 ? `
|
|
4274
|
+
${bodyLines.join("\n")}` : "");
|
|
4275
|
+
const trigger = bodyLines.find((l) => l.toLowerCase().startsWith("triggers:")) ?? "";
|
|
4276
|
+
const skillContent = [
|
|
4277
|
+
"---",
|
|
4278
|
+
`name: ${createPayload.name.trim()}`,
|
|
4279
|
+
"description: |",
|
|
4280
|
+
` ${descriptionText.replace(/\n/g, "\n ")}`,
|
|
4281
|
+
`version: 1.0.0`,
|
|
4282
|
+
"---",
|
|
4283
|
+
"",
|
|
4284
|
+
`# ${createPayload.name.trim().split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}`,
|
|
4285
|
+
"",
|
|
4286
|
+
"## Overview",
|
|
4287
|
+
"",
|
|
4288
|
+
firstLine,
|
|
4289
|
+
"",
|
|
4290
|
+
...bodyLines.length > 0 ? bodyLines.filter((l) => !l.toLowerCase().startsWith("triggers:")) : [],
|
|
4291
|
+
"",
|
|
4292
|
+
"## Rules",
|
|
4293
|
+
"- TODO: add your first rule",
|
|
4294
|
+
"",
|
|
4295
|
+
"## Patterns",
|
|
4296
|
+
"### Do",
|
|
4297
|
+
"```ts",
|
|
4298
|
+
"// TODO: add a good example",
|
|
4299
|
+
"```",
|
|
4300
|
+
"",
|
|
4301
|
+
"### Don't",
|
|
4302
|
+
"```ts",
|
|
4303
|
+
"// TODO: add a bad example",
|
|
4304
|
+
"```",
|
|
4305
|
+
"",
|
|
4306
|
+
"## Workflow",
|
|
4307
|
+
"1. TODO: describe step one",
|
|
4308
|
+
"2. TODO: describe step two",
|
|
4309
|
+
"",
|
|
4310
|
+
trigger ? `
|
|
4311
|
+
${trigger}
|
|
4312
|
+
` : "",
|
|
4313
|
+
"## Skills in scope",
|
|
4314
|
+
"- `bug-hunter` \u2014 for systematic bug detection patterns",
|
|
4315
|
+
"- `output-standards` \u2014 for standardized `<next_steps>` formatting"
|
|
4316
|
+
].join("\n");
|
|
4317
|
+
await fs8.writeFile(path8.join(targetDir, "SKILL.md"), skillContent, "utf-8");
|
|
4318
|
+
send(ws, {
|
|
4319
|
+
type: "skills.created",
|
|
4320
|
+
payload: {
|
|
4321
|
+
success: true,
|
|
4322
|
+
error: null,
|
|
4323
|
+
skill: { name: createPayload.name.trim(), path: path8.join(targetDir, "SKILL.md"), scope: createPayload.scope }
|
|
4324
|
+
}
|
|
4325
|
+
});
|
|
4326
|
+
} catch (err) {
|
|
4327
|
+
send(ws, { type: "skills.created", payload: { success: false, error: errMessage(err) } });
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
async function handleSkillsEdit(ws, ctx, msg) {
|
|
4331
|
+
if (!ctx.skillLoader) {
|
|
4332
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skills not enabled" } });
|
|
4333
|
+
return;
|
|
4334
|
+
}
|
|
4335
|
+
const editPayload = msg.payload;
|
|
4336
|
+
if (!editPayload?.name?.trim()) {
|
|
4337
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skill name is required" } });
|
|
4338
|
+
return;
|
|
4339
|
+
}
|
|
4340
|
+
if (!editPayload?.body) {
|
|
4341
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skill body is required" } });
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4344
|
+
try {
|
|
4345
|
+
const entries = await ctx.skillLoader.listEntries();
|
|
4346
|
+
const entry = entries.find((e) => e.name.toLowerCase() === editPayload.name.toLowerCase());
|
|
4347
|
+
if (!entry) {
|
|
4348
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: `Skill "${editPayload.name}" not found` } });
|
|
4349
|
+
return;
|
|
2958
4350
|
}
|
|
2959
|
-
|
|
4351
|
+
if (entry.scope.includes("bundled")) {
|
|
4352
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Bundled skills cannot be edited" } });
|
|
4353
|
+
return;
|
|
4354
|
+
}
|
|
4355
|
+
await fs8.writeFile(entry.path, editPayload.body, "utf-8");
|
|
4356
|
+
send(ws, { type: "skills.edited", payload: { success: true, error: null } });
|
|
2960
4357
|
} catch (err) {
|
|
2961
|
-
|
|
4358
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: errMessage(err) } });
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
async function handleSkillsExport(ws, ctx) {
|
|
4362
|
+
if (!ctx.skillLoader) {
|
|
4363
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: "Skills not enabled" } });
|
|
4364
|
+
return;
|
|
4365
|
+
}
|
|
4366
|
+
try {
|
|
4367
|
+
const entries = await ctx.skillLoader.listEntries();
|
|
4368
|
+
const zip = new JSZip();
|
|
4369
|
+
for (const entry of entries) {
|
|
4370
|
+
try {
|
|
4371
|
+
const body = await ctx.skillLoader.readBody(entry.name);
|
|
4372
|
+
const safeName = entry.name.replace(/\//g, "_");
|
|
4373
|
+
zip.file(`${safeName}/SKILL.md`, body);
|
|
4374
|
+
} catch {
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
const zipBuffer = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
|
4378
|
+
const zipBase64 = zipBuffer.toString("base64");
|
|
4379
|
+
send(ws, { type: "skills.exported", payload: { zipBase64, skillCount: entries.length, error: void 0 } });
|
|
4380
|
+
} catch (err) {
|
|
4381
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: errMessage(err) } });
|
|
2962
4382
|
}
|
|
2963
4383
|
}
|
|
2964
4384
|
|
|
@@ -3049,6 +4469,21 @@ async function startWebUI(opts = {}) {
|
|
|
3049
4469
|
toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
|
|
3050
4470
|
toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
|
|
3051
4471
|
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
4472
|
+
const mcpRegistry = new MCPRegistry({
|
|
4473
|
+
toolRegistry,
|
|
4474
|
+
events,
|
|
4475
|
+
log: logger,
|
|
4476
|
+
// Lazy-connect (per-server `lazy`) manifest cache + default idle auto-sleep.
|
|
4477
|
+
cacheDir: wpaths.cacheDir
|
|
4478
|
+
});
|
|
4479
|
+
if (config.features.mcp && config.mcpServers) {
|
|
4480
|
+
for (const [name2, cfg] of Object.entries(config.mcpServers)) {
|
|
4481
|
+
if (cfg.enabled === false) continue;
|
|
4482
|
+
void mcpRegistry.start({ ...cfg, name: name2 }).catch((err) => {
|
|
4483
|
+
logger.warn(`MCP server "${name2}" failed to start at boot`, err);
|
|
4484
|
+
});
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
3052
4487
|
let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
|
|
3053
4488
|
if (!opts.services?.session) {
|
|
3054
4489
|
sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
|
|
@@ -3076,15 +4511,22 @@ async function startWebUI(opts = {}) {
|
|
|
3076
4511
|
sessionId: session.id,
|
|
3077
4512
|
projectSlug: wpaths.projectSlug,
|
|
3078
4513
|
projectRoot,
|
|
3079
|
-
projectName:
|
|
4514
|
+
projectName: path9.basename(projectRoot),
|
|
3080
4515
|
workingDir,
|
|
4516
|
+
clientType: "webui",
|
|
3081
4517
|
pid: process.pid,
|
|
3082
4518
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3083
4519
|
});
|
|
3084
|
-
|
|
4520
|
+
const fleetNotifier = new FleetNotifier({
|
|
4521
|
+
baseDir: wpaths.globalRoot,
|
|
4522
|
+
projectRoot,
|
|
4523
|
+
selfPid: process.pid
|
|
4524
|
+
});
|
|
4525
|
+
statusTracker = new AgentStatusTracker({ events, registry, onUpdate: () => fleetNotifier.notify() });
|
|
3085
4526
|
statusTracker.start();
|
|
3086
4527
|
const stopTracking = async () => {
|
|
3087
4528
|
try {
|
|
4529
|
+
fleetNotifier.dispose();
|
|
3088
4530
|
await registry.markClosing();
|
|
3089
4531
|
statusTracker?.stop();
|
|
3090
4532
|
} catch {
|
|
@@ -3124,6 +4566,13 @@ async function startWebUI(opts = {}) {
|
|
|
3124
4566
|
supportsReasoning: resolvedModel.capabilities.reasoning
|
|
3125
4567
|
} : void 0;
|
|
3126
4568
|
const skillLoader = config.features.skills ? new DefaultSkillLoader2({ paths: wpaths }) : void 0;
|
|
4569
|
+
const skillInstaller = config.features.skills ? new SkillInstaller({
|
|
4570
|
+
manifestPath: path9.join(wstackGlobalRoot2(), "installed-skills.json"),
|
|
4571
|
+
projectSkillsDir: path9.join(projectRoot, ".wrongstack", "skills"),
|
|
4572
|
+
globalSkillsDir: path9.join(wstackGlobalRoot2(), "skills"),
|
|
4573
|
+
projectHash: projectHash(projectRoot),
|
|
4574
|
+
skillLoader
|
|
4575
|
+
}) : void 0;
|
|
3127
4576
|
const systemPromptBuilder = new DefaultSystemPromptBuilder2({
|
|
3128
4577
|
memoryStore,
|
|
3129
4578
|
skillLoader,
|
|
@@ -3193,7 +4642,7 @@ async function startWebUI(opts = {}) {
|
|
|
3193
4642
|
}
|
|
3194
4643
|
} else {
|
|
3195
4644
|
throw new Error(
|
|
3196
|
-
"No provider configured. Run `wrongstack
|
|
4645
|
+
"No provider configured. Run `wrongstack auth` to set up, or configure via the WebUI."
|
|
3197
4646
|
);
|
|
3198
4647
|
}
|
|
3199
4648
|
}
|
|
@@ -3281,7 +4730,7 @@ async function startWebUI(opts = {}) {
|
|
|
3281
4730
|
const write = async () => {
|
|
3282
4731
|
let raw;
|
|
3283
4732
|
try {
|
|
3284
|
-
raw = await
|
|
4733
|
+
raw = await fs9.readFile(globalConfigPath, "utf8");
|
|
3285
4734
|
} catch {
|
|
3286
4735
|
raw = "{}";
|
|
3287
4736
|
}
|
|
@@ -3590,7 +5039,7 @@ async function startWebUI(opts = {}) {
|
|
|
3590
5039
|
inputCost,
|
|
3591
5040
|
outputCost,
|
|
3592
5041
|
cacheReadCost,
|
|
3593
|
-
projectName:
|
|
5042
|
+
projectName: path9.basename(projectRoot) || projectRoot,
|
|
3594
5043
|
projectRoot,
|
|
3595
5044
|
cwd: workingDir,
|
|
3596
5045
|
mode: modeId,
|
|
@@ -3644,10 +5093,11 @@ async function startWebUI(opts = {}) {
|
|
|
3644
5093
|
const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
|
|
3645
5094
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
3646
5095
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
3647
|
-
|
|
5096
|
+
let connSeq = 0;
|
|
5097
|
+
function checkRateLimit(_ws, client) {
|
|
3648
5098
|
if (RATE_LIMIT_MESSAGES <= 0) return true;
|
|
3649
5099
|
const now = Date.now();
|
|
3650
|
-
const key = client.
|
|
5100
|
+
const key = client.connId;
|
|
3651
5101
|
const limit = rateLimits.get(key);
|
|
3652
5102
|
if (!limit || now > limit.resetAt) {
|
|
3653
5103
|
rateLimits.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
|
|
@@ -3663,7 +5113,12 @@ async function startWebUI(opts = {}) {
|
|
|
3663
5113
|
);
|
|
3664
5114
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
3665
5115
|
const handleConnection = (ws) => {
|
|
3666
|
-
const client = {
|
|
5116
|
+
const client = {
|
|
5117
|
+
ws,
|
|
5118
|
+
sessionId: session.id,
|
|
5119
|
+
connectedAt: Date.now(),
|
|
5120
|
+
connId: `c${++connSeq}`
|
|
5121
|
+
};
|
|
3667
5122
|
clients.set(ws, client);
|
|
3668
5123
|
void sessionStartPayload().then((payload) => {
|
|
3669
5124
|
send(ws, { type: "session.start", payload });
|
|
@@ -3693,7 +5148,7 @@ async function startWebUI(opts = {}) {
|
|
|
3693
5148
|
const rawObj = JSON.parse(data.toString());
|
|
3694
5149
|
if (typeof rawObj === "object" && rawObj !== null) {
|
|
3695
5150
|
const obj = rawObj;
|
|
3696
|
-
if ("__proto__"
|
|
5151
|
+
if (Object.hasOwn(obj, "__proto__") || Object.hasOwn(obj, "constructor") || Object.hasOwn(obj, "prototype")) {
|
|
3697
5152
|
send(ws, {
|
|
3698
5153
|
type: "error",
|
|
3699
5154
|
payload: { phase: "parse", message: "Invalid message object" }
|
|
@@ -3714,8 +5169,9 @@ async function startWebUI(opts = {}) {
|
|
|
3714
5169
|
}
|
|
3715
5170
|
});
|
|
3716
5171
|
ws.on("close", () => {
|
|
5172
|
+
const closing = clients.get(ws);
|
|
3717
5173
|
clients.delete(ws);
|
|
3718
|
-
rateLimits.delete(
|
|
5174
|
+
if (closing) rateLimits.delete(closing.connId);
|
|
3719
5175
|
if (pendingConfirms.size > 0) {
|
|
3720
5176
|
for (const [id, resolve5] of pendingConfirms) {
|
|
3721
5177
|
resolve5("no");
|
|
@@ -3741,11 +5197,27 @@ async function startWebUI(opts = {}) {
|
|
|
3741
5197
|
{ sampling: sessionLogging.sampling }
|
|
3742
5198
|
);
|
|
3743
5199
|
let eventsArmed = false;
|
|
5200
|
+
let disposeEvents = null;
|
|
5201
|
+
let fleetBroadcast = null;
|
|
3744
5202
|
const armOnce = (label) => {
|
|
3745
5203
|
if (eventsArmed) return;
|
|
3746
5204
|
eventsArmed = true;
|
|
3747
5205
|
console.log(`[WebUI] Backend ready (${label})`);
|
|
3748
|
-
setupEvents({
|
|
5206
|
+
disposeEvents = setupEvents({
|
|
5207
|
+
events,
|
|
5208
|
+
broadcast,
|
|
5209
|
+
clients,
|
|
5210
|
+
config,
|
|
5211
|
+
context,
|
|
5212
|
+
pendingConfirms,
|
|
5213
|
+
globalConfigPath,
|
|
5214
|
+
sessionBridge,
|
|
5215
|
+
wpaths,
|
|
5216
|
+
watcherMetrics,
|
|
5217
|
+
onFleetBroadcaster: (fn) => {
|
|
5218
|
+
fleetBroadcast = fn;
|
|
5219
|
+
}
|
|
5220
|
+
});
|
|
3749
5221
|
};
|
|
3750
5222
|
wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
|
|
3751
5223
|
wssPrimary.on("connection", handleConnection);
|
|
@@ -3782,33 +5254,33 @@ async function startWebUI(opts = {}) {
|
|
|
3782
5254
|
});
|
|
3783
5255
|
}
|
|
3784
5256
|
async function touchProjectEntry(root, workDir) {
|
|
3785
|
-
const resolved =
|
|
5257
|
+
const resolved = path9.resolve(root);
|
|
3786
5258
|
const manifest = await loadManifest(globalConfigPath);
|
|
3787
5259
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3788
|
-
const existing = manifest.projects.find((p) =>
|
|
5260
|
+
const existing = manifest.projects.find((p) => path9.resolve(p.root) === resolved);
|
|
3789
5261
|
if (existing) {
|
|
3790
5262
|
existing.lastSeen = now;
|
|
3791
|
-
if (workDir) existing.lastWorkingDir =
|
|
5263
|
+
if (workDir) existing.lastWorkingDir = path9.resolve(workDir);
|
|
3792
5264
|
} else {
|
|
3793
5265
|
manifest.projects.push({
|
|
3794
|
-
name:
|
|
5266
|
+
name: path9.basename(resolved),
|
|
3795
5267
|
root: resolved,
|
|
3796
5268
|
slug: generateProjectSlug(resolved),
|
|
3797
5269
|
createdAt: now,
|
|
3798
5270
|
lastSeen: now,
|
|
3799
|
-
lastWorkingDir: workDir ?
|
|
5271
|
+
lastWorkingDir: workDir ? path9.resolve(workDir) : void 0
|
|
3800
5272
|
});
|
|
3801
5273
|
}
|
|
3802
5274
|
await saveManifest(manifest, globalConfigPath);
|
|
3803
5275
|
await ensureProjectDataDir(generateProjectSlug(resolved), globalConfigPath);
|
|
3804
5276
|
}
|
|
3805
5277
|
function projectsJsonPath(globalConfigPath2) {
|
|
3806
|
-
const base =
|
|
3807
|
-
return
|
|
5278
|
+
const base = path9.dirname(globalConfigPath2);
|
|
5279
|
+
return path9.join(base, "projects.json");
|
|
3808
5280
|
}
|
|
3809
5281
|
async function loadManifest(globalConfigPath2) {
|
|
3810
5282
|
try {
|
|
3811
|
-
const raw = await
|
|
5283
|
+
const raw = await fs9.readFile(projectsJsonPath(globalConfigPath2), "utf8");
|
|
3812
5284
|
const parsed = JSON.parse(raw);
|
|
3813
5285
|
return { projects: parsed.projects ?? [] };
|
|
3814
5286
|
} catch {
|
|
@@ -3817,16 +5289,16 @@ async function startWebUI(opts = {}) {
|
|
|
3817
5289
|
}
|
|
3818
5290
|
async function saveManifest(manifest, globalConfigPath2) {
|
|
3819
5291
|
const file = projectsJsonPath(globalConfigPath2);
|
|
3820
|
-
await
|
|
3821
|
-
await
|
|
5292
|
+
await fs9.mkdir(path9.dirname(file), { recursive: true });
|
|
5293
|
+
await fs9.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
3822
5294
|
}
|
|
3823
5295
|
function generateProjectSlug(rootPath) {
|
|
3824
5296
|
return projectSlug(rootPath);
|
|
3825
5297
|
}
|
|
3826
5298
|
async function ensureProjectDataDir(slug, globalConfigPath2) {
|
|
3827
|
-
const base =
|
|
3828
|
-
const dir =
|
|
3829
|
-
await
|
|
5299
|
+
const base = path9.dirname(globalConfigPath2);
|
|
5300
|
+
const dir = path9.join(base, "projects", slug);
|
|
5301
|
+
await fs9.mkdir(dir, { recursive: true });
|
|
3830
5302
|
return dir;
|
|
3831
5303
|
}
|
|
3832
5304
|
async function handleMessage(ws, _client, msg) {
|
|
@@ -3936,7 +5408,7 @@ async function startWebUI(opts = {}) {
|
|
|
3936
5408
|
context.readFiles.clear();
|
|
3937
5409
|
context.fileMtimes.clear();
|
|
3938
5410
|
tokenCounter.reset();
|
|
3939
|
-
|
|
5411
|
+
sendResult2(ws, true, "Context cleared");
|
|
3940
5412
|
broadcast(clients, {
|
|
3941
5413
|
type: "session.start",
|
|
3942
5414
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
@@ -3973,13 +5445,13 @@ async function startWebUI(opts = {}) {
|
|
|
3973
5445
|
repaired: report.repaired
|
|
3974
5446
|
}
|
|
3975
5447
|
});
|
|
3976
|
-
|
|
5448
|
+
sendResult2(
|
|
3977
5449
|
ws,
|
|
3978
5450
|
true,
|
|
3979
5451
|
`Compacted: ${report.before} \u2192 ${report.after} tokens (saved ~${Math.max(0, report.before - report.after)})`
|
|
3980
5452
|
);
|
|
3981
5453
|
} catch (err) {
|
|
3982
|
-
|
|
5454
|
+
sendResult2(ws, false, errMessage(err));
|
|
3983
5455
|
}
|
|
3984
5456
|
break;
|
|
3985
5457
|
}
|
|
@@ -3998,7 +5470,7 @@ async function startWebUI(opts = {}) {
|
|
|
3998
5470
|
};
|
|
3999
5471
|
broadcast(clients, { type: "context.repaired", payload });
|
|
4000
5472
|
const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
|
|
4001
|
-
|
|
5473
|
+
sendResult2(
|
|
4002
5474
|
ws,
|
|
4003
5475
|
true,
|
|
4004
5476
|
removed > 0 ? `Context repaired: removed ${removed} orphan protocol item(s)` : "Context repair found no orphan protocol blocks"
|
|
@@ -4032,14 +5504,14 @@ async function startWebUI(opts = {}) {
|
|
|
4032
5504
|
);
|
|
4033
5505
|
const custom = customModes.find((m) => m.id === id);
|
|
4034
5506
|
if (!custom) {
|
|
4035
|
-
|
|
5507
|
+
sendResult2(ws, false, `Unknown context mode "${id}"`);
|
|
4036
5508
|
break;
|
|
4037
5509
|
}
|
|
4038
5510
|
policy = custom;
|
|
4039
5511
|
}
|
|
4040
5512
|
context.meta["contextWindowMode"] = policy.id;
|
|
4041
5513
|
context.meta["contextWindowPolicy"] = policy;
|
|
4042
|
-
|
|
5514
|
+
sendResult2(ws, true, `Context mode switched to ${policy.id}`);
|
|
4043
5515
|
broadcast(clients, {
|
|
4044
5516
|
type: "context.mode.changed",
|
|
4045
5517
|
payload: { id: policy.id, name: policy.name, policy }
|
|
@@ -4059,7 +5531,7 @@ async function startWebUI(opts = {}) {
|
|
|
4059
5531
|
aggressiveOn: "soft",
|
|
4060
5532
|
targetLoad: 0.65
|
|
4061
5533
|
});
|
|
4062
|
-
|
|
5534
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${payload.id}" created`);
|
|
4063
5535
|
break;
|
|
4064
5536
|
}
|
|
4065
5537
|
case "context.mode.update": {
|
|
@@ -4075,7 +5547,7 @@ async function startWebUI(opts = {}) {
|
|
|
4075
5547
|
preserveK: payload.preserveK,
|
|
4076
5548
|
eliseThreshold: payload.eliseThreshold
|
|
4077
5549
|
});
|
|
4078
|
-
|
|
5550
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${payload.id}" updated`);
|
|
4079
5551
|
break;
|
|
4080
5552
|
}
|
|
4081
5553
|
case "context.mode.delete": {
|
|
@@ -4085,7 +5557,7 @@ async function startWebUI(opts = {}) {
|
|
|
4085
5557
|
context.meta["contextWindowPolicy"] = resolveContextWindowPolicy({}, DEFAULT_CONTEXT_WINDOW_MODE_ID);
|
|
4086
5558
|
}
|
|
4087
5559
|
const result = customModeStore.remove(id);
|
|
4088
|
-
|
|
5560
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${id}" deleted`);
|
|
4089
5561
|
break;
|
|
4090
5562
|
}
|
|
4091
5563
|
case "providers.list": {
|
|
@@ -4132,27 +5604,17 @@ async function startWebUI(opts = {}) {
|
|
|
4132
5604
|
}
|
|
4133
5605
|
case "provider.models": {
|
|
4134
5606
|
const providerId = msg.payload.providerId;
|
|
4135
|
-
const
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
inputCost: m.cost?.input,
|
|
4147
|
-
outputCost: m.cost?.output,
|
|
4148
|
-
capabilities: [
|
|
4149
|
-
...m.tool_call ? ["tools"] : [],
|
|
4150
|
-
...m.reasoning ? ["reasoning"] : []
|
|
4151
|
-
]
|
|
4152
|
-
}))
|
|
4153
|
-
}
|
|
4154
|
-
});
|
|
4155
|
-
}
|
|
5607
|
+
const saved = await providerHandlers.loadConfigProviders();
|
|
5608
|
+
const cfg = saved[providerId];
|
|
5609
|
+
const catalogId = cfg?.type && cfg.type !== providerId ? cfg.type : providerId;
|
|
5610
|
+
const provider2 = await modelsRegistry.getProvider(catalogId);
|
|
5611
|
+
send(ws, {
|
|
5612
|
+
type: "provider.models",
|
|
5613
|
+
payload: {
|
|
5614
|
+
provider: providerId,
|
|
5615
|
+
models: resolveProviderModelList(cfg?.models, provider2)
|
|
5616
|
+
}
|
|
5617
|
+
});
|
|
4156
5618
|
break;
|
|
4157
5619
|
}
|
|
4158
5620
|
case "model.switch": {
|
|
@@ -4166,14 +5628,15 @@ async function startWebUI(opts = {}) {
|
|
|
4166
5628
|
context.provider = newProv;
|
|
4167
5629
|
updateAutoCompactionMaxContext?.(newProv);
|
|
4168
5630
|
try {
|
|
4169
|
-
|
|
4170
|
-
const raw = await
|
|
5631
|
+
const next = configWriteLock.then(async () => {
|
|
5632
|
+
const raw = await fs9.readFile(globalConfigPath, "utf8");
|
|
4171
5633
|
const parsed = JSON.parse(raw);
|
|
4172
5634
|
parsed.provider = newProvider;
|
|
4173
5635
|
parsed.model = newModel;
|
|
4174
5636
|
await atomicWrite5(globalConfigPath, JSON.stringify(parsed, null, 2));
|
|
4175
5637
|
});
|
|
4176
|
-
|
|
5638
|
+
configWriteLock = next.then(() => void 0, () => void 0);
|
|
5639
|
+
await next;
|
|
4177
5640
|
} catch (err) {
|
|
4178
5641
|
console.warn(JSON.stringify({
|
|
4179
5642
|
level: "warn",
|
|
@@ -4326,13 +5789,13 @@ async function startWebUI(opts = {}) {
|
|
|
4326
5789
|
const { id } = msg.payload;
|
|
4327
5790
|
try {
|
|
4328
5791
|
if (id === session.id) {
|
|
4329
|
-
|
|
5792
|
+
sendResult2(ws, false, "Cannot delete the active session");
|
|
4330
5793
|
break;
|
|
4331
5794
|
}
|
|
4332
5795
|
await sessionStore.delete(id);
|
|
4333
|
-
|
|
5796
|
+
sendResult2(ws, true, `Session ${id} deleted`);
|
|
4334
5797
|
} catch (err) {
|
|
4335
|
-
|
|
5798
|
+
sendResult2(ws, false, errMessage(err));
|
|
4336
5799
|
}
|
|
4337
5800
|
break;
|
|
4338
5801
|
}
|
|
@@ -4340,7 +5803,7 @@ async function startWebUI(opts = {}) {
|
|
|
4340
5803
|
const { id } = msg.payload;
|
|
4341
5804
|
try {
|
|
4342
5805
|
if (id === session.id) {
|
|
4343
|
-
|
|
5806
|
+
sendResult2(ws, false, "Session is already active");
|
|
4344
5807
|
break;
|
|
4345
5808
|
}
|
|
4346
5809
|
const resumed = await sessionStore.resume(id);
|
|
@@ -4370,14 +5833,14 @@ async function startWebUI(opts = {}) {
|
|
|
4370
5833
|
replayUsage: resumed.data.usage
|
|
4371
5834
|
}
|
|
4372
5835
|
});
|
|
4373
|
-
|
|
5836
|
+
sendResult2(ws, true, `Resumed session ${id}`);
|
|
4374
5837
|
} catch (err) {
|
|
4375
|
-
|
|
5838
|
+
sendResult2(ws, false, errMessage(err));
|
|
4376
5839
|
}
|
|
4377
5840
|
break;
|
|
4378
5841
|
}
|
|
4379
5842
|
case "session.save": {
|
|
4380
|
-
|
|
5843
|
+
sendResult2(ws, true, `Session ${session.id} is auto-saved`);
|
|
4381
5844
|
break;
|
|
4382
5845
|
}
|
|
4383
5846
|
case "tools.list": {
|
|
@@ -4400,6 +5863,28 @@ async function startWebUI(opts = {}) {
|
|
|
4400
5863
|
return handleMemoryRemember(ws, msg, memoryStore);
|
|
4401
5864
|
case "memory.forget":
|
|
4402
5865
|
return handleMemoryForget(ws, msg, memoryStore);
|
|
5866
|
+
// ── MCP operations — delegated to shared handlers (mcp-handlers.ts),
|
|
5867
|
+
// backed by the live MCPRegistry constructed above. ──
|
|
5868
|
+
case "mcp.list":
|
|
5869
|
+
return handleMcpList(ws, msg, globalConfigPath, mcpRegistry);
|
|
5870
|
+
case "mcp.add":
|
|
5871
|
+
return handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry);
|
|
5872
|
+
case "mcp.remove":
|
|
5873
|
+
return handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry);
|
|
5874
|
+
case "mcp.update":
|
|
5875
|
+
return handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry);
|
|
5876
|
+
case "mcp.wake":
|
|
5877
|
+
return handleMcpWake(ws, msg, globalConfigPath, mcpRegistry);
|
|
5878
|
+
case "mcp.sleep":
|
|
5879
|
+
return handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry);
|
|
5880
|
+
case "mcp.discover":
|
|
5881
|
+
return handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry);
|
|
5882
|
+
case "mcp.enable":
|
|
5883
|
+
return handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry);
|
|
5884
|
+
case "mcp.disable":
|
|
5885
|
+
return handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry);
|
|
5886
|
+
case "mcp.restart":
|
|
5887
|
+
return handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry);
|
|
4403
5888
|
case "skills.list": {
|
|
4404
5889
|
if (!skillLoader) {
|
|
4405
5890
|
send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
|
|
@@ -4409,6 +5894,18 @@ async function startWebUI(opts = {}) {
|
|
|
4409
5894
|
const manifests = await skillLoader.list();
|
|
4410
5895
|
const entries = await skillLoader.listEntries();
|
|
4411
5896
|
const byName = new Map(entries.map((e) => [e.name, e]));
|
|
5897
|
+
const sourceUrlsByName = /* @__PURE__ */ new Map();
|
|
5898
|
+
const refsByName = /* @__PURE__ */ new Map();
|
|
5899
|
+
if (skillInstaller) {
|
|
5900
|
+
try {
|
|
5901
|
+
const installed = await skillInstaller.listInstalled();
|
|
5902
|
+
for (const entry of installed) {
|
|
5903
|
+
sourceUrlsByName.set(entry.name, entry.source);
|
|
5904
|
+
refsByName.set(entry.name, entry.ref);
|
|
5905
|
+
}
|
|
5906
|
+
} catch {
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
4412
5909
|
send(ws, {
|
|
4413
5910
|
type: "skills.list",
|
|
4414
5911
|
payload: {
|
|
@@ -4418,6 +5915,8 @@ async function startWebUI(opts = {}) {
|
|
|
4418
5915
|
description: m.description,
|
|
4419
5916
|
version: m.version ?? "",
|
|
4420
5917
|
source: m.source,
|
|
5918
|
+
sourceUrl: sourceUrlsByName.get(m.name) ?? "",
|
|
5919
|
+
ref: refsByName.get(m.name) ?? "",
|
|
4421
5920
|
path: m.path,
|
|
4422
5921
|
trigger: byName.get(m.name)?.trigger ?? "",
|
|
4423
5922
|
scope: byName.get(m.name)?.scope ?? []
|
|
@@ -4436,6 +5935,261 @@ async function startWebUI(opts = {}) {
|
|
|
4436
5935
|
}
|
|
4437
5936
|
break;
|
|
4438
5937
|
}
|
|
5938
|
+
case "skills.content": {
|
|
5939
|
+
if (!skillLoader) {
|
|
5940
|
+
send(ws, { type: "skills.content", payload: { name: "", body: "", path: "", source: "", relatedFiles: [], references: [], error: "Skills not enabled" } });
|
|
5941
|
+
break;
|
|
5942
|
+
}
|
|
5943
|
+
const contentPayload = msg.payload;
|
|
5944
|
+
if (!contentPayload?.name) {
|
|
5945
|
+
send(ws, { type: "skills.content", payload: { name: "", body: "", path: "", source: "", relatedFiles: [], references: [], error: "Skill name is required" } });
|
|
5946
|
+
break;
|
|
5947
|
+
}
|
|
5948
|
+
try {
|
|
5949
|
+
const { name: name2, source } = contentPayload;
|
|
5950
|
+
const entries = await skillLoader.listEntries();
|
|
5951
|
+
const entry = entries.find((e) => e.name.toLowerCase() === name2.toLowerCase());
|
|
5952
|
+
if (!entry) {
|
|
5953
|
+
send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
|
|
5954
|
+
break;
|
|
5955
|
+
}
|
|
5956
|
+
const body = await skillLoader.readBody(name2);
|
|
5957
|
+
const skillDir = path9.dirname(entry.path);
|
|
5958
|
+
let relatedFiles = [];
|
|
5959
|
+
try {
|
|
5960
|
+
const files = await fs9.readdir(skillDir);
|
|
5961
|
+
relatedFiles = files.filter((f) => f !== path9.basename(entry.path)).map((f) => path9.join(skillDir, f));
|
|
5962
|
+
} catch {
|
|
5963
|
+
}
|
|
5964
|
+
const refs = [];
|
|
5965
|
+
for (const e of entries) {
|
|
5966
|
+
if (e.name.toLowerCase() === name2.toLowerCase()) continue;
|
|
5967
|
+
try {
|
|
5968
|
+
const content = await skillLoader.readBody(e.name);
|
|
5969
|
+
if (content.toLowerCase().includes(name2.toLowerCase())) {
|
|
5970
|
+
refs.push(e.name);
|
|
5971
|
+
}
|
|
5972
|
+
} catch {
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
5975
|
+
send(ws, { type: "skills.content", payload: { name: name2, body, path: entry.path, source, relatedFiles, references: refs } });
|
|
5976
|
+
} catch (err) {
|
|
5977
|
+
send(ws, { type: "skills.content", payload: { name: contentPayload.name, body: "", path: "", source: contentPayload.source, relatedFiles: [], references: [], error: errMessage(err) } });
|
|
5978
|
+
}
|
|
5979
|
+
break;
|
|
5980
|
+
}
|
|
5981
|
+
case "skills.install": {
|
|
5982
|
+
if (!skillInstaller) {
|
|
5983
|
+
send(ws, { type: "skills.installed", payload: { success: false, error: "Skills not enabled" } });
|
|
5984
|
+
break;
|
|
5985
|
+
}
|
|
5986
|
+
const installPayload = msg.payload;
|
|
5987
|
+
if (!installPayload?.ref?.trim()) {
|
|
5988
|
+
send(ws, { type: "skills.installed", payload: { success: false, error: "Skill reference is required (e.g. owner/repo or https://github.com/owner/repo)" } });
|
|
5989
|
+
break;
|
|
5990
|
+
}
|
|
5991
|
+
try {
|
|
5992
|
+
const results = await skillInstaller.install(installPayload.ref.trim(), { global: installPayload.global });
|
|
5993
|
+
send(ws, {
|
|
5994
|
+
type: "skills.installed",
|
|
5995
|
+
payload: {
|
|
5996
|
+
success: true,
|
|
5997
|
+
results,
|
|
5998
|
+
error: null
|
|
5999
|
+
}
|
|
6000
|
+
});
|
|
6001
|
+
} catch (err) {
|
|
6002
|
+
send(ws, {
|
|
6003
|
+
type: "skills.installed",
|
|
6004
|
+
payload: {
|
|
6005
|
+
success: false,
|
|
6006
|
+
error: errMessage(err)
|
|
6007
|
+
}
|
|
6008
|
+
});
|
|
6009
|
+
}
|
|
6010
|
+
break;
|
|
6011
|
+
}
|
|
6012
|
+
case "skills.uninstall": {
|
|
6013
|
+
if (!skillInstaller) {
|
|
6014
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: "Skills not enabled" } });
|
|
6015
|
+
break;
|
|
6016
|
+
}
|
|
6017
|
+
const uninstallPayload = msg.payload;
|
|
6018
|
+
if (!uninstallPayload?.name?.trim()) {
|
|
6019
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: "Skill name is required" } });
|
|
6020
|
+
break;
|
|
6021
|
+
}
|
|
6022
|
+
try {
|
|
6023
|
+
await skillInstaller.uninstall(uninstallPayload.name.trim(), { global: uninstallPayload.global });
|
|
6024
|
+
send(ws, { type: "skills.uninstalled", payload: { success: true, error: null } });
|
|
6025
|
+
} catch (err) {
|
|
6026
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: errMessage(err) } });
|
|
6027
|
+
}
|
|
6028
|
+
break;
|
|
6029
|
+
}
|
|
6030
|
+
case "skills.update": {
|
|
6031
|
+
if (!skillInstaller) {
|
|
6032
|
+
send(ws, { type: "skills.updated", payload: { success: false, error: "Skills not enabled" } });
|
|
6033
|
+
break;
|
|
6034
|
+
}
|
|
6035
|
+
const updatePayload = msg.payload;
|
|
6036
|
+
try {
|
|
6037
|
+
const result = await skillInstaller.update(updatePayload?.name, { global: updatePayload?.global });
|
|
6038
|
+
send(ws, {
|
|
6039
|
+
type: "skills.updated",
|
|
6040
|
+
payload: {
|
|
6041
|
+
success: true,
|
|
6042
|
+
error: null,
|
|
6043
|
+
updated: result.updated,
|
|
6044
|
+
unchanged: result.unchanged,
|
|
6045
|
+
errors: result.errors
|
|
6046
|
+
}
|
|
6047
|
+
});
|
|
6048
|
+
} catch (err) {
|
|
6049
|
+
send(ws, { type: "skills.updated", payload: { success: false, error: errMessage(err) } });
|
|
6050
|
+
}
|
|
6051
|
+
break;
|
|
6052
|
+
}
|
|
6053
|
+
case "skills.create": {
|
|
6054
|
+
const createPayload = msg.payload;
|
|
6055
|
+
if (!createPayload?.name?.trim()) {
|
|
6056
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Skill name is required" } });
|
|
6057
|
+
break;
|
|
6058
|
+
}
|
|
6059
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(createPayload.name.trim())) {
|
|
6060
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Skill name must be kebab-case (e.g. my-new-skill)" } });
|
|
6061
|
+
break;
|
|
6062
|
+
}
|
|
6063
|
+
if (!createPayload?.description?.trim()) {
|
|
6064
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Description/trigger is required" } });
|
|
6065
|
+
break;
|
|
6066
|
+
}
|
|
6067
|
+
try {
|
|
6068
|
+
const targetDir = createPayload.scope === "global" ? path9.join(wstackGlobalRoot2(), "skills", createPayload.name.trim()) : path9.join(projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
6069
|
+
try {
|
|
6070
|
+
await fs9.access(targetDir);
|
|
6071
|
+
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
6072
|
+
break;
|
|
6073
|
+
} catch {
|
|
6074
|
+
}
|
|
6075
|
+
await fs9.mkdir(targetDir, { recursive: true });
|
|
6076
|
+
const lines = createPayload.description.trim().split("\n");
|
|
6077
|
+
const firstLine = lines[0].trim();
|
|
6078
|
+
const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
|
|
6079
|
+
const descriptionText = firstLine + (bodyLines.length > 0 ? `
|
|
6080
|
+
${bodyLines.join("\n")}` : "");
|
|
6081
|
+
const trigger = bodyLines.find((l) => l.toLowerCase().startsWith("triggers:")) ?? "";
|
|
6082
|
+
const skillContent = [
|
|
6083
|
+
"---",
|
|
6084
|
+
`name: ${createPayload.name.trim()}`,
|
|
6085
|
+
"description: |",
|
|
6086
|
+
` ${descriptionText.replace(/\n/g, "\n ")}`,
|
|
6087
|
+
`version: 1.0.0`,
|
|
6088
|
+
"---",
|
|
6089
|
+
"",
|
|
6090
|
+
`# ${createPayload.name.trim().split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}`,
|
|
6091
|
+
"",
|
|
6092
|
+
"## Overview",
|
|
6093
|
+
"",
|
|
6094
|
+
firstLine,
|
|
6095
|
+
"",
|
|
6096
|
+
...bodyLines.length > 0 ? bodyLines.filter((l) => !l.toLowerCase().startsWith("triggers:")) : [],
|
|
6097
|
+
"",
|
|
6098
|
+
"## Rules",
|
|
6099
|
+
"- TODO: add your first rule",
|
|
6100
|
+
"",
|
|
6101
|
+
"## Patterns",
|
|
6102
|
+
"### Do",
|
|
6103
|
+
"```ts",
|
|
6104
|
+
"// TODO: add a good example",
|
|
6105
|
+
"```",
|
|
6106
|
+
"",
|
|
6107
|
+
"### Don't",
|
|
6108
|
+
"```ts",
|
|
6109
|
+
"// TODO: add a bad example",
|
|
6110
|
+
"```",
|
|
6111
|
+
"",
|
|
6112
|
+
"## Workflow",
|
|
6113
|
+
"1. TODO: describe step one",
|
|
6114
|
+
"2. TODO: describe step two",
|
|
6115
|
+
"",
|
|
6116
|
+
trigger ? `
|
|
6117
|
+
${trigger}
|
|
6118
|
+
` : "",
|
|
6119
|
+
"## Skills in scope",
|
|
6120
|
+
"- `bug-hunter` \u2014 for systematic bug detection patterns",
|
|
6121
|
+
"- `output-standards` \u2014 for standardized `<next_steps>` formatting"
|
|
6122
|
+
].join("\n");
|
|
6123
|
+
await fs9.writeFile(path9.join(targetDir, "SKILL.md"), skillContent, "utf-8");
|
|
6124
|
+
send(ws, {
|
|
6125
|
+
type: "skills.created",
|
|
6126
|
+
payload: {
|
|
6127
|
+
success: true,
|
|
6128
|
+
error: null,
|
|
6129
|
+
skill: { name: createPayload.name.trim(), path: path9.join(targetDir, "SKILL.md"), scope: createPayload.scope }
|
|
6130
|
+
}
|
|
6131
|
+
});
|
|
6132
|
+
} catch (err) {
|
|
6133
|
+
send(ws, { type: "skills.created", payload: { success: false, error: errMessage(err) } });
|
|
6134
|
+
}
|
|
6135
|
+
break;
|
|
6136
|
+
}
|
|
6137
|
+
case "skills.edit": {
|
|
6138
|
+
if (!skillLoader) {
|
|
6139
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skills not enabled" } });
|
|
6140
|
+
break;
|
|
6141
|
+
}
|
|
6142
|
+
const editPayload = msg.payload;
|
|
6143
|
+
if (!editPayload?.name?.trim()) {
|
|
6144
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skill name is required" } });
|
|
6145
|
+
break;
|
|
6146
|
+
}
|
|
6147
|
+
if (!editPayload?.body) {
|
|
6148
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skill body is required" } });
|
|
6149
|
+
break;
|
|
6150
|
+
}
|
|
6151
|
+
try {
|
|
6152
|
+
const entries = await skillLoader.listEntries();
|
|
6153
|
+
const entry = entries.find((e) => e.name.toLowerCase() === editPayload.name.toLowerCase());
|
|
6154
|
+
if (!entry) {
|
|
6155
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: `Skill "${editPayload.name}" not found` } });
|
|
6156
|
+
break;
|
|
6157
|
+
}
|
|
6158
|
+
if (entry.scope.includes("bundled")) {
|
|
6159
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Bundled skills cannot be edited" } });
|
|
6160
|
+
break;
|
|
6161
|
+
}
|
|
6162
|
+
await fs9.writeFile(entry.path, editPayload.body, "utf-8");
|
|
6163
|
+
send(ws, { type: "skills.edited", payload: { success: true, error: null } });
|
|
6164
|
+
} catch (err) {
|
|
6165
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: errMessage(err) } });
|
|
6166
|
+
}
|
|
6167
|
+
break;
|
|
6168
|
+
}
|
|
6169
|
+
case "skills.export": {
|
|
6170
|
+
if (!skillLoader) {
|
|
6171
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: "Skills not enabled" } });
|
|
6172
|
+
break;
|
|
6173
|
+
}
|
|
6174
|
+
try {
|
|
6175
|
+
const entries = await skillLoader.listEntries();
|
|
6176
|
+
const zip = new JSZip2();
|
|
6177
|
+
for (const entry of entries) {
|
|
6178
|
+
try {
|
|
6179
|
+
const body = await skillLoader.readBody(entry.name);
|
|
6180
|
+
const safeName = entry.name.replace(/\//g, "_");
|
|
6181
|
+
zip.file(`${safeName}/SKILL.md`, body);
|
|
6182
|
+
} catch {
|
|
6183
|
+
}
|
|
6184
|
+
}
|
|
6185
|
+
const zipBuffer = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
|
6186
|
+
const zipBase64 = zipBuffer.toString("base64");
|
|
6187
|
+
send(ws, { type: "skills.exported", payload: { zipBase64, skillCount: entries.length, error: void 0 } });
|
|
6188
|
+
} catch (err) {
|
|
6189
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: errMessage(err) } });
|
|
6190
|
+
}
|
|
6191
|
+
break;
|
|
6192
|
+
}
|
|
4439
6193
|
case "diag.get": {
|
|
4440
6194
|
const usage = tokenCounter.total();
|
|
4441
6195
|
send(ws, {
|
|
@@ -4463,194 +6217,84 @@ async function startWebUI(opts = {}) {
|
|
|
4463
6217
|
break;
|
|
4464
6218
|
}
|
|
4465
6219
|
case "todos.get": {
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
6220
|
+
const ctx = {
|
|
6221
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6222
|
+
send: (w, m) => send(w, m),
|
|
6223
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6224
|
+
};
|
|
6225
|
+
handleTodosGet(ctx, ws);
|
|
4470
6226
|
break;
|
|
4471
6227
|
}
|
|
4472
6228
|
case "todos.clear": {
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
6229
|
+
const ctx = {
|
|
6230
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6231
|
+
send: (w, m) => send(w, m),
|
|
6232
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6233
|
+
};
|
|
6234
|
+
handleTodosClear(ctx, ws);
|
|
4476
6235
|
break;
|
|
4477
6236
|
}
|
|
4478
6237
|
case "todos.remove": {
|
|
4479
|
-
const
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
}
|
|
4484
|
-
|
|
4485
|
-
let targetIdx = -1;
|
|
4486
|
-
if (typeof id === "string") {
|
|
4487
|
-
targetIdx = context.todos.findIndex((t) => t.id === id);
|
|
4488
|
-
} else if (typeof index === "number" && index > 0) {
|
|
4489
|
-
targetIdx = index - 1;
|
|
4490
|
-
}
|
|
4491
|
-
if (targetIdx < 0 || !context.todos[targetIdx]) {
|
|
4492
|
-
sendResult(ws, false, "Todo not found");
|
|
4493
|
-
break;
|
|
4494
|
-
}
|
|
4495
|
-
const removed = expectDefined2(context.todos[targetIdx]);
|
|
4496
|
-
const next = [...context.todos.slice(0, targetIdx), ...context.todos.slice(targetIdx + 1)];
|
|
4497
|
-
context.state.replaceTodos(next);
|
|
4498
|
-
sendResult(ws, true, `Removed: ${removed.content}`);
|
|
4499
|
-
broadcast(clients, { type: "todos.updated", payload: { todos: next } });
|
|
6238
|
+
const ctx = {
|
|
6239
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6240
|
+
send: (w, m) => send(w, m),
|
|
6241
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6242
|
+
};
|
|
6243
|
+
handleTodosRemove(ctx, ws, msg.payload);
|
|
4500
6244
|
break;
|
|
4501
6245
|
}
|
|
4502
6246
|
case "tasks.get": {
|
|
4503
|
-
const
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
type: "tasks.updated",
|
|
4510
|
-
payload: { tasks: file?.tasks ?? [] }
|
|
4511
|
-
});
|
|
4512
|
-
} catch {
|
|
4513
|
-
send(ws, { type: "tasks.updated", payload: { tasks: [] } });
|
|
4514
|
-
}
|
|
4515
|
-
} else {
|
|
4516
|
-
send(ws, { type: "tasks.updated", payload: { tasks: [], error: "Task storage not configured." } });
|
|
4517
|
-
}
|
|
6247
|
+
const ctx = {
|
|
6248
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6249
|
+
send: (w, m) => send(w, m),
|
|
6250
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6251
|
+
};
|
|
6252
|
+
await handleTasksGet(ctx, ws);
|
|
4518
6253
|
break;
|
|
4519
6254
|
}
|
|
4520
6255
|
case "plan.get": {
|
|
4521
|
-
const
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
type: "plan.updated",
|
|
4528
|
-
payload: {
|
|
4529
|
-
plan: plan ?? {
|
|
4530
|
-
version: 1,
|
|
4531
|
-
sessionId: session.id,
|
|
4532
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4533
|
-
items: []
|
|
4534
|
-
}
|
|
4535
|
-
}
|
|
4536
|
-
});
|
|
4537
|
-
} catch {
|
|
4538
|
-
send(ws, {
|
|
4539
|
-
type: "plan.updated",
|
|
4540
|
-
payload: {
|
|
4541
|
-
plan: {
|
|
4542
|
-
version: 1,
|
|
4543
|
-
sessionId: session.id,
|
|
4544
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4545
|
-
items: []
|
|
4546
|
-
}
|
|
4547
|
-
}
|
|
4548
|
-
});
|
|
4549
|
-
}
|
|
4550
|
-
} else {
|
|
4551
|
-
send(ws, {
|
|
4552
|
-
type: "plan.updated",
|
|
4553
|
-
payload: { plan: null, error: "Plan storage is not configured for this session." }
|
|
4554
|
-
});
|
|
4555
|
-
}
|
|
6256
|
+
const ctx = {
|
|
6257
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6258
|
+
send: (w, m) => send(w, m),
|
|
6259
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6260
|
+
};
|
|
6261
|
+
await handlePlanGet(ctx, ws);
|
|
4556
6262
|
break;
|
|
4557
6263
|
}
|
|
4558
6264
|
case "plan.template_use": {
|
|
4559
|
-
const
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
try {
|
|
4566
|
-
const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem } = await import("@wrongstack/core");
|
|
4567
|
-
const tpl = getPlanTemplate(template);
|
|
4568
|
-
if (!tpl) {
|
|
4569
|
-
sendResult(ws, false, `Unknown template "${template}".`);
|
|
4570
|
-
break;
|
|
4571
|
-
}
|
|
4572
|
-
let plan = await loadPlan(planPath) ?? emptyPlan(session.id);
|
|
4573
|
-
for (const item of tpl.items) {
|
|
4574
|
-
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
4575
|
-
}
|
|
4576
|
-
await savePlan(planPath, plan);
|
|
4577
|
-
sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
|
|
4578
|
-
broadcast(clients, {
|
|
4579
|
-
type: "plan.updated",
|
|
4580
|
-
payload: { plan }
|
|
4581
|
-
});
|
|
4582
|
-
} catch (err) {
|
|
4583
|
-
sendResult(ws, false, errMessage(err));
|
|
4584
|
-
}
|
|
6265
|
+
const ctx = {
|
|
6266
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6267
|
+
send: (w, m) => send(w, m),
|
|
6268
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6269
|
+
};
|
|
6270
|
+
await handlePlanTemplateUse(ctx, ws, msg.payload.template);
|
|
4585
6271
|
break;
|
|
4586
6272
|
}
|
|
4587
6273
|
case "todo.update": {
|
|
4588
|
-
const
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
break;
|
|
4593
|
-
}
|
|
4594
|
-
const next = [...context.todos];
|
|
4595
|
-
const existing = expectDefined2(next[idx]);
|
|
4596
|
-
next[idx] = {
|
|
4597
|
-
...existing,
|
|
4598
|
-
status: payload.status ?? existing.status,
|
|
4599
|
-
activeForm: payload.activeForm !== void 0 ? payload.activeForm : existing.activeForm
|
|
6274
|
+
const ctx = {
|
|
6275
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6276
|
+
send: (w, m) => send(w, m),
|
|
6277
|
+
broadcast: (m) => broadcast(clients, m)
|
|
4600
6278
|
};
|
|
4601
|
-
|
|
4602
|
-
sendResult(ws, true, `Todo "${existing.content}" updated`);
|
|
4603
|
-
broadcast(clients, { type: "todos.updated", payload: { todos: next } });
|
|
6279
|
+
handleTodoUpdate(ctx, ws, msg.payload);
|
|
4604
6280
|
break;
|
|
4605
6281
|
}
|
|
4606
6282
|
case "task.update": {
|
|
4607
|
-
const
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
try {
|
|
4614
|
-
const { mutateTasks } = await import("@wrongstack/core");
|
|
4615
|
-
const file = await mutateTasks(taskPath, session.id, async (f) => {
|
|
4616
|
-
const task = f.tasks.find((t) => t.id === payload.id);
|
|
4617
|
-
if (!task) return f;
|
|
4618
|
-
task.status = payload.status;
|
|
4619
|
-
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4620
|
-
return f;
|
|
4621
|
-
});
|
|
4622
|
-
sendResult(ws, true, `Task status updated to "${payload.status}".`);
|
|
4623
|
-
broadcast(clients, { type: "tasks.updated", payload: { tasks: file.tasks } });
|
|
4624
|
-
} catch (err) {
|
|
4625
|
-
sendResult(ws, false, errMessage(err));
|
|
4626
|
-
}
|
|
6283
|
+
const ctx = {
|
|
6284
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6285
|
+
send: (w, m) => send(w, m),
|
|
6286
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6287
|
+
};
|
|
6288
|
+
await handleTaskUpdate(ctx, ws, msg.payload);
|
|
4627
6289
|
break;
|
|
4628
6290
|
}
|
|
4629
6291
|
case "plan.item.update": {
|
|
4630
|
-
const
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
try {
|
|
4637
|
-
const { mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
|
|
4638
|
-
let changed = false;
|
|
4639
|
-
const plan = await mutatePlan(planPath, session.id, async (p) => {
|
|
4640
|
-
const before = p.updatedAt;
|
|
4641
|
-
const updated = setPlanItemStatus(p, payload.target, payload.status);
|
|
4642
|
-
changed = updated.updatedAt !== before;
|
|
4643
|
-
return updated;
|
|
4644
|
-
});
|
|
4645
|
-
if (!changed) {
|
|
4646
|
-
sendResult(ws, false, `No plan item matched "${payload.target}".`);
|
|
4647
|
-
break;
|
|
4648
|
-
}
|
|
4649
|
-
sendResult(ws, true, `Plan item status updated to "${payload.status}".`);
|
|
4650
|
-
broadcast(clients, { type: "plan.updated", payload: { plan } });
|
|
4651
|
-
} catch (err) {
|
|
4652
|
-
sendResult(ws, false, errMessage(err));
|
|
4653
|
-
}
|
|
6292
|
+
const ctx = {
|
|
6293
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6294
|
+
send: (w, m) => send(w, m),
|
|
6295
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6296
|
+
};
|
|
6297
|
+
await handlePlanItemUpdate(ctx, ws, msg.payload);
|
|
4654
6298
|
break;
|
|
4655
6299
|
}
|
|
4656
6300
|
// ── File operations — delegated to shared handlers (file-handlers.ts) ──
|
|
@@ -4720,13 +6364,13 @@ async function startWebUI(opts = {}) {
|
|
|
4720
6364
|
provider: config.provider,
|
|
4721
6365
|
model: config.model
|
|
4722
6366
|
});
|
|
4723
|
-
|
|
6367
|
+
sendResult2(ws, true, `Switched to mode "${id}"`);
|
|
4724
6368
|
broadcast(clients, {
|
|
4725
6369
|
type: "session.start",
|
|
4726
6370
|
payload: { ...await sessionStartPayload() }
|
|
4727
6371
|
});
|
|
4728
6372
|
} catch (err) {
|
|
4729
|
-
|
|
6373
|
+
sendResult2(ws, false, errMessage(err));
|
|
4730
6374
|
}
|
|
4731
6375
|
break;
|
|
4732
6376
|
}
|
|
@@ -4780,13 +6424,13 @@ async function startWebUI(opts = {}) {
|
|
|
4780
6424
|
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
4781
6425
|
const proc = getProcessRegistry().get(pid);
|
|
4782
6426
|
if (proc?.protected) {
|
|
4783
|
-
|
|
6427
|
+
sendResult2(ws, false, `Cannot kill protected process (PID ${pid})`);
|
|
4784
6428
|
break;
|
|
4785
6429
|
}
|
|
4786
6430
|
getProcessRegistry().kill(pid);
|
|
4787
|
-
|
|
6431
|
+
sendResult2(ws, true, `Killed PID ${pid}`);
|
|
4788
6432
|
} catch (err) {
|
|
4789
|
-
|
|
6433
|
+
sendResult2(ws, false, errMessage(err));
|
|
4790
6434
|
}
|
|
4791
6435
|
break;
|
|
4792
6436
|
}
|
|
@@ -4794,47 +6438,33 @@ async function startWebUI(opts = {}) {
|
|
|
4794
6438
|
try {
|
|
4795
6439
|
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
4796
6440
|
getProcessRegistry().killAll();
|
|
4797
|
-
|
|
6441
|
+
sendResult2(ws, true, "All processes killed");
|
|
4798
6442
|
} catch (err) {
|
|
4799
|
-
|
|
6443
|
+
sendResult2(ws, false, errMessage(err));
|
|
4800
6444
|
}
|
|
4801
6445
|
break;
|
|
4802
6446
|
}
|
|
4803
6447
|
case "git.info": {
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
const branch = branchRaw || "(detached)";
|
|
4819
|
-
const diffMatch = /\+\s*(\d+)\s*deletion/i.exec(diffRaw);
|
|
4820
|
-
const addMatch = /(\d+)\s*insertion/i.exec(diffRaw) ?? /(\d+)\s*addition/i.exec(diffRaw);
|
|
4821
|
-
const delMatch = /\+\s*(\d+)\s*deletion/i.exec(diffRaw);
|
|
4822
|
-
const added = addMatch ? Number(addMatch[1]) : 0;
|
|
4823
|
-
const deleted = delMatch ? Number(delMatch[1]) : 0;
|
|
4824
|
-
const untracked = statusRaw.split("\n").filter((l) => l.startsWith("??")).length;
|
|
4825
|
-
const [aheadRaw, behindRaw] = (upstreamRaw || "0 0").split(" ");
|
|
4826
|
-
const ahead = Number(aheadRaw) || 0;
|
|
4827
|
-
const behind = Number(behindRaw) || 0;
|
|
4828
|
-
send(ws, {
|
|
4829
|
-
type: "git.info",
|
|
4830
|
-
payload: { branch, added, deleted, untracked, ahead, behind }
|
|
4831
|
-
});
|
|
6448
|
+
await handleGitInfo(ws, projectRoot);
|
|
6449
|
+
break;
|
|
6450
|
+
}
|
|
6451
|
+
case "git.changes": {
|
|
6452
|
+
await handleGitChanges(ws, projectRoot);
|
|
6453
|
+
break;
|
|
6454
|
+
}
|
|
6455
|
+
case "git.diff": {
|
|
6456
|
+
await handleGitDiff(ws, projectRoot, String(msg.payload?.path ?? ""));
|
|
6457
|
+
break;
|
|
6458
|
+
}
|
|
6459
|
+
case "webui.shutdown": {
|
|
6460
|
+
console.log("[WebUI] Shutdown requested from client");
|
|
6461
|
+
process.kill(process.pid, "SIGINT");
|
|
4832
6462
|
break;
|
|
4833
6463
|
}
|
|
4834
6464
|
case "goal.get": {
|
|
4835
6465
|
try {
|
|
4836
|
-
const goalPath =
|
|
4837
|
-
const raw = await
|
|
6466
|
+
const goalPath = resolveWstackPaths({ projectRoot }).projectGoal;
|
|
6467
|
+
const raw = await fs9.readFile(goalPath, "utf8");
|
|
4838
6468
|
const goal = JSON.parse(raw);
|
|
4839
6469
|
broadcast(clients, { type: "goal.updated", payload: goal });
|
|
4840
6470
|
} catch {
|
|
@@ -4845,7 +6475,7 @@ async function startWebUI(opts = {}) {
|
|
|
4845
6475
|
case "autonomy.switch": {
|
|
4846
6476
|
const { mode } = msg.payload;
|
|
4847
6477
|
context.meta["autonomy"] = mode;
|
|
4848
|
-
|
|
6478
|
+
sendResult2(ws, true, `Autonomy mode set to "${mode}"`);
|
|
4849
6479
|
broadcast(clients, { type: "prefs.updated", payload: { autonomy: mode } });
|
|
4850
6480
|
void persistPrefsToConfig({ autonomy: mode });
|
|
4851
6481
|
break;
|
|
@@ -4894,7 +6524,7 @@ async function startWebUI(opts = {}) {
|
|
|
4894
6524
|
try {
|
|
4895
6525
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4896
6526
|
const rewinder = new DefaultSessionRewinder(
|
|
4897
|
-
|
|
6527
|
+
path9.join(projectRoot, ".wrongstack", "sessions"),
|
|
4898
6528
|
projectRoot
|
|
4899
6529
|
);
|
|
4900
6530
|
const checkpoints = await rewinder.listCheckpoints(session.id);
|
|
@@ -4915,18 +6545,18 @@ async function startWebUI(opts = {}) {
|
|
|
4915
6545
|
try {
|
|
4916
6546
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4917
6547
|
const rewinder = new DefaultSessionRewinder(
|
|
4918
|
-
|
|
6548
|
+
path9.join(projectRoot, ".wrongstack", "sessions"),
|
|
4919
6549
|
projectRoot
|
|
4920
6550
|
);
|
|
4921
6551
|
await rewinder.rewindToCheckpoint(session.id, checkpointIndex);
|
|
4922
6552
|
await context.session.truncateToCheckpoint(checkpointIndex);
|
|
4923
|
-
|
|
6553
|
+
sendResult2(ws, true, `Rewound to checkpoint ${checkpointIndex}`);
|
|
4924
6554
|
broadcast(clients, {
|
|
4925
6555
|
type: "session.start",
|
|
4926
6556
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
4927
6557
|
});
|
|
4928
6558
|
} catch (err) {
|
|
4929
|
-
|
|
6559
|
+
sendResult2(ws, false, errMessage(err));
|
|
4930
6560
|
}
|
|
4931
6561
|
break;
|
|
4932
6562
|
}
|
|
@@ -4949,9 +6579,9 @@ async function startWebUI(opts = {}) {
|
|
|
4949
6579
|
case "projects.add": {
|
|
4950
6580
|
const { root: addRoot, name: displayName } = msg.payload;
|
|
4951
6581
|
try {
|
|
4952
|
-
const resolved =
|
|
4953
|
-
await
|
|
4954
|
-
const stat2 = await
|
|
6582
|
+
const resolved = path9.resolve(addRoot);
|
|
6583
|
+
await fs9.access(resolved);
|
|
6584
|
+
const stat2 = await fs9.stat(resolved);
|
|
4955
6585
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4956
6586
|
const manifest = await loadManifest(globalConfigPath);
|
|
4957
6587
|
const existing = manifest.projects.find((p) => p.root === resolved);
|
|
@@ -4967,26 +6597,26 @@ async function startWebUI(opts = {}) {
|
|
|
4967
6597
|
});
|
|
4968
6598
|
break;
|
|
4969
6599
|
}
|
|
4970
|
-
const
|
|
6600
|
+
const name2 = displayName?.trim() || path9.basename(resolved);
|
|
4971
6601
|
const slug = generateProjectSlug(resolved);
|
|
4972
6602
|
await ensureProjectDataDir(slug, globalConfigPath);
|
|
4973
6603
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4974
|
-
manifest.projects.push({ name, root: resolved, slug, lastSeen: now, createdAt: now });
|
|
6604
|
+
manifest.projects.push({ name: name2, root: resolved, slug, lastSeen: now, createdAt: now });
|
|
4975
6605
|
await saveManifest(manifest, globalConfigPath);
|
|
4976
6606
|
send(ws, {
|
|
4977
6607
|
type: "projects.added",
|
|
4978
6608
|
payload: {
|
|
4979
|
-
name,
|
|
6609
|
+
name: name2,
|
|
4980
6610
|
root: resolved,
|
|
4981
6611
|
slug,
|
|
4982
|
-
message: `Registered project "${
|
|
6612
|
+
message: `Registered project "${name2}"`
|
|
4983
6613
|
}
|
|
4984
6614
|
});
|
|
4985
6615
|
} catch (err) {
|
|
4986
6616
|
send(ws, {
|
|
4987
6617
|
type: "projects.added",
|
|
4988
6618
|
payload: {
|
|
4989
|
-
name:
|
|
6619
|
+
name: path9.basename(addRoot),
|
|
4990
6620
|
root: addRoot,
|
|
4991
6621
|
slug: "",
|
|
4992
6622
|
message: errMessage(err)
|
|
@@ -4998,17 +6628,17 @@ async function startWebUI(opts = {}) {
|
|
|
4998
6628
|
case "projects.select": {
|
|
4999
6629
|
const { root: selRoot, name: selName } = msg.payload;
|
|
5000
6630
|
try {
|
|
5001
|
-
const resolved =
|
|
6631
|
+
const resolved = path9.resolve(selRoot);
|
|
5002
6632
|
try {
|
|
5003
|
-
await
|
|
5004
|
-
const stat2 = await
|
|
6633
|
+
await fs9.access(resolved);
|
|
6634
|
+
const stat2 = await fs9.stat(resolved);
|
|
5005
6635
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
5006
6636
|
} catch (err) {
|
|
5007
6637
|
send(ws, {
|
|
5008
6638
|
type: "projects.selected",
|
|
5009
6639
|
payload: {
|
|
5010
6640
|
root: selRoot,
|
|
5011
|
-
name: selName ||
|
|
6641
|
+
name: selName || path9.basename(selRoot),
|
|
5012
6642
|
message: `Cannot switch: ${errMessage(err)}`
|
|
5013
6643
|
}
|
|
5014
6644
|
});
|
|
@@ -5020,10 +6650,10 @@ async function startWebUI(opts = {}) {
|
|
|
5020
6650
|
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
5021
6651
|
entry.lastWorkingDir = resolved;
|
|
5022
6652
|
} else {
|
|
5023
|
-
const
|
|
6653
|
+
const name2 = selName?.trim() || path9.basename(resolved);
|
|
5024
6654
|
const slug = generateProjectSlug(resolved);
|
|
5025
6655
|
manifest.projects.push({
|
|
5026
|
-
name,
|
|
6656
|
+
name: name2,
|
|
5027
6657
|
root: resolved,
|
|
5028
6658
|
slug,
|
|
5029
6659
|
lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -5061,13 +6691,13 @@ async function startWebUI(opts = {}) {
|
|
|
5061
6691
|
});
|
|
5062
6692
|
} catch {
|
|
5063
6693
|
}
|
|
5064
|
-
const newSessionsDir =
|
|
5065
|
-
|
|
6694
|
+
const newSessionsDir = path9.join(
|
|
6695
|
+
path9.dirname(globalConfigPath),
|
|
5066
6696
|
"projects",
|
|
5067
6697
|
switchSlug,
|
|
5068
6698
|
"sessions"
|
|
5069
6699
|
);
|
|
5070
|
-
await
|
|
6700
|
+
await fs9.mkdir(newSessionsDir, { recursive: true });
|
|
5071
6701
|
const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
|
|
5072
6702
|
const oldSessionId = session.id;
|
|
5073
6703
|
try {
|
|
@@ -5099,8 +6729,9 @@ async function startWebUI(opts = {}) {
|
|
|
5099
6729
|
sessionId: session.id,
|
|
5100
6730
|
projectSlug: switchSlug,
|
|
5101
6731
|
projectRoot,
|
|
5102
|
-
projectName:
|
|
6732
|
+
projectName: path9.basename(projectRoot),
|
|
5103
6733
|
workingDir,
|
|
6734
|
+
clientType: "webui",
|
|
5104
6735
|
pid: process.pid,
|
|
5105
6736
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5106
6737
|
});
|
|
@@ -5110,8 +6741,8 @@ async function startWebUI(opts = {}) {
|
|
|
5110
6741
|
type: "projects.selected",
|
|
5111
6742
|
payload: {
|
|
5112
6743
|
root: resolved,
|
|
5113
|
-
name: selName ||
|
|
5114
|
-
message: `Switched to ${selName ||
|
|
6744
|
+
name: selName || path9.basename(resolved),
|
|
6745
|
+
message: `Switched to ${selName || path9.basename(resolved)}`
|
|
5115
6746
|
}
|
|
5116
6747
|
});
|
|
5117
6748
|
broadcast(clients, {
|
|
@@ -5134,7 +6765,7 @@ async function startWebUI(opts = {}) {
|
|
|
5134
6765
|
type: "projects.selected",
|
|
5135
6766
|
payload: {
|
|
5136
6767
|
root: selRoot,
|
|
5137
|
-
name: selName ||
|
|
6768
|
+
name: selName || path9.basename(selRoot),
|
|
5138
6769
|
message: errMessage(err)
|
|
5139
6770
|
}
|
|
5140
6771
|
});
|
|
@@ -5145,17 +6776,17 @@ async function startWebUI(opts = {}) {
|
|
|
5145
6776
|
case "working_dir.set": {
|
|
5146
6777
|
const { path: newPath } = msg.payload;
|
|
5147
6778
|
try {
|
|
5148
|
-
const resolved =
|
|
5149
|
-
if (!resolved.startsWith(projectRoot +
|
|
5150
|
-
|
|
6779
|
+
const resolved = path9.resolve(projectRoot, newPath);
|
|
6780
|
+
if (!resolved.startsWith(projectRoot + path9.sep) && resolved !== projectRoot) {
|
|
6781
|
+
sendResult2(ws, false, `Path must stay inside the project root: ${projectRoot}`);
|
|
5151
6782
|
break;
|
|
5152
6783
|
}
|
|
5153
6784
|
try {
|
|
5154
|
-
await
|
|
5155
|
-
const stat2 = await
|
|
6785
|
+
await fs9.access(resolved);
|
|
6786
|
+
const stat2 = await fs9.stat(resolved);
|
|
5156
6787
|
if (!stat2.isDirectory()) throw new Error("Not a directory");
|
|
5157
6788
|
} catch {
|
|
5158
|
-
|
|
6789
|
+
sendResult2(ws, false, `Directory not found or not accessible: ${resolved}`);
|
|
5159
6790
|
break;
|
|
5160
6791
|
}
|
|
5161
6792
|
workingDir = resolved;
|
|
@@ -5164,9 +6795,9 @@ async function startWebUI(opts = {}) {
|
|
|
5164
6795
|
type: "working_dir.changed",
|
|
5165
6796
|
payload: { cwd: resolved, projectRoot }
|
|
5166
6797
|
});
|
|
5167
|
-
|
|
6798
|
+
sendResult2(ws, true, `Working directory set to ${resolved}`);
|
|
5168
6799
|
} catch (err) {
|
|
5169
|
-
|
|
6800
|
+
sendResult2(ws, false, errMessage(err));
|
|
5170
6801
|
}
|
|
5171
6802
|
break;
|
|
5172
6803
|
}
|
|
@@ -5176,31 +6807,31 @@ async function startWebUI(opts = {}) {
|
|
|
5176
6807
|
msg.payload,
|
|
5177
6808
|
logger
|
|
5178
6809
|
);
|
|
5179
|
-
|
|
6810
|
+
sendResult2(ws, result.success, result.message);
|
|
5180
6811
|
break;
|
|
5181
6812
|
}
|
|
5182
6813
|
// ── Mailbox operations — project-level inter-agent messaging ────
|
|
5183
6814
|
case "mailbox.messages":
|
|
5184
6815
|
return handleMailboxMessages(
|
|
5185
6816
|
ws,
|
|
5186
|
-
{ projectRoot, globalRoot:
|
|
6817
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
5187
6818
|
msg.payload
|
|
5188
6819
|
);
|
|
5189
6820
|
case "mailbox.agents":
|
|
5190
6821
|
return handleMailboxAgents(
|
|
5191
6822
|
ws,
|
|
5192
|
-
{ projectRoot, globalRoot:
|
|
6823
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
5193
6824
|
msg.payload
|
|
5194
6825
|
);
|
|
5195
6826
|
case "mailbox.clear":
|
|
5196
6827
|
return handleMailboxClear(
|
|
5197
6828
|
ws,
|
|
5198
|
-
{ projectRoot, globalRoot:
|
|
6829
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) }
|
|
5199
6830
|
);
|
|
5200
6831
|
case "mailbox.purge":
|
|
5201
6832
|
return handleMailboxPurge(
|
|
5202
6833
|
ws,
|
|
5203
|
-
{ projectRoot, globalRoot:
|
|
6834
|
+
{ projectRoot, globalRoot: path9.dirname(globalConfigPath) },
|
|
5204
6835
|
msg.payload
|
|
5205
6836
|
);
|
|
5206
6837
|
// ── Brain — status, autonomy ceiling, direct decision support ───
|
|
@@ -5214,7 +6845,7 @@ async function startWebUI(opts = {}) {
|
|
|
5214
6845
|
const level = msg.payload?.level ?? "";
|
|
5215
6846
|
const valid = ["off", "low", "medium", "high", "all"];
|
|
5216
6847
|
if (!valid.includes(level)) {
|
|
5217
|
-
|
|
6848
|
+
sendResult2(ws, false, `Unknown risk level "${level}". Use: ${valid.join(", ")}.`);
|
|
5218
6849
|
break;
|
|
5219
6850
|
}
|
|
5220
6851
|
brainSettings.maxAutoRisk = level;
|
|
@@ -5227,7 +6858,7 @@ async function startWebUI(opts = {}) {
|
|
|
5227
6858
|
case "brain.ask": {
|
|
5228
6859
|
const question = msg.payload?.question?.trim();
|
|
5229
6860
|
if (!question) {
|
|
5230
|
-
|
|
6861
|
+
sendResult2(ws, false, "Usage: /brain ask <question>");
|
|
5231
6862
|
break;
|
|
5232
6863
|
}
|
|
5233
6864
|
try {
|
|
@@ -5240,7 +6871,7 @@ async function startWebUI(opts = {}) {
|
|
|
5240
6871
|
});
|
|
5241
6872
|
send(ws, { type: "brain.answer", payload: { question, decision } });
|
|
5242
6873
|
} catch (err) {
|
|
5243
|
-
|
|
6874
|
+
sendResult2(ws, false, `Brain consultation failed: ${errMessage(err)}`);
|
|
5244
6875
|
}
|
|
5245
6876
|
break;
|
|
5246
6877
|
}
|
|
@@ -5267,14 +6898,28 @@ async function startWebUI(opts = {}) {
|
|
|
5267
6898
|
broadcast,
|
|
5268
6899
|
clients
|
|
5269
6900
|
});
|
|
6901
|
+
const watcherMetrics = {
|
|
6902
|
+
fileChangesDetected: 0,
|
|
6903
|
+
filesProcessed: 0,
|
|
6904
|
+
broadcastsSent: 0,
|
|
6905
|
+
debounceResets: 0,
|
|
6906
|
+
totalDebounceDelayMs: 0,
|
|
6907
|
+
activeProjects: 0,
|
|
6908
|
+
averageDebounceDelayMs: 0,
|
|
6909
|
+
watcherActive: false
|
|
6910
|
+
};
|
|
5270
6911
|
const httpServer = createHttpServer({
|
|
5271
6912
|
host: wsHost,
|
|
5272
|
-
distDir:
|
|
6913
|
+
distDir: path9.resolve(import.meta.dirname, "../../dist"),
|
|
5273
6914
|
wsPort,
|
|
5274
6915
|
globalRoot: wpaths.globalRoot,
|
|
5275
|
-
apiToken: wsToken
|
|
6916
|
+
apiToken: wsToken,
|
|
6917
|
+
watcherMetrics,
|
|
6918
|
+
onFleetPing: () => {
|
|
6919
|
+
void fleetBroadcast?.();
|
|
6920
|
+
}
|
|
5276
6921
|
});
|
|
5277
|
-
const registryBaseDir =
|
|
6922
|
+
const registryBaseDir = path9.dirname(globalConfigPath);
|
|
5278
6923
|
httpServer.listen(httpPort, wsHost, () => {
|
|
5279
6924
|
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
5280
6925
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
@@ -5286,7 +6931,7 @@ async function startWebUI(opts = {}) {
|
|
|
5286
6931
|
wsPort,
|
|
5287
6932
|
host: wsHost,
|
|
5288
6933
|
projectRoot,
|
|
5289
|
-
projectName:
|
|
6934
|
+
projectName: path9.basename(projectRoot) || projectRoot,
|
|
5290
6935
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5291
6936
|
url: `http://${wsHost}:${httpPort}`
|
|
5292
6937
|
},
|
|
@@ -5313,6 +6958,11 @@ async function startWebUI(opts = {}) {
|
|
|
5313
6958
|
// reality. Crash exits are healed by the next register()/list() prune pass.
|
|
5314
6959
|
onShutdown: () => {
|
|
5315
6960
|
brainMonitor.stop();
|
|
6961
|
+
void mcpRegistry.stopAll().catch(() => void 0);
|
|
6962
|
+
if (disposeEvents) {
|
|
6963
|
+
disposeEvents();
|
|
6964
|
+
disposeEvents = null;
|
|
6965
|
+
}
|
|
5316
6966
|
if (eternalSubscription) {
|
|
5317
6967
|
eternalSubscription.dispose();
|
|
5318
6968
|
eternalSubscription = null;
|
|
@@ -5343,10 +6993,30 @@ export {
|
|
|
5343
6993
|
handleFilesRead,
|
|
5344
6994
|
handleFilesTree,
|
|
5345
6995
|
handleFilesWrite,
|
|
6996
|
+
handleGitChanges,
|
|
6997
|
+
handleGitDiff,
|
|
6998
|
+
handleGitInfo,
|
|
6999
|
+
handleMcpAdd,
|
|
7000
|
+
handleMcpDisable,
|
|
7001
|
+
handleMcpDiscover,
|
|
7002
|
+
handleMcpEnable,
|
|
7003
|
+
handleMcpList,
|
|
7004
|
+
handleMcpRemove,
|
|
7005
|
+
handleMcpRestart,
|
|
7006
|
+
handleMcpSleep,
|
|
7007
|
+
handleMcpUpdate,
|
|
7008
|
+
handleMcpWake,
|
|
5346
7009
|
handleMemoryForget,
|
|
5347
7010
|
handleMemoryList,
|
|
5348
7011
|
handleMemoryRemember,
|
|
5349
7012
|
handleShellOpen,
|
|
7013
|
+
handleSkillsContent,
|
|
7014
|
+
handleSkillsCreate,
|
|
7015
|
+
handleSkillsEdit,
|
|
7016
|
+
handleSkillsExport,
|
|
7017
|
+
handleSkillsInstall,
|
|
7018
|
+
handleSkillsUninstall,
|
|
7019
|
+
handleSkillsUpdate,
|
|
5350
7020
|
hostHeaderOk,
|
|
5351
7021
|
injectWsPort,
|
|
5352
7022
|
isLoopbackBind,
|
|
@@ -5364,7 +7034,7 @@ export {
|
|
|
5364
7034
|
removeProvider,
|
|
5365
7035
|
saveProviders,
|
|
5366
7036
|
send,
|
|
5367
|
-
sendResult,
|
|
7037
|
+
sendResult2 as sendResult,
|
|
5368
7038
|
setActiveKey,
|
|
5369
7039
|
startWebUI,
|
|
5370
7040
|
stringifyContent,
|