/** * @module cli/commands/neural * @description Neural CLI commands: semantic similarity, clustering, * hierarchy, neighbors, outlier detection, and visualization data export. * Registered in `src/cli/index.ts` as `similar` / `cluster` / `related` / * `hierarchy` / `outliers` / `visualize`. Each command is one-shot: it * initializes the shared Brainy instance, runs the neural operation, then * closes the store and exits explicitly. */ import inquirer from 'inquirer' import chalk from 'chalk' import ora from 'ora' import fs from 'node:fs' import path from 'node:path' import { Brainy } from '../../brainy.js' interface CommandArguments { action?: string; id?: string; query?: string; threshold?: number; format?: string; output?: string; limit?: number; algorithm?: string; dimensions?: number; explain?: boolean; _: string[]; } let brainyInstance: Brainy | null = null const getBrainy = (): Brainy => { if (!brainyInstance) { brainyInstance = new Brainy() } return brainyInstance } async function handleSimilarCommand(neural: any, argv: CommandArguments): Promise { const spinner = ora('🧠 Calculating semantic similarity...').start() try { let itemA: string, itemB: string if (argv.id && argv.query) { itemA = argv.id itemB = argv.query } else if (argv._ && argv._.length >= 3) { itemA = argv._[1] itemB = argv._[2] } else { spinner.stop() const answers = await inquirer.prompt([ { type: 'input', name: 'itemA', message: 'First item (ID or text):', validate: (input: string) => input.length > 0 }, { type: 'input', name: 'itemB', message: 'Second item (ID or text):', validate: (input: string) => input.length > 0 } ]) itemA = answers.itemA itemB = answers.itemB spinner.start() } const result = await neural.similar(itemA, itemB, { explain: argv.explain, includeBreakdown: argv.explain }) spinner.succeed('βœ… Similarity calculated') if (typeof result === 'number') { console.log(`\nπŸ”— Similarity: ${chalk.cyan((result * 100).toFixed(1))}%`) } else { console.log(`\nπŸ”— Similarity: ${chalk.cyan((result.score * 100).toFixed(1))}%`) if (result.explanation) { console.log(`πŸ’­ Explanation: ${result.explanation}`) } if (result.breakdown) { console.log('\nπŸ“Š Breakdown:') console.log(` Semantic: ${chalk.yellow((result.breakdown.semantic! * 100).toFixed(1))}%`) if (result.breakdown.taxonomic !== undefined) { console.log(` Taxonomic: ${chalk.yellow((result.breakdown.taxonomic * 100).toFixed(1))}%`) } if (result.breakdown.contextual !== undefined) { console.log(` Contextual: ${chalk.yellow((result.breakdown.contextual * 100).toFixed(1))}%`) } } if (result.hierarchy) { console.log(`\n🌳 Hierarchy: ${result.hierarchy.sharedParent ? `Shared parent at distance ${result.hierarchy.distance}` : 'No shared parent found'}`) } } if (argv.output) { await saveToFile(argv.output, result, argv.format!) } } catch (error) { spinner.fail('πŸ’₯ Failed to calculate similarity') throw error } } async function handleClustersCommand(neural: any, argv: CommandArguments): Promise { let spinner: any = null try { let options: any = { algorithm: argv.algorithm as any, threshold: argv.threshold, maxClusters: argv.limit } // Interactive mode if no algorithm specified or using defaults if (!argv.algorithm || argv.algorithm === 'hierarchical') { const answers = await inquirer.prompt([ { type: 'list', name: 'algorithm', message: 'Choose clustering algorithm:', default: argv.algorithm || 'hierarchical', choices: [ { name: '🌳 Hierarchical (Tree-based, best for natural grouping)', value: 'hierarchical' }, { name: 'πŸ“Š K-Means (Fixed number of clusters)', value: 'kmeans' }, { name: '🎯 DBSCAN (Density-based, finds arbitrary shapes)', value: 'dbscan' } ] }, { type: 'number', name: 'maxClusters', message: 'Maximum number of clusters:', default: argv.limit || 5, when: (answers: any) => answers.algorithm === 'kmeans' }, { type: 'number', name: 'threshold', message: 'Similarity threshold (0-1):', default: argv.threshold || 0.7, validate: (input: number) => (input >= 0 && input <= 1) || 'Must be between 0 and 1' } ]) options = { algorithm: answers.algorithm, threshold: answers.threshold, maxClusters: answers.maxClusters || options.maxClusters } } const spinner = ora('🎯 Finding semantic clusters...').start() const clusters = await neural.clusters(argv.query || options) spinner.succeed(`βœ… Found ${clusters.length} clusters`) if (argv.format === 'json') { console.log(JSON.stringify(clusters, null, 2)) } else { console.log(`\n🎯 ${chalk.cyan(clusters.length)} Semantic Clusters:\n`) clusters.forEach((cluster, index) => { console.log(`${chalk.yellow(`Cluster ${index + 1}:`)} ${cluster.label || cluster.id}`) console.log(` πŸ“Š Confidence: ${chalk.green((cluster.confidence * 100).toFixed(1))}%`) console.log(` πŸ‘₯ Members: ${cluster.members.length}`) if (cluster.members.length <= 5) { cluster.members.forEach(member => { console.log(` β€’ ${member}`) }) } else { cluster.members.slice(0, 3).forEach(member => { console.log(` β€’ ${member}`) }) console.log(` ... and ${cluster.members.length - 3} more`) } console.log() }) } if (argv.output) { await saveToFile(argv.output, clusters, argv.format!) } } catch (error) { if (spinner) spinner.fail('πŸ’₯ Failed to find clusters') throw error } } async function handleHierarchyCommand(neural: any, argv: CommandArguments): Promise { const spinner = ora('🌳 Building semantic hierarchy...').start() try { const id = argv.id || argv._[1] if (!id) { spinner.stop() const answer = await inquirer.prompt([{ type: 'input', name: 'id', message: 'Enter item ID:', validate: (input: string) => input.length > 0 }]) spinner.start() const hierarchy = await neural.hierarchy(answer.id) displayHierarchy(hierarchy) } else { const hierarchy = await neural.hierarchy(id) spinner.succeed('βœ… Hierarchy built') displayHierarchy(hierarchy) } if (argv.output) { const hierarchy = await neural.hierarchy(id || argv._[1]) await saveToFile(argv.output, hierarchy, argv.format!) } } catch (error) { spinner.fail('πŸ’₯ Failed to build hierarchy') throw error } } function displayHierarchy(hierarchy: any): void { console.log(`\n🌳 Semantic Hierarchy for ${chalk.cyan(hierarchy.self.id)}:`) if (hierarchy.root) { console.log(`πŸ” Root: ${hierarchy.root.id} (${(hierarchy.root.similarity * 100).toFixed(1)}%)`) } if (hierarchy.grandparent) { console.log(`πŸ‘΄ Grandparent: ${hierarchy.grandparent.id} (${(hierarchy.grandparent.similarity * 100).toFixed(1)}%)`) } if (hierarchy.parent) { console.log(`πŸ‘¨ Parent: ${hierarchy.parent.id} (${(hierarchy.parent.similarity * 100).toFixed(1)}%)`) } console.log(`🎯 ${chalk.bold('Self:')} ${hierarchy.self.id}`) if (hierarchy.siblings && hierarchy.siblings.length > 0) { console.log(`πŸ‘₯ Siblings: ${hierarchy.siblings.length}`) hierarchy.siblings.forEach((sibling: any) => { console.log(` β€’ ${sibling.id} (${(sibling.similarity * 100).toFixed(1)}%)`) }) } if (hierarchy.children && hierarchy.children.length > 0) { console.log(`πŸ‘Ά Children: ${hierarchy.children.length}`) hierarchy.children.forEach((child: any) => { console.log(` β€’ ${child.id} (${(child.similarity * 100).toFixed(1)}%)`) }) } } async function handleNeighborsCommand(neural: any, argv: CommandArguments): Promise { const spinner = ora('πŸ•ΈοΈ Finding semantic neighbors...').start() try { const id = argv.id || argv._[1] if (!id) { spinner.stop() const answer = await inquirer.prompt([{ type: 'input', name: 'id', message: 'Enter item ID:', validate: (input: string) => input.length > 0 }]) spinner.start() } const targetId = id || (await inquirer.prompt([{ type: 'input', name: 'id', message: 'Enter item ID:', validate: (input: string) => input.length > 0 }])).id const graph = await neural.neighbors(targetId, { limit: argv.limit, includeEdges: true }) spinner.succeed(`βœ… Found ${graph.neighbors.length} neighbors`) console.log(`\nπŸ•ΈοΈ Neighbors of ${chalk.cyan(graph.center)}:`) graph.neighbors.forEach((neighbor, index) => { console.log(`${index + 1}. ${neighbor.id} (${(neighbor.similarity * 100).toFixed(1)}%)`) if (neighbor.type) { console.log(` Type: ${neighbor.type}`) } if (neighbor.connections) { console.log(` Connections: ${neighbor.connections}`) } }) if (graph.edges && graph.edges.length > 0) { console.log(`\nπŸ”— ${graph.edges.length} semantic connections found`) } if (argv.output) { await saveToFile(argv.output, graph, argv.format!) } } catch (error) { spinner.fail('πŸ’₯ Failed to find neighbors') throw error } } async function handleOutliersCommand(neural: any, argv: CommandArguments): Promise { const spinner = ora('🚨 Detecting semantic outliers...').start() try { const outliers = await neural.outliers(argv.threshold) spinner.succeed(`βœ… Found ${outliers.length} outliers`) if (outliers.length === 0) { console.log('\nπŸŽ‰ No outliers detected - all items are well connected!') } else { console.log(`\n🚨 ${chalk.red(outliers.length)} Semantic Outliers:`) outliers.forEach((outlier, index) => { console.log(`${index + 1}. ${outlier}`) }) console.log(`\nπŸ’‘ These items have similarity < ${argv.threshold} to their nearest neighbors`) } if (argv.output) { await saveToFile(argv.output, outliers, argv.format!) } } catch (error) { spinner.fail('πŸ’₯ Failed to detect outliers') throw error } } async function handleVisualizeCommand(neural: any, argv: CommandArguments): Promise { const spinner = ora('πŸ“Š Generating visualization data...').start() try { const vizData = await neural.visualize({ dimensions: argv.dimensions as 2 | 3, maxNodes: argv.limit }) spinner.succeed('βœ… Visualization data generated') console.log(`\nπŸ“Š Visualization Data (${vizData.format} layout):`) console.log(`πŸ“ Nodes: ${vizData.nodes.length}`) console.log(`πŸ”— Edges: ${vizData.edges.length}`) console.log(`🎯 Clusters: ${vizData.clusters?.length || 0}`) console.log(`πŸ“ Dimensions: ${vizData.layout?.dimensions}D`) if (argv.format === 'json') { console.log('\nData:') console.log(JSON.stringify(vizData, null, 2)) } else { console.log('\n🎨 Style Settings:') console.log(` Node Colors: ${vizData.style?.nodeColors}`) console.log(` Edge Width: ${vizData.style?.edgeWidth}`) console.log(` Labels: ${vizData.style?.labels}`) } if (argv.output) { await saveToFile(argv.output, vizData, 'json') console.log(`\nπŸ’Ύ Visualization data saved to: ${chalk.green(argv.output)}`) } else { console.log(`\nπŸ’‘ Use --output to save visualization data for external tools`) } } catch (error) { spinner.fail('πŸ’₯ Failed to generate visualization') throw error } } async function saveToFile(filepath: string, data: any, format: string): Promise { const dir = path.dirname(filepath) if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) } let output: string switch (format) { case 'json': output = JSON.stringify(data, null, 2) break case 'table': output = formatAsTable(data) break default: output = JSON.stringify(data, null, 2) } fs.writeFileSync(filepath, output, 'utf8') console.log(`πŸ’Ύ Saved to: ${chalk.green(filepath)}`) } function formatAsTable(data: any): string { // Simple table formatting - could be enhanced with a table library if (Array.isArray(data)) { return data.map((item, index) => `${index + 1}. ${JSON.stringify(item)}`).join('\n') } return JSON.stringify(data, null, 2) } /** * @description Run a neural CLI handler with the one-shot lifecycle every * Brainy command follows: init β†’ work β†’ `close()` β†’ explicit `process.exit`. * close() releases the writer lock and indexes, but global timers * (UnifiedCache bookkeeping, PathResolver stats) keep the event loop alive, * so one-shot commands must exit explicitly. * @param work - The handler body, given the initialized neural API. */ async function runNeuralCommand(work: (neural: ReturnType) => Promise): Promise { try { const brain = getBrainy() await brain.init() await work(brain.neural()) await brain.close() process.exit(0) } catch (error) { console.error(chalk.red('πŸ’₯ Error:'), error instanceof Error ? error.message : error) process.exit(1) } } // Commander-compatible wrappers export const neuralCommands = { async similar(a?: string, b?: string, options?: any) { // Build argv-style object for handler const argv: CommandArguments = { _: ['neural', 'similar', a || '', b || ''].filter(x => x), id: a, query: b, explain: options?.explain, ...options } await runNeuralCommand((neural) => handleSimilarCommand(neural, argv)) }, async cluster(options?: any) { const argv: CommandArguments = { _: ['neural', 'cluster'], algorithm: options?.algorithm || 'hierarchical', threshold: options?.threshold ? parseFloat(options.threshold) : 0.7, limit: options?.maxClusters ? parseInt(options.maxClusters) : 10, query: options?.near, ...options } await runNeuralCommand((neural) => handleClustersCommand(neural, argv)) }, async hierarchy(id?: string, options?: any) { const argv: CommandArguments = { _: ['neural', 'hierarchy', id || ''].filter(x => x), id, ...options } await runNeuralCommand((neural) => handleHierarchyCommand(neural, argv)) }, async related(id?: string, options?: any) { const argv: CommandArguments = { _: ['neural', 'related', id || ''].filter(x => x), id, limit: options?.limit ? parseInt(options.limit) : 10, threshold: options?.radius ? parseFloat(options.radius) : 0.3, ...options } await runNeuralCommand((neural) => handleNeighborsCommand(neural, argv)) }, async outliers(options?: any) { const argv: CommandArguments = { _: ['neural', 'outliers'], threshold: options?.threshold ? parseFloat(options.threshold) : 0.3, explain: options?.explain, ...options } await runNeuralCommand((neural) => handleOutliersCommand(neural, argv)) }, async visualize(options?: any) { const argv: CommandArguments = { _: ['neural', 'visualize'], format: options?.format || 'json', dimensions: options?.dimensions ? parseInt(options.dimensions) : 2, limit: options?.maxNodes ? parseInt(options.maxNodes) : 500, output: options?.output, ...options } await runNeuralCommand((neural) => handleVisualizeCommand(neural, argv)) } }