// Extracted from newOS4 index.html β€” WebX11AIAssistantModule (Full Agent Loop) class WebX11AIAssistantModule { constructor() { this.commands = {}; this.terminal = null; this.systemPrompt = this.buildSystemPrompt(); this.context = []; this.maxContext = 10; this.maxContextTokens = 2000; this.MAX_TOOL_TURNS = 8; this._pendingConfirmation = null; this._confirmResolve = null; this._errorLog = []; this._commandFreq = {}; this._messageCount = 0; this._userProfile = {}; this._sessionMemoryLoaded = false; this._sessionSaved = false; this._thinkingTimer = null; this._thinkingStartTime = 0; this._validatorSuppressed = false; this._toolDefs = null; this._toolCallLog = []; this._debugPanelEnabled = false; // πŸ”’ Guardrail Layer this._guardrailPolicy = null; this._shadowSnapshot = null; this._guardrailViolations = {}; this._confirmTimeoutMs = 120000; this._skillDebounceMs = 1800000; this._chatLockTimeoutMs = 10000; this._loadGuardrailPolicy(); // Restore AI config from localStorage (maxContext, MAX_TOOL_TURNS, etc.) try { const cfg = localStorage.getItem('webx11_ai_config'); if (cfg) { const p = JSON.parse(cfg); if (p.maxTurns) this.MAX_TOOL_TURNS = p.maxTurns; if (p.maxContext) this.maxContext = p.maxContext; if (p.maxContextTokens) this.maxContextTokens = p.maxContextTokens; if (p.confirmTimeoutMs) this._confirmTimeoutMs = p.confirmTimeoutMs; if (p.skillDebounceMs) this._skillDebounceMs = p.skillDebounceMs; if (p.chatLockTimeoutMs) this._chatLockTimeoutMs = p.chatLockTimeoutMs; } } catch (e) {} try { const s = localStorage.getItem('webx11_ai_patterns'); if (s) this._commandFreq = JSON.parse(s); } catch (e) {} try { const p = localStorage.getItem('webx11_user_profile'); if (p) this._userProfile = JSON.parse(p); } catch (e) {} // Restore AI conversation context from localStorage try { const saved = localStorage.getItem('webx11_ai_context'); if (saved) { const parsed = JSON.parse(saved); if (parsed && Array.isArray(parsed.context)) { this.context = parsed.context; this._messageCount = parsed.messageCount || 0; } } } catch (e) {} } 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.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); const nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule; let nvfsNote = ''; try { if (nvfs && nvfs.lastScan && nvfs.lastScan !== '(loaded from disk)') { const ageMs = Date.now() - new Date(nvfs.lastScan).getTime(); if (ageMs > 14400000) nvfsNote = ' |nvfs ' + Math.floor(ageMs/3600000) + 'h stale'; } } catch (e) {} return 'Current date: ' + dateStr + nvfsNote + '\n' + 'VFSβ†’Modulesβ†’AI. nvfs-index for commands. Toolβ†’Observeβ†’Answer. nvfs-resolve. Destructive cmds need confirm.\n' + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills\n' + 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n' + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* β€” discoverable via help'; } getToolDefinitions() { if (!this._toolDefs) this._toolDefs = this._buildToolDefs(); return this._toolDefs; } _buildToolDefs() { return [{ type: 'function', function: { name: 'list_files', description: 'List VFS directory contents', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Dir path ("" for root)' } }, required: [] } } }, { type: 'function', function: { name: 'read_file', description: 'Read VFS file', parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path' } }, required: ['path'] } } }, { type: 'function', function: { name: 'write_file', description: 'Write file (DESTRUCTIVE)', parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, content: { type: 'string', description: 'Content' } }, required: ['path', 'content'] } } }, { type: 'function', function: { name: 'make_directory', description: 'Create VFS directory', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Dir name' } }, required: ['path'] } } }, { type: 'function', function: { name: 'delete_file', description: 'Delete file or empty dir (DESTRUCTIVE)', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Path to delete' } }, required: ['path'] } } }, { type: 'function', function: { name: 'get_status', description: 'System status: FS, modules, memory', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'get_help', description: 'Command reference', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'web_search', description: 'Search web via SearXNG + DDG', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search query' } }, required: ['query'] } } }, { type: 'function', function: { name: 'web_fetch', description: 'Fetch & extract URL content. Supports GET (default) and POST (for APIs like Hyperliquid /info).', parameters: { type: 'object', properties: { url: { type: 'string', description: 'Full URL' }, method: { type: 'string', enum: ['GET', 'POST'], description: 'HTTP method (default GET)' }, body: { type: 'string', description: 'Request body for POST (JSON string, max 10KB)' }, content_type: { type: 'string', description: 'Content-Type header (default application/json for POST)' } }, required: ['url'] } } }, { type: 'function', function: { name: 'wpm_install', description: 'Install WPM package', parameters: { type: 'object', properties: { package: { type: 'string', description: 'Package name' } }, required: ['package'] } } }, { type: 'function', function: { name: 'wpm_search', description: 'Search WPM packages', parameters: { type: 'object', properties: { keyword: { type: 'string', description: 'Keyword' } }, required: ['keyword'] } } }, { type: 'function', function: { name: 'run_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'] } } }, { type: 'function', function: { name: 'get_current_dir', description: 'Current VFS directory', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'view_module', description: 'View module source code', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } } }, { type: 'function', function: { name: 'edit_module', description: 'Edit module source (DESTRUCTIVE)', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } } }, { type: 'function', function: { name: 'test_module', description: 'Test edited module from /_mods/', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } } }, { type: 'function', function: { name: 'reload_module', description: 'Reload module from original URL', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } } }, { type: 'function', function: { name: 'list_modified_modules', description: 'List modules with local edits', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'get_module_status', description: 'Module status: loaded, modified, URLs', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'load_skills', description: 'Search skills relevant to task', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Task description' } }, required: ['query'] } } }, { type: 'function', function: { name: 'create_skill', description: 'Save a new skill', parameters: { type: 'object', properties: { name: { type: 'string', description: 'kebab-case name' }, description: { type: 'string', description: 'One-line description' }, content: { type: 'string', description: 'Markdown with YAML frontmatter' }, tags: { type: 'string', description: 'Comma-separated' }, category: { type: 'string', description: 'e.g. research, system, dev' } }, required: ['name', 'description', 'content'] } } }, { type: 'function', function: { name: 'patch_skill', description: 'Replace skill content', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Skill name' }, content: { type: 'string', description: 'New markdown content' } }, required: ['name', 'content'] } } }, { type: 'function', function: { name: 'list_skills', description: 'Browse skills (optional filter)', parameters: { type: 'object', properties: { category: { type: 'string', description: 'Filter by category' } }, required: [] } } }, { type: 'function', function: { name: 'check_updates', description: 'Check module URLs are reachable', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'user_get', description: 'Read user profile key', parameters: { type: 'object', properties: { key: { type: 'string', description: 'e.g. preferredModel' } }, required: ['key'] } } }, { type: 'function', function: { name: 'user_set', description: 'Write user profile key', parameters: { type: 'object', properties: { key: { type: 'string', description: 'Profile key' }, value: { type: 'string', description: 'Value' } }, required: ['key', 'value'] } } }, { type: 'function', function: { name: 'user_list', description: 'List stored profile keys', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'user_forget', description: 'Delete profile key', parameters: { type: 'object', properties: { key: { type: 'string', description: 'Key to delete' } }, required: ['key'] } } }, { type: 'function', function: { name: 'session_save', description: 'Save session summary', parameters: { type: 'object', properties: { summary: { type: 'string', description: 'What was accomplished' } }, required: ['summary'] } } }, { type: 'function', function: { name: 'session_load', description: 'Load past session by date', parameters: { type: 'object', properties: { date: { type: 'string', description: 'YYYY-MM-DD' } }, required: ['date'] } } }, { type: 'function', function: { name: 'session_recent', description: 'Show recent sessions', parameters: { type: 'object', properties: { count: { type: 'number', description: 'Count (default 3)' } }, required: [] } } }, { type: 'function', function: { name: 'session_terminal', description: 'Read last terminal output', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'session_stats', description: 'Session aggregate stats', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'list_agents', description: 'List registered agent IDs, status, and capabilities', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'delegate_task', description: '(DESTRUCTIVE) Delegate a task to a worker agent. Auto-spawns if needed.', parameters: { type: 'object', properties: { task: { type: 'string', description: 'The task description to delegate' } }, required: ['task'] } } }, { type: 'function', function: { name: 'spawn_agent', description: '(DESTRUCTIVE) Spawn a new worker agent with a role and capabilities', parameters: { type: 'object', properties: { role: { type: 'string', description: 'Agent role (e.g. researcher, writer)' }, capabilities: { type: 'string', description: 'Comma-separated capabilities (optional)' } }, required: ['role'] } } }, { type: 'function', function: { name: 'nvfs_resolve', description: 'Search VFS files by semantic concept using Neural VFS index. Call nvfs-index first to build the index.', parameters: { type: 'object', properties: { concept: { type: 'string', description: 'Concept to search (e.g. skill, memory, module, webdav, security)' } }, required: ['concept'] } } }, { type: 'function', function: { name: 'list_available_modules', description: 'List all available modules with metadata (core/ext, commands, class name, layer). Filter by type with type=core or type=ext.', parameters: { type: 'object', properties: { type: { type: 'string', description: 'Optional: filter to "core" or "ext"' } }, required: [] } } }, { type: 'function', function: { name: 'smart_context_list', description: 'List available AI context profiles (e.g. coding, debug, creative, technical). Returns profile names and system prompts.', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'smart_context_set', description: "Set the active AI context profile. Changes the AI's behavior (temperature, style) for subsequent responses.", parameters: { type: 'object', properties: { name: { type: 'string', description: 'Profile name (e.g. "coding", "debug", "creative", "technical", "chat")' } }, required: ['name'] } } }, { type: 'function', function: { name: 'create_mindmap', description: 'Create a mindmap with the given topic and child nodes, then render it as inline SVG in the terminal. Creates root and all children in one call. Use this when the user asks for a mindmap or visual architecture.', parameters: { type: 'object', properties: { topic: { type: 'string', description: 'Root topic of the mindmap (e.g. "WebX11 Architecture")' }, children: { type: 'string', description: 'Comma-separated list of child node names (e.g. "AI Engine, VFS System, Modules")' } }, required: ['topic'] } } }]; } getHelpInfo() { return { 'ai': 'ai β€” Ask the AI assistant anything', 'ask': 'ask β€” Alias for ai command', 'ai-clear': 'ai-clear β€” Clear conversation context', 'ai-system': 'ai-system β€” Set system prompt', 'ai-context': 'ai-context β€” Show current conversation context', 'ai-allow': 'ai-allow β€” Confirm pending AI action', 'ai-deny': 'ai-deny β€” Deny pending AI action', '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))', 'mem-debug': 'mem-debug β€” Show memory breakdown by component', 'reset-mem': 'reset-mem β€” Reset AI memory (logs, patterns) without losing VFS or config' }; } registerCommands() { this.commands['ai'] = (args) => this.chat(args); this.commands['ask'] = (args) => this.chat(args); this.commands['ai-clear'] = () => this.clearContext(); this.commands['ai-system'] = (args) => this.setSystemPrompt(args); this.commands['ai-context'] = () => this.showContext(); this.commands['ai-allow'] = () => this.confirmPending(); this.commands['ai-deny'] = () => this.denyPending(); this.commands['ai-ignore'] = () => this.ignoreValidator(); this.commands['ai-stop'] = () => this.cancelAi(); this.commands['ai-model'] = (args) => this.showModel(args); this.commands['ai-compress'] = () => this._manualCompress(); this.commands['ai-config'] = (args) => this.aiConfig(args); this.commands['debug'] = () => this.toggleDebug(); this.commands['guardrail'] = (args) => this.guardrailCmd(args); this.commands['guardrail-add'] = (args) => this.guardrailCmd(['add', ...args]); this.commands['guardrail-remove'] = (args) => this.guardrailCmd(['remove', ...args]); 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(' '); if (!userMessage) { this.terminal.print('Usage: ai ', 'warning'); return; } this._cancelled = false; this.terminal.setAiBusy(true); const lockStart = Date.now(); while (this._chatLock) { if (Date.now() - lockStart > this._chatLockTimeoutMs) { this._chatLock = false; this.terminal.print('⚠️ Chat lock timed out β€” force released', 'warning'); break; } await new Promise(r => setTimeout(r, 50)); } this._chatLock = true; this.terminal.markDirty(); // Re-read context from localStorage (auto-restore may have written it after init) this._loadContext(); // Compress context if it exceeds the token threshold this._compressContext(); let loopResult; try { const messages = [ { role: 'system', content: this.systemPrompt } ]; this.context.slice().forEach(msg => messages.push(msg)); messages.push({ role: 'user', content: userMessage }); this.terminal.print('πŸ€– WebX11AI thinking...', 'info'); // Auto-load skills relevant to the user's query (awaited, not fire-and-forget) // Skip skill loading for trivial queries (< 10 chars, no actionable keywords) const optEnabled = this.terminal?.ollamaManager?.optimizationEnabled !== false; const isTrivial = optEnabled && userMessage.length < 10 && !/\b(search|write|install|edit|create|make|find|delete|remove|change|update|improve|add|fix|build|set|load|save|help)\b/i.test(userMessage); if (!isTrivial) { try { const skillRes = await this.runTool('load_skills', { query: userMessage }); const skillData = JSON.parse(skillRes); if (skillData && skillData.matches && skillData.matches.length > 0) { const skillNotes = skillData.matches.map(m => `[Skill: ${m.name}] ${m.content}`).join('\n\n---\n\n'); messages.push({ role: 'system', content: `Relevant skills for this task:\n\n${skillNotes}\n\nUse the steps above as guidance. If they're wrong or outdated, use patch_skill to fix them after the task.` }); this.terminal.print(`πŸ“š Loaded ${skillData.count} relevant skill(s)`, 'info'); } } catch (e) { /* skills not available yet β€” first run */ } } // Track conversation patternsfor learning this._trackPattern(userMessage); // Extract facts from user message this._extractFacts(userMessage, ''); // On first message of session, auto-load recent memory if (!this._sessionMemoryLoaded) { this._sessionMemoryLoaded = true; this.terminal.fs.mkdirRecursive('/memory'); try { const recentRes = await this.runTool('session_recent', { count: 3 }); const rd = JSON.parse(recentRes); if (rd.sessions && rd.sessions.length > 0) { const memNotes = rd.sessions.map(s => '[' + s.date + '] ' + (s.tasks || 1) + ' tasks: ' + (s.summary || 'No summary')).join('\n'); messages.push({ role: 'system', content: 'Recent sessions:\n' + memNotes + '\nThis provides continuity. Refer to past work when relevant.' }); this.terminal.print('πŸ“– Loaded ' + rd.sessions.length + ' recent session(s)', 'info'); } } catch (e) {} } // Inject memory graph context for cross-session awareness try { const memgraph = this.terminal.moduleManager.modules['MemoryGraphModule']; if (memgraph) { const mctx = memgraph.getContextForPrompt(); if (mctx) { messages.push({ role: 'system', content: mctx }); } } } catch (e) { /* memory graph not available */ } // Inject world model context for system self-awareness try { const wm = this.terminal.moduleManager.modules['WorldModelModule']; if (wm) { const wctx = wm.getContextForPrompt(); if (wctx) { messages.push({ role: 'system', content: wctx }); } } } catch (e) { /* world model not available */ } loopResult = await this.agenticLoop(messages); this.context.push({ role: 'user', content: userMessage }); if (loopResult.finalAnswer) { this.context.push({ role: 'assistant', content: loopResult.finalAnswer }); } if (this.context.length > this.maxContext * 2) { this.context = this.context.slice(-this.maxContext * 2); } // Reflection: after complex tasks, run health scan and auto-skill creation if (loopResult.toolTurns >= 2 && !this._cancelled) { await this.afterActionReflection(userMessage, loopResult.toolTurns); } // Auto-create skills from repeated errors if (this._errorLog.length >= 3) { await this._autoCreateErrorSkill(); } // Skill quality audit every 20 messages this._messageCount++; if (this._messageCount % 20 === 0) { await this._auditSkills(); } // Sound notification on AI response if (this.terminal.playNotificationSound) { setTimeout(() => this.terminal.playNotificationSound(), 100); } } finally { this._chatLock = false; this.terminal.setAiBusy(false); // Persist conversation context after each exchange this._saveContext(); // Log the AI interaction this.terminal.log('INFO', 'AI', 'Query: ' + userMessage.substring(0, 100)); if (loopResult && loopResult.toolTurns) { this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s)'); } } // Register session save on page unload (runs once per session) if (!this._sessionSaved) { this._sessionSaved = true; window.addEventListener('beforeunload', () => { this.terminal.fs.mkdirRecursive('/memory'); const today = new Date().toISOString().split('T')[0]; const index = this.terminal.fs.fsReadJSON('/memory/index.json') || {}; index[today] = index[today] || { summary: 'Session in progress', tasks: 0 }; index[today].tasks = (index[today].tasks || 0) + 1; index[today].updated = new Date().toISOString(); this.terminal.fs.fsWriteJSON('/memory/index.json', index); // Also persist AI context on page close this._saveContext(); }); } } _summarizeObservation(result, maxLen) { if (!result || typeof result !== 'string' || result.length <= maxLen) return result || ''; const lines = result.split('\n'); if (lines.length > 5) { return lines.slice(0, 2).join('\n') + '\n...(' + (lines.length - 4) + ' more lines)...\n' + lines.slice(-2).join('\n'); } return result.substring(0, maxLen) + '...(truncated from ' + result.length + ' chars)'; } _escapeHtml(str) { return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } _updateDebugPanel() { if (!this._debugPanelEnabled) return; const panel = document.getElementById('debug-panel'); if (!panel) return; const recent = this._toolCallLog.slice(-20); let html = ''; for (const entry of recent) { const statusClass = entry.error ? 'debug-entry-error' : 'debug-entry-ok'; const argsStr = typeof entry.args === 'string' ? entry.args.substring(0, 60) : JSON.stringify(entry.args).substring(0, 60); html += '
' + '#' + entry.turn + ' ' + '' + this._escapeHtml(entry.name) + ' ' + '' + this._escapeHtml(argsStr) + ' ' + '' + entry.duration + 's' + (entry.error ? ' ⚠️' : '') + '
'; } panel.innerHTML = '
' + 'πŸ”§ Tool Log ' + this._toolCallLog.length + ' calls' + '' + '' + '' + '' + 'βœ•' + '
' + html; } _isToolError(result) { if (!result || typeof result !== 'string') return null; const trimmed = result.trim(); if (trimmed.startsWith('{"error":') || trimmed.startsWith("{'error':")) { try { const parsed = JSON.parse(trimmed); return parsed.error || null; } catch (e) { return null; } } return null; } _trackToolFailure(toolName, error) { try { if (!this._toolFailuresBuffer) this._toolFailuresBuffer = {}; if (!this._toolFailuresBuffer[toolName]) this._toolFailuresBuffer[toolName] = { count: 0, last: '', errors: [] }; const entry = this._toolFailuresBuffer[toolName]; entry.count++; entry.last = new Date().toISOString(); entry.errors.push(error); if (entry.errors.length > 5) entry.errors.shift(); if (entry.count >= 3) { this.terminal.print(`⚠️ Tool '${toolName}' has failed ${entry.count} times. Consider creating a workaround skill.`, 'warning'); } if (this._toolFailureFlushTimer) clearTimeout(this._toolFailureFlushTimer); this._toolFailureFlushTimer = setTimeout(() => this._flushToolFailures(), 2000); } catch (e) { /* ignore */ } } _flushToolFailures() { try { if (!this._toolFailuresBuffer || Object.keys(this._toolFailuresBuffer).length === 0) return; const key = 'webx11_tool_failures'; const raw = localStorage.getItem(key) || '{}'; const map = JSON.parse(raw); for (const [toolName, entry] of Object.entries(this._toolFailuresBuffer)) { if (!map[toolName]) map[toolName] = { count: 0, last: '', errors: [] }; map[toolName].count += entry.count; map[toolName].last = entry.last; map[toolName].errors.push(...entry.errors); if (map[toolName].errors.length > 10) map[toolName].errors = map[toolName].errors.slice(-10); } localStorage.setItem(key, JSON.stringify(map)); this._toolFailuresBuffer = {}; } catch (e) { /* ignore */ } } async agenticLoop(messages) { let toolTurns = 0; while (toolTurns < this.MAX_TOOL_TURNS) { if (this._cancelled) { this.terminal.print('AI loop cancelled', 'warning'); return { finalAnswer: null, toolTurns }; } toolTurns++; // On last turn, inject wrap-up prompt if (toolTurns === this.MAX_TOOL_TURNS) { this.terminal.print(`⏱ Final turn (${toolTurns}/${this.MAX_TOOL_TURNS}) β€” AI must provide final answer`, 'warning'); messages.push({ role: 'system', content: 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.' }); } const thinkStart = Date.now(); const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions()); const thinkTime = Math.round((Date.now() - thinkStart) / 1000); if (this._cancelled) return { finalAnswer: null, toolTurns }; if (res.error) { this.terminal.print(`⚠️ AI model error (${thinkTime}s): ${res.error}`, 'error'); return { finalAnswer: null, toolTurns }; } if (res.message && !res.tool_calls) { const text = res.message.content || ''; this.terminal.printAIResponse(text || '(no response)'); if (text) this.terminal.ollamaManager.fireResponseHooks(text); this.terminal.print(`πŸ’¬ AI answered (turn ${toolTurns}/${this.MAX_TOOL_TURNS}, ${thinkTime}s)`, 'info'); return { finalAnswer: text, toolTurns }; } if (res.tool_calls && res.tool_calls.length > 0) { 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, 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; const t0 = Date.now(); const toolResult = await this.runTool(fn.name, fn.arguments); const t1 = Math.round((Date.now() - t0) / 1000); this._toolCallLog.push({ turn: toolTurns, name: fn.name, args: fn.arguments, result: toolResult, 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)); if (this._debugPanelEnabled) this._updateDebugPanel(); return { fn, toolResult, t1 }; })); for (const { fn, toolResult, t1 } of safeResults) { this.terminal.print(`πŸ”§ Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info'); // Memory graph: record tool call + file access try { const _mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule']; if (_mg) { _mg.recordToolCall(fn.name, !this._isToolError(toolResult)); if (fn.name === 'read_file' || fn.name === 'write_file' || fn.name === 'view_module' || fn.name === 'edit_module') { const _path = (typeof fn.arguments === 'object' ? (fn.arguments?.path || '') : ''); if (_path) _mg.recordFileAccess(_path); } } } catch (_e) { /* memory graph not available */ } const obs = this._summarizeObservation(toolResult, 150); const err = this._isToolError(toolResult); if (err) { this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${err} (${t1}s)`); this._trackToolFailure(fn.name, err); this.terminal.print(`πŸ“Š Result (${t1}s): ⚠️ Error: ${err}`, 'warning'); } else if (toolResult && toolResult.length > 150) { this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`); const safeId = 'toolresult-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6); this.terminal.printHTML(`πŸ“Š Result (${t1}s): ${this._escapeHtml(obs)} [+] show full (${toolResult.length} chars)`); } else { this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`); this.terminal.print(`πŸ“Š Result (${t1}s): ${obs}`, 'info'); } messages.push({ role: 'tool', content: toolResult }); } // Only block on destructive tools if (destructiveTools.length > 0) { // 🎯 CONFIDENCE ESCALATION β€” auto-approve only if confidence >= 90% let autoApprove = false; if (pendingText) { const confidence = this._parseConfidence(pendingText); if (confidence !== null && confidence >= 90) { autoApprove = true; this.terminal.print(`🎯 AI confidence: ${confidence}% β€” auto-approved`, 'success'); } else if (confidence !== null) { this.terminal.print(`🎯 AI confidence: ${confidence}% β€” below 90% threshold, requiring manual approval`, 'warning'); } else { this.terminal.print('🎯 No confidence declared for destructive action β€” requiring manual approval', 'warning'); } } const approved = autoApprove || await this.confirmActionMulti(destructiveTools); if (approved) { for (const tc of destructiveTools) { const fn = tc.function; const t0 = Date.now(); this.terminal.print(`πŸ”§ Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info'); const toolResult = await this.runTool(fn.name, fn.arguments); const t1 = Math.round((Date.now() - t0) / 1000); this._toolCallLog.push({ turn: toolTurns, name: fn.name, args: fn.arguments, result: toolResult, 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)); if (this._debugPanelEnabled) this._updateDebugPanel(); // Memory graph: record destructive tool call try { const _mg2 = this.terminal?.moduleManager?.modules?.['MemoryGraphModule']; if (_mg2) { _mg2.recordToolCall(fn.name, !this._isToolError(toolResult)); if (fn.name === 'write_file' || fn.name === 'edit_module') { const _path2 = (typeof fn.arguments === 'object' ? (fn.arguments?.path || '') : ''); if (_path2) _mg2.recordFileAccess(_path2); } } } catch (_e2) { /* memory graph not available */ } const dObs = this._summarizeObservation(toolResult, 150); const dErr = this._isToolError(toolResult); if (dErr) { this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${dErr} (${t1}s)`); this._trackToolFailure(fn.name, dErr); this.terminal.print(`πŸ“Š Result (${t1}s): ⚠️ Error: ${dErr}`, 'warning'); } else { this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`); this.terminal.print(`πŸ“Š Result (${t1}s): ${dObs}`, 'info'); } messages.push({ role: 'tool', content: toolResult }); } } else { messages.push({ role: 'tool', content: `Destructive actions (${destructiveTools.map(tc => tc.function.name).join(', ')}) denied by user. Non-destructive actions already executed.` }); } } // Validator: check safe tool results for consistency if (safeResults.length > 0 && pendingText && !this._validatorSuppressed) { const om = this.terminal.ollamaManager; if (om && om.validatorEnabled && om.validatorModel) { let invalidTools = []; for (const { fn, toolResult } of safeResults) { const valid = await om.validateToolResult(fn.name, fn.arguments, toolResult, pendingText || ''); if (!valid && !this._cancelled) { invalidTools.push(fn.name); this.terminal.print('πŸ” Validator: ' + fn.name + ' result β€” AI note may not match (logged)', 'info'); } } if (invalidTools.length > 0) { pendingText += '\n\n⚠️ **Validator flagged** ' + invalidTools.join(', ') + ' β€” the AI analysis may not match what was returned. Use `ai-ignore` to dismiss and proceed.'; } } } if (pendingText) { messages.push({ role: 'assistant', content: pendingText }); } continue; } return { finalAnswer: null, toolTurns }; } // Loop exited without final answer β€” auto-summary const toolNames = Object.keys(this._commandFreq).slice(-3).join(', '); this.terminal.print(`πŸ€– AI reached the ${toolTurns}/${this.MAX_TOOL_TURNS} tool limit.`, 'warning'); this.terminal.print('Tools were executed but no final answer was produced β€” try a more specific query.', 'info'); return { finalAnswer: null, toolTurns }; } async afterActionReflection(query, toolTurns) { // Debounce: only suggest skill creation once per 30 minutes const now = Date.now(); if (this._lastSkillSuggestion && (now - this._lastSkillSuggestion) < this._skillDebounceMs) { return; } const suggestions = []; const fs = this.terminal.fs; try { // Batch: read all status info in one VFS pass const reg = this.terminal.moduleManager.moduleRegistry || []; const total = reg.length; const loaded = Object.keys(this.terminal.moduleManager.modules).length; if (loaded < total) { suggestions.push(loaded + '/' + total + ' modules loaded β€” use loadmod to restore'); } } catch (e) {} try { const skillIdx = fs.fsReadJSON('/skills/index.json'); const skillCount = skillIdx ? Object.keys(skillIdx).length : 0; if (skillCount === 0 && toolTurns >= 4) { suggestions.push('Save this approach as a skill with create_skill'); this._lastSkillSuggestion = now; } } catch (e) {} try { const modsDir = fs.getCurrentDir(); const mc = modsDir.content['_mods']; const modCount = mc && mc.content ? Object.keys(mc.content).filter(k => k.endsWith('.js') && !k.endsWith('.orig.js')).length : 0; if (modCount > 0) { suggestions.push(modCount + ' pending module edit(s) in /_mods/ β€” use test_module to verify'); } } catch (e) {} try { if (performance.memory && performance.memory.usedJSHeapSize > 100 * 1048576) { suggestions.push('High memory (' + (performance.memory.usedJSHeapSize / 1048576).toFixed(0) + 'MB) β€” consider clearing terminal'); } } catch (e) {} if (toolTurns >= 4 && suggestions.length === 0) { suggestions.push('Used ' + toolTurns + ' tools β€” create a skill to save this workflow'); this._lastSkillSuggestion = now; } if (suggestions.length > 0) { this.terminal.print('πŸ’‘ ' + suggestions.join(' | '), 'info'); } } 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; 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) { // Fallback to promise resolve return true; } area.innerHTML = ` ⚠️ ${names}(${truncatedArgs}) Type: ai-allow or ai-deny `; area.classList.add('visible'); this._confirmArea = area; this._confirmBar = area; // Attach button handlers via both addEventListener + DOM property (inline onclick fallback) const allowBtn = document.getElementById('ai-confirm-allow'); const denyBtn = document.getElementById('ai-confirm-deny'); const onAllow = () => { this._resolveConfirm(true); }; const onDeny = () => { this._resolveConfirm(false); }; allowBtn.addEventListener('click', onAllow); denyBtn.addEventListener('click', onDeny); allowBtn.__allow = onAllow; denyBtn.__deny = onDeny; const startTime = Date.now(); return new Promise((resolve) => { this._pendingConfirmation = { names, args }; this._confirmResolve = resolve; this._confirmAllowBtn = allowBtn; this._confirmDenyBtn = denyBtn; this._confirmOnAllow = onAllow; this._confirmOnDeny = onDeny; // Countdown timer updates with escalating visibility const timerInterval = setInterval(() => { const elapsed = Math.round((Date.now() - startTime) / 1000); const remaining = Math.max(0, (this._confirmTimeoutMs / 1000) - elapsed); const timerEl = document.getElementById('ai-confirm-timer'); if (timerEl) { timerEl.textContent = `⏱ ${remaining}s`; timerEl.className = 'confirm-timer'; if (remaining > 30) { timerEl.style.color = 'var(--warning-color)'; } else if (remaining > 10) { timerEl.classList.add('urgent'); } else { timerEl.classList.add('critical'); } } if (remaining === 60) { this.terminal.print(`⏱ 60s remaining to respond to AI confirmation for: ${names}`, 'warning'); } else if (remaining === 30) { this.terminal.print(`⏱ 30s β€” ${names} will be auto-denied soon!`, 'warning'); } else if (remaining === 10) { this.terminal.print(`⏱ 10s remaining! Type ai-allow or click Allow now!`, 'warning'); } }, 500); this._confirmTimerInterval = timerInterval; this._confirmTimeout = setTimeout(() => { clearInterval(timerInterval); if (this._confirmResolve) { this._confirmResolve(false); this._cleanupConfirmBar(); this.terminal.print('⏱ Confirmation timed out β€” tool not executed', 'warning'); } }, this._confirmTimeoutMs); }); } _resolveConfirm(approved) { if (this._confirmResolve) { this._confirmResolve(approved); this._cleanupConfirmBar(); if (approved) { this.terminal.print('Action allowed', 'success'); } else { this.terminal.print('Action denied', 'warning'); } } } _cleanupConfirmBar() { clearInterval(this._confirmTimerInterval); clearTimeout(this._confirmTimeout); this._confirmTimeout = null; this._confirmTimerInterval = null; this._confirmResolve = null; this._pendingConfirmation = null; this._confirmAllowBtn = null; this._confirmDenyBtn = null; this._confirmOnAllow = null; this._confirmOnDeny = null; const area = document.getElementById('ai-confirm-area'); if (area) { area.classList.remove('visible'); area.innerHTML = ''; } this._confirmArea = null; this._confirmBar = null; const timerEl = document.getElementById('ai-confirm-timer'); if (timerEl) timerEl.style.animation = ''; } confirmPending() { this._resolveConfirm(true); } denyPending() { this._resolveConfirm(false); } ignoreValidator() { this._validatorSuppressed = true; this.terminal.print('πŸ” Validator suppressed for this session', 'info'); } cancelAi() { this._cancelled = true; this.terminal.print('⏹ Cancelling AI...', 'warning'); // Abort any in-flight HTTP request if (this.terminal.ollamaManager) { this.terminal.ollamaManager.abort(); } // Clear any pending confirmation if (this._confirmResolve) { this._resolveConfirm(false); } // Immediately reset UI so user knows it stopped this.terminal.setAiBusy(false); } toggleDebug() { this._debugPanelEnabled = !this._debugPanelEnabled; let panel = document.getElementById('debug-panel'); if (!panel) { // Create the debug panel panel = document.createElement('div'); panel.id = 'debug-panel'; panel.style.cssText = 'position:fixed;right:10px;top:50px;width:340px;max-height:350px;overflow-y:auto;background:#0a0a0a;border:1px solid #333;border-radius:6px;z-index:100;font-family:monospace;font-size:0.75em;display:none;'; document.body.appendChild(panel); // Add close button handler panel.addEventListener('click', function() { // Toggle collapsed/expanded state }); } panel.style.display = this._debugPanelEnabled ? 'block' : 'none'; if (this._debugPanelEnabled) { this._updateDebugPanel(); this.terminal.print('πŸ”§ Debug panel enabled β€” shows tool call log in bottom-right corner. Close with: debug', 'info'); } else { this.terminal.print('πŸ”§ Debug panel disabled', 'info'); } } // πŸ”’ === GUARDRAIL LAYER === _loadGuardrailPolicy() { try { const raw = localStorage.getItem('webx11_guardrail_policy'); if (raw) { this._guardrailPolicy = JSON.parse(raw); } else { // Default policy β€” seed on first run this._guardrailPolicy = { version: 1, rules: [ { id: 'rm-rf-block', pattern: '/rm\\s+-rf/', description: 'Prevent recursive force deletes outside trash', severity: 'deny', tools: ['run_command', 'write_file'] }, { id: 'curl-pipe-bash', pattern: '/(curl|wget)\\s+.*\\|\\s*(bash|sh)/', description: 'Block curl-to-bash execution', severity: 'deny', tools: ['run_command'] }, { id: 'rm-outer-vfs', pattern: '/delete_file.*\\.(user\\.json|token|session|key)/', description: 'Block deletion of sensitive metadata files', severity: 'deny', tools: ['delete_file'] }, { id: 'mass-delete', pattern: '/rm\\s+-rf\\s+\\/|delete_file.*\\*|\\.*\\/\\*/', description: 'Block mass-delete operations', severity: 'deny', tools: ['run_command', 'delete_file'] }, { id: 'chmod-777', pattern: '/chmod\\s+777/', description: 'Block world-writable permissions', severity: 'deny', tools: ['run_command'] }, { id: 'external-dl', pattern: '/(wget|curl)\\s+-[oO]\\s+/', description: 'External file download β€” requires high confidence', severity: 'warn', tools: ['run_command'] }, { id: 'env-overwrite', pattern: '/export\\s+\\w+\\s*=\\s*/', description: 'Modifying environment variables', severity: 'warn', tools: ['run_command'] }, { id: 'eval-danger', pattern: '/(eval|exec|system)\\s*\\(/', description: 'Dynamic code execution', severity: 'deny', tools: ['run_command'] } ], descriptions: {} }; localStorage.setItem('webx11_guardrail_policy', JSON.stringify(this._guardrailPolicy)); } } catch (e) { this._guardrailPolicy = { version: 1, rules: [] }; } } _saveGuardrailPolicy() { try { localStorage.setItem('webx11_guardrail_policy', JSON.stringify(this._guardrailPolicy)); } catch (e) {} } checkGuardrail(toolName, args) { if (!this._guardrailPolicy || !this._guardrailPolicy.rules) return null; const argsStr = typeof args === 'object' ? JSON.stringify(args) : String(args); for (const rule of this._guardrailPolicy.rules) { if (!rule.tools.includes(toolName)) continue; try { const re = new RegExp(rule.pattern.slice(1, -1), 'i'); if (re.test(argsStr)) { this._guardrailViolations[rule.id] = (this._guardrailViolations[rule.id] || 0) + 1; return rule; // { id, pattern, description, severity, tools } } } catch (e) { /* skip malformed regex */ } } return null; } // 🎯 === CONFIDENCE ESCALATION === _parseConfidence(text) { if (!text || typeof text !== 'string') return null; // Look for patterns like "Confidence: 85%" or "confidence=95%" or "I am 72% confident" const patterns = [ /confidence[:\s]*(\d+)%/i, /confidence[=:]?\s*(\d+)\s*%/i, /(\d+)%\s*confiden/i, /confiden[ts]\s+(\d+)%/i ]; for (const re of patterns) { const m = text.match(re); if (m) return parseInt(m[1], 10); } return null; } // πŸ“Έ === SHADOW VFS (Sandboxed Execution) === _snapshotShadowVFS() { // Deep-clone the VFS root state into a snapshot try { const root = this.terminal.fs.root; this._shadowSnapshot = JSON.parse(JSON.stringify(root)); return true; } catch (e) { this.terminal.print('⚠️ Shadow VFS snapshot failed: ' + e.message, 'error'); return false; } } _diffShadowVFS(consume = true) { if (!this._shadowSnapshot) return null; try { const current = this.terminal.fs.root; const changes = []; this._walkDiff(this._shadowSnapshot, current, '', changes); if (consume) { this._shadowSnapshot = null; } return changes; } catch (e) { this._shadowSnapshot = null; return [{ type: 'diff-error', message: e.message }]; } } _walkDiff(snapshot, live, path, changes) { const snapKeys = snapshot && snapshot.content ? Object.keys(snapshot.content) : []; const liveKeys = live && live.content ? Object.keys(live.content) : []; // New files in live for (const key of liveKeys) { const livePath = path ? path + '/' + key : key; const liveEntry = live.content[key]; const snapEntry = snapshot.content ? snapshot.content[key] : undefined; if (!snapEntry) { const entrySize = liveEntry.type === 'file' ? (liveEntry.content || '').length : 0; changes.push({ type: 'created', path: livePath, entryType: liveEntry.type, size: entrySize }); } else if (liveEntry.type === 'file' && liveEntry.content !== snapEntry.content) { const oldLen = (snapEntry.content || '').length; const newLen = (liveEntry.content || '').length; changes.push({ type: 'modified', path: livePath, sizeBefore: oldLen, sizeAfter: newLen }); } // Recurse into directories if (liveEntry.type === 'dir' && snapEntry && snapEntry.type === 'dir') { this._walkDiff(snapEntry, liveEntry, livePath, changes); } } // Deleted files for (const key of snapKeys) { if (!liveKeys.includes(key)) { changes.push({ type: 'deleted', path: path ? path + '/' + key : key }); } } } // πŸ”’ === GUARDRAIL COMMANDS === guardrailCmd(args) { if (args.length === 0) { this.terminal.print('=== Guardrail Policy ===', 'info'); if (!this._guardrailPolicy) { this.terminal.print('No guardrail policy loaded', 'warning'); return; } this.terminal.print(`Rules: ${this._guardrailPolicy.rules.length}`, 'info'); for (const rule of this._guardrailPolicy.rules) { const icon = rule.severity === 'deny' ? 'πŸ”΄' : '🟑'; this.terminal.print(`${icon} ${rule.id}: ${rule.description}`, 'info'); } const violations = Object.values(this._guardrailViolations).reduce((a, b) => a + b, 0); if (violations > 0) { this.terminal.print(`⚠️ ${violations} violation(s) this session`, 'warning'); } this.terminal.print('Usage: guardrail-add | guardrail-remove ', 'info'); return; } const sub = args[0].toLowerCase(); if (sub === 'add' && args.length >= 4) { const id = args[1]; const severity = args[2]; const pattern = args[3]; const tools = args.slice(4); this._guardrailPolicy.rules.push({ id, severity, pattern, tools, description: 'Custom rule' }); this._saveGuardrailPolicy(); this.terminal.print(`βœ… Guardrail rule '${id}' added`, 'success'); } else if (sub === 'remove' && args.length >= 2) { const idx = this._guardrailPolicy.rules.findIndex(r => r.id === args[1]); if (idx >= 0) { this._guardrailPolicy.rules.splice(idx, 1); this._saveGuardrailPolicy(); this.terminal.print(`βœ… Guardrail rule '${args[1]}' removed`, 'success'); } else { this.terminal.print(`❌ Rule '${args[1]}' not found`, 'error'); } } else { this.terminal.print('Usage: guardrail-add | guardrail-remove ', 'warning'); } } // 🎯 SHADOW COMMAND shadowExec(args) { // Usage: shadow // Snapshot VFS, run command, diff output if (args.length === 0) { this.terminal.print('Usage: shadow β€” Execute command in shadow VFS (sandbox preview)', 'warning'); return; } const cmd = args.join(' '); this._snapshotShadowVFS(); const cmdName = cmd.split(' ')[0].toLowerCase(); const cmdArgs = cmd.split(' ').slice(1); const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; this.terminal.print = (text, type) => { output += text + '\n'; }; this.terminal.dispatchCommand(cmdName, cmdArgs).then(() => { this.terminal.print = originalPrint; const diff = this._diffShadowVFS(false); if (!diff || diff.length === 0) { this.terminal.print('πŸ“¦ Shadow: No filesystem changes detected.', 'success'); } else { this.terminal.print(`πŸ“¦ Shadow: ${diff.length} change(s) detected:`, 'info'); for (const c of diff) { const icon = c.type === 'created' ? 'βž•' : c.type === 'modified' ? '✏️' : 'πŸ—‘οΈ'; const sizeStr = c.size !== undefined ? c.size + 'B' : ''; const detail = c.type === 'created' ? `(${c.entryType}${sizeStr ? ', ' + sizeStr : ''})` : c.type === 'modified' ? `(${c.sizeBefore}β†’${c.sizeAfter}B)` : ''; this.terminal.print(`${icon} ${c.path} ${detail}`, c.type === 'deleted' ? 'warning' : 'info'); } // Ask to commit or rollback this.terminal.print('πŸ”’ VFS modified β€” type "shadow-commit" to accept or "shadow-rollback" to undo.', 'warning'); } }).catch(e => { this.terminal.print = originalPrint; this.terminal.print('⚠️ Shadow command error: ' + e.message, 'error'); }); } shadowCommit() { this._shadowSnapshot = null; this.terminal.print('βœ… Shadow changes committed to VFS', 'success'); } shadowRollback() { if (!this._shadowSnapshot) { this.terminal.print('❌ No shadow snapshot to rollback to', 'error'); return; } try { this.terminal.fs.root = JSON.parse(JSON.stringify(this._shadowSnapshot)); this.terminal.fs.saveToLocalStorage(); this._shadowSnapshot = null; this.terminal.print('↩️ VFS rolled back to pre-shadow state', 'success'); } catch (e) { this.terminal.print('⚠️ Rollback failed: ' + e.message, 'error'); } } // πŸ”’ OVERRIDES for isDestructive β€” adds runTool intercept // 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', '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); } async runTool(name, argsStr) { let a = {}; try { // πŸ”’ GUARDRAIL CHECK β€” intercept before any tool executes const guardrail = this.checkGuardrail(name, argsStr); if (guardrail && guardrail.severity === 'deny') { const errMsg = `πŸ”’ GUARDRAIL BLOCKED: ${guardrail.description} (rule: ${guardrail.id})`; this.terminal.print(errMsg, 'error'); this.terminal.log('WARN', 'GUARDRAIL', errMsg); return JSON.stringify({ error: errMsg }); } if (guardrail && guardrail.severity === 'warn') { this.terminal.print(`🟑 GUARDRAIL WARNING: ${guardrail.description} (rule: ${guardrail.id})`, 'warning'); } a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {}); switch (name) { case 'list_files': { const originalPath = a.path || ''; const p = originalPath.replace(/^\//, ''); const saved = [...this.terminal.fs.currentPath]; if (originalPath.startsWith('/')) { this.terminal.fs.currentPath = []; } if (p) { 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 { 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 !== '..')); } const files = this.terminal.fs.ls(); this.terminal.fs.currentPath = saved; return JSON.stringify(files.map(f => ({ name: f.name, type: f.type, size: f.size }))); } case 'read_file': { const rp = a.path || ''; const savedR = [...this.terminal.fs.currentPath]; if (rp.startsWith('/')) this.terminal.fs.currentPath = []; const path = rp.replace(/^\//, ''); const content = this.terminal.fs.readFile(path); this.terminal.fs.currentPath = savedR; if (content === null) return JSON.stringify({ error: `File not found: ${path}` }); return content.length > 8000 ? content.substring(0, 8000) + '\n...(truncated)' : content; } case 'write_file': { const wp = a.path || ''; const savedW = [...this.terminal.fs.currentPath]; if (wp.startsWith('/')) this.terminal.fs.currentPath = []; const path = wp.replace(/^\//, ''); const content = a.content || ''; this.terminal.fs.writeFile(path, content); this.terminal.fs.currentPath = savedW; return `File written: ${path} (${content.length} chars)`; } case 'make_directory': { const mp = a.path || ''; const savedM = [...this.terminal.fs.currentPath]; if (mp.startsWith('/')) this.terminal.fs.currentPath = []; const path = mp.replace(/^\//, ''); const ok = this.terminal.fs.mkdir(path); this.terminal.fs.currentPath = savedM; if (ok) return `Directory created: ${path}`; return JSON.stringify({ error: `Cannot create: ${path}` }); } case 'delete_file': { const dp = a.path || ''; const savedD = [...this.terminal.fs.currentPath]; if (dp.startsWith('/')) this.terminal.fs.currentPath = []; const path = dp.replace(/^\//, ''); const delOk = this.terminal.fs.deleteFile(path); this.terminal.fs.currentPath = savedD; if (delOk) return `Deleted: ${path}`; return JSON.stringify({ error: `Cannot delete: ${path}` }); } case 'get_status': { const fs = this.terminal.fs; const mm = this.terminal.moduleManager; const om = this.terminal.ollamaManager; return JSON.stringify({ files: fs.ls().length, dirs: fs.currentPath.length ? '/' + fs.currentPath.join('/') : '/', modules: Object.keys(mm.modules).length, ollama: om.connected ? `Connected (${om.currentModel})` : 'Disconnected', memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A' }); } case 'get_help': { const cmds = this.terminal.moduleManager.getAllCommands(); return JSON.stringify(Object.keys(cmds).sort()); } case 'get_current_dir': { return '/' + this.terminal.fs.currentPath.join('/'); } case 'web_search': { const query = a.query || ''; if (!query) return JSON.stringify({ error: 'No query provided' }); try { const resp = await fetch('/api/search?q=' + encodeURIComponent(query)); if (!resp.ok) { const err = await resp.json().catch(() => ({})); return JSON.stringify({ error: 'Search failed: ' + (err.error || `HTTP ${resp.status}`), status: resp.status, details: err.details || '' }); } const data = await resp.json(); const results = data.results || []; if (results.length === 0) return JSON.stringify({ message: 'No results found' }); return JSON.stringify({ count: results.length, source: data.source, results: results.map(r => ({ title: r.title, url: r.url, content: (r.content || '').substring(0, 300) })) }); } catch (e) { return JSON.stringify({ error: 'Search failed: ' + e.message }); } } case 'web_fetch': { const url = a.url || ''; if (!url) return JSON.stringify({ error: 'No URL provided' }); const method = (a.method || 'GET').toUpperCase(); const body = a.body || ''; const contentType = a.content_type || (method === 'POST' ? 'application/json' : ''); try { // Use /api/fetch for POST, /api/extract for GET (extract is cleaner for readable content) const endpoint = method === 'POST' ? '/api/fetch' : '/api/extract'; const params = new URLSearchParams({ url }); if (method === 'POST') { params.set('method', 'POST'); params.set('body', body); if (contentType) params.set('content_type', contentType); } const resp = await fetch(endpoint + '?' + params.toString()); if (resp.status === 403) return JSON.stringify({ error: 'URL blocked β€” internal network' }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); return JSON.stringify({ error: 'Fetch failed: ' + (err.error || `HTTP ${resp.status}`), status: resp.status, details: err.details || '' }); } // For POST/route, the response is raw text; for GET/extract it's JSON if (method === 'POST') { const text = await resp.text(); return JSON.stringify({ method, content: text.substring(0, 4000), content_length: text.length }); } const data = await resp.json(); return JSON.stringify({ method, title: data.title || '', content: (data.content || '').substring(0, 4000), word_count: data.word_count || 0, method_proxy: data.method || 'extract' }); } catch (e) { return JSON.stringify({ error: 'Fetch failed: ' + e.message }); } } case 'wpm_install': { const pkg = a.package || ''; if (!pkg) return JSON.stringify({ error: 'No package name' }); const mod = this.terminal.moduleManager.modules['WpmModule']; if (mod) { await mod.install(pkg); return `Package "${pkg}" installation attempted`; } return JSON.stringify({ error: 'WPM module not available' }); } case 'wpm_search': { const kw = a.keyword || ''; if (!kw) return JSON.stringify({ error: 'No keyword' }); const mod = this.terminal.moduleManager.modules['WpmModule']; if (mod) { const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; this.terminal.print = (text, type) => { output += text + '\n'; }; try { await mod.search(kw); } finally { this.terminal.print = originalPrint; } return output.trim() || 'Search completed'; } return JSON.stringify({ error: 'WPM module not available' }); } case 'run_command': { const cmd = a.command || ''; if (!cmd) return JSON.stringify({ error: 'No command provided' }); // πŸ“Έ Shadow VFS auto-snapshot before run_command this._snapshotShadowVFS(); const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; this.terminal.print = (text, type) => { output += text + '\n'; }; 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) { const diffSummary = shadowDiff.map(c => `${c.type}:${c.path}`).join(', '); output += `\nπŸ“¦ [Shadow VFS: ${shadowDiff.length} change(s) β€” ${diffSummary}]`; this._shadowSnapshot = null; // Changes considered "committed" since AI explicitly ran command } return output.trim() || '(no output)'; } case 'check_updates': { const reg = this.terminal.moduleManager.moduleRegistry; const results = []; for (const mod of reg) { if (mod.url && mod.url.startsWith('http')) { try { const resp = await fetch(mod.url, { method: 'HEAD' }); results.push({ name: mod.name, url: mod.url, status: resp.ok ? 'reachable' : 'error (' + resp.status + ')' }); } catch (e) { results.push({ name: mod.name, url: mod.url, status: 'unreachable' }); } } } return JSON.stringify({ checked: results.length, modules: results }); } case 'user_get': { const key = a.key || ''; if (!key) return JSON.stringify({ error: 'Key is required' }); const val = key.split('.').reduce((o, k) => (o && typeof o === 'object') ? o[k] : undefined, this._userProfile); return JSON.stringify({ key, value: val !== undefined ? val : null }); } case 'user_set': { const ukey = a.key || ''; if (!ukey) return JSON.stringify({ error: 'Key and value are required' }); const uval = a.value || ''; const keys = ukey.split('.'); let obj = this._userProfile; for (let i = 0; i < keys.length - 1; i++) { if (!obj[keys[i]] || typeof obj[keys[i]] !== 'object') obj[keys[i]] = {}; obj = obj[keys[i]]; } obj[keys[keys.length - 1]] = uval; try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {} return JSON.stringify({ success: true, key: ukey, value: uval }); } case 'user_list': { return JSON.stringify({ profile: this._userProfile, keys: Object.keys(this._userProfile) }); } case 'user_forget': { const fkey = a.key || ''; if (!fkey) return JSON.stringify({ error: 'Key is required' }); if (fkey.includes('.')) { const parts = fkey.split('.'); let obj = this._userProfile; for (let i = 0; i < parts.length - 1; i++) { if (!obj[parts[i]]) return JSON.stringify({ error: 'Key not found' }); obj = obj[parts[i]]; } delete obj[parts[parts.length - 1]]; } else { delete this._userProfile[fkey]; } try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {} return JSON.stringify({ success: true, key: fkey }); } case 'session_save': { const summary = a.summary || 'No summary provided'; const fs = this.terminal.fs; fs.mkdirRecursive('/memory'); const today = new Date().toISOString().split('T')[0]; const log = { date: today, summary, toolCallCount: 0, skillsCreated: [], modulesEdited: [], timestamp: new Date().toISOString() }; fs.fsWriteJSON('/memory/' + today + '.json', log); const index = fs.fsReadJSON('/memory/index.json') || {}; index[today] = { summary, tasks: (index[today]?.tasks || 0) + 1, updated: new Date().toISOString() }; fs.fsWriteJSON('/memory/index.json', index); return JSON.stringify({ success: true, date: today }); } case 'session_load': { const sdate = a.date || ''; if (!sdate) return JSON.stringify({ error: 'Date required (YYYY-MM-DD)' }); const sfs = this.terminal.fs; const slog = sfs.fsReadJSON('/memory/' + sdate + '.json'); if (!slog) return JSON.stringify({ error: 'No session log for ' + sdate }); return JSON.stringify(slog); } case 'session_recent': { const count = a.count || 3; const rfs = this.terminal.fs; const rindex = rfs.fsReadJSON('/memory/index.json') || {}; const dates = Object.keys(rindex).sort().reverse().slice(0, count); const sessions = dates.map(d => ({ date: d, ...rindex[d] })); return JSON.stringify({ sessions, count: sessions.length }); } case 'session_terminal': { const tfs = this.terminal.fs; try { const raw = tfs.readFile('_sessions/_auto.json'); if (!raw) return JSON.stringify({ error: 'No auto-saved session found' }); const data = JSON.parse(raw); const lines = (data.terminalLines || []).slice(-50); const text = lines.map(l => { const ts = l.t ? '[' + l.t + '] ' : ''; return ts + l.l; }).join('\n'); return JSON.stringify({ lines: lines.length, output: text, savedAt: data.savedAt || '' }); } catch (e) { return JSON.stringify({ error: 'Could not read terminal history: ' + e.message }); } } case 'session_stats': { const sfs = this.terminal.fs; const sindex = sfs.fsReadJSON('/memory/index.json') || {}; const entries = Object.entries(sindex); let totalTasks = 0; for (const [, meta] of entries) totalTasks += meta.tasks || 0; return JSON.stringify({ totalSessions: entries.length, totalTasks, dates: entries.map(([d]) => d) }); } case 'list_agents': { const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; if (!registry) return JSON.stringify({ error: 'Agent registry not available' }); const agents = this.terminal.fs.ls('/agents/'); const results = []; if (agents) { agents.forEach(a => { if (a.type === 'dir' && a.name !== 'configured') { const data = this.terminal.fs.readFile(`/agents/${a.name}/status.json`); if (data) results.push(JSON.parse(data)); } }); } return JSON.stringify({ agents: results, count: results.length }); } case 'delegate_task': { const task = a.task || ''; if (!task) return JSON.stringify({ error: 'No task provided' }); const orch = this.terminal.moduleManager.modules['OrchestratorModule']; if (!orch) return JSON.stringify({ error: 'Orchestrator not available' }); orch.delegateTask(task); return JSON.stringify({ message: `Task delegated: ${task.substring(0, 80)}` }); } case 'spawn_agent': { const role = a.role || 'worker'; const caps = a.capabilities ? a.capabilities.split(',').map(s => s.trim()) : ['list_files', 'read_file']; const orch = this.terminal.moduleManager.modules['OrchestratorModule']; if (!orch) return JSON.stringify({ error: 'Orchestrator not available' }); const agent = orch.spawnAgent(role, caps); if (!agent) return JSON.stringify({ error: 'Agent spawn failed β€” limit reached or concurrent spawn conflict' }); return JSON.stringify({ message: `Agent spawned: ${agent.id}`, agent }); } case 'load_skills': { const query = (a && a.query || '').toLowerCase(); const fs = this.terminal.fs; const index = fs.fsReadJSON('/skills/index.json') || {}; if (!query) { const names = Object.keys(index); return JSON.stringify({ skills: names, count: names.length, message: 'No query provided β€” showing all skill names. Call again with a query to load content.' }); } const matches = []; for (const [name, meta] of Object.entries(index)) { const score = (name.includes(query) ? 3 : 0) + (meta.description.toLowerCase().includes(query) ? 2 : 0) + ((meta.tags || []).some(t => t.toLowerCase().includes(query)) ? 2 : 0); if (score > 0) { const content = fs.readFile(`/skills/${name}.md`) || ''; matches.push({ name, description: meta.description, category: meta.category, score, content: content.substring(0, 2000) }); } } matches.sort((a, b) => b.score - a.score); const top = matches.slice(0, 3); return JSON.stringify(top.length > 0 ? { matches: top.map(m => ({ name: m.name, description: m.description, content: m.content })), count: top.length } : { matches: [], count: 0, message: 'No matching skills found. You can create one after this task.' }); } case 'create_skill': { const name = (a.name || '').trim().replace(/\s+/g, '-').toLowerCase(); if (!name) return JSON.stringify({ error: 'Skill name is required' }); const description = a.description || ''; let content = a.content || ''; const tags = (a.tags || '').split(',').map(t => t.trim()).filter(Boolean); const category = a.category || 'general'; if (!content.startsWith('---')) { content = `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n## Steps\n${content}`; } const fs = this.terminal.fs; fs.mkdirRecursive('/skills'); fs.writeFile(`/skills/${name}.md`, content); const index = fs.fsReadJSON('/skills/index.json') || {}; index[name] = { name, description, tags, category, created: index[name]?.created || new Date().toISOString(), updated: new Date().toISOString() }; fs.fsWriteJSON('/skills/index.json', index); this.terminal.print(`πŸ’‘ Skill saved: ${name}`, 'success'); return JSON.stringify({ success: true, name, message: `Skill "${name}" created (${content.length} chars)` }); } case 'patch_skill': { const pname = (a.name || '').trim(); if (!pname) return JSON.stringify({ error: 'Skill name is required' }); const pcontent = a.content || ''; if (!pcontent) return JSON.stringify({ error: 'Skill content is required' }); const fs = this.terminal.fs; const index = fs.fsReadJSON('/skills/index.json') || {}; if (!index[pname]) return JSON.stringify({ error: `Skill "${pname}" not found` }); fs.writeFile(`/skills/${pname}.md`, pcontent); index[pname].updated = new Date().toISOString(); // Re-parse frontmatter for metadata updates const metaMatch = pcontent.match(/^---\n([\s\S]*?)\n---/); if (metaMatch) { const metaLines = metaMatch[1].split('\n'); metaLines.forEach(line => { const [k, ...v] = line.split(':'); if (k === 'description') index[pname].description = v.join(':').trim(); if (k === 'tags') index[pname].tags = v.join(':').trim().replace(/[\[\]]/g, '').split(',').map(t => t.trim()); if (k === 'category') index[pname].category = v.join(':').trim(); }); } fs.fsWriteJSON('/skills/index.json', index); this.terminal.print(`πŸ’‘ Skill patched: ${pname}`, 'success'); return JSON.stringify({ success: true, name: pname, message: `Skill "${pname}" updated` }); } case 'list_skills': { const filterCat = (a.category || '').toLowerCase(); const fs = this.terminal.fs; const index = fs.fsReadJSON('/skills/index.json') || {}; const list = Object.entries(index).map(([name, meta]) => ({ name, description: meta.description, category: meta.category || 'general', tags: meta.tags || [], updated: meta.updated })) .filter(s => !filterCat || (s.category || '').toLowerCase() === filterCat); return JSON.stringify({ skills: list, count: list.length }); } case 'nvfs_resolve': { const concept = (a.concept || '').toLowerCase(); if (!concept) return JSON.stringify({ error: 'Concept is required' }); const nvfs = this.terminal.moduleManager.modules['NeuralVFSModule']; if (!nvfs) return JSON.stringify({ error: 'Neural VFS module not loaded β€” run nvfs-index first' }); // Build index if empty if (Object.keys(nvfs.index).length === 0) { const saved = this.terminal.fs.fsReadJSON('/.semantic_index/map.json'); if (saved) { nvfs.index = saved; } else { return JSON.stringify({ error: 'No semantic index found β€” run nvfs-index first' }); } } const exact = nvfs.index[concept]; const fuzzy = Object.keys(nvfs.index).filter(k => k.includes(concept) || concept.includes(k) ).filter(k => k !== concept); const results = []; if (exact) results.push(...exact.map(e => ({ concept, path: e.path, summary: e.summary }))); for (const f of fuzzy.slice(0, 5)) { results.push(...nvfs.index[f].slice(0, 3).map(e => ({ concept: f, path: e.path, summary: e.summary }))); } return JSON.stringify({ concept, exact: !!exact, count: results.length, results: results.slice(0, 20) }); } case 'list_available_modules': { try { const resp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || '')); if (!resp.ok) return JSON.stringify({ error: 'Module index not available' }); const idx = await resp.json(); const filterType = (a.type || '').toLowerCase(); let mods = Object.values(idx.modules); if (filterType) mods = mods.filter(m => m.type === filterType); return JSON.stringify({ total: mods.length, available: idx.total, type: filterType || 'all', modules: mods.map(m => ({ name: m.name, type: m.type, class: m.class, commands: m.commands.slice(0, 8), description: (m.description || '').substring(0, 80), auto_load: m.auto_load })) }); } catch (e) { return JSON.stringify({ error: 'Failed to load module index: ' + e.message }); } } case 'smart_context_list': { const scm = this.terminal.moduleManager.modules['SmartContextModule']; if (!scm) return JSON.stringify({ error: 'SmartContextModule not loaded', profiles: [] }); const profiles = scm.contextProfiles || {}; return JSON.stringify({ profiles: Object.entries(profiles).map(([key, p]) => ({ name: key, displayName: p.name || key, systemPrompt: (p.systemPrompt || '').substring(0, 80), temperature: p.temperature || 0.7, active: scm.activeProfile === key })) }); } case 'smart_context_set': { const scm = this.terminal.moduleManager.modules['SmartContextModule']; if (!scm) return JSON.stringify({ error: 'SmartContextModule not loaded' }); const name = (a.name || '').toLowerCase(); if (!scm.contextProfiles[name]) { return JSON.stringify({ error: `Profile '${name}' not found`, available: Object.keys(scm.contextProfiles) }); } scm.activeProfile = name; const profile = scm.contextProfiles[name]; return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature }); } case 'create_mindmap': { const mindmap = this.terminal.moduleManager.modules['MindmapModule']; if (!mindmap) return JSON.stringify({ error: 'MindmapModule not loaded' }); const topic = a.topic || 'Mindmap'; const children = a.children || ''; mindmap.root = null; const args = topic.split(' '); mindmap._new(args); if (!mindmap.root) return JSON.stringify({ error: 'Failed to create root' }); const rootId = mindmap.root.id; const added = []; if (children) { const childNames = children.split(',').map(c => c.trim()).filter(Boolean); for (const child of childNames) { mindmap._add([rootId, child]); added.push(child); } } mindmap._svg([]); return JSON.stringify({ success: true, topic, children: added.length ? added : 'none', nodes: mindmap._countNodes(mindmap.root), rendered: 'inline SVG in terminal' }); } case 'view_module': case 'edit_module': case 'test_module': case 'reload_module': case 'list_modified_modules': case 'get_module_status': return await this.runModuleTool(name, argsStr); default: return JSON.stringify({ error: `Unknown tool: ${name}` }); } } 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) }); } } async runModuleTool(name, argsStr) { let a = {}; try { a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {}); switch (name) { case 'view_module': { const modName = a.name || ''; const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName); const code = this.terminal.fs.readFile(`_mods/${modName}.js`); if (code) return code; const url = entry ? entry.url : null; // Try registry URL first if (url) { try { const resp = await fetch(url); if (resp.ok) { const src = await resp.text(); this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src); return src; } } catch (e) { /* fall through */ } } // Fallback: use modules-index.json to map class name to filename try { const idxResp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || '')); if (idxResp.ok) { const idx = await idxResp.json(); // modName is the class name (e.g. "WebX11AIAssistantModule") // or already a filename (e.g. "module-aiassist") // Try matching by class name first let match = Object.values(idx.modules).find(m => m.class === modName); // Then by name if (!match) match = idx.modules[modName]; if (match) { const filePath = match.file; // e.g. "modules/core/module-aiassist.js" const fetchUrl = `/spaces/newos4/${filePath}`; const resp = await fetch(fetchUrl); if (resp.ok) { const src = await resp.text(); this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src); return src; } } } } catch (e) { /* fall through */ } return JSON.stringify({ error: `Cannot find source for ${modName} β€” not in registry, not in modules-index.json` }); } case 'edit_module': { const modName = a.name || ''; const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName); const url = entry ? entry.url : null; let code = this.terminal.fs.readFile(`_mods/${modName}.js`); if (!code && url) { try { const resp = await fetch(url); if (resp.ok) { code = await resp.text(); this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, code); this.terminal.fs.writeFile(`_mods/${modName}.js`, code); this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ originalUrl: url, modifiedAt: new Date().toISOString(), editCount: 0 })); } } catch (e) {} } // Fallback: use modules-index.json for core modules (not in registry) if (!code) { try { const idxResp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || '')); if (idxResp.ok) { const idx = await idxResp.json(); let match = Object.values(idx.modules).find(m => m.class === modName); if (!match) match = idx.modules[modName]; if (match) { const filePath = match.file; const fetchUrl = `/spaces/newos4/${filePath}`; const resp = await fetch(fetchUrl); if (resp.ok) { code = await resp.text(); this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, code); this.terminal.fs.writeFile(`_mods/${modName}.js`, code); this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ originalUrl: fetchUrl, modifiedAt: new Date().toISOString(), editCount: 0 })); } } } } catch (e) {} } if (code) this.terminal.openCodeEditor(modName, url || '', code); return code ? `Editor opened for ${modName} (${code.length} chars)` : JSON.stringify({ error: `Could not load source for ${modName}` }); } case 'test_module': { const modName = a.name || ''; const code = this.terminal.fs.readFile(`_mods/${modName}.js`); if (!code) return JSON.stringify({ error: `No local edit for ${modName}` }); this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`); return `Module "${modName}" loaded from local edit for testing`; } case 'reload_module': { const modName = a.name || ''; const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName); this.terminal.fs.deleteFile(`_mods/${modName}.js`); this.terminal.fs.deleteFile(`_mods/${modName}.orig.js`); this.terminal.fs.deleteFile(`_mods/${modName}.meta.json`); if (entry && entry.url) { this.terminal.moduleManager.unloadModule(modName); await this.terminal.moduleManager.loadModuleFromURL(entry.url); return `Module "${modName}" reloaded from ${entry.url}`; } return JSON.stringify({ error: `No original URL for ${modName}` }); } case 'list_modified_modules': { const modsDir = this.terminal.fs.getCurrentDir(); const mc = modsDir.content['_mods']; if (!mc || Object.keys(mc.content).length === 0) return JSON.stringify({ modified: [], message: 'No local edits' }); const files = Object.keys(mc.content).filter(k => k.endsWith('.js') && !k.endsWith('.orig.js')); return JSON.stringify({ modified: files.map(f => f.replace('.js', '')), count: files.length }); } case 'get_module_status': { const mm = this.terminal.moduleManager; const modsDir = this.terminal.fs.getCurrentDir(); const hasMods = modsDir.content['_mods'] ? !!Object.keys(modsDir.content['_mods'].content).length : false; // Load module index for file path lookup let idxMap = {}; try { const resp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || '')); if (resp.ok) { const idx = await resp.json(); for (const [name, meta] of Object.entries(idx.modules)) { idxMap[meta.class] = '/spaces/newos4/' + meta.file; } } } catch (e) {} const modules = []; const seen = new Set(); for (const [className, instance] of Object.entries(mm.modules)) { const name = instance.constructor ? instance.constructor.name : className; if (seen.has(name)) continue; seen.add(name); modules.push({ name: name, url: idxMap[name] || null, enabled: true, loaded: true, modified: !!this.terminal.fs.readFile(`_mods/${name}.js`), source: 'loaded' }); } for (const m of mm.moduleRegistry) { if (seen.has(m.name)) continue; seen.add(m.name); modules.push({ name: m.name, url: m.url || idxMap[m.name] || null, enabled: m.enabled, loaded: !!mm.modules[m.name], modified: !!this.terminal.fs.readFile(`_mods/${m.name}.js`), source: m.url ? 'ext' : 'core' }); } return JSON.stringify({ modules, total: modules.length, hasLocalEdits: hasMods }); } default: return JSON.stringify({ error: `Unknown module tool: ${name}` }); } } 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) }); } } clearContext() { this.context = []; localStorage.removeItem('webx11_ai_context'); this.terminal.print('AI conversation context cleared', 'success'); } _saveContext() { try { const data = JSON.stringify({ context: this.context.slice(-20), messageCount: this._messageCount, savedAt: Date.now() }); localStorage.setItem('webx11_ai_context', data); // Also persist to VFS for crash survival try { const fs = this.terminal.fs; if (fs) { fs.mkdirRecursive('_sessions'); fs.writeFile('_sessions/_auto-context.json', data); } } catch (e) {} } catch (e) { // localStorage quota exceeded β€” silently ignore } } _loadContext() { try { const saved = localStorage.getItem('webx11_ai_context'); if (saved) { const data = JSON.parse(saved); if (data && data.context) { this.context = data.context.slice(-20); this._messageCount = data.messageCount || 0; } } } catch (e) {} } setSystemPrompt(args) { if (!args[0]) { 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(' '); // 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; if (args.length > 0) { const name = args.join(' '); if (om.models && om.models.some(m => m.name === name)) { om.currentModel = name; localStorage.setItem('webx11_ollama_model', name); this.terminal.print(`AI model set to: ${name}`, 'success'); } else { this.terminal.print(`Model "${name}" not found in available models. Use ai-model to list.`, 'warning'); } return; } this.terminal.print(`Current AI model: ${om.currentModel || 'none'}`, 'info'); if (om.models && om.models.length > 0) { const maxShow = 20; const names = om.models.slice(0, maxShow).map(m => m.name).join(', '); this.terminal.print(`Available (${om.models.length}): ${names}${om.models.length > maxShow ? `... +${om.models.length - maxShow} more` : ''}`, 'info'); } else { this.terminal.print('No models available. Use ollamaconnect first.', 'warning'); } } aiConfig(args) { if (args.length === 0) { this.terminal.print('=== AI Config ===', 'info'); this.terminal.print(`max-turns: ${this.MAX_TOOL_TURNS} (default: 8)`, ''); 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 > 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(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization', 'info'); return; } const key = args[0]; const val = args[1]; if (key === 'max-turns') { const n = parseInt(val); if (isNaN(n) || n < 1 || n > 50) return this.terminal.print('max-turns must be 1-50', 'warning'); this.MAX_TOOL_TURNS = n; } else if (key === 'max-context') { const n = parseInt(val); if (isNaN(n) || n < 1 || n > 100) return this.terminal.print('max-context must be 1-100', 'warning'); this.maxContext = n; } else if (key === 'max-context-tokens') { const n = parseInt(val); if (isNaN(n) || n < 100 || n > 10000) return this.terminal.print('max-context-tokens must be 100-10000', 'warning'); this.maxContextTokens = n; } else if (key === 'confirm-timeout') { const n = parseInt(val); 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); if (isNaN(n) || n < 1 || n > 60) return this.terminal.print('chat-lock-timeout must be 1-60 seconds', 'warning'); this._chatLockTimeoutMs = n * 1000; } else if (key === 'skill-debounce') { const n = parseInt(val); if (isNaN(n) || n < 1 || n > 240) return this.terminal.print('skill-debounce must be 1-240 minutes', 'warning'); this._skillDebounceMs = n * 60000; } else if (key === 'trivial-optimization') { if (this.terminal?.ollamaManager) { this.terminal.ollamaManager.optimizationEnabled = val === 'on'; } } else { return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization`, 'warning'); } // Persist localStorage.setItem('webx11_ai_config', JSON.stringify({ maxTurns: this.MAX_TOOL_TURNS, maxContext: this.maxContext, maxContextTokens: this.maxContextTokens, confirmTimeoutMs: this._confirmTimeoutMs, chatLockTimeoutMs: this._chatLockTimeoutMs, skillDebounceMs: this._skillDebounceMs })); this.terminal.print(`ai-config ${key} set to ${val}`, 'success'); } showContext() { if (this.context.length === 0) { this.terminal.print('No conversation context', 'info'); return; } this.terminal.print(`=== Context (${this.context.length} messages) ===`, 'info'); this.context.forEach(msg => { const icon = msg.role === 'user' ? 'πŸ‘€' : msg.role === 'assistant' ? 'πŸ€–' : 'πŸ”§'; const content = typeof msg.content === 'string' ? msg.content.substring(0, 60) : JSON.stringify(msg).substring(0, 60); this.terminal.print(`${icon} ${content}...`); }); } _extractFacts(userMsg, aiResp) { const patterns = [ { re: /my name is (\w+)/i, key: 'name' }, { re: /call me (\w+)/i, key: 'name' }, { re: /i(?:'m| am) working on (.+)/i, key: 'project' }, { re: /(?:my|the) project is (.+)/i, key: 'project' }, { re: /i have a server at (.+)/i, key: 'server' }, { re: /i use (\w+).*?(?:editor|ide|text)/i, key: 'preferredEditor' }, { re: /i (?:like|prefer|love) (.+)/i, key: 'preference' }, ]; let changed = false; for (const { re, key } of patterns) { const m = (userMsg + ' ' + (aiResp || '')).match(re); if (m && m[1]) { if (!this._userProfile.knownFacts) this._userProfile.knownFacts = {}; if (this._userProfile.knownFacts[key] !== m[1].trim()) { this._userProfile.knownFacts[key] = m[1].trim(); changed = true; } } } if (changed) { try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {} } } _trackPattern(userMessage) { const lower = userMessage.toLowerCase(); const patterns = ['font size','bigger text','smaller text','export','backup','save','install','load module','search','find','color','theme','skill','learn','remember','mindmap','brainstorm']; for (const p of patterns) { if (lower.includes(p)) this._commandFreq[p] = (this._commandFreq[p] || 0) + 1; } try { localStorage.setItem('webx11_ai_patterns', JSON.stringify(this._commandFreq)); } catch (e) {} } async _autoCreateErrorSkill() { const grouped = {}; for (const e of this._errorLog) { if (!grouped[e.tool]) grouped[e.tool] = []; grouped[e.tool].push(e); } for (const [tool, errors] of Object.entries(grouped)) { if (errors.length >= 3) { const skillName = 'tool-' + tool + '-workaround'; const existing = this.terminal.fs.readFile('/skills/' + skillName + '.md'); if (existing) continue; const steps = errors.map((e, i) => (i + 1) + '. When calling ' + tool + ', avoid: "' + JSON.stringify(e.args) + '" which caused: ' + e.error).join('\n'); const content = '---\nname: ' + skillName + '\ndescription: Workarounds for ' + tool + ' failures\ntags: [' + tool + ', error, workaround]\ncategory: system\n---\n## Steps\n' + steps; await this.runTool('create_skill', { name: skillName, description: 'Workarounds for ' + tool + ' failures', content: content, tags: tool + ',error,workaround', category: 'system' }); this.terminal.print('πŸ€– Auto-created skill from repeated errors: ' + skillName, 'info'); } } this._errorLog = []; } async _auditSkills() { const fs = this.terminal.fs; const index = fs.fsReadJSON('/skills/index.json') || {}; const now = Date.now(); const issues = []; for (const [name, meta] of Object.entries(index)) { const content = fs.readFile('/skills/' + name + '.md'); if (!content || content.length < 50) { issues.push({ name, issue: 'empty or too short' }); continue; } const updated = new Date(meta.updated || 0).getTime(); const ageDays = (now - updated) / 86400000; if (ageDays > 30) issues.push({ name, issue: 'not updated in ' + Math.round(ageDays) + ' days' }); } if (issues.length > 0) { this.terminal.print('πŸ“‹ Skill audit: ' + issues.length + ' issue(s) found', 'info'); issues.slice(0, 3).forEach(i => this.terminal.print(' - ' + i.name + ': ' + i.issue, 'warning')); } } _estimateTokens(text) { if (!text || typeof text !== 'string') return 0; return Math.ceil(text.length / 4); } _estimateContextTokens() { let total = 0; for (const msg of this.context) { total += this._estimateTokens(msg.content || ''); } return total; } async _compressContext() { if (this.context.length <= 6) return; const totalTokens = this._estimateContextTokens(); if (totalTokens < this.maxContextTokens) return; const keepCount = 3; const oldMsgs = this.context.slice(0, -keepCount * 2); if (oldMsgs.length < 2) return; const recentMsgs = this.context.slice(-keepCount * 2); const summaryText = oldMsgs.map(m => (m.role === 'user' ? 'πŸ‘€' : m.role === 'assistant' ? 'πŸ€–' : 'βš™οΈ') + ': ' + (m.content || '').substring(0, 150) ).join(' | '); let compressed = `[Earlier context (${oldMsgs.length} messages): ${summaryText}]`; // Try AI summarization, fallback to truncation try { if (this.terminal.ollamaManager && this.terminal.ollamaManager.connected) { const sumPrompt = 'Summarize these conversation exchanges into a single compact sentence. Focus on what the user asked and what actions were taken. Keep it under 200 characters.\n\n' + JSON.stringify(oldMsgs.map(m => ({ role: m.role, content: (m.content || '').substring(0, 200) }))); const sumRes = await this.terminal.ollamaManager.chat(sumPrompt); if (sumRes && sumRes.length < 300) { compressed = '[Earlier context: ' + sumRes.trim() + ']'; } } } catch (e) { /* fallback to truncation */ } this.context = [{ role: 'system', content: compressed }, ...recentMsgs]; this.terminal.print('πŸ“¦ Context compressed (' + oldMsgs.length + ' messages β†’ 1 summary, ~' + totalTokens + ' tokens)', 'info'); } _manualCompress() { this._compressContext().then(() => { this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens', 'info'); }).catch(e => { this.terminal.print('Compression error: ' + e.message, 'error'); }); } }