cntx-ui
Version:
Autonomous Repository Intelligence engine with web UI and MCP server. Unified semantic code understanding, local RAG, and agent working memory.
418 lines (408 loc) • 19 kB
JavaScript
/**
* Agent Runtime for Codebase Exploration and Development
* Now stateful with SQLite-based working memory
*/
import AgentTools from './agent-tools.js';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
export class AgentRuntime {
cntxServer;
db;
tools;
currentSessionId;
constructor(cntxServer) {
this.cntxServer = cntxServer;
this.db = cntxServer.databaseManager;
this.tools = new AgentTools(cntxServer);
this.currentSessionId = null;
}
/**
* Initialize or resume a session
*/
async startSession(id = null, title = 'New Exploration') {
this.currentSessionId = id || crypto.randomUUID();
this.db.createSession(this.currentSessionId, title);
// Refresh manifest when a new session starts
await this.generateAgentManifest();
return this.currentSessionId;
}
/**
* Generates a .cntx/AGENT.md manifest for machine consumption
*/
async generateAgentManifest() {
const overview = await this.getCodebaseOverview();
const summary = await this.getSemanticSummary();
const bundles = await this.analyzeBundles('all');
// Auto-generate tool reference from MCP server
let toolsReference = '';
if (this.cntxServer.mcpServer) {
const tools = this.cntxServer.mcpServer.getToolDefinitions();
toolsReference = tools
.filter(t => !t.name?.includes('activities'))
.map(t => {
let params = [];
if (t.inputSchema?.properties) {
params = Object.entries(t.inputSchema.properties).map(([name, prop]) => {
const isReq = t.inputSchema.required?.includes(name) ? 'required' : 'optional';
return `\`${name}\` (${prop.type}, ${isReq}): ${prop.description}`;
});
}
return `### \`${t.name}\`\n${t.description}\n${params.length > 0 ? '**Parameters:**\n- ' + params.join('\n- ') : '*No parameters required*'}\n`;
}).join('\n');
}
// Find TOOLS.md template
let toolsMdPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../templates/TOOLS.md');
if (!fs.existsSync(toolsMdPath)) {
// Fallback for dist/lib/ context
toolsMdPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../templates/TOOLS.md');
}
const manifest = `# 🤖 Agent Handshake: ${overview.projectPath.split('/').pop()}
## Project Overview
- **Path:** \`${overview.projectPath}\`
- **Total Files:** ${overview.totalFiles}
- **Semantic Intelligence:** ${summary.totalChunks} persistent chunks indexed.
## Codebase Organization (Bundles)
${bundles.map(b => `- **${b.name}**: ${b.purpose} (${b.fileCount} files)`).join('\n')}
## Intelligence Interface (MCP Tools)
You have access to a specialized "Repository Intelligence" engine. Use these tools for high-signal exploration:
${toolsReference || '*(MCP Server not yet initialized, tools will appear here)*'}
---
## 🛠 Complete Tool & API Reference
Refer to the dynamic reference below for full parameter schemas and HTTP fallback endpoints.
${fs.existsSync(toolsMdPath) ? fs.readFileSync(toolsMdPath, 'utf8') : '*(Tools documentation missing)*'}
## Working Memory
This agent is **stateful**. All interactions in this directory are logged to a persistent SQLite database (\`.cntx/bundles.db\`), allowing for context retention across sessions.
---
*Generated automatically by cntx-ui. Optimized for LLM consumption.*
`;
const manifestPath = path.join(this.cntxServer.CNTX_DIR, 'AGENT.md');
fs.writeFileSync(manifestPath, manifest, 'utf8');
if (this.cntxServer.verbose)
console.log('📄 Agent manifest updated: .cntx/AGENT.md');
}
/**
* Log an interaction to the agent's memory
*/
async logInteraction(role, content, metadata = {}) {
if (!this.currentSessionId)
await this.startSession();
this.db.addMessage(this.currentSessionId, role, content, metadata);
}
/**
* Discovery Mode: "Tell me about this codebase"
* Now logs the discovery process to memory
*/
async discoverCodebase(options = {}) {
const { scope = 'all', includeDetails = true, verbose = false } = options;
try {
await this.logInteraction('agent', `Starting codebase discovery for scope: ${scope}`);
const discovery = {
overview: await this.getCodebaseOverview(),
bundles: await this.analyzeBundles(scope, verbose),
architecture: await this.analyzeArchitecture(),
patterns: await this.identifyPatterns(),
recommendations: []
};
if (includeDetails) {
discovery.semanticSummary = await this.getSemanticSummary();
discovery.fileTypes = await this.analyzeFileTypes();
discovery.complexity = await this.analyzeComplexity();
}
discovery.recommendations = await this.generateDiscoveryRecommendations();
await this.logInteraction('agent', `Discovery complete. Found ${discovery.overview.totalFiles} files.`, { discovery });
return discovery;
}
catch (error) {
await this.logInteraction('agent', `Discovery failed: ${error.message}`);
throw new Error(`Discovery failed: ${error.message}`);
}
}
/**
* Query Mode: "Where is the user authentication handled?"
* Now recalls previous context from SQLite
*/
async answerQuery(question, options = {}) {
const { maxResults = 10, includeCode = false, query } = options;
const actualQuestion = question || query;
if (!actualQuestion) {
throw new Error('Missing question or query for search.');
}
try {
await this.logInteraction('user', actualQuestion);
// Perform semantic search via Vector Store
let combinedResults = await this.cntxServer.vectorStore.search(actualQuestion, { limit: maxResults });
// Heuristic fallback for common onboarding questions if results are poor
const lowConfidence = combinedResults.length === 0 || combinedResults[0].similarity < 0.6;
const isEntryQuery = /entry|start|main|index|run/i.test(actualQuestion);
const isModelQuery = /model|schema|data|db|database/i.test(actualQuestion);
let fallbackFiles = [];
if (lowConfidence && (isEntryQuery || isModelQuery)) {
const allFiles = this.cntxServer.fileSystemManager.getAllFiles();
if (isEntryQuery) {
// Look for common entry points like main.tsx, cli.js, bin.js, App.tsx, etc.
const entryPatterns = [/main/i, /index/i, /app\./i, /router/i, /server/i, /cli/i, /bin/i];
entryPatterns.forEach(pattern => {
fallbackFiles.push(...allFiles.filter(f => pattern.test(f)).slice(0, 3));
});
}
if (isModelQuery) {
const modelPatterns = [/model/i, /schema/i, /db/i, /database/i, /entity/i];
modelPatterns.forEach(pattern => {
fallbackFiles.push(...allFiles.filter(f => pattern.test(f)).slice(0, 3));
});
}
fallbackFiles = [...new Set(fallbackFiles)].slice(0, 8);
}
// Generate contextual answer
const answer = await this.generateContextualAnswer(question, {
chunks: combinedResults,
files: fallbackFiles
}, includeCode);
// If no semantic results but we found fallbacks, improve the answer
if (combinedResults.length === 0 && fallbackFiles.length > 0) {
answer.response = `I couldn't find exact semantic matches, but based on common project structures, these files look relevant: ${fallbackFiles.join(', ')}`;
answer.confidence = 0.4;
}
const response = {
question,
answer: answer.response,
evidence: answer.evidence,
confidence: answer.confidence,
relatedFiles: [...new Set([
...combinedResults.map(c => c.filePath),
...fallbackFiles
])].slice(0, 8)
};
await this.logInteraction('agent', response.answer, { response });
return response;
}
catch (error) {
throw new Error(`Query failed: ${error.message}`);
}
}
/**
* Feature Investigation Mode: Now persists the investigation approach
*/
async investigateFeature(featureDescription, options = {}) {
const { includeRecommendations = true, feature, description, area } = options;
const actualDescription = featureDescription || feature || description || area;
if (!actualDescription) {
throw new Error('Missing feature description for investigation.');
}
try {
await this.logInteraction('user', `Investigating feature: ${actualDescription}`);
const investigation = {
feature: actualDescription,
existing: await this.findExistingImplementations(actualDescription),
related: await this.findRelatedCode(actualDescription),
integration: await this.findIntegrationPoints(actualDescription)
};
if (includeRecommendations) {
investigation.approach = await this.suggestImplementationApproach(investigation);
}
await this.logInteraction('agent', `Investigation complete for ${featureDescription}`, { investigation });
return investigation;
}
catch (error) {
throw new Error(`Feature investigation failed: ${error.message}`);
}
}
// --- Helper Methods ---
async getCodebaseOverview() {
const bundles = Array.from(this.cntxServer.bundleManager.getAllBundleInfo());
const totalFiles = this.cntxServer.fileSystemManager.getAllFiles().length;
const masterBundle = bundles.find(b => b.name === 'master');
const totalSize = masterBundle ? masterBundle.size : bundles.reduce((sum, b) => sum + b.size, 0);
return {
projectPath: this.cntxServer.CWD,
totalBundles: bundles.length,
totalFiles,
totalSize,
bundleNames: bundles.map(b => b.name)
};
}
async analyzeBundles(scope, verbose = false) {
const bundles = this.cntxServer.bundleManager.getAllBundleInfo();
const filtered = scope === 'all' ? bundles : bundles.filter(b => b.name === scope);
return filtered.map(b => {
const files = b.files || [];
const purpose = this.inferBundlePurpose(b.name, files);
// Implement compact mode: only show top 5 files if not verbose
let displayFiles = files;
if (!verbose && files.length > 5) {
// Pick high-signal files: main, index, App, or just the first few
const keyFiles = files.filter(f => /main|index|app|router|api|models/i.test(f));
displayFiles = [...new Set([...keyFiles, ...files])].slice(0, 5);
}
return {
...b,
purpose,
files: displayFiles,
totalFiles: files.length,
isTruncated: !verbose && files.length > 5
};
});
}
inferBundlePurpose(name, files) {
const n = name.toLowerCase();
if (n === 'master')
return 'Full Project Index (Source of Truth)';
if (n.startsWith('smart:'))
return 'Auto-grouped Code Structures (' + n.split('-').pop() + ')';
if (n.includes('component') || n.includes('ui') || n.includes('view') || n.includes('screen'))
return 'UI Components & Views';
if (n.includes('api') || n.includes('server') || n.includes('backend') || n.includes('netlify'))
return 'Backend API & Functions';
if (n.includes('hook'))
return 'React Hooks';
if (n.includes('util') || n.includes('helper'))
return 'Utility functions';
if (n.includes('lib') || n.includes('service') || n.includes('store'))
return 'Business logic & services';
if (n.includes('database') || n.includes('db') || n.includes('model') || n.includes('schema'))
return 'Data models & DB';
if (n.includes('test') || n.includes('spec'))
return 'Test suite';
if (n.includes('doc') || n.includes('readme'))
return 'Documentation';
if (n.includes('script') || n.includes('bin'))
return 'Scripts & CLI';
if (n.includes('asset') || n.includes('public'))
return 'Assets & static files';
if (n.includes('style') || n.includes('css'))
return 'Styles';
// Fallback to file extension analysis if name is generic
if (files.some(f => f.endsWith('.rs')))
return 'Rust Source';
if (files.some(f => f.endsWith('.ts') || f.endsWith('.tsx')))
return 'TypeScript Source';
return 'General Module';
}
async analyzeArchitecture() {
return {
type: 'Dynamic Architecture',
timestamp: new Date().toISOString()
};
}
async identifyPatterns() {
return {
coding: 'Modern Node.js',
style: 'Functional / Modular'
};
}
async getSemanticSummary() {
const chunks = this.db.db.prepare('SELECT COUNT(*) as count FROM semantic_chunks').get();
return { totalChunks: chunks.count };
}
async analyzeFileTypes() {
const rows = this.db.db.prepare('SELECT file_path FROM semantic_chunks').all();
const exts = {};
rows.forEach(r => {
const ext = r.file_path.split('.').pop() || 'unknown';
exts[ext] = (exts[ext] || 0) + 1;
});
return exts;
}
async analyzeComplexity() {
const rows = this.db.db.prepare('SELECT complexity_score FROM semantic_chunks').all();
const scores = { low: 0, medium: 0, high: 0 };
rows.forEach(r => {
if (r.complexity_score < 5)
scores.low++;
else if (r.complexity_score < 15)
scores.medium++;
else
scores.high++;
});
return scores;
}
async generateDiscoveryRecommendations() {
return [{ type: 'info', message: 'Continue organizing by semantic purpose.' }];
}
async findExistingImplementations(featureDescription) {
const results = await this.cntxServer.vectorStore.search(featureDescription, { limit: 5 });
return results.map(r => ({
file: r.filePath,
name: r.name,
purpose: r.purpose,
relevance: r.similarity
}));
}
async findRelatedCode(featureDescription) {
// Search for keywords in the description
const keywords = featureDescription.split(' ').filter(w => w.length > 4);
const allFiles = this.cntxServer.fileSystemManager.getAllFiles();
const matches = allFiles.filter(f => keywords.some(k => f.toLowerCase().includes(k.toLowerCase()))).slice(0, 5);
return matches.map(f => ({
file: f,
reason: 'Filename contains relevant keywords'
}));
}
async findIntegrationPoints(featureDescription) {
const existing = await this.findExistingImplementations(featureDescription);
const related = await this.findRelatedCode(featureDescription);
const candidates = [...new Set([
...existing.map(e => e.file),
...related.map(r => r.file)
])];
return candidates.map(f => {
const ext = path.extname(f);
let role = 'Likely touch point';
if (ext === '.rs')
role = 'Backend logic (Rust)';
if (ext === '.tsx')
role = 'UI/Frontend component';
if (f.includes('router') || f.includes('api'))
role = 'API/Routing';
if (f.includes('store') || f.includes('hook'))
role = 'State/Data management';
return { file: f, role };
});
}
async suggestImplementationApproach(investigation) {
const points = investigation.integration || [];
if (points.length === 0) {
return {
strategy: 'Exploratory Search',
description: 'No clear integration points found. Recommendation: Perform a broader semantic search for core business entities.'
};
}
const primaryFile = points[0].file;
return {
strategy: `Extend ${primaryFile}`,
description: `Based on the feature description, the primary integration point seems to be ${primaryFile}. You should examine this file and its dependencies to determine the exact insertion point.`,
steps: [
`1. Analyze ${primaryFile} for existing patterns.`,
`2. Check related files: ${points.slice(1, 3).map((p) => p.file).join(', ')}`,
`3. Implement the feature following the established coding style.`
]
};
}
async generateContextualAnswer(question, results, includeCode) {
let response = `Based on the codebase analysis:\n\n`;
const hasSemantic = results.chunks.length > 0;
const hasFallbacks = results.files && results.files.length > 0;
if (hasSemantic) {
const top = results.chunks[0];
response += `The most relevant implementation found is \`${top.name}\` in \`${top.filePath}\` (Purpose: ${top.purpose}).\n\n`;
}
else if (hasFallbacks) {
response += `I couldn't find an exact semantic match for your query, but these files look like strong candidates for the entry point or data model:\n\n`;
results.files.forEach((f) => {
response += `- \`${f}\`\n`;
});
response += `\nYou should start by examining these files.`;
}
else {
response += `No direct semantic matches found. Try refining your query.`;
}
return {
response,
evidence: results.chunks.slice(0, 3),
confidence: hasSemantic ? 0.8 : (hasFallbacks ? 0.4 : 0.2)
};
}
}
export default AgentRuntime;