trace.ai-cli
Version:
A powerful AI-powered CLI tool
1,154 lines (1,021 loc) • 33.6 kB
JavaScript
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;