visaire
Version:
A fully autonomous agentic CLI tool for interacting with large language models (Claude, Gemini, GPT) that can execute multi-step tasks automatically
1,100 lines (958 loc) • 29.5 kB
JavaScript
const EventEmitter = require('events');
const fs = require('fs-extra');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const ignore = require('ignore');
const glob = require('glob');
/**
* Advanced context management for intelligent code understanding and context building
* Implements Forge-style context awareness with AST parsing and semantic analysis
*/
class ContextManager extends EventEmitter {
constructor(options = {}) {
super();
this.logger = options.logger;
this.maxSize = options.maxSize || 100000; // Max context size in characters
this.compressionThreshold = options.compressionThreshold || 0.8;
this.retentionStrategy = options.retentionStrategy || 'importance';
this.workingDirectory = options.workingDirectory || process.cwd();
// Context storage
this.contexts = new Map();
this.fileCache = new Map();
this.astCache = new Map();
this.dependencyGraph = new Map();
// Ignore patterns
this.ignorePatterns = this.initializeIgnorePatterns();
// File type handlers
this.fileHandlers = this.initializeFileHandlers();
// Context templates
this.contextTemplates = this.initializeContextTemplates();
}
/**
* Initialize ignore patterns for file traversal
*/
initializeIgnorePatterns() {
const ig = ignore();
// Default ignore patterns
ig.add([
'node_modules/**',
'.git/**',
'.visaire/**',
'dist/**',
'build/**',
'coverage/**',
'*.log',
'.DS_Store',
'Thumbs.db',
'*.tmp',
'*.temp'
]);
// Load .gitignore if it exists
try {
const gitignorePath = path.join(this.workingDirectory, '.gitignore');
if (fs.existsSync(gitignorePath)) {
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
ig.add(gitignoreContent);
}
} catch (error) {
if (this.logger) {
this.logger.warn('Failed to load .gitignore', { error: error.message });
}
}
// Load .visaireignore if it exists
try {
const visaireignorePath = path.join(this.workingDirectory, '.visaireignore');
if (fs.existsSync(visaireignorePath)) {
const visaireIgnoreContent = fs.readFileSync(visaireignorePath, 'utf8');
ig.add(visaireIgnoreContent);
}
} catch (error) {
// Silently ignore if .visaireignore doesn't exist
}
return ig;
}
/**
* Initialize file type handlers
*/
initializeFileHandlers() {
return {
'.js': this.handleJavaScriptFile.bind(this),
'.ts': this.handleTypeScriptFile.bind(this),
'.jsx': this.handleJavaScriptFile.bind(this),
'.tsx': this.handleTypeScriptFile.bind(this),
'.json': this.handleJsonFile.bind(this),
'.md': this.handleMarkdownFile.bind(this),
'.txt': this.handleTextFile.bind(this),
'.yml': this.handleYamlFile.bind(this),
'.yaml': this.handleYamlFile.bind(this),
'.html': this.handleHtmlFile.bind(this),
'.css': this.handleCssFile.bind(this),
'.py': this.handlePythonFile.bind(this),
'.default': this.handleDefaultFile.bind(this)
};
}
/**
* Initialize context templates
*/
initializeContextTemplates() {
return {
fileSystem: {
name: 'File System Context',
priority: 1,
builder: this.buildFileSystemContext.bind(this)
},
codeStructure: {
name: 'Code Structure Context',
priority: 2,
builder: this.buildCodeStructureContext.bind(this)
},
dependencies: {
name: 'Dependencies Context',
priority: 3,
builder: this.buildDependenciesContext.bind(this)
},
conversation: {
name: 'Conversation Context',
priority: 4,
builder: this.buildConversationContext.bind(this)
},
environment: {
name: 'Environment Context',
priority: 5,
builder: this.buildEnvironmentContext.bind(this)
}
};
}
/**
* Build comprehensive context for reasoning
*/
async buildContext(options = {}) {
const startTime = Date.now();
const contextId = uuidv4();
try {
if (this.logger) {
this.logger.debug('Building context', { id: contextId, options });
}
let context = {
id: contextId,
timestamp: new Date().toISOString(),
workingDirectory: this.workingDirectory,
input: options.input,
conversation: options.conversation,
options: options.options || {},
sections: {}
};
// Build each context section
for (const [sectionName, template] of Object.entries(this.contextTemplates)) {
try {
const sectionContext = await template.builder(options);
context.sections[sectionName] = {
...sectionContext,
priority: template.priority,
timestamp: new Date().toISOString()
};
} catch (error) {
if (this.logger) {
this.logger.warn(`Failed to build ${sectionName} context`, { error: error.message });
}
context.sections[sectionName] = {
error: error.message,
priority: template.priority
};
}
}
// Calculate total size and compress if needed
const totalSize = this.calculateContextSize(context);
if (totalSize > this.maxSize) {
context = await this.compressContext(context);
}
// Store context
this.contexts.set(contextId, context);
const buildTime = Date.now() - startTime;
if (this.logger) {
this.logger.info('Context built successfully', {
id: contextId,
size: totalSize,
buildTime,
sections: Object.keys(context.sections).length
});
}
this.emit('context:built', { context, buildTime });
return context;
} catch (error) {
if (this.logger) {
this.logger.error('Failed to build context', { id: contextId, error: error.message });
}
throw error;
}
}
/**
* Build file system context
*/
async buildFileSystemContext(options = {}) {
const files = await this.scanFileSystem();
const recentFiles = await this.getRecentFiles();
const projectStructure = this.analyzeProjectStructure(files);
return {
files: files.slice(0, 50), // Limit to 50 most relevant files
recentFiles,
projectStructure,
totalFiles: files.length,
workingDirectory: this.workingDirectory
};
}
/**
* Build code structure context
*/
async buildCodeStructureContext(options = {}) {
const codeFiles = await this.getCodeFiles();
const structures = [];
for (const file of codeFiles.slice(0, 10)) { // Limit to 10 files for performance
try {
const structure = await this.analyzeCodeStructure(file);
if (structure) {
structures.push(structure);
}
} catch (error) {
if (this.logger) {
this.logger.debug('Failed to analyze code structure', { file: file.path, error: error.message });
}
}
}
return {
codeFiles: codeFiles.length,
structures,
dependencies: this.buildDependencyMap(structures)
};
}
/**
* Build dependencies context
*/
async buildDependenciesContext(options = {}) {
const packageJson = await this.readPackageJson();
const lockFiles = await this.findLockFiles();
const imports = await this.analyzeImports();
return {
packageJson,
lockFiles,
imports,
installedPackages: packageJson?.dependencies ? Object.keys(packageJson.dependencies) : [],
devDependencies: packageJson?.devDependencies ? Object.keys(packageJson.devDependencies) : []
};
}
/**
* Build conversation context
*/
async buildConversationContext(options = {}) {
const conversation = options.conversation || {};
return {
id: conversation.id,
messageCount: conversation.messages?.length || 0,
recentMessages: conversation.messages?.slice(-5) || [],
context: conversation.context || {},
actions: conversation.actions || [],
reasoning: conversation.reasoning || []
};
}
/**
* Build environment context
*/
async buildEnvironmentContext(options = {}) {
return {
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
cwd: process.cwd(),
env: process.env.NODE_ENV || 'development',
memory: process.memoryUsage(),
uptime: process.uptime()
};
}
/**
* Scan file system for relevant files
*/
async scanFileSystem(directory = this.workingDirectory) {
const files = [];
try {
const pattern = path.join(directory, '**/*');
const globFiles = glob.sync(pattern, {
nodir: true,
dot: false,
ignore: ['node_modules/**', '.git/**']
});
for (const filePath of globFiles) {
const relativePath = path.relative(directory, filePath);
// Apply ignore patterns
if (this.ignorePatterns.ignores(relativePath)) {
continue;
}
try {
const stats = await fs.stat(filePath);
const ext = path.extname(filePath);
files.push({
path: filePath,
relativePath,
name: path.basename(filePath),
ext,
size: stats.size,
modified: stats.mtime,
type: this.getFileType(ext),
importance: this.calculateFileImportance(filePath, stats)
});
} catch (error) {
// Skip files that can't be accessed
}
}
// Sort by importance
files.sort((a, b) => b.importance - a.importance);
return files;
} catch (error) {
if (this.logger) {
this.logger.error('Failed to scan file system', { directory, error: error.message });
}
return [];
}
}
/**
* Get recently modified files
*/
async getRecentFiles(limit = 10) {
const files = await this.scanFileSystem();
return files
.sort((a, b) => new Date(b.modified) - new Date(a.modified))
.slice(0, limit);
}
/**
* Get code files for analysis
*/
async getCodeFiles() {
const files = await this.scanFileSystem();
const codeExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.cs'];
return files.filter(file => codeExtensions.includes(file.ext));
}
/**
* Analyze code structure using AST
*/
async analyzeCodeStructure(file) {
try {
// Check cache first
if (this.astCache.has(file.path)) {
const cached = this.astCache.get(file.path);
if (cached.modified >= file.modified) {
return cached.structure;
}
}
const content = await fs.readFile(file.path, 'utf8');
const handler = this.fileHandlers[file.ext] || this.fileHandlers['.default'];
const structure = await handler(file.path, content);
// Cache the result
this.astCache.set(file.path, {
structure,
modified: file.modified
});
return structure;
} catch (error) {
if (this.logger) {
this.logger.debug('Failed to analyze code structure', { file: file.path, error: error.message });
}
return null;
}
}
/**
* Handle JavaScript file analysis
*/
async handleJavaScriptFile(filePath, content) {
try {
const ast = parser.parse(content, {
sourceType: 'module',
plugins: ['jsx', 'typescript', 'decorators-legacy', 'classProperties']
});
const structure = {
path: filePath,
type: 'javascript',
exports: [],
imports: [],
functions: [],
classes: [],
variables: [],
dependencies: []
};
traverse(ast, {
ImportDeclaration(path) {
structure.imports.push({
source: path.node.source.value,
specifiers: path.node.specifiers.map(spec => ({
type: spec.type,
name: spec.local.name,
imported: spec.imported?.name
}))
});
},
ExportDefaultDeclaration(path) {
structure.exports.push({
type: 'default',
name: path.node.declaration.name || 'default'
});
},
ExportNamedDeclaration(path) {
if (path.node.specifiers) {
path.node.specifiers.forEach(spec => {
structure.exports.push({
type: 'named',
name: spec.exported.name,
local: spec.local.name
});
});
}
},
FunctionDeclaration(path) {
structure.functions.push({
name: path.node.id?.name || 'anonymous',
params: path.node.params.map(param => param.name || 'unknown'),
async: path.node.async,
generator: path.node.generator
});
},
ClassDeclaration(path) {
structure.classes.push({
name: path.node.id.name,
superClass: path.node.superClass?.name,
methods: []
});
},
VariableDeclaration(path) {
path.node.declarations.forEach(decl => {
if (decl.id.name) {
structure.variables.push({
name: decl.id.name,
kind: path.node.kind
});
}
});
}
});
return structure;
} catch (error) {
if (this.logger) {
this.logger.debug('Failed to parse JavaScript file', { filePath, error: error.message });
}
return this.handleDefaultFile(filePath, content);
}
}
/**
* Handle TypeScript file analysis
*/
async handleTypeScriptFile(filePath, content) {
try {
const ast = parser.parse(content, {
sourceType: 'module',
plugins: ['typescript', 'jsx', 'decorators-legacy', 'classProperties']
});
// Similar to JavaScript but with TypeScript-specific handling
return await this.handleJavaScriptFile(filePath, content);
} catch (error) {
if (this.logger) {
this.logger.debug('Failed to parse TypeScript file', { filePath, error: error.message });
}
return this.handleDefaultFile(filePath, content);
}
}
/**
* Handle JSON file analysis
*/
async handleJsonFile(filePath, content) {
try {
const data = JSON.parse(content);
return {
path: filePath,
type: 'json',
keys: Object.keys(data),
structure: this.analyzeJsonStructure(data),
size: content.length
};
} catch (error) {
return this.handleDefaultFile(filePath, content);
}
}
/**
* Handle Markdown file analysis
*/
async handleMarkdownFile(filePath, content) {
const lines = content.split('\n');
const headings = lines
.filter(line => line.startsWith('#'))
.map(line => ({
level: (line.match(/^#+/) || [''])[0].length,
text: line.replace(/^#+\s*/, '')
}));
return {
path: filePath,
type: 'markdown',
headings,
lineCount: lines.length,
wordCount: content.split(/\s+/).length
};
}
/**
* Handle text file analysis
*/
async handleTextFile(filePath, content) {
return {
path: filePath,
type: 'text',
lineCount: content.split('\n').length,
wordCount: content.split(/\s+/).length,
charCount: content.length
};
}
/**
* Handle YAML file analysis
*/
async handleYamlFile(filePath, content) {
try {
const yaml = require('yaml');
const data = yaml.parse(content);
return {
path: filePath,
type: 'yaml',
keys: typeof data === 'object' ? Object.keys(data) : [],
structure: this.analyzeJsonStructure(data),
size: content.length
};
} catch (error) {
return this.handleDefaultFile(filePath, content);
}
}
/**
* Handle HTML file analysis
*/
async handleHtmlFile(filePath, content) {
const tags = (content.match(/<(\w+)/g) || [])
.map(match => match.substring(1))
.filter((tag, index, arr) => arr.indexOf(tag) === index);
return {
path: filePath,
type: 'html',
tags,
hasScript: content.includes('<script'),
hasStyle: content.includes('<style'),
size: content.length
};
}
/**
* Handle CSS file analysis
*/
async handleCssFile(filePath, content) {
const selectors = (content.match(/[^{}]+(?=\s*{)/g) || [])
.map(selector => selector.trim())
.filter(selector => selector.length > 0);
return {
path: filePath,
type: 'css',
selectors: selectors.slice(0, 20), // Limit to 20 selectors
ruleCount: selectors.length,
size: content.length
};
}
/**
* Handle Python file analysis
*/
async handlePythonFile(filePath, content) {
const lines = content.split('\n');
const imports = lines
.filter(line => line.trim().startsWith('import ') || line.trim().startsWith('from '))
.map(line => line.trim());
const functions = lines
.filter(line => line.trim().startsWith('def '))
.map(line => {
const match = line.match(/def\s+(\w+)/);
return match ? match[1] : null;
})
.filter(Boolean);
const classes = lines
.filter(line => line.trim().startsWith('class '))
.map(line => {
const match = line.match(/class\s+(\w+)/);
return match ? match[1] : null;
})
.filter(Boolean);
return {
path: filePath,
type: 'python',
imports,
functions,
classes,
lineCount: lines.length
};
}
/**
* Handle default file analysis
*/
async handleDefaultFile(filePath, content) {
return {
path: filePath,
type: 'unknown',
size: content.length,
lineCount: content.split('\n').length,
isBinary: this.isBinaryContent(content)
};
}
/**
* Analyze JSON structure recursively
*/
analyzeJsonStructure(obj, depth = 0, maxDepth = 3) {
if (depth > maxDepth || obj === null || typeof obj !== 'object') {
return typeof obj;
}
if (Array.isArray(obj)) {
return {
type: 'array',
length: obj.length,
elementType: obj.length > 0 ? this.analyzeJsonStructure(obj[0], depth + 1, maxDepth) : 'unknown'
};
}
const structure = {};
for (const [key, value] of Object.entries(obj)) {
structure[key] = this.analyzeJsonStructure(value, depth + 1, maxDepth);
}
return structure;
}
/**
* Build dependency map from code structures
*/
buildDependencyMap(structures) {
const dependencies = new Map();
for (const structure of structures) {
if (structure.imports) {
for (const imp of structure.imports) {
if (!dependencies.has(imp.source)) {
dependencies.set(imp.source, []);
}
dependencies.get(imp.source).push(structure.path);
}
}
}
return Object.fromEntries(dependencies);
}
/**
* Read package.json if it exists
*/
async readPackageJson() {
try {
const packagePath = path.join(this.workingDirectory, 'package.json');
if (await fs.pathExists(packagePath)) {
return await fs.readJson(packagePath);
}
} catch (error) {
if (this.logger) {
this.logger.debug('Failed to read package.json', { error: error.message });
}
}
return null;
}
/**
* Find lock files
*/
async findLockFiles() {
const lockFiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
const found = [];
for (const lockFile of lockFiles) {
const lockPath = path.join(this.workingDirectory, lockFile);
if (await fs.pathExists(lockPath)) {
found.push(lockFile);
}
}
return found;
}
/**
* Analyze imports across all files
*/
async analyzeImports() {
const codeFiles = await this.getCodeFiles();
const imports = new Map();
for (const file of codeFiles.slice(0, 20)) { // Limit for performance
try {
const structure = await this.analyzeCodeStructure(file);
if (structure?.imports) {
for (const imp of structure.imports) {
if (!imports.has(imp.source)) {
imports.set(imp.source, { count: 0, files: [] });
}
const entry = imports.get(imp.source);
entry.count++;
entry.files.push(file.relativePath);
}
}
} catch (error) {
// Skip files that can't be analyzed
}
}
return Object.fromEntries(imports);
}
/**
* Analyze project structure
*/
analyzeProjectStructure(files) {
const structure = {
directories: new Set(),
fileTypes: new Map(),
patterns: []
};
for (const file of files) {
// Track directories
const dir = path.dirname(file.relativePath);
if (dir !== '.') {
structure.directories.add(dir);
}
// Track file types
const ext = file.ext || 'no-extension';
if (!structure.fileTypes.has(ext)) {
structure.fileTypes.set(ext, 0);
}
structure.fileTypes.set(ext, structure.fileTypes.get(ext) + 1);
}
// Detect common patterns
if (structure.directories.has('src')) structure.patterns.push('src-based');
if (structure.directories.has('lib')) structure.patterns.push('lib-based');
if (structure.directories.has('components')) structure.patterns.push('component-based');
if (structure.directories.has('pages')) structure.patterns.push('page-based');
if (structure.fileTypes.has('.js') || structure.fileTypes.has('.ts')) structure.patterns.push('javascript');
if (structure.fileTypes.has('.py')) structure.patterns.push('python');
return {
directories: Array.from(structure.directories),
fileTypes: Object.fromEntries(structure.fileTypes),
patterns: structure.patterns
};
}
/**
* Calculate file importance for context prioritization
*/
calculateFileImportance(filePath, stats) {
let importance = 0;
const fileName = path.basename(filePath);
const ext = path.extname(filePath);
const dir = path.dirname(filePath);
// Important file names
const importantFiles = [
'package.json', 'README.md', 'index.js', 'index.ts', 'main.js', 'main.ts',
'app.js', 'app.ts', 'server.js', 'server.ts', 'config.js', 'config.ts'
];
if (importantFiles.includes(fileName)) {
importance += 10;
}
// Important extensions
const importantExts = ['.js', '.ts', '.jsx', '.tsx', '.json', '.md'];
if (importantExts.includes(ext)) {
importance += 5;
}
// Root level files are more important
if (dir === '.') {
importance += 3;
}
// Recently modified files are more important
const daysSinceModified = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24);
if (daysSinceModified < 1) importance += 5;
else if (daysSinceModified < 7) importance += 3;
else if (daysSinceModified < 30) importance += 1;
// Smaller files are easier to include in context
if (stats.size < 1000) importance += 2;
else if (stats.size < 10000) importance += 1;
return importance;
}
/**
* Get file type from extension
*/
getFileType(ext) {
const typeMap = {
'.js': 'javascript',
'.ts': 'typescript',
'.jsx': 'react',
'.tsx': 'react-typescript',
'.json': 'json',
'.md': 'markdown',
'.html': 'html',
'.css': 'css',
'.py': 'python',
'.java': 'java',
'.cpp': 'cpp',
'.c': 'c',
'.cs': 'csharp'
};
return typeMap[ext] || 'unknown';
}
/**
* Check if content is binary
*/
isBinaryContent(content) {
// Simple binary detection
const nullBytes = (content.match(/\0/g) || []).length;
return nullBytes > content.length * 0.01; // More than 1% null bytes
}
/**
* Calculate total context size
*/
calculateContextSize(context) {
return JSON.stringify(context).length;
}
/**
* Compress context when it exceeds size limits
*/
async compressContext(context) {
if (this.logger) {
this.logger.info('Compressing context due to size limit', {
currentSize: this.calculateContextSize(context),
maxSize: this.maxSize
});
}
// Compress based on retention strategy
switch (this.retentionStrategy) {
case 'importance':
return this.compressByImportance(context);
case 'recency':
return this.compressByRecency(context);
case 'fifo':
return this.compressByFifo(context);
default:
return this.compressByImportance(context);
}
}
/**
* Compress context by importance
*/
compressByImportance(context) {
// Keep most important sections and files
if (context.sections.fileSystem?.files) {
context.sections.fileSystem.files = context.sections.fileSystem.files
.sort((a, b) => b.importance - a.importance)
.slice(0, 20);
}
if (context.sections.codeStructure?.structures) {
context.sections.codeStructure.structures = context.sections.codeStructure.structures.slice(0, 5);
}
return context;
}
/**
* Compress context by recency
*/
compressByRecency(context) {
// Keep most recent files and conversations
if (context.sections.fileSystem?.files) {
context.sections.fileSystem.files = context.sections.fileSystem.files
.sort((a, b) => new Date(b.modified) - new Date(a.modified))
.slice(0, 20);
}
return context;
}
/**
* Compress context by FIFO
*/
compressByFifo(context) {
// Simple truncation
if (context.sections.fileSystem?.files) {
context.sections.fileSystem.files = context.sections.fileSystem.files.slice(0, 20);
}
if (context.sections.codeStructure?.structures) {
context.sections.codeStructure.structures = context.sections.codeStructure.structures.slice(0, 5);
}
return context;
}
/**
* Update context with new information
*/
async updateContext(options = {}) {
const { conversationId, reasoning, executionResults } = options;
if (conversationId && this.contexts.has(conversationId)) {
const context = this.contexts.get(conversationId);
// Update with reasoning results
if (reasoning) {
context.reasoning = reasoning;
}
// Update with execution results
if (executionResults) {
context.executionResults = executionResults;
// Update file cache if files were modified
if (executionResults.results) {
for (const result of executionResults.results) {
if (result.action?.tool === 'filesystem') {
// Invalidate file cache for modified files
this.invalidateFileCache(result.action.parameters);
}
}
}
}
context.lastUpdated = new Date().toISOString();
this.contexts.set(conversationId, context);
}
}
/**
* Invalidate file cache for modified files
*/
invalidateFileCache(parameters) {
if (parameters && parameters[0]) {
const filePath = path.resolve(parameters[0]);
this.fileCache.delete(filePath);
this.astCache.delete(filePath);
}
}
/**
* Update configuration
*/
async updateConfig(newConfig) {
if (newConfig.maxSize) {
this.maxSize = newConfig.maxSize;
}
if (newConfig.compressionThreshold) {
this.compressionThreshold = newConfig.compressionThreshold;
}
if (newConfig.retentionStrategy) {
this.retentionStrategy = newConfig.retentionStrategy;
}
if (this.logger) {
this.logger.info('Context manager configuration updated', newConfig);
}
}
/**
* Cleanup old contexts and caches
*/
async cleanup() {
// Clear old contexts (keep last 10)
if (this.contexts.size > 10) {
const entries = Array.from(this.contexts.entries());
const toKeep = entries.slice(-10);
this.contexts.clear();
for (const [id, context] of toKeep) {
this.contexts.set(id, context);
}
}
// Clear file cache
this.fileCache.clear();
// Clear AST cache for old files
const now = Date.now();
const maxAge = 60 * 60 * 1000; // 1 hour
for (const [filePath, cached] of this.astCache.entries()) {
if (now - cached.modified.getTime() > maxAge) {
this.astCache.delete(filePath);
}
}
if (this.logger) {
this.logger.info('Context manager cleanup completed');
}
}
/**
* Get context manager status
*/
getStatus() {
return {
maxSize: this.maxSize,
compressionThreshold: this.compressionThreshold,
retentionStrategy: this.retentionStrategy,
workingDirectory: this.workingDirectory,
contextsCount: this.contexts.size,
fileCacheSize: this.fileCache.size,
astCacheSize: this.astCache.size
};
}
}
module.exports = ContextManager;