anyclaude-sdk 0.2.0 → 0.3.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/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
- yield {
833
- type: 'user',
834
- message: { role: 'user', content: toolResultBlocks },
835
- parent_tool_use_id: null,
836
- isSynthetic: true,
837
- timestamp: new Date().toISOString(),
838
- uuid: uuid(),
839
- session_id: sessionId,
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: hit the time budget. The transcript is persisted (above); signal
906
- // the client to continue the run in a fresh invocation (resume + continueRun).
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/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,2 @@
1
+ import type { Tool } from './types.js';
2
+ export declare const askUserQuestion: Tool;
@@ -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
+ };
@@ -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. */
@@ -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,
@@ -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.2.0",
3
+ "version": "0.3.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",