@@ -33,6 +33,46 @@ export interface ExportedSkill {
3333 files : ExportedSkillFile [ ] ;
3434}
3535
36+ /**
37+ * Serializes a SKILL.md file from frontmatter metadata plus a markdown body.
38+ *
39+ * The output must round-trip through `parseSkillFrontmatter` and also be valid
40+ * YAML for the agents that consume these files, so scalars fall back from plain
41+ * → double-quoted → literal block as they get more hostile. Lives here (shared)
42+ * so both the workspace-server bundler and the web-host bundler produce the
43+ * exact same SKILL.md — this is a serialization contract consumed by the cloud
44+ * sandbox, so it must not drift between hosts.
45+ */
46+ export function serializeSkillMarkdown (
47+ meta : { name : string ; description : string } ,
48+ body : string ,
49+ ) : string {
50+ const frontmatter = [
51+ "---" ,
52+ `name: ${ serializeSkillScalar ( meta . name ) } ` ,
53+ `description: ${ serializeSkillScalar ( meta . description ) } ` ,
54+ "---" ,
55+ ] . join ( "\n" ) ;
56+
57+ const trimmedBody = body . replace ( / ^ \n + / , "" ) ;
58+ return `${ frontmatter } \n\n${ trimmedBody . trimEnd ( ) } \n` ;
59+ }
60+
61+ const SKILL_PLAIN_SAFE = / ^ [ A - Z a - z 0 - 9 ] [ A - Z a - z 0 - 9 _ . , ; ( ) / - ] * $ / ;
62+
63+ function serializeSkillScalar ( value : string ) : string {
64+ if ( value === "" ) return '""' ;
65+ if ( ! value . includes ( "\n" ) ) {
66+ if ( SKILL_PLAIN_SAFE . test ( value ) && ! value . endsWith ( " " ) ) return value ;
67+ if ( ! value . includes ( '"' ) && ! value . includes ( "\\" ) ) return `"${ value } "` ;
68+ }
69+ // Literal block: survives quotes, backslashes, and newlines.
70+ const lines = value
71+ . split ( "\n" )
72+ . map ( ( line ) => ( line . trim ( ) ? ` ${ line } ` : "" ) ) ;
73+ return `|-\n${ lines . join ( "\n" ) } ` ;
74+ }
75+
3676/**
3777 * Server "skill already exists" messages must include this marker verbatim;
3878 * the UI keys its overwrite-confirmation flow on it.
0 commit comments