using System.Text.Json; using MarketAlly.AIPlugin; namespace MarketAlly.AIPlugin.Context { /// /// Plugin for deleting context entries from storage. /// Handles both individual entry deletion and bulk operations. /// [AIPlugin("ContextDeletion", "Delete context entries from storage with support for individual and bulk operations")] public class ContextDeletionPlugin : IAIPlugin { [AIParameter("ID of the context entry to delete", required: true)] public string EntryId { get; set; } = ""; [AIParameter("Project path where context is stored", required: false)] public string? ProjectPath { get; set; } [AIParameter("Type of deletion: 'single', 'bulk', 'by_tag', 'by_type', 'by_date_range'", required: false)] public string DeletionType { get; set; } = "single"; [AIParameter("Additional criteria for bulk deletion (JSON format)", required: false)] public string? DeletionCriteria { get; set; } [AIParameter("Confirm deletion (must be true to proceed)", required: false)] public bool Confirm { get; set; } = false; public IReadOnlyDictionary SupportedParameters => new Dictionary { ["entryId"] = typeof(string), ["entryid"] = typeof(string), ["projectPath"] = typeof(string), ["projectpath"] = typeof(string), ["deletionType"] = typeof(string), ["deletiontype"] = typeof(string), ["deletionCriteria"] = typeof(string), ["deletioncriteria"] = typeof(string), ["confirm"] = typeof(bool) }; public async Task ExecuteAsync(IReadOnlyDictionary parameters) { try { // Extract parameters var entryId = parameters["entryId"].ToString()!; var projectPath = parameters.TryGetValue("projectPath", out var pp) ? pp?.ToString() : null; var deletionType = parameters.TryGetValue("deletionType", out var dt) ? dt.ToString()!.ToLower() : "single"; var deletionCriteria = parameters.TryGetValue("deletionCriteria", out var dc) ? dc?.ToString() : null; var confirm = parameters.TryGetValue("confirm", out var c) ? Convert.ToBoolean(c) : false; if (!confirm) { return new AIPluginResult(new { Error = "Deletion not confirmed" }, "Deletion requires explicit confirmation. Set 'confirm' parameter to true."); } var storagePath = await GetStoragePathAsync(projectPath); return deletionType switch { "single" => await DeleteSingleEntryAsync(entryId, storagePath), "bulk" => await DeleteBulkEntriesAsync(deletionCriteria, storagePath), "by_tag" => await DeleteByTagAsync(entryId, storagePath), // entryId as tag name "by_type" => await DeleteByTypeAsync(entryId, storagePath), // entryId as type name "by_date_range" => await DeleteByDateRangeAsync(deletionCriteria, storagePath), _ => new AIPluginResult(new { Error = "Invalid deletion type" }, $"Unknown deletion type: {deletionType}") }; } catch (Exception ex) { return new AIPluginResult(ex, "Failed to delete context entry/entries"); } } private async Task GetStoragePathAsync(string? projectPath) { if (string.IsNullOrEmpty(projectPath)) { projectPath = Directory.GetCurrentDirectory(); } var contextDir = Path.Combine(projectPath, ".context"); return contextDir; } private async Task DeleteSingleEntryAsync(string entryId, string storagePath) { try { var deletedFromFiles = 0; var filesProcessed = 0; // Get all context files var contextFiles = Directory.GetFiles(storagePath, "context-*.json") .Where(f => !f.EndsWith("context-index.json")) .ToList(); foreach (var filePath in contextFiles) { filesProcessed++; var fileContent = await File.ReadAllTextAsync(filePath); var entries = JsonSerializer.Deserialize>(fileContent); if (entries == null) continue; var originalCount = entries.Count; entries.RemoveAll(e => e.Id == entryId); if (entries.Count < originalCount) { // Entry was found and removed var updatedJson = JsonSerializer.Serialize(entries, new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); await File.WriteAllTextAsync(filePath, updatedJson); deletedFromFiles++; // Update the index await RemoveFromIndexAsync(entryId, storagePath); return new AIPluginResult(new { Success = true, EntryId = entryId, DeletedFrom = Path.GetFileName(filePath), RemainingEntries = entries.Count, Operation = "single_deletion" }, $"Successfully deleted entry {entryId}"); } } return new AIPluginResult(new { Success = false, EntryId = entryId, FilesSearched = filesProcessed, Operation = "single_deletion" }, $"Entry {entryId} not found in any context files"); } catch (Exception ex) { return new AIPluginResult(ex, $"Failed to delete entry {entryId}"); } } private async Task DeleteBulkEntriesAsync(string? criteriaJson, string storagePath) { try { if (string.IsNullOrEmpty(criteriaJson)) { return new AIPluginResult(new { Error = "Bulk deletion requires criteria" }, "Provide deletion criteria as JSON with fields like 'type', 'priority', 'tags', 'olderThan'"); } var criteria = JsonSerializer.Deserialize(criteriaJson); if (criteria == null) { return new AIPluginResult(new { Error = "Invalid criteria format" }, "Failed to parse deletion criteria JSON"); } var totalDeleted = 0; var filesProcessed = 0; var deletedEntries = new List(); var contextFiles = Directory.GetFiles(storagePath, "context-*.json") .Where(f => !f.EndsWith("context-index.json")) .ToList(); foreach (var filePath in contextFiles) { filesProcessed++; var fileContent = await File.ReadAllTextAsync(filePath); var entries = JsonSerializer.Deserialize>(fileContent); if (entries == null) continue; var originalCount = entries.Count; var toDelete = entries.Where(e => MatchesCriteria(e, criteria)).ToList(); foreach (var entry in toDelete) { entries.Remove(entry); deletedEntries.Add(entry.Id); totalDeleted++; } if (entries.Count < originalCount) { var updatedJson = JsonSerializer.Serialize(entries, new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); await File.WriteAllTextAsync(filePath, updatedJson); } } // Update index for all deleted entries foreach (var entryId in deletedEntries) { await RemoveFromIndexAsync(entryId, storagePath); } return new AIPluginResult(new { Success = true, TotalDeleted = totalDeleted, FilesProcessed = filesProcessed, DeletedEntries = deletedEntries.Take(10).ToList(), // Show first 10 Criteria = criteria, Operation = "bulk_deletion" }, $"Successfully deleted {totalDeleted} entries matching criteria"); } catch (Exception ex) { return new AIPluginResult(ex, "Failed to perform bulk deletion"); } } private async Task DeleteByTagAsync(string tag, string storagePath) { var criteria = new BulkDeletionCriteria { Tags = new List { tag } }; var criteriaJson = JsonSerializer.Serialize(criteria); return await DeleteBulkEntriesAsync(criteriaJson, storagePath); } private async Task DeleteByTypeAsync(string type, string storagePath) { var criteria = new BulkDeletionCriteria { Type = type }; var criteriaJson = JsonSerializer.Serialize(criteria); return await DeleteBulkEntriesAsync(criteriaJson, storagePath); } private async Task DeleteByDateRangeAsync(string? criteriaJson, string storagePath) { try { if (string.IsNullOrEmpty(criteriaJson)) { return new AIPluginResult(new { Error = "Date range deletion requires criteria" }, "Provide criteria with 'olderThan' or 'newerThan' dates"); } var criteria = JsonSerializer.Deserialize(criteriaJson); if (criteria == null) { return new AIPluginResult(new { Error = "Invalid date criteria" }, "Failed to parse date range criteria"); } return await DeleteBulkEntriesAsync(criteriaJson, storagePath); } catch (Exception ex) { return new AIPluginResult(ex, "Failed to delete by date range"); } } private bool MatchesCriteria(StoredContextEntry entry, BulkDeletionCriteria criteria) { // Check type if (!string.IsNullOrEmpty(criteria.Type) && !entry.Type.Equals(criteria.Type, StringComparison.OrdinalIgnoreCase)) { return false; } // Check priority if (!string.IsNullOrEmpty(criteria.Priority) && !entry.Priority.Equals(criteria.Priority, StringComparison.OrdinalIgnoreCase)) { return false; } // Check tags if (criteria.Tags?.Any() == true) { var hasMatchingTag = criteria.Tags.Any(tag => entry.Tags.Any(entryTag => entryTag.Equals(tag, StringComparison.OrdinalIgnoreCase))); if (!hasMatchingTag) return false; } // Check date range if (criteria.OlderThan.HasValue && entry.Timestamp >= criteria.OlderThan.Value) { return false; } if (criteria.NewerThan.HasValue && entry.Timestamp <= criteria.NewerThan.Value) { return false; } // Check project path if (!string.IsNullOrEmpty(criteria.ProjectPath) && !entry.ProjectPath.Equals(criteria.ProjectPath, StringComparison.OrdinalIgnoreCase)) { return false; } return true; } private async Task RemoveFromIndexAsync(string entryId, string storagePath) { try { var indexPath = Path.Combine(storagePath, "context-index.json"); if (!File.Exists(indexPath)) return; var indexContent = await File.ReadAllTextAsync(indexPath); var indexEntries = JsonSerializer.Deserialize>(indexContent); if (indexEntries == null) return; indexEntries.RemoveAll(e => e.Id == entryId); var updatedIndexJson = JsonSerializer.Serialize(indexEntries, new JsonSerializerOptions { WriteIndented = true }); await File.WriteAllTextAsync(indexPath, updatedIndexJson); } catch (Exception ex) { // Log but don't fail the deletion if index update fails Console.WriteLine($"Warning: Failed to update index after deletion: {ex.Message}"); } } } /// /// Criteria for bulk deletion operations /// public class BulkDeletionCriteria { public string? Type { get; set; } public string? Priority { get; set; } public List? Tags { get; set; } public DateTime? OlderThan { get; set; } public DateTime? NewerThan { get; set; } public string? ProjectPath { get; set; } } }