@harness-fe/mcp-server 3.0.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/dist/auth.d.ts +53 -0
  4. package/dist/auth.js +212 -0
  5. package/dist/bridge.d.ts +302 -0
  6. package/dist/bridge.js +1580 -0
  7. package/dist/cli.d.ts +18 -0
  8. package/dist/cli.js +277 -0
  9. package/dist/daemon.d.ts +98 -0
  10. package/dist/daemon.js +80 -0
  11. package/dist/dashboardApi.d.ts +40 -0
  12. package/dist/dashboardApi.js +142 -0
  13. package/dist/dashboardSpa.d.ts +18 -0
  14. package/dist/dashboardSpa.js +180 -0
  15. package/dist/dashboardUrl.d.ts +13 -0
  16. package/dist/dashboardUrl.js +18 -0
  17. package/dist/eventsHandler.d.ts +24 -0
  18. package/dist/eventsHandler.js +114 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.js +6 -0
  21. package/dist/mcp.d.ts +15 -0
  22. package/dist/mcp.js +923 -0
  23. package/dist/mcpHttp.d.ts +39 -0
  24. package/dist/mcpHttp.js +49 -0
  25. package/dist/openBrowser.d.ts +33 -0
  26. package/dist/openBrowser.js +63 -0
  27. package/dist/remoteBridge.d.ts +61 -0
  28. package/dist/remoteBridge.js +307 -0
  29. package/dist/replayCreate.d.ts +36 -0
  30. package/dist/replayCreate.js +156 -0
  31. package/dist/replayViewer.d.ts +20 -0
  32. package/dist/replayViewer.js +168 -0
  33. package/dist/sessionRouter.d.ts +42 -0
  34. package/dist/sessionRouter.js +88 -0
  35. package/dist/store/JsonMemoryStore.d.ts +52 -0
  36. package/dist/store/JsonMemoryStore.js +119 -0
  37. package/dist/store/JsonTaskStore.d.ts +21 -0
  38. package/dist/store/JsonTaskStore.js +53 -0
  39. package/dist/store/JsonlStore.d.ts +128 -0
  40. package/dist/store/JsonlStore.js +1168 -0
  41. package/dist/store/MemoryEventStore.d.ts +47 -0
  42. package/dist/store/MemoryEventStore.js +111 -0
  43. package/dist/store/WriteQueue.d.ts +51 -0
  44. package/dist/store/WriteQueue.js +142 -0
  45. package/dist/store/index.d.ts +6 -0
  46. package/dist/store/index.js +5 -0
  47. package/dist/store/types.d.ts +416 -0
  48. package/dist/store/types.js +19 -0
  49. package/package.json +63 -0
  50. package/src/auth.test.ts +90 -0
  51. package/src/auth.ts +248 -0
  52. package/src/bridge-auth.test.ts +196 -0
  53. package/src/bridge.test.ts +1708 -0
  54. package/src/bridge.ts +1804 -0
  55. package/src/cli.ts +315 -0
  56. package/src/daemon.test.ts +123 -0
  57. package/src/daemon.ts +161 -0
  58. package/src/dashboardApi.test.ts +235 -0
  59. package/src/dashboardApi.ts +184 -0
  60. package/src/dashboardSpa.test.ts +239 -0
  61. package/src/dashboardSpa.ts +195 -0
  62. package/src/dashboardUrl.test.ts +46 -0
  63. package/src/dashboardUrl.ts +28 -0
  64. package/src/eventsHandler.test.ts +247 -0
  65. package/src/eventsHandler.ts +136 -0
  66. package/src/index.ts +26 -0
  67. package/src/mcp.ts +1407 -0
  68. package/src/mcpHttp.test.ts +101 -0
  69. package/src/mcpHttp.ts +88 -0
  70. package/src/openBrowser.test.ts +103 -0
  71. package/src/openBrowser.ts +81 -0
  72. package/src/remoteBridge.test.ts +119 -0
  73. package/src/remoteBridge.ts +404 -0
  74. package/src/replay.test.ts +271 -0
  75. package/src/replayCreate.ts +194 -0
  76. package/src/replayViewer.ts +173 -0
  77. package/src/sessionRouter.ts +116 -0
  78. package/src/store/JsonMemoryStore.test.ts +175 -0
  79. package/src/store/JsonMemoryStore.ts +128 -0
  80. package/src/store/JsonTaskStore.test.ts +212 -0
  81. package/src/store/JsonTaskStore.ts +59 -0
  82. package/src/store/JsonlStore.test.ts +1538 -0
  83. package/src/store/JsonlStore.ts +1321 -0
  84. package/src/store/MemoryEventStore.test.ts +119 -0
  85. package/src/store/MemoryEventStore.ts +151 -0
  86. package/src/store/WriteQueue.ts +165 -0
  87. package/src/store/index.ts +29 -0
  88. package/src/store/types.ts +517 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * MCP over HTTP transport mounted onto the bridge's existing HTTP server.
3
+ *
4
+ * Reuses the bridge's auth wrapper, so the same `--token` that protects
5
+ * the dashboard also protects MCP tool calls. Remote agents talk to
6
+ * `http://<host>:<port>/mcp` and authenticate via `Authorization: Bearer
7
+ * <token>` like any other client.
8
+ */
9
+ import type { IBridge } from './bridge.js';
10
+ import type { EventStore } from './store/types.js';
11
+ export interface McpHttpOptions {
12
+ /** URL path the transport listens on. Default `/mcp`. */
13
+ path?: string;
14
+ /**
15
+ * Whether to use stateful sessions (sessionId in headers) or stateless
16
+ * one-shot requests. Stateful is the spec default and matches what
17
+ * Claude Code expects.
18
+ */
19
+ stateful?: boolean;
20
+ /**
21
+ * EventStore for SSE resumability via `Last-Event-ID`. If a client
22
+ * reconnects after a transient disconnect, the transport replays the
23
+ * events it missed. Defaults to a `MemoryEventStore` with conservative
24
+ * caps (1000 events / 5 minutes / 50 MiB total). Pass `null` to
25
+ * disable resumability entirely.
26
+ */
27
+ eventStore?: EventStore | null;
28
+ }
29
+ export interface McpHttpHandle {
30
+ /** Close the MCP server and detach the transport. */
31
+ close(): Promise<void>;
32
+ path: string;
33
+ }
34
+ /**
35
+ * Mount the MCP HTTP transport on the bridge's HTTP server. Bridge must
36
+ * already have been started; calls `prependHttpHandler` so it runs before
37
+ * the dashboard/replay/events handler chain.
38
+ */
39
+ export declare function startMcpHttpServer(bridge: IBridge, opts?: McpHttpOptions): Promise<McpHttpHandle>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * MCP over HTTP transport mounted onto the bridge's existing HTTP server.
3
+ *
4
+ * Reuses the bridge's auth wrapper, so the same `--token` that protects
5
+ * the dashboard also protects MCP tool calls. Remote agents talk to
6
+ * `http://<host>:<port>/mcp` and authenticate via `Authorization: Bearer
7
+ * <token>` like any other client.
8
+ */
9
+ import { randomUUID } from 'node:crypto';
10
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
11
+ import { createMcpServer } from './mcp.js';
12
+ import { MemoryEventStore } from './store/MemoryEventStore.js';
13
+ /**
14
+ * Mount the MCP HTTP transport on the bridge's HTTP server. Bridge must
15
+ * already have been started; calls `prependHttpHandler` so it runs before
16
+ * the dashboard/replay/events handler chain.
17
+ */
18
+ export async function startMcpHttpServer(bridge, opts = {}) {
19
+ const path = opts.path ?? '/mcp';
20
+ const stateful = opts.stateful !== false;
21
+ const eventStore = opts.eventStore === null
22
+ ? undefined
23
+ : opts.eventStore ?? new MemoryEventStore();
24
+ const server = createMcpServer(bridge);
25
+ const transport = new StreamableHTTPServerTransport({
26
+ sessionIdGenerator: stateful ? () => randomUUID() : undefined,
27
+ eventStore,
28
+ });
29
+ await server.connect(transport);
30
+ const b = bridge;
31
+ if (typeof b.prependHttpHandler !== 'function') {
32
+ throw new Error('mcpHttp: bridge does not support prependHttpHandler (need a Bridge instance with HTTP server)');
33
+ }
34
+ b.prependHttpHandler(async (req, res) => {
35
+ const url = req.url ?? '';
36
+ const qi = url.indexOf('?');
37
+ const reqPath = qi < 0 ? url : url.slice(0, qi);
38
+ if (reqPath !== path)
39
+ return false;
40
+ await transport.handleRequest(req, res);
41
+ return true;
42
+ });
43
+ return {
44
+ path,
45
+ async close() {
46
+ await server.close();
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Cross-platform "open this URL in the user's default browser" — a tiny
3
+ * wrapper around the OS-native command.
4
+ *
5
+ * Detection rules:
6
+ * - darwin → `open <url>`
7
+ * - linux → `xdg-open <url>`
8
+ * - win32 → `cmd /c start "" <url>` (the empty title is required, otherwise `start` treats the URL as a title)
9
+ *
10
+ * Escape hatches:
11
+ * - `HARNESS_FE_HEADLESS=1` short-circuits and returns `false` without
12
+ * spawning anything — useful when the daemon runs in Docker / CI /
13
+ * remote host where there's no GUI to open
14
+ * - any other platform returns `false`
15
+ *
16
+ * The spawned process is detached and stdio'd to ignore so we don't
17
+ * accidentally tie its lifetime to ours.
18
+ */
19
+ import { spawn } from 'node:child_process';
20
+ export interface OpenBrowserOptions {
21
+ /** Inject an alternate `process.platform` value, for tests. */
22
+ platformOverride?: NodeJS.Platform;
23
+ /** Inject the env lookup, for tests. */
24
+ envOverride?: Record<string, string | undefined>;
25
+ /** Inject the spawn function, for tests. */
26
+ spawnOverride?: typeof spawn;
27
+ }
28
+ export interface OpenBrowserResult {
29
+ opened: boolean;
30
+ /** Set when `opened` is false to explain why. */
31
+ reason?: string;
32
+ }
33
+ export declare function openBrowser(url: string, opts?: OpenBrowserOptions): OpenBrowserResult;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Cross-platform "open this URL in the user's default browser" — a tiny
3
+ * wrapper around the OS-native command.
4
+ *
5
+ * Detection rules:
6
+ * - darwin → `open <url>`
7
+ * - linux → `xdg-open <url>`
8
+ * - win32 → `cmd /c start "" <url>` (the empty title is required, otherwise `start` treats the URL as a title)
9
+ *
10
+ * Escape hatches:
11
+ * - `HARNESS_FE_HEADLESS=1` short-circuits and returns `false` without
12
+ * spawning anything — useful when the daemon runs in Docker / CI /
13
+ * remote host where there's no GUI to open
14
+ * - any other platform returns `false`
15
+ *
16
+ * The spawned process is detached and stdio'd to ignore so we don't
17
+ * accidentally tie its lifetime to ours.
18
+ */
19
+ import { spawn } from 'node:child_process';
20
+ export function openBrowser(url, opts = {}) {
21
+ const env = opts.envOverride ?? process.env;
22
+ if (env.HARNESS_FE_HEADLESS === '1') {
23
+ return { opened: false, reason: 'HARNESS_FE_HEADLESS=1' };
24
+ }
25
+ const platform = opts.platformOverride ?? process.platform;
26
+ const spawnFn = opts.spawnOverride ?? spawn;
27
+ let cmd;
28
+ let args;
29
+ switch (platform) {
30
+ case 'darwin':
31
+ cmd = 'open';
32
+ args = [url];
33
+ break;
34
+ case 'linux':
35
+ cmd = 'xdg-open';
36
+ args = [url];
37
+ break;
38
+ case 'win32':
39
+ // `start` is a cmd builtin, not a standalone exe. The first
40
+ // empty-string arg is the window title — required, because
41
+ // otherwise `start "https://…"` treats the URL as the title
42
+ // and never opens anything.
43
+ cmd = 'cmd';
44
+ args = ['/c', 'start', '', url];
45
+ break;
46
+ default:
47
+ return { opened: false, reason: `unsupported platform: ${platform}` };
48
+ }
49
+ try {
50
+ const child = spawnFn(cmd, args, {
51
+ detached: true,
52
+ stdio: 'ignore',
53
+ });
54
+ child.unref();
55
+ return { opened: true };
56
+ }
57
+ catch (err) {
58
+ return {
59
+ opened: false,
60
+ reason: err instanceof Error ? err.message : String(err),
61
+ };
62
+ }
63
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * RemoteBridge — IBridge implementation backed by a WS connection to an
3
+ * already-running daemon (leader).
4
+ *
5
+ * Used when this process starts as a follower: another cli.js is already
6
+ * listening on :47729, so we attach as a ws client and proxy every MCP tool
7
+ * call through the new `mcp.call` / `mcp.return` control frames.
8
+ */
9
+ import { type McpCallFrame, type TabInfo, type Task, type TaskStatus } from '@harness-fe/protocol';
10
+ import type { IBridge, SendCommandOptions } from './bridge.js';
11
+ import type { IMemoryStore, IStore } from './store/index.js';
12
+ export interface RemoteBridgeOptions {
13
+ port: number;
14
+ host?: string;
15
+ /** Per-call timeout. Must be ≥ daemon's command timeout to surface upstream errors first. */
16
+ callTimeoutMs?: number;
17
+ /** Token used to authenticate against the leader, if it requires one. */
18
+ token?: string;
19
+ }
20
+ export declare class RemoteBridge implements IBridge {
21
+ private ws?;
22
+ private pending;
23
+ private closed;
24
+ private readonly url;
25
+ private readonly callTimeoutMs;
26
+ private readonly host;
27
+ private readonly port;
28
+ private readonly token;
29
+ constructor(opts: RemoteBridgeOptions);
30
+ connect(): Promise<void>;
31
+ stop(): Promise<void>;
32
+ sendCommand(command: string, args: unknown, opts?: SendCommandOptions): Promise<unknown>;
33
+ listTabs(): Promise<TabInfo[]>;
34
+ listTasks(filter?: {
35
+ status?: TaskStatus | 'all';
36
+ limit?: number;
37
+ }): Promise<Task[]>;
38
+ claimTask(id: string): Promise<Task | undefined>;
39
+ resolveTask(id: string, note?: string): Promise<Task | undefined>;
40
+ /**
41
+ * Returns a RemoteMemoryStore that proxies all memory operations to the
42
+ * leader via the mcp.call channel. This allows follower instances to use
43
+ * the same project.memory.* tools as the leader.
44
+ */
45
+ getMemoryStore(): IMemoryStore;
46
+ getViewerBaseUrl(): string | undefined;
47
+ getAuthToken(): string | undefined;
48
+ getTaskAttachmentData(_taskId: string, _attachmentId: string): Promise<string | null>;
49
+ /**
50
+ * Returns a RemoteStore that proxies all store read/query operations to
51
+ * the leader via the mcp.call channel. Write operations (openSession,
52
+ * append, etc.) are not proxied — followers are read-only.
53
+ */
54
+ getStore(): IStore;
55
+ /** @internal — used by RemoteMemoryStore and RemoteStore */
56
+ invokeRemote(method: McpCallFrame['method'], args: unknown): Promise<unknown>;
57
+ private invoke;
58
+ private attachHandlers;
59
+ private handleReturn;
60
+ private handleClose;
61
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * RemoteBridge — IBridge implementation backed by a WS connection to an
3
+ * already-running daemon (leader).
4
+ *
5
+ * Used when this process starts as a follower: another cli.js is already
6
+ * listening on :47729, so we attach as a ws client and proxy every MCP tool
7
+ * call through the new `mcp.call` / `mcp.return` control frames.
8
+ */
9
+ import { WebSocket } from 'ws';
10
+ import { randomUUID } from 'node:crypto';
11
+ import { frameSchema, } from '@harness-fe/protocol';
12
+ const DEFAULT_CALL_TIMEOUT_MS = 30_000;
13
+ export class RemoteBridge {
14
+ ws;
15
+ pending = new Map();
16
+ closed = false;
17
+ url;
18
+ callTimeoutMs;
19
+ host;
20
+ port;
21
+ token;
22
+ constructor(opts) {
23
+ const host = opts.host ?? '127.0.0.1';
24
+ this.host = host;
25
+ this.port = opts.port;
26
+ this.token = opts.token;
27
+ const tokenQs = opts.token ? `?token=${encodeURIComponent(opts.token)}` : '';
28
+ this.url = `ws://${host}:${opts.port}${tokenQs}`;
29
+ this.callTimeoutMs = opts.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
30
+ }
31
+ async connect() {
32
+ return new Promise((resolve, reject) => {
33
+ const headers = {};
34
+ if (this.token)
35
+ headers.authorization = `Bearer ${this.token}`;
36
+ const ws = new WebSocket(this.url, { headers });
37
+ this.ws = ws;
38
+ const onOpen = () => {
39
+ ws.off('error', onErr);
40
+ this.attachHandlers(ws);
41
+ resolve();
42
+ };
43
+ const onErr = (err) => {
44
+ ws.off('open', onOpen);
45
+ reject(err);
46
+ };
47
+ ws.once('open', onOpen);
48
+ ws.once('error', onErr);
49
+ });
50
+ }
51
+ async stop() {
52
+ this.closed = true;
53
+ const ws = this.ws;
54
+ if (!ws)
55
+ return;
56
+ try {
57
+ ws.close();
58
+ }
59
+ catch {
60
+ /* swallow */
61
+ }
62
+ }
63
+ sendCommand(command, args, opts) {
64
+ return this.invoke('sendCommand', { command, args, opts });
65
+ }
66
+ listTabs() {
67
+ return this.invoke('listTabs', {});
68
+ }
69
+ listTasks(filter = {}) {
70
+ return this.invoke('listTasks', filter);
71
+ }
72
+ claimTask(id) {
73
+ return this.invoke('claimTask', { id });
74
+ }
75
+ resolveTask(id, note) {
76
+ return this.invoke('resolveTask', { id, note });
77
+ }
78
+ /**
79
+ * Returns a RemoteMemoryStore that proxies all memory operations to the
80
+ * leader via the mcp.call channel. This allows follower instances to use
81
+ * the same project.memory.* tools as the leader.
82
+ */
83
+ getMemoryStore() {
84
+ return new RemoteMemoryStore(this);
85
+ }
86
+ getViewerBaseUrl() {
87
+ // Followers share the same WS/HTTP port as the leader.
88
+ return `http://${this.host}:${this.port}`;
89
+ }
90
+ getAuthToken() {
91
+ // Followers connect to the leader using their own configured token
92
+ // (passed in via RemoteBridge constructor). Surface it so dashboard
93
+ // links the follower hands out are pre-authenticated.
94
+ return this.token;
95
+ }
96
+ async getTaskAttachmentData(_taskId, _attachmentId) {
97
+ // Follower mode: attachment reads are not proxied in v0.6; direct leader access needed.
98
+ return null;
99
+ }
100
+ /**
101
+ * Returns a RemoteStore that proxies all store read/query operations to
102
+ * the leader via the mcp.call channel. Write operations (openSession,
103
+ * append, etc.) are not proxied — followers are read-only.
104
+ */
105
+ getStore() {
106
+ return new RemoteStore(this);
107
+ }
108
+ /** @internal — used by RemoteMemoryStore and RemoteStore */
109
+ invokeRemote(method, args) {
110
+ return this.invoke(method, args);
111
+ }
112
+ invoke(method, args) {
113
+ const ws = this.ws;
114
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
115
+ return Promise.reject(new Error('remote-bridge: not connected'));
116
+ }
117
+ const id = randomUUID();
118
+ const frame = { type: 'mcp.call', id, method, args };
119
+ return new Promise((resolve, reject) => {
120
+ const timer = setTimeout(() => {
121
+ this.pending.delete(id);
122
+ reject(new Error(`remote-bridge: "${method}" timed out after ${this.callTimeoutMs}ms`));
123
+ }, this.callTimeoutMs);
124
+ this.pending.set(id, { resolve, reject, timer });
125
+ try {
126
+ ws.send(JSON.stringify(frame));
127
+ }
128
+ catch (err) {
129
+ clearTimeout(timer);
130
+ this.pending.delete(id);
131
+ reject(err);
132
+ }
133
+ });
134
+ }
135
+ attachHandlers(ws) {
136
+ ws.on('message', (raw) => {
137
+ let parsed;
138
+ try {
139
+ parsed = JSON.parse(raw.toString());
140
+ }
141
+ catch {
142
+ return;
143
+ }
144
+ const frame = frameSchema.safeParse(parsed);
145
+ if (!frame.success)
146
+ return;
147
+ if (frame.data.type !== 'mcp.return')
148
+ return;
149
+ this.handleReturn(frame.data);
150
+ });
151
+ ws.on('close', () => this.handleClose());
152
+ ws.on('error', () => {
153
+ /* close will follow */
154
+ });
155
+ }
156
+ handleReturn(frame) {
157
+ const p = this.pending.get(frame.id);
158
+ if (!p)
159
+ return;
160
+ clearTimeout(p.timer);
161
+ this.pending.delete(frame.id);
162
+ if (frame.ok) {
163
+ p.resolve(frame.result);
164
+ }
165
+ else {
166
+ p.reject(new Error(frame.error?.message ?? 'remote-bridge: unknown error'));
167
+ }
168
+ }
169
+ handleClose() {
170
+ const err = new Error(this.closed
171
+ ? 'remote-bridge: connection closed'
172
+ : 'remote-bridge: lost connection to daemon');
173
+ for (const p of this.pending.values()) {
174
+ clearTimeout(p.timer);
175
+ p.reject(err);
176
+ }
177
+ this.pending.clear();
178
+ }
179
+ }
180
+ // ─── RemoteMemoryStore ────────────────────────────────────────────────────────
181
+ //
182
+ // Proxies IMemoryStore operations to the leader via mcp.call frames.
183
+ // Used by follower instances so project.memory.* tools work in all windows.
184
+ class RemoteMemoryStore {
185
+ bridge;
186
+ constructor(bridge) {
187
+ this.bridge = bridge;
188
+ }
189
+ get(projectId, key) {
190
+ // Synchronous interface — not directly awaitable. The MCP tool layer
191
+ // wraps calls in async handlers, so we return a thenable-compatible
192
+ // object. In practice mcp.ts awaits the result via the async handler.
193
+ // We throw here to signal that callers must use the async path.
194
+ throw new Error('RemoteMemoryStore.get() must be called via the async MCP tool handler. ' +
195
+ 'Use remoteMemoryStore.getAsync() instead.');
196
+ }
197
+ /** Async variant used by the MCP tool handlers in mcp.ts. */
198
+ async getAsync(projectId, key) {
199
+ return this.bridge.invokeRemote('memoryGet', { projectId, key });
200
+ }
201
+ set(projectId, key, value) {
202
+ throw new Error('RemoteMemoryStore.set() must be called via setAsync().');
203
+ }
204
+ async setAsync(projectId, key, value) {
205
+ return this.bridge.invokeRemote('memorySet', { projectId, key, value });
206
+ }
207
+ delete(projectId, key) {
208
+ throw new Error('RemoteMemoryStore.delete() must be called via deleteAsync().');
209
+ }
210
+ async deleteAsync(projectId, key) {
211
+ return this.bridge.invokeRemote('memoryDelete', { projectId, key });
212
+ }
213
+ list(projectId) {
214
+ throw new Error('RemoteMemoryStore.list() must be called via listAsync().');
215
+ }
216
+ async listAsync(projectId) {
217
+ return this.bridge.invokeRemote('memoryList', { projectId });
218
+ }
219
+ }
220
+ // ─── RemoteStore ──────────────────────────────────────────────────────────────
221
+ //
222
+ // Proxies IStore read operations to the leader via mcp.call frames.
223
+ // Write operations throw — followers are read-only for the store.
224
+ class RemoteStore {
225
+ bridge;
226
+ constructor(bridge) {
227
+ this.bridge = bridge;
228
+ }
229
+ // ── Read operations (proxied) ──────────────────────────────────────────
230
+ async listProjectsAsync() {
231
+ return this.bridge.invokeRemote('storeListProjects', {});
232
+ }
233
+ async listSessionsAsync(opts) {
234
+ return this.bridge.invokeRemote('storeListSessions', opts ?? {});
235
+ }
236
+ async summaryAsync(sessionId) {
237
+ return this.bridge.invokeRemote('storeSummary', { sessionId });
238
+ }
239
+ async tailAsync(sessionId, opts) {
240
+ return this.bridge.invokeRemote('storeTail', { sessionId, opts });
241
+ }
242
+ async searchAsync(sessionId, query, opts) {
243
+ return this.bridge.invokeRemote('storeSearch', { sessionId, query, opts });
244
+ }
245
+ async listRecordingsAsync(sessionId) {
246
+ return this.bridge.invokeRemote('storeRecordingsList', { sessionId });
247
+ }
248
+ async sliceRecordingsAsync(sessionId, since, until) {
249
+ return this.bridge.invokeRemote('storeRecordingsSlice', { sessionId, since, until });
250
+ }
251
+ async replayCreateAsync(args) {
252
+ return this.bridge.invokeRemote('storeReplayCreate', args);
253
+ }
254
+ async purgeAsync(policy) {
255
+ return this.bridge.invokeRemote('storePurge', policy ?? {});
256
+ }
257
+ // ── Synchronous IStore interface stubs (not used by follower) ─────────
258
+ // These satisfy the interface but throw — the MCP tool handlers in mcp.ts
259
+ // use the async variants above when running in follower mode.
260
+ // Build lifecycle
261
+ openBuild(_p, _patch) { throw notSupported('openBuild'); }
262
+ closeBuild(_b, _c) { throw notSupported('closeBuild'); }
263
+ // Tab lifecycle
264
+ upsertTab(_t, _patch) { throw notSupported('upsertTab'); }
265
+ getTab(_t) { throw notSupported('getTab'); }
266
+ closeTab(_t, _d) { throw notSupported('closeTab'); }
267
+ // Session lifecycle
268
+ upsertSession(_s, _m) { throw notSupported('upsertSession'); }
269
+ closeSession(_s, _e) { throw notSupported('closeSession'); }
270
+ getSession(_id) { throw notSupported('getSession'); }
271
+ listSessions(_opts) { throw notSupported('listSessions'); }
272
+ // Write
273
+ appendEvent(_s, _e) { throw notSupported('appendEvent'); }
274
+ appendEventBatch(_s, _e) { throw notSupported('appendEventBatch'); }
275
+ appendRecording(_s, _c) { throw notSupported('appendRecording'); }
276
+ writeNote(_p, _k, _v) { throw notSupported('writeNote'); }
277
+ // Project metadata
278
+ listProjects() { throw notSupported('listProjects'); }
279
+ upsertProject(_p, _patch) { throw notSupported('upsertProject'); }
280
+ getProject(_p) { throw notSupported('getProject'); }
281
+ getProjectTree(_r) { throw notSupported('getProjectTree'); }
282
+ // Build metadata
283
+ upsertBuild(_p, _b, _patch) { throw notSupported('upsertBuild'); }
284
+ getBuild(_p, _b) { throw notSupported('getBuild'); }
285
+ listBuilds(_p, _l) { throw notSupported('listBuilds'); }
286
+ // Visitor metadata (0.5+) — followers don't proxy yet; leader-only for now.
287
+ upsertVisitor(_v, _patch) { throw notSupported('upsertVisitor'); }
288
+ getVisitor(_v) { throw notSupported('getVisitor'); }
289
+ listVisitors(_opts) { throw notSupported('listVisitors'); }
290
+ // Read
291
+ tail(_s, _o) { throw notSupported('tail'); }
292
+ search(_s, _q, _o) { throw notSupported('search'); }
293
+ listRecordings(_s) { throw notSupported('listRecordings'); }
294
+ sliceRecordings(_s, _since, _until) { throw notSupported('sliceRecordings'); }
295
+ writeExport(_i) { throw notSupported('writeExport'); }
296
+ getExport(_id) { throw notSupported('getExport'); }
297
+ readExportEvents(_id) { throw notSupported('readExportEvents'); }
298
+ listExports(_p, _l) { throw notSupported('listExports'); }
299
+ summary(_s) { throw notSupported('summary'); }
300
+ listNotes(_p) { throw notSupported('listNotes'); }
301
+ // Maintenance
302
+ purge(_p) { throw notSupported('purge'); }
303
+ close() { }
304
+ }
305
+ function notSupported(method) {
306
+ return new Error(`remote-bridge: IStore.${method}() is not available in follower mode`);
307
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Shared replay-export logic used by both the leader's MCP tool handler and
3
+ * the leader's mcp.call dispatcher (for follower proxy calls).
4
+ *
5
+ * Takes a time window (or a center timestamp), pulls the overlapping rrweb
6
+ * chunks for a single tab, concatenates the events, persists them as an
7
+ * export, and returns the metadata + viewerUrl.
8
+ */
9
+ import type { IStore } from './store/index.js';
10
+ export interface ReplayCreateArgs {
11
+ sessionId: string;
12
+ tabId?: string;
13
+ ts?: number;
14
+ windowMs?: number;
15
+ since?: number;
16
+ until?: number;
17
+ label?: string;
18
+ }
19
+ export interface ReplayCreateResult {
20
+ exportId?: string;
21
+ viewerUrl?: string;
22
+ sessionId: string;
23
+ tabId?: string;
24
+ since: number;
25
+ until: number;
26
+ startTs?: number;
27
+ endTs?: number;
28
+ durationMs?: number;
29
+ eventCount?: number;
30
+ chunkCount?: number;
31
+ bytes?: number;
32
+ createdAt?: number;
33
+ label?: string;
34
+ error?: string;
35
+ }
36
+ export declare function createReplayExport(store: IStore, baseUrl: string | undefined, input: ReplayCreateArgs): ReplayCreateResult;