Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 52 additions & 49 deletions docs/FLUENT_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -188,9 +193,14 @@ class CustomLogger implements Logger {

// Implement convenience methods
error(message: string, context?: Record<string, any>): 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
}
```
Expand All @@ -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)));
Expand All @@ -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;
}
```
Expand All @@ -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;
}
Expand All @@ -283,14 +288,15 @@ import { claude } from '@instantlyeasy/claude-code-sdk-ts';
await claude()
.withModel('sonnet')
.query('Hello')
.stream(async (message) => {
.stream(async message => {
// Process messages
});
```

### Common Migration Patterns

1. **Simple text extraction**:

```typescript
// Before
let text = '';
Expand All @@ -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 = [];
Expand All @@ -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 {
Expand All @@ -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.
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.
50 changes: 21 additions & 29 deletions src/_internal/transport/subprocess-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ export class SubprocessCLITransport {

private async findCLI(): Promise<string> {
// 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);
Expand All @@ -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');
Expand Down Expand Up @@ -70,18 +67,15 @@ 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
);
}

// Try global npm/yarn paths
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
Expand All @@ -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();
Expand All @@ -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

Expand All @@ -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');

Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand All @@ -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;
}
Expand All @@ -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);
}
}
}
Expand All @@ -242,4 +234,4 @@ export class SubprocessCLITransport {
this.process = undefined;
}
}
}
}
Loading