feat(worker): add AgentFileService for filesystem agent management
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
76
src/ClaudeDo.Worker/Services/AgentFileService.cs
Normal file
76
src/ClaudeDo.Worker/Services/AgentFileService.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Services;
|
||||||
|
|
||||||
|
public sealed class AgentFileService
|
||||||
|
{
|
||||||
|
private readonly string _agentsDir;
|
||||||
|
|
||||||
|
public AgentFileService(string agentsDir)
|
||||||
|
{
|
||||||
|
_agentsDir = agentsDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<AgentInfo>> ScanAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var agents = new List<AgentInfo>();
|
||||||
|
if (!Directory.Exists(_agentsDir))
|
||||||
|
return Task.FromResult(agents);
|
||||||
|
|
||||||
|
foreach (var file in Directory.EnumerateFiles(_agentsDir, "*.md"))
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
var (name, description) = ParseFrontmatter(file);
|
||||||
|
agents.Add(new AgentInfo(name, description, file));
|
||||||
|
}
|
||||||
|
|
||||||
|
agents.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
return Task.FromResult(agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> ReadAsync(string path, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await File.ReadAllTextAsync(path, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WriteAsync(string path, string content, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(path);
|
||||||
|
if (dir is not null) Directory.CreateDirectory(dir);
|
||||||
|
await File.WriteAllTextAsync(path, content, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteAsync(string path, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
if (File.Exists(path)) File.Delete(path);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string name, string description) ParseFrontmatter(string filePath)
|
||||||
|
{
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
string name = fileName;
|
||||||
|
string description = "";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(filePath);
|
||||||
|
var firstLine = reader.ReadLine();
|
||||||
|
if (firstLine?.Trim() != "---")
|
||||||
|
return (name, description);
|
||||||
|
|
||||||
|
while (reader.ReadLine() is { } line)
|
||||||
|
{
|
||||||
|
if (line.Trim() == "---") break;
|
||||||
|
if (line.StartsWith("name:"))
|
||||||
|
name = line["name:".Length..].Trim();
|
||||||
|
else if (line.StartsWith("description:"))
|
||||||
|
description = line["description:".Length..].Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* Can't read file -- use filename fallback */ }
|
||||||
|
|
||||||
|
return (name, description);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using ClaudeDo.Worker.Services;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.Services;
|
||||||
|
|
||||||
|
public sealed class AgentFileServiceTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _agentDir;
|
||||||
|
private readonly AgentFileService _service;
|
||||||
|
|
||||||
|
public AgentFileServiceTests()
|
||||||
|
{
|
||||||
|
_agentDir = Path.Combine(Path.GetTempPath(), $"claudedo_agents_test_{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(_agentDir);
|
||||||
|
_service = new AgentFileService(_agentDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Scan_Returns_Empty_For_Empty_Directory()
|
||||||
|
{
|
||||||
|
var agents = await _service.ScanAsync();
|
||||||
|
Assert.Empty(agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Scan_Parses_Frontmatter()
|
||||||
|
{
|
||||||
|
var content = "---\nname: Test Agent\ndescription: A test agent for unit tests\n---\n\nYou are a test agent.";
|
||||||
|
await File.WriteAllTextAsync(Path.Combine(_agentDir, "test.md"), content);
|
||||||
|
|
||||||
|
var agents = await _service.ScanAsync();
|
||||||
|
Assert.Single(agents);
|
||||||
|
Assert.Equal("Test Agent", agents[0].Name);
|
||||||
|
Assert.Equal("A test agent for unit tests", agents[0].Description);
|
||||||
|
Assert.EndsWith("test.md", agents[0].Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Scan_Uses_Filename_When_No_Frontmatter()
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(Path.Combine(_agentDir, "simple.md"), "Just instructions.");
|
||||||
|
|
||||||
|
var agents = await _service.ScanAsync();
|
||||||
|
Assert.Single(agents);
|
||||||
|
Assert.Equal("simple", agents[0].Name);
|
||||||
|
Assert.Equal("", agents[0].Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Write_And_Read_Roundtrips()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_agentDir, "new-agent.md");
|
||||||
|
var content = "---\nname: New\ndescription: Desc\n---\nBody";
|
||||||
|
await _service.WriteAsync(path, content);
|
||||||
|
|
||||||
|
var read = await _service.ReadAsync(path);
|
||||||
|
Assert.Equal(content, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Delete_Removes_File()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_agentDir, "to-delete.md");
|
||||||
|
await File.WriteAllTextAsync(path, "temp");
|
||||||
|
|
||||||
|
await _service.DeleteAsync(path);
|
||||||
|
Assert.False(File.Exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Scan_Ignores_Non_Md_Files()
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(Path.Combine(_agentDir, "notes.txt"), "not an agent");
|
||||||
|
await File.WriteAllTextAsync(Path.Combine(_agentDir, "agent.md"), "---\nname: Real\ndescription: Yes\n---\nBody");
|
||||||
|
|
||||||
|
var agents = await _service.ScanAsync();
|
||||||
|
Assert.Single(agents);
|
||||||
|
Assert.Equal("Real", agents[0].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { Directory.Delete(_agentDir, true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user