feat(claude-code-hooks): add PreCompact hook support for experimental.session.compacting event (#139)
This commit is contained in:
@@ -9,6 +9,7 @@ export interface DisabledHooksConfig {
|
|||||||
PreToolUse?: string[]
|
PreToolUse?: string[]
|
||||||
PostToolUse?: string[]
|
PostToolUse?: string[]
|
||||||
UserPromptSubmit?: string[]
|
UserPromptSubmit?: string[]
|
||||||
|
PreCompact?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginExtendedConfig {
|
export interface PluginExtendedConfig {
|
||||||
@@ -47,6 +48,7 @@ function mergeDisabledHooks(
|
|||||||
PreToolUse: override.PreToolUse ?? base.PreToolUse,
|
PreToolUse: override.PreToolUse ?? base.PreToolUse,
|
||||||
PostToolUse: override.PostToolUse ?? base.PostToolUse,
|
PostToolUse: override.PostToolUse ?? base.PostToolUse,
|
||||||
UserPromptSubmit: override.UserPromptSubmit ?? base.UserPromptSubmit,
|
UserPromptSubmit: override.UserPromptSubmit ?? base.UserPromptSubmit,
|
||||||
|
PreCompact: override.PreCompact ?? base.PreCompact,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface RawClaudeHooksConfig {
|
|||||||
PostToolUse?: RawHookMatcher[]
|
PostToolUse?: RawHookMatcher[]
|
||||||
UserPromptSubmit?: RawHookMatcher[]
|
UserPromptSubmit?: RawHookMatcher[]
|
||||||
Stop?: RawHookMatcher[]
|
Stop?: RawHookMatcher[]
|
||||||
|
PreCompact?: RawHookMatcher[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeHookMatcher(raw: RawHookMatcher): HookMatcher {
|
function normalizeHookMatcher(raw: RawHookMatcher): HookMatcher {
|
||||||
@@ -30,6 +31,7 @@ function normalizeHooksConfig(raw: RawClaudeHooksConfig): ClaudeHooksConfig {
|
|||||||
"PostToolUse",
|
"PostToolUse",
|
||||||
"UserPromptSubmit",
|
"UserPromptSubmit",
|
||||||
"Stop",
|
"Stop",
|
||||||
|
"PreCompact",
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const eventType of eventTypes) {
|
for (const eventType of eventTypes) {
|
||||||
@@ -66,6 +68,7 @@ function mergeHooksConfig(
|
|||||||
"PostToolUse",
|
"PostToolUse",
|
||||||
"UserPromptSubmit",
|
"UserPromptSubmit",
|
||||||
"Stop",
|
"Stop",
|
||||||
|
"PreCompact",
|
||||||
]
|
]
|
||||||
for (const eventType of eventTypes) {
|
for (const eventType of eventTypes) {
|
||||||
if (override[eventType]) {
|
if (override[eventType]) {
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import {
|
|||||||
executeStopHooks,
|
executeStopHooks,
|
||||||
type StopContext,
|
type StopContext,
|
||||||
} from "./stop"
|
} from "./stop"
|
||||||
|
import {
|
||||||
|
executePreCompactHooks,
|
||||||
|
type PreCompactContext,
|
||||||
|
} from "./pre-compact"
|
||||||
import { cacheToolInput, getToolInput } from "./tool-input-cache"
|
import { cacheToolInput, getToolInput } from "./tool-input-cache"
|
||||||
import { recordToolUse, recordToolResult, getTranscriptPath, recordUserMessage } from "./transcript"
|
import { recordToolUse, recordToolResult, getTranscriptPath, recordUserMessage } from "./transcript"
|
||||||
import type { PluginConfig } from "./types"
|
import type { PluginConfig } from "./types"
|
||||||
@@ -31,6 +35,35 @@ const sessionInterruptState = new Map<string, { interrupted: boolean }>()
|
|||||||
|
|
||||||
export function createClaudeCodeHooksHook(ctx: PluginInput, config: PluginConfig = {}) {
|
export function createClaudeCodeHooksHook(ctx: PluginInput, config: PluginConfig = {}) {
|
||||||
return {
|
return {
|
||||||
|
"experimental.session.compacting": async (
|
||||||
|
input: { sessionID: string },
|
||||||
|
output: { context: string[] }
|
||||||
|
): Promise<void> => {
|
||||||
|
if (isHookDisabled(config, "PreCompact")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const claudeConfig = await loadClaudeHooksConfig()
|
||||||
|
const extendedConfig = await loadPluginExtendedConfig()
|
||||||
|
|
||||||
|
const preCompactCtx: PreCompactContext = {
|
||||||
|
sessionId: input.sessionID,
|
||||||
|
cwd: ctx.directory,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await executePreCompactHooks(preCompactCtx, claudeConfig, extendedConfig)
|
||||||
|
|
||||||
|
if (result.context.length > 0) {
|
||||||
|
log("PreCompact hooks injecting context", {
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
contextCount: result.context.length,
|
||||||
|
hookName: result.hookName,
|
||||||
|
elapsedMs: result.elapsedMs,
|
||||||
|
})
|
||||||
|
output.context.push(...result.context)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"chat.message": async (
|
"chat.message": async (
|
||||||
input: {
|
input: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
|
|||||||
109
src/hooks/claude-code-hooks/pre-compact.ts
Normal file
109
src/hooks/claude-code-hooks/pre-compact.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import type {
|
||||||
|
PreCompactInput,
|
||||||
|
PreCompactOutput,
|
||||||
|
ClaudeHooksConfig,
|
||||||
|
} from "./types"
|
||||||
|
import { findMatchingHooks, executeHookCommand, log } from "../../shared"
|
||||||
|
import { DEFAULT_CONFIG } from "./plugin-config"
|
||||||
|
import { isHookCommandDisabled, type PluginExtendedConfig } from "./config-loader"
|
||||||
|
|
||||||
|
export interface PreCompactContext {
|
||||||
|
sessionId: string
|
||||||
|
cwd: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PreCompactResult {
|
||||||
|
context: string[]
|
||||||
|
elapsedMs?: number
|
||||||
|
hookName?: string
|
||||||
|
continue?: boolean
|
||||||
|
stopReason?: string
|
||||||
|
suppressOutput?: boolean
|
||||||
|
systemMessage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executePreCompactHooks(
|
||||||
|
ctx: PreCompactContext,
|
||||||
|
config: ClaudeHooksConfig | null,
|
||||||
|
extendedConfig?: PluginExtendedConfig | null
|
||||||
|
): Promise<PreCompactResult> {
|
||||||
|
if (!config) {
|
||||||
|
return { context: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchers = findMatchingHooks(config, "PreCompact", "*")
|
||||||
|
if (matchers.length === 0) {
|
||||||
|
return { context: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdinData: PreCompactInput = {
|
||||||
|
session_id: ctx.sessionId,
|
||||||
|
cwd: ctx.cwd,
|
||||||
|
hook_event_name: "PreCompact",
|
||||||
|
hook_source: "opencode-plugin",
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now()
|
||||||
|
let firstHookName: string | undefined
|
||||||
|
const collectedContext: string[] = []
|
||||||
|
|
||||||
|
for (const matcher of matchers) {
|
||||||
|
for (const hook of matcher.hooks) {
|
||||||
|
if (hook.type !== "command") continue
|
||||||
|
|
||||||
|
if (isHookCommandDisabled("PreCompact", hook.command, extendedConfig ?? null)) {
|
||||||
|
log("PreCompact hook command skipped (disabled by config)", { command: hook.command })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const hookName = hook.command.split("/").pop() || hook.command
|
||||||
|
if (!firstHookName) firstHookName = hookName
|
||||||
|
|
||||||
|
const result = await executeHookCommand(
|
||||||
|
hook.command,
|
||||||
|
JSON.stringify(stdinData),
|
||||||
|
ctx.cwd,
|
||||||
|
{ forceZsh: DEFAULT_CONFIG.forceZsh, zshPath: DEFAULT_CONFIG.zshPath }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.exitCode === 2) {
|
||||||
|
log("PreCompact hook blocked", { hookName, stderr: result.stderr })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.stdout) {
|
||||||
|
try {
|
||||||
|
const output = JSON.parse(result.stdout) as PreCompactOutput
|
||||||
|
|
||||||
|
if (output.hookSpecificOutput?.additionalContext) {
|
||||||
|
collectedContext.push(...output.hookSpecificOutput.additionalContext)
|
||||||
|
} else if (output.context) {
|
||||||
|
collectedContext.push(...output.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.continue === false) {
|
||||||
|
return {
|
||||||
|
context: collectedContext,
|
||||||
|
elapsedMs: Date.now() - startTime,
|
||||||
|
hookName: firstHookName,
|
||||||
|
continue: output.continue,
|
||||||
|
stopReason: output.stopReason,
|
||||||
|
suppressOutput: output.suppressOutput,
|
||||||
|
systemMessage: output.systemMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (result.stdout.trim()) {
|
||||||
|
collectedContext.push(result.stdout.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
context: collectedContext,
|
||||||
|
elapsedMs: Date.now() - startTime,
|
||||||
|
hookName: firstHookName,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ export type ClaudeHookEvent =
|
|||||||
| "PostToolUse"
|
| "PostToolUse"
|
||||||
| "UserPromptSubmit"
|
| "UserPromptSubmit"
|
||||||
| "Stop"
|
| "Stop"
|
||||||
|
| "PreCompact"
|
||||||
|
|
||||||
export interface HookMatcher {
|
export interface HookMatcher {
|
||||||
matcher: string
|
matcher: string
|
||||||
@@ -24,6 +25,7 @@ export interface ClaudeHooksConfig {
|
|||||||
PostToolUse?: HookMatcher[]
|
PostToolUse?: HookMatcher[]
|
||||||
UserPromptSubmit?: HookMatcher[]
|
UserPromptSubmit?: HookMatcher[]
|
||||||
Stop?: HookMatcher[]
|
Stop?: HookMatcher[]
|
||||||
|
PreCompact?: HookMatcher[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreToolUseInput {
|
export interface PreToolUseInput {
|
||||||
@@ -82,6 +84,13 @@ export interface StopInput {
|
|||||||
hook_source?: HookSource
|
hook_source?: HookSource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PreCompactInput {
|
||||||
|
session_id: string
|
||||||
|
cwd: string
|
||||||
|
hook_event_name: "PreCompact"
|
||||||
|
hook_source?: HookSource
|
||||||
|
}
|
||||||
|
|
||||||
export type PermissionDecision = "allow" | "deny" | "ask"
|
export type PermissionDecision = "allow" | "deny" | "ask"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,6 +175,16 @@ export interface StopOutput {
|
|||||||
inject_prompt?: string
|
inject_prompt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PreCompactOutput extends HookCommonOutput {
|
||||||
|
/** Additional context to inject into compaction prompt */
|
||||||
|
context?: string[]
|
||||||
|
hookSpecificOutput?: {
|
||||||
|
hookEventName: "PreCompact"
|
||||||
|
/** Additional context strings to inject */
|
||||||
|
additionalContext?: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type ClaudeCodeContent =
|
export type ClaudeCodeContent =
|
||||||
| { type: "text"; text: string }
|
| { type: "text"; text: string }
|
||||||
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }
|
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }
|
||||||
|
|||||||
Reference in New Issue
Block a user