auto-update-daily-20260202
1/**
2 * OpenCode plugin that wraps Claude Code hooks (Go binaries)
3 *
4 * This plugin provides compatibility with existing claude-hooks-* binaries
5 * by translating OpenCode's event format to Claude Code's JSON format.
6 *
7 * Hooks ported:
8 * - tool.execute.before -> claude-hooks-validate-git-push
9 * - tool.execute.after -> claude-hooks-capture-tool-output, claude-hooks-update-terminal-title
10 * - session.created -> claude-hooks-initialize-session
11 * - session.idle -> claude-hooks-save-session (approximation of SessionEnd)
12 */
13
14import type { Plugin } from "@opencode-ai/plugin"
15
16/**
17 * Convert OpenCode tool input to Claude Code PreToolUse format
18 */
19function toClaudePreToolUse(input: any): string {
20 return JSON.stringify({
21 tool_name: capitalizeFirst(input.tool || input.call?.name || "unknown"),
22 tool_input: input.args || input.call?.input || {},
23 conversation_id: "opencode",
24 })
25}
26
27/**
28 * Convert OpenCode tool result to Claude Code PostToolUse format
29 */
30function toClaudePostToolUse(input: any, result: any): string {
31 return JSON.stringify({
32 tool_name: capitalizeFirst(input.tool || input.call?.name || "unknown"),
33 tool_input: input.args || input.call?.input || {},
34 tool_response: result || {},
35 conversation_id: "opencode",
36 timestamp: new Date().toISOString(),
37 })
38}
39
40function capitalizeFirst(str: string): string {
41 return str.charAt(0).toUpperCase() + str.slice(1)
42}
43
44export const ClaudeHooksPlugin: Plugin = async ({ $ }) => {
45 // Track if we've already run session init (debounce)
46 let sessionInitialized = false
47
48 return {
49 /**
50 * PreToolUse equivalent - validate git push commands
51 */
52 "tool.execute.before": async (input, output) => {
53 // Only check bash/shell commands
54 const toolName = input.tool || input.call?.name || ""
55 if (toolName.toLowerCase() !== "bash" && toolName.toLowerCase() !== "shell") {
56 return
57 }
58
59 try {
60 const jsonInput = toClaudePreToolUse(input)
61 const result = await $`echo ${jsonInput} | claude-hooks-validate-git-push`.quiet()
62
63 if (result.exitCode !== 0) {
64 // Block the command
65 const stderr = result.stderr.toString().trim()
66 output.abort = stderr || "Blocked by claude-hooks-validate-git-push"
67 }
68 } catch (error) {
69 // Don't block on hook errors - fail open
70 console.error("[claude-hooks] validate-git-push error:", error)
71 }
72 },
73
74 /**
75 * PostToolUse equivalent - capture tool output and update terminal title
76 */
77 "tool.execute.after": async (input, output) => {
78 try {
79 const jsonInput = toClaudePostToolUse(input, output)
80
81 // Run both hooks in parallel (fire and forget)
82 await Promise.allSettled([
83 $`echo ${jsonInput} | claude-hooks-capture-tool-output`.quiet(),
84 $`echo ${jsonInput} | claude-hooks-update-terminal-title`.quiet(),
85 ])
86 } catch (error) {
87 // Silent failure - don't disrupt workflow
88 console.error("[claude-hooks] post-tool error:", error)
89 }
90 },
91
92 /**
93 * Event handler for session lifecycle
94 */
95 event: async ({ event }) => {
96 // SessionStart equivalent
97 if (event.type === "session.created" && !sessionInitialized) {
98 sessionInitialized = true
99 try {
100 await $`claude-hooks-initialize-session`.quiet()
101 } catch (error) {
102 console.error("[claude-hooks] initialize-session error:", error)
103 }
104 }
105
106 // SessionEnd equivalent (session.idle is closest approximation)
107 // Note: This triggers when the session becomes idle, not on explicit end
108 // Uncomment if you want save-session prompts on idle:
109 // if (event.type === "session.idle") {
110 // try {
111 // await $`claude-hooks-save-session`.quiet()
112 // } catch (error) {
113 // console.error("[claude-hooks] save-session error:", error)
114 // }
115 // }
116 },
117 }
118}
119
120// Default export for OpenCode plugin discovery
121export default ClaudeHooksPlugin