init claude-code
This commit is contained in:
+290
@@ -0,0 +1,290 @@
|
||||
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
|
||||
import { z } from 'zod/v4'
|
||||
import { lazySchema } from '../utils/lazySchema.js'
|
||||
import {
|
||||
type HookEvent,
|
||||
HOOK_EVENTS,
|
||||
type HookInput,
|
||||
type PermissionUpdate,
|
||||
} from 'src/entrypoints/agentSdkTypes.js'
|
||||
import type {
|
||||
HookJSONOutput,
|
||||
AsyncHookJSONOutput,
|
||||
SyncHookJSONOutput,
|
||||
} from 'src/entrypoints/agentSdkTypes.js'
|
||||
import type { Message } from 'src/types/message.js'
|
||||
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
|
||||
import { permissionBehaviorSchema } from 'src/utils/permissions/PermissionRule.js'
|
||||
import { permissionUpdateSchema } from 'src/utils/permissions/PermissionUpdateSchema.js'
|
||||
import type { AppState } from '../state/AppState.js'
|
||||
import type { AttributionState } from '../utils/commitAttribution.js'
|
||||
|
||||
export function isHookEvent(value: string): value is HookEvent {
|
||||
return HOOK_EVENTS.includes(value as HookEvent)
|
||||
}
|
||||
|
||||
// Prompt elicitation protocol types. The `prompt` key acts as discriminator
|
||||
// (mirroring the {async:true} pattern), with the id as its value.
|
||||
export const promptRequestSchema = lazySchema(() =>
|
||||
z.object({
|
||||
prompt: z.string(), // request id
|
||||
message: z.string(),
|
||||
options: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
label: z.string(),
|
||||
description: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
|
||||
export type PromptRequest = z.infer<ReturnType<typeof promptRequestSchema>>
|
||||
|
||||
export type PromptResponse = {
|
||||
prompt_response: string // request id
|
||||
selected: string
|
||||
}
|
||||
|
||||
// Sync hook response schema
|
||||
export const syncHookResponseSchema = lazySchema(() =>
|
||||
z.object({
|
||||
continue: z
|
||||
.boolean()
|
||||
.describe('Whether Claude should continue after hook (default: true)')
|
||||
.optional(),
|
||||
suppressOutput: z
|
||||
.boolean()
|
||||
.describe('Hide stdout from transcript (default: false)')
|
||||
.optional(),
|
||||
stopReason: z
|
||||
.string()
|
||||
.describe('Message shown when continue is false')
|
||||
.optional(),
|
||||
decision: z.enum(['approve', 'block']).optional(),
|
||||
reason: z.string().describe('Explanation for the decision').optional(),
|
||||
systemMessage: z
|
||||
.string()
|
||||
.describe('Warning message shown to the user')
|
||||
.optional(),
|
||||
hookSpecificOutput: z
|
||||
.union([
|
||||
z.object({
|
||||
hookEventName: z.literal('PreToolUse'),
|
||||
permissionDecision: permissionBehaviorSchema().optional(),
|
||||
permissionDecisionReason: z.string().optional(),
|
||||
updatedInput: z.record(z.string(), z.unknown()).optional(),
|
||||
additionalContext: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('UserPromptSubmit'),
|
||||
additionalContext: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('SessionStart'),
|
||||
additionalContext: z.string().optional(),
|
||||
initialUserMessage: z.string().optional(),
|
||||
watchPaths: z
|
||||
.array(z.string())
|
||||
.describe('Absolute paths to watch for FileChanged hooks')
|
||||
.optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('Setup'),
|
||||
additionalContext: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('SubagentStart'),
|
||||
additionalContext: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('PostToolUse'),
|
||||
additionalContext: z.string().optional(),
|
||||
updatedMCPToolOutput: z
|
||||
.unknown()
|
||||
.describe('Updates the output for MCP tools')
|
||||
.optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('PostToolUseFailure'),
|
||||
additionalContext: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('PermissionDenied'),
|
||||
retry: z.boolean().optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('Notification'),
|
||||
additionalContext: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('PermissionRequest'),
|
||||
decision: z.union([
|
||||
z.object({
|
||||
behavior: z.literal('allow'),
|
||||
updatedInput: z.record(z.string(), z.unknown()).optional(),
|
||||
updatedPermissions: z.array(permissionUpdateSchema()).optional(),
|
||||
}),
|
||||
z.object({
|
||||
behavior: z.literal('deny'),
|
||||
message: z.string().optional(),
|
||||
interrupt: z.boolean().optional(),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('Elicitation'),
|
||||
action: z.enum(['accept', 'decline', 'cancel']).optional(),
|
||||
content: z.record(z.string(), z.unknown()).optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('ElicitationResult'),
|
||||
action: z.enum(['accept', 'decline', 'cancel']).optional(),
|
||||
content: z.record(z.string(), z.unknown()).optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('CwdChanged'),
|
||||
watchPaths: z
|
||||
.array(z.string())
|
||||
.describe('Absolute paths to watch for FileChanged hooks')
|
||||
.optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('FileChanged'),
|
||||
watchPaths: z
|
||||
.array(z.string())
|
||||
.describe('Absolute paths to watch for FileChanged hooks')
|
||||
.optional(),
|
||||
}),
|
||||
z.object({
|
||||
hookEventName: z.literal('WorktreeCreate'),
|
||||
worktreePath: z.string(),
|
||||
}),
|
||||
])
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
// Zod schema for hook JSON output validation
|
||||
export const hookJSONOutputSchema = lazySchema(() => {
|
||||
// Async hook response schema
|
||||
const asyncHookResponseSchema = z.object({
|
||||
async: z.literal(true),
|
||||
asyncTimeout: z.number().optional(),
|
||||
})
|
||||
return z.union([asyncHookResponseSchema, syncHookResponseSchema()])
|
||||
})
|
||||
|
||||
// Infer the TypeScript type from the schema
|
||||
type SchemaHookJSONOutput = z.infer<ReturnType<typeof hookJSONOutputSchema>>
|
||||
|
||||
// Type guard function to check if response is sync
|
||||
export function isSyncHookJSONOutput(
|
||||
json: HookJSONOutput,
|
||||
): json is SyncHookJSONOutput {
|
||||
return !('async' in json && json.async === true)
|
||||
}
|
||||
|
||||
// Type guard function to check if response is async
|
||||
export function isAsyncHookJSONOutput(
|
||||
json: HookJSONOutput,
|
||||
): json is AsyncHookJSONOutput {
|
||||
return 'async' in json && json.async === true
|
||||
}
|
||||
|
||||
// Compile-time assertion that SDK and Zod types match
|
||||
import type { IsEqual } from 'type-fest'
|
||||
type Assert<T extends true> = T
|
||||
type _assertSDKTypesMatch = Assert<
|
||||
IsEqual<SchemaHookJSONOutput, HookJSONOutput>
|
||||
>
|
||||
|
||||
/** Context passed to callback hooks for state access */
|
||||
export type HookCallbackContext = {
|
||||
getAppState: () => AppState
|
||||
updateAttributionState: (
|
||||
updater: (prev: AttributionState) => AttributionState,
|
||||
) => void
|
||||
}
|
||||
|
||||
/** Hook that is a callback. */
|
||||
export type HookCallback = {
|
||||
type: 'callback'
|
||||
callback: (
|
||||
input: HookInput,
|
||||
toolUseID: string | null,
|
||||
abort: AbortSignal | undefined,
|
||||
/** Hook index for SessionStart hooks to compute CLAUDE_ENV_FILE path */
|
||||
hookIndex?: number,
|
||||
/** Optional context for accessing app state */
|
||||
context?: HookCallbackContext,
|
||||
) => Promise<HookJSONOutput>
|
||||
/** Timeout in seconds for this hook */
|
||||
timeout?: number
|
||||
/** Internal hooks (e.g. session file access analytics) are excluded from tengu_run_hook metrics */
|
||||
internal?: boolean
|
||||
}
|
||||
|
||||
export type HookCallbackMatcher = {
|
||||
matcher?: string
|
||||
hooks: HookCallback[]
|
||||
pluginName?: string
|
||||
}
|
||||
|
||||
export type HookProgress = {
|
||||
type: 'hook_progress'
|
||||
hookEvent: HookEvent
|
||||
hookName: string
|
||||
command: string
|
||||
promptText?: string
|
||||
statusMessage?: string
|
||||
}
|
||||
|
||||
export type HookBlockingError = {
|
||||
blockingError: string
|
||||
command: string
|
||||
}
|
||||
|
||||
export type PermissionRequestResult =
|
||||
| {
|
||||
behavior: 'allow'
|
||||
updatedInput?: Record<string, unknown>
|
||||
updatedPermissions?: PermissionUpdate[]
|
||||
}
|
||||
| {
|
||||
behavior: 'deny'
|
||||
message?: string
|
||||
interrupt?: boolean
|
||||
}
|
||||
|
||||
export type HookResult = {
|
||||
message?: Message
|
||||
systemMessage?: Message
|
||||
blockingError?: HookBlockingError
|
||||
outcome: 'success' | 'blocking' | 'non_blocking_error' | 'cancelled'
|
||||
preventContinuation?: boolean
|
||||
stopReason?: string
|
||||
permissionBehavior?: 'ask' | 'deny' | 'allow' | 'passthrough'
|
||||
hookPermissionDecisionReason?: string
|
||||
additionalContext?: string
|
||||
initialUserMessage?: string
|
||||
updatedInput?: Record<string, unknown>
|
||||
updatedMCPToolOutput?: unknown
|
||||
permissionRequestResult?: PermissionRequestResult
|
||||
retry?: boolean
|
||||
}
|
||||
|
||||
export type AggregatedHookResult = {
|
||||
message?: Message
|
||||
blockingErrors?: HookBlockingError[]
|
||||
preventContinuation?: boolean
|
||||
stopReason?: string
|
||||
hookPermissionDecisionReason?: string
|
||||
permissionBehavior?: PermissionResult['behavior']
|
||||
additionalContexts?: string[]
|
||||
initialUserMessage?: string
|
||||
updatedInput?: Record<string, unknown>
|
||||
updatedMCPToolOutput?: unknown
|
||||
permissionRequestResult?: PermissionRequestResult
|
||||
retry?: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user