Skip to content
Open
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
27 changes: 24 additions & 3 deletions cli/src/sandbox-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,27 @@ export interface SkillFrontmatter {
requiredCapabilities?: string[];
}

function stripSkillInlineComment(value: string): string {
for (let i = 0; i < value.length; i++) {
const ch = value[i];
const prev = value[i - 1];
const afterSpace = i === 0 || prev === " " || prev === "\t";
if ((ch === '"' || ch === "'") && (afterSpace || prev === "[" || prev === ",")) {
let close = i + 1;
while (close < value.length) {
if (ch === '"' && value[close] === "\\") close += 2;
else if (value[close] !== ch) close++;
else if (ch === "'" && value[close + 1] === "'") close += 2;
else break;
}
if (close < value.length) i = close;
} else if (ch === "#" && afterSpace) {
return value.slice(0, i).trimEnd();
}
}
return value;
}

function stripSkillQuotes(value: string): string {
const trimmed = value.trim();
if (
Expand Down Expand Up @@ -47,7 +68,7 @@ export function parseSkillFrontmatter(md: string, sourcePath: string): SkillFron
const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
if (!kv) continue;
const key = kv[1]!;
const rest = kv[2] ?? "";
const rest = stripSkillInlineComment(kv[2] ?? "");
if (rest === ">" || rest === "|" || rest === ">-" || rest === "|-") {
const literal = rest[0] === "|";
const baseIndent = skillIndent(line);
Expand Down Expand Up @@ -75,10 +96,10 @@ export function parseSkillFrontmatter(md: string, sourcePath: string): SkillFron
if (!next.trim() || skillIndent(next) <= baseIndent) break;
const item = /^\s*-\s+(.*)$/.exec(next);
if (!item) break;
items.push(stripSkillQuotes(item[1]!));
items.push(stripSkillQuotes(stripSkillInlineComment(item[1]!)));
i++;
}
fields[key] = items;
fields[key] = items.filter(Boolean);
}

const name = fields["name"];
Expand Down
26 changes: 26 additions & 0 deletions cli/test/tool-descriptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,21 @@ test("parseSkillFrontmatter requires name + description + body; requiredCapabili
"SKILL.md",
);
assert.deepEqual(caps.requiredCapabilities, ["a", "b"]);
const commented = parseSkillFrontmatter(
'---\nname: demo # note\ndescription: "a # inside quotes" # note\ntag: c#sharp\nrequiredCapabilities: [a, b] # why\n---\nbody',
"SKILL.md",
);
assert.equal(commented.name, "demo");
assert.equal(commented.description, "a # inside quotes");
assert.deepEqual(commented.requiredCapabilities, ["a", "b"]);
assert.throws(
() => parseSkillFrontmatter("---\nname: # note\ndescription: y\n---\nbody", "SKILL.md"),
/missing "name"/,
);
const long = `a${" ".repeat(200_000)}${'\\"'.repeat(100_000)}b`;
const started = performance.now();
assert.equal(parseSkillFrontmatter(`---\nname: x\ndescription: "${long}"\n---\nbody`, "SKILL.md").description, long);
assert.ok(performance.now() - started < 2_000, "parseSkillFrontmatter must not backtrack on long scalars");
assert.throws(() => parseSkillFrontmatter("no frontmatter", "SKILL.md"), /missing YAML frontmatter/);
assert.throws(() => parseSkillFrontmatter("---\ndescription: y\n---\nbody", "SKILL.md"), /missing "name"/);
assert.throws(() => parseSkillFrontmatter("---\nname: x\n---\nbody", "SKILL.md"), /missing "description"/);
Expand All @@ -278,6 +293,17 @@ test("drift-lock: cli skill parser matches the core seed parser's output", () =>
"---\nname: a\ndescription: b\n---\nbody\n",
"\uFEFF---\r\nname: a\r\ndescription: b\r\n---\r\nbody\r\n",
"---\nname: a\ndescription: b\nrequiredCapabilities: [one, 'two']\n---\nbody\n",
"---\nname: a # inline note\ndescription: b # inline note\n---\nbody\n",
"---\nname: a\ndescription: b\nrequiredCapabilities: [one, two] # why\n---\nbody\n",
"---\nname: a\ndescription: > # note\n folded\n---\nbody\n",
"---\nname: a\ndescription: b\nrequiredCapabilities:\n - one # why\n---\nbody\n",
"---\nname: # note\ndescription: b\n---\nbody\n",
"---\nname: a\ndescription: 'b # c'\n---\nbody\n",
"---\nname: a\ndescription: b#c\n---\nbody\n",
'---\nname: a\ndescription: "He said \\"go\\" # really"\n---\nbody\n',
"---\nname: a\ndescription: 'it''s # fine'\n---\nbody\n",
"---\nname: a\ndescription: use 'em all # note\n---\nbody\n",
"---\nname: a\ndescription: b\nrequiredCapabilities:\n - one # why\n - # dropped\n - two\n---\nbody\n",
"---\nname: a\ndescription: b\nrequiredCapabilities:\n - one\n - two\n---\nbody\n",
"---\nname: a\ndescription: >\n folded description\n---\nbody\n",
"---\nname: a\ndescription: b\nrequiredCapabilities: one, two\n---\nbody\n",
Expand Down
27 changes: 24 additions & 3 deletions src/skills/frontmatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ export interface Frontmatter {
body: string;
}

function stripInlineComment(value: string): string {
for (let i = 0; i < value.length; i++) {
const ch = value[i];
const prev = value[i - 1];
const afterSpace = i === 0 || prev === " " || prev === "\t";
if ((ch === '"' || ch === "'") && (afterSpace || prev === "[" || prev === ",")) {
let close = i + 1;
while (close < value.length) {
if (ch === '"' && value[close] === "\\") close += 2;
else if (value[close] !== ch) close++;
else if (ch === "'" && value[close + 1] === "'") close += 2;
else break;
}
if (close < value.length) i = close;
} else if (ch === "#" && afterSpace) {
return value.slice(0, i).trimEnd();
}
}
return value;
}

function stripQuotes(value: string): string {
const t = value.trim();
if (t.length >= 2 && ((t[0] === '"' && t.at(-1) === '"') || (t[0] === "'" && t.at(-1) === "'"))) {
Expand Down Expand Up @@ -44,7 +65,7 @@ export function parseFrontmatter(raw: string): Frontmatter {
const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
if (!m) continue;
const key = m[1]!;
const rest = m[2] ?? "";
const rest = stripInlineComment(m[2] ?? "");

if (rest === ">" || rest === "|" || rest === ">-" || rest === "|-") {
const literal = rest[0] === "|";
Expand Down Expand Up @@ -74,10 +95,10 @@ export function parseFrontmatter(raw: string): Frontmatter {
if (indentOf(next) <= baseIndent) break;
const item = /^\s*-\s+(.*)$/.exec(next);
if (!item) break;
items.push(stripQuotes(item[1]!));
items.push(stripQuotes(stripInlineComment(item[1]!)));
i++;
}
attrs[key] = items;
attrs[key] = items.filter((s) => s.length > 0);
}

return { attrs, body: source.slice(end + closing[0].length).replace(/^\s+/, "") };
Expand Down
54 changes: 54 additions & 0 deletions test/frontmatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,57 @@ test("seed skill names cannot escape or alias their materialized directory", ()
}
assert.equal(parseSeedSkillFrontmatter(skill("foo-bar_v1.2")).name, "foo-bar_v1.2");
});

test("drops unquoted inline comments but keeps # inside quotes or words", () => {
const { attrs } = parseFrontmatter(`---
name: demo # human-readable note
description: "a # inside quotes" # trailing note
tag: c#sharp
joined: foo# bar
caps: [one, two] # why
folded: > # note
folded body
empty: # nothing left
list:
- command:demo # why
---
body`);
assert.equal(attrs.name, "demo");
assert.equal(attrs.description, "a # inside quotes");
assert.equal(attrs.tag, "c#sharp");
assert.equal(attrs.joined, "foo# bar");
assert.deepEqual(attrs.caps, ["one", "two"]);
assert.equal(attrs.folded, "folded body");
assert.deepEqual(attrs.empty, []);
assert.deepEqual(attrs.list, ["command:demo"]);
});

test("a commented-out value is missing, not a literal comment", () => {
assert.throws(() => parseSeedSkillFrontmatter("---\nname: # note\ndescription: d\n---\nbody"), /requires name/);
assert.equal(parseSeedSkillFrontmatter("---\nname: demo # note\ndescription: d # note\n---\nbody").name, "demo");
});

test("a # inside quotes (with escapes, doubling, or an unclosed quote) is not a comment", () => {
const { attrs } = parseFrontmatter(`---
escaped: "He said \\"go\\" # really"
doubled: 'it''s # fine'
unclosed: use 'em all # note
caps:
- one # why
- # dropped
- two
---
body`);
assert.equal(attrs.escaped, 'He said \\"go\\" # really');
assert.equal(attrs.doubled, "it''s # fine");
assert.equal(attrs.unclosed, "use 'em all");
assert.deepEqual(attrs.caps, ["one", "two"]);
});

test("inline comment stripping stays linear on long scalars", () => {
const value = `a${" ".repeat(200_000)}${'\\"'.repeat(100_000)}b`;
const started = performance.now();
const { attrs } = parseFrontmatter(`---\nname: demo\ndescription: "${value}"\n---\nbody`);
assert.ok(performance.now() - started < 2_000, "parseFrontmatter must not backtrack on long scalars");
assert.equal(attrs.description, value);
});
Loading