feat(hooks): add verify, doctor, export, import commands and enhanced init

New commands:
- hooks verify: Check if hooks are working correctly
- hooks doctor: Diagnose and fix setup issues (with --fix)
- hooks export: Export intelligence data for backup
- hooks import: Import intelligence data (with --merge, --dry-run)

Enhanced init:
- Auto-detect project type (Rust, Node, Python, Go, Ruby, Java)
- Project-specific permissions (cargo for Rust, npm for Node, etc.)
- StatusLine configuration with .claude/statusline.sh
- MCP server configuration (claude-flow)
- .gitignore update (adds .ruvector/)
- Creates .ruvector/ directory

New options for init:
- --no-gitignore: Skip .gitignore update
- --no-mcp: Skip MCP server configuration
- --no-statusline: Skip statusLine configuration
This commit is contained in:
rUv 2025-12-30 21:18:29 +00:00
parent 48067cb09e
commit 5ab5b69d35
2 changed files with 399 additions and 25 deletions

View file

@ -2267,6 +2267,40 @@ class Intelligence {
// Hooks command group
const hooksCmd = program.command('hooks').description('Self-learning intelligence hooks for Claude Code');
// Helper: Detect project type
function detectProjectType() {
const cwd = process.cwd();
const types = [];
if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) types.push('rust');
if (fs.existsSync(path.join(cwd, 'package.json'))) types.push('node');
if (fs.existsSync(path.join(cwd, 'requirements.txt')) || fs.existsSync(path.join(cwd, 'pyproject.toml'))) types.push('python');
if (fs.existsSync(path.join(cwd, 'go.mod'))) types.push('go');
if (fs.existsSync(path.join(cwd, 'Gemfile'))) types.push('ruby');
if (fs.existsSync(path.join(cwd, 'pom.xml')) || fs.existsSync(path.join(cwd, 'build.gradle'))) types.push('java');
return types.length > 0 ? types : ['generic'];
}
// Helper: Get permissions for project type
function getPermissionsForProjectType(types) {
const basePermissions = [
'Bash(git status)', 'Bash(git diff:*)', 'Bash(git log:*)', 'Bash(git add:*)',
'Bash(git commit:*)', 'Bash(git push)', 'Bash(git branch:*)', 'Bash(git checkout:*)',
'Bash(ls:*)', 'Bash(pwd)', 'Bash(cat:*)', 'Bash(mkdir:*)', 'Bash(which:*)', 'Bash(ruvector:*)'
];
const typePermissions = {
rust: ['Bash(cargo:*)', 'Bash(rustc:*)', 'Bash(rustfmt:*)', 'Bash(clippy:*)', 'Bash(wasm-pack:*)'],
node: ['Bash(npm:*)', 'Bash(npx:*)', 'Bash(node:*)', 'Bash(yarn:*)', 'Bash(pnpm:*)'],
python: ['Bash(python:*)', 'Bash(pip:*)', 'Bash(pytest:*)', 'Bash(poetry:*)', 'Bash(uv:*)'],
go: ['Bash(go:*)', 'Bash(gofmt:*)'],
ruby: ['Bash(ruby:*)', 'Bash(gem:*)', 'Bash(bundle:*)', 'Bash(rails:*)'],
java: ['Bash(mvn:*)', 'Bash(gradle:*)', 'Bash(java:*)', 'Bash(javac:*)'],
generic: ['Bash(make:*)']
};
let perms = [...basePermissions];
types.forEach(t => { if (typePermissions[t]) perms = perms.concat(typePermissions[t]); });
return [...new Set(perms)];
}
hooksCmd.command('init')
.description('Initialize hooks in current project')
.option('--force', 'Force overwrite existing settings')
@ -2274,6 +2308,9 @@ hooksCmd.command('init')
.option('--no-claude-md', 'Skip CLAUDE.md creation')
.option('--no-permissions', 'Skip permissions configuration')
.option('--no-env', 'Skip environment variables')
.option('--no-gitignore', 'Skip .gitignore update')
.option('--no-mcp', 'Skip MCP server configuration')
.option('--no-statusline', 'Skip statusLine configuration')
.action((opts) => {
const settingsPath = path.join(process.cwd(), '.claude', 'settings.json');
const settingsDir = path.dirname(settingsPath);
@ -2294,6 +2331,10 @@ hooksCmd.command('init')
if (settings.hooks.End) { delete settings.hooks.End; }
}
// Detect project type
const projectTypes = detectProjectType();
console.log(chalk.blue(` ✓ Detected project type(s): ${projectTypes.join(', ')}`));
// Environment variables for intelligence (unless --minimal or --no-env)
if (!opts.minimal && opts.env !== false) {
settings.env = settings.env || {};
@ -2304,36 +2345,56 @@ hooksCmd.command('init')
console.log(chalk.blue(' ✓ Environment variables configured'));
}
// Permissions (unless --minimal or --no-permissions)
// Permissions based on detected project type (unless --minimal or --no-permissions)
if (!opts.minimal && opts.permissions !== false) {
settings.permissions = settings.permissions || {};
settings.permissions.allow = settings.permissions.allow || [
'Bash(npm run:*)',
'Bash(npm test:*)',
'Bash(npm install:*)',
'Bash(npx:*)',
'Bash(git status)',
'Bash(git diff:*)',
'Bash(git log:*)',
'Bash(git add:*)',
'Bash(git commit:*)',
'Bash(git push)',
'Bash(git branch:*)',
'Bash(git checkout:*)',
'Bash(ls:*)',
'Bash(pwd)',
'Bash(cat:*)',
'Bash(mkdir:*)',
'Bash(which:*)',
'Bash(node:*)',
'Bash(ruvector:*)'
];
settings.permissions.allow = settings.permissions.allow || getPermissionsForProjectType(projectTypes);
settings.permissions.deny = settings.permissions.deny || [
'Bash(rm -rf /)',
'Bash(sudo rm:*)',
'Bash(chmod 777:*)'
'Bash(chmod 777:*)',
'Bash(:(){ :|:& };:)'
];
console.log(chalk.blue(' ✓ Permissions configured'));
console.log(chalk.blue(' ✓ Permissions configured (project-specific)'));
}
// MCP server configuration (unless --minimal or --no-mcp)
if (!opts.minimal && opts.mcp !== false) {
settings.mcpServers = settings.mcpServers || {};
// Only add if not already configured
if (!settings.mcpServers['claude-flow'] && !settings.enabledMcpjsonServers?.includes('claude-flow')) {
settings.enabledMcpjsonServers = settings.enabledMcpjsonServers || [];
if (!settings.enabledMcpjsonServers.includes('claude-flow')) {
settings.enabledMcpjsonServers.push('claude-flow');
}
}
console.log(chalk.blue(' ✓ MCP servers configured'));
}
// StatusLine configuration (unless --minimal or --no-statusline)
if (!opts.minimal && opts.statusline !== false) {
if (!settings.statusLine) {
// Create a simple statusline script
const statuslineScript = path.join(settingsDir, 'statusline.sh');
const statuslineContent = `#!/bin/bash
# RuVector Status Line - shows intelligence stats
INTEL_FILE=".ruvector/intelligence.json"
if [ -f "$INTEL_FILE" ]; then
PATTERNS=$(jq -r '.patterns | length // 0' "$INTEL_FILE" 2>/dev/null || echo "0")
MEMORIES=$(jq -r '.memories | length // 0' "$INTEL_FILE" 2>/dev/null || echo "0")
echo "🧠 $PATTERNS patterns | 💾 $MEMORIES memories"
else
echo "🧠 RuVector"
fi
`;
fs.writeFileSync(statuslineScript, statuslineContent);
fs.chmodSync(statuslineScript, '755');
settings.statusLine = {
type: 'command',
command: '.claude/statusline.sh'
};
console.log(chalk.blue(' ✓ StatusLine configured'));
}
}
// Core hooks (always included)
@ -2486,6 +2547,32 @@ npx ruvector hooks init --force # Overwrite existing
} else if (fs.existsSync(claudeMdPath) && !opts.force) {
console.log(chalk.yellow(' CLAUDE.md already exists (use --force to overwrite)'));
}
// Update .gitignore (unless --no-gitignore)
if (opts.gitignore !== false) {
const gitignorePath = path.join(process.cwd(), '.gitignore');
const entriesToAdd = ['.ruvector/', '.claude/statusline.sh'];
let gitignoreContent = '';
if (fs.existsSync(gitignorePath)) {
gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
}
const linesToAdd = entriesToAdd.filter(entry => !gitignoreContent.includes(entry));
if (linesToAdd.length > 0) {
const newContent = gitignoreContent.trim() + '\n\n# RuVector intelligence data\n' + linesToAdd.join('\n') + '\n';
fs.writeFileSync(gitignorePath, newContent);
console.log(chalk.blue(' ✓ .gitignore updated'));
}
}
// Create .ruvector directory for intelligence data
const ruvectorDir = path.join(process.cwd(), '.ruvector');
if (!fs.existsSync(ruvectorDir)) {
fs.mkdirSync(ruvectorDir, { recursive: true });
console.log(chalk.blue(' ✓ .ruvector/ directory created'));
}
console.log(chalk.green('\n✅ RuVector hooks initialization complete!'));
console.log(chalk.dim(' Run `npx ruvector hooks verify` to test the setup'));
});
hooksCmd.command('stats').description('Show intelligence statistics').action(() => {
@ -2622,4 +2709,291 @@ hooksCmd.command('track-notification').description('Track notification').action(
console.log(JSON.stringify({ tracked: true }));
});
// Verify hooks are working
hooksCmd.command('verify')
.description('Verify hooks are working correctly')
.option('--verbose', 'Show detailed output')
.action((opts) => {
console.log(chalk.bold.cyan('\n🔍 RuVector Hooks Verification\n'));
const checks = [];
// Check 1: Settings file exists
const settingsPath = path.join(process.cwd(), '.claude', 'settings.json');
if (fs.existsSync(settingsPath)) {
checks.push({ name: 'Settings file', status: 'pass', detail: '.claude/settings.json exists' });
try {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
// Check hooks
const requiredHooks = ['PreToolUse', 'PostToolUse', 'SessionStart', 'Stop'];
const missingHooks = requiredHooks.filter(h => !settings.hooks?.[h]);
if (missingHooks.length === 0) {
checks.push({ name: 'Required hooks', status: 'pass', detail: 'All core hooks configured' });
} else {
checks.push({ name: 'Required hooks', status: 'fail', detail: `Missing: ${missingHooks.join(', ')}` });
}
// Check advanced hooks
const advancedHooks = ['UserPromptSubmit', 'PreCompact', 'Notification'];
const hasAdvanced = advancedHooks.filter(h => settings.hooks?.[h]);
if (hasAdvanced.length > 0) {
checks.push({ name: 'Advanced hooks', status: 'pass', detail: `${hasAdvanced.length}/3 configured` });
} else {
checks.push({ name: 'Advanced hooks', status: 'warn', detail: 'None configured (optional)' });
}
// Check env
if (settings.env?.RUVECTOR_INTELLIGENCE_ENABLED) {
checks.push({ name: 'Environment vars', status: 'pass', detail: 'Intelligence enabled' });
} else {
checks.push({ name: 'Environment vars', status: 'warn', detail: 'Not configured' });
}
// Check permissions
if (settings.permissions?.allow?.length > 0) {
checks.push({ name: 'Permissions', status: 'pass', detail: `${settings.permissions.allow.length} allowed patterns` });
} else {
checks.push({ name: 'Permissions', status: 'warn', detail: 'Not configured' });
}
} catch (e) {
checks.push({ name: 'Settings parse', status: 'fail', detail: 'Invalid JSON' });
}
} else {
checks.push({ name: 'Settings file', status: 'fail', detail: 'Run `npx ruvector hooks init` first' });
}
// Check 2: .ruvector directory
const ruvectorDir = path.join(process.cwd(), '.ruvector');
if (fs.existsSync(ruvectorDir)) {
checks.push({ name: 'Data directory', status: 'pass', detail: '.ruvector/ exists' });
const intelFile = path.join(ruvectorDir, 'intelligence.json');
if (fs.existsSync(intelFile)) {
const stats = fs.statSync(intelFile);
checks.push({ name: 'Intelligence file', status: 'pass', detail: `${(stats.size / 1024).toFixed(1)}KB` });
} else {
checks.push({ name: 'Intelligence file', status: 'warn', detail: 'Will be created on first use' });
}
} else {
checks.push({ name: 'Data directory', status: 'warn', detail: 'Will be created on first use' });
}
// Check 3: Hook command execution
try {
const { execSync } = require('child_process');
execSync('npx ruvector hooks stats', { stdio: 'pipe', timeout: 5000 });
checks.push({ name: 'Command execution', status: 'pass', detail: 'Hooks commands work' });
} catch (e) {
checks.push({ name: 'Command execution', status: 'fail', detail: 'Commands failed to execute' });
}
// Display results
let passCount = 0, warnCount = 0, failCount = 0;
checks.forEach(c => {
const icon = c.status === 'pass' ? chalk.green('✓') : c.status === 'warn' ? chalk.yellow('⚠') : chalk.red('✗');
const statusColor = c.status === 'pass' ? chalk.green : c.status === 'warn' ? chalk.yellow : chalk.red;
console.log(` ${icon} ${c.name}: ${statusColor(c.detail)}`);
if (c.status === 'pass') passCount++;
else if (c.status === 'warn') warnCount++;
else failCount++;
});
console.log('');
if (failCount === 0) {
console.log(chalk.green(`✅ Verification passed! ${passCount} checks passed, ${warnCount} warnings`));
} else {
console.log(chalk.red(`❌ Verification failed: ${failCount} issues found`));
console.log(chalk.dim(' Run `npx ruvector hooks doctor` for detailed diagnostics'));
}
});
// Doctor - diagnose setup issues
hooksCmd.command('doctor')
.description('Diagnose and fix setup issues')
.option('--fix', 'Automatically fix issues')
.action((opts) => {
console.log(chalk.bold.cyan('\n🩺 RuVector Hooks Doctor\n'));
const issues = [];
const fixes = [];
// Check settings file
const settingsPath = path.join(process.cwd(), '.claude', 'settings.json');
if (!fs.existsSync(settingsPath)) {
issues.push({ severity: 'error', message: 'No .claude/settings.json found', fix: 'Run `npx ruvector hooks init`' });
} else {
try {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
// Check for invalid schema
if (settings.$schema && !settings.$schema.includes('schemastore.org')) {
issues.push({ severity: 'warning', message: 'Invalid schema URL', fix: 'Will be corrected' });
if (opts.fix) {
settings.$schema = 'https://json.schemastore.org/claude-code-settings.json';
fixes.push('Fixed schema URL');
}
}
// Check for old hook names
if (settings.hooks?.Start || settings.hooks?.End) {
issues.push({ severity: 'error', message: 'Invalid hook names (Start/End)', fix: 'Should be SessionStart/Stop' });
if (opts.fix) {
delete settings.hooks.Start;
delete settings.hooks.End;
fixes.push('Removed invalid hook names');
}
}
// Check hook format
const hookNames = ['PreToolUse', 'PostToolUse'];
hookNames.forEach(name => {
if (settings.hooks?.[name]) {
settings.hooks[name].forEach((hook, i) => {
if (typeof hook.matcher === 'object') {
issues.push({ severity: 'error', message: `${name}[${i}].matcher should be string, not object`, fix: 'Will be corrected' });
}
});
}
});
// Check for npx vs direct command
const checkCommands = (hooks) => {
if (!hooks) return;
hooks.forEach(h => {
h.hooks?.forEach(hh => {
if (hh.command && hh.command.includes('ruvector') && !hh.command.startsWith('npx ') && !hh.command.includes('/bin/')) {
issues.push({ severity: 'warning', message: `Command should use 'npx ruvector' for portability`, fix: 'Update to use npx' });
}
});
});
};
Object.values(settings.hooks || {}).forEach(checkCommands);
// Save fixes
if (opts.fix && fixes.length > 0) {
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
}
} catch (e) {
issues.push({ severity: 'error', message: 'Invalid JSON in settings file', fix: 'Re-run `npx ruvector hooks init --force`' });
}
}
// Check .gitignore
const gitignorePath = path.join(process.cwd(), '.gitignore');
if (fs.existsSync(gitignorePath)) {
const content = fs.readFileSync(gitignorePath, 'utf-8');
if (!content.includes('.ruvector/')) {
issues.push({ severity: 'warning', message: '.ruvector/ not in .gitignore', fix: 'Add to prevent committing learning data' });
if (opts.fix) {
fs.appendFileSync(gitignorePath, '\n# RuVector intelligence data\n.ruvector/\n');
fixes.push('Added .ruvector/ to .gitignore');
}
}
}
// Display results
if (issues.length === 0) {
console.log(chalk.green(' ✓ No issues found! Your setup looks healthy.'));
} else {
issues.forEach(i => {
const icon = i.severity === 'error' ? chalk.red('✗') : chalk.yellow('⚠');
console.log(` ${icon} ${i.message}`);
console.log(chalk.dim(` Fix: ${i.fix}`));
});
if (opts.fix && fixes.length > 0) {
console.log(chalk.green(`\n✅ Applied ${fixes.length} fix(es):`));
fixes.forEach(f => console.log(chalk.green(`${f}`)));
} else if (issues.some(i => i.severity === 'error')) {
console.log(chalk.yellow('\n💡 Run with --fix to automatically fix issues'));
}
}
});
// Export intelligence data
hooksCmd.command('export')
.description('Export intelligence data for backup')
.option('-o, --output <file>', 'Output file path', 'ruvector-export.json')
.option('--include-all', 'Include all data (patterns, memories, trajectories)')
.action((opts) => {
const intel = new Intelligence();
const exportData = {
version: '1.0',
exported_at: new Date().toISOString(),
patterns: intel.data?.patterns || {},
memories: opts.includeAll ? (intel.data?.memories || []) : [],
trajectories: opts.includeAll ? (intel.data?.trajectories || []) : [],
errors: intel.data?.errors || {},
stats: intel.stats()
};
const outputPath = path.resolve(opts.output);
fs.writeFileSync(outputPath, JSON.stringify(exportData, null, 2));
console.log(chalk.green(`✅ Exported intelligence data to ${outputPath}`));
console.log(chalk.dim(` ${Object.keys(exportData.patterns).length} patterns`));
console.log(chalk.dim(` ${exportData.memories.length} memories`));
console.log(chalk.dim(` ${exportData.trajectories.length} trajectories`));
});
// Import intelligence data
hooksCmd.command('import')
.description('Import intelligence data from backup')
.argument('<file>', 'Import file path')
.option('--merge', 'Merge with existing data (default: replace)')
.option('--dry-run', 'Show what would be imported without making changes')
.action((file, opts) => {
const importPath = path.resolve(file);
if (!fs.existsSync(importPath)) {
console.error(chalk.red(`❌ File not found: ${importPath}`));
process.exit(1);
}
try {
const importData = JSON.parse(fs.readFileSync(importPath, 'utf-8'));
if (!importData.version) {
console.error(chalk.red('❌ Invalid export file (missing version)'));
process.exit(1);
}
console.log(chalk.cyan(`📦 Import file: ${file}`));
console.log(chalk.dim(` Version: ${importData.version}`));
console.log(chalk.dim(` Exported: ${importData.exported_at}`));
console.log(chalk.dim(` Patterns: ${Object.keys(importData.patterns || {}).length}`));
console.log(chalk.dim(` Memories: ${(importData.memories || []).length}`));
console.log(chalk.dim(` Trajectories: ${(importData.trajectories || []).length}`));
if (opts.dryRun) {
console.log(chalk.yellow('\n⚠ Dry run - no changes made'));
return;
}
const intel = new Intelligence();
if (opts.merge) {
// Merge patterns
Object.assign(intel.data.patterns, importData.patterns || {});
// Merge memories (deduplicate by content)
const existingContent = new Set((intel.data.memories || []).map(m => m.content));
(importData.memories || []).forEach(m => {
if (!existingContent.has(m.content)) {
intel.data.memories.push(m);
}
});
// Merge trajectories
intel.data.trajectories = (intel.data.trajectories || []).concat(importData.trajectories || []);
// Merge errors
Object.assign(intel.data.errors, importData.errors || {});
console.log(chalk.green('✅ Merged intelligence data'));
} else {
intel.data.patterns = importData.patterns || {};
intel.data.memories = importData.memories || [];
intel.data.trajectories = importData.trajectories || [];
intel.data.errors = importData.errors || {};
console.log(chalk.green('✅ Replaced intelligence data'));
}
intel.save();
console.log(chalk.dim(' Data saved to .ruvector/intelligence.json'));
} catch (e) {
console.error(chalk.red(`❌ Failed to import: ${e.message}`));
process.exit(1);
}
});
program.parse();

View file

@ -1,6 +1,6 @@
{
"name": "ruvector",
"version": "0.1.45",
"version": "0.1.46",
"description": "High-performance vector database for Node.js with automatic native/WASM fallback",
"main": "dist/index.js",
"types": "dist/index.d.ts",