V11 / ca6f28eff240
WorkspaceReadExtensions.cs
Lilith/Core/Workspace/Extensions/WorkspaceReadExtensions.cs
Unchanged from the preceding versionCS
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 });
}
}