brainy/src/cli/commands/core.ts
David Snelling c44678390e 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.
2026-06-11 10:53:55 -07:00

962 lines
No EOL
29 KiB
TypeScript

/**
* @module cli/commands/core
* @description Essential database CLI commands: add, search, get, update,
* delete, relate, unrelate, and diagnostics. Database export lives under
* `brainy snapshot` (aliased as `brainy export`) — a `db.persist()` snapshot
* is the full-fidelity export format.
*/
import chalk from 'chalk'
import ora from 'ora'
import inquirer from 'inquirer'
import { Brainy } from '../../brainy.js'
import { BrainyTypes, NounType, VerbType } from '../../index.js'
interface CoreOptions {
verbose?: boolean
json?: boolean
pretty?: boolean
}
interface AddOptions extends CoreOptions {
id?: string
metadata?: string
type?: string
/**
* Sub-classification within the noun type. Defaults to `'cli-add'` when not
* supplied so the CLI works against strict-mode brains; production-style
* ingestion should pass `--subtype` explicitly to use the consumer's
* registered vocabulary. Added 7.30.1.
*/
subtype?: string
confidence?: string
weight?: string
}
interface SearchOptions extends CoreOptions {
limit?: string
offset?: string
threshold?: string
type?: string
where?: string
near?: string
connectedTo?: string
connectedFrom?: string
via?: string
explain?: boolean
includeRelations?: boolean
includeVfs?: boolean
fusion?: string
vectorWeight?: string
graphWeight?: string
fieldWeight?: string
}
interface GetOptions extends CoreOptions {
withConnections?: boolean
}
interface RelateOptions extends CoreOptions {
weight?: string
metadata?: string
/**
* Sub-classification within the verb type. Defaults to `'cli-relate'` when
* not supplied so the CLI works against strict-mode brains; production-style
* ingestion should pass `--subtype` explicitly. Added 7.30.1.
*/
subtype?: string
}
let brainyInstance: Brainy | null = null
const getBrainy = (): Brainy => {
if (!brainyInstance) {
brainyInstance = new Brainy()
}
return brainyInstance
}
const formatOutput = (data: any, options: CoreOptions): void => {
if (options.json) {
console.log(options.pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data))
}
}
export const coreCommands = {
/**
* Add data to the neural database
*/
async add(text: string | undefined, options: AddOptions) {
let spinner: any = null
try {
// Interactive mode if no text provided
if (!text) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'content',
message: 'Enter content:',
validate: (input: string) => input.trim().length > 0 || 'Content cannot be empty'
},
{
type: 'input',
name: 'nounType',
message: 'Noun type (optional, press Enter to auto-detect):',
default: ''
},
{
type: 'input',
name: 'metadata',
message: 'Metadata (JSON, optional):',
default: '',
validate: (input: string) => {
if (!input.trim()) return true
try {
JSON.parse(input)
return true
} catch {
return 'Invalid JSON format'
}
}
}
])
text = answers.content
if (answers.nounType) {
options.type = answers.nounType
}
if (answers.metadata) {
options.metadata = answers.metadata
}
}
const spinner = ora('Adding to neural database...').start()
const brain = getBrainy()
await brain.init()
let metadata: any = {}
if (options.metadata) {
try {
metadata = JSON.parse(options.metadata)
} catch {
spinner.fail('Invalid metadata JSON')
process.exit(1)
}
}
if (options.id) {
metadata.id = options.id
}
// Determine noun type
let nounType: NounType
if (options.type) {
// Validate provided type
if (!BrainyTypes.isValidNoun(options.type)) {
spinner.fail(`Invalid noun type: ${options.type}`)
console.log(chalk.dim('Run "brainy types --noun" to see valid types'))
process.exit(1)
}
nounType = options.type as NounType
} else {
// Default to Thing when no type specified
nounType = NounType.Thing
spinner.text = `No type specified, using default: ${nounType}`
}
// Add with explicit type. Subtype precedence: caller-supplied
// `--subtype <value>` → Brainy default `'cli-add'`. The default ensures
// CLI invocations against a strict-mode brain succeed without the user
// needing to know the vocabulary in advance — for production-style
// ingestion, pass `--subtype` explicitly (added 7.30.1).
const addParams: any = {
data: text,
type: nounType,
subtype: options.subtype ?? 'cli-add',
metadata
}
// Add confidence and weight if provided
if (options.confidence) {
addParams.confidence = parseFloat(options.confidence)
}
if (options.weight) {
addParams.weight = parseFloat(options.weight)
}
const result = await brain.add(addParams)
spinner.succeed('Added successfully')
if (!options.json) {
console.log(chalk.green(`✓ Added with ID: ${result}`))
if (options.type) {
console.log(chalk.dim(` Type: ${options.type}`))
}
if (options.confidence) {
console.log(chalk.dim(` Confidence: ${options.confidence}`))
}
if (options.weight) {
console.log(chalk.dim(` Weight: ${options.weight}`))
}
if (Object.keys(metadata).length > 0) {
console.log(chalk.dim(` Metadata: ${JSON.stringify(metadata)}`))
}
} 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))
process.exit(1)
}
},
/**
* Search the neural database with Triple Intelligence™
*/
async search(query: string | undefined, options: SearchOptions) {
let spinner: any = null
try {
// Interactive mode if no query provided
if (!query) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'query',
message: 'What are you looking for?',
validate: (input: string) => input.trim().length > 0 || 'Query cannot be empty'
},
{
type: 'number',
name: 'limit',
message: 'Number of results:',
default: 10
},
{
type: 'confirm',
name: 'useAdvanced',
message: 'Use advanced filters?',
default: false
}
])
query = answers.query
if (!options.limit) {
options.limit = answers.limit.toString()
}
// Advanced filters
if (answers.useAdvanced) {
const advancedAnswers = await inquirer.prompt([
{
type: 'input',
name: 'type',
message: 'Filter by type (optional):',
default: ''
},
{
type: 'input',
name: 'threshold',
message: 'Similarity threshold (0-1, default 0.7):',
default: '0.7',
validate: (input: string) => {
const num = parseFloat(input)
return (num >= 0 && num <= 1) || 'Must be between 0 and 1'
}
},
{
type: 'confirm',
name: 'explain',
message: 'Show scoring breakdown?',
default: false
}
])
if (advancedAnswers.type) options.type = advancedAnswers.type
if (advancedAnswers.threshold) options.threshold = advancedAnswers.threshold
options.explain = advancedAnswers.explain
}
}
const spinner = ora('Searching with Triple Intelligence™...').start()
const brain = getBrainy()
await brain.init()
// Build comprehensive search params
const searchParams: any = {
query,
limit: options.limit ? parseInt(options.limit) : 10
}
// Pagination
if (options.offset) {
searchParams.offset = parseInt(options.offset)
}
// Metadata Intelligence - type filtering
if (options.type) {
const types = options.type.split(',').map(t => t.trim())
searchParams.type = types.length === 1 ? types[0] : types
}
// Metadata Intelligence - field filtering
if (options.where) {
try {
searchParams.where = JSON.parse(options.where)
} catch {
spinner.fail('Invalid --where JSON')
console.log(chalk.dim('Example: --where \'{"status":"active","priority":{"$gte":5}}\''))
process.exit(1)
}
}
// 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,
threshold: options.threshold ? parseFloat(options.threshold) : 0.7
}
}
// Graph Intelligence - connection constraints
if (options.connectedTo || options.connectedFrom || options.via) {
searchParams.connected = {}
if (options.connectedTo) {
searchParams.connected.to = options.connectedTo
}
if (options.connectedFrom) {
searchParams.connected.from = options.connectedFrom
}
if (options.via) {
const vias = options.via.split(',').map(v => v.trim())
searchParams.connected.via = vias.length === 1 ? vias[0] : vias
}
}
// Explanation
if (options.explain) {
searchParams.explain = true
}
// Include relationships
if (options.includeRelations) {
searchParams.includeRelations = true
}
// VFS is now part of the knowledge graph (included by default)
// Users can exclude VFS with --where vfsType exists:false if needed
// Triple Intelligence Fusion - custom weighting
if (options.fusion || options.vectorWeight || options.graphWeight || options.fieldWeight) {
searchParams.fusion = {
strategy: options.fusion || 'adaptive',
weights: {}
}
if (options.vectorWeight) {
searchParams.fusion.weights.vector = parseFloat(options.vectorWeight)
}
if (options.graphWeight) {
searchParams.fusion.weights.graph = parseFloat(options.graphWeight)
}
if (options.fieldWeight) {
searchParams.fusion.weights.field = parseFloat(options.fieldWeight)
}
}
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`)
if (!options.json) {
if (results.length === 0) {
console.log(chalk.yellow('\nNo results found'))
// Show helpful hints
console.log(chalk.dim('\nTips:'))
console.log(chalk.dim(' • Try different search terms'))
console.log(chalk.dim(' • Remove filters (--type, --where, --connected-to)'))
console.log(chalk.dim(' • Lower the --threshold value'))
} else {
console.log(chalk.cyan(`\n📊 Triple Intelligence Results:\n`))
results.forEach((result, i) => {
const entity = result.entity || result
console.log(chalk.bold(`${i + 1}. ${entity.id}`))
// Show score with breakdown
if (result.score !== undefined) {
console.log(chalk.green(` Score: ${(result.score * 100).toFixed(1)}%`))
if (options.explain && (result as any).scores) {
const scores = (result as any).scores
if (scores.vector !== undefined) {
console.log(chalk.dim(` Vector: ${(scores.vector * 100).toFixed(1)}%`))
}
if (scores.graph !== undefined) {
console.log(chalk.dim(` Graph: ${(scores.graph * 100).toFixed(1)}%`))
}
if (scores.field !== undefined) {
console.log(chalk.dim(` Field: ${(scores.field * 100).toFixed(1)}%`))
}
}
}
// Show type
if ((entity as any).type) {
console.log(chalk.dim(` Type: ${(entity as any).type}`))
}
// Show content preview
if ((entity as any).content) {
const preview = (entity as any).content.substring(0, 80)
console.log(chalk.dim(` Content: ${preview}${(entity as any).content.length > 80 ? '...' : ''}`))
}
// Show metadata
if ((entity as any).metadata && Object.keys((entity as any).metadata).length > 0) {
console.log(chalk.dim(` Metadata: ${JSON.stringify((entity as any).metadata)}`))
}
// Show relationships
if (options.includeRelations && (result as any).relations) {
const relations = (result as any).relations
if (relations.length > 0) {
console.log(chalk.dim(` Relations: ${relations.length} connections`))
}
}
console.log()
})
// Show search summary
console.log(chalk.cyan('Search Configuration:'))
if (searchParams.type) {
console.log(chalk.dim(` Type filter: ${Array.isArray(searchParams.type) ? searchParams.type.join(', ') : searchParams.type}`))
}
if (searchParams.where) {
console.log(chalk.dim(` Field filter: ${JSON.stringify(searchParams.where)}`))
}
if (searchParams.connected) {
console.log(chalk.dim(` Graph filter: ${JSON.stringify(searchParams.connected)}`))
}
if (searchParams.fusion) {
console.log(chalk.dim(` Fusion: ${searchParams.fusion.strategy}`))
if (searchParams.fusion.weights && Object.keys(searchParams.fusion.weights).length > 0) {
console.log(chalk.dim(` Weights: ${JSON.stringify(searchParams.fusion.weights)}`))
}
}
}
} 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))
if (options.verbose) {
console.error(chalk.dim(error.stack))
}
process.exit(1)
}
},
/**
* Get item by ID
*/
async get(id: string | undefined, options: GetOptions) {
let spinner: any = null
try {
// Interactive mode if no ID provided
if (!id) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'id',
message: 'Enter item ID:',
validate: (input: string) => input.trim().length > 0 || 'ID cannot be empty'
},
{
type: 'confirm',
name: 'withConnections',
message: 'Include connections?',
default: false
}
])
id = answers.id
options.withConnections = answers.withConnections
}
const spinner = ora('Fetching item...').start()
const brain = getBrainy()
await brain.init()
// Try to get the item
const item = await brain.get(id)
if (!item) {
spinner.fail('Item not found')
console.log(chalk.yellow(`No item found with ID: ${id}`))
process.exit(1)
}
spinner.succeed('Item found')
if (!options.json) {
console.log(chalk.cyan('\nItem Details:'))
console.log(` ID: ${item.id}`)
console.log(` Content: ${(item as any).content || 'N/A'}`)
if (item.metadata) {
console.log(` Metadata: ${JSON.stringify(item.metadata, null, 2)}`)
}
if (options.withConnections) {
// Get verbs/relationships
// Get connections if method exists
const connections = (brain as any).getConnections ? await (brain as any).getConnections(id) : []
if (connections && connections.length > 0) {
console.log(chalk.cyan('\nConnections:'))
connections.forEach((conn: any) => {
console.log(` ${conn.source} --[${conn.type}]--> ${conn.target}`)
})
}
}
} 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))
process.exit(1)
}
},
/**
* Create relationship between items
*/
async relate(source: string | undefined, verb: string | undefined, target: string | undefined, options: RelateOptions) {
let spinner: any = null
try {
// Interactive mode if parameters missing
if (!source || !verb || !target) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'source',
message: 'Source entity ID:',
default: source || '',
validate: (input: string) => input.trim().length > 0 || 'Source ID cannot be empty'
},
{
type: 'input',
name: 'verb',
message: 'Relationship type (verb):',
default: verb || '',
validate: (input: string) => input.trim().length > 0 || 'Verb cannot be empty'
},
{
type: 'input',
name: 'target',
message: 'Target entity ID:',
default: target || '',
validate: (input: string) => input.trim().length > 0 || 'Target ID cannot be empty'
},
{
type: 'input',
name: 'weight',
message: 'Relationship weight (0-1, optional):',
default: '',
validate: (input: string) => {
if (!input.trim()) return true
const num = parseFloat(input)
return (num >= 0 && num <= 1) || 'Must be between 0 and 1'
}
}
])
source = answers.source
verb = answers.verb
target = answers.target
if (answers.weight) {
options.weight = answers.weight
}
}
const spinner = ora('Creating relationship...').start()
const brain = getBrainy()
await brain.init()
let metadata: any = {}
if (options.metadata) {
try {
metadata = JSON.parse(options.metadata)
} catch {
spinner.fail('Invalid metadata JSON')
process.exit(1)
}
}
if (options.weight) {
metadata.weight = parseFloat(options.weight)
}
// Create the relationship. Subtype precedence: caller-supplied
// `--subtype <value>` → Brainy default `'cli-relate'`. Same rationale
// as `brainy add` — guarantees CLI works under strict-mode brains
// (added 7.30.1).
const result = await brain.relate({
from: source,
to: target,
type: verb as any,
subtype: options.subtype ?? 'cli-relate',
metadata
})
spinner.succeed('Relationship created')
if (!options.json) {
console.log(chalk.green(`✓ Created relationship with ID: ${result}`))
console.log(chalk.dim(` ${source} --[${verb}]--> ${target}`))
if (metadata.weight) {
console.log(chalk.dim(` Weight: ${metadata.weight}`))
}
} 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))
process.exit(1)
}
},
/**
* Update an existing entity
*/
async update(id: string | undefined, options: AddOptions & { content?: string }) {
let spinner: any = null
try {
// Interactive mode if no ID provided
if (!id) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'id',
message: 'Entity ID to update:',
validate: (input: string) => input.trim().length > 0 || 'ID cannot be empty'
},
{
type: 'input',
name: 'content',
message: 'New content (optional, press Enter to skip):',
default: ''
},
{
type: 'input',
name: 'metadata',
message: 'Metadata to merge (JSON, optional):',
default: '',
validate: (input: string) => {
if (!input.trim()) return true
try {
JSON.parse(input)
return true
} catch {
return 'Invalid JSON format'
}
}
}
])
id = answers.id
if (answers.content) {
options.content = answers.content
}
if (answers.metadata) {
options.metadata = answers.metadata
}
}
spinner = ora('Updating entity...').start()
const brain = getBrainy()
await brain.init()
// Get existing entity first
const existing = await brain.get(id)
if (!existing) {
spinner.fail('Entity not found')
console.log(chalk.yellow(`No entity found with ID: ${id}`))
process.exit(1)
}
// Build update params
const updateParams: any = { id }
if (options.content) {
updateParams.data = options.content
}
if (options.metadata) {
try {
const newMetadata = JSON.parse(options.metadata)
updateParams.metadata = {
...existing.metadata,
...newMetadata
}
} catch {
spinner.fail('Invalid metadata JSON')
process.exit(1)
}
}
if (options.type) {
updateParams.type = options.type
}
await brain.update(updateParams)
spinner.succeed('Entity updated successfully')
if (!options.json) {
console.log(chalk.green(`✓ Updated entity: ${id}`))
if (options.content) {
console.log(chalk.dim(` New content: ${options.content.substring(0, 80)}...`))
}
if (updateParams.metadata) {
console.log(chalk.dim(` Metadata: ${JSON.stringify(updateParams.metadata)}`))
}
} 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))
process.exit(1)
}
},
/**
* Delete an entity
*/
async deleteEntity(id: string | undefined, options: CoreOptions & { force?: boolean }) {
let spinner: any = null
try {
// Interactive mode if no ID provided
if (!id) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'id',
message: 'Entity ID to delete:',
validate: (input: string) => input.trim().length > 0 || 'ID cannot be empty'
},
{
type: 'confirm',
name: 'confirm',
message: 'Are you sure? This cannot be undone.',
default: false
}
])
if (!answers.confirm) {
console.log(chalk.yellow('Delete cancelled'))
return
}
id = answers.id
} else if (!options.force) {
// Confirmation for non-interactive mode
const answer = await inquirer.prompt([{
type: 'confirm',
name: 'confirm',
message: `Delete entity ${id}? This cannot be undone.`,
default: false
}])
if (!answer.confirm) {
console.log(chalk.yellow('Delete cancelled'))
return
}
}
spinner = ora('Deleting entity...').start()
const brain = getBrainy()
await brain.init()
await brain.delete(id)
spinner.succeed('Entity deleted successfully')
if (!options.json) {
console.log(chalk.green(`✓ Deleted entity: ${id}`))
} 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))
process.exit(1)
}
},
/**
* Remove a relationship
*/
async unrelate(id: string | undefined, options: CoreOptions & { force?: boolean }) {
let spinner: any = null
try {
// Interactive mode if no ID provided
if (!id) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'id',
message: 'Relationship ID to remove:',
validate: (input: string) => input.trim().length > 0 || 'ID cannot be empty'
},
{
type: 'confirm',
name: 'confirm',
message: 'Remove this relationship?',
default: false
}
])
if (!answers.confirm) {
console.log(chalk.yellow('Operation cancelled'))
return
}
id = answers.id
} else if (!options.force) {
const answer = await inquirer.prompt([{
type: 'confirm',
name: 'confirm',
message: `Remove relationship ${id}?`,
default: false
}])
if (!answer.confirm) {
console.log(chalk.yellow('Operation cancelled'))
return
}
}
spinner = ora('Removing relationship...').start()
const brain = getBrainy()
await brain.init()
await brain.unrelate(id)
spinner.succeed('Relationship removed successfully')
if (!options.json) {
console.log(chalk.green(`✓ Removed relationship: ${id}`))
} 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))
process.exit(1)
}
},
/**
* Show plugin and provider diagnostics
*/
async diagnostics(options: CoreOptions) {
try {
const brain = getBrainy()
await brain.init()
const diag = brain.diagnostics()
if (options.json) {
formatOutput(diag, options)
// One-shot command — see add() for why the explicit close + exit.
await brain.close()
process.exit(0)
}
console.log(chalk.bold('\nBrainy Diagnostics'))
console.log(chalk.dim(`Version: ${diag.version}`))
console.log()
// Plugins
console.log(chalk.bold('Plugins:'))
if (diag.plugins.count === 0) {
console.log(chalk.dim(' (none active)'))
} else {
for (const name of diag.plugins.active) {
console.log(chalk.green(`${name}`))
}
}
console.log()
// Providers
console.log(chalk.bold('Providers:'))
for (const [key, info] of Object.entries(diag.providers)) {
const icon = info.source === 'plugin' ? chalk.green('✓ plugin') : chalk.dim('default')
console.log(` ${key.padEnd(16)} ${icon}`)
}
console.log()
// Indexes
console.log(chalk.bold('Indexes:'))
console.log(` HNSW: ${diag.indexes.hnsw.type} (${diag.indexes.hnsw.size} vectors)`)
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)
}
}
}