feat(8.0): brain.fillSubtypes migration helper + pre-RC1 gap closure

- brain.fillSubtypes(rules): idempotent subtype back-fill for pre-8.0 data.
  One rule per NounType/VerbType (literal default or per-entry function);
  fills only entries still missing a subtype through the public update()/
  updateRelation() paths; returns { scanned, filled, skipped, errors, byType }.
  Full unit suite in tests/unit/brainy/fill-subtypes.test.ts.
- Fix getNouns/getVerbs pagination hasMore (peek one past the window) —
  was permanently false, silently truncating every multi-page walk.
- find({ near }) without near.id now throws a teaching error instead of an
  opaque storage sharding failure; CLI --threshold without --near applies a
  plain score floor.
- CLI init/close audit: every one-shot command init()s, close()s, and exits
  explicitly; delete the unmaintained interactive REPL; replace the cloud-era
  storage subcommands with status/batch-delete; new types/validate commands.
- requireSubtype JSDoc now documents the 8.0 default-on contract; audit()
  recommendation points at fillSubtypes.
- Docs: data-storage-architecture rewritten to the real 8.0 on-disk layout;
  README storage section reflects filesystem+memory and snapshots; eli5 and
  SEMANTIC_VFS /as-of/ semantics corrected; internal tracker IDs and
  .strategy references scrubbed from published files.
This commit is contained in:
David Snelling 2026-06-11 10:42:34 -07:00
parent 9b0f4acd5b
commit c44678390e
30 changed files with 1517 additions and 3226 deletions

View file

@ -132,6 +132,7 @@ export const coreCommands = {
const spinner = ora('Adding to neural database...').start()
const brain = getBrainy()
await brain.init()
let metadata: any = {}
if (options.metadata) {
@ -204,6 +205,12 @@ export const coreCommands = {
} else {
formatOutput({ id: result, metadata, confidence: addParams.confidence, weight: addParams.weight }, options)
}
// close() releases the writer lock and indexes, but global timers
// (UnifiedCache bookkeeping, PathResolver stats) keep the event loop
// alive. CLI commands are one-shot — exit explicitly.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Failed to add data')
console.error(chalk.red('Failed to add data:', error.message))
@ -280,6 +287,7 @@ export const coreCommands = {
const spinner = ora('Searching with Triple Intelligence™...').start()
const brain = getBrainy()
await brain.init()
// Build comprehensive search params
const searchParams: any = {
@ -292,11 +300,6 @@ export const coreCommands = {
searchParams.offset = parseInt(options.offset)
}
// Vector Intelligence - similarity threshold
if (options.threshold) {
searchParams.near = { threshold: parseFloat(options.threshold) }
}
// Metadata Intelligence - type filtering
if (options.type) {
const types = options.type.split(',').map(t => t.trim())
@ -314,7 +317,9 @@ export const coreCommands = {
}
}
// Vector Intelligence - proximity search
// Vector Intelligence - proximity search around an anchor entity.
// `near` requires an id; a bare --threshold (no --near) is applied as a
// plain score floor on the fused results after find() returns.
if (options.near) {
searchParams.near = {
id: options.near,
@ -371,7 +376,14 @@ export const coreCommands = {
}
}
const results = await brain.find(searchParams)
let results = await brain.find(searchParams)
// Without --near there is no proximity anchor; apply --threshold as a
// minimum-score filter on the fused results instead.
if (!options.near && options.threshold) {
const minScore = parseFloat(options.threshold)
results = results.filter((r) => r.score === undefined || r.score >= minScore)
}
spinner.succeed(`Found ${results.length} results`)
@ -457,6 +469,10 @@ export const coreCommands = {
} else {
formatOutput(results, options)
}
// One-shot command — see add() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Search failed')
console.error(chalk.red('Search failed:', error.message))
@ -496,6 +512,7 @@ export const coreCommands = {
const spinner = ora('Fetching item...').start()
const brain = getBrainy()
await brain.init()
// Try to get the item
const item = await brain.get(id)
@ -529,6 +546,10 @@ export const coreCommands = {
} else {
formatOutput(item, options)
}
// One-shot command — see add() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Failed to get item')
console.error(chalk.red('Failed to get item:', error.message))
@ -589,6 +610,7 @@ export const coreCommands = {
const spinner = ora('Creating relationship...').start()
const brain = getBrainy()
await brain.init()
let metadata: any = {}
if (options.metadata) {
@ -627,6 +649,10 @@ export const coreCommands = {
} else {
formatOutput({ id: result, source, verb, target, metadata }, options)
}
// One-shot command — see add() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Failed to create relationship')
console.error(chalk.red('Failed to create relationship:', error.message))
@ -683,6 +709,7 @@ export const coreCommands = {
spinner = ora('Updating entity...').start()
const brain = getBrainy()
await brain.init()
// Get existing entity first
const existing = await brain.get(id)
@ -731,6 +758,10 @@ export const coreCommands = {
} else {
formatOutput({ id, updated: true }, options)
}
// One-shot command — see add() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Failed to update entity')
console.error(chalk.red('Update failed:', error.message))
@ -784,6 +815,7 @@ export const coreCommands = {
spinner = ora('Deleting entity...').start()
const brain = getBrainy()
await brain.init()
await brain.delete(id)
@ -794,6 +826,10 @@ export const coreCommands = {
} else {
formatOutput({ id, deleted: true }, options)
}
// One-shot command — see add() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Failed to delete entity')
console.error(chalk.red('Delete failed:', error.message))
@ -846,6 +882,7 @@ export const coreCommands = {
spinner = ora('Removing relationship...').start()
const brain = getBrainy()
await brain.init()
await brain.unrelate(id)
@ -856,6 +893,10 @@ export const coreCommands = {
} else {
formatOutput({ id, removed: true }, options)
}
// One-shot command — see add() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Failed to remove relationship')
console.error(chalk.red('Unrelate failed:', error.message))
@ -875,7 +916,9 @@ export const coreCommands = {
if (options.json) {
formatOutput(diag, options)
return
// One-shot command — see add() for why the explicit close + exit.
await brain.close()
process.exit(0)
}
console.log(chalk.bold('\nBrainy Diagnostics'))
@ -907,6 +950,10 @@ export const coreCommands = {
console.log(` Metadata: ${diag.indexes.metadata.type} (initialized: ${diag.indexes.metadata.initialized})`)
console.log(` Graph: ${diag.indexes.graph.type} (initialized: ${diag.indexes.graph.initialized}, wired: ${diag.indexes.graph.wiredToStorage})`)
console.log()
// One-shot command — see add() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
console.error(chalk.red('Diagnostics failed: ' + error.message))
process.exit(1)

View file

@ -152,6 +152,7 @@ export const importCommands = {
spinner = ora('Initializing import...').start()
const brain = getBrainy()
await brain.init()
// Handle different source types
let result: any
@ -401,6 +402,12 @@ export const importCommands = {
} else if (options.json) {
formatOutput(result, options)
}
// close() releases the writer lock and indexes, but global timers
// (UnifiedCache bookkeeping, PathResolver stats) keep the event loop
// alive. CLI commands are one-shot — exit explicitly.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Import failed')
console.error(chalk.red('Import failed:', error.message))
@ -472,9 +479,10 @@ export const importCommands = {
spinner = ora('Initializing VFS import...').start()
const brain = getBrainy()
await brain.init()
// Get VFS
const vfs = await brain.vfs
const vfs = brain.vfs
// Load DirectoryImporter
const { DirectoryImporter } = await import('../../vfs/importers/DirectoryImporter.js')
@ -536,6 +544,10 @@ export const importCommands = {
} else if (options.json) {
formatOutput(result, options)
}
// One-shot command — see import() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('VFS import failed')
console.error(chalk.red('VFS import failed:', error.message))

View file

@ -41,6 +41,7 @@ export const insightsCommands = {
try {
const brain = getBrainy()
await brain.init()
// Get insights from Brainy
const insights = await brain.insights()
@ -102,6 +103,12 @@ export const insightsCommands = {
} else {
formatOutput(insights, options)
}
// close() releases the writer lock and indexes, but global timers
// (UnifiedCache bookkeeping, PathResolver stats) keep the event loop
// alive. CLI commands are one-shot — exit explicitly.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Failed to get insights')
console.error(chalk.red('Insights failed:', error.message))
@ -120,6 +127,7 @@ export const insightsCommands = {
try {
const brain = getBrainy()
await brain.init()
// Get available fields from metadata index
const fields = await brain.getAvailableFields()
@ -166,6 +174,10 @@ export const insightsCommands = {
}))
formatOutput(fieldsWithStats, options)
}
// One-shot command — see insights() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Failed to get fields')
console.error(chalk.red('Fields analysis failed:', error.message))
@ -186,6 +198,7 @@ export const insightsCommands = {
if (!field) {
spinner = ora('Getting available fields...').start()
const brain = getBrainy()
await brain.init()
const availableFields = await brain.getAvailableFields()
spinner.stop()
@ -202,6 +215,7 @@ export const insightsCommands = {
spinner = ora(`Getting values for field: ${field}...`).start()
const brain = getBrainy()
await brain.init()
const values = await brain.getFieldValues(field)
const limit = options.limit ? parseInt(options.limit) : 100
@ -242,6 +256,10 @@ export const insightsCommands = {
} else {
formatOutput({ field, values, count: values.length }, options)
}
// One-shot command — see insights() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Failed to get field values')
console.error(chalk.red('Field values failed:', error.message))
@ -289,6 +307,7 @@ export const insightsCommands = {
spinner = ora('Analyzing optimal query plan...').start()
const brain = getBrainy()
await brain.init()
const plan = await brain.getOptimalQueryPlan(filters)
@ -328,6 +347,10 @@ export const insightsCommands = {
} else {
formatOutput({ filters, plan }, options)
}
// One-shot command — see insights() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Failed to generate query plan')
console.error(chalk.red('Query plan failed:', error.message))

View file

@ -1,7 +1,11 @@
/**
* 🧠 Neural Similarity API Commands
*
* CLI interface for semantic similarity, clustering, and neural operations
* @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'
@ -25,136 +29,13 @@ interface CommandArguments {
_: string[];
}
export const neuralCommand = {
command: 'neural [action]',
describe: '🧠 Neural similarity and clustering operations',
builder: (yargs: any) => {
return yargs
.positional('action', {
describe: 'Neural operation to perform',
type: 'string',
choices: ['similar', 'clusters', 'hierarchy', 'neighbors', 'path', 'outliers', 'visualize']
})
.option('id', {
describe: 'Item ID for similarity operations',
type: 'string',
alias: 'i'
})
.option('query', {
describe: 'Query text for similarity search',
type: 'string',
alias: 'q'
})
.option('threshold', {
describe: 'Similarity threshold (0-1)',
type: 'number',
default: 0.7,
alias: 't'
})
.option('format', {
describe: 'Output format',
type: 'string',
choices: ['json', 'table', 'tree', 'graph'],
default: 'table',
alias: 'f'
})
.option('output', {
describe: 'Output file path',
type: 'string',
alias: 'o'
})
.option('limit', {
describe: 'Maximum number of results',
type: 'number',
default: 10,
alias: 'l'
})
.option('algorithm', {
describe: 'Clustering algorithm',
type: 'string',
choices: ['hierarchical', 'kmeans', 'dbscan', 'auto'],
default: 'auto',
alias: 'a'
})
.option('dimensions', {
describe: 'Visualization dimensions (2 or 3)',
type: 'number',
choices: [2, 3],
default: 2,
alias: 'd'
})
.option('explain', {
describe: 'Include detailed explanations',
type: 'boolean',
default: false,
alias: 'e'
})
},
handler: async (argv: CommandArguments) => {
console.log(chalk.cyan('\n🧠 NEURAL SIMILARITY API'))
console.log(chalk.gray('━'.repeat(50)))
let brainyInstance: Brainy | null = null
// Initialize Brainy and Neural API
const brain = new Brainy()
const neural = brain.neural()
try {
const action = argv.action || await promptForAction()
switch (action) {
case 'similar':
await handleSimilarCommand(neural, argv)
break
case 'clusters':
await handleClustersCommand(neural, argv)
break
case 'hierarchy':
await handleHierarchyCommand(neural, argv)
break
case 'neighbors':
await handleNeighborsCommand(neural, argv)
break
case 'path':
console.log(chalk.yellow('\n⚠ Semantic path finding coming soon'))
console.log(chalk.dim('This feature requires implementing graph traversal algorithms'))
console.log(chalk.dim('Use "neighbors" and "hierarchy" commands to explore connections'))
break
case 'outliers':
await handleOutliersCommand(neural, argv)
break
case 'visualize':
await handleVisualizeCommand(neural, argv)
break
default:
console.log(chalk.red(`❌ Unknown action: ${action}`))
showHelp()
}
} catch (error) {
console.error(chalk.red('💥 Error:'), error instanceof Error ? error.message : error)
process.exit(1)
}
const getBrainy = (): Brainy => {
if (!brainyInstance) {
brainyInstance = new Brainy()
}
}
async function promptForAction(): Promise<string> {
const answer = await inquirer.prompt([{
type: 'list',
name: 'action',
message: 'Choose a neural operation:',
choices: [
{ name: '🔗 Calculate similarity between items', value: 'similar' },
{ name: '🎯 Find semantic clusters', value: 'clusters' },
{ name: '🌳 Show item hierarchy', value: 'hierarchy' },
{ name: '🕸️ Find semantic neighbors', value: 'neighbors' },
{ name: '🛣️ Find semantic path between items (coming soon)', value: 'path', disabled: true },
{ name: '🚨 Detect outliers', value: 'outliers' },
{ name: '📊 Generate visualization data', value: 'visualize' }
]
}])
return answer.action
return brainyInstance
}
async function handleSimilarCommand(neural: any, argv: CommandArguments): Promise<void> {
@ -436,63 +317,6 @@ async function handleNeighborsCommand(neural: any, argv: CommandArguments): Prom
}
}
async function handlePathCommand(neural: any, argv: CommandArguments): Promise<void> {
const spinner = ora('🛣️ Finding semantic path...').start()
try {
let fromId: string, toId: string
if (argv._ && argv._.length >= 3) {
fromId = argv._[1]
toId = argv._[2]
} else {
spinner.stop()
const answers = await inquirer.prompt([
{
type: 'input',
name: 'from',
message: 'From item ID:',
validate: (input: string) => input.length > 0
},
{
type: 'input',
name: 'to',
message: 'To item ID:',
validate: (input: string) => input.length > 0
}
])
fromId = answers.from
toId = answers.to
spinner.start()
}
const path = await neural.semanticPath(fromId, toId)
if (path.length === 0) {
spinner.warn('🚫 No semantic path found')
console.log(`No path found between ${chalk.cyan(fromId)} and ${chalk.cyan(toId)}`)
} else {
spinner.succeed(`✅ Found path with ${path.length} hops`)
console.log(`\n🛣 Semantic Path from ${chalk.cyan(fromId)} to ${chalk.cyan(toId)}:`)
console.log(`${chalk.cyan(fromId)} (start)`)
path.forEach((hop, index) => {
console.log(`${' '.repeat(index + 1)}${(hop.similarity * 100).toFixed(1)}%`)
console.log(`${' '.repeat(index + 1)}${hop.id} (hop ${hop.hop})`)
})
}
if (argv.output) {
await saveToFile(argv.output, path, argv.format!)
}
} catch (error) {
spinner.fail('💥 Failed to find path')
throw error
}
}
async function handleOutliersCommand(neural: any, argv: CommandArguments): Promise<void> {
const spinner = ora('🚨 Detecting semantic outliers...').start()
@ -592,32 +416,32 @@ function formatAsTable(data: any): string {
return JSON.stringify(data, null, 2)
}
function showHelp(): void {
console.log('\n🧠 Neural Similarity API Commands:')
console.log('')
console.log(' brainy neural similar <item1> <item2> Calculate similarity')
console.log(' brainy neural clusters Find semantic clusters')
console.log(' brainy neural hierarchy <id> Show item hierarchy')
console.log(' brainy neural neighbors <id> Find semantic neighbors')
console.log(' brainy neural path <from> <to> Find semantic path')
console.log(' brainy neural outliers Detect outliers')
console.log(' brainy neural visualize Generate visualization data')
console.log('')
console.log('Options:')
console.log(' --threshold, -t Similarity threshold (0-1)')
console.log(' --format, -f Output format (json|table|tree|graph)')
console.log(' --output, -o Save to file')
console.log(' --limit, -l Maximum results')
console.log(' --explain, -e Include explanations')
console.log('')
/**
* @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<Brainy['neural']>) => Promise<void>): Promise<void> {
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) {
const brain = new Brainy()
const neural = brain.neural()
// Build argv-style object for handler
const argv: CommandArguments = {
_: ['neural', 'similar', a || '', b || ''].filter(x => x),
@ -627,13 +451,10 @@ export const neuralCommands = {
...options
}
await handleSimilarCommand(neural, argv)
await runNeuralCommand((neural) => handleSimilarCommand(neural, argv))
},
async cluster(options?: any) {
const brain = new Brainy()
const neural = brain.neural()
const argv: CommandArguments = {
_: ['neural', 'cluster'],
algorithm: options?.algorithm || 'hierarchical',
@ -643,26 +464,20 @@ export const neuralCommands = {
...options
}
await handleClustersCommand(neural, argv)
await runNeuralCommand((neural) => handleClustersCommand(neural, argv))
},
async hierarchy(id?: string, options?: any) {
const brain = new Brainy()
const neural = brain.neural()
const argv: CommandArguments = {
_: ['neural', 'hierarchy', id || ''].filter(x => x),
id,
...options
}
await handleHierarchyCommand(neural, argv)
await runNeuralCommand((neural) => handleHierarchyCommand(neural, argv))
},
async related(id?: string, options?: any) {
const brain = new Brainy()
const neural = brain.neural()
const argv: CommandArguments = {
_: ['neural', 'related', id || ''].filter(x => x),
id,
@ -671,13 +486,10 @@ export const neuralCommands = {
...options
}
await handleNeighborsCommand(neural, argv)
await runNeuralCommand((neural) => handleNeighborsCommand(neural, argv))
},
async outliers(options?: any) {
const brain = new Brainy()
const neural = brain.neural()
const argv: CommandArguments = {
_: ['neural', 'outliers'],
threshold: options?.threshold ? parseFloat(options.threshold) : 0.3,
@ -685,13 +497,10 @@ export const neuralCommands = {
...options
}
await handleOutliersCommand(neural, argv)
await runNeuralCommand((neural) => handleOutliersCommand(neural, argv))
},
async visualize(options?: any) {
const brain = new Brainy()
const neural = brain.neural()
const argv: CommandArguments = {
_: ['neural', 'visualize'],
format: options?.format || 'json',
@ -701,8 +510,6 @@ export const neuralCommands = {
...options
}
await handleVisualizeCommand(neural, argv)
await runNeuralCommand((neural) => handleVisualizeCommand(neural, argv))
}
}
export default neuralCommand
}

View file

@ -62,6 +62,7 @@ export const nlpCommands = {
spinner = ora('Extracting entities with neural NLP...').start()
const brain = getBrainy()
await brain.init()
// Extract entities using Brainy's neural entity extractor
const entities = await brain.extract(text)
@ -105,6 +106,12 @@ export const nlpCommands = {
} else {
formatOutput(entities, options)
}
// close() releases the writer lock and indexes, but global timers
// (UnifiedCache bookkeeping, PathResolver stats) keep the event loop
// alive. CLI commands are one-shot — exit explicitly.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Entity extraction failed')
console.error(chalk.red('Extraction failed:', error.message))
@ -147,6 +154,7 @@ export const nlpCommands = {
spinner = ora('Extracting concepts with neural analysis...').start()
const brain = getBrainy()
await brain.init()
const confidence = options.threshold ? parseFloat(options.threshold) : 0.5
const concepts = await brain.extractConcepts(text, { confidence })
@ -171,6 +179,10 @@ export const nlpCommands = {
} else {
formatOutput(concepts, options)
}
// One-shot command — see extract() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Concept extraction failed')
console.error(chalk.red('Extraction failed:', error.message))
@ -201,6 +213,7 @@ export const nlpCommands = {
spinner = ora('Analyzing text with neural NLP...').start()
const brain = getBrainy()
await brain.init()
// Run both entity extraction and concept extraction
const [entities, concepts] = await Promise.all([
@ -265,6 +278,10 @@ export const nlpCommands = {
concepts
}, options)
}
// One-shot command — see extract() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
if (spinner) spinner.fail('Analysis failed')
console.error(chalk.red('Analysis failed:', error.message))

File diff suppressed because it is too large Load diff

View file

@ -22,8 +22,7 @@ interface StatsOptions extends UtilityOptions {
}
interface CleanOptions extends UtilityOptions {
removeOrphans?: boolean
rebuildIndex?: boolean
force?: boolean
}
interface BenchmarkOptions extends UtilityOptions {
@ -63,6 +62,7 @@ export const utilityCommands = {
try {
const brain = getBrainy()
await brain.init()
const nounCount = await brain.getNounCount()
const verbCount = await brain.getVerbCount()
const memUsage = process.memoryUsage()
@ -77,7 +77,11 @@ export const utilityCommands = {
if (options.json) {
formatOutput(stats, options)
return
// close() releases the writer lock and indexes, but global timers
// (UnifiedCache bookkeeping, PathResolver stats) keep the event loop
// alive. CLI commands are one-shot — exit explicitly.
await brain.close()
process.exit(0)
}
console.log(chalk.cyan('\n📊 Database Statistics\n'))
@ -112,7 +116,10 @@ export const utilityCommands = {
)
console.log(memTable.toString())
// One-shot command — see the --json branch for why the explicit exit.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Failed to gather statistics')
console.error(chalk.red(error.message))
@ -121,30 +128,33 @@ export const utilityCommands = {
},
/**
* Clean and optimize database
* Clear the database (all entities, relationships, and indexes).
* Destructive asks for confirmation unless --force is passed.
*/
async clean(options: CleanOptions) {
const spinner = ora('Cleaning database...').start()
let spinner: ReturnType<typeof ora> | null = null
try {
const brain = getBrainy()
// Destructive operation — confirm first (skipped with --force).
if (!options.force) {
const inquirer = (await import('inquirer')).default
const { confirm } = await inquirer.prompt([{
type: 'confirm',
name: 'confirm',
message: chalk.yellow('⚠️ Permanently delete ALL data (entities, relationships, indexes)?'),
default: false
}])
// For now, only support full clear
// removeOrphans and rebuildIndex would require new Brainy APIs
if (options.removeOrphans || options.rebuildIndex) {
spinner.warn('Advanced cleanup options not yet implemented')
console.log(chalk.yellow('\n⚠ Advanced cleanup features coming soon:'))
console.log(chalk.dim(' • --remove-orphans: Remove disconnected items'))
console.log(chalk.dim(' • --rebuild-index: Rebuild vector index'))
console.log(chalk.dim('\nUse "brainy clean" without options to clear the database'))
return
if (!confirm) {
console.log(chalk.yellow('Clean cancelled'))
process.exit(0)
}
}
// Show warning before clearing
console.log(chalk.yellow('\n⚠ WARNING: This will permanently delete ALL data!'))
const brain = getBrainy()
// Clear all data (entities, relationships, and every index)
spinner.text = 'Clearing all data...'
spinner = ora('Clearing all data...').start()
await brain.init()
await brain.clear()
@ -163,7 +173,7 @@ export const utilityCommands = {
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Cleanup failed')
if (spinner) spinner.fail('Cleanup failed')
console.error(chalk.red(error.message))
process.exit(1)
}
@ -185,7 +195,8 @@ export const utilityCommands = {
try {
const brain = getBrainy()
await brain.init()
// Benchmark different operations
const benchmarks = [
{ name: 'add', enabled: operations === 'all' || operations.includes('add') },
@ -205,7 +216,8 @@ export const utilityCommands = {
switch (bench.name) {
case 'add':
await brain.add({ data: `Test item ${i}`, type: NounType.Thing, metadata: { benchmark: true } })
// 8.0 requires a subtype on every write by default.
await brain.add({ data: `Test item ${i}`, type: NounType.Thing, subtype: 'benchmark', metadata: { benchmark: true } })
break
case 'search':
await brain.find({ query: 'test', limit: 10 })
@ -284,7 +296,10 @@ export const utilityCommands = {
} else {
formatOutput(results, options)
}
// One-shot command — see stats() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
console.error(chalk.red('Benchmark failed:'), error.message)
process.exit(1)

View file

@ -70,6 +70,12 @@ export const vfsCommands = {
} else {
formatOutput({ path, content: buffer.toString(), size: buffer.length }, options)
}
// close() releases the writer lock and indexes, but global timers
// (UnifiedCache bookkeeping, PathResolver stats) keep the event loop
// alive. CLI commands are one-shot — exit explicitly.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Failed to read file')
console.error(chalk.red(error.message))
@ -109,6 +115,10 @@ export const vfsCommands = {
} else {
formatOutput({ path, size: Buffer.byteLength(data) }, options)
}
// One-shot command — see read() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Failed to write file')
console.error(chalk.red(error.message))
@ -132,7 +142,9 @@ export const vfsCommands = {
if (!options.json) {
if (!Array.isArray(entries) || entries.length === 0) {
console.log(chalk.yellow('Directory is empty'))
return
// One-shot command — see read() for why the explicit close + exit.
await brain.close()
process.exit(0)
}
if (options.long) {
@ -171,6 +183,10 @@ export const vfsCommands = {
} else {
formatOutput(entries, options)
}
// One-shot command — see read() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Failed to list directory')
console.error(chalk.red(error.message))
@ -202,6 +218,10 @@ export const vfsCommands = {
} else {
formatOutput(stats, options)
}
// One-shot command — see read() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Failed to get stats')
console.error(chalk.red(error.message))
@ -227,6 +247,10 @@ export const vfsCommands = {
} else {
formatOutput({ path, created: true }, options)
}
// One-shot command — see read() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Failed to create directory')
console.error(chalk.red(error.message))
@ -258,12 +282,16 @@ export const vfsCommands = {
} else {
formatOutput({ path, removed: true }, options)
}
// One-shot command — see read() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Failed to remove')
console.error(chalk.red(error.message))
if (!options.force) {
process.exit(1)
}
// --force tolerates the failure but the process is still one-shot:
// exit cleanly (0) instead of falling off the event loop.
process.exit(options.force ? 0 : 1)
}
},
@ -303,6 +331,10 @@ export const vfsCommands = {
} else {
formatOutput(results, options)
}
// One-shot command — see read() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Search failed')
console.error(chalk.red(error.message))
@ -343,6 +375,10 @@ export const vfsCommands = {
} else {
formatOutput(results, options)
}
// One-shot command — see read() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Failed to find similar files')
console.error(chalk.red(error.message))
@ -371,6 +407,10 @@ export const vfsCommands = {
} else {
formatOutput(tree, options)
}
// One-shot command — see read() for why the explicit close + exit.
await brain.close()
process.exit(0)
} catch (error: any) {
spinner.fail('Failed to build tree')
console.error(chalk.red(error.message))