// 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() { const now = new Date(); const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); const optimizationActive = this.terminal?.ollamaManager?.optimizationEnabled !== false; const CR = String.fromCharCode(10); const optNote = CR + CR + '== TOOL EXECUTION RULES (STRICT) ==' + CR + 'STRICT RULE 1: NEVER describe what the filesystem looks like from your training data. ALWAYS call list_files() or read_file() first, then report the ACTUAL result.' + CR + 'STRICT RULE 2: If you return text without calling any tool, you have failed. Use tools for every factual question.' + CR + 'STRICT RULE 3: A response like "/bin/, /etc/, /home/" without calling list_files is ILLEGAL. Call the tool.' + CR + 'STRICT RULE 4: When calling a tool, base your answer ENTIRELY on the tool result. If result is empty, say "empty".' + CR + CR; return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n\nYou are WebX11AI, the AI assistant embedded in the WebX11 browser terminal. You have direct access to read/write files, search the web, install packages, edit modules, and run terminal commands — all via tool calls. Use tools proactively. Keep responses concise.${optNote}== TOOL REFERENCE == You have these tools available. Use them WITHOUT asking permission unless the tool description says (DESTRUCTIVE): FILESYSTEM: - list_files(path?) — List files in VFS directory (omit for current dir) - read_file(path) — Read file content (truncated past 8000 chars) - write_file(path, content) — Write to file (DESTRUCTIVE — confirm with user first) - make_directory(path) — Create a directory (DESTRUCTIVE — confirm first) - delete_file(path) — Delete file (DESTRUCTIVE — confirm first) - get_current_dir — Show current working directory SYSTEM: - get_status — Show filesystem, modules, Ollama, memory status - get_help — List all available terminal commands - run_command(command) — Execute any terminal command and capture output - font-size <8-32> — Change terminal font size (persists in localStorage) SEARCH: - web_search(query) — Search the web via proxy API (SearXNG + DDG fallback, results with title/url/snippet) - web_fetch(url) — Fetch and extract readable content from a URL via proxy API (returns title + clean text + word count) PACKAGES: - wpm_install(package) — Install WPM package by name (e.g. "widget", "calc") - wpm_search(keyword) — Search available WPM packages SKILLS (PERSISTENT PROCEDURAL MEMORY): - list_skills(category?) — Browse available skills, optionally filtered by category - load_skills(query) — Search skills relevant to current task. Call at START of every task. - create_skill(name, description, content, tags, category) — Save a new stored procedure. Call after completing complex multi-step tasks (3+ tool calls). - patch_skill(name, content) — Update an existing skill when the approach changes MODULE SELF-IMPROVEMENT: - view_module(name) — View source code of any loaded module - edit_module(name) — Copy module into editor. Opens editor for modifications. - test_module(name) — Load edited module from /_mods/ for live testing - reload_module(name) — Discard edits and reload from original URL - list_modified_modules — List modules with local edits pending - get_module_status — Full status of all modules (loaded, modified, URLs) == SKILLS WORKFLOW == 1. At the START of every task, call load_skills(query) to check if a stored procedure exists 2. Follow the skill's steps. If they're wrong, call patch_skill to fix them. 3. After a complex multi-step task (3+ tool calls), call create_skill to save the approach 4. Skills persist in /skills/ as markdown files with YAML frontmatter == MEMORY SYSTEM == - I maintain a conversation context (this.context) that persists across messages in this session - The last ${this.maxContext} exchanges are remembered - Type "ai-clear" to reset conversation memory - Type "ai-context" to see what I remember - Type "ai-model" to see/switch models - Type "ai-stop" to cancel a running AI request - Smart context profiles (set via "context ") adjust my temperature and system prompt style - Saved prompts (via "prompt", "prompt-save") let you reuse template with {variable} substitution == USER PROFILE == - Use user_get, user_set to read/write persistent user preferences across sessions - user_set dot-notation supported: user_set knownFacts.name "Alex" - Profile auto-stores known facts (name, project, preferences) - Use session_save after complex tasks to log what was done - Use session_load to review past sessions - Use session_recent at start of session to remember what was worked on before == SELF-IMPROVEMENT WORKFLOW == 1. User asks for module changes → call get_module_status to list modules 2. Call view_module(name) to read the module source 3. Call edit_module(name) to copy source into editor for modification 4. After editing, call test_module(name) to load and test the edit 5. If it works, the user keeps it. If not, call reload_module(name). == AUTO-IMPROVEMENT (SYSTEM-DRIVEN) == - If I encounter the same tool error 3+ times, a workaround skill is created automatically - After complex tasks (3+ tool calls), I run health checks and suggest improvements - I track conversation patterns and surface frequent topics - Every 20 messages, skills are audited for staleness - Use check_updates to verify module source URLs are reachable == RESPONSE STYLE == - Be concise. Use bullet points for lists. - When using tools, explain what you're doing briefly. - If a tool fails, try an alternative approach. - For web research tasks: search first, then analyze the results, then present findings. |- When the user says "update" or "improve" without specifics, use get_status + get_help to understand the current state first. == REACT PROTOCOL == For every task, follow this cycle: 1. **REASON** — Think about what you need next. What information is missing? 2. **ACT** — Call ONE tool at a time (unless multiple independent lookups are needed). 3. **OBSERVE** — Study the tool result carefully. Did it succeed? Is the data useful? 4. **DECIDE** — Do you have enough to answer? If not, go back to step 1. RULES: - Call tools in sequence when the second depends on the first result - Independent lookups (e.g. reading two unrelated files) can run in parallel - If a tool returns an error, try a different approach before giving up - After 6 tool turns, start synthesizing a final answer - Always provide a complete, helpful final answer — don't leave the user hanging`; } 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', parameters: { type: 'object', properties: { url: { type: 'string', description: 'Full URL' } }, 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', 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)' }; } 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(); } 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)); const safeTools = res.tool_calls.filter(tc => !this.isDestructive(tc.function.name)); // 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 }); // 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 }); // 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) { const names = toolCalls.map(tc => tc.function.name).join(', '); const args = toolCalls.map(tc => typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments)).join(' | '); const truncatedArgs = args.length > 60 ? args.substring(0, 60) + '...' : args; this.terminal.printHTML(`āš ļø AI wants to: ${names}(${truncatedArgs}). ai-allow to confirm, ai-deny to refuse. (Auto-denies in 120s)`); // 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 isDestructive(name) { return name === 'write_file' || name === 'delete_file' || name === 'run_command' || name === 'make_directory' || name === 'edit_module'; } 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; for (const part of parts) { if (part === '..') { saved.pop(); continue; } if (part && cur.content[part] && cur.content[part].type === 'dir') cur = cur.content[part]; else return JSON.stringify({ error: `Directory not found: ${p}` }); } 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) { return JSON.stringify({ error: 'Search API returned HTTP ' + resp.status }); } 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' }); try { const resp = await fetch('/api/extract?url=' + encodeURIComponent(url)); if (resp.status === 403) return JSON.stringify({ error: 'URL blocked — internal network' }); if (!resp.ok) { return JSON.stringify({ error: 'Fetch API returned HTTP ' + resp.status }); } const data = await resp.json(); return JSON.stringify({ title: data.title || '', content: (data.content || '').substring(0, 4000), word_count: data.word_count || 0, method: 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.dispatchCommand(cmd.split(' ')[0].toLowerCase(), cmd.split(' ').slice(1)); } 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); 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() }); 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() }); 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(`Current system prompt: ${this.systemPrompt.substring(0, 50)}...`, 'info'); return; } this.systemPrompt = args.join(' '); this.terminal.print('System prompt updated', 'success'); } 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 / 1000}s (default: 120s)`, ''); this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, ''); this.terminal.print(`skill-debounce: ${this._skillDebounceMs / 60000}min (default: 30min)`, ''); this.terminal.print('Usage: ai-config ', 'info'); this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization', 'info'); 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 < 5 || n > 600) return this.terminal.print('confirm-timeout must be 5-600 seconds', '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'); }); } }