git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
1176 lines
39 KiB
JavaScript
1176 lines
39 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* RuVector CLI - Command-line interface for RuVector vector database
|
|
*
|
|
* This CLI provides access to hooks, memory, learning, and swarm commands.
|
|
* Supports PostgreSQL storage (preferred) with JSON fallback.
|
|
*
|
|
* Set RUVECTOR_POSTGRES_URL or DATABASE_URL for PostgreSQL support.
|
|
*/
|
|
|
|
import { program } from 'commander';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
|
|
const INTEL_PATH = path.join(os.homedir(), '.ruvector', 'intelligence.json');
|
|
|
|
interface QPattern {
|
|
state: string;
|
|
action: string;
|
|
q_value: number;
|
|
visits: number;
|
|
last_update: number;
|
|
}
|
|
|
|
interface MemoryEntry {
|
|
id: string;
|
|
memory_type: string;
|
|
content: string;
|
|
embedding: number[];
|
|
metadata: Record<string, string>;
|
|
timestamp: number;
|
|
}
|
|
|
|
interface Trajectory {
|
|
id: string;
|
|
state: string;
|
|
action: string;
|
|
outcome: string;
|
|
reward: number;
|
|
timestamp: number;
|
|
}
|
|
|
|
interface ErrorPattern {
|
|
code: string;
|
|
error_type: string;
|
|
message: string;
|
|
fixes: string[];
|
|
occurrences: number;
|
|
}
|
|
|
|
interface SwarmAgent {
|
|
id: string;
|
|
agent_type: string;
|
|
capabilities: string[];
|
|
success_rate: number;
|
|
task_count: number;
|
|
status: string;
|
|
}
|
|
|
|
interface SwarmEdge {
|
|
source: string;
|
|
target: string;
|
|
weight: number;
|
|
coordination_count: number;
|
|
}
|
|
|
|
interface FileSequence {
|
|
from_file: string;
|
|
to_file: string;
|
|
count: number;
|
|
}
|
|
|
|
interface IntelligenceStats {
|
|
total_patterns: number;
|
|
total_memories: number;
|
|
total_trajectories: number;
|
|
total_errors: number;
|
|
session_count: number;
|
|
last_session: number;
|
|
}
|
|
|
|
interface IntelligenceData {
|
|
patterns: Record<string, QPattern>;
|
|
memories: MemoryEntry[];
|
|
trajectories: Trajectory[];
|
|
errors: Record<string, ErrorPattern>;
|
|
file_sequences: FileSequence[];
|
|
agents: Record<string, SwarmAgent>;
|
|
edges: SwarmEdge[];
|
|
stats: IntelligenceStats;
|
|
}
|
|
|
|
class Intelligence {
|
|
private data: IntelligenceData;
|
|
private alpha = 0.1;
|
|
private lastEditedFile: string | null = null;
|
|
|
|
constructor() {
|
|
this.data = this.load();
|
|
}
|
|
|
|
private load(): IntelligenceData {
|
|
try {
|
|
if (fs.existsSync(INTEL_PATH)) {
|
|
return JSON.parse(fs.readFileSync(INTEL_PATH, 'utf-8'));
|
|
}
|
|
} catch {}
|
|
return {
|
|
patterns: {},
|
|
memories: [],
|
|
trajectories: [],
|
|
errors: {},
|
|
file_sequences: [],
|
|
agents: {},
|
|
edges: [],
|
|
stats: {
|
|
total_patterns: 0,
|
|
total_memories: 0,
|
|
total_trajectories: 0,
|
|
total_errors: 0,
|
|
session_count: 0,
|
|
last_session: 0
|
|
}
|
|
};
|
|
}
|
|
|
|
save(): void {
|
|
const dir = path.dirname(INTEL_PATH);
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
fs.writeFileSync(INTEL_PATH, JSON.stringify(this.data, null, 2));
|
|
}
|
|
|
|
private now(): number {
|
|
return Math.floor(Date.now() / 1000);
|
|
}
|
|
|
|
private embed(text: string): number[] {
|
|
const embedding = new Array(64).fill(0);
|
|
for (let i = 0; i < text.length; i++) {
|
|
const idx = (text.charCodeAt(i) + i * 7) % 64;
|
|
embedding[idx] += 1.0;
|
|
}
|
|
const norm = Math.sqrt(embedding.reduce((a, b) => a + b * b, 0));
|
|
if (norm > 0) {
|
|
for (let i = 0; i < embedding.length; i++) {
|
|
embedding[i] /= norm;
|
|
}
|
|
}
|
|
return embedding;
|
|
}
|
|
|
|
private similarity(a: number[], b: number[]): number {
|
|
if (a.length !== b.length) return 0;
|
|
const dot = a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
const normA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0));
|
|
const normB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0));
|
|
return normA > 0 && normB > 0 ? dot / (normA * normB) : 0;
|
|
}
|
|
|
|
remember(memoryType: string, content: string, metadata: Record<string, string> = {}): string {
|
|
const id = `mem_${this.now()}`;
|
|
this.data.memories.push({
|
|
id,
|
|
memory_type: memoryType,
|
|
content,
|
|
embedding: this.embed(content),
|
|
metadata,
|
|
timestamp: this.now()
|
|
});
|
|
if (this.data.memories.length > 5000) {
|
|
this.data.memories.splice(0, 1000);
|
|
}
|
|
this.data.stats.total_memories = this.data.memories.length;
|
|
return id;
|
|
}
|
|
|
|
recall(query: string, topK: number): MemoryEntry[] {
|
|
const queryEmbed = this.embed(query);
|
|
return this.data.memories
|
|
.map(m => ({ score: this.similarity(queryEmbed, m.embedding), memory: m }))
|
|
.sort((a, b) => b.score - a.score)
|
|
.slice(0, topK)
|
|
.map(r => r.memory);
|
|
}
|
|
|
|
private getQ(state: string, action: string): number {
|
|
const key = `${state}|${action}`;
|
|
return this.data.patterns[key]?.q_value ?? 0;
|
|
}
|
|
|
|
private updateQ(state: string, action: string, reward: number): void {
|
|
const key = `${state}|${action}`;
|
|
if (!this.data.patterns[key]) {
|
|
this.data.patterns[key] = { state, action, q_value: 0, visits: 0, last_update: 0 };
|
|
}
|
|
const p = this.data.patterns[key];
|
|
p.q_value = p.q_value + this.alpha * (reward - p.q_value);
|
|
p.visits++;
|
|
p.last_update = this.now();
|
|
this.data.stats.total_patterns = Object.keys(this.data.patterns).length;
|
|
}
|
|
|
|
learn(state: string, action: string, outcome: string, reward: number): string {
|
|
const id = `traj_${this.now()}`;
|
|
this.updateQ(state, action, reward);
|
|
this.data.trajectories.push({ id, state, action, outcome, reward, timestamp: this.now() });
|
|
if (this.data.trajectories.length > 1000) {
|
|
this.data.trajectories.splice(0, 200);
|
|
}
|
|
this.data.stats.total_trajectories = this.data.trajectories.length;
|
|
return id;
|
|
}
|
|
|
|
suggest(state: string, actions: string[]): { action: string; confidence: number } {
|
|
let bestAction = actions[0] ?? '';
|
|
let bestQ = -Infinity;
|
|
for (const action of actions) {
|
|
const q = this.getQ(state, action);
|
|
if (q > bestQ) {
|
|
bestQ = q;
|
|
bestAction = action;
|
|
}
|
|
}
|
|
return { action: bestAction, confidence: bestQ > 0 ? Math.min(bestQ, 1) : 0 };
|
|
}
|
|
|
|
route(task: string, file?: string, crateName?: string, operation = 'edit'): { agent: string; confidence: number; reason: string } {
|
|
const fileType = file ? path.extname(file).slice(1) : 'unknown';
|
|
const state = `${operation}_${fileType}_in_${crateName ?? 'project'}`;
|
|
|
|
const agentMap: Record<string, string[]> = {
|
|
rs: ['rust-developer', 'coder', 'reviewer', 'tester'],
|
|
ts: ['typescript-developer', 'coder', 'frontend-dev'],
|
|
tsx: ['typescript-developer', 'coder', 'frontend-dev'],
|
|
js: ['coder', 'frontend-dev'],
|
|
jsx: ['coder', 'frontend-dev'],
|
|
py: ['python-developer', 'coder', 'ml-developer'],
|
|
md: ['docs-writer', 'coder']
|
|
};
|
|
|
|
const agents = agentMap[fileType] ?? ['coder', 'reviewer'];
|
|
const { action, confidence } = this.suggest(state, agents);
|
|
|
|
const reason = confidence > 0.5 ? 'learned from past success'
|
|
: confidence > 0 ? 'based on patterns'
|
|
: `default for ${fileType} files`;
|
|
|
|
return { agent: action, confidence, reason };
|
|
}
|
|
|
|
shouldTest(file: string): { suggest: boolean; command: string } {
|
|
const ext = path.extname(file).slice(1);
|
|
switch (ext) {
|
|
case 'rs': {
|
|
const crateMatch = file.match(/crates\/([^/]+)/);
|
|
return crateMatch
|
|
? { suggest: true, command: `cargo test -p ${crateMatch[1]}` }
|
|
: { suggest: true, command: 'cargo test' };
|
|
}
|
|
case 'ts':
|
|
case 'tsx':
|
|
case 'js':
|
|
case 'jsx':
|
|
return { suggest: true, command: 'npm test' };
|
|
case 'py':
|
|
return { suggest: true, command: 'pytest' };
|
|
default:
|
|
return { suggest: false, command: '' };
|
|
}
|
|
}
|
|
|
|
// Record file edit sequence for prediction
|
|
recordFileSequence(fromFile: string, toFile: string): void {
|
|
const existing = this.data.file_sequences.find(
|
|
s => s.from_file === fromFile && s.to_file === toFile
|
|
);
|
|
if (existing) {
|
|
existing.count++;
|
|
} else {
|
|
this.data.file_sequences.push({ from_file: fromFile, to_file: toFile, count: 1 });
|
|
}
|
|
this.lastEditedFile = toFile;
|
|
}
|
|
|
|
// Suggest next files based on sequences
|
|
suggestNext(file: string, limit = 3): { file: string; score: number }[] {
|
|
return this.data.file_sequences
|
|
.filter(s => s.from_file === file)
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, limit)
|
|
.map(s => ({ file: s.to_file, score: s.count }));
|
|
}
|
|
|
|
// Record error pattern
|
|
recordError(command: string, message: string): string[] {
|
|
const codeMatch = message.match(/error\[([A-Z]\d+)\]/i) || message.match(/([A-Z]\d{4})/);
|
|
const codes: string[] = [];
|
|
|
|
if (codeMatch) {
|
|
const code = codeMatch[1];
|
|
codes.push(code);
|
|
|
|
if (!this.data.errors[code]) {
|
|
this.data.errors[code] = {
|
|
code,
|
|
error_type: this.classifyError(code),
|
|
message: message.slice(0, 500),
|
|
fixes: [],
|
|
occurrences: 0
|
|
};
|
|
}
|
|
this.data.errors[code].occurrences++;
|
|
this.data.errors[code].message = message.slice(0, 500);
|
|
this.data.stats.total_errors = Object.keys(this.data.errors).length;
|
|
}
|
|
|
|
return codes;
|
|
}
|
|
|
|
private classifyError(code: string): string {
|
|
if (code.startsWith('E0')) return 'type-error';
|
|
if (code.startsWith('E1')) return 'borrow-error';
|
|
if (code.startsWith('E2')) return 'lifetime-error';
|
|
if (code.startsWith('E3')) return 'trait-error';
|
|
if (code.startsWith('E4')) return 'macro-error';
|
|
if (code.startsWith('E5')) return 'pattern-error';
|
|
if (code.startsWith('E6')) return 'import-error';
|
|
if (code.startsWith('E7')) return 'async-error';
|
|
return 'unknown-error';
|
|
}
|
|
|
|
// Get fix suggestions for error code
|
|
suggestFix(code: string): { code: string; type: string; fixes: string[]; occurrences: number } | null {
|
|
const error = this.data.errors[code];
|
|
if (!error) return null;
|
|
return {
|
|
code: error.code,
|
|
type: error.error_type,
|
|
fixes: error.fixes,
|
|
occurrences: error.occurrences
|
|
};
|
|
}
|
|
|
|
// Classify command type
|
|
classifyCommand(command: string): { category: string; subcategory: string; risk: string } {
|
|
const cmd = command.toLowerCase();
|
|
|
|
if (cmd.includes('cargo') || cmd.includes('rustc')) {
|
|
return { category: 'rust', subcategory: cmd.includes('test') ? 'test' : 'build', risk: 'low' };
|
|
}
|
|
if (cmd.includes('npm') || cmd.includes('node') || cmd.includes('yarn')) {
|
|
return { category: 'javascript', subcategory: cmd.includes('test') ? 'test' : 'build', risk: 'low' };
|
|
}
|
|
if (cmd.includes('git')) {
|
|
const risk = cmd.includes('push') || cmd.includes('force') ? 'medium' : 'low';
|
|
return { category: 'git', subcategory: 'vcs', risk };
|
|
}
|
|
if (cmd.includes('rm') || cmd.includes('delete')) {
|
|
return { category: 'filesystem', subcategory: 'destructive', risk: 'high' };
|
|
}
|
|
|
|
return { category: 'shell', subcategory: 'general', risk: 'low' };
|
|
}
|
|
|
|
// Swarm methods
|
|
swarmRegister(id: string, agentType: string, capabilities: string[]): void {
|
|
this.data.agents[id] = {
|
|
id,
|
|
agent_type: agentType,
|
|
capabilities,
|
|
success_rate: 1.0,
|
|
task_count: 0,
|
|
status: 'active'
|
|
};
|
|
}
|
|
|
|
swarmCoordinate(source: string, target: string, weight: number): void {
|
|
const existing = this.data.edges.find(e => e.source === source && e.target === target);
|
|
if (existing) {
|
|
existing.weight = (existing.weight + weight) / 2;
|
|
existing.coordination_count++;
|
|
} else {
|
|
this.data.edges.push({ source, target, weight, coordination_count: 1 });
|
|
}
|
|
}
|
|
|
|
swarmOptimize(tasks: string[]): { task: string; agents: number; edges: number }[] {
|
|
return tasks.map(task => ({
|
|
task,
|
|
agents: Object.keys(this.data.agents).length,
|
|
edges: this.data.edges.length
|
|
}));
|
|
}
|
|
|
|
swarmRecommend(taskType: string): { agent: string; type: string; score: number } | null {
|
|
const agents = Object.values(this.data.agents);
|
|
if (agents.length === 0) return null;
|
|
|
|
// Find agent with matching capability or best success rate
|
|
const matching = agents.filter(a =>
|
|
a.capabilities.some(c => taskType.toLowerCase().includes(c.toLowerCase()))
|
|
);
|
|
|
|
const best = matching.length > 0
|
|
? matching.sort((a, b) => b.success_rate - a.success_rate)[0]
|
|
: agents.sort((a, b) => b.success_rate - a.success_rate)[0];
|
|
|
|
return { agent: best.id, type: best.agent_type, score: best.success_rate };
|
|
}
|
|
|
|
swarmHeal(failedAgentId: string): { healed: boolean; replacement: string | null } {
|
|
const failed = this.data.agents[failedAgentId];
|
|
if (!failed) return { healed: false, replacement: null };
|
|
|
|
// Mark as failed
|
|
failed.status = 'failed';
|
|
failed.success_rate = 0;
|
|
|
|
// Find replacement with same type
|
|
const replacement = Object.values(this.data.agents).find(
|
|
a => a.agent_type === failed.agent_type && a.status === 'active' && a.id !== failedAgentId
|
|
);
|
|
|
|
return { healed: true, replacement: replacement?.id ?? null };
|
|
}
|
|
|
|
swarmStats(): { agents: number; edges: number; avgSuccess: number } {
|
|
const agents = Object.keys(this.data.agents).length;
|
|
const edges = this.data.edges.length;
|
|
const activeAgents = Object.values(this.data.agents).filter(a => a.status === 'active');
|
|
const avgSuccess = activeAgents.length > 0
|
|
? activeAgents.reduce((sum, a) => sum + a.success_rate, 0) / activeAgents.length
|
|
: 0;
|
|
return { agents, edges, avgSuccess };
|
|
}
|
|
|
|
stats(): IntelligenceStats {
|
|
return this.data.stats;
|
|
}
|
|
|
|
sessionStart(): void {
|
|
this.data.stats.session_count++;
|
|
this.data.stats.last_session = this.now();
|
|
}
|
|
|
|
sessionEnd(): { duration: number; actions: number } {
|
|
const duration = this.now() - this.data.stats.last_session;
|
|
const actions = this.data.trajectories.filter(t => t.timestamp >= this.data.stats.last_session).length;
|
|
return { duration, actions };
|
|
}
|
|
|
|
getLastEditedFile(): string | null {
|
|
return this.lastEditedFile;
|
|
}
|
|
}
|
|
|
|
// Generate Claude hooks configuration
|
|
function generateClaudeHooksConfig(): object {
|
|
return {
|
|
hooks: {
|
|
PreToolUse: [
|
|
{
|
|
matcher: "Edit|Write|MultiEdit",
|
|
hooks: [
|
|
"npx @ruvector/cli hooks pre-edit \"$TOOL_INPUT_file_path\""
|
|
]
|
|
},
|
|
{
|
|
matcher: "Bash",
|
|
hooks: [
|
|
"npx @ruvector/cli hooks pre-command \"$TOOL_INPUT_command\""
|
|
]
|
|
}
|
|
],
|
|
PostToolUse: [
|
|
{
|
|
matcher: "Edit|Write|MultiEdit",
|
|
hooks: [
|
|
"npx @ruvector/cli hooks post-edit --success \"$TOOL_INPUT_file_path\""
|
|
]
|
|
},
|
|
{
|
|
matcher: "Bash",
|
|
hooks: [
|
|
"npx @ruvector/cli hooks post-command --success \"$TOOL_INPUT_command\""
|
|
]
|
|
}
|
|
],
|
|
SessionStart: [
|
|
"npx @ruvector/cli hooks session-start"
|
|
],
|
|
Stop: [
|
|
"npx @ruvector/cli hooks session-end"
|
|
],
|
|
PreCompact: [
|
|
"npx @ruvector/cli hooks pre-compact"
|
|
]
|
|
}
|
|
};
|
|
}
|
|
|
|
// CLI setup
|
|
program
|
|
.name('ruvector')
|
|
.description('RuVector CLI - High-performance vector database')
|
|
.version('0.1.27');
|
|
|
|
const hooks = program.command('hooks').description('Self-learning intelligence hooks for Claude Code');
|
|
|
|
// ============================================================================
|
|
// Core Commands
|
|
// ============================================================================
|
|
|
|
hooks.command('init')
|
|
.description('Initialize hooks in current project')
|
|
.option('--force', 'Force overwrite existing configuration')
|
|
.action((opts: { force?: boolean }) => {
|
|
const configPath = path.join(process.cwd(), '.ruvector', 'hooks.json');
|
|
const configDir = path.dirname(configPath);
|
|
const claudeDir = path.join(process.cwd(), '.claude');
|
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
|
|
// Check if already initialized
|
|
if (fs.existsSync(settingsPath) && !opts.force) {
|
|
console.log('Hooks already initialized. Use --force to overwrite.');
|
|
return;
|
|
}
|
|
|
|
// Create .ruvector config
|
|
if (!fs.existsSync(configDir)) {
|
|
fs.mkdirSync(configDir, { recursive: true });
|
|
}
|
|
|
|
const config = {
|
|
version: '1.0.0',
|
|
enabled: true,
|
|
storage: 'json',
|
|
postgres_url: null,
|
|
learning: { alpha: 0.1, gamma: 0.95, epsilon: 0.1 }
|
|
};
|
|
|
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
|
|
// Create .claude/settings.json with hooks
|
|
if (!fs.existsSync(claudeDir)) {
|
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
}
|
|
|
|
let settings: Record<string, unknown> = {};
|
|
if (fs.existsSync(settingsPath)) {
|
|
try {
|
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
} catch {}
|
|
}
|
|
|
|
const hooksConfig = generateClaudeHooksConfig();
|
|
settings = { ...settings, ...hooksConfig };
|
|
|
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
|
|
console.log('✅ Hooks initialized!');
|
|
console.log(' Created: .ruvector/hooks.json');
|
|
console.log(' Created: .claude/settings.json');
|
|
console.log('\nNext steps:');
|
|
console.log(' 1. Restart Claude Code to activate hooks');
|
|
console.log(" 2. Run 'ruvector hooks stats' to verify");
|
|
});
|
|
|
|
hooks.command('install')
|
|
.description('Install hooks into Claude settings')
|
|
.option('--settings-dir <dir>', 'Claude settings directory', '.claude')
|
|
.action((opts: { settingsDir: string }) => {
|
|
const settingsPath = path.join(process.cwd(), opts.settingsDir, 'settings.json');
|
|
const settingsDir = path.dirname(settingsPath);
|
|
|
|
if (!fs.existsSync(settingsDir)) {
|
|
fs.mkdirSync(settingsDir, { recursive: true });
|
|
}
|
|
|
|
let settings: Record<string, unknown> = {};
|
|
if (fs.existsSync(settingsPath)) {
|
|
try {
|
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
} catch {}
|
|
}
|
|
|
|
const hooksConfig = generateClaudeHooksConfig();
|
|
settings = { ...settings, ...hooksConfig };
|
|
|
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
console.log(`✅ Hooks installed to ${settingsPath}`);
|
|
console.log('\nInstalled hooks:');
|
|
console.log(' - PreToolUse: Edit, Write, MultiEdit, Bash');
|
|
console.log(' - PostToolUse: Edit, Write, MultiEdit, Bash');
|
|
console.log(' - SessionStart, Stop, PreCompact');
|
|
});
|
|
|
|
hooks.command('stats')
|
|
.description('Show intelligence statistics')
|
|
.action(() => {
|
|
const intel = new Intelligence();
|
|
const stats = intel.stats();
|
|
const swarm = intel.swarmStats();
|
|
|
|
console.log('\x1b[36m\x1b[1m🧠 RuVector Intelligence Stats\x1b[0m\n');
|
|
console.log(` \x1b[32m${stats.total_patterns}\x1b[0m Q-learning patterns`);
|
|
console.log(` \x1b[32m${stats.total_memories}\x1b[0m vector memories`);
|
|
console.log(` \x1b[32m${stats.total_trajectories}\x1b[0m learning trajectories`);
|
|
console.log(` \x1b[32m${stats.total_errors}\x1b[0m error patterns\n`);
|
|
console.log('\x1b[1mSwarm Status:\x1b[0m');
|
|
console.log(` \x1b[36m${swarm.agents}\x1b[0m agents registered`);
|
|
console.log(` \x1b[36m${swarm.edges}\x1b[0m coordination edges`);
|
|
const rate = swarm.avgSuccess > 0 ? `${(swarm.avgSuccess * 100).toFixed(0)}%` : 'N/A';
|
|
console.log(` \x1b[36m${rate}\x1b[0m average success rate`);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Session Hooks
|
|
// ============================================================================
|
|
|
|
hooks.command('session-start')
|
|
.description('Session start hook')
|
|
.action(() => {
|
|
const intel = new Intelligence();
|
|
intel.sessionStart();
|
|
intel.save();
|
|
console.log('\x1b[36m\x1b[1m🧠 RuVector Intelligence Layer Active\x1b[0m\n');
|
|
console.log('⚡ Intelligence guides: agent routing, error fixes, file sequences');
|
|
});
|
|
|
|
hooks.command('session-end')
|
|
.description('Session end hook')
|
|
.option('--export-metrics', 'Export session metrics')
|
|
.action((opts: { exportMetrics?: boolean }) => {
|
|
const intel = new Intelligence();
|
|
const sessionInfo = intel.sessionEnd();
|
|
intel.save();
|
|
|
|
console.log('📊 Session ended. Learning data saved.');
|
|
if (opts.exportMetrics) {
|
|
console.log(JSON.stringify({
|
|
duration_seconds: sessionInfo.duration,
|
|
actions_recorded: sessionInfo.actions,
|
|
saved: true
|
|
}, null, 2));
|
|
}
|
|
});
|
|
|
|
hooks.command('pre-compact')
|
|
.description('Pre-compact hook - save state before context compaction')
|
|
.action(() => {
|
|
const intel = new Intelligence();
|
|
const stats = intel.stats();
|
|
intel.save();
|
|
|
|
console.log(`🗜️ Pre-compact: ${stats.total_trajectories} trajectories, ${stats.total_memories} memories saved`);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Edit Hooks
|
|
// ============================================================================
|
|
|
|
hooks.command('pre-edit')
|
|
.description('Pre-edit intelligence hook')
|
|
.argument('<file>', 'File path')
|
|
.action((file: string) => {
|
|
const intel = new Intelligence();
|
|
const fileName = path.basename(file);
|
|
const crateMatch = file.match(/crates\/([^/]+)/);
|
|
const crate = crateMatch?.[1];
|
|
const { agent, confidence, reason } = intel.route(`edit ${fileName}`, file, crate, 'edit');
|
|
|
|
console.log('\x1b[1m🧠 Intelligence Analysis:\x1b[0m');
|
|
console.log(` 📁 \x1b[36m${crate ?? 'project'}\x1b[0m/${fileName}`);
|
|
console.log(` 🤖 Recommended: \x1b[32m\x1b[1m${agent}\x1b[0m (${(confidence * 100).toFixed(0)}% confidence)`);
|
|
if (reason) console.log(` → \x1b[2m${reason}\x1b[0m`);
|
|
|
|
// Show suggested next files
|
|
const nextFiles = intel.suggestNext(file, 3);
|
|
if (nextFiles.length > 0) {
|
|
console.log(' 📎 Likely next files:');
|
|
nextFiles.forEach(n => console.log(` - ${n.file} (${n.score} edits)`));
|
|
}
|
|
});
|
|
|
|
hooks.command('post-edit')
|
|
.description('Post-edit learning hook')
|
|
.argument('<file>', 'File path')
|
|
.option('--success', 'Edit succeeded')
|
|
.action((file: string, opts: { success?: boolean }) => {
|
|
const intel = new Intelligence();
|
|
const success = opts.success ?? false;
|
|
const ext = path.extname(file).slice(1);
|
|
const crateMatch = file.match(/crates\/([^/]+)/);
|
|
const crate = crateMatch?.[1] ?? 'project';
|
|
const state = `edit_${ext}_in_${crate}`;
|
|
|
|
// Record file sequence
|
|
const lastFile = intel.getLastEditedFile();
|
|
if (lastFile && lastFile !== file) {
|
|
intel.recordFileSequence(lastFile, file);
|
|
}
|
|
|
|
intel.learn(state, success ? 'successful-edit' : 'failed-edit', success ? 'completed' : 'failed', success ? 1.0 : -0.5);
|
|
intel.remember('edit', `${success ? 'successful' : 'failed'} edit of ${ext} in ${crate}`);
|
|
intel.save();
|
|
|
|
console.log(`📊 Learning recorded: ${success ? '✅' : '❌'} ${path.basename(file)}`);
|
|
|
|
const test = intel.shouldTest(file);
|
|
if (test.suggest) console.log(` 🧪 Consider: \x1b[36m${test.command}\x1b[0m`);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Command Hooks
|
|
// ============================================================================
|
|
|
|
hooks.command('pre-command')
|
|
.description('Pre-command intelligence hook')
|
|
.argument('<command...>', 'Command to analyze')
|
|
.action((command: string[]) => {
|
|
const intel = new Intelligence();
|
|
const cmd = command.join(' ');
|
|
const classification = intel.classifyCommand(cmd);
|
|
|
|
console.log('\x1b[1m🧠 Command Analysis:\x1b[0m');
|
|
console.log(` 📦 Category: \x1b[36m${classification.category}\x1b[0m`);
|
|
console.log(` 🏷️ Type: ${classification.subcategory}`);
|
|
|
|
if (classification.risk === 'high') {
|
|
console.log(' ⚠️ Risk: \x1b[31mHIGH\x1b[0m - Review carefully');
|
|
} else if (classification.risk === 'medium') {
|
|
console.log(' ⚡ Risk: \x1b[33mMEDIUM\x1b[0m');
|
|
} else {
|
|
console.log(' ✅ Risk: \x1b[32mLOW\x1b[0m');
|
|
}
|
|
});
|
|
|
|
hooks.command('post-command')
|
|
.description('Post-command learning hook')
|
|
.argument('<command...>', 'Command that ran')
|
|
.option('--success', 'Command succeeded')
|
|
.option('--stderr <stderr>', 'Stderr output for error learning')
|
|
.action((command: string[], opts: { success?: boolean; stderr?: string }) => {
|
|
const intel = new Intelligence();
|
|
const cmd = command.join(' ');
|
|
const success = opts.success ?? true;
|
|
|
|
// Learn from command outcome
|
|
const classification = intel.classifyCommand(cmd);
|
|
intel.learn(
|
|
`cmd_${classification.category}_${classification.subcategory}`,
|
|
success ? 'success' : 'failure',
|
|
success ? 'completed' : 'failed',
|
|
success ? 0.8 : -0.3
|
|
);
|
|
|
|
// Learn from errors if stderr provided
|
|
if (opts.stderr) {
|
|
const errorCodes = intel.recordError(cmd, opts.stderr);
|
|
if (errorCodes.length > 0) {
|
|
console.log(`📊 Learned error patterns: ${errorCodes.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
intel.remember('command', `${cmd} ${success ? 'succeeded' : 'failed'}`);
|
|
intel.save();
|
|
|
|
console.log(`📊 Command ${success ? '✅' : '❌'} recorded`);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Error Learning
|
|
// ============================================================================
|
|
|
|
hooks.command('record-error')
|
|
.description('Record error pattern for learning')
|
|
.argument('<command>', 'Command that produced error')
|
|
.argument('<message>', 'Error message')
|
|
.action((command: string, message: string) => {
|
|
const intel = new Intelligence();
|
|
const codes = intel.recordError(command, message);
|
|
intel.save();
|
|
|
|
console.log(JSON.stringify({ errors: codes, recorded: codes.length }));
|
|
});
|
|
|
|
hooks.command('suggest-fix')
|
|
.description('Get suggested fix for error code')
|
|
.argument('<code>', 'Error code (e.g., E0308)')
|
|
.action((code: string) => {
|
|
const intel = new Intelligence();
|
|
const fix = intel.suggestFix(code);
|
|
|
|
if (fix) {
|
|
console.log(JSON.stringify(fix, null, 2));
|
|
} else {
|
|
console.log(JSON.stringify({ code, fixes: [], occurrences: 0, type: 'unknown' }));
|
|
}
|
|
});
|
|
|
|
hooks.command('suggest-next')
|
|
.description('Suggest next files to edit based on patterns')
|
|
.argument('<file>', 'Current file')
|
|
.option('-n, --limit <n>', 'Number of suggestions', '3')
|
|
.action((file: string, opts: { limit: string }) => {
|
|
const intel = new Intelligence();
|
|
const suggestions = intel.suggestNext(file, parseInt(opts.limit));
|
|
|
|
console.log(JSON.stringify({
|
|
current_file: file,
|
|
suggestions: suggestions.map(s => ({ file: s.file, frequency: s.score }))
|
|
}, null, 2));
|
|
});
|
|
|
|
// ============================================================================
|
|
// Memory Commands
|
|
// ============================================================================
|
|
|
|
hooks.command('remember')
|
|
.description('Store content in semantic memory')
|
|
.requiredOption('-t, --type <type>', 'Memory type')
|
|
.argument('<content...>', 'Content to remember')
|
|
.action((content: string[], opts: { type: string }) => {
|
|
const intel = new Intelligence();
|
|
const id = intel.remember(opts.type, content.join(' '));
|
|
intel.save();
|
|
console.log(JSON.stringify({ success: true, id }));
|
|
});
|
|
|
|
hooks.command('recall')
|
|
.description('Search memory semantically')
|
|
.argument('<query...>', 'Search query')
|
|
.option('-k, --top-k <n>', 'Number of results', '5')
|
|
.action((query: string[], opts: { topK: string }) => {
|
|
const intel = new Intelligence();
|
|
const results = intel.recall(query.join(' '), parseInt(opts.topK));
|
|
console.log(JSON.stringify({
|
|
query: query.join(' '),
|
|
results: results.map(r => ({
|
|
type: r.memory_type,
|
|
content: r.content.slice(0, 200),
|
|
timestamp: r.timestamp
|
|
}))
|
|
}, null, 2));
|
|
});
|
|
|
|
// ============================================================================
|
|
// Learning Commands
|
|
// ============================================================================
|
|
|
|
hooks.command('learn')
|
|
.description('Record a learning trajectory')
|
|
.argument('<state>', 'State identifier')
|
|
.argument('<action>', 'Action taken')
|
|
.option('-r, --reward <n>', 'Reward value', '0.0')
|
|
.action((state: string, action: string, opts: { reward: string }) => {
|
|
const intel = new Intelligence();
|
|
const id = intel.learn(state, action, 'recorded', parseFloat(opts.reward));
|
|
intel.save();
|
|
console.log(JSON.stringify({ success: true, id, state, action, reward: parseFloat(opts.reward) }));
|
|
});
|
|
|
|
hooks.command('suggest')
|
|
.description('Get action suggestion for state')
|
|
.argument('<state>', 'Current state')
|
|
.requiredOption('-a, --actions <actions>', 'Available actions (comma-separated)')
|
|
.action((state: string, opts: { actions: string }) => {
|
|
const intel = new Intelligence();
|
|
const actions = opts.actions.split(',').map(s => s.trim());
|
|
const result = intel.suggest(state, actions);
|
|
console.log(JSON.stringify({ state, ...result }, null, 2));
|
|
});
|
|
|
|
hooks.command('route')
|
|
.description('Route task to best agent')
|
|
.argument('<task...>', 'Task description')
|
|
.option('--file <file>', 'File being worked on')
|
|
.option('--crate-name <crate>', 'Crate/module context')
|
|
.action((task: string[], opts: { file?: string; crateName?: string }) => {
|
|
const intel = new Intelligence();
|
|
const result = intel.route(task.join(' '), opts.file, opts.crateName);
|
|
console.log(JSON.stringify({
|
|
task: task.join(' '),
|
|
recommended: result.agent,
|
|
confidence: result.confidence,
|
|
reasoning: result.reason
|
|
}, null, 2));
|
|
});
|
|
|
|
hooks.command('should-test')
|
|
.description('Check if tests should run')
|
|
.argument('<file>', 'File that was edited')
|
|
.action((file: string) => {
|
|
const intel = new Intelligence();
|
|
console.log(JSON.stringify(intel.shouldTest(file), null, 2));
|
|
});
|
|
|
|
// ============================================================================
|
|
// Swarm Commands
|
|
// ============================================================================
|
|
|
|
hooks.command('swarm-register')
|
|
.description('Register agent in swarm')
|
|
.argument('<id>', 'Agent ID')
|
|
.argument('<type>', 'Agent type')
|
|
.option('--capabilities <caps>', 'Capabilities (comma-separated)')
|
|
.action((id: string, type: string, opts: { capabilities?: string }) => {
|
|
const intel = new Intelligence();
|
|
const caps = opts.capabilities?.split(',').map(s => s.trim()) ?? [];
|
|
intel.swarmRegister(id, type, caps);
|
|
intel.save();
|
|
console.log(JSON.stringify({ success: true, agent_id: id, type }));
|
|
});
|
|
|
|
hooks.command('swarm-coordinate')
|
|
.description('Record agent coordination')
|
|
.argument('<source>', 'Source agent ID')
|
|
.argument('<target>', 'Target agent ID')
|
|
.option('-w, --weight <n>', 'Coordination weight', '1.0')
|
|
.action((source: string, target: string, opts: { weight: string }) => {
|
|
const intel = new Intelligence();
|
|
intel.swarmCoordinate(source, target, parseFloat(opts.weight));
|
|
intel.save();
|
|
console.log(JSON.stringify({ success: true, source, target, weight: parseFloat(opts.weight) }));
|
|
});
|
|
|
|
hooks.command('swarm-optimize')
|
|
.description('Optimize task distribution')
|
|
.argument('<tasks>', 'Tasks (comma-separated)')
|
|
.action((tasks: string) => {
|
|
const intel = new Intelligence();
|
|
const taskList = tasks.split(',').map(s => s.trim());
|
|
const result = intel.swarmOptimize(taskList);
|
|
console.log(JSON.stringify({ tasks: taskList.length, assignments: result }, null, 2));
|
|
});
|
|
|
|
hooks.command('swarm-recommend')
|
|
.description('Recommend agent for task type')
|
|
.argument('<task-type>', 'Type of task')
|
|
.action((taskType: string) => {
|
|
const intel = new Intelligence();
|
|
const result = intel.swarmRecommend(taskType);
|
|
if (result) {
|
|
console.log(JSON.stringify({ task_type: taskType, recommended: result.agent, type: result.type, score: result.score }));
|
|
} else {
|
|
console.log(JSON.stringify({ task_type: taskType, recommended: null, message: 'No matching agent found' }));
|
|
}
|
|
});
|
|
|
|
hooks.command('swarm-heal')
|
|
.description('Handle agent failure')
|
|
.argument('<agent-id>', 'Failed agent ID')
|
|
.action((agentId: string) => {
|
|
const intel = new Intelligence();
|
|
const result = intel.swarmHeal(agentId);
|
|
intel.save();
|
|
console.log(JSON.stringify({ failed_agent: agentId, healed: result.healed, replacement: result.replacement }));
|
|
});
|
|
|
|
hooks.command('swarm-stats')
|
|
.description('Show swarm statistics')
|
|
.action(() => {
|
|
const intel = new Intelligence();
|
|
const stats = intel.swarmStats();
|
|
console.log(JSON.stringify({
|
|
agents: stats.agents,
|
|
edges: stats.edges,
|
|
average_success_rate: stats.avgSuccess,
|
|
topology: 'mesh'
|
|
}, null, 2));
|
|
});
|
|
|
|
// ============================================================================
|
|
// Claude Code v2.0.55+ Features
|
|
// ============================================================================
|
|
|
|
hooks.command('lsp-diagnostic')
|
|
.description('Process LSP diagnostic events (Claude Code 2.0.55+)')
|
|
.option('--file <path>', 'File with diagnostic')
|
|
.option('--severity <level>', 'Diagnostic severity (error, warning, info, hint)')
|
|
.option('--message <text>', 'Diagnostic message')
|
|
.action((opts: { file?: string; severity?: string; message?: string }) => {
|
|
const intel = new Intelligence();
|
|
|
|
// Read hook input from stdin if available
|
|
let stdinData: any = null;
|
|
try {
|
|
const inputPath = process.env.CLAUDE_HOOK_INPUT;
|
|
if (inputPath && fs.existsSync(inputPath)) {
|
|
stdinData = JSON.parse(fs.readFileSync(inputPath, 'utf-8'));
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
const file = opts.file || stdinData?.tool_input?.file || 'unknown';
|
|
const severity = opts.severity || stdinData?.tool_input?.severity || 'info';
|
|
const message = opts.message || stdinData?.tool_input?.message || '';
|
|
|
|
// Learn from LSP diagnostics
|
|
if (severity === 'error' || severity === 'warning') {
|
|
// Record error and get codes
|
|
const codes = intel.recordError(`lsp:${file}`, message);
|
|
const errorCode = codes[0] || `${severity}-unknown`;
|
|
|
|
// Record trajectory for learning
|
|
const state = `lsp_${severity}_${path.extname(file).slice(1) || 'unknown'}`;
|
|
intel.learn(state, 'diagnostic', message.slice(0, 100), severity === 'error' ? -0.5 : -0.2);
|
|
intel.save();
|
|
|
|
// Output context for Claude
|
|
const fixInfo = intel.suggestFix(errorCode);
|
|
const learnedFixes = fixInfo?.fixes ?? [];
|
|
console.log(JSON.stringify({
|
|
file,
|
|
severity,
|
|
error_code: errorCode,
|
|
learned_fixes: learnedFixes.slice(0, 3),
|
|
recommendation: learnedFixes.length > 0 ? 'Apply learned fix' : 'Investigate error pattern'
|
|
}));
|
|
} else {
|
|
console.log(JSON.stringify({ file, severity, message, action: 'logged' }));
|
|
}
|
|
});
|
|
|
|
hooks.command('suggest-ultrathink')
|
|
.description('Recommend ultrathink mode for complex tasks (Claude Code 2.0.55+)')
|
|
.argument('<task...>', 'Task description')
|
|
.option('--file <path>', 'File being worked on')
|
|
.action((task: string[], opts: { file?: string }) => {
|
|
const intel = new Intelligence();
|
|
const taskStr = task.join(' ').toLowerCase();
|
|
const file = opts.file;
|
|
|
|
// Complexity patterns that suggest ultrathink mode
|
|
const complexityPatterns: Array<[string, number]> = [
|
|
['algorithm', 0.8], ['optimize', 0.7], ['refactor', 0.6],
|
|
['debug', 0.7], ['performance', 0.7], ['concurrent', 0.8],
|
|
['async', 0.6], ['architecture', 0.8], ['security', 0.7],
|
|
['cryptograph', 0.9], ['distributed', 0.8], ['consensus', 0.9],
|
|
['neural', 0.8], ['ml', 0.7], ['complex', 0.6],
|
|
['migrate', 0.7], ['integration', 0.6], ['api design', 0.7],
|
|
['database schema', 0.7], ['state machine', 0.8], ['parser', 0.8],
|
|
['compiler', 0.9], ['memory management', 0.8], ['thread', 0.7],
|
|
];
|
|
|
|
let complexityScore = 0;
|
|
const triggers: string[] = [];
|
|
|
|
for (const [pattern, weight] of complexityPatterns) {
|
|
if (taskStr.includes(pattern)) {
|
|
complexityScore = Math.max(complexityScore, weight);
|
|
triggers.push(pattern);
|
|
}
|
|
}
|
|
|
|
// Check file extension complexity
|
|
if (file) {
|
|
const ext = path.extname(file).slice(1);
|
|
const complexExts: Record<string, number> = {
|
|
rs: 0.5, cpp: 0.5, c: 0.4, zig: 0.5,
|
|
asm: 0.7, wasm: 0.6, sql: 0.4
|
|
};
|
|
if (complexExts[ext]) {
|
|
complexityScore = Math.max(complexityScore, complexExts[ext]);
|
|
triggers.push(`${ext} file`);
|
|
}
|
|
}
|
|
|
|
// Check learned patterns
|
|
const state = `ultrathink_${triggers[0] || 'general'}`;
|
|
const suggested = intel.suggest(state, ['enable', 'skip']);
|
|
|
|
const recommendUltrathink = complexityScore >= 0.6;
|
|
|
|
// Record trajectory for learning
|
|
intel.learn(state, recommendUltrathink ? 'enable' : 'skip', taskStr.slice(0, 100), 0);
|
|
|
|
// Build output
|
|
const output: Record<string, unknown> = {
|
|
task: task.join(' '),
|
|
complexity_score: complexityScore,
|
|
triggers,
|
|
recommend_ultrathink: recommendUltrathink,
|
|
learned_preference: suggested
|
|
};
|
|
|
|
if (recommendUltrathink) {
|
|
output.message = '🧠 Complex task detected - ultrathink mode recommended';
|
|
output.reasoning_depth = complexityScore >= 0.8 ? 'deep' : 'moderate';
|
|
} else {
|
|
output.message = 'Standard processing sufficient';
|
|
}
|
|
|
|
intel.save();
|
|
console.log(JSON.stringify(output, null, 2));
|
|
});
|
|
|
|
hooks.command('async-agent')
|
|
.description('Coordinate async sub-agent execution (Claude Code 2.0.55+)')
|
|
.option('--action <type>', 'Action: spawn, sync, complete', 'spawn')
|
|
.option('--agent-id <id>', 'Agent identifier')
|
|
.option('--task <description>', 'Task description (for spawn)')
|
|
.action((opts: { action: string; agentId?: string; task?: string }) => {
|
|
const intel = new Intelligence();
|
|
const action = opts.action;
|
|
const agentId = opts.agentId || `async-${Date.now()}`;
|
|
const task = opts.task || '';
|
|
|
|
switch (action) {
|
|
case 'spawn': {
|
|
// Register async agent
|
|
intel.swarmRegister(agentId, 'async-subagent', ['parallel', 'autonomous']);
|
|
|
|
// Record spawn event
|
|
const state = `async_spawn_${task.split(' ')[0] || 'general'}`;
|
|
intel.learn(state, 'spawn', task.slice(0, 100), 0.1);
|
|
|
|
// Get learned patterns for similar tasks
|
|
const suggested = intel.suggest(state, ['coder', 'researcher', 'tester', 'reviewer']);
|
|
|
|
intel.save();
|
|
console.log(JSON.stringify({
|
|
action: 'spawned',
|
|
agent_id: agentId,
|
|
task,
|
|
suggested_type: suggested.action,
|
|
status: 'running',
|
|
async: true
|
|
}));
|
|
break;
|
|
}
|
|
|
|
case 'sync': {
|
|
// Check agent status and coordinate
|
|
const stats = intel.swarmStats();
|
|
console.log(JSON.stringify({
|
|
action: 'sync',
|
|
agent_id: agentId,
|
|
swarm_agents: stats.agents,
|
|
status: 'synchronized',
|
|
pending_results: 0
|
|
}));
|
|
break;
|
|
}
|
|
|
|
case 'complete': {
|
|
// Mark agent complete and record success
|
|
const state = `async_complete_${agentId}`;
|
|
intel.learn(state, 'complete', task.slice(0, 100), 1.0);
|
|
|
|
// Update agent status
|
|
intel.swarmHeal(agentId); // Resets/removes the agent
|
|
|
|
intel.save();
|
|
console.log(JSON.stringify({
|
|
action: 'completed',
|
|
agent_id: agentId,
|
|
status: 'success',
|
|
learning_recorded: true
|
|
}));
|
|
break;
|
|
}
|
|
|
|
default:
|
|
console.log(JSON.stringify({
|
|
error: `Unknown action: ${action}`,
|
|
valid_actions: ['spawn', 'sync', 'complete']
|
|
}));
|
|
}
|
|
});
|
|
|
|
program.parse();
|