Features: - 30+ Custom Skills (cognitive, development, UI/UX, autonomous agents) - RalphLoop autonomous agent integration - Multi-AI consultation (Qwen) - Agent management system with sync capabilities - Custom hooks for session management - MCP servers integration - Plugin marketplace setup - Comprehensive installation script Components: - Skills: always-use-superpowers, ralph, brainstorming, ui-ux-pro-max, etc. - Agents: 100+ agents across engineering, marketing, product, etc. - Hooks: session-start-superpowers, qwen-consult, ralph-auto-trigger - Commands: /brainstorm, /write-plan, /execute-plan - MCP Servers: zai-mcp-server, web-search-prime, web-reader, zread - Binaries: ralphloop wrapper Installation: ./supercharge.sh
404 lines
11 KiB
TypeScript
404 lines
11 KiB
TypeScript
/**
|
|
* Claude Code Hook System - Event-based Plugin Hooks
|
|
*
|
|
* Features:
|
|
* - Multiple hook types (pre/post events)
|
|
* - Priority-based execution
|
|
* - Async hook support
|
|
* - Error handling and recovery
|
|
* - Hook chaining
|
|
*/
|
|
|
|
import fs from 'fs/promises'
|
|
import path from 'path'
|
|
import { spawn } from 'child_process'
|
|
|
|
// ============================================================================
|
|
// TYPES AND INTERFACES
|
|
// ============================================================================
|
|
|
|
export type HookEvent =
|
|
| 'UserPromptSubmit'
|
|
| 'UserPromptSubmit-hook'
|
|
| 'PreToolUse'
|
|
| 'PostToolUse'
|
|
| 'PreFileEdit'
|
|
| 'PostFileEdit'
|
|
| 'PreCommand'
|
|
| 'PostCommand'
|
|
| 'SessionStart'
|
|
| 'SessionEnd'
|
|
| 'PluginLoad'
|
|
| 'PluginUnload'
|
|
| 'Error'
|
|
|
|
export interface HookContext {
|
|
event: HookEvent
|
|
timestamp: string
|
|
sessionId?: string
|
|
messageId?: string
|
|
data: Record<string, any>
|
|
}
|
|
|
|
export interface HookResult {
|
|
success: boolean
|
|
data?: any
|
|
error?: string
|
|
modifications?: {
|
|
args?: any
|
|
output?: any
|
|
cancel?: boolean
|
|
}
|
|
}
|
|
|
|
export interface HookDefinition {
|
|
type: 'command' | 'module'
|
|
command?: string
|
|
module?: string
|
|
handler?: string
|
|
timeout?: number
|
|
priority?: number
|
|
condition?: string
|
|
enabled?: boolean
|
|
}
|
|
|
|
export interface HookConfig {
|
|
description?: string
|
|
hooks: Record<string, { hooks: HookDefinition[] }>
|
|
}
|
|
|
|
// ============================================================================
|
|
// HOOK SYSTEM CLASS
|
|
// ============================================================================
|
|
|
|
export class HookSystem {
|
|
private hooksFile: string
|
|
private hooksDir: string
|
|
private hooks: Map<HookEvent, HookDefinition[]> = new Map()
|
|
private hookResults: Map<string, HookResult[]> = new Map()
|
|
|
|
constructor(claudeDir: string = path.join(process.env.HOME || '', '.claude')) {
|
|
this.hooksDir = path.join(claudeDir, 'hooks')
|
|
this.hooksFile = path.join(claudeDir, 'hooks.json')
|
|
}
|
|
|
|
// ============================================================================
|
|
// INITIALIZATION
|
|
// ============================================================================
|
|
|
|
async initialize(): Promise<void> {
|
|
await this.ensureDirectories()
|
|
await this.loadHooks()
|
|
}
|
|
|
|
private async ensureDirectories(): Promise<void> {
|
|
try {
|
|
await fs.mkdir(this.hooksDir, { recursive: true })
|
|
} catch (error) {
|
|
// Directory might already exist
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// HOOK LOADING
|
|
// ============================================================================
|
|
|
|
async loadHooks(): Promise<void> {
|
|
try {
|
|
const config: HookConfig = JSON.parse(
|
|
await fs.readFile(this.hooksFile, 'utf-8')
|
|
)
|
|
|
|
for (const [event, hookGroup] of Object.entries(config.hooks)) {
|
|
this.hooks.set(event as HookEvent, hookGroup.hooks)
|
|
}
|
|
} catch (error) {
|
|
// No hooks file or invalid JSON
|
|
await this.saveHooks()
|
|
}
|
|
}
|
|
|
|
async loadPluginHooks(pluginPath: string): Promise<void> {
|
|
const hooksJsonPath = path.join(pluginPath, 'hooks', 'hooks.json')
|
|
|
|
try {
|
|
const config: HookConfig = JSON.parse(
|
|
await fs.readFile(hooksJsonPath, 'utf-8')
|
|
)
|
|
|
|
for (const [event, hookGroup] of Object.entries(config.hooks)) {
|
|
const existing = this.hooks.get(event as HookEvent) || []
|
|
this.hooks.set(event as HookEvent, [...existing, ...hookGroup.hooks])
|
|
}
|
|
} catch {
|
|
// No hooks file
|
|
}
|
|
}
|
|
|
|
async saveHooks(): Promise<void> {
|
|
const config: HookConfig = {
|
|
description: 'Claude Code Hooks Configuration',
|
|
hooks: {},
|
|
}
|
|
|
|
for (const [event, hooks] of this.hooks.entries()) {
|
|
config.hooks[event] = { hooks }
|
|
}
|
|
|
|
await fs.writeFile(this.hooksFile, JSON.stringify(config, null, 2))
|
|
}
|
|
|
|
// ============================================================================
|
|
// HOOK REGISTRATION
|
|
// ============================================================================
|
|
|
|
registerHook(event: HookEvent, hook: HookDefinition): void {
|
|
const existing = this.hooks.get(event) || []
|
|
existing.push(hook)
|
|
this.hooks.set(event, existing.sort((a, b) => (b.priority || 0) - (a.priority || 0)))
|
|
}
|
|
|
|
unregisterHook(event: HookEvent, hookIdentifier: string): void {
|
|
const existing = this.hooks.get(event) || []
|
|
const filtered = existing.filter(
|
|
(h) => h.command !== hookIdentifier && h.module !== hookIdentifier
|
|
)
|
|
this.hooks.set(event, filtered)
|
|
}
|
|
|
|
clearHooks(event?: HookEvent): void {
|
|
if (event) {
|
|
this.hooks.delete(event)
|
|
} else {
|
|
this.hooks.clear()
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// HOOK EXECUTION
|
|
// ============================================================================
|
|
|
|
async executeHook(event: HookEvent, context: HookContext): Promise<HookResult[]> {
|
|
const hooks = this.hooks.get(event) || []
|
|
const results: HookResult[] = []
|
|
|
|
for (const hook of hooks) {
|
|
if (hook.enabled === false) continue
|
|
|
|
try {
|
|
const result = await this.executeSingleHook(hook, context)
|
|
results.push(result)
|
|
|
|
// Check if hook wants to cancel the operation
|
|
if (result.modifications?.cancel) {
|
|
break
|
|
}
|
|
} catch (error) {
|
|
results.push({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Store results for later retrieval
|
|
this.hookResults.set(`${event}-${context.timestamp}`, results)
|
|
|
|
return results
|
|
}
|
|
|
|
private async executeSingleHook(
|
|
hook: HookDefinition,
|
|
context: HookContext
|
|
): Promise<HookResult> {
|
|
const timeout = hook.timeout || 5000
|
|
|
|
if (hook.type === 'command' && hook.command) {
|
|
return await this.executeCommandHook(hook.command, context, timeout)
|
|
} else if (hook.type === 'module' && hook.module) {
|
|
return await this.executeModuleHook(hook.module, context, timeout)
|
|
}
|
|
|
|
throw new Error(`Unknown hook type`)
|
|
}
|
|
|
|
private async executeCommandHook(
|
|
command: string,
|
|
context: HookContext,
|
|
timeout: number
|
|
): Promise<HookResult> {
|
|
return new Promise((resolve, reject) => {
|
|
const [cmd, ...args] = command.split(' ')
|
|
|
|
const proc = spawn(cmd, args, {
|
|
env: {
|
|
...process.env,
|
|
HOOK_EVENT: context.event,
|
|
HOOK_DATA: JSON.stringify(context.data),
|
|
HOOK_TIMESTAMP: context.timestamp,
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
})
|
|
|
|
let stdout = ''
|
|
let stderr = ''
|
|
|
|
proc.stdout?.on('data', (data) => {
|
|
stdout += data.toString()
|
|
})
|
|
|
|
proc.stderr?.on('data', (data) => {
|
|
stderr += data.toString()
|
|
})
|
|
|
|
const timer = setTimeout(() => {
|
|
proc.kill()
|
|
reject(new Error(`Hook timeout after ${timeout}ms`))
|
|
}, timeout)
|
|
|
|
proc.on('close', (code) => {
|
|
clearTimeout(timer)
|
|
|
|
if (code === 0) {
|
|
try {
|
|
// Try to parse output as JSON for modifications
|
|
const modifications = stdout.trim() ? JSON.parse(stdout) : undefined
|
|
resolve({
|
|
success: true,
|
|
data: stdout,
|
|
modifications,
|
|
})
|
|
} catch {
|
|
resolve({
|
|
success: true,
|
|
data: stdout,
|
|
})
|
|
}
|
|
} else {
|
|
reject(new Error(`Hook failed: ${stderr || `exit code ${code}`}`))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
private async executeModuleHook(
|
|
modulePath: string,
|
|
context: HookContext,
|
|
timeout: number
|
|
): Promise<HookResult> {
|
|
// Dynamic import for TypeScript/JavaScript modules
|
|
const startTime = Date.now()
|
|
|
|
try {
|
|
const module = await import(modulePath)
|
|
const handler = module.default || module.hook || module.handler
|
|
|
|
if (typeof handler !== 'function') {
|
|
throw new Error(`Module ${modulePath} does not export a handler function`)
|
|
}
|
|
|
|
// Execute with timeout
|
|
const result = await Promise.race([
|
|
handler(context),
|
|
new Promise<any>((_, reject) =>
|
|
setTimeout(() => reject(new Error('Hook timeout')), timeout)
|
|
),
|
|
])
|
|
|
|
return {
|
|
success: true,
|
|
data: result,
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// UTILITY FUNCTIONS
|
|
// ============================================================================
|
|
|
|
getHookResults(event: HookEvent, timestamp: string): HookResult[] | undefined {
|
|
return this.hookResults.get(`${event}-${timestamp}`)
|
|
}
|
|
|
|
getRegisteredHooks(event?: HookEvent): Map<HookEvent, HookDefinition[]> {
|
|
if (event) {
|
|
const hooks = this.hooks.get(event)
|
|
return new Map(hooks ? [[event, hooks]] : [])
|
|
}
|
|
return this.hooks
|
|
}
|
|
|
|
async listHooksByEvent(event: HookEvent): Promise<HookDefinition[]> {
|
|
return this.hooks.get(event) || []
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// HOOK BUILDER - Convenient API for Creating Hooks
|
|
// ============================================================================
|
|
|
|
export class HookBuilder {
|
|
private hooks: HookDefinition[] = []
|
|
|
|
command(cmd: string, options?: Partial<HookDefinition>): this {
|
|
this.hooks.push({
|
|
type: 'command',
|
|
command: cmd,
|
|
priority: options?.priority || 0,
|
|
timeout: options?.timeout || 5000,
|
|
enabled: options?.enabled !== false,
|
|
condition: options?.condition,
|
|
})
|
|
return this
|
|
}
|
|
|
|
module(mod: string, options?: Partial<HookDefinition>): this {
|
|
this.hooks.push({
|
|
type: 'module',
|
|
module: mod,
|
|
priority: options?.priority || 0,
|
|
timeout: options?.timeout || 5000,
|
|
enabled: options?.enabled !== false,
|
|
condition: options?.condition,
|
|
})
|
|
return this
|
|
}
|
|
|
|
build(): HookDefinition[] {
|
|
return this.hooks
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// HELPER FUNCTIONS
|
|
// ============================================================================
|
|
|
|
export function createHookBuilder(): HookBuilder {
|
|
return new HookBuilder()
|
|
}
|
|
|
|
export async function executeHooks(
|
|
hookSystem: HookSystem,
|
|
event: HookEvent,
|
|
data: Record<string, any>
|
|
): Promise<HookResult[]> {
|
|
const context: HookContext = {
|
|
event,
|
|
timestamp: Date.now().toString(),
|
|
data,
|
|
}
|
|
|
|
return await hookSystem.executeHook(event, context)
|
|
}
|
|
|
|
// ============================================================================
|
|
// EXPORTS
|
|
// ============================================================================
|
|
|
|
export default HookSystem
|