SuperCharge Claude Code v1.0.0 - Complete Customization Package
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
This commit is contained in:
403
plugins/core/hook-system.ts
Normal file
403
plugins/core/hook-system.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* 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
|
||||
Reference in New Issue
Block a user