259 lines
8.2 KiB
C#
Executable File
259 lines
8.2 KiB
C#
Executable File
using System.Text.Json;
|
|
using MarketAlly.AIPlugin;
|
|
|
|
namespace MarketAlly.AIPlugin.Context
|
|
{
|
|
/// <summary>
|
|
/// Plugin for storing conversation context and important information for future retrieval.
|
|
/// Allows Claude to persist key decisions, code changes, and discussion points across sessions.
|
|
/// </summary>
|
|
[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<string, Type> SupportedParameters => new Dictionary<string, Type>
|
|
{
|
|
["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<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> 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<string>(),
|
|
ProjectPath = projectPath ?? Directory.GetCurrentDirectory(),
|
|
Priority = priority,
|
|
Timestamp = DateTime.UtcNow,
|
|
Metadata = metadata != null ? JsonSerializer.Deserialize<Dictionary<string, object>>(metadata) : new Dictionary<string, object>()
|
|
};
|
|
|
|
// 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<string> 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<bool> 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<StoredContextEntry> existingEntries = new();
|
|
|
|
// Load existing entries if file exists
|
|
if (File.Exists(filePath))
|
|
{
|
|
var existingJson = await File.ReadAllTextAsync(filePath);
|
|
var existing = JsonSerializer.Deserialize<List<StoredContextEntry>>(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<ContextIndexEntry> indexEntries = new();
|
|
|
|
// Load existing index
|
|
if (File.Exists(indexPath))
|
|
{
|
|
var indexJson = await File.ReadAllTextAsync(indexPath);
|
|
var existing = JsonSerializer.Deserialize<List<ContextIndexEntry>>(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
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a stored context entry with full content and metadata
|
|
/// </summary>
|
|
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<string> Tags { get; set; } = new();
|
|
public string ProjectPath { get; set; } = "";
|
|
public string Priority { get; set; } = "";
|
|
public DateTime Timestamp { get; set; }
|
|
public Dictionary<string, object> Metadata { get; set; } = new();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents an index entry for quick context lookup
|
|
/// </summary>
|
|
public class ContextIndexEntry
|
|
{
|
|
public string Id { get; set; } = "";
|
|
public string Type { get; set; } = "";
|
|
public string Summary { get; set; } = "";
|
|
public List<string> Tags { get; set; } = new();
|
|
public string Priority { get; set; } = "";
|
|
public DateTime Timestamp { get; set; }
|
|
public string FileName { get; set; } = "";
|
|
}
|
|
} |