using System.Text.Json; using MarketAlly.AIPlugin; namespace MarketAlly.AIPlugin.Context { /// /// Plugin for retrieving and managing conversation and codebase context across long chat sessions. /// Allows Claude to access previous discussion history, code changes, and project context. /// [AIPlugin("ContextRetrieval", "Retrieves conversation context, code history, and project information to maintain continuity across long chat sessions")] public class ContextRetrievalPlugin : IAIPlugin { [AIParameter("Type of context to retrieve: 'conversation', 'codebase', 'changes', 'project', or 'all'", required: true)] public string ContextType { get; set; } = "all"; [AIParameter("Project or directory path to analyze for context", required: false)] public string? ProjectPath { get; set; } [AIParameter("Number of recent conversation entries to retrieve (default: 10)", required: false)] public int ConversationLimit { get; set; } = 10; [AIParameter("Include file content summaries in context", required: false)] public bool IncludeFileSummaries { get; set; } = true; [AIParameter("Include recent git changes in context", required: false)] public bool IncludeGitHistory { get; set; } = true; [AIParameter("Maximum context size in characters (default: 50000)", required: false)] public int MaxContextSize { get; set; } = 50000; public IReadOnlyDictionary SupportedParameters => new Dictionary { ["contextType"] = typeof(string), ["contexttype"] = typeof(string), // Add this lowercase variant ["projectPath"] = typeof(string), ["projectpath"] = typeof(string), // Add this too ["conversationLimit"] = typeof(int), ["conversationlimit"] = typeof(int), // And this ["includeFileSummaries"] = typeof(bool), ["includefilesummaries"] = typeof(bool), ["includeGitHistory"] = typeof(bool), ["includegithistory"] = typeof(bool), ["maxContextSize"] = typeof(int), ["maxcontextsize"] = typeof(int) }; public async Task ExecuteAsync(IReadOnlyDictionary parameters) { try { // Extract parameters var contextType = parameters.TryGetValue("contextType", out var ct) ? ct.ToString()!.ToLower() : "all"; var projectPath = parameters.TryGetValue("projectPath", out var pp) ? pp?.ToString() : null; var conversationLimit = parameters.TryGetValue("conversationLimit", out var cl) ? Convert.ToInt32(cl) : 10; var includeFileSummaries = parameters.TryGetValue("includeFileSummaries", out var ifs) ? Convert.ToBoolean(ifs) : true; var includeGitHistory = parameters.TryGetValue("includeGitHistory", out var igh) ? Convert.ToBoolean(igh) : true; var maxContextSize = parameters.TryGetValue("maxContextSize", out var mcs) ? Convert.ToInt32(mcs) : 50000; var context = new ContextInfo(); // Retrieve different types of context based on request switch (contextType) { case "conversation": context.ConversationHistory = await GetConversationHistoryAsync(conversationLimit); break; case "codebase": context.CodebaseInfo = await GetCodebaseContextAsync(projectPath, includeFileSummaries); break; case "changes": context.RecentChanges = await GetRecentChangesAsync(projectPath, includeGitHistory); break; case "project": context.ProjectInfo = await GetProjectInfoAsync(projectPath); break; case "all": default: context.ConversationHistory = await GetConversationHistoryAsync(conversationLimit); context.CodebaseInfo = await GetCodebaseContextAsync(projectPath, includeFileSummaries); context.RecentChanges = await GetRecentChangesAsync(projectPath, includeGitHistory); context.ProjectInfo = await GetProjectInfoAsync(projectPath); break; } // Trim context if it exceeds size limit var contextJson = JsonSerializer.Serialize(context, new JsonSerializerOptions { WriteIndented = true }); if (contextJson.Length > maxContextSize) { context = await TrimContextToSizeAsync(context, maxContextSize); contextJson = JsonSerializer.Serialize(context, new JsonSerializerOptions { WriteIndented = true }); } return new AIPluginResult(context, $"Retrieved {contextType} context successfully. Context size: {contextJson.Length} characters"); } catch (Exception ex) { return new AIPluginResult(ex, "Failed to retrieve context"); } } private async Task GetConversationHistoryAsync(int limit) { var history = new ConversationHistory(); // Look for conversation history in common locations var possiblePaths = new[] { ".context/conversation.json", ".ai/chat-history.json", "conversation-context.json", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".claude", "conversations.json") }; foreach (var path in possiblePaths) { if (File.Exists(path)) { try { var content = await File.ReadAllTextAsync(path); var conversations = JsonSerializer.Deserialize(content); if (conversations != null) { history.Entries = conversations.Take(limit).ToList(); history.Source = path; break; } } catch { // Continue to next possible path } } } // If no history file found, create a placeholder structure if (history.Entries.Count == 0) { history.Entries = new List { new ConversationEntry { Timestamp = DateTime.UtcNow, Type = "system", Content = "No previous conversation history found. This appears to be a new session.", Context = "Starting fresh conversation context" } }; history.Source = "Generated - no history file found"; } return history; } private async Task GetCodebaseContextAsync(string? projectPath, bool includeFileSummaries) { var codebaseInfo = new CodebaseInfo(); if (string.IsNullOrEmpty(projectPath)) { projectPath = Directory.GetCurrentDirectory(); } if (!Directory.Exists(projectPath)) { return codebaseInfo; } codebaseInfo.RootPath = projectPath; codebaseInfo.LastAnalyzed = DateTime.UtcNow; // Get project structure var projectFiles = Directory.GetFiles(projectPath, "*.csproj", SearchOption.AllDirectories) .Union(Directory.GetFiles(projectPath, "*.sln", SearchOption.TopDirectoryOnly)) .ToList(); codebaseInfo.ProjectFiles = projectFiles.Select(f => new FileInfo { Path = Path.GetRelativePath(projectPath, f), LastModified = File.GetLastWriteTime(f), Size = new System.IO.FileInfo(f).Length }).ToList(); // Get source files with summaries if requested if (includeFileSummaries) { var sourceFiles = Directory.GetFiles(projectPath, "*.cs", SearchOption.AllDirectories) .Where(f => !f.Contains("bin") && !f.Contains("obj")) .Take(20) // Limit to prevent overwhelming context .ToList(); codebaseInfo.SourceFiles = new List(); foreach (var file in sourceFiles) { var fileInfo = new FileInfo { Path = Path.GetRelativePath(projectPath, file), LastModified = File.GetLastWriteTime(file), Size = new System.IO.FileInfo(file).Length }; // Add summary for smaller files if (fileInfo.Size < 10000) // Only summarize files smaller than 10KB { try { var content = await File.ReadAllTextAsync(file); fileInfo.Summary = GenerateFileSummary(content, file); } catch { fileInfo.Summary = "Unable to read file content"; } } codebaseInfo.SourceFiles.Add(fileInfo); } } return codebaseInfo; } private async Task GetRecentChangesAsync(string? projectPath, bool includeGitHistory) { var changes = new RecentChanges(); if (string.IsNullOrEmpty(projectPath)) { projectPath = Directory.GetCurrentDirectory(); } // Look for recent file modifications if (Directory.Exists(projectPath)) { var recentFiles = Directory.GetFiles(projectPath, "*.*", SearchOption.AllDirectories) .Where(f => !f.Contains("bin") && !f.Contains("obj") && !f.Contains(".git")) .Where(f => File.GetLastWriteTime(f) > DateTime.Now.AddDays(-7)) .OrderByDescending(f => File.GetLastWriteTime(f)) .Take(10) .ToList(); changes.ModifiedFiles = recentFiles.Select(f => new FileChange { Path = Path.GetRelativePath(projectPath, f), ModifiedDate = File.GetLastWriteTime(f), ChangeType = "Modified" }).ToList(); } // Get git history if available and requested if (includeGitHistory && Directory.Exists(Path.Combine(projectPath!, ".git"))) { try { changes.GitCommits = await GetRecentGitCommitsAsync(projectPath!); } catch { // Git history not available or accessible } } return changes; } private async Task GetProjectInfoAsync(string? projectPath) { var projectInfo = new ProjectInfo(); if (string.IsNullOrEmpty(projectPath)) { projectPath = Directory.GetCurrentDirectory(); } projectInfo.Path = projectPath; projectInfo.Name = Path.GetFileName(projectPath); // Look for configuration files var configFiles = new[] { "refactor-config.json", "appsettings.json", "package.json", "project.json" }; foreach (var configFile in configFiles) { var fullPath = Path.Combine(projectPath, configFile); if (File.Exists(fullPath)) { try { var content = await File.ReadAllTextAsync(fullPath); projectInfo.ConfigurationFiles[configFile] = content; } catch { projectInfo.ConfigurationFiles[configFile] = "Unable to read configuration file"; } } } // Analyze project structure if (Directory.Exists(projectPath)) { var directories = Directory.GetDirectories(projectPath, "*", SearchOption.TopDirectoryOnly) .Where(d => !Path.GetFileName(d).StartsWith(".") && !Path.GetFileName(d).Equals("bin", StringComparison.OrdinalIgnoreCase) && !Path.GetFileName(d).Equals("obj", StringComparison.OrdinalIgnoreCase)) .Select(d => Path.GetFileName(d)) .ToList(); projectInfo.DirectoryStructure = directories; } return projectInfo; } private async Task> GetRecentGitCommitsAsync(string projectPath) { var commits = new List(); try { // Use git command line to get recent commits var processInfo = new System.Diagnostics.ProcessStartInfo { FileName = "git", Arguments = "log --oneline -10 --date=short --pretty=format:\"%h|%ad|%s|%an\"", WorkingDirectory = projectPath, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; using var process = System.Diagnostics.Process.Start(processInfo); if (process != null) { var output = await process.StandardOutput.ReadToEndAsync(); await process.WaitForExitAsync(); var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { var parts = line.Split('|'); if (parts.Length >= 4) { commits.Add(new GitCommit { Hash = parts[0].Trim('"'), Date = parts[1], Message = parts[2], Author = parts[3].Trim('"') }); } } } } catch { // Git not available or accessible } return commits; } private string GenerateFileSummary(string content, string filePath) { var lines = content.Split('\n'); var summary = new List(); // Extract key information summary.Add($"File: {Path.GetFileName(filePath)} ({lines.Length} lines)"); // Look for class/interface definitions var typeDefinitions = lines.Where(l => l.Trim().StartsWith("public class") || l.Trim().StartsWith("public interface") || l.Trim().StartsWith("public enum")) .Select(l => l.Trim()) .Take(3) .ToList(); if (typeDefinitions.Any()) { summary.Add("Types: " + string.Join(", ", typeDefinitions)); } // Look for public methods var methods = lines.Where(l => l.Trim().StartsWith("public") && l.Contains("(")) .Select(l => l.Trim()) .Take(5) .ToList(); if (methods.Any()) { summary.Add("Key Methods: " + string.Join("; ", methods.Select(m => m.Length > 60 ? m.Substring(0, 60) + "..." : m))); } return string.Join(" | ", summary); } private async Task TrimContextToSizeAsync(ContextInfo context, int maxSize) { // Start by trimming the largest sections first var trimmedContext = new ContextInfo { ConversationHistory = context.ConversationHistory, ProjectInfo = context.ProjectInfo // Keep project info as it's usually small but important }; var currentSize = JsonSerializer.Serialize(trimmedContext).Length; // Add codebase info if there's room if (currentSize < maxSize * 0.7) // Reserve 30% for other content { var trimmedCodebase = context.CodebaseInfo; if (trimmedCodebase?.SourceFiles?.Count > 10) { trimmedCodebase.SourceFiles = trimmedCodebase.SourceFiles.Take(10).ToList(); } trimmedContext.CodebaseInfo = trimmedCodebase; } // Add recent changes if there's room currentSize = JsonSerializer.Serialize(trimmedContext).Length; if (currentSize < maxSize * 0.9) // Reserve 10% buffer { var trimmedChanges = context.RecentChanges; if (trimmedChanges?.ModifiedFiles?.Count > 5) { trimmedChanges.ModifiedFiles = trimmedChanges.ModifiedFiles.Take(5).ToList(); } if (trimmedChanges?.GitCommits?.Count > 5) { trimmedChanges.GitCommits = trimmedChanges.GitCommits.Take(5).ToList(); } trimmedContext.RecentChanges = trimmedChanges; } return trimmedContext; } } // Supporting data structures public class ContextInfo { public ConversationHistory? ConversationHistory { get; set; } public CodebaseInfo? CodebaseInfo { get; set; } public RecentChanges? RecentChanges { get; set; } public ProjectInfo? ProjectInfo { get; set; } } public class ConversationHistory { public List Entries { get; set; } = new(); public string Source { get; set; } = ""; } public class ConversationEntry { public DateTime Timestamp { get; set; } public string Type { get; set; } = ""; // "user", "assistant", "system" public string Content { get; set; } = ""; public string Context { get; set; } = ""; } public class CodebaseInfo { public string RootPath { get; set; } = ""; public DateTime LastAnalyzed { get; set; } public List ProjectFiles { get; set; } = new(); public List SourceFiles { get; set; } = new(); } public class FileInfo { public string Path { get; set; } = ""; public DateTime LastModified { get; set; } public long Size { get; set; } public string? Summary { get; set; } } public class RecentChanges { public List ModifiedFiles { get; set; } = new(); public List GitCommits { get; set; } = new(); } public class FileChange { public string Path { get; set; } = ""; public DateTime ModifiedDate { get; set; } public string ChangeType { get; set; } = ""; } public class GitCommit { public string Hash { get; set; } = ""; public string Date { get; set; } = ""; public string Message { get; set; } = ""; public string Author { get; set; } = ""; } public class ProjectInfo { public string Path { get; set; } = ""; public string Name { get; set; } = ""; public Dictionary ConfigurationFiles { get; set; } = new(); public List DirectoryStructure { get; set; } = new(); } }