@wrongstack/webui 0.275.0 → 0.276.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-D8YeqUJA.css +2 -0
- package/dist/assets/index-ZU8u12kr.js +141 -0
- package/dist/assets/vendor-C3aCZA5Q.css +1 -0
- package/dist/assets/{vendor-Cl_sFcw4.js → vendor-Co2oLbbP.js} +291 -284
- package/dist/index.html +4 -4
- package/dist/index.js +28003 -25775
- package/dist/index.js.map +1 -1
- package/dist/providers.json +107 -0
- package/dist/server/entry.js +10512 -9687
- package/dist/server/entry.js.map +1 -1
- package/dist/server/handlers.js +21 -15
- package/dist/server/handlers.js.map +1 -1
- package/dist/server/index.d.ts +727 -727
- package/dist/server/index.js +10477 -9658
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +120 -63
- package/package.json +8 -6
- package/dist/assets/index-BG4jUAmc.js +0 -141
- package/dist/assets/index-Cw32ELnp.css +0 -2
- package/dist/assets/vendor-Doh9e_v3.css +0 -1
package/dist/server/index.d.ts
CHANGED
|
@@ -1,9 +1,214 @@
|
|
|
1
1
|
import { WebSocket } from 'ws';
|
|
2
|
-
import { Agent, EventBus, SessionStore, ToolRegistry, ModelsRegistry, ConfigStore, SecretVault, JournalEntry,
|
|
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
3
|
import * as http from 'node:http';
|
|
4
4
|
import { MCPRegistry } from '@wrongstack/mcp';
|
|
5
5
|
import { SkillInstaller } from '@wrongstack/core/skills';
|
|
6
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
|
+
|
|
7
212
|
interface WSServerMessage {
|
|
8
213
|
type: string;
|
|
9
214
|
payload: unknown;
|
|
@@ -92,53 +297,144 @@ interface ConnectedClient {
|
|
|
92
297
|
connId: string;
|
|
93
298
|
}
|
|
94
299
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
debounceResets: number;
|
|
101
|
-
totalDebounceDelayMs: number;
|
|
102
|
-
activeProjects: number;
|
|
103
|
-
/** Average debounce delay in ms across all broadcasts. */
|
|
104
|
-
averageDebounceDelayMs: number;
|
|
105
|
-
/** Whether the file watcher is currently active. */
|
|
106
|
-
watcherActive: boolean;
|
|
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;
|
|
107
305
|
}
|
|
306
|
+
declare function createEternalSubscription<C>(subscribe: EternalSubscribe, broadcast: EternalBroadcast<C>, clientsRef: () => Map<WebSocket, C>): EternalSubscription;
|
|
108
307
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
fileChangesDetected: number;
|
|
394
|
+
filesProcessed: number;
|
|
395
|
+
broadcastsSent: number;
|
|
396
|
+
debounceResets: number;
|
|
397
|
+
totalDebounceDelayMs: number;
|
|
398
|
+
activeProjects: number;
|
|
399
|
+
/** Average debounce delay in ms across all broadcasts. */
|
|
400
|
+
averageDebounceDelayMs: number;
|
|
401
|
+
/** Whether the file watcher is currently active. */
|
|
402
|
+
watcherActive: boolean;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
interface CreateHttpServerOptions {
|
|
406
|
+
/** Port to listen on. Defaults to 3456 (or the `PORT` env var). */
|
|
407
|
+
port?: number | undefined;
|
|
408
|
+
/** Host/interface to bind. Typically the loopback for the WebUI. */
|
|
409
|
+
host: string;
|
|
410
|
+
/** Resolved path to the directory containing the built React assets. */
|
|
411
|
+
distDir: string;
|
|
412
|
+
/**
|
|
413
|
+
* WS port — appears in the CSP `connect-src` directive so the browser
|
|
414
|
+
* is allowed to open a WebSocket back to the local server.
|
|
415
|
+
*/
|
|
416
|
+
wsPort: number;
|
|
417
|
+
/**
|
|
418
|
+
* Public WebSocket URL injected into the frontend. Use this behind tunnels or
|
|
419
|
+
* reverse proxies where the browser-facing WS URL differs from host:wsPort.
|
|
420
|
+
*/
|
|
421
|
+
publicWsUrl?: string | undefined;
|
|
422
|
+
/**
|
|
423
|
+
* Path to the global WrongStack root (~/.wrongstack). Used by the
|
|
424
|
+
* /api/sessions and /api/sessions/:id/agents endpoints to read the
|
|
425
|
+
* cross-process SessionRegistry.
|
|
426
|
+
*/
|
|
427
|
+
globalRoot?: string | undefined;
|
|
428
|
+
/**
|
|
429
|
+
* Shared auth token for HTTP and WS access. Required for non-loopback
|
|
430
|
+
* binds (LAN exposure). Loopback binds accept local browser access without
|
|
431
|
+
* a token (the WS path's loopback-bootstrap policy — see ws-auth.ts).
|
|
432
|
+
*/
|
|
433
|
+
apiToken?: string | undefined;
|
|
434
|
+
/** Force HTTP token auth even on loopback binds, useful behind public tunnels. */
|
|
435
|
+
requireToken?: boolean | undefined;
|
|
436
|
+
/**
|
|
437
|
+
* If true, the `/ws-auth` endpoint exchanges a `?token=` query param (or
|
|
142
438
|
* `X-WS-Token` header) for an `HttpOnly` auth cookie. The cookie is then
|
|
143
439
|
* sent automatically on the WS upgrade, closing the C-598 query-string
|
|
144
440
|
* token exposure class. Default: true. Set to false to keep the legacy
|
|
@@ -180,231 +476,163 @@ declare function buildCspHeader(wsPort: number, requestHost?: string | undefined
|
|
|
180
476
|
declare function createHttpServer(opts: CreateHttpServerOptions): http.Server;
|
|
181
477
|
|
|
182
478
|
/**
|
|
183
|
-
*
|
|
479
|
+
* Running-instance registry for the standalone WebUI server.
|
|
184
480
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* the served HTML and the instance registry so everything stays consistent.
|
|
481
|
+
* Every live `wstackui` process records itself in a single JSON file under the
|
|
482
|
+
* wstack home dir (`~/.wrongstack/webui-instances.json`) so a user running
|
|
483
|
+
* several instances (one per project, or several per project on different
|
|
484
|
+
* ports) can see at a glance which ports are open for which path.
|
|
190
485
|
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
486
|
+
* Design notes:
|
|
487
|
+
* - **Self-healing**: every register/unregister/list prunes entries whose PID
|
|
488
|
+
* is no longer alive (`process.kill(pid, 0)`), so a crashed instance that
|
|
489
|
+
* never got to unregister doesn't leave a ghost behind.
|
|
490
|
+
* - **Atomic writes**: the file is rewritten via `atomicWrite` (tmp + rename),
|
|
491
|
+
* so a concurrent reader never sees a half-written file. Two instances
|
|
492
|
+
* starting at the *exact* same millisecond could still race the
|
|
493
|
+
* read-modify-write — acceptable for a best-effort tracking file, and the
|
|
494
|
+
* next register() heals any dropped entry.
|
|
495
|
+
* - **Best-effort**: a failure to read/write the registry must NEVER take the
|
|
496
|
+
* server down. Callers wrap these in `.catch()`.
|
|
195
497
|
*/
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
498
|
+
/** One running WebUI process. */
|
|
499
|
+
interface WebUIInstanceRecord {
|
|
500
|
+
/** OS process id — also the liveness key. */
|
|
501
|
+
pid: number;
|
|
502
|
+
/** HTTP port serving the React frontend. */
|
|
503
|
+
httpPort: number;
|
|
504
|
+
/** WebSocket port for the agent backend. */
|
|
505
|
+
wsPort: number;
|
|
506
|
+
/** Bind host (e.g. 127.0.0.1 or 0.0.0.0). */
|
|
507
|
+
host: string;
|
|
508
|
+
/** Absolute project root the instance booted against. */
|
|
509
|
+
projectRoot: string;
|
|
510
|
+
/** Display name (basename of projectRoot). */
|
|
511
|
+
projectName: string;
|
|
512
|
+
/** ISO timestamp when the instance registered. */
|
|
513
|
+
startedAt: string;
|
|
514
|
+
/** Convenience open-in-browser URL. */
|
|
515
|
+
url: string;
|
|
203
516
|
}
|
|
517
|
+
/** Default wstack home dir (`~/.wrongstack`). Callers may override the base. */
|
|
518
|
+
declare function defaultBaseDir(): string;
|
|
519
|
+
/** Resolve the registry file path for a given base dir. */
|
|
520
|
+
declare function registryPath(baseDir?: string): string;
|
|
204
521
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
522
|
+
* Register (or refresh) this instance. Prunes dead entries and any stale entry
|
|
523
|
+
* for our own PID before adding the current record. Best-effort — rejects only
|
|
524
|
+
* on a hard fs error, which callers swallow.
|
|
207
525
|
*/
|
|
208
|
-
declare function
|
|
526
|
+
declare function registerInstance(record: WebUIInstanceRecord, baseDir?: string): Promise<void>;
|
|
527
|
+
/** Remove this instance (called on graceful shutdown). Also prunes dead pids. */
|
|
528
|
+
declare function unregisterInstance(pid: number, baseDir?: string): Promise<void>;
|
|
529
|
+
/** List live instances, pruning any dead entries encountered. */
|
|
530
|
+
declare function listInstances(baseDir?: string): Promise<WebUIInstanceRecord[]>;
|
|
531
|
+
/** Human-readable table of running instances for `wstackui --list`. */
|
|
532
|
+
declare function formatInstances(instances: WebUIInstanceRecord[]): string;
|
|
209
533
|
|
|
210
534
|
/**
|
|
211
|
-
*
|
|
535
|
+
* MCP management handlers for the WebUI server (both the standalone
|
|
536
|
+
* `wstackui` server and the CLI's embedded `--webui` server).
|
|
212
537
|
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
538
|
+
* These are thin WebSocket translators over the shared, surface-agnostic
|
|
539
|
+
* management core in `@wrongstack/mcp` (`manage.ts`) — the SAME core the REPL
|
|
540
|
+
* `/mcp` command writes against (same config.json, same MCPRegistry). All the
|
|
541
|
+
* config IO, url/header persistence, and live registry start/stop logic lives
|
|
542
|
+
* there; here we only map structured results to WS events the browser expects.
|
|
216
543
|
*/
|
|
217
|
-
/** Resolve the platform's URL-opener command + args. */
|
|
218
|
-
declare function browserOpenCommand(url: string, platform?: NodeJS.Platform): {
|
|
219
|
-
command: string;
|
|
220
|
-
args: string[];
|
|
221
|
-
};
|
|
222
|
-
/** Spawn the OS browser-opener for `url` and register it as a protected
|
|
223
|
-
* process so it survives kill/killAll. Never throws. */
|
|
224
|
-
declare function openBrowser(url: string, platform?: NodeJS.Platform): void;
|
|
225
544
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
545
|
+
/** mcp.list — configured servers merged with live registry status + tools. */
|
|
546
|
+
declare function handleMcpList(ws: WebSocket, _msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
547
|
+
/** mcp.add — persist a new server (incl. url/headers) and start it if enabled. */
|
|
548
|
+
declare function handleMcpAdd(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
549
|
+
/** mcp.update — re-persist config (incl. url/headers) and re-apply to registry. */
|
|
550
|
+
declare function handleMcpUpdate(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
551
|
+
/** mcp.remove — stop the server and delete it from config. */
|
|
552
|
+
declare function handleMcpRemove(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
553
|
+
/** mcp.enable — flip enabled:true in config and start the server. */
|
|
554
|
+
declare function handleMcpEnable(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
555
|
+
/** mcp.disable — stop the server and flip enabled:false in config. */
|
|
556
|
+
declare function handleMcpDisable(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
557
|
+
/** mcp.sleep — stop a running server (config stays enabled). */
|
|
558
|
+
declare function handleMcpSleep(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
559
|
+
/** mcp.wake — restart a sleeping/stopped server from config. */
|
|
560
|
+
declare function handleMcpWake(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
561
|
+
/** mcp.restart — stop + start a server. */
|
|
562
|
+
declare function handleMcpRestart(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
563
|
+
/** mcp.discover — ensure the server is running and report its live tools. */
|
|
564
|
+
declare function handleMcpDiscover(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
565
|
+
|
|
231
566
|
/**
|
|
232
|
-
*
|
|
233
|
-
* the
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
567
|
+
* Shared memory-operation WebSocket handlers for both the standalone WebUI
|
|
568
|
+
* server and the CLI's `--webui` embedded server. Extracted from the
|
|
569
|
+
* duplicated switch cases in `index.ts` and `cli/src/webui-server.ts`.
|
|
570
|
+
*
|
|
571
|
+
* Each function handles the full request→response cycle for one message
|
|
572
|
+
* type. Callers drop them into their switch statement:
|
|
573
|
+
*
|
|
574
|
+
* case 'memory.list': return handleMemoryList(ws, memoryStore);
|
|
237
575
|
*/
|
|
238
|
-
declare class WorktreeWebSocketHandler {
|
|
239
|
-
private readonly events;
|
|
240
|
-
private readonly logger;
|
|
241
|
-
private readonly management?;
|
|
242
|
-
private readonly clients;
|
|
243
|
-
private readonly handles;
|
|
244
|
-
private baseBranch;
|
|
245
|
-
private broadcastInterval;
|
|
246
|
-
private readonly offs;
|
|
247
|
-
constructor(events: EventBus, logger: Logger, management?: WorktreeManagementDeps | undefined);
|
|
248
|
-
addClient(ws: WebSocket): void;
|
|
249
|
-
/** Handle worktree-panel control messages (scan / clean / per-row ops). */
|
|
250
|
-
handleMessage(msg: {
|
|
251
|
-
type: string;
|
|
252
|
-
payload?: Record<string, unknown>;
|
|
253
|
-
}): Promise<boolean>;
|
|
254
|
-
dispose(): void;
|
|
255
|
-
/** Absolute managed-worktrees root for this project. */
|
|
256
|
-
private worktreesRoot;
|
|
257
|
-
/** True iff `dir` resolves strictly inside the managed worktrees root. */
|
|
258
|
-
private underRoot;
|
|
259
|
-
/** Branches of worktrees a live in-session run currently owns. */
|
|
260
|
-
private liveActiveBranches;
|
|
261
|
-
/**
|
|
262
|
-
* Scan the disk for managed worktrees/branches NOT owned by a live in-session
|
|
263
|
-
* run and broadcast them as orphans, with whether it is safe to clean now.
|
|
264
|
-
* No-op (empty inventory) when management deps were not wired.
|
|
265
|
-
*/
|
|
266
|
-
private scanAndBroadcast;
|
|
267
|
-
/**
|
|
268
|
-
* Force-remove every orphaned worktree + branch. Refused while a run is live —
|
|
269
|
-
* in this session (active handles) OR another process (the SDD board liveness
|
|
270
|
-
* guard inside cleanupStaleSddWorktrees). Best-effort; reports the outcome.
|
|
271
|
-
*/
|
|
272
|
-
private cleanupOrphans;
|
|
273
|
-
/** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
|
|
274
|
-
private removeOne;
|
|
275
|
-
/** Squash-merge ONE branch into base. Refused while a live run owns it. */
|
|
276
|
-
private mergeBranch;
|
|
277
|
-
/** Compact change summary for one worktree checkout. */
|
|
278
|
-
private diffOne;
|
|
279
|
-
private subscribe;
|
|
280
|
-
private upsert;
|
|
281
|
-
private patch;
|
|
282
|
-
private activity;
|
|
283
|
-
private stateMessage;
|
|
284
|
-
private broadcastState;
|
|
285
|
-
private ensureBroadcast;
|
|
286
|
-
private stopBroadcast;
|
|
287
|
-
private broadcast;
|
|
288
|
-
private send;
|
|
289
|
-
}
|
|
290
576
|
|
|
291
577
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
*
|
|
298
|
-
*
|
|
578
|
+
* List all memory entries across all scopes.
|
|
579
|
+
* Responds with `{ type: 'memory.list', payload: { text } }`.
|
|
580
|
+
*/
|
|
581
|
+
declare function handleMemoryList(ws: WebSocket, memoryStore: MemoryStore): Promise<void>;
|
|
582
|
+
/**
|
|
583
|
+
* Persist a new memory entry.
|
|
584
|
+
* Responds with `{ type: 'key.operation_result', payload: { success, message } }`.
|
|
585
|
+
*/
|
|
586
|
+
declare function handleMemoryRemember(ws: WebSocket, msg: unknown, memoryStore: MemoryStore): Promise<void>;
|
|
587
|
+
/**
|
|
588
|
+
* Remove memory entries matching the given text.
|
|
589
|
+
* Responds with `{ type: 'key.operation_result', payload: { success, message } }`.
|
|
590
|
+
*/
|
|
591
|
+
declare function handleMemoryForget(ws: WebSocket, msg: unknown, memoryStore: MemoryStore): Promise<void>;
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Best-effort "open this URL in the default browser" for `--webui --open`.
|
|
299
595
|
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
596
|
+
* Cross-platform via the OS opener (`start` / `open` / `xdg-open`). Fully
|
|
597
|
+
* fire-and-forget: a missing opener, a headless box, or a spawn failure must
|
|
598
|
+
* NEVER take the server down — the URL is always also printed to the console.
|
|
302
599
|
*/
|
|
303
|
-
/**
|
|
304
|
-
declare function
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
interface MessageTokenEntry {
|
|
312
|
-
index: number;
|
|
313
|
-
role: string;
|
|
314
|
-
tokens: number;
|
|
315
|
-
preview: string;
|
|
316
|
-
}
|
|
317
|
-
interface ContextBreakdown {
|
|
318
|
-
total: number;
|
|
319
|
-
systemPrompt: number;
|
|
320
|
-
tools: {
|
|
321
|
-
total: number;
|
|
322
|
-
count: number;
|
|
323
|
-
breakdown: ToolTokenEntry[];
|
|
324
|
-
};
|
|
325
|
-
messages: {
|
|
326
|
-
total: number;
|
|
327
|
-
count: number;
|
|
328
|
-
breakdown: MessageTokenEntry[];
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
declare function messageTokens(content: unknown): number;
|
|
332
|
-
declare function messagePreview(content: unknown): string;
|
|
600
|
+
/** Resolve the platform's URL-opener command + args. */
|
|
601
|
+
declare function browserOpenCommand(url: string, platform?: NodeJS.Platform): {
|
|
602
|
+
command: string;
|
|
603
|
+
args: string[];
|
|
604
|
+
};
|
|
605
|
+
/** Spawn the OS browser-opener for `url` and register it as a protected
|
|
606
|
+
* process so it survives kill/killAll. Never throws. */
|
|
607
|
+
declare function openBrowser(url: string, platform?: NodeJS.Platform): void;
|
|
333
608
|
|
|
334
609
|
/**
|
|
335
|
-
*
|
|
610
|
+
* Free-port discovery for the standalone WebUI server.
|
|
336
611
|
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
612
|
+
* When a user runs several instances, the default ports (HTTP 3456 / WS 3457)
|
|
613
|
+
* are taken by the first one. Rather than make the user hand-pick `PORT` /
|
|
614
|
+
* `WS_PORT` for every extra instance, the server probes upward from the
|
|
615
|
+
* requested port and binds the first free one — then stamps that real port into
|
|
616
|
+
* the served HTML and the instance registry so everything stays consistent.
|
|
341
617
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* - **Atomic writes**: the file is rewritten via `atomicWrite` (tmp + rename),
|
|
347
|
-
* so a concurrent reader never sees a half-written file. Two instances
|
|
348
|
-
* starting at the *exact* same millisecond could still race the
|
|
349
|
-
* read-modify-write — acceptable for a best-effort tracking file, and the
|
|
350
|
-
* next register() heals any dropped entry.
|
|
351
|
-
* - **Best-effort**: a failure to read/write the registry must NEVER take the
|
|
352
|
-
* server down. Callers wrap these in `.catch()`.
|
|
618
|
+
* The probe binds a throwaway `net.Server`, then closes it, so there is a tiny
|
|
619
|
+
* TOCTOU window between "found free" and "the real server binds it". For local
|
|
620
|
+
* single-user multi-instance use that race is negligible; if it ever loses, the
|
|
621
|
+
* real bind fails loudly with EADDRINUSE exactly as before.
|
|
353
622
|
*/
|
|
354
|
-
/**
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
wsPort: number;
|
|
362
|
-
/** Bind host (e.g. 127.0.0.1 or 0.0.0.0). */
|
|
363
|
-
host: string;
|
|
364
|
-
/** Absolute project root the instance booted against. */
|
|
365
|
-
projectRoot: string;
|
|
366
|
-
/** Display name (basename of projectRoot). */
|
|
367
|
-
projectName: string;
|
|
368
|
-
/** ISO timestamp when the instance registered. */
|
|
369
|
-
startedAt: string;
|
|
370
|
-
/** Convenience open-in-browser URL. */
|
|
371
|
-
url: string;
|
|
623
|
+
/** Resolve true when `port` can be bound on `host`, false on EADDRINUSE/EACCES. */
|
|
624
|
+
declare function isPortFree(host: string, port: number): Promise<boolean>;
|
|
625
|
+
interface FindFreePortOptions {
|
|
626
|
+
/** Ports to skip even if free (e.g. one already chosen for the sibling server). */
|
|
627
|
+
exclude?: Set<number> | undefined;
|
|
628
|
+
/** How many consecutive ports to try before giving up. Default 200. */
|
|
629
|
+
maxTries?: number | undefined;
|
|
372
630
|
}
|
|
373
|
-
/** Default wstack home dir (`~/.wrongstack`). Callers may override the base. */
|
|
374
|
-
declare function defaultBaseDir(): string;
|
|
375
|
-
/** Resolve the registry file path for a given base dir. */
|
|
376
|
-
declare function registryPath(baseDir?: string): string;
|
|
377
631
|
/**
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
* on a hard fs error, which callers swallow.
|
|
632
|
+
* Find the first free port at or above `startPort` on `host`, skipping any in
|
|
633
|
+
* `exclude`. Throws if nothing is free within `maxTries` steps.
|
|
381
634
|
*/
|
|
382
|
-
declare function
|
|
383
|
-
/** Remove this instance (called on graceful shutdown). Also prunes dead pids. */
|
|
384
|
-
declare function unregisterInstance(pid: number, baseDir?: string): Promise<void>;
|
|
385
|
-
/** List live instances, pruning any dead entries encountered. */
|
|
386
|
-
declare function listInstances(baseDir?: string): Promise<WebUIInstanceRecord[]>;
|
|
387
|
-
/** Human-readable table of running instances for `wstackui --list`. */
|
|
388
|
-
declare function formatInstances(instances: WebUIInstanceRecord[]): string;
|
|
389
|
-
|
|
390
|
-
type EternalSubscribe = (fn: (entry: JournalEntry) => void) => () => void;
|
|
391
|
-
type EternalBroadcast<C> = (clients: Map<WebSocket, C>, msg: WSServerMessage) => void;
|
|
392
|
-
interface EternalSubscription {
|
|
393
|
-
/** Tear down the underlying engine subscription. Idempotent. */
|
|
394
|
-
dispose: () => void;
|
|
395
|
-
}
|
|
396
|
-
declare function createEternalSubscription<C>(subscribe: EternalSubscribe, broadcast: EternalBroadcast<C>, clientsRef: () => Map<WebSocket, C>): EternalSubscription;
|
|
397
|
-
|
|
398
|
-
type ShellOpenTarget = 'terminal' | 'file-manager';
|
|
399
|
-
interface ShellOpenRequest {
|
|
400
|
-
path: string;
|
|
401
|
-
target: ShellOpenTarget;
|
|
402
|
-
}
|
|
403
|
-
interface ShellOpenResult {
|
|
404
|
-
success: boolean;
|
|
405
|
-
message: string;
|
|
406
|
-
}
|
|
407
|
-
declare function handleShellOpen(req: ShellOpenRequest, logger: Logger): Promise<ShellOpenResult>;
|
|
635
|
+
declare function findFreePort(host: string, startPort: number, opts?: FindFreePortOptions): Promise<number>;
|
|
408
636
|
|
|
409
637
|
/**
|
|
410
638
|
* Send a JSON message to a single WebSocket client.
|
|
@@ -443,290 +671,65 @@ declare function buildWebUIAccessUrl(opts: {
|
|
|
443
671
|
declare function envFlag(name: string): boolean;
|
|
444
672
|
|
|
445
673
|
/**
|
|
446
|
-
* Shared
|
|
447
|
-
* server and the CLI's `--webui` embedded server
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
*
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
* Read a file's content for the Monaco editor.
|
|
469
|
-
*
|
|
470
|
-
* Guards against path traversal (`../` escapes). Responds with
|
|
471
|
-
* `{ type: 'files.read', payload: { filePath, content } }`.
|
|
472
|
-
*/
|
|
473
|
-
declare function handleFilesRead(ws: WebSocket, msg: unknown, projectRoot: string): Promise<void>;
|
|
474
|
-
/**
|
|
475
|
-
* Write file content back to disk (atomic write via tmp + rename).
|
|
476
|
-
*
|
|
477
|
-
* Guards against path traversal. Responds with
|
|
478
|
-
* `{ type: 'files.written', payload: { filePath, success } }`.
|
|
479
|
-
*/
|
|
480
|
-
declare function handleFilesWrite(ws: WebSocket, msg: unknown, projectRoot: string, opts?: FilesWriteOptions): Promise<void>;
|
|
481
|
-
/**
|
|
482
|
-
* Lightweight project file picker for the chat `@` mention popup.
|
|
483
|
-
*
|
|
484
|
-
* Walks `projectRoot` (max depth 8), skipping hidden and heavyweight
|
|
485
|
-
* dirs, then fuzzy-ranks results against `query`. Responds with
|
|
486
|
-
* `{ type: 'files.list', payload: { files } }`.
|
|
487
|
-
*/
|
|
488
|
-
declare function handleFilesList(ws: WebSocket, msg: unknown, projectRoot: string): Promise<void>;
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Context-aware editor completion for the WebUI Monaco surface.
|
|
492
|
-
*
|
|
493
|
-
* The handler combines fast symbol-index hits with a short, JSON-only LLM call.
|
|
494
|
-
* It is intentionally side-effect free: it never writes files and only reads the
|
|
495
|
-
* existing codebase index when available.
|
|
496
|
-
*/
|
|
497
|
-
|
|
498
|
-
type CompletionItemKind = 'text' | 'method' | 'function' | 'constructor' | 'field' | 'variable' | 'class' | 'interface' | 'module' | 'property' | 'unit' | 'value' | 'enum' | 'keyword' | 'snippet' | 'file' | 'reference';
|
|
499
|
-
interface CompletionSuggestion {
|
|
500
|
-
label: string;
|
|
501
|
-
insertText: string;
|
|
502
|
-
kind?: CompletionItemKind | undefined;
|
|
503
|
-
detail?: string | undefined;
|
|
504
|
-
documentation?: string | undefined;
|
|
505
|
-
sortText?: string | undefined;
|
|
506
|
-
source?: 'llm' | 'index' | 'lsp' | undefined;
|
|
507
|
-
}
|
|
508
|
-
interface CompletionHandlerOptions {
|
|
509
|
-
projectRoot: string;
|
|
510
|
-
provider?: Provider | undefined;
|
|
511
|
-
model?: string | undefined;
|
|
512
|
-
indexDir?: string | undefined;
|
|
513
|
-
lspCompletion?: LspCompletionSource | undefined;
|
|
514
|
-
timeoutMs?: number | undefined;
|
|
515
|
-
}
|
|
516
|
-
interface LspCompletionSourceRequest {
|
|
517
|
-
filePath: string;
|
|
518
|
-
lineNumber: number;
|
|
519
|
-
column: number;
|
|
520
|
-
content?: string | undefined;
|
|
521
|
-
triggerCharacter?: string | undefined;
|
|
522
|
-
signal: AbortSignal;
|
|
523
|
-
}
|
|
524
|
-
type LspCompletionSource = (request: LspCompletionSourceRequest) => Promise<CompletionSuggestion[]>;
|
|
525
|
-
declare function handleCompletionRequest(ws: WebSocket, msg: unknown, opts: CompletionHandlerOptions): Promise<void>;
|
|
526
|
-
declare function createToolLspCompletionSource(tool: Tool | undefined, ctx: Context): LspCompletionSource | undefined;
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Shared `git.info` WebSocket handler for both the standalone WebUI server and
|
|
530
|
-
* the CLI's `--webui` embedded server. Extracted from the duplicated switch
|
|
531
|
-
* cases in `index.ts` and `cli/src/webui-server.ts`, which had drifted (the
|
|
532
|
-
* standalone copy transposed ahead/behind and never matched deletions). One
|
|
533
|
-
* implementation here keeps both surfaces in lockstep.
|
|
534
|
-
*
|
|
535
|
-
* case 'git.info': return handleGitInfo(ws, projectRoot);
|
|
536
|
-
*/
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Read git branch, change stats, and upstream sync status from `projectRoot`
|
|
540
|
-
* and broadcast a `git.info` message. Never throws — a non-repo / missing-git
|
|
541
|
-
* directory yields an empty-but-valid payload.
|
|
542
|
-
*/
|
|
543
|
-
declare function handleGitInfo(ws: WebSocket, projectRoot: string): Promise<void>;
|
|
544
|
-
/**
|
|
545
|
-
* Read the working-tree change set (everything that differs from HEAD:
|
|
546
|
-
* staged, unstaged, and untracked) and broadcast a `git.changes` message.
|
|
547
|
-
*
|
|
548
|
-
* The file list comes from `git status --porcelain -z` (NUL-delimited so
|
|
549
|
-
* paths with spaces/unicode survive intact, and renames are unambiguous).
|
|
550
|
-
* Per-file line counts come from `--numstat` of both the unstaged and the
|
|
551
|
-
* staged diff, summed. Untracked files intentionally report 0/0 here so the
|
|
552
|
-
* list view does not read every untracked file; `git.diff` loads a selected
|
|
553
|
-
* file lazily on demand.
|
|
554
|
-
* Never throws — a non-repo yields an empty list.
|
|
555
|
-
*/
|
|
556
|
-
declare function handleGitChanges(ws: WebSocket, projectRoot: string): Promise<void>;
|
|
557
|
-
/**
|
|
558
|
-
* Resolve the before/after text for a single file and broadcast a `git.diff`
|
|
559
|
-
* message. `oldText` is the file at HEAD (`git show HEAD:<path>`), `newText`
|
|
560
|
-
* is the current working-tree content. New/untracked files have empty
|
|
561
|
-
* `oldText`; deleted files have empty `newText`. Binary or oversized files
|
|
562
|
-
* are reported with a flag instead of content so the client can show a notice.
|
|
563
|
-
*/
|
|
564
|
-
declare function handleGitDiff(ws: WebSocket, projectRoot: string, path: string): Promise<void>;
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Shared memory-operation WebSocket handlers for both the standalone WebUI
|
|
568
|
-
* server and the CLI's `--webui` embedded server. Extracted from the
|
|
569
|
-
* duplicated switch cases in `index.ts` and `cli/src/webui-server.ts`.
|
|
570
|
-
*
|
|
571
|
-
* Each function handles the full request→response cycle for one message
|
|
572
|
-
* type. Callers drop them into their switch statement:
|
|
573
|
-
*
|
|
574
|
-
* case 'memory.list': return handleMemoryList(ws, memoryStore);
|
|
575
|
-
*/
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* List all memory entries across all scopes.
|
|
579
|
-
* Responds with `{ type: 'memory.list', payload: { text } }`.
|
|
580
|
-
*/
|
|
581
|
-
declare function handleMemoryList(ws: WebSocket, memoryStore: MemoryStore): Promise<void>;
|
|
582
|
-
/**
|
|
583
|
-
* Persist a new memory entry.
|
|
584
|
-
* Responds with `{ type: 'key.operation_result', payload: { success, message } }`.
|
|
585
|
-
*/
|
|
586
|
-
declare function handleMemoryRemember(ws: WebSocket, msg: unknown, memoryStore: MemoryStore): Promise<void>;
|
|
587
|
-
/**
|
|
588
|
-
* Remove memory entries matching the given text.
|
|
589
|
-
* Responds with `{ type: 'key.operation_result', payload: { success, message } }`.
|
|
590
|
-
*/
|
|
591
|
-
declare function handleMemoryForget(ws: WebSocket, msg: unknown, memoryStore: MemoryStore): Promise<void>;
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* MCP management handlers for the WebUI server (both the standalone
|
|
595
|
-
* `wstackui` server and the CLI's embedded `--webui` server).
|
|
596
|
-
*
|
|
597
|
-
* These are thin WebSocket translators over the shared, surface-agnostic
|
|
598
|
-
* management core in `@wrongstack/mcp` (`manage.ts`) — the SAME core the REPL
|
|
599
|
-
* `/mcp` command writes against (same config.json, same MCPRegistry). All the
|
|
600
|
-
* config IO, url/header persistence, and live registry start/stop logic lives
|
|
601
|
-
* there; here we only map structured results to WS events the browser expects.
|
|
602
|
-
*/
|
|
603
|
-
|
|
604
|
-
/** mcp.list — configured servers merged with live registry status + tools. */
|
|
605
|
-
declare function handleMcpList(ws: WebSocket, _msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
606
|
-
/** mcp.add — persist a new server (incl. url/headers) and start it if enabled. */
|
|
607
|
-
declare function handleMcpAdd(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
608
|
-
/** mcp.update — re-persist config (incl. url/headers) and re-apply to registry. */
|
|
609
|
-
declare function handleMcpUpdate(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
610
|
-
/** mcp.remove — stop the server and delete it from config. */
|
|
611
|
-
declare function handleMcpRemove(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
612
|
-
/** mcp.enable — flip enabled:true in config and start the server. */
|
|
613
|
-
declare function handleMcpEnable(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
614
|
-
/** mcp.disable — stop the server and flip enabled:false in config. */
|
|
615
|
-
declare function handleMcpDisable(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
616
|
-
/** mcp.sleep — stop a running server (config stays enabled). */
|
|
617
|
-
declare function handleMcpSleep(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
618
|
-
/** mcp.wake — restart a sleeping/stopped server from config. */
|
|
619
|
-
declare function handleMcpWake(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
620
|
-
/** mcp.restart — stop + start a server. */
|
|
621
|
-
declare function handleMcpRestart(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
622
|
-
/** mcp.discover — ensure the server is running and report its live tools. */
|
|
623
|
-
declare function handleMcpDiscover(ws: WebSocket, msg: WSClientMessage, globalConfigPath: string, mcpRegistry?: MCPRegistry): Promise<void>;
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* Custom context modes — user-defined presets that are loaded from disk,
|
|
627
|
-
* merged with the built-in modes, and managed via WebSocket CRUD handlers.
|
|
628
|
-
*
|
|
629
|
-
* Stored in: ~/.wrongstack/custom-context-modes.json
|
|
630
|
-
* Format: { "modes": ContextWindowMode[] }
|
|
631
|
-
*/
|
|
632
|
-
interface CustomContextMode {
|
|
633
|
-
id: string;
|
|
634
|
-
name: string;
|
|
635
|
-
description: string;
|
|
636
|
-
thresholds: {
|
|
637
|
-
warn: number;
|
|
638
|
-
soft: number;
|
|
639
|
-
hard: number;
|
|
640
|
-
};
|
|
641
|
-
aggressiveOn: string;
|
|
642
|
-
preserveK: number;
|
|
643
|
-
eliseThreshold: number;
|
|
644
|
-
targetLoad: number;
|
|
645
|
-
/** Whether this is a user-defined (custom) or built-in mode. */
|
|
646
|
-
custom: boolean;
|
|
647
|
-
}
|
|
648
|
-
interface CustomModeStore {
|
|
649
|
-
modes: Map<string, CustomContextMode>;
|
|
650
|
-
load: () => Promise<void>;
|
|
651
|
-
save: () => Promise<void>;
|
|
652
|
-
create: (mode: CustomContextMode) => {
|
|
653
|
-
ok: boolean;
|
|
654
|
-
error?: string | undefined;
|
|
655
|
-
};
|
|
656
|
-
update: (id: string, patch: Partial<CustomContextMode>) => {
|
|
657
|
-
ok: boolean;
|
|
658
|
-
error?: string | undefined;
|
|
659
|
-
};
|
|
660
|
-
remove: (id: string) => {
|
|
661
|
-
ok: boolean;
|
|
662
|
-
error?: string | undefined;
|
|
663
|
-
};
|
|
664
|
-
list: () => CustomContextMode[];
|
|
674
|
+
* Shared prompt-library WebSocket handlers for BOTH the standalone WebUI server
|
|
675
|
+
* (`packages/webui/src/server/index.ts`) and the CLI's `--webui` embedded server
|
|
676
|
+
* (`packages/cli/src/webui-server.ts`). One source of truth so the two servers
|
|
677
|
+
* never drift (the lesson from skills-handlers).
|
|
678
|
+
*
|
|
679
|
+
* Each function handles one request→response cycle; callers drop them into their
|
|
680
|
+
* switch:
|
|
681
|
+
*
|
|
682
|
+
* case 'prompts.search': return handlePromptsSearch(ws, promptsCtx, msg);
|
|
683
|
+
*
|
|
684
|
+
* The prompt library is read across three layers (builtin + user + project) by
|
|
685
|
+
* the injected `PromptLoader`; writes (create/favorite) go to the user layer
|
|
686
|
+
* with copy-on-write for builtins. Treat synced/builtin content as DATA — these
|
|
687
|
+
* handlers never execute it; the client inserts a chosen prompt into the chat
|
|
688
|
+
* input as an ordinary user turn.
|
|
689
|
+
*/
|
|
690
|
+
|
|
691
|
+
interface PromptsContext {
|
|
692
|
+
/** Backs all prompt ops. Absent ⇒ feature unavailable. */
|
|
693
|
+
promptLoader: PromptLoader | undefined;
|
|
694
|
+
/** Records per-slug insert counts (shared with CLI `/prompt recent`). */
|
|
695
|
+
promptUsage?: PromptUsageStore | undefined;
|
|
665
696
|
}
|
|
666
|
-
declare function
|
|
697
|
+
declare function handlePromptsList(ws: WSLike, ctx: PromptsContext): Promise<void>;
|
|
698
|
+
declare function handlePromptsSearch(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
699
|
+
declare function handlePromptsContent(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
700
|
+
declare function handlePromptsFavorite(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
701
|
+
declare function handlePromptsCreate(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
702
|
+
/** Record that a prompt was inserted (best-effort; feeds CLI `/prompt recent`). */
|
|
703
|
+
declare function handlePromptsUsed(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
704
|
+
/** Return recently-inserted prompt slugs (most-recent first) for the modal's Recent view. */
|
|
705
|
+
declare function handlePromptsRecent(ws: WSLike, ctx: PromptsContext): Promise<void>;
|
|
706
|
+
/** Minimal structural type for the ws.send sink (matches `ws` WebSocket). */
|
|
707
|
+
type WSLike = Parameters<typeof send>[0];
|
|
667
708
|
|
|
668
|
-
/** A hostname that refers to the local machine. */
|
|
669
|
-
declare function isLoopbackHostname(hostname: string): boolean;
|
|
670
|
-
/** True when the server is bound to a loopback interface (vs. LAN/0.0.0.0). */
|
|
671
|
-
declare function isLoopbackBind(wsHost: string): boolean;
|
|
672
709
|
/**
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
* byte-by-byte via response timing.
|
|
710
|
+
* Read the `providers` section from the global config, decrypting
|
|
711
|
+
* secret-bearing fields. Returns an empty record when the config file
|
|
712
|
+
* doesn't exist or has no `providers` key.
|
|
677
713
|
*/
|
|
678
|
-
declare function
|
|
679
|
-
/** Pull the `token` query param out of a request URL (`/?token=…`). */
|
|
680
|
-
declare function extractToken(url: string): string | undefined;
|
|
714
|
+
declare function loadSavedProviders(configPath: string, vault: SecretVault): Promise<Record<string, ProviderConfig>>;
|
|
681
715
|
/**
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
716
|
+
* Write `providers` back into the global config, encrypting secrets first.
|
|
717
|
+
* Refuses to overwrite a corrupt-but-existing config file (the operator
|
|
718
|
+
* should fix it manually). When the config file is missing (ENOENT), starts
|
|
719
|
+
* from an empty object.
|
|
686
720
|
*/
|
|
687
|
-
declare function
|
|
688
|
-
hostHeader: string | undefined;
|
|
689
|
-
wsHost: string;
|
|
690
|
-
allowedHostnames?: readonly string[] | undefined;
|
|
691
|
-
}): boolean;
|
|
692
|
-
interface VerifyClientInput {
|
|
693
|
-
/** Browser `Origin` header, or undefined for non-browser clients. */
|
|
694
|
-
origin?: string | undefined;
|
|
695
|
-
/** Request URL (`req.url`) — carries the `?token=…` query param. */
|
|
696
|
-
url: string;
|
|
697
|
-
/** `Host` header (`req.headers.host`). */
|
|
698
|
-
hostHeader?: string | undefined;
|
|
699
|
-
/** Peer address (`req.socket.remoteAddress`). */
|
|
700
|
-
remoteAddress?: string | undefined;
|
|
701
|
-
/** `Cookie` header (`req.headers.cookie`). Carries `ws_token=…` when the
|
|
702
|
-
* browser went through `/ws-auth` to set the HttpOnly auth cookie. */
|
|
703
|
-
cookieHeader?: string | string[] | undefined;
|
|
704
|
-
/** Host/interface the WS server is bound to. */
|
|
705
|
-
wsHost: string;
|
|
706
|
-
/** The server's generated auth token. */
|
|
707
|
-
expectedToken: string;
|
|
708
|
-
/** Force token auth even for loopback binds, useful behind public tunnels. */
|
|
709
|
-
requireToken?: boolean | undefined;
|
|
710
|
-
/** Extra Host header names allowed on loopback binds, e.g. a tunnel hostname. */
|
|
711
|
-
allowedHostnames?: readonly string[] | undefined;
|
|
712
|
-
/** Allow browser WS URL tokens for explicit public WS URLs where cookies cannot cross hostnames. */
|
|
713
|
-
allowBrowserUrlToken?: boolean | undefined;
|
|
714
|
-
}
|
|
721
|
+
declare function saveProviders(configPath: string, vault: SecretVault, providers: Record<string, ProviderConfig>): Promise<void>;
|
|
715
722
|
/**
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
* 1. `Cookie: ws_token=…` (browser clients that went through `/ws-auth`)
|
|
722
|
-
* 2. `?token=…` URL query param (non-browser clients: curl, scripts)
|
|
723
|
-
*
|
|
724
|
-
* Browser clients (with an `Origin` header) are restricted to the cookie path —
|
|
725
|
-
* URL token is rejected for them, closing the C-598 query-string token
|
|
726
|
-
* exposure class. Non-browser clients keep the URL-token fallback so curl
|
|
727
|
-
* and tests continue to work.
|
|
723
|
+
* Small helper for the standalone WebUI entry point: create a
|
|
724
|
+
* `{ load, save }` pair from a config path alone (uses the
|
|
725
|
+
* config-directory-relative `.key` file for the vault). The `--webui`
|
|
726
|
+
* CLI mode and the standalone server both need to read/write the
|
|
727
|
+
* `providers` map identically.
|
|
728
728
|
*/
|
|
729
|
-
declare function
|
|
729
|
+
declare function createProviderConfigIO(configPath: string): {
|
|
730
|
+
load: () => Promise<Record<string, ProviderConfig>>;
|
|
731
|
+
save: (providers: Record<string, ProviderConfig>) => Promise<void>;
|
|
732
|
+
};
|
|
730
733
|
|
|
731
734
|
/**
|
|
732
735
|
* Pure provider/API-key record transforms for the WebUI server's `key.*` and
|
|
@@ -781,147 +784,6 @@ declare function addProvider(providers: ProvidersRecord, payload: {
|
|
|
781
784
|
/** Remove an entire provider and all its keys. */
|
|
782
785
|
declare function removeProvider(providers: ProvidersRecord, providerId: string): KeyOpResult;
|
|
783
786
|
|
|
784
|
-
/**
|
|
785
|
-
* Read the `providers` section from the global config, decrypting
|
|
786
|
-
* secret-bearing fields. Returns an empty record when the config file
|
|
787
|
-
* doesn't exist or has no `providers` key.
|
|
788
|
-
*/
|
|
789
|
-
declare function loadSavedProviders(configPath: string, vault: SecretVault): Promise<Record<string, ProviderConfig>>;
|
|
790
|
-
/**
|
|
791
|
-
* Write `providers` back into the global config, encrypting secrets first.
|
|
792
|
-
* Refuses to overwrite a corrupt-but-existing config file (the operator
|
|
793
|
-
* should fix it manually). When the config file is missing (ENOENT), starts
|
|
794
|
-
* from an empty object.
|
|
795
|
-
*/
|
|
796
|
-
declare function saveProviders(configPath: string, vault: SecretVault, providers: Record<string, ProviderConfig>): Promise<void>;
|
|
797
|
-
/**
|
|
798
|
-
* Small helper for the standalone WebUI entry point: create a
|
|
799
|
-
* `{ load, save }` pair from a config path alone (uses the
|
|
800
|
-
* config-directory-relative `.key` file for the vault). The `--webui`
|
|
801
|
-
* CLI mode and the standalone server both need to read/write the
|
|
802
|
-
* `providers` map identically.
|
|
803
|
-
*/
|
|
804
|
-
declare function createProviderConfigIO(configPath: string): {
|
|
805
|
-
load: () => Promise<Record<string, ProviderConfig>>;
|
|
806
|
-
save: (providers: Record<string, ProviderConfig>) => Promise<void>;
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
interface AutoPhaseWSMessage {
|
|
810
|
-
type: string;
|
|
811
|
-
payload?: Record<string, unknown>;
|
|
812
|
-
}
|
|
813
|
-
/**
|
|
814
|
-
* AutoPhaseWebSocketHandler — WebSocket-based AutoPhase control.
|
|
815
|
-
*
|
|
816
|
-
* Message types:
|
|
817
|
-
* autophase.start → { title, phases?, autonomous? }
|
|
818
|
-
* autophase.pause → {}
|
|
819
|
-
* autophase.resume → {}
|
|
820
|
-
* autophase.stop → {}
|
|
821
|
-
* autophase.status → {}
|
|
822
|
-
* autophase.selectPhase → { phaseId }
|
|
823
|
-
* autophase.taskStatus → { taskId, status }
|
|
824
|
-
*/
|
|
825
|
-
declare class AutoPhaseWebSocketHandler {
|
|
826
|
-
private agent;
|
|
827
|
-
private context;
|
|
828
|
-
private logger;
|
|
829
|
-
private events?;
|
|
830
|
-
private projectRoot?;
|
|
831
|
-
private orchestrator;
|
|
832
|
-
private graph;
|
|
833
|
-
private store;
|
|
834
|
-
private clients;
|
|
835
|
-
private broadcastInterval;
|
|
836
|
-
/** Aborts in-flight task agents AND the planning turn when the run is stopped. */
|
|
837
|
-
private abort;
|
|
838
|
-
/** Set the instant a stop/clear/revert is requested, so a planning turn that
|
|
839
|
-
* resolves afterwards never launches the orchestrator (the abort alone can't
|
|
840
|
-
* cover the window between the LLM call resolving and the orchestrator start). */
|
|
841
|
-
private stopping;
|
|
842
|
-
/** Optional per-phase git-worktree isolation (lazily created at start). */
|
|
843
|
-
private worktrees;
|
|
844
|
-
/** Base branch + tip SHA captured at run start so a revert can git-revert the
|
|
845
|
-
* run's squash commits (history-preserving) instead of a destructive reset. */
|
|
846
|
-
private runBase;
|
|
847
|
-
/** Per-run worker identities so the board can show "who is on what". */
|
|
848
|
-
private usedNicknames;
|
|
849
|
-
constructor(agent: Agent, context: Context, logger: Logger, storeDir: string, events?: EventBus | undefined, projectRoot?: string | undefined);
|
|
850
|
-
addClient(ws: WebSocket): void;
|
|
851
|
-
handleMessage(msg: AutoPhaseWSMessage): Promise<void>;
|
|
852
|
-
private handleStart;
|
|
853
|
-
/**
|
|
854
|
-
* Halt the run NOW — at any phase. Sets `stopping` (so a planning turn that
|
|
855
|
-
* resolves afterwards bails), aborts in-flight agents, stops the orchestrator
|
|
856
|
-
* tick, and ends the live broadcast. The board is kept for review; use
|
|
857
|
-
* `autophase.clear` to reset or `autophase.revert` to undo the changes.
|
|
858
|
-
*/
|
|
859
|
-
private handleStop;
|
|
860
|
-
/**
|
|
861
|
-
* Stop + wipe: tear down phase worktrees and reset to an empty board so the UI
|
|
862
|
-
* returns to the start screen ("new one"). Does NOT touch already-merged commits
|
|
863
|
-
* on the base branch — that is `autophase.revert`.
|
|
864
|
-
*/
|
|
865
|
-
private handleClear;
|
|
866
|
-
/**
|
|
867
|
-
* Stop + undo: remove phase worktrees, then history-preservingly `git revert`
|
|
868
|
-
* every commit this run landed on the base branch (captured `runBase`..HEAD),
|
|
869
|
-
* then reset to an empty board. Refuses (reports a reason) on a dirty tree or a
|
|
870
|
-
* conflicting revert rather than leaving the tree half-reverted.
|
|
871
|
-
*/
|
|
872
|
-
private handleRevert;
|
|
873
|
-
/** Generic fallback phases when the LLM planner produces nothing usable. */
|
|
874
|
-
private defaultPhases;
|
|
875
|
-
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
876
|
-
* The caller passes the run's abort signal so a stop during planning cancels
|
|
877
|
-
* the LLM turn (the previous fresh, never-aborted controller made planning
|
|
878
|
-
* uninterruptible). */
|
|
879
|
-
private planPhases;
|
|
880
|
-
private executeTaskWithAgent;
|
|
881
|
-
/** Persist + broadcast after an interactive board mutation. */
|
|
882
|
-
private afterBoardMutation;
|
|
883
|
-
private handleTaskStatusChange;
|
|
884
|
-
private startBroadcast;
|
|
885
|
-
private stopBroadcast;
|
|
886
|
-
private broadcastState;
|
|
887
|
-
private buildState;
|
|
888
|
-
private sendState;
|
|
889
|
-
private broadcast;
|
|
890
|
-
private send;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
interface SpecsWSMessage {
|
|
894
|
-
type: string;
|
|
895
|
-
payload?: Record<string, unknown>;
|
|
896
|
-
}
|
|
897
|
-
/**
|
|
898
|
-
* SpecsWebSocketHandler — read-only-ish browser of persisted SDD specs and their
|
|
899
|
-
* task graphs, rendered as a FORGE-style dependency board (topological phase
|
|
900
|
-
* columns + dependency refs). Shared by both webui servers via specs-routes.
|
|
901
|
-
*
|
|
902
|
-
* Message types:
|
|
903
|
-
* specs.list → all specs + progress
|
|
904
|
-
* specs.get { specId } → one spec's dependency board
|
|
905
|
-
* specs.taskStatus { graphId, taskId, status } → update + rebroadcast
|
|
906
|
-
*/
|
|
907
|
-
declare class SpecsWebSocketHandler {
|
|
908
|
-
private specStore;
|
|
909
|
-
private graphStore;
|
|
910
|
-
private clients;
|
|
911
|
-
constructor(specsDir: string, taskGraphsDir: string);
|
|
912
|
-
addClient(ws: WebSocket): void;
|
|
913
|
-
handleMessage(msg: SpecsWSMessage): Promise<void>;
|
|
914
|
-
private buildList;
|
|
915
|
-
private broadcastList;
|
|
916
|
-
private sendList;
|
|
917
|
-
private broadcastDetail;
|
|
918
|
-
private findGraphForSpec;
|
|
919
|
-
private buildDetail;
|
|
920
|
-
private updateTaskStatus;
|
|
921
|
-
private broadcast;
|
|
922
|
-
private send;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
787
|
interface SddBoardWSMessage {
|
|
926
788
|
type: string;
|
|
927
789
|
payload?: Record<string, unknown>;
|
|
@@ -1061,7 +923,18 @@ interface SddWizardWiringOptions {
|
|
|
1061
923
|
projectDir: string;
|
|
1062
924
|
};
|
|
1063
925
|
}
|
|
1064
|
-
declare function buildSddWizardDeps(opts: SddWizardWiringOptions): SddWizardDeps;
|
|
926
|
+
declare function buildSddWizardDeps(opts: SddWizardWiringOptions): SddWizardDeps;
|
|
927
|
+
|
|
928
|
+
type ShellOpenTarget = 'terminal' | 'file-manager';
|
|
929
|
+
interface ShellOpenRequest {
|
|
930
|
+
path: string;
|
|
931
|
+
target: ShellOpenTarget;
|
|
932
|
+
}
|
|
933
|
+
interface ShellOpenResult {
|
|
934
|
+
success: boolean;
|
|
935
|
+
message: string;
|
|
936
|
+
}
|
|
937
|
+
declare function handleShellOpen(req: ShellOpenRequest, logger: Logger): Promise<ShellOpenResult>;
|
|
1065
938
|
|
|
1066
939
|
/**
|
|
1067
940
|
* Shared skills WebSocket handlers for both the standalone WebUI server
|
|
@@ -1128,81 +1001,208 @@ declare function handleSkillsEdit(ws: WebSocket, ctx: SkillsContext, msg: unknow
|
|
|
1128
1001
|
*/
|
|
1129
1002
|
declare function handleSkillsExport(ws: WebSocket, ctx: SkillsContext): Promise<void>;
|
|
1130
1003
|
|
|
1004
|
+
interface SpecsWSMessage {
|
|
1005
|
+
type: string;
|
|
1006
|
+
payload?: Record<string, unknown>;
|
|
1007
|
+
}
|
|
1131
1008
|
/**
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
* never drift (the lesson from skills-handlers).
|
|
1136
|
-
*
|
|
1137
|
-
* Each function handles one request→response cycle; callers drop them into their
|
|
1138
|
-
* switch:
|
|
1139
|
-
*
|
|
1140
|
-
* case 'prompts.search': return handlePromptsSearch(ws, promptsCtx, msg);
|
|
1009
|
+
* SpecsWebSocketHandler — read-only-ish browser of persisted SDD specs and their
|
|
1010
|
+
* task graphs, rendered as a FORGE-style dependency board (topological phase
|
|
1011
|
+
* columns + dependency refs). Shared by both webui servers via specs-routes.
|
|
1141
1012
|
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
* input as an ordinary user turn.
|
|
1013
|
+
* Message types:
|
|
1014
|
+
* specs.list → all specs + progress
|
|
1015
|
+
* specs.get { specId } → one spec's dependency board
|
|
1016
|
+
* specs.taskStatus { graphId, taskId, status } → update + rebroadcast
|
|
1147
1017
|
*/
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1018
|
+
declare class SpecsWebSocketHandler {
|
|
1019
|
+
private specStore;
|
|
1020
|
+
private graphStore;
|
|
1021
|
+
private clients;
|
|
1022
|
+
constructor(specsDir: string, taskGraphsDir: string);
|
|
1023
|
+
addClient(ws: WebSocket): void;
|
|
1024
|
+
handleMessage(msg: SpecsWSMessage): Promise<void>;
|
|
1025
|
+
private buildList;
|
|
1026
|
+
private broadcastList;
|
|
1027
|
+
private sendList;
|
|
1028
|
+
private broadcastDetail;
|
|
1029
|
+
private findGraphForSpec;
|
|
1030
|
+
private buildDetail;
|
|
1031
|
+
private updateTaskStatus;
|
|
1032
|
+
private broadcast;
|
|
1033
|
+
private send;
|
|
1154
1034
|
}
|
|
1155
|
-
declare function handlePromptsList(ws: WSLike, ctx: PromptsContext): Promise<void>;
|
|
1156
|
-
declare function handlePromptsSearch(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
1157
|
-
declare function handlePromptsContent(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
1158
|
-
declare function handlePromptsFavorite(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
1159
|
-
declare function handlePromptsCreate(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
1160
|
-
/** Record that a prompt was inserted (best-effort; feeds CLI `/prompt recent`). */
|
|
1161
|
-
declare function handlePromptsUsed(ws: WSLike, ctx: PromptsContext, msg: unknown): Promise<void>;
|
|
1162
|
-
/** Return recently-inserted prompt slugs (most-recent first) for the modal's Recent view. */
|
|
1163
|
-
declare function handlePromptsRecent(ws: WSLike, ctx: PromptsContext): Promise<void>;
|
|
1164
|
-
/** Minimal structural type for the ws.send sink (matches `ws` WebSocket). */
|
|
1165
|
-
type WSLike = Parameters<typeof send>[0];
|
|
1166
1035
|
|
|
1167
1036
|
/**
|
|
1168
|
-
*
|
|
1169
|
-
* (`packages/webui/src/server/index.ts`) and the CLI's `--webui` embedded
|
|
1170
|
-
* server (`packages/cli/src/webui-server.ts`). One source of truth keeps the two
|
|
1171
|
-
* servers at parity (enforced by ws-handler-parity.test.ts).
|
|
1037
|
+
* Per-section context-window token estimate for the `context.debug` command.
|
|
1172
1038
|
*
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1039
|
+
* Uses the simple 4-chars-per-token heuristic — not exact, but close enough to
|
|
1040
|
+
* spot which section (system prompt, tool schemas, or message history) is
|
|
1041
|
+
* eating the context window. Tool schemas in particular are easy to overlook:
|
|
1042
|
+
* each tool ships its full JSON schema to the model every turn, so 20+ builtins
|
|
1043
|
+
* can cost 10-20k tokens on their own.
|
|
1178
1044
|
*
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
* writes the (override-applied) tokens to a real theme file on disk.
|
|
1045
|
+
* Extracted from `index.ts` as a pure function so the breakdown maths can be
|
|
1046
|
+
* unit tested without standing up a Context/ToolRegistry.
|
|
1182
1047
|
*/
|
|
1048
|
+
/** 4-chars-per-token heuristic estimate for a string. */
|
|
1049
|
+
declare function estimateTokens(s: string): number;
|
|
1050
|
+
/** Stringify arbitrary content for length estimation (JSON, with fallbacks). */
|
|
1051
|
+
declare function stringifyContent(c: unknown): string;
|
|
1052
|
+
interface ToolTokenEntry {
|
|
1053
|
+
name: string;
|
|
1054
|
+
tokens: number;
|
|
1055
|
+
}
|
|
1056
|
+
interface MessageTokenEntry {
|
|
1057
|
+
index: number;
|
|
1058
|
+
role: string;
|
|
1059
|
+
tokens: number;
|
|
1060
|
+
preview: string;
|
|
1061
|
+
}
|
|
1062
|
+
interface ContextBreakdown {
|
|
1063
|
+
total: number;
|
|
1064
|
+
systemPrompt: number;
|
|
1065
|
+
tools: {
|
|
1066
|
+
total: number;
|
|
1067
|
+
count: number;
|
|
1068
|
+
breakdown: ToolTokenEntry[];
|
|
1069
|
+
};
|
|
1070
|
+
messages: {
|
|
1071
|
+
total: number;
|
|
1072
|
+
count: number;
|
|
1073
|
+
breakdown: MessageTokenEntry[];
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
declare function messageTokens(content: unknown): number;
|
|
1077
|
+
declare function messagePreview(content: unknown): string;
|
|
1183
1078
|
|
|
1184
|
-
interface
|
|
1079
|
+
interface WorktreeManagementDeps {
|
|
1185
1080
|
projectRoot: string;
|
|
1186
|
-
/**
|
|
1187
|
-
|
|
1188
|
-
meta: Record<string, unknown>;
|
|
1189
|
-
} | undefined;
|
|
1081
|
+
/** Board snapshot dir — powers the cross-process liveness guard on cleanup. */
|
|
1082
|
+
boardsDir: string;
|
|
1190
1083
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1084
|
+
/**
|
|
1085
|
+
* WorktreeWebSocketHandler — mirrors AutoPhaseWebSocketHandler. Subscribes to
|
|
1086
|
+
* the shared EventBus `worktree.*` lifecycle events, keeps a live snapshot of
|
|
1087
|
+
* every worktree, and broadcasts:
|
|
1088
|
+
* - `worktree.event` incrementally (drives the flowing activity strip)
|
|
1089
|
+
* - `worktree.state` on connect + on a 2s timer (drives swim-lanes/DAG)
|
|
1090
|
+
*/
|
|
1091
|
+
declare class WorktreeWebSocketHandler {
|
|
1092
|
+
private readonly events;
|
|
1093
|
+
private readonly logger;
|
|
1094
|
+
private readonly management?;
|
|
1095
|
+
private readonly clients;
|
|
1096
|
+
private readonly handles;
|
|
1097
|
+
private baseBranch;
|
|
1098
|
+
private broadcastInterval;
|
|
1099
|
+
private readonly offs;
|
|
1100
|
+
constructor(events: EventBus, logger: Logger, management?: WorktreeManagementDeps | undefined);
|
|
1101
|
+
addClient(ws: WebSocket): void;
|
|
1102
|
+
/** Handle worktree-panel control messages (scan / clean / per-row ops). */
|
|
1103
|
+
handleMessage(msg: {
|
|
1104
|
+
type: string;
|
|
1105
|
+
payload?: Record<string, unknown>;
|
|
1106
|
+
}): Promise<boolean>;
|
|
1107
|
+
dispose(): void;
|
|
1108
|
+
/** Absolute managed-worktrees root for this project. */
|
|
1109
|
+
private worktreesRoot;
|
|
1110
|
+
/** True iff `dir` resolves strictly inside the managed worktrees root. */
|
|
1111
|
+
private underRoot;
|
|
1112
|
+
/** Branches of worktrees a live in-session run currently owns. */
|
|
1113
|
+
private liveActiveBranches;
|
|
1114
|
+
/**
|
|
1115
|
+
* Scan the disk for managed worktrees/branches NOT owned by a live in-session
|
|
1116
|
+
* run and broadcast them as orphans, with whether it is safe to clean now.
|
|
1117
|
+
* No-op (empty inventory) when management deps were not wired.
|
|
1118
|
+
*/
|
|
1119
|
+
private scanAndBroadcast;
|
|
1120
|
+
/**
|
|
1121
|
+
* Force-remove every orphaned worktree + branch. Refused while a run is live —
|
|
1122
|
+
* in this session (active handles) OR another process (the SDD board liveness
|
|
1123
|
+
* guard inside cleanupStaleSddWorktrees). Best-effort; reports the outcome.
|
|
1124
|
+
*/
|
|
1125
|
+
private cleanupOrphans;
|
|
1126
|
+
/** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
|
|
1127
|
+
private removeOne;
|
|
1128
|
+
/** Squash-merge ONE branch into base. Refused while a live run owns it. */
|
|
1129
|
+
private mergeBranch;
|
|
1130
|
+
/** Compact change summary for one worktree checkout. */
|
|
1131
|
+
private diffOne;
|
|
1132
|
+
private subscribe;
|
|
1133
|
+
private upsert;
|
|
1134
|
+
private patch;
|
|
1135
|
+
private activity;
|
|
1136
|
+
private stateMessage;
|
|
1137
|
+
private broadcastState;
|
|
1138
|
+
private ensureBroadcast;
|
|
1139
|
+
private stopBroadcast;
|
|
1140
|
+
private broadcast;
|
|
1141
|
+
private send;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/** A hostname that refers to the local machine. */
|
|
1145
|
+
declare function isLoopbackHostname(hostname: string): boolean;
|
|
1146
|
+
/** True when the server is bound to a loopback interface (vs. LAN/0.0.0.0). */
|
|
1147
|
+
declare function isLoopbackBind(wsHost: string): boolean;
|
|
1148
|
+
/**
|
|
1149
|
+
* Constant-time comparison of a provided token against the expected one.
|
|
1150
|
+
* A length mismatch short-circuits (lengths aren't secret); equal-length
|
|
1151
|
+
* inputs are compared with `timingSafeEqual` so the token can't be recovered
|
|
1152
|
+
* byte-by-byte via response timing.
|
|
1153
|
+
*/
|
|
1154
|
+
declare function tokenMatches(provided: string | undefined, expected: string): boolean;
|
|
1155
|
+
/** Pull the `token` query param out of a request URL (`/?token=…`). */
|
|
1156
|
+
declare function extractToken(url: string): string | undefined;
|
|
1157
|
+
/**
|
|
1158
|
+
* DNS-rebinding defense. On a loopback bind, the `Host` header must resolve to
|
|
1159
|
+
* a loopback name. When the operator deliberately exposes the socket (wsHost is
|
|
1160
|
+
* a LAN/0.0.0.0 address) the Host is legitimately non-loopback, so the guard is
|
|
1161
|
+
* skipped and connection auth falls to the token check.
|
|
1162
|
+
*/
|
|
1163
|
+
declare function hostHeaderOk(input: {
|
|
1164
|
+
hostHeader: string | undefined;
|
|
1165
|
+
wsHost: string;
|
|
1166
|
+
allowedHostnames?: readonly string[] | undefined;
|
|
1167
|
+
}): boolean;
|
|
1168
|
+
interface VerifyClientInput {
|
|
1169
|
+
/** Browser `Origin` header, or undefined for non-browser clients. */
|
|
1170
|
+
origin?: string | undefined;
|
|
1171
|
+
/** Request URL (`req.url`) — carries the `?token=…` query param. */
|
|
1172
|
+
url: string;
|
|
1173
|
+
/** `Host` header (`req.headers.host`). */
|
|
1174
|
+
hostHeader?: string | undefined;
|
|
1175
|
+
/** Peer address (`req.socket.remoteAddress`). */
|
|
1176
|
+
remoteAddress?: string | undefined;
|
|
1177
|
+
/** `Cookie` header (`req.headers.cookie`). Carries `ws_token=…` when the
|
|
1178
|
+
* browser went through `/ws-auth` to set the HttpOnly auth cookie. */
|
|
1179
|
+
cookieHeader?: string | string[] | undefined;
|
|
1180
|
+
/** Host/interface the WS server is bound to. */
|
|
1181
|
+
wsHost: string;
|
|
1182
|
+
/** The server's generated auth token. */
|
|
1183
|
+
expectedToken: string;
|
|
1184
|
+
/** Force token auth even for loopback binds, useful behind public tunnels. */
|
|
1185
|
+
requireToken?: boolean | undefined;
|
|
1186
|
+
/** Extra Host header names allowed on loopback binds, e.g. a tunnel hostname. */
|
|
1187
|
+
allowedHostnames?: readonly string[] | undefined;
|
|
1188
|
+
/** Allow browser WS URL tokens for explicit public WS URLs where cookies cannot cross hostnames. */
|
|
1189
|
+
allowBrowserUrlToken?: boolean | undefined;
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Decide whether to accept an incoming WebSocket handshake. Pure mirror of the
|
|
1193
|
+
* closure previously inlined in `index.ts`; see the module doc for the layered
|
|
1194
|
+
* policy. Returns `true` to accept, `false` to reject.
|
|
1195
|
+
*
|
|
1196
|
+
* Token sources, in priority order:
|
|
1197
|
+
* 1. `Cookie: ws_token=…` (browser clients that went through `/ws-auth`)
|
|
1198
|
+
* 2. `?token=…` URL query param (non-browser clients: curl, scripts)
|
|
1199
|
+
*
|
|
1200
|
+
* Browser clients (with an `Origin` header) are restricted to the cookie path —
|
|
1201
|
+
* URL token is rejected for them, closing the C-598 query-string token
|
|
1202
|
+
* exposure class. Non-browser clients keep the URL-token fallback so curl
|
|
1203
|
+
* and tests continue to work.
|
|
1204
|
+
*/
|
|
1205
|
+
declare function verifyClient(input: VerifyClientInput): boolean;
|
|
1206
1206
|
|
|
1207
1207
|
declare function startWebUI(opts?: WebUIOptions & {
|
|
1208
1208
|
wsPort?: number | undefined;
|