refactor: wave 1 - extract leaf modules, rename catch-all files, split index.ts hooks
- Split 25+ index.ts files into hook.ts + extracted modules - Rename all catch-all utils.ts/helpers.ts to domain-specific names - Split src/tools/lsp/ into ~15 focused modules - Split src/tools/delegate-task/ into ~18 focused modules - Separate shared types from implementation - 155 files changed, 60+ new files created - All typecheck clean, 61 tests pass
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
|
||||
import { AGENTS_FILENAME } from "./constants";
|
||||
|
||||
export function resolveFilePath(rootDirectory: string, path: string): string | null {
|
||||
if (!path) return null;
|
||||
if (path.startsWith("/")) return path;
|
||||
return resolve(rootDirectory, path);
|
||||
}
|
||||
|
||||
export function findAgentsMdUp(input: {
|
||||
startDir: string;
|
||||
rootDir: string;
|
||||
}): string[] {
|
||||
const found: string[] = [];
|
||||
let current = input.startDir;
|
||||
|
||||
while (true) {
|
||||
// Skip root AGENTS.md - OpenCode's system.ts already loads it via custom()
|
||||
// See: https://github.com/code-yeongyu/oh-my-opencode/issues/379
|
||||
const isRootDir = current === input.rootDir;
|
||||
if (!isRootDir) {
|
||||
const agentsPath = join(current, AGENTS_FILENAME);
|
||||
if (existsSync(agentsPath)) {
|
||||
found.push(agentsPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRootDir) break;
|
||||
const parent = dirname(current);
|
||||
if (parent === current) break;
|
||||
if (!parent.startsWith(input.rootDir)) break;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return found.reverse();
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin";
|
||||
|
||||
import { createDynamicTruncator } from "../../shared/dynamic-truncator";
|
||||
import { processFilePathForAgentsInjection } from "./injector";
|
||||
import { clearInjectedPaths } from "./storage";
|
||||
|
||||
interface ToolExecuteInput {
|
||||
tool: string;
|
||||
sessionID: string;
|
||||
callID: string;
|
||||
}
|
||||
|
||||
interface ToolExecuteOutput {
|
||||
title: string;
|
||||
output: string;
|
||||
metadata: unknown;
|
||||
}
|
||||
|
||||
interface ToolExecuteBeforeOutput {
|
||||
args: unknown;
|
||||
}
|
||||
|
||||
interface EventInput {
|
||||
event: {
|
||||
type: string;
|
||||
properties?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export function createDirectoryAgentsInjectorHook(ctx: PluginInput) {
|
||||
const sessionCaches = new Map<string, Set<string>>();
|
||||
const truncator = createDynamicTruncator(ctx);
|
||||
|
||||
const toolExecuteAfter = async (input: ToolExecuteInput, output: ToolExecuteOutput) => {
|
||||
const toolName = input.tool.toLowerCase();
|
||||
|
||||
if (toolName === "read") {
|
||||
await processFilePathForAgentsInjection({
|
||||
ctx,
|
||||
truncator,
|
||||
sessionCaches,
|
||||
filePath: output.title,
|
||||
sessionID: input.sessionID,
|
||||
output,
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const toolExecuteBefore = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteBeforeOutput,
|
||||
): Promise<void> => {
|
||||
void input;
|
||||
void output;
|
||||
};
|
||||
|
||||
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.before": toolExecuteBefore,
|
||||
"tool.execute.after": toolExecuteAfter,
|
||||
event: eventHandler,
|
||||
};
|
||||
}
|
||||
@@ -1,153 +1 @@
|
||||
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";
|
||||
import { createDynamicTruncator } from "../../shared/dynamic-truncator";
|
||||
|
||||
interface ToolExecuteInput {
|
||||
tool: string;
|
||||
sessionID: string;
|
||||
callID: string;
|
||||
}
|
||||
|
||||
interface ToolExecuteOutput {
|
||||
title: string;
|
||||
output: string;
|
||||
metadata: unknown;
|
||||
}
|
||||
|
||||
interface ToolExecuteBeforeOutput {
|
||||
args: unknown;
|
||||
}
|
||||
|
||||
interface EventInput {
|
||||
event: {
|
||||
type: string;
|
||||
properties?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export function createDirectoryAgentsInjectorHook(ctx: PluginInput) {
|
||||
const sessionCaches = new Map<string, Set<string>>();
|
||||
const truncator = createDynamicTruncator(ctx);
|
||||
|
||||
function getSessionCache(sessionID: string): Set<string> {
|
||||
if (!sessionCaches.has(sessionID)) {
|
||||
sessionCaches.set(sessionID, loadInjectedPaths(sessionID));
|
||||
}
|
||||
return sessionCaches.get(sessionID)!;
|
||||
}
|
||||
|
||||
function resolveFilePath(path: string): string | null {
|
||||
if (!path) return null;
|
||||
if (path.startsWith("/")) return path;
|
||||
return resolve(ctx.directory, path);
|
||||
}
|
||||
|
||||
function findAgentsMdUp(startDir: string): string[] {
|
||||
const found: string[] = [];
|
||||
let current = startDir;
|
||||
|
||||
while (true) {
|
||||
// Skip root AGENTS.md - OpenCode's system.ts already loads it via custom()
|
||||
// See: https://github.com/code-yeongyu/oh-my-opencode/issues/379
|
||||
const isRootDir = current === ctx.directory;
|
||||
if (!isRootDir) {
|
||||
const agentsPath = join(current, AGENTS_FILENAME);
|
||||
if (existsSync(agentsPath)) {
|
||||
found.push(agentsPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRootDir) break;
|
||||
const parent = dirname(current);
|
||||
if (parent === current) break;
|
||||
if (!parent.startsWith(ctx.directory)) break;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return found.reverse();
|
||||
}
|
||||
|
||||
async function processFilePathForInjection(
|
||||
filePath: string,
|
||||
sessionID: string,
|
||||
output: ToolExecuteOutput,
|
||||
): Promise<void> {
|
||||
const resolved = resolveFilePath(filePath);
|
||||
if (!resolved) return;
|
||||
|
||||
const dir = dirname(resolved);
|
||||
const cache = getSessionCache(sessionID);
|
||||
const agentsPaths = findAgentsMdUp(dir);
|
||||
|
||||
for (const agentsPath of agentsPaths) {
|
||||
const agentsDir = dirname(agentsPath);
|
||||
if (cache.has(agentsDir)) continue;
|
||||
|
||||
try {
|
||||
const content = readFileSync(agentsPath, "utf-8");
|
||||
const { result, truncated } = await truncator.truncate(sessionID, content);
|
||||
const truncationNotice = truncated
|
||||
? `\n\n[Note: Content was truncated to save context window space. For full context, please read the file directly: ${agentsPath}]`
|
||||
: "";
|
||||
output.output += `\n\n[Directory Context: ${agentsPath}]\n${result}${truncationNotice}`;
|
||||
cache.add(agentsDir);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
saveInjectedPaths(sessionID, cache);
|
||||
}
|
||||
|
||||
const toolExecuteAfter = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteOutput,
|
||||
) => {
|
||||
const toolName = input.tool.toLowerCase();
|
||||
|
||||
if (toolName === "read") {
|
||||
await processFilePathForInjection(output.title, input.sessionID, output);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const toolExecuteBefore = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteBeforeOutput,
|
||||
): Promise<void> => {
|
||||
void input;
|
||||
void output;
|
||||
};
|
||||
|
||||
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.before": toolExecuteBefore,
|
||||
"tool.execute.after": toolExecuteAfter,
|
||||
event: eventHandler,
|
||||
};
|
||||
}
|
||||
export { createDirectoryAgentsInjectorHook } from "./hook";
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
import type { createDynamicTruncator } from "../../shared/dynamic-truncator";
|
||||
import { findAgentsMdUp, resolveFilePath } from "./finder";
|
||||
import { loadInjectedPaths, saveInjectedPaths } from "./storage";
|
||||
|
||||
type DynamicTruncator = ReturnType<typeof createDynamicTruncator>;
|
||||
|
||||
function getSessionCache(
|
||||
sessionCaches: Map<string, Set<string>>,
|
||||
sessionID: string,
|
||||
): Set<string> {
|
||||
if (!sessionCaches.has(sessionID)) {
|
||||
sessionCaches.set(sessionID, loadInjectedPaths(sessionID));
|
||||
}
|
||||
return sessionCaches.get(sessionID)!;
|
||||
}
|
||||
|
||||
export async function processFilePathForAgentsInjection(input: {
|
||||
ctx: PluginInput;
|
||||
truncator: DynamicTruncator;
|
||||
sessionCaches: Map<string, Set<string>>;
|
||||
filePath: string;
|
||||
sessionID: string;
|
||||
output: { title: string; output: string; metadata: unknown };
|
||||
}): Promise<void> {
|
||||
const resolved = resolveFilePath(input.ctx.directory, input.filePath);
|
||||
if (!resolved) return;
|
||||
|
||||
const dir = dirname(resolved);
|
||||
const cache = getSessionCache(input.sessionCaches, input.sessionID);
|
||||
const agentsPaths = findAgentsMdUp({ startDir: dir, rootDir: input.ctx.directory });
|
||||
|
||||
for (const agentsPath of agentsPaths) {
|
||||
const agentsDir = dirname(agentsPath);
|
||||
if (cache.has(agentsDir)) continue;
|
||||
|
||||
try {
|
||||
const content = readFileSync(agentsPath, "utf-8");
|
||||
const { result, truncated } = await input.truncator.truncate(
|
||||
input.sessionID,
|
||||
content,
|
||||
);
|
||||
const truncationNotice = truncated
|
||||
? `\n\n[Note: Content was truncated to save context window space. For full context, please read the file directly: ${agentsPath}]`
|
||||
: "";
|
||||
input.output.output += `\n\n[Directory Context: ${agentsPath}]\n${result}${truncationNotice}`;
|
||||
cache.add(agentsDir);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
saveInjectedPaths(input.sessionID, cache);
|
||||
}
|
||||
Reference in New Issue
Block a user