From 91f93ce61533a9cadd1d107e09a442451c09f5db Mon Sep 17 00:00:00 2001 From: emsanakhchivan Date: Wed, 29 Apr 2026 10:53:01 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20SDK=20Foundation=20=E2=80=94=20Type=20D?= =?UTF-8?q?eclarations,=20Errors,=20and=20Utilities=20(#866)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(sdk): add SDK foundation — type declarations, errors, and utilities Adds standalone SDK building blocks with no SDK source dependencies: - sdk.d.ts: ambient type declarations for SDK bundle - coreSchemas.ts + coreTypes.generated.ts: Zod schemas and generated types - errors.ts: SDK-specific error classes - validation.ts: input validation utilities - messageFilters.ts: extracted message filter logic - handlePromptSubmit.ts: imports from messageFilters - 16 generated-types tests * fix(sdk): narrow assertFunction type from broad Function to callable signature Code review finding: assertFunction used `asserts value is Function` which accepts any function-like value without narrowing. Changed to `(...args: any[]) => any` for better type safety. * fix(sdk): update sdk.d.ts header — manually maintained, not generated Reviewer noted the header said "Generated from index.ts" but no generator produces this file. Updated to "Manually maintained — keep in sync with index.ts". Drift detection added in validate-externals.ts (PR 3). * fix(sdk): align sdk.d.ts types with canonical coreTypes.generated.ts Tighten SDK public type contract to resolve reviewer blockers: - PermissionResult: unknown[] → precise 6-shape discriminated union (addRules/replaceRules/removeRules/setMode/addDirectories/removeDirectories) - SDKSessionInfo: snake_case → camelCase (sessionId, lastModified, etc.) - ForkSessionResult: session_id → sessionId - SDKPermissionRequestMessage: uuid + session_id now required - SDKPermissionTimeoutMessage: added uuid + session_id - SessionMessage: parent_uuid → parentUuid - SDKMessage/SDKUserMessage/SDKResultMessage: replaced loose inline definitions with re-exports from coreTypes.generated.ts --------- Co-authored-by: Ali Alakbarli --- src/entrypoints/sdk.d.ts | 518 +++++ src/entrypoints/sdk/coreSchemas.ts | 15 +- src/entrypoints/sdk/coreTypes.generated.ts | 2357 +++++++++++++++++++- src/utils/errors.ts | 89 + src/utils/handlePromptSubmit.ts | 2 +- src/utils/messageFilters.ts | 81 + src/utils/user.test.ts | 9 +- src/utils/validation.ts | 54 + tests/sdk/generated-types.test.ts | 279 +++ 9 files changed, 3397 insertions(+), 7 deletions(-) create mode 100644 src/entrypoints/sdk.d.ts create mode 100644 src/utils/messageFilters.ts create mode 100644 src/utils/validation.ts create mode 100644 tests/sdk/generated-types.test.ts diff --git a/src/entrypoints/sdk.d.ts b/src/entrypoints/sdk.d.ts new file mode 100644 index 00000000..53a50f2e --- /dev/null +++ b/src/entrypoints/sdk.d.ts @@ -0,0 +1,518 @@ +// Type declarations for @gitlawb/openclaude SDK +// Manually maintained — keep in sync with src/entrypoints/sdk/index.ts +// Drift is caught by validate-externals.ts (runs in CI) + +// ============================================================================ +// Error +// ============================================================================ + +export class AbortError extends Error { + override readonly name: 'AbortError' +} + +export class ClaudeError extends Error { + constructor(message: string) +} + +export class SDKError extends ClaudeError { + constructor(message: string) +} + +export class SDKAuthenticationError extends SDKError { + constructor(message?: string) +} + +export class SDKBillingError extends SDKError { + constructor(message?: string) +} + +export class SDKRateLimitError extends SDKError { + constructor( + message?: string, + readonly resetsAt?: number, + readonly rateLimitType?: string, + ) +} + +export class SDKInvalidRequestError extends SDKError { + constructor(message?: string) +} + +export class SDKServerError extends SDKError { + constructor(message?: string) +} + +export class SDKMaxOutputTokensError extends SDKError { + constructor(message?: string) +} + +export type SDKAssistantMessageError = + | 'authentication_failed' + | 'billing_error' + | 'rate_limit' + | 'invalid_request' + | 'server_error' + | 'unknown' + | 'max_output_tokens' + +export function sdkErrorFromType( + errorType: SDKAssistantMessageError, + message?: string, +): SDKError | ClaudeError + +// ============================================================================ +// Types +// ============================================================================ + +export type ApiKeySource = 'user' | 'project' | 'org' | 'temporary' | 'oauth' | 'none' + +export type RewindFilesResult = { + canRewind: boolean + error?: string + filesChanged?: string[] + insertions?: number + deletions?: number +} + +export type McpServerStatus = { + name: string + status: 'connected' | 'failed' | 'needs-auth' | 'pending' | 'disabled' + serverInfo?: { name: string; version: string } + error?: string + scope?: string + tools?: { + name: string + description?: string + annotations?: { + readOnly?: boolean + destructive?: boolean + openWorld?: boolean + } + }[] +} + +export type PermissionResult = ({ + behavior: 'allow' + updatedInput?: Record + updatedPermissions?: ({ + type: 'addRules' + rules: { toolName: string; ruleContent?: string }[] + behavior: 'allow' | 'deny' | 'ask' + destination: 'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg' + }) | ({ + type: 'replaceRules' + rules: { toolName: string; ruleContent?: string }[] + behavior: 'allow' | 'deny' | 'ask' + destination: 'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg' + }) | ({ + type: 'removeRules' + rules: { toolName: string; ruleContent?: string }[] + behavior: 'allow' | 'deny' | 'ask' + destination: 'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg' + }) | ({ + type: 'setMode' + mode: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk' + destination: 'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg' + }) | ({ + type: 'addDirectories' + directories: string[] + destination: 'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg' + }) | ({ + type: 'removeDirectories' + directories: string[] + destination: 'userSettings' | 'projectSettings' | 'localSettings' | 'session' | 'cliArg' + })[] + toolUseID?: string + decisionClassification?: 'user_temporary' | 'user_permanent' | 'user_reject' +}) | ({ + behavior: 'deny' + message: string + interrupt?: boolean + toolUseID?: string + decisionClassification?: 'user_temporary' | 'user_permanent' | 'user_reject' +}) + +export type SDKSessionInfo = { + sessionId: string + summary: string + lastModified: number + fileSize?: number + customTitle?: string + firstPrompt?: string + gitBranch?: string + cwd?: string + tag?: string + createdAt?: number +} + +export type ListSessionsOptions = { + dir?: string + limit?: number + offset?: number + includeWorktrees?: boolean +} + +export type GetSessionInfoOptions = { + dir?: string +} + +export type GetSessionMessagesOptions = { + dir?: string + limit?: number + offset?: number + includeSystemMessages?: boolean +} + +export type SessionMutationOptions = { + dir?: string +} + +export type ForkSessionOptions = { + dir?: string + upToMessageId?: string + title?: string +} + +export type ForkSessionResult = { + sessionId: string +} + +export type SessionMessage = { + role: 'user' | 'assistant' | 'system' + content: unknown + timestamp?: string + uuid?: string + parentUuid?: string | null + [key: string]: unknown +} + +// Re-export precise SDK message types from generated types +// These use camelCase field names and discriminated unions for full IntelliSense +export type { SDKMessage as SDKMessage } from './sdk/coreTypes.generated.js' +export type { SDKUserMessage as SDKUserMessage } from './sdk/coreTypes.generated.js' +export type { SDKResultMessage as SDKResultMessage } from './sdk/coreTypes.generated.js' + +// ============================================================================ +// Query types +// ============================================================================ + +export type QueryPermissionMode = + | 'default' + | 'plan' + | 'auto-accept' + | 'bypass-permissions' + | 'bypassPermissions' + | 'acceptEdits' + +export type QueryOptions = { + cwd: string + additionalDirectories?: string[] + model?: string + sessionId?: string + /** Fork the session before resuming (requires sessionId). */ + fork?: boolean + /** Alias for fork. When true, resumed session forks to a new session ID. */ + forkSession?: boolean + /** Resume the most recent session for this cwd (no sessionId needed). */ + continue?: boolean + resume?: string + /** When resuming, resume messages up to and including this message UUID. */ + resumeSessionAt?: string + permissionMode?: QueryPermissionMode + abortController?: AbortController + executable?: string + allowDangerouslySkipPermissions?: boolean + disallowedTools?: string[] + hooks?: Record + mcpServers?: Record + settings?: { + env?: Record + attribution?: { commit: string; pr: string } + } + /** Environment variables to apply during query execution. Overrides process.env. Takes precedence over settings.env. */ + env?: Record + /** + * Callback invoked before each tool use. Return `{ behavior: 'allow' }` to + * permit the call or `{ behavior: 'deny', message?: string }` to reject it. + * + * **Secure-by-default**: If neither `canUseTool` nor `onPermissionRequest` + * is provided, ALL tool uses are denied. You MUST provide at least one of + * these callbacks to allow tool execution. + */ + canUseTool?: ( + name: string, + input: unknown, + options?: { toolUseID?: string }, + ) => Promise<{ behavior: 'allow' | 'deny'; message?: string; updatedInput?: unknown }> + /** + * Callback invoked when a tool needs permission approval. The host receives + * the request immediately and can resolve it by calling + * `query.respondToPermission(toolUseId, decision)` before the timeout. + * If omitted, tools that require permission fall through to the default + * permission logic immediately (no timeout). + */ + onPermissionRequest?: (message: SDKPermissionRequestMessage) => void + systemPrompt?: + | string + | { type: 'preset'; preset: string; append?: string } + | { type: 'custom'; content: string } + /** Agent definitions to register with the query engine. */ + agents?: Record + settingSources?: string[] + /** When true, yields stream_event messages for token-by-token streaming. */ + includePartialMessages?: boolean + /** @internal Timeout in ms for permission request resolution. Default 30000. */ + _permissionTimeoutMs?: number + stderr?: (data: string) => void +} + +export interface Query { + readonly sessionId: string + [Symbol.asyncIterator](): AsyncIterator + setModel(model: string): Promise + setPermissionMode(mode: QueryPermissionMode): Promise + close(): void + interrupt(): void + respondToPermission(toolUseId: string, decision: PermissionResult): void + /** Check if file rewind is possible. */ + rewindFiles(): RewindFilesResult + /** Actually perform the file rewind. Returns files changed and diff stats. */ + rewindFilesAsync(): Promise + supportedCommands(): string[] + supportedModels(): string[] + supportedAgents(): string[] + mcpServerStatus(): McpServerStatus[] + accountInfo(): Promise<{ apiKeySource: ApiKeySource; [key: string]: unknown }> + setMaxThinkingTokens(tokens: number): void +} + +/** + * Permission request message emitted when a tool needs permission approval. + * Hosts can respond via respondToPermission() using the request_id. + */ +export type SDKPermissionRequestMessage = { + type: 'permission_request' + request_id: string + tool_name: string + tool_use_id: string + input: Record + uuid: string + session_id: string +} + +export type SDKPermissionTimeoutMessage = { + type: 'permission_timeout' + tool_name: string + tool_use_id: string + timed_out_after_ms: number + uuid: string + session_id: string +} + +// ============================================================================ +// V2 API types +// ============================================================================ + +export type SDKSessionOptions = { + cwd: string + model?: string + permissionMode?: QueryPermissionMode + abortController?: AbortController + /** + * Callback invoked before each tool use. Return `{ behavior: 'allow' }` to + * permit the call or `{ behavior: 'deny', message?: string }` to reject it. + * + * **Secure-by-default**: If neither `canUseTool` nor `onPermissionRequest` + * is provided, ALL tool uses are denied. You MUST provide at least one of + * these callbacks to allow tool execution. + */ + canUseTool?: ( + name: string, + input: unknown, + options?: { toolUseID?: string }, + ) => Promise<{ behavior: 'allow' | 'deny'; message?: string; updatedInput?: unknown }> + /** MCP server configurations for this session. */ + mcpServers?: Record + /** + * Callback invoked when a tool needs permission approval. The host receives + * the request immediately and can resolve it via respondToPermission(). + */ + onPermissionRequest?: (message: SDKPermissionRequestMessage) => void +} + +export interface SDKSession { + sessionId: string + sendMessage(content: string): AsyncIterable + getMessages(): SDKMessage[] + interrupt(): void + /** Respond to a pending permission prompt. */ + respondToPermission(toolUseId: string, decision: PermissionResult): void +} + +// ============================================================================ +// MCP tool types +// ============================================================================ + +export interface SdkMcpToolDefinition { + name: string + description: string + inputSchema: Schema + handler: (args: any, extra: unknown) => Promise + annotations?: any + searchHint?: string + alwaysLoad?: boolean +} + +// ============================================================================ +// Session functions +// ============================================================================ + +export function listSessions( + options?: ListSessionsOptions, +): Promise + +export function getSessionInfo( + sessionId: string, + options?: GetSessionInfoOptions, +): Promise + +export function getSessionMessages( + sessionId: string, + options?: GetSessionMessagesOptions, +): Promise + +export function renameSession( + sessionId: string, + title: string, + options?: SessionMutationOptions, +): Promise + +export function tagSession( + sessionId: string, + tag: string | null, + options?: SessionMutationOptions, +): Promise + +export function forkSession( + sessionId: string, + options?: ForkSessionOptions, +): Promise + +export function deleteSession( + sessionId: string, + options?: SessionMutationOptions, +): Promise + +// ============================================================================ +// Query functions +// ============================================================================ + +export function query(params: { + prompt: string | AsyncIterable + options?: QueryOptions +}): Query + +export function queryAsync(params: { + prompt: string | AsyncIterable + options?: QueryOptions +}): Promise + +// ============================================================================ +// V2 API functions +// ============================================================================ + +export function unstable_v2_createSession(options: SDKSessionOptions): SDKSession + +export function unstable_v2_resumeSession( + sessionId: string, + options: SDKSessionOptions, +): Promise + +export function unstable_v2_prompt( + message: string, + options: SDKSessionOptions, +): Promise + +// ============================================================================ +// MCP tool functions +// ============================================================================ + +export function tool( + name: string, + description: string, + inputSchema: Schema, + handler: (args: any, extra: unknown) => Promise, + extras?: { + annotations?: any + searchHint?: string + alwaysLoad?: boolean + }, +): SdkMcpToolDefinition + +/** + * MCP server transport configuration types. + * Matches McpServerConfigForProcessTransport from coreTypes.generated.ts. + */ +export type SdkMcpStdioConfig = { + type?: "stdio" + command: string + args?: string[] + env?: Record +} + +export type SdkMcpSSEConfig = { + type: "sse" + url: string + headers?: Record +} + +export type SdkMcpHttpConfig = { + type: "http" + url: string + headers?: Record +} + +export type SdkMcpSdkConfig = { + type: "sdk" + name: string +} + +export type SdkMcpServerConfig = SdkMcpStdioConfig | SdkMcpSSEConfig | SdkMcpHttpConfig | SdkMcpSdkConfig + +/** + * Scoped MCP server config with session scope. + * Returned by createSdkMcpServer() for use with mcpServers option. + */ +export type SdkScopedMcpServerConfig = SdkMcpServerConfig & { + scope: "session" +} + +/** + * Wraps an MCP server configuration for use with the SDK. + * Adds the 'session' scope marker so the SDK knows this server + * should be connected per-session (not globally). + * + * @param config - MCP server config (stdio, sse, http, or sdk type) + * @returns Scoped config with scope: 'session' added + * + * @example + * ```typescript + * const server = createSdkMcpServer({ + * type: 'stdio', + * command: 'npx', + * args: ['-y', '@modelcontextprotocol/server-filesystem', '/tmp'], + * }) + * const session = unstable_v2_createSession({ + * cwd: '/my/project', + * mcpServers: { 'fs': server }, + * }) + * ``` + */ +export function createSdkMcpServer(config: SdkMcpServerConfig): SdkScopedMcpServerConfig diff --git a/src/entrypoints/sdk/coreSchemas.ts b/src/entrypoints/sdk/coreSchemas.ts index 4d5b9d0a..36ee996f 100644 --- a/src/entrypoints/sdk/coreSchemas.ts +++ b/src/entrypoints/sdk/coreSchemas.ts @@ -55,7 +55,7 @@ export const OutputFormatSchema = lazySchema(() => // ============================================================================ export const ApiKeySourceSchema = lazySchema(() => - z.enum(['user', 'project', 'org', 'temporary', 'oauth']), + z.enum(['user', 'project', 'org', 'temporary', 'oauth', 'none']), ) export const ConfigScopeSchema = lazySchema(() => @@ -1851,6 +1851,18 @@ export const SDKSessionInfoSchema = lazySchema(() => .describe('Session metadata returned by listSessions and getSessionInfo.'), ) +export const SDKPermissionRequestMessageSchema = lazySchema(() => + z.object({ + type: z.literal('permission_request'), + request_id: z.string().describe('Unique request ID for this permission prompt'), + tool_name: z.string().describe('Name of the tool requesting permission'), + tool_use_id: z.string().describe('Tool use ID for matching with respondToPermission'), + input: z.record(z.string(), z.unknown()).describe('Tool input parameters'), + uuid: UUIDPlaceholder(), + session_id: z.string(), + }), +) + export const SDKMessageSchema = lazySchema(() => z.union([ SDKAssistantMessageSchema(), @@ -1877,6 +1889,7 @@ export const SDKMessageSchema = lazySchema(() => SDKRateLimitEventSchema(), SDKElicitationCompleteMessageSchema(), SDKPromptSuggestionMessageSchema(), + SDKPermissionRequestMessageSchema(), ]), ) diff --git a/src/entrypoints/sdk/coreTypes.generated.ts b/src/entrypoints/sdk/coreTypes.generated.ts index 16bc5089..0a9438ee 100644 --- a/src/entrypoints/sdk/coreTypes.generated.ts +++ b/src/entrypoints/sdk/coreTypes.generated.ts @@ -1,2 +1,2355 @@ -// Stub — generated types not included in source snapshot -export type {} +// AUTO-GENERATED — do not edit manually. +// Regenerate with: bun scripts/generate-sdk-types.ts +// +// Generated from Zod schemas in coreSchemas.ts + +export type ModelUsage = { + inputTokens: number + outputTokens: number + cacheReadInputTokens: number + cacheCreationInputTokens: number + webSearchRequests: number + costUSD: number + contextWindow: number + maxOutputTokens: number +} + +export type OutputFormatType = "json_schema" + +export type BaseOutputFormat = { + type: "json_schema" +} + +export type JsonSchemaOutputFormat = { + type: "json_schema" + schema: Record +} + +export type OutputFormat = { + type: "json_schema" + schema: Record +} + +export type ApiKeySource = "user" | "project" | "org" | "temporary" | "oauth" | "none" + +/** Config scope for settings. */ +export type ConfigScope = "local" | "user" | "project" + +export type SdkBeta = "context-1m-2025-08-07" + +/** Claude decides when and how much to think (Opus 4.6+). */ +export type ThinkingAdaptive = { + type: "adaptive" +} + +/** Fixed thinking token budget (older models) */ +export type ThinkingEnabled = { + type: "enabled" + budgetTokens?: number +} + +/** No extended thinking */ +export type ThinkingDisabled = { + type: "disabled" +} + +/** Controls Claude's thinking/reasoning behavior. When set, takes precedence over the deprecated maxThinkingTokens. */ +export type ThinkingConfig = ({ + type: "adaptive" +}) | ({ + type: "enabled" + budgetTokens?: number +}) | ({ + type: "disabled" +}) + +export type McpStdioServerConfig = { + type?: "stdio" + command: string + args?: string[] + env?: Record +} + +export type McpSSEServerConfig = { + type: "sse" + url: string + headers?: Record +} + +export type McpHttpServerConfig = { + type: "http" + url: string + headers?: Record +} + +export type McpSdkServerConfig = { + type: "sdk" + name: string +} + +export type McpServerConfigForProcessTransport = ({ + type?: "stdio" + command: string + args?: string[] + env?: Record +}) | ({ + type: "sse" + url: string + headers?: Record +}) | ({ + type: "http" + url: string + headers?: Record +}) | ({ + type: "sdk" + name: string +}) + +export type McpClaudeAIProxyServerConfig = { + type: "claudeai-proxy" + url: string + id: string +} + +export type McpServerStatusConfig = (({ + type?: "stdio" + command: string + args?: string[] + env?: Record +}) | ({ + type: "sse" + url: string + headers?: Record +}) | ({ + type: "http" + url: string + headers?: Record +}) | ({ + type: "sdk" + name: string +})) | ({ + type: "claudeai-proxy" + url: string + id: string +}) + +/** Status information for an MCP server connection. */ +export type McpServerStatus = { + name: string + status: "connected" | "failed" | "needs-auth" | "pending" | "disabled" + serverInfo?: { + name: string + version: string + } + error?: string + config?: (({ + type?: "stdio" + command: string + args?: string[] + env?: Record + }) | ({ + type: "sse" + url: string + headers?: Record + }) | ({ + type: "http" + url: string + headers?: Record + }) | ({ + type: "sdk" + name: string + })) | ({ + type: "claudeai-proxy" + url: string + id: string + }) + scope?: string + tools?: { + name: string + description?: string + annotations?: { + readOnly?: boolean + destructive?: boolean + openWorld?: boolean + } + }[] + capabilities?: { + experimental?: Record + } +} + +/** Result of a setMcpServers operation. */ +export type McpSetServersResult = { + added: string[] + removed: string[] + errors: Record +} + +export type PermissionUpdateDestination = "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + +export type PermissionBehavior = "allow" | "deny" | "ask" + +export type PermissionRuleValue = { + toolName: string + ruleContent?: string +} + +export type PermissionUpdate = ({ + type: "addRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" +}) | ({ + type: "replaceRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" +}) | ({ + type: "removeRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" +}) | ({ + type: "setMode" + mode: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" +}) | ({ + type: "addDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" +}) | ({ + type: "removeDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" +}) + +/** Classification of this permission decision for telemetry. SDK hosts that prompt users (desktop apps, IDEs) should set this to reflect what actually happened: user_temporary for allow-once, user_permanent for always-allow (both the click and later cache hits), user_reject for deny. If unset, the CLI infers conservatively (temporary for allow, reject for deny). The vocabulary matches tool_decision OTel events (monitoring-usage docs). */ +export type PermissionDecisionClassification = "user_temporary" | "user_permanent" | "user_reject" + +export type PermissionResult = ({ + behavior: "allow" + updatedInput?: Record + updatedPermissions?: ({ + type: "addRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "replaceRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "removeRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "setMode" + mode: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "addDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "removeDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + })[] + toolUseID?: string + decisionClassification?: "user_temporary" | "user_permanent" | "user_reject" +}) | ({ + behavior: "deny" + message: string + interrupt?: boolean + toolUseID?: string + decisionClassification?: "user_temporary" | "user_permanent" | "user_reject" +}) + +/** Permission mode for controlling how tool executions are handled. 'default' - Standard behavior, prompts for dangerous operations. 'acceptEdits' - Auto-accept file edit operations. 'bypassPermissions' - Bypass all permission checks (requires allowDangerouslySkipPermissions). 'plan' - Planning mode, no actual tool execution. 'dontAsk' - Don't prompt for permissions, deny if not pre-approved. */ +export type PermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" + +export type HookEvent = "PreToolUse" | "PostToolUse" | "PostToolUseFailure" | "Notification" | "UserPromptSubmit" | "SessionStart" | "SessionEnd" | "Stop" | "StopFailure" | "SubagentStart" | "SubagentStop" | "PreCompact" | "PostCompact" | "PermissionRequest" | "PermissionDenied" | "Setup" | "TeammateIdle" | "TaskCreated" | "TaskCompleted" | "Elicitation" | "ElicitationResult" | "ConfigChange" | "WorktreeCreate" | "WorktreeRemove" | "InstructionsLoaded" | "CwdChanged" | "FileChanged" + +export type BaseHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} + +export type PreToolUseHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PreToolUse" + tool_name: string + tool_input: unknown + tool_use_id: string +} + +export type PostToolUseHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PostToolUse" + tool_name: string + tool_input: unknown + tool_response: unknown + tool_use_id: string +} + +export type PostToolUseFailureHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PostToolUseFailure" + tool_name: string + tool_input: unknown + tool_use_id: string + error: string + is_interrupt?: boolean +} + +export type PermissionDeniedHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PermissionDenied" + tool_name: string + tool_input: unknown + tool_use_id: string + reason: string +} + +export type NotificationHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "Notification" + message: string + title?: string + notification_type: string +} + +export type UserPromptSubmitHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "UserPromptSubmit" + prompt: string +} + +export type SessionStartHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "SessionStart" + source: "startup" | "resume" | "clear" | "compact" + agent_type?: string + model?: string +} + +export type SessionEndHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "SessionEnd" + reason: "clear" | "resume" | "logout" | "prompt_input_exit" | "other" | "bypass_permissions_disabled" +} + +export type StopHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "Stop" + stop_hook_active: boolean + last_assistant_message?: string +} + +export type StopFailureHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "StopFailure" + error: "authentication_failed" | "billing_error" | "rate_limit" | "invalid_request" | "server_error" | "unknown" | "max_output_tokens" + error_details?: string + last_assistant_message?: string +} + +export type SubagentStartHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "SubagentStart" + agent_id: string + agent_type: string +} + +export type SubagentStopHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "SubagentStop" + stop_hook_active: boolean + agent_id: string + agent_transcript_path: string + agent_type: string + last_assistant_message?: string +} + +export type PreCompactHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PreCompact" + trigger: "manual" | "auto" + custom_instructions: string | null +} + +export type PostCompactHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PostCompact" + trigger: "manual" | "auto" + compact_summary: string +} + +export type PermissionRequestHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PermissionRequest" + tool_name: string + tool_input: unknown + permission_suggestions?: ({ + type: "addRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "replaceRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "removeRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "setMode" + mode: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "addDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "removeDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + })[] +} + +export type SetupHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "Setup" + trigger: "init" | "maintenance" +} + +export type TeammateIdleHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "TeammateIdle" + teammate_name: string + team_name: string +} + +export type TaskCreatedHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "TaskCreated" + task_id: string + task_subject: string + task_description?: string + teammate_name?: string + team_name?: string +} + +export type TaskCompletedHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "TaskCompleted" + task_id: string + task_subject: string + task_description?: string + teammate_name?: string + team_name?: string +} + +/** Hook input for the Elicitation event. Fired when an MCP server requests user input. Hooks can auto-respond (accept/decline) instead of showing the dialog. */ +export type ElicitationHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "Elicitation" + mcp_server_name: string + message: string + mode?: "form" | "url" + url?: string + elicitation_id?: string + requested_schema?: Record +} + +/** Hook input for the ElicitationResult event. Fired after the user responds to an MCP elicitation. Hooks can observe or override the response before it is sent to the server. */ +export type ElicitationResultHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "ElicitationResult" + mcp_server_name: string + elicitation_id?: string + mode?: "form" | "url" + action: "accept" | "decline" | "cancel" + content?: Record +} + +export type ConfigChangeHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "ConfigChange" + source: "user_settings" | "project_settings" | "local_settings" | "policy_settings" | "skills" + file_path?: string +} + +export type InstructionsLoadedHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "InstructionsLoaded" + file_path: string + memory_type: "User" | "Project" | "Local" | "Managed" + load_reason: "session_start" | "nested_traversal" | "path_glob_match" | "include" | "compact" + globs?: string[] + trigger_file_path?: string + parent_file_path?: string +} + +export type WorktreeCreateHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "WorktreeCreate" + name: string +} + +export type WorktreeRemoveHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "WorktreeRemove" + worktree_path: string +} + +export type CwdChangedHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "CwdChanged" + old_cwd: string + new_cwd: string +} + +export type FileChangedHookInput = { + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "FileChanged" + file_path: string + event: "change" | "add" | "unlink" +} + +export type HookInput = ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PreToolUse" + tool_name: string + tool_input: unknown + tool_use_id: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PostToolUse" + tool_name: string + tool_input: unknown + tool_response: unknown + tool_use_id: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PostToolUseFailure" + tool_name: string + tool_input: unknown + tool_use_id: string + error: string + is_interrupt?: boolean +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PermissionDenied" + tool_name: string + tool_input: unknown + tool_use_id: string + reason: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "Notification" + message: string + title?: string + notification_type: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "UserPromptSubmit" + prompt: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "SessionStart" + source: "startup" | "resume" | "clear" | "compact" + agent_type?: string + model?: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "SessionEnd" + reason: "clear" | "resume" | "logout" | "prompt_input_exit" | "other" | "bypass_permissions_disabled" +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "Stop" + stop_hook_active: boolean + last_assistant_message?: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "StopFailure" + error: "authentication_failed" | "billing_error" | "rate_limit" | "invalid_request" | "server_error" | "unknown" | "max_output_tokens" + error_details?: string + last_assistant_message?: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "SubagentStart" + agent_id: string + agent_type: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "SubagentStop" + stop_hook_active: boolean + agent_id: string + agent_transcript_path: string + agent_type: string + last_assistant_message?: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PreCompact" + trigger: "manual" | "auto" + custom_instructions: string | null +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PostCompact" + trigger: "manual" | "auto" + compact_summary: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "PermissionRequest" + tool_name: string + tool_input: unknown + permission_suggestions?: ({ + type: "addRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "replaceRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "removeRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "setMode" + mode: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "addDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "removeDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + })[] +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "Setup" + trigger: "init" | "maintenance" +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "TeammateIdle" + teammate_name: string + team_name: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "TaskCreated" + task_id: string + task_subject: string + task_description?: string + teammate_name?: string + team_name?: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "TaskCompleted" + task_id: string + task_subject: string + task_description?: string + teammate_name?: string + team_name?: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "Elicitation" + mcp_server_name: string + message: string + mode?: "form" | "url" + url?: string + elicitation_id?: string + requested_schema?: Record +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "ElicitationResult" + mcp_server_name: string + elicitation_id?: string + mode?: "form" | "url" + action: "accept" | "decline" | "cancel" + content?: Record +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "ConfigChange" + source: "user_settings" | "project_settings" | "local_settings" | "policy_settings" | "skills" + file_path?: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "InstructionsLoaded" + file_path: string + memory_type: "User" | "Project" | "Local" | "Managed" + load_reason: "session_start" | "nested_traversal" | "path_glob_match" | "include" | "compact" + globs?: string[] + trigger_file_path?: string + parent_file_path?: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "WorktreeCreate" + name: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "WorktreeRemove" + worktree_path: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "CwdChanged" + old_cwd: string + new_cwd: string +}) | ({ + session_id: string + transcript_path: string + cwd: string + permission_mode?: string + agent_id?: string + agent_type?: string +} & { + hook_event_name: "FileChanged" + file_path: string + event: "change" | "add" | "unlink" +}) + +export type AsyncHookJSONOutput = { + async: true + asyncTimeout?: number +} + +export type PreToolUseHookSpecificOutput = { + hookEventName: "PreToolUse" + permissionDecision?: "allow" | "deny" | "ask" + permissionDecisionReason?: string + updatedInput?: Record + additionalContext?: string +} + +export type UserPromptSubmitHookSpecificOutput = { + hookEventName: "UserPromptSubmit" + additionalContext?: string +} + +export type SessionStartHookSpecificOutput = { + hookEventName: "SessionStart" + additionalContext?: string + initialUserMessage?: string + watchPaths?: string[] +} + +export type SetupHookSpecificOutput = { + hookEventName: "Setup" + additionalContext?: string +} + +export type SubagentStartHookSpecificOutput = { + hookEventName: "SubagentStart" + additionalContext?: string +} + +export type PostToolUseHookSpecificOutput = { + hookEventName: "PostToolUse" + additionalContext?: string + updatedMCPToolOutput?: unknown +} + +export type PostToolUseFailureHookSpecificOutput = { + hookEventName: "PostToolUseFailure" + additionalContext?: string +} + +export type PermissionDeniedHookSpecificOutput = { + hookEventName: "PermissionDenied" + retry?: boolean +} + +export type NotificationHookSpecificOutput = { + hookEventName: "Notification" + additionalContext?: string +} + +export type PermissionRequestHookSpecificOutput = { + hookEventName: "PermissionRequest" + decision: ({ + behavior: "allow" + updatedInput?: Record + updatedPermissions?: ({ + type: "addRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "replaceRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "removeRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "setMode" + mode: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "addDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "removeDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + })[] + }) | ({ + behavior: "deny" + message?: string + interrupt?: boolean + }) +} + +export type CwdChangedHookSpecificOutput = { + hookEventName: "CwdChanged" + watchPaths?: string[] +} + +export type FileChangedHookSpecificOutput = { + hookEventName: "FileChanged" + watchPaths?: string[] +} + +/** Hook-specific output for the Elicitation event. Return this to programmatically accept or decline an MCP elicitation request. */ +export type ElicitationHookSpecificOutput = { + hookEventName: "Elicitation" + action?: "accept" | "decline" | "cancel" + content?: Record +} + +/** Hook-specific output for the ElicitationResult event. Return this to override the action or content before the response is sent to the MCP server. */ +export type ElicitationResultHookSpecificOutput = { + hookEventName: "ElicitationResult" + action?: "accept" | "decline" | "cancel" + content?: Record +} + +/** Hook-specific output for the WorktreeCreate event. Provides the absolute path to the created worktree directory. Command hooks print the path on stdout instead. */ +export type WorktreeCreateHookSpecificOutput = { + hookEventName: "WorktreeCreate" + worktreePath: string +} + +export type SyncHookJSONOutput = { + continue?: boolean + suppressOutput?: boolean + stopReason?: string + decision?: "approve" | "block" + systemMessage?: string + reason?: string + hookSpecificOutput?: ({ + hookEventName: "PreToolUse" + permissionDecision?: "allow" | "deny" | "ask" + permissionDecisionReason?: string + updatedInput?: Record + additionalContext?: string + }) | ({ + hookEventName: "UserPromptSubmit" + additionalContext?: string + }) | ({ + hookEventName: "SessionStart" + additionalContext?: string + initialUserMessage?: string + watchPaths?: string[] + }) | ({ + hookEventName: "Setup" + additionalContext?: string + }) | ({ + hookEventName: "SubagentStart" + additionalContext?: string + }) | ({ + hookEventName: "PostToolUse" + additionalContext?: string + updatedMCPToolOutput?: unknown + }) | ({ + hookEventName: "PostToolUseFailure" + additionalContext?: string + }) | ({ + hookEventName: "PermissionDenied" + retry?: boolean + }) | ({ + hookEventName: "Notification" + additionalContext?: string + }) | ({ + hookEventName: "PermissionRequest" + decision: ({ + behavior: "allow" + updatedInput?: Record + updatedPermissions?: ({ + type: "addRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "replaceRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "removeRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "setMode" + mode: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "addDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "removeDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + })[] + }) | ({ + behavior: "deny" + message?: string + interrupt?: boolean + }) + }) | ({ + hookEventName: "Elicitation" + action?: "accept" | "decline" | "cancel" + content?: Record + }) | ({ + hookEventName: "ElicitationResult" + action?: "accept" | "decline" | "cancel" + content?: Record + }) | ({ + hookEventName: "CwdChanged" + watchPaths?: string[] + }) | ({ + hookEventName: "FileChanged" + watchPaths?: string[] + }) | ({ + hookEventName: "WorktreeCreate" + worktreePath: string + }) +} + +export type HookJSONOutput = ({ + async: true + asyncTimeout?: number +}) | ({ + continue?: boolean + suppressOutput?: boolean + stopReason?: string + decision?: "approve" | "block" + systemMessage?: string + reason?: string + hookSpecificOutput?: ({ + hookEventName: "PreToolUse" + permissionDecision?: "allow" | "deny" | "ask" + permissionDecisionReason?: string + updatedInput?: Record + additionalContext?: string + }) | ({ + hookEventName: "UserPromptSubmit" + additionalContext?: string + }) | ({ + hookEventName: "SessionStart" + additionalContext?: string + initialUserMessage?: string + watchPaths?: string[] + }) | ({ + hookEventName: "Setup" + additionalContext?: string + }) | ({ + hookEventName: "SubagentStart" + additionalContext?: string + }) | ({ + hookEventName: "PostToolUse" + additionalContext?: string + updatedMCPToolOutput?: unknown + }) | ({ + hookEventName: "PostToolUseFailure" + additionalContext?: string + }) | ({ + hookEventName: "PermissionDenied" + retry?: boolean + }) | ({ + hookEventName: "Notification" + additionalContext?: string + }) | ({ + hookEventName: "PermissionRequest" + decision: ({ + behavior: "allow" + updatedInput?: Record + updatedPermissions?: ({ + type: "addRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "replaceRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "removeRules" + rules: { + toolName: string + ruleContent?: string + }[] + behavior: "allow" | "deny" | "ask" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "setMode" + mode: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "addDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + }) | ({ + type: "removeDirectories" + directories: string[] + destination: "userSettings" | "projectSettings" | "localSettings" | "session" | "cliArg" + })[] + }) | ({ + behavior: "deny" + message?: string + interrupt?: boolean + }) + }) | ({ + hookEventName: "Elicitation" + action?: "accept" | "decline" | "cancel" + content?: Record + }) | ({ + hookEventName: "ElicitationResult" + action?: "accept" | "decline" | "cancel" + content?: Record + }) | ({ + hookEventName: "CwdChanged" + watchPaths?: string[] + }) | ({ + hookEventName: "FileChanged" + watchPaths?: string[] + }) | ({ + hookEventName: "WorktreeCreate" + worktreePath: string + }) +}) + +export type PromptRequestOption = { + key: string + label: string + description?: string +} + +export type PromptRequest = { + prompt: string + message: string + options: { + key: string + label: string + description?: string + }[] +} + +export type PromptResponse = { + prompt_response: string + selected: string +} + +/** Information about an available skill (invoked via /command syntax). */ +export type SlashCommand = { + name: string + description: string + argumentHint: string +} + +/** Information about an available subagent that can be invoked via the Task tool. */ +export type AgentInfo = { + name: string + description: string + model?: string +} + +/** Information about an available model. */ +export type ModelInfo = { + value: string + displayName: string + description: string + supportsEffort?: boolean + supportedEffortLevels?: "low" | "medium" | "high" | "max"[] + supportsAdaptiveThinking?: boolean + supportsFastMode?: boolean + supportsAutoMode?: boolean +} + +/** Information about the logged in user's account. */ +export type AccountInfo = { + email?: string + organization?: string + subscriptionType?: string + tokenSource?: string + apiKeySource?: string + apiProvider?: "firstParty" | "bedrock" | "vertex" | "foundry" +} + +export type AgentMcpServerSpec = string | (Record +}) | ({ + type: "sse" + url: string + headers?: Record +}) | ({ + type: "http" + url: string + headers?: Record +}) | ({ + type: "sdk" + name: string +})>) + +/** Definition for a custom subagent that can be invoked via the Agent tool. */ +export type AgentDefinition = { + description: string + tools?: string[] + disallowedTools?: string[] + prompt: string + model?: string + mcpServers?: string | (Record + }) | ({ + type: "sse" + url: string + headers?: Record + }) | ({ + type: "http" + url: string + headers?: Record + }) | ({ + type: "sdk" + name: string + })>)[] + criticalSystemReminder_EXPERIMENTAL?: string + skills?: string[] + initialPrompt?: string + maxTurns?: number + background?: boolean + memory?: "user" | "project" | "local" + effort?: "low" | "medium" | "high" | "max" | number + permissionMode?: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" +} + +/** Source for loading filesystem-based settings. 'user' - Global user settings (~/.claude/settings.json). 'project' - Project settings (.claude/settings.json). 'local' - Local settings (.claude/settings.local.json). */ +export type SettingSource = "user" | "project" | "local" + +/** Configuration for loading a plugin. */ +export type SdkPluginConfig = { + type: "local" + path: string +} + +/** Result of a rewindFiles operation. */ +export type RewindFilesResult = { + canRewind: boolean + error?: string + filesChanged?: string[] + insertions?: number + deletions?: number +} + +export type SDKAssistantMessageError = "authentication_failed" | "billing_error" | "rate_limit" | "invalid_request" | "server_error" | "unknown" | "max_output_tokens" + +export type SDKStatus = "compacting" | null + +export type SDKUserMessage = { + type: "user" + message: Record & { role: "user", content: string | Array } + parent_tool_use_id: string | null + isSynthetic?: boolean + tool_use_result?: unknown + priority?: "now" | "next" | "later" + timestamp?: string + uuid?: string + session_id?: string +} + +export type SDKUserMessageReplay = { + type: "user" + message: Record & { role: "user", content: string | Array } + parent_tool_use_id: string | null + isSynthetic?: boolean + tool_use_result?: unknown + priority?: "now" | "next" | "later" + timestamp?: string + uuid: string + session_id: string + isReplay: true +} + +/** Rate limit information for claude.ai subscription users. */ +export type SDKRateLimitInfo = { + status: "allowed" | "allowed_warning" | "rejected" + resetsAt?: number + rateLimitType?: "five_hour" | "seven_day" | "seven_day_opus" | "seven_day_sonnet" | "overage" + utilization?: number + overageStatus?: "allowed" | "allowed_warning" | "rejected" + overageResetsAt?: number + overageDisabledReason?: "overage_not_provisioned" | "org_level_disabled" | "org_level_disabled_until" | "out_of_credits" | "seat_tier_level_disabled" | "member_level_disabled" | "seat_tier_zero_credit_limit" | "group_zero_credit_limit" | "member_zero_credit_limit" | "org_service_level_disabled" | "org_service_zero_credit_limit" | "no_limits_configured" | "unknown" + isUsingOverage?: boolean + surpassedThreshold?: number +} + +export type SDKAssistantMessage = { + type: "assistant" + message: Record & { role: "assistant", content: Array } + parent_tool_use_id: string | null + error?: "authentication_failed" | "billing_error" | "rate_limit" | "invalid_request" | "server_error" | "unknown" | "max_output_tokens" + uuid: string + session_id: string +} + +/** Rate limit event emitted when rate limit info changes. */ +export type SDKRateLimitEvent = { + type: "rate_limit_event" + rate_limit_info: { + status: "allowed" | "allowed_warning" | "rejected" + resetsAt?: number + rateLimitType?: "five_hour" | "seven_day" | "seven_day_opus" | "seven_day_sonnet" | "overage" + utilization?: number + overageStatus?: "allowed" | "allowed_warning" | "rejected" + overageResetsAt?: number + overageDisabledReason?: "overage_not_provisioned" | "org_level_disabled" | "org_level_disabled_until" | "out_of_credits" | "seat_tier_level_disabled" | "member_level_disabled" | "seat_tier_zero_credit_limit" | "group_zero_credit_limit" | "member_zero_credit_limit" | "org_service_level_disabled" | "org_service_zero_credit_limit" | "no_limits_configured" | "unknown" + isUsingOverage?: boolean + surpassedThreshold?: number + } + uuid: string + session_id: string +} + +/** @internal Streamlined text message - replaces SDKAssistantMessage in streamlined output. Text content preserved, thinking and tool_use blocks removed. */ +export type SDKStreamlinedTextMessage = { + type: "streamlined_text" + text: string + session_id: string + uuid: string +} + +/** @internal Streamlined tool use summary - replaces tool_use blocks in streamlined output with a cumulative summary string. */ +export type SDKStreamlinedToolUseSummaryMessage = { + type: "streamlined_tool_use_summary" + tool_summary: string + session_id: string + uuid: string +} + +export type SDKPermissionDenial = { + tool_name: string + tool_use_id: string + tool_input: Record +} + +export type SDKResultSuccess = { + type: "result" + subtype: "success" + duration_ms: number + duration_api_ms: number + is_error: boolean + num_turns: number + result: string + stop_reason: string | null + total_cost_usd: number + usage: Record + modelUsage: Record + permission_denials: { + tool_name: string + tool_use_id: string + tool_input: Record + }[] + structured_output?: unknown + fast_mode_state?: "off" | "cooldown" | "on" + uuid: string + session_id: string +} + +export type SDKResultError = { + type: "result" + subtype: "error_during_execution" | "error_max_turns" | "error_max_budget_usd" | "error_max_structured_output_retries" + duration_ms: number + duration_api_ms: number + is_error: boolean + num_turns: number + stop_reason: string | null + total_cost_usd: number + usage: Record + modelUsage: Record + permission_denials: { + tool_name: string + tool_use_id: string + tool_input: Record + }[] + errors: string[] + fast_mode_state?: "off" | "cooldown" | "on" + uuid: string + session_id: string +} + +export type SDKResultMessage = ({ + type: "result" + subtype: "success" + duration_ms: number + duration_api_ms: number + is_error: boolean + num_turns: number + result: string + stop_reason: string | null + total_cost_usd: number + usage: Record + modelUsage: Record + permission_denials: { + tool_name: string + tool_use_id: string + tool_input: Record + }[] + structured_output?: unknown + fast_mode_state?: "off" | "cooldown" | "on" + uuid: string + session_id: string +}) | ({ + type: "result" + subtype: "error_during_execution" | "error_max_turns" | "error_max_budget_usd" | "error_max_structured_output_retries" + duration_ms: number + duration_api_ms: number + is_error: boolean + num_turns: number + stop_reason: string | null + total_cost_usd: number + usage: Record + modelUsage: Record + permission_denials: { + tool_name: string + tool_use_id: string + tool_input: Record + }[] + errors: string[] + fast_mode_state?: "off" | "cooldown" | "on" + uuid: string + session_id: string +}) + +export type SDKSystemMessage = { + type: "system" + subtype: "init" + agents?: string[] + apiKeySource: "user" | "project" | "org" | "temporary" | "oauth" | "none" + betas?: string[] + claude_code_version: string + cwd: string + tools: string[] + mcp_servers: { + name: string + status: string + }[] + model: string + permissionMode: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" + slash_commands: string[] + output_style: string + skills: string[] + plugins: { + name: string + path: string + source?: string + }[] + fast_mode_state?: "off" | "cooldown" | "on" + uuid: string + session_id: string +} + +export type SDKPartialAssistantMessage = { + type: "stream_event" + event: Record + parent_tool_use_id: string | null + uuid: string + session_id: string +} + +export type SDKCompactBoundaryMessage = { + type: "system" + subtype: "compact_boundary" + compact_metadata: { + trigger: "manual" | "auto" + pre_tokens: number + preserved_segment?: { + head_uuid: string + anchor_uuid: string + tail_uuid: string + } + } + uuid: string + session_id: string +} + +export type SDKStatusMessage = { + type: "system" + subtype: "status" + status: "compacting" | null + permissionMode?: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" + uuid: string + session_id: string +} + +/** @internal Background post-turn summary emitted after each assistant turn. summarizes_uuid points to the assistant message this summarizes. */ +export type SDKPostTurnSummaryMessage = { + type: "system" + subtype: "post_turn_summary" + summarizes_uuid: string + status_category: "blocked" | "waiting" | "completed" | "review_ready" | "failed" + status_detail: string + is_noteworthy: boolean + title: string + description: string + recent_action: string + needs_action: string + artifact_urls: string[] + uuid: string + session_id: string +} + +/** Emitted when an API request fails with a retryable error and will be retried after a delay. error_status is null for connection errors (e.g. timeouts) that had no HTTP response. */ +export type SDKAPIRetryMessage = { + type: "system" + subtype: "api_retry" + attempt: number + max_retries: number + retry_delay_ms: number + error_status: number | null + error: "authentication_failed" | "billing_error" | "rate_limit" | "invalid_request" | "server_error" | "unknown" | "max_output_tokens" + uuid: string + session_id: string +} + +/** Output from a local slash command (e.g. /voice, /cost). Displayed as assistant-style text in the transcript. */ +export type SDKLocalCommandOutputMessage = { + type: "system" + subtype: "local_command_output" + content: string + uuid: string + session_id: string +} + +export type SDKHookStartedMessage = { + type: "system" + subtype: "hook_started" + hook_id: string + hook_name: string + hook_event: string + uuid: string + session_id: string +} + +export type SDKHookProgressMessage = { + type: "system" + subtype: "hook_progress" + hook_id: string + hook_name: string + hook_event: string + stdout: string + stderr: string + output: string + uuid: string + session_id: string +} + +export type SDKHookResponseMessage = { + type: "system" + subtype: "hook_response" + hook_id: string + hook_name: string + hook_event: string + output: string + stdout: string + stderr: string + exit_code?: number + outcome: "success" | "error" | "cancelled" + uuid: string + session_id: string +} + +export type SDKToolProgressMessage = { + type: "tool_progress" + tool_use_id: string + tool_name: string + parent_tool_use_id: string | null + elapsed_time_seconds: number + task_id?: string + uuid: string + session_id: string +} + +export type SDKAuthStatusMessage = { + type: "auth_status" + isAuthenticating: boolean + output: string[] + error?: string + uuid: string + session_id: string +} + +export type SDKFilesPersistedEvent = { + type: "system" + subtype: "files_persisted" + files: { + filename: string + file_id: string + }[] + failed: { + filename: string + error: string + }[] + processed_at: string + uuid: string + session_id: string +} + +export type SDKTaskNotificationMessage = { + type: "system" + subtype: "task_notification" + task_id: string + tool_use_id?: string + status: "completed" | "failed" | "stopped" + output_file: string + summary: string + usage?: { + total_tokens: number + tool_uses: number + duration_ms: number + } + uuid: string + session_id: string +} + +export type SDKTaskStartedMessage = { + type: "system" + subtype: "task_started" + task_id: string + tool_use_id?: string + description: string + task_type?: string + workflow_name?: string + prompt?: string + uuid: string + session_id: string +} + +export type SDKTaskProgressMessage = { + type: "system" + subtype: "task_progress" + task_id: string + tool_use_id?: string + description: string + usage: { + total_tokens: number + tool_uses: number + duration_ms: number + } + last_tool_name?: string + summary?: string + uuid: string + session_id: string +} + +/** Mirrors notifySessionStateChanged. 'idle' fires after heldBackResult flushes and the bg-agent do-while exits — authoritative turn-over signal. */ +export type SDKSessionStateChangedMessage = { + type: "system" + subtype: "session_state_changed" + state: "idle" | "running" | "requires_action" + uuid: string + session_id: string +} + +export type SDKToolUseSummaryMessage = { + type: "tool_use_summary" + summary: string + preceding_tool_use_ids: string[] + uuid: string + session_id: string +} + +/** Emitted when an MCP server confirms that a URL-mode elicitation is complete. */ +export type SDKElicitationCompleteMessage = { + type: "system" + subtype: "elicitation_complete" + mcp_server_name: string + elicitation_id: string + uuid: string + session_id: string +} + +/** Predicted next user prompt, emitted after each turn when promptSuggestions is enabled. */ +export type SDKPromptSuggestionMessage = { + type: "prompt_suggestion" + suggestion: string + uuid: string + session_id: string +} + +/** Session metadata returned by listSessions and getSessionInfo. */ +export type SDKSessionInfo = { + sessionId: string + summary: string + lastModified: number + fileSize?: number + customTitle?: string + firstPrompt?: string + gitBranch?: string + cwd?: string + tag?: string + createdAt?: number +} + +export type SDKMessage = ({ + type: "assistant" + message: Record & { role: "assistant", content: Array } + parent_tool_use_id: string | null + error?: "authentication_failed" | "billing_error" | "rate_limit" | "invalid_request" | "server_error" | "unknown" | "max_output_tokens" + uuid: string + session_id: string +}) | ({ + type: "user" + message: Record & { role: "user", content: string | Array } + parent_tool_use_id: string | null + isSynthetic?: boolean + tool_use_result?: unknown + priority?: "now" | "next" | "later" + timestamp?: string + uuid?: string + session_id?: string +}) | ({ + type: "user" + message: Record & { role: "user", content: string | Array } + parent_tool_use_id: string | null + isSynthetic?: boolean + tool_use_result?: unknown + priority?: "now" | "next" | "later" + timestamp?: string + uuid: string + session_id: string + isReplay: true +}) | (({ + type: "result" + subtype: "success" + duration_ms: number + duration_api_ms: number + is_error: boolean + num_turns: number + result: string + stop_reason: string | null + total_cost_usd: number + usage: Record + modelUsage: Record + permission_denials: { + tool_name: string + tool_use_id: string + tool_input: Record + }[] + structured_output?: unknown + fast_mode_state?: "off" | "cooldown" | "on" + uuid: string + session_id: string +}) | ({ + type: "result" + subtype: "error_during_execution" | "error_max_turns" | "error_max_budget_usd" | "error_max_structured_output_retries" + duration_ms: number + duration_api_ms: number + is_error: boolean + num_turns: number + stop_reason: string | null + total_cost_usd: number + usage: Record + modelUsage: Record + permission_denials: { + tool_name: string + tool_use_id: string + tool_input: Record + }[] + errors: string[] + fast_mode_state?: "off" | "cooldown" | "on" + uuid: string + session_id: string +})) | ({ + type: "system" + subtype: "init" + agents?: string[] + apiKeySource: "user" | "project" | "org" | "temporary" | "oauth" | "none" + betas?: string[] + claude_code_version: string + cwd: string + tools: string[] + mcp_servers: { + name: string + status: string + }[] + model: string + permissionMode: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" + slash_commands: string[] + output_style: string + skills: string[] + plugins: { + name: string + path: string + source?: string + }[] + fast_mode_state?: "off" | "cooldown" | "on" + uuid: string + session_id: string +}) | ({ + type: "stream_event" + event: Record + parent_tool_use_id: string | null + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "compact_boundary" + compact_metadata: { + trigger: "manual" | "auto" + pre_tokens: number + preserved_segment?: { + head_uuid: string + anchor_uuid: string + tail_uuid: string + } + } + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "status" + status: "compacting" | null + permissionMode?: "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "api_retry" + attempt: number + max_retries: number + retry_delay_ms: number + error_status: number | null + error: "authentication_failed" | "billing_error" | "rate_limit" | "invalid_request" | "server_error" | "unknown" | "max_output_tokens" + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "local_command_output" + content: string + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "hook_started" + hook_id: string + hook_name: string + hook_event: string + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "hook_progress" + hook_id: string + hook_name: string + hook_event: string + stdout: string + stderr: string + output: string + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "hook_response" + hook_id: string + hook_name: string + hook_event: string + output: string + stdout: string + stderr: string + exit_code?: number + outcome: "success" | "error" | "cancelled" + uuid: string + session_id: string +}) | ({ + type: "tool_progress" + tool_use_id: string + tool_name: string + parent_tool_use_id: string | null + elapsed_time_seconds: number + task_id?: string + uuid: string + session_id: string +}) | ({ + type: "auth_status" + isAuthenticating: boolean + output: string[] + error?: string + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "task_notification" + task_id: string + tool_use_id?: string + status: "completed" | "failed" | "stopped" + output_file: string + summary: string + usage?: { + total_tokens: number + tool_uses: number + duration_ms: number + } + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "task_started" + task_id: string + tool_use_id?: string + description: string + task_type?: string + workflow_name?: string + prompt?: string + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "task_progress" + task_id: string + tool_use_id?: string + description: string + usage: { + total_tokens: number + tool_uses: number + duration_ms: number + } + last_tool_name?: string + summary?: string + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "session_state_changed" + state: "idle" | "running" | "requires_action" + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "files_persisted" + files: { + filename: string + file_id: string + }[] + failed: { + filename: string + error: string + }[] + processed_at: string + uuid: string + session_id: string +}) | ({ + type: "tool_use_summary" + summary: string + preceding_tool_use_ids: string[] + uuid: string + session_id: string +}) | ({ + type: "rate_limit_event" + rate_limit_info: { + status: "allowed" | "allowed_warning" | "rejected" + resetsAt?: number + rateLimitType?: "five_hour" | "seven_day" | "seven_day_opus" | "seven_day_sonnet" | "overage" + utilization?: number + overageStatus?: "allowed" | "allowed_warning" | "rejected" + overageResetsAt?: number + overageDisabledReason?: "overage_not_provisioned" | "org_level_disabled" | "org_level_disabled_until" | "out_of_credits" | "seat_tier_level_disabled" | "member_level_disabled" | "seat_tier_zero_credit_limit" | "group_zero_credit_limit" | "member_zero_credit_limit" | "org_service_level_disabled" | "org_service_zero_credit_limit" | "no_limits_configured" | "unknown" + isUsingOverage?: boolean + surpassedThreshold?: number + } + uuid: string + session_id: string +}) | ({ + type: "system" + subtype: "elicitation_complete" + mcp_server_name: string + elicitation_id: string + uuid: string + session_id: string +}) | ({ + type: "prompt_suggestion" + suggestion: string + uuid: string + session_id: string +}) | ({ + type: "permission_request" + request_id: string + tool_name: string + tool_use_id: string + input: Record + uuid: string + session_id: string +}) + +/** Fast mode state: off, in cooldown after rate limit, or actively enabled. */ +export type FastModeState = "off" | "cooldown" | "on" + +export type ExitReason = "clear" | "resume" | "logout" | "prompt_input_exit" | "other" | "bypass_permissions_disabled" diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 6a7f46e0..502b8f4f 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -201,6 +201,95 @@ export type AxiosErrorKind = | 'http' // other axios error (may have status) | 'other' // not an axios error +// ============================================================================ +// SDK-specific error classes +// ============================================================================ + +/** + * Base class for all SDK errors. Extends ClaudeError so that existing + * `catch (e) { if (e instanceof ClaudeError) … }` checks still work, + * while giving SDK consumers a more specific base to match against. + */ +export class SDKError extends ClaudeError { + constructor(message: string) { + super(message) + this.name = 'SDKError' + } +} + +export class SDKAuthenticationError extends SDKError { + constructor(message?: string) { + super(message ?? 'Authentication failed') + this.name = 'SDKAuthenticationError' + } +} + +export class SDKBillingError extends SDKError { + constructor(message?: string) { + super(message ?? 'Billing error - check subscription') + this.name = 'SDKBillingError' + } +} + +export class SDKRateLimitError extends SDKError { + constructor( + message?: string, + public readonly resetsAt?: number, + public readonly rateLimitType?: string, + ) { + super(message ?? 'Rate limit exceeded') + this.name = 'SDKRateLimitError' + } +} + +export class SDKInvalidRequestError extends SDKError { + constructor(message?: string) { + super(message ?? 'Invalid request') + this.name = 'SDKInvalidRequestError' + } +} + +export class SDKServerError extends SDKError { + constructor(message?: string) { + super(message ?? 'Server error') + this.name = 'SDKServerError' + } +} + +export class SDKMaxOutputTokensError extends SDKError { + constructor(message?: string) { + super(message ?? 'Max output tokens reached') + this.name = 'SDKMaxOutputTokensError' + } +} + +export type SDKAssistantMessageError = + | 'authentication_failed' + | 'billing_error' + | 'rate_limit' + | 'invalid_request' + | 'server_error' + | 'unknown' + | 'max_output_tokens' + +/** + * Convert an SDKAssistantMessageError type string to the proper Error class. + */ +export function sdkErrorFromType( + errorType: SDKAssistantMessageError, + message?: string, +): SDKError | ClaudeError { + switch (errorType) { + case 'authentication_failed': return new SDKAuthenticationError(message) + case 'billing_error': return new SDKBillingError(message) + case 'rate_limit': return new SDKRateLimitError(message) + case 'invalid_request': return new SDKInvalidRequestError(message) + case 'server_error': return new SDKServerError(message) + case 'max_output_tokens': return new SDKMaxOutputTokensError(message) + default: return new ClaudeError(message ?? 'Unknown error') + } +} + /** * Classify a caught error from an axios request into one of a few buckets. * Replaces the ~20-line isAxiosError → 401/403 → ECONNABORTED → ECONNREFUSED diff --git a/src/utils/handlePromptSubmit.ts b/src/utils/handlePromptSubmit.ts index c11de745..461306a8 100644 --- a/src/utils/handlePromptSubmit.ts +++ b/src/utils/handlePromptSubmit.ts @@ -2,7 +2,7 @@ import type { UUID } from 'crypto' import { logEvent } from 'src/services/analytics/index.js' import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/metadata.js' import { type Command, getCommandName, isCommandEnabled } from '../commands.js' -import { selectableUserMessagesFilter } from '../components/MessageSelector.js' +import { selectableUserMessagesFilter } from './messageFilters.js' import type { SpinnerMode } from '../components/Spinner/types.js' import type { QuerySource } from '../constants/querySource.js' import { expandPastedTextRefs, parseReferences } from '../history.js' diff --git a/src/utils/messageFilters.ts b/src/utils/messageFilters.ts new file mode 100644 index 00000000..831d7e68 --- /dev/null +++ b/src/utils/messageFilters.ts @@ -0,0 +1,81 @@ +import type { ContentBlockParam, TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' +import type { Message, UserMessage } from '../types/message.js' +import { + BASH_STDERR_TAG, + BASH_STDOUT_TAG, + LOCAL_COMMAND_STDERR_TAG, + LOCAL_COMMAND_STDOUT_TAG, + TASK_NOTIFICATION_TAG, + TEAMMATE_MESSAGE_TAG, + TICK_TAG, +} from '../constants/xml.js' +import { isSyntheticMessage, isToolUseResultMessage } from './messages.js' + +function isTextBlock(block: ContentBlockParam): block is TextBlockParam { + return block.type === 'text' +} + +export function selectableUserMessagesFilter(message: Message): message is UserMessage { + if (message.type !== 'user') { + return false + } + if (Array.isArray(message.message.content) && message.message.content[0]?.type === 'tool_result') { + return false + } + if (isSyntheticMessage(message)) { + return false + } + if (message.isMeta) { + return false + } + if (message.isCompactSummary || message.isVisibleInTranscriptOnly) { + return false + } + const content = message.message.content + const lastBlock = typeof content === 'string' ? null : content[content.length - 1] + const messageText = typeof content === 'string' ? content.trim() : lastBlock && isTextBlock(lastBlock) ? lastBlock.text.trim() : '' + + // Filter out non-user-authored messages (command outputs, task notifications, ticks). + if (messageText.indexOf(`<${LOCAL_COMMAND_STDOUT_TAG}>`) !== -1 || messageText.indexOf(`<${LOCAL_COMMAND_STDERR_TAG}>`) !== -1 || messageText.indexOf(`<${BASH_STDOUT_TAG}>`) !== -1 || messageText.indexOf(`<${BASH_STDERR_TAG}>`) !== -1 || messageText.indexOf(`<${TASK_NOTIFICATION_TAG}>`) !== -1 || messageText.indexOf(`<${TICK_TAG}>`) !== -1 || messageText.indexOf(`<${TEAMMATE_MESSAGE_TAG}`) !== -1) { + return false + } + return true +} + +/** + * Checks if all messages after the given index are synthetic (interruptions, cancels, etc.) + * or non-meaningful content. Returns true if there's nothing meaningful to confirm - + * for example, if the user hit enter then immediately cancelled. + */ +export function messagesAfterAreOnlySynthetic(messages: Message[], fromIndex: number): boolean { + for (let i = fromIndex + 1; i < messages.length; i++) { + const msg = messages[i] + if (!msg) continue + + // Skip known non-meaningful message types + if (isSyntheticMessage(msg)) continue + if (isToolUseResultMessage(msg)) continue + if (msg.type === 'progress') continue + if (msg.type === 'system') continue + if (msg.type === 'attachment') continue + if (msg.type === 'user' && msg.isMeta) continue + + // Assistant with actual content = meaningful + if (msg.type === 'assistant') { + const content = msg.message.content + if (Array.isArray(content)) { + const hasMeaningfulContent = content.some(block => block.type === 'text' && block.text.trim() || block.type === 'tool_use') + if (hasMeaningfulContent) return false + } + continue + } + + // User messages that aren't synthetic or meta = meaningful + if (msg.type === 'user') { + return false + } + + // Other types (e.g., tombstone) are non-meaningful, continue + } + return true +} \ No newline at end of file diff --git a/src/utils/user.test.ts b/src/utils/user.test.ts index 9679f9ba..2c5654ba 100644 --- a/src/utils/user.test.ts +++ b/src/utils/user.test.ts @@ -10,9 +10,12 @@ function installCommonMocks(options?: { oauthEmail?: string gitEmail?: string }) { - mock.module('../bootstrap/state.js', () => ({ - getSessionId: () => 'session-test', - })) + // NOTE: Do NOT mock ../bootstrap/state.js here. + // mock.module() is process-global in bun:test and mock.restore() does NOT + // undo it. Mocking state.js leaks getSessionId = () => 'session-test' into + // every other test file that imports state.js (e.g. SDK CON-1 tests). + // The dynamic import (importFreshUserModule) will use the real state.js, + // which is fine — these tests only assert email, not sessionId. mock.module('./auth.js', () => ({ getOauthAccountInfo: () => diff --git a/src/utils/validation.ts b/src/utils/validation.ts new file mode 100644 index 00000000..784da306 --- /dev/null +++ b/src/utils/validation.ts @@ -0,0 +1,54 @@ +/** + * Shared validation utilities for SDK-facing APIs. + */ + +/** + * Validate an array of items using a per-item validator. + * Throws TypeError with the index and missing field if validation fails. + */ +export function validateArrayOf( + items: unknown[], + validator: (item: unknown, index: number) => T, + label: string, +): T[] { + if (!Array.isArray(items)) { + throw new TypeError(`${label}: expected an array, got ${typeof items}`) + } + return items.map((item, i) => { + try { + return validator(item, i) + } catch (err) { + if (err instanceof TypeError) { + throw new TypeError(`${label}: item at index ${i} - ${err.message}`) + } + throw err + } + }) +} + +/** + * Assert that a value is a non-empty string. + */ +export function assertNonEmptyString(value: unknown, field: string): asserts value is string { + if (typeof value !== 'string' || value.length === 0) { + throw new TypeError(`missing or empty '${field}' (expected non-empty string)`) + } +} + +/** + * Assert that a value is a non-null object (but not an array). + */ +export function assertObject(value: unknown, field: string): asserts value is Record { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new TypeError(`missing or invalid '${field}' (expected object)`) + } +} + +/** + * Assert that a value is a function. + */ +export function assertFunction(value: unknown, field: string): asserts value is (...args: any[]) => any { + if (typeof value !== 'function') { + throw new TypeError(`missing or invalid '${field}' (expected function)`) + } +} diff --git a/tests/sdk/generated-types.test.ts b/tests/sdk/generated-types.test.ts new file mode 100644 index 00000000..1ee61957 --- /dev/null +++ b/tests/sdk/generated-types.test.ts @@ -0,0 +1,279 @@ +import { describe, test, expect } from 'bun:test' +import { + SDKAssistantMessageSchema, + SDKSystemMessageSchema, + SDKCompactBoundaryMessageSchema, + SDKMessageSchema, + SDKUserMessageSchema, + SDKResultMessageSchema, + SDKResultSuccessSchema, + SDKResultErrorSchema, + SDKSessionInfoSchema, + PermissionModeSchema, + ThinkingConfigSchema, + AgentDefinitionSchema, + McpServerStatusSchema, + ModelUsageSchema, + FastModeStateSchema, + HookInputSchema, + ExitReasonSchema, +} from '../../src/entrypoints/sdk/coreSchemas.js' +import { z } from 'zod/v4' + +/** + * Tests for generated SDK types from Zod schemas. + * + * These tests verify that: + * 1. All schemas materialize correctly (no lazy errors) + * 2. Schemas can parse valid data + * 3. Key discriminated fields are correct + * 4. The full SDKMessage union accepts all message variants + */ +describe('SDK Zod schemas (type generation source)', () => { + test('SDKAssistantMessageSchema accepts valid data', () => { + const schema = SDKAssistantMessageSchema() + const result = schema.safeParse({ + type: 'assistant', + message: { role: 'assistant', content: [{ type: 'text', text: 'hi' }] }, + parent_tool_use_id: null, + uuid: '12345678-1234-1234-1234-123456789012', + session_id: '12345678-1234-1234-1234-123456789012', + }) + expect(result.success).toBe(true) + }) + + test('SDKSystemMessageSchema accepts valid data', () => { + const schema = SDKSystemMessageSchema() + const result = schema.safeParse({ + type: 'system', + subtype: 'init', + apiKeySource: 'user', + claude_code_version: '0.3.0', + cwd: '/home/user/project', + tools: ['Read', 'Write'], + mcp_servers: [{ name: 'test', status: 'connected' }], + model: 'claude-sonnet-4-6', + permissionMode: 'default', + slash_commands: [], + output_style: 'default', + skills: [], + plugins: [], + uuid: '12345678-1234-1234-1234-123456789012', + session_id: '12345678-1234-1234-1234-123456789012', + }) + expect(result.success).toBe(true) + }) + + test('SDKCompactBoundaryMessageSchema accepts valid data', () => { + const schema = SDKCompactBoundaryMessageSchema() + const result = schema.safeParse({ + type: 'system', + subtype: 'compact_boundary', + compact_metadata: { + trigger: 'manual', + pre_tokens: 1000, + }, + uuid: '12345678-1234-1234-1234-123456789012', + session_id: '12345678-1234-1234-1234-123456789012', + }) + expect(result.success).toBe(true) + }) + + test('SDKCompactBoundaryMessageSchema accepts preserved_segment', () => { + const schema = SDKCompactBoundaryMessageSchema() + const result = schema.safeParse({ + type: 'system', + subtype: 'compact_boundary', + compact_metadata: { + trigger: 'auto', + pre_tokens: 50000, + preserved_segment: { + head_uuid: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + anchor_uuid: 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + tail_uuid: 'cccccccc-cccc-cccc-cccc-cccccccccccc', + }, + }, + uuid: '12345678-1234-1234-1234-123456789012', + session_id: '12345678-1234-1234-1234-123456789012', + }) + expect(result.success).toBe(true) + }) + + test('SDKUserMessageSchema accepts valid data', () => { + const schema = SDKUserMessageSchema() + const result = schema.safeParse({ + type: 'user', + message: { role: 'user', content: 'hello' }, + parent_tool_use_id: null, + }) + expect(result.success).toBe(true) + }) + + test('SDKResultSuccessSchema accepts valid data', () => { + const schema = SDKResultSuccessSchema() + const result = schema.safeParse({ + type: 'result', + subtype: 'success', + duration_ms: 1500, + duration_api_ms: 1200, + is_error: false, + num_turns: 1, + result: 'Done', + stop_reason: 'end_turn', + total_cost_usd: 0.01, + usage: { input_tokens: 100, output_tokens: 50 }, + modelUsage: {}, + permission_denials: [], + uuid: '12345678-1234-1234-1234-123456789012', + session_id: '12345678-1234-1234-1234-123456789012', + }) + expect(result.success).toBe(true) + }) + + test('SDKResultErrorSchema accepts valid data', () => { + const schema = SDKResultErrorSchema() + const result = schema.safeParse({ + type: 'result', + subtype: 'error_during_execution', + duration_ms: 100, + duration_api_ms: 80, + is_error: true, + num_turns: 1, + stop_reason: null, + total_cost_usd: 0.001, + usage: { input_tokens: 50, output_tokens: 10 }, + modelUsage: {}, + permission_denials: [], + errors: ['Something went wrong'], + uuid: '12345678-1234-1234-1234-123456789012', + session_id: '12345678-1234-1234-1234-123456789012', + }) + expect(result.success).toBe(true) + }) + + test('SDKMessageSchema accepts all message types', () => { + const schema = SDKMessageSchema() + + const messages = [ + { + type: 'assistant', + message: {}, + parent_tool_use_id: null, + uuid: '12345678-1234-1234-1234-123456789012', + session_id: '12345678-1234-1234-1234-123456789012', + }, + { + type: 'user', + message: {}, + parent_tool_use_id: null, + }, + { + type: 'system', + subtype: 'init', + apiKeySource: 'user', + claude_code_version: '0.3.0', + cwd: '/tmp', + tools: [], + mcp_servers: [], + model: 'sonnet', + permissionMode: 'default', + slash_commands: [], + output_style: 'default', + skills: [], + plugins: [], + uuid: '12345678-1234-1234-1234-123456789012', + session_id: '12345678-1234-1234-1234-123456789012', + }, + { + type: 'system', + subtype: 'compact_boundary', + compact_metadata: { trigger: 'manual', pre_tokens: 100 }, + uuid: '12345678-1234-1234-1234-123456789012', + session_id: '12345678-1234-1234-1234-123456789012', + }, + ] + + for (const msg of messages) { + const result = schema.safeParse(msg) + expect(result.success).toBe(true) + } + }) + + test('SDKSessionInfoSchema accepts valid data', () => { + const schema = SDKSessionInfoSchema() + const result = schema.safeParse({ + sessionId: '12345678-1234-1234-1234-123456789012', + summary: 'Test session', + lastModified: Date.now(), + }) + expect(result.success).toBe(true) + }) + + test('PermissionModeSchema accepts valid modes', () => { + const schema = PermissionModeSchema() + const modes = ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'dontAsk'] + for (const mode of modes) { + expect(schema.safeParse(mode).success).toBe(true) + } + expect(schema.safeParse('invalid').success).toBe(false) + }) + + test('ThinkingConfigSchema accepts all variants', () => { + const schema = ThinkingConfigSchema() + expect(schema.safeParse({ type: 'adaptive' }).success).toBe(true) + expect(schema.safeParse({ type: 'enabled' }).success).toBe(true) + expect(schema.safeParse({ type: 'enabled', budgetTokens: 10000 }).success).toBe(true) + expect(schema.safeParse({ type: 'disabled' }).success).toBe(true) + expect(schema.safeParse({ type: 'unknown' }).success).toBe(false) + }) + + test('FastModeStateSchema accepts valid states', () => { + const schema = FastModeStateSchema() + expect(schema.safeParse('off').success).toBe(true) + expect(schema.safeParse('cooldown').success).toBe(true) + expect(schema.safeParse('on').success).toBe(true) + expect(schema.safeParse('unknown').success).toBe(false) + }) + + test('ExitReasonSchema accepts valid reasons', () => { + const schema = ExitReasonSchema() + const reasons = ['clear', 'resume', 'logout', 'prompt_input_exit', 'other', 'bypass_permissions_disabled'] + for (const r of reasons) { + expect(schema.safeParse(r).success).toBe(true) + } + expect(schema.safeParse('invalid').success).toBe(false) + }) + + test('ModelUsageSchema accepts valid data', () => { + const schema = ModelUsageSchema() + const result = schema.safeParse({ + inputTokens: 100, + outputTokens: 50, + cacheReadInputTokens: 200, + cacheCreationInputTokens: 300, + webSearchRequests: 1, + costUSD: 0.01, + contextWindow: 200000, + maxOutputTokens: 8192, + }) + expect(result.success).toBe(true) + }) + + test('AgentDefinitionSchema accepts valid data', () => { + const schema = AgentDefinitionSchema() + const result = schema.safeParse({ + description: 'Test agent', + prompt: 'You are a test agent', + }) + expect(result.success).toBe(true) + }) + + test('McpServerStatusSchema accepts valid data', () => { + const schema = McpServerStatusSchema() + const result = schema.safeParse({ + name: 'test-server', + status: 'connected', + }) + expect(result.success).toBe(true) + }) +})