Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
claude-code Tool.ts
30 kB · 792 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793import type { ToolResultBlockParam, ToolUseBlockParam,} from '@anthropic-ai/sdk/resources/index.mjs'import type { ElicitRequestURLParams, ElicitResult,} from '@modelcontextprotocol/sdk/types.js'import type { UUID } from 'crypto'import type { z } from 'zod/v4'import type { Command } from './commands.js'import type { CanUseToolFn } from './hooks/useCanUseTool.js'import type { ThinkingConfig } from './utils/thinking.js'
export type ToolInputJSONSchema = { [x: string]: unknown type: 'object' properties?: { [x: string]: unknown }}
import type { Notification } from './context/notifications.js'import type { MCPServerConnection, ServerResource,} from './services/mcp/types.js'import type { AgentDefinition, AgentDefinitionsResult,} from './tools/AgentTool/loadAgentsDir.js'import type { AssistantMessage, AttachmentMessage, Message, ProgressMessage, SystemLocalCommandMessage, SystemMessage, UserMessage,} from './types/message.js'// Import permission types from centralized location to break import cycles// Import PermissionResult from centralized location to break import cyclesimport type { AdditionalWorkingDirectory, PermissionMode, PermissionResult,} from './types/permissions.js'// Import tool progress types from centralized location to break import cyclesimport type { AgentToolProgress, BashProgress, MCPProgress, REPLToolProgress, SkillToolProgress, TaskOutputProgress, ToolProgressData, WebSearchProgress,} from './types/tools.js'import type { FileStateCache } from './utils/fileStateCache.js'import type { DenialTrackingState } from './utils/permissions/denialTracking.js'import type { SystemPrompt } from './utils/systemPromptType.js'import type { ContentReplacementState } from './utils/toolResultStorage.js'
// Re-export progress types for backwards compatibilityexport type { AgentToolProgress, BashProgress, MCPProgress, REPLToolProgress, SkillToolProgress, TaskOutputProgress, WebSearchProgress,}
import type { SpinnerMode } from './components/Spinner.js'import type { QuerySource } from './constants/querySource.js'import type { SDKStatus } from './entrypoints/agentSdkTypes.js'import type { AppState } from './state/AppState.js'import type { HookProgress, PromptRequest, PromptResponse,} from './types/hooks.js'import type { AgentId } from './types/ids.js'import type { DeepImmutable } from './types/utils.js'import type { AttributionState } from './utils/commitAttribution.js'import type { FileHistoryState } from './utils/fileHistory.js'import type { Theme, ThemeName } from './utils/theme.js'
export type QueryChainTracking = { chainId: string depth: number}
export type ValidationResult = | { result: true } | { result: false message: string errorCode: number }
export type SetToolJSXFn = ( args: { jsx: React.ReactNode | null shouldHidePromptInput: boolean shouldContinueAnimation?: true showSpinner?: boolean isLocalJSXCommand?: boolean isImmediate?: boolean /** Set to true to clear a local JSX command (e.g., from its onDone callback) */ clearLocalJSX?: boolean } | null,) => void
// Import tool permission types from centralized location to break import cyclesimport type { ToolPermissionRulesBySource } from './types/permissions.js'
// Re-export for backwards compatibilityexport type { ToolPermissionRulesBySource }
// Apply DeepImmutable to the imported typeexport type ToolPermissionContext = DeepImmutable<{ mode: PermissionMode additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory> alwaysAllowRules: ToolPermissionRulesBySource alwaysDenyRules: ToolPermissionRulesBySource alwaysAskRules: ToolPermissionRulesBySource isBypassPermissionsModeAvailable: boolean isAutoModeAvailable?: boolean strippedDangerousRules?: ToolPermissionRulesBySource /** When true, permission prompts are auto-denied (e.g., background agents that can't show UI) */ shouldAvoidPermissionPrompts?: boolean /** When true, automated checks (classifier, hooks) are awaited before showing the permission dialog (coordinator workers) */ awaitAutomatedChecksBeforeDialog?: boolean /** Stores the permission mode before model-initiated plan mode entry, so it can be restored on exit */ prePlanMode?: PermissionMode}>
export const getEmptyToolPermissionContext: () => ToolPermissionContext = () => ({ mode: 'default', additionalWorkingDirectories: new Map(), alwaysAllowRules: {}, alwaysDenyRules: {}, alwaysAskRules: {}, isBypassPermissionsModeAvailable: false, })
export type CompactProgressEvent = | { type: 'hooks_start' hookType: 'pre_compact' | 'post_compact' | 'session_start' } | { type: 'compact_start' } | { type: 'compact_end' }
export type ToolUseContext = { options: { commands: Command[] debug: boolean mainLoopModel: string tools: Tools verbose: boolean thinkingConfig: ThinkingConfig mcpClients: MCPServerConnection[] mcpResources: Record<string, ServerResource[]> isNonInteractiveSession: boolean agentDefinitions: AgentDefinitionsResult maxBudgetUsd?: number /** Custom system prompt that replaces the default system prompt */ customSystemPrompt?: string /** Additional system prompt appended after the main system prompt */ appendSystemPrompt?: string /** Override querySource for analytics tracking */ querySource?: QuerySource /** Optional callback to get the latest tools (e.g., after MCP servers connect mid-query) */ refreshTools?: () => Tools } abortController: AbortController readFileState: FileStateCache getAppState(): AppState setAppState(f: (prev: AppState) => AppState): void /** * Always-shared setAppState for session-scoped infrastructure (background * tasks, session hooks). Unlike setAppState, which is no-op for async agents * (see createSubagentContext), this always reaches the root store so agents * at any nesting depth can register/clean up infrastructure that outlives * a single turn. Only set by createSubagentContext; main-thread contexts * fall back to setAppState. */ setAppStateForTasks?: (f: (prev: AppState) => AppState) => void /** * Optional handler for URL elicitations triggered by tool call errors (-32042). * In print/SDK mode, this delegates to structuredIO.handleElicitation. * In REPL mode, this is undefined and the queue-based UI path is used. */ handleElicitation?: ( serverName: string, params: ElicitRequestURLParams, signal: AbortSignal, ) => Promise<ElicitResult> setToolJSX?: SetToolJSXFn addNotification?: (notif: Notification) => void /** Append a UI-only system message to the REPL message list. Stripped at the * normalizeMessagesForAPI boundary — the Exclude<> makes that type-enforced. */ appendSystemMessage?: ( msg: Exclude<SystemMessage, SystemLocalCommandMessage>, ) => void /** Send an OS-level notification (iTerm2, Kitty, Ghostty, bell, etc.) */ sendOSNotification?: (opts: { message: string notificationType: string }) => void nestedMemoryAttachmentTriggers?: Set<string> /** * CLAUDE.md paths already injected as nested_memory attachments this * session. Dedup for memoryFilesToAttachments — readFileState is an LRU * that evicts entries in busy sessions, so its .has() check alone can * re-inject the same CLAUDE.md dozens of times. */ loadedNestedMemoryPaths?: Set<string> dynamicSkillDirTriggers?: Set<string> /** Skill names surfaced via skill_discovery this session. Telemetry only (feeds was_discovered). */ discoveredSkillNames?: Set<string> userModified?: boolean setInProgressToolUseIDs: (f: (prev: Set<string>) => Set<string>) => void /** Only wired in interactive (REPL) contexts; SDK/QueryEngine don't set this. */ setHasInterruptibleToolInProgress?: (v: boolean) => void setResponseLength: (f: (prev: number) => number) => void /** Ant-only: push a new API metrics entry for OTPS tracking. * Called by subagent streaming when a new API request starts. */ pushApiMetricsEntry?: (ttftMs: number) => void setStreamMode?: (mode: SpinnerMode) => void onCompactProgress?: (event: CompactProgressEvent) => void setSDKStatus?: (status: SDKStatus) => void openMessageSelector?: () => void updateFileHistoryState: ( updater: (prev: FileHistoryState) => FileHistoryState, ) => void updateAttributionState: ( updater: (prev: AttributionState) => AttributionState, ) => void setConversationId?: (id: UUID) => void agentId?: AgentId // Only set for subagents; use getSessionId() for session ID. Hooks use this to distinguish subagent calls. agentType?: string // Subagent type name. For the main thread's --agent type, hooks fall back to getMainThreadAgentType(). /** When true, canUseTool must always be called even when hooks auto-approve. * Used by speculation for overlay file path rewriting. */ requireCanUseTool?: boolean messages: Message[] fileReadingLimits?: { maxTokens?: number maxSizeBytes?: number } globLimits?: { maxResults?: number } toolDecisions?: Map< string, { source: string decision: 'accept' | 'reject' timestamp: number } > queryTracking?: QueryChainTracking /** Callback factory for requesting interactive prompts from the user. * Returns a prompt callback bound to the given source name. * Only available in interactive (REPL) contexts. */ requestPrompt?: ( sourceName: string, toolInputSummary?: string | null, ) => (request: PromptRequest) => Promise<PromptResponse> toolUseId?: string criticalSystemReminder_EXPERIMENTAL?: string /** When true, preserve toolUseResult on messages even for subagents. * Used by in-process teammates whose transcripts are viewable by the user. */ preserveToolUseResults?: boolean /** Local denial tracking state for async subagents whose setAppState is a * no-op. Without this, the denial counter never accumulates and the * fallback-to-prompting threshold is never reached. Mutable — the * permissions code updates it in place. */ localDenialTracking?: DenialTrackingState /** * Per-conversation-thread content replacement state for the tool result * budget. When present, query.ts applies the aggregate tool result budget. * Main thread: REPL provisions once (never resets — stale UUID keys * are inert). Subagents: createSubagentContext clones the parent's state * by default (cache-sharing forks need identical decisions), or * resumeAgentBackground threads one reconstructed from sidechain records. */ contentReplacementState?: ContentReplacementState /** * Parent's rendered system prompt bytes, frozen at turn start. * Used by fork subagents to share the parent's prompt cache — re-calling * getSystemPrompt() at fork-spawn time can diverge (GrowthBook cold→warm) * and bust the cache. See forkSubagent.ts. */ renderedSystemPrompt?: SystemPrompt}
// Re-export ToolProgressData from centralized locationexport type { ToolProgressData }
export type Progress = ToolProgressData | HookProgress
export type ToolProgress<P extends ToolProgressData> = { toolUseID: string data: P}
export function filterToolProgressMessages( progressMessagesForMessage: ProgressMessage[],): ProgressMessage<ToolProgressData>[] { return progressMessagesForMessage.filter( (msg): msg is ProgressMessage<ToolProgressData> => msg.data?.type !== 'hook_progress', )}
export type ToolResult<T> = { data: T newMessages?: ( | UserMessage | AssistantMessage | AttachmentMessage | SystemMessage )[] // contextModifier is only honored for tools that aren't concurrency safe. contextModifier?: (context: ToolUseContext) => ToolUseContext /** MCP protocol metadata (structuredContent, _meta) to pass through to SDK consumers */ mcpMeta?: { _meta?: Record<string, unknown> structuredContent?: Record<string, unknown> }}
export type ToolCallProgress<P extends ToolProgressData = ToolProgressData> = ( progress: ToolProgress<P>,) => void
// Type for any schema that outputs an object with string keysexport type AnyObject = z.ZodType<{ [key: string]: unknown }>
/** * Checks if a tool matches the given name (primary name or alias). */export function toolMatchesName( tool: { name: string; aliases?: string[] }, name: string,): boolean { return tool.name === name || (tool.aliases?.includes(name) ?? false)}
/** * Finds a tool by name or alias from a list of tools. */export function findToolByName(tools: Tools, name: string): Tool | undefined { return tools.find(t => toolMatchesName(t, name))}
export type Tool< Input extends AnyObject = AnyObject, Output = unknown, P extends ToolProgressData = ToolProgressData,> = { /** * Optional aliases for backwards compatibility when a tool is renamed. * The tool can be looked up by any of these names in addition to its primary name. */ aliases?: string[] /** * One-line capability phrase used by ToolSearch for keyword matching. * Helps the model find this tool via keyword search when it's deferred. * 3–10 words, no trailing period. * Prefer terms not already in the tool name (e.g. 'jupyter' for NotebookEdit). */ searchHint?: string call( args: z.infer<Input>, context: ToolUseContext, canUseTool: CanUseToolFn, parentMessage: AssistantMessage, onProgress?: ToolCallProgress<P>, ): Promise<ToolResult<Output>> description( input: z.infer<Input>, options: { isNonInteractiveSession: boolean toolPermissionContext: ToolPermissionContext tools: Tools }, ): Promise<string> readonly inputSchema: Input // Type for MCP tools that can specify their input schema directly in JSON Schema format // rather than converting from Zod schema readonly inputJSONSchema?: ToolInputJSONSchema // Optional because TungstenTool doesn't define this. TODO: Make it required. // When we do that, we can also go through and make this a bit more type-safe. outputSchema?: z.ZodType<unknown> inputsEquivalent?(a: z.infer<Input>, b: z.infer<Input>): boolean isConcurrencySafe(input: z.infer<Input>): boolean isEnabled(): boolean isReadOnly(input: z.infer<Input>): boolean /** Defaults to false. Only set when the tool performs irreversible operations (delete, overwrite, send). */ isDestructive?(input: z.infer<Input>): boolean /** * What should happen when the user submits a new message while this tool * is running. * * - `'cancel'` — stop the tool and discard its result * - `'block'` — keep running; the new message waits * * Defaults to `'block'` when not implemented. */ interruptBehavior?(): 'cancel' | 'block' /** * Returns information about whether this tool use is a search or read operation * that should be collapsed into a condensed display in the UI. Examples include * file searching (Grep, Glob), file reading (Read), and bash commands like find, * grep, wc, etc. * * Returns an object indicating whether the operation is a search or read operation: * - `isSearch: true` for search operations (grep, find, glob patterns) * - `isRead: true` for read operations (cat, head, tail, file read) * - `isList: true` for directory-listing operations (ls, tree, du) * - All can be false if the operation shouldn't be collapsed */ isSearchOrReadCommand?(input: z.infer<Input>): { isSearch: boolean isRead: boolean isList?: boolean } isOpenWorld?(input: z.infer<Input>): boolean requiresUserInteraction?(): boolean isMcp?: boolean isLsp?: boolean /** * When true, this tool is deferred (sent with defer_loading: true) and requires * ToolSearch to be used before it can be called. */ readonly shouldDefer?: boolean /** * When true, this tool is never deferred — its full schema appears in the * initial prompt even when ToolSearch is enabled. For MCP tools, set via * `_meta['anthropic/alwaysLoad']`. Use for tools the model must see on * turn 1 without a ToolSearch round-trip. */ readonly alwaysLoad?: boolean /** * For MCP tools: the server and tool names as received from the MCP server (unnormalized). * Present on all MCP tools regardless of whether `name` is prefixed (mcp__server__tool) * or unprefixed (CLAUDE_AGENT_SDK_MCP_NO_PREFIX mode). */ mcpInfo?: { serverName: string; toolName: string } readonly name: string /** * Maximum size in characters for tool result before it gets persisted to disk. * When exceeded, the result is saved to a file and Claude receives a preview * with the file path instead of the full content. * * Set to Infinity for tools whose output must never be persisted (e.g. Read, * where persisting creates a circular Read→file→Read loop and the tool * already self-bounds via its own limits). */ maxResultSizeChars: number /** * When true, enables strict mode for this tool, which causes the API to * more strictly adhere to tool instructions and parameter schemas. * Only applied when the tengu_tool_pear is enabled. */ readonly strict?: boolean
/** * Called on copies of tool_use input before observers see it (SDK stream, * transcript, canUseTool, PreToolUse/PostToolUse hooks). Mutate in place * to add legacy/derived fields. Must be idempotent. The original API-bound * input is never mutated (preserves prompt cache). Not re-applied when a * hook/permission returns a fresh updatedInput — those own their shape. */ backfillObservableInput?(input: Record<string, unknown>): void
/** * Determines if this tool is allowed to run with this input in the current context. * It informs the model of why the tool use failed, and does not directly display any UI. * @param input * @param context */ validateInput?( input: z.infer<Input>, context: ToolUseContext, ): Promise<ValidationResult>
/** * Determines if the user is asked for permission. Only called after validateInput() passes. * General permission logic is in permissions.ts. This method contains tool-specific logic. * @param input * @param context */ checkPermissions( input: z.infer<Input>, context: ToolUseContext, ): Promise<PermissionResult>
// Optional method for tools that operate on a file path getPath?(input: z.infer<Input>): string
/** * Prepare a matcher for hook `if` conditions (permission-rule patterns like * "git *" from "Bash(git *)"). Called once per hook-input pair; any * expensive parsing happens here. Returns a closure that is called per * hook pattern. If not implemented, only tool-name-level matching works. */ preparePermissionMatcher?( input: z.infer<Input>, ): Promise<(pattern: string) => boolean>
prompt(options: { getToolPermissionContext: () => Promise<ToolPermissionContext> tools: Tools agents: AgentDefinition[] allowedAgentTypes?: string[] }): Promise<string> userFacingName(input: Partial<z.infer<Input>> | undefined): string userFacingNameBackgroundColor?( input: Partial<z.infer<Input>> | undefined, ): keyof Theme | undefined /** * Transparent wrappers (e.g. REPL) delegate all rendering to their progress * handler, which emits native-looking blocks for each inner tool call. * The wrapper itself shows nothing. */ isTransparentWrapper?(): boolean /** * Returns a short string summary of this tool use for display in compact views. * @param input The tool input * @returns A short string summary, or null to not display */ getToolUseSummary?(input: Partial<z.infer<Input>> | undefined): string | null /** * Returns a human-readable present-tense activity description for spinner display. * Example: "Reading src/foo.ts", "Running bun test", "Searching for pattern" * @param input The tool input * @returns Activity description string, or null to fall back to tool name */ getActivityDescription?( input: Partial<z.infer<Input>> | undefined, ): string | null /** * Returns a compact representation of this tool use for the auto-mode * security classifier. Examples: `ls -la` for Bash, `/tmp/x: new content` * for Edit. Return '' to skip this tool in the classifier transcript * (e.g. tools with no security relevance). May return an object to avoid * double-encoding when the caller JSON-wraps the value. */ toAutoClassifierInput(input: z.infer<Input>): unknown mapToolResultToToolResultBlockParam( content: Output, toolUseID: string, ): ToolResultBlockParam /** * Optional. When omitted, the tool result renders nothing (same as returning * null). Omit for tools whose results are surfaced elsewhere (e.g., TodoWrite * updates the todo panel, not the transcript). */ renderToolResultMessage?( content: Output, progressMessagesForMessage: ProgressMessage<P>[], options: { style?: 'condensed' theme: ThemeName tools: Tools verbose: boolean isTranscriptMode?: boolean isBriefOnly?: boolean /** Original tool_use input, when available. Useful for compact result * summaries that reference what was requested (e.g. "Sent to #foo"). */ input?: unknown }, ): React.ReactNode /** * Flattened text of what renderToolResultMessage shows IN TRANSCRIPT * MODE (verbose=true, isTranscriptMode=true). For transcript search * indexing: the index counts occurrences in this string, the highlight * overlay scans the actual screen buffer. For count ≡ highlight, this * must return the text that ends up visible — not the model-facing * serialization from mapToolResultToToolResultBlockParam (which adds * system-reminders, persisted-output wrappers). * * Chrome can be skipped (under-count is fine). "Found 3 files in 12ms" * isn't worth indexing. Phantoms are not fine — text that's claimed * here but doesn't render is a count≠highlight bug. * * Optional: omitted → field-name heuristic in transcriptSearch.ts. * Drift caught by test/utils/transcriptSearch.renderFidelity.test.tsx * which renders sample outputs and flags text that's indexed-but-not- * rendered (phantom) or rendered-but-not-indexed (under-count warning). */ extractSearchText?(out: Output): string /** * Render the tool use message. Note that `input` is partial because we render * the message as soon as possible, possibly before tool parameters have fully * streamed in. */ renderToolUseMessage( input: Partial<z.infer<Input>>, options: { theme: ThemeName; verbose: boolean; commands?: Command[] }, ): React.ReactNode /** * Returns true when the non-verbose rendering of this output is truncated * (i.e., clicking to expand would reveal more content). Gates * click-to-expand in fullscreen — only messages where verbose actually * shows more get a hover/click affordance. Unset means never truncated. */ isResultTruncated?(output: Output): boolean /** * Renders an optional tag to display after the tool use message. * Used for additional metadata like timeout, model, resume ID, etc. * Returns null to not display anything. */ renderToolUseTag?(input: Partial<z.infer<Input>>): React.ReactNode /** * Optional. When omitted, no progress UI is shown while the tool runs. */ renderToolUseProgressMessage?( progressMessagesForMessage: ProgressMessage<P>[], options: { tools: Tools verbose: boolean terminalSize?: { columns: number; rows: number } inProgressToolCallCount?: number isTranscriptMode?: boolean }, ): React.ReactNode renderToolUseQueuedMessage?(): React.ReactNode /** * Optional. When omitted, falls back to <FallbackToolUseRejectedMessage />. * Only define this for tools that need custom rejection UI (e.g., file edits * that show the rejected diff). */ renderToolUseRejectedMessage?( input: z.infer<Input>, options: { columns: number messages: Message[] style?: 'condensed' theme: ThemeName tools: Tools verbose: boolean progressMessagesForMessage: ProgressMessage<P>[] isTranscriptMode?: boolean }, ): React.ReactNode /** * Optional. When omitted, falls back to <FallbackToolUseErrorMessage />. * Only define this for tools that need custom error UI (e.g., search tools * that show "File not found" instead of the raw error). */ renderToolUseErrorMessage?( result: ToolResultBlockParam['content'], options: { progressMessagesForMessage: ProgressMessage<P>[] tools: Tools verbose: boolean isTranscriptMode?: boolean }, ): React.ReactNode
/** * Renders multiple parallel instances of this tool as a group. * @returns React node to render, or null to fall back to individual rendering */ /** * Renders multiple tool uses as a group (non-verbose mode only). * In verbose mode, individual tool uses render at their original positions. * @returns React node to render, or null to fall back to individual rendering */ renderGroupedToolUse?( toolUses: Array<{ param: ToolUseBlockParam isResolved: boolean isError: boolean isInProgress: boolean progressMessages: ProgressMessage<P>[] result?: { param: ToolResultBlockParam output: unknown } }>, options: { shouldAnimate: boolean tools: Tools }, ): React.ReactNode | null}
/** * A collection of tools. Use this type instead of `Tool[]` to make it easier * to track where tool sets are assembled, passed, and filtered across the codebase. */export type Tools = readonly Tool[]
/** * Methods that `buildTool` supplies a default for. A `ToolDef` may omit these; * the resulting `Tool` always has them. */type DefaultableToolKeys = | 'isEnabled' | 'isConcurrencySafe' | 'isReadOnly' | 'isDestructive' | 'checkPermissions' | 'toAutoClassifierInput' | 'userFacingName'
/** * Tool definition accepted by `buildTool`. Same shape as `Tool` but with the * defaultable methods optional — `buildTool` fills them in so callers always * see a complete `Tool`. */export type ToolDef< Input extends AnyObject = AnyObject, Output = unknown, P extends ToolProgressData = ToolProgressData,> = Omit<Tool<Input, Output, P>, DefaultableToolKeys> & Partial<Pick<Tool<Input, Output, P>, DefaultableToolKeys>>
/** * Type-level spread mirroring `{ ...TOOL_DEFAULTS, ...def }`. For each * defaultable key: if D provides it (required), D's type wins; if D omits * it or has it optional (inherited from Partial<> in the constraint), the * default fills in. All other keys come from D verbatim — preserving arity, * optional presence, and literal types exactly as `satisfies Tool` did. */type BuiltTool<D> = Omit<D, DefaultableToolKeys> & { [K in DefaultableToolKeys]-?: K extends keyof D ? undefined extends D[K] ? ToolDefaults[K] : D[K] : ToolDefaults[K]}
/** * Build a complete `Tool` from a partial definition, filling in safe defaults * for the commonly-stubbed methods. All tool exports should go through this so * that defaults live in one place and callers never need `?.() ?? default`. * * Defaults (fail-closed where it matters): * - `isEnabled` → `true` * - `isConcurrencySafe` → `false` (assume not safe) * - `isReadOnly` → `false` (assume writes) * - `isDestructive` → `false` * - `checkPermissions` → `{ behavior: 'allow', updatedInput }` (defer to general permission system) * - `toAutoClassifierInput` → `''` (skip classifier — security-relevant tools must override) * - `userFacingName` → `name` */const TOOL_DEFAULTS = { isEnabled: () => true, isConcurrencySafe: (_input?: unknown) => false, isReadOnly: (_input?: unknown) => false, isDestructive: (_input?: unknown) => false, checkPermissions: ( input: { [key: string]: unknown }, _ctx?: ToolUseContext, ): Promise<PermissionResult> => Promise.resolve({ behavior: 'allow', updatedInput: input }), toAutoClassifierInput: (_input?: unknown) => '', userFacingName: (_input?: unknown) => '',}
// The defaults type is the ACTUAL shape of TOOL_DEFAULTS (optional params so// both 0-arg and full-arg call sites type-check — stubs varied in arity and// tests relied on that), not the interface's strict signatures.type ToolDefaults = typeof TOOL_DEFAULTS
// D infers the concrete object-literal type from the call site. The// constraint provides contextual typing for method parameters; `any` in// constraint position is structural and never leaks into the return type.// BuiltTool<D> mirrors runtime `{...TOOL_DEFAULTS, ...def}` at the type level.// eslint-disable-next-line @typescript-eslint/no-explicit-anytype AnyToolDef = ToolDef<any, any, any>
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> { // The runtime spread is straightforward; the `as` bridges the gap between // the structural-any constraint and the precise BuiltTool<D> return. The // type semantics are proven by the 0-error typecheck across all 60+ tools. return { ...TOOL_DEFAULTS, userFacingName: () => def.name, ...def, } as BuiltTool<D>}