3272037c3e
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>
242 lines
8.1 KiB
TypeScript
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);
|
|
}
|