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:
48
notepad.md
48
notepad.md
@@ -795,3 +795,51 @@ All tasks execution STARTED: Thu Dec 4 16:52:57 KST 2025
|
|||||||
소요 시간: ~6분
|
소요 시간: ~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분
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import {
|
|||||||
type PostToolUseContext,
|
type PostToolUseContext,
|
||||||
type PostToolUseClient,
|
type PostToolUseClient,
|
||||||
} from "./post-tool-use"
|
} from "./post-tool-use"
|
||||||
|
import {
|
||||||
|
executeUserPromptSubmitHooks,
|
||||||
|
type UserPromptSubmitContext,
|
||||||
|
type MessagePart,
|
||||||
|
} from "./user-prompt-submit"
|
||||||
import {
|
import {
|
||||||
executeStopHooks,
|
executeStopHooks,
|
||||||
type StopContext,
|
type StopContext,
|
||||||
@@ -17,10 +22,99 @@ import {
|
|||||||
import { cacheToolInput, getToolInput } from "./tool-input-cache"
|
import { cacheToolInput, getToolInput } from "./tool-input-cache"
|
||||||
import { getTranscriptPath } from "./transcript"
|
import { getTranscriptPath } from "./transcript"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared"
|
||||||
|
import { injectHookMessage } from "../../features/hook-message-injector"
|
||||||
|
|
||||||
export function createClaudeCodeHooksHook(ctx: PluginInput) {
|
export function createClaudeCodeHooksHook(ctx: PluginInput) {
|
||||||
|
const sessionFirstMessageProcessed = new Set<string>()
|
||||||
|
|
||||||
return {
|
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 (
|
"tool.execute.before": async (
|
||||||
input: { tool: string; sessionID: string; callID: string },
|
input: { tool: string; sessionID: string; callID: string },
|
||||||
output: { args: Record<string, unknown> }
|
output: { args: Record<string, unknown> }
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
return {
|
return {
|
||||||
tool: builtinTools,
|
tool: builtinTools,
|
||||||
|
|
||||||
|
"chat.message": async (input, output) => {
|
||||||
|
await claudeCodeHooks["chat.message"]?.(input, output)
|
||||||
|
},
|
||||||
|
|
||||||
config: async (config) => {
|
config: async (config) => {
|
||||||
const builtinAgents = createBuiltinAgents(
|
const builtinAgents = createBuiltinAgents(
|
||||||
pluginConfig.disabled_agents,
|
pluginConfig.disabled_agents,
|
||||||
|
|||||||
Reference in New Issue
Block a user