The Claude Code SDK now includes a powerful fluent API that makes it easier to build and execute queries with a chainable interface.
- Getting Started
- Query Builder
- Response Parser
- Session Management
- Logging Framework
- Advanced Patterns
- Migration Guide
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();The QueryBuilder class provides chainable methods for configuring your query:
claude()
.withModel('opus') // or 'sonnet'
.withTimeout(60000) // 60 seconds
.debug(true) // Enable debug modeclaude()
.allowTools('Read', 'Write', 'Edit') // Explicitly allow tools
.denyTools('Bash', 'WebSearch') // Explicitly deny tools
.allowTools() // No arguments = read-only mode (denies all tools)claude()
.skipPermissions() // Bypass all permission prompts
.acceptEdits() // Auto-accept file edits
.withPermissions('default') // Use default permission handlingclaude()
.inDirectory('/path/to/project')
.withEnv({ NODE_ENV: 'production' })claude()
.addDirectory('/path/to/dir') // Add single directory
.addDirectory(['../apps', '../lib']) // Add multiple directories
.addDirectory('/another/dir') // Accumulate with multiple callsThe addDirectory method allows you to add additional working directories for Claude to access:
- Single directory: Pass a string path
- Multiple directories: Pass an array of string paths
- Accumulative: Multiple calls to
addDirectorywill accumulate all directories - CLI mapping: Generates
--add-dirflag with space-separated paths
claude()
.withMCP(
{ command: 'mcp-server-filesystem', args: ['--readonly'] },
{ command: 'mcp-server-git' }
)claude()
.onMessage(msg => console.log('Message:', msg.type))
.onAssistant(content => console.log('Assistant says...'))
.onToolUse(tool => console.log(`Using ${tool.name}`))The ResponseParser provides multiple ways to extract data from Claude's responses:
const parser = claude().query('Your prompt');
// Get raw text
const text = await parser.asText();
// Get final result
const result = await parser.asResult();
// Get all messages
const messages = await parser.asArray();// Extract JSON from response
const data = await parser.asJSON<MyInterface>();
// Get tool execution details
const executions = await parser.asToolExecutions();
// Find specific tool results
const fileContents = await parser.findToolResults('Read');
const firstFile = await parser.findToolResult('Read');const usage = await parser.getUsage();
console.log(`Tokens: ${usage.totalTokens}`);
console.log(`Cost: $${usage.totalCost}`);Important Note: This SDK streams complete messages, not individual tokens. Each assistant message contains the full text block, not incremental updates.
await parser.stream(async (message) => {
if (message.type === 'assistant') {
// Each message contains complete text, not token-by-token updates
console.log(message.content[0].text); // Full text block
}
});import { claude, AbortError } from '@instantlyeasy/claude-code-sdk-ts';
const controller = new AbortController();
const parser = claude()
.withSignal(controller.signal)
.query('Long running query');
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
await parser.stream(async (message) => {
// Process messages
});
} catch (error) {
if (error instanceof AbortError) {
console.log('Query was cancelled');
} else {
throw error;
}
}Note: Due to how Node.js handles child process aborts, you may see "AbortError" warnings in the console. These can be safely ignored. The SDK properly throws AbortError instances that you can catch.
const success = await parser.succeeded();
const errors = await parser.getErrors();const customData = await parser.transform(messages => {
// Your custom logic
return processMessages(messages);
});The SDK provides built-in support for maintaining conversation context across multiple queries using session IDs:
// First query - start a new session
const parser = claude()
.withModel('sonnet')
.query('Remember this number: 42');
// Get the session ID for later use
const sessionId = await parser.getSessionId();
const firstResponse = await parser.asText();
// Continue the conversation in the same session
const followUp = await claude()
.withModel('sonnet')
.withSessionId(sessionId)
.query('What number did I ask you to remember?')
.asText();
console.log(followUp); // Claude will remember "42"async function interactiveSession() {
const builder = claude()
.withModel('opus')
.skipPermissions();
// Initial context setup
const parser = builder.query('You are helping me refactor a codebase. Start by analyzing the current structure.');
const sessionId = await parser.getSessionId();
await parser.asText();
// Continue with multiple related tasks
const analysis = await builder
.withSessionId(sessionId)
.query('What are the main issues you found?')
.asText();
const plan = await builder
.withSessionId(sessionId)
.query('Create a refactoring plan to address these issues')
.asText();
const implementation = await builder
.withSessionId(sessionId)
.allowTools('Read', 'Write', 'Edit')
.query('Implement the first step of the refactoring plan')
.asResult();
return { sessionId, analysis, plan, implementation };
}- Store session IDs: Save session IDs if you need to resume conversations later
- Context preservation: Sessions maintain full conversation history
- Tool state: File changes and tool usage are preserved within a session
- Session expiration: Sessions may expire after a period of inactivity
The SDK includes a pluggable logging system:
import { ConsoleLogger, JSONLogger, LogLevel } from '@instantlyeasy/claude-code-sdk-ts';
// Console logger with custom prefix
const logger = new ConsoleLogger(LogLevel.DEBUG, '[MyApp]');
// JSON logger for structured logging
const jsonLogger = new JSONLogger(LogLevel.INFO);
// Use with QueryBuilder
claude()
.withLogger(logger)
.query('...');import { Logger, LogEntry } from '@instantlyeasy/claude-code-sdk-ts';
class CustomLogger implements Logger {
log(entry: LogEntry): void {
// Send to your logging service
myLoggingService.send({
level: LogLevel[entry.level],
message: entry.message,
timestamp: entry.timestamp,
...entry.context
});
}
// Implement convenience methods
error(message: string, context?: Record<string, any>): void {
this.log({ level: LogLevel.ERROR, message, timestamp: new Date(), context });
}
// ... implement warn, info, debug, trace
}import { MultiLogger, ConsoleLogger, JSONLogger } from '@instantlyeasy/claude-code-sdk-ts';
const multiLogger = new MultiLogger([
new ConsoleLogger(LogLevel.INFO),
new JSONLogger(LogLevel.DEBUG, line => fs.appendFileSync('app.log', line + '\n'))
]);async function queryWithRetry(prompt: string, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
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)));
}
}
}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;
}const cache = new Map();
async function cachedQuery(prompt: string) {
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();
cache.set(cacheKey, result);
return result;
}The original API still works and is fully supported:
// Original API
import { query } from '@instantlyeasy/claude-code-sdk-ts';
for await (const message of query('Hello', { model: 'sonnet' })) {
// Process messages
}
// Fluent API equivalent
import { claude } from '@instantlyeasy/claude-code-sdk-ts';
await claude()
.withModel('sonnet')
.query('Hello')
.stream(async (message) => {
// Process messages
});- Simple text extraction:
// Before
let text = '';
for await (const message of query('Generate text')) {
if (message.type === 'assistant') {
for (const block of message.content) {
if (block.type === 'text') {
text += block.text;
}
}
}
}
// After
const text = await claude()
.query('Generate text')
.asText();- Tool result extraction:
// Before
const results = [];
for await (const message of query('Read files', { allowedTools: ['Read'] })) {
if (message.type === 'assistant') {
for (const block of message.content) {
if (block.type === 'tool_result' && !block.is_error) {
results.push(block.content);
}
}
}
}
// After
const results = await claude()
.allowTools('Read')
.query('Read files')
.findToolResults('Read');- Error handling:
// Before
try {
for await (const message of query('Do something')) {
// Process and check for errors manually
}
} catch (error) {
console.error('Failed:', error);
}
// After
const parser = claude().query('Do something');
const success = await parser.succeeded();
if (!success) {
const errors = await parser.getErrors();
console.error('Failed:', errors);
}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.