Skip to content

Commit fae8d22

Browse files
committed
Add fluent API addDirectory option and classic config option
1 parent 2e2bdc0 commit fae8d22

6 files changed

Lines changed: 345 additions & 229 deletions

File tree

docs/FLUENT_API.md

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
The Claude Code SDK now includes a powerful fluent API that makes it easier to build and execute queries with a chainable interface.
44

55
## Table of Contents
6+
67
- [Getting Started](#getting-started)
78
- [Query Builder](#query-builder)
89
- [Response Parser](#response-parser)
@@ -18,11 +19,7 @@ The fluent API provides a more intuitive way to interact with Claude Code:
1819
import { claude } from '@instantlyeasy/claude-code-sdk-ts';
1920

2021
// Simple example
21-
const response = await claude()
22-
.withModel('sonnet')
23-
.skipPermissions()
24-
.query('Hello, Claude!')
25-
.asText();
22+
const response = await claude().withModel('sonnet').skipPermissions().query('Hello, Claude!').asText();
2623
```
2724

2825
## Query Builder
@@ -33,44 +30,54 @@ The `QueryBuilder` class provides chainable methods for configuring your query:
3330

3431
```typescript
3532
claude()
36-
.withModel('opus') // or 'sonnet', 'haiku'
37-
.withTimeout(60000) // 60 seconds
38-
.debug(true) // Enable debug mode
33+
.withModel('opus') // or 'sonnet', 'haiku'
34+
.withTimeout(60000) // 60 seconds
35+
.debug(true); // Enable debug mode
3936
```
4037

4138
### Tool Management
4239

4340
```typescript
4441
claude()
45-
.allowTools('Read', 'Write', 'Edit') // Explicitly allow tools
46-
.denyTools('Bash', 'WebSearch') // Explicitly deny tools
42+
.allowTools('Read', 'Write', 'Edit') // Explicitly allow tools
43+
.denyTools('Bash', 'WebSearch'); // Explicitly deny tools
4744
```
4845

4946
### Permissions
5047

5148
```typescript
5249
claude()
53-
.skipPermissions() // Bypass all permission prompts
54-
.acceptEdits() // Auto-accept file edits
55-
.withPermissions('default') // Use default permission handling
50+
.skipPermissions() // Bypass all permission prompts
51+
.acceptEdits() // Auto-accept file edits
52+
.withPermissions('default'); // Use default permission handling
5653
```
5754

5855
### Environment Configuration
5956

57+
```typescript
58+
claude().inDirectory('/path/to/project').withEnv({ NODE_ENV: 'production' });
59+
```
60+
61+
### Directory Context
62+
6063
```typescript
6164
claude()
62-
.inDirectory('/path/to/project')
63-
.withEnv({ NODE_ENV: 'production' })
65+
.addDirectory('/path/to/dir') // Add single directory
66+
.addDirectory(['../apps', '../lib']) // Add multiple directories
67+
.addDirectory('/another/dir'); // Accumulate with multiple calls
6468
```
6569

70+
The `addDirectory` method allows you to add additional working directories for Claude to access (validates each path exists as a directory).
71+
72+
- **Single directory**: Pass a string path
73+
- **Multiple directories**: Pass an array of string paths
74+
- **Accumulative**: Multiple calls to `addDirectory` will accumulate all directories
75+
- **CLI mapping**: Generates `--add-dir` flag with space-separated paths
76+
6677
### MCP Servers
6778

6879
```typescript
69-
claude()
70-
.withMCP(
71-
{ command: 'mcp-server-filesystem', args: ['--readonly'] },
72-
{ command: 'mcp-server-git' }
73-
)
80+
claude().withMCP({ command: 'mcp-server-filesystem', args: ['--readonly'] }, { command: 'mcp-server-git' });
7481
```
7582

7683
### Event Handlers
@@ -79,7 +86,7 @@ claude()
7986
claude()
8087
.onMessage(msg => console.log('Message:', msg.type))
8188
.onAssistant(content => console.log('Assistant says...'))
82-
.onToolUse(tool => console.log(`Using ${tool.name}`))
89+
.onToolUse(tool => console.log(`Using ${tool.name}`));
8390
```
8491

8592
## Response Parser
@@ -126,7 +133,7 @@ console.log(`Cost: $${usage.totalCost}`);
126133
### Streaming
127134

128135
```typescript
129-
await parser.stream(async (message) => {
136+
await parser.stream(async message => {
130137
if (message.type === 'assistant') {
131138
// Handle streaming content
132139
}
@@ -165,9 +172,7 @@ const logger = new ConsoleLogger(LogLevel.DEBUG, '[MyApp]');
165172
const jsonLogger = new JSONLogger(LogLevel.INFO);
166173

167174
// Use with QueryBuilder
168-
claude()
169-
.withLogger(logger)
170-
.query('...');
175+
claude().withLogger(logger).query('...');
171176
```
172177

173178
### Custom Logger Implementation
@@ -188,9 +193,14 @@ class CustomLogger implements Logger {
188193

189194
// Implement convenience methods
190195
error(message: string, context?: Record<string, any>): void {
191-
this.log({ level: LogLevel.ERROR, message, timestamp: new Date(), context });
196+
this.log({
197+
level: LogLevel.ERROR,
198+
message,
199+
timestamp: new Date(),
200+
context
201+
});
192202
}
193-
203+
194204
// ... implement warn, info, debug, trace
195205
}
196206
```
@@ -214,10 +224,7 @@ const multiLogger = new MultiLogger([
214224
async function queryWithRetry(prompt: string, maxRetries = 3) {
215225
for (let i = 0; i < maxRetries; i++) {
216226
try {
217-
return await claude()
218-
.withTimeout(30000)
219-
.query(prompt)
220-
.asText();
227+
return await claude().withTimeout(30000).query(prompt).asText();
221228
} catch (error) {
222229
if (i === maxRetries - 1) throw error;
223230
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
@@ -231,13 +238,13 @@ async function queryWithRetry(prompt: string, maxRetries = 3) {
231238
```typescript
232239
function createQuery(options: { readonly?: boolean }) {
233240
const builder = claude();
234-
241+
235242
if (options.readonly) {
236243
builder.allowTools('Read', 'Grep', 'Glob').denyTools('Write', 'Edit');
237244
} else {
238245
builder.allowTools('Read', 'Write', 'Edit');
239246
}
240-
247+
241248
return builder;
242249
}
243250
```
@@ -248,16 +255,14 @@ function createQuery(options: { readonly?: boolean }) {
248255
const cache = new Map();
249256

250257
async function cachedQuery(prompt: string) {
251-
const cacheKey = `${prompt}:${Date.now() / 60000 | 0}`; // 1-minute cache
252-
258+
const cacheKey = `${prompt}:${(Date.now() / 60000) | 0}`; // 1-minute cache
259+
253260
if (cache.has(cacheKey)) {
254261
return cache.get(cacheKey);
255262
}
256-
257-
const result = await claude()
258-
.query(prompt)
259-
.asText();
260-
263+
264+
const result = await claude().query(prompt).asText();
265+
261266
cache.set(cacheKey, result);
262267
return result;
263268
}
@@ -283,14 +288,15 @@ import { claude } from '@instantlyeasy/claude-code-sdk-ts';
283288
await claude()
284289
.withModel('sonnet')
285290
.query('Hello')
286-
.stream(async (message) => {
291+
.stream(async message => {
287292
// Process messages
288293
});
289294
```
290295

291296
### Common Migration Patterns
292297

293298
1. **Simple text extraction**:
299+
294300
```typescript
295301
// Before
296302
let text = '';
@@ -305,12 +311,11 @@ for await (const message of query('Generate text')) {
305311
}
306312

307313
// After
308-
const text = await claude()
309-
.query('Generate text')
310-
.asText();
314+
const text = await claude().query('Generate text').asText();
311315
```
312316

313317
2. **Tool result extraction**:
318+
314319
```typescript
315320
// Before
316321
const results = [];
@@ -325,13 +330,11 @@ for await (const message of query('Read files', { allowedTools: ['Read'] })) {
325330
}
326331

327332
// After
328-
const results = await claude()
329-
.allowTools('Read')
330-
.query('Read files')
331-
.findToolResults('Read');
333+
const results = await claude().allowTools('Read').query('Read files').findToolResults('Read');
332334
```
333335

334336
3. **Error handling**:
337+
335338
```typescript
336339
// Before
337340
try {
@@ -351,4 +354,4 @@ if (!success) {
351354
}
352355
```
353356

354-
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.
357+
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.

src/_internal/transport/subprocess-cli.ts

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,8 @@ export class SubprocessCLITransport {
2020

2121
private async findCLI(): Promise<string> {
2222
// First check for local Claude installation (newer version with --output-format support)
23-
const localPaths = [
24-
join(homedir(), '.claude', 'local', 'claude'),
25-
join(homedir(), '.claude', 'bin', 'claude')
26-
];
27-
23+
const localPaths = [join(homedir(), '.claude', 'local', 'claude'), join(homedir(), '.claude', 'bin', 'claude')];
24+
2825
for (const path of localPaths) {
2926
try {
3027
await access(path, constants.X_OK);
@@ -33,7 +30,7 @@ export class SubprocessCLITransport {
3330
// Continue checking
3431
}
3532
}
36-
33+
3734
// Then try to find in PATH - try both 'claude' and 'claude-code' for compatibility
3835
try {
3936
return await which('claude');
@@ -70,18 +67,15 @@ export class SubprocessCLITransport {
7067
join(home, '.local', 'bin', 'claude-code'),
7168
join(home, 'bin', 'claude'),
7269
join(home, 'bin', 'claude-code'),
73-
join(home, '.claude', 'local', 'claude') // Claude's custom installation path
70+
join(home, '.claude', 'local', 'claude') // Claude's custom installation path
7471
);
7572
}
7673

7774
// Try global npm/yarn paths
7875
try {
7976
const { stdout: npmPrefix } = await execa('npm', ['config', 'get', 'prefix']);
8077
if (npmPrefix) {
81-
paths.push(
82-
join(npmPrefix.trim(), 'bin', 'claude'),
83-
join(npmPrefix.trim(), 'bin', 'claude-code')
84-
);
78+
paths.push(join(npmPrefix.trim(), 'bin', 'claude'), join(npmPrefix.trim(), 'bin', 'claude-code'));
8579
}
8680
} catch {
8781
// Ignore error and continue
@@ -93,8 +87,8 @@ export class SubprocessCLITransport {
9387
await execa(path, ['--version']);
9488
return path;
9589
} catch {
96-
// Ignore error and continue
97-
}
90+
// Ignore error and continue
91+
}
9892
}
9993

10094
throw new CLINotFoundError();
@@ -107,7 +101,7 @@ export class SubprocessCLITransport {
107101
// Claude CLI supported flags (from --help)
108102
if (this.options.model) args.push('--model', this.options.model);
109103
// Don't pass --debug flag as it produces non-JSON output
110-
104+
111105
// Note: Claude CLI handles authentication internally
112106
// It will use either session auth or API key based on user's setup
113107

@@ -133,6 +127,11 @@ export class SubprocessCLITransport {
133127
args.push('--mcp-config', JSON.stringify(mcpConfig));
134128
}
135129

130+
// Handle add directories
131+
if (this.options.addDirectories && this.options.addDirectories.length > 0) {
132+
args.push('--add-dir', this.options.addDirectories.join(' '));
133+
}
134+
136135
// Add --print flag (prompt will be sent via stdin)
137136
args.push('--print');
138137

@@ -163,7 +162,7 @@ export class SubprocessCLITransport {
163162
stderr: 'pipe',
164163
buffer: false
165164
});
166-
165+
167166
// Send prompt via stdin
168167
if (this.process.stdin) {
169168
this.process.stdin.write(this.prompt);
@@ -185,8 +184,8 @@ export class SubprocessCLITransport {
185184
input: this.process.stderr,
186185
crlfDelay: Infinity
187186
});
188-
189-
stderrRl.on('line', (line) => {
187+
188+
stderrRl.on('line', line => {
190189
if (this.options.debug) {
191190
console.error('DEBUG stderr:', line);
192191
}
@@ -202,21 +201,18 @@ export class SubprocessCLITransport {
202201
for await (const line of rl) {
203202
const trimmedLine = line.trim();
204203
if (!trimmedLine) continue;
205-
204+
206205
if (this.options.debug) {
207206
console.error('DEBUG stdout:', trimmedLine);
208207
}
209-
208+
210209
try {
211210
const parsed = JSON.parse(trimmedLine) as CLIOutput;
212211
yield parsed;
213212
} catch (error) {
214213
// Skip non-JSON lines (like Python SDK does)
215214
if (trimmedLine.startsWith('{') || trimmedLine.startsWith('[')) {
216-
throw new CLIJSONDecodeError(
217-
`Failed to parse CLI output: ${error}`,
218-
trimmedLine
219-
);
215+
throw new CLIJSONDecodeError(`Failed to parse CLI output: ${error}`, trimmedLine);
220216
}
221217
continue;
222218
}
@@ -227,11 +223,7 @@ export class SubprocessCLITransport {
227223
await this.process;
228224
} catch (error: any) {
229225
if (error.exitCode !== 0) {
230-
throw new ProcessError(
231-
`Claude Code CLI exited with code ${error.exitCode}`,
232-
error.exitCode,
233-
error.signal
234-
);
226+
throw new ProcessError(`Claude Code CLI exited with code ${error.exitCode}`, error.exitCode, error.signal);
235227
}
236228
}
237229
}
@@ -242,4 +234,4 @@ export class SubprocessCLITransport {
242234
this.process = undefined;
243235
}
244236
}
245-
}
237+
}

0 commit comments

Comments
 (0)