diff --git a/src/agent/agent-system-prompt.ts b/src/agent/agent-system-prompt.ts
index ea8153b..8b2ed79 100644
--- a/src/agent/agent-system-prompt.ts
+++ b/src/agent/agent-system-prompt.ts
@@ -145,24 +145,76 @@ export function getInteractiveSystemPrompt(): string {
{ "path": "string", "depth": "integer ≥ 1" }
+
+
+ Find similar code patterns or implementations in the codebase for a given domain or file type.
+
+ {
+ "domain": "string - domain or feature area (e.g., 'components', 'auth', 'api')",
+ "filePattern": "string - file pattern to match (e.g., '*.tsx', 'Button*'), or empty string for no pattern",
+ "maxResults": "integer - maximum examples to return (1-10, default 5)"
+ }
+
+
+
+
+ Find available utility functions, hooks, or services that might be relevant to the current changes.
+
+ {
+ "category": "string - category to search for (e.g., 'validation', 'formatting', 'api', 'hooks')",
+ "keyword": "string - optional keyword to filter results, or empty string for no keyword filter"
+ }
+
+
+
+
+ Analyze the architectural context and relationships for changed files.
+
+ {
+ "filePaths": "array of strings - file paths to analyze",
+ "depth": "integer - depth of analysis (1-3, default 2)"
+ }
+
+
-
+
- Analyse the diff using the review guidelines below.
+
+ **Understand the Intent**: Review the provided context about PR description, feature intent, and business goals.
+ What is this change trying to accomplish? Why was this approach chosen?
+
- If extra context is needed (e.g., to verify imports, understand full function context, check test coverage), call exactly **one** tool and STOP.
- • Do not output any other text.
- • Example call (JSON is generated automatically by the model):
- { "name": "fetch_file", "arguments": { "path": "src/api/user.py" } }
+ **Gather Strategic Context**: Before diving into line-by-line review, use tools to understand:
+ • How similar features are implemented in this codebase (find_patterns)
+ • What utilities/patterns are available that might be relevant (find_utilities)
+ • How the changed files fit into the broader architecture (analyze_architecture)
+ • Full context of changed files if the diff seems incomplete (fetch_file)
- After the tool result arrives (as a tool message), repeat
- steps 1-2 until no more context is required.
+ **Review for Architectural Fit**: Focus on high-impact issues:
+ • Does this follow established patterns in the codebase?
+ • Are they using the right abstractions and utilities?
+ • Does this integrate well with the existing architecture?
+ • Are there better alternatives available in the codebase?
- When confident, emit review comments using the XML schema in the response format section.
+ **Quality and Consistency**: Review for:
+ • Code quality and maintainability
+ • Consistency with team conventions
+ • Potential edge cases or bugs
+ • Test coverage if applicable
+
+ **Prioritize and Consolidate**: Focus on the most important issues that would help the developer improve.
+ Avoid duplicate comments on the same pattern across different locations.
+ Group similar issues into comprehensive feedback.
+
+
+ **Tool Usage Guidelines**:
+ • Call exactly **one** tool per response and STOP (no other text)
+ • Use tools strategically to understand patterns and context, not just verify syntax
+ • Example: { "name": "find_patterns", "arguments": { "domain": "components", "filePattern": "*Button*" } }
diff --git a/src/agent/index.ts b/src/agent/index.ts
index 731198d..6fde45e 100644
--- a/src/agent/index.ts
+++ b/src/agent/index.ts
@@ -123,11 +123,29 @@ export async function reviewChunkWithAgent(
existingCommentsContext = contextLines.join("\n");
}
+ // Extract feature intent from the diff summary for contextual review
+ let featureContext = "";
+ if (diffSummary) {
+ featureContext = `
+
+ ${escapeXml(diffSummary.prType || "Code changes")}
+ ${escapeXml(diffSummary.summaryPoints.join(". ") || "No summary available")}
+ ${escapeXml(diffSummary.prDescription || "No description provided")}
+ ${diffSummary.keyRisks.length > 0 ? `
+
+ ${diffSummary.keyRisks.map(risk => `${escapeXml(risk.description)}`).join('\n ')}
+ ` : ''}
+`;
+ }
+
const initialMessage = `
${escapeXml(fileList)}
+
+ ${featureContext}
+
${escapeXml(summaryContext)}
@@ -139,7 +157,24 @@ export async function reviewChunkWithAgent(
- Please review this diff chunk using the provided context. ${existingComments.length > 0 ? "Pay special attention to the existing comments:\n 1. Avoid creating duplicate or similar comments unless you have significantly different insights.\n 2. Analyze whether any existing comments have been addressed by the changes in this diff.\n 3. If you find that an existing comment has been resolved by the code changes, include it in the section with a clear explanation of how it was addressed." : ""}
+ Review this diff chunk like a human team member would:
+
+ 1. **Understand the Intent**: What is this change trying to accomplish? Use the feature context above.
+
+ 2. **Gather Context**: Use the available tools to understand:
+ - How similar features are implemented in this codebase
+ - What utilities or patterns are available that might be relevant
+ - How these files fit into the broader architecture
+
+ 3. **Focus on Architecture**: Does this follow established patterns? Are they using the right abstractions?
+
+ 4. **Quality Review**: Look for maintainability, edge cases, and consistency with team conventions.
+
+ 5. **Prioritize Impact**: Focus on issues that would genuinely help the developer improve the codebase.
+
+ ${existingComments.length > 0 ? "Special attention to existing comments:\n - Avoid creating duplicate or similar comments unless you have significantly different insights\n - Analyze whether any existing comments have been addressed by the changes\n - If existing comments are resolved by the code changes, include them in the section" : ""}
+
+ Take your time to gather the right context before providing feedback. Use the tools strategically to understand patterns and best practices in this codebase.
`;
diff --git a/src/agent/tools.ts b/src/agent/tools.ts
index a669870..5c55685 100644
--- a/src/agent/tools.ts
+++ b/src/agent/tools.ts
@@ -282,4 +282,218 @@ export const depGraphTool = tool({
},
});
-export const allTools = [fetchFileTool, fetchSnippetTool, depGraphTool];
+/**
+ * Tool to find similar patterns in the codebase
+ */
+export const findPatternsTool = tool({
+ name: "find_patterns",
+ description: "Find similar code patterns or implementations in the codebase for a given domain or file type",
+ parameters: z.object({
+ domain: z.string().describe("Domain or feature area (e.g., 'components', 'auth', 'api')"),
+ filePattern: z.string().nullable().default("").describe("File pattern to match (e.g., '*.tsx', 'Button*'), or empty string for no pattern"),
+ maxResults: z.number().int().min(1).max(10).default(5).describe("Maximum number of examples to return"),
+ }),
+ execute: async ({ domain, filePattern, maxResults }) => {
+ const sourceFiles = getAllSourceFiles();
+
+ let matchingFiles = sourceFiles.filter(file => {
+ if (filePattern && filePattern.trim() !== "") {
+ const pattern = filePattern.replace(/\*/g, '.*');
+ const regex = new RegExp(pattern, 'i');
+ return regex.test(file);
+ }
+ return file.toLowerCase().includes(domain.toLowerCase());
+ });
+
+ matchingFiles = matchingFiles.slice(0, maxResults);
+
+ const results: string[] = [];
+ for (const file of matchingFiles) {
+ try {
+ const absolutePath = resolve(process.cwd(), file);
+ if (existsSync(absolutePath)) {
+ const content = readFileSync(absolutePath, 'utf-8');
+ // Get first 50 lines for pattern analysis
+ const preview = content.split('\n').slice(0, 50).join('\n');
+ results.push(`\n=== ${file} ===\n${preview}\n`);
+ }
+ } catch {
+ results.push(`\n=== ${file} ===\nError reading file\n`);
+ }
+ }
+
+ if (results.length === 0) {
+ return `No patterns found for domain "${domain}"${filePattern && filePattern.trim() !== "" ? ` with pattern "${filePattern}"` : ''}`;
+ }
+
+ return `Found ${results.length} pattern examples:\n${results.join('\n')}`;
+ },
+});
+
+/**
+ * Tool to find available utilities in the codebase
+ */
+export const findUtilitiesTool = tool({
+ name: "find_utilities",
+ description: "Find available utility functions, hooks, or services that might be relevant to the current changes",
+ parameters: z.object({
+ category: z.string().describe("Category to search for (e.g., 'validation', 'formatting', 'api', 'hooks')"),
+ keyword: z.string().nullable().default("").describe("Optional keyword to filter results, or empty string for no keyword filter"),
+ }),
+ execute: async ({ category, keyword }) => {
+ const sourceFiles = getAllSourceFiles();
+
+ // Look for utility files
+ const utilityFiles = sourceFiles.filter(file => {
+ const lowerFile = file.toLowerCase();
+ return (
+ lowerFile.includes('util') ||
+ lowerFile.includes('helper') ||
+ lowerFile.includes('hook') ||
+ lowerFile.includes('service') ||
+ lowerFile.includes(category.toLowerCase()) ||
+ (keyword && keyword.trim() !== "" && lowerFile.includes(keyword.toLowerCase()))
+ );
+ }).slice(0, 8);
+
+ const utilities: string[] = [];
+
+ for (const file of utilityFiles) {
+ try {
+ const absolutePath = resolve(process.cwd(), file);
+ if (existsSync(absolutePath)) {
+ const content = readFileSync(absolutePath, 'utf-8');
+
+ // Extract exported functions/constants
+ const exportMatches = [
+ ...content.matchAll(/export\s+(function|const|class)\s+(\w+)/g),
+ ...content.matchAll(/export\s*{\s*([^}]+)\s*}/g),
+ ];
+
+ if (exportMatches.length > 0) {
+ const exports = exportMatches.map(match => {
+ if (match[1] === 'function' || match[1] === 'const' || match[1] === 'class') {
+ return match[2];
+ } else {
+ // Parse export list
+ return match[1].split(',').map(exp => exp.trim()).join(', ');
+ }
+ }).join(', ');
+
+ // Get JSDoc comment if available
+ const commentMatch = content.match(/\/\*\*([\s\S]*?)\*\//);
+ const description = commentMatch
+ ? commentMatch[1].replace(/\s*\*\s?/g, ' ').trim().slice(0, 100)
+ : '';
+
+ utilities.push(`\n=== ${file} ===\nExports: ${exports}\n${description ? `Description: ${description}` : ''}`);
+ }
+ }
+ } catch {
+ // Skip files we can't read
+ }
+ }
+
+ if (utilities.length === 0) {
+ return `No utilities found for category "${category}"${keyword && keyword.trim() !== "" ? ` with keyword "${keyword}"` : ''}`;
+ }
+
+ return `Found ${utilities.length} relevant utilities:\n${utilities.join('\n')}`;
+ },
+});
+
+/**
+ * Tool to analyze architectural context
+ */
+export const analyzeArchitectureTool = tool({
+ name: "analyze_architecture",
+ description: "Analyze the architectural context and relationships for changed files",
+ parameters: z.object({
+ filePaths: z.array(z.string()).describe("Array of file paths to analyze"),
+ depth: z.number().int().min(1).max(3).default(2).describe("Depth of analysis"),
+ }),
+ execute: async ({ filePaths }) => {
+ const results: string[] = [];
+
+ for (const filePath of filePaths.slice(0, 5)) { // Limit to avoid overwhelming
+ if (!existsSync(resolve(process.cwd(), filePath))) {
+ results.push(`\n=== ${filePath} ===\nFile not found`);
+ continue;
+ }
+
+ // Get dependency information for the file
+ const { imports } = extractDependencies(filePath);
+ const importedBy = findImporters(filePath, getAllSourceFiles());
+
+ let depGraph = `Dependencies: ${imports.length > 0 ? imports.join(', ') : 'None'}`;
+ depGraph += `\nImported by: ${importedBy.length > 0 ? importedBy.join(', ') : 'None'}`;
+
+ // Analyze the file type and purpose
+ const fileType = inferFileType(filePath);
+ const purpose = await inferFilePurpose(filePath);
+
+ results.push(`\n=== ${filePath} ===`);
+ results.push(`Type: ${fileType}`);
+ results.push(`Purpose: ${purpose}`);
+ results.push(`Dependencies:`);
+ results.push(depGraph);
+ }
+
+ return results.join('\n');
+ },
+});
+
+// Helper functions for the new tools
+
+function inferFileType(filePath: string): string {
+ const ext = extname(filePath);
+ const dir = dirname(filePath);
+ const name = filePath.toLowerCase();
+
+ if (name.includes('component') || name.includes('page') || ext === '.tsx') {
+ return 'React Component';
+ }
+ if (name.includes('hook')) return 'Custom Hook';
+ if (name.includes('util') || name.includes('helper')) return 'Utility';
+ if (name.includes('service') || name.includes('api')) return 'Service';
+ if (name.includes('type') || name.includes('interface')) return 'Type Definition';
+ if (name.includes('test') || name.includes('spec')) return 'Test';
+ if (dir.includes('store') || name.includes('reducer')) return 'State Management';
+
+ return 'Module';
+}
+
+async function inferFilePurpose(filePath: string): Promise {
+ try {
+ const content = readFileSync(resolve(process.cwd(), filePath), 'utf-8');
+
+ // Look for JSDoc comments at the top
+ const docMatch = content.match(/\/\*\*([\s\S]*?)\*\//);
+ if (docMatch) {
+ const doc = docMatch[1].replace(/\s*\*\s?/g, ' ').trim();
+ if (doc.length > 10) {
+ return doc.slice(0, 100) + (doc.length > 100 ? '...' : '');
+ }
+ }
+
+ // Look for single-line comments
+ const commentMatch = content.match(/^\/\/\s*(.+)/m);
+ if (commentMatch) {
+ return commentMatch[1].trim().slice(0, 100);
+ }
+
+ // Infer from exports
+ if (content.includes('export default')) {
+ const defaultExport = content.match(/export default\s+(\w+)/);
+ if (defaultExport) {
+ return `Exports ${defaultExport[1]}`;
+ }
+ }
+
+ return 'Purpose not documented';
+ } catch {
+ return 'Could not analyze';
+ }
+}
+
+export const allTools = [fetchFileTool, fetchSnippetTool, depGraphTool, findPatternsTool, findUtilitiesTool, analyzeArchitectureTool];
diff --git a/src/context-gatherer.ts b/src/context-gatherer.ts
new file mode 100644
index 0000000..8766f77
--- /dev/null
+++ b/src/context-gatherer.ts
@@ -0,0 +1,396 @@
+import { ProcessableChunk } from "./diff-parser";
+import { DiffSummary, ModelConfig } from "./types";
+import { debugLog } from "./debug";
+import { readFileSync, existsSync } from "fs";
+import { resolve, dirname, extname } from "path";
+
+export interface ReviewContext {
+ featureIntent: FeatureIntent;
+ codebasePatterns: CodebasePattern[];
+ architecturalContext: ArchitecturalContext;
+ availableUtilities: AvailableUtility[];
+}
+
+export interface FeatureIntent {
+ summary: string;
+ domain: string; // e.g., "authentication", "ui-components", "data-layer"
+ changeType: string; // e.g., "feature", "bugfix", "refactor", "performance"
+ businessContext: string;
+ technicalApproach: string;
+}
+
+export interface CodebasePattern {
+ type: string; // e.g., "component-structure", "error-handling", "state-management"
+ pattern: string;
+ examples: string[];
+ files: string[];
+}
+
+export interface ArchitecturalContext {
+ relatedFiles: string[];
+ dataFlow: string[];
+ integrationPoints: string[];
+ potentialImpacts: string[];
+}
+
+export interface AvailableUtility {
+ type: string; // e.g., "helper-function", "hook", "component", "service"
+ name: string;
+ file: string;
+ description: string;
+ usage: string;
+}
+
+/**
+ * Gathers comprehensive context for human-like code reviews
+ */
+export class ContextGatherer {
+ private repoRoot: string;
+ private allSourceFiles: string[] = [];
+
+ constructor() {
+ this.repoRoot = process.cwd();
+ this.allSourceFiles = this.getAllSourceFiles();
+ }
+
+ /**
+ * Main method to gather all context before review
+ */
+ async gatherReviewContext(
+ chunks: ProcessableChunk[],
+ diffSummary?: DiffSummary,
+ prInfo?: any
+ ): Promise {
+ debugLog("🔍 Gathering comprehensive review context...");
+
+ const featureIntent = this.extractFeatureIntent(diffSummary, prInfo);
+ const codebasePatterns = await this.discoverCodebasePatterns(chunks, featureIntent);
+ const architecturalContext = await this.analyzeArchitecturalContext(chunks);
+ const availableUtilities = await this.findAvailableUtilities(chunks, featureIntent);
+
+ return {
+ featureIntent,
+ codebasePatterns,
+ architecturalContext,
+ availableUtilities,
+ };
+ }
+
+ /**
+ * Extract feature intent from PR description and diff summary
+ */
+ private extractFeatureIntent(
+ diffSummary?: DiffSummary,
+ prInfo?: any
+ ): FeatureIntent {
+ let summary = "Code changes";
+ let businessContext = "";
+ let technicalApproach = "";
+
+ // Use auto-generated PR description if available
+ if (diffSummary?.prDescription) {
+ summary = diffSummary.prDescription;
+ businessContext = this.extractBusinessContext(diffSummary.prDescription);
+ technicalApproach = this.extractTechnicalApproach(diffSummary.prDescription);
+ }
+
+ // Fallback to PR title/body if no generated description
+ if (prInfo?.title && (!diffSummary?.prDescription || diffSummary.prDescription.trim() === "")) {
+ summary = prInfo.title + (prInfo.body ? `\n${prInfo.body}` : "");
+ }
+
+ // Determine domain from file paths and content
+ const domain = this.inferDomain(diffSummary);
+ const changeType = this.inferChangeType(summary, diffSummary);
+
+ return {
+ summary,
+ domain,
+ changeType,
+ businessContext,
+ technicalApproach,
+ };
+ }
+
+ /**
+ * Discover existing patterns in the codebase that are relevant to the changes
+ */
+ private async discoverCodebasePatterns(
+ chunks: ProcessableChunk[],
+ featureIntent: FeatureIntent
+ ): Promise {
+ const patterns: CodebasePattern[] = [];
+
+ // Get all unique file paths from chunks
+ const changedFiles = [...new Set(chunks.map(chunk => chunk.fileName))];
+
+ for (const file of changedFiles) {
+ // Find similar files based on naming patterns and directory structure
+ const similarFiles = this.findSimilarFiles(file, featureIntent.domain);
+
+ if (similarFiles.length > 0) {
+ // Analyze patterns in similar files
+ const filePatterns = await this.extractPatternsFromFiles(similarFiles, file);
+ patterns.push(...filePatterns);
+ }
+ }
+
+ return this.deduplicatePatterns(patterns);
+ }
+
+ /**
+ * Analyze architectural context and relationships
+ */
+ private async analyzeArchitecturalContext(
+ chunks: ProcessableChunk[]
+ ): Promise {
+ const relatedFiles: string[] = [];
+ const dataFlow: string[] = [];
+ const integrationPoints: string[] = [];
+ const potentialImpacts: string[] = [];
+
+ // For each changed file, find its dependencies and dependents
+ for (const chunk of chunks) {
+ const deps = this.extractDependencies(chunk.fileName);
+ relatedFiles.push(...deps.imports);
+
+ // Find files that import this file
+ const dependents = this.findDependents(chunk.fileName);
+ potentialImpacts.push(...dependents);
+ }
+
+ return {
+ relatedFiles: [...new Set(relatedFiles)],
+ dataFlow,
+ integrationPoints,
+ potentialImpacts: [...new Set(potentialImpacts)],
+ };
+ }
+
+ /**
+ * Find available utilities that could be relevant to the changes
+ */
+ private async findAvailableUtilities(
+ chunks: ProcessableChunk[],
+ featureIntent: FeatureIntent
+ ): Promise {
+ const utilities: AvailableUtility[] = [];
+
+ // Based on the domain, look for relevant utility files
+ const utilityPatterns = this.getUtilityPatterns(featureIntent.domain);
+
+ for (const pattern of utilityPatterns) {
+ const matchingFiles = this.allSourceFiles.filter(file =>
+ file.includes(pattern) ||
+ file.includes('utils') ||
+ file.includes('helpers') ||
+ file.includes('hooks') ||
+ file.includes('services')
+ );
+
+ for (const file of matchingFiles.slice(0, 5)) { // Limit to avoid overwhelming
+ const utility = await this.analyzeUtilityFile(file);
+ if (utility) {
+ utilities.push(utility);
+ }
+ }
+ }
+
+ return utilities;
+ }
+
+ // Helper methods
+
+ private extractBusinessContext(description: string): string {
+ // Look for business context keywords
+ const businessKeywords = /\b(user|customer|business|feature|requirement|goal)\b/gi;
+ const lines = description.split('\n');
+ return lines.filter(line => businessKeywords.test(line)).join(' ').slice(0, 200);
+ }
+
+ private extractTechnicalApproach(description: string): string {
+ // Look for technical approach keywords
+ const techKeywords = /\b(implement|using|with|library|framework|pattern|architecture)\b/gi;
+ const lines = description.split('\n');
+ return lines.filter(line => techKeywords.test(line)).join(' ').slice(0, 200);
+ }
+
+ private inferDomain(diffSummary?: DiffSummary): string {
+ if (!diffSummary) return "general";
+
+ // Use the PR type if available
+ if (diffSummary.prType) {
+ return diffSummary.prType.toLowerCase().replace(/\s+/g, '-');
+ }
+
+ // Fallback to analyzing file paths
+ return "general";
+ }
+
+ private inferChangeType(summary: string, diffSummary?: DiffSummary): string {
+ const lowerSummary = summary.toLowerCase();
+
+ if (lowerSummary.includes('add') || lowerSummary.includes('new')) return 'feature';
+ if (lowerSummary.includes('fix') || lowerSummary.includes('bug')) return 'bugfix';
+ if (lowerSummary.includes('refactor') || lowerSummary.includes('restructure')) return 'refactor';
+ if (lowerSummary.includes('performance') || lowerSummary.includes('optimize')) return 'performance';
+ if (lowerSummary.includes('update') || lowerSummary.includes('upgrade')) return 'update';
+
+ return 'enhancement';
+ }
+
+ private findSimilarFiles(targetFile: string, domain: string): string[] {
+ const targetDir = dirname(targetFile);
+ const targetExt = extname(targetFile);
+
+ return this.allSourceFiles.filter(file => {
+ // Same directory or similar naming pattern
+ return (
+ dirname(file) === targetDir ||
+ file.includes(domain) ||
+ (extname(file) === targetExt && this.calculateSimilarity(file, targetFile) > 0.3)
+ );
+ }).slice(0, 10); // Limit results
+ }
+
+ private calculateSimilarity(file1: string, file2: string): number {
+ // Simple similarity based on path segments
+ const segments1 = file1.split('/');
+ const segments2 = file2.split('/');
+
+ const commonSegments = segments1.filter(seg => segments2.includes(seg));
+ return commonSegments.length / Math.max(segments1.length, segments2.length);
+ }
+
+ private async extractPatternsFromFiles(files: string[], contextFile: string): Promise {
+ // This would analyze the files to extract common patterns
+ // For now, return a simplified version
+ return [
+ {
+ type: "component-structure",
+ pattern: "Function components with TypeScript interfaces",
+ examples: files.slice(0, 3),
+ files,
+ }
+ ];
+ }
+
+ private deduplicatePatterns(patterns: CodebasePattern[]): CodebasePattern[] {
+ const seen = new Set();
+ return patterns.filter(pattern => {
+ const key = `${pattern.type}-${pattern.pattern}`;
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+ }
+
+ private extractDependencies(filePath: string): { imports: string[]; exports: string[] } {
+ // Simplified version - in practice, this would parse the file
+ return { imports: [], exports: [] };
+ }
+
+ private findDependents(filePath: string): string[] {
+ // Find files that import the given file
+ return this.allSourceFiles.filter(file => {
+ if (file === filePath) return false;
+
+ try {
+ const content = readFileSync(resolve(this.repoRoot, file), 'utf-8');
+ return content.includes(filePath) || content.includes(filePath.replace('.ts', ''));
+ } catch {
+ return false;
+ }
+ }).slice(0, 5); // Limit results
+ }
+
+ private getUtilityPatterns(domain: string): string[] {
+ const commonPatterns = ['utils', 'helpers', 'hooks', 'services'];
+
+ const domainSpecific: { [key: string]: string[] } = {
+ 'ui-components': ['components', 'ui', 'styled'],
+ 'authentication': ['auth', 'security', 'session'],
+ 'data-layer': ['api', 'store', 'models', 'schema'],
+ 'routing': ['router', 'navigation', 'routes'],
+ };
+
+ return [...commonPatterns, ...(domainSpecific[domain] || [])];
+ }
+
+ private async analyzeUtilityFile(filePath: string): Promise {
+ try {
+ const content = readFileSync(resolve(this.repoRoot, filePath), 'utf-8');
+
+ // Extract exported functions/components
+ const exportMatches = content.match(/export\s+(function|const|class)\s+(\w+)/g);
+ if (exportMatches && exportMatches.length > 0) {
+ const name = exportMatches[0].split(/\s+/).pop() || '';
+
+ return {
+ type: this.inferUtilityType(filePath, content),
+ name,
+ file: filePath,
+ description: this.extractUtilityDescription(content),
+ usage: this.extractUsageExample(content, name),
+ };
+ }
+ } catch (error) {
+ debugLog(`Failed to analyze utility file ${filePath}:`, error);
+ }
+
+ return null;
+ }
+
+ private inferUtilityType(filePath: string, content: string): string {
+ if (filePath.includes('hook') || content.includes('useState') || content.includes('useEffect')) {
+ return 'hook';
+ }
+ if (filePath.includes('component') || content.includes('React') || content.includes('JSX')) {
+ return 'component';
+ }
+ if (filePath.includes('service') || filePath.includes('api')) {
+ return 'service';
+ }
+ return 'helper-function';
+ }
+
+ private extractUtilityDescription(content: string): string {
+ // Look for JSDoc comments or comments near exports
+ const commentMatch = content.match(/\/\*\*([\s\S]*?)\*\//);
+ if (commentMatch) {
+ return commentMatch[1].replace(/\s*\*\s?/g, ' ').trim().slice(0, 100);
+ }
+
+ // Look for single-line comments
+ const singleLineMatch = content.match(/\/\/\s*(.+)/);
+ if (singleLineMatch) {
+ return singleLineMatch[1].trim().slice(0, 100);
+ }
+
+ return 'Utility function';
+ }
+
+ private extractUsageExample(content: string, name: string): string {
+ // Try to find usage examples in comments
+ const lines = content.split('\n');
+ for (let i = 0; i < lines.length; i++) {
+ if (lines[i].includes('@example') || lines[i].includes('Example:')) {
+ const exampleLines = [];
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
+ if (lines[j].trim() && !lines[j].includes('*/')) {
+ exampleLines.push(lines[j].trim());
+ } else break;
+ }
+ return exampleLines.join('\n');
+ }
+ }
+
+ return `${name}()`;
+ }
+
+ private getAllSourceFiles(): string[] {
+ // This would be implemented to get all source files
+ // For now, return empty array - in practice, this would scan the repo
+ return [];
+ }
+}
\ No newline at end of file