UNPKG

permamind

Version:

An MCP server that provides an immortal memory layer for AI agents and clients

307 lines (306 loc) 11.1 kB
import { exec } from "child_process"; import * as path from "path"; import { promisify } from "util"; // ClaudeCodeAgentService removed - BMAD functionality const execAsync = promisify(exec); export class GitContextService { constructor() { // Agent service removed - BMAD functionality } async analyzeRepository(repoPath) { try { const isRepository = await this.isGitRepository(repoPath); if (!isRepository) { return { currentBranch: "", isRepository: false, modifiedFiles: [], projectStage: "unknown", recentCommits: [], }; } const [currentBranch, recentCommits, modifiedFiles] = await Promise.all([ this.getCurrentBranch(repoPath), this.getRecentCommits(repoPath), this.getModifiedFiles(repoPath), ]); const projectStage = this.determineProjectStage(recentCommits, modifiedFiles); return { currentBranch, isRepository: true, modifiedFiles, projectStage, recentCommits, }; } catch (error) { throw new Error(`Failed to analyze repository: ${error instanceof Error ? error.message : "Unknown error"}`); } } async detectAgentFromGitContext(repoPath, gitContext) { try { // Analyze recent commits for patterns const commitMessages = gitContext.recentCommits .map((commit) => commit.message) .join(" "); // Analyze modified files for patterns const fileExtensions = gitContext.modifiedFiles .map((file) => path.extname(file)) .filter((ext) => ext); // Development context indicators if (commitMessages.match(/(feat|fix|refactor|perf)/i) || fileExtensions.some((ext) => [".go", ".java", ".js", ".py", ".ts"].includes(ext))) { return "developer"; } // Documentation context indicators if (commitMessages.match(/(docs|readme|documentation)/i) || fileExtensions.some((ext) => [".md", ".rst", ".txt"].includes(ext))) { return "pm"; // PM often handles documentation } // Test context indicators if (commitMessages.match(/(test|spec|coverage)/i) || gitContext.modifiedFiles.some((file) => file.includes("test"))) { return "qa"; } // Architecture context indicators if (commitMessages.match(/(architect|design|structure)/i) || gitContext.modifiedFiles.some((file) => file.includes("architecture"))) { return "architect"; } return null; } catch (error) { console.warn(`Error detecting agent from git context: ${error}`); return null; } } async getBranchContext(repoPath) { try { const [currentBranch, allBranches, remoteBranches] = await Promise.all([ this.getCurrentBranch(repoPath), this.getAllBranches(repoPath), this.getRemoteBranches(repoPath), ]); return { allBranches, currentBranch, isFeatureBranch: currentBranch.startsWith("feature/") || currentBranch.startsWith("feat/"), isHotfixBranch: currentBranch.startsWith("hotfix/") || currentBranch.startsWith("fix/"), isMainBranch: ["develop", "main", "master"].includes(currentBranch), remoteBranches, }; } catch (error) { return {}; } } async getCommitPatternAnalysis(repoPath, days = 7) { try { const { stdout } = await execAsync(`git log --since="${days} days ago" --pretty=format:"%s" --no-merges`, { cwd: repoPath }); const commits = stdout.split("\n").filter((line) => line.trim()); const patterns = {}; for (const commit of commits) { // Extract conventional commit prefixes const match = commit.match(/^(\w+)(\(.+\))?\s*:/); if (match) { const prefix = match[1].toLowerCase(); patterns[prefix] = (patterns[prefix] || 0) + 1; } } return patterns; } catch (error) { return {}; } } async monitorFileChanges(repoPath, callback) { try { // Simple file monitoring implementation // Note: In a real implementation, this would use fs.watch properly // For now, we'll implement a polling-based approach const pollInterval = setInterval(async () => { try { const changes = await this.getFileChanges(repoPath); if (changes.added.length > 0 || changes.modified.length > 0 || changes.deleted.length > 0 || changes.renamed.length > 0) { callback(changes); } } catch (error) { console.warn(`Error monitoring file changes: ${error}`); } }, 2000); // Poll every 2 seconds // Store interval for potential cleanup (not implemented here) // In a real implementation, you'd want to return a cleanup function } catch (error) { throw new Error(`Failed to monitor file changes: ${error instanceof Error ? error.message : "Unknown error"}`); } } determineProjectStage(commits, modifiedFiles) { const recentMessages = commits .map((c) => c.message.toLowerCase()) .join(" "); // Check for deployment indicators if (recentMessages.includes("deploy") || recentMessages.includes("release") || recentMessages.includes("production")) { return "deployment"; } // Check for testing indicators if (recentMessages.includes("test") || recentMessages.includes("spec") || modifiedFiles.some((file) => file.includes("test"))) { return "testing"; } // Check for development indicators if (recentMessages.includes("feat") || recentMessages.includes("feature") || recentMessages.includes("implement")) { return "development"; } return "unknown"; } async getAllBranches(repoPath) { try { const { stdout } = await execAsync("git branch --format='%(refname:short)'", { cwd: repoPath, }); return stdout.split("\n").filter((branch) => branch.trim()); } catch { return []; } } async getCurrentBranch(repoPath) { try { const { stdout } = await execAsync("git branch --show-current", { cwd: repoPath, }); return stdout.trim(); } catch { return "unknown"; } } async getFileChanges(repoPath) { try { const { stdout } = await execAsync("git status --porcelain", { cwd: repoPath, }); const changes = { added: [], deleted: [], modified: [], renamed: [], }; for (const line of stdout.split("\n")) { if (!line.trim()) continue; const status = line.substring(0, 2); const filename = line.substring(3); switch (status.trim()) { case "A": changes.added.push(filename); break; case "D": changes.deleted.push(filename); break; case "M": changes.modified.push(filename); break; case "R": { // Renamed files have format "R old -> new" const [oldName, newName] = filename.split(" -> "); if (oldName && newName) { changes.renamed.push({ from: oldName, to: newName }); } break; } } } return changes; } catch { return { added: [], deleted: [], modified: [], renamed: [], }; } } async getModifiedFiles(repoPath) { try { const { stdout } = await execAsync("git status --porcelain", { cwd: repoPath, }); return stdout .split("\n") .filter((line) => line.trim()) .map((line) => line.substring(3)); // Remove status prefix } catch { return []; } } async getRecentCommits(repoPath, count = 10) { try { const { stdout } = await execAsync(`git log --oneline -n ${count} --pretty=format:"%H|%s|%an|%ai"`, { cwd: repoPath }); return stdout .split("\n") .filter((line) => line.trim()) .map((line) => { const [hash, message, author, timestamp] = line.split("|"); return { author, hash: hash.substring(0, 8), // Short hash message, timestamp, }; }); } catch { return []; } } async getRemoteBranches(repoPath) { try { const { stdout } = await execAsync("git branch -r --format='%(refname:short)'", { cwd: repoPath, }); return stdout.split("\n").filter((branch) => branch.trim()); } catch { return []; } } async isGitRepository(repoPath) { try { await execAsync("git rev-parse --git-dir", { cwd: repoPath }); return true; } catch { return false; } } shouldIgnoreFile(filename) { const ignorePaths = [ ".git/", "node_modules/", ".vscode/", ".idea/", "dist/", "build/", ".next/", ".nuxt/", ".tmp/", ".temp/", ]; return ignorePaths.some((ignorePath) => filename.includes(ignorePath)); } }