using Microsoft.AspNetCore.Mvc; using Nexus.Api.Helpers; namespace Nexus.Api.Controllers; [ApiController] [Route("api/v1/memory")] public class MemoryController : ControllerBase { [HttpGet] public IResult GetAll() { var basePath = "/mnt/workspace-iris/memory"; if (!Directory.Exists(basePath)) return Results.Ok(Array.Empty()); var files = Directory.GetFiles(basePath, "*.md") .Select(f => new FileInfo(f)) .OrderByDescending(f => f.Name) .Select(f => new { name = f.Name, path = f.FullName.Replace(basePath, "").TrimStart('/'), size = f.Length, modifiedAt = f.LastWriteTimeUtc }) .ToList(); var longTermPath = "/mnt/workspace-iris/MEMORY.md"; if (System.IO.File.Exists(longTermPath)) { var fi = new FileInfo(longTermPath); files.Insert(0, new { name = "MEMORY.md", path = "MEMORY.md", size = fi.Length, modifiedAt = fi.LastWriteTimeUtc }); } return Results.Ok(files); } [HttpGet("search")] public async Task Search([FromQuery] string q) { if (string.IsNullOrWhiteSpace(q) || q.Length < 2) return Results.BadRequest("Query must be at least 2 characters."); var basePath = "/mnt/workspace-iris/memory"; var results = new List(); const int maxFiles = 50; const int maxFileSize = 1_000_000; async Task SearchDir(string dir) { if (!Directory.Exists(dir)) return; var files = Directory.GetFiles(dir, "*.md").Take(maxFiles); foreach (var file in files) { var fi = new FileInfo(file); if (fi.Length > maxFileSize) continue; string content; using (var reader = new StreamReader(file)) content = await reader.ReadToEndAsync(); if (content.Contains(q, StringComparison.OrdinalIgnoreCase)) { var idx = content.IndexOf(q, StringComparison.OrdinalIgnoreCase); var start = Math.Max(0, idx - 60); var excerpt = (start > 0 ? "\u2026" : "") + content.Substring(start, Math.Min(200, content.Length - start)) + "\u2026"; results.Add(new { name = Path.GetFileName(file), path = file.Replace(basePath, "").TrimStart('/'), excerpt, size = fi.Length }); } } } await SearchDir(basePath); var longTermPath = "/mnt/workspace-iris/MEMORY.md"; if (System.IO.File.Exists(longTermPath)) { string content; using (var reader = new StreamReader(longTermPath)) content = await reader.ReadToEndAsync(); if (content.Contains(q, StringComparison.OrdinalIgnoreCase)) { var idx = content.IndexOf(q, StringComparison.OrdinalIgnoreCase); var start = Math.Max(0, idx - 60); var excerpt = (start > 0 ? "\u2026" : "") + content.Substring(start, Math.Min(200, content.Length - start)) + "\u2026"; results.Insert(0, new { name = "MEMORY.md", path = "MEMORY.md", excerpt, size = content.Length }); } } return Results.Ok(results); } [HttpGet("{name}")] public async Task GetFile(string name) { if (!PathSecurityHelper.TryResolveSafePath("/mnt/workspace-iris/memory", name, out var filePath)) return Results.BadRequest("Invalid filename."); var longTermPath = "/mnt/workspace-iris/MEMORY.md"; if (name.Equals("MEMORY.md", StringComparison.OrdinalIgnoreCase)) filePath = longTermPath; if (!System.IO.File.Exists(filePath!)) return Results.NotFound(); var content = await System.IO.File.ReadAllTextAsync(filePath!); return Results.Ok(new { name, path = name, content, size = content.Length, modifiedAt = System.IO.File.GetLastWriteTimeUtc(filePath!) }); } }