@wrongstack/webui 0.255.0 → 0.256.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { WebSocket } from 'ws';
2
- import { Agent, EventBus, SessionWriter, ToolRegistry, ModelsRegistry, ConfigStore, SecretVault, MemoryStore, ProviderConfig, ProviderApiKey, Context, Logger } from '@wrongstack/core';
2
+ import { Agent, EventBus, SessionStore, ToolRegistry, ModelsRegistry, ConfigStore, SecretVault, JournalEntry, Logger, MemoryStore, ProviderConfig, ProviderApiKey, Context } from '@wrongstack/core';
3
3
  import * as http from 'node:http';
4
4
 
5
5
  interface WSServerMessage {
@@ -13,11 +13,50 @@ interface WSClientMessage {
13
13
  interface WebUIOptions {
14
14
  port?: number | undefined;
15
15
  webuiPort?: number | undefined;
16
+ /**
17
+ * Pre-built backend services. When provided, `startWebUI` skips its
18
+ * default agent/event-bus/session/store construction and wires the
19
+ * supplied instances into the WS message router and HTTP API
20
+ * handlers instead.
21
+ *
22
+ * Intended for callers (most notably `cli/webui-server.ts`) that
23
+ * already own the agent lifecycle — the CLI's `runWebUI` constructs
24
+ * the Agent, EventBus, SessionStore, and friends so it can run an
25
+ * eternal iteration against them, then hands the lot to the webui
26
+ * for the human-facing surface.
27
+ *
28
+ * `session` is typed as `SessionStore` (read + write) rather than
29
+ * the narrower `SessionWriter` because `startWebUI` needs to
30
+ * `load()` existing session history to project the chat view, and
31
+ * `list()` past sessions to populate the sessions dashboard. A
32
+ * `SessionWriter`-only field would force `startWebUI` to take a
33
+ * separate `sessionStore` for reads, which is a worse API for the
34
+ * CLI caller (`runWebUI` already has one store, not two).
35
+ *
36
+ * When `services` is omitted, `startWebUI` retains its existing
37
+ * behavior (builds the defaults in-place). This keeps the standalone
38
+ * `node dist/index.js webui` flow fully back-compatible.
39
+ */
40
+ services?: BackendServices | undefined;
41
+ /**
42
+ * Subscribe to live per-iteration events from the eternal-autonomy
43
+ * engine. When provided, `startWebUI` wires a WS broadcast that
44
+ * pushes each `JournalEntry` to every connected client. Observability
45
+ * only — starting the loop still goes through REPL/TUI or `--eternal`,
46
+ * since the webui has no slash-command dispatch surface yet.
47
+ *
48
+ * The argument is a *function* the caller supplies that performs the
49
+ * actual subscription; the returned disposer is invoked on
50
+ * `shutdown()`. This indirection lets the caller (most commonly
51
+ * `cli/webui-server.ts`) own the engine lifecycle and merely hand the
52
+ * webui an observer slot.
53
+ */
54
+ subscribeEternalIteration?: ((fn: (entry: JournalEntry) => void) => () => void) | undefined;
16
55
  }
17
56
  interface BackendServices {
18
57
  agent: Agent;
19
58
  events: EventBus;
20
- session: SessionWriter;
59
+ session: SessionStore;
21
60
  toolRegistry: ToolRegistry;
22
61
  modelsRegistry: ModelsRegistry;
23
62
  configStore: ConfigStore;
@@ -49,6 +88,20 @@ interface CreateHttpServerOptions {
49
88
  * cross-process SessionRegistry.
50
89
  */
51
90
  globalRoot?: string | undefined;
91
+ /**
92
+ * Shared auth token for `/api/*` endpoints. Required for non-loopback
93
+ * binds (LAN exposure). Loopback binds accept any local origin without
94
+ * a token (the WS path's loopback-bootstrap policy — see ws-auth.ts).
95
+ */
96
+ apiToken?: string | undefined;
97
+ /**
98
+ * If true, the `/ws-auth` endpoint exchanges a `?token=` query param (or
99
+ * `X-WS-Token` header) for an `HttpOnly` auth cookie. The cookie is then
100
+ * sent automatically on the WS upgrade, closing the C-598 query-string
101
+ * token exposure class. Default: true. Set to false to keep the legacy
102
+ * URL-token-only flow (e.g. in tests that don't want cookie state).
103
+ */
104
+ enableWsCookie?: boolean | undefined;
52
105
  }
53
106
  /**
54
107
  * Inject the live WS port into the served HTML so the frontend connects to
@@ -116,6 +169,49 @@ declare function browserOpenCommand(url: string, platform?: NodeJS.Platform): {
116
169
  * process so it survives kill/killAll. Never throws. */
117
170
  declare function openBrowser(url: string, platform?: NodeJS.Platform): void;
118
171
 
172
+ /**
173
+ * Per-section context-window token estimate for the `context.debug` command.
174
+ *
175
+ * Uses the simple 4-chars-per-token heuristic — not exact, but close enough to
176
+ * spot which section (system prompt, tool schemas, or message history) is
177
+ * eating the context window. Tool schemas in particular are easy to overlook:
178
+ * each tool ships its full JSON schema to the model every turn, so 20+ builtins
179
+ * can cost 10-20k tokens on their own.
180
+ *
181
+ * Extracted from `index.ts` as a pure function so the breakdown maths can be
182
+ * unit tested without standing up a Context/ToolRegistry.
183
+ */
184
+ /** 4-chars-per-token heuristic estimate for a string. */
185
+ declare function estimateTokens(s: string): number;
186
+ /** Stringify arbitrary content for length estimation (JSON, with fallbacks). */
187
+ declare function stringifyContent(c: unknown): string;
188
+ interface ToolTokenEntry {
189
+ name: string;
190
+ tokens: number;
191
+ }
192
+ interface MessageTokenEntry {
193
+ index: number;
194
+ role: string;
195
+ tokens: number;
196
+ preview: string;
197
+ }
198
+ interface ContextBreakdown {
199
+ total: number;
200
+ systemPrompt: number;
201
+ tools: {
202
+ total: number;
203
+ count: number;
204
+ breakdown: ToolTokenEntry[];
205
+ };
206
+ messages: {
207
+ total: number;
208
+ count: number;
209
+ breakdown: MessageTokenEntry[];
210
+ };
211
+ }
212
+ declare function messageTokens(content: unknown): number;
213
+ declare function messagePreview(content: unknown): string;
214
+
119
215
  /**
120
216
  * Running-instance registry for the standalone WebUI server.
121
217
  *
@@ -172,6 +268,25 @@ declare function listInstances(baseDir?: string): Promise<WebUIInstanceRecord[]>
172
268
  /** Human-readable table of running instances for `webui --list`. */
173
269
  declare function formatInstances(instances: WebUIInstanceRecord[]): string;
174
270
 
271
+ type EternalSubscribe = (fn: (entry: JournalEntry) => void) => () => void;
272
+ type EternalBroadcast<C> = (clients: Map<WebSocket, C>, msg: WSServerMessage) => void;
273
+ interface EternalSubscription {
274
+ /** Tear down the underlying engine subscription. Idempotent. */
275
+ dispose: () => void;
276
+ }
277
+ declare function createEternalSubscription<C>(subscribe: EternalSubscribe, broadcast: EternalBroadcast<C>, clientsRef: () => Map<WebSocket, C>): EternalSubscription;
278
+
279
+ type ShellOpenTarget = 'terminal' | 'file-manager';
280
+ interface ShellOpenRequest {
281
+ path: string;
282
+ target: ShellOpenTarget;
283
+ }
284
+ interface ShellOpenResult {
285
+ success: boolean;
286
+ message: string;
287
+ }
288
+ declare function handleShellOpen(req: ShellOpenRequest, logger: Logger): Promise<ShellOpenResult>;
289
+
175
290
  /**
176
291
  * Send a JSON message to a single WebSocket client.
177
292
  * No-op when the socket is not in OPEN state (disconnected / closing).
@@ -342,6 +457,9 @@ interface VerifyClientInput {
342
457
  hostHeader?: string | undefined;
343
458
  /** Peer address (`req.socket.remoteAddress`). */
344
459
  remoteAddress?: string | undefined;
460
+ /** `Cookie` header (`req.headers.cookie`). Carries `ws_token=…` when the
461
+ * browser went through `/ws-auth` to set the HttpOnly auth cookie. */
462
+ cookieHeader?: string | string[] | undefined;
345
463
  /** Host/interface the WS server is bound to. */
346
464
  wsHost: string;
347
465
  /** The server's generated auth token. */
@@ -351,6 +469,15 @@ interface VerifyClientInput {
351
469
  * Decide whether to accept an incoming WebSocket handshake. Pure mirror of the
352
470
  * closure previously inlined in `index.ts`; see the module doc for the layered
353
471
  * policy. Returns `true` to accept, `false` to reject.
472
+ *
473
+ * Token sources, in priority order:
474
+ * 1. `Cookie: ws_token=…` (browser clients that went through `/ws-auth`)
475
+ * 2. `?token=…` URL query param (non-browser clients: curl, scripts)
476
+ *
477
+ * Browser clients (with an `Origin` header) are restricted to the cookie path —
478
+ * URL token is rejected for them, closing the C-598 query-string token
479
+ * exposure class. Non-browser clients keep the URL-token fallback so curl
480
+ * and tests continue to work.
354
481
  */
355
482
  declare function verifyClient(input: VerifyClientInput): boolean;
356
483
 
@@ -480,10 +607,10 @@ declare class AutoPhaseWebSocketHandler {
480
607
  private send;
481
608
  }
482
609
 
483
- declare function startWebUI(opts?: {
610
+ declare function startWebUI(opts?: WebUIOptions & {
484
611
  wsPort?: number | undefined;
485
612
  wsHost?: string | undefined;
486
613
  open?: boolean | undefined;
487
614
  }): Promise<void>;
488
615
 
489
- export { AutoPhaseWebSocketHandler, type BackendServices, type ConnectedClient, type CustomContextMode, type CustomModeStore, type KeyOpResult, type ProvidersRecord, type VerifyClientInput, type WSClientMessage, type WSServerMessage, type WebUIInstanceRecord, type WebUIOptions, addProvider, broadcast, browserOpenCommand, buildCspHeader, createCustomModeStore, createHttpServer, createProviderConfigIO, defaultBaseDir, deleteKey, errMessage, extractToken, findFreePort, formatInstances, generateAuthToken, handleFilesList, handleFilesRead, handleFilesTree, handleFilesWrite, handleMemoryForget, handleMemoryList, handleMemoryRemember, hostHeaderOk, injectWsPort, isLoopbackBind, isLoopbackHostname, isPortFree, listInstances, loadSavedProviders, maskedKey, normalizeKeys, openBrowser, registerInstance, registryPath, removeProvider, saveProviders, send, sendResult, setActiveKey, startWebUI, tokenMatches, unregisterInstance, upsertKey, verifyClient, writeKeysBack };
616
+ export { AutoPhaseWebSocketHandler, type BackendServices, type ConnectedClient, type ContextBreakdown, type CreateHttpServerOptions, type CustomContextMode, type CustomModeStore, type EternalBroadcast, type EternalSubscribe, type EternalSubscription, type KeyOpResult, type MessageTokenEntry, type ProvidersRecord, type ShellOpenRequest, type ShellOpenResult, type ShellOpenTarget, type ToolTokenEntry, type VerifyClientInput, type WSClientMessage, type WSServerMessage, type WebUIInstanceRecord, type WebUIOptions, addProvider, broadcast, browserOpenCommand, buildCspHeader, createCustomModeStore, createEternalSubscription, createHttpServer, createProviderConfigIO, defaultBaseDir, deleteKey, errMessage, estimateTokens, extractToken, findFreePort, formatInstances, generateAuthToken, handleFilesList, handleFilesRead, handleFilesTree, handleFilesWrite, handleMemoryForget, handleMemoryList, handleMemoryRemember, handleShellOpen, hostHeaderOk, injectWsPort, isLoopbackBind, isLoopbackHostname, isPortFree, listInstances, loadSavedProviders, maskedKey, messagePreview, messageTokens, normalizeKeys, openBrowser, registerInstance, registryPath, removeProvider, saveProviders, send, sendResult, setActiveKey, startWebUI, stringifyContent, tokenMatches, unregisterInstance, upsertKey, verifyClient, writeKeysBack };