@wrongstack/webui 0.264.0 → 0.265.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,7 @@
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, Config, MCPRegistryHandle, ProviderConfig, ProviderApiKey, Context, SkillLoader } from '@wrongstack/core';
3
3
  import * as http from 'node:http';
4
+ import { SkillInstaller } from '@wrongstack/core/skills';
4
5
 
5
6
  interface WSServerMessage {
6
7
  type: string;
@@ -68,6 +69,25 @@ interface ConnectedClient {
68
69
  ws: WebSocket;
69
70
  sessionId: string | null;
70
71
  connectedAt: number;
72
+ /** Unique per-connection id — used to key per-connection state (e.g. the
73
+ * rate-limit bucket) so distinct browser tabs that share the same
74
+ * `sessionId` do not collide, and so the entry is reliably removable on
75
+ * close (`String(ws)` is `"[object Object]"` for every socket). */
76
+ connId: string;
77
+ }
78
+
79
+ /** Metrics for the file watcher that watches status.json files. */
80
+ interface FileWatcherMetrics {
81
+ fileChangesDetected: number;
82
+ filesProcessed: number;
83
+ broadcastsSent: number;
84
+ debounceResets: number;
85
+ totalDebounceDelayMs: number;
86
+ activeProjects: number;
87
+ /** Average debounce delay in ms across all broadcasts. */
88
+ averageDebounceDelayMs: number;
89
+ /** Whether the file watcher is currently active. */
90
+ watcherActive: boolean;
71
91
  }
72
92
 
73
93
  interface CreateHttpServerOptions {
@@ -102,6 +122,17 @@ interface CreateHttpServerOptions {
102
122
  * URL-token-only flow (e.g. in tests that don't want cookie state).
103
123
  */
104
124
  enableWsCookie?: boolean | undefined;
125
+ /**
126
+ * Optional file watcher metrics object. When provided, the
127
+ * /debug/watcher-metrics endpoint will be enabled to expose these metrics.
128
+ */
129
+ watcherMetrics?: FileWatcherMetrics | undefined;
130
+ /**
131
+ * Push-on-write hook. `POST /api/fleet/ping` (loopback only) invokes this to
132
+ * trigger an immediate fleet re-broadcast, so a TUI/REPL's registry write
133
+ * reaches the map without waiting on the file-watch/poll. Best-effort.
134
+ */
135
+ onFleetPing?: (() => void) | undefined;
105
136
  }
106
137
  /**
107
138
  * Inject the live WS port into the served HTML so the frontend connects to
@@ -291,13 +322,13 @@ declare function handleShellOpen(req: ShellOpenRequest, logger: Logger): Promise
291
322
  * Send a JSON message to a single WebSocket client.
292
323
  * No-op when the socket is not in OPEN state (disconnected / closing).
293
324
  */
294
- declare function send(ws: WebSocket, msg: WSServerMessage): void;
325
+ declare function send(ws: WebSocket, msg: object): void;
295
326
  /**
296
327
  * Broadcast a JSON message to every connected client.
297
328
  * Swallows per-socket send errors — a client that disconnected between the
298
329
  * readyState check and `ws.send()` is cleaned up by its own `close` handler.
299
330
  */
300
- declare function broadcast(clients: Map<WebSocket, ConnectedClient>, msg: WSServerMessage): void;
331
+ declare function broadcast(clients: Map<WebSocket, ConnectedClient>, msg: object): void;
301
332
  /**
302
333
  * Send a success/failure result message (used by key.* and provider.* handlers).
303
334
  * The frontend expects `key.operation_result` with `{ success, message }`.
@@ -355,6 +386,23 @@ declare function handleFilesWrite(ws: WebSocket, msg: unknown, projectRoot: stri
355
386
  */
356
387
  declare function handleFilesList(ws: WebSocket, msg: unknown, projectRoot: string): Promise<void>;
357
388
 
389
+ /**
390
+ * Shared `git.info` WebSocket handler for both the standalone WebUI server and
391
+ * the CLI's `--webui` embedded server. Extracted from the duplicated switch
392
+ * cases in `index.ts` and `cli/src/webui-server.ts`, which had drifted (the
393
+ * standalone copy transposed ahead/behind and never matched deletions). One
394
+ * implementation here keeps both surfaces in lockstep.
395
+ *
396
+ * case 'git.info': return handleGitInfo(ws, projectRoot);
397
+ */
398
+
399
+ /**
400
+ * Read git branch, change stats, and upstream sync status from `projectRoot`
401
+ * and broadcast a `git.info` message. Never throws — a non-repo / missing-git
402
+ * directory yields an empty-but-valid payload.
403
+ */
404
+ declare function handleGitInfo(ws: WebSocket, projectRoot: string): Promise<void>;
405
+
358
406
  /**
359
407
  * Shared memory-operation WebSocket handlers for both the standalone WebUI
360
408
  * server and the CLI's `--webui` embedded server. Extracted from the
@@ -382,6 +430,32 @@ declare function handleMemoryRemember(ws: WebSocket, msg: unknown, memoryStore:
382
430
  */
383
431
  declare function handleMemoryForget(ws: WebSocket, msg: unknown, memoryStore: MemoryStore): Promise<void>;
384
432
 
433
+ /**
434
+ * MCP management handlers for the WebUI server.
435
+ * Handles MCP-related WS messages from the browser client.
436
+ */
437
+
438
+ /** Handle mcp.list — return all configured MCP servers */
439
+ declare function handleMcpList(ws: WebSocket, _msg: WSClientMessage, config: Config, _globalConfigPath: string, mcpRegistry?: MCPRegistryHandle): Promise<void>;
440
+ /** Handle mcp.add — add a new MCP server configuration */
441
+ declare function handleMcpAdd(ws: WebSocket, msg: WSClientMessage, config: Config, globalConfigPath: string, mcpRegistry?: MCPRegistryHandle): Promise<void>;
442
+ /** Handle mcp.remove — remove an MCP server configuration */
443
+ declare function handleMcpRemove(ws: WebSocket, msg: WSClientMessage, _config: Config, globalConfigPath: string, mcpRegistry?: MCPRegistryHandle): Promise<void>;
444
+ /** Handle mcp.update — update an existing MCP server configuration */
445
+ declare function handleMcpUpdate(ws: WebSocket, msg: WSClientMessage, _config: Config, globalConfigPath: string): Promise<void>;
446
+ /** Handle mcp.wake — wake a sleeping MCP server (restart it) */
447
+ declare function handleMcpWake(ws: WebSocket, msg: WSClientMessage, _config: Config, _globalConfigPath: string, mcpRegistry?: MCPRegistryHandle): Promise<void>;
448
+ /** Handle mcp.sleep — put an MCP server to sleep (stop it) */
449
+ declare function handleMcpSleep(ws: WebSocket, msg: WSClientMessage, _config: Config, _globalConfigPath: string, mcpRegistry?: MCPRegistryHandle): Promise<void>;
450
+ /** Handle mcp.discover — perform one-time tool discovery on an MCP server */
451
+ declare function handleMcpDiscover(ws: WebSocket, msg: WSClientMessage, _config: Config, _globalConfigPath: string, _mcpRegistry?: MCPRegistryHandle): Promise<void>;
452
+ /** Handle mcp.enable — enable an MCP server */
453
+ declare function handleMcpEnable(ws: WebSocket, msg: WSClientMessage, _config: Config, _globalConfigPath: string): Promise<void>;
454
+ /** Handle mcp.disable — disable an MCP server */
455
+ declare function handleMcpDisable(ws: WebSocket, msg: WSClientMessage, _config: Config, _globalConfigPath: string): Promise<void>;
456
+ /** Handle mcp.restart — restart an MCP server */
457
+ declare function handleMcpRestart(ws: WebSocket, msg: WSClientMessage, _config: Config, _globalConfigPath: string): Promise<void>;
458
+
385
459
  /**
386
460
  * Custom context modes — user-defined presets that are loaded from disk,
387
461
  * merged with the built-in modes, and managed via WebSocket CRUD handlers.
@@ -510,8 +584,10 @@ interface KeyOpResult {
510
584
  declare function normalizeKeys(cfg: ProviderConfig): ProviderApiKey[];
511
585
  /**
512
586
  * 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.
587
+ * when empty, otherwise sync `apiKeys` and re-point `activeKey` if it no longer
588
+ * names a present key. Does NOT mirror the plaintext key to the legacy `apiKey`
589
+ * field — that would leak the secret on accidental serialization. Consumers
590
+ * that need the real key should read from `apiKeys[]` directly.
515
591
  */
516
592
  declare function writeKeysBack(cfg: ProviderConfig, keys: ProviderApiKey[]): void;
517
593
  /** Mask a secret for display: `••••` for short keys, `abcd…wxyz` otherwise. */
@@ -607,10 +683,75 @@ declare class AutoPhaseWebSocketHandler {
607
683
  private send;
608
684
  }
609
685
 
686
+ /**
687
+ * Shared skills WebSocket handlers for both the standalone WebUI server
688
+ * (`packages/webui/src/server/index.ts`) and the CLI's `--webui` embedded
689
+ * server (`packages/cli/src/webui-server.ts`).
690
+ *
691
+ * These were previously inlined in BOTH servers, and the CLI copy had
692
+ * drifted — it only wired `skills.list`, so `skills.content` /
693
+ * `skills.export` / `skills.update` (and install/uninstall/create/edit)
694
+ * fell through to the "Unhandled message type" warning even though the
695
+ * SkillsPanel sends them. Extracting the full set here gives both servers
696
+ * one source of truth. Each function handles the full request→response
697
+ * cycle for one message type; callers drop them into their switch:
698
+ *
699
+ * case 'skills.content': return handleSkillsContent(ws, skillsCtx, msg);
700
+ *
701
+ * The logic is a verbatim lift of the standalone's inline cases — only the
702
+ * dependency references changed (`skillLoader`/`skillInstaller`/
703
+ * `projectRoot` → `ctx.*`, local `send`/`errMessage` → imported helpers).
704
+ */
705
+
706
+ interface SkillsContext {
707
+ /** Backs skills.list/content/edit/export. Absent ⇒ feature disabled. */
708
+ skillLoader: SkillLoader | undefined;
709
+ /** Backs skills.install/uninstall/update. Absent ⇒ those ops disabled. */
710
+ skillInstaller: SkillInstaller | undefined;
711
+ /** Project root — used by skills.create to write `.wrongstack/skills/…`. */
712
+ projectRoot: string;
713
+ }
714
+ /**
715
+ * Read a single skill's body + its directory's related files + which other
716
+ * skills reference it by name. Powers the skill detail/preview view.
717
+ */
718
+ declare function handleSkillsContent(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
719
+ /**
720
+ * Install a skill from a git ref (`owner/repo` or URL). Optional `global`
721
+ * installs into the user-wide skills dir instead of the project's.
722
+ */
723
+ declare function handleSkillsInstall(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
724
+ /**
725
+ * Uninstall a skill by name. Optional `global` restricts/Targets the
726
+ * user-wide install.
727
+ */
728
+ declare function handleSkillsUninstall(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
729
+ /**
730
+ * Update one skill (`name`) or all installed skills (when `name` is
731
+ * omitted). Reports per-skill updated/unchanged/error tallies.
732
+ */
733
+ declare function handleSkillsUpdate(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
734
+ /**
735
+ * Scaffold a new project- or global-scoped skill from a name + description.
736
+ * Writes a templated `SKILL.md` under `.wrongstack/skills/<name>/` (project)
737
+ * or the user-wide skills dir (global).
738
+ */
739
+ declare function handleSkillsCreate(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
740
+ /**
741
+ * Overwrite a skill's body. Refuses bundled skills (read-only) and unknown
742
+ * names.
743
+ */
744
+ declare function handleSkillsEdit(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
745
+ /**
746
+ * Export every readable skill as a base64-encoded zip (one folder per skill,
747
+ * each with its `SKILL.md`). Powers the panel's "Export all" button.
748
+ */
749
+ declare function handleSkillsExport(ws: WebSocket, ctx: SkillsContext): Promise<void>;
750
+
610
751
  declare function startWebUI(opts?: WebUIOptions & {
611
752
  wsPort?: number | undefined;
612
753
  wsHost?: string | undefined;
613
754
  open?: boolean | undefined;
614
755
  }): Promise<void>;
615
756
 
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 };
757
+ 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, 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 };