diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-04 06:01:04 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-04 06:01:04 +0200 |
| commit | a5023e61be61f2f2287342039addab6bb28ad2fd (patch) | |
| tree | 9b1b3c166869cd6fee91bffe1f4134513005b8b1 /modules/core | |
| parent | 301f75ab03302a05b3c35950a3b0ab49f9f69ed6 (diff) | |
auto: 2026-06-04_06:00 changes
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/module-agent-registry.js | 128 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 208 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 6 | ||||
| -rw-r--r-- | modules/core/module-modmgr.js | 34 | ||||
| -rw-r--r-- | modules/core/module-neural-vfs.js | 271 | ||||
| -rw-r--r-- | modules/core/module-ollama.js | 177 | ||||
| -rw-r--r-- | modules/core/module-ollamamodels.js | 87 | ||||
| -rw-r--r-- | modules/core/module-orchestrator.js | 16 | ||||
| -rw-r--r-- | modules/core/module-servermon.js | 172 | ||||
| -rw-r--r-- | modules/core/terminal.js | 5 | ||||
| -rw-r--r-- | modules/core/virtual-fs.js | 52 |
11 files changed, 947 insertions, 209 deletions
diff --git a/modules/core/module-agent-registry.js b/modules/core/module-agent-registry.js index 367f8b1..2439ab8 100644 --- a/modules/core/module-agent-registry.js +++ b/modules/core/module-agent-registry.js @@ -1,58 +1,137 @@ -// module-agent-registry.js — VFS-backed agent directory -// Command: agent-list, agent-register, agent-unregister, agent-status +// module-agent-registry.js — VFS-backed agent directory with A2A contracts +// Commands: agent-list, agent-register, agent-unregister, agent-status, contract-register, contract-list class AgentRegistryModule { constructor() { this.commands = {}; this.terminal = null; + this._contracts = {}; } registerCommands() { this.commands['agent-list'] = () => this.listAgents(); this.commands['agent-status'] = (args) => this.showAgentStatus(args[0]); this.commands['agent-register'] = (args) => this.registerAgent(args); this.commands['agent-unregister'] = (args) => this.unregisterAgent(args[0]); + this.commands['contract-register'] = (args) => this.registerContract(args); + this.commands['contract-list'] = () => this.listContracts(); } init(terminal) { this.terminal = terminal; - if (!this.terminal.fs.readFile('/agents/')) { - this.terminal.fs.mkdir('agents'); - } + if (!this.terminal.fs.readDir('/agents/')) this.terminal.fs.mkdir('agents'); + if (!this.terminal.fs.readDir('/.contracts/')) this.terminal.fs.mkdir('.contracts'); + this._loadContracts(); this.terminal.print('📋 Agent Registry loaded', 'info'); } - listAgents() { - const agents = this.terminal.fs.ls('/agents/'); - if (!agents || agents.length === 0) { - return this.terminal.print('No agents registered', 'info'); + _loadContracts() { + try { + const data = this.terminal.fs.readFile('/.contracts/index.json'); + if (data) this._contracts = JSON.parse(data); + } catch (e) {} + // Try loading individual contract files + const dir = this.terminal.fs.readDir('/.contracts/'); + if (dir) { + dir.forEach(e => { + if (e.type === 'file' && e.name.endsWith('.contract.json')) { + try { + const c = JSON.parse(this.terminal.fs.readFile('/.contracts/' + e.name)); + this._contracts[c.role] = c; + } catch (e) { console.warn('Bad contract:', e.name, e.message); } + } + }); } - this.terminal.print(`📋 Registered Agents (${agents.length}):`, 'header'); - agents.forEach(a => { - if (a.type === 'dir' && a.name !== 'configured') { - const data = this.terminal.fs.readFile(`/agents/${a.name}/status.json`); - const s = data ? JSON.parse(data) : { status: 'unknown', capabilities: [] }; - this.terminal.print(` ${a.name} — ${s.status} (${(s.capabilities || []).length} capabilities)`, 'info'); - } - }); + } + _saveContracts() { + try { + this.terminal.fs.fsWriteJSON('/.contracts/index.json', this._contracts); + } catch (e) { console.error('Contract save error:', e); } } registerAgent(args) { const id = args[1] || `agent-${Date.now()}`; const capabilities = args.slice(2).length ? args.slice(2) : ['list_files', 'read_file']; const agentDir = `agents/${id}`; - if (!this.terminal.fs.readFile('/' + agentDir + '/')) this.terminal.fs.mkdir(agentDir); + if (!this.terminal.fs.readDir('/' + agentDir + '/')) this.terminal.fs.mkdir(agentDir); this.terminal.fs.writeFile(`${agentDir}/status.json`, JSON.stringify({ id, status: 'idle', capabilities, registered: new Date().toISOString(), - lastHeartbeat: new Date().toISOString() + lastHeartbeat: new Date().toISOString(), + contract: null })); this.terminal.print(`✅ Agent '${id}' registered with ${capabilities.length} capabilities`, 'success'); } - unregisterAgent(id) { - const ok = this.terminal.fs.deleteFile(`agents/${id}`); - if (ok) { - this.terminal.print(`✅ Agent '${id}' unregistered`, 'success'); + registerContract(args) { + const role = args[0]; + if (!role) return this.terminal.print('Usage: contract-register <role> [input_fields...] [--output output_fields...]', 'warning'); + + const inputIdx = args.indexOf('--output'); + const hasOutputFlag = inputIdx !== -1; + + let inputFields = [], outputFields = []; + if (hasOutputFlag) { + inputFields = args.slice(1, inputIdx).filter(f => !f.startsWith('--')); + outputFields = args.slice(inputIdx + 1).filter(f => !f.startsWith('--')); } else { - this.terminal.print(`❌ Agent '${id}' not found`, 'error'); + inputFields = args.slice(1).filter(f => !f.startsWith('--')); + outputFields = ['result']; // default output + } + + const contract = { + role, + capabilities: inputFields, + input_schema: inputFields, + output_schema: outputFields, + created: new Date().toISOString(), + updated: new Date().toISOString() + }; + this._contracts[role] = contract; + // Save as individual contract file + this.terminal.fs.fsWriteJSON(`/.contracts/${role}.contract.json`, contract); + this._saveContracts(); + this.terminal.print(`✅ Contract registered for role '${role}'`, 'success'); + this.terminal.print(` Input fields: ${inputFields.join(', ') || '(free-form)'}`, 'info'); + this.terminal.print(` Output fields: ${outputFields.join(', ') || '(free-form)'}`, 'info'); + } + listContracts() { + const roles = Object.keys(this._contracts); + if (roles.length === 0) return this.terminal.print('No contracts registered', 'info'); + this.terminal.print(`📜 A2A Contracts (${roles.length}):`, 'header'); + roles.forEach(r => { + const c = this._contracts[r]; + this.terminal.print(` ${r} — input:[${c.input_schema.join(',')}] → output:[${c.output_schema.join(',')}]`, 'info'); + }); + } + validateTask(role, task) { + const contract = this._contracts[role]; + if (!contract) return { valid: true, reason: 'no contract for role' }; // permissive fallback + if (contract.input_schema.length === 0) return { valid: true, reason: 'free-form input' }; + // Simple validation: check required input fields are present in task + const missing = contract.input_schema.filter(f => !task.toLowerCase().includes(f.toLowerCase())); + if (missing.length > 0) { + return { valid: false, reason: `missing required fields: ${missing.join(', ')}` }; } + return { valid: true, reason: 'matched' }; + } + getContract(role) { + return this._contracts[role] || null; + } + listAgents() { + const agents = this.terminal.fs.ls('/agents/'); + if (!agents || agents.length === 0) { + return this.terminal.print('No agents registered', 'info'); + } + this.terminal.print(`📋 Registered Agents (${agents.length}):`, 'header'); + agents.forEach(a => { + if (a.type === 'dir' && a.name !== 'configured') { + const data = this.terminal.fs.readFile(`/agents/${a.name}/status.json`); + const s = data ? JSON.parse(data) : { status: 'unknown', capabilities: [] }; + this.terminal.print(` ${a.name} — ${s.status} (${(s.capabilities || []).length} capabilities)${s.contract ? ` → contract:${s.contract}` : ''}`, 'info'); + } + }); + } + unregisterAgent(id) { + const ok = this.terminal.fs.deleteFile('agents/' + id); + if (ok) this.terminal.print(`✅ Agent '${id}' unregistered`, 'success'); + else this.terminal.print(`❌ Agent '${id}' not found`, 'error'); } showAgentStatus(id) { if (!id) return this.terminal.print('Usage: agent-status <agent-id>', 'error'); @@ -62,6 +141,7 @@ class AgentRegistryModule { this.terminal.print(`Agent: ${s.id}`, 'header'); this.terminal.print(` Status: ${s.status}`, 'info'); this.terminal.print(` Capabilities: ${(s.capabilities || []).join(', ')}`, 'info'); + this.terminal.print(` Contract: ${s.contract || 'none'}`, 'info'); this.terminal.print(` Registered: ${s.registered}`, 'info'); this.terminal.print(` Last Heartbeat: ${s.lastHeartbeat}`, 'info'); } diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index eb41af9..cb1482f 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -418,6 +418,56 @@ RULES: 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'] + } + } }]; } getHelpInfo() { @@ -695,6 +745,9 @@ RULES: 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 }; })); @@ -734,6 +787,9 @@ RULES: 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(); const dObs = this._summarizeObservation(toolResult, 150); const dErr = this._isToolError(toolResult); @@ -1338,6 +1394,76 @@ RULES: .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 'view_module': case 'edit_module': case 'test_module': @@ -1364,16 +1490,41 @@ RULES: const code = this.terminal.fs.readFile(`_mods/${modName}.js`); if (code) return code; const url = entry ? entry.url : null; - if (!url) return JSON.stringify({ error: `No source URL for ${modName}` }); + // 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.substring(0, 8000); + } + } catch (e) { /* fall through */ } + } + // Fallback: use modules-index.json to map class name to filename 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.substring(0, 8000); + 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.substring(0, 8000); + } + } } - return JSON.stringify({ error: `HTTP ${resp.status}` }); - } catch (e) { return JSON.stringify({ error: e.message }); } + } 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 || ''; @@ -1425,11 +1576,42 @@ RULES: const mm = this.terminal.moduleManager; const modsDir = this.terminal.fs.getCurrentDir(); const hasMods = modsDir.content['_mods'] ? !!Object.keys(modsDir.content['_mods'].content).length : false; - const modules = mm.moduleRegistry.map(m => ({ - name: m.name, url: m.url, enabled: m.enabled, - loaded: !!mm.modules[m.name], - modified: !!this.terminal.fs.readFile(`_mods/${m.name}.js`) - })); + // 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}` }); diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 2ffdb17..e7fc64a 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -18,7 +18,6 @@ 'module-my.js', 'module-searxng.js', 'module-servermon.js', - 'module-ollamamodels.js', 'module-config.js', 'module-simplegui.js', 'module-io.js', @@ -33,7 +32,8 @@ 'module-chatstudio.js', 'module-agent-registry.js', 'module-leases.js', - 'module-orchestrator.js' + 'module-orchestrator.js', + 'module-neural-vfs.js' ]; function getSelectedModules() { @@ -102,7 +102,7 @@ window.__moduleLoaderReload = reloadWithSelection; -const LATEST_VER = '2026-06-02-webdav2'; +const LATEST_VER = '2026-06-03-neuralvfs'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/module-modmgr.js b/modules/core/module-modmgr.js index 1104883..32bb2cc 100644 --- a/modules/core/module-modmgr.js +++ b/modules/core/module-modmgr.js @@ -5,6 +5,7 @@ return { 'modules': 'modules — Open the module manager GUI', 'listmods': 'listmods — List all loaded modules with status', + 'modules-list': 'modules-list [core|ext] — List all available modules from the module index (46 total)', 'loadmod': 'loadmod <url> — Load a module from a URL', 'removemod': 'removemod <name> — Remove a loaded module by name', 'startup-modules': 'startup-modules — Open the startup modules selector', @@ -36,5 +37,38 @@ this.terminal.print(`${status} ${m.name}${badge}`); }); }; + this.commands['modules-list'] = async (args) => { + try { + const filter = (args[0] || '').toLowerCase(); + const resp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || '')); + if (!resp.ok) return this.terminal.print('Failed to load module index', 'error'); + const idx = await resp.json(); + let mods = Object.values(idx.modules); + if (filter === 'core' || filter === 'ext') mods = mods.filter(m => m.type === filter); + if (filter === 'infra') mods = mods.filter(m => m.layer === 'infrastructure'); + const loaded = this.terminal.moduleManager.modules || {}; + const loadedCount = Object.keys(loaded).length; + const byLayer = {}; + mods.forEach(m => { + const l = m.layer || m.type || 'other'; + if (!byLayer[l]) byLayer[l] = []; + byLayer[l].push(m); + }); + const layerSummary = Object.entries(byLayer).map(([l, ms]) => `${ms.length} ${l}`).join(', '); + this.terminal.print(`📦 Module Index (${mods.length} modules: ${layerSummary} — ${loadedCount} loaded)`, 'header'); + for (const [layer, layerMods] of Object.entries(byLayer)) { + this.terminal.print(` ── ${layer.toUpperCase()} (${layerMods.length}) ──`, 'info'); + for (const m of layerMods) { + const isLoaded = loaded[m.class] ? '✓' : ' '; + const layerTag = m.layer === 'infrastructure' ? '[infra]' : m.type === 'core' ? '[core]' : '[ext]'; + const cmdHint = m.commands.length > 0 ? ` ${m.commands.slice(0, 4).join(', ')}${m.commands.length > 4 ? '...' : ''}` : ''; + const desc = m.description ? ` — ${m.description.substring(0, 60)}` : ''; + this.terminal.print(` ${isLoaded} ${layerTag} ${m.name}${cmdHint}${desc}`, ''); + } + } + } catch (e) { + this.terminal.print(`modules-list error: ${e.message}`, 'error'); + } + }; } } diff --git a/modules/core/module-neural-vfs.js b/modules/core/module-neural-vfs.js new file mode 100644 index 0000000..e9ffafb --- /dev/null +++ b/modules/core/module-neural-vfs.js @@ -0,0 +1,271 @@ +class NeuralVFSModule { + constructor() { + this.commands = {}; + this.terminal = null; + this.index = {}; + this.indexPath = '/.semantic_index/map.json'; + } + + getHelpInfo() { + return { + 'nvfs-index': 'nvfs-index — Scan VFS and build semantic concept index', + 'nvfs-status': 'nvfs-status — Show semantic index stats and last scan time', + 'nvfs-resolve': 'nvfs-resolve <concept> — Find files related to a concept', + 'nvfs-tree': 'nvfs-tree [concept] — Show concept-to-file mapping as a tree' + }; + } + + registerCommands() { + this.commands['nvfs-index'] = (args) => this.buildIndex(); + this.commands['nvfs-status'] = (args) => this.showStatus(); + this.commands['nvfs-resolve'] = (args) => this.resolve(args); + this.commands['nvfs-tree'] = (args) => this.tree(args); + } + + // ── Core Index Builder ── + + buildIndex() { + const fs = this.terminal.fs; + if (!fs) return this.terminal.print('VFS not available', 'error'); + + this.terminal.print('🔍 Scanning VFS for semantic concepts...', 'info'); + + const start = Date.now(); + const newIndex = {}; + let fileCount = 0; + let conceptCount = 0; + + // Walk VFS recursively — skip system dirs + this._walk('/', fs, newIndex, (path, node) => { + if (node.type !== 'file') return; + if (this._isSystemFile(path)) return; + + fileCount++; + const concepts = this._extractConcepts(path, node.content || ''); + for (const concept of concepts) { + if (!newIndex[concept]) { + newIndex[concept] = []; + conceptCount++; + } + // Avoid dupes + if (!newIndex[concept].find(e => e.path === path)) { + newIndex[concept].push({ path, summary: this._summarize(node.content) }); + } + } + }); + + // Also index skills + this._indexSkills(fs, newIndex); + + this.index = newIndex; + this.lastScan = new Date().toISOString(); + this.fileCount = fileCount; + this.conceptCount = conceptCount; + + // Save to VFS + this._saveIndex(fs); + // Clear VFS concept cache so concept:// URIs reflect the new index + if (fs._clearConceptCache) fs._clearConceptCache(); + + const elapsed = ((Date.now() - start) / 1000).toFixed(1); + this.terminal.print(`✅ Indexed ${fileCount} files → ${conceptCount} concepts in ${elapsed}s`, 'success'); + this.terminal.print(` Index saved to ${this.indexPath}`, 'info'); + } + + // ── Resolve ── + + resolve(args) { + if (!args[0]) return this.terminal.print('Usage: nvfs-resolve <concept>', 'warning'); + + const concept = args.join(' ').toLowerCase(); + const matches = this.index[concept] || []; + + // Fuzzy fallback — find partial matches + const fuzzy = Object.keys(this.index).filter(k => + k.includes(concept) || concept.includes(k) + ).filter(k => k !== concept); + + if (matches.length === 0 && fuzzy.length === 0) { + return this.terminal.print(`No files found for concept "${concept}"`, 'warning'); + } + + if (matches.length > 0) { + this.terminal.print(`📂 "${concept}" → ${matches.length} file(s):`, 'info'); + for (const m of matches.slice(0, 20)) { + this.terminal.print(` ${m.path}`, 'success'); + if (m.summary) this.terminal.print(` ${m.summary}`, '') + } + if (matches.length > 20) { + this.terminal.print(` ... and ${matches.length - 20} more`, 'info'); + } + } + + if (fuzzy.length > 0) { + this.terminal.print(`\n🔀 Similar concepts: ${fuzzy.slice(0, 10).join(', ')}`, 'info'); + } + } + + // ── Status ── + + showStatus() { + const count = Object.keys(this.index).length; + if (count === 0) { + // Try loading from VFS + const fs = this.terminal.fs; + const saved = fs.fsReadJSON(this.indexPath); + if (saved) { + this.index = saved; + this.lastScan = '(loaded from disk)'; + this.terminal.print(`📖 Loaded index from ${this.indexPath}`, 'info'); + } else { + return this.terminal.print('No semantic index found. Run nvfs-index first.', 'warning'); + } + } + + const totalFiles = new Set(); + for (const entries of Object.values(this.index)) { + for (const e of entries) totalFiles.add(e.path); + } + + this.terminal.print(`🧠 Neural VFS Index`, 'info'); + this.terminal.print(` Concepts: ${Object.keys(this.index).length}`, ''); + this.terminal.print(` Files indexed: ${totalFiles.size}`, ''); + this.terminal.print(` Last scan: ${this.lastScan || 'never'}`, ''); + } + + // ── Tree View ── + + tree(args) { + const concepts = Object.keys(this.index); + if (concepts.length === 0) { + return this.terminal.print('No index. Run nvfs-index first.', 'warning'); + } + + const filter = args[0] ? args[0].toLowerCase() : null; + const filtered = filter ? concepts.filter(c => c.includes(filter)) : concepts; + + this.terminal.print(`🌳 Concept Tree${filter ? ` (filter: "${filter}")` : ''}:`, 'info'); + for (const concept of filtered.slice(0, 15)) { + this.terminal.print(` 📁 ${concept}`, 'success'); + for (const entry of this.index[concept].slice(0, 5)) { + this.terminal.print(` └─ ${entry.path}`, ''); + } + if (this.index[concept].length > 5) { + this.terminal.print(` └─ ... (+${this.index[concept].length - 5} more)`, ''); + } + } + if (filtered.length > 15) { + this.terminal.print(` ... and ${filtered.length - 15} more concepts`, 'info'); + } + } + + // ── Private Helpers ── + + _walk(path, fs, index, cb) { + const entries = fs.readDir(path); + if (!entries) return; + for (const entry of entries) { + const fullPath = path === '/' ? '/' + entry.name : path + '/' + entry.name; + if (entry.type === 'file') { + cb(fullPath, { type: 'file', content: fs.readFile(fullPath) || '' }); + } else if (entry.type === 'dir') { + this._walk(fullPath, fs, index, cb); + } + } + } + + _isSystemFile(path) { + const sysPrefixes = ['/.trash', '/.semantic_index', '/_snapshots', '/_sessions']; + return sysPrefixes.some(p => path.startsWith(p) || path.includes('/node_modules')); + } + + _extractConcepts(path, content) { + const concepts = new Set(); + const name = path.split('/').pop() || ''; + const nameLower = name.toLowerCase(); + + // Extract from filename (split on camelCase, kebab, dots, underscores) + const words = nameLower + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/[-_.]/g, ' ') + .replace(/\.(md|js|html|json|css|txt|yaml|yml|toml|env|sh)$/i, '') + .split(/\s+/) + .filter(w => w.length > 2 && !/^\d+$/.test(w)); + + for (const word of words) { + concepts.add(word); + } + + // Extract file extension as concept + const ext = path.split('.').pop(); + if (ext && ['md', 'js', 'html', 'json', 'css'].includes(ext)) { + concepts.add(ext + '-file'); + } + + // Extract path-level concepts + const parts = path.split('/').filter(p => p.length > 0); + for (const part of parts) { + if (['modules', 'skills', 'core', 'ext', 'backups', 'chat', 'downloads'].includes(part)) { + concepts.add(part); + } + } + + // Extract #tags from content (markdown frontmatter or inline) + if (content) { + const tagMatches = content.match(/tags:\s*\[([^\]]+)\]/i) || content.match(/#[a-zA-Z]\w+/g); + if (tagMatches) { + const tags = tagMatches[0].replace(/tags:\s*\[/i, '').replace(/\]/, '').split(',').map(t => t.trim().replace(/^#/, '').replace(/"/g, '')); + for (const tag of tags) { + if (tag.length > 2) concepts.add(tag.toLowerCase()); + } + } + + // Extract first H1/H2 as concept + const headerMatch = content.match(/^#\s+(.+)/m) || content.match(/^##\s+(.+)/m); + if (headerMatch) { + const header = headerMatch[1].toLowerCase().replace(/[^a-z0-9\s]/g, '').trim(); + if (header.length > 3 && header.length < 40) { + concepts.add(header); + } + } + } + + return [...concepts]; + } + + _summarize(content) { + if (!content || content.length < 10) return ''; + const cleaned = content.replace(/---[\s\S]*?---/, '').trim(); + const firstLine = cleaned.split('\n').find(l => l.trim().length > 20); + if (!firstLine) return ''; + return firstLine.trim().slice(0, 100) + (firstLine.length > 100 ? '...' : ''); + } + + _indexSkills(fs, index) { + const skills = fs.readDir('/skills'); + if (!skills) return; + for (const skill of skills) { + if (skill.type !== 'file' || !skill.name.endsWith('.md')) continue; + // Already indexed by _walk — but we add a "skill" tag + const base = skill.name.replace('.md', ''); + if (!index['skill']) index['skill'] = []; + index['skill'].push({ path: '/skills/' + skill.name, summary: 'Skill: ' + base }); + } + } + + _saveIndex(fs) { + try { + if (!fs.readDir('/.semantic_index')) { + fs.mkdirRecursive('/.semantic_index'); + } + fs.fsWriteJSON(this.indexPath, this.index); + } catch (e) { + console.error('NeuralVFS save error:', e); + } + } +} + +// Required for module system +if (typeof window !== 'undefined') { + window.NeuralVFSModule = NeuralVFSModule; +}
\ No newline at end of file diff --git a/modules/core/module-ollama.js b/modules/core/module-ollama.js index 3eabe19..ba8ad57 100644 --- a/modules/core/module-ollama.js +++ b/modules/core/module-ollama.js @@ -1,52 +1,131 @@ -// Extracted from newOS4 index.html — OllamaModule - class OllamaModule { - constructor() { this.commands = {}; this.terminal = null; } - getHelpInfo() { - return { - 'ollama': 'ollama — Open the Ollama model manager UI', - 'ollamaconnect': 'ollamaconnect — Connect to the Ollama server', - 'ollamadisconnect': 'ollamadisconnect — Disconnect from the Ollama server', - 'ollamamodels': 'ollamamodels — List all available models from Ollama', - 'ollamasetmodel': 'ollamasetmodel <name> — Set the active Ollama model', - 'ollamasetproxy': 'ollamasetproxy [endpoint] — View or change the Ollama endpoint URL', - }; +// Extracted from newOS4 index.html — OllamaModule (merged with OllamaModelsModule) +// Commands: ollama, ollamaconnect, ollamadisconnect, ollamamodels, ollamasetmodel, ollamasetproxy, models, model-info, model-pull, model-delete, model-run, ai, ask +class OllamaModule { + constructor() { this.commands = {}; this.terminal = null; } + getHelpInfo() { + return { + 'ollama': 'ollama — Open the Ollama model manager UI', + 'ollamaconnect': 'ollamaconnect — Connect to the Ollama server', + 'ollamadisconnect': 'ollamadisconnect — Disconnect from the Ollama server', + 'ollamamodels': 'ollamamodels — List all available models from Ollama', + 'models': 'models — List all available Ollama models (table format)', + 'model-info': 'model-info <model-name> — Show details about a specific model', + 'model-pull': 'model-pull <model-name> — Pull a model from Ollama registry', + 'model-delete': 'model-delete <model-name> — Delete a model from local storage', + 'model-run': 'model-run <model-name> [prompt] — Run inference with a model', + 'ollamasetmodel': 'ollamasetmodel <name> — Set the active Ollama model', + 'ollamasetproxy': 'ollamasetproxy [endpoint] — View or change the Ollama endpoint URL', + }; + } + registerCommands() { + this.commands['ollama'] = () => { this.terminal.openOllamaManager(); }; + this.commands['ollamaconnect'] = async () => { + await this.terminal.ollamaManager.connect(); + this.terminal.updateOllamaStatus(); + }; + this.commands['ollamadisconnect'] = () => { + this.terminal.ollamaManager.connected = false; + this.terminal.ollamaManager.models = []; + this.terminal.print('Ollama disconnected', 'info'); + this.terminal.updateOllamaStatus(); + }; + this.commands['ollamamodels'] = () => { + const models = this.terminal.ollamaManager.getModels(); + if(models.length === 0) return this.terminal.print("No models found. Connect first."); + models.forEach(m => this.terminal.print(`${m.name} (${(m.size/1e9).toFixed(1)}GB)`)); + }; + this.commands['ollamasetmodel'] = (args) => { + if(!args[0]) return this.terminal.print("Usage: ollamasetmodel <name>", 'warning'); + this.terminal.ollamaManager.setModel(args[0]); + }; + this.commands['ollamasetproxy'] = (args) => { + if(!args[0]) { + const ep = this.terminal.ollamaManager.endpoint; + return this.terminal.print(`Current Ollama endpoint: ${ep}`, 'info'); } - registerCommands() { - this.commands['ollama'] = () => { this.terminal.openOllamaManager(); }; - this.commands['ollamaconnect'] = async () => { - await this.terminal.ollamaManager.connect(); - this.terminal.updateOllamaStatus(); - }; - this.commands['ollamadisconnect'] = () => { - this.terminal.ollamaManager.connected = false; - this.terminal.ollamaManager.models = []; - this.terminal.print('Ollama disconnected', 'info'); - this.terminal.updateOllamaStatus(); - }; - this.commands['ollamamodels'] = () => { - const models = this.terminal.ollamaManager.getModels(); - if(models.length === 0) return this.terminal.print("No models found. Connect first."); - models.forEach(m => this.terminal.print(`${m.name} (${(m.size/1e9).toFixed(1)}GB)`)); - }; - this.commands['ollamasetmodel'] = (args) => { - if(!args[0]) return this.terminal.print("Usage: ollamasetmodel <name>", 'warning'); - this.terminal.ollamaManager.setModel(args[0]); - }; - this.commands['ollamasetproxy'] = (args) => { - if(!args[0]) { - const ep = this.terminal.ollamaManager.endpoint; - return this.terminal.print(`Current Ollama endpoint: ${ep}`, 'info'); - } - this.terminal.ollamaManager.setEndpoint(args[0]); - this.terminal.print(`Ollama endpoint set to: ${args[0]}`, 'success'); - }; - this.commands['ai'] = async (args) => { - if(!args[0]) return this.terminal.print("Usage: ai <prompt>", 'warning'); - await this.terminal.ollamaManager.chat(args.join(' ')); - }; - this.commands['ask'] = async (args) => { - if(!args[0]) return this.terminal.print("Usage: ask <prompt>", 'warning'); - await this.terminal.ollamaManager.chat(args.join(' ')); - }; + this.terminal.ollamaManager.setEndpoint(args[0]); + this.terminal.print(`Ollama endpoint set to: ${args[0]}`, 'success'); + }; + this.commands['ai'] = async (args) => { + if(!args[0]) return this.terminal.print("Usage: ai <prompt>", 'warning'); + await this.terminal.ollamaManager.chat(args.join(' ')); + }; + this.commands['ask'] = async (args) => { + if(!args[0]) return this.terminal.print("Usage: ask <prompt>", 'warning'); + await this.terminal.ollamaManager.chat(args.join(' ')); + }; + // Merged from OllamaModelsModule + this.commands['models'] = () => this.listModels(); + this.commands['model-info'] = (args) => this.modelInfo(args); + this.commands['model-pull'] = (args) => this.pullModel(args); + this.commands['model-delete'] = (args) => this.deleteModel(args); + this.commands['model-run'] = (args) => this.runModel(args); + } + async listModels() { + const endpoint = localStorage.getItem('webx11_ollama_endpoint') || 'http://localhost:11434'; + try { + const response = await fetch(`${endpoint}/api/tags`); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); + const models = data.models || []; + if (models.length === 0) { + this.terminal.print("No models found. Run 'ollama pull <model>' first.", "warning"); return; } + this.terminal.print("=== Available Ollama Models ===", "info"); + this.terminal.printHTML(`<table style="width: 100%; border-collapse: collapse; margin: 10px 0;"> + <tr style="border-bottom: 1px solid #005500;"> + <th style="text-align: left; padding: 5px;">Name</th> + <th style="text-align: left; padding: 5px;">Size</th> + <th style="text-align: left; padding: 5px;">Family</th> + </tr>`); + models.forEach(m => { + const sizeGB = (m.size / 1e9).toFixed(2); + this.terminal.printHTML(`<tr style="border-bottom: 1px solid #111;"> + <td style="padding: 5px; color: #00ff00;">${m.name}</td> + <td style="padding: 5px; color: #888888;">${sizeGB} GB</td> + <td style="padding: 5px; color: #00aaff;">${m.details?.family || 'N/A'}</td> + </tr>`); + }); + this.terminal.printHTML(`</table>`); + } catch (e) { + this.terminal.print(`Failed to fetch models: ${e.message}`, "error"); } + } + async modelInfo(args) { + if (!args[0]) { + this.terminal.print("Usage: model-info <model-name>", "warning"); return; + } + const endpoint = localStorage.getItem('webx11_ollama_endpoint') || 'http://localhost:11434'; + try { + const response = await fetch(`${endpoint}/api/show`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: args[0] }) + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); + this.terminal.print(`=== ${args[0]} ===`, "info"); + this.terminal.print(`License: ${data.license || 'N/A'}`); + this.terminal.print(`Parameters: ${data.details?.parameter_size || 'N/A'}`); + this.terminal.print(`Family: ${data.details?.family || 'N/A'}`); + } catch (e) { + this.terminal.print(`Failed to get model info: ${e.message}`, "error"); + } + } + pullModel(args) { + this.terminal.print("⚠️ Model pulling must be done via Ollama CLI", "warning"); + this.terminal.print(`Run: ollama pull ${args[0] || '<model-name>'}`, "info"); + } + deleteModel(args) { + this.terminal.print("⚠️ Model deletion must be done via Ollama CLI", "warning"); + this.terminal.print(`Run: ollama rm ${args[0] || '<model-name>'}`, "info"); + } + async runModel(args) { + if (!args[0]) { + this.terminal.print("Usage: model-run <model-name> [prompt]", "warning"); return; + } + const model = args[0]; + const prompt = args.slice(1).join(' ') || 'Hello!'; + this.terminal.print(`Running ${model}...`, "info"); + await this.terminal.ollamaManager.chat(prompt, model); + } +}
\ No newline at end of file diff --git a/modules/core/module-ollamamodels.js b/modules/core/module-ollamamodels.js deleted file mode 100644 index 9a7763a..0000000 --- a/modules/core/module-ollamamodels.js +++ /dev/null @@ -1,87 +0,0 @@ -// Extracted from newOS4 index.html — OllamaModelsModule - class OllamaModelsModule { - constructor() { this.commands = {}; this.terminal = null; } - getHelpInfo() { - return { - 'models': 'models — List all available Ollama models', - 'model-info': 'model-info <model-name> — Show details about a specific model', - 'model-pull': 'model-pull <model-name> — Pull a model from Ollama registry', - 'model-delete': 'model-delete <model-name> — Delete a model from local storage', - 'model-run': 'model-run <model-name> [prompt] — Run inference with a model' - }; - } - registerCommands() { - this.commands['models'] = () => this.listModels(); - this.commands['model-info'] = (args) => this.modelInfo(args); - this.commands['model-pull'] = (args) => this.pullModel(args); - this.commands['model-delete'] = (args) => this.deleteModel(args); - this.commands['model-run'] = (args) => this.runModel(args); - } - async listModels() { - const endpoint = localStorage.getItem('webx11_ollama_endpoint') || 'http://localhost:11434'; - try { - const response = await fetch(`${endpoint}/api/tags`); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - const data = await response.json(); - const models = data.models || []; - if (models.length === 0) { - this.terminal.print("No models found. Run 'ollama pull <model>' first.", "warning"); return; - } - this.terminal.print("=== Available Ollama Models ===", "info"); - this.terminal.printHTML(`<table style="width: 100%; border-collapse: collapse; margin: 10px 0;"> - <tr style="border-bottom: 1px solid #005500;"> - <th style="text-align: left; padding: 5px;">Name</th> - <th style="text-align: left; padding: 5px;">Size</th> - <th style="text-align: left; padding: 5px;">Family</th> - </tr>`); - models.forEach(m => { - const sizeGB = (m.size / 1e9).toFixed(2); - this.terminal.printHTML(`<tr style="border-bottom: 1px solid #111;"> - <td style="padding: 5px; color: #00ff00;">${m.name}</td> - <td style="padding: 5px; color: #888888;">${sizeGB} GB</td> - <td style="padding: 5px; color: #00aaff;">${m.details?.family || 'N/A'}</td> - </tr>`); - }); - this.terminal.printHTML(`</table>`); - } catch (e) { - this.terminal.print(`Failed to fetch models: ${e.message}`, "error"); - } - } - async modelInfo(args) { - if (!args[0]) { - this.terminal.print("Usage: model-info <model-name>", "warning"); return; - } - const endpoint = localStorage.getItem('webx11_ollama_endpoint') || 'http://localhost:11434'; - try { - const response = await fetch(`${endpoint}/api/show`, { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: args[0] }) - }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - const data = await response.json(); - this.terminal.print(`=== ${args[0]} ===`, "info"); - this.terminal.print(`License: ${data.license || 'N/A'}`); - this.terminal.print(`Parameters: ${data.details?.parameter_size || 'N/A'}`); - this.terminal.print(`Family: ${data.details?.family || 'N/A'}`); - } catch (e) { - this.terminal.print(`Failed to get model info: ${e.message}`, "error"); - } - } - pullModel(args) { - this.terminal.print("⚠️ Model pulling must be done via Ollama CLI", "warning"); - this.terminal.print(`Run: ollama pull ${args[0] || '<model-name>'}`, "info"); - } - deleteModel(args) { - this.terminal.print("⚠️ Model deletion must be done via Ollama CLI", "warning"); - this.terminal.print(`Run: ollama rm ${args[0] || '<model-name>'}`, "info"); - } - async runModel(args) { - if (!args[0]) { - this.terminal.print("Usage: model-run <model-name> [prompt]", "warning"); return; - } - const model = args[0]; - const prompt = args.slice(1).join(' ') || 'Hello!'; - this.terminal.print(`Running ${model}...`, "info"); - await this.terminal.ollamaManager.chat(prompt, model); - } - } diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js index 4fab599..de88424 100644 --- a/modules/core/module-orchestrator.js +++ b/modules/core/module-orchestrator.js @@ -75,6 +75,22 @@ class OrchestratorModule { agent = Object.values(this._agents).find(a => a.status === 'idle'); } if (!agent) return this.terminal.print('❌ No available agents', 'error'); + + // A2A Contract handshake — validate task against agent's contract + const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; + let contractValid = true; + if (registry) { + const result = registry.validateTask(agent.role, task); + if (!result.valid) { + this.terminal.print(`⚠️ A2A validation: ${result.reason}`, 'warning'); + this.terminal.print(` Contract for '${agent.role}' requires: ${(registry.getContract(agent.role) || {}).input_schema || 'free-form'}`, 'warning'); + // Offer to proceed anyway (permissive) — user can create a proper contract + contractValid = false; + } else if (result.reason === 'matched') { + this.terminal.print(`✅ A2A contract matched for role '${agent.role}'`, 'success'); + } + } + // Create a lease const leaseModule = this.terminal.moduleManager.modules['LeaseModule']; const lease = leaseModule ? leaseModule.createLease(['lease-create', agent.id, '60', task]) : null; diff --git a/modules/core/module-servermon.js b/modules/core/module-servermon.js index 23407db..293e9af 100644 --- a/modules/core/module-servermon.js +++ b/modules/core/module-servermon.js @@ -1,35 +1,143 @@ -// Extracted from newOS4 index.html — ServerMonitorModule - class ServerMonitorModule { - constructor() { - this.commands = {}; this.terminal = null; - this.servers = this.loadServers(); this.monitorInterval = null; - } - getHelpInfo() { - return { - 'server': 'Add a server to monitor: server <name> <url>', - 'servers': 'List all monitored servers', - 'server-check': 'Check a server status: server-check <name-or-index>', - 'server-remove': 'Remove a server: server-remove <name-or-index>', - 'monitor': 'Toggle auto-monitoring (checks every 60s)' - }; - } - loadServers() { - try { - const saved = localStorage.getItem('webx11_servers'); - return saved ? JSON.parse(saved) : []; - } catch (e) { return []; } - } - saveServers() { - localStorage.setItem('webx11_servers', JSON.stringify(this.servers)); - } - registerCommands() { - this.commands['server'] = (args) => this.addServer(args); - this.commands['servers'] = () => this.listServers(); - this.commands['server-check'] = (args) => this.checkServer(args); - this.commands['server-remove'] = (args) => this.removeServer(args); - this.commands['monitor'] = (args) => this.toggleMonitor(args); - } - addServer(args) { +// module-servermon.js — Server monitoring + self-optimization telemetry +// Commands: server, servers, server-check, server-remove, monitor, system-health +class ServerMonitorModule { + constructor() { + this.commands = {}; this.terminal = null; + this.servers = this.loadServers(); this.monitorInterval = null; + this._latencyLog = []; + this._maxLatencyEntries = 200; + } + getHelpInfo() { + return { + 'server': 'server <name> <url> — Add a server to monitor', + 'servers': 'servers — List all monitored servers', + 'server-check': 'server-check <name-or-index> — Check a server status', + 'server-remove': 'server-remove <name-or-index> — Remove a server', + 'monitor': 'monitor — Toggle auto-monitoring (checks every 60s)', + 'system-health': 'system-health — Show tool latency, module load times, VFS size, and error rates' + }; + } + registerCommands() { + this.commands['server'] = (args) => this.addServer(args); + this.commands['servers'] = () => this.listServers(); + this.commands['server-check'] = (args) => this.checkServer(args); + this.commands['server-remove'] = (args) => this.removeServer(args); + this.commands['monitor'] = (args) => this.toggleMonitor(args); + this.commands['system-health'] = () => this.showSystemHealth(); + } + logToolCall(name, durationMs, error) { + this._latencyLog.push({ + name, + duration: durationMs, + error: !!error, + time: Date.now() + }); + if (this._latencyLog.length > this._maxLatencyEntries) { + this._latencyLog = this._latencyLog.slice(-this._maxLatencyEntries); + } + // Persist every 20 calls + if (this._latencyLog.length % 20 === 0) { + this._persistLatency(); + } + } + _persistLatency() { + try { + this.terminal.fs.fsWriteJSON('/system/metrics/tool-latency.json', this._latencyLog.slice(-100)); + } catch (e) {} + } + _loadLatency() { + try { + const data = this.terminal.fs.fsReadJSON('/system/metrics/tool-latency.json'); + if (data) this._latencyLog = data; + } catch (e) {} + } + showSystemHealth() { + this._loadLatency(); + const fs = this.terminal.fs; + + // Tool latency stats + const toolStats = {}; + for (const entry of this._latencyLog) { + 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); + } + + this.terminal.print('📊 System Health', 'header'); + + // Tool latency table (sorted by avg desc) + this.terminal.print('── Tool Latency ──', 'info'); + const sorted = Object.entries(toolStats) + .map(([name, s]) => ({ name, avg: (s.totalMs / s.calls).toFixed(0), ...s })) + .sort((a, b) => b.avg - a.avg); + for (const t of sorted.slice(0, 10)) { + const avgMs = t.avg.padStart(5); + const maxMs = String(t.maxMs).padStart(5); + const err = t.errors > 0 ? ` ⚠️${t.errors}` : ''; + this.terminal.print(` ${avgMs}ms avg / ${maxMs}ms max — ${t.name} (${t.calls} calls)${err}`, t.errors > 0 ? 'warning' : ''); + } + if (sorted.length > 10) this.terminal.print(` ... and ${sorted.length - 10} more tools`, 'info'); + + // VFS stats + this.terminal.print('── VFS Stats ──', 'info'); + try { + const totalSize = this._vfsSize(fs.root); + const fileCount = this._vfsCount(fs.root, 'file'); + const dirCount = this._vfsCount(fs.root, 'dir'); + this.terminal.print(` ${fileCount} files, ${dirCount} dirs, ${(totalSize / 1024).toFixed(0)} KB total`, ''); + } catch (e) { this.terminal.print(' VFS stats unavailable', 'warning'); } + + // Module stats + this.terminal.print('── Module Stats ──', 'info'); + const mm = this.terminal.moduleManager; + const loaded = Object.keys(mm.modules).length; + const reg = (mm.moduleRegistry || []).length; + this.terminal.print(` ${loaded} modules loaded, ${reg} in registry`, ''); + + // Memory + this.terminal.print('── Memory ──', 'info'); + if (performance.memory) { + const heap = (performance.memory.usedJSHeapSize / 1048576).toFixed(1); + const total = (performance.memory.jsHeapSizeLimit / 1048576).toFixed(0); + this.terminal.print(` ${heap} MB used / ${total} MB limit`, heap > 100 ? 'warning' : ''); + } else { + this.terminal.print(' Memory info not available (non-Chrome browser)', ''); + } + + // Total tool call count + const totalCalls = this._latencyLog.length; + const totalErrors = this._latencyLog.filter(e => e.error).length; + this.terminal.print(` ${totalCalls} tool calls logged, ${totalErrors} errors (${totalCalls > 0 ? (totalErrors / totalCalls * 100).toFixed(1) : 0}% error rate)`, totalErrors > 0 ? 'warning' : ''); + } + _vfsSize(node) { + if (!node) return 0; + if (node.type === 'file') return (node.content || '').length; + let s = 0; + for (const k in (node.content || {})) s += this._vfsSize(node.content[k]); + return s; + } + _vfsCount(node, type) { + if (!node) return 0; + let c = 0; + for (const k in (node.content || {})) { + const child = node.content[k]; + if (child.type === type) c++; + c += this._vfsCount(child, type); + } + return c; + } + loadServers() { + try { + const saved = localStorage.getItem('webx11_servers'); + return saved ? JSON.parse(saved) : []; + } catch (e) { return []; } + } + saveServers() { + localStorage.setItem('webx11_servers', JSON.stringify(this.servers)); + } + addServer(args) { if (args.length < 2) { this.terminal.print("Usage: server <name> <url>", "warning"); return; } diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 4111f5f..e3f222c 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -14,7 +14,7 @@ this._registerBuiltIn(); } _registerBuiltIn() { - const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','startup-modules','smart-prompt','cmd-allow','cmd-deny']; + const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor']; internals.forEach(c => this.registerCommand(c, { category: 'builtin' })); } setLevel(n) { @@ -1027,6 +1027,9 @@ this._safeRegister('AgentRegistryModule', 'agent registry'); this._safeRegister('LeaseModule', 'lease system'); this._safeRegister('OrchestratorModule', 'orchestrator'); + this._safeRegister('NeuralVFSModule', 'neural VFS'); + this._safeRegister('ServerMonitorModule', 'server monitor'); + this._safeRegister('SimpleGUIModule', 'simple GUI builder'); } _safeRegister(className, label) { let cls; diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js index d2d3103..3ab25b1 100644 --- a/modules/core/virtual-fs.js +++ b/modules/core/virtual-fs.js @@ -3,6 +3,7 @@ class VirtualFileSystem { this.root = { type: 'dir', content: {} }; this.currentPath = []; this._ready = this.loadFromStorage(); + this._conceptCache = null; } _u8ToBase64(u8) { let s = ''; @@ -82,6 +83,27 @@ class VirtualFileSystem { } catch (e) { console.error('FS save error:', e); } }, 2000); } + getCurrentPath() { + return '/' + this.currentPath.join('/'); + } + setCurrentPath(path) { + const segs = this._toAbsoluteSegments(path); + if (segs === null) return false; + this.currentPath = segs; + return true; + } + _loadConceptIndex() { + if (this._conceptCache) return this._conceptCache; + try { + const raw = this.readFile('/.semantic_index/map.json'); + if (raw) { + this._conceptCache = JSON.parse(raw); + return this._conceptCache; + } + } catch (e) {} + return null; + } + _clearConceptCache() { this._conceptCache = null; } getCurrentDir() { let dir = this.root; for (let part of this.currentPath) { @@ -130,6 +152,18 @@ class VirtualFileSystem { return dir; } ls(path) { + // concept:// listing + if (path && path.startsWith('concept://')) { + const concept = path.replace('concept://', '').replace(/\/.*$/, '').toLowerCase(); + const index = this._loadConceptIndex(); + if (!index) return []; + let matches = index[concept] || []; + if (matches.length === 0) { + const fuzzy = Object.keys(index).filter(k => k.includes(concept) || concept.includes(k)); + if (fuzzy.length > 0) matches = index[fuzzy[0]]; + } + return (matches || []).map(m => ({ name: m.path.split('/').pop(), type: 'file', path: m.path, size: 0, concept: true })); + } let dir; if (path) { const resolved = this._resolvePath(path); @@ -161,6 +195,24 @@ class VirtualFileSystem { return true; } readFile(path) { + // concept:// URI resolution + if (path && path.startsWith('concept://')) { + const concept = path.replace('concept://', '').replace(/\/.*$/, '').toLowerCase(); + try { + const index = this._loadConceptIndex(); + if (!index) return 'No semantic index found. Run nvfs-index first.'; + let matches = index[concept]; + if (!matches) { + const fuzzy = Object.keys(index).filter(k => k.includes(concept) || concept.includes(k)); + if (fuzzy.length > 0) matches = index[fuzzy[0]]; + } + if (!matches || matches.length === 0) return `No files found for concept "${concept}"`; + const first = matches[0]; + const content = this.readFile(first.path); + if (content) return `--- ${first.path} (concept://${concept}) ---\n${content}`; + return `Concept "${concept}" → ${matches.length} file(s):\n${matches.map(m => ` ${m.path} — ${m.summary || ''}`).join('\n')}`; + } catch (e) { return `concept:// error: ${e.message}`; } + } const resolved = this._resolvePath(path); if (!resolved || !resolved.name) return null; const node = resolved.dir.content[resolved.name]; |