aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/module-agent-registry.js79
-rw-r--r--modules/core/module-aiassist.js74
-rw-r--r--modules/core/module-chatstudio.js32
-rw-r--r--modules/core/module-fs.js34
-rw-r--r--modules/core/module-leases.js102
-rw-r--r--modules/core/module-loader.js7
-rw-r--r--modules/core/module-orchestrator.js151
-rw-r--r--modules/core/module-system.js20
-rw-r--r--modules/core/terminal.js7
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;