mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
fix: Handle colons in skill description frontmatter (#16345)
Co-authored-by: gemini-code-assist[bot] <gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -100,4 +100,98 @@ describe('skillLoader', () => {
|
||||
expect(skills).toEqual([]);
|
||||
expect(coreEvents.emitFeedback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should parse skill with colon in description (issue #16323)', async () => {
|
||||
const skillDir = path.join(testRootDir, 'colon-skill');
|
||||
await fs.mkdir(skillDir, { recursive: true });
|
||||
const skillFile = path.join(skillDir, 'SKILL.md');
|
||||
await fs.writeFile(
|
||||
skillFile,
|
||||
`---
|
||||
name: foo
|
||||
description: Simple story generation assistant for fiction writing. Use for creating characters, scenes, storylines, and prose. Trigger words: character, scene, storyline, story, prose, fiction, writing.
|
||||
---
|
||||
# Instructions
|
||||
Do something.
|
||||
`,
|
||||
);
|
||||
|
||||
const skills = await loadSkillsFromDir(testRootDir);
|
||||
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].name).toBe('foo');
|
||||
expect(skills[0].description).toContain('Trigger words:');
|
||||
});
|
||||
|
||||
it('should parse skill with multiple colons in description', async () => {
|
||||
const skillDir = path.join(testRootDir, 'multi-colon-skill');
|
||||
await fs.mkdir(skillDir, { recursive: true });
|
||||
const skillFile = path.join(skillDir, 'SKILL.md');
|
||||
await fs.writeFile(
|
||||
skillFile,
|
||||
`---
|
||||
name: multi-colon
|
||||
description: Use this for tasks like: coding, reviewing, testing. Keywords: async, await, promise.
|
||||
---
|
||||
# Instructions
|
||||
Do something.
|
||||
`,
|
||||
);
|
||||
|
||||
const skills = await loadSkillsFromDir(testRootDir);
|
||||
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].name).toBe('multi-colon');
|
||||
expect(skills[0].description).toContain('tasks like:');
|
||||
expect(skills[0].description).toContain('Keywords:');
|
||||
});
|
||||
|
||||
it('should parse skill with quoted YAML description (backward compatibility)', async () => {
|
||||
const skillDir = path.join(testRootDir, 'quoted-skill');
|
||||
await fs.mkdir(skillDir, { recursive: true });
|
||||
const skillFile = path.join(skillDir, 'SKILL.md');
|
||||
await fs.writeFile(
|
||||
skillFile,
|
||||
`---
|
||||
name: quoted-skill
|
||||
description: "A skill with colons: like this one: and another."
|
||||
---
|
||||
# Instructions
|
||||
Do something.
|
||||
`,
|
||||
);
|
||||
|
||||
const skills = await loadSkillsFromDir(testRootDir);
|
||||
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].name).toBe('quoted-skill');
|
||||
expect(skills[0].description).toBe(
|
||||
'A skill with colons: like this one: and another.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse skill with multi-line YAML description', async () => {
|
||||
const skillDir = path.join(testRootDir, 'multiline-skill');
|
||||
await fs.mkdir(skillDir, { recursive: true });
|
||||
const skillFile = path.join(skillDir, 'SKILL.md');
|
||||
await fs.writeFile(
|
||||
skillFile,
|
||||
`---
|
||||
name: multiline-skill
|
||||
description:
|
||||
Expertise in reviewing code for style, security, and performance. Use when the
|
||||
user asks for "feedback," a "review," or to "check" their changes.
|
||||
---
|
||||
# Instructions
|
||||
Do something.
|
||||
`,
|
||||
);
|
||||
|
||||
const skills = await loadSkillsFromDir(testRootDir);
|
||||
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].name).toBe('multiline-skill');
|
||||
expect(skills[0].description).toContain('Expertise in reviewing code');
|
||||
expect(skills[0].description).toContain('check');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,74 @@ export interface SkillDefinition {
|
||||
export const FRONTMATTER_REGEX =
|
||||
/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?/;
|
||||
|
||||
/**
|
||||
* Parses frontmatter content using YAML with a fallback to simple key-value parsing.
|
||||
* This handles cases where description contains colons that would break YAML parsing.
|
||||
*/
|
||||
function parseFrontmatter(
|
||||
content: string,
|
||||
): { name: string; description: string } | null {
|
||||
try {
|
||||
const parsed = yaml.load(content);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
const { name, description } = parsed as Record<string, unknown>;
|
||||
if (typeof name === 'string' && typeof description === 'string') {
|
||||
return { name, description };
|
||||
}
|
||||
}
|
||||
} catch (yamlError) {
|
||||
debugLogger.debug(
|
||||
'YAML frontmatter parsing failed, falling back to simple parser:',
|
||||
yamlError,
|
||||
);
|
||||
}
|
||||
|
||||
return parseSimpleFrontmatter(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple frontmatter parser that extracts name and description fields.
|
||||
* Handles cases where values contain colons that would break YAML parsing.
|
||||
*/
|
||||
function parseSimpleFrontmatter(
|
||||
content: string,
|
||||
): { name: string; description: string } | null {
|
||||
const lines = content.split(/\r?\n/);
|
||||
let name: string | undefined;
|
||||
let description: string | undefined;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line.startsWith('name:')) {
|
||||
name = line.substring(5).trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('description:')) {
|
||||
const descLines = [line.substring(12).trim()];
|
||||
|
||||
while (i + 1 < lines.length) {
|
||||
const nextLine = lines[i + 1];
|
||||
if (nextLine.match(/^[ \t]+\S/)) {
|
||||
descLines.push(nextLine.trim());
|
||||
i++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
description = descLines.filter(Boolean).join(' ');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (name !== undefined && description !== undefined) {
|
||||
return { name, description };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers and loads all skills in the provided directory.
|
||||
*/
|
||||
@@ -92,19 +160,14 @@ export async function loadSkillFromFile(
|
||||
return null;
|
||||
}
|
||||
|
||||
const frontmatter = yaml.load(match[1]);
|
||||
if (!frontmatter || typeof frontmatter !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name, description } = frontmatter as Record<string, unknown>;
|
||||
if (typeof name !== 'string' || typeof description !== 'string') {
|
||||
const frontmatter = parseFrontmatter(match[1]);
|
||||
if (!frontmatter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
name: frontmatter.name,
|
||||
description: frontmatter.description,
|
||||
location: filePath,
|
||||
body: match[2].trim(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user