diff options
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/module-agent-registry.js | 79 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 74 | ||||
| -rw-r--r-- | modules/core/module-chatstudio.js | 32 | ||||
| -rw-r--r-- | modules/core/module-fs.js | 34 | ||||
| -rw-r--r-- | modules/core/module-leases.js | 102 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 7 | ||||
| -rw-r--r-- | modules/core/module-orchestrator.js | 151 | ||||
| -rw-r--r-- | modules/core/module-system.js | 20 | ||||
| -rw-r--r-- | modules/core/terminal.js | 7 |
9 files changed, 465 insertions, 41 deletions
diff --git a/modules/core/module-agent-registry.js b/modules/core/module-agent-registry.js new file mode 100644 index 0000000..367f8b1 --- /dev/null +++ b/modules/core/module-agent-registry.js @@ -0,0 +1,79 @@ +// module-agent-registry.js โ VFS-backed agent directory +// Command: agent-list, agent-register, agent-unregister, agent-status +class AgentRegistryModule { + constructor() { + this.commands = {}; + this.terminal = null; + } + 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]); + } + init(terminal) { + this.terminal = terminal; + if (!this.terminal.fs.readFile('/agents/')) { + this.terminal.fs.mkdir('agents'); + } + 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'); + } + 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'); + } + }); + } + 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); + this.terminal.fs.writeFile(`${agentDir}/status.json`, JSON.stringify({ + id, + status: 'idle', + capabilities, + registered: new Date().toISOString(), + lastHeartbeat: new Date().toISOString() + })); + 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'); + } else { + this.terminal.print(`โ Agent '${id}' not found`, 'error'); + } + } + showAgentStatus(id) { + if (!id) return this.terminal.print('Usage: agent-status <agent-id>', 'error'); + const data = this.terminal.fs.readFile(`/agents/${id}/status.json`); + if (!data) return this.terminal.print(`โ Agent '${id}' not found`, 'error'); + const s = JSON.parse(data); + 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(` Registered: ${s.registered}`, 'info'); + this.terminal.print(` Last Heartbeat: ${s.lastHeartbeat}`, 'info'); + } + getAgent(id) { + const data = this.terminal.fs.readFile(`/agents/${id}/status.json`); + return data ? JSON.parse(data) : null; + } + updateHeartbeat(id) { + const agent = this.getAgent(id); + if (!agent) return false; + agent.lastHeartbeat = new Date().toISOString(); + this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent)); + return true; + } +}
\ No newline at end of file diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index edc5225..eb41af9 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -384,6 +384,40 @@ RULES: description: 'Session aggregate stats', parameters: { type: 'object', properties: {}, required: [] } } + }, { + type: 'function', + function: { + name: 'list_agents', + description: 'List registered agent IDs, status, and capabilities', + parameters: { type: 'object', properties: {}, required: [] } + } + }, { + type: 'function', + function: { + name: 'delegate_task', + description: '(DESTRUCTIVE) Delegate a task to a worker agent. Auto-spawns if needed.', + parameters: { + type: 'object', + properties: { + task: { type: 'string', description: 'The task description to delegate' } + }, + required: ['task'] + } + } + }, { + type: 'function', + function: { + name: 'spawn_agent', + description: '(DESTRUCTIVE) Spawn a new worker agent with a role and capabilities', + parameters: { + type: 'object', + properties: { + role: { type: 'string', description: 'Agent role (e.g. researcher, writer)' }, + capabilities: { type: 'string', description: 'Comma-separated capabilities (optional)' } + }, + required: ['role'] + } + } }]; } getHelpInfo() { @@ -568,7 +602,14 @@ RULES: (entry.error ? ' <span class="debug-err">โ ๏ธ</span>' : '') + '</div>'; } - panel.innerHTML = '<div class="debug-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;border-bottom:1px solid #333;font-weight:bold;color:var(--success-color);font-size:0.9em;">๐ง Tool Log <span class="debug-count" style="color:var(--text-muted);font-weight:normal;font-size:0.85em;">' + this._toolCallLog.length + ' calls</span><span style="cursor:pointer;color:var(--text-muted);font-size:1.1em;touch-action:manipulation;" onclick="var p=document.getElementById(\'debug-panel\');if(p)p.style.display=\'none\';var a=window.webx11&&webx11.moduleManager&&webx11.moduleManager.modules&&webx11.moduleManager.modules.WebX11AIAssistantModule;if(a)a._debugPanelEnabled=false;">โ</span></div>' + html; + panel.innerHTML = '<div class="debug-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;border-bottom:1px solid #333;font-weight:bold;color:var(--success-color);font-size:0.9em;flex-wrap:wrap;gap:4px;">' + + '<span>๐ง Tool Log <span class="debug-count" style="color:var(--text-muted);font-weight:normal;font-size:0.85em;">' + this._toolCallLog.length + ' calls</span></span>' + + '<span style="display:flex;gap:4px;">' + + '<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var t=m._toolCallLog.map(function(e){return\'#\'+e.turn+\' \'+e.name+\' (\'+e.duration+\'s)\'+(e.error?\' โ ๏ธ\':\'\')+\'\\n\'+JSON.stringify(e.args).substring(0,80)}).join(\'\\n\\n\');navigator.clipboard.writeText(t).then(function(){m.terminal&&m.terminal.print(\'๐ Debug log copied to clipboard\',\'success\')}).catch(function(){prompt(\'Copy debug log:\',t)})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">๐</button>' + + '<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var json=JSON.stringify(m._toolCallLog,null,2);var b=new Blob([json],{type:\'application/json\'});var u=URL.createObjectURL(b);var a=document.createElement(\'a\');a.href=u;a.download=\'debug-log-\'+new Date().toISOString().slice(0,19)+\'.json\';document.body.appendChild(a);a.click();setTimeout(function(){document.body.removeChild(a);URL.revokeObjectURL(u)},2000);m.terminal&&m.terminal.print(\'๐ฅ Debug log exported\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">๐ฅ</button>' + + '<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var existing=[];var raw=m.terminal.fs.readFile(\'/debug.log\');if(raw){try{existing=JSON.parse(raw);}catch(e){}}var log=m._toolCallLog;var seen={};existing.forEach(function(e){seen[e.turn]=true;});log.forEach(function(e){if(!seen[e.turn]){existing.push(e);seen[e.turn]=true;}});var json=JSON.stringify(existing,null,2);m.terminal.fs.writeFile(\'/debug.log\',json);m.terminal.print(\'๐พ Debug log appended โ \'+existing.length+\' entries total\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">๐พ</button>' + + '<span style="cursor:pointer;color:var(--text-muted);font-size:1.1em;touch-action:manipulation;" onclick="var p=document.getElementById(\'debug-panel\');if(p)p.style.display=\'none\';var a=window.webx11&&webx11.moduleManager&&webx11.moduleManager.modules&&webx11.moduleManager.modules.WebX11AIAssistantModule;if(a)a._debugPanelEnabled=false;">โ</span>' + + '</span></div>' + html; } _isToolError(result) { if (!result || typeof result !== 'string') return null; @@ -1194,6 +1235,37 @@ RULES: for (const [, meta] of entries) totalTasks += meta.tasks || 0; return JSON.stringify({ totalSessions: entries.length, totalTasks, dates: entries.map(([d]) => d) }); } + case 'list_agents': { + const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; + if (!registry) return JSON.stringify({ error: 'Agent registry not available' }); + const agents = this.terminal.fs.ls('/agents/'); + const results = []; + if (agents) { + agents.forEach(a => { + if (a.type === 'dir' && a.name !== 'configured') { + const data = this.terminal.fs.readFile(`/agents/${a.name}/status.json`); + if (data) results.push(JSON.parse(data)); + } + }); + } + return JSON.stringify({ agents: results, count: results.length }); + } + case 'delegate_task': { + const task = a.task || ''; + if (!task) return JSON.stringify({ error: 'No task provided' }); + const orch = this.terminal.moduleManager.modules['OrchestratorModule']; + if (!orch) return JSON.stringify({ error: 'Orchestrator not available' }); + orch.delegateTask(task); + return JSON.stringify({ message: `Task delegated: ${task.substring(0, 80)}` }); + } + case 'spawn_agent': { + const role = a.role || 'worker'; + const caps = a.capabilities ? a.capabilities.split(',').map(s => s.trim()) : ['list_files', 'read_file']; + const orch = this.terminal.moduleManager.modules['OrchestratorModule']; + if (!orch) return JSON.stringify({ error: 'Orchestrator not available' }); + const agent = orch.spawnAgent(role, caps); + return JSON.stringify({ message: `Agent spawned: ${agent.id}`, agent }); + } case 'load_skills': { const query = (a && a.query || '').toLowerCase(); const fs = this.terminal.fs; diff --git a/modules/core/module-chatstudio.js b/modules/core/module-chatstudio.js index 762eae3..9643fff 100644 --- a/modules/core/module-chatstudio.js +++ b/modules/core/module-chatstudio.js @@ -17,32 +17,12 @@ class ChatStudioModule { getHelpInfo() { return { - 'chat': 'Toggle the ChatStudio overlay', - 'chat-setapi': 'Set the API base URL for ChatStudio', - 'chat-setmodel': 'Set the default model for ChatStudio', - 'chat-ask': 'Ask a question directly via ChatStudio' + 'chat': 'Toggle the ChatStudio overlay (inside: /setapi, /setmodel, /ask, /help)', }; } registerCommands() { this.commands['chat'] = () => this.toggleChatOverlay(); - this.commands['chat-setapi'] = (args) => { - if (!args[0]) { - const base = window.API_BASE || '/api'; - this.terminal.print('Current API base: ' + base, 'info'); - this.terminal.print('Usage: chat-setapi <base-url>', 'info'); - this.terminal.print('Example: chat-setapi /api', 'info'); - return; - } - const url = args[0].replace(/\/+$/, ''); - window.API_BASE = url; - try { - var settings = JSON.parse(localStorage.getItem('webx11_chat_settings') || '{}'); - settings.apiBase = url; - localStorage.setItem('webx11_chat_settings', JSON.stringify(settings)); - } catch (e) {} - this.terminal.print('API base set to: ' + url + ' (reopen chat to apply)', 'success'); - }; } toggleChatOverlay() { @@ -59,7 +39,7 @@ class ChatStudioModule { if (!this._cssLoaded) { const link = document.createElement('link'); link.rel = 'stylesheet'; - link.href = 'chat/chat-ui.css?v=4'; + link.href = 'chat/chat-ui.css?v=5'; link.onload = () => { this._cssLoaded = true; }; link.onerror = () => reject(new Error('Failed to load chat/chat-ui.css')); document.head.appendChild(link); @@ -120,7 +100,7 @@ class ChatStudioModule { resolve(); return; } - fetch('chat/chat-ui.html?v=4') + fetch('chat/chat-ui.html?v=5') .then(r => r.text()) .then(html => { this._overlayHtml = html; // cache for re-open @@ -134,7 +114,7 @@ class ChatStudioModule { const loadScript = (src) => { return new Promise((res, rej) => { const s = document.createElement('script'); - s.src = src; + s.src = src + '?v=5'; s.onload = () => res(); s.onerror = () => rej(new Error('Failed to load ' + src)); document.body.appendChild(s); @@ -142,8 +122,8 @@ class ChatStudioModule { }; Promise.all([ - loadScript('chat/chat-commands.js?v=4'), - loadScript('chat/chat-modules.js?v=4') + loadScript('chat/chat-commands.js'), + loadScript('chat/chat-modules.js') ]).then(() => { resolve(); }); diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js index da2e740..b119343 100644 --- a/modules/core/module-fs.js +++ b/modules/core/module-fs.js @@ -279,18 +279,44 @@ class FileSystemModule { this.commands['upload'] = () => { document.getElementById('file-upload-input').click(); }; this.commands['download'] = (args) => { if(!args[0]) return this.terminal.print('Usage: download <filename>', 'warning'); - const content = this.terminal.fs.readFile(args[0]); + const fname = args[0]; + const content = this.terminal.fs.readFile(fname); if(content !== null && content !== undefined) { + const size = content.length; + const sizeStr = size > 1024 ? (size / 1024).toFixed(1) + ' KB' : size + ' B'; + // Desktop: programmatic click (works on desktop Chrome/Firefox) const blob = new Blob([content], {type: 'application/octet-stream'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); - a.href = url; a.download = args[0]; + a.href = url; a.download = fname; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 2000); - this.terminal.print('Downloaded: ' + args[0], 'success'); - } else { this.terminal.print('download: ' + args[0] + ': No such file', 'error'); } + // Mobile fallback: build a DOM button with real user gesture + const line = document.createElement('div'); + line.className = 'output-line'; + const msg = document.createElement('span'); + msg.textContent = 'โฌ ' + fname + ' (' + sizeStr + ') '; + const btn = document.createElement('button'); + btn.textContent = 'โฌ Download'; + btn.className = 'btn'; + btn.style.cssText = 'background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--success-color);padding:4px 12px;border-radius:4px;cursor:pointer;font-size:0.85em;min-height:36px;'; + btn.onclick = () => { + const b = new Blob([this.terminal.fs.readFile(fname)], {type: 'application/octet-stream'}); + const u = URL.createObjectURL(b); + const el = document.createElement('a'); + el.href = u; el.download = fname; + el.style.display = 'none'; + document.body.appendChild(el); + el.click(); + setTimeout(() => { document.body.removeChild(el); URL.revokeObjectURL(u); }, 2000); + }; + line.appendChild(msg); + line.appendChild(btn); + this.terminal.outputContainer.appendChild(line); + this.terminal.scrollToBottom(); + } else { this.terminal.print('download: ' + fname + ': No such file', 'error'); } }; this.commands['vfs-export'] = () => { const data = this.terminal.fs.root; diff --git a/modules/core/module-leases.js b/modules/core/module-leases.js new file mode 100644 index 0000000..b79870c --- /dev/null +++ b/modules/core/module-leases.js @@ -0,0 +1,102 @@ +// module-leases.js โ Lease system for agent task delegation +// Commands: lease-create, lease-list, lease-revoke, lease-renew +class LeaseModule { + constructor() { + this.commands = {}; + this.terminal = null; + this._leases = {}; + this._watchdogTimer = null; + } + registerCommands() { + this.commands['lease-create'] = (args) => this.createLease(args); + this.commands['lease-list'] = () => this.listLeases(); + this.commands['lease-revoke'] = (args) => this.revokeLease(args[0]); + this.commands['lease-renew'] = (args) => this.renewLease(args[0], parseInt(args[1]) || 30); + } + init(terminal) { + this.terminal = terminal; + if (!this.terminal.fs.readFile('/leases/')) { + this.terminal.fs.mkdir('leases'); + } + // Restore persisted leases + const files = this.terminal.fs.ls('/leases/'); + if (files) { + files.forEach(f => { + if (f.name.endsWith('.json')) { + const data = this.terminal.fs.readFile(`/leases/${f.name}`); + if (data) { + try { + const lease = JSON.parse(data); + this._leases[lease.id] = lease; + } catch (e) {} + } + } + }); + } + // Start watchdog every 5 seconds + this._watchdogTimer = setInterval(() => this._checkExpired(), 5000); + this.terminal.print('๐ Lease system loaded', 'info'); + } + createLease(args) { + const workerId = args[1]; + const duration = parseInt(args[2]) || 30; + const task = args.slice(3).join(' ') || 'generic task'; + if (!workerId) return this.terminal.print('Usage: lease-create <agent-id> [duration_sec] [task]', 'error'); + + const lease = { + id: `lease-${Date.now()}`, + workerId, + orchestratorId: 'orchestrator-0', + task, + createdAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + duration * 1000).toISOString(), + status: 'active', + duration + }; + this._leases[lease.id] = lease; + this.terminal.fs.writeFile(`leases/${lease.id}.json`, JSON.stringify(lease)); + this.terminal.print(`๐ Lease ${lease.id} created for ${workerId} (${duration}s, task: ${task.substring(0, 40)})`, 'success'); + return lease; + } + listLeases() { + const all = Object.values(this._leases); + const active = all.filter(l => l.status === 'active'); + const expired = all.filter(l => l.status === 'expired'); + const revoked = all.filter(l => l.status === 'revoked'); + this.terminal.print(`๐ Leases: ${active.length} active, ${expired.length} expired, ${revoked.length} revoked`, 'header'); + active.forEach(l => { + const remaining = Math.max(0, Math.round((new Date(l.expiresAt) - Date.now()) / 1000)); + this.terminal.print(` ${l.id} โ ${l.workerId} (${remaining}s remain) โ ${l.task.substring(0, 40)}`, 'info'); + }); + } + revokeLease(id) { + if (!this._leases[id]) return this.terminal.print(`โ Lease ${id} not found`, 'error'); + this._leases[id].status = 'revoked'; + this._leases[id].revokedAt = new Date().toISOString(); + this.terminal.fs.writeFile(`leases/${id}.json`, JSON.stringify(this._leases[id])); + this.terminal.print(`โ
Lease ${id} revoked`, 'warning'); + } + renewLease(id, additionalSeconds) { + if (!this._leases[id] || this._leases[id].status !== 'active') { + return this.terminal.print(`โ Lease ${id} not found or not active`, 'error'); + } + const lease = this._leases[id]; + const newExpiry = new Date(Date.now() + additionalSeconds * 1000); + lease.expiresAt = newExpiry.toISOString(); + lease.duration += additionalSeconds; + lease.renewedAt = new Date().toISOString(); + this.terminal.fs.writeFile(`leases/${id}.json`, JSON.stringify(lease)); + this.terminal.print(`โ
Lease ${id} renewed (+${additionalSeconds}s)`, 'success'); + } + _checkExpired() { + const now = Date.now(); + Object.values(this._leases).forEach(lease => { + if (lease.status === 'active' && new Date(lease.expiresAt) < now) { + lease.status = 'expired'; + lease.expiredAt = new Date().toISOString(); + this.terminal.fs.writeFile(`leases/${lease.id}.json`, JSON.stringify(lease)); + this.terminal.log('INFO', 'Lease', `Lease ${lease.id} for ${lease.workerId} expired`); + } + }); + } +}
\ No newline at end of file diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 7187fec..2ffdb17 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -30,7 +30,10 @@ 'module-fetcher.js', 'module-skills.js', 'secret-vault.js', - 'module-chatstudio.js' + 'module-chatstudio.js', + 'module-agent-registry.js', + 'module-leases.js', + 'module-orchestrator.js' ]; function getSelectedModules() { @@ -99,7 +102,7 @@ window.__moduleLoaderReload = reloadWithSelection; -const LATEST_VER = '2026-06-02-fontfix3'; +const LATEST_VER = '2026-06-02-webdav2'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js new file mode 100644 index 0000000..4fab599 --- /dev/null +++ b/modules/core/module-orchestrator.js @@ -0,0 +1,151 @@ +// module-orchestrator.js โ Multi-agent orchestration system +// Commands: orchestrate, orchestrator-status, orchestrator-spawn, orchestrator-kill, orchestrator-purge +class OrchestratorModule { + constructor() { + this.commands = {}; + this.terminal = null; + this._agents = {}; + this._SPAWN_LIMIT = 3; + this._heartbeatInterval = null; + } + registerCommands() { + this.commands['orchestrate'] = (args) => this.delegateTask(args.join(' ')); + this.commands['orchestrator-status'] = () => this.showStatus(); + this.commands['orchestrator-spawn'] = (args) => this.spawnAgent(args[0], args.slice(1)); + this.commands['orchestrator-kill'] = (args) => this.killAgent(args[0]); + this.commands['orchestrator-purge'] = () => this.purgeDeadAgents(); + } + init(terminal) { + this.terminal = terminal; + // Register orchestrator in the agent registry + const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; + if (registry) { + registry.registerAgent(['register', 'orchestrator-0', 'delegate', 'spawn', 'monitor', 'purge']); + } + // Heartbeat watchdog every 3 seconds + this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), 3000); + this.terminal.print('๐ง Orchestrator loaded โ ready to delegate', 'success'); + } + spawnAgent(role, capabilities) { + const id = `agent-${Object.keys(this._agents).length + 1}-${role || 'worker'}`; + if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) { + this.terminal.print(`โ Agent spawn limit (${this._SPAWN_LIMIT}) reached`, 'error'); + return null; + } + const agent = { + id, + role: role || 'worker', + capabilities: capabilities.length ? capabilities : ['list_files', 'read_file', 'web_search'], + createdAt: new Date().toISOString(), + lastHeartbeat: Date.now(), + status: 'idle', + currentTask: null + }; + this._agents[id] = agent; + // Register in agent registry + const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; + if (registry) registry.registerAgent(['register', id, ...agent.capabilities]); + this.terminal.print(`๐ค Agent '${id}' spawned (role: ${role || 'worker'}, ${agent.capabilities.length} caps)`, 'success'); + return agent; + } + killAgent(id) { + if (!this._agents[id]) return this.terminal.print(`โ Agent '${id}' not found`, 'error'); + // Revoke any active lease + const leaseModule = this.terminal.moduleManager.modules['LeaseModule']; + if (leaseModule) { + Object.values(leaseModule._leases).forEach(l => { + if (l.workerId === id && l.status === 'active') leaseModule.revokeLease(l.id); + }); + } + // Unregister from registry + const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; + if (registry) registry.unregisterAgent(id); + delete this._agents[id]; + this.terminal.print(`๐ Agent '${id}' killed`, 'warning'); + } + async delegateTask(task) { + if (!task) return this.terminal.print('Usage: orchestrate <task description>', 'error'); + // Find an idle agent or auto-spawn + let agent = Object.values(this._agents).find(a => a.status === 'idle'); + if (!agent) { + if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) { + return this.terminal.print('โ All agents busy and spawn limit reached', 'error'); + } + this.spawnAgent('worker', ['list_files', 'read_file', 'write_file', 'web_search']); + agent = Object.values(this._agents).find(a => a.status === 'idle'); + } + if (!agent) return this.terminal.print('โ No available agents', 'error'); + // Create a lease + const leaseModule = this.terminal.moduleManager.modules['LeaseModule']; + const lease = leaseModule ? leaseModule.createLease(['lease-create', agent.id, '60', task]) : null; + // Mark agent as busy + agent.status = 'busy'; + agent.currentTask = task; + agent.leaseId = lease?.id; + // Write task to agent's inbox + if (!this.terminal.fs.readFile(`/agents/${agent.id}/`)) this.terminal.fs.mkdir(`agents/${agent.id}`); + this.terminal.fs.writeFile(`agents/${agent.id}/inbox.json`, JSON.stringify({ + task, + from: 'orchestrator-0', + leaseId: lease?.id, + timestamp: new Date().toISOString(), + ttl_seconds: 60 + })); + this.terminal.print(`๐ฏ Delegated "${task.substring(0, 60)}" to ${agent.id} (lease: ${lease?.id || 'none'})`, 'info'); + // Execute via AI assistant + try { + const aiModule = this.terminal.moduleManager.modules['WebX11AIAssistantModule']; + if (aiModule && typeof aiModule.chat === 'function') { + const result = await aiModule.chat([`[DELEGATED] ${task}`]); + agent.status = 'completed'; + this.terminal.fs.writeFile(`agents/${agent.id}/outbox.json`, JSON.stringify({ + result: result || 'completed', + status: 'completed', + timestamp: new Date().toISOString() + })); + this.terminal.print(`โ
${agent.id} completed task`, 'success'); + } else { + this.terminal.print(`โน๏ธ AI module not available โ task queued for ${agent.id}`, 'info'); + } + } catch (e) { + agent.status = 'failed'; + this.terminal.print(`โ ${agent.id} failed: ${e.message}`, 'error'); + } + // Return to idle pool + agent.status = 'idle'; + agent.currentTask = null; + agent.lastHeartbeat = Date.now(); + } + _checkHeartbeats() { + const now = Date.now(); + Object.entries(this._agents).forEach(([id, agent]) => { + if (agent.status === 'busy' && now - agent.lastHeartbeat > 15000) { + this.terminal.print(`โ ๏ธ Agent '${id}' unresponsive (${Math.round((now - agent.lastHeartbeat) / 1000)}s since heartbeat)`, 'warning'); + } + }); + } + purgeDeadAgents() { + const now = Date.now(); + let count = 0; + Object.entries(this._agents).forEach(([id, agent]) => { + if (now - agent.lastHeartbeat > 60000) { + this.killAgent(id); + count++; + } + }); + this.terminal.print(`๐งน Purged ${count} dead agents`, count > 0 ? 'warning' : 'info'); + } + showStatus() { + const total = Object.keys(this._agents).length; + const busy = Object.values(this._agents).filter(a => a.status === 'busy').length; + const idle = Object.values(this._agents).filter(a => a.status === 'idle').length; + const failed = Object.values(this._agents).filter(a => a.status === 'failed').length; + this.terminal.print('๐ง Orchestrator Status', 'header'); + this.terminal.print(` Agents: ${total} (${busy} busy, ${idle} idle, ${failed} failed)`, 'info'); + this.terminal.print(` Spawn limit: ${this._SPAWN_LIMIT}`, 'info'); + Object.values(this._agents).forEach(a => { + const task = a.currentTask ? ` (task: ${a.currentTask.substring(0, 40)})` : ''; + this.terminal.print(` ${a.id} โ ${a.status}${task}`, 'info'); + }); + } +}
\ No newline at end of file diff --git a/modules/core/module-system.js b/modules/core/module-system.js index e54a5fe..c0dbe14 100644 --- a/modules/core/module-system.js +++ b/modules/core/module-system.js @@ -36,6 +36,13 @@ this.terminal.print(`Memory Usage: ${mem} MB`, 'info'); document.getElementById('header-status').innerText = `Mem: ${mem}MB`; }; + // Refresh memory display on boot + setTimeout(() => { + if (performance.memory) { + const mem = (performance.memory.usedJSHeapSize / 1048576).toFixed(1); + document.getElementById('header-status').innerText = `Mem: ${mem}MB`; + } + }, 500); this.commands['status'] = () => { this.terminal.print(`System: WebX11 v4.2`); this.terminal.print(`Network: ${navigator.onLine ? 'Online' : 'Offline'}`); @@ -223,12 +230,12 @@ } // Categorize all commands const cats = { - files: ['ls','ll','cat','put','rm','mv','cp','mkdir','rmdir','cd','pwd','find','grep','head','tail','sort','wc','touch','tree','du','upload','download','vfs-export','vfs-import','edit','diff'], - ai: ['ai','ask','ai-clear','ai-system','ai-context','ai-allow','ai-deny','ai-ignore','ai-stop','ai-model'], + files: ['ls','ll','cat','put','rm','mv','cp','mkdir','rmdir','cd','pwd','find','grep','head','tail','sort','wc','touch','tree','du','upload','download','vfs-export','vfs-import','edit','diff','trash','trash-dir','ls-trash','empty-trash'], + ai: ['ai','ask','ai-clear','ai-system','ai-context','ai-allow','ai-deny','ai-ignore','ai-stop','ai-model','ai-compress','debug'], search: ['search','s','ddg','duck','fetch','web','extract'], tools: ['calc','cron-every','cron-list','cron-stop','cron-clear','wpm','mindmap','theme','voice','alias','unalias', - 'which','env','date','cal','yes','seq','base64','printf','cut','uniq','hostname','whoami','basename','dirname','sleep','expand','tutorial'], - system: ['help','clear','cls','echo','mem','status','font-size','setfont','smart-prompt','logs','preview','factory-reset','reset-vfs','export','unset','script','set','test'], + 'which','env','date','cal','yes','seq','base64','printf','cut','uniq','hostname','whoami','basename','dirname','sleep','expand','tutorial','gzip','gunzip','encrypt','decrypt','files','session-save','session-load','session-list','session-delete','session-export','session-import'], + system: ['help','clear','cls','echo','mem','status','font-size','setfont','smart-prompt','logs','preview','factory-reset','reset-vfs','export','unset','script','set','test','logrotate','health-check','memory-map-export','commands','check','checkpoint','checkpoint-list','rollback','checkpoint-del'], net: ['server','servers','server-check','server-remove','monitor'], mods: ['modules','listmods','loadmod','removemod','load-local','loadlocal','startup-modules'], ollama: ['ollama','ollamaconnect','ollamadisconnect','ollamamodels','ollamasetmodel','ollamasetproxy','models','model-info','model-pull','model-delete','model-run'], @@ -236,14 +243,15 @@ config: ['config','config-set','config-list','config-delete','config-export','config-reset'], webx: ['webxai','wx','webx','context','contexts','context-new','context-delete','smart-ask','prompt','prompt-save','prompt-list','prompt-delete','prompt-var','prompt-vars'], data: ['my','my-set','my-get','my-list','my-export','my-import','my-clear','export-mods','import-mods','backup','restore','share-mod'], - ext: ['chat','chat-setapi','chat-setmodel','chat-ask','webdav-config','webdav-status','webdav-ls','webdav-push','webdav-pull','webdav-backup','webdav-restore','webdav-backup-list'], + ext: ['chat','chat-setapi','chat-setmodel','chat-ask','webdav-config','webdav-status','webdav-ls','webdav-push','webdav-pull','webdav-backup','webdav-restore','webdav-backup-list','webdav-explorer','webdav-browser'], gui: ['gui','gui-button','gui-input','gui-text','gui-clear','gui-run'], + vault: ['secret-set','secret-get','secret-list','secret-del','secret-lock','secret-unlock','secret-setpin','secret-status'], }; const labels = { files:'๐ Filesystem', ai:'๐ค AI', search:'๐ Search & Web', tools:'๐ง Tools', system:'โ๏ธ System', net:'๐ก Network', mods:'๐งฉ Modules', ollama:'๐ฆ Ollama', skills:'๐ Skills', config:'โ๏ธ Config', webx:'๐ WebX', data:'๐พ Data', - ext:'๐งฐ Extras', gui:'๐ฅ๏ธ GUI' + ext:'๐งฐ Extras', gui:'๐ฅ๏ธ GUI', vault:'๐ Vault' }; let result = 'โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\nโ ๐ท WebX11 v4.2 โ All Commands โ\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n\n'; const total = Object.keys(helpMap).length; diff --git a/modules/core/terminal.js b/modules/core/terminal.js index f0ef036..4111f5f 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -868,10 +868,10 @@ const lowerInput = input.toLowerCase(); // Expanded command lists for autocomplete - const fileArgCommands = ['cat', 'preview', 'rm', 'rmdir', 'edit', 'view', 'cp', 'mv', 'touch', 'put', 'run', 'chmod', 'gzip', 'gunzip', 'encrypt', 'decrypt', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list']; + const fileArgCommands = ['cat', 'preview', 'rm', 'rmdir', 'edit', 'view', 'cp', 'mv', 'touch', 'put', 'run', 'chmod', 'gzip', 'gunzip', 'encrypt', 'decrypt', 'download', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list']; const dirArgCommands = ['ls', 'cd', 'mkdir', 'rmdir']; const sessionArgCommands = ['session-load', 'session-delete', 'session-export', 'session-save', 'session-list', 'session-import']; - const pathArgCommands = ['cat', 'preview', 'rm', 'edit', 'view', 'cp', 'mv', 'touch', 'put', 'run', 'chmod', 'gzip', 'gunzip', 'encrypt', 'decrypt', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list', 'ls', 'cd', 'mkdir', 'rmdir', 'cat', 'preview']; + const pathArgCommands = ['cat', 'preview', 'rm', 'edit', 'view', 'cp', 'mv', 'touch', 'put', 'run', 'chmod', 'gzip', 'gunzip', 'encrypt', 'decrypt', 'download', 'webdav-backup', 'webdav-restore', 'webdav-upload', 'webdav-download', 'webdav-list', 'ls', 'cd', 'mkdir', 'rmdir', 'cat', 'preview']; // Collect commands that take arguments from ext module metadata const extFileCmds = new Set(); @@ -1024,6 +1024,9 @@ this._safeRegister('SecretVault', 'secret vault'); this.reloadPersistedModules(); this._safeRegister('ChatStudioModule', 'chat studio'); + this._safeRegister('AgentRegistryModule', 'agent registry'); + this._safeRegister('LeaseModule', 'lease system'); + this._safeRegister('OrchestratorModule', 'orchestrator'); } _safeRegister(className, label) { let cls; |