@wrongstack/webui 0.260.0 → 0.265.1
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-CC3KdGXG.js +118 -0
- package/dist/assets/index-CFHDQU3T.css +2 -0
- package/dist/assets/{vendor-CEQg2uSG.css → vendor-B2D6LvU3.css} +1 -1
- package/dist/assets/vendor-BX-wfDR9.js +1326 -0
- package/dist/index.html +4 -4
- package/dist/index.js +9731 -4596
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +1833 -299
- 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 +147 -6
- package/dist/server/index.js +2148 -347
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +1597 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +7 -5
- package/dist/assets/index-6rPVh7TJ.css +0 -2
- package/dist/assets/index-CfIQObXO.js +0 -165
- package/dist/assets/vendor-BRkhRU94.js +0 -1303
package/dist/server/entry.js
CHANGED
|
@@ -1,14 +1,180 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
});
|
|
2
|
+
// src/server/index.ts
|
|
3
|
+
import { expectDefined as expectDefined2, GlobalMailbox as GlobalMailbox2, projectSlug, getSessionRegistry, AgentStatusTracker, FleetNotifier } from "@wrongstack/core";
|
|
4
|
+
|
|
5
|
+
// src/server/handlers/worklist-handlers.ts
|
|
6
|
+
function sendResult(ws, ctx, ok, message) {
|
|
7
|
+
ctx.send(ws, { type: ok ? "ok" : "error", message });
|
|
8
|
+
}
|
|
9
|
+
function handleTodosGet(ctx, ws) {
|
|
10
|
+
ctx.send(ws, { type: "todos.updated", payload: { todos: ctx.context.todos } });
|
|
11
|
+
}
|
|
12
|
+
function handleTodosClear(ctx, ws) {
|
|
13
|
+
ctx.replaceTodos?.([]);
|
|
14
|
+
ctx.broadcast({ type: "todos.cleared" });
|
|
15
|
+
sendResult(ws, ctx, true, "Todo board cleared.");
|
|
16
|
+
}
|
|
17
|
+
function handleTodosRemove(ctx, ws, payload) {
|
|
18
|
+
if (!payload || payload.id === void 0 && payload.index === void 0) {
|
|
19
|
+
sendResult(ws, ctx, false, "todos.remove requires id or index.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const next = payload.id !== void 0 ? ctx.context.todos.filter((t) => t.id !== payload.id) : ctx.context.todos.filter((_, i) => i !== payload.index);
|
|
23
|
+
ctx.replaceTodos?.(next);
|
|
24
|
+
ctx.broadcast({ type: "todos.updated", payload: { todos: next } });
|
|
25
|
+
sendResult(ws, ctx, true, "Todo item removed.");
|
|
26
|
+
}
|
|
27
|
+
function handleTodoUpdate(ctx, ws, payload) {
|
|
28
|
+
const todo = ctx.context.todos.find((t) => t.id === payload.id);
|
|
29
|
+
if (!todo) {
|
|
30
|
+
sendResult(ws, ctx, false, `No todo with id "${payload.id}".`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const next = ctx.context.todos.map(
|
|
34
|
+
(t) => t.id === payload.id ? { ...t, ...payload.status !== void 0 && { status: payload.status }, ...payload.activeForm !== void 0 && { activeForm: payload.activeForm } } : t
|
|
35
|
+
);
|
|
36
|
+
ctx.replaceTodos?.(next);
|
|
37
|
+
ctx.broadcast({ type: "todos.updated", payload: { todos: next } });
|
|
38
|
+
sendResult(ws, ctx, true, `Todo "${todo.content}" updated.`);
|
|
39
|
+
}
|
|
40
|
+
async function handleTasksGet(ctx, ws) {
|
|
41
|
+
const taskPath = ctx.context.meta["task.path"];
|
|
42
|
+
if (typeof taskPath === "string" && taskPath) {
|
|
43
|
+
try {
|
|
44
|
+
const { loadTasks } = await import("@wrongstack/core");
|
|
45
|
+
const file = await loadTasks(taskPath);
|
|
46
|
+
ctx.send(ws, { type: "tasks.updated", payload: { tasks: file?.tasks ?? [] } });
|
|
47
|
+
} catch {
|
|
48
|
+
ctx.send(ws, { type: "tasks.updated", payload: { tasks: [] } });
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
ctx.send(ws, {
|
|
52
|
+
type: "tasks.updated",
|
|
53
|
+
payload: { tasks: [], error: "Task storage not configured." }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function handleTaskUpdate(ctx, ws, payload) {
|
|
58
|
+
const taskPath = ctx.context.meta["task.path"];
|
|
59
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
60
|
+
sendResult(ws, ctx, false, "Task storage is not configured for this session.");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const { loadTasks, saveTasks } = await import("@wrongstack/core");
|
|
65
|
+
const file = await loadTasks(taskPath);
|
|
66
|
+
if (!file) {
|
|
67
|
+
sendResult(ws, ctx, false, "No task file found.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const idx = file.tasks.findIndex((t) => t.id === payload.id);
|
|
71
|
+
if (idx === -1) {
|
|
72
|
+
sendResult(ws, ctx, false, `Task "${payload.id}" not found.`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
file.tasks[idx] = { ...file.tasks[idx], status: payload.status };
|
|
76
|
+
await saveTasks(taskPath, file);
|
|
77
|
+
ctx.broadcast({ type: "tasks.updated", payload: { tasks: file.tasks } });
|
|
78
|
+
sendResult(ws, ctx, true, `Task "${payload.id}" marked ${payload.status}.`);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
sendResult(ws, ctx, false, String(err));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function handlePlanGet(ctx, ws) {
|
|
84
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
85
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
86
|
+
if (typeof planPath === "string" && planPath) {
|
|
87
|
+
try {
|
|
88
|
+
const { loadPlan } = await import("@wrongstack/core");
|
|
89
|
+
const plan = await loadPlan(planPath);
|
|
90
|
+
ctx.send(ws, {
|
|
91
|
+
type: "plan.updated",
|
|
92
|
+
payload: {
|
|
93
|
+
plan: plan ?? {
|
|
94
|
+
version: 1,
|
|
95
|
+
sessionId,
|
|
96
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
97
|
+
items: []
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
} catch {
|
|
102
|
+
ctx.send(ws, {
|
|
103
|
+
type: "plan.updated",
|
|
104
|
+
payload: {
|
|
105
|
+
plan: {
|
|
106
|
+
version: 1,
|
|
107
|
+
sessionId,
|
|
108
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
109
|
+
items: []
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
ctx.send(ws, {
|
|
116
|
+
type: "plan.updated",
|
|
117
|
+
payload: { plan: null, error: "Plan storage is not configured for this session." }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function handlePlanTemplateUse(ctx, ws, template) {
|
|
122
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
123
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
124
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
125
|
+
sendResult(ws, ctx, false, "Plan storage is not configured for this session.");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem } = await import("@wrongstack/core");
|
|
130
|
+
const tpl = getPlanTemplate(template);
|
|
131
|
+
if (!tpl) {
|
|
132
|
+
sendResult(ws, ctx, false, `Unknown template "${template}".`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
let plan = await loadPlan(planPath) ?? emptyPlan(sessionId);
|
|
136
|
+
for (const item of tpl.items) {
|
|
137
|
+
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
138
|
+
}
|
|
139
|
+
await savePlan(planPath, plan);
|
|
140
|
+
sendResult(ws, ctx, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
|
|
141
|
+
ctx.broadcast({ type: "plan.updated", payload: { plan } });
|
|
142
|
+
} catch (err) {
|
|
143
|
+
sendResult(ws, ctx, false, String(err));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function handlePlanItemUpdate(ctx, ws, payload) {
|
|
147
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
148
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
149
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
150
|
+
sendResult(ws, ctx, false, "Plan storage is not configured for this session.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const { loadPlan, savePlan, mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
|
|
155
|
+
let changed = false;
|
|
156
|
+
const plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
157
|
+
const before = p.updatedAt;
|
|
158
|
+
const updated = setPlanItemStatus(p, payload.target, payload.status);
|
|
159
|
+
changed = updated.updatedAt !== before;
|
|
160
|
+
return updated;
|
|
161
|
+
});
|
|
162
|
+
if (!changed) {
|
|
163
|
+
sendResult(ws, ctx, false, `No plan item matched "${payload.target}".`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
sendResult(ws, ctx, true, `Plan item status updated to "${payload.status}".`);
|
|
167
|
+
ctx.broadcast({ type: "plan.updated", payload: { plan } });
|
|
168
|
+
} catch (err) {
|
|
169
|
+
sendResult(ws, ctx, false, String(err));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
8
172
|
|
|
9
173
|
// src/server/index.ts
|
|
10
|
-
import { expectDefined as expectDefined2, GlobalMailbox as GlobalMailbox2, projectSlug, getSessionRegistry, AgentStatusTracker } from "@wrongstack/core";
|
|
11
174
|
import { makeMailboxTool, makeMailSendTool, makeMailInboxTool, mailboxSessionTag } from "@wrongstack/core";
|
|
175
|
+
import { toErrorMessage as toErrorMessage5, wstackGlobalRoot as wstackGlobalRoot2, projectHash, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
176
|
+
import { SkillInstaller } from "@wrongstack/core/skills";
|
|
177
|
+
import JSZip2 from "jszip";
|
|
12
178
|
import {
|
|
13
179
|
BrainMonitor,
|
|
14
180
|
DefaultBrainArbiter,
|
|
@@ -16,8 +182,8 @@ import {
|
|
|
16
182
|
createAutonomyBrain,
|
|
17
183
|
createTieredBrainArbiter
|
|
18
184
|
} from "@wrongstack/core";
|
|
19
|
-
import * as
|
|
20
|
-
import * as
|
|
185
|
+
import * as fs10 from "fs/promises";
|
|
186
|
+
import * as path10 from "path";
|
|
21
187
|
|
|
22
188
|
// src/server/http-server.ts
|
|
23
189
|
import * as fs from "fs/promises";
|
|
@@ -25,7 +191,7 @@ import * as http from "http";
|
|
|
25
191
|
import * as path from "path";
|
|
26
192
|
|
|
27
193
|
// src/server/ws-auth.ts
|
|
28
|
-
import { Buffer
|
|
194
|
+
import { Buffer } from "buffer";
|
|
29
195
|
import { timingSafeEqual } from "crypto";
|
|
30
196
|
function isLoopbackHostname(hostname) {
|
|
31
197
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
@@ -44,8 +210,8 @@ function isLoopbackBind(wsHost) {
|
|
|
44
210
|
}
|
|
45
211
|
function tokenMatches(provided, expected) {
|
|
46
212
|
if (!provided) return false;
|
|
47
|
-
const a =
|
|
48
|
-
const b =
|
|
213
|
+
const a = Buffer.from(provided);
|
|
214
|
+
const b = Buffer.from(expected);
|
|
49
215
|
if (a.length !== b.length) return false;
|
|
50
216
|
return timingSafeEqual(a, b);
|
|
51
217
|
}
|
|
@@ -136,6 +302,13 @@ function isInsideDist(candidate, distDir) {
|
|
|
136
302
|
const resolved = path.resolve(candidate);
|
|
137
303
|
return resolved === root || resolved.startsWith(root + path.sep);
|
|
138
304
|
}
|
|
305
|
+
function decodeSessionId(segment) {
|
|
306
|
+
try {
|
|
307
|
+
return decodeURIComponent(segment);
|
|
308
|
+
} catch {
|
|
309
|
+
return segment;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
139
312
|
function createHttpServer(opts) {
|
|
140
313
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
141
314
|
const distDir = path.resolve(opts.distDir);
|
|
@@ -161,6 +334,22 @@ function createHttpServer(opts) {
|
|
|
161
334
|
res.end("ok");
|
|
162
335
|
return;
|
|
163
336
|
}
|
|
337
|
+
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
338
|
+
const headerToken = req.headers["x-ws-token"];
|
|
339
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
340
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
341
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
342
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
opts.onFleetPing?.();
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
res.writeHead(204);
|
|
350
|
+
res.end();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
164
353
|
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
165
354
|
const headerToken = req.headers["x-ws-token"];
|
|
166
355
|
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
@@ -181,7 +370,89 @@ function createHttpServer(opts) {
|
|
|
181
370
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
182
371
|
return;
|
|
183
372
|
}
|
|
184
|
-
await handleApiSessionAgents(res, opts.globalRoot, agentsMatch[1]);
|
|
373
|
+
await handleApiSessionAgents(res, opts.globalRoot, decodeSessionId(agentsMatch[1]));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const eventsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
|
|
377
|
+
if (eventsMatch && req.method === "GET") {
|
|
378
|
+
const headerToken = req.headers["x-ws-token"];
|
|
379
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
380
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
381
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
382
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
|
|
386
|
+
const limit = Math.min(500, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
|
|
387
|
+
await handleApiSessionEvents(res, opts.globalRoot, decodeSessionId(eventsMatch[1]), limit);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const msgMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/message$/);
|
|
391
|
+
if (msgMatch && req.method === "POST") {
|
|
392
|
+
const headerToken = req.headers["x-ws-token"];
|
|
393
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
394
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
395
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
396
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
await handleApiSessionMessage(res, req, opts.globalRoot, decodeSessionId(msgMatch[1]));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const mailboxMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/mailbox$/);
|
|
403
|
+
if (mailboxMatch && req.method === "GET") {
|
|
404
|
+
const headerToken = req.headers["x-ws-token"];
|
|
405
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
406
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
407
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
408
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
await handleApiSessionMailbox(res, opts.globalRoot, decodeSessionId(mailboxMatch[1]));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const interruptMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/interrupt$/);
|
|
415
|
+
if (interruptMatch && req.method === "POST") {
|
|
416
|
+
const headerToken = req.headers["x-ws-token"];
|
|
417
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
418
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
419
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
420
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
await handleApiSessionInterrupt(
|
|
424
|
+
res,
|
|
425
|
+
req,
|
|
426
|
+
opts.globalRoot,
|
|
427
|
+
decodeSessionId(interruptMatch[1])
|
|
428
|
+
);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (url.pathname === "/api/fleet/broadcast" && req.method === "POST") {
|
|
432
|
+
const headerToken = req.headers["x-ws-token"];
|
|
433
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
434
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
435
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
436
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
await handleApiFleetBroadcast(res, req, opts.globalRoot);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (url.pathname === "/debug/watcher-metrics" && req.method === "GET") {
|
|
443
|
+
if (opts.watcherMetrics) {
|
|
444
|
+
const avgDelay = opts.watcherMetrics.broadcastsSent > 0 ? opts.watcherMetrics.totalDebounceDelayMs / opts.watcherMetrics.broadcastsSent : 0;
|
|
445
|
+
const response = {
|
|
446
|
+
...opts.watcherMetrics,
|
|
447
|
+
averageDebounceDelayMs: avgDelay,
|
|
448
|
+
timestamp: Date.now()
|
|
449
|
+
};
|
|
450
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
451
|
+
res.end(JSON.stringify(response));
|
|
452
|
+
} else {
|
|
453
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
454
|
+
res.end(JSON.stringify({ error: "File watcher metrics not available" }));
|
|
455
|
+
}
|
|
185
456
|
return;
|
|
186
457
|
}
|
|
187
458
|
let filePath;
|
|
@@ -313,6 +584,324 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
|
313
584
|
res.end(JSON.stringify({ error: String(err) }));
|
|
314
585
|
}
|
|
315
586
|
}
|
|
587
|
+
function blocksToText(content) {
|
|
588
|
+
if (typeof content === "string") return content;
|
|
589
|
+
if (Array.isArray(content)) {
|
|
590
|
+
return content.filter(
|
|
591
|
+
(b) => !!b && typeof b === "object" && b.type === "text" && typeof b.text === "string"
|
|
592
|
+
).map((b) => b.text).join("\n");
|
|
593
|
+
}
|
|
594
|
+
return "";
|
|
595
|
+
}
|
|
596
|
+
function clip(s, n = 600) {
|
|
597
|
+
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
598
|
+
}
|
|
599
|
+
function asString(v) {
|
|
600
|
+
if (typeof v === "string") return v;
|
|
601
|
+
try {
|
|
602
|
+
return JSON.stringify(v);
|
|
603
|
+
} catch {
|
|
604
|
+
return String(v);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function mapWatchEntry(ev) {
|
|
608
|
+
const ts = typeof ev["ts"] === "string" ? ev["ts"] : "";
|
|
609
|
+
switch (ev["type"]) {
|
|
610
|
+
case "user_input":
|
|
611
|
+
return { ts, role: "user", text: clip(blocksToText(ev["content"])) };
|
|
612
|
+
case "llm_response": {
|
|
613
|
+
const text = blocksToText(ev["content"]);
|
|
614
|
+
return text.trim() ? { ts, role: "assistant", text: clip(text) } : null;
|
|
615
|
+
}
|
|
616
|
+
case "tool_use":
|
|
617
|
+
case "tool_call_start": {
|
|
618
|
+
const input = ev["input"] ?? ev["args"];
|
|
619
|
+
const preview = input !== void 0 && input !== null ? clip(asString(input), 160) : "";
|
|
620
|
+
return { ts, role: "tool", tool: String(ev["name"] ?? "tool"), text: preview };
|
|
621
|
+
}
|
|
622
|
+
case "tool_result": {
|
|
623
|
+
if (ev["isError"]) return { ts, role: "error", text: clip(asString(ev["content"])) };
|
|
624
|
+
const out = asString(ev["content"]).trim();
|
|
625
|
+
return out ? { ts, role: "tool", tool: "\u21B3 result", text: clip(out, 240) } : null;
|
|
626
|
+
}
|
|
627
|
+
case "error":
|
|
628
|
+
case "provider_error":
|
|
629
|
+
return { ts, role: "error", text: clip(String(ev["message"] ?? "error")) };
|
|
630
|
+
case "agent_spawned":
|
|
631
|
+
return { ts, role: "system", text: `spawned ${String(ev["role"] ?? "agent")}` };
|
|
632
|
+
case "task_completed":
|
|
633
|
+
return { ts, role: "system", text: `task done: ${String(ev["title"] ?? "")}` };
|
|
634
|
+
case "task_failed":
|
|
635
|
+
return { ts, role: "system", text: `task failed: ${String(ev["title"] ?? "")}` };
|
|
636
|
+
default:
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
641
|
+
if (!globalRoot) {
|
|
642
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
643
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
|
|
648
|
+
const registry = new SessionRegistry(globalRoot);
|
|
649
|
+
const entry = await registry.get(sessionId);
|
|
650
|
+
if (!entry) {
|
|
651
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
652
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
656
|
+
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
657
|
+
const reader = new DefaultSessionReader2({ store });
|
|
658
|
+
const all = [];
|
|
659
|
+
for await (const ev of reader.replay(sessionId)) {
|
|
660
|
+
const mapped = mapWatchEntry(ev);
|
|
661
|
+
if (mapped) all.push(mapped);
|
|
662
|
+
}
|
|
663
|
+
const tail = all.slice(-limit);
|
|
664
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
665
|
+
res.end(
|
|
666
|
+
JSON.stringify({
|
|
667
|
+
sessionId,
|
|
668
|
+
status: entry.status,
|
|
669
|
+
clientType: entry.clientType,
|
|
670
|
+
projectName: entry.projectName,
|
|
671
|
+
total: all.length,
|
|
672
|
+
entries: tail
|
|
673
|
+
})
|
|
674
|
+
);
|
|
675
|
+
} catch (err) {
|
|
676
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
677
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function readJsonBody(req) {
|
|
681
|
+
return new Promise((resolve5, reject) => {
|
|
682
|
+
let data = "";
|
|
683
|
+
req.on("data", (chunk) => {
|
|
684
|
+
data += chunk;
|
|
685
|
+
if (data.length > 64e3) {
|
|
686
|
+
reject(new Error("Request body too large"));
|
|
687
|
+
req.destroy();
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
req.on("end", () => {
|
|
691
|
+
try {
|
|
692
|
+
resolve5(data ? JSON.parse(data) : {});
|
|
693
|
+
} catch (err) {
|
|
694
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
req.on("error", reject);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
701
|
+
if (!globalRoot) {
|
|
702
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
703
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
let body;
|
|
707
|
+
try {
|
|
708
|
+
body = await readJsonBody(req);
|
|
709
|
+
} catch {
|
|
710
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
711
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const text = typeof body["text"] === "string" ? body["text"].trim() : "";
|
|
715
|
+
if (!text) {
|
|
716
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
717
|
+
res.end(JSON.stringify({ error: "text is required" }));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
721
|
+
const ALLOWED = /* @__PURE__ */ new Set(["steer", "ask", "assign", "note", "btw"]);
|
|
722
|
+
const rawType = typeof body["type"] === "string" ? body["type"] : "steer";
|
|
723
|
+
const type = ALLOWED.has(rawType) ? rawType : "steer";
|
|
724
|
+
const rawPriority = typeof body["priority"] === "string" ? body["priority"] : "";
|
|
725
|
+
const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
|
|
726
|
+
const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
|
|
727
|
+
try {
|
|
728
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
729
|
+
const registry = new SessionRegistry(globalRoot);
|
|
730
|
+
const entry = await registry.get(sessionId);
|
|
731
|
+
if (!entry) {
|
|
732
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
733
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
737
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
738
|
+
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
739
|
+
const sent = await mailbox.send({ from, to, type, subject, body: text, priority });
|
|
740
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
741
|
+
res.end(JSON.stringify({ ok: true, id: sent.id, to, type, delivered: entry.status }));
|
|
742
|
+
} catch (err) {
|
|
743
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
744
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
748
|
+
if (!globalRoot) {
|
|
749
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
750
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
755
|
+
const registry = new SessionRegistry(globalRoot);
|
|
756
|
+
const entry = await registry.get(sessionId);
|
|
757
|
+
if (!entry) {
|
|
758
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
759
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
763
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
764
|
+
const leaderAddr = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
765
|
+
const [inbound, outbound] = await Promise.all([
|
|
766
|
+
mailbox.query({ to: leaderAddr, limit: 50 }),
|
|
767
|
+
mailbox.query({ from: leaderAddr, limit: 50 })
|
|
768
|
+
]);
|
|
769
|
+
const seen = /* @__PURE__ */ new Set();
|
|
770
|
+
const thread = [...inbound, ...outbound].filter((m) => {
|
|
771
|
+
if (seen.has(m.id)) return false;
|
|
772
|
+
seen.add(m.id);
|
|
773
|
+
return true;
|
|
774
|
+
}).sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)).map((m) => ({
|
|
775
|
+
id: m.id,
|
|
776
|
+
from: m.from,
|
|
777
|
+
to: m.to,
|
|
778
|
+
type: m.type,
|
|
779
|
+
subject: m.subject,
|
|
780
|
+
body: m.body,
|
|
781
|
+
priority: m.priority,
|
|
782
|
+
// Whether the leader has read it, and when.
|
|
783
|
+
readByLeader: m.readBy?.[leaderAddr] ?? null,
|
|
784
|
+
readByCount: Object.keys(m.readBy ?? {}).length,
|
|
785
|
+
completed: m.completed,
|
|
786
|
+
outcome: m.outcome ?? null,
|
|
787
|
+
timestamp: m.timestamp,
|
|
788
|
+
replyTo: m.replyTo ?? null,
|
|
789
|
+
fromLeader: m.from === leaderAddr
|
|
790
|
+
}));
|
|
791
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
792
|
+
res.end(JSON.stringify({ sessionId, leader: leaderAddr, status: entry.status, thread }));
|
|
793
|
+
} catch (err) {
|
|
794
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
795
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
799
|
+
if (!globalRoot) {
|
|
800
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
801
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
let body = {};
|
|
805
|
+
try {
|
|
806
|
+
body = await readJsonBody(req);
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
|
|
810
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
811
|
+
try {
|
|
812
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
813
|
+
const registry = new SessionRegistry(globalRoot);
|
|
814
|
+
const entry = await registry.get(sessionId);
|
|
815
|
+
if (!entry) {
|
|
816
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
817
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
821
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
822
|
+
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
823
|
+
const sent = await mailbox.send({
|
|
824
|
+
from,
|
|
825
|
+
to,
|
|
826
|
+
type: "control",
|
|
827
|
+
subject: "interrupt",
|
|
828
|
+
body: reason,
|
|
829
|
+
priority: "high"
|
|
830
|
+
});
|
|
831
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
832
|
+
res.end(JSON.stringify({ ok: true, id: sent.id, to, delivered: entry.status }));
|
|
833
|
+
} catch (err) {
|
|
834
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
835
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
839
|
+
if (!globalRoot) {
|
|
840
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
841
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
let body;
|
|
845
|
+
try {
|
|
846
|
+
body = await readJsonBody(req);
|
|
847
|
+
} catch {
|
|
848
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
849
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const text = typeof body["text"] === "string" ? body["text"].trim() : "";
|
|
853
|
+
if (!text) {
|
|
854
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
855
|
+
res.end(JSON.stringify({ error: "text is required" }));
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
859
|
+
try {
|
|
860
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
861
|
+
const registry = new SessionRegistry(globalRoot);
|
|
862
|
+
const all = await registry.list();
|
|
863
|
+
const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
|
|
864
|
+
const targets = all.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true);
|
|
865
|
+
if (targets.length === 0) {
|
|
866
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
867
|
+
res.end(JSON.stringify({ ok: true, delivered: 0 }));
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const mbByDir = /* @__PURE__ */ new Map();
|
|
871
|
+
const mailboxFor = (projectRoot) => {
|
|
872
|
+
const dir = resolveWstackPaths2({ projectRoot, globalRoot }).projectDir;
|
|
873
|
+
let mb = mbByDir.get(dir);
|
|
874
|
+
if (!mb) {
|
|
875
|
+
mb = new GlobalMailbox3(dir);
|
|
876
|
+
mbByDir.set(dir, mb);
|
|
877
|
+
}
|
|
878
|
+
return mb;
|
|
879
|
+
};
|
|
880
|
+
let delivered = 0;
|
|
881
|
+
await Promise.all(
|
|
882
|
+
targets.map(async (s) => {
|
|
883
|
+
try {
|
|
884
|
+
const mb = mailboxFor(s.projectRoot);
|
|
885
|
+
await mb.send({
|
|
886
|
+
from,
|
|
887
|
+
to: `leader@${mailboxSessionTag2(s.sessionId)}`,
|
|
888
|
+
type: "steer",
|
|
889
|
+
subject: "Broadcast from Fleet HQ",
|
|
890
|
+
body: text,
|
|
891
|
+
priority: "high"
|
|
892
|
+
});
|
|
893
|
+
delivered++;
|
|
894
|
+
} catch {
|
|
895
|
+
}
|
|
896
|
+
})
|
|
897
|
+
);
|
|
898
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
899
|
+
res.end(JSON.stringify({ ok: true, delivered, targets: targets.length }));
|
|
900
|
+
} catch (err) {
|
|
901
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
902
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
316
905
|
|
|
317
906
|
// src/server/file-handlers.ts
|
|
318
907
|
import * as fs2 from "fs/promises";
|
|
@@ -386,7 +975,7 @@ function broadcast(clients, msg) {
|
|
|
386
975
|
}
|
|
387
976
|
}
|
|
388
977
|
}
|
|
389
|
-
function
|
|
978
|
+
function sendResult2(ws, success, message) {
|
|
390
979
|
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
391
980
|
}
|
|
392
981
|
function errMessage(err) {
|
|
@@ -535,23 +1124,265 @@ async function handleMemoryRemember(ws, msg, memoryStore) {
|
|
|
535
1124
|
const { text, scope } = msg.payload;
|
|
536
1125
|
try {
|
|
537
1126
|
await memoryStore.remember(text, scope ?? "project-memory");
|
|
538
|
-
|
|
1127
|
+
sendResult2(ws, true, "Saved to memory");
|
|
539
1128
|
} catch (err) {
|
|
540
|
-
|
|
1129
|
+
sendResult2(ws, false, errMessage(err));
|
|
541
1130
|
}
|
|
542
1131
|
}
|
|
543
1132
|
async function handleMemoryForget(ws, msg, memoryStore) {
|
|
544
1133
|
const { text, scope } = msg.payload;
|
|
545
1134
|
try {
|
|
546
1135
|
const removed = await memoryStore.forget(text, scope ?? "project-memory");
|
|
547
|
-
|
|
1136
|
+
sendResult2(
|
|
548
1137
|
ws,
|
|
549
1138
|
removed > 0,
|
|
550
1139
|
removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
|
|
551
1140
|
);
|
|
552
1141
|
} catch (err) {
|
|
553
|
-
|
|
1142
|
+
sendResult2(ws, false, errMessage(err));
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/server/mcp-handlers.ts
|
|
1147
|
+
import * as fs3 from "fs/promises";
|
|
1148
|
+
import * as path3 from "path";
|
|
1149
|
+
function isMcpServerRecord(val) {
|
|
1150
|
+
if (typeof val !== "object" || val === null) return false;
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
function projectServer(name, cfg, _status = "stopped", tools = []) {
|
|
1154
|
+
return {
|
|
1155
|
+
name,
|
|
1156
|
+
transport: cfg.transport,
|
|
1157
|
+
status: _status,
|
|
1158
|
+
enabled: cfg.enabled ?? true,
|
|
1159
|
+
description: cfg.description,
|
|
1160
|
+
tools
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
async function readConfig(configPath) {
|
|
1164
|
+
try {
|
|
1165
|
+
const content = await fs3.readFile(configPath, "utf-8");
|
|
1166
|
+
return JSON.parse(content);
|
|
1167
|
+
} catch {
|
|
1168
|
+
return {};
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
async function writeConfig(configPath, cfg) {
|
|
1172
|
+
const dir = path3.dirname(configPath);
|
|
1173
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
1174
|
+
await fs3.writeFile(configPath, JSON.stringify(cfg, null, 2), "utf-8");
|
|
1175
|
+
}
|
|
1176
|
+
async function getMcpServers(config, globalConfigPath) {
|
|
1177
|
+
const servers = [];
|
|
1178
|
+
const configured = isMcpServerRecord(config.mcpServers) ? config.mcpServers : {};
|
|
1179
|
+
for (const [name, cfg] of Object.entries(configured)) {
|
|
1180
|
+
servers.push(projectServer(name, cfg));
|
|
1181
|
+
}
|
|
1182
|
+
return servers;
|
|
1183
|
+
}
|
|
1184
|
+
function getRegistryStates(mcpRegistry) {
|
|
1185
|
+
const states = /* @__PURE__ */ new Map();
|
|
1186
|
+
if (!mcpRegistry?.list) return states;
|
|
1187
|
+
try {
|
|
1188
|
+
const list = mcpRegistry.list();
|
|
1189
|
+
for (const item of list) {
|
|
1190
|
+
states.set(item.name, { state: item.state, toolCount: item.toolCount });
|
|
1191
|
+
}
|
|
1192
|
+
} catch {
|
|
1193
|
+
}
|
|
1194
|
+
return states;
|
|
1195
|
+
}
|
|
1196
|
+
async function handleMcpList(ws, _msg, config, _globalConfigPath, mcpRegistry) {
|
|
1197
|
+
const servers = await getMcpServers(config, _globalConfigPath);
|
|
1198
|
+
const registryStates = getRegistryStates(mcpRegistry);
|
|
1199
|
+
for (const server of servers) {
|
|
1200
|
+
const registryState = registryStates.get(server.name);
|
|
1201
|
+
if (registryState) {
|
|
1202
|
+
server.status = registryState.state;
|
|
1203
|
+
server.tools = Array.from({ length: registryState.toolCount }, (_, i) => `tool-${i + 1}`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
send(ws, { type: "mcp.list", payload: { servers } });
|
|
1207
|
+
}
|
|
1208
|
+
async function handleMcpAdd(ws, msg, config, globalConfigPath, mcpRegistry) {
|
|
1209
|
+
const payload = msg.payload;
|
|
1210
|
+
if (!payload.name) {
|
|
1211
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
try {
|
|
1215
|
+
const diskConfig = await readConfig(globalConfigPath);
|
|
1216
|
+
const mcpServers = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : {};
|
|
1217
|
+
if (mcpServers[payload.name]) {
|
|
1218
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Server "${payload.name}" already exists` } });
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
mcpServers[payload.name] = {
|
|
1222
|
+
transport: payload.transport,
|
|
1223
|
+
description: payload.description,
|
|
1224
|
+
enabled: payload.enabled ?? true,
|
|
1225
|
+
command: payload.command,
|
|
1226
|
+
args: payload.args,
|
|
1227
|
+
env: payload.env,
|
|
1228
|
+
allowedTools: payload.allowedTools
|
|
1229
|
+
};
|
|
1230
|
+
diskConfig.mcpServers = mcpServers;
|
|
1231
|
+
await writeConfig(globalConfigPath, diskConfig);
|
|
1232
|
+
const newServer = projectServer(payload.name, mcpServers[payload.name]);
|
|
1233
|
+
send(ws, { type: "mcp.server.added", payload: { server: newServer } });
|
|
1234
|
+
if (mcpRegistry && (payload.enabled ?? true)) {
|
|
1235
|
+
const serverConfig = mcpServers[payload.name];
|
|
1236
|
+
try {
|
|
1237
|
+
await mcpRegistry.start({
|
|
1238
|
+
name: payload.name,
|
|
1239
|
+
transport: payload.transport,
|
|
1240
|
+
command: payload.command,
|
|
1241
|
+
args: payload.args,
|
|
1242
|
+
env: payload.env,
|
|
1243
|
+
allowedTools: payload.allowedTools,
|
|
1244
|
+
enabled: true
|
|
1245
|
+
});
|
|
1246
|
+
} catch (err) {
|
|
1247
|
+
send(ws, { type: "mcp.server.error", payload: { name: payload.name, error: String(err) } });
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" added` } });
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to add server: ${err}` } });
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
async function handleMcpRemove(ws, msg, _config, globalConfigPath, mcpRegistry) {
|
|
1256
|
+
const payload = msg.payload;
|
|
1257
|
+
if (!payload.name) {
|
|
1258
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
try {
|
|
1262
|
+
if (mcpRegistry) {
|
|
1263
|
+
try {
|
|
1264
|
+
await mcpRegistry.stop(payload.name);
|
|
1265
|
+
} catch {
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const diskConfig = await readConfig(globalConfigPath);
|
|
1269
|
+
const mcpServers = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : {};
|
|
1270
|
+
if (!mcpServers[payload.name]) {
|
|
1271
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Server "${payload.name}" not found` } });
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
delete mcpServers[payload.name];
|
|
1275
|
+
diskConfig.mcpServers = mcpServers;
|
|
1276
|
+
await writeConfig(globalConfigPath, diskConfig);
|
|
1277
|
+
send(ws, { type: "mcp.server.removed", payload: { name: payload.name } });
|
|
1278
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" removed` } });
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to remove server: ${err}` } });
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
async function handleMcpUpdate(ws, msg, _config, globalConfigPath) {
|
|
1284
|
+
const payload = msg.payload;
|
|
1285
|
+
if (!payload.name) {
|
|
1286
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
try {
|
|
1290
|
+
const diskConfig = await readConfig(globalConfigPath);
|
|
1291
|
+
const mcpServers = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : {};
|
|
1292
|
+
if (!mcpServers[payload.name]) {
|
|
1293
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Server "${payload.name}" not found` } });
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const existing = mcpServers[payload.name];
|
|
1297
|
+
mcpServers[payload.name] = {
|
|
1298
|
+
transport: payload.transport ?? existing.transport,
|
|
1299
|
+
description: payload.description ?? existing.description,
|
|
1300
|
+
enabled: payload.enabled ?? existing.enabled,
|
|
1301
|
+
command: payload.command ?? existing.command,
|
|
1302
|
+
args: payload.args ?? existing.args,
|
|
1303
|
+
env: payload.env ?? existing.env,
|
|
1304
|
+
allowedTools: payload.allowedTools ?? existing.allowedTools
|
|
1305
|
+
};
|
|
1306
|
+
diskConfig.mcpServers = mcpServers;
|
|
1307
|
+
await writeConfig(globalConfigPath, diskConfig);
|
|
1308
|
+
const updatedServer = projectServer(payload.name, mcpServers[payload.name]);
|
|
1309
|
+
send(ws, { type: "mcp.server.updated", payload: { server: updatedServer } });
|
|
1310
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" updated` } });
|
|
1311
|
+
} catch (err) {
|
|
1312
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to update server: ${err}` } });
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
async function handleMcpWake(ws, msg, _config, _globalConfigPath, mcpRegistry) {
|
|
1316
|
+
const payload = msg.payload;
|
|
1317
|
+
if (!payload.name) {
|
|
1318
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (!mcpRegistry) {
|
|
1322
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "MCP registry not available" } });
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
try {
|
|
1326
|
+
send(ws, { type: "mcp.server.waking", payload: { name: payload.name } });
|
|
1327
|
+
await mcpRegistry.restart(payload.name);
|
|
1328
|
+
send(ws, { type: "mcp.server.connected", payload: { name: payload.name } });
|
|
1329
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" restarted` } });
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
send(ws, { type: "mcp.server.error", payload: { name: payload.name, error: String(err) } });
|
|
1332
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to restart "${payload.name}": ${err}` } });
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
async function handleMcpSleep(ws, msg, _config, _globalConfigPath, mcpRegistry) {
|
|
1336
|
+
const payload = msg.payload;
|
|
1337
|
+
if (!payload.name) {
|
|
1338
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (!mcpRegistry) {
|
|
1342
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "MCP registry not available" } });
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
try {
|
|
1346
|
+
await mcpRegistry.stop(payload.name);
|
|
1347
|
+
send(ws, { type: "mcp.server.sleeping", payload: { name: payload.name } });
|
|
1348
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" stopped` } });
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
send(ws, { type: "mcp.server.error", payload: { name: payload.name, error: String(err) } });
|
|
1351
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to stop "${payload.name}": ${err}` } });
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async function handleMcpDiscover(ws, msg, _config, _globalConfigPath, _mcpRegistry) {
|
|
1355
|
+
const payload = msg.payload;
|
|
1356
|
+
if (!payload.name) {
|
|
1357
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
send(ws, { type: "mcp.server.discovered", payload: { name: payload.name, tools: [] } });
|
|
1361
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" tools were discovered on connect` } });
|
|
1362
|
+
}
|
|
1363
|
+
async function handleMcpEnable(ws, msg, _config, _globalConfigPath) {
|
|
1364
|
+
const payload = msg.payload;
|
|
1365
|
+
if (!payload.name) {
|
|
1366
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Enable command sent for "${payload.name}"` } });
|
|
1370
|
+
}
|
|
1371
|
+
async function handleMcpDisable(ws, msg, _config, _globalConfigPath) {
|
|
1372
|
+
const payload = msg.payload;
|
|
1373
|
+
if (!payload.name) {
|
|
1374
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1375
|
+
return;
|
|
554
1376
|
}
|
|
1377
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Disable command sent for "${payload.name}"` } });
|
|
1378
|
+
}
|
|
1379
|
+
async function handleMcpRestart(ws, msg, _config, _globalConfigPath) {
|
|
1380
|
+
const payload = msg.payload;
|
|
1381
|
+
if (!payload.name) {
|
|
1382
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Restart command sent for "${payload.name}"` } });
|
|
555
1386
|
}
|
|
556
1387
|
|
|
557
1388
|
// src/server/index.ts
|
|
@@ -711,6 +1542,7 @@ function patchConfig(config, updates) {
|
|
|
711
1542
|
|
|
712
1543
|
// src/server/autophase-ws-handler.ts
|
|
713
1544
|
import { spawnSync } from "child_process";
|
|
1545
|
+
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
714
1546
|
import {
|
|
715
1547
|
AutoPhasePlanner,
|
|
716
1548
|
PhaseGraphBuilder,
|
|
@@ -880,7 +1712,7 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
880
1712
|
);
|
|
881
1713
|
this.broadcastState();
|
|
882
1714
|
}).catch((err) => {
|
|
883
|
-
this.logger.error(`[AutoPhase] Aborted: ${
|
|
1715
|
+
this.logger.error(`[AutoPhase] Aborted: ${toErrorMessage(err)}`);
|
|
884
1716
|
this.stopBroadcast();
|
|
885
1717
|
this.broadcast({ type: "autophase.failed", payload: { title, error: String(err) } });
|
|
886
1718
|
});
|
|
@@ -913,7 +1745,7 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
913
1745
|
}
|
|
914
1746
|
this.logger.info(`[AutoPhase] Planner produced no phases; using defaults for: ${goal}`);
|
|
915
1747
|
} catch (err) {
|
|
916
|
-
this.logger.error(`[AutoPhase] Planning failed, using defaults: ${
|
|
1748
|
+
this.logger.error(`[AutoPhase] Planning failed, using defaults: ${toErrorMessage(err)}`);
|
|
917
1749
|
}
|
|
918
1750
|
return this.defaultPhases();
|
|
919
1751
|
}
|
|
@@ -1040,6 +1872,7 @@ Type: ${task.type}`;
|
|
|
1040
1872
|
|
|
1041
1873
|
// src/server/collaboration-ws-handler.ts
|
|
1042
1874
|
import { randomUUID } from "crypto";
|
|
1875
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
1043
1876
|
var REPLAY_LIMIT = 50;
|
|
1044
1877
|
var PAUSE_TIMEOUT_MS = 6e4;
|
|
1045
1878
|
var CollaborationWebSocketHandler = class {
|
|
@@ -1167,7 +2000,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1167
2000
|
if (this.reader) {
|
|
1168
2001
|
this.replayHistory(ws, sessionId).catch((err) => {
|
|
1169
2002
|
this.logger.debug?.(
|
|
1170
|
-
`collab: replay failed for ${sessionId}: ${
|
|
2003
|
+
`collab: replay failed for ${sessionId}: ${toErrorMessage2(err)}`
|
|
1171
2004
|
);
|
|
1172
2005
|
});
|
|
1173
2006
|
}
|
|
@@ -1277,7 +2110,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1277
2110
|
this.send(
|
|
1278
2111
|
ws,
|
|
1279
2112
|
this.errorMessage(
|
|
1280
|
-
`annotation rejected: ${
|
|
2113
|
+
`annotation rejected: ${toErrorMessage2(err)}`
|
|
1281
2114
|
)
|
|
1282
2115
|
);
|
|
1283
2116
|
}
|
|
@@ -1344,7 +2177,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1344
2177
|
this.send(
|
|
1345
2178
|
ws,
|
|
1346
2179
|
this.errorMessage(
|
|
1347
|
-
`resolve failed: ${
|
|
2180
|
+
`resolve failed: ${toErrorMessage2(err)}`
|
|
1348
2181
|
)
|
|
1349
2182
|
);
|
|
1350
2183
|
}
|
|
@@ -1392,7 +2225,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1392
2225
|
if (p.ws.readyState === 1) p.ws.send(data);
|
|
1393
2226
|
} catch (err) {
|
|
1394
2227
|
this.logger.debug?.(
|
|
1395
|
-
`collab broadcast failed: ${
|
|
2228
|
+
`collab broadcast failed: ${toErrorMessage2(err)}`
|
|
1396
2229
|
);
|
|
1397
2230
|
}
|
|
1398
2231
|
}
|
|
@@ -1419,7 +2252,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1419
2252
|
}
|
|
1420
2253
|
} catch (err) {
|
|
1421
2254
|
this.logger.debug?.(
|
|
1422
|
-
`collab: session reader rejected ${sessionId}: ${
|
|
2255
|
+
`collab: session reader rejected ${sessionId}: ${toErrorMessage2(err)}`
|
|
1423
2256
|
);
|
|
1424
2257
|
return;
|
|
1425
2258
|
}
|
|
@@ -1500,7 +2333,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1500
2333
|
if (p.ws.readyState === 1) p.ws.send(data);
|
|
1501
2334
|
} catch (err) {
|
|
1502
2335
|
this.logger.debug?.(
|
|
1503
|
-
`collab broadcast failed: ${
|
|
2336
|
+
`collab broadcast failed: ${toErrorMessage2(err)}`
|
|
1504
2337
|
);
|
|
1505
2338
|
}
|
|
1506
2339
|
}
|
|
@@ -1697,6 +2530,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1697
2530
|
};
|
|
1698
2531
|
|
|
1699
2532
|
// src/server/worktree-ws-handler.ts
|
|
2533
|
+
import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
|
|
1700
2534
|
var MAX_ACTIVITY = 6;
|
|
1701
2535
|
var WorktreeWebSocketHandler = class {
|
|
1702
2536
|
constructor(events, logger) {
|
|
@@ -1822,7 +2656,7 @@ var WorktreeWebSocketHandler = class {
|
|
|
1822
2656
|
try {
|
|
1823
2657
|
if (ws.readyState === 1) ws.send(data);
|
|
1824
2658
|
} catch (err) {
|
|
1825
|
-
this.logger.debug?.(`worktree broadcast failed: ${
|
|
2659
|
+
this.logger.debug?.(`worktree broadcast failed: ${toErrorMessage3(err)}`);
|
|
1826
2660
|
}
|
|
1827
2661
|
}
|
|
1828
2662
|
}
|
|
@@ -1835,22 +2669,14 @@ var WorktreeWebSocketHandler = class {
|
|
|
1835
2669
|
};
|
|
1836
2670
|
|
|
1837
2671
|
// src/server/mailbox-handlers.ts
|
|
1838
|
-
import
|
|
1839
|
-
import { GlobalMailbox } from "@wrongstack/core";
|
|
1840
|
-
function resolveProjectDir(projectRoot, globalRoot) {
|
|
1841
|
-
const { createHash } = __require("crypto");
|
|
1842
|
-
const hash = createHash("sha256").update(path3.resolve(projectRoot)).digest("hex").slice(0, 6);
|
|
1843
|
-
const slug = path3.basename(projectRoot).toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40) || "project";
|
|
1844
|
-
return path3.join(globalRoot, "projects", `${slug}-${hash}`);
|
|
1845
|
-
}
|
|
2672
|
+
import { GlobalMailbox, resolveProjectDir } from "@wrongstack/core";
|
|
1846
2673
|
async function handleMailboxMessages(ws, deps, payload) {
|
|
1847
2674
|
try {
|
|
1848
2675
|
const dir = resolveProjectDir(deps.projectRoot, deps.globalRoot);
|
|
1849
2676
|
const mb = new GlobalMailbox(dir);
|
|
1850
2677
|
const messages = await mb.query({
|
|
1851
2678
|
limit: payload?.limit ?? 30,
|
|
1852
|
-
|
|
1853
|
-
unreadBy: payload?.unreadOnly ? payload.agentId : void 0
|
|
2679
|
+
incompleteOnly: payload?.incompleteOnly ?? false
|
|
1854
2680
|
});
|
|
1855
2681
|
send(ws, {
|
|
1856
2682
|
type: "mailbox.messages",
|
|
@@ -1867,10 +2693,12 @@ async function handleMailboxMessages(ws, deps, payload) {
|
|
|
1867
2693
|
readByCount: Object.keys(m.readBy).length,
|
|
1868
2694
|
completed: m.completed,
|
|
1869
2695
|
completedBy: m.completedBy,
|
|
2696
|
+
completedAt: m.completedAt,
|
|
1870
2697
|
outcome: m.outcome,
|
|
1871
2698
|
timestamp: m.timestamp,
|
|
1872
2699
|
replyTo: m.replyTo,
|
|
1873
|
-
senderSessionId: m.senderSessionId
|
|
2700
|
+
senderSessionId: m.senderSessionId,
|
|
2701
|
+
taskContext: m.taskContext
|
|
1874
2702
|
}))
|
|
1875
2703
|
}
|
|
1876
2704
|
});
|
|
@@ -1917,6 +2745,16 @@ async function handleMailboxClear(ws, deps) {
|
|
|
1917
2745
|
send(ws, { type: "mailbox.cleared", payload: { error: errMessage(err) } });
|
|
1918
2746
|
}
|
|
1919
2747
|
}
|
|
2748
|
+
async function handleMailboxPurge(ws, deps, opts) {
|
|
2749
|
+
try {
|
|
2750
|
+
const dir = resolveProjectDir(deps.projectRoot, deps.globalRoot);
|
|
2751
|
+
const mb = new GlobalMailbox(dir);
|
|
2752
|
+
const result = await mb.purgeStale(opts);
|
|
2753
|
+
send(ws, { type: "mailbox.purged", payload: result });
|
|
2754
|
+
} catch (err) {
|
|
2755
|
+
send(ws, { type: "mailbox.purged", payload: { error: errMessage(err) } });
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
1920
2758
|
|
|
1921
2759
|
// src/server/lifecycle.ts
|
|
1922
2760
|
function createShutdown(res) {
|
|
@@ -1957,7 +2795,7 @@ function registerShutdownHandlers(res) {
|
|
|
1957
2795
|
// src/server/instance-registry.ts
|
|
1958
2796
|
import * as os from "os";
|
|
1959
2797
|
import * as path4 from "path";
|
|
1960
|
-
import * as
|
|
2798
|
+
import * as fs4 from "fs/promises";
|
|
1961
2799
|
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
1962
2800
|
function defaultBaseDir() {
|
|
1963
2801
|
return path4.join(os.homedir(), ".wrongstack");
|
|
@@ -1976,7 +2814,7 @@ function isPidAlive(pid) {
|
|
|
1976
2814
|
}
|
|
1977
2815
|
async function load(file) {
|
|
1978
2816
|
try {
|
|
1979
|
-
const raw = await
|
|
2817
|
+
const raw = await fs4.readFile(file, "utf8");
|
|
1980
2818
|
const parsed = JSON.parse(raw);
|
|
1981
2819
|
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
1982
2820
|
return parsed;
|
|
@@ -2035,16 +2873,16 @@ function formatInstances(instances) {
|
|
|
2035
2873
|
// src/server/port-utils.ts
|
|
2036
2874
|
import * as net from "net";
|
|
2037
2875
|
function isPortFree(host, port) {
|
|
2038
|
-
return new Promise((
|
|
2876
|
+
return new Promise((resolve5) => {
|
|
2039
2877
|
const srv = net.createServer();
|
|
2040
|
-
srv.once("error", () =>
|
|
2878
|
+
srv.once("error", () => resolve5(false));
|
|
2041
2879
|
srv.once("listening", () => {
|
|
2042
|
-
srv.close(() =>
|
|
2880
|
+
srv.close(() => resolve5(true));
|
|
2043
2881
|
});
|
|
2044
2882
|
try {
|
|
2045
2883
|
srv.listen(port, host);
|
|
2046
2884
|
} catch {
|
|
2047
|
-
|
|
2885
|
+
resolve5(false);
|
|
2048
2886
|
}
|
|
2049
2887
|
});
|
|
2050
2888
|
}
|
|
@@ -2119,8 +2957,12 @@ function computeUsageCost(usage, rates) {
|
|
|
2119
2957
|
return (usage.input * rates.input + usage.output * rates.output + (usage.cacheRead ?? 0) * rates.cacheRead) / 1e6;
|
|
2120
2958
|
}
|
|
2121
2959
|
|
|
2960
|
+
// src/server/provider-handlers.ts
|
|
2961
|
+
import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core";
|
|
2962
|
+
import { probeLocalLlm } from "@wrongstack/runtime/probe";
|
|
2963
|
+
|
|
2122
2964
|
// src/server/provider-config-io.ts
|
|
2123
|
-
import * as
|
|
2965
|
+
import * as fs5 from "fs/promises";
|
|
2124
2966
|
import * as path5 from "path";
|
|
2125
2967
|
import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
|
|
2126
2968
|
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
@@ -2128,7 +2970,7 @@ import { DefaultSecretVault } from "@wrongstack/core";
|
|
|
2128
2970
|
async function loadSavedProviders(configPath, vault) {
|
|
2129
2971
|
let raw;
|
|
2130
2972
|
try {
|
|
2131
|
-
raw = await
|
|
2973
|
+
raw = await fs5.readFile(configPath, "utf8");
|
|
2132
2974
|
} catch {
|
|
2133
2975
|
return {};
|
|
2134
2976
|
}
|
|
@@ -2145,7 +2987,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
2145
2987
|
let raw;
|
|
2146
2988
|
let fileExists = true;
|
|
2147
2989
|
try {
|
|
2148
|
-
raw = await
|
|
2990
|
+
raw = await fs5.readFile(configPath, "utf8");
|
|
2149
2991
|
} catch (err) {
|
|
2150
2992
|
if (err.code !== "ENOENT") {
|
|
2151
2993
|
throw new Error(
|
|
@@ -2173,6 +3015,9 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
2173
3015
|
await atomicWrite3(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
2174
3016
|
}
|
|
2175
3017
|
|
|
3018
|
+
// src/server/provider-handlers.ts
|
|
3019
|
+
import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
|
|
3020
|
+
|
|
2176
3021
|
// src/server/provider-keys.ts
|
|
2177
3022
|
import { expectDefined } from "@wrongstack/core";
|
|
2178
3023
|
function normalizeKeys(cfg) {
|
|
@@ -2193,7 +3038,7 @@ function writeKeysBack(cfg, keys) {
|
|
|
2193
3038
|
}
|
|
2194
3039
|
cfg.apiKeys = keys;
|
|
2195
3040
|
const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined(keys[0]);
|
|
2196
|
-
cfg.apiKey
|
|
3041
|
+
delete cfg.apiKey;
|
|
2197
3042
|
if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
|
|
2198
3043
|
cfg.activeKey = active.label;
|
|
2199
3044
|
}
|
|
@@ -2270,6 +3115,28 @@ function removeProvider(providers, providerId) {
|
|
|
2270
3115
|
}
|
|
2271
3116
|
|
|
2272
3117
|
// src/server/provider-handlers.ts
|
|
3118
|
+
function projectSavedProviders(providers) {
|
|
3119
|
+
return Object.entries(providers).map(([id, cfg]) => {
|
|
3120
|
+
const keys = normalizeKeys(cfg);
|
|
3121
|
+
const models = cfg.models;
|
|
3122
|
+
const view = {
|
|
3123
|
+
id,
|
|
3124
|
+
family: cfg.family ?? id,
|
|
3125
|
+
baseUrl: cfg.baseUrl,
|
|
3126
|
+
models,
|
|
3127
|
+
apiKeys: keys.map((k) => ({
|
|
3128
|
+
label: k.label,
|
|
3129
|
+
maskedKey: maskedKey(k.apiKey),
|
|
3130
|
+
isActive: k.label === cfg.activeKey,
|
|
3131
|
+
createdAt: k.createdAt
|
|
3132
|
+
}))
|
|
3133
|
+
};
|
|
3134
|
+
const picked = models && models.length > 0 ? models[0] : void 0;
|
|
3135
|
+
if (picked !== void 0) view.pickedModelId = picked;
|
|
3136
|
+
return view;
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
var probeScrubber = new DefaultSecretScrubber2();
|
|
2273
3140
|
function createProviderHandlers(deps) {
|
|
2274
3141
|
const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps;
|
|
2275
3142
|
let configWriteLock = deps.getConfigWriteLock();
|
|
@@ -2278,7 +3145,7 @@ function createProviderHandlers(deps) {
|
|
|
2278
3145
|
}
|
|
2279
3146
|
async function saveConfigProviders(providers) {
|
|
2280
3147
|
const next = configWriteLock.then(() => saveProviders(globalConfigPath, vault, providers)).catch((err) => {
|
|
2281
|
-
const msg =
|
|
3148
|
+
const msg = toErrorMessage4(err);
|
|
2282
3149
|
console.error(JSON.stringify({
|
|
2283
3150
|
level: "error",
|
|
2284
3151
|
event: "webui.provider_save_failed",
|
|
@@ -2295,9 +3162,9 @@ function createProviderHandlers(deps) {
|
|
|
2295
3162
|
const providers = await loadConfigProviders();
|
|
2296
3163
|
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
2297
3164
|
if (result.ok) await saveConfigProviders(providers);
|
|
2298
|
-
|
|
3165
|
+
sendResult2(ws, result.ok, result.message);
|
|
2299
3166
|
} catch (err) {
|
|
2300
|
-
|
|
3167
|
+
sendResult2(ws, false, errMessage(err));
|
|
2301
3168
|
}
|
|
2302
3169
|
}
|
|
2303
3170
|
async function handleKeyDelete(ws, providerId, label) {
|
|
@@ -2305,9 +3172,9 @@ function createProviderHandlers(deps) {
|
|
|
2305
3172
|
const providers = await loadConfigProviders();
|
|
2306
3173
|
const result = deleteKey(providers, providerId, label);
|
|
2307
3174
|
if (result.ok) await saveConfigProviders(providers);
|
|
2308
|
-
|
|
3175
|
+
sendResult2(ws, result.ok, result.message);
|
|
2309
3176
|
} catch (err) {
|
|
2310
|
-
|
|
3177
|
+
sendResult2(ws, false, errMessage(err));
|
|
2311
3178
|
}
|
|
2312
3179
|
}
|
|
2313
3180
|
async function handleKeySetActive(ws, providerId, label) {
|
|
@@ -2315,9 +3182,9 @@ function createProviderHandlers(deps) {
|
|
|
2315
3182
|
const providers = await loadConfigProviders();
|
|
2316
3183
|
const result = setActiveKey(providers, providerId, label);
|
|
2317
3184
|
if (result.ok) await saveConfigProviders(providers);
|
|
2318
|
-
|
|
3185
|
+
sendResult2(ws, result.ok, result.message);
|
|
2319
3186
|
} catch (err) {
|
|
2320
|
-
|
|
3187
|
+
sendResult2(ws, false, errMessage(err));
|
|
2321
3188
|
}
|
|
2322
3189
|
}
|
|
2323
3190
|
async function handleProviderAdd(ws, payload) {
|
|
@@ -2325,31 +3192,13 @@ function createProviderHandlers(deps) {
|
|
|
2325
3192
|
const providers = await loadConfigProviders();
|
|
2326
3193
|
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
2327
3194
|
if (result.ok) await saveConfigProviders(providers);
|
|
2328
|
-
|
|
3195
|
+
sendResult2(ws, result.ok, result.message);
|
|
2329
3196
|
if (result.ok) {
|
|
2330
3197
|
console.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
2331
|
-
|
|
2332
|
-
type: "providers.saved",
|
|
2333
|
-
payload: {
|
|
2334
|
-
providers: Object.entries(providers).map(([id, cfg]) => {
|
|
2335
|
-
const keys = normalizeKeys(cfg);
|
|
2336
|
-
return {
|
|
2337
|
-
id,
|
|
2338
|
-
family: cfg.family ?? id,
|
|
2339
|
-
baseUrl: cfg.baseUrl,
|
|
2340
|
-
apiKeys: keys.map((k) => ({
|
|
2341
|
-
label: k.label,
|
|
2342
|
-
maskedKey: maskedKey(k.apiKey),
|
|
2343
|
-
isActive: k.label === cfg.activeKey,
|
|
2344
|
-
createdAt: k.createdAt
|
|
2345
|
-
}))
|
|
2346
|
-
};
|
|
2347
|
-
})
|
|
2348
|
-
}
|
|
2349
|
-
});
|
|
3198
|
+
broadcastSaved(providers);
|
|
2350
3199
|
}
|
|
2351
3200
|
} catch (err) {
|
|
2352
|
-
|
|
3201
|
+
sendResult2(ws, false, errMessage(err));
|
|
2353
3202
|
}
|
|
2354
3203
|
}
|
|
2355
3204
|
async function handleProviderRemove(ws, providerId) {
|
|
@@ -2357,18 +3206,116 @@ function createProviderHandlers(deps) {
|
|
|
2357
3206
|
const providers = await loadConfigProviders();
|
|
2358
3207
|
const result = removeProvider(providers, providerId);
|
|
2359
3208
|
if (result.ok) await saveConfigProviders(providers);
|
|
2360
|
-
|
|
3209
|
+
sendResult2(ws, result.ok, result.message);
|
|
3210
|
+
} catch (err) {
|
|
3211
|
+
sendResult2(ws, false, errMessage(err));
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
function broadcastSaved(providers) {
|
|
3215
|
+
broadcast2(clients, {
|
|
3216
|
+
type: "providers.saved",
|
|
3217
|
+
payload: { providers: projectSavedProviders(providers) }
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
async function handleProviderClearModels(ws, providerId) {
|
|
3221
|
+
try {
|
|
3222
|
+
const providers = await loadConfigProviders();
|
|
3223
|
+
const cfg = providers[providerId];
|
|
3224
|
+
if (!cfg) {
|
|
3225
|
+
sendResult2(ws, false, `Unknown provider "${providerId}"`);
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
delete cfg.models;
|
|
3229
|
+
await saveConfigProviders(providers);
|
|
3230
|
+
sendResult2(ws, true, `Cleared model allowlist for ${providerId}`);
|
|
3231
|
+
broadcastSaved(providers);
|
|
3232
|
+
} catch (err) {
|
|
3233
|
+
sendResult2(ws, false, errMessage(err));
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
async function handleProviderUndoClear(ws, providerId, previousModels) {
|
|
3237
|
+
try {
|
|
3238
|
+
const providers = await loadConfigProviders();
|
|
3239
|
+
const cfg = providers[providerId];
|
|
3240
|
+
if (!cfg) {
|
|
3241
|
+
sendResult2(ws, false, `Unknown provider "${providerId}"`);
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
cfg.models = [...previousModels];
|
|
3245
|
+
await saveConfigProviders(providers);
|
|
3246
|
+
sendResult2(ws, true, `Restored ${previousModels.length} model(s) for ${providerId}`);
|
|
3247
|
+
broadcastSaved(providers);
|
|
3248
|
+
} catch (err) {
|
|
3249
|
+
sendResult2(ws, false, errMessage(err));
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
async function handleProviderUpdate(ws, payload) {
|
|
3253
|
+
try {
|
|
3254
|
+
const providers = await loadConfigProviders();
|
|
3255
|
+
const cfg = providers[payload.id];
|
|
3256
|
+
if (!cfg) {
|
|
3257
|
+
sendResult2(ws, false, `Unknown provider "${payload.id}"`);
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
if (payload.family !== void 0) cfg.family = payload.family;
|
|
3261
|
+
if (payload.baseUrl !== void 0) cfg.baseUrl = payload.baseUrl;
|
|
3262
|
+
if (payload.envVars !== void 0) cfg.envVars = payload.envVars;
|
|
3263
|
+
if (payload.models !== void 0) cfg.models = payload.models;
|
|
3264
|
+
await saveConfigProviders(providers);
|
|
3265
|
+
sendResult2(ws, true, `Updated ${payload.id}`);
|
|
3266
|
+
broadcastSaved(providers);
|
|
3267
|
+
} catch (err) {
|
|
3268
|
+
sendResult2(ws, false, errMessage(err));
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
async function handleProviderProbe(ws, providerId, timeoutMs) {
|
|
3272
|
+
const reply = (payload) => send(ws, { type: "provider.probe", payload: { providerId, ...payload } });
|
|
3273
|
+
try {
|
|
3274
|
+
const providers = await loadConfigProviders();
|
|
3275
|
+
const cfg = providers[providerId];
|
|
3276
|
+
if (!cfg) {
|
|
3277
|
+
reply({ ok: false, status: "no_provider" });
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3280
|
+
if (!cfg.baseUrl) {
|
|
3281
|
+
reply({ ok: false, status: "no_base_url" });
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
const keys = normalizeKeys(cfg);
|
|
3285
|
+
const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
|
|
3286
|
+
const result = await probeLocalLlm({
|
|
3287
|
+
baseUrl: cfg.baseUrl,
|
|
3288
|
+
apiKey: active?.apiKey,
|
|
3289
|
+
noAuth: false,
|
|
3290
|
+
scrubber: probeScrubber,
|
|
3291
|
+
...timeoutMs !== void 0 ? { timeoutMs } : {}
|
|
3292
|
+
});
|
|
3293
|
+
reply(result);
|
|
2361
3294
|
} catch (err) {
|
|
2362
|
-
|
|
3295
|
+
reply({ ok: false, status: "unreachable", detail: errMessage(err) });
|
|
2363
3296
|
}
|
|
2364
3297
|
}
|
|
2365
|
-
return {
|
|
3298
|
+
return {
|
|
3299
|
+
handleKeyUpsert,
|
|
3300
|
+
handleKeyDelete,
|
|
3301
|
+
handleKeySetActive,
|
|
3302
|
+
handleProviderAdd,
|
|
3303
|
+
handleProviderRemove,
|
|
3304
|
+
handleProviderClearModels,
|
|
3305
|
+
handleProviderUndoClear,
|
|
3306
|
+
handleProviderUpdate,
|
|
3307
|
+
handleProviderProbe,
|
|
3308
|
+
loadConfigProviders
|
|
3309
|
+
};
|
|
2366
3310
|
}
|
|
2367
3311
|
|
|
2368
3312
|
// src/server/setup-events.ts
|
|
3313
|
+
import * as fs6 from "fs/promises";
|
|
3314
|
+
import { watch as fsWatch } from "fs";
|
|
2369
3315
|
import * as path6 from "path";
|
|
2370
3316
|
function setupEvents(deps) {
|
|
2371
|
-
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge } = deps;
|
|
3317
|
+
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps;
|
|
3318
|
+
const disposers = [];
|
|
2372
3319
|
events.on("iteration.started", (e) => {
|
|
2373
3320
|
const maxIt = typeof context.meta["maxIterations"] === "number" ? context.meta["maxIterations"] : config.tools?.maxIterations ?? 100;
|
|
2374
3321
|
broadcast2(clients, {
|
|
@@ -2399,7 +3346,11 @@ function setupEvents(deps) {
|
|
|
2399
3346
|
events.on("tool.progress", (e) => {
|
|
2400
3347
|
broadcast2(clients, {
|
|
2401
3348
|
type: "tool.progress",
|
|
2402
|
-
|
|
3349
|
+
// Nested `event` shape — the client handler reads `payload.event?.text`
|
|
3350
|
+
// and early-returns on a falsy text, so a flat { eventType, text } payload
|
|
3351
|
+
// makes live tool progress (bash streaming, partial_output, warnings)
|
|
3352
|
+
// never render. Must match WSToolProgress and the CLI server.
|
|
3353
|
+
payload: { id: e.id, name: e.name, event: { type: e.event.type, text: e.event.text, data: e.event.data } }
|
|
2403
3354
|
});
|
|
2404
3355
|
sessionBridge?.append({
|
|
2405
3356
|
type: "tool_progress",
|
|
@@ -2565,20 +3516,165 @@ function setupEvents(deps) {
|
|
|
2565
3516
|
events.onPattern("brain.*", (eventName, payload) => {
|
|
2566
3517
|
broadcast2(clients, { type: "brain.event", payload: { event: eventName, ...payload } });
|
|
2567
3518
|
});
|
|
3519
|
+
events.on("client.status", async (e) => {
|
|
3520
|
+
broadcast2(clients, { type: "client.status_update", payload: e });
|
|
3521
|
+
if (wpaths?.projectStatus) {
|
|
3522
|
+
try {
|
|
3523
|
+
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
3524
|
+
const dir = path6.dirname(statusFile);
|
|
3525
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
3526
|
+
await fs6.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
|
|
3527
|
+
} catch (err) {
|
|
3528
|
+
console.error("[setup-events] Failed to write status.json:", err);
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
});
|
|
3532
|
+
if (wpaths?.projectStatus && wpaths.configDir) {
|
|
3533
|
+
const projectsDir = path6.join(wpaths.configDir, "projects");
|
|
3534
|
+
const knownProjectHashes = /* @__PURE__ */ new Set();
|
|
3535
|
+
const debounceTimers = /* @__PURE__ */ new Map();
|
|
3536
|
+
const DEBOUNCE_MS = 150;
|
|
3537
|
+
const pendingStatuses = /* @__PURE__ */ new Map();
|
|
3538
|
+
if (watcherMetrics) {
|
|
3539
|
+
watcherMetrics.fileChangesDetected = 0;
|
|
3540
|
+
watcherMetrics.filesProcessed = 0;
|
|
3541
|
+
watcherMetrics.broadcastsSent = 0;
|
|
3542
|
+
watcherMetrics.debounceResets = 0;
|
|
3543
|
+
watcherMetrics.totalDebounceDelayMs = 0;
|
|
3544
|
+
watcherMetrics.activeProjects = 0;
|
|
3545
|
+
watcherMetrics.averageDebounceDelayMs = 0;
|
|
3546
|
+
watcherMetrics.watcherActive = true;
|
|
3547
|
+
}
|
|
3548
|
+
const getAverageDebounceDelay = () => {
|
|
3549
|
+
if (!watcherMetrics || watcherMetrics.broadcastsSent === 0) return 0;
|
|
3550
|
+
return watcherMetrics.totalDebounceDelayMs / watcherMetrics.broadcastsSent;
|
|
3551
|
+
};
|
|
3552
|
+
const logWatcherMetrics = () => {
|
|
3553
|
+
if (!watcherMetrics) return;
|
|
3554
|
+
watcherMetrics.averageDebounceDelayMs = getAverageDebounceDelay();
|
|
3555
|
+
console.log(
|
|
3556
|
+
`[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`
|
|
3557
|
+
);
|
|
3558
|
+
};
|
|
3559
|
+
const metricsInterval = setInterval(logWatcherMetrics, 6e4);
|
|
3560
|
+
const broadcastStatus = (projectHash2, statusData, actualDelayMs) => {
|
|
3561
|
+
broadcast2(clients, { type: "client.status_update", payload: statusData });
|
|
3562
|
+
if (watcherMetrics) {
|
|
3563
|
+
watcherMetrics.broadcastsSent++;
|
|
3564
|
+
watcherMetrics.totalDebounceDelayMs += actualDelayMs;
|
|
3565
|
+
watcherMetrics.averageDebounceDelayMs = getAverageDebounceDelay();
|
|
3566
|
+
}
|
|
3567
|
+
};
|
|
3568
|
+
const scheduleBroadcast = (projectHash2, statusData) => {
|
|
3569
|
+
const now = Date.now();
|
|
3570
|
+
const existing = pendingStatuses.get(projectHash2);
|
|
3571
|
+
if (existing && watcherMetrics) {
|
|
3572
|
+
watcherMetrics.debounceResets++;
|
|
3573
|
+
}
|
|
3574
|
+
pendingStatuses.set(projectHash2, {
|
|
3575
|
+
data: statusData,
|
|
3576
|
+
firstWriteAt: existing ? existing.firstWriteAt : now
|
|
3577
|
+
});
|
|
3578
|
+
const existingTimer = debounceTimers.get(projectHash2);
|
|
3579
|
+
if (existingTimer) {
|
|
3580
|
+
clearTimeout(existingTimer);
|
|
3581
|
+
}
|
|
3582
|
+
const timer = setTimeout(() => {
|
|
3583
|
+
debounceTimers.delete(projectHash2);
|
|
3584
|
+
const pending = pendingStatuses.get(projectHash2);
|
|
3585
|
+
if (pending) {
|
|
3586
|
+
const actualDelay = Date.now() - pending.firstWriteAt;
|
|
3587
|
+
broadcastStatus(projectHash2, pending.data, actualDelay);
|
|
3588
|
+
pendingStatuses.delete(projectHash2);
|
|
3589
|
+
}
|
|
3590
|
+
}, DEBOUNCE_MS);
|
|
3591
|
+
debounceTimers.set(projectHash2, timer);
|
|
3592
|
+
};
|
|
3593
|
+
let watcher;
|
|
3594
|
+
const startWatcher = async () => {
|
|
3595
|
+
try {
|
|
3596
|
+
await fs6.mkdir(projectsDir, { recursive: true });
|
|
3597
|
+
watcher = fsWatch(projectsDir, { persistent: true, recursive: true }, async (eventType, filename) => {
|
|
3598
|
+
if (eventType === "change") {
|
|
3599
|
+
if (filename == null) return;
|
|
3600
|
+
if (watcherMetrics) watcherMetrics.fileChangesDetected++;
|
|
3601
|
+
const targetFile = path6.join(projectsDir, String(filename));
|
|
3602
|
+
if (targetFile.endsWith("status.json")) {
|
|
3603
|
+
const projectHash2 = path6.basename(path6.dirname(targetFile));
|
|
3604
|
+
if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
|
|
3605
|
+
return;
|
|
3606
|
+
}
|
|
3607
|
+
if (watcherMetrics) watcherMetrics.filesProcessed++;
|
|
3608
|
+
try {
|
|
3609
|
+
const content = await fs6.readFile(targetFile, "utf-8");
|
|
3610
|
+
const statusData = JSON.parse(content);
|
|
3611
|
+
if (statusData.projectHash) {
|
|
3612
|
+
const hash = String(statusData.projectHash);
|
|
3613
|
+
if (!knownProjectHashes.has(hash)) {
|
|
3614
|
+
knownProjectHashes.add(hash);
|
|
3615
|
+
if (watcherMetrics) watcherMetrics.activeProjects = knownProjectHashes.size;
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
scheduleBroadcast(projectHash2, statusData);
|
|
3619
|
+
} catch {
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
});
|
|
3624
|
+
console.log(`[setup-events] Watching ${projectsDir} for status.json changes (hash-filtered, debounced)`);
|
|
3625
|
+
} catch (err) {
|
|
3626
|
+
console.error("[setup-events] Failed to start status file watcher:", err);
|
|
3627
|
+
}
|
|
3628
|
+
};
|
|
3629
|
+
events.on("client.status", (e) => {
|
|
3630
|
+
if (e.projectHash) {
|
|
3631
|
+
const hash = String(e.projectHash);
|
|
3632
|
+
if (!knownProjectHashes.has(hash)) {
|
|
3633
|
+
knownProjectHashes.add(hash);
|
|
3634
|
+
if (watcherMetrics) watcherMetrics.activeProjects = knownProjectHashes.size;
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
});
|
|
3638
|
+
startWatcher();
|
|
3639
|
+
disposers.push(() => {
|
|
3640
|
+
clearInterval(metricsInterval);
|
|
3641
|
+
logWatcherMetrics();
|
|
3642
|
+
if (watcherMetrics) watcherMetrics.watcherActive = false;
|
|
3643
|
+
for (const [projectHash2, pending] of pendingStatuses) {
|
|
3644
|
+
const timer = debounceTimers.get(projectHash2);
|
|
3645
|
+
if (timer) {
|
|
3646
|
+
clearTimeout(timer);
|
|
3647
|
+
broadcastStatus(projectHash2, pending.data, 0);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
for (const timer of debounceTimers.values()) {
|
|
3651
|
+
clearTimeout(timer);
|
|
3652
|
+
}
|
|
3653
|
+
debounceTimers.clear();
|
|
3654
|
+
pendingStatuses.clear();
|
|
3655
|
+
if (watcher) {
|
|
3656
|
+
watcher.close();
|
|
3657
|
+
console.log("[setup-events] Closed status file watcher");
|
|
3658
|
+
}
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
2568
3661
|
const globalRoot = globalConfigPath ? path6.dirname(globalConfigPath) : void 0;
|
|
2569
3662
|
if (globalRoot) {
|
|
2570
|
-
const
|
|
3663
|
+
const broadcastSessions = async () => {
|
|
2571
3664
|
try {
|
|
2572
3665
|
const { SessionRegistry } = await import("@wrongstack/core");
|
|
2573
3666
|
const registry = new SessionRegistry(globalRoot);
|
|
2574
3667
|
const sessions = await registry.list();
|
|
2575
|
-
const
|
|
3668
|
+
const mySlug = sessions.find((s) => s.pid === process.pid)?.projectSlug;
|
|
3669
|
+
const live = sessions.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true).map((s) => ({
|
|
2576
3670
|
sessionId: s.sessionId,
|
|
2577
3671
|
projectName: s.projectName,
|
|
2578
3672
|
projectSlug: s.projectSlug,
|
|
2579
3673
|
projectRoot: s.projectRoot,
|
|
2580
3674
|
workingDir: s.workingDir,
|
|
2581
3675
|
gitBranch: s.gitBranch,
|
|
3676
|
+
// Surface (tui/webui/cli) so Fleet HQ can label each live client node.
|
|
3677
|
+
clientType: s.clientType,
|
|
2582
3678
|
status: s.status,
|
|
2583
3679
|
pid: s.pid,
|
|
2584
3680
|
startedAt: s.startedAt,
|
|
@@ -2590,20 +3686,52 @@ function setupEvents(deps) {
|
|
|
2590
3686
|
currentTool: a.currentTool,
|
|
2591
3687
|
iterations: a.iterations,
|
|
2592
3688
|
toolCalls: a.toolCalls,
|
|
3689
|
+
costUsd: a.costUsd,
|
|
3690
|
+
tokensIn: a.tokensIn,
|
|
3691
|
+
tokensOut: a.tokensOut,
|
|
3692
|
+
ctxPct: a.ctxPct,
|
|
3693
|
+
model: a.model,
|
|
3694
|
+
partialText: a.partialText,
|
|
2593
3695
|
lastActivityAt: a.lastActivityAt
|
|
2594
3696
|
}))
|
|
2595
3697
|
}));
|
|
2596
3698
|
broadcast2(clients, { type: "sessions.status_update", payload: { sessions: live } });
|
|
2597
3699
|
} catch {
|
|
2598
3700
|
}
|
|
2599
|
-
}
|
|
3701
|
+
};
|
|
3702
|
+
onFleetBroadcaster?.(broadcastSessions);
|
|
3703
|
+
const statusInterval = setInterval(() => void broadcastSessions(), 5e3);
|
|
2600
3704
|
if (statusInterval.unref) statusInterval.unref();
|
|
3705
|
+
disposers.push(() => clearInterval(statusInterval));
|
|
3706
|
+
let regDebounce;
|
|
3707
|
+
try {
|
|
3708
|
+
const regWatcher = fsWatch(globalRoot, { persistent: false }, (_event, filename) => {
|
|
3709
|
+
const name = filename ? String(filename) : "";
|
|
3710
|
+
if (!name.startsWith("session-registry.json") || name.endsWith(".lock")) return;
|
|
3711
|
+
if (regDebounce) clearTimeout(regDebounce);
|
|
3712
|
+
regDebounce = setTimeout(() => void broadcastSessions(), 150);
|
|
3713
|
+
});
|
|
3714
|
+
disposers.push(() => {
|
|
3715
|
+
if (regDebounce) clearTimeout(regDebounce);
|
|
3716
|
+
regWatcher.close();
|
|
3717
|
+
});
|
|
3718
|
+
} catch {
|
|
3719
|
+
}
|
|
3720
|
+
void broadcastSessions();
|
|
2601
3721
|
}
|
|
3722
|
+
return () => {
|
|
3723
|
+
for (const dispose of disposers) {
|
|
3724
|
+
try {
|
|
3725
|
+
dispose();
|
|
3726
|
+
} catch {
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
};
|
|
2602
3730
|
}
|
|
2603
3731
|
|
|
2604
3732
|
// src/server/custom-context-modes.ts
|
|
2605
3733
|
import { listContextWindowModes, atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
2606
|
-
import * as
|
|
3734
|
+
import * as fs7 from "fs/promises";
|
|
2607
3735
|
import * as path7 from "path";
|
|
2608
3736
|
var STORE_FILENAME = "custom-context-modes.json";
|
|
2609
3737
|
function storePath(wrongstackDir) {
|
|
@@ -2615,7 +3743,7 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
2615
3743
|
const load2 = async () => {
|
|
2616
3744
|
modes.clear();
|
|
2617
3745
|
try {
|
|
2618
|
-
const raw = await
|
|
3746
|
+
const raw = await fs7.readFile(storePath(wrongstackDir), "utf8");
|
|
2619
3747
|
const parsed = JSON.parse(raw);
|
|
2620
3748
|
if (Array.isArray(parsed.modes)) {
|
|
2621
3749
|
for (const m of parsed.modes) {
|
|
@@ -2795,14 +3923,14 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
|
|
|
2795
3923
|
}
|
|
2796
3924
|
|
|
2797
3925
|
// src/server/shell-open.ts
|
|
2798
|
-
import * as
|
|
3926
|
+
import * as fs8 from "fs/promises";
|
|
2799
3927
|
import * as path8 from "path";
|
|
2800
3928
|
import { spawn as spawn2 } from "child_process";
|
|
2801
3929
|
var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
|
|
2802
3930
|
async function handleShellOpen(req, logger) {
|
|
2803
3931
|
try {
|
|
2804
3932
|
const resolved = path8.resolve(req.path);
|
|
2805
|
-
await
|
|
3933
|
+
await fs8.access(resolved);
|
|
2806
3934
|
if (METACHAR_REGEX.test(resolved)) {
|
|
2807
3935
|
return { success: false, message: "Path contains unsupported characters." };
|
|
2808
3936
|
}
|
|
@@ -2848,6 +3976,43 @@ async function handleShellOpen(req, logger) {
|
|
|
2848
3976
|
}
|
|
2849
3977
|
}
|
|
2850
3978
|
|
|
3979
|
+
// src/server/git-handlers.ts
|
|
3980
|
+
async function handleGitInfo(ws, projectRoot) {
|
|
3981
|
+
const cwd = projectRoot || void 0;
|
|
3982
|
+
try {
|
|
3983
|
+
const { execFile: ef } = await import("child_process");
|
|
3984
|
+
const git = (args) => new Promise((resolve5) => {
|
|
3985
|
+
ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
|
|
3986
|
+
resolve5(err ? "" : stdout.trim());
|
|
3987
|
+
});
|
|
3988
|
+
});
|
|
3989
|
+
const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
|
|
3990
|
+
git(["branch", "--show-current"]),
|
|
3991
|
+
git(["diff", "--stat"]),
|
|
3992
|
+
git(["status", "--porcelain"]),
|
|
3993
|
+
git(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"])
|
|
3994
|
+
]);
|
|
3995
|
+
const branch = branchRaw || "(detached)";
|
|
3996
|
+
const addMatch = /(\d+)\s+insertion/i.exec(diffRaw);
|
|
3997
|
+
const delMatch = /(\d+)\s+deletion/i.exec(diffRaw);
|
|
3998
|
+
const added = addMatch ? Number(addMatch[1]) : 0;
|
|
3999
|
+
const deleted = delMatch ? Number(delMatch[1]) : 0;
|
|
4000
|
+
const untracked = statusRaw.split("\n").filter((l) => l.startsWith("??")).length;
|
|
4001
|
+
const [behindRaw, aheadRaw] = (upstreamRaw || "0 0").split(" ");
|
|
4002
|
+
const behind = Number(behindRaw) || 0;
|
|
4003
|
+
const ahead = Number(aheadRaw) || 0;
|
|
4004
|
+
send(ws, { type: "git.info", payload: { branch, added, deleted, untracked, ahead, behind } });
|
|
4005
|
+
} catch {
|
|
4006
|
+
send(ws, { type: "git.info", payload: { branch: "", added: 0, deleted: 0, untracked: 0, ahead: 0, behind: 0 } });
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
// src/server/skills-handlers.ts
|
|
4011
|
+
import { promises as fs9 } from "fs";
|
|
4012
|
+
import path9 from "path";
|
|
4013
|
+
import JSZip from "jszip";
|
|
4014
|
+
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
4015
|
+
|
|
2851
4016
|
// src/server/index.ts
|
|
2852
4017
|
async function startWebUI(opts = {}) {
|
|
2853
4018
|
const requestedWsPort = opts.wsPort ?? 3457;
|
|
@@ -2913,7 +4078,7 @@ async function startWebUI(opts = {}) {
|
|
|
2913
4078
|
console.warn(JSON.stringify({
|
|
2914
4079
|
level: "warn",
|
|
2915
4080
|
event: "webui.provider_registry_load_failed",
|
|
2916
|
-
message:
|
|
4081
|
+
message: toErrorMessage5(err),
|
|
2917
4082
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2918
4083
|
}));
|
|
2919
4084
|
}
|
|
@@ -2962,15 +4127,22 @@ async function startWebUI(opts = {}) {
|
|
|
2962
4127
|
sessionId: session.id,
|
|
2963
4128
|
projectSlug: wpaths.projectSlug,
|
|
2964
4129
|
projectRoot,
|
|
2965
|
-
projectName:
|
|
4130
|
+
projectName: path10.basename(projectRoot),
|
|
2966
4131
|
workingDir,
|
|
4132
|
+
clientType: "webui",
|
|
2967
4133
|
pid: process.pid,
|
|
2968
4134
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2969
4135
|
});
|
|
2970
|
-
|
|
4136
|
+
const fleetNotifier = new FleetNotifier({
|
|
4137
|
+
baseDir: wpaths.globalRoot,
|
|
4138
|
+
projectRoot,
|
|
4139
|
+
selfPid: process.pid
|
|
4140
|
+
});
|
|
4141
|
+
statusTracker = new AgentStatusTracker({ events, registry, onUpdate: () => fleetNotifier.notify() });
|
|
2971
4142
|
statusTracker.start();
|
|
2972
4143
|
const stopTracking = async () => {
|
|
2973
4144
|
try {
|
|
4145
|
+
fleetNotifier.dispose();
|
|
2974
4146
|
await registry.markClosing();
|
|
2975
4147
|
statusTracker?.stop();
|
|
2976
4148
|
} catch {
|
|
@@ -3010,6 +4182,13 @@ async function startWebUI(opts = {}) {
|
|
|
3010
4182
|
supportsReasoning: resolvedModel.capabilities.reasoning
|
|
3011
4183
|
} : void 0;
|
|
3012
4184
|
const skillLoader = config.features.skills ? new DefaultSkillLoader2({ paths: wpaths }) : void 0;
|
|
4185
|
+
const skillInstaller = config.features.skills ? new SkillInstaller({
|
|
4186
|
+
manifestPath: path10.join(wstackGlobalRoot2(), "installed-skills.json"),
|
|
4187
|
+
projectSkillsDir: path10.join(projectRoot, ".wrongstack", "skills"),
|
|
4188
|
+
globalSkillsDir: path10.join(wstackGlobalRoot2(), "skills"),
|
|
4189
|
+
projectHash: projectHash(projectRoot),
|
|
4190
|
+
skillLoader
|
|
4191
|
+
}) : void 0;
|
|
3013
4192
|
const systemPromptBuilder = new DefaultSystemPromptBuilder2({
|
|
3014
4193
|
memoryStore,
|
|
3015
4194
|
skillLoader,
|
|
@@ -3050,7 +4229,7 @@ async function startWebUI(opts = {}) {
|
|
|
3050
4229
|
console.error(JSON.stringify({
|
|
3051
4230
|
level: "error",
|
|
3052
4231
|
event: "webui.provider_create_failed",
|
|
3053
|
-
message:
|
|
4232
|
+
message: toErrorMessage5(err),
|
|
3054
4233
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3055
4234
|
}));
|
|
3056
4235
|
throw err;
|
|
@@ -3072,14 +4251,14 @@ async function startWebUI(opts = {}) {
|
|
|
3072
4251
|
console.error(JSON.stringify({
|
|
3073
4252
|
level: "error",
|
|
3074
4253
|
event: "webui.provider_stub_create_failed",
|
|
3075
|
-
message:
|
|
4254
|
+
message: toErrorMessage5(err),
|
|
3076
4255
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3077
4256
|
}));
|
|
3078
4257
|
throw err;
|
|
3079
4258
|
}
|
|
3080
4259
|
} else {
|
|
3081
4260
|
throw new Error(
|
|
3082
|
-
"No provider configured. Run `wrongstack
|
|
4261
|
+
"No provider configured. Run `wrongstack auth` to set up, or configure via the WebUI."
|
|
3083
4262
|
);
|
|
3084
4263
|
}
|
|
3085
4264
|
}
|
|
@@ -3121,6 +4300,12 @@ async function startWebUI(opts = {}) {
|
|
|
3121
4300
|
context.meta["logLevel"] = config.log?.level ?? "info";
|
|
3122
4301
|
context.meta["auditLevel"] = config.session?.auditLevel ?? "standard";
|
|
3123
4302
|
context.meta["maxIterations"] = config.tools?.maxIterations ?? 500;
|
|
4303
|
+
const tgExt = config.extensions?.["telegram"];
|
|
4304
|
+
context.meta["tgConfigured"] = typeof tgExt?.["botToken"] === "string" && tgExt["botToken"].length > 0;
|
|
4305
|
+
context.meta["tgSessionEnd"] = tgExt?.["notifyOnSessionEnd"] === true;
|
|
4306
|
+
context.meta["tgDelegate"] = tgExt?.["notifyOnDelegate"] !== false;
|
|
4307
|
+
const tgMs = tgExt?.["longToolThresholdMs"];
|
|
4308
|
+
context.meta["tgLongToolMs"] = typeof tgMs === "number" ? tgMs : 3e4;
|
|
3124
4309
|
}
|
|
3125
4310
|
const PREF_KEYS = [
|
|
3126
4311
|
"autonomy",
|
|
@@ -3144,7 +4329,11 @@ async function startWebUI(opts = {}) {
|
|
|
3144
4329
|
"contextAutoCompact",
|
|
3145
4330
|
"contextStrategy",
|
|
3146
4331
|
"logLevel",
|
|
3147
|
-
"auditLevel"
|
|
4332
|
+
"auditLevel",
|
|
4333
|
+
"tgConfigured",
|
|
4334
|
+
"tgSessionEnd",
|
|
4335
|
+
"tgDelegate",
|
|
4336
|
+
"tgLongToolMs"
|
|
3148
4337
|
];
|
|
3149
4338
|
const prefSnapshot = () => {
|
|
3150
4339
|
const snapshot = {};
|
|
@@ -3157,7 +4346,7 @@ async function startWebUI(opts = {}) {
|
|
|
3157
4346
|
const write = async () => {
|
|
3158
4347
|
let raw;
|
|
3159
4348
|
try {
|
|
3160
|
-
raw = await
|
|
4349
|
+
raw = await fs10.readFile(globalConfigPath, "utf8");
|
|
3161
4350
|
} catch {
|
|
3162
4351
|
raw = "{}";
|
|
3163
4352
|
}
|
|
@@ -3229,6 +4418,22 @@ async function startWebUI(opts = {}) {
|
|
|
3229
4418
|
toolsCfg.maxIterations = payload["maxIterations"];
|
|
3230
4419
|
decrypted.tools = toolsCfg;
|
|
3231
4420
|
}
|
|
4421
|
+
const tgTouched = typeof payload["tgSessionEnd"] === "boolean" || typeof payload["tgDelegate"] === "boolean" || typeof payload["tgLongToolMs"] === "number";
|
|
4422
|
+
if (tgTouched) {
|
|
4423
|
+
const ext = decrypted.extensions ?? {};
|
|
4424
|
+
const tg = ext["telegram"] ?? {};
|
|
4425
|
+
if (typeof payload["tgSessionEnd"] === "boolean") {
|
|
4426
|
+
tg["notifyOnSessionEnd"] = payload["tgSessionEnd"];
|
|
4427
|
+
}
|
|
4428
|
+
if (typeof payload["tgDelegate"] === "boolean") {
|
|
4429
|
+
tg["notifyOnDelegate"] = payload["tgDelegate"];
|
|
4430
|
+
}
|
|
4431
|
+
if (typeof payload["tgLongToolMs"] === "number") {
|
|
4432
|
+
tg["longToolThresholdMs"] = payload["tgLongToolMs"];
|
|
4433
|
+
}
|
|
4434
|
+
ext["telegram"] = tg;
|
|
4435
|
+
decrypted.extensions = ext;
|
|
4436
|
+
}
|
|
3232
4437
|
const encrypted = encryptConfigSecrets2(decrypted, vault);
|
|
3233
4438
|
await atomicWrite5(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
3234
4439
|
};
|
|
@@ -3450,7 +4655,7 @@ async function startWebUI(opts = {}) {
|
|
|
3450
4655
|
inputCost,
|
|
3451
4656
|
outputCost,
|
|
3452
4657
|
cacheReadCost,
|
|
3453
|
-
projectName:
|
|
4658
|
+
projectName: path10.basename(projectRoot) || projectRoot,
|
|
3454
4659
|
projectRoot,
|
|
3455
4660
|
cwd: workingDir,
|
|
3456
4661
|
mode: modeId,
|
|
@@ -3504,10 +4709,11 @@ async function startWebUI(opts = {}) {
|
|
|
3504
4709
|
const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
|
|
3505
4710
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
3506
4711
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
3507
|
-
|
|
4712
|
+
let connSeq = 0;
|
|
4713
|
+
function checkRateLimit(_ws, client) {
|
|
3508
4714
|
if (RATE_LIMIT_MESSAGES <= 0) return true;
|
|
3509
4715
|
const now = Date.now();
|
|
3510
|
-
const key = client.
|
|
4716
|
+
const key = client.connId;
|
|
3511
4717
|
const limit = rateLimits.get(key);
|
|
3512
4718
|
if (!limit || now > limit.resetAt) {
|
|
3513
4719
|
rateLimits.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
|
|
@@ -3523,7 +4729,12 @@ async function startWebUI(opts = {}) {
|
|
|
3523
4729
|
);
|
|
3524
4730
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
3525
4731
|
const handleConnection = (ws) => {
|
|
3526
|
-
const client = {
|
|
4732
|
+
const client = {
|
|
4733
|
+
ws,
|
|
4734
|
+
sessionId: session.id,
|
|
4735
|
+
connectedAt: Date.now(),
|
|
4736
|
+
connId: `c${++connSeq}`
|
|
4737
|
+
};
|
|
3527
4738
|
clients.set(ws, client);
|
|
3528
4739
|
void sessionStartPayload().then((payload) => {
|
|
3529
4740
|
send(ws, { type: "session.start", payload });
|
|
@@ -3531,7 +4742,7 @@ async function startWebUI(opts = {}) {
|
|
|
3531
4742
|
console.warn(JSON.stringify({
|
|
3532
4743
|
level: "warn",
|
|
3533
4744
|
event: "webui.session_start_payload_failed",
|
|
3534
|
-
message:
|
|
4745
|
+
message: toErrorMessage5(err),
|
|
3535
4746
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3536
4747
|
}));
|
|
3537
4748
|
});
|
|
@@ -3553,7 +4764,7 @@ async function startWebUI(opts = {}) {
|
|
|
3553
4764
|
const rawObj = JSON.parse(data.toString());
|
|
3554
4765
|
if (typeof rawObj === "object" && rawObj !== null) {
|
|
3555
4766
|
const obj = rawObj;
|
|
3556
|
-
if ("__proto__"
|
|
4767
|
+
if (Object.hasOwn(obj, "__proto__") || Object.hasOwn(obj, "constructor") || Object.hasOwn(obj, "prototype")) {
|
|
3557
4768
|
send(ws, {
|
|
3558
4769
|
type: "error",
|
|
3559
4770
|
payload: { phase: "parse", message: "Invalid message object" }
|
|
@@ -3568,17 +4779,18 @@ async function startWebUI(opts = {}) {
|
|
|
3568
4779
|
console.error(JSON.stringify({
|
|
3569
4780
|
level: "error",
|
|
3570
4781
|
event: "webui.ws_message_parse_failed",
|
|
3571
|
-
message:
|
|
4782
|
+
message: toErrorMessage5(err),
|
|
3572
4783
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3573
4784
|
}));
|
|
3574
4785
|
}
|
|
3575
4786
|
});
|
|
3576
4787
|
ws.on("close", () => {
|
|
4788
|
+
const closing = clients.get(ws);
|
|
3577
4789
|
clients.delete(ws);
|
|
3578
|
-
rateLimits.delete(
|
|
4790
|
+
if (closing) rateLimits.delete(closing.connId);
|
|
3579
4791
|
if (pendingConfirms.size > 0) {
|
|
3580
|
-
for (const [id,
|
|
3581
|
-
|
|
4792
|
+
for (const [id, resolve5] of pendingConfirms) {
|
|
4793
|
+
resolve5("no");
|
|
3582
4794
|
pendingConfirms.delete(id);
|
|
3583
4795
|
}
|
|
3584
4796
|
}
|
|
@@ -3601,11 +4813,27 @@ async function startWebUI(opts = {}) {
|
|
|
3601
4813
|
{ sampling: sessionLogging.sampling }
|
|
3602
4814
|
);
|
|
3603
4815
|
let eventsArmed = false;
|
|
4816
|
+
let disposeEvents = null;
|
|
4817
|
+
let fleetBroadcast = null;
|
|
3604
4818
|
const armOnce = (label) => {
|
|
3605
4819
|
if (eventsArmed) return;
|
|
3606
4820
|
eventsArmed = true;
|
|
3607
4821
|
console.log(`[WebUI] Backend ready (${label})`);
|
|
3608
|
-
setupEvents({
|
|
4822
|
+
disposeEvents = setupEvents({
|
|
4823
|
+
events,
|
|
4824
|
+
broadcast,
|
|
4825
|
+
clients,
|
|
4826
|
+
config,
|
|
4827
|
+
context,
|
|
4828
|
+
pendingConfirms,
|
|
4829
|
+
globalConfigPath,
|
|
4830
|
+
sessionBridge,
|
|
4831
|
+
wpaths,
|
|
4832
|
+
watcherMetrics,
|
|
4833
|
+
onFleetBroadcaster: (fn) => {
|
|
4834
|
+
fleetBroadcast = fn;
|
|
4835
|
+
}
|
|
4836
|
+
});
|
|
3609
4837
|
};
|
|
3610
4838
|
wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
|
|
3611
4839
|
wssPrimary.on("connection", handleConnection);
|
|
@@ -3614,7 +4842,7 @@ async function startWebUI(opts = {}) {
|
|
|
3614
4842
|
level: "error",
|
|
3615
4843
|
event: "webui.ws_server_error",
|
|
3616
4844
|
host: wsHost,
|
|
3617
|
-
message:
|
|
4845
|
+
message: toErrorMessage5(err),
|
|
3618
4846
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3619
4847
|
}));
|
|
3620
4848
|
});
|
|
@@ -3642,33 +4870,33 @@ async function startWebUI(opts = {}) {
|
|
|
3642
4870
|
});
|
|
3643
4871
|
}
|
|
3644
4872
|
async function touchProjectEntry(root, workDir) {
|
|
3645
|
-
const resolved =
|
|
4873
|
+
const resolved = path10.resolve(root);
|
|
3646
4874
|
const manifest = await loadManifest(globalConfigPath);
|
|
3647
4875
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3648
|
-
const existing = manifest.projects.find((p) =>
|
|
4876
|
+
const existing = manifest.projects.find((p) => path10.resolve(p.root) === resolved);
|
|
3649
4877
|
if (existing) {
|
|
3650
4878
|
existing.lastSeen = now;
|
|
3651
|
-
if (workDir) existing.lastWorkingDir =
|
|
4879
|
+
if (workDir) existing.lastWorkingDir = path10.resolve(workDir);
|
|
3652
4880
|
} else {
|
|
3653
4881
|
manifest.projects.push({
|
|
3654
|
-
name:
|
|
4882
|
+
name: path10.basename(resolved),
|
|
3655
4883
|
root: resolved,
|
|
3656
4884
|
slug: generateProjectSlug(resolved),
|
|
3657
4885
|
createdAt: now,
|
|
3658
4886
|
lastSeen: now,
|
|
3659
|
-
lastWorkingDir: workDir ?
|
|
4887
|
+
lastWorkingDir: workDir ? path10.resolve(workDir) : void 0
|
|
3660
4888
|
});
|
|
3661
4889
|
}
|
|
3662
4890
|
await saveManifest(manifest, globalConfigPath);
|
|
3663
4891
|
await ensureProjectDataDir(generateProjectSlug(resolved), globalConfigPath);
|
|
3664
4892
|
}
|
|
3665
4893
|
function projectsJsonPath(globalConfigPath2) {
|
|
3666
|
-
const base =
|
|
3667
|
-
return
|
|
4894
|
+
const base = path10.dirname(globalConfigPath2);
|
|
4895
|
+
return path10.join(base, "projects.json");
|
|
3668
4896
|
}
|
|
3669
4897
|
async function loadManifest(globalConfigPath2) {
|
|
3670
4898
|
try {
|
|
3671
|
-
const raw = await
|
|
4899
|
+
const raw = await fs10.readFile(projectsJsonPath(globalConfigPath2), "utf8");
|
|
3672
4900
|
const parsed = JSON.parse(raw);
|
|
3673
4901
|
return { projects: parsed.projects ?? [] };
|
|
3674
4902
|
} catch {
|
|
@@ -3677,16 +4905,16 @@ async function startWebUI(opts = {}) {
|
|
|
3677
4905
|
}
|
|
3678
4906
|
async function saveManifest(manifest, globalConfigPath2) {
|
|
3679
4907
|
const file = projectsJsonPath(globalConfigPath2);
|
|
3680
|
-
await
|
|
3681
|
-
await
|
|
4908
|
+
await fs10.mkdir(path10.dirname(file), { recursive: true });
|
|
4909
|
+
await fs10.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
3682
4910
|
}
|
|
3683
4911
|
function generateProjectSlug(rootPath) {
|
|
3684
4912
|
return projectSlug(rootPath);
|
|
3685
4913
|
}
|
|
3686
4914
|
async function ensureProjectDataDir(slug, globalConfigPath2) {
|
|
3687
|
-
const base =
|
|
3688
|
-
const dir =
|
|
3689
|
-
await
|
|
4915
|
+
const base = path10.dirname(globalConfigPath2);
|
|
4916
|
+
const dir = path10.join(base, "projects", slug);
|
|
4917
|
+
await fs10.mkdir(dir, { recursive: true });
|
|
3690
4918
|
return dir;
|
|
3691
4919
|
}
|
|
3692
4920
|
async function handleMessage(ws, _client, msg) {
|
|
@@ -3697,7 +4925,9 @@ async function startWebUI(opts = {}) {
|
|
|
3697
4925
|
case "collab.join":
|
|
3698
4926
|
case "collab.leave":
|
|
3699
4927
|
case "collab.annotate":
|
|
3700
|
-
case "collab.resolve":
|
|
4928
|
+
case "collab.resolve":
|
|
4929
|
+
case "collab.request_pause":
|
|
4930
|
+
case "collab.resume": {
|
|
3701
4931
|
collabHandler.handleMessage(ws, msg);
|
|
3702
4932
|
return;
|
|
3703
4933
|
}
|
|
@@ -3748,10 +4978,10 @@ async function startWebUI(opts = {}) {
|
|
|
3748
4978
|
}
|
|
3749
4979
|
case "tool.confirm_result": {
|
|
3750
4980
|
const { id, decision } = msg.payload;
|
|
3751
|
-
const
|
|
3752
|
-
if (
|
|
4981
|
+
const resolve5 = pendingConfirms.get(id);
|
|
4982
|
+
if (resolve5) {
|
|
3753
4983
|
pendingConfirms.delete(id);
|
|
3754
|
-
|
|
4984
|
+
resolve5(decision);
|
|
3755
4985
|
}
|
|
3756
4986
|
break;
|
|
3757
4987
|
}
|
|
@@ -3794,7 +5024,7 @@ async function startWebUI(opts = {}) {
|
|
|
3794
5024
|
context.readFiles.clear();
|
|
3795
5025
|
context.fileMtimes.clear();
|
|
3796
5026
|
tokenCounter.reset();
|
|
3797
|
-
|
|
5027
|
+
sendResult2(ws, true, "Context cleared");
|
|
3798
5028
|
broadcast(clients, {
|
|
3799
5029
|
type: "session.start",
|
|
3800
5030
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
@@ -3831,13 +5061,13 @@ async function startWebUI(opts = {}) {
|
|
|
3831
5061
|
repaired: report.repaired
|
|
3832
5062
|
}
|
|
3833
5063
|
});
|
|
3834
|
-
|
|
5064
|
+
sendResult2(
|
|
3835
5065
|
ws,
|
|
3836
5066
|
true,
|
|
3837
5067
|
`Compacted: ${report.before} \u2192 ${report.after} tokens (saved ~${Math.max(0, report.before - report.after)})`
|
|
3838
5068
|
);
|
|
3839
5069
|
} catch (err) {
|
|
3840
|
-
|
|
5070
|
+
sendResult2(ws, false, errMessage(err));
|
|
3841
5071
|
}
|
|
3842
5072
|
break;
|
|
3843
5073
|
}
|
|
@@ -3856,7 +5086,7 @@ async function startWebUI(opts = {}) {
|
|
|
3856
5086
|
};
|
|
3857
5087
|
broadcast(clients, { type: "context.repaired", payload });
|
|
3858
5088
|
const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
|
|
3859
|
-
|
|
5089
|
+
sendResult2(
|
|
3860
5090
|
ws,
|
|
3861
5091
|
true,
|
|
3862
5092
|
removed > 0 ? `Context repaired: removed ${removed} orphan protocol item(s)` : "Context repair found no orphan protocol blocks"
|
|
@@ -3890,14 +5120,14 @@ async function startWebUI(opts = {}) {
|
|
|
3890
5120
|
);
|
|
3891
5121
|
const custom = customModes.find((m) => m.id === id);
|
|
3892
5122
|
if (!custom) {
|
|
3893
|
-
|
|
5123
|
+
sendResult2(ws, false, `Unknown context mode "${id}"`);
|
|
3894
5124
|
break;
|
|
3895
5125
|
}
|
|
3896
5126
|
policy = custom;
|
|
3897
5127
|
}
|
|
3898
5128
|
context.meta["contextWindowMode"] = policy.id;
|
|
3899
5129
|
context.meta["contextWindowPolicy"] = policy;
|
|
3900
|
-
|
|
5130
|
+
sendResult2(ws, true, `Context mode switched to ${policy.id}`);
|
|
3901
5131
|
broadcast(clients, {
|
|
3902
5132
|
type: "context.mode.changed",
|
|
3903
5133
|
payload: { id: policy.id, name: policy.name, policy }
|
|
@@ -3917,7 +5147,7 @@ async function startWebUI(opts = {}) {
|
|
|
3917
5147
|
aggressiveOn: "soft",
|
|
3918
5148
|
targetLoad: 0.65
|
|
3919
5149
|
});
|
|
3920
|
-
|
|
5150
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${payload.id}" created`);
|
|
3921
5151
|
break;
|
|
3922
5152
|
}
|
|
3923
5153
|
case "context.mode.update": {
|
|
@@ -3933,7 +5163,7 @@ async function startWebUI(opts = {}) {
|
|
|
3933
5163
|
preserveK: payload.preserveK,
|
|
3934
5164
|
eliseThreshold: payload.eliseThreshold
|
|
3935
5165
|
});
|
|
3936
|
-
|
|
5166
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${payload.id}" updated`);
|
|
3937
5167
|
break;
|
|
3938
5168
|
}
|
|
3939
5169
|
case "context.mode.delete": {
|
|
@@ -3943,7 +5173,7 @@ async function startWebUI(opts = {}) {
|
|
|
3943
5173
|
context.meta["contextWindowPolicy"] = resolveContextWindowPolicy({}, DEFAULT_CONTEXT_WINDOW_MODE_ID);
|
|
3944
5174
|
}
|
|
3945
5175
|
const result = customModeStore.remove(id);
|
|
3946
|
-
|
|
5176
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${id}" deleted`);
|
|
3947
5177
|
break;
|
|
3948
5178
|
}
|
|
3949
5179
|
case "providers.list": {
|
|
@@ -4024,19 +5254,20 @@ async function startWebUI(opts = {}) {
|
|
|
4024
5254
|
context.provider = newProv;
|
|
4025
5255
|
updateAutoCompactionMaxContext?.(newProv);
|
|
4026
5256
|
try {
|
|
4027
|
-
|
|
4028
|
-
const raw = await
|
|
5257
|
+
const next = configWriteLock.then(async () => {
|
|
5258
|
+
const raw = await fs10.readFile(globalConfigPath, "utf8");
|
|
4029
5259
|
const parsed = JSON.parse(raw);
|
|
4030
5260
|
parsed.provider = newProvider;
|
|
4031
5261
|
parsed.model = newModel;
|
|
4032
5262
|
await atomicWrite5(globalConfigPath, JSON.stringify(parsed, null, 2));
|
|
4033
5263
|
});
|
|
4034
|
-
|
|
5264
|
+
configWriteLock = next.then(() => void 0, () => void 0);
|
|
5265
|
+
await next;
|
|
4035
5266
|
} catch (err) {
|
|
4036
5267
|
console.warn(JSON.stringify({
|
|
4037
5268
|
level: "warn",
|
|
4038
5269
|
event: "webui.config_save_failed",
|
|
4039
|
-
message:
|
|
5270
|
+
message: toErrorMessage5(err),
|
|
4040
5271
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4041
5272
|
}));
|
|
4042
5273
|
}
|
|
@@ -4134,6 +5365,26 @@ async function startWebUI(opts = {}) {
|
|
|
4134
5365
|
await providerHandlers.handleProviderRemove(ws, providerId);
|
|
4135
5366
|
break;
|
|
4136
5367
|
}
|
|
5368
|
+
case "provider.clear_models": {
|
|
5369
|
+
const { providerId } = msg.payload;
|
|
5370
|
+
await providerHandlers.handleProviderClearModels(ws, providerId);
|
|
5371
|
+
break;
|
|
5372
|
+
}
|
|
5373
|
+
case "provider.undo_clear": {
|
|
5374
|
+
const { providerId, previousModels } = msg.payload;
|
|
5375
|
+
await providerHandlers.handleProviderUndoClear(ws, providerId, previousModels);
|
|
5376
|
+
break;
|
|
5377
|
+
}
|
|
5378
|
+
case "provider.update": {
|
|
5379
|
+
const p = msg.payload;
|
|
5380
|
+
await providerHandlers.handleProviderUpdate(ws, p);
|
|
5381
|
+
break;
|
|
5382
|
+
}
|
|
5383
|
+
case "provider.probe": {
|
|
5384
|
+
const { providerId, timeoutMs } = msg.payload;
|
|
5385
|
+
await providerHandlers.handleProviderProbe(ws, providerId, timeoutMs);
|
|
5386
|
+
break;
|
|
5387
|
+
}
|
|
4137
5388
|
case "sessions.list": {
|
|
4138
5389
|
const limit = msg.payload?.limit ?? 50;
|
|
4139
5390
|
try {
|
|
@@ -4164,13 +5415,13 @@ async function startWebUI(opts = {}) {
|
|
|
4164
5415
|
const { id } = msg.payload;
|
|
4165
5416
|
try {
|
|
4166
5417
|
if (id === session.id) {
|
|
4167
|
-
|
|
5418
|
+
sendResult2(ws, false, "Cannot delete the active session");
|
|
4168
5419
|
break;
|
|
4169
5420
|
}
|
|
4170
5421
|
await sessionStore.delete(id);
|
|
4171
|
-
|
|
5422
|
+
sendResult2(ws, true, `Session ${id} deleted`);
|
|
4172
5423
|
} catch (err) {
|
|
4173
|
-
|
|
5424
|
+
sendResult2(ws, false, errMessage(err));
|
|
4174
5425
|
}
|
|
4175
5426
|
break;
|
|
4176
5427
|
}
|
|
@@ -4178,7 +5429,7 @@ async function startWebUI(opts = {}) {
|
|
|
4178
5429
|
const { id } = msg.payload;
|
|
4179
5430
|
try {
|
|
4180
5431
|
if (id === session.id) {
|
|
4181
|
-
|
|
5432
|
+
sendResult2(ws, false, "Session is already active");
|
|
4182
5433
|
break;
|
|
4183
5434
|
}
|
|
4184
5435
|
const resumed = await sessionStore.resume(id);
|
|
@@ -4208,14 +5459,14 @@ async function startWebUI(opts = {}) {
|
|
|
4208
5459
|
replayUsage: resumed.data.usage
|
|
4209
5460
|
}
|
|
4210
5461
|
});
|
|
4211
|
-
|
|
5462
|
+
sendResult2(ws, true, `Resumed session ${id}`);
|
|
4212
5463
|
} catch (err) {
|
|
4213
|
-
|
|
5464
|
+
sendResult2(ws, false, errMessage(err));
|
|
4214
5465
|
}
|
|
4215
5466
|
break;
|
|
4216
5467
|
}
|
|
4217
5468
|
case "session.save": {
|
|
4218
|
-
|
|
5469
|
+
sendResult2(ws, true, `Session ${session.id} is auto-saved`);
|
|
4219
5470
|
break;
|
|
4220
5471
|
}
|
|
4221
5472
|
case "tools.list": {
|
|
@@ -4238,6 +5489,27 @@ async function startWebUI(opts = {}) {
|
|
|
4238
5489
|
return handleMemoryRemember(ws, msg, memoryStore);
|
|
4239
5490
|
case "memory.forget":
|
|
4240
5491
|
return handleMemoryForget(ws, msg, memoryStore);
|
|
5492
|
+
// ── MCP operations — delegated to shared handlers (mcp-handlers.ts) ──
|
|
5493
|
+
case "mcp.list":
|
|
5494
|
+
return handleMcpList(ws, msg, config, globalConfigPath, void 0);
|
|
5495
|
+
case "mcp.add":
|
|
5496
|
+
return handleMcpAdd(ws, msg, config, globalConfigPath, void 0);
|
|
5497
|
+
case "mcp.remove":
|
|
5498
|
+
return handleMcpRemove(ws, msg, config, globalConfigPath, void 0);
|
|
5499
|
+
case "mcp.update":
|
|
5500
|
+
return handleMcpUpdate(ws, msg, config, globalConfigPath);
|
|
5501
|
+
case "mcp.wake":
|
|
5502
|
+
return handleMcpWake(ws, msg, config, globalConfigPath, void 0);
|
|
5503
|
+
case "mcp.sleep":
|
|
5504
|
+
return handleMcpSleep(ws, msg, config, globalConfigPath, void 0);
|
|
5505
|
+
case "mcp.discover":
|
|
5506
|
+
return handleMcpDiscover(ws, msg, config, globalConfigPath);
|
|
5507
|
+
case "mcp.enable":
|
|
5508
|
+
return handleMcpEnable(ws, msg, config, globalConfigPath);
|
|
5509
|
+
case "mcp.disable":
|
|
5510
|
+
return handleMcpDisable(ws, msg, config, globalConfigPath);
|
|
5511
|
+
case "mcp.restart":
|
|
5512
|
+
return handleMcpRestart(ws, msg, config, globalConfigPath);
|
|
4241
5513
|
case "skills.list": {
|
|
4242
5514
|
if (!skillLoader) {
|
|
4243
5515
|
send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
|
|
@@ -4247,6 +5519,18 @@ async function startWebUI(opts = {}) {
|
|
|
4247
5519
|
const manifests = await skillLoader.list();
|
|
4248
5520
|
const entries = await skillLoader.listEntries();
|
|
4249
5521
|
const byName = new Map(entries.map((e) => [e.name, e]));
|
|
5522
|
+
const sourceUrlsByName = /* @__PURE__ */ new Map();
|
|
5523
|
+
const refsByName = /* @__PURE__ */ new Map();
|
|
5524
|
+
if (skillInstaller) {
|
|
5525
|
+
try {
|
|
5526
|
+
const installed = await skillInstaller.listInstalled();
|
|
5527
|
+
for (const entry of installed) {
|
|
5528
|
+
sourceUrlsByName.set(entry.name, entry.source);
|
|
5529
|
+
refsByName.set(entry.name, entry.ref);
|
|
5530
|
+
}
|
|
5531
|
+
} catch {
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
4250
5534
|
send(ws, {
|
|
4251
5535
|
type: "skills.list",
|
|
4252
5536
|
payload: {
|
|
@@ -4256,6 +5540,8 @@ async function startWebUI(opts = {}) {
|
|
|
4256
5540
|
description: m.description,
|
|
4257
5541
|
version: m.version ?? "",
|
|
4258
5542
|
source: m.source,
|
|
5543
|
+
sourceUrl: sourceUrlsByName.get(m.name) ?? "",
|
|
5544
|
+
ref: refsByName.get(m.name) ?? "",
|
|
4259
5545
|
path: m.path,
|
|
4260
5546
|
trigger: byName.get(m.name)?.trigger ?? "",
|
|
4261
5547
|
scope: byName.get(m.name)?.scope ?? []
|
|
@@ -4274,6 +5560,261 @@ async function startWebUI(opts = {}) {
|
|
|
4274
5560
|
}
|
|
4275
5561
|
break;
|
|
4276
5562
|
}
|
|
5563
|
+
case "skills.content": {
|
|
5564
|
+
if (!skillLoader) {
|
|
5565
|
+
send(ws, { type: "skills.content", payload: { name: "", body: "", path: "", source: "", relatedFiles: [], references: [], error: "Skills not enabled" } });
|
|
5566
|
+
break;
|
|
5567
|
+
}
|
|
5568
|
+
const contentPayload = msg.payload;
|
|
5569
|
+
if (!contentPayload?.name) {
|
|
5570
|
+
send(ws, { type: "skills.content", payload: { name: "", body: "", path: "", source: "", relatedFiles: [], references: [], error: "Skill name is required" } });
|
|
5571
|
+
break;
|
|
5572
|
+
}
|
|
5573
|
+
try {
|
|
5574
|
+
const { name, source } = contentPayload;
|
|
5575
|
+
const entries = await skillLoader.listEntries();
|
|
5576
|
+
const entry = entries.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
5577
|
+
if (!entry) {
|
|
5578
|
+
send(ws, { type: "skills.content", payload: { name, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name}" not found` } });
|
|
5579
|
+
break;
|
|
5580
|
+
}
|
|
5581
|
+
const body = await skillLoader.readBody(name);
|
|
5582
|
+
const skillDir = path10.dirname(entry.path);
|
|
5583
|
+
let relatedFiles = [];
|
|
5584
|
+
try {
|
|
5585
|
+
const files = await fs10.readdir(skillDir);
|
|
5586
|
+
relatedFiles = files.filter((f) => f !== path10.basename(entry.path)).map((f) => path10.join(skillDir, f));
|
|
5587
|
+
} catch {
|
|
5588
|
+
}
|
|
5589
|
+
const refs = [];
|
|
5590
|
+
for (const e of entries) {
|
|
5591
|
+
if (e.name.toLowerCase() === name.toLowerCase()) continue;
|
|
5592
|
+
try {
|
|
5593
|
+
const content = await skillLoader.readBody(e.name);
|
|
5594
|
+
if (content.toLowerCase().includes(name.toLowerCase())) {
|
|
5595
|
+
refs.push(e.name);
|
|
5596
|
+
}
|
|
5597
|
+
} catch {
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
send(ws, { type: "skills.content", payload: { name, body, path: entry.path, source, relatedFiles, references: refs } });
|
|
5601
|
+
} catch (err) {
|
|
5602
|
+
send(ws, { type: "skills.content", payload: { name: contentPayload.name, body: "", path: "", source: contentPayload.source, relatedFiles: [], references: [], error: errMessage(err) } });
|
|
5603
|
+
}
|
|
5604
|
+
break;
|
|
5605
|
+
}
|
|
5606
|
+
case "skills.install": {
|
|
5607
|
+
if (!skillInstaller) {
|
|
5608
|
+
send(ws, { type: "skills.installed", payload: { success: false, error: "Skills not enabled" } });
|
|
5609
|
+
break;
|
|
5610
|
+
}
|
|
5611
|
+
const installPayload = msg.payload;
|
|
5612
|
+
if (!installPayload?.ref?.trim()) {
|
|
5613
|
+
send(ws, { type: "skills.installed", payload: { success: false, error: "Skill reference is required (e.g. owner/repo or https://github.com/owner/repo)" } });
|
|
5614
|
+
break;
|
|
5615
|
+
}
|
|
5616
|
+
try {
|
|
5617
|
+
const results = await skillInstaller.install(installPayload.ref.trim(), { global: installPayload.global });
|
|
5618
|
+
send(ws, {
|
|
5619
|
+
type: "skills.installed",
|
|
5620
|
+
payload: {
|
|
5621
|
+
success: true,
|
|
5622
|
+
results,
|
|
5623
|
+
error: null
|
|
5624
|
+
}
|
|
5625
|
+
});
|
|
5626
|
+
} catch (err) {
|
|
5627
|
+
send(ws, {
|
|
5628
|
+
type: "skills.installed",
|
|
5629
|
+
payload: {
|
|
5630
|
+
success: false,
|
|
5631
|
+
error: errMessage(err)
|
|
5632
|
+
}
|
|
5633
|
+
});
|
|
5634
|
+
}
|
|
5635
|
+
break;
|
|
5636
|
+
}
|
|
5637
|
+
case "skills.uninstall": {
|
|
5638
|
+
if (!skillInstaller) {
|
|
5639
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: "Skills not enabled" } });
|
|
5640
|
+
break;
|
|
5641
|
+
}
|
|
5642
|
+
const uninstallPayload = msg.payload;
|
|
5643
|
+
if (!uninstallPayload?.name?.trim()) {
|
|
5644
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: "Skill name is required" } });
|
|
5645
|
+
break;
|
|
5646
|
+
}
|
|
5647
|
+
try {
|
|
5648
|
+
await skillInstaller.uninstall(uninstallPayload.name.trim(), { global: uninstallPayload.global });
|
|
5649
|
+
send(ws, { type: "skills.uninstalled", payload: { success: true, error: null } });
|
|
5650
|
+
} catch (err) {
|
|
5651
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: errMessage(err) } });
|
|
5652
|
+
}
|
|
5653
|
+
break;
|
|
5654
|
+
}
|
|
5655
|
+
case "skills.update": {
|
|
5656
|
+
if (!skillInstaller) {
|
|
5657
|
+
send(ws, { type: "skills.updated", payload: { success: false, error: "Skills not enabled" } });
|
|
5658
|
+
break;
|
|
5659
|
+
}
|
|
5660
|
+
const updatePayload = msg.payload;
|
|
5661
|
+
try {
|
|
5662
|
+
const result = await skillInstaller.update(updatePayload?.name, { global: updatePayload?.global });
|
|
5663
|
+
send(ws, {
|
|
5664
|
+
type: "skills.updated",
|
|
5665
|
+
payload: {
|
|
5666
|
+
success: true,
|
|
5667
|
+
error: null,
|
|
5668
|
+
updated: result.updated,
|
|
5669
|
+
unchanged: result.unchanged,
|
|
5670
|
+
errors: result.errors
|
|
5671
|
+
}
|
|
5672
|
+
});
|
|
5673
|
+
} catch (err) {
|
|
5674
|
+
send(ws, { type: "skills.updated", payload: { success: false, error: errMessage(err) } });
|
|
5675
|
+
}
|
|
5676
|
+
break;
|
|
5677
|
+
}
|
|
5678
|
+
case "skills.create": {
|
|
5679
|
+
const createPayload = msg.payload;
|
|
5680
|
+
if (!createPayload?.name?.trim()) {
|
|
5681
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Skill name is required" } });
|
|
5682
|
+
break;
|
|
5683
|
+
}
|
|
5684
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(createPayload.name.trim())) {
|
|
5685
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Skill name must be kebab-case (e.g. my-new-skill)" } });
|
|
5686
|
+
break;
|
|
5687
|
+
}
|
|
5688
|
+
if (!createPayload?.description?.trim()) {
|
|
5689
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Description/trigger is required" } });
|
|
5690
|
+
break;
|
|
5691
|
+
}
|
|
5692
|
+
try {
|
|
5693
|
+
const targetDir = createPayload.scope === "global" ? path10.join(wstackGlobalRoot2(), "skills", createPayload.name.trim()) : path10.join(projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
5694
|
+
try {
|
|
5695
|
+
await fs10.access(targetDir);
|
|
5696
|
+
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
5697
|
+
break;
|
|
5698
|
+
} catch {
|
|
5699
|
+
}
|
|
5700
|
+
await fs10.mkdir(targetDir, { recursive: true });
|
|
5701
|
+
const lines = createPayload.description.trim().split("\n");
|
|
5702
|
+
const firstLine = lines[0].trim();
|
|
5703
|
+
const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
|
|
5704
|
+
const descriptionText = firstLine + (bodyLines.length > 0 ? `
|
|
5705
|
+
${bodyLines.join("\n")}` : "");
|
|
5706
|
+
const trigger = bodyLines.find((l) => l.toLowerCase().startsWith("triggers:")) ?? "";
|
|
5707
|
+
const skillContent = [
|
|
5708
|
+
"---",
|
|
5709
|
+
`name: ${createPayload.name.trim()}`,
|
|
5710
|
+
"description: |",
|
|
5711
|
+
` ${descriptionText.replace(/\n/g, "\n ")}`,
|
|
5712
|
+
`version: 1.0.0`,
|
|
5713
|
+
"---",
|
|
5714
|
+
"",
|
|
5715
|
+
`# ${createPayload.name.trim().split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}`,
|
|
5716
|
+
"",
|
|
5717
|
+
"## Overview",
|
|
5718
|
+
"",
|
|
5719
|
+
firstLine,
|
|
5720
|
+
"",
|
|
5721
|
+
...bodyLines.length > 0 ? bodyLines.filter((l) => !l.toLowerCase().startsWith("triggers:")) : [],
|
|
5722
|
+
"",
|
|
5723
|
+
"## Rules",
|
|
5724
|
+
"- TODO: add your first rule",
|
|
5725
|
+
"",
|
|
5726
|
+
"## Patterns",
|
|
5727
|
+
"### Do",
|
|
5728
|
+
"```ts",
|
|
5729
|
+
"// TODO: add a good example",
|
|
5730
|
+
"```",
|
|
5731
|
+
"",
|
|
5732
|
+
"### Don't",
|
|
5733
|
+
"```ts",
|
|
5734
|
+
"// TODO: add a bad example",
|
|
5735
|
+
"```",
|
|
5736
|
+
"",
|
|
5737
|
+
"## Workflow",
|
|
5738
|
+
"1. TODO: describe step one",
|
|
5739
|
+
"2. TODO: describe step two",
|
|
5740
|
+
"",
|
|
5741
|
+
trigger ? `
|
|
5742
|
+
${trigger}
|
|
5743
|
+
` : "",
|
|
5744
|
+
"## Skills in scope",
|
|
5745
|
+
"- `bug-hunter` \u2014 for systematic bug detection patterns",
|
|
5746
|
+
"- `output-standards` \u2014 for standardized `<next_steps>` formatting"
|
|
5747
|
+
].join("\n");
|
|
5748
|
+
await fs10.writeFile(path10.join(targetDir, "SKILL.md"), skillContent, "utf-8");
|
|
5749
|
+
send(ws, {
|
|
5750
|
+
type: "skills.created",
|
|
5751
|
+
payload: {
|
|
5752
|
+
success: true,
|
|
5753
|
+
error: null,
|
|
5754
|
+
skill: { name: createPayload.name.trim(), path: path10.join(targetDir, "SKILL.md"), scope: createPayload.scope }
|
|
5755
|
+
}
|
|
5756
|
+
});
|
|
5757
|
+
} catch (err) {
|
|
5758
|
+
send(ws, { type: "skills.created", payload: { success: false, error: errMessage(err) } });
|
|
5759
|
+
}
|
|
5760
|
+
break;
|
|
5761
|
+
}
|
|
5762
|
+
case "skills.edit": {
|
|
5763
|
+
if (!skillLoader) {
|
|
5764
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skills not enabled" } });
|
|
5765
|
+
break;
|
|
5766
|
+
}
|
|
5767
|
+
const editPayload = msg.payload;
|
|
5768
|
+
if (!editPayload?.name?.trim()) {
|
|
5769
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skill name is required" } });
|
|
5770
|
+
break;
|
|
5771
|
+
}
|
|
5772
|
+
if (!editPayload?.body) {
|
|
5773
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skill body is required" } });
|
|
5774
|
+
break;
|
|
5775
|
+
}
|
|
5776
|
+
try {
|
|
5777
|
+
const entries = await skillLoader.listEntries();
|
|
5778
|
+
const entry = entries.find((e) => e.name.toLowerCase() === editPayload.name.toLowerCase());
|
|
5779
|
+
if (!entry) {
|
|
5780
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: `Skill "${editPayload.name}" not found` } });
|
|
5781
|
+
break;
|
|
5782
|
+
}
|
|
5783
|
+
if (entry.scope.includes("bundled")) {
|
|
5784
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Bundled skills cannot be edited" } });
|
|
5785
|
+
break;
|
|
5786
|
+
}
|
|
5787
|
+
await fs10.writeFile(entry.path, editPayload.body, "utf-8");
|
|
5788
|
+
send(ws, { type: "skills.edited", payload: { success: true, error: null } });
|
|
5789
|
+
} catch (err) {
|
|
5790
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: errMessage(err) } });
|
|
5791
|
+
}
|
|
5792
|
+
break;
|
|
5793
|
+
}
|
|
5794
|
+
case "skills.export": {
|
|
5795
|
+
if (!skillLoader) {
|
|
5796
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: "Skills not enabled" } });
|
|
5797
|
+
break;
|
|
5798
|
+
}
|
|
5799
|
+
try {
|
|
5800
|
+
const entries = await skillLoader.listEntries();
|
|
5801
|
+
const zip = new JSZip2();
|
|
5802
|
+
for (const entry of entries) {
|
|
5803
|
+
try {
|
|
5804
|
+
const body = await skillLoader.readBody(entry.name);
|
|
5805
|
+
const safeName = entry.name.replace(/\//g, "_");
|
|
5806
|
+
zip.file(`${safeName}/SKILL.md`, body);
|
|
5807
|
+
} catch {
|
|
5808
|
+
}
|
|
5809
|
+
}
|
|
5810
|
+
const zipBuffer = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
|
5811
|
+
const zipBase64 = zipBuffer.toString("base64");
|
|
5812
|
+
send(ws, { type: "skills.exported", payload: { zipBase64, skillCount: entries.length, error: void 0 } });
|
|
5813
|
+
} catch (err) {
|
|
5814
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: errMessage(err) } });
|
|
5815
|
+
}
|
|
5816
|
+
break;
|
|
5817
|
+
}
|
|
4277
5818
|
case "diag.get": {
|
|
4278
5819
|
const usage = tokenCounter.total();
|
|
4279
5820
|
send(ws, {
|
|
@@ -4301,125 +5842,84 @@ async function startWebUI(opts = {}) {
|
|
|
4301
5842
|
break;
|
|
4302
5843
|
}
|
|
4303
5844
|
case "todos.get": {
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
5845
|
+
const ctx = {
|
|
5846
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5847
|
+
send: (w, m) => send(w, m),
|
|
5848
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5849
|
+
};
|
|
5850
|
+
handleTodosGet(ctx, ws);
|
|
4308
5851
|
break;
|
|
4309
5852
|
}
|
|
4310
5853
|
case "todos.clear": {
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
5854
|
+
const ctx = {
|
|
5855
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5856
|
+
send: (w, m) => send(w, m),
|
|
5857
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5858
|
+
};
|
|
5859
|
+
handleTodosClear(ctx, ws);
|
|
4314
5860
|
break;
|
|
4315
5861
|
}
|
|
4316
5862
|
case "todos.remove": {
|
|
4317
|
-
const
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
}
|
|
4322
|
-
|
|
4323
|
-
let targetIdx = -1;
|
|
4324
|
-
if (typeof id === "string") {
|
|
4325
|
-
targetIdx = context.todos.findIndex((t) => t.id === id);
|
|
4326
|
-
} else if (typeof index === "number" && index > 0) {
|
|
4327
|
-
targetIdx = index - 1;
|
|
4328
|
-
}
|
|
4329
|
-
if (targetIdx < 0 || !context.todos[targetIdx]) {
|
|
4330
|
-
sendResult(ws, false, "Todo not found");
|
|
4331
|
-
break;
|
|
4332
|
-
}
|
|
4333
|
-
const removed = expectDefined2(context.todos[targetIdx]);
|
|
4334
|
-
const next = [...context.todos.slice(0, targetIdx), ...context.todos.slice(targetIdx + 1)];
|
|
4335
|
-
context.state.replaceTodos(next);
|
|
4336
|
-
sendResult(ws, true, `Removed: ${removed.content}`);
|
|
4337
|
-
broadcast(clients, { type: "todos.updated", payload: { todos: next } });
|
|
5863
|
+
const ctx = {
|
|
5864
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5865
|
+
send: (w, m) => send(w, m),
|
|
5866
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5867
|
+
};
|
|
5868
|
+
handleTodosRemove(ctx, ws, msg.payload);
|
|
4338
5869
|
break;
|
|
4339
5870
|
}
|
|
4340
5871
|
case "tasks.get": {
|
|
4341
|
-
const
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
type: "tasks.updated",
|
|
4348
|
-
payload: { tasks: file?.tasks ?? [] }
|
|
4349
|
-
});
|
|
4350
|
-
} catch {
|
|
4351
|
-
send(ws, { type: "tasks.updated", payload: { tasks: [] } });
|
|
4352
|
-
}
|
|
4353
|
-
} else {
|
|
4354
|
-
send(ws, { type: "tasks.updated", payload: { tasks: [], error: "Task storage not configured." } });
|
|
4355
|
-
}
|
|
5872
|
+
const ctx = {
|
|
5873
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5874
|
+
send: (w, m) => send(w, m),
|
|
5875
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5876
|
+
};
|
|
5877
|
+
await handleTasksGet(ctx, ws);
|
|
4356
5878
|
break;
|
|
4357
5879
|
}
|
|
4358
5880
|
case "plan.get": {
|
|
4359
|
-
const
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
type: "plan.updated",
|
|
4366
|
-
payload: {
|
|
4367
|
-
plan: plan ?? {
|
|
4368
|
-
version: 1,
|
|
4369
|
-
sessionId: session.id,
|
|
4370
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4371
|
-
items: []
|
|
4372
|
-
}
|
|
4373
|
-
}
|
|
4374
|
-
});
|
|
4375
|
-
} catch {
|
|
4376
|
-
send(ws, {
|
|
4377
|
-
type: "plan.updated",
|
|
4378
|
-
payload: {
|
|
4379
|
-
plan: {
|
|
4380
|
-
version: 1,
|
|
4381
|
-
sessionId: session.id,
|
|
4382
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4383
|
-
items: []
|
|
4384
|
-
}
|
|
4385
|
-
}
|
|
4386
|
-
});
|
|
4387
|
-
}
|
|
4388
|
-
} else {
|
|
4389
|
-
send(ws, {
|
|
4390
|
-
type: "plan.updated",
|
|
4391
|
-
payload: { plan: null, error: "Plan storage is not configured for this session." }
|
|
4392
|
-
});
|
|
4393
|
-
}
|
|
5881
|
+
const ctx = {
|
|
5882
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5883
|
+
send: (w, m) => send(w, m),
|
|
5884
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5885
|
+
};
|
|
5886
|
+
await handlePlanGet(ctx, ws);
|
|
4394
5887
|
break;
|
|
4395
5888
|
}
|
|
4396
5889
|
case "plan.template_use": {
|
|
4397
|
-
const
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
5890
|
+
const ctx = {
|
|
5891
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5892
|
+
send: (w, m) => send(w, m),
|
|
5893
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5894
|
+
};
|
|
5895
|
+
await handlePlanTemplateUse(ctx, ws, msg.payload.template);
|
|
5896
|
+
break;
|
|
5897
|
+
}
|
|
5898
|
+
case "todo.update": {
|
|
5899
|
+
const ctx = {
|
|
5900
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5901
|
+
send: (w, m) => send(w, m),
|
|
5902
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5903
|
+
};
|
|
5904
|
+
handleTodoUpdate(ctx, ws, msg.payload);
|
|
5905
|
+
break;
|
|
5906
|
+
}
|
|
5907
|
+
case "task.update": {
|
|
5908
|
+
const ctx = {
|
|
5909
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5910
|
+
send: (w, m) => send(w, m),
|
|
5911
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5912
|
+
};
|
|
5913
|
+
await handleTaskUpdate(ctx, ws, msg.payload);
|
|
5914
|
+
break;
|
|
5915
|
+
}
|
|
5916
|
+
case "plan.item.update": {
|
|
5917
|
+
const ctx = {
|
|
5918
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
5919
|
+
send: (w, m) => send(w, m),
|
|
5920
|
+
broadcast: (m) => broadcast(clients, m)
|
|
5921
|
+
};
|
|
5922
|
+
await handlePlanItemUpdate(ctx, ws, msg.payload);
|
|
4423
5923
|
break;
|
|
4424
5924
|
}
|
|
4425
5925
|
// ── File operations — delegated to shared handlers (file-handlers.ts) ──
|
|
@@ -4489,13 +5989,13 @@ async function startWebUI(opts = {}) {
|
|
|
4489
5989
|
provider: config.provider,
|
|
4490
5990
|
model: config.model
|
|
4491
5991
|
});
|
|
4492
|
-
|
|
5992
|
+
sendResult2(ws, true, `Switched to mode "${id}"`);
|
|
4493
5993
|
broadcast(clients, {
|
|
4494
5994
|
type: "session.start",
|
|
4495
5995
|
payload: { ...await sessionStartPayload() }
|
|
4496
5996
|
});
|
|
4497
5997
|
} catch (err) {
|
|
4498
|
-
|
|
5998
|
+
sendResult2(ws, false, errMessage(err));
|
|
4499
5999
|
}
|
|
4500
6000
|
break;
|
|
4501
6001
|
}
|
|
@@ -4549,13 +6049,13 @@ async function startWebUI(opts = {}) {
|
|
|
4549
6049
|
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
4550
6050
|
const proc = getProcessRegistry().get(pid);
|
|
4551
6051
|
if (proc?.protected) {
|
|
4552
|
-
|
|
6052
|
+
sendResult2(ws, false, `Cannot kill protected process (PID ${pid})`);
|
|
4553
6053
|
break;
|
|
4554
6054
|
}
|
|
4555
6055
|
getProcessRegistry().kill(pid);
|
|
4556
|
-
|
|
6056
|
+
sendResult2(ws, true, `Killed PID ${pid}`);
|
|
4557
6057
|
} catch (err) {
|
|
4558
|
-
|
|
6058
|
+
sendResult2(ws, false, errMessage(err));
|
|
4559
6059
|
}
|
|
4560
6060
|
break;
|
|
4561
6061
|
}
|
|
@@ -4563,16 +6063,25 @@ async function startWebUI(opts = {}) {
|
|
|
4563
6063
|
try {
|
|
4564
6064
|
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
4565
6065
|
getProcessRegistry().killAll();
|
|
4566
|
-
|
|
6066
|
+
sendResult2(ws, true, "All processes killed");
|
|
4567
6067
|
} catch (err) {
|
|
4568
|
-
|
|
6068
|
+
sendResult2(ws, false, errMessage(err));
|
|
4569
6069
|
}
|
|
4570
6070
|
break;
|
|
4571
6071
|
}
|
|
6072
|
+
case "git.info": {
|
|
6073
|
+
await handleGitInfo(ws, projectRoot);
|
|
6074
|
+
break;
|
|
6075
|
+
}
|
|
6076
|
+
case "webui.shutdown": {
|
|
6077
|
+
console.log("[WebUI] Shutdown requested from client");
|
|
6078
|
+
process.kill(process.pid, "SIGINT");
|
|
6079
|
+
break;
|
|
6080
|
+
}
|
|
4572
6081
|
case "goal.get": {
|
|
4573
6082
|
try {
|
|
4574
|
-
const goalPath =
|
|
4575
|
-
const raw = await
|
|
6083
|
+
const goalPath = resolveWstackPaths({ projectRoot }).projectGoal;
|
|
6084
|
+
const raw = await fs10.readFile(goalPath, "utf8");
|
|
4576
6085
|
const goal = JSON.parse(raw);
|
|
4577
6086
|
broadcast(clients, { type: "goal.updated", payload: goal });
|
|
4578
6087
|
} catch {
|
|
@@ -4583,7 +6092,7 @@ async function startWebUI(opts = {}) {
|
|
|
4583
6092
|
case "autonomy.switch": {
|
|
4584
6093
|
const { mode } = msg.payload;
|
|
4585
6094
|
context.meta["autonomy"] = mode;
|
|
4586
|
-
|
|
6095
|
+
sendResult2(ws, true, `Autonomy mode set to "${mode}"`);
|
|
4587
6096
|
broadcast(clients, { type: "prefs.updated", payload: { autonomy: mode } });
|
|
4588
6097
|
void persistPrefsToConfig({ autonomy: mode });
|
|
4589
6098
|
break;
|
|
@@ -4632,7 +6141,7 @@ async function startWebUI(opts = {}) {
|
|
|
4632
6141
|
try {
|
|
4633
6142
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4634
6143
|
const rewinder = new DefaultSessionRewinder(
|
|
4635
|
-
|
|
6144
|
+
path10.join(projectRoot, ".wrongstack", "sessions"),
|
|
4636
6145
|
projectRoot
|
|
4637
6146
|
);
|
|
4638
6147
|
const checkpoints = await rewinder.listCheckpoints(session.id);
|
|
@@ -4653,18 +6162,18 @@ async function startWebUI(opts = {}) {
|
|
|
4653
6162
|
try {
|
|
4654
6163
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4655
6164
|
const rewinder = new DefaultSessionRewinder(
|
|
4656
|
-
|
|
6165
|
+
path10.join(projectRoot, ".wrongstack", "sessions"),
|
|
4657
6166
|
projectRoot
|
|
4658
6167
|
);
|
|
4659
6168
|
await rewinder.rewindToCheckpoint(session.id, checkpointIndex);
|
|
4660
6169
|
await context.session.truncateToCheckpoint(checkpointIndex);
|
|
4661
|
-
|
|
6170
|
+
sendResult2(ws, true, `Rewound to checkpoint ${checkpointIndex}`);
|
|
4662
6171
|
broadcast(clients, {
|
|
4663
6172
|
type: "session.start",
|
|
4664
6173
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
4665
6174
|
});
|
|
4666
6175
|
} catch (err) {
|
|
4667
|
-
|
|
6176
|
+
sendResult2(ws, false, errMessage(err));
|
|
4668
6177
|
}
|
|
4669
6178
|
break;
|
|
4670
6179
|
}
|
|
@@ -4687,9 +6196,9 @@ async function startWebUI(opts = {}) {
|
|
|
4687
6196
|
case "projects.add": {
|
|
4688
6197
|
const { root: addRoot, name: displayName } = msg.payload;
|
|
4689
6198
|
try {
|
|
4690
|
-
const resolved =
|
|
4691
|
-
await
|
|
4692
|
-
const stat2 = await
|
|
6199
|
+
const resolved = path10.resolve(addRoot);
|
|
6200
|
+
await fs10.access(resolved);
|
|
6201
|
+
const stat2 = await fs10.stat(resolved);
|
|
4693
6202
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4694
6203
|
const manifest = await loadManifest(globalConfigPath);
|
|
4695
6204
|
const existing = manifest.projects.find((p) => p.root === resolved);
|
|
@@ -4705,7 +6214,7 @@ async function startWebUI(opts = {}) {
|
|
|
4705
6214
|
});
|
|
4706
6215
|
break;
|
|
4707
6216
|
}
|
|
4708
|
-
const name = displayName?.trim() ||
|
|
6217
|
+
const name = displayName?.trim() || path10.basename(resolved);
|
|
4709
6218
|
const slug = generateProjectSlug(resolved);
|
|
4710
6219
|
await ensureProjectDataDir(slug, globalConfigPath);
|
|
4711
6220
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -4724,7 +6233,7 @@ async function startWebUI(opts = {}) {
|
|
|
4724
6233
|
send(ws, {
|
|
4725
6234
|
type: "projects.added",
|
|
4726
6235
|
payload: {
|
|
4727
|
-
name:
|
|
6236
|
+
name: path10.basename(addRoot),
|
|
4728
6237
|
root: addRoot,
|
|
4729
6238
|
slug: "",
|
|
4730
6239
|
message: errMessage(err)
|
|
@@ -4736,17 +6245,17 @@ async function startWebUI(opts = {}) {
|
|
|
4736
6245
|
case "projects.select": {
|
|
4737
6246
|
const { root: selRoot, name: selName } = msg.payload;
|
|
4738
6247
|
try {
|
|
4739
|
-
const resolved =
|
|
6248
|
+
const resolved = path10.resolve(selRoot);
|
|
4740
6249
|
try {
|
|
4741
|
-
await
|
|
4742
|
-
const stat2 = await
|
|
6250
|
+
await fs10.access(resolved);
|
|
6251
|
+
const stat2 = await fs10.stat(resolved);
|
|
4743
6252
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4744
6253
|
} catch (err) {
|
|
4745
6254
|
send(ws, {
|
|
4746
6255
|
type: "projects.selected",
|
|
4747
6256
|
payload: {
|
|
4748
6257
|
root: selRoot,
|
|
4749
|
-
name: selName ||
|
|
6258
|
+
name: selName || path10.basename(selRoot),
|
|
4750
6259
|
message: `Cannot switch: ${errMessage(err)}`
|
|
4751
6260
|
}
|
|
4752
6261
|
});
|
|
@@ -4758,7 +6267,7 @@ async function startWebUI(opts = {}) {
|
|
|
4758
6267
|
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
4759
6268
|
entry.lastWorkingDir = resolved;
|
|
4760
6269
|
} else {
|
|
4761
|
-
const name = selName?.trim() ||
|
|
6270
|
+
const name = selName?.trim() || path10.basename(resolved);
|
|
4762
6271
|
const slug = generateProjectSlug(resolved);
|
|
4763
6272
|
manifest.projects.push({
|
|
4764
6273
|
name,
|
|
@@ -4799,13 +6308,13 @@ async function startWebUI(opts = {}) {
|
|
|
4799
6308
|
});
|
|
4800
6309
|
} catch {
|
|
4801
6310
|
}
|
|
4802
|
-
const newSessionsDir =
|
|
4803
|
-
|
|
6311
|
+
const newSessionsDir = path10.join(
|
|
6312
|
+
path10.dirname(globalConfigPath),
|
|
4804
6313
|
"projects",
|
|
4805
6314
|
switchSlug,
|
|
4806
6315
|
"sessions"
|
|
4807
6316
|
);
|
|
4808
|
-
await
|
|
6317
|
+
await fs10.mkdir(newSessionsDir, { recursive: true });
|
|
4809
6318
|
const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
|
|
4810
6319
|
const oldSessionId = session.id;
|
|
4811
6320
|
try {
|
|
@@ -4837,8 +6346,9 @@ async function startWebUI(opts = {}) {
|
|
|
4837
6346
|
sessionId: session.id,
|
|
4838
6347
|
projectSlug: switchSlug,
|
|
4839
6348
|
projectRoot,
|
|
4840
|
-
projectName:
|
|
6349
|
+
projectName: path10.basename(projectRoot),
|
|
4841
6350
|
workingDir,
|
|
6351
|
+
clientType: "webui",
|
|
4842
6352
|
pid: process.pid,
|
|
4843
6353
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4844
6354
|
});
|
|
@@ -4848,8 +6358,8 @@ async function startWebUI(opts = {}) {
|
|
|
4848
6358
|
type: "projects.selected",
|
|
4849
6359
|
payload: {
|
|
4850
6360
|
root: resolved,
|
|
4851
|
-
name: selName ||
|
|
4852
|
-
message: `Switched to ${selName ||
|
|
6361
|
+
name: selName || path10.basename(resolved),
|
|
6362
|
+
message: `Switched to ${selName || path10.basename(resolved)}`
|
|
4853
6363
|
}
|
|
4854
6364
|
});
|
|
4855
6365
|
broadcast(clients, {
|
|
@@ -4872,7 +6382,7 @@ async function startWebUI(opts = {}) {
|
|
|
4872
6382
|
type: "projects.selected",
|
|
4873
6383
|
payload: {
|
|
4874
6384
|
root: selRoot,
|
|
4875
|
-
name: selName ||
|
|
6385
|
+
name: selName || path10.basename(selRoot),
|
|
4876
6386
|
message: errMessage(err)
|
|
4877
6387
|
}
|
|
4878
6388
|
});
|
|
@@ -4883,17 +6393,17 @@ async function startWebUI(opts = {}) {
|
|
|
4883
6393
|
case "working_dir.set": {
|
|
4884
6394
|
const { path: newPath } = msg.payload;
|
|
4885
6395
|
try {
|
|
4886
|
-
const resolved =
|
|
4887
|
-
if (!resolved.startsWith(projectRoot +
|
|
4888
|
-
|
|
6396
|
+
const resolved = path10.resolve(projectRoot, newPath);
|
|
6397
|
+
if (!resolved.startsWith(projectRoot + path10.sep) && resolved !== projectRoot) {
|
|
6398
|
+
sendResult2(ws, false, `Path must stay inside the project root: ${projectRoot}`);
|
|
4889
6399
|
break;
|
|
4890
6400
|
}
|
|
4891
6401
|
try {
|
|
4892
|
-
await
|
|
4893
|
-
const stat2 = await
|
|
6402
|
+
await fs10.access(resolved);
|
|
6403
|
+
const stat2 = await fs10.stat(resolved);
|
|
4894
6404
|
if (!stat2.isDirectory()) throw new Error("Not a directory");
|
|
4895
6405
|
} catch {
|
|
4896
|
-
|
|
6406
|
+
sendResult2(ws, false, `Directory not found or not accessible: ${resolved}`);
|
|
4897
6407
|
break;
|
|
4898
6408
|
}
|
|
4899
6409
|
workingDir = resolved;
|
|
@@ -4902,9 +6412,9 @@ async function startWebUI(opts = {}) {
|
|
|
4902
6412
|
type: "working_dir.changed",
|
|
4903
6413
|
payload: { cwd: resolved, projectRoot }
|
|
4904
6414
|
});
|
|
4905
|
-
|
|
6415
|
+
sendResult2(ws, true, `Working directory set to ${resolved}`);
|
|
4906
6416
|
} catch (err) {
|
|
4907
|
-
|
|
6417
|
+
sendResult2(ws, false, errMessage(err));
|
|
4908
6418
|
}
|
|
4909
6419
|
break;
|
|
4910
6420
|
}
|
|
@@ -4914,26 +6424,32 @@ async function startWebUI(opts = {}) {
|
|
|
4914
6424
|
msg.payload,
|
|
4915
6425
|
logger
|
|
4916
6426
|
);
|
|
4917
|
-
|
|
6427
|
+
sendResult2(ws, result.success, result.message);
|
|
4918
6428
|
break;
|
|
4919
6429
|
}
|
|
4920
6430
|
// ── Mailbox operations — project-level inter-agent messaging ────
|
|
4921
6431
|
case "mailbox.messages":
|
|
4922
6432
|
return handleMailboxMessages(
|
|
4923
6433
|
ws,
|
|
4924
|
-
{ projectRoot, globalRoot:
|
|
6434
|
+
{ projectRoot, globalRoot: path10.dirname(globalConfigPath) },
|
|
4925
6435
|
msg.payload
|
|
4926
6436
|
);
|
|
4927
6437
|
case "mailbox.agents":
|
|
4928
6438
|
return handleMailboxAgents(
|
|
4929
6439
|
ws,
|
|
4930
|
-
{ projectRoot, globalRoot:
|
|
6440
|
+
{ projectRoot, globalRoot: path10.dirname(globalConfigPath) },
|
|
4931
6441
|
msg.payload
|
|
4932
6442
|
);
|
|
4933
6443
|
case "mailbox.clear":
|
|
4934
6444
|
return handleMailboxClear(
|
|
4935
6445
|
ws,
|
|
4936
|
-
{ projectRoot, globalRoot:
|
|
6446
|
+
{ projectRoot, globalRoot: path10.dirname(globalConfigPath) }
|
|
6447
|
+
);
|
|
6448
|
+
case "mailbox.purge":
|
|
6449
|
+
return handleMailboxPurge(
|
|
6450
|
+
ws,
|
|
6451
|
+
{ projectRoot, globalRoot: path10.dirname(globalConfigPath) },
|
|
6452
|
+
msg.payload
|
|
4937
6453
|
);
|
|
4938
6454
|
// ── Brain — status, autonomy ceiling, direct decision support ───
|
|
4939
6455
|
case "brain.status":
|
|
@@ -4946,7 +6462,7 @@ async function startWebUI(opts = {}) {
|
|
|
4946
6462
|
const level = msg.payload?.level ?? "";
|
|
4947
6463
|
const valid = ["off", "low", "medium", "high", "all"];
|
|
4948
6464
|
if (!valid.includes(level)) {
|
|
4949
|
-
|
|
6465
|
+
sendResult2(ws, false, `Unknown risk level "${level}". Use: ${valid.join(", ")}.`);
|
|
4950
6466
|
break;
|
|
4951
6467
|
}
|
|
4952
6468
|
brainSettings.maxAutoRisk = level;
|
|
@@ -4959,7 +6475,7 @@ async function startWebUI(opts = {}) {
|
|
|
4959
6475
|
case "brain.ask": {
|
|
4960
6476
|
const question = msg.payload?.question?.trim();
|
|
4961
6477
|
if (!question) {
|
|
4962
|
-
|
|
6478
|
+
sendResult2(ws, false, "Usage: /brain ask <question>");
|
|
4963
6479
|
break;
|
|
4964
6480
|
}
|
|
4965
6481
|
try {
|
|
@@ -4972,7 +6488,7 @@ async function startWebUI(opts = {}) {
|
|
|
4972
6488
|
});
|
|
4973
6489
|
send(ws, { type: "brain.answer", payload: { question, decision } });
|
|
4974
6490
|
} catch (err) {
|
|
4975
|
-
|
|
6491
|
+
sendResult2(ws, false, `Brain consultation failed: ${errMessage(err)}`);
|
|
4976
6492
|
}
|
|
4977
6493
|
break;
|
|
4978
6494
|
}
|
|
@@ -4999,14 +6515,28 @@ async function startWebUI(opts = {}) {
|
|
|
4999
6515
|
broadcast,
|
|
5000
6516
|
clients
|
|
5001
6517
|
});
|
|
6518
|
+
const watcherMetrics = {
|
|
6519
|
+
fileChangesDetected: 0,
|
|
6520
|
+
filesProcessed: 0,
|
|
6521
|
+
broadcastsSent: 0,
|
|
6522
|
+
debounceResets: 0,
|
|
6523
|
+
totalDebounceDelayMs: 0,
|
|
6524
|
+
activeProjects: 0,
|
|
6525
|
+
averageDebounceDelayMs: 0,
|
|
6526
|
+
watcherActive: false
|
|
6527
|
+
};
|
|
5002
6528
|
const httpServer = createHttpServer({
|
|
5003
6529
|
host: wsHost,
|
|
5004
|
-
distDir:
|
|
6530
|
+
distDir: path10.resolve(import.meta.dirname, "../../dist"),
|
|
5005
6531
|
wsPort,
|
|
5006
6532
|
globalRoot: wpaths.globalRoot,
|
|
5007
|
-
apiToken: wsToken
|
|
6533
|
+
apiToken: wsToken,
|
|
6534
|
+
watcherMetrics,
|
|
6535
|
+
onFleetPing: () => {
|
|
6536
|
+
void fleetBroadcast?.();
|
|
6537
|
+
}
|
|
5008
6538
|
});
|
|
5009
|
-
const registryBaseDir =
|
|
6539
|
+
const registryBaseDir = path10.dirname(globalConfigPath);
|
|
5010
6540
|
httpServer.listen(httpPort, wsHost, () => {
|
|
5011
6541
|
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
5012
6542
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
@@ -5018,7 +6548,7 @@ async function startWebUI(opts = {}) {
|
|
|
5018
6548
|
wsPort,
|
|
5019
6549
|
host: wsHost,
|
|
5020
6550
|
projectRoot,
|
|
5021
|
-
projectName:
|
|
6551
|
+
projectName: path10.basename(projectRoot) || projectRoot,
|
|
5022
6552
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5023
6553
|
url: `http://${wsHost}:${httpPort}`
|
|
5024
6554
|
},
|
|
@@ -5045,6 +6575,10 @@ async function startWebUI(opts = {}) {
|
|
|
5045
6575
|
// reality. Crash exits are healed by the next register()/list() prune pass.
|
|
5046
6576
|
onShutdown: () => {
|
|
5047
6577
|
brainMonitor.stop();
|
|
6578
|
+
if (disposeEvents) {
|
|
6579
|
+
disposeEvents();
|
|
6580
|
+
disposeEvents = null;
|
|
6581
|
+
}
|
|
5048
6582
|
if (eternalSubscription) {
|
|
5049
6583
|
eternalSubscription.dispose();
|
|
5050
6584
|
eternalSubscription = null;
|