zero-ai-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/MERGE_REPORT.md +265 -0
  2. package/README.md +127 -0
  3. package/package.json +55 -0
  4. package/packages/cli/src/index.ts +271 -0
  5. package/packages/cli/src/interactive.ts +190 -0
  6. package/packages/cli/src/once.ts +50 -0
  7. package/packages/core/src/agent/config-loader.ts +174 -0
  8. package/packages/core/src/agent/engine.ts +266 -0
  9. package/packages/core/src/agent/registry-tools.ts +58 -0
  10. package/packages/core/src/agent/registry.ts +213 -0
  11. package/packages/core/src/commands/index.ts +116 -0
  12. package/packages/core/src/config/index.ts +139 -0
  13. package/packages/core/src/confirmation/message-bus.ts +206 -0
  14. package/packages/core/src/diff/index.ts +232 -0
  15. package/packages/core/src/diff-detect/index.ts +207 -0
  16. package/packages/core/src/edit-coder/index.ts +194 -0
  17. package/packages/core/src/events/index.ts +151 -0
  18. package/packages/core/src/file-tracker/index.ts +183 -0
  19. package/packages/core/src/git/index.ts +305 -0
  20. package/packages/core/src/history/index.ts +236 -0
  21. package/packages/core/src/image/index.ts +131 -0
  22. package/packages/core/src/index.ts +131 -0
  23. package/packages/core/src/lsp/index.ts +251 -0
  24. package/packages/core/src/mcp/index.ts +186 -0
  25. package/packages/core/src/memory/index.ts +141 -0
  26. package/packages/core/src/memory/prompts.ts +52 -0
  27. package/packages/core/src/orchestrator/protocol.ts +140 -0
  28. package/packages/core/src/orchestrator/server.ts +319 -0
  29. package/packages/core/src/output/index.ts +164 -0
  30. package/packages/core/src/permissions/index.ts +126 -0
  31. package/packages/core/src/prompts/engine.ts +188 -0
  32. package/packages/core/src/providers/registry.ts +687 -0
  33. package/packages/core/src/routing/model-router.ts +160 -0
  34. package/packages/core/src/server/index.ts +232 -0
  35. package/packages/core/src/session/index.ts +174 -0
  36. package/packages/core/src/session/service.ts +322 -0
  37. package/packages/core/src/skills/index.ts +205 -0
  38. package/packages/core/src/streaming/index.ts +166 -0
  39. package/packages/core/src/sub-agent/index.ts +179 -0
  40. package/packages/core/src/tools/builtin.ts +568 -0
  41. package/packages/core/src/types.ts +356 -0
  42. package/packages/sdk/src/index.ts +247 -0
  43. package/packages/tui/src/index.ts +141 -0
  44. package/tsconfig.json +26 -0
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * ZERO CLI - Main Entry Point
4
+ *
5
+ * Command-line interface for ZERO AI Coding Assistant
6
+ * Inspired by: gemini-cli, crush, opencode CLI interfaces
7
+ */
8
+
9
+ import { Command } from "commander";
10
+ import {
11
+ ConfigManager,
12
+ ProviderRegistry,
13
+ ToolRegistry,
14
+ AgentRegistry,
15
+ DEFAULT_AGENTS,
16
+ SessionManager,
17
+ MCPManager,
18
+ FileMemoryStore,
19
+ ContextManager,
20
+ createPermissionHandler,
21
+ getAllBuiltinTools,
22
+ AgentEngine,
23
+ DEFAULT_PERMISSION_CONFIG,
24
+ } from "@zero/core";
25
+ import type { PermissionRequest, PermissionDecision, Message } from "@zero/core";
26
+ import { runInteractive } from "./interactive.js";
27
+ import { runOnce } from "./once.js";
28
+
29
+ const program = new Command();
30
+
31
+ program
32
+ .name("zero")
33
+ .description("ZERO - Unified AI Coding Assistant")
34
+ .version("1.0.0");
35
+
36
+ // Default interactive mode
37
+ program
38
+ .argument("[prompt]", "Initial prompt to send to ZERO")
39
+ .option("-m, --model <model>", "Model to use", "gpt-4o")
40
+ .option("-p, --provider <provider>", "LLM provider", "openai")
41
+ .option("-d, --directory <path>", "Working directory", process.cwd())
42
+ .option("--auto-approve", "Auto-approve all tool calls", false)
43
+ .option("--verbose", "Show verbose output", false)
44
+ .action(async (prompt, options) => {
45
+ const { zero, session, memory, contextManager } = await initializeZero(options);
46
+
47
+ if (prompt) {
48
+ await runOnce(zero, prompt, session.id, options);
49
+ } else {
50
+ await runInteractive(zero, session.id, memory, contextManager, options);
51
+ }
52
+ });
53
+
54
+ // Init command
55
+ program
56
+ .command("init")
57
+ .description("Initialize ZERO in the current project")
58
+ .action(async () => {
59
+ const config = new ConfigManager(process.cwd());
60
+ await config.init();
61
+ console.log("✅ ZERO initialized successfully!");
62
+ console.log(` Config: ${config.configPath || "zero.config.json"}`);
63
+ console.log(` Data: ${config.getDataDir()}`);
64
+ });
65
+
66
+ // Config command
67
+ program
68
+ .command("config")
69
+ .description("Show or modify ZERO configuration")
70
+ .option("--show", "Show current configuration")
71
+ .option("--set <key=value>", "Set a configuration value")
72
+ .action(async (options) => {
73
+ const config = new ConfigManager(process.cwd());
74
+ await config.load();
75
+
76
+ if (options.show || (!options.set)) {
77
+ console.log(JSON.stringify(config.get(), null, 2));
78
+ }
79
+
80
+ if (options.set) {
81
+ const [key, value] = options.set.split("=");
82
+ config.update({ [key]: value } as any);
83
+ await config.save();
84
+ console.log(`✅ Set ${key} = ${value}`);
85
+ }
86
+ });
87
+
88
+ // Session management
89
+ program
90
+ .command("sessions")
91
+ .description("List or manage sessions")
92
+ .option("--list", "List all sessions")
93
+ .option("--delete <id>", "Delete a session")
94
+ .action(async (options) => {
95
+ const config = new ConfigManager(process.cwd());
96
+ await config.load();
97
+ const sessionMgr = new SessionManager({
98
+ storagePath: `${config.getDataDir()}/sessions`,
99
+ defaultConfig: {},
100
+ });
101
+ await sessionMgr.loadAll();
102
+
103
+ if (options.delete) {
104
+ await sessionMgr.delete(options.delete);
105
+ console.log(`✅ Session ${options.delete} deleted`);
106
+ return;
107
+ }
108
+
109
+ const sessions = sessionMgr.list();
110
+ if (sessions.length === 0) {
111
+ console.log("No sessions found.");
112
+ return;
113
+ }
114
+
115
+ console.log("📋 Sessions:");
116
+ for (const s of sessions) {
117
+ const date = new Date(s.updatedAt).toLocaleString();
118
+ console.log(` ${s.id} - ${s.name} (${s.messages.length} messages) - ${date}`);
119
+ }
120
+ });
121
+
122
+ // Agents list
123
+ program
124
+ .command("agents")
125
+ .description("List available agents")
126
+ .action(() => {
127
+ console.log("🤖 Available Agents:");
128
+ for (const agent of DEFAULT_AGENTS) {
129
+ console.log(` ${agent.id}: ${agent.description}`);
130
+ console.log(` Tools: ${agent.tools.join(", ")}`);
131
+ console.log(` Max turns: ${agent.maxTurns}`);
132
+ }
133
+ });
134
+
135
+ // Tools list
136
+ program
137
+ .command("tools")
138
+ .description("List available tools")
139
+ .action(() => {
140
+ const tools = getAllBuiltinTools();
141
+ console.log("🔧 Available Tools:");
142
+ for (const tool of tools) {
143
+ const approval = tool.requiresApproval ? "⚠️ requires approval" : "✅ auto-approved";
144
+ console.log(` ${tool.name} [${tool.category}] - ${approval}`);
145
+ console.log(` ${tool.description.split("\n")[0]}`);
146
+ }
147
+ });
148
+
149
+ // Providers list
150
+ program
151
+ .command("providers")
152
+ .description("List available LLM providers")
153
+ .action(() => {
154
+ const registry = new ProviderRegistry();
155
+ registry.loadBuiltins();
156
+
157
+ console.log("🌐 Available Providers:");
158
+ for (const provider of registry.list()) {
159
+ console.log(` ${provider.id}: ${provider.name}`);
160
+ for (const model of provider.models) {
161
+ console.log(` ${model.id} - ${model.name} (${model.contextWindow} context)`);
162
+ }
163
+ }
164
+ });
165
+
166
+ async function initializeZero(options: any) {
167
+ const config = new ConfigManager(options.directory || process.cwd());
168
+ await config.load();
169
+
170
+ // Initialize providers
171
+ const providerRegistry = new ProviderRegistry();
172
+ providerRegistry.loadBuiltins();
173
+
174
+ // Initialize tools
175
+ const toolRegistry = new ToolRegistry();
176
+ for (const tool of getAllBuiltinTools()) {
177
+ toolRegistry.register(tool);
178
+ }
179
+
180
+ // Initialize agents
181
+ const sessionMgr = new SessionManager({
182
+ storagePath: `${config.getDataDir()}/sessions`,
183
+ defaultConfig: {
184
+ provider: options.provider || config.get().defaultProvider,
185
+ model: options.model || config.get().defaultModel,
186
+ workingDirectory: options.directory || process.cwd(),
187
+ },
188
+ });
189
+ await sessionMgr.loadAll();
190
+
191
+ const session = await sessionMgr.create();
192
+
193
+ // Initialize memory
194
+ const memory = new FileMemoryStore(`${config.getDataDir()}/memory`);
195
+ await memory.init();
196
+ const contextManager = new ContextManager(memory);
197
+
198
+ // Create permission handler
199
+ let permissionHandler;
200
+ if (options.autoApprove) {
201
+ permissionHandler = async () => "allow" as PermissionDecision;
202
+ } else {
203
+ permissionHandler = createPermissionHandler(
204
+ DEFAULT_PERMISSION_CONFIG,
205
+ async (request: PermissionRequest) => {
206
+ // In CLI mode, ask the user
207
+ const readline = await import("node:readline");
208
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
209
+ return new Promise<PermissionDecision>((resolve) => {
210
+ rl.question(`\n⚠️ Permission required: ${request.type} - ${request.resource}\n ${request.details || ""}\n Allow? [y/n/a(lways)] `, (answer) => {
211
+ rl.close();
212
+ if (answer === "a") resolve("allow_always");
213
+ else if (answer === "y") resolve("allow");
214
+ else resolve("deny");
215
+ });
216
+ });
217
+ },
218
+ );
219
+ }
220
+
221
+ // Create agent registry
222
+ const agentRegistry = new AgentRegistry({
223
+ clientFactory: (model) => {
224
+ const modelId = model === "default" ? (options.model || config.get().defaultModel) : model;
225
+ const providerId = options.provider || config.get().defaultProvider;
226
+ const providerConfig = config.get().providers[providerId] || {};
227
+ return providerRegistry.createClient(providerId, {
228
+ model: modelId,
229
+ apiKey: providerConfig.apiKey,
230
+ baseUrl: providerConfig.baseUrl,
231
+ });
232
+ },
233
+ tools: toolRegistry.getMap(),
234
+ permissionHandler,
235
+ workingDirectory: options.directory || process.cwd(),
236
+ sessionId: session.id,
237
+ });
238
+
239
+ // Register default agents
240
+ for (const agent of DEFAULT_AGENTS) {
241
+ agentRegistry.register(agent);
242
+ }
243
+
244
+ // Initialize MCP
245
+ const mcpManager = new MCPManager();
246
+ const mcpServers = config.get().mcpServers || [];
247
+ for (const server of mcpServers) {
248
+ await mcpManager.connect(server);
249
+ }
250
+
251
+ // Register MCP tools
252
+ for (const tool of mcpManager.getToolsAsDefinitions()) {
253
+ toolRegistry.register(tool);
254
+ }
255
+
256
+ return {
257
+ zero: {
258
+ agentRegistry,
259
+ toolRegistry,
260
+ providerRegistry,
261
+ sessionMgr,
262
+ mcpManager,
263
+ config,
264
+ },
265
+ session,
266
+ memory,
267
+ contextManager,
268
+ };
269
+ }
270
+
271
+ program.parse();
@@ -0,0 +1,190 @@
1
+ /**
2
+ * ZERO CLI - Interactive Mode
3
+ * REPL interface with streaming, colors, and tool approval
4
+ */
5
+
6
+ import * as readline from "node:readline";
7
+ import chalk from "chalk";
8
+ import type { AgentEngine, Message, FileMemoryStore, ContextManager } from "@zero/core";
9
+
10
+ interface ZeroRuntime {
11
+ agentRegistry: any;
12
+ toolRegistry: any;
13
+ providerRegistry: any;
14
+ sessionMgr: any;
15
+ mcpManager: any;
16
+ config: any;
17
+ }
18
+
19
+ const BANNER = `
20
+ ${chalk.bold.hex("#8B5CF6")("╔═══════════════════════════════════════╗")}
21
+ ${chalk.bold.hex("#8B5CF6")("║")} ${chalk.bold.white("ZERO")} ${chalk.gray("v1.0.0")} — ${chalk.gray("Unified AI Assistant")} ${chalk.bold.hex("#8B5CF6")("║")}
22
+ ${chalk.bold.hex("#8B5CF6")("║")} ${chalk.gray("Type your message or /help")} ${chalk.bold.hex("#8B5CF6")("║")}
23
+ ${chalk.bold.hex("#8B5CF6")("╚═══════════════════════════════════════╝")}
24
+ `;
25
+
26
+ export async function runInteractive(
27
+ zero: ZeroRuntime,
28
+ sessionId: string,
29
+ memory: FileMemoryStore,
30
+ contextManager: ContextManager,
31
+ options: any,
32
+ ): Promise<void> {
33
+ console.log(BANNER);
34
+
35
+ const rl = readline.createInterface({
36
+ input: process.stdin,
37
+ output: process.stdout,
38
+ });
39
+
40
+ const prompt = () => {
41
+ rl.question(chalk.hex("#8B5CF6")("❯ "), async (input) => {
42
+ input = input.trim();
43
+
44
+ if (!input) {
45
+ prompt();
46
+ return;
47
+ }
48
+
49
+ // Handle commands
50
+ if (input.startsWith("/")) {
51
+ await handleCommand(input, zero, rl);
52
+ prompt();
53
+ return;
54
+ }
55
+
56
+ // Build context from memory
57
+ const context = await contextManager.buildContext(input);
58
+
59
+ // Create agent engine
60
+ const engine = zero.agentRegistry.createEngine("coder");
61
+
62
+ // Add context if available
63
+ let fullPrompt = input;
64
+ if (context) {
65
+ fullPrompt = `${input}\n\n${context}`;
66
+ }
67
+
68
+ // Run agent
69
+ console.log(chalk.gray("\n⚡ Processing...\n"));
70
+
71
+ const result = await engine.run(fullPrompt);
72
+
73
+ // Display results
74
+ for (const message of result.messages) {
75
+ if (message.role === "assistant") {
76
+ for (const content of message.content) {
77
+ if (content.type === "text") {
78
+ console.log(chalk.white(content.text));
79
+ } else if (content.type === "tool_call") {
80
+ console.log(chalk.yellow(`\n🔧 Tool: ${content.name}`));
81
+ console.log(chalk.gray(` Args: ${JSON.stringify(content.arguments)}`));
82
+ }
83
+ }
84
+ } else if (message.role === "tool") {
85
+ for (const content of message.content) {
86
+ if (content.type === "tool_result") {
87
+ if (content.error) {
88
+ console.log(chalk.red(` ❌ ${content.error}`));
89
+ } else {
90
+ const output = typeof content.result === "string" ? content.result : JSON.stringify(content.result);
91
+ const preview = output.length > 500 ? output.slice(0, 500) + "..." : output;
92
+ console.log(chalk.green(` ✓ ${preview}`));
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ // Show usage stats
100
+ console.log(chalk.gray(`\n📊 Tokens: ${result.tokenUsage.inputTokens} in / ${result.tokenUsage.outputTokens} out | Duration: ${(result.duration / 1000).toFixed(1)}s`));
101
+
102
+ // Save important facts to memory
103
+ await contextManager.saveFact(`User asked: ${input}`, ["conversation"]);
104
+
105
+ prompt();
106
+ });
107
+ };
108
+
109
+ prompt();
110
+ }
111
+
112
+ async function handleCommand(input: string, zero: ZeroRuntime, rl: readline.Interface): Promise<void> {
113
+ const [command, ...args] = input.slice(1).split(" ");
114
+
115
+ switch (command) {
116
+ case "help":
117
+ console.log(`
118
+ ${chalk.bold("Commands:")}
119
+ ${chalk.cyan("/help")} Show this help message
120
+ ${chalk.cyan("/clear")} Clear the conversation
121
+ ${chalk.cyan("/model <name>")} Switch to a different model
122
+ ${chalk.cyan("/provider <id>")} Switch to a different provider
123
+ ${chalk.cyan("/agents")} List available agents
124
+ ${chalk.cyan("/tools")} List available tools
125
+ ${chalk.cyan("/memory")} Show stored memories
126
+ ${chalk.cyan("/session")} Show session info
127
+ ${chalk.cyan("/quit")} Exit ZERO
128
+ `);
129
+ break;
130
+
131
+ case "clear":
132
+ console.clear();
133
+ console.log(chalk.gray("Conversation cleared."));
134
+ break;
135
+
136
+ case "model":
137
+ if (args[0]) {
138
+ console.log(chalk.green(`✅ Model switched to: ${args[0]}`));
139
+ } else {
140
+ console.log(chalk.yellow("Usage: /model <model-name>"));
141
+ }
142
+ break;
143
+
144
+ case "provider":
145
+ if (args[0]) {
146
+ console.log(chalk.green(`✅ Provider switched to: ${args[0]}`));
147
+ } else {
148
+ console.log(chalk.yellow("Usage: /provider <provider-id>"));
149
+ }
150
+ break;
151
+
152
+ case "agents":
153
+ const agents = zero.agentRegistry.list();
154
+ console.log(chalk.bold("\n🤖 Agents:"));
155
+ for (const agent of agents) {
156
+ console.log(` ${chalk.cyan(agent.id)}: ${agent.description}`);
157
+ }
158
+ break;
159
+
160
+ case "tools":
161
+ const tools = zero.toolRegistry.getAll();
162
+ console.log(chalk.bold("\n🔧 Tools:"));
163
+ for (const tool of tools) {
164
+ console.log(` ${chalk.cyan(tool.name)} [${tool.category}]`);
165
+ }
166
+ break;
167
+
168
+ case "memory":
169
+ const memory = zero.memory || [];
170
+ console.log(chalk.bold("\n🧠 Memory:"));
171
+ if (memory.length === 0) {
172
+ console.log(chalk.gray(" No memories stored."));
173
+ }
174
+ break;
175
+
176
+ case "session":
177
+ console.log(chalk.bold("\n📋 Session Info:"));
178
+ console.log(` Status: Active`);
179
+ break;
180
+
181
+ case "quit":
182
+ case "exit":
183
+ case "q":
184
+ console.log(chalk.gray("\n👋 Goodbye!"));
185
+ process.exit(0);
186
+
187
+ default:
188
+ console.log(chalk.red(`Unknown command: /${command}. Type /help for available commands.`));
189
+ }
190
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * ZERO CLI - One-shot Mode
3
+ * Execute a single prompt and exit
4
+ */
5
+
6
+ import chalk from "chalk";
7
+ import type { AgentEngine } from "@zero/core";
8
+
9
+ interface ZeroRuntime {
10
+ agentRegistry: any;
11
+ toolRegistry: any;
12
+ providerRegistry: any;
13
+ sessionMgr: any;
14
+ mcpManager: any;
15
+ config: any;
16
+ }
17
+
18
+ export async function runOnce(
19
+ zero: ZeroRuntime,
20
+ prompt: string,
21
+ sessionId: string,
22
+ options: any,
23
+ ): Promise<void> {
24
+ const engine = zero.agentRegistry.createEngine("coder");
25
+
26
+ if (options.verbose) {
27
+ console.log(chalk.gray(`Provider: ${options.provider || "openai"}`));
28
+ console.log(chalk.gray(`Model: ${options.model || "gpt-4o"}`));
29
+ console.log(chalk.gray(`Directory: ${options.directory || process.cwd()}`));
30
+ console.log("");
31
+ }
32
+
33
+ const result = await engine.run(prompt);
34
+
35
+ // Output results
36
+ for (const message of result.messages) {
37
+ if (message.role === "assistant") {
38
+ for (const content of message.content) {
39
+ if (content.type === "text") {
40
+ console.log(content.text);
41
+ } else if (content.type === "tool_call" && options.verbose) {
42
+ console.error(chalk.yellow(`[Tool: ${content.name}]`));
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ // Exit with appropriate code
49
+ process.exit(result.success ? 0 : 1);
50
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * ZERO Agent Config Loader
3
+ * Adapted from: cline - Apache 2.0
4
+ *
5
+ * Loads agent configurations from YAML/Markdown frontmatter files.
6
+ * Supports file watching for dynamic agent reloading.
7
+ */
8
+
9
+ import * as fs from "node:fs/promises";
10
+ import * as path from "node:path";
11
+ import type { AgentConfig, ToolCategory } from "../types.js";
12
+
13
+ /**
14
+ * Built-in tool names available for agents
15
+ */
16
+ export const BUILTIN_TOOLS = [
17
+ "read_file",
18
+ "write_file",
19
+ "edit_file",
20
+ "list_directory",
21
+ "search_files",
22
+ "search_code",
23
+ "execute_shell",
24
+ "git_status",
25
+ "git_diff",
26
+ "git_commit",
27
+ "web_fetch",
28
+ "web_search",
29
+ "delegate_to_agent",
30
+ ] as const;
31
+
32
+ export type BuiltinTool = (typeof BUILTIN_TOOLS)[number];
33
+
34
+ /**
35
+ * Schema for agent configuration in YAML frontmatter
36
+ */
37
+ export interface AgentFrontmatterConfig {
38
+ name: string;
39
+ description: string;
40
+ modelId?: string;
41
+ tools?: string | string[];
42
+ skills?: string | string[];
43
+ maxTurns?: number;
44
+ }
45
+
46
+ /**
47
+ * Parse YAML frontmatter from a markdown file
48
+ */
49
+ export function parseFrontmatter(content: string): { frontmatter: Record<string, unknown>; body: string } {
50
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
51
+ if (!match) {
52
+ return { frontmatter: {}, body: content };
53
+ }
54
+
55
+ const yamlStr = match[1];
56
+ const body = match[2];
57
+
58
+ // Simple YAML parser for key: value pairs
59
+ const frontmatter: Record<string, unknown> = {};
60
+ const lines = yamlStr.split("\n");
61
+ let currentKey = "";
62
+ let currentArray: string[] = [];
63
+ let inArray = false;
64
+
65
+ for (const line of lines) {
66
+ const trimmed = line.trim();
67
+ if (!trimmed || trimmed.startsWith("#")) continue;
68
+
69
+ if (trimmed.startsWith("- ") && inArray) {
70
+ currentArray.push(trimmed.slice(2).trim());
71
+ continue;
72
+ }
73
+
74
+ if (inArray && currentKey) {
75
+ frontmatter[currentKey] = currentArray;
76
+ inArray = false;
77
+ currentArray = [];
78
+ }
79
+
80
+ const colonIdx = trimmed.indexOf(":");
81
+ if (colonIdx > 0) {
82
+ const key = trimmed.slice(0, colonIdx).trim();
83
+ const value = trimmed.slice(colonIdx + 1).trim();
84
+
85
+ if (value === "" || value === "[]") {
86
+ currentKey = key;
87
+ inArray = true;
88
+ currentArray = [];
89
+ } else {
90
+ frontmatter[key] = value.replace(/^["']|["']$/g, "");
91
+ }
92
+ }
93
+ }
94
+
95
+ if (inArray && currentKey) {
96
+ frontmatter[currentKey] = currentArray;
97
+ }
98
+
99
+ return { frontmatter, body };
100
+ }
101
+
102
+ /**
103
+ * Parse tools from config (string or array)
104
+ */
105
+ function parseTools(tools: string | string[] | undefined): string[] {
106
+ if (!tools) return [...BUILTIN_TOOLS].filter((t) => t !== "delegate_to_agent");
107
+
108
+ const rawTools = Array.isArray(tools) ? tools : tools.split(",").map((t) => t.trim());
109
+ return Array.from(new Set(rawTools.filter((t) => BUILTIN_TOOLS.includes(t as BuiltinTool))));
110
+ }
111
+
112
+ /**
113
+ * Parse skills from config
114
+ */
115
+ function parseSkills(skills: string | string[] | undefined): string[] {
116
+ if (!skills) return [];
117
+ return Array.isArray(skills) ? skills : skills.split(",").map((s) => s.trim());
118
+ }
119
+
120
+ /**
121
+ * Load an agent configuration from a markdown file
122
+ */
123
+ export async function loadAgentFromFile(filePath: string): Promise<AgentConfig> {
124
+ const content = await fs.readFile(filePath, "utf-8");
125
+ const { frontmatter, body } = parseFrontmatter(content);
126
+
127
+ const config = frontmatter as unknown as AgentFrontmatterConfig;
128
+
129
+ if (!config.name) {
130
+ throw new Error(`Agent config in ${filePath} missing required field: name`);
131
+ }
132
+
133
+ if (!config.description) {
134
+ throw new Error(`Agent config in ${filePath} missing required field: description`);
135
+ }
136
+
137
+ const id = path.basename(filePath, path.extname(filePath)).toLowerCase().replace(/[^a-z0-9]/g, "-");
138
+
139
+ return {
140
+ id,
141
+ name: config.name,
142
+ description: config.description,
143
+ systemPrompt: body.trim() || `You are ${config.name}, a specialized agent.`,
144
+ tools: parseTools(config.tools),
145
+ maxTurns: config.maxTurns || 50,
146
+ model: config.modelId || "default",
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Load all agent configurations from a directory
152
+ */
153
+ export async function loadAgentsFromDirectory(dirPath: string): Promise<AgentConfig[]> {
154
+ const agents: AgentConfig[] = [];
155
+
156
+ try {
157
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
158
+
159
+ for (const entry of entries) {
160
+ if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".yaml"))) {
161
+ try {
162
+ const agent = await loadAgentFromFile(path.join(dirPath, entry.name));
163
+ agents.push(agent);
164
+ } catch (error) {
165
+ // Skip invalid files
166
+ }
167
+ }
168
+ }
169
+ } catch {
170
+ // Directory doesn't exist
171
+ }
172
+
173
+ return agents;
174
+ }