feat(hooks): implement directory-agents-injector hook
This commit is contained in:
9
src/hooks/directory-agents-injector/constants.ts
Normal file
9
src/hooks/directory-agents-injector/constants.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
|
import { xdgData } from "xdg-basedir";
|
||||||
|
|
||||||
|
export const OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage");
|
||||||
|
export const AGENTS_INJECTOR_STORAGE = join(
|
||||||
|
OPENCODE_STORAGE,
|
||||||
|
"directory-agents",
|
||||||
|
);
|
||||||
|
export const AGENTS_FILENAME = "AGENTS.md";
|
||||||
126
src/hooks/directory-agents-injector/index.ts
Normal file
126
src/hooks/directory-agents-injector/index.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
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<string, Set<string>>();
|
||||||
|
|
||||||
|
function getSessionCache(sessionID: string): Set<string> {
|
||||||
|
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<string, unknown> | 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
48
src/hooks/directory-agents-injector/storage.ts
Normal file
48
src/hooks/directory-agents-injector/storage.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
writeFileSync,
|
||||||
|
unlinkSync,
|
||||||
|
} from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { AGENTS_INJECTOR_STORAGE } from "./constants";
|
||||||
|
import type { InjectedPathsData } from "./types";
|
||||||
|
|
||||||
|
function getStoragePath(sessionID: string): string {
|
||||||
|
return join(AGENTS_INJECTOR_STORAGE, `${sessionID}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadInjectedPaths(sessionID: string): Set<string> {
|
||||||
|
const filePath = getStoragePath(sessionID);
|
||||||
|
if (!existsSync(filePath)) return new Set();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = readFileSync(filePath, "utf-8");
|
||||||
|
const data: InjectedPathsData = JSON.parse(content);
|
||||||
|
return new Set(data.injectedPaths);
|
||||||
|
} catch {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveInjectedPaths(sessionID: string, paths: Set<string>): void {
|
||||||
|
if (!existsSync(AGENTS_INJECTOR_STORAGE)) {
|
||||||
|
mkdirSync(AGENTS_INJECTOR_STORAGE, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: InjectedPathsData = {
|
||||||
|
sessionID,
|
||||||
|
injectedPaths: [...paths],
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFileSync(getStoragePath(sessionID), JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearInjectedPaths(sessionID: string): void {
|
||||||
|
const filePath = getStoragePath(sessionID);
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/hooks/directory-agents-injector/types.ts
Normal file
5
src/hooks/directory-agents-injector/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface InjectedPathsData {
|
||||||
|
sessionID: string;
|
||||||
|
injectedPaths: string[];
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user