diff --git a/docs/FLUENT_API.md b/docs/FLUENT_API.md index d97dd1e..e170d64 100644 --- a/docs/FLUENT_API.md +++ b/docs/FLUENT_API.md @@ -3,6 +3,7 @@ The Claude Code SDK now includes a powerful fluent API that makes it easier to build and execute queries with a chainable interface. ## Table of Contents + - [Getting Started](#getting-started) - [Query Builder](#query-builder) - [Response Parser](#response-parser) @@ -18,11 +19,7 @@ The fluent API provides a more intuitive way to interact with Claude Code: import { claude } from '@instantlyeasy/claude-code-sdk-ts'; // Simple example -const response = await claude() - .withModel('sonnet') - .skipPermissions() - .query('Hello, Claude!') - .asText(); +const response = await claude().withModel('sonnet').skipPermissions().query('Hello, Claude!').asText(); ``` ## Query Builder @@ -33,44 +30,54 @@ The `QueryBuilder` class provides chainable methods for configuring your query: ```typescript claude() - .withModel('opus') // or 'sonnet', 'haiku' - .withTimeout(60000) // 60 seconds - .debug(true) // Enable debug mode + .withModel('opus') // or 'sonnet', 'haiku' + .withTimeout(60000) // 60 seconds + .debug(true); // Enable debug mode ``` ### Tool Management ```typescript claude() - .allowTools('Read', 'Write', 'Edit') // Explicitly allow tools - .denyTools('Bash', 'WebSearch') // Explicitly deny tools + .allowTools('Read', 'Write', 'Edit') // Explicitly allow tools + .denyTools('Bash', 'WebSearch'); // Explicitly deny tools ``` ### Permissions ```typescript claude() - .skipPermissions() // Bypass all permission prompts - .acceptEdits() // Auto-accept file edits - .withPermissions('default') // Use default permission handling + .skipPermissions() // Bypass all permission prompts + .acceptEdits() // Auto-accept file edits + .withPermissions('default'); // Use default permission handling ``` ### Environment Configuration +```typescript +claude().inDirectory('/path/to/project').withEnv({ NODE_ENV: 'production' }); +``` + +### Directory Context + ```typescript claude() - .inDirectory('/path/to/project') - .withEnv({ NODE_ENV: 'production' }) + .addDirectory('/path/to/dir') // Add single directory + .addDirectory(['../apps', '../lib']) // Add multiple directories + .addDirectory('/another/dir'); // Accumulate with multiple calls ``` +The `addDirectory` method allows you to add additional working directories for Claude to access (validates each path exists as a directory). + +- **Single directory**: Pass a string path +- **Multiple directories**: Pass an array of string paths +- **Accumulative**: Multiple calls to `addDirectory` will accumulate all directories +- **CLI mapping**: Generates `--add-dir` flag with space-separated paths + ### MCP Servers ```typescript -claude() - .withMCP( - { command: 'mcp-server-filesystem', args: ['--readonly'] }, - { command: 'mcp-server-git' } - ) +claude().withMCP({ command: 'mcp-server-filesystem', args: ['--readonly'] }, { command: 'mcp-server-git' }); ``` ### Event Handlers @@ -79,7 +86,7 @@ claude() claude() .onMessage(msg => console.log('Message:', msg.type)) .onAssistant(content => console.log('Assistant says...')) - .onToolUse(tool => console.log(`Using ${tool.name}`)) + .onToolUse(tool => console.log(`Using ${tool.name}`)); ``` ## Response Parser @@ -126,7 +133,7 @@ console.log(`Cost: $${usage.totalCost}`); ### Streaming ```typescript -await parser.stream(async (message) => { +await parser.stream(async message => { if (message.type === 'assistant') { // Handle streaming content } @@ -165,9 +172,7 @@ const logger = new ConsoleLogger(LogLevel.DEBUG, '[MyApp]'); const jsonLogger = new JSONLogger(LogLevel.INFO); // Use with QueryBuilder -claude() - .withLogger(logger) - .query('...'); +claude().withLogger(logger).query('...'); ``` ### Custom Logger Implementation @@ -188,9 +193,14 @@ class CustomLogger implements Logger { // Implement convenience methods error(message: string, context?: Record): void { - this.log({ level: LogLevel.ERROR, message, timestamp: new Date(), context }); + this.log({ + level: LogLevel.ERROR, + message, + timestamp: new Date(), + context + }); } - + // ... implement warn, info, debug, trace } ``` @@ -214,10 +224,7 @@ const multiLogger = new MultiLogger([ async function queryWithRetry(prompt: string, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { - return await claude() - .withTimeout(30000) - .query(prompt) - .asText(); + return await claude().withTimeout(30000).query(prompt).asText(); } catch (error) { if (i === maxRetries - 1) throw error; await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i))); @@ -231,13 +238,13 @@ async function queryWithRetry(prompt: string, maxRetries = 3) { ```typescript function createQuery(options: { readonly?: boolean }) { const builder = claude(); - + if (options.readonly) { builder.allowTools('Read', 'Grep', 'Glob').denyTools('Write', 'Edit'); } else { builder.allowTools('Read', 'Write', 'Edit'); } - + return builder; } ``` @@ -248,16 +255,14 @@ function createQuery(options: { readonly?: boolean }) { const cache = new Map(); async function cachedQuery(prompt: string) { - const cacheKey = `${prompt}:${Date.now() / 60000 | 0}`; // 1-minute cache - + const cacheKey = `${prompt}:${(Date.now() / 60000) | 0}`; // 1-minute cache + if (cache.has(cacheKey)) { return cache.get(cacheKey); } - - const result = await claude() - .query(prompt) - .asText(); - + + const result = await claude().query(prompt).asText(); + cache.set(cacheKey, result); return result; } @@ -283,7 +288,7 @@ import { claude } from '@instantlyeasy/claude-code-sdk-ts'; await claude() .withModel('sonnet') .query('Hello') - .stream(async (message) => { + .stream(async message => { // Process messages }); ``` @@ -291,6 +296,7 @@ await claude() ### Common Migration Patterns 1. **Simple text extraction**: + ```typescript // Before let text = ''; @@ -305,12 +311,11 @@ for await (const message of query('Generate text')) { } // After -const text = await claude() - .query('Generate text') - .asText(); +const text = await claude().query('Generate text').asText(); ``` 2. **Tool result extraction**: + ```typescript // Before const results = []; @@ -325,13 +330,11 @@ for await (const message of query('Read files', { allowedTools: ['Read'] })) { } // After -const results = await claude() - .allowTools('Read') - .query('Read files') - .findToolResults('Read'); +const results = await claude().allowTools('Read').query('Read files').findToolResults('Read'); ``` 3. **Error handling**: + ```typescript // Before try { @@ -351,4 +354,4 @@ if (!success) { } ``` -The fluent API is designed to reduce boilerplate while maintaining the full power of the original API. You can mix and match approaches as needed for your use case. \ No newline at end of file +The fluent API is designed to reduce boilerplate while maintaining the full power of the original API. You can mix and match approaches as needed for your use case. diff --git a/src/_internal/transport/subprocess-cli.ts b/src/_internal/transport/subprocess-cli.ts index 4d2864f..b016698 100644 --- a/src/_internal/transport/subprocess-cli.ts +++ b/src/_internal/transport/subprocess-cli.ts @@ -20,11 +20,8 @@ export class SubprocessCLITransport { private async findCLI(): Promise { // First check for local Claude installation (newer version with --output-format support) - const localPaths = [ - join(homedir(), '.claude', 'local', 'claude'), - join(homedir(), '.claude', 'bin', 'claude') - ]; - + const localPaths = [join(homedir(), '.claude', 'local', 'claude'), join(homedir(), '.claude', 'bin', 'claude')]; + for (const path of localPaths) { try { await access(path, constants.X_OK); @@ -33,7 +30,7 @@ export class SubprocessCLITransport { // Continue checking } } - + // Then try to find in PATH - try both 'claude' and 'claude-code' for compatibility try { return await which('claude'); @@ -70,7 +67,7 @@ export class SubprocessCLITransport { join(home, '.local', 'bin', 'claude-code'), join(home, 'bin', 'claude'), join(home, 'bin', 'claude-code'), - join(home, '.claude', 'local', 'claude') // Claude's custom installation path + join(home, '.claude', 'local', 'claude') // Claude's custom installation path ); } @@ -78,10 +75,7 @@ export class SubprocessCLITransport { try { const { stdout: npmPrefix } = await execa('npm', ['config', 'get', 'prefix']); if (npmPrefix) { - paths.push( - join(npmPrefix.trim(), 'bin', 'claude'), - join(npmPrefix.trim(), 'bin', 'claude-code') - ); + paths.push(join(npmPrefix.trim(), 'bin', 'claude'), join(npmPrefix.trim(), 'bin', 'claude-code')); } } catch { // Ignore error and continue @@ -93,8 +87,8 @@ export class SubprocessCLITransport { await execa(path, ['--version']); return path; } catch { - // Ignore error and continue - } + // Ignore error and continue + } } throw new CLINotFoundError(); @@ -107,7 +101,7 @@ export class SubprocessCLITransport { // Claude CLI supported flags (from --help) if (this.options.model) args.push('--model', this.options.model); // Don't pass --debug flag as it produces non-JSON output - + // Note: Claude CLI handles authentication internally // It will use either session auth or API key based on user's setup @@ -133,6 +127,11 @@ export class SubprocessCLITransport { args.push('--mcp-config', JSON.stringify(mcpConfig)); } + // Handle add directories + if (this.options.addDirectories && this.options.addDirectories.length > 0) { + args.push('--add-dir', this.options.addDirectories.join(' ')); + } + // Add --print flag (prompt will be sent via stdin) args.push('--print'); @@ -163,7 +162,7 @@ export class SubprocessCLITransport { stderr: 'pipe', buffer: false }); - + // Send prompt via stdin if (this.process.stdin) { this.process.stdin.write(this.prompt); @@ -185,8 +184,8 @@ export class SubprocessCLITransport { input: this.process.stderr, crlfDelay: Infinity }); - - stderrRl.on('line', (line) => { + + stderrRl.on('line', line => { if (this.options.debug) { console.error('DEBUG stderr:', line); } @@ -202,21 +201,18 @@ export class SubprocessCLITransport { for await (const line of rl) { const trimmedLine = line.trim(); if (!trimmedLine) continue; - + if (this.options.debug) { console.error('DEBUG stdout:', trimmedLine); } - + try { const parsed = JSON.parse(trimmedLine) as CLIOutput; yield parsed; } catch (error) { // Skip non-JSON lines (like Python SDK does) if (trimmedLine.startsWith('{') || trimmedLine.startsWith('[')) { - throw new CLIJSONDecodeError( - `Failed to parse CLI output: ${error}`, - trimmedLine - ); + throw new CLIJSONDecodeError(`Failed to parse CLI output: ${error}`, trimmedLine); } continue; } @@ -227,11 +223,7 @@ export class SubprocessCLITransport { await this.process; } catch (error: any) { if (error.exitCode !== 0) { - throw new ProcessError( - `Claude Code CLI exited with code ${error.exitCode}`, - error.exitCode, - error.signal - ); + throw new ProcessError(`Claude Code CLI exited with code ${error.exitCode}`, error.exitCode, error.signal); } } } @@ -242,4 +234,4 @@ export class SubprocessCLITransport { this.process = undefined; } } -} \ No newline at end of file +} diff --git a/src/fluent.ts b/src/fluent.ts index c963c70..d6f963e 100644 --- a/src/fluent.ts +++ b/src/fluent.ts @@ -5,7 +5,7 @@ import { Logger } from './logger.js'; /** * Fluent API for building Claude Code queries with chainable methods - * + * * @example * ```typescript * const result = await claude() @@ -111,6 +111,18 @@ export class QueryBuilder { return this; } + /** + * Add directory(-ies) to include in the context + */ + addDirectory(directories: string | string[]): this { + if (!this.options.addDirectories) { + this.options.addDirectories = []; + } + const dirsToAdd = Array.isArray(directories) ? directories : [directories]; + this.options.addDirectories.push(...dirsToAdd); + return this; + } + /** * Set logger */ @@ -131,7 +143,7 @@ export class QueryBuilder { * Add handler for specific message type */ onAssistant(handler: (content: any) => void): this { - this.messageHandlers.push((msg) => { + this.messageHandlers.push(msg => { if (msg.type === 'assistant') { handler((msg as any).content); } @@ -143,7 +155,7 @@ export class QueryBuilder { * Add handler for tool usage */ onToolUse(handler: (tool: { name: string; input: any }) => void): this { - this.messageHandlers.push((msg) => { + this.messageHandlers.push(msg => { if (msg.type === 'assistant') { for (const block of msg.content) { if (block.type === 'tool_use') { @@ -159,11 +171,7 @@ export class QueryBuilder { * Execute query and return response parser */ query(prompt: string): ResponseParser { - const parser = new ResponseParser( - baseQuery(prompt, this.options), - this.messageHandlers, - this.logger - ); + const parser = new ResponseParser(baseQuery(prompt, this.options), this.messageHandlers, this.logger); return parser; } @@ -172,10 +180,10 @@ export class QueryBuilder { */ async *queryRaw(prompt: string): AsyncGenerator { this.logger?.info('Starting query', { prompt, options: this.options }); - + for await (const message of baseQuery(prompt, this.options)) { this.logger?.debug('Received message', { type: message.type }); - + // Run handlers for (const handler of this.messageHandlers) { try { @@ -184,10 +192,10 @@ export class QueryBuilder { this.logger?.error('Message handler error', { error }); } } - + yield message; } - + this.logger?.info('Query completed'); } @@ -201,7 +209,7 @@ export class QueryBuilder { /** * Factory function for creating a new query builder - * + * * @example * ```typescript * const response = await claude() @@ -216,4 +224,4 @@ export function claude(): QueryBuilder { // Re-export for convenience export { ResponseParser } from './parser.js'; -export { Logger, LogLevel, ConsoleLogger } from './logger.js'; \ No newline at end of file +export { Logger, LogLevel, ConsoleLogger } from './logger.js'; diff --git a/src/types.ts b/src/types.ts index ac3ff08..7f7e036 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,7 @@ export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions'; // Tool names that can be allowed or denied -export type ToolName = +export type ToolName = | 'Read' | 'Write' | 'Edit' @@ -104,6 +104,7 @@ export interface ClaudeCodeOptions { env?: Record; timeout?: number; debug?: boolean; + addDirectories?: string[]; } // Additional types for internal use @@ -125,4 +126,4 @@ export interface CLIEnd { type: 'end'; } -export type CLIOutput = CLIMessage | CLIError | CLIEnd; \ No newline at end of file +export type CLIOutput = CLIMessage | CLIError | CLIEnd; diff --git a/tests/fluent.test.ts b/tests/fluent.test.ts index abcc505..0a93033 100644 --- a/tests/fluent.test.ts +++ b/tests/fluent.test.ts @@ -40,9 +40,12 @@ describe('QueryBuilder', () => { const builder = claude().withModel('opus'); builder.query('test'); - expect(mockQuery).toHaveBeenCalledWith('test', expect.objectContaining({ - model: 'opus' - })); + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + model: 'opus' + }) + ); }); it('should chain multiple configurations', () => { @@ -50,19 +53,17 @@ describe('QueryBuilder', () => { yield { type: 'result', content: 'done' }; }); - claude() - .withModel('sonnet') - .withTimeout(5000) - .debug(true) - .inDirectory('/test/dir') - .query('test'); - - expect(mockQuery).toHaveBeenCalledWith('test', expect.objectContaining({ - model: 'sonnet', - timeout: 5000, - debug: true, - cwd: '/test/dir' - })); + claude().withModel('sonnet').withTimeout(5000).debug(true).inDirectory('/test/dir').query('test'); + + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + model: 'sonnet', + timeout: 5000, + debug: true, + cwd: '/test/dir' + }) + ); }); }); @@ -72,13 +73,14 @@ describe('QueryBuilder', () => { yield { type: 'result', content: 'done' }; }); - claude() - .allowTools('Read', 'Write', 'Edit') - .query('test'); + claude().allowTools('Read', 'Write', 'Edit').query('test'); - expect(mockQuery).toHaveBeenCalledWith('test', expect.objectContaining({ - allowedTools: ['Read', 'Write', 'Edit'] - })); + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + allowedTools: ['Read', 'Write', 'Edit'] + }) + ); }); it('should set denied tools', () => { @@ -86,13 +88,14 @@ describe('QueryBuilder', () => { yield { type: 'result', content: 'done' }; }); - claude() - .denyTools('Bash', 'WebSearch') - .query('test'); + claude().denyTools('Bash', 'WebSearch').query('test'); - expect(mockQuery).toHaveBeenCalledWith('test', expect.objectContaining({ - deniedTools: ['Bash', 'WebSearch'] - })); + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + deniedTools: ['Bash', 'WebSearch'] + }) + ); }); it('should handle both allowed and denied tools', () => { @@ -100,15 +103,15 @@ describe('QueryBuilder', () => { yield { type: 'result', content: 'done' }; }); - claude() - .allowTools('Read') - .denyTools('Write') - .query('test'); + claude().allowTools('Read').denyTools('Write').query('test'); - expect(mockQuery).toHaveBeenCalledWith('test', expect.objectContaining({ - allowedTools: ['Read'], - deniedTools: ['Write'] - })); + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + allowedTools: ['Read'], + deniedTools: ['Write'] + }) + ); }); }); @@ -118,13 +121,14 @@ describe('QueryBuilder', () => { yield { type: 'result', content: 'done' }; }); - claude() - .skipPermissions() - .query('test'); + claude().skipPermissions().query('test'); - expect(mockQuery).toHaveBeenCalledWith('test', expect.objectContaining({ - permissionMode: 'bypassPermissions' - })); + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + permissionMode: 'bypassPermissions' + }) + ); }); it('should set accept edits', () => { @@ -132,13 +136,14 @@ describe('QueryBuilder', () => { yield { type: 'result', content: 'done' }; }); - claude() - .acceptEdits() - .query('test'); + claude().acceptEdits().query('test'); - expect(mockQuery).toHaveBeenCalledWith('test', expect.objectContaining({ - permissionMode: 'acceptEdits' - })); + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + permissionMode: 'acceptEdits' + }) + ); }); it('should set custom permission mode', () => { @@ -146,13 +151,14 @@ describe('QueryBuilder', () => { yield { type: 'result', content: 'done' }; }); - claude() - .withPermissions('default') - .query('test'); + claude().withPermissions('default').query('test'); - expect(mockQuery).toHaveBeenCalledWith('test', expect.objectContaining({ - permissionMode: 'default' - })); + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + permissionMode: 'default' + }) + ); }); }); @@ -162,13 +168,14 @@ describe('QueryBuilder', () => { yield { type: 'result', content: 'done' }; }); - claude() - .withEnv({ NODE_ENV: 'test', API_KEY: 'secret' }) - .query('test'); + claude().withEnv({ NODE_ENV: 'test', API_KEY: 'secret' }).query('test'); - expect(mockQuery).toHaveBeenCalledWith('test', expect.objectContaining({ - env: { NODE_ENV: 'test', API_KEY: 'secret' } - })); + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + env: { NODE_ENV: 'test', API_KEY: 'secret' } + }) + ); }); it('should merge multiple env calls', () => { @@ -176,14 +183,14 @@ describe('QueryBuilder', () => { yield { type: 'result', content: 'done' }; }); - claude() - .withEnv({ NODE_ENV: 'test' }) - .withEnv({ API_KEY: 'secret' }) - .query('test'); + claude().withEnv({ NODE_ENV: 'test' }).withEnv({ API_KEY: 'secret' }).query('test'); - expect(mockQuery).toHaveBeenCalledWith('test', expect.objectContaining({ - env: { NODE_ENV: 'test', API_KEY: 'secret' } - })); + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + env: { NODE_ENV: 'test', API_KEY: 'secret' } + }) + ); }); }); @@ -194,18 +201,15 @@ describe('QueryBuilder', () => { }); claude() - .withMCP( - { command: 'mcp-server-1' }, - { command: 'mcp-server-2', args: ['--flag'] } - ) + .withMCP({ command: 'mcp-server-1' }, { command: 'mcp-server-2', args: ['--flag'] }) .query('test'); - expect(mockQuery).toHaveBeenCalledWith('test', expect.objectContaining({ - mcpServers: [ - { command: 'mcp-server-1' }, - { command: 'mcp-server-2', args: ['--flag'] } - ] - })); + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + mcpServers: [{ command: 'mcp-server-1' }, { command: 'mcp-server-2', args: ['--flag'] }] + }) + ); }); it('should accumulate MCP servers from multiple calls', () => { @@ -213,24 +217,72 @@ describe('QueryBuilder', () => { yield { type: 'result', content: 'done' }; }); + claude().withMCP({ command: 'server1' }).withMCP({ command: 'server2' }).query('test'); + + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + mcpServers: [{ command: 'server1' }, { command: 'server2' }] + }) + ); + }); + }); + + describe('Add Directories', () => { + it('should add a single directory as string', () => { + mockQuery.mockImplementation(async function* () { + yield { type: 'result', content: 'done' }; + }); + + claude().addDirectory('/path/to/dir').query('test'); + + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + addDirectories: ['/path/to/dir'] + }) + ); + }); + + it('should add multiple directories as array', () => { + mockQuery.mockImplementation(async function* () { + yield { type: 'result', content: 'done' }; + }); + + claude().addDirectory(['/path/to/dir1', '/path/to/dir2']).query('test'); + + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + addDirectories: ['/path/to/dir1', '/path/to/dir2'] + }) + ); + }); + + it('should accumulate directories from multiple calls', () => { + mockQuery.mockImplementation(async function* () { + yield { type: 'result', content: 'done' }; + }); + claude() - .withMCP({ command: 'server1' }) - .withMCP({ command: 'server2' }) + .addDirectory('/first/dir') + .addDirectory(['/second/dir', '/third/dir']) + .addDirectory('/fourth/dir') .query('test'); - expect(mockQuery).toHaveBeenCalledWith('test', expect.objectContaining({ - mcpServers: [ - { command: 'server1' }, - { command: 'server2' } - ] - })); + expect(mockQuery).toHaveBeenCalledWith( + 'test', + expect.objectContaining({ + addDirectories: ['/first/dir', '/second/dir', '/third/dir', '/fourth/dir'] + }) + ); }); }); describe('Event Handlers', () => { it('should call message handlers', async () => { const handler = vi.fn(); - + mockQuery.mockImplementation(async function* () { yield { type: 'user', content: 'Hello' }; yield { type: 'assistant', content: [{ type: 'text', text: 'Hi!' }] }; @@ -250,7 +302,7 @@ describe('QueryBuilder', () => { it('should call assistant handlers', async () => { const handler = vi.fn(); - + mockQuery.mockImplementation(async function* () { yield { type: 'user', content: 'Hello' }; yield { type: 'assistant', content: [{ type: 'text', text: 'Hi!' }] }; @@ -267,13 +319,18 @@ describe('QueryBuilder', () => { it('should call tool use handlers', async () => { const handler = vi.fn(); - + mockQuery.mockImplementation(async function* () { yield { type: 'assistant', content: [ { type: 'text', text: 'Let me read that file' }, - { type: 'tool_use', id: '1', name: 'Read', input: { path: 'test.txt' } } + { + type: 'tool_use', + id: '1', + name: 'Read', + input: { path: 'test.txt' } + } ] }; }); @@ -295,15 +352,12 @@ describe('QueryBuilder', () => { }); const logger = new ConsoleLogger(); const logSpy = vi.spyOn(logger, 'error'); - + mockQuery.mockImplementation(async function* () { yield { type: 'result', content: 'done' }; }); - for await (const _ of claude() - .withLogger(logger) - .onMessage(errorHandler) - .queryRaw('test')) { + for await (const _ of claude().withLogger(logger).onMessage(errorHandler).queryRaw('test')) { // Process } @@ -316,7 +370,7 @@ describe('QueryBuilder', () => { const logger = new ConsoleLogger(); const infoSpy = vi.spyOn(logger, 'info'); const debugSpy = vi.spyOn(logger, 'debug'); - + mockQuery.mockImplementation(async function* () { yield { type: 'user', content: 'test' }; yield { type: 'result', content: 'done' }; @@ -326,11 +380,18 @@ describe('QueryBuilder', () => { // Process } - expect(infoSpy).toHaveBeenCalledWith('Starting query', expect.objectContaining({ - prompt: 'test prompt' - })); - expect(debugSpy).toHaveBeenCalledWith('Received message', { type: 'user' }); - expect(debugSpy).toHaveBeenCalledWith('Received message', { type: 'result' }); + expect(infoSpy).toHaveBeenCalledWith( + 'Starting query', + expect.objectContaining({ + prompt: 'test prompt' + }) + ); + expect(debugSpy).toHaveBeenCalledWith('Received message', { + type: 'user' + }); + expect(debugSpy).toHaveBeenCalledWith('Received message', { + type: 'result' + }); expect(infoSpy).toHaveBeenCalledWith('Query completed'); }); }); @@ -347,19 +408,18 @@ describe('QueryBuilder', () => { it('should pass handlers to ResponseParser', async () => { const handler = vi.fn(); - + mockQuery.mockImplementation(async function* () { yield { type: 'assistant', content: [{ type: 'text', text: 'Hello' }] }; }); - await claude() - .onMessage(handler) - .query('test') - .asText(); + await claude().onMessage(handler).query('test').asText(); - expect(handler).toHaveBeenCalledWith(expect.objectContaining({ - type: 'assistant' - })); + expect(handler).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'assistant' + }) + ); }); }); -}); \ No newline at end of file +}); diff --git a/tests/subprocess-cli.test.ts b/tests/subprocess-cli.test.ts index c91a51e..51b42c2 100644 --- a/tests/subprocess-cli.test.ts +++ b/tests/subprocess-cli.test.ts @@ -18,17 +18,17 @@ describe('SubprocessCLITransport', () => { stdoutStream = new Readable({ read() {} }); - + const stdinStream = new Readable({ read() {} }); (stdinStream as any).write = vi.fn(); (stdinStream as any).end = vi.fn(); - + mockProcess = { stdout: stdoutStream, stderr: new Readable({ read() {} }), stdin: stdinStream, cancel: vi.fn(), - then: vi.fn((onfulfilled) => { + then: vi.fn(onfulfilled => { // Simulate successful process completion if (onfulfilled) onfulfilled({ exitCode: 0 }); return Promise.resolve({ exitCode: 0 }); @@ -105,10 +105,7 @@ describe('SubprocessCLITransport', () => { deniedTools: ['WebSearch'] as any, permissionMode: 'acceptEdits' as any, context: ['file1.txt', 'file2.txt'], - mcpServers: [ - { command: 'server1', args: ['--port', '3000'] }, - { command: 'server2' } - ] + mcpServers: [{ command: 'server1', args: ['--port', '3000'] }, { command: 'server2' }] }; const transport = new SubprocessCLITransport('test prompt', options); @@ -116,27 +113,74 @@ describe('SubprocessCLITransport', () => { const expectedArgs = [ 'test prompt', - '--output-format', 'json', - '--model', 'claude-3', - '--api-key', 'test-key', - '--base-url', 'https://api.test.com', - '--max-tokens', '1000', - '--temperature', '0.7', - '--timeout', '30000', + '--output-format', + 'json', + '--model', + 'claude-3', + '--api-key', + 'test-key', + '--base-url', + 'https://api.test.com', + '--max-tokens', + '1000', + '--temperature', + '0.7', + '--timeout', + '30000', '--debug', - '--tools', 'Read,Write', - '--allowed-tools', 'Bash', - '--denied-tools', 'WebSearch', - '--permission-mode', 'acceptEdits', - '--context', 'file1.txt', - '--context', 'file2.txt', - '--mcp-server', 'server1 --port 3000', - '--mcp-server', 'server2' + '--tools', + 'Read,Write', + '--allowed-tools', + 'Bash', + '--denied-tools', + 'WebSearch', + '--permission-mode', + 'acceptEdits', + '--context', + 'file1.txt', + '--context', + 'file2.txt', + '--mcp-server', + 'server1 --port 3000', + '--mcp-server', + 'server2' ]; + expect(execa).toHaveBeenCalledWith('/usr/local/bin/claude-code', expectedArgs, expect.any(Object)); + }); + + it('should include --add-dir flag when addDirectories is provided', async () => { + vi.mocked(which as any).mockResolvedValue('/usr/local/bin/claude-code'); + vi.mocked(execa).mockReturnValue(mockProcess as any); + + const options = { + addDirectories: ['/Users/toby/Code/workspace', '/tmp'] + }; + + const transport = new SubprocessCLITransport('test prompt', options); + await transport.connect(); + + expect(execa).toHaveBeenCalledWith( + '/usr/local/bin/claude-code', + expect.arrayContaining(['--add-dir', '/Users/toby/Code/workspace /tmp']), + expect.any(Object) + ); + }); + + it('should handle single directory in addDirectories', async () => { + vi.mocked(which as any).mockResolvedValue('/usr/local/bin/claude-code'); + vi.mocked(execa).mockReturnValue(mockProcess as any); + + const options = { + addDirectories: ['/single/directory'] + }; + + const transport = new SubprocessCLITransport('test prompt', options); + await transport.connect(); + expect(execa).toHaveBeenCalledWith( '/usr/local/bin/claude-code', - expectedArgs, + expect.arrayContaining(['--add-dir', '/single/directory']), expect.any(Object) ); }); @@ -183,7 +227,10 @@ describe('SubprocessCLITransport', () => { const messages = [ { type: 'message', data: { type: 'user', content: 'Hello' } }, - { type: 'message', data: { type: 'assistant', content: [{ type: 'text', text: 'Hi!' }] } }, + { + type: 'message', + data: { type: 'assistant', content: [{ type: 'text', text: 'Hi!' }] } + }, { type: 'end' } ]; @@ -242,7 +289,12 @@ describe('SubprocessCLITransport', () => { setTimeout(() => { stdoutStream.push('\n'); stdoutStream.push(' \n'); - stdoutStream.push(JSON.stringify({ type: 'message', data: { type: 'user', content: 'Hello' } }) + '\n'); + stdoutStream.push( + JSON.stringify({ + type: 'message', + data: { type: 'user', content: 'Hello' } + }) + '\n' + ); stdoutStream.push('\n'); stdoutStream.push(JSON.stringify({ type: 'end' }) + '\n'); stdoutStream.push(null); @@ -279,4 +331,4 @@ describe('SubprocessCLITransport', () => { await expect(transport.disconnect()).resolves.not.toThrow(); }); }); -}); \ No newline at end of file +});