diff --git a/npm/packages/ruvector/bin/cli.js b/npm/packages/ruvector/bin/cli.js index 663d1913..a0342008 100755 --- a/npm/packages/ruvector/bin/cli.js +++ b/npm/packages/ruvector/bin/cli.js @@ -2091,4 +2091,337 @@ program } }); +// ============================================ +// Self-Learning Intelligence Hooks +// ============================================ + +const INTEL_PATH = path.join(require('os').homedir(), '.ruvector', 'intelligence.json'); + +class Intelligence { + constructor() { + this.data = this.load(); + this.alpha = 0.1; + this.lastEditedFile = null; + } + + load() { + 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() { + 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)); + } + + now() { return Math.floor(Date.now() / 1000); } + + embed(text) { + 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; + } + + similarity(a, b) { + 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, content, metadata = {}) { + 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, topK) { + 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); + } + + getQ(state, action) { + const key = `${state}|${action}`; + return this.data.patterns[key]?.q_value ?? 0; + } + + updateQ(state, action, reward) { + 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, action, outcome, reward) { + 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, actions) { + 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, file, crateName, operation = 'edit') { + const fileType = file ? path.extname(file).slice(1) : 'unknown'; + const state = `${operation}_${fileType}_in_${crateName ?? 'project'}`; + const agentMap = { + rs: ['rust-developer', 'coder', 'reviewer', 'tester'], + ts: ['typescript-developer', 'coder', 'frontend-dev'], + tsx: ['typescript-developer', 'coder', 'frontend-dev'], + js: ['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) { + 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: '' }; + } + } + + recordFileSequence(fromFile, toFile) { + 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; + } + + suggestNext(file, limit = 3) { + 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 })); + } + + classifyCommand(command) { + 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')) return { category: 'javascript', subcategory: cmd.includes('test') ? 'test' : 'build', risk: 'low' }; + if (cmd.includes('git')) return { category: 'git', subcategory: 'vcs', risk: cmd.includes('push') ? 'medium' : 'low' }; + if (cmd.includes('rm') || cmd.includes('delete')) return { category: 'filesystem', subcategory: 'destructive', risk: 'high' }; + return { category: 'shell', subcategory: 'general', risk: 'low' }; + } + + swarmStats() { + const agents = Object.keys(this.data.agents).length; + const edges = this.data.edges.length; + return { agents, edges }; + } + + stats() { return this.data.stats; } + sessionStart() { this.data.stats.session_count++; this.data.stats.last_session = this.now(); } + sessionEnd() { + 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() { return this.lastEditedFile; } +} + +// Hooks command group +const hooksCmd = program.command('hooks').description('Self-learning intelligence hooks for Claude Code'); + +hooksCmd.command('init').description('Initialize hooks in current project').option('--force', 'Force overwrite').action((opts) => { + const settingsPath = path.join(process.cwd(), '.claude', 'settings.json'); + const settingsDir = path.dirname(settingsPath); + if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true }); + let settings = {}; + if (fs.existsSync(settingsPath) && !opts.force) { + try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); } catch {} + } + settings.hooks = settings.hooks || {}; + settings.hooks.PreToolUse = [ + { matcher: 'Edit|Write|MultiEdit', hooks: ['ruvector hooks pre-edit "$TOOL_INPUT_file_path"'] }, + { matcher: 'Bash', hooks: ['ruvector hooks pre-command "$TOOL_INPUT_command"'] } + ]; + settings.hooks.PostToolUse = [ + { matcher: 'Edit|Write|MultiEdit', hooks: ['ruvector hooks post-edit "$TOOL_INPUT_file_path"'] }, + { matcher: 'Bash', hooks: ['ruvector hooks post-command "$TOOL_INPUT_command"'] } + ]; + settings.hooks.SessionStart = ['ruvector hooks session-start']; + settings.hooks.Stop = ['ruvector hooks session-end']; + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); + console.log(chalk.green('โœ… Hooks initialized in .claude/settings.json')); +}); + +hooksCmd.command('stats').description('Show intelligence statistics').action(() => { + const intel = new Intelligence(); + const stats = intel.stats(); + const swarm = intel.swarmStats(); + console.log(chalk.bold.cyan('\n๐Ÿง  RuVector Intelligence Stats\n')); + console.log(` ${chalk.green(stats.total_patterns)} Q-learning patterns`); + console.log(` ${chalk.green(stats.total_memories)} vector memories`); + console.log(` ${chalk.green(stats.total_trajectories)} learning trajectories`); + console.log(` ${chalk.green(stats.total_errors)} error patterns\n`); + console.log(chalk.bold('Swarm Status:')); + console.log(` ${chalk.cyan(swarm.agents)} agents registered`); + console.log(` ${chalk.cyan(swarm.edges)} coordination edges`); +}); + +hooksCmd.command('session-start').description('Session start hook').option('--resume', 'Resume previous session').action(() => { + const intel = new Intelligence(); + intel.sessionStart(); + intel.save(); + console.log(chalk.bold.cyan('๐Ÿง  RuVector Intelligence Layer Active')); + console.log('โšก Intelligence guides: agent routing, error fixes, file sequences'); +}); + +hooksCmd.command('session-end').description('Session end hook').option('--export-metrics', 'Export metrics').action((opts) => { + 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 })); +}); + +hooksCmd.command('pre-edit').description('Pre-edit intelligence').argument('', 'File path').action((file) => { + 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(chalk.bold('๐Ÿง  Intelligence Analysis:')); + console.log(` ๐Ÿ“ ${chalk.cyan(crate ?? 'project')}/${fileName}`); + console.log(` ๐Ÿค– Recommended: ${chalk.green.bold(agent)} (${(confidence * 100).toFixed(0)}% confidence)`); + if (reason) console.log(` โ†’ ${chalk.dim(reason)}`); + 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)`)); + } +}); + +hooksCmd.command('post-edit').description('Post-edit learning').argument('', 'File path').option('--success', 'Edit succeeded').option('--error ', 'Error message').action((file, opts) => { + const intel = new Intelligence(); + const success = opts.error ? false : (opts.success ?? true); + const ext = path.extname(file).slice(1); + const crateMatch = file.match(/crates\/([^/]+)/); + const crate = crateMatch?.[1] ?? 'project'; + const state = `edit_${ext}_in_${crate}`; + 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: ${chalk.cyan(test.command)}`); +}); + +hooksCmd.command('pre-command').description('Pre-command intelligence').argument('', 'Command').action((command) => { + const intel = new Intelligence(); + const cmd = command.join(' '); + const classification = intel.classifyCommand(cmd); + console.log(chalk.bold('๐Ÿง  Command Analysis:')); + console.log(` ๐Ÿ“ฆ Category: ${chalk.cyan(classification.category)}`); + console.log(` ๐Ÿท๏ธ Type: ${classification.subcategory}`); + if (classification.risk === 'high') console.log(` โš ๏ธ Risk: ${chalk.red('HIGH')} - Review carefully`); + else if (classification.risk === 'medium') console.log(` โšก Risk: ${chalk.yellow('MEDIUM')}`); + else console.log(` โœ… Risk: ${chalk.green('LOW')}`); +}); + +hooksCmd.command('post-command').description('Post-command learning').argument('', 'Command').option('--success', 'Success').option('--error ', 'Error message').action((command, opts) => { + const intel = new Intelligence(); + const cmd = command.join(' '); + const success = opts.error ? false : (opts.success ?? true); + const classification = intel.classifyCommand(cmd); + intel.learn(`cmd_${classification.category}_${classification.subcategory}`, success ? 'success' : 'failure', success ? 'completed' : 'failed', success ? 0.8 : -0.3); + intel.remember('command', `${cmd} ${success ? 'succeeded' : 'failed'}`); + intel.save(); + console.log(`๐Ÿ“Š Command ${success ? 'โœ…' : 'โŒ'} recorded`); +}); + +hooksCmd.command('route').description('Route task to agent').argument('', 'Task').option('--file ', 'File').option('--crate ', 'Crate').action((task, opts) => { + const intel = new Intelligence(); + const result = intel.route(task.join(' '), opts.file, opts.crate); + console.log(JSON.stringify({ task: task.join(' '), recommended: result.agent, confidence: result.confidence, reasoning: result.reason }, null, 2)); +}); + +hooksCmd.command('suggest-context').description('Suggest relevant context').action(() => { + const intel = new Intelligence(); + const stats = intel.stats(); + console.log(`RuVector Intelligence: ${stats.total_patterns} learned patterns, ${stats.total_errors} error fixes available. Use 'ruvector hooks route' for agent suggestions.`); +}); + +hooksCmd.command('remember').description('Store in memory').requiredOption('-t, --type ', 'Memory type').argument('', 'Content').action((content, opts) => { + const intel = new Intelligence(); + const id = intel.remember(opts.type, content.join(' ')); + intel.save(); + console.log(JSON.stringify({ success: true, id })); +}); + +hooksCmd.command('recall').description('Search memory').argument('', 'Query').option('-k, --top-k ', 'Results', '5').action((query, opts) => { + 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)); +}); + +hooksCmd.command('pre-compact').description('Pre-compact hook').option('--auto', 'Auto mode').action(() => { + const intel = new Intelligence(); + intel.save(); + console.log('๐Ÿ—œ๏ธ Pre-compact: State saved'); +}); + +hooksCmd.command('swarm-recommend').description('Recommend agent for task').argument('', 'Task type').action((taskType) => { + console.log(JSON.stringify({ task_type: taskType, recommended: 'coder', type: 'default', score: 0.8 })); +}); + +hooksCmd.command('async-agent').description('Async agent hook').option('--action ', 'Action').option('--agent-id ', 'Agent ID').option('--task ', 'Task').action((opts) => { + console.log(JSON.stringify({ action: opts.action, agent_id: opts.agentId, status: 'ok' })); +}); + +hooksCmd.command('lsp-diagnostic').description('LSP diagnostic hook').option('--file ', 'File').option('--severity ', 'Severity').option('--message ', 'Message').action((opts) => { + console.log(JSON.stringify({ file: opts.file, severity: opts.severity, action: 'logged' })); +}); + +hooksCmd.command('track-notification').description('Track notification').action(() => { + console.log(JSON.stringify({ tracked: true })); +}); + program.parse(); diff --git a/npm/packages/ruvector/package.json b/npm/packages/ruvector/package.json index 9abb4416..866632e4 100644 --- a/npm/packages/ruvector/package.json +++ b/npm/packages/ruvector/package.json @@ -1,6 +1,6 @@ { "name": "ruvector", - "version": "0.1.35", + "version": "0.1.39", "description": "High-performance vector database for Node.js with automatic native/WASM fallback", "main": "dist/index.js", "types": "dist/index.d.ts",