mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 12:35:35 +02:00
The Go SKILL.md frontmatter parser unmarshalled into a {Name,Description}
string struct, so a non-scalar value (a list/map written where a scalar
belongs) made the whole decode fail and dropped even a valid sibling
`name`. The TS parser instead kept the name and JSON-encoded the value,
so the file-viewer (TS) and the import path (Go) could disagree about
the same SKILL.md.
Decode into a generic map and coerce per key on the Go side, mirroring
the TS coercion (scalars -> literal form, sequences/mappings -> JSON), so
both sides produce identical results and a structured value never
discards a sibling key. Rename ParseFrontmatter -> ParseSkillFrontmatter
to remove the cross-language name clash with the TS parseFrontmatter
(which returns {frontmatter, body}), and drop the unused TS
parseSkillFrontmatter export.
Add parity tests for sequence/mapping values plus name-only,
description-only, leading-blank-line and triple-dash-in-body edge cases
on both sides.
Follow-up to #3543 / MUL-2842.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
50 lines
1.4 KiB
TypeScript
50 lines
1.4 KiB
TypeScript
import { parse as parseYaml } from "yaml";
|
|
|
|
// Keeping the trailing newline inside the capture group matters: yaml's `|`
|
|
// clip chomping only preserves a final newline when the input itself contains
|
|
// one. Mirrors the regex used by the Go side in `server/internal/skill`.
|
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?\r?\n)---\r?\n?/;
|
|
|
|
export type SkillFrontmatter = Record<string, string>;
|
|
|
|
export interface ParsedFrontmatter {
|
|
frontmatter: SkillFrontmatter | null;
|
|
body: string;
|
|
}
|
|
|
|
export function parseFrontmatter(raw: string): ParsedFrontmatter {
|
|
const match = FRONTMATTER_RE.exec(raw);
|
|
if (!match) return { frontmatter: null, body: raw };
|
|
|
|
const yamlBlock = match[1]!;
|
|
const body = raw.slice(match[0].length);
|
|
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = parseYaml(yamlBlock);
|
|
} catch {
|
|
return { frontmatter: null, body };
|
|
}
|
|
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
return { frontmatter: null, body };
|
|
}
|
|
|
|
const result: SkillFrontmatter = {};
|
|
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
if (value == null) continue;
|
|
if (typeof value === "string") {
|
|
result[key] = value;
|
|
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
result[key] = String(value);
|
|
} else {
|
|
result[key] = JSON.stringify(value);
|
|
}
|
|
}
|
|
|
|
return {
|
|
frontmatter: Object.keys(result).length > 0 ? result : null,
|
|
body,
|
|
};
|
|
}
|