@wrongstack/webui 0.264.0 → 0.267.0

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