open-claude-agent-sdk 0.9.0

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 (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/dist/api/MessageQueue.d.ts +39 -0
  4. package/dist/api/MessageQueue.d.ts.map +1 -0
  5. package/dist/api/MessageQueue.js +90 -0
  6. package/dist/api/MessageQueue.js.map +1 -0
  7. package/dist/api/MessageRouter.d.ts +41 -0
  8. package/dist/api/MessageRouter.d.ts.map +1 -0
  9. package/dist/api/MessageRouter.js +95 -0
  10. package/dist/api/MessageRouter.js.map +1 -0
  11. package/dist/api/ProcessFactory.d.ts +23 -0
  12. package/dist/api/ProcessFactory.d.ts.map +1 -0
  13. package/dist/api/ProcessFactory.js +103 -0
  14. package/dist/api/ProcessFactory.js.map +1 -0
  15. package/dist/api/QueryImpl.d.ts +103 -0
  16. package/dist/api/QueryImpl.d.ts.map +1 -0
  17. package/dist/api/QueryImpl.js +417 -0
  18. package/dist/api/QueryImpl.js.map +1 -0
  19. package/dist/api/query.d.ts +32 -0
  20. package/dist/api/query.d.ts.map +1 -0
  21. package/dist/api/query.js +31 -0
  22. package/dist/api/query.js.map +1 -0
  23. package/dist/core/argBuilder.d.ts +16 -0
  24. package/dist/core/argBuilder.d.ts.map +1 -0
  25. package/dist/core/argBuilder.js +204 -0
  26. package/dist/core/argBuilder.js.map +1 -0
  27. package/dist/core/control.d.ts +66 -0
  28. package/dist/core/control.d.ts.map +1 -0
  29. package/dist/core/control.js +222 -0
  30. package/dist/core/control.js.map +1 -0
  31. package/dist/core/hookConfig.d.ts +31 -0
  32. package/dist/core/hookConfig.d.ts.map +1 -0
  33. package/dist/core/hookConfig.js +45 -0
  34. package/dist/core/hookConfig.js.map +1 -0
  35. package/dist/core/mcpBridge.d.ts +29 -0
  36. package/dist/core/mcpBridge.d.ts.map +1 -0
  37. package/dist/core/mcpBridge.js +71 -0
  38. package/dist/core/mcpBridge.js.map +1 -0
  39. package/dist/core/spawn.d.ts +33 -0
  40. package/dist/core/spawn.d.ts.map +1 -0
  41. package/dist/core/spawn.js +102 -0
  42. package/dist/core/spawn.js.map +1 -0
  43. package/dist/index.d.ts +9 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +9 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/mcp/index.d.ts +6 -0
  48. package/dist/mcp/index.d.ts.map +1 -0
  49. package/dist/mcp/index.js +6 -0
  50. package/dist/mcp/index.js.map +1 -0
  51. package/dist/mcp-entry.d.ts +22 -0
  52. package/dist/mcp-entry.d.ts.map +1 -0
  53. package/dist/mcp-entry.js +22 -0
  54. package/dist/mcp-entry.js.map +1 -0
  55. package/dist/mcp.d.ts +101 -0
  56. package/dist/mcp.d.ts.map +1 -0
  57. package/dist/mcp.js +78 -0
  58. package/dist/mcp.js.map +1 -0
  59. package/dist/query.d.ts +19 -0
  60. package/dist/query.d.ts.map +1 -0
  61. package/dist/query.js +18 -0
  62. package/dist/query.js.map +1 -0
  63. package/dist/tools/index.d.ts +6 -0
  64. package/dist/tools/index.d.ts.map +1 -0
  65. package/dist/tools/index.js +6 -0
  66. package/dist/tools/index.js.map +1 -0
  67. package/dist/types/control.d.ts +149 -0
  68. package/dist/types/control.d.ts.map +1 -0
  69. package/dist/types/control.js +40 -0
  70. package/dist/types/control.js.map +1 -0
  71. package/dist/types/index.d.ts +53 -0
  72. package/dist/types/index.d.ts.map +1 -0
  73. package/dist/types/index.js +11 -0
  74. package/dist/types/index.js.map +1 -0
  75. package/package.json +85 -0
  76. package/src/api/MessageQueue.ts +99 -0
  77. package/src/api/MessageRouter.ts +112 -0
  78. package/src/api/ProcessFactory.ts +124 -0
  79. package/src/api/QueryImpl.ts +543 -0
  80. package/src/api/query.ts +36 -0
  81. package/src/core/argBuilder.ts +236 -0
  82. package/src/core/control.ts +295 -0
  83. package/src/core/hookConfig.ts +70 -0
  84. package/src/core/mcpBridge.ts +81 -0
  85. package/src/core/spawn.ts +125 -0
  86. package/src/index.ts +12 -0
  87. package/src/mcp/index.ts +6 -0
  88. package/src/mcp-entry.ts +22 -0
  89. package/src/mcp.ts +148 -0
  90. package/src/query.ts +21 -0
  91. package/src/tools/README.md +171 -0
  92. package/src/tools/capture-cli.cjs +202 -0
  93. package/src/tools/index.ts +6 -0
  94. package/src/tools/proxy-cli.cjs +162 -0
  95. package/src/types/control.ts +196 -0
  96. package/src/types/index.ts +204 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Process factory for dependency injection
3
+ *
4
+ * Allows unit tests to inject mock processes without spawning real CLI.
5
+ *
6
+ * @internal
7
+ */
8
+
9
+ import type { ChildProcess } from 'node:child_process';
10
+ import { buildCliArgs } from '../core/argBuilder.ts';
11
+ import { detectClaudeBinary, spawnClaude } from '../core/spawn.ts';
12
+ import type { Options } from '../types/index.ts';
13
+
14
+ /**
15
+ * Interface for creating CLI processes
16
+ * Allows dependency injection for testing
17
+ */
18
+ export interface ProcessFactory {
19
+ spawn(options: Options): ChildProcess;
20
+ }
21
+
22
+ /**
23
+ * Check if a path is a native binary (not a JS file)
24
+ * Matches official SDK: if NOT .js/.mjs/.tsx/.ts/.jsx, it's a native binary
25
+ */
26
+ function isNativeBinary(path: string): boolean {
27
+ return !['.js', '.mjs', '.tsx', '.ts', '.jsx'].some((ext) => path.endsWith(ext));
28
+ }
29
+
30
+ /**
31
+ * Get default JavaScript runtime
32
+ */
33
+ function getDefaultExecutable(): string {
34
+ return typeof process.versions.bun !== 'undefined' ? 'bun' : 'node';
35
+ }
36
+
37
+ /**
38
+ * Default implementation that spawns real Claude CLI
39
+ */
40
+ export class DefaultProcessFactory implements ProcessFactory {
41
+ spawn(options: Options): ChildProcess {
42
+ const args = buildCliArgs({ ...options, prompt: '' });
43
+
44
+ // Build environment with enableFileCheckpointing support
45
+ const env: Record<string, string | undefined> = { ...(options.env ?? {}) };
46
+ if (options.enableFileCheckpointing) {
47
+ env.CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING = 'true';
48
+ }
49
+
50
+ // Custom spawn function takes priority
51
+ if (options.spawnClaudeCodeProcess) {
52
+ const scriptPath = detectClaudeBinary(options);
53
+ const native = isNativeBinary(scriptPath);
54
+ const executable = options.executable ?? getDefaultExecutable();
55
+ const executableArgs = options.executableArgs ?? [];
56
+ const command = native ? scriptPath : executable;
57
+ const spawnArgs = native
58
+ ? [...executableArgs, ...args]
59
+ : [...executableArgs, scriptPath, ...args];
60
+
61
+ // Build full env for SpawnOptions
62
+ const fullEnv: Record<string, string | undefined> = { ...process.env, ...env };
63
+ if (!fullEnv.CLAUDE_CODE_ENTRYPOINT) fullEnv.CLAUDE_CODE_ENTRYPOINT = 'sdk-ts';
64
+ delete fullEnv.NODE_OPTIONS;
65
+ if (fullEnv.DEBUG_CLAUDE_AGENT_SDK) fullEnv.DEBUG = '1';
66
+ else delete fullEnv.DEBUG;
67
+
68
+ const spawnedProcess = options.spawnClaudeCodeProcess({
69
+ command,
70
+ args: spawnArgs,
71
+ cwd: options.cwd,
72
+ env: fullEnv,
73
+ signal: AbortSignal.timeout(3600000), // 1 hour default
74
+ });
75
+
76
+ // Wrap SpawnedProcess to ChildProcess-compatible object
77
+ return spawnedProcess as unknown as ChildProcess;
78
+ }
79
+
80
+ const scriptPath = detectClaudeBinary(options);
81
+
82
+ // When executable is explicitly set, use it as the command with script as arg
83
+ if (options.executable) {
84
+ const executableArgs = options.executableArgs ?? [];
85
+ const fullArgs = [...executableArgs, scriptPath, ...args];
86
+ return spawnClaude(options.executable, fullArgs, {
87
+ cwd: options.cwd,
88
+ env,
89
+ stderr: options.stderr,
90
+ });
91
+ }
92
+
93
+ // Default: use detected binary directly (shebang handles runtime)
94
+ const executableArgs = options.executableArgs ?? [];
95
+
96
+ if (isNativeBinary(scriptPath)) {
97
+ // Native binary: command is the binary, executableArgs before CLI args
98
+ const fullArgs = [...executableArgs, ...args];
99
+ return spawnClaude(scriptPath, fullArgs, {
100
+ cwd: options.cwd,
101
+ env,
102
+ stderr: options.stderr,
103
+ });
104
+ }
105
+
106
+ // JS file with executableArgs: need explicit runtime
107
+ if (executableArgs.length > 0) {
108
+ const executable = getDefaultExecutable();
109
+ const fullArgs = [...executableArgs, scriptPath, ...args];
110
+ return spawnClaude(executable, fullArgs, {
111
+ cwd: options.cwd,
112
+ env,
113
+ stderr: options.stderr,
114
+ });
115
+ }
116
+
117
+ // JS file without executableArgs: use script directly (shebang handles runtime)
118
+ return spawnClaude(scriptPath, args, {
119
+ cwd: options.cwd,
120
+ env,
121
+ stderr: options.stderr,
122
+ });
123
+ }
124
+ }
@@ -0,0 +1,543 @@
1
+ /**
2
+ * Query implementation with bidirectional control protocol support
3
+ *
4
+ * Combines AsyncIterableIterator pattern with control methods for:
5
+ * - Multi-turn conversations (streamInput)
6
+ * - Runtime control (interrupt, setPermissionMode, setModel)
7
+ * - Background control protocol handling
8
+ *
9
+ * @internal
10
+ */
11
+
12
+ import type { ChildProcess } from 'node:child_process';
13
+ import {
14
+ ControlProtocolHandler,
15
+ ControlRequests,
16
+ type OutboundControlRequest,
17
+ } from '../core/control.ts';
18
+ import { buildHookConfig } from '../core/hookConfig.ts';
19
+ import { McpServerBridge } from '../core/mcpBridge.ts';
20
+ import { MessageType, RequestSubtype, ResponseSubtype } from '../types/control.ts';
21
+ import type {
22
+ AccountInfo,
23
+ McpServerConfig,
24
+ McpServerStatus,
25
+ McpSetServersResult,
26
+ ModelInfo,
27
+ Options,
28
+ PermissionMode,
29
+ Query,
30
+ RewindFilesResult,
31
+ SDKControlInitializeResponse,
32
+ SDKMessage,
33
+ SDKUserMessage,
34
+ SlashCommand,
35
+ } from '../types/index.ts';
36
+ import { MessageQueue } from './MessageQueue.ts';
37
+ import { type ControlResponsePayload, MessageRouter } from './MessageRouter.ts';
38
+ import { DefaultProcessFactory, type ProcessFactory } from './ProcessFactory.ts';
39
+
40
+ export class QueryImpl implements Query {
41
+ private closed = false;
42
+ private abortHandler: (() => void) | null = null;
43
+
44
+ // Pending control request/response map (for mcpServerStatus, etc.)
45
+ private pendingControlResponses = new Map<
46
+ string,
47
+ // biome-ignore lint/suspicious/noExplicitAny: response shape varies by request type
48
+ { resolve: (value: any) => void; reject: (reason: Error) => void }
49
+ >();
50
+
51
+ private constructor(
52
+ private process: ChildProcess,
53
+ private messageQueue: MessageQueue<SDKMessage>,
54
+ private controlHandler: ControlProtocolHandler,
55
+ private router: MessageRouter,
56
+ private initResponsePromise: Promise<SDKControlInitializeResponse>,
57
+ private initResolve: (value: SDKControlInitializeResponse) => void,
58
+ private initReject: (reason: Error) => void,
59
+ private initRequestId: string,
60
+ private sdkMcpServerNames: string[],
61
+ private isSingleUserTurn: boolean,
62
+ private abortController?: AbortController
63
+ ) {}
64
+
65
+ /**
66
+ * Factory method — performs all initialization that was previously in the constructor.
67
+ */
68
+ static create(
69
+ params: { prompt: string | AsyncIterable<SDKUserMessage>; options?: Options },
70
+ processFactory: ProcessFactory = new DefaultProcessFactory()
71
+ ): QueryImpl {
72
+ const { prompt, options = {} } = params;
73
+
74
+ // Check for pre-aborted signal BEFORE spawning process (save resources)
75
+ if (options.abortController?.signal.aborted) {
76
+ return QueryImpl.createAborted();
77
+ }
78
+
79
+ // 1. Spawn process via factory
80
+ const childProcess = processFactory.spawn(options);
81
+
82
+ // Validate stdio streams exist (they should with stdio: 'pipe')
83
+ if (!childProcess.stdin || !childProcess.stdout) {
84
+ throw new Error('Process stdin/stdout not available');
85
+ }
86
+
87
+ // 2. Initialize message queue
88
+ const messageQueue = new MessageQueue<SDKMessage>();
89
+
90
+ // 3. Initialize control protocol handler
91
+ const controlHandler = new ControlProtocolHandler(childProcess.stdin, options);
92
+
93
+ // 3.5. Connect SDK MCP servers (in-process servers with `instance` property)
94
+ const sdkMcpServerNames: string[] = [];
95
+ QueryImpl.connectMcpBridges(options, controlHandler, sdkMcpServerNames);
96
+
97
+ // 4. Set up init response promise before sending init request
98
+ let initResolve!: (value: SDKControlInitializeResponse) => void;
99
+ let initReject!: (reason: Error) => void;
100
+ const initResponsePromise = new Promise<SDKControlInitializeResponse>((resolve, reject) => {
101
+ initResolve = resolve;
102
+ initReject = reject;
103
+ });
104
+
105
+ const initRequestId = `init_${Date.now()}`;
106
+ const isSingleUserTurn = typeof prompt === 'string';
107
+
108
+ // 5. Construct instance
109
+ const instance = new QueryImpl(
110
+ childProcess,
111
+ messageQueue,
112
+ controlHandler,
113
+ // router placeholder — set below after constructing with callbacks
114
+ null as unknown as MessageRouter,
115
+ initResponsePromise,
116
+ initResolve,
117
+ initReject,
118
+ initRequestId,
119
+ sdkMcpServerNames,
120
+ isSingleUserTurn,
121
+ options.abortController
122
+ );
123
+
124
+ // 6. Initialize message router with callbacks (needs instance for closures)
125
+ instance.router = new MessageRouter(
126
+ childProcess.stdout,
127
+ controlHandler,
128
+ (msg) => instance.handleMessage(msg),
129
+ (error) => instance.handleDone(error),
130
+ (response) => instance.handleControlResponse(response)
131
+ );
132
+
133
+ // 7. Start background reading
134
+ instance.router.startReading();
135
+
136
+ // 8. Send control protocol initialization
137
+ instance.sendControlProtocolInit(options);
138
+
139
+ // 9. Handle input based on type
140
+ if (typeof prompt === 'string') {
141
+ instance.sendInitialPrompt(prompt);
142
+ } else {
143
+ instance.consumeInputGenerator(prompt);
144
+ }
145
+
146
+ // 10. Setup process exit/error handlers + abort listener
147
+ instance.setupProcessHandlers();
148
+
149
+ return instance;
150
+ }
151
+
152
+ /**
153
+ * Create an already-aborted QueryImpl (no process spawned).
154
+ */
155
+ private static createAborted(): QueryImpl {
156
+ const messageQueue = new MessageQueue<SDKMessage>();
157
+ messageQueue.complete();
158
+ const abortError = new Error('Query was aborted before initialization');
159
+ const initResponsePromise = Promise.reject(abortError);
160
+ initResponsePromise.catch(() => {}); // Prevent unhandled rejection
161
+
162
+ const instance = new QueryImpl(
163
+ null as unknown as ChildProcess,
164
+ messageQueue,
165
+ null as unknown as ControlProtocolHandler,
166
+ null as unknown as MessageRouter,
167
+ initResponsePromise,
168
+ () => {},
169
+ () => {},
170
+ '',
171
+ [],
172
+ false
173
+ );
174
+ instance.closed = true;
175
+ return instance;
176
+ }
177
+
178
+ /**
179
+ * Connect SDK MCP server bridges (in-process servers with `instance` property).
180
+ */
181
+ private static connectMcpBridges(
182
+ options: Options,
183
+ controlHandler: ControlProtocolHandler,
184
+ sdkMcpServerNames: string[]
185
+ ): void {
186
+ if (!options.mcpServers) return;
187
+
188
+ const bridges = new Map<string, McpServerBridge>();
189
+ for (const [name, config] of Object.entries(options.mcpServers)) {
190
+ if ('instance' in config && config.instance) {
191
+ const bridge = new McpServerBridge(config.instance);
192
+ bridge.connect(); // async but we don't await — server connects in background
193
+ bridges.set(name, bridge);
194
+ sdkMcpServerNames.push(name);
195
+ }
196
+ }
197
+ if (bridges.size > 0) {
198
+ controlHandler.setMcpServerBridges(bridges);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Set up process exit/error handlers and abort controller listener.
204
+ */
205
+ private setupProcessHandlers(): void {
206
+ this.process.on('exit', (code) => {
207
+ if (code !== 0 && code !== null && !this.messageQueue.isDone()) {
208
+ const error = new Error(`Claude CLI exited with code ${code}`);
209
+ this.messageQueue.complete(error);
210
+ this.rejectPendingPromises(error);
211
+ } else if (!this.messageQueue.isDone()) {
212
+ this.messageQueue.complete();
213
+ this.rejectPendingPromises(new Error('CLI exited before responding'));
214
+ }
215
+ });
216
+
217
+ this.process.on('error', (err) => {
218
+ if (!this.messageQueue.isDone()) {
219
+ this.messageQueue.complete(err);
220
+ }
221
+ this.rejectPendingPromises(err);
222
+ });
223
+
224
+ if (this.abortController) {
225
+ this.abortHandler = () => {
226
+ this.interrupt();
227
+ };
228
+ this.abortController.signal.addEventListener('abort', this.abortHandler);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Handle incoming message from router
234
+ */
235
+ private handleMessage(msg: SDKMessage): void {
236
+ this.messageQueue.push(msg);
237
+
238
+ // For single-turn queries, close stdin on result to signal CLI to exit
239
+ if (msg.type === 'result' && this.isSingleUserTurn) {
240
+ this.process.stdin?.end();
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Handle stream completion from router
246
+ */
247
+ private handleDone(error?: Error): void {
248
+ if (!this.messageQueue.isDone()) {
249
+ this.messageQueue.complete(error);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Handle control_response messages from CLI
255
+ * Routes to init promise or pending request/response handlers
256
+ */
257
+ private handleControlResponse(response: ControlResponsePayload): void {
258
+ if (!response) return;
259
+
260
+ const requestId = response.request_id;
261
+
262
+ // Check if this is the init response
263
+ if (requestId === this.initRequestId) {
264
+ if (response.subtype === ResponseSubtype.SUCCESS) {
265
+ this.initResolve(response.response as SDKControlInitializeResponse);
266
+ } else {
267
+ this.initReject(new Error(`Initialization failed: ${response.error || 'unknown error'}`));
268
+ }
269
+ return;
270
+ }
271
+
272
+ // Check if there's a pending request/response handler
273
+ const pending = requestId ? this.pendingControlResponses.get(requestId) : undefined;
274
+ if (pending) {
275
+ const { resolve, reject } = pending;
276
+ this.pendingControlResponses.delete(requestId);
277
+ if (response.subtype === ResponseSubtype.SUCCESS) {
278
+ resolve(response.response);
279
+ } else {
280
+ reject(new Error(`Control request failed: ${response.error || 'unknown error'}`));
281
+ }
282
+ }
283
+ }
284
+
285
+ // ============================================================================
286
+ // AsyncGenerator implementation
287
+ // ============================================================================
288
+
289
+ async next(): Promise<IteratorResult<SDKMessage>> {
290
+ return this.messageQueue.next();
291
+ }
292
+
293
+ async return(_value?: unknown): Promise<IteratorResult<SDKMessage>> {
294
+ this.close();
295
+ return { value: undefined as unknown as SDKMessage, done: true };
296
+ }
297
+
298
+ async throw(e?: unknown): Promise<IteratorResult<SDKMessage>> {
299
+ this.close();
300
+ throw e;
301
+ }
302
+
303
+ [Symbol.asyncIterator](): AsyncGenerator<SDKMessage, void> {
304
+ return this as unknown as AsyncGenerator<SDKMessage, void>;
305
+ }
306
+
307
+ async [Symbol.asyncDispose](): Promise<void> {
308
+ this.close();
309
+ }
310
+
311
+ // ============================================================================
312
+ // Control methods (12 methods from Query interface)
313
+ // ============================================================================
314
+
315
+ async interrupt(): Promise<void> {
316
+ this.sendControlRequest(ControlRequests.interrupt());
317
+ }
318
+
319
+ async setPermissionMode(mode: PermissionMode): Promise<void> {
320
+ this.sendControlRequest(ControlRequests.setPermissionMode(mode));
321
+ }
322
+
323
+ async setModel(model?: string): Promise<void> {
324
+ this.sendControlRequest(ControlRequests.setModel(model));
325
+ }
326
+
327
+ async setMaxThinkingTokens(maxThinkingTokens: number | null): Promise<void> {
328
+ this.sendControlRequest(ControlRequests.setMaxThinkingTokens(maxThinkingTokens));
329
+ }
330
+
331
+ async streamInput(stream: AsyncIterable<SDKUserMessage>): Promise<void> {
332
+ for await (const msg of stream) {
333
+ this.writeToStdin(msg);
334
+ }
335
+ }
336
+
337
+ close(): void {
338
+ if (!this.closed) {
339
+ this.closed = true;
340
+ // Clean up abort controller listener
341
+ if (this.abortController && this.abortHandler) {
342
+ this.abortController.signal.removeEventListener('abort', this.abortHandler);
343
+ this.abortHandler = null;
344
+ }
345
+ this.router?.close();
346
+ this.process?.kill();
347
+ if (!this.messageQueue.isDone()) {
348
+ this.messageQueue.complete();
349
+ }
350
+ // Reject any pending control response promises
351
+ this.rejectPendingPromises(new Error('Query closed'));
352
+ }
353
+ }
354
+
355
+ async initializationResult(): Promise<SDKControlInitializeResponse> {
356
+ return this.initResponsePromise;
357
+ }
358
+
359
+ async supportedCommands(): Promise<SlashCommand[]> {
360
+ const init = await this.initResponsePromise;
361
+ return init.commands;
362
+ }
363
+
364
+ async supportedModels(): Promise<ModelInfo[]> {
365
+ const init = await this.initResponsePromise;
366
+ return init.models;
367
+ }
368
+
369
+ async availableOutputStyles(): Promise<string[]> {
370
+ const init = await this.initResponsePromise;
371
+ return init.available_output_styles;
372
+ }
373
+
374
+ async currentOutputStyle(): Promise<string> {
375
+ const init = await this.initResponsePromise;
376
+ return init.output_style;
377
+ }
378
+
379
+ async mcpServerStatus(): Promise<McpServerStatus[]> {
380
+ const response = await this.sendControlRequestWithResponse<{ mcpServers: McpServerStatus[] }>(
381
+ ControlRequests.mcpStatus()
382
+ );
383
+ return response.mcpServers;
384
+ }
385
+
386
+ async accountInfo(): Promise<AccountInfo> {
387
+ const init = await this.initResponsePromise;
388
+ return init.account;
389
+ }
390
+
391
+ async rewindFiles(
392
+ _userMessageId: string,
393
+ _options?: { dryRun?: boolean }
394
+ ): Promise<RewindFilesResult> {
395
+ throw new Error('rewindFiles() not yet implemented');
396
+ }
397
+
398
+ async reconnectMcpServer(serverName: string): Promise<void> {
399
+ await this.sendControlRequestWithResponse(ControlRequests.mcpReconnect(serverName));
400
+ }
401
+
402
+ async toggleMcpServer(serverName: string, enabled: boolean): Promise<void> {
403
+ await this.sendControlRequestWithResponse(ControlRequests.mcpToggle(serverName, enabled));
404
+ }
405
+
406
+ async setMcpServers(servers: Record<string, McpServerConfig>): Promise<McpSetServersResult> {
407
+ return this.sendControlRequestWithResponse(ControlRequests.mcpSetServers(servers));
408
+ }
409
+
410
+ // ============================================================================
411
+ // Private helpers
412
+ // ============================================================================
413
+
414
+ /**
415
+ * Reject initResponsePromise and all pending control response promises
416
+ */
417
+ private rejectPendingPromises(error: Error): void {
418
+ this.initReject?.(error);
419
+ for (const [, { reject }] of this.pendingControlResponses) {
420
+ reject(error);
421
+ }
422
+ this.pendingControlResponses.clear();
423
+ }
424
+
425
+ /** Write an NDJSON message to the CLI stdin */
426
+ private writeToStdin(msg: unknown): void {
427
+ this.process.stdin?.write(`${JSON.stringify(msg)}\n`);
428
+ }
429
+
430
+ /** Build a control_request envelope for the wire */
431
+ private buildControlRequest(request: OutboundControlRequest, requestId?: string) {
432
+ return {
433
+ type: MessageType.CONTROL_REQUEST,
434
+ request_id: requestId ?? this.generateRequestId(),
435
+ request,
436
+ };
437
+ }
438
+
439
+ private sendControlRequest(request: OutboundControlRequest): void {
440
+ this.writeToStdin(this.buildControlRequest(request));
441
+ }
442
+
443
+ /**
444
+ * Send a control request and return a Promise that resolves when the CLI responds
445
+ */
446
+ // biome-ignore lint/suspicious/noExplicitAny: response shape varies by request type
447
+ private sendControlRequestWithResponse<T = any>(request: OutboundControlRequest): Promise<T> {
448
+ if (this.closed) {
449
+ return Promise.reject(new Error('Cannot send control request: query is closed'));
450
+ }
451
+ const envelope = this.buildControlRequest(request);
452
+ const promise = new Promise<T>((resolve, reject) => {
453
+ this.pendingControlResponses.set(envelope.request_id, { resolve, reject });
454
+ });
455
+ this.writeToStdin(envelope);
456
+ return promise;
457
+ }
458
+
459
+ private generateRequestId(): string {
460
+ return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
461
+ }
462
+
463
+ private async sendControlProtocolInit(options: Options): Promise<void> {
464
+ const requestId = `init_${Date.now()}`;
465
+ this.initRequestId = requestId;
466
+
467
+ // Resolve systemPrompt to match official SDK behavior:
468
+ // - undefined → systemPrompt: "" (use minimal prompt, saves tokens)
469
+ // - string → systemPrompt: "..." (custom full prompt)
470
+ // - { type: 'preset', preset: 'claude_code' } → neither field (use claude_code preset)
471
+ // - { type: 'preset', preset: 'claude_code', append: '...' } → appendSystemPrompt: "..."
472
+ let systemPrompt: string | undefined;
473
+ let appendSystemPrompt: string | undefined;
474
+
475
+ if (options.systemPrompt === undefined) {
476
+ systemPrompt = '';
477
+ } else if (typeof options.systemPrompt === 'string') {
478
+ systemPrompt = options.systemPrompt;
479
+ } else if (options.systemPrompt.type === 'preset' && options.systemPrompt.append) {
480
+ appendSystemPrompt = options.systemPrompt.append;
481
+ }
482
+
483
+ const request: {
484
+ subtype: typeof RequestSubtype.INITIALIZE;
485
+ systemPrompt?: string;
486
+ appendSystemPrompt?: string;
487
+ sdkMcpServers?: string[];
488
+ agents?: Record<string, unknown>;
489
+ hooks?: ReturnType<typeof buildHookConfig>;
490
+ } = {
491
+ subtype: RequestSubtype.INITIALIZE,
492
+ ...(systemPrompt !== undefined && { systemPrompt }),
493
+ ...(appendSystemPrompt !== undefined && { appendSystemPrompt }),
494
+ ...(this.sdkMcpServerNames.length > 0 && { sdkMcpServers: this.sdkMcpServerNames }),
495
+ ...(options.agents && { agents: options.agents }),
496
+ };
497
+
498
+ // Register hooks if configured
499
+ if (options.hooks) {
500
+ request.hooks = buildHookConfig(options.hooks, this.controlHandler);
501
+ }
502
+
503
+ const init = {
504
+ type: MessageType.CONTROL_REQUEST,
505
+ request_id: requestId,
506
+ request,
507
+ };
508
+
509
+ if (process.env.DEBUG_HOOKS) {
510
+ console.error('[DEBUG] Sending control protocol init:', JSON.stringify(init, null, 2));
511
+ }
512
+
513
+ this.writeToStdin(init);
514
+ }
515
+
516
+ private sendInitialPrompt(prompt: string): void {
517
+ const initialMessage: SDKUserMessage = {
518
+ type: 'user',
519
+ message: {
520
+ role: 'user',
521
+ content: [{ type: 'text', text: prompt }],
522
+ },
523
+ session_id: '',
524
+ parent_tool_use_id: null,
525
+ };
526
+
527
+ this.writeToStdin(initialMessage);
528
+ }
529
+
530
+ private async consumeInputGenerator(generator: AsyncIterable<SDKUserMessage>): Promise<void> {
531
+ try {
532
+ for await (const userMsg of generator) {
533
+ this.writeToStdin(userMsg);
534
+ }
535
+ } catch (error: unknown) {
536
+ const wrappedError = error instanceof Error ? error : new Error(String(error));
537
+ console.error('[QueryImpl] Error consuming input generator:', wrappedError);
538
+ if (!this.messageQueue.isDone()) {
539
+ this.messageQueue.complete(wrappedError);
540
+ }
541
+ }
542
+ }
543
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Main query implementation - spawns Claude CLI and streams messages
3
+ *
4
+ * Reference: https://buildwithaws.substack.com/p/inside-the-claude-agent-sdk-from
5
+ */
6
+
7
+ import type { Options, Query, SDKUserMessage } from '../types/index.ts';
8
+ import { QueryImpl } from './QueryImpl.ts';
9
+
10
+ /**
11
+ * Main query function - returns Query interface with control methods
12
+ *
13
+ * Features:
14
+ * - AsyncGenerator for streaming messages
15
+ * - Control methods: interrupt(), setPermissionMode(), setModel(), etc.
16
+ * - Multi-turn conversations via streamInput() OR AsyncIterable input
17
+ * - Permission callbacks via options.canUseTool
18
+ * - Hook callbacks via options.hooks
19
+ *
20
+ * Input modes:
21
+ * - String: Simple one-shot or multi-turn via streamInput()
22
+ * - AsyncIterable: Streaming input mode (recommended for complex flows)
23
+ *
24
+ * Tip: Cast to ExtendedQuery to access extra convenience methods:
25
+ * const q = query({ prompt: '...' }) as ExtendedQuery;
26
+ * const styles = await q.availableOutputStyles();
27
+ *
28
+ * @param params Query parameters (prompt and options)
29
+ * @returns Query interface (AsyncGenerator + control methods)
30
+ */
31
+ export function query(params: {
32
+ prompt: string | AsyncIterable<SDKUserMessage>;
33
+ options?: Options;
34
+ }): Query {
35
+ return QueryImpl.create(params);
36
+ }