@@ -13,6 +13,131 @@ import { walkSkillsTree, copySkillsDirectory } from './SkillsUtils';
1313import { parseFrontmatter } from './FrontmatterParser' ;
1414import type { IAgent } from '../agents/IAgent' ;
1515
16+ /**
17+ * For non-Claude agents, compile a wrapper SKILL.md (body is a single @reference)
18+ * into a standalone SKILL.md with the referenced file's body inlined.
19+ *
20+ * We intentionally keep this conservative: only expand when the body is *just*
21+ * an @reference line, to avoid accidentally treating email addresses or
22+ * "@mentions" inside real content as file references.
23+ */
24+ async function compileSkillMdForNonClaudeAgents (
25+ skillMdContent : string ,
26+ projectRoot : string ,
27+ skillFolderPath : string ,
28+ ) : Promise < string > {
29+ const { frontmatter, rawFrontmatter, body } =
30+ parseFrontmatter ( skillMdContent ) ;
31+ const refCheck = isReferenceBody ( body ) ;
32+
33+ if ( ! refCheck . isReference || ! refCheck . referencePath ) {
34+ return skillMdContent ;
35+ }
36+
37+ const referencePath = refCheck . referencePath ;
38+ const absoluteRefPath =
39+ referencePath . startsWith ( './' ) || referencePath . startsWith ( '../' )
40+ ? path . resolve ( skillFolderPath , referencePath )
41+ : path . resolve ( projectRoot , referencePath ) ;
42+
43+ // Security: only inline references within the project root.
44+ const normalizedProjectRoot = path . resolve ( projectRoot ) ;
45+ const normalizedAbsoluteRefPath = path . resolve ( absoluteRefPath ) ;
46+ if ( ! normalizedAbsoluteRefPath . startsWith ( normalizedProjectRoot + path . sep ) ) {
47+ return skillMdContent ;
48+ }
49+
50+ let referencedContent : string ;
51+ try {
52+ referencedContent = await fs . readFile ( normalizedAbsoluteRefPath , 'utf8' ) ;
53+ } catch {
54+ return skillMdContent ;
55+ }
56+
57+ const { body : referencedBody } = parseFrontmatter ( referencedContent ) ;
58+
59+ const fmData =
60+ rawFrontmatter && Object . keys ( rawFrontmatter ) . length > 0
61+ ? rawFrontmatter
62+ : frontmatter && Object . keys ( frontmatter ) . length > 0
63+ ? frontmatter
64+ : null ;
65+
66+ if ( fmData ) {
67+ return `---
68+ ${ yaml . dump ( fmData , { lineWidth : - 1 , noRefs : true } ) . trim ( ) }
69+ ---
70+
71+ ${ referencedBody }
72+ ` ;
73+ }
74+
75+ return `${ referencedBody } \n` ;
76+ }
77+
78+ /**
79+ * Copies a single skill directory to an agent skill directory:
80+ * - SKILL.md is compiled (inlines @reference wrapper content)
81+ * - .mdc files are excluded (Claude-only sources)
82+ * - all other files are copied as-is
83+ */
84+ async function copySkillDirectoryForNonClaudeAgents (
85+ src : string ,
86+ dest : string ,
87+ projectRoot : string ,
88+ skillFolderPath : string ,
89+ depth : number = 0 ,
90+ ) : Promise < void > {
91+ // Security: Prevent DoS via deeply nested directories
92+ if ( depth >= MAX_RECURSION_DEPTH ) {
93+ return ;
94+ }
95+
96+ const stat = await fs . stat ( src ) ;
97+
98+ if ( stat . isDirectory ( ) ) {
99+ await fs . mkdir ( dest , { recursive : true } ) ;
100+ const entries = await fs . readdir ( src , { withFileTypes : true } ) ;
101+
102+ for ( const entry of entries ) {
103+ // Exclude all .mdc files from agent skills directories.
104+ if ( entry . isFile ( ) && entry . name . endsWith ( '.mdc' ) ) {
105+ continue ;
106+ }
107+
108+ const srcPath = path . join ( src , entry . name ) ;
109+ const destPath = path . join ( dest , entry . name ) ;
110+ await copySkillDirectoryForNonClaudeAgents (
111+ srcPath ,
112+ destPath ,
113+ projectRoot ,
114+ skillFolderPath ,
115+ depth + 1 ,
116+ ) ;
117+ }
118+ return ;
119+ }
120+
121+ // Files
122+ if ( path . basename ( src ) === SKILL_MD_FILENAME ) {
123+ const content = await fs . readFile ( src , 'utf8' ) ;
124+ const compiled = await compileSkillMdForNonClaudeAgents (
125+ content ,
126+ projectRoot ,
127+ skillFolderPath ,
128+ ) ;
129+ await fs . writeFile ( dest , compiled , 'utf8' ) ;
130+ return ;
131+ }
132+
133+ // Extra guard: skip .mdc even if reached via recursion.
134+ if ( src . endsWith ( '.mdc' ) ) {
135+ return ;
136+ }
137+
138+ await fs . copyFile ( src , dest ) ;
139+ }
140+
16141/**
17142 * Check if SKILL.md body is just a reference (single non-empty line starting with @).
18143 * This replaces the previous synced: true frontmatter detection.
@@ -281,7 +406,9 @@ ${yaml.dump(skillFrontmatter || { name: skillName }, { lineWidth: -1, noRefs: tr
281406 if ( refCheck . referencePath ?. includes ( '/rules/' ) ) {
282407 const refFileName = path . basename ( refCheck . referencePath ) ;
283408 const refBaseName = path . basename ( refFileName , '.mdc' ) ;
284- candidatePaths . push ( path . join ( skillsDir , refBaseName , refFileName ) ) ;
409+ candidatePaths . push (
410+ path . join ( skillsDir , refBaseName , refFileName ) ,
411+ ) ;
285412 }
286413
287414 let referencedContent : string | null = null ;
@@ -444,6 +571,7 @@ export async function discoverSkills(
444571export async function copySkillsToAgent (
445572 sourceSkillsDir : string ,
446573 targetSkillsDir : string ,
574+ projectRoot : string ,
447575 verbose : boolean ,
448576 dryRun : boolean ,
449577) : Promise < { copied : number ; warnings : string [ ] } > {
@@ -481,7 +609,12 @@ export async function copySkillsToAgent(
481609 const targetSkillPath = path . join ( targetSkillsDir , relativeSkillPath ) ;
482610
483611 if ( ! dryRun ) {
484- await copySkillsDirectory ( skillPath , targetSkillPath ) ;
612+ await copySkillDirectoryForNonClaudeAgents (
613+ skillPath ,
614+ targetSkillPath ,
615+ projectRoot ,
616+ skillPath ,
617+ ) ;
485618 }
486619
487620 logVerboseInfo (
@@ -619,6 +752,7 @@ export async function propagateSkills(
619752 const result = await copySkillsToAgent (
620753 skillsDir ,
621754 targetPath ,
755+ projectRoot ,
622756 verbose ,
623757 dryRun ,
624758 ) ;
0 commit comments