brainy/src/cortex/commands/index.ts
David Snelling 89d6d1e8ce feat: add Cortex - complete CLI command center for configuration and coordination
- New CLI tool for managing Brainy databases
- Encrypted configuration management (replaces .env files)
- Distributed storage migration coordination
- Advanced MongoDB-style query interface
- Backup/restore, health checks, statistics
- Interactive shell mode
- One-line integration: await brainy.loadEnvironment()
- Full documentation and migration guides
- Node.js only (browser safe with environment detection)

BREAKING CHANGE: Package size increased ~250KB due to CLI dependencies
2025-08-07 13:53:17 -07:00

713 lines
No EOL
20 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Cortex Commands Implementation
*
* All CLI commands for managing Brainy through Cortex
*/
import { CortexConfig } from '../config.js'
import { BrainyData } from '../../brainyData.js'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as readline from 'readline/promises'
import { stdin as input, stdout as output } from 'process'
import chalk from 'chalk'
import Table from 'cli-table3'
export class CortexCommands {
/**
* Initialize Cortex in a project
*/
static async init(options: any): Promise<void> {
const config = CortexConfig.getInstance()
if (options.interactive) {
const rl = readline.createInterface({ input, output })
console.log(chalk.cyan('\n🧠 Welcome to Cortex - Brainy Command Center\n'))
// Storage type
const storageType = await rl.question(
'Storage type (s3/filesystem/memory/opfs) [s3]: '
) || 's3'
let storageConfig: any = { type: storageType }
// Storage-specific configuration
if (storageType === 's3') {
storageConfig.bucket = await rl.question('S3 bucket name: ')
storageConfig.region = await rl.question('AWS region [us-east-1]: ') || 'us-east-1'
const useEnvCreds = await rl.question(
'Use AWS credentials from environment? (y/n) [y]: '
) || 'y'
if (useEnvCreds !== 'y') {
storageConfig.accessKeyId = await rl.question('AWS Access Key ID: ')
storageConfig.secretAccessKey = await rl.question('AWS Secret Access Key: ')
}
} else if (storageType === 'filesystem') {
storageConfig.path = await rl.question(
`Storage path [${path.join(process.cwd(), '.brainy-data')}]: `
) || path.join(process.cwd(), '.brainy-data')
}
// Encryption
const enableEncryption = await rl.question(
'Enable encrypted configuration? (y/n) [y]: '
) || 'y'
// Coordination
const enableCoordination = await rl.question(
'Enable distributed coordination? (y/n) [y]: '
) || 'y'
rl.close()
await config.init({
storage: storageConfig,
encryption: {
enabled: enableEncryption === 'y',
keyDerivation: 'pbkdf2'
},
coordination: {
enabled: enableCoordination === 'y',
realtime: false,
pollInterval: 30000
}
})
console.log(chalk.green('\n✅ Cortex initialized successfully!'))
console.log(chalk.gray('Configuration saved to .brainy/cortex.json'))
console.log(chalk.gray('Master key saved to .brainy/cortex.key'))
console.log(chalk.yellow('\n⚠ Keep your master key safe!'))
} else {
// Non-interactive mode
await config.init({
storage: options.storage ? { type: options.storage } : undefined
})
}
}
/**
* Set a configuration value
*/
static async configSet(key: string, value: string | undefined, options: any): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
// If no value provided, prompt for it
if (!value) {
const rl = readline.createInterface({ input, output })
value = await rl.question(`Enter value for ${key}: `)
rl.close()
}
await config.set(key, value, { encrypt: options.encrypt })
console.log(chalk.green(`✅ Set ${key}`))
}
/**
* Get a configuration value
*/
static async configGet(key: string): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
const value = await config.get(key)
if (value === undefined) {
console.log(chalk.yellow(`Configuration key '${key}' not found`))
} else {
console.log(value)
}
}
/**
* List all configuration keys
*/
static async configList(options: any): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
const configs = await config.list()
if (configs.length === 0) {
console.log(chalk.yellow('No configurations found'))
return
}
const table = new Table({
head: ['Key', 'Encrypted', 'Environment'],
style: { head: ['cyan'] }
})
for (const cfg of configs) {
const row = [
cfg.key,
cfg.encrypted ? chalk.green('✓') : chalk.gray('-'),
cfg.environment
]
if (options.showValues) {
const value = await config.get(cfg.key)
row.push(value ? String(value).substring(0, 50) : '')
}
table.push(row)
}
console.log(table.toString())
}
/**
* Import configuration from .env file
*/
static async configImport(file: string): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
await config.importEnv(file)
console.log(chalk.green(`✅ Imported configuration from ${file}`))
}
/**
* Load environment variables
*/
static async envLoad(options: any): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
const env = await config.loadEnvironment()
if (options.export) {
// Output as shell export commands
for (const [key, value] of Object.entries(env)) {
console.log(`export ${key}="${value}"`)
}
} else {
// Set in current process
Object.assign(process.env, env)
console.log(chalk.green(`✅ Loaded ${Object.keys(env).length} environment variables`))
}
}
/**
* Migrate to new storage
*/
static async migrate(options: any): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
const brainy = config.getBrainy()
// Parse target storage URL
const targetUrl = new URL(options.to)
let targetConfig: any = {}
if (targetUrl.protocol === 's3:') {
targetConfig = {
type: 's3',
bucket: targetUrl.hostname,
region: targetUrl.searchParams.get('region') || 'us-east-1'
}
} else if (targetUrl.protocol === 'file:') {
targetConfig = {
type: 'filesystem',
path: targetUrl.pathname
}
}
// Create coordination plan
const coordinationPlan = {
version: 1,
timestamp: new Date().toISOString(),
migration: {
enabled: true,
target: targetConfig,
strategy: options.strategy,
dryRun: options.dryRun || false
}
}
// Store coordination plan in current storage
await brainy.addNoun({
id: '_system/coordination',
type: 'cortex_coordination',
metadata: coordinationPlan
})
console.log(chalk.cyan('📋 Migration plan created:'))
console.log(JSON.stringify(coordinationPlan, null, 2))
if (!options.dryRun) {
console.log(chalk.yellow('\n⏳ Migration will begin shortly...'))
console.log(chalk.gray('All services will automatically detect and execute the migration'))
}
}
/**
* Synchronize services
*/
static async sync(options: any): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
const brainy = config.getBrainy()
// Trigger sync by updating coordination timestamp
await brainy.addNoun({
id: '_system/sync',
type: 'cortex_sync',
metadata: {
timestamp: new Date().toISOString(),
force: options.force || false
}
})
console.log(chalk.green('✅ Sync signal sent to all services'))
}
/**
* Query data with advanced filtering
*/
static async query(pattern: string, options: any): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
const brainy = config.getBrainy()
// Parse metadata filter if provided
let metadataFilter = null
if (options.filter || options.metadata) {
try {
metadataFilter = JSON.parse(options.filter || options.metadata)
} catch (error) {
console.log(chalk.red(`Invalid filter JSON: ${error.message}`))
return
}
}
// Show query plan if requested
if (options.explain) {
console.log(chalk.cyan('\n📋 Query Plan:'))
console.log(`Pattern: "${pattern}"`)
if (metadataFilter) {
console.log(`Filter: ${JSON.stringify(metadataFilter, null, 2)}`)
}
console.log(`Limit: ${options.limit || 10}\n`)
}
// Perform search with metadata filtering
const results = await brainy.searchNouns(pattern, {
limit: parseInt(options.limit) || 10,
metadata: metadataFilter
})
if (options.json) {
console.log(JSON.stringify(results, null, 2))
} else {
if (results.length === 0) {
console.log(chalk.yellow('No results found'))
return
}
const table = new Table({
head: ['ID', 'Type', 'Score', 'Sample Metadata'],
style: { head: ['cyan'] },
colWidths: [30, 15, 10, 40]
})
for (const result of results) {
// Show a sample of metadata
let metaSample = ''
if (result.metadata) {
const keys = Object.keys(result.metadata).slice(0, 3)
metaSample = keys.map(k => `${k}: ${JSON.stringify(result.metadata[k])}`).join(', ')
if (Object.keys(result.metadata).length > 3) {
metaSample += ', ...'
}
}
table.push([
result.id.substring(0, 28),
result.type || '-',
result.score?.toFixed(3) || '-',
metaSample.substring(0, 38)
])
}
console.log(table.toString())
console.log(chalk.gray(`\nFound ${results.length} results`))
}
}
/**
* Show statistics
*/
static async stats(options: any): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
const brainy = config.getBrainy()
const stats = await brainy.getStatistics()
if (options.json) {
console.log(JSON.stringify(stats, null, 2))
} else {
console.log(chalk.cyan('\n📊 Brainy Statistics\n'))
console.log(`Nouns: ${chalk.green(stats.totalNouns)}`)
console.log(`Verbs: ${chalk.green(stats.totalVerbs)}`)
console.log(`Storage Used: ${chalk.green(this.formatBytes(stats.storageUsed || 0))}`)
console.log(`Index Size: ${chalk.green(this.formatBytes(stats.indexSize || 0))}`)
if (stats.metadata) {
console.log(`\nMetadata:`)
for (const [key, value] of Object.entries(stats.metadata)) {
console.log(` ${key}: ${chalk.gray(JSON.stringify(value))}`)
}
}
}
}
/**
* Create backup
*/
static async backup(destination: string | undefined, options: any): Promise<string> {
const config = CortexConfig.getInstance()
await config.load()
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const backupName = `brainy-backup-${timestamp}`
const backupPath = destination || path.join(process.cwd(), `${backupName}.json`)
const brainy = config.getBrainy()
// Export all data
const backup: any = {
version: 1,
timestamp: new Date().toISOString(),
data: {
nouns: [],
verbs: []
}
}
// Export nouns with pagination
let offset = 0
let hasMore = true
while (hasMore) {
const result = await brainy.getNouns({
pagination: { offset, limit: 100 }
})
backup.data.nouns.push(...result.items)
hasMore = result.hasMore
offset += 100
}
// Export verbs with pagination
offset = 0
hasMore = true
while (hasMore) {
const result = await brainy.getVerbs({
pagination: { offset, limit: 100 }
})
backup.data.verbs.push(...result.items)
hasMore = result.hasMore
offset += 100
}
// Include configuration if requested
if (options.includeConfig) {
backup.config = await config.list()
}
// Save backup
const backupContent = JSON.stringify(backup, null, 2)
if (options.compress) {
const zlib = await import('zlib')
const compressed = await new Promise<Buffer>((resolve, reject) => {
zlib.gzip(Buffer.from(backupContent), (err, result) => {
if (err) reject(err)
else resolve(result)
})
})
await fs.writeFile(`${backupPath}.gz`, compressed)
return `${backupPath}.gz`
} else {
await fs.writeFile(backupPath, backupContent)
return backupPath
}
}
/**
* Restore from backup
*/
static async restore(source: string, options: any): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
let backupContent: string
if (source.endsWith('.gz')) {
const zlib = await import('zlib')
const compressed = await fs.readFile(source)
backupContent = await new Promise<string>((resolve, reject) => {
zlib.gunzip(compressed, (err, result) => {
if (err) reject(err)
else resolve(result.toString())
})
})
} else {
backupContent = await fs.readFile(source, 'utf-8')
}
const backup = JSON.parse(backupContent)
const brainy = config.getBrainy()
if (!options.force) {
const rl = readline.createInterface({ input, output })
const confirm = await rl.question(
chalk.yellow('⚠️ This will overwrite existing data. Continue? (y/n): ')
)
rl.close()
if (confirm !== 'y') {
console.log('Restore cancelled')
return
}
}
// Restore nouns
for (const noun of backup.data.nouns) {
await brainy.addNoun(noun)
}
// Restore verbs
for (const verb of backup.data.verbs) {
await brainy.addVerb(verb)
}
console.log(chalk.green(`✅ Restored ${backup.data.nouns.length} nouns and ${backup.data.verbs.length} verbs`))
}
/**
* Inspect an item
*/
static async inspect(id: string, options: any): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
const brainy = config.getBrainy()
try {
// Try as noun first
const noun = await brainy.getNoun(id)
if (noun) {
if (options.raw) {
console.log(JSON.stringify(noun, null, 2))
} else {
console.log(chalk.cyan(`\n📦 Noun: ${id}\n`))
console.log(`Type: ${chalk.green(noun.type || 'unknown')}`)
if (noun.metadata) {
console.log(`Metadata:`)
console.log(JSON.stringify(noun.metadata, null, 2))
}
}
return
}
} catch (e) {
// Not a noun, try verb
}
try {
// Try as verb
const verb = await brainy.getVerb(id)
if (verb) {
if (options.raw) {
console.log(JSON.stringify(verb, null, 2))
} else {
console.log(chalk.cyan(`\n🔗 Verb: ${id}\n`))
console.log(`Type: ${chalk.green(verb.type)}`)
console.log(`Source: ${chalk.blue(verb.source)}`)
console.log(`Target: ${chalk.blue(verb.target)}`)
if (verb.metadata) {
console.log(`Metadata:`)
console.log(JSON.stringify(verb.metadata, null, 2))
}
}
return
}
} catch (e) {
// Not found
}
console.log(chalk.red(`Item '${id}' not found`))
}
/**
* Reindex database
*/
static async reindex(options: any): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
const brainy = config.getBrainy()
if (options.type === 'metadata' || options.type === 'all') {
await brainy.rebuildMetadataIndex()
}
if (options.type === 'hnsw' || options.type === 'all') {
// HNSW reindex would go here
console.log(chalk.yellow('HNSW reindexing not yet implemented'))
}
}
/**
* Health check
*/
static async health(options: any): Promise<void> {
const config = CortexConfig.getInstance()
try {
await config.load()
const brainy = config.getBrainy()
const stats = await brainy.getStatistics()
console.log(chalk.green('✅ Cortex: Healthy'))
console.log(chalk.green('✅ Brainy: Connected'))
console.log(chalk.green(`✅ Storage: Active (${stats.totalNouns} nouns, ${stats.totalVerbs} verbs)`))
if (options.verbose) {
console.log('\nDetailed Health:')
console.log(`- Config Path: ${path.join(process.cwd(), '.brainy/cortex.json')}`)
console.log(`- Storage Type: ${config.getStorageConfig()?.type}`)
console.log(`- Environment: ${config.getCurrentEnvironment()}`)
}
} catch (error: any) {
console.log(chalk.red('❌ Cortex: Error'))
console.log(chalk.red(` ${error.message}`))
process.exit(1)
}
}
/**
* Interactive shell
*/
static async shell(): Promise<void> {
const config = CortexConfig.getInstance()
await config.load()
console.log(chalk.cyan('\n🧠 Cortex Interactive Shell\n'))
console.log(chalk.gray('Type "help" for commands, "exit" to quit\n'))
const rl = readline.createInterface({ input, output })
while (true) {
const command = await rl.question(chalk.cyan('cortex> '))
if (command === 'exit' || command === 'quit') {
break
}
if (command === 'help') {
console.log(`
Available commands:
query <pattern> - Search for data
get <key> - Get config value
set <key> <value> - Set config value
stats - Show statistics
health - Check health
exit - Exit shell
`)
continue
}
// Parse and execute command
const [cmd, ...args] = command.split(' ')
try {
switch (cmd) {
case 'query':
await this.query(args.join(' '), { limit: 5 })
break
case 'get':
await this.configGet(args[0])
break
case 'set':
await this.configSet(args[0], args.slice(1).join(' '), { encrypt: true })
break
case 'stats':
await this.stats({})
break
case 'health':
await this.health({})
break
default:
console.log(chalk.yellow(`Unknown command: ${cmd}`))
}
} catch (error: any) {
console.log(chalk.red(`Error: ${error.message}`))
}
}
rl.close()
console.log(chalk.gray('\nGoodbye!\n'))
}
/**
* Show status
*/
static async status(): Promise<void> {
const config = CortexConfig.getInstance()
try {
await config.load()
const brainy = config.getBrainy()
const stats = await brainy.getStatistics()
console.log(chalk.cyan('\n🧠 Cortex Status\n'))
const table = new Table({
style: { head: ['cyan'] }
})
table.push(
{ 'Configuration': chalk.green('✓ Loaded') },
{ 'Storage': `${config.getStorageConfig()?.type}` },
{ 'Environment': config.getCurrentEnvironment() },
{ 'Encryption': chalk.green('✓ Enabled') },
{ 'Total Nouns': stats.totalNouns.toString() },
{ 'Total Verbs': stats.totalVerbs.toString() },
{ 'Storage Used': this.formatBytes(stats.storageUsed || 0) }
)
console.log(table.toString())
// Check for active migrations
const coordinationNoun = await brainy.getNoun('_system/coordination')
if (coordinationNoun?.metadata?.migration?.enabled) {
console.log(chalk.yellow('\n⚠ Active Migration:'))
console.log(` Target: ${coordinationNoun.metadata.migration.target.type}`)
console.log(` Strategy: ${coordinationNoun.metadata.migration.strategy}`)
}
} catch (error: any) {
console.log(chalk.red('❌ Cortex not initialized'))
console.log(chalk.gray('Run "cortex init" to get started'))
}
}
/**
* Format bytes to human readable
*/
private static formatBytes(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`
}
}