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();
}
}