diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-23 21:20:19 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-23 21:20:19 +0200 |
| commit | 64e62b6d82ed7098e5b0f1cc45d520dfe6ea3a04 (patch) | |
| tree | 78054f513dc11416e4921e915d75e5fa4f07f4de | |
| parent | 4b69d3666a8d842d07df2689200a364b8fdd662e (diff) | |
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.
| -rw-r--r-- | help.html | 18 | ||||
| -rw-r--r-- | main.html | 6 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/ext/skill-pack-core.js | 51 |
4 files changed, 58 insertions, 19 deletions
@@ -1689,6 +1689,24 @@ listmods</pre> <h2>📋 Changelog</h2> <div class="version-item"> + <div class="v-header"><span class="v-num">v4.6.82 — October 2026</span> + <span class="v-date">skill-create parser</span></div> + <ul> + <li><strong>A skill created from the terminal is now the skill you wrote.</strong> The old + parser joined every argument with a space — so line breaks vanished — and treated the last + argument as a tag list whenever it contained a comma, which it then also left inside the + body. A multi-line skill arrived as one line carrying a dozen bogus tags, and it still + reported success.</li> + <li><strong>Bodies can come from a file.</strong> + <code>skill-create <name> <desc> /path/to/body.md --tags=a,b --category=x</code> + keeps markdown exactly as written; inline text still works for one-liners.</li> + <li><strong>Tags are opt-in.</strong> Only <code>--tags=</code> creates tags — a comma in your + prose is no longer a taxonomy. <code>skill-patch</code> accepts a path too, for the same + reason.</li> + </ul> + </div> + + <div class="version-item"> <div class="v-header"><span class="v-num">v4.6.81 — October 2026</span> <span class="v-date">two silent failures</span></div> <ul> @@ -1349,13 +1349,13 @@ </script> <!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix --> -<script src="modules/core/core-bundle.js?v=v4.6.81"></script> +<script src="modules/core/core-bundle.js?v=v4.6.82"></script> <!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules --> -<script src="modules/core/module-loader.js?v=v4.6.81"></script> +<script src="modules/core/module-loader.js?v=v4.6.82"></script> <!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work --> -<script src="modules/ext/critical-ext-bundle.js?v=v4.6.81"></script> +<script src="modules/ext/critical-ext-bundle.js?v=v4.6.82"></script> <script> // ═══ ONE TRUE VERSION SOURCE ═══ diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 74e3a86..626d7bc 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -4,7 +4,7 @@ // Core module bundle loaded synchronously in main.html. // This loader only handles version cache-busting, splash, terminal init, and optional ext modules. - const LATEST_VER = 'v4.6.81'; + const LATEST_VER = 'v4.6.82'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { 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 <name> — View a skill\'s full content', - 'skill-create': 'skill-create <name> <desc> "<steps>" [tags] — Create a new skill', + 'skill-create': 'skill-create <name> <desc> <path-or-text> [--tags=a,b] [--category=x] — Create a new skill', 'skill-patch': 'skill-patch <name> "<content>" — Replace a skill\'s full content', 'skill-delete': 'skill-delete <name> — Delete a skill', 'skill-export': 'skill-export <name> — 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 <name> <description> "<markdown content>" [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 <name> <description> <path-or-text> [--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 <name> "<full content>"', 'warning'); + this.terminal.print('Usage: skill-patch <name> <path-or-text> (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]) { |