@wrongstack/webui 0.283.0 → 0.284.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.
Files changed (96) hide show
  1. package/dist/assets/AnalyticsDashboard-BBoAzpgL.js +1 -0
  2. package/dist/assets/AutoPhaseView-D053zOhN.js +1 -0
  3. package/dist/assets/ChangesView-4X9cQM4r.js +1 -0
  4. package/dist/assets/CodeEditor-C8hiA7LI.js +1 -0
  5. package/dist/assets/DebugDashboard-DBPKvghW.js +1 -0
  6. package/dist/assets/{DesignGalleryView-DaLPPF4Q.js → DesignGalleryView-CzF24Ppi.js} +1 -1
  7. package/dist/assets/{KanbanView-BdsVeyFI.js → KanbanView-DWBGErwm.js} +1 -1
  8. package/dist/assets/MailboxDetailView-CCtq-c8u.js +1 -0
  9. package/dist/assets/OfficeMapPanel-BBKsytG0.js +4 -0
  10. package/dist/assets/ProcessMonitor-pm8Ih7-q.js +1 -0
  11. package/dist/assets/{QueuePanel-BweqXLzT.js → QueuePanel-Lv54tvvk.js} +1 -1
  12. package/dist/assets/{RefreshDebugView-LYcQUNfU.js → RefreshDebugView-0VzJJqhZ.js} +1 -1
  13. package/dist/assets/SddBoardView-Q-k1UjLv.js +2 -0
  14. package/dist/assets/SddFlowGraph-BaWwH9Zc.js +1 -0
  15. package/dist/assets/{SddWizard-C8wm_izV.js → SddWizard-C4wdyhYe.js} +1 -1
  16. package/dist/assets/SessionsDashboard-CveBdrQJ.js +1 -0
  17. package/dist/assets/SkillDetailView-B4U0AtHj.js +2 -0
  18. package/dist/assets/SpecsView-QssXs9kF.js +1 -0
  19. package/dist/assets/{TerminalPanel-DxtPDRk5.js → TerminalPanel-CSDEzJNZ.js} +1 -1
  20. package/dist/assets/{activity-CVWYwjgf.js → activity-3lpGdUIy.js} +1 -1
  21. package/dist/assets/{activity-Cjm0UoMi.js → activity-Bi6wI_Cg.js} +1 -1
  22. package/dist/assets/{activity-BEjzb11U.js → activity-CSPSX--m.js} +1 -1
  23. package/dist/assets/{activity-Lmjx6MD1.js → activity-CcEdxJjx.js} +1 -1
  24. package/dist/assets/{activity-DN6Bmnaw.js → activity-Cv0CmTyM.js} +1 -1
  25. package/dist/assets/{activity-ByEwzh5Q.js → activity-Doy5Yots.js} +1 -1
  26. package/dist/assets/{activity-BjnFo4NL.js → activity-T9kySYDT.js} +1 -1
  27. package/dist/assets/chat--Zry3Atx.js +1 -0
  28. package/dist/assets/chat-0DeGMNFQ.js +1 -0
  29. package/dist/assets/chat-BErfSQjE.js +1 -0
  30. package/dist/assets/chat-CX5cjaC-.js +1 -0
  31. package/dist/assets/chat-CibJIps5.js +1 -0
  32. package/dist/assets/chat-Dr5SJikx.js +1 -0
  33. package/dist/assets/chat-Siz8aWTB.js +1 -0
  34. package/dist/assets/{chat-store-uNXIpAx_.js → chat-store-De7J1E26.js} +1 -1
  35. package/dist/assets/common-B28qhvZh.js +1 -0
  36. package/dist/assets/common-C9ikgX7d.js +1 -0
  37. package/dist/assets/common-CeTsF3jX.js +1 -0
  38. package/dist/assets/common-D29Zdkxb.js +1 -0
  39. package/dist/assets/{common-BAg8_MKg.js → common-DMM-xyW3.js} +1 -1
  40. package/dist/assets/common-DoUBSHRY.js +1 -0
  41. package/dist/assets/common-vdwxzODB.js +1 -0
  42. package/dist/assets/i18n-CkkVJCs9.js +2 -0
  43. package/dist/assets/index-BqAVsSCe.js +154 -0
  44. package/dist/assets/index-DTFjO9oF.css +1 -0
  45. package/dist/assets/local-prefs-C1YdbI5g.js +1 -0
  46. package/dist/assets/{monaco-DqY9Mr3N.js → monaco-XnLmrAXJ.js} +212 -212
  47. package/dist/assets/monaco-theme-Lsd14tM-.js +1 -0
  48. package/dist/assets/{session-store-DhcyG2AC.js → session-store-P-JnaPPK.js} +1 -1
  49. package/dist/assets/{ui-store-isn1eTG7.js → ui-store-FTgDlcX2.js} +1 -1
  50. package/dist/assets/utils-CU_o9ioU.js +1 -0
  51. package/dist/assets/{vendor-OWPFcqeg.js → vendor-CyLvG8N0.js} +14 -14
  52. package/dist/assets/{ws-client-Dglowar-.js → ws-client-lDxBz-7w.js} +1 -1
  53. package/dist/assets/{xyflow-CGx_hPAX.js → xyflow-oYDGUYW6.js} +1 -1
  54. package/dist/index.html +14 -13
  55. package/dist/index.js +7819 -7458
  56. package/dist/index.js.map +1 -1
  57. package/dist/server/index.d.ts +1 -1223
  58. package/dist/server/index.js +3 -12753
  59. package/dist/types.d.ts +17 -1
  60. package/package.json +23 -36
  61. package/dist/assets/AnalyticsDashboard-vbaFTrJa.js +0 -1
  62. package/dist/assets/AutoPhaseView-Ce8a1eUl.js +0 -1
  63. package/dist/assets/CodeEditor-CduSdsMI.js +0 -1
  64. package/dist/assets/DebugDashboard-DoVJhkxa.js +0 -1
  65. package/dist/assets/MailboxDetailView-BYR6CSvz.js +0 -1
  66. package/dist/assets/OfficeMapPanel-BkaZBJEZ.js +0 -4
  67. package/dist/assets/ProcessMonitor-C7OgHnln.js +0 -1
  68. package/dist/assets/SddBoardView-c87Bwdig.js +0 -2
  69. package/dist/assets/SddFlowGraph-DK7RD9NA.js +0 -1
  70. package/dist/assets/SessionsDashboard-DySTssFb.js +0 -1
  71. package/dist/assets/SkillDetailView-y0BgfnBr.js +0 -2
  72. package/dist/assets/SpecsView-BudIVvtN.js +0 -1
  73. package/dist/assets/chat-CdLXf1yN.js +0 -1
  74. package/dist/assets/chat-Cfq81NCh.js +0 -1
  75. package/dist/assets/chat-Cvy-F2d6.js +0 -1
  76. package/dist/assets/chat-DCAGDU3F.js +0 -1
  77. package/dist/assets/chat-DNIePdaQ.js +0 -1
  78. package/dist/assets/chat-DhofDrgA.js +0 -1
  79. package/dist/assets/chat-cxuX4gFa.js +0 -1
  80. package/dist/assets/common-BH1mb9Im.js +0 -1
  81. package/dist/assets/common-C29pCVfm.js +0 -1
  82. package/dist/assets/common-CrGjglH4.js +0 -1
  83. package/dist/assets/common-D7usadq4.js +0 -1
  84. package/dist/assets/common-KzRPhobM.js +0 -1
  85. package/dist/assets/common-tmj0IcQS.js +0 -1
  86. package/dist/assets/i18n-DD-_8vnT.js +0 -2
  87. package/dist/assets/index-0LBBDKsK.js +0 -147
  88. package/dist/assets/index-Dv6bXQoD.css +0 -1
  89. package/dist/assets/utils-B-rhScDN.js +0 -1
  90. package/dist/server/entry.d.ts +0 -2
  91. package/dist/server/entry.js +0 -12763
  92. package/dist/server/entry.js.map +0 -1
  93. package/dist/server/handlers.d.ts +0 -51
  94. package/dist/server/handlers.js +0 -253
  95. package/dist/server/handlers.js.map +0 -1
  96. package/dist/server/index.js.map +0 -1
@@ -1,1223 +1 @@
1
- import { WebSocket } from 'ws';
2
- import { Agent, Context, Logger, EventBus, Provider, Tool, SessionStore, ToolRegistry, ModelsRegistry, ConfigStore, SecretVault, JournalEntry, MemoryStore, PromptLoader, PromptUsageStore, ProviderConfig, ProviderApiKey, SddInterviewDriver, AgentFactory, BrainArbiter, SkillLoader } from '@wrongstack/core';
3
- import * as http from 'node:http';
4
- import { MCPRegistry } from '@wrongstack/mcp';
5
- import { SkillInstaller } from '@wrongstack/core/skills';
6
-
7
- interface AutoPhaseWSMessage {
8
- type: string;
9
- payload?: Record<string, unknown>;
10
- }
11
- /**
12
- * AutoPhaseWebSocketHandler — WebSocket-based AutoPhase control.
13
- *
14
- * Message types:
15
- * autophase.start → { title, phases?, autonomous? }
16
- * autophase.pause → {}
17
- * autophase.resume → {}
18
- * autophase.stop → {}
19
- * autophase.status → {}
20
- * autophase.selectPhase → { phaseId }
21
- * autophase.taskStatus → { taskId, status }
22
- */
23
- declare class AutoPhaseWebSocketHandler {
24
- private agent;
25
- private context;
26
- private logger;
27
- private events?;
28
- private projectRoot?;
29
- private orchestrator;
30
- private graph;
31
- private store;
32
- private clients;
33
- private broadcastInterval;
34
- /** Aborts in-flight task agents AND the planning turn when the run is stopped. */
35
- private abort;
36
- /** Set the instant a stop/clear/revert is requested, so a planning turn that
37
- * resolves afterwards never launches the orchestrator (the abort alone can't
38
- * cover the window between the LLM call resolving and the orchestrator start). */
39
- private stopping;
40
- /** Optional per-phase git-worktree isolation (lazily created at start). */
41
- private worktrees;
42
- /** Base branch + tip SHA captured at run start so a revert can git-revert the
43
- * run's squash commits (history-preserving) instead of a destructive reset. */
44
- private runBase;
45
- /** Per-run worker identities so the board can show "who is on what". */
46
- private usedNicknames;
47
- constructor(agent: Agent, context: Context, logger: Logger, storeDir: string, events?: EventBus | undefined, projectRoot?: string | undefined);
48
- addClient(ws: WebSocket): void;
49
- handleMessage(msg: AutoPhaseWSMessage): Promise<void>;
50
- private handleStart;
51
- /**
52
- * Halt the run NOW — at any phase. Sets `stopping` (so a planning turn that
53
- * resolves afterwards bails), aborts in-flight agents, stops the orchestrator
54
- * tick, and ends the live broadcast. The board is kept for review; use
55
- * `autophase.clear` to reset or `autophase.revert` to undo the changes.
56
- */
57
- private handleStop;
58
- /**
59
- * Stop + wipe: tear down phase worktrees and reset to an empty board so the UI
60
- * returns to the start screen ("new one"). Does NOT touch already-merged commits
61
- * on the base branch — that is `autophase.revert`.
62
- */
63
- private handleClear;
64
- /**
65
- * Stop + undo: remove phase worktrees, then history-preservingly `git revert`
66
- * every commit this run landed on the base branch (captured `runBase`..HEAD),
67
- * then reset to an empty board. Refuses (reports a reason) on a dirty tree or a
68
- * conflicting revert rather than leaving the tree half-reverted.
69
- */
70
- private handleRevert;
71
- /** Generic fallback phases when the LLM planner produces nothing usable. */
72
- private defaultPhases;
73
- /** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
74
- * The caller passes the run's abort signal so a stop during planning cancels
75
- * the LLM turn (the previous fresh, never-aborted controller made planning
76
- * uninterruptible). */
77
- private planPhases;
78
- private executeTaskWithAgent;
79
- /** Persist + broadcast after an interactive board mutation. */
80
- private afterBoardMutation;
81
- private handleTaskStatusChange;
82
- private startBroadcast;
83
- private stopBroadcast;
84
- private broadcastState;
85
- private buildState;
86
- private sendState;
87
- private broadcast;
88
- private send;
89
- }
90
-
91
- /**
92
- * Context-aware editor completion for the WebUI Monaco surface.
93
- *
94
- * The handler combines fast symbol-index hits with a short, JSON-only LLM call.
95
- * It is intentionally side-effect free: it never writes files and only reads the
96
- * existing codebase index when available.
97
- */
98
-
99
- type CompletionItemKind = 'text' | 'method' | 'function' | 'constructor' | 'field' | 'variable' | 'class' | 'interface' | 'module' | 'property' | 'unit' | 'value' | 'enum' | 'keyword' | 'snippet' | 'file' | 'reference';
100
- interface CompletionSuggestion {
101
- label: string;
102
- insertText: string;
103
- kind?: CompletionItemKind | undefined;
104
- detail?: string | undefined;
105
- documentation?: string | undefined;
106
- sortText?: string | undefined;
107
- source?: 'llm' | 'index' | 'lsp' | undefined;
108
- }
109
- interface CompletionHandlerOptions {
110
- projectRoot: string;
111
- provider?: Provider | undefined;
112
- model?: string | undefined;
113
- indexDir?: string | undefined;
114
- lspCompletion?: LspCompletionSource | undefined;
115
- timeoutMs?: number | undefined;
116
- }
117
- interface LspCompletionSourceRequest {
118
- filePath: string;
119
- lineNumber: number;
120
- column: number;
121
- content?: string | undefined;
122
- triggerCharacter?: string | undefined;
123
- signal: AbortSignal;
124
- }
125
- type LspCompletionSource = (request: LspCompletionSourceRequest) => Promise<CompletionSuggestion[]>;
126
- declare function handleCompletionRequest(ws: WebSocket, msg: unknown, opts: CompletionHandlerOptions): Promise<void>;
127
- declare function createToolLspCompletionSource(tool: Tool | undefined, ctx: Context): LspCompletionSource | undefined;
128
-
129
- /**
130
- * Custom context modes — user-defined presets that are loaded from disk,
131
- * merged with the built-in modes, and managed via WebSocket CRUD handlers.
132
- *
133
- * Stored in: ~/.wrongstack/custom-context-modes.json
134
- * Format: { "modes": ContextWindowMode[] }
135
- */
136
- interface CustomContextMode {
137
- id: string;
138
- name: string;
139
- description: string;
140
- thresholds: {
141
- warn: number;
142
- soft: number;
143
- hard: number;
144
- };
145
- aggressiveOn: string;
146
- preserveK: number;
147
- eliseThreshold: number;
148
- targetLoad: number;
149
- /** Whether this is a user-defined (custom) or built-in mode. */
150
- custom: boolean;
151
- }
152
- interface CustomModeStore {
153
- modes: Map<string, CustomContextMode>;
154
- load: () => Promise<void>;
155
- save: () => Promise<void>;
156
- create: (mode: CustomContextMode) => {
157
- ok: boolean;
158
- error?: string | undefined;
159
- };
160
- update: (id: string, patch: Partial<CustomContextMode>) => {
161
- ok: boolean;
162
- error?: string | undefined;
163
- };
164
- remove: (id: string) => {
165
- ok: boolean;
166
- error?: string | undefined;
167
- };
168
- list: () => CustomContextMode[];
169
- }
170
- declare function createCustomModeStore(wrongstackDir: string): CustomModeStore;
171
-
172
- /**
173
- * Shared Design Studio WebSocket handlers for both the standalone WebUI server
174
- * (`packages/webui/src/server/index.ts`) and the CLI's `--webui` embedded
175
- * server (`packages/cli/src/webui-server.ts`). One source of truth keeps the two
176
- * servers at parity (enforced by ws-handler-parity.test.ts).
177
- *
178
- * case 'design.list': return handleDesignList(ws, designCtx);
179
- * case 'design.use': return handleDesignUse(ws, designCtx, msg);
180
- * case 'design.state': return handleDesignState(ws, designCtx);
181
- * case 'design.set': return handleDesignSet(ws, designCtx, msg);
182
- * case 'design.materialize': return handleDesignMaterialize(ws, designCtx, msg);
183
- *
184
- * Browsing + customization of curated UI design kits; `design.use` pins the
185
- * active kit, `design.set` records color/token overrides, `design.materialize`
186
- * writes the (override-applied) tokens to a real theme file on disk.
187
- */
188
-
189
- interface DesignContext {
190
- projectRoot: string;
191
- /** Live agent context whose `meta.designStudio` we read/pin. Optional. */
192
- agentMeta?: {
193
- meta: Record<string, unknown>;
194
- } | undefined;
195
- }
196
- declare function handleDesignList(ws: WebSocket, ctx: DesignContext): Promise<void>;
197
- declare function handleDesignState(ws: WebSocket, ctx: DesignContext): Promise<void>;
198
- declare function handleDesignUse(ws: WebSocket, ctx: DesignContext, msg: {
199
- payload?: unknown;
200
- }): Promise<void>;
201
- /** Record structured color/token overrides without changing the pinned kit. */
202
- declare function handleDesignSet(ws: WebSocket, ctx: DesignContext, msg: {
203
- payload?: unknown;
204
- }): Promise<void>;
205
- /** Write the active kit's (override-applied) tokens to a real theme file. */
206
- declare function handleDesignMaterialize(ws: WebSocket, ctx: DesignContext, msg: {
207
- payload?: unknown;
208
- }): Promise<void>;
209
- /** Scan project UI files for off-palette colors against the active kit. */
210
- declare function handleDesignVerify(ws: WebSocket, ctx: DesignContext): Promise<void>;
211
-
212
- interface WSServerMessage {
213
- type: string;
214
- payload: unknown;
215
- }
216
- interface WSClientMessage {
217
- type: string;
218
- payload?: unknown | undefined;
219
- }
220
- interface WebUIOptions {
221
- /** HTTP frontend port. Prefer `httpPort`; `port` is kept for compatibility. */
222
- port?: number | undefined;
223
- /** HTTP frontend port. */
224
- httpPort?: number | undefined;
225
- webuiPort?: number | undefined;
226
- /** WebSocket backend port. */
227
- wsPort?: number | undefined;
228
- /** Host/interface to bind. */
229
- wsHost?: string | undefined;
230
- /** Fixed access token/password. Defaults to WEBUI_TOKEN or random per process. */
231
- accessToken?: string | undefined;
232
- /** Browser-facing HTTP URL, used in startup output and instance registry. */
233
- publicUrl?: string | undefined;
234
- /** Browser-facing WebSocket URL, injected into the frontend for tunnels/proxies. */
235
- publicWsUrl?: string | undefined;
236
- /** Force token/password protection even on loopback binds. */
237
- requireToken?: boolean | undefined;
238
- /**
239
- * Pre-built backend services. When provided, `startWebUI` skips its
240
- * default agent/event-bus/session/store construction and wires the
241
- * supplied instances into the WS message router and HTTP API
242
- * handlers instead.
243
- *
244
- * Intended for callers (most notably `cli/webui-server.ts`) that
245
- * already own the agent lifecycle — the CLI's `runWebUI` constructs
246
- * the Agent, EventBus, SessionStore, and friends so it can run an
247
- * eternal iteration against them, then hands the lot to the webui
248
- * for the human-facing surface.
249
- *
250
- * `session` is typed as `SessionStore` (read + write) rather than
251
- * the narrower `SessionWriter` because `startWebUI` needs to
252
- * `load()` existing session history to project the chat view, and
253
- * `list()` past sessions to populate the sessions dashboard. A
254
- * `SessionWriter`-only field would force `startWebUI` to take a
255
- * separate `sessionStore` for reads, which is a worse API for the
256
- * CLI caller (`runWebUI` already has one store, not two).
257
- *
258
- * When `services` is omitted, `startWebUI` retains its existing
259
- * behavior (builds the defaults in-place). This keeps the standalone
260
- * `node dist/index.js webui` flow fully back-compatible.
261
- */
262
- services?: BackendServices | undefined;
263
- /**
264
- * Subscribe to live per-iteration events from the eternal-autonomy
265
- * engine. When provided, `startWebUI` wires a WS broadcast that
266
- * pushes each `JournalEntry` to every connected client. Observability
267
- * only — starting the loop still goes through REPL/TUI or `--eternal`,
268
- * since the webui has no slash-command dispatch surface yet.
269
- *
270
- * The argument is a *function* the caller supplies that performs the
271
- * actual subscription; the returned disposer is invoked on
272
- * `shutdown()`. This indirection lets the caller (most commonly
273
- * `cli/webui-server.ts`) own the engine lifecycle and merely hand the
274
- * webui an observer slot.
275
- */
276
- subscribeEternalIteration?: ((fn: (entry: JournalEntry) => void) => () => void) | undefined;
277
- }
278
- interface BackendServices {
279
- agent: Agent;
280
- events: EventBus;
281
- session: SessionStore;
282
- toolRegistry: ToolRegistry;
283
- modelsRegistry: ModelsRegistry;
284
- configStore: ConfigStore;
285
- vault: SecretVault;
286
- globalConfigPath: string;
287
- projectRoot: string;
288
- }
289
- interface ConnectedClient {
290
- ws: WebSocket;
291
- sessionId: string | null;
292
- connectedAt: number;
293
- /** Unique per-connection id — used to key per-connection state (e.g. the
294
- * rate-limit bucket) so distinct browser tabs that share the same
295
- * `sessionId` do not collide, and so the entry is reliably removable on
296
- * close (`String(ws)` is `"[object Object]"` for every socket). */
297
- connId: string;
298
- }
299
-
300
- type EternalSubscribe = (fn: (entry: JournalEntry) => void) => () => void;
301
- type EternalBroadcast<C> = (clients: Map<WebSocket, C>, msg: WSServerMessage) => void;
302
- interface EternalSubscription {
303
- /** Tear down the underlying engine subscription. Idempotent. */
304
- dispose: () => void;
305
- }
306
- declare function createEternalSubscription<C>(subscribe: EternalSubscribe, broadcast: EternalBroadcast<C>, clientsRef: () => Map<WebSocket, C>): EternalSubscription;
307
-
308
- /**
309
- * Shared file-operation WebSocket handlers for both the standalone WebUI
310
- * server and the CLI's `--webui` embedded server. Extracted from the
311
- * duplicated switch cases in `index.ts` and `cli/src/webui-server.ts`.
312
- *
313
- * Each function handles the full request→response cycle for one message
314
- * type. Callers drop them into their switch statement:
315
- *
316
- * case 'files.tree': return handleFilesTree(ws, msg, projectRoot);
317
- */
318
-
319
- interface FilesWriteOptions {
320
- onWritten?: ((filePath: string) => void | Promise<void>) | undefined;
321
- }
322
- /**
323
- * Build and send a nested directory tree for the File Explorer.
324
- *
325
- * Walks `projectRoot` to depth 10 max, skipping heavyweight dirs
326
- * (node_modules, .git, dist, …) and dot-entries. Responds with
327
- * `{ type: 'files.tree', payload: { root, tree } }`.
328
- */
329
- declare function handleFilesTree(ws: WebSocket, msg: unknown, projectRoot: string): Promise<void>;
330
- /**
331
- * Read a file's content for the Monaco editor.
332
- *
333
- * Guards against path traversal (`../` escapes). Responds with
334
- * `{ type: 'files.read', payload: { filePath, content } }`.
335
- */
336
- declare function handleFilesRead(ws: WebSocket, msg: unknown, projectRoot: string): Promise<void>;
337
- /**
338
- * Write file content back to disk (atomic write via tmp + rename).
339
- *
340
- * Guards against path traversal. Responds with
341
- * `{ type: 'files.written', payload: { filePath, success } }`.
342
- */
343
- declare function handleFilesWrite(ws: WebSocket, msg: unknown, projectRoot: string, opts?: FilesWriteOptions): Promise<void>;
344
- /**
345
- * Lightweight project file picker for the chat `@` mention popup.
346
- *
347
- * Walks `projectRoot` (max depth 8), skipping hidden and heavyweight
348
- * dirs, then fuzzy-ranks results against `query`. Responds with
349
- * `{ type: 'files.list', payload: { files } }`.
350
- */
351
- declare function handleFilesList(ws: WebSocket, msg: unknown, projectRoot: string): Promise<void>;
352
-
353
- /**
354
- * Shared `git.info` WebSocket handler for both the standalone WebUI server and
355
- * the CLI's `--webui` embedded server. Extracted from the duplicated switch
356
- * cases in `index.ts` and `cli/src/webui-server.ts`, which had drifted (the
357
- * standalone copy transposed ahead/behind and never matched deletions). One
358
- * implementation here keeps both surfaces in lockstep.
359
- *
360
- * case 'git.info': return handleGitInfo(ws, projectRoot);
361
- */
362
-
363
- /**
364
- * Read git branch, change stats, and upstream sync status from `projectRoot`
365
- * and broadcast a `git.info` message. Never throws — a non-repo / missing-git
366
- * directory yields an empty-but-valid payload.
367
- */
368
- declare function handleGitInfo(ws: WebSocket, projectRoot: string): Promise<void>;
369
- /**
370
- * Read the working-tree change set (everything that differs from HEAD:
371
- * staged, unstaged, and untracked) and broadcast a `git.changes` message.
372
- *
373
- * The file list comes from `git status --porcelain -z` (NUL-delimited so
374
- * paths with spaces/unicode survive intact, and renames are unambiguous).
375
- * Per-file line counts come from `--numstat` of both the unstaged and the
376
- * staged diff, summed. Untracked files intentionally report 0/0 here so the
377
- * list view does not read every untracked file; `git.diff` loads a selected
378
- * file lazily on demand.
379
- * Never throws — a non-repo yields an empty list.
380
- */
381
- declare function handleGitChanges(ws: WebSocket, projectRoot: string): Promise<void>;
382
- /**
383
- * Resolve the before/after text for a single file and broadcast a `git.diff`
384
- * message. `oldText` is the file at HEAD (`git show HEAD:<path>`), `newText`
385
- * is the current working-tree content. New/untracked files have empty
386
- * `oldText`; deleted files have empty `newText`. Binary or oversized files
387
- * are reported with a flag instead of content so the client can show a notice.
388
- */
389
- declare function handleGitDiff(ws: WebSocket, projectRoot: string, path: string): Promise<void>;
390
-
391
- /** Metrics for the file watcher that watches status.json files. */
392
- interface FileWatcherMetrics {
393
- /** Number of status.json filesystem events detected after filename filtering. */
394
- fileChangesDetected: number;
395
- filesProcessed: number;
396
- broadcastsSent: number;
397
- debounceResets: number;
398
- totalDebounceDelayMs: number;
399
- activeProjects: number;
400
- /** Average debounce delay in ms across all broadcasts. */
401
- averageDebounceDelayMs: number;
402
- /** Whether the file watcher is currently active. */
403
- watcherActive: boolean;
404
- }
405
-
406
- interface CreateHttpServerOptions {
407
- /** Port to listen on. Defaults to 3456 (or the `PORT` env var). */
408
- port?: number | undefined;
409
- /** Host/interface to bind. Typically the loopback for the WebUI. */
410
- host: string;
411
- /** Resolved path to the directory containing the built React assets. */
412
- distDir: string;
413
- /**
414
- * WS port — appears in the CSP `connect-src` directive so the browser
415
- * is allowed to open a WebSocket back to the local server.
416
- */
417
- wsPort: number;
418
- /**
419
- * Public WebSocket URL injected into the frontend. Use this behind tunnels or
420
- * reverse proxies where the browser-facing WS URL differs from host:wsPort.
421
- */
422
- publicWsUrl?: string | undefined;
423
- /**
424
- * Path to the global WrongStack root (~/.wrongstack). Used by the
425
- * /api/sessions and /api/sessions/:id/agents endpoints to read the
426
- * cross-process SessionRegistry.
427
- */
428
- globalRoot?: string | undefined;
429
- /**
430
- * Shared auth token for HTTP and WS access. Required for non-loopback
431
- * binds (LAN exposure). Loopback binds accept local browser access without
432
- * a token (the WS path's loopback-bootstrap policy — see ws-auth.ts).
433
- */
434
- apiToken?: string | undefined;
435
- /** Force HTTP token auth even on loopback binds, useful behind public tunnels. */
436
- requireToken?: boolean | undefined;
437
- /**
438
- * If true, the `/ws-auth` endpoint exchanges a `?token=` query param (or
439
- * `X-WS-Token` header) for an `HttpOnly` auth cookie. The cookie is then
440
- * sent automatically on the WS upgrade, closing the C-598 query-string
441
- * token exposure class. Default: true. Set to false to keep the legacy
442
- * URL-token-only flow (e.g. in tests that don't want cookie state).
443
- */
444
- enableWsCookie?: boolean | undefined;
445
- /**
446
- * Optional file watcher metrics object. When provided, the
447
- * /debug/watcher-metrics endpoint will be enabled to expose these metrics.
448
- */
449
- watcherMetrics?: FileWatcherMetrics | undefined;
450
- /**
451
- * Push-on-write hook. `POST /api/fleet/ping` (loopback only) invokes this to
452
- * trigger an immediate fleet re-broadcast, so a TUI/REPL's registry write
453
- * reaches the map without waiting on the file-watch/poll. Best-effort.
454
- */
455
- onFleetPing?: (() => void) | undefined;
456
- }
457
- /**
458
- * Inject the live WS port into the served HTML so the frontend connects to
459
- * THIS instance's backend instead of a hardcoded default. Enables running
460
- * several WebUI instances simultaneously on different PORT/WS_PORT pairs
461
- * (e.g. one per project) — each instance serves HTML stamped with its own
462
- * WS port.
463
- *
464
- * A `<meta>` tag is used deliberately rather than an inline `<script>`: the
465
- * CSP sets `script-src 'self'`, which would block an inline script, but meta
466
- * tags are not subject to script-src. The frontend reads
467
- * `meta[name="wrongstack-ws-port"]` (see ws-client.ts `defaultWsUrl`).
468
- */
469
- declare function injectWsPort(html: string, wsPort: number): string;
470
- /** Build the Content-Security-Policy value for the given WS port. */
471
- declare function buildCspHeader(wsPort: number, requestHost?: string | undefined, publicWsUrl?: string | undefined): string;
472
- /**
473
- * Create the static-file HTTP server. Returns the `http.Server` (not
474
- * listening yet) so the caller can attach to a `shutdown()` hook and
475
- * coordinate the listen() with the WebSocket bootstrap.
476
- */
477
- declare function createHttpServer(opts: CreateHttpServerOptions): http.Server;
478
-
479
- /**
480
- * Running-instance registry for the standalone WebUI server.
481
- *
482
- * Every live `wstackui` process records itself in a single JSON file under the
483
- * wstack home dir (`~/.wrongstack/webui-instances.json`) so a user running
484
- * several instances (one per project, or several per project on different
485
- * ports) can see at a glance which ports are open for which path.
486
- *
487
- * Design notes:
488
- * - **Self-healing**: every register/unregister/list prunes entries whose PID
489
- * is no longer alive (`process.kill(pid, 0)`), so a crashed instance that
490
- * never got to unregister doesn't leave a ghost behind.
491
- * - **Atomic writes**: the file is rewritten via `atomicWrite` (tmp + rename),
492
- * so a concurrent reader never sees a half-written file. Two instances
493
- * starting at the *exact* same millisecond could still race the
494
- * read-modify-write — acceptable for a best-effort tracking file, and the
495
- * next register() heals any dropped entry.
496
- * - **Best-effort**: a failure to read/write the registry must NEVER take the
497
- * server down. Callers wrap these in `.catch()`.
498
- */
499
- /** One running WebUI process. */
500
- interface WebUIInstanceRecord {
501
- /** OS process id — also the liveness key. */
502
- pid: number;
503
- /** HTTP port serving the React frontend. */
504
- httpPort: number;
505
- /** WebSocket port for the agent backend. */
506
- wsPort: number;
507
- /** Bind host (e.g. 127.0.0.1 or 0.0.0.0). */
508
- host: string;
509
- /** Absolute project root the instance booted against. */
510
- projectRoot: string;
511
- /** Display name (basename of projectRoot). */
512
- projectName: string;
513
- /** ISO timestamp when the instance registered. */
514
- startedAt: string;
515
- /** Convenience open-in-browser URL. */
516
- url: string;
517
- }
518
- /** Default wstack home dir (`~/.wrongstack`). Callers may override the base. */
519
- declare function defaultBaseDir(): string;
520
- /** Resolve the registry file path for a given base dir. */
521
- declare function registryPath(baseDir?: string): string;
522
- /**
523
- * Register (or refresh) this instance. Prunes dead entries and any stale entry
524
- * for our own PID before adding the current record. Best-effort — rejects only
525
- * on a hard fs error, which callers swallow.
526
- */
527
- declare function registerInstance(record: WebUIInstanceRecord, baseDir?: string): Promise<void>;
528
- /** Remove this instance (called on graceful shutdown). Also prunes dead pids. */
529
- declare function unregisterInstance(pid: number, baseDir?: string): Promise<void>;
530
- /** List live instances, pruning any dead entries encountered. */
531
- declare function listInstances(baseDir?: string): Promise<WebUIInstanceRecord[]>;
532
- /** Human-readable table of running instances for `wstackui --list`. */
533
- declare function formatInstances(instances: WebUIInstanceRecord[]): string;
534
-
535
- /**
536
- * MCP management handlers for the WebUI server (both the standalone
537
- * `wstackui` server and the CLI's embedded `--webui` server).
538
- *
539
- * These are thin WebSocket translators over the shared, surface-agnostic
540
- * management core in `@wrongstack/mcp` (`manage.ts`) — the SAME core the REPL
541
- * `/mcp` command writes against (same config.json, same MCPRegistry). All the
542
- * config IO, url/header persistence, and live registry start/stop logic lives
543
- * there; here we only map structured results to WS events the browser expects.
544
- */
545
-
546
- /** mcp.list — configured servers merged with live registry status + tools. */
547
- declare function handleMcpList(ws: WebSocket, _msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
548
- /** mcp.add — persist a new server (incl. url/headers) and start it if enabled. */
549
- declare function handleMcpAdd(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
550
- /** mcp.update — re-persist config (incl. url/headers) and re-apply to registry. */
551
- declare function handleMcpUpdate(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
552
- /** mcp.remove — stop the server and delete it from config. */
553
- declare function handleMcpRemove(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
554
- /** mcp.enable — flip enabled:true in config and start the server. */
555
- declare function handleMcpEnable(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
556
- /** mcp.disable — stop the server and flip enabled:false in config. */
557
- declare function handleMcpDisable(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
558
- /** mcp.sleep — stop a running server (config stays enabled). */
559
- declare function handleMcpSleep(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
560
- /** mcp.wake — restart a sleeping/stopped server from config. */
561
- declare function handleMcpWake(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
562
- /** mcp.restart — stop + start a server. */
563
- declare function handleMcpRestart(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
564
- /** mcp.discover — ensure the server is running and report its live tools. */
565
- declare function handleMcpDiscover(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
566
-
567
- /**
568
- * Shared memory-operation WebSocket handlers for both the standalone WebUI
569
- * server and the CLI's `--webui` embedded server. Extracted from the
570
- * duplicated switch cases in `index.ts` and `cli/src/webui-server.ts`.
571
- *
572
- * Each function handles the full request→response cycle for one message
573
- * type. Callers drop them into their switch statement:
574
- *
575
- * case 'memory.list': return handleMemoryList(ws, memoryStore);
576
- */
577
-
578
- /**
579
- * List all memory entries across all scopes.
580
- * Responds with `{ type: 'memory.list', payload: { text } }`.
581
- */
582
- declare function handleMemoryList(ws: WebSocket, memoryStore: MemoryStore): Promise<void>;
583
- /**
584
- * Persist a new memory entry.
585
- * Responds with `{ type: 'key.operation_result', payload: { success, message } }`.
586
- */
587
- declare function handleMemoryRemember(ws: WebSocket, msg: unknown, memoryStore: MemoryStore): Promise<void>;
588
- /**
589
- * Remove memory entries matching the given text.
590
- * Responds with `{ type: 'key.operation_result', payload: { success, message } }`.
591
- */
592
- declare function handleMemoryForget(ws: WebSocket, msg: unknown, memoryStore: MemoryStore): Promise<void>;
593
-
594
- /**
595
- * Best-effort "open this URL in the default browser" for `--webui --open`.
596
- *
597
- * Cross-platform via the OS opener (`start` / `open` / `xdg-open`). Fully
598
- * fire-and-forget: a missing opener, a headless box, or a spawn failure must
599
- * NEVER take the server down — the URL is always also printed to the console.
600
- */
601
- /** Resolve the platform's URL-opener command + args. */
602
- declare function browserOpenCommand(url: string, platform?: NodeJS.Platform): {
603
- command: string;
604
- args: string[];
605
- };
606
- /** Spawn the OS browser-opener for `url` and register it as a protected
607
- * process so it survives kill/killAll. Never throws. */
608
- declare function openBrowser(url: string, platform?: NodeJS.Platform): void;
609
-
610
- /**
611
- * Free-port discovery for the standalone WebUI server.
612
- *
613
- * When a user runs several instances, the default ports (HTTP 3456 / WS 3457)
614
- * are taken by the first one. Rather than make the user hand-pick `PORT` /
615
- * `WS_PORT` for every extra instance, the server probes upward from the
616
- * requested port and binds the first free one — then stamps that real port into
617
- * the served HTML and the instance registry so everything stays consistent.
618
- *
619
- * The probe binds a throwaway `net.Server`, then closes it, so there is a tiny
620
- * TOCTOU window between "found free" and "the real server binds it". For local
621
- * single-user multi-instance use that race is negligible; if it ever loses, the
622
- * real bind fails loudly with EADDRINUSE exactly as before.
623
- */
624
- /** Resolve true when `port` can be bound on `host`, false on EADDRINUSE/EACCES. */
625
- declare function isPortFree(host: string, port: number): Promise<boolean>;
626
- interface FindFreePortOptions {
627
- /** Ports to skip even if free (e.g. one already chosen for the sibling server). */
628
- exclude?: Set<number> | undefined;
629
- /** How many consecutive ports to try before giving up. Default 200. */
630
- maxTries?: number | undefined;
631
- }
632
- /**
633
- * Find the first free port at or above `startPort` on `host`, skipping any in
634
- * `exclude`. Throws if nothing is free within `maxTries` steps.
635
- */
636
- declare function findFreePort(host: string, startPort: number, opts?: FindFreePortOptions): Promise<number>;
637
-
638
- /**
639
- * Send a JSON message to a single WebSocket client.
640
- * No-op when the socket is not in OPEN state (disconnected / closing).
641
- */
642
- declare function send(ws: WebSocket, msg: object): void;
643
- /**
644
- * Broadcast a JSON message to every connected client.
645
- * Swallows per-socket send errors — a client that disconnected between the
646
- * readyState check and `ws.send()` is cleaned up by its own `close` handler.
647
- */
648
- declare function broadcast(clients: Map<WebSocket, ConnectedClient>, msg: object): void;
649
- /**
650
- * Send a success/failure result message (used by key.* and provider.* handlers).
651
- * The frontend expects `key.operation_result` with `{ success, message }`.
652
- */
653
- declare function sendResult(ws: WebSocket, success: boolean, message: string): void;
654
- /**
655
- * Extract a human-readable message from an unknown thrown value.
656
- */
657
- declare function errMessage(err: unknown): string;
658
- /**
659
- * Generate a cryptographically random WebSocket auth token (hex string).
660
- * Shared between standalone and CLI-embedded WebUI servers.
661
- */
662
- declare function generateAuthToken(): string;
663
- declare function resolveAuthToken(explicit?: string | undefined): string;
664
- declare function hostForBrowserUrl(bindHost: string): string;
665
- declare function buildWebUIAccessUrl(opts: {
666
- host: string;
667
- port: number;
668
- token?: string | undefined;
669
- protocol?: 'http' | 'https' | undefined;
670
- publicUrl?: string | undefined;
671
- }): string;
672
- declare function envFlag(name: string): boolean;
673
-
674
- /**
675
- * Shared prompt-library WebSocket handlers for BOTH the standalone WebUI server
676
- * (`packages/webui/src/server/index.ts`) and the CLI's `--webui` embedded server
677
- * (`packages/cli/src/webui-server.ts`). One source of truth so the two servers
678
- * never drift (the lesson from skills-handlers).
679
- *
680
- * Each function handles one request→response cycle; callers drop them into their
681
- * switch:
682
- *
683
- * case 'prompts.search': return handlePromptsSearch(ws, promptsCtx, msg);
684
- *
685
- * The prompt library is read across three layers (builtin + user + project) by
686
- * the injected `PromptLoader`; writes (create/favorite) go to the user layer
687
- * with copy-on-write for builtins. Treat synced/builtin content as DATA — these
688
- * handlers never execute it; the client inserts a chosen prompt into the chat
689
- * input as an ordinary user turn.
690
- */
691
-
692
- interface PromptsContext {
693
- /** Backs all prompt ops. Absent ⇒ feature unavailable. */
694
- promptLoader: PromptLoader | undefined;
695
- /** Records per-slug insert counts (shared with CLI `/prompt recent`). */
696
- promptUsage?: PromptUsageStore | undefined;
697
- }
698
- declare function handlePromptsList(ws: WSLike, ctx: PromptsContext): Promise<void>;
699
- declare function handlePromptsSearch(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
700
- declare function handlePromptsContent(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
701
- declare function handlePromptsFavorite(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
702
- declare function handlePromptsCreate(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
703
- /** Record that a prompt was inserted (best-effort; feeds CLI `/prompt recent`). */
704
- declare function handlePromptsUsed(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
705
- /** Return recently-inserted prompt slugs (most-recent first) for the modal's Recent view. */
706
- declare function handlePromptsRecent(ws: WSLike, ctx: PromptsContext): Promise<void>;
707
- /** Minimal structural type for the ws.send sink (matches `ws` WebSocket). */
708
- type WSLike = Parameters<typeof send>[0];
709
-
710
- /**
711
- * Read the `providers` section from the global config, decrypting
712
- * secret-bearing fields. Returns an empty record when the config file
713
- * doesn't exist or has no `providers` key.
714
- */
715
- declare function loadSavedProviders(configPath: string, vault: SecretVault): Promise<Record<string, ProviderConfig>>;
716
- /**
717
- * Write `providers` back into the global config, encrypting secrets first.
718
- * Refuses to overwrite a corrupt-but-existing config file (the operator
719
- * should fix it manually). When the config file is missing (ENOENT), starts
720
- * from an empty object.
721
- */
722
- declare function saveProviders(configPath: string, vault: SecretVault, providers: Record<string, ProviderConfig>): Promise<void>;
723
- /**
724
- * Small helper for the standalone WebUI entry point: create a
725
- * `{ load, save }` pair from a config path alone (uses the
726
- * config-directory-relative `.key` file for the vault). The `--webui`
727
- * CLI mode and the standalone server both need to read/write the
728
- * `providers` map identically.
729
- */
730
- declare function createProviderConfigIO(configPath: string): {
731
- load: () => Promise<Record<string, ProviderConfig>>;
732
- save: (providers: Record<string, ProviderConfig>) => Promise<void>;
733
- };
734
-
735
- /**
736
- * Pure provider/API-key record transforms for the WebUI server's `key.*` and
737
- * `provider.*` WebSocket handlers.
738
- *
739
- * These operate on an in-memory `providers` record (the decrypted
740
- * `config.providers` map) and return a `{ ok, message }` result mirroring the
741
- * status string the handler sends back to the client. All persistence
742
- * (load/decrypt, encrypt/atomic-write) and WS messaging stays in `index.ts` —
743
- * keeping this layer pure means the security-sensitive key bookkeeping (which
744
- * key is active, when a provider is dropped, how legacy single-key configs are
745
- * normalized) is unit-testable without a vault or a socket.
746
- *
747
- * Extracted from `index.ts`; transforms mutate the passed record in place, the
748
- * same way the original handlers did before calling `saveProviders`.
749
- */
750
-
751
- type ProvidersRecord = Record<string, ProviderConfig>;
752
- interface KeyOpResult {
753
- ok: boolean;
754
- message: string;
755
- }
756
- /**
757
- * Normalize a provider's keys to the array form, upgrading a legacy single
758
- * `apiKey` string to a one-element `[{ label: 'default', ... }]` list. Returns
759
- * fresh copies so callers can mutate without aliasing the stored config.
760
- */
761
- declare function normalizeKeys(cfg: ProviderConfig): ProviderApiKey[];
762
- /**
763
- * Write a normalized key list back onto a provider config: drop all key fields
764
- * when empty, otherwise sync `apiKeys` and re-point `activeKey` if it no longer
765
- * names a present key. Does NOT mirror the plaintext key to the legacy `apiKey`
766
- * field — that would leak the secret on accidental serialization. Consumers
767
- * that need the real key should read from `apiKeys[]` directly.
768
- */
769
- declare function writeKeysBack(cfg: ProviderConfig, keys: ProviderApiKey[]): void;
770
- /** Mask a secret for display: `••••` for short keys, `abcd…wxyz` otherwise. */
771
- declare function maskedKey(key: string | undefined): string;
772
- /** Add or replace a labeled key for a provider, creating the provider if new. */
773
- declare function upsertKey(providers: ProvidersRecord, providerId: string, label: string, apiKey: string, nowIso: string): KeyOpResult;
774
- /** Remove a labeled key; drops the provider entirely when its last key goes. */
775
- declare function deleteKey(providers: ProvidersRecord, providerId: string, label: string): KeyOpResult;
776
- /** Point a provider's active key at the given label. */
777
- declare function setActiveKey(providers: ProvidersRecord, providerId: string, label: string): KeyOpResult;
778
- /** Register a brand-new provider (optionally with an initial `default` key). */
779
- declare function addProvider(providers: ProvidersRecord, payload: {
780
- id: string;
781
- family: string;
782
- baseUrl?: string | undefined;
783
- apiKey?: string | undefined;
784
- }, nowIso: string): KeyOpResult;
785
- /** Remove an entire provider and all its keys. */
786
- declare function removeProvider(providers: ProvidersRecord, providerId: string): KeyOpResult;
787
-
788
- interface SddBoardWSMessage {
789
- type: string;
790
- payload?: Record<string, unknown>;
791
- }
792
- /** Project paths the handler needs to apply lifecycle ops directly. */
793
- interface SddBoardLifecycleDeps {
794
- projectRoot: string;
795
- paths: {
796
- projectSpecs: string;
797
- projectTaskGraphs: string;
798
- projectSddSession: string;
799
- projectSddBoards: string;
800
- };
801
- }
802
- /**
803
- * SddBoardWebSocketHandler — streams the live SDD multi-agent board to clients
804
- * and relays control commands back to the CLI-owned run.
805
- *
806
- * Two observe modes (one class, shared by both webui servers):
807
- * • in-process (CLI-hosted): subscribe the shared EventBus `sdd.board.snapshot`
808
- * for instant updates;
809
- * • standalone (separate process): poll the on-disk snapshot store (the CLI
810
- * run persists JSON every change).
811
- *
812
- * Control is uniform + cross-process: every command is appended to the run's
813
- * `<runId>.control.jsonl`, which the CLI run drains and applies — so the run
814
- * stays the single driver and nothing races on shared state.
815
- */
816
- declare class SddBoardWebSocketHandler {
817
- private readonly store;
818
- private readonly clients;
819
- private readonly lifecycle?;
820
- private latest;
821
- private poll;
822
- private unsub;
823
- constructor(boardsDir: string, events?: EventBus, lifecycle?: SddBoardLifecycleDeps);
824
- addClient(ws: WebSocket): void;
825
- handleMessage(msg: SddBoardWSMessage): Promise<void>;
826
- /**
827
- * Apply a cleanup/rollback/destroy from disk and broadcast a structured
828
- * `sdd.board.lifecycle_result`. Refuses (no-op) while a run is still active —
829
- * the user must stop it first; the UI gates the buttons on `!active` and the
830
- * Destroy flow auto-stops then waits before sending `destroy`.
831
- */
832
- private applyLifecycle;
833
- dispose(): void;
834
- private pollLatest;
835
- private sendCurrent;
836
- private broadcastCurrent;
837
- private loadLatestFromDisk;
838
- private broadcast;
839
- private send;
840
- }
841
-
842
- interface WizardMessage {
843
- type: string;
844
- payload?: Record<string, unknown>;
845
- }
846
- /**
847
- * Dependencies each webui server supplies. The handler is deliberately
848
- * agent-agnostic: every surface decides how to build a driver, how to run an
849
- * interview turn (on an isolated agent, off the main chat bus), and how to
850
- * start the real multi-agent run (CLI's director-backed factory vs the runtime
851
- * light factory). This keeps the wizard protocol identical across both servers.
852
- */
853
- interface SddWizardDeps {
854
- /** Build a fresh interview driver (disk spec/graph stores + session path). */
855
- makeDriver: () => SddInterviewDriver;
856
- /**
857
- * Run one interview turn: feed the AI prompt to an isolated agent and return
858
- * its final text. MUST NOT run on the main chat agent's bus — the wizard owns
859
- * this conversation, separate from the user's chat.
860
- */
861
- runInterviewTurn: (prompt: string) => Promise<string>;
862
- /**
863
- * Start the real multi-agent SDD run for the driver's task graph. Returns the
864
- * runId; the live board flows through the existing board handler.
865
- */
866
- startRun: (driver: SddInterviewDriver, opts: {
867
- parallelSlots?: number | undefined;
868
- defaultModel?: string | undefined;
869
- defaultProvider?: string | undefined;
870
- fallbackModels?: string[] | undefined;
871
- /** Per-run worktree-isolation override; undefined → env default. */
872
- worktrees?: boolean | undefined;
873
- }) => Promise<{
874
- runId: string;
875
- }>;
876
- }
877
- /**
878
- * SddWizardWebSocketHandler — drives the interactive "New SDD Project" wizard
879
- * (goal → Q&A → spec → task graph → start run) over WebSocket. Shared by both
880
- * webui servers; server-specific construction (agent, factory) is injected via
881
- * {@link SddWizardDeps}.
882
- */
883
- declare class SddWizardWebSocketHandler {
884
- private readonly deps;
885
- private readonly clients;
886
- private driver;
887
- /** The agent's most recent question — paired with the next user answer. */
888
- private lastAgentText;
889
- /** Guards against overlapping interview turns (one in flight at a time). */
890
- private busy;
891
- constructor(deps: SddWizardDeps);
892
- addClient(ws: WebSocket): void;
893
- handleMessage(msg: WizardMessage): Promise<void>;
894
- private onStart;
895
- private onMessage;
896
- private onApprove;
897
- private onRunStart;
898
- /** Run one interview turn against the isolated agent, then ingest + broadcast. */
899
- private runTurn;
900
- private snapshotMsg;
901
- private broadcast;
902
- private send;
903
- }
904
-
905
- interface SddWizardWiringOptions {
906
- /** Leader agent — seeds the run's default factory + project context. */
907
- agent: Agent;
908
- /** Shared EventBus — the board projector emits sdd.board.snapshot on it. */
909
- events: EventBus;
910
- projectRoot: string;
911
- /** Per-task agent factory: CLI's director-backed one, or the runtime light one. */
912
- subagentFactory: AgentFactory;
913
- /**
914
- * Decision authority for the failure supervisor (the server's bound
915
- * TOKENS.BrainArbiter). Omit to run without a supervisor (plain terminal-fail,
916
- * matching a bare run) — but parity with the CLI wants it wired.
917
- */
918
- brain?: BrainArbiter | undefined;
919
- /** Persisted-store directories (from resolveWstackPaths). */
920
- paths: {
921
- projectSpecs: string;
922
- projectTaskGraphs: string;
923
- projectSddBoards: string;
924
- projectDir: string;
925
- };
926
- }
927
- declare function buildSddWizardDeps(opts: SddWizardWiringOptions): SddWizardDeps;
928
-
929
- type ShellOpenTarget = 'terminal' | 'file-manager';
930
- interface ShellOpenRequest {
931
- path: string;
932
- target: ShellOpenTarget;
933
- }
934
- interface ShellOpenResult {
935
- success: boolean;
936
- message: string;
937
- }
938
- declare function handleShellOpen(req: ShellOpenRequest, logger: Logger): Promise<ShellOpenResult>;
939
-
940
- /**
941
- * Shared skills WebSocket handlers for both the standalone WebUI server
942
- * (`packages/webui/src/server/index.ts`) and the CLI's `--webui` embedded
943
- * server (`packages/cli/src/webui-server.ts`).
944
- *
945
- * These were previously inlined in BOTH servers, and the CLI copy had
946
- * drifted — it only wired `skills.list`, so `skills.content` /
947
- * `skills.export` / `skills.update` (and install/uninstall/create/edit)
948
- * fell through to the "Unhandled message type" warning even though the
949
- * SkillsPanel sends them. Extracting the full set here gives both servers
950
- * one source of truth. Each function handles the full request→response
951
- * cycle for one message type; callers drop them into their switch:
952
- *
953
- * case 'skills.content': return handleSkillsContent(ws, skillsCtx, msg);
954
- *
955
- * The logic is a verbatim lift of the standalone's inline cases — only the
956
- * dependency references changed (`skillLoader`/`skillInstaller`/
957
- * `projectRoot` → `ctx.*`, local `send`/`errMessage` → imported helpers).
958
- */
959
-
960
- interface SkillsContext {
961
- /** Backs skills.list/content/edit/export. Absent ⇒ feature disabled. */
962
- skillLoader: SkillLoader | undefined;
963
- /** Backs skills.install/uninstall/update. Absent ⇒ those ops disabled. */
964
- skillInstaller: SkillInstaller | undefined;
965
- /** Project root — used by skills.create to write `.wrongstack/skills/…`. */
966
- projectRoot: string;
967
- /** Project skills directory, normally `<project>/.wrongstack/skills`. */
968
- projectSkillsDir?: string | undefined;
969
- /** User-global skills directory, normally `~/.wrongstack/skills`. */
970
- globalSkillsDir?: string | undefined;
971
- }
972
- /**
973
- * Read a single skill's body + its directory's related files + which other
974
- * skills reference it by name. Powers the skill detail/preview view.
975
- */
976
- declare function handleSkillsContent(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
977
- /**
978
- * Install a skill from a git ref (`owner/repo` or URL). Optional `global`
979
- * installs into the user-wide skills dir instead of the project's.
980
- */
981
- declare function handleSkillsInstall(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
982
- /**
983
- * Uninstall a skill by name. Optional `global` restricts/Targets the
984
- * user-wide install.
985
- */
986
- declare function handleSkillsUninstall(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
987
- /**
988
- * Update one skill (`name`) or all installed skills (when `name` is
989
- * omitted). Reports per-skill updated/unchanged/error tallies.
990
- */
991
- declare function handleSkillsUpdate(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
992
- /**
993
- * Scaffold a new project- or global-scoped skill from a name + description.
994
- * Writes a templated `SKILL.md` under `.wrongstack/skills/<name>/` (project)
995
- * or the user-wide skills dir (global).
996
- */
997
- declare function handleSkillsCreate(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
998
- /**
999
- * Overwrite a skill's body. Refuses bundled skills (read-only) and unknown
1000
- * names.
1001
- */
1002
- declare function handleSkillsEdit(ws: WebSocket, ctx: SkillsContext, msg: unknown): Promise<void>;
1003
- /**
1004
- * Export every readable skill as a base64-encoded zip (one folder per skill,
1005
- * each with its `SKILL.md`). Powers the panel's "Export all" button.
1006
- */
1007
- declare function handleSkillsExport(ws: WebSocket, ctx: SkillsContext): Promise<void>;
1008
-
1009
- interface SpecsWSMessage {
1010
- type: string;
1011
- payload?: Record<string, unknown>;
1012
- }
1013
- /**
1014
- * SpecsWebSocketHandler — read-only-ish browser of persisted SDD specs and their
1015
- * task graphs, rendered as a FORGE-style dependency board (topological phase
1016
- * columns + dependency refs). Shared by both webui servers via specs-routes.
1017
- *
1018
- * Message types:
1019
- * specs.list → all specs + progress
1020
- * specs.get { specId } → one spec's dependency board
1021
- * specs.taskStatus { graphId, taskId, status } → update + rebroadcast
1022
- */
1023
- declare class SpecsWebSocketHandler {
1024
- private specStore;
1025
- private graphStore;
1026
- private clients;
1027
- constructor(specsDir: string, taskGraphsDir: string);
1028
- addClient(ws: WebSocket): void;
1029
- handleMessage(msg: SpecsWSMessage): Promise<void>;
1030
- private buildList;
1031
- private broadcastList;
1032
- private sendList;
1033
- private broadcastDetail;
1034
- private findGraphForSpec;
1035
- private buildDetail;
1036
- private updateTaskStatus;
1037
- private broadcast;
1038
- private send;
1039
- }
1040
-
1041
- /**
1042
- * Per-section context-window token estimate for the `context.debug` command.
1043
- *
1044
- * Uses the simple 4-chars-per-token heuristic — not exact, but close enough to
1045
- * spot which section (system prompt, tool schemas, or message history) is
1046
- * eating the context window. Tool schemas in particular are easy to overlook:
1047
- * each tool ships its full JSON schema to the model every turn, so 20+ builtins
1048
- * can cost 10-20k tokens on their own.
1049
- *
1050
- * Extracted from `index.ts` as a pure function so the breakdown maths can be
1051
- * unit tested without standing up a Context/ToolRegistry.
1052
- */
1053
- /** 4-chars-per-token heuristic estimate for a string. */
1054
- declare function estimateTokens(s: string): number;
1055
- /** Stringify arbitrary content for length estimation (JSON, with fallbacks). */
1056
- declare function stringifyContent(c: unknown): string;
1057
- interface ToolTokenEntry {
1058
- name: string;
1059
- tokens: number;
1060
- }
1061
- interface MessageTokenEntry {
1062
- index: number;
1063
- role: string;
1064
- tokens: number;
1065
- preview: string;
1066
- }
1067
- interface ContextBreakdown {
1068
- total: number;
1069
- systemPrompt: number;
1070
- tools: {
1071
- total: number;
1072
- count: number;
1073
- breakdown: ToolTokenEntry[];
1074
- };
1075
- messages: {
1076
- total: number;
1077
- count: number;
1078
- breakdown: MessageTokenEntry[];
1079
- };
1080
- }
1081
- declare function messageTokens(content: unknown): number;
1082
- declare function messagePreview(content: unknown): string;
1083
-
1084
- interface WorktreeManagementDeps {
1085
- projectRoot: string;
1086
- /** Board snapshot dir — powers the cross-process liveness guard on cleanup. */
1087
- boardsDir: string;
1088
- }
1089
- /**
1090
- * WorktreeWebSocketHandler — mirrors AutoPhaseWebSocketHandler. Subscribes to
1091
- * the shared EventBus `worktree.*` lifecycle events, keeps a live snapshot of
1092
- * every worktree, and broadcasts:
1093
- * - `worktree.event` incrementally (drives the flowing activity strip)
1094
- * - `worktree.state` on connect + on a 2s timer (drives swim-lanes/DAG)
1095
- */
1096
- declare class WorktreeWebSocketHandler {
1097
- private readonly events;
1098
- private readonly logger;
1099
- private readonly management?;
1100
- private readonly clients;
1101
- private readonly handles;
1102
- private baseBranch;
1103
- private broadcastInterval;
1104
- private readonly offs;
1105
- constructor(events: EventBus, logger: Logger, management?: WorktreeManagementDeps | undefined);
1106
- addClient(ws: WebSocket): void;
1107
- /** Handle worktree-panel control messages (scan / clean / per-row ops). */
1108
- handleMessage(msg: {
1109
- type: string;
1110
- payload?: Record<string, unknown>;
1111
- }): Promise<boolean>;
1112
- dispose(): void;
1113
- /** Absolute managed-worktrees root for this project. */
1114
- private worktreesRoot;
1115
- /** True iff `dir` resolves strictly inside the managed worktrees root. */
1116
- private underRoot;
1117
- /** Branches of worktrees a live in-session run currently owns. */
1118
- private liveActiveBranches;
1119
- /**
1120
- * Scan the disk for managed worktrees/branches NOT owned by a live in-session
1121
- * run and broadcast them as orphans, with whether it is safe to clean now.
1122
- * No-op (empty inventory) when management deps were not wired.
1123
- */
1124
- private scanAndBroadcast;
1125
- /**
1126
- * Force-remove every orphaned worktree + branch. Refused while a run is live —
1127
- * in this session (active handles) OR another process (the SDD board liveness
1128
- * guard inside cleanupStaleSddWorktrees). Best-effort; reports the outcome.
1129
- */
1130
- private cleanupOrphans;
1131
- /** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
1132
- private removeOne;
1133
- /** Squash-merge ONE branch into base. Refused while a live run owns it. */
1134
- private mergeBranch;
1135
- /** Compact change summary for one worktree checkout. */
1136
- private diffOne;
1137
- private subscribe;
1138
- private upsert;
1139
- private patch;
1140
- private activity;
1141
- private stateMessage;
1142
- private broadcastState;
1143
- private ensureBroadcast;
1144
- private stopBroadcast;
1145
- private broadcast;
1146
- private send;
1147
- }
1148
-
1149
- /** A hostname that refers to the local machine. */
1150
- declare function isLoopbackHostname(hostname: string): boolean;
1151
- /** True when the server is bound to a loopback interface (vs. LAN/0.0.0.0). */
1152
- declare function isLoopbackBind(wsHost: string): boolean;
1153
- /**
1154
- * Constant-time comparison of a provided token against the expected one.
1155
- * A length mismatch short-circuits (lengths aren't secret); equal-length
1156
- * inputs are compared with `timingSafeEqual` so the token can't be recovered
1157
- * byte-by-byte via response timing.
1158
- */
1159
- declare function tokenMatches(provided: string | undefined, expected: string): boolean;
1160
- /** Pull the `token` query param out of a request URL (`/?token=…`). */
1161
- declare function extractToken(url: string): string | undefined;
1162
- /**
1163
- * DNS-rebinding defense. On a loopback bind, the `Host` header must resolve to
1164
- * a loopback name. When the operator deliberately exposes the socket (wsHost is
1165
- * a LAN/0.0.0.0 address) the Host is legitimately non-loopback, so the guard is
1166
- * skipped and connection auth falls to the token check.
1167
- */
1168
- declare function hostHeaderOk(input: {
1169
- hostHeader: string | undefined;
1170
- wsHost: string;
1171
- allowedHostnames?: readonly string[] | undefined;
1172
- }): boolean;
1173
- interface VerifyClientInput {
1174
- /** Browser `Origin` header, or undefined for non-browser clients. */
1175
- origin?: string | undefined;
1176
- /** Request URL (`req.url`) — carries the `?token=…` query param. */
1177
- url: string;
1178
- /** `Host` header (`req.headers.host`). */
1179
- hostHeader?: string | undefined;
1180
- /** Peer address (`req.socket.remoteAddress`). */
1181
- remoteAddress?: string | undefined;
1182
- /** `Cookie` header (`req.headers.cookie`). Carries `ws_token=…` when the
1183
- * browser went through `/ws-auth` to set the HttpOnly auth cookie. */
1184
- cookieHeader?: string | string[] | undefined;
1185
- /** Host/interface the WS server is bound to. */
1186
- wsHost: string;
1187
- /** The server's generated auth token. */
1188
- expectedToken: string;
1189
- /** Force token auth even for loopback binds, useful behind public tunnels. */
1190
- requireToken?: boolean | undefined;
1191
- /** Extra Host header names allowed on loopback binds, e.g. a tunnel hostname. */
1192
- allowedHostnames?: readonly string[] | undefined;
1193
- /** Allow browser WS URL tokens for explicit public WS URLs where cookies cannot cross hostnames. */
1194
- allowBrowserUrlToken?: boolean | undefined;
1195
- }
1196
- /**
1197
- * Decide whether to accept an incoming WebSocket handshake. Pure mirror of the
1198
- * closure previously inlined in `index.ts`; see the module doc for the layered
1199
- * policy. Returns `true` to accept, `false` to reject.
1200
- *
1201
- * Token sources, in priority order:
1202
- * 1. `Cookie: ws_token=…` (browser clients that went through `/ws-auth`)
1203
- * 2. `?token=…` URL query param (non-browser clients: curl, scripts)
1204
- *
1205
- * Browser clients (with an `Origin` header) are restricted to the cookie path —
1206
- * URL token is rejected for them, closing the C-598 query-string token
1207
- * exposure class. Non-browser clients keep the URL-token fallback so curl
1208
- * and tests continue to work.
1209
- */
1210
- declare function verifyClient(input: VerifyClientInput): boolean;
1211
-
1212
- declare function startWebUI(opts?: WebUIOptions & {
1213
- wsPort?: number | undefined;
1214
- wsHost?: string | undefined;
1215
- httpPort?: number | undefined;
1216
- accessToken?: string | undefined;
1217
- publicUrl?: string | undefined;
1218
- publicWsUrl?: string | undefined;
1219
- requireToken?: boolean | undefined;
1220
- open?: boolean | undefined;
1221
- }): Promise<void>;
1222
-
1223
- export { AutoPhaseWebSocketHandler, type BackendServices, type CompletionHandlerOptions, type CompletionItemKind, type CompletionSuggestion, type ConnectedClient, type ContextBreakdown, type CreateHttpServerOptions, type CustomContextMode, type CustomModeStore, type DesignContext, type EternalBroadcast, type EternalSubscribe, type EternalSubscription, type KeyOpResult, type LspCompletionSource, type LspCompletionSourceRequest, type MessageTokenEntry, type PromptsContext, type ProvidersRecord, SddBoardWebSocketHandler, type SddWizardDeps, SddWizardWebSocketHandler, type SddWizardWiringOptions, type ShellOpenRequest, type ShellOpenResult, type ShellOpenTarget, type SkillsContext, SpecsWebSocketHandler, type ToolTokenEntry, type VerifyClientInput, type WSClientMessage, type WSServerMessage, type WebUIInstanceRecord, type WebUIOptions, WorktreeWebSocketHandler, addProvider, broadcast, browserOpenCommand, buildCspHeader, buildSddWizardDeps, buildWebUIAccessUrl, createCustomModeStore, createEternalSubscription, createHttpServer, createProviderConfigIO, createToolLspCompletionSource, defaultBaseDir, deleteKey, envFlag, errMessage, estimateTokens, extractToken, findFreePort, formatInstances, generateAuthToken, handleCompletionRequest, handleDesignList, handleDesignMaterialize, handleDesignSet, handleDesignState, handleDesignUse, handleDesignVerify, handleFilesList, handleFilesRead, handleFilesTree, handleFilesWrite, handleGitChanges, handleGitDiff, handleGitInfo, handleMcpAdd, handleMcpDisable, handleMcpDiscover, handleMcpEnable, handleMcpList, handleMcpRemove, handleMcpRestart, handleMcpSleep, handleMcpUpdate, handleMcpWake, handleMemoryForget, handleMemoryList, handleMemoryRemember, handlePromptsContent, handlePromptsCreate, handlePromptsFavorite, handlePromptsList, handlePromptsRecent, handlePromptsSearch, handlePromptsUsed, handleShellOpen, handleSkillsContent, handleSkillsCreate, handleSkillsEdit, handleSkillsExport, handleSkillsInstall, handleSkillsUninstall, handleSkillsUpdate, hostForBrowserUrl, hostHeaderOk, injectWsPort, isLoopbackBind, isLoopbackHostname, isPortFree, listInstances, loadSavedProviders, maskedKey, messagePreview, messageTokens, normalizeKeys, openBrowser, registerInstance, registryPath, removeProvider, resolveAuthToken, saveProviders, send, sendResult, setActiveKey, startWebUI, stringifyContent, tokenMatches, unregisterInstance, upsertKey, verifyClient, writeKeysBack };
1
+ export * from '@wrongstack/webui-server';