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;
+}