Skip to content

Commit 7b209ed

Browse files
Merge branch 'main' into feature/docker/run
2 parents f9a3654 + 83e891d commit 7b209ed

2 files changed

Lines changed: 48 additions & 3 deletions

File tree

src/modes/store.ts

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

17+
/**
18+
* Converts a human-readable name to a URL-safe slug.
19+
* @example kebabCase('Claude Persona') → 'claude-persona'
20+
*/
21+
function kebabCase(name: string): string {
22+
return name
23+
.toLowerCase()
24+
.replace(/[^a-z0-9]+/g, '-')
25+
.replace(/^-+|-+$/g, '')
26+
}
27+
28+
/**
29+
* Extracts YAML frontmatter and Markdown body from a string.
30+
* Expects the format used by Claude Code SKILL.md, OpenCode agents,
31+
* and Cursor rules: `---` delimited YAML followed by Markdown content.
32+
*
33+
* @throws {Error} If the string does not contain valid `---` delimiters.
34+
* @returns The parsed frontmatter object and the body text.
35+
*/
36+
function parseMarkdownFrontmatter(raw: string): {
37+
frontmatter: Record<string, unknown>
38+
body: string
39+
} {
40+
const parts = raw.split(/^---$/m)
41+
if (parts.length < 3) {
42+
throw new Error('Invalid markdown frontmatter: missing --- delimiters')
43+
}
44+
return {
45+
frontmatter: parseYaml(parts[1]) as Record<string, unknown>,
46+
body: parts.slice(2).join('---').trim(),
47+
}
48+
}
49+
1750
function loadCustomModes(): CCBMode[] {
1851
if (customModes !== null) return customModes
1952
customModes = []
@@ -23,19 +56,30 @@ function loadCustomModes(): CCBMode[] {
2356
mkdirSync(modesDir, { recursive: true })
2457
}
2558
const files = readdirSync(modesDir).filter(
26-
f => f.endsWith('.yaml') || f.endsWith('.yml'),
59+
f => f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.md'),
2760
)
2861
for (const file of files) {
2962
try {
3063
const raw = readFileSync(join(modesDir, file), 'utf-8')
31-
const data = parseYaml(raw) as Record<string, unknown>
64+
let data: Record<string, unknown>
65+
if (file.endsWith('.md')) {
66+
const { frontmatter, body } = parseMarkdownFrontmatter(raw)
67+
data = { ...frontmatter, system_prompt: body }
68+
if (!data.slug) {
69+
data.slug = data.name ? kebabCase(String(data.name)) : ''
70+
}
71+
data.icon = data.icon || '🤖'
72+
} else {
73+
data = parseYaml(raw) as Record<string, unknown>
74+
}
3275
if (!data.slug || !data.name) continue
3376
customModes.push({
3477
name: String(data.name),
3578
slug: String(data.slug),
3679
description: String(data.description || ''),
3780
icon: String(data.icon || '🔧'),
3881
systemPrompt: String(data.system_prompt || ''),
82+
model: data.model ? String(data.model) : undefined,
3983
ui: {
4084
accentColor: String(
4185
(data.ui as Record<string, unknown>)?.accent_color || '#00D4AA',
@@ -62,7 +106,7 @@ function loadCustomModes(): CCBMode[] {
62106
},
63107
})
64108
} catch {
65-
// skip invalid yaml files
109+
// skip invalid yaml or markdown files
66110
}
67111
}
68112
} 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)