repository-analyzer
Version:
Transform code repositories into strategic intelligence using extensible AI agents. Analyze technical debt, business value, and deployment readiness automatically.
390 lines ⢠16.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentDiscovery = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const glob_1 = require("glob");
const chalk_1 = __importDefault(require("chalk"));
class AgentDiscovery {
constructor() {
this.agentPaths = [];
this.discoveredAgents = new Map();
this.initializeAgentPaths();
}
initializeAgentPaths() {
// Core agents (bundled with package)
const packageRoot = this.getPackageRoot();
this.agentPaths.push(path.join(packageRoot, 'agents'));
// User-specific agents directory
const homeDir = process.env.HOME || process.env.USERPROFILE;
if (homeDir) {
this.agentPaths.push(path.join(homeDir, '.repo-analyzer', 'agents'));
}
// Project-specific agents (current working directory)
this.agentPaths.push(path.join(process.cwd(), 'agents'));
this.agentPaths.push(path.join(process.cwd(), '.repo-analyzer', 'agents'));
}
getPackageRoot() {
// Find package root by looking for package.json
let currentDir = __dirname;
while (currentDir !== path.dirname(currentDir)) {
if (fs.existsSync(path.join(currentDir, 'package.json'))) {
return currentDir;
}
currentDir = path.dirname(currentDir);
}
return __dirname; // fallback
}
async discoverAgents() {
this.discoveredAgents.clear();
for (const agentsPath of this.agentPaths) {
if (await fs.pathExists(agentsPath)) {
await this.scanDirectory(agentsPath);
}
}
// Convert to array and sort by order and name
const agents = Array.from(this.discoveredAgents.values()).sort((a, b) => {
if (a.order !== b.order) {
return a.order - b.order;
}
return a.name.localeCompare(b.name);
});
return agents;
}
async scanDirectory(dirPath) {
const category = this.getCategoryForPath(dirPath);
try {
const agentFiles = await (0, glob_1.glob)('**/*.md', {
cwd: dirPath,
absolute: true,
ignore: ['**/node_modules/**', '**/.*/**']
});
for (const agentFile of agentFiles) {
const agentInfo = await this.parseAgentFile(agentFile, category);
if (agentInfo) {
// Prioritize core agents - don't override core agents with custom ones
const existingAgent = this.discoveredAgents.get(agentInfo.id);
if (!existingAgent || (existingAgent.category !== 'core' || agentInfo.category === 'core')) {
this.discoveredAgents.set(agentInfo.id, agentInfo);
}
}
}
}
catch (error) {
// Silently ignore directories that can't be read
}
}
getCategoryForPath(dirPath) {
const packageRoot = this.getPackageRoot();
const coreAgentsPath = path.join(packageRoot, 'agents');
if (dirPath.startsWith(coreAgentsPath)) {
return 'core';
}
if (dirPath.includes('.repo-analyzer')) {
return 'community';
}
return 'custom';
}
async parseAgentFile(filePath, category) {
try {
const content = await fs.readFile(filePath, 'utf8');
const fileName = path.basename(filePath, '.md');
// Extract metadata from markdown frontmatter or comments
const metadata = this.extractMetadata(content, fileName);
return {
id: metadata.id || this.generateIdFromFileName(fileName),
name: metadata.name || this.generateNameFromFileName(fileName),
description: metadata.description || this.extractDescription(content),
filePath,
order: metadata.order || this.inferOrderFromFileName(fileName),
dependencies: metadata.dependencies || this.extractDependencies(content),
outputs: metadata.outputs || this.extractOutputs(content),
category
};
}
catch (error) {
console.warn(chalk_1.default.yellow(`Warning: Could not parse agent file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`));
return null;
}
}
extractMetadata(content, fileName) {
const metadata = {};
// Try to extract YAML frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
try {
const yaml = require('yaml');
const frontmatter = yaml.parse(frontmatterMatch[1]);
metadata.id = frontmatter.id;
metadata.name = frontmatter.name;
metadata.description = frontmatter.description;
metadata.order = frontmatter.order;
metadata.dependencies = frontmatter.dependencies || frontmatter.requires;
metadata.outputs = frontmatter.outputs || frontmatter.produces;
}
catch (error) {
// Ignore YAML parsing errors
}
}
// Try to extract from HTML-style comments
const commentMatches = content.matchAll(/<!--\s*(\w+):\s*(.*?)\s*-->/g);
for (const match of commentMatches) {
const [, key, value] = match;
switch (key.toLowerCase()) {
case 'id':
metadata.id = value;
break;
case 'name':
metadata.name = value;
break;
case 'description':
metadata.description = value;
break;
case 'order':
metadata.order = parseInt(value, 10);
break;
case 'dependencies':
case 'requires':
metadata.dependencies = value.split(',').map(s => s.trim());
break;
case 'outputs':
case 'produces':
metadata.outputs = value.split(',').map(s => s.trim());
break;
}
}
return metadata;
}
generateIdFromFileName(fileName) {
// Remove numeric prefixes and convert to kebab-case
return fileName
.replace(/^\d+-/, '') // Remove leading numbers and dash
.replace(/[_\s]+/g, '-') // Replace underscores and spaces with dashes
.toLowerCase();
}
generateNameFromFileName(fileName) {
// Convert filename to readable name
return fileName
.replace(/^\d+-/, '') // Remove leading numbers and dash
.replace(/[_-]/g, ' ') // Replace underscores and dashes with spaces
.replace(/\b\w/g, l => l.toUpperCase()); // Capitalize words
}
extractDescription(content) {
// Try to find description in various places
// Look for "## Description" or "## Role" section
const descMatch = content.match(/##\s+(Description|Role)\s*\n(.*?)(?=\n##|\n#|$)/s);
if (descMatch) {
return descMatch[2].trim().split('\n')[0]; // First line of description
}
// Look for "You are..." pattern
const roleMatch = content.match(/You are ([^.]+)/);
if (roleMatch) {
return roleMatch[1];
}
// Look for first paragraph after title
const paragraphMatch = content.match(/^#[^\n]*\n\s*\n([^\n]+)/);
if (paragraphMatch) {
return paragraphMatch[1].trim();
}
return 'No description available';
}
inferOrderFromFileName(fileName) {
// Extract numeric prefix if present
const orderMatch = fileName.match(/^(\d+)/);
if (orderMatch) {
return parseInt(orderMatch[1], 10);
}
// Default ordering for common agent types
const orderMap = {
'scanner': 10,
'scan': 10,
'discovery': 10,
'technical': 20,
'tech': 20,
'business': 30,
'biz': 30,
'report': 90,
'generator': 90,
'summary': 95,
};
for (const [keyword, order] of Object.entries(orderMap)) {
if (fileName.toLowerCase().includes(keyword)) {
return order;
}
}
return 50; // Default order
}
extractDependencies(content) {
const dependencies = [];
// Look for "Prerequisites" or "Dependencies" section
const depsMatch = content.match(/##\s+(Prerequisites|Dependencies|Requires)\s*\n(.*?)(?=\n##|\n#|$)/s);
if (depsMatch) {
const depsText = depsMatch[2];
// Extract agent references
const agentRefs = depsText.matchAll(/(?:output from|requires?|depends on)\s+([a-zA-Z-_]+)\s+agent/gi);
for (const match of agentRefs) {
dependencies.push(match[1].toLowerCase());
}
}
// Look for variable references like {SCANNER_OUTPUT}
const varRefs = content.matchAll(/\{(\w+)_OUTPUT\}/g);
for (const match of varRefs) {
dependencies.push(match[1].toLowerCase());
}
return [...new Set(dependencies)]; // Remove duplicates
}
extractOutputs(content) {
const outputs = [];
// Look for "Output" section
const outputMatch = content.match(/##\s+(Output|Returns?|Produces?)\s*\n(.*?)(?=\n##|\n#|$)/s);
if (outputMatch) {
const outputText = outputMatch[2];
// Extract JSON file references
const jsonRefs = outputText.matchAll(/(\w+(?:_output)?\.json)/gi);
for (const match of jsonRefs) {
outputs.push(match[1]);
}
}
// Infer output from agent name
const agentName = path.basename(path.dirname(this.discoveredAgents.values().next().value?.filePath || ''));
if (agentName) {
outputs.push(`${agentName}_output.json`);
}
return [...new Set(outputs)]; // Remove duplicates
}
async getAgent(agentId) {
const agents = await this.discoverAgents();
return agents.find(agent => agent.id === agentId) || null;
}
async getAgentsByCategory(category) {
const agents = await this.discoverAgents();
return agents.filter(agent => agent.category === category);
}
async validateAgentDependencies(selectedAgents) {
const agents = await this.discoverAgents();
const agentMap = new Map(agents.map(agent => [agent.id, agent]));
const missing = [];
for (const agentId of selectedAgents) {
const agent = agentMap.get(agentId);
if (!agent) {
missing.push(agentId);
continue;
}
for (const dependency of agent.dependencies) {
if (!selectedAgents.includes(dependency)) {
missing.push(`${agentId} requires ${dependency}`);
}
}
}
return {
valid: missing.length === 0,
missing
};
}
async createAgentExecutionPlan(selectedAgents) {
const agents = await this.discoverAgents();
const agentMap = new Map(agents.map(agent => [agent.id, agent]));
// Topological sort based on dependencies
const visited = new Set();
const visiting = new Set();
const plan = [];
const currentBatch = [];
const visit = (agentId) => {
if (visiting.has(agentId)) {
throw new Error(`Circular dependency detected involving ${agentId}`);
}
if (visited.has(agentId)) {
return;
}
visiting.add(agentId);
const agent = agentMap.get(agentId);
if (agent) {
// Visit dependencies first
for (const dep of agent.dependencies) {
if (selectedAgents.includes(dep)) {
visit(dep);
}
}
}
visiting.delete(agentId);
visited.add(agentId);
// Add to current batch if no unresolved dependencies
if (agent) {
const unresolvedDeps = agent.dependencies.filter(dep => selectedAgents.includes(dep) && !visited.has(dep));
if (unresolvedDeps.length === 0) {
currentBatch.push(agentId);
}
}
};
// Process all selected agents
for (const agentId of selectedAgents) {
if (!visited.has(agentId)) {
visit(agentId);
if (currentBatch.length > 0) {
plan.push([...currentBatch]);
currentBatch.length = 0;
}
}
}
return plan;
}
printAgentList(agents) {
console.log(chalk_1.default.bold('\nš Available Agents:\n'));
const categories = ['core', 'custom', 'community'];
for (const category of categories) {
const categoryAgents = agents.filter(agent => agent.category === category);
if (categoryAgents.length === 0)
continue;
const categoryIcon = category === 'core' ? 'š§' : category === 'custom' ? 'šØ' : 'š';
console.log(chalk_1.default.bold(`${categoryIcon} ${category.toUpperCase()} AGENTS:`));
for (const agent of categoryAgents) {
const orderStr = agent.order.toString().padStart(2, '0');
const nameColor = category === 'core' ? chalk_1.default.blue : category === 'custom' ? chalk_1.default.green : chalk_1.default.cyan;
console.log(` ${orderStr}. ${nameColor(agent.name)} (${agent.id})`);
console.log(` ${chalk_1.default.gray(agent.description)}`);
if (agent.dependencies.length > 0) {
console.log(` ${chalk_1.default.yellow('Requires:')} ${agent.dependencies.join(', ')}`);
}
console.log();
}
}
}
}
exports.AgentDiscovery = AgentDiscovery;
//# sourceMappingURL=AgentDiscovery.js.map