From 531062aeaf406ecac706a3793bdf6e8cd880083d Mon Sep 17 00:00:00 2001 From: liqoingyu Date: Sun, 18 Jan 2026 17:11:30 +0800 Subject: [PATCH] fix(core): parse skills frontmatter with CRLF/BOM --- .../core/src/skills/skill-manager.test.ts | 56 +++++++++++++++++++ packages/core/src/skills/skill-manager.ts | 16 +++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/core/src/skills/skill-manager.test.ts b/packages/core/src/skills/skill-manager.test.ts index 076816f86..3e5125a4d 100644 --- a/packages/core/src/skills/skill-manager.test.ts +++ b/packages/core/src/skills/skill-manager.test.ts @@ -112,6 +112,62 @@ You are a helpful assistant with this skill. expect(config.filePath).toBe(validSkillConfig.filePath); }); + it('should parse markdown with CRLF line endings', () => { + const markdownCrlf = `---\r +name: test-skill\r +description: A test skill\r +---\r +\r +You are a helpful assistant with this skill.\r +`; + + const config = manager.parseSkillContent( + markdownCrlf, + validSkillConfig.filePath, + 'project', + ); + + expect(config.name).toBe('test-skill'); + expect(config.description).toBe('A test skill'); + expect(config.body).toBe('You are a helpful assistant with this skill.'); + }); + + it('should parse markdown with UTF-8 BOM', () => { + const markdownWithBom = `\uFEFF--- +name: test-skill +description: A test skill +--- + +You are a helpful assistant with this skill. +`; + + const config = manager.parseSkillContent( + markdownWithBom, + validSkillConfig.filePath, + 'project', + ); + + expect(config.name).toBe('test-skill'); + expect(config.description).toBe('A test skill'); + }); + + it('should parse markdown when body is empty and file ends after frontmatter', () => { + const frontmatterOnly = `--- +name: test-skill +description: A test skill +---`; + + const config = manager.parseSkillContent( + frontmatterOnly, + validSkillConfig.filePath, + 'project', + ); + + expect(config.name).toBe('test-skill'); + expect(config.description).toBe('A test skill'); + expect(config.body).toBe(''); + }); + it('should parse content with allowedTools', () => { const markdownWithTools = `--- name: test-skill diff --git a/packages/core/src/skills/skill-manager.ts b/packages/core/src/skills/skill-manager.ts index 84328a362..6509b712d 100644 --- a/packages/core/src/skills/skill-manager.ts +++ b/packages/core/src/skills/skill-manager.ts @@ -307,9 +307,11 @@ export class SkillManager { level: SkillLevel, ): SkillConfig { try { + const normalizedContent = normalizeSkillFileContent(content); + // Split frontmatter and content - const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/; - const match = content.match(frontmatterRegex); + const frontmatterRegex = /^---\n([\s\S]*?)\n---(?:\n|$)([\s\S]*)$/; + const match = normalizedContent.match(frontmatterRegex); if (!match) { throw new Error('Invalid format: missing YAML frontmatter'); @@ -556,3 +558,13 @@ export class SkillManager { } } } + +function normalizeSkillFileContent(content: string): string { + // Strip UTF-8 BOM to ensure frontmatter starts at the first character. + let normalized = content.replace(/^\uFEFF/, ''); + + // Normalize line endings so skills authored on Windows (CRLF) parse correctly. + normalized = normalized.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + return normalized; +}