feat(hooks): implement UserPromptSubmit with chat.message hook and injectHookMessage

- Add chat.message handler to createClaudeCodeHooksHook factory
- Integrate executeUserPromptSubmitHooks() for user prompt processing
- Use injectHookMessage() for file system based message injection
- Add sessionFirstMessageProcessed tracking for title generation skip
- Register chat.message hook in plugin entry point

This completes 100% port of Claude Code hooks from opencode-cc-plugin.

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-09 18:20:13 +09:00
parent 441fc1a219
commit 2ec351d0d8
3 changed files with 146 additions and 0 deletions

View File

@@ -10,6 +10,11 @@ import {
type PostToolUseContext,
type PostToolUseClient,
} from "./post-tool-use"
import {
executeUserPromptSubmitHooks,
type UserPromptSubmitContext,
type MessagePart,
} from "./user-prompt-submit"
import {
executeStopHooks,
type StopContext,
@@ -17,10 +22,99 @@ import {
import { cacheToolInput, getToolInput } from "./tool-input-cache"
import { getTranscriptPath } from "./transcript"
import { log } from "../../shared"
import { injectHookMessage } from "../../features/hook-message-injector"
export function createClaudeCodeHooksHook(ctx: PluginInput) {
const sessionFirstMessageProcessed = new Set<string>()
return {
"chat.message": async (
input: {
sessionID: string
agent?: string
model?: { providerID: string; modelID: string }
messageID?: string
},
output: {
message: Record<string, unknown>
parts: Array<{ type: string; text?: string; [key: string]: unknown }>
}
): Promise<void> => {
try {
const claudeConfig = await loadClaudeHooksConfig()
const extendedConfig = await loadPluginExtendedConfig()
const textParts = output.parts.filter((p) => p.type === "text" && p.text)
const prompt = textParts.map((p) => p.text ?? "").join("\n")
const isFirstMessage = !sessionFirstMessageProcessed.has(input.sessionID)
sessionFirstMessageProcessed.add(input.sessionID)
if (isFirstMessage) {
log("[Claude Hooks] Skipping UserPromptSubmit on first message for title generation")
return
}
let parentSessionId: string | undefined
try {
const sessionInfo = await ctx.client.session.get({
path: { id: input.sessionID },
})
parentSessionId = sessionInfo.data?.parentID
} catch {}
const messageParts: MessagePart[] = textParts.map((p) => ({
type: p.type as "text",
text: p.text,
}))
const userPromptCtx: UserPromptSubmitContext = {
sessionId: input.sessionID,
parentSessionId,
prompt,
parts: messageParts,
cwd: ctx.directory,
}
const result = await executeUserPromptSubmitHooks(
userPromptCtx,
claudeConfig,
extendedConfig
)
if (result.block) {
throw new Error(result.reason ?? "Hook blocked the prompt")
}
if (result.messages.length > 0) {
const hookContent = result.messages.join("\n\n")
const message = output.message as {
agent?: string
model?: { modelID?: string; providerID?: string }
path?: { cwd?: string; root?: string }
tools?: Record<string, boolean>
}
const success = injectHookMessage(input.sessionID, hookContent, {
agent: message.agent,
model: message.model,
path: message.path ?? { cwd: ctx.directory, root: "/" },
tools: message.tools,
})
log(
success
? "[Claude Hooks] Hook message injected via file system"
: "[Claude Hooks] File injection failed",
{ sessionID: input.sessionID }
)
}
} catch (error) {
log("[Claude Hooks] chat.message error:", error)
throw error
}
},
"tool.execute.before": async (
input: { tool: string; sessionID: string; callID: string },
output: { args: Record<string, unknown> }