anyclaude-sdk 0.2.0 → 0.4.0
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 +75 -12
- package/dist/agent.d.ts +21 -1
- package/dist/agent.js +68 -12
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/projection.d.ts +29 -0
- package/dist/projection.js +99 -0
- package/dist/query.d.ts +19 -0
- package/dist/query.js +3 -0
- package/dist/tools/ask_user.d.ts +2 -0
- package/dist/tools/ask_user.js +54 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/types.d.ts +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# anyclaude-sdk
|
|
2
2
|
|
|
3
|
-
Claude Code agent capabilities — tools, the tool loop, multi-turn conversations
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
no native binaries.
|
|
3
|
+
Claude Code agent capabilities — tools, the tool loop, multi-turn conversations,
|
|
4
|
+
MCP, sub-agents, sessions — against **any OpenAI- or Anthropic-compatible LLM
|
|
5
|
+
endpoint**, running in the **browser** ([WebContainer](https://webcontainers.io)),
|
|
6
|
+
**Node**, and **Bun**. No backend required, no OAuth, no native binaries.
|
|
7
|
+
|
|
8
|
+
> **Live demo:** [a full IDE running in your browser](https://anyclaude-docs.puter.site/demo/) ·
|
|
9
|
+
> **Docs:** [anyclaude-docs.puter.site](https://anyclaude-docs.puter.site) ·
|
|
10
|
+
> **React UI kit:** [`anyclaude-react`](anyclaude-react/)
|
|
7
11
|
|
|
8
12
|
It exposes the same `query()` async-generator interface and the same `SDKMessage`
|
|
9
13
|
envelope as `@anthropic-ai/claude-agent-sdk`, so code written against the official
|
|
@@ -267,18 +271,75 @@ await fs.writeFile('/app/index.ts', 'export const x = 1')
|
|
|
267
271
|
const workspace = composeWorkspace(fs, new NoopCommandExecutor())
|
|
268
272
|
```
|
|
269
273
|
|
|
274
|
+
## Serverless & the "survivor"
|
|
275
|
+
|
|
276
|
+
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:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
// pause near the deadline, persist to the store, emit a `paused` message
|
|
280
|
+
query({ prompt, workspace, llm, sessionStore, maxDurationMs: 20_000 })
|
|
281
|
+
// later — resume + continue the tool loop with NO new user message
|
|
282
|
+
query({ workspace, llm, sessionStore, resume: true, continueRun: true })
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Pluggable `SessionStore` adapters (all implement `SessionStoreLike`): `SessionStore` (IndexedDB), `MemorySessionStore`, `KVSessionStore` (Vercel KV / Upstash), `RedisSessionStore`, `PostgresSessionStore` (Neon / pg / postgres.js), `SupabaseSessionStore`.
|
|
286
|
+
|
|
287
|
+
## Client-side tools — server brain, browser hands
|
|
288
|
+
|
|
289
|
+
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:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
query({ prompt, llm, workspace, sessionId, clientTools: ['bash'] }) // → emits client_tool_request + pauses
|
|
293
|
+
query({ llm, workspace, sessionId, resume: true, continueRun: true, clientToolResults }) // → continues
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Interactive — `ask_user_question`
|
|
297
|
+
|
|
298
|
+
Provide `onAskUser` and the agent gains an `ask_user_question` tool to put a multiple-choice decision to the user:
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
query({ prompt, workspace, llm, onAskUser: async ({ question, options }) => pickOne(question, options) })
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Hiding your prompt from the browser (projection)
|
|
305
|
+
|
|
306
|
+
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:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { projectMessages } from 'anyclaude-sdk'
|
|
310
|
+
for await (const m of projectMessages(query({ /* ... */ }), { preset: 'public' }))
|
|
311
|
+
res.write(JSON.stringify(m) + '\n')
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`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.)
|
|
315
|
+
|
|
316
|
+
## React UI kit — `anyclaude-react`
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
npm install anyclaude-react
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`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.
|
|
323
|
+
|
|
324
|
+
## Examples & live demo
|
|
325
|
+
|
|
326
|
+
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/)**.
|
|
327
|
+
|
|
270
328
|
## API
|
|
271
329
|
|
|
272
330
|
- `query(options): AsyncGenerator<SDKMessage>` — main entry.
|
|
273
331
|
- `prompt: string | AsyncIterable<SDKUserMessage>`
|
|
274
332
|
- `workspace: FileSystem & CommandExecutor`
|
|
275
333
|
- `llm: LLMClient`
|
|
276
|
-
- `tools?`, `model?`, `systemPrompt?`, `maxTurns?` (default 50), `cwd?`, `abortController?`
|
|
277
|
-
-
|
|
278
|
-
- `
|
|
279
|
-
- `
|
|
334
|
+
- `tools?`, `extraTools?`, `allowedTools?`/`disallowedTools?`, `model?`, `systemPrompt?`/`appendSystemPrompt?`, `maxTurns?` (default 50), `cwd?`, `abortController?`
|
|
335
|
+
- serverless: `sessionStore?`, `resume?`, `maxDurationMs?`, `continueRun?`
|
|
336
|
+
- client tools: `clientTools?`, `clientToolResults?`; interactive: `onAskUser?`
|
|
337
|
+
- also: `mcpServers?`, `agents?`, `commands?`, `hooks?`, `background?`, `team?`, `memory?`, `permissionMode?`/`canUseTool?`, `messageQueue?`
|
|
338
|
+
- `createOpenAIClient` / `createAnthropicClient` / `createResponsesClient`
|
|
339
|
+
- `WebContainerWorkspace`, `MemoryFileSystem`, `NoopCommandExecutor`, `LocalSandbox`, `composeWorkspace`
|
|
340
|
+
- `defineTool` (custom tools), `projectMessages` (server-side stream redaction)
|
|
280
341
|
- `ALL_CLAUDE_CODE_TOOLS`, individual tools, `toolDefs`, `toolByName`
|
|
281
|
-
- All `SDK*` message types, `ContentBlockParam`, `LLMClient`, `ToolDef`, etc.
|
|
342
|
+
- All `SDK*` message types, `ContentBlockParam`, `LLMClient`, `ToolDef`, `SessionStoreLike`, etc.
|
|
282
343
|
|
|
283
344
|
## Differences from the official SDK
|
|
284
345
|
|
|
@@ -286,9 +347,11 @@ const workspace = composeWorkspace(fs, new NoopCommandExecutor())
|
|
|
286
347
|
|---------|-------------|--------------------|
|
|
287
348
|
| Auth | OAuth token | None required |
|
|
288
349
|
| Backend | claude.ai API | Any OpenAI/Anthropic endpoint |
|
|
289
|
-
|
|
|
290
|
-
|
|
|
291
|
-
|
|
|
350
|
+
| Runtime | Node only | Browser, Node, Bun |
|
|
351
|
+
| File ops | Native filesystem | Pluggable (WebContainer / Memory / IndexedDB / local) |
|
|
352
|
+
| Commands | Native shell | jsh (WebContainer) / local / client-side tools |
|
|
353
|
+
| MCP / slash commands / background tasks / sub-agents | Built-in | Built-in |
|
|
354
|
+
| Serverless survivor + prompt projection | — | Built-in |
|
|
292
355
|
|
|
293
356
|
## License
|
|
294
357
|
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentDefinition, CanUseTool, CommandExecutor, FileSystem, HookCallback, HookEvent, PermissionMode, SDKMessage, SDKUserMessage } from './types/index.js';
|
|
1
|
+
import type { AgentDefinition, CanUseTool, CommandExecutor, ContentBlockParam, FileSystem, HookCallback, HookEvent, PermissionMode, SDKMessage, SDKUserMessage } from './types/index.js';
|
|
2
2
|
import type { FileReadLimits, Tool } from './tools/types.js';
|
|
3
3
|
import { type McpServers, type McpProxy } from './mcp/index.js';
|
|
4
4
|
import type { SlashCommand } from './commands/index.js';
|
|
@@ -34,6 +34,16 @@ export interface AgentOptions {
|
|
|
34
34
|
/** Resume + CONTINUE the tool loop on the stored transcript without a new user
|
|
35
35
|
* message (pairs with `resume`). Used to continue after a `paused` boundary. */
|
|
36
36
|
continueRun?: boolean;
|
|
37
|
+
/** Tool names executed by the HOST/client, not the server. When the agent calls
|
|
38
|
+
* one, the loop emits a `client_tool_request` + pauses; the client runs it and
|
|
39
|
+
* resumes (continueRun) with `clientToolResults`. (e.g. bash on a browser WebContainer.) */
|
|
40
|
+
clientTools?: string[];
|
|
41
|
+
/** Results for client-tool calls, injected into the transcript before continuing. */
|
|
42
|
+
clientToolResults?: Array<{
|
|
43
|
+
tool_use_id: string;
|
|
44
|
+
content: string | ContentBlockParam[];
|
|
45
|
+
is_error?: boolean;
|
|
46
|
+
}>;
|
|
37
47
|
cwd?: string;
|
|
38
48
|
sessionId?: string;
|
|
39
49
|
abortController?: AbortController;
|
|
@@ -102,6 +112,16 @@ export interface AgentOptions {
|
|
|
102
112
|
};
|
|
103
113
|
/** Prompt callback for 'ask' decisions; if absent, default mode allows / dontAsk denies. */
|
|
104
114
|
onPermissionAsk?: (toolName: string, input: Record<string, unknown>) => Promise<boolean>;
|
|
115
|
+
/** Surfaces `ask_user_question` to the host UI. When set, the tool is registered. */
|
|
116
|
+
onAskUser?: (q: {
|
|
117
|
+
question: string;
|
|
118
|
+
header?: string;
|
|
119
|
+
options: Array<{
|
|
120
|
+
label: string;
|
|
121
|
+
description?: string;
|
|
122
|
+
}>;
|
|
123
|
+
multiSelect?: boolean;
|
|
124
|
+
}) => Promise<string | string[]>;
|
|
105
125
|
/** Load + apply `.claude/settings.json` (project/local cascade) under explicit options. true, or a Settings object. */
|
|
106
126
|
settings?: boolean | Settings;
|
|
107
127
|
/** Load `.claude/skills/*.md` as slash commands + skill registry. true, or a Skill[] array. */
|
package/dist/agent.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// 7. Repeat until no tool calls or max turns reached
|
|
11
11
|
import { ALL_CLAUDE_CODE_TOOLS, toolByName, toolDefs } from './tools/index.js';
|
|
12
12
|
import { task as taskTool } from './tools/task.js';
|
|
13
|
+
import { askUserQuestion } from './tools/ask_user.js';
|
|
13
14
|
import { loadMcpServers } from './mcp/index.js';
|
|
14
15
|
import { runSlashCommand } from './commands/index.js';
|
|
15
16
|
import { BackgroundTaskManager, BACKGROUND_TOOLS } from './background/index.js';
|
|
@@ -201,6 +202,7 @@ export async function* runAgent(options) {
|
|
|
201
202
|
? options.backgroundManager ?? new BackgroundTaskManager()
|
|
202
203
|
: undefined;
|
|
203
204
|
const messageQueue = options.messageQueue;
|
|
205
|
+
const clientTools = new Set(options.clientTools ?? []);
|
|
204
206
|
// Teammates: a shared Mailbox + TaskBoard (reused from the parent when this
|
|
205
207
|
// is a sub-agent) + team tools + coordinator prompt.
|
|
206
208
|
const teamEnabled = options.team === true;
|
|
@@ -214,6 +216,9 @@ export async function* runAgent(options) {
|
|
|
214
216
|
const present = new Set(localTools.map((t) => t.def.function.name));
|
|
215
217
|
localTools = [...localTools, ...BACKGROUND_TOOLS.filter((t) => !present.has(t.def.function.name))];
|
|
216
218
|
}
|
|
219
|
+
if (options.onAskUser && !localTools.some((t) => t.def.function.name === 'ask_user_question')) {
|
|
220
|
+
localTools = [...localTools, askUserQuestion];
|
|
221
|
+
}
|
|
217
222
|
if (teamEnabled) {
|
|
218
223
|
const present = new Set(localTools.map((t) => t.def.function.name));
|
|
219
224
|
const teamSet = subagentsEnabled ? [...TEAM_TOOLS, ...TEAM_DISPATCH_TOOLS] : TEAM_TOOLS;
|
|
@@ -267,6 +272,7 @@ export async function* runAgent(options) {
|
|
|
267
272
|
signal,
|
|
268
273
|
store,
|
|
269
274
|
limits,
|
|
275
|
+
askUser: options.onAskUser,
|
|
270
276
|
background,
|
|
271
277
|
mailbox,
|
|
272
278
|
board,
|
|
@@ -308,6 +314,7 @@ export async function* runAgent(options) {
|
|
|
308
314
|
tools: subTools,
|
|
309
315
|
model: def?.model ?? model,
|
|
310
316
|
systemPrompt: subSystem,
|
|
317
|
+
onAskUser: options.onAskUser,
|
|
311
318
|
maxTurns,
|
|
312
319
|
cwd,
|
|
313
320
|
abortController: childController,
|
|
@@ -405,6 +412,8 @@ export async function* runAgent(options) {
|
|
|
405
412
|
const sessionUsage = emptyUsage();
|
|
406
413
|
const maxDurationMs = options.maxDurationMs;
|
|
407
414
|
let paused = false;
|
|
415
|
+
// Pending client-executed tool calls for the current turn (emitted on pause).
|
|
416
|
+
let clientRequests = [];
|
|
408
417
|
// Resume: seed the transcript from a prior session before the first turn.
|
|
409
418
|
if (options.resume && options.sessionStore) {
|
|
410
419
|
const prior = await options.sessionStore.load(sessionId);
|
|
@@ -495,6 +504,28 @@ export async function* runAgent(options) {
|
|
|
495
504
|
if (extra)
|
|
496
505
|
history.push({ role: 'user', content: extra });
|
|
497
506
|
}
|
|
507
|
+
// Continue after a client-tool pause: inject the host-executed results into
|
|
508
|
+
// the transcript so the (paused) assistant tool_use calls are now resolved.
|
|
509
|
+
if (isContinue && options.clientToolResults?.length) {
|
|
510
|
+
const blocks = [];
|
|
511
|
+
for (const r of options.clientToolResults) {
|
|
512
|
+
history.push({
|
|
513
|
+
role: 'tool',
|
|
514
|
+
tool_call_id: r.tool_use_id,
|
|
515
|
+
content: typeof r.content === 'string' ? r.content : JSON.stringify(r.content),
|
|
516
|
+
});
|
|
517
|
+
blocks.push({ type: 'tool_result', tool_use_id: r.tool_use_id, content: r.content, is_error: r.is_error || undefined });
|
|
518
|
+
}
|
|
519
|
+
yield {
|
|
520
|
+
type: 'user',
|
|
521
|
+
message: { role: 'user', content: blocks },
|
|
522
|
+
parent_tool_use_id: null,
|
|
523
|
+
isSynthetic: true,
|
|
524
|
+
timestamp: new Date().toISOString(),
|
|
525
|
+
uuid: uuid(),
|
|
526
|
+
session_id: sessionId,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
498
529
|
let turns = 0;
|
|
499
530
|
let lastText = '';
|
|
500
531
|
let resultModel = model ?? 'unknown';
|
|
@@ -659,12 +690,19 @@ export async function* runAgent(options) {
|
|
|
659
690
|
}
|
|
660
691
|
// Execute tool calls (permission gate + hooks around each).
|
|
661
692
|
const toolResultBlocks = [];
|
|
693
|
+
clientRequests = [];
|
|
662
694
|
const turnMedia = [];
|
|
663
695
|
for (const call of calls) {
|
|
664
696
|
if (signal?.aborted)
|
|
665
697
|
break;
|
|
666
698
|
const name = call.function.name;
|
|
667
699
|
let input = safeParse(call.function.arguments);
|
|
700
|
+
// Client-executed tool: don't run it here — record it; we pause after this
|
|
701
|
+
// turn's server tools and let the host execute + resume with the result.
|
|
702
|
+
if (clientTools.has(name)) {
|
|
703
|
+
clientRequests.push({ tool_use_id: call.id, name, input });
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
668
706
|
const tool = byName.get(name);
|
|
669
707
|
let content = '';
|
|
670
708
|
let isError = false;
|
|
@@ -829,15 +867,23 @@ export async function* runAgent(options) {
|
|
|
829
867
|
],
|
|
830
868
|
});
|
|
831
869
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
870
|
+
if (toolResultBlocks.length) {
|
|
871
|
+
yield {
|
|
872
|
+
type: 'user',
|
|
873
|
+
message: { role: 'user', content: toolResultBlocks },
|
|
874
|
+
parent_tool_use_id: null,
|
|
875
|
+
isSynthetic: true,
|
|
876
|
+
timestamp: new Date().toISOString(),
|
|
877
|
+
uuid: uuid(),
|
|
878
|
+
session_id: sessionId,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
// Client-executed tools were requested this turn → pause; the host runs
|
|
882
|
+
// them and resumes (continueRun + clientToolResults).
|
|
883
|
+
if (clientRequests.length) {
|
|
884
|
+
paused = true;
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
841
887
|
}
|
|
842
888
|
await runHooks('Stop', {
|
|
843
889
|
hook_event_name: 'Stop',
|
|
@@ -902,13 +948,23 @@ export async function* runAgent(options) {
|
|
|
902
948
|
/* persistence is best-effort */
|
|
903
949
|
}
|
|
904
950
|
}
|
|
905
|
-
// Survivor:
|
|
906
|
-
//
|
|
951
|
+
// Survivor / client-tools: paused at a boundary. Transcript persisted above.
|
|
952
|
+
// Emit any client-tool requests for the host to execute, then signal the
|
|
953
|
+
// client to continue in a fresh invocation (resume + continueRun [+ results]).
|
|
907
954
|
if (paused) {
|
|
955
|
+
for (const req of clientRequests) {
|
|
956
|
+
yield {
|
|
957
|
+
type: 'system',
|
|
958
|
+
subtype: 'client_tool_request',
|
|
959
|
+
request: req,
|
|
960
|
+
session_id: sessionId,
|
|
961
|
+
uuid: uuid(),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
908
964
|
yield {
|
|
909
965
|
type: 'system',
|
|
910
966
|
subtype: 'paused',
|
|
911
|
-
reason: 'time_budget',
|
|
967
|
+
reason: clientRequests.length ? 'client_tool' : 'time_budget',
|
|
912
968
|
session_id: sessionId,
|
|
913
969
|
uuid: uuid(),
|
|
914
970
|
};
|
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
|
+
}
|
package/dist/query.d.ts
CHANGED
|
@@ -26,6 +26,15 @@ export interface QueryOptions {
|
|
|
26
26
|
maxDurationMs?: number;
|
|
27
27
|
/** Resume + continue the tool loop with no new user message (after a `paused` boundary). */
|
|
28
28
|
continueRun?: boolean;
|
|
29
|
+
/** Tool names the HOST/client executes (e.g. bash on a browser WebContainer). The agent
|
|
30
|
+
* emits a `client_tool_request` + pauses; the client runs it and resumes with results. */
|
|
31
|
+
clientTools?: string[];
|
|
32
|
+
/** Results for client-tool calls, injected before continuing (with continueRun). */
|
|
33
|
+
clientToolResults?: Array<{
|
|
34
|
+
tool_use_id: string;
|
|
35
|
+
content: string | import('./types/index.js').ContentBlockParam[];
|
|
36
|
+
is_error?: boolean;
|
|
37
|
+
}>;
|
|
29
38
|
cwd?: string;
|
|
30
39
|
sessionId?: string;
|
|
31
40
|
abortController?: AbortController;
|
|
@@ -82,6 +91,16 @@ export interface QueryOptions {
|
|
|
82
91
|
};
|
|
83
92
|
/** Prompt callback for 'ask' permission decisions. */
|
|
84
93
|
onPermissionAsk?: (toolName: string, input: Record<string, unknown>) => Promise<boolean>;
|
|
94
|
+
/** Handler for the `ask_user_question` tool. When set, the tool is registered. */
|
|
95
|
+
onAskUser?: (q: {
|
|
96
|
+
question: string;
|
|
97
|
+
header?: string;
|
|
98
|
+
options: Array<{
|
|
99
|
+
label: string;
|
|
100
|
+
description?: string;
|
|
101
|
+
}>;
|
|
102
|
+
multiSelect?: boolean;
|
|
103
|
+
}) => Promise<string | string[]>;
|
|
85
104
|
/** Load `.claude/settings.json` (project/local cascade), or pass a Settings object. */
|
|
86
105
|
settings?: boolean | import('./settings/index.js').Settings;
|
|
87
106
|
/** Load `.claude/skills/*.md` as slash commands + a skill registry, or pass a Skill[]. */
|
package/dist/query.js
CHANGED
|
@@ -22,6 +22,8 @@ export function query(options) {
|
|
|
22
22
|
maxTurns: options.maxTurns,
|
|
23
23
|
maxDurationMs: options.maxDurationMs,
|
|
24
24
|
continueRun: options.continueRun,
|
|
25
|
+
clientTools: options.clientTools,
|
|
26
|
+
clientToolResults: options.clientToolResults,
|
|
25
27
|
cwd: options.cwd,
|
|
26
28
|
sessionId: options.sessionId,
|
|
27
29
|
abortController,
|
|
@@ -50,6 +52,7 @@ export function query(options) {
|
|
|
50
52
|
memory: options.memory,
|
|
51
53
|
permissionRules: options.permissionRules,
|
|
52
54
|
onPermissionAsk: options.onPermissionAsk,
|
|
55
|
+
onAskUser: options.onAskUser,
|
|
53
56
|
settings: options.settings,
|
|
54
57
|
skills: options.skills,
|
|
55
58
|
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const DESCRIPTION = `Ask the user a multiple-choice question and wait for their answer. Use ONLY when you hit a decision that's genuinely the user's to make (choosing between distinct approaches, confirming ambiguous scope) and you can't resolve it from the request or sensible defaults. Provide 2-4 concrete, mutually-exclusive options. Prefer acting on a reasonable default over asking.`;
|
|
2
|
+
export const askUserQuestion = {
|
|
3
|
+
def: {
|
|
4
|
+
type: 'function',
|
|
5
|
+
function: {
|
|
6
|
+
name: 'ask_user_question',
|
|
7
|
+
description: DESCRIPTION,
|
|
8
|
+
parameters: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
question: { type: 'string', description: 'The question to ask the user.' },
|
|
12
|
+
header: { type: 'string', description: 'Very short label/chip for the question (max ~12 chars).' },
|
|
13
|
+
options: {
|
|
14
|
+
type: 'array',
|
|
15
|
+
description: '2-4 options the user can choose from.',
|
|
16
|
+
items: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
label: { type: 'string', description: 'Concise choice text (1-5 words).' },
|
|
20
|
+
description: { type: 'string', description: 'What this option means / its trade-off.' },
|
|
21
|
+
},
|
|
22
|
+
required: ['label'],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
multiSelect: { type: 'boolean', description: 'Allow selecting multiple options.' },
|
|
26
|
+
},
|
|
27
|
+
required: ['question', 'options'],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
async run(input, ctx) {
|
|
32
|
+
if (!ctx.askUser) {
|
|
33
|
+
return {
|
|
34
|
+
content: 'Interactive questions are unavailable in this environment. Choose the most reasonable option yourself, state the assumption, and continue.',
|
|
35
|
+
isError: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const question = String(input.question ?? '').trim();
|
|
39
|
+
const raw = Array.isArray(input.options) ? input.options : [];
|
|
40
|
+
const options = raw.map((o) => typeof o === 'string'
|
|
41
|
+
? { label: o }
|
|
42
|
+
: { label: String(o.label ?? ''), description: o.description ? String(o.description) : undefined });
|
|
43
|
+
if (!question || !options.length)
|
|
44
|
+
return { content: 'Error: `question` and `options` are required.', isError: true };
|
|
45
|
+
const answer = await ctx.askUser({
|
|
46
|
+
question,
|
|
47
|
+
header: input.header ? String(input.header) : undefined,
|
|
48
|
+
options,
|
|
49
|
+
multiSelect: !!input.multiSelect,
|
|
50
|
+
});
|
|
51
|
+
const chosen = Array.isArray(answer) ? answer.join(', ') : String(answer);
|
|
52
|
+
return { content: `User answered "${question}" → ${chosen || '(no selection)'}` };
|
|
53
|
+
},
|
|
54
|
+
};
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export { bash, readFile, writeFile, editFile, deleteFile, listFiles, glob, grep
|
|
|
20
20
|
export { multiEdit, notebookEdit, todoWrite, webFetch, webSearch, toolSearch, config };
|
|
21
21
|
export { walk, globToRegExp, joinPath, DEFAULT_IGNORE } from './walk.js';
|
|
22
22
|
export { defineTool, type DefineToolSpec } from './define.js';
|
|
23
|
+
export { askUserQuestion } from './ask_user.js';
|
|
23
24
|
/** Every built-in Claude Code tool, ready to pass to `query()`. */
|
|
24
25
|
export declare const ALL_CLAUDE_CODE_TOOLS: Tool[];
|
|
25
26
|
/** Extract the OpenAI-shape definitions to send to the LLM. */
|
package/dist/tools/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export { bash, readFile, writeFile, editFile, deleteFile, listFiles, glob, grep
|
|
|
17
17
|
export { multiEdit, notebookEdit, todoWrite, webFetch, webSearch, toolSearch, config };
|
|
18
18
|
export { walk, globToRegExp, joinPath, DEFAULT_IGNORE } from './walk.js';
|
|
19
19
|
export { defineTool } from './define.js';
|
|
20
|
+
export { askUserQuestion } from './ask_user.js';
|
|
20
21
|
/** Every built-in Claude Code tool, ready to pass to `query()`. */
|
|
21
22
|
export const ALL_CLAUDE_CODE_TOOLS = [
|
|
22
23
|
bash,
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -41,6 +41,16 @@ export interface ToolContext {
|
|
|
41
41
|
text: string;
|
|
42
42
|
isError?: boolean;
|
|
43
43
|
}>;
|
|
44
|
+
/** Ask the user a multiple-choice question (wired from query({ onAskUser })). */
|
|
45
|
+
askUser?: (q: {
|
|
46
|
+
question: string;
|
|
47
|
+
header?: string;
|
|
48
|
+
options: Array<{
|
|
49
|
+
label: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
}>;
|
|
52
|
+
multiSelect?: boolean;
|
|
53
|
+
}) => Promise<string | string[]>;
|
|
44
54
|
/** Background task manager, present when background tasks are enabled. */
|
|
45
55
|
background?: import('../background/manager.js').BackgroundTaskManager;
|
|
46
56
|
/** Inter-agent mailbox, present when teammates are enabled. Shared with sub-agents. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anyclaude-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|