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:
YeonGyu-Kim
2026-02-08 13:57:26 +09:00
parent 7a15c1f3f9
commit df03300211
156 changed files with 7280 additions and 6771 deletions
@@ -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
View File
@@ -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);
}