← All source files

V16 / 6df58cd3ed86

WorkspaceReadExtensions.cs

Lilith/Core/Workspace/Extensions/WorkspaceReadExtensions.cs

Unchanged from the preceding version
CS
using System.Text;
using System.Text.Json;

namespace Lilith.Core.Workspace.Extensions;

// Operation helpers are called under the session's gate and exclusive workspace lease.
internal static class WorkspaceReadExtensions
{
    internal static WorkspaceResult CheckExists(this WorkspaceRequest request, WorkspaceOptions options)
    {
        string path = options.ResolvePath(request.Path);
        return new("completed", "Workspace existence checked.", Data: new { path = request.Path, exists = File.Exists(path) || Directory.Exists(path) });
    }
    internal static WorkspaceResult ListPending(this WorkspaceRequest request, WorkspaceOptions options)
    {
        string pending = Path.Combine(options.GetMetadataDirectory(), "pending");
        var proposals = Directory.Exists(pending) ? Directory.EnumerateFiles(pending, "*.json").Order().Skip(request.Offset).Take(20)
            .Select(p => JsonSerializer.Deserialize<WorkspaceProposal>(File.ReadAllText(p))!).ToArray() : [];
        return new("completed", "Pending proposals (use host approval commands).", Data: proposals.Select(p => new { p.Id, p.Request.Operation, p.Request.Path, p.Request.Destination, p.CreatedAt }));
    }

    internal static WorkspaceResult ListBin(this WorkspaceRequest request, WorkspaceOptions options)
    {
        string bin = Path.Combine(options.GetMetadataDirectory(), "bin");
        return new("completed", "Workspace bin entries.", Data: Directory.Exists(bin) ? Directory.EnumerateDirectories(bin).Where(p => File.Exists(Path.Combine(p, "item")) || Directory.Exists(Path.Combine(p, "item"))).Order().Skip(request.Offset).Take(20)
            .Select(p => new { id = Path.GetFileName(p), original_path = options.GetOriginalBinPath(Path.GetFileName(p)) }).ToArray() : []);
    }

    internal static async Task<WorkspaceResult> ReadAsync(this WorkspaceRequest request, WorkspaceOptions options, CancellationToken ct)
    {
        string path = options.ResolvePath(request.Path);
        if (!path.IsTextFile()) throw new IOException("Text reading supports Markdown, TXT, JSON, CSV, XML and HTML. PDF/Office extraction is not implemented.");
        if (new FileInfo(path).Length > 1000000) throw new IOException("File exceeds the text-read limit.");
        string text = await File.ReadAllTextAsync(path, new UTF8Encoding(false, true), ct);
        int start = Math.Min(request.Offset, text.Length), length = Math.Min(4000, text.Length - start);
        return new("completed", "Read workspace text.", Data: new { path = request.Path, text = text.Substring(start, length), next_offset = start + length < text.Length ? start + length : (int?)null });
    }

    internal static WorkspaceResult ListEntries(this WorkspaceRequest request, WorkspaceOptions options)
    {
        string directory = request.Path.Length == 0 ? options.RootDirectory : options.ResolvePath(request.Path);
        var items = Directory.EnumerateFileSystemEntries(directory).Where(p => !Path.GetFileName(p).StartsWith('.')).Order().Skip(request.Offset).Take(21).ToArray();
        foreach (string item in items) item.EnsureNoLinks();
        return new("completed", "Workspace folder entries.", Data: new { matches = items.Take(20).Select(p => new { path = Path.GetRelativePath(options.RootDirectory, p).Replace('\\', '/'), folder = Directory.Exists(p), excerpt = (string?)null }), next_offset = items.Length > 20 ? request.Offset + 20 : (int?)null });
    }

    internal static async Task<WorkspaceResult> SearchAsync(this WorkspaceRequest request, WorkspaceOptions options, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(request.Query) || request.Query.Length > 200) throw new ArgumentException("Supply a query up to 200 characters.");
        string scope = request.Path.Length == 0 ? options.RootDirectory : options.ResolvePath(request.Path);
        var matches = new List<object>(); int found = 0;
        foreach (string item in scope.EnumerateEntries(2000))
        {
            ct.ThrowIfCancellationRequested();
            string relative = Path.GetRelativePath(options.RootDirectory, item).Replace('\\', '/');
            string? excerpt = null;
            if (File.Exists(item) && new FileInfo(item).Length <= 1000000 && item.IsTextFile())
            {
                string text = await File.ReadAllTextAsync(item, new UTF8Encoding(false, true), ct);
                int index = text.IndexOf(request.Query, StringComparison.OrdinalIgnoreCase);
                if (index >= 0) excerpt = text.Substring(Math.Max(0, index - 40), Math.Min(200, text.Length - Math.Max(0, index - 40)));
            }
            if (!relative.Contains(request.Query, StringComparison.OrdinalIgnoreCase) && excerpt is null) continue;
            if (found++ < request.Offset) continue;
            matches.Add(new { path = relative, folder = Directory.Exists(item), excerpt });
            if (matches.Count == 20) break;
        }
        return new("completed", "Workspace search results; use read for selected excerpts.", Data: new { matches, next_offset = matches.Count == 20 ? request.Offset + 20 : (int?)null });
    }

}