diff options
| author | Hermes SysOp <sysop@webx11.duckdns.org> | 2026-11-09 23:58:35 +0100 |
|---|---|---|
| committer | Hermes SysOp <sysop@webx11.duckdns.org> | 2026-11-09 23:58:35 +0100 |
| commit | 9f6744dd5cb99b904f9a23fce6f2d98e8332bbf8 (patch) | |
| tree | d3f3a525e8594db010bbf0e2aa6e4c2590445339 /modules/ext/wpm.js | |
| parent | 8e3a6155f6a5a1266ff1fa801c8864484bcdbda9 (diff) | |
fix(newos4): six crash classes from the feedback queue (v4.6.158)
Each item is symptom -> cause -> fix, re-verified against the served bytes.
1. debug-ai panel threw "Cannot read properties of undefined (reading 'substring')"
(RPT-20261108-231622). _updateDebugPanel and two sibling previews called
JSON.stringify(entry.args).substring(); JSON.stringify(undefined) returns
undefined, and _toolCallLog entries carry args: undefined for any tool call
with no arguments. All three previews now go through _formatToolArgs(), which
renders "null" and never throws.
2. test_module threw "Cannot convert undefined or null to object"
(RPT-20261108-232127). Object.keys(instance.commands) assumed every module
initialises a command map. The throw landed after loadModuleFromCode had
already succeeded, so the rolledBack:true branches were skipped and an
unverified module stayed loaded. Both sites now require instance.commands.
3. list_files threw "originalPath.replace is not a function" (RPT-20261109-172742,
3 occurrences in errors.json). A model can send a non-string path despite the
schema. The path family (list_files/read_file/write_file/make_directory/
delete_file) now coerces through _pathArg() and answers the tool's own honest
error instead of a TypeError.
4. wpm search failed for EVERY keyword (RPT-20261109-123907): packages.json entry
`kanban` used `description` while the other 31 use `desc`, and search() called
pkg.desc.toLowerCase() mid-loop. Catalog reads now go through _pkg() /
_packageMap() (desc || description, values coerced to strings), and the kanban
entry carries name/version/desc/commands. Re-running the original loop for
pdf/git/zzz is clean.
5. wx/webxai/webx turned a chat-lock timeout into an unhandled rejection
(RPT-20261107-150726): forwardToAI() called chat(args) in a synchronous
function without returning or catching the promise, so the 10 s busy throw
never reached dispatchCommand's catch. It now returns the promise with a
.catch that prints "AI error: ..." through terminal.print — console.error is
hooked by the debug supervisor and would re-file the same report.
6. CryptoGzip._base64ToBytes threw a raw DOMException on non-base64 input
(RPT-20261102-203616). It now validates first and throws
Error("not valid base64 input (<reason>)"). The duplicate shadowing
definitions at the bottom of the class were deleted — the later copy is the
one that carried the unguarded atob().
Also regenerated modules/core/core-bundle.js (generated, never hand-edited) and
bumped the build v4.6.157 -> v4.6.158 (main.html x3 + module-loader.js).
Verified: 35/35 node tests, including 6 new regression tests each proven red
against the pre-fix source; an independent orchestrator harness 36/36; and
newos4-maintain.py --fix all PASS after regenerating the stale changelog page.
Served bytes are byte-identical for the bundle, main.html and both ext modules,
and a live headless-Chromium run against v4.6.158 shows WpmModule.search('pdf')
returning 1 result, the debug panel rendering a no-arg entry as "null",
_base64ToBytes('grüße') raising a plain Error that names base64, and 0 JS errors
on load.
m.pull (RPT-20261102-211438) is closed as a usage error: no module in the tree
defines pull(); the real interfaces are the webdav-pull and model-pull commands.
Diffstat (limited to 'modules/ext/wpm.js')
| -rw-r--r-- | modules/ext/wpm.js | 56 |
1 files changed, 45 insertions, 11 deletions
diff --git a/modules/ext/wpm.js b/modules/ext/wpm.js index 68ac9cb..e9b9b50 100644 --- a/modules/ext/wpm.js +++ b/modules/ext/wpm.js @@ -21,6 +21,35 @@ class WpmModule { saveInstalled() { localStorage.setItem('webx11_wpm_installed', JSON.stringify(this.installed)); } + // ── Catalog normalisation ─────────────────────────────────────────────── + // packages.json is hand-edited JSON and its field names have drifted: `kanban` + // shipped with `description` while the other 31 entries use `desc` + // (RPT-20261109-123907-5a6273ab), and an entry can be missing `category` / + // `commands` entirely. Reading pkg.desc.toLowerCase() straight out of the index + // threw a TypeError out of `wpm search` for EVERY keyword (the bad entry is only + // reached mid-loop), so all catalog reads go through _pkg()/_packageMap(). + // `desc` wins when both keys exist; values are coerced so .toLowerCase() is safe. + _pkg(name, raw) { + const src = (raw && typeof raw === 'object') ? raw : {}; + const str = (v) => (v === undefined || v === null) ? '' : String(v); + let commands = src.commands; + if (typeof commands === 'string') commands = [commands]; + if (!Array.isArray(commands)) commands = []; + return Object.assign({}, src, { + name: str(src.name) || str(name), + version: str(src.version) || '?', + desc: str(src.desc) || str(src.description), + category: str(src.category), + url: str(src.url), + commands: commands, + }); + } + _packageMap(idx) { + const pkgs = (idx && idx.packages && typeof idx.packages === 'object') ? idx.packages : {}; + const out = {}; + Object.keys(pkgs).forEach(k => { out[k] = this._pkg(k, pkgs[k]); }); + return out; + } registerCommands() { this.commands['wpm'] = (args) => this.handle(args); } @@ -52,8 +81,9 @@ class WpmModule { if (!name) return this.terminal.print('Usage: wpm install <package>', 'warning'); const idx = await this.loadIndex(); if (!idx) return; - const pkg = idx.packages[name]; - if (!pkg) return this.terminal.print(`Package "${name}" not found. Try: wpm search <keyword>`, 'error'); + const raw = idx.packages && idx.packages[name]; + if (!raw) return this.terminal.print(`Package "${name}" not found. Try: wpm search <keyword>`, 'error'); + const pkg = this._pkg(name, raw); if (this.installed[name]) return this.terminal.print(`Package "${name}" already installed. Use wpm update to reinstall.`, 'warning'); this.terminal.print(`Installing ${pkg.name} v${pkg.version}...`, 'info'); this.terminal.print(` ${pkg.desc}`, 'info'); @@ -106,8 +136,9 @@ class WpmModule { }); } this.terminal.print('\n=== Available Packages ===', 'info'); - Object.keys(idx.packages).sort().forEach(name => { - const pkg = idx.packages[name]; + const pkgs = this._packageMap(idx); + Object.keys(pkgs).sort().forEach(name => { + const pkg = pkgs[name]; const installed = this.installed[name] ? ' ✅' : ''; this.terminal.print(` ${name} - ${pkg.desc}${installed}`); }); @@ -116,10 +147,10 @@ class WpmModule { if (!keyword) return this.terminal.print('Usage: wpm search <keyword>', 'warning'); const idx = await this.loadIndex(); if (!idx) return; - const kw = keyword.toLowerCase(); + const kw = String(keyword).toLowerCase(); const results = []; - Object.entries(idx.packages).forEach(([name, pkg]) => { - if (name.includes(kw) || pkg.desc.toLowerCase().includes(kw) || pkg.category.toLowerCase().includes(kw)) { + Object.entries(this._packageMap(idx)).forEach(([name, pkg]) => { + if (name.toLowerCase().includes(kw) || pkg.desc.toLowerCase().includes(kw) || pkg.category.toLowerCase().includes(kw)) { results.push({ name, ...pkg }); } }); @@ -137,8 +168,9 @@ class WpmModule { if (!this.index) return; this.terminal.print('Index refreshed.', 'success'); const outdated = []; + const pkgs = this._packageMap(this.index); Object.keys(this.installed).forEach(name => { - const pkg = this.index.packages[name]; + const pkg = pkgs[name]; if (pkg && pkg.version !== this.installed[name].version) { outdated.push(name); } @@ -151,7 +183,8 @@ class WpmModule { if (!name) return this.terminal.print('Usage: wpm remove <package>', 'warning'); if (!this.installed[name]) return this.terminal.print(`Package "${name}" is not installed via wpm`, 'warning'); const idx = await this.loadIndex(); - const className = idx && idx.packages[name] ? idx.packages[name].name.replace(/\s+/g, '') + 'Module' : null; + const raw = idx && idx.packages ? idx.packages[name] : null; + const className = raw ? this._pkg(name, raw).name.replace(/\s+/g, '') + 'Module' : null; if (className && this.terminal.moduleManager.unregisterModule(className)) { this.terminal.print(`Module ${className} unregistered`, 'info'); } @@ -163,8 +196,9 @@ class WpmModule { if (!name) return this.terminal.print('Usage: wpm info <package>', 'warning'); const idx = await this.loadIndex(); if (!idx) return; - const pkg = idx.packages[name]; - if (!pkg) return this.terminal.print(`Package "${name}" not found`, 'error'); + const raw = idx.packages && idx.packages[name]; + if (!raw) return this.terminal.print(`Package "${name}" not found`, 'error'); + const pkg = this._pkg(name, raw); this.terminal.print(`=== ${pkg.name} v${pkg.version} ===`, 'info'); this.terminal.print(`Description: ${pkg.desc}`); this.terminal.print(`Category: ${pkg.category}`); |