@wrongstack/webui 0.264.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 +8949 -4311
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +1612 -339
- 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 +1945 -405
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +391 -2
- package/package.json +7 -5
- package/dist/assets/index-BBPaC1tO.js +0 -170
- package/dist/assets/index-DJmqJ5Wo.css +0 -2
- package/dist/assets/vendor-pWpGJmMc.js +0 -1303
package/dist/server/index.js
CHANGED
|
@@ -1,7 +1,179 @@
|
|
|
1
1
|
// src/server/index.ts
|
|
2
|
-
import { expectDefined as expectDefined2, GlobalMailbox as GlobalMailbox2, projectSlug, getSessionRegistry, AgentStatusTracker } from "@wrongstack/core";
|
|
2
|
+
import { expectDefined as expectDefined2, GlobalMailbox as GlobalMailbox2, projectSlug, getSessionRegistry, AgentStatusTracker, FleetNotifier } from "@wrongstack/core";
|
|
3
|
+
|
|
4
|
+
// src/server/handlers/worklist-handlers.ts
|
|
5
|
+
function sendResult(ws, ctx, ok, message) {
|
|
6
|
+
ctx.send(ws, { type: ok ? "ok" : "error", message });
|
|
7
|
+
}
|
|
8
|
+
function handleTodosGet(ctx, ws) {
|
|
9
|
+
ctx.send(ws, { type: "todos.updated", payload: { todos: ctx.context.todos } });
|
|
10
|
+
}
|
|
11
|
+
function handleTodosClear(ctx, ws) {
|
|
12
|
+
ctx.replaceTodos?.([]);
|
|
13
|
+
ctx.broadcast({ type: "todos.cleared" });
|
|
14
|
+
sendResult(ws, ctx, true, "Todo board cleared.");
|
|
15
|
+
}
|
|
16
|
+
function handleTodosRemove(ctx, ws, payload) {
|
|
17
|
+
if (!payload || payload.id === void 0 && payload.index === void 0) {
|
|
18
|
+
sendResult(ws, ctx, false, "todos.remove requires id or index.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const next = payload.id !== void 0 ? ctx.context.todos.filter((t) => t.id !== payload.id) : ctx.context.todos.filter((_, i) => i !== payload.index);
|
|
22
|
+
ctx.replaceTodos?.(next);
|
|
23
|
+
ctx.broadcast({ type: "todos.updated", payload: { todos: next } });
|
|
24
|
+
sendResult(ws, ctx, true, "Todo item removed.");
|
|
25
|
+
}
|
|
26
|
+
function handleTodoUpdate(ctx, ws, payload) {
|
|
27
|
+
const todo = ctx.context.todos.find((t) => t.id === payload.id);
|
|
28
|
+
if (!todo) {
|
|
29
|
+
sendResult(ws, ctx, false, `No todo with id "${payload.id}".`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const next = ctx.context.todos.map(
|
|
33
|
+
(t) => t.id === payload.id ? { ...t, ...payload.status !== void 0 && { status: payload.status }, ...payload.activeForm !== void 0 && { activeForm: payload.activeForm } } : t
|
|
34
|
+
);
|
|
35
|
+
ctx.replaceTodos?.(next);
|
|
36
|
+
ctx.broadcast({ type: "todos.updated", payload: { todos: next } });
|
|
37
|
+
sendResult(ws, ctx, true, `Todo "${todo.content}" updated.`);
|
|
38
|
+
}
|
|
39
|
+
async function handleTasksGet(ctx, ws) {
|
|
40
|
+
const taskPath = ctx.context.meta["task.path"];
|
|
41
|
+
if (typeof taskPath === "string" && taskPath) {
|
|
42
|
+
try {
|
|
43
|
+
const { loadTasks } = await import("@wrongstack/core");
|
|
44
|
+
const file = await loadTasks(taskPath);
|
|
45
|
+
ctx.send(ws, { type: "tasks.updated", payload: { tasks: file?.tasks ?? [] } });
|
|
46
|
+
} catch {
|
|
47
|
+
ctx.send(ws, { type: "tasks.updated", payload: { tasks: [] } });
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
ctx.send(ws, {
|
|
51
|
+
type: "tasks.updated",
|
|
52
|
+
payload: { tasks: [], error: "Task storage not configured." }
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function handleTaskUpdate(ctx, ws, payload) {
|
|
57
|
+
const taskPath = ctx.context.meta["task.path"];
|
|
58
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
59
|
+
sendResult(ws, ctx, false, "Task storage is not configured for this session.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const { loadTasks, saveTasks } = await import("@wrongstack/core");
|
|
64
|
+
const file = await loadTasks(taskPath);
|
|
65
|
+
if (!file) {
|
|
66
|
+
sendResult(ws, ctx, false, "No task file found.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const idx = file.tasks.findIndex((t) => t.id === payload.id);
|
|
70
|
+
if (idx === -1) {
|
|
71
|
+
sendResult(ws, ctx, false, `Task "${payload.id}" not found.`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
file.tasks[idx] = { ...file.tasks[idx], status: payload.status };
|
|
75
|
+
await saveTasks(taskPath, file);
|
|
76
|
+
ctx.broadcast({ type: "tasks.updated", payload: { tasks: file.tasks } });
|
|
77
|
+
sendResult(ws, ctx, true, `Task "${payload.id}" marked ${payload.status}.`);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
sendResult(ws, ctx, false, String(err));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function handlePlanGet(ctx, ws) {
|
|
83
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
84
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
85
|
+
if (typeof planPath === "string" && planPath) {
|
|
86
|
+
try {
|
|
87
|
+
const { loadPlan } = await import("@wrongstack/core");
|
|
88
|
+
const plan = await loadPlan(planPath);
|
|
89
|
+
ctx.send(ws, {
|
|
90
|
+
type: "plan.updated",
|
|
91
|
+
payload: {
|
|
92
|
+
plan: plan ?? {
|
|
93
|
+
version: 1,
|
|
94
|
+
sessionId,
|
|
95
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
96
|
+
items: []
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
ctx.send(ws, {
|
|
102
|
+
type: "plan.updated",
|
|
103
|
+
payload: {
|
|
104
|
+
plan: {
|
|
105
|
+
version: 1,
|
|
106
|
+
sessionId,
|
|
107
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
108
|
+
items: []
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
ctx.send(ws, {
|
|
115
|
+
type: "plan.updated",
|
|
116
|
+
payload: { plan: null, error: "Plan storage is not configured for this session." }
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function handlePlanTemplateUse(ctx, ws, template) {
|
|
121
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
122
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
123
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
124
|
+
sendResult(ws, ctx, false, "Plan storage is not configured for this session.");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem } = await import("@wrongstack/core");
|
|
129
|
+
const tpl = getPlanTemplate(template);
|
|
130
|
+
if (!tpl) {
|
|
131
|
+
sendResult(ws, ctx, false, `Unknown template "${template}".`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let plan = await loadPlan(planPath) ?? emptyPlan(sessionId);
|
|
135
|
+
for (const item of tpl.items) {
|
|
136
|
+
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
137
|
+
}
|
|
138
|
+
await savePlan(planPath, plan);
|
|
139
|
+
sendResult(ws, ctx, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
|
|
140
|
+
ctx.broadcast({ type: "plan.updated", payload: { plan } });
|
|
141
|
+
} catch (err) {
|
|
142
|
+
sendResult(ws, ctx, false, String(err));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function handlePlanItemUpdate(ctx, ws, payload) {
|
|
146
|
+
const planPath = ctx.context.meta["plan.path"];
|
|
147
|
+
const sessionId = ctx.context.session?.id ?? "";
|
|
148
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
149
|
+
sendResult(ws, ctx, false, "Plan storage is not configured for this session.");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const { loadPlan, savePlan, mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
|
|
154
|
+
let changed = false;
|
|
155
|
+
const plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
156
|
+
const before = p.updatedAt;
|
|
157
|
+
const updated = setPlanItemStatus(p, payload.target, payload.status);
|
|
158
|
+
changed = updated.updatedAt !== before;
|
|
159
|
+
return updated;
|
|
160
|
+
});
|
|
161
|
+
if (!changed) {
|
|
162
|
+
sendResult(ws, ctx, false, `No plan item matched "${payload.target}".`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
sendResult(ws, ctx, true, `Plan item status updated to "${payload.status}".`);
|
|
166
|
+
ctx.broadcast({ type: "plan.updated", payload: { plan } });
|
|
167
|
+
} catch (err) {
|
|
168
|
+
sendResult(ws, ctx, false, String(err));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/server/index.ts
|
|
3
173
|
import { makeMailboxTool, makeMailSendTool, makeMailInboxTool, mailboxSessionTag } from "@wrongstack/core";
|
|
4
|
-
import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
|
|
174
|
+
import { toErrorMessage as toErrorMessage5, wstackGlobalRoot as wstackGlobalRoot2, projectHash, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
175
|
+
import { SkillInstaller } from "@wrongstack/core/skills";
|
|
176
|
+
import JSZip2 from "jszip";
|
|
5
177
|
import {
|
|
6
178
|
BrainMonitor,
|
|
7
179
|
DefaultBrainArbiter,
|
|
@@ -9,8 +181,8 @@ import {
|
|
|
9
181
|
createAutonomyBrain,
|
|
10
182
|
createTieredBrainArbiter
|
|
11
183
|
} from "@wrongstack/core";
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
184
|
+
import * as fs10 from "fs/promises";
|
|
185
|
+
import * as path10 from "path";
|
|
14
186
|
|
|
15
187
|
// src/server/http-server.ts
|
|
16
188
|
import * as fs from "fs/promises";
|
|
@@ -129,6 +301,13 @@ function isInsideDist(candidate, distDir) {
|
|
|
129
301
|
const resolved = path.resolve(candidate);
|
|
130
302
|
return resolved === root || resolved.startsWith(root + path.sep);
|
|
131
303
|
}
|
|
304
|
+
function decodeSessionId(segment) {
|
|
305
|
+
try {
|
|
306
|
+
return decodeURIComponent(segment);
|
|
307
|
+
} catch {
|
|
308
|
+
return segment;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
132
311
|
function createHttpServer(opts) {
|
|
133
312
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
134
313
|
const distDir = path.resolve(opts.distDir);
|
|
@@ -154,6 +333,22 @@ function createHttpServer(opts) {
|
|
|
154
333
|
res.end("ok");
|
|
155
334
|
return;
|
|
156
335
|
}
|
|
336
|
+
if (url.pathname === "/api/fleet/ping" && req.method === "POST") {
|
|
337
|
+
const headerToken = req.headers["x-ws-token"];
|
|
338
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
339
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
340
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
341
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
opts.onFleetPing?.();
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
res.writeHead(204);
|
|
349
|
+
res.end();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
157
352
|
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
158
353
|
const headerToken = req.headers["x-ws-token"];
|
|
159
354
|
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
@@ -174,7 +369,89 @@ function createHttpServer(opts) {
|
|
|
174
369
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
175
370
|
return;
|
|
176
371
|
}
|
|
177
|
-
await handleApiSessionAgents(res, opts.globalRoot, agentsMatch[1]);
|
|
372
|
+
await handleApiSessionAgents(res, opts.globalRoot, decodeSessionId(agentsMatch[1]));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const eventsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
|
|
376
|
+
if (eventsMatch && req.method === "GET") {
|
|
377
|
+
const headerToken = req.headers["x-ws-token"];
|
|
378
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
379
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
380
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
381
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
|
|
385
|
+
const limit = Math.min(500, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
|
|
386
|
+
await handleApiSessionEvents(res, opts.globalRoot, decodeSessionId(eventsMatch[1]), limit);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const msgMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/message$/);
|
|
390
|
+
if (msgMatch && req.method === "POST") {
|
|
391
|
+
const headerToken = req.headers["x-ws-token"];
|
|
392
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
393
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
394
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
395
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
await handleApiSessionMessage(res, req, opts.globalRoot, decodeSessionId(msgMatch[1]));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const mailboxMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/mailbox$/);
|
|
402
|
+
if (mailboxMatch && req.method === "GET") {
|
|
403
|
+
const headerToken = req.headers["x-ws-token"];
|
|
404
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
405
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
406
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
407
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
await handleApiSessionMailbox(res, opts.globalRoot, decodeSessionId(mailboxMatch[1]));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const interruptMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/interrupt$/);
|
|
414
|
+
if (interruptMatch && req.method === "POST") {
|
|
415
|
+
const headerToken = req.headers["x-ws-token"];
|
|
416
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
417
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
418
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
419
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
await handleApiSessionInterrupt(
|
|
423
|
+
res,
|
|
424
|
+
req,
|
|
425
|
+
opts.globalRoot,
|
|
426
|
+
decodeSessionId(interruptMatch[1])
|
|
427
|
+
);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (url.pathname === "/api/fleet/broadcast" && req.method === "POST") {
|
|
431
|
+
const headerToken = req.headers["x-ws-token"];
|
|
432
|
+
const provided = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
433
|
+
if (requireApiToken && !tokenMatches(provided, opts.apiToken ?? "")) {
|
|
434
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
435
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
await handleApiFleetBroadcast(res, req, opts.globalRoot);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (url.pathname === "/debug/watcher-metrics" && req.method === "GET") {
|
|
442
|
+
if (opts.watcherMetrics) {
|
|
443
|
+
const avgDelay = opts.watcherMetrics.broadcastsSent > 0 ? opts.watcherMetrics.totalDebounceDelayMs / opts.watcherMetrics.broadcastsSent : 0;
|
|
444
|
+
const response = {
|
|
445
|
+
...opts.watcherMetrics,
|
|
446
|
+
averageDebounceDelayMs: avgDelay,
|
|
447
|
+
timestamp: Date.now()
|
|
448
|
+
};
|
|
449
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
450
|
+
res.end(JSON.stringify(response));
|
|
451
|
+
} else {
|
|
452
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
453
|
+
res.end(JSON.stringify({ error: "File watcher metrics not available" }));
|
|
454
|
+
}
|
|
178
455
|
return;
|
|
179
456
|
}
|
|
180
457
|
let filePath;
|
|
@@ -306,6 +583,324 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
|
306
583
|
res.end(JSON.stringify({ error: String(err) }));
|
|
307
584
|
}
|
|
308
585
|
}
|
|
586
|
+
function blocksToText(content) {
|
|
587
|
+
if (typeof content === "string") return content;
|
|
588
|
+
if (Array.isArray(content)) {
|
|
589
|
+
return content.filter(
|
|
590
|
+
(b) => !!b && typeof b === "object" && b.type === "text" && typeof b.text === "string"
|
|
591
|
+
).map((b) => b.text).join("\n");
|
|
592
|
+
}
|
|
593
|
+
return "";
|
|
594
|
+
}
|
|
595
|
+
function clip(s, n = 600) {
|
|
596
|
+
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
597
|
+
}
|
|
598
|
+
function asString(v) {
|
|
599
|
+
if (typeof v === "string") return v;
|
|
600
|
+
try {
|
|
601
|
+
return JSON.stringify(v);
|
|
602
|
+
} catch {
|
|
603
|
+
return String(v);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function mapWatchEntry(ev) {
|
|
607
|
+
const ts = typeof ev["ts"] === "string" ? ev["ts"] : "";
|
|
608
|
+
switch (ev["type"]) {
|
|
609
|
+
case "user_input":
|
|
610
|
+
return { ts, role: "user", text: clip(blocksToText(ev["content"])) };
|
|
611
|
+
case "llm_response": {
|
|
612
|
+
const text = blocksToText(ev["content"]);
|
|
613
|
+
return text.trim() ? { ts, role: "assistant", text: clip(text) } : null;
|
|
614
|
+
}
|
|
615
|
+
case "tool_use":
|
|
616
|
+
case "tool_call_start": {
|
|
617
|
+
const input = ev["input"] ?? ev["args"];
|
|
618
|
+
const preview = input !== void 0 && input !== null ? clip(asString(input), 160) : "";
|
|
619
|
+
return { ts, role: "tool", tool: String(ev["name"] ?? "tool"), text: preview };
|
|
620
|
+
}
|
|
621
|
+
case "tool_result": {
|
|
622
|
+
if (ev["isError"]) return { ts, role: "error", text: clip(asString(ev["content"])) };
|
|
623
|
+
const out = asString(ev["content"]).trim();
|
|
624
|
+
return out ? { ts, role: "tool", tool: "\u21B3 result", text: clip(out, 240) } : null;
|
|
625
|
+
}
|
|
626
|
+
case "error":
|
|
627
|
+
case "provider_error":
|
|
628
|
+
return { ts, role: "error", text: clip(String(ev["message"] ?? "error")) };
|
|
629
|
+
case "agent_spawned":
|
|
630
|
+
return { ts, role: "system", text: `spawned ${String(ev["role"] ?? "agent")}` };
|
|
631
|
+
case "task_completed":
|
|
632
|
+
return { ts, role: "system", text: `task done: ${String(ev["title"] ?? "")}` };
|
|
633
|
+
case "task_failed":
|
|
634
|
+
return { ts, role: "system", text: `task failed: ${String(ev["title"] ?? "")}` };
|
|
635
|
+
default:
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
640
|
+
if (!globalRoot) {
|
|
641
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
642
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
|
|
647
|
+
const registry = new SessionRegistry(globalRoot);
|
|
648
|
+
const entry = await registry.get(sessionId);
|
|
649
|
+
if (!entry) {
|
|
650
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
651
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
655
|
+
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
656
|
+
const reader = new DefaultSessionReader2({ store });
|
|
657
|
+
const all = [];
|
|
658
|
+
for await (const ev of reader.replay(sessionId)) {
|
|
659
|
+
const mapped = mapWatchEntry(ev);
|
|
660
|
+
if (mapped) all.push(mapped);
|
|
661
|
+
}
|
|
662
|
+
const tail = all.slice(-limit);
|
|
663
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
664
|
+
res.end(
|
|
665
|
+
JSON.stringify({
|
|
666
|
+
sessionId,
|
|
667
|
+
status: entry.status,
|
|
668
|
+
clientType: entry.clientType,
|
|
669
|
+
projectName: entry.projectName,
|
|
670
|
+
total: all.length,
|
|
671
|
+
entries: tail
|
|
672
|
+
})
|
|
673
|
+
);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
676
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function readJsonBody(req) {
|
|
680
|
+
return new Promise((resolve5, reject) => {
|
|
681
|
+
let data = "";
|
|
682
|
+
req.on("data", (chunk) => {
|
|
683
|
+
data += chunk;
|
|
684
|
+
if (data.length > 64e3) {
|
|
685
|
+
reject(new Error("Request body too large"));
|
|
686
|
+
req.destroy();
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
req.on("end", () => {
|
|
690
|
+
try {
|
|
691
|
+
resolve5(data ? JSON.parse(data) : {});
|
|
692
|
+
} catch (err) {
|
|
693
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
req.on("error", reject);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
700
|
+
if (!globalRoot) {
|
|
701
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
702
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
let body;
|
|
706
|
+
try {
|
|
707
|
+
body = await readJsonBody(req);
|
|
708
|
+
} catch {
|
|
709
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
710
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const text = typeof body["text"] === "string" ? body["text"].trim() : "";
|
|
714
|
+
if (!text) {
|
|
715
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
716
|
+
res.end(JSON.stringify({ error: "text is required" }));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
720
|
+
const ALLOWED = /* @__PURE__ */ new Set(["steer", "ask", "assign", "note", "btw"]);
|
|
721
|
+
const rawType = typeof body["type"] === "string" ? body["type"] : "steer";
|
|
722
|
+
const type = ALLOWED.has(rawType) ? rawType : "steer";
|
|
723
|
+
const rawPriority = typeof body["priority"] === "string" ? body["priority"] : "";
|
|
724
|
+
const priority = ["low", "normal", "high"].includes(rawPriority) ? rawPriority : "high";
|
|
725
|
+
const subject = typeof body["subject"] === "string" && body["subject"].trim() ? body["subject"].trim() : "Message from Fleet HQ";
|
|
726
|
+
try {
|
|
727
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
728
|
+
const registry = new SessionRegistry(globalRoot);
|
|
729
|
+
const entry = await registry.get(sessionId);
|
|
730
|
+
if (!entry) {
|
|
731
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
732
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
736
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
737
|
+
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
738
|
+
const sent = await mailbox.send({ from, to, type, subject, body: text, priority });
|
|
739
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
740
|
+
res.end(JSON.stringify({ ok: true, id: sent.id, to, type, delivered: entry.status }));
|
|
741
|
+
} catch (err) {
|
|
742
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
743
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
747
|
+
if (!globalRoot) {
|
|
748
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
749
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
754
|
+
const registry = new SessionRegistry(globalRoot);
|
|
755
|
+
const entry = await registry.get(sessionId);
|
|
756
|
+
if (!entry) {
|
|
757
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
758
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
762
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
763
|
+
const leaderAddr = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
764
|
+
const [inbound, outbound] = await Promise.all([
|
|
765
|
+
mailbox.query({ to: leaderAddr, limit: 50 }),
|
|
766
|
+
mailbox.query({ from: leaderAddr, limit: 50 })
|
|
767
|
+
]);
|
|
768
|
+
const seen = /* @__PURE__ */ new Set();
|
|
769
|
+
const thread = [...inbound, ...outbound].filter((m) => {
|
|
770
|
+
if (seen.has(m.id)) return false;
|
|
771
|
+
seen.add(m.id);
|
|
772
|
+
return true;
|
|
773
|
+
}).sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)).map((m) => ({
|
|
774
|
+
id: m.id,
|
|
775
|
+
from: m.from,
|
|
776
|
+
to: m.to,
|
|
777
|
+
type: m.type,
|
|
778
|
+
subject: m.subject,
|
|
779
|
+
body: m.body,
|
|
780
|
+
priority: m.priority,
|
|
781
|
+
// Whether the leader has read it, and when.
|
|
782
|
+
readByLeader: m.readBy?.[leaderAddr] ?? null,
|
|
783
|
+
readByCount: Object.keys(m.readBy ?? {}).length,
|
|
784
|
+
completed: m.completed,
|
|
785
|
+
outcome: m.outcome ?? null,
|
|
786
|
+
timestamp: m.timestamp,
|
|
787
|
+
replyTo: m.replyTo ?? null,
|
|
788
|
+
fromLeader: m.from === leaderAddr
|
|
789
|
+
}));
|
|
790
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
791
|
+
res.end(JSON.stringify({ sessionId, leader: leaderAddr, status: entry.status, thread }));
|
|
792
|
+
} catch (err) {
|
|
793
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
794
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
798
|
+
if (!globalRoot) {
|
|
799
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
800
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
let body = {};
|
|
804
|
+
try {
|
|
805
|
+
body = await readJsonBody(req);
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
|
|
809
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
810
|
+
try {
|
|
811
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
812
|
+
const registry = new SessionRegistry(globalRoot);
|
|
813
|
+
const entry = await registry.get(sessionId);
|
|
814
|
+
if (!entry) {
|
|
815
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
816
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
820
|
+
const mailbox = new GlobalMailbox3(paths.projectDir);
|
|
821
|
+
const to = `leader@${mailboxSessionTag2(sessionId)}`;
|
|
822
|
+
const sent = await mailbox.send({
|
|
823
|
+
from,
|
|
824
|
+
to,
|
|
825
|
+
type: "control",
|
|
826
|
+
subject: "interrupt",
|
|
827
|
+
body: reason,
|
|
828
|
+
priority: "high"
|
|
829
|
+
});
|
|
830
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
831
|
+
res.end(JSON.stringify({ ok: true, id: sent.id, to, delivered: entry.status }));
|
|
832
|
+
} catch (err) {
|
|
833
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
834
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
838
|
+
if (!globalRoot) {
|
|
839
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
840
|
+
res.end(JSON.stringify({ error: "SessionRegistry not available" }));
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
let body;
|
|
844
|
+
try {
|
|
845
|
+
body = await readJsonBody(req);
|
|
846
|
+
} catch {
|
|
847
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
848
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const text = typeof body["text"] === "string" ? body["text"].trim() : "";
|
|
852
|
+
if (!text) {
|
|
853
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
854
|
+
res.end(JSON.stringify({ error: "text is required" }));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const from = typeof body["from"] === "string" && body["from"].trim() ? body["from"].trim() : "human@webui";
|
|
858
|
+
try {
|
|
859
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, GlobalMailbox: GlobalMailbox3, mailboxSessionTag: mailboxSessionTag2 } = await import("@wrongstack/core");
|
|
860
|
+
const registry = new SessionRegistry(globalRoot);
|
|
861
|
+
const all = await registry.list();
|
|
862
|
+
const mySlug = all.find((s) => s.pid === process.pid)?.projectSlug;
|
|
863
|
+
const targets = all.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true);
|
|
864
|
+
if (targets.length === 0) {
|
|
865
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
866
|
+
res.end(JSON.stringify({ ok: true, delivered: 0 }));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const mbByDir = /* @__PURE__ */ new Map();
|
|
870
|
+
const mailboxFor = (projectRoot) => {
|
|
871
|
+
const dir = resolveWstackPaths2({ projectRoot, globalRoot }).projectDir;
|
|
872
|
+
let mb = mbByDir.get(dir);
|
|
873
|
+
if (!mb) {
|
|
874
|
+
mb = new GlobalMailbox3(dir);
|
|
875
|
+
mbByDir.set(dir, mb);
|
|
876
|
+
}
|
|
877
|
+
return mb;
|
|
878
|
+
};
|
|
879
|
+
let delivered = 0;
|
|
880
|
+
await Promise.all(
|
|
881
|
+
targets.map(async (s) => {
|
|
882
|
+
try {
|
|
883
|
+
const mb = mailboxFor(s.projectRoot);
|
|
884
|
+
await mb.send({
|
|
885
|
+
from,
|
|
886
|
+
to: `leader@${mailboxSessionTag2(s.sessionId)}`,
|
|
887
|
+
type: "steer",
|
|
888
|
+
subject: "Broadcast from Fleet HQ",
|
|
889
|
+
body: text,
|
|
890
|
+
priority: "high"
|
|
891
|
+
});
|
|
892
|
+
delivered++;
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
})
|
|
896
|
+
);
|
|
897
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
898
|
+
res.end(JSON.stringify({ ok: true, delivered, targets: targets.length }));
|
|
899
|
+
} catch (err) {
|
|
900
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
901
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
309
904
|
|
|
310
905
|
// src/server/file-handlers.ts
|
|
311
906
|
import * as fs2 from "fs/promises";
|
|
@@ -379,7 +974,7 @@ function broadcast(clients, msg) {
|
|
|
379
974
|
}
|
|
380
975
|
}
|
|
381
976
|
}
|
|
382
|
-
function
|
|
977
|
+
function sendResult2(ws, success, message) {
|
|
383
978
|
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
384
979
|
}
|
|
385
980
|
function errMessage(err) {
|
|
@@ -511,40 +1106,282 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
511
1106
|
payload: { files: rankFiles(results, payload.query ?? "", limit) }
|
|
512
1107
|
});
|
|
513
1108
|
}
|
|
514
|
-
|
|
515
|
-
// src/server/memory-handlers.ts
|
|
516
|
-
async function handleMemoryList(ws, memoryStore) {
|
|
1109
|
+
|
|
1110
|
+
// src/server/memory-handlers.ts
|
|
1111
|
+
async function handleMemoryList(ws, memoryStore) {
|
|
1112
|
+
try {
|
|
1113
|
+
const text = await memoryStore.readAll();
|
|
1114
|
+
send(ws, { type: "memory.list", payload: { text } });
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
send(ws, {
|
|
1117
|
+
type: "memory.list",
|
|
1118
|
+
payload: { text: "", error: errMessage(err) }
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
async function handleMemoryRemember(ws, msg, memoryStore) {
|
|
1123
|
+
const { text, scope } = msg.payload;
|
|
1124
|
+
try {
|
|
1125
|
+
await memoryStore.remember(text, scope ?? "project-memory");
|
|
1126
|
+
sendResult2(ws, true, "Saved to memory");
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
sendResult2(ws, false, errMessage(err));
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
async function handleMemoryForget(ws, msg, memoryStore) {
|
|
1132
|
+
const { text, scope } = msg.payload;
|
|
1133
|
+
try {
|
|
1134
|
+
const removed = await memoryStore.forget(text, scope ?? "project-memory");
|
|
1135
|
+
sendResult2(
|
|
1136
|
+
ws,
|
|
1137
|
+
removed > 0,
|
|
1138
|
+
removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
|
|
1139
|
+
);
|
|
1140
|
+
} catch (err) {
|
|
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}` } });
|
|
1280
|
+
}
|
|
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
|
+
}
|
|
517
1288
|
try {
|
|
518
|
-
const
|
|
519
|
-
|
|
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` } });
|
|
520
1310
|
} catch (err) {
|
|
521
|
-
send(ws, {
|
|
522
|
-
type: "memory.list",
|
|
523
|
-
payload: { text: "", error: errMessage(err) }
|
|
524
|
-
});
|
|
1311
|
+
send(ws, { type: "mcp.operation_result", payload: { success: false, message: `Failed to update server: ${err}` } });
|
|
525
1312
|
}
|
|
526
1313
|
}
|
|
527
|
-
async function
|
|
528
|
-
const
|
|
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
|
+
}
|
|
529
1324
|
try {
|
|
530
|
-
|
|
531
|
-
|
|
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` } });
|
|
532
1329
|
} catch (err) {
|
|
533
|
-
|
|
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}` } });
|
|
534
1332
|
}
|
|
535
1333
|
}
|
|
536
|
-
async function
|
|
537
|
-
const
|
|
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
|
+
}
|
|
538
1344
|
try {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
removed > 0,
|
|
543
|
-
removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
|
|
544
|
-
);
|
|
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` } });
|
|
545
1348
|
} catch (err) {
|
|
546
|
-
|
|
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;
|
|
547
1383
|
}
|
|
1384
|
+
send(ws, { type: "mcp.operation_result", payload: { success: true, message: `Restart command sent for "${payload.name}"` } });
|
|
548
1385
|
}
|
|
549
1386
|
|
|
550
1387
|
// src/server/index.ts
|
|
@@ -1956,14 +2793,14 @@ function registerShutdownHandlers(res) {
|
|
|
1956
2793
|
|
|
1957
2794
|
// src/server/instance-registry.ts
|
|
1958
2795
|
import * as os from "os";
|
|
1959
|
-
import * as
|
|
1960
|
-
import * as
|
|
2796
|
+
import * as path4 from "path";
|
|
2797
|
+
import * as fs4 from "fs/promises";
|
|
1961
2798
|
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
1962
2799
|
function defaultBaseDir() {
|
|
1963
|
-
return
|
|
2800
|
+
return path4.join(os.homedir(), ".wrongstack");
|
|
1964
2801
|
}
|
|
1965
2802
|
function registryPath(baseDir = defaultBaseDir()) {
|
|
1966
|
-
return
|
|
2803
|
+
return path4.join(baseDir, "webui-instances.json");
|
|
1967
2804
|
}
|
|
1968
2805
|
function isPidAlive(pid) {
|
|
1969
2806
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -1976,7 +2813,7 @@ function isPidAlive(pid) {
|
|
|
1976
2813
|
}
|
|
1977
2814
|
async function load(file) {
|
|
1978
2815
|
try {
|
|
1979
|
-
const raw = await
|
|
2816
|
+
const raw = await fs4.readFile(file, "utf8");
|
|
1980
2817
|
const parsed = JSON.parse(raw);
|
|
1981
2818
|
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
1982
2819
|
return parsed;
|
|
@@ -2124,15 +2961,15 @@ import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/cor
|
|
|
2124
2961
|
import { probeLocalLlm } from "@wrongstack/runtime/probe";
|
|
2125
2962
|
|
|
2126
2963
|
// src/server/provider-config-io.ts
|
|
2127
|
-
import * as
|
|
2128
|
-
import * as
|
|
2964
|
+
import * as fs5 from "fs/promises";
|
|
2965
|
+
import * as path5 from "path";
|
|
2129
2966
|
import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
|
|
2130
2967
|
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
2131
2968
|
import { DefaultSecretVault } from "@wrongstack/core";
|
|
2132
2969
|
async function loadSavedProviders(configPath, vault) {
|
|
2133
2970
|
let raw;
|
|
2134
2971
|
try {
|
|
2135
|
-
raw = await
|
|
2972
|
+
raw = await fs5.readFile(configPath, "utf8");
|
|
2136
2973
|
} catch {
|
|
2137
2974
|
return {};
|
|
2138
2975
|
}
|
|
@@ -2149,7 +2986,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
2149
2986
|
let raw;
|
|
2150
2987
|
let fileExists = true;
|
|
2151
2988
|
try {
|
|
2152
|
-
raw = await
|
|
2989
|
+
raw = await fs5.readFile(configPath, "utf8");
|
|
2153
2990
|
} catch (err) {
|
|
2154
2991
|
if (err.code !== "ENOENT") {
|
|
2155
2992
|
throw new Error(
|
|
@@ -2177,7 +3014,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
2177
3014
|
await atomicWrite3(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
2178
3015
|
}
|
|
2179
3016
|
function createProviderConfigIO(configPath) {
|
|
2180
|
-
const keyFile =
|
|
3017
|
+
const keyFile = path5.join(path5.dirname(configPath), ".key");
|
|
2181
3018
|
const vault = new DefaultSecretVault({ keyFile });
|
|
2182
3019
|
return {
|
|
2183
3020
|
load: () => loadSavedProviders(configPath, vault),
|
|
@@ -2208,7 +3045,7 @@ function writeKeysBack(cfg, keys) {
|
|
|
2208
3045
|
}
|
|
2209
3046
|
cfg.apiKeys = keys;
|
|
2210
3047
|
const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined(keys[0]);
|
|
2211
|
-
cfg.apiKey
|
|
3048
|
+
delete cfg.apiKey;
|
|
2212
3049
|
if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
|
|
2213
3050
|
cfg.activeKey = active.label;
|
|
2214
3051
|
}
|
|
@@ -2332,9 +3169,9 @@ function createProviderHandlers(deps) {
|
|
|
2332
3169
|
const providers = await loadConfigProviders();
|
|
2333
3170
|
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
2334
3171
|
if (result.ok) await saveConfigProviders(providers);
|
|
2335
|
-
|
|
3172
|
+
sendResult2(ws, result.ok, result.message);
|
|
2336
3173
|
} catch (err) {
|
|
2337
|
-
|
|
3174
|
+
sendResult2(ws, false, errMessage(err));
|
|
2338
3175
|
}
|
|
2339
3176
|
}
|
|
2340
3177
|
async function handleKeyDelete(ws, providerId, label) {
|
|
@@ -2342,9 +3179,9 @@ function createProviderHandlers(deps) {
|
|
|
2342
3179
|
const providers = await loadConfigProviders();
|
|
2343
3180
|
const result = deleteKey(providers, providerId, label);
|
|
2344
3181
|
if (result.ok) await saveConfigProviders(providers);
|
|
2345
|
-
|
|
3182
|
+
sendResult2(ws, result.ok, result.message);
|
|
2346
3183
|
} catch (err) {
|
|
2347
|
-
|
|
3184
|
+
sendResult2(ws, false, errMessage(err));
|
|
2348
3185
|
}
|
|
2349
3186
|
}
|
|
2350
3187
|
async function handleKeySetActive(ws, providerId, label) {
|
|
@@ -2352,9 +3189,9 @@ function createProviderHandlers(deps) {
|
|
|
2352
3189
|
const providers = await loadConfigProviders();
|
|
2353
3190
|
const result = setActiveKey(providers, providerId, label);
|
|
2354
3191
|
if (result.ok) await saveConfigProviders(providers);
|
|
2355
|
-
|
|
3192
|
+
sendResult2(ws, result.ok, result.message);
|
|
2356
3193
|
} catch (err) {
|
|
2357
|
-
|
|
3194
|
+
sendResult2(ws, false, errMessage(err));
|
|
2358
3195
|
}
|
|
2359
3196
|
}
|
|
2360
3197
|
async function handleProviderAdd(ws, payload) {
|
|
@@ -2362,13 +3199,13 @@ function createProviderHandlers(deps) {
|
|
|
2362
3199
|
const providers = await loadConfigProviders();
|
|
2363
3200
|
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
2364
3201
|
if (result.ok) await saveConfigProviders(providers);
|
|
2365
|
-
|
|
3202
|
+
sendResult2(ws, result.ok, result.message);
|
|
2366
3203
|
if (result.ok) {
|
|
2367
3204
|
console.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
2368
3205
|
broadcastSaved(providers);
|
|
2369
3206
|
}
|
|
2370
3207
|
} catch (err) {
|
|
2371
|
-
|
|
3208
|
+
sendResult2(ws, false, errMessage(err));
|
|
2372
3209
|
}
|
|
2373
3210
|
}
|
|
2374
3211
|
async function handleProviderRemove(ws, providerId) {
|
|
@@ -2376,9 +3213,9 @@ function createProviderHandlers(deps) {
|
|
|
2376
3213
|
const providers = await loadConfigProviders();
|
|
2377
3214
|
const result = removeProvider(providers, providerId);
|
|
2378
3215
|
if (result.ok) await saveConfigProviders(providers);
|
|
2379
|
-
|
|
3216
|
+
sendResult2(ws, result.ok, result.message);
|
|
2380
3217
|
} catch (err) {
|
|
2381
|
-
|
|
3218
|
+
sendResult2(ws, false, errMessage(err));
|
|
2382
3219
|
}
|
|
2383
3220
|
}
|
|
2384
3221
|
function broadcastSaved(providers) {
|
|
@@ -2392,15 +3229,15 @@ function createProviderHandlers(deps) {
|
|
|
2392
3229
|
const providers = await loadConfigProviders();
|
|
2393
3230
|
const cfg = providers[providerId];
|
|
2394
3231
|
if (!cfg) {
|
|
2395
|
-
|
|
3232
|
+
sendResult2(ws, false, `Unknown provider "${providerId}"`);
|
|
2396
3233
|
return;
|
|
2397
3234
|
}
|
|
2398
3235
|
delete cfg.models;
|
|
2399
3236
|
await saveConfigProviders(providers);
|
|
2400
|
-
|
|
3237
|
+
sendResult2(ws, true, `Cleared model allowlist for ${providerId}`);
|
|
2401
3238
|
broadcastSaved(providers);
|
|
2402
3239
|
} catch (err) {
|
|
2403
|
-
|
|
3240
|
+
sendResult2(ws, false, errMessage(err));
|
|
2404
3241
|
}
|
|
2405
3242
|
}
|
|
2406
3243
|
async function handleProviderUndoClear(ws, providerId, previousModels) {
|
|
@@ -2408,15 +3245,15 @@ function createProviderHandlers(deps) {
|
|
|
2408
3245
|
const providers = await loadConfigProviders();
|
|
2409
3246
|
const cfg = providers[providerId];
|
|
2410
3247
|
if (!cfg) {
|
|
2411
|
-
|
|
3248
|
+
sendResult2(ws, false, `Unknown provider "${providerId}"`);
|
|
2412
3249
|
return;
|
|
2413
3250
|
}
|
|
2414
3251
|
cfg.models = [...previousModels];
|
|
2415
3252
|
await saveConfigProviders(providers);
|
|
2416
|
-
|
|
3253
|
+
sendResult2(ws, true, `Restored ${previousModels.length} model(s) for ${providerId}`);
|
|
2417
3254
|
broadcastSaved(providers);
|
|
2418
3255
|
} catch (err) {
|
|
2419
|
-
|
|
3256
|
+
sendResult2(ws, false, errMessage(err));
|
|
2420
3257
|
}
|
|
2421
3258
|
}
|
|
2422
3259
|
async function handleProviderUpdate(ws, payload) {
|
|
@@ -2424,7 +3261,7 @@ function createProviderHandlers(deps) {
|
|
|
2424
3261
|
const providers = await loadConfigProviders();
|
|
2425
3262
|
const cfg = providers[payload.id];
|
|
2426
3263
|
if (!cfg) {
|
|
2427
|
-
|
|
3264
|
+
sendResult2(ws, false, `Unknown provider "${payload.id}"`);
|
|
2428
3265
|
return;
|
|
2429
3266
|
}
|
|
2430
3267
|
if (payload.family !== void 0) cfg.family = payload.family;
|
|
@@ -2432,10 +3269,10 @@ function createProviderHandlers(deps) {
|
|
|
2432
3269
|
if (payload.envVars !== void 0) cfg.envVars = payload.envVars;
|
|
2433
3270
|
if (payload.models !== void 0) cfg.models = payload.models;
|
|
2434
3271
|
await saveConfigProviders(providers);
|
|
2435
|
-
|
|
3272
|
+
sendResult2(ws, true, `Updated ${payload.id}`);
|
|
2436
3273
|
broadcastSaved(providers);
|
|
2437
3274
|
} catch (err) {
|
|
2438
|
-
|
|
3275
|
+
sendResult2(ws, false, errMessage(err));
|
|
2439
3276
|
}
|
|
2440
3277
|
}
|
|
2441
3278
|
async function handleProviderProbe(ws, providerId, timeoutMs) {
|
|
@@ -2480,9 +3317,12 @@ function createProviderHandlers(deps) {
|
|
|
2480
3317
|
}
|
|
2481
3318
|
|
|
2482
3319
|
// src/server/setup-events.ts
|
|
2483
|
-
import * as
|
|
3320
|
+
import * as fs6 from "fs/promises";
|
|
3321
|
+
import { watch as fsWatch } from "fs";
|
|
3322
|
+
import * as path6 from "path";
|
|
2484
3323
|
function setupEvents(deps) {
|
|
2485
|
-
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 = [];
|
|
2486
3326
|
events.on("iteration.started", (e) => {
|
|
2487
3327
|
const maxIt = typeof context.meta["maxIterations"] === "number" ? context.meta["maxIterations"] : config.tools?.maxIterations ?? 100;
|
|
2488
3328
|
broadcast2(clients, {
|
|
@@ -2513,7 +3353,11 @@ function setupEvents(deps) {
|
|
|
2513
3353
|
events.on("tool.progress", (e) => {
|
|
2514
3354
|
broadcast2(clients, {
|
|
2515
3355
|
type: "tool.progress",
|
|
2516
|
-
|
|
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 } }
|
|
2517
3361
|
});
|
|
2518
3362
|
sessionBridge?.append({
|
|
2519
3363
|
type: "tool_progress",
|
|
@@ -2679,20 +3523,165 @@ function setupEvents(deps) {
|
|
|
2679
3523
|
events.onPattern("brain.*", (eventName, payload) => {
|
|
2680
3524
|
broadcast2(clients, { type: "brain.event", payload: { event: eventName, ...payload } });
|
|
2681
3525
|
});
|
|
2682
|
-
|
|
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
|
+
}
|
|
3668
|
+
const globalRoot = globalConfigPath ? path6.dirname(globalConfigPath) : void 0;
|
|
2683
3669
|
if (globalRoot) {
|
|
2684
|
-
const
|
|
3670
|
+
const broadcastSessions = async () => {
|
|
2685
3671
|
try {
|
|
2686
3672
|
const { SessionRegistry } = await import("@wrongstack/core");
|
|
2687
3673
|
const registry = new SessionRegistry(globalRoot);
|
|
2688
3674
|
const sessions = await registry.list();
|
|
2689
|
-
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) => ({
|
|
2690
3677
|
sessionId: s.sessionId,
|
|
2691
3678
|
projectName: s.projectName,
|
|
2692
3679
|
projectSlug: s.projectSlug,
|
|
2693
3680
|
projectRoot: s.projectRoot,
|
|
2694
3681
|
workingDir: s.workingDir,
|
|
2695
3682
|
gitBranch: s.gitBranch,
|
|
3683
|
+
// Surface (tui/webui/cli) so Fleet HQ can label each live client node.
|
|
3684
|
+
clientType: s.clientType,
|
|
2696
3685
|
status: s.status,
|
|
2697
3686
|
pid: s.pid,
|
|
2698
3687
|
startedAt: s.startedAt,
|
|
@@ -2704,24 +3693,56 @@ function setupEvents(deps) {
|
|
|
2704
3693
|
currentTool: a.currentTool,
|
|
2705
3694
|
iterations: a.iterations,
|
|
2706
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,
|
|
2707
3702
|
lastActivityAt: a.lastActivityAt
|
|
2708
3703
|
}))
|
|
2709
3704
|
}));
|
|
2710
3705
|
broadcast2(clients, { type: "sessions.status_update", payload: { sessions: live } });
|
|
2711
3706
|
} catch {
|
|
2712
3707
|
}
|
|
2713
|
-
}
|
|
3708
|
+
};
|
|
3709
|
+
onFleetBroadcaster?.(broadcastSessions);
|
|
3710
|
+
const statusInterval = setInterval(() => void broadcastSessions(), 5e3);
|
|
2714
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();
|
|
2715
3728
|
}
|
|
3729
|
+
return () => {
|
|
3730
|
+
for (const dispose of disposers) {
|
|
3731
|
+
try {
|
|
3732
|
+
dispose();
|
|
3733
|
+
} catch {
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
};
|
|
2716
3737
|
}
|
|
2717
3738
|
|
|
2718
3739
|
// src/server/custom-context-modes.ts
|
|
2719
3740
|
import { listContextWindowModes, atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
2720
|
-
import * as
|
|
2721
|
-
import * as
|
|
3741
|
+
import * as fs7 from "fs/promises";
|
|
3742
|
+
import * as path7 from "path";
|
|
2722
3743
|
var STORE_FILENAME = "custom-context-modes.json";
|
|
2723
3744
|
function storePath(wrongstackDir) {
|
|
2724
|
-
return
|
|
3745
|
+
return path7.join(wrongstackDir, STORE_FILENAME);
|
|
2725
3746
|
}
|
|
2726
3747
|
var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
|
|
2727
3748
|
function createCustomModeStore(wrongstackDir) {
|
|
@@ -2729,7 +3750,7 @@ function createCustomModeStore(wrongstackDir) {
|
|
|
2729
3750
|
const load2 = async () => {
|
|
2730
3751
|
modes.clear();
|
|
2731
3752
|
try {
|
|
2732
|
-
const raw = await
|
|
3753
|
+
const raw = await fs7.readFile(storePath(wrongstackDir), "utf8");
|
|
2733
3754
|
const parsed = JSON.parse(raw);
|
|
2734
3755
|
if (Array.isArray(parsed.modes)) {
|
|
2735
3756
|
for (const m of parsed.modes) {
|
|
@@ -2905,60 +3926,346 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
|
|
|
2905
3926
|
disposed = true;
|
|
2906
3927
|
dispose();
|
|
2907
3928
|
}
|
|
2908
|
-
};
|
|
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
|
+
}
|
|
2909
4247
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
|
|
2916
|
-
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
|
+
}
|
|
2917
4253
|
try {
|
|
2918
|
-
const
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
detached: true,
|
|
2927
|
-
stdio: "ignore",
|
|
2928
|
-
windowsHide: true
|
|
2929
|
-
});
|
|
2930
|
-
child.on("error", (err) => {
|
|
2931
|
-
logger.warn(`shell.open spawn failed: ${err.message}`);
|
|
2932
|
-
onError?.();
|
|
2933
|
-
});
|
|
2934
|
-
child.unref();
|
|
2935
|
-
};
|
|
2936
|
-
if (req.target === "file-manager") {
|
|
2937
|
-
if (platform === "win32") launch("explorer", [resolved]);
|
|
2938
|
-
else if (platform === "darwin") launch("open", [resolved]);
|
|
2939
|
-
else launch("xdg-open", [resolved]);
|
|
2940
|
-
} else if (req.target === "terminal") {
|
|
2941
|
-
if (platform === "win32") {
|
|
2942
|
-
launch("cmd", ["/c", "start", "cmd", "/k", "cd", "/d", resolved]);
|
|
2943
|
-
} else if (platform === "darwin") {
|
|
2944
|
-
launch("open", ["-a", "Terminal", resolved]);
|
|
2945
|
-
} else {
|
|
2946
|
-
launch(
|
|
2947
|
-
"x-terminal-emulator",
|
|
2948
|
-
[`--working-directory=${resolved}`],
|
|
2949
|
-
() => launch(
|
|
2950
|
-
"gnome-terminal",
|
|
2951
|
-
[`--working-directory=${resolved}`],
|
|
2952
|
-
() => launch("xterm", ["-e", `cd '${resolved}' && ${process.env["SHELL"] ?? "sh"}`])
|
|
2953
|
-
)
|
|
2954
|
-
);
|
|
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 {
|
|
2955
4262
|
}
|
|
2956
|
-
} else {
|
|
2957
|
-
return { success: false, message: `Unknown shell.open target: ${String(req.target)}` };
|
|
2958
4263
|
}
|
|
2959
|
-
|
|
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 } });
|
|
2960
4267
|
} catch (err) {
|
|
2961
|
-
|
|
4268
|
+
send(ws, { type: "skills.exported", payload: { zipBase64: "", skillCount: 0, error: errMessage(err) } });
|
|
2962
4269
|
}
|
|
2963
4270
|
}
|
|
2964
4271
|
|
|
@@ -3076,15 +4383,22 @@ async function startWebUI(opts = {}) {
|
|
|
3076
4383
|
sessionId: session.id,
|
|
3077
4384
|
projectSlug: wpaths.projectSlug,
|
|
3078
4385
|
projectRoot,
|
|
3079
|
-
projectName:
|
|
4386
|
+
projectName: path10.basename(projectRoot),
|
|
3080
4387
|
workingDir,
|
|
4388
|
+
clientType: "webui",
|
|
3081
4389
|
pid: process.pid,
|
|
3082
4390
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3083
4391
|
});
|
|
3084
|
-
|
|
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() });
|
|
3085
4398
|
statusTracker.start();
|
|
3086
4399
|
const stopTracking = async () => {
|
|
3087
4400
|
try {
|
|
4401
|
+
fleetNotifier.dispose();
|
|
3088
4402
|
await registry.markClosing();
|
|
3089
4403
|
statusTracker?.stop();
|
|
3090
4404
|
} catch {
|
|
@@ -3124,6 +4438,13 @@ async function startWebUI(opts = {}) {
|
|
|
3124
4438
|
supportsReasoning: resolvedModel.capabilities.reasoning
|
|
3125
4439
|
} : void 0;
|
|
3126
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;
|
|
3127
4448
|
const systemPromptBuilder = new DefaultSystemPromptBuilder2({
|
|
3128
4449
|
memoryStore,
|
|
3129
4450
|
skillLoader,
|
|
@@ -3193,7 +4514,7 @@ async function startWebUI(opts = {}) {
|
|
|
3193
4514
|
}
|
|
3194
4515
|
} else {
|
|
3195
4516
|
throw new Error(
|
|
3196
|
-
"No provider configured. Run `wrongstack
|
|
4517
|
+
"No provider configured. Run `wrongstack auth` to set up, or configure via the WebUI."
|
|
3197
4518
|
);
|
|
3198
4519
|
}
|
|
3199
4520
|
}
|
|
@@ -3281,7 +4602,7 @@ async function startWebUI(opts = {}) {
|
|
|
3281
4602
|
const write = async () => {
|
|
3282
4603
|
let raw;
|
|
3283
4604
|
try {
|
|
3284
|
-
raw = await
|
|
4605
|
+
raw = await fs10.readFile(globalConfigPath, "utf8");
|
|
3285
4606
|
} catch {
|
|
3286
4607
|
raw = "{}";
|
|
3287
4608
|
}
|
|
@@ -3590,7 +4911,7 @@ async function startWebUI(opts = {}) {
|
|
|
3590
4911
|
inputCost,
|
|
3591
4912
|
outputCost,
|
|
3592
4913
|
cacheReadCost,
|
|
3593
|
-
projectName:
|
|
4914
|
+
projectName: path10.basename(projectRoot) || projectRoot,
|
|
3594
4915
|
projectRoot,
|
|
3595
4916
|
cwd: workingDir,
|
|
3596
4917
|
mode: modeId,
|
|
@@ -3644,10 +4965,11 @@ async function startWebUI(opts = {}) {
|
|
|
3644
4965
|
const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
|
|
3645
4966
|
const RATE_LIMIT_WINDOW_MS = 6e4;
|
|
3646
4967
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
3647
|
-
|
|
4968
|
+
let connSeq = 0;
|
|
4969
|
+
function checkRateLimit(_ws, client) {
|
|
3648
4970
|
if (RATE_LIMIT_MESSAGES <= 0) return true;
|
|
3649
4971
|
const now = Date.now();
|
|
3650
|
-
const key = client.
|
|
4972
|
+
const key = client.connId;
|
|
3651
4973
|
const limit = rateLimits.get(key);
|
|
3652
4974
|
if (!limit || now > limit.resetAt) {
|
|
3653
4975
|
rateLimits.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
|
|
@@ -3663,7 +4985,12 @@ async function startWebUI(opts = {}) {
|
|
|
3663
4985
|
);
|
|
3664
4986
|
const pendingConfirms = /* @__PURE__ */ new Map();
|
|
3665
4987
|
const handleConnection = (ws) => {
|
|
3666
|
-
const client = {
|
|
4988
|
+
const client = {
|
|
4989
|
+
ws,
|
|
4990
|
+
sessionId: session.id,
|
|
4991
|
+
connectedAt: Date.now(),
|
|
4992
|
+
connId: `c${++connSeq}`
|
|
4993
|
+
};
|
|
3667
4994
|
clients.set(ws, client);
|
|
3668
4995
|
void sessionStartPayload().then((payload) => {
|
|
3669
4996
|
send(ws, { type: "session.start", payload });
|
|
@@ -3693,7 +5020,7 @@ async function startWebUI(opts = {}) {
|
|
|
3693
5020
|
const rawObj = JSON.parse(data.toString());
|
|
3694
5021
|
if (typeof rawObj === "object" && rawObj !== null) {
|
|
3695
5022
|
const obj = rawObj;
|
|
3696
|
-
if ("__proto__"
|
|
5023
|
+
if (Object.hasOwn(obj, "__proto__") || Object.hasOwn(obj, "constructor") || Object.hasOwn(obj, "prototype")) {
|
|
3697
5024
|
send(ws, {
|
|
3698
5025
|
type: "error",
|
|
3699
5026
|
payload: { phase: "parse", message: "Invalid message object" }
|
|
@@ -3714,8 +5041,9 @@ async function startWebUI(opts = {}) {
|
|
|
3714
5041
|
}
|
|
3715
5042
|
});
|
|
3716
5043
|
ws.on("close", () => {
|
|
5044
|
+
const closing = clients.get(ws);
|
|
3717
5045
|
clients.delete(ws);
|
|
3718
|
-
rateLimits.delete(
|
|
5046
|
+
if (closing) rateLimits.delete(closing.connId);
|
|
3719
5047
|
if (pendingConfirms.size > 0) {
|
|
3720
5048
|
for (const [id, resolve5] of pendingConfirms) {
|
|
3721
5049
|
resolve5("no");
|
|
@@ -3741,11 +5069,27 @@ async function startWebUI(opts = {}) {
|
|
|
3741
5069
|
{ sampling: sessionLogging.sampling }
|
|
3742
5070
|
);
|
|
3743
5071
|
let eventsArmed = false;
|
|
5072
|
+
let disposeEvents = null;
|
|
5073
|
+
let fleetBroadcast = null;
|
|
3744
5074
|
const armOnce = (label) => {
|
|
3745
5075
|
if (eventsArmed) return;
|
|
3746
5076
|
eventsArmed = true;
|
|
3747
5077
|
console.log(`[WebUI] Backend ready (${label})`);
|
|
3748
|
-
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
|
+
});
|
|
3749
5093
|
};
|
|
3750
5094
|
wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
|
|
3751
5095
|
wssPrimary.on("connection", handleConnection);
|
|
@@ -3782,33 +5126,33 @@ async function startWebUI(opts = {}) {
|
|
|
3782
5126
|
});
|
|
3783
5127
|
}
|
|
3784
5128
|
async function touchProjectEntry(root, workDir) {
|
|
3785
|
-
const resolved =
|
|
5129
|
+
const resolved = path10.resolve(root);
|
|
3786
5130
|
const manifest = await loadManifest(globalConfigPath);
|
|
3787
5131
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3788
|
-
const existing = manifest.projects.find((p) =>
|
|
5132
|
+
const existing = manifest.projects.find((p) => path10.resolve(p.root) === resolved);
|
|
3789
5133
|
if (existing) {
|
|
3790
5134
|
existing.lastSeen = now;
|
|
3791
|
-
if (workDir) existing.lastWorkingDir =
|
|
5135
|
+
if (workDir) existing.lastWorkingDir = path10.resolve(workDir);
|
|
3792
5136
|
} else {
|
|
3793
5137
|
manifest.projects.push({
|
|
3794
|
-
name:
|
|
5138
|
+
name: path10.basename(resolved),
|
|
3795
5139
|
root: resolved,
|
|
3796
5140
|
slug: generateProjectSlug(resolved),
|
|
3797
5141
|
createdAt: now,
|
|
3798
5142
|
lastSeen: now,
|
|
3799
|
-
lastWorkingDir: workDir ?
|
|
5143
|
+
lastWorkingDir: workDir ? path10.resolve(workDir) : void 0
|
|
3800
5144
|
});
|
|
3801
5145
|
}
|
|
3802
5146
|
await saveManifest(manifest, globalConfigPath);
|
|
3803
5147
|
await ensureProjectDataDir(generateProjectSlug(resolved), globalConfigPath);
|
|
3804
5148
|
}
|
|
3805
5149
|
function projectsJsonPath(globalConfigPath2) {
|
|
3806
|
-
const base =
|
|
3807
|
-
return
|
|
5150
|
+
const base = path10.dirname(globalConfigPath2);
|
|
5151
|
+
return path10.join(base, "projects.json");
|
|
3808
5152
|
}
|
|
3809
5153
|
async function loadManifest(globalConfigPath2) {
|
|
3810
5154
|
try {
|
|
3811
|
-
const raw = await
|
|
5155
|
+
const raw = await fs10.readFile(projectsJsonPath(globalConfigPath2), "utf8");
|
|
3812
5156
|
const parsed = JSON.parse(raw);
|
|
3813
5157
|
return { projects: parsed.projects ?? [] };
|
|
3814
5158
|
} catch {
|
|
@@ -3817,16 +5161,16 @@ async function startWebUI(opts = {}) {
|
|
|
3817
5161
|
}
|
|
3818
5162
|
async function saveManifest(manifest, globalConfigPath2) {
|
|
3819
5163
|
const file = projectsJsonPath(globalConfigPath2);
|
|
3820
|
-
await
|
|
3821
|
-
await
|
|
5164
|
+
await fs10.mkdir(path10.dirname(file), { recursive: true });
|
|
5165
|
+
await fs10.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
3822
5166
|
}
|
|
3823
5167
|
function generateProjectSlug(rootPath) {
|
|
3824
5168
|
return projectSlug(rootPath);
|
|
3825
5169
|
}
|
|
3826
5170
|
async function ensureProjectDataDir(slug, globalConfigPath2) {
|
|
3827
|
-
const base =
|
|
3828
|
-
const dir =
|
|
3829
|
-
await
|
|
5171
|
+
const base = path10.dirname(globalConfigPath2);
|
|
5172
|
+
const dir = path10.join(base, "projects", slug);
|
|
5173
|
+
await fs10.mkdir(dir, { recursive: true });
|
|
3830
5174
|
return dir;
|
|
3831
5175
|
}
|
|
3832
5176
|
async function handleMessage(ws, _client, msg) {
|
|
@@ -3936,7 +5280,7 @@ async function startWebUI(opts = {}) {
|
|
|
3936
5280
|
context.readFiles.clear();
|
|
3937
5281
|
context.fileMtimes.clear();
|
|
3938
5282
|
tokenCounter.reset();
|
|
3939
|
-
|
|
5283
|
+
sendResult2(ws, true, "Context cleared");
|
|
3940
5284
|
broadcast(clients, {
|
|
3941
5285
|
type: "session.start",
|
|
3942
5286
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
@@ -3973,13 +5317,13 @@ async function startWebUI(opts = {}) {
|
|
|
3973
5317
|
repaired: report.repaired
|
|
3974
5318
|
}
|
|
3975
5319
|
});
|
|
3976
|
-
|
|
5320
|
+
sendResult2(
|
|
3977
5321
|
ws,
|
|
3978
5322
|
true,
|
|
3979
5323
|
`Compacted: ${report.before} \u2192 ${report.after} tokens (saved ~${Math.max(0, report.before - report.after)})`
|
|
3980
5324
|
);
|
|
3981
5325
|
} catch (err) {
|
|
3982
|
-
|
|
5326
|
+
sendResult2(ws, false, errMessage(err));
|
|
3983
5327
|
}
|
|
3984
5328
|
break;
|
|
3985
5329
|
}
|
|
@@ -3998,7 +5342,7 @@ async function startWebUI(opts = {}) {
|
|
|
3998
5342
|
};
|
|
3999
5343
|
broadcast(clients, { type: "context.repaired", payload });
|
|
4000
5344
|
const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
|
|
4001
|
-
|
|
5345
|
+
sendResult2(
|
|
4002
5346
|
ws,
|
|
4003
5347
|
true,
|
|
4004
5348
|
removed > 0 ? `Context repaired: removed ${removed} orphan protocol item(s)` : "Context repair found no orphan protocol blocks"
|
|
@@ -4032,14 +5376,14 @@ async function startWebUI(opts = {}) {
|
|
|
4032
5376
|
);
|
|
4033
5377
|
const custom = customModes.find((m) => m.id === id);
|
|
4034
5378
|
if (!custom) {
|
|
4035
|
-
|
|
5379
|
+
sendResult2(ws, false, `Unknown context mode "${id}"`);
|
|
4036
5380
|
break;
|
|
4037
5381
|
}
|
|
4038
5382
|
policy = custom;
|
|
4039
5383
|
}
|
|
4040
5384
|
context.meta["contextWindowMode"] = policy.id;
|
|
4041
5385
|
context.meta["contextWindowPolicy"] = policy;
|
|
4042
|
-
|
|
5386
|
+
sendResult2(ws, true, `Context mode switched to ${policy.id}`);
|
|
4043
5387
|
broadcast(clients, {
|
|
4044
5388
|
type: "context.mode.changed",
|
|
4045
5389
|
payload: { id: policy.id, name: policy.name, policy }
|
|
@@ -4059,7 +5403,7 @@ async function startWebUI(opts = {}) {
|
|
|
4059
5403
|
aggressiveOn: "soft",
|
|
4060
5404
|
targetLoad: 0.65
|
|
4061
5405
|
});
|
|
4062
|
-
|
|
5406
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${payload.id}" created`);
|
|
4063
5407
|
break;
|
|
4064
5408
|
}
|
|
4065
5409
|
case "context.mode.update": {
|
|
@@ -4075,7 +5419,7 @@ async function startWebUI(opts = {}) {
|
|
|
4075
5419
|
preserveK: payload.preserveK,
|
|
4076
5420
|
eliseThreshold: payload.eliseThreshold
|
|
4077
5421
|
});
|
|
4078
|
-
|
|
5422
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${payload.id}" updated`);
|
|
4079
5423
|
break;
|
|
4080
5424
|
}
|
|
4081
5425
|
case "context.mode.delete": {
|
|
@@ -4085,7 +5429,7 @@ async function startWebUI(opts = {}) {
|
|
|
4085
5429
|
context.meta["contextWindowPolicy"] = resolveContextWindowPolicy({}, DEFAULT_CONTEXT_WINDOW_MODE_ID);
|
|
4086
5430
|
}
|
|
4087
5431
|
const result = customModeStore.remove(id);
|
|
4088
|
-
|
|
5432
|
+
sendResult2(ws, result.ok, result.error ?? `Mode "${id}" deleted`);
|
|
4089
5433
|
break;
|
|
4090
5434
|
}
|
|
4091
5435
|
case "providers.list": {
|
|
@@ -4166,14 +5510,15 @@ async function startWebUI(opts = {}) {
|
|
|
4166
5510
|
context.provider = newProv;
|
|
4167
5511
|
updateAutoCompactionMaxContext?.(newProv);
|
|
4168
5512
|
try {
|
|
4169
|
-
|
|
4170
|
-
const raw = await
|
|
5513
|
+
const next = configWriteLock.then(async () => {
|
|
5514
|
+
const raw = await fs10.readFile(globalConfigPath, "utf8");
|
|
4171
5515
|
const parsed = JSON.parse(raw);
|
|
4172
5516
|
parsed.provider = newProvider;
|
|
4173
5517
|
parsed.model = newModel;
|
|
4174
5518
|
await atomicWrite5(globalConfigPath, JSON.stringify(parsed, null, 2));
|
|
4175
5519
|
});
|
|
4176
|
-
|
|
5520
|
+
configWriteLock = next.then(() => void 0, () => void 0);
|
|
5521
|
+
await next;
|
|
4177
5522
|
} catch (err) {
|
|
4178
5523
|
console.warn(JSON.stringify({
|
|
4179
5524
|
level: "warn",
|
|
@@ -4326,13 +5671,13 @@ async function startWebUI(opts = {}) {
|
|
|
4326
5671
|
const { id } = msg.payload;
|
|
4327
5672
|
try {
|
|
4328
5673
|
if (id === session.id) {
|
|
4329
|
-
|
|
5674
|
+
sendResult2(ws, false, "Cannot delete the active session");
|
|
4330
5675
|
break;
|
|
4331
5676
|
}
|
|
4332
5677
|
await sessionStore.delete(id);
|
|
4333
|
-
|
|
5678
|
+
sendResult2(ws, true, `Session ${id} deleted`);
|
|
4334
5679
|
} catch (err) {
|
|
4335
|
-
|
|
5680
|
+
sendResult2(ws, false, errMessage(err));
|
|
4336
5681
|
}
|
|
4337
5682
|
break;
|
|
4338
5683
|
}
|
|
@@ -4340,7 +5685,7 @@ async function startWebUI(opts = {}) {
|
|
|
4340
5685
|
const { id } = msg.payload;
|
|
4341
5686
|
try {
|
|
4342
5687
|
if (id === session.id) {
|
|
4343
|
-
|
|
5688
|
+
sendResult2(ws, false, "Session is already active");
|
|
4344
5689
|
break;
|
|
4345
5690
|
}
|
|
4346
5691
|
const resumed = await sessionStore.resume(id);
|
|
@@ -4370,14 +5715,14 @@ async function startWebUI(opts = {}) {
|
|
|
4370
5715
|
replayUsage: resumed.data.usage
|
|
4371
5716
|
}
|
|
4372
5717
|
});
|
|
4373
|
-
|
|
5718
|
+
sendResult2(ws, true, `Resumed session ${id}`);
|
|
4374
5719
|
} catch (err) {
|
|
4375
|
-
|
|
5720
|
+
sendResult2(ws, false, errMessage(err));
|
|
4376
5721
|
}
|
|
4377
5722
|
break;
|
|
4378
5723
|
}
|
|
4379
5724
|
case "session.save": {
|
|
4380
|
-
|
|
5725
|
+
sendResult2(ws, true, `Session ${session.id} is auto-saved`);
|
|
4381
5726
|
break;
|
|
4382
5727
|
}
|
|
4383
5728
|
case "tools.list": {
|
|
@@ -4400,6 +5745,27 @@ async function startWebUI(opts = {}) {
|
|
|
4400
5745
|
return handleMemoryRemember(ws, msg, memoryStore);
|
|
4401
5746
|
case "memory.forget":
|
|
4402
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);
|
|
4403
5769
|
case "skills.list": {
|
|
4404
5770
|
if (!skillLoader) {
|
|
4405
5771
|
send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
|
|
@@ -4409,6 +5775,18 @@ async function startWebUI(opts = {}) {
|
|
|
4409
5775
|
const manifests = await skillLoader.list();
|
|
4410
5776
|
const entries = await skillLoader.listEntries();
|
|
4411
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
|
+
}
|
|
4412
5790
|
send(ws, {
|
|
4413
5791
|
type: "skills.list",
|
|
4414
5792
|
payload: {
|
|
@@ -4418,6 +5796,8 @@ async function startWebUI(opts = {}) {
|
|
|
4418
5796
|
description: m.description,
|
|
4419
5797
|
version: m.version ?? "",
|
|
4420
5798
|
source: m.source,
|
|
5799
|
+
sourceUrl: sourceUrlsByName.get(m.name) ?? "",
|
|
5800
|
+
ref: refsByName.get(m.name) ?? "",
|
|
4421
5801
|
path: m.path,
|
|
4422
5802
|
trigger: byName.get(m.name)?.trigger ?? "",
|
|
4423
5803
|
scope: byName.get(m.name)?.scope ?? []
|
|
@@ -4436,6 +5816,261 @@ async function startWebUI(opts = {}) {
|
|
|
4436
5816
|
}
|
|
4437
5817
|
break;
|
|
4438
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
|
+
}
|
|
4439
6074
|
case "diag.get": {
|
|
4440
6075
|
const usage = tokenCounter.total();
|
|
4441
6076
|
send(ws, {
|
|
@@ -4463,194 +6098,84 @@ async function startWebUI(opts = {}) {
|
|
|
4463
6098
|
break;
|
|
4464
6099
|
}
|
|
4465
6100
|
case "todos.get": {
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
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);
|
|
4470
6107
|
break;
|
|
4471
6108
|
}
|
|
4472
6109
|
case "todos.clear": {
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
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);
|
|
4476
6116
|
break;
|
|
4477
6117
|
}
|
|
4478
6118
|
case "todos.remove": {
|
|
4479
|
-
const
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
}
|
|
4484
|
-
|
|
4485
|
-
let targetIdx = -1;
|
|
4486
|
-
if (typeof id === "string") {
|
|
4487
|
-
targetIdx = context.todos.findIndex((t) => t.id === id);
|
|
4488
|
-
} else if (typeof index === "number" && index > 0) {
|
|
4489
|
-
targetIdx = index - 1;
|
|
4490
|
-
}
|
|
4491
|
-
if (targetIdx < 0 || !context.todos[targetIdx]) {
|
|
4492
|
-
sendResult(ws, false, "Todo not found");
|
|
4493
|
-
break;
|
|
4494
|
-
}
|
|
4495
|
-
const removed = expectDefined2(context.todos[targetIdx]);
|
|
4496
|
-
const next = [...context.todos.slice(0, targetIdx), ...context.todos.slice(targetIdx + 1)];
|
|
4497
|
-
context.state.replaceTodos(next);
|
|
4498
|
-
sendResult(ws, true, `Removed: ${removed.content}`);
|
|
4499
|
-
broadcast(clients, { type: "todos.updated", payload: { todos: next } });
|
|
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);
|
|
4500
6125
|
break;
|
|
4501
6126
|
}
|
|
4502
6127
|
case "tasks.get": {
|
|
4503
|
-
const
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
type: "tasks.updated",
|
|
4510
|
-
payload: { tasks: file?.tasks ?? [] }
|
|
4511
|
-
});
|
|
4512
|
-
} catch {
|
|
4513
|
-
send(ws, { type: "tasks.updated", payload: { tasks: [] } });
|
|
4514
|
-
}
|
|
4515
|
-
} else {
|
|
4516
|
-
send(ws, { type: "tasks.updated", payload: { tasks: [], error: "Task storage not configured." } });
|
|
4517
|
-
}
|
|
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);
|
|
4518
6134
|
break;
|
|
4519
6135
|
}
|
|
4520
6136
|
case "plan.get": {
|
|
4521
|
-
const
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
type: "plan.updated",
|
|
4528
|
-
payload: {
|
|
4529
|
-
plan: plan ?? {
|
|
4530
|
-
version: 1,
|
|
4531
|
-
sessionId: session.id,
|
|
4532
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4533
|
-
items: []
|
|
4534
|
-
}
|
|
4535
|
-
}
|
|
4536
|
-
});
|
|
4537
|
-
} catch {
|
|
4538
|
-
send(ws, {
|
|
4539
|
-
type: "plan.updated",
|
|
4540
|
-
payload: {
|
|
4541
|
-
plan: {
|
|
4542
|
-
version: 1,
|
|
4543
|
-
sessionId: session.id,
|
|
4544
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4545
|
-
items: []
|
|
4546
|
-
}
|
|
4547
|
-
}
|
|
4548
|
-
});
|
|
4549
|
-
}
|
|
4550
|
-
} else {
|
|
4551
|
-
send(ws, {
|
|
4552
|
-
type: "plan.updated",
|
|
4553
|
-
payload: { plan: null, error: "Plan storage is not configured for this session." }
|
|
4554
|
-
});
|
|
4555
|
-
}
|
|
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);
|
|
4556
6143
|
break;
|
|
4557
6144
|
}
|
|
4558
6145
|
case "plan.template_use": {
|
|
4559
|
-
const
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
try {
|
|
4566
|
-
const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem } = await import("@wrongstack/core");
|
|
4567
|
-
const tpl = getPlanTemplate(template);
|
|
4568
|
-
if (!tpl) {
|
|
4569
|
-
sendResult(ws, false, `Unknown template "${template}".`);
|
|
4570
|
-
break;
|
|
4571
|
-
}
|
|
4572
|
-
let plan = await loadPlan(planPath) ?? emptyPlan(session.id);
|
|
4573
|
-
for (const item of tpl.items) {
|
|
4574
|
-
({ plan } = addPlanItem(plan, item.title, item.details));
|
|
4575
|
-
}
|
|
4576
|
-
await savePlan(planPath, plan);
|
|
4577
|
-
sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
|
|
4578
|
-
broadcast(clients, {
|
|
4579
|
-
type: "plan.updated",
|
|
4580
|
-
payload: { plan }
|
|
4581
|
-
});
|
|
4582
|
-
} catch (err) {
|
|
4583
|
-
sendResult(ws, false, errMessage(err));
|
|
4584
|
-
}
|
|
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);
|
|
4585
6152
|
break;
|
|
4586
6153
|
}
|
|
4587
6154
|
case "todo.update": {
|
|
4588
|
-
const
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
break;
|
|
4593
|
-
}
|
|
4594
|
-
const next = [...context.todos];
|
|
4595
|
-
const existing = expectDefined2(next[idx]);
|
|
4596
|
-
next[idx] = {
|
|
4597
|
-
...existing,
|
|
4598
|
-
status: payload.status ?? existing.status,
|
|
4599
|
-
activeForm: payload.activeForm !== void 0 ? payload.activeForm : existing.activeForm
|
|
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)
|
|
4600
6159
|
};
|
|
4601
|
-
|
|
4602
|
-
sendResult(ws, true, `Todo "${existing.content}" updated`);
|
|
4603
|
-
broadcast(clients, { type: "todos.updated", payload: { todos: next } });
|
|
6160
|
+
handleTodoUpdate(ctx, ws, msg.payload);
|
|
4604
6161
|
break;
|
|
4605
6162
|
}
|
|
4606
6163
|
case "task.update": {
|
|
4607
|
-
const
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
try {
|
|
4614
|
-
const { mutateTasks } = await import("@wrongstack/core");
|
|
4615
|
-
const file = await mutateTasks(taskPath, session.id, async (f) => {
|
|
4616
|
-
const task = f.tasks.find((t) => t.id === payload.id);
|
|
4617
|
-
if (!task) return f;
|
|
4618
|
-
task.status = payload.status;
|
|
4619
|
-
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4620
|
-
return f;
|
|
4621
|
-
});
|
|
4622
|
-
sendResult(ws, true, `Task status updated to "${payload.status}".`);
|
|
4623
|
-
broadcast(clients, { type: "tasks.updated", payload: { tasks: file.tasks } });
|
|
4624
|
-
} catch (err) {
|
|
4625
|
-
sendResult(ws, false, errMessage(err));
|
|
4626
|
-
}
|
|
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);
|
|
4627
6170
|
break;
|
|
4628
6171
|
}
|
|
4629
6172
|
case "plan.item.update": {
|
|
4630
|
-
const
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
try {
|
|
4637
|
-
const { mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
|
|
4638
|
-
let changed = false;
|
|
4639
|
-
const plan = await mutatePlan(planPath, session.id, async (p) => {
|
|
4640
|
-
const before = p.updatedAt;
|
|
4641
|
-
const updated = setPlanItemStatus(p, payload.target, payload.status);
|
|
4642
|
-
changed = updated.updatedAt !== before;
|
|
4643
|
-
return updated;
|
|
4644
|
-
});
|
|
4645
|
-
if (!changed) {
|
|
4646
|
-
sendResult(ws, false, `No plan item matched "${payload.target}".`);
|
|
4647
|
-
break;
|
|
4648
|
-
}
|
|
4649
|
-
sendResult(ws, true, `Plan item status updated to "${payload.status}".`);
|
|
4650
|
-
broadcast(clients, { type: "plan.updated", payload: { plan } });
|
|
4651
|
-
} catch (err) {
|
|
4652
|
-
sendResult(ws, false, errMessage(err));
|
|
4653
|
-
}
|
|
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);
|
|
4654
6179
|
break;
|
|
4655
6180
|
}
|
|
4656
6181
|
// ── File operations — delegated to shared handlers (file-handlers.ts) ──
|
|
@@ -4720,13 +6245,13 @@ async function startWebUI(opts = {}) {
|
|
|
4720
6245
|
provider: config.provider,
|
|
4721
6246
|
model: config.model
|
|
4722
6247
|
});
|
|
4723
|
-
|
|
6248
|
+
sendResult2(ws, true, `Switched to mode "${id}"`);
|
|
4724
6249
|
broadcast(clients, {
|
|
4725
6250
|
type: "session.start",
|
|
4726
6251
|
payload: { ...await sessionStartPayload() }
|
|
4727
6252
|
});
|
|
4728
6253
|
} catch (err) {
|
|
4729
|
-
|
|
6254
|
+
sendResult2(ws, false, errMessage(err));
|
|
4730
6255
|
}
|
|
4731
6256
|
break;
|
|
4732
6257
|
}
|
|
@@ -4780,13 +6305,13 @@ async function startWebUI(opts = {}) {
|
|
|
4780
6305
|
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
4781
6306
|
const proc = getProcessRegistry().get(pid);
|
|
4782
6307
|
if (proc?.protected) {
|
|
4783
|
-
|
|
6308
|
+
sendResult2(ws, false, `Cannot kill protected process (PID ${pid})`);
|
|
4784
6309
|
break;
|
|
4785
6310
|
}
|
|
4786
6311
|
getProcessRegistry().kill(pid);
|
|
4787
|
-
|
|
6312
|
+
sendResult2(ws, true, `Killed PID ${pid}`);
|
|
4788
6313
|
} catch (err) {
|
|
4789
|
-
|
|
6314
|
+
sendResult2(ws, false, errMessage(err));
|
|
4790
6315
|
}
|
|
4791
6316
|
break;
|
|
4792
6317
|
}
|
|
@@ -4794,47 +6319,25 @@ async function startWebUI(opts = {}) {
|
|
|
4794
6319
|
try {
|
|
4795
6320
|
const { getProcessRegistry } = await import("@wrongstack/tools");
|
|
4796
6321
|
getProcessRegistry().killAll();
|
|
4797
|
-
|
|
6322
|
+
sendResult2(ws, true, "All processes killed");
|
|
4798
6323
|
} catch (err) {
|
|
4799
|
-
|
|
6324
|
+
sendResult2(ws, false, errMessage(err));
|
|
4800
6325
|
}
|
|
4801
6326
|
break;
|
|
4802
6327
|
}
|
|
4803
6328
|
case "git.info": {
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
});
|
|
4811
|
-
});
|
|
4812
|
-
const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
|
|
4813
|
-
execFile("git", ["branch", "--show-current"]),
|
|
4814
|
-
execFile("git", ["diff", "--stat"]),
|
|
4815
|
-
execFile("git", ["status", "--porcelain"]),
|
|
4816
|
-
execFile("git", ["rev-list", "--left-right", "--count", "@{upstream}...HEAD"])
|
|
4817
|
-
]);
|
|
4818
|
-
const branch = branchRaw || "(detached)";
|
|
4819
|
-
const diffMatch = /\+\s*(\d+)\s*deletion/i.exec(diffRaw);
|
|
4820
|
-
const addMatch = /(\d+)\s*insertion/i.exec(diffRaw) ?? /(\d+)\s*addition/i.exec(diffRaw);
|
|
4821
|
-
const delMatch = /\+\s*(\d+)\s*deletion/i.exec(diffRaw);
|
|
4822
|
-
const added = addMatch ? Number(addMatch[1]) : 0;
|
|
4823
|
-
const deleted = delMatch ? Number(delMatch[1]) : 0;
|
|
4824
|
-
const untracked = statusRaw.split("\n").filter((l) => l.startsWith("??")).length;
|
|
4825
|
-
const [aheadRaw, behindRaw] = (upstreamRaw || "0 0").split(" ");
|
|
4826
|
-
const ahead = Number(aheadRaw) || 0;
|
|
4827
|
-
const behind = Number(behindRaw) || 0;
|
|
4828
|
-
send(ws, {
|
|
4829
|
-
type: "git.info",
|
|
4830
|
-
payload: { branch, added, deleted, untracked, ahead, behind }
|
|
4831
|
-
});
|
|
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");
|
|
4832
6335
|
break;
|
|
4833
6336
|
}
|
|
4834
6337
|
case "goal.get": {
|
|
4835
6338
|
try {
|
|
4836
|
-
const goalPath =
|
|
4837
|
-
const raw = await
|
|
6339
|
+
const goalPath = resolveWstackPaths({ projectRoot }).projectGoal;
|
|
6340
|
+
const raw = await fs10.readFile(goalPath, "utf8");
|
|
4838
6341
|
const goal = JSON.parse(raw);
|
|
4839
6342
|
broadcast(clients, { type: "goal.updated", payload: goal });
|
|
4840
6343
|
} catch {
|
|
@@ -4845,7 +6348,7 @@ async function startWebUI(opts = {}) {
|
|
|
4845
6348
|
case "autonomy.switch": {
|
|
4846
6349
|
const { mode } = msg.payload;
|
|
4847
6350
|
context.meta["autonomy"] = mode;
|
|
4848
|
-
|
|
6351
|
+
sendResult2(ws, true, `Autonomy mode set to "${mode}"`);
|
|
4849
6352
|
broadcast(clients, { type: "prefs.updated", payload: { autonomy: mode } });
|
|
4850
6353
|
void persistPrefsToConfig({ autonomy: mode });
|
|
4851
6354
|
break;
|
|
@@ -4894,7 +6397,7 @@ async function startWebUI(opts = {}) {
|
|
|
4894
6397
|
try {
|
|
4895
6398
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4896
6399
|
const rewinder = new DefaultSessionRewinder(
|
|
4897
|
-
|
|
6400
|
+
path10.join(projectRoot, ".wrongstack", "sessions"),
|
|
4898
6401
|
projectRoot
|
|
4899
6402
|
);
|
|
4900
6403
|
const checkpoints = await rewinder.listCheckpoints(session.id);
|
|
@@ -4915,18 +6418,18 @@ async function startWebUI(opts = {}) {
|
|
|
4915
6418
|
try {
|
|
4916
6419
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
4917
6420
|
const rewinder = new DefaultSessionRewinder(
|
|
4918
|
-
|
|
6421
|
+
path10.join(projectRoot, ".wrongstack", "sessions"),
|
|
4919
6422
|
projectRoot
|
|
4920
6423
|
);
|
|
4921
6424
|
await rewinder.rewindToCheckpoint(session.id, checkpointIndex);
|
|
4922
6425
|
await context.session.truncateToCheckpoint(checkpointIndex);
|
|
4923
|
-
|
|
6426
|
+
sendResult2(ws, true, `Rewound to checkpoint ${checkpointIndex}`);
|
|
4924
6427
|
broadcast(clients, {
|
|
4925
6428
|
type: "session.start",
|
|
4926
6429
|
payload: { ...await sessionStartPayload(), reset: true }
|
|
4927
6430
|
});
|
|
4928
6431
|
} catch (err) {
|
|
4929
|
-
|
|
6432
|
+
sendResult2(ws, false, errMessage(err));
|
|
4930
6433
|
}
|
|
4931
6434
|
break;
|
|
4932
6435
|
}
|
|
@@ -4949,9 +6452,9 @@ async function startWebUI(opts = {}) {
|
|
|
4949
6452
|
case "projects.add": {
|
|
4950
6453
|
const { root: addRoot, name: displayName } = msg.payload;
|
|
4951
6454
|
try {
|
|
4952
|
-
const resolved =
|
|
4953
|
-
await
|
|
4954
|
-
const stat2 = await
|
|
6455
|
+
const resolved = path10.resolve(addRoot);
|
|
6456
|
+
await fs10.access(resolved);
|
|
6457
|
+
const stat2 = await fs10.stat(resolved);
|
|
4955
6458
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
4956
6459
|
const manifest = await loadManifest(globalConfigPath);
|
|
4957
6460
|
const existing = manifest.projects.find((p) => p.root === resolved);
|
|
@@ -4967,7 +6470,7 @@ async function startWebUI(opts = {}) {
|
|
|
4967
6470
|
});
|
|
4968
6471
|
break;
|
|
4969
6472
|
}
|
|
4970
|
-
const name = displayName?.trim() ||
|
|
6473
|
+
const name = displayName?.trim() || path10.basename(resolved);
|
|
4971
6474
|
const slug = generateProjectSlug(resolved);
|
|
4972
6475
|
await ensureProjectDataDir(slug, globalConfigPath);
|
|
4973
6476
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -4986,7 +6489,7 @@ async function startWebUI(opts = {}) {
|
|
|
4986
6489
|
send(ws, {
|
|
4987
6490
|
type: "projects.added",
|
|
4988
6491
|
payload: {
|
|
4989
|
-
name:
|
|
6492
|
+
name: path10.basename(addRoot),
|
|
4990
6493
|
root: addRoot,
|
|
4991
6494
|
slug: "",
|
|
4992
6495
|
message: errMessage(err)
|
|
@@ -4998,17 +6501,17 @@ async function startWebUI(opts = {}) {
|
|
|
4998
6501
|
case "projects.select": {
|
|
4999
6502
|
const { root: selRoot, name: selName } = msg.payload;
|
|
5000
6503
|
try {
|
|
5001
|
-
const resolved =
|
|
6504
|
+
const resolved = path10.resolve(selRoot);
|
|
5002
6505
|
try {
|
|
5003
|
-
await
|
|
5004
|
-
const stat2 = await
|
|
6506
|
+
await fs10.access(resolved);
|
|
6507
|
+
const stat2 = await fs10.stat(resolved);
|
|
5005
6508
|
if (!stat2.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
5006
6509
|
} catch (err) {
|
|
5007
6510
|
send(ws, {
|
|
5008
6511
|
type: "projects.selected",
|
|
5009
6512
|
payload: {
|
|
5010
6513
|
root: selRoot,
|
|
5011
|
-
name: selName ||
|
|
6514
|
+
name: selName || path10.basename(selRoot),
|
|
5012
6515
|
message: `Cannot switch: ${errMessage(err)}`
|
|
5013
6516
|
}
|
|
5014
6517
|
});
|
|
@@ -5020,7 +6523,7 @@ async function startWebUI(opts = {}) {
|
|
|
5020
6523
|
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
5021
6524
|
entry.lastWorkingDir = resolved;
|
|
5022
6525
|
} else {
|
|
5023
|
-
const name = selName?.trim() ||
|
|
6526
|
+
const name = selName?.trim() || path10.basename(resolved);
|
|
5024
6527
|
const slug = generateProjectSlug(resolved);
|
|
5025
6528
|
manifest.projects.push({
|
|
5026
6529
|
name,
|
|
@@ -5061,13 +6564,13 @@ async function startWebUI(opts = {}) {
|
|
|
5061
6564
|
});
|
|
5062
6565
|
} catch {
|
|
5063
6566
|
}
|
|
5064
|
-
const newSessionsDir =
|
|
5065
|
-
|
|
6567
|
+
const newSessionsDir = path10.join(
|
|
6568
|
+
path10.dirname(globalConfigPath),
|
|
5066
6569
|
"projects",
|
|
5067
6570
|
switchSlug,
|
|
5068
6571
|
"sessions"
|
|
5069
6572
|
);
|
|
5070
|
-
await
|
|
6573
|
+
await fs10.mkdir(newSessionsDir, { recursive: true });
|
|
5071
6574
|
const newSessionStore = new DefaultSessionStore2({ dir: newSessionsDir });
|
|
5072
6575
|
const oldSessionId = session.id;
|
|
5073
6576
|
try {
|
|
@@ -5099,8 +6602,9 @@ async function startWebUI(opts = {}) {
|
|
|
5099
6602
|
sessionId: session.id,
|
|
5100
6603
|
projectSlug: switchSlug,
|
|
5101
6604
|
projectRoot,
|
|
5102
|
-
projectName:
|
|
6605
|
+
projectName: path10.basename(projectRoot),
|
|
5103
6606
|
workingDir,
|
|
6607
|
+
clientType: "webui",
|
|
5104
6608
|
pid: process.pid,
|
|
5105
6609
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5106
6610
|
});
|
|
@@ -5110,8 +6614,8 @@ async function startWebUI(opts = {}) {
|
|
|
5110
6614
|
type: "projects.selected",
|
|
5111
6615
|
payload: {
|
|
5112
6616
|
root: resolved,
|
|
5113
|
-
name: selName ||
|
|
5114
|
-
message: `Switched to ${selName ||
|
|
6617
|
+
name: selName || path10.basename(resolved),
|
|
6618
|
+
message: `Switched to ${selName || path10.basename(resolved)}`
|
|
5115
6619
|
}
|
|
5116
6620
|
});
|
|
5117
6621
|
broadcast(clients, {
|
|
@@ -5134,7 +6638,7 @@ async function startWebUI(opts = {}) {
|
|
|
5134
6638
|
type: "projects.selected",
|
|
5135
6639
|
payload: {
|
|
5136
6640
|
root: selRoot,
|
|
5137
|
-
name: selName ||
|
|
6641
|
+
name: selName || path10.basename(selRoot),
|
|
5138
6642
|
message: errMessage(err)
|
|
5139
6643
|
}
|
|
5140
6644
|
});
|
|
@@ -5145,17 +6649,17 @@ async function startWebUI(opts = {}) {
|
|
|
5145
6649
|
case "working_dir.set": {
|
|
5146
6650
|
const { path: newPath } = msg.payload;
|
|
5147
6651
|
try {
|
|
5148
|
-
const resolved =
|
|
5149
|
-
if (!resolved.startsWith(projectRoot +
|
|
5150
|
-
|
|
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}`);
|
|
5151
6655
|
break;
|
|
5152
6656
|
}
|
|
5153
6657
|
try {
|
|
5154
|
-
await
|
|
5155
|
-
const stat2 = await
|
|
6658
|
+
await fs10.access(resolved);
|
|
6659
|
+
const stat2 = await fs10.stat(resolved);
|
|
5156
6660
|
if (!stat2.isDirectory()) throw new Error("Not a directory");
|
|
5157
6661
|
} catch {
|
|
5158
|
-
|
|
6662
|
+
sendResult2(ws, false, `Directory not found or not accessible: ${resolved}`);
|
|
5159
6663
|
break;
|
|
5160
6664
|
}
|
|
5161
6665
|
workingDir = resolved;
|
|
@@ -5164,9 +6668,9 @@ async function startWebUI(opts = {}) {
|
|
|
5164
6668
|
type: "working_dir.changed",
|
|
5165
6669
|
payload: { cwd: resolved, projectRoot }
|
|
5166
6670
|
});
|
|
5167
|
-
|
|
6671
|
+
sendResult2(ws, true, `Working directory set to ${resolved}`);
|
|
5168
6672
|
} catch (err) {
|
|
5169
|
-
|
|
6673
|
+
sendResult2(ws, false, errMessage(err));
|
|
5170
6674
|
}
|
|
5171
6675
|
break;
|
|
5172
6676
|
}
|
|
@@ -5176,31 +6680,31 @@ async function startWebUI(opts = {}) {
|
|
|
5176
6680
|
msg.payload,
|
|
5177
6681
|
logger
|
|
5178
6682
|
);
|
|
5179
|
-
|
|
6683
|
+
sendResult2(ws, result.success, result.message);
|
|
5180
6684
|
break;
|
|
5181
6685
|
}
|
|
5182
6686
|
// ── Mailbox operations — project-level inter-agent messaging ────
|
|
5183
6687
|
case "mailbox.messages":
|
|
5184
6688
|
return handleMailboxMessages(
|
|
5185
6689
|
ws,
|
|
5186
|
-
{ projectRoot, globalRoot:
|
|
6690
|
+
{ projectRoot, globalRoot: path10.dirname(globalConfigPath) },
|
|
5187
6691
|
msg.payload
|
|
5188
6692
|
);
|
|
5189
6693
|
case "mailbox.agents":
|
|
5190
6694
|
return handleMailboxAgents(
|
|
5191
6695
|
ws,
|
|
5192
|
-
{ projectRoot, globalRoot:
|
|
6696
|
+
{ projectRoot, globalRoot: path10.dirname(globalConfigPath) },
|
|
5193
6697
|
msg.payload
|
|
5194
6698
|
);
|
|
5195
6699
|
case "mailbox.clear":
|
|
5196
6700
|
return handleMailboxClear(
|
|
5197
6701
|
ws,
|
|
5198
|
-
{ projectRoot, globalRoot:
|
|
6702
|
+
{ projectRoot, globalRoot: path10.dirname(globalConfigPath) }
|
|
5199
6703
|
);
|
|
5200
6704
|
case "mailbox.purge":
|
|
5201
6705
|
return handleMailboxPurge(
|
|
5202
6706
|
ws,
|
|
5203
|
-
{ projectRoot, globalRoot:
|
|
6707
|
+
{ projectRoot, globalRoot: path10.dirname(globalConfigPath) },
|
|
5204
6708
|
msg.payload
|
|
5205
6709
|
);
|
|
5206
6710
|
// ── Brain — status, autonomy ceiling, direct decision support ───
|
|
@@ -5214,7 +6718,7 @@ async function startWebUI(opts = {}) {
|
|
|
5214
6718
|
const level = msg.payload?.level ?? "";
|
|
5215
6719
|
const valid = ["off", "low", "medium", "high", "all"];
|
|
5216
6720
|
if (!valid.includes(level)) {
|
|
5217
|
-
|
|
6721
|
+
sendResult2(ws, false, `Unknown risk level "${level}". Use: ${valid.join(", ")}.`);
|
|
5218
6722
|
break;
|
|
5219
6723
|
}
|
|
5220
6724
|
brainSettings.maxAutoRisk = level;
|
|
@@ -5227,7 +6731,7 @@ async function startWebUI(opts = {}) {
|
|
|
5227
6731
|
case "brain.ask": {
|
|
5228
6732
|
const question = msg.payload?.question?.trim();
|
|
5229
6733
|
if (!question) {
|
|
5230
|
-
|
|
6734
|
+
sendResult2(ws, false, "Usage: /brain ask <question>");
|
|
5231
6735
|
break;
|
|
5232
6736
|
}
|
|
5233
6737
|
try {
|
|
@@ -5240,7 +6744,7 @@ async function startWebUI(opts = {}) {
|
|
|
5240
6744
|
});
|
|
5241
6745
|
send(ws, { type: "brain.answer", payload: { question, decision } });
|
|
5242
6746
|
} catch (err) {
|
|
5243
|
-
|
|
6747
|
+
sendResult2(ws, false, `Brain consultation failed: ${errMessage(err)}`);
|
|
5244
6748
|
}
|
|
5245
6749
|
break;
|
|
5246
6750
|
}
|
|
@@ -5267,14 +6771,28 @@ async function startWebUI(opts = {}) {
|
|
|
5267
6771
|
broadcast,
|
|
5268
6772
|
clients
|
|
5269
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
|
+
};
|
|
5270
6784
|
const httpServer = createHttpServer({
|
|
5271
6785
|
host: wsHost,
|
|
5272
|
-
distDir:
|
|
6786
|
+
distDir: path10.resolve(import.meta.dirname, "../../dist"),
|
|
5273
6787
|
wsPort,
|
|
5274
6788
|
globalRoot: wpaths.globalRoot,
|
|
5275
|
-
apiToken: wsToken
|
|
6789
|
+
apiToken: wsToken,
|
|
6790
|
+
watcherMetrics,
|
|
6791
|
+
onFleetPing: () => {
|
|
6792
|
+
void fleetBroadcast?.();
|
|
6793
|
+
}
|
|
5276
6794
|
});
|
|
5277
|
-
const registryBaseDir =
|
|
6795
|
+
const registryBaseDir = path10.dirname(globalConfigPath);
|
|
5278
6796
|
httpServer.listen(httpPort, wsHost, () => {
|
|
5279
6797
|
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
5280
6798
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
@@ -5286,7 +6804,7 @@ async function startWebUI(opts = {}) {
|
|
|
5286
6804
|
wsPort,
|
|
5287
6805
|
host: wsHost,
|
|
5288
6806
|
projectRoot,
|
|
5289
|
-
projectName:
|
|
6807
|
+
projectName: path10.basename(projectRoot) || projectRoot,
|
|
5290
6808
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5291
6809
|
url: `http://${wsHost}:${httpPort}`
|
|
5292
6810
|
},
|
|
@@ -5313,6 +6831,10 @@ async function startWebUI(opts = {}) {
|
|
|
5313
6831
|
// reality. Crash exits are healed by the next register()/list() prune pass.
|
|
5314
6832
|
onShutdown: () => {
|
|
5315
6833
|
brainMonitor.stop();
|
|
6834
|
+
if (disposeEvents) {
|
|
6835
|
+
disposeEvents();
|
|
6836
|
+
disposeEvents = null;
|
|
6837
|
+
}
|
|
5316
6838
|
if (eternalSubscription) {
|
|
5317
6839
|
eternalSubscription.dispose();
|
|
5318
6840
|
eternalSubscription = null;
|
|
@@ -5343,10 +6865,28 @@ export {
|
|
|
5343
6865
|
handleFilesRead,
|
|
5344
6866
|
handleFilesTree,
|
|
5345
6867
|
handleFilesWrite,
|
|
6868
|
+
handleGitInfo,
|
|
6869
|
+
handleMcpAdd,
|
|
6870
|
+
handleMcpDisable,
|
|
6871
|
+
handleMcpDiscover,
|
|
6872
|
+
handleMcpEnable,
|
|
6873
|
+
handleMcpList,
|
|
6874
|
+
handleMcpRemove,
|
|
6875
|
+
handleMcpRestart,
|
|
6876
|
+
handleMcpSleep,
|
|
6877
|
+
handleMcpUpdate,
|
|
6878
|
+
handleMcpWake,
|
|
5346
6879
|
handleMemoryForget,
|
|
5347
6880
|
handleMemoryList,
|
|
5348
6881
|
handleMemoryRemember,
|
|
5349
6882
|
handleShellOpen,
|
|
6883
|
+
handleSkillsContent,
|
|
6884
|
+
handleSkillsCreate,
|
|
6885
|
+
handleSkillsEdit,
|
|
6886
|
+
handleSkillsExport,
|
|
6887
|
+
handleSkillsInstall,
|
|
6888
|
+
handleSkillsUninstall,
|
|
6889
|
+
handleSkillsUpdate,
|
|
5350
6890
|
hostHeaderOk,
|
|
5351
6891
|
injectWsPort,
|
|
5352
6892
|
isLoopbackBind,
|
|
@@ -5364,7 +6904,7 @@ export {
|
|
|
5364
6904
|
removeProvider,
|
|
5365
6905
|
saveProviders,
|
|
5366
6906
|
send,
|
|
5367
|
-
sendResult,
|
|
6907
|
+
sendResult2 as sendResult,
|
|
5368
6908
|
setActiveKey,
|
|
5369
6909
|
startWebUI,
|
|
5370
6910
|
stringifyContent,
|