()
| 73 | * matcher-scoped pass. |
| 74 | */ |
| 75 | export function buildPluginSource(): string { |
| 76 | return `// ${TEAMAI_MARKER} hooks plugin — generated by teamai, do not edit by hand. |
| 77 | // |
| 78 | // Bridges OpenCode's plugin events to \`teamai hook-dispatch\`, mirroring the |
| 79 | // Claude built-in hook set. Feeds the same STDIN JSON payload other agents send |
| 80 | // so the track / hint handlers and project-scope gating work. Errors are |
| 81 | // swallowed; OpenCode cannot inject a hook's stdout back into the session, so |
| 82 | // this runs the dispatch purely for its side effects (status report / sync / |
| 83 | // update). |
| 84 | // |
| 85 | // NOTE: OpenCode awaits its named hooks (chat.message, tool.execute.after), so |
| 86 | // the dispatch is not truly fire-and-forget for those two — see the timeout |
| 87 | // note below. |
| 88 | |
| 89 | /** OpenCode lowercase tool ids → Claude PascalCase matcher names. */ |
| 90 | const TOOL_MATCHER = { skill: 'Skill', todowrite: 'TodoWrite' }; |
| 91 | |
| 92 | /** @param {{ $: any, directory?: string, worktree?: string }} ctx */ |
| 93 | export const TeamaiHooks = async ({ $, directory, worktree }) => { |
| 94 | const cwd = directory || worktree; |
| 95 | // Dispatch one hook event, forwarding a JSON payload on STDIN. \`payload\` |
| 96 | // fields (cwd / tool_name / tool_input / prompt) match what hook-dispatch's |
| 97 | // handlers read; \`matcher\` scopes PostToolUse handlers (Skill / TodoWrite). |
| 98 | const dispatch = async (event, matcher, payload) => { |
| 99 | try { |
| 100 | const args = ['hook-dispatch', event, '--tool', 'opencode']; |
| 101 | if (matcher) { |
| 102 | args.push('--matcher', matcher); |
| 103 | } |
| 104 | const stdin = JSON.stringify({ cwd, ...(payload || {}) }); |
| 105 | // Redirect the payload into STDIN via a Response (Bun shell can only |
| 106 | // redirect Response/Buffer/Blob, not a bare string). .quiet() suppresses |
| 107 | // output; .nothrow() keeps a non-zero exit (e.g. no teamai on PATH) from |
| 108 | // throwing into the agent session. |
| 109 | await $\`teamai \${args} < \${new Response(stdin)}\`.quiet().nothrow(); |
| 110 | } catch { |
| 111 | // never block the agent |
| 112 | } |
| 113 | }; |
| 114 | |
| 115 | return { |
| 116 | event: async ({ event }) => { |
| 117 | if (event.type === 'session.created') { |
| 118 | await dispatch('session-start'); |
| 119 | } else if (event.type === 'session.idle') { |
| 120 | await dispatch('stop'); |
| 121 | } |
| 122 | }, |
| 123 | // A new user message maps to Claude's UserPromptSubmit. The prompt text |
| 124 | // lives in output.parts (text parts); forward it so track-slash can see |
| 125 | // slash-command usage. |
| 126 | 'chat.message': async (_input, output) => { |
| 127 | let prompt = ''; |
| 128 | const parts = (output && output.parts) || []; |
| 129 | for (const part of parts) { |
| 130 | if (part && part.type === 'text' && typeof part.text === 'string') { |
| 131 | prompt += part.text; |
| 132 | } |
no outgoing calls
no test coverage detected