@stacksjs/ai 0.70.53 → 0.70.55

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.
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Claude Agent SDK Driver
3
+ *
4
+ * Provides full agentic capabilities using the official Claude Agent SDK.
5
+ * This driver uses @anthropic-ai/claude-agent-sdk for programmatic access
6
+ * to Claude Code's capabilities including built-in tools for file operations,
7
+ * command execution, and code analysis.
8
+ *
9
+ * Authentication:
10
+ * - Uses ANTHROPIC_API_KEY environment variable if set
11
+ * - Falls back to local Claude Code authentication if available
12
+ *
13
+ * @see https://docs.anthropic.com/en/docs/claude-code/sdk
14
+ */
15
+
16
+ import type { AIDriver, AIDriverConfig, AIMessage, StreamingResult } from '../../types'
17
+
18
+ // Lazy import to avoid issues if SDK is not installed
19
+ let sdkModule: typeof import('@anthropic-ai/claude-agent-sdk') | null = null
20
+
21
+ async function getSDK() {
22
+ if (!sdkModule) {
23
+ try {
24
+ sdkModule = await import('@anthropic-ai/claude-agent-sdk')
25
+ }
26
+ catch {
27
+ throw new Error(
28
+ 'Claude Agent SDK not installed. Run: bun add @anthropic-ai/claude-agent-sdk',
29
+ )
30
+ }
31
+ }
32
+ return sdkModule
33
+ }
34
+
35
+ export interface ClaudeAgentSDKConfig extends AIDriverConfig {
36
+ /** Maximum agent turns before stopping (default: 25) */
37
+ maxTurns?: number
38
+ /** Working directory for the agent */
39
+ cwd?: string
40
+ /** Tools the agent is allowed to use */
41
+ allowedTools?: string[]
42
+ /** Tools the agent is not allowed to use */
43
+ disallowedTools?: string[]
44
+ /** Permission mode for tool execution */
45
+ permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'
46
+ /** Custom system prompt (overrides default) */
47
+ customSystemPrompt?: string
48
+ /** Appended to the default system prompt */
49
+ appendSystemPrompt?: string
50
+ /** Session ID to resume a previous conversation */
51
+ resumeSessionId?: string
52
+ }
53
+
54
+ // SDK state for session management
55
+ interface SDKState {
56
+ lastSessionId?: string
57
+ }
58
+
59
+ const sdkState: SDKState = {
60
+ lastSessionId: undefined,
61
+ }
62
+
63
+ // Default configuration
64
+ const DEFAULT_CONFIG: Required<Pick<ClaudeAgentSDKConfig, 'maxTurns' | 'allowedTools' | 'permissionMode'>> = {
65
+ maxTurns: 25,
66
+ allowedTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'],
67
+ permissionMode: 'bypassPermissions',
68
+ }
69
+
70
+ /**
71
+ * Create a Claude Agent SDK driver instance
72
+ */
73
+ export function createClaudeAgentSDKDriver(config: ClaudeAgentSDKConfig = {}): AIDriver {
74
+ const {
75
+ maxTurns = DEFAULT_CONFIG.maxTurns,
76
+ cwd,
77
+ allowedTools = DEFAULT_CONFIG.allowedTools,
78
+ disallowedTools,
79
+ permissionMode = DEFAULT_CONFIG.permissionMode,
80
+ customSystemPrompt,
81
+ appendSystemPrompt,
82
+ resumeSessionId,
83
+ } = config
84
+
85
+ return {
86
+ name: 'Claude Agent SDK',
87
+
88
+ async process(command: string, systemPrompt: string, _history: AIMessage[]): Promise<string> {
89
+ const sdk = await getSDK()
90
+ const { query } = sdk
91
+
92
+ // Build full prompt with context
93
+ const fullPrompt = systemPrompt
94
+ ? `${systemPrompt}\n\nUser request: ${command}`
95
+ : command
96
+
97
+ // Build SDK options
98
+ const options: Record<string, unknown> = {
99
+ allowedTools,
100
+ permissionMode,
101
+ maxTurns,
102
+ }
103
+
104
+ if (disallowedTools) {
105
+ options.disallowedTools = disallowedTools
106
+ }
107
+
108
+ if (customSystemPrompt) {
109
+ options.customSystemPrompt = customSystemPrompt
110
+ }
111
+
112
+ if (appendSystemPrompt) {
113
+ options.appendSystemPrompt = appendSystemPrompt
114
+ }
115
+
116
+ if (cwd) {
117
+ options.cwd = cwd
118
+ }
119
+
120
+ if (resumeSessionId || sdkState.lastSessionId) {
121
+ options.resume = resumeSessionId || sdkState.lastSessionId
122
+ }
123
+
124
+ let result = ''
125
+
126
+ try {
127
+ for await (const message of query({ prompt: fullPrompt, options })) {
128
+ // Capture session ID for potential resume
129
+ if (message.type === 'system' && message.subtype === 'init') {
130
+ sdkState.lastSessionId = (message as { session_id: string }).session_id
131
+ }
132
+
133
+ // Capture the final result
134
+ if ('result' in message && typeof message.result === 'string') {
135
+ result = message.result
136
+ }
137
+
138
+ // Tool usage events are available for debugging if needed
139
+ }
140
+
141
+ return result || 'No response from Claude Agent SDK'
142
+ }
143
+ catch (error) {
144
+ const err = error as Error
145
+ if (err.message.includes('ANTHROPIC_API_KEY')) {
146
+ throw new Error(
147
+ 'Claude Agent SDK requires ANTHROPIC_API_KEY environment variable or Claude Code authentication.',
148
+ )
149
+ }
150
+ throw new Error(`Claude Agent SDK error: ${err.message}`)
151
+ }
152
+ },
153
+
154
+ async *stream(command: string, systemPrompt: string, _history: AIMessage[]): AsyncGenerator<string> {
155
+ const sdk = await getSDK()
156
+ const { query } = sdk
157
+
158
+ // Build full prompt with context
159
+ const fullPrompt = systemPrompt
160
+ ? `${systemPrompt}\n\nUser request: ${command}`
161
+ : command
162
+
163
+ // Build SDK options
164
+ const options: Record<string, unknown> = {
165
+ allowedTools,
166
+ permissionMode,
167
+ maxTurns,
168
+ }
169
+
170
+ if (disallowedTools) {
171
+ options.disallowedTools = disallowedTools
172
+ }
173
+
174
+ if (customSystemPrompt) {
175
+ options.customSystemPrompt = customSystemPrompt
176
+ }
177
+
178
+ if (appendSystemPrompt) {
179
+ options.appendSystemPrompt = appendSystemPrompt
180
+ }
181
+
182
+ if (cwd) {
183
+ options.cwd = cwd
184
+ }
185
+
186
+ if (resumeSessionId || sdkState.lastSessionId) {
187
+ options.resume = resumeSessionId || sdkState.lastSessionId
188
+ }
189
+
190
+ try {
191
+ for await (const message of query({ prompt: fullPrompt, options })) {
192
+ // Capture session ID
193
+ if (message.type === 'system' && message.subtype === 'init') {
194
+ sdkState.lastSessionId = (message as { session_id: string }).session_id
195
+ }
196
+
197
+ // Yield assistant text content as it streams
198
+ if (message.type === 'assistant') {
199
+ const assistantMsg = message as {
200
+ message?: { content?: Array<{ type: string, text?: string }> }
201
+ }
202
+ if (assistantMsg.message?.content) {
203
+ for (const block of assistantMsg.message.content) {
204
+ if (block.type === 'text' && block.text) {
205
+ yield block.text
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ // Yield the final result
212
+ if ('result' in message && typeof message.result === 'string') {
213
+ yield message.result
214
+ }
215
+ }
216
+ }
217
+ catch (error) {
218
+ const err = error as Error
219
+ if (err.message.includes('ANTHROPIC_API_KEY')) {
220
+ throw new Error(
221
+ 'Claude Agent SDK requires ANTHROPIC_API_KEY environment variable or Claude Code authentication.',
222
+ )
223
+ }
224
+ throw new Error(`Claude Agent SDK error: ${err.message}`)
225
+ }
226
+ },
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Process a command with streaming and return a StreamingResult
232
+ */
233
+ export async function processStreaming(
234
+ command: string,
235
+ cwd?: string,
236
+ config: Omit<ClaudeAgentSDKConfig, 'cwd'> = {},
237
+ ): Promise<StreamingResult> {
238
+ const sdk = await getSDK()
239
+ const { query } = sdk
240
+
241
+ const {
242
+ maxTurns = DEFAULT_CONFIG.maxTurns,
243
+ allowedTools = DEFAULT_CONFIG.allowedTools,
244
+ disallowedTools,
245
+ permissionMode = DEFAULT_CONFIG.permissionMode,
246
+ customSystemPrompt,
247
+ appendSystemPrompt,
248
+ resumeSessionId,
249
+ } = config
250
+
251
+ // Build SDK options
252
+ const options: Record<string, unknown> = {
253
+ allowedTools,
254
+ permissionMode,
255
+ maxTurns,
256
+ }
257
+
258
+ if (disallowedTools) options.disallowedTools = disallowedTools
259
+ if (customSystemPrompt) options.customSystemPrompt = customSystemPrompt
260
+ if (appendSystemPrompt) options.appendSystemPrompt = appendSystemPrompt
261
+ if (cwd) options.cwd = cwd
262
+ if (resumeSessionId || sdkState.lastSessionId) {
263
+ options.resume = resumeSessionId || sdkState.lastSessionId
264
+ }
265
+
266
+ const encoder = new TextEncoder()
267
+ let fullResponse = ''
268
+ let resolveFullResponse: (value: string) => void
269
+
270
+ const fullResponsePromise = new Promise<string>((resolve) => {
271
+ resolveFullResponse = resolve
272
+ })
273
+
274
+ const stream = new ReadableStream<Uint8Array>({
275
+ async start(controller) {
276
+ try {
277
+ for await (const message of query({ prompt: command, options })) {
278
+ // Capture session ID
279
+ if (message.type === 'system' && message.subtype === 'init') {
280
+ sdkState.lastSessionId = (message as { session_id: string }).session_id
281
+ }
282
+
283
+ // Stream assistant text content
284
+ if (message.type === 'assistant') {
285
+ const assistantMsg = message as {
286
+ message?: { content?: Array<{ type: string, text?: string }> }
287
+ }
288
+ if (assistantMsg.message?.content) {
289
+ for (const block of assistantMsg.message.content) {
290
+ if (block.type === 'text' && block.text) {
291
+ fullResponse += block.text
292
+ controller.enqueue(encoder.encode(block.text))
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ // Capture final result
299
+ if ('result' in message && typeof message.result === 'string') {
300
+ if (!fullResponse) {
301
+ fullResponse = message.result
302
+ controller.enqueue(encoder.encode(message.result))
303
+ }
304
+ }
305
+ }
306
+
307
+ resolveFullResponse(fullResponse)
308
+ controller.close()
309
+ }
310
+ catch (error) {
311
+ resolveFullResponse(fullResponse)
312
+ controller.error(error)
313
+ }
314
+ },
315
+ })
316
+
317
+ return {
318
+ stream,
319
+ fullResponse: fullResponsePromise,
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Resume a previous SDK session
325
+ */
326
+ export async function resumeSession(sessionId: string, prompt: string): Promise<string> {
327
+ const sdk = await getSDK()
328
+ const { query } = sdk
329
+
330
+ let result = ''
331
+
332
+ for await (const message of query({
333
+ prompt,
334
+ options: {
335
+ resume: sessionId,
336
+ permissionMode: 'bypassPermissions',
337
+ },
338
+ })) {
339
+ if ('result' in message && typeof message.result === 'string') {
340
+ result = message.result
341
+ }
342
+ }
343
+
344
+ return result || 'No response from resumed session'
345
+ }
346
+
347
+ /**
348
+ * Get the last session ID for potential resume
349
+ */
350
+ export function getLastSessionId(): string | undefined {
351
+ return sdkState.lastSessionId
352
+ }
353
+
354
+ /**
355
+ * Clear the stored session ID
356
+ */
357
+ export function clearSession(): void {
358
+ sdkState.lastSessionId = undefined
359
+ }
360
+
361
+ // Export the driver creator and utilities
362
+ export const claudeAgentSDK = {
363
+ createDriver: createClaudeAgentSDKDriver,
364
+ processStreaming,
365
+ resumeSession,
366
+ getLastSessionId,
367
+ clearSession,
368
+ }
369
+
370
+ export default claudeAgentSDK
@@ -0,0 +1,14 @@
1
+ /**
2
+ * AI Drivers
3
+ *
4
+ * Export all available AI drivers.
5
+ */
6
+
7
+ export { createAnthropicDriver, anthropicDriver, anthropic, estimateTokens } from './anthropic'
8
+ export type { AnthropicDriverConfig } from './anthropic'
9
+ export { createOpenAIDriver, openaiDriver, openai } from './openai'
10
+ export type { OpenAIDriverConfig } from './openai'
11
+ export { createOllamaDriver, ollamaDriver, ollama } from './ollama'
12
+ export type { OllamaDriverConfig } from './ollama'
13
+ export { createClaudeAgentSDKDriver, claudeAgentSDK, getLastSessionId, clearSession } from './claude-agent-sdk'
14
+ export type { ClaudeAgentSDKConfig } from './claude-agent-sdk'