2025-06-05 11:18:20 -07:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Brainy CLI
|
|
|
|
|
* A command-line interface for interacting with the Brainy vector database
|
|
|
|
|
*/
|
|
|
|
|
|
2025-06-05 11:44:18 -07:00
|
|
|
import { BrainyData, NounType, VerbType, FileSystemStorage } from './index.js'
|
|
|
|
|
import { fileURLToPath } from 'url'
|
|
|
|
|
import { dirname, join } from 'path'
|
|
|
|
|
import fs from 'fs'
|
|
|
|
|
import { Command } from 'commander'
|
|
|
|
|
import omelette from 'omelette'
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// Get the directory of the current module
|
2025-06-05 11:44:18 -07:00
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
|
|
|
const __dirname = dirname(__filename)
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// Import package.json for version info
|
|
|
|
|
const packageJson = {
|
|
|
|
|
name: '@soulcraft/brainy',
|
|
|
|
|
version: '0.6.0',
|
|
|
|
|
description: 'A vector database using HNSW indexing with Origin Private File System storage'
|
2025-06-05 11:44:18 -07:00
|
|
|
}
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// Helper function to parse JSON safely
|
|
|
|
|
function parseJSON(str: string): any {
|
|
|
|
|
try {
|
2025-06-05 11:44:18 -07:00
|
|
|
return JSON.parse(str)
|
2025-06-05 11:18:20 -07:00
|
|
|
} catch (e) {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.error('Error parsing JSON:', (e as Error).message)
|
|
|
|
|
return {}
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to resolve noun type
|
|
|
|
|
function resolveNounType(type: string | number | undefined): NounType {
|
2025-06-05 11:44:18 -07:00
|
|
|
if (!type) return NounType.Thing
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// If it's a string, try to match it to a NounType
|
|
|
|
|
if (typeof type === 'string') {
|
|
|
|
|
const nounTypeKey = Object.keys(NounType).find(
|
|
|
|
|
key => key.toLowerCase() === type.toLowerCase()
|
2025-06-05 11:44:18 -07:00
|
|
|
)
|
|
|
|
|
return nounTypeKey ? NounType[nounTypeKey as keyof typeof NounType] : NounType.Thing
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert number to string type for safety
|
2025-06-05 11:44:18 -07:00
|
|
|
return Object.values(NounType)[type as number] || NounType.Thing
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to resolve verb type
|
|
|
|
|
function resolveVerbType(type: string | number | undefined): VerbType {
|
2025-06-05 11:44:18 -07:00
|
|
|
if (!type) return VerbType.RelatedTo
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// If it's a string, try to match it to a VerbType
|
|
|
|
|
if (typeof type === 'string') {
|
|
|
|
|
const verbTypeKey = Object.keys(VerbType).find(
|
|
|
|
|
key => key.toLowerCase() === type.toLowerCase()
|
2025-06-05 11:44:18 -07:00
|
|
|
)
|
|
|
|
|
return verbTypeKey ? VerbType[verbTypeKey as keyof typeof VerbType] : VerbType.RelatedTo
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert number to string type for safety
|
2025-06-05 11:44:18 -07:00
|
|
|
return Object.values(VerbType)[type as number] || VerbType.RelatedTo
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a new Command instance
|
2025-06-05 11:44:18 -07:00
|
|
|
const program = new Command()
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// Configure the program
|
|
|
|
|
program
|
|
|
|
|
.name(packageJson.name)
|
|
|
|
|
.description(packageJson.description)
|
2025-06-05 11:44:18 -07:00
|
|
|
.version(packageJson.version)
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// Create data directory if it doesn't exist
|
2025-06-05 11:44:18 -07:00
|
|
|
const dataDir = join(__dirname, '..', 'data')
|
2025-06-05 11:18:20 -07:00
|
|
|
if (!fs.existsSync(dataDir)) {
|
2025-06-05 11:44:18 -07:00
|
|
|
fs.mkdirSync(dataDir, { recursive: true })
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a database instance with file system storage
|
|
|
|
|
const createDb = () => {
|
|
|
|
|
return new BrainyData({
|
|
|
|
|
storageAdapter: new FileSystemStorage(dataDir)
|
2025-06-05 11:44:18 -07:00
|
|
|
})
|
|
|
|
|
}
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// Define commands
|
|
|
|
|
program
|
|
|
|
|
.command('init')
|
|
|
|
|
.description('Initialize a new database')
|
|
|
|
|
.action(async () => {
|
|
|
|
|
try {
|
2025-06-05 11:44:18 -07:00
|
|
|
const db = createDb()
|
|
|
|
|
await db.init()
|
|
|
|
|
console.log('Database initialized successfully')
|
2025-06-05 11:18:20 -07:00
|
|
|
} catch (error) {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.error('Error:', (error as Error).message)
|
|
|
|
|
process.exit(1)
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
2025-06-05 11:44:18 -07:00
|
|
|
})
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
program
|
|
|
|
|
.command('add')
|
|
|
|
|
.description('Add a new noun with the given text and optional metadata')
|
|
|
|
|
.argument('<text>', 'Text to add as a noun')
|
|
|
|
|
.argument('[metadata]', 'Optional metadata as JSON string')
|
|
|
|
|
.action(async (text, metadataStr) => {
|
|
|
|
|
try {
|
2025-06-05 11:44:18 -07:00
|
|
|
const db = createDb()
|
|
|
|
|
await db.init()
|
2025-06-05 11:18:20 -07:00
|
|
|
|
2025-06-05 11:44:18 -07:00
|
|
|
const metadata = metadataStr ? parseJSON(metadataStr) : {}
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// Process metadata to handle noun type
|
|
|
|
|
if (metadata.noun) {
|
2025-06-05 11:44:18 -07:00
|
|
|
metadata.noun = resolveNounType(metadata.noun)
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
|
|
|
|
|
2025-06-05 11:44:18 -07:00
|
|
|
const id = await db.add(text, metadata)
|
|
|
|
|
console.log(`Added noun with ID: ${id}`)
|
2025-06-05 11:18:20 -07:00
|
|
|
} catch (error) {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.error('Error:', (error as Error).message)
|
|
|
|
|
process.exit(1)
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
2025-06-05 11:44:18 -07:00
|
|
|
})
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
program
|
|
|
|
|
.command('search')
|
|
|
|
|
.description('Search for nouns similar to the query')
|
|
|
|
|
.argument('<query>', 'Search query text')
|
|
|
|
|
.option('-l, --limit <number>', 'Maximum number of results to return', '5')
|
|
|
|
|
.action(async (query, options) => {
|
|
|
|
|
try {
|
2025-06-05 11:44:18 -07:00
|
|
|
const db = createDb()
|
|
|
|
|
await db.init()
|
2025-06-05 11:18:20 -07:00
|
|
|
|
2025-06-05 11:44:18 -07:00
|
|
|
const limit = parseInt(options.limit, 10)
|
|
|
|
|
const results = await db.searchText(query, limit)
|
2025-06-05 11:18:20 -07:00
|
|
|
|
2025-06-05 11:44:18 -07:00
|
|
|
console.log(`Search results for "${query}":`)
|
2025-06-05 11:18:20 -07:00
|
|
|
results.forEach((result, index) => {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.log(`${index + 1}. ID: ${result.id}`)
|
|
|
|
|
console.log(` Score: ${result.score.toFixed(4)}`)
|
|
|
|
|
console.log(` Metadata: ${JSON.stringify(result.metadata)}`)
|
|
|
|
|
console.log(` Vector: [${result.vector.slice(0, 3).map(v => v.toFixed(2)).join(', ')}...]`)
|
|
|
|
|
console.log()
|
|
|
|
|
})
|
2025-06-05 11:18:20 -07:00
|
|
|
} catch (error) {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.error('Error:', (error as Error).message)
|
|
|
|
|
process.exit(1)
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
2025-06-05 11:44:18 -07:00
|
|
|
})
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
program
|
|
|
|
|
.command('get')
|
|
|
|
|
.description('Get a noun by ID')
|
|
|
|
|
.argument('<id>', 'ID of the noun to get')
|
|
|
|
|
.action(async (id) => {
|
|
|
|
|
try {
|
2025-06-05 11:44:18 -07:00
|
|
|
const db = createDb()
|
|
|
|
|
await db.init()
|
2025-06-05 11:18:20 -07:00
|
|
|
|
2025-06-05 11:44:18 -07:00
|
|
|
const noun = await db.get(id)
|
2025-06-05 11:18:20 -07:00
|
|
|
if (noun) {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.log(`Noun ID: ${noun.id}`)
|
|
|
|
|
console.log(`Metadata: ${JSON.stringify(noun.metadata)}`)
|
|
|
|
|
console.log(`Vector: [${noun.vector.slice(0, 5).map(v => v.toFixed(2)).join(', ')}...]`)
|
2025-06-05 11:18:20 -07:00
|
|
|
} else {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.log(`No noun found with ID: ${id}`)
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.error('Error:', (error as Error).message)
|
|
|
|
|
process.exit(1)
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
2025-06-05 11:44:18 -07:00
|
|
|
})
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
program
|
|
|
|
|
.command('delete')
|
|
|
|
|
.description('Delete a noun by ID')
|
|
|
|
|
.argument('<id>', 'ID of the noun to delete')
|
|
|
|
|
.action(async (id) => {
|
|
|
|
|
try {
|
2025-06-05 11:44:18 -07:00
|
|
|
const db = createDb()
|
|
|
|
|
await db.init()
|
2025-06-05 11:18:20 -07:00
|
|
|
|
2025-06-05 11:44:18 -07:00
|
|
|
await db.delete(id)
|
|
|
|
|
console.log(`Deleted noun with ID: ${id}`)
|
2025-06-05 11:18:20 -07:00
|
|
|
} catch (error) {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.error('Error:', (error as Error).message)
|
|
|
|
|
process.exit(1)
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
2025-06-05 11:44:18 -07:00
|
|
|
})
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
program
|
|
|
|
|
.command('addVerb')
|
|
|
|
|
.description('Add a relationship between nouns')
|
|
|
|
|
.argument('<sourceId>', 'ID of the source noun')
|
|
|
|
|
.argument('<targetId>', 'ID of the target noun')
|
|
|
|
|
.argument('<verbType>', 'Type of relationship')
|
|
|
|
|
.argument('[metadata]', 'Optional metadata as JSON string')
|
|
|
|
|
.action(async (sourceId, targetId, verbTypeStr, metadataStr) => {
|
|
|
|
|
try {
|
2025-06-05 11:44:18 -07:00
|
|
|
const db = createDb()
|
|
|
|
|
await db.init()
|
2025-06-05 11:18:20 -07:00
|
|
|
|
2025-06-05 11:44:18 -07:00
|
|
|
const verbType = resolveVerbType(verbTypeStr)
|
|
|
|
|
const verbMetadata = metadataStr ? parseJSON(metadataStr) : {}
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// Add verb type to metadata
|
2025-06-05 11:44:18 -07:00
|
|
|
verbMetadata.verb = verbType
|
2025-06-05 11:18:20 -07:00
|
|
|
|
2025-06-05 11:44:18 -07:00
|
|
|
const verbId = await db.addVerb(sourceId, targetId, undefined, {
|
|
|
|
|
type: verbType,
|
|
|
|
|
metadata: verbMetadata
|
|
|
|
|
})
|
|
|
|
|
console.log(`Added verb with ID: ${verbId}`)
|
2025-06-05 11:18:20 -07:00
|
|
|
} catch (error) {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.error('Error:', (error as Error).message)
|
|
|
|
|
process.exit(1)
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
2025-06-05 11:44:18 -07:00
|
|
|
})
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
program
|
|
|
|
|
.command('getVerbs')
|
|
|
|
|
.description('Get all relationships for a noun')
|
|
|
|
|
.argument('<id>', 'ID of the noun to get relationships for')
|
|
|
|
|
.action(async (id) => {
|
|
|
|
|
try {
|
2025-06-05 11:44:18 -07:00
|
|
|
const db = createDb()
|
|
|
|
|
await db.init()
|
2025-06-05 11:18:20 -07:00
|
|
|
|
2025-06-05 11:44:18 -07:00
|
|
|
const verbs = await db.getVerbsBySource(id)
|
2025-06-05 11:18:20 -07:00
|
|
|
|
2025-06-05 11:44:18 -07:00
|
|
|
console.log(`Relationships for noun ${id}:`)
|
2025-06-05 11:18:20 -07:00
|
|
|
if (verbs.length === 0) {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.log('No relationships found')
|
2025-06-05 11:18:20 -07:00
|
|
|
} else {
|
|
|
|
|
verbs.forEach((verb, index) => {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.log(`${index + 1}. ID: ${verb.id}`)
|
|
|
|
|
console.log(` Type: ${Object.keys(VerbType).find(key => VerbType[key as keyof typeof VerbType] === verb.metadata.verb) || verb.metadata.verb}`)
|
|
|
|
|
console.log(` Target: ${verb.targetId}`)
|
|
|
|
|
console.log(` Metadata: ${JSON.stringify(verb.metadata)}`)
|
|
|
|
|
console.log()
|
|
|
|
|
})
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.error('Error:', (error as Error).message)
|
|
|
|
|
process.exit(1)
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
2025-06-05 11:44:18 -07:00
|
|
|
})
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
program
|
|
|
|
|
.command('status')
|
|
|
|
|
.description('Show database status')
|
|
|
|
|
.action(async () => {
|
|
|
|
|
try {
|
2025-06-05 11:44:18 -07:00
|
|
|
const db = createDb()
|
|
|
|
|
await db.init()
|
2025-06-05 11:18:20 -07:00
|
|
|
|
2025-06-05 11:44:18 -07:00
|
|
|
const status = await db.status()
|
|
|
|
|
console.log('Database Status:')
|
|
|
|
|
console.log(`Storage type: ${status.type}`)
|
|
|
|
|
console.log(`Storage used: ${status.used} bytes`)
|
|
|
|
|
console.log(`Storage quota: ${status.quota !== null ? `${status.quota} bytes` : 'unlimited'}`)
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// Display additional details if available
|
|
|
|
|
if (status.details) {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.log('Additional details:')
|
2025-06-05 11:18:20 -07:00
|
|
|
Object.entries(status.details).forEach(([key, value]) => {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.log(` ${key}: ${JSON.stringify(value)}`)
|
|
|
|
|
})
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-06-05 11:44:18 -07:00
|
|
|
console.error('Error:', (error as Error).message)
|
|
|
|
|
process.exit(1)
|
2025-06-05 11:18:20 -07:00
|
|
|
}
|
2025-06-05 11:44:18 -07:00
|
|
|
})
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// Add examples to help text
|
|
|
|
|
program.addHelpText('after', `
|
|
|
|
|
Examples:
|
|
|
|
|
$ brainy init
|
|
|
|
|
$ brainy add "Cats are independent pets" '{"noun":"Thing","category":"animal"}'
|
|
|
|
|
$ brainy search "feline pets" --limit 5
|
|
|
|
|
$ brainy addVerb id1 id2 RelatedTo '{"description":"Both are pets"}'
|
2025-06-05 11:44:18 -07:00
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
// Setup autocomplete
|
|
|
|
|
const completion = omelette('brainy')
|
|
|
|
|
|
|
|
|
|
// Helper function to get all noun types
|
|
|
|
|
const getNounTypes = () => Object.keys(NounType)
|
|
|
|
|
|
|
|
|
|
// Helper function to get all verb types
|
|
|
|
|
const getVerbTypes = () => Object.keys(VerbType)
|
|
|
|
|
|
|
|
|
|
// Define autocomplete handlers
|
|
|
|
|
completion.tree({
|
|
|
|
|
// First level commands - suggest all available commands
|
|
|
|
|
_: () => [
|
|
|
|
|
'add',
|
|
|
|
|
'addVerb',
|
|
|
|
|
'search',
|
|
|
|
|
'get',
|
|
|
|
|
'delete',
|
|
|
|
|
'getVerbs',
|
|
|
|
|
'status',
|
|
|
|
|
'completion-setup',
|
|
|
|
|
'init',
|
|
|
|
|
'help'
|
|
|
|
|
],
|
|
|
|
|
// Command-specific completions
|
|
|
|
|
add: {
|
|
|
|
|
// For the second argument of 'add' command (metadata)
|
|
|
|
|
_: () => {
|
|
|
|
|
// Generate templates for each noun type
|
|
|
|
|
return getNounTypes().map(type =>
|
|
|
|
|
`{"noun":"${type}","category":"example"}`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
addVerb: {
|
|
|
|
|
// First two arguments are IDs, third is verb type
|
|
|
|
|
'<sourceId>': {
|
|
|
|
|
'<targetId>': {
|
|
|
|
|
_: () => {
|
|
|
|
|
// Suggest all available verb types
|
|
|
|
|
return getVerbTypes()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// Add autocomplete for other commands
|
|
|
|
|
search: {},
|
|
|
|
|
get: {},
|
|
|
|
|
delete: {},
|
|
|
|
|
getVerbs: {},
|
|
|
|
|
status: {},
|
|
|
|
|
'completion-setup': {},
|
|
|
|
|
init: {},
|
|
|
|
|
help: {}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Initialize autocomplete
|
|
|
|
|
completion.init()
|
|
|
|
|
|
|
|
|
|
// If this script is run with --completion-setup flag, set up the autocomplete
|
|
|
|
|
if (process.argv.includes('--completion-setup')) {
|
|
|
|
|
completion.setupShellInitFile()
|
|
|
|
|
console.log('Autocomplete setup complete. Please restart your shell.')
|
|
|
|
|
process.exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add a command for setting up autocomplete
|
|
|
|
|
program
|
|
|
|
|
.command('completion-setup')
|
|
|
|
|
.description('Setup shell autocomplete for the Brainy CLI')
|
|
|
|
|
.action(() => {
|
|
|
|
|
completion.setupShellInitFile()
|
|
|
|
|
console.log('Autocomplete setup complete. Please restart your shell.')
|
|
|
|
|
})
|
2025-06-05 11:18:20 -07:00
|
|
|
|
|
|
|
|
// Parse command line arguments
|
2025-06-05 11:44:18 -07:00
|
|
|
program.parse()
|