anyclaude-sdk
v0.11.2
Published
Standalone, browser-compatible SDK providing Claude Code agent capabilities (tools, tool loop, multi-turn, MCP, sub-agents, sessions) against any OpenAI/Anthropic-compatible LLM endpoint. Runs in the browser (WebContainer), Node, and Bun — no backend requ
Maintainers
Readme
anyclaude-sdk
Claude Code agent capabilities — tools, the tool loop, multi-turn conversations, MCP, sub-agents, sessions — against any OpenAI- or Anthropic-compatible LLM endpoint, running in the browser (WebContainer), Node, and Bun. No backend required, no OAuth, no native binaries.
Live demo: a full IDE running in your browser · Docs: anyclaude-docs.puter.site · React UI kit:
anyclaude-react
It exposes the same query() async-generator interface and the same SDKMessage
envelope as @anthropic-ai/claude-agent-sdk, so code written against the official
SDK can iterate our output unchanged.
Install
npm install anyclaude-sdk @webcontainer/api@webcontainer/api is an optional peer dependency — only needed if you use
WebContainerWorkspace. You can supply your own FileSystem/CommandExecutor.
Quick start
import { WebContainer } from '@webcontainer/api'
import {
query,
WebContainerWorkspace,
createOpenAIClient,
ALL_CLAUDE_CODE_TOOLS,
} from 'anyclaude-sdk'
// 1. Boot a WebContainer and wrap it as a workspace.
const wc = await WebContainer.boot()
const workspace = new WebContainerWorkspace(wc)
// 2. Point at any OpenAI-compatible endpoint.
const llm = createOpenAIClient({
apiKey: import.meta.env.VITE_OPENAI_API_KEY,
baseUrl: 'https://api.openai.com/v1', // or Groq, Together, OpenRouter, local…
model: 'gpt-4o',
})
// 3. Run the agent — same shape as the official SDK.
for await (const msg of query({ prompt: 'List the files and summarize the project', workspace, llm })) {
if (msg.type === 'assistant') {
for (const block of msg.message.content) {
if (block.type === 'text') console.log(block.text)
}
} else if (msg.type === 'result' && msg.subtype === 'success') {
console.log('Done:', msg.result)
}
}MCP servers (external + in-process)
Connect external MCP servers or define in-process tools. Because browsers block
direct cross-origin MCP fetches (CORS), pass a mcpProxy for remote servers:
import { createSdkMcpServer, tool } from 'anyclaude-sdk'
const calc = createSdkMcpServer({
name: 'calc',
tools: [tool('add', 'Add two numbers',
{ type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } }, required: ['a', 'b'] },
(args) => ({ content: [{ type: 'text', text: String(args.a + args.b) }] }))],
})
query({
prompt, workspace, llm,
mcpServers: {
calc, // in-process, no network
docs: { type: 'http', url: 'https://mcp.example.com' }, // remote
},
// Route remote MCP through a CORS proxy (function, `{url}`/`{rawUrl}` template, or bare prefix):
mcpProxy: 'https://my-proxy.example/?url={url}',
})Remote tools are exposed as mcp__<server>__<tool>.
Providers
Three transport clients, all implementing the same LLMClient interface:
import { createOpenAIClient, createAnthropicClient, createResponsesClient } from 'anyclaude-sdk'
// OpenAI-compatible Chat Completions (OpenAI, Groq, Together, OpenRouter, xAI, Kilo, local…)
const a = createOpenAIClient({ apiKey, baseUrl: 'https://api.x.ai/v1', model: 'grok-build-0.1' })
// Anthropic Messages API
const b = createAnthropicClient({ apiKey, model: 'claude-sonnet-4-6' })
// OpenAI Responses API (POST /v1/responses)
const c = createResponsesClient({ apiKey, model: 'gpt-4o' })All three normalize tool calls, streaming, and usage to the same StreamResult,
and include a fallback parser for models that emit tool calls as inline text.
Multi-turn / interactive sessions
Use a PromptStream to push user turns over time:
import { query, PromptStream } from 'anyclaude-sdk'
const prompts = new PromptStream()
const session = query({ prompt: prompts, workspace, llm, model: 'gpt-4o' })
prompts.push('Create a hello.txt with a greeting')
// …later, based on UI input:
prompts.push('Now translate it to French')
prompts.end() // close the conversation
for await (const msg of session) {
// render msg…
}Tools
ALL_CLAUDE_CODE_TOOLS includes:
| Tool | Purpose |
|------|---------|
| bash | Run shell commands via jsh (2>&1//dev/null redirects are stripped) |
| read_file | Read text (numbered lines, offset/limit), images (auto-downsampled base64), PDFs (document block), and notebooks (.ipynb cells + outputs); binary files are rejected with guidance |
| write_file | Write a file, creating parent dirs |
| edit_file | Exact-match string replace (requires a prior read) |
| multi_edit | Apply a sequence of edits to one file atomically |
| notebook_edit | Replace/insert/delete cells in a .ipynb |
| delete_file | Remove a file/dir |
| glob | Find files by glob pattern (**, *, ?) |
| grep | Regex search across files |
| list_files | List a directory |
| todo_write | Track a multi-step task list across turns |
| web_fetch | Fetch a URL → clean Markdown via the Jina Reader (CORS-free, JS-rendered) |
| web_search | Web search via Jina + DuckDuckGo HTML; returns top-N title/URL/snippet |
File reading: images, PDFs, notebooks
read_file dispatches by file type. Image and PDF bytes are forwarded to the
model automatically as a follow-up user turn (Anthropic gets native
image/document blocks; OpenAI-compatible endpoints get image_url/file
parts), so the model can actually see the file, not just a text summary.
Tune the caps via limits:
query({ prompt, workspace, llm, limits: { maxTokens: 25000, maxImageBytes: 3_750_000, maxPdfPages: 20 } })Pass a subset, or your own Tool[], via tools::
import { readFile, writeFile, editFile } from 'anyclaude-sdk'
query({ prompt, workspace, llm, tools: [readFile, writeFile, editFile] })Slash commands
A user turn beginning with / is intercepted. Built-ins: /help, /clear,
/compact [focus] (summarizes history to free context), /tools, /cost,
/model. Define your own prompt-template commands:
import { query, promptCommand } from 'anyclaude-sdk'
query({
prompt: promptStream, workspace, llm,
commands: [promptCommand('review', 'Review the diff', 'Review this code and list issues: $ARGUMENTS')],
})
// user types: /review src/app.tsBackground tasks
Enable with background: true to run sub-agents or long work off the critical
path. The task tool gains run_in_background (returns a task id immediately),
and task_list / task_output / task_stop tools let the agent poll them.
Optional off-main-thread execution via a Comlink worker harness
(exposeBackgroundWorker / wrapWorker); the in-thread manager works without it.
query({ prompt, workspace, llm, agents: {}, background: true })Agents in separate Web Workers
Two halves: Comlink for main→worker control (wrapWorker / exposeBackgroundWorker,
above), and BroadcastChannelMailbox so agents in different workers gossip
mailbox-style. It's a drop-in Mailbox, so the existing team tools
(send_message / dispatch_tasks) work unchanged across workers:
import { BroadcastChannelMailbox } from 'anyclaude-sdk'
// inside each Web Worker / tab / worker_thread, same channel name:
const mailbox = new BroadcastChannelMailbox({ channelName: 'team', origin: 'planner' })
query({ prompt, workspace, llm, team: true, mailbox })
// messages sent by one worker land in the addressed agent's inbox in another.Uses the global BroadcastChannel by default. For durable cross-tab delivery
(IndexedDB/localStorage fallbacks, older browsers, Node) use the one-call helper
— it's backed by the bundled broadcast-channel
package, lazy-imported so it stays out of bundles that don't use it:
const mailbox = await BroadcastChannelMailbox.crossTab({ channelName: 'team', origin: 'planner' })
query({ prompt, workspace, llm, team: true, mailbox })Push delivery to a running agent. Messages addressed to an agent are
auto-injected into its transcript at the next turn boundary — same model as the
message queue, but from the shared mailbox. So a coordinator (or peer, or another
worker) can redirect a running sub-agent mid-task and it lands on the
sub-agent's next tool round, no polling tool needed. dispatch_tasks names each
worker worker:<taskId> so you can target a specific one:
mailbox.send('coordinator', 'worker:task_1', 'while you work: also add logging')
// worker:task_1 sees "[Team messages] - from coordinator: ..." on its next step.On by default with team: true; opt out via query({ deliverTeamMessages: false }).
Pluggable backends
You aren't tied to WebContainer. A Sandbox is just a FileSystem plus a
CommandExecutor, and you can mix and match.
Any sandbox provider
Adapters wrap each provider's client structurally (no hard dependency on their SDKs — install only the one you use):
import { E2BSandbox, VercelSandbox, DaytonaSandbox, CloudflareSandbox } from 'anyclaude-sdk'
// e.g. E2B
import { Sandbox } from 'e2b'
const sbx = await Sandbox.create()
const workspace = new E2BSandbox(sbx)
query({ prompt, workspace, llm })Supported: WebContainer, E2B, Vercel Sandbox, Daytona,
Cloudflare Sandbox, and LocalSandbox (real OS). All implement the same
Sandbox interface.
Local real-OS sandbox (Node)
Run the agent directly against the host machine's filesystem and shell — like Claude Code — with automatic platform detection (Windows / macOS / Linux):
import { LocalSandbox, createAnthropicClient, query } from 'anyclaude-sdk'
const workspace = new LocalSandbox({ cwd: '/path/to/project' }) // defaults to process.cwd()
const llm = createAnthropicClient({ baseUrl, model: 'claude-sonnet-4-6', apiKey })
for await (const msg of query({ prompt: 'add a CLI flag and run the tests', workspace, llm })) { /* … */ }The agent's working directory is taken from the sandbox automatically. See
examples/local-agent.mjs for a runnable headless demo. On Windows it uses
cmd.exe; elsewhere $SHELL//bin/sh (override via shell/shellArgs).
Persistent, full Linux-style filesystem (no server)
For a durable local filesystem in the browser, use a DB-backed FS and seed a
standard Linux tree. DexieFileSystem (IndexedDB) is the recommended default
— persistent across reloads, indexed for fast readdir/glob, with metadata
(mode, mtime, symlinks):
import {
DexieFileSystem, OpfsFileSystem, seedLinuxTree, composeWorkspace, NoopCommandExecutor,
} from 'anyclaude-sdk'
const fs = new DexieFileSystem('my-project-fs') // or: new OpfsFileSystem()
await seedLinuxTree(fs) // /bin /etc /home/user /tmp /usr …
// File-only agent (no shell):
const workspace = composeWorkspace(fs, new NoopCommandExecutor(), '/home/user')
// …or pair a persistent FS with a remote shell:
// const workspace = composeWorkspace(fs, new E2BSandbox(sbx), '/home/user')OpfsFileSystem (Origin Private File System) is offered alongside Dexie for
large-binary / native-handle scenarios; use OpfsFileSystem.isSupported() to
feature-detect.
A MemoryFileSystem also ships for tests:
import { MemoryFileSystem, NoopCommandExecutor, composeWorkspace } from 'anyclaude-sdk'
const fs = new MemoryFileSystem()
await fs.writeFile('/app/index.ts', 'export const x = 1')
const workspace = composeWorkspace(fs, new NoopCommandExecutor())Skills (programmatic)
Declare reusable prompt-skills inline — each becomes a /name slash command and is invokable by the agent through the skill tool. $ARGUMENTS is substituted at call time:
import { query, defineSkill } from 'anyclaude-sdk'
query({
prompt, workspace, llm,
skills: [
defineSkill({
name: 'changelog',
description: 'Summarize git changes into a changelog entry',
instructions: 'Write a concise changelog entry for: $ARGUMENTS',
argumentHint: '<since>',
}),
],
})You can also pass plain Skill objects, or skills: true to load .claude/skills/*.md from the workspace.
Serverless & the "survivor"
Run query() in a serverless function and stream SDKMessages to the browser. For runs longer than the platform's time cap, checkpoint at a turn boundary and continue transparently in a fresh invocation:
// pause near the deadline, persist to the store, emit a `paused` message
query({ prompt, workspace, llm, sessionStore, maxDurationMs: 20_000 })
// later — resume + continue the tool loop with NO new user message
query({ workspace, llm, sessionStore, resume: true, continueRun: true })Pluggable SessionStore adapters (all implement SessionStoreLike): SessionStore (IndexedDB), MemorySessionStore, KVSessionStore (Vercel KV / Upstash), RedisSessionStore, PostgresSessionStore (Neon / pg / postgres.js), SupabaseSessionStore.
Client-side tools — server brain, browser hands
Declare tools the host executes — e.g. run bash in the user's browser WebContainer while the agent loop runs on your server. The run pauses with a client_tool_request; the client executes it and you resume with the result:
import { WORKSPACE_TOOL_NAMES } from 'anyclaude-sdk'
query({ prompt, llm, workspace, sessionId, clientTools: WORKSPACE_TOOL_NAMES }) // → emits client_tool_request + pauses
query({ llm, workspace, sessionId, resume: true, continueRun: true, clientToolResults }) // → continuesOn the browser side, anyclaude-react turns those into a ready executor map backed by any workspace — a WebContainer (real shell + files), the user's IndexedDB (DexieFileSystem), OPFS, or memory:
import { createWebContainerClientTools, createWorkspaceClientTools } from 'anyclaude-react'
useAgent({ endpoint: '/api/agent', clientTools: createWebContainerClientTools(wc) }) // files + bash
useAgent({ endpoint: '/api/agent', clientTools: createWorkspaceClientTools(new DexieFileSystem('my-db')) }) // IndexedDBInteractive — ask_user_question
Provide onAskUser and the agent gains an ask_user_question tool to put a multiple-choice decision to the user:
query({ prompt, workspace, llm, onAskUser: async ({ question, options }) => pickOne(question, options) })Hiding your prompt from the browser (projection)
The agent loop runs server-side, so your system prompt, tool instructions, and retrieved context live in the server→LLM request and never reach the browser. To also strip sensitive artifacts (reasoning, raw tool output / RAG, model identity) from the streamed messages, wrap the stream — a pure, opt-in output transform:
import { projectMessages } from 'anyclaude-sdk'
for await (const m of projectMessages(query({ /* ... */ }), { preset: 'public' }))
res.write(JSON.stringify(m) + '\n')paused and client_tool_request control messages are always preserved. (Note: anything that runs in the browser — createAgentClient mode — necessarily exposes its request; use the server/endpoint path when the prompt is proprietary.)
React UI kit — anyclaude-react
npm install anyclaude-reactuseAgent() plus restylable components — chat (AgentChat, ChatPanel, Transcript, MarkdownMessage, Composer, Working, ToolCall) and an IDE set (Terminal, FileExplorer, CodeEditor, AskUser). createAgentClient / createEndpointClient auto-stitch paused continuations and run clientTools in the browser.
Run Claude Code against any model — anyclaude-sdk/anthropic-endpoint
Stand up an Anthropic Messages API-compatible endpoint backed by any OpenAI-compatible model, so Claude Code itself (or any Anthropic-Messages client) runs against DeepSeek / Qwen / GLM / Kimi / local Ollama. Unlike a naive proxy, inline tool-call dialects are recovered into proper tool_use blocks, so tool use actually works on cheap models.
import { createOpenAIClient } from 'anyclaude-sdk/llm'
import { anthropicToChat, anthropicSSE } from 'anyclaude-sdk/anthropic-endpoint'
const llm = createOpenAIClient({ baseUrl: 'https://api.deepseek.com/v1', model: 'deepseek-chat', apiKey })
// POST /v1/messages:
for await (const evt of anthropicSSE(llm, anthropicToChat(body), { model: 'deepseek-chat' })) res.write(evt)
// then: ANTHROPIC_BASE_URL=http://localhost:8787 claudeRunnable: examples/claude-code-router.
Reliable tool use on cheap / open models
Frontier models emit clean native function-calls; cheaper ones often don't. Three layers (in anyclaude-sdk/llm) close the gap: tool-call dialects (parseToolCalls — xml-function / hermes / json-fence), auto-detected model profiles (profileForModel — qwen/deepseek/moonshot/zhipu/mistral/llama), and self-healing argument repair (query({ repairToolCalls }), on by default — validates args and feeds the model a corrective tool_result instead of running with garbage). Prove it on your endpoints with scripts/compat-matrix.mjs → COMPATIBILITY.md.
Scaffold an in-browser AI IDE
npm create anyclaude-app@latest my-app # template: bolt — WebContainer + chat + live preview, no backendThe bolt template wires useWebContainerPreview({ wc }) (boot a dev server → live preview URL) + a browser-side query() + the IDE components. See anyclaude-react.
Token efficiency — deferred tools
Keep a large pool of rarely-used tools out of the per-turn payload (big savings on weak/uncached models) while staying discoverable + callable. Mark them deferred; tool_search indexes them and the loop arms a tool (sends its schema on subsequent turns) once search surfaces it — then it executes normally.
query({ prompt, workspace, llm,
extraTools: [deploy, ...integrationTools], // e.g. 35 integration tools
deferredTools: ['stripe_charge', 'supabase_query', /* … the niche ones */],
})
// or per-tool: defineTool({ name, description, parameters, run, defer: true })Only the lean core + tool_search are sent each turn; the model searches when it needs a niche tool, the SDK arms it, and the call goes through. Register 35, send ~10.
Agent-loop tuning (cheap / lightweight / fast)
Opt-in knobs for token cost and latency — especially on weak / uncached models:
query({
prompt, workspace, llm,
systemPromptPreset: 'lean', // ~70% shorter built-in prompt — saved every turn on uncached models
keepToolResults: 6, // context editing: stub tool_results older than the last 6 (caps transcript growth)
parallelToolExecution: true, // run a turn's read-only tool calls concurrently (~2× faster on multi-read turns)
deferredTools: [/* niche tools */], // keep rarely-used tools out of the payload until tool_search arms them
})
// custom read tool opting into parallelism:
defineTool({ name: 'get_logs', description: '…', parameters, run, parallelSafe: true })Mutating tools / bash / delegated client tools always execute serially; keepToolResults and parallelToolExecution preserve correctness, just trim cost/latency.
Other niceties
- Live compaction marker —
autoCompactemits acompact_boundarywithstatus: 'start'before summarizing (for a live "compacting…" shimmer) andstatus: 'end'after withpost_tokens. - Cancel a queued message —
MessageQueue.push()returns a stable id;remove(id)cancels a single pending message (per-pill ✕ in a UI). - BYO LLM client — reuse the SDK's wire codec:
toOpenAIMessages,consumeSSE, and the LLM types fromanyclaude-sdk/llm(no bare-root import in browser bundles).
Examples & live demo
Runnable Vite projects in examples/: browser-ide (WebContainer IDE — real shell + Node in the tab), browser-chat, claude-code-router, vercel-kv-survivor, vercel-supabase-survivor, vercel-indexeddb-survivor, vercel-clienttools (server brain / browser hands). Try the live demo.
API
query(options): AsyncGenerator<SDKMessage>— main entry.prompt: string | AsyncIterable<SDKUserMessage>workspace: FileSystem & CommandExecutorllm: LLMClienttools?,extraTools?,allowedTools?/disallowedTools?,deferredTools?(lazy-load),model?,systemPrompt?/appendSystemPrompt?,maxTurns?(default 50),cwd?,abortController?- serverless:
sessionStore?,resume?,maxDurationMs?,continueRun? - client tools:
clientTools?,clientToolResults?; interactive:onAskUser? - also:
mcpServers?,agents?,commands?,hooks?,background?,team?,memory?,permissionMode?/canUseTool?,messageQueue?
createOpenAIClient/createAnthropicClient/createResponsesClientWebContainerWorkspace,MemoryFileSystem,NoopCommandExecutor,LocalSandbox,composeWorkspacedefineTool(custom tools),projectMessages(server-side stream redaction)ALL_CLAUDE_CODE_TOOLS, individual tools,toolDefs,toolByName- browser-clean subpaths:
anyclaude-sdk/{query,loop,llm,fs,workspace,tools,session,memory,compact,permissions,skills,queue,prompt,anthropic-endpoint,telemetry} anyclaude-sdk/llm:parseToolCalls+ dialects,profileForModel(model profiles),validateToolArguments(repair),toOpenAIMessages/consumeSSE(BYO-client codec)anyclaude-sdk/anthropic-endpoint:anthropicToChat,anthropicSSE,streamResultToAnthropicMessage(Claude-Code router)runToolLoop(/loop),compactWithWindow(/compact),track/telemetryEnabled(/telemetry)- All
SDK*message types,ContentBlockParam,LLMClient,ToolDef,SessionStoreLike, etc.
Differences from the official SDK
| Feature | Official SDK | anyclaude-sdk | |---------|-------------|--------------------| | Auth | OAuth token | None required | | Backend | claude.ai API | Any OpenAI/Anthropic endpoint | | Runtime | Node only | Browser, Node, Bun | | File ops | Native filesystem | Pluggable (WebContainer / Memory / IndexedDB / local) | | Commands | Native shell | jsh (WebContainer) / local / client-side tools | | MCP / slash commands / background tasks / sub-agents | Built-in | Built-in | | Serverless survivor + prompt projection | — | Built-in |
Telemetry
The SDK emits anonymous, opt-out usage telemetry (SDK version, runtime, a coarse model-family bucket, and which features are used) — never code, prompts, repo identity, paths, or keys. It sends to an aggregate-only collector (a Puter Worker; source in examples/telemetry-collector). Disable with ANYCLAUDE_TELEMETRY=0, DO_NOT_TRACK=1, or query({ disableTelemetry: true }); repoint with ANYCLAUDE_TELEMETRY_URL (or set it to '' to send nowhere). Full disclosure: TELEMETRY.md.
License
MIT
