From 0fa227601d51263b0eb787d53225de3f0b44cad1 Mon Sep 17 00:00:00 2001 From: Agando Date: Sun, 7 Jun 2026 18:00:19 +0200 Subject: auto: 2026-06-07_18:00 changes --- modules/core/module-aiassist.js | 72 ++++++-- modules/core/module-loader.js | 2 +- modules/core/module-neural-vfs.js | 370 ++++++++++++++++++++++++++++++++++++-- modules/core/terminal.js | 2 +- modules/core/virtual-fs.js | 16 ++ 5 files changed, 436 insertions(+), 26 deletions(-) (limited to 'modules/core') diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 5a4341a..ed76a8a 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -49,9 +49,20 @@ 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 + 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.`; + } + } + } catch (e) {} const CR = String.fromCharCode(10); const optNote = CR + CR + '== TOOL EXECUTION RULES (STRICT) ==' + CR + 'STRICT RULE 1: NEVER describe what the filesystem looks like from your training data. ALWAYS call list_files() or read_file() first, then report the ACTUAL result.' + CR + 'STRICT RULE 2: If you return text without calling any tool, you have failed. Use tools for every factual question.' + CR + 'STRICT RULE 3: A response like "/bin/, /etc/, /home/" without calling list_files is ILLEGAL. Call the tool.' + CR + 'STRICT RULE 4: When calling a tool, base your answer ENTIRELY on the tool result. If result is empty, say "empty".' + CR + CR; - return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n\nYou are WebX11AI, the AI assistant embedded in the WebX11 browser terminal. You have direct access to read/write files, search the web, install packages, edit modules, and run terminal commands — all via tool calls. Use tools proactively. Keep responses concise.${optNote}== TOOL REFERENCE == + return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n${nvfsNote}\n\nYou are WebX11AI, the AI assistant embedded in the WebX11 browser terminal. You have direct access to read/write files, search the web, install packages, edit modules, and run terminal commands — all via tool calls. Use tools proactively. Keep responses concise.${optNote}== TOOL REFERENCE == You have these tools available. Use them WITHOUT asking permission unless the tool description says (DESTRUCTIVE): FILESYSTEM: @@ -235,7 +246,7 @@ RULES: type: 'function', function: { name: 'run_command', - description: 'Run terminal command', + description: 'Run terminal command (safe-read commands auto-approved; mutating ones require confirmation)', parameters: { type: 'object', properties: { command: { type: 'string', description: 'Command to run' } }, required: ['command'] } } }, { @@ -506,7 +517,7 @@ RULES: '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)' + 'ai-config': 'ai-config [key] [value] — View or set AI loop config (max-turns, max-context, confirm-timeout(0=disabled))' }; } registerCommands() { @@ -776,8 +787,8 @@ RULES: this.terminal.print(`🔧 AI wants ${res.tool_calls.length} tool(s) — turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info'); messages.push(res.message); let pendingText = res.message?.content || null; - const destructiveTools = res.tool_calls.filter(tc => this.isDestructive(tc.function.name)); - const safeTools = res.tool_calls.filter(tc => !this.isDestructive(tc.function.name)); + const destructiveTools = res.tool_calls.filter(tc => this.isDestructive(tc.function.name, tc.function.arguments)); + const safeTools = res.tool_calls.filter(tc => !this.isDestructive(tc.function.name, tc.function.arguments)); // Execute non-destructive tools in parallel (independent read-only operations) const safeResults = await Promise.all(safeTools.map(async (tc) => { const fn = tc.function; @@ -969,10 +980,18 @@ RULES: } } async confirmActionMulti(toolCalls) { + // No-confirm mode: auto-approve + if (this._confirmTimeoutMs === 0) { + this.terminal.print(`⚡ No-confirm mode: auto-approving ${toolCalls.map(tc => tc.function.name).join(', ')}`, 'info'); + return true; + } const names = toolCalls.map(tc => tc.function.name).join(', '); const args = toolCalls.map(tc => typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments)).join(' | '); const truncatedArgs = args.length > 60 ? args.substring(0, 60) + '...' : args; - this.terminal.printHTML(`⚠️ AI wants to: ${names}(${truncatedArgs}). ai-allow to confirm, ai-deny to refuse. (Auto-denies in 120s)`); + const timeoutLabel = this._confirmTimeoutMs === 0 + ? '' + : ` (Auto-denies in ${this._confirmTimeoutMs / 1000}s)`; + this.terminal.printHTML(`⚠️ AI wants to: ${names}(${truncatedArgs}). ai-allow to confirm, ai-deny to refuse.${timeoutLabel}`); // Show confirm bar OUTSIDE scrollable output (always visible) const area = document.getElementById('ai-confirm-area'); if (!area) { @@ -1390,8 +1409,19 @@ RULES: } } // 🔒 OVERRIDES for isDestructive — adds runTool intercept - isDestructive(name) { - return name === 'write_file' || name === 'delete_file' || name === 'run_command' || name === 'make_directory' || name === 'edit_module'; + // Now command-aware: run_command is only destructive for mutating commands + isDestructive(name, args = {}) { + if (name === 'run_command') { + const cmdStr = typeof args === 'object' ? (args?.command || '') : String(args); + const cmd = cmdStr.split(' ')[0].toLowerCase(); + const safeRead = new Set(['ls','cat','pwd','echo','date','env','whoami','which','hostname', + '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']); + return !safeRead.has(cmd); + } + return ['write_file','delete_file','make_directory','edit_module'].includes(name); } async runTool(name, argsStr) { let a = {}; @@ -1420,10 +1450,26 @@ RULES: const parts = p.split('/'); const dir = this.terminal.fs.root; let cur = dir; + let found = true; for (const part of parts) { if (part === '..') { saved.pop(); continue; } if (part && cur.content[part] && cur.content[part].type === 'dir') cur = cur.content[part]; - else return JSON.stringify({ error: `Directory not found: ${p}` }); + else { found = false; break; } + } + if (!found) { + // Retry from root — cwd may be stale + this.terminal.fs.currentPath = []; + cur = this.terminal.fs.root; + found = true; + for (const part of parts) { + if (part === '..') continue; + if (part && cur.content[part] && cur.content[part].type === 'dir') cur = cur.content[part]; + else { found = false; break; } + } + if (!found) { + this.terminal.fs.currentPath = saved; + return JSON.stringify({ error: `Directory not found: ${p}` }); + } } this.terminal.fs.currentPath = saved.concat(parts.filter(x => x !== '..')); } @@ -1541,7 +1587,7 @@ RULES: const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; this.terminal.print = (text, type) => { output += text + '\n'; }; - try { await this.terminal.dispatchCommand(cmd.split(' ')[0].toLowerCase(), cmd.split(' ').slice(1)); } finally { this.terminal.print = originalPrint; } + try { await this.terminal.security.checkAndRun(cmd); } finally { this.terminal.print = originalPrint; } // 📸 Diff and append shadow report const shadowDiff = this._diffShadowVFS(); if (shadowDiff && shadowDiff.length > 0) { @@ -2104,11 +2150,11 @@ RULES: this.terminal.print(`max-context: ${this.maxContext} (default: 10)`, ''); this.terminal.print(`max-context-tokens: ${this.maxContextTokens} (default: 2000)`, ''); this.terminal.print(`trivial-optimization: ${(this.terminal?.ollamaManager?.optimizationEnabled !== false) ? 'on' : 'off'}`, ''); - this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs / 1000}s (default: 120s)`, ''); + this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (default: 120s)`, ''); this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, ''); this.terminal.print(`skill-debounce: ${this._skillDebounceMs / 60000}min (default: 30min)`, ''); this.terminal.print('Usage: ai-config ', 'info'); - this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization', 'info'); + this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization', 'info'); return; } const key = args[0]; @@ -2127,7 +2173,7 @@ RULES: this.maxContextTokens = n; } else if (key === 'confirm-timeout') { const n = parseInt(val); - if (isNaN(n) || n < 5 || n > 600) return this.terminal.print('confirm-timeout must be 5-600 seconds', 'warning'); + if (isNaN(n) || n < 0) return this.terminal.print('confirm-timeout must be 0+ seconds (0 = no confirm / auto-approve all)', 'warning'); this._confirmTimeoutMs = n * 1000; } else if (key === 'chat-lock-timeout') { const n = parseInt(val); diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index f4bd20b..1d1cbda 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-06-v4.5.9'; +const LATEST_VER = '2026-06-06-v4.5.12'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/module-neural-vfs.js b/modules/core/module-neural-vfs.js index 2644e85..5c30718 100644 --- a/modules/core/module-neural-vfs.js +++ b/modules/core/module-neural-vfs.js @@ -12,6 +12,26 @@ class NeuralVFSModule { this._treeConcepts = 15; this._resolveFiles = 20; this._loadConfig(); + // Auto-sync state + this._dirtyPaths = new Set(); + this._autoSyncTimer = null; + this._aliasMap = {}; + this._loadAliases(); + // Deep Indexing toggle + this._deepIndexEnabled = localStorage.getItem('webx11_nvfs_deep') === 'true'; + this._deepModel = localStorage.getItem('webx11_nvfs_deep_model') || 'ministral-3:3b-cloud'; + this._deepIndexing = false; + this._deepQueue = []; + this._deepProcessed = 0; + this._deepTotal = 0; + // Status bar polling — updates #status-text with indexing indicator + if (typeof window !== 'undefined') { + this._statusInterval = setInterval(() => this._updateStatusBar(), 2000); + } + // Register as window-level event listener for mutation events from VirtualFS + if (typeof window !== 'undefined') { + window.__nvfsMarkDirty = (path) => this.markDirty(path); + } } _loadConfig() { try { const c = localStorage.getItem('webx11_nvfs_config'); if (c) { const p = JSON.parse(c); if (p.fuzzyLimit) this._fuzzyLimit = p.fuzzyLimit; if (p.treeFiles) this._treeFiles = p.treeFiles; if (p.treeConcepts) this._treeConcepts = p.treeConcepts; if (p.resolveFiles) this._resolveFiles = p.resolveFiles; } } catch (e) {} @@ -22,29 +42,34 @@ class NeuralVFSModule { getHelpInfo() { return { - 'nvfs-index': 'nvfs-index — Scan VFS and build semantic concept index', + 'nvfs-index': 'nvfs-index [--deep] — Scan VFS and build semantic concept index', 'nvfs-status': 'nvfs-status — Show semantic index stats and last scan time', 'nvfs-resolve': 'nvfs-resolve — Find files related to a concept', 'nvfs-tree': 'nvfs-tree [concept] — Show concept-to-file mapping as a tree', - 'nvfs-config': 'nvfs-config [key] [value] — View or set Neural VFS display limits (fuzzy-limit, tree-files, tree-concepts, resolve-files)' + '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)' }; } registerCommands() { - this.commands['nvfs-index'] = (args) => this.buildIndex(); + this.commands['nvfs-index'] = (args) => this.buildIndex(args); this.commands['nvfs-status'] = (args) => this.showStatus(); this.commands['nvfs-resolve'] = (args) => this.resolve(args); this.commands['nvfs-tree'] = (args) => this.tree(args); 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); } // ── Core Index Builder (async, chunked) ── - async buildIndex() { + async buildIndex(args) { + const useDeep = this._deepIndexEnabled || (args && args.includes('--deep')); const fs = this.terminal.fs; if (!fs) return this.terminal.print('VFS not available', 'error'); - this.terminal.print('🔍 Scanning VFS for semantic concepts...', 'info'); + this.terminal.print(`🔍 Scanning VFS for semantic concepts${useDeep ? ' (deep mode)' : ''}...`, 'info'); const start = Date.now(); let newIndex = {}; @@ -99,13 +124,13 @@ class NeuralVFSModule { cachedCount++; if (!livePaths.has(path)) { // File was in cache but somehow missing from prevIndex - this._reindexFile(path, content, newIndex); + this._reindexFile(path, content, newIndex, useDeep); } continue; } // Changed or new file — extract concepts fresh - this._reindexFile(path, content, newIndex); + this._reindexFile(path, content, newIndex, useDeep); this._hashCache[path] = { hash }; } i = end; @@ -133,7 +158,22 @@ class NeuralVFSModule { processChunk(); } - _reindexFile(path, content, index) { + _reindexFile(path, content, index, useDeep) { + if (useDeep) { + // Deep mode: queue for async LLM concept extraction — fast-path filename concepts first + const fastConcepts = this._extractConcepts(path, ''); + for (const concept of fastConcepts) { + if (!index[concept]) index[concept] = []; + const exists = index[concept].some(e => e.path === path); + if (!exists) index[concept].push({ path, summary: this._summarize(content), source: 'filename' }); + } + this._deepQueue.push({ path, content }); + if (!this._deepIndexing) { + this._deepIndexing = true; + this._processDeepQueue(index).catch(e => console.error('Deep index error:', e)); + } + return; + } const concepts = this._extractConcepts(path, content); // Remove old entries for this path for (const entries of Object.values(index)) { @@ -170,6 +210,15 @@ class NeuralVFSModule { if (!args[0]) return this.terminal.print('Usage: nvfs-resolve ', 'warning'); const concept = args.join(' ').toLowerCase(); + + // Check aliases first + if (this._aliasMap[concept]) { + const targets = this._aliasMap[concept]; + this.terminal.print(`🔗 Alias "${concept}" → ${targets.length} target(s):`, 'info'); + for (const t of targets) this.terminal.print(` ${t}`, 'success'); + return; + } + const matches = this.index[concept] || []; // Fuzzy fallback — find partial matches @@ -178,6 +227,18 @@ class NeuralVFSModule { ).filter(k => k !== concept); if (matches.length === 0 && fuzzy.length === 0) { + // Hybrid discovery: fall back to live VFS scan + const shellResult = this._hybridShellCheck(concept); + if (shellResult.length > 0) { + this.terminal.print(`🔍 Index empty for "${concept}" but found ${shellResult.length} file(s) via live scan. Run nvfs-index to refresh.`, 'warning'); + for (const f of shellResult.slice(0, this._resolveFiles)) { + this.terminal.print(` ${f.path}${f.summary ? ' — ' + f.summary : ''}`, 'success'); + } + if (shellResult.length > this._resolveFiles) { + this.terminal.print(` ... and ${shellResult.length - this._resolveFiles} more`, 'info'); + } + return; + } return this.terminal.print(`No files found for concept "${concept}"`, 'warning'); } @@ -223,6 +284,31 @@ class NeuralVFSModule { this.terminal.print(` Concepts: ${Object.keys(this.index).length}`, ''); this.terminal.print(` Files indexed: ${totalFiles.size}`, ''); this.terminal.print(` Last scan: ${this.lastScan || 'never'}`, ''); + this.terminal.print(` Mode: ${this._deepIndexEnabled ? '🌊 Deep (LLM-assisted)' : '⚡ Fast (metadata-only)'}`, ''); + + // Staleness check + if (this.lastScan && this.lastScan !== '(loaded from disk)') { + const ageMs = Date.now() - new Date(this.lastScan).getTime(); + const ageHours = Math.floor(ageMs / 3600000); + const ageMinutes = Math.floor((ageMs % 3600000) / 60000); + if (ageHours >= 1) { + this.terminal.print(` Age: ${ageHours}h${ageMinutes > 0 ? ` ${ageMinutes}m` : ''}`, ''); + } + if (ageHours >= 4) { + this.terminal.print(` ⚠️ Index is ${ageHours} hours old — structural changes may be missing. Run nvfs-index to refresh.`, 'warning'); + } + } + + // Dirty file warning + if (this._dirtyPaths && this._dirtyPaths.size > 0) { + this.terminal.print(` ⚠️ ${this._dirtyPaths.size} file(s) changed since last scan (auto-sync pending)`, 'warning'); + } + + // Alias count + const aliasCount = Object.keys(this._aliasMap).length; + if (aliasCount > 0) { + this.terminal.print(` Aliases: ${aliasCount}`, ''); + } } // ── Tree View ── @@ -260,22 +346,89 @@ class NeuralVFSModule { this.terminal.print(`tree-files: ${this._treeFiles} (default: 5)`, ''); this.terminal.print(`tree-concepts: ${this._treeConcepts} (default: 15)`, ''); this.terminal.print(`resolve-files: ${this._resolveFiles} (default: 20)`, ''); + this.terminal.print(`deep-index: ${this._deepIndexEnabled ? 'on' : 'off'} (LLM-assisted concept extraction)`, ''); + this.terminal.print(`deep-model: ${this._deepModel} (model for deep indexing)`, ''); this.terminal.print('Usage: nvfs-config ', 'info'); - this.terminal.print('Keys: fuzzy-limit, tree-files, tree-concepts, resolve-files', 'info'); + this.terminal.print('Keys: fuzzy-limit, tree-files, tree-concepts, resolve-files, deep-index (on|off), deep-model ', 'info'); return; } const key = args[0]; + if (key === 'deep-index') { + const val = args[1]; + if (val !== 'on' && val !== 'off') return this.terminal.print('deep-index must be "on" or "off"', 'warning'); + this._deepIndexEnabled = val === 'on'; + localStorage.setItem('webx11_nvfs_deep', this._deepIndexEnabled ? 'true' : 'false'); + // If turning off mid-queue, abort pending deep work + if (!this._deepIndexEnabled && this._deepIndexing) { + this._deepQueue = []; + this._deepIndexing = false; + this._deepProcessed = 0; + this._deepTotal = 0; + this._updateStatusBar(); + this.terminal.print(' Deep index queue aborted', 'warning'); + } + this.terminal.print(`nvfs-config deep-index set to ${val}`, 'success'); + if (this._deepIndexEnabled) this.terminal.print(` Deep mode uses ${this._deepModel} for semantic concept extraction`, 'info'); + return; + } + if (key === 'deep-model') { + const val = args.slice(1).join(' '); + if (!val) return this.terminal.print('Usage: nvfs-config deep-model ', 'warning'); + this._deepModel = val; + localStorage.setItem('webx11_nvfs_deep_model', val); + this.terminal.print(`nvfs-config deep-model set to ${val}`, 'success'); + return; + } const val = parseInt(args[1]); if (isNaN(val) || val < 1 || val > 200) return this.terminal.print('Value must be 1-200', 'warning'); if (key === 'fuzzy-limit') { this._fuzzyLimit = val; } else if (key === 'tree-files') { this._treeFiles = val; } else if (key === 'tree-concepts') { this._treeConcepts = val; } else if (key === 'resolve-files') { this._resolveFiles = val; } - else { return this.terminal.print(`Unknown key: ${key}. Valid: fuzzy-limit, tree-files, tree-concepts, resolve-files`, 'warning'); } + else { return this.terminal.print(`Unknown key: ${key}. Valid: fuzzy-limit, tree-files, tree-concepts, resolve-files, deep-index (on|off), deep-model `, 'warning'); } this._saveConfig(); this.terminal.print(`nvfs-config ${key} set to ${val}`, 'success'); } + // ── Nvfs Alias ── + + nvfsAlias(args) { + const action = args[0]; + if (!action || action === 'list') { + const keys = Object.keys(this._aliasMap); + if (keys.length === 0) return this.terminal.print('No aliases defined. Use nvfs-alias add ', 'info'); + this.terminal.print('🔗 Concept Aliases:', 'info'); + for (const [name, targets] of Object.entries(this._aliasMap)) { + this.terminal.print(` ${name} → ${targets.join(', ')}`, 'success'); + } + return; + } + if (action === 'add') { + const name = args[1]; + if (!name || !args[2]) return this.terminal.print('Usage: nvfs-alias add ', 'warning'); + const targets = args.slice(2).join(' ').split(',').map(t => t.trim()).filter(Boolean); + this._aliasMap[name] = targets; + this._saveAliases(); + this.terminal.print(`Alias "${name}" added (${targets.length} target(s))`, 'success'); + return; + } + if (action === 'remove') { + const name = args[1]; + if (!name || !this._aliasMap[name]) return this.terminal.print('Alias not found', 'warning'); + delete this._aliasMap[name]; + this._saveAliases(); + this.terminal.print(`Alias "${name}" removed`, 'success'); + return; + } + if (action === 'clear') { + this._aliasMap = {}; + this._saveAliases(); + this.terminal.print('All aliases cleared', 'success'); + return; + } + this.terminal.print('Usage: nvfs-alias list|add |remove |clear', 'warning'); + } + // ── Private Helpers ── _walk(path, fs, index, cb) { @@ -292,7 +445,7 @@ class NeuralVFSModule { } _isSystemFile(path) { - const sysPrefixes = ['/.trash', '/.semantic_index', '/_snapshots', '/_sessions']; + const sysPrefixes = ['/.trash', '/.semantic_index', '/.memory_graph', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/_mods', '/backups']; return sysPrefixes.some(p => path.startsWith(p) || path.includes('/node_modules')); } @@ -371,6 +524,22 @@ class NeuralVFSModule { return [...concepts]; } + _hybridShellCheck(concept) { + const fs = this.terminal?.fs; + if (!fs) return []; + const words = concept.split(/\s+/).filter(w => w.length > 2); + if (words.length === 0) return []; + const results = []; + this._walk('/', fs, {}, (path, node) => { + if (this._isSystemFile(path)) return; + const lower = path.toLowerCase(); + if (words.some(w => lower.includes(w))) { + results.push({ path, summary: this._summarize(node.content || '') }); + } + }); + return results; + } + _summarize(content) { if (!content || content.length < 10) return ''; // Strip YAML frontmatter @@ -432,6 +601,185 @@ class NeuralVFSModule { console.error('NeuralVFS save error:', e); } } + + // ── Auto-Sync ── + + markDirty(path) { + this._dirtyPaths.add(path); + // Debounced auto-sync: wait 10s after last mutation + if (this._autoSyncTimer) clearTimeout(this._autoSyncTimer); + this._autoSyncTimer = setTimeout(() => this._reindexDirtyPaths(), 10000); + } + + async _reindexDirtyPaths() { + if (this._dirtyPaths.size === 0) return; + const fs = this.terminal?.fs; + if (!fs) { this._dirtyPaths.clear(); return; } + + const start = Date.now(); + let reindexed = 0; + this._loadHashCache(fs); + + for (const path of [...this._dirtyPaths]) { + const content = fs.readFile(path); + if (content !== null) { + const hash = this._contentHash(content); + const cached = this._getCached(path, hash); + if (!cached) { + this._reindexFile(path, content, this.index); + this._hashCache[path] = { hash }; + reindexed++; + } + } else { + // File was deleted — remove from index + for (const entries of Object.values(this.index)) { + const idx = entries.findIndex(e => e.path === path); + if (idx !== -1) entries.splice(idx, 1); + } + delete this._hashCache[path]; + } + } + this._dirtyPaths.clear(); + this._autoSyncTimer = null; + this.lastScan = new Date().toISOString(); + this._saveIndex(fs); + const elapsed = ((Date.now() - start) / 1000).toFixed(1); + if (reindexed > 0) { + this.terminal?.print(`🔄 Auto-sync: re-indexed ${reindexed} changed file(s) in ${elapsed}s`, 'success'); + } + } + + // ── Aliases ── + + _loadAliases() { + try { const r = localStorage.getItem('webx11_nvfs_aliases'); if (r) this._aliasMap = JSON.parse(r); } catch (e) {} + } + _saveAliases() { + localStorage.setItem('webx11_nvfs_aliases', JSON.stringify(this._aliasMap)); + } + + // ── Status Bar Polling ── + + _updateStatusBar() { + if (!this.terminal || !this.terminal.statusBar) return; + const el = this.terminal.statusBar; + if (this._deepIndexing) { + const pct = this._deepTotal > 0 ? Math.round(this._deepProcessed / this._deepTotal * 100) : 0; + el.textContent = `🧠 Deep Indexing... ${this._deepProcessed}/${this._deepTotal} (${pct}%)`; + el.style.color = 'var(--accent, #00ff88)'; + } else if (this._autoSyncTimer !== null || this._dirtyPaths.size > 0) { + el.textContent = `🔄 Auto-sync pending (${this._dirtyPaths.size} dirty)`; + el.style.color = 'var(--warning, #ffaa00)'; + } else { + const count = Object.keys(this.index).length; + el.textContent = `🧠 ${count} concepts${this._deepIndexEnabled ? ' 🌊' : ''}`; + el.style.color = ''; + } + } + + // ── Deep Indexing (LLM-assisted) ── + + _parseLLMConcepts(llmResponse) { + // LLM returns markdown bullet list: "- concept: description" + const concepts = []; + if (!llmResponse) return concepts; + const lines = llmResponse.split('\n'); + for (const line of lines) { + const trimmed = line.replace(/^[-*\s]+/, '').trim(); + if (!trimmed) continue; + // Split on first colon or comma or dash + const colonIdx = trimmed.indexOf(':'); + const commaIdx = trimmed.indexOf(','); + const delimIdx = colonIdx > 0 ? colonIdx : (commaIdx > 0 ? commaIdx : -1); + const concept = delimIdx > 0 ? trimmed.slice(0, delimIdx).trim().toLowerCase() : trimmed.toLowerCase(); + if (concept && concept.length > 2 && concept.length < 60) { + concepts.push(concept); + } + } + return [...new Set(concepts)]; + } + + async _callLLM(prompt, context) { + const om = this.terminal?.ollamaManager; + if (!om || !om.connected) return null; + const fullPrompt = `Extract 2-5 key searchable concepts from this file. Return one per line as lowercase keywords (1-3 words). + +File: ${context.path} +Content: +${context.content.slice(0, 2000)} + +Concepts:`; + try { + const result = await om._chatWithProvider(om.currentProvider, fullPrompt, this._deepModel); + return result; + } catch (e) { + console.warn('LLM concept extraction failed:', e); + return null; + } + } + + async _processDeepQueue(index) { + this._deepProcessed = 0; + this._deepTotal = this._deepQueue.length; + const batchSize = 3; + const results = []; + + while (this._deepQueue.length > 0) { + const batch = this._deepQueue.splice(0, batchSize); + for (const item of batch) { + const context = { path: item.path, content: item.content }; + const llmResponse = await this._callLLM('', context); + if (llmResponse) { + const concepts = this._parseLLMConcepts(llmResponse); + results.push({ path: item.path, concepts }); + for (const concept of concepts) { + if (!index[concept]) index[concept] = []; + const exists = index[concept].some(e => e.path === item.path); + if (!exists) index[concept].push({ + path: item.path, + summary: this._summarize(item.content), + source: 'llm' + }); + } + } + this._deepProcessed++; + // Update status bar live + this._updateStatusBar(); + } + // Yield to UI between batches + await new Promise(r => setTimeout(r, 50)); + } + + this._deepIndexing = false; + this._deepQueue = []; + this._deepProcessed = 0; + this._deepTotal = 0; + this._updateStatusBar(); + + if (results.length > 0) { + 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'); + } + } + + // ── Nvfs Sync (incremental background) ── + + async nvfsSync(args) { + const useDeep = args && args.includes('--deep'); + if (this._dirtyPaths.size === 0) { + this.terminal.print('No dirty files to sync', 'info'); + return; + } + this.terminal.print(`🔄 Syncing ${this._dirtyPaths.size} dirty file(s)${useDeep ? ' (deep mode)' : ''}...`, 'info'); + const fs = this.terminal?.fs; + if (!fs) return; + await this._reindexDirtyPaths(); + // After reindex, if deep mode, queue remaining queued items for LLM processing + if (useDeep && this._deepQueue.length > 0) { + this._deepIndexing = true; + await this._processDeepQueue(this.index); + } + } } // Required for module system diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 8f1ad19..2825340 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']); + 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']); // 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){} diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js index 59ab18b..12bb7f1 100644 --- a/modules/core/virtual-fs.js +++ b/modules/core/virtual-fs.js @@ -83,6 +83,14 @@ class VirtualFileSystem { } catch (e) { console.error('FS save error:', e); } }, 2000); } + _emitChange(path) { + // Notify NeuralVFS via window event — zero coupling + // Skip internal index/system files to prevent re-trigger loops + if (path && (path.startsWith('/.semantic_index') || path.startsWith('/.memory_graph') || path.startsWith('/.trash') || path.startsWith('/_snapshots') || path.startsWith('/_sessions') || path.startsWith('/logs') || path.startsWith('/memory') || path.startsWith('/system') || path.startsWith('/_mods') || path.startsWith('/backups'))) return; + if (typeof window !== 'undefined' && window.__nvfsMarkDirty) { + window.__nvfsMarkDirty(path); + } + } getCurrentPath() { return '/' + this.currentPath.join('/'); } @@ -192,6 +200,7 @@ class VirtualFileSystem { if (resolved.dir.content[name] && resolved.dir.content[name].type === 'dir') return false; resolved.dir.content[name] = { type: 'file', content: content, modified: new Date() }; this.saveToStorage(); + this._emitChange(path); return true; } readFile(path) { @@ -242,6 +251,7 @@ class VirtualFileSystem { const resolved = this._resolvePath(path); if (resolved) delete resolved.dir.content[resolved.name]; this.saveToStorage(); + this._emitChange(path); } catch (e) { console.error('Trash error:', e); } } trashDir(path) { @@ -272,6 +282,7 @@ class VirtualFileSystem { flatten(node, ts + '-dir-' + name); delete resolved.dir.content[resolved.name]; this.saveToStorage(); + this._emitChange(path); return true; } catch (e) { console.error('Trash dir error:', e); } } @@ -290,6 +301,7 @@ class VirtualFileSystem { dir = dir.content[seg]; } this.saveToStorage(); + this._emitChange(path); return true; } rename(oldPath, newPath) { @@ -302,6 +314,8 @@ class VirtualFileSystem { dst.dir.content[dst.name] = node; delete src.dir.content[src.name]; this.saveToStorage(); + this._emitChange(oldPath); + this._emitChange(newPath); return true; } copy(srcPath, dstPath) { @@ -313,6 +327,7 @@ class VirtualFileSystem { if (!node) return false; dst.dir.content[dst.name] = JSON.parse(JSON.stringify(node)); this.saveToStorage(); + this._emitChange(dstPath); return true; } move(srcPath, dstPath) { @@ -347,6 +362,7 @@ class VirtualFileSystem { if (Object.keys(node.content).length > 0) return false; delete resolved.dir.content[resolved.name]; this.saveToStorage(); + this._emitChange(path); return true; } return false; -- cgit v1.2.3