using Nexus.Api.Helpers; namespace Nexus.Api.Services; public sealed class MemoryService : IMemoryService { private const string BasePath = "/mnt/workspace-iris/memory"; private const string LongTermPath = "/mnt/workspace-iris/MEMORY.md"; private const int MaxFileSize = 1_000_000; private const int MaxFiles = 50; public Task> GetAllAsync() { var files = new List(); if (File.Exists(LongTermPath)) { var fi = new FileInfo(LongTermPath); files.Add(new MemoryFileInfo("MEMORY.md", "MEMORY.md", fi.Length, fi.LastWriteTimeUtc)); } if (Directory.Exists(BasePath)) { var memFiles = Directory.GetFiles(BasePath, "*.md") .Select(f => new FileInfo(f)) .OrderByDescending(f => f.Name) .Select(f => new MemoryFileInfo( f.Name, f.FullName.Replace(BasePath, "").TrimStart('/'), f.Length, f.LastWriteTimeUtc)); files.AddRange(memFiles); } return Task.FromResult>(files); } public async Task> SearchAsync(string query) { var results = new List(); async Task SearchDir(string dir) { if (!Directory.Exists(dir)) return; foreach (var file in Directory.GetFiles(dir, "*.md").Take(MaxFiles)) { var fi = new FileInfo(file); if (fi.Length > MaxFileSize) continue; var content = await File.ReadAllTextAsync(file); if (!content.Contains(query, StringComparison.OrdinalIgnoreCase)) continue; var idx = content.IndexOf(query, StringComparison.OrdinalIgnoreCase); var start = Math.Max(0, idx - 60); var excerpt = (start > 0 ? "…" : "") + content.Substring(start, Math.Min(200, content.Length - start)) + "…"; results.Add(new MemorySearchResult( Path.GetFileName(file), file.Replace(BasePath, "").TrimStart('/'), excerpt, fi.Length)); } } await SearchDir(BasePath); if (File.Exists(LongTermPath)) { var content = await File.ReadAllTextAsync(LongTermPath); if (content.Contains(query, StringComparison.OrdinalIgnoreCase)) { var idx = content.IndexOf(query, StringComparison.OrdinalIgnoreCase); var start = Math.Max(0, idx - 60); var excerpt = (start > 0 ? "…" : "") + content.Substring(start, Math.Min(200, content.Length - start)) + "…"; results.Insert(0, new MemorySearchResult("MEMORY.md", "MEMORY.md", excerpt, content.Length)); } } return results; } public async Task GetFileAsync(string name) { string? filePath; if (name.Equals("MEMORY.md", StringComparison.OrdinalIgnoreCase)) { filePath = LongTermPath; } else { if (!PathSecurityHelper.TryResolveSafePath(BasePath, name, out filePath)) return null; } if (!File.Exists(filePath!)) return null; var content = await File.ReadAllTextAsync(filePath!); return new MemoryFileContent(name, name, content, content.Length, File.GetLastWriteTimeUtc(filePath!)); } }