feat(hooks): add rules-injector hook for .cursor/rules and .claude/rules support

Implements adaptive rule injection similar to Claude Code's rule system:
- Searches .cursor/rules and .claude/rules directories recursively
- Supports YAML frontmatter with globs, paths, alwaysApply, description
- Adaptive project root detection (finds markers even outside ctx.directory)
- Symlink duplicate detection via realpath comparison
- Content hash deduplication (SHA-256) to avoid re-injecting same rules
- picomatch-based glob pattern matching for file-specific rules

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-11 11:07:31 +09:00
parent c12f73f774
commit fcdfcd3186
12 changed files with 803 additions and 2 deletions
+59
View File
@@ -0,0 +1,59 @@
import {
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
unlinkSync,
} from "node:fs";
import { join } from "node:path";
import { RULES_INJECTOR_STORAGE } from "./constants";
import type { InjectedRulesData } from "./types";
function getStoragePath(sessionID: string): string {
return join(RULES_INJECTOR_STORAGE, `${sessionID}.json`);
}
export function loadInjectedRules(sessionID: string): {
contentHashes: Set<string>;
realPaths: Set<string>;
} {
const filePath = getStoragePath(sessionID);
if (!existsSync(filePath))
return { contentHashes: new Set(), realPaths: new Set() };
try {
const content = readFileSync(filePath, "utf-8");
const data: InjectedRulesData = JSON.parse(content);
return {
contentHashes: new Set(data.injectedHashes),
realPaths: new Set(data.injectedRealPaths ?? []),
};
} catch {
return { contentHashes: new Set(), realPaths: new Set() };
}
}
export function saveInjectedRules(
sessionID: string,
data: { contentHashes: Set<string>; realPaths: Set<string> }
): void {
if (!existsSync(RULES_INJECTOR_STORAGE)) {
mkdirSync(RULES_INJECTOR_STORAGE, { recursive: true });
}
const storageData: InjectedRulesData = {
sessionID,
injectedHashes: [...data.contentHashes],
injectedRealPaths: [...data.realPaths],
updatedAt: Date.now(),
};
writeFileSync(getStoragePath(sessionID), JSON.stringify(storageData, null, 2));
}
export function clearInjectedRules(sessionID: string): void {
const filePath = getStoragePath(sessionID);
if (existsSync(filePath)) {
unlinkSync(filePath);
}
}