anyclaude-sdk 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,19 @@
1
1
  # anyclaude-sdk
2
2
 
3
- Claude Code agent capabilities — tools, the tool loop, multi-turn conversations —
4
- running **entirely in the browser** via [WebContainer](https://webcontainers.io),
5
- against **any OpenAI- or Anthropic-compatible LLM endpoint**. No backend, no OAuth,
6
- no native binaries.
3
+ [![npm version](https://img.shields.io/npm/v/anyclaude-sdk.svg)](https://www.npmjs.com/package/anyclaude-sdk)
4
+ [![anyclaude-react](https://img.shields.io/npm/v/anyclaude-react.svg?label=anyclaude-react)](https://www.npmjs.com/package/anyclaude-react)
5
+ [![license](https://img.shields.io/npm/l/anyclaude-sdk.svg)](LICENSE)
6
+ [![docs](https://img.shields.io/badge/docs-anyclaude--docs.puter.site-4dd0e1.svg)](https://anyclaude-docs.puter.site)
7
+ [![live demo](https://img.shields.io/badge/live%20demo-browser%20IDE-4dd0e1.svg)](https://anyclaude-docs.puter.site/demo/)
8
+
9
+ Claude Code agent capabilities — tools, the tool loop, multi-turn conversations,
10
+ MCP, sub-agents, sessions — against **any OpenAI- or Anthropic-compatible LLM
11
+ endpoint**, running in the **browser** ([WebContainer](https://webcontainers.io)),
12
+ **Node**, and **Bun**. No backend required, no OAuth, no native binaries.
13
+
14
+ > **Live demo:** [a full IDE running in your browser](https://anyclaude-docs.puter.site/demo/) ·
15
+ > **Docs:** [anyclaude-docs.puter.site](https://anyclaude-docs.puter.site) ·
16
+ > **React UI kit:** [`anyclaude-react`](anyclaude-react/)
7
17
 
8
18
  It exposes the same `query()` async-generator interface and the same `SDKMessage`
9
19
  envelope as `@anthropic-ai/claude-agent-sdk`, so code written against the official
@@ -267,18 +277,75 @@ await fs.writeFile('/app/index.ts', 'export const x = 1')
267
277
  const workspace = composeWorkspace(fs, new NoopCommandExecutor())
268
278
  ```
269
279
 
280
+ ## Serverless & the "survivor"
281
+
282
+ Run `query()` in a serverless function and stream `SDKMessage`s to the browser. For runs longer than the platform's time cap, checkpoint at a turn boundary and continue transparently in a fresh invocation:
283
+
284
+ ```ts
285
+ // pause near the deadline, persist to the store, emit a `paused` message
286
+ query({ prompt, workspace, llm, sessionStore, maxDurationMs: 20_000 })
287
+ // later — resume + continue the tool loop with NO new user message
288
+ query({ workspace, llm, sessionStore, resume: true, continueRun: true })
289
+ ```
290
+
291
+ Pluggable `SessionStore` adapters (all implement `SessionStoreLike`): `SessionStore` (IndexedDB), `MemorySessionStore`, `KVSessionStore` (Vercel KV / Upstash), `RedisSessionStore`, `PostgresSessionStore` (Neon / pg / postgres.js), `SupabaseSessionStore`.
292
+
293
+ ## Client-side tools — server brain, browser hands
294
+
295
+ 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:
296
+
297
+ ```ts
298
+ query({ prompt, llm, workspace, sessionId, clientTools: ['bash'] }) // → emits client_tool_request + pauses
299
+ query({ llm, workspace, sessionId, resume: true, continueRun: true, clientToolResults }) // → continues
300
+ ```
301
+
302
+ ## Interactive — `ask_user_question`
303
+
304
+ Provide `onAskUser` and the agent gains an `ask_user_question` tool to put a multiple-choice decision to the user:
305
+
306
+ ```ts
307
+ query({ prompt, workspace, llm, onAskUser: async ({ question, options }) => pickOne(question, options) })
308
+ ```
309
+
310
+ ## Hiding your prompt from the browser (projection)
311
+
312
+ 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:
313
+
314
+ ```ts
315
+ import { projectMessages } from 'anyclaude-sdk'
316
+ for await (const m of projectMessages(query({ /* ... */ }), { preset: 'public' }))
317
+ res.write(JSON.stringify(m) + '\n')
318
+ ```
319
+
320
+ `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.)
321
+
322
+ ## React UI kit — `anyclaude-react`
323
+
324
+ ```bash
325
+ npm install anyclaude-react
326
+ ```
327
+
328
+ `useAgent()` 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.
329
+
330
+ ## Examples & live demo
331
+
332
+ Runnable Vite projects in [`examples/`](examples/): **`browser-ide`** (WebContainer IDE — real shell + Node in the tab), `browser-chat`, `vercel-kv-survivor`, `vercel-supabase-survivor`, `vercel-indexeddb-survivor`, **`vercel-clienttools`** (server brain / browser hands). Try the **[live demo](https://anyclaude-docs.puter.site/demo/)**.
333
+
270
334
  ## API
271
335
 
272
336
  - `query(options): AsyncGenerator<SDKMessage>` — main entry.
273
337
  - `prompt: string | AsyncIterable<SDKUserMessage>`
274
338
  - `workspace: FileSystem & CommandExecutor`
275
339
  - `llm: LLMClient`
276
- - `tools?`, `model?`, `systemPrompt?`, `maxTurns?` (default 50), `cwd?`, `abortController?`
277
- - `createOpenAIClient(options): LLMClient`
278
- - `createAnthropicClient(options): LLMClient`
279
- - `WebContainerWorkspace`, `MemoryFileSystem`, `NoopCommandExecutor`
340
+ - `tools?`, `extraTools?`, `allowedTools?`/`disallowedTools?`, `model?`, `systemPrompt?`/`appendSystemPrompt?`, `maxTurns?` (default 50), `cwd?`, `abortController?`
341
+ - serverless: `sessionStore?`, `resume?`, `maxDurationMs?`, `continueRun?`
342
+ - client tools: `clientTools?`, `clientToolResults?`; interactive: `onAskUser?`
343
+ - also: `mcpServers?`, `agents?`, `commands?`, `hooks?`, `background?`, `team?`, `memory?`, `permissionMode?`/`canUseTool?`, `messageQueue?`
344
+ - `createOpenAIClient` / `createAnthropicClient` / `createResponsesClient`
345
+ - `WebContainerWorkspace`, `MemoryFileSystem`, `NoopCommandExecutor`, `LocalSandbox`, `composeWorkspace`
346
+ - `defineTool` (custom tools), `projectMessages` (server-side stream redaction)
280
347
  - `ALL_CLAUDE_CODE_TOOLS`, individual tools, `toolDefs`, `toolByName`
281
- - All `SDK*` message types, `ContentBlockParam`, `LLMClient`, `ToolDef`, etc.
348
+ - All `SDK*` message types, `ContentBlockParam`, `LLMClient`, `ToolDef`, `SessionStoreLike`, etc.
282
349
 
283
350
  ## Differences from the official SDK
284
351
 
@@ -286,9 +353,11 @@ const workspace = composeWorkspace(fs, new NoopCommandExecutor())
286
353
  |---------|-------------|--------------------|
287
354
  | Auth | OAuth token | None required |
288
355
  | Backend | claude.ai API | Any OpenAI/Anthropic endpoint |
289
- | File ops | Native filesystem | WebContainer fs (pluggable) |
290
- | Commands | Native shell | jsh (WebContainer) |
291
- | MCP / slash commands / background tasks | Built-in | Not included |
356
+ | Runtime | Node only | Browser, Node, Bun |
357
+ | File ops | Native filesystem | Pluggable (WebContainer / Memory / IndexedDB / local) |
358
+ | Commands | Native shell | jsh (WebContainer) / local / client-side tools |
359
+ | MCP / slash commands / background tasks / sub-agents | Built-in | Built-in |
360
+ | Serverless survivor + prompt projection | — | Built-in |
292
361
 
293
362
  ## License
294
363
 
package/dist/agent.js CHANGED
@@ -797,7 +797,19 @@ export async function* runAgent(options) {
797
797
  isError = !!r.isError;
798
798
  }
799
799
  catch (err) {
800
- content = `Error executing ${name}: ${err instanceof Error ? err.message : String(err)}`;
800
+ const detail = err instanceof Error
801
+ ? err.message
802
+ : typeof err === 'string'
803
+ ? err
804
+ : (() => {
805
+ try {
806
+ return JSON.stringify(err);
807
+ }
808
+ catch {
809
+ return String(err);
810
+ }
811
+ })();
812
+ content = `Error executing ${name}: ${detail}`;
801
813
  isError = true;
802
814
  }
803
815
  if (isError) {
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export * from './mcp/index.js';
13
13
  export * from './commands/index.js';
14
14
  export * from './background/index.js';
15
15
  export * from './queue.js';
16
+ export { projectMessages, projectMessage, type ProjectionOptions, type ProjectionPreset, } from './projection.js';
16
17
  export * from './team/index.js';
17
18
  export * from './session/index.js';
18
19
  export * from './memory/index.js';
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export * from './mcp/index.js';
15
15
  export * from './commands/index.js';
16
16
  export * from './background/index.js';
17
17
  export * from './queue.js';
18
+ export { projectMessages, projectMessage, } from './projection.js';
18
19
  export * from './team/index.js';
19
20
  export * from './session/index.js';
20
21
  export * from './memory/index.js';
@@ -0,0 +1,29 @@
1
+ import type { SDKMessage } from './types/index.js';
2
+ export type ProjectionPreset = 'public' | 'raw';
3
+ export interface ProjectionOptions {
4
+ /** 'public' (browser-safe defaults) or 'raw' (passthrough; only explicit opts apply). Default 'public'. */
5
+ preset?: ProjectionPreset;
6
+ /** Replace tool_result block content with a placeholder — hides raw tool output + RAG context. */
7
+ redactToolResults?: boolean;
8
+ /** Drop synthetic tool_result messages entirely instead of redacting their content. */
9
+ dropToolResults?: boolean;
10
+ /** Remove thinking/reasoning blocks and deltas. */
11
+ stripReasoning?: boolean;
12
+ /** Remove tool_use input args (tool NAMES still appear via usage). */
13
+ stripToolInput?: boolean;
14
+ /** Remove model/provider-identifying fields (model name, tool list, mcp servers, modelUsage). */
15
+ stripModelInfo?: boolean;
16
+ /** Placeholder substituted for redacted content. Default '[redacted]'. */
17
+ placeholder?: string;
18
+ /** Drop any message whose `type` or `subtype` is in this list. */
19
+ drop?: string[];
20
+ /** Custom transform applied last; return null to drop the message. */
21
+ redact?: (m: SDKMessage) => SDKMessage | null;
22
+ }
23
+ /** Project one message. Returns null to drop it. */
24
+ export declare function projectMessage(msg: SDKMessage, options?: ProjectionOptions): SDKMessage | null;
25
+ /**
26
+ * Wrap an SDKMessage stream, projecting each message for browser delivery.
27
+ * Pure output transform — does NOT affect the agent loop. Opt-in.
28
+ */
29
+ export declare function projectMessages(source: AsyncIterable<SDKMessage>, options?: ProjectionOptions): AsyncGenerator<SDKMessage>;
@@ -0,0 +1,99 @@
1
+ function resolve(o) {
2
+ const pub = (o.preset ?? 'public') === 'public';
3
+ return {
4
+ redactToolResults: o.redactToolResults ?? pub,
5
+ dropToolResults: o.dropToolResults ?? false,
6
+ stripReasoning: o.stripReasoning ?? pub,
7
+ stripToolInput: o.stripToolInput ?? false,
8
+ stripModelInfo: o.stripModelInfo ?? pub,
9
+ placeholder: o.placeholder ?? '[redacted]',
10
+ drop: o.drop ?? [],
11
+ redact: o.redact,
12
+ };
13
+ }
14
+ /** Project one message. Returns null to drop it. */
15
+ export function projectMessage(msg, options = {}) {
16
+ const o = resolve(options);
17
+ const m = msg;
18
+ const type = m.type;
19
+ const subtype = m.subtype;
20
+ if (o.drop.includes(type) || (subtype && o.drop.includes(subtype)))
21
+ return null;
22
+ // Control messages the client must act on — never alter (survivor + client tools).
23
+ if (type === 'system' && (subtype === 'paused' || subtype === 'client_tool_request')) {
24
+ return o.redact ? o.redact(msg) : msg;
25
+ }
26
+ let out = msg;
27
+ if (type === 'user') {
28
+ // Synthetic tool_result messages carry raw tool output + retrieved context.
29
+ const message = m.message;
30
+ const content = message?.content;
31
+ if (Array.isArray(content) && content.some((b) => b.type === 'tool_result')) {
32
+ if (o.dropToolResults)
33
+ return o.redact ? o.redact(msg) ?? null : null;
34
+ if (o.redactToolResults) {
35
+ const newContent = content.map((b) => b.type === 'tool_result' ? { ...b, content: o.placeholder } : b);
36
+ out = { ...m, message: { ...message, content: newContent } };
37
+ }
38
+ }
39
+ }
40
+ else if (type === 'assistant') {
41
+ const message = m.message;
42
+ let content = message.content;
43
+ if (o.stripReasoning)
44
+ content = content.filter((b) => b.type !== 'thinking');
45
+ if (o.stripToolInput)
46
+ content = content.map((b) => (b.type === 'tool_use' ? { ...b, input: {} } : b));
47
+ const newMessage = { ...message, content };
48
+ if (o.stripModelInfo)
49
+ newMessage.model = '';
50
+ out = { ...m, message: newMessage };
51
+ }
52
+ else if (type === 'stream_event') {
53
+ const event = m.event;
54
+ const et = event?.type;
55
+ if (o.stripReasoning) {
56
+ if (et === 'content_block_delta' && event.delta?.type === 'thinking_delta')
57
+ return null;
58
+ if (et === 'content_block_start' && event.content_block?.type === 'thinking')
59
+ return null;
60
+ }
61
+ if (o.stripModelInfo && et === 'message_start') {
62
+ const sm = { ...event.message };
63
+ delete sm.model;
64
+ out = { ...m, event: { ...event, message: sm } };
65
+ }
66
+ }
67
+ else if (type === 'system' && subtype === 'init') {
68
+ if (o.stripModelInfo) {
69
+ out = {
70
+ ...m,
71
+ model: '',
72
+ tools: [],
73
+ mcp_servers: [],
74
+ slash_commands: [],
75
+ skills: [],
76
+ agents: [],
77
+ apiKeySource: 'none',
78
+ cwd: '',
79
+ };
80
+ }
81
+ }
82
+ else if (type === 'result') {
83
+ if (o.stripModelInfo) {
84
+ out = { ...m, modelUsage: {} };
85
+ }
86
+ }
87
+ return o.redact ? o.redact(out) : out;
88
+ }
89
+ /**
90
+ * Wrap an SDKMessage stream, projecting each message for browser delivery.
91
+ * Pure output transform — does NOT affect the agent loop. Opt-in.
92
+ */
93
+ export async function* projectMessages(source, options = {}) {
94
+ for await (const m of source) {
95
+ const out = projectMessage(m, options);
96
+ if (out)
97
+ yield out;
98
+ }
99
+ }
@@ -32,7 +32,15 @@ export const bash = {
32
32
  return { content: 'Error: `command` is required.', isError: true };
33
33
  }
34
34
  const timeout = typeof input.timeout_ms === 'number' ? input.timeout_ms : undefined;
35
- const { output, exitCode } = await ctx.exec.exec(command, timeout);
35
+ let result;
36
+ try {
37
+ result = await ctx.exec.exec(command, timeout);
38
+ }
39
+ catch (e) {
40
+ const msg = e instanceof Error ? e.message : typeof e === 'string' ? e : JSON.stringify(e);
41
+ return { content: `Failed to run command: ${msg}`, isError: true };
42
+ }
43
+ const { output, exitCode } = result;
36
44
  if (exitCode !== 0) {
37
45
  const body = output || '(no output)';
38
46
  return {
@@ -39,7 +39,11 @@ export interface WebContainerProcess {
39
39
  export declare class WebContainerWorkspace implements FileSystem, CommandExecutor {
40
40
  readonly wc: WebContainerLike;
41
41
  readonly cwd: string;
42
+ private cwdReady;
42
43
  constructor(wc: WebContainerLike, cwd?: string);
44
+ /** Ensure the working directory exists before spawning into it (memoized).
45
+ * A fresh WebContainer has only `/`, so spawning with a missing cwd rejects. */
46
+ private ensureCwd;
43
47
  /** Resolve a possibly-relative path against the workspace cwd. */
44
48
  resolve(path: string): string;
45
49
  readFile(path: string): Promise<string | null>;
@@ -8,9 +8,20 @@ const textDecoder = new TextDecoder();
8
8
  */
9
9
  export class WebContainerWorkspace {
10
10
  constructor(wc, cwd) {
11
+ this.cwdReady = null;
11
12
  this.wc = wc;
12
13
  this.cwd = cwd ?? wc.workdir ?? '/home/projects';
13
14
  }
15
+ /** Ensure the working directory exists before spawning into it (memoized).
16
+ * A fresh WebContainer has only `/`, so spawning with a missing cwd rejects. */
17
+ ensureCwd() {
18
+ if (!this.cwdReady) {
19
+ this.cwdReady = Promise.resolve(this.wc.fs.mkdir(this.cwd, { recursive: true }))
20
+ .then(() => undefined)
21
+ .catch(() => undefined);
22
+ }
23
+ return this.cwdReady;
24
+ }
14
25
  /** Resolve a possibly-relative path against the workspace cwd. */
15
26
  resolve(path) {
16
27
  if (path.startsWith('/'))
@@ -80,10 +91,21 @@ export class WebContainerWorkspace {
80
91
  // ---- CommandExecutor ----
81
92
  async exec(command, timeoutMs = 120_000, env) {
82
93
  const sanitized = sanitizeCommand(command);
83
- const proc = await this.wc.spawn('jsh', ['-c', sanitized], {
84
- env,
85
- cwd: this.cwd,
86
- });
94
+ await this.ensureCwd();
95
+ let proc;
96
+ try {
97
+ proc = await this.wc.spawn('jsh', ['-c', sanitized], { env, cwd: this.cwd });
98
+ }
99
+ catch (e) {
100
+ // Last-resort: spawn without an explicit cwd (defaults to /) so a bad cwd
101
+ // never hard-fails the tool; surface a real message if even that fails.
102
+ try {
103
+ proc = await this.wc.spawn('jsh', ['-c', sanitized], { env });
104
+ }
105
+ catch {
106
+ throw new Error(`failed to start shell (jsh): ${e instanceof Error ? e.message : String(e)}`);
107
+ }
108
+ }
87
109
  let output = '';
88
110
  const reader = proc.output.getReader();
89
111
  const collect = (async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyclaude-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "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 required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",