@wrongstack/webui-server 0.307.1 → 0.308.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.
@@ -73,6 +73,12 @@ export interface StaticServeOptions {
73
73
  * listening and the WebSocketServer being attached.
74
74
  */
75
75
  deferListen?: boolean | undefined;
76
+ /**
77
+ * Fail-fast port binding (no auto-advance on EADDRINUSE). Mirrors
78
+ * `WEBUI_STRICT_PORT` semantics for hosts that resolved the port without
79
+ * probing.
80
+ */
81
+ strictPort?: boolean | undefined;
76
82
  /** Package-resolution/build seams supplied by the owning host. */
77
83
  ensureDistDeps?: EnsureDistDeps | undefined;
78
84
  /**
@@ -84,6 +90,16 @@ export interface StaticServeOptions {
84
90
  * routes correctly answer 503.
85
91
  */
86
92
  intakeService?: CreateHttpServerOptions['intakeService'];
93
+ /**
94
+ * Optional vector-memory store. When provided, the four
95
+ * `/api/vector-memory/{status,search,store,store/:id}` endpoints become
96
+ * active. When omitted, the routes respond with `{ enabled: false }` or
97
+ * 503 — a non-CLI webui-server host stays on its existing surface with
98
+ * zero behavior change.
99
+ */
100
+ getVectorMemoryStore?: CreateHttpServerOptions['getVectorMemoryStore'];
101
+ /** Model cache directory for the vector-memory provider. */
102
+ vectorMemoryModelCacheDir?: string | undefined;
87
103
  }
88
104
  /**
89
105
  * Resolve the webui package's built `dist` directory.
@@ -1,5 +1,6 @@
1
1
  import type * as http from 'node:http';
2
2
  import { type TechStackEvent } from '../techstack-handlers.js';
3
+ import type { VectorMemoryStore } from '@wrongstack/vector-memory';
3
4
  export interface ApiRouterDeps {
4
5
  globalRoot?: string | undefined;
5
6
  projectRoot?: string | undefined;
@@ -13,6 +14,23 @@ export interface ApiRouterDeps {
13
14
  model: string;
14
15
  } | undefined) | undefined;
15
16
  executePackageOperation?: import('../techstack-handlers.js').TechStackHandlerDeps['executePackageOperation'];
17
+ /**
18
+ * Optional vector memory store. When provided, the four
19
+ * `GET /api/vector-memory/status|search` and `POST /api/vector-memory/store`
20
+ * / `DELETE /api/vector-memory/store/:id` routes become active.
21
+ * Defaults to undefined so non-CLI webui-server hosts (e.g. a headless
22
+ * fleet dashboard) are unaffected.
23
+ */
24
+ getVectorMemoryStore?: (() => VectorMemoryStore | undefined) | undefined;
25
+ /** Model cache directory for the vector memory provider. */
26
+ vectorMemoryModelCacheDir?: string | undefined;
27
+ /**
28
+ * Optional SAGE memory store. When provided, the
29
+ * `GET /api/memory/search?q=…&explain=1` route becomes active. Same
30
+ * strict opt-in contract as the vector memory store — non-CLI hosts
31
+ * that don't wire a memory store stay on their existing surface.
32
+ */
33
+ getMemoryStore?: (() => import('@wrongstack/core/types').MemoryPort | undefined) | undefined;
16
34
  }
17
35
  export declare function handleApiRoutes(req: http.IncomingMessage, res: http.ServerResponse, url: URL, deps: ApiRouterDeps, requireAccessToken: boolean, accessTokenOk: boolean, getTechStackRuntime: () => Promise<{
18
36
  store: import('@wrongstack/techstack').TechStackStore;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Vector memory HTTP handlers — minimal visibility surface for the
3
+ * WebUI/SimpleUI. Exposes the active store/provider, the model cache
4
+ * location, entry counts, and a search endpoint. Strictly opt-in:
5
+ * `getVectorMemoryStore` defaults to undefined so non-CLI webui-server
6
+ * hosts (e.g. a headless fleet dashboard) are unaffected.
7
+ */
8
+ import type * as http from 'node:http';
9
+ import type { VectorMemoryStore } from '@wrongstack/vector-memory';
10
+ import type { MemoryPort } from '@wrongstack/core/types';
11
+ export interface VectorMemoryStatusResponse {
12
+ enabled: boolean;
13
+ storePath?: string | undefined;
14
+ modelCacheDir?: string | undefined;
15
+ providerId?: string | undefined;
16
+ modelId?: string | undefined;
17
+ dimensions?: number | undefined;
18
+ entries?: number | undefined;
19
+ vectors?: number | undefined;
20
+ providers?: string[] | undefined;
21
+ cache?: {
22
+ entries: number;
23
+ providers: number;
24
+ totalUseCount: number;
25
+ oldestLastUsedAt: string | null;
26
+ } | undefined;
27
+ }
28
+ export interface VectorMemorySearchHit {
29
+ id: string;
30
+ score: number;
31
+ text: string;
32
+ summary?: string | undefined;
33
+ tags: string[];
34
+ }
35
+ export interface VectorMemorySearchResponse {
36
+ hits: VectorMemorySearchHit[];
37
+ count: number;
38
+ /**
39
+ * Pairwise cosine similarity matrix between returned hits, in hit order.
40
+ * Cell [i][j] is the similarity between hits[i] and hits[j]. Optional —
41
+ * the route only computes it when `?similarity=1` is set, to keep the
42
+ * default response cheap. Used by the WebUI's heatmap view to surface
43
+ * whether the top-K results form coherent clusters.
44
+ */
45
+ similarity?: number[][] | undefined;
46
+ }
47
+ /** Shape the store exposes. Kept narrow so we don't leak the full class. */
48
+ export interface VectorMemorySnapshot {
49
+ storePath?: string | undefined;
50
+ modelCacheDir?: string | undefined;
51
+ stats: {
52
+ entries: number;
53
+ vectors: number;
54
+ providers: string[];
55
+ modelId: string;
56
+ dimensions: number;
57
+ };
58
+ }
59
+ /** Handle `GET /api/vector-memory/status`. */
60
+ export declare function handleVectorMemoryStatus(res: http.ServerResponse, getStore: () => VectorMemoryStore | undefined, opts?: {
61
+ projectRoot?: string;
62
+ modelCacheDir?: string;
63
+ }): Promise<void>;
64
+ /** Handle `GET /api/vector-memory/search?q=…&limit=…&threshold=…`. */
65
+ export declare function handleVectorMemorySearch(res: http.ServerResponse, url: URL, getStore: () => VectorMemoryStore | undefined): Promise<void>;
66
+ /** Handle `POST /api/vector-memory/store`. */
67
+ export declare function handleVectorMemoryStore(res: http.ServerResponse, req: http.IncomingMessage, getStore: () => VectorMemoryStore | undefined): Promise<void>;
68
+ /** Handle `DELETE /api/vector-memory/store/:id`. */
69
+ export declare function handleVectorMemoryForget(res: http.ServerResponse, url: URL, getStore: () => VectorMemoryStore | undefined): Promise<void>;
70
+ export interface MemorySearchHit {
71
+ id: string;
72
+ text: string;
73
+ kind: string;
74
+ status: string;
75
+ tags: string[];
76
+ /** Per-channel score breakdown — null on the side that didn't contribute. */
77
+ lexicalScore: number | null;
78
+ vectorScore: number | null;
79
+ /** RRF-style combined score, monotonically higher = better. */
80
+ finalScore: number;
81
+ /** Which channel(s) produced this hit. */
82
+ source: 'lexical' | 'vector' | 'both';
83
+ }
84
+ export interface MemorySearchResponse {
85
+ hits: MemorySearchHit[];
86
+ count: number;
87
+ /**
88
+ * Where the score breakdown came from. `breakdown` means the rich
89
+ * variant was used (vector channel was wired); `lexical` means the
90
+ * fallback synthesized a position-derived score because the surface
91
+ * doesn't ship `searchSageWithBreakdown`. The UI branches on this to
92
+ * decide whether to show a dual-column score card or a single column.
93
+ */
94
+ channel: 'breakdown' | 'lexical';
95
+ }
96
+ /**
97
+ * Handle `GET /api/memory/search?q=…&limit=…&explain=1`.
98
+ *
99
+ * Returns SAGE search hits. When `explain=1` is set and the underlying
100
+ * memory store exposes `searchSageWithBreakdown`, each hit carries a
101
+ * per-channel score breakdown (lexical, vector, RRF final, `source`
102
+ * attribution). Without the flag — or when the rich variant isn't
103
+ * available — the response degrades to lexical-only hits with
104
+ * `vectorScore: null` and `source: 'lexical'`, plus a top-level
105
+ * `channel: 'lexical'` so the caller can branch.
106
+ */
107
+ export declare function handleMemorySearch(res: http.ServerResponse, url: URL, getStore: () => MemoryPort | undefined): Promise<void>;
108
+ //# sourceMappingURL=vector-memory-handlers.d.ts.map
@@ -121,6 +121,17 @@ export interface CreateHttpServerOptions {
121
121
  * respond 503.
122
122
  */
123
123
  intakeService?: import('@wrongstack/requirement-intake').RequirementIntakeService | undefined;
124
+ /**
125
+ * Optional vector-memory store. When provided, the four
126
+ * `/api/vector-memory/{status,search,store,store/:id}` endpoints become
127
+ * active and return live data. When omitted, the route responds with
128
+ * `{ enabled: false }` (status) or 503 (search/store/forget), so a
129
+ * non-CLI webui-server host stays on its existing surface with zero
130
+ * behavior change.
131
+ */
132
+ getVectorMemoryStore?: (() => import('@wrongstack/vector-memory').VectorMemoryStore | undefined) | undefined;
133
+ /** Model cache directory for the vector-memory provider. */
134
+ vectorMemoryModelCacheDir?: string | undefined;
124
135
  }
125
136
  /**
126
137
  * Create the static-file HTTP server. Returns the `http.Server` (not
@@ -69,7 +69,7 @@ export { browserOpenCommand, openBrowser } from './open-browser.js';
69
69
  export { isPathInside, resolveWorkingDirInsideProject } from './path-containment.js';
70
70
  export type { ConfirmDecision, PendingConfirm } from './pending-confirms.js';
71
71
  export { isDestructivePendingConfirm, resolveAllPendingConfirms, resolveYoloEligiblePendingConfirms, } from './pending-confirms.js';
72
- export { findFreePort, getSurfaceDefaultPorts, isPortFree, SURFACE_DEFAULT_PORTS, type SurfaceKind, surfaceLabel, } from './port-utils.js';
72
+ export { findFreePort, getSurfaceDefaultPorts, isPortFree, isStrictPort, listenWithRetry, SURFACE_DEFAULT_PORTS, type SurfaceKind, surfaceLabel, } from './port-utils.js';
73
73
  export { ensureDistDir, resolveDistDir, startStaticServe, type EnsureDistDeps, type ResolveDistOptions, type StaticServeDeps, type StaticServeHandle, type StaticServeOptions, } from './frontend-static-serve.js';
74
74
  export { announceWebuiReady, createWebuiShutdown, registerWebuiInstance, registerWebuiSignalHandlers, type AnnounceWebuiReadyParams, type RegisterWebuiInstanceDeps, type RegisterWebuiInstanceParams, type WebuiShutdownResources, } from './embedded-lifecycle.js';
75
75
  export { createStreamCoalescer, type StreamCoalescer, type StreamCoalescerDeps, type ToolProgressPayload, } from './stream-coalescer.js';
@@ -40,6 +40,25 @@ export declare function handleSageList(ws: WebSocket, memoryStore: MemoryPort):
40
40
  * a full `listSage()` for stores that predate `listSagePage`.
41
41
  */
42
42
  export declare function handleSageListPage(ws: WebSocket, msg: unknown, memoryStore: MemoryPort): Promise<void>;
43
+ /**
44
+ * Run a search and return the per-channel score breakdown
45
+ * (lexical / vector / final + `source` attribution). Used by the
46
+ * WebUI's Memory panel so the operator can see WHY a memory is in
47
+ * the result list.
48
+ *
49
+ * Request: { type: 'memory.sage.searchBreakdown', payload: { query, limit?, includeStale? } }
50
+ * Response: { type: 'memory.sage.searchBreakdown', payload: { hits, source } }
51
+ *
52
+ * `source` is one of:
53
+ * - `breakdown`: the underlying surface returned a `VectorAugmentHit[]`
54
+ * with both lexical and vector scores populated per hit.
55
+ * - `lexical`: the surface only has the flat `searchSage` — we wrap
56
+ * the results with a position-derived score and `source: 'lexical'`.
57
+ *
58
+ * Clients branch on `source` to decide whether to render the dual
59
+ * score panel or a single lexical column.
60
+ */
61
+ export declare function handleSageSearchBreakdown(ws: WebSocket, msg: unknown, memoryStore: MemoryPort): Promise<void>;
43
62
  /**
44
63
  * Get a single Sage entry by ID.
45
64
  * Request: { type: 'memory.sage.get', payload: { id } }
@@ -9,14 +9,15 @@
9
9
  * starts at 3456, SimpleUI at 3466. Set `WEBUI_STRICT_PORT=1` to disable.
10
10
  * - **TOCTOU window.** The probe in `findFreePort` binds a throwaway
11
11
  * `net.Server` then closes it, so there is a tiny race between "found free"
12
- * and "real server binds". For local multi-instance use that race is
13
- * negligible; if it ever loses, the real bind fails loudly with EADDRINUSE
14
- * exactly as before.
12
+ * and "real server binds". When that race is lost the real bind fails with
13
+ * EADDRINUSE; `listenWithRetry` is the safety net it advances past the
14
+ * competitor (bounded) instead of crashing or hanging.
15
15
  * - **Instance registry.** Every live process records itself in
16
16
  * `~/.wrongstack/webui-instances.json` with its PID, surface, port, and
17
17
  * project root. A crashed instance is pruned on the next registry read
18
18
  * (`process.kill(pid, 0)` probe), so ghosts don't accumulate.
19
19
  */
20
+ import * as net from 'node:net';
20
21
  /**
21
22
  * Surface-specific default single port.
22
23
  *
@@ -37,6 +38,12 @@ export declare const SURFACE_DEFAULT_PORTS: {
37
38
  export type SurfaceKind = keyof typeof SURFACE_DEFAULT_PORTS;
38
39
  /** Human-readable label for surfaces. */
39
40
  export declare function surfaceLabel(surface: SurfaceKind): string;
41
+ /**
42
+ * True when `WEBUI_STRICT_PORT` requests fail-fast port binding (no
43
+ * auto-advance). Single source of truth for both `resolvePorts` (probe-time)
44
+ * and `startWebUI`'s bind-time retry — the two must not drift apart.
45
+ */
46
+ export declare function isStrictPort(): boolean;
40
47
  /**
41
48
  * Return the surface-specific default HTTP port.
42
49
  */
@@ -56,4 +63,17 @@ export interface FindFreePortOptions {
56
63
  * `exclude`. Throws if nothing is free within `maxTries` steps.
57
64
  */
58
65
  export declare function findFreePort(host: string, startPort: number, opts?: FindFreePortOptions): Promise<number>;
66
+ export interface ListenWithRetryOptions {
67
+ /** Bind attempts before giving up on EADDRINUSE. Default 10. */
68
+ maxTries?: number | undefined;
69
+ }
70
+ /**
71
+ * Listen `server` on `host` starting at `port`, advancing one port on each
72
+ * EADDRINUSE — the bind-time safety net for the `findFreePort` TOCTOU window
73
+ * (probe closes, competitor binds, real listen loses the race). Any other
74
+ * error (EACCES, EAFNOSUPPORT, …) rejects immediately; EADDRINUSE past the
75
+ * final attempt rejects with the last error. Resolves with the port the
76
+ * server actually bound, which exceeds the request when a retry advanced.
77
+ */
78
+ export declare function listenWithRetry(server: net.Server, host: string, port: number, opts?: ListenWithRetryOptions): Promise<number>;
59
79
  //# sourceMappingURL=port-utils.d.ts.map
@@ -117,6 +117,16 @@ export declare function startHttpServer(opts: {
117
117
  /** Optional pre-built intake service (tests/embeds). Defaults to a fresh
118
118
  * per-project service backed by `projectRequirementIntakes`. */
119
119
  intakeService?: import('@wrongstack/requirement-intake').RequirementIntakeService | undefined;
120
+ /**
121
+ * Optional vector-memory store. When provided, the four
122
+ * `/api/vector-memory/{status,search,store,store/:id}` endpoints become
123
+ * active. When omitted, the routes respond with `{ enabled: false }` or
124
+ * 503 — a non-CLI webui-server host stays on its existing surface with
125
+ * zero behavior change.
126
+ */
127
+ getVectorMemoryStore?: (() => import('@wrongstack/vector-memory').VectorMemoryStore | undefined) | undefined;
128
+ /** Model cache directory for the vector-memory provider. */
129
+ vectorMemoryModelCacheDir?: string | undefined;
120
130
  }): import('node:http').Server;
121
131
  interface ShutdownDeps {
122
132
  flushSession: () => Promise<void>;
@@ -12,5 +12,13 @@ import * as http from 'node:http';
12
12
  * fallback for systems where `::` does not accept IPv4-mapped
13
13
  * connections (net.ipv6.bindv6only=1, some Windows configs).
14
14
  */
15
- export declare function setupCompanionServer(httpServer: http.Server, wsHost: string | undefined, httpPort: number): http.Server | null;
15
+ export declare function setupCompanionServer(httpServer: http.Server, wsHost: string | undefined,
16
+ /**
17
+ * The port the PRIMARY server actually bound — not the originally
18
+ * requested port. The companion must mirror it exactly: advertised URLs
19
+ * point at the primary's port, so a companion bound elsewhere would be
20
+ * unreachable dead weight. A single bind attempt; EADDRINUSE here means
21
+ * the mirror is impossible, so the companion degrades to null.
22
+ */
23
+ httpPort: number): Promise<http.Server | null>;
16
24
  //# sourceMappingURL=start-webui-companion.d.ts.map
@@ -38,6 +38,11 @@ export declare function setupWebuiShutdown(options: {
38
38
  clearEternalSubscription: () => void;
39
39
  codebaseIndexing: any;
40
40
  memoryStore: any;
41
+ /** Vector memory store (mirrors CLI's teardown). `undefined` when the
42
+ * standalone WebUI host failed to construct one (read-only FS, etc.). */
43
+ vectorMemoryStore: {
44
+ close(): void;
45
+ } | undefined;
41
46
  globalConfigPath: string;
42
47
  }): () => void;
43
48
  //# sourceMappingURL=start-webui-shutdown.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/webui-server",
3
- "version": "0.307.1",
3
+ "version": "0.308.1",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack WebUI HTTP/WebSocket server module — extracted from @wrongstack/webui in PR #243/244 to remove the CLI -> @wrongstack/webui/server cross-package edge (audit §3.1.1). Pure backend: HTTP routes, WebSocket handlers, MCP tool wrappers, HTML serving. The web frontend lives in @wrongstack/webui; this package is the standalone server it can run on.",
6
6
  "keywords": [
@@ -40,16 +40,17 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "ws": "^8.21.3",
43
- "@wrongstack/core": "0.307.1",
44
- "@wrongstack/providers": "0.307.1",
45
- "@wrongstack/kanban": "0.307.1",
46
- "@wrongstack/requirement-intake": "0.307.1",
47
- "@wrongstack/runtime": "0.307.1",
48
- "@wrongstack/mcp": "0.307.1",
49
- "@wrongstack/sage": "0.307.1",
50
- "@wrongstack/sdd": "0.307.1",
51
- "@wrongstack/tools": "0.307.1",
52
- "@wrongstack/techstack": "0.307.1"
43
+ "@wrongstack/core": "0.308.1",
44
+ "@wrongstack/requirement-intake": "0.308.1",
45
+ "@wrongstack/kanban": "0.308.1",
46
+ "@wrongstack/sdd": "0.308.1",
47
+ "@wrongstack/mcp": "0.308.1",
48
+ "@wrongstack/providers": "0.308.1",
49
+ "@wrongstack/runtime": "0.308.1",
50
+ "@wrongstack/techstack": "0.308.1",
51
+ "@wrongstack/sage": "0.308.1",
52
+ "@wrongstack/tools": "0.308.1",
53
+ "@wrongstack/vector-memory": "0.308.1"
53
54
  },
54
55
  "devDependencies": {
55
56
  "@types/node": "^26.2.0",
@@ -62,6 +63,7 @@
62
63
  "scripts": {
63
64
  "build": "node ../../scripts/build-package.mjs",
64
65
  "typecheck": "tsc --noEmit",
66
+ "test": "vitest run --config vitest.config.ts",
65
67
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
66
68
  }
67
69
  }