diff options
| author | Agando <roduccomm@gmail.com> | 2026-07-09 06:00:32 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-07-09 06:00:32 +0200 |
| commit | cf7461ffeb4f53f3f50c9c692a2e4fdcecf17e82 (patch) | |
| tree | e7780cd41271381cf108c9c81d71a3cb65cfd555 /modules/core/module-aiassist.js | |
| parent | ea4d09fb5ccba12ed774fcf7c913edfa1e6a851a (diff) | |
auto: 2026-07-09_06:00 changes
Diffstat (limited to 'modules/core/module-aiassist.js')
| -rw-r--r-- | modules/core/module-aiassist.js | 934 |
1 files changed, 908 insertions, 26 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 8a0a7c8..73078d0 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -86,10 +86,10 @@ buildSystemPrompt() { } catch (e) {} let base = `Current date: ${dateStr} (${dayStr}) ${timeStr}${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 | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet | get_usage_stats(model,provider,since_hours) — LLM token/latency stats\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. ' - + 'JS: node <file.js> executes JS from VFS (alias for js-run), node -e "expr" for inline eval. No real Node.js runtime — browser JS only.'; + + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind | wallet-lookup | get_usage_stats\\n' + + 'web_fetch: GET for pages, POST with method+body for JSON APIs. JS: node <file.js> or node -e "expr" (browser JS only).\n' + + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, world-model, predict, guardrail, tar, secret-* — via help.\n' + + 'predict_task: estimate duration before complex tasks. world_model_query: ask about system state, changes, health.'; if (profilePrompt) { base = profilePrompt + '\n\n' + base; } @@ -99,6 +99,18 @@ buildSystemPrompt() { if (lp.systemPrompt.prepend) base = lp.systemPrompt.prepend + '\n\n' + base; if (lp.systemPrompt.append) base = base + '\n\n' + lp.systemPrompt.append; } + // Inject auto-fix suggestions from DebugSupervisorModule + try { + var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule; + if (ds && this._autoFixSuggestions && this._autoFixSuggestions.length > 0) { + base += '\n\n[Auto-Fix Suggestions]\n'; + for (var fi = 0; fi < this._autoFixSuggestions.length; fi++) { + var s = this._autoFixSuggestions[fi]; + base += '- ' + s.module + ': ' + s.suggestion + ' (' + s.reason + ')\n'; + } + base += '[/Auto-Fix Suggestions]'; + } + } catch (e) {} return base; } getToolDefinitions() { @@ -165,6 +177,13 @@ buildSystemPrompt() { }, { type: 'function', function: { + name: 'get_dashboard_data', + description: 'Full dashboard telemetry: modules, agents, memory, VFS, Ollama, token usage, tool latency, system health', + parameters: { type: 'object', properties: {}, required: [] } + } + }, { + type: 'function', + function: { name: 'get_help', description: 'Command reference', parameters: { type: 'object', properties: {}, required: [] } @@ -231,15 +250,18 @@ buildSystemPrompt() { type: 'function', function: { name: 'edit_module', - description: 'Edit module source (DESTRUCTIVE)', - parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } + description: 'Edit module source (DESTRUCTIVE). If code is provided, writes directly to VFS and auto-tests. If no code, opens the visual editor (requires manual Save).', + parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' }, code: { type: 'string', description: 'Full new source code. When provided, writes directly and auto-tests without opening the editor.' } }, 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'] } + description: 'Test an edited module: loads it, checks JS errors, verifies commands registered, runs optional test assertions. Auto-rolls-back on failure.', + parameters: { type: 'object', properties: { + name: { type: 'string', description: 'Module class name' }, + tests: { type: 'array', description: 'Optional test assertions: [{command, expect}]', items: { type: 'object', properties: { command: { type: 'string' }, expect: { type: 'string' } } } } + }, required: ['name'] } } }, { type: 'function', @@ -440,7 +462,7 @@ buildSystemPrompt() { required: [] } } -}, { + }, { type: 'function', function: { name: 'smart_context_set', @@ -471,7 +493,7 @@ buildSystemPrompt() { type: 'function', function: { name: 'vaultmind_query', - description: "Query VaultMind AI for Hyperliquid market data (prices, portfolio, transactions, copy-trade) or ask an AI agent. Uses same-origin /vmai/ API.", + description: "Query VaultMind AI for Hyperliquid market data (prices, portfolio, transactions, copy-trade) or ask an AI agent. Uses same-origin /vmai-api/ API.", parameters: { type: 'object', properties: { @@ -533,11 +555,11 @@ buildSystemPrompt() { }, required: [] } - }, - { - type: 'function', - function: { - name: 'agent_loop_list', + } + }, { + type: 'function', + function: { + name: 'agent_loop_list', description: 'List all available agent loop profiles with their settings and benchmark results.', parameters: { type: 'object', properties: {}, required: [] } } @@ -589,9 +611,77 @@ buildSystemPrompt() { required: [] } } - } - ]; - } + }, + { + type: 'function', + function: { + name: 'predict_task', + description: 'Predict task duration, tool turns, and confidence based on historical execution data. Use before starting complex tasks to estimate resource needs.', + parameters: { + type: 'object', + properties: { + task: { type: 'string', description: 'Task description to predict' } + }, + required: ['task'] + } + } + }, + { + type: 'function', + function: { + name: 'world_model_query', + description: 'Query the system world model for self-awareness: modules, skills, files, agents, health, memory graph, recent changes, active projects, telemetry. Ask questions like "what changed" or "how many modules".', + parameters: { + type: 'object', + properties: { + question: { type: 'string', description: 'What to ask: modules, skills, files, agents, health, memory, changes, projects, telemetry, summary' } + }, + required: ['question'] + } + } + }, + { + type: 'function', + function: { + name: 'health_check', + description: 'Run a system health check after a module edit. Verifies: no JS errors, all core commands work, world model is valid, agents are alive.', + parameters: { type: 'object', properties: {} } + } + }, + { + type: 'function', + function: { + name: 'diff_module', + description: 'Show a diff between the original module source and the edited version in _mods/. Returns line-by-line changes.', + parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } + } + }, + { + type: 'function', + function: { + name: 'ship_module', + description: 'Full pipeline: edit → test → health_check → regression suite. Auto-rolls-back on any failure. Set commit:true to also persist to server (requires human approval). Without commit, runs as dry-run.', + parameters: { type: 'object', properties: { + name: { type: 'string', description: 'Module class name' }, + code: { type: 'string', description: 'Full new source code' }, + tests: { type: 'array', description: 'Test assertions [{command, expect}]', items: { type: 'object', properties: { command: { type: 'string' }, expect: { type: 'string' } } } }, + commit: { type: 'boolean', description: 'If true, request server persistence (needs human approval). Default: false.' } + }, required: ['name', 'code'] } + } + }, + { + type: 'function', + function: { + name: 'commit_module', + description: 'Request to persist an edited module from VFS _mods/ to the server file system via WebDAV. Creates a pending commit — user must type commit-approve in the terminal to execute. Includes bundle sync and version bump.', + parameters: { type: 'object', properties: { + name: { type: 'string', description: 'Module class name' }, + file: { type: 'string', description: 'Target server path (e.g. modules/ext/shell.js). Auto-detect from module registry if omitted.' } + }, required: ['name'] } + } + } + ]; + } getHelpInfo() { return { 'ai': 'ai <question> — Ask the AI assistant anything', @@ -614,6 +704,8 @@ buildSystemPrompt() { 'reset-mem': 'reset-mem — Reset AI memory (logs, context, profile, guardrails) without losing VFS or config', 'vaultmind-connect': 'vaultmind-connect [testnet|mainnet] — Connect VaultMind wallet using hyperliquid_key from SecretVault', 'vaultmind-status': 'vaultmind-status — Check VaultMind session status', + 'vaultmind-query': 'vaultmind-query <prices|portfolio|transactions|copy-trade|chat> [message] — Query VaultMind API', + 'wallet-lookup': 'wallet-lookup <address> — Look up Hyperliquid wallet summary', }; } registerCommands() { @@ -629,6 +721,21 @@ buildSystemPrompt() { this.commands['ai-model'] = (args) => this.showModel(args); this.commands['ai-compress'] = () => this._manualCompress(); this.commands['ai-config'] = (args) => this.aiConfig(args); + this.commands['commit-approve'] = (args) => this._executeCommit(args.join(' ')); + this.commands['commit-deny'] = () => { + if (!this._pendingCommit) return this.terminal.print('No pending commit.', 'warning'); + const name = this._pendingCommit.name; + this._pendingCommit = null; + this.terminal.print('❌ Commit denied for ' + name, 'warning'); + }; + this.commands['commit-status'] = () => { + if (!this._pendingCommit) return this.terminal.print('No pending commit.', 'info'); + const pc = this._pendingCommit; + this.terminal.print('📋 Pending commit: ' + pc.name, 'info'); + this.terminal.print(' Target: ' + pc.serverPath, 'info'); + if (pc.bundlePath) this.terminal.print(' Bundle: ' + pc.bundlePath, 'info'); + this.terminal.print(' Changes: ' + pc.changes + ' line(s)', 'info'); + }; this.commands['agent-loops'] = (args) => this.agentLoopsCmd(args); this.commands['bench'] = (args) => this.benchCmd(args); this.commands['bench-compare'] = (args) => this.benchCompareCmd(args); @@ -649,6 +756,8 @@ buildSystemPrompt() { this.commands['reset-mem'] = () => this.resetMem(); this.commands['vaultmind-connect'] = (args) => this._vaultmindConnectCmd(args); this.commands['vaultmind-status'] = () => this._vaultmindStatusCmd(); + this.commands['vaultmind-query'] = (args) => this._vaultmindQueryCmd(args); + this.commands['wallet-lookup'] = (args) => this._walletLookupCmd(args); } async chat(args) { const userMessage = args.join(' '); @@ -668,6 +777,18 @@ buildSystemPrompt() { } this._chatLock = true; this.terminal.markDirty(); + // Auto-route context based on intent + try { + const scm = this.terminal?.moduleManager?.modules?.SmartContextModule; + if (scm && typeof scm.autoRouteContext === 'function') { + const routeResult = scm.autoRouteContext(userMessage); + if (routeResult.switched) { + this.terminal.print('🔄 Context auto-switched: ' + routeResult.from + ' → ' + routeResult.to + ' (' + routeResult.confidence + '% confidence)', 'info'); + // Rebuild system prompt after context switch + this.systemPrompt = this.buildSystemPrompt(); + } + } + } catch (e) { console.warn('[AI] Auto-routing error:', e.message); } // Re-read context from localStorage (auto-restore may have written it after init) this._loadContext(); // Compress context if it exceeds the token threshold @@ -751,6 +872,24 @@ buildSystemPrompt() { if (loopResult.toolTurns >= 2 && !this._cancelled) { await this.afterActionReflection(userMessage, loopResult.toolTurns); } + // Feed result to predictor for learning (cold module activation) + if (loopResult && !this._cancelled && loopResult.toolTurns >= 1) { + try { + const predictor = this.terminal.moduleManager.modules['PredictorModule']; + if (predictor && typeof predictor.recordResult === 'function') { + const features = predictor._extractFeatures(userMessage); + const duration = (Date.now() - lockStart) / 1000; + predictor.recordResult(features, duration, loopResult.toolTurns, !this._cancelled); + } + } catch (e) { /* predictor learning is best-effort */ } + // Record task in memory graph for direct AI conversations (not just orchestrator) + try { + const mg = this.terminal.moduleManager.modules['MemoryGraphModule']; + if (mg && typeof mg.recordTask === 'function') { + mg.recordTask(userMessage, loopResult.finalAnswer || '', 'ai-assistant'); + } + } catch (e) { /* memory graph is best-effort */ } + } // Self-improvement: post-run anomaly detection if (loopResult && !this._cancelled) { try { @@ -804,6 +943,280 @@ buildSystemPrompt() { }); } } + // ─── Self-Improvement Helpers ─── + async _captureCommandOutput(cmd) { + 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; } + return output.trim(); + } + async _captureShellCommand(cmd) { + // Routes through executeCommand (shell parser) for >, |, &&, export, etc. + const originalPrint = this.terminal.print.bind(this.terminal); + let output = ''; + this.terminal.print = (text, type) => { output += text + '\n'; }; + try { await this.terminal.executeCommand(cmd); } + finally { this.terminal.print = originalPrint; } + return output.trim(); + } + async _captureConsoleErrors(durationMs) { + durationMs = durationMs || 500; + var errors = []; + var origError = console.error; + var origOnError = window.onerror; + console.error = function() { + var args = Array.prototype.slice.call(arguments); + errors.push(args.join(' ')); + }; + window.onerror = function(msg, src, line, col, err) { + errors.push(msg + (err ? ' — ' + err.message : '')); + }; + await new Promise(function(r) { setTimeout(r, durationMs); }); + console.error = origError; + window.onerror = origOnError; + return errors; + } + _regressionTasks() { + return [ + { id: 'shell-export', module: 'ShellModule', command: 'export REGTEST=ok', expect: 'REGTEST', useShell: true }, + { id: 'shell-redirect', module: 'ShellModule', command: 'echo hi > _regtest.txt', expect: 'Redirected', useShell: true }, + { id: 'shell-chain', module: 'ShellModule', command: 'echo a && echo b', expect: 'b', useShell: true }, + { id: 'shell-pipe', module: 'ShellModule', command: 'echo hello | grep hello', expect: 'hello', useShell: true }, + { id: 'fs-ls', module: 'SystemModule', command: 'ls', expect: '' }, + { id: 'fs-pwd', module: 'SystemModule', command: 'pwd', expect: '/' }, + { id: 'orch-agents', module: 'OrchestratorModule', command: 'list-agents', expect: 'agent' }, + { id: 'ai-tools', module: 'WebX11AIAssistantModule', command: 'list_modules', expect: 'Module' }, + { id: 'calc-basic', module: 'CalcModule', command: 'calc 1+1', expect: '2' }, + { id: 'calc-advanced', module: 'CalcModule', command: 'calc 2*3', expect: '6' }, + { id: 'webdav-ls', module: 'WebDAVModule', command: 'webdav-ls', expect: '' }, + { id: 'cron-list', module: 'CronModule', command: 'cron-list', expect: '' }, + { id: 'theme-list', module: 'ThemeModule', command: 'theme-list', expect: '' }, + { id: 'mindmap-list', module: 'MindmapModule', command: 'mindmap-list', expect: '' }, + { id: 'media-status', module: 'MediaModule', command: 'media-status', expect: '' }, + { id: 'skill-list', module: 'SkillPackCoreModule', command: 'skill-list', expect: '' }, + { id: 'crypto-encode', module: 'CryptoGzipModule', command: 'crypto-encode test', expect: '' }, + { id: 'debug-toggle', module: 'DebugPanelModule', command: 'debug', expect: '' }, + { id: 'voice-status', module: 'VoiceModule', command: 'voice-status', expect: '' }, + { id: 'wpm-status', module: 'WPMModule', command: 'wpm-status', expect: '' }, + { id: 'console-help', module: 'ConsoleModule', command: 'console-help', expect: '' }, + { id: 'tutorial-status', module: 'TutorialModule', command: 'tutorial-status', expect: '' }, + ]; + } + async _runRegressionSuite(moduleName) { + const tests = this._regressionTasks().filter(t => t.module === moduleName); + if (tests.length === 0) return { module: moduleName, total: 0, passed: 0, failed: 0, results: [], allPassed: true, message: 'No regression tests for ' + moduleName }; + const results = []; + for (const t of tests) { + try { + const out = t.useShell ? await this._captureShellCommand(t.command) : await this._captureCommandOutput(t.command); + const pass = t.expect ? out.includes(t.expect) : out.length > 0; + results.push({ id: t.id, command: t.command, pass, output: out.substring(0, 100) }); + } catch (e) { + results.push({ id: t.id, command: t.command, pass: false, output: 'Error: ' + e.message }); + } + } + const passed = results.filter(r => r.pass).length; + return { module: moduleName, total: results.length, passed, failed: results.length - passed, results, allPassed: passed === results.length }; + } + // ─── Phase D: Server Persistence ─── + async _webdavPush(remotePath, content) { + const webdav = this.terminal.moduleManager.modules['WebDAVModule']; + if (!webdav) throw new Error('WebDAV module not loaded'); + // Use the webdav module's internal request method + const resp = await webdav._webdavRequest('PUT', remotePath, { + body: content, + headers: { 'Content-Type': 'text/plain' } + }); + if (!resp.ok && resp.status !== 201 && resp.status !== 204) { + throw new Error('WebDAV PUT failed: HTTP ' + resp.status); + } + return true; + } + async _webdavGet(remotePath) { + const webdav = this.terminal.moduleManager.modules['WebDAVModule']; + if (!webdav) throw new Error('WebDAV module not loaded'); + const resp = await webdav._webdavRequest('GET', remotePath); + if (!resp.ok) throw new Error('WebDAV GET failed: HTTP ' + resp.status); + return await resp.text(); + } + _extractClassName(code) { + const m = code.match(/^class\s+(\w+)/m); + return m ? m[1] : null; + } + _extractClassBlock(code, className) { + // Extract the class definition from standalone code (without self-registration block) + const classStart = code.indexOf('class ' + className); + if (classStart === -1) return null; + // Find matching closing brace by counting + let depth = 0; + let end = classStart; + for (let i = classStart; i < code.length; i++) { + if (code[i] === '{') depth++; + else if (code[i] === '}') { + depth--; + if (depth === 0) { end = i + 1; break; } + } + } + return code.substring(classStart, end); + } + async _syncBundle(bundlePath, modName, newCode) { + try { + // Download current bundle + const bundle = await this._webdavGet(bundlePath); + // Find the class block in the bundle + const className = modName; + const classStart = bundle.indexOf('class ' + className); + if (classStart === -1) return { synced: false, reason: 'Class not found in bundle' }; + // Find matching closing brace + let depth = 0; + let classEnd = classStart; + for (let i = classStart; i < bundle.length; i++) { + if (bundle[i] === '{') depth++; + else if (bundle[i] === '}') { + depth--; + if (depth === 0) { classEnd = i + 1; break; } + } + } + // Extract new class block from the edited code + const newClassBlock = this._extractClassBlock(newCode, className); + if (!newClassBlock) return { synced: false, reason: 'Could not extract class from new code' }; + // Replace in bundle + const newBundle = bundle.substring(0, classStart) + newClassBlock + bundle.substring(classEnd); + // Validate with vm.Script equivalent (Function constructor) + try { new Function(newBundle); } catch (e) { + return { synced: false, reason: 'Bundle syntax error after replace: ' + e.message }; + } + // Upload + await this._webdavPush(bundlePath, newBundle); + return { synced: true, bytes: newBundle.length }; + } catch (e) { + return { synced: false, reason: e.message }; + } + } + _parseVersion(ver) { + // Handle both old format (2026-07-06-v4.6.01) and new format (v4.6.01) + const m = ver.match(/v(\d+)\.(\d+)\.(\d+)/); + if (!m) return { major: 4, minor: 6, patch: 0 }; + return { major: parseInt(m[1]), minor: parseInt(m[2]), patch: parseInt(m[3]) }; + } + _bumpVersionString(ver) { + const v = this._parseVersion(ver); + return 'v' + v.major + '.' + v.minor + '.' + (v.patch + 1); + } + async _bumpVersion() { + // Read current version from the page + const currentVer = window.NEWOS4_VERSION || 'v4.6.01'; + const newVer = this._bumpVersionString(currentVer); + // 1. Update main.html — replace all ?v= occurrences + const mainHtml = await this._webdavGet('main.html'); + const updatedHtml = mainHtml.replace(/\?v=[^"']+/g, '?v=' + newVer); + await this._webdavPush('main.html', updatedHtml); + // 2. Update module-loader.js — replace LATEST_VER + const loader = await this._webdavGet('modules/core/module-loader.js'); + const updatedLoader = loader.replace(/const LATEST_VER = '[^']+'/, "const LATEST_VER = '" + newVer + "'"); + await this._webdavPush('modules/core/module-loader.js', updatedLoader); + return { from: currentVer, to: newVer }; + } + _calculateConfidence(modName, testResult, healthResult, regResult, changeCount) { + var score = 100; + if (changeCount > 50) score -= 20; + if (changeCount > 100) score -= 20; + if (regResult.total === 0) score -= 30; + var critical = ['ShellModule', 'SystemModule', 'OrchestratorModule', 'WebX11AIAssistantModule']; + if (critical.indexOf(modName) !== -1) score -= 15; + if (testResult.tests && testResult.tests.length >= 3) score += 10; + if (healthResult.jsErrors && healthResult.jsErrors.length > 0) score -= 50; + return Math.max(0, Math.min(100, score)); + } + + async _executeCommit(comment) { + const pc = this._pendingCommit; + if (!pc) return this.terminal.print('No pending commit. Use commit_module first.', 'warning'); + // Rate limit check + const today = new Date().toISOString().split('T')[0]; + const commitLog = this.terminal.fs.fsReadJSON('/_mods/commit-log.json') || {}; + if (commitLog[today] && commitLog[today] >= 5) { + return this.terminal.print('⚠️ Rate limit: 5 commits today. Try tomorrow.', 'warning'); + } + // Cooldown check (10 min) + if (commitLog._lastCommitTime && Date.now() - commitLog._lastCommitTime < 600000) { + const wait = Math.ceil((600000 - (Date.now() - commitLog._lastCommitTime)) / 60000); + return this.terminal.print('⏱ Cooldown: ' + wait + ' min until next commit.', 'warning'); + } + this.terminal.print('🚀 Executing commit for ' + pc.name + '...', 'info'); + const results = { module: pc.name, steps: [] }; + // Step 1: Push standalone file + try { + await this._webdavPush(pc.serverPath, pc.code); + results.steps.push({ step: 'standalone', status: 'ok', path: pc.serverPath }); + this.terminal.print(' ✅ Standalone: ' + pc.serverPath, 'success'); + } catch (e) { + results.steps.push({ step: 'standalone', status: 'fail', error: e.message }); + this.terminal.print(' ❌ Standalone failed: ' + e.message, 'error'); + return results; + } + // Step 1b: Post-commit syntax verification + try { + const verifyResp = await fetch(pc.serverPath + '?v=' + Date.now()); + const committedCode = await verifyResp.text(); + new Function(committedCode); + results.steps.push({ step: 'verify', status: 'ok' }); + this.terminal.print(' ✅ Post-commit syntax check passed', 'success'); + } catch (e) { + results.steps.push({ step: 'verify', status: 'fail', error: e.message }); + this.terminal.print(' ❌ Post-commit syntax check failed: ' + e.message, 'error'); + // Auto-revert + try { + await this._webdavPush(pc.serverPath, pc.origCode || ''); + this.terminal.print(' ⏪ Auto-reverted to original code', 'warning'); + results.reverted = true; + results.syntaxError = e.message; + } catch (revertErr) { + this.terminal.print(' ❌ Auto-revert FAILED: ' + revertErr.message, 'error'); + results.revertFailed = true; + } + this._pendingCommit = null; + return results; + } + // Step 2: Sync bundle + if (pc.bundlePath) { + try { + const syncResult = await this._syncBundle(pc.bundlePath, pc.name, pc.code); + results.steps.push({ step: 'bundle', status: syncResult.synced ? 'ok' : 'skip', detail: syncResult }); + if (syncResult.synced) this.terminal.print(' ✅ Bundle: ' + pc.bundlePath, 'success'); + else this.terminal.print(' ⚠️ Bundle sync skipped: ' + syncResult.reason, 'warning'); + } catch (e) { + results.steps.push({ step: 'bundle', status: 'fail', error: e.message }); + this.terminal.print(' ❌ Bundle sync failed: ' + e.message, 'error'); + } + } + // Step 3: Bump version + try { + const verResult = await this._bumpVersion(); + results.steps.push({ step: 'version', status: 'ok', from: verResult.from, to: verResult.to }); + this.terminal.print(' ✅ Version: ' + verResult.from + ' → ' + verResult.to, 'success'); + results.newVersion = verResult.to; + } catch (e) { + results.steps.push({ step: 'version', status: 'fail', error: e.message }); + this.terminal.print(' ⚠️ Version bump failed: ' + e.message, 'warning'); + } + // Step 4: Cleanup _mods/ + this.terminal.fs.deleteFile('_mods/' + pc.name + '.js'); + this.terminal.fs.deleteFile('_mods/' + pc.name + '.orig.js'); + this.terminal.fs.deleteFile('_mods/' + pc.name + '.meta.json'); + // Step 5: Rate limit update + commitLog[today] = (commitLog[today] || 0) + 1; + commitLog._lastCommitTime = Date.now(); + this.terminal.fs.fsWriteJSON('/_mods/commit-log.json', commitLog); + // Clear pending + this._pendingCommit = null; + this.terminal.print('🎉 Commit complete! Refresh to load ' + (results.newVersion || 'new version'), 'success'); + if (comment) this.terminal.print(' Comment: ' + comment, 'info'); + results.success = true; + return results; + } _summarizeObservation(result, maxLen) { if (!result || typeof result !== 'string' || result.length <= maxLen) return result || ''; const lines = result.split('\n'); @@ -1630,7 +2043,7 @@ buildSystemPrompt() { } try { this.terminal.fs.root = JSON.parse(JSON.stringify(this._shadowSnapshot)); - this.terminal.fs.saveToLocalStorage(); + this.terminal.fs.saveToStorage(); this._shadowSnapshot = null; this.terminal.print('↩️ VFS rolled back to pre-shadow state', 'success'); } catch (e) { @@ -1651,8 +2064,6 @@ buildSystemPrompt() { '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', 'debug','debug-ai','session-new','mem-debug','reset-mem', - // Node bridge — alias for js-run - 'node', // Synced from terminal whitelist — read-only orchestration/A2A/agent commands 'orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result', 'orchestrator-status','orchestrator-list','orchestrator-config','orchestrator-restart-policy', @@ -1769,6 +2180,9 @@ buildSystemPrompt() { memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A' }); } + case 'get_dashboard_data': { + return JSON.stringify(await this._collectDashboardData()); + } case 'get_help': { const cmds = this.terminal.moduleManager.getAllCommands(); return JSON.stringify(Object.keys(cmds).sort()); @@ -1982,8 +2396,12 @@ buildSystemPrompt() { 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)}` }); + try { + const result = await orch.delegateTask(task); + return JSON.stringify({ success: true, task: task.substring(0, 80), result: typeof result === 'string' ? result.substring(0, 500) : 'completed' }); + } catch (e) { + return JSON.stringify({ error: 'Delegation failed: ' + e.message }); + } } case 'spawn_agent': { const role = a.role || 'worker'; @@ -2236,7 +2654,7 @@ buildSystemPrompt() { return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + validEps.join(', ') }); } try { - const epMap = { prices: '/vmai/market/prices', portfolio: '/vmai/portfolio', transactions: '/vmai/transactions/summary', 'copy-trade': '/vmai/copy-trading/traders', chat: '/vmai/chat' }; + const epMap = { prices: '/vmai-api/market/prices', portfolio: '/vmai-api/portfolio', transactions: '/vmai-api/transactions/summary', 'copy-trade': '/vmai-api/copy-trading/traders', chat: '/vmai-api/chat' }; let url = epMap[ep]; let opts = { method: 'GET', headers: { 'Accept': 'application/json' } }; // Portfolio, transactions, copy-trade need auth token @@ -2322,12 +2740,48 @@ buildSystemPrompt() { }.bind(this)); return JSON.stringify({ success: true, message: "Benchmark started on '" + profileName + "'", tasks: taskIds || 'all' }); } + case 'predict_task': { + var predictor = this.terminal.moduleManager.modules['PredictorModule']; + if (!predictor) return JSON.stringify({ error: 'Predictor module not loaded' }); + var taskDesc = a.task || ''; + if (!taskDesc) return JSON.stringify({ error: 'task parameter required' }); + var pFeatures = predictor._extractFeatures(taskDesc); + var pSimilar = predictor._findSimilar(pFeatures); + var pResult = predictor._computePrediction(pSimilar, pFeatures); + return JSON.stringify({ + task: taskDesc.substring(0, 80), + estimatedDuration: pResult.duration + 's', + estimatedToolTurns: pResult.toolTurns, + confidence: Math.round(pResult.confidence * 100) + '%', + similarTasks: pResult.similarCount, + recommendation: pResult.confidence >= predictor._CONFIDENCE_THRESHOLD && pResult.duration > 30 + ? 'Set TTL to ' + Math.ceil(pResult.duration * 1.5) + 's' + : 'No special action needed' + }); + } + case 'world_model_query': { + var wm = this.terminal.moduleManager.modules['WorldModelModule']; + if (!wm) return JSON.stringify({ error: 'WorldModelModule not loaded' }); + if (!wm._model) return JSON.stringify({ error: 'World model not synthesized yet — run world-model-refresh' }); + var wmAnswer = wm._answerQuery(a.question || ''); + return JSON.stringify({ question: a.question, answer: wmAnswer, version: wm._model.version, synthesized: wm._model.synthesized }); + } + case 'health_check': + case 'diff_module': + case 'ship_module': + case 'commit_module': + 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(); + // Also feed into DebugSupervisorModule + try { + var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule; + if (ds) ds.recordToolError(name, a, e.message); + } catch (dsErr) {} return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) }); } } @@ -2421,10 +2875,61 @@ buildSystemPrompt() { } case 'test_module': { const modName = a.name || ''; + const tests = a.tests || null; 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`; + // Save current module state for rollback + const oldInstance = this.terminal.moduleManager.modules[modName]; + const oldCommands = oldInstance ? Object.keys(oldInstance.commands) : []; + // Load new version + const ok = await this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`); + if (!ok) { + if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance; + return JSON.stringify({ error: 'Module failed to load', rolledBack: true }); + } + // Check for JS errors after load + const jsErrors = await this._captureConsoleErrors(300); + if (jsErrors.length > 0) { + if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance; + return JSON.stringify({ error: 'JS errors after load', errors: jsErrors.slice(0, 5), rolledBack: true }); + } + // Verify commands registered + const newInstance = this.terminal.moduleManager.modules[modName]; + const newCommands = newInstance ? Object.keys(newInstance.commands) : []; + const lostCommands = oldCommands.filter(c => !newCommands.includes(c)); + if (lostCommands.length > 0) { + if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance; + return JSON.stringify({ error: 'Commands lost after edit', lost: lostCommands, rolledBack: true }); + } + // Run test assertions if provided + if (tests && Array.isArray(tests) && tests.length > 0) { + const results = []; + for (const t of tests) { + const cmd = t.command || ''; + // Use shell parser for commands with >, |, &&, ||, ;, export, set + const useShell = /[>|&;]/.test(cmd) || cmd.startsWith('export ') || cmd.startsWith('set '); + const output = useShell ? await this._captureShellCommand(cmd) : await this._captureCommandOutput(cmd); + const passed = t.expect ? output.includes(t.expect) : output.length > 0; + results.push({ command: cmd, expected: t.expect || '(any output)', actual: output.substring(0, 200), pass: passed }); + } + const failed = results.filter(r => !r.pass).length; + if (failed > 0) { + if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance; + return JSON.stringify({ error: failed + '/' + results.length + ' tests failed', results, rolledBack: true }); + } + return JSON.stringify({ success: true, module: modName, commands: newCommands.length, tests: results }); + } + // Smoke test: run first command if available + if (newCommands.length > 0) { + try { + const smokeOut = await this._captureCommandOutput(newCommands[0]); + return JSON.stringify({ success: true, module: modName, commands: newCommands.length, smokeTest: { command: newCommands[0], output: smokeOut.substring(0, 200) } }); + } catch (e) { + if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance; + return JSON.stringify({ error: 'Smoke test failed: ' + e.message, rolledBack: true }); + } + } + return JSON.stringify({ success: true, module: modName, commands: newCommands.length }); } case 'reload_module': { const modName = a.name || ''; @@ -2488,11 +2993,181 @@ buildSystemPrompt() { } return JSON.stringify({ modules, total: modules.length, hasLocalEdits: hasMods }); } + case 'health_check': { + const results = {}; + const coreCmds = ['ls', 'pwd', 'echo test', 'help', 'grep', 'find', 'tree', 'cat']; + results.coreCommands = {}; + for (const cmd of coreCmds) { + try { + const out = await this._captureCommandOutput(cmd); + results.coreCommands[cmd] = (out.length > 0 || cmd === 'ls') ? 'ok' : 'empty output'; + } catch (e) { + results.coreCommands[cmd] = 'FAIL: ' + e.message; + } + } + const mm = this.terminal.moduleManager; + results.moduleCount = Object.keys(mm.modules).length; + const wm = this.terminal.fs.fsReadJSON('system/world-model.json'); + results.worldModelVersion = wm ? wm.version : 'not found'; + results.versionMatch = wm && wm.version === (typeof window !== 'undefined' ? window.NEWOS4_VERSION : ''); + const orch = mm.modules['OrchestratorModule']; + if (orch) { + const agents = Object.values(orch._agents); + results.agents = { total: agents.length, healthy: agents.filter(a => a.status !== 'failed').length, failed: agents.filter(a => a.status === 'failed').length }; + } + results.jsErrors = await this._captureConsoleErrors(500); + const allCoreOk = Object.values(results.coreCommands).every(v => v === 'ok'); + results.healthy = allCoreOk && results.versionMatch && results.jsErrors.length === 0; + return JSON.stringify(results); + } + case 'diff_module': { + const modName = a.name || ''; + const newCode = this.terminal.fs.readFile(`_mods/${modName}.js`); + if (!newCode) return JSON.stringify({ error: `No edit for ${modName}` }); + let origCode = this.terminal.fs.readFile(`_mods/${modName}.orig.js`); + if (!origCode || origCode === 'placeholder') { + const instance = this.terminal.moduleManager.modules[modName]; + if (instance) origCode = instance.constructor.toString(); + } + if (!origCode) return JSON.stringify({ error: 'No original source found' }); + const origLines = origCode.split('\n'); + const newLines = newCode.split('\n'); + const changes = []; + const maxLines = Math.max(origLines.length, newLines.length); + for (let i = 0; i < maxLines; i++) { + if (origLines[i] !== newLines[i]) { + if (origLines[i] && !newLines[i]) changes.push({ line: i+1, type: 'removed', content: origLines[i].substring(0, 120) }); + else if (!origLines[i] && newLines[i]) changes.push({ line: i+1, type: 'added', content: newLines[i].substring(0, 120) }); + else changes.push({ line: i+1, type: 'changed', old: (origLines[i]||'').substring(0, 80), new: (newLines[i]||'').substring(0, 80) }); + } + } + return JSON.stringify({ module: modName, originalLines: origLines.length, newLines: newLines.length, changes: changes.length, diff: changes.slice(0, 50) }); + } + case 'ship_module': { + const modName = a.name || ''; + const newCode = a.code || ''; + const tests = a.tests || []; + const shouldCommit = a.commit === true; + if (!modName || !newCode) return JSON.stringify({ error: 'name and code required' }); + // Step 1: Write to _mods/ + this.terminal.fs.writeFile(`_mods/${modName}.js`, newCode); + this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ editedAt: new Date().toISOString(), editedBy: 'ship_module' })); + // Step 2: Test + const testResult = await this.runModuleTool('test_module', JSON.stringify({ name: modName, tests })); + const testParsed = JSON.parse(testResult); + if (testParsed.error) { + return JSON.stringify({ step: 'test', error: testParsed.error, rolledBack: testParsed.rolledBack, pipeline: 'aborted at test' }); + } + // Step 3: Health check + const healthResult = await this.runModuleTool('health_check', '{}'); + const healthParsed = JSON.parse(healthResult); + if (!healthParsed.healthy) { + await this.runModuleTool('reload_module', JSON.stringify({ name: modName })); + return JSON.stringify({ step: 'health_check', error: 'System unhealthy after edit', health: healthParsed, pipeline: 'aborted at health_check, rolled back' }); + } + // Step 4: Regression suite + const regResult = await this._runRegressionSuite(modName); + if (!regResult.allPassed) { + await this.runModuleTool('reload_module', JSON.stringify({ name: modName })); + return JSON.stringify({ step: 'regression', error: 'Regression tests failed', regression: regResult, pipeline: 'aborted at regression, rolled back' }); + } + // Step 5: Calculate confidence score + const changeCount = newCode.split('\n').length; + const confidence = this._calculateConfidence(modName, testParsed, healthParsed, regResult, changeCount); + // Step 6: Commit (if requested) + if (shouldCommit) { + if (confidence >= 70) { + // Auto-commit: execute directly + const commitResult = await this.runModuleTool('commit_module', JSON.stringify({ name: modName })); + const commitParsed = JSON.parse(commitResult); + if (commitParsed.pendingCommit) { + // Execute the pending commit immediately + const execResult = await this._executeCommit('auto: confidence=' + confidence); + return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → AUTO-COMMIT (confidence=' + confidence + ')', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, autoCommitted: true, commitResult: execResult }); + } + return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → auto-commit', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, commit: commitParsed }); + } else { + // Confidence too low — human approval required + const commitResult = await this.runModuleTool('commit_module', JSON.stringify({ name: modName })); + return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → HUMAN APPROVAL REQUIRED (confidence=' + confidence + ' < 70)', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, commit: JSON.parse(commitResult), humanApprovalRequired: true }); + } + } + return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression (dry-run)', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, next: 'Call commit_module or ship_module with commit:true to persist' }); + } + case 'commit_module': { + const modName = a.name || ''; + const targetFile = a.file || ''; + const newCode = this.terminal.fs.readFile(`_mods/${modName}.js`); + if (!newCode) return JSON.stringify({ error: `No local edit for ${modName}. Run edit_module or ship_module first.` }); + + // Auto-detect target file from module registry + let serverPath = targetFile; + if (!serverPath) { + const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName); + if (entry && entry.url) { + serverPath = entry.url.replace(/^https?:\/\/[^/]+/, '').replace(/\?v=.*/, ''); + } + } + if (!serverPath) return JSON.stringify({ error: 'Cannot determine server path. Provide file= parameter (e.g. modules/ext/shell.js).' }); + + // Get diff + let origCode = this.terminal.fs.readFile(`_mods/${modName}.orig.js`); + if (!origCode || origCode === 'placeholder') { + const instance = this.terminal.moduleManager.modules[modName]; + if (instance) origCode = instance.constructor.toString(); + } + const origLines = origCode ? origCode.split('\n') : []; + const newLines = newCode.split('\n'); + let changeCount = 0; + for (let i = 0; i < Math.max(origLines.length, newLines.length); i++) { + if (origLines[i] !== newLines[i]) changeCount++; + } + + // Detect bundle path + let bundlePath = null; + if (serverPath.includes('/modules/core/') || serverPath.includes('module-aiassist')) { + bundlePath = 'modules/core/core-bundle.js'; + } else if (serverPath.includes('/modules/ext/')) { + bundlePath = 'modules/ext/critical-ext-bundle.js'; + } + + // Store pending commit + this._pendingCommit = { + name: modName, + code: newCode, + origCode: origCode || '', + serverPath: serverPath, + bundlePath: bundlePath, + changes: changeCount, + createdAt: Date.now() + }; + + this.terminal.print('📋 Commit request created for ' + modName, 'info'); + this.terminal.print(' Target: ' + serverPath, 'info'); + if (bundlePath) this.terminal.print(' Bundle: ' + bundlePath, 'info'); + this.terminal.print(' Changes: ' + changeCount + ' line(s) different', 'info'); + this.terminal.print(' Type \'commit-approve\' to persist or \'commit-deny\' to cancel', 'warning'); + + return JSON.stringify({ + pendingCommit: true, + module: modName, + serverPath: serverPath, + bundlePath: bundlePath, + changes: changeCount, + newLines: newLines.length, + message: 'Commit request created. User must type commit-approve to execute.' + }); + } 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(); + // Also feed into DebugSupervisorModule + try { + var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule; + if (ds) ds.recordToolError(name, a, e.message); + } catch (dsErr) {} return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) }); } } @@ -2827,6 +3502,19 @@ buildSystemPrompt() { } try { localStorage.setItem('webx11_ai_patterns', JSON.stringify(this._commandFreq)); } catch (e) {} } + _buildToolToModuleMap() { + this._toolToModuleMap = {}; + const mm = this.terminal.moduleManager; + if (!mm || !mm.modules) return; + for (const [modName, mod] of Object.entries(mm.modules)) { + if (mod.commands) { + for (const cmdName of Object.keys(mod.commands)) { + this._toolToModuleMap[cmdName] = modName; + } + } + } + } + async _autoCreateErrorSkill() { const grouped = {}; for (const e of this._errorLog) { @@ -3106,6 +3794,49 @@ buildSystemPrompt() { this.terminal.print('❌ No VaultMind session. Use: vaultmind-connect [testnet|mainnet]', 'warning'); } } + async _vaultmindQueryCmd(args) { + const ep = args[0]; + const validEps = ['prices', 'portfolio', 'transactions', 'copy-trade', 'chat']; + if (!validEps.includes(ep)) { + this.terminal.print('Usage: vaultmind-query <prices|portfolio|transactions|copy-trade|chat> [message|agent]', 'warning'); return; + } + try { + const epMap = { prices: '/vmai-api/market/prices', portfolio: '/vmai-api/portfolio', transactions: '/vmai-api/transactions/summary', 'copy-trade': '/vmai-api/copy-trading/traders', chat: '/vmai-api/chat' }; + let url = epMap[ep]; + let opts = { method: 'GET', headers: { 'Accept': 'application/json' } }; + const authEps = ['portfolio', 'transactions', 'copy-trade']; + if (authEps.includes(ep)) { + const token = localStorage.getItem('vaultmind_token'); + if (token) opts.headers['Authorization'] = 'Bearer ' + token; + } + if (ep === 'chat') { + opts.method = 'POST'; + opts.headers['Content-Type'] = 'application/json'; + const token = localStorage.getItem('vaultmind_token'); + if (token) opts.headers['Authorization'] = 'Bearer ' + token; + opts.body = JSON.stringify({ + message: args.slice(1).join(' ') || 'What is the current market status?', + agent: 'market' + }); + } + const resp = await fetch(url, opts); + const data = await resp.json(); + this.terminal.print(JSON.stringify(data, null, 2), 'success'); + } catch (e) { + this.terminal.print('VaultMind query error: ' + e.message, 'error'); + } + } + async _walletLookupCmd(args) { + const addr = args[0]; + if (!addr) { this.terminal.print('Usage: wallet-lookup <address>', 'warning'); return; } + try { + const resp = await fetch('/wallet/' + encodeURIComponent(addr)); + const data = await resp.json(); + this.terminal.print(JSON.stringify(data, null, 2), 'success'); + } catch (e) { + this.terminal.print('Wallet lookup error: ' + e.message, 'error'); + } + } // ── Agent Loop Profile System ── _defaultLoopProfile(name, desc) { return { @@ -3411,6 +4142,20 @@ buildSystemPrompt() { else if (a.type === 'high_token_usage') suggestions.push('High token usage (' + (a.value/1000).toFixed(0) + 'K) — consider a minimal-tools profile for simpler tasks'); else if (a.type === 'tool_failures') { suggestions.push(a.count + ' tool failures (' + a.tools.join(', ') + ') — check if modules are loaded or paths correct'); + // Auto-fix: map failed tools to modules and suggest fix + if (!this._autoFixSuggestions) this._autoFixSuggestions = []; + if (!this._toolToModuleMap) this._buildToolToModuleMap(); + for (var fi = 0; fi < a.tools.length; fi++) { + var failedMod = this._toolToModuleMap[a.tools[fi]]; + if (failedMod && this._autoFixSuggestions.length < 3) { + this._autoFixSuggestions.push({ + module: failedMod, + tool: a.tools[fi], + reason: 'Repeated tool failure in agent loop', + suggestion: 'Consider running ship_module to fix ' + failedMod + }); + } + } // Auto-correct: inject negative constraint into profile append if (this._activeLoopProfile && this._activeLoopProfile.systemPrompt) { var constraint = '[Constraint] Avoid tools that failed: ' + a.tools.join(', ') + '. Verify module state before calling these tools.'; @@ -3476,4 +4221,141 @@ buildSystemPrompt() { if (profileAdjusted) msg += ' — profile auto-adjusted'; this.terminal.print(msg, 'info'); } + async _collectDashboardData() { + try { + const terminal = this.terminal; + const mm = terminal.moduleManager; + const om = terminal.ollamaManager; + const fs = terminal.fs; + const data = {}; + + // Version + data.version = window.NEWOS4_VERSION || 'v?'; + + // Modules + const mods = mm ? Object.keys(mm.modules) : []; + const registry = mm ? (mm.moduleRegistry || []) : []; + data.modules = { + loaded: mods.length, + total: registry.length || mods.length, + modifiedCount: 0 + }; + try { + const modsDir = fs ? fs.readDir('/_mods/') : null; + if (modsDir) data.modules.modifiedCount = modsDir.filter(function(e) { return e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js'); }).length; + } catch (e) {} + + // Agents + const orch = mm ? mm.modules['OrchestratorModule'] : null; + if (orch && orch._agents) { + var agentList = Object.values(orch._agents); + data.agents = { + total: agentList.length, + busy: agentList.filter(function(a) { return a.status === 'busy'; }).length, + idle: agentList.filter(function(a) { return a.status === 'idle'; }).length, + sleeping: agentList.filter(function(a) { return a.status === 'sleeping'; }).length, + failed: agentList.filter(function(a) { return a.status === 'failed' || a.status === 'restarting'; }).length, + list: agentList.map(function(a) { return { id: a.id, role: a.role, status: a.status, caps: (a.capabilities || []).length }; }) + }; + } else { + data.agents = { total: 0, busy: 0, idle: 0, sleeping: 0, failed: 0, list: [] }; + } + + // Memory + var heapStr = 'N/A'; + try { if (performance.memory) heapStr = (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB'; } catch (e) {} + var graphNodes = 0; + var mg = mm ? mm.modules['MemoryGraphModule'] : null; + if (mg && mg._nodes) graphNodes = Object.keys(mg._nodes).length; + data.memory = { heap: heapStr, graphNodes: graphNodes }; + + // VFS + try { + if (fs) { + var fileCount = 0, dirCount = 0; + var walk = function(path) { + var entries = fs.ls(path); + if (!entries) return; + for (var i = 0; i < entries.length; i++) { + if (entries[i].type === 'file') fileCount++; + else { dirCount++; walk(path === '/' ? '/' + entries[i].name : path + '/' + entries[i].name); } + } + }; + walk('/'); + data.vfs = { files: fileCount, dirs: dirCount, size: (JSON.stringify(fs.root || {}).length / 1024).toFixed(1) + 'KB' }; + } + } catch (e) { data.vfs = { files: 0, dirs: 0, size: '0KB' }; } + + // Ollama + data.ollama = { + connected: om ? om.connected : false, + model: om ? om.currentModel : null, + provider: om && om.currentProvider ? om.currentProvider.name : null + }; + + // Skills + try { + var skillIdx = fs ? fs.fsReadJSON('/skills/index.json') : null; + data.skills = { count: skillIdx ? Object.keys(skillIdx).length : 0 }; + } catch (e) { data.skills = { count: 0 }; } + + // Token usage + try { + if (om && typeof om.getUsageStats === 'function') { + var usage = om.getUsageStats({ since_hours: 24 }); + data.usage = usage; + } + } catch (e) { data.usage = { total: 0 }; } + + // Tool latency + var sm = mm ? mm.modules['ServerMonitorModule'] : null; + if (sm && sm._latencyLog) { + var toolStats = {}; + for (var i = 0; i < sm._latencyLog.length; i++) { + var entry = sm._latencyLog[i]; + if (!toolStats[entry.name]) toolStats[entry.name] = { calls: 0, totalMs: 0, errors: 0, maxMs: 0 }; + toolStats[entry.name].calls++; + toolStats[entry.name].totalMs += entry.duration; + toolStats[entry.name].errors += entry.error ? 1 : 0; + toolStats[entry.name].maxMs = Math.max(toolStats[entry.name].maxMs, entry.duration); + } + data.latency = Object.entries(toolStats) + .map(function(e) { return { name: e[0], avg: Math.round(e[1].totalMs / e[1].calls) + 'ms', max: e[1].maxMs + 'ms', calls: e[1].calls, errors: e[1].errors }; }) + .sort(function(a, b) { return parseInt(b.avg) - parseInt(a.avg); }) + .slice(0, 10); + } else { + data.latency = []; + } + + // System health + var totalErrors = 0; + try { + var ds = mm ? mm.modules['DebugSupervisorModule'] : null; + if (ds && ds._errorDB) totalErrors += Object.keys(ds._errorDB.errors).length; + if (sm && sm._latencyLog) totalErrors += sm._latencyLog.filter(function(e) { return e.error; }).length; + } catch (e) {} + var predictor = mm ? mm.modules['PredictorModule'] : null; + var predictorAcc = 'N/A'; + if (predictor && predictor._accuracy && predictor._accuracy.total > 0) { + predictorAcc = Math.round(predictor._accuracy.correct / predictor._accuracy.total * 100) + '%'; + } + data.health = { + modules: data.modules.loaded + '/' + data.modules.total, + vfsSize: data.vfs ? data.vfs.size : '?', + online: typeof navigator !== 'undefined' ? navigator.onLine : true, + errors: totalErrors, + predictorAccuracy: predictorAcc + }; + + data.timestamp = Date.now(); + + // Write to localStorage for the dashboard HTML page to read + try { localStorage.setItem('webx11_dashboard_data', JSON.stringify(data)); } catch (e) {} + + return data; + } catch (e) { + console.error('[Dashboard] Collection error:', e.message); + return { error: e.message, timestamp: Date.now() }; + } + } } |