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.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/api/MessageQueue.d.ts +39 -0
- package/dist/api/MessageQueue.d.ts.map +1 -0
- package/dist/api/MessageQueue.js +90 -0
- package/dist/api/MessageQueue.js.map +1 -0
- package/dist/api/MessageRouter.d.ts +41 -0
- package/dist/api/MessageRouter.d.ts.map +1 -0
- package/dist/api/MessageRouter.js +95 -0
- package/dist/api/MessageRouter.js.map +1 -0
- package/dist/api/ProcessFactory.d.ts +23 -0
- package/dist/api/ProcessFactory.d.ts.map +1 -0
- package/dist/api/ProcessFactory.js +103 -0
- package/dist/api/ProcessFactory.js.map +1 -0
- package/dist/api/QueryImpl.d.ts +103 -0
- package/dist/api/QueryImpl.d.ts.map +1 -0
- package/dist/api/QueryImpl.js +417 -0
- package/dist/api/QueryImpl.js.map +1 -0
- package/dist/api/query.d.ts +32 -0
- package/dist/api/query.d.ts.map +1 -0
- package/dist/api/query.js +31 -0
- package/dist/api/query.js.map +1 -0
- package/dist/core/argBuilder.d.ts +16 -0
- package/dist/core/argBuilder.d.ts.map +1 -0
- package/dist/core/argBuilder.js +204 -0
- package/dist/core/argBuilder.js.map +1 -0
- package/dist/core/control.d.ts +66 -0
- package/dist/core/control.d.ts.map +1 -0
- package/dist/core/control.js +222 -0
- package/dist/core/control.js.map +1 -0
- package/dist/core/hookConfig.d.ts +31 -0
- package/dist/core/hookConfig.d.ts.map +1 -0
- package/dist/core/hookConfig.js +45 -0
- package/dist/core/hookConfig.js.map +1 -0
- package/dist/core/mcpBridge.d.ts +29 -0
- package/dist/core/mcpBridge.d.ts.map +1 -0
- package/dist/core/mcpBridge.js +71 -0
- package/dist/core/mcpBridge.js.map +1 -0
- package/dist/core/spawn.d.ts +33 -0
- package/dist/core/spawn.d.ts.map +1 -0
- package/dist/core/spawn.js +102 -0
- package/dist/core/spawn.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +6 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp-entry.d.ts +22 -0
- package/dist/mcp-entry.d.ts.map +1 -0
- package/dist/mcp-entry.js +22 -0
- package/dist/mcp-entry.js.map +1 -0
- package/dist/mcp.d.ts +101 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +78 -0
- package/dist/mcp.js.map +1 -0
- package/dist/query.d.ts +19 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +18 -0
- package/dist/query.js.map +1 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types/control.d.ts +149 -0
- package/dist/types/control.d.ts.map +1 -0
- package/dist/types/control.js +40 -0
- package/dist/types/control.js.map +1 -0
- package/dist/types/index.d.ts +53 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +11 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +85 -0
- package/src/api/MessageQueue.ts +99 -0
- package/src/api/MessageRouter.ts +112 -0
- package/src/api/ProcessFactory.ts +124 -0
- package/src/api/QueryImpl.ts +543 -0
- package/src/api/query.ts +36 -0
- package/src/core/argBuilder.ts +236 -0
- package/src/core/control.ts +295 -0
- package/src/core/hookConfig.ts +70 -0
- package/src/core/mcpBridge.ts +81 -0
- package/src/core/spawn.ts +125 -0
- package/src/index.ts +12 -0
- package/src/mcp/index.ts +6 -0
- package/src/mcp-entry.ts +22 -0
- package/src/mcp.ts +148 -0
- package/src/query.ts +21 -0
- package/src/tools/README.md +171 -0
- package/src/tools/capture-cli.cjs +202 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/proxy-cli.cjs +162 -0
- package/src/types/control.ts +196 -0
- 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
|
+
}
|
package/src/api/query.ts
ADDED
|
@@ -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
|
+
}
|