From 994ec72d917ff3cf7c3d2a4df39b8b2a66626849 Mon Sep 17 00:00:00 2001 From: harshbansal7 Date: Wed, 18 Feb 2026 16:55:20 +0530 Subject: [PATCH 1/4] Fix parsing of SKILL.md file frontmatter - regex --- pkg/skills/loader.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/skills/loader.go b/pkg/skills/loader.go index 0c63ae067..15e82c31b 100644 --- a/pkg/skills/loader.go +++ b/pkg/skills/loader.go @@ -9,6 +9,8 @@ import ( "path/filepath" "regexp" "strings" + + "github.com/sipeed/picoclaw/pkg/logger" ) var namePattern = regexp.MustCompile(`^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$`) @@ -251,6 +253,11 @@ func (sl *SkillsLoader) BuildSkillsSummary() string { func (sl *SkillsLoader) getSkillMetadata(skillPath string) *SkillMetadata { content, err := os.ReadFile(skillPath) if err != nil { + logger.WarnCF("skills", "Failed to read skill metadata", + map[string]interface{}{ + "skill_path": skillPath, + "error": err.Error(), + }) return nil } @@ -306,9 +313,9 @@ func (sl *SkillsLoader) parseSimpleYAML(content string) map[string]string { } func (sl *SkillsLoader) extractFrontmatter(content string) string { - // (?s) enables DOTALL mode so . matches newlines - // Match first ---, capture everything until next --- on its own line - re := regexp.MustCompile(`(?s)^---\n(.*)\n---`) + // Support both Unix (\n) and Windows (\r\n) line endings for frontmatter blocks + // (?s) enables DOTALL so . matches newlines; ^--- at start, then ... --- at start of line + re := regexp.MustCompile(`(?s)^---\r?\n(.*?)\r?\n---`) match := re.FindStringSubmatch(content) if len(match) > 1 { return match[1] From 02b5811b95abf1b6102f1cbce2afc9cace1f3cb4 Mon Sep 17 00:00:00 2001 From: harshbansal7 Date: Wed, 18 Feb 2026 16:58:27 +0530 Subject: [PATCH 2/4] add support for \r as well --- pkg/skills/loader.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/skills/loader.go b/pkg/skills/loader.go index 15e82c31b..c9731b6ae 100644 --- a/pkg/skills/loader.go +++ b/pkg/skills/loader.go @@ -313,9 +313,10 @@ func (sl *SkillsLoader) parseSimpleYAML(content string) map[string]string { } func (sl *SkillsLoader) extractFrontmatter(content string) string { - // Support both Unix (\n) and Windows (\r\n) line endings for frontmatter blocks - // (?s) enables DOTALL so . matches newlines; ^--- at start, then ... --- at start of line - re := regexp.MustCompile(`(?s)^---\r?\n(.*?)\r?\n---`) + // Support \n (Unix), \r\n (Windows), and \r (classic Mac) line endings for frontmatter blocks + // (?s) enables DOTALL so . matches newlines; + // ^--- at start, then ... --- at start of line, honoring all three line ending types + re := regexp.MustCompile(`(?s)^---(?:\r\n|\n|\r)(.*?)(?:\r\n|\n|\r)---`) match := re.FindStringSubmatch(content) if len(match) > 1 { return match[1] From 287100f3030b337ac5f24d7577b0e28e26357616 Mon Sep 17 00:00:00 2001 From: harshbansal7 Date: Wed, 18 Feb 2026 23:13:47 +0530 Subject: [PATCH 3/4] Comments resolved --- pkg/skills/loader.go | 13 ++++- pkg/skills/loader_test.go | 102 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/pkg/skills/loader.go b/pkg/skills/loader.go index c9731b6ae..bb0abbdcc 100644 --- a/pkg/skills/loader.go +++ b/pkg/skills/loader.go @@ -290,10 +290,15 @@ func (sl *SkillsLoader) getSkillMetadata(skillPath string) *SkillMetadata { // parseSimpleYAML parses simple key: value YAML format // Example: name: github\n description: "..." +// Normalizes line endings to handle \n (Unix), \r\n (Windows), and \r (classic Mac) func (sl *SkillsLoader) parseSimpleYAML(content string) map[string]string { result := make(map[string]string) - for _, line := range strings.Split(content, "\n") { + // Normalize line endings: convert \r\n and \r to \n + normalized := strings.ReplaceAll(content, "\r\n", "\n") + normalized = strings.ReplaceAll(normalized, "\r", "\n") + + for _, line := range strings.Split(normalized, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue @@ -325,7 +330,11 @@ func (sl *SkillsLoader) extractFrontmatter(content string) string { } func (sl *SkillsLoader) stripFrontmatter(content string) string { - re := regexp.MustCompile(`^---\n.*?\n---\n`) + // Support \n (Unix), \r\n (Windows), and \r (classic Mac) line endings for frontmatter blocks + // (?s) enables DOTALL so . matches newlines; + // ^--- at start, then ... --- at start of line, honoring all three line ending types + // Match zero or more trailing line endings after closing --- (handles both with and without blank lines) + re := regexp.MustCompile(`(?s)^---(?:\r\n|\n|\r)(.*?)(?:\r\n|\n|\r)---(?:\r\n|\n|\r)*`) return re.ReplaceAllString(content, "") } diff --git a/pkg/skills/loader_test.go b/pkg/skills/loader_test.go index e0e7109cf..539d24646 100644 --- a/pkg/skills/loader_test.go +++ b/pkg/skills/loader_test.go @@ -75,3 +75,105 @@ func TestSkillsInfoValidate(t *testing.T) { }) } } + +func TestExtractFrontmatter(t *testing.T) { + sl := &SkillsLoader{} + + testcases := []struct { + name string + content string + expectedName string + expectedDesc string + lineEndingType string + }{ + { + name: "unix-line-endings", + lineEndingType: "Unix (\\n)", + content: "---\nname: test-skill\ndescription: A test skill\n---\n\n# Skill Content", + expectedName: "test-skill", + expectedDesc: "A test skill", + }, + { + name: "windows-line-endings", + lineEndingType: "Windows (\\r\\n)", + content: "---\r\nname: test-skill\r\ndescription: A test skill\r\n---\r\n\r\n# Skill Content", + expectedName: "test-skill", + expectedDesc: "A test skill", + }, + { + name: "classic-mac-line-endings", + lineEndingType: "Classic Mac (\\r)", + content: "---\rname: test-skill\rdescription: A test skill\r---\r\r# Skill Content", + expectedName: "test-skill", + expectedDesc: "A test skill", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + // Extract frontmatter + frontmatter := sl.extractFrontmatter(tc.content) + assert.NotEmpty(t, frontmatter, "Frontmatter should be extracted for %s line endings", tc.lineEndingType) + + // Parse YAML to get name and description (parseSimpleYAML now handles all line ending types) + yamlMeta := sl.parseSimpleYAML(frontmatter) + assert.Equal(t, tc.expectedName, yamlMeta["name"], "Name should be correctly parsed from frontmatter with %s line endings", tc.lineEndingType) + assert.Equal(t, tc.expectedDesc, yamlMeta["description"], "Description should be correctly parsed from frontmatter with %s line endings", tc.lineEndingType) + }) + } +} + +func TestStripFrontmatter(t *testing.T) { + sl := &SkillsLoader{} + + testcases := []struct { + name string + content string + expectedContent string + lineEndingType string + }{ + { + name: "unix-line-endings", + lineEndingType: "Unix (\\n)", + content: "---\nname: test-skill\ndescription: A test skill\n---\n\n# Skill Content", + expectedContent: "# Skill Content", + }, + { + name: "windows-line-endings", + lineEndingType: "Windows (\\r\\n)", + content: "---\r\nname: test-skill\r\ndescription: A test skill\r\n---\r\n\r\n# Skill Content", + expectedContent: "# Skill Content", + }, + { + name: "classic-mac-line-endings", + lineEndingType: "Classic Mac (\\r)", + content: "---\rname: test-skill\rdescription: A test skill\r---\r\r# Skill Content", + expectedContent: "# Skill Content", + }, + { + name: "unix-line-endings-without-trailing-newline", + lineEndingType: "Unix (\\n) without trailing newline", + content: "---\nname: test-skill\ndescription: A test skill\n---\n# Skill Content", + expectedContent: "# Skill Content", + }, + { + name: "windows-line-endings-without-trailing-newline", + lineEndingType: "Windows (\\r\\n) without trailing newline", + content: "---\r\nname: test-skill\r\ndescription: A test skill\r\n---\r\n# Skill Content", + expectedContent: "# Skill Content", + }, + { + name: "no-frontmatter", + lineEndingType: "No frontmatter", + content: "# Skill Content\n\nSome content here.", + expectedContent: "# Skill Content\n\nSome content here.", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + result := sl.stripFrontmatter(tc.content) + assert.Equal(t, tc.expectedContent, result, "Frontmatter should be stripped correctly for %s", tc.lineEndingType) + }) + } +} From b122abd30f2305631f2dc90f4d894b169ab37451 Mon Sep 17 00:00:00 2001 From: harshbansal7 Date: Thu, 19 Feb 2026 02:28:44 +0530 Subject: [PATCH 4/4] fix --- pkg/skills/loader_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/skills/loader_test.go b/pkg/skills/loader_test.go index 539d24646..efadcdbf2 100644 --- a/pkg/skills/loader_test.go +++ b/pkg/skills/loader_test.go @@ -80,11 +80,11 @@ func TestExtractFrontmatter(t *testing.T) { sl := &SkillsLoader{} testcases := []struct { - name string - content string - expectedName string - expectedDesc string - lineEndingType string + name string + content string + expectedName string + expectedDesc string + lineEndingType string }{ { name: "unix-line-endings",