lark-remote 0.1.7 → 0.1.8

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 (67) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/bridge/approval-coordinator.d.ts +15 -1
  3. package/dist/bridge/approval-coordinator.js +35 -2
  4. package/dist/bridge/index.d.ts +19 -6
  5. package/dist/bridge/index.js +25 -8
  6. package/dist/card/approval-render.js +187 -45
  7. package/dist/card/card-shared.d.ts +6 -4
  8. package/dist/card/card-shared.js +6 -4
  9. package/dist/card/run-renderer.d.ts +7 -0
  10. package/dist/card/run-renderer.js +22 -11
  11. package/dist/config/index.d.ts +24 -1
  12. package/dist/config/index.js +34 -3
  13. package/dist/index.js +28 -14
  14. package/dist/router/card-action-payload.d.ts +30 -0
  15. package/dist/router/card-action-payload.js +39 -0
  16. package/dist/router/config/claude.js +8 -3
  17. package/dist/router/config/dsh.d.ts +51 -0
  18. package/dist/router/config/dsh.js +179 -0
  19. package/dist/router/config/index.d.ts +1 -1
  20. package/dist/router/config/index.js +4 -1
  21. package/dist/router/config/types.d.ts +6 -0
  22. package/dist/router/index.d.ts +20 -1
  23. package/dist/router/index.js +113 -7
  24. package/dist/router/utils.js +12 -2
  25. package/dist/runner/agent-choices-common.js +3 -0
  26. package/dist/runner/claude/session.js +24 -5
  27. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -0
  28. package/dist/runner/codex/app-server/protocol-types.js +1 -1
  29. package/dist/runner/codex/app-server/runner.d.ts +10 -0
  30. package/dist/runner/codex/app-server/runner.js +56 -1
  31. package/dist/runner/codex/app-server/translator.d.ts +7 -0
  32. package/dist/runner/codex/app-server/translator.js +45 -0
  33. package/dist/runner/common/acp/protocol-types.d.ts +33 -0
  34. package/dist/runner/common/acp/protocol-types.js +2 -0
  35. package/dist/runner/dsh/client.d.ts +95 -0
  36. package/dist/runner/dsh/client.js +418 -0
  37. package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
  38. package/dist/runner/dsh/fake-dsh-server.js +156 -0
  39. package/dist/runner/dsh/index.d.ts +3 -0
  40. package/dist/runner/dsh/index.js +4 -0
  41. package/dist/runner/dsh/runner.d.ts +57 -0
  42. package/dist/runner/dsh/runner.js +191 -0
  43. package/dist/runner/dsh/translator.d.ts +28 -0
  44. package/dist/runner/dsh/translator.js +98 -0
  45. package/dist/runner/dsh/types.d.ts +155 -0
  46. package/dist/runner/dsh/types.js +14 -0
  47. package/dist/runner/index.d.ts +1 -0
  48. package/dist/runner/index.js +1 -0
  49. package/dist/runner/kimi/acp/runner.js +69 -1
  50. package/dist/runner/kimi/acp/translator.d.ts +14 -0
  51. package/dist/runner/kimi/acp/translator.js +108 -3
  52. package/dist/runner/pi/rpc/runner.d.ts +1 -7
  53. package/dist/runner/pi/rpc/runner.js +2 -11
  54. package/dist/runner/probe.js +6 -1
  55. package/dist/runner/question-common.d.ts +26 -0
  56. package/dist/runner/question-common.js +42 -0
  57. package/dist/runner/types.d.ts +63 -10
  58. package/dist/session/claude/sessions.js +37 -15
  59. package/dist/session/dsh/index.d.ts +1 -0
  60. package/dist/session/dsh/index.js +2 -0
  61. package/dist/session/dsh/sessions.d.ts +39 -0
  62. package/dist/session/dsh/sessions.js +237 -0
  63. package/dist/session/index.d.ts +1 -0
  64. package/dist/session/index.js +1 -0
  65. package/dist/workspace/index.d.ts +4 -0
  66. package/dist/workspace/index.js +8 -1
  67. package/package.json +2 -1
@@ -0,0 +1,418 @@
1
+ /**
2
+ * DshClient — HTTP client for the DSH Web Host proxy.
3
+ *
4
+ * Two transports:
5
+ * 1. unary POST /api/<method> (client-request → server-response).
6
+ * 2. SSE GET /api/events.mux (all-session aggregated stream). Frames are
7
+ * ServerRequest envelopes; `method` equals the MuxFrame's `type`.
8
+ *
9
+ * The client is stateless apart from the base URL: each `run()` opens its own
10
+ * mux subscription and closes it on terminal. `sessionEvents()` wraps the raw
11
+ * mux with per-session filtering plus reconnect + history-replay dedup
12
+ * (DSH v1 has no `since` resume — reconnection = reopen stream + refetch
13
+ * history, dedup by event seq).
14
+ */
15
+ import { getLogger } from '../../logger/index.js';
16
+ function defaultWebSocketFactory(url) {
17
+ const Ctor = globalThis.WebSocket;
18
+ if (!Ctor)
19
+ throw new Error('DSH WebSocket is unavailable');
20
+ return new Ctor(url);
21
+ }
22
+ export class DshError extends Error {
23
+ code;
24
+ details;
25
+ constructor(code, message, details = {}) {
26
+ super(message);
27
+ this.name = 'DshError';
28
+ this.code = code;
29
+ this.details = details;
30
+ }
31
+ }
32
+ /** Backoff between mux reconnects to avoid a hot loop on a dropping server. */
33
+ const RECONNECT_BACKOFF_MS = 200;
34
+ export class DshClient {
35
+ baseUrl;
36
+ fetchImpl;
37
+ webSocketImpl;
38
+ constructor(baseUrl, fetchImpl = fetch, webSocketImpl = defaultWebSocketFactory) {
39
+ this.baseUrl = baseUrl.replace(/\/$/, '');
40
+ this.fetchImpl = fetchImpl;
41
+ this.webSocketImpl = webSocketImpl;
42
+ }
43
+ /** Perform a unary POST and return the ok value; throws DshError on failure. */
44
+ async unary(method, payload) {
45
+ const rpcId = `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
46
+ let res;
47
+ try {
48
+ res = await this.fetchImpl(`${this.baseUrl}/api/${method}`, {
49
+ method: 'POST',
50
+ headers: { 'content-type': 'application/json' },
51
+ body: JSON.stringify({ type: 'client-request', rpcId, method, payload }),
52
+ });
53
+ }
54
+ catch (err) {
55
+ throw new DshError('transport', `DSH unary ${method} transport error: ${err.message}`);
56
+ }
57
+ if (!res.ok) {
58
+ throw new DshError('http', `DSH HTTP ${res.status} for ${method}`);
59
+ }
60
+ let body;
61
+ try {
62
+ body = (await res.json());
63
+ }
64
+ catch (err) {
65
+ throw new DshError('protocol', `DSH ${method} returned non-JSON: ${err.message}`);
66
+ }
67
+ if (body.result?.ok !== true) {
68
+ const err = body.result?.error;
69
+ throw new DshError(err?.code ?? 'error', err?.message ?? `DSH ${method} failed`, err?.details ?? {});
70
+ }
71
+ return body.result.value;
72
+ }
73
+ /** Create a session. Pass `agentPreset` to pin the session's preset (fixed at
74
+ * creation). */
75
+ async createSession(payload) {
76
+ return (await this.unary('session.create', payload));
77
+ }
78
+ /** Align the session's model/reasoningEffort (also persists as global default
79
+ * server-side). Failures are surfaced as DshError for the caller to downgrade. */
80
+ async selectModel(payload) {
81
+ const value = (await this.unary('session.selectModel', payload));
82
+ return value.selected;
83
+ }
84
+ /** Read the session's current model selection + full model catalog. */
85
+ async sessionModels(sessionId) {
86
+ return (await this.unary('session.models', { sessionId }));
87
+ }
88
+ /** Read the model catalog (session-independent, llm.models). */
89
+ async listModels() {
90
+ return (await this.unary('llm.models', {}));
91
+ }
92
+ /** Read the preset catalog (agentPreset.list). */
93
+ async listPresets() {
94
+ return (await this.unary('agentPreset.list', {}));
95
+ }
96
+ /**
97
+ * Return the highest event `seq` currently persisted in session.history.
98
+ *
99
+ * The caller snapshots this before `session.prompt`. `session.history` is a
100
+ * full-session replay rather than a delta, so this high-water mark is what
101
+ * lets a subsequent replay skip every already-completed turn.
102
+ */
103
+ async latestEventSeq(sessionId) {
104
+ const value = (await this.unary('session.history', {
105
+ sessionId,
106
+ maxMessages: 1,
107
+ }));
108
+ let max = -1;
109
+ for (const entry of value.events ?? []) {
110
+ const seq = entry.event?.seq;
111
+ if (typeof seq === 'number' && seq > max)
112
+ max = seq;
113
+ }
114
+ return max;
115
+ }
116
+ /**
117
+ * Raw SSE mux stream: yields ServerRequest envelopes until the stream ends,
118
+ * errors, or `signal` aborts. Does NOT reconnect (the caller owns retry).
119
+ */
120
+ async *mux(signal) {
121
+ let res;
122
+ try {
123
+ res = await this.fetchImpl(`${this.baseUrl}/api/events.mux`, {
124
+ method: 'GET',
125
+ headers: { accept: 'text/event-stream' },
126
+ signal,
127
+ });
128
+ }
129
+ catch (err) {
130
+ if (signal.aborted)
131
+ return;
132
+ throw new DshError('transport', `DSH mux open error: ${err.message}`);
133
+ }
134
+ if (!res.ok || !res.body) {
135
+ throw new DshError('http', `DSH mux HTTP ${res.status}`, { status: res.status });
136
+ }
137
+ const reader = res.body.getReader();
138
+ const decoder = new TextDecoder();
139
+ let buffer = '';
140
+ try {
141
+ for (;;) {
142
+ if (signal.aborted)
143
+ break;
144
+ let result;
145
+ try {
146
+ result = await reader.read();
147
+ }
148
+ catch (err) {
149
+ if (signal.aborted)
150
+ break;
151
+ throw new DshError('transport', `DSH mux read error: ${err.message}`);
152
+ }
153
+ if (result.done)
154
+ break;
155
+ buffer += decoder.decode(result.value, { stream: true });
156
+ let idx;
157
+ while ((idx = buffer.indexOf('\n\n')) !== -1) {
158
+ const chunk = buffer.slice(0, idx);
159
+ buffer = buffer.slice(idx + 2);
160
+ const dataLine = chunk.split('\n').find((l) => l.startsWith('data:'));
161
+ if (!dataLine)
162
+ continue;
163
+ const json = dataLine.slice(5).trim();
164
+ if (!json)
165
+ continue;
166
+ try {
167
+ const frame = JSON.parse(json);
168
+ if (frame?.type === 'server-request')
169
+ yield frame;
170
+ }
171
+ catch {
172
+ getLogger().warn('[dsh] dropped malformed mux frame');
173
+ }
174
+ }
175
+ }
176
+ }
177
+ finally {
178
+ reader.releaseLock();
179
+ try {
180
+ await res.body.cancel();
181
+ }
182
+ catch {
183
+ /* ignore cancel error */
184
+ }
185
+ }
186
+ }
187
+ /**
188
+ * WebSocket mux stream. The real DSH `/api/events.mux` endpoint is a
189
+ * WebSocket upgrade, not HTTP SSE: each text message is already a
190
+ * `DshServerRequest` JSON envelope. This yields those envelopes and ends on
191
+ * close/abort, letting `sessionEvents()` reconnect through the same
192
+ * history-replay loop.
193
+ */
194
+ async *muxWebSocket(signal) {
195
+ let socket;
196
+ try {
197
+ socket = this.webSocketImpl(`${this.baseUrl.replace(/^http/, 'ws')}/api/events.mux`);
198
+ }
199
+ catch (err) {
200
+ throw new DshError('transport', `DSH mux WebSocket open error: ${err.message}`);
201
+ }
202
+ const queue = [];
203
+ let waiter = null;
204
+ let ended = false;
205
+ let failure;
206
+ const settle = (frame) => {
207
+ if (waiter) {
208
+ const resolve = waiter;
209
+ waiter = null;
210
+ resolve(frame);
211
+ return;
212
+ }
213
+ if (frame)
214
+ queue.push(frame);
215
+ else
216
+ ended = true;
217
+ };
218
+ socket.onmessage = (event) => {
219
+ if (ended)
220
+ return;
221
+ if (typeof event.data !== 'string')
222
+ return;
223
+ try {
224
+ const frame = JSON.parse(event.data);
225
+ if (frame?.type === 'server-request')
226
+ settle(frame);
227
+ }
228
+ catch {
229
+ getLogger().warn('[dsh] dropped malformed WebSocket mux frame');
230
+ }
231
+ };
232
+ socket.onerror = () => {
233
+ if (ended)
234
+ return;
235
+ failure = new DshError('transport', 'DSH mux WebSocket error');
236
+ ended = true;
237
+ settle(undefined);
238
+ };
239
+ socket.onclose = () => {
240
+ if (ended)
241
+ return;
242
+ ended = true;
243
+ settle(undefined);
244
+ };
245
+ const onAbort = () => {
246
+ if (ended)
247
+ return;
248
+ ended = true;
249
+ settle(undefined);
250
+ try {
251
+ socket.close();
252
+ }
253
+ catch {
254
+ /* ignore close error */
255
+ }
256
+ };
257
+ signal.addEventListener('abort', onAbort, { once: true });
258
+ const next = () => new Promise((resolve) => {
259
+ if (queue.length > 0) {
260
+ resolve(queue.shift());
261
+ return;
262
+ }
263
+ if (ended) {
264
+ resolve(undefined);
265
+ return;
266
+ }
267
+ waiter = resolve;
268
+ });
269
+ try {
270
+ while (!signal.aborted) {
271
+ const frame = await next();
272
+ if (frame === undefined) {
273
+ if (failure && !signal.aborted)
274
+ throw failure;
275
+ return;
276
+ }
277
+ yield frame;
278
+ }
279
+ }
280
+ finally {
281
+ signal.removeEventListener('abort', onAbort);
282
+ try {
283
+ socket.close();
284
+ }
285
+ catch {
286
+ /* ignore close error */
287
+ }
288
+ }
289
+ }
290
+ /**
291
+ * Replay session.history events that are newer than `state.lastSeq`, updating
292
+ * `state.lastSeq` as it goes. Yields each non-duplicate SessionEvent. Used for
293
+ * both the initial baseline (first subscription) and reconnect gap-fill.
294
+ */
295
+ async *replayHistory(sessionId, signal, state, logTag) {
296
+ try {
297
+ const value = (await this.unary('session.history', {
298
+ sessionId,
299
+ maxMessages: 100,
300
+ }));
301
+ for (const entry of value.events ?? []) {
302
+ if (signal.aborted)
303
+ return;
304
+ const ev = entry.event;
305
+ if (!ev)
306
+ continue;
307
+ const seq = ev.seq;
308
+ if (typeof seq === 'number' && seq <= state.lastSeq)
309
+ continue;
310
+ if (typeof seq === 'number')
311
+ state.lastSeq = seq;
312
+ yield ev;
313
+ }
314
+ }
315
+ catch (err) {
316
+ if (signal.aborted)
317
+ return;
318
+ getLogger().warn(`[dsh] ${logTag} failed for session ${sessionId}: ${err.message}`);
319
+ }
320
+ }
321
+ /**
322
+ * Per-session subscription: yields session events (and approval notices) for
323
+ * `sessionId`, auto-reconnecting on stream drop/error and replaying
324
+ * `session.history` to fill the gap (dedup by seq). Terminates on turn/end
325
+ * or `signal` abort.
326
+ */
327
+ async *sessionEvents(sessionId, cwd, signal, initialSeq = -1) {
328
+ const state = { lastSeq: initialSeq };
329
+ // Initial baseline: before opening the first live mux subscription, replay
330
+ // history once so events produced between prompt-accept and the mux opening
331
+ // (e.g. a fast turn that already ended) are not lost. Dedup by seq keeps the
332
+ // replay and the live stream convergent.
333
+ for await (const ev of this.replayHistory(sessionId, signal, state, 'initial history baseline')) {
334
+ if (signal.aborted)
335
+ return;
336
+ yield { kind: 'event', sessionId, event: ev };
337
+ if (ev.type === 'turn/end')
338
+ return;
339
+ }
340
+ let muxMode = 'fetch';
341
+ while (!signal.aborted) {
342
+ let dropped = false;
343
+ try {
344
+ const frames = muxMode === 'websocket' ? this.muxWebSocket(signal) : this.mux(signal);
345
+ for await (const frame of frames) {
346
+ if (signal.aborted)
347
+ return;
348
+ if (frame.method === 'stream/error') {
349
+ dropped = true;
350
+ break;
351
+ }
352
+ if (frame.method === 'approval/requested') {
353
+ const p = frame.payload;
354
+ if (p?.sessionId === sessionId && p.approvalId) {
355
+ yield {
356
+ kind: 'approval',
357
+ sessionId,
358
+ approvalId: p.approvalId,
359
+ toolName: p.toolName ?? '',
360
+ reason: p.reason,
361
+ };
362
+ }
363
+ continue;
364
+ }
365
+ if (frame.method !== 'session/event')
366
+ continue;
367
+ const payload = frame.payload;
368
+ if (!payload || payload.sessionId !== sessionId || !payload.event)
369
+ continue;
370
+ const seq = payload.event.seq;
371
+ if (typeof seq === 'number' && seq <= state.lastSeq)
372
+ continue; // dedup
373
+ if (typeof seq === 'number')
374
+ state.lastSeq = seq;
375
+ yield { kind: 'event', sessionId, event: payload.event };
376
+ if (payload.event.type === 'turn/end')
377
+ return;
378
+ }
379
+ dropped = true; // stream closed normally → treat as drop, replay history
380
+ }
381
+ catch (err) {
382
+ if (signal.aborted)
383
+ return;
384
+ if (muxMode === 'fetch' &&
385
+ err instanceof DshError &&
386
+ err.code === 'http' &&
387
+ err.details.status === 426) {
388
+ getLogger().info(`[dsh] mux endpoint requires WebSocket upgrade; switching transports for session ${sessionId}`);
389
+ muxMode = 'websocket';
390
+ dropped = true;
391
+ continue;
392
+ }
393
+ getLogger().warn(`[dsh] mux disconnected for session ${sessionId}: ${err.message}; replaying history`);
394
+ dropped = true;
395
+ }
396
+ if (signal.aborted)
397
+ return;
398
+ if (!dropped)
399
+ break;
400
+ // Reconnect baseline: replay history events after lastSeq (dedup keeps
401
+ // the live stream and the replay convergent).
402
+ for await (const ev of this.replayHistory(sessionId, signal, state, 'history replay')) {
403
+ if (signal.aborted)
404
+ return;
405
+ yield { kind: 'event', sessionId, event: ev };
406
+ if (ev.type === 'turn/end')
407
+ return;
408
+ }
409
+ // Bound reconnect frequency.
410
+ await new Promise((resolve) => setTimeout(resolve, RECONNECT_BACKOFF_MS));
411
+ }
412
+ }
413
+ /** Extract TokenUsage from an assistant/message data payload (absent → undefined). */
414
+ static usageOf(data) {
415
+ return data.usage ?? undefined;
416
+ }
417
+ }
418
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Fake DSH Web Host HTTP server for agent-dsh tests.
3
+ *
4
+ * Mimics the real DSH wire protocol (deepseek-harness apiproxy handler.ts):
5
+ * - POST /api/<method>: body {type:'client-request',rpcId,method,payload},
6
+ * response {type:'server-response',rpcId,result:{ok:true,value}|{ok:false,error}}.
7
+ * - SSE GET /api/events.mux: frames `data: {type:'server-request',rpcId,
8
+ * method:<frame.type>, payload:<frame>}\n\n`.
9
+ *
10
+ * The mux frame queue is consumed ACROSS connections (shared pointer): the
11
+ * first connection drains a prefix then closes; a reconnect drains the rest —
12
+ * which is exactly how a server that drops mid-turn is modelled. Unary
13
+ * handlers and the history value are registered by the test.
14
+ */
15
+ import { type Server } from 'node:http';
16
+ export interface FakeRpcResult {
17
+ ok: true;
18
+ value: unknown;
19
+ }
20
+ export interface FakeRpcError {
21
+ ok: false;
22
+ error: {
23
+ code: string;
24
+ message: string;
25
+ details?: Record<string, unknown>;
26
+ };
27
+ }
28
+ export type FakeUnaryHandler = (payload: unknown, rpcId: string) => FakeRpcResult | FakeRpcError;
29
+ export interface DshServerRequestRecord {
30
+ method: string;
31
+ rpcId: string;
32
+ payload: unknown;
33
+ }
34
+ export declare class FakeDshServer {
35
+ readonly server: Server;
36
+ private readonly unaryHandlers;
37
+ private muxFrames;
38
+ private historyValue;
39
+ private port;
40
+ private closedConnections;
41
+ readonly requests: DshServerRequestRecord[];
42
+ constructor();
43
+ get baseUrl(): string;
44
+ get connectionCount(): number;
45
+ start(): Promise<void>;
46
+ stop(): Promise<void>;
47
+ register(method: string, handler: FakeUnaryHandler): void;
48
+ setMuxFrames(frames: Array<object | {
49
+ close: true;
50
+ }>): void;
51
+ setHistoryValue(value: unknown): void;
52
+ /** Synchronous unary dispatch (no HTTP) — used by tests that inject the
53
+ * reader's sync transport to avoid the spawnSync-in-process deadlock. */
54
+ dispatchSync(method: string, payload: unknown, rpcId?: string): {
55
+ type: string;
56
+ rpcId: string;
57
+ result: FakeRpcResult | FakeRpcError;
58
+ };
59
+ private handleMux;
60
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Fake DSH Web Host HTTP server for agent-dsh tests.
3
+ *
4
+ * Mimics the real DSH wire protocol (deepseek-harness apiproxy handler.ts):
5
+ * - POST /api/<method>: body {type:'client-request',rpcId,method,payload},
6
+ * response {type:'server-response',rpcId,result:{ok:true,value}|{ok:false,error}}.
7
+ * - SSE GET /api/events.mux: frames `data: {type:'server-request',rpcId,
8
+ * method:<frame.type>, payload:<frame>}\n\n`.
9
+ *
10
+ * The mux frame queue is consumed ACROSS connections (shared pointer): the
11
+ * first connection drains a prefix then closes; a reconnect drains the rest —
12
+ * which is exactly how a server that drops mid-turn is modelled. Unary
13
+ * handlers and the history value are registered by the test.
14
+ */
15
+ import { createServer } from 'node:http';
16
+ import { randomUUID } from 'node:crypto';
17
+ export class FakeDshServer {
18
+ server;
19
+ unaryHandlers = new Map();
20
+ muxFrames = [];
21
+ historyValue = { events: [] };
22
+ port = 0;
23
+ closedConnections = 0;
24
+ requests = [];
25
+ constructor() {
26
+ this.server = createServer((req, res) => {
27
+ const url = req.url ?? '';
28
+ const path = url.split('?')[0];
29
+ if (req.method === 'GET' && path === '/api/events.mux') {
30
+ this.closedConnections++;
31
+ this.handleMux(res);
32
+ return;
33
+ }
34
+ if (req.method !== 'POST' || !path.startsWith('/api/')) {
35
+ res.writeHead(404);
36
+ res.end('not found');
37
+ return;
38
+ }
39
+ let raw = '';
40
+ req.on('data', (c) => (raw += c));
41
+ req.on('end', () => {
42
+ let body = {};
43
+ try {
44
+ body = JSON.parse(raw);
45
+ }
46
+ catch {
47
+ res.writeHead(400);
48
+ res.end('bad request');
49
+ return;
50
+ }
51
+ const method = path.slice('/api/'.length);
52
+ if (body.type !== 'client-request' || !body.rpcId || body.method !== method) {
53
+ res.writeHead(400);
54
+ res.end('bad request');
55
+ return;
56
+ }
57
+ const rpcId = body.rpcId;
58
+ this.requests.push({ method, rpcId, payload: body.payload });
59
+ const handler = this.unaryHandlers.get(method);
60
+ let result;
61
+ if (handler) {
62
+ result = handler(body.payload, rpcId);
63
+ }
64
+ else if (method === 'session.history') {
65
+ result = { ok: true, value: this.historyValue };
66
+ }
67
+ else if (method === 'session.list') {
68
+ result = { ok: true, value: { items: [] } };
69
+ }
70
+ else {
71
+ result = { ok: true, value: {} };
72
+ }
73
+ res.writeHead(200, { 'content-type': 'application/json' });
74
+ res.end(JSON.stringify({ type: 'server-response', rpcId, result }));
75
+ });
76
+ });
77
+ }
78
+ get baseUrl() {
79
+ return `http://127.0.0.1:${this.port}`;
80
+ }
81
+ get connectionCount() {
82
+ return this.closedConnections;
83
+ }
84
+ async start() {
85
+ await new Promise((resolve) => this.server.listen(0, '127.0.0.1', resolve));
86
+ const addr = this.server.address();
87
+ this.port = addr.port;
88
+ }
89
+ async stop() {
90
+ await new Promise((resolve) => this.server.close(() => resolve()));
91
+ }
92
+ register(method, handler) {
93
+ this.unaryHandlers.set(method, handler);
94
+ }
95
+ setMuxFrames(frames) {
96
+ this.muxFrames = frames;
97
+ }
98
+ setHistoryValue(value) {
99
+ this.historyValue = value;
100
+ }
101
+ /** Synchronous unary dispatch (no HTTP) — used by tests that inject the
102
+ * reader's sync transport to avoid the spawnSync-in-process deadlock. */
103
+ dispatchSync(method, payload, rpcId = `rpc-${randomUUID()}`) {
104
+ this.requests.push({ method, rpcId, payload });
105
+ const handler = this.unaryHandlers.get(method);
106
+ let result;
107
+ if (handler) {
108
+ result = handler(payload, rpcId);
109
+ }
110
+ else if (method === 'session.history') {
111
+ result = { ok: true, value: this.historyValue };
112
+ }
113
+ else if (method === 'session.list') {
114
+ result = { ok: true, value: { items: [] } };
115
+ }
116
+ else {
117
+ result = { ok: true, value: {} };
118
+ }
119
+ return { type: 'server-response', rpcId, result };
120
+ }
121
+ handleMux(res) {
122
+ res.writeHead(200, {
123
+ 'content-type': 'text/event-stream',
124
+ 'cache-control': 'no-cache',
125
+ connection: 'keep-alive',
126
+ });
127
+ // Drain the SHARED script (shift): the first connection consumes a prefix
128
+ // then closes (a {close:true} marker or an exhausted queue); a reconnect
129
+ // continues from where it left off — the natural model of a mid-turn drop.
130
+ const timer = setInterval(() => {
131
+ const item = this.muxFrames.shift();
132
+ if (item === undefined) {
133
+ clearInterval(timer);
134
+ res.end();
135
+ return;
136
+ }
137
+ if ('close' in item && item.close === true) {
138
+ clearInterval(timer);
139
+ res.end();
140
+ return;
141
+ }
142
+ const frame = item;
143
+ res.write(`data: ${JSON.stringify({
144
+ type: 'server-request',
145
+ rpcId: `mux-${randomUUID()}`,
146
+ method: frame.type,
147
+ payload: frame,
148
+ })}\n\n`);
149
+ }, 5);
150
+ reqClose(res, timer);
151
+ }
152
+ }
153
+ function reqClose(res, timer) {
154
+ res.on('close', () => clearInterval(timer));
155
+ }
156
+ //# sourceMappingURL=fake-dsh-server.js.map
@@ -0,0 +1,3 @@
1
+ export { DshRunner, type DshRunnerOptions } from './runner.js';
2
+ export { DshClient, DshError } from './client.js';
3
+ export { DshTranslator, mapUsage } from './translator.js';
@@ -0,0 +1,4 @@
1
+ export { DshRunner } from './runner.js';
2
+ export { DshClient, DshError } from './client.js';
3
+ export { DshTranslator, mapUsage } from './translator.js';
4
+ //# sourceMappingURL=index.js.map