aboutsummaryrefslogtreecommitdiff
path: root/modules/core/terminal.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core/terminal.js')
-rw-r--r--modules/core/terminal.js452
1 files changed, 338 insertions, 114 deletions
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index ee1afa4..2617f79 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -1,4 +1,184 @@
// Extracted from newOS4 index.html — WebX11Terminal
+ class WebX11SecurityManager {
+ constructor(terminal, level = 1) {
+ this.terminal = terminal;
+ this.level = level;
+ this.commandMeta = new Map();
+ this.lastRouteTime = 0;
+ this.lastRouteCmd = '';
+ this.blockedCount = 0;
+ this.destructiveCmds = new Set(['rm','delete','del','edit','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._registerBuiltIn();
+ }
+ _registerBuiltIn() {
+ const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','startup-modules','smart-prompt','cmd-allow','cmd-deny'];
+ internals.forEach(c => this.registerCommand(c, { category: 'builtin' }));
+ }
+ setLevel(n) {
+ if (n >= 0 && n <= 3) {
+ this.level = n;
+ this.terminal.print(`Security level set to ${n} (${this.getLevelName()})`, 'success');
+ if (n === 0) {
+ this.terminal.print('Warning: Permissive mode — commands will not be blocked or confirmed.', 'warning');
+ }
+ }
+ else { this.terminal.print('Level must be 0-3', 'error'); }
+ }
+ getLevel() { return this.level; }
+ getLevelName() {
+ return ['Permissive','Standard','Paranoid','Audit'][this.level] || 'Unknown';
+ }
+ registerCommand(name, meta = {}) {
+ this.commandMeta.set(name, { destructive: false, category: 'user', ...meta });
+ }
+ _collectAllCommands() {
+ const cmds = new Set([...this.commandMeta.keys()]);
+ for (const modName in this.terminal.moduleManager.modules) {
+ const mod = this.terminal.moduleManager.modules[modName];
+ if (mod.commands) {
+ for (const c in mod.commands) cmds.add(c);
+ }
+ }
+ const builtins = ['clear','help','ls','df','security','security-level'];
+ builtins.forEach(c => cmds.add(c));
+ return [...cmds];
+ }
+ _levenshtein(a, b) {
+ a = a.toLowerCase(); b = b.toLowerCase();
+ const m = a.length, n = b.length;
+ if (!m) return n; if (!n) return m;
+ let prev = new Uint16Array(n + 1);
+ let curr = new Uint16Array(n + 1);
+ for (let j = 0; j <= n; j++) prev[j] = j;
+ for (let i = 1; i <= m; i++) {
+ let next = new Uint16Array(n + 1);
+ next[0] = i;
+ for (let j = 1; j <= n; j++) {
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
+ next[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
+ }
+ [prev, curr] = [curr, next];
+ }
+ return prev[n];
+ }
+ suggest(input, maxDist = 2) {
+ const all = this._collectAllCommands();
+ const lower = input.toLowerCase();
+ let best = '', bestDist = Infinity;
+ for (const c of all) {
+ const d = this._levenshtein(lower, c);
+ if (d < bestDist && d <= maxDist) { bestDist = d; best = c; }
+ }
+ return best ? { cmd: best, dist: bestDist } : null;
+ }
+ _classify(raw) {
+ const parts = raw.trim().split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+ const args = parts.slice(1);
+ // Internal commands always allowed at terminal level, but security still classifies
+ const all = this._collectAllCommands();
+ const known = all.includes(cmd);
+ const hasSuspicious = args.some(a => this.suspiciousFlags.has(a.toLowerCase()));
+ const destructive = this.destructiveCmds.has(cmd) || hasSuspicious;
+ const system = this.systemCmds.has(cmd);
+ const unknown = !known;
+ return { cmd, args, known, destructive, system, unknown, hasSuspicious };
+ }
+ async checkAndRun(rawInput) {
+ const { cmd, args, known, destructive, system, unknown, hasSuspicious } = this._classify(rawInput);
+ // Level 3 Audit: just log and allow
+ if (this.level === 3) {
+ this.terminal.print(`[AUDIT] ${rawInput} → known=${known} destructive=${destructive} system=${system}`, 'info');
+ return this._dispatch(cmd, args);
+ }
+ // Always allow builtins that are safe, regardless of level
+ if (['help','security','security-level'].includes(cmd)) {
+ return this._dispatch(cmd, args);
+ }
+ // Unknown command handling
+ if (unknown && !this.whitelist.has(cmd)) {
+ this.blockedCount++;
+ const sug = this.suggest(cmd);
+ if (sug) {
+ this.terminal.print(`Security: Unknown command "${cmd}". Did you mean: "${sug.cmd}"?`, 'warning');
+ } else {
+ this.terminal.print(`Security: Unknown command "${cmd}" — blocked. Use 'security-level 0' to disable.`, 'warning');
+ }
+ // At levels 0-1: suggest/fix but still route to AI (terminal is also a chat interface)
+ // At level 2+: block entirely (paranoid)
+ if (this.level >= 2) {
+ this.terminal.print(`Security: Unknown command "${cmd}" blocked (level ${this.level}).`, 'error');
+ return;
+ }
+ if (this.level >= 1) {
+ if (sug) {
+ this.terminal.print(`Security: Did you mean "${sug.cmd}"? Routing to AI...`, 'warning');
+ } else {
+ this.terminal.print(`Security: Routing to AI...`, 'info');
+ }
+ }
+ return this._tryAIRoute(rawInput);
+ }
+ // Suspicious flags (e.g. --execute) at any level > 0
+ if (hasSuspicious && this.level > 0) {
+ this.terminal.print(`Security: Suspicious flag detected in "${rawInput}" — blocked.`, 'error');
+ this.blockedCount++;
+ return;
+ }
+ // Destructive commands
+ if (destructive) {
+ if (this.level === 0) {
+ this.terminal.print(`Warning: destructive command "${cmd}"`, 'warning');
+ return this._dispatch(cmd, args);
+ }
+ if (this.level >= 1) {
+ // Reuse your existing confirm UI if present, otherwise print confirm message
+ if (this.terminal.showConfirmationBar) {
+ this.terminal._pendingCommand = () => this._dispatch(cmd, args);
+ this.terminal.showConfirmationBar(`Run destructive: ${rawInput}`);
+ return;
+ } else {
+ this.terminal.print(`Security: Destructive command "${cmd}" requires confirmation.`, 'warning');
+ return;
+ }
+ }
+ }
+ // System commands at level >=1
+ if (system && this.level >= 1) {
+ this.terminal.print(`Security: System command "${cmd}" blocked (level ${this.level}).`, 'error');
+ this.blockedCount++;
+ return;
+ }
+ // Standard dispatch for everything else
+ return this._dispatch(cmd, args);
+ }
+ async _dispatch(cmd, args) {
+ return this.terminal.dispatchCommand(cmd, args);
+ }
+ async _tryAIRoute(rawInput) {
+ // AI routing cooldown guard
+ const now = Date.now();
+ if (this.lastRouteCmd === rawInput && (now - this.lastRouteTime) < 5000) {
+ this.terminal.print(`Security: Already routed "${rawInput}" to AI recently. Use 'security-level 0' to force.`, 'warning');
+ return;
+ }
+ this.lastRouteCmd = rawInput;
+ this.lastRouteTime = now;
+ try {
+ const aiMod = this.terminal.moduleManager.modules['WebX11AIAssistantModule'];
+ if (aiMod) {
+ this.terminal.print('→ Routing to AI assistant...', 'info');
+ await aiMod.chat([rawInput.trim()]);
+ }
+ } catch (e) {
+ this.terminal.print(`Security: AI error: ${e.message}`, 'error');
+ }
+ }
+ }
+
class WebX11Terminal {
constructor() {
this.outputContainer = document.getElementById('terminal-container');
@@ -15,6 +195,7 @@
}
} catch (e) { console.warn('SecretVault init:', e.message); }
this.moduleManager = new ModuleManager(this);
+ this.security = new WebX11SecurityManager(this, parseInt(localStorage.getItem('webx11_security_level')) || 1);
this.ollamaManager = new OllamaManager(this);
this._pipeInput = null;
this._pipeBufferSize = 5000;
@@ -267,6 +448,53 @@
this.outputContainer.appendChild(line);
this.scrollToBottom();
}
+ showConfirmationBar(desc) {
+ const area = document.getElementById('ai-confirm-area');
+ if (!area) {
+ this.print('Confirmation UI unavailable — command blocked.', 'error');
+ return;
+ }
+ this._pendingCommandDesc = desc;
+ area.innerHTML = `
+ <span class="confirm-desc">⚠️ ${desc}</span>
+ <button class="confirm-btn" id="cmd-confirm-allow" onclick="(function(){var a=document.getElementById('cmd-confirm-allow');if(a.__allow) a.__allow();}).call(this)">✅ Allow</button>
+ <button class="confirm-btn deny" id="cmd-confirm-deny" onclick="(function(){var d=document.getElementById('cmd-confirm-deny');if(d.__deny) d.__deny();}).call(this)">❌ Deny</button>
+ <span style="font-size:0.75em;color:var(--text-muted);width:100%;text-align:center">Type: cmd-allow or cmd-deny</span>
+ `;
+ area.classList.add('visible');
+ const allowBtn = document.getElementById('cmd-confirm-allow');
+ const denyBtn = document.getElementById('cmd-confirm-deny');
+ const onAllow = () => {
+ this._resolveCmdConfirm(true);
+ };
+ const onDeny = () => {
+ this._resolveCmdConfirm(false);
+ };
+ allowBtn.addEventListener('click', onAllow);
+ denyBtn.addEventListener('click', onDeny);
+ allowBtn.__allow = onAllow;
+ denyBtn.__deny = onDeny;
+ }
+ _resolveCmdConfirm(approved) {
+ const area = document.getElementById('ai-confirm-area');
+ if (area) {
+ area.classList.remove('visible');
+ area.innerHTML = '';
+ }
+ if (approved) {
+ this.print(`Allowed: ${this._pendingCommandDesc || 'command'}`, 'success');
+ if (this._pendingCommand) {
+ const fn = this._pendingCommand;
+ this._pendingCommand = null;
+ this._pendingCommandDesc = null;
+ fn();
+ }
+ } else {
+ this.print(`Denied: ${this._pendingCommandDesc || 'command'}`, 'warning');
+ this._pendingCommand = null;
+ this._pendingCommandDesc = null;
+ }
+ }
_loadHistory() {
try {
const saved = localStorage.getItem('webx11_terminal_output');
@@ -331,7 +559,7 @@
setTimeout(() => {
this._historySavePending = false;
try {
- const slice = this._historyBuffer.slice(-30);
+ const slice = this._historyBuffer.slice(-this._historyMaxLines);
localStorage.setItem('webx11_terminal_output', JSON.stringify(slice));
} catch (e) {
// Silently ignore storage errors (e.g. quota exceeded)
@@ -425,6 +653,55 @@
scrollToBottom() {
this.outputContainer.scrollTop = this.outputContainer.scrollHeight;
}
+ _getPathSuggestions(partial, type) {
+ // type: 'dir' = dirs only, 'file' = files + dirs (dirs get trailing /), 'session' = _sessions/*.json
+ if (type === 'session') {
+ this.fs.mkdirRecursive('_sessions');
+ const all = this.fs.ls('_sessions');
+ const names = all
+ .filter(e => e.type === 'file' && e.name.endsWith('.json'))
+ .map(e => e.name.replace('.json', ''));
+ return partial
+ ? names.filter(n => n.startsWith(partial)).map(n => n)
+ : names;
+ }
+ // Resolve the partial path to determine which directory to list
+ let dirPath = '';
+ let filterPrefix = '';
+ let entries = [];
+ let prefix = '';
+
+ if (partial.includes('/')) {
+ // "dir1/dir2" or "dir1/dir2/" or "dir1/"
+ const lastSlash = partial.lastIndexOf('/');
+ const beforeSlash = partial.slice(0, lastSlash); // dir1
+ filterPrefix = partial.slice(lastSlash + 1); // dir2 (or '' if ends with /)
+ // Resolve "dir1" relative to current path
+ const resolvedDir = this.fs._resolveDir(beforeSlash || '.');
+ if (!resolvedDir) return [];
+ entries = Object.keys(resolvedDir.content).map(name => ({
+ name,
+ type: resolvedDir.content[name].type,
+ size: resolvedDir.content[name].content ? resolvedDir.content[name].content.length : 0
+ }));
+ prefix = beforeSlash + '/';
+ } else {
+ // No slash — list current dir
+ entries = this.fs.ls();
+ filterPrefix = partial;
+ prefix = '';
+ }
+
+ if (type === 'dir') {
+ return entries
+ .filter(e => e.type === 'dir' && e.name.startsWith(filterPrefix))
+ .map(e => prefix + e.name + '/');
+ }
+ // 'file' — show files + dirs (dirs with trailing /)
+ return entries
+ .filter(e => e.name.startsWith(filterPrefix))
+ .map(e => e.type === 'dir' ? prefix + e.name + '/' : prefix + e.name);
+ }
showAutocomplete() {
const input = this.inputField.value;
const trimmed = input.trim();
@@ -442,11 +719,24 @@
const cmdPart = parts[0].toLowerCase();
const lowerInput = input.toLowerCase();
- // Commands that take file/dir arguments for autocomplete
- const fileArgCommands = ['cat', 'preview', 'rm', 'rmdir', 'edit', 'view'];
- const dirArgCommands = ['ls', 'cd'];
- // Session commands that autocomplete from _sessions/*.json
- const sessionArgCommands = ['session-load', 'session-delete', 'session-export', 'session-save'];
+ // Expanded command lists for autocomplete
+ const fileArgCommands = ['cat', 'preview', 'rm', 'rmdir', 'edit', 'view', 'cp', 'mv', 'touch', 'put', 'run', 'chmod', 'gzip', 'gunzip', 'encrypt', 'decrypt', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list'];
+ const dirArgCommands = ['ls', 'cd', 'mkdir', 'rmdir'];
+ const sessionArgCommands = ['session-load', 'session-delete', 'session-export', 'session-save', 'session-list', 'session-import'];
+ const pathArgCommands = ['cat', 'preview', 'rm', 'edit', 'view', 'cp', 'mv', 'touch', 'put', 'run', 'chmod', 'gzip', 'gunzip', 'encrypt', 'decrypt', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list', 'ls', 'cd', 'mkdir', 'rmdir', 'cat', 'preview'];
+
+ // Collect commands that take arguments from ext module metadata
+ const extFileCmds = new Set();
+ const extDirCmds = new Set();
+ for (const modName in this.moduleManager.modules) {
+ const mod = this.moduleManager.modules[modName];
+ if (mod.commandsMeta) {
+ for (const [c, meta] of Object.entries(mod.commandsMeta)) {
+ if (meta.argType === 'file' || meta.argType === 'path') extFileCmds.add(c);
+ if (meta.argType === 'dir') extDirCmds.add(c);
+ }
+ }
+ }
// If we have more than one word, we're past the command name — autocomplete file/dir/session names
if (parts.length > 1 && parts[1].length > 0) {
@@ -458,25 +748,13 @@
return;
}
- // Determine if we should show session names, dirs only, or both
if (sessionArgCommands.includes(cmdPart)) {
- this.fs.mkdirRecursive('_sessions');
- const all = this.fs.ls('_sessions');
- this.suggestions = all
- .filter(e => e.type === 'file' && e.name.endsWith('.json'))
- .map(e => e.name.replace('.json', ''))
- .filter(n => n.startsWith(partialTrimmed));
- } else if (cmdPart === 'cd' || cmdPart === 'rmdir' || dirArgCommands.includes(cmdPart)) {
- // Dir-only commands
- const entries = this.fs.ls().filter(f => f.type === 'dir');
- this.suggestions = entries
- .filter(e => e.name.startsWith(partialTrimmed))
- .map(e => e.name + '/');
- } else if (fileArgCommands.includes(cmdPart)) {
- const all = this.fs.ls();
- this.suggestions = all
- .filter(e => e.name.startsWith(partialTrimmed))
- .map(e => e.type === 'dir' ? e.name + '/' : e.name);
+ this.suggestions = this._getPathSuggestions(partialTrimmed, 'session');
+ } else if (dirArgCommands.includes(cmdPart) || extDirCmds.has(cmdPart)) {
+ this.suggestions = this._getPathSuggestions(partialTrimmed, 'dir');
+ this._isCdAutocomplete = true;
+ } else if (fileArgCommands.includes(cmdPart) || extFileCmds.has(cmdPart)) {
+ this.suggestions = this._getPathSuggestions(partialTrimmed, 'file');
} else {
this.hideAutocomplete();
return;
@@ -484,7 +762,6 @@
this._autocompletePrefix = cmdPart + ' ';
if (this.suggestions.length > 0) {
- this._isCdAutocomplete = (cmdPart === 'cd' || cmdPart === 'rmdir');
this.renderAutocomplete();
} else {
this.hideAutocomplete();
@@ -495,24 +772,18 @@
// Just typed a space after command — show all matching entries
if (parts.length > 1) {
if (sessionArgCommands.includes(cmdPart)) {
- this.fs.mkdirRecursive('_sessions');
- const all = this.fs.ls('_sessions');
- this.suggestions = all
- .filter(e => e.type === 'file' && e.name.endsWith('.json'))
- .map(e => e.name.replace('.json', ''));
- } else if (cmdPart === 'cd' || cmdPart === 'rmdir' || dirArgCommands.includes(cmdPart)) {
- const dirs = this.fs.ls().filter(f => f.type === 'dir');
- this.suggestions = dirs.map(d => d.name + '/');
- } else if (fileArgCommands.includes(cmdPart)) {
- const all = this.fs.ls();
- this.suggestions = all.map(e => e.type === 'dir' ? e.name + '/' : e.name);
+ this.suggestions = this._getPathSuggestions('', 'session');
+ } else if (dirArgCommands.includes(cmdPart) || extDirCmds.has(cmdPart)) {
+ this.suggestions = this._getPathSuggestions('', 'dir');
+ this._isCdAutocomplete = true;
+ } else if (fileArgCommands.includes(cmdPart) || extFileCmds.has(cmdPart)) {
+ this.suggestions = this._getPathSuggestions('', 'file');
} else {
this.hideAutocomplete();
return;
}
this._autocompletePrefix = cmdPart + ' ';
if (this.suggestions.length > 0) {
- this._isCdAutocomplete = (cmdPart === 'cd' || cmdPart === 'rmdir');
this.renderAutocomplete();
} else {
this.hideAutocomplete();
@@ -520,10 +791,9 @@
return;
}
- // Directory autocomplete for bare 'cd ' (no partial yet) — should be unreachable due to above block
+ // Directory autocomplete for bare 'cd ' (no partial yet)
if (lowerInput.startsWith('cd ')) {
- const dirs = this.fs.ls().filter(f => f.type === 'dir');
- this.suggestions = dirs.map(d => d.name + '/');
+ this.suggestions = this._getPathSuggestions('', 'dir');
if (this.suggestions.length > 0) {
this._isCdAutocomplete = true;
this.renderAutocomplete();
@@ -623,15 +893,17 @@
catch (e) { console.warn(`[SafeRegister] Skipped ${label}: ${e.message}`); }
}
async _loadExtModules(filterKeys) {
- const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder'];
+ const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell'];
+ const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
try {
- const res = await fetch('packages.json');
+ const res = await fetch('packages.json?v=' + cacheVer);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const index = await res.json();
for (const key of autoLoadKeys) {
const pkg = index.packages[key];
if (pkg) {
- await this.moduleManager.loadModuleFromURL(pkg.url);
+ const url = pkg.url + (pkg.url.includes('?') ? '&v=' : '?v=') + cacheVer;
+ await this.moduleManager.loadModuleFromURL(url);
}
}
} catch (err) {
@@ -640,7 +912,7 @@
}
async reloadPersistedModules() {
const registry = this.moduleManager.moduleRegistry;
- const skipList = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder'];
+ const skipList = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell'];
for (const mod of registry) {
if (mod.enabled && mod.url) {
// Skip ext modules — they load via _loadExtModules
@@ -659,65 +931,8 @@
await this.executePipeChain(pipeParts);
return;
}
- const parts = rawInput.trim().split(/\s+/);
- const cmd = parts[0].toLowerCase();
- const args = parts.slice(1);
- if (cmd === 'startup-modules') {
- this.openStartupModules();
- return;
- }
- if (cmd === 'font-size' || cmd === 'setfont') {
- this.setFontSize(args[0]);
- return;
- }
- if (cmd === 'load-local' || cmd === 'loadlocal') {
- this.loadLocalModule(args[0]);
- return;
- }
- if (cmd === 'smart-prompt') {
- const val = args[0] && args[0] === 'off' ? 'off' : 'on';
- localStorage.setItem('webx11_smart_prompt', val);
- this.print('Smart prompt ' + val, 'success');
- this.updatePrompt();
- return;
- }
- if (cmd === 'mem') {
- this.printMemoryStats();
- return;
- }
- if (cmd === 'clear') {
- this.outputContainer.innerHTML = '';
- this._historyBuffer = [];
- localStorage.removeItem('webx11_terminal_output');
- return;
- }
- let executed = false;
- for (const modName in this.moduleManager.modules) {
- const mod = this.moduleManager.modules[modName];
- if (mod.commands && mod.commands[cmd]) {
- try {
- await mod.commands[cmd](args);
- executed = true;
- } catch (err) {
- this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error');
- }
- break;
- }
- }
- if (!executed) {
- // Route unknown commands to the AI agent
- try {
- const aiMod = this.moduleManager.modules['WebX11AIAssistantModule'];
- if (aiMod) {
- this.print('→', 'info');
- await aiMod.chat([rawInput.trim()]);
- return;
- }
- } catch (e) {
- this.print(`Agent error: ${e.message}`, 'error');
- return;
- }
- }
+ // Security gate for all single commands and internal builtins
+ await this.security.checkAndRun(rawInput);
}
splitPipe(input) {
const parts = [];
@@ -761,6 +976,25 @@
}
async dispatchCommand(cmd, args) {
if (cmd === 'clear') { this.outputContainer.innerHTML = ''; this._historyBuffer = []; localStorage.removeItem('webx11_terminal_output'); return; }
+ if (cmd === 'startup-modules') { this.openStartupModules(); return; }
+ if (cmd === 'font-size' || cmd === 'setfont') { this.setFontSize(args[0]); return; }
+ if (cmd === 'load-local' || cmd === 'loadlocal') { this.loadLocalModule(args[0]); return; }
+ if (cmd === 'smart-prompt') { const val = args[0] && args[0] === 'off' ? 'off' : 'on'; localStorage.setItem('webx11_smart_prompt', val); this.print('Smart prompt ' + val, 'success'); this.updatePrompt(); return; }
+ if (cmd === 'mem') { this.printMemoryStats(); return; }
+ if (cmd === 'security') {
+ this.print(`Security level: ${this.security.getLevel()} (${this.security.getLevelName()})`, 'info');
+ this.print(`Blocked commands: ${this.security.blockedCount}`, 'info');
+ return;
+ }
+ if (cmd === 'security-level') {
+ const lv = parseInt(args[0]);
+ if (isNaN(lv)) { this.print('Usage: security-level 0|1|2|3', 'warning'); return; }
+ this.security.setLevel(lv);
+ localStorage.setItem('webx11_security_level', lv);
+ return;
+ }
+ if (cmd === 'cmd-allow') { this._resolveCmdConfirm(true); return; }
+ if (cmd === 'cmd-deny') { this._resolveCmdConfirm(false); return; }
for (const modName in this.moduleManager.modules) {
const mod = this.moduleManager.modules[modName];
if (mod.commands && mod.commands[cmd]) {
@@ -768,17 +1002,7 @@
catch (err) { this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); return; }
}
}
- // Route unknown commands to the AI agent
- try {
- const aiMod = this.moduleManager.modules['WebX11AIAssistantModule'];
- if (aiMod) {
- this.print('→ Routing to AI assistant...', 'info');
- await aiMod.chat([cmd + ' ' + args.join(' ')]);
- return;
- }
- } catch (e) {
- this.print(`Agent error: ${e.message}`, 'error');
- }
+ // No AI routing here — security manager handles that via checkAndRun
this.print(`Command not found: ${cmd}`, 'error');
}
openFileBrowser() {