diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 35fbca6805..6b2a55ac62 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -224,7 +224,11 @@ export type { SkillSource, UploadableSkillSource, } from "./skills"; -export { SKILL_EXISTS_MARKER, stripFrontmatter } from "./skills"; +export { + SKILL_EXISTS_MARKER, + serializeSkillMarkdown, + stripFrontmatter, +} from "./skills"; export type { ArtifactType, PostHogAPIConfig, diff --git a/packages/shared/src/skills.ts b/packages/shared/src/skills.ts index 540929b7be..540b92eba5 100644 --- a/packages/shared/src/skills.ts +++ b/packages/shared/src/skills.ts @@ -33,6 +33,46 @@ export interface ExportedSkill { files: ExportedSkillFile[]; } +/** + * Serializes a SKILL.md file from frontmatter metadata plus a markdown body. + * + * The output must round-trip through `parseSkillFrontmatter` and also be valid + * YAML for the agents that consume these files, so scalars fall back from plain + * → double-quoted → literal block as they get more hostile. Lives here (shared) + * so both the workspace-server bundler and the web-host bundler produce the + * exact same SKILL.md — this is a serialization contract consumed by the cloud + * sandbox, so it must not drift between hosts. + */ +export function serializeSkillMarkdown( + meta: { name: string; description: string }, + body: string, +): string { + const frontmatter = [ + "---", + `name: ${serializeSkillScalar(meta.name)}`, + `description: ${serializeSkillScalar(meta.description)}`, + "---", + ].join("\n"); + + const trimmedBody = body.replace(/^\n+/, ""); + return `${frontmatter}\n\n${trimmedBody.trimEnd()}\n`; +} + +const SKILL_PLAIN_SAFE = /^[A-Za-z0-9][A-Za-z0-9 _.,;()/-]*$/; + +function serializeSkillScalar(value: string): string { + if (value === "") return '""'; + if (!value.includes("\n")) { + if (SKILL_PLAIN_SAFE.test(value) && !value.endsWith(" ")) return value; + if (!value.includes('"') && !value.includes("\\")) return `"${value}"`; + } + // Literal block: survives quotes, backslashes, and newlines. + const lines = value + .split("\n") + .map((line) => (line.trim() ? ` ${line}` : "")); + return `|-\n${lines.join("\n")}`; +} + /** * Server "skill already exists" messages must include this marker verbatim; * the UI keys its overwrite-confirmation flow on it. diff --git a/packages/workspace-server/src/services/skills/write-skill-frontmatter.ts b/packages/workspace-server/src/services/skills/write-skill-frontmatter.ts index 0bc24979b1..b597453ada 100644 --- a/packages/workspace-server/src/services/skills/write-skill-frontmatter.ts +++ b/packages/workspace-server/src/services/skills/write-skill-frontmatter.ts @@ -1,36 +1,4 @@ -/** - * Serializes a SKILL.md file from frontmatter metadata plus a markdown body. - * - * The output must round-trip through `parseSkillFrontmatter` and also be - * valid YAML for the agents that consume these files, so scalars fall back - * from plain → double-quoted → literal block as they get more hostile. - */ -export function serializeSkillMarkdown( - meta: { name: string; description: string }, - body: string, -): string { - const frontmatter = [ - "---", - `name: ${serializeScalar(meta.name)}`, - `description: ${serializeScalar(meta.description)}`, - "---", - ].join("\n"); - - const trimmedBody = body.replace(/^\n+/, ""); - return `${frontmatter}\n\n${trimmedBody.trimEnd()}\n`; -} - -const PLAIN_SAFE = /^[A-Za-z0-9][A-Za-z0-9 _.,;()/-]*$/; - -function serializeScalar(value: string): string { - if (value === "") return '""'; - if (!value.includes("\n")) { - if (PLAIN_SAFE.test(value) && !value.endsWith(" ")) return value; - if (!value.includes('"') && !value.includes("\\")) return `"${value}"`; - } - // Literal block: survives quotes, backslashes, and newlines. - const lines = value - .split("\n") - .map((line) => (line.trim() ? ` ${line}` : "")); - return `|-\n${lines.join("\n")}`; -} +// The SKILL.md serializer lives in @posthog/shared so the workspace-server +// bundler and the web-host bundler emit byte-identical SKILL.md files — this is +// a serialization contract the cloud sandbox consumes and must not drift. +export { serializeSkillMarkdown } from "@posthog/shared";