Files
oh-my-opencode/packages/omo-claude/plugin/components/rules/src/codex-hook.ts
T
YeonGyu-Kim 3272037c3e feat(omo-claude): vendor rules component with CC patches
model/turn_id optional (SessionStart keeps model), CLAUDE_PLUGIN_ROOT/DATA env
fallback, .claude-plugin manifest path, PostToolUse matcher Write|Edit|MultiEdit,
OMO_CLAUDE_RULES_* env aliases, bundled-rules vendored. Injects w/o turn_id (QA).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:40:11 +09:00

242 lines
8.1 KiB
TypeScript

import { configFromEnvironment } from "./config.js";
import { createHookDebugTimer } from "./debug-log.js";
import { fingerprintDynamicTargets } from "./dynamic-target-fingerprints.js";
import { formatAdditionalContextOutput } from "./hook-output.js";
import { displayPath, uniqueStrings } from "./path-utils.js";
import {
clearSessionState,
hasPostCompactPending,
hydrateEngineState,
isPostCompactPending,
markSessionCompacted,
persistEngineState,
sessionCachePath,
} from "./persistent-cache.js";
import { withPostCompactBudget } from "./post-compact-budget.js";
import { createRulesEngine } from "./rules-engine-factory.js";
import { extractCodexToolPaths } from "./tool-paths.js";
import { filterRulesAlreadyInTranscript } from "./transcript-rule-filter.js";
import type { TranscriptSearchOptions } from "./transcript-search.js";
export type CodexSessionStartInput = {
session_id: string;
transcript_path: string | null;
cwd: string;
hook_event_name: "SessionStart";
model: string;
permission_mode: string;
source: "startup" | "resume" | "clear";
};
export type CodexUserPromptSubmitInput = {
session_id: string;
turn_id: string;
transcript_path: string | null;
cwd: string;
hook_event_name: "UserPromptSubmit";
model: string;
permission_mode: string;
prompt: string;
};
export type CodexPostToolUseInput = {
session_id: string;
turn_id: string;
transcript_path: string | null;
cwd: string;
hook_event_name: "PostToolUse";
model: string;
permission_mode: string;
tool_name: string;
tool_input: unknown;
tool_response: unknown;
tool_use_id: string;
};
export type CodexPostCompactInput = {
session_id: string;
turn_id: string;
transcript_path: string | null;
cwd: string;
hook_event_name: "PostCompact";
model: string;
trigger: "manual" | "auto";
};
export interface CodexRulesHookOptions {
env?: NodeJS.ProcessEnv;
pluginDataRoot?: string;
}
export async function runSessionStartHook(
input: CodexSessionStartInput,
options: CodexRulesHookOptions = {},
): Promise<string> {
const cachePath = sessionCachePath(input.session_id, options.pluginDataRoot);
if (input.source === "clear") {
clearSessionState(cachePath);
} else if (input.source !== "resume" && !hasPostCompactPending(cachePath)) {
clearSessionState(cachePath);
}
const postCompactPending = input.source !== "clear" && isPostCompactPending(cachePath, "static");
const transcriptPath = input.source === "clear" ? null : input.transcript_path;
return runStaticInjection(
input.cwd,
transcriptPath,
"SessionStart",
cachePath,
options,
postCompactPending ? "static" : undefined,
{ latestCompactedReplacementOnly: postCompactPending },
);
}
export async function runPostCompactHook(
input: CodexPostCompactInput,
options: CodexRulesHookOptions = {},
): Promise<string> {
markSessionCompacted(sessionCachePath(input.session_id, options.pluginDataRoot));
return "";
}
export async function runUserPromptSubmitHook(
input: CodexUserPromptSubmitInput,
options: CodexRulesHookOptions = {},
): Promise<string> {
const cachePath = sessionCachePath(input.session_id, options.pluginDataRoot);
const postCompactPending = isPostCompactPending(cachePath, "static");
return runStaticInjection(
input.cwd,
input.transcript_path,
"UserPromptSubmit",
cachePath,
options,
postCompactPending ? "static" : undefined,
{ latestCompactedReplacementOnly: postCompactPending },
);
}
export async function runPostToolUseHook(
input: CodexPostToolUseInput,
options: CodexRulesHookOptions = {},
): Promise<string> {
const debugTimer = createHookDebugTimer("PostToolUse");
const config = configFromEnvironment(options.env);
debugTimer.lap("config", { disabled: config.disabled, mode: config.mode });
if (config.disabled || config.mode === "off" || config.mode === "static") {
debugTimer.done({ outputBytes: 0, reason: "disabled" });
return "";
}
const targetPaths = extractCodexToolPaths(input, input.cwd);
debugTimer.lap("extract", {
targets: targetPaths.length,
uniqueTargets: uniqueStrings(targetPaths).length,
tool: input.tool_name,
});
const firstTargetPath = targetPaths[0];
if (firstTargetPath === undefined) {
debugTimer.done({ outputBytes: 0, reason: "no-target" });
return "";
}
const cachePath = sessionCachePath(input.session_id, options.pluginDataRoot);
const postCompactPending = isPostCompactPending(cachePath, "dynamic");
const engine = createRulesEngine(options, postCompactPending ? withPostCompactBudget(config) : config);
hydrateEngineState(engine, cachePath);
debugTimer.lap("hydrate", {
dynamicDedupScopes: engine.state.dynamicDedup.size,
dynamicTargetFingerprints: engine.state.dynamicTargetFingerprints.size,
staticDedup: engine.state.staticDedup.size,
});
const dynamicTargetFingerprints = fingerprintDynamicTargets(input.cwd, targetPaths, config);
debugTimer.lap("fingerprint", { fingerprints: dynamicTargetFingerprints.length });
const pendingTargetFingerprints = dynamicTargetFingerprints.filter(
(target) => engine.state.dynamicTargetFingerprints.get(target.cacheKey) !== target.fingerprint,
);
debugTimer.lap("pending", { pending: pendingTargetFingerprints.length });
if (pendingTargetFingerprints.length === 0) {
persistEngineState(engine, cachePath, postCompactPending ? "dynamic" : undefined);
debugTimer.lap("persist", { reason: "no-pending" });
debugTimer.done({ outputBytes: 0, reason: "no-pending" });
return "";
}
const loaded = engine.loadDynamicRules(
input.cwd,
pendingTargetFingerprints.map((target) => target.targetPath),
);
debugTimer.lap("load", { diagnostics: loaded.diagnostics.length, loadedRules: loaded.rules.length });
const rules = filterRulesAlreadyInTranscript(
loaded.rules.filter((rule) => !engine.isStaticInjected(rule) && !engine.isDynamicInjected(rule)),
input.transcript_path,
(rule) => {
engine.markDynamicInjected(rule);
},
{ latestCompactedReplacementOnly: postCompactPending },
);
debugTimer.lap("filter", { rules: rules.length });
for (const target of pendingTargetFingerprints) {
engine.state.dynamicTargetFingerprints.set(target.cacheKey, target.fingerprint);
}
if (rules.length === 0) {
persistEngineState(engine, cachePath, postCompactPending ? "dynamic" : undefined);
debugTimer.lap("persist", { reason: "no-rules" });
debugTimer.done({ outputBytes: 0, reason: "no-rules" });
return "";
}
const firstPendingTargetPath = pendingTargetFingerprints[0]?.targetPath ?? firstTargetPath;
const block = engine.formatDynamic(rules, displayPath(input.cwd, firstPendingTargetPath));
debugTimer.lap("format", { blockChars: block.length, rules: rules.length });
for (const rule of rules) {
engine.markDynamicInjected(rule);
}
persistEngineState(engine, cachePath, postCompactPending ? "dynamic" : undefined);
debugTimer.lap("persist", { reason: "emit" });
const output = formatAdditionalContextOutput("PostToolUse", block);
debugTimer.done({ outputBytes: Buffer.byteLength(output), reason: "emit" });
return output;
}
function runStaticInjection(
cwd: string,
transcriptPath: string | null,
eventName: "SessionStart" | "UserPromptSubmit",
cachePath: string,
options: CodexRulesHookOptions,
completedPostCompactChannel?: "static",
transcriptSearchOptions: TranscriptSearchOptions = {},
): string {
const config = configFromEnvironment(options.env);
if (config.disabled || config.mode === "off" || config.mode === "dynamic") {
return "";
}
const effectiveConfig = completedPostCompactChannel === undefined ? config : withPostCompactBudget(config);
const engine = createRulesEngine(options, effectiveConfig);
hydrateEngineState(engine, cachePath);
engine.state.cwd = cwd;
const loaded = engine.loadStaticRules(cwd);
const rules = filterRulesAlreadyInTranscript(
loaded.rules.filter((rule) => !engine.isStaticInjected(rule)),
transcriptPath,
(rule) => {
engine.markStaticInjected(rule);
},
transcriptSearchOptions,
);
if (rules.length === 0) {
persistEngineState(engine, cachePath, completedPostCompactChannel);
return "";
}
const block = engine.formatStatic(rules);
for (const rule of rules) {
engine.markStaticInjected(rule);
}
persistEngineState(engine, cachePath, completedPostCompactChannel);
return formatAdditionalContextOutput(eventName, block);
}