Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion apps/server/src/provider/Drivers/ClaudeSkills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,48 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => {
}),
);

it.effect("recovers colon-bearing descriptions with invocation metadata intact", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-skills-" });
const configDir = path.join(tempDir, "claude-home");

for (const [description, comment] of [
["Browser automation + AI test authoring via kane-cli: run browser objectives, ...", ""],
['Read C:\\skills\\guide#tag: continue with "quoted".', " # trailing: comment"],
] as const) {
yield* writeSkill(
path.join(configDir, "skills"),
"kane-cli",
[
"---",
"name: frontmatter-alias",
`description: ${description}${comment}`,
"allowed-tools: [Read, Write]",
"disable-model-invocation: yes",
"user-invocable: no",
"---",
].join("\n"),
);

const skills = yield* discoverClaudeSkills({ homePath: configDir }, undefined);

assert.deepEqual(skills, [
{
name: "kane-cli",
path: path.join(configDir, "skills", "kane-cli", "SKILL.md"),
enabled: true,
scope: "user",
description,
userInvocationOnly: true,
userInvocable: false,
},
]);
}
}),
);

it.effect("falls back to the directory name and skips malformed frontmatter", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand All @@ -163,7 +205,17 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => {
const skillsDir = path.join(configDir, "skills");

yield* writeSkill(skillsDir, "no-frontmatter", "# Just a heading\n");
yield* writeSkill(skillsDir, "broken-yaml", "---\nname: [unclosed\n---\n");
for (const [directoryName, field] of [
["broken-yaml", "name: [unclosed"],
["broken-tools", "allowed-tools: [Read, Write"],
["broken-quoted", 'name: "unclosed: text'],
] as const) {
yield* writeSkill(
skillsDir,
directoryName,
["---", "description: Run: browser objectives.", field, "---"].join("\n"),
);
}
// A stray file (not a directory with SKILL.md) must be skipped.
yield* fs.makeDirectory(skillsDir, { recursive: true });
yield* fs.writeFileString(path.join(skillsDir, "README.md"), "not a skill");
Expand Down
21 changes: 19 additions & 2 deletions apps/server/src/provider/Drivers/ClaudeSkills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,28 @@ function parseSkillFrontmatter(contents: string): SkillFrontmatter {
return { kind: "missing" };
}

const frontmatter = match[1] ?? "";
let parsed: unknown;
try {
parsed = parseYamlDocument(match[1] ?? "");
parsed = parseYamlDocument(frontmatter);
} catch {
return { kind: "malformed" };
// Claude Code accepts plain scalars containing `: `. Repair only those,
// leaving comments and YAML structure for the full-document parser.
const repaired = frontmatter.replace(
/^([\w-]+:[ \t]*)([^\r\n]*)/gm,
(line, prefix: string, value: string) => {
const scalar = value.split(/[ \t]+#/)[0] ?? "";
if (!/:[ \t]/.test(scalar) || /^(?:["'[\]{}|>&*!#%@`]|[-?:](?:[ \t]|$))/.test(scalar)) {
return line;
}
return `${prefix}${JSON.stringify(scalar)}${value.slice(scalar.length)}`;
},
);
try {
parsed = parseYamlDocument(repaired);
} catch {
return { kind: "malformed" };
}
}
if (typeof parsed !== "object" || parsed === null) {
return { kind: "malformed" };
Expand Down
Loading