rowboat/apps/cli/src/agents/repo.ts
2026-01-16 12:05:33 +05:30

94 lines
No EOL
3.3 KiB
TypeScript

import { WorkDir } from "../config/config.js";
import fs from "fs/promises";
import { glob } from "node:fs/promises";
import path from "path";
import z from "zod";
import { Agent } from "./agents.js";
import { parse, stringify } from "yaml";
const UpdateAgentSchema = Agent.omit({ name: true });
export interface IAgentsRepo {
list(): Promise<z.infer<typeof Agent>[]>;
fetch(id: string): Promise<z.infer<typeof Agent>>;
create(agent: z.infer<typeof Agent>): Promise<void>;
update(id: string, agent: z.infer<typeof Agent>): Promise<void>;
delete(id: string): Promise<void>;
}
export class FSAgentsRepo implements IAgentsRepo {
private readonly agentsDir = path.join(WorkDir, "agents");
async list(): Promise<z.infer<typeof Agent>[]> {
const result: z.infer<typeof Agent>[] = [];
// list all md files in workdir/agents/
const matches = await Array.fromAsync(glob("**/*.md", { cwd: this.agentsDir }));
for (const file of matches) {
try {
const agent = await this.parseAgentMd(path.join(this.agentsDir, file));
result.push(agent);
} catch (error) {
console.error(`Error parsing agent ${file}: ${error instanceof Error ? error.message : String(error)}`);
continue;
}
}
return result;
}
private async parseAgentMd(filePath: string): Promise<z.infer<typeof Agent>> {
const raw = await fs.readFile(filePath, "utf8");
// strip the path prefix from the file name
// and the .md extension
const agentName = filePath
.replace(this.agentsDir + "/", "")
.replace(/\.md$/, "");
let agent: z.infer<typeof Agent> = {
name: agentName,
instructions: raw,
};
let content = raw;
// check for frontmatter markers at start
if (raw.startsWith("---")) {
const end = raw.indexOf("\n---", 3);
if (end !== -1) {
const fm = raw.slice(3, end).trim(); // YAML text
content = raw.slice(end + 4).trim(); // body after frontmatter
const yaml = parse(fm);
const parsed = Agent
.omit({ name: true, instructions: true })
.parse(yaml);
agent = {
...agent,
...parsed,
instructions: content,
};
}
}
return agent;
}
async fetch(id: string): Promise<z.infer<typeof Agent>> {
return this.parseAgentMd(path.join(this.agentsDir, `${id}.md`));
}
async create(agent: z.infer<typeof Agent>): Promise<void> {
const { instructions, ...rest } = agent;
const contents = `---\n${stringify(rest)}\n---\n${instructions}`;
await fs.writeFile(path.join(this.agentsDir, `${agent.name}.md`), contents);
}
async update(id: string, agent: z.infer<typeof UpdateAgentSchema>): Promise<void> {
const { instructions, ...rest } = agent;
const contents = `---\n${stringify(rest)}\n---\n${instructions}`;
await fs.writeFile(path.join(this.agentsDir, `${id}.md`), contents);
}
async delete(id: string): Promise<void> {
await fs.unlink(path.join(this.agentsDir, `${id}.md`));
}
}