Skip to content

Commit 5f91a59

Browse files
committed
Add support for loading agent configurations from markdown files Implement MarkdownAgentParser utility to parse YAML frontmatter from markdown agent files, replacing JSON-based configuration. Update AgentManager to load .md files instead of .json files, with full frontmatter parsing and validation. Adds comprehensive test coverage for markdown parsing including edge cases and real-world agent file examples. Fixes #91
1 parent 1fa8e43 commit 5f91a59

10 files changed

+963
-191
lines changed

src/lib/AgentManager.test.ts

Lines changed: 396 additions & 62 deletions
Large diffs are not rendered by default.

src/lib/AgentManager.ts

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readFile } from 'fs/promises'
22
import { accessSync } from 'fs'
33
import path from 'path'
44
import { fileURLToPath } from 'url'
5+
import { MarkdownAgentParser } from '../utils/MarkdownAgentParser.js'
56
import { logger } from '../utils/logger.js'
67

78
// Agent schema interface
@@ -53,34 +54,37 @@ export class AgentManager {
5354
}
5455

5556
/**
56-
* Load all agent configuration files
57+
* Load all agent configuration files from markdown (.md) format
5758
* Throws error if agents directory doesn't exist or files are malformed
5859
*/
5960
async loadAgents(): Promise<AgentConfigs> {
60-
// Load all .json files from the agents directory
61+
// Load all .md files from the agents directory
6162
const { readdir } = await import('fs/promises')
6263
const files = await readdir(this.agentDir)
63-
const agentFiles = files.filter(file => file.endsWith('.json'))
64+
const agentFiles = files.filter(file => file.endsWith('.md'))
6465

6566
const agents: AgentConfigs = {}
6667

6768
for (const filename of agentFiles) {
6869
const agentPath = path.join(this.agentDir, filename)
69-
const agentName = path.basename(filename, '.json')
7070

7171
try {
7272
const content = await readFile(agentPath, 'utf-8')
73-
const agentConfig = JSON.parse(content) as AgentConfig
73+
74+
// Parse markdown with frontmatter
75+
const parsed = this.parseMarkdownAgent(content, filename)
76+
const agentConfig = parsed.config
77+
const agentName = parsed.name
7478

7579
// Validate required fields
7680
this.validateAgentConfig(agentConfig, agentName)
7781

7882
agents[agentName] = agentConfig
7983
logger.debug(`Loaded agent: ${agentName}`)
8084
} catch (error) {
81-
logger.error(`Failed to load agent ${agentName}`, { error })
85+
logger.error(`Failed to load agent from ${filename}`, { error })
8286
throw new Error(
83-
`Failed to load agent ${agentName}: ${error instanceof Error ? error.message : 'Unknown error'}`,
87+
`Failed to load agent from ${filename}: ${error instanceof Error ? error.message : 'Unknown error'}`,
8488
)
8589
}
8690
}
@@ -105,6 +109,63 @@ export class AgentManager {
105109
}
106110
}
107111

112+
/**
113+
* Parse markdown agent file with YAML frontmatter
114+
* @param content - Raw markdown file content
115+
* @param filename - Original filename for error messages
116+
* @returns Parsed agent config and name
117+
*/
118+
private parseMarkdownAgent(content: string, filename: string): { config: AgentConfig; name: string } {
119+
try {
120+
// Parse frontmatter using custom parser
121+
const { data, content: markdownBody } = MarkdownAgentParser.parse(content)
122+
123+
// Validate frontmatter has required fields
124+
if (!data.name) {
125+
throw new Error('Missing required field: name')
126+
}
127+
if (!data.description) {
128+
throw new Error('Missing required field: description')
129+
}
130+
if (!data.tools) {
131+
throw new Error('Missing required field: tools')
132+
}
133+
if (!data.model) {
134+
throw new Error('Missing required field: model')
135+
}
136+
137+
// Parse tools from comma-separated string to array
138+
const tools = data.tools
139+
.split(',')
140+
.map((tool: string) => tool.trim())
141+
.filter((tool: string) => tool.length > 0)
142+
143+
// Validate model and warn if non-standard
144+
const validModels = ['sonnet', 'opus', 'haiku']
145+
if (!validModels.includes(data.model)) {
146+
logger.warn(
147+
`Agent ${data.name} uses model "${data.model}" which may not be recognized by Claude CLI, and your workflow may fail or produce unexpected results. ` +
148+
`Valid values are: ${validModels.join(', ')}`
149+
)
150+
}
151+
152+
// Construct AgentConfig
153+
const config: AgentConfig = {
154+
description: data.description,
155+
prompt: markdownBody.trim(),
156+
tools,
157+
model: data.model,
158+
...(data.color && { color: data.color }),
159+
}
160+
161+
return { config, name: data.name }
162+
} catch (error) {
163+
throw new Error(
164+
`Failed to parse markdown agent ${filename}: ${error instanceof Error ? error.message : 'Unknown error'}`
165+
)
166+
}
167+
}
168+
108169
/**
109170
* Format loaded agents for Claude CLI --agents flag
110171
* Returns object suitable for JSON.stringify

0 commit comments

Comments
 (0)