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 71ac54c33e
commit 29155ec7bc
156 changed files with 7280 additions and 6771 deletions
+27
View File
@@ -0,0 +1,27 @@
import { clearInjectedRules, loadInjectedRules } from "./storage";
export type SessionInjectedRulesCache = {
contentHashes: Set<string>;
realPaths: Set<string>;
};
export function createSessionCacheStore(): {
getSessionCache: (sessionID: string) => SessionInjectedRulesCache;
clearSessionCache: (sessionID: string) => void;
} {
const sessionCaches = new Map<string, SessionInjectedRulesCache>();
function getSessionCache(sessionID: string): SessionInjectedRulesCache {
if (!sessionCaches.has(sessionID)) {
sessionCaches.set(sessionID, loadInjectedRules(sessionID));
}
return sessionCaches.get(sessionID)!;
}
function clearSessionCache(sessionID: string): void {
sessionCaches.delete(sessionID);
clearInjectedRules(sessionID);
}
return { getSessionCache, clearSessionCache };
}
+87
View File
@@ -0,0 +1,87 @@
import type { PluginInput } from "@opencode-ai/plugin";
import { createDynamicTruncator } from "../../shared/dynamic-truncator";
import { getRuleInjectionFilePath } from "./output-path";
import { createSessionCacheStore } from "./cache";
import { createRuleInjectionProcessor } from "./injector";
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;
};
}
const TRACKED_TOOLS = ["read", "write", "edit", "multiedit"];
export function createRulesInjectorHook(ctx: PluginInput) {
const truncator = createDynamicTruncator(ctx);
const { getSessionCache, clearSessionCache } = createSessionCacheStore();
const { processFilePathForInjection } = createRuleInjectionProcessor({
workspaceDirectory: ctx.directory,
truncator,
getSessionCache,
});
const toolExecuteAfter = async (
input: ToolExecuteInput,
output: ToolExecuteOutput
) => {
const toolName = input.tool.toLowerCase();
if (TRACKED_TOOLS.includes(toolName)) {
const filePath = getRuleInjectionFilePath(output);
if (!filePath) return;
await processFilePathForInjection(filePath, 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) {
clearSessionCache(sessionInfo.id);
}
}
if (event.type === "session.compacted") {
const sessionID = (props?.sessionID ??
(props?.info as { id?: string } | undefined)?.id) as string | undefined;
if (sessionID) {
clearSessionCache(sessionID);
}
}
};
return {
"tool.execute.before": toolExecuteBefore,
"tool.execute.after": toolExecuteAfter,
event: eventHandler,
};
}
+1 -190
View File
@@ -1,190 +1 @@
import type { PluginInput } from "@opencode-ai/plugin";
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { relative, resolve } from "node:path";
import { findProjectRoot, findRuleFiles } from "./finder";
import {
createContentHash,
isDuplicateByContentHash,
isDuplicateByRealPath,
shouldApplyRule,
} from "./matcher";
import { parseRuleFrontmatter } from "./parser";
import {
clearInjectedRules,
loadInjectedRules,
saveInjectedRules,
} from "./storage";
import { createDynamicTruncator } from "../../shared/dynamic-truncator";
import { getRuleInjectionFilePath } from "./output-path";
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;
};
}
interface RuleToInject {
relativePath: string;
matchReason: string;
content: string;
distance: number;
}
const TRACKED_TOOLS = ["read", "write", "edit", "multiedit"];
export function createRulesInjectorHook(ctx: PluginInput) {
const sessionCaches = new Map<
string,
{ contentHashes: Set<string>; realPaths: Set<string> }
>();
const truncator = createDynamicTruncator(ctx);
function getSessionCache(sessionID: string): {
contentHashes: Set<string>;
realPaths: Set<string>;
} {
if (!sessionCaches.has(sessionID)) {
sessionCaches.set(sessionID, loadInjectedRules(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);
}
async function processFilePathForInjection(
filePath: string,
sessionID: string,
output: ToolExecuteOutput
): Promise<void> {
const resolved = resolveFilePath(filePath);
if (!resolved) return;
const projectRoot = findProjectRoot(resolved);
const cache = getSessionCache(sessionID);
const home = homedir();
const ruleFileCandidates = findRuleFiles(projectRoot, home, resolved);
const toInject: RuleToInject[] = [];
for (const candidate of ruleFileCandidates) {
if (isDuplicateByRealPath(candidate.realPath, cache.realPaths)) continue;
try {
const rawContent = readFileSync(candidate.path, "utf-8");
const { metadata, body } = parseRuleFrontmatter(rawContent);
let matchReason: string;
if (candidate.isSingleFile) {
matchReason = "copilot-instructions (always apply)";
} else {
const matchResult = shouldApplyRule(metadata, resolved, projectRoot);
if (!matchResult.applies) continue;
matchReason = matchResult.reason ?? "matched";
}
const contentHash = createContentHash(body);
if (isDuplicateByContentHash(contentHash, cache.contentHashes)) continue;
const relativePath = projectRoot
? relative(projectRoot, candidate.path)
: candidate.path;
toInject.push({
relativePath,
matchReason,
content: body,
distance: candidate.distance,
});
cache.realPaths.add(candidate.realPath);
cache.contentHashes.add(contentHash);
} catch {}
}
if (toInject.length === 0) return;
toInject.sort((a, b) => a.distance - b.distance);
for (const rule of toInject) {
const { result, truncated } = await truncator.truncate(sessionID, rule.content);
const truncationNotice = truncated
? `\n\n[Note: Content was truncated to save context window space. For full context, please read the file directly: ${rule.relativePath}]`
: "";
output.output += `\n\n[Rule: ${rule.relativePath}]\n[Match: ${rule.matchReason}]\n${result}${truncationNotice}`;
}
saveInjectedRules(sessionID, cache);
}
const toolExecuteAfter = async (
input: ToolExecuteInput,
output: ToolExecuteOutput
) => {
const toolName = input.tool.toLowerCase();
if (TRACKED_TOOLS.includes(toolName)) {
const filePath = getRuleInjectionFilePath(output);
if (!filePath) return;
await processFilePathForInjection(filePath, 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);
clearInjectedRules(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);
clearInjectedRules(sessionID);
}
}
};
return {
"tool.execute.before": toolExecuteBefore,
"tool.execute.after": toolExecuteAfter,
event: eventHandler,
};
}
export { createRulesInjectorHook } from "./hook";
+126
View File
@@ -0,0 +1,126 @@
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { relative, resolve } from "node:path";
import { findProjectRoot, findRuleFiles } from "./finder";
import {
createContentHash,
isDuplicateByContentHash,
isDuplicateByRealPath,
shouldApplyRule,
} from "./matcher";
import { parseRuleFrontmatter } from "./parser";
import { saveInjectedRules } from "./storage";
import type { SessionInjectedRulesCache } from "./cache";
type ToolExecuteOutput = {
title: string;
output: string;
metadata: unknown;
};
type RuleToInject = {
relativePath: string;
matchReason: string;
content: string;
distance: number;
};
type DynamicTruncator = {
truncate: (
sessionID: string,
content: string
) => Promise<{ result: string; truncated: boolean }>;
};
function resolveFilePath(
workspaceDirectory: string,
path: string
): string | null {
if (!path) return null;
if (path.startsWith("/")) return path;
return resolve(workspaceDirectory, path);
}
export function createRuleInjectionProcessor(deps: {
workspaceDirectory: string;
truncator: DynamicTruncator;
getSessionCache: (sessionID: string) => SessionInjectedRulesCache;
}): {
processFilePathForInjection: (
filePath: string,
sessionID: string,
output: ToolExecuteOutput
) => Promise<void>;
} {
const { workspaceDirectory, truncator, getSessionCache } = deps;
async function processFilePathForInjection(
filePath: string,
sessionID: string,
output: ToolExecuteOutput
): Promise<void> {
const resolved = resolveFilePath(workspaceDirectory, filePath);
if (!resolved) return;
const projectRoot = findProjectRoot(resolved);
const cache = getSessionCache(sessionID);
const home = homedir();
const ruleFileCandidates = findRuleFiles(projectRoot, home, resolved);
const toInject: RuleToInject[] = [];
for (const candidate of ruleFileCandidates) {
if (isDuplicateByRealPath(candidate.realPath, cache.realPaths)) continue;
try {
const rawContent = readFileSync(candidate.path, "utf-8");
const { metadata, body } = parseRuleFrontmatter(rawContent);
let matchReason: string;
if (candidate.isSingleFile) {
matchReason = "copilot-instructions (always apply)";
} else {
const matchResult = shouldApplyRule(metadata, resolved, projectRoot);
if (!matchResult.applies) continue;
matchReason = matchResult.reason ?? "matched";
}
const contentHash = createContentHash(body);
if (isDuplicateByContentHash(contentHash, cache.contentHashes)) continue;
const relativePath = projectRoot
? relative(projectRoot, candidate.path)
: candidate.path;
toInject.push({
relativePath,
matchReason,
content: body,
distance: candidate.distance,
});
cache.realPaths.add(candidate.realPath);
cache.contentHashes.add(contentHash);
} catch {}
}
if (toInject.length === 0) return;
toInject.sort((a, b) => a.distance - b.distance);
for (const rule of toInject) {
const { result, truncated } = await truncator.truncate(
sessionID,
rule.content
);
const truncationNotice = truncated
? `\n\n[Note: Content was truncated to save context window space. For full context, please read the file directly: ${rule.relativePath}]`
: "";
output.output += `\n\n[Rule: ${rule.relativePath}]\n[Match: ${rule.matchReason}]\n${result}${truncationNotice}`;
}
saveInjectedRules(sessionID, cache);
}
return { processFilePathForInjection };
}