From 64e62b6d82ed7098e5b0f1cc45d520dfe6ea3a04 Mon Sep 17 00:00:00 2001 From: Agando Date: Fri, 23 Oct 2026 21:20:19 +0200 Subject: fix(newos4): skill-create stops mangling what the agent wrote (v4.6.82) Two defects in one parser, both silent: - content = args.slice(2).join(' ') destroyed every newline, so markdown arrived as one line - the last argument was treated as tags whenever it contained a comma, and stayed in the body: a multi-line skill became a single line with ~13 bogus tags, reported as success Bodies can now be read from a VFS path (the only way markdown survives an argv splitter), tags are opt-in via --tags=, category via --category=. skill-patch takes a path too. Verified: body from file -> 6 newlines preserved, tags [alpha,beta], category testing; inline body containing four commas -> tags [], body intact. --- modules/ext/skill-pack-core.js | 51 +++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 15 deletions(-) (limited to 'modules/ext/skill-pack-core.js') diff --git a/modules/ext/skill-pack-core.js b/modules/ext/skill-pack-core.js index 5ff57f8..67adf80 100644 --- a/modules/ext/skill-pack-core.js +++ b/modules/ext/skill-pack-core.js @@ -29,7 +29,7 @@ class SkillPackCoreModule { 'skill-pack-core': 'Seed and manage built-in core skills (navigation, memory, search, modules, backup, agents, webdav, security, etc.)', 'skill-list': 'skill-list [category] — List all skills, optionally filtered by category', 'skill-view': 'skill-view — View a skill\'s full content', - 'skill-create': 'skill-create "" [tags] — Create a new skill', + 'skill-create': 'skill-create [--tags=a,b] [--category=x] — Create a new skill', 'skill-patch': 'skill-patch "" — Replace a skill\'s full content', 'skill-delete': 'skill-delete — Delete a skill', 'skill-export': 'skill-export — Export a skill as JSON', @@ -713,20 +713,35 @@ after the behaviour itself was fixed in v4.6.58.`), } _cmdSkillCreate(args) { - if (args.length < 3) { - this.terminal.print('Usage: skill-create "" [tags]', 'warning'); + // v4.6.82: the old parser body = args.slice(2).join(' ') collapsed every newline into a + // space, and any body containing a comma had its last word eaten as "tags" — so a + // multi-line skill arrived as one line with 13 bogus tags while reporting success. + // Flags are now explicit, and a path as the third argument reads the body from a file, + // which is the only way to keep markdown intact through an argv splitter. + const flags = { tags: [], category: 'general' }; + const positional = []; + for (const a of args) { + if (a.startsWith('--tags=')) flags.tags.push(...a.slice(7).split(',').map(t => t.trim()).filter(Boolean)); + else if (a.startsWith('--category=')) flags.category = a.slice(11).trim() || 'general'; + else positional.push(a); + } + if (positional.length < 3) { + this.terminal.print('Usage: skill-create [--tags=a,b] [--category=x]', 'warning'); return; } - const name = args[0].replace(/[^a-z0-9-]/g, '').toLowerCase(); + const name = positional[0].replace(/[^a-z0-9-]/g, '').toLowerCase(); if (!name) { this.terminal.print('Name must be kebab-case (letters, numbers, hyphens only)', 'error'); return; } - const desc = args[1]; - const content = args.slice(2).join(' '); - const tags = []; - // Parse tags from the last argument if it looks like a comma-separated list - const lastArg = args[args.length - 1]; - if (lastArg && !lastArg.startsWith('"') && lastArg.includes(',')) { - tags.push(...lastArg.split(',').map(t => t.trim()).filter(Boolean)); + const desc = positional[1]; + const bodyArg = positional.slice(2).join(' '); + const fs0 = this.terminal.fs; + let content = bodyArg; + if (bodyArg.startsWith('/')) { + try { + const fromFile = fs0.readFile(bodyArg); + if (typeof fromFile === 'string' && fromFile.length) content = fromFile; + } catch (e) { /* not a file: treat the argument as literal text */ } } + const tags = flags.tags; const fs = this.terminal.fs; // Check for duplicate const index = fs.fsReadJSON('/skills/index.json') || {}; @@ -735,20 +750,26 @@ after the behaviour itself was fixed in v4.6.58.`), return; } const now = new Date().toISOString(); - const skillContent = `---\nname: ${name}\ndescription: ${desc}\ntags: [${tags.join(', ')}]\ncategory: general\n---\n${content}`; + const skillContent = `---\nname: ${name}\ndescription: ${desc}\ntags: [${tags.join(', ')}]\ncategory: ${flags.category}\n---\n${content}`; fs.writeFile(`/skills/${name}.md`, skillContent); - index[name] = { name, description: desc, tags, category: 'general', created: now, updated: now }; + index[name] = { name, description: desc, tags, category: flags.category, created: now, updated: now }; fs.fsWriteJSON('/skills/index.json', index); this.terminal.print(`✅ Skill "${name}" created (${(skillContent.length / 1024).toFixed(1)}KB)`, 'success'); } _cmdSkillPatch(args) { if (args.length < 2) { - this.terminal.print('Usage: skill-patch ""', 'warning'); + this.terminal.print('Usage: skill-patch (a VFS path keeps newlines)', 'warning'); return; } const name = args[0].replace(/\.md$/, ''); - const content = args.slice(1).join(' '); + let content = args.slice(1).join(' '); + if (content.startsWith('/')) { + try { + const fromFile = this.terminal.fs.readFile(content); + if (typeof fromFile === 'string' && fromFile.length) content = fromFile; + } catch (e) { /* literal text */ } + } const fs = this.terminal.fs; const index = fs.fsReadJSON('/skills/index.json') || {}; if (!index[name]) { -- cgit v1.2.3