Skip to content

Commit 493e69f

Browse files
committed
feat: support markdown agent format (.md with YAML frontmatter) in mode loader
Extends the mode loader to accept .md files alongside .yaml/.yml in ~/.claude/modes/. Markdown files use YAML frontmatter for metadata and the body as systemPrompt — the same format supported by OpenCode, Claude Code agents, and Cursor rules. .md data is normalized to the same shape as .yaml data, reusing the existing CCBMode mapping with zero code duplication. - Add kebabCase() helper for slug derivation from name - Add parseMarkdownFrontmatter() helper (uses existing yaml package) - .md: body → system_prompt, auto-slug if missing, icon default 🤖 - Add optional model field to CCBMode for cross-tool alignment - Existing .yaml/.yml path: unchanged
1 parent 7e694fb commit 493e69f

2 files changed

Lines changed: 36 additions & 3 deletions

File tree

src/modes/store.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,27 @@ let currentModeSlug: string | null = null
1414
let customModes: CCBMode[] | null = null
1515
const modeListeners = new Set<() => void>()
1616

17+
function kebabCase(name: string): string {
18+
return name
19+
.toLowerCase()
20+
.replace(/[^a-z0-9]+/g, '-')
21+
.replace(/^-+|-+$/g, '')
22+
}
23+
24+
function parseMarkdownFrontmatter(raw: string): {
25+
frontmatter: Record<string, unknown>
26+
body: string
27+
} {
28+
const parts = raw.split(/^---$/m)
29+
if (parts.length < 3) {
30+
throw new Error('Invalid markdown frontmatter: missing --- delimiters')
31+
}
32+
return {
33+
frontmatter: parseYaml(parts[1]) as Record<string, unknown>,
34+
body: parts.slice(2).join('---').trim(),
35+
}
36+
}
37+
1738
function loadCustomModes(): CCBMode[] {
1839
if (customModes !== null) return customModes
1940
customModes = []
@@ -23,19 +44,30 @@ function loadCustomModes(): CCBMode[] {
2344
mkdirSync(modesDir, { recursive: true })
2445
}
2546
const files = readdirSync(modesDir).filter(
26-
f => f.endsWith('.yaml') || f.endsWith('.yml'),
47+
f => f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.md'),
2748
)
2849
for (const file of files) {
2950
try {
3051
const raw = readFileSync(join(modesDir, file), 'utf-8')
31-
const data = parseYaml(raw) as Record<string, unknown>
52+
let data: Record<string, unknown>
53+
if (file.endsWith('.md')) {
54+
const { frontmatter, body } = parseMarkdownFrontmatter(raw)
55+
data = { ...frontmatter, system_prompt: body }
56+
if (!data.slug) {
57+
data.slug = data.name ? kebabCase(String(data.name)) : ''
58+
}
59+
data.icon = data.icon || '🤖'
60+
} else {
61+
data = parseYaml(raw) as Record<string, unknown>
62+
}
3263
if (!data.slug || !data.name) continue
3364
customModes.push({
3465
name: String(data.name),
3566
slug: String(data.slug),
3667
description: String(data.description || ''),
3768
icon: String(data.icon || '🔧'),
3869
systemPrompt: String(data.system_prompt || ''),
70+
model: data.model ? String(data.model) : undefined,
3971
ui: {
4072
accentColor: String(
4173
(data.ui as Record<string, unknown>)?.accent_color || '#00D4AA',
@@ -62,7 +94,7 @@ function loadCustomModes(): CCBMode[] {
6294
},
6395
})
6496
} catch {
65-
// skip invalid yaml files
97+
// skip invalid yaml or markdown files
6698
}
6799
}
68100
} catch {

src/modes/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface CCBMode {
66
description: string
77
icon: string
88
systemPrompt: string
9+
model?: string
910
ui: {
1011
accentColor: string
1112
promptPrefix: string

0 commit comments

Comments
 (0)