import type { PluginInput } from "@opencode-ai/plugin"; import { existsSync, readFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { loadInjectedPaths, saveInjectedPaths, clearInjectedPaths, } from "./storage"; import { AGENTS_FILENAME } from "./constants"; interface ToolExecuteInput { tool: string; sessionID: string; callID: string; } interface ToolExecuteOutput { title: string; output: string; metadata: unknown; } interface EventInput { event: { type: string; properties?: unknown; }; } export function createDirectoryAgentsInjectorHook(ctx: PluginInput) { const sessionCaches = new Map>(); function getSessionCache(sessionID: string): Set { if (!sessionCaches.has(sessionID)) { sessionCaches.set(sessionID, loadInjectedPaths(sessionID)); } return sessionCaches.get(sessionID)!; } function resolveFilePath(title: string): string | null { if (!title) return null; if (title.startsWith("/")) return title; return resolve(ctx.directory, title); } function findAgentsMdUp(startDir: string): string[] { const found: string[] = []; let current = startDir; while (true) { const agentsPath = join(current, AGENTS_FILENAME); if (existsSync(agentsPath)) { found.push(agentsPath); } if (current === ctx.directory) break; const parent = dirname(current); if (parent === current) break; if (!parent.startsWith(ctx.directory)) break; current = parent; } return found.reverse(); } const toolExecuteAfter = async ( input: ToolExecuteInput, output: ToolExecuteOutput, ) => { if (input.tool.toLowerCase() !== "read") return; const filePath = resolveFilePath(output.title); if (!filePath) return; const dir = dirname(filePath); const cache = getSessionCache(input.sessionID); const agentsPaths = findAgentsMdUp(dir); const toInject: { path: string; content: string }[] = []; for (const agentsPath of agentsPaths) { const agentsDir = dirname(agentsPath); if (cache.has(agentsDir)) continue; try { const content = readFileSync(agentsPath, "utf-8"); toInject.push({ path: agentsPath, content }); cache.add(agentsDir); } catch {} } if (toInject.length === 0) return; for (const { path, content } of toInject) { output.output += `\n\n[Directory Context: ${path}]\n${content}`; } saveInjectedPaths(input.sessionID, cache); }; const eventHandler = async ({ event }: EventInput) => { const props = event.properties as Record | undefined; if (event.type === "session.deleted") { const sessionInfo = props?.info as { id?: string } | undefined; if (sessionInfo?.id) { sessionCaches.delete(sessionInfo.id); clearInjectedPaths(sessionInfo.id); } } if (event.type === "session.compacted") { const sessionID = (props?.sessionID ?? (props?.info as { id?: string } | undefined)?.id) as string | undefined; if (sessionID) { sessionCaches.delete(sessionID); clearInjectedPaths(sessionID); } } }; return { "tool.execute.after": toolExecuteAfter, event: eventHandler, }; }