@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/index.js
CHANGED
|
@@ -1,13 +1,179 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
});
|
|
1
|
+
// src/server/index.ts
|
|
2
|
+
import { expectDefined as expectDefined2, GlobalMailbox as GlobalMailbox2, projectSlug, getSessionRegistry, AgentStatusTracker, FleetNotifier } from "@wrongstack/core";
|
|
3
|
+
|
|
4
|
+
// src/server/handlers/worklist-handlers.ts
|
|
5
|
+
function sendResult(ws, ctx, ok, message) {
|
|
6
|
+
ctx.send(ws, { type: ok ? "ok" : "error", message });
|
|
7
|
+
}
|
|
8
|
+
function handleTodosGet(ctx, ws) {
|
|
9
|
+
ctx.send(ws, { type: "todos.updated", payload: { todos: ctx.context.todos } });
|
|
10
|
+
}
|
|
11
|
+
function handleTodosClear(ctx, ws) {
|
|
12
|
+
ctx.replaceTodos?.([]);
|
|
13
|
+
ctx.broadcast({ type: "todos.cleared" });
|
|
14
|
+
sendResult(ws, ctx, true, "Todo board cleared.");
|
|
15
|
+
}
|
|
16
|
+
function handleTodosRemove(ctx, ws, payload) {
|
|
17
|
+
if (!payload || payload.id === void 0 && payload.index === void 0) {
|
|
18
|
+
sendResult(ws, ctx, false, "todos.remove requires id or index.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const next = payload.id !== void 0 ? ctx.context.todos.filter((t) => t.id !== payload.id) : ctx.context.todos.filter((_, i) => i !== payload.index);
|
|
22
|
+
ctx.replaceTodos?.(next);
|
|
23
|
+
ctx.broadcast({ type: "todos.updated", payload: { todos: next } });
|
|
24
|
+
sendResult(ws, ctx, true, "Todo item removed.");
|
|
25
|
+
}
|
|
26
|
+
function handleTodoUpdate(ctx, ws, payload) {
|
|
27
|
+
const todo = ctx.context.todos.find((t) => t.id === payload.id);
|
|
28
|
+
if (!todo) {
|
|
29
|
+
sendResult(ws, ctx, false, `No todo with id "${payload.id}".`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const next = ctx.context.todos.map(
|
|
33
|
+
(t) => t.id === payload.id ? { ...t, ...payload.status !== void 0 && { status: payload.status }, ...payload.activeForm !== void 0 && { activeForm: payload.activeForm } } : t
|
|
34
|
+
);
|
|
35
|
+
ctx.replaceTodos?.(next);
|
|
36
|
+
ctx.broadcast({ type: "todos.updated", payload: { todos: next } });
|
|
37
|
+
sendResult(ws, ctx, true, `Todo "${todo.content}" updated.`);
|
|
38
|
+
}
|
|
39
|
+
async function handleTasksGet(ctx, ws) {
|
|
40
|
+
const taskPath = ctx.context.meta["task.path"];
|
|
41
|
+
if (typeof taskPath === "string" && taskPath) {
|
|
42
|
+
try {
|
|
43
|
+
const { loadTasks } = await import("@wrongstack/core");
|
|
44
|
+
const file = await loadTasks(taskPath);
|
|
45
|
+
ctx.send(ws, { type: "tasks.updated", payload: { tasks: file?.tasks ?? [] } });
|
|
46
|
+
} catch {
|
|
47
|
+
ctx.send(ws, { type: "tasks.updated", payload: { tasks: [] } });
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
ctx.send(ws, {
|
|
51
|
+
type: "tasks.updated",
|
|
52
|
+
payload: { tasks: [], error: "Task storage not configured." }
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function handleTaskUpdate(ctx, ws, payload) {
|
|
57
|
+
const taskPath = ctx.context.meta["task.path"];
|
|
58
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
59
|
+
sendResult(ws, ctx, false, "Task storage is not configured for this session.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const { loadTasks, saveTasks } = await import("@wrongstack/core");
|
|
64
|
+
const file = await loadTasks(taskPath);
|
|
65
|
+
if (!file) {
|
|
66
|
+
sendResult(ws, ctx, false, "No task file found.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const idx = file.tasks.findIndex((t) => t.id === payload.id);
|
|
70
|
+
if (idx === -1) {
|
|
71
|
+
sendResult(ws, ctx, false, `Task "${payload.id}" not found.`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
file.tasks[idx] = { ...file.tasks[idx], status: payload.status };
|
|
75
|
+
await saveTasks(taskPath, file);
|
|
76
|
+
ctx.broadcast({ type: "tasks.updated", payload: { tasks: file.tasks } });
|
|
77
|
+
sendResult(ws, ctx, true, `Task "${payload.id}" marked ${payload.status}.`);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
sendResult(ws, ctx, false, String(err));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function handlePlanGet(ctx, ws) {
|
|
83
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
84
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
85
|
+
if (typeof planPath === "string" && planPath) {
|
|
86
|
+
try {
|
|
87
|
+
const { loadPlan } = await import("@wrongstack/core");
|
|
88
|
+
const plan = await loadPlan(planPath);
|
|
89
|
+
ctx.send(ws, {
|
|
90
|
+
type: "plan.updated",
|
|
91
|
+
payload: {
|
|
92
|
+
plan: plan ?? {
|
|
93
|
+
version: 1,
|
|
94
|
+
sessionId,
|
|
95
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
96
|
+
items: []
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
ctx.send(ws, {
|
|
102
|
+
type: "plan.updated",
|
|
103
|
+
payload: {
|
|
104
|
+
plan: {
|
|
105
|
+
version: 1,
|
|
106
|
+
sessionId,
|
|
107
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
108
|
+
items: []
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
ctx.send(ws, {
|
|
115
|
+
type: "plan.updated",
|
|
116
|
+
payload: { plan: null, error: "Plan storage is not configured for this session." }
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function handlePlanTemplateUse(ctx, ws, template) {
|
|
121
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
122
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
123
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
124
|
+
sendResult(ws, ctx, false, "Plan storage is not configured for this session.");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem } = await import("@wrongstack/core");
|
|
129
|
+
const tpl = getPlanTemplate(template);
|
|
130
|
+
if (!tpl) {
|
|
131
|
+
sendResult(ws, ctx, false, `Unknown template "${template}".`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let plan = await loadPlan(planPath) ?? emptyPlan(sessionId);
|
|
135
|
+
for (const item of tpl.items) {
|
|
136
|
+
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
137
|
+
}
|
|
138
|
+
await savePlan(planPath, plan);
|
|
139
|
+
sendResult(ws, ctx, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
|
|
140
|
+
ctx.broadcast({ type: "plan.updated", payload: { plan } });
|
|
141
|
+
} catch (err) {
|
|
142
|
+
sendResult(ws, ctx, false, String(err));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function handlePlanItemUpdate(ctx, ws, payload) {
|
|
146
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
147
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
148
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
149
|
+
sendResult(ws, ctx, false, "Plan storage is not configured for this session.");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const { loadPlan, savePlan, mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
|
|
154
|
+
let changed = false;
|
|
155
|
+
const plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
156
|
+
const before = p.updatedAt;
|
|
157
|
+
const updated = setPlanItemStatus(p, payload.target, payload.status);
|
|
158
|
+
changed = updated.updatedAt !== before;
|
|
159
|
+
return updated;
|
|
160
|
+
});
|
|
161
|
+
if (!changed) {
|
|
162
|
+
sendResult(ws, ctx, false, `No plan item matched "${payload.target}".`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
sendResult(ws, ctx, true, `Plan item status updated to "${payload.status}".`);
|
|
166
|
+
ctx.broadcast({ type: "plan.updated", payload: { plan } });
|
|
167
|
+
} catch (err) {
|
|
168
|
+
sendResult(ws, ctx, false, String(err));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
7
171
|
|
|
8
172
|
// src/server/index.ts
|
|
9
|
-
import { expectDefined as expectDefined2, GlobalMailbox as GlobalMailbox2, projectSlug, getSessionRegistry, AgentStatusTracker } from "@wrongstack/core";
|
|
10
173
|
import { makeMailboxTool, makeMailSendTool, makeMailInboxTool, mailboxSessionTag } from "@wrongstack/core";
|
|
174
|
+
import { toErrorMessage as toErrorMessage5, wstackGlobalRoot as wstackGlobalRoot2, projectHash, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
175
|
+
import { SkillInstaller } from "@wrongstack/core/skills";
|
|
176
|
+
import JSZip2 from "jszip";
|
|
11
177
|
import {
|
|
12
178
|
BrainMonitor,
|
|
13
179
|
DefaultBrainArbiter,
|
|
@@ -15,8 +181,8 @@ import {
|
|
|
15
181
|
createAutonomyBrain,
|
|
16
182
|
createTieredBrainArbiter
|
|
17
183
|
} from "@wrongstack/core";
|
|
18
|
-
import * as
|
|
19
|
-
import * as
|
|
184
|
+
import * as fs10 from "fs/promises";
|
|
185
|
+
import * as path10 from "path";
|
|
20
186
|
|
|
21
187
|
// src/server/http-server.ts
|
|
22
188
|
import * as fs from "fs/promises";
|
|
@@ -24,7 +190,7 @@ import * as http from "http";
|
|
|
24
190
|
import * as path from "path";
|
|
25
191
|
|
|
26
192
|
// src/server/ws-auth.ts
|
|
27
|
-
import { Buffer
|
|
193
|
+
import { Buffer } from "buffer";
|
|
28
194
|
import { timingSafeEqual } from "crypto";
|
|
29
195
|
function isLoopbackHostname(hostname) {
|
|
30
196
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
@@ -43,8 +209,8 @@ function isLoopbackBind(wsHost) {
|
|
|
43
209
|
}
|
|
44
210
|
function tokenMatches(provided, expected) {
|
|
45
211
|
if (!provided) return false;
|
|
46
|
-
const a =
|
|
47
|
-
const b =
|
|
212
|
+
const a = Buffer.from(provided);
|
|
213
|
+
const b = Buffer.from(expected);
|
|
48
214
|
if (a.length !== b.length) return false;
|
|
49
215
|
return timingSafeEqual(a, b);
|
|
50
216
|
}
|
|
@@ -135,6 +301,13 @@ function isInsideDist(candidate, distDir) {
|
|
|
135
301
|
const resolved = path.resolve(candidate);
|
|
136
302
|
return resolved === root || resolved.startsWith(root + path.sep);
|
|
137
303
|
}
|
|
304
|
+
function decodeSessionId(segment) {
|
|
305
|
+
try {
|
|
306
|
+
return decodeURIComponent(segment);
|
|
307
|
+
} catch {
|
|
308
|
+
return segment;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
138
311
|
function createHttpServer(opts) {
|
|
139
312
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
140
313
|
const distDir = path.resolve(opts.distDir);
|
|
@@ -160,6 +333,22 @@ function createHttpServer(opts) {
|
|
|
160
333
|
res.end("ok");
|
|
161
334
|
return;
|
|
162
335
|
}
|
|
336
|
+
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
337
|
+
const headerToken = req.headers["x-ws-token"];
|
|
338
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
339
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
340
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
341
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
opts.onFleetPing?.();
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
res.writeHead(204);
|
|
349
|
+
res.end();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
163
352
|
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
164
353
|
const headerToken = req.headers["x-ws-token"];
|
|
165
354
|
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
@@ -180,7 +369,89 @@ function createHttpServer(opts) {
|
|
|
180
369
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
181
370
|
return;
|
|
182
371
|
}
|
|
183
|
-
await handleApiSessionAgents(res, opts.globalRoot, agentsMatch[1]);
|
|
372
|
+
await handleApiSessionAgents(res, opts.globalRoot, decodeSessionId(agentsMatch[1]));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const eventsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
|
|
376
|
+
if (eventsMatch && req.method === "GET") {
|
|
377
|
+
const headerToken = req.headers["x-ws-token"];
|
|
378
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
379
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
380
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
381
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
|
|
385
|
+
const limit = Math.min(500, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
|
|
386
|
+
await handleApiSessionEvents(res, opts.globalRoot, decodeSessionId(eventsMatch[1]), limit);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const msgMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/message$/);
|
|
390
|
+
if (msgMatch && req.method === "POST") {
|
|
391
|
+
const headerToken = req.headers["x-ws-token"];
|
|
392
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
393
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
394
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
395
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
await handleApiSessionMessage(res, req, opts.globalRoot, decodeSessionId(msgMatch[1]));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const mailboxMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/mailbox$/);
|
|
402
|
+
if (mailboxMatch && req.method === "GET") {
|
|
403
|
+
const headerToken = req.headers["x-ws-token"];
|
|
404
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
405
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
406
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
407
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
await handleApiSessionMailbox(res, opts.globalRoot, decodeSessionId(mailboxMatch[1]));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const interruptMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/interrupt$/);
|
|
414
|
+
if (interruptMatch && req.method === "POST") {
|
|
415
|
+
const headerToken = req.headers["x-ws-token"];
|
|
416
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
417
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
418
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
419
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
await handleApiSessionInterrupt(
|
|
423
|
+
res,
|
|
424
|
+
req,
|
|
425
|
+
opts.globalRoot,
|
|
426
|
+
decodeSessionId(interruptMatch[1])
|
|
427
|
+
);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (url.pathname === "/api/fleet/broadcast" && req.method === "POST") {
|
|
431
|
+
const headerToken = req.headers["x-ws-token"];
|
|
432
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
433
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
434
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
435
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
await handleApiFleetBroadcast(res, req, opts.globalRoot);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (url.pathname === "/debug/watcher-metrics" && req.method === "GET") {
|
|
442
|
+
if (opts.watcherMetrics) {
|
|
443
|
+
const avgDelay = opts.watcherMetrics.broadcastsSent > 0 ? opts.watcherMetrics.totalDebounceDelayMs / opts.watcherMetrics.broadcastsSent : 0;
|
|
444
|
+
const response = {
|
|
445
|
+
...opts.watcherMetrics,
|
|
446
|
+
averageDebounceDelayMs: avgDelay,
|
|
447
|
+
timestamp: Date.now()
|
|
448
|
+
};
|
|
449
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
450
|
+
res.end(JSON.stringify(response));
|
|
451
|
+
} else {
|
|
452
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
453
|
+
res.end(JSON.stringify({ error: "File watcher metrics not available" }));
|
|
454
|
+
}
|
|
184
455
|
return;
|
|
185
456
|
}
|
|
186
457
|
let filePath;
|
|
@@ -312,6 +583,324 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
|
312
583
|
res.end(JSON.stringify({ error: String(err) }));
|
|
313
584
|
}
|
|
314
585
|
}
|
|
586
|
+
function blocksToText(content) {
|
|
587
|
+
if (typeof content === "string") return content;
|
|
588
|
+
if (Array.isArray(content)) {
|
|
589
|
+
return content.filter(
|
|
590
|
+
(b) => !!b && typeof b === "object" && b.type === "text" && typeof b.text === "string"
|
|
591
|
+
).map((b) => b.text).join("\n");
|
|
592
|
+
}
|
|
593
|
+
return "";
|
|
594
|
+
}
|
|
595
|
+
function clip(s, n = 600) {
|
|
596
|
+
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
597
|
+
}
|
|
598
|
+
function asString(v) {
|
|
599
|
+
if (typeof v === "string") return v;
|
|
600
|
+
try {
|
|
601
|
+
return JSON.stringify(v);
|
|
602
|
+
} catch {
|
|
603
|
+
return String(v);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function mapWatchEntry(ev) {
|
|
607
|
+
const ts = typeof ev["ts"] === "string" ? ev["ts"] : "";
|
|
608
|
+
switch (ev["type"]) {
|
|
609
|
+
case "user_input":
|
|
610
|
+
return { ts, role: "user", text: clip(blocksToText(ev["content"])) };
|
|
611
|
+
case "llm_response": {
|
|
612
|
+
const text = blocksToText(ev["content"]);
|
|
613
|
+
return text.trim() ? { ts, role: "assistant", text: clip(text) } : null;
|
|
614
|
+
}
|
|
615
|
+
case "tool_use":
|
|
616
|
+
case "tool_call_start": {
|
|
617
|
+
const input = ev["input"] ?? ev["args"];
|
|
618
|
+
const preview = input !== void 0 && input !== null ? clip(asString(input), 160) : "";
|
|
619
|
+
return { ts, role: "tool", tool: String(ev["name"] ?? "tool"), text: preview };
|
|
620
|
+
}
|
|
621
|
+
case "tool_result": {
|
|
622
|
+
if (ev["isError"]) return { ts, role: "error", text: clip(asString(ev["content"])) };
|
|
623
|
+
const out = asString(ev["content"]).trim();
|
|
624
|
+
return out ? { ts, role: "tool", tool: "\u21B3 result", text: clip(out, 240) } : null;
|
|
625
|
+
}
|
|
626
|
+
case "error":
|
|
627
|
+
case "provider_error":
|
|
628
|
+
return { ts, role: "error", text: clip(String(ev["message"] ?? "error")) };
|
|
629
|
+
case "agent_spawned":
|
|
630
|
+
return { ts, role: "system", text: `spawned ${String(ev["role"] ?? "agent")}` };
|
|
631
|
+
case "task_completed":
|
|
632
|
+
return { ts, role: "system", text: `task done: ${String(ev["title"] ?? "")}` };
|
|
633
|
+
case "task_failed":
|
|
634
|
+
return { ts, role: "system", text: `task failed: ${String(ev["title"] ?? "")}` };
|
|
635
|
+
default:
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
640
|
+
if (!globalRoot) {
|
|
641
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
642
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
|
|
647
|
+
const registry = new SessionRegistry(globalRoot);
|
|
648
|
+
const entry = await registry.get(sessionId);
|
|
649
|
+
if (!entry) {
|
|
650
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
651
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
655
|
+
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
656
|
+
const reader = new DefaultSessionReader2({ store });
|
|
657
|
+
const all = [];
|
|
658
|
+
for await (const ev of reader.replay(sessionId)) {
|
|
659
|
+
const mapped = mapWatchEntry(ev);
|
|
660
|
+
if (mapped) all.push(mapped);
|
|
661
|
+
}
|
|
662
|
+
const tail = all.slice(-limit);
|
|
663
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
664
|
+
res.end(
|
|
665
|
+
JSON.stringify({
|
|
666
|
+
sessionId,
|
|
667
|
+
status: entry.status,
|
|
668
|
+
clientType: entry.clientType,
|
|
669
|
+
projectName: entry.projectName,
|
|
670
|
+
total: all.length,
|
|
671
|
+
entries: tail
|
|
672
|
+
})
|
|
673
|
+
);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
676
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function readJsonBody(req) {
|
|
680
|
+
return new Promise((resolve5, reject) => {
|
|
681
|
+
let data = "";
|
|
682
|
+
req.on("data", (chunk) => {
|
|
683
|
+
data += chunk;
|
|
684
|
+
if (data.length > 64e3) {
|
|
685
|
+
reject(new Error("Request body too large"));
|
|
686
|
+
req.destroy();
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
req.on("end", () => {
|
|
690
|
+
try {
|
|
691
|
+
resolve5(data ? JSON.parse(data) : {});
|
|
692
|
+
} catch (err) {
|
|
693
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
req.on("error", reject);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
700
|
+
if (!globalRoot) {
|
|
701
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
702
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
let body;
|
|
706
|
+
try {
|
|
707
|
+
body = await readJsonBody(req);
|
|
708
|
+
} catch {
|
|
709
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
710
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const text = typeof body["text"] === "string" ? body["text"].trim() : "";
|
|
714
|
+
if (!text) {
|
|
715
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
716
|
+
res.end(JSON.stringify({ error: "text is required" }));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
720
|
+
const ALLOWED = /* @__PURE__ */ new Set(["steer", "ask", "assign", "note", "btw"]);
|
|
721
|
+
const rawType = typeof body["type"] === "string" ? body["type"] : "steer";
|
|
722
|
+
const type = ALLOWED.has(rawType) ? rawType : "steer";
|
|
723
|
+
const rawPriority = typeof body["priority"] === "string" ? body["priority"] : "";
|
|
724
|
+
const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
|
|
725
|
+
const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
|
|
726
|
+
try {
|
|
727
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
728
|
+
const registry = new SessionRegistry(globalRoot);
|
|
729
|
+
const entry = await registry.get(sessionId);
|
|
730
|
+
if (!entry) {
|
|
731
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
732
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
736
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
737
|
+
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
738
|
+
const sent = await mailbox.send({ from, to, type, subject, body: text, priority });
|
|
739
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
740
|
+
res.end(JSON.stringify({ ok: true, id: sent.id, to, type, delivered: entry.status }));
|
|
741
|
+
} catch (err) {
|
|
742
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
743
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
747
|
+
if (!globalRoot) {
|
|
748
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
749
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
754
|
+
const registry = new SessionRegistry(globalRoot);
|
|
755
|
+
const entry = await registry.get(sessionId);
|
|
756
|
+
if (!entry) {
|
|
757
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
758
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
762
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
763
|
+
const leaderAddr = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
764
|
+
const [inbound, outbound] = await Promise.all([
|
|
765
|
+
mailbox.query({ to: leaderAddr, limit: 50 }),
|
|
766
|
+
mailbox.query({ from: leaderAddr, limit: 50 })
|
|
767
|
+
]);
|
|
768
|
+
const seen = /* @__PURE__ */ new Set();
|
|
769
|
+
const thread = [...inbound, ...outbound].filter((m) => {
|
|
770
|
+
if (seen.has(m.id)) return false;
|
|
771
|
+
seen.add(m.id);
|
|
772
|
+
return true;
|
|
773
|
+
}).sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)).map((m) => ({
|
|
774
|
+
id: m.id,
|
|
775
|
+
from: m.from,
|
|
776
|
+
to: m.to,
|
|
777
|
+
type: m.type,
|
|
778
|
+
subject: m.subject,
|
|
779
|
+
body: m.body,
|
|
780
|
+
priority: m.priority,
|
|
781
|
+
// Whether the leader has read it, and when.
|
|
782
|
+
readByLeader: m.readBy?.[leaderAddr] ?? null,
|
|
783
|
+
readByCount: Object.keys(m.readBy ?? {}).length,
|
|
784
|
+
completed: m.completed,
|
|
785
|
+
outcome: m.outcome ?? null,
|
|
786
|
+
timestamp: m.timestamp,
|
|
787
|
+
replyTo: m.replyTo ?? null,
|
|
788
|
+
fromLeader: m.from === leaderAddr
|
|
789
|
+
}));
|
|
790
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
791
|
+
res.end(JSON.stringify({ sessionId, leader: leaderAddr, status: entry.status, thread }));
|
|
792
|
+
} catch (err) {
|
|
793
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
794
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
798
|
+
if (!globalRoot) {
|
|
799
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
800
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
let body = {};
|
|
804
|
+
try {
|
|
805
|
+
body = await readJsonBody(req);
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
|
|
809
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
810
|
+
try {
|
|
811
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
812
|
+
const registry = new SessionRegistry(globalRoot);
|
|
813
|
+
const entry = await registry.get(sessionId);
|
|
814
|
+
if (!entry) {
|
|
815
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
816
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
820
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
821
|
+
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
822
|
+
const sent = await mailbox.send({
|
|
823
|
+
from,
|
|
824
|
+
to,
|
|
825
|
+
type: "control",
|
|
826
|
+
subject: "interrupt",
|
|
827
|
+
body: reason,
|
|
828
|
+
priority: "high"
|
|
829
|
+
});
|
|
830
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
831
|
+
res.end(JSON.stringify({ ok: true, id: sent.id, to, delivered: entry.status }));
|
|
832
|
+
} catch (err) {
|
|
833
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
834
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
838
|
+
if (!globalRoot) {
|
|
839
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
840
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
let body;
|
|
844
|
+
try {
|
|
845
|
+
body = await readJsonBody(req);
|
|
846
|
+
} catch {
|
|
847
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
848
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const text = typeof body["text"] === "string" ? body["text"].trim() : "";
|
|
852
|
+
if (!text) {
|
|
853
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
854
|
+
res.end(JSON.stringify({ error: "text is required" }));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
858
|
+
try {
|
|
859
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
860
|
+
const registry = new SessionRegistry(globalRoot);
|
|
861
|
+
const all = await registry.list();
|
|
862
|
+
const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
|
|
863
|
+
const targets = all.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true);
|
|
864
|
+
if (targets.length === 0) {
|
|
865
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
866
|
+
res.end(JSON.stringify({ ok: true, delivered: 0 }));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const mbByDir = /* @__PURE__ */ new Map();
|
|
870
|
+
const mailboxFor = (projectRoot) => {
|
|
871
|
+
const dir = resolveWstackPaths2({ projectRoot, globalRoot }).projectDir;
|
|
872
|
+
let mb = mbByDir.get(dir);
|
|
873
|
+
if (!mb) {
|
|
874
|
+
mb = new GlobalMailbox3(dir);
|
|
875
|
+
mbByDir.set(dir, mb);
|
|
876
|
+
}
|
|
877
|
+
return mb;
|
|
878
|
+
};
|
|
879
|
+
let delivered = 0;
|
|
880
|
+
await Promise.all(
|
|
881
|
+
targets.map(async (s) => {
|
|
882
|
+
try {
|
|
883
|
+
const mb = mailboxFor(s.projectRoot);
|
|
884
|
+
await mb.send({
|
|
885
|
+
from,
|
|
886
|
+
to: `leader@${mailboxSessionTag2(s.sessionId)}`,
|
|
887
|
+
type: "steer",
|
|
888
|
+
subject: "Broadcast from Fleet HQ",
|
|
889
|
+
body: text,
|
|
890
|
+
priority: "high"
|
|
891
|
+
});
|
|
892
|
+
delivered++;
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
})
|
|
896
|
+
);
|
|
897
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
898
|
+
res.end(JSON.stringify({ ok: true, delivered, targets: targets.length }));
|
|
899
|
+
} catch (err) {
|
|
900
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
901
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
315
904
|
|
|
316
905
|
// src/server/file-handlers.ts
|
|
317
906
|
import * as fs2 from "fs/promises";
|
|
@@ -385,7 +974,7 @@ function broadcast(clients, msg) {
|
|
|
385
974
|
}
|
|
386
975
|
}
|
|
387
976
|
}
|
|
388
|
-
function
|
|
977
|
+
function sendResult2(ws, success, message) {
|
|
389
978
|
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
390
979
|
}
|
|
391
980
|
function errMessage(err) {
|
|
@@ -534,24 +1123,266 @@ async function handleMemoryRemember(ws, msg, memoryStore) {
|
|
|
534
1123
|
const { text, scope } = msg.payload;
|
|
535
1124
|
try {
|
|
536
1125
|
await memoryStore.remember(text, scope ?? "project-memory");
|
|
537
|
-
|
|
1126
|
+
sendResult2(ws, true, "Saved to memory");
|
|
538
1127
|
} catch (err) {
|
|
539
|
-
|
|
1128
|
+
sendResult2(ws, false, errMessage(err));
|
|
540
1129
|
}
|
|
541
1130
|
}
|
|
542
1131
|
async function handleMemoryForget(ws, msg, memoryStore) {
|
|
543
1132
|
const { text, scope } = msg.payload;
|
|
544
1133
|
try {
|
|
545
1134
|
const removed = await memoryStore.forget(text, scope ?? "project-memory");
|
|
546
|
-
|
|
1135
|
+
sendResult2(
|
|
547
1136
|
ws,
|
|
548
1137
|
removed > 0,
|
|
549
1138
|
removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
|
|
550
1139
|
);
|
|
551
1140
|
} catch (err) {
|
|
552
|
-
|
|
1141
|
+
sendResult2(ws, false, errMessage(err));
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/server/mcp-handlers.ts
|
|
1146
|
+
import * as fs3 from "fs/promises";
|
|
1147
|
+
import * as path3 from "path";
|
|
1148
|
+
function isMcpServerRecord(val) {
|
|
1149
|
+
if (typeof val !== "object" || val === null) return false;
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
function projectServer(name, cfg, _status = "stopped", tools = []) {
|
|
1153
|
+
return {
|
|
1154
|
+
name,
|
|
1155
|
+
transport: cfg.transport,
|
|
1156
|
+
status: _status,
|
|
1157
|
+
enabled: cfg.enabled ?? true,
|
|
1158
|
+
description: cfg.description,
|
|
1159
|
+
tools
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
async function readConfig(configPath) {
|
|
1163
|
+
try {
|
|
1164
|
+
const content = await fs3.readFile(configPath, "utf-8");
|
|
1165
|
+
return JSON.parse(content);
|
|
1166
|
+
} catch {
|
|
1167
|
+
return {};
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
async function writeConfig(configPath, cfg) {
|
|
1171
|
+
const dir = path3.dirname(configPath);
|
|
1172
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
1173
|
+
await fs3.writeFile(configPath, JSON.stringify(cfg, null, 2), "utf-8");
|
|
1174
|
+
}
|
|
1175
|
+
async function getMcpServers(config, globalConfigPath) {
|
|
1176
|
+
const servers = [];
|
|
1177
|
+
const configured = isMcpServerRecord(config.mcpServers) ? config.mcpServers : {};
|
|
1178
|
+
for (const [name, cfg] of Object.entries(configured)) {
|
|
1179
|
+
servers.push(projectServer(name, cfg));
|
|
1180
|
+
}
|
|
1181
|
+
return servers;
|
|
1182
|
+
}
|
|
1183
|
+
function getRegistryStates(mcpRegistry) {
|
|
1184
|
+
const states = /* @__PURE__ */ new Map();
|
|
1185
|
+
if (!mcpRegistry?.list) return states;
|
|
1186
|
+
try {
|
|
1187
|
+
const list = mcpRegistry.list();
|
|
1188
|
+
for (const item of list) {
|
|
1189
|
+
states.set(item.name, { state: item.state, toolCount: item.toolCount });
|
|
1190
|
+
}
|
|
1191
|
+
} catch {
|
|
1192
|
+
}
|
|
1193
|
+
return states;
|
|
1194
|
+
}
|
|
1195
|
+
async function handleMcpList(ws, _msg, config, _globalConfigPath, mcpRegistry) {
|
|
1196
|
+
const servers = await getMcpServers(config, _globalConfigPath);
|
|
1197
|
+
const registryStates = getRegistryStates(mcpRegistry);
|
|
1198
|
+
for (const server of servers) {
|
|
1199
|
+
const registryState = registryStates.get(server.name);
|
|
1200
|
+
if (registryState) {
|
|
1201
|
+
server.status = registryState.state;
|
|
1202
|
+
server.tools = Array.from({ length: registryState.toolCount }, (_, i) => `tool-${i + 1}`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
send(ws, { type: "mcp.list", payload: { servers } });
|
|
1206
|
+
}
|
|
1207
|
+
async function handleMcpAdd(ws, msg, config, globalConfigPath, mcpRegistry) {
|
|
1208
|
+
const payload = msg.payload;
|
|
1209
|
+
if (!payload.name) {
|
|
1210
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
try {
|
|
1214
|
+
const diskConfig = await readConfig(globalConfigPath);
|
|
1215
|
+
const mcpServers = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : {};
|
|
1216
|
+
if (mcpServers[payload.name]) {
|
|
1217
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Server "${payload.name}" already exists` } });
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
mcpServers[payload.name] = {
|
|
1221
|
+
transport: payload.transport,
|
|
1222
|
+
description: payload.description,
|
|
1223
|
+
enabled: payload.enabled ?? true,
|
|
1224
|
+
command: payload.command,
|
|
1225
|
+
args: payload.args,
|
|
1226
|
+
env: payload.env,
|
|
1227
|
+
allowedTools: payload.allowedTools
|
|
1228
|
+
};
|
|
1229
|
+
diskConfig.mcpServers = mcpServers;
|
|
1230
|
+
await writeConfig(globalConfigPath, diskConfig);
|
|
1231
|
+
const newServer = projectServer(payload.name, mcpServers[payload.name]);
|
|
1232
|
+
send(ws, { type: "mcp.server.added", payload: { server: newServer } });
|
|
1233
|
+
if (mcpRegistry && (payload.enabled ?? true)) {
|
|
1234
|
+
const serverConfig = mcpServers[payload.name];
|
|
1235
|
+
try {
|
|
1236
|
+
await mcpRegistry.start({
|
|
1237
|
+
name: payload.name,
|
|
1238
|
+
transport: payload.transport,
|
|
1239
|
+
command: payload.command,
|
|
1240
|
+
args: payload.args,
|
|
1241
|
+
env: payload.env,
|
|
1242
|
+
allowedTools: payload.allowedTools,
|
|
1243
|
+
enabled: true
|
|
1244
|
+
});
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
send(ws, { type: "mcp.server.error", payload: { name: payload.name, error: String(err) } });
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" added` } });
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to add server: ${err}` } });
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async function handleMcpRemove(ws, msg, _config, globalConfigPath, mcpRegistry) {
|
|
1255
|
+
const payload = msg.payload;
|
|
1256
|
+
if (!payload.name) {
|
|
1257
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
try {
|
|
1261
|
+
if (mcpRegistry) {
|
|
1262
|
+
try {
|
|
1263
|
+
await mcpRegistry.stop(payload.name);
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const diskConfig = await readConfig(globalConfigPath);
|
|
1268
|
+
const mcpServers = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : {};
|
|
1269
|
+
if (!mcpServers[payload.name]) {
|
|
1270
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Server "${payload.name}" not found` } });
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
delete mcpServers[payload.name];
|
|
1274
|
+
diskConfig.mcpServers = mcpServers;
|
|
1275
|
+
await writeConfig(globalConfigPath, diskConfig);
|
|
1276
|
+
send(ws, { type: "mcp.server.removed", payload: { name: payload.name } });
|
|
1277
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" removed` } });
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to remove server: ${err}` } });
|
|
553
1280
|
}
|
|
554
1281
|
}
|
|
1282
|
+
async function handleMcpUpdate(ws, msg, _config, globalConfigPath) {
|
|
1283
|
+
const payload = msg.payload;
|
|
1284
|
+
if (!payload.name) {
|
|
1285
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
try {
|
|
1289
|
+
const diskConfig = await readConfig(globalConfigPath);
|
|
1290
|
+
const mcpServers = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : {};
|
|
1291
|
+
if (!mcpServers[payload.name]) {
|
|
1292
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Server "${payload.name}" not found` } });
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
const existing = mcpServers[payload.name];
|
|
1296
|
+
mcpServers[payload.name] = {
|
|
1297
|
+
transport: payload.transport ?? existing.transport,
|
|
1298
|
+
description: payload.description ?? existing.description,
|
|
1299
|
+
enabled: payload.enabled ?? existing.enabled,
|
|
1300
|
+
command: payload.command ?? existing.command,
|
|
1301
|
+
args: payload.args ?? existing.args,
|
|
1302
|
+
env: payload.env ?? existing.env,
|
|
1303
|
+
allowedTools: payload.allowedTools ?? existing.allowedTools
|
|
1304
|
+
};
|
|
1305
|
+
diskConfig.mcpServers = mcpServers;
|
|
1306
|
+
await writeConfig(globalConfigPath, diskConfig);
|
|
1307
|
+
const updatedServer = projectServer(payload.name, mcpServers[payload.name]);
|
|
1308
|
+
send(ws, { type: "mcp.server.updated", payload: { server: updatedServer } });
|
|
1309
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" updated` } });
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to update server: ${err}` } });
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
async function handleMcpWake(ws, msg, _config, _globalConfigPath, mcpRegistry) {
|
|
1315
|
+
const payload = msg.payload;
|
|
1316
|
+
if (!payload.name) {
|
|
1317
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (!mcpRegistry) {
|
|
1321
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "MCP registry not available" } });
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
try {
|
|
1325
|
+
send(ws, { type: "mcp.server.waking", payload: { name: payload.name } });
|
|
1326
|
+
await mcpRegistry.restart(payload.name);
|
|
1327
|
+
send(ws, { type: "mcp.server.connected", payload: { name: payload.name } });
|
|
1328
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" restarted` } });
|
|
1329
|
+
} catch (err) {
|
|
1330
|
+
send(ws, { type: "mcp.server.error", payload: { name: payload.name, error: String(err) } });
|
|
1331
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to restart "${payload.name}": ${err}` } });
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
async function handleMcpSleep(ws, msg, _config, _globalConfigPath, mcpRegistry) {
|
|
1335
|
+
const payload = msg.payload;
|
|
1336
|
+
if (!payload.name) {
|
|
1337
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (!mcpRegistry) {
|
|
1341
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "MCP registry not available" } });
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
try {
|
|
1345
|
+
await mcpRegistry.stop(payload.name);
|
|
1346
|
+
send(ws, { type: "mcp.server.sleeping", payload: { name: payload.name } });
|
|
1347
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" stopped` } });
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
send(ws, { type: "mcp.server.error", payload: { name: payload.name, error: String(err) } });
|
|
1350
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to stop "${payload.name}": ${err}` } });
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
async function handleMcpDiscover(ws, msg, _config, _globalConfigPath, _mcpRegistry) {
|
|
1354
|
+
const payload = msg.payload;
|
|
1355
|
+
if (!payload.name) {
|
|
1356
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
send(ws, { type: "mcp.server.discovered", payload: { name: payload.name, tools: [] } });
|
|
1360
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Server "${payload.name}" tools were discovered on connect` } });
|
|
1361
|
+
}
|
|
1362
|
+
async function handleMcpEnable(ws, msg, _config, _globalConfigPath) {
|
|
1363
|
+
const payload = msg.payload;
|
|
1364
|
+
if (!payload.name) {
|
|
1365
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Enable command sent for "${payload.name}"` } });
|
|
1369
|
+
}
|
|
1370
|
+
async function handleMcpDisable(ws, msg, _config, _globalConfigPath) {
|
|
1371
|
+
const payload = msg.payload;
|
|
1372
|
+
if (!payload.name) {
|
|
1373
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Disable command sent for "${payload.name}"` } });
|
|
1377
|
+
}
|
|
1378
|
+
async function handleMcpRestart(ws, msg, _config, _globalConfigPath) {
|
|
1379
|
+
const payload = msg.payload;
|
|
1380
|
+
if (!payload.name) {
|
|
1381
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: "Server name is required" } });
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Restart command sent for "${payload.name}"` } });
|
|
1385
|
+
}
|
|
555
1386
|
|
|
556
1387
|
// src/server/index.ts
|
|
557
1388
|
import {
|
|
@@ -710,6 +1541,7 @@ function patchConfig(config, updates) {
|
|
|
710
1541
|
|
|
711
1542
|
// src/server/autophase-ws-handler.ts
|
|
712
1543
|
import { spawnSync } from "child_process";
|
|
1544
|
+
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
713
1545
|
import {
|
|
714
1546
|
AutoPhasePlanner,
|
|
715
1547
|
PhaseGraphBuilder,
|
|
@@ -879,7 +1711,7 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
879
1711
|
);
|
|
880
1712
|
this.broadcastState();
|
|
881
1713
|
}).catch((err) => {
|
|
882
|
-
this.logger.error(`[AutoPhase] Aborted: ${
|
|
1714
|
+
this.logger.error(`[AutoPhase] Aborted: ${toErrorMessage(err)}`);
|
|
883
1715
|
this.stopBroadcast();
|
|
884
1716
|
this.broadcast({ type: "autophase.failed", payload: { title, error: String(err) } });
|
|
885
1717
|
});
|
|
@@ -912,7 +1744,7 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
912
1744
|
}
|
|
913
1745
|
this.logger.info(`[AutoPhase] Planner produced no phases; using defaults for: ${goal}`);
|
|
914
1746
|
} catch (err) {
|
|
915
|
-
this.logger.error(`[AutoPhase] Planning failed, using defaults: ${
|
|
1747
|
+
this.logger.error(`[AutoPhase] Planning failed, using defaults: ${toErrorMessage(err)}`);
|
|
916
1748
|
}
|
|
917
1749
|
return this.defaultPhases();
|
|
918
1750
|
}
|
|
@@ -1039,6 +1871,7 @@ Type: ${task.type}`;
|
|
|
1039
1871
|
|
|
1040
1872
|
// src/server/collaboration-ws-handler.ts
|
|
1041
1873
|
import { randomUUID } from "crypto";
|
|
1874
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
1042
1875
|
var REPLAY_LIMIT = 50;
|
|
1043
1876
|
var PAUSE_TIMEOUT_MS = 6e4;
|
|
1044
1877
|
var CollaborationWebSocketHandler = class {
|
|
@@ -1166,7 +1999,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1166
1999
|
if (this.reader) {
|
|
1167
2000
|
this.replayHistory(ws, sessionId).catch((err) => {
|
|
1168
2001
|
this.logger.debug?.(
|
|
1169
|
-
`collab: replay failed for ${sessionId}: ${
|
|
2002
|
+
`collab: replay failed for ${sessionId}: ${toErrorMessage2(err)}`
|
|
1170
2003
|
);
|
|
1171
2004
|
});
|
|
1172
2005
|
}
|
|
@@ -1276,7 +2109,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1276
2109
|
this.send(
|
|
1277
2110
|
ws,
|
|
1278
2111
|
this.errorMessage(
|
|
1279
|
-
`annotation rejected: ${
|
|
2112
|
+
`annotation rejected: ${toErrorMessage2(err)}`
|
|
1280
2113
|
)
|
|
1281
2114
|
);
|
|
1282
2115
|
}
|
|
@@ -1343,7 +2176,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1343
2176
|
this.send(
|
|
1344
2177
|
ws,
|
|
1345
2178
|
this.errorMessage(
|
|
1346
|
-
`resolve failed: ${
|
|
2179
|
+
`resolve failed: ${toErrorMessage2(err)}`
|
|
1347
2180
|
)
|
|
1348
2181
|
);
|
|
1349
2182
|
}
|
|
@@ -1391,7 +2224,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1391
2224
|
if (p.ws.readyState === 1) p.ws.send(data);
|
|
1392
2225
|
} catch (err) {
|
|
1393
2226
|
this.logger.debug?.(
|
|
1394
|
-
`collab broadcast failed: ${
|
|
2227
|
+
`collab broadcast failed: ${toErrorMessage2(err)}`
|
|
1395
2228
|
);
|
|
1396
2229
|
}
|
|
1397
2230
|
}
|
|
@@ -1418,7 +2251,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1418
2251
|
}
|
|
1419
2252
|
} catch (err) {
|
|
1420
2253
|
this.logger.debug?.(
|
|
1421
|
-
`collab: session reader rejected ${sessionId}: ${
|
|
2254
|
+
`collab: session reader rejected ${sessionId}: ${toErrorMessage2(err)}`
|
|
1422
2255
|
);
|
|
1423
2256
|
return;
|
|
1424
2257
|
}
|
|
@@ -1499,7 +2332,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1499
2332
|
if (p.ws.readyState === 1) p.ws.send(data);
|
|
1500
2333
|
} catch (err) {
|
|
1501
2334
|
this.logger.debug?.(
|
|
1502
|
-
`collab broadcast failed: ${
|
|
2335
|
+
`collab broadcast failed: ${toErrorMessage2(err)}`
|
|
1503
2336
|
);
|
|
1504
2337
|
}
|
|
1505
2338
|
}
|
|
@@ -1696,6 +2529,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1696
2529
|
};
|
|
1697
2530
|
|
|
1698
2531
|
// src/server/worktree-ws-handler.ts
|
|
2532
|
+
import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
|
|
1699
2533
|
var MAX_ACTIVITY = 6;
|
|
1700
2534
|
var WorktreeWebSocketHandler = class {
|
|
1701
2535
|
constructor(events, logger) {
|
|
@@ -1821,7 +2655,7 @@ var WorktreeWebSocketHandler = class {
|
|
|
1821
2655
|
try {
|
|
1822
2656
|
if (ws.readyState === 1) ws.send(data);
|
|
1823
2657
|
} catch (err) {
|
|
1824
|
-
this.logger.debug?.(`worktree broadcast failed: ${
|
|
2658
|
+
this.logger.debug?.(`worktree broadcast failed: ${toErrorMessage3(err)}`);
|
|
1825
2659
|
}
|
|
1826
2660
|
}
|
|
1827
2661
|
}
|
|
@@ -1834,22 +2668,14 @@ var WorktreeWebSocketHandler = class {
|
|
|
1834
2668
|
};
|
|
1835
2669
|
|
|
1836
2670
|
// src/server/mailbox-handlers.ts
|
|
1837
|
-
import
|
|
1838
|
-
import { GlobalMailbox } from "@wrongstack/core";
|
|
1839
|
-
function resolveProjectDir(projectRoot, globalRoot) {
|
|
1840
|
-
const { createHash } = __require("crypto");
|
|
1841
|
-
const hash = createHash("sha256").update(path3.resolve(projectRoot)).digest("hex").slice(0, 6);
|
|
1842
|
-
const slug = path3.basename(projectRoot).toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40) || "project";
|
|
1843
|
-
return path3.join(globalRoot, "projects", `${slug}-${hash}`);
|
|
1844
|
-
}
|
|
2671
|
+
import { GlobalMailbox, resolveProjectDir } from "@wrongstack/core";
|
|
1845
2672
|
async function handleMailboxMessages(ws, deps, payload) {
|
|
1846
2673
|
try {
|
|
1847
2674
|
const dir = resolveProjectDir(deps.projectRoot, deps.globalRoot);
|
|
1848
2675
|
const mb = new GlobalMailbox(dir);
|
|
1849
2676
|
const messages = await mb.query({
|
|
1850
2677
|
limit: payload?.limit ?? 30,
|
|
1851
|
-
|
|
1852
|
-
unreadBy: payload?.unreadOnly ? payload.agentId : void 0
|
|
2678
|
+
incompleteOnly: payload?.incompleteOnly ?? false
|
|
1853
2679
|
});
|
|
1854
2680
|
send(ws, {
|
|
1855
2681
|
type: "mailbox.messages",
|
|
@@ -1866,10 +2692,12 @@ async function handleMailboxMessages(ws, deps, payload) {
|
|
|
1866
2692
|
readByCount: Object.keys(m.readBy).length,
|
|
1867
2693
|
completed: m.completed,
|
|
1868
2694
|
completedBy: m.completedBy,
|
|
2695
|
+
completedAt: m.completedAt,
|
|
1869
2696
|
outcome: m.outcome,
|
|
1870
2697
|
timestamp: m.timestamp,
|
|
1871
2698
|
replyTo: m.replyTo,
|
|
1872
|
-
senderSessionId: m.senderSessionId
|
|
2699
|
+
senderSessionId: m.senderSessionId,
|
|
2700
|
+
taskContext: m.taskContext
|
|
1873
2701
|
}))
|
|
1874
2702
|
}
|
|
1875
2703
|
});
|
|
@@ -1916,6 +2744,16 @@ async function handleMailboxClear(ws, deps) {
|
|
|
1916
2744
|
send(ws, { type: "mailbox.cleared", payload: { error: errMessage(err) } });
|
|
1917
2745
|
}
|
|
1918
2746
|
}
|
|
2747
|
+
async function handleMailboxPurge(ws, deps, opts) {
|
|
2748
|
+
try {
|
|
2749
|
+
const dir = resolveProjectDir(deps.projectRoot, deps.globalRoot);
|
|
2750
|
+
const mb = new GlobalMailbox(dir);
|
|
2751
|
+
const result = await mb.purgeStale(opts);
|
|
2752
|
+
send(ws, { type: "mailbox.purged", payload: result });
|
|
2753
|
+
} catch (err) {
|
|
2754
|
+
send(ws, { type: "mailbox.purged", payload: { error: errMessage(err) } });
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
1919
2757
|
|
|
1920
2758
|
// src/server/lifecycle.ts
|
|
1921
2759
|
function createShutdown(res) {
|
|
@@ -1956,7 +2794,7 @@ function registerShutdownHandlers(res) {
|
|
|
1956
2794
|
// src/server/instance-registry.ts
|
|
1957
2795
|
import * as os from "os";
|
|
1958
2796
|
import * as path4 from "path";
|
|
1959
|
-
import * as
|
|
2797
|
+
import * as fs4 from "fs/promises";
|
|
1960
2798
|
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
1961
2799
|
function defaultBaseDir() {
|
|
1962
2800
|
return path4.join(os.homedir(), ".wrongstack");
|
|
@@ -1975,7 +2813,7 @@ function isPidAlive(pid) {
|
|
|
1975
2813
|
}
|
|
1976
2814
|
async function load(file) {
|
|
1977
2815
|
try {
|
|
1978
|
-
const raw = await
|
|
2816
|
+
const raw = await fs4.readFile(file, "utf8");
|
|
1979
2817
|
const parsed = JSON.parse(raw);
|
|
1980
2818
|
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
1981
2819
|
return parsed;
|
|
@@ -2034,16 +2872,16 @@ function formatInstances(instances) {
|
|
|
2034
2872
|
// src/server/port-utils.ts
|
|
2035
2873
|
import * as net from "net";
|
|
2036
2874
|
function isPortFree(host, port) {
|
|
2037
|
-
return new Promise((
|
|
2875
|
+
return new Promise((resolve5) => {
|
|
2038
2876
|
const srv = net.createServer();
|
|
2039
|
-
srv.once("error", () =>
|
|
2877
|
+
srv.once("error", () => resolve5(false));
|
|
2040
2878
|
srv.once("listening", () => {
|
|
2041
|
-
srv.close(() =>
|
|
2879
|
+
srv.close(() => resolve5(true));
|
|
2042
2880
|
});
|
|
2043
2881
|
try {
|
|
2044
2882
|
srv.listen(port, host);
|
|
2045
2883
|
} catch {
|
|
2046
|
-
|
|
2884
|
+
resolve5(false);
|
|
2047
2885
|
}
|
|
2048
2886
|
});
|
|
2049
2887
|
}
|
|
@@ -2118,8 +2956,12 @@ function computeUsageCost(usage, rates) {
|
|
|
2118
2956
|
return (usage.input * rates.input + usage.output * rates.output + (usage.cacheRead ?? 0) * rates.cacheRead) / 1e6;
|
|
2119
2957
|
}
|
|
2120
2958
|
|
|
2959
|
+
// src/server/provider-handlers.ts
|
|
2960
|
+
import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core";
|
|
2961
|
+
import { probeLocalLlm } from "@wrongstack/runtime/probe";
|
|
2962
|
+
|
|
2121
2963
|
// src/server/provider-config-io.ts
|
|
2122
|
-
import * as
|
|
2964
|
+
import * as fs5 from "fs/promises";
|
|
2123
2965
|
import * as path5 from "path";
|
|
2124
2966
|
import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
|
|
2125
2967
|
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
@@ -2127,7 +2969,7 @@ import { DefaultSecretVault } from "@wrongstack/core";
|
|
|
2127
2969
|
async function loadSavedProviders(configPath, vault) {
|
|
2128
2970
|
let raw;
|
|
2129
2971
|
try {
|
|
2130
|
-
raw = await
|
|
2972
|
+
raw = await fs5.readFile(configPath, "utf8");
|
|
2131
2973
|
} catch {
|
|
2132
2974
|
return {};
|
|
2133
2975
|
}
|
|
@@ -2144,7 +2986,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
2144
2986
|
let raw;
|
|
2145
2987
|
let fileExists = true;
|
|
2146
2988
|
try {
|
|
2147
|
-
raw = await
|
|
2989
|
+
raw = await fs5.readFile(configPath, "utf8");
|
|
2148
2990
|
} catch (err) {
|
|
2149
2991
|
if (err.code !== "ENOENT") {
|
|
2150
2992
|
throw new Error(
|
|
@@ -2180,6 +3022,9 @@ function createProviderConfigIO(configPath) {
|
|
|
2180
3022
|
};
|
|
2181
3023
|
}
|
|
2182
3024
|
|
|
3025
|
+
// src/server/provider-handlers.ts
|
|
3026
|
+
import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
|
|
3027
|
+
|
|
2183
3028
|
// src/server/provider-keys.ts
|
|
2184
3029
|
import { expectDefined } from "@wrongstack/core";
|
|
2185
3030
|
function normalizeKeys(cfg) {
|
|
@@ -2200,7 +3045,7 @@ function writeKeysBack(cfg, keys) {
|
|
|
2200
3045
|
}
|
|
2201
3046
|
cfg.apiKeys = keys;
|
|
2202
3047
|
const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined(keys[0]);
|
|
2203
|
-
cfg.apiKey
|
|
3048
|
+
delete cfg.apiKey;
|
|
2204
3049
|
if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
|
|
2205
3050
|
cfg.activeKey = active.label;
|
|
2206
3051
|
}
|
|
@@ -2277,6 +3122,28 @@ function removeProvider(providers, providerId) {
|
|
|
2277
3122
|
}
|
|
2278
3123
|
|
|
2279
3124
|
// src/server/provider-handlers.ts
|
|
3125
|
+
function projectSavedProviders(providers) {
|
|
3126
|
+
return Object.entries(providers).map(([id, cfg]) => {
|
|
3127
|
+
const keys = normalizeKeys(cfg);
|
|
3128
|
+
const models = cfg.models;
|
|
3129
|
+
const view = {
|
|
3130
|
+
id,
|
|
3131
|
+
family: cfg.family ?? id,
|
|
3132
|
+
baseUrl: cfg.baseUrl,
|
|
3133
|
+
models,
|
|
3134
|
+
apiKeys: keys.map((k) => ({
|
|
3135
|
+
label: k.label,
|
|
3136
|
+
maskedKey: maskedKey(k.apiKey),
|
|
3137
|
+
isActive: k.label === cfg.activeKey,
|
|
3138
|
+
createdAt: k.createdAt
|
|
3139
|
+
}))
|
|
3140
|
+
};
|
|
3141
|
+
const picked = models && models.length > 0 ? models[0] : void 0;
|
|
3142
|
+
if (picked !== void 0) view.pickedModelId = picked;
|
|
3143
|
+
return view;
|
|
3144
|
+
});
|
|
3145
|
+
}
|
|
3146
|
+
var probeScrubber = new DefaultSecretScrubber2();
|
|
2280
3147
|
function createProviderHandlers(deps) {
|
|
2281
3148
|
const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps;
|
|
2282
3149
|
let configWriteLock = deps.getConfigWriteLock();
|
|
@@ -2285,7 +3152,7 @@ function createProviderHandlers(deps) {
|
|
|
2285
3152
|
}
|
|
2286
3153
|
async function saveConfigProviders(providers) {
|
|
2287
3154
|
const next = configWriteLock.then(() => saveProviders(globalConfigPath, vault, providers)).catch((err) => {
|
|
2288
|
-
const msg =
|
|
3155
|
+
const msg = toErrorMessage4(err);
|
|
2289
3156
|
console.error(JSON.stringify({
|
|
2290
3157
|
level: "error",
|
|
2291
3158
|
event: "webui.provider_save_failed",
|
|
@@ -2302,9 +3169,9 @@ function createProviderHandlers(deps) {
|
|
|
2302
3169
|
const providers = await loadConfigProviders();
|
|
2303
3170
|
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
2304
3171
|
if (result.ok) await saveConfigProviders(providers);
|
|
2305
|
-
|
|
3172
|
+
sendResult2(ws, result.ok, result.message);
|
|
2306
3173
|
} catch (err) {
|
|
2307
|
-
|
|
3174
|
+
sendResult2(ws, false, errMessage(err));
|
|
2308
3175
|
}
|
|
2309
3176
|
}
|
|
2310
3177
|
async function handleKeyDelete(ws, providerId, label) {
|
|
@@ -2312,9 +3179,9 @@ function createProviderHandlers(deps) {
|
|
|
2312
3179
|
const providers = await loadConfigProviders();
|
|
2313
3180
|
const result = deleteKey(providers, providerId, label);
|
|
2314
3181
|
if (result.ok) await saveConfigProviders(providers);
|
|
2315
|
-
|
|
3182
|
+
sendResult2(ws, result.ok, result.message);
|
|
2316
3183
|
} catch (err) {
|
|
2317
|
-
|
|
3184
|
+
sendResult2(ws, false, errMessage(err));
|
|
2318
3185
|
}
|
|
2319
3186
|
}
|
|
2320
3187
|
async function handleKeySetActive(ws, providerId, label) {
|
|
@@ -2322,9 +3189,9 @@ function createProviderHandlers(deps) {
|
|
|
2322
3189
|
const providers = await loadConfigProviders();
|
|
2323
3190
|
const result = setActiveKey(providers, providerId, label);
|
|
2324
3191
|
if (result.ok) await saveConfigProviders(providers);
|
|
2325
|
-
|
|
3192
|
+
sendResult2(ws, result.ok, result.message);
|
|
2326
3193
|
} catch (err) {
|
|
2327
|
-
|
|
3194
|
+
sendResult2(ws, false, errMessage(err));
|
|
2328
3195
|
}
|
|
2329
3196
|
}
|
|
2330
3197
|
async function handleProviderAdd(ws, payload) {
|
|
@@ -2332,31 +3199,13 @@ function createProviderHandlers(deps) {
|
|
|
2332
3199
|
const providers = await loadConfigProviders();
|
|
2333
3200
|
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
2334
3201
|
if (result.ok) await saveConfigProviders(providers);
|
|
2335
|
-
|
|
3202
|
+
sendResult2(ws, result.ok, result.message);
|
|
2336
3203
|
if (result.ok) {
|
|
2337
3204
|
console.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
2338
|
-
|
|
2339
|
-
type: "providers.saved",
|
|
2340
|
-
payload: {
|
|
2341
|
-
providers: Object.entries(providers).map(([id, cfg]) => {
|
|
2342
|
-
const keys = normalizeKeys(cfg);
|
|
2343
|
-
return {
|
|
2344
|
-
id,
|
|
2345
|
-
family: cfg.family ?? id,
|
|
2346
|
-
baseUrl: cfg.baseUrl,
|
|
2347
|
-
apiKeys: keys.map((k) => ({
|
|
2348
|
-
label: k.label,
|
|
2349
|
-
maskedKey: maskedKey(k.apiKey),
|
|
2350
|
-
isActive: k.label === cfg.activeKey,
|
|
2351
|
-
createdAt: k.createdAt
|
|
2352
|
-
}))
|
|
2353
|
-
};
|
|
2354
|
-
})
|
|
2355
|
-
}
|
|
2356
|
-
});
|
|
3205
|
+
broadcastSaved(providers);
|
|
2357
3206
|
}
|
|
2358
3207
|
} catch (err) {
|
|
2359
|
-
|
|
3208
|
+
sendResult2(ws, false, errMessage(err));
|
|
2360
3209
|
}
|
|
2361
3210
|
}
|
|
2362
3211
|
async function handleProviderRemove(ws, providerId) {
|
|
@@ -2364,18 +3213,116 @@ function createProviderHandlers(deps) {
|
|
|
2364
3213
|
const providers = await loadConfigProviders();
|
|
2365
3214
|
const result = removeProvider(providers, providerId);
|
|
2366
3215
|
if (result.ok) await saveConfigProviders(providers);
|
|
2367
|
-
|
|
3216
|
+
sendResult2(ws, result.ok, result.message);
|
|
3217
|
+
} catch (err) {
|
|
3218
|
+
sendResult2(ws, false, errMessage(err));
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
function broadcastSaved(providers) {
|
|
3222
|
+
broadcast2(clients, {
|
|
3223
|
+
type: "providers.saved",
|
|
3224
|
+
payload: { providers: projectSavedProviders(providers) }
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
async function handleProviderClearModels(ws, providerId) {
|
|
3228
|
+
try {
|
|
3229
|
+
const providers = await loadConfigProviders();
|
|
3230
|
+
const cfg = providers[providerId];
|
|
3231
|
+
if (!cfg) {
|
|
3232
|
+
sendResult2(ws, false, `Unknown provider "${providerId}"`);
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
delete cfg.models;
|
|
3236
|
+
await saveConfigProviders(providers);
|
|
3237
|
+
sendResult2(ws, true, `Cleared model allowlist for ${providerId}`);
|
|
3238
|
+
broadcastSaved(providers);
|
|
3239
|
+
} catch (err) {
|
|
3240
|
+
sendResult2(ws, false, errMessage(err));
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
async function handleProviderUndoClear(ws, providerId, previousModels) {
|
|
3244
|
+
try {
|
|
3245
|
+
const providers = await loadConfigProviders();
|
|
3246
|
+
const cfg = providers[providerId];
|
|
3247
|
+
if (!cfg) {
|
|
3248
|
+
sendResult2(ws, false, `Unknown provider "${providerId}"`);
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3251
|
+
cfg.models = [...previousModels];
|
|
3252
|
+
await saveConfigProviders(providers);
|
|
3253
|
+
sendResult2(ws, true, `Restored ${previousModels.length} model(s) for ${providerId}`);
|
|
3254
|
+
broadcastSaved(providers);
|
|
3255
|
+
} catch (err) {
|
|
3256
|
+
sendResult2(ws, false, errMessage(err));
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
async function handleProviderUpdate(ws, payload) {
|
|
3260
|
+
try {
|
|
3261
|
+
const providers = await loadConfigProviders();
|
|
3262
|
+
const cfg = providers[payload.id];
|
|
3263
|
+
if (!cfg) {
|
|
3264
|
+
sendResult2(ws, false, `Unknown provider "${payload.id}"`);
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
if (payload.family !== void 0) cfg.family = payload.family;
|
|
3268
|
+
if (payload.baseUrl !== void 0) cfg.baseUrl = payload.baseUrl;
|
|
3269
|
+
if (payload.envVars !== void 0) cfg.envVars = payload.envVars;
|
|
3270
|
+
if (payload.models !== void 0) cfg.models = payload.models;
|
|
3271
|
+
await saveConfigProviders(providers);
|
|
3272
|
+
sendResult2(ws, true, `Updated ${payload.id}`);
|
|
3273
|
+
broadcastSaved(providers);
|
|
3274
|
+
} catch (err) {
|
|
3275
|
+
sendResult2(ws, false, errMessage(err));
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
async function handleProviderProbe(ws, providerId, timeoutMs) {
|
|
3279
|
+
const reply = (payload) => send(ws, { type: "provider.probe", payload: { providerId, ...payload } });
|
|
3280
|
+
try {
|
|
3281
|
+
const providers = await loadConfigProviders();
|
|
3282
|
+
const cfg = providers[providerId];
|
|
3283
|
+
if (!cfg) {
|
|
3284
|
+
reply({ ok: false, status: "no_provider" });
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
if (!cfg.baseUrl) {
|
|
3288
|
+
reply({ ok: false, status: "no_base_url" });
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3291
|
+
const keys = normalizeKeys(cfg);
|
|
3292
|
+
const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
|
|
3293
|
+
const result = await probeLocalLlm({
|
|
3294
|
+
baseUrl: cfg.baseUrl,
|
|
3295
|
+
apiKey: active?.apiKey,
|
|
3296
|
+
noAuth: false,
|
|
3297
|
+
scrubber: probeScrubber,
|
|
3298
|
+
...timeoutMs !== void 0 ? { timeoutMs } : {}
|
|
3299
|
+
});
|
|
3300
|
+
reply(result);
|
|
2368
3301
|
} catch (err) {
|
|
2369
|
-
|
|
3302
|
+
reply({ ok: false, status: "unreachable", detail: errMessage(err) });
|
|
2370
3303
|
}
|
|
2371
3304
|
}
|
|
2372
|
-
return {
|
|
3305
|
+
return {
|
|
3306
|
+
handleKeyUpsert,
|
|
3307
|
+
handleKeyDelete,
|
|
3308
|
+
handleKeySetActive,
|
|
3309
|
+
handleProviderAdd,
|
|
3310
|
+
handleProviderRemove,
|
|
3311
|
+
handleProviderClearModels,
|
|
3312
|
+
handleProviderUndoClear,
|
|
3313
|
+
handleProviderUpdate,
|
|
3314
|
+
handleProviderProbe,
|
|
3315
|
+
loadConfigProviders
|
|
3316
|
+
};
|
|
2373
3317
|
}
|
|
2374
3318
|
|
|
2375
3319
|
// src/server/setup-events.ts
|
|
3320
|
+
import * as fs6 from "fs/promises";
|
|
3321
|
+
import { watch as fsWatch } from "fs";
|
|
2376
3322
|
import * as path6 from "path";
|
|
2377
3323
|
function setupEvents(deps) {
|
|
2378
|
-
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge } = deps;
|
|
3324
|
+
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps;
|
|
3325
|
+
const disposers = [];
|
|
2379
3326
|
events.on("iteration.started", (e) => {
|
|
2380
3327
|
const maxIt = typeof context.meta["maxIterations"] === "number" ? context.meta["maxIterations"] : config.tools?.maxIterations ?? 100;
|
|
2381
3328
|
broadcast2(clients, {
|
|
@@ -2406,7 +3353,11 @@ function setupEvents(deps) {
|
|
|
2406
3353
|
events.on("tool.progress", (e) => {
|
|
2407
3354
|
broadcast2(clients, {
|
|
2408
3355
|
type: "tool.progress",
|
|
2409
|
-
|
|
3356
|
+
// Nested `event` shape — the client handler reads `payload.event?.text`
|
|
3357
|
+
// and early-returns on a falsy text, so a flat { eventType, text } payload
|
|
3358
|
+
// makes live tool progress (bash streaming, partial_output, warnings)
|
|
3359
|
+
// never render. Must match WSToolProgress and the CLI server.
|
|
3360
|
+
payload: { id: e.id, name: e.name, event: { type: e.event.type, text: e.event.text, data: e.event.data } }
|
|
2410
3361
|
});
|
|
2411
3362
|
sessionBridge?.append({
|
|
2412
3363
|
type: "tool_progress",
|
|
@@ -2572,20 +3523,165 @@ function setupEvents(deps) {
|
|
|
2572
3523
|
events.onPattern("brain.*", (eventName, payload) => {
|
|
2573
3524
|
broadcast2(clients, { type: "brain.event", payload: { event: eventName, ...payload } });
|
|
2574
3525
|
});
|
|
3526
|
+
events.on("client.status", async (e) => {
|
|
3527
|
+
broadcast2(clients, { type: "client.status_update", payload: e });
|
|
3528
|
+
if (wpaths?.projectStatus) {
|
|
3529
|
+
try {
|
|
3530
|
+
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
3531
|
+
const dir = path6.dirname(statusFile);
|
|
3532
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
3533
|
+
await fs6.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
|
|
3534
|
+
} catch (err) {
|
|
3535
|
+
console.error("[setup-events] Failed to write status.json:", err);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
});
|
|
3539
|
+
if (wpaths?.projectStatus && wpaths.configDir) {
|
|
3540
|
+
const projectsDir = path6.join(wpaths.configDir, "projects");
|
|
3541
|
+
const knownProjectHashes = /* @__PURE__ */ new Set();
|
|
3542
|
+
const debounceTimers = /* @__PURE__ */ new Map();
|
|
3543
|
+
const DEBOUNCE_MS = 150;
|
|
3544
|
+
const pendingStatuses = /* @__PURE__ */ new Map();
|
|
3545
|
+
if (watcherMetrics) {
|
|
3546
|
+
watcherMetrics.fileChangesDetected = 0;
|
|
3547
|
+
watcherMetrics.filesProcessed = 0;
|
|
3548
|
+
watcherMetrics.broadcastsSent = 0;
|
|
3549
|
+
watcherMetrics.debounceResets = 0;
|
|
3550
|
+
watcherMetrics.totalDebounceDelayMs = 0;
|
|
3551
|
+
watcherMetrics.activeProjects = 0;
|
|
3552
|
+
watcherMetrics.averageDebounceDelayMs = 0;
|
|
3553
|
+
watcherMetrics.watcherActive = true;
|
|
3554
|
+
}
|
|
3555
|
+
const getAverageDebounceDelay = () => {
|
|
3556
|
+
if (!watcherMetrics || watcherMetrics.broadcastsSent === 0) return 0;
|
|
3557
|
+
return watcherMetrics.totalDebounceDelayMs / watcherMetrics.broadcastsSent;
|
|
3558
|
+
};
|
|
3559
|
+
const logWatcherMetrics = () => {
|
|
3560
|
+
if (!watcherMetrics) return;
|
|
3561
|
+
watcherMetrics.averageDebounceDelayMs = getAverageDebounceDelay();
|
|
3562
|
+
console.log(
|
|
3563
|
+
`[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`
|
|
3564
|
+
);
|
|
3565
|
+
};
|
|
3566
|
+
const metricsInterval = setInterval(logWatcherMetrics, 6e4);
|
|
3567
|
+
const broadcastStatus = (projectHash2, statusData, actualDelayMs) => {
|
|
3568
|
+
broadcast2(clients, { type: "client.status_update", payload: statusData });
|
|
3569
|
+
if (watcherMetrics) {
|
|
3570
|
+
watcherMetrics.broadcastsSent++;
|
|
3571
|
+
watcherMetrics.totalDebounceDelayMs += actualDelayMs;
|
|
3572
|
+
watcherMetrics.averageDebounceDelayMs = getAverageDebounceDelay();
|
|
3573
|
+
}
|
|
3574
|
+
};
|
|
3575
|
+
const scheduleBroadcast = (projectHash2, statusData) => {
|
|
3576
|
+
const now = Date.now();
|
|
3577
|
+
const existing = pendingStatuses.get(projectHash2);
|
|
3578
|
+
if (existing && watcherMetrics) {
|
|
3579
|
+
watcherMetrics.debounceResets++;
|
|
3580
|
+
}
|
|
3581
|
+
pendingStatuses.set(projectHash2, {
|
|
3582
|
+
data: statusData,
|
|
3583
|
+
firstWriteAt: existing ? existing.firstWriteAt : now
|
|
3584
|
+
});
|
|
3585
|
+
const existingTimer = debounceTimers.get(projectHash2);
|
|
3586
|
+
if (existingTimer) {
|
|
3587
|
+
clearTimeout(existingTimer);
|
|
3588
|
+
}
|
|
3589
|
+
const timer = setTimeout(() => {
|
|
3590
|
+
debounceTimers.delete(projectHash2);
|
|
3591
|
+
const pending = pendingStatuses.get(projectHash2);
|
|
3592
|
+
if (pending) {
|
|
3593
|
+
const actualDelay = Date.now() - pending.firstWriteAt;
|
|
3594
|
+
broadcastStatus(projectHash2, pending.data, actualDelay);
|
|
3595
|
+
pendingStatuses.delete(projectHash2);
|
|
3596
|
+
}
|
|
3597
|
+
}, DEBOUNCE_MS);
|
|
3598
|
+
debounceTimers.set(projectHash2, timer);
|
|
3599
|
+
};
|
|
3600
|
+
let watcher;
|
|
3601
|
+
const startWatcher = async () => {
|
|
3602
|
+
try {
|
|
3603
|
+
await fs6.mkdir(projectsDir, { recursive: true });
|
|
3604
|
+
watcher = fsWatch(projectsDir, { persistent: true, recursive: true }, async (eventType, filename) => {
|
|
3605
|
+
if (eventType === "change") {
|
|
3606
|
+
if (filename == null) return;
|
|
3607
|
+
if (watcherMetrics) watcherMetrics.fileChangesDetected++;
|
|
3608
|
+
const targetFile = path6.join(projectsDir, String(filename));
|
|
3609
|
+
if (targetFile.endsWith("status.json")) {
|
|
3610
|
+
const projectHash2 = path6.basename(path6.dirname(targetFile));
|
|
3611
|
+
if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
|
|
3612
|
+
return;
|
|
3613
|
+
}
|
|
3614
|
+
if (watcherMetrics) watcherMetrics.filesProcessed++;
|
|
3615
|
+
try {
|
|
3616
|
+
const content = await fs6.readFile(targetFile, "utf-8");
|
|
3617
|
+
const statusData = JSON.parse(content);
|
|
3618
|
+
if (statusData.projectHash) {
|
|
3619
|
+
const hash = String(statusData.projectHash);
|
|
3620
|
+
if (!knownProjectHashes.has(hash)) {
|
|
3621
|
+
knownProjectHashes.add(hash);
|
|
3622
|
+
if (watcherMetrics) watcherMetrics.activeProjects = knownProjectHashes.size;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
scheduleBroadcast(projectHash2, statusData);
|
|
3626
|
+
} catch {
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
});
|
|
3631
|
+
console.log(`[setup-events] Watching ${projectsDir} for status.json changes (hash-filtered, debounced)`);
|
|
3632
|
+
} catch (err) {
|
|
3633
|
+
console.error("[setup-events] Failed to start status file watcher:", err);
|
|
3634
|
+
}
|
|
3635
|
+
};
|
|
3636
|
+
events.on("client.status", (e) => {
|
|
3637
|
+
if (e.projectHash) {
|
|
3638
|
+
const hash = String(e.projectHash);
|
|
3639
|
+
if (!knownProjectHashes.has(hash)) {
|
|
3640
|
+
knownProjectHashes.add(hash);
|
|
3641
|
+
if (watcherMetrics) watcherMetrics.activeProjects = knownProjectHashes.size;
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
});
|
|
3645
|
+
startWatcher();
|
|
3646
|
+
disposers.push(() => {
|
|
3647
|
+
clearInterval(metricsInterval);
|
|
3648
|
+
logWatcherMetrics();
|
|
3649
|
+
if (watcherMetrics) watcherMetrics.watcherActive = false;
|
|
3650
|
+
for (const [projectHash2, pending] of pendingStatuses) {
|
|
3651
|
+
const timer = debounceTimers.get(projectHash2);
|
|
3652
|
+
if (timer) {
|
|
3653
|
+
clearTimeout(timer);
|
|
3654
|
+
broadcastStatus(projectHash2, pending.data, 0);
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
for (const timer of debounceTimers.values()) {
|
|
3658
|
+
clearTimeout(timer);
|
|
3659
|
+
}
|
|
3660
|
+
debounceTimers.clear();
|
|
3661
|
+
pendingStatuses.clear();
|
|
3662
|
+
if (watcher) {
|
|
3663
|
+
watcher.close();
|
|
3664
|
+
console.log("[setup-events] Closed status file watcher");
|
|
3665
|
+
}
|
|
3666
|
+
});
|
|
3667
|
+
}
|
|
2575
3668
|
const globalRoot = globalConfigPath ? path6.dirname(globalConfigPath) : void 0;
|
|
2576
3669
|
if (globalRoot) {
|
|
2577
|
-
const
|
|
3670
|
+
const broadcastSessions = async () => {
|
|
2578
3671
|
try {
|
|
2579
3672
|
const { SessionRegistry } = await import("@wrongstack/core");
|
|
2580
3673
|
const registry = new SessionRegistry(globalRoot);
|
|
2581
3674
|
const sessions = await registry.list();
|
|
2582
|
-
const
|
|
3675
|
+
const mySlug = sessions.find((s) => s.pid === process.pid)?.projectSlug;
|
|
3676
|
+
const live = sessions.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true).map((s) => ({
|
|
2583
3677
|
sessionId: s.sessionId,
|
|
2584
3678
|
projectName: s.projectName,
|
|
2585
3679
|
projectSlug: s.projectSlug,
|
|
2586
3680
|
projectRoot: s.projectRoot,
|
|
2587
3681
|
workingDir: s.workingDir,
|
|
2588
3682
|
gitBranch: s.gitBranch,
|
|
3683
|
+
// Surface (tui/webui/cli) so Fleet HQ can label each live client node.
|
|
3684
|
+
clientType: s.clientType,
|
|
2589
3685
|
status: s.status,
|
|
2590
3686
|
pid: s.pid,
|
|
2591
3687
|
startedAt: s.startedAt,
|
|
@@ -2597,20 +3693,52 @@ function setupEvents(deps) {
|
|
|
2597
3693
|
currentTool: a.currentTool,
|
|
2598
3694
|
iterations: a.iterations,
|
|
2599
3695
|
toolCalls: a.toolCalls,
|
|
3696
|
+
costUsd: a.costUsd,
|
|
3697
|
+
tokensIn: a.tokensIn,
|
|
3698
|
+
tokensOut: a.tokensOut,
|
|
3699
|
+
ctxPct: a.ctxPct,
|
|
3700
|
+
model: a.model,
|
|
3701
|
+
partialText: a.partialText,
|
|
2600
3702
|
lastActivityAt: a.lastActivityAt
|
|
2601
3703
|
}))
|
|
2602
3704
|
}));
|
|
2603
3705
|
broadcast2(clients, { type: "sessions.status_update", payload: { sessions: live } });
|
|
2604
3706
|
} catch {
|
|
2605
3707
|
}
|
|
2606
|
-
}
|
|
3708
|
+
};
|
|
3709
|
+
onFleetBroadcaster?.(broadcastSessions);
|
|
3710
|
+
const statusInterval = setInterval(() => void broadcastSessions(), 5e3);
|
|
2607
3711
|
if (statusInterval.unref) statusInterval.unref();
|
|
3712
|
+
disposers.push(() => clearInterval(statusInterval));
|
|
3713
|
+
let regDebounce;
|
|
3714
|
+
try {
|
|
3715
|
+
const regWatcher = fsWatch(globalRoot, { persistent: false }, (_event, filename) => {
|
|
3716
|
+
const name = filename ? String(filename) : "";
|
|
3717
|
+
if (!name.startsWith("session-registry.json") || name.endsWith(".lock")) return;
|
|
3718
|
+
if (regDebounce) clearTimeout(regDebounce);
|
|
3719
|
+
regDebounce = setTimeout(() => void broadcastSessions(), 150);
|
|
3720
|
+
});
|
|
3721
|
+
disposers.push(() => {
|
|
3722
|
+
if (regDebounce) clearTimeout(regDebounce);
|
|
3723
|
+
regWatcher.close();
|
|
3724
|
+
});
|
|
3725
|
+
} catch {
|
|
3726
|
+
}
|
|
3727
|
+
void broadcastSessions();
|
|
2608
3728
|
}
|
|
3729
|
+
return () => {
|
|
3730
|
+
for (const dispose of disposers) {
|
|
3731
|
+
try {
|
|
3732
|
+
dispose();
|
|
3733
|
+
} catch {
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
};
|
|
2609
3737
|
}
|
|
2610
3738
|
|
|
2611
3739
|
// src/server/custom-context-modes.ts
|
|
2612
3740
|
import { listContextWindowModes, atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
2613
|
-
import * as
|
|
3741
|
+
import * as fs7 from "fs/promises";
|
|
2614
3742
|
import * as path7 from "path";
|
|
2615
3743
|
var STORE_FILENAME = "custom-context-modes.json";
|
|
2616
3744
|
function storePath(wrongstackDir) {
|
|
@@ -2622,7 +3750,7 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
2622
3750
|
const load2 = async () => {
|
|
2623
3751
|
modes.clear();
|
|
2624
3752
|
try {
|
|
2625
|
-
const raw = await
|
|
3753
|
+
const raw = await fs7.readFile(storePath(wrongstackDir), "utf8");
|
|
2626
3754
|
const parsed = JSON.parse(raw);
|
|
2627
3755
|
if (Array.isArray(parsed.modes)) {
|
|
2628
3756
|
for (const m of parsed.modes) {
|
|
@@ -2798,60 +3926,346 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
|
|
|
2798
3926
|
disposed = true;
|
|
2799
3927
|
dispose();
|
|
2800
3928
|
}
|
|
2801
|
-
};
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
// src/server/shell-open.ts
|
|
3933
|
+
import * as fs8 from "fs/promises";
|
|
3934
|
+
import * as path8 from "path";
|
|
3935
|
+
import { spawn as spawn2 } from "child_process";
|
|
3936
|
+
var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
|
|
3937
|
+
async function handleShellOpen(req, logger) {
|
|
3938
|
+
try {
|
|
3939
|
+
const resolved = path8.resolve(req.path);
|
|
3940
|
+
await fs8.access(resolved);
|
|
3941
|
+
if (METACHAR_REGEX.test(resolved)) {
|
|
3942
|
+
return { success: false, message: "Path contains unsupported characters." };
|
|
3943
|
+
}
|
|
3944
|
+
const platform = process.platform;
|
|
3945
|
+
const launch = (cmd, args, onError) => {
|
|
3946
|
+
const child = spawn2(cmd, args, {
|
|
3947
|
+
detached: true,
|
|
3948
|
+
stdio: "ignore",
|
|
3949
|
+
windowsHide: true
|
|
3950
|
+
});
|
|
3951
|
+
child.on("error", (err) => {
|
|
3952
|
+
logger.warn(`shell.open spawn failed: ${err.message}`);
|
|
3953
|
+
onError?.();
|
|
3954
|
+
});
|
|
3955
|
+
child.unref();
|
|
3956
|
+
};
|
|
3957
|
+
if (req.target === "file-manager") {
|
|
3958
|
+
if (platform === "win32") launch("explorer", [resolved]);
|
|
3959
|
+
else if (platform === "darwin") launch("open", [resolved]);
|
|
3960
|
+
else launch("xdg-open", [resolved]);
|
|
3961
|
+
} else if (req.target === "terminal") {
|
|
3962
|
+
if (platform === "win32") {
|
|
3963
|
+
launch("cmd", ["/c", "start", "cmd", "/k", "cd", "/d", resolved]);
|
|
3964
|
+
} else if (platform === "darwin") {
|
|
3965
|
+
launch("open", ["-a", "Terminal", resolved]);
|
|
3966
|
+
} else {
|
|
3967
|
+
launch(
|
|
3968
|
+
"x-terminal-emulator",
|
|
3969
|
+
[`--working-directory=${resolved}`],
|
|
3970
|
+
() => launch(
|
|
3971
|
+
"gnome-terminal",
|
|
3972
|
+
[`--working-directory=${resolved}`],
|
|
3973
|
+
() => launch("xterm", ["-e", `cd '${resolved}' && ${process.env["SHELL"] ?? "sh"}`])
|
|
3974
|
+
)
|
|
3975
|
+
);
|
|
3976
|
+
}
|
|
3977
|
+
} else {
|
|
3978
|
+
return { success: false, message: `Unknown shell.open target: ${String(req.target)}` };
|
|
3979
|
+
}
|
|
3980
|
+
return { success: true, message: `Opened ${req.target} at ${resolved}` };
|
|
3981
|
+
} catch (err) {
|
|
3982
|
+
return { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
// src/server/git-handlers.ts
|
|
3987
|
+
async function handleGitInfo(ws, projectRoot) {
|
|
3988
|
+
const cwd = projectRoot || void 0;
|
|
3989
|
+
try {
|
|
3990
|
+
const { execFile: ef } = await import("child_process");
|
|
3991
|
+
const git = (args) => new Promise((resolve5) => {
|
|
3992
|
+
ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
|
|
3993
|
+
resolve5(err ? "" : stdout.trim());
|
|
3994
|
+
});
|
|
3995
|
+
});
|
|
3996
|
+
const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
|
|
3997
|
+
git(["branch", "--show-current"]),
|
|
3998
|
+
git(["diff", "--stat"]),
|
|
3999
|
+
git(["status", "--porcelain"]),
|
|
4000
|
+
git(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"])
|
|
4001
|
+
]);
|
|
4002
|
+
const branch = branchRaw || "(detached)";
|
|
4003
|
+
const addMatch = /(\d+)\s+insertion/i.exec(diffRaw);
|
|
4004
|
+
const delMatch = /(\d+)\s+deletion/i.exec(diffRaw);
|
|
4005
|
+
const added = addMatch ? Number(addMatch[1]) : 0;
|
|
4006
|
+
const deleted = delMatch ? Number(delMatch[1]) : 0;
|
|
4007
|
+
const untracked = statusRaw.split("\n").filter((l) => l.startsWith("??")).length;
|
|
4008
|
+
const [behindRaw, aheadRaw] = (upstreamRaw || "0 0").split(" ");
|
|
4009
|
+
const behind = Number(behindRaw) || 0;
|
|
4010
|
+
const ahead = Number(aheadRaw) || 0;
|
|
4011
|
+
send(ws, { type: "git.info", payload: { branch, added, deleted, untracked, ahead, behind } });
|
|
4012
|
+
} catch {
|
|
4013
|
+
send(ws, { type: "git.info", payload: { branch: "", added: 0, deleted: 0, untracked: 0, ahead: 0, behind: 0 } });
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
// src/server/skills-handlers.ts
|
|
4018
|
+
import { promises as fs9 } from "fs";
|
|
4019
|
+
import path9 from "path";
|
|
4020
|
+
import JSZip from "jszip";
|
|
4021
|
+
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
4022
|
+
async function handleSkillsContent(ws, ctx, msg) {
|
|
4023
|
+
if (!ctx.skillLoader) {
|
|
4024
|
+
send(ws, { type: "skills.content", payload: { name: "", body: "", path: "", source: "", relatedFiles: [], references: [], error: "Skills not enabled" } });
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
const contentPayload = msg.payload;
|
|
4028
|
+
if (!contentPayload?.name) {
|
|
4029
|
+
send(ws, { type: "skills.content", payload: { name: "", body: "", path: "", source: "", relatedFiles: [], references: [], error: "Skill name is required" } });
|
|
4030
|
+
return;
|
|
4031
|
+
}
|
|
4032
|
+
try {
|
|
4033
|
+
const { name, source } = contentPayload;
|
|
4034
|
+
const entries = await ctx.skillLoader.listEntries();
|
|
4035
|
+
const entry = entries.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
4036
|
+
if (!entry) {
|
|
4037
|
+
send(ws, { type: "skills.content", payload: { name, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name}" not found` } });
|
|
4038
|
+
return;
|
|
4039
|
+
}
|
|
4040
|
+
const body = await fs9.readFile(entry.path, "utf8");
|
|
4041
|
+
const skillDir = path9.dirname(entry.path);
|
|
4042
|
+
let relatedFiles = [];
|
|
4043
|
+
try {
|
|
4044
|
+
const files = await fs9.readdir(skillDir);
|
|
4045
|
+
relatedFiles = files.filter((f) => f !== path9.basename(entry.path)).map((f) => path9.join(skillDir, f));
|
|
4046
|
+
} catch {
|
|
4047
|
+
}
|
|
4048
|
+
const nameLower = name.toLowerCase();
|
|
4049
|
+
const refResults = await Promise.all(
|
|
4050
|
+
entries.filter((e) => e.name.toLowerCase() !== nameLower).map(async (e) => {
|
|
4051
|
+
try {
|
|
4052
|
+
const content = await fs9.readFile(e.path, "utf8");
|
|
4053
|
+
return [e.name, content.toLowerCase().includes(nameLower)];
|
|
4054
|
+
} catch {
|
|
4055
|
+
return [e.name, false];
|
|
4056
|
+
}
|
|
4057
|
+
})
|
|
4058
|
+
);
|
|
4059
|
+
const refs = refResults.filter(([, hasRef]) => hasRef).map(([n]) => n);
|
|
4060
|
+
send(ws, { type: "skills.content", payload: { name, body, path: entry.path, source, relatedFiles, references: refs } });
|
|
4061
|
+
} catch (err) {
|
|
4062
|
+
send(ws, { type: "skills.content", payload: { name: contentPayload.name, body: "", path: "", source: contentPayload.source, relatedFiles: [], references: [], error: errMessage(err) } });
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
async function handleSkillsInstall(ws, ctx, msg) {
|
|
4066
|
+
if (!ctx.skillInstaller) {
|
|
4067
|
+
send(ws, { type: "skills.installed", payload: { success: false, error: "Skills not enabled" } });
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
const installPayload = msg.payload;
|
|
4071
|
+
if (!installPayload?.ref?.trim()) {
|
|
4072
|
+
send(ws, { type: "skills.installed", payload: { success: false, error: "Skill reference is required (e.g. owner/repo or https://github.com/owner/repo)" } });
|
|
4073
|
+
return;
|
|
4074
|
+
}
|
|
4075
|
+
try {
|
|
4076
|
+
const results = await ctx.skillInstaller.install(installPayload.ref.trim(), { global: installPayload.global });
|
|
4077
|
+
send(ws, {
|
|
4078
|
+
type: "skills.installed",
|
|
4079
|
+
payload: {
|
|
4080
|
+
success: true,
|
|
4081
|
+
results,
|
|
4082
|
+
error: null
|
|
4083
|
+
}
|
|
4084
|
+
});
|
|
4085
|
+
} catch (err) {
|
|
4086
|
+
send(ws, {
|
|
4087
|
+
type: "skills.installed",
|
|
4088
|
+
payload: {
|
|
4089
|
+
success: false,
|
|
4090
|
+
error: errMessage(err)
|
|
4091
|
+
}
|
|
4092
|
+
});
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
async function handleSkillsUninstall(ws, ctx, msg) {
|
|
4096
|
+
if (!ctx.skillInstaller) {
|
|
4097
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: "Skills not enabled" } });
|
|
4098
|
+
return;
|
|
4099
|
+
}
|
|
4100
|
+
const uninstallPayload = msg.payload;
|
|
4101
|
+
if (!uninstallPayload?.name?.trim()) {
|
|
4102
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: "Skill name is required" } });
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
try {
|
|
4106
|
+
await ctx.skillInstaller.uninstall(uninstallPayload.name.trim(), { global: uninstallPayload.global });
|
|
4107
|
+
send(ws, { type: "skills.uninstalled", payload: { success: true, error: null } });
|
|
4108
|
+
} catch (err) {
|
|
4109
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: errMessage(err) } });
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
async function handleSkillsUpdate(ws, ctx, msg) {
|
|
4113
|
+
if (!ctx.skillInstaller) {
|
|
4114
|
+
send(ws, { type: "skills.updated", payload: { success: false, error: "Skills not enabled" } });
|
|
4115
|
+
return;
|
|
4116
|
+
}
|
|
4117
|
+
const updatePayload = msg.payload;
|
|
4118
|
+
try {
|
|
4119
|
+
const result = await ctx.skillInstaller.update(updatePayload?.name, { global: updatePayload?.global });
|
|
4120
|
+
send(ws, {
|
|
4121
|
+
type: "skills.updated",
|
|
4122
|
+
payload: {
|
|
4123
|
+
success: true,
|
|
4124
|
+
error: null,
|
|
4125
|
+
updated: result.updated,
|
|
4126
|
+
unchanged: result.unchanged,
|
|
4127
|
+
errors: result.errors
|
|
4128
|
+
}
|
|
4129
|
+
});
|
|
4130
|
+
} catch (err) {
|
|
4131
|
+
send(ws, { type: "skills.updated", payload: { success: false, error: errMessage(err) } });
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
async function handleSkillsCreate(ws, ctx, msg) {
|
|
4135
|
+
const createPayload = msg.payload;
|
|
4136
|
+
if (!createPayload?.name?.trim()) {
|
|
4137
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Skill name is required" } });
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(createPayload.name.trim())) {
|
|
4141
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Skill name must be kebab-case (e.g. my-new-skill)" } });
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
if (!createPayload?.description?.trim()) {
|
|
4145
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Description/trigger is required" } });
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
try {
|
|
4149
|
+
const targetDir = createPayload.scope === "global" ? path9.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path9.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
4150
|
+
try {
|
|
4151
|
+
await fs9.access(targetDir);
|
|
4152
|
+
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
4153
|
+
return;
|
|
4154
|
+
} catch {
|
|
4155
|
+
}
|
|
4156
|
+
await fs9.mkdir(targetDir, { recursive: true });
|
|
4157
|
+
const lines = createPayload.description.trim().split("\n");
|
|
4158
|
+
const firstLine = lines[0].trim();
|
|
4159
|
+
const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
|
|
4160
|
+
const descriptionText = firstLine + (bodyLines.length > 0 ? `
|
|
4161
|
+
${bodyLines.join("\n")}` : "");
|
|
4162
|
+
const trigger = bodyLines.find((l) => l.toLowerCase().startsWith("triggers:")) ?? "";
|
|
4163
|
+
const skillContent = [
|
|
4164
|
+
"---",
|
|
4165
|
+
`name: ${createPayload.name.trim()}`,
|
|
4166
|
+
"description: |",
|
|
4167
|
+
` ${descriptionText.replace(/\n/g, "\n ")}`,
|
|
4168
|
+
`version: 1.0.0`,
|
|
4169
|
+
"---",
|
|
4170
|
+
"",
|
|
4171
|
+
`# ${createPayload.name.trim().split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}`,
|
|
4172
|
+
"",
|
|
4173
|
+
"## Overview",
|
|
4174
|
+
"",
|
|
4175
|
+
firstLine,
|
|
4176
|
+
"",
|
|
4177
|
+
...bodyLines.length > 0 ? bodyLines.filter((l) => !l.toLowerCase().startsWith("triggers:")) : [],
|
|
4178
|
+
"",
|
|
4179
|
+
"## Rules",
|
|
4180
|
+
"- TODO: add your first rule",
|
|
4181
|
+
"",
|
|
4182
|
+
"## Patterns",
|
|
4183
|
+
"### Do",
|
|
4184
|
+
"```ts",
|
|
4185
|
+
"// TODO: add a good example",
|
|
4186
|
+
"```",
|
|
4187
|
+
"",
|
|
4188
|
+
"### Don't",
|
|
4189
|
+
"```ts",
|
|
4190
|
+
"// TODO: add a bad example",
|
|
4191
|
+
"```",
|
|
4192
|
+
"",
|
|
4193
|
+
"## Workflow",
|
|
4194
|
+
"1. TODO: describe step one",
|
|
4195
|
+
"2. TODO: describe step two",
|
|
4196
|
+
"",
|
|
4197
|
+
trigger ? `
|
|
4198
|
+
${trigger}
|
|
4199
|
+
` : "",
|
|
4200
|
+
"## Skills in scope",
|
|
4201
|
+
"- `bug-hunter` \u2014 for systematic bug detection patterns",
|
|
4202
|
+
"- `output-standards` \u2014 for standardized `<next_steps>` formatting"
|
|
4203
|
+
].join("\n");
|
|
4204
|
+
await fs9.writeFile(path9.join(targetDir, "SKILL.md"), skillContent, "utf-8");
|
|
4205
|
+
send(ws, {
|
|
4206
|
+
type: "skills.created",
|
|
4207
|
+
payload: {
|
|
4208
|
+
success: true,
|
|
4209
|
+
error: null,
|
|
4210
|
+
skill: { name: createPayload.name.trim(), path: path9.join(targetDir, "SKILL.md"), scope: createPayload.scope }
|
|
4211
|
+
}
|
|
4212
|
+
});
|
|
4213
|
+
} catch (err) {
|
|
4214
|
+
send(ws, { type: "skills.created", payload: { success: false, error: errMessage(err) } });
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
async function handleSkillsEdit(ws, ctx, msg) {
|
|
4218
|
+
if (!ctx.skillLoader) {
|
|
4219
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skills not enabled" } });
|
|
4220
|
+
return;
|
|
4221
|
+
}
|
|
4222
|
+
const editPayload = msg.payload;
|
|
4223
|
+
if (!editPayload?.name?.trim()) {
|
|
4224
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skill name is required" } });
|
|
4225
|
+
return;
|
|
4226
|
+
}
|
|
4227
|
+
if (!editPayload?.body) {
|
|
4228
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skill body is required" } });
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
try {
|
|
4232
|
+
const entries = await ctx.skillLoader.listEntries();
|
|
4233
|
+
const entry = entries.find((e) => e.name.toLowerCase() === editPayload.name.toLowerCase());
|
|
4234
|
+
if (!entry) {
|
|
4235
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: `Skill "${editPayload.name}" not found` } });
|
|
4236
|
+
return;
|
|
4237
|
+
}
|
|
4238
|
+
if (entry.scope.includes("bundled")) {
|
|
4239
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Bundled skills cannot be edited" } });
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
await fs9.writeFile(entry.path, editPayload.body, "utf-8");
|
|
4243
|
+
send(ws, { type: "skills.edited", payload: { success: true, error: null } });
|
|
4244
|
+
} catch (err) {
|
|
4245
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: errMessage(err) } });
|
|
4246
|
+
}
|
|
2802
4247
|
}
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
|
|
2809
|
-
async function handleShellOpen(req, logger) {
|
|
4248
|
+
async function handleSkillsExport(ws, ctx) {
|
|
4249
|
+
if (!ctx.skillLoader) {
|
|
4250
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: "Skills not enabled" } });
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
2810
4253
|
try {
|
|
2811
|
-
const
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
detached: true,
|
|
2820
|
-
stdio: "ignore",
|
|
2821
|
-
windowsHide: true
|
|
2822
|
-
});
|
|
2823
|
-
child.on("error", (err) => {
|
|
2824
|
-
logger.warn(`shell.open spawn failed: ${err.message}`);
|
|
2825
|
-
onError?.();
|
|
2826
|
-
});
|
|
2827
|
-
child.unref();
|
|
2828
|
-
};
|
|
2829
|
-
if (req.target === "file-manager") {
|
|
2830
|
-
if (platform === "win32") launch("explorer", [resolved]);
|
|
2831
|
-
else if (platform === "darwin") launch("open", [resolved]);
|
|
2832
|
-
else launch("xdg-open", [resolved]);
|
|
2833
|
-
} else if (req.target === "terminal") {
|
|
2834
|
-
if (platform === "win32") {
|
|
2835
|
-
launch("cmd", ["/c", "start", "cmd", "/k", "cd", "/d", resolved]);
|
|
2836
|
-
} else if (platform === "darwin") {
|
|
2837
|
-
launch("open", ["-a", "Terminal", resolved]);
|
|
2838
|
-
} else {
|
|
2839
|
-
launch(
|
|
2840
|
-
"x-terminal-emulator",
|
|
2841
|
-
[`--working-directory=${resolved}`],
|
|
2842
|
-
() => launch(
|
|
2843
|
-
"gnome-terminal",
|
|
2844
|
-
[`--working-directory=${resolved}`],
|
|
2845
|
-
() => launch("xterm", ["-e", `cd '${resolved}' && ${process.env["SHELL"] ?? "sh"}`])
|
|
2846
|
-
)
|
|
2847
|
-
);
|
|
4254
|
+
const entries = await ctx.skillLoader.listEntries();
|
|
4255
|
+
const zip = new JSZip();
|
|
4256
|
+
for (const entry of entries) {
|
|
4257
|
+
try {
|
|
4258
|
+
const body = await ctx.skillLoader.readBody(entry.name);
|
|
4259
|
+
const safeName = entry.name.replace(/\//g, "_");
|
|
4260
|
+
zip.file(`${safeName}/SKILL.md`, body);
|
|
4261
|
+
} catch {
|
|
2848
4262
|
}
|
|
2849
|
-
} else {
|
|
2850
|
-
return { success: false, message: `Unknown shell.open target: ${String(req.target)}` };
|
|
2851
4263
|
}
|
|
2852
|
-
|
|
4264
|
+
const zipBuffer = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
|
4265
|
+
const zipBase64 = zipBuffer.toString("base64");
|
|
4266
|
+
send(ws, { type: "skills.exported", payload: { zipBase64, skillCount: entries.length, error: void 0 } });
|
|
2853
4267
|
} catch (err) {
|
|
2854
|
-
|
|
4268
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: errMessage(err) } });
|
|
2855
4269
|
}
|
|
2856
4270
|
}
|
|
2857
4271
|
|
|
@@ -2920,7 +4334,7 @@ async function startWebUI(opts = {}) {
|
|
|
2920
4334
|
console.warn(JSON.stringify({
|
|
2921
4335
|
level: "warn",
|
|
2922
4336
|
event: "webui.provider_registry_load_failed",
|
|
2923
|
-
message:
|
|
4337
|
+
message: toErrorMessage5(err),
|
|
2924
4338
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2925
4339
|
}));
|
|
2926
4340
|
}
|
|
@@ -2969,15 +4383,22 @@ async function startWebUI(opts = {}) {
|
|
|
2969
4383
|
sessionId: session.id,
|
|
2970
4384
|
projectSlug: wpaths.projectSlug,
|
|
2971
4385
|
projectRoot,
|
|
2972
|
-
projectName:
|
|
4386
|
+
projectName: path10.basename(projectRoot),
|
|
2973
4387
|
workingDir,
|
|
4388
|
+
clientType: "webui",
|
|
2974
4389
|
pid: process.pid,
|
|
2975
4390
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2976
4391
|
});
|
|
2977
|
-
|
|
4392
|
+
const fleetNotifier = new FleetNotifier({
|
|
4393
|
+
baseDir: wpaths.globalRoot,
|
|
4394
|
+
projectRoot,
|
|
4395
|
+
selfPid: process.pid
|
|
4396
|
+
});
|
|
4397
|
+
statusTracker = new AgentStatusTracker({ events, registry, onUpdate: () => fleetNotifier.notify() });
|
|
2978
4398
|
statusTracker.start();
|
|
2979
4399
|
const stopTracking = async () => {
|
|
2980
4400
|
try {
|
|
4401
|
+
fleetNotifier.dispose();
|
|
2981
4402
|
await registry.markClosing();
|
|
2982
4403
|
statusTracker?.stop();
|
|
2983
4404
|
} catch {
|
|
@@ -3017,6 +4438,13 @@ async function startWebUI(opts = {}) {
|
|
|
3017
4438
|
supportsReasoning: resolvedModel.capabilities.reasoning
|
|
3018
4439
|
} : void 0;
|
|
3019
4440
|
const skillLoader = config.features.skills ? new DefaultSkillLoader2({ paths: wpaths }) : void 0;
|
|
4441
|
+
const skillInstaller = config.features.skills ? new SkillInstaller({
|
|
4442
|
+
manifestPath: path10.join(wstackGlobalRoot2(), "installed-skills.json"),
|
|
4443
|
+
projectSkillsDir: path10.join(projectRoot, ".wrongstack", "skills"),
|
|
4444
|
+
globalSkillsDir: path10.join(wstackGlobalRoot2(), "skills"),
|
|
4445
|
+
projectHash: projectHash(projectRoot),
|
|
4446
|
+
skillLoader
|
|
4447
|
+
}) : void 0;
|
|
3020
4448
|
const systemPromptBuilder = new DefaultSystemPromptBuilder2({
|
|
3021
4449
|
memoryStore,
|
|
3022
4450
|
skillLoader,
|
|
@@ -3057,7 +4485,7 @@ async function startWebUI(opts = {}) {
|
|
|
3057
4485
|
console.error(JSON.stringify({
|
|
3058
4486
|
level: "error",
|
|
3059
4487
|
event: "webui.provider_create_failed",
|
|
3060
|
-
message:
|
|
4488
|
+
message: toErrorMessage5(err),
|
|
3061
4489
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3062
4490
|
}));
|
|
3063
4491
|
throw err;
|
|
@@ -3079,14 +4507,14 @@ async function startWebUI(opts = {}) {
|
|
|
3079
4507
|
console.error(JSON.stringify({
|
|
3080
4508
|
level: "error",
|
|
3081
4509
|
event: "webui.provider_stub_create_failed",
|
|
3082
|
-
message:
|
|
4510
|
+
message: toErrorMessage5(err),
|
|
3083
4511
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3084
4512
|
}));
|
|
3085
4513
|
throw err;
|
|
3086
4514
|
}
|
|
3087
4515
|
} else {
|
|
3088
4516
|
throw new Error(
|
|
3089
|
-
"No provider configured. Run `wrongstack
|
|
4517
|
+
"No provider configured. Run `wrongstack auth` to set up, or configure via the WebUI."
|
|
3090
4518
|
);
|
|
3091
4519
|
}
|
|
3092
4520
|
}
|
|
@@ -3128,6 +4556,12 @@ async function startWebUI(opts = {}) {
|
|
|
3128
4556
|
context.meta["logLevel"] = config.log?.level ?? "info";
|
|
3129
4557
|
context.meta["auditLevel"] = config.session?.auditLevel ?? "standard";
|
|
3130
4558
|
context.meta["maxIterations"] = config.tools?.maxIterations ?? 500;
|
|
4559
|
+
const tgExt = config.extensions?.["telegram"];
|
|
4560
|
+
context.meta["tgConfigured"] = typeof tgExt?.["botToken"] === "string" && tgExt["botToken"].length > 0;
|
|
4561
|
+
context.meta["tgSessionEnd"] = tgExt?.["notifyOnSessionEnd"] === true;
|
|
4562
|
+
context.meta["tgDelegate"] = tgExt?.["notifyOnDelegate"] !== false;
|
|
4563
|
+
const tgMs = tgExt?.["longToolThresholdMs"];
|
|
4564
|
+
context.meta["tgLongToolMs"] = typeof tgMs === "number" ? tgMs : 3e4;
|
|
3131
4565
|
}
|
|
3132
4566
|
const PREF_KEYS = [
|
|
3133
4567
|
"autonomy",
|
|
@@ -3151,7 +4585,11 @@ async function startWebUI(opts = {}) {
|
|
|
3151
4585
|
"contextAutoCompact",
|
|
3152
4586
|
"contextStrategy",
|
|
3153
4587
|
"logLevel",
|
|
3154
|
-
"auditLevel"
|
|
4588
|
+
"auditLevel",
|
|
4589
|
+
"tgConfigured",
|
|
4590
|
+
"tgSessionEnd",
|
|
4591
|
+
"tgDelegate",
|
|
4592
|
+
"tgLongToolMs"
|
|
3155
4593
|
];
|
|
3156
4594
|
const prefSnapshot = () => {
|
|
3157
4595
|
const snapshot = {};
|
|
@@ -3164,7 +4602,7 @@ async function startWebUI(opts = {}) {
|
|
|
3164
4602
|
const write = async () => {
|
|
3165
4603
|
let raw;
|
|
3166
4604
|
try {
|
|
3167
|
-
raw = await
|
|
4605
|
+
raw = await fs10.readFile(globalConfigPath, "utf8");
|
|
3168
4606
|
} catch {
|
|
3169
4607
|
raw = "{}";
|
|
3170
4608
|
}
|
|
@@ -3236,6 +4674,22 @@ async function startWebUI(opts = {}) {
|
|
|
3236
4674
|
toolsCfg.maxIterations = payload["maxIterations"];
|
|
3237
4675
|
decrypted.tools = toolsCfg;
|
|
3238
4676
|
}
|
|
4677
|
+
const tgTouched = typeof payload["tgSessionEnd"] === "boolean" || typeof payload["tgDelegate"] === "boolean" || typeof payload["tgLongToolMs"] === "number";
|
|
4678
|
+
if (tgTouched) {
|
|
4679
|
+
const ext = decrypted.extensions ?? {};
|
|
4680
|
+
const tg = ext["telegram"] ?? {};
|
|
4681
|
+
if (typeof payload["tgSessionEnd"] === "boolean") {
|
|
4682
|
+
tg["notifyOnSessionEnd"] = payload["tgSessionEnd"];
|
|
4683
|
+
}
|
|
4684
|
+
if (typeof payload["tgDelegate"] === "boolean") {
|
|
4685
|
+
tg["notifyOnDelegate"] = payload["tgDelegate"];
|
|
4686
|
+
}
|
|
4687
|
+
if (typeof payload["tgLongToolMs"] === "number") {
|
|
4688
|
+
tg["longToolThresholdMs"] = payload["tgLongToolMs"];
|
|
4689
|
+
}
|
|
4690
|
+
ext["telegram"] = tg;
|
|
4691
|
+
decrypted.extensions = ext;
|
|
4692
|
+
}
|
|
3239
4693
|
const encrypted = encryptConfigSecrets2(decrypted, vault);
|
|
3240
4694
|
await atomicWrite5(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
3241
4695
|
};
|
|
@@ -3457,7 +4911,7 @@ async function startWebUI(opts = {}) {
|
|
|
3457
4911
|
inputCost,
|
|
3458
4912
|
outputCost,
|
|
3459
4913
|
cacheReadCost,
|
|
3460
|
-
projectName:
|
|
4914
|
+
projectName: path10.basename(projectRoot) || projectRoot,
|
|
3461
4915
|
projectRoot,
|
|
3462
4916
|
cwd: workingDir,
|
|
3463
4917
|
mode: modeId,
|
|
@@ -3511,10 +4965,11 @@ async function startWebUI(opts = {}) {
|
|
|
3511
4965
|
const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
|
|
3512
4966
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
3513
4967
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
3514
|
-
|
|
4968
|
+
let connSeq = 0;
|
|
4969
|
+
function checkRateLimit(_ws, client) {
|
|
3515
4970
|
if (RATE_LIMIT_MESSAGES <= 0) return true;
|
|
3516
4971
|
const now = Date.now();
|
|
3517
|
-
const key = client.
|
|
4972
|
+
const key = client.connId;
|
|
3518
4973
|
const limit = rateLimits.get(key);
|
|
3519
4974
|
if (!limit || now > limit.resetAt) {
|
|
3520
4975
|
rateLimits.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
|
|
@@ -3530,7 +4985,12 @@ async function startWebUI(opts = {}) {
|
|
|
3530
4985
|
);
|
|
3531
4986
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
3532
4987
|
const handleConnection = (ws) => {
|
|
3533
|
-
const client = {
|
|
4988
|
+
const client = {
|
|
4989
|
+
ws,
|
|
4990
|
+
sessionId: session.id,
|
|
4991
|
+
connectedAt: Date.now(),
|
|
4992
|
+
connId: `c${++connSeq}`
|
|
4993
|
+
};
|
|
3534
4994
|
clients.set(ws, client);
|
|
3535
4995
|
void sessionStartPayload().then((payload) => {
|
|
3536
4996
|
send(ws, { type: "session.start", payload });
|
|
@@ -3538,7 +4998,7 @@ async function startWebUI(opts = {}) {
|
|
|
3538
4998
|
console.warn(JSON.stringify({
|
|
3539
4999
|
level: "warn",
|
|
3540
5000
|
event: "webui.session_start_payload_failed",
|
|
3541
|
-
message:
|
|
5001
|
+
message: toErrorMessage5(err),
|
|
3542
5002
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3543
5003
|
}));
|
|
3544
5004
|
});
|
|
@@ -3560,7 +5020,7 @@ async function startWebUI(opts = {}) {
|
|
|
3560
5020
|
const rawObj = JSON.parse(data.toString());
|
|
3561
5021
|
if (typeof rawObj === "object" && rawObj !== null) {
|
|
3562
5022
|
const obj = rawObj;
|
|
3563
|
-
if ("__proto__"
|
|
5023
|
+
if (Object.hasOwn(obj, "__proto__") || Object.hasOwn(obj, "constructor") || Object.hasOwn(obj, "prototype")) {
|
|
3564
5024
|
send(ws, {
|
|
3565
5025
|
type: "error",
|
|
3566
5026
|
payload: { phase: "parse", message: "Invalid message object" }
|
|
@@ -3575,17 +5035,18 @@ async function startWebUI(opts = {}) {
|
|
|
3575
5035
|
console.error(JSON.stringify({
|
|
3576
5036
|
level: "error",
|
|
3577
5037
|
event: "webui.ws_message_parse_failed",
|
|
3578
|
-
message:
|
|
5038
|
+
message: toErrorMessage5(err),
|
|
3579
5039
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3580
5040
|
}));
|
|
3581
5041
|
}
|
|
3582
5042
|
});
|
|
3583
5043
|
ws.on("close", () => {
|
|
5044
|
+
const closing = clients.get(ws);
|
|
3584
5045
|
clients.delete(ws);
|
|
3585
|
-
rateLimits.delete(
|
|
5046
|
+
if (closing) rateLimits.delete(closing.connId);
|
|
3586
5047
|
if (pendingConfirms.size > 0) {
|
|
3587
|
-
for (const [id,
|
|
3588
|
-
|
|
5048
|
+
for (const [id, resolve5] of pendingConfirms) {
|
|
5049
|
+
resolve5("no");
|
|
3589
5050
|
pendingConfirms.delete(id);
|
|
3590
5051
|
}
|
|
3591
5052
|
}
|
|
@@ -3608,11 +5069,27 @@ async function startWebUI(opts = {}) {
|
|
|
3608
5069
|
{ sampling: sessionLogging.sampling }
|
|
3609
5070
|
);
|
|
3610
5071
|
let eventsArmed = false;
|
|
5072
|
+
let disposeEvents = null;
|
|
5073
|
+
let fleetBroadcast = null;
|
|
3611
5074
|
const armOnce = (label) => {
|
|
3612
5075
|
if (eventsArmed) return;
|
|
3613
5076
|
eventsArmed = true;
|
|
3614
5077
|
console.log(`[WebUI] Backend ready (${label})`);
|
|
3615
|
-
setupEvents({
|
|
5078
|
+
disposeEvents = setupEvents({
|
|
5079
|
+
events,
|
|
5080
|
+
broadcast,
|
|
5081
|
+
clients,
|
|
5082
|
+
config,
|
|
5083
|
+
context,
|
|
5084
|
+
pendingConfirms,
|
|
5085
|
+
globalConfigPath,
|
|
5086
|
+
sessionBridge,
|
|
5087
|
+
wpaths,
|
|
5088
|
+
watcherMetrics,
|
|
5089
|
+
onFleetBroadcaster: (fn) => {
|
|
5090
|
+
fleetBroadcast = fn;
|
|
5091
|
+
}
|
|
5092
|
+
});
|
|
3616
5093
|
};
|
|
3617
5094
|
wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
|
|
3618
5095
|
wssPrimary.on("connection", handleConnection);
|
|
@@ -3621,7 +5098,7 @@ async function startWebUI(opts = {}) {
|
|
|
3621
5098
|
level: "error",
|
|
3622
5099
|
event: "webui.ws_server_error",
|
|
3623
5100
|
host: wsHost,
|
|
3624
|
-
message:
|
|
5101
|
+
message: toErrorMessage5(err),
|
|
3625
5102
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3626
5103
|
}));
|
|
3627
5104
|
});
|
|
@@ -3649,33 +5126,33 @@ async function startWebUI(opts = {}) {
|
|
|
3649
5126
|
});
|
|
3650
5127
|
}
|
|
3651
5128
|
async function touchProjectEntry(root, workDir) {
|
|
3652
|
-
const resolved =
|
|
5129
|
+
const resolved = path10.resolve(root);
|
|
3653
5130
|
const manifest = await loadManifest(globalConfigPath);
|
|
3654
5131
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3655
|
-
const existing = manifest.projects.find((p) =>
|
|
5132
|
+
const existing = manifest.projects.find((p) => path10.resolve(p.root) === resolved);
|
|
3656
5133
|
if (existing) {
|
|
3657
5134
|
existing.lastSeen = now;
|
|
3658
|
-
if (workDir) existing.lastWorkingDir =
|
|
5135
|
+
if (workDir) existing.lastWorkingDir = path10.resolve(workDir);
|
|
3659
5136
|
} else {
|
|
3660
5137
|
manifest.projects.push({
|
|
3661
|
-
name:
|
|
5138
|
+
name: path10.basename(resolved),
|
|
3662
5139
|
root: resolved,
|
|
3663
5140
|
slug: generateProjectSlug(resolved),
|
|
3664
5141
|
createdAt: now,
|
|
3665
5142
|
lastSeen: now,
|
|
3666
|
-
lastWorkingDir: workDir ?
|
|
5143
|
+
lastWorkingDir: workDir ? path10.resolve(workDir) : void 0
|
|
3667
5144
|
});
|
|
3668
5145
|
}
|
|
3669
5146
|
await saveManifest(manifest, globalConfigPath);
|
|
3670
5147
|
await ensureProjectDataDir(generateProjectSlug(resolved), globalConfigPath);
|
|
3671
5148
|
}
|
|
3672
5149
|
function projectsJsonPath(globalConfigPath2) {
|
|
3673
|
-
const base =
|
|
3674
|
-
return
|
|
5150
|
+
const base = path10.dirname(globalConfigPath2);
|
|
5151
|
+
return path10.join(base, "projects.json");
|
|
3675
5152
|
}
|
|
3676
5153
|
async function loadManifest(globalConfigPath2) {
|
|
3677
5154
|
try {
|
|
3678
|
-
const raw = await
|
|
5155
|
+
const raw = await fs10.readFile(projectsJsonPath(globalConfigPath2), "utf8");
|
|
3679
5156
|
const parsed = JSON.parse(raw);
|
|
3680
5157
|
return { projects: parsed.projects ?? [] };
|
|
3681
5158
|
} catch {
|
|
@@ -3684,16 +5161,16 @@ async function startWebUI(opts = {}) {
|
|
|
3684
5161
|
}
|
|
3685
5162
|
async function saveManifest(manifest, globalConfigPath2) {
|
|
3686
5163
|
const file = projectsJsonPath(globalConfigPath2);
|
|
3687
|
-
await
|
|
3688
|
-
await
|
|
5164
|
+
await fs10.mkdir(path10.dirname(file), { recursive: true });
|
|
5165
|
+
await fs10.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
3689
5166
|
}
|
|
3690
5167
|
function generateProjectSlug(rootPath) {
|
|
3691
5168
|
return projectSlug(rootPath);
|
|
3692
5169
|
}
|
|
3693
5170
|
async function ensureProjectDataDir(slug, globalConfigPath2) {
|
|
3694
|
-
const base =
|
|
3695
|
-
const dir =
|
|
3696
|
-
await
|
|
5171
|
+
const base = path10.dirname(globalConfigPath2);
|
|
5172
|
+
const dir = path10.join(base, "projects", slug);
|
|
5173
|
+
await fs10.mkdir(dir, { recursive: true });
|
|
3697
5174
|
return dir;
|
|
3698
5175
|
}
|
|
3699
5176
|
async function handleMessage(ws, _client, msg) {
|
|
@@ -3704,7 +5181,9 @@ async function startWebUI(opts = {}) {
|
|
|
3704
5181
|
case "collab.join":
|
|
3705
5182
|
case "collab.leave":
|
|
3706
5183
|
case "collab.annotate":
|
|
3707
|
-
case "collab.resolve":
|
|
5184
|
+
case "collab.resolve":
|
|
5185
|
+
case "collab.request_pause":
|
|
5186
|
+
case "collab.resume": {
|
|
3708
5187
|
collabHandler.handleMessage(ws, msg);
|
|
3709
5188
|
return;
|
|
3710
5189
|
}
|
|
@@ -3755,10 +5234,10 @@ async function startWebUI(opts = {}) {
|
|
|
3755
5234
|
}
|
|
3756
5235
|
case "tool.confirm_result": {
|
|
3757
5236
|
const { id, decision } = msg.payload;
|
|
3758
|
-
const
|
|
3759
|
-
if (
|
|
5237
|
+
const resolve5 = pendingConfirms.get(id);
|
|
5238
|
+
if (resolve5) {
|
|
3760
5239
|
pendingConfirms.delete(id);
|
|
3761
|
-
|
|
5240
|
+
resolve5(decision);
|
|
3762
5241
|
}
|
|
3763
5242
|
break;
|
|
3764
5243
|
}
|
|
@@ -3801,7 +5280,7 @@ async function startWebUI(opts = {}) {
|
|
|
3801
5280
|
context.readFiles.clear();
|
|
3802
5281
|
context.fileMtimes.clear();
|
|
3803
5282
|
tokenCounter.reset();
|
|
3804
|
-
|
|
5283
|
+
sendResult2(ws, true, "Context cleared");
|
|
3805
5284
|
broadcast(clients, {
|
|
3806
5285
|
type: "session.start",
|
|
3807
5286
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
@@ -3838,13 +5317,13 @@ async function startWebUI(opts = {}) {
|
|
|
3838
5317
|
repaired: report.repaired
|
|
3839
5318
|
}
|
|
3840
5319
|
});
|
|
3841
|
-
|
|
5320
|
+
sendResult2(
|
|
3842
5321
|
ws,
|
|
3843
5322
|
true,
|
|
3844
5323
|
`Compacted: ${report.before} \u2192 ${report.after} tokens (saved ~${Math.max(0, report.before - report.after)})`
|
|
3845
5324
|
);
|
|
3846
5325
|
} catch (err) {
|
|
3847
|
-
|
|
5326
|
+
sendResult2(ws, false, errMessage(err));
|
|
3848
5327
|
}
|
|
3849
5328
|
break;
|
|
3850
5329
|
}
|
|
@@ -3863,7 +5342,7 @@ async function startWebUI(opts = {}) {
|
|
|
3863
5342
|
};
|
|
3864
5343
|
broadcast(clients, { type: "context.repaired", payload });
|
|
3865
5344
|
const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
|
|
3866
|
-
|
|
5345
|
+
sendResult2(
|
|
3867
5346
|
ws,
|
|
3868
5347
|
true,
|
|
3869
5348
|
removed > 0 ? `Context repaired: removed ${removed} orphan protocol item(s)` : "Context repair found no orphan protocol blocks"
|
|
@@ -3897,14 +5376,14 @@ async function startWebUI(opts = {}) {
|
|
|
3897
5376
|
);
|
|
3898
5377
|
const custom = customModes.find((m) => m.id === id);
|
|
3899
5378
|
if (!custom) {
|
|
3900
|
-
|
|
5379
|
+
sendResult2(ws, false, `Unknown context mode "${id}"`);
|
|
3901
5380
|
break;
|
|
3902
5381
|
}
|
|
3903
5382
|
policy = custom;
|
|
3904
5383
|
}
|
|
3905
5384
|
context.meta["contextWindowMode"] = policy.id;
|
|
3906
5385
|
context.meta["contextWindowPolicy"] = policy;
|
|
3907
|
-
|
|
5386
|
+
sendResult2(ws, true, `Context mode switched to ${policy.id}`);
|
|
3908
5387
|
broadcast(clients, {
|
|
3909
5388
|
type: "context.mode.changed",
|
|
3910
5389
|
payload: { id: policy.id, name: policy.name, policy }
|
|
@@ -3924,7 +5403,7 @@ async function startWebUI(opts = {}) {
|
|
|
3924
5403
|
aggressiveOn: "soft",
|
|
3925
5404
|
targetLoad: 0.65
|
|
3926
5405
|
});
|
|
3927
|
-
|
|
5406
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${payload.id}" created`);
|
|
3928
5407
|
break;
|
|
3929
5408
|
}
|
|
3930
5409
|
case "context.mode.update": {
|
|
@@ -3940,7 +5419,7 @@ async function startWebUI(opts = {}) {
|
|
|
3940
5419
|
preserveK: payload.preserveK,
|
|
3941
5420
|
eliseThreshold: payload.eliseThreshold
|
|
3942
5421
|
});
|
|
3943
|
-
|
|
5422
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${payload.id}" updated`);
|
|
3944
5423
|
break;
|
|
3945
5424
|
}
|
|
3946
5425
|
case "context.mode.delete": {
|
|
@@ -3950,7 +5429,7 @@ async function startWebUI(opts = {}) {
|
|
|
3950
5429
|
context.meta["contextWindowPolicy"] = resolveContextWindowPolicy({}, DEFAULT_CONTEXT_WINDOW_MODE_ID);
|
|
3951
5430
|
}
|
|
3952
5431
|
const result = customModeStore.remove(id);
|
|
3953
|
-
|
|
5432
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${id}" deleted`);
|
|
3954
5433
|
break;
|
|
3955
5434
|
}
|
|
3956
5435
|
case "providers.list": {
|
|
@@ -4031,19 +5510,20 @@ async function startWebUI(opts = {}) {
|
|
|
4031
5510
|
context.provider = newProv;
|
|
4032
5511
|
updateAutoCompactionMaxContext?.(newProv);
|
|
4033
5512
|
try {
|
|
4034
|
-
|
|
4035
|
-
const raw = await
|
|
5513
|
+
const next = configWriteLock.then(async () => {
|
|
5514
|
+
const raw = await fs10.readFile(globalConfigPath, "utf8");
|
|
4036
5515
|
const parsed = JSON.parse(raw);
|
|
4037
5516
|
parsed.provider = newProvider;
|
|
4038
5517
|
parsed.model = newModel;
|
|
4039
5518
|
await atomicWrite5(globalConfigPath, JSON.stringify(parsed, null, 2));
|
|
4040
5519
|
});
|
|
4041
|
-
|
|
5520
|
+
configWriteLock = next.then(() => void 0, () => void 0);
|
|
5521
|
+
await next;
|
|
4042
5522
|
} catch (err) {
|
|
4043
5523
|
console.warn(JSON.stringify({
|
|
4044
5524
|
level: "warn",
|
|
4045
5525
|
event: "webui.config_save_failed",
|
|
4046
|
-
message:
|
|
5526
|
+
message: toErrorMessage5(err),
|
|
4047
5527
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4048
5528
|
}));
|
|
4049
5529
|
}
|
|
@@ -4141,6 +5621,26 @@ async function startWebUI(opts = {}) {
|
|
|
4141
5621
|
await providerHandlers.handleProviderRemove(ws, providerId);
|
|
4142
5622
|
break;
|
|
4143
5623
|
}
|
|
5624
|
+
case "provider.clear_models": {
|
|
5625
|
+
const { providerId } = msg.payload;
|
|
5626
|
+
await providerHandlers.handleProviderClearModels(ws, providerId);
|
|
5627
|
+
break;
|
|
5628
|
+
}
|
|
5629
|
+
case "provider.undo_clear": {
|
|
5630
|
+
const { providerId, previousModels } = msg.payload;
|
|
5631
|
+
await providerHandlers.handleProviderUndoClear(ws, providerId, previousModels);
|
|
5632
|
+
break;
|
|
5633
|
+
}
|
|
5634
|
+
case "provider.update": {
|
|
5635
|
+
const p = msg.payload;
|
|
5636
|
+
await providerHandlers.handleProviderUpdate(ws, p);
|
|
5637
|
+
break;
|
|
5638
|
+
}
|
|
5639
|
+
case "provider.probe": {
|
|
5640
|
+
const { providerId, timeoutMs } = msg.payload;
|
|
5641
|
+
await providerHandlers.handleProviderProbe(ws, providerId, timeoutMs);
|
|
5642
|
+
break;
|
|
5643
|
+
}
|
|
4144
5644
|
case "sessions.list": {
|
|
4145
5645
|
const limit = msg.payload?.limit ?? 50;
|
|
4146
5646
|
try {
|
|
@@ -4171,13 +5671,13 @@ async function startWebUI(opts = {}) {
|
|
|
4171
5671
|
const { id } = msg.payload;
|
|
4172
5672
|
try {
|
|
4173
5673
|
if (id === session.id) {
|
|
4174
|
-
|
|
5674
|
+
sendResult2(ws, false, "Cannot delete the active session");
|
|
4175
5675
|
break;
|
|
4176
5676
|
}
|
|
4177
5677
|
await sessionStore.delete(id);
|
|
4178
|
-
|
|
5678
|
+
sendResult2(ws, true, `Session ${id} deleted`);
|
|
4179
5679
|
} catch (err) {
|
|
4180
|
-
|
|
5680
|
+
sendResult2(ws, false, errMessage(err));
|
|
4181
5681
|
}
|
|
4182
5682
|
break;
|
|
4183
5683
|
}
|
|
@@ -4185,7 +5685,7 @@ async function startWebUI(opts = {}) {
|
|
|
4185
5685
|
const { id } = msg.payload;
|
|
4186
5686
|
try {
|
|
4187
5687
|
if (id === session.id) {
|
|
4188
|
-
|
|
5688
|
+
sendResult2(ws, false, "Session is already active");
|
|
4189
5689
|
break;
|
|
4190
5690
|
}
|
|
4191
5691
|
const resumed = await sessionStore.resume(id);
|
|
@@ -4215,14 +5715,14 @@ async function startWebUI(opts = {}) {
|
|
|
4215
5715
|
replayUsage: resumed.data.usage
|
|
4216
5716
|
}
|
|
4217
5717
|
});
|
|
4218
|
-
|
|
5718
|
+
sendResult2(ws, true, `Resumed session ${id}`);
|
|
4219
5719
|
} catch (err) {
|
|
4220
|
-
|
|
5720
|
+
sendResult2(ws, false, errMessage(err));
|
|
4221
5721
|
}
|
|
4222
5722
|
break;
|
|
4223
5723
|
}
|
|
4224
5724
|
case "session.save": {
|
|
4225
|
-
|
|
5725
|
+
sendResult2(ws, true, `Session ${session.id} is auto-saved`);
|
|
4226
5726
|
break;
|
|
4227
5727
|
}
|
|
4228
5728
|
case "tools.list": {
|
|
@@ -4245,6 +5745,27 @@ async function startWebUI(opts = {}) {
|
|
|
4245
5745
|
return handleMemoryRemember(ws, msg, memoryStore);
|
|
4246
5746
|
case "memory.forget":
|
|
4247
5747
|
return handleMemoryForget(ws, msg, memoryStore);
|
|
5748
|
+
// ── MCP operations — delegated to shared handlers (mcp-handlers.ts) ──
|
|
5749
|
+
case "mcp.list":
|
|
5750
|
+
return handleMcpList(ws, msg, config, globalConfigPath, void 0);
|
|
5751
|
+
case "mcp.add":
|
|
5752
|
+
return handleMcpAdd(ws, msg, config, globalConfigPath, void 0);
|
|
5753
|
+
case "mcp.remove":
|
|
5754
|
+
return handleMcpRemove(ws, msg, config, globalConfigPath, void 0);
|
|
5755
|
+
case "mcp.update":
|
|
5756
|
+
return handleMcpUpdate(ws, msg, config, globalConfigPath);
|
|
5757
|
+
case "mcp.wake":
|
|
5758
|
+
return handleMcpWake(ws, msg, config, globalConfigPath, void 0);
|
|
5759
|
+
case "mcp.sleep":
|
|
5760
|
+
return handleMcpSleep(ws, msg, config, globalConfigPath, void 0);
|
|
5761
|
+
case "mcp.discover":
|
|
5762
|
+
return handleMcpDiscover(ws, msg, config, globalConfigPath);
|
|
5763
|
+
case "mcp.enable":
|
|
5764
|
+
return handleMcpEnable(ws, msg, config, globalConfigPath);
|
|
5765
|
+
case "mcp.disable":
|
|
5766
|
+
return handleMcpDisable(ws, msg, config, globalConfigPath);
|
|
5767
|
+
case "mcp.restart":
|
|
5768
|
+
return handleMcpRestart(ws, msg, config, globalConfigPath);
|
|
4248
5769
|
case "skills.list": {
|
|
4249
5770
|
if (!skillLoader) {
|
|
4250
5771
|
send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
|
|
@@ -4254,6 +5775,18 @@ async function startWebUI(opts = {}) {
|
|
|
4254
5775
|
const manifests = await skillLoader.list();
|
|
4255
5776
|
const entries = await skillLoader.listEntries();
|
|
4256
5777
|
const byName = new Map(entries.map((e) => [e.name, e]));
|
|
5778
|
+
const sourceUrlsByName = /* @__PURE__ */ new Map();
|
|
5779
|
+
const refsByName = /* @__PURE__ */ new Map();
|
|
5780
|
+
if (skillInstaller) {
|
|
5781
|
+
try {
|
|
5782
|
+
const installed = await skillInstaller.listInstalled();
|
|
5783
|
+
for (const entry of installed) {
|
|
5784
|
+
sourceUrlsByName.set(entry.name, entry.source);
|
|
5785
|
+
refsByName.set(entry.name, entry.ref);
|
|
5786
|
+
}
|
|
5787
|
+
} catch {
|
|
5788
|
+
}
|
|
5789
|
+
}
|
|
4257
5790
|
send(ws, {
|
|
4258
5791
|
type: "skills.list",
|
|
4259
5792
|
payload: {
|
|
@@ -4263,6 +5796,8 @@ async function startWebUI(opts = {}) {
|
|
|
4263
5796
|
description: m.description,
|
|
4264
5797
|
version: m.version ?? "",
|
|
4265
5798
|
source: m.source,
|
|
5799
|
+
sourceUrl: sourceUrlsByName.get(m.name) ?? "",
|
|
5800
|
+
ref: refsByName.get(m.name) ?? "",
|
|
4266
5801
|
path: m.path,
|
|
4267
5802
|
trigger: byName.get(m.name)?.trigger ?? "",
|
|
4268
5803
|
scope: byName.get(m.name)?.scope ?? []
|
|
@@ -4281,6 +5816,261 @@ async function startWebUI(opts = {}) {
|
|
|
4281
5816
|
}
|
|
4282
5817
|
break;
|
|
4283
5818
|
}
|
|
5819
|
+
case "skills.content": {
|
|
5820
|
+
if (!skillLoader) {
|
|
5821
|
+
send(ws, { type: "skills.content", payload: { name: "", body: "", path: "", source: "", relatedFiles: [], references: [], error: "Skills not enabled" } });
|
|
5822
|
+
break;
|
|
5823
|
+
}
|
|
5824
|
+
const contentPayload = msg.payload;
|
|
5825
|
+
if (!contentPayload?.name) {
|
|
5826
|
+
send(ws, { type: "skills.content", payload: { name: "", body: "", path: "", source: "", relatedFiles: [], references: [], error: "Skill name is required" } });
|
|
5827
|
+
break;
|
|
5828
|
+
}
|
|
5829
|
+
try {
|
|
5830
|
+
const { name, source } = contentPayload;
|
|
5831
|
+
const entries = await skillLoader.listEntries();
|
|
5832
|
+
const entry = entries.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
5833
|
+
if (!entry) {
|
|
5834
|
+
send(ws, { type: "skills.content", payload: { name, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name}" not found` } });
|
|
5835
|
+
break;
|
|
5836
|
+
}
|
|
5837
|
+
const body = await skillLoader.readBody(name);
|
|
5838
|
+
const skillDir = path10.dirname(entry.path);
|
|
5839
|
+
let relatedFiles = [];
|
|
5840
|
+
try {
|
|
5841
|
+
const files = await fs10.readdir(skillDir);
|
|
5842
|
+
relatedFiles = files.filter((f) => f !== path10.basename(entry.path)).map((f) => path10.join(skillDir, f));
|
|
5843
|
+
} catch {
|
|
5844
|
+
}
|
|
5845
|
+
const refs = [];
|
|
5846
|
+
for (const e of entries) {
|
|
5847
|
+
if (e.name.toLowerCase() === name.toLowerCase()) continue;
|
|
5848
|
+
try {
|
|
5849
|
+
const content = await skillLoader.readBody(e.name);
|
|
5850
|
+
if (content.toLowerCase().includes(name.toLowerCase())) {
|
|
5851
|
+
refs.push(e.name);
|
|
5852
|
+
}
|
|
5853
|
+
} catch {
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
send(ws, { type: "skills.content", payload: { name, body, path: entry.path, source, relatedFiles, references: refs } });
|
|
5857
|
+
} catch (err) {
|
|
5858
|
+
send(ws, { type: "skills.content", payload: { name: contentPayload.name, body: "", path: "", source: contentPayload.source, relatedFiles: [], references: [], error: errMessage(err) } });
|
|
5859
|
+
}
|
|
5860
|
+
break;
|
|
5861
|
+
}
|
|
5862
|
+
case "skills.install": {
|
|
5863
|
+
if (!skillInstaller) {
|
|
5864
|
+
send(ws, { type: "skills.installed", payload: { success: false, error: "Skills not enabled" } });
|
|
5865
|
+
break;
|
|
5866
|
+
}
|
|
5867
|
+
const installPayload = msg.payload;
|
|
5868
|
+
if (!installPayload?.ref?.trim()) {
|
|
5869
|
+
send(ws, { type: "skills.installed", payload: { success: false, error: "Skill reference is required (e.g. owner/repo or https://github.com/owner/repo)" } });
|
|
5870
|
+
break;
|
|
5871
|
+
}
|
|
5872
|
+
try {
|
|
5873
|
+
const results = await skillInstaller.install(installPayload.ref.trim(), { global: installPayload.global });
|
|
5874
|
+
send(ws, {
|
|
5875
|
+
type: "skills.installed",
|
|
5876
|
+
payload: {
|
|
5877
|
+
success: true,
|
|
5878
|
+
results,
|
|
5879
|
+
error: null
|
|
5880
|
+
}
|
|
5881
|
+
});
|
|
5882
|
+
} catch (err) {
|
|
5883
|
+
send(ws, {
|
|
5884
|
+
type: "skills.installed",
|
|
5885
|
+
payload: {
|
|
5886
|
+
success: false,
|
|
5887
|
+
error: errMessage(err)
|
|
5888
|
+
}
|
|
5889
|
+
});
|
|
5890
|
+
}
|
|
5891
|
+
break;
|
|
5892
|
+
}
|
|
5893
|
+
case "skills.uninstall": {
|
|
5894
|
+
if (!skillInstaller) {
|
|
5895
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: "Skills not enabled" } });
|
|
5896
|
+
break;
|
|
5897
|
+
}
|
|
5898
|
+
const uninstallPayload = msg.payload;
|
|
5899
|
+
if (!uninstallPayload?.name?.trim()) {
|
|
5900
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: "Skill name is required" } });
|
|
5901
|
+
break;
|
|
5902
|
+
}
|
|
5903
|
+
try {
|
|
5904
|
+
await skillInstaller.uninstall(uninstallPayload.name.trim(), { global: uninstallPayload.global });
|
|
5905
|
+
send(ws, { type: "skills.uninstalled", payload: { success: true, error: null } });
|
|
5906
|
+
} catch (err) {
|
|
5907
|
+
send(ws, { type: "skills.uninstalled", payload: { success: false, error: errMessage(err) } });
|
|
5908
|
+
}
|
|
5909
|
+
break;
|
|
5910
|
+
}
|
|
5911
|
+
case "skills.update": {
|
|
5912
|
+
if (!skillInstaller) {
|
|
5913
|
+
send(ws, { type: "skills.updated", payload: { success: false, error: "Skills not enabled" } });
|
|
5914
|
+
break;
|
|
5915
|
+
}
|
|
5916
|
+
const updatePayload = msg.payload;
|
|
5917
|
+
try {
|
|
5918
|
+
const result = await skillInstaller.update(updatePayload?.name, { global: updatePayload?.global });
|
|
5919
|
+
send(ws, {
|
|
5920
|
+
type: "skills.updated",
|
|
5921
|
+
payload: {
|
|
5922
|
+
success: true,
|
|
5923
|
+
error: null,
|
|
5924
|
+
updated: result.updated,
|
|
5925
|
+
unchanged: result.unchanged,
|
|
5926
|
+
errors: result.errors
|
|
5927
|
+
}
|
|
5928
|
+
});
|
|
5929
|
+
} catch (err) {
|
|
5930
|
+
send(ws, { type: "skills.updated", payload: { success: false, error: errMessage(err) } });
|
|
5931
|
+
}
|
|
5932
|
+
break;
|
|
5933
|
+
}
|
|
5934
|
+
case "skills.create": {
|
|
5935
|
+
const createPayload = msg.payload;
|
|
5936
|
+
if (!createPayload?.name?.trim()) {
|
|
5937
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Skill name is required" } });
|
|
5938
|
+
break;
|
|
5939
|
+
}
|
|
5940
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(createPayload.name.trim())) {
|
|
5941
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Skill name must be kebab-case (e.g. my-new-skill)" } });
|
|
5942
|
+
break;
|
|
5943
|
+
}
|
|
5944
|
+
if (!createPayload?.description?.trim()) {
|
|
5945
|
+
send(ws, { type: "skills.created", payload: { success: false, error: "Description/trigger is required" } });
|
|
5946
|
+
break;
|
|
5947
|
+
}
|
|
5948
|
+
try {
|
|
5949
|
+
const targetDir = createPayload.scope === "global" ? path10.join(wstackGlobalRoot2(), "skills", createPayload.name.trim()) : path10.join(projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
5950
|
+
try {
|
|
5951
|
+
await fs10.access(targetDir);
|
|
5952
|
+
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
5953
|
+
break;
|
|
5954
|
+
} catch {
|
|
5955
|
+
}
|
|
5956
|
+
await fs10.mkdir(targetDir, { recursive: true });
|
|
5957
|
+
const lines = createPayload.description.trim().split("\n");
|
|
5958
|
+
const firstLine = lines[0].trim();
|
|
5959
|
+
const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
|
|
5960
|
+
const descriptionText = firstLine + (bodyLines.length > 0 ? `
|
|
5961
|
+
${bodyLines.join("\n")}` : "");
|
|
5962
|
+
const trigger = bodyLines.find((l) => l.toLowerCase().startsWith("triggers:")) ?? "";
|
|
5963
|
+
const skillContent = [
|
|
5964
|
+
"---",
|
|
5965
|
+
`name: ${createPayload.name.trim()}`,
|
|
5966
|
+
"description: |",
|
|
5967
|
+
` ${descriptionText.replace(/\n/g, "\n ")}`,
|
|
5968
|
+
`version: 1.0.0`,
|
|
5969
|
+
"---",
|
|
5970
|
+
"",
|
|
5971
|
+
`# ${createPayload.name.trim().split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}`,
|
|
5972
|
+
"",
|
|
5973
|
+
"## Overview",
|
|
5974
|
+
"",
|
|
5975
|
+
firstLine,
|
|
5976
|
+
"",
|
|
5977
|
+
...bodyLines.length > 0 ? bodyLines.filter((l) => !l.toLowerCase().startsWith("triggers:")) : [],
|
|
5978
|
+
"",
|
|
5979
|
+
"## Rules",
|
|
5980
|
+
"- TODO: add your first rule",
|
|
5981
|
+
"",
|
|
5982
|
+
"## Patterns",
|
|
5983
|
+
"### Do",
|
|
5984
|
+
"```ts",
|
|
5985
|
+
"// TODO: add a good example",
|
|
5986
|
+
"```",
|
|
5987
|
+
"",
|
|
5988
|
+
"### Don't",
|
|
5989
|
+
"```ts",
|
|
5990
|
+
"// TODO: add a bad example",
|
|
5991
|
+
"```",
|
|
5992
|
+
"",
|
|
5993
|
+
"## Workflow",
|
|
5994
|
+
"1. TODO: describe step one",
|
|
5995
|
+
"2. TODO: describe step two",
|
|
5996
|
+
"",
|
|
5997
|
+
trigger ? `
|
|
5998
|
+
${trigger}
|
|
5999
|
+
` : "",
|
|
6000
|
+
"## Skills in scope",
|
|
6001
|
+
"- `bug-hunter` \u2014 for systematic bug detection patterns",
|
|
6002
|
+
"- `output-standards` \u2014 for standardized `<next_steps>` formatting"
|
|
6003
|
+
].join("\n");
|
|
6004
|
+
await fs10.writeFile(path10.join(targetDir, "SKILL.md"), skillContent, "utf-8");
|
|
6005
|
+
send(ws, {
|
|
6006
|
+
type: "skills.created",
|
|
6007
|
+
payload: {
|
|
6008
|
+
success: true,
|
|
6009
|
+
error: null,
|
|
6010
|
+
skill: { name: createPayload.name.trim(), path: path10.join(targetDir, "SKILL.md"), scope: createPayload.scope }
|
|
6011
|
+
}
|
|
6012
|
+
});
|
|
6013
|
+
} catch (err) {
|
|
6014
|
+
send(ws, { type: "skills.created", payload: { success: false, error: errMessage(err) } });
|
|
6015
|
+
}
|
|
6016
|
+
break;
|
|
6017
|
+
}
|
|
6018
|
+
case "skills.edit": {
|
|
6019
|
+
if (!skillLoader) {
|
|
6020
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skills not enabled" } });
|
|
6021
|
+
break;
|
|
6022
|
+
}
|
|
6023
|
+
const editPayload = msg.payload;
|
|
6024
|
+
if (!editPayload?.name?.trim()) {
|
|
6025
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skill name is required" } });
|
|
6026
|
+
break;
|
|
6027
|
+
}
|
|
6028
|
+
if (!editPayload?.body) {
|
|
6029
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Skill body is required" } });
|
|
6030
|
+
break;
|
|
6031
|
+
}
|
|
6032
|
+
try {
|
|
6033
|
+
const entries = await skillLoader.listEntries();
|
|
6034
|
+
const entry = entries.find((e) => e.name.toLowerCase() === editPayload.name.toLowerCase());
|
|
6035
|
+
if (!entry) {
|
|
6036
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: `Skill "${editPayload.name}" not found` } });
|
|
6037
|
+
break;
|
|
6038
|
+
}
|
|
6039
|
+
if (entry.scope.includes("bundled")) {
|
|
6040
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: "Bundled skills cannot be edited" } });
|
|
6041
|
+
break;
|
|
6042
|
+
}
|
|
6043
|
+
await fs10.writeFile(entry.path, editPayload.body, "utf-8");
|
|
6044
|
+
send(ws, { type: "skills.edited", payload: { success: true, error: null } });
|
|
6045
|
+
} catch (err) {
|
|
6046
|
+
send(ws, { type: "skills.edited", payload: { success: false, error: errMessage(err) } });
|
|
6047
|
+
}
|
|
6048
|
+
break;
|
|
6049
|
+
}
|
|
6050
|
+
case "skills.export": {
|
|
6051
|
+
if (!skillLoader) {
|
|
6052
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: "Skills not enabled" } });
|
|
6053
|
+
break;
|
|
6054
|
+
}
|
|
6055
|
+
try {
|
|
6056
|
+
const entries = await skillLoader.listEntries();
|
|
6057
|
+
const zip = new JSZip2();
|
|
6058
|
+
for (const entry of entries) {
|
|
6059
|
+
try {
|
|
6060
|
+
const body = await skillLoader.readBody(entry.name);
|
|
6061
|
+
const safeName = entry.name.replace(/\//g, "_");
|
|
6062
|
+
zip.file(`${safeName}/SKILL.md`, body);
|
|
6063
|
+
} catch {
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
6066
|
+
const zipBuffer = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
|
6067
|
+
const zipBase64 = zipBuffer.toString("base64");
|
|
6068
|
+
send(ws, { type: "skills.exported", payload: { zipBase64, skillCount: entries.length, error: void 0 } });
|
|
6069
|
+
} catch (err) {
|
|
6070
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: errMessage(err) } });
|
|
6071
|
+
}
|
|
6072
|
+
break;
|
|
6073
|
+
}
|
|
4284
6074
|
case "diag.get": {
|
|
4285
6075
|
const usage = tokenCounter.total();
|
|
4286
6076
|
send(ws, {
|
|
@@ -4308,125 +6098,84 @@ async function startWebUI(opts = {}) {
|
|
|
4308
6098
|
break;
|
|
4309
6099
|
}
|
|
4310
6100
|
case "todos.get": {
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
6101
|
+
const ctx = {
|
|
6102
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6103
|
+
send: (w, m) => send(w, m),
|
|
6104
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6105
|
+
};
|
|
6106
|
+
handleTodosGet(ctx, ws);
|
|
4315
6107
|
break;
|
|
4316
6108
|
}
|
|
4317
6109
|
case "todos.clear": {
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
6110
|
+
const ctx = {
|
|
6111
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6112
|
+
send: (w, m) => send(w, m),
|
|
6113
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6114
|
+
};
|
|
6115
|
+
handleTodosClear(ctx, ws);
|
|
4321
6116
|
break;
|
|
4322
6117
|
}
|
|
4323
6118
|
case "todos.remove": {
|
|
4324
|
-
const
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
}
|
|
4329
|
-
|
|
4330
|
-
let targetIdx = -1;
|
|
4331
|
-
if (typeof id === "string") {
|
|
4332
|
-
targetIdx = context.todos.findIndex((t) => t.id === id);
|
|
4333
|
-
} else if (typeof index === "number" && index > 0) {
|
|
4334
|
-
targetIdx = index - 1;
|
|
4335
|
-
}
|
|
4336
|
-
if (targetIdx < 0 || !context.todos[targetIdx]) {
|
|
4337
|
-
sendResult(ws, false, "Todo not found");
|
|
4338
|
-
break;
|
|
4339
|
-
}
|
|
4340
|
-
const removed = expectDefined2(context.todos[targetIdx]);
|
|
4341
|
-
const next = [...context.todos.slice(0, targetIdx), ...context.todos.slice(targetIdx + 1)];
|
|
4342
|
-
context.state.replaceTodos(next);
|
|
4343
|
-
sendResult(ws, true, `Removed: ${removed.content}`);
|
|
4344
|
-
broadcast(clients, { type: "todos.updated", payload: { todos: next } });
|
|
6119
|
+
const ctx = {
|
|
6120
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6121
|
+
send: (w, m) => send(w, m),
|
|
6122
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6123
|
+
};
|
|
6124
|
+
handleTodosRemove(ctx, ws, msg.payload);
|
|
4345
6125
|
break;
|
|
4346
6126
|
}
|
|
4347
6127
|
case "tasks.get": {
|
|
4348
|
-
const
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
type: "tasks.updated",
|
|
4355
|
-
payload: { tasks: file?.tasks ?? [] }
|
|
4356
|
-
});
|
|
4357
|
-
} catch {
|
|
4358
|
-
send(ws, { type: "tasks.updated", payload: { tasks: [] } });
|
|
4359
|
-
}
|
|
4360
|
-
} else {
|
|
4361
|
-
send(ws, { type: "tasks.updated", payload: { tasks: [], error: "Task storage not configured." } });
|
|
4362
|
-
}
|
|
6128
|
+
const ctx = {
|
|
6129
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6130
|
+
send: (w, m) => send(w, m),
|
|
6131
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6132
|
+
};
|
|
6133
|
+
await handleTasksGet(ctx, ws);
|
|
4363
6134
|
break;
|
|
4364
6135
|
}
|
|
4365
6136
|
case "plan.get": {
|
|
4366
|
-
const
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
type: "plan.updated",
|
|
4373
|
-
payload: {
|
|
4374
|
-
plan: plan ?? {
|
|
4375
|
-
version: 1,
|
|
4376
|
-
sessionId: session.id,
|
|
4377
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4378
|
-
items: []
|
|
4379
|
-
}
|
|
4380
|
-
}
|
|
4381
|
-
});
|
|
4382
|
-
} catch {
|
|
4383
|
-
send(ws, {
|
|
4384
|
-
type: "plan.updated",
|
|
4385
|
-
payload: {
|
|
4386
|
-
plan: {
|
|
4387
|
-
version: 1,
|
|
4388
|
-
sessionId: session.id,
|
|
4389
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4390
|
-
items: []
|
|
4391
|
-
}
|
|
4392
|
-
}
|
|
4393
|
-
});
|
|
4394
|
-
}
|
|
4395
|
-
} else {
|
|
4396
|
-
send(ws, {
|
|
4397
|
-
type: "plan.updated",
|
|
4398
|
-
payload: { plan: null, error: "Plan storage is not configured for this session." }
|
|
4399
|
-
});
|
|
4400
|
-
}
|
|
6137
|
+
const ctx = {
|
|
6138
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6139
|
+
send: (w, m) => send(w, m),
|
|
6140
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6141
|
+
};
|
|
6142
|
+
await handlePlanGet(ctx, ws);
|
|
4401
6143
|
break;
|
|
4402
6144
|
}
|
|
4403
6145
|
case "plan.template_use": {
|
|
4404
|
-
const
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
6146
|
+
const ctx = {
|
|
6147
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6148
|
+
send: (w, m) => send(w, m),
|
|
6149
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6150
|
+
};
|
|
6151
|
+
await handlePlanTemplateUse(ctx, ws, msg.payload.template);
|
|
6152
|
+
break;
|
|
6153
|
+
}
|
|
6154
|
+
case "todo.update": {
|
|
6155
|
+
const ctx = {
|
|
6156
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6157
|
+
send: (w, m) => send(w, m),
|
|
6158
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6159
|
+
};
|
|
6160
|
+
handleTodoUpdate(ctx, ws, msg.payload);
|
|
6161
|
+
break;
|
|
6162
|
+
}
|
|
6163
|
+
case "task.update": {
|
|
6164
|
+
const ctx = {
|
|
6165
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6166
|
+
send: (w, m) => send(w, m),
|
|
6167
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6168
|
+
};
|
|
6169
|
+
await handleTaskUpdate(ctx, ws, msg.payload);
|
|
6170
|
+
break;
|
|
6171
|
+
}
|
|
6172
|
+
case "plan.item.update": {
|
|
6173
|
+
const ctx = {
|
|
6174
|
+
context: { todos: context.todos, meta: context.meta, session: context.session ? { id: context.session.id } : null, state: context.state },
|
|
6175
|
+
send: (w, m) => send(w, m),
|
|
6176
|
+
broadcast: (m) => broadcast(clients, m)
|
|
6177
|
+
};
|
|
6178
|
+
await handlePlanItemUpdate(ctx, ws, msg.payload);
|
|
4430
6179
|
break;
|
|
4431
6180
|
}
|
|
4432
6181
|
// ── File operations — delegated to shared handlers (file-handlers.ts) ──
|
|
@@ -4496,13 +6245,13 @@ async function startWebUI(opts = {}) {
|
|
|
4496
6245
|
provider: config.provider,
|
|
4497
6246
|
model: config.model
|
|
4498
6247
|
});
|
|
4499
|
-
|
|
6248
|
+
sendResult2(ws, true, `Switched to mode "${id}"`);
|
|
4500
6249
|
broadcast(clients, {
|
|
4501
6250
|
type: "session.start",
|
|
4502
6251
|
payload: { ...await sessionStartPayload() }
|
|
4503
6252
|
});
|
|
4504
6253
|
} catch (err) {
|
|
4505
|
-
|
|
6254
|
+
sendResult2(ws, false, errMessage(err));
|
|
4506
6255
|
}
|
|
4507
6256
|
break;
|
|
4508
6257
|
}
|
|
@@ -4556,13 +6305,13 @@ async function startWebUI(opts = {}) {
|
|
|
4556
6305
|
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
4557
6306
|
const proc = getProcessRegistry().get(pid);
|
|
4558
6307
|
if (proc?.protected) {
|
|
4559
|
-
|
|
6308
|
+
sendResult2(ws, false, `Cannot kill protected process (PID ${pid})`);
|
|
4560
6309
|
break;
|
|
4561
6310
|
}
|
|
4562
6311
|
getProcessRegistry().kill(pid);
|
|
4563
|
-
|
|
6312
|
+
sendResult2(ws, true, `Killed PID ${pid}`);
|
|
4564
6313
|
} catch (err) {
|
|
4565
|
-
|
|
6314
|
+
sendResult2(ws, false, errMessage(err));
|
|
4566
6315
|
}
|
|
4567
6316
|
break;
|
|
4568
6317
|
}
|
|
@@ -4570,16 +6319,25 @@ async function startWebUI(opts = {}) {
|
|
|
4570
6319
|
try {
|
|
4571
6320
|
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
4572
6321
|
getProcessRegistry().killAll();
|
|
4573
|
-
|
|
6322
|
+
sendResult2(ws, true, "All processes killed");
|
|
4574
6323
|
} catch (err) {
|
|
4575
|
-
|
|
6324
|
+
sendResult2(ws, false, errMessage(err));
|
|
4576
6325
|
}
|
|
4577
6326
|
break;
|
|
4578
6327
|
}
|
|
6328
|
+
case "git.info": {
|
|
6329
|
+
await handleGitInfo(ws, projectRoot);
|
|
6330
|
+
break;
|
|
6331
|
+
}
|
|
6332
|
+
case "webui.shutdown": {
|
|
6333
|
+
console.log("[WebUI] Shutdown requested from client");
|
|
6334
|
+
process.kill(process.pid, "SIGINT");
|
|
6335
|
+
break;
|
|
6336
|
+
}
|
|
4579
6337
|
case "goal.get": {
|
|
4580
6338
|
try {
|
|
4581
|
-
const goalPath =
|
|
4582
|
-
const raw = await
|
|
6339
|
+
const goalPath = resolveWstackPaths({ projectRoot }).projectGoal;
|
|
6340
|
+
const raw = await fs10.readFile(goalPath, "utf8");
|
|
4583
6341
|
const goal = JSON.parse(raw);
|
|
4584
6342
|
broadcast(clients, { type: "goal.updated", payload: goal });
|
|
4585
6343
|
} catch {
|
|
@@ -4590,7 +6348,7 @@ async function startWebUI(opts = {}) {
|
|
|
4590
6348
|
case "autonomy.switch": {
|
|
4591
6349
|
const { mode } = msg.payload;
|
|
4592
6350
|
context.meta["autonomy"] = mode;
|
|
4593
|
-
|
|
6351
|
+
sendResult2(ws, true, `Autonomy mode set to "${mode}"`);
|
|
4594
6352
|
broadcast(clients, { type: "prefs.updated", payload: { autonomy: mode } });
|
|
4595
6353
|
void persistPrefsToConfig({ autonomy: mode });
|
|
4596
6354
|
break;
|
|
@@ -4639,7 +6397,7 @@ async function startWebUI(opts = {}) {
|
|
|
4639
6397
|
try {
|
|
4640
6398
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4641
6399
|
const rewinder = new DefaultSessionRewinder(
|
|
4642
|
-
|
|
6400
|
+
path10.join(projectRoot, ".wrongstack", "sessions"),
|
|
4643
6401
|
projectRoot
|
|
4644
6402
|
);
|
|
4645
6403
|
const checkpoints = await rewinder.listCheckpoints(session.id);
|
|
@@ -4660,18 +6418,18 @@ async function startWebUI(opts = {}) {
|
|
|
4660
6418
|
try {
|
|
4661
6419
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4662
6420
|
const rewinder = new DefaultSessionRewinder(
|
|
4663
|
-
|
|
6421
|
+
path10.join(projectRoot, ".wrongstack", "sessions"),
|
|
4664
6422
|
projectRoot
|
|
4665
6423
|
);
|
|
4666
6424
|
await rewinder.rewindToCheckpoint(session.id, checkpointIndex);
|
|
4667
6425
|
await context.session.truncateToCheckpoint(checkpointIndex);
|
|
4668
|
-
|
|
6426
|
+
sendResult2(ws, true, `Rewound to checkpoint ${checkpointIndex}`);
|
|
4669
6427
|
broadcast(clients, {
|
|
4670
6428
|
type: "session.start",
|
|
4671
6429
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
4672
6430
|
});
|
|
4673
6431
|
} catch (err) {
|
|
4674
|
-
|
|
6432
|
+
sendResult2(ws, false, errMessage(err));
|
|
4675
6433
|
}
|
|
4676
6434
|
break;
|
|
4677
6435
|
}
|
|
@@ -4694,9 +6452,9 @@ async function startWebUI(opts = {}) {
|
|
|
4694
6452
|
case "projects.add": {
|
|
4695
6453
|
const { root: addRoot, name: displayName } = msg.payload;
|
|
4696
6454
|
try {
|
|
4697
|
-
const resolved =
|
|
4698
|
-
await
|
|
4699
|
-
const stat2 = await
|
|
6455
|
+
const resolved = path10.resolve(addRoot);
|
|
6456
|
+
await fs10.access(resolved);
|
|
6457
|
+
const stat2 = await fs10.stat(resolved);
|
|
4700
6458
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4701
6459
|
const manifest = await loadManifest(globalConfigPath);
|
|
4702
6460
|
const existing = manifest.projects.find((p) => p.root === resolved);
|
|
@@ -4712,7 +6470,7 @@ async function startWebUI(opts = {}) {
|
|
|
4712
6470
|
});
|
|
4713
6471
|
break;
|
|
4714
6472
|
}
|
|
4715
|
-
const name = displayName?.trim() ||
|
|
6473
|
+
const name = displayName?.trim() || path10.basename(resolved);
|
|
4716
6474
|
const slug = generateProjectSlug(resolved);
|
|
4717
6475
|
await ensureProjectDataDir(slug, globalConfigPath);
|
|
4718
6476
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -4731,7 +6489,7 @@ async function startWebUI(opts = {}) {
|
|
|
4731
6489
|
send(ws, {
|
|
4732
6490
|
type: "projects.added",
|
|
4733
6491
|
payload: {
|
|
4734
|
-
name:
|
|
6492
|
+
name: path10.basename(addRoot),
|
|
4735
6493
|
root: addRoot,
|
|
4736
6494
|
slug: "",
|
|
4737
6495
|
message: errMessage(err)
|
|
@@ -4743,17 +6501,17 @@ async function startWebUI(opts = {}) {
|
|
|
4743
6501
|
case "projects.select": {
|
|
4744
6502
|
const { root: selRoot, name: selName } = msg.payload;
|
|
4745
6503
|
try {
|
|
4746
|
-
const resolved =
|
|
6504
|
+
const resolved = path10.resolve(selRoot);
|
|
4747
6505
|
try {
|
|
4748
|
-
await
|
|
4749
|
-
const stat2 = await
|
|
6506
|
+
await fs10.access(resolved);
|
|
6507
|
+
const stat2 = await fs10.stat(resolved);
|
|
4750
6508
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4751
6509
|
} catch (err) {
|
|
4752
6510
|
send(ws, {
|
|
4753
6511
|
type: "projects.selected",
|
|
4754
6512
|
payload: {
|
|
4755
6513
|
root: selRoot,
|
|
4756
|
-
name: selName ||
|
|
6514
|
+
name: selName || path10.basename(selRoot),
|
|
4757
6515
|
message: `Cannot switch: ${errMessage(err)}`
|
|
4758
6516
|
}
|
|
4759
6517
|
});
|
|
@@ -4765,7 +6523,7 @@ async function startWebUI(opts = {}) {
|
|
|
4765
6523
|
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
4766
6524
|
entry.lastWorkingDir = resolved;
|
|
4767
6525
|
} else {
|
|
4768
|
-
const name = selName?.trim() ||
|
|
6526
|
+
const name = selName?.trim() || path10.basename(resolved);
|
|
4769
6527
|
const slug = generateProjectSlug(resolved);
|
|
4770
6528
|
manifest.projects.push({
|
|
4771
6529
|
name,
|
|
@@ -4806,13 +6564,13 @@ async function startWebUI(opts = {}) {
|
|
|
4806
6564
|
});
|
|
4807
6565
|
} catch {
|
|
4808
6566
|
}
|
|
4809
|
-
const newSessionsDir =
|
|
4810
|
-
|
|
6567
|
+
const newSessionsDir = path10.join(
|
|
6568
|
+
path10.dirname(globalConfigPath),
|
|
4811
6569
|
"projects",
|
|
4812
6570
|
switchSlug,
|
|
4813
6571
|
"sessions"
|
|
4814
6572
|
);
|
|
4815
|
-
await
|
|
6573
|
+
await fs10.mkdir(newSessionsDir, { recursive: true });
|
|
4816
6574
|
const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
|
|
4817
6575
|
const oldSessionId = session.id;
|
|
4818
6576
|
try {
|
|
@@ -4844,8 +6602,9 @@ async function startWebUI(opts = {}) {
|
|
|
4844
6602
|
sessionId: session.id,
|
|
4845
6603
|
projectSlug: switchSlug,
|
|
4846
6604
|
projectRoot,
|
|
4847
|
-
projectName:
|
|
6605
|
+
projectName: path10.basename(projectRoot),
|
|
4848
6606
|
workingDir,
|
|
6607
|
+
clientType: "webui",
|
|
4849
6608
|
pid: process.pid,
|
|
4850
6609
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4851
6610
|
});
|
|
@@ -4855,8 +6614,8 @@ async function startWebUI(opts = {}) {
|
|
|
4855
6614
|
type: "projects.selected",
|
|
4856
6615
|
payload: {
|
|
4857
6616
|
root: resolved,
|
|
4858
|
-
name: selName ||
|
|
4859
|
-
message: `Switched to ${selName ||
|
|
6617
|
+
name: selName || path10.basename(resolved),
|
|
6618
|
+
message: `Switched to ${selName || path10.basename(resolved)}`
|
|
4860
6619
|
}
|
|
4861
6620
|
});
|
|
4862
6621
|
broadcast(clients, {
|
|
@@ -4879,7 +6638,7 @@ async function startWebUI(opts = {}) {
|
|
|
4879
6638
|
type: "projects.selected",
|
|
4880
6639
|
payload: {
|
|
4881
6640
|
root: selRoot,
|
|
4882
|
-
name: selName ||
|
|
6641
|
+
name: selName || path10.basename(selRoot),
|
|
4883
6642
|
message: errMessage(err)
|
|
4884
6643
|
}
|
|
4885
6644
|
});
|
|
@@ -4890,17 +6649,17 @@ async function startWebUI(opts = {}) {
|
|
|
4890
6649
|
case "working_dir.set": {
|
|
4891
6650
|
const { path: newPath } = msg.payload;
|
|
4892
6651
|
try {
|
|
4893
|
-
const resolved =
|
|
4894
|
-
if (!resolved.startsWith(projectRoot +
|
|
4895
|
-
|
|
6652
|
+
const resolved = path10.resolve(projectRoot, newPath);
|
|
6653
|
+
if (!resolved.startsWith(projectRoot + path10.sep) && resolved !== projectRoot) {
|
|
6654
|
+
sendResult2(ws, false, `Path must stay inside the project root: ${projectRoot}`);
|
|
4896
6655
|
break;
|
|
4897
6656
|
}
|
|
4898
6657
|
try {
|
|
4899
|
-
await
|
|
4900
|
-
const stat2 = await
|
|
6658
|
+
await fs10.access(resolved);
|
|
6659
|
+
const stat2 = await fs10.stat(resolved);
|
|
4901
6660
|
if (!stat2.isDirectory()) throw new Error("Not a directory");
|
|
4902
6661
|
} catch {
|
|
4903
|
-
|
|
6662
|
+
sendResult2(ws, false, `Directory not found or not accessible: ${resolved}`);
|
|
4904
6663
|
break;
|
|
4905
6664
|
}
|
|
4906
6665
|
workingDir = resolved;
|
|
@@ -4909,9 +6668,9 @@ async function startWebUI(opts = {}) {
|
|
|
4909
6668
|
type: "working_dir.changed",
|
|
4910
6669
|
payload: { cwd: resolved, projectRoot }
|
|
4911
6670
|
});
|
|
4912
|
-
|
|
6671
|
+
sendResult2(ws, true, `Working directory set to ${resolved}`);
|
|
4913
6672
|
} catch (err) {
|
|
4914
|
-
|
|
6673
|
+
sendResult2(ws, false, errMessage(err));
|
|
4915
6674
|
}
|
|
4916
6675
|
break;
|
|
4917
6676
|
}
|
|
@@ -4921,26 +6680,32 @@ async function startWebUI(opts = {}) {
|
|
|
4921
6680
|
msg.payload,
|
|
4922
6681
|
logger
|
|
4923
6682
|
);
|
|
4924
|
-
|
|
6683
|
+
sendResult2(ws, result.success, result.message);
|
|
4925
6684
|
break;
|
|
4926
6685
|
}
|
|
4927
6686
|
// ── Mailbox operations — project-level inter-agent messaging ────
|
|
4928
6687
|
case "mailbox.messages":
|
|
4929
6688
|
return handleMailboxMessages(
|
|
4930
6689
|
ws,
|
|
4931
|
-
{ projectRoot, globalRoot:
|
|
6690
|
+
{ projectRoot, globalRoot: path10.dirname(globalConfigPath) },
|
|
4932
6691
|
msg.payload
|
|
4933
6692
|
);
|
|
4934
6693
|
case "mailbox.agents":
|
|
4935
6694
|
return handleMailboxAgents(
|
|
4936
6695
|
ws,
|
|
4937
|
-
{ projectRoot, globalRoot:
|
|
6696
|
+
{ projectRoot, globalRoot: path10.dirname(globalConfigPath) },
|
|
4938
6697
|
msg.payload
|
|
4939
6698
|
);
|
|
4940
6699
|
case "mailbox.clear":
|
|
4941
6700
|
return handleMailboxClear(
|
|
4942
6701
|
ws,
|
|
4943
|
-
{ projectRoot, globalRoot:
|
|
6702
|
+
{ projectRoot, globalRoot: path10.dirname(globalConfigPath) }
|
|
6703
|
+
);
|
|
6704
|
+
case "mailbox.purge":
|
|
6705
|
+
return handleMailboxPurge(
|
|
6706
|
+
ws,
|
|
6707
|
+
{ projectRoot, globalRoot: path10.dirname(globalConfigPath) },
|
|
6708
|
+
msg.payload
|
|
4944
6709
|
);
|
|
4945
6710
|
// ── Brain — status, autonomy ceiling, direct decision support ───
|
|
4946
6711
|
case "brain.status":
|
|
@@ -4953,7 +6718,7 @@ async function startWebUI(opts = {}) {
|
|
|
4953
6718
|
const level = msg.payload?.level ?? "";
|
|
4954
6719
|
const valid = ["off", "low", "medium", "high", "all"];
|
|
4955
6720
|
if (!valid.includes(level)) {
|
|
4956
|
-
|
|
6721
|
+
sendResult2(ws, false, `Unknown risk level "${level}". Use: ${valid.join(", ")}.`);
|
|
4957
6722
|
break;
|
|
4958
6723
|
}
|
|
4959
6724
|
brainSettings.maxAutoRisk = level;
|
|
@@ -4966,7 +6731,7 @@ async function startWebUI(opts = {}) {
|
|
|
4966
6731
|
case "brain.ask": {
|
|
4967
6732
|
const question = msg.payload?.question?.trim();
|
|
4968
6733
|
if (!question) {
|
|
4969
|
-
|
|
6734
|
+
sendResult2(ws, false, "Usage: /brain ask <question>");
|
|
4970
6735
|
break;
|
|
4971
6736
|
}
|
|
4972
6737
|
try {
|
|
@@ -4979,7 +6744,7 @@ async function startWebUI(opts = {}) {
|
|
|
4979
6744
|
});
|
|
4980
6745
|
send(ws, { type: "brain.answer", payload: { question, decision } });
|
|
4981
6746
|
} catch (err) {
|
|
4982
|
-
|
|
6747
|
+
sendResult2(ws, false, `Brain consultation failed: ${errMessage(err)}`);
|
|
4983
6748
|
}
|
|
4984
6749
|
break;
|
|
4985
6750
|
}
|
|
@@ -5006,14 +6771,28 @@ async function startWebUI(opts = {}) {
|
|
|
5006
6771
|
broadcast,
|
|
5007
6772
|
clients
|
|
5008
6773
|
});
|
|
6774
|
+
const watcherMetrics = {
|
|
6775
|
+
fileChangesDetected: 0,
|
|
6776
|
+
filesProcessed: 0,
|
|
6777
|
+
broadcastsSent: 0,
|
|
6778
|
+
debounceResets: 0,
|
|
6779
|
+
totalDebounceDelayMs: 0,
|
|
6780
|
+
activeProjects: 0,
|
|
6781
|
+
averageDebounceDelayMs: 0,
|
|
6782
|
+
watcherActive: false
|
|
6783
|
+
};
|
|
5009
6784
|
const httpServer = createHttpServer({
|
|
5010
6785
|
host: wsHost,
|
|
5011
|
-
distDir:
|
|
6786
|
+
distDir: path10.resolve(import.meta.dirname, "../../dist"),
|
|
5012
6787
|
wsPort,
|
|
5013
6788
|
globalRoot: wpaths.globalRoot,
|
|
5014
|
-
apiToken: wsToken
|
|
6789
|
+
apiToken: wsToken,
|
|
6790
|
+
watcherMetrics,
|
|
6791
|
+
onFleetPing: () => {
|
|
6792
|
+
void fleetBroadcast?.();
|
|
6793
|
+
}
|
|
5015
6794
|
});
|
|
5016
|
-
const registryBaseDir =
|
|
6795
|
+
const registryBaseDir = path10.dirname(globalConfigPath);
|
|
5017
6796
|
httpServer.listen(httpPort, wsHost, () => {
|
|
5018
6797
|
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
5019
6798
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
@@ -5025,7 +6804,7 @@ async function startWebUI(opts = {}) {
|
|
|
5025
6804
|
wsPort,
|
|
5026
6805
|
host: wsHost,
|
|
5027
6806
|
projectRoot,
|
|
5028
|
-
projectName:
|
|
6807
|
+
projectName: path10.basename(projectRoot) || projectRoot,
|
|
5029
6808
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5030
6809
|
url: `http://${wsHost}:${httpPort}`
|
|
5031
6810
|
},
|
|
@@ -5052,6 +6831,10 @@ async function startWebUI(opts = {}) {
|
|
|
5052
6831
|
// reality. Crash exits are healed by the next register()/list() prune pass.
|
|
5053
6832
|
onShutdown: () => {
|
|
5054
6833
|
brainMonitor.stop();
|
|
6834
|
+
if (disposeEvents) {
|
|
6835
|
+
disposeEvents();
|
|
6836
|
+
disposeEvents = null;
|
|
6837
|
+
}
|
|
5055
6838
|
if (eternalSubscription) {
|
|
5056
6839
|
eternalSubscription.dispose();
|
|
5057
6840
|
eternalSubscription = null;
|
|
@@ -5082,10 +6865,28 @@ export {
|
|
|
5082
6865
|
handleFilesRead,
|
|
5083
6866
|
handleFilesTree,
|
|
5084
6867
|
handleFilesWrite,
|
|
6868
|
+
handleGitInfo,
|
|
6869
|
+
handleMcpAdd,
|
|
6870
|
+
handleMcpDisable,
|
|
6871
|
+
handleMcpDiscover,
|
|
6872
|
+
handleMcpEnable,
|
|
6873
|
+
handleMcpList,
|
|
6874
|
+
handleMcpRemove,
|
|
6875
|
+
handleMcpRestart,
|
|
6876
|
+
handleMcpSleep,
|
|
6877
|
+
handleMcpUpdate,
|
|
6878
|
+
handleMcpWake,
|
|
5085
6879
|
handleMemoryForget,
|
|
5086
6880
|
handleMemoryList,
|
|
5087
6881
|
handleMemoryRemember,
|
|
5088
6882
|
handleShellOpen,
|
|
6883
|
+
handleSkillsContent,
|
|
6884
|
+
handleSkillsCreate,
|
|
6885
|
+
handleSkillsEdit,
|
|
6886
|
+
handleSkillsExport,
|
|
6887
|
+
handleSkillsInstall,
|
|
6888
|
+
handleSkillsUninstall,
|
|
6889
|
+
handleSkillsUpdate,
|
|
5089
6890
|
hostHeaderOk,
|
|
5090
6891
|
injectWsPort,
|
|
5091
6892
|
isLoopbackBind,
|
|
@@ -5103,7 +6904,7 @@ export {
|
|
|
5103
6904
|
removeProvider,
|
|
5104
6905
|
saveProviders,
|
|
5105
6906
|
send,
|
|
5106
|
-
sendResult,
|
|
6907
|
+
sendResult2 as sendResult,
|
|
5107
6908
|
setActiveKey,
|
|
5108
6909
|
startWebUI,
|
|
5109
6910
|
stringifyContent,
|