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