@@ -14,6 +14,39 @@ let currentModeSlug: string | null = null
1414let customModes : CCBMode [ ] | null = null
1515const 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 - z 0 - 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+
1750function 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 {
0 commit comments