Skip to main content
Glama
agentTools.ts44 kB
/** * Agent Tools - Claude Code / GitHub Copilot-like capabilities * Provides file operations, command execution, and package management */ import chalk from 'chalk'; import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; import * as readline from 'readline'; export interface FileOperation { type: 'create' | 'edit' | 'delete' | 'read'; path: string; content?: string; searchReplace?: { search: string; replace: string }[]; } export interface CommandOperation { type: 'shell' | 'npm' | 'git'; command: string; args?: string[]; cwd?: string; } export interface AgentAction { type: 'file' | 'command' | 'package' | 'search'; operation: FileOperation | CommandOperation | PackageOperation | SearchOperation; description: string; requiresConfirmation?: boolean; } export interface PackageOperation { type: 'install' | 'uninstall'; packages: string[]; dev?: boolean; } export interface SearchOperation { type: 'file' | 'content' | 'symbol'; query: string; path?: string; extensions?: string[]; } export interface ProjectAnalysis { projectType: string; framework: string; language: string; packageManager: 'npm' | 'yarn' | 'pnpm' | 'bun' | 'unknown'; dependencies: string[]; devDependencies: string[]; structure: DirectoryTree; relevantFiles: RelevantFile[]; } export interface DirectoryTree { name: string; type: 'file' | 'directory'; children?: DirectoryTree[]; path: string; } export interface RelevantFile { path: string; type: string; summary?: string; content?: string; } // ============= Project Analysis ============= /** * Analyze project structure and detect type */ export function analyzeProject(rootDir: string = process.cwd()): ProjectAnalysis { const analysis: ProjectAnalysis = { projectType: 'unknown', framework: 'none', language: 'unknown', packageManager: 'unknown', dependencies: [], devDependencies: [], structure: { name: path.basename(rootDir), type: 'directory', path: rootDir, children: [] }, relevantFiles: [] }; // Detect package manager if (fs.existsSync(path.join(rootDir, 'bun.lockb'))) { analysis.packageManager = 'bun'; } else if (fs.existsSync(path.join(rootDir, 'pnpm-lock.yaml'))) { analysis.packageManager = 'pnpm'; } else if (fs.existsSync(path.join(rootDir, 'yarn.lock'))) { analysis.packageManager = 'yarn'; } else if (fs.existsSync(path.join(rootDir, 'package-lock.json'))) { analysis.packageManager = 'npm'; } // Read package.json const packageJsonPath = path.join(rootDir, 'package.json'); if (fs.existsSync(packageJsonPath)) { try { const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); analysis.dependencies = Object.keys(pkg.dependencies || {}); analysis.devDependencies = Object.keys(pkg.devDependencies || {}); // Detect framework const allDeps = [...analysis.dependencies, ...analysis.devDependencies]; if (allDeps.includes('svelte') || allDeps.includes('@sveltejs/kit')) { analysis.framework = 'SvelteKit'; analysis.projectType = 'web'; } else if (allDeps.includes('next')) { analysis.framework = 'Next.js'; analysis.projectType = 'web'; } else if (allDeps.includes('react')) { analysis.framework = 'React'; analysis.projectType = 'web'; } else if (allDeps.includes('vue')) { analysis.framework = 'Vue'; analysis.projectType = 'web'; } else if (allDeps.includes('express') || allDeps.includes('fastify')) { analysis.framework = allDeps.includes('express') ? 'Express' : 'Fastify'; analysis.projectType = 'api'; } else if (allDeps.includes('electron')) { analysis.framework = 'Electron'; analysis.projectType = 'desktop'; } // Detect language if (allDeps.includes('typescript') || fs.existsSync(path.join(rootDir, 'tsconfig.json'))) { analysis.language = 'TypeScript'; } else { analysis.language = 'JavaScript'; } } catch { console.log(chalk.yellow('⚠️ Could not parse package.json')); } } // Build directory structure analysis.structure = buildDirectoryTree(rootDir, 3); // Find relevant files analysis.relevantFiles = findRelevantFiles(rootDir); return analysis; } /** * Build directory tree with depth limit */ function buildDirectoryTree(dir: string, maxDepth: number, currentDepth: number = 0): DirectoryTree { const name = path.basename(dir); const tree: DirectoryTree = { name, type: 'directory', path: dir, children: [] }; if (currentDepth >= maxDepth) return tree; const ignoreDirs = ['node_modules', '.git', 'dist', 'build', '.svelte-kit', '.next', 'coverage', '__pycache__', '.venv']; try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (ignoreDirs.includes(entry.name)) continue; if (entry.name.startsWith('.') && entry.name !== '.env.example') continue; const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { if (tree.children) { tree.children.push(buildDirectoryTree(fullPath, maxDepth, currentDepth + 1)); } } else { if (tree.children) { tree.children.push({ name: entry.name, type: 'file', path: fullPath }); } } } } catch { // Permission denied or other error } return tree; } /** * Find relevant files for context */ function findRelevantFiles(rootDir: string): RelevantFile[] { const relevantFiles: RelevantFile[] = []; const importantFiles = [ 'package.json', 'tsconfig.json', 'vite.config.ts', 'svelte.config.js', 'README.md', '.env.example', 'docker-compose.yml', 'Dockerfile' ]; // Find important config files for (const file of importantFiles) { const filePath = path.join(rootDir, file); if (fs.existsSync(filePath)) { relevantFiles.push({ path: filePath, type: 'config', content: fs.readFileSync(filePath, 'utf-8').substring(0, 2000) // Limit content }); } } // Find source files const srcDirs = ['src', 'lib', 'app', 'pages', 'components']; for (const srcDir of srcDirs) { const srcPath = path.join(rootDir, srcDir); if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) { scanSourceFiles(srcPath, relevantFiles, 10); // Limit to 10 files per dir } } return relevantFiles.slice(0, 30); // Max 30 files for context } /** * Scan source files recursively */ function scanSourceFiles(dir: string, files: RelevantFile[], limit: number, currentCount: number = 0): number { if (currentCount >= limit) return currentCount; const codeExtensions = ['.ts', '.tsx', '.js', '.jsx', '.svelte', '.vue', '.py', '.go', '.rs']; try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (currentCount >= limit) break; const fullPath = path.join(dir, entry.name); if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') { currentCount = scanSourceFiles(fullPath, files, limit, currentCount); } else if (entry.isFile()) { const ext = path.extname(entry.name); if (codeExtensions.includes(ext)) { const content = fs.readFileSync(fullPath, 'utf-8'); files.push({ path: fullPath, type: 'source', summary: extractFileSummary(content, ext), content: content.substring(0, 3000) // Limit content }); currentCount++; } } } } catch { // Permission denied } return currentCount; } /** * Extract summary from file content */ function extractFileSummary(content: string, ext: string): string { const lines = content.split('\n'); const summary: string[] = []; // Extract imports/exports for JS/TS if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) { const imports = lines.filter(l => l.trim().startsWith('import ') || l.trim().startsWith('export ')); summary.push(...imports.slice(0, 5)); } // Extract function/class definitions const definitions = lines.filter(l => l.match(/^(export\s+)?(async\s+)?function\s+\w+/) || l.match(/^(export\s+)?class\s+\w+/) || l.match(/^(export\s+)?interface\s+\w+/) || l.match(/^(export\s+)?type\s+\w+/) ); summary.push(...definitions.slice(0, 5)); return summary.join('\n'); } // ============= Search Operations ============= /** * Search for files by name pattern */ export function searchFiles(rootDir: string, pattern: string, extensions?: string[]): string[] { const results: string[] = []; const regex = new RegExp(pattern, 'i'); function search(dir: string) { try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue; const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { search(fullPath); } else if (entry.isFile()) { const ext = path.extname(entry.name); if (extensions && !extensions.includes(ext)) continue; if (regex.test(entry.name)) { results.push(fullPath); } } } } catch { // Skip directories with permission issues } } search(rootDir); return results; } /** * Search for content in files */ export function searchContent(rootDir: string, query: string, extensions?: string[]): { file: string; line: number; content: string }[] { const results: { file: string; line: number; content: string }[] = []; const regex = new RegExp(query, 'gi'); const defaultExtensions = ['.ts', '.tsx', '.js', '.jsx', '.svelte', '.vue', '.json', '.md']; const searchExtensions = extensions || defaultExtensions; function search(dir: string) { try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue; const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { search(fullPath); } else if (entry.isFile()) { const ext = path.extname(entry.name); if (!searchExtensions.includes(ext)) continue; try { const content = fs.readFileSync(fullPath, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { if (regex.test(line)) { results.push({ file: fullPath, line: index + 1, content: line.trim().substring(0, 200) }); } }); } catch { // Skip files that can't be read } } } } catch { // Skip directories with permission issues } } search(rootDir); return results.slice(0, 50); // Limit results } // ============= File Operations ============= /** * Create or overwrite a file */ export function createFile(filePath: string, content: string): { success: boolean; error?: string } { try { // Create directory if it doesn't exist const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, content, 'utf-8'); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Edit a file with search/replace */ export function editFile(filePath: string, searchReplace: { search: string; replace: string }[]): { success: boolean; error?: string; changes: number } { try { if (!fs.existsSync(filePath)) { return { success: false, error: 'File not found', changes: 0 }; } let content = fs.readFileSync(filePath, 'utf-8'); let changes = 0; for (const { search, replace } of searchReplace) { const regex = new RegExp(escapeRegex(search), 'g'); const matches = content.match(regex); if (matches) { changes += matches.length; content = content.replace(regex, replace); } } fs.writeFileSync(filePath, content, 'utf-8'); return { success: true, changes }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), changes: 0 }; } } /** * Delete a file */ export function deleteFile(filePath: string): { success: boolean; error?: string } { try { if (!fs.existsSync(filePath)) { return { success: false, error: 'File not found' }; } fs.unlinkSync(filePath); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Read file content */ export function readFile(filePath: string): { success: boolean; content?: string; error?: string } { try { if (!fs.existsSync(filePath)) { return { success: false, error: 'File not found' }; } const content = fs.readFileSync(filePath, 'utf-8'); return { success: true, content }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } // ============= Command Execution ============= /** * Execute a shell command */ export function executeCommand(command: string, cwd?: string): { success: boolean; output?: string; error?: string } { try { const output = execSync(command, { cwd: cwd || process.cwd(), encoding: 'utf-8', timeout: 60000, // 60 second timeout stdio: ['pipe', 'pipe', 'pipe'] }); return { success: true, output: output.trim() }; } catch (error: unknown) { const err = error as { stdout?: Buffer; stderr?: Buffer; message?: string }; return { success: false, output: err.stdout?.toString() || '', error: err.stderr?.toString() || err.message || String(error) }; } } /** * Install packages */ export function installPackages(packages: string[], dev: boolean = false, packageManager: string = 'npm'): { success: boolean; output?: string; error?: string } { let command: string; switch (packageManager) { case 'yarn': command = `yarn add ${dev ? '-D ' : ''}${packages.join(' ')}`; break; case 'pnpm': command = `pnpm add ${dev ? '-D ' : ''}${packages.join(' ')}`; break; case 'bun': command = `bun add ${dev ? '-d ' : ''}${packages.join(' ')}`; break; default: command = `npm install ${dev ? '--save-dev ' : ''}${packages.join(' ')}`; } console.log(chalk.dim(`📦 Running: ${command}`)); return executeCommand(command); } // ============= User Confirmation ============= /** * Ask for user confirmation */ export async function confirmAction(message: string): Promise<boolean> { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { rl.question(chalk.yellow(`⚠️ ${message} (y/N): `), (answer) => { rl.close(); resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); }); }); } // ============= Action Parser ============= /** * Parse AI response for actions */ export function parseActionsFromResponse(response: string): AgentAction[] { const actions: AgentAction[] = []; // Parse FILE_CREATE blocks const createRegex = /```(?:FILE_CREATE|create)\s+([^\n]+)\n([\s\S]*?)```/g; let match; while ((match = createRegex.exec(response)) !== null) { actions.push({ type: 'file', operation: { type: 'create', path: match[1].trim(), content: match[2].trim() } as FileOperation, description: `Create file: ${match[1].trim()}`, requiresConfirmation: true }); } // Parse FILE_EDIT blocks const editRegex = /```(?:FILE_EDIT|edit)\s+([^\n]+)\n([\s\S]*?)```/g; while ((match = editRegex.exec(response)) !== null) { const content = match[2].trim(); actions.push({ type: 'file', operation: { type: 'edit', path: match[1].trim(), content: content } as FileOperation, description: `Edit file: ${match[1].trim()}`, requiresConfirmation: true }); } // Parse COMMAND blocks const commandRegex = /```(?:COMMAND|shell|bash)\n([\s\S]*?)```/g; while ((match = commandRegex.exec(response)) !== null) { const commands = match[1].trim().split('\n').filter(c => c.trim() && !c.startsWith('#')); for (const cmd of commands) { actions.push({ type: 'command', operation: { type: 'shell', command: cmd.trim() } as CommandOperation, description: `Run command: ${cmd.trim()}`, requiresConfirmation: true }); } } // Parse INSTALL blocks const installRegex = /```(?:INSTALL|npm|yarn|pnpm)\n([\s\S]*?)```/g; while ((match = installRegex.exec(response)) !== null) { const packages = match[1].trim().split('\n') .filter(p => p.trim() && !p.startsWith('#')) .map(p => p.trim()); if (packages.length > 0) { actions.push({ type: 'package', operation: { type: 'install', packages: packages } as PackageOperation, description: `Install packages: ${packages.join(', ')}`, requiresConfirmation: true }); } } return actions; } /** * Execute a single action */ export async function executeAction(action: AgentAction, projectAnalysis: ProjectAnalysis): Promise<{ success: boolean; message: string }> { switch (action.type) { case 'file': { const fileOp = action.operation as FileOperation; if (fileOp.type === 'create' || fileOp.type === 'edit') { const result = createFile(fileOp.path, fileOp.content || ''); return { success: result.success, message: result.success ? `✅ ${fileOp.type === 'create' ? 'Created' : 'Updated'} ${fileOp.path}` : `❌ Failed: ${result.error}` }; } else if (fileOp.type === 'delete') { const result = deleteFile(fileOp.path); return { success: result.success, message: result.success ? `✅ Deleted ${fileOp.path}` : `❌ Failed: ${result.error}` }; } break; } case 'command': { const cmdOp = action.operation as CommandOperation; const cmdResult = executeCommand(cmdOp.command, cmdOp.cwd); return { success: cmdResult.success, message: cmdResult.success ? `✅ Command executed:\n${cmdResult.output}` : `❌ Command failed: ${cmdResult.error}` }; } case 'package': { const pkgOp = action.operation as PackageOperation; const pkgResult = installPackages(pkgOp.packages, pkgOp.dev, projectAnalysis.packageManager); return { success: pkgResult.success, message: pkgResult.success ? `✅ Packages installed` : `❌ Installation failed: ${pkgResult.error}` }; } } return { success: false, message: 'Unknown action type' }; } // ============= Helpers ============= function escapeRegex(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Format project analysis for prompt */ export function formatProjectAnalysisForPrompt(analysis: ProjectAnalysis): string { const lines: string[] = []; lines.push('## Project Analysis'); lines.push(`- Type: ${analysis.projectType}`); lines.push(`- Framework: ${analysis.framework}`); lines.push(`- Language: ${analysis.language}`); lines.push(`- Package Manager: ${analysis.packageManager}`); if (analysis.dependencies.length > 0) { lines.push(`\n### Dependencies (${analysis.dependencies.length})`); lines.push(analysis.dependencies.slice(0, 15).join(', ')); } lines.push('\n### Project Structure'); lines.push(formatDirectoryTree(analysis.structure, 0)); if (analysis.relevantFiles.length > 0) { lines.push('\n### Relevant Files'); for (const file of analysis.relevantFiles.slice(0, 10)) { const relativePath = path.relative(process.cwd(), file.path); lines.push(`\n#### ${relativePath}`); if (file.summary) { lines.push('```'); lines.push(file.summary); lines.push('```'); } } } return lines.join('\n'); } function formatDirectoryTree(tree: DirectoryTree, indent: number): string { const prefix = ' '.repeat(indent); const lines: string[] = []; if (tree.type === 'directory') { lines.push(`${prefix}📁 ${tree.name}/`); if (tree.children) { for (const child of tree.children) { lines.push(formatDirectoryTree(child, indent + 1)); } } } else { lines.push(`${prefix}📄 ${tree.name}`); } return lines.join('\n'); } // ============= Chunked File Reading ============= export interface FileChunk { content: string; startLine: number; endLine: number; totalLines: number; hasMore: boolean; } /** * Read file in chunks for large files */ export function readFileChunked(filePath: string, startLine: number = 1, maxLines: number = 200): FileChunk { try { if (!fs.existsSync(filePath)) { return { content: '', startLine: 0, endLine: 0, totalLines: 0, hasMore: false }; } const content = fs.readFileSync(filePath, 'utf-8'); const lines = content.split('\n'); const totalLines = lines.length; const actualStart = Math.max(0, startLine - 1); const actualEnd = Math.min(actualStart + maxLines, totalLines); const chunk = lines.slice(actualStart, actualEnd).join('\n'); return { content: chunk, startLine: actualStart + 1, endLine: actualEnd, totalLines, hasMore: actualEnd < totalLines }; } catch { return { content: '', startLine: 0, endLine: 0, totalLines: 0, hasMore: false }; } } /** * Get file summary with line counts and key sections */ export function getFileSummary(filePath: string): { lines: number; size: string; language: string; sections: string[] } { try { const stat = fs.statSync(filePath); const content = fs.readFileSync(filePath, 'utf-8'); const lines = content.split('\n').length; const ext = path.extname(filePath); const langMap: Record<string, string> = { '.ts': 'TypeScript', '.tsx': 'TypeScript React', '.js': 'JavaScript', '.jsx': 'JavaScript React', '.py': 'Python', '.rs': 'Rust', '.go': 'Go', '.java': 'Java', '.cpp': 'C++', '.c': 'C', '.svelte': 'Svelte', '.vue': 'Vue', '.md': 'Markdown', '.json': 'JSON', '.yaml': 'YAML', '.yml': 'YAML' }; const language = langMap[ext] || ext.substring(1).toUpperCase(); // Find key sections (functions, classes, exports) const sections: string[] = []; const lineArr = content.split('\n'); for (let i = 0; i < lineArr.length; i++) { const line = lineArr[i]; if (line.match(/^(export\s+)?(async\s+)?function\s+\w+/) || line.match(/^(export\s+)?class\s+\w+/) || line.match(/^(export\s+)?interface\s+\w+/) || line.match(/^(export\s+)?type\s+\w+\s*=/) || line.match(/^def\s+\w+/) || line.match(/^class\s+\w+/)) { sections.push(`Line ${i + 1}: ${line.trim().substring(0, 80)}`); } } return { lines, size: formatFileSize(stat.size), language, sections: sections.slice(0, 20) }; } catch { return { lines: 0, size: '0 B', language: 'Unknown', sections: [] }; } } function formatFileSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } // ============= Git Integration ============= export interface GitStatus { branch: string; staged: string[]; modified: string[]; untracked: string[]; ahead: number; behind: number; } /** * Check if directory is a git repository */ export function isGitRepo(dir: string = process.cwd()): boolean { return fs.existsSync(path.join(dir, '.git')); } /** * Get git status */ export function getGitStatus(dir: string = process.cwd()): GitStatus | null { if (!isGitRepo(dir)) return null; try { const status: GitStatus = { branch: '', staged: [], modified: [], untracked: [], ahead: 0, behind: 0 }; // Get current branch try { status.branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: dir, encoding: 'utf-8' }).trim(); } catch { status.branch = 'unknown'; } // Get status const statusOutput = execSync('git status --porcelain', { cwd: dir, encoding: 'utf-8' }); const lines = statusOutput.split('\n').filter(l => l.trim()); for (const line of lines) { const index = line[0]; const workTree = line[1]; const file = line.substring(3); if (index !== ' ' && index !== '?') { status.staged.push(file); } if (workTree === 'M' || workTree === 'D') { status.modified.push(file); } if (index === '?') { status.untracked.push(file); } } // Get ahead/behind try { const aheadBehind = execSync('git rev-list --left-right --count HEAD...@{upstream}', { cwd: dir, encoding: 'utf-8' }).trim(); const [ahead, behind] = aheadBehind.split('\t').map(Number); status.ahead = ahead || 0; status.behind = behind || 0; } catch { // No upstream configured } return status; } catch { return null; } } /** * Git commit with message */ export function gitCommit(message: string, dir: string = process.cwd()): { success: boolean; error?: string } { try { execSync(`git add -A`, { cwd: dir, encoding: 'utf-8' }); execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: dir, encoding: 'utf-8' }); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Get git diff for file or all changes */ export function getGitDiff(file?: string, staged: boolean = false, dir: string = process.cwd()): string { try { const stagedFlag = staged ? '--staged' : ''; const fileArg = file ? `-- "${file}"` : ''; return execSync(`git diff ${stagedFlag} ${fileArg}`, { cwd: dir, encoding: 'utf-8' }); } catch { return ''; } } /** * Create a new git branch */ export function gitCreateBranch(branchName: string, dir: string = process.cwd()): { success: boolean; error?: string } { try { execSync(`git checkout -b ${branchName}`, { cwd: dir, encoding: 'utf-8' }); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Checkout existing branch */ export function gitCheckout(branchName: string, dir: string = process.cwd()): { success: boolean; error?: string } { try { execSync(`git checkout ${branchName}`, { cwd: dir, encoding: 'utf-8' }); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } // ============= Undo/Rollback System ============= interface FileBackup { path: string; content: string; timestamp: number; } const backupStack: FileBackup[] = []; const MAX_BACKUPS = 50; /** * Create backup before modifying file */ export function createBackup(filePath: string): boolean { try { if (!fs.existsSync(filePath)) { backupStack.push({ path: filePath, content: '__FILE_DID_NOT_EXIST__', timestamp: Date.now() }); } else { const content = fs.readFileSync(filePath, 'utf-8'); backupStack.push({ path: filePath, content, timestamp: Date.now() }); } // Limit backup stack size while (backupStack.length > MAX_BACKUPS) { backupStack.shift(); } return true; } catch { return false; } } /** * Undo last file change */ export function undoLastChange(): { success: boolean; file?: string; error?: string } { if (backupStack.length === 0) { return { success: false, error: 'No changes to undo' }; } const backup = backupStack.pop(); if (!backup) { return { success: false, error: 'No changes to undo' }; }; try { if (backup.content === '__FILE_DID_NOT_EXIST__') { // File was created, delete it if (fs.existsSync(backup.path)) { fs.unlinkSync(backup.path); } } else { // Restore original content fs.writeFileSync(backup.path, backup.content, 'utf-8'); } return { success: true, file: backup.path }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Get list of changes that can be undone */ export function getUndoHistory(): { path: string; timestamp: number }[] { return backupStack.map(b => ({ path: b.path, timestamp: b.timestamp })).reverse(); } /** * Clear all backups */ export function clearBackups(): void { backupStack.length = 0; } // ============= Multi-file Transaction ============= interface TransactionFile { path: string; originalContent: string | null; // null means file didn't exist newContent: string; } let currentTransaction: TransactionFile[] | null = null; /** * Start a multi-file transaction */ export function startTransaction(): void { currentTransaction = []; } /** * Add file to transaction */ export function addToTransaction(filePath: string, newContent: string): boolean { if (!currentTransaction) { console.log(chalk.red('❌ No transaction started')); return false; } let originalContent: string | null = null; if (fs.existsSync(filePath)) { originalContent = fs.readFileSync(filePath, 'utf-8'); } currentTransaction.push({ path: filePath, originalContent, newContent }); return true; } /** * Commit transaction (apply all changes) */ export function commitTransaction(): { success: boolean; filesChanged: number; error?: string } { if (!currentTransaction || currentTransaction.length === 0) { return { success: false, filesChanged: 0, error: 'No transaction or empty transaction' }; } const appliedChanges: TransactionFile[] = []; try { for (const file of currentTransaction) { // Create directory if needed const dir = path.dirname(file.path); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // Create backup createBackup(file.path); // Apply change fs.writeFileSync(file.path, file.newContent, 'utf-8'); appliedChanges.push(file); } const count = currentTransaction.length; currentTransaction = null; return { success: true, filesChanged: count }; } catch (error) { // Rollback all applied changes for (const file of appliedChanges) { try { if (file.originalContent === null) { if (fs.existsSync(file.path)) { fs.unlinkSync(file.path); } } else { fs.writeFileSync(file.path, file.originalContent, 'utf-8'); } } catch { // Ignore rollback errors } } currentTransaction = null; return { success: false, filesChanged: 0, error: error instanceof Error ? error.message : String(error) }; } } /** * Rollback transaction (discard all pending changes) */ export function rollbackTransaction(): void { currentTransaction = null; } /** * Get pending transaction files */ export function getTransactionFiles(): string[] { return currentTransaction ? currentTransaction.map(f => f.path) : []; } // ============= Smart Search ============= export interface SearchResult { file: string; matches: { line: number; content: string; context: string[]; }[]; score: number; } /** * Smart search with relevance scoring */ export function smartSearch(query: string, rootDir: string = process.cwd()): SearchResult[] { const results: SearchResult[] = []; const searchTerms = query.toLowerCase().split(/\s+/).filter(t => t.length > 2); const searchExtensions = ['.ts', '.tsx', '.js', '.jsx', '.svelte', '.vue', '.py', '.go', '.rs', '.java', '.md', '.json']; function searchDir(dir: string) { try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.name === 'node_modules' || entry.name.startsWith('.') || entry.name === 'dist' || entry.name === 'build') continue; const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { searchDir(fullPath); } else if (entry.isFile()) { const ext = path.extname(entry.name); if (!searchExtensions.includes(ext)) continue; try { const content = fs.readFileSync(fullPath, 'utf-8'); const lines = content.split('\n'); const matches: SearchResult['matches'] = []; let score = 0; // Score based on filename match const fileName = entry.name.toLowerCase(); for (const term of searchTerms) { if (fileName.includes(term)) { score += 10; } } // Search content for (let i = 0; i < lines.length; i++) { const lineLower = lines[i].toLowerCase(); let lineScore = 0; for (const term of searchTerms) { if (lineLower.includes(term)) { lineScore += 1; } } if (lineScore > 0) { score += lineScore; const context = [ i > 0 ? lines[i - 1] : '', lines[i], i < lines.length - 1 ? lines[i + 1] : '' ].filter(Boolean); matches.push({ line: i + 1, content: lines[i].trim().substring(0, 150), context }); } } if (matches.length > 0) { results.push({ file: fullPath, matches: matches.slice(0, 5), // Limit matches per file score }); } } catch { // Skip files that can't be read } } } } catch { // Skip directories with permission issues } } searchDir(rootDir); // Sort by score and return top results return results .sort((a, b) => b.score - a.score) .slice(0, 20); } /** * Find files by semantic meaning (looking for related code) */ export function findRelatedFiles(filePath: string, _rootDir: string = process.cwd()): string[] { try { const content = fs.readFileSync(filePath, 'utf-8'); const ext = path.extname(filePath); const relatedFiles: string[] = []; // Extract imports/requires const importMatches = content.match(/(?:import|require)\s*\(?['"](\.{1,2}\/[^'"]+)['"]\)?/g); if (importMatches) { for (const match of importMatches) { const pathMatch = match.match(/['"](\.{1,2}\/[^'"]+)['"]/); if (pathMatch) { let importPath = pathMatch[1]; const dir = path.dirname(filePath); let fullPath = path.resolve(dir, importPath); // Try adding extensions if (!fs.existsSync(fullPath)) { const extensions = ['.ts', '.tsx', '.js', '.jsx', ext, '/index.ts', '/index.js']; for (const tryExt of extensions) { if (fs.existsSync(fullPath + tryExt)) { fullPath = fullPath + tryExt; break; } } } if (fs.existsSync(fullPath)) { relatedFiles.push(fullPath); } } } } // Find test files const baseName = path.basename(filePath, ext); const testPatterns = [ `${baseName}.test${ext}`, `${baseName}.spec${ext}`, `__tests__/${baseName}${ext}` ]; const dir = path.dirname(filePath); for (const pattern of testPatterns) { const testPath = path.join(dir, pattern); if (fs.existsSync(testPath)) { relatedFiles.push(testPath); } } return [...new Set(relatedFiles)]; // Remove duplicates } catch { return []; } } // ============= Conversation History ============= interface ConversationMessage { role: 'user' | 'assistant' | 'system'; content: string; timestamp: number; } interface ConversationSession { id: string; messages: ConversationMessage[]; projectPath: string; createdAt: number; updatedAt: number; } const HISTORY_DIR = path.join(process.cwd(), '.mcp-history'); /** * Save conversation to history */ export function saveConversation(sessionId: string, messages: { role: string; content: string }[], projectPath: string): boolean { try { if (!fs.existsSync(HISTORY_DIR)) { fs.mkdirSync(HISTORY_DIR, { recursive: true }); } const session: ConversationSession = { id: sessionId, messages: messages.map(m => ({ role: m.role as 'user' | 'assistant' | 'system', content: m.content, timestamp: Date.now() })), projectPath, createdAt: Date.now(), updatedAt: Date.now() }; const filePath = path.join(HISTORY_DIR, `${sessionId}.json`); fs.writeFileSync(filePath, JSON.stringify(session, null, 2), 'utf-8'); return true; } catch { return false; } } /** * Load conversation from history */ export function loadConversation(sessionId: string): ConversationSession | null { try { const filePath = path.join(HISTORY_DIR, `${sessionId}.json`); if (!fs.existsSync(filePath)) return null; const content = fs.readFileSync(filePath, 'utf-8'); return JSON.parse(content) as ConversationSession; } catch { return null; } } /** * List recent conversations */ export function listConversations(limit: number = 10): { id: string; updatedAt: number; messageCount: number }[] { try { if (!fs.existsSync(HISTORY_DIR)) return []; const files = fs.readdirSync(HISTORY_DIR) .filter(f => f.endsWith('.json')) .map(f => { const filePath = path.join(HISTORY_DIR, f); const content = fs.readFileSync(filePath, 'utf-8'); const session = JSON.parse(content) as ConversationSession; return { id: session.id, updatedAt: session.updatedAt, messageCount: session.messages.length }; }) .sort((a, b) => b.updatedAt - a.updatedAt) .slice(0, limit); return files; } catch { return []; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babasida246/ai-mcp-gateway'

If you have feedback or need assistance with the MCP directory API, please join our Discord server