// Entire CLI plugin for OpenCode // Auto-generated by `entire enable --agent opencode` // Do not edit manually — changes will be overwritten on next install. // Requires Bun runtime (used by OpenCode's plugin system for loading ESM plugins). import type { Plugin } from "@opencode-ai/plugin" export const EntirePlugin: Plugin = async ({ directory }) => { const ENTIRE_CMD = 'entire' // Track seen user messages to fire turn-start only once per message const seenUserMessages = new Set() // Track current session ID for message events (which don't include sessionID) let currentSessionID: string | null = null // Track the model used by the most recent assistant message let currentModel: string | null = null // In-memory store for message metadata (role, tokens, etc.) const messageStore = new Map() /** * Build the shell command for a hook invocation. * Uses sh -c so that shell command substitution in ENTIRE_CMD * (e.g., $(git rev-parse --show-toplevel) for local-dev) is interpreted. */ function hookCmd(hookName: string): string[] { if (ENTIRE_CMD !== "entire") { return ["sh", "-c", `${ENTIRE_CMD} hooks opencode ${hookName}`] } return ["sh", "-c", `if ! command -v entire >/dev/null 2>&1; then exit 0; fi; exec entire hooks opencode ${hookName}`] } /** * Pipe JSON payload to an entire hooks command (async). * Errors are logged but never thrown — plugin failures must not crash OpenCode. */ async function callHook(hookName: string, payload: Record) { try { const json = JSON.stringify(payload) const proc = Bun.spawn(hookCmd(hookName), { cwd: directory, stdin: new Blob([json + "\n"]), stdout: "ignore", stderr: "ignore", }) await proc.exited } catch { // Silently ignore — plugin failures must not crash OpenCode } } /** * Synchronous variant for hooks that must complete before subsequent agent work * or process exit. `turn-start` must finish initializing session state before a * fast mid-turn commit can hit git hooks, and `turn-end` / `session-end` must * finish before `opencode run` tears down its event loop. */ function callHookSync(hookName: string, payload: Record) { try { const json = JSON.stringify(payload) Bun.spawnSync(hookCmd(hookName), { cwd: directory, stdin: new TextEncoder().encode(json + "\n"), stdout: "ignore", stderr: "ignore", }) } catch { // Silently ignore — plugin failures must not crash OpenCode } } function resetSessionTracking(sessionID: string) { if (currentSessionID === sessionID) { return false } seenUserMessages.clear() messageStore.clear() currentModel = null currentSessionID = sessionID return true } return { event: async ({ event }) => { try { switch (event.type) { case "session.created": { const session = (event as any).properties?.info if (!session?.id) break // Reset per-session tracking state when switching sessions. if (resetSessionTracking(session.id)) { const json = JSON.stringify({ session_id: session.id, }) const proc = Bun.spawn(hookCmd("session-start"), { cwd: directory, stdin: new Blob([json + "\n"]), stdout: "ignore", stderr: "ignore", }) await proc.exited } break } case "message.updated": { const msg = (event as any).properties?.info if (!msg) break if (msg.sessionID && resetSessionTracking(msg.sessionID)) { callHookSync("session-start", { session_id: msg.sessionID, }) } // Store message metadata (role, time, tokens, etc.) messageStore.set(msg.id, msg) // Track model from assistant messages if (msg.role === "assistant" && msg.modelID) { currentModel = msg.modelID } // Fallback: some opencode run flows commit before any message.part.updated // event is delivered for the user's prompt. Start the turn from the // user message itself so git hooks see an ACTIVE session in time. if (msg.role === "user" && !seenUserMessages.has(msg.id)) { seenUserMessages.add(msg.id) const sessionID = msg.sessionID ?? currentSessionID if (sessionID) { callHookSync("turn-start", { session_id: sessionID, prompt: "", model: currentModel ?? "", }) } } break } case "message.part.updated": { const part = (event as any).properties?.part if (!part?.messageID) break // Fire turn-start on the first text part of a new user message const msg = messageStore.get(part.messageID) if (msg?.role === "user" && part.type === "text" && !seenUserMessages.has(msg.id)) { seenUserMessages.add(msg.id) const sessionID = msg.sessionID ?? currentSessionID if (sessionID) { callHookSync("turn-start", { session_id: sessionID, prompt: part.text ?? "", model: currentModel ?? "", }) } } break } case "session.status": { // session.status fires in both TUI and non-interactive (run) mode. // session.idle is deprecated and not reliably emitted in run mode. const props = (event as any).properties if (props?.status?.type !== "idle") break const sessionID = props?.sessionID ?? currentSessionID if (!sessionID) break // Use sync variant: `opencode run` exits on the same idle event, // so an async hook would be killed before completing. callHookSync("turn-end", { session_id: sessionID, model: currentModel ?? "", }) break } case "session.compacted": { const sessionID = (event as any).properties?.sessionID if (!sessionID) break await callHook("compaction", { session_id: sessionID, }) break } case "session.deleted": { const session = (event as any).properties?.info if (!session?.id) break seenUserMessages.clear() messageStore.clear() currentSessionID = null // Use sync variant: session-end may fire during shutdown. callHookSync("session-end", { session_id: session.id, }) break } case "server.instance.disposed": { // Fires when OpenCode shuts down (TUI close or `opencode run` exit). // session.deleted only fires on explicit user deletion, not on quit, // so this is the only reliable way to end sessions on exit. if (!currentSessionID) break const sessionID = currentSessionID seenUserMessages.clear() messageStore.clear() currentSessionID = null // Use sync variant: this is the last event before process exit. callHookSync("session-end", { session_id: sessionID, }) break } } } catch { // Silently ignore — plugin failures must not crash OpenCode } }, } }