claude-code-bridge-sdk
v0.0.1
Published
Ergonomic TypeScript SDK for building apps on top of Claude Code. Sessions, pools, AskUserQuestion bridge, and on-disk session inspection over @anthropic-ai/claude-agent-sdk.
Maintainers
Readme
claude-code-bridge-sdk
A thin, ergonomic TypeScript SDK for building apps on top of Claude Code.
Wraps the official @anthropic-ai/claude-agent-sdk and adds the missing pieces:
- a stateful
Sessionwith.send(),.invoke(),.kill(),.fork(),.setModel(),.commands(),.models() - a
Poolfor orchestrating N concurrent Claude Code instances withmap,race,broadcast,pipeline, and unified events - a
RunHandlethat is bothawait-able andfor await-able, with.sessionIdexposed onsystem/init(before the first token) - a first-class
onAskUserbridge for Claude Code'sAskUserQuestiontool (1–4 questions × 2–4 options × optional multiSelect) commands()/models()/invoke()for slash-command + skill discovery and invocationinspect()/list()to snapshot any session's state from disk — across process boundaries
Zero translation of CLI args. Storage-agnostic. Re-exports upstream types verbatim.
Status
Pre-release. Targets @anthropic-ai/claude-agent-sdk ^0.3.0.
Install
pnpm add claude-code-bridge-sdk
# or
npm install claude-code-bridge-sdkThe upstream @anthropic-ai/claude-agent-sdk is a hard dependency and is installed automatically. The claude CLI itself is not bundled — install it globally for full functionality:
npm install -g @anthropic-ai/claude-codeESM-only
This package is ESM-only by design. It only ships dist/index.js (ESM) and dist/index.d.ts; no CJS bundle is emitted. Upstream @anthropic-ai/claude-agent-sdk is itself ESM-only, so a CJS facade here would only mask the real failure mode (ERR_REQUIRE_ESM on first require of upstream).
Use it from:
- Node ≥ 18.17 with
"type": "module"in yourpackage.json, or - a
.mjsfile, or - TypeScript with
"module": "NodeNext"/"Bundler", or - any bundler (Vite, Webpack, Rollup, esbuild, tsdown, etc.).
If you need to consume this from CJS, dynamically import:
// in a CommonJS file
async function main() {
const { claude } = await import('claude-code-bridge-sdk');
// …
}Quickstart
import { claude } from 'claude-code-bridge-sdk';
// One-shot, streaming
const r = claude.run({ prompt: 'Write a haiku about TypeScript' });
for await (const chunk of r.text()) process.stdout.write(chunk);
const final = await r;
console.log(final.sessionId, final.costUsd);Why this exists vs. raw @anthropic-ai/claude-agent-sdk
| Need | Raw agent-sdk | claude-code-bridge-sdk |
|------|---------------|-----------------------------|
| Stream + collect a final result on one handle | iterate manually, accumulate yourself | await AND for await on the same object |
| Get sessionId before completion | walk messages, watch for system/init | await r.sessionId |
| Run many agents with a concurrency cap | hand-roll a semaphore | pool({ concurrency: N }) |
| Kill all in-flight agents | track them yourself | pool.kill('all') |
| Multi-turn session with .send() ergonomics | feed an AsyncIterable manually | session(...).send(prompt) |
| Fork / resume a session | manually pass forkSession + resume per call | s.fork() / claude.session({ resume }) |
| Handle Claude's AskUserQuestion tool | register a hook + format the output yourself | onAskUser: ({questions}) => … |
| List + invoke discoverable commands & skills | call supportedCommands() + format /name args | s.commands() / s.invoke('name', 'args') |
| Check if a session is still running across processes | parse ~/.claude/projects/... JSONLs yourself | claude.inspect(sessionId) / claude.list({ cwd }) |
API surface
import { claude, run, session, pool, inspect, list } from 'claude-code-bridge-sdk';Everything is re-exported as named exports (tree-shake friendly) and on the claude namespace (discoverable).
run() — one-shot, awaitable + iterable
const r = claude.run({
prompt: 'Refactor auth.ts',
model: 'claude-sonnet-4-6',
allowedTools: ['Read', 'Edit'],
permissionMode: 'acceptEdits',
onAskUser: async ({ questions }) => ({
answers: { [questions[0]!.question]: questions[0]!.options[0]!.label },
}),
});
const sid = await r.sessionId; // resolves on system/init
for await (const chunk of r.text()) process.stdout.write(chunk);
const final = await r; // FinalResultsession() — stateful, multi-turn
const s = claude.session({ model: 'claude-opus-4-7', permissionMode: 'plan' });
const sessionId = await s.sessionId;
await s.send('remember the number 42').result;
await s.send('what number?').result; // recalls "42"
const branch = s.fork(); // new sessionId, branched history
await s.kill();pool() — N concurrent agents
const p = claude.pool({ concurrency: 4, defaults: { model: 'claude-haiku-4-5' } });
// fan-out
const out = await p.map(files, (f) => ({ prompt: `Document ${f}` }));
// race
const winner = await p.race([
{ prompt: 'solve', model: 'claude-opus-4-7' },
{ prompt: 'solve', model: 'claude-sonnet-4-6' },
]);
// unified observability
for await (const ev of p.events()) {
if (ev.type === 'error') console.error(ev);
}
await p.kill('all');inspect() / list() — read-only session snapshots
const snap = await claude.inspect(sessionId, { cwd: '/repo' });
console.log(snap.derivedStatus); // 'active' | 'completed' | 'interrupted' | 'unknown'
console.log(snap.numTurns, snap.totalCostUsd);
const all = await claude.list({ cwd: '/repo' });
// sorted by most recent activity first
appearsActiveis a heuristic based onmtime+ absence of aresultmessage. Pair with your own job registry for authoritative liveness.
How the AskUserQuestion bridge actually works
Claude Code's built-in AskUserQuestion tool has no TTY to render a picker in headless SDK mode, and PostToolUse does not fire for it. The bridge intercepts at PreToolUse instead:
- The model emits a
tool_useforAskUserQuestionwith the questions. - Our PreToolUse hook calls your
onAskUserhandler with the unmarshalled questions and awaits the response. - The hook returns
permissionDecision: 'deny'with the answers JSON-encoded inpermissionDecisionReason. The model receives the JSON as the tool result.
You'll see a "denied tool call" entry in the transcript for each AskUserQuestion. That's intentional — it's the only fire-able interception point in v0.1.
Errors
All errors thrown by the SDK extend ClaudeError and carry a stable string code:
import { ClaudeError, KilledError, BudgetExceededError } from 'claude-code-bridge-sdk';
try {
await claude.run({ prompt: 'long task' });
} catch (e) {
if (e instanceof KilledError) /* user clicked stop */;
if (e instanceof BudgetExceededError) console.log(e.costUsd, e.limitUsd);
}Codes: SESSION_NOT_FOUND, PERMISSION_DENIED, MAX_TURNS_EXCEEDED, BUDGET_EXCEEDED, INTERRUPTED, KILLED, CLI_ERROR, TIMEOUT.
Compatibility
| claude-code-bridge-sdk | @anthropic-ai/claude-agent-sdk |
|----------------------------|-----------------------------------|
| 0.0.x | ^0.3.0 |
If the installed upstream is outside the tested range, the SDK logs a one-time console.warn on first use. It does not throw.
Development
This is a Turborepo + pnpm monorepo.
pnpm install
pnpm build
pnpm test # unit + types
pnpm test:e2e # requires `claude` on PATH + ANTHROPIC_API_KEYExamples live in ./examples/. Each is a workspace consumer of the SDK.
Upstream attribution
This package wraps @anthropic-ai/claude-agent-sdk, which is © Anthropic PBC and governed by Anthropic's Legal Agreements. Using claude-code-bridge-sdk requires that you also accept those terms — the MIT license on this wrapper applies only to the wrapper's source code.
License
MIT — see LICENSE. Wrapper code only; upstream Anthropic packages retain their own terms.
