From 2ec351d0d86f285b9cadcc226434f48d4ed87e9a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 9 Dec 2025 18:20:13 +0900 Subject: [PATCH] feat(hooks): implement UserPromptSubmit with chat.message hook and injectHookMessage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- notepad.md | 48 ++++++++++++++ src/hooks/claude-code-hooks/index.ts | 94 ++++++++++++++++++++++++++++ src/index.ts | 4 ++ 3 files changed, 146 insertions(+) diff --git a/notepad.md b/notepad.md index daee604..313eeec 100644 --- a/notepad.md +++ b/notepad.md @@ -795,3 +795,51 @@ All tasks execution STARTED: Thu Dec 4 16:52:57 KST 2025 소요 시간: ~6분 --- + +## [2025-12-09 18:16] - hook-message-injector 사용 패턴 크로스체크 + +### DISCOVERED ISSUES +- **CRITICAL**: UserPromptSubmit hooks가 oh-my-opencode에 완전히 누락됨 +- opencode-cc-plugin에서는 chat.message hook으로 UserPromptSubmit 처리 +- oh-my-opencode에는 chat.message hook이 구현되지 않음 +- user-prompt-submit.ts는 정의만 있고 실제 사용처 없음 + +### IMPLEMENTATION DECISIONS +- PostToolUse는 이미 올바르게 구현됨: + * opencode-cc-plugin: result.message를 tool output에 append + * oh-my-opencode: 동일한 방식 사용 (claude-code-hooks/index.ts:95-97) + * injectHookMessage() 불필요 +- UserPromptSubmit 구현 추가: + * chat.message hook handler 추가 (claude-code-hooks/index.ts) + * executeUserPromptSubmitHooks() 호출 + * sessionFirstMessageProcessed Set으로 첫 메시지 skip (title generation) + * result.messages가 있으면 injectHookMessage() 호출 + * src/index.ts에 hook 등록 +- Import 추가: + * executeUserPromptSubmitHooks, UserPromptSubmitContext, MessagePart from ./user-prompt-submit + * injectHookMessage from ../../features/hook-message-injector + +### PROBLEMS FOR NEXT TASKS +- None - UserPromptSubmit 통합 완료 + +### VERIFICATION RESULTS +- Ran: `bunx tsc --noEmit` → exit 0, no errors +- Ran: `bun run build` → exit 0, successful build +- Files modified: + * src/hooks/claude-code-hooks/index.ts (chat.message handler 추가) + * src/index.ts (chat.message hook 등록) +- Verified OpenCode Plugin API: chat.message hook 공식 지원 확인 (@opencode-ai/plugin/dist/index.d.ts:112-123) + +### LEARNINGS +- OpenCode Plugin API에 chat.message hook 존재: + * input: sessionID, agent?, model?, messageID? + * output: message, parts[] +- PostToolUse는 tool output에 직접 append (injectHookMessage 불필요) +- UserPromptSubmit는 file system injection 사용 (injectHookMessage 필수) +- opencode-cc-plugin 구조: src/plugin/chat-handler.ts → handleChatMessage() +- oh-my-opencode 구조: src/hooks/claude-code-hooks/index.ts → createClaudeCodeHooksHook() +- 첫 메시지 skip 로직: title generation을 위해 UserPromptSubmit hooks 실행 안 함 + +소요 시간: ~8분 + +--- diff --git a/src/hooks/claude-code-hooks/index.ts b/src/hooks/claude-code-hooks/index.ts index 904f596..754ce7f 100644 --- a/src/hooks/claude-code-hooks/index.ts +++ b/src/hooks/claude-code-hooks/index.ts @@ -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() return { + "chat.message": async ( + input: { + sessionID: string + agent?: string + model?: { providerID: string; modelID: string } + messageID?: string + }, + output: { + message: Record + parts: Array<{ type: string; text?: string; [key: string]: unknown }> + } + ): Promise => { + 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 + } + + 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 } diff --git a/src/index.ts b/src/index.ts index 5998f06..e3caa2f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,6 +86,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { return { tool: builtinTools, + "chat.message": async (input, output) => { + await claudeCodeHooks["chat.message"]?.(input, output) + }, + config: async (config) => { const builtinAgents = createBuiltinAgents( pluginConfig.disabled_agents,