feat: add infinite agent memory with MCP integration

Implement comprehensive conversation management system enabling AI agents
like Claude Code to maintain infinite context and history. Provides semantic
search, smart context retrieval, and automatic artifact linking using Brainy's
existing Triple Intelligence infrastructure.

Core Features:
- ConversationManager API for message storage and retrieval
- MCP protocol integration with 6 tools for Claude Code
- Context ranking using semantic, temporal, and graph scoring
- Neural clustering for theme discovery and deduplication
- Virtual filesystem integration for code artifact linking
- CLI commands for setup and management

Zero new infrastructure required - uses existing Brainy features:
- Storage via brain.add() with NounType.Message
- Relationships via brain.relate() with VerbType.Precedes
- Search via brain.find() with Triple Intelligence
- Clustering via brain.neural()
- Artifacts via brain.vfs()

One-command setup: brainy conversation setup

Version: 3.19.0
This commit is contained in:
David Snelling 2025-09-29 15:37:11 -07:00
parent e3a21c6075
commit ced639cab1
15 changed files with 3304 additions and 7 deletions

View file

@ -0,0 +1,519 @@
/**
* 💬 Conversation CLI Commands
*
* CLI interface for infinite agent memory and conversation management
*/
import inquirer from 'inquirer'
import chalk from 'chalk'
import ora from 'ora'
import * as fs from '../../universal/fs.js'
import * as path from '../../universal/path.js'
import { Brainy } from '../../brainy.js'
interface CommandArguments {
action?: string
conversationId?: string
query?: string
role?: string
limit?: number
format?: string
output?: string
_: string[]
}
export const conversationCommand = {
command: 'conversation [action]',
describe: '💬 Conversation and context management',
builder: (yargs: any) => {
return yargs
.positional('action', {
describe: 'Conversation operation to perform',
type: 'string',
choices: ['setup', 'search', 'context', 'thread', 'stats', 'export', 'import']
})
.option('conversation-id', {
describe: 'Conversation ID',
type: 'string',
alias: 'c'
})
.option('query', {
describe: 'Search query or context query',
type: 'string',
alias: 'q'
})
.option('role', {
describe: 'Filter by message role',
type: 'string',
choices: ['user', 'assistant', 'system', 'tool'],
alias: 'r'
})
.option('limit', {
describe: 'Maximum results',
type: 'number',
default: 10,
alias: 'l'
})
.option('format', {
describe: 'Output format',
type: 'string',
choices: ['json', 'table', 'text'],
default: 'table',
alias: 'f'
})
.option('output', {
describe: 'Output file path',
type: 'string',
alias: 'o'
})
.example('$0 conversation setup', 'Set up MCP server for Claude Code')
.example('$0 conversation search -q "authentication" -l 5', 'Search messages')
.example('$0 conversation context -q "how to implement JWT"', 'Get relevant context')
.example('$0 conversation thread -c conv_123', 'Get conversation thread')
.example('$0 conversation stats', 'Show conversation statistics')
},
handler: async (argv: CommandArguments) => {
const action = argv.action || 'setup'
try {
switch (action) {
case 'setup':
await handleSetup(argv)
break
case 'search':
await handleSearch(argv)
break
case 'context':
await handleContext(argv)
break
case 'thread':
await handleThread(argv)
break
case 'stats':
await handleStats(argv)
break
case 'export':
await handleExport(argv)
break
case 'import':
await handleImport(argv)
break
default:
console.log(chalk.yellow(`Unknown action: ${action}`))
console.log('Run "brainy conversation --help" for usage information')
}
} catch (error: any) {
console.error(chalk.red(`Error: ${error.message}`))
process.exit(1)
}
}
}
/**
* Handle setup command - Set up MCP server for Claude Code
*/
async function handleSetup(argv: CommandArguments) {
console.log(chalk.bold.cyan('\n🧠 Brainy Infinite Memory Setup\n'))
// Check for existing setup
const homeDir = process.env.HOME || process.env.USERPROFILE || '~'
const brainyDir = path.join(homeDir, '.brainy-memory')
const dataDir = path.join(brainyDir, 'data')
const serverPath = path.join(brainyDir, 'mcp-server.js')
const configPath = path.join(homeDir, '.config', 'claude-code', 'mcp-servers.json')
// Check if already set up
if (await fs.exists(brainyDir)) {
const { overwrite } = await inquirer.prompt([
{
type: 'confirm',
name: 'overwrite',
message: 'Brainy memory setup already exists. Overwrite?',
default: false
}
])
if (!overwrite) {
console.log(chalk.yellow('Setup cancelled'))
return
}
}
const spinner = ora('Creating Brainy memory directory...').start()
try {
// Create directories
await fs.mkdir(brainyDir, { recursive: true })
await fs.mkdir(dataDir, { recursive: true })
spinner.succeed('Created Brainy memory directory')
// Create MCP server script
spinner.start('Creating MCP server script...')
const serverScript = `#!/usr/bin/env node
/**
* Brainy Infinite Memory MCP Server
*
* This server provides conversation and context management
* for Claude Code through the Model Control Protocol (MCP).
*/
import { Brainy } from '@soulcraft/brainy'
import { BrainyMCPService } from '@soulcraft/brainy'
import { MCPConversationToolset } from '@soulcraft/brainy'
async function main() {
try {
// Initialize Brainy with filesystem storage
const brain = new Brainy({
storage: {
type: 'filesystem',
path: '${dataDir.replace(/\\/g, '/')}'
},
silent: true // Suppress console output
})
await brain.init()
// Create MCP service
const mcpService = new BrainyMCPService(brain, {
enableAuth: false // Local usage, no auth needed
})
// Create conversation toolset
const conversationTools = new MCPConversationToolset(brain)
await conversationTools.init()
// Register conversation tools
const tools = await conversationTools.getAvailableTools()
console.error('🧠 Brainy Memory Server started')
console.error(\`📊 \${tools.length} conversation tools available\`)
console.error('✅ Ready for Claude Code integration')
// Handle MCP requests via stdio
process.stdin.on('data', async (data) => {
try {
const request = JSON.parse(data.toString())
// Route conversation tool requests
let response
if (request.toolName && request.toolName.startsWith('conversation_')) {
response = await conversationTools.handleRequest(request)
} else {
response = await mcpService.handleRequest(request)
}
// Write response to stdout
process.stdout.write(JSON.stringify(response) + '\\n')
} catch (error) {
console.error('Error handling request:', error)
}
})
// Handle shutdown gracefully
process.on('SIGINT', () => {
console.error('\\n🛑 Shutting down Brainy Memory Server')
process.exit(0)
})
} catch (error) {
console.error('Failed to start Brainy Memory Server:', error)
process.exit(1)
}
}
main()
`
await fs.writeFile(serverPath, serverScript, { encoding: 'utf8', mode: 0o755 })
spinner.succeed('Created MCP server script')
// Create Claude Code config
spinner.start('Configuring Claude Code...')
const configDir = path.dirname(configPath)
await fs.mkdir(configDir, { recursive: true })
let mcpConfig: any = {}
if (await fs.exists(configPath)) {
const existingConfig = await fs.readFile(configPath, 'utf8')
mcpConfig = JSON.parse(existingConfig)
}
mcpConfig['brainy-memory'] = {
command: 'node',
args: [serverPath],
env: {
NODE_ENV: 'production'
}
}
await fs.writeFile(configPath, JSON.stringify(mcpConfig, null, 2), 'utf8')
spinner.succeed('Configured Claude Code')
// Initialize Brainy database
spinner.start('Initializing Brainy database...')
const brain = new Brainy({
storage: {
type: 'filesystem',
path: dataDir
},
silent: true
})
await brain.init()
spinner.succeed('Initialized Brainy database')
// Success!
console.log(chalk.bold.green('\n✅ Setup complete!\n'))
console.log(chalk.cyan('📁 Memory storage:'), brainyDir)
console.log(chalk.cyan('🔧 MCP server:'), serverPath)
console.log(chalk.cyan('⚙️ Claude Code config:'), configPath)
console.log()
console.log(chalk.bold('🚀 Next steps:'))
console.log(' 1. Restart Claude Code to load the MCP server')
console.log(' 2. Start a new conversation - your history will be saved automatically!')
console.log(' 3. Claude will use past context to help you work faster')
console.log()
console.log(chalk.dim('Run "brainy conversation stats" to see your conversation statistics'))
} catch (error: any) {
spinner.fail('Setup failed')
throw error
}
}
/**
* Handle search command - Search messages
*/
async function handleSearch(argv: CommandArguments) {
if (!argv.query) {
console.log(chalk.yellow('Query required. Use -q or --query'))
return
}
const spinner = ora('Searching conversations...').start()
const brain = new Brainy()
await brain.init()
const conv = brain.conversation()
await conv.init()
const results = await conv.searchMessages({
query: argv.query,
limit: argv.limit || 10,
role: argv.role as any,
includeContent: true,
includeMetadata: true
})
spinner.succeed(`Found ${results.length} messages`)
if (results.length === 0) {
console.log(chalk.yellow('No messages found'))
return
}
// Display results
console.log()
for (const result of results) {
console.log(chalk.bold.cyan(`${result.message.role}:`), result.snippet)
console.log(chalk.dim(` Score: ${result.score.toFixed(3)} | Conv: ${result.conversationId}`))
console.log()
}
}
/**
* Handle context command - Get relevant context
*/
async function handleContext(argv: CommandArguments) {
if (!argv.query) {
console.log(chalk.yellow('Query required. Use -q or --query'))
return
}
const spinner = ora('Retrieving relevant context...').start()
const brain = new Brainy()
await brain.init()
const conv = brain.conversation()
await conv.init()
const context = await conv.getRelevantContext(argv.query, {
limit: argv.limit || 10,
includeArtifacts: true,
includeSimilarConversations: true
})
spinner.succeed(`Retrieved ${context.messages.length} relevant messages`)
if (context.messages.length === 0) {
console.log(chalk.yellow('No relevant context found'))
return
}
// Display context
console.log()
console.log(chalk.bold('📊 Context Statistics:'))
console.log(chalk.dim(` Messages: ${context.messages.length}`))
console.log(chalk.dim(` Tokens: ${context.totalTokens}`))
console.log(chalk.dim(` Query time: ${context.metadata.queryTime}ms`))
console.log()
console.log(chalk.bold('💬 Relevant Messages:'))
for (const msg of context.messages) {
console.log()
console.log(chalk.cyan(`${msg.role} (score: ${msg.relevanceScore.toFixed(3)}):`))
console.log(msg.content.substring(0, 200) + (msg.content.length > 200 ? '...' : ''))
}
if (context.similarConversations && context.similarConversations.length > 0) {
console.log()
console.log(chalk.bold('🔗 Similar Conversations:'))
for (const conv of context.similarConversations) {
console.log(chalk.dim(` - ${conv.title || conv.id} (${conv.relevance.toFixed(2)})`))
}
}
}
/**
* Handle thread command - Get conversation thread
*/
async function handleThread(argv: CommandArguments) {
if (!argv.conversationId) {
console.log(chalk.yellow('Conversation ID required. Use -c or --conversation-id'))
return
}
const spinner = ora('Loading conversation thread...').start()
const brain = new Brainy()
await brain.init()
const conv = brain.conversation()
await conv.init()
const thread = await conv.getConversationThread(argv.conversationId, {
includeArtifacts: true
})
spinner.succeed(`Loaded ${thread.messages.length} messages`)
// Display thread
console.log()
console.log(chalk.bold('📊 Thread Information:'))
console.log(chalk.dim(` Conversation: ${thread.id}`))
console.log(chalk.dim(` Messages: ${thread.metadata.messageCount}`))
console.log(chalk.dim(` Tokens: ${thread.metadata.totalTokens}`))
console.log(chalk.dim(` Started: ${new Date(thread.metadata.startTime).toLocaleString()}`))
console.log()
console.log(chalk.bold('💬 Messages:'))
for (const msg of thread.messages) {
console.log()
console.log(chalk.cyan(`${msg.role}:`), msg.content)
console.log(chalk.dim(` ${new Date(msg.createdAt).toLocaleString()}`))
}
}
/**
* Handle stats command - Show statistics
*/
async function handleStats(argv: CommandArguments) {
const spinner = ora('Calculating statistics...').start()
const brain = new Brainy()
await brain.init()
const conv = brain.conversation()
await conv.init()
const stats = await conv.getConversationStats()
spinner.succeed('Statistics calculated')
// Display stats
console.log()
console.log(chalk.bold.cyan('📊 Conversation Statistics\n'))
console.log(chalk.bold('Overall:'))
console.log(chalk.dim(` Conversations: ${stats.totalConversations}`))
console.log(chalk.dim(` Messages: ${stats.totalMessages}`))
console.log(chalk.dim(` Total Tokens: ${stats.totalTokens.toLocaleString()}`))
console.log(chalk.dim(` Avg Messages/Conversation: ${stats.averageMessagesPerConversation.toFixed(1)}`))
console.log(chalk.dim(` Avg Tokens/Message: ${stats.averageTokensPerMessage.toFixed(1)}`))
console.log()
if (Object.keys(stats.roles).length > 0) {
console.log(chalk.bold('By Role:'))
for (const [role, count] of Object.entries(stats.roles)) {
console.log(chalk.dim(` ${role}: ${count}`))
}
console.log()
}
if (Object.keys(stats.phases).length > 0) {
console.log(chalk.bold('By Phase:'))
for (const [phase, count] of Object.entries(stats.phases)) {
console.log(chalk.dim(` ${phase}: ${count}`))
}
}
}
/**
* Handle export command - Export conversation
*/
async function handleExport(argv: CommandArguments) {
if (!argv.conversationId) {
console.log(chalk.yellow('Conversation ID required. Use -c or --conversation-id'))
return
}
const spinner = ora('Exporting conversation...').start()
const brain = new Brainy()
await brain.init()
const conv = brain.conversation()
await conv.init()
const exported = await conv.exportConversation(argv.conversationId)
const output = argv.output || `conversation_${argv.conversationId}.json`
await fs.writeFile(output, JSON.stringify(exported, null, 2), 'utf8')
spinner.succeed(`Exported to ${output}`)
}
/**
* Handle import command - Import conversation
*/
async function handleImport(argv: CommandArguments) {
const inputFile = argv.output
if (!inputFile) {
console.log(chalk.yellow('Input file required. Use -o or --output'))
return
}
const spinner = ora('Importing conversation...').start()
const brain = new Brainy()
await brain.init()
const conv = brain.conversation()
await conv.init()
const data = JSON.parse(await fs.readFile(inputFile, 'utf8'))
const conversationId = await conv.importConversation(data)
spinner.succeed(`Imported as conversation ${conversationId}`)
}
export default conversationCommand

View file

@ -13,6 +13,7 @@ import { Brainy } from '../brainy.js'
import { neuralCommands } from './commands/neural.js'
import { coreCommands } from './commands/core.js'
import { utilityCommands } from './commands/utility.js'
import conversationCommand from './commands/conversation.js'
import { version } from '../package.json'
// CLI Configuration
@ -137,6 +138,55 @@ program
.option('-o, --output <file>', 'Output file')
.action(neuralCommands.visualize)
// ===== Conversation Commands (Infinite Memory) =====
program
.command('conversation')
.alias('conv')
.description('💬 Infinite agent memory and context management')
.addCommand(
new Command('setup')
.description('Set up MCP server for Claude Code integration')
.action(async () => {
await conversationCommand.handler({ action: 'setup', _: [] })
})
)
.addCommand(
new Command('search')
.description('Search messages across conversations')
.requiredOption('-q, --query <query>', 'Search query')
.option('-c, --conversation-id <id>', 'Filter by conversation')
.option('-r, --role <role>', 'Filter by role')
.option('-l, --limit <number>', 'Maximum results', '10')
.action(async (options) => {
await conversationCommand.handler({ action: 'search', ...options as any, _: [] })
})
)
.addCommand(
new Command('context')
.description('Get relevant context for a query')
.requiredOption('-q, --query <query>', 'Context query')
.option('-l, --limit <number>', 'Maximum messages', '10')
.action(async (options) => {
await conversationCommand.handler({ action: 'context', ...options as any, _: [] })
})
)
.addCommand(
new Command('thread')
.description('Get full conversation thread')
.requiredOption('-c, --conversation-id <id>', 'Conversation ID')
.action(async (options) => {
await conversationCommand.handler({ action: 'thread', ...options as any, _: [] })
})
)
.addCommand(
new Command('stats')
.description('Show conversation statistics')
.action(async () => {
await conversationCommand.handler({ action: 'stats', _: [] })
})
)
// ===== Utility Commands =====
program