Unofficial TypeScript SDK for Claude Code - the powerful CLI tool for interacting with Claude.
β¨ What's New in v0.4.0:
- π¬ Interactive streaming session with working visual typewriter effects
- π‘οΈ Typed error handling you catch with
instanceofβ no error categories, no wrappers - β±οΈ Timeouts and edit-acceptance that actually take effect β
withTimeout()throws a realTimeoutError,acceptEdits()maps to the CLI's--permission-mode - π§ Production-ready examples that actually work as advertised
Note: For the classic async generator API, see Classic API Documentation.
npm install @instantlyeasy/claude-code-sdk-ts
# or
yarn add @instantlyeasy/claude-code-sdk-ts
# or
pnpm add @instantlyeasy/claude-code-sdk-tsLatest Version: v0.4.0 with enhanced features and working visual streaming!
Prerequisites:
- Node.js 20 or later
- Claude Code CLI installed (
npm install -g @anthropic-ai/claude-code)
import { claude } from '@instantlyeasy/claude-code-sdk-ts';
// Simple query
const response = await claude()
.query('Say "Hello World!"')
.asText();
console.log(response); // "Hello World!"This SDK delegates all authentication to the Claude CLI:
# One-time setup - login with your Claude account
claude loginThe SDK does not handle authentication directly and has no apiKey option. The CLI owns auth β either via claude login or an ANTHROPIC_API_KEY that the CLI reads from the environment. If you see authentication errors, authenticate using the Claude CLI first.
Chain methods for clean, readable code:
const result = await claude()
.withModel('sonnet') // Choose model
.allowTools('Read', 'Write') // Configure permissions
.acceptEdits() // Auto-accept file edits (--permission-mode acceptEdits)
.inDirectory('/path/to/project') // Set working directory
.query('Refactor this code') // Your prompt
.asText(); // Get response as text
acceptEdits()andskipPermissions()(full bypass) both map to the CLI's--permission-modeand take effect on the run.
Extract exactly what you need:
// Get plain text
const text = await claude()
.query('Explain this concept')
.asText();
// Parse JSON response
const data = await claude()
.query('Return a JSON array of files')
.asJSON<string[]>();
// Get the final result
const result = await claude()
.query('Complete this task')
.asResult();
// Analyze tool usage
const tools = await claude()
.allowTools('Read', 'Grep')
.query('Find all TODO comments')
.asToolExecutions();
for (const execution of tools) {
console.log(`${execution.tool}: ${execution.isError ? 'Failed' : 'Success'}`);
}Fine-grained control over Claude's capabilities:
// Allow specific tools
await claude()
.allowTools('Read', 'Grep', 'LS')
.query('Analyze this codebase')
.asText();
// Deny dangerous tools
await claude()
.denyTools('Bash', 'Write')
.query('Review this code')
.asText();
// Read-only mode
await claude()
.allowTools() // No arguments = deny the mutating tools (Write, Edit, Bash, ...), keep Read/Grep/Glob
.query('Explain this architecture')
.asText();Maintain conversation context across queries. Get the session ID from the first real query's parser, then resume it on a new builder with withSessionId():
// First query β keep the parser so we can read its session ID
const firstQuery = claude()
.withModel('sonnet')
.skipPermissions()
.query('Pick a random number between 1 and 100');
const response1 = await firstQuery.asText();
const sessionId = await firstQuery.getSessionId(); // resolved from the CLI's init message
// Resume that same conversation on a new builder
const response2 = await claude()
.withModel('sonnet')
.skipPermissions()
.withSessionId(sessionId)
.query('What number did you pick?')
.asText();
// Claude remembers the number!Don't call
query('')just to obtain a session ID β an empty prompt starts a brand-new session, so the follow-up would have nothing to remember.
Cancel long-running operations:
import { claude, AbortError } from '@instantlyeasy/claude-code-sdk-ts';
const controller = new AbortController();
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
const response = await claude()
.withSignal(controller.signal)
.query('Long running task')
.asText();
} catch (error) {
if (error instanceof AbortError) {
console.log('Query was cancelled');
}
}Built-in logging with multiple implementations:
import { ConsoleLogger, LogLevel } from '@instantlyeasy/claude-code-sdk-ts';
const logger = new ConsoleLogger(LogLevel.DEBUG);
const response = await claude()
.withLogger(logger)
.query('Debug this issue')
.asText();
// Also available: JSONLogger, MultiLogger, NullLoggerReact to events during execution:
await claude()
.onMessage(msg => console.log('Message:', msg.type))
.onAssistant(content => console.log('Claude sent', content.length, 'block(s)')) // content is ContentBlock[]
.onToolUse(tool => console.log(`Using ${tool.name}...`)) // tool is { name, input }
.query('Perform analysis')
.stream(async (message) => {
// Handle streaming messages
});The SDK automatically loads safe configuration from environment. The variables are namespaced so unrelated tooling can't flip them on by accident:
CLAUDE_SDK_DEBUG- Enable debug mode (values:true,1,yes,on)CLAUDE_SDK_VERBOSE- Enable verbose outputCLAUDE_SDK_LOG_LEVEL- Set log level (0-4)NODE_ENV- Node environment
The generic
DEBUG/VERBOSE/LOG_LEVELvariables are intentionally not read β they're set ubiquitously by CI and other tools. Use theCLAUDE_SDK_variants.
ANTHROPIC_API_KEY and has no apiKey option β authentication belongs to the Claude CLI (see above). See Environment Variables Documentation.
The SDK throws typed error classes. Catch the ones you care about with instanceof β each carries fields specific to what went wrong:
import {
claude,
RateLimitError,
AuthenticationError,
TimeoutError,
AbortError,
ProcessError
} from '@instantlyeasy/claude-code-sdk-ts';
try {
await claude().query('Task').asText();
} catch (error) {
if (error instanceof RateLimitError) {
console.error(`Rate limited β retry after ${error.retryAfter}s`);
} else if (error instanceof AuthenticationError) {
console.error('Not authenticated. Run `claude login` and try again.');
} else if (error instanceof TimeoutError) {
console.error('The query timed out.');
} else if (error instanceof AbortError) {
console.error('The query was cancelled.');
} else if (error instanceof ProcessError) {
console.error(`Claude CLI exited with code ${error.exitCode}`);
} else {
throw error; // Unknown error β re-throw
}
}The classes fall into two families: the API/enhanced errors (RateLimitError, AuthenticationError, TimeoutError, NetworkError, ValidationError, ToolPermissionError) extend BaseSDKError, while the process/CLI errors (ProcessError, AbortError, CLINotFoundError, CLIConnectionError) extend ClaudeSDKError. Both ultimately extend the built-in Error, so a bare catch always works as a fallback.
Load settings and define reusable roles from YAML or JSON.
withConfigFile()andwithRolesFile()are async β they read and parse a file, so each returns aPromise.awaitthe call on its own line before you chain the synchronous builder methods; you can't chain straight off the promise.
// withConfigFile applies MCP servers, global settings, and tool permissions
const builder = claude();
await builder.withConfigFile('./config/claude.yaml');
const result = await builder
.query('Generate component')
.asText();Roles live in their own file (loaded with withRolesFile()) and provide reusable configurations with:
- Model preference
- Tool permissions (
allowed/denied) and a permissionmode - A prompting template with
${variable}substitution - A system prompt (delivered to the CLI via
--append-system-prompt) - Inheritance via
extends
Example roles file:
version: "1.0"
# Define reusable roles
roles:
developer:
model: sonnet
permissions:
mode: default
tools:
allowed: [Read, Write, Edit]
denied: [Bash]
promptingTemplate: |
You are an expert ${language} developer using ${framework}.
senior-developer:
extends: developer # Inherit from the developer role
model: opus
permissions:
mode: acceptEdits
tools:
allowed: [TodoWrite] # Additional tools
systemPrompt: |
Prioritize performance, readability, and test coverage.// Load the roles file (async), then apply a role by name with template variables
const roleBuilder = claude();
await roleBuilder.withRolesFile('./roles.yaml');
const response = await roleBuilder
.withRole('senior-developer', {
language: 'TypeScript',
framework: 'Next.js'
})
.query('Optimize this React component')
.asText();See Roles Documentation for complete details.
// query() already returns the ResponseParser β just hold on to it
const parser = claude()
.query('Complex task');
const usage = await parser.getUsage();
console.log('Tokens:', usage.totalTokens);
console.log('Cost: $', usage.totalCost);await claude()
.query('Tell me a story')
.stream(async (message) => {
if (message.type === 'assistant') {
// Stream complete messages (not individual tokens)
console.log(message.content[0].text);
}
});// withTimeout enforces a real deadline β it throws TimeoutError if the run exceeds it
const response = await claude()
.withModel('claude-3-opus-20240229')
.withTimeout(30000)
.query('Complex analysis')
.asText();Create typewriter effects and real-time response display:
import { claude, createTokenStream } from '@instantlyeasy/claude-code-sdk-ts';
// Collect response for controlled display
const messageGenerator = claude()
.withModel('sonnet')
.queryRaw('Write a story about AI');
const tokenStream = createTokenStream(messageGenerator);
const allTokens = [];
for await (const chunk of tokenStream.tokens()) {
allTokens.push(chunk.token);
}
// Display with typewriter effect
const fullText = allTokens.join('');
for (const char of fullText) {
process.stdout.write(char);
await new Promise(resolve => setTimeout(resolve, 30));
}Handle specific error types with smart retry logic:
import { claude, detectErrorType, withRetry } from '@instantlyeasy/claude-code-sdk-ts';
// withRetry(fn, options) returns a WRAPPER function β it does not run fn itself.
// Call the wrapper to execute with retries.
const run = withRetry(
() => claude().query('Complex task').asText(),
{
maxAttempts: 3,
shouldRetry: (error) => {
const errorType = detectErrorType(error.message);
return ['network_error', 'timeout_error'].includes(errorType);
}
}
);
try {
const result = await run(); // or, as one expression: await withRetry(fn, options)()
} catch (error) {
const errorType = detectErrorType(error.message);
console.log(`Failed with error type: ${errorType}`);
}NEW! Complete chat interface with visual streaming:
# Try the interactive streaming example
node examples/fluent-api/new-features/interactive-streaming.jsFeatures working character-by-character display, conversation history, speed control, and model switching!
Comprehensive examples are available in the examples directory:
- fluent-api-demo.js - Complete fluent API showcase
- sessions.js - Session management patterns
- yaml-config-demo.js - Configuration examples
Advanced Features (new-features directory)
- interactive-streaming.js - π¬ Interactive chat with visual streaming
- token-streaming.js - Working typewriter effects
- error-handling.js - Advanced error patterns
- retry-strategies.js - Multiple retry strategies
- File Operations - Reading, writing, and analyzing code
- Web Research - Using Claude's web capabilities
- Interactive Sessions - Building conversational interfaces
The SDK maintains full backward compatibility. The classic query() function still works:
import { query } from '@instantlyeasy/claude-code-sdk-ts';
for await (const message of query('Hello')) {
// Classic async generator API
}However, we recommend the fluent API for new projects. See Migration Guide.
Creates a new query builder:
claude()
.withModel(model: string)
.allowTools(...tools: ToolName[]) // no args = read-only (denies mutating tools)
.denyTools(...tools: ToolName[])
.skipPermissions() // full bypass (--permission-mode bypassPermissions)
.acceptEdits() // auto-accept edits (--permission-mode acceptEdits)
.withTimeout(ms: number) // throws TimeoutError on expiry
.inDirectory(path: string)
.withSessionId(id: string)
.withSignal(signal: AbortSignal)
.withLogger(logger: Logger)
.withConfigFile(path: string): Promise<this> // async β await before chaining
.withRolesFile(path: string): Promise<this> // async β await before chaining
.withRole(roleName: string) // overload 1: apply a loaded role by name
.withRole(role: RoleDefinition, vars?: Record<string, string>) // overload 2: inline definition
.onMessage(handler: (msg: Message) => void)
.onAssistant(handler: (content: ContentBlock[]) => void)
.onToolUse(handler: (tool: { name: string; input: Record<string, unknown> }) => void)
.query(prompt: string): ResponseParserasText()- Extract plain textasJSON<T>()- Parse JSON responseasResult()- Get the final result messageasArray()- Get every message as an arrayasToolExecutions()- Get tool execution detailsfindToolResults(name)- Find all results for a toolfindToolResult(name)- Get the first result for a toolgetUsage()- Get token usage statsgetSessionId()- Get the session ID (resolved from the init message)stream(callback)- Stream messagessucceeded()- Whether the run finished without errorsgetErrors()- Collect run/tool error messagestransform(fn)- Apply a custom transformer to the messages
See TypeScript definitions for complete type information.
See CHANGELOG.md for version history.
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
MIT Β© Daniel King & Claude