512 lines
16 KiB
C#
Executable File
512 lines
16 KiB
C#
Executable File
using System.Text.Json;
|
|
using MarketAlly.AIPlugin;
|
|
|
|
namespace MarketAlly.AIPlugin.Context
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<string, Type> SupportedParameters => new Dictionary<string, Type>
|
|
{
|
|
["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<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> 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<ConversationHistory> 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<ConversationEntry[]>(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<ConversationEntry>
|
|
{
|
|
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<CodebaseInfo> 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<FileInfo>();
|
|
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<RecentChanges> 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<ProjectInfo> 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<List<GitCommit>> GetRecentGitCommitsAsync(string projectPath)
|
|
{
|
|
var commits = new List<GitCommit>();
|
|
|
|
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<string>();
|
|
|
|
// 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<ContextInfo> 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<ConversationEntry> 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<FileInfo> ProjectFiles { get; set; } = new();
|
|
public List<FileInfo> 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<FileChange> ModifiedFiles { get; set; } = new();
|
|
public List<GitCommit> 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<string, string> ConfigurationFiles { get; set; } = new();
|
|
public List<string> DirectoryStructure { get; set; } = new();
|
|
}
|
|
} |