diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/core/module-aiassist.js | 119 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 4 | ||||
| -rw-r--r-- | modules/core/module-neural-vfs.js | 44 | ||||
| -rw-r--r-- | modules/core/ollama-manager.js | 4 | ||||
| -rw-r--r-- | modules/core/terminal.js | 6 | ||||
| -rw-r--r-- | modules/core/virtual-fs.js | 354 | ||||
| -rw-r--r-- | modules/ext/console.js | 76 |
7 files changed, 545 insertions, 62 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index ecd4637..3af7f9b 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -44,53 +44,31 @@ } } catch (e) {} } - buildSystemPrompt() { +buildSystemPrompt() { + // Check for user-overridden prompt in localStorage + try { + const saved = localStorage.getItem('webx11_ai_system_prompt'); + if (saved) { + const now = new Date(); + const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); + const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); + return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n\n${saved}`; + } + } catch (e) {} const now = new Date(); - const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); - const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); - const optimizationActive = this.terminal?.ollamaManager?.optimizationEnabled !== false; - // Neural VFS staleness check + const dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); + const nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule; let nvfsNote = ''; try { - const nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule; if (nvfs && nvfs.lastScan && nvfs.lastScan !== '(loaded from disk)') { const ageMs = Date.now() - new Date(nvfs.lastScan).getTime(); - if (ageMs > 14400000) { // > 4 hours - nvfsNote = `\nNote: Neural VFS index is ${Math.floor(ageMs/3600000)}h old. nvfs-resolve results may be stale. Use list_files/read_file directly for live data.`; - } + if (ageMs > 14400000) nvfsNote = ' |nvfs ' + Math.floor(ageMs/3600000) + 'h stale'; } } catch (e) {} - return `Current date: ${dateStr} -Current time: ${timeStr}${nvfsNote} - -You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS → Modules → AI. You can edit your own modules via edit_module/test_module/reload_module. - -=== WORKFLOW === -1. Tool → Observe → Answer. Don't re-verify what a tool just told you. -2. find_files("about X") → nvfs_resolve(X) FIRST. NVFS is built-in core, never WPM. -3. load_skills("anti-loop-protocol") BEFORE tool #4 to prevent loops. -4. If editing a module: load_skills("module-editing-workflow") FIRST. -5. run_command may be blocked by security-level/guardrails — read the output. -6. Concise. Bullets. If a tool fails, try another. - -=== CAPABILITY MAP === -[FS] list_files, read_file, write_file, make_directory, delete_file, get_current_dir -[NVFS] nvfs_resolve(concept), run_command: nvfs-status, nvfs-index, nvfs-tree. Skills: vfs-content-search, neural-vfs-management. -[MODULES] view_module, edit_module, test_module, reload_module, list_modified_modules, get_module_status. CORE modules (never WPM): NVFS, Floating, Config, Vault, Sandbox, Orchestrator, Memory-Graph, World-Model, Predictor, Neural-VFS. -[WPM] wpm_install, wpm_search, list_available_modules -[SEARCH] web_search, web_fetch -[AGENTS] delegate_task, list_agents, spawn_agent. A2A: a2a-send, a2a-list. Leases: lease-create, lease-list, lease-revoke. -[SANDBOX] run_command: sandbox-status, sandbox-run, sandbox-config -[CONFIG] run_command: config, config-set, ai-config -[SKILLS] load_skills, create_skill, patch_skill, list_skills -[PROFILE/SESSION] user_get/set/list/forget, session_save/load/recent/stats/terminal -[SMART-CTX] smart_context_list, smart_context_set -[MINDS] create_mindmap -[SAFETY] run_command: guardrail, guardrail-add/remove, shadow, shadow-commit, shadow-rollback -[VAULT] run_command: secret-set/get/list/del, secret-setpin, secret-status -[CHECKPOINTS] run_command: checkpoint, checkpoint-list, checkpoint-del, rollback -[WEBDAV] run_command: webdav-config/ls/push/pull/backup/restore -[ALL OTHERS] cron, theme, voice, edit, diff, alias, mindmap, ll, cls, trash, gui, server, modules, ollama, context, world-model, memgraph, model, webxai, predict, predictor-config, memgraph-export, tar, untar, wget`; + return 'Current date: ' + dateStr + nvfsNote + '\n' + + 'VFS\u2192Modules\u2192AI. nvfs-index for commands. Tool\u2192Observe\u2192Answer. nvfs-resolve. Destructive cmds need confirm.\n' + + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch | Mod: view/edit/test/reload | Config | Skills\n' + + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* \u2014 discoverable via help'; } getToolDefinitions() { if (!this._toolDefs) this._toolDefs = this._buildToolDefs(); @@ -449,7 +427,9 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS 'ai-ignore': 'ai-ignore — Suppress validator warnings for this session', 'ai-stop': 'ai-stop — Cancel current AI operation', 'ai-model': 'ai-model [model] — Show or set AI model', - 'ai-config': 'ai-config [key] [value] — View or set AI loop config (max-turns, max-context, confirm-timeout(0=disabled))' + 'ai-config': 'ai-config [key] [value] — View or set AI loop config (max-turns, max-context, confirm-timeout(0=disabled))', + 'mem-debug': 'mem-debug — Show memory breakdown by component', + 'reset-mem': 'reset-mem — Reset AI memory (logs, patterns) without losing VFS or config' }; } registerCommands() { @@ -472,6 +452,8 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS this.commands['shadow'] = (args) => this.shadowExec(args); this.commands['shadow-commit'] = () => this.shadowCommit(); this.commands['shadow-rollback'] = () => this.shadowRollback(); + this.commands['mem-debug'] = () => this.memDebug(); + this.commands['reset-mem'] = () => this.resetMem(); } async chat(args) { const userMessage = args.join(' '); @@ -735,6 +717,10 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS duration: t1, error: this._isToolError(toolResult) ? true : false }); + // Auto-trim to 50 — prevents unbounded memory growth + if (this._toolCallLog.length > 50) { + this._toolCallLog = this._toolCallLog.slice(-50); + } // Telemetry const sm = this.terminal.moduleManager.modules['ServerMonitorModule']; if (sm && sm.logToolCall) sm.logToolCall(fn.name, (t1 * 1000) || 1, this._isToolError(toolResult)); @@ -801,6 +787,10 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS duration: t1, error: this._isToolError(toolResult) ? true : false }); + // Auto-trim to 50 — prevents unbounded memory growth + if (this._toolCallLog.length > 50) { + this._toolCallLog = this._toolCallLog.slice(-50); + } // Telemetry const sm2 = this.terminal.moduleManager.modules['ServerMonitorModule']; if (sm2 && sm2.logToolCall) sm2.logToolCall(fn.name, (t1 * 1000) || 1, this._isToolError(toolResult)); @@ -1350,7 +1340,10 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS 'cal','tree','find','grep','head','tail','sort','wc','du','diff','basename','dirname', 'yes','seq','cut','uniq','expand','printf','base64','help','clear','set','export', 'unalias','history','ll','h','mem','status','df','free','top','ps','health-check', - 'system-health','modules','listmods','modules-list','get-help-info','alias']); + 'system-health','modules','listmods','modules-list','get-help-info','alias', + 'nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync', + 'floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle', + 'mem-debug','reset-mem']); return !safeRead.has(cmd); } return ['write_file','delete_file','make_directory','edit_module'].includes(name); @@ -1843,6 +1836,7 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS } } catch (e) { this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() }); + if (this._errorLog.length > 20) this._errorLog.shift(); return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) }); } } @@ -2007,6 +2001,7 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS } } catch (e) { this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() }); + if (this._errorLog.length > 20) this._errorLog.shift(); return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) }); } } @@ -2049,10 +2044,46 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS } setSystemPrompt(args) { if (!args[0]) { - this.terminal.print(`Current system prompt: ${this.systemPrompt.substring(0, 50)}...`, 'info'); return; + this.terminal.print('=== System Prompt ===', 'info'); + this.systemPrompt.split('\n').forEach(line => this.terminal.print(line, 'text-muted')); + this.terminal.print(`=== End (${this.systemPrompt.length} chars) ===`, 'info'); + return; } this.systemPrompt = args.join(' '); - this.terminal.print('System prompt updated', 'success'); + // Persist to localStorage — next page load will use this instead of built-in + try { localStorage.setItem('webx11_ai_system_prompt', this.systemPrompt); } catch (e) {} + this.terminal.print('System prompt updated (persisted)', 'success'); + } + memDebug() { + const toolLogSize = this._toolCallLog.length; + const errLogSize = this._errorLog.length; + const freqKeys = Object.keys(this._commandFreq).length; + const contextSize = this.context.length; + const vfsSize = (localStorage.getItem('webx11_filesystem') || '').length + + (localStorage.getItem('webx11_filesystem_v2') || '').length; + const aiCtxSize = (localStorage.getItem('webx11_ai_context') || '').length; + this.terminal.print('=== Memory Debug ===', 'info'); + this.terminal.print(` _toolCallLog: ${toolLogSize} entries (cap: 50)`, toolLogSize > 30 ? 'warning' : 'text-muted'); + this.terminal.print(` _errorLog: ${errLogSize} entries (cap: 20)`, errLogSize > 10 ? 'warning' : 'text-muted'); + this.terminal.print(` _commandFreq: ${freqKeys} tracked patterns`, freqKeys > 15 ? 'warning' : 'text-muted'); + this.terminal.print(` AI context: ${contextSize} messages (max: ${this.maxContext * 2})`, 'text-muted'); + this.terminal.print(` VFS localStorage: ${(vfsSize / 1024).toFixed(1)} KB`, 'text-muted'); + this.terminal.print(` AI context localStorage: ${(aiCtxSize / 1024).toFixed(1)} KB`, 'text-muted'); + const heap = performance.memory ? performance.memory.usedJSHeapSize : null; + if (heap) { + this.terminal.print(` JS heap: ${(heap / 1048576).toFixed(1)} MB / ${(performance.memory.jsHeapSizeLimit / 1048576).toFixed(1)} MB`, 'text-muted'); + } + this.terminal.print('Use reset-mem to clear AI internal logs without touching VFS/config', 'info'); + } + resetMem() { + this._toolCallLog = []; + this._errorLog = []; + this._commandFreq = {}; + this.context = []; + localStorage.removeItem('webx11_ai_context'); + localStorage.removeItem('webx11_ai_patterns'); + this.terminal.print('AI memory reset: tool logs, error logs, command patterns, and context cleared', 'success'); + this.terminal.print('VFS, config, skills, system prompt, and floating elements preserved', 'text-muted'); } showModel(args) { const om = this.terminal.ollamaManager; diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index f0d3f4c..ab73202 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-09-v4.5.27'; + const LATEST_VER = '2026-06-10-v4.5.34'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { @@ -164,7 +164,7 @@ // Phase 4: Ext modules (filtered) if (typeof webx11._loadExtModules === 'function') { - const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'tutorial', 'shell', 'busybox', 'floating']; + const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'tutorial', 'shell', 'busybox', 'floating', 'console']; const extSelected = selected ? extKeys.filter(k => isSelected(k + '.js', selected)) : extKeys; diff --git a/modules/core/module-neural-vfs.js b/modules/core/module-neural-vfs.js index bdf1ffb..613d53a 100644 --- a/modules/core/module-neural-vfs.js +++ b/modules/core/module-neural-vfs.js @@ -7,6 +7,7 @@ class NeuralVFSModule { 'nvfs-tree': { argType: 'file' }, 'nvfs-config': { argType: 'file' }, 'nvfs-alias': { argType: 'file' }, 'nvfs-sync': { argType: 'file' }, + 'nvfs-save': { argType: 'none' }, }; this.terminal = null; this.index = {}; @@ -55,7 +56,8 @@ class NeuralVFSModule { 'nvfs-tree': 'nvfs-tree [concept] — Show concept-to-file mapping as a tree', 'nvfs-config': 'nvfs-config [key] [value] — View or set Neural VFS config (fuzzy-limit, tree-files, tree-concepts, resolve-files, deep-index, deep-model)', 'nvfs-alias': 'nvfs-alias list|add|remove|clear — Manage concept aliases', - 'nvfs-sync': 'nvfs-sync [--deep] — Incremental background sync (auto-sync dirty files)' + 'nvfs-sync': 'nvfs-sync [--deep] — Incremental background sync (auto-sync dirty files)', + 'nvfs-save': 'nvfs-save — Force-save current in-memory index to VFS (survives refresh)' }; } @@ -67,6 +69,7 @@ class NeuralVFSModule { this.commands['nvfs-config'] = (args) => this.nvfsConfig(args); this.commands['nvfs-alias'] = (args) => this.nvfsAlias(args); this.commands['nvfs-sync'] = async (args) => this.nvfsSync(args); + this.commands['nvfs-save'] = (args) => this.nvfsSave(); } // ── Core Index Builder (async, chunked) ── @@ -76,6 +79,19 @@ class NeuralVFSModule { const fs = this.terminal.fs; if (!fs) return this.terminal.print('VFS not available', 'error'); + // Safety guard: warn if deep mode is off but the saved index has LLM concepts + if (!useDeep) { + const prevIndex = fs.fsReadJSON(this.indexPath) || {}; + const hasLLM = Object.values(prevIndex).some(entries => + Array.isArray(entries) && entries.some(e => e.source === 'llm') + ); + if (hasLLM) { + this.terminal.print('⚠️ Deep indexing is OFF but the saved index contains LLM-generated concepts.', 'warning'); + this.terminal.print(' Running nvfs-index without --deep will STRIP all LLM concepts from re-indexed files.', 'warning'); + this.terminal.print(' To keep them: nvfs-config deep-index on or nvfs-index --deep', 'warning'); + } + } + this.terminal.print(`🔍 Scanning VFS for semantic concepts${useDeep ? ' (deep mode)' : ''}...`, 'info'); const start = Date.now(); @@ -634,7 +650,7 @@ class NeuralVFSModule { const hash = this._contentHash(content); const cached = this._getCached(path, hash); if (!cached) { - this._reindexFile(path, content, this.index); + this._reindexFile(path, content, this.index, this._deepIndexEnabled); this._hashCache[path] = { hash }; reindexed++; } @@ -768,6 +784,30 @@ Concepts:`; const llmConcepts = results.reduce((s, r) => s + r.concepts.length, 0); this.terminal?.print(`🧠 Deep pass complete: ${llmConcepts} LLM-generated concepts across ${results.length} files`, 'success'); } + // Persist LLM-generated concepts to disk so they survive page refresh + // Must force-save because _saveIndex() calls saveToStorage() which has a + // debounce guard (_storageSavePending) that may still be set from the + // initial buildIndex() save — without clearing it, the LLM concepts are + // never written to localStorage and are lost on page refresh. + const vfs = this.terminal?.fs; + if (vfs) { + vfs._storageSavePending = false; + this._saveIndex(vfs); + } + } + + // ── Nvfs Save (force-persist in-memory index) ── + + nvfsSave() { + const fs = this.terminal?.fs; + if (!fs) return this.terminal?.print('VFS not available', 'error'); + const count = Object.keys(this.index).length; + if (count === 0) return this.terminal?.print('No index in memory to save', 'warning'); + // Bypass debounce guard to ensure write + fs._storageSavePending = false; + this._saveIndex(fs); + this.terminal?.print(`✅ Saved ${count} concepts to ${this.indexPath}`, 'success'); + this.terminal?.print(' Index will survive page refresh', 'info'); } // ── Nvfs Sync (incremental background) ── diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js index 9999184..253e319 100644 --- a/modules/core/ollama-manager.js +++ b/modules/core/ollama-manager.js @@ -14,9 +14,9 @@ class OllamaManager { // --- AI Agent Loop Optimization Toggle --- this.optimizationEnabled = localStorage.getItem('webx11_optimization_enabled') !== 'false'; // --- Streaming Toggle --- - this.streamEnabled = localStorage.getItem('webx11_stream_enabled') !== 'false'; + this.streamEnabled = localStorage.getItem('webx11_stream_enabled') === 'true'; // --- Validator Agent Config --- - this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') !== 'false'; + this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true'; this.validatorModel = localStorage.getItem('webx11_validator_model') || 'ministral-3:3b-cloud'; this.validatorEndpoint = localStorage.getItem('webx11_validator_endpoint') || ''; // Build fallback endpoints diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 87636d6..fb9f332 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -10,7 +10,7 @@ this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']); this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']); this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']); - this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle']); + this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem']); // Load persisted overrides from localStorage try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){} try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){} @@ -1183,7 +1183,7 @@ } catch (e) {} } async _loadExtModules(filterKeys) { - const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating']; + const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console']; const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14'; try { const res = await fetch('packages.json?v=' + cacheVer); @@ -1202,7 +1202,7 @@ } async reloadPersistedModules() { const registry = this.moduleManager.moduleRegistry; - const skipList = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating']; + const skipList = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console']; for (const mod of registry) { if (mod.enabled && mod.url) { // Skip ext modules — they load via _loadExtModules diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js index f6e2826..9d3ea79 100644 --- a/modules/core/virtual-fs.js +++ b/modules/core/virtual-fs.js @@ -3,6 +3,308 @@ class VirtualFileSystem { this.root = { type: 'dir', content: {} }; this.currentPath = []; this._ready = this.loadFromStorage(); + this._virtualMounts = null; // lazy-init on first /sys/ access + // Register /sys/ in emit-change exclusion (also added below) + } + // ── Virtual /sys/ filesystem (live runtime state, never persisted) ── + _initVirtualMounts() { + if (this._virtualMounts) return; + const vm = (name, factory) => ({ _factory: factory, _name: name }); + const dir = () => ({ _dir: true, content: {} }); + const vmCache = {}; // factories can cache costly results within same tick + const mm = () => typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null; + const ai = () => mm()?.WebX11AIAssistantModule || null; + const heap = () => typeof performance !== 'undefined' && performance.memory ? performance.memory : null; + const fmt = (bytes) => { + if (!bytes && bytes !== 0) return 'N/A'; + const kb = bytes / 1024; + if (kb < 1024) return kb.toFixed(1) + ' KB'; + return (kb / 1024).toFixed(1) + ' MB'; + }; + const fmtBytes = (bytes) => { + if (!bytes && bytes !== 0) return 'N/A'; + if (bytes < 1024) return bytes + ' B'; + const kb = bytes / 1024; + if (kb < 1024) return kb.toFixed(1) + ' KB'; + return (kb / 1024).toFixed(1) + ' MB'; + }; + // Walk VFS root to count files/dirs and find largest + const vfsWalk = (dir, prefix) => { + let files = 0, dirs = 0, total = 0, largest = []; + const walk = (node, p) => { + if (!node) return; + if (node._factory || node._dir) return; // skip virtual + if (node.type === 'file') { + files++; + const size = node.content ? node.content.length : 0; + total += size; + largest.push({ path: p, size }); + } else if (node.type === 'dir') { + dirs++; + for (const [name, child] of Object.entries(node.content || {})) { + walk(child, p + '/' + name); + } + } + }; + walk(dir, prefix); + largest.sort((a, b) => b.size - a.size); + return { files, dirs, total, largest: largest.slice(0, 20) }; + }; + this._virtualMounts = { + // /sys/info + info: vm('info', () => [ + '=== newOS4 Runtime Debug (/sys/) ===', + `Version: ${window.NEWOS4_VERSION || '?'}`, + `JS heap: ${fmt(heap()?.usedJSHeapSize)} / ${fmt(heap()?.jsHeapSizeLimit)}`, + `DOM nodes: ${typeof document !== 'undefined' ? document.querySelectorAll('*').length : 'N/A'}`, + `Loaded modules: ${mm() ? Object.keys(mm()).length : 'N/A'}`, + '', + ' cat /sys/heap/stats — Memory overview', + ' cat /sys/heap/breakdown — What consumes what', + ' cat /sys/logs/tool-calls — AI tool call log', + ' cat /sys/logs/errors — AI error log', + ' cat /sys/vfs/stats — VFS file/dir counts', + ' cat /sys/vfs/largest — Top 20 largest files', + ' ls /sys/modules/ — Loaded modules', + ' cat /sys/config/ai — AI config parameters', + ' cat /sys/dom/node-count — DOM element breakdown', + ].join('\n')), + // /sys/heap/ + heap: { _dir: true, content: { + stats: vm('stats', () => { + const h = heap(); + const lsSize = (k) => { try { return (localStorage.getItem(k) || '').length / 1024; } catch(e) { return 0; } }; + return [ + '=== Heap Stats ===', + ` JS heap used: ${fmt(h?.usedJSHeapSize)}`, + ` JS heap limit: ${fmt(h?.jsHeapSizeLimit)}`, + ` JS heap % used: ${h ? ((h.usedJSHeapSize / h.jsHeapSizeLimit) * 100).toFixed(1) + '%' : 'N/A'}`, + '', + ` VFS localStorage: ${lsSize('webx11_filesystem_v2').toFixed(1)} KB`, + ` AI localStorage: ${lsSize('webx11_ai_context').toFixed(1)} KB`, + ` Config localStorage: ${lsSize('webx11_ai_config').toFixed(1)} KB`, + ].join('\n'); + }), + breakdown: vm('breakdown', () => { + const a = ai(); + const cCount = a?.context?.length || 0; + const lCount = a?._toolCallLog?.length || 0; + const eCount = a?._errorLog?.length || 0; + const modules = mm() ? Object.keys(mm()).length : 0; + const domNodes = typeof document !== 'undefined' ? document.querySelectorAll('*').length : 0; + return [ + '=== Heap Breakdown (estimates) ===', + ` Module code + state: ~${(modules * 1.2).toFixed(1)} MB (${modules} modules × ~1.2MB)`, + ` VFS tree (in-memory): varies with file count`, + ` AI context messages: ${cCount} (max: ${a?.maxContext || '?'})`, + ` AI tool call log: ${lCount} (cap: 50)`, + ` AI error log: ${eCount} (cap: 20)`, + ` DOM nodes: ${domNodes}`, + ].join('\n'); + }) + }}, + // /sys/modules/ + modules: { _dir: true, content: { + list: vm('list', () => { + const m = mm(); + if (!m) return '(moduleManager not available)'; + return Object.keys(m).sort().join('\n'); + }) + // module subdirectories are dynamic — handled in _virtualLs + }}, + // /sys/vfs/ + vfs: { _dir: true, content: { + stats: vm('stats', () => { + const w = vfsWalk(this.root, ''); + return [ + '=== VFS Stats ===', + ` Files: ${w.files}`, + ` Directories: ${w.dirs}`, + ` Total content: ${fmtBytes(w.total)}`, + ` localStorage: ${((localStorage.getItem('webx11_filesystem_v2') || '').length / 1024).toFixed(1)} KB (compressed)`, + ].join('\n'); + }), + largest: vm('largest', () => { + const w = vfsWalk(this.root, ''); + if (w.largest.length === 0) return '(empty VFS)'; + const maxW = Math.max(...w.largest.map(f => f.path.length), 8); + let out = '=== Top 20 Largest Files ===\n'; + w.largest.forEach((f, i) => { + out += ` ${(i + 1).toString().padStart(2)}. ${f.path.padEnd(maxW)} ${fmtBytes(f.size)}\n`; + }); + return out; + }) + }}, + // /sys/logs/ + logs: { _dir: true, content: { + 'tool-calls': vm('tool-calls', () => { + const a = ai(); + if (!a || !a._toolCallLog || a._toolCallLog.length === 0) return '(empty)'; + return a._toolCallLog.map((e, i) => `[${i}] ${JSON.stringify(e)}`).join('\n'); + }), + 'errors': vm('errors', () => { + const a = ai(); + if (!a || !a._errorLog || a._errorLog.length === 0) return '(empty)'; + return a._errorLog.map((e, i) => `[${i}] ${JSON.stringify(e)}`).join('\n'); + }), + 'commands': vm('commands', () => { + const a = ai(); + if (!a || !a._commandFreq || Object.keys(a._commandFreq).length === 0) return '(empty)'; + const sorted = Object.entries(a._commandFreq).sort((a,b) => b[1] - a[1]); + return sorted.map(([cmd, n]) => `${cmd.padEnd(20)} ${n}x`).join('\n'); + }) + }}, + // /sys/dom/ + dom: { _dir: true, content: { + 'node-count': vm('node-count', () => { + if (typeof document === 'undefined') return 'N/A'; + const all = document.querySelectorAll('*'); + const byTag = {}; + all.forEach(el => { const t = el.tagName.toLowerCase(); byTag[t] = (byTag[t] || 0) + 1; }); + const sorted = Object.entries(byTag).sort((a,b) => b[1] - a[1]); + let out = `Total DOM nodes: ${all.length}\n\n=== By Tag ===\n`; + sorted.forEach(([tag, count]) => { + out += ` ${tag.padEnd(15)} ${count}\n`; + }); + return out; + }) + }}, + // /sys/config/ + config: { _dir: true, content: { + version: vm('version', () => { + return `NEWOS4_VERSION: ${window.NEWOS4_VERSION || '?'}\n`; + }), + ai: vm('ai', () => { + const a = ai(); + if (!a) return '(AI module not loaded)'; + return [ + '=== AI Config ===', + ` maxContext: ${a.maxContext}`, + ` MAX_TOOL_TURNS: ${a.MAX_TOOL_TURNS}`, + ` maxContextTokens: ${a.maxContextTokens}`, + ` confirmTimeoutMs: ${a._confirmTimeoutMs}`, + ` skillDebounceMs: ${a._skillDebounceMs}`, + ` context messages: ${a.context?.length || 0}`, + ` tool call log cap: 50 (auto-capped)`, + ` error log cap: 20 (auto-capped)`, + ].join('\n'); + }) + }} + }; + } + // Check if a path is under /sys/ + _isVirtualPath(path) { + if (!path) return false; + const normalized = path.startsWith('/') ? path : '/' + path; + return normalized === '/sys' || normalized.startsWith('/sys/'); + } + // Resolve a virtual path: returns {dir: virtualNode, name: lastSegment} or null + _resolveVirtualPath(path) { + this._initVirtualMounts(); + const normalized = path.startsWith('/') ? path : '/' + path; + let segments = normalized.split('/').filter(s => s); + // Remove leading 'sys' + if (segments[0] === 'sys') segments = segments.slice(1); + if (segments.length === 0) { + // /sys itself — return the root virtual mounts node as a dir + const allContent = {}; + for (const [k, v] of Object.entries(this._virtualMounts)) { + if (v._dir) allContent[k] = { type: 'dir', content: v.content, _virtual: true }; + else allContent[k] = { type: 'file', content: null, _virtual: true, _factory: v._factory }; + } + // Add dynamic module subdirs to modules listing + if (this._virtualMounts.modules?._dir) { + const mm = typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null; + if (mm) { + for (const modName of Object.keys(mm)) { + if (!allContent[modName]) { + allContent[modName] = { type: 'dir', content: {}, _virtual: true }; + } + } + } + } + return { dir: { content: allContent }, name: null }; + } + // Walk the tree + let node = this._virtualMounts; + for (let i = 0; i < segments.length - 1; i++) { + const seg = segments[i]; + if (!node[seg]) { + // Check dynamic module subdir under /sys/modules/ + const mods = typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null; + if (node === this._virtualMounts?.modules?.content && mods && mods[seg]) { + // Dynamic module subdir — create virtual node + node = { + _dir: true, + content: { + info: { type: 'file', content: null, _virtual: true, _factory: () => { + const mod = mods[seg]; + const cmdKeys = mod.commands ? Object.keys(mod.commands) : []; + return [ + `Module: ${seg}`, + ` Type: ${typeof mod}`, + ` Commands: ${cmdKeys.join(', ') || '(none)'}`, + ` Has help: ${mod.getHelpInfo ? 'yes' : 'no'}`, + ` Constructor: ${mod.constructor?.name || 'anonymous'}`, + ].join('\n'); + }} + } + }; + continue; + } + return null; + } + node = node[seg]; + if (node && node._dir) node = node.content || {}; + } + const lastName = segments[segments.length - 1]; + if (node && node[lastName]) { + const entry = node[lastName]; + if (entry._dir) { + // Static subdir with known content + const allContent = {}; + for (const [k, v] of Object.entries(entry.content || {})) { + if (v._dir) allContent[k] = { type: 'dir', content: v.content, _virtual: true }; + else allContent[k] = { type: 'file', content: null, _virtual: true, _factory: v._factory }; + } + // Add dynamic module subdirs + if (segments[0] === 'modules' || (segments.length >= 2 && segments[segments.length - 2] === 'modules')) { + const mm = typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null; + if (mm) { + for (const modName of Object.keys(mm)) { + if (!allContent[modName]) { + allContent[modName] = { type: 'dir', content: {}, _virtual: true }; + } + } + } + } + return { dir: { content: allContent }, name: null }; + } else { + return { dir: { content: node }, name: lastName }; + } + } + // Check if it's a module subdirectory itself (e.g. /sys/modules/calc) + const mods = typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null; + if (node && mods && mods[lastName] && segments.length >= 1) { + // It's a module name — return its info dir + return { + dir: { content: { + info: { type: 'file', content: null, _virtual: true, _factory: () => { + const mod = mods[lastName]; + const cmdKeys = mod.commands ? Object.keys(mod.commands) : []; + return [ + `Module: ${lastName}`, + ` Type: ${typeof mod}`, + ` Commands: ${cmdKeys.join(', ') || '(none)'}`, + ` Has help: ${mod.getHelpInfo ? 'yes' : 'no'}`, + ` Constructor: ${mod.constructor?.name || 'anonymous'}`, + ].join('\n'); + }} + }}, + name: null + }; + } + return null; } _u8ToBase64(u8) { let s = ''; @@ -70,7 +372,7 @@ class VirtualFileSystem { // Skip internal index/system files to prevent re-trigger loops if (!path) return; const normalized = path.startsWith('/') ? path : '/' + path; - const sysPaths = ['/.semantic_index', '/.memory_graph', '/.trash', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/_mods', '/backups']; + const sysPaths = ['/.semantic_index', '/.memory_graph', '/.trash', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/_mods', '/backups', '/sys']; if (sysPaths.some(p => normalized.startsWith(p))) return; if (typeof window !== 'undefined' && window.__nvfsMarkDirty) { window.__nvfsMarkDirty(path); @@ -119,6 +421,7 @@ class VirtualFileSystem { return result; } _resolvePath(path) { + if (this._isVirtualPath(path)) return this._resolveVirtualPath(path); const segs = this._toAbsoluteSegments(path); if (segs === null) return null; if (segs.length === 0) return { dir: this.root, name: null }; @@ -130,6 +433,10 @@ class VirtualFileSystem { return { dir, name: segs[segs.length - 1] }; } _resolveDir(path) { + if (this._isVirtualPath(path)) { + const r = this._resolveVirtualPath(path); + return r ? r.dir : null; + } const segs = this._toAbsoluteSegments(path); if (segs === null) return null; if (segs.length === 0) return this.root; @@ -156,12 +463,19 @@ class VirtualFileSystem { dir = this.getCurrentDir(); } if (!dir) return []; - return Object.keys(dir.content).map(name => ({ + const entries = Object.keys(dir.content).map(name => ({ name, type: dir.content[name].type, - size: dir.content[name].content ? dir.content[name].content.length : 0 + size: dir.content[name]._factory ? 0 : (dir.content[name].content ? dir.content[name].content.length : 0), + _virtual: !!dir.content[name]._virtual })); + // Inject virtual /sys/ at root level + if ((!path || path === '/' || path === '') && !entries.some(e => e.name === 'sys' && e._virtual)) { + entries.push({ name: 'sys', type: 'dir', size: 0, _virtual: true }); + } + return entries; } writeFile(path, content) { + if (this._isVirtualPath(path)) return false; const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return false; const name = resolved.name; @@ -176,10 +490,14 @@ class VirtualFileSystem { const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return null; const node = resolved.dir.content[resolved.name]; + if (node && node._factory) { + return node._factory(); + } if (node && node.type === 'file') return node.content; return null; } deleteFile(path) { + if (this._isVirtualPath(path)) return false; const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return false; const node = resolved.dir.content[resolved.name]; @@ -243,6 +561,7 @@ class VirtualFileSystem { return this.trashDir(path); } mkdirRecursive(path) { + if (this._isVirtualPath(path)) return false; const segs = this._toAbsoluteSegments(path); if (!segs) return false; let dir = this.root; @@ -259,6 +578,7 @@ class VirtualFileSystem { return true; } rename(oldPath, newPath) { + if (this._isVirtualPath(oldPath) || this._isVirtualPath(newPath)) return false; const src = this._resolvePath(oldPath); if (!src || !src.name) return false; const dst = this._resolvePath(newPath); @@ -273,6 +593,7 @@ class VirtualFileSystem { return true; } copy(srcPath, dstPath) { + if (this._isVirtualPath(srcPath) || this._isVirtualPath(dstPath)) return false; const src = this._resolvePath(srcPath); if (!src || !src.name) return false; const dst = this._resolvePath(dstPath); @@ -294,6 +615,7 @@ class VirtualFileSystem { return this.rename(srcPath, dstPath); } rm(path) { + if (this._isVirtualPath(path)) return false; const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return false; const node = resolved.dir.content[resolved.name]; @@ -309,6 +631,7 @@ class VirtualFileSystem { return false; } rmdir(path) { + if (this._isVirtualPath(path)) return false; const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return false; const node = resolved.dir.content[resolved.name]; @@ -322,6 +645,7 @@ class VirtualFileSystem { return false; } touch(path) { + if (this._isVirtualPath(path)) return false; return this.writeFile(path, ''); } readDir(path) { @@ -330,16 +654,22 @@ class VirtualFileSystem { let dir; if (resolved.name) { const node = resolved.dir.content[resolved.name]; - if (node && node.type === 'dir') dir = node; + if (node && (node.type === 'dir' || node._virtual)) dir = node; else return null; } else { dir = resolved.dir; } - return Object.keys(dir.content).map(name => ({ + const entries = Object.keys(dir.content).map(name => ({ name, - type: dir.content[name].type, - size: dir.content[name].content ? dir.content[name].content.length : 0 + type: dir.content[name].type || (dir.content[name]._virtual ? 'file' : '?'), + size: dir.content[name]._factory ? 0 : (dir.content[name].content ? dir.content[name].content.length : 0), + _virtual: !!dir.content[name]._virtual })); + // Inject virtual /sys/ at root level + if ((!path || path === '/' || path === '') && !entries.some(e => e.name === 'sys' && e._virtual)) { + entries.push({ name: 'sys', type: 'dir', size: 0, _virtual: true }); + } + return entries; } fsWriteJSON(path, obj) { return this.writeFile(path, JSON.stringify(obj)); @@ -354,7 +684,7 @@ class VirtualFileSystem { } } tree(path = '', prefix = '') { - const resolved = this._resolvePath(path); + const resolved = this._resolvePath(path || '/'); if (!resolved) return ''; let dir; if (resolved.name) { @@ -366,12 +696,15 @@ class VirtualFileSystem { } let result = ''; const entries = Object.keys(dir.content).sort(); + // Inject /sys/ at root level in tree view + const atRoot = !path || path === '' || path === '/'; + if (atRoot && !entries.includes('sys')) entries.push('sys'); for (let i = 0; i < entries.length; i++) { const name = entries[i]; const isLast = i === entries.length - 1; const connector = isLast ? '└── ' : '├── '; result += prefix + connector + name + '\n'; - if (dir.content[name].type === 'dir') { + if (dir.content[name].type === 'dir' || dir.content[name]._virtual) { const ext = isLast ? ' ' : '│ '; result += this.tree(path + '/' + name, prefix + ext); } @@ -379,6 +712,7 @@ class VirtualFileSystem { return result; } find(pattern, path = '') { + if (this._isVirtualPath(path || '/')) return []; const resolved = this._resolvePath(path); if (!resolved) return []; let dir; @@ -415,6 +749,7 @@ class VirtualFileSystem { return results; } chmod(path, mode) { + if (this._isVirtualPath(path)) return false; const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return false; const node = resolved.dir.content[resolved.name]; @@ -424,6 +759,7 @@ class VirtualFileSystem { return true; } du(path = '') { + if (this._isVirtualPath(path || '/')) return 0; const resolved = this._resolvePath(path); if (!resolved) return 0; let dir; diff --git a/modules/ext/console.js b/modules/ext/console.js new file mode 100644 index 0000000..b0f6938 --- /dev/null +++ b/modules/ext/console.js @@ -0,0 +1,76 @@ +class JSConsoleModule { + constructor() { + this.commands = {}; + this.commandsMeta = { 'js': { argType: 'file' }, 'js-run': { argType: 'file' } }; + this.terminal = null; + } + + getHelpInfo() { + return { + 'js': 'js <expression> — Evaluate JavaScript in page context', + 'js-run': 'js-run <path> — Load and execute a JS file from VFS' + }; + } + + registerCommands() { + this.commands['js'] = (args) => this.evalJS(args); + this.commands['js-run'] = (args) => this.evalJSFile(args); + } + + evalJS(args) { + if (!args[0]) return this.terminal.print('Usage: js <expression>', 'warning'); + const expr = args.join(' '); + try { + const result = eval(expr); + const output = result === undefined ? 'undefined' : + typeof result === 'string' ? result : + result === null ? 'null' : + typeof result === 'object' ? JSON.stringify(result, null, 2) : + String(result); + this.terminal.print(output, 'success'); + } catch (e) { + this.terminal.print(`Error: ${e.message}`, 'error'); + } + } + + evalJSFile(args) { + if (!args[0]) return this.terminal.print('Usage: js-run <path>', 'warning'); + const path = args.join(' '); + try { + const content = this.terminal.fs.readFile(path); + if (content === null || content === undefined || content === false) { + return this.terminal.print(`js-run: ${path}: No such file`, 'error'); + } + // Wrap in an async IIFE so multi-line scripts with await work + const wrapped = `(async () => {\n${content}\n})()`; + const result = eval(wrapped); + // If it returns a promise, wait for it via .then + if (result && typeof result.then === 'function') { + result.then(val => { + const output = val === undefined ? '✓ Script executed' : + typeof val === 'string' ? val : + val === null ? 'null' : + typeof val === 'object' ? JSON.stringify(val, null, 2) : + String(val); + this.terminal.print(output, 'success'); + }).catch(e => { + this.terminal.print(`Error: ${e.message}`, 'error'); + }); + return; + } + const output = result === undefined ? '✓ Script executed' : + typeof result === 'string' ? result : + result === null ? 'null' : + typeof result === 'object' ? JSON.stringify(result, null, 2) : + String(result); + this.terminal.print(output, 'success'); + } catch (e) { + this.terminal.print(`Error: ${e.message}`, 'error'); + } + } + + seed() {} + init() {} +} + +if (typeof window !== 'undefined') window.JSConsoleModule = JSConsoleModule; |