feat: SDK Foundation — Type Declarations, Errors, and Utilities (#866)
* 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 <ali.alakbarli@users.noreply.github.com>
This commit is contained in:
518
src/entrypoints/sdk.d.ts
vendored
Normal file
518
src/entrypoints/sdk.d.ts
vendored
Normal file
@@ -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<string, unknown>
|
||||||
|
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<string, unknown[]>
|
||||||
|
mcpServers?: Record<string, unknown>
|
||||||
|
settings?: {
|
||||||
|
env?: Record<string, string>
|
||||||
|
attribution?: { commit: string; pr: string }
|
||||||
|
}
|
||||||
|
/** Environment variables to apply during query execution. Overrides process.env. Takes precedence over settings.env. */
|
||||||
|
env?: Record<string, string | undefined>
|
||||||
|
/**
|
||||||
|
* 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<string, {
|
||||||
|
description: string
|
||||||
|
prompt: string
|
||||||
|
tools?: string[]
|
||||||
|
disallowedTools?: string[]
|
||||||
|
model?: string
|
||||||
|
maxTurns?: number
|
||||||
|
}>
|
||||||
|
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<SDKMessage>
|
||||||
|
setModel(model: string): Promise<void>
|
||||||
|
setPermissionMode(mode: QueryPermissionMode): Promise<void>
|
||||||
|
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<RewindFilesResult>
|
||||||
|
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<string, unknown>
|
||||||
|
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<string, unknown>
|
||||||
|
/**
|
||||||
|
* 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<SDKMessage>
|
||||||
|
getMessages(): SDKMessage[]
|
||||||
|
interrupt(): void
|
||||||
|
/** Respond to a pending permission prompt. */
|
||||||
|
respondToPermission(toolUseId: string, decision: PermissionResult): void
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MCP tool types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface SdkMcpToolDefinition<Schema = any> {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
inputSchema: Schema
|
||||||
|
handler: (args: any, extra: unknown) => Promise<any>
|
||||||
|
annotations?: any
|
||||||
|
searchHint?: string
|
||||||
|
alwaysLoad?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Session functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export function listSessions(
|
||||||
|
options?: ListSessionsOptions,
|
||||||
|
): Promise<SDKSessionInfo[]>
|
||||||
|
|
||||||
|
export function getSessionInfo(
|
||||||
|
sessionId: string,
|
||||||
|
options?: GetSessionInfoOptions,
|
||||||
|
): Promise<SDKSessionInfo | undefined>
|
||||||
|
|
||||||
|
export function getSessionMessages(
|
||||||
|
sessionId: string,
|
||||||
|
options?: GetSessionMessagesOptions,
|
||||||
|
): Promise<SessionMessage[]>
|
||||||
|
|
||||||
|
export function renameSession(
|
||||||
|
sessionId: string,
|
||||||
|
title: string,
|
||||||
|
options?: SessionMutationOptions,
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
|
export function tagSession(
|
||||||
|
sessionId: string,
|
||||||
|
tag: string | null,
|
||||||
|
options?: SessionMutationOptions,
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
|
export function forkSession(
|
||||||
|
sessionId: string,
|
||||||
|
options?: ForkSessionOptions,
|
||||||
|
): Promise<ForkSessionResult>
|
||||||
|
|
||||||
|
export function deleteSession(
|
||||||
|
sessionId: string,
|
||||||
|
options?: SessionMutationOptions,
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Query functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export function query(params: {
|
||||||
|
prompt: string | AsyncIterable<SDKUserMessage>
|
||||||
|
options?: QueryOptions
|
||||||
|
}): Query
|
||||||
|
|
||||||
|
export function queryAsync(params: {
|
||||||
|
prompt: string | AsyncIterable<SDKUserMessage>
|
||||||
|
options?: QueryOptions
|
||||||
|
}): Promise<Query>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// V2 API functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export function unstable_v2_createSession(options: SDKSessionOptions): SDKSession
|
||||||
|
|
||||||
|
export function unstable_v2_resumeSession(
|
||||||
|
sessionId: string,
|
||||||
|
options: SDKSessionOptions,
|
||||||
|
): Promise<SDKSession>
|
||||||
|
|
||||||
|
export function unstable_v2_prompt(
|
||||||
|
message: string,
|
||||||
|
options: SDKSessionOptions,
|
||||||
|
): Promise<SDKResultMessage>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MCP tool functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export function tool<Schema = any>(
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
inputSchema: Schema,
|
||||||
|
handler: (args: any, extra: unknown) => Promise<any>,
|
||||||
|
extras?: {
|
||||||
|
annotations?: any
|
||||||
|
searchHint?: string
|
||||||
|
alwaysLoad?: boolean
|
||||||
|
},
|
||||||
|
): SdkMcpToolDefinition<Schema>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP server transport configuration types.
|
||||||
|
* Matches McpServerConfigForProcessTransport from coreTypes.generated.ts.
|
||||||
|
*/
|
||||||
|
export type SdkMcpStdioConfig = {
|
||||||
|
type?: "stdio"
|
||||||
|
command: string
|
||||||
|
args?: string[]
|
||||||
|
env?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SdkMcpSSEConfig = {
|
||||||
|
type: "sse"
|
||||||
|
url: string
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SdkMcpHttpConfig = {
|
||||||
|
type: "http"
|
||||||
|
url: string
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -55,7 +55,7 @@ export const OutputFormatSchema = lazySchema(() =>
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export const ApiKeySourceSchema = lazySchema(() =>
|
export const ApiKeySourceSchema = lazySchema(() =>
|
||||||
z.enum(['user', 'project', 'org', 'temporary', 'oauth']),
|
z.enum(['user', 'project', 'org', 'temporary', 'oauth', 'none']),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const ConfigScopeSchema = lazySchema(() =>
|
export const ConfigScopeSchema = lazySchema(() =>
|
||||||
@@ -1851,6 +1851,18 @@ export const SDKSessionInfoSchema = lazySchema(() =>
|
|||||||
.describe('Session metadata returned by listSessions and getSessionInfo.'),
|
.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(() =>
|
export const SDKMessageSchema = lazySchema(() =>
|
||||||
z.union([
|
z.union([
|
||||||
SDKAssistantMessageSchema(),
|
SDKAssistantMessageSchema(),
|
||||||
@@ -1877,6 +1889,7 @@ export const SDKMessageSchema = lazySchema(() =>
|
|||||||
SDKRateLimitEventSchema(),
|
SDKRateLimitEventSchema(),
|
||||||
SDKElicitationCompleteMessageSchema(),
|
SDKElicitationCompleteMessageSchema(),
|
||||||
SDKPromptSuggestionMessageSchema(),
|
SDKPromptSuggestionMessageSchema(),
|
||||||
|
SDKPermissionRequestMessageSchema(),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -201,6 +201,95 @@ export type AxiosErrorKind =
|
|||||||
| 'http' // other axios error (may have status)
|
| 'http' // other axios error (may have status)
|
||||||
| 'other' // not an axios error
|
| '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.
|
* Classify a caught error from an axios request into one of a few buckets.
|
||||||
* Replaces the ~20-line isAxiosError → 401/403 → ECONNABORTED → ECONNREFUSED
|
* Replaces the ~20-line isAxiosError → 401/403 → ECONNABORTED → ECONNREFUSED
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { UUID } from 'crypto'
|
|||||||
import { logEvent } from 'src/services/analytics/index.js'
|
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 { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/metadata.js'
|
||||||
import { type Command, getCommandName, isCommandEnabled } from '../commands.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 { SpinnerMode } from '../components/Spinner/types.js'
|
||||||
import type { QuerySource } from '../constants/querySource.js'
|
import type { QuerySource } from '../constants/querySource.js'
|
||||||
import { expandPastedTextRefs, parseReferences } from '../history.js'
|
import { expandPastedTextRefs, parseReferences } from '../history.js'
|
||||||
|
|||||||
81
src/utils/messageFilters.ts
Normal file
81
src/utils/messageFilters.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -10,9 +10,12 @@ function installCommonMocks(options?: {
|
|||||||
oauthEmail?: string
|
oauthEmail?: string
|
||||||
gitEmail?: string
|
gitEmail?: string
|
||||||
}) {
|
}) {
|
||||||
mock.module('../bootstrap/state.js', () => ({
|
// NOTE: Do NOT mock ../bootstrap/state.js here.
|
||||||
getSessionId: () => 'session-test',
|
// 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', () => ({
|
mock.module('./auth.js', () => ({
|
||||||
getOauthAccountInfo: () =>
|
getOauthAccountInfo: () =>
|
||||||
|
|||||||
54
src/utils/validation.ts
Normal file
54
src/utils/validation.ts
Normal file
@@ -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<T>(
|
||||||
|
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<string, unknown> {
|
||||||
|
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)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
279
tests/sdk/generated-types.test.ts
Normal file
279
tests/sdk/generated-types.test.ts
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user