using System.Text.Json; using MarketAlly.AIPlugin; namespace MarketAlly.AIPlugin.Context { /// /// Plugin for storing conversation context and important information for future retrieval. /// Allows Claude to persist key decisions, code changes, and discussion points across sessions. /// [AIPlugin("ContextStorage", "Stores conversation context, decisions, and important information for future retrieval across chat sessions")] public class ContextStoragePlugin : IAIPlugin { [AIParameter("Type of context to store: 'conversation', 'decision', 'codechange', 'insight', 'milestone', or 'documentation'", required: true)] public string ContextType { get; set; } = "conversation"; [AIParameter("The content/information to store", required: true)] public string Content { get; set; } = ""; [AIParameter("Brief summary or title for this context entry", required: true)] public string Summary { get; set; } = ""; [AIParameter("Tags to categorize this context (comma-separated)", required: false)] public string? Tags { get; set; } [AIParameter("Project path to associate this context with", required: false)] public string? ProjectPath { get; set; } [AIParameter("Priority level: 'low', 'medium', 'high', 'critical'", required: false)] public string Priority { get; set; } = "medium"; [AIParameter("Additional metadata as JSON string", required: false)] public string? Metadata { get; set; } public IReadOnlyDictionary SupportedParameters => new Dictionary { ["contextType"] = typeof(string), ["contexttype"] = typeof(string), ["content"] = typeof(string), ["summary"] = typeof(string), ["tags"] = typeof(string), ["projectPath"] = typeof(string), ["projectpath"] = typeof(string), ["priority"] = typeof(string), ["metadata"] = typeof(string) }; public async Task ExecuteAsync(IReadOnlyDictionary parameters) { try { // Validate required parameters if (!parameters.ContainsKey("content")) { return new AIPluginResult(new ArgumentException("Missing required parameter: content"), "The 'content' parameter is required"); } if (!parameters.ContainsKey("summary")) { return new AIPluginResult(new ArgumentException("Missing required parameter: summary"), "The 'summary' parameter is required"); } // Extract parameters var contextType = parameters.TryGetValue("contextType", out var ct) ? ct.ToString()!.ToLower() : "conversation"; var content = parameters["content"].ToString()!; var summary = parameters["summary"].ToString()!; var tags = parameters.TryGetValue("tags", out var t) ? t?.ToString() : null; var projectPath = parameters.TryGetValue("projectPath", out var pp) ? pp?.ToString() : null; var priority = parameters.TryGetValue("priority", out var p) ? p.ToString()!.ToLower() : "medium"; var metadata = parameters.TryGetValue("metadata", out var m) ? m?.ToString() : null; // Create context entry var contextEntry = new StoredContextEntry { Id = Guid.NewGuid().ToString(), Type = contextType, Content = content, Summary = summary, Tags = !string.IsNullOrWhiteSpace(tags) ? tags.Split(',').Select(tag => tag.Trim()).Where(tag => !string.IsNullOrWhiteSpace(tag)).ToList() : new List(), ProjectPath = projectPath ?? Directory.GetCurrentDirectory(), Priority = priority, Timestamp = DateTime.UtcNow, Metadata = metadata != null ? JsonSerializer.Deserialize>(metadata) : new Dictionary() }; // Store the context var storagePath = await GetStoragePathAsync(projectPath); var success = await StoreContextEntryAsync(contextEntry, storagePath); if (success) { // Also update the quick access index await UpdateContextIndexAsync(contextEntry, storagePath); return new AIPluginResult(new { Success = true, EntryId = contextEntry.Id, StoredAt = storagePath, Type = contextType, Summary = summary, Timestamp = contextEntry.Timestamp, Message = "Context stored successfully" }, $"Successfully stored {contextType} context: {summary}"); } else { return new AIPluginResult(new { Success = false }, "Failed to store context"); } } catch (Exception ex) { return new AIPluginResult(ex, "Failed to store context"); } } private async Task GetStoragePathAsync(string? projectPath) { if (string.IsNullOrEmpty(projectPath)) { projectPath = Directory.GetCurrentDirectory(); } // Create .context directory in project root var contextDir = Path.Combine(projectPath, ".context"); if (!Directory.Exists(contextDir)) { Directory.CreateDirectory(contextDir); } return contextDir; } private async Task StoreContextEntryAsync(StoredContextEntry entry, string storagePath) { try { // Store in monthly files to keep manageable file sizes var fileName = $"context-{DateTime.UtcNow:yyyy-MM}.json"; var filePath = Path.Combine(storagePath, fileName); List existingEntries = new(); // Load existing entries if file exists if (File.Exists(filePath)) { var existingJson = await File.ReadAllTextAsync(filePath); var existing = JsonSerializer.Deserialize>(existingJson); if (existing != null) { existingEntries = existing; } } // Add new entry existingEntries.Add(entry); // Sort by timestamp (newest first) existingEntries = existingEntries.OrderByDescending(e => e.Timestamp).ToList(); // Save back to file var json = JsonSerializer.Serialize(existingEntries, new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); await File.WriteAllTextAsync(filePath, json); return true; } catch { return false; } } private async Task UpdateContextIndexAsync(StoredContextEntry entry, string storagePath) { try { var indexPath = Path.Combine(storagePath, "context-index.json"); List indexEntries = new(); // Load existing index if (File.Exists(indexPath)) { var indexJson = await File.ReadAllTextAsync(indexPath); var existing = JsonSerializer.Deserialize>(indexJson); if (existing != null) { indexEntries = existing; } } // Add new index entry var indexEntry = new ContextIndexEntry { Id = entry.Id, Type = entry.Type, Summary = entry.Summary, Tags = entry.Tags, Priority = entry.Priority, Timestamp = entry.Timestamp, FileName = $"context-{entry.Timestamp:yyyy-MM}.json" }; indexEntries.Add(indexEntry); // Keep only the most recent 1000 entries in the index indexEntries = indexEntries.OrderByDescending(e => e.Timestamp).Take(1000).ToList(); // Save index var indexJsonString = JsonSerializer.Serialize(indexEntries, new JsonSerializerOptions { WriteIndented = true }); await File.WriteAllTextAsync(indexPath, indexJsonString); } catch { // Index update failed, but main storage succeeded } } } /// /// Represents a stored context entry with full content and metadata /// public class StoredContextEntry { public string Id { get; set; } = ""; public string Type { get; set; } = ""; public string Content { get; set; } = ""; public string Summary { get; set; } = ""; public List Tags { get; set; } = new(); public string ProjectPath { get; set; } = ""; public string Priority { get; set; } = ""; public DateTime Timestamp { get; set; } public Dictionary Metadata { get; set; } = new(); } /// /// Represents an index entry for quick context lookup /// public class ContextIndexEntry { public string Id { get; set; } = ""; public string Type { get; set; } = ""; public string Summary { get; set; } = ""; public List Tags { get; set; } = new(); public string Priority { get; set; } = ""; public DateTime Timestamp { get; set; } public string FileName { get; set; } = ""; } }