UNPKG

trace.ai-cli

Version:

A powerful AI-powered CLI tool

1,154 lines (1,021 loc) 33.6 kB
const readline = require('readline'); const figlet = require('figlet'); const chalk = require('chalk'); const path = require('path'); const fs = require('fs').promises; const { execSync } = require('child_process'); const WebSocket = require('ws'); const { version: packageVersion } = require('../package.json'); const { analyzeFile } = require('../services/fileAnalyzer'); const { analyzeFolderStructure } = require('../services/folderAnalyzer'); const { extractTextFromImage } = require('../services/imageService'); const { processWithAI } = require('../services/aiService'); const AgentHandler = require('../services/agentHandler'); const DomeHandler = require('../services/domeHandler'); const AuthService = require('../services/authService'); const os = require('os'); const { allCommands, mainCommands, memoryCommands, autocompleteCommands, allSlashCommands } = require('../utils/commands'); const styles = require('../utils/styles'); const MEMORY_DIR = path.join(os.homedir(), '.traceai'); const MEMORY_FILE = path.join(MEMORY_DIR, 'memories.json'); class TraceAI { constructor() { this._baseCommandList = autocompleteCommands; this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '', completer: (line) => this.completer(line) }); this.contexts = []; this.activeMemories = new Set(); this.sessionStartTime = new Date(); this.queryCount = 0; this.agent = new AgentHandler(this); this.dome = new DomeHandler(this); this.auth = new AuthService(); this.aiMode = 1; this._lastSuggestionLines = 0; this._selectedIndex = 0; this._currentMatches = []; this._dropdownOpen = false; this._dropdownType = 'command'; // 'command', 'file', or 'agent' this._fileCache = []; this._agentCache = []; this._selectedFiles = new Set(); // multi-select for @ picker this._placeholder = 'Type your message or /'; this._placeholderVisible = false; this._setupKeypress(); } get commandList() { return this._baseCommandList.map(c => { if (c.cmd === '/agent') { return this.auth && this.auth.isLoggedIn ? { cmd: '/agent <url>', desc: 'Connect to agent' } : { cmd: '/agent', desc: 'Sign in with Google' }; } return c; }); } _setupKeypress() { const origWrite = this.rl._ttyWrite.bind(this.rl); this.rl._ttyWrite = (s, key) => { if (!key) { origWrite(s, key); return; } // Escape closes dropdown if (key.name === 'escape' && this._dropdownOpen) { this._clearSuggestions(); return; } // Enter: if dropdown open, accept selected if (key.name === 'return' && this._dropdownOpen && this._currentMatches.length > 0) { const idx = Math.min(this._selectedIndex, this._currentMatches.length - 1); const selected = this._currentMatches[idx]; if (!selected) { this._clearSuggestions(); origWrite(s, key); return; } if (this._dropdownType === 'agent') { // Send selected agent's workflow to the connected agent const currentLine = this.rl.line || ''; const atIdx = currentLine.lastIndexOf('@'); const before = currentLine.substring(0, atIdx); this._clearSuggestions(); this.rl.write(null, { ctrl: true, name: 'u' }); this.rl.write(before + '@' + selected.cmd + ' '); } else if (this._dropdownType === 'file') { // Enter in file picker: if files are selected, confirm them; otherwise select current + confirm if (this._selectedFiles.size === 0) { this._selectedFiles.add(selected.cmd); } const currentLine = this.rl.line || ''; const atIdx = currentLine.lastIndexOf('@'); const before = currentLine.substring(0, atIdx); const filePaths = [...this._selectedFiles].join(' '); this._selectedFiles.clear(); this._clearSuggestions(); this.rl.write(null, { ctrl: true, name: 'u' }); this.rl.write(before + filePaths + ' '); } else { this._clearSuggestions(); this.rl.write(null, { ctrl: true, name: 'u' }); this.rl.write(selected.cmd + ' '); } return; // block Enter from submitting } // Enter without dropdown: just clear any leftover if (key.name === 'return') { this._clearSuggestions(); origWrite(s, key); return; } // Arrow up in dropdown if (key.name === 'up' && this._dropdownOpen) { this._selectedIndex = Math.max(0, this._selectedIndex - 1); this._renderDropdown(); return; // don't let readline handle up (history) } // Arrow down in dropdown if (key.name === 'down' && this._dropdownOpen) { this._selectedIndex = Math.min(this._currentMatches.length - 1, this._selectedIndex + 1); this._renderDropdown(); return; // don't let readline handle down (history) } // Tab to accept selected suggestion if (key.name === 'tab' && this._dropdownOpen && this._currentMatches.length > 0) { const idx = Math.min(this._selectedIndex, this._currentMatches.length - 1); const selected = this._currentMatches[idx]; if (!selected) { this._clearSuggestions(); return; } if (this._dropdownType === 'agent') { const currentLine = this.rl.line || ''; const atIdx = currentLine.lastIndexOf('@'); const before = currentLine.substring(0, atIdx); this._clearSuggestions(); this.rl.write(null, { ctrl: true, name: 'u' }); this.rl.write(before + '@' + selected.cmd + ' '); return; } else if (this._dropdownType === 'file') { // Tab in file picker: toggle selection and keep dropdown open if (this._selectedFiles.has(selected.cmd)) { this._selectedFiles.delete(selected.cmd); } else { this._selectedFiles.add(selected.cmd); } this._renderDropdown(); return; } else { this._clearSuggestions(); this.rl.write(null, { ctrl: true, name: 'u' }); this.rl.write(selected.cmd + ' '); } return; } // Clear placeholder on any input if (this._placeholderVisible) { this._clearPlaceholder(); } // Let readline handle the key normally first origWrite(s, key); // Then update suggestions based on new line content setImmediate(() => { // Restore placeholder if line becomes empty if (!(this.rl.line || '').length && !this._dropdownOpen) { this._showPlaceholder(); } const currentLine = this.rl.line || ''; if (currentLine.startsWith('/') && currentLine.length >= 1) { this._dropdownType = 'command'; this._updateSuggestions(currentLine); } else if (currentLine.includes('@')) { this._handleAtPicker(currentLine); } else { this._clearSuggestions(); } }); }; } _updateSuggestions(partial) { const trimmed = partial.trim(); // Check if user typed a base command + space → show sub-commands const baseCmd = trimmed.split(' ')[0]; const hasSpace = partial.includes(' '); let matches; if (hasSpace && this.commandList.some(c => c.cmd === baseCmd)) { // Show sub-commands from full list matches = allSlashCommands.filter(c => c.cmd.startsWith(trimmed) && c.cmd !== trimmed ).slice(0, 8); } else { // Show base commands matches = this.commandList.filter(c => c.cmd.startsWith(trimmed) || c.cmd.includes(trimmed) ).slice(0, 8); // Close if typed text exactly matches a command if (matches.some(m => m.cmd === trimmed)) { this._clearSuggestions(); return; } } this._currentMatches = matches; this._dropdownType = 'command'; this._selectedIndex = Math.min(this._selectedIndex, matches.length - 1); this._dropdownOpen = true; this._renderDropdown(); } async _handleAtPicker(currentLine) { // If connected to agent, show trained agents dropdown if (this.agent.connected) { if (!this.auth.isLoggedIn) { this._currentMatches = [{ cmd: '/auth', desc: 'Sign in to see agents' }]; this._dropdownType = 'command'; this._selectedIndex = 0; this._dropdownOpen = true; this._renderDropdown(); return; } const atIdx = currentLine.lastIndexOf('@'); const partial = currentLine.substring(atIdx + 1).toLowerCase(); if (this._agentCache.length === 0) { // Show loading message process.stdout.write('\n' + chalk.dim(' Loading your trained agents...')); this._lastSuggestionLines = 1; this._agentCache = await this.auth.fetchAgents(); // Clear loading message process.stdout.write('\x1b[2K\r'); process.stdout.write('\x1b[1A'); readline.cursorTo(process.stdout, this._getPromptLength() + (this.rl.line || '').length); this._lastSuggestionLines = 0; } if (this._agentCache.length === 0) { // No agents found - show as temporary dropdown message this._currentMatches = [{ cmd: 'No trained agents found', desc: '' }]; this._dropdownType = 'command'; this._selectedIndex = 0; this._dropdownOpen = true; this._renderDropdown(); setTimeout(() => this._clearSuggestions(), 2000); return; } const matches = this._agentCache .filter(a => a.agent_name.toLowerCase().includes(partial)) .map(a => ({ cmd: a.agent_name, desc: `${(a.workflow || []).length} steps`, id: a.id, workflow: a.workflow })); if (matches.length === 0) { this._clearSuggestions(); return; } this._currentMatches = matches; this._dropdownType = 'agent'; this._selectedIndex = Math.min(this._selectedIndex, matches.length - 1); this._dropdownOpen = true; this._renderDropdown(); return; } // Otherwise show files from active memories const atIdx = currentLine.lastIndexOf('@'); const partial = currentLine.substring(atIdx + 1).toLowerCase(); // Build file list from active memory contexts await this._buildFileCache(); const matches = this._fileCache .filter(f => f.cmd.toLowerCase().includes(partial) || f.desc.toLowerCase().includes(partial)); if (matches.length === 0) { this._clearSuggestions(); return; } this._currentMatches = matches; this._dropdownType = 'file'; this._selectedIndex = Math.min(this._selectedIndex, matches.length - 1); this._dropdownOpen = true; this._renderDropdown(); } async _buildFileCache() { this._fileCache = []; for (let idx = 0; idx < this.contexts.length; idx++) { if (!this.activeMemories.has(idx + 1)) continue; const ctx = this.contexts[idx]; if (ctx.type === 'folder' && ctx.content) { const lines = ctx.content.split('\n'); const folderName = ctx.filename || 'folder'; const folderPath = ctx.folderPath || ''; for (let i = 1; i < lines.length; i++) { const item = lines[i].trim(); if (item) { const fullPath = folderPath ? path.join(folderPath, item) : item; this._fileCache.push({ cmd: fullPath, desc: folderName }); } } } else if (ctx.type === 'file') { const fullPath = ctx.filePath || ctx.filename || 'unknown'; this._fileCache.push({ cmd: fullPath, desc: 'file' }); } } } _renderDropdown() { // Clear previous this._clearSuggestionsRaw(); const matches = this._currentMatches; if (matches.length === 0) return; process.stdout.write('\n'); let lineCount = 1; if (this._dropdownType === 'file') { // Group files by folder (desc field) const groups = new Map(); for (const m of matches) { const group = m.desc || 'files'; if (!groups.has(group)) groups.set(group, []); groups.get(group).push(m); } // Render hint process.stdout.write(chalk.dim(' Tab=toggle select, Enter=confirm') + '\n'); lineCount++; let flatIdx = 0; for (const [folder, files] of groups) { process.stdout.write(chalk.bold.cyan(` ${folder}/`) + '\n'); lineCount++; for (const f of files) { const isSelected = flatIdx === this._selectedIndex; const isChecked = this._selectedFiles.has(f.cmd); const check = isChecked ? chalk.green('[x]') : chalk.gray('[ ]'); const prefix = isSelected ? chalk.cyan(' > ') : ' '; const name = path.basename(f.cmd); const line = isSelected ? chalk.bgGray.white(name) : chalk.white(name); process.stdout.write(`${prefix}${check} ${line}\n`); lineCount++; flatIdx++; } } } else if (this._dropdownType === 'agent') { process.stdout.write(chalk.dim(' Select a trained agent') + '\n'); lineCount++; matches.forEach((m, i) => { const isSelected = i === this._selectedIndex; const prefix = isSelected ? chalk.cyan(' > ') : ' '; const name = isSelected ? chalk.bgGray.white(m.cmd) : chalk.white(m.cmd); const desc = chalk.gray(m.desc); process.stdout.write(`${prefix}${name} ${desc}\n`); lineCount++; }); } else { matches.forEach((m, i) => { const isSelected = i === this._selectedIndex; const prefix = isSelected ? chalk.cyan(' ') : ' '; const line = isSelected ? chalk.bgGray.white(`${m.cmd.padEnd(20)} ${m.desc}`) : chalk.gray(`${m.cmd.padEnd(20)} ${m.desc}`); process.stdout.write(`${prefix}${line}\n`); }); lineCount += matches.length; } this._lastSuggestionLines = lineCount; // Move cursor back up process.stdout.write(`\x1b[${this._lastSuggestionLines}A`); readline.cursorTo(process.stdout, this._getPromptLength() + (this.rl.line || '').length); } _clearSuggestions() { if (this._lastSuggestionLines > 0) { this._clearSuggestionsRaw(); this._lastSuggestionLines = 0; } this._dropdownOpen = false; this._dropdownType = 'command'; this._currentMatches = []; this._selectedIndex = 0; this._selectedFiles.clear(); } _clearSuggestionsRaw() { if (this._lastSuggestionLines > 0) { // Save position, clear lines below, restore const saved = `\x1b7`; // save cursor const restored = `\x1b8`; // restore cursor process.stdout.write(saved); for (let i = 0; i < this._lastSuggestionLines; i++) { process.stdout.write('\n\x1b[2K'); // move down + clear line } process.stdout.write(restored); } } _getPromptLength() { return this.getPromptPlain().length; } getPrompt() { return styles.prompt(this.activeMemories.size, this.contexts.length); } getPromptPlain() { return styles.promptPlain(this.activeMemories.size, this.contexts.length); } restorePrompt() { process.stdout.write(this.getPrompt() + (this.rl.line || '')); if (!(this.rl.line || '').length) { this._showPlaceholder(); } } _showPlaceholder() { if (this._placeholderVisible) return; const ph = styles.palette.muted(this._placeholder); process.stdout.write(ph); // Move cursor back to start of placeholder process.stdout.write(`\x1b[${this._placeholder.length}D`); this._placeholderVisible = true; } _clearPlaceholder() { if (!this._placeholderVisible) return; // Erase the placeholder text process.stdout.write(`\x1b[${this._placeholder.length}C`); process.stdout.write(`\x1b[${this._placeholder.length}D`); process.stdout.write(' '.repeat(this._placeholder.length)); process.stdout.write(`\x1b[${this._placeholder.length}D`); this._placeholderVisible = false; } completer(line) { if (!line.startsWith('/')) return [[], line]; const matches = this.commandList .map(c => c.cmd) .filter(c => c.startsWith(line)); return [matches.length ? matches : this.commandList.map(c => c.cmd), line]; } // Enhanced startup with animated loading async start() { // Clear screen for clean start console.clear(); // Ctrl+C: disconnect agent first press, exit second press this.rl.on('SIGINT', () => { if (this.agent.connected) { this.agent.disconnect(); } else { this.close(); } }); // Load saved memories await this.loadMemories(); // Load saved auth await this.auth.load(); // Animated startup sequence await this.showLoadingAnimation(); // Main header with enhanced ASCII art await this.displayHeader(); // Show welcome message and commands this.displayWelcomeMessage(); // Start the interactive session this.promptUser(); } async loadMemories() { try { const data = await fs.readFile(MEMORY_FILE, 'utf8'); this.contexts = JSON.parse(data); // Activate all memories by default this.activeMemories = new Set(this.contexts.map((_, i) => i + 1)); } catch { this.contexts = []; this.activeMemories = new Set(); } } async saveMemories() { try { await fs.mkdir(MEMORY_DIR, { recursive: true }); await fs.writeFile(MEMORY_FILE, JSON.stringify(this.contexts, null, 2)); this._fileCache = []; } catch (error) { console.error(chalk.yellow('Could not save memories:', error.message)); } } async showLoadingAnimation() { const messages = [ 'Initializing Trace.AI...', 'Loading AI modules...', 'Preparing analysis engines...', 'Ready to trace!' ]; await styles.playBootSequence(messages); } async displayHeader() { const asciiArt = figlet.textSync('TRACE.AI', { font: 'ANSI Shadow', horizontalLayout: 'fitted', verticalLayout: 'default' }); console.log(styles.hero(asciiArt, packageVersion)); } displayWelcomeMessage() { console.log(styles.welcomeBlock(mainCommands)); this.displayStatusBar(); } displayStatusBar() { const contextCount = this.contexts.length; const modeNames = { 1: 'Fast', 2: 'Balanced', 3: 'Think' }; const userName = this.auth.isLoggedIn ? (this.auth.user?.user_metadata?.full_name || this.auth.user?.email || 'User') : 'Not signed in'; console.log(styles.statusBlock({ userName, modeName: modeNames[this.aiMode], contextCount, activeCount: this.activeMemories.size, })); } promptUser() { if (this.rl.closed) { return; } this.rl.question(this.getPrompt(), async (input) => { this._clearPlaceholder(); this._clearSuggestions(); try { if (input.trim()) { this.queryCount++; await this.handleInput(input); } } catch (error) { this.displayError(error.message); } if (!this.rl.closed) { this.promptUser(); } }); setImmediate(() => this._showPlaceholder()); } async handleInput(input) { const trimmedInput = input.trim(); if (!trimmedInput) return; // Command routing with enhanced feedback const commands = { '/memory': () => this.handleMemoryCommand(trimmedInput), '/clear': () => this.handleClearCommand(trimmedInput), '/help': () => this.displayHelp(), '/stats': () => this.displayStats(), '/system': () => this.handleSystemCommand(trimmedInput), '/mode': () => this.handleModeCommand(trimmedInput), '/agent': () => this.handleAgentCommand(trimmedInput), '/auth': () => this.handleAuthCommand(trimmedInput), '/exit': () => this.handleExit() }; // Find and execute command const commandKey = Object.keys(commands).find(cmd => trimmedInput.startsWith(cmd)); if (commandKey) { await commands[commandKey](); } else if (trimmedInput.startsWith('!')) { await this.handleShellCommand(trimmedInput); } else if (this.agent.connected) { // Check for @agent_name to include workflow context in query if (trimmedInput.startsWith('@') && this._agentCache.length > 0) { const inputAfterAt = trimmedInput.slice(1); // Match against actual agent names (longest first to handle multi-word names) const sorted = [...this._agentCache].sort((a, b) => b.agent_name.length - a.agent_name.length); const trainedAgent = sorted.find(a => inputAfterAt.toLowerCase().startsWith(a.agent_name.toLowerCase())); if (trainedAgent && trainedAgent.workflow) { const query = inputAfterAt.slice(trainedAgent.agent_name.length).trim(); const context = `[Agent: ${trainedAgent.agent_name}]\n[Workflow: ${JSON.stringify(trainedAgent.workflow)}]\n\n${query}`; this.agent.send(context); } else { this.agent.send(trimmedInput); } } else { this.agent.send(trimmedInput); } } else { // Auto-detect path: try to resolve the first argument as a file/folder await this.handleAutoDetect(trimmedInput); } } async handleShellCommand(input) { const cmd = input.substring(1).trim(); if (!cmd) { console.log(chalk.yellow('Usage: !<command> (e.g., !ls, !npm run start)')); return; } console.log(chalk.dim(`$ ${cmd}`)); try { const output = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 30000, shell: true, cwd: process.cwd() }); if (output.trim()) console.log(output.trimEnd()); } catch (err) { if (err.stdout && err.stdout.trim()) console.log(err.stdout.trimEnd()); if (err.stderr && err.stderr.trim()) console.log(chalk.red(err.stderr.trimEnd())); if (err.status !== undefined) console.log(chalk.dim(`Exit code: ${err.status}`)); } } async handleAutoDetect(input) { const parts = this.parseCommand(input); const firstArg = this.cleanPath(parts[0]); const resolvedPath = path.resolve(firstArg); try { const stats = await fs.stat(resolvedPath); const query = parts.slice(1).join(' ').replace(/^"|"$/g, ''); if (stats.isDirectory()) { // Folder analysis await this.executeWithSpinner( `Analyzing folder: ${path.basename(resolvedPath)}`, async () => { const result = await analyzeFolderStructure(resolvedPath, query || 'Analyze this folder structure'); this.displayResult('Folder Analysis', result.text, firstArg); } ); } else if (stats.isFile()) { const { getFileType } = require('../utils/fileUtils'); const fileType = getFileType(resolvedPath); if (fileType === 'image') { // Image analysis await this.executeWithSpinner( `Analyzing image: ${path.basename(resolvedPath)}`, async () => { const result = await extractTextFromImage(resolvedPath, query || 'Describe this image', this.aiMode); this.displayResult('Image Analysis', result, firstArg); } ); } else { // File analysis (code, text, document, etc.) await this.executeWithSpinner( `Analyzing file: ${path.basename(resolvedPath)}`, async () => { const result = await analyzeFile(resolvedPath, query || 'Analyze this file', this.aiMode); this.displayResult('File Analysis', result.text, firstArg); } ); } } } catch (err) { // Not a valid path — treat as a text query await this.handleTextQuery(input); } } async handleMemoryCommand(input) { const rest = input.substring('/memory'.length).trim(); if (!rest || rest === 'show') { // /memory show this.displayMemories(); return; } // /memory 0 → deselect all active memories if (rest === '0') { this.activeMemories.clear(); console.log(chalk.green(' All memories deselected')); return; } // Check for wss:// or ws:// URL if (rest.startsWith('wss://') || rest.startsWith('ws://')) { this.contexts.push({ type: 'wss', content: rest, filename: rest.split('/').pop() || 'websocket', url: rest, timestamp: new Date().toLocaleTimeString() }); this.activeMemories.add(this.contexts.length); await this.saveMemories(); console.log(chalk.green(` Source added to memory`)); return; } // Check for index selection: /memory 1 or /memory 1,2,3 const indexMatch = rest.match(/^[\d,\s]+$/); if (indexMatch) { const indices = rest.split(/[,\s]+/).map(Number).filter(n => !isNaN(n) && n >= 1 && n <= this.contexts.length); if (indices.length === 0) { console.log(chalk.yellow(`Invalid index. You have ${this.contexts.length} memories (1-${this.contexts.length})`)); return; } this.activeMemories = new Set(indices); const names = indices.map(i => { const ctx = this.contexts[i - 1]; return ctx.filename || `"${ctx.content.substring(0, 30)}..."`; }); console.log(chalk.green(` Active memories: [${indices.join(', ')}] ${names.join(', ')}`)); return; } // Check if it's quoted text: /memory "some text" const quotedMatch = rest.match(/^"(.+)"$/); if (quotedMatch) { const text = quotedMatch[1]; this.contexts.push({ type: 'text', content: text, timestamp: new Date().toLocaleTimeString() }); console.log(chalk.green(' Memory added successfully')); this.activeMemories.add(this.contexts.length); await this.saveMemories(); return; } // Otherwise treat as a file/folder path const filePath = this.cleanPath(rest); try { const resolvedPath = path.resolve(filePath); const stats = await fs.stat(resolvedPath); if (stats.isDirectory()) { const items = await fs.readdir(resolvedPath); const tree = items.map(item => ` ${item}`).join('\n'); const content = `Folder ${path.basename(resolvedPath)}:\n${tree}`; this.contexts.push({ type: 'folder', content, filename: path.basename(resolvedPath), folderPath: resolvedPath, timestamp: new Date().toLocaleTimeString() }); console.log(chalk.green(` Folder ${path.basename(resolvedPath)} (${items.length} items) added to memory`)); } else { const content = await fs.readFile(resolvedPath, 'utf8'); this.contexts.push({ type: 'file', content: `File ${path.basename(resolvedPath)}:\n${content}`, filename: path.basename(resolvedPath), filePath: resolvedPath, timestamp: new Date().toLocaleTimeString() }); console.log(chalk.green(` File ${path.basename(resolvedPath)} added to memory`)); } this.activeMemories.add(this.contexts.length); await this.saveMemories(); } catch (error) { this.displayError(`Error reading path: ${error.message}`); } } displayMemories() { console.log(styles.memoriesBlock(this.contexts, this.activeMemories)); console.log(styles.helpBlock(memoryCommands, 'Memory Commands', 50)); } async handleClearCommand(input) { const arg = input.substring('/clear'.length).trim(); if (!arg) { // /clear → clear screen console.clear(); await this.displayHeader(); return; } // /clear m → clear all memories // /clear m1,2,3 → clear specific memories const memoryMatch = arg.match(/^m([\d,\s]*)$/); if (memoryMatch) { const indexStr = memoryMatch[1].trim(); if (!indexStr) { // /clear m → clear all this.contexts = []; this.activeMemories = new Set(); await this.saveMemories(); console.log(chalk.green(' All memories cleared')); } else { // /clear m1,2,3 → clear specific const indices = indexStr.split(/[,\s]+/).map(Number).filter(n => !isNaN(n) && n >= 1 && n <= this.contexts.length); if (indices.length === 0) { console.log(chalk.yellow(`Invalid index. You have ${this.contexts.length} memories (1-${this.contexts.length})`)); return; } // Remove in reverse order to preserve indices const sortedDesc = [...new Set(indices)].sort((a, b) => b - a); const removed = []; for (const idx of sortedDesc) { removed.push(this.contexts[idx - 1].filename || `memory ${idx}`); this.contexts.splice(idx - 1, 1); } // Rebuild active memories set this.activeMemories = new Set( [...this.activeMemories] .filter(i => !indices.includes(i)) .map(i => { let newIdx = i; for (const idx of sortedDesc) { if (i > idx) newIdx--; } return newIdx; }) .filter(i => i >= 1 && i <= this.contexts.length) ); await this.saveMemories(); console.log(chalk.green(` Cleared: ${removed.reverse().join(', ')}`)); } return; } console.log(chalk.cyan('Usage: /clear (screen) | /clear m (all memories) | /clear m1,2 (specific)')); } displayHelp() { console.log(styles.helpBlock(allCommands)); } displayStats() { const uptime = Math.floor((new Date() - this.sessionStartTime) / 1000); const minutes = Math.floor(uptime / 60); const seconds = uptime % 60; console.log(styles.statsBlock({ minutes, seconds, queryCount: this.queryCount, contextCount: this.contexts.length, width: process.stdout.columns || 'Unknown', startedAt: this.sessionStartTime.toLocaleString(), })); } async handleSystemCommand(input) { const query = input.substring('/system'.length).trim(); await this.executeWithSpinner( 'Retrieving system information', async () => { const result = await processWithAI(query ? `Get system information about ${query}` : 'Get system information', '', this.aiMode); this.displayResult('System Information', result); } ); } async handleTextQuery(input) { // Check if any active memory is a wss source const wssMemory = this.contexts.find((ctx, i) => this.activeMemories.has(i + 1) && ctx.type === 'wss' ); if (wssMemory) { await this.executeWithSpinner( 'Processing your query', async () => { const result = await this.dome.query(wssMemory.url, input); this.displayResult('Trace.Ai Response', result); } ); return; } await this.executeWithSpinner( 'Processing your query', async () => { const context = this.contexts .filter((_, i) => this.activeMemories.has(i + 1)) .map(ctx => ctx.content) .join('\n\n'); const result = await processWithAI(input, context, this.aiMode); this.displayResult('Trace.Ai Response', result.text || result); } ); } handleModeCommand(input) { const parts = this.parseCommand(input); if (!this.validateCommand(parts, 2, 'Usage: /mode <1|2|3> (1=Fast, 2=Balanced, 3=Think)')) return; const mode = parseInt(parts[1]); if (![1, 2, 3].includes(mode)) { console.log(chalk.yellow('Invalid mode. Use 1 (Fast), 2 (Balanced), or 3 (Think)')); return; } this.aiMode = mode; const modeNames = { 1: 'Fast', 2: 'Balanced', 3: 'Think' }; console.log(chalk.green(`AI mode set to: ${modeNames[mode]}`)); } async handleAgentCommand(input) { if (!this.auth.isLoggedIn) { console.log(chalk.bold.cyan('\n Sign in required to connect to an agent\n')); console.log(chalk.cyan(' > ') + chalk.bgGray.white('1. Sign in with Google')); console.log(chalk.gray(' (Press Enter to sign in)\n')); const confirmed = await new Promise((resolve) => { this.rl.question(chalk.gray(' '), (answer) => { resolve(answer.trim().toLowerCase() !== 'n'); }); }); if (!confirmed) { console.log(chalk.yellow(' Sign in cancelled')); return; } try { await this.auth.loginWithGoogle(); const name = this.auth.user?.user_metadata?.full_name || this.auth.user?.email || 'User'; console.log(chalk.green(` Signed in as ${name}`)); console.log(chalk.gray(' Now use: /agent <url>\n')); } catch (err) { this.displayError(`Sign in failed: ${err.message}`); } return; } const parts = this.parseCommand(input); if (parts.length < 2) { console.log(chalk.cyan('Usage: /agent <url>')); return; } const url = this.cleanPath(parts[1]); await this.agent.connect(url); } async handleAuthCommand(input) { const arg = (input || '').substring('/auth'.length).trim(); if (arg === 'out') { if (!this.auth.isLoggedIn) { console.log(chalk.yellow(' Not signed in')); return; } await this.auth.logout(); console.log(chalk.green(' Signed out')); return; } if (!this.auth.isLoggedIn) { console.log(chalk.bold.cyan('\nAUTH')); console.log(chalk.gray('-'.repeat(50))); console.log(chalk.yellow(' Not signed in')); console.log(); console.log(chalk.cyan(' > ') + chalk.bgGray.white('1. Sign in with Google')); console.log(chalk.gray(' (Press Enter to sign in)\n')); const confirmed = await new Promise((resolve) => { this.rl.question(chalk.gray(' '), (answer) => { resolve(answer.trim().toLowerCase() !== 'n'); }); }); if (!confirmed) return; try { await this.auth.loginWithGoogle(); const name = this.auth.user?.user_metadata?.full_name || this.auth.user?.email || 'User'; console.log(chalk.green(` Signed in as ${name}`)); } catch (err) { this.displayError(`Sign in failed: ${err.message}`); } return; } const user = this.auth.user || {}; const name = user.user_metadata?.full_name || 'Unknown'; const email = user.email || 'Unknown'; const avatar = user.user_metadata?.avatar_url || ''; const provider = user.app_metadata?.provider || 'Unknown'; console.log(chalk.bold.cyan('\nAUTH')); console.log(chalk.gray('-'.repeat(50))); console.log(` ${chalk.white('Name:')} ${chalk.green(name)}`); console.log(` ${chalk.white('Email:')} ${chalk.green(email)}`); console.log(` ${chalk.white('Provider:')} ${chalk.green(provider)}`); console.log(chalk.gray('-'.repeat(50))); console.log(); console.log(chalk.gray(' Type /auth out to sign out')); } async handleLogin() { if (this.auth.isLoggedIn) { const name = this.auth.user?.user_metadata?.full_name || this.auth.user?.email || 'User'; console.log(chalk.green(` Already signed in as ${name}`)); return; } try { await this.auth.loginWithGoogle(); const name = this.auth.user?.user_metadata?.full_name || this.auth.user?.email || 'User'; console.log(chalk.green(` Signed in as ${name}`)); } catch (err) { this.displayError(`Sign in failed: ${err.message}`); } } async handleLogout() { if (!this.auth.isLoggedIn) { console.log(chalk.yellow(' Not signed in')); return; } await this.auth.logout(); console.log(chalk.green(' Signed out')); } handleExit() { if (this.agent.connected) { this.agent.disconnect(); } else { this.close(); } } safeLog(message) { process.stdout.write('\r\x1b[K'); console.log(message); this.restorePrompt(); } // Utility methods parseCommand(input) { return input.match(/(?:[^\s"]+|"[^"]*")+/g) || []; } cleanPath(path) { // First remove quotes from beginning and end let cleanedPath = path.replace(/^"|"$/g, ''); // Then handle escaped spaces and other escaped characters cleanedPath = cleanedPath.replace(/\\([ \(\)\[\]\{\}\&\^\%\$\#\@\!\,\.\;\:\'\"])/g, '$1'); return cleanedPath; } validateCommand(parts, minLength, usage) { if (!parts || parts.length < minLength) { console.log(chalk.cyan(usage)); return false; } return true; } async executeWithSpinner(message, task) { const spinner = styles.startSpinner(message); try { await task(); styles.stopSpinnerSuccess(spinner, message); } catch (error) { styles.stopSpinnerError(spinner, message); this.displayError(error.message); } } displayResult(title, content, filePath = null) { const { markdownToAnsi } = require('../utils/markdown'); console.log(styles.resultHeader(title, filePath)); // Use markdown renderer if content looks like markdown const formatted = content.includes('**') || content.includes('```') || content.includes('- ') ? markdownToAnsi(content) : chalk.white(content); console.log(formatted); console.log(styles.resultFooter()); this.restorePrompt(); } displayError(message) { console.log(styles.errorBlock(message)); this.restorePrompt(); } close() { const farewell = figlet.textSync('Goodbye!', { font: 'Small', horizontalLayout: 'fitted' }); const uptime = Math.floor((new Date() - this.sessionStartTime) / 1000); const minutes = Math.floor(uptime / 60); const seconds = uptime % 60; console.log(styles.farewell(farewell, [ `Session Duration: ${minutes}m ${seconds}s`, `Total Queries: ${this.queryCount}`, `Memories Used: ${this.contexts.length}`, 'Visit us at: https://traceai.netlify.app', ])); if (this.agent.ws) { this.agent.ws.close(); this.agent.ws = null; } this.dome.disconnect(); this.rl.close(); process.exit(0); } } module.exports = TraceAI;