aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/core/module-aiassist.js72
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-neural-vfs.js370
-rw-r--r--modules/core/terminal.js2
-rw-r--r--modules/core/virtual-fs.js16
5 files changed, 436 insertions, 26 deletions
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}). <span class="cmd-link" onclick="webx11.executeCommand('ai-allow')" style="color:var(--success-color);cursor:pointer;text-decoration:underline;">ai-allow</span> to confirm, <span class="cmd-link" onclick="webx11.executeCommand('ai-deny')" style="color:var(--error-color);cursor:pointer;text-decoration:underline;">ai-deny</span> 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}). <span class="cmd-link" onclick="webx11.executeCommand('ai-allow')" style="color:var(--success-color);cursor:pointer;text-decoration:underline;">ai-allow</span> to confirm, <span class="cmd-link" onclick="webx11.executeCommand('ai-deny')" style="color:var(--error-color);cursor:pointer;text-decoration:underline;">ai-deny</span> 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 <key> <value>', '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 <concept> — 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 <concept>', '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 <key> <value>', '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 <name>', '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 <model-name>', '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 <name>`, '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 <name> <target,paths>', '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 <name> <target1,target2,...>', '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 <name> <targets>|remove <name>|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;