mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
- 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.
317 lines
8.3 KiB
TypeScript
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);
|
|
}
|
|
}
|