chore: initialize recovered claude workspace
This commit is contained in:
211
src/utils/hooks/execPromptHook.ts
Normal file
211
src/utils/hooks/execPromptHook.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import type { HookEvent } from 'src/entrypoints/agentSdkTypes.js'
|
||||
import { queryModelWithoutStreaming } from '../../services/api/claude.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import { createAttachmentMessage } from '../attachments.js'
|
||||
import { createCombinedAbortSignal } from '../combinedAbortSignal.js'
|
||||
import { logForDebugging } from '../debug.js'
|
||||
import { errorMessage } from '../errors.js'
|
||||
import type { HookResult } from '../hooks.js'
|
||||
import { safeParseJSON } from '../json.js'
|
||||
import { createUserMessage, extractTextContent } from '../messages.js'
|
||||
import { getSmallFastModel } from '../model/model.js'
|
||||
import type { PromptHook } from '../settings/types.js'
|
||||
import { asSystemPrompt } from '../systemPromptType.js'
|
||||
import { addArgumentsToPrompt, hookResponseSchema } from './hookHelpers.js'
|
||||
|
||||
/**
|
||||
* Execute a prompt-based hook using an LLM
|
||||
*/
|
||||
export async function execPromptHook(
|
||||
hook: PromptHook,
|
||||
hookName: string,
|
||||
hookEvent: HookEvent,
|
||||
jsonInput: string,
|
||||
signal: AbortSignal,
|
||||
toolUseContext: ToolUseContext,
|
||||
messages?: Message[],
|
||||
toolUseID?: string,
|
||||
): Promise<HookResult> {
|
||||
// Use provided toolUseID or generate a new one
|
||||
const effectiveToolUseID = toolUseID || `hook-${randomUUID()}`
|
||||
try {
|
||||
// Replace $ARGUMENTS with the JSON input
|
||||
const processedPrompt = addArgumentsToPrompt(hook.prompt, jsonInput)
|
||||
logForDebugging(
|
||||
`Hooks: Processing prompt hook with prompt: ${processedPrompt}`,
|
||||
)
|
||||
|
||||
// Create user message directly - no need for processUserInput which would
|
||||
// trigger UserPromptSubmit hooks and cause infinite recursion
|
||||
const userMessage = createUserMessage({ content: processedPrompt })
|
||||
|
||||
// Prepend conversation history if provided
|
||||
const messagesToQuery =
|
||||
messages && messages.length > 0
|
||||
? [...messages, userMessage]
|
||||
: [userMessage]
|
||||
|
||||
logForDebugging(
|
||||
`Hooks: Querying model with ${messagesToQuery.length} messages`,
|
||||
)
|
||||
|
||||
// Query the model with Haiku
|
||||
const hookTimeoutMs = hook.timeout ? hook.timeout * 1000 : 30000
|
||||
|
||||
// Combined signal: aborts if either the hook signal or timeout triggers
|
||||
const { signal: combinedSignal, cleanup: cleanupSignal } =
|
||||
createCombinedAbortSignal(signal, { timeoutMs: hookTimeoutMs })
|
||||
|
||||
try {
|
||||
const response = await queryModelWithoutStreaming({
|
||||
messages: messagesToQuery,
|
||||
systemPrompt: asSystemPrompt([
|
||||
`You are evaluating a hook in Claude Code.
|
||||
|
||||
Your response must be a JSON object matching one of the following schemas:
|
||||
1. If the condition is met, return: {"ok": true}
|
||||
2. If the condition is not met, return: {"ok": false, "reason": "Reason for why it is not met"}`,
|
||||
]),
|
||||
thinkingConfig: { type: 'disabled' as const },
|
||||
tools: toolUseContext.options.tools,
|
||||
signal: combinedSignal,
|
||||
options: {
|
||||
async getToolPermissionContext() {
|
||||
const appState = toolUseContext.getAppState()
|
||||
return appState.toolPermissionContext
|
||||
},
|
||||
model: hook.model ?? getSmallFastModel(),
|
||||
toolChoice: undefined,
|
||||
isNonInteractiveSession: true,
|
||||
hasAppendSystemPrompt: false,
|
||||
agents: [],
|
||||
querySource: 'hook_prompt',
|
||||
mcpTools: [],
|
||||
agentId: toolUseContext.agentId,
|
||||
outputFormat: {
|
||||
type: 'json_schema',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ok: { type: 'boolean' },
|
||||
reason: { type: 'string' },
|
||||
},
|
||||
required: ['ok'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cleanupSignal()
|
||||
|
||||
// Extract text content from response
|
||||
const content = extractTextContent(response.message.content)
|
||||
|
||||
// Update response length for spinner display
|
||||
toolUseContext.setResponseLength(length => length + content.length)
|
||||
|
||||
const fullResponse = content.trim()
|
||||
logForDebugging(`Hooks: Model response: ${fullResponse}`)
|
||||
|
||||
const json = safeParseJSON(fullResponse)
|
||||
if (!json) {
|
||||
logForDebugging(
|
||||
`Hooks: error parsing response as JSON: ${fullResponse}`,
|
||||
)
|
||||
return {
|
||||
hook,
|
||||
outcome: 'non_blocking_error',
|
||||
message: createAttachmentMessage({
|
||||
type: 'hook_non_blocking_error',
|
||||
hookName,
|
||||
toolUseID: effectiveToolUseID,
|
||||
hookEvent,
|
||||
stderr: 'JSON validation failed',
|
||||
stdout: fullResponse,
|
||||
exitCode: 1,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = hookResponseSchema().safeParse(json)
|
||||
if (!parsed.success) {
|
||||
logForDebugging(
|
||||
`Hooks: model response does not conform to expected schema: ${parsed.error.message}`,
|
||||
)
|
||||
return {
|
||||
hook,
|
||||
outcome: 'non_blocking_error',
|
||||
message: createAttachmentMessage({
|
||||
type: 'hook_non_blocking_error',
|
||||
hookName,
|
||||
toolUseID: effectiveToolUseID,
|
||||
hookEvent,
|
||||
stderr: `Schema validation failed: ${parsed.error.message}`,
|
||||
stdout: fullResponse,
|
||||
exitCode: 1,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// Failed to meet condition
|
||||
if (!parsed.data.ok) {
|
||||
logForDebugging(
|
||||
`Hooks: Prompt hook condition was not met: ${parsed.data.reason}`,
|
||||
)
|
||||
return {
|
||||
hook,
|
||||
outcome: 'blocking',
|
||||
blockingError: {
|
||||
blockingError: `Prompt hook condition was not met: ${parsed.data.reason}`,
|
||||
command: hook.prompt,
|
||||
},
|
||||
preventContinuation: true,
|
||||
stopReason: parsed.data.reason,
|
||||
}
|
||||
}
|
||||
|
||||
// Condition was met
|
||||
logForDebugging(`Hooks: Prompt hook condition was met`)
|
||||
return {
|
||||
hook,
|
||||
outcome: 'success',
|
||||
message: createAttachmentMessage({
|
||||
type: 'hook_success',
|
||||
hookName,
|
||||
toolUseID: effectiveToolUseID,
|
||||
hookEvent,
|
||||
content: '',
|
||||
}),
|
||||
}
|
||||
} catch (error) {
|
||||
cleanupSignal()
|
||||
|
||||
if (combinedSignal.aborted) {
|
||||
return {
|
||||
hook,
|
||||
outcome: 'cancelled',
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = errorMessage(error)
|
||||
logForDebugging(`Hooks: Prompt hook error: ${errorMsg}`)
|
||||
return {
|
||||
hook,
|
||||
outcome: 'non_blocking_error',
|
||||
message: createAttachmentMessage({
|
||||
type: 'hook_non_blocking_error',
|
||||
hookName,
|
||||
toolUseID: effectiveToolUseID,
|
||||
hookEvent,
|
||||
stderr: `Error executing prompt hook: ${errorMsg}`,
|
||||
stdout: '',
|
||||
exitCode: 1,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user