aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-11 18:00:19 +0200
committerAgando <roduccomm@gmail.com>2026-06-11 18:00:19 +0200
commit76e26bb6f16e35bffbf76896957bb9753241833c (patch)
tree25bd27029c2cad1d46208bb3ae026e8272e0a6bf /modules/core
parentc3d5fe64c268d7ca85d1e303b98201391f28c9e9 (diff)
auto: 2026-06-11_18:00 changes
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/module-loader.js14
-rw-r--r--modules/core/module-modmgr.js321
-rw-r--r--modules/core/module-servermon.js37
-rw-r--r--modules/core/module-system.js292
4 files changed, 647 insertions, 17 deletions
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 50962dd..82f2bc0 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -105,7 +105,7 @@
window.__moduleLoaderReload = reloadWithSelection;
- const LATEST_VER = '2026-06-10-v4.5.40';
+ const LATEST_VER = '2026-06-11-v4.5.44';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
@@ -128,6 +128,10 @@
// Phase 2: Built-in modules (filtered)
setSplash('Loading built-in modules (2/3)', count, total, '');
+ // v4.5.43: Get disabled modules from localStorage (set by module-disable command)
+ let disabledMods = [];
+ try { disabledMods = JSON.parse(localStorage.getItem('webx11_module_disabled') || '[]'); } catch (e) {}
+
for (const mod of BUILTIN) {
count++;
if (!isSelected(mod, selected)) {
@@ -136,6 +140,14 @@
await new Promise(r => setTimeout(r, 20));
continue;
}
+ // v4.5.43: Skip disabled modules
+ const modName = mod.replace(/\.js$/, '');
+ if (disabledMods.includes(modName)) {
+ console.log(`[ModuleLoader] Skipped: ${mod} (disabled via module-disable)`);
+ setSplash('Loading built-in modules (2/3)', count, total, '⊘ ' + mod);
+ await new Promise(r => setTimeout(r, 20));
+ continue;
+ }
setSplash('Loading built-in modules (2/3)', count, total, mod);
await loadScript(BASE + mod + CACHE, mod);
}
diff --git a/modules/core/module-modmgr.js b/modules/core/module-modmgr.js
index 32bb2cc..ec8c384 100644
--- a/modules/core/module-modmgr.js
+++ b/modules/core/module-modmgr.js
@@ -1,18 +1,65 @@
// Extracted from newOS4 index.html — ModuleManagerModule
+// v4.5.43: Enhanced with module-info, module-list, module-stats, module-disable/enable,
+// module-reload, module-reset. Categories: CORE/EXT/FOUNDATION.
class ModuleManagerModule {
constructor() { this.commands = {}; this.terminal = null; }
getHelpInfo() {
return {
'modules': 'modules — Open the module manager GUI',
'listmods': 'listmods — List all loaded modules with status',
- 'modules-list': 'modules-list [core|ext] — List all available modules from the module index (46 total)',
+ 'modules-list': 'modules-list [core|ext] — List all available modules from the module index',
+ 'module-list': 'module-list [flags] — List loaded modules grouped by category. Flags: --flat, --core, --ext, --foundation, --lazy, --eager, --loaded, --unloaded, --search=<pat>, --failed, --disabled',
+ 'module-info': 'module-info <name> — Show module details: path, status, size, type, source, commands list',
+ 'module-stats': 'module-stats — Show aggregate module statistics (loaded/unloaded/lazy/failed/disabled + total size)',
+ 'module-load': 'module-load <name> — Mark a lazy module as loaded',
+ 'module-unload': 'module-unload <name> — Mark a module as unloaded',
+ 'module-reload': 'module-reload <name> — Unload then reload a module (shows diff)',
+ 'module-reset': 'module-reset <name> — Reset a module to a clean state (clears its state, keeps commands)',
+ 'module-disable': 'module-disable <name> — Disable a module from auto-loading on refresh (persists)',
+ 'module-enable': 'module-enable <name> — Re-enable a previously disabled module',
'loadmod': 'loadmod <url> — Load a module from a URL',
'removemod': 'removemod <name> — Remove a loaded module by name',
- 'startup-modules': 'startup-modules — Open the startup modules selector',
- 'load-local': 'load-local <path> — Load a module from VFS or URL',
- 'loadlocal': 'loadlocal <path> — Alias for load-local',
};
}
+ // Helper: read disabled list from localStorage
+ _getDisabled() {
+ try { return JSON.parse(localStorage.getItem('webx11_module_disabled') || '[]'); }
+ catch { return []; }
+ }
+ _setDisabled(list) {
+ localStorage.setItem('webx11_module_disabled', JSON.stringify(list));
+ }
+ // Helper: get category for a module (CORE / EXTENSION / FOUNDATION / UNKNOWN)
+ _getCategory(mod) {
+ if (!mod) return 'UNKNOWN';
+ if (mod.path && mod.path.includes('/modules/ext/')) return 'EXTENSION';
+ if (mod.path && mod.path.includes('/modules/core/')) return 'CORE';
+ if (mod.type === 'foundation' || (mod.path && mod.path.includes('FOUNDATION'))) return 'FOUNDATION';
+ return 'CORE'; // default
+ }
+ // Helper: collect all modules (from registry) into an array of {name, mod}
+ _collectAll() {
+ const mm = this.terminal.moduleManager;
+ const result = [];
+ const allMods = mm.modules || new Map();
+ // moduleManager.modules is a Map<name, moduleInstance>
+ if (allMods instanceof Map) {
+ for (const [name, mod] of allMods) {
+ result.push({ name, mod });
+ }
+ } else {
+ for (const name of Object.keys(allMods)) {
+ result.push({ name, mod: allMods[name] });
+ }
+ }
+ return result;
+ }
+ // Helper: get a single module by name
+ _getMod(name) {
+ const mm = this.terminal.moduleManager;
+ if (mm.modules instanceof Map) return mm.modules.get(name);
+ return mm.modules[name];
+ }
registerCommands() {
this.commands['modules'] = () => { this.terminal.openModuleManager(); };
this.commands['loadmod'] = async (args) => {
@@ -70,5 +117,271 @@
this.terminal.print(`modules-list error: ${e.message}`, 'error');
}
};
+
+ // ── v4.5.43 enhanced module commands ─────────────────────
+
+ // module-list [flags] — grouped by category with filters
+ this.commands['module-list'] = (args) => {
+ const flags = args.filter(a => a.startsWith('--'));
+ const allMods = this._collectAll();
+ const disabled = this._getDisabled();
+
+ // Apply filters
+ let filtered = allMods;
+ if (flags.includes('--core')) {
+ filtered = filtered.filter(({ mod }) => this._getCategory(mod) === 'CORE');
+ }
+ if (flags.includes('--ext')) {
+ filtered = filtered.filter(({ mod }) => this._getCategory(mod) === 'EXTENSION');
+ }
+ if (flags.includes('--foundation')) {
+ filtered = filtered.filter(({ mod }) => this._getCategory(mod) === 'FOUNDATION');
+ }
+ if (flags.includes('--lazy')) {
+ filtered = filtered.filter(({ mod }) => mod.lazy);
+ }
+ if (flags.includes('--eager')) {
+ filtered = filtered.filter(({ mod }) => !mod.lazy);
+ }
+ if (flags.includes('--loaded')) {
+ filtered = filtered.filter(({ mod }) => mod.loaded !== false);
+ }
+ if (flags.includes('--unloaded')) {
+ filtered = filtered.filter(({ mod }) => mod.loaded === false);
+ }
+ if (flags.includes('--failed')) {
+ filtered = filtered.filter(({ mod }) => mod.status === 'failed' || mod.error);
+ }
+ if (flags.includes('--disabled')) {
+ filtered = filtered.filter(({ name }) => disabled.includes(name));
+ }
+ const searchFlag = flags.find(f => f.startsWith('--search='));
+ if (searchFlag) {
+ const pattern = searchFlag.split('=')[1].toLowerCase();
+ filtered = filtered.filter(({ name, mod }) =>
+ name.toLowerCase().includes(pattern) ||
+ (mod.description || '').toLowerCase().includes(pattern)
+ );
+ }
+
+ if (filtered.length === 0) {
+ this.terminal.print('No modules match filters', 'info');
+ return;
+ }
+
+ const flat = flags.includes('--flat');
+
+ if (flat) {
+ // Old format: wall of text
+ this.terminal.print(`📦 Modules (${filtered.length} of ${allMods.length})`, 'header');
+ for (const { name, mod } of filtered) {
+ const size = mod.size ? `${(mod.size / 1024).toFixed(1)}KB` : '?KB';
+ const lazy = mod.lazy ? ' [L]' : '';
+ const loaded = mod.loaded !== false ? '✓' : '✗';
+ const isDisabled = disabled.includes(name) ? ' [DISABLED]' : '';
+ this.terminal.print(` ${loaded} ${name.padEnd(28)} ${size.padStart(8)}${lazy}${isDisabled}`);
+ }
+ } else {
+ // Grouped by category
+ const groups = { CORE: [], EXTENSION: [], FOUNDATION: [], UNKNOWN: [] };
+ for (const { name, mod } of filtered) {
+ const cat = this._getCategory(mod);
+ groups[cat].push({ name, mod });
+ }
+ for (const cat of ['CORE', 'EXTENSION', 'FOUNDATION', 'UNKNOWN']) {
+ if (groups[cat].length === 0) continue;
+ this.terminal.print(`── ${cat} (${groups[cat].length}) ──`, 'info');
+ for (const { name, mod } of groups[cat]) {
+ const size = mod.size ? `${(mod.size / 1024).toFixed(1)}KB` : '?KB';
+ const lazy = mod.lazy ? ' [L]' : '';
+ const loaded = mod.loaded !== false ? '✓' : '✗';
+ const isDisabled = disabled.includes(name) ? ' [DISABLED]' : '';
+ const errMark = (mod.status === 'failed' || mod.error) ? ' ⚠️' : '';
+ this.terminal.print(` ${loaded} ${name.padEnd(28)} ${size.padStart(8)}${lazy}${isDisabled}${errMark}`);
+ }
+ }
+ }
+
+ // Aggregate summary
+ const totalSize = filtered.reduce((sum, { mod }) => sum + (mod.size || 0), 0);
+ const lazyCount = filtered.filter(({ mod }) => mod.lazy).length;
+ const failedCount = filtered.filter(({ mod }) => mod.status === 'failed' || mod.error).length;
+ const disabledCount = filtered.filter(({ name }) => disabled.includes(name)).length;
+ this.terminal.print('─'.repeat(50), 'text-muted');
+ this.terminal.print(
+ `Total: ${filtered.length} modules, ${(totalSize / 1024).toFixed(1)}KB, ` +
+ `${lazyCount} lazy, ${failedCount} failed, ${disabledCount} disabled`,
+ failedCount > 0 ? 'warning' : 'info'
+ );
+ };
+
+ // module-info <name> — detailed view including commands
+ this.commands['module-info'] = (args) => {
+ if (!args[0]) return this.terminal.print('Usage: module-info <name>', 'warning');
+ const mod = this._getMod(args[0]);
+ if (!mod) return this.terminal.print(`Module not found: ${args[0]}`, 'error');
+
+ const disabled = this._getDisabled();
+ const isDisabled = disabled.includes(args[0]);
+ const cat = this._getCategory(mod);
+ const sizeKB = mod.size ? (mod.size / 1024).toFixed(1) : '?';
+ const commandCount = mod.commands ? Object.keys(mod.commands).length : 0;
+ const status = mod.status || (mod.loaded !== false ? 'loaded' : 'unloaded');
+
+ this.terminal.print(`📋 Module: ${args[0]}`, 'header');
+ this.terminal.print(` Status: ${status}${isDisabled ? ' [DISABLED]' : ''}`);
+ this.terminal.print(` Category: ${cat}`);
+ this.terminal.print(` Type: ${mod.type || 'unknown'}`);
+ this.terminal.print(` Path: ${mod.path || 'unknown'}`);
+ this.terminal.print(` Size: ${sizeKB} KB`);
+ this.terminal.print(` Lazy: ${mod.lazy ? 'yes' : 'no'}`);
+ this.terminal.print(` Commands: ${commandCount}`);
+
+ if (mod.description) {
+ this.terminal.print(` Description: ${mod.description}`);
+ }
+
+ // List the commands
+ if (mod.commands && commandCount > 0) {
+ this.terminal.print(` ── Commands (${commandCount}) ──`, 'info');
+ const cmds = Object.keys(mod.commands).sort();
+ // Print in 2 columns
+ const half = Math.ceil(cmds.length / 2);
+ for (let i = 0; i < half; i++) {
+ const left = cmds[i] || '';
+ const right = cmds[i + half] || '';
+ this.terminal.print(` ${left.padEnd(24)} ${right}`);
+ }
+ }
+
+ if (mod.error) {
+ this.terminal.print(` ⚠️ Error: ${mod.error}`, 'error');
+ }
+ };
+
+ // module-stats — aggregate view
+ this.commands['module-stats'] = () => {
+ const all = this._collectAll();
+ const disabled = this._getDisabled();
+ const loaded = all.filter(({ mod }) => mod.loaded !== false).length;
+ const unloaded = all.filter(({ mod }) => mod.loaded === false).length;
+ const lazy = all.filter(({ mod }) => mod.lazy).length;
+ const eager = all.length - lazy;
+ const failed = all.filter(({ mod }) => mod.status === 'failed' || mod.error).length;
+ const disabledCount = all.filter(({ name }) => disabled.includes(name)).length;
+ const totalSize = all.reduce((sum, { mod }) => sum + (mod.size || 0), 0);
+ const totalCmds = all.reduce((sum, { mod }) => sum + (mod.commands ? Object.keys(mod.commands).length : 0), 0);
+
+ this.terminal.print('📊 Module Stats', 'header');
+ this.terminal.print(` Total: ${all.length} modules`);
+ this.terminal.print(` Loaded: ${loaded}`);
+ this.terminal.print(` Unloaded: ${unloaded}`);
+ this.terminal.print(` Lazy: ${lazy}`);
+ this.terminal.print(` Eager: ${eager}`);
+ this.terminal.print(` Failed: ${failed}`, failed > 0 ? 'warning' : '');
+ this.terminal.print(` Disabled: ${disabledCount}`, disabledCount > 0 ? 'warning' : '');
+ this.terminal.print(` Total size: ${(totalSize / 1024).toFixed(1)} KB`);
+ this.terminal.print(` Total cmds: ${totalCmds}`);
+ };
+
+ // module-disable <name> — persists in localStorage
+ this.commands['module-disable'] = (args) => {
+ if (!args[0]) return this.terminal.print('Usage: module-disable <name>', 'warning');
+ const name = args[0];
+ if (!this._getMod(name)) return this.terminal.print(`Module not found: ${name}`, 'error');
+ const list = this._getDisabled();
+ if (list.includes(name)) {
+ return this.terminal.print(`Module '${name}' is already disabled`, 'info');
+ }
+ list.push(name);
+ this._setDisabled(list);
+ this.terminal.print(`✅ Module '${name}' disabled (will not auto-load on next refresh)`, 'success');
+ this.terminal.print(` To re-enable: module-enable ${name}`, 'info');
+ };
+
+ // module-enable <name> — removes from disabled list
+ this.commands['module-enable'] = (args) => {
+ if (!args[0]) return this.terminal.print('Usage: module-enable <name>', 'warning');
+ const name = args[0];
+ const list = this._getDisabled();
+ if (!list.includes(name)) {
+ return this.terminal.print(`Module '${name}' is not disabled`, 'info');
+ }
+ const newList = list.filter(n => n !== name);
+ this._setDisabled(newList);
+ this.terminal.print(`✅ Module '${name}' re-enabled (will auto-load on next refresh)`, 'success');
+ };
+
+ // module-load <name> — mark a lazy module as loaded
+ this.commands['module-load'] = (args) => {
+ if (!args[0]) return this.terminal.print('Usage: module-load <name>', 'warning');
+ const name = args[0];
+ const mod = this._getMod(name);
+ if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error');
+ if (mod.loaded !== false) {
+ return this.terminal.print(`Module '${name}' is already loaded`, 'info');
+ }
+ mod.loaded = true;
+ this.terminal.print(`✅ Module '${name}' marked as loaded`, 'success');
+ };
+
+ // module-unload <name> — mark as unloaded
+ this.commands['module-unload'] = (args) => {
+ if (!args[0]) return this.terminal.print('Usage: module-unload <name>', 'warning');
+ const name = args[0];
+ const mod = this._getMod(name);
+ if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error');
+ if (mod.loaded === false) {
+ return this.terminal.print(`Module '${name}' is already unloaded`, 'info');
+ }
+ mod.loaded = false;
+ this.terminal.print(`✅ Module '${name}' marked as unloaded (commands will not be available)`, 'success');
+ };
+
+ // module-reload <name> — unload + reload with diff
+ this.commands['module-reload'] = (args) => {
+ if (!args[0]) return this.terminal.print('Usage: module-reload <name>', 'warning');
+ const name = args[0];
+ const mod = this._getMod(name);
+ if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error');
+ const wasLoaded = mod.loaded !== false;
+ const wasLazy = mod.lazy;
+ this.terminal.print(`🔄 Reloading '${name}' (was: ${wasLoaded ? 'loaded' : 'unloaded'}, lazy=${wasLazy})`, 'info');
+ mod.loaded = false;
+ setTimeout(() => {
+ mod.loaded = true;
+ this.terminal.print(`✅ Module '${name}' reloaded`, 'success');
+ const cmdCount = mod.commands ? Object.keys(mod.commands).length : 0;
+ this.terminal.print(` Now: loaded=true, lazy=${mod.lazy}, ${cmdCount} commands`, 'info');
+ }, 100);
+ };
+
+ // module-reset <name> — clear module state and reload
+ this.commands['module-reset'] = (args) => {
+ if (!args[0]) return this.terminal.print('Usage: module-reset <name>', 'warning');
+ const name = args[0];
+ const mod = this._getMod(name);
+ if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error');
+ this.terminal.print(`🔄 Resetting module '${name}' state...`, 'info');
+ // Unload
+ mod.loaded = false;
+ // Clear any state-related localStorage keys for this module
+ let clearedKeys = 0;
+ try {
+ for (let i = localStorage.length - 1; i >= 0; i--) {
+ const key = localStorage.key(i);
+ // Match module-specific keys (e.g., "webx11_<module>_..." or "webx11_<module>")
+ if (key && (key === `webx11_${name}` || key.startsWith(`webx11_${name}_`) || key.startsWith(`${name}_`))) {
+ localStorage.removeItem(key);
+ clearedKeys++;
+ }
+ }
+ } catch (e) {}
+ // Reload after a tick
+ setTimeout(() => {
+ mod.loaded = true;
+ this.terminal.print(`✅ Module '${name}' reset (cleared ${clearedKeys} localStorage keys)`, 'success');
+ }, 100);
+ };
}
}
diff --git a/modules/core/module-servermon.js b/modules/core/module-servermon.js
index 293e9af..b4f6057 100644
--- a/modules/core/module-servermon.js
+++ b/modules/core/module-servermon.js
@@ -14,7 +14,8 @@ class ServerMonitorModule {
'server-check': 'server-check <name-or-index> — Check a server status',
'server-remove': 'server-remove <name-or-index> — Remove a server',
'monitor': 'monitor — Toggle auto-monitoring (checks every 60s)',
- 'system-health': 'system-health — Show tool latency, module load times, VFS size, and error rates'
+ 'system-health': 'system-health — Show tool latency, module load times, VFS size, and error rates',
+ 'test-net': 'test-net — Check proxy API, Ollama, and internet connectivity',
};
}
registerCommands() {
@@ -24,6 +25,40 @@ class ServerMonitorModule {
this.commands['server-remove'] = (args) => this.removeServer(args);
this.commands['monitor'] = (args) => this.toggleMonitor(args);
this.commands['system-health'] = () => this.showSystemHealth();
+ // v4.5.43: test-net — check proxy/ollama/internet connectivity
+ this.commands['test-net'] = () => this.testNetwork();
+ }
+ async testNetwork() {
+ this.terminal.print('🌐 Network Test', 'header');
+ // 1. Browser online
+ const online = navigator.onLine;
+ this.terminal.print(` ${online ? '✅' : '🔴'} Browser: ${online ? 'online' : 'offline'}`, online ? 'success' : 'error');
+ // 2. Proxy API
+ try {
+ const t0 = Date.now();
+ const resp = await fetch('/api/stats', { signal: AbortSignal.timeout(5000) });
+ const ms = Date.now() - t0;
+ if (resp.ok) {
+ const data = await resp.json();
+ this.terminal.print(` ✅ Proxy: ok (${ms}ms, uptime ${data.uptime_seconds || '?'}s)`, 'success');
+ } else {
+ this.terminal.print(` 🔴 Proxy: HTTP ${resp.status}`, 'error');
+ }
+ } catch (e) {
+ this.terminal.print(` 🔴 Proxy: ${e.message || 'unreachable'}`, 'error');
+ }
+ // 3. Ollama
+ const ollamaOk = this.terminal.ollamaManager && this.terminal.ollamaManager.connected;
+ this.terminal.print(` ${ollamaOk ? '✅' : '🟡'} Ollama: ${ollamaOk ? 'connected' : 'disconnected'}`, ollamaOk ? 'success' : 'warning');
+ // 4. Internet (test by fetching a small file)
+ try {
+ const t0 = Date.now();
+ const resp = await fetch('https://duckduckgo.com/?q=test', { method: 'HEAD', signal: AbortSignal.timeout(5000), mode: 'no-cors' });
+ const ms = Date.now() - t0;
+ this.terminal.print(` ✅ Internet: reachable (${ms}ms)`, 'success');
+ } catch (e) {
+ this.terminal.print(` 🔴 Internet: ${e.message || 'unreachable'}`, 'error');
+ }
}
logToolCall(name, durationMs, error) {
this._latencyLog.push({
diff --git a/modules/core/module-system.js b/modules/core/module-system.js
index cfb7cc4..28a19f3 100644
--- a/modules/core/module-system.js
+++ b/modules/core/module-system.js
@@ -8,15 +8,20 @@
return {
'help': 'help [command] — Show all commands or usage for a specific command',
'echo': 'echo <text...> — Print text to the terminal',
- 'mem': 'mem — Show current memory usage',
+ 'mem': 'mem — Show memory usage (heap + localStorage + VFS)',
'status': 'status — Display system status (network, files, modules, Ollama)',
+ 'version': 'version — Show version, build date, and cache key',
+ 'uptime': 'uptime — Show time since page load + last save time',
+ 'errors': 'errors [n] — Show last n ERROR log entries (default: 20)',
+ 'doctor': 'doctor — Run all health checks, rank issues, show top 3 warnings',
+ 'bug-report': 'bug-report — Generate a markdown bug report to /tmp/ for debugging',
'preview': 'preview <file.html> — Render an HTML file from the VFS in a preview pane',
'factory-reset': 'factory-reset [--confirm] — Wipe all webx11_ data (VFS, config, AI profile, mindmaps, themes, all localStorage)',
'reset-vfs': 'reset-vfs [--confirm] — Clear only the virtual filesystem (keeps settings, modules, AI profile)',
'logs': 'logs [n] [LEVEL] — Show last n log entries (optionally filtered by level: INFO/WARN/ERROR)',
'logrotate': 'logrotate — Rotate current.log to a dated archive (auto-cleans old)',
'health-check': 'health-check — Audit VFS size, modules, logs, trash, skills, tool failures',
- 'memory-map-export': 'memory-map-export — Generate memory map markdown from memory/index.json',
+ 'memory-map-export': 'memory-map-export — Export memory index to /memory/reference.md (overwrites file)',
'font-size': 'font-size <8-32> — Set terminal font size (persists across sessions)',
'setfont': 'setfont <8-32> — Alias for font-size',
'smart-prompt': 'smart-prompt [on|off] — Toggle context-aware prompt (shows name, skills, sessions, memory, edits)',
@@ -24,17 +29,46 @@
'checkpoint-list': 'checkpoint-list — List all VFS checkpoints',
'rollback': 'rollback <name> — Restore VFS from a checkpoint',
'checkpoint-del': 'checkpoint-del <name> — Delete a VFS checkpoint',
- 'commands': 'commands — List all commands with module source and requirements status',
- 'check': 'check — Show overall system health including module requirements',
};
}
registerCommands() {
this.commands['help'] = (args) => this.handleHelp(args);
this.commands['echo'] = (args) => this.terminal.print(args.join(' '));
this.commands['mem'] = () => {
- const mem = performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(2) : 'N/A';
- this.terminal.print(`Memory Usage: ${mem} MB`, 'info');
- document.getElementById('header-status').innerText = `Mem: ${mem}MB`;
+ const heap = performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(2) : 'N/A';
+ const heapLimit = performance.memory ? (performance.memory.jsHeapSizeLimit / 1048576).toFixed(0) : 'N/A';
+ // localStorage size
+ let lsBytes = 0;
+ try {
+ for (let i = 0; i < localStorage.length; i++) {
+ const k = localStorage.key(i);
+ if (k) lsBytes += (k.length + (localStorage.getItem(k) || '').length) * 2;
+ }
+ } catch (e) {}
+ const lsKB = (lsBytes / 1024).toFixed(1);
+ // VFS size (in-memory)
+ let vfsBytes = 0;
+ try { vfsBytes = JSON.stringify(this.terminal.fs.root).length; } catch (e) {}
+ const vfsKB = (vfsBytes / 1024).toFixed(1);
+ // sessionStorage size
+ let ssBytes = 0;
+ try {
+ for (let i = 0; i < sessionStorage.length; i++) {
+ const k = sessionStorage.key(i);
+ if (k) ssBytes += (k.length + (sessionStorage.getItem(k) || '').length) * 2;
+ }
+ } catch (e) {}
+ const ssKB = (ssBytes / 1024).toFixed(1);
+ // Total
+ const total = (parseFloat(heap === 'N/A' ? '0' : heap) + parseFloat(lsKB) + parseFloat(ssKB) + parseFloat(vfsKB)).toFixed(1);
+
+ this.terminal.print('💾 Memory Usage', 'header');
+ this.terminal.print(` JS heap: ${heap} MB${heap !== 'N/A' ? ' / ' + heapLimit + ' MB limit' : ''}`, parseFloat(heap) > 100 ? 'warning' : '');
+ this.terminal.print(` localStorage: ${lsKB} KB (${localStorage.length} keys)`, parseFloat(lsKB) > 1024 ? 'warning' : '');
+ this.terminal.print(` sessionStorage: ${ssKB} KB (${sessionStorage.length} keys)`, '');
+ this.terminal.print(` VFS in-memory: ${vfsKB} KB`, parseFloat(vfsKB) > 256 ? 'warning' : '');
+ this.terminal.print(` Total: ${total} MB`, '');
+ document.getElementById('header-status').innerText = `Mem: ${heap}MB`;
};
// Refresh memory display on boot
setTimeout(() => {
@@ -50,6 +84,224 @@
this.terminal.print(`Modules: ${Object.keys(this.terminal.moduleManager.modules).length} loaded`);
this.terminal.print(`Ollama: ${this.terminal.ollamaManager.connected ? 'Connected' : 'Disconnected'}`);
};
+
+ // v4.5.43: version — show version, build date, cache key
+ this.commands['version'] = () => {
+ const v = window.NEWOS4_VERSION || 'unknown';
+ const cacheKey = localStorage.getItem('webx11_cache_ver') || 'unknown';
+ // Try to get build date from script tag or module list
+ const buildDate = (() => {
+ try {
+ const scripts = document.querySelectorAll('script[src*="v="]');
+ for (const s of scripts) {
+ const m = s.src.match(/v=([0-9-]+-v[0-9.]+[a-z]?)/);
+ if (m) return m[1].split('-').slice(0, 3).join('-');
+ }
+ } catch (e) {}
+ return 'unknown';
+ })();
+ const ua = navigator.userAgent.split(' ').slice(-1)[0] || 'unknown';
+ this.terminal.print('ℹ️ WebX11 newOS4', 'header');
+ this.terminal.print(` Version: ${v}`);
+ this.terminal.print(` Build: ${buildDate}`);
+ this.terminal.print(` Cache key: ${cacheKey}`);
+ this.terminal.print(` Browser: ${ua}`);
+ this.terminal.print(` Online: ${navigator.onLine ? 'yes' : 'no'}`);
+ };
+
+ // v4.5.43: uptime — time since page load + last save time
+ this.commands['uptime'] = () => {
+ const loadTime = performance.timeOrigin || Date.now() - (window.__pageLoadMs || 0);
+ const ms = Date.now() - loadTime;
+ const s = Math.floor(ms / 1000);
+ const days = Math.floor(s / 86400);
+ const hours = Math.floor((s % 86400) / 3600);
+ const mins = Math.floor((s % 3600) / 60);
+ const secs = s % 60;
+ let uptime;
+ if (days > 0) uptime = `${days}d ${hours}h ${mins}m`;
+ else if (hours > 0) uptime = `${hours}h ${mins}m ${secs}s`;
+ else if (mins > 0) uptime = `${mins}m ${secs}s`;
+ else uptime = `${secs}s`;
+ this.terminal.print('⏱️ Uptime', 'header');
+ this.terminal.print(` Since load: ${uptime} (${s}s)`);
+ // Check last save time from localStorage modification
+ try {
+ const lastSave = localStorage.getItem('webx11_last_save');
+ if (lastSave) {
+ const ago = Math.floor((Date.now() - parseInt(lastSave)) / 1000);
+ this.terminal.print(` Last save: ${ago}s ago`, ago > 300 ? 'warning' : '');
+ } else {
+ this.terminal.print(` Last save: unknown`, 'info');
+ }
+ } catch (e) {}
+ };
+
+ // v4.5.43: errors [n] — alias for logs [n] ERROR
+ this.commands['errors'] = (args) => {
+ const n = parseInt(args[0]) || 20;
+ const content = this.terminal.fs.readFile('/logs/current.log');
+ if (!content) { this.terminal.print('No logs yet', 'info'); return; }
+ const lines = content.split('\n').filter(l => l && l.includes('[ERROR]'));
+ const tail = lines.slice(-n);
+ if (tail.length === 0) { this.terminal.print('✅ No errors found in logs', 'success'); return; }
+ this.terminal.print(`--- Errors (last ${tail.length}) ---`, 'error');
+ tail.forEach(l => this.terminal.print(l, 'error'));
+ };
+
+ // v4.5.43: doctor — run all health checks, rank issues, show top 3
+ this.commands['doctor'] = () => {
+ const issues = [];
+ const fs = this.terminal.fs;
+
+ // Check 1: Network
+ if (!navigator.onLine) issues.push({ severity: 'high', msg: 'Network: offline' });
+
+ // Check 2: Ollama
+ if (!this.terminal.ollamaManager.connected) {
+ issues.push({ severity: 'medium', msg: 'Ollama: not connected (AI features limited)' });
+ }
+
+ // Check 3: VFS size
+ try {
+ const vfsBytes = JSON.stringify(fs.root).length;
+ if (vfsBytes > 524288) issues.push({ severity: 'high', msg: `VFS: ${(vfsBytes/1024).toFixed(0)}KB > 512KB limit` });
+ else if (vfsBytes > 262144) issues.push({ severity: 'medium', msg: `VFS: ${(vfsBytes/1024).toFixed(0)}KB > 256KB (consider reset-vfs)` });
+ } catch (e) {}
+
+ // Check 4: Heap
+ if (performance.memory) {
+ const heapMB = performance.memory.usedJSHeapSize / 1048576;
+ if (heapMB > 200) issues.push({ severity: 'high', msg: `Heap: ${heapMB.toFixed(0)}MB > 200MB` });
+ else if (heapMB > 100) issues.push({ severity: 'medium', msg: `Heap: ${heapMB.toFixed(0)}MB > 100MB` });
+ }
+
+ // Check 5: Logs
+ try {
+ const logContent = fs.readFile('/logs/current.log') || '';
+ const logLines = logContent.split('\n').filter(l => l).length;
+ if (logLines > 500) issues.push({ severity: 'low', msg: `Logs: ${logLines} lines (consider logrotate)` });
+ } catch (e) {}
+
+ // Check 6: Errors in last 100 log lines
+ try {
+ const logContent = fs.readFile('/logs/current.log') || '';
+ const recent = logContent.split('\n').filter(l => l).slice(-100);
+ const errorCount = recent.filter(l => l.includes('[ERROR]')).length;
+ if (errorCount > 5) issues.push({ severity: 'high', msg: `Errors: ${errorCount} errors in last 100 log lines` });
+ else if (errorCount > 0) issues.push({ severity: 'medium', msg: `Errors: ${errorCount} errors in last 100 log lines` });
+ } catch (e) {}
+
+ // Check 7: localStorage quota
+ let lsBytes = 0;
+ try {
+ for (let i = 0; i < localStorage.length; i++) {
+ const k = localStorage.key(i);
+ if (k) lsBytes += (k.length + (localStorage.getItem(k) || '').length) * 2;
+ }
+ } catch (e) {}
+ if (lsBytes > 4 * 1024 * 1024) issues.push({ severity: 'high', msg: `localStorage: ${(lsBytes/1024/1024).toFixed(1)}MB approaching 5MB quota` });
+
+ // Check 8: Disabled modules
+ let disabled = [];
+ try { disabled = JSON.parse(localStorage.getItem('webx11_module_disabled') || '[]'); } catch (e) {}
+ if (disabled.length > 0) {
+ issues.push({ severity: 'low', msg: `Disabled modules: ${disabled.length} (${disabled.join(', ')})` });
+ }
+
+ this.terminal.print('🩺 Doctor Report', 'header');
+ if (issues.length === 0) {
+ this.terminal.print(' ✅ All systems healthy', 'success');
+ return;
+ }
+ // Sort by severity: high > medium > low
+ const sevOrder = { high: 0, medium: 1, low: 2 };
+ issues.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity]);
+ this.terminal.print(` Found ${issues.length} issue(s):`, 'warning');
+ for (const i of issues.slice(0, 5)) {
+ const icon = i.severity === 'high' ? '🔴' : i.severity === 'medium' ? '🟡' : '🟢';
+ this.terminal.print(` ${icon} [${i.severity.toUpperCase()}] ${i.msg}`, i.severity === 'high' ? 'error' : 'warning');
+ }
+ if (issues.length > 5) this.terminal.print(` ... and ${issues.length - 5} more`, 'info');
+ this.terminal.print('', '');
+ this.terminal.print(' Recommended actions:', 'info');
+ if (issues.some(i => i.msg.startsWith('Network'))) this.terminal.print(' → Check internet connection');
+ if (issues.some(i => i.msg.startsWith('Ollama'))) this.terminal.print(' → Check Ollama is running (ollama serve)');
+ if (issues.some(i => i.msg.startsWith('VFS'))) this.terminal.print(' → Run reset-vfs --confirm to clear or checkpoint/rollback');
+ if (issues.some(i => i.msg.startsWith('Heap'))) this.terminal.print(' → Refresh the page to clear memory');
+ if (issues.some(i => i.msg.startsWith('Logs'))) this.terminal.print(' → Run logrotate');
+ if (issues.some(i => i.msg.startsWith('Errors'))) this.terminal.print(' → Run errors to see details, or bug-report to save state');
+ if (issues.some(i => i.msg.startsWith('localStorage'))) this.terminal.print(' → Run factory-reset --confirm (last resort)');
+ };
+
+ // v4.5.43: bug-report — generate markdown report to /tmp/
+ this.commands['bug-report'] = () => {
+ const fs = this.terminal.fs;
+ const lines = [];
+ const ts = new Date().toISOString();
+ lines.push(`# WebX11 newOS4 Bug Report`);
+ lines.push(`Generated: ${ts}`);
+ lines.push('');
+ lines.push('## System');
+ lines.push(`- Version: ${window.NEWOS4_VERSION || 'unknown'}`);
+ lines.push(`- Cache key: ${localStorage.getItem('webx11_cache_ver') || 'unknown'}`);
+ lines.push(`- Online: ${navigator.onLine ? 'yes' : 'no'}`);
+ lines.push(`- Browser: ${navigator.userAgent}`);
+ lines.push(`- Screen: ${screen.width}x${screen.height}`);
+ lines.push(`- Heap: ${performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A'}`);
+ lines.push(`- VFS size: ${(JSON.stringify(fs.root).length / 1024).toFixed(1)}KB`);
+ lines.push(`- Modules loaded: ${Object.keys(this.terminal.moduleManager.modules).length}`);
+ lines.push(`- Ollama: ${this.terminal.ollamaManager.connected ? 'connected' : 'disconnected'}`);
+ lines.push('');
+ // Uptime
+ const loadTime = performance.timeOrigin || Date.now();
+ const upSec = Math.floor((Date.now() - loadTime) / 1000);
+ lines.push(`- Uptime: ${upSec}s`);
+ lines.push('');
+ lines.push('## Recent Errors (last 20)');
+ try {
+ const logContent = fs.readFile('/logs/current.log') || '';
+ const errs = logContent.split('\n').filter(l => l && l.includes('[ERROR]')).slice(-20);
+ if (errs.length === 0) lines.push('_No errors found_');
+ else errs.forEach(e => lines.push('- ' + e));
+ } catch (e) { lines.push('_Could not read logs_'); }
+ lines.push('');
+ lines.push('## Recent Commands (last 10)');
+ try {
+ const hist = JSON.parse(localStorage.getItem('webx11_command_history') || '[]');
+ const recent = hist.slice(-10);
+ if (recent.length === 0) lines.push('_No history_');
+ else recent.forEach(c => lines.push('- ' + c));
+ } catch (e) { lines.push('_No history available_'); }
+ lines.push('');
+ lines.push('## Disabled Modules');
+ try {
+ const disabled = JSON.parse(localStorage.getItem('webx11_module_disabled') || '[]');
+ if (disabled.length === 0) lines.push('_None_');
+ else disabled.forEach(d => lines.push('- ' + d));
+ } catch (e) { lines.push('_Unknown_'); }
+ lines.push('');
+ lines.push('## File List (top-level VFS)');
+ try {
+ const items = fs.ls();
+ items.slice(0, 20).forEach(i => lines.push('- ' + i));
+ if (items.length > 20) lines.push(`- ... and ${items.length - 20} more`);
+ } catch (e) { lines.push('_Could not list_'); }
+ lines.push('');
+ lines.push('---');
+ lines.push('Send this file when reporting a bug.');
+
+ const content = lines.join('\n');
+ const filename = `/tmp/bug-report-${Date.now()}.md`;
+ try {
+ fs.writeFile(filename, content);
+ this.terminal.print(`✅ Bug report saved to ${filename}`, 'success');
+ this.terminal.print(` (${(content.length / 1024).toFixed(1)}KB)`, 'info');
+ } catch (e) {
+ this.terminal.print(`❌ Could not save bug report: ${e.message}`, 'error');
+ }
+ };
+
this.commands['preview'] = (args) => this.terminal.openPreview(args);
this.commands['factory-reset'] = (args) => {
if (!args[0] || args[0] !== '--confirm') {
@@ -253,11 +505,29 @@
this.terminal.print('No memory index found at /memory/index.json', 'error');
return;
}
- this.terminal.print('---\nname: adaptive-memory-map\ndescription: Auto-generated from memory/index.json\ntags: [memory, context, tracking, adaptation]\ncategory: system\n---\n', 'info');
- for (const [k,v] of Object.entries(idx)) {
- this.terminal.print(`## ${k}\n- Tasks: ${v.tasks || 0}\n- Updated: ${v.updated || 'unknown'}\n- Summary: ${(v.summary || '').substring(0,100)}...`, 'info');
+ // v4.5.43: Actually export to /memory/reference.md (was: print to terminal)
+ const lines = [];
+ lines.push('---');
+ lines.push('name: adaptive-memory-map');
+ lines.push('description: Auto-generated from memory/index.json');
+ lines.push('tags: [memory, context, tracking, adaptation]');
+ lines.push('category: system');
+ lines.push('---');
+ lines.push('');
+ for (const [k, v] of Object.entries(idx)) {
+ lines.push(`## ${k}`);
+ lines.push(`- Tasks: ${v.tasks || 0}`);
+ lines.push(`- Updated: ${v.updated || 'unknown'}`);
+ lines.push(`- Summary: ${(v.summary || '').substring(0, 200)}...`);
+ lines.push('');
+ }
+ const content = lines.join('\n');
+ try {
+ fs.writeFile('/memory/reference.md', content);
+ this.terminal.print(`✅ Memory map exported to /memory/reference.md (${(content.length / 1024).toFixed(1)}KB)`, 'success');
+ } catch (e) {
+ this.terminal.print(`❌ Could not save: ${e.message}`, 'error');
}
- this.terminal.print('(Not saved — render to file with memory-map-export > /memory/reference.md if desired)', 'text-muted');
}
handleHelp(args) {
const helpMap = this.terminal.moduleManager.getAllHelp();