Files
Claude-Code-Workflow/ccw/src/commands/hook.ts
catlog22 b00113d212 feat: Enhance embedding management and model configuration
- Updated embedding_manager.py to include backend parameter in model configuration.
- Modified model_manager.py to utilize cache_name for ONNX models.
- Refactored hybrid_search.py to improve embedder initialization based on backend type.
- Added backend column to vector_store.py for better model configuration management.
- Implemented migration for existing database to include backend information.
- Enhanced API settings implementation with comprehensive provider and endpoint management.
- Introduced LiteLLM integration guide detailing configuration and usage.
- Added examples for LiteLLM usage in TypeScript.
2025-12-24 14:03:59 +08:00

317 lines
8.3 KiB
TypeScript

/**
* Hook Command - CLI endpoint for Claude Code hooks
* Provides simplified interface for hook operations, replacing complex bash/curl commands
*/
import chalk from 'chalk';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { homedir } from 'os';
interface HookOptions {
stdin?: boolean;
sessionId?: string;
prompt?: string;
type?: 'session-start' | 'context';
}
interface HookData {
session_id?: string;
prompt?: string;
cwd?: string;
tool_input?: Record<string, unknown>;
}
interface SessionState {
firstLoad: string;
loadCount: number;
lastPrompt?: string;
}
/**
* Read JSON data from stdin (for Claude Code hooks)
*/
async function readStdin(): Promise<string> {
return new Promise((resolve) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('readable', () => {
let chunk;
while ((chunk = process.stdin.read()) !== null) {
data += chunk;
}
});
process.stdin.on('end', () => {
resolve(data);
});
// Handle case where stdin is empty or not piped
if (process.stdin.isTTY) {
resolve('');
}
});
}
/**
* Get session state file path
* Uses ~/.claude/.ccw-sessions/ for reliable persistence across sessions
*/
function getSessionStateFile(sessionId: string): string {
const stateDir = join(homedir(), '.claude', '.ccw-sessions');
if (!existsSync(stateDir)) {
mkdirSync(stateDir, { recursive: true });
}
return join(stateDir, `session-${sessionId}.json`);
}
/**
* Load session state from file
*/
function loadSessionState(sessionId: string): SessionState | null {
const stateFile = getSessionStateFile(sessionId);
if (!existsSync(stateFile)) {
return null;
}
try {
const content = readFileSync(stateFile, 'utf-8');
return JSON.parse(content) as SessionState;
} catch {
return null;
}
}
/**
* Save session state to file
*/
function saveSessionState(sessionId: string, state: SessionState): void {
const stateFile = getSessionStateFile(sessionId);
writeFileSync(stateFile, JSON.stringify(state, null, 2));
}
/**
* Get project path from hook data or current working directory
*/
function getProjectPath(hookCwd?: string): string {
return hookCwd || process.cwd();
}
/**
* Session context action - provides progressive context loading
* First prompt: returns session overview with clusters
* Subsequent prompts: returns intent-matched sessions
*/
async function sessionContextAction(options: HookOptions): Promise<void> {
let { stdin, sessionId, prompt } = options;
let hookCwd: string | undefined;
// If --stdin flag is set, read from stdin (Claude Code hook format)
if (stdin) {
try {
const stdinData = await readStdin();
if (stdinData) {
const hookData = JSON.parse(stdinData) as HookData;
sessionId = hookData.session_id || sessionId;
hookCwd = hookData.cwd;
prompt = hookData.prompt || prompt;
}
} catch {
// Silently continue if stdin parsing fails
}
}
if (!sessionId) {
if (!stdin) {
console.error(chalk.red('Error: --session-id is required'));
console.error(chalk.gray('Usage: ccw hook session-context --session-id <id>'));
console.error(chalk.gray(' ccw hook session-context --stdin'));
}
process.exit(stdin ? 0 : 1);
}
try {
const projectPath = getProjectPath(hookCwd);
// Load existing session state
const existingState = loadSessionState(sessionId);
const isFirstPrompt = !existingState;
// Update session state
const newState: SessionState = isFirstPrompt
? {
firstLoad: new Date().toISOString(),
loadCount: 1,
lastPrompt: prompt
}
: {
...existingState,
loadCount: existingState.loadCount + 1,
lastPrompt: prompt
};
saveSessionState(sessionId, newState);
// Determine context type and generate content
let contextType: 'session-start' | 'context';
let content = '';
// Dynamic import to avoid circular dependencies
const { SessionClusteringService } = await import('../core/session-clustering-service.js');
const clusteringService = new SessionClusteringService(projectPath);
if (isFirstPrompt) {
// First prompt: return session overview with clusters
contextType = 'session-start';
content = await clusteringService.getProgressiveIndex({
type: 'session-start',
sessionId
});
} else if (prompt && prompt.trim().length > 0) {
// Subsequent prompts with content: return intent-matched sessions
contextType = 'context';
content = await clusteringService.getProgressiveIndex({
type: 'context',
sessionId,
prompt
});
} else {
// Subsequent prompts without content: return minimal context
contextType = 'context';
content = ''; // No context needed for empty prompts
}
if (stdin) {
// For hooks: output content directly to stdout
if (content) {
process.stdout.write(content);
}
process.exit(0);
}
// Interactive mode: show detailed output
console.log(chalk.green('Session Context'));
console.log(chalk.gray('─'.repeat(40)));
console.log(chalk.cyan('Session ID:'), sessionId);
console.log(chalk.cyan('Type:'), contextType);
console.log(chalk.cyan('First Prompt:'), isFirstPrompt ? 'Yes' : 'No');
console.log(chalk.cyan('Load Count:'), newState.loadCount);
console.log(chalk.gray('─'.repeat(40)));
if (content) {
console.log(content);
} else {
console.log(chalk.gray('(No context generated)'));
}
} catch (error) {
if (stdin) {
// Silent failure for hooks
process.exit(0);
}
console.error(chalk.red(`Error: ${(error as Error).message}`));
process.exit(1);
}
}
/**
* Notify dashboard action - send notification to running ccw view server
*/
async function notifyAction(options: HookOptions): Promise<void> {
const { stdin } = options;
let hookData: HookData = {};
if (stdin) {
try {
const stdinData = await readStdin();
if (stdinData) {
hookData = JSON.parse(stdinData) as HookData;
}
} catch {
// Silently continue if stdin parsing fails
}
}
try {
const { notifyRefreshRequired } = await import('../tools/notifier.js');
await notifyRefreshRequired();
if (!stdin) {
console.log(chalk.green('Notification sent to dashboard'));
}
process.exit(0);
} catch (error) {
if (stdin) {
process.exit(0);
}
console.error(chalk.red(`Error: ${(error as Error).message}`));
process.exit(1);
}
}
/**
* Show help for hook command
*/
function showHelp(): void {
console.log(`
${chalk.bold('ccw hook')} - CLI endpoint for Claude Code hooks
${chalk.bold('USAGE')}
ccw hook <subcommand> [options]
${chalk.bold('SUBCOMMANDS')}
session-context Progressive session context loading (replaces curl/bash hook)
notify Send notification to ccw view dashboard
${chalk.bold('OPTIONS')}
--stdin Read input from stdin (for Claude Code hooks)
--session-id Session ID (alternative to stdin)
--prompt Current prompt text (alternative to stdin)
${chalk.bold('EXAMPLES')}
${chalk.gray('# Use in Claude Code hook (settings.json):')}
ccw hook session-context --stdin
${chalk.gray('# Interactive usage:')}
ccw hook session-context --session-id abc123
${chalk.gray('# Notify dashboard:')}
ccw hook notify --stdin
${chalk.bold('HOOK CONFIGURATION')}
${chalk.gray('Add to .claude/settings.json:')}
{
"hooks": {
"UserPromptSubmit": [{
"hooks": [{
"type": "command",
"command": "ccw hook session-context --stdin"
}]
}]
}
}
`);
}
/**
* Main hook command handler
*/
export async function hookCommand(
subcommand: string,
args: string | string[],
options: HookOptions
): Promise<void> {
switch (subcommand) {
case 'session-context':
case 'context':
await sessionContextAction(options);
break;
case 'notify':
await notifyAction(options);
break;
case 'help':
case undefined:
showHelp();
break;
default:
console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
console.error(chalk.gray('Run "ccw hook help" for usage information'));
process.exit(1);
}
}