diff --git a/apps/cli/package-lock.json b/apps/cli/package-lock.json index 3bad75cc..1660cd7a 100644 --- a/apps/cli/package-lock.json +++ b/apps/cli/package-lock.json @@ -35,6 +35,7 @@ "node-html-markdown": "^2.0.0", "ollama-ai-provider-v2": "^1.5.4", "react": "^18.3.1", + "yaml": "^2.8.2", "yargs": "^18.0.0", "zod": "^4.1.12" }, @@ -3608,6 +3609,21 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", diff --git a/apps/cli/package.json b/apps/cli/package.json index ca8f7c91..d254a82d 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -51,6 +51,7 @@ "node-html-markdown": "^2.0.0", "ollama-ai-provider-v2": "^1.5.4", "react": "^18.3.1", + "yaml": "^2.8.2", "yargs": "^18.0.0", "zod": "^4.1.12" } diff --git a/apps/cli/src/agents/agents.ts b/apps/cli/src/agents/agents.ts index 3c74f109..2ebfaef8 100644 --- a/apps/cli/src/agents/agents.ts +++ b/apps/cli/src/agents/agents.ts @@ -29,7 +29,7 @@ export const Agent = z.object({ name: z.string(), provider: z.string().optional(), model: z.string().optional(), - description: z.string(), + description: z.string().optional(), instructions: z.string(), tools: z.record(z.string(), ToolAttachment).optional(), }); diff --git a/apps/cli/src/agents/repo.ts b/apps/cli/src/agents/repo.ts index 615a8afc..317beb0e 100644 --- a/apps/cli/src/agents/repo.ts +++ b/apps/cli/src/agents/repo.ts @@ -1,8 +1,12 @@ 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[]>; @@ -13,33 +17,76 @@ export interface IAgentsRepo { } export class FSAgentsRepo implements IAgentsRepo { + private readonly agentsDir = path.join(WorkDir, "agents"); + async list(): Promise[]> { const result: z.infer[] = []; - // list all json files in workdir/agents/ - const agentsDir = path.join(WorkDir, "agents"); - const files = await fs.readdir(agentsDir); - for (const file of files) { - const contents = await fs.readFile(path.join(agentsDir, file), "utf8"); - result.push(Agent.parse(JSON.parse(contents))); + // 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> { + 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 = { + 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> { - const contents = await fs.readFile(path.join(WorkDir, "agents", `${id}.json`), "utf8"); - return Agent.parse(JSON.parse(contents)); + return this.parseAgentMd(path.join(this.agentsDir, `${id}.md`)); } async create(agent: z.infer): Promise { - await fs.writeFile(path.join(WorkDir, "agents", `${agent.name}.json`), JSON.stringify(agent, null, 2)); + await fs.writeFile(path.join(this.agentsDir, `${agent.name}.md`), agent.instructions); } - - async update(id: string, agent: z.infer): Promise { - await fs.writeFile(path.join(WorkDir, "agents", `${id}.json`), JSON.stringify(agent, null, 2)); + + async update(id: string, agent: z.infer): Promise { + 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 { - await fs.unlink(path.join(WorkDir, "agents", `${id}.json`)); + await fs.unlink(path.join(this.agentsDir, `${id}.md`)); } } \ No newline at end of file diff --git a/apps/cli/src/application/lib/bus.ts b/apps/cli/src/application/lib/bus.ts index d49c5d36..0987978e 100644 --- a/apps/cli/src/application/lib/bus.ts +++ b/apps/cli/src/application/lib/bus.ts @@ -13,7 +13,6 @@ export class InMemoryBus implements IBus { private subscribers: Map) => Promise)[]> = new Map(); async publish(event: z.infer): Promise { - console.log(this.subscribers); const pending: Promise[] = []; for (const subscriber of this.subscribers.get(event.runId) || []) { pending.push(subscriber(event)); @@ -21,7 +20,6 @@ export class InMemoryBus implements IBus { for (const subscriber of this.subscribers.get('*') || []) { pending.push(subscriber(event)); } - console.log(pending.length); await Promise.all(pending); } @@ -30,7 +28,6 @@ export class InMemoryBus implements IBus { this.subscribers.set(runId, []); } this.subscribers.get(runId)!.push(handler); - console.log(this.subscribers); return () => { this.subscribers.get(runId)!.splice(this.subscribers.get(runId)!.indexOf(handler), 1); }; diff --git a/apps/cli/src/server.ts b/apps/cli/src/server.ts index ac4ee333..be281d27 100644 --- a/apps/cli/src/server.ts +++ b/apps/cli/src/server.ts @@ -621,7 +621,6 @@ const routes = new Hono() unsub = await bus.subscribe('*', async (event) => { if (aborted) return; - console.log('got ev', event); await stream.writeSSE({ data: JSON.stringify(event), event: "message", diff --git a/apps/cli/src/tui/ui.tsx b/apps/cli/src/tui/ui.tsx index 860db411..b11bad5e 100644 --- a/apps/cli/src/tui/ui.tsx +++ b/apps/cli/src/tui/ui.tsx @@ -941,7 +941,7 @@ function AgentPickerModal({ onCancel: () => void; }) { const items = agents.map((agent) => ({ - label: `${agent.name} – ${truncate(agent.description, 40)}`, + label: `${agent.name}${agent.description ? ` – ${truncate(agent.description, 40)}` : ""}`, value: agent.name, })); return (