@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.
@@ -0,0 +1,45 @@
1
+ import { WebSocket } from 'ws';
2
+ import { TodoItem } from '@wrongstack/core';
3
+
4
+ interface WorklistContext {
5
+ context: {
6
+ todos: TodoItem[];
7
+ meta: Record<string, unknown>;
8
+ session: {
9
+ id: string;
10
+ } | null;
11
+ state?: unknown;
12
+ };
13
+ send: (ws: WebSocket, msg: object) => void;
14
+ broadcast: (msg: object) => void;
15
+ /**
16
+ * Optional mutator for in-memory todo state. Servers that manage live
17
+ * agent state (e.g. the CLI embedded server) provide this so handlers
18
+ * can update the agent's todo list directly. Standalone server may omit.
19
+ */
20
+ replaceTodos?: (todos: TodoItem[]) => void;
21
+ }
22
+ declare function handleTodosGet(ctx: WorklistContext, ws: WebSocket): void;
23
+ declare function handleTodosClear(ctx: WorklistContext, ws: WebSocket): void;
24
+ declare function handleTodosRemove(ctx: WorklistContext, ws: WebSocket, payload: {
25
+ id?: string;
26
+ index?: number;
27
+ } | undefined): void;
28
+ declare function handleTodoUpdate(ctx: WorklistContext, ws: WebSocket, payload: {
29
+ id: string;
30
+ status?: TodoItem['status'];
31
+ activeForm?: string;
32
+ }): void;
33
+ declare function handleTasksGet(ctx: WorklistContext, ws: WebSocket): Promise<void>;
34
+ declare function handleTaskUpdate(ctx: WorklistContext, ws: WebSocket, payload: {
35
+ id: string;
36
+ status: 'pending' | 'in_progress' | 'blocked' | 'failed' | 'review' | 'completed';
37
+ }): Promise<void>;
38
+ declare function handlePlanGet(ctx: WorklistContext, ws: WebSocket): Promise<void>;
39
+ declare function handlePlanTemplateUse(ctx: WorklistContext, ws: WebSocket, template: string): Promise<void>;
40
+ declare function handlePlanItemUpdate(ctx: WorklistContext, ws: WebSocket, payload: {
41
+ target: string;
42
+ status: 'open' | 'in_progress' | 'done';
43
+ }): Promise<void>;
44
+
45
+ export { type WorklistContext, handlePlanGet, handlePlanItemUpdate, handlePlanTemplateUse, handleTaskUpdate, handleTasksGet, handleTodoUpdate, handleTodosClear, handleTodosGet, handleTodosRemove };
@@ -0,0 +1,179 @@
1
+ // src/server/handlers/worklist-handlers.ts
2
+ function sendResult(ws, ctx, ok, message) {
3
+ ctx.send(ws, { type: ok ? "ok" : "error", message });
4
+ }
5
+ function handleTodosGet(ctx, ws) {
6
+ ctx.send(ws, { type: "todos.updated", payload: { todos: ctx.context.todos } });
7
+ }
8
+ function handleTodosClear(ctx, ws) {
9
+ ctx.replaceTodos?.([]);
10
+ ctx.broadcast({ type: "todos.cleared" });
11
+ sendResult(ws, ctx, true, "Todo board cleared.");
12
+ }
13
+ function handleTodosRemove(ctx, ws, payload) {
14
+ if (!payload || payload.id === void 0 && payload.index === void 0) {
15
+ sendResult(ws, ctx, false, "todos.remove requires id or index.");
16
+ return;
17
+ }
18
+ const next = payload.id !== void 0 ? ctx.context.todos.filter((t) => t.id !== payload.id) : ctx.context.todos.filter((_, i) => i !== payload.index);
19
+ ctx.replaceTodos?.(next);
20
+ ctx.broadcast({ type: "todos.updated", payload: { todos: next } });
21
+ sendResult(ws, ctx, true, "Todo item removed.");
22
+ }
23
+ function handleTodoUpdate(ctx, ws, payload) {
24
+ const todo = ctx.context.todos.find((t) => t.id === payload.id);
25
+ if (!todo) {
26
+ sendResult(ws, ctx, false, `No todo with id "${payload.id}".`);
27
+ return;
28
+ }
29
+ const next = ctx.context.todos.map(
30
+ (t) => t.id === payload.id ? { ...t, ...payload.status !== void 0 && { status: payload.status }, ...payload.activeForm !== void 0 && { activeForm: payload.activeForm } } : t
31
+ );
32
+ ctx.replaceTodos?.(next);
33
+ ctx.broadcast({ type: "todos.updated", payload: { todos: next } });
34
+ sendResult(ws, ctx, true, `Todo "${todo.content}" updated.`);
35
+ }
36
+ async function handleTasksGet(ctx, ws) {
37
+ const taskPath = ctx.context.meta["task.path"];
38
+ if (typeof taskPath === "string" && taskPath) {
39
+ try {
40
+ const { loadTasks } = await import("@wrongstack/core");
41
+ const file = await loadTasks(taskPath);
42
+ ctx.send(ws, { type: "tasks.updated", payload: { tasks: file?.tasks ?? [] } });
43
+ } catch {
44
+ ctx.send(ws, { type: "tasks.updated", payload: { tasks: [] } });
45
+ }
46
+ } else {
47
+ ctx.send(ws, {
48
+ type: "tasks.updated",
49
+ payload: { tasks: [], error: "Task storage not configured." }
50
+ });
51
+ }
52
+ }
53
+ async function handleTaskUpdate(ctx, ws, payload) {
54
+ const taskPath = ctx.context.meta["task.path"];
55
+ if (typeof taskPath !== "string" || !taskPath) {
56
+ sendResult(ws, ctx, false, "Task storage is not configured for this session.");
57
+ return;
58
+ }
59
+ try {
60
+ const { loadTasks, saveTasks } = await import("@wrongstack/core");
61
+ const file = await loadTasks(taskPath);
62
+ if (!file) {
63
+ sendResult(ws, ctx, false, "No task file found.");
64
+ return;
65
+ }
66
+ const idx = file.tasks.findIndex((t) => t.id === payload.id);
67
+ if (idx === -1) {
68
+ sendResult(ws, ctx, false, `Task "${payload.id}" not found.`);
69
+ return;
70
+ }
71
+ file.tasks[idx] = { ...file.tasks[idx], status: payload.status };
72
+ await saveTasks(taskPath, file);
73
+ ctx.broadcast({ type: "tasks.updated", payload: { tasks: file.tasks } });
74
+ sendResult(ws, ctx, true, `Task "${payload.id}" marked ${payload.status}.`);
75
+ } catch (err) {
76
+ sendResult(ws, ctx, false, String(err));
77
+ }
78
+ }
79
+ async function handlePlanGet(ctx, ws) {
80
+ const planPath = ctx.context.meta["plan.path"];
81
+ const sessionId = ctx.context.session?.id ?? "";
82
+ if (typeof planPath === "string" && planPath) {
83
+ try {
84
+ const { loadPlan } = await import("@wrongstack/core");
85
+ const plan = await loadPlan(planPath);
86
+ ctx.send(ws, {
87
+ type: "plan.updated",
88
+ payload: {
89
+ plan: plan ?? {
90
+ version: 1,
91
+ sessionId,
92
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
93
+ items: []
94
+ }
95
+ }
96
+ });
97
+ } catch {
98
+ ctx.send(ws, {
99
+ type: "plan.updated",
100
+ payload: {
101
+ plan: {
102
+ version: 1,
103
+ sessionId,
104
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
105
+ items: []
106
+ }
107
+ }
108
+ });
109
+ }
110
+ } else {
111
+ ctx.send(ws, {
112
+ type: "plan.updated",
113
+ payload: { plan: null, error: "Plan storage is not configured for this session." }
114
+ });
115
+ }
116
+ }
117
+ async function handlePlanTemplateUse(ctx, ws, template) {
118
+ const planPath = ctx.context.meta["plan.path"];
119
+ const sessionId = ctx.context.session?.id ?? "";
120
+ if (typeof planPath !== "string" || !planPath) {
121
+ sendResult(ws, ctx, false, "Plan storage is not configured for this session.");
122
+ return;
123
+ }
124
+ try {
125
+ const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem } = await import("@wrongstack/core");
126
+ const tpl = getPlanTemplate(template);
127
+ if (!tpl) {
128
+ sendResult(ws, ctx, false, `Unknown template "${template}".`);
129
+ return;
130
+ }
131
+ let plan = await loadPlan(planPath) ?? emptyPlan(sessionId);
132
+ for (const item of tpl.items) {
133
+ ({ plan } = addPlanItem(plan, item.title, item.details));
134
+ }
135
+ await savePlan(planPath, plan);
136
+ sendResult(ws, ctx, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
137
+ ctx.broadcast({ type: "plan.updated", payload: { plan } });
138
+ } catch (err) {
139
+ sendResult(ws, ctx, false, String(err));
140
+ }
141
+ }
142
+ async function handlePlanItemUpdate(ctx, ws, payload) {
143
+ const planPath = ctx.context.meta["plan.path"];
144
+ const sessionId = ctx.context.session?.id ?? "";
145
+ if (typeof planPath !== "string" || !planPath) {
146
+ sendResult(ws, ctx, false, "Plan storage is not configured for this session.");
147
+ return;
148
+ }
149
+ try {
150
+ const { loadPlan, savePlan, mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
151
+ let changed = false;
152
+ const plan = await mutatePlan(planPath, sessionId, async (p) => {
153
+ const before = p.updatedAt;
154
+ const updated = setPlanItemStatus(p, payload.target, payload.status);
155
+ changed = updated.updatedAt !== before;
156
+ return updated;
157
+ });
158
+ if (!changed) {
159
+ sendResult(ws, ctx, false, `No plan item matched "${payload.target}".`);
160
+ return;
161
+ }
162
+ sendResult(ws, ctx, true, `Plan item status updated to "${payload.status}".`);
163
+ ctx.broadcast({ type: "plan.updated", payload: { plan } });
164
+ } catch (err) {
165
+ sendResult(ws, ctx, false, String(err));
166
+ }
167
+ }
168
+ export {
169
+ handlePlanGet,
170
+ handlePlanItemUpdate,
171
+ handlePlanTemplateUse,
172
+ handleTaskUpdate,
173
+ handleTasksGet,
174
+ handleTodoUpdate,
175
+ handleTodosClear,
176
+ handleTodosGet,
177
+ handleTodosRemove
178
+ };
179
+ //# sourceMappingURL=handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/handlers/worklist-handlers.ts"],"sourcesContent":["// ── Shared Worklist Handlers ─────────────────────────────────────────────────\n// Extracted from standalone server (packages/webui/src/server/index.ts) and CLI\n// embedded server (packages/cli/src/webui-server/). Both servers use these\n// handlers for todos, tasks, and plan operations. Keep them in sync.\n//\n// Message types handled here:\n// todos.get | todos.clear | todos.remove | todo.update\n// tasks.get | task.update\n// plan.get | plan.template_use | plan.item.update\n// ─────────────────────────────────────────────────────────────────────────────\n\nimport type { WebSocket } from 'ws';\nimport type { TodoItem } from '@wrongstack/core';\n\n// ── Shared result helper ───────────────────────────────────────────────────────\n\nfunction sendResult(\n ws: WebSocket,\n ctx: WorklistContext,\n ok: boolean,\n message: string,\n): void {\n ctx.send(ws, { type: ok ? 'ok' : 'error', message });\n}\n\n// ── Context interface ─────────────────────────────────────────────────────────\n// Both servers satisfy this with their own local state.\n\nexport interface WorklistContext {\n context: {\n todos: TodoItem[];\n meta: Record<string, unknown>;\n session: { id: string } | null;\n state?: unknown;\n };\n send: (ws: WebSocket, msg: object) => void;\n broadcast: (msg: object) => void;\n /**\n * Optional mutator for in-memory todo state. Servers that manage live\n * agent state (e.g. the CLI embedded server) provide this so handlers\n * can update the agent's todo list directly. Standalone server may omit.\n */\n replaceTodos?: (todos: TodoItem[]) => void;\n}\n\n// ── Todos ─────────────────────────────────────────────────────────────────────\n\nexport function handleTodosGet(ctx: WorklistContext, ws: WebSocket): void {\n ctx.send(ws, { type: 'todos.updated', payload: { todos: ctx.context.todos } });\n}\n\nexport function handleTodosClear(ctx: WorklistContext, ws: WebSocket): void {\n ctx.replaceTodos?.([]);\n ctx.broadcast({ type: 'todos.cleared' });\n sendResult(ws, ctx, true, 'Todo board cleared.');\n}\n\nexport function handleTodosRemove(\n ctx: WorklistContext,\n ws: WebSocket,\n payload: { id?: string; index?: number } | undefined,\n): void {\n if (!payload || (payload.id === undefined && payload.index === undefined)) {\n sendResult(ws, ctx, false, 'todos.remove requires id or index.');\n return;\n }\n const next =\n payload.id !== undefined\n ? ctx.context.todos.filter((t) => t.id !== payload.id)\n : ctx.context.todos.filter((_, i) => i !== (payload.index as number));\n ctx.replaceTodos?.(next);\n ctx.broadcast({ type: 'todos.updated', payload: { todos: next } });\n sendResult(ws, ctx, true, 'Todo item removed.');\n}\n\nexport function handleTodoUpdate(\n ctx: WorklistContext,\n ws: WebSocket,\n payload: { id: string; status?: TodoItem['status']; activeForm?: string },\n): void {\n const todo = ctx.context.todos.find((t) => t.id === payload.id);\n if (!todo) {\n sendResult(ws, ctx, false, `No todo with id \"${payload.id}\".`);\n return;\n }\n const next = ctx.context.todos.map((t) =>\n t.id === payload.id\n ? { ...t, ...(payload.status !== undefined && { status: payload.status }), ...(payload.activeForm !== undefined && { activeForm: payload.activeForm }) }\n : t,\n );\n ctx.replaceTodos?.(next);\n ctx.broadcast({ type: 'todos.updated', payload: { todos: next } });\n sendResult(ws, ctx, true, `Todo \"${todo.content}\" updated.`);\n}\n\n// ── Tasks ─────────────────────────────────────────────────────────────────────\n\nexport async function handleTasksGet(ctx: WorklistContext, ws: WebSocket): Promise<void> {\n const taskPath = ctx.context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n try {\n const { loadTasks } = await import('@wrongstack/core');\n const file = await loadTasks(taskPath);\n ctx.send(ws, { type: 'tasks.updated', payload: { tasks: file?.tasks ?? [] } });\n } catch {\n ctx.send(ws, { type: 'tasks.updated', payload: { tasks: [] } });\n }\n } else {\n ctx.send(ws, {\n type: 'tasks.updated',\n payload: { tasks: [], error: 'Task storage not configured.' },\n });\n }\n}\n\nexport async function handleTaskUpdate(\n ctx: WorklistContext,\n ws: WebSocket,\n payload: {\n id: string;\n status: 'pending' | 'in_progress' | 'blocked' | 'failed' | 'review' | 'completed';\n },\n): Promise<void> {\n const taskPath = ctx.context.meta['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) {\n sendResult(ws, ctx, false, 'Task storage is not configured for this session.');\n return;\n }\n try {\n const { loadTasks, saveTasks } = await import('@wrongstack/core');\n const file = await loadTasks(taskPath);\n if (!file) {\n sendResult(ws, ctx, false, 'No task file found.');\n return;\n }\n const idx = file.tasks.findIndex((t) => t.id === payload.id);\n if (idx === -1) {\n sendResult(ws, ctx, false, `Task \"${payload.id}\" not found.`);\n return;\n }\n file.tasks[idx] = { ...file.tasks[idx], status: payload.status };\n await saveTasks(taskPath, file);\n ctx.broadcast({ type: 'tasks.updated', payload: { tasks: file.tasks } });\n sendResult(ws, ctx, true, `Task \"${payload.id}\" marked ${payload.status}.`);\n } catch (err) {\n sendResult(ws, ctx, false, String(err));\n }\n}\n\n// ── Plan ───────────────────────────────────────────────────────────────────────\n\nexport async function handlePlanGet(ctx: WorklistContext, ws: WebSocket): Promise<void> {\n const planPath = ctx.context.meta['plan.path'];\n const sessionId = ctx.context.session?.id ?? '';\n if (typeof planPath === 'string' && planPath) {\n try {\n const { loadPlan } = await import('@wrongstack/core');\n const plan = await loadPlan(planPath);\n ctx.send(ws, {\n type: 'plan.updated',\n payload: {\n plan: plan ?? {\n version: 1,\n sessionId,\n updatedAt: new Date().toISOString(),\n items: [],\n },\n },\n });\n } catch {\n ctx.send(ws, {\n type: 'plan.updated',\n payload: {\n plan: {\n version: 1,\n sessionId,\n updatedAt: new Date().toISOString(),\n items: [],\n },\n },\n });\n }\n } else {\n ctx.send(ws, {\n type: 'plan.updated',\n payload: { plan: null, error: 'Plan storage is not configured for this session.' },\n });\n }\n}\n\nexport async function handlePlanTemplateUse(ctx: WorklistContext, ws: WebSocket, template: string): Promise<void> {\n const planPath = ctx.context.meta['plan.path'];\n const sessionId = ctx.context.session?.id ?? '';\n if (typeof planPath !== 'string' || !planPath) {\n sendResult(ws, ctx, false, 'Plan storage is not configured for this session.');\n return;\n }\n try {\n const { getPlanTemplate, loadPlan, savePlan, emptyPlan, addPlanItem } = await import('@wrongstack/core');\n const tpl = getPlanTemplate(template);\n if (!tpl) {\n sendResult(ws, ctx, false, `Unknown template \"${template}\".`);\n return;\n }\n let plan = (await loadPlan(planPath)) ?? emptyPlan(sessionId);\n for (const item of tpl.items) {\n ({ plan } = addPlanItem(plan, item.title, item.details));\n }\n await savePlan(planPath, plan);\n sendResult(ws, ctx, true, `Applied template \"${tpl.name}\" — ${tpl.items.length} items added.`);\n ctx.broadcast({ type: 'plan.updated', payload: { plan } });\n } catch (err) {\n sendResult(ws, ctx, false, String(err));\n }\n}\n\nexport async function handlePlanItemUpdate(\n ctx: WorklistContext,\n ws: WebSocket,\n payload: { target: string; status: 'open' | 'in_progress' | 'done' },\n): Promise<void> {\n const planPath = ctx.context.meta['plan.path'];\n const sessionId = ctx.context.session?.id ?? '';\n if (typeof planPath !== 'string' || !planPath) {\n sendResult(ws, ctx, false, 'Plan storage is not configured for this session.');\n return;\n }\n try {\n const { loadPlan, savePlan, mutatePlan, setPlanItemStatus } = await import('@wrongstack/core');\n let changed = false;\n const plan = await mutatePlan(planPath, sessionId, async (p) => {\n const before = p.updatedAt;\n const updated = setPlanItemStatus(p, payload.target, payload.status);\n changed = updated.updatedAt !== before;\n return updated;\n });\n if (!changed) {\n sendResult(ws, ctx, false, `No plan item matched \"${payload.target}\".`);\n return;\n }\n sendResult(ws, ctx, true, `Plan item status updated to \"${payload.status}\".`);\n ctx.broadcast({ type: 'plan.updated', payload: { plan } });\n } catch (err) {\n sendResult(ws, ctx, false, String(err));\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,8 @@
1
1
  import { WebSocket } from 'ws';
2
- import { Agent, EventBus, SessionStore, ToolRegistry, ModelsRegistry, ConfigStore, SecretVault, JournalEntry, Logger, MemoryStore, ProviderConfig, ProviderApiKey, Context } from '@wrongstack/core';
2
+ import { Agent, EventBus, SessionStore, ToolRegistry, ModelsRegistry, ConfigStore, SecretVault, JournalEntry, Logger, MemoryStore, ProviderConfig, ProviderApiKey, Context, SkillLoader } from '@wrongstack/core';
3
3
  import * as http from 'node:http';
4
+ import { MCPRegistry } from '@wrongstack/mcp';
5
+ import { SkillInstaller } from '@wrongstack/core/skills';
4
6
 
5
7
  interface WSServerMessage {
6
8
  type: string;
@@ -68,6 +70,25 @@ interface ConnectedClient {
68
70
  ws: WebSocket;
69
71
  sessionId: string | null;
70
72
  connectedAt: number;
73
+ /** Unique per-connection id — used to key per-connection state (e.g. the
74
+ * rate-limit bucket) so distinct browser tabs that share the same
75
+ * `sessionId` do not collide, and so the entry is reliably removable on
76
+ * close (`String(ws)` is `"[object Object]"` for every socket). */
77
+ connId: string;
78
+ }
79
+
80
+ /** Metrics for the file watcher that watches status.json files. */
81
+ interface FileWatcherMetrics {
82
+ fileChangesDetected: number;
83
+ filesProcessed: number;
84
+ broadcastsSent: number;
85
+ debounceResets: number;
86
+ totalDebounceDelayMs: number;
87
+ activeProjects: number;
88
+ /** Average debounce delay in ms across all broadcasts. */
89
+ averageDebounceDelayMs: number;
90
+ /** Whether the file watcher is currently active. */
91
+ watcherActive: boolean;
71
92
  }
72
93
 
73
94
  interface CreateHttpServerOptions {
@@ -102,6 +123,17 @@ interface CreateHttpServerOptions {
102
123
  * URL-token-only flow (e.g. in tests that don't want cookie state).
103
124
  */
104
125
  enableWsCookie?: boolean | undefined;
126
+ /**
127
+ * Optional file watcher metrics object. When provided, the
128
+ * /debug/watcher-metrics endpoint will be enabled to expose these metrics.
129
+ */
130
+ watcherMetrics?: FileWatcherMetrics | undefined;
131
+ /**
132
+ * Push-on-write hook. `POST /api/fleet/ping` (loopback only) invokes this to
133
+ * trigger an immediate fleet re-broadcast, so a TUI/REPL's registry write
134
+ * reaches the map without waiting on the file-watch/poll. Best-effort.
135
+ */
136
+ onFleetPing?: (() => void) | undefined;
105
137
  }
106
138
  /**
107
139
  * Inject the live WS port into the served HTML so the frontend connects to
@@ -291,13 +323,13 @@ declare function handleShellOpen(req: ShellOpenRequest, logger: Logger): Promise
291
323
  * Send a JSON message to a single WebSocket client.
292
324
  * No-op when the socket is not in OPEN state (disconnected / closing).
293
325
  */
294
- declare function send(ws: WebSocket, msg: WSServerMessage): void;
326
+ declare function send(ws: WebSocket, msg: object): void;
295
327
  /**
296
328
  * Broadcast a JSON message to every connected client.
297
329
  * Swallows per-socket send errors — a client that disconnected between the
298
330
  * readyState check and `ws.send()` is cleaned up by its own `close` handler.
299
331
  */
300
- declare function broadcast(clients: Map<WebSocket, ConnectedClient>, msg: WSServerMessage): void;
332
+ declare function broadcast(clients: Map<WebSocket, ConnectedClient>, msg: object): void;
301
333
  /**
302
334
  * Send a success/failure result message (used by key.* and provider.* handlers).
303
335
  * The frontend expects `key.operation_result` with `{ success, message }`.
@@ -355,6 +387,42 @@ declare function handleFilesWrite(ws: WebSocket, msg: unknown, projectRoot: stri
355
387
  */
356
388
  declare function handleFilesList(ws: WebSocket, msg: unknown, projectRoot: string): Promise<void>;
357
389
 
390
+ /**
391
+ * Shared `git.info` WebSocket handler for both the standalone WebUI server and
392
+ * the CLI's `--webui` embedded server. Extracted from the duplicated switch
393
+ * cases in `index.ts` and `cli/src/webui-server.ts`, which had drifted (the
394
+ * standalone copy transposed ahead/behind and never matched deletions). One
395
+ * implementation here keeps both surfaces in lockstep.
396
+ *
397
+ * case 'git.info': return handleGitInfo(ws, projectRoot);
398
+ */
399
+
400
+ /**
401
+ * Read git branch, change stats, and upstream sync status from `projectRoot`
402
+ * and broadcast a `git.info` message. Never throws — a non-repo / missing-git
403
+ * directory yields an empty-but-valid payload.
404
+ */
405
+ declare function handleGitInfo(ws: WebSocket, projectRoot: string): Promise<void>;
406
+ /**
407
+ * Read the working-tree change set (everything that differs from HEAD:
408
+ * staged, unstaged, and untracked) and broadcast a `git.changes` message.
409
+ *
410
+ * The file list comes from `git status --porcelain -z` (NUL-delimited so
411
+ * paths with spaces/unicode survive intact, and renames are unambiguous).
412
+ * Per-file line counts come from `--numstat` of both the unstaged and the
413
+ * staged diff, summed; untracked files count their own lines as additions.
414
+ * Never throws — a non-repo yields an empty list.
415
+ */
416
+ declare function handleGitChanges(ws: WebSocket, projectRoot: string): Promise<void>;
417
+ /**
418
+ * Resolve the before/after text for a single file and broadcast a `git.diff`
419
+ * message. `oldText` is the file at HEAD (`git show HEAD:<path>`), `newText`
420
+ * is the current working-tree content. New/untracked files have empty
421
+ * `oldText`; deleted files have empty `newText`. Binary or oversized files
422
+ * are reported with a flag instead of content so the client can show a notice.
423
+ */
424
+ declare function handleGitDiff(ws: WebSocket, projectRoot: string, path: string): Promise<void>;
425
+
358
426
  /**
359
427
  * Shared memory-operation WebSocket handlers for both the standalone WebUI
360
428
  * server and the CLI's `--webui` embedded server. Extracted from the
@@ -382,6 +450,38 @@ declare function handleMemoryRemember(ws: WebSocket, msg: unknown, memoryStore:
382
450
  */
383
451
  declare function handleMemoryForget(ws: WebSocket, msg: unknown, memoryStore: MemoryStore): Promise<void>;
384
452
 
453
+ /**
454
+ * MCP management handlers for the WebUI server (both the standalone
455
+ * `wrongstack webui` server and the CLI's embedded `--webui` server).
456
+ *
457
+ * These are thin WebSocket translators over the shared, surface-agnostic
458
+ * management core in `@wrongstack/mcp` (`manage.ts`) — the SAME core the REPL
459
+ * `/mcp` command writes against (same config.json, same MCPRegistry). All the
460
+ * config IO, url/header persistence, and live registry start/stop logic lives
461
+ * there; here we only map structured results to WS events the browser expects.
462
+ */
463
+
464
+ /** mcp.list — configured servers merged with live registry status + tools. */
465
+ declare function handleMcpList(ws: WebSocket, _msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
466
+ /** mcp.add — persist a new server (incl. url/headers) and start it if enabled. */
467
+ declare function handleMcpAdd(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
468
+ /** mcp.update — re-persist config (incl. url/headers) and re-apply to registry. */
469
+ declare function handleMcpUpdate(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
470
+ /** mcp.remove — stop the server and delete it from config. */
471
+ declare function handleMcpRemove(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
472
+ /** mcp.enable — flip enabled:true in config and start the server. */
473
+ declare function handleMcpEnable(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
474
+ /** mcp.disable — stop the server and flip enabled:false in config. */
475
+ declare function handleMcpDisable(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
476
+ /** mcp.sleep — stop a running server (config stays enabled). */
477
+ declare function handleMcpSleep(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
478
+ /** mcp.wake — restart a sleeping/stopped server from config. */
479
+ declare function handleMcpWake(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
480
+ /** mcp.restart — stop + start a server. */
481
+ declare function handleMcpRestart(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
482
+ /** mcp.discover — ensure the server is running and report its live tools. */
483
+ declare function handleMcpDiscover(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
484
+
385
485
  /**
386
486
  * Custom context modes — user-defined presets that are loaded from disk,
387
487
  * merged with the built-in modes, and managed via WebSocket CRUD handlers.
@@ -510,8 +610,10 @@ interface KeyOpResult {
510
610
  declare function normalizeKeys(cfg: ProviderConfig): ProviderApiKey[];
511
611
  /**
512
612
  * Write a normalized key list back onto a provider config: drop all key fields
513
- * when empty, otherwise sync `apiKeys`, the legacy `apiKey` mirror (the active
514
- * key), and re-point `activeKey` if it no longer names a present key.
613
+ * when empty, otherwise sync `apiKeys` and re-point `activeKey` if it no longer
614
+ * names a present key. Does NOT mirror the plaintext key to the legacy `apiKey`
615
+ * field — that would leak the secret on accidental serialization. Consumers
616
+ * that need the real key should read from `apiKeys[]` directly.
515
617
  */
516
618
  declare function writeKeysBack(cfg: ProviderConfig, keys: ProviderApiKey[]): void;
517
619
  /** Mask a secret for display: `••••` for short keys, `abcd…wxyz` otherwise. */
@@ -607,10 +709,75 @@ declare class AutoPhaseWebSocketHandler {
607
709
  private send;
608
710
  }
609
711
 
712
+ /**
713
+ * Shared skills WebSocket handlers for both the standalone WebUI server
714
+ * (`packages/webui/src/server/index.ts`) and the CLI's `--webui` embedded
715
+ * server (`packages/cli/src/webui-server.ts`).
716
+ *
717
+ * These were previously inlined in BOTH servers, and the CLI copy had
718
+ * drifted — it only wired `skills.list`, so `skills.content` /
719
+ * `skills.export` / `skills.update` (and install/uninstall/create/edit)
720
+ * fell through to the "Unhandled message type" warning even though the
721
+ * SkillsPanel sends them. Extracting the full set here gives both servers
722
+ * one source of truth. Each function handles the full request→response
723
+ * cycle for one message type; callers drop them into their switch:
724
+ *
725
+ * case 'skills.content': return handleSkillsContent(ws, skillsCtx, msg);
726
+ *
727
+ * The logic is a verbatim lift of the standalone's inline cases — only the
728
+ * dependency references changed (`skillLoader`/`skillInstaller`/
729
+ * `projectRoot` → `ctx.*`, local `send`/`errMessage` → imported helpers).
730
+ */
731
+
732
+ interface SkillsContext {
733
+ /** Backs skills.list/content/edit/export. Absent ⇒ feature disabled. */
734
+ skillLoader: SkillLoader | undefined;
735
+ /** Backs skills.install/uninstall/update. Absent ⇒ those ops disabled. */
736
+ skillInstaller: SkillInstaller | undefined;
737
+ /** Project root — used by skills.create to write `.wrongstack/skills/…`. */
738
+ projectRoot: string;
739
+ }
740
+ /**
741
+ * Read a single skill's body + its directory's related files + which other
742
+ * skills reference it by name. Powers the skill detail/preview view.
743
+ */
744
+ declare function handleSkillsContent(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
745
+ /**
746
+ * Install a skill from a git ref (`owner/repo` or URL). Optional `global`
747
+ * installs into the user-wide skills dir instead of the project's.
748
+ */
749
+ declare function handleSkillsInstall(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
750
+ /**
751
+ * Uninstall a skill by name. Optional `global` restricts/Targets the
752
+ * user-wide install.
753
+ */
754
+ declare function handleSkillsUninstall(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
755
+ /**
756
+ * Update one skill (`name`) or all installed skills (when `name` is
757
+ * omitted). Reports per-skill updated/unchanged/error tallies.
758
+ */
759
+ declare function handleSkillsUpdate(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
760
+ /**
761
+ * Scaffold a new project- or global-scoped skill from a name + description.
762
+ * Writes a templated `SKILL.md` under `.wrongstack/skills/<name>/` (project)
763
+ * or the user-wide skills dir (global).
764
+ */
765
+ declare function handleSkillsCreate(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
766
+ /**
767
+ * Overwrite a skill's body. Refuses bundled skills (read-only) and unknown
768
+ * names.
769
+ */
770
+ declare function handleSkillsEdit(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
771
+ /**
772
+ * Export every readable skill as a base64-encoded zip (one folder per skill,
773
+ * each with its `SKILL.md`). Powers the panel's "Export all" button.
774
+ */
775
+ declare function handleSkillsExport(ws: WebSocket, ctx: SkillsContext): Promise<void>;
776
+
610
777
  declare function startWebUI(opts?: WebUIOptions & {
611
778
  wsPort?: number | undefined;
612
779
  wsHost?: string | undefined;
613
780
  open?: boolean | undefined;
614
781
  }): Promise<void>;
615
782
 
616
- export { AutoPhaseWebSocketHandler, type BackendServices, type ConnectedClient, type ContextBreakdown, type CreateHttpServerOptions, type CustomContextMode, type CustomModeStore, type EternalBroadcast, type EternalSubscribe, type EternalSubscription, type KeyOpResult, type MessageTokenEntry, type ProvidersRecord, type ShellOpenRequest, type ShellOpenResult, type ShellOpenTarget, type ToolTokenEntry, type VerifyClientInput, type WSClientMessage, type WSServerMessage, type WebUIInstanceRecord, type WebUIOptions, addProvider, broadcast, browserOpenCommand, buildCspHeader, createCustomModeStore, createEternalSubscription, createHttpServer, createProviderConfigIO, defaultBaseDir, deleteKey, errMessage, estimateTokens, extractToken, findFreePort, formatInstances, generateAuthToken, handleFilesList, handleFilesRead, handleFilesTree, handleFilesWrite, handleMemoryForget, handleMemoryList, handleMemoryRemember, handleShellOpen, hostHeaderOk, injectWsPort, isLoopbackBind, isLoopbackHostname, isPortFree, listInstances, loadSavedProviders, maskedKey, messagePreview, messageTokens, normalizeKeys, openBrowser, registerInstance, registryPath, removeProvider, saveProviders, send, sendResult, setActiveKey, startWebUI, stringifyContent, tokenMatches, unregisterInstance, upsertKey, verifyClient, writeKeysBack };
783
+ export { AutoPhaseWebSocketHandler, type BackendServices, type ConnectedClient, type ContextBreakdown, type CreateHttpServerOptions, type CustomContextMode, type CustomModeStore, type EternalBroadcast, type EternalSubscribe, type EternalSubscription, type KeyOpResult, type MessageTokenEntry, type ProvidersRecord, type ShellOpenRequest, type ShellOpenResult, type ShellOpenTarget, type SkillsContext, type ToolTokenEntry, type VerifyClientInput, type WSClientMessage, type WSServerMessage, type WebUIInstanceRecord, type WebUIOptions, addProvider, broadcast, browserOpenCommand, buildCspHeader, createCustomModeStore, createEternalSubscription, createHttpServer, createProviderConfigIO, defaultBaseDir, deleteKey, errMessage, estimateTokens, extractToken, findFreePort, formatInstances, generateAuthToken, handleFilesList, handleFilesRead, handleFilesTree, handleFilesWrite, handleGitChanges, handleGitDiff, handleGitInfo, handleMcpAdd, handleMcpDisable, handleMcpDiscover, handleMcpEnable, handleMcpList, handleMcpRemove, handleMcpRestart, handleMcpSleep, handleMcpUpdate, handleMcpWake, handleMemoryForget, handleMemoryList, handleMemoryRemember, handleShellOpen, handleSkillsContent, handleSkillsCreate, handleSkillsEdit, handleSkillsExport, handleSkillsInstall, handleSkillsUninstall, handleSkillsUpdate, hostHeaderOk, injectWsPort, isLoopbackBind, isLoopbackHostname, isPortFree, listInstances, loadSavedProviders, maskedKey, messagePreview, messageTokens, normalizeKeys, openBrowser, registerInstance, registryPath, removeProvider, saveProviders, send, sendResult, setActiveKey, startWebUI, stringifyContent, tokenMatches, unregisterInstance, upsertKey, verifyClient, writeKeysBack };