aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/_write-probe.txt1
-rw-r--r--modules/core/core-bundle.js3424
-rw-r--r--modules/core/module-aiassist.js934
-rw-r--r--modules/core/module-chatstudio.js3
-rw-r--r--modules/core/module-debug-supervisor.js701
-rw-r--r--modules/core/module-duckduckgo.js38
-rw-r--r--modules/core/module-dynamicprompts.js102
-rw-r--r--modules/core/module-io.js80
-rw-r--r--modules/core/module-leases.js17
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-memory-graph.js303
-rw-r--r--modules/core/module-my.js82
-rw-r--r--modules/core/module-neural-vfs.js58
-rw-r--r--modules/core/module-orchestrator.js140
-rw-r--r--modules/core/module-smartctx.js112
-rw-r--r--modules/core/module-world-model.js96
-rw-r--r--modules/core/ollama-manager.js2
-rw-r--r--modules/core/terminal.js291
-rw-r--r--modules/core/virtual-fs.js12
-rw-r--r--modules/ext/critical-ext-bundle.js315
-rw-r--r--modules/ext/shell.js315
-rw-r--r--modules/ext/skill-pack-core.js298
-rw-r--r--modules/ext/three.js1411
-rw-r--r--modules/ext/tutorial.js6
-rw-r--r--modules/ext/voice.js91
25 files changed, 7587 insertions, 1247 deletions
diff --git a/modules/_write-probe.txt b/modules/_write-probe.txt
new file mode 100644
index 0000000..5e2a6f8
--- /dev/null
+++ b/modules/_write-probe.txt
@@ -0,0 +1 @@
+write-probe-test \ No newline at end of file
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index d576f14..aab0206 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -39,6 +39,7 @@ class VirtualFileSystem {
const vmCache = {}; // factories can cache costly results within same tick
const mm = () => typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null;
const ai = () => mm()?.WebX11AIAssistantModule || null;
+ const ds = () => mm()?.DebugSupervisorModule || null;
const heap = () => typeof performance !== 'undefined' && performance.memory ? performance.memory : null;
const fmt = (bytes) => {
if (!bytes && bytes !== 0) return 'N/A';
@@ -114,7 +115,7 @@ class VirtualFileSystem {
const a = ai();
const cCount = a?.context?.length || 0;
const lCount = a?._toolCallLog?.length || 0;
- const eCount = a?._errorLog?.length || 0;
+ const eCount = ds() ? Object.keys(ds()._errorDB?.errors || {}).length : 0;
const modules = mm() ? Object.keys(mm()).length : 0;
const domNodes = typeof document !== 'undefined' ? document.querySelectorAll('*').length : 0;
return [
@@ -168,9 +169,12 @@ class VirtualFileSystem {
return a._toolCallLog.map((e, i) => `[${i}] ${JSON.stringify(e)}`).join('\n');
}),
'errors': vm('errors', () => {
- const a = ai();
- if (!a || !a._errorLog || a._errorLog.length === 0) return '(empty)';
- return a._errorLog.map((e, i) => `[${i}] ${JSON.stringify(e)}`).join('\n');
+ const d = ds();
+ if (!d || !d._errorDB || Object.keys(d._errorDB.errors).length === 0) return '(empty)';
+ return Object.entries(d._errorDB.errors)
+ .sort((a, b) => b[1].lastSeen - a[1].lastSeen)
+ .map(([hash, e]) => `[${hash}] ${e.type}/${e.severity} ×${e.count}: ${e.message.substring(0, 100)}`)
+ .join('\n');
}),
'commands': vm('commands', () => {
const a = ai();
@@ -1188,7 +1192,7 @@ class OllamaManager {
this.streamEnabled = localStorage.getItem('webx11_stream_enabled') === 'true';
// --- Validator Agent Config ---
this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true';
- this.validatorModel = localStorage.getItem('webx11_validator_model') || 'ministral-3:3b-cloud';
+ this.validatorModel = localStorage.getItem('webx11_validator_model') || 'gemma4:31b-cloud';
this.validatorEndpoint = localStorage.getItem('webx11_validator_endpoint') || '';
// --- Model Parameters (temperature, top_p, max_tokens, thinking) ---
this.modelParams = this._loadModelParams();
@@ -2005,15 +2009,7 @@ class OllamaManager {
// DeepSeek: empty response with no tools — retry once with stream forcing
if (isDeepSeek && content === '' && (!toolCalls || toolCalls.length === 0)) {
- this.terminal.log('WARN', 'Ollama', 'DeepSeek empty response — model=' + useModel + ', messages=' + messages.length + ', tools=' + (tools ? tools.length : 0));
this.terminal.print('⚠️ DeepSeek returned empty — retrying with stream...', 'warning');
- // Auto-truncate context: keep system prompt + last 5 messages
- if (messages.length > 6) {
- var sysMsg = messages[0];
- var recentMsgs = messages.slice(-5);
- body.messages = [sysMsg].concat(recentMsgs);
- this.terminal.print('📏 Context truncated from ' + messages.length + ' to ' + body.messages.length + ' messages for retry', 'info');
- }
body.stream = true;
delete body.stop;
this._abortController = new AbortController();
@@ -2048,16 +2044,12 @@ class OllamaManager {
}
}
if (fullText) {
- this.terminal.log('INFO', 'Ollama', 'DeepSeek stream retry succeeded: ' + fullText.length + ' chars');
return {
message: { role: 'assistant', content: fullText },
tool_calls: null,
error: null
};
}
- this.terminal.log('WARN', 'Ollama', 'DeepSeek stream retry also returned empty');
- } else {
- this.terminal.log('WARN', 'Ollama', 'DeepSeek stream retry HTTP ' + retryResponse.status);
}
return {
message: { role: 'assistant', content: '' },
@@ -2441,19 +2433,71 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
this.lastRouteTime = 0;
this.lastRouteCmd = '';
this.blockedCount = 0;
- this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']);
- this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']);
- this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']);
- this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze']);
- // Load persisted overrides from localStorage
- try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){}
- try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){}
- try { const w = localStorage.getItem('webx11_security_whitelist'); if(w){JSON.parse(w).forEach(c=>this.whitelist.add(c));} }catch(e){}
- try { const f = localStorage.getItem('webx11_security_flags'); if(f){JSON.parse(f).forEach(c=>this.suspiciousFlags.add(c));} }catch(e){}
+ this._baseDestructive = ['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot'];
+ this._baseSystem = ['reboot','shutdown','format','kill','halt','poweroff'];
+ this._baseSuspiciousFlags = ['--dump','--execute','--eval','--exec','--shell','--script'];
+ this._baseWhitelist = ['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','debug-stats','debug-history','debug-analyze','debug-trends','debug-fix','debug-health','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard','context-auto'];
+ this.destructiveCmds = new Set(this._baseDestructive);
+ this.systemCmds = new Set(this._baseSystem);
+ this.suspiciousFlags = new Set(this._baseSuspiciousFlags);
+ this.whitelist = new Set(this._baseWhitelist);
+ // Load persisted user deltas (add/remove) so hardcoded defaults can be demoted.
+ this._loadSecurityDeltas();
this._registerBuiltIn();
}
+ _loadSecurityDeltas() {
+ try {
+ const schema = this.terminal._getDataSchemaVersion ? this.terminal._getDataSchemaVersion() : 1;
+ localStorage.setItem('webx11_data_schema_version', String(schema));
+ const raw = localStorage.getItem('webx11_security_deltas');
+ if (!raw) {
+ // One-time migration from legacy flat keys to delta format.
+ const legacy = {};
+ try { legacy.destructive = JSON.parse(localStorage.getItem('webx11_security_destructive') || '[]'); } catch (e) {}
+ try { legacy.system = JSON.parse(localStorage.getItem('webx11_security_system') || '[]'); } catch (e) {}
+ try { legacy.whitelist = JSON.parse(localStorage.getItem('webx11_security_whitelist') || '[]'); } catch (e) {}
+ try { legacy.flags = JSON.parse(localStorage.getItem('webx11_security_flags') || '[]'); } catch (e) {}
+ const delta = { add: {}, remove: {} };
+ for (const c of (legacy.destructive || [])) if (!this._baseDestructive.includes(c)) { if (!delta.add.destructive) delta.add.destructive = []; delta.add.destructive.push(c); }
+ for (const c of (legacy.system || [])) if (!this._baseSystem.includes(c)) { if (!delta.add.system) delta.add.system = []; delta.add.system.push(c); }
+ for (const c of (legacy.whitelist || [])) if (!this._baseWhitelist.includes(c)) { if (!delta.add.whitelist) delta.add.whitelist = []; delta.add.whitelist.push(c); }
+ for (const f of (legacy.flags || [])) if (!this._baseSuspiciousFlags.includes(f)) { if (!delta.add.flags) delta.add.flags = []; delta.add.flags.push(f); }
+ localStorage.setItem('webx11_security_deltas', JSON.stringify(delta));
+ localStorage.removeItem('webx11_security_destructive');
+ localStorage.removeItem('webx11_security_system');
+ localStorage.removeItem('webx11_security_whitelist');
+ localStorage.removeItem('webx11_security_flags');
+ return;
+ }
+ const d = JSON.parse(raw);
+ if (d.remove) {
+ for (const c of (d.remove.destructive || [])) this.destructiveCmds.delete(c);
+ for (const c of (d.remove.system || [])) this.systemCmds.delete(c);
+ for (const c of (d.remove.whitelist || [])) this.whitelist.delete(c);
+ for (const f of (d.remove.flags || [])) this.suspiciousFlags.delete(f);
+ }
+ if (d.add) {
+ for (const c of (d.add.destructive || [])) this.destructiveCmds.add(c);
+ for (const c of (d.add.system || [])) this.systemCmds.add(c);
+ for (const c of (d.add.whitelist || [])) this.whitelist.add(c);
+ for (const f of (d.add.flags || [])) this.suspiciousFlags.add(f);
+ }
+ } catch (e) {}
+ }
+ _saveSecurityDeltas() {
+ const delta = { add: {}, remove: {} };
+ for (const c of this.destructiveCmds) if (!this._baseDestructive.includes(c)) { if (!delta.add.destructive) delta.add.destructive = []; delta.add.destructive.push(c); }
+ for (const c of this._baseDestructive) if (!this.destructiveCmds.has(c)) { if (!delta.remove.destructive) delta.remove.destructive = []; delta.remove.destructive.push(c); }
+ for (const c of this.systemCmds) if (!this._baseSystem.includes(c)) { if (!delta.add.system) delta.add.system = []; delta.add.system.push(c); }
+ for (const c of this._baseSystem) if (!this.systemCmds.has(c)) { if (!delta.remove.system) delta.remove.system = []; delta.remove.system.push(c); }
+ for (const c of this.whitelist) if (!this._baseWhitelist.includes(c)) { if (!delta.add.whitelist) delta.add.whitelist = []; delta.add.whitelist.push(c); }
+ for (const c of this._baseWhitelist) if (!this.whitelist.has(c)) { if (!delta.remove.whitelist) delta.remove.whitelist = []; delta.remove.whitelist.push(c); }
+ for (const f of this.suspiciousFlags) if (!this._baseSuspiciousFlags.includes(f)) { if (!delta.add.flags) delta.add.flags = []; delta.add.flags.push(f); }
+ for (const f of this._baseSuspiciousFlags) if (!this.suspiciousFlags.has(f)) { if (!delta.remove.flags) delta.remove.flags = []; delta.remove.flags.push(f); }
+ localStorage.setItem('webx11_security_deltas', JSON.stringify(delta));
+ }
_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','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','agent-loops','bench','bench-compare'];
+ const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard'];
internals.forEach(c => this.registerCommand(c, { category: 'builtin' }));
}
setLevel(n) {
@@ -2472,8 +2516,13 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
}
registerCommand(name, meta = {}) {
this.commandMeta.set(name, { destructive: false, category: 'user', ...meta });
+ this.invalidateCommandCache();
+ }
+ invalidateCommandCache() {
+ this._commandCacheDirty = true;
}
_collectAllCommands() {
+ if (!this._commandCacheDirty && this._commandCache) return this._commandCache;
const cmds = new Set([...this.commandMeta.keys()]);
for (const modName in this.terminal.moduleManager.modules) {
const mod = this.terminal.moduleManager.modules[modName];
@@ -2483,7 +2532,9 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
}
const builtins = ['clear','help','ls','df','security','security-level'];
builtins.forEach(c => cmds.add(c));
- return [...cmds];
+ this._commandCache = [...cmds];
+ this._commandCacheDirty = false;
+ return this._commandCache;
}
_levenshtein(a, b) {
a = a.toLowerCase(); b = b.toLowerCase();
@@ -2712,6 +2763,19 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
// Apply config overrides after everything is initialized
this._applyTerminalConfig();
}
+ /* ---------- helpers ---------- */
+ static _escapeHtml(str) {
+ if (str == null) return '';
+ return String(str).replace(/[&<>'"]/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[m]));
+ }
+ _escapeHtml(str) { return WebX11Terminal._escapeHtml(str); }
+ _getDataSchemaVersion() { return 1; }
+ _escapeYamlString(str) {
+ return '"' + String(str == null ? '' : str).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
+ }
+ _invalidateCommandCache() {
+ if (this.security) this.security.invalidateCommandCache();
+ }
initEventListeners() {
this.inputField.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -3629,6 +3693,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
this._safeRegister('ServerMonitorModule', 'server monitor');
this._safeRegister('SmartContextModule', 'smart context');
this._safeRegister('SimpleGUIModule', 'simple GUI builder');
+ this._safeRegister('DebugSupervisorModule', 'debug supervisor');
// v4.5.47: Critical ext modules promoted to boot path
this._safeRegister('EditorModule', 'editor');
this._safeRegister('ShellModule', 'shell');
@@ -3676,6 +3741,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
DiffModule: typeof DiffModule !== 'undefined' ? DiffModule : undefined,
BusyBoxModule: typeof BusyBoxModule !== 'undefined' ? BusyBoxModule : undefined,
SmartContextModule: typeof SmartContextModule !== 'undefined' ? SmartContextModule : undefined,
+ DebugSupervisorModule: typeof DebugSupervisorModule !== 'undefined' ? DebugSupervisorModule : undefined,
};
const cls = registry[className];
if (typeof cls === 'undefined') {
@@ -3737,7 +3803,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
} catch (e) {}
}
async _loadExtModules(filterKeys) {
- const autoLoadKeys = filterKeys || ['debug-panel'];
+ const autoLoadKeys = filterKeys || ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console', 'media'];
const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
let index = null;
let lastErr = null;
@@ -3760,9 +3826,6 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
return;
}
- // Cache packages index for lazy-load dispatch
- try { localStorage.setItem('webx11_packages_index', JSON.stringify(index)); } catch (e) {}
-
for (const key of autoLoadKeys) {
const pkg = index.packages[key];
if (pkg) {
@@ -3773,7 +3836,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
}
async reloadPersistedModules() {
const registry = this.moduleManager.moduleRegistry;
- const skipList = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console', 'media', 'three'];
+ const skipList = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console', 'media'];
for (const mod of registry) {
if (mod.enabled && mod.url) {
// Skip ext modules — they load via _loadExtModules
@@ -3808,10 +3871,12 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
splitPipe(input) {
const parts = [];
let current = '';
- let inQuote = false;
+ let inSingle = false;
+ let inDouble = false;
for (const ch of input) {
- if (ch === '"' || ch === "'") { inQuote = !inQuote; current += ch; }
- else if (ch === '|' && !inQuote) { parts.push(current.trim()); current = ''; }
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; current += ch; }
+ else if (ch === "'" && !inDouble) { inSingle = !inSingle; current += ch; }
+ else if (ch === '|' && !inSingle && !inDouble) { parts.push(current.trim()); current = ''; }
else { current += ch; }
}
parts.push(current.trim());
@@ -3851,8 +3916,11 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
const originalPrint = this.print.bind(this);
let output = '';
this.print = (text, type) => { output += text + '\n'; };
- await this.dispatchCommand(cmd, args);
- this.print = originalPrint;
+ try {
+ await this.dispatchCommand(cmd, args);
+ } finally {
+ this.print = originalPrint;
+ }
const trimmed = output.trim();
if (trimmed.length > this._pipeBufferSize) {
return trimmed.slice(0, this._pipeBufferSize);
@@ -3868,6 +3936,21 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
if (cmd === 'load-local' || cmd === 'loadlocal') { this.loadLocalModule(args[0]); return; }
if (cmd === 'smart-prompt') { const val = args[0] && args[0] === 'off' ? 'off' : 'on'; localStorage.setItem('webx11_smart_prompt', val); this.print('Smart prompt ' + val, 'success'); this.updatePrompt(); return; }
if (cmd === 'mem') { this.printMemoryStats(); return; }
+ if (cmd === 'dashboard') {
+ // Collect dashboard data and open the dashboard page
+ const aiMod = this.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && typeof aiMod._collectDashboardData === 'function') {
+ aiMod._collectDashboardData().then(function() {
+ this.print('📊 Dashboard data collected — opening dashboard...', 'success');
+ window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ }.bind(this));
+ } else {
+ // Fallback: just open the page (it will try localStorage)
+ this.print('📊 Opening dashboard...', 'info');
+ window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ }
+ return;
+ }
if (cmd === 'config') {
if (args.length === 0) {
this.print('=== Terminal Config ===', 'info');
@@ -3930,7 +4013,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
if (action === 'add') { this.security.destructiveCmds.add(args[1]); }
else if (action === 'remove') { this.security.destructiveCmds.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_destructive', JSON.stringify([...this.security.destructiveCmds]));
+ this.security._saveSecurityDeltas();
this.print(`Updated destructive set (${this.security.destructiveCmds.size} commands)`, 'success');
return;
}
@@ -3946,7 +4029,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
if (action === 'add') { this.security.systemCmds.add(args[1]); }
else if (action === 'remove') { this.security.systemCmds.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_system', JSON.stringify([...this.security.systemCmds]));
+ this.security._saveSecurityDeltas();
this.print(`Updated system set (${this.security.systemCmds.size} commands)`, 'success');
return;
}
@@ -3962,7 +4045,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
if (action === 'add') { this.security.whitelist.add(args[1]); }
else if (action === 'remove') { this.security.whitelist.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_whitelist', JSON.stringify([...this.security.whitelist]));
+ this.security._saveSecurityDeltas();
this.print(`Updated whitelist (${this.security.whitelist.size} commands)`, 'success');
return;
}
@@ -3979,15 +4062,15 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
if (action === 'add') { this.security.suspiciousFlags.add(flag); }
else if (action === 'remove') { this.security.suspiciousFlags.delete(flag); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_flags', JSON.stringify([...this.security.suspiciousFlags]));
+ this.security._saveSecurityDeltas();
this.print(`Updated suspicious flags (${this.security.suspiciousFlags.size} flags)`, 'success');
return;
}
if (cmd === 'cmd-allow') { this._resolveCmdConfirm(true); return; }
if (cmd === 'cmd-deny') { this._resolveCmdConfirm(false); return; }
if (cmd === 'vfs' && args[0] === 'reset') {
- if (args[1] !== '--force') {
- this.print('⚠ This will permanently delete all files! Use vfs reset --force to confirm.', 'warning');
+ if (args[1] !== '--confirm' && args[1] !== '--force') {
+ this.print('⚠ This will permanently delete all files! Use vfs reset --confirm to confirm.', 'warning');
return;
}
this.fs.reset();
@@ -4029,8 +4112,8 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
return;
}
if (cmd === 'empty-trash') {
- if (args[0] !== '--force') {
- this.print('⚠ This will permanently delete all trashed files! Use empty-trash --force to confirm.', 'warning');
+ if (args[0] !== '--confirm' && args[0] !== '--force') {
+ this.print('⚠ This will permanently delete all trashed files! Use empty-trash --confirm to confirm.', 'warning');
return;
}
if (this.fs.emptyTrash()) {
@@ -4147,44 +4230,14 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
} catch (_e) {}
}
return; }
- catch (err) { this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); return; }
- }
- }
- // Enhanced: check fuzzy suggestions before showing "not found"
- // Lazy-load: check if command belongs to an unloaded ext module
- if (!this._extModuleCommandMap) {
- this._extModuleCommandMap = {};
- this._extModuleUrlMap = {};
- try {
- const idx = JSON.parse(localStorage.getItem('webx11_packages_index') || '{}');
- if (idx.packages) {
- for (const [key, pkg] of Object.entries(idx.packages)) {
- if (pkg.commands && Array.isArray(pkg.commands)) {
- for (const c of pkg.commands) {
- this._extModuleCommandMap[c] = key;
- this._extModuleUrlMap[key] = pkg.url;
- }
- }
- }
- }
- } catch (e) {}
- }
- if (this._extModuleCommandMap[cmd] && !this.moduleManager.modules[this._extModuleCommandMap[cmd]]) {
- const key = this._extModuleCommandMap[cmd];
- const url = this._extModuleUrlMap[key];
- if (url) {
- this.print('⏳ Loading module for "' + cmd + '"...', 'info');
- const loaded = await this.moduleManager.loadModuleFromURL(url);
- if (loaded) {
- // Retry dispatch
- const mod = this.moduleManager.modules[key];
- if (mod && mod.commands && mod.commands[cmd]) {
- try { await mod.commands[cmd](args); return; }
- catch (err) { this.print('Error: ' + err.message, 'error'); return; }
- }
+ catch (err) {
+ this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error');
+ console.error(`[Terminal] Error in ${modName}.${cmd}:`, err);
+ return;
}
}
}
+ // Enhanced: check fuzzy suggestions before showing "not found"
const suggestion = this.security.suggest(cmd);
if (suggestion && suggestion.dist <= 2 && suggestion.dist > 0) {
this.print(`Command not found: ${cmd}`, 'error');
@@ -4218,7 +4271,17 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
files.forEach(f => {
const div = document.createElement('div');
div.className = `file-item ${f.type === 'dir' ? 'dir' : ''}`;
- div.innerHTML = `<span>${f.type === 'dir' ? '📁' : '📄'} ${f.name}</span> <span style="font-size:0.8em">${f.size}b</span>`;
+ const icon = document.createElement('span');
+ icon.textContent = f.type === 'dir' ? '📁' : '📄';
+ const name = document.createElement('span');
+ name.textContent = ' ' + f.name;
+ const size = document.createElement('span');
+ size.style.fontSize = '0.8em';
+ size.textContent = ' ' + f.size + 'b';
+ div.appendChild(icon);
+ div.appendChild(name);
+ div.appendChild(document.createTextNode(' '));
+ div.appendChild(size);
div.onclick = () => {
if (f.type === 'file') {
const content = this.fs.readFile(f.name);
@@ -4257,14 +4320,29 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
modules.forEach(mod => {
const div = document.createElement('div');
div.className = `module-item ${mod.loaded ? '' : 'disabled'}`;
- const badge = mod.url ? '' : '<span class="module-badge builtin">Built-in</span>';
- div.innerHTML = `
- <div>
- <strong>${mod.name}</strong>${badge}
- <div style="font-size:0.8em; color:#666">${mod.url || 'Built-in'}</div>
- </div>
- ${mod.url ? `<button class="btn btn-danger" onclick="webx11.removeModule('${mod.name}')" style="padding:4px 8px;">✕</button>` : ''}
- `;
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.textContent = mod.name;
+ info.appendChild(strong);
+ if (!mod.url) {
+ const badge = document.createElement('span');
+ badge.className = 'module-badge builtin';
+ badge.textContent = 'Built-in';
+ info.appendChild(badge);
+ }
+ const sub = document.createElement('div');
+ sub.style.cssText = 'font-size:0.8em; color:#666';
+ sub.textContent = mod.url || 'Built-in';
+ info.appendChild(sub);
+ div.appendChild(info);
+ if (mod.url) {
+ const btn = document.createElement('button');
+ btn.className = 'btn btn-danger';
+ btn.style.padding = '4px 8px';
+ btn.textContent = '✕';
+ btn.onclick = () => this.removeModule(mod.name);
+ div.appendChild(btn);
+ }
list.appendChild(div);
});
}
@@ -4376,6 +4454,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
renderConfigList() {
const list = document.getElementById('config-list');
if (!list) return;
+ list.innerHTML = '';
const prefix = 'webx11_config_';
const entries = [];
for (let i = 0; i < localStorage.length; i++) {
@@ -4388,14 +4467,28 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
list.innerHTML = '<div style="padding:20px; text-align:center; color:#666">No configuration saved.</div>';
return;
}
- list.innerHTML = entries.map(e => {
- const displayVal = e.key.toLowerCase().includes('password') || e.key.toLowerCase().includes('token') || e.key.toLowerCase().includes('secret')
- ? '****' : e.value;
- return `<div class="module-item">
- <div><strong>${e.key}</strong> <span style="color:var(--text-muted)">${displayVal}</span></div>
- <button class="btn btn-danger" onclick="webx11.deleteConfigFromUI('${e.key.replace(/'/g, "\\'")}')" style="padding:4px 8px; font-size:0.8em">✕</button>
- </div>`;
- }).join('');
+ entries.forEach(e => {
+ const item = document.createElement('div');
+ item.className = 'module-item';
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.textContent = e.key;
+ info.appendChild(strong);
+ const val = document.createElement('span');
+ val.style.color = 'var(--text-muted)';
+ const sensitive = /password|token|secret|key|private|credential|auth/i.test(e.key);
+ val.textContent = sensitive ? '****' : e.value;
+ info.appendChild(document.createTextNode(' '));
+ info.appendChild(val);
+ const btn = document.createElement('button');
+ btn.className = 'btn btn-danger';
+ btn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ btn.textContent = '✕';
+ btn.onclick = () => this.deleteConfigFromUI(e.key);
+ item.appendChild(info);
+ item.appendChild(btn);
+ list.appendChild(item);
+ });
}
addConfigFromUI() {
const key = document.getElementById('config-key-input').value.trim();
@@ -4448,10 +4541,24 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
// Stats
if (statsEl) {
const cats = new Set(Object.values(index).map(s => s.category || 'general'));
- statsEl.innerHTML = `<span>📚 ${names.length} skills</span><span>📂 ${cats.size} categories</span>${query ? `<span>🔍 ${filtered.length} match '${query}'</span>` : ''}`;
+ statsEl.innerHTML = '';
+ const totalSpan = document.createElement('span');
+ totalSpan.textContent = '📚 ' + names.length + ' skills';
+ statsEl.appendChild(totalSpan);
+ const catSpan = document.createElement('span');
+ catSpan.textContent = '📂 ' + cats.size + ' categories';
+ statsEl.appendChild(catSpan);
+ if (query) {
+ const matchSpan = document.createElement('span');
+ matchSpan.textContent = '🔍 ' + filtered.length + ' match';
+ statsEl.appendChild(matchSpan);
+ }
}
if (filtered.length === 0) {
- list.innerHTML = '<div style="padding:20px; text-align:center; color:#666">' + (query ? 'No skills match your search.' : 'No skills yet. Create one below!') + '</div>';
+ const empty = document.createElement('div');
+ empty.style.cssText = 'padding:20px; text-align:center; color:#666';
+ empty.textContent = query ? 'No skills match your search.' : 'No skills yet. Create one below!';
+ list.appendChild(empty);
return;
}
// Group by category
@@ -4477,18 +4584,41 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
const tags = (s.tags || []).join(', ');
const div = document.createElement('div');
div.className = 'module-item';
- div.innerHTML = `
- <div>
- <strong style="color:var(--success-color)">${name}</strong>
- <div style="font-size:0.85em; color:#aaa">${s.description || ''}</div>
- <div style="font-size:0.75em; color:#666">${tags ? '🏷 ' + tags : ''}</div>
- </div>
- <div style="display:flex;gap:4px">
- <button class="btn" onclick="webx11.viewSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">👁</button>
- <button class="btn" onclick="webx11.editSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">✏️</button>
- <button class="btn btn-danger" onclick="webx11.deleteSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">✕</button>
- </div>
- `;
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.style.color = 'var(--success-color)';
+ strong.textContent = name;
+ info.appendChild(strong);
+ const desc = document.createElement('div');
+ desc.style.cssText = 'font-size:0.85em; color:#aaa';
+ desc.textContent = s.description || '';
+ info.appendChild(desc);
+ const tagsDiv = document.createElement('div');
+ tagsDiv.style.cssText = 'font-size:0.75em; color:#666';
+ tagsDiv.textContent = tags ? '🏷 ' + tags : '';
+ info.appendChild(tagsDiv);
+ const actions = document.createElement('div');
+ actions.style.cssText = 'display:flex;gap:4px';
+ const viewBtn = document.createElement('button');
+ viewBtn.className = 'btn';
+ viewBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ viewBtn.textContent = '👁';
+ viewBtn.onclick = () => this.viewSkillUI(name);
+ const editBtn = document.createElement('button');
+ editBtn.className = 'btn';
+ editBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ editBtn.textContent = '✏️';
+ editBtn.onclick = () => this.editSkillUI(name);
+ const delBtn = document.createElement('button');
+ delBtn.className = 'btn btn-danger';
+ delBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ delBtn.textContent = '✕';
+ delBtn.onclick = () => this.deleteSkillUI(name);
+ actions.appendChild(viewBtn);
+ actions.appendChild(editBtn);
+ actions.appendChild(delBtn);
+ div.appendChild(info);
+ div.appendChild(actions);
body.appendChild(div);
}
section.appendChild(body);
@@ -4552,9 +4682,9 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
alert(`Skill '${name}' already exists. Delete it first or use skill-patch command.`);
return;
}
- let content = contentInput || `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n## Steps\n1. (to be filled)\n`;
+ let content = contentInput || `---\nname: ${this._escapeYamlString(name)}\ndescription: ${this._escapeYamlString(description)}\ntags: [${tags.map(t => this._escapeYamlString(t)).join(', ')}]\ncategory: ${this._escapeYamlString(category)}\n---\n## Steps\n1. (to be filled)\n`;
if (!content.startsWith('---')) {
- content = `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n${content}`;
+ content = `---\nname: ${this._escapeYamlString(name)}\ndescription: ${this._escapeYamlString(description)}\ntags: [${tags.map(t => this._escapeYamlString(t)).join(', ')}]\ncategory: ${this._escapeYamlString(category)}\n---\n${content}`;
}
fs.writeFile(`/skills/${name}.md`, content);
const index = fs.fsReadJSON('/skills/index.json') || {};
@@ -7093,11 +7223,14 @@ class OllamaModule {
const mod = this._getMod(name);
if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error');
const mm = this.terminal.moduleManager;
+ // Find the module's URL from registry
const regEntry = mm.moduleRegistry.find(m => m.name === name);
const url = regEntry ? regEntry.url : null;
if (!url) return this.terminal.print(`Module '${name}' has no URL — cannot reload (built-in)`, 'warning');
this.terminal.print(`🔄 Reloading '${name}' from ${url}...`, 'info');
+ // Unregister old instance
mm.unregisterModule(name);
+ // Re-fetch and re-register
const success = await mm.loadModuleFromURL(url);
if (success) {
this.terminal.print(`✅ Module '${name}' reloaded successfully`, 'success');
@@ -7768,13 +7901,31 @@ buildSystemPrompt() {
} catch (e) {}
let base = `Current date: ${dateStr} (${dayStr}) ${timeStr}${nvfsNote}\n`
+ 'VFS→Modules→AI. nvfs-index for commands. Tool→Observe→Answer. nvfs-resolve. Destructive cmds need confirm.\n'
- + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet | get_usage_stats(model,provider,since_hours) — LLM token/latency stats\n'
- + 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n'
- + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* — discoverable via help. '
- + 'JS: node <file.js> executes JS from VFS (alias for js-run), node -e "expr" for inline eval. No real Node.js runtime — browser JS only.';
+ + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind | wallet-lookup | get_usage_stats\\n'
+ + 'web_fetch: GET for pages, POST with method+body for JSON APIs. JS: node <file.js> or node -e "expr" (browser JS only).\n'
+ + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, world-model, predict, guardrail, tar, secret-* — via help.\n'
+ + 'predict_task: estimate duration before complex tasks. world_model_query: ask about system state, changes, health.';
if (profilePrompt) {
base = profilePrompt + '\n\n' + base;
}
+ // Apply agent loop profile prepend/append directives
+ var lp = this._activeLoopProfile;
+ if (lp && lp.systemPrompt) {
+ if (lp.systemPrompt.prepend) base = lp.systemPrompt.prepend + '\n\n' + base;
+ if (lp.systemPrompt.append) base = base + '\n\n' + lp.systemPrompt.append;
+ }
+ // Inject auto-fix suggestions from DebugSupervisorModule
+ try {
+ var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule;
+ if (ds && this._autoFixSuggestions && this._autoFixSuggestions.length > 0) {
+ base += '\n\n[Auto-Fix Suggestions]\n';
+ for (var fi = 0; fi < this._autoFixSuggestions.length; fi++) {
+ var s = this._autoFixSuggestions[fi];
+ base += '- ' + s.module + ': ' + s.suggestion + ' (' + s.reason + ')\n';
+ }
+ base += '[/Auto-Fix Suggestions]';
+ }
+ } catch (e) {}
return base;
}
getToolDefinitions() {
@@ -7784,13 +7935,13 @@ buildSystemPrompt() {
_filterToolsByProfile(allTools) {
var p = this._activeLoopProfile;
if (!p || !p.tools) return allTools;
- if (p.tools.mode === "all") {
+ if (p.tools.mode === 'all') {
if (p.tools.disabled && p.tools.disabled.length > 0) {
return allTools.filter(function(t) { return p.tools.disabled.indexOf(t.function.name) === -1; });
}
return allTools;
}
- if (p.tools.mode === "custom" && p.tools.enabled && p.tools.enabled.length > 0) {
+ if (p.tools.mode === 'custom' && p.tools.enabled && p.tools.enabled.length > 0) {
return allTools.filter(function(t) { return p.tools.enabled.indexOf(t.function.name) !== -1; });
}
return allTools;
@@ -7841,6 +7992,13 @@ buildSystemPrompt() {
}, {
type: 'function',
function: {
+ name: 'get_dashboard_data',
+ description: 'Full dashboard telemetry: modules, agents, memory, VFS, Ollama, token usage, tool latency, system health',
+ parameters: { type: 'object', properties: {}, required: [] }
+ }
+ }, {
+ type: 'function',
+ function: {
name: 'get_help',
description: 'Command reference',
parameters: { type: 'object', properties: {}, required: [] }
@@ -7907,15 +8065,18 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'edit_module',
- description: 'Edit module source (DESTRUCTIVE)',
- parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] }
+ description: 'Edit module source (DESTRUCTIVE). If code is provided, writes directly to VFS and auto-tests. If no code, opens the visual editor (requires manual Save).',
+ parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' }, code: { type: 'string', description: 'Full new source code. When provided, writes directly and auto-tests without opening the editor.' } }, required: ['name'] }
}
}, {
type: 'function',
function: {
name: 'test_module',
- description: 'Test edited module from /_mods/',
- parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] }
+ description: 'Test an edited module: loads it, checks JS errors, verifies commands registered, runs optional test assertions. Auto-rolls-back on failure.',
+ parameters: { type: 'object', properties: {
+ name: { type: 'string', description: 'Module class name' },
+ tests: { type: 'array', description: 'Optional test assertions: [{command, expect}]', items: { type: 'object', properties: { command: { type: 'string' }, expect: { type: 'string' } } } }
+ }, required: ['name'] }
}
}, {
type: 'function',
@@ -8116,7 +8277,7 @@ buildSystemPrompt() {
required: []
}
}
-}, {
+ }, {
type: 'function',
function: {
name: 'smart_context_set',
@@ -8146,32 +8307,8 @@ buildSystemPrompt() {
}, {
type: 'function',
function: {
- name: 'create_3d_scene',
- description: 'Create a 3D scene with Three.js objects. Supports: boxes, spheres, torus, text, particles, lines. Also supports presets: "solar" (solar system), "grid" (color grid). Describe what you want in natural language and the module will parse it.',
- parameters: {
- type: 'object',
- properties: {
- description: { type: 'string', description: 'Natural language description of the 3D scene (e.g. "a red box and two blue spheres with particles")' }
- },
- required: ['description']
- }
- }
- }, {
- type: 'function',
- function: {
- name: 'visualize_mindmap_3d',
- description: 'Render the current mindmap as an interactive 3D force-directed graph with colored spheres and curved connecting lines. Requires MindmapModule to have an active mindmap.',
- parameters: {
- type: 'object',
- properties: {},
- required: []
- }
- }
- }, {
- type: 'function',
- function: {
name: 'vaultmind_query',
- description: "Query VaultMind AI for Hyperliquid market data (prices, portfolio, transactions, copy-trade) or ask an AI agent. Uses same-origin /vmai/ API.",
+ description: "Query VaultMind AI for Hyperliquid market data (prices, portfolio, transactions, copy-trade) or ask an AI agent. Uses same-origin /vmai-api/ API.",
parameters: {
type: 'object',
properties: {
@@ -8234,8 +8371,132 @@ buildSystemPrompt() {
required: []
}
}
- }];
- }
+ }, {
+ type: 'function',
+ function: {
+ name: 'agent_loop_list',
+ description: 'List all available agent loop profiles with their settings and benchmark results.',
+ parameters: { type: 'object', properties: {}, required: [] }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'agent_loop_switch',
+ description: 'Switch to a named agent loop profile. Changes tools, max turns, system prompt directives, and loop behavior.',
+ parameters: {
+ type: 'object',
+ properties: { name: { type: 'string', description: 'Profile name to switch to' } },
+ required: ['name']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'agent_loop_create',
+ description: 'Create a new agent loop profile with custom settings (tools, max turns, temperature, system prompt directives).',
+ parameters: {
+ type: 'object',
+ properties: {
+ name: { type: 'string', description: 'Profile name (lowercase, hyphens)' },
+ description: { type: 'string', description: 'Profile description' },
+ maxTurns: { type: 'number', description: 'Max tool turns (default: 8)' },
+ toolsMode: { type: 'string', description: 'all or custom', enum: ['all', 'custom'] },
+ toolsEnabled: { type: 'array', items: { type: 'string' }, description: 'Tool names to enable (if mode=custom)' },
+ toolsDisabled: { type: 'array', items: { type: 'string' }, description: 'Tool names to disable (if mode=all)' },
+ prependPrompt: { type: 'string', description: 'Directive to prepend to system prompt' },
+ appendPrompt: { type: 'string', description: 'Directive to append to system prompt' }
+ },
+ required: ['name']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'agent_loop_benchmark',
+ description: 'Run the benchmark suite on a specified agent loop profile. Returns task results with latency, tokens, and pass/fail.',
+ parameters: {
+ type: 'object',
+ properties: {
+ profile: { type: 'string', description: 'Profile name to benchmark (default: active)' },
+ tasks: { type: 'array', items: { type: 'string' }, description: 'Specific task IDs to run (default: all)' }
+ },
+ required: []
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'predict_task',
+ description: 'Predict task duration, tool turns, and confidence based on historical execution data. Use before starting complex tasks to estimate resource needs.',
+ parameters: {
+ type: 'object',
+ properties: {
+ task: { type: 'string', description: 'Task description to predict' }
+ },
+ required: ['task']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'world_model_query',
+ description: 'Query the system world model for self-awareness: modules, skills, files, agents, health, memory graph, recent changes, active projects, telemetry. Ask questions like "what changed" or "how many modules".',
+ parameters: {
+ type: 'object',
+ properties: {
+ question: { type: 'string', description: 'What to ask: modules, skills, files, agents, health, memory, changes, projects, telemetry, summary' }
+ },
+ required: ['question']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'health_check',
+ description: 'Run a system health check after a module edit. Verifies: no JS errors, all core commands work, world model is valid, agents are alive.',
+ parameters: { type: 'object', properties: {} }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'diff_module',
+ description: 'Show a diff between the original module source and the edited version in _mods/. Returns line-by-line changes.',
+ parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'ship_module',
+ description: 'Full pipeline: edit → test → health_check → regression suite. Auto-rolls-back on any failure. Set commit:true to also persist to server (requires human approval). Without commit, runs as dry-run.',
+ parameters: { type: 'object', properties: {
+ name: { type: 'string', description: 'Module class name' },
+ code: { type: 'string', description: 'Full new source code' },
+ tests: { type: 'array', description: 'Test assertions [{command, expect}]', items: { type: 'object', properties: { command: { type: 'string' }, expect: { type: 'string' } } } },
+ commit: { type: 'boolean', description: 'If true, request server persistence (needs human approval). Default: false.' }
+ }, required: ['name', 'code'] }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'commit_module',
+ description: 'Request to persist an edited module from VFS _mods/ to the server file system via WebDAV. Creates a pending commit — user must type commit-approve in the terminal to execute. Includes bundle sync and version bump.',
+ parameters: { type: 'object', properties: {
+ name: { type: 'string', description: 'Module class name' },
+ file: { type: 'string', description: 'Target server path (e.g. modules/ext/shell.js). Auto-detect from module registry if omitted.' }
+ }, required: ['name'] }
+ }
+ }
+ ];
+ }
getHelpInfo() {
return {
'ai': 'ai <question> — Ask the AI assistant anything',
@@ -8258,6 +8519,8 @@ buildSystemPrompt() {
'reset-mem': 'reset-mem — Reset AI memory (logs, context, profile, guardrails) without losing VFS or config',
'vaultmind-connect': 'vaultmind-connect [testnet|mainnet] — Connect VaultMind wallet using hyperliquid_key from SecretVault',
'vaultmind-status': 'vaultmind-status — Check VaultMind session status',
+ 'vaultmind-query': 'vaultmind-query <prices|portfolio|transactions|copy-trade|chat> [message] — Query VaultMind API',
+ 'wallet-lookup': 'wallet-lookup <address> — Look up Hyperliquid wallet summary',
};
}
registerCommands() {
@@ -8273,6 +8536,21 @@ buildSystemPrompt() {
this.commands['ai-model'] = (args) => this.showModel(args);
this.commands['ai-compress'] = () => this._manualCompress();
this.commands['ai-config'] = (args) => this.aiConfig(args);
+ this.commands['commit-approve'] = (args) => this._executeCommit(args.join(' '));
+ this.commands['commit-deny'] = () => {
+ if (!this._pendingCommit) return this.terminal.print('No pending commit.', 'warning');
+ const name = this._pendingCommit.name;
+ this._pendingCommit = null;
+ this.terminal.print('❌ Commit denied for ' + name, 'warning');
+ };
+ this.commands['commit-status'] = () => {
+ if (!this._pendingCommit) return this.terminal.print('No pending commit.', 'info');
+ const pc = this._pendingCommit;
+ this.terminal.print('📋 Pending commit: ' + pc.name, 'info');
+ this.terminal.print(' Target: ' + pc.serverPath, 'info');
+ if (pc.bundlePath) this.terminal.print(' Bundle: ' + pc.bundlePath, 'info');
+ this.terminal.print(' Changes: ' + pc.changes + ' line(s)', 'info');
+ };
this.commands['agent-loops'] = (args) => this.agentLoopsCmd(args);
this.commands['bench'] = (args) => this.benchCmd(args);
this.commands['bench-compare'] = (args) => this.benchCompareCmd(args);
@@ -8283,8 +8561,6 @@ buildSystemPrompt() {
this.commands['debug-ai-log'] = () => this._showToolCallLog();
this.commands['archive-list'] = () => this._archiveList();
this.commands['archive-read'] = (args) => this._archiveRead(args);
- this.commands['workflow-list'] = () => this._workflowList();
- this.commands['workflow-run'] = (args) => this._workflowRun(args);
this.commands['guardrail'] = (args) => this.guardrailCmd(args);
this.commands['guardrail-add'] = (args) => this.guardrailCmd(['add', ...args]);
this.commands['guardrail-remove'] = (args) => this.guardrailCmd(['remove', ...args]);
@@ -8295,6 +8571,8 @@ buildSystemPrompt() {
this.commands['reset-mem'] = () => this.resetMem();
this.commands['vaultmind-connect'] = (args) => this._vaultmindConnectCmd(args);
this.commands['vaultmind-status'] = () => this._vaultmindStatusCmd();
+ this.commands['vaultmind-query'] = (args) => this._vaultmindQueryCmd(args);
+ this.commands['wallet-lookup'] = (args) => this._walletLookupCmd(args);
}
async chat(args) {
const userMessage = args.join(' ');
@@ -8314,6 +8592,18 @@ buildSystemPrompt() {
}
this._chatLock = true;
this.terminal.markDirty();
+ // Auto-route context based on intent
+ try {
+ const scm = this.terminal?.moduleManager?.modules?.SmartContextModule;
+ if (scm && typeof scm.autoRouteContext === 'function') {
+ const routeResult = scm.autoRouteContext(userMessage);
+ if (routeResult.switched) {
+ this.terminal.print('🔄 Context auto-switched: ' + routeResult.from + ' → ' + routeResult.to + ' (' + routeResult.confidence + '% confidence)', 'info');
+ // Rebuild system prompt after context switch
+ this.systemPrompt = this.buildSystemPrompt();
+ }
+ }
+ } catch (e) { console.warn('[AI] Auto-routing error:', e.message); }
// Re-read context from localStorage (auto-restore may have written it after init)
this._loadContext();
// Compress context if it exceeds the token threshold
@@ -8397,12 +8687,31 @@ buildSystemPrompt() {
if (loopResult.toolTurns >= 2 && !this._cancelled) {
await this.afterActionReflection(userMessage, loopResult.toolTurns);
}
+ // Feed result to predictor for learning (cold module activation)
+ if (loopResult && !this._cancelled && loopResult.toolTurns >= 1) {
+ try {
+ const predictor = this.terminal.moduleManager.modules['PredictorModule'];
+ if (predictor && typeof predictor.recordResult === 'function') {
+ const features = predictor._extractFeatures(userMessage);
+ const duration = (Date.now() - lockStart) / 1000;
+ predictor.recordResult(features, duration, loopResult.toolTurns, !this._cancelled);
+ }
+ } catch (e) { /* predictor learning is best-effort */ }
+ // Record task in memory graph for direct AI conversations (not just orchestrator)
+ try {
+ const mg = this.terminal.moduleManager.modules['MemoryGraphModule'];
+ if (mg && typeof mg.recordTask === 'function') {
+ mg.recordTask(userMessage, loopResult.finalAnswer || '', 'ai-assistant');
+ }
+ } catch (e) { /* memory graph is best-effort */ }
+ }
+ // Self-improvement: post-run anomaly detection
if (loopResult && !this._cancelled) {
try {
var om = this.terminal ? this.terminal.ollamaManager : null;
var usageData = om && om._usageLog && om._usageLog.length > 0 ? om._usageLog[om._usageLog.length - 1] : null;
this._postRunReview(loopResult, this._toolCallLog.slice(-(loopResult.toolTurns || 0)), usageData);
- } catch (e) {}
+ } catch (e) { /* self-improvement is best-effort */ }
}
// Auto-create skills from repeated errors
if (this._errorLog.length >= 3) {
@@ -8425,7 +8734,7 @@ buildSystemPrompt() {
// Log the AI interaction
this.terminal.log('INFO', 'AI', 'Query: ' + userMessage.substring(0, 100));
if (loopResult && loopResult.toolTurns) {
- var toolNames = this._toolCallLog.slice(-(loopResult.toolTurns || 0)).map(function(e) { return e.name; }).join(', ');
+ const toolNames = this._toolCallLog.slice(-(loopResult.toolTurns || 0)).map(function(e) { return e.name; }).join(', ');
this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s): ' + toolNames);
}
}
@@ -8449,6 +8758,280 @@ buildSystemPrompt() {
});
}
}
+ // ─── Self-Improvement Helpers ───
+ async _captureCommandOutput(cmd) {
+ const originalPrint = this.terminal.print.bind(this.terminal);
+ let output = '';
+ this.terminal.print = (text, type) => { output += text + '\n'; };
+ try { await this.terminal.security.checkAndRun(cmd); }
+ finally { this.terminal.print = originalPrint; }
+ return output.trim();
+ }
+ async _captureShellCommand(cmd) {
+ // Routes through executeCommand (shell parser) for >, |, &&, export, etc.
+ const originalPrint = this.terminal.print.bind(this.terminal);
+ let output = '';
+ this.terminal.print = (text, type) => { output += text + '\n'; };
+ try { await this.terminal.executeCommand(cmd); }
+ finally { this.terminal.print = originalPrint; }
+ return output.trim();
+ }
+ async _captureConsoleErrors(durationMs) {
+ durationMs = durationMs || 500;
+ var errors = [];
+ var origError = console.error;
+ var origOnError = window.onerror;
+ console.error = function() {
+ var args = Array.prototype.slice.call(arguments);
+ errors.push(args.join(' '));
+ };
+ window.onerror = function(msg, src, line, col, err) {
+ errors.push(msg + (err ? ' — ' + err.message : ''));
+ };
+ await new Promise(function(r) { setTimeout(r, durationMs); });
+ console.error = origError;
+ window.onerror = origOnError;
+ return errors;
+ }
+ _regressionTasks() {
+ return [
+ { id: 'shell-export', module: 'ShellModule', command: 'export REGTEST=ok', expect: 'REGTEST', useShell: true },
+ { id: 'shell-redirect', module: 'ShellModule', command: 'echo hi > _regtest.txt', expect: 'Redirected', useShell: true },
+ { id: 'shell-chain', module: 'ShellModule', command: 'echo a && echo b', expect: 'b', useShell: true },
+ { id: 'shell-pipe', module: 'ShellModule', command: 'echo hello | grep hello', expect: 'hello', useShell: true },
+ { id: 'fs-ls', module: 'SystemModule', command: 'ls', expect: '' },
+ { id: 'fs-pwd', module: 'SystemModule', command: 'pwd', expect: '/' },
+ { id: 'orch-agents', module: 'OrchestratorModule', command: 'list-agents', expect: 'agent' },
+ { id: 'ai-tools', module: 'WebX11AIAssistantModule', command: 'list_modules', expect: 'Module' },
+ { id: 'calc-basic', module: 'CalcModule', command: 'calc 1+1', expect: '2' },
+ { id: 'calc-advanced', module: 'CalcModule', command: 'calc 2*3', expect: '6' },
+ { id: 'webdav-ls', module: 'WebDAVModule', command: 'webdav-ls', expect: '' },
+ { id: 'cron-list', module: 'CronModule', command: 'cron-list', expect: '' },
+ { id: 'theme-list', module: 'ThemeModule', command: 'theme-list', expect: '' },
+ { id: 'mindmap-list', module: 'MindmapModule', command: 'mindmap-list', expect: '' },
+ { id: 'media-status', module: 'MediaModule', command: 'media-status', expect: '' },
+ { id: 'skill-list', module: 'SkillPackCoreModule', command: 'skill-list', expect: '' },
+ { id: 'crypto-encode', module: 'CryptoGzipModule', command: 'crypto-encode test', expect: '' },
+ { id: 'debug-toggle', module: 'DebugPanelModule', command: 'debug', expect: '' },
+ { id: 'voice-status', module: 'VoiceModule', command: 'voice-status', expect: '' },
+ { id: 'wpm-status', module: 'WPMModule', command: 'wpm-status', expect: '' },
+ { id: 'console-help', module: 'ConsoleModule', command: 'console-help', expect: '' },
+ { id: 'tutorial-status', module: 'TutorialModule', command: 'tutorial-status', expect: '' },
+ ];
+ }
+ async _runRegressionSuite(moduleName) {
+ const tests = this._regressionTasks().filter(t => t.module === moduleName);
+ if (tests.length === 0) return { module: moduleName, total: 0, passed: 0, failed: 0, results: [], allPassed: true, message: 'No regression tests for ' + moduleName };
+ const results = [];
+ for (const t of tests) {
+ try {
+ const out = t.useShell ? await this._captureShellCommand(t.command) : await this._captureCommandOutput(t.command);
+ const pass = t.expect ? out.includes(t.expect) : out.length > 0;
+ results.push({ id: t.id, command: t.command, pass, output: out.substring(0, 100) });
+ } catch (e) {
+ results.push({ id: t.id, command: t.command, pass: false, output: 'Error: ' + e.message });
+ }
+ }
+ const passed = results.filter(r => r.pass).length;
+ return { module: moduleName, total: results.length, passed, failed: results.length - passed, results, allPassed: passed === results.length };
+ }
+ // ─── Phase D: Server Persistence ───
+ async _webdavPush(remotePath, content) {
+ const webdav = this.terminal.moduleManager.modules['WebDAVModule'];
+ if (!webdav) throw new Error('WebDAV module not loaded');
+ // Use the webdav module's internal request method
+ const resp = await webdav._webdavRequest('PUT', remotePath, {
+ body: content,
+ headers: { 'Content-Type': 'text/plain' }
+ });
+ if (!resp.ok && resp.status !== 201 && resp.status !== 204) {
+ throw new Error('WebDAV PUT failed: HTTP ' + resp.status);
+ }
+ return true;
+ }
+ async _webdavGet(remotePath) {
+ const webdav = this.terminal.moduleManager.modules['WebDAVModule'];
+ if (!webdav) throw new Error('WebDAV module not loaded');
+ const resp = await webdav._webdavRequest('GET', remotePath);
+ if (!resp.ok) throw new Error('WebDAV GET failed: HTTP ' + resp.status);
+ return await resp.text();
+ }
+ _extractClassName(code) {
+ const m = code.match(/^class\s+(\w+)/m);
+ return m ? m[1] : null;
+ }
+ _extractClassBlock(code, className) {
+ // Extract the class definition from standalone code (without self-registration block)
+ const classStart = code.indexOf('class ' + className);
+ if (classStart === -1) return null;
+ // Find matching closing brace by counting
+ let depth = 0;
+ let end = classStart;
+ for (let i = classStart; i < code.length; i++) {
+ if (code[i] === '{') depth++;
+ else if (code[i] === '}') {
+ depth--;
+ if (depth === 0) { end = i + 1; break; }
+ }
+ }
+ return code.substring(classStart, end);
+ }
+ async _syncBundle(bundlePath, modName, newCode) {
+ try {
+ // Download current bundle
+ const bundle = await this._webdavGet(bundlePath);
+ // Find the class block in the bundle
+ const className = modName;
+ const classStart = bundle.indexOf('class ' + className);
+ if (classStart === -1) return { synced: false, reason: 'Class not found in bundle' };
+ // Find matching closing brace
+ let depth = 0;
+ let classEnd = classStart;
+ for (let i = classStart; i < bundle.length; i++) {
+ if (bundle[i] === '{') depth++;
+ else if (bundle[i] === '}') {
+ depth--;
+ if (depth === 0) { classEnd = i + 1; break; }
+ }
+ }
+ // Extract new class block from the edited code
+ const newClassBlock = this._extractClassBlock(newCode, className);
+ if (!newClassBlock) return { synced: false, reason: 'Could not extract class from new code' };
+ // Replace in bundle
+ const newBundle = bundle.substring(0, classStart) + newClassBlock + bundle.substring(classEnd);
+ // Validate with vm.Script equivalent (Function constructor)
+ try { new Function(newBundle); } catch (e) {
+ return { synced: false, reason: 'Bundle syntax error after replace: ' + e.message };
+ }
+ // Upload
+ await this._webdavPush(bundlePath, newBundle);
+ return { synced: true, bytes: newBundle.length };
+ } catch (e) {
+ return { synced: false, reason: e.message };
+ }
+ }
+ _parseVersion(ver) {
+ // Handle both old format (2026-07-06-v4.6.01) and new format (v4.6.01)
+ const m = ver.match(/v(\d+)\.(\d+)\.(\d+)/);
+ if (!m) return { major: 4, minor: 6, patch: 0 };
+ return { major: parseInt(m[1]), minor: parseInt(m[2]), patch: parseInt(m[3]) };
+ }
+ _bumpVersionString(ver) {
+ const v = this._parseVersion(ver);
+ return 'v' + v.major + '.' + v.minor + '.' + (v.patch + 1);
+ }
+ async _bumpVersion() {
+ // Read current version from the page
+ const currentVer = window.NEWOS4_VERSION || 'v4.6.01';
+ const newVer = this._bumpVersionString(currentVer);
+ // 1. Update main.html — replace all ?v= occurrences
+ const mainHtml = await this._webdavGet('main.html');
+ const updatedHtml = mainHtml.replace(/\?v=[^"']+/g, '?v=' + newVer);
+ await this._webdavPush('main.html', updatedHtml);
+ // 2. Update module-loader.js — replace LATEST_VER
+ const loader = await this._webdavGet('modules/core/module-loader.js');
+ const updatedLoader = loader.replace(/const LATEST_VER = '[^']+'/, "const LATEST_VER = '" + newVer + "'");
+ await this._webdavPush('modules/core/module-loader.js', updatedLoader);
+ return { from: currentVer, to: newVer };
+ }
+ _calculateConfidence(modName, testResult, healthResult, regResult, changeCount) {
+ var score = 100;
+ if (changeCount > 50) score -= 20;
+ if (changeCount > 100) score -= 20;
+ if (regResult.total === 0) score -= 30;
+ var critical = ['ShellModule', 'SystemModule', 'OrchestratorModule', 'WebX11AIAssistantModule'];
+ if (critical.indexOf(modName) !== -1) score -= 15;
+ if (testResult.tests && testResult.tests.length >= 3) score += 10;
+ if (healthResult.jsErrors && healthResult.jsErrors.length > 0) score -= 50;
+ return Math.max(0, Math.min(100, score));
+ }
+
+ async _executeCommit(comment) {
+ const pc = this._pendingCommit;
+ if (!pc) return this.terminal.print('No pending commit. Use commit_module first.', 'warning');
+ // Rate limit check
+ const today = new Date().toISOString().split('T')[0];
+ const commitLog = this.terminal.fs.fsReadJSON('/_mods/commit-log.json') || {};
+ if (commitLog[today] && commitLog[today] >= 5) {
+ return this.terminal.print('⚠️ Rate limit: 5 commits today. Try tomorrow.', 'warning');
+ }
+ // Cooldown check (10 min)
+ if (commitLog._lastCommitTime && Date.now() - commitLog._lastCommitTime < 600000) {
+ const wait = Math.ceil((600000 - (Date.now() - commitLog._lastCommitTime)) / 60000);
+ return this.terminal.print('⏱ Cooldown: ' + wait + ' min until next commit.', 'warning');
+ }
+ this.terminal.print('🚀 Executing commit for ' + pc.name + '...', 'info');
+ const results = { module: pc.name, steps: [] };
+ // Step 1: Push standalone file
+ try {
+ await this._webdavPush(pc.serverPath, pc.code);
+ results.steps.push({ step: 'standalone', status: 'ok', path: pc.serverPath });
+ this.terminal.print(' ✅ Standalone: ' + pc.serverPath, 'success');
+ } catch (e) {
+ results.steps.push({ step: 'standalone', status: 'fail', error: e.message });
+ this.terminal.print(' ❌ Standalone failed: ' + e.message, 'error');
+ return results;
+ }
+ // Step 1b: Post-commit syntax verification
+ try {
+ const verifyResp = await fetch(pc.serverPath + '?v=' + Date.now());
+ const committedCode = await verifyResp.text();
+ new Function(committedCode);
+ results.steps.push({ step: 'verify', status: 'ok' });
+ this.terminal.print(' ✅ Post-commit syntax check passed', 'success');
+ } catch (e) {
+ results.steps.push({ step: 'verify', status: 'fail', error: e.message });
+ this.terminal.print(' ❌ Post-commit syntax check failed: ' + e.message, 'error');
+ // Auto-revert
+ try {
+ await this._webdavPush(pc.serverPath, pc.origCode || '');
+ this.terminal.print(' ⏪ Auto-reverted to original code', 'warning');
+ results.reverted = true;
+ results.syntaxError = e.message;
+ } catch (revertErr) {
+ this.terminal.print(' ❌ Auto-revert FAILED: ' + revertErr.message, 'error');
+ results.revertFailed = true;
+ }
+ this._pendingCommit = null;
+ return results;
+ }
+ // Step 2: Sync bundle
+ if (pc.bundlePath) {
+ try {
+ const syncResult = await this._syncBundle(pc.bundlePath, pc.name, pc.code);
+ results.steps.push({ step: 'bundle', status: syncResult.synced ? 'ok' : 'skip', detail: syncResult });
+ if (syncResult.synced) this.terminal.print(' ✅ Bundle: ' + pc.bundlePath, 'success');
+ else this.terminal.print(' ⚠️ Bundle sync skipped: ' + syncResult.reason, 'warning');
+ } catch (e) {
+ results.steps.push({ step: 'bundle', status: 'fail', error: e.message });
+ this.terminal.print(' ❌ Bundle sync failed: ' + e.message, 'error');
+ }
+ }
+ // Step 3: Bump version
+ try {
+ const verResult = await this._bumpVersion();
+ results.steps.push({ step: 'version', status: 'ok', from: verResult.from, to: verResult.to });
+ this.terminal.print(' ✅ Version: ' + verResult.from + ' → ' + verResult.to, 'success');
+ results.newVersion = verResult.to;
+ } catch (e) {
+ results.steps.push({ step: 'version', status: 'fail', error: e.message });
+ this.terminal.print(' ⚠️ Version bump failed: ' + e.message, 'warning');
+ }
+ // Step 4: Cleanup _mods/
+ this.terminal.fs.deleteFile('_mods/' + pc.name + '.js');
+ this.terminal.fs.deleteFile('_mods/' + pc.name + '.orig.js');
+ this.terminal.fs.deleteFile('_mods/' + pc.name + '.meta.json');
+ // Step 5: Rate limit update
+ commitLog[today] = (commitLog[today] || 0) + 1;
+ commitLog._lastCommitTime = Date.now();
+ this.terminal.fs.fsWriteJSON('/_mods/commit-log.json', commitLog);
+ // Clear pending
+ this._pendingCommit = null;
+ this.terminal.print('🎉 Commit complete! Refresh to load ' + (results.newVersion || 'new version'), 'success');
+ if (comment) this.terminal.print(' Comment: ' + comment, 'info');
+ results.success = true;
+ return results;
+ }
_summarizeObservation(result, maxLen) {
if (!result || typeof result !== 'string' || result.length <= maxLen) return result || '';
const lines = result.split('\n');
@@ -8560,7 +9143,7 @@ buildSystemPrompt() {
var profile = this._activeLoopProfile || {};
var loopCfg = profile.loop || {};
var maxTurns = loopCfg.maxTurns || this.MAX_TOOL_TURNS;
- var loopDetect = loopCfg.loopDetection || { enabled: false, threshold: 6, windowMs: 30000 };
+ var loopDetect = loopCfg.loopDetection || { enabled: true, threshold: 6, windowMs: 30000 };
var confEsc = loopCfg.confidenceEscalation || { enabled: true, threshold: 90 };
var finalPrompt = loopCfg.finalTurnPrompt || 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.';
// Memory-augmented planning: inject prior loop lessons as negative constraints
@@ -8592,9 +9175,9 @@ buildSystemPrompt() {
if (this._cancelled) { this.terminal.print('AI loop cancelled', 'warning'); return { finalAnswer: null, toolTurns }; }
toolTurns++;
// On last turn, inject wrap-up prompt
- if (toolTurns === this.MAX_TOOL_TURNS) {
- this.terminal.print(`⏱ Final turn (${toolTurns}/${this.MAX_TOOL_TURNS}) — AI must provide final answer`, 'warning');
- messages.push({ role: 'system', content: 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.' });
+ if (toolTurns === maxTurns) {
+ this.terminal.print(`⏱ Final turn (${toolTurns}/${maxTurns}) — AI must provide final answer`, 'warning');
+ messages.push({ role: 'system', content: finalPrompt });
}
const thinkStart = Date.now();
const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions());
@@ -8613,7 +9196,7 @@ buildSystemPrompt() {
const vm = this.terminal.moduleManager && this.terminal.moduleManager.modules['VoiceModule'];
if (vm && vm.speakText) vm.speakText(text);
}
- this.terminal.print(`💬 AI answered (turn ${toolTurns}/${this.MAX_TOOL_TURNS}, ${thinkTime}s)`, 'info');
+ this.terminal.print(`💬 AI answered (turn ${toolTurns}/${maxTurns}, ${thinkTime}s)`, 'info');
return { finalAnswer: text, toolTurns };
}
if (res.tool_calls && res.tool_calls.length > 0) {
@@ -8631,7 +9214,7 @@ buildSystemPrompt() {
entry.count++;
entry.ts = now;
_seenCalls.set(sig, entry);
- if (loopDetect.enabled !== false && entry.count >= (loopDetect.threshold || 6)) {
+ if (entry.count >= (loopDetect.threshold || 6)) {
this.terminal.print(`🔄 Loop detected: ${tc.function.name} called ${entry.count}x with same args in 30s — breaking loop`, 'warning');
looping = true;
break;
@@ -8642,7 +9225,7 @@ buildSystemPrompt() {
messages.push({ role: 'system', content: 'You are repeating the same tool call with identical arguments. Stop calling tools and provide your final answer based on what you already know.' });
continue;
}
- this.terminal.print(`🔧 AI wants ${res.tool_calls.length} tool(s) — turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info');
+ this.terminal.print(`🔧 AI wants ${res.tool_calls.length} tool(s) — turn ${toolTurns}/${maxTurns} (${thinkTime}s)`, 'info');
messages.push(res.message);
let pendingText = res.message?.content || null;
const destructiveTools = res.tool_calls.filter(tc => this.isDestructive(tc.function.name, tc.function.arguments));
@@ -8702,15 +9285,15 @@ buildSystemPrompt() {
}
// Only block on destructive tools
if (destructiveTools.length > 0) {
- // 🎯 CONFIDENCE ESCALATION — auto-approve only if confidence >= 90%
+ // 🎯 CONFIDENCE ESCALATION — auto-approve only if confidence >= threshold (from profile)
let autoApprove = false;
- if (pendingText) {
+ if (confEsc.enabled !== false && pendingText) {
const confidence = this._parseConfidence(pendingText);
- if (confidence !== null && confidence >= 90) {
+ if (confidence !== null && confidence >= (confEsc.threshold || 90)) {
autoApprove = true;
- this.terminal.print(`🎯 AI confidence: ${confidence}% — auto-approved`, 'success');
+ this.terminal.print(`🎯 AI confidence: ${confidence}% — auto-approved (threshold: ${confEsc.threshold || 90}%)`, 'info');
} else if (confidence !== null) {
- this.terminal.print(`🎯 AI confidence: ${confidence}% — below 90% threshold, requiring manual approval`, 'warning');
+ this.terminal.print(`🎯 AI confidence: ${confidence}% — below ${confEsc.threshold || 90}% threshold, requiring manual approval`, 'warning');
} else {
this.terminal.print('🎯 No confidence declared for destructive action — requiring manual approval', 'warning');
}
@@ -8795,7 +9378,7 @@ buildSystemPrompt() {
}
// Loop exited without final answer — auto-summary
const toolNames = Object.keys(this._commandFreq).slice(-3).join(', ');
- this.terminal.print(`🤖 AI reached the ${toolTurns}/${this.MAX_TOOL_TURNS} tool limit.`, 'warning');
+ this.terminal.print(`🤖 AI reached the ${toolTurns}/${maxTurns} tool limit.`, 'warning');
this.terminal.print('Tools were executed but no final answer was produced — try a more specific query.', 'info');
return { finalAnswer: null, toolTurns };
}
@@ -9275,7 +9858,7 @@ buildSystemPrompt() {
}
try {
this.terminal.fs.root = JSON.parse(JSON.stringify(this._shadowSnapshot));
- this.terminal.fs.saveToLocalStorage();
+ this.terminal.fs.saveToStorage();
this._shadowSnapshot = null;
this.terminal.print('↩️ VFS rolled back to pre-shadow state', 'success');
} catch (e) {
@@ -9296,8 +9879,6 @@ buildSystemPrompt() {
'nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync',
'floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle',
'debug','debug-ai','session-new','mem-debug','reset-mem',
- // Node bridge — alias for js-run
- 'node',
// Synced from terminal whitelist — read-only orchestration/A2A/agent commands
'orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result',
'orchestrator-status','orchestrator-list','orchestrator-config','orchestrator-restart-policy',
@@ -9406,35 +9987,17 @@ buildSystemPrompt() {
const fs = this.terminal.fs;
const mm = this.terminal.moduleManager;
const om = this.terminal.ollamaManager;
- var agentInfo = {};
- try {
- var orch = mm.modules['OrchestratorModule'];
- var agentReg = mm.modules['AgentRegistryModule'];
- if (orch) {
- var agents = orch._agents || {};
- var busy = 0, failed = 0;
- for (var ak in agents) {
- var s = agents[ak].status;
- if (s === 'busy') busy++;
- if (s === 'failed' || s === 'restarting') failed++;
- }
- agentInfo.activeAgents = Object.keys(agents).length;
- agentInfo.busyAgents = busy;
- agentInfo.failedAgents = failed;
- }
- if (agentReg) {
- agentInfo.registeredAgents = Object.keys(agentReg._agents || {}).length;
- }
- } catch (e) {}
return JSON.stringify({
files: fs.ls().length,
dirs: fs.currentPath.length ? '/' + fs.currentPath.join('/') : '/',
modules: Object.keys(mm.modules).length,
ollama: om.connected ? `Connected (${om.currentModel})` : 'Disconnected',
- memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A',
- agents: agentInfo
+ memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A'
});
}
+ case 'get_dashboard_data': {
+ return JSON.stringify(await this._collectDashboardData());
+ }
case 'get_help': {
const cmds = this.terminal.moduleManager.getAllCommands();
return JSON.stringify(Object.keys(cmds).sort());
@@ -9648,8 +10211,12 @@ buildSystemPrompt() {
if (!task) return JSON.stringify({ error: 'No task provided' });
const orch = this.terminal.moduleManager.modules['OrchestratorModule'];
if (!orch) return JSON.stringify({ error: 'Orchestrator not available' });
- orch.delegateTask(task);
- return JSON.stringify({ message: `Task delegated: ${task.substring(0, 80)}` });
+ try {
+ const result = await orch.delegateTask(task);
+ return JSON.stringify({ success: true, task: task.substring(0, 80), result: typeof result === 'string' ? result.substring(0, 500) : 'completed' });
+ } catch (e) {
+ return JSON.stringify({ error: 'Delegation failed: ' + e.message });
+ }
}
case 'spawn_agent': {
const role = a.role || 'worker';
@@ -9841,26 +10408,6 @@ buildSystemPrompt() {
mindmap._svg([]);
return JSON.stringify({ success: true, topic, children: added.length ? added : 'none', nodes: mindmap._countNodes(cur.root), rendered: 'inline SVG in terminal' });
}
- case 'create_3d_scene': {
- const three = this.terminal.moduleManager.modules['ThreeModule'];
- if (!three) return JSON.stringify({ error: 'ThreeModule not loaded' });
- try {
- const result = await three.createSceneFromDescription(a.description || '');
- return JSON.stringify({ success: true, result });
- } catch (e) {
- return JSON.stringify({ error: '3D scene creation failed: ' + e.message });
- }
- }
- case 'visualize_mindmap_3d': {
- const three = this.terminal.moduleManager.modules['ThreeModule'];
- if (!three) return JSON.stringify({ error: 'ThreeModule not loaded' });
- try {
- const result = await three.visualizeMindmap3D(a.name || '');
- return JSON.stringify({ success: true, result });
- } catch (e) {
- return JSON.stringify({ error: '3D mindmap visualization failed: ' + e.message });
- }
- }
case 'view_module':
case 'edit_module':
case 'test_module':
@@ -9922,7 +10469,7 @@ buildSystemPrompt() {
return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + validEps.join(', ') });
}
try {
- const epMap = { prices: '/vmai/market/prices', portfolio: '/vmai/portfolio', transactions: '/vmai/transactions/summary', 'copy-trade': '/vmai/copy-trading/traders', chat: '/vmai/chat' };
+ const epMap = { prices: '/vmai-api/market/prices', portfolio: '/vmai-api/portfolio', transactions: '/vmai-api/transactions/summary', 'copy-trade': '/vmai-api/copy-trading/traders', chat: '/vmai-api/chat' };
let url = epMap[ep];
let opts = { method: 'GET', headers: { 'Accept': 'application/json' } };
// Portfolio, transactions, copy-trade need auth token
@@ -9969,12 +10516,87 @@ buildSystemPrompt() {
const stats = om.getUsageStats(filters);
return JSON.stringify({ success: true, stats });
}
+ case 'agent_loop_list': {
+ var profiles = this._loopProfiles;
+ var list = Object.keys(profiles).map(function(k) {
+ var p = profiles[k];
+ return { name: k, displayName: p.displayName, description: p.description, active: this._activeLoopProfile === p, maxTurns: p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS, toolsMode: p.tools ? p.tools.mode : 'all', benchmark: p.benchmark || null };
+ }.bind(this));
+ return JSON.stringify({ profiles: list, count: list.length, active: this._activeLoopProfile ? this._activeLoopProfile.name : 'none' });
+ }
+ case 'agent_loop_switch': {
+ var name = (a.name || '').trim();
+ if (!name) return JSON.stringify({ error: 'Profile name required' });
+ if (!this._loopProfiles[name]) return JSON.stringify({ error: "Profile '" + name + "' not found", available: Object.keys(this._loopProfiles) });
+ this._switchLoopProfile(name);
+ return JSON.stringify({ success: true, name: name, message: "Switched to '" + name + "'" });
+ }
+ case 'agent_loop_create': {
+ var name = (a.name || '').trim().replace(/\s+/g, '-').toLowerCase();
+ if (!name) return JSON.stringify({ error: 'Name required' });
+ var base = this._defaultLoopProfile(name, a.description || 'Custom profile');
+ if (a.maxTurns) base.loop.maxTurns = a.maxTurns;
+ if (a.toolsMode) base.tools.mode = a.toolsMode;
+ if (a.toolsEnabled) base.tools.enabled = a.toolsEnabled;
+ if (a.toolsDisabled) base.tools.disabled = a.toolsDisabled;
+ if (a.prependPrompt) base.systemPrompt.prepend = a.prependPrompt;
+ if (a.appendPrompt) base.systemPrompt.append = a.appendPrompt;
+ this._loopProfiles[name] = base;
+ this._saveLoopProfiles();
+ return JSON.stringify({ success: true, name: name, message: "Profile '" + name + "' created" });
+ }
+ case 'agent_loop_benchmark': {
+ var profileName = a.profile || (this._activeLoopProfile ? this._activeLoopProfile.name : 'default');
+ var taskIds = a.tasks;
+ this._runBenchmark(profileName, taskIds).then(function(results) {
+ this.terminal.print("📊 Benchmark complete for '" + profileName + "'", 'success');
+ }.bind(this)).catch(function(e) {
+ this.terminal.print('Benchmark error: ' + e.message, 'error');
+ }.bind(this));
+ return JSON.stringify({ success: true, message: "Benchmark started on '" + profileName + "'", tasks: taskIds || 'all' });
+ }
+ case 'predict_task': {
+ var predictor = this.terminal.moduleManager.modules['PredictorModule'];
+ if (!predictor) return JSON.stringify({ error: 'Predictor module not loaded' });
+ var taskDesc = a.task || '';
+ if (!taskDesc) return JSON.stringify({ error: 'task parameter required' });
+ var pFeatures = predictor._extractFeatures(taskDesc);
+ var pSimilar = predictor._findSimilar(pFeatures);
+ var pResult = predictor._computePrediction(pSimilar, pFeatures);
+ return JSON.stringify({
+ task: taskDesc.substring(0, 80),
+ estimatedDuration: pResult.duration + 's',
+ estimatedToolTurns: pResult.toolTurns,
+ confidence: Math.round(pResult.confidence * 100) + '%',
+ similarTasks: pResult.similarCount,
+ recommendation: pResult.confidence >= predictor._CONFIDENCE_THRESHOLD && pResult.duration > 30
+ ? 'Set TTL to ' + Math.ceil(pResult.duration * 1.5) + 's'
+ : 'No special action needed'
+ });
+ }
+ case 'world_model_query': {
+ var wm = this.terminal.moduleManager.modules['WorldModelModule'];
+ if (!wm) return JSON.stringify({ error: 'WorldModelModule not loaded' });
+ if (!wm._model) return JSON.stringify({ error: 'World model not synthesized yet — run world-model-refresh' });
+ var wmAnswer = wm._answerQuery(a.question || '');
+ return JSON.stringify({ question: a.question, answer: wmAnswer, version: wm._model.version, synthesized: wm._model.synthesized });
+ }
+ case 'health_check':
+ case 'diff_module':
+ case 'ship_module':
+ case 'commit_module':
+ return await this.runModuleTool(name, argsStr);
default:
return JSON.stringify({ error: `Unknown tool: ${name}` });
}
} catch (e) {
this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
if (this._errorLog.length > 20) this._errorLog.shift();
+ // Also feed into DebugSupervisorModule
+ try {
+ var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule;
+ if (ds) ds.recordToolError(name, a, e.message);
+ } catch (dsErr) {}
return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
@@ -10068,10 +10690,61 @@ buildSystemPrompt() {
}
case 'test_module': {
const modName = a.name || '';
+ const tests = a.tests || null;
const code = this.terminal.fs.readFile(`_mods/${modName}.js`);
if (!code) return JSON.stringify({ error: `No local edit for ${modName}` });
- this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`);
- return `Module "${modName}" loaded from local edit for testing`;
+ // Save current module state for rollback
+ const oldInstance = this.terminal.moduleManager.modules[modName];
+ const oldCommands = oldInstance ? Object.keys(oldInstance.commands) : [];
+ // Load new version
+ const ok = await this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`);
+ if (!ok) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'Module failed to load', rolledBack: true });
+ }
+ // Check for JS errors after load
+ const jsErrors = await this._captureConsoleErrors(300);
+ if (jsErrors.length > 0) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'JS errors after load', errors: jsErrors.slice(0, 5), rolledBack: true });
+ }
+ // Verify commands registered
+ const newInstance = this.terminal.moduleManager.modules[modName];
+ const newCommands = newInstance ? Object.keys(newInstance.commands) : [];
+ const lostCommands = oldCommands.filter(c => !newCommands.includes(c));
+ if (lostCommands.length > 0) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'Commands lost after edit', lost: lostCommands, rolledBack: true });
+ }
+ // Run test assertions if provided
+ if (tests && Array.isArray(tests) && tests.length > 0) {
+ const results = [];
+ for (const t of tests) {
+ const cmd = t.command || '';
+ // Use shell parser for commands with >, |, &&, ||, ;, export, set
+ const useShell = /[>|&;]/.test(cmd) || cmd.startsWith('export ') || cmd.startsWith('set ');
+ const output = useShell ? await this._captureShellCommand(cmd) : await this._captureCommandOutput(cmd);
+ const passed = t.expect ? output.includes(t.expect) : output.length > 0;
+ results.push({ command: cmd, expected: t.expect || '(any output)', actual: output.substring(0, 200), pass: passed });
+ }
+ const failed = results.filter(r => !r.pass).length;
+ if (failed > 0) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: failed + '/' + results.length + ' tests failed', results, rolledBack: true });
+ }
+ return JSON.stringify({ success: true, module: modName, commands: newCommands.length, tests: results });
+ }
+ // Smoke test: run first command if available
+ if (newCommands.length > 0) {
+ try {
+ const smokeOut = await this._captureCommandOutput(newCommands[0]);
+ return JSON.stringify({ success: true, module: modName, commands: newCommands.length, smokeTest: { command: newCommands[0], output: smokeOut.substring(0, 200) } });
+ } catch (e) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'Smoke test failed: ' + e.message, rolledBack: true });
+ }
+ }
+ return JSON.stringify({ success: true, module: modName, commands: newCommands.length });
}
case 'reload_module': {
const modName = a.name || '';
@@ -10135,11 +10808,181 @@ buildSystemPrompt() {
}
return JSON.stringify({ modules, total: modules.length, hasLocalEdits: hasMods });
}
+ case 'health_check': {
+ const results = {};
+ const coreCmds = ['ls', 'pwd', 'echo test', 'help', 'grep', 'find', 'tree', 'cat'];
+ results.coreCommands = {};
+ for (const cmd of coreCmds) {
+ try {
+ const out = await this._captureCommandOutput(cmd);
+ results.coreCommands[cmd] = (out.length > 0 || cmd === 'ls') ? 'ok' : 'empty output';
+ } catch (e) {
+ results.coreCommands[cmd] = 'FAIL: ' + e.message;
+ }
+ }
+ const mm = this.terminal.moduleManager;
+ results.moduleCount = Object.keys(mm.modules).length;
+ const wm = this.terminal.fs.fsReadJSON('system/world-model.json');
+ results.worldModelVersion = wm ? wm.version : 'not found';
+ results.versionMatch = wm && wm.version === (typeof window !== 'undefined' ? window.NEWOS4_VERSION : '');
+ const orch = mm.modules['OrchestratorModule'];
+ if (orch) {
+ const agents = Object.values(orch._agents);
+ results.agents = { total: agents.length, healthy: agents.filter(a => a.status !== 'failed').length, failed: agents.filter(a => a.status === 'failed').length };
+ }
+ results.jsErrors = await this._captureConsoleErrors(500);
+ const allCoreOk = Object.values(results.coreCommands).every(v => v === 'ok');
+ results.healthy = allCoreOk && results.versionMatch && results.jsErrors.length === 0;
+ return JSON.stringify(results);
+ }
+ case 'diff_module': {
+ const modName = a.name || '';
+ const newCode = this.terminal.fs.readFile(`_mods/${modName}.js`);
+ if (!newCode) return JSON.stringify({ error: `No edit for ${modName}` });
+ let origCode = this.terminal.fs.readFile(`_mods/${modName}.orig.js`);
+ if (!origCode || origCode === 'placeholder') {
+ const instance = this.terminal.moduleManager.modules[modName];
+ if (instance) origCode = instance.constructor.toString();
+ }
+ if (!origCode) return JSON.stringify({ error: 'No original source found' });
+ const origLines = origCode.split('\n');
+ const newLines = newCode.split('\n');
+ const changes = [];
+ const maxLines = Math.max(origLines.length, newLines.length);
+ for (let i = 0; i < maxLines; i++) {
+ if (origLines[i] !== newLines[i]) {
+ if (origLines[i] && !newLines[i]) changes.push({ line: i+1, type: 'removed', content: origLines[i].substring(0, 120) });
+ else if (!origLines[i] && newLines[i]) changes.push({ line: i+1, type: 'added', content: newLines[i].substring(0, 120) });
+ else changes.push({ line: i+1, type: 'changed', old: (origLines[i]||'').substring(0, 80), new: (newLines[i]||'').substring(0, 80) });
+ }
+ }
+ return JSON.stringify({ module: modName, originalLines: origLines.length, newLines: newLines.length, changes: changes.length, diff: changes.slice(0, 50) });
+ }
+ case 'ship_module': {
+ const modName = a.name || '';
+ const newCode = a.code || '';
+ const tests = a.tests || [];
+ const shouldCommit = a.commit === true;
+ if (!modName || !newCode) return JSON.stringify({ error: 'name and code required' });
+ // Step 1: Write to _mods/
+ this.terminal.fs.writeFile(`_mods/${modName}.js`, newCode);
+ this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ editedAt: new Date().toISOString(), editedBy: 'ship_module' }));
+ // Step 2: Test
+ const testResult = await this.runModuleTool('test_module', JSON.stringify({ name: modName, tests }));
+ const testParsed = JSON.parse(testResult);
+ if (testParsed.error) {
+ return JSON.stringify({ step: 'test', error: testParsed.error, rolledBack: testParsed.rolledBack, pipeline: 'aborted at test' });
+ }
+ // Step 3: Health check
+ const healthResult = await this.runModuleTool('health_check', '{}');
+ const healthParsed = JSON.parse(healthResult);
+ if (!healthParsed.healthy) {
+ await this.runModuleTool('reload_module', JSON.stringify({ name: modName }));
+ return JSON.stringify({ step: 'health_check', error: 'System unhealthy after edit', health: healthParsed, pipeline: 'aborted at health_check, rolled back' });
+ }
+ // Step 4: Regression suite
+ const regResult = await this._runRegressionSuite(modName);
+ if (!regResult.allPassed) {
+ await this.runModuleTool('reload_module', JSON.stringify({ name: modName }));
+ return JSON.stringify({ step: 'regression', error: 'Regression tests failed', regression: regResult, pipeline: 'aborted at regression, rolled back' });
+ }
+ // Step 5: Calculate confidence score
+ const changeCount = newCode.split('\n').length;
+ const confidence = this._calculateConfidence(modName, testParsed, healthParsed, regResult, changeCount);
+ // Step 6: Commit (if requested)
+ if (shouldCommit) {
+ if (confidence >= 70) {
+ // Auto-commit: execute directly
+ const commitResult = await this.runModuleTool('commit_module', JSON.stringify({ name: modName }));
+ const commitParsed = JSON.parse(commitResult);
+ if (commitParsed.pendingCommit) {
+ // Execute the pending commit immediately
+ const execResult = await this._executeCommit('auto: confidence=' + confidence);
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → AUTO-COMMIT (confidence=' + confidence + ')', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, autoCommitted: true, commitResult: execResult });
+ }
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → auto-commit', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, commit: commitParsed });
+ } else {
+ // Confidence too low — human approval required
+ const commitResult = await this.runModuleTool('commit_module', JSON.stringify({ name: modName }));
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → HUMAN APPROVAL REQUIRED (confidence=' + confidence + ' < 70)', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, commit: JSON.parse(commitResult), humanApprovalRequired: true });
+ }
+ }
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression (dry-run)', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, next: 'Call commit_module or ship_module with commit:true to persist' });
+ }
+ case 'commit_module': {
+ const modName = a.name || '';
+ const targetFile = a.file || '';
+ const newCode = this.terminal.fs.readFile(`_mods/${modName}.js`);
+ if (!newCode) return JSON.stringify({ error: `No local edit for ${modName}. Run edit_module or ship_module first.` });
+
+ // Auto-detect target file from module registry
+ let serverPath = targetFile;
+ if (!serverPath) {
+ const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName);
+ if (entry && entry.url) {
+ serverPath = entry.url.replace(/^https?:\/\/[^/]+/, '').replace(/\?v=.*/, '');
+ }
+ }
+ if (!serverPath) return JSON.stringify({ error: 'Cannot determine server path. Provide file= parameter (e.g. modules/ext/shell.js).' });
+
+ // Get diff
+ let origCode = this.terminal.fs.readFile(`_mods/${modName}.orig.js`);
+ if (!origCode || origCode === 'placeholder') {
+ const instance = this.terminal.moduleManager.modules[modName];
+ if (instance) origCode = instance.constructor.toString();
+ }
+ const origLines = origCode ? origCode.split('\n') : [];
+ const newLines = newCode.split('\n');
+ let changeCount = 0;
+ for (let i = 0; i < Math.max(origLines.length, newLines.length); i++) {
+ if (origLines[i] !== newLines[i]) changeCount++;
+ }
+
+ // Detect bundle path
+ let bundlePath = null;
+ if (serverPath.includes('/modules/core/') || serverPath.includes('module-aiassist')) {
+ bundlePath = 'modules/core/core-bundle.js';
+ } else if (serverPath.includes('/modules/ext/')) {
+ bundlePath = 'modules/ext/critical-ext-bundle.js';
+ }
+
+ // Store pending commit
+ this._pendingCommit = {
+ name: modName,
+ code: newCode,
+ origCode: origCode || '',
+ serverPath: serverPath,
+ bundlePath: bundlePath,
+ changes: changeCount,
+ createdAt: Date.now()
+ };
+
+ this.terminal.print('📋 Commit request created for ' + modName, 'info');
+ this.terminal.print(' Target: ' + serverPath, 'info');
+ if (bundlePath) this.terminal.print(' Bundle: ' + bundlePath, 'info');
+ this.terminal.print(' Changes: ' + changeCount + ' line(s) different', 'info');
+ this.terminal.print(' Type \'commit-approve\' to persist or \'commit-deny\' to cancel', 'warning');
+
+ return JSON.stringify({
+ pendingCommit: true,
+ module: modName,
+ serverPath: serverPath,
+ bundlePath: bundlePath,
+ changes: changeCount,
+ newLines: newLines.length,
+ message: 'Commit request created. User must type commit-approve to execute.'
+ });
+ }
default: return JSON.stringify({ error: `Unknown module tool: ${name}` });
}
} catch (e) {
this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
if (this._errorLog.length > 20) this._errorLog.shift();
+ // Also feed into DebugSupervisorModule
+ try {
+ var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule;
+ if (ds) ds.recordToolError(name, a, e.message);
+ } catch (dsErr) {}
return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
@@ -10183,8 +11026,8 @@ buildSystemPrompt() {
setSystemPrompt(args) {
if (!args[0]) {
this.terminal.print('=== System Prompt ===', 'info');
- this.systemPrompt.split('\n').forEach(line => this.terminal.print(line, 'text-muted'));
- this.terminal.print(`=== End (${this.systemPrompt.length} chars) ===`, 'info');
+ this.systemPrompt.split('\n').forEach(function(line) { this.terminal.print(line, 'text-muted'); }.bind(this));
+ this.terminal.print('=== End (' + this.systemPrompt.length + ' chars) ===', 'info');
return;
}
var arg = args.join(' ');
@@ -10337,8 +11180,6 @@ buildSystemPrompt() {
this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (default: 120s)`, '');
this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, '');
this.terminal.print(`skill-debounce: ${this._skillDebounceMs / 60000}min (default: 30min)`, '');
- var lp = this._activeLoopProfile;
- this.terminal.print('loop-detection: ' + (lp && lp.loop && lp.loop.loopDetection ? (lp.loop.loopDetection.enabled ? 'on' : 'off') : 'off (default)') + ' (profile: ' + (lp ? lp.name : 'none') + ')', '');
const om = this.terminal?.ollamaManager;
if (om && om.modelParams) {
this.terminal.print(`temperature: ${om.modelParams.temperature} (default: 0.7)`, '');
@@ -10347,7 +11188,7 @@ buildSystemPrompt() {
this.terminal.print(`thinking: ${om.modelParams.thinking} (default: off)`, '');
}
this.terminal.print('Usage: ai-config <key> <value>', 'info');
- this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking, loop-detection', 'info');
+ this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking', 'info');
return;
}
const key = args[0];
@@ -10400,17 +11241,8 @@ buildSystemPrompt() {
if (!validModes.includes(val)) return this.terminal.print(`thinking must be one of: ${validModes.join(', ')}`, 'warning');
const om = this.terminal?.ollamaManager;
if (om && om.modelParams) { om.modelParams.thinking = val; om._saveModelParams(); }
- } else if (key === 'loop-detection') {
- var lp = this._activeLoopProfile;
- if (!lp) { this.terminal.print('No active profile', 'error'); return; }
- if (!lp.loop) lp.loop = {};
- if (!lp.loop.loopDetection) lp.loop.loopDetection = { enabled: false, threshold: 6, windowMs: 30000 };
- lp.loop.loopDetection.enabled = val !== 'off';
- this._saveLoopProfile(lp);
- this.terminal.print('Loop detection ' + (lp.loop.loopDetection.enabled ? 'enabled' : 'disabled') + ' (profile: ' + lp.name + ')', 'success');
- return;
} else {
- return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking, loop-detection`, 'warning');
+ return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking`, 'warning');
}
// Persist
localStorage.setItem('webx11_ai_config', JSON.stringify({
@@ -10485,6 +11317,19 @@ buildSystemPrompt() {
}
try { localStorage.setItem('webx11_ai_patterns', JSON.stringify(this._commandFreq)); } catch (e) {}
}
+ _buildToolToModuleMap() {
+ this._toolToModuleMap = {};
+ const mm = this.terminal.moduleManager;
+ if (!mm || !mm.modules) return;
+ for (const [modName, mod] of Object.entries(mm.modules)) {
+ if (mod.commands) {
+ for (const cmdName of Object.keys(mod.commands)) {
+ this._toolToModuleMap[cmdName] = modName;
+ }
+ }
+ }
+ }
+
async _autoCreateErrorSkill() {
const grouped = {};
for (const e of this._errorLog) {
@@ -10597,76 +11442,6 @@ buildSystemPrompt() {
this.terminal.print('Error reading archive: ' + e.message, 'error');
}
}
- _workflowList() {
- var workflows = [
- { name: 'daily-health-report', desc: 'Schedule a daily system health report using Cron + WorldModel + ServerMonitor' },
- { name: 'predict-and-monitor', desc: 'Predict task duration with Predictor, then schedule monitoring via Cron' },
- { name: 'compress-archives', desc: 'Compress and archive old session files using CryptoGzip + Cron' },
- { name: 'media-summarize', desc: 'Analyze a media file and summarize it with AI' },
- { name: 'sandbox-test', desc: 'Run a command in the sandbox and report results' }
- ];
- var out = '── Workflows (' + workflows.length + ') ──\n';
- for (var i = 0; i < workflows.length; i++) {
- out += ' ' + workflows[i].name + ' — ' + workflows[i].desc + '\n';
- }
- out += '── End ──\nUse: workflow-run <name>';
- this.terminal.print(out, 'info');
- }
- _workflowRun(args) {
- var name = (args && args[0]) || '';
- if (!name) {
- this.terminal.print('Usage: workflow-run <name> (e.g. daily-health-report)', 'warning');
- return;
- }
- var workflows = {
- 'daily-health-report': {
- steps: [
- 'Run: status — check current system state',
- 'Run: cron-every 1h "status" — schedule hourly health check',
- 'The WorldModel will auto-synthesize system awareness each cycle'
- ]
- },
- 'predict-and-monitor': {
- steps: [
- 'Run: predict "run system health check" — estimate duration',
- 'Run: cron-every 30m "status" — monitor progress',
- 'Check cron-list to see scheduled tasks'
- ]
- },
- 'compress-archives': {
- steps: [
- 'Run: archive-list — see current archives',
- 'Run: gzip /memory/archive/2026-06-25-*.json — compress old archives',
- 'Run: cron-every 1d "gzip /memory/archive/*.json" — auto-compress daily'
- ]
- },
- 'media-summarize': {
- steps: [
- 'Run: media list — see available media files',
- 'Run: media analyze <file> — analyze with AI',
- 'The AI will summarize the media content'
- ]
- },
- 'sandbox-test': {
- steps: [
- 'Run: sandbox-status — check sandbox pool',
- 'Run: sandbox-run "ls /" — test command in sandbox',
- 'Check output for isolation verification'
- ]
- }
- };
- var wf = workflows[name];
- if (!wf) {
- this.terminal.print('Workflow not found: ' + name + '. Use workflow-list to see available workflows.', 'error');
- return;
- }
- var out = '── Workflow: ' + name + ' ──\n';
- for (var i = 0; i < wf.steps.length; i++) {
- out += ' ' + (i + 1) + '. ' + wf.steps[i] + '\n';
- }
- out += '── End ──';
- this.terminal.print(out, 'info');
- }
_showLastRawResponse() {
if (!this._lastRawResponse) {
this.terminal.print('No AI response recorded yet. Send a message first.', 'warning');
@@ -10826,105 +11601,576 @@ buildSystemPrompt() {
this.terminal.print('Error: ' + e.message, 'error');
}
}
+ _vaultmindStatusCmd() {
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) {
+ this.terminal.print('✅ VaultMind session active (token: ' + token.substring(0, 12) + '...)', 'success');
+ } else {
+ this.terminal.print('❌ No VaultMind session. Use: vaultmind-connect [testnet|mainnet]', 'warning');
+ }
+ }
+ async _vaultmindQueryCmd(args) {
+ const ep = args[0];
+ const validEps = ['prices', 'portfolio', 'transactions', 'copy-trade', 'chat'];
+ if (!validEps.includes(ep)) {
+ this.terminal.print('Usage: vaultmind-query <prices|portfolio|transactions|copy-trade|chat> [message|agent]', 'warning'); return;
+ }
+ try {
+ const epMap = { prices: '/vmai-api/market/prices', portfolio: '/vmai-api/portfolio', transactions: '/vmai-api/transactions/summary', 'copy-trade': '/vmai-api/copy-trading/traders', chat: '/vmai-api/chat' };
+ let url = epMap[ep];
+ let opts = { method: 'GET', headers: { 'Accept': 'application/json' } };
+ const authEps = ['portfolio', 'transactions', 'copy-trade'];
+ if (authEps.includes(ep)) {
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) opts.headers['Authorization'] = 'Bearer ' + token;
+ }
+ if (ep === 'chat') {
+ opts.method = 'POST';
+ opts.headers['Content-Type'] = 'application/json';
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) opts.headers['Authorization'] = 'Bearer ' + token;
+ opts.body = JSON.stringify({
+ message: args.slice(1).join(' ') || 'What is the current market status?',
+ agent: 'market'
+ });
+ }
+ const resp = await fetch(url, opts);
+ const data = await resp.json();
+ this.terminal.print(JSON.stringify(data, null, 2), 'success');
+ } catch (e) {
+ this.terminal.print('VaultMind query error: ' + e.message, 'error');
+ }
+ }
+ async _walletLookupCmd(args) {
+ const addr = args[0];
+ if (!addr) { this.terminal.print('Usage: wallet-lookup <address>', 'warning'); return; }
+ try {
+ const resp = await fetch('/wallet/' + encodeURIComponent(addr));
+ const data = await resp.json();
+ this.terminal.print(JSON.stringify(data, null, 2), 'success');
+ } catch (e) {
+ this.terminal.print('Wallet lookup error: ' + e.message, 'error');
+ }
+ }
// ── Agent Loop Profile System ──
_defaultLoopProfile(name, desc) {
- return { name: name, displayName: desc, description: desc, version: 1, created: new Date().toISOString(), updated: new Date().toISOString(), systemPrompt: { base: null, prepend: "", append: "", useDefault: true }, loop: { strategy: "standard", maxTurns: this.MAX_TOOL_TURNS, loopDetection: { enabled: false, threshold: 6, windowMs: 30000 }, confidenceEscalation: { enabled: true, threshold: 90 }, finalTurnPrompt: "This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know." }, tools: { mode: "all", enabled: [], disabled: [] }, model: { temperature: null, topP: null, maxTokens: null, thinking: null }, provider: { name: null, model: null }, benchmark: { lastRun: null, avgLatency: null, avgTokens: null, taskCompletionRate: null, tasks: [] } };
+ return {
+ name: name, displayName: desc, description: desc,
+ version: 1, created: new Date().toISOString(), updated: new Date().toISOString(),
+ systemPrompt: { base: null, prepend: '', append: '', useDefault: true },
+ loop: {
+ strategy: 'standard', maxTurns: this.MAX_TOOL_TURNS,
+ loopDetection: { enabled: true, threshold: 6, windowMs: 30000 },
+ confidenceEscalation: { enabled: true, threshold: 90 },
+ finalTurnPrompt: 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.'
+ },
+ tools: { mode: 'all', enabled: [], disabled: [] },
+ model: { temperature: null, topP: null, maxTokens: null, thinking: null },
+ provider: { name: null, model: null },
+ benchmark: { lastRun: null, avgLatency: null, avgTokens: null, taskCompletionRate: null, tasks: [] }
+ };
}
_initLoopProfiles() {
- try { var saved = localStorage.getItem("webx11_agent_loops"); if (saved) { this._loopProfiles = JSON.parse(saved); } } catch (e) {}
+ try {
+ var saved = localStorage.getItem('webx11_agent_loops');
+ if (saved) { this._loopProfiles = JSON.parse(saved); }
+ } catch (e) {}
if (!this._loopProfiles || Object.keys(this._loopProfiles).length === 0) {
this._loopProfiles = {};
- this._loopProfiles["default"] = this._defaultLoopProfile("default", "Balanced loop for general tasks");
- var p = this._defaultLoopProfile("minimal", "Fast, low-token for simple tasks"); p.loop.maxTurns = 4; p.tools = { mode: "custom", enabled: ["list_files","read_file","write_file","nvfs_resolve"], disabled: [] }; p.model.temperature = 0.3; this._loopProfiles["minimal"] = p;
- p = this._defaultLoopProfile("research", "Read-only exploration, web + files"); p.loop.maxTurns = 6; p.tools = { mode: "custom", enabled: ["web_search","web_fetch","read_file","list_files","nvfs_resolve","view_module","list_modules"], disabled: [] }; p.model.temperature = 0.5; this._loopProfiles["research"] = p;
- p = this._defaultLoopProfile("safe", "No destructive tools"); p.tools = { mode: "all", enabled: [], disabled: ["delete_file","edit_module","run_command","make_directory"] }; p.model.temperature = 0.5; this._loopProfiles["safe"] = p;
- p = this._defaultLoopProfile("creative", "More turns, higher temp"); p.loop.maxTurns = 12; p.model.temperature = 0.9; this._loopProfiles["creative"] = p;
- p = this._defaultLoopProfile("coder", "Code-focused, low temp"); p.loop.maxTurns = 10; p.model.temperature = 0.2; p.systemPrompt.append = "You are a code expert. Always show the exact code changes. Use edit_module for modifications."; this._loopProfiles["coder"] = p;
+ this._loopProfiles['default'] = this._defaultLoopProfile('default', 'Balanced loop for general tasks');
+ var p = this._defaultLoopProfile('minimal', 'Fast, low-token for simple tasks');
+ p.loop.maxTurns = 4; p.tools = { mode: 'custom', enabled: ['list_files','read_file','write_file','nvfs_resolve'], disabled: [] }; p.model.temperature = 0.3;
+ this._loopProfiles['minimal'] = p;
+ p = this._defaultLoopProfile('research', 'Read-only exploration, web + files');
+ p.loop.maxTurns = 6; p.tools = { mode: 'custom', enabled: ['web_search','web_fetch','read_file','list_files','nvfs_resolve','view_module','list_modules'], disabled: [] }; p.model.temperature = 0.5;
+ this._loopProfiles['research'] = p;
+ p = this._defaultLoopProfile('safe', 'No destructive tools');
+ p.tools = { mode: 'all', enabled: [], disabled: ['delete_file','edit_module','run_command','make_directory'] }; p.model.temperature = 0.5;
+ this._loopProfiles['safe'] = p;
+ p = this._defaultLoopProfile('creative', 'More turns, higher temp');
+ p.loop.maxTurns = 12; p.model.temperature = 0.9;
+ this._loopProfiles['creative'] = p;
+ p = this._defaultLoopProfile('coder', 'Code-focused, low temp');
+ p.loop.maxTurns = 10; p.model.temperature = 0.2; p.systemPrompt.append = 'You are a code expert. Always show the exact code changes. Use edit_module for modifications.';
+ this._loopProfiles['coder'] = p;
this._saveLoopProfiles();
}
- try { var active = localStorage.getItem("webx11_active_loop_profile"); if (active && this._loopProfiles[active]) { this._activeLoopProfile = this._loopProfiles[active]; } else { this._activeLoopProfile = this._loopProfiles["default"]; } } catch (e) { this._activeLoopProfile = this._loopProfiles["default"]; }
+ try {
+ var active = localStorage.getItem('webx11_active_loop_profile');
+ if (active && this._loopProfiles[active]) { this._activeLoopProfile = this._loopProfiles[active]; }
+ else { this._activeLoopProfile = this._loopProfiles['default']; }
+ } catch (e) { this._activeLoopProfile = this._loopProfiles['default']; }
+ }
+ _saveLoopProfiles() {
+ try { localStorage.setItem('webx11_agent_loops', JSON.stringify(this._loopProfiles)); } catch (e) {}
+ }
+ _saveLoopProfile(p) {
+ if (p && p.name) { p.updated = new Date().toISOString(); this._loopProfiles[p.name] = p; this._saveLoopProfiles(); }
}
- _saveLoopProfiles() { try { localStorage.setItem("webx11_agent_loops", JSON.stringify(this._loopProfiles)); } catch (e) {} }
- _saveLoopProfile(p) { if (p && p.name) { p.updated = new Date().toISOString(); this._loopProfiles[p.name] = p; this._saveLoopProfiles(); } }
_switchLoopProfile(name) {
if (!this._loopProfiles[name]) return false;
this._activeLoopProfile = this._loopProfiles[name];
- localStorage.setItem("webx11_active_loop_profile", name);
+ localStorage.setItem('webx11_active_loop_profile', name);
this._toolDefs = null;
this.systemPrompt = this.buildSystemPrompt();
var mp = this._activeLoopProfile.model;
var om = this.terminal ? this.terminal.ollamaManager : null;
- if (om && mp) { if (mp.temperature !== null) { om.modelParams.temperature = mp.temperature; om._saveModelParams(); } if (mp.topP !== null) { om.modelParams.top_p = mp.topP; om._saveModelParams(); } if (mp.maxTokens !== null) { om.modelParams.max_tokens = mp.maxTokens; om._saveModelParams(); } if (mp.thinking !== null) { om.modelParams.thinking = mp.thinking; om._saveModelParams(); } }
+ if (om && mp) {
+ if (mp.temperature !== null) { om.modelParams.temperature = mp.temperature; om._saveModelParams(); }
+ if (mp.topP !== null) { om.modelParams.top_p = mp.topP; om._saveModelParams(); }
+ if (mp.maxTokens !== null) { om.modelParams.max_tokens = mp.maxTokens; om._saveModelParams(); }
+ if (mp.thinking !== null) { om.modelParams.thinking = mp.thinking; om._saveModelParams(); }
+ }
return true;
}
agentLoopsCmd(args) {
if (!args[0]) {
- this.terminal.print("=== Agent Loop Profiles ===", "info");
+ this.terminal.print('=== Agent Loop Profiles ===', 'info');
var keys = Object.keys(this._loopProfiles);
- for (var ki = 0; ki < keys.length; ki++) { var k = keys[ki]; var p = this._loopProfiles[k]; var active = this._activeLoopProfile === p ? " ✅" : ""; var bench = p.benchmark && p.benchmark.taskCompletionRate != null ? " (bench: " + Math.round(p.benchmark.taskCompletionRate * 100) + "%)" : ""; var tools = p.tools && p.tools.mode === "custom" ? (p.tools.enabled ? p.tools.enabled.length : 0) + " tools" : "all tools"; this.terminal.print(" " + k + ": " + p.displayName + active + " — " + (p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS) + " turns, " + tools + bench); }
- this.terminal.print("Usage: agent-loops <name> | new <name> <desc> | edit <name> <key> <val> | delete <name> | clone <src> <dst> | export <name> | reset <name>", "text-muted"); return;
+ for (var ki = 0; ki < keys.length; ki++) {
+ var k = keys[ki];
+ var p = this._loopProfiles[k];
+ var active = this._activeLoopProfile === p ? ' ✅' : '';
+ var bench = p.benchmark && p.benchmark.taskCompletionRate != null ? ' (bench: ' + Math.round(p.benchmark.taskCompletionRate * 100) + '%)' : '';
+ var tools = p.tools && p.tools.mode === 'custom' ? (p.tools.enabled ? p.tools.enabled.length : 0) + ' tools' : 'all tools';
+ this.terminal.print(' ' + k + ': ' + p.displayName + active + ' — ' + (p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS) + ' turns, ' + tools + bench);
+ }
+ this.terminal.print('Usage: agent-loops <name> | new <name> <desc> | edit <name> <key> <val> | delete <name> | clone <src> <dst> | export <name> | reset <name>', 'text-muted');
+ return;
}
var sub = args[0];
- if (sub === "new") { var name = args[1] ? args[1].replace(/s+/g, "-").toLowerCase() : ""; if (!name) { this.terminal.print("Usage: agent-loops new <name> <description>", "warning"); return; } var desc = args.slice(2).join(" ") || "Custom profile"; this._loopProfiles[name] = this._defaultLoopProfile(name, desc); this._saveLoopProfiles(); this.terminal.print("Profile '" + name + "' created. Use: agent-loops edit " + name + " <key> <value>", "success"); }
- else if (sub === "edit") { var name = args[1]; if (!name || !this._loopProfiles[name]) { this.terminal.print("Profile '" + name + "' not found", "error"); return; } var key = args[2]; var val = args.slice(3).join(" "); if (!key || !val) { this.terminal.print("Usage: agent-loops edit <name> <key> <value>", "warning"); return; } this._setNestedKey(this._loopProfiles[name], key, val); this._saveLoopProfiles(); if (this._activeLoopProfile === this._loopProfiles[name]) { this._toolDefs = null; this.systemPrompt = this.buildSystemPrompt(); } this.terminal.print("Edited " + name + "." + key + " = " + val, "success"); }
- else if (sub === "delete") { var name = args[1]; if (!name) { this.terminal.print("Usage: agent-loops delete <name>", "warning"); return; } if (name === "default") { this.terminal.print("Cannot delete default profile", "error"); return; } if (this._activeLoopProfile === this._loopProfiles[name]) { this.terminal.print("Cannot delete active profile — switch first", "error"); return; } delete this._loopProfiles[name]; this._saveLoopProfiles(); this.terminal.print("Profile '" + name + "' deleted", "success"); }
- else if (sub === "clone") { var src = args[1], dst = args[2] ? args[2].replace(/s+/g, "-").toLowerCase() : ""; if (!src || !dst || !this._loopProfiles[src]) { this.terminal.print("Usage: agent-loops clone <src> <dst>", "warning"); return; } this._loopProfiles[dst] = JSON.parse(JSON.stringify(this._loopProfiles[src])); this._loopProfiles[dst].name = dst; this._loopProfiles[dst].created = new Date().toISOString(); this._saveLoopProfiles(); this.terminal.print("Cloned '" + src + "' -> '" + dst + "'", "success"); }
- else if (sub === "export") { var name = args[1]; if (!name || !this._loopProfiles[name]) { this.terminal.print("Profile '" + name + "' not found", "error"); return; } this.terminal.printJSON(JSON.stringify(this._loopProfiles[name], null, 2)); }
- else if (sub === "reset") { var name = args[1]; if (!name) { this.terminal.print("Usage: agent-loops reset <name>", "warning"); return; } var desc = this._loopProfiles[name] ? this._loopProfiles[name].displayName : "Reset profile"; this._loopProfiles[name] = this._defaultLoopProfile(name, desc); this._saveLoopProfiles(); if (this._activeLoopProfile && this._activeLoopProfile.name === name) { this._switchLoopProfile(name); } this.terminal.print("Profile '" + name + "' reset to defaults", "success"); }
- else { if (this._loopProfiles[sub]) { this._switchLoopProfile(sub); var p = this._activeLoopProfile; this.terminal.print("Switched to '" + sub + "': " + p.displayName, "success"); this.terminal.print(" Max turns: " + (p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS) + " | Tools: " + (p.tools ? p.tools.mode : "all") + " | Temp: " + (p.model && p.model.temperature !== null ? p.model.temperature : "inherit"), "text-muted"); } else { this.terminal.print("Profile '" + sub + "' not found. Available: " + Object.keys(this._loopProfiles).join(", "), "error"); } }
- }
- _setNestedKey(obj, key, val) { var parts = key.split("."); var o = obj; for (var i = 0; i < parts.length - 1; i++) { if (!o[parts[i]]) o[parts[i]] = {}; o = o[parts[i]]; } var last = parts[parts.length - 1]; try { o[last] = JSON.parse(val); } catch (e) { o[last] = val; } }
+ if (sub === 'new') {
+ var name = args[1] ? args[1].replace(/\s+/g, '-').toLowerCase() : '';
+ if (!name) { this.terminal.print('Usage: agent-loops new <name> <description>', 'warning'); return; }
+ var desc = args.slice(2).join(' ') || 'Custom profile';
+ this._loopProfiles[name] = this._defaultLoopProfile(name, desc);
+ this._saveLoopProfiles();
+ this.terminal.print("Profile '" + name + "' created. Use: agent-loops edit " + name + " <key> <value>", 'success');
+ } else if (sub === 'edit') {
+ var name = args[1];
+ if (!name || !this._loopProfiles[name]) { this.terminal.print("Profile '" + name + "' not found", 'error'); return; }
+ var key = args[2];
+ var val = args.slice(3).join(' ');
+ if (!key || !val) { this.terminal.print('Usage: agent-loops edit <name> <key> <value>', 'warning'); return; }
+ this._setNestedKey(this._loopProfiles[name], key, val);
+ this._saveLoopProfiles();
+ if (this._activeLoopProfile === this._loopProfiles[name]) { this._toolDefs = null; this.systemPrompt = this.buildSystemPrompt(); }
+ this.terminal.print('Edited ' + name + '.' + key + ' = ' + val, 'success');
+ } else if (sub === 'delete') {
+ var name = args[1];
+ if (!name) { this.terminal.print('Usage: agent-loops delete <name>', 'warning'); return; }
+ if (name === 'default') { this.terminal.print('Cannot delete default profile', 'error'); return; }
+ if (this._activeLoopProfile === this._loopProfiles[name]) { this.terminal.print('Cannot delete active profile — switch first', 'error'); return; }
+ delete this._loopProfiles[name];
+ this._saveLoopProfiles();
+ this.terminal.print("Profile '" + name + "' deleted", 'success');
+ } else if (sub === 'clone') {
+ var src = args[1], dst = args[2] ? args[2].replace(/\s+/g, '-').toLowerCase() : '';
+ if (!src || !dst || !this._loopProfiles[src]) { this.terminal.print('Usage: agent-loops clone <src> <dst>', 'warning'); return; }
+ this._loopProfiles[dst] = JSON.parse(JSON.stringify(this._loopProfiles[src]));
+ this._loopProfiles[dst].name = dst;
+ this._loopProfiles[dst].created = new Date().toISOString();
+ this._saveLoopProfiles();
+ this.terminal.print("Cloned '" + src + "' -> '" + dst + "'", 'success');
+ } else if (sub === 'export') {
+ var name = args[1];
+ if (!name || !this._loopProfiles[name]) { this.terminal.print("Profile '" + name + "' not found", 'error'); return; }
+ this.terminal.printJSON(JSON.stringify(this._loopProfiles[name], null, 2));
+ } else if (sub === 'reset') {
+ var name = args[1];
+ if (!name) { this.terminal.print('Usage: agent-loops reset <name>', 'warning'); return; }
+ var desc = this._loopProfiles[name] ? this._loopProfiles[name].displayName : 'Reset profile';
+ this._loopProfiles[name] = this._defaultLoopProfile(name, desc);
+ this._saveLoopProfiles();
+ if (this._activeLoopProfile && this._activeLoopProfile.name === name) { this._switchLoopProfile(name); }
+ this.terminal.print("Profile '" + name + "' reset to defaults", 'success');
+ } else {
+ if (this._loopProfiles[sub]) {
+ this._switchLoopProfile(sub);
+ var p = this._activeLoopProfile;
+ this.terminal.print("Switched to '" + sub + "': " + p.displayName, 'success');
+ this.terminal.print(' Max turns: ' + (p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS) + ' | Tools: ' + (p.tools ? p.tools.mode : 'all') + ' | Temp: ' + (p.model && p.model.temperature !== null ? p.model.temperature : 'inherit'), 'text-muted');
+ } else {
+ this.terminal.print("Profile '" + sub + "' not found. Available: " + Object.keys(this._loopProfiles).join(', '), 'error');
+ }
+ }
+ }
+ _setNestedKey(obj, key, val) {
+ var parts = key.split('.');
+ var o = obj;
+ for (var i = 0; i < parts.length - 1; i++) {
+ if (!o[parts[i]]) o[parts[i]] = {};
+ o = o[parts[i]];
+ }
+ var last = parts[parts.length - 1];
+ try { o[last] = JSON.parse(val); } catch (e) { o[last] = val; }
+ }
// ── BENCHMARK SUITE ──
- _benchmarkTasks() { return [ { id: "fs-navigate", prompt: "List all files in the root directory and tell me how many .js files exist", maxTurns: 3, successCheck: function(r, log) { return r.length > 10 && log.some(function(t) { return t.name === "list_files"; }); } }, { id: "fs-create-read", prompt: "Create a file called /tmp/bench-test.txt with 'hello bench', then read it back to verify", maxTurns: 4, successCheck: function(r) { return r.toLowerCase().indexOf("hello") !== -1; }, destructive: true }, { id: "nvfs-resolve", prompt: "What terminal command handles file operations? Use nvfs-resolve to find it", maxTurns: 3, successCheck: function(r, log) { return log.some(function(t) { return t.name === "nvfs_resolve"; }) && r.length > 20; } }, { id: "web-search", prompt: "Search the web for 'what is the capital of France' and tell me the answer", maxTurns: 3, successCheck: function(r) { return r.toLowerCase().indexOf("paris") !== -1; } }, { id: "multi-tool", prompt: "Read the file /package.json, then list all modules, then summarize both results", maxTurns: 6, successCheck: function(r, log) { return log.filter(function(t) { return ["read_file","list_files","list_modules"].indexOf(t.name) !== -1; }).length >= 2; } }, { id: "code-edit", prompt: "Create a file /tmp/bench-code.js with a function that adds two numbers, then read it back to verify", maxTurns: 5, successCheck: function(r, log) { return log.some(function(t) { return t.name === "write_file"; }); }, destructive: true } ]; }
+ _benchmarkTasks() {
+ return [
+ { id: 'fs-navigate', prompt: 'List all files in the root directory and tell me how many .js files exist', maxTurns: 3, successCheck: function(r, log) { return r.length > 10 && log.some(function(t) { return t.name === 'list_files'; }); } },
+ { id: 'fs-create-read', prompt: 'Create a file called /tmp/bench-test.txt with "hello bench", then read it back to verify', maxTurns: 4, successCheck: function(r) { return r.toLowerCase().indexOf('hello') !== -1; }, destructive: true },
+ { id: 'nvfs-resolve', prompt: 'What terminal command handles file operations? Use nvfs-resolve to find it', maxTurns: 3, successCheck: function(r, log) { return log.some(function(t) { return t.name === 'nvfs_resolve'; }) && r.length > 20; } },
+ { id: 'web-search', prompt: 'Search the web for "what is the capital of France" and tell me the answer', maxTurns: 3, successCheck: function(r) { return r.toLowerCase().indexOf('paris') !== -1; } },
+ { id: 'multi-tool', prompt: 'Read the file /package.json, then list all modules, then summarize both results', maxTurns: 6, successCheck: function(r, log) { return log.filter(function(t) { return ['read_file','list_files','list_modules'].indexOf(t.name) !== -1; }).length >= 2; } },
+ { id: 'code-edit', prompt: 'Create a file /tmp/bench-code.js with a function that adds two numbers, then read it back to verify', maxTurns: 5, successCheck: function(r, log) { return log.some(function(t) { return t.name === 'write_file'; }); }, destructive: true },
+ ];
+ }
async _runBenchmark(profileName, taskIds) {
- var tasks = this._benchmarkTasks(); if (taskIds) tasks = tasks.filter(function(t) { return taskIds.indexOf(t.id) !== -1; });
+ var tasks = this._benchmarkTasks();
+ if (taskIds) tasks = tasks.filter(function(t) { return taskIds.indexOf(t.id) !== -1; });
var prevProfile = this._activeLoopProfile ? this._activeLoopProfile.name : null;
if (profileName && this._loopProfiles[profileName]) this._switchLoopProfile(profileName);
- this.terminal.print("📊 Benchmark: " + profileName + " (" + tasks.length + " tasks)", "info");
+ this.terminal.print('📊 Benchmark: ' + profileName + ' (' + tasks.length + ' tasks)', 'info');
var results = [];
- for (var ti = 0; ti < tasks.length; ti++) { var task = tasks[ti]; this.terminal.print(" ▶ " + task.id + "...", "text-muted"); var t0 = Date.now(); var toolLogBefore = this._toolCallLog.length; var messages = [{ role: "user", content: task.prompt }]; messages.unshift({ role: "system", content: this.systemPrompt }); var savedMax = this._activeLoopProfile && this._activeLoopProfile.loop ? this._activeLoopProfile.loop.maxTurns : null; if (this._activeLoopProfile && this._activeLoopProfile.loop) { this._activeLoopProfile.loop.maxTurns = task.maxTurns; } try { var result = await this.agenticLoop(messages); var latency = Date.now() - t0; var newTools = this._toolCallLog.slice(toolLogBefore); var passed = task.successCheck(result.finalAnswer || "", newTools); var tokens = await this._estimateTokensForRun(newTools, result.finalAnswer); results.push({ id: task.id, turns: result.toolTurns, tokens: tokens, latency: latency, pass: passed }); this.terminal.print(" " + (passed ? "✅" : "❌") + " " + result.toolTurns + " turns, " + tokens + " tokens, " + (latency/1000).toFixed(1) + "s", passed ? "success" : "warning"); } catch (e) { results.push({ id: task.id, turns: 0, tokens: 0, latency: Date.now() - t0, pass: false, error: e.message }); this.terminal.print(" ❌ Error: " + e.message, "error"); } if (this._activeLoopProfile && this._activeLoopProfile.loop && savedMax != null) { this._activeLoopProfile.loop.maxTurns = savedMax; } this.context = []; }
+ for (var ti = 0; ti < tasks.length; ti++) {
+ var task = tasks[ti];
+ this.terminal.print(' ▶ ' + task.id + '...', 'text-muted');
+ var t0 = Date.now();
+ var toolLogBefore = this._toolCallLog.length;
+ var messages = [{ role: 'user', content: task.prompt }];
+ messages.unshift({ role: 'system', content: this.systemPrompt });
+ var savedMax = this._activeLoopProfile && this._activeLoopProfile.loop ? this._activeLoopProfile.loop.maxTurns : null;
+ if (this._activeLoopProfile && this._activeLoopProfile.loop) { this._activeLoopProfile.loop.maxTurns = task.maxTurns; }
+ try {
+ var result = await this.agenticLoop(messages);
+ var latency = Date.now() - t0;
+ var newTools = this._toolCallLog.slice(toolLogBefore);
+ var passed = task.successCheck(result.finalAnswer || '', newTools);
+ var tokens = await this._estimateTokensForRun(newTools, result.finalAnswer);
+ results.push({ id: task.id, turns: result.toolTurns, tokens: tokens, latency: latency, pass: passed });
+ this.terminal.print(' ' + (passed ? '✅' : '❌') + ' ' + result.toolTurns + ' turns, ' + tokens + ' tokens, ' + (latency/1000).toFixed(1) + 's', passed ? 'success' : 'warning');
+ } catch (e) {
+ results.push({ id: task.id, turns: 0, tokens: 0, latency: Date.now() - t0, pass: false, error: e.message });
+ this.terminal.print(' ❌ Error: ' + e.message, 'error');
+ }
+ if (this._activeLoopProfile && this._activeLoopProfile.loop && savedMax != null) { this._activeLoopProfile.loop.maxTurns = savedMax; }
+ this.context = [];
+ }
var profile = this._loopProfiles[profileName];
- if (profile) { var passed = results.filter(function(r) { return r.pass; }).length; profile.benchmark = { lastRun: new Date().toISOString(), tasks: results, avgLatency: Math.round(results.reduce(function(s, r) { return s + r.latency; }, 0) / results.length), avgTokens: Math.round(results.reduce(function(s, r) { return s + r.tokens; }, 0) / results.length), taskCompletionRate: passed / results.length }; this._saveLoopProfile(profile); }
- this.terminal.print("┌──────────────────────────────────────────────────────────┐", "info"); this.terminal.print("│ Benchmark: " + profileName, "info"); this.terminal.print("├──────────────────────────────────────────────────────────┤", "info");
- for (var ri = 0; ri < results.length; ri++) { var r = results[ri]; var status = r.pass ? "✅" : "❌"; this.terminal.print("│ " + r.id + " " + r.turns + "t " + r.tokens + "tok " + (r.latency/1000).toFixed(1) + "s " + status, "info"); }
- this.terminal.print("├──────────────────────────────────────────────────────────┤", "info");
- var passed = results.filter(function(r) { return r.pass; }).length; var avgLat = (results.reduce(function(s, r) { return s + r.latency; }, 0) / results.length / 1000).toFixed(1); var avgTok = Math.round(results.reduce(function(s, r) { return s + r.tokens; }, 0) / results.length);
- this.terminal.print("│ " + passed + "/" + results.length + " passed (" + Math.round(passed/results.length*100) + "%) | Avg: " + avgLat + "s, " + avgTok + "tok", "info"); this.terminal.print("└──────────────────────────────────────────────────────────┘", "info");
- if (prevProfile && prevProfile !== profileName) this._switchLoopProfile(prevProfile); return results;
- }
- async _estimateTokensForRun(toolLog, finalAnswer) { var om = this.terminal ? this.terminal.ollamaManager : null; if (om && om._usageLog && om._usageLog.length > 0) { var last = om._usageLog[om._usageLog.length - 1]; return last.total_tokens || 0; } var toolChars = toolLog.reduce(function(s, t) { return s + (t.result ? t.result.length : 0); }, 0); var answerChars = (finalAnswer || "").length; return Math.round((toolChars + answerChars) / 4); }
- benchCmd(args) { var profile = null; var taskIds = null; for (var ai = 0; ai < args.length; ai++) { if (args[ai].indexOf("--tasks=") === 0) taskIds = args[ai].split("=")[1].split(","); else if (args[ai].indexOf("--") !== 0) profile = args[ai]; } if (!profile) { this.terminal.print("Usage: bench [profile] [--tasks=id1,id2] [--repeat=N]", "warning"); this.terminal.print("Available tasks: " + this._benchmarkTasks().map(function(t) { return t.id; }).join(", "), "text-muted"); return; } this._runBenchmark(profile, taskIds); }
- benchCompareCmd(args) { var p1 = args[0], p2 = args[1]; if (!p1 || !p2) { this.terminal.print("Usage: bench-compare <profile1> <profile2>", "warning"); return; } var b1 = this._loopProfiles[p1] ? this._loopProfiles[p1].benchmark : null; var b2 = this._loopProfiles[p2] ? this._loopProfiles[p2].benchmark : null; if (!b1 || !b1.lastRun) { this.terminal.print("No benchmark for '" + p1 + "'. Run: bench " + p1, "warning"); return; } if (!b2 || !b2.lastRun) { this.terminal.print("No benchmark for '" + p2 + "'. Run: bench " + p2, "warning"); return; } this.terminal.print("┌──────────────────────────────────────────────────────────┐", "info"); this.terminal.print("│ " + p1 + " vs " + p2, "info"); this.terminal.print("├──────────────────────────────────────────────────────────┤", "info"); this.terminal.print("│ Pass rate: " + (b1.taskCompletionRate*100).toFixed(0) + "% vs " + (b2.taskCompletionRate*100).toFixed(0) + "%", "info"); this.terminal.print("│ Avg tokens: " + b1.avgTokens + " vs " + b2.avgTokens, "info"); this.terminal.print("│ Avg latency: " + (b1.avgLatency/1000).toFixed(1) + "s vs " + (b2.avgLatency/1000).toFixed(1) + "s", "info"); this.terminal.print("│ Last run: " + b1.lastRun.substring(0,10) + " vs " + b2.lastRun.substring(0,10), "info"); this.terminal.print("└──────────────────────────────────────────────────────────┘", "info"); }
+ if (profile) {
+ var passed = results.filter(function(r) { return r.pass; }).length;
+ profile.benchmark = {
+ lastRun: new Date().toISOString(),
+ tasks: results,
+ avgLatency: Math.round(results.reduce(function(s, r) { return s + r.latency; }, 0) / results.length),
+ avgTokens: Math.round(results.reduce(function(s, r) { return s + r.tokens; }, 0) / results.length),
+ taskCompletionRate: passed / results.length
+ };
+ this._saveLoopProfile(profile);
+ }
+ this.terminal.print('┌──────────────────────────────────────────────────────────┐', 'info');
+ this.terminal.print('│ Benchmark: ' + profileName, 'info');
+ this.terminal.print('├──────────────────────────────────────────────────────────┤', 'info');
+ for (var ri = 0; ri < results.length; ri++) {
+ var r = results[ri];
+ var status = r.pass ? '✅' : '❌';
+ this.terminal.print('│ ' + r.id + ' ' + r.turns + 't ' + r.tokens + 'tok ' + (r.latency/1000).toFixed(1) + 's ' + status, 'info');
+ }
+ this.terminal.print('├──────────────────────────────────────────────────────────┤', 'info');
+ var passed = results.filter(function(r) { return r.pass; }).length;
+ var avgLat = (results.reduce(function(s, r) { return s + r.latency; }, 0) / results.length / 1000).toFixed(1);
+ var avgTok = Math.round(results.reduce(function(s, r) { return s + r.tokens; }, 0) / results.length);
+ this.terminal.print('│ ' + passed + '/' + results.length + ' passed (' + Math.round(passed/results.length*100) + '%) | Avg: ' + avgLat + 's, ' + avgTok + 'tok', 'info');
+ this.terminal.print('└──────────────────────────────────────────────────────────┘', 'info');
+ if (prevProfile && prevProfile !== profileName) this._switchLoopProfile(prevProfile);
+ return results;
+ }
+ async _estimateTokensForRun(toolLog, finalAnswer) {
+ var om = this.terminal ? this.terminal.ollamaManager : null;
+ if (om && om._usageLog && om._usageLog.length > 0) {
+ var last = om._usageLog[om._usageLog.length - 1];
+ return last.total_tokens || 0;
+ }
+ var toolChars = toolLog.reduce(function(s, t) { return s + (t.result ? t.result.length : 0); }, 0);
+ var answerChars = (finalAnswer || '').length;
+ return Math.round((toolChars + answerChars) / 4);
+ }
+ benchCmd(args) {
+ var profile = null;
+ var taskIds = null;
+ for (var ai = 0; ai < args.length; ai++) {
+ if (args[ai].indexOf('--tasks=') === 0) taskIds = args[ai].split('=')[1].split(',');
+ else if (args[ai].indexOf('--') !== 0) profile = args[ai];
+ }
+ if (!profile) {
+ this.terminal.print('Usage: bench [profile] [--tasks=id1,id2] [--repeat=N]', 'warning');
+ this.terminal.print('Available tasks: ' + this._benchmarkTasks().map(function(t) { return t.id; }).join(', '), 'text-muted');
+ return;
+ }
+ this._runBenchmark(profile, taskIds);
+ }
+ benchCompareCmd(args) {
+ var p1 = args[0], p2 = args[1];
+ if (!p1 || !p2) { this.terminal.print('Usage: bench-compare <profile1> <profile2>', 'warning'); return; }
+ var b1 = this._loopProfiles[p1] ? this._loopProfiles[p1].benchmark : null;
+ var b2 = this._loopProfiles[p2] ? this._loopProfiles[p2].benchmark : null;
+ if (!b1 || !b1.lastRun) { this.terminal.print("No benchmark for '" + p1 + "'. Run: bench " + p1, 'warning'); return; }
+ if (!b2 || !b2.lastRun) { this.terminal.print("No benchmark for '" + p2 + "'. Run: bench " + p2, 'warning'); return; }
+ this.terminal.print('┌──────────────────────────────────────────────────────────┐', 'info');
+ this.terminal.print('│ ' + p1 + ' vs ' + p2, 'info');
+ this.terminal.print('├──────────────────────────────────────────────────────────┤', 'info');
+ this.terminal.print('│ Pass rate: ' + (b1.taskCompletionRate*100).toFixed(0) + '% vs ' + (b2.taskCompletionRate*100).toFixed(0) + '%', 'info');
+ this.terminal.print('│ Avg tokens: ' + b1.avgTokens + ' vs ' + b2.avgTokens, 'info');
+ this.terminal.print('│ Avg latency: ' + (b1.avgLatency/1000).toFixed(1) + 's vs ' + (b2.avgLatency/1000).toFixed(1) + 's', 'info');
+ this.terminal.print('│ Last run: ' + b1.lastRun.substring(0,10) + ' vs ' + b2.lastRun.substring(0,10), 'info');
+ this.terminal.print('└──────────────────────────────────────────────────────────┘', 'info');
+ }
// ── SELF-IMPROVEMENT LOOP ──
_postRunReview(result, toolLog, usageData) {
- var anomalies = []; var maxT = this._activeLoopProfile && this._activeLoopProfile.loop ? this._activeLoopProfile.loop.maxTurns : this.MAX_TOOL_TURNS;
- if (result.toolTurns >= maxT * 0.8) { anomalies.push({ type: "max_turns_near_limit", value: result.toolTurns, max: maxT }); }
- if (usageData && usageData.total_tokens > 50000) { anomalies.push({ type: "high_token_usage", value: usageData.total_tokens }); }
- var failedTools = toolLog.filter(function(t) { return t.error; }); if (failedTools.length > 2) { var uniqueTools = []; failedTools.forEach(function(t) { if (uniqueTools.indexOf(t.name) === -1) uniqueTools.push(t.name); }); anomalies.push({ type: "tool_failures", count: failedTools.length, tools: uniqueTools }); }
- var bench = this._activeLoopProfile ? this._activeLoopProfile.benchmark : null; if (bench && bench.avgTokens && usageData) { if (usageData.total_tokens > bench.avgTokens * 1.5) { anomalies.push({ type: "above_benchmark", value: usageData.total_tokens, benchmark: bench.avgTokens, ratio: (usageData.total_tokens / bench.avgTokens).toFixed(1) }); } }
+ var anomalies = [];
+ var maxT = this._activeLoopProfile && this._activeLoopProfile.loop ? this._activeLoopProfile.loop.maxTurns : this.MAX_TOOL_TURNS;
+ if (result.toolTurns >= maxT * 0.8) {
+ anomalies.push({ type: 'max_turns_near_limit', value: result.toolTurns, max: maxT });
+ }
+ if (usageData && usageData.total_tokens > 50000) {
+ anomalies.push({ type: 'high_token_usage', value: usageData.total_tokens });
+ }
+ var failedTools = toolLog.filter(function(t) { return t.error; });
+ if (failedTools.length > 2) {
+ var uniqueTools = [];
+ failedTools.forEach(function(t) { if (uniqueTools.indexOf(t.name) === -1) uniqueTools.push(t.name); });
+ anomalies.push({ type: 'tool_failures', count: failedTools.length, tools: uniqueTools });
+ }
+ var bench = this._activeLoopProfile ? this._activeLoopProfile.benchmark : null;
+ if (bench && bench.avgTokens && usageData) {
+ if (usageData.total_tokens > bench.avgTokens * 1.5) {
+ anomalies.push({ type: 'above_benchmark', value: usageData.total_tokens, benchmark: bench.avgTokens, ratio: (usageData.total_tokens / bench.avgTokens).toFixed(1) });
+ }
+ }
if (anomalies.length === 0) return;
- var suggestions = []; var profileAdjusted = false;
- for (var ai = 0; ai < anomalies.length; ai++) { var a = anomalies[ai];
- if (a.type === "max_turns_near_limit") {
- suggestions.push("This task used " + result.toolTurns + "/" + maxT + " turns — consider increasing max-turns or splitting the task");
- if (this._activeLoopProfile && this._activeLoopProfile.loop) { var newMax = (this._activeLoopProfile.loop.maxTurns || this.MAX_TOOL_TURNS) + 2; this._activeLoopProfile.loop.maxTurns = newMax; this._saveLoopProfile(this._activeLoopProfile); suggestions.push(" → Auto-adjusted maxTurns to " + newMax); profileAdjusted = true; }
- } else if (a.type === "high_token_usage") suggestions.push("High token usage (" + (a.value/1000).toFixed(0) + "K) — consider a minimal-tools profile for simpler tasks");
- else if (a.type === "tool_failures") {
- suggestions.push(a.count + " tool failures (" + a.tools.join(", ") + ") — check if modules are loaded or paths correct");
- if (this._activeLoopProfile && this._activeLoopProfile.systemPrompt) { var constraint = "[Constraint] Avoid tools that failed: " + a.tools.join(", ") + ". Verify module state before calling these tools."; if (this._activeLoopProfile.systemPrompt.append) { if (this._activeLoopProfile.systemPrompt.append.indexOf(constraint) === -1) { this._activeLoopProfile.systemPrompt.append += "\n" + constraint; } } else { this._activeLoopProfile.systemPrompt.append = constraint; } this._saveLoopProfile(this._activeLoopProfile); suggestions.push(" → Added constraint to profile: avoid " + a.tools.join(", ")); profileAdjusted = true; }
- } else if (a.type === "above_benchmark") suggestions.push("This run used " + a.ratio + "x more tokens than benchmark avg (" + a.benchmark + ") — consider profile optimization");
- }
- var suggestion = suggestions.join("\n");
- try { var fs = this.terminal.fs; var index = fs.fsReadJSON("/skills/index.json") || {}; var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf("auto-improvement") !== -1; }); if (!this._loopLessonCleaned) { this._loopLessonCleaned = true; autoSkills.forEach(function(e) { try { fs.unlink("/skills/" + e[0] + ".md"); } catch (ex) {} delete index[e[0]]; }); autoSkills = []; } var now = Date.now(); autoSkills = autoSkills.filter(function(e) { var created = new Date(e[1].created).getTime(); if (now - created > 86400000) { try { fs.unlink("/skills/" + e[0] + ".md"); } catch (ex) {} delete index[e[0]]; return false; } return true; }); var anomalyTypes = anomalies.map(function(a) { return a.type; }).sort().join(","); var isDuplicate = autoSkills.length > 0 && autoSkills[autoSkills.length - 1][1].anomalyTypes === anomalyTypes; if (!isDuplicate && autoSkills.length >= 3) { var oldest = autoSkills[0]; try { fs.unlink("/skills/" + oldest[0] + ".md"); } catch (ex) {} delete index[oldest[0]]; autoSkills.shift(); } if (!isDuplicate) { var skillName = "loop-lesson-" + Date.now(); var content = "---\nname: " + skillName + "\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n" + JSON.stringify(anomalies, null, 2) + "\n\n## Suggestions\n\n" + suggestion + "\n\n## Context\n- Profile: " + (this._activeLoopProfile ? this._activeLoopProfile.name : "none") + "\n- Turns: " + result.toolTurns + "\n- Tools used: " + toolLog.map(function(t) { return t.name; }).join(", "); fs.mkdirRecursive("/skills"); fs.writeFile("/skills/" + skillName + ".md", content); index[skillName] = { name: skillName, description: "Auto-saved loop lesson", tags: ["auto-improvement", "agent-loop", "auto"], category: "system", created: new Date().toISOString(), updated: new Date().toISOString(), anomalyTypes: anomalyTypes }; fs.fsWriteJSON("/skills/index.json", index); } } catch (e) {}
- var msg = "💡 Self-improvement: " + anomalies.length + " anomaly(ies)"; if (profileAdjusted) msg += " — profile auto-adjusted";
- this.terminal.print(msg, "info");
+ var suggestions = [];
+ var profileAdjusted = false;
+ for (var ai = 0; ai < anomalies.length; ai++) {
+ var a = anomalies[ai];
+ if (a.type === 'max_turns_near_limit') {
+ suggestions.push('This task used ' + result.toolTurns + '/' + maxT + ' turns — consider increasing max-turns or splitting the task');
+ // Auto-correct: bump maxTurns by +2
+ if (this._activeLoopProfile && this._activeLoopProfile.loop) {
+ var newMax = (this._activeLoopProfile.loop.maxTurns || this.MAX_TOOL_TURNS) + 2;
+ this._activeLoopProfile.loop.maxTurns = newMax;
+ this._saveLoopProfile(this._activeLoopProfile);
+ suggestions.push(' → Auto-adjusted maxTurns to ' + newMax);
+ profileAdjusted = true;
+ }
+ }
+ else if (a.type === 'high_token_usage') suggestions.push('High token usage (' + (a.value/1000).toFixed(0) + 'K) — consider a minimal-tools profile for simpler tasks');
+ else if (a.type === 'tool_failures') {
+ suggestions.push(a.count + ' tool failures (' + a.tools.join(', ') + ') — check if modules are loaded or paths correct');
+ // Auto-fix: map failed tools to modules and suggest fix
+ if (!this._autoFixSuggestions) this._autoFixSuggestions = [];
+ if (!this._toolToModuleMap) this._buildToolToModuleMap();
+ for (var fi = 0; fi < a.tools.length; fi++) {
+ var failedMod = this._toolToModuleMap[a.tools[fi]];
+ if (failedMod && this._autoFixSuggestions.length < 3) {
+ this._autoFixSuggestions.push({
+ module: failedMod,
+ tool: a.tools[fi],
+ reason: 'Repeated tool failure in agent loop',
+ suggestion: 'Consider running ship_module to fix ' + failedMod
+ });
+ }
+ }
+ // Auto-correct: inject negative constraint into profile append
+ if (this._activeLoopProfile && this._activeLoopProfile.systemPrompt) {
+ var constraint = '[Constraint] Avoid tools that failed: ' + a.tools.join(', ') + '. Verify module state before calling these tools.';
+ if (this._activeLoopProfile.systemPrompt.append) {
+ if (this._activeLoopProfile.systemPrompt.append.indexOf(constraint) === -1) {
+ this._activeLoopProfile.systemPrompt.append += '\n' + constraint;
+ }
+ } else {
+ this._activeLoopProfile.systemPrompt.append = constraint;
+ }
+ this._saveLoopProfile(this._activeLoopProfile);
+ suggestions.push(' → Added constraint to profile: avoid ' + a.tools.join(', '));
+ profileAdjusted = true;
+ }
+ }
+ else if (a.type === 'above_benchmark') suggestions.push('This run used ' + a.ratio + 'x more tokens than benchmark avg (' + a.benchmark + ') — consider profile optimization');
+ }
+ var suggestion = suggestions.join('\n');
+ try {
+ var fs = this.terminal.fs;
+ var index = fs.fsReadJSON('/skills/index.json') || {};
+ var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf('auto-improvement') !== -1; });
+ // One-time cleanup: remove all existing loop-lesson files on first run
+ if (!this._loopLessonCleaned) {
+ this._loopLessonCleaned = true;
+ autoSkills.forEach(function(e) {
+ try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {}
+ delete index[e[0]];
+ });
+ autoSkills = [];
+ }
+ // Age limit: remove lessons older than 24h
+ var now = Date.now();
+ autoSkills = autoSkills.filter(function(e) {
+ var created = new Date(e[1].created).getTime();
+ if (now - created > 86400000) {
+ try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {}
+ delete index[e[0]];
+ return false;
+ }
+ return true;
+ });
+ // Dedup: skip if latest lesson has same anomaly types
+ var anomalyTypes = anomalies.map(function(a) { return a.type; }).sort().join(',');
+ var isDuplicate = autoSkills.length > 0 && autoSkills[autoSkills.length - 1][1].anomalyTypes === anomalyTypes;
+ // FIFO rotation: if at cap, delete oldest before creating
+ if (!isDuplicate && autoSkills.length >= 3) {
+ var oldest = autoSkills[0];
+ try { fs.unlink('/skills/' + oldest[0] + '.md'); } catch (ex) {}
+ delete index[oldest[0]];
+ autoSkills.shift();
+ }
+ if (!isDuplicate) {
+ var skillName = 'loop-lesson-' + Date.now();
+ var content = '---\nname: ' + skillName + '\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n' + JSON.stringify(anomalies, null, 2) + '\n\n## Suggestions\n\n' + suggestion + '\n\n## Context\n- Profile: ' + (this._activeLoopProfile ? this._activeLoopProfile.name : 'none') + '\n- Turns: ' + result.toolTurns + '\n- Tools used: ' + toolLog.map(function(t) { return t.name; }).join(', ');
+ fs.mkdirRecursive('/skills');
+ fs.writeFile('/skills/' + skillName + '.md', content);
+ index[skillName] = { name: skillName, description: 'Auto-saved loop lesson', tags: ['auto-improvement', 'agent-loop', 'auto'], category: 'system', created: new Date().toISOString(), updated: new Date().toISOString(), anomalyTypes: anomalyTypes };
+ fs.fsWriteJSON('/skills/index.json', index);
+ }
+ } catch (e) { /* VFS not available */ }
+ var msg = '💡 Self-improvement: ' + anomalies.length + ' anomaly(ies)';
+ if (profileAdjusted) msg += ' — profile auto-adjusted';
+ this.terminal.print(msg, 'info');
}
- _vaultmindStatusCmd() {
- const token = localStorage.getItem('vaultmind_token');
- if (token) {
- this.terminal.print('✅ VaultMind session active (token: ' + token.substring(0, 12) + '...)', 'success');
- } else {
- this.terminal.print('❌ No VaultMind session. Use: vaultmind-connect [testnet|mainnet]', 'warning');
+ async _collectDashboardData() {
+ try {
+ const terminal = this.terminal;
+ const mm = terminal.moduleManager;
+ const om = terminal.ollamaManager;
+ const fs = terminal.fs;
+ const data = {};
+
+ // Version
+ data.version = window.NEWOS4_VERSION || 'v?';
+
+ // Modules
+ const mods = mm ? Object.keys(mm.modules) : [];
+ const registry = mm ? (mm.moduleRegistry || []) : [];
+ data.modules = {
+ loaded: mods.length,
+ total: registry.length || mods.length,
+ modifiedCount: 0
+ };
+ try {
+ const modsDir = fs ? fs.readDir('/_mods/') : null;
+ if (modsDir) data.modules.modifiedCount = modsDir.filter(function(e) { return e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js'); }).length;
+ } catch (e) {}
+
+ // Agents
+ const orch = mm ? mm.modules['OrchestratorModule'] : null;
+ if (orch && orch._agents) {
+ var agentList = Object.values(orch._agents);
+ data.agents = {
+ total: agentList.length,
+ busy: agentList.filter(function(a) { return a.status === 'busy'; }).length,
+ idle: agentList.filter(function(a) { return a.status === 'idle'; }).length,
+ sleeping: agentList.filter(function(a) { return a.status === 'sleeping'; }).length,
+ failed: agentList.filter(function(a) { return a.status === 'failed' || a.status === 'restarting'; }).length,
+ list: agentList.map(function(a) { return { id: a.id, role: a.role, status: a.status, caps: (a.capabilities || []).length }; })
+ };
+ } else {
+ data.agents = { total: 0, busy: 0, idle: 0, sleeping: 0, failed: 0, list: [] };
+ }
+
+ // Memory
+ var heapStr = 'N/A';
+ try { if (performance.memory) heapStr = (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB'; } catch (e) {}
+ var graphNodes = 0;
+ var mg = mm ? mm.modules['MemoryGraphModule'] : null;
+ if (mg && mg._nodes) graphNodes = Object.keys(mg._nodes).length;
+ data.memory = { heap: heapStr, graphNodes: graphNodes };
+
+ // VFS
+ try {
+ if (fs) {
+ var fileCount = 0, dirCount = 0;
+ var walk = function(path) {
+ var entries = fs.ls(path);
+ if (!entries) return;
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i].type === 'file') fileCount++;
+ else { dirCount++; walk(path === '/' ? '/' + entries[i].name : path + '/' + entries[i].name); }
+ }
+ };
+ walk('/');
+ data.vfs = { files: fileCount, dirs: dirCount, size: (JSON.stringify(fs.root || {}).length / 1024).toFixed(1) + 'KB' };
+ }
+ } catch (e) { data.vfs = { files: 0, dirs: 0, size: '0KB' }; }
+
+ // Ollama
+ data.ollama = {
+ connected: om ? om.connected : false,
+ model: om ? om.currentModel : null,
+ provider: om && om.currentProvider ? om.currentProvider.name : null
+ };
+
+ // Skills
+ try {
+ var skillIdx = fs ? fs.fsReadJSON('/skills/index.json') : null;
+ data.skills = { count: skillIdx ? Object.keys(skillIdx).length : 0 };
+ } catch (e) { data.skills = { count: 0 }; }
+
+ // Token usage
+ try {
+ if (om && typeof om.getUsageStats === 'function') {
+ var usage = om.getUsageStats({ since_hours: 24 });
+ data.usage = usage;
+ }
+ } catch (e) { data.usage = { total: 0 }; }
+
+ // Tool latency
+ var sm = mm ? mm.modules['ServerMonitorModule'] : null;
+ if (sm && sm._latencyLog) {
+ var toolStats = {};
+ for (var i = 0; i < sm._latencyLog.length; i++) {
+ var entry = sm._latencyLog[i];
+ if (!toolStats[entry.name]) toolStats[entry.name] = { calls: 0, totalMs: 0, errors: 0, maxMs: 0 };
+ toolStats[entry.name].calls++;
+ toolStats[entry.name].totalMs += entry.duration;
+ toolStats[entry.name].errors += entry.error ? 1 : 0;
+ toolStats[entry.name].maxMs = Math.max(toolStats[entry.name].maxMs, entry.duration);
+ }
+ data.latency = Object.entries(toolStats)
+ .map(function(e) { return { name: e[0], avg: Math.round(e[1].totalMs / e[1].calls) + 'ms', max: e[1].maxMs + 'ms', calls: e[1].calls, errors: e[1].errors }; })
+ .sort(function(a, b) { return parseInt(b.avg) - parseInt(a.avg); })
+ .slice(0, 10);
+ } else {
+ data.latency = [];
+ }
+
+ // System health
+ var totalErrors = 0;
+ try {
+ var ds = mm ? mm.modules['DebugSupervisorModule'] : null;
+ if (ds && ds._errorDB) totalErrors += Object.keys(ds._errorDB.errors).length;
+ if (sm && sm._latencyLog) totalErrors += sm._latencyLog.filter(function(e) { return e.error; }).length;
+ } catch (e) {}
+ var predictor = mm ? mm.modules['PredictorModule'] : null;
+ var predictorAcc = 'N/A';
+ if (predictor && predictor._accuracy && predictor._accuracy.total > 0) {
+ predictorAcc = Math.round(predictor._accuracy.correct / predictor._accuracy.total * 100) + '%';
+ }
+ data.health = {
+ modules: data.modules.loaded + '/' + data.modules.total,
+ vfsSize: data.vfs ? data.vfs.size : '?',
+ online: typeof navigator !== 'undefined' ? navigator.onLine : true,
+ errors: totalErrors,
+ predictorAccuracy: predictorAcc
+ };
+
+ data.timestamp = Date.now();
+
+ // Write to localStorage for the dashboard HTML page to read
+ try { localStorage.setItem('webx11_dashboard_data', JSON.stringify(data)); } catch (e) {}
+
+ return data;
+ } catch (e) {
+ console.error('[Dashboard] Collection error:', e.message);
+ return { error: e.message, timestamp: Date.now() };
}
}
}
@@ -12021,8 +13267,7 @@ class ChatStudioModule {
var shortNames = {
'llama3.2': 'Llama3.2',
'llama3': 'Llama3',
- 'gemma3:4b': 'Gemma3-4B',
- 'gemma3:1b': 'Gemma3-1B',
+ 'gemma4:31b-cloud': 'Gemma4-31B',
'gemma2:2b': 'Gemma2',
'qwen2.5:7b-instruct': 'Qwen2.5',
'qwen3:8b': 'Qwen3',
@@ -13173,10 +14418,15 @@ class LeaseModule {
this.terminal.print('📜 Lease system loaded', 'info');
}
createLease(args) {
+ // Parse --write flag
+ const writeIdx = args.indexOf('--write');
+ const writeAccess = writeIdx !== -1;
+ if (writeIdx !== -1) args.splice(writeIdx, 1);
+
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');
+ if (!workerId) return this.terminal.print('Usage: lease-create <agent-id> [duration_sec] [task] [--write]', 'error');
const lease = {
id: `lease-${Date.now()}`,
@@ -13186,7 +14436,8 @@ class LeaseModule {
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + duration * 1000).toISOString(),
status: 'active',
- duration
+ duration,
+ write_access: writeAccess
};
this._leases[lease.id] = lease;
this.terminal.fs.writeFile(`leases/${lease.id}.json`, JSON.stringify(lease));
@@ -13214,9 +14465,14 @@ class LeaseModule {
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');
+ const writeTag = l.write_access ? ' ✏️write' : ' 🔍read';
+ this.terminal.print(` ${l.id} → ${l.workerId} (${remaining}s remain)${writeTag} — ${l.task.substring(0, 40)}`, 'info');
});
}
+ hasWriteAccess(agentId) {
+ // Check if any active lease grants write access for this agent
+ return Object.values(this._leases).some(l => l.workerId === agentId && l.status === 'active' && l.write_access === true);
+ }
revokeLease(id) {
if (!this._leases[id]) return this.terminal.print(`❌ Lease ${id} not found`, 'error');
this._leases[id].status = 'revoked';
@@ -13296,6 +14552,8 @@ class OrchestratorModule {
this._activeConsensus = null;
this._pipelineHistory = [];
this._PIPELINE_CONFIG = { autoFix: false, stageTimeout: 30000 };
+ this._schedules = [];
+ this._scheduleTimer = null;
this._loadConfig();
}
_loadConfig() {
@@ -13348,6 +14606,9 @@ class OrchestratorModule {
this.commands['consensus-status'] = () => this.consensusStatus();
this.commands['consensus-history'] = () => this.consensusHistory();
this.commands['pipeline'] = (args) => this.runPipeline(args);
+ this.commands['orchestrator-schedule'] = (args) => this.addSchedule(args);
+ this.commands['orchestrator-schedules'] = () => this.listSchedules();
+ this.commands['orchestrator-unschedule'] = (args) => this.removeSchedule(args[0]);
}
// ─── Task Decomposition Engine ───
_decomposeGoal(goal) {
@@ -13410,6 +14671,9 @@ class OrchestratorModule {
'consensus-status': 'consensus-status — Show current/active consensus round status',
'consensus-history': 'consensus-history — Show past consensus rounds and their resolutions',
'pipeline': 'pipeline <test|deploy|verify|history|config> — Autonomous CI/CD: test agents/queue/modules, deploy (test→auto-fix→verify), history (show runs), config (show config)',
+ 'orchestrator-schedule': 'orchestrator-schedule <role> <interval_seconds> <task...> — Schedule a recurring task delegation',
+ 'orchestrator-schedules': 'orchestrator-schedules — List all active recurring schedules',
+ 'orchestrator-unschedule': 'orchestrator-unschedule <id> — Remove a recurring schedule',
'orchestrator-status': 'orchestrator-status — Show agent pool status and config',
'orchestrator-spawn': 'orchestrator-spawn [role] [capabilities...] — Manually spawn a new agent',
'orchestrator-kill': 'orchestrator-kill <agent-id> — Terminate an agent and revoke leases',
@@ -13434,13 +14698,17 @@ class OrchestratorModule {
this._reanimateFromVFS();
// A2A mailbox polling loop (500ms)
if (this._A2A_POLLING) {
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
}
// Heartbeat watchdog
- this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), this._HEARTBEAT_CHECK);
+ this._heartbeatInterval = this.terminal.moduleManager._setInterval(() => this._checkHeartbeats(), this._HEARTBEAT_CHECK);
// Idle timeout sweep
- this._idleSweepInterval = setInterval(() => this._sweepIdle(), this._IDLE_SWEEP_INTERVAL);
+ this._idleSweepInterval = this.terminal.moduleManager._setInterval(() => this._sweepIdle(), this._IDLE_SWEEP_INTERVAL);
this.terminal.print('🧠 Orchestrator loaded — A2A mailbox transport active', 'success');
+ // Load recurring schedules from VFS
+ this._loadSchedules();
+ // Pre-spawn default agents after a short delay (let other modules init first)
+ setTimeout(() => this._preSpawnDefaultAgents(), 2000);
// Subscribe to event bus for in-memory A2A delivery (instant, no VFS poll)
this._on('a2a:message', (msg) => {
if (msg.type === 'heartbeat' && msg.from && this._agents[msg.from]) {
@@ -13825,16 +15093,16 @@ class OrchestratorModule {
'a2a-polling': () => {
this._A2A_POLLING = val === 'on' || val === 'true';
if (this._A2A_POLLING && !this._a2aPollTimer) {
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
} else if (!this._A2A_POLLING && this._a2aPollTimer) {
- clearInterval(this._a2aPollTimer);
+ this.terminal.moduleManager._clearTimer(this._a2aPollTimer);
this._a2aPollTimer = null;
}
return `a2a-polling ${this._A2A_POLLING ? 'on' : 'off'}`;
},
- 'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; clearInterval(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; },
- 'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; clearInterval(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; },
- 'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; clearInterval(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; },
+ 'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; this.terminal.moduleManager._clearTimer(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; },
+ 'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; this.terminal.moduleManager._clearTimer(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = this.terminal.moduleManager._setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; },
+ 'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this.terminal.moduleManager._clearTimer(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = this.terminal.moduleManager._setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; },
'dead-purge-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this._DEAD_PURGE_TIMEOUT = n; return `dead-purge-timeout set to ${n}ms`; },
'consensus-size': () => { const n = parseInt(val); if (isNaN(n) || n < 2 || n > 10) return 'Must be 2-10'; this._CONSENSUS_SIZE = n; return `consensus-size set to ${n}`; },
'consensus-strategy': () => { if (!['majority', 'unanimous', 'weighted'].includes(val)) return 'Must be majority, unanimous, or weighted'; this._CONSENSUS_STRATEGY = val; return `consensus-strategy set to ${val}`; },
@@ -14247,7 +15515,7 @@ class OrchestratorModule {
// Fix 2: Restart A2A poller if stopped
if (!this._A2A_POLLING) {
this._A2A_POLLING = true;
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
this.terminal.print(' Restarted A2A polling', 'warning');
fixed++;
}
@@ -14377,9 +15645,12 @@ class OrchestratorModule {
}
}
- // Create a lease
+ // Create a lease — with --write if agent has write capabilities
const leaseModule = this.terminal.moduleManager.modules['LeaseModule'];
- const lease = leaseModule ? leaseModule.createLease(['lease-create', agent.id, '60', task]) : null;
+ const hasWriteCap = (agent.capabilities || []).some(function(c) { return c === 'write_file' || c === 'edit_module' || c === 'delete_file'; });
+ const leaseArgs = ['lease-create', agent.id, '60', task];
+ if (hasWriteCap) leaseArgs.push('--write');
+ const lease = leaseModule ? leaseModule.createLease(leaseArgs) : null;
// Mark agent as busy
agent.status = 'busy';
@@ -14448,6 +15719,109 @@ class OrchestratorModule {
return outboxResult || 'completed';
}
+ // ─── Recurring Scheduling ───
+
+ addSchedule(args) {
+ if (args.length < 2) return this.terminal.print('Usage: orchestrator-schedule <role> <interval_seconds> <task...>', 'error');
+ const role = args[0];
+ const interval = parseInt(args[1]);
+ if (isNaN(interval) || interval < 10) return this.terminal.print('Interval must be >= 10 seconds', 'error');
+ const task = args.slice(2).join(' ');
+ if (!task) return this.terminal.print('Task description required', 'error');
+
+ const schedule = {
+ id: 'sched-' + Date.now(),
+ role: role,
+ interval: interval * 1000,
+ task: task,
+ createdAt: new Date().toISOString(),
+ lastRun: null,
+ nextRun: Date.now() + interval * 1000,
+ runs: 0
+ };
+ this._schedules.push(schedule);
+ this._saveSchedules();
+ this._startScheduleTimer();
+ this.terminal.print('📅 Schedule created: "' + task.substring(0, 40) + '" every ' + interval + 's (role: ' + role + ')', 'success');
+ return schedule;
+ }
+
+ listSchedules() {
+ if (this._schedules.length === 0) return this.terminal.print('No active schedules', 'info');
+ this.terminal.print('📅 Recurring Schedules (' + this._schedules.length + '):', 'header');
+ for (const s of this._schedules) {
+ const nextIn = Math.max(0, Math.round((s.nextRun - Date.now()) / 1000));
+ this.terminal.print(' ' + s.id + ' — "' + s.task.substring(0, 40) + '" every ' + (s.interval / 1000) + 's [' + s.role + '] (next: ' + nextIn + 's, runs: ' + s.runs + ')', 'info');
+ }
+ }
+
+ removeSchedule(id) {
+ const idx = this._schedules.findIndex(function(s) { return s.id === id; });
+ if (idx === -1) return this.terminal.print('Schedule ' + id + ' not found', 'error');
+ this._schedules.splice(idx, 1);
+ this._saveSchedules();
+ if (this._schedules.length === 0 && this._scheduleTimer) {
+ clearInterval(this._scheduleTimer);
+ this._scheduleTimer = null;
+ }
+ this.terminal.print('Schedule ' + id + ' removed', 'success');
+ }
+
+ _startScheduleTimer() {
+ if (this._scheduleTimer) return;
+ this._scheduleTimer = setInterval(function() {
+ var now = Date.now();
+ for (var i = 0; i < this._schedules.length; i++) {
+ var s = this._schedules[i];
+ if (now >= s.nextRun) {
+ s.lastRun = new Date().toISOString();
+ s.nextRun = now + s.interval;
+ s.runs++;
+ this.terminal.print('📅 Schedule trigger: "' + s.task.substring(0, 40) + '"', 'info');
+ this.delegateTask(s.task);
+ }
+ }
+ this._saveSchedules();
+ }.bind(this), 5000);
+ }
+
+ _saveSchedules() {
+ try {
+ this.terminal.fs.fsWriteJSON('/orchestrator/schedules.json', this._schedules);
+ } catch (e) { console.warn('[Orchestrator] Schedule save error:', e.message); }
+ }
+
+ _loadSchedules() {
+ try {
+ const data = this.terminal.fs.fsReadJSON('/orchestrator/schedules.json');
+ if (data && Array.isArray(data)) {
+ this._schedules = data;
+ if (this._schedules.length > 0) this._startScheduleTimer();
+ }
+ } catch (e) { this._schedules = []; }
+ }
+
+ _preSpawnDefaultAgents() {
+ // Spawn 3 role-specific agents at boot
+ var roles = [
+ { role: 'auto-learner', caps: ['list_files', 'read_file', 'write_file', 'view_module'], task: 'audit skills' },
+ { role: 'kernel-architect', caps: ['list_files', 'read_file', 'write_file', 'run_command'], task: 'index VFS' },
+ { role: 'searcher', caps: ['list_files', 'read_file', 'web_search'], task: 'check stale data' }
+ ];
+ for (var i = 0; i < roles.length; i++) {
+ var r = roles[i];
+ // Check if an agent with this role already exists
+ var existing = Object.values(this._agents).find(function(a) { return a.role === r.role; });
+ if (!existing) {
+ var agent = this.spawnAgent(r.role, r.caps);
+ if (agent) {
+ agent.restartPolicy = 'always';
+ this.terminal.print('🤖 Pre-spawned agent: ' + r.role + ' (' + r.caps.length + ' caps)', 'info');
+ }
+ }
+ }
+ }
+
// ─── Monitoring ───
_checkHeartbeats() {
@@ -14914,6 +16288,11 @@ class MemoryGraphModule {
this._debounceTimer = null;
this._initialized = false;
this._totalNodeAccesses = 0;
+ // v4.5.89: Decay configuration
+ this._decayDays = 30;
+ this._decayMinUses = 3;
+ this._decayRate = 0.01; // Daily decay rate
+ this._lastDecayRun = 0;
}
getHelpInfo() {
@@ -14923,6 +16302,10 @@ class MemoryGraphModule {
'memgraph-stats': 'memgraph-stats — Most connected nodes, strongest edges, cluster info',
'memgraph-export': 'memgraph-export — Export graph as JSON',
'memgraph-prune': 'memgraph-prune — Remove stale low-weight edges (>30d, <3 accesses)',
+ 'memgraph-learn': 'memgraph-learn <entity> <fact> — Add a fact to the memory graph',
+ 'memgraph-session': 'memgraph-session [date] — Show graph nodes for a session (default: today)',
+ 'memgraph-agent': 'memgraph-agent <agentId> — Show namespaced nodes for an agent',
+ 'memgraph-decay-config': 'memgraph-decay-config <days> <min_uses> — Configure decay policy (default: 30d, 3 uses)',
};
}
@@ -14932,6 +16315,11 @@ class MemoryGraphModule {
this.commands['memgraph-stats'] = () => this.showStats();
this.commands['memgraph-export'] = () => this.exportGraph();
this.commands['memgraph-prune'] = () => this.prune();
+ this.commands['memgraph-learn'] = (args) => this.learn(args);
+ this.commands['memgraph-session'] = (args) => this.showSession(args.join(' '));
+ this.commands['memgraph-agent'] = (args) => this.showAgent(args.join(' '));
+ // v4.5.89: Decay configuration
+ this.commands['memgraph-decay-config'] = (args) => this.decayConfig(args);
}
init(terminal) {
@@ -14940,13 +16328,16 @@ class MemoryGraphModule {
this._initialized = true;
this.terminal.print('🧠 Memory Graph loaded — cross-session context active', 'info');
// Log startup for graph
- this.addNode('session', `session-${Date.now()}`, `Session ${new Date().toLocaleDateString()}`);
+ // v4.5.89 fix: Use day-floored key to match edge source keys (P0-1 from review)
+ this.addNode('session', `session-${Math.floor(Date.now() / 86400000)}`, `Session ${new Date().toLocaleDateString()}`);
}
// ─── Node API ───
- addNode(type, id, label, metadata = {}) {
- const key = `${type}:${id}`;
+ addNode(type, id, label, metadata = {}, agentId = null) {
+ // v4.5.88: Support agent namespacing with dual-lookup
+ const ns = agentId ? `agent:${agentId}` : 'global';
+ const key = agentId ? `${ns}:${type}:${id}` : `${type}:${id}`;
if (!this._nodes[key]) {
this._nodes[key] = {
id: key,
@@ -14955,7 +16346,9 @@ class MemoryGraphModule {
metadata: metadata || {},
created: new Date().toISOString(),
updated: new Date().toISOString(),
- access_count: 1
+ access_count: 1,
+ namespace: ns,
+ source_session: `session-${Math.floor(Date.now() / 86400000)}`
};
} else {
this._nodes[key].updated = new Date().toISOString();
@@ -14969,8 +16362,14 @@ class MemoryGraphModule {
return key;
}
- getNode(type, id) {
- return this._nodes[`${type}:${id}`] || null;
+ // v4.5.88: Dual-lookup — try namespaced key first, fall back to global
+ getNode(type, id, agentId = null) {
+ if (agentId) {
+ const nsKey = `agent:${agentId}:${type}:${id}`;
+ if (this._nodes[nsKey]) return this._nodes[nsKey];
+ }
+ const globalKey = `${type}:${id}`;
+ return this._nodes[globalKey] || null;
}
findNodes(query) {
@@ -14984,7 +16383,8 @@ class MemoryGraphModule {
// ─── Edge API ───
- addEdge(sourceKey, targetKey, type, weight = 1) {
+ // v4.5.88: addEdge tracks source_agents for multi-agent attribution
+ addEdge(sourceKey, targetKey, type, weight = 1, agentId = null) {
// Check if edge already exists
let edge = this._edges.find(e =>
e.source === sourceKey && e.target === targetKey && e.type === type
@@ -14993,6 +16393,7 @@ class MemoryGraphModule {
edge.weight = (edge.weight + weight) / 2;
edge.last_used = new Date().toISOString();
edge.use_count++;
+ if (agentId) edge.source_agents = [...new Set([...(edge.source_agents || []), agentId])];
} else {
this._edges.push({
source: sourceKey,
@@ -15001,7 +16402,8 @@ class MemoryGraphModule {
weight,
created: new Date().toISOString(),
last_used: new Date().toISOString(),
- use_count: 1
+ use_count: 1,
+ source_agents: agentId ? [agentId] : []
});
}
this._debounceSave();
@@ -15009,11 +16411,12 @@ class MemoryGraphModule {
// ─── Event-based Learning ───
- recordFileAccess(path) {
+ // v4.5.89 fix: Thread agentId through record* methods (P1-3 from review)
+ recordFileAccess(path, agentId = null) {
const name = path.split('/').pop() || path;
- const nodeKey = this.addNode('file', path, name);
+ const nodeKey = this.addNode('file', path, name, {}, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, nodeKey, 'accessed', 0.5);
+ this.addEdge(sessionKey, nodeKey, 'accessed', 0.5, agentId);
// Extract file extension edge
const ext = path.split('.').pop();
@@ -15023,10 +16426,10 @@ class MemoryGraphModule {
}
}
- recordToolCall(toolName, success) {
- const nodeKey = this.addNode('tool', toolName, toolName);
+ recordToolCall(toolName, success, agentId = null) {
+ const nodeKey = this.addNode('tool', toolName, toolName, {}, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8);
+ this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8, agentId);
}
recordConcept(concept, filePath) {
@@ -15036,15 +16439,16 @@ class MemoryGraphModule {
this.addEdge(fileKey, conceptKey, 'tagged_as', 0.7);
}
+ // v4.5.88: recordTask now passes agentId for namespacing + adds source_session link
recordTask(task, result, agentId) {
const taskId = `task-${Date.now()}`;
const taskKey = this.addNode('task', taskId, task.substring(0, 60), {
full: task,
result: result?.substring(0, 100),
agent: agentId
- });
+ }, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, taskKey, 'executed', 0.6);
+ this.addEdge(sessionKey, taskKey, 'executed', 0.6, agentId);
// Extract keywords as concepts
const words = task.toLowerCase().split(/\s+/).filter(w => w.length > 3);
@@ -15103,18 +16507,38 @@ class MemoryGraphModule {
}
}
- return lines.join('\\n');
+ return lines.join('\n');
}
getContextForPrompt() {
const summary = this.getContextSummary();
if (!summary) return '';
- return `[Memory Graph Context]\\n${summary}\\n`;
+ // Suppress noise: don't inject when graph is nearly empty
+ const totalNodes = Object.keys(this._nodes).length;
+ const totalEdges = this._edges.length;
+ if (totalNodes <= 2 && totalEdges === 0) return '';
+ return `[Memory Graph Context]\n${summary}\n`;
+ }
+
+ // Alias for orchestrator compatibility (orchestrator calls getStats, not showStats)
+ getStats() {
+ const types = {};
+ for (const n of Object.values(this._nodes)) {
+ types[n.type] = (types[n.type] || 0) + 1;
+ }
+ return {
+ nodes: Object.keys(this._nodes).length,
+ edges: this._edges.length,
+ types: types,
+ totalAccesses: this._totalNodeAccesses
+ };
}
// ─── Commands ───
showStatus() {
+ // v4.5.89: Lazy decay — run applyDecay() on status check
+ this.applyDecay();
const types = {};
for (const n of Object.values(this._nodes)) {
types[n.type] = (types[n.type] || 0) + 1;
@@ -15129,6 +16553,8 @@ class MemoryGraphModule {
this.terminal.print(` Edges: ${this._edges.length}`, 'info');
this.terminal.print(` Total accesses: ${this._totalNodeAccesses}`, 'info');
this.terminal.print(` Path: ${this._indexPath}`, 'info');
+ // v4.5.89: Show decay config
+ this.terminal.print(` Decay: ${this._decayDays}d threshold, ${this._decayMinUses} min uses, rate ${this._decayRate}/day`, 'text-muted');
}
search(query) {
@@ -15248,6 +16674,181 @@ class MemoryGraphModule {
this.terminal.print(`🧹 Pruned ${pruned} edges and ${nodesPruned} orphan nodes`, nodesPruned > 0 ? 'warning' : 'info');
}
+ // v4.5.89: Apply decay scores to nodes based on age and access patterns
+ applyDecay() {
+ const now = Date.now();
+ if (now - this._lastDecayRun < 3600000) return;
+ this._lastDecayRun = now;
+
+ // v4.5.89b fix: Compute decay rate from _decayDays (P0 from review)
+ const effectiveRate = 0.9 / this._decayDays;
+ let decayed = 0;
+ let removed = 0;
+
+ for (const key of Object.keys(this._nodes)) {
+ const node = this._nodes[key];
+ const ageDays = (now - new Date(node.updated || node.created).getTime()) / 86400000;
+ node.decay_score = Math.max(0, 1.0 - (ageDays * effectiveRate));
+ if (node.decay_score < 0.1 && node.access_count < this._decayMinUses) {
+ if (node.type === 'session') continue;
+ if (node.namespace && node.namespace.startsWith('agent:')) continue;
+ delete this._nodes[key];
+ removed++;
+ continue;
+ }
+ decayed++;
+ }
+
+ this._edges = this._edges.filter(e => {
+ const ageDays = (now - new Date(e.last_used).getTime()) / 86400000;
+ e.decay_score = Math.max(0, 1.0 - (ageDays * effectiveRate));
+ return !(e.decay_score < 0.1 && (e.use_count || 1) < this._decayMinUses);
+ });
+
+ if (removed > 0) {
+ this._debounceSave();
+ console.log('[MemoryGraph] Decay: ' + decayed + ' scored, ' + removed + ' auto-removed');
+ }
+
+ // v4.5.90: Check for stale skills
+ var staleSkills = this.getStaleSkills();
+ if (staleSkills.length > 0) {
+ this.terminal.print('🧠 Stale skills: ' + staleSkills.length + ' (' + staleSkills.map(function(s) { return s.name; }).join(', ') + ')', 'warning');
+ }
+ }
+
+ // v4.5.90: Record skill usage for decay tracking
+ recordSkillUse(name, success) {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ if (!index[name]) index[name] = { useCount: 0, successCount: 0 };
+ index[name].lastUsed = new Date().toISOString();
+ index[name].useCount = (index[name].useCount || 0) + 1;
+ if (success !== false) {
+ index[name].successCount = (index[name].successCount || 0) + 1;
+ }
+ fs.fsWriteJSON('/skills/index.json', index);
+ } catch (e) {
+ console.warn('[MemoryGraph] Skill use tracking error:', e.message);
+ }
+ }
+
+ // v4.5.90: Get stale skills (unused >_decayDays, useCount == 0, not immune)
+ getStaleSkills() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return [];
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const now = Date.now();
+ // v4.5.90b fix: Use configurable _decayDays (P1 from review)
+ const staleThreshold = this._decayDays * 24 * 60 * 60 * 1000;
+ var stale = [];
+ for (var name of Object.keys(index)) {
+ var stats = index[name] || {};
+ if (stats.immune) continue;
+ var useCount = stats.useCount || 0;
+ var lastUsed = stats.lastUsed ? new Date(stats.lastUsed).getTime() : 0;
+ if (useCount === 0 && (now - lastUsed > staleThreshold || lastUsed === 0)) {
+ var daysUnused = lastUsed > 0 ? Math.floor((now - lastUsed) / 86400000) : 999;
+ stale.push({ name: name, daysUnused: daysUnused });
+ }
+ }
+ return stale;
+ } catch (e) {
+ return [];
+ }
+ }
+
+ // v4.5.89: Configure decay policy
+ decayConfig(args) {
+ if (args.length < 2) {
+ this.terminal.print('Usage: memgraph-decay-config <days> <min_uses>', 'error');
+ this.terminal.print(' Current: ' + this._decayDays + 'd threshold, ' + this._decayMinUses + ' min uses', 'info');
+ this.terminal.print(' Decay rate: ' + this._decayRate + '/day (0.01 = 1% per day)', 'info');
+ return;
+ }
+ const days = parseInt(args[0]);
+ const minUses = parseInt(args[1]);
+ if (isNaN(days) || days < 1) return this.terminal.print('Days must be >= 1', 'error');
+ if (isNaN(minUses) || minUses < 0) return this.terminal.print('Min uses must be >= 0', 'error');
+ this._decayDays = days;
+ this._decayMinUses = minUses;
+ this._debounceSave();
+ this.terminal.print('✅ Decay config: ' + days + 'd threshold, ' + minUses + ' min uses', 'success');
+ }
+
+ learn(args) {
+ if (!args || args.length < 2) return this.terminal.print('Usage: memgraph-learn <entity> <fact>', 'error');
+ const entity = args[0];
+ const fact = args.slice(1).join(' ');
+ this.addNode('concept', entity, entity);
+ const factKey = this.addNode('fact', entity + '-' + Date.now(), fact);
+ const conceptKey = `concept:${entity}`;
+ this.addEdge(conceptKey, factKey, 'has_fact', 0.9);
+ this.terminal.print(`✅ Learned: ${entity} → ${fact}`, 'success');
+ // v4.5.89b fix: Index only the new fact, not all facts (P2 from review — O(n) → O(1))
+ const nvfs = this.terminal.moduleManager.modules['NeuralVFSModule'];
+ if (nvfs && typeof nvfs.indexGraphFacts === 'function') {
+ nvfs.indexSingleFact(factKey, entity, fact);
+ }
+ }
+
+ // v4.5.88: Show graph nodes for a specific session
+ showSession(dateStr) {
+ let sessionKey;
+ if (dateStr) {
+ const d = new Date(dateStr);
+ if (isNaN(d.getTime())) return this.terminal.print('Usage: memgraph-session [YYYY-MM-DD]', 'error');
+ sessionKey = `session-${Math.floor(d.getTime() / 86400000)}`;
+ } else {
+ sessionKey = `session-${Math.floor(Date.now() / 86400000)}`;
+ }
+
+ const sessionNodes = Object.values(this._nodes).filter(function(n) {
+ return n.source_session === sessionKey || n.type === 'session' && n.id.includes(sessionKey);
+ });
+
+ this.terminal.print('📅 Session Graph: ' + sessionKey, 'header');
+ if (sessionNodes.length === 0) {
+ this.terminal.print(' No nodes found for this session', 'info');
+ return;
+ }
+ this.terminal.print(' ' + sessionNodes.length + ' nodes:', 'info');
+ for (var n of sessionNodes.slice(0, 15)) {
+ var ns = n.namespace || 'global';
+ this.terminal.print(' ' + n.type + ':' + n.label + ' (' + ns + ', ' + n.access_count + 'x)', 'info');
+ }
+ var sessionEdges = this._edges.filter(function(e) {
+ return sessionNodes.some(function(n) { return n.id === e.source || n.id === e.target; });
+ });
+ this.terminal.print(' ' + sessionEdges.length + ' edges', 'info');
+ }
+
+ // v4.5.88: Show namespaced nodes for a specific agent
+ showAgent(agentId) {
+ if (!agentId) return this.terminal.print('Usage: memgraph-agent <agentId>', 'error');
+ var ns = 'agent:' + agentId;
+ var agentNodes = Object.values(this._nodes).filter(function(n) {
+ return n.namespace === ns;
+ });
+ var agentEdges = this._edges.filter(function(e) {
+ return (e.source_agents || []).includes(agentId);
+ });
+
+ this.terminal.print('🤖 Agent Memory: ' + agentId, 'header');
+ if (agentNodes.length === 0) {
+ this.terminal.print(' No namespaced nodes for this agent', 'info');
+ this.terminal.print(' (Agent may have used global namespace)', 'text-muted');
+ return;
+ }
+ this.terminal.print(' ' + agentNodes.length + ' nodes, ' + agentEdges.length + ' edges:', 'info');
+ for (var n of agentNodes.slice(0, 15)) {
+ this.terminal.print(' ' + n.type + ':' + n.label + ' (' + n.access_count + 'x)', 'info');
+ }
+ }
+
// ─── Persistence ───
_loadFromVFS() {
@@ -15259,18 +16860,48 @@ class MemoryGraphModule {
const exportData = fs.fsReadJSON(`${this._indexPath}export.json`);
if (exportData && exportData.nodes) {
for (const n of exportData.nodes) {
+ // v4.5.89 fix: Migrate old-format nodes (P1-1 from review)
+ if (!n.namespace) n.namespace = 'global';
+ if (!n.source_session) {
+ const createdMs = new Date(n.created).getTime();
+ n.source_session = isNaN(createdMs) ? null : `session-${Math.floor(createdMs / 86400000)}`;
+ }
this._nodes[n.id] = n;
}
this._edges = exportData.edges || [];
+ for (const e of this._edges) {
+ if (!e.source_agents) e.source_agents = [];
+ }
this._totalNodeAccesses = (exportData.stats?.totalAccesses) || 0;
return;
}
// Fallback: load nodes and edges separately
const nodes = fs.fsReadJSON(`${this._indexPath}nodes.json`);
- if (nodes) this._nodes = nodes;
+ if (nodes) {
+ this._nodes = nodes;
+ for (const n of Object.values(this._nodes)) {
+ if (!n.namespace) n.namespace = 'global';
+ if (!n.source_session) {
+ const createdMs = new Date(n.created).getTime();
+ n.source_session = isNaN(createdMs) ? null : `session-${Math.floor(createdMs / 86400000)}`;
+ }
+ }
+ }
const edges = fs.fsReadJSON(`${this._indexPath}edges.json`);
- if (edges) this._edges = edges;
+ if (edges) {
+ this._edges = edges;
+ for (const e of this._edges) {
+ if (!e.source_agents) e.source_agents = [];
+ }
+ }
+ // v4.5.89b fix: Load persisted decay config (P1 from review)
+ const config = fs.fsReadJSON(`${this._indexPath}config.json`);
+ if (config) {
+ if (typeof config.decayDays === 'number') this._decayDays = config.decayDays;
+ if (typeof config.decayMinUses === 'number') this._decayMinUses = config.decayMinUses;
+ if (typeof config.lastDecayRun === 'number') this._lastDecayRun = config.lastDecayRun;
+ }
} catch (e) {
console.warn('[MemoryGraph] Load error (fresh start):', e.message);
}
@@ -15285,6 +16916,12 @@ class MemoryGraphModule {
}
fs.fsWriteJSON(`${this._indexPath}nodes.json`, this._nodes);
fs.fsWriteJSON(`${this._indexPath}edges.json`, this._edges);
+ // v4.5.89b fix: Persist decay config (P1 from review)
+ fs.fsWriteJSON(`${this._indexPath}config.json`, {
+ decayDays: this._decayDays,
+ decayMinUses: this._decayMinUses,
+ lastDecayRun: this._lastDecayRun
+ });
} catch (e) {
console.error('[MemoryGraph] Save error:', e.message);
}
@@ -15595,9 +17232,14 @@ class WorldModelModule {
this.terminal = null;
this._model = null;
this._lastRefresh = null;
- this._REFRESH_INTERVAL = 300000; // 5 minutes
+ this._REFRESH_INTERVAL = 300000; // 5 minutes (fallback)
this._MAX_PROJECTS = 10;
this._refreshTimer = null;
+ // v4.5.89: Event-driven sync state
+ this._syncDebounce = null;
+ this._syncing = false;
+ this._lastEventSync = 0;
+ this._EVENT_DEBOUNCE_MS = 500;
}
getHelpInfo() {
@@ -15639,6 +17281,25 @@ class WorldModelModule {
this.terminal.print('🌐 World Model loaded — system self-awareness active', 'info');
}
+ // v4.5.89: Event-driven sync — debounced, recursion-guarded
+ _scheduleSync() {
+ if (this._syncing) return; // Prevent recursion during active synthesis
+ if (this._syncDebounce) clearTimeout(this._syncDebounce);
+ const now = Date.now();
+ // Skip if we synced < 2s ago (rate limit beyond debounce)
+ if (now - this._lastEventSync < 2000) return;
+ this._syncDebounce = setTimeout(() => {
+ this._syncDebounce = null;
+ this._syncing = true;
+ try {
+ this._synthesize();
+ this._lastEventSync = Date.now();
+ } finally {
+ this._syncing = false;
+ }
+ }, this._EVENT_DEBOUNCE_MS);
+ }
+
// ─── Synthesis Engine ───
_synthesize() {
@@ -15707,9 +17368,10 @@ class WorldModelModule {
try {
const mm = this.terminal?.moduleManager;
if (!mm) return 0;
- // Count registry entries (all known modules) vs loaded
- const registry = mm.moduleRegistry || [];
- return registry.length || Object.keys(mm.modules || {}).length;
+ // Use max of registry vs loaded so the ratio is never >1 (e.g. 43/7 → 43/43)
+ const loadedCount = Object.keys(mm.modules || {}).length;
+ const registryCount = (mm.moduleRegistry || []).length;
+ return Math.max(loadedCount, registryCount);
} catch (e) {
console.warn('[WorldModel] Total modules gather error:', e.message);
return Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0;
@@ -15717,10 +17379,10 @@ class WorldModelModule {
}
_gatherSystemInfo(fs, moduleCount, version) {
- // Get real health data from ServerMonitorModule's _latencyLog and AI assistant's _errorLog
+ // Get real health data from ServerMonitorModule's _latencyLog and DebugSupervisorModule
const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
- const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule'];
- const errorCount = ai?._errorLog?.length || 0;
+ const ds = this.terminal?.moduleManager?.modules?.['DebugSupervisorModule'];
+ const errorCount = ds ? Object.keys(ds._errorDB?.errors || {}).length : 0;
const latencyErrors = sm?._latencyLog ? Object.values(sm._latencyLog).filter(e => e.status === 'error').length : 0;
const totalErrors = errorCount + latencyErrors;
return {
@@ -15816,11 +17478,11 @@ class WorldModelModule {
_gatherTelemetry(fs) {
try {
- // Get real latency data from ServerMonitorModule and errors from AI assistant
+ // Get real latency data from ServerMonitorModule and errors from DebugSupervisorModule
const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
- const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule'];
+ const ds = this.terminal?.moduleManager?.modules?.['DebugSupervisorModule'];
const latencyDataPoints = sm?._latencyLog ? Object.keys(sm._latencyLog).length : 0;
- const recentErrors = ai?._errorLog?.length || 0;
+ const recentErrors = ds ? Object.keys(ds._errorDB?.errors || {}).length : 0;
return {
toolLatencyDataPoints: latencyDataPoints,
recentErrors: recentErrors,
@@ -15940,41 +17602,44 @@ class WorldModelModule {
if (!question) return this.terminal.print('Usage: world-model-ask <question>', 'error');
if (!this._model) return this.terminal.print('No model — run world-model-refresh first', 'warning');
- const q = question.toLowerCase();
+ const answer = this._answerQuery(question);
+ this.terminal.print(`❓ ${question}`, 'info');
+ this.terminal.print(`💡 ${answer}`, 'success');
+ }
+
+ // Shared query routing — used by both ask() (terminal) and world_model_query (AI tool)
+ _answerQuery(question) {
+ if (!this._model) return 'World model not synthesized yet — run world-model-refresh';
+ const q = (question || '').toLowerCase();
const m = this._model;
- let answer = '';
if (q.includes('module') || q.includes('loaded')) {
- answer = `Loaded ${m.modules.loaded} modules (${m.modules.modifiedCount} modified: ${m.modules.modified.join(', ') || 'none'})`;
+ return `Loaded ${m.modules.loaded} modules (${m.modules.modifiedCount} modified: ${m.modules.modified.join(', ') || 'none'})`;
} else if (q.includes('skill')) {
- answer = `Installed ${m.skills.count} skills: ${m.skills.skills.join(', ') || 'none'}`;
+ return `Installed ${m.skills.count} skills: ${m.skills.skills.join(', ') || 'none'}`;
} else if (q.includes('file') || q.includes('vfs')) {
- answer = `${m.files.total} files in ${m.files.directories} directories, total ${m.files.size}`;
+ return `${m.files.total} files in ${m.files.directories} directories, total ${m.files.size}`;
} else if (q.includes('agent') || q.includes('orchestrat')) {
- answer = `${m.agents.agents} agents (${m.agents.active} active)`;
+ return `${m.agents.agents} agents (${m.agents.active} active)`;
} else if (q.includes('health') || q.includes('status')) {
- answer = `Health: ${m.system.health} · Memory: ${m.system.memory} · ${m.telemetry.recentErrors > 0 ? `${m.telemetry.recentErrors} recent errors` : 'No errors'}`;
+ return `Health: ${m.system.health} · Memory: ${m.system.memory} · ${m.telemetry.recentErrors > 0 ? `${m.telemetry.recentErrors} recent errors` : 'No errors'}`;
} else if (q.includes('memory') || q.includes('graph')) {
- answer = `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`;
+ return `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`;
} else if (q.includes('change') || q.includes('yesterday') || q.includes('recent')) {
const recent = m.recent;
- answer = recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected';
+ return recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected';
} else if (q.includes('project') || q.includes('work')) {
const projects = m.projects.activeProjects;
- answer = projects.length > 0
+ return projects.length > 0
? `Active projects: ${projects.map(p => `${p.name} (${p.accesses}x)`).join(', ')}`
: 'No active projects detected';
} else if (q.includes('telemetry') || q.includes('latency')) {
- answer = `${m.telemetry.toolLatencyDataPoints} tool latency data points tracked`;
+ return `${m.telemetry.toolLatencyDataPoints} tool latency data points tracked, ${m.telemetry.recentErrors} recent errors`;
} else if (q.includes('summary') || q.includes('overview') || q.includes('all')) {
- answer = this.showSummary();
- return;
+ return `newOS4 ${m.version} · ${m.modules.loaded}/${m.modules.total} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents · ${m.memory.nodes} graph nodes · ${m.telemetry.recentErrors > 0 ? `⚠️ ${m.telemetry.recentErrors} errors` : '✅ OK'}`;
} else {
- answer = `I'm not sure how to answer "${question}". Try: modules, skills, files, agents, health, memory, changes, projects, telemetry, or summary.`;
+ return `Unknown query. Try: modules, skills, files, agents, health, memory, changes, projects, telemetry, or summary.`;
}
-
- this.terminal.print(`❓ ${question}`, 'info');
- this.terminal.print(`💡 ${answer}`, 'success');
}
refresh() {
@@ -16017,7 +17682,7 @@ class WorldModelModule {
const agentStr = m.agents.error
? 'agents: N/A'
: `${m.agents.agents} agents`;
- return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
+ return `[World Model Context]\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
}
// ─── Persistence ───
@@ -16036,10 +17701,20 @@ class WorldModelModule {
}
}
+ // v4.5.89: Atomic write — temp file + rename to prevent corruption
_saveToVFS(fs) {
try {
if (!fs.readDir('/system')) fs.mkdir('system');
- fs.fsWriteJSON('/system/world-model.json', this._model);
+ // v4.5.89b fix: Clean stale temp from previous crash (P1 from review)
+ try { fs.deleteFile('/system/world-model.tmp.json'); } catch (_) {}
+ fs.fsWriteJSON('/system/world-model.tmp.json', this._model);
+ const tmp = fs.fsReadJSON('/system/world-model.tmp.json');
+ if (tmp) {
+ fs.fsWriteJSON('/system/world-model.json', tmp);
+ fs.deleteFile('/system/world-model.tmp.json');
+ } else {
+ console.error('[WorldModel] Save failed: temp file corrupt after write');
+ }
} catch (e) {
console.error('[WorldModel] Save error:', e.message);
}
@@ -16087,7 +17762,7 @@ class NeuralVFSModule {
this._loadAliases();
// Deep Indexing toggle
this._deepIndexEnabled = localStorage.getItem('webx11_nvfs_deep') === 'true';
- this._deepModel = localStorage.getItem('webx11_nvfs_deep_model') || 'ministral-3:3b-cloud';
+ this._deepModel = localStorage.getItem('webx11_nvfs_deep_model') || 'gemma4:31b-cloud';
this._deepIndexing = false;
this._deepQueue = [];
this._deepProcessed = 0;
@@ -16134,6 +17809,7 @@ class NeuralVFSModule {
this.commands['nvfs-sync'] = async (args) => this.nvfsSync(args);
this.commands['nvfs-save'] = async (args) => this.nvfsSave();
this.commands['nvfs-cancel'] = (args) => this.cancelIndex();
+ this.commands['nvfs-index-graph'] = () => this.indexGraphFacts();
}
// ── Core Index Builder (async, chunked) ──
@@ -16306,12 +17982,6 @@ class NeuralVFSModule {
resolve(args) {
if (!args[0]) return this.terminal.print('Usage: nvfs-resolve <concept>', 'warning');
- // Auto-sync dirty paths before resolving
- if (this._dirtyPaths.size > 0) {
- this.terminal.print('🔄 Auto-syncing ' + this._dirtyPaths.size + ' dirty file(s) before resolve...', 'info');
- this._reindexDirtyPaths();
- }
-
const concept = args.join(' ').toLowerCase();
// Check aliases first
@@ -17104,6 +18774,61 @@ Concepts:`;
}
}
}
+
+ // v4.5.89: Semantic bridge — mirror graph facts into VFS for unified search
+ indexGraphFacts() {
+ const mg = this.terminal.moduleManager.modules['MemoryGraphModule'];
+ if (!mg) return this.terminal.print('❌ Memory Graph module not loaded', 'error');
+ const fs = this.terminal.fs;
+ if (!fs) return this.terminal.print('❌ VFS not available', 'error');
+
+ const factNodes = Object.values(mg._nodes).filter(function(n) {
+ return n.type === 'concept' || n.type === 'fact' || n.type === 'entity' || n.type === 'task';
+ });
+
+ if (factNodes.length === 0) {
+ return this.terminal.print('No graph fact nodes to index. Use memgraph-learn first.', 'info');
+ }
+
+ if (!fs.readDir('/_graph_facts')) fs.mkdir('_graph_facts');
+
+ // v4.5.89b fix: Clean stale files for deleted nodes (P2 from review)
+ const validKeys = new Set(factNodes.map(function(n) { return n.id.replace(/[^a-zA-Z0-9_-]/g, '_') + '.txt'; }));
+ try {
+ const existing = fs.readDir('/_graph_facts') || [];
+ for (const f of existing) {
+ if (!validKeys.has(f)) fs.deleteFile('/_graph_facts/' + f);
+ }
+ } catch (_) {}
+
+ let indexed = 0;
+ for (const node of factNodes) {
+ let content = node.label + '\n';
+ if (node.metadata) {
+ if (node.metadata.full) content += node.metadata.full + '\n';
+ if (node.metadata.result) content += 'Result: ' + node.metadata.result + '\n';
+ }
+ content += 'Type: ' + node.type + '\n';
+ content += 'Namespace: ' + (node.namespace || 'global') + '\n';
+ if (node.source_session) content += 'Session: ' + node.source_session + '\n';
+ const safeName = node.id.replace(/[^a-zA-Z0-9_-]/g, '_');
+ fs.writeFile('/_graph_facts/' + safeName + '.txt', content);
+ indexed++;
+ }
+
+ this.terminal.print('🔗 Indexed ' + indexed + ' graph facts into /_graph_facts/', 'success');
+ this.terminal.print(' Run nvfs-index to include them in semantic search', 'info');
+ }
+
+ // v4.5.89b: Index a single fact — O(1) instead of O(n) re-index (P2 from review)
+ indexSingleFact(factKey, entity, fact) {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ if (!fs.readDir('/_graph_facts')) fs.mkdir('_graph_facts');
+ const safeName = factKey.replace(/[^a-zA-Z0-9_-]/g, '_');
+ const content = entity + '\n' + fact + '\nType: fact\nNamespace: global\n';
+ fs.writeFile('/_graph_facts/' + safeName + '.txt', content);
+ }
}
// Required for module system
@@ -17124,6 +18849,7 @@ if (typeof window !== 'undefined') {
'contexts': 'contexts — List all available context profiles',
'context-new': 'context-new <name> <displayName> <systemPrompt> — Create a new context profile',
'context-delete': 'context-delete <name> — Delete a context profile',
+ 'context-auto': 'context-auto [on|off] — Toggle auto-routing (detects intent from user input)',
'smart-ask': 'smart-ask <question> — Ask a question using the active context'
};
}
@@ -17169,6 +18895,7 @@ if (typeof window !== 'undefined') {
this.commands['contexts'] = () => this.listContexts();
this.commands['context-new'] = (args) => this.createContext(args);
this.commands['context-delete'] = (args) => this.deleteContext(args);
+ this.commands['context-auto'] = (args) => this.toggleAutoRouting(args);
this.commands['smart-ask'] = (args) => this.smartAsk(args);
}
setContext(args) {
@@ -17238,6 +18965,116 @@ if (typeof window !== 'undefined') {
this.terminal.print(`Profile '${args[0]}' not found`, "error");
}
}
+ // ─── Intent Detection Engine ───
+ _getIntentPatterns() {
+ return {
+ 'coding': [
+ /^code\b/i, /\b(edit|fix|write|create|implement|refactor|build)\s+(module|function|class|file|code|script)/i,
+ /\b(debug|bug|error|fix)\s+(code|module|function|script)/i, /\b(implement|write|create)\s+(a\s+)?(function|class|module|command)/i,
+ /\b(refactor|rewrite|optimize)\s+(code|module|function)/i, /\b(add\s+)(feature|command|functionality)/i
+ ],
+ 'debug': [
+ /^debug\b/i, /\b(error|crash|bug|fail|traceback|exception|broken|not working)\b/i,
+ /\b(why|what happened|diagnose|investigate|find out why)\b/i, /\b(check|inspect|examine)\s+(log|error|issue|problem)/i
+ ],
+ 'research': [
+ /^research\b/i, /\b(search|find|look up|research|investigate|analyze)\b/i,
+ /\b(what is|how does|tell me about|explain)\b.*\?$/i, /\b(find|search)\s+(for\s+)?(information|docs|documentation|article)/i
+ ],
+ 'creative': [
+ /^brainstorm\b/i, /\b(create|design|imagine|generate|compose|draft)\s+(story|poem|art|design|idea|concept|mindmap)/i,
+ /\b(creative|inspire|innovative|novel)\b/i
+ ],
+ 'system-admin': [
+ /^(config|setup|install|deploy|backup|restore|security|monitor|maintenance)\b/i,
+ /\b(configure|setup|install|deploy|backup|restore)\s+(system|server|module|service)/i,
+ /\b(security|permission|access|block|allow)\b/i
+ ],
+ 'chat': [
+ /^(hi|hello|hey|how are you|what's up|good morning|good evening)\b/i,
+ /^.{0,30}\?$/ // short questions
+ ]
+ };
+ }
+ autoDetectIntent(text) {
+ if (!text || typeof text !== 'string') return { profile: null, confidence: 0 };
+ const patterns = this._getIntentPatterns();
+ const scores = {};
+ const lower = text.toLowerCase();
+
+ for (const [profile, regexes] of Object.entries(patterns)) {
+ let matches = 0;
+ for (const re of regexes) {
+ if (re.test(text) || re.test(lower)) matches++;
+ }
+ if (matches > 0) scores[profile] = matches;
+ }
+
+ // Code snippet detection (bias toward coding)
+ if (/[{([\s]*['"`]/.test(text) || /\b(function|class|const|let|var|import|export|=>)\b/.test(text)) {
+ scores['coding'] = (scores['coding'] || 0) + 2;
+ }
+
+ // Very short text → chat
+ if (text.length < 15 && !scores['coding'] && !scores['debug']) {
+ scores['chat'] = (scores['chat'] || 0) + 1;
+ }
+
+ // Find best match
+ let bestProfile = null;
+ let bestScore = 0;
+ let totalScore = 0;
+ for (const [profile, score] of Object.entries(scores)) {
+ totalScore += score;
+ if (score > bestScore) { bestScore = score; bestProfile = profile; }
+ }
+
+ // Calculate confidence (0-100)
+ const confidence = totalScore > 0 ? Math.round((bestScore / totalScore) * 100) : 0;
+
+ return { profile: bestProfile, confidence };
+ }
+ autoRouteContext(text) {
+ if (!text) return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: 0, reason: 'no input' };
+
+ // Check if auto-routing is enabled
+ const enabled = localStorage.getItem('webx11_context_auto') !== 'off';
+ if (!enabled) return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: 0, reason: 'auto-routing disabled' };
+
+ const result = this.autoDetectIntent(text);
+ if (!result.profile || result.confidence < 70) {
+ return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: result.confidence, reason: 'confidence too low (<70%)' };
+ }
+
+ // Don't switch if already on the best profile
+ if (result.profile === this.activeProfile) {
+ return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: result.confidence, reason: 'already on best profile' };
+ }
+
+ // Check if the target profile exists
+ if (!this.contextProfiles[result.profile]) {
+ return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: result.confidence, reason: 'profile "' + result.profile + '" not found' };
+ }
+
+ // Switch
+ const from = this.activeProfile;
+ this.setContext([result.profile]);
+ return { switched: true, from: from, to: result.profile, confidence: result.confidence, reason: 'intent detected' };
+ }
+ toggleAutoRouting(args) {
+ const val = args && args[0] ? args[0].toLowerCase() : null;
+ if (val === 'on' || val === 'true' || val === '1') {
+ localStorage.setItem('webx11_context_auto', 'on');
+ this.terminal.print('✅ Auto-routing enabled — context will switch based on intent', 'success');
+ } else if (val === 'off' || val === 'false' || val === '0') {
+ localStorage.setItem('webx11_context_auto', 'off');
+ this.terminal.print('⛔ Auto-routing disabled — use context <name> to switch manually', 'warning');
+ } else {
+ const current = localStorage.getItem('webx11_context_auto') !== 'off';
+ this.terminal.print('Auto-routing: ' + (current ? 'ON' : 'OFF'), 'info');
+ this.terminal.print('Usage: context-auto on|off', 'info');
+ }
+ }
async smartAsk(args) {
if (!args[0]) {
this.terminal.print("Usage: smart-ask <your-question>", "warning"); return;
@@ -17285,4 +19122,707 @@ if (typeof window !== 'undefined') {
}
}
+/* === module-debug-supervisor.js === */
+// module-debug-supervisor.js — Persistent error capture, clustering, root-cause analysis, and auto-fix
+// Commands: debug-stats, debug-history, debug-analyze, debug-trends, debug-fix, debug-health
+class DebugSupervisorModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._healthy = true;
+ this._errorDB = { errors: {}, clusters: {}, patterns: {}, sessionId: null };
+ this._maxErrors = 500;
+ this._ttlDays = 7;
+ this._writeDebounce = null;
+ this._pendingWrites = 0;
+ this._originalConsole = {};
+ this._originalOnError = null;
+ this._originalOnRejection = null;
+ this._originalFetch = null;
+ this._interceptorInstalled = false;
+ this._sessionId = this._generateSessionId();
+ }
+
+ _generateSessionId() {
+ try {
+ let sid = localStorage.getItem('webx11_debug_session');
+ if (!sid) {
+ sid = Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8);
+ localStorage.setItem('webx11_debug_session', sid);
+ }
+ return sid;
+ } catch (e) {
+ return Date.now().toString(36);
+ }
+ }
+
+ getHelpInfo() {
+ return {
+ 'debug-stats': 'debug-stats — Show error summary (total, unique, clusters, top issues)',
+ 'debug-history': 'debug-history [n] — Show last N errors (default: 20)',
+ 'debug-analyze': 'debug-analyze <hash> — Deep analysis of a specific error',
+ 'debug-trends': 'debug-trends [days] — Error frequency over time (default: 7)',
+ 'debug-fix': 'debug-fix <hash> — Suggest and optionally apply fix for an error',
+ 'debug-health': 'debug-health — Check supervisor module health status',
+ };
+ }
+
+ registerCommands() {
+ this.commands['debug-stats'] = () => this._cmdStats();
+ this.commands['debug-history'] = (args) => this._cmdHistory(args);
+ this.commands['debug-analyze'] = (args) => this._cmdAnalyze(args);
+ this.commands['debug-trends'] = (args) => this._cmdTrends(args);
+ this.commands['debug-fix'] = (args) => this._cmdFix(args);
+ this.commands['debug-health'] = () => this._cmdHealth();
+ }
+
+ seed() {
+ // seed() is not called automatically by module manager — interceptors go in init()
+ }
+
+ init() {
+ this._loadDB();
+ this._installInterceptors();
+ }
+
+ // ── Error Database ──
+
+ _loadDB() {
+ try {
+ const raw = localStorage.getItem('webx11_debug_db');
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ this._errorDB = parsed;
+ this._pruneExpired();
+ }
+ } catch (e) {
+ // Corrupted DB — start fresh
+ this._errorDB = { errors: {}, clusters: {}, patterns: {}, sessionId: this._sessionId };
+ }
+ }
+
+ _saveDB() {
+ try {
+ localStorage.setItem('webx11_debug_db', JSON.stringify(this._errorDB));
+ } catch (e) {
+ // Quota exceeded — evict oldest 20% and retry
+ this._evictLRU(0.2);
+ try {
+ localStorage.setItem('webx11_debug_db', JSON.stringify(this._errorDB));
+ } catch (e2) {
+ // Still failing — supervisor degrades gracefully
+ console.warn('[DebugSupervisor] DB write failed, errors may be lost');
+ }
+ }
+ }
+
+ _scheduleSave() {
+ this._pendingWrites++;
+ if (this._writeDebounce) return;
+ this._writeDebounce = setTimeout(() => {
+ this._writeDebounce = null;
+ if (this._pendingWrites > 0) {
+ this._saveDB();
+ this._pendingWrites = 0;
+ }
+ }, 2000);
+ }
+
+ _pruneExpired() {
+ const now = Date.now();
+ const maxAge = this._ttlDays * 86400000;
+ let changed = false;
+ for (const [hash, entry] of Object.entries(this._errorDB.errors)) {
+ if (now - entry.lastSeen > maxAge) {
+ delete this._errorDB.errors[hash];
+ changed = true;
+ }
+ }
+ if (changed) this._saveDB();
+ }
+
+ _evictLRU(fraction) {
+ const entries = Object.entries(this._errorDB.errors);
+ const sorted = entries.sort((a, b) => a[1].lastSeen - b[1].lastSeen);
+ const toRemove = Math.ceil(sorted.length * fraction);
+ for (let i = 0; i < toRemove; i++) {
+ delete this._errorDB.errors[sorted[i][0]];
+ }
+ }
+
+ _checkQuota() {
+ try {
+ const total = 0;
+ for (const key in localStorage) {
+ // Estimate: key + value length
+ }
+ // Simple check: try writing a test key
+ const testKey = '__quota_test__';
+ localStorage.setItem(testKey, '1');
+ localStorage.removeItem(testKey);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ // ── Error Classification ──
+
+ _classifyError(message, source, stack) {
+ const type = this._detectType(message, stack);
+ const severity = this._detectSeverity(type, message);
+ const category = this._detectCategory(message, source, type);
+ return { type, severity, category };
+ }
+
+ _detectType(message, stack) {
+ if (!message) return 'unknown';
+ if (message.includes('ReferenceError') || message.includes('is not defined')) return 'ReferenceError';
+ if (message.includes('TypeError') || message.includes('is not a function') || message.includes('cannot read property')) return 'TypeError';
+ if (message.includes('SyntaxError') || message.includes('Unexpected token')) return 'SyntaxError';
+ if (message.includes('HTTP') || message.includes('NetworkError') || message.includes('Failed to fetch')) return 'HTTP';
+ if (message.includes('timeout') || message.includes('Timeout') || message.includes('AbortError')) return 'timeout';
+ if (message.includes('QuotaExceededError') || message.includes('quota')) return 'quota';
+ if (message.includes('SecurityError') || message.includes('permission')) return 'security';
+ if (stack && stack.includes('tool_')) return 'tool';
+ return 'runtime';
+ }
+
+ _detectSeverity(type, message) {
+ if (type === 'SyntaxError' || type === 'ReferenceError' || type === 'quota') return 'fatal';
+ if (type === 'HTTP' && message.includes('5')) return 'error';
+ if (type === 'timeout' || type === 'security') return 'error';
+ if (type === 'tool') return 'warning';
+ return 'error';
+ }
+
+ _detectCategory(message, source, type) {
+ if (type === 'HTTP' || type === 'timeout') return 'network';
+ if (type === 'quota') return 'vfs';
+ if (type === 'security') return 'security';
+ if (source && (source.includes('module-') || source.includes('ext/'))) return 'module';
+ if (type === 'tool') return 'tool';
+ return 'runtime';
+ }
+
+ _extractModule(source) {
+ if (!source) return null;
+ const m = source.match(/(module-[a-z-]+)\.js/);
+ return m ? m[1] : null;
+ }
+
+ _computeHash(message, source, type) {
+ const raw = (message || '') + '|' + (source || '') + '|' + (type || '');
+ // Simple hash (Fowler-Noll-Vo)
+ let hash = 0x811c9dc5;
+ for (let i = 0; i < raw.length; i++) {
+ hash ^= raw.charCodeAt(i);
+ hash = Math.imul(hash, 0x01000193);
+ }
+ return (hash >>> 0).toString(16);
+ }
+
+ // ── Error Interception ──
+
+ _installInterceptors() {
+ if (this._interceptorInstalled) return;
+ const self = this;
+
+ // Console.error
+ this._originalConsole.error = console.error;
+ console.error = function() {
+ const args = Array.prototype.slice.call(arguments);
+ const msg = args.map(a => typeof a === 'object' ? (a.message || JSON.stringify(a)) : String(a)).join(' ');
+ self._recordError(msg, 'console.error', null);
+ return self._originalConsole.error.apply(console, args);
+ };
+
+ // window.onerror
+ this._originalOnError = window.onerror;
+ window.onerror = function(msg, src, line, col, err) {
+ const fullMsg = msg + (err ? ' — ' + err.message : '') + ' (' + src + ':' + line + ':' + col + ')';
+ self._recordError(fullMsg, src || 'window.onerror', err ? err.stack : null);
+ if (self._originalOnError) return self._originalOnError.call(window, msg, src, line, col, err);
+ };
+
+ // Unhandled rejections
+ this._originalOnRejection = window.onunhandledrejection;
+ window.addEventListener('unhandledrejection', function(e) {
+ const msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled Promise rejection';
+ self._recordError(msg, 'unhandledrejection', e.reason ? e.reason.stack : null);
+ });
+
+ // Fetch failures
+ this._originalFetch = window.fetch;
+ window.fetch = function() {
+ const args = arguments;
+ const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '?');
+ return self._originalFetch.apply(window, args).catch(function(err) {
+ self._recordError('Fetch failed: ' + url + ' — ' + (err.message || err), url, err.stack);
+ throw err;
+ });
+ };
+
+ this._interceptorInstalled = true;
+ }
+
+ // ── Record Error ──
+
+ recordToolError(toolName, args, errorMessage) {
+ this._recordError(errorMessage, 'tool:' + toolName, null);
+ }
+
+ _recordError(message, source, stack) {
+ if (!this._healthy) return;
+ const classification = this._classifyError(message, source, stack);
+ const hash = this._computeHash(message, source, classification.type);
+ const now = Date.now();
+
+ if (this._errorDB.errors[hash]) {
+ this._errorDB.errors[hash].count++;
+ this._errorDB.errors[hash].lastSeen = now;
+ if (this._errorDB.errors[hash].sessions.indexOf(this._sessionId) === -1) {
+ this._errorDB.errors[hash].sessions.push(this._sessionId);
+ }
+ } else {
+ // LRU eviction if at cap
+ const keys = Object.keys(this._errorDB.errors);
+ if (keys.length >= this._maxErrors) {
+ this._evictLRU(0.1);
+ }
+ this._errorDB.errors[hash] = {
+ count: 1,
+ firstSeen: now,
+ lastSeen: now,
+ message: message,
+ type: classification.type,
+ severity: classification.severity,
+ category: classification.category,
+ source: source,
+ stack: stack ? stack.split('\n').slice(0, 10).join('\n') : null,
+ module: this._extractModule(source),
+ sessions: [this._sessionId],
+ };
+ }
+
+ this._scheduleSave();
+ }
+
+ // ── Clustering (Jaccard similarity) ──
+
+ _normalizeMessage(msg) {
+ if (!msg) return '';
+ return msg
+ .toLowerCase()
+ .replace(/\b0x[a-f0-9]+\b/g, '0xHEX')
+ .replace(/\b\d+\b/g, 'N')
+ .replace(/["'].*?["']/g, '"STR"')
+ .replace(/[^\w\s]/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+ }
+
+ _jaccardSimilarity(a, b) {
+ const setA = new Set(a.split(' ').filter(Boolean));
+ const setB = new Set(b.split(' ').filter(Boolean));
+ if (setA.size === 0 && setB.size === 0) return 1;
+ let intersection = 0;
+ for (const token of setA) {
+ if (setB.has(token)) intersection++;
+ }
+ const union = setA.size + setB.size - intersection;
+ return union === 0 ? 0 : intersection / union;
+ }
+
+ _clusterErrors() {
+ const errors = Object.entries(this._errorDB.errors);
+ if (errors.length === 0) return [];
+
+ const clusters = [];
+ const assigned = new Set();
+
+ for (let i = 0; i < errors.length; i++) {
+ if (assigned.has(i)) continue;
+ const [hashI, entryI] = errors[i];
+ const normI = this._normalizeMessage(entryI.message);
+ const cluster = { hashes: [hashI], messages: [entryI.message], count: entryI.count, types: new Set([entryI.type]), categories: new Set([entryI.category]) };
+ assigned.add(i);
+
+ for (let j = i + 1; j < errors.length; j++) {
+ if (assigned.has(j)) continue;
+ const [hashJ, entryJ] = errors[j];
+ const normJ = this._normalizeMessage(entryJ.message);
+ if (this._jaccardSimilarity(normI, normJ) >= 0.35) {
+ cluster.hashes.push(hashJ);
+ cluster.messages.push(entryJ.message);
+ cluster.count += entryJ.count;
+ cluster.types.add(entryJ.type);
+ cluster.categories.add(entryJ.category);
+ assigned.add(j);
+ }
+ }
+
+ cluster.label = this._mostFrequent(cluster.messages);
+ cluster.typeCount = cluster.types.size;
+ cluster.categoryCount = cluster.categories.size;
+ delete cluster.types;
+ delete cluster.categories;
+ clusters.push(cluster);
+ }
+
+ clusters.sort((a, b) => b.count - a.count);
+ return clusters;
+ }
+
+ _mostFrequent(arr) {
+ const freq = {};
+ let best = arr[0], bestCount = 1;
+ for (const item of arr) {
+ freq[item] = (freq[item] || 0) + 1;
+ if (freq[item] > bestCount) {
+ best = item;
+ bestCount = freq[item];
+ }
+ }
+ return best;
+ }
+
+ // ── Root-Cause Analysis ──
+
+ _analyzeRootCause(hash) {
+ const entry = this._errorDB.errors[hash];
+ if (!entry) return { found: false };
+
+ const findings = [];
+ const fixes = [];
+
+ // Tool failure → check module
+ if (entry.source && entry.source.startsWith('tool:')) {
+ const toolName = entry.source.replace('tool:', '');
+ const mm = this.terminal?.moduleManager;
+ if (mm) {
+ // Check if any module has this command
+ let found = false;
+ for (const [modName, mod] of Object.entries(mm.modules)) {
+ if (mod.commands && mod.commands[toolName]) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ findings.push({ type: 'module_not_loaded', detail: 'No module provides command "' + toolName + '"' });
+ fixes.push({ action: 'loadmod', params: toolName, description: 'Load module providing ' + toolName });
+ }
+ }
+ }
+
+ // HTTP error → check network
+ if (entry.type === 'HTTP' || entry.type === 'timeout') {
+ findings.push({ type: 'network_issue', detail: 'Network request failed: ' + entry.message.substring(0, 100) });
+ fixes.push({ action: 'test-net', params: null, description: 'Run network diagnostics' });
+ }
+
+ // ReferenceError → check bundle
+ if (entry.type === 'ReferenceError') {
+ findings.push({ type: 'missing_class', detail: 'ReferenceError suggests a class or variable is not defined' });
+ fixes.push({ action: 'reload', params: null, description: 'Reload the page to re-initialize all modules' });
+ }
+
+ // Module error → check module loader
+ if (entry.category === 'module') {
+ findings.push({ type: 'module_error', detail: 'Error in module: ' + (entry.module || 'unknown') });
+ fixes.push({ action: 'loadmod', params: entry.module, description: 'Reload module: ' + entry.module });
+ }
+
+ return { found: findings.length > 0, findings, fixes };
+ }
+
+ // ── Fix Registry ──
+
+ _buildFixSuggestion(hash) {
+ const entry = this._errorDB.errors[hash];
+ if (!entry) return null;
+
+ const rca = this._analyzeRootCause(hash);
+ if (!rca.found) {
+ return { confidence: 0, suggestion: 'No known fix pattern for this error', autoApply: false };
+ }
+
+ // Calculate confidence based on frequency
+ let confidence = Math.min(0.5, entry.count / 10);
+ if (entry.count >= 3) confidence = 0.7;
+ if (entry.count >= 10) confidence = 0.9;
+
+ return {
+ confidence: confidence,
+ suggestion: rca.findings.map(f => f.detail).join('; '),
+ fixes: rca.fixes,
+ autoApply: confidence >= 0.9,
+ };
+ }
+
+ // ── Terminal Commands ──
+
+ _cmdStats() {
+ const db = this._errorDB.errors;
+ const keys = Object.keys(db);
+ if (keys.length === 0) {
+ this.terminal.print('📊 Debug Stats: No errors recorded', 'success');
+ return;
+ }
+
+ const totalCount = keys.reduce((sum, k) => sum + db[k].count, 0);
+ const bySeverity = {};
+ const byCategory = {};
+ for (const k of keys) {
+ const e = db[k];
+ bySeverity[e.severity] = (bySeverity[e.severity] || 0) + e.count;
+ byCategory[e.category] = (byCategory[e.category] || 0) + e.count;
+ }
+
+ const clusters = this._clusterErrors();
+ const topClusters = clusters.slice(0, 5);
+
+ this.terminal.print('📊 Debug Stats', 'header');
+ this.terminal.print(' Total errors: ' + totalCount, 'info');
+ this.terminal.print(' Unique errors: ' + keys.length, 'info');
+ this.terminal.print(' Clusters: ' + clusters.length, 'info');
+ this.terminal.print(' Session: ' + this._sessionId, 'text-muted');
+ this.terminal.print(' Supervisor: ' + (this._healthy ? '✅ healthy' : '🔴 degraded'), this._healthy ? 'success' : 'error');
+
+ this.terminal.print('── By Severity ──', 'info');
+ for (const [sev, count] of Object.entries(bySeverity)) {
+ const icon = sev === 'fatal' ? '🔴' : sev === 'error' ? '🟡' : '🟢';
+ this.terminal.print(' ' + icon + ' ' + sev + ': ' + count, sev === 'fatal' ? 'error' : 'warning');
+ }
+
+ this.terminal.print('── By Category ──', 'info');
+ for (const [cat, count] of Object.entries(byCategory)) {
+ this.terminal.print(' • ' + cat + ': ' + count, 'text-muted');
+ }
+
+ if (topClusters.length > 0) {
+ this.terminal.print('── Top Clusters ──', 'info');
+ for (const c of topClusters) {
+ this.terminal.print(' [' + c.hashes.length + ' errors ×' + c.count + '] ' + c.label.substring(0, 80), 'warning');
+ }
+ this.terminal.print(' Run debug-analyze <hash> for details', 'text-muted');
+ }
+ }
+
+ _cmdHistory(args) {
+ const n = (args && args[0]) ? parseInt(args[0]) : 20;
+ const entries = Object.entries(this._errorDB.errors)
+ .sort((a, b) => b[1].lastSeen - a[1].lastSeen)
+ .slice(0, n);
+
+ if (entries.length === 0) {
+ this.terminal.print('No errors recorded', 'success');
+ return;
+ }
+
+ this.terminal.print('── Error History (last ' + entries.length + ') ──', 'header');
+ for (const [hash, e] of entries) {
+ const time = new Date(e.lastSeen).toLocaleTimeString();
+ const icon = e.severity === 'fatal' ? '🔴' : e.severity === 'error' ? '🟡' : '🟢';
+ const msg = e.message.substring(0, 80);
+ this.terminal.print(' ' + icon + ' [' + time + '] ' + msg, e.severity === 'fatal' ? 'error' : 'text-muted');
+ this.terminal.print(' hash=' + hash + ' type=' + e.type + ' cat=' + e.category + ' ×' + e.count, 'text-muted');
+ }
+ this.terminal.print('── End ──', 'info');
+ this.terminal.print('Run debug-analyze <hash> for details', 'text-muted');
+ }
+
+ _cmdAnalyze(args) {
+ const hash = (args && args[0]) || '';
+ if (!hash) {
+ this.terminal.print('Usage: debug-analyze <hash>', 'warning');
+ return;
+ }
+
+ const entry = this._errorDB.errors[hash];
+ if (!entry) {
+ this.terminal.print('Error not found: ' + hash, 'error');
+ return;
+ }
+
+ this.terminal.print('── Error Analysis: ' + hash + ' ──', 'header');
+ this.terminal.print(' Message: ' + entry.message, 'info');
+ this.terminal.print(' Type: ' + entry.type + ' | Severity: ' + entry.severity + ' | Category: ' + entry.category, 'info');
+ this.terminal.print(' Source: ' + (entry.source || '?'), 'text-muted');
+ this.terminal.print(' Module: ' + (entry.module || 'unknown'), 'text-muted');
+ this.terminal.print(' Count: ' + entry.count + ' | First: ' + new Date(entry.firstSeen).toLocaleString() + ' | Last: ' + new Date(entry.lastSeen).toLocaleString(), 'info');
+ this.terminal.print(' Sessions: ' + (entry.sessions || []).length, 'text-muted');
+ if (entry.stack) {
+ this.terminal.print('── Stack Trace (top 10) ──', 'info');
+ this.terminal.print(entry.stack, 'text-muted');
+ }
+
+ // Root-cause analysis
+ const rca = this._analyzeRootCause(hash);
+ if (rca.found) {
+ this.terminal.print('── Root Cause Analysis ──', 'warning');
+ for (const f of rca.findings) {
+ this.terminal.print(' • [' + f.type + '] ' + f.detail, 'warning');
+ }
+ if (rca.fixes.length > 0) {
+ this.terminal.print('── Suggested Fixes ──', 'success');
+ for (const fix of rca.fixes) {
+ this.terminal.print(' → ' + fix.action + (fix.params ? ' ' + fix.params : '') + ' — ' + fix.description, 'success');
+ }
+ }
+ } else {
+ this.terminal.print(' No root cause identified', 'text-muted');
+ }
+
+ // Fix suggestion
+ const fix = this._buildFixSuggestion(hash);
+ if (fix && fix.confidence > 0) {
+ this.terminal.print('── Auto-Fix Confidence: ' + Math.round(fix.confidence * 100) + '% ──', fix.autoApply ? 'success' : 'warning');
+ this.terminal.print(' ' + fix.suggestion, 'info');
+ if (fix.autoApply) {
+ this.terminal.print(' ⚡ Confidence ≥ 90% — fix can be auto-applied with debug-fix ' + hash, 'success');
+ }
+ }
+ }
+
+ _cmdTrends(args) {
+ const days = (args && args[0]) ? parseInt(args[0]) : 7;
+ const cutoff = Date.now() - days * 86400000;
+
+ const entries = Object.values(this._errorDB.errors).filter(e => e.lastSeen >= cutoff);
+ if (entries.length === 0) {
+ this.terminal.print('No errors in the last ' + days + ' days', 'success');
+ return;
+ }
+
+ // Group by day
+ const byDay = {};
+ for (const e of entries) {
+ const day = new Date(e.lastSeen).toISOString().split('T')[0];
+ if (!byDay[day]) byDay[day] = { total: 0, byType: {} };
+ byDay[day].total += e.count;
+ byDay[day].byType[e.type] = (byDay[day].byType[e.type] || 0) + e.count;
+ }
+
+ const sortedDays = Object.keys(byDay).sort();
+ this.terminal.print('── Error Trends (last ' + days + ' days) ──', 'header');
+ for (const day of sortedDays) {
+ const d = byDay[day];
+ const types = Object.entries(d.byType).map(([t, c]) => t + ':' + c).join(', ');
+ this.terminal.print(' ' + day + ': ' + d.total + ' errors (' + types + ')', d.total > 5 ? 'warning' : 'text-muted');
+ }
+
+ // Top error types
+ const typeTotals = {};
+ for (const e of entries) {
+ typeTotals[e.type] = (typeTotals[e.type] || 0) + e.count;
+ }
+ this.terminal.print('── By Type ──', 'info');
+ for (const [type, count] of Object.entries(typeTotals).sort((a, b) => b[1] - a[1])) {
+ this.terminal.print(' • ' + type + ': ' + count, 'text-muted');
+ }
+ }
+
+ _cmdFix(args) {
+ const hash = (args && args[0]) || '';
+ if (!hash) {
+ this.terminal.print('Usage: debug-fix <hash>', 'warning');
+ return;
+ }
+
+ const fix = this._buildFixSuggestion(hash);
+ if (!fix || fix.confidence === 0) {
+ this.terminal.print('No fix available for error: ' + hash, 'warning');
+ return;
+ }
+
+ this.terminal.print('── Fix Suggestion for ' + hash + ' ──', 'header');
+ this.terminal.print(' Confidence: ' + Math.round(fix.confidence * 100) + '%', fix.autoApply ? 'success' : 'warning');
+ this.terminal.print(' Suggestion: ' + fix.suggestion, 'info');
+
+ if (fix.fixes && fix.fixes.length > 0) {
+ this.terminal.print('── Available Fixes ──', 'info');
+ for (const f of fix.fixes) {
+ this.terminal.print(' → ' + f.action + (f.params ? ' ' + f.params : ''), 'success');
+ this.terminal.print(' ' + f.description, 'text-muted');
+ }
+ }
+
+ if (fix.autoApply) {
+ this.terminal.print('⚡ Auto-applying fix...', 'info');
+ this._applyFix(hash, fix);
+ } else {
+ this.terminal.print('Confidence below auto-apply threshold. Run the suggested command manually.', 'warning');
+ }
+ }
+
+ _applyFix(hash, fix) {
+ if (!fix.fixes || fix.fixes.length === 0) return;
+ const firstFix = fix.fixes[0];
+ try {
+ if (firstFix.action === 'loadmod' && this.terminal) {
+ this.terminal.executeCommand('loadmod ' + (firstFix.params || ''));
+ this.terminal.print(' ✅ Applied: loadmod ' + (firstFix.params || ''), 'success');
+ } else if (firstFix.action === 'test-net' && this.terminal) {
+ this.terminal.executeCommand('test-net');
+ this.terminal.print(' ✅ Applied: test-net', 'success');
+ } else if (firstFix.action === 'reload') {
+ this.terminal.print(' ℹ️ Manual action needed: reload the page', 'warning');
+ } else {
+ this.terminal.print(' ℹ️ Cannot auto-apply: ' + firstFix.action, 'warning');
+ }
+ } catch (e) {
+ this.terminal.print(' ❌ Fix failed: ' + e.message, 'error');
+ }
+ }
+
+ _cmdHealth() {
+ this.terminal.print('── DebugSupervisor Health ──', 'header');
+ this.terminal.print(' Status: ' + (this._healthy ? '✅ Healthy' : '🔴 Degraded'), this._healthy ? 'success' : 'error');
+ this.terminal.print(' Session: ' + this._sessionId, 'text-muted');
+ this.terminal.print(' Errors tracked: ' + Object.keys(this._errorDB.errors).length + ' unique', 'info');
+ this.terminal.print(' Max errors: ' + this._maxErrors, 'text-muted');
+ this.terminal.print(' TTL: ' + this._ttlDays + ' days', 'text-muted');
+ this.terminal.print(' Interceptors: ' + (this._interceptorInstalled ? '✅ active' : '🔴 inactive'), this._interceptorInstalled ? 'success' : 'error');
+ this.terminal.print(' Quota: ' + (this._checkQuota() ? '✅ available' : '🔴 low'), this._checkQuota() ? 'success' : 'error');
+ }
+
+ // ── Dashboard Data ──
+
+ getDashboardData() {
+ const db = this._errorDB.errors;
+ const keys = Object.keys(db);
+ const totalCount = keys.reduce((sum, k) => sum + db[k].count, 0);
+ const clusters = this._clusterErrors().slice(0, 5);
+
+ return {
+ totalUnique: keys.length,
+ totalCount: totalCount,
+ clusters: clusters.map(c => ({ label: c.label.substring(0, 60), count: c.count, hashes: c.hashes.length })),
+ healthy: this._healthy,
+ topType: this._getTopType(db),
+ topCategory: this._getTopCategory(db),
+ };
+ }
+
+ _getTopType(db) {
+ const freq = {};
+ for (const e of Object.values(db)) {
+ freq[e.type] = (freq[e.type] || 0) + e.count;
+ }
+ const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]);
+ return sorted.length > 0 ? sorted[0][0] : null;
+ }
+
+ _getTopCategory(db) {
+ const freq = {};
+ for (const e of Object.values(db)) {
+ freq[e.category] = (freq[e.category] || 0) + e.count;
+ }
+ const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]);
+ return sorted.length > 0 ? sorted[0][0] : null;
+ }
+}
+
/* === end core module bundle === */
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 8a0a7c8..73078d0 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -86,10 +86,10 @@ buildSystemPrompt() {
} catch (e) {}
let base = `Current date: ${dateStr} (${dayStr}) ${timeStr}${nvfsNote}\n`
+ 'VFS→Modules→AI. nvfs-index for commands. Tool→Observe→Answer. nvfs-resolve. Destructive cmds need confirm.\n'
- + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet | get_usage_stats(model,provider,since_hours) — LLM token/latency stats\n'
- + 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n'
- + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* — discoverable via help. '
- + 'JS: node <file.js> executes JS from VFS (alias for js-run), node -e "expr" for inline eval. No real Node.js runtime — browser JS only.';
+ + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind | wallet-lookup | get_usage_stats\\n'
+ + 'web_fetch: GET for pages, POST with method+body for JSON APIs. JS: node <file.js> or node -e "expr" (browser JS only).\n'
+ + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, world-model, predict, guardrail, tar, secret-* — via help.\n'
+ + 'predict_task: estimate duration before complex tasks. world_model_query: ask about system state, changes, health.';
if (profilePrompt) {
base = profilePrompt + '\n\n' + base;
}
@@ -99,6 +99,18 @@ buildSystemPrompt() {
if (lp.systemPrompt.prepend) base = lp.systemPrompt.prepend + '\n\n' + base;
if (lp.systemPrompt.append) base = base + '\n\n' + lp.systemPrompt.append;
}
+ // Inject auto-fix suggestions from DebugSupervisorModule
+ try {
+ var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule;
+ if (ds && this._autoFixSuggestions && this._autoFixSuggestions.length > 0) {
+ base += '\n\n[Auto-Fix Suggestions]\n';
+ for (var fi = 0; fi < this._autoFixSuggestions.length; fi++) {
+ var s = this._autoFixSuggestions[fi];
+ base += '- ' + s.module + ': ' + s.suggestion + ' (' + s.reason + ')\n';
+ }
+ base += '[/Auto-Fix Suggestions]';
+ }
+ } catch (e) {}
return base;
}
getToolDefinitions() {
@@ -165,6 +177,13 @@ buildSystemPrompt() {
}, {
type: 'function',
function: {
+ name: 'get_dashboard_data',
+ description: 'Full dashboard telemetry: modules, agents, memory, VFS, Ollama, token usage, tool latency, system health',
+ parameters: { type: 'object', properties: {}, required: [] }
+ }
+ }, {
+ type: 'function',
+ function: {
name: 'get_help',
description: 'Command reference',
parameters: { type: 'object', properties: {}, required: [] }
@@ -231,15 +250,18 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'edit_module',
- description: 'Edit module source (DESTRUCTIVE)',
- parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] }
+ description: 'Edit module source (DESTRUCTIVE). If code is provided, writes directly to VFS and auto-tests. If no code, opens the visual editor (requires manual Save).',
+ parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' }, code: { type: 'string', description: 'Full new source code. When provided, writes directly and auto-tests without opening the editor.' } }, required: ['name'] }
}
}, {
type: 'function',
function: {
name: 'test_module',
- description: 'Test edited module from /_mods/',
- parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] }
+ description: 'Test an edited module: loads it, checks JS errors, verifies commands registered, runs optional test assertions. Auto-rolls-back on failure.',
+ parameters: { type: 'object', properties: {
+ name: { type: 'string', description: 'Module class name' },
+ tests: { type: 'array', description: 'Optional test assertions: [{command, expect}]', items: { type: 'object', properties: { command: { type: 'string' }, expect: { type: 'string' } } } }
+ }, required: ['name'] }
}
}, {
type: 'function',
@@ -440,7 +462,7 @@ buildSystemPrompt() {
required: []
}
}
-}, {
+ }, {
type: 'function',
function: {
name: 'smart_context_set',
@@ -471,7 +493,7 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'vaultmind_query',
- description: "Query VaultMind AI for Hyperliquid market data (prices, portfolio, transactions, copy-trade) or ask an AI agent. Uses same-origin /vmai/ API.",
+ description: "Query VaultMind AI for Hyperliquid market data (prices, portfolio, transactions, copy-trade) or ask an AI agent. Uses same-origin /vmai-api/ API.",
parameters: {
type: 'object',
properties: {
@@ -533,11 +555,11 @@ buildSystemPrompt() {
},
required: []
}
- },
- {
- type: 'function',
- function: {
- name: 'agent_loop_list',
+ }
+ }, {
+ type: 'function',
+ function: {
+ name: 'agent_loop_list',
description: 'List all available agent loop profiles with their settings and benchmark results.',
parameters: { type: 'object', properties: {}, required: [] }
}
@@ -589,9 +611,77 @@ buildSystemPrompt() {
required: []
}
}
- }
- ];
- }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'predict_task',
+ description: 'Predict task duration, tool turns, and confidence based on historical execution data. Use before starting complex tasks to estimate resource needs.',
+ parameters: {
+ type: 'object',
+ properties: {
+ task: { type: 'string', description: 'Task description to predict' }
+ },
+ required: ['task']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'world_model_query',
+ description: 'Query the system world model for self-awareness: modules, skills, files, agents, health, memory graph, recent changes, active projects, telemetry. Ask questions like "what changed" or "how many modules".',
+ parameters: {
+ type: 'object',
+ properties: {
+ question: { type: 'string', description: 'What to ask: modules, skills, files, agents, health, memory, changes, projects, telemetry, summary' }
+ },
+ required: ['question']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'health_check',
+ description: 'Run a system health check after a module edit. Verifies: no JS errors, all core commands work, world model is valid, agents are alive.',
+ parameters: { type: 'object', properties: {} }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'diff_module',
+ description: 'Show a diff between the original module source and the edited version in _mods/. Returns line-by-line changes.',
+ parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'ship_module',
+ description: 'Full pipeline: edit → test → health_check → regression suite. Auto-rolls-back on any failure. Set commit:true to also persist to server (requires human approval). Without commit, runs as dry-run.',
+ parameters: { type: 'object', properties: {
+ name: { type: 'string', description: 'Module class name' },
+ code: { type: 'string', description: 'Full new source code' },
+ tests: { type: 'array', description: 'Test assertions [{command, expect}]', items: { type: 'object', properties: { command: { type: 'string' }, expect: { type: 'string' } } } },
+ commit: { type: 'boolean', description: 'If true, request server persistence (needs human approval). Default: false.' }
+ }, required: ['name', 'code'] }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'commit_module',
+ description: 'Request to persist an edited module from VFS _mods/ to the server file system via WebDAV. Creates a pending commit — user must type commit-approve in the terminal to execute. Includes bundle sync and version bump.',
+ parameters: { type: 'object', properties: {
+ name: { type: 'string', description: 'Module class name' },
+ file: { type: 'string', description: 'Target server path (e.g. modules/ext/shell.js). Auto-detect from module registry if omitted.' }
+ }, required: ['name'] }
+ }
+ }
+ ];
+ }
getHelpInfo() {
return {
'ai': 'ai <question> — Ask the AI assistant anything',
@@ -614,6 +704,8 @@ buildSystemPrompt() {
'reset-mem': 'reset-mem — Reset AI memory (logs, context, profile, guardrails) without losing VFS or config',
'vaultmind-connect': 'vaultmind-connect [testnet|mainnet] — Connect VaultMind wallet using hyperliquid_key from SecretVault',
'vaultmind-status': 'vaultmind-status — Check VaultMind session status',
+ 'vaultmind-query': 'vaultmind-query <prices|portfolio|transactions|copy-trade|chat> [message] — Query VaultMind API',
+ 'wallet-lookup': 'wallet-lookup <address> — Look up Hyperliquid wallet summary',
};
}
registerCommands() {
@@ -629,6 +721,21 @@ buildSystemPrompt() {
this.commands['ai-model'] = (args) => this.showModel(args);
this.commands['ai-compress'] = () => this._manualCompress();
this.commands['ai-config'] = (args) => this.aiConfig(args);
+ this.commands['commit-approve'] = (args) => this._executeCommit(args.join(' '));
+ this.commands['commit-deny'] = () => {
+ if (!this._pendingCommit) return this.terminal.print('No pending commit.', 'warning');
+ const name = this._pendingCommit.name;
+ this._pendingCommit = null;
+ this.terminal.print('❌ Commit denied for ' + name, 'warning');
+ };
+ this.commands['commit-status'] = () => {
+ if (!this._pendingCommit) return this.terminal.print('No pending commit.', 'info');
+ const pc = this._pendingCommit;
+ this.terminal.print('📋 Pending commit: ' + pc.name, 'info');
+ this.terminal.print(' Target: ' + pc.serverPath, 'info');
+ if (pc.bundlePath) this.terminal.print(' Bundle: ' + pc.bundlePath, 'info');
+ this.terminal.print(' Changes: ' + pc.changes + ' line(s)', 'info');
+ };
this.commands['agent-loops'] = (args) => this.agentLoopsCmd(args);
this.commands['bench'] = (args) => this.benchCmd(args);
this.commands['bench-compare'] = (args) => this.benchCompareCmd(args);
@@ -649,6 +756,8 @@ buildSystemPrompt() {
this.commands['reset-mem'] = () => this.resetMem();
this.commands['vaultmind-connect'] = (args) => this._vaultmindConnectCmd(args);
this.commands['vaultmind-status'] = () => this._vaultmindStatusCmd();
+ this.commands['vaultmind-query'] = (args) => this._vaultmindQueryCmd(args);
+ this.commands['wallet-lookup'] = (args) => this._walletLookupCmd(args);
}
async chat(args) {
const userMessage = args.join(' ');
@@ -668,6 +777,18 @@ buildSystemPrompt() {
}
this._chatLock = true;
this.terminal.markDirty();
+ // Auto-route context based on intent
+ try {
+ const scm = this.terminal?.moduleManager?.modules?.SmartContextModule;
+ if (scm && typeof scm.autoRouteContext === 'function') {
+ const routeResult = scm.autoRouteContext(userMessage);
+ if (routeResult.switched) {
+ this.terminal.print('🔄 Context auto-switched: ' + routeResult.from + ' → ' + routeResult.to + ' (' + routeResult.confidence + '% confidence)', 'info');
+ // Rebuild system prompt after context switch
+ this.systemPrompt = this.buildSystemPrompt();
+ }
+ }
+ } catch (e) { console.warn('[AI] Auto-routing error:', e.message); }
// Re-read context from localStorage (auto-restore may have written it after init)
this._loadContext();
// Compress context if it exceeds the token threshold
@@ -751,6 +872,24 @@ buildSystemPrompt() {
if (loopResult.toolTurns >= 2 && !this._cancelled) {
await this.afterActionReflection(userMessage, loopResult.toolTurns);
}
+ // Feed result to predictor for learning (cold module activation)
+ if (loopResult && !this._cancelled && loopResult.toolTurns >= 1) {
+ try {
+ const predictor = this.terminal.moduleManager.modules['PredictorModule'];
+ if (predictor && typeof predictor.recordResult === 'function') {
+ const features = predictor._extractFeatures(userMessage);
+ const duration = (Date.now() - lockStart) / 1000;
+ predictor.recordResult(features, duration, loopResult.toolTurns, !this._cancelled);
+ }
+ } catch (e) { /* predictor learning is best-effort */ }
+ // Record task in memory graph for direct AI conversations (not just orchestrator)
+ try {
+ const mg = this.terminal.moduleManager.modules['MemoryGraphModule'];
+ if (mg && typeof mg.recordTask === 'function') {
+ mg.recordTask(userMessage, loopResult.finalAnswer || '', 'ai-assistant');
+ }
+ } catch (e) { /* memory graph is best-effort */ }
+ }
// Self-improvement: post-run anomaly detection
if (loopResult && !this._cancelled) {
try {
@@ -804,6 +943,280 @@ buildSystemPrompt() {
});
}
}
+ // ─── Self-Improvement Helpers ───
+ async _captureCommandOutput(cmd) {
+ const originalPrint = this.terminal.print.bind(this.terminal);
+ let output = '';
+ this.terminal.print = (text, type) => { output += text + '\n'; };
+ try { await this.terminal.security.checkAndRun(cmd); }
+ finally { this.terminal.print = originalPrint; }
+ return output.trim();
+ }
+ async _captureShellCommand(cmd) {
+ // Routes through executeCommand (shell parser) for >, |, &&, export, etc.
+ const originalPrint = this.terminal.print.bind(this.terminal);
+ let output = '';
+ this.terminal.print = (text, type) => { output += text + '\n'; };
+ try { await this.terminal.executeCommand(cmd); }
+ finally { this.terminal.print = originalPrint; }
+ return output.trim();
+ }
+ async _captureConsoleErrors(durationMs) {
+ durationMs = durationMs || 500;
+ var errors = [];
+ var origError = console.error;
+ var origOnError = window.onerror;
+ console.error = function() {
+ var args = Array.prototype.slice.call(arguments);
+ errors.push(args.join(' '));
+ };
+ window.onerror = function(msg, src, line, col, err) {
+ errors.push(msg + (err ? ' — ' + err.message : ''));
+ };
+ await new Promise(function(r) { setTimeout(r, durationMs); });
+ console.error = origError;
+ window.onerror = origOnError;
+ return errors;
+ }
+ _regressionTasks() {
+ return [
+ { id: 'shell-export', module: 'ShellModule', command: 'export REGTEST=ok', expect: 'REGTEST', useShell: true },
+ { id: 'shell-redirect', module: 'ShellModule', command: 'echo hi > _regtest.txt', expect: 'Redirected', useShell: true },
+ { id: 'shell-chain', module: 'ShellModule', command: 'echo a && echo b', expect: 'b', useShell: true },
+ { id: 'shell-pipe', module: 'ShellModule', command: 'echo hello | grep hello', expect: 'hello', useShell: true },
+ { id: 'fs-ls', module: 'SystemModule', command: 'ls', expect: '' },
+ { id: 'fs-pwd', module: 'SystemModule', command: 'pwd', expect: '/' },
+ { id: 'orch-agents', module: 'OrchestratorModule', command: 'list-agents', expect: 'agent' },
+ { id: 'ai-tools', module: 'WebX11AIAssistantModule', command: 'list_modules', expect: 'Module' },
+ { id: 'calc-basic', module: 'CalcModule', command: 'calc 1+1', expect: '2' },
+ { id: 'calc-advanced', module: 'CalcModule', command: 'calc 2*3', expect: '6' },
+ { id: 'webdav-ls', module: 'WebDAVModule', command: 'webdav-ls', expect: '' },
+ { id: 'cron-list', module: 'CronModule', command: 'cron-list', expect: '' },
+ { id: 'theme-list', module: 'ThemeModule', command: 'theme-list', expect: '' },
+ { id: 'mindmap-list', module: 'MindmapModule', command: 'mindmap-list', expect: '' },
+ { id: 'media-status', module: 'MediaModule', command: 'media-status', expect: '' },
+ { id: 'skill-list', module: 'SkillPackCoreModule', command: 'skill-list', expect: '' },
+ { id: 'crypto-encode', module: 'CryptoGzipModule', command: 'crypto-encode test', expect: '' },
+ { id: 'debug-toggle', module: 'DebugPanelModule', command: 'debug', expect: '' },
+ { id: 'voice-status', module: 'VoiceModule', command: 'voice-status', expect: '' },
+ { id: 'wpm-status', module: 'WPMModule', command: 'wpm-status', expect: '' },
+ { id: 'console-help', module: 'ConsoleModule', command: 'console-help', expect: '' },
+ { id: 'tutorial-status', module: 'TutorialModule', command: 'tutorial-status', expect: '' },
+ ];
+ }
+ async _runRegressionSuite(moduleName) {
+ const tests = this._regressionTasks().filter(t => t.module === moduleName);
+ if (tests.length === 0) return { module: moduleName, total: 0, passed: 0, failed: 0, results: [], allPassed: true, message: 'No regression tests for ' + moduleName };
+ const results = [];
+ for (const t of tests) {
+ try {
+ const out = t.useShell ? await this._captureShellCommand(t.command) : await this._captureCommandOutput(t.command);
+ const pass = t.expect ? out.includes(t.expect) : out.length > 0;
+ results.push({ id: t.id, command: t.command, pass, output: out.substring(0, 100) });
+ } catch (e) {
+ results.push({ id: t.id, command: t.command, pass: false, output: 'Error: ' + e.message });
+ }
+ }
+ const passed = results.filter(r => r.pass).length;
+ return { module: moduleName, total: results.length, passed, failed: results.length - passed, results, allPassed: passed === results.length };
+ }
+ // ─── Phase D: Server Persistence ───
+ async _webdavPush(remotePath, content) {
+ const webdav = this.terminal.moduleManager.modules['WebDAVModule'];
+ if (!webdav) throw new Error('WebDAV module not loaded');
+ // Use the webdav module's internal request method
+ const resp = await webdav._webdavRequest('PUT', remotePath, {
+ body: content,
+ headers: { 'Content-Type': 'text/plain' }
+ });
+ if (!resp.ok && resp.status !== 201 && resp.status !== 204) {
+ throw new Error('WebDAV PUT failed: HTTP ' + resp.status);
+ }
+ return true;
+ }
+ async _webdavGet(remotePath) {
+ const webdav = this.terminal.moduleManager.modules['WebDAVModule'];
+ if (!webdav) throw new Error('WebDAV module not loaded');
+ const resp = await webdav._webdavRequest('GET', remotePath);
+ if (!resp.ok) throw new Error('WebDAV GET failed: HTTP ' + resp.status);
+ return await resp.text();
+ }
+ _extractClassName(code) {
+ const m = code.match(/^class\s+(\w+)/m);
+ return m ? m[1] : null;
+ }
+ _extractClassBlock(code, className) {
+ // Extract the class definition from standalone code (without self-registration block)
+ const classStart = code.indexOf('class ' + className);
+ if (classStart === -1) return null;
+ // Find matching closing brace by counting
+ let depth = 0;
+ let end = classStart;
+ for (let i = classStart; i < code.length; i++) {
+ if (code[i] === '{') depth++;
+ else if (code[i] === '}') {
+ depth--;
+ if (depth === 0) { end = i + 1; break; }
+ }
+ }
+ return code.substring(classStart, end);
+ }
+ async _syncBundle(bundlePath, modName, newCode) {
+ try {
+ // Download current bundle
+ const bundle = await this._webdavGet(bundlePath);
+ // Find the class block in the bundle
+ const className = modName;
+ const classStart = bundle.indexOf('class ' + className);
+ if (classStart === -1) return { synced: false, reason: 'Class not found in bundle' };
+ // Find matching closing brace
+ let depth = 0;
+ let classEnd = classStart;
+ for (let i = classStart; i < bundle.length; i++) {
+ if (bundle[i] === '{') depth++;
+ else if (bundle[i] === '}') {
+ depth--;
+ if (depth === 0) { classEnd = i + 1; break; }
+ }
+ }
+ // Extract new class block from the edited code
+ const newClassBlock = this._extractClassBlock(newCode, className);
+ if (!newClassBlock) return { synced: false, reason: 'Could not extract class from new code' };
+ // Replace in bundle
+ const newBundle = bundle.substring(0, classStart) + newClassBlock + bundle.substring(classEnd);
+ // Validate with vm.Script equivalent (Function constructor)
+ try { new Function(newBundle); } catch (e) {
+ return { synced: false, reason: 'Bundle syntax error after replace: ' + e.message };
+ }
+ // Upload
+ await this._webdavPush(bundlePath, newBundle);
+ return { synced: true, bytes: newBundle.length };
+ } catch (e) {
+ return { synced: false, reason: e.message };
+ }
+ }
+ _parseVersion(ver) {
+ // Handle both old format (2026-07-06-v4.6.01) and new format (v4.6.01)
+ const m = ver.match(/v(\d+)\.(\d+)\.(\d+)/);
+ if (!m) return { major: 4, minor: 6, patch: 0 };
+ return { major: parseInt(m[1]), minor: parseInt(m[2]), patch: parseInt(m[3]) };
+ }
+ _bumpVersionString(ver) {
+ const v = this._parseVersion(ver);
+ return 'v' + v.major + '.' + v.minor + '.' + (v.patch + 1);
+ }
+ async _bumpVersion() {
+ // Read current version from the page
+ const currentVer = window.NEWOS4_VERSION || 'v4.6.01';
+ const newVer = this._bumpVersionString(currentVer);
+ // 1. Update main.html — replace all ?v= occurrences
+ const mainHtml = await this._webdavGet('main.html');
+ const updatedHtml = mainHtml.replace(/\?v=[^"']+/g, '?v=' + newVer);
+ await this._webdavPush('main.html', updatedHtml);
+ // 2. Update module-loader.js — replace LATEST_VER
+ const loader = await this._webdavGet('modules/core/module-loader.js');
+ const updatedLoader = loader.replace(/const LATEST_VER = '[^']+'/, "const LATEST_VER = '" + newVer + "'");
+ await this._webdavPush('modules/core/module-loader.js', updatedLoader);
+ return { from: currentVer, to: newVer };
+ }
+ _calculateConfidence(modName, testResult, healthResult, regResult, changeCount) {
+ var score = 100;
+ if (changeCount > 50) score -= 20;
+ if (changeCount > 100) score -= 20;
+ if (regResult.total === 0) score -= 30;
+ var critical = ['ShellModule', 'SystemModule', 'OrchestratorModule', 'WebX11AIAssistantModule'];
+ if (critical.indexOf(modName) !== -1) score -= 15;
+ if (testResult.tests && testResult.tests.length >= 3) score += 10;
+ if (healthResult.jsErrors && healthResult.jsErrors.length > 0) score -= 50;
+ return Math.max(0, Math.min(100, score));
+ }
+
+ async _executeCommit(comment) {
+ const pc = this._pendingCommit;
+ if (!pc) return this.terminal.print('No pending commit. Use commit_module first.', 'warning');
+ // Rate limit check
+ const today = new Date().toISOString().split('T')[0];
+ const commitLog = this.terminal.fs.fsReadJSON('/_mods/commit-log.json') || {};
+ if (commitLog[today] && commitLog[today] >= 5) {
+ return this.terminal.print('⚠️ Rate limit: 5 commits today. Try tomorrow.', 'warning');
+ }
+ // Cooldown check (10 min)
+ if (commitLog._lastCommitTime && Date.now() - commitLog._lastCommitTime < 600000) {
+ const wait = Math.ceil((600000 - (Date.now() - commitLog._lastCommitTime)) / 60000);
+ return this.terminal.print('⏱ Cooldown: ' + wait + ' min until next commit.', 'warning');
+ }
+ this.terminal.print('🚀 Executing commit for ' + pc.name + '...', 'info');
+ const results = { module: pc.name, steps: [] };
+ // Step 1: Push standalone file
+ try {
+ await this._webdavPush(pc.serverPath, pc.code);
+ results.steps.push({ step: 'standalone', status: 'ok', path: pc.serverPath });
+ this.terminal.print(' ✅ Standalone: ' + pc.serverPath, 'success');
+ } catch (e) {
+ results.steps.push({ step: 'standalone', status: 'fail', error: e.message });
+ this.terminal.print(' ❌ Standalone failed: ' + e.message, 'error');
+ return results;
+ }
+ // Step 1b: Post-commit syntax verification
+ try {
+ const verifyResp = await fetch(pc.serverPath + '?v=' + Date.now());
+ const committedCode = await verifyResp.text();
+ new Function(committedCode);
+ results.steps.push({ step: 'verify', status: 'ok' });
+ this.terminal.print(' ✅ Post-commit syntax check passed', 'success');
+ } catch (e) {
+ results.steps.push({ step: 'verify', status: 'fail', error: e.message });
+ this.terminal.print(' ❌ Post-commit syntax check failed: ' + e.message, 'error');
+ // Auto-revert
+ try {
+ await this._webdavPush(pc.serverPath, pc.origCode || '');
+ this.terminal.print(' ⏪ Auto-reverted to original code', 'warning');
+ results.reverted = true;
+ results.syntaxError = e.message;
+ } catch (revertErr) {
+ this.terminal.print(' ❌ Auto-revert FAILED: ' + revertErr.message, 'error');
+ results.revertFailed = true;
+ }
+ this._pendingCommit = null;
+ return results;
+ }
+ // Step 2: Sync bundle
+ if (pc.bundlePath) {
+ try {
+ const syncResult = await this._syncBundle(pc.bundlePath, pc.name, pc.code);
+ results.steps.push({ step: 'bundle', status: syncResult.synced ? 'ok' : 'skip', detail: syncResult });
+ if (syncResult.synced) this.terminal.print(' ✅ Bundle: ' + pc.bundlePath, 'success');
+ else this.terminal.print(' ⚠️ Bundle sync skipped: ' + syncResult.reason, 'warning');
+ } catch (e) {
+ results.steps.push({ step: 'bundle', status: 'fail', error: e.message });
+ this.terminal.print(' ❌ Bundle sync failed: ' + e.message, 'error');
+ }
+ }
+ // Step 3: Bump version
+ try {
+ const verResult = await this._bumpVersion();
+ results.steps.push({ step: 'version', status: 'ok', from: verResult.from, to: verResult.to });
+ this.terminal.print(' ✅ Version: ' + verResult.from + ' → ' + verResult.to, 'success');
+ results.newVersion = verResult.to;
+ } catch (e) {
+ results.steps.push({ step: 'version', status: 'fail', error: e.message });
+ this.terminal.print(' ⚠️ Version bump failed: ' + e.message, 'warning');
+ }
+ // Step 4: Cleanup _mods/
+ this.terminal.fs.deleteFile('_mods/' + pc.name + '.js');
+ this.terminal.fs.deleteFile('_mods/' + pc.name + '.orig.js');
+ this.terminal.fs.deleteFile('_mods/' + pc.name + '.meta.json');
+ // Step 5: Rate limit update
+ commitLog[today] = (commitLog[today] || 0) + 1;
+ commitLog._lastCommitTime = Date.now();
+ this.terminal.fs.fsWriteJSON('/_mods/commit-log.json', commitLog);
+ // Clear pending
+ this._pendingCommit = null;
+ this.terminal.print('🎉 Commit complete! Refresh to load ' + (results.newVersion || 'new version'), 'success');
+ if (comment) this.terminal.print(' Comment: ' + comment, 'info');
+ results.success = true;
+ return results;
+ }
_summarizeObservation(result, maxLen) {
if (!result || typeof result !== 'string' || result.length <= maxLen) return result || '';
const lines = result.split('\n');
@@ -1630,7 +2043,7 @@ buildSystemPrompt() {
}
try {
this.terminal.fs.root = JSON.parse(JSON.stringify(this._shadowSnapshot));
- this.terminal.fs.saveToLocalStorage();
+ this.terminal.fs.saveToStorage();
this._shadowSnapshot = null;
this.terminal.print('↩️ VFS rolled back to pre-shadow state', 'success');
} catch (e) {
@@ -1651,8 +2064,6 @@ buildSystemPrompt() {
'nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync',
'floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle',
'debug','debug-ai','session-new','mem-debug','reset-mem',
- // Node bridge — alias for js-run
- 'node',
// Synced from terminal whitelist — read-only orchestration/A2A/agent commands
'orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result',
'orchestrator-status','orchestrator-list','orchestrator-config','orchestrator-restart-policy',
@@ -1769,6 +2180,9 @@ buildSystemPrompt() {
memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A'
});
}
+ case 'get_dashboard_data': {
+ return JSON.stringify(await this._collectDashboardData());
+ }
case 'get_help': {
const cmds = this.terminal.moduleManager.getAllCommands();
return JSON.stringify(Object.keys(cmds).sort());
@@ -1982,8 +2396,12 @@ buildSystemPrompt() {
if (!task) return JSON.stringify({ error: 'No task provided' });
const orch = this.terminal.moduleManager.modules['OrchestratorModule'];
if (!orch) return JSON.stringify({ error: 'Orchestrator not available' });
- orch.delegateTask(task);
- return JSON.stringify({ message: `Task delegated: ${task.substring(0, 80)}` });
+ try {
+ const result = await orch.delegateTask(task);
+ return JSON.stringify({ success: true, task: task.substring(0, 80), result: typeof result === 'string' ? result.substring(0, 500) : 'completed' });
+ } catch (e) {
+ return JSON.stringify({ error: 'Delegation failed: ' + e.message });
+ }
}
case 'spawn_agent': {
const role = a.role || 'worker';
@@ -2236,7 +2654,7 @@ buildSystemPrompt() {
return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + validEps.join(', ') });
}
try {
- const epMap = { prices: '/vmai/market/prices', portfolio: '/vmai/portfolio', transactions: '/vmai/transactions/summary', 'copy-trade': '/vmai/copy-trading/traders', chat: '/vmai/chat' };
+ const epMap = { prices: '/vmai-api/market/prices', portfolio: '/vmai-api/portfolio', transactions: '/vmai-api/transactions/summary', 'copy-trade': '/vmai-api/copy-trading/traders', chat: '/vmai-api/chat' };
let url = epMap[ep];
let opts = { method: 'GET', headers: { 'Accept': 'application/json' } };
// Portfolio, transactions, copy-trade need auth token
@@ -2322,12 +2740,48 @@ buildSystemPrompt() {
}.bind(this));
return JSON.stringify({ success: true, message: "Benchmark started on '" + profileName + "'", tasks: taskIds || 'all' });
}
+ case 'predict_task': {
+ var predictor = this.terminal.moduleManager.modules['PredictorModule'];
+ if (!predictor) return JSON.stringify({ error: 'Predictor module not loaded' });
+ var taskDesc = a.task || '';
+ if (!taskDesc) return JSON.stringify({ error: 'task parameter required' });
+ var pFeatures = predictor._extractFeatures(taskDesc);
+ var pSimilar = predictor._findSimilar(pFeatures);
+ var pResult = predictor._computePrediction(pSimilar, pFeatures);
+ return JSON.stringify({
+ task: taskDesc.substring(0, 80),
+ estimatedDuration: pResult.duration + 's',
+ estimatedToolTurns: pResult.toolTurns,
+ confidence: Math.round(pResult.confidence * 100) + '%',
+ similarTasks: pResult.similarCount,
+ recommendation: pResult.confidence >= predictor._CONFIDENCE_THRESHOLD && pResult.duration > 30
+ ? 'Set TTL to ' + Math.ceil(pResult.duration * 1.5) + 's'
+ : 'No special action needed'
+ });
+ }
+ case 'world_model_query': {
+ var wm = this.terminal.moduleManager.modules['WorldModelModule'];
+ if (!wm) return JSON.stringify({ error: 'WorldModelModule not loaded' });
+ if (!wm._model) return JSON.stringify({ error: 'World model not synthesized yet — run world-model-refresh' });
+ var wmAnswer = wm._answerQuery(a.question || '');
+ return JSON.stringify({ question: a.question, answer: wmAnswer, version: wm._model.version, synthesized: wm._model.synthesized });
+ }
+ case 'health_check':
+ case 'diff_module':
+ case 'ship_module':
+ case 'commit_module':
+ return await this.runModuleTool(name, argsStr);
default:
return JSON.stringify({ error: `Unknown tool: ${name}` });
}
} catch (e) {
this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
if (this._errorLog.length > 20) this._errorLog.shift();
+ // Also feed into DebugSupervisorModule
+ try {
+ var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule;
+ if (ds) ds.recordToolError(name, a, e.message);
+ } catch (dsErr) {}
return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
@@ -2421,10 +2875,61 @@ buildSystemPrompt() {
}
case 'test_module': {
const modName = a.name || '';
+ const tests = a.tests || null;
const code = this.terminal.fs.readFile(`_mods/${modName}.js`);
if (!code) return JSON.stringify({ error: `No local edit for ${modName}` });
- this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`);
- return `Module "${modName}" loaded from local edit for testing`;
+ // Save current module state for rollback
+ const oldInstance = this.terminal.moduleManager.modules[modName];
+ const oldCommands = oldInstance ? Object.keys(oldInstance.commands) : [];
+ // Load new version
+ const ok = await this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`);
+ if (!ok) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'Module failed to load', rolledBack: true });
+ }
+ // Check for JS errors after load
+ const jsErrors = await this._captureConsoleErrors(300);
+ if (jsErrors.length > 0) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'JS errors after load', errors: jsErrors.slice(0, 5), rolledBack: true });
+ }
+ // Verify commands registered
+ const newInstance = this.terminal.moduleManager.modules[modName];
+ const newCommands = newInstance ? Object.keys(newInstance.commands) : [];
+ const lostCommands = oldCommands.filter(c => !newCommands.includes(c));
+ if (lostCommands.length > 0) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'Commands lost after edit', lost: lostCommands, rolledBack: true });
+ }
+ // Run test assertions if provided
+ if (tests && Array.isArray(tests) && tests.length > 0) {
+ const results = [];
+ for (const t of tests) {
+ const cmd = t.command || '';
+ // Use shell parser for commands with >, |, &&, ||, ;, export, set
+ const useShell = /[>|&;]/.test(cmd) || cmd.startsWith('export ') || cmd.startsWith('set ');
+ const output = useShell ? await this._captureShellCommand(cmd) : await this._captureCommandOutput(cmd);
+ const passed = t.expect ? output.includes(t.expect) : output.length > 0;
+ results.push({ command: cmd, expected: t.expect || '(any output)', actual: output.substring(0, 200), pass: passed });
+ }
+ const failed = results.filter(r => !r.pass).length;
+ if (failed > 0) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: failed + '/' + results.length + ' tests failed', results, rolledBack: true });
+ }
+ return JSON.stringify({ success: true, module: modName, commands: newCommands.length, tests: results });
+ }
+ // Smoke test: run first command if available
+ if (newCommands.length > 0) {
+ try {
+ const smokeOut = await this._captureCommandOutput(newCommands[0]);
+ return JSON.stringify({ success: true, module: modName, commands: newCommands.length, smokeTest: { command: newCommands[0], output: smokeOut.substring(0, 200) } });
+ } catch (e) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'Smoke test failed: ' + e.message, rolledBack: true });
+ }
+ }
+ return JSON.stringify({ success: true, module: modName, commands: newCommands.length });
}
case 'reload_module': {
const modName = a.name || '';
@@ -2488,11 +2993,181 @@ buildSystemPrompt() {
}
return JSON.stringify({ modules, total: modules.length, hasLocalEdits: hasMods });
}
+ case 'health_check': {
+ const results = {};
+ const coreCmds = ['ls', 'pwd', 'echo test', 'help', 'grep', 'find', 'tree', 'cat'];
+ results.coreCommands = {};
+ for (const cmd of coreCmds) {
+ try {
+ const out = await this._captureCommandOutput(cmd);
+ results.coreCommands[cmd] = (out.length > 0 || cmd === 'ls') ? 'ok' : 'empty output';
+ } catch (e) {
+ results.coreCommands[cmd] = 'FAIL: ' + e.message;
+ }
+ }
+ const mm = this.terminal.moduleManager;
+ results.moduleCount = Object.keys(mm.modules).length;
+ const wm = this.terminal.fs.fsReadJSON('system/world-model.json');
+ results.worldModelVersion = wm ? wm.version : 'not found';
+ results.versionMatch = wm && wm.version === (typeof window !== 'undefined' ? window.NEWOS4_VERSION : '');
+ const orch = mm.modules['OrchestratorModule'];
+ if (orch) {
+ const agents = Object.values(orch._agents);
+ results.agents = { total: agents.length, healthy: agents.filter(a => a.status !== 'failed').length, failed: agents.filter(a => a.status === 'failed').length };
+ }
+ results.jsErrors = await this._captureConsoleErrors(500);
+ const allCoreOk = Object.values(results.coreCommands).every(v => v === 'ok');
+ results.healthy = allCoreOk && results.versionMatch && results.jsErrors.length === 0;
+ return JSON.stringify(results);
+ }
+ case 'diff_module': {
+ const modName = a.name || '';
+ const newCode = this.terminal.fs.readFile(`_mods/${modName}.js`);
+ if (!newCode) return JSON.stringify({ error: `No edit for ${modName}` });
+ let origCode = this.terminal.fs.readFile(`_mods/${modName}.orig.js`);
+ if (!origCode || origCode === 'placeholder') {
+ const instance = this.terminal.moduleManager.modules[modName];
+ if (instance) origCode = instance.constructor.toString();
+ }
+ if (!origCode) return JSON.stringify({ error: 'No original source found' });
+ const origLines = origCode.split('\n');
+ const newLines = newCode.split('\n');
+ const changes = [];
+ const maxLines = Math.max(origLines.length, newLines.length);
+ for (let i = 0; i < maxLines; i++) {
+ if (origLines[i] !== newLines[i]) {
+ if (origLines[i] && !newLines[i]) changes.push({ line: i+1, type: 'removed', content: origLines[i].substring(0, 120) });
+ else if (!origLines[i] && newLines[i]) changes.push({ line: i+1, type: 'added', content: newLines[i].substring(0, 120) });
+ else changes.push({ line: i+1, type: 'changed', old: (origLines[i]||'').substring(0, 80), new: (newLines[i]||'').substring(0, 80) });
+ }
+ }
+ return JSON.stringify({ module: modName, originalLines: origLines.length, newLines: newLines.length, changes: changes.length, diff: changes.slice(0, 50) });
+ }
+ case 'ship_module': {
+ const modName = a.name || '';
+ const newCode = a.code || '';
+ const tests = a.tests || [];
+ const shouldCommit = a.commit === true;
+ if (!modName || !newCode) return JSON.stringify({ error: 'name and code required' });
+ // Step 1: Write to _mods/
+ this.terminal.fs.writeFile(`_mods/${modName}.js`, newCode);
+ this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ editedAt: new Date().toISOString(), editedBy: 'ship_module' }));
+ // Step 2: Test
+ const testResult = await this.runModuleTool('test_module', JSON.stringify({ name: modName, tests }));
+ const testParsed = JSON.parse(testResult);
+ if (testParsed.error) {
+ return JSON.stringify({ step: 'test', error: testParsed.error, rolledBack: testParsed.rolledBack, pipeline: 'aborted at test' });
+ }
+ // Step 3: Health check
+ const healthResult = await this.runModuleTool('health_check', '{}');
+ const healthParsed = JSON.parse(healthResult);
+ if (!healthParsed.healthy) {
+ await this.runModuleTool('reload_module', JSON.stringify({ name: modName }));
+ return JSON.stringify({ step: 'health_check', error: 'System unhealthy after edit', health: healthParsed, pipeline: 'aborted at health_check, rolled back' });
+ }
+ // Step 4: Regression suite
+ const regResult = await this._runRegressionSuite(modName);
+ if (!regResult.allPassed) {
+ await this.runModuleTool('reload_module', JSON.stringify({ name: modName }));
+ return JSON.stringify({ step: 'regression', error: 'Regression tests failed', regression: regResult, pipeline: 'aborted at regression, rolled back' });
+ }
+ // Step 5: Calculate confidence score
+ const changeCount = newCode.split('\n').length;
+ const confidence = this._calculateConfidence(modName, testParsed, healthParsed, regResult, changeCount);
+ // Step 6: Commit (if requested)
+ if (shouldCommit) {
+ if (confidence >= 70) {
+ // Auto-commit: execute directly
+ const commitResult = await this.runModuleTool('commit_module', JSON.stringify({ name: modName }));
+ const commitParsed = JSON.parse(commitResult);
+ if (commitParsed.pendingCommit) {
+ // Execute the pending commit immediately
+ const execResult = await this._executeCommit('auto: confidence=' + confidence);
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → AUTO-COMMIT (confidence=' + confidence + ')', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, autoCommitted: true, commitResult: execResult });
+ }
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → auto-commit', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, commit: commitParsed });
+ } else {
+ // Confidence too low — human approval required
+ const commitResult = await this.runModuleTool('commit_module', JSON.stringify({ name: modName }));
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → HUMAN APPROVAL REQUIRED (confidence=' + confidence + ' < 70)', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, commit: JSON.parse(commitResult), humanApprovalRequired: true });
+ }
+ }
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression (dry-run)', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, next: 'Call commit_module or ship_module with commit:true to persist' });
+ }
+ case 'commit_module': {
+ const modName = a.name || '';
+ const targetFile = a.file || '';
+ const newCode = this.terminal.fs.readFile(`_mods/${modName}.js`);
+ if (!newCode) return JSON.stringify({ error: `No local edit for ${modName}. Run edit_module or ship_module first.` });
+
+ // Auto-detect target file from module registry
+ let serverPath = targetFile;
+ if (!serverPath) {
+ const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName);
+ if (entry && entry.url) {
+ serverPath = entry.url.replace(/^https?:\/\/[^/]+/, '').replace(/\?v=.*/, '');
+ }
+ }
+ if (!serverPath) return JSON.stringify({ error: 'Cannot determine server path. Provide file= parameter (e.g. modules/ext/shell.js).' });
+
+ // Get diff
+ let origCode = this.terminal.fs.readFile(`_mods/${modName}.orig.js`);
+ if (!origCode || origCode === 'placeholder') {
+ const instance = this.terminal.moduleManager.modules[modName];
+ if (instance) origCode = instance.constructor.toString();
+ }
+ const origLines = origCode ? origCode.split('\n') : [];
+ const newLines = newCode.split('\n');
+ let changeCount = 0;
+ for (let i = 0; i < Math.max(origLines.length, newLines.length); i++) {
+ if (origLines[i] !== newLines[i]) changeCount++;
+ }
+
+ // Detect bundle path
+ let bundlePath = null;
+ if (serverPath.includes('/modules/core/') || serverPath.includes('module-aiassist')) {
+ bundlePath = 'modules/core/core-bundle.js';
+ } else if (serverPath.includes('/modules/ext/')) {
+ bundlePath = 'modules/ext/critical-ext-bundle.js';
+ }
+
+ // Store pending commit
+ this._pendingCommit = {
+ name: modName,
+ code: newCode,
+ origCode: origCode || '',
+ serverPath: serverPath,
+ bundlePath: bundlePath,
+ changes: changeCount,
+ createdAt: Date.now()
+ };
+
+ this.terminal.print('📋 Commit request created for ' + modName, 'info');
+ this.terminal.print(' Target: ' + serverPath, 'info');
+ if (bundlePath) this.terminal.print(' Bundle: ' + bundlePath, 'info');
+ this.terminal.print(' Changes: ' + changeCount + ' line(s) different', 'info');
+ this.terminal.print(' Type \'commit-approve\' to persist or \'commit-deny\' to cancel', 'warning');
+
+ return JSON.stringify({
+ pendingCommit: true,
+ module: modName,
+ serverPath: serverPath,
+ bundlePath: bundlePath,
+ changes: changeCount,
+ newLines: newLines.length,
+ message: 'Commit request created. User must type commit-approve to execute.'
+ });
+ }
default: return JSON.stringify({ error: `Unknown module tool: ${name}` });
}
} catch (e) {
this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
if (this._errorLog.length > 20) this._errorLog.shift();
+ // Also feed into DebugSupervisorModule
+ try {
+ var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule;
+ if (ds) ds.recordToolError(name, a, e.message);
+ } catch (dsErr) {}
return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
@@ -2827,6 +3502,19 @@ buildSystemPrompt() {
}
try { localStorage.setItem('webx11_ai_patterns', JSON.stringify(this._commandFreq)); } catch (e) {}
}
+ _buildToolToModuleMap() {
+ this._toolToModuleMap = {};
+ const mm = this.terminal.moduleManager;
+ if (!mm || !mm.modules) return;
+ for (const [modName, mod] of Object.entries(mm.modules)) {
+ if (mod.commands) {
+ for (const cmdName of Object.keys(mod.commands)) {
+ this._toolToModuleMap[cmdName] = modName;
+ }
+ }
+ }
+ }
+
async _autoCreateErrorSkill() {
const grouped = {};
for (const e of this._errorLog) {
@@ -3106,6 +3794,49 @@ buildSystemPrompt() {
this.terminal.print('❌ No VaultMind session. Use: vaultmind-connect [testnet|mainnet]', 'warning');
}
}
+ async _vaultmindQueryCmd(args) {
+ const ep = args[0];
+ const validEps = ['prices', 'portfolio', 'transactions', 'copy-trade', 'chat'];
+ if (!validEps.includes(ep)) {
+ this.terminal.print('Usage: vaultmind-query <prices|portfolio|transactions|copy-trade|chat> [message|agent]', 'warning'); return;
+ }
+ try {
+ const epMap = { prices: '/vmai-api/market/prices', portfolio: '/vmai-api/portfolio', transactions: '/vmai-api/transactions/summary', 'copy-trade': '/vmai-api/copy-trading/traders', chat: '/vmai-api/chat' };
+ let url = epMap[ep];
+ let opts = { method: 'GET', headers: { 'Accept': 'application/json' } };
+ const authEps = ['portfolio', 'transactions', 'copy-trade'];
+ if (authEps.includes(ep)) {
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) opts.headers['Authorization'] = 'Bearer ' + token;
+ }
+ if (ep === 'chat') {
+ opts.method = 'POST';
+ opts.headers['Content-Type'] = 'application/json';
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) opts.headers['Authorization'] = 'Bearer ' + token;
+ opts.body = JSON.stringify({
+ message: args.slice(1).join(' ') || 'What is the current market status?',
+ agent: 'market'
+ });
+ }
+ const resp = await fetch(url, opts);
+ const data = await resp.json();
+ this.terminal.print(JSON.stringify(data, null, 2), 'success');
+ } catch (e) {
+ this.terminal.print('VaultMind query error: ' + e.message, 'error');
+ }
+ }
+ async _walletLookupCmd(args) {
+ const addr = args[0];
+ if (!addr) { this.terminal.print('Usage: wallet-lookup <address>', 'warning'); return; }
+ try {
+ const resp = await fetch('/wallet/' + encodeURIComponent(addr));
+ const data = await resp.json();
+ this.terminal.print(JSON.stringify(data, null, 2), 'success');
+ } catch (e) {
+ this.terminal.print('Wallet lookup error: ' + e.message, 'error');
+ }
+ }
// ── Agent Loop Profile System ──
_defaultLoopProfile(name, desc) {
return {
@@ -3411,6 +4142,20 @@ buildSystemPrompt() {
else if (a.type === 'high_token_usage') suggestions.push('High token usage (' + (a.value/1000).toFixed(0) + 'K) — consider a minimal-tools profile for simpler tasks');
else if (a.type === 'tool_failures') {
suggestions.push(a.count + ' tool failures (' + a.tools.join(', ') + ') — check if modules are loaded or paths correct');
+ // Auto-fix: map failed tools to modules and suggest fix
+ if (!this._autoFixSuggestions) this._autoFixSuggestions = [];
+ if (!this._toolToModuleMap) this._buildToolToModuleMap();
+ for (var fi = 0; fi < a.tools.length; fi++) {
+ var failedMod = this._toolToModuleMap[a.tools[fi]];
+ if (failedMod && this._autoFixSuggestions.length < 3) {
+ this._autoFixSuggestions.push({
+ module: failedMod,
+ tool: a.tools[fi],
+ reason: 'Repeated tool failure in agent loop',
+ suggestion: 'Consider running ship_module to fix ' + failedMod
+ });
+ }
+ }
// Auto-correct: inject negative constraint into profile append
if (this._activeLoopProfile && this._activeLoopProfile.systemPrompt) {
var constraint = '[Constraint] Avoid tools that failed: ' + a.tools.join(', ') + '. Verify module state before calling these tools.';
@@ -3476,4 +4221,141 @@ buildSystemPrompt() {
if (profileAdjusted) msg += ' — profile auto-adjusted';
this.terminal.print(msg, 'info');
}
+ async _collectDashboardData() {
+ try {
+ const terminal = this.terminal;
+ const mm = terminal.moduleManager;
+ const om = terminal.ollamaManager;
+ const fs = terminal.fs;
+ const data = {};
+
+ // Version
+ data.version = window.NEWOS4_VERSION || 'v?';
+
+ // Modules
+ const mods = mm ? Object.keys(mm.modules) : [];
+ const registry = mm ? (mm.moduleRegistry || []) : [];
+ data.modules = {
+ loaded: mods.length,
+ total: registry.length || mods.length,
+ modifiedCount: 0
+ };
+ try {
+ const modsDir = fs ? fs.readDir('/_mods/') : null;
+ if (modsDir) data.modules.modifiedCount = modsDir.filter(function(e) { return e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js'); }).length;
+ } catch (e) {}
+
+ // Agents
+ const orch = mm ? mm.modules['OrchestratorModule'] : null;
+ if (orch && orch._agents) {
+ var agentList = Object.values(orch._agents);
+ data.agents = {
+ total: agentList.length,
+ busy: agentList.filter(function(a) { return a.status === 'busy'; }).length,
+ idle: agentList.filter(function(a) { return a.status === 'idle'; }).length,
+ sleeping: agentList.filter(function(a) { return a.status === 'sleeping'; }).length,
+ failed: agentList.filter(function(a) { return a.status === 'failed' || a.status === 'restarting'; }).length,
+ list: agentList.map(function(a) { return { id: a.id, role: a.role, status: a.status, caps: (a.capabilities || []).length }; })
+ };
+ } else {
+ data.agents = { total: 0, busy: 0, idle: 0, sleeping: 0, failed: 0, list: [] };
+ }
+
+ // Memory
+ var heapStr = 'N/A';
+ try { if (performance.memory) heapStr = (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB'; } catch (e) {}
+ var graphNodes = 0;
+ var mg = mm ? mm.modules['MemoryGraphModule'] : null;
+ if (mg && mg._nodes) graphNodes = Object.keys(mg._nodes).length;
+ data.memory = { heap: heapStr, graphNodes: graphNodes };
+
+ // VFS
+ try {
+ if (fs) {
+ var fileCount = 0, dirCount = 0;
+ var walk = function(path) {
+ var entries = fs.ls(path);
+ if (!entries) return;
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i].type === 'file') fileCount++;
+ else { dirCount++; walk(path === '/' ? '/' + entries[i].name : path + '/' + entries[i].name); }
+ }
+ };
+ walk('/');
+ data.vfs = { files: fileCount, dirs: dirCount, size: (JSON.stringify(fs.root || {}).length / 1024).toFixed(1) + 'KB' };
+ }
+ } catch (e) { data.vfs = { files: 0, dirs: 0, size: '0KB' }; }
+
+ // Ollama
+ data.ollama = {
+ connected: om ? om.connected : false,
+ model: om ? om.currentModel : null,
+ provider: om && om.currentProvider ? om.currentProvider.name : null
+ };
+
+ // Skills
+ try {
+ var skillIdx = fs ? fs.fsReadJSON('/skills/index.json') : null;
+ data.skills = { count: skillIdx ? Object.keys(skillIdx).length : 0 };
+ } catch (e) { data.skills = { count: 0 }; }
+
+ // Token usage
+ try {
+ if (om && typeof om.getUsageStats === 'function') {
+ var usage = om.getUsageStats({ since_hours: 24 });
+ data.usage = usage;
+ }
+ } catch (e) { data.usage = { total: 0 }; }
+
+ // Tool latency
+ var sm = mm ? mm.modules['ServerMonitorModule'] : null;
+ if (sm && sm._latencyLog) {
+ var toolStats = {};
+ for (var i = 0; i < sm._latencyLog.length; i++) {
+ var entry = sm._latencyLog[i];
+ if (!toolStats[entry.name]) toolStats[entry.name] = { calls: 0, totalMs: 0, errors: 0, maxMs: 0 };
+ toolStats[entry.name].calls++;
+ toolStats[entry.name].totalMs += entry.duration;
+ toolStats[entry.name].errors += entry.error ? 1 : 0;
+ toolStats[entry.name].maxMs = Math.max(toolStats[entry.name].maxMs, entry.duration);
+ }
+ data.latency = Object.entries(toolStats)
+ .map(function(e) { return { name: e[0], avg: Math.round(e[1].totalMs / e[1].calls) + 'ms', max: e[1].maxMs + 'ms', calls: e[1].calls, errors: e[1].errors }; })
+ .sort(function(a, b) { return parseInt(b.avg) - parseInt(a.avg); })
+ .slice(0, 10);
+ } else {
+ data.latency = [];
+ }
+
+ // System health
+ var totalErrors = 0;
+ try {
+ var ds = mm ? mm.modules['DebugSupervisorModule'] : null;
+ if (ds && ds._errorDB) totalErrors += Object.keys(ds._errorDB.errors).length;
+ if (sm && sm._latencyLog) totalErrors += sm._latencyLog.filter(function(e) { return e.error; }).length;
+ } catch (e) {}
+ var predictor = mm ? mm.modules['PredictorModule'] : null;
+ var predictorAcc = 'N/A';
+ if (predictor && predictor._accuracy && predictor._accuracy.total > 0) {
+ predictorAcc = Math.round(predictor._accuracy.correct / predictor._accuracy.total * 100) + '%';
+ }
+ data.health = {
+ modules: data.modules.loaded + '/' + data.modules.total,
+ vfsSize: data.vfs ? data.vfs.size : '?',
+ online: typeof navigator !== 'undefined' ? navigator.onLine : true,
+ errors: totalErrors,
+ predictorAccuracy: predictorAcc
+ };
+
+ data.timestamp = Date.now();
+
+ // Write to localStorage for the dashboard HTML page to read
+ try { localStorage.setItem('webx11_dashboard_data', JSON.stringify(data)); } catch (e) {}
+
+ return data;
+ } catch (e) {
+ console.error('[Dashboard] Collection error:', e.message);
+ return { error: e.message, timestamp: Date.now() };
+ }
+ }
}
diff --git a/modules/core/module-chatstudio.js b/modules/core/module-chatstudio.js
index 8e9d208..dd5ec65 100644
--- a/modules/core/module-chatstudio.js
+++ b/modules/core/module-chatstudio.js
@@ -501,8 +501,7 @@ class ChatStudioModule {
var shortNames = {
'llama3.2': 'Llama3.2',
'llama3': 'Llama3',
- 'gemma3:4b': 'Gemma3-4B',
- 'gemma3:1b': 'Gemma3-1B',
+ 'gemma4:31b-cloud': 'Gemma4-31B',
'gemma2:2b': 'Gemma2',
'qwen2.5:7b-instruct': 'Qwen2.5',
'qwen3:8b': 'Qwen3',
diff --git a/modules/core/module-debug-supervisor.js b/modules/core/module-debug-supervisor.js
new file mode 100644
index 0000000..15fe5d4
--- /dev/null
+++ b/modules/core/module-debug-supervisor.js
@@ -0,0 +1,701 @@
+// module-debug-supervisor.js — Persistent error capture, clustering, root-cause analysis, and auto-fix
+// Commands: debug-stats, debug-history, debug-analyze, debug-trends, debug-fix, debug-health
+class DebugSupervisorModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._healthy = true;
+ this._errorDB = { errors: {}, clusters: {}, patterns: {}, sessionId: null };
+ this._maxErrors = 500;
+ this._ttlDays = 7;
+ this._writeDebounce = null;
+ this._pendingWrites = 0;
+ this._originalConsole = {};
+ this._originalOnError = null;
+ this._originalOnRejection = null;
+ this._originalFetch = null;
+ this._interceptorInstalled = false;
+ this._sessionId = this._generateSessionId();
+ }
+
+ _generateSessionId() {
+ try {
+ let sid = localStorage.getItem('webx11_debug_session');
+ if (!sid) {
+ sid = Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8);
+ localStorage.setItem('webx11_debug_session', sid);
+ }
+ return sid;
+ } catch (e) {
+ return Date.now().toString(36);
+ }
+ }
+
+ getHelpInfo() {
+ return {
+ 'debug-stats': 'debug-stats — Show error summary (total, unique, clusters, top issues)',
+ 'debug-history': 'debug-history [n] — Show last N errors (default: 20)',
+ 'debug-analyze': 'debug-analyze <hash> — Deep analysis of a specific error',
+ 'debug-trends': 'debug-trends [days] — Error frequency over time (default: 7)',
+ 'debug-fix': 'debug-fix <hash> — Suggest and optionally apply fix for an error',
+ 'debug-health': 'debug-health — Check supervisor module health status',
+ };
+ }
+
+ registerCommands() {
+ this.commands['debug-stats'] = () => this._cmdStats();
+ this.commands['debug-history'] = (args) => this._cmdHistory(args);
+ this.commands['debug-analyze'] = (args) => this._cmdAnalyze(args);
+ this.commands['debug-trends'] = (args) => this._cmdTrends(args);
+ this.commands['debug-fix'] = (args) => this._cmdFix(args);
+ this.commands['debug-health'] = () => this._cmdHealth();
+ }
+
+ seed() {
+ // seed() is not called automatically by module manager — interceptors go in init()
+ }
+
+ init() {
+ this._loadDB();
+ this._installInterceptors();
+ }
+
+ // ── Error Database ──
+
+ _loadDB() {
+ try {
+ const raw = localStorage.getItem('webx11_debug_db');
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ this._errorDB = parsed;
+ this._pruneExpired();
+ }
+ } catch (e) {
+ // Corrupted DB — start fresh
+ this._errorDB = { errors: {}, clusters: {}, patterns: {}, sessionId: this._sessionId };
+ }
+ }
+
+ _saveDB() {
+ try {
+ localStorage.setItem('webx11_debug_db', JSON.stringify(this._errorDB));
+ } catch (e) {
+ // Quota exceeded — evict oldest 20% and retry
+ this._evictLRU(0.2);
+ try {
+ localStorage.setItem('webx11_debug_db', JSON.stringify(this._errorDB));
+ } catch (e2) {
+ // Still failing — supervisor degrades gracefully
+ console.warn('[DebugSupervisor] DB write failed, errors may be lost');
+ }
+ }
+ }
+
+ _scheduleSave() {
+ this._pendingWrites++;
+ if (this._writeDebounce) return;
+ this._writeDebounce = setTimeout(() => {
+ this._writeDebounce = null;
+ if (this._pendingWrites > 0) {
+ this._saveDB();
+ this._pendingWrites = 0;
+ }
+ }, 2000);
+ }
+
+ _pruneExpired() {
+ const now = Date.now();
+ const maxAge = this._ttlDays * 86400000;
+ let changed = false;
+ for (const [hash, entry] of Object.entries(this._errorDB.errors)) {
+ if (now - entry.lastSeen > maxAge) {
+ delete this._errorDB.errors[hash];
+ changed = true;
+ }
+ }
+ if (changed) this._saveDB();
+ }
+
+ _evictLRU(fraction) {
+ const entries = Object.entries(this._errorDB.errors);
+ const sorted = entries.sort((a, b) => a[1].lastSeen - b[1].lastSeen);
+ const toRemove = Math.ceil(sorted.length * fraction);
+ for (let i = 0; i < toRemove; i++) {
+ delete this._errorDB.errors[sorted[i][0]];
+ }
+ }
+
+ _checkQuota() {
+ try {
+ const total = 0;
+ for (const key in localStorage) {
+ // Estimate: key + value length
+ }
+ // Simple check: try writing a test key
+ const testKey = '__quota_test__';
+ localStorage.setItem(testKey, '1');
+ localStorage.removeItem(testKey);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ // ── Error Classification ──
+
+ _classifyError(message, source, stack) {
+ const type = this._detectType(message, stack);
+ const severity = this._detectSeverity(type, message);
+ const category = this._detectCategory(message, source, type);
+ return { type, severity, category };
+ }
+
+ _detectType(message, stack) {
+ if (!message) return 'unknown';
+ if (message.includes('ReferenceError') || message.includes('is not defined')) return 'ReferenceError';
+ if (message.includes('TypeError') || message.includes('is not a function') || message.includes('cannot read property')) return 'TypeError';
+ if (message.includes('SyntaxError') || message.includes('Unexpected token')) return 'SyntaxError';
+ if (message.includes('HTTP') || message.includes('NetworkError') || message.includes('Failed to fetch')) return 'HTTP';
+ if (message.includes('timeout') || message.includes('Timeout') || message.includes('AbortError')) return 'timeout';
+ if (message.includes('QuotaExceededError') || message.includes('quota')) return 'quota';
+ if (message.includes('SecurityError') || message.includes('permission')) return 'security';
+ if (stack && stack.includes('tool_')) return 'tool';
+ return 'runtime';
+ }
+
+ _detectSeverity(type, message) {
+ if (type === 'SyntaxError' || type === 'ReferenceError' || type === 'quota') return 'fatal';
+ if (type === 'HTTP' && message.includes('5')) return 'error';
+ if (type === 'timeout' || type === 'security') return 'error';
+ if (type === 'tool') return 'warning';
+ return 'error';
+ }
+
+ _detectCategory(message, source, type) {
+ if (type === 'HTTP' || type === 'timeout') return 'network';
+ if (type === 'quota') return 'vfs';
+ if (type === 'security') return 'security';
+ if (source && (source.includes('module-') || source.includes('ext/'))) return 'module';
+ if (type === 'tool') return 'tool';
+ return 'runtime';
+ }
+
+ _extractModule(source) {
+ if (!source) return null;
+ const m = source.match(/(module-[a-z-]+)\.js/);
+ return m ? m[1] : null;
+ }
+
+ _computeHash(message, source, type) {
+ const raw = (message || '') + '|' + (source || '') + '|' + (type || '');
+ // Simple hash (Fowler-Noll-Vo)
+ let hash = 0x811c9dc5;
+ for (let i = 0; i < raw.length; i++) {
+ hash ^= raw.charCodeAt(i);
+ hash = Math.imul(hash, 0x01000193);
+ }
+ return (hash >>> 0).toString(16);
+ }
+
+ // ── Error Interception ──
+
+ _installInterceptors() {
+ if (this._interceptorInstalled) return;
+ const self = this;
+
+ // Console.error
+ this._originalConsole.error = console.error;
+ console.error = function() {
+ const args = Array.prototype.slice.call(arguments);
+ const msg = args.map(a => typeof a === 'object' ? (a.message || JSON.stringify(a)) : String(a)).join(' ');
+ self._recordError(msg, 'console.error', null);
+ return self._originalConsole.error.apply(console, args);
+ };
+
+ // window.onerror
+ this._originalOnError = window.onerror;
+ window.onerror = function(msg, src, line, col, err) {
+ const fullMsg = msg + (err ? ' — ' + err.message : '') + ' (' + src + ':' + line + ':' + col + ')';
+ self._recordError(fullMsg, src || 'window.onerror', err ? err.stack : null);
+ if (self._originalOnError) return self._originalOnError.call(window, msg, src, line, col, err);
+ };
+
+ // Unhandled rejections
+ this._originalOnRejection = window.onunhandledrejection;
+ window.addEventListener('unhandledrejection', function(e) {
+ const msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled Promise rejection';
+ self._recordError(msg, 'unhandledrejection', e.reason ? e.reason.stack : null);
+ });
+
+ // Fetch failures
+ this._originalFetch = window.fetch;
+ window.fetch = function() {
+ const args = arguments;
+ const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '?');
+ return self._originalFetch.apply(window, args).catch(function(err) {
+ self._recordError('Fetch failed: ' + url + ' — ' + (err.message || err), url, err.stack);
+ throw err;
+ });
+ };
+
+ this._interceptorInstalled = true;
+ }
+
+ // ── Record Error ──
+
+ recordToolError(toolName, args, errorMessage) {
+ this._recordError(errorMessage, 'tool:' + toolName, null);
+ }
+
+ _recordError(message, source, stack) {
+ if (!this._healthy) return;
+ const classification = this._classifyError(message, source, stack);
+ const hash = this._computeHash(message, source, classification.type);
+ const now = Date.now();
+
+ if (this._errorDB.errors[hash]) {
+ this._errorDB.errors[hash].count++;
+ this._errorDB.errors[hash].lastSeen = now;
+ if (this._errorDB.errors[hash].sessions.indexOf(this._sessionId) === -1) {
+ this._errorDB.errors[hash].sessions.push(this._sessionId);
+ }
+ } else {
+ // LRU eviction if at cap
+ const keys = Object.keys(this._errorDB.errors);
+ if (keys.length >= this._maxErrors) {
+ this._evictLRU(0.1);
+ }
+ this._errorDB.errors[hash] = {
+ count: 1,
+ firstSeen: now,
+ lastSeen: now,
+ message: message,
+ type: classification.type,
+ severity: classification.severity,
+ category: classification.category,
+ source: source,
+ stack: stack ? stack.split('\n').slice(0, 10).join('\n') : null,
+ module: this._extractModule(source),
+ sessions: [this._sessionId],
+ };
+ }
+
+ this._scheduleSave();
+ }
+
+ // ── Clustering (Jaccard similarity) ──
+
+ _normalizeMessage(msg) {
+ if (!msg) return '';
+ return msg
+ .toLowerCase()
+ .replace(/\b0x[a-f0-9]+\b/g, '0xHEX')
+ .replace(/\b\d+\b/g, 'N')
+ .replace(/["'].*?["']/g, '"STR"')
+ .replace(/[^\w\s]/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+ }
+
+ _jaccardSimilarity(a, b) {
+ const setA = new Set(a.split(' ').filter(Boolean));
+ const setB = new Set(b.split(' ').filter(Boolean));
+ if (setA.size === 0 && setB.size === 0) return 1;
+ let intersection = 0;
+ for (const token of setA) {
+ if (setB.has(token)) intersection++;
+ }
+ const union = setA.size + setB.size - intersection;
+ return union === 0 ? 0 : intersection / union;
+ }
+
+ _clusterErrors() {
+ const errors = Object.entries(this._errorDB.errors);
+ if (errors.length === 0) return [];
+
+ const clusters = [];
+ const assigned = new Set();
+
+ for (let i = 0; i < errors.length; i++) {
+ if (assigned.has(i)) continue;
+ const [hashI, entryI] = errors[i];
+ const normI = this._normalizeMessage(entryI.message);
+ const cluster = { hashes: [hashI], messages: [entryI.message], count: entryI.count, types: new Set([entryI.type]), categories: new Set([entryI.category]) };
+ assigned.add(i);
+
+ for (let j = i + 1; j < errors.length; j++) {
+ if (assigned.has(j)) continue;
+ const [hashJ, entryJ] = errors[j];
+ const normJ = this._normalizeMessage(entryJ.message);
+ if (this._jaccardSimilarity(normI, normJ) >= 0.35) {
+ cluster.hashes.push(hashJ);
+ cluster.messages.push(entryJ.message);
+ cluster.count += entryJ.count;
+ cluster.types.add(entryJ.type);
+ cluster.categories.add(entryJ.category);
+ assigned.add(j);
+ }
+ }
+
+ cluster.label = this._mostFrequent(cluster.messages);
+ cluster.typeCount = cluster.types.size;
+ cluster.categoryCount = cluster.categories.size;
+ delete cluster.types;
+ delete cluster.categories;
+ clusters.push(cluster);
+ }
+
+ clusters.sort((a, b) => b.count - a.count);
+ return clusters;
+ }
+
+ _mostFrequent(arr) {
+ const freq = {};
+ let best = arr[0], bestCount = 1;
+ for (const item of arr) {
+ freq[item] = (freq[item] || 0) + 1;
+ if (freq[item] > bestCount) {
+ best = item;
+ bestCount = freq[item];
+ }
+ }
+ return best;
+ }
+
+ // ── Root-Cause Analysis ──
+
+ _analyzeRootCause(hash) {
+ const entry = this._errorDB.errors[hash];
+ if (!entry) return { found: false };
+
+ const findings = [];
+ const fixes = [];
+
+ // Tool failure → check module
+ if (entry.source && entry.source.startsWith('tool:')) {
+ const toolName = entry.source.replace('tool:', '');
+ const mm = this.terminal?.moduleManager;
+ if (mm) {
+ // Check if any module has this command
+ let found = false;
+ for (const [modName, mod] of Object.entries(mm.modules)) {
+ if (mod.commands && mod.commands[toolName]) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ findings.push({ type: 'module_not_loaded', detail: 'No module provides command "' + toolName + '"' });
+ fixes.push({ action: 'loadmod', params: toolName, description: 'Load module providing ' + toolName });
+ }
+ }
+ }
+
+ // HTTP error → check network
+ if (entry.type === 'HTTP' || entry.type === 'timeout') {
+ findings.push({ type: 'network_issue', detail: 'Network request failed: ' + entry.message.substring(0, 100) });
+ fixes.push({ action: 'test-net', params: null, description: 'Run network diagnostics' });
+ }
+
+ // ReferenceError → check bundle
+ if (entry.type === 'ReferenceError') {
+ findings.push({ type: 'missing_class', detail: 'ReferenceError suggests a class or variable is not defined' });
+ fixes.push({ action: 'reload', params: null, description: 'Reload the page to re-initialize all modules' });
+ }
+
+ // Module error → check module loader
+ if (entry.category === 'module') {
+ findings.push({ type: 'module_error', detail: 'Error in module: ' + (entry.module || 'unknown') });
+ fixes.push({ action: 'loadmod', params: entry.module, description: 'Reload module: ' + entry.module });
+ }
+
+ return { found: findings.length > 0, findings, fixes };
+ }
+
+ // ── Fix Registry ──
+
+ _buildFixSuggestion(hash) {
+ const entry = this._errorDB.errors[hash];
+ if (!entry) return null;
+
+ const rca = this._analyzeRootCause(hash);
+ if (!rca.found) {
+ return { confidence: 0, suggestion: 'No known fix pattern for this error', autoApply: false };
+ }
+
+ // Calculate confidence based on frequency
+ let confidence = Math.min(0.5, entry.count / 10);
+ if (entry.count >= 3) confidence = 0.7;
+ if (entry.count >= 10) confidence = 0.9;
+
+ return {
+ confidence: confidence,
+ suggestion: rca.findings.map(f => f.detail).join('; '),
+ fixes: rca.fixes,
+ autoApply: confidence >= 0.9,
+ };
+ }
+
+ // ── Terminal Commands ──
+
+ _cmdStats() {
+ const db = this._errorDB.errors;
+ const keys = Object.keys(db);
+ if (keys.length === 0) {
+ this.terminal.print('📊 Debug Stats: No errors recorded', 'success');
+ return;
+ }
+
+ const totalCount = keys.reduce((sum, k) => sum + db[k].count, 0);
+ const bySeverity = {};
+ const byCategory = {};
+ for (const k of keys) {
+ const e = db[k];
+ bySeverity[e.severity] = (bySeverity[e.severity] || 0) + e.count;
+ byCategory[e.category] = (byCategory[e.category] || 0) + e.count;
+ }
+
+ const clusters = this._clusterErrors();
+ const topClusters = clusters.slice(0, 5);
+
+ this.terminal.print('📊 Debug Stats', 'header');
+ this.terminal.print(' Total errors: ' + totalCount, 'info');
+ this.terminal.print(' Unique errors: ' + keys.length, 'info');
+ this.terminal.print(' Clusters: ' + clusters.length, 'info');
+ this.terminal.print(' Session: ' + this._sessionId, 'text-muted');
+ this.terminal.print(' Supervisor: ' + (this._healthy ? '✅ healthy' : '🔴 degraded'), this._healthy ? 'success' : 'error');
+
+ this.terminal.print('── By Severity ──', 'info');
+ for (const [sev, count] of Object.entries(bySeverity)) {
+ const icon = sev === 'fatal' ? '🔴' : sev === 'error' ? '🟡' : '🟢';
+ this.terminal.print(' ' + icon + ' ' + sev + ': ' + count, sev === 'fatal' ? 'error' : 'warning');
+ }
+
+ this.terminal.print('── By Category ──', 'info');
+ for (const [cat, count] of Object.entries(byCategory)) {
+ this.terminal.print(' • ' + cat + ': ' + count, 'text-muted');
+ }
+
+ if (topClusters.length > 0) {
+ this.terminal.print('── Top Clusters ──', 'info');
+ for (const c of topClusters) {
+ this.terminal.print(' [' + c.hashes.length + ' errors ×' + c.count + '] ' + c.label.substring(0, 80), 'warning');
+ }
+ this.terminal.print(' Run debug-analyze <hash> for details', 'text-muted');
+ }
+ }
+
+ _cmdHistory(args) {
+ const n = (args && args[0]) ? parseInt(args[0]) : 20;
+ const entries = Object.entries(this._errorDB.errors)
+ .sort((a, b) => b[1].lastSeen - a[1].lastSeen)
+ .slice(0, n);
+
+ if (entries.length === 0) {
+ this.terminal.print('No errors recorded', 'success');
+ return;
+ }
+
+ this.terminal.print('── Error History (last ' + entries.length + ') ──', 'header');
+ for (const [hash, e] of entries) {
+ const time = new Date(e.lastSeen).toLocaleTimeString();
+ const icon = e.severity === 'fatal' ? '🔴' : e.severity === 'error' ? '🟡' : '🟢';
+ const msg = e.message.substring(0, 80);
+ this.terminal.print(' ' + icon + ' [' + time + '] ' + msg, e.severity === 'fatal' ? 'error' : 'text-muted');
+ this.terminal.print(' hash=' + hash + ' type=' + e.type + ' cat=' + e.category + ' ×' + e.count, 'text-muted');
+ }
+ this.terminal.print('── End ──', 'info');
+ this.terminal.print('Run debug-analyze <hash> for details', 'text-muted');
+ }
+
+ _cmdAnalyze(args) {
+ const hash = (args && args[0]) || '';
+ if (!hash) {
+ this.terminal.print('Usage: debug-analyze <hash>', 'warning');
+ return;
+ }
+
+ const entry = this._errorDB.errors[hash];
+ if (!entry) {
+ this.terminal.print('Error not found: ' + hash, 'error');
+ return;
+ }
+
+ this.terminal.print('── Error Analysis: ' + hash + ' ──', 'header');
+ this.terminal.print(' Message: ' + entry.message, 'info');
+ this.terminal.print(' Type: ' + entry.type + ' | Severity: ' + entry.severity + ' | Category: ' + entry.category, 'info');
+ this.terminal.print(' Source: ' + (entry.source || '?'), 'text-muted');
+ this.terminal.print(' Module: ' + (entry.module || 'unknown'), 'text-muted');
+ this.terminal.print(' Count: ' + entry.count + ' | First: ' + new Date(entry.firstSeen).toLocaleString() + ' | Last: ' + new Date(entry.lastSeen).toLocaleString(), 'info');
+ this.terminal.print(' Sessions: ' + (entry.sessions || []).length, 'text-muted');
+ if (entry.stack) {
+ this.terminal.print('── Stack Trace (top 10) ──', 'info');
+ this.terminal.print(entry.stack, 'text-muted');
+ }
+
+ // Root-cause analysis
+ const rca = this._analyzeRootCause(hash);
+ if (rca.found) {
+ this.terminal.print('── Root Cause Analysis ──', 'warning');
+ for (const f of rca.findings) {
+ this.terminal.print(' • [' + f.type + '] ' + f.detail, 'warning');
+ }
+ if (rca.fixes.length > 0) {
+ this.terminal.print('── Suggested Fixes ──', 'success');
+ for (const fix of rca.fixes) {
+ this.terminal.print(' → ' + fix.action + (fix.params ? ' ' + fix.params : '') + ' — ' + fix.description, 'success');
+ }
+ }
+ } else {
+ this.terminal.print(' No root cause identified', 'text-muted');
+ }
+
+ // Fix suggestion
+ const fix = this._buildFixSuggestion(hash);
+ if (fix && fix.confidence > 0) {
+ this.terminal.print('── Auto-Fix Confidence: ' + Math.round(fix.confidence * 100) + '% ──', fix.autoApply ? 'success' : 'warning');
+ this.terminal.print(' ' + fix.suggestion, 'info');
+ if (fix.autoApply) {
+ this.terminal.print(' ⚡ Confidence ≥ 90% — fix can be auto-applied with debug-fix ' + hash, 'success');
+ }
+ }
+ }
+
+ _cmdTrends(args) {
+ const days = (args && args[0]) ? parseInt(args[0]) : 7;
+ const cutoff = Date.now() - days * 86400000;
+
+ const entries = Object.values(this._errorDB.errors).filter(e => e.lastSeen >= cutoff);
+ if (entries.length === 0) {
+ this.terminal.print('No errors in the last ' + days + ' days', 'success');
+ return;
+ }
+
+ // Group by day
+ const byDay = {};
+ for (const e of entries) {
+ const day = new Date(e.lastSeen).toISOString().split('T')[0];
+ if (!byDay[day]) byDay[day] = { total: 0, byType: {} };
+ byDay[day].total += e.count;
+ byDay[day].byType[e.type] = (byDay[day].byType[e.type] || 0) + e.count;
+ }
+
+ const sortedDays = Object.keys(byDay).sort();
+ this.terminal.print('── Error Trends (last ' + days + ' days) ──', 'header');
+ for (const day of sortedDays) {
+ const d = byDay[day];
+ const types = Object.entries(d.byType).map(([t, c]) => t + ':' + c).join(', ');
+ this.terminal.print(' ' + day + ': ' + d.total + ' errors (' + types + ')', d.total > 5 ? 'warning' : 'text-muted');
+ }
+
+ // Top error types
+ const typeTotals = {};
+ for (const e of entries) {
+ typeTotals[e.type] = (typeTotals[e.type] || 0) + e.count;
+ }
+ this.terminal.print('── By Type ──', 'info');
+ for (const [type, count] of Object.entries(typeTotals).sort((a, b) => b[1] - a[1])) {
+ this.terminal.print(' • ' + type + ': ' + count, 'text-muted');
+ }
+ }
+
+ _cmdFix(args) {
+ const hash = (args && args[0]) || '';
+ if (!hash) {
+ this.terminal.print('Usage: debug-fix <hash>', 'warning');
+ return;
+ }
+
+ const fix = this._buildFixSuggestion(hash);
+ if (!fix || fix.confidence === 0) {
+ this.terminal.print('No fix available for error: ' + hash, 'warning');
+ return;
+ }
+
+ this.terminal.print('── Fix Suggestion for ' + hash + ' ──', 'header');
+ this.terminal.print(' Confidence: ' + Math.round(fix.confidence * 100) + '%', fix.autoApply ? 'success' : 'warning');
+ this.terminal.print(' Suggestion: ' + fix.suggestion, 'info');
+
+ if (fix.fixes && fix.fixes.length > 0) {
+ this.terminal.print('── Available Fixes ──', 'info');
+ for (const f of fix.fixes) {
+ this.terminal.print(' → ' + f.action + (f.params ? ' ' + f.params : ''), 'success');
+ this.terminal.print(' ' + f.description, 'text-muted');
+ }
+ }
+
+ if (fix.autoApply) {
+ this.terminal.print('⚡ Auto-applying fix...', 'info');
+ this._applyFix(hash, fix);
+ } else {
+ this.terminal.print('Confidence below auto-apply threshold. Run the suggested command manually.', 'warning');
+ }
+ }
+
+ _applyFix(hash, fix) {
+ if (!fix.fixes || fix.fixes.length === 0) return;
+ const firstFix = fix.fixes[0];
+ try {
+ if (firstFix.action === 'loadmod' && this.terminal) {
+ this.terminal.executeCommand('loadmod ' + (firstFix.params || ''));
+ this.terminal.print(' ✅ Applied: loadmod ' + (firstFix.params || ''), 'success');
+ } else if (firstFix.action === 'test-net' && this.terminal) {
+ this.terminal.executeCommand('test-net');
+ this.terminal.print(' ✅ Applied: test-net', 'success');
+ } else if (firstFix.action === 'reload') {
+ this.terminal.print(' ℹ️ Manual action needed: reload the page', 'warning');
+ } else {
+ this.terminal.print(' ℹ️ Cannot auto-apply: ' + firstFix.action, 'warning');
+ }
+ } catch (e) {
+ this.terminal.print(' ❌ Fix failed: ' + e.message, 'error');
+ }
+ }
+
+ _cmdHealth() {
+ this.terminal.print('── DebugSupervisor Health ──', 'header');
+ this.terminal.print(' Status: ' + (this._healthy ? '✅ Healthy' : '🔴 Degraded'), this._healthy ? 'success' : 'error');
+ this.terminal.print(' Session: ' + this._sessionId, 'text-muted');
+ this.terminal.print(' Errors tracked: ' + Object.keys(this._errorDB.errors).length + ' unique', 'info');
+ this.terminal.print(' Max errors: ' + this._maxErrors, 'text-muted');
+ this.terminal.print(' TTL: ' + this._ttlDays + ' days', 'text-muted');
+ this.terminal.print(' Interceptors: ' + (this._interceptorInstalled ? '✅ active' : '🔴 inactive'), this._interceptorInstalled ? 'success' : 'error');
+ this.terminal.print(' Quota: ' + (this._checkQuota() ? '✅ available' : '🔴 low'), this._checkQuota() ? 'success' : 'error');
+ }
+
+ // ── Dashboard Data ──
+
+ getDashboardData() {
+ const db = this._errorDB.errors;
+ const keys = Object.keys(db);
+ const totalCount = keys.reduce((sum, k) => sum + db[k].count, 0);
+ const clusters = this._clusterErrors().slice(0, 5);
+
+ return {
+ totalUnique: keys.length,
+ totalCount: totalCount,
+ clusters: clusters.map(c => ({ label: c.label.substring(0, 60), count: c.count, hashes: c.hashes.length })),
+ healthy: this._healthy,
+ topType: this._getTopType(db),
+ topCategory: this._getTopCategory(db),
+ };
+ }
+
+ _getTopType(db) {
+ const freq = {};
+ for (const e of Object.values(db)) {
+ freq[e.type] = (freq[e.type] || 0) + e.count;
+ }
+ const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]);
+ return sorted.length > 0 ? sorted[0][0] : null;
+ }
+
+ _getTopCategory(db) {
+ const freq = {};
+ for (const e of Object.values(db)) {
+ freq[e.category] = (freq[e.category] || 0) + e.count;
+ }
+ const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]);
+ return sorted.length > 0 ? sorted[0][0] : null;
+ }
+}
diff --git a/modules/core/module-duckduckgo.js b/modules/core/module-duckduckgo.js
deleted file mode 100644
index c443b72..0000000
--- a/modules/core/module-duckduckgo.js
+++ /dev/null
@@ -1,38 +0,0 @@
-// Extracted from newOS4 index.html — DuckDuckGoModule
- class DuckDuckGoModule {
- constructor() {
- this.commands = {};
- this.terminal = null;
- }
- getHelpInfo() {
- return {
- 'ddg': 'ddg <query> — Search DuckDuckGo (opens query in new tab)',
- 'duck': 'duck <query> — Alias for ddg',
- };
- }
- registerCommands() {
- this.commands['ddg'] = (args) => this.search(args);
- this.commands['duck'] = (args) => this.search(args);
- }
- async search(args) {
- if (!args[0]) {
- this.terminal.print("Usage: ddg <search-query>", "warning");
- this.terminal.print("Example: ddg javascript tutorials", "info");
- this.terminal.print("Note: Due to CORS, results may be limited. Try 'search' command for better results.", "warning");
- return;
- }
- const query = args.join(' ');
- this.terminal.print(`🦆 DuckDuckGo Search: ${query}`, "info");
-
- // DuckDuckGo doesn't have a public API, so we provide alternatives
- this.terminal.print("⚠️ DuckDuckGo doesn't provide a public API for direct access.", "warning");
- this.terminal.print("Opening search in new tab...", "info");
-
- // Open in new tab as fallback
- const ddgUrl = `https://duckduckgo.com/?q=${encodeURIComponent(query)}`;
- window.open(ddgUrl, '_blank');
-
- this.terminal.print(`\n💡 Alternative: Use 'search <query>' for SearXNG (privacy-focused)`, "info");
- this.terminal.print(`🔗 Direct link: ${ddgUrl}`, "info");
- }
- }
diff --git a/modules/core/module-dynamicprompts.js b/modules/core/module-dynamicprompts.js
deleted file mode 100644
index ff8c5ea..0000000
--- a/modules/core/module-dynamicprompts.js
+++ /dev/null
@@ -1,102 +0,0 @@
-// Extracted from newOS4 index.html — DynamicPromptsModule
- class DynamicPromptsModule {
- constructor() {
- this.commands = {}; this.terminal = null;
- this.prompts = this.loadPrompts(); this.variables = {};
- }
- getHelpInfo() {
- return {
- 'prompt': 'prompt <name> [additional-text] — Use a saved prompt template',
- 'prompt-save': 'prompt-save <name> <template> — Save a new prompt template',
- 'prompt-list': 'prompt-list — List all saved prompt templates',
- 'prompt-delete': 'prompt-delete <name> — Delete a saved prompt',
- 'prompt-var': 'prompt-var <name> <value> — Set a template variable',
- 'prompt-vars': 'prompt-vars — List all template variables'
- };
- }
- loadPrompts() {
- try {
- const saved = localStorage.getItem('webx11_prompts');
- return saved ? JSON.parse(saved) : {};
- } catch (e) { return {}; }
- }
- savePrompts() {
- localStorage.setItem('webx11_prompts', JSON.stringify(this.prompts));
- }
- registerCommands() {
- this.commands['prompt'] = (args) => this.usePrompt(args);
- this.commands['prompt-save'] = (args) => this.savePrompt(args);
- this.commands['prompt-list'] = () => this.listPrompts();
- this.commands['prompt-delete'] = (args) => this.deletePrompt(args);
- this.commands['prompt-var'] = (args) => this.setVariable(args);
- this.commands['prompt-vars'] = () => this.listVariables();
- }
- usePrompt(args) {
- if (!args[0]) {
- this.terminal.print("Usage: prompt <name> [additional-text]", "warning"); return;
- }
- const name = args[0], additional = args.slice(1).join(' ');
- if (!this.prompts[name]) {
- this.terminal.print(`Prompt '${name}' not found`, "error"); return;
- }
- let template = this.prompts[name].template;
- Object.entries(this.variables).forEach(([k, v]) => {
- template = template.replace(new RegExp(`\\{${k}\\}`, 'g'), v);
- });
- const finalPrompt = additional ? `${template} ${additional}` : template;
- this.terminal.print("=== Generated Prompt ===", "info");
- this.terminal.printHTML(`<div class="output-line ollama" style="margin-top: 5px;">
- <button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentElement.innerText.replace('Copy','').trim())">Copy</button>
- ${finalPrompt}
- </div>`);
- this.terminal.print("\nUse with: ai <generated-prompt>", "info");
- }
- savePrompt(args) {
- if (args.length < 2) {
- this.terminal.print("Usage: prompt-save <name> <template>", "warning"); return;
- }
- const name = args[0], template = args.slice(1).join(' ');
- this.prompts[name] = { template, created: new Date().toISOString() };
- this.savePrompts();
- this.terminal.print(`Prompt '${name}' saved`, "success");
- }
- listPrompts() {
- const names = Object.keys(this.prompts);
- if (names.length === 0) {
- this.terminal.print("No saved prompts", "info"); return;
- }
- this.terminal.print("=== Saved Prompts ===", "info");
- names.forEach(name => {
- const p = this.prompts[name];
- this.terminal.print(`📝 ${name}: ${p.template.substring(0, 50)}...`);
- });
- }
- deletePrompt(args) {
- if (!args[0]) {
- this.terminal.print("Usage: prompt-delete <name>", "warning"); return;
- }
- if (this.prompts[args[0]]) {
- delete this.prompts[args[0]];
- this.savePrompts();
- this.terminal.print(`Prompt '${args[0]}' deleted`, "success");
- } else {
- this.terminal.print(`Prompt '${args[0]}' not found`, "error");
- }
- }
- setVariable(args) {
- if (args.length < 2) {
- this.terminal.print("Usage: prompt-var <name> <value>", "warning"); return;
- }
- const name = args[0], value = args.slice(1).join(' ');
- this.variables[name] = value;
- this.terminal.print(`Variable set: ${name} = ${value}`, "success");
- }
- listVariables() {
- const names = Object.keys(this.variables);
- if (names.length === 0) {
- this.terminal.print("No variables set", "info"); return;
- }
- this.terminal.print("=== Variables ===", "info");
- names.forEach(name => { this.terminal.print(`${name}: ${this.variables[name]}`); });
- }
- }
diff --git a/modules/core/module-io.js b/modules/core/module-io.js
deleted file mode 100644
index d7ff996..0000000
--- a/modules/core/module-io.js
+++ /dev/null
@@ -1,80 +0,0 @@
-// Extracted from newOS4 index.html — ModuleIOModule
- class ModuleIOModule {
- constructor() { this.commands = {}; this.terminal = null; }
- getHelpInfo() {
- return {
- 'export-mods': 'export-mods — Export all loaded modules to a JSON file',
- 'import-mods': 'import-mods <filename> — Import modules from a JSON file',
- 'backup': 'backup — Create a full system backup',
- 'restore': 'restore <filename> — Restore system from a backup file',
- 'share-mod': 'share-mod <module-name> — Share a module as JSON to clipboard'
- };
- }
- registerCommands() {
- this.commands['export-mods'] = () => this.exportModules();
- this.commands['import-mods'] = (args) => this.importModules(args);
- this.commands['backup'] = () => this.fullBackup();
- this.commands['restore'] = (args) => this.restoreBackup(args);
- this.commands['share-mod'] = (args) => this.shareModule(args);
- }
- exportModules() {
- const registry = this.terminal.moduleManager.moduleRegistry;
- if (registry.length === 0) {
- this.terminal.print("No modules to export", "warning"); return;
- }
- const exportData = { version: '4.1', exported: new Date().toISOString(), modules: registry };
- const json = JSON.stringify(exportData, null, 2);
- const blob = new Blob([json], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url; a.download = `webx11-modules-${Date.now()}.json`; a.click();
- URL.revokeObjectURL(url);
- this.terminal.print(`Exported ${registry.length} module(s)`, "success");
- }
- importModules(args) {
- this.terminal.print("⚠️ To import modules:", "warning");
- this.terminal.print("1. Upload the JSON file using 'upload'", "info");
- this.terminal.print("2. Use: import-mods <filename>", "info");
- }
- async fullBackup() {
- const backup = {
- version: '4.1', timestamp: new Date().toISOString(),
- filesystem: localStorage.getItem('webx11_filesystem'),
- modules: localStorage.getItem('webx11_modules'),
- ollama: {
- endpoint: localStorage.getItem('webx11_ollama_endpoint'),
- model: localStorage.getItem('webx11_ollama_model')
- }, config: {}
- };
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (key.startsWith('webx11_')) { backup.config[key] = localStorage.getItem(key); }
- }
- const json = JSON.stringify(backup, null, 2);
- const blob = new Blob([json], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url; a.download = `webx11-backup-${Date.now()}.json`; a.click();
- URL.revokeObjectURL(url);
- this.terminal.print("Full backup created", "success");
- }
- restoreBackup(args) {
- this.terminal.print("⚠️ To restore backup:", "warning");
- this.terminal.print("1. Upload the backup JSON file", "info");
- this.terminal.print("2. Use: restore <filename>", "info");
- }
- shareModule(args) {
- if (!args[0]) {
- this.terminal.print("Usage: share-mod <module-name>", "warning"); return;
- }
- const mod = this.terminal.moduleManager.moduleRegistry.find(m => m.name === args[0]);
- if (!mod) {
- this.terminal.print(`Module '${args[0]}' not found`, "error"); return;
- }
- const shareData = { name: mod.name, url: mod.url, shared: new Date().toISOString() };
- const json = JSON.stringify(shareData, null, 2);
- navigator.clipboard.writeText(json).then(() => {
- this.terminal.print("Module info copied to clipboard", "success");
- }).catch(() => { this.terminal.print(json, "info"); });
- }
- }
diff --git a/modules/core/module-leases.js b/modules/core/module-leases.js
index 7489c92..60e8d98 100644
--- a/modules/core/module-leases.js
+++ b/modules/core/module-leases.js
@@ -38,10 +38,15 @@ class LeaseModule {
this.terminal.print('📜 Lease system loaded', 'info');
}
createLease(args) {
+ // Parse --write flag
+ const writeIdx = args.indexOf('--write');
+ const writeAccess = writeIdx !== -1;
+ if (writeIdx !== -1) args.splice(writeIdx, 1);
+
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');
+ if (!workerId) return this.terminal.print('Usage: lease-create <agent-id> [duration_sec] [task] [--write]', 'error');
const lease = {
id: `lease-${Date.now()}`,
@@ -51,7 +56,8 @@ class LeaseModule {
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + duration * 1000).toISOString(),
status: 'active',
- duration
+ duration,
+ write_access: writeAccess
};
this._leases[lease.id] = lease;
this.terminal.fs.writeFile(`leases/${lease.id}.json`, JSON.stringify(lease));
@@ -79,9 +85,14 @@ class LeaseModule {
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');
+ const writeTag = l.write_access ? ' ✏️write' : ' 🔍read';
+ this.terminal.print(` ${l.id} → ${l.workerId} (${remaining}s remain)${writeTag} — ${l.task.substring(0, 40)}`, 'info');
});
}
+ hasWriteAccess(agentId) {
+ // Check if any active lease grants write access for this agent
+ return Object.values(this._leases).some(l => l.workerId === agentId && l.status === 'active' && l.write_access === true);
+ }
revokeLease(id) {
if (!this._leases[id]) return this.terminal.print(`❌ Lease ${id} not found`, 'error');
this._leases[id].status = 'revoked';
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 7bb7c89..52af62a 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = '2026-06-30-v4.5.83';
+ const LATEST_VER = 'v4.6.10';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/core/module-memory-graph.js b/modules/core/module-memory-graph.js
index 2382a4a..ffc79e0 100644
--- a/modules/core/module-memory-graph.js
+++ b/modules/core/module-memory-graph.js
@@ -12,6 +12,11 @@ class MemoryGraphModule {
this._debounceTimer = null;
this._initialized = false;
this._totalNodeAccesses = 0;
+ // v4.5.89: Decay configuration
+ this._decayDays = 30;
+ this._decayMinUses = 3;
+ this._decayRate = 0.01; // Daily decay rate
+ this._lastDecayRun = 0;
}
getHelpInfo() {
@@ -21,6 +26,10 @@ class MemoryGraphModule {
'memgraph-stats': 'memgraph-stats — Most connected nodes, strongest edges, cluster info',
'memgraph-export': 'memgraph-export — Export graph as JSON',
'memgraph-prune': 'memgraph-prune — Remove stale low-weight edges (>30d, <3 accesses)',
+ 'memgraph-learn': 'memgraph-learn <entity> <fact> — Add a fact to the memory graph',
+ 'memgraph-session': 'memgraph-session [date] — Show graph nodes for a session (default: today)',
+ 'memgraph-agent': 'memgraph-agent <agentId> — Show namespaced nodes for an agent',
+ 'memgraph-decay-config': 'memgraph-decay-config <days> <min_uses> — Configure decay policy (default: 30d, 3 uses)',
};
}
@@ -30,6 +39,11 @@ class MemoryGraphModule {
this.commands['memgraph-stats'] = () => this.showStats();
this.commands['memgraph-export'] = () => this.exportGraph();
this.commands['memgraph-prune'] = () => this.prune();
+ this.commands['memgraph-learn'] = (args) => this.learn(args);
+ this.commands['memgraph-session'] = (args) => this.showSession(args.join(' '));
+ this.commands['memgraph-agent'] = (args) => this.showAgent(args.join(' '));
+ // v4.5.89: Decay configuration
+ this.commands['memgraph-decay-config'] = (args) => this.decayConfig(args);
}
init(terminal) {
@@ -38,13 +52,16 @@ class MemoryGraphModule {
this._initialized = true;
this.terminal.print('🧠 Memory Graph loaded — cross-session context active', 'info');
// Log startup for graph
- this.addNode('session', `session-${Date.now()}`, `Session ${new Date().toLocaleDateString()}`);
+ // v4.5.89 fix: Use day-floored key to match edge source keys (P0-1 from review)
+ this.addNode('session', `session-${Math.floor(Date.now() / 86400000)}`, `Session ${new Date().toLocaleDateString()}`);
}
// ─── Node API ───
- addNode(type, id, label, metadata = {}) {
- const key = `${type}:${id}`;
+ addNode(type, id, label, metadata = {}, agentId = null) {
+ // v4.5.88: Support agent namespacing with dual-lookup
+ const ns = agentId ? `agent:${agentId}` : 'global';
+ const key = agentId ? `${ns}:${type}:${id}` : `${type}:${id}`;
if (!this._nodes[key]) {
this._nodes[key] = {
id: key,
@@ -53,7 +70,9 @@ class MemoryGraphModule {
metadata: metadata || {},
created: new Date().toISOString(),
updated: new Date().toISOString(),
- access_count: 1
+ access_count: 1,
+ namespace: ns,
+ source_session: `session-${Math.floor(Date.now() / 86400000)}`
};
} else {
this._nodes[key].updated = new Date().toISOString();
@@ -67,8 +86,14 @@ class MemoryGraphModule {
return key;
}
- getNode(type, id) {
- return this._nodes[`${type}:${id}`] || null;
+ // v4.5.88: Dual-lookup — try namespaced key first, fall back to global
+ getNode(type, id, agentId = null) {
+ if (agentId) {
+ const nsKey = `agent:${agentId}:${type}:${id}`;
+ if (this._nodes[nsKey]) return this._nodes[nsKey];
+ }
+ const globalKey = `${type}:${id}`;
+ return this._nodes[globalKey] || null;
}
findNodes(query) {
@@ -82,7 +107,8 @@ class MemoryGraphModule {
// ─── Edge API ───
- addEdge(sourceKey, targetKey, type, weight = 1) {
+ // v4.5.88: addEdge tracks source_agents for multi-agent attribution
+ addEdge(sourceKey, targetKey, type, weight = 1, agentId = null) {
// Check if edge already exists
let edge = this._edges.find(e =>
e.source === sourceKey && e.target === targetKey && e.type === type
@@ -91,6 +117,7 @@ class MemoryGraphModule {
edge.weight = (edge.weight + weight) / 2;
edge.last_used = new Date().toISOString();
edge.use_count++;
+ if (agentId) edge.source_agents = [...new Set([...(edge.source_agents || []), agentId])];
} else {
this._edges.push({
source: sourceKey,
@@ -99,7 +126,8 @@ class MemoryGraphModule {
weight,
created: new Date().toISOString(),
last_used: new Date().toISOString(),
- use_count: 1
+ use_count: 1,
+ source_agents: agentId ? [agentId] : []
});
}
this._debounceSave();
@@ -107,11 +135,12 @@ class MemoryGraphModule {
// ─── Event-based Learning ───
- recordFileAccess(path) {
+ // v4.5.89 fix: Thread agentId through record* methods (P1-3 from review)
+ recordFileAccess(path, agentId = null) {
const name = path.split('/').pop() || path;
- const nodeKey = this.addNode('file', path, name);
+ const nodeKey = this.addNode('file', path, name, {}, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, nodeKey, 'accessed', 0.5);
+ this.addEdge(sessionKey, nodeKey, 'accessed', 0.5, agentId);
// Extract file extension edge
const ext = path.split('.').pop();
@@ -121,10 +150,10 @@ class MemoryGraphModule {
}
}
- recordToolCall(toolName, success) {
- const nodeKey = this.addNode('tool', toolName, toolName);
+ recordToolCall(toolName, success, agentId = null) {
+ const nodeKey = this.addNode('tool', toolName, toolName, {}, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8);
+ this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8, agentId);
}
recordConcept(concept, filePath) {
@@ -134,15 +163,16 @@ class MemoryGraphModule {
this.addEdge(fileKey, conceptKey, 'tagged_as', 0.7);
}
+ // v4.5.88: recordTask now passes agentId for namespacing + adds source_session link
recordTask(task, result, agentId) {
const taskId = `task-${Date.now()}`;
const taskKey = this.addNode('task', taskId, task.substring(0, 60), {
full: task,
result: result?.substring(0, 100),
agent: agentId
- });
+ }, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, taskKey, 'executed', 0.6);
+ this.addEdge(sessionKey, taskKey, 'executed', 0.6, agentId);
// Extract keywords as concepts
const words = task.toLowerCase().split(/\s+/).filter(w => w.length > 3);
@@ -201,18 +231,38 @@ class MemoryGraphModule {
}
}
- return lines.join('\\n');
+ return lines.join('\n');
}
getContextForPrompt() {
const summary = this.getContextSummary();
if (!summary) return '';
- return `[Memory Graph Context]\\n${summary}\\n`;
+ // Suppress noise: don't inject when graph is nearly empty
+ const totalNodes = Object.keys(this._nodes).length;
+ const totalEdges = this._edges.length;
+ if (totalNodes <= 2 && totalEdges === 0) return '';
+ return `[Memory Graph Context]\n${summary}\n`;
+ }
+
+ // Alias for orchestrator compatibility (orchestrator calls getStats, not showStats)
+ getStats() {
+ const types = {};
+ for (const n of Object.values(this._nodes)) {
+ types[n.type] = (types[n.type] || 0) + 1;
+ }
+ return {
+ nodes: Object.keys(this._nodes).length,
+ edges: this._edges.length,
+ types: types,
+ totalAccesses: this._totalNodeAccesses
+ };
}
// ─── Commands ───
showStatus() {
+ // v4.5.89: Lazy decay — run applyDecay() on status check
+ this.applyDecay();
const types = {};
for (const n of Object.values(this._nodes)) {
types[n.type] = (types[n.type] || 0) + 1;
@@ -227,6 +277,8 @@ class MemoryGraphModule {
this.terminal.print(` Edges: ${this._edges.length}`, 'info');
this.terminal.print(` Total accesses: ${this._totalNodeAccesses}`, 'info');
this.terminal.print(` Path: ${this._indexPath}`, 'info');
+ // v4.5.89: Show decay config
+ this.terminal.print(` Decay: ${this._decayDays}d threshold, ${this._decayMinUses} min uses, rate ${this._decayRate}/day`, 'text-muted');
}
search(query) {
@@ -346,6 +398,181 @@ class MemoryGraphModule {
this.terminal.print(`🧹 Pruned ${pruned} edges and ${nodesPruned} orphan nodes`, nodesPruned > 0 ? 'warning' : 'info');
}
+ // v4.5.89: Apply decay scores to nodes based on age and access patterns
+ applyDecay() {
+ const now = Date.now();
+ if (now - this._lastDecayRun < 3600000) return;
+ this._lastDecayRun = now;
+
+ // v4.5.89b fix: Compute decay rate from _decayDays (P0 from review)
+ const effectiveRate = 0.9 / this._decayDays;
+ let decayed = 0;
+ let removed = 0;
+
+ for (const key of Object.keys(this._nodes)) {
+ const node = this._nodes[key];
+ const ageDays = (now - new Date(node.updated || node.created).getTime()) / 86400000;
+ node.decay_score = Math.max(0, 1.0 - (ageDays * effectiveRate));
+ if (node.decay_score < 0.1 && node.access_count < this._decayMinUses) {
+ if (node.type === 'session') continue;
+ if (node.namespace && node.namespace.startsWith('agent:')) continue;
+ delete this._nodes[key];
+ removed++;
+ continue;
+ }
+ decayed++;
+ }
+
+ this._edges = this._edges.filter(e => {
+ const ageDays = (now - new Date(e.last_used).getTime()) / 86400000;
+ e.decay_score = Math.max(0, 1.0 - (ageDays * effectiveRate));
+ return !(e.decay_score < 0.1 && (e.use_count || 1) < this._decayMinUses);
+ });
+
+ if (removed > 0) {
+ this._debounceSave();
+ console.log('[MemoryGraph] Decay: ' + decayed + ' scored, ' + removed + ' auto-removed');
+ }
+
+ // v4.5.90: Check for stale skills
+ var staleSkills = this.getStaleSkills();
+ if (staleSkills.length > 0) {
+ this.terminal.print('🧠 Stale skills: ' + staleSkills.length + ' (' + staleSkills.map(function(s) { return s.name; }).join(', ') + ')', 'warning');
+ }
+ }
+
+ // v4.5.90: Record skill usage for decay tracking
+ recordSkillUse(name, success) {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ if (!index[name]) index[name] = { useCount: 0, successCount: 0 };
+ index[name].lastUsed = new Date().toISOString();
+ index[name].useCount = (index[name].useCount || 0) + 1;
+ if (success !== false) {
+ index[name].successCount = (index[name].successCount || 0) + 1;
+ }
+ fs.fsWriteJSON('/skills/index.json', index);
+ } catch (e) {
+ console.warn('[MemoryGraph] Skill use tracking error:', e.message);
+ }
+ }
+
+ // v4.5.90: Get stale skills (unused >_decayDays, useCount == 0, not immune)
+ getStaleSkills() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return [];
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const now = Date.now();
+ // v4.5.90b fix: Use configurable _decayDays (P1 from review)
+ const staleThreshold = this._decayDays * 24 * 60 * 60 * 1000;
+ var stale = [];
+ for (var name of Object.keys(index)) {
+ var stats = index[name] || {};
+ if (stats.immune) continue;
+ var useCount = stats.useCount || 0;
+ var lastUsed = stats.lastUsed ? new Date(stats.lastUsed).getTime() : 0;
+ if (useCount === 0 && (now - lastUsed > staleThreshold || lastUsed === 0)) {
+ var daysUnused = lastUsed > 0 ? Math.floor((now - lastUsed) / 86400000) : 999;
+ stale.push({ name: name, daysUnused: daysUnused });
+ }
+ }
+ return stale;
+ } catch (e) {
+ return [];
+ }
+ }
+
+ // v4.5.89: Configure decay policy
+ decayConfig(args) {
+ if (args.length < 2) {
+ this.terminal.print('Usage: memgraph-decay-config <days> <min_uses>', 'error');
+ this.terminal.print(' Current: ' + this._decayDays + 'd threshold, ' + this._decayMinUses + ' min uses', 'info');
+ this.terminal.print(' Decay rate: ' + this._decayRate + '/day (0.01 = 1% per day)', 'info');
+ return;
+ }
+ const days = parseInt(args[0]);
+ const minUses = parseInt(args[1]);
+ if (isNaN(days) || days < 1) return this.terminal.print('Days must be >= 1', 'error');
+ if (isNaN(minUses) || minUses < 0) return this.terminal.print('Min uses must be >= 0', 'error');
+ this._decayDays = days;
+ this._decayMinUses = minUses;
+ this._debounceSave();
+ this.terminal.print('✅ Decay config: ' + days + 'd threshold, ' + minUses + ' min uses', 'success');
+ }
+
+ learn(args) {
+ if (!args || args.length < 2) return this.terminal.print('Usage: memgraph-learn <entity> <fact>', 'error');
+ const entity = args[0];
+ const fact = args.slice(1).join(' ');
+ this.addNode('concept', entity, entity);
+ const factKey = this.addNode('fact', entity + '-' + Date.now(), fact);
+ const conceptKey = `concept:${entity}`;
+ this.addEdge(conceptKey, factKey, 'has_fact', 0.9);
+ this.terminal.print(`✅ Learned: ${entity} → ${fact}`, 'success');
+ // v4.5.89b fix: Index only the new fact, not all facts (P2 from review — O(n) → O(1))
+ const nvfs = this.terminal.moduleManager.modules['NeuralVFSModule'];
+ if (nvfs && typeof nvfs.indexGraphFacts === 'function') {
+ nvfs.indexSingleFact(factKey, entity, fact);
+ }
+ }
+
+ // v4.5.88: Show graph nodes for a specific session
+ showSession(dateStr) {
+ let sessionKey;
+ if (dateStr) {
+ const d = new Date(dateStr);
+ if (isNaN(d.getTime())) return this.terminal.print('Usage: memgraph-session [YYYY-MM-DD]', 'error');
+ sessionKey = `session-${Math.floor(d.getTime() / 86400000)}`;
+ } else {
+ sessionKey = `session-${Math.floor(Date.now() / 86400000)}`;
+ }
+
+ const sessionNodes = Object.values(this._nodes).filter(function(n) {
+ return n.source_session === sessionKey || n.type === 'session' && n.id.includes(sessionKey);
+ });
+
+ this.terminal.print('📅 Session Graph: ' + sessionKey, 'header');
+ if (sessionNodes.length === 0) {
+ this.terminal.print(' No nodes found for this session', 'info');
+ return;
+ }
+ this.terminal.print(' ' + sessionNodes.length + ' nodes:', 'info');
+ for (var n of sessionNodes.slice(0, 15)) {
+ var ns = n.namespace || 'global';
+ this.terminal.print(' ' + n.type + ':' + n.label + ' (' + ns + ', ' + n.access_count + 'x)', 'info');
+ }
+ var sessionEdges = this._edges.filter(function(e) {
+ return sessionNodes.some(function(n) { return n.id === e.source || n.id === e.target; });
+ });
+ this.terminal.print(' ' + sessionEdges.length + ' edges', 'info');
+ }
+
+ // v4.5.88: Show namespaced nodes for a specific agent
+ showAgent(agentId) {
+ if (!agentId) return this.terminal.print('Usage: memgraph-agent <agentId>', 'error');
+ var ns = 'agent:' + agentId;
+ var agentNodes = Object.values(this._nodes).filter(function(n) {
+ return n.namespace === ns;
+ });
+ var agentEdges = this._edges.filter(function(e) {
+ return (e.source_agents || []).includes(agentId);
+ });
+
+ this.terminal.print('🤖 Agent Memory: ' + agentId, 'header');
+ if (agentNodes.length === 0) {
+ this.terminal.print(' No namespaced nodes for this agent', 'info');
+ this.terminal.print(' (Agent may have used global namespace)', 'text-muted');
+ return;
+ }
+ this.terminal.print(' ' + agentNodes.length + ' nodes, ' + agentEdges.length + ' edges:', 'info');
+ for (var n of agentNodes.slice(0, 15)) {
+ this.terminal.print(' ' + n.type + ':' + n.label + ' (' + n.access_count + 'x)', 'info');
+ }
+ }
+
// ─── Persistence ───
_loadFromVFS() {
@@ -357,18 +584,48 @@ class MemoryGraphModule {
const exportData = fs.fsReadJSON(`${this._indexPath}export.json`);
if (exportData && exportData.nodes) {
for (const n of exportData.nodes) {
+ // v4.5.89 fix: Migrate old-format nodes (P1-1 from review)
+ if (!n.namespace) n.namespace = 'global';
+ if (!n.source_session) {
+ const createdMs = new Date(n.created).getTime();
+ n.source_session = isNaN(createdMs) ? null : `session-${Math.floor(createdMs / 86400000)}`;
+ }
this._nodes[n.id] = n;
}
this._edges = exportData.edges || [];
+ for (const e of this._edges) {
+ if (!e.source_agents) e.source_agents = [];
+ }
this._totalNodeAccesses = (exportData.stats?.totalAccesses) || 0;
return;
}
// Fallback: load nodes and edges separately
const nodes = fs.fsReadJSON(`${this._indexPath}nodes.json`);
- if (nodes) this._nodes = nodes;
+ if (nodes) {
+ this._nodes = nodes;
+ for (const n of Object.values(this._nodes)) {
+ if (!n.namespace) n.namespace = 'global';
+ if (!n.source_session) {
+ const createdMs = new Date(n.created).getTime();
+ n.source_session = isNaN(createdMs) ? null : `session-${Math.floor(createdMs / 86400000)}`;
+ }
+ }
+ }
const edges = fs.fsReadJSON(`${this._indexPath}edges.json`);
- if (edges) this._edges = edges;
+ if (edges) {
+ this._edges = edges;
+ for (const e of this._edges) {
+ if (!e.source_agents) e.source_agents = [];
+ }
+ }
+ // v4.5.89b fix: Load persisted decay config (P1 from review)
+ const config = fs.fsReadJSON(`${this._indexPath}config.json`);
+ if (config) {
+ if (typeof config.decayDays === 'number') this._decayDays = config.decayDays;
+ if (typeof config.decayMinUses === 'number') this._decayMinUses = config.decayMinUses;
+ if (typeof config.lastDecayRun === 'number') this._lastDecayRun = config.lastDecayRun;
+ }
} catch (e) {
console.warn('[MemoryGraph] Load error (fresh start):', e.message);
}
@@ -383,6 +640,12 @@ class MemoryGraphModule {
}
fs.fsWriteJSON(`${this._indexPath}nodes.json`, this._nodes);
fs.fsWriteJSON(`${this._indexPath}edges.json`, this._edges);
+ // v4.5.89b fix: Persist decay config (P1 from review)
+ fs.fsWriteJSON(`${this._indexPath}config.json`, {
+ decayDays: this._decayDays,
+ decayMinUses: this._decayMinUses,
+ lastDecayRun: this._lastDecayRun
+ });
} catch (e) {
console.error('[MemoryGraph] Save error:', e.message);
}
diff --git a/modules/core/module-my.js b/modules/core/module-my.js
deleted file mode 100644
index 5fc13ca..0000000
--- a/modules/core/module-my.js
+++ /dev/null
@@ -1,82 +0,0 @@
-class MyModule {
- constructor() {
- this.commands = {}; this.terminal = null; this.userData = {};
- try { const d = localStorage.getItem('webx11_my_data'); if (d) this.userData = JSON.parse(d); } catch (e) {}
- }
- getHelpInfo() {
- return {
- 'my': 'my — Show module info and stored key count',
- 'my-set': 'my-set <key> <value> — Store a key-value pair',
- 'my-get': 'my-get <key> — Retrieve a stored value by key',
- 'my-list': 'my-list — List all stored keys',
- 'my-export': 'my-export — Export all stored data as JSON',
- 'my-import': 'my-import <json> — Import data from JSON string',
- 'my-clear': 'my-clear — Clear all stored data'
- };
- }
- _save() {
- try { localStorage.setItem('webx11_my_data', JSON.stringify(this.userData)); } catch (e) {}
- }
- registerCommands() {
- this.commands['my'] = () => this.showInfo();
- this.commands['my-set'] = (args) => this.setData(args);
- this.commands['my-get'] = (args) => this.getData(args);
- this.commands['my-list'] = () => this.listData();
- this.commands['my-export'] = () => this.exportData();
- this.commands['my-import'] = (args) => this.importData(args);
- this.commands['my-clear'] = () => this.clearData();
- }
- showInfo() {
- this.terminal.print("=== My Module v2.0 ===", "info");
- this.terminal.print("Personal workspace for custom commands");
- this.terminal.print(`Stored keys: ${Object.keys(this.userData).length}`, "success");
- }
- setData(args) {
- if (args.length < 2) {
- this.terminal.print("Usage: my-set <key> <value>", "warning"); return;
- }
- const key = args[0], value = args.slice(1).join(' ');
- this.userData[key] = { value, timestamp: new Date().toISOString() };
- this._save();
- this.terminal.print(`Saved: ${key} = ${value}`, "success");
- }
- getData(args) {
- if (!args[0]) {
- this.terminal.print("Usage: my-get <key>", "warning"); return;
- }
- if (this.userData[args[0]]) {
- this.terminal.print(`${args[0]}: ${this.userData[args[0]].value}`, "info");
- } else {
- this.terminal.print(`Key '${args[0]}' not found`, "error");
- }
- }
- listData() {
- const keys = Object.keys(this.userData);
- if (!keys.length) return this.terminal.print('No data stored.', 'info');
- this.terminal.print('=== My Data ===', 'info');
- keys.forEach(k => {
- const v = this.userData[k];
- const type = typeof v.value;
- this.terminal.print(` ${k} (${type}): ${String(v.value).substring(0, 40)}`);
- });
- }
- exportData() {
- this.terminal.print(JSON.stringify(this.userData, null, 2));
- }
- importData(args) {
- if (!args[0]) return this.terminal.print('Usage: my-import <json>', 'warning');
- try {
- const data = JSON.parse(args.join(' '));
- Object.assign(this.userData, data);
- this._save();
- this.terminal.print(`Imported ${Object.keys(data).length} keys`, 'success');
- } catch (e) {
- this.terminal.print('Invalid JSON: ' + e.message, 'error');
- }
- }
- clearData() {
- this.userData = {};
- localStorage.removeItem('webx11_my_data');
- this.terminal.print("Personal data cleared", "success");
- }
-}
diff --git a/modules/core/module-neural-vfs.js b/modules/core/module-neural-vfs.js
index 32fb0eb..1019299 100644
--- a/modules/core/module-neural-vfs.js
+++ b/modules/core/module-neural-vfs.js
@@ -33,7 +33,7 @@ class NeuralVFSModule {
this._loadAliases();
// Deep Indexing toggle
this._deepIndexEnabled = localStorage.getItem('webx11_nvfs_deep') === 'true';
- this._deepModel = localStorage.getItem('webx11_nvfs_deep_model') || 'ministral-3:3b-cloud';
+ this._deepModel = localStorage.getItem('webx11_nvfs_deep_model') || 'gemma4:31b-cloud';
this._deepIndexing = false;
this._deepQueue = [];
this._deepProcessed = 0;
@@ -80,6 +80,7 @@ class NeuralVFSModule {
this.commands['nvfs-sync'] = async (args) => this.nvfsSync(args);
this.commands['nvfs-save'] = async (args) => this.nvfsSave();
this.commands['nvfs-cancel'] = (args) => this.cancelIndex();
+ this.commands['nvfs-index-graph'] = () => this.indexGraphFacts();
}
// ── Core Index Builder (async, chunked) ──
@@ -1044,6 +1045,61 @@ Concepts:`;
}
}
}
+
+ // v4.5.89: Semantic bridge — mirror graph facts into VFS for unified search
+ indexGraphFacts() {
+ const mg = this.terminal.moduleManager.modules['MemoryGraphModule'];
+ if (!mg) return this.terminal.print('❌ Memory Graph module not loaded', 'error');
+ const fs = this.terminal.fs;
+ if (!fs) return this.terminal.print('❌ VFS not available', 'error');
+
+ const factNodes = Object.values(mg._nodes).filter(function(n) {
+ return n.type === 'concept' || n.type === 'fact' || n.type === 'entity' || n.type === 'task';
+ });
+
+ if (factNodes.length === 0) {
+ return this.terminal.print('No graph fact nodes to index. Use memgraph-learn first.', 'info');
+ }
+
+ if (!fs.readDir('/_graph_facts')) fs.mkdir('_graph_facts');
+
+ // v4.5.89b fix: Clean stale files for deleted nodes (P2 from review)
+ const validKeys = new Set(factNodes.map(function(n) { return n.id.replace(/[^a-zA-Z0-9_-]/g, '_') + '.txt'; }));
+ try {
+ const existing = fs.readDir('/_graph_facts') || [];
+ for (const f of existing) {
+ if (!validKeys.has(f)) fs.deleteFile('/_graph_facts/' + f);
+ }
+ } catch (_) {}
+
+ let indexed = 0;
+ for (const node of factNodes) {
+ let content = node.label + '\n';
+ if (node.metadata) {
+ if (node.metadata.full) content += node.metadata.full + '\n';
+ if (node.metadata.result) content += 'Result: ' + node.metadata.result + '\n';
+ }
+ content += 'Type: ' + node.type + '\n';
+ content += 'Namespace: ' + (node.namespace || 'global') + '\n';
+ if (node.source_session) content += 'Session: ' + node.source_session + '\n';
+ const safeName = node.id.replace(/[^a-zA-Z0-9_-]/g, '_');
+ fs.writeFile('/_graph_facts/' + safeName + '.txt', content);
+ indexed++;
+ }
+
+ this.terminal.print('🔗 Indexed ' + indexed + ' graph facts into /_graph_facts/', 'success');
+ this.terminal.print(' Run nvfs-index to include them in semantic search', 'info');
+ }
+
+ // v4.5.89b: Index a single fact — O(1) instead of O(n) re-index (P2 from review)
+ indexSingleFact(factKey, entity, fact) {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ if (!fs.readDir('/_graph_facts')) fs.mkdir('_graph_facts');
+ const safeName = factKey.replace(/[^a-zA-Z0-9_-]/g, '_');
+ const content = entity + '\n' + fact + '\nType: fact\nNamespace: global\n';
+ fs.writeFile('/_graph_facts/' + safeName + '.txt', content);
+ }
}
// Required for module system
diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js
index ba43dfd..59d9136 100644
--- a/modules/core/module-orchestrator.js
+++ b/modules/core/module-orchestrator.js
@@ -32,6 +32,8 @@ class OrchestratorModule {
this._activeConsensus = null;
this._pipelineHistory = [];
this._PIPELINE_CONFIG = { autoFix: false, stageTimeout: 30000 };
+ this._schedules = [];
+ this._scheduleTimer = null;
this._loadConfig();
}
_loadConfig() {
@@ -84,6 +86,9 @@ class OrchestratorModule {
this.commands['consensus-status'] = () => this.consensusStatus();
this.commands['consensus-history'] = () => this.consensusHistory();
this.commands['pipeline'] = (args) => this.runPipeline(args);
+ this.commands['orchestrator-schedule'] = (args) => this.addSchedule(args);
+ this.commands['orchestrator-schedules'] = () => this.listSchedules();
+ this.commands['orchestrator-unschedule'] = (args) => this.removeSchedule(args[0]);
}
// ─── Task Decomposition Engine ───
_decomposeGoal(goal) {
@@ -146,6 +151,9 @@ class OrchestratorModule {
'consensus-status': 'consensus-status — Show current/active consensus round status',
'consensus-history': 'consensus-history — Show past consensus rounds and their resolutions',
'pipeline': 'pipeline <test|deploy|verify|history|config> — Autonomous CI/CD: test agents/queue/modules, deploy (test→auto-fix→verify), history (show runs), config (show config)',
+ 'orchestrator-schedule': 'orchestrator-schedule <role> <interval_seconds> <task...> — Schedule a recurring task delegation',
+ 'orchestrator-schedules': 'orchestrator-schedules — List all active recurring schedules',
+ 'orchestrator-unschedule': 'orchestrator-unschedule <id> — Remove a recurring schedule',
'orchestrator-status': 'orchestrator-status — Show agent pool status and config',
'orchestrator-spawn': 'orchestrator-spawn [role] [capabilities...] — Manually spawn a new agent',
'orchestrator-kill': 'orchestrator-kill <agent-id> — Terminate an agent and revoke leases',
@@ -170,13 +178,17 @@ class OrchestratorModule {
this._reanimateFromVFS();
// A2A mailbox polling loop (500ms)
if (this._A2A_POLLING) {
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
}
// Heartbeat watchdog
- this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), this._HEARTBEAT_CHECK);
+ this._heartbeatInterval = this.terminal.moduleManager._setInterval(() => this._checkHeartbeats(), this._HEARTBEAT_CHECK);
// Idle timeout sweep
- this._idleSweepInterval = setInterval(() => this._sweepIdle(), this._IDLE_SWEEP_INTERVAL);
+ this._idleSweepInterval = this.terminal.moduleManager._setInterval(() => this._sweepIdle(), this._IDLE_SWEEP_INTERVAL);
this.terminal.print('🧠 Orchestrator loaded — A2A mailbox transport active', 'success');
+ // Load recurring schedules from VFS
+ this._loadSchedules();
+ // Pre-spawn default agents after a short delay (let other modules init first)
+ setTimeout(() => this._preSpawnDefaultAgents(), 2000);
// Subscribe to event bus for in-memory A2A delivery (instant, no VFS poll)
this._on('a2a:message', (msg) => {
if (msg.type === 'heartbeat' && msg.from && this._agents[msg.from]) {
@@ -561,16 +573,16 @@ class OrchestratorModule {
'a2a-polling': () => {
this._A2A_POLLING = val === 'on' || val === 'true';
if (this._A2A_POLLING && !this._a2aPollTimer) {
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
} else if (!this._A2A_POLLING && this._a2aPollTimer) {
- clearInterval(this._a2aPollTimer);
+ this.terminal.moduleManager._clearTimer(this._a2aPollTimer);
this._a2aPollTimer = null;
}
return `a2a-polling ${this._A2A_POLLING ? 'on' : 'off'}`;
},
- 'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; clearInterval(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; },
- 'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; clearInterval(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; },
- 'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; clearInterval(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; },
+ 'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; this.terminal.moduleManager._clearTimer(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; },
+ 'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; this.terminal.moduleManager._clearTimer(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = this.terminal.moduleManager._setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; },
+ 'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this.terminal.moduleManager._clearTimer(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = this.terminal.moduleManager._setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; },
'dead-purge-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this._DEAD_PURGE_TIMEOUT = n; return `dead-purge-timeout set to ${n}ms`; },
'consensus-size': () => { const n = parseInt(val); if (isNaN(n) || n < 2 || n > 10) return 'Must be 2-10'; this._CONSENSUS_SIZE = n; return `consensus-size set to ${n}`; },
'consensus-strategy': () => { if (!['majority', 'unanimous', 'weighted'].includes(val)) return 'Must be majority, unanimous, or weighted'; this._CONSENSUS_STRATEGY = val; return `consensus-strategy set to ${val}`; },
@@ -983,7 +995,7 @@ class OrchestratorModule {
// Fix 2: Restart A2A poller if stopped
if (!this._A2A_POLLING) {
this._A2A_POLLING = true;
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
this.terminal.print(' Restarted A2A polling', 'warning');
fixed++;
}
@@ -1113,9 +1125,12 @@ class OrchestratorModule {
}
}
- // Create a lease
+ // Create a lease — with --write if agent has write capabilities
const leaseModule = this.terminal.moduleManager.modules['LeaseModule'];
- const lease = leaseModule ? leaseModule.createLease(['lease-create', agent.id, '60', task]) : null;
+ const hasWriteCap = (agent.capabilities || []).some(function(c) { return c === 'write_file' || c === 'edit_module' || c === 'delete_file'; });
+ const leaseArgs = ['lease-create', agent.id, '60', task];
+ if (hasWriteCap) leaseArgs.push('--write');
+ const lease = leaseModule ? leaseModule.createLease(leaseArgs) : null;
// Mark agent as busy
agent.status = 'busy';
@@ -1184,6 +1199,109 @@ class OrchestratorModule {
return outboxResult || 'completed';
}
+ // ─── Recurring Scheduling ───
+
+ addSchedule(args) {
+ if (args.length < 2) return this.terminal.print('Usage: orchestrator-schedule <role> <interval_seconds> <task...>', 'error');
+ const role = args[0];
+ const interval = parseInt(args[1]);
+ if (isNaN(interval) || interval < 10) return this.terminal.print('Interval must be >= 10 seconds', 'error');
+ const task = args.slice(2).join(' ');
+ if (!task) return this.terminal.print('Task description required', 'error');
+
+ const schedule = {
+ id: 'sched-' + Date.now(),
+ role: role,
+ interval: interval * 1000,
+ task: task,
+ createdAt: new Date().toISOString(),
+ lastRun: null,
+ nextRun: Date.now() + interval * 1000,
+ runs: 0
+ };
+ this._schedules.push(schedule);
+ this._saveSchedules();
+ this._startScheduleTimer();
+ this.terminal.print('📅 Schedule created: "' + task.substring(0, 40) + '" every ' + interval + 's (role: ' + role + ')', 'success');
+ return schedule;
+ }
+
+ listSchedules() {
+ if (this._schedules.length === 0) return this.terminal.print('No active schedules', 'info');
+ this.terminal.print('📅 Recurring Schedules (' + this._schedules.length + '):', 'header');
+ for (const s of this._schedules) {
+ const nextIn = Math.max(0, Math.round((s.nextRun - Date.now()) / 1000));
+ this.terminal.print(' ' + s.id + ' — "' + s.task.substring(0, 40) + '" every ' + (s.interval / 1000) + 's [' + s.role + '] (next: ' + nextIn + 's, runs: ' + s.runs + ')', 'info');
+ }
+ }
+
+ removeSchedule(id) {
+ const idx = this._schedules.findIndex(function(s) { return s.id === id; });
+ if (idx === -1) return this.terminal.print('Schedule ' + id + ' not found', 'error');
+ this._schedules.splice(idx, 1);
+ this._saveSchedules();
+ if (this._schedules.length === 0 && this._scheduleTimer) {
+ clearInterval(this._scheduleTimer);
+ this._scheduleTimer = null;
+ }
+ this.terminal.print('Schedule ' + id + ' removed', 'success');
+ }
+
+ _startScheduleTimer() {
+ if (this._scheduleTimer) return;
+ this._scheduleTimer = setInterval(function() {
+ var now = Date.now();
+ for (var i = 0; i < this._schedules.length; i++) {
+ var s = this._schedules[i];
+ if (now >= s.nextRun) {
+ s.lastRun = new Date().toISOString();
+ s.nextRun = now + s.interval;
+ s.runs++;
+ this.terminal.print('📅 Schedule trigger: "' + s.task.substring(0, 40) + '"', 'info');
+ this.delegateTask(s.task);
+ }
+ }
+ this._saveSchedules();
+ }.bind(this), 5000);
+ }
+
+ _saveSchedules() {
+ try {
+ this.terminal.fs.fsWriteJSON('/orchestrator/schedules.json', this._schedules);
+ } catch (e) { console.warn('[Orchestrator] Schedule save error:', e.message); }
+ }
+
+ _loadSchedules() {
+ try {
+ const data = this.terminal.fs.fsReadJSON('/orchestrator/schedules.json');
+ if (data && Array.isArray(data)) {
+ this._schedules = data;
+ if (this._schedules.length > 0) this._startScheduleTimer();
+ }
+ } catch (e) { this._schedules = []; }
+ }
+
+ _preSpawnDefaultAgents() {
+ // Spawn 3 role-specific agents at boot
+ var roles = [
+ { role: 'auto-learner', caps: ['list_files', 'read_file', 'write_file', 'view_module'], task: 'audit skills' },
+ { role: 'kernel-architect', caps: ['list_files', 'read_file', 'write_file', 'run_command'], task: 'index VFS' },
+ { role: 'searcher', caps: ['list_files', 'read_file', 'web_search'], task: 'check stale data' }
+ ];
+ for (var i = 0; i < roles.length; i++) {
+ var r = roles[i];
+ // Check if an agent with this role already exists
+ var existing = Object.values(this._agents).find(function(a) { return a.role === r.role; });
+ if (!existing) {
+ var agent = this.spawnAgent(r.role, r.caps);
+ if (agent) {
+ agent.restartPolicy = 'always';
+ this.terminal.print('🤖 Pre-spawned agent: ' + r.role + ' (' + r.caps.length + ' caps)', 'info');
+ }
+ }
+ }
+ }
+
// ─── Monitoring ───
_checkHeartbeats() {
diff --git a/modules/core/module-smartctx.js b/modules/core/module-smartctx.js
index 0bfc91a..fc3882d 100644
--- a/modules/core/module-smartctx.js
+++ b/modules/core/module-smartctx.js
@@ -10,6 +10,7 @@
'contexts': 'contexts — List all available context profiles',
'context-new': 'context-new <name> <displayName> <systemPrompt> — Create a new context profile',
'context-delete': 'context-delete <name> — Delete a context profile',
+ 'context-auto': 'context-auto [on|off] — Toggle auto-routing (detects intent from user input)',
'smart-ask': 'smart-ask <question> — Ask a question using the active context'
};
}
@@ -55,6 +56,7 @@
this.commands['contexts'] = () => this.listContexts();
this.commands['context-new'] = (args) => this.createContext(args);
this.commands['context-delete'] = (args) => this.deleteContext(args);
+ this.commands['context-auto'] = (args) => this.toggleAutoRouting(args);
this.commands['smart-ask'] = (args) => this.smartAsk(args);
}
setContext(args) {
@@ -124,6 +126,116 @@
this.terminal.print(`Profile '${args[0]}' not found`, "error");
}
}
+ // ─── Intent Detection Engine ───
+ _getIntentPatterns() {
+ return {
+ 'coding': [
+ /^code\b/i, /\b(edit|fix|write|create|implement|refactor|build)\s+(module|function|class|file|code|script)/i,
+ /\b(debug|bug|error|fix)\s+(code|module|function|script)/i, /\b(implement|write|create)\s+(a\s+)?(function|class|module|command)/i,
+ /\b(refactor|rewrite|optimize)\s+(code|module|function)/i, /\b(add\s+)(feature|command|functionality)/i
+ ],
+ 'debug': [
+ /^debug\b/i, /\b(error|crash|bug|fail|traceback|exception|broken|not working)\b/i,
+ /\b(why|what happened|diagnose|investigate|find out why)\b/i, /\b(check|inspect|examine)\s+(log|error|issue|problem)/i
+ ],
+ 'research': [
+ /^research\b/i, /\b(search|find|look up|research|investigate|analyze)\b/i,
+ /\b(what is|how does|tell me about|explain)\b.*\?$/i, /\b(find|search)\s+(for\s+)?(information|docs|documentation|article)/i
+ ],
+ 'creative': [
+ /^brainstorm\b/i, /\b(create|design|imagine|generate|compose|draft)\s+(story|poem|art|design|idea|concept|mindmap)/i,
+ /\b(creative|inspire|innovative|novel)\b/i
+ ],
+ 'system-admin': [
+ /^(config|setup|install|deploy|backup|restore|security|monitor|maintenance)\b/i,
+ /\b(configure|setup|install|deploy|backup|restore)\s+(system|server|module|service)/i,
+ /\b(security|permission|access|block|allow)\b/i
+ ],
+ 'chat': [
+ /^(hi|hello|hey|how are you|what's up|good morning|good evening)\b/i,
+ /^.{0,30}\?$/ // short questions
+ ]
+ };
+ }
+ autoDetectIntent(text) {
+ if (!text || typeof text !== 'string') return { profile: null, confidence: 0 };
+ const patterns = this._getIntentPatterns();
+ const scores = {};
+ const lower = text.toLowerCase();
+
+ for (const [profile, regexes] of Object.entries(patterns)) {
+ let matches = 0;
+ for (const re of regexes) {
+ if (re.test(text) || re.test(lower)) matches++;
+ }
+ if (matches > 0) scores[profile] = matches;
+ }
+
+ // Code snippet detection (bias toward coding)
+ if (/[{([\s]*['"`]/.test(text) || /\b(function|class|const|let|var|import|export|=>)\b/.test(text)) {
+ scores['coding'] = (scores['coding'] || 0) + 2;
+ }
+
+ // Very short text → chat
+ if (text.length < 15 && !scores['coding'] && !scores['debug']) {
+ scores['chat'] = (scores['chat'] || 0) + 1;
+ }
+
+ // Find best match
+ let bestProfile = null;
+ let bestScore = 0;
+ let totalScore = 0;
+ for (const [profile, score] of Object.entries(scores)) {
+ totalScore += score;
+ if (score > bestScore) { bestScore = score; bestProfile = profile; }
+ }
+
+ // Calculate confidence (0-100)
+ const confidence = totalScore > 0 ? Math.round((bestScore / totalScore) * 100) : 0;
+
+ return { profile: bestProfile, confidence };
+ }
+ autoRouteContext(text) {
+ if (!text) return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: 0, reason: 'no input' };
+
+ // Check if auto-routing is enabled
+ const enabled = localStorage.getItem('webx11_context_auto') !== 'off';
+ if (!enabled) return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: 0, reason: 'auto-routing disabled' };
+
+ const result = this.autoDetectIntent(text);
+ if (!result.profile || result.confidence < 70) {
+ return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: result.confidence, reason: 'confidence too low (<70%)' };
+ }
+
+ // Don't switch if already on the best profile
+ if (result.profile === this.activeProfile) {
+ return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: result.confidence, reason: 'already on best profile' };
+ }
+
+ // Check if the target profile exists
+ if (!this.contextProfiles[result.profile]) {
+ return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: result.confidence, reason: 'profile "' + result.profile + '" not found' };
+ }
+
+ // Switch
+ const from = this.activeProfile;
+ this.setContext([result.profile]);
+ return { switched: true, from: from, to: result.profile, confidence: result.confidence, reason: 'intent detected' };
+ }
+ toggleAutoRouting(args) {
+ const val = args && args[0] ? args[0].toLowerCase() : null;
+ if (val === 'on' || val === 'true' || val === '1') {
+ localStorage.setItem('webx11_context_auto', 'on');
+ this.terminal.print('✅ Auto-routing enabled — context will switch based on intent', 'success');
+ } else if (val === 'off' || val === 'false' || val === '0') {
+ localStorage.setItem('webx11_context_auto', 'off');
+ this.terminal.print('⛔ Auto-routing disabled — use context <name> to switch manually', 'warning');
+ } else {
+ const current = localStorage.getItem('webx11_context_auto') !== 'off';
+ this.terminal.print('Auto-routing: ' + (current ? 'ON' : 'OFF'), 'info');
+ this.terminal.print('Usage: context-auto on|off', 'info');
+ }
+ }
async smartAsk(args) {
if (!args[0]) {
this.terminal.print("Usage: smart-ask <your-question>", "warning"); return;
diff --git a/modules/core/module-world-model.js b/modules/core/module-world-model.js
index 6f0c4e5..61eecfc 100644
--- a/modules/core/module-world-model.js
+++ b/modules/core/module-world-model.js
@@ -7,9 +7,14 @@ class WorldModelModule {
this.terminal = null;
this._model = null;
this._lastRefresh = null;
- this._REFRESH_INTERVAL = 300000; // 5 minutes
+ this._REFRESH_INTERVAL = 300000; // 5 minutes (fallback)
this._MAX_PROJECTS = 10;
this._refreshTimer = null;
+ // v4.5.89: Event-driven sync state
+ this._syncDebounce = null;
+ this._syncing = false;
+ this._lastEventSync = 0;
+ this._EVENT_DEBOUNCE_MS = 500;
}
getHelpInfo() {
@@ -51,6 +56,25 @@ class WorldModelModule {
this.terminal.print('🌐 World Model loaded — system self-awareness active', 'info');
}
+ // v4.5.89: Event-driven sync — debounced, recursion-guarded
+ _scheduleSync() {
+ if (this._syncing) return; // Prevent recursion during active synthesis
+ if (this._syncDebounce) clearTimeout(this._syncDebounce);
+ const now = Date.now();
+ // Skip if we synced < 2s ago (rate limit beyond debounce)
+ if (now - this._lastEventSync < 2000) return;
+ this._syncDebounce = setTimeout(() => {
+ this._syncDebounce = null;
+ this._syncing = true;
+ try {
+ this._synthesize();
+ this._lastEventSync = Date.now();
+ } finally {
+ this._syncing = false;
+ }
+ }, this._EVENT_DEBOUNCE_MS);
+ }
+
// ─── Synthesis Engine ───
_synthesize() {
@@ -119,9 +143,10 @@ class WorldModelModule {
try {
const mm = this.terminal?.moduleManager;
if (!mm) return 0;
- // Count registry entries (all known modules) vs loaded
- const registry = mm.moduleRegistry || [];
- return registry.length || Object.keys(mm.modules || {}).length;
+ // Use max of registry vs loaded so the ratio is never >1 (e.g. 43/7 → 43/43)
+ const loadedCount = Object.keys(mm.modules || {}).length;
+ const registryCount = (mm.moduleRegistry || []).length;
+ return Math.max(loadedCount, registryCount);
} catch (e) {
console.warn('[WorldModel] Total modules gather error:', e.message);
return Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0;
@@ -129,10 +154,10 @@ class WorldModelModule {
}
_gatherSystemInfo(fs, moduleCount, version) {
- // Get real health data from ServerMonitorModule's _latencyLog and AI assistant's _errorLog
+ // Get real health data from ServerMonitorModule's _latencyLog and DebugSupervisorModule
const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
- const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule'];
- const errorCount = ai?._errorLog?.length || 0;
+ const ds = this.terminal?.moduleManager?.modules?.['DebugSupervisorModule'];
+ const errorCount = ds ? Object.keys(ds._errorDB?.errors || {}).length : 0;
const latencyErrors = sm?._latencyLog ? Object.values(sm._latencyLog).filter(e => e.status === 'error').length : 0;
const totalErrors = errorCount + latencyErrors;
return {
@@ -228,11 +253,11 @@ class WorldModelModule {
_gatherTelemetry(fs) {
try {
- // Get real latency data from ServerMonitorModule and errors from AI assistant
+ // Get real latency data from ServerMonitorModule and errors from DebugSupervisorModule
const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
- const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule'];
+ const ds = this.terminal?.moduleManager?.modules?.['DebugSupervisorModule'];
const latencyDataPoints = sm?._latencyLog ? Object.keys(sm._latencyLog).length : 0;
- const recentErrors = ai?._errorLog?.length || 0;
+ const recentErrors = ds ? Object.keys(ds._errorDB?.errors || {}).length : 0;
return {
toolLatencyDataPoints: latencyDataPoints,
recentErrors: recentErrors,
@@ -352,41 +377,44 @@ class WorldModelModule {
if (!question) return this.terminal.print('Usage: world-model-ask <question>', 'error');
if (!this._model) return this.terminal.print('No model — run world-model-refresh first', 'warning');
- const q = question.toLowerCase();
+ const answer = this._answerQuery(question);
+ this.terminal.print(`❓ ${question}`, 'info');
+ this.terminal.print(`💡 ${answer}`, 'success');
+ }
+
+ // Shared query routing — used by both ask() (terminal) and world_model_query (AI tool)
+ _answerQuery(question) {
+ if (!this._model) return 'World model not synthesized yet — run world-model-refresh';
+ const q = (question || '').toLowerCase();
const m = this._model;
- let answer = '';
if (q.includes('module') || q.includes('loaded')) {
- answer = `Loaded ${m.modules.loaded} modules (${m.modules.modifiedCount} modified: ${m.modules.modified.join(', ') || 'none'})`;
+ return `Loaded ${m.modules.loaded} modules (${m.modules.modifiedCount} modified: ${m.modules.modified.join(', ') || 'none'})`;
} else if (q.includes('skill')) {
- answer = `Installed ${m.skills.count} skills: ${m.skills.skills.join(', ') || 'none'}`;
+ return `Installed ${m.skills.count} skills: ${m.skills.skills.join(', ') || 'none'}`;
} else if (q.includes('file') || q.includes('vfs')) {
- answer = `${m.files.total} files in ${m.files.directories} directories, total ${m.files.size}`;
+ return `${m.files.total} files in ${m.files.directories} directories, total ${m.files.size}`;
} else if (q.includes('agent') || q.includes('orchestrat')) {
- answer = `${m.agents.agents} agents (${m.agents.active} active)`;
+ return `${m.agents.agents} agents (${m.agents.active} active)`;
} else if (q.includes('health') || q.includes('status')) {
- answer = `Health: ${m.system.health} · Memory: ${m.system.memory} · ${m.telemetry.recentErrors > 0 ? `${m.telemetry.recentErrors} recent errors` : 'No errors'}`;
+ return `Health: ${m.system.health} · Memory: ${m.system.memory} · ${m.telemetry.recentErrors > 0 ? `${m.telemetry.recentErrors} recent errors` : 'No errors'}`;
} else if (q.includes('memory') || q.includes('graph')) {
- answer = `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`;
+ return `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`;
} else if (q.includes('change') || q.includes('yesterday') || q.includes('recent')) {
const recent = m.recent;
- answer = recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected';
+ return recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected';
} else if (q.includes('project') || q.includes('work')) {
const projects = m.projects.activeProjects;
- answer = projects.length > 0
+ return projects.length > 0
? `Active projects: ${projects.map(p => `${p.name} (${p.accesses}x)`).join(', ')}`
: 'No active projects detected';
} else if (q.includes('telemetry') || q.includes('latency')) {
- answer = `${m.telemetry.toolLatencyDataPoints} tool latency data points tracked`;
+ return `${m.telemetry.toolLatencyDataPoints} tool latency data points tracked, ${m.telemetry.recentErrors} recent errors`;
} else if (q.includes('summary') || q.includes('overview') || q.includes('all')) {
- answer = this.showSummary();
- return;
+ return `newOS4 ${m.version} · ${m.modules.loaded}/${m.modules.total} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents · ${m.memory.nodes} graph nodes · ${m.telemetry.recentErrors > 0 ? `⚠️ ${m.telemetry.recentErrors} errors` : '✅ OK'}`;
} else {
- answer = `I'm not sure how to answer "${question}". Try: modules, skills, files, agents, health, memory, changes, projects, telemetry, or summary.`;
+ return `Unknown query. Try: modules, skills, files, agents, health, memory, changes, projects, telemetry, or summary.`;
}
-
- this.terminal.print(`❓ ${question}`, 'info');
- this.terminal.print(`💡 ${answer}`, 'success');
}
refresh() {
@@ -429,7 +457,7 @@ class WorldModelModule {
const agentStr = m.agents.error
? 'agents: N/A'
: `${m.agents.agents} agents`;
- return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
+ return `[World Model Context]\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
}
// ─── Persistence ───
@@ -448,10 +476,20 @@ class WorldModelModule {
}
}
+ // v4.5.89: Atomic write — temp file + rename to prevent corruption
_saveToVFS(fs) {
try {
if (!fs.readDir('/system')) fs.mkdir('system');
- fs.fsWriteJSON('/system/world-model.json', this._model);
+ // v4.5.89b fix: Clean stale temp from previous crash (P1 from review)
+ try { fs.deleteFile('/system/world-model.tmp.json'); } catch (_) {}
+ fs.fsWriteJSON('/system/world-model.tmp.json', this._model);
+ const tmp = fs.fsReadJSON('/system/world-model.tmp.json');
+ if (tmp) {
+ fs.fsWriteJSON('/system/world-model.json', tmp);
+ fs.deleteFile('/system/world-model.tmp.json');
+ } else {
+ console.error('[WorldModel] Save failed: temp file corrupt after write');
+ }
} catch (e) {
console.error('[WorldModel] Save error:', e.message);
}
diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js
index 13472fc..4f47db2 100644
--- a/modules/core/ollama-manager.js
+++ b/modules/core/ollama-manager.js
@@ -26,7 +26,7 @@ class OllamaManager {
this.streamEnabled = localStorage.getItem('webx11_stream_enabled') === 'true';
// --- Validator Agent Config ---
this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true';
- this.validatorModel = localStorage.getItem('webx11_validator_model') || 'ministral-3:3b-cloud';
+ this.validatorModel = localStorage.getItem('webx11_validator_model') || 'gemma4:31b-cloud';
this.validatorEndpoint = localStorage.getItem('webx11_validator_endpoint') || '';
// --- Model Parameters (temperature, top_p, max_tokens, thinking) ---
this.modelParams = this._loadModelParams();
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index 3abb074..9896bee 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -7,19 +7,71 @@
this.lastRouteTime = 0;
this.lastRouteCmd = '';
this.blockedCount = 0;
- this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']);
- this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']);
- this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']);
- this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze']);
- // Load persisted overrides from localStorage
- try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){}
- try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){}
- try { const w = localStorage.getItem('webx11_security_whitelist'); if(w){JSON.parse(w).forEach(c=>this.whitelist.add(c));} }catch(e){}
- try { const f = localStorage.getItem('webx11_security_flags'); if(f){JSON.parse(f).forEach(c=>this.suspiciousFlags.add(c));} }catch(e){}
+ this._baseDestructive = ['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot'];
+ this._baseSystem = ['reboot','shutdown','format','kill','halt','poweroff'];
+ this._baseSuspiciousFlags = ['--dump','--execute','--eval','--exec','--shell','--script'];
+ this._baseWhitelist = ['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','debug-stats','debug-history','debug-analyze','debug-trends','debug-fix','debug-health','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard','context-auto'];
+ this.destructiveCmds = new Set(this._baseDestructive);
+ this.systemCmds = new Set(this._baseSystem);
+ this.suspiciousFlags = new Set(this._baseSuspiciousFlags);
+ this.whitelist = new Set(this._baseWhitelist);
+ // Load persisted user deltas (add/remove) so hardcoded defaults can be demoted.
+ this._loadSecurityDeltas();
this._registerBuiltIn();
}
+ _loadSecurityDeltas() {
+ try {
+ const schema = this.terminal._getDataSchemaVersion ? this.terminal._getDataSchemaVersion() : 1;
+ localStorage.setItem('webx11_data_schema_version', String(schema));
+ const raw = localStorage.getItem('webx11_security_deltas');
+ if (!raw) {
+ // One-time migration from legacy flat keys to delta format.
+ const legacy = {};
+ try { legacy.destructive = JSON.parse(localStorage.getItem('webx11_security_destructive') || '[]'); } catch (e) {}
+ try { legacy.system = JSON.parse(localStorage.getItem('webx11_security_system') || '[]'); } catch (e) {}
+ try { legacy.whitelist = JSON.parse(localStorage.getItem('webx11_security_whitelist') || '[]'); } catch (e) {}
+ try { legacy.flags = JSON.parse(localStorage.getItem('webx11_security_flags') || '[]'); } catch (e) {}
+ const delta = { add: {}, remove: {} };
+ for (const c of (legacy.destructive || [])) if (!this._baseDestructive.includes(c)) { if (!delta.add.destructive) delta.add.destructive = []; delta.add.destructive.push(c); }
+ for (const c of (legacy.system || [])) if (!this._baseSystem.includes(c)) { if (!delta.add.system) delta.add.system = []; delta.add.system.push(c); }
+ for (const c of (legacy.whitelist || [])) if (!this._baseWhitelist.includes(c)) { if (!delta.add.whitelist) delta.add.whitelist = []; delta.add.whitelist.push(c); }
+ for (const f of (legacy.flags || [])) if (!this._baseSuspiciousFlags.includes(f)) { if (!delta.add.flags) delta.add.flags = []; delta.add.flags.push(f); }
+ localStorage.setItem('webx11_security_deltas', JSON.stringify(delta));
+ localStorage.removeItem('webx11_security_destructive');
+ localStorage.removeItem('webx11_security_system');
+ localStorage.removeItem('webx11_security_whitelist');
+ localStorage.removeItem('webx11_security_flags');
+ return;
+ }
+ const d = JSON.parse(raw);
+ if (d.remove) {
+ for (const c of (d.remove.destructive || [])) this.destructiveCmds.delete(c);
+ for (const c of (d.remove.system || [])) this.systemCmds.delete(c);
+ for (const c of (d.remove.whitelist || [])) this.whitelist.delete(c);
+ for (const f of (d.remove.flags || [])) this.suspiciousFlags.delete(f);
+ }
+ if (d.add) {
+ for (const c of (d.add.destructive || [])) this.destructiveCmds.add(c);
+ for (const c of (d.add.system || [])) this.systemCmds.add(c);
+ for (const c of (d.add.whitelist || [])) this.whitelist.add(c);
+ for (const f of (d.add.flags || [])) this.suspiciousFlags.add(f);
+ }
+ } catch (e) {}
+ }
+ _saveSecurityDeltas() {
+ const delta = { add: {}, remove: {} };
+ for (const c of this.destructiveCmds) if (!this._baseDestructive.includes(c)) { if (!delta.add.destructive) delta.add.destructive = []; delta.add.destructive.push(c); }
+ for (const c of this._baseDestructive) if (!this.destructiveCmds.has(c)) { if (!delta.remove.destructive) delta.remove.destructive = []; delta.remove.destructive.push(c); }
+ for (const c of this.systemCmds) if (!this._baseSystem.includes(c)) { if (!delta.add.system) delta.add.system = []; delta.add.system.push(c); }
+ for (const c of this._baseSystem) if (!this.systemCmds.has(c)) { if (!delta.remove.system) delta.remove.system = []; delta.remove.system.push(c); }
+ for (const c of this.whitelist) if (!this._baseWhitelist.includes(c)) { if (!delta.add.whitelist) delta.add.whitelist = []; delta.add.whitelist.push(c); }
+ for (const c of this._baseWhitelist) if (!this.whitelist.has(c)) { if (!delta.remove.whitelist) delta.remove.whitelist = []; delta.remove.whitelist.push(c); }
+ for (const f of this.suspiciousFlags) if (!this._baseSuspiciousFlags.includes(f)) { if (!delta.add.flags) delta.add.flags = []; delta.add.flags.push(f); }
+ for (const f of this._baseSuspiciousFlags) if (!this.suspiciousFlags.has(f)) { if (!delta.remove.flags) delta.remove.flags = []; delta.remove.flags.push(f); }
+ localStorage.setItem('webx11_security_deltas', JSON.stringify(delta));
+ }
_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','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze'];
+ const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard'];
internals.forEach(c => this.registerCommand(c, { category: 'builtin' }));
}
setLevel(n) {
@@ -38,8 +90,13 @@
}
registerCommand(name, meta = {}) {
this.commandMeta.set(name, { destructive: false, category: 'user', ...meta });
+ this.invalidateCommandCache();
+ }
+ invalidateCommandCache() {
+ this._commandCacheDirty = true;
}
_collectAllCommands() {
+ if (!this._commandCacheDirty && this._commandCache) return this._commandCache;
const cmds = new Set([...this.commandMeta.keys()]);
for (const modName in this.terminal.moduleManager.modules) {
const mod = this.terminal.moduleManager.modules[modName];
@@ -49,7 +106,9 @@
}
const builtins = ['clear','help','ls','df','security','security-level'];
builtins.forEach(c => cmds.add(c));
- return [...cmds];
+ this._commandCache = [...cmds];
+ this._commandCacheDirty = false;
+ return this._commandCache;
}
_levenshtein(a, b) {
a = a.toLowerCase(); b = b.toLowerCase();
@@ -278,6 +337,19 @@
// Apply config overrides after everything is initialized
this._applyTerminalConfig();
}
+ /* ---------- helpers ---------- */
+ static _escapeHtml(str) {
+ if (str == null) return '';
+ return String(str).replace(/[&<>'"]/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[m]));
+ }
+ _escapeHtml(str) { return WebX11Terminal._escapeHtml(str); }
+ _getDataSchemaVersion() { return 1; }
+ _escapeYamlString(str) {
+ return '"' + String(str == null ? '' : str).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
+ }
+ _invalidateCommandCache() {
+ if (this.security) this.security.invalidateCommandCache();
+ }
initEventListeners() {
this.inputField.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -1195,6 +1267,7 @@
this._safeRegister('ServerMonitorModule', 'server monitor');
this._safeRegister('SmartContextModule', 'smart context');
this._safeRegister('SimpleGUIModule', 'simple GUI builder');
+ this._safeRegister('DebugSupervisorModule', 'debug supervisor');
// v4.5.47: Critical ext modules promoted to boot path
this._safeRegister('EditorModule', 'editor');
this._safeRegister('ShellModule', 'shell');
@@ -1242,6 +1315,7 @@
DiffModule: typeof DiffModule !== 'undefined' ? DiffModule : undefined,
BusyBoxModule: typeof BusyBoxModule !== 'undefined' ? BusyBoxModule : undefined,
SmartContextModule: typeof SmartContextModule !== 'undefined' ? SmartContextModule : undefined,
+ DebugSupervisorModule: typeof DebugSupervisorModule !== 'undefined' ? DebugSupervisorModule : undefined,
};
const cls = registry[className];
if (typeof cls === 'undefined') {
@@ -1371,10 +1445,12 @@
splitPipe(input) {
const parts = [];
let current = '';
- let inQuote = false;
+ let inSingle = false;
+ let inDouble = false;
for (const ch of input) {
- if (ch === '"' || ch === "'") { inQuote = !inQuote; current += ch; }
- else if (ch === '|' && !inQuote) { parts.push(current.trim()); current = ''; }
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; current += ch; }
+ else if (ch === "'" && !inDouble) { inSingle = !inSingle; current += ch; }
+ else if (ch === '|' && !inSingle && !inDouble) { parts.push(current.trim()); current = ''; }
else { current += ch; }
}
parts.push(current.trim());
@@ -1414,8 +1490,11 @@
const originalPrint = this.print.bind(this);
let output = '';
this.print = (text, type) => { output += text + '\n'; };
- await this.dispatchCommand(cmd, args);
- this.print = originalPrint;
+ try {
+ await this.dispatchCommand(cmd, args);
+ } finally {
+ this.print = originalPrint;
+ }
const trimmed = output.trim();
if (trimmed.length > this._pipeBufferSize) {
return trimmed.slice(0, this._pipeBufferSize);
@@ -1431,6 +1510,21 @@
if (cmd === 'load-local' || cmd === 'loadlocal') { this.loadLocalModule(args[0]); return; }
if (cmd === 'smart-prompt') { const val = args[0] && args[0] === 'off' ? 'off' : 'on'; localStorage.setItem('webx11_smart_prompt', val); this.print('Smart prompt ' + val, 'success'); this.updatePrompt(); return; }
if (cmd === 'mem') { this.printMemoryStats(); return; }
+ if (cmd === 'dashboard') {
+ // Collect dashboard data and open the dashboard page
+ const aiMod = this.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && typeof aiMod._collectDashboardData === 'function') {
+ aiMod._collectDashboardData().then(function() {
+ this.print('📊 Dashboard data collected — opening dashboard...', 'success');
+ window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ }.bind(this));
+ } else {
+ // Fallback: just open the page (it will try localStorage)
+ this.print('📊 Opening dashboard...', 'info');
+ window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ }
+ return;
+ }
if (cmd === 'config') {
if (args.length === 0) {
this.print('=== Terminal Config ===', 'info');
@@ -1493,7 +1587,7 @@
if (action === 'add') { this.security.destructiveCmds.add(args[1]); }
else if (action === 'remove') { this.security.destructiveCmds.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_destructive', JSON.stringify([...this.security.destructiveCmds]));
+ this.security._saveSecurityDeltas();
this.print(`Updated destructive set (${this.security.destructiveCmds.size} commands)`, 'success');
return;
}
@@ -1509,7 +1603,7 @@
if (action === 'add') { this.security.systemCmds.add(args[1]); }
else if (action === 'remove') { this.security.systemCmds.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_system', JSON.stringify([...this.security.systemCmds]));
+ this.security._saveSecurityDeltas();
this.print(`Updated system set (${this.security.systemCmds.size} commands)`, 'success');
return;
}
@@ -1525,7 +1619,7 @@
if (action === 'add') { this.security.whitelist.add(args[1]); }
else if (action === 'remove') { this.security.whitelist.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_whitelist', JSON.stringify([...this.security.whitelist]));
+ this.security._saveSecurityDeltas();
this.print(`Updated whitelist (${this.security.whitelist.size} commands)`, 'success');
return;
}
@@ -1542,15 +1636,15 @@
if (action === 'add') { this.security.suspiciousFlags.add(flag); }
else if (action === 'remove') { this.security.suspiciousFlags.delete(flag); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_flags', JSON.stringify([...this.security.suspiciousFlags]));
+ this.security._saveSecurityDeltas();
this.print(`Updated suspicious flags (${this.security.suspiciousFlags.size} flags)`, 'success');
return;
}
if (cmd === 'cmd-allow') { this._resolveCmdConfirm(true); return; }
if (cmd === 'cmd-deny') { this._resolveCmdConfirm(false); return; }
if (cmd === 'vfs' && args[0] === 'reset') {
- if (args[1] !== '--force') {
- this.print('⚠ This will permanently delete all files! Use vfs reset --force to confirm.', 'warning');
+ if (args[1] !== '--confirm' && args[1] !== '--force') {
+ this.print('⚠ This will permanently delete all files! Use vfs reset --confirm to confirm.', 'warning');
return;
}
this.fs.reset();
@@ -1592,8 +1686,8 @@
return;
}
if (cmd === 'empty-trash') {
- if (args[0] !== '--force') {
- this.print('⚠ This will permanently delete all trashed files! Use empty-trash --force to confirm.', 'warning');
+ if (args[0] !== '--confirm' && args[0] !== '--force') {
+ this.print('⚠ This will permanently delete all trashed files! Use empty-trash --confirm to confirm.', 'warning');
return;
}
if (this.fs.emptyTrash()) {
@@ -1710,7 +1804,11 @@
} catch (_e) {}
}
return; }
- catch (err) { this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); return; }
+ catch (err) {
+ this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error');
+ console.error(`[Terminal] Error in ${modName}.${cmd}:`, err);
+ return;
+ }
}
}
// Enhanced: check fuzzy suggestions before showing "not found"
@@ -1747,7 +1845,17 @@
files.forEach(f => {
const div = document.createElement('div');
div.className = `file-item ${f.type === 'dir' ? 'dir' : ''}`;
- div.innerHTML = `<span>${f.type === 'dir' ? '📁' : '📄'} ${f.name}</span> <span style="font-size:0.8em">${f.size}b</span>`;
+ const icon = document.createElement('span');
+ icon.textContent = f.type === 'dir' ? '📁' : '📄';
+ const name = document.createElement('span');
+ name.textContent = ' ' + f.name;
+ const size = document.createElement('span');
+ size.style.fontSize = '0.8em';
+ size.textContent = ' ' + f.size + 'b';
+ div.appendChild(icon);
+ div.appendChild(name);
+ div.appendChild(document.createTextNode(' '));
+ div.appendChild(size);
div.onclick = () => {
if (f.type === 'file') {
const content = this.fs.readFile(f.name);
@@ -1786,14 +1894,29 @@
modules.forEach(mod => {
const div = document.createElement('div');
div.className = `module-item ${mod.loaded ? '' : 'disabled'}`;
- const badge = mod.url ? '' : '<span class="module-badge builtin">Built-in</span>';
- div.innerHTML = `
- <div>
- <strong>${mod.name}</strong>${badge}
- <div style="font-size:0.8em; color:#666">${mod.url || 'Built-in'}</div>
- </div>
- ${mod.url ? `<button class="btn btn-danger" onclick="webx11.removeModule('${mod.name}')" style="padding:4px 8px;">✕</button>` : ''}
- `;
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.textContent = mod.name;
+ info.appendChild(strong);
+ if (!mod.url) {
+ const badge = document.createElement('span');
+ badge.className = 'module-badge builtin';
+ badge.textContent = 'Built-in';
+ info.appendChild(badge);
+ }
+ const sub = document.createElement('div');
+ sub.style.cssText = 'font-size:0.8em; color:#666';
+ sub.textContent = mod.url || 'Built-in';
+ info.appendChild(sub);
+ div.appendChild(info);
+ if (mod.url) {
+ const btn = document.createElement('button');
+ btn.className = 'btn btn-danger';
+ btn.style.padding = '4px 8px';
+ btn.textContent = '✕';
+ btn.onclick = () => this.removeModule(mod.name);
+ div.appendChild(btn);
+ }
list.appendChild(div);
});
}
@@ -1905,6 +2028,7 @@
renderConfigList() {
const list = document.getElementById('config-list');
if (!list) return;
+ list.innerHTML = '';
const prefix = 'webx11_config_';
const entries = [];
for (let i = 0; i < localStorage.length; i++) {
@@ -1917,14 +2041,28 @@
list.innerHTML = '<div style="padding:20px; text-align:center; color:#666">No configuration saved.</div>';
return;
}
- list.innerHTML = entries.map(e => {
- const displayVal = e.key.toLowerCase().includes('password') || e.key.toLowerCase().includes('token') || e.key.toLowerCase().includes('secret')
- ? '****' : e.value;
- return `<div class="module-item">
- <div><strong>${e.key}</strong> <span style="color:var(--text-muted)">${displayVal}</span></div>
- <button class="btn btn-danger" onclick="webx11.deleteConfigFromUI('${e.key.replace(/'/g, "\\'")}')" style="padding:4px 8px; font-size:0.8em">✕</button>
- </div>`;
- }).join('');
+ entries.forEach(e => {
+ const item = document.createElement('div');
+ item.className = 'module-item';
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.textContent = e.key;
+ info.appendChild(strong);
+ const val = document.createElement('span');
+ val.style.color = 'var(--text-muted)';
+ const sensitive = /password|token|secret|key|private|credential|auth/i.test(e.key);
+ val.textContent = sensitive ? '****' : e.value;
+ info.appendChild(document.createTextNode(' '));
+ info.appendChild(val);
+ const btn = document.createElement('button');
+ btn.className = 'btn btn-danger';
+ btn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ btn.textContent = '✕';
+ btn.onclick = () => this.deleteConfigFromUI(e.key);
+ item.appendChild(info);
+ item.appendChild(btn);
+ list.appendChild(item);
+ });
}
addConfigFromUI() {
const key = document.getElementById('config-key-input').value.trim();
@@ -1977,10 +2115,24 @@
// Stats
if (statsEl) {
const cats = new Set(Object.values(index).map(s => s.category || 'general'));
- statsEl.innerHTML = `<span>📚 ${names.length} skills</span><span>📂 ${cats.size} categories</span>${query ? `<span>🔍 ${filtered.length} match '${query}'</span>` : ''}`;
+ statsEl.innerHTML = '';
+ const totalSpan = document.createElement('span');
+ totalSpan.textContent = '📚 ' + names.length + ' skills';
+ statsEl.appendChild(totalSpan);
+ const catSpan = document.createElement('span');
+ catSpan.textContent = '📂 ' + cats.size + ' categories';
+ statsEl.appendChild(catSpan);
+ if (query) {
+ const matchSpan = document.createElement('span');
+ matchSpan.textContent = '🔍 ' + filtered.length + ' match';
+ statsEl.appendChild(matchSpan);
+ }
}
if (filtered.length === 0) {
- list.innerHTML = '<div style="padding:20px; text-align:center; color:#666">' + (query ? 'No skills match your search.' : 'No skills yet. Create one below!') + '</div>';
+ const empty = document.createElement('div');
+ empty.style.cssText = 'padding:20px; text-align:center; color:#666';
+ empty.textContent = query ? 'No skills match your search.' : 'No skills yet. Create one below!';
+ list.appendChild(empty);
return;
}
// Group by category
@@ -2006,18 +2158,41 @@
const tags = (s.tags || []).join(', ');
const div = document.createElement('div');
div.className = 'module-item';
- div.innerHTML = `
- <div>
- <strong style="color:var(--success-color)">${name}</strong>
- <div style="font-size:0.85em; color:#aaa">${s.description || ''}</div>
- <div style="font-size:0.75em; color:#666">${tags ? '🏷 ' + tags : ''}</div>
- </div>
- <div style="display:flex;gap:4px">
- <button class="btn" onclick="webx11.viewSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">👁</button>
- <button class="btn" onclick="webx11.editSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">✏️</button>
- <button class="btn btn-danger" onclick="webx11.deleteSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">✕</button>
- </div>
- `;
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.style.color = 'var(--success-color)';
+ strong.textContent = name;
+ info.appendChild(strong);
+ const desc = document.createElement('div');
+ desc.style.cssText = 'font-size:0.85em; color:#aaa';
+ desc.textContent = s.description || '';
+ info.appendChild(desc);
+ const tagsDiv = document.createElement('div');
+ tagsDiv.style.cssText = 'font-size:0.75em; color:#666';
+ tagsDiv.textContent = tags ? '🏷 ' + tags : '';
+ info.appendChild(tagsDiv);
+ const actions = document.createElement('div');
+ actions.style.cssText = 'display:flex;gap:4px';
+ const viewBtn = document.createElement('button');
+ viewBtn.className = 'btn';
+ viewBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ viewBtn.textContent = '👁';
+ viewBtn.onclick = () => this.viewSkillUI(name);
+ const editBtn = document.createElement('button');
+ editBtn.className = 'btn';
+ editBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ editBtn.textContent = '✏️';
+ editBtn.onclick = () => this.editSkillUI(name);
+ const delBtn = document.createElement('button');
+ delBtn.className = 'btn btn-danger';
+ delBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ delBtn.textContent = '✕';
+ delBtn.onclick = () => this.deleteSkillUI(name);
+ actions.appendChild(viewBtn);
+ actions.appendChild(editBtn);
+ actions.appendChild(delBtn);
+ div.appendChild(info);
+ div.appendChild(actions);
body.appendChild(div);
}
section.appendChild(body);
@@ -2081,9 +2256,9 @@
alert(`Skill '${name}' already exists. Delete it first or use skill-patch command.`);
return;
}
- let content = contentInput || `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n## Steps\n1. (to be filled)\n`;
+ let content = contentInput || `---\nname: ${this._escapeYamlString(name)}\ndescription: ${this._escapeYamlString(description)}\ntags: [${tags.map(t => this._escapeYamlString(t)).join(', ')}]\ncategory: ${this._escapeYamlString(category)}\n---\n## Steps\n1. (to be filled)\n`;
if (!content.startsWith('---')) {
- content = `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n${content}`;
+ content = `---\nname: ${this._escapeYamlString(name)}\ndescription: ${this._escapeYamlString(description)}\ntags: [${tags.map(t => this._escapeYamlString(t)).join(', ')}]\ncategory: ${this._escapeYamlString(category)}\n---\n${content}`;
}
fs.writeFile(`/skills/${name}.md`, content);
const index = fs.fsReadJSON('/skills/index.json') || {};
diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js
index 7f6b441..d8b5098 100644
--- a/modules/core/virtual-fs.js
+++ b/modules/core/virtual-fs.js
@@ -37,6 +37,7 @@ class VirtualFileSystem {
const vmCache = {}; // factories can cache costly results within same tick
const mm = () => typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null;
const ai = () => mm()?.WebX11AIAssistantModule || null;
+ const ds = () => mm()?.DebugSupervisorModule || null;
const heap = () => typeof performance !== 'undefined' && performance.memory ? performance.memory : null;
const fmt = (bytes) => {
if (!bytes && bytes !== 0) return 'N/A';
@@ -112,7 +113,7 @@ class VirtualFileSystem {
const a = ai();
const cCount = a?.context?.length || 0;
const lCount = a?._toolCallLog?.length || 0;
- const eCount = a?._errorLog?.length || 0;
+ const eCount = ds() ? Object.keys(ds()._errorDB?.errors || {}).length : 0;
const modules = mm() ? Object.keys(mm()).length : 0;
const domNodes = typeof document !== 'undefined' ? document.querySelectorAll('*').length : 0;
return [
@@ -166,9 +167,12 @@ class VirtualFileSystem {
return a._toolCallLog.map((e, i) => `[${i}] ${JSON.stringify(e)}`).join('\n');
}),
'errors': vm('errors', () => {
- const a = ai();
- if (!a || !a._errorLog || a._errorLog.length === 0) return '(empty)';
- return a._errorLog.map((e, i) => `[${i}] ${JSON.stringify(e)}`).join('\n');
+ const d = ds();
+ if (!d || !d._errorDB || Object.keys(d._errorDB.errors).length === 0) return '(empty)';
+ return Object.entries(d._errorDB.errors)
+ .sort((a, b) => b[1].lastSeen - a[1].lastSeen)
+ .map(([hash, e]) => `[${hash}] ${e.type}/${e.severity} ×${e.count}: ${e.message.substring(0, 100)}`)
+ .join('\n');
}),
'commands': vm('commands', () => {
const a = ai();
diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js
index 3c5d54b..2230498 100644
--- a/modules/ext/critical-ext-bundle.js
+++ b/modules/ext/critical-ext-bundle.js
@@ -186,7 +186,18 @@ class ShellModule {
if (!rawInput.trim()) return;
this.markDirty();
this.printUserPrompt(rawInput);
-
+
+ // Save to command history (preserved from original executeCommand)
+ const cmdRaw = rawInput.trim();
+ if (this.history.length === 0 || this.history[this.history.length - 1] !== cmdRaw) {
+ this.history.push(cmdRaw);
+ }
+ if (this.history.length > this._historyCap) {
+ this.history = this.history.slice(-this._historyCap);
+ }
+ this.historyIndex = this.history.length;
+ this._saveCommandHistory();
+
// For simple single-word commands without special chars, skip shell parsing
if (/^[a-zA-Z0-9._\/-]+$/.test(rawInput.trim()) && !rawInput.includes(' ') && !rawInput.includes('$')) {
try {
@@ -196,19 +207,20 @@ class ShellModule {
return;
}
}
-
+
// Substitute variables in the raw input
const substituted = self._substituteVars(rawInput);
// Split on ; first (sequence)
const seqParts = self._splitSequence(substituted);
for (const seqPart of seqParts) {
if (!seqPart.trim()) continue;
- // Split on && and ||
+ // Split on && and || — operator now attached to the FOLLOWING segment
const chainParts = self._splitChain(seqPart.trim());
let lastOk = true;
for (const chainPart of chainParts) {
if (!chainPart.cmd.trim()) continue;
const { cmd: chainCmd, operator } = chainPart;
+ // Check the operator that PRECEDES this command
if (operator === '&&' && !lastOk) continue;
if (operator === '||' && lastOk) continue;
// Check for redirects
@@ -219,28 +231,40 @@ class ShellModule {
lastOk = redirectInfo.exitOk;
} else {
// Normal or piped command
- const pipeParts = self.terminal.splitPipe(chainCmd);
+ const pipeParts = this.splitPipe(chainCmd);
if (pipeParts.length > 1) {
- await self.terminal.executePipeChain(pipeParts);
+ await this.executePipeChain(pipeParts);
} else {
- // Single command
+ // Single command — check for shell built-ins first
const parts = chainCmd.split(/\s+/);
const cmd = parts[0].toLowerCase();
const args = parts.slice(1);
- try {
- await this.security.checkAndRun(chainCmd);
- } catch(e) {
- this.terminal && this.terminal.print ? this.terminal.print('Error: ' + e.message, 'error') : console.error(e);
+ if (self.commands[cmd]) {
+ try {
+ this.markDirty();
+ await self.commands[cmd](args);
+ lastOk = true;
+ } catch(e) {
+ this.print('Error: ' + e.message, 'error');
+ lastOk = false;
+ }
+ } else {
+ try {
+ await this.security.checkAndRun(chainCmd);
+ lastOk = true;
+ } catch(e) {
+ this.print('Error: ' + e.message, 'error');
+ lastOk = false;
+ }
}
}
- lastOk = true;
}
}
}
};
this.terminal._shellModule = this;
- // Modify pipe chain to read _pipeInput for commands that support it
+ // Modify pipe chain to add pipe-aware injection and security checks
const origPipeChain = this.terminal.executePipeChain.bind(this.terminal);
this.terminal.executePipeChain = async function(pipeParts) {
// Write pipe input from previous segment as temp file for next segment
@@ -251,6 +275,22 @@ class ShellModule {
const parts = cmdText.split(/\s+/);
const cmd = parts[0].toLowerCase();
let args = parts.slice(1);
+
+ // Security gate — replicate the original executePipeChain checks
+ const cls = this.security._classify(cmdText);
+ if (cls.destructive && this.security.level > 0) {
+ this.print(`⚠️ Security: Destructive command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error');
+ return;
+ }
+ if (cls.system && this.security.level > 0) {
+ this.print(`⚠️ Security: System command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error');
+ return;
+ }
+ if (!cls.known && this.security.level >= 2) {
+ this.print(`⚠️ Security: Unknown command "${cmd}" blocked in pipe (paranoid mode)`, 'error');
+ return;
+ }
+
this._pipeInput = pipeInput;
if (pipeInput !== null) {
@@ -265,16 +305,17 @@ class ShellModule {
}
}
- if (i < pipeParts.length - 1) {
- pipeInput = await this.capturePipeOutput(cmd, args);
- } else {
- await this.dispatchCommand(cmd, args);
- }
-
- // Clean up temp file
- if (this.fs.readFile('__pipebuf__') !== null) {
- this.fs.root.content['__pipebuf__'] = undefined;
- delete this.fs.root.content['__pipebuf__'];
+ try {
+ if (i < pipeParts.length - 1) {
+ pipeInput = await this.capturePipeOutput(cmd, args);
+ } else {
+ await this.dispatchCommand(cmd, args);
+ }
+ } finally {
+ // Clean up temp file
+ if (this.fs.readFile('__pipebuf__') !== null) {
+ delete this.fs.root.content['__pipebuf__'];
+ }
}
}
this._pipeInput = null;
@@ -307,10 +348,12 @@ class ShellModule {
_splitChain(input) {
// Split on && and ||, preserving operator info
+ // Operator is attached to the FOLLOWING segment (the one it guards)
const parts = [];
let current = '';
let inQuote = false;
let quoteChar = '';
+ let pendingOp = null; // operator from previous split, applies to next segment
let i = 0;
while (i < input.length) {
const ch = input[i];
@@ -319,16 +362,18 @@ class ShellModule {
} else if (ch === quoteChar && inQuote) {
inQuote = false; quoteChar = ''; current += ch; i++;
} else if (!inQuote && input.substring(i, i+2) === '&&') {
- parts.push({ cmd: current.trim(), operator: '&&' });
+ parts.push({ cmd: current.trim(), operator: pendingOp });
+ pendingOp = '&&';
current = ''; i += 2;
} else if (!inQuote && input.substring(i, i+2) === '||') {
- parts.push({ cmd: current.trim(), operator: '||' });
+ parts.push({ cmd: current.trim(), operator: pendingOp });
+ pendingOp = '||';
current = ''; i += 2;
} else {
current += ch; i++;
}
}
- parts.push({ cmd: current.trim(), operator: null });
+ parts.push({ cmd: current.trim(), operator: pendingOp });
return parts;
}
@@ -351,7 +396,7 @@ class ShellModule {
inQuote = false; quoteChar = ''; current += ch; i++;
} else if (!inQuote) {
if (input.substring(i, i+2) === '>>') {
- if (current.trim()) parts.push(current.trim());
+ if (current.trim()) { parts.push(current.trim()); current = ''; }
hasRedirect = true;
redirectOp = '>>';
append = true;
@@ -366,7 +411,7 @@ class ShellModule {
if (i < input.length && (input[i] === '>' || input[i] === '<')) continue;
break;
} else if (input.substring(i, i+1) === '>') {
- if (current.trim()) parts.push(current.trim());
+ if (current.trim()) { parts.push(current.trim()); current = ''; }
hasRedirect = true;
redirectOp = '>';
append = false;
@@ -380,7 +425,7 @@ class ShellModule {
if (i < input.length && (input[i] === '>' || input[i] === '<')) continue;
break;
} else if (input.substring(i, i+1) === '<') {
- if (current.trim()) parts.push(current.trim());
+ if (current.trim()) { parts.push(current.trim()); current = ''; }
hasRedirect = true;
redirectOp = '<';
i += 1;
@@ -421,28 +466,38 @@ class ShellModule {
const inputContent = fs.readFile(redirectFile);
if (inputContent === null) {
this.terminal.print(`shell: ${redirectFile}: No such file`, 'error');
+ redirectInfo.exitOk = false;
return;
}
this.terminal._pipeInput = inputContent;
- // Execute the command with pipe input available
- const parts = cmdText.split(/\s+/);
- const cmd = parts[0].toLowerCase();
- const args = parts.slice(1);
- // If command reads files, inject pipe content as temp file
- if (this._isPipeAware(cmd)) {
- fs.writeFile('__pipebuf__', inputContent);
- const newArgs = [...args];
- if (!newArgs.some(a => !a.startsWith('-'))) {
- newArgs.push('__pipebuf__');
- }
- await this.terminal.dispatchCommand(cmd, newArgs);
- if (fs.readFile('__pipebuf__') !== null) {
- delete fs.root.content['__pipebuf__'];
+ try {
+ const parts = cmdText.split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+ const args = parts.slice(1);
+ // If command reads files, inject pipe content as temp file
+ if (this._isPipeAware(cmd)) {
+ fs.writeFile('__pipebuf__', inputContent);
+ const newArgs = [...args];
+ if (!newArgs.some(a => !a.startsWith('-'))) {
+ newArgs.push('__pipebuf__');
+ }
+ try {
+ await this.terminal.dispatchCommand(cmd, newArgs);
+ } finally {
+ if (fs.readFile('__pipebuf__') !== null) {
+ delete fs.root.content['__pipebuf__'];
+ }
+ }
+ } else {
+ await this.terminal.dispatchCommand(cmd, args);
}
- } else {
- await this.terminal.dispatchCommand(cmd, args);
+ redirectInfo.exitOk = true;
+ } catch(e) {
+ this.terminal.print('Error: ' + e.message, 'error');
+ redirectInfo.exitOk = false;
+ } finally {
+ this.terminal._pipeInput = null;
}
- this.terminal._pipeInput = null;
return;
}
@@ -450,42 +505,57 @@ class ShellModule {
const originalPrint = this.terminal.print.bind(this.terminal);
let output = '';
this.terminal.print = (text, type) => { output += text + '\n'; };
+ let cmdOk = true;
- // Check for pipes within the command before redirect
- const pipeParts = this.terminal.splitPipe(cmdText);
- if (pipeParts.length > 1) {
- // Execute pipe chain manually to capture all output
- let pipeInput = null;
- for (let i = 0; i < pipeParts.length; i++) {
- const pText = pipeParts[i];
- const pParts = pText.split(/\s+/);
- const pCmd = pParts[0].toLowerCase();
- const pArgs = pParts.slice(1);
- if (pipeInput !== null) {
- fs.writeFile('__pipebuf__', pipeInput);
- }
- if (i < pipeParts.length - 1) {
- const captPrint = this.terminal.print.bind(this.terminal);
- let capt = '';
- this.terminal.print = (t, ty) => { capt += t + '\n'; };
- await this.terminal.dispatchCommand(pCmd, pArgs);
- this.terminal.print = captPrint;
- pipeInput = capt.trim();
- } else {
- await this.terminal.dispatchCommand(pCmd, pArgs);
- }
- if (fs.readFile('__pipebuf__') !== null) {
- delete fs.root.content['__pipebuf__'];
+ try {
+ // Check for pipes within the command before redirect
+ const pipeParts = this.terminal.splitPipe(cmdText);
+ if (pipeParts.length > 1) {
+ // Execute pipe chain manually to capture all output
+ let pipeInput = null;
+ for (let i = 0; i < pipeParts.length; i++) {
+ const pText = pipeParts[i];
+ const pParts = pText.split(/\s+/);
+ const pCmd = pParts[0].toLowerCase();
+ const pArgs = pParts.slice(1);
+ if (pipeInput !== null) {
+ fs.writeFile('__pipebuf__', pipeInput);
+ }
+ if (i < pipeParts.length - 1) {
+ const captPrint = this.terminal.print.bind(this.terminal);
+ let capt = '';
+ this.terminal.print = (t, ty) => { capt += t + '\n'; };
+ try {
+ await this.terminal.dispatchCommand(pCmd, pArgs);
+ } finally {
+ this.terminal.print = captPrint;
+ }
+ pipeInput = capt.trim();
+ } else {
+ await this.terminal.dispatchCommand(pCmd, pArgs);
+ }
+ if (fs.readFile('__pipebuf__') !== null) {
+ delete fs.root.content['__pipebuf__'];
+ }
}
+ } else {
+ const parts = cmdText.split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+ const args = parts.slice(1);
+ await this.terminal.dispatchCommand(cmd, args);
}
- } else {
- const parts = cmdText.split(/\s+/);
- const cmd = parts[0].toLowerCase();
- const args = parts.slice(1);
- await this.terminal.dispatchCommand(cmd, args);
+ } catch(e) {
+ cmdOk = false;
+ // Restore print before showing the error
+ this.terminal.print = originalPrint;
+ this.terminal.print('Error: ' + e.message, 'error');
+ redirectInfo.exitOk = false;
+ return;
+ } finally {
+ // Always restore print — critical: without this, terminal is permanently broken
+ this.terminal.print = originalPrint;
}
- this.terminal.print = originalPrint;
const captured = output.trimEnd();
if (captured) {
@@ -503,6 +573,7 @@ class ShellModule {
this.terminal.print(`Redirected (empty) to ${redirectFile}`, 'success');
}
}
+ redirectInfo.exitOk = cmdOk;
}
_isPipeAware(cmd) {
@@ -622,54 +693,66 @@ class ShellModule {
const content = this.terminal.fs.readFile(path);
if (content === null) return this.terminal.print(`script: ${path}: No such file`, 'error');
this._scriptDepth++;
- this.terminal.print(`Running script: ${path}`, 'info');
- const lines = content.split('\n');
- let lineNum = 0;
- for (const rawLine of lines) {
- lineNum++;
- const line = rawLine.trim();
- if (!line || line.startsWith('#')) continue;
- // Run each line through the shell (by calling executeCommand directly)
- // We need to manually call the wrapped executeCommand
- this.terminal.print(`[${path}:${lineNum}] ${line}`, 'command-prompt');
- // Parse inline: handle pipe, redirect, chain, sequence
- const substituted = this._substituteVars(line);
- const seqParts = this._splitSequence(substituted);
- for (const seqPart of seqParts) {
- if (!seqPart.trim()) continue;
- const chainParts = this._splitChain(seqPart.trim());
- let lastOk = true;
- for (const chainPart of chainParts) {
- if (!chainPart.cmd.trim()) continue;
- if (chainPart.operator === '&&' && !lastOk) continue;
- if (chainPart.operator === '||' && lastOk) continue;
- const redirectInfo = this._extractRedirects(chainPart.cmd);
- if (redirectInfo.hasRedirect) {
- await this._executeWithRedirect(redirectInfo);
- lastOk = redirectInfo.exitOk;
- } else {
- const pipeParts = this.terminal.splitPipe(chainPart.cmd);
- if (pipeParts.length > 1) {
- await this.terminal.executePipeChain(pipeParts);
+ try {
+ this.terminal.print(`Running script: ${path}`, 'info');
+ const lines = content.split('\n');
+ let lineNum = 0;
+ for (const rawLine of lines) {
+ lineNum++;
+ const line = rawLine.trim();
+ if (!line || line.startsWith('#')) continue;
+ this.terminal.print(`[${path}:${lineNum}] ${line}`, 'command-prompt');
+ const substituted = this._substituteVars(line);
+ const seqParts = this._splitSequence(substituted);
+ for (const seqPart of seqParts) {
+ if (!seqPart.trim()) continue;
+ const chainParts = this._splitChain(seqPart.trim());
+ let lastOk = true;
+ for (const chainPart of chainParts) {
+ if (!chainPart.cmd.trim()) continue;
+ if (chainPart.operator === '&&' && !lastOk) continue;
+ if (chainPart.operator === '||' && lastOk) continue;
+ const redirectInfo = this._extractRedirects(chainPart.cmd);
+ if (redirectInfo.hasRedirect) {
+ await this._executeWithRedirect(redirectInfo);
+ lastOk = redirectInfo.exitOk;
} else {
- const parts = chainPart.cmd.split(/\s+/);
- const cmd = parts[0].toLowerCase();
- const args = parts.slice(1);
- // Check if it's our built-in command
- if (this.commands[cmd]) {
- this.terminal.markDirty();
- await this.commands[cmd](args);
+ const pipeParts = this.terminal.splitPipe(chainPart.cmd);
+ if (pipeParts.length > 1) {
+ await this.terminal.executePipeChain(pipeParts);
} else {
- await this.terminal.security.checkAndRun(chainPart.cmd);
+ const parts = chainPart.cmd.split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+ const args = parts.slice(1);
+ // Check if it's a shell built-in command
+ if (this.commands[cmd]) {
+ try {
+ this.terminal.markDirty();
+ await this.commands[cmd](args);
+ lastOk = true;
+ } catch(e) {
+ this.terminal.print('Error: ' + e.message, 'error');
+ lastOk = false;
+ }
+ } else {
+ try {
+ await this.terminal.security.checkAndRun(chainPart.cmd);
+ lastOk = true;
+ } catch(e) {
+ this.terminal.print('Error: ' + e.message, 'error');
+ lastOk = false;
+ }
+ }
}
}
- lastOk = true;
}
}
}
+ this.terminal.print(`Script ${path} completed (${lineNum} lines)`, 'success');
+ } finally {
+ // Always decrement — even on error or thrown exception
+ this._scriptDepth--;
}
- this._scriptDepth--;
- this.terminal.print(`Script ${path} completed (${lineNum} lines)`, 'success');
}
}
if (typeof window.webx11 !== 'undefined' && window.webx11) {
diff --git a/modules/ext/shell.js b/modules/ext/shell.js
index 55d66de..80bc841 100644
--- a/modules/ext/shell.js
+++ b/modules/ext/shell.js
@@ -50,7 +50,18 @@ class ShellModule {
if (!rawInput.trim()) return;
this.markDirty();
this.printUserPrompt(rawInput);
-
+
+ // Save to command history (preserved from original executeCommand)
+ const cmdRaw = rawInput.trim();
+ if (this.history.length === 0 || this.history[this.history.length - 1] !== cmdRaw) {
+ this.history.push(cmdRaw);
+ }
+ if (this.history.length > this._historyCap) {
+ this.history = this.history.slice(-this._historyCap);
+ }
+ this.historyIndex = this.history.length;
+ this._saveCommandHistory();
+
// For simple single-word commands without special chars, skip shell parsing
if (/^[a-zA-Z0-9._\/-]+$/.test(rawInput.trim()) && !rawInput.includes(' ') && !rawInput.includes('$')) {
try {
@@ -60,19 +71,20 @@ class ShellModule {
return;
}
}
-
+
// Substitute variables in the raw input
const substituted = self._substituteVars(rawInput);
// Split on ; first (sequence)
const seqParts = self._splitSequence(substituted);
for (const seqPart of seqParts) {
if (!seqPart.trim()) continue;
- // Split on && and ||
+ // Split on && and || — operator now attached to the FOLLOWING segment
const chainParts = self._splitChain(seqPart.trim());
let lastOk = true;
for (const chainPart of chainParts) {
if (!chainPart.cmd.trim()) continue;
const { cmd: chainCmd, operator } = chainPart;
+ // Check the operator that PRECEDES this command
if (operator === '&&' && !lastOk) continue;
if (operator === '||' && lastOk) continue;
// Check for redirects
@@ -83,28 +95,40 @@ class ShellModule {
lastOk = redirectInfo.exitOk;
} else {
// Normal or piped command
- const pipeParts = self.terminal.splitPipe(chainCmd);
+ const pipeParts = this.splitPipe(chainCmd);
if (pipeParts.length > 1) {
- await self.terminal.executePipeChain(pipeParts);
+ await this.executePipeChain(pipeParts);
} else {
- // Single command
+ // Single command — check for shell built-ins first
const parts = chainCmd.split(/\s+/);
const cmd = parts[0].toLowerCase();
const args = parts.slice(1);
- try {
- await this.security.checkAndRun(chainCmd);
- } catch(e) {
- this.terminal && this.terminal.print ? this.terminal.print('Error: ' + e.message, 'error') : console.error(e);
+ if (self.commands[cmd]) {
+ try {
+ this.markDirty();
+ await self.commands[cmd](args);
+ lastOk = true;
+ } catch(e) {
+ this.print('Error: ' + e.message, 'error');
+ lastOk = false;
+ }
+ } else {
+ try {
+ await this.security.checkAndRun(chainCmd);
+ lastOk = true;
+ } catch(e) {
+ this.print('Error: ' + e.message, 'error');
+ lastOk = false;
+ }
}
}
- lastOk = true;
}
}
}
};
this.terminal._shellModule = this;
- // Modify pipe chain to read _pipeInput for commands that support it
+ // Modify pipe chain to add pipe-aware injection and security checks
const origPipeChain = this.terminal.executePipeChain.bind(this.terminal);
this.terminal.executePipeChain = async function(pipeParts) {
// Write pipe input from previous segment as temp file for next segment
@@ -115,6 +139,22 @@ class ShellModule {
const parts = cmdText.split(/\s+/);
const cmd = parts[0].toLowerCase();
let args = parts.slice(1);
+
+ // Security gate — replicate the original executePipeChain checks
+ const cls = this.security._classify(cmdText);
+ if (cls.destructive && this.security.level > 0) {
+ this.print(`⚠️ Security: Destructive command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error');
+ return;
+ }
+ if (cls.system && this.security.level > 0) {
+ this.print(`⚠️ Security: System command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error');
+ return;
+ }
+ if (!cls.known && this.security.level >= 2) {
+ this.print(`⚠️ Security: Unknown command "${cmd}" blocked in pipe (paranoid mode)`, 'error');
+ return;
+ }
+
this._pipeInput = pipeInput;
if (pipeInput !== null) {
@@ -129,16 +169,17 @@ class ShellModule {
}
}
- if (i < pipeParts.length - 1) {
- pipeInput = await this.capturePipeOutput(cmd, args);
- } else {
- await this.dispatchCommand(cmd, args);
- }
-
- // Clean up temp file
- if (this.fs.readFile('__pipebuf__') !== null) {
- this.fs.root.content['__pipebuf__'] = undefined;
- delete this.fs.root.content['__pipebuf__'];
+ try {
+ if (i < pipeParts.length - 1) {
+ pipeInput = await this.capturePipeOutput(cmd, args);
+ } else {
+ await this.dispatchCommand(cmd, args);
+ }
+ } finally {
+ // Clean up temp file
+ if (this.fs.readFile('__pipebuf__') !== null) {
+ delete this.fs.root.content['__pipebuf__'];
+ }
}
}
this._pipeInput = null;
@@ -171,10 +212,12 @@ class ShellModule {
_splitChain(input) {
// Split on && and ||, preserving operator info
+ // Operator is attached to the FOLLOWING segment (the one it guards)
const parts = [];
let current = '';
let inQuote = false;
let quoteChar = '';
+ let pendingOp = null; // operator from previous split, applies to next segment
let i = 0;
while (i < input.length) {
const ch = input[i];
@@ -183,16 +226,18 @@ class ShellModule {
} else if (ch === quoteChar && inQuote) {
inQuote = false; quoteChar = ''; current += ch; i++;
} else if (!inQuote && input.substring(i, i+2) === '&&') {
- parts.push({ cmd: current.trim(), operator: '&&' });
+ parts.push({ cmd: current.trim(), operator: pendingOp });
+ pendingOp = '&&';
current = ''; i += 2;
} else if (!inQuote && input.substring(i, i+2) === '||') {
- parts.push({ cmd: current.trim(), operator: '||' });
+ parts.push({ cmd: current.trim(), operator: pendingOp });
+ pendingOp = '||';
current = ''; i += 2;
} else {
current += ch; i++;
}
}
- parts.push({ cmd: current.trim(), operator: null });
+ parts.push({ cmd: current.trim(), operator: pendingOp });
return parts;
}
@@ -215,7 +260,7 @@ class ShellModule {
inQuote = false; quoteChar = ''; current += ch; i++;
} else if (!inQuote) {
if (input.substring(i, i+2) === '>>') {
- if (current.trim()) parts.push(current.trim());
+ if (current.trim()) { parts.push(current.trim()); current = ''; }
hasRedirect = true;
redirectOp = '>>';
append = true;
@@ -230,7 +275,7 @@ class ShellModule {
if (i < input.length && (input[i] === '>' || input[i] === '<')) continue;
break;
} else if (input.substring(i, i+1) === '>') {
- if (current.trim()) parts.push(current.trim());
+ if (current.trim()) { parts.push(current.trim()); current = ''; }
hasRedirect = true;
redirectOp = '>';
append = false;
@@ -244,7 +289,7 @@ class ShellModule {
if (i < input.length && (input[i] === '>' || input[i] === '<')) continue;
break;
} else if (input.substring(i, i+1) === '<') {
- if (current.trim()) parts.push(current.trim());
+ if (current.trim()) { parts.push(current.trim()); current = ''; }
hasRedirect = true;
redirectOp = '<';
i += 1;
@@ -285,28 +330,38 @@ class ShellModule {
const inputContent = fs.readFile(redirectFile);
if (inputContent === null) {
this.terminal.print(`shell: ${redirectFile}: No such file`, 'error');
+ redirectInfo.exitOk = false;
return;
}
this.terminal._pipeInput = inputContent;
- // Execute the command with pipe input available
- const parts = cmdText.split(/\s+/);
- const cmd = parts[0].toLowerCase();
- const args = parts.slice(1);
- // If command reads files, inject pipe content as temp file
- if (this._isPipeAware(cmd)) {
- fs.writeFile('__pipebuf__', inputContent);
- const newArgs = [...args];
- if (!newArgs.some(a => !a.startsWith('-'))) {
- newArgs.push('__pipebuf__');
- }
- await this.terminal.dispatchCommand(cmd, newArgs);
- if (fs.readFile('__pipebuf__') !== null) {
- delete fs.root.content['__pipebuf__'];
+ try {
+ const parts = cmdText.split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+ const args = parts.slice(1);
+ // If command reads files, inject pipe content as temp file
+ if (this._isPipeAware(cmd)) {
+ fs.writeFile('__pipebuf__', inputContent);
+ const newArgs = [...args];
+ if (!newArgs.some(a => !a.startsWith('-'))) {
+ newArgs.push('__pipebuf__');
+ }
+ try {
+ await this.terminal.dispatchCommand(cmd, newArgs);
+ } finally {
+ if (fs.readFile('__pipebuf__') !== null) {
+ delete fs.root.content['__pipebuf__'];
+ }
+ }
+ } else {
+ await this.terminal.dispatchCommand(cmd, args);
}
- } else {
- await this.terminal.dispatchCommand(cmd, args);
+ redirectInfo.exitOk = true;
+ } catch(e) {
+ this.terminal.print('Error: ' + e.message, 'error');
+ redirectInfo.exitOk = false;
+ } finally {
+ this.terminal._pipeInput = null;
}
- this.terminal._pipeInput = null;
return;
}
@@ -314,42 +369,57 @@ class ShellModule {
const originalPrint = this.terminal.print.bind(this.terminal);
let output = '';
this.terminal.print = (text, type) => { output += text + '\n'; };
+ let cmdOk = true;
- // Check for pipes within the command before redirect
- const pipeParts = this.terminal.splitPipe(cmdText);
- if (pipeParts.length > 1) {
- // Execute pipe chain manually to capture all output
- let pipeInput = null;
- for (let i = 0; i < pipeParts.length; i++) {
- const pText = pipeParts[i];
- const pParts = pText.split(/\s+/);
- const pCmd = pParts[0].toLowerCase();
- const pArgs = pParts.slice(1);
- if (pipeInput !== null) {
- fs.writeFile('__pipebuf__', pipeInput);
- }
- if (i < pipeParts.length - 1) {
- const captPrint = this.terminal.print.bind(this.terminal);
- let capt = '';
- this.terminal.print = (t, ty) => { capt += t + '\n'; };
- await this.terminal.dispatchCommand(pCmd, pArgs);
- this.terminal.print = captPrint;
- pipeInput = capt.trim();
- } else {
- await this.terminal.dispatchCommand(pCmd, pArgs);
- }
- if (fs.readFile('__pipebuf__') !== null) {
- delete fs.root.content['__pipebuf__'];
+ try {
+ // Check for pipes within the command before redirect
+ const pipeParts = this.terminal.splitPipe(cmdText);
+ if (pipeParts.length > 1) {
+ // Execute pipe chain manually to capture all output
+ let pipeInput = null;
+ for (let i = 0; i < pipeParts.length; i++) {
+ const pText = pipeParts[i];
+ const pParts = pText.split(/\s+/);
+ const pCmd = pParts[0].toLowerCase();
+ const pArgs = pParts.slice(1);
+ if (pipeInput !== null) {
+ fs.writeFile('__pipebuf__', pipeInput);
+ }
+ if (i < pipeParts.length - 1) {
+ const captPrint = this.terminal.print.bind(this.terminal);
+ let capt = '';
+ this.terminal.print = (t, ty) => { capt += t + '\n'; };
+ try {
+ await this.terminal.dispatchCommand(pCmd, pArgs);
+ } finally {
+ this.terminal.print = captPrint;
+ }
+ pipeInput = capt.trim();
+ } else {
+ await this.terminal.dispatchCommand(pCmd, pArgs);
+ }
+ if (fs.readFile('__pipebuf__') !== null) {
+ delete fs.root.content['__pipebuf__'];
+ }
}
+ } else {
+ const parts = cmdText.split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+ const args = parts.slice(1);
+ await this.terminal.dispatchCommand(cmd, args);
}
- } else {
- const parts = cmdText.split(/\s+/);
- const cmd = parts[0].toLowerCase();
- const args = parts.slice(1);
- await this.terminal.dispatchCommand(cmd, args);
+ } catch(e) {
+ cmdOk = false;
+ // Restore print before showing the error
+ this.terminal.print = originalPrint;
+ this.terminal.print('Error: ' + e.message, 'error');
+ redirectInfo.exitOk = false;
+ return;
+ } finally {
+ // Always restore print — critical: without this, terminal is permanently broken
+ this.terminal.print = originalPrint;
}
- this.terminal.print = originalPrint;
const captured = output.trimEnd();
if (captured) {
@@ -367,6 +437,7 @@ class ShellModule {
this.terminal.print(`Redirected (empty) to ${redirectFile}`, 'success');
}
}
+ redirectInfo.exitOk = cmdOk;
}
_isPipeAware(cmd) {
@@ -486,54 +557,66 @@ class ShellModule {
const content = this.terminal.fs.readFile(path);
if (content === null) return this.terminal.print(`script: ${path}: No such file`, 'error');
this._scriptDepth++;
- this.terminal.print(`Running script: ${path}`, 'info');
- const lines = content.split('\n');
- let lineNum = 0;
- for (const rawLine of lines) {
- lineNum++;
- const line = rawLine.trim();
- if (!line || line.startsWith('#')) continue;
- // Run each line through the shell (by calling executeCommand directly)
- // We need to manually call the wrapped executeCommand
- this.terminal.print(`[${path}:${lineNum}] ${line}`, 'command-prompt');
- // Parse inline: handle pipe, redirect, chain, sequence
- const substituted = this._substituteVars(line);
- const seqParts = this._splitSequence(substituted);
- for (const seqPart of seqParts) {
- if (!seqPart.trim()) continue;
- const chainParts = this._splitChain(seqPart.trim());
- let lastOk = true;
- for (const chainPart of chainParts) {
- if (!chainPart.cmd.trim()) continue;
- if (chainPart.operator === '&&' && !lastOk) continue;
- if (chainPart.operator === '||' && lastOk) continue;
- const redirectInfo = this._extractRedirects(chainPart.cmd);
- if (redirectInfo.hasRedirect) {
- await this._executeWithRedirect(redirectInfo);
- lastOk = redirectInfo.exitOk;
- } else {
- const pipeParts = this.terminal.splitPipe(chainPart.cmd);
- if (pipeParts.length > 1) {
- await this.terminal.executePipeChain(pipeParts);
+ try {
+ this.terminal.print(`Running script: ${path}`, 'info');
+ const lines = content.split('\n');
+ let lineNum = 0;
+ for (const rawLine of lines) {
+ lineNum++;
+ const line = rawLine.trim();
+ if (!line || line.startsWith('#')) continue;
+ this.terminal.print(`[${path}:${lineNum}] ${line}`, 'command-prompt');
+ const substituted = this._substituteVars(line);
+ const seqParts = this._splitSequence(substituted);
+ for (const seqPart of seqParts) {
+ if (!seqPart.trim()) continue;
+ const chainParts = this._splitChain(seqPart.trim());
+ let lastOk = true;
+ for (const chainPart of chainParts) {
+ if (!chainPart.cmd.trim()) continue;
+ if (chainPart.operator === '&&' && !lastOk) continue;
+ if (chainPart.operator === '||' && lastOk) continue;
+ const redirectInfo = this._extractRedirects(chainPart.cmd);
+ if (redirectInfo.hasRedirect) {
+ await this._executeWithRedirect(redirectInfo);
+ lastOk = redirectInfo.exitOk;
} else {
- const parts = chainPart.cmd.split(/\s+/);
- const cmd = parts[0].toLowerCase();
- const args = parts.slice(1);
- // Check if it's our built-in command
- if (this.commands[cmd]) {
- this.terminal.markDirty();
- await this.commands[cmd](args);
+ const pipeParts = this.terminal.splitPipe(chainPart.cmd);
+ if (pipeParts.length > 1) {
+ await this.terminal.executePipeChain(pipeParts);
} else {
- await this.terminal.security.checkAndRun(chainPart.cmd);
+ const parts = chainPart.cmd.split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+ const args = parts.slice(1);
+ // Check if it's a shell built-in command
+ if (this.commands[cmd]) {
+ try {
+ this.terminal.markDirty();
+ await this.commands[cmd](args);
+ lastOk = true;
+ } catch(e) {
+ this.terminal.print('Error: ' + e.message, 'error');
+ lastOk = false;
+ }
+ } else {
+ try {
+ await this.terminal.security.checkAndRun(chainPart.cmd);
+ lastOk = true;
+ } catch(e) {
+ this.terminal.print('Error: ' + e.message, 'error');
+ lastOk = false;
+ }
+ }
}
}
- lastOk = true;
}
}
}
+ this.terminal.print(`Script ${path} completed (${lineNum} lines)`, 'success');
+ } finally {
+ // Always decrement — even on error or thrown exception
+ this._scriptDepth--;
}
- this._scriptDepth--;
- this.terminal.print(`Script ${path} completed (${lineNum} lines)`, 'success');
}
}
if (typeof window.webx11 !== 'undefined' && window.webx11) {
diff --git a/modules/ext/skill-pack-core.js b/modules/ext/skill-pack-core.js
index 455a7f4..f204e70 100644
--- a/modules/ext/skill-pack-core.js
+++ b/modules/ext/skill-pack-core.js
@@ -35,6 +35,10 @@ class SkillPackCoreModule {
'skill-export': 'skill-export <name> — Export a skill as JSON',
'skill-import': 'skill-import <json> — Import a skill from JSON string',
'skill-sync': 'skill-sync — Rebuild skills index from /skills/ directory',
+ 'learn-skill': 'learn-skill <topic> — Reflect on topic, draft a skill from experience',
+ 'skill-validate': 'skill-validate <name> — Validate skill structure and get confidence score',
+ 'skill-audit': 'skill-audit — Audit skills for stale, duplicate, and broken references',
+ 'skill-restore': 'skill-restore <name> — Restore an archived skill',
'skill-webdav-sync': 'skill-webdav-sync — Push all skills to WebDAV as JSON files',
};
}
@@ -528,6 +532,11 @@ class SkillPackCoreModule {
this.commands['skill-export'] = (args) => this._cmdSkillExport(args);
this.commands['skill-import'] = (args) => this._cmdSkillImport(args);
this.commands['skill-sync'] = () => this._cmdSkillSync();
+ // v4.5.90: Agent Skills — learn, validate, audit, restore
+ this.commands['learn-skill'] = (args) => this._cmdLearnSkill(args);
+ this.commands['skill-validate'] = (args) => this._cmdSkillValidate(args);
+ this.commands['skill-audit'] = () => this._cmdSkillAudit();
+ this.commands['skill-restore'] = (args) => this._cmdSkillRestore(args);
this.commands['skill-webdav-sync'] = () => this._cmdSkillWebdavSync();
}
// ── Skill Commands ──
@@ -574,6 +583,9 @@ class SkillPackCoreModule {
this.terminal.print(line, '');
}
this.terminal.print(`=== End ${name} ===`, 'info');
+ // v4.5.90: Record skill use for decay tracking
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ if (mg && typeof mg.recordSkillUse === 'function') mg.recordSkillUse(name, true);
}
_cmdSkillCreate(args) {
@@ -846,6 +858,292 @@ class SkillPackCoreModule {
}
this._seeded = true;
}
+
+ // ── v4.5.90: Agent Skills ──
+
+ _cmdLearnSkill(args) {
+ if (!args || args.length === 0) return this.terminal.print('Usage: learn-skill <topic>', 'warning');
+ const topic = args.join(' ');
+ this.terminal.print('🧠 Reflecting on: ' + topic + '...', 'info');
+
+ // 1. Pull context from memgraph
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ let graphNodes = [];
+ if (mg) {
+ graphNodes = mg.findNodes(topic) || [];
+ this.terminal.print(' 📊 Found ' + graphNodes.length + ' graph nodes matching topic', 'info');
+ }
+
+ // 2. Pull recent sessions
+ const fs = this.terminal.fs;
+ let sessionInsights = [];
+ try {
+ const memDir = fs.readDir('/memory/') || [];
+ // v4.5.90b fix: readDir returns objects, filter to .json files (P0 from review)
+ const recent = memDir.filter(function(e) { return e.type === 'file' && e.name.endsWith('.json'); }).slice(-3);
+ for (const f of recent) {
+ const data = fs.fsReadJSON('/memory/' + f.name);
+ if (data && data.summary) sessionInsights.push(data.summary);
+ }
+ } catch (_) {}
+ this.terminal.print(' 📅 Loaded ' + sessionInsights.length + ' recent sessions', 'info');
+
+ // 3. Pull existing skills
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const skillNames = Object.keys(index).filter(function(n) {
+ return n.toLowerCase().includes(topic.toLowerCase().split(' ')[0]);
+ });
+ this.terminal.print(' 📚 Found ' + skillNames.length + ' related skills', 'info');
+
+ // 4. Draft skill
+ const draft = this._draftSkill(topic, graphNodes, sessionInsights, skillNames);
+ this.terminal.print('', '');
+ this.terminal.print('📝 Draft skill:', 'header');
+ const lines = draft.split('\n');
+ for (const line of lines) this.terminal.print(line, '');
+ this.terminal.print('', '');
+
+ // 5. Validate
+ const validation = this._validateSkillContent(draft);
+ if (validation.errors.length > 0) {
+ this.terminal.print('❌ Validation errors:', 'error');
+ for (const e of validation.errors) this.terminal.print(' • ' + e, 'error');
+ this.terminal.print('Fix the issues and use skill-create to save manually.', 'info');
+ return;
+ }
+ this.terminal.print('✅ Validation passed. Confidence: ' + validation.confidence.toFixed(2), 'success');
+
+ // 6. Propose
+ this.terminal.print('Ship this skill? Use: skill-create ' + topic.replace(/\s+/g, '-') + ' "<desc>" "<content>"', 'info');
+ this.terminal.print('Or discard and continue.', 'text-muted');
+ }
+
+ _draftSkill(topic, graphNodes, sessions, existingSkills) {
+ var lines = [];
+ lines.push('---');
+ lines.push('name: ' + topic.toLowerCase().replace(/\s+/g, '-'));
+ lines.push('description: ' + topic + ' — auto-drafted from agent experience');
+ lines.push('tags: [auto-learned, ' + topic.toLowerCase().split(' ')[0] + ']');
+ lines.push('---');
+ lines.push('');
+ lines.push('# ' + topic);
+ lines.push('');
+ lines.push('## Steps');
+ lines.push('');
+ // Generate steps from graph node patterns
+ var stepNum = 1;
+ if (graphNodes && graphNodes.length > 0) {
+ for (var n of graphNodes.slice(0, 5)) {
+ lines.push(stepNum + '. ' + n.label + ' (type: ' + n.type + ', uses: ' + (n.access_count || 1) + ')');
+ stepNum++;
+ }
+ } else {
+ lines.push('1. Identify the core ' + topic + ' workflow');
+ lines.push('2. Check for existing tools and modules');
+ lines.push('3. Execute the task step by step');
+ lines.push('4. Record results in memory graph');
+ }
+ if (sessions && sessions.length > 0) {
+ lines.push('');
+ lines.push('## Insights from Recent Sessions');
+ for (var s of sessions.slice(0, 3)) {
+ lines.push('- ' + s.substring(0, 100));
+ }
+ }
+ if (existingSkills && existingSkills.length > 0) {
+ lines.push('');
+ lines.push('## Related Skills');
+ for (var sk of existingSkills) lines.push('- skill-view ' + sk);
+ }
+ lines.push('');
+ lines.push('## Pitfalls');
+ lines.push('- This skill was auto-drafted — validate before relying on it');
+ return lines.join('\n');
+ }
+
+ _cmdSkillValidate(args) {
+ if (!args || args.length === 0) return this.terminal.print('Usage: skill-validate <name>', 'warning');
+ const name = args[0].replace(/\.md$/, '');
+ const fs = this.terminal.fs;
+ const content = fs.readFile('/skills/' + name + '.md');
+ if (!content) return this.terminal.print('Skill not found: ' + name, 'error');
+
+ const result = this._validateSkillContent(content);
+ this.terminal.print('📊 Validation: ' + name, 'header');
+ if (result.errors.length === 0) {
+ this.terminal.print(' ✅ Valid', 'success');
+ } else {
+ this.terminal.print(' ❌ ' + result.errors.length + ' error(s):', 'error');
+ for (var e of result.errors) this.terminal.print(' • ' + e, 'error');
+ }
+ if (result.warnings.length > 0) {
+ this.terminal.print(' ⚠️ ' + result.warnings.length + ' warning(s):', 'warning');
+ for (var w of result.warnings) this.terminal.print(' • ' + w, 'warning');
+ }
+ this.terminal.print(' 📈 Confidence: ' + result.confidence.toFixed(2), 'info');
+ }
+
+ _validateSkillContent(content) {
+ var errors = [];
+ var warnings = [];
+ var confidence = 0;
+
+ // v4.5.90b fix: Normalize CRLF to LF (P2 from review)
+ content = content.replace(/\r\n/g, '\n');
+
+ // Check YAML frontmatter
+ var fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
+ if (!fmMatch) {
+ errors.push('Missing YAML frontmatter (--- ... ---)');
+ } else {
+ var fm = fmMatch[1];
+ if (!fm.match(/^name:\s*\S+/m)) errors.push('Missing "name" field in frontmatter');
+ if (!fm.match(/^description:\s*\S+/m)) errors.push('Missing "description" field in frontmatter');
+ var nameMatch = fm.match(/^name:\s*(\S+)/m);
+ if (nameMatch && !nameMatch[1].match(/^[a-z0-9-]+$/)) {
+ errors.push('Name must be kebab-case (lowercase, hyphens, numbers only)');
+ }
+ var tagsMatch = fm.match(/^tags:\s*\[(.+)\]/m);
+ if (tagsMatch && !tagsMatch[1].trim()) warnings.push('Tags field is empty');
+ }
+
+ // Check for ## Steps heading
+ if (!content.match(/##\s+Steps/i)) {
+ errors.push('Missing "## Steps" heading');
+ } else {
+ // Check for at least 1 numbered step
+ var steps = content.match(/^\d+\.\s+/gm);
+ if (!steps || steps.length === 0) errors.push('No numbered steps found under ## Steps');
+ }
+
+ // Check broken skill-view references
+ var refMatches = content.match(/skill-view\s+(\S+)/g) || [];
+ var fs = this.terminal.fs;
+ for (var ref of refMatches) {
+ var refName = ref.replace(/skill-view\s+/, '').replace(/\.md$/, '');
+ if (fs && !fs.readFile('/skills/' + refName + '.md')) {
+ warnings.push('Broken skill-view reference: ' + refName);
+ }
+ }
+
+ // v4.5.90b fix: Confidence = success_rate × (uses / (uses + 5)) — saturates at ~5 perfect uses, discriminates better (P2 from review)
+ var index = fs ? (fs.fsReadJSON('/skills/index.json') || {}) : {};
+ var fmName = fmMatch ? fmMatch[1].match(/^name:\s*(\S+)/m) : null;
+ var skillName = fmName ? fmName[1] : '';
+ var stats = index[skillName] || {};
+ var uses = stats.useCount || 0;
+ var successes = stats.successCount || 0;
+ if (uses > 0) {
+ var successRate = successes / uses;
+ confidence = successRate * (uses / (uses + 5));
+ }
+
+ return { valid: errors.length === 0, errors: errors, warnings: warnings, confidence: Math.min(confidence, 1.0) };
+ }
+
+ _cmdSkillAudit() {
+ const fs = this.terminal.fs;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const allNames = Object.keys(index);
+ const now = Date.now();
+
+ var stale = [];
+ var duplicates = [];
+ var brokenRefs = 0;
+
+ // 1. Stale check
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ var staleSkills = [];
+ if (mg && typeof mg.getStaleSkills === 'function') {
+ staleSkills = mg.getStaleSkills();
+ }
+ for (var s of staleSkills) {
+ if (!index[s.name] || index[s.name].immune) continue;
+ stale.push(s.name + ' (' + s.daysUnused + 'd unused)');
+ }
+
+ // 2. Duplicate check (simple Levenshtein on name)
+ const auditCap = 200;
+ const auditNames = allNames.slice(0, auditCap);
+ for (var i = 0; i < auditNames.length; i++) {
+ for (var j = i + 1; j < auditNames.length; j++) {
+ if (this._levenshtein(auditNames[i], auditNames[j]) <= 2) {
+ duplicates.push(auditNames[i] + ' ≈ ' + auditNames[j]);
+ }
+ }
+ }
+ if (allNames.length > auditCap) {
+ warnings.push('Duplicate check only scanned first ' + auditCap + ' of ' + allNames.length + ' skills');
+ }
+
+ // 3. Reference check
+ for (var name of allNames) {
+ var content = fs.readFile('/skills/' + name + '.md');
+ if (!content) continue;
+ var refs = content.match(/skill-view\s+(\S+)/g) || [];
+ for (var ref of refs) {
+ var refName = ref.replace(/skill-view\s+/, '').replace(/\.md$/, '');
+ if (!fs.readFile('/skills/' + refName + '.md')) brokenRefs++;
+ }
+ }
+
+ var healthy = allNames.length - stale.length;
+ this.terminal.print('📊 Skill Audit', 'header');
+ this.terminal.print('────────────────────────────────', 'text-muted');
+ this.terminal.print('Total skills: ' + allNames.length, 'info');
+ this.terminal.print('Healthy: ' + healthy, 'success');
+ this.terminal.print('Stale: ' + stale.length + (stale.length > 0 ? ' (' + stale.join(', ') + ')' : ''), stale.length > 0 ? 'warning' : 'info');
+ this.terminal.print('Duplicates: ' + duplicates.length + (duplicates.length > 0 ? ' (' + duplicates.join(', ') + ')' : ''), duplicates.length > 0 ? 'warning' : 'info');
+ this.terminal.print('Broken refs: ' + brokenRefs, brokenRefs > 0 ? 'error' : 'info');
+ this.terminal.print('', '');
+
+ if (stale.length > 0 || duplicates.length > 0 || brokenRefs > 0) {
+ this.terminal.print('Suggestions:', 'info');
+ for (var st of stale) this.terminal.print('• Archive ' + st, 'warning');
+ for (var dup of duplicates) this.terminal.print('• Merge ' + dup, 'warning');
+ if (brokenRefs > 0) this.terminal.print('• Fix ' + brokenRefs + ' broken reference(s)', 'warning');
+ } else {
+ this.terminal.print('✅ All skills healthy', 'success');
+ }
+ }
+
+ _cmdSkillRestore(args) {
+ if (!args || args.length === 0) return this.terminal.print('Usage: skill-restore <name>', 'warning');
+ const name = args[0].replace(/\.md$/, '');
+ const fs = this.terminal.fs;
+ if (!fs.readDir('/skills/.archive/')) return this.terminal.print('No archive directory found', 'error');
+ const content = fs.readFile('/skills/.archive/' + name + '.md');
+ if (!content) return this.terminal.print('Skill not found in archive: ' + name, 'error');
+ // v4.5.90b fix: Don't overwrite a live skill (P1 from review)
+ const existing = fs.readFile('/skills/' + name + '.md');
+ if (existing) return this.terminal.print('Skill "' + name + '" already exists live. Use skill-patch or delete it first.', 'warning');
+ fs.writeFile('/skills/' + name + '.md', content);
+ fs.deleteFile('/skills/.archive/' + name + '.md');
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ index[name] = index[name] || {};
+ index[name].lastUsed = new Date().toISOString();
+ index[name].useCount = index[name].useCount || 0;
+ fs.fsWriteJSON('/skills/index.json', index);
+ this.terminal.print('✅ Restored: ' + name, 'success');
+ }
+
+ _levenshtein(a, b) {
+ if (a.length === 0) return b.length;
+ if (b.length === 0) return a.length;
+ var matrix = [];
+ for (var i = 0; i <= b.length; i++) matrix[i] = [i];
+ for (var j = 0; j <= a.length; j++) matrix[0][j] = j;
+ for (var i = 1; i <= b.length; i++) {
+ for (var j = 1; j <= a.length; j++) {
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
+ matrix[i][j] = matrix[i - 1][j - 1];
+ } else {
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
+ }
+ }
+ }
+ return matrix[b.length][a.length];
+ }
}
if (typeof window.webx11 !== 'undefined' && window.webx11) {
webx11.moduleManager.registerModule(new SkillPackCoreModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/skill-pack-core.js');
diff --git a/modules/ext/three.js b/modules/ext/three.js
index d72afa1..2fc5284 100644
--- a/modules/ext/three.js
+++ b/modules/ext/three.js
@@ -1,7 +1,17 @@
-// newOS4 Three.js 3D Module (v4.5.82)
+// newOS4 Three.js 3D Module (v4.5.86)
// Commands: three <subcommand> [...]
// Two modes: top panel (default) + fullscreen with terminal overlay
// Lazy-loads Three.js from /downloads/ on first use.
+//
+// Features:
+// - 3D Design: box, sphere, cylinder, cone, torus, torusKnot, text, particles, line
+// - Transforms: position, rotation, scale, color, material, duplicate, group
+// - Animations: rotate, bounce, float, orbit, keyframe, timeline
+// - Avatars: load, pose, look-at, speak, emotion
+// - Interaction: click, hover, follow, path
+// - Import/Export: GLTF, GLB, HTML
+// - Presets: solar system, grid, particle field
+// - Mindmap 3D rendering
class ThreeModule {
constructor() {
@@ -11,11 +21,12 @@ class ThreeModule {
};
this.terminal = null;
this._loaded = false;
+ this._loadingPromise = null;
this._scene = null;
this._camera = null;
this._renderer = null;
this._controls = null;
- this._objects = [];
+ this._objects = new Map();
this._animId = null;
this._container = null;
this._closeBtn = null;
@@ -29,6 +40,31 @@ class ThreeModule {
this._savedInputStyles = null;
this._savedHeaderStyles = null;
this._escHandler = null;
+ this._animations = {}; // id -> { type, speed, active }
+ this._timeline = []; // [{ time, id, props }]
+ this._timelinePlaying = false;
+ this._timelineTime = 0;
+ this._clickHandlers = {}; // id -> command string
+ this._hoverHandlers = {}; // id -> { enterColor, leaveColor }
+ this._followTargets = {}; // id -> targetId
+ this._paths = {}; // id -> [{x,y,z}, ...]
+ this._pathProgress = {}; // id -> index
+ this._avatar = null; // current avatar object
+ this._avatarMood = 'neutral';
+ this._speechBubble = null;
+ this._speechTimeout = null;
+ this._raycaster = null;
+ this._mouse = null;
+ this._clickableObjects = [];
+ this._clock = null;
+ this._animTime = 0;
+ this._importCache = {}; // URL -> loaded data
+ this._bloomEnabled = true;
+ this._composer = null;
+ this._transformControls = null;
+ this._selectedId = null;
+ this._undoStack = [];
+ this._redoStack = [];
}
requirements() {
@@ -46,12 +82,17 @@ class ThreeModule {
getHelpInfo() {
return {
- 'three': 'three <subcommand> [...] — 3D scene builder (Three.js). Subcommands: help, scene, fullscreen, hide, mode, add, camera, orbit, clear, list, remove, mindmap, export, preset, bgcolor. Run `three help` for full list.'
+ 'three': 'three <subcommand> [...] — 3D scene builder (Three.js). Run `three help` for full list.'
};
}
_listSubcommands() {
- return ['help', 'scene', 'fullscreen', 'hide', 'mode', 'add', 'camera', 'orbit', 'clear', 'list', 'remove', 'mindmap', 'export', 'preset', 'bgcolor'];
+ return ['help', 'scene', 'fullscreen', 'hide', 'mode', 'add', 'camera', 'orbit',
+ 'pos', 'rotate', 'scale', 'color', 'material', 'duplicate', 'group', 'ungroup',
+ 'animate', 'stop', 'timeline', 'click', 'hover', 'follow', 'unfollow', 'path',
+ 'avatar', 'speak', 'emotion',
+ 'clear', 'list', 'remove', 'mindmap', 'export', 'import', 'preset', 'bgcolor', 'bloom', 'select', 'gizmo', 'undo', 'redo', 'rename', 'batch',
+ 'snapshot'];
}
registerCommands() {
@@ -62,20 +103,27 @@ class ThreeModule {
async _ensureLoaded() {
if (this._loaded) return true;
+ if (this._loadingPromise) return this._loadingPromise;
if (typeof THREE !== 'undefined') {
this._THREE = THREE;
this._loaded = true;
return true;
}
+ this._loadingPromise = this._doLoad();
+ return this._loadingPromise;
+ }
+ async _doLoad() {
this.terminal.print('📦 Loading Three.js (590KB)...', 'info');
try {
await this._loadScript('downloads/three.min.js');
await this._loadScript('downloads/OrbitControls.js');
this._THREE = THREE;
this._loaded = true;
+ this._loadingPromise = null;
this.terminal.print('✅ Three.js loaded', 'success');
return true;
} catch (e) {
+ this._loadingPromise = null;
this.terminal.print('❌ Failed to load Three.js: ' + e.message, 'error');
return false;
}
@@ -85,8 +133,12 @@ class ThreeModule {
return new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = url;
- s.onload = () => { s.remove(); resolve(); };
- s.onerror = () => { s.remove(); reject(new Error('Failed to load ' + url)); };
+ const timeout = setTimeout(() => {
+ s.remove();
+ reject(new Error('Timeout loading ' + url));
+ }, 30000);
+ s.onload = () => { clearTimeout(timeout); s.remove(); resolve(); };
+ s.onerror = () => { clearTimeout(timeout); s.remove(); reject(new Error('Failed to load ' + url)); };
document.head.appendChild(s);
});
}
@@ -96,7 +148,6 @@ class ThreeModule {
_createContainer() {
if (this._container) return;
- // Main container
this._container = document.createElement('div');
this._container.id = 'three-canvas-container';
this._container.style.cssText = `
@@ -109,7 +160,6 @@ class ThreeModule {
`;
document.body.appendChild(this._container);
- // Close button
this._closeBtn = document.createElement('button');
this._closeBtn.id = 'three-close-btn';
this._closeBtn.textContent = '✕';
@@ -134,7 +184,6 @@ class ThreeModule {
this._closeBtn.addEventListener('click', () => this._hideContainer());
document.body.appendChild(this._closeBtn);
- // Escape key handler
this._escHandler = (e) => {
if (e.key === 'Escape' && this._visible) {
this._hideContainer();
@@ -147,8 +196,6 @@ class ThreeModule {
if (!this._container) return;
this._mode = mode || this._mode;
this._visible = true;
-
- // Show close button
this._closeBtn.style.display = 'block';
if (this._mode === 'top') {
@@ -161,7 +208,6 @@ class ThreeModule {
}
_applyTopPanel() {
- // 3D container: top 50vh
this._container.style.cssText = `
position: fixed;
top: 0; left: 0;
@@ -171,7 +217,6 @@ class ThreeModule {
display: block;
`;
- // Terminal: push down
const term = document.getElementById('terminal-container');
if (term) {
if (!this._savedTerminalStyles) {
@@ -181,7 +226,6 @@ class ThreeModule {
term.style.height = '50vh';
}
- // Input area: stays at bottom
const inputArea = document.getElementById('input-area');
if (inputArea) {
if (!this._savedInputStyles) {
@@ -189,12 +233,10 @@ class ThreeModule {
}
}
- // Resize renderer
this._resizeRenderer(window.innerWidth, window.innerHeight / 2);
}
_applyFullscreen() {
- // 3D container: full viewport
this._container.style.cssText = `
position: fixed;
top: 0; left: 0;
@@ -204,7 +246,6 @@ class ThreeModule {
display: block;
`;
- // Terminal: float as overlay
const term = document.getElementById('terminal-container');
if (term) {
if (!this._savedTerminalStyles) {
@@ -227,7 +268,6 @@ class ThreeModule {
`;
}
- // Input area: fixed at bottom
const inputArea = document.getElementById('input-area');
if (inputArea) {
if (!this._savedInputStyles) {
@@ -245,7 +285,6 @@ class ThreeModule {
`;
}
- // Header: hide
const header = document.querySelector('header');
if (header) {
if (!this._savedHeaderStyles) {
@@ -297,7 +336,6 @@ class ThreeModule {
this._container.style.pointerEvents = 'none';
if (this._closeBtn) this._closeBtn.style.display = 'none';
- // Pause animation (don't cancel — scene stays intact)
if (this._animId) {
cancelAnimationFrame(this._animId);
this._animId = null;
@@ -317,7 +355,26 @@ class ThreeModule {
// ── Scene management ──
_createScene() {
+ // Dispose existing GPU resources before creating new ones
+ if (this._renderer) {
+ this._stopLoop();
+ if (this._controls) {
+ this._controls.dispose();
+ this._controls = null;
+ }
+ if (this._webglLostHandler) {
+ this._renderer.domElement.removeEventListener('webglcontextlost', this._webglLostHandler);
+ }
+ if (this._webglRestoredHandler) {
+ this._renderer.domElement.removeEventListener('webglcontextrestored', this._webglRestoredHandler);
+ }
+ this._renderer.dispose();
+ this._renderer = null;
+ if (this._container) this._container.innerHTML = '';
+ }
+ this._disposeAllObjects();
const T = this._THREE;
+
this._scene = new T.Scene();
this._scene.background = new T.Color(this._backgroundColor);
@@ -348,6 +405,9 @@ class ThreeModule {
this._controls.minDistance = 2;
this._controls.maxDistance = 500;
+ // TransformControls gizmo
+ this._initTransformControls();
+
// Lights
const ambient = new T.AmbientLight(0x404060);
this._scene.add(ambient);
@@ -363,7 +423,7 @@ class ThreeModule {
const grid = new T.GridHelper(30, 20, 0x00ff88, 0x333355);
this._scene.add(grid);
- // Resize handler — remove old first to prevent leak
+ // Resize handler
if (window._threeResizeHandler) {
window.removeEventListener('resize', window._threeResizeHandler);
}
@@ -374,21 +434,242 @@ class ThreeModule {
} else {
this._resizeRenderer(window.innerWidth, window.innerHeight);
}
+ if (this._composer) {
+ this._composer.setSize(
+ this._mode === 'top' ? window.innerWidth : window.innerWidth,
+ this._mode === 'top' ? window.innerHeight / 2 : window.innerHeight
+ );
+ }
}
};
window.addEventListener('resize', window._threeResizeHandler);
- this._objects = [];
+ // Raycaster for click/hover
+ this._raycaster = new T.Raycaster();
+ this._mouse = new T.Vector2();
+ this._clock = new T.Clock();
+
+ // Bloom post-processing
+ this._initBloom();
+
+ // Click/mousemove event listeners for interaction
+ const dom = this._renderer.domElement;
+ dom.style.pointerEvents = 'auto';
+ dom.addEventListener('click', (event) => {
+ if (!this._raycaster || !this._camera || this._clickableObjects.length === 0) return;
+ const rect = dom.getBoundingClientRect();
+ this._mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
+ this._mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
+ this._raycaster.setFromCamera(this._mouse, this._camera);
+ const meshes = this._clickableObjects.map(id => {
+ const obj = this._objects.get(id);
+ return obj ? obj.mesh : null;
+ }).filter(Boolean);
+ const intersects = this._raycaster.intersectObjects(meshes, true);
+ if (intersects.length > 0) {
+ // Walk up to find the tracked object
+ let hit = intersects[0].object;
+ const entry = this._objects.find(o => o.mesh === hit);
+ if (entry && this._clickHandlers[entry.id]) {
+ this.terminal.executeCommand(this._clickHandlers[entry.id]);
+ }
+ }
+ });
+ dom.addEventListener('mousemove', (event) => {
+ if (!this._raycaster || !this._camera || Object.keys(this._hoverHandlers).length === 0) return;
+ const rect = dom.getBoundingClientRect();
+ this._mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
+ this._mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
+ this._raycaster.setFromCamera(this._mouse, this._camera);
+ const meshes = this._objects.filter(o => this._hoverHandlers[o.id]).map(o => o.mesh).filter(Boolean);
+ const intersects = this._raycaster.intersectObjects(meshes, true);
+ // Reset all hovered objects to leave color
+ for (const [hid, h] of Object.entries(this._hoverHandlers)) {
+ const ho = this._objects.get(hid);
+ if (ho && ho.mesh && ho.mesh.material && ho.mesh.material.color) {
+ ho.mesh.material.color.setHex(h.origColor != null ? h.origColor : 0x818cf8);
+ }
+ }
+ if (intersects.length > 0) {
+ let hit = intersects[0].object;
+ const entry = this._objects.find(o => o.mesh === hit);
+ if (entry && this._hoverHandlers[entry.id]) {
+ try {
+ hit.material.color.set(this._hoverHandlers[entry.id].enterColor);
+ } catch (e) {}
+ }
+ }
+ });
+
+ this._objects = new Map();
this._idCounter = 0;
+ this._animations = {};
+ this._clickHandlers = {};
+ this._hoverHandlers = {};
+ this._followTargets = {};
+ this._paths = {};
+ this._pathProgress = {};
+ this._clickableObjects = [];
+ this._avatar = null;
+ this._speechBubble = null;
+ }
+
+ async _initTransformControls() {
+ try {
+ if (typeof THREE.TransformControls === 'undefined') {
+ await this._loadScript('downloads/TransformControls.js');
+ }
+ const T = this._THREE;
+ this._transformControls = new T.TransformControls(this._camera, this._renderer.domElement);
+ this._transformControls.addEventListener('dragging-changed', (event) => {
+ this._controls.enabled = !event.value;
+ });
+ this._scene.add(this._transformControls);
+ } catch (e) {
+ this.terminal?.print('TransformControls not available: ' + e.message, 'warning');
+ this._transformControls = null;
+ }
+ }
+
+ async _initBloom() {
+ try {
+ if (typeof THREE.EffectComposer === 'undefined') {
+ await this._loadScript('downloads/CopyShader.js');
+ await this._loadScript('downloads/EffectComposer.js');
+ await this._loadScript('downloads/ShaderPass.js');
+ await this._loadScript('downloads/RenderPass.js');
+ await this._loadScript('downloads/LuminosityHighPassShader.js');
+ await this._loadScript('downloads/UnrealBloomPass.js');
+ }
+ const T = this._THREE;
+ this._composer = new T.EffectComposer(this._renderer);
+ this._composer.addPass(new T.RenderPass(this._scene, this._camera));
+ const bloomPass = new T.UnrealBloomPass(
+ new T.Vector2(window.innerWidth, window.innerHeight),
+ 0.2, 0.1, 0.05
+ );
+ this._composer.addPass(bloomPass);
+ } catch (e) {
+ this.terminal?.print('Bloom not available: ' + e.message, 'warning');
+ this._composer = null;
+ }
}
_startLoop() {
if (this._animId) cancelAnimationFrame(this._animId);
+ this._animTime = 0;
const loop = () => {
this._animId = requestAnimationFrame(loop);
+ const delta = Math.min(this._clock ? this._clock.getDelta() : 0.016, 0.1);
+ this._animTime += delta;
+
if (this._controls) this._controls.update();
+
+ // ── Animations ──
+ for (const [id, anim] of Object.entries(this._animations)) {
+ if (!anim.active) continue;
+ const obj = this._objects.get(id);
+ if (!obj || !obj.mesh) continue;
+ const m = obj.mesh;
+ switch (anim.type) {
+ case 'rotate':
+ m.rotation.y += anim.speed * delta * 2;
+ break;
+ case 'bounce':
+ m.position.y = anim.baseY + Math.sin(this._animTime * anim.speed * 2) * anim.height;
+ break;
+ case 'float':
+ m.position.y = anim.baseY + Math.sin(this._animTime * anim.speed * 2) * anim.height;
+ m.rotation.z = Math.sin(this._animTime * anim.speed) * 0.1;
+ break;
+ case 'ease': {
+ const t = (this._animTime * anim.speed) % 1;
+ const e = this._easeInOut(t);
+ m.position.y = anim.baseY + e * anim.height;
+ break;
+ }
+ case 'elastic': {
+ const t = (this._animTime * anim.speed) % 1;
+ const e = this._easeElastic(t);
+ m.position.y = anim.baseY + e * anim.height;
+ break;
+ }
+ case 'bounce-ease': {
+ const t = (this._animTime * anim.speed) % 1;
+ const e = this._easeBounce(t);
+ m.position.y = anim.baseY + e * anim.height;
+ break;
+ }
+ case 'orbit': {
+ const d = anim;
+ d.angle = (d.angle || 0) + d.speed * delta;
+ m.position.x = Math.cos(d.angle) * (d.orbit || 1);
+ m.position.z = Math.sin(d.angle) * (d.orbit || 1);
+ break;
+ }
+ }
+ }
+
+ // ── Follow targets ──
+ for (const [id, targetId] of Object.entries(this._followTargets)) {
+ const obj = this._objects.get(id);
+ const target = this._objects.get(targetId);
+ if (obj && target && obj.mesh && target.mesh) {
+ obj.mesh.position.lerp(target.mesh.position, 0.05);
+ }
+ }
+
+ // ── Path following ──
+ for (const [id, path] of Object.entries(this._paths)) {
+ const obj = this._objects.get(id);
+ if (!obj || !obj.mesh || path.length < 2) continue;
+ const prog = this._pathProgress[id] || 0;
+ const next = Math.min(prog + 1, path.length - 1);
+ const p0 = path[prog];
+ const p1 = path[next];
+ const t = 0.05;
+ obj.mesh.position.x += (p0.x - obj.mesh.position.x) * t;
+ obj.mesh.position.y += (p0.y - obj.mesh.position.y) * t;
+ obj.mesh.position.z += (p0.z - obj.mesh.position.z) * t;
+ const dist = obj.mesh.position.distanceTo(new T.Vector3(p0.x, p0.y, p0.z));
+ if (dist < 0.1) {
+ this._pathProgress[id] = next;
+ if (next >= path.length - 1) this._pathProgress[id] = 0; // loop
+ }
+ }
+
+ // ── Avatar idle animation ──
+ if (this._avatar && this._avatarMood !== 'talking') {
+ this._avatar.position.y += Math.sin(this._animTime * 0.2) * 0.002;
+ }
+
+ // ── Timeline ──
+ if (this._timelinePlaying) {
+ this._timelineTime += delta;
+ for (const key of this._timeline) {
+ if (key.time <= this._timelineTime && !key._applied) {
+ key._applied = true;
+ const obj = this._objects.get(key.id);
+ if (obj && obj.mesh) {
+ if (key.pos) obj.mesh.position.set(key.pos[0], key.pos[1], key.pos[2]);
+ if (key.rot) obj.mesh.rotation.set(key.rot[0], key.rot[1], key.rot[2]);
+ if (key.scale) obj.mesh.scale.set(key.scale[0], key.scale[1], key.scale[2]);
+ }
+ }
+ }
+ // Check if timeline ended
+ const lastTime = this._timeline.length > 0 ? this._timeline[this._timeline.length - 1].time : 0;
+ if (this._timelineTime > lastTime) {
+ this._timelinePlaying = false;
+ }
+ }
+
if (this._renderer && this._scene && this._camera) {
- this._renderer.render(this._scene, this._camera);
+ if (this._composer && this._bloomEnabled) {
+ this._composer.render();
+ } else {
+ this._renderer.render(this._scene, this._camera);
+ }
}
};
loop();
@@ -406,7 +687,6 @@ class ThreeModule {
destroy() {
this._stopLoop();
this._hideContainer();
- // Remove event listeners
if (window._threeResizeHandler) {
window.removeEventListener('resize', window._threeResizeHandler);
window._threeResizeHandler = null;
@@ -425,9 +705,7 @@ class ThreeModule {
this._renderer.dispose();
this._renderer = null;
}
- // Dispose all GPU resources
this._disposeAllObjects();
- // Remove DOM elements
if (this._container) {
this._container.remove();
this._container = null;
@@ -441,10 +719,11 @@ class ThreeModule {
this._controls = null;
this._loaded = false;
this._visible = false;
+ this._importCache = {};
}
_disposeAllObjects() {
- for (const obj of this._objects) {
+ for (const obj of this._objects.values()) {
if (this._scene) this._scene.remove(obj.mesh);
if (obj.mesh.geometry) obj.mesh.geometry.dispose();
if (obj.mesh.material) {
@@ -455,7 +734,7 @@ class ThreeModule {
});
}
}
- this._objects = [];
+ this._objects = new Map();
this._idCounter = 0;
}
@@ -475,7 +754,8 @@ class ThreeModule {
mesh.castShadow = true;
const id = this._genId();
this._scene.add(mesh);
- this._objects.push({ mesh, id, label: 'Box ' + id });
+ this._objects.set(id, { mesh, id, label: 'Box ' + id });
+ this._pushUndo('add', { mesh, id, label: 'Box ' + id });
this.terminal.print(`Added box (${id}) at origin`, 'success');
return id;
}
@@ -490,11 +770,47 @@ class ThreeModule {
mesh.castShadow = true;
const id = this._genId();
this._scene.add(mesh);
- this._objects.push({ mesh, id, label: 'Sphere ' + id });
+ this._objects.set(id, { mesh, id, label: 'Sphere ' + id });
+ this._pushUndo('add', { mesh, id, label: 'Sphere ' + id });
this.terminal.print(`Added sphere (${id}) at origin`, 'success');
return id;
}
+ _addCylinder(args) {
+ const T = this._THREE;
+ const radiusTop = parseFloat(args[0]) || 0.5;
+ const radiusBottom = parseFloat(args[1]) || 0.5;
+ const height = parseFloat(args[2]) || 1.5;
+ const color = args[3] || '#fbbf24';
+ const geo = new T.CylinderGeometry(radiusTop, radiusBottom, height, 24);
+ const mat = new T.MeshStandardMaterial({ color, metalness: 0.3, roughness: 0.5 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.castShadow = true;
+ const id = this._genId();
+ this._scene.add(mesh);
+ this._objects.set(id, { mesh, id, label: 'Cylinder ' + id });
+ this._pushUndo('add', { mesh, id, label: 'Cylinder ' + id });
+ this.terminal.print(`Added cylinder (${id}) at origin`, 'success');
+ return id;
+ }
+
+ _addCone(args) {
+ const T = this._THREE;
+ const radius = parseFloat(args[0]) || 0.6;
+ const height = parseFloat(args[1]) || 1.5;
+ const color = args[2] || '#fb7185';
+ const geo = new T.ConeGeometry(radius, height, 24);
+ const mat = new T.MeshStandardMaterial({ color, metalness: 0.2, roughness: 0.5 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.castShadow = true;
+ const id = this._genId();
+ this._scene.add(mesh);
+ this._objects.set(id, { mesh, id, label: 'Cone ' + id });
+ this._pushUndo('add', { mesh, id, label: 'Cone ' + id });
+ this.terminal.print(`Added cone (${id}) at origin`, 'success');
+ return id;
+ }
+
_addTorus(args) {
const T = this._THREE;
const radius = parseFloat(args[0]) || 1;
@@ -506,11 +822,29 @@ class ThreeModule {
mesh.castShadow = true;
const id = this._genId();
this._scene.add(mesh);
- this._objects.push({ mesh, id, label: 'Torus ' + id });
+ this._objects.set(id, { mesh, id, label: 'Torus ' + id });
+ this._pushUndo('add', { mesh, id, label: 'Torus ' + id });
this.terminal.print(`Added torus (${id}) at origin`, 'success');
return id;
}
+ _addTorusKnot(args) {
+ const T = this._THREE;
+ const radius = parseFloat(args[0]) || 0.8;
+ const tube = parseFloat(args[1]) || 0.25;
+ const color = args[2] || '#22d3ee';
+ const geo = new T.TorusKnotGeometry(radius, tube, 64, 8, 2, 3);
+ const mat = new T.MeshStandardMaterial({ color, metalness: 0.5, roughness: 0.2 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.castShadow = true;
+ const id = this._genId();
+ this._scene.add(mesh);
+ this._objects.set(id, { mesh, id, label: 'TorusKnot ' + id });
+ this._pushUndo('add', { mesh, id, label: 'TorusKnot ' + id });
+ this.terminal.print(`Added torus knot (${id}) at origin`, 'success');
+ return id;
+ }
+
_addText(args) {
if (!args[0]) return this.terminal.print('Usage: three add text <text> [color]', 'warning');
const T = this._THREE;
@@ -532,7 +866,8 @@ class ThreeModule {
sprite.scale.set(4, 1, 1);
const id = this._genId();
this._scene.add(sprite);
- this._objects.push({ mesh: sprite, id, label: 'Text: ' + text });
+ this._objects.set(id, { mesh: sprite, id, label: 'Text: ' + text });
+ this._pushUndo('add', { mesh: sprite, id, label: 'Text: ' + text });
this.terminal.print(`Added text "${text}" (${id})`, 'success');
return id;
}
@@ -553,7 +888,8 @@ class ThreeModule {
const points = new T.Points(geo, mat);
const id = this._genId();
this._scene.add(points);
- this._objects.push({ mesh: points, id, label: 'Particles (' + count + ')' });
+ this._objects.set(id, { mesh: points, id, label: 'Particles (' + count + ')' });
+ this._pushUndo('add', { mesh: points, id, label: 'Particles (' + count + ')' });
this.terminal.print(`Added ${count} particles (${id})`, 'success');
return id;
}
@@ -573,27 +909,592 @@ class ThreeModule {
const line = new T.Line(geo, mat);
const id = this._genId();
this._scene.add(line);
- this._objects.push({ mesh: line, id, label: 'Line ' + id });
+ this._objects.set(id, { mesh: line, id, label: 'Line ' + id });
+ this._pushUndo('add', { mesh: line, id, label: 'Line ' + id });
this.terminal.print(`Added line (${id})`, 'success');
return id;
}
- // ── Position helpers ──
+ // ── Transforms ──
_setPosition(id, x, y, z) {
- const obj = this._objects.find(o => o.id === id);
+ const obj = this._objects.get(id);
if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
obj.mesh.position.set(x, y, z);
this.terminal.print(`Moved ${id} to (${x}, ${y}, ${z})`, 'success');
}
_setRotation(id, rx, ry, rz) {
- const obj = this._objects.find(o => o.id === id);
+ const obj = this._objects.get(id);
if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
obj.mesh.rotation.set(rx, ry, rz);
this.terminal.print(`Rotated ${id}`, 'success');
}
+ _setScale(id, sx, sy, sz) {
+ const obj = this._objects.get(id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ obj.mesh.scale.set(sx, sy, sz);
+ this.terminal.print(`Scaled ${id} to (${sx}, ${sy}, ${sz})`, 'success');
+ }
+
+ _setColor(id, colorHex) {
+ const obj = this._objects.get(id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ if (!obj.mesh.material || !obj.mesh.material.color) return this.terminal.print('Object has no color property', 'warning');
+ const T = this._THREE;
+ try {
+ obj.mesh.material.color.set(colorHex);
+ this.terminal.print(`Set ${id} color to ${colorHex}`, 'success');
+ } catch (e) {
+ this.terminal.print('Invalid color: ' + colorHex, 'warning');
+ }
+ }
+
+ _setMaterial(id, key, value) {
+ const obj = this._objects.get(id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ if (!obj.mesh.material) return this.terminal.print('Object has no material', 'warning');
+ const validKeys = ['metalness', 'roughness', 'opacity', 'emissiveIntensity'];
+ if (!validKeys.includes(key)) return this.terminal.print('Valid keys: ' + validKeys.join(', '), 'warning');
+ const num = parseFloat(value);
+ if (isNaN(num) || num < 0 || num > 1) return this.terminal.print('Value must be 0-1', 'warning');
+ obj.mesh.material[key] = num;
+ if (key === 'opacity') {
+ obj.mesh.material.transparent = num < 1;
+ }
+ obj.mesh.material.needsUpdate = true;
+ this.terminal.print(`Set ${id} ${key} to ${num}`, 'success');
+ }
+
+ _duplicateObject(id) {
+ const obj = this._objects.get(id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ const T = this._THREE;
+ let newMesh;
+ if (obj.mesh.type === 'Line' || obj.mesh.type === 'LineSegments') {
+ const geo = obj.mesh.geometry.clone();
+ const mat = obj.mesh.material.clone();
+ newMesh = new T.Line(geo, mat);
+ newMesh.position.copy(obj.mesh.position);
+ newMesh.position.x += 1.5;
+ } else if (obj.mesh.type === 'Sprite') {
+ const mat = obj.mesh.material.clone();
+ newMesh = new T.Sprite(mat);
+ newMesh.position.copy(obj.mesh.position);
+ newMesh.position.x += 1.5;
+ newMesh.scale.copy(obj.mesh.scale);
+ } else if (obj.mesh.type === 'Points') {
+ const geo = obj.mesh.geometry.clone();
+ const mat = obj.mesh.material.clone();
+ newMesh = new T.Points(geo, mat);
+ newMesh.position.copy(obj.mesh.position);
+ newMesh.position.x += 1.5;
+ } else if (obj.mesh.geometry && obj.mesh.material) {
+ const geo = obj.mesh.geometry.clone();
+ const mat = obj.mesh.material.clone();
+ newMesh = new T.Mesh(geo, mat);
+ newMesh.position.copy(obj.mesh.position);
+ newMesh.position.x += 1.5;
+ newMesh.rotation.copy(obj.mesh.rotation);
+ newMesh.scale.copy(obj.mesh.scale);
+ newMesh.castShadow = true;
+ } else {
+ return this.terminal.print('Cannot duplicate this object type', 'warning');
+ }
+ const newId = this._genId();
+ this._scene.add(newMesh);
+ this._objects.set(newId, { mesh: newMesh, id: newId, label: obj.label + ' (copy)' });
+ this.terminal.print(`Duplicated ${id} → ${newId}`, 'success');
+ return newId;
+ }
+
+ _groupObjects(ids) {
+ const T = this._THREE;
+ const group = new T.Group();
+ const moved = [];
+ for (const id of ids) {
+ if (!this._objects.has(id)) { this.terminal.print('Object not found: ' + id, 'warning'); continue; }
+ const obj = this._objects.get(id);
+ this._scene.remove(obj.mesh);
+ group.add(obj.mesh);
+ this._objects.delete(id);
+ moved.push(id);
+ }
+ this._scene.add(group);
+ const groupId = this._genId();
+ this._objects.set(groupId, { mesh: group, id: groupId, label: 'Group (' + moved.join(',') + ')', _isGroup: true });
+ this.terminal.print(`Grouped ${moved.length} objects as ${groupId}`, 'success');
+ return groupId;
+ }
+
+ _ungroupObject(id) {
+ const obj = this._objects.get(id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ if (!obj._isGroup) return this.terminal.print('Not a group: ' + id, 'warning');
+ const T = this._THREE;
+ const group = obj.mesh;
+ const children = [...group.children];
+ const newIds = [];
+ for (const child of children) {
+ group.remove(child);
+ this._scene.add(child);
+ const newId = this._genId();
+ this._objects.set(newId, { mesh: child, id: newId, label: 'Ungrouped ' + newId });
+ newIds.push(newId);
+ }
+ this._scene.remove(group);
+ this._objects.delete(id);
+ this.terminal.print(`Ungrouped ${id} → ${newIds.length} objects`, 'success');
+ return newIds;
+ }
+
+ // ── Animations ──
+
+ _pushUndo(action, data) {
+ this._undoStack.push({ action, data, timestamp: Date.now() });
+ if (this._undoStack.length > 50) this._undoStack.shift();
+ this._redoStack = [];
+ }
+
+ _undo() {
+ if (this._undoStack.length === 0) return this.terminal.print('Nothing to undo', 'info');
+ const entry = this._undoStack.pop();
+ this._redoStack.push(entry);
+ if (entry.action === 'remove') {
+ // Re-add the object
+ const obj = entry.data;
+ this._scene.add(obj.mesh);
+ this._objects.set(obj.id, obj);
+ this.terminal.print('Undo: restored ' + obj.id, 'success');
+ } else if (entry.action === 'add') {
+ // Remove the object
+ const obj = entry.data;
+ this._scene.remove(obj.mesh);
+ this._objects.delete(obj.id);
+ this.terminal.print('Undo: removed ' + obj.id, 'success');
+ } else if (entry.action === 'clear') {
+ // Restore all objects
+ for (const obj of entry.data) {
+ this._scene.add(obj.mesh);
+ this._objects.set(obj.id, obj);
+ }
+ this.terminal.print('Undo: restored scene', 'success');
+ } else if (entry.action === 'transform') {
+ const { id, pos, rot, scale } = entry.data;
+ const obj = this._objects.get(id);
+ if (obj) {
+ if (pos) obj.mesh.position.set(pos[0], pos[1], pos[2]);
+ if (rot) obj.mesh.rotation.set(rot[0], rot[1], rot[2]);
+ if (scale) obj.mesh.scale.set(scale[0], scale[1], scale[2]);
+ this.terminal.print('Undo: reverted ' + id, 'success');
+ }
+ }
+ }
+
+ _redo() {
+ if (this._redoStack.length === 0) return this.terminal.print('Nothing to redo', 'info');
+ const entry = this._redoStack.pop();
+ this._undoStack.push(entry);
+ if (entry.action === 'add') {
+ const obj = entry.data;
+ this._scene.add(obj.mesh);
+ this._objects.set(obj.id, obj);
+ this.terminal.print('Redo: restored ' + obj.id, 'success');
+ } else if (entry.action === 'remove') {
+ const obj = entry.data;
+ this._scene.remove(obj.mesh);
+ this._objects.delete(obj.id);
+ this.terminal.print('Redo: removed ' + obj.id, 'success');
+ } else if (entry.action === 'clear') {
+ for (const obj of entry.data) {
+ this._scene.remove(obj.mesh);
+ this._objects.delete(obj.id);
+ }
+ this.terminal.print('Redo: cleared scene', 'success');
+ }
+ }
+
+ _renameObject(id, newName) {
+ const obj = this._objects.get(id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ obj.label = newName;
+ this.terminal.print(`Renamed ${id} to "${newName}"`, 'success');
+ }
+
+ _selectObject(id) {
+ if (!this._transformControls) return this.terminal.print('TransformControls not loaded', 'warning');
+ if (!id) {
+ this._transformControls.detach();
+ this._selectedId = null;
+ this.terminal.print('Deselected', 'info');
+ return;
+ }
+ const obj = this._objects.get(id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ this._transformControls.attach(obj.mesh);
+ this._selectedId = id;
+ this.terminal.print('Selected ' + id, 'success');
+ }
+
+ // Easing functions
+ _easeInOut(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }
+ _easeOut(t) { return 1 - Math.pow(1 - t, 3); }
+ _easeElastic(t) { return Math.pow(2, -10 * t) * Math.sin((t - 0.075) * (2 * Math.PI) / 0.3) + 1; }
+ _easeBounce(t) {
+ const n1 = 7.5625, d1 = 2.75;
+ if (t < 1 / d1) return n1 * t * t;
+ else if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
+ else if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
+ else return n1 * (t -= 2.625 / d1) * t + 0.984375;
+ }
+
+ _animateObject(args) {
+ if (args.length < 2) return this.terminal.print('Usage: three animate <id> rotate|bounce|float|orbit|ease|elastic|bounce-ease [params] [easing]', 'warning');
+ const id = args[0];
+ const type = args[1];
+ const easing = args[args.length - 1] === 'ease' || args[args.length - 1] === 'elastic' || args[args.length - 1] === 'bounce' ? args.pop() : 'linear';
+ const obj = this._objects.get(id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+
+ const anim = { type, speed: 1, active: true, baseY: obj.mesh.position.y, height: 0.5, angle: 0, orbit: 2, easing };
+ if (type === 'orbit') {
+ anim.orbit = parseFloat(args[2]) || 2;
+ anim.speed = parseFloat(args[3]) || 0.5;
+ } else if (type === 'bounce' || type === 'float') {
+ anim.height = parseFloat(args[2]) || 0.5;
+ anim.speed = parseFloat(args[3]) || 1;
+ } else {
+ anim.speed = parseFloat(args[2]) || 1;
+ }
+ this._animations[id] = anim;
+ this.terminal.print(`▶️ Animated ${id}: ${type} (speed: ${anim.speed})`, 'success');
+ }
+
+ _stopAnimation(args) {
+ const id = args[0];
+ if (!id) {
+ this._animations = {};
+ this.terminal.print('All animations stopped', 'info');
+ return;
+ }
+ if (this._animations[id]) {
+ delete this._animations[id];
+ this.terminal.print(`Stopped animation for ${id}`, 'info');
+ } else {
+ this.terminal.print('No animation for ' + id, 'warning');
+ }
+ }
+
+ // ── Timeline ──
+
+ _timelineCommand(args) {
+ if (args.length === 0) return this.terminal.print('Usage: three timeline add|play|pause|stop|clear|list', 'warning');
+ const sub = args[0];
+ const rest = args.slice(1);
+ switch (sub) {
+ case 'add': {
+ if (rest.length < 5) return this.terminal.print('Usage: three timeline add <id> <time_sec> <x> <y> <z>', 'warning');
+ const id = rest[0];
+ const time = parseFloat(rest[1]);
+ if (isNaN(time)) return this.terminal.print('Time must be a number', 'warning');
+ const x = parseFloat(rest[2]), y = parseFloat(rest[3]), z = parseFloat(rest[4]);
+ this._timeline.push({ id, time, pos: [x, y, z], _applied: false });
+ this._timeline.sort((a, b) => a.time - b.time);
+ this.terminal.print(`Keyframe added: ${id} @ ${time}s → (${x}, ${y}, ${z})`, 'success');
+ break;
+ }
+ case 'play':
+ this._timeline.forEach(k => k._applied = false);
+ this._timelinePlaying = true;
+ this._timelineTime = 0;
+ this.terminal.print('▶️ Timeline playing', 'success');
+ break;
+ case 'pause':
+ this._timelinePlaying = false;
+ this.terminal.print('⏸ Timeline paused', 'info');
+ break;
+ case 'stop':
+ this._timelinePlaying = false;
+ this._timelineTime = 0;
+ this._timeline.forEach(k => k._applied = false);
+ this.terminal.print('⏹ Timeline stopped', 'info');
+ break;
+ case 'clear':
+ this._timeline = [];
+ this._timelinePlaying = false;
+ this._timelineTime = 0;
+ this.terminal.print('Timeline cleared', 'info');
+ break;
+ case 'list':
+ if (this._timeline.length === 0) return this.terminal.print('No keyframes', 'info');
+ this.terminal.print('=== Timeline (' + this._timeline.length + ' keyframes) ===', 'header');
+ for (const k of this._timeline) {
+ this.terminal.print(` ${k.id} @ ${k.time}s → (${k.pos[0]}, ${k.pos[1]}, ${k.pos[2]})`, 'info');
+ }
+ break;
+ default:
+ this.terminal.print('Unknown: ' + sub + '. Try: add, play, pause, stop, clear, list', 'warning');
+ }
+ }
+
+ // ── Interaction ──
+
+ _setClickHandler(args) {
+ if (args.length < 2) return this.terminal.print('Usage: three click <id> <command...>', 'warning');
+ const id = args[0];
+ const cmd = args.slice(1).join(' ');
+ const obj = this._objects.get(id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ this._clickHandlers[id] = cmd;
+ if (!this._clickableObjects.includes(id)) this._clickableObjects.push(id);
+ this.terminal.print(`Click handler set: ${id} → "${cmd}"`, 'success');
+ }
+
+ _setHoverHandler(args) {
+ if (args.length < 3) return this.terminal.print('Usage: three hover <id> <enterColor> <leaveColor>', 'warning');
+ const id = args[0];
+ const obj = this._objects.get(id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ this._hoverHandlers[id] = { enterColor: args[1], leaveColor: args[2], origColor: obj.mesh.material && obj.mesh.material.color ? obj.mesh.material.color.getHex() : null };
+ this.terminal.print(`Hover set: ${id}`, 'success');
+ }
+
+ _setFollow(id, targetId) {
+ const obj = this._objects.get(id);
+ const target = this._objects.get(targetId);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ if (!target) return this.terminal.print('Target not found: ' + targetId, 'error');
+ this._followTargets[id] = targetId;
+ this.terminal.print(`${id} now follows ${targetId}`, 'success');
+ }
+
+ _unfollow(id) {
+ if (this._followTargets[id]) {
+ delete this._followTargets[id];
+ this.terminal.print(`${id} stopped following`, 'info');
+ } else {
+ this.terminal.print('Not following: ' + id, 'warning');
+ }
+ }
+
+ _setPath(args) {
+ if (args.length < 4) return this.terminal.print('Usage: three path <id> <x> <y> <z> [more points...]', 'warning');
+ const id = args[0];
+ const obj = this._objects.get(id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ const coords = args.slice(1).map(parseFloat);
+ const points = [];
+ for (let i = 0; i < coords.length; i += 3) {
+ if (coords.length - i < 3) break;
+ points.push({ x: coords[i], y: coords[i+1], z: coords[i+2] });
+ }
+ if (points.length < 2) return this.terminal.print('Need at least 2 points (6 coordinates)', 'warning');
+ this._paths[id] = points;
+ this._pathProgress[id] = 0;
+ this.terminal.print(`Path set for ${id}: ${points.length} waypoints`, 'success');
+ }
+
+ // ── Avatars ──
+
+ _createAvatar(args) {
+ const T = this._THREE;
+ const name = args[0] || 'assistant';
+ const color = args[1] || '#00ff88';
+
+ // Remove existing avatar
+ if (this._avatar) {
+ this._scene.remove(this._avatar);
+ this._avatar = null;
+ }
+ if (this._speechBubble) {
+ this._scene.remove(this._speechBubble);
+ this._speechBubble = null;
+ }
+
+ const group = new T.Group();
+
+ // Body (rounded cylinder)
+ const bodyGeo = new T.CylinderGeometry(0.6, 0.7, 1.2, 12);
+ const bodyMat = new T.MeshStandardMaterial({ color, metalness: 0.3, roughness: 0.4, emissive: color, emissiveIntensity: 0.1 });
+ const body = new T.Mesh(bodyGeo, bodyMat);
+ body.position.y = 0.6;
+ body.castShadow = true;
+ group.add(body);
+
+ // Head (sphere)
+ const headGeo = new T.SphereGeometry(0.4, 16, 16);
+ const headMat = new T.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.1, roughness: 0.3 });
+ const head = new T.Mesh(headGeo, headMat);
+ head.position.y = 1.4;
+ head.castShadow = true;
+ group.add(head);
+
+ // Eyes (two small spheres)
+ const eyeMat = new T.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.5 });
+ const eyeGeo = new T.SphereGeometry(0.08, 8, 8);
+ const leftEye = new T.Mesh(eyeGeo, eyeMat);
+ leftEye.position.set(-0.15, 1.5, 0.35);
+ group.add(leftEye);
+ const rightEye = new T.Mesh(eyeGeo, eyeMat);
+ rightEye.position.set(0.15, 1.5, 0.35);
+ group.add(rightEye);
+
+ // Arms (thin cylinders)
+ const armMat = new T.MeshStandardMaterial({ color: 0xaaaaaa, metalness: 0.2, roughness: 0.5 });
+ const armGeo = new T.CylinderGeometry(0.06, 0.06, 0.6, 6);
+ const leftArm = new T.Mesh(armGeo, armMat);
+ leftArm.position.set(-0.7, 0.8, 0);
+ leftArm.rotation.z = 0.2;
+ group.add(leftArm);
+ const rightArm = new T.Mesh(armGeo, armMat);
+ rightArm.position.set(0.7, 0.8, 0);
+ rightArm.rotation.z = -0.2;
+ group.add(rightArm);
+
+ group.position.set(0, 0, 0);
+ this._scene.add(group);
+ this._avatar = group;
+ this._avatarMood = 'neutral';
+
+ // Store body parts for animation
+ group.userData = { body, head, leftEye, rightEye, leftArm, rightArm, bodyMat, headMat };
+
+ this.terminal.print(`🤖 Avatar "${name}" created`, 'success');
+ return group;
+ }
+
+ _avatarPose(pose) {
+ if (!this._avatar) return this.terminal.print('No avatar. Create one with three avatar <name>', 'warning');
+ const parts = this._avatar.userData;
+ if (!parts) return;
+
+ switch (pose) {
+ case 'idle':
+ parts.leftArm.rotation.z = 0.2;
+ parts.rightArm.rotation.z = -0.2;
+ break;
+ case 'wave':
+ parts.rightArm.rotation.z = -1.5;
+ break;
+ case 'think':
+ parts.leftArm.rotation.z = 0.8;
+ parts.rightArm.rotation.z = -0.8;
+ parts.head.rotation.x = -0.2;
+ break;
+ case 'point':
+ parts.rightArm.rotation.z = -0.5;
+ parts.rightArm.rotation.x = -0.8;
+ break;
+ case 'cross':
+ parts.leftArm.rotation.z = 0.8;
+ parts.rightArm.rotation.z = -0.8;
+ break;
+ default:
+ this.terminal.print('Poses: idle, wave, think, point, cross', 'warning');
+ }
+ }
+
+ _avatarLookAt(x, y, z) {
+ if (!this._avatar) return this.terminal.print('No avatar', 'warning');
+ const parts = this._avatar.userData;
+ if (!parts || !parts.head) return;
+ const dx = x - this._avatar.position.x;
+ const dy = y - (this._avatar.position.y + 1.4);
+ const dz = z - this._avatar.position.z;
+ parts.head.rotation.x = Math.atan2(dy, Math.sqrt(dx*dx + dz*dz)) * 0.5;
+ parts.head.rotation.y = Math.atan2(dx, dz) * 0.5;
+ }
+
+ _avatarSpeak(text) {
+ if (!this._avatar) return this.terminal.print('No avatar', 'warning');
+ const T = this._THREE;
+
+ // Remove old speech bubble
+ if (this._speechBubble) {
+ if (this._speechBubble.material) {
+ if (this._speechBubble.material.map) this._speechBubble.material.map.dispose();
+ this._speechBubble.material.dispose();
+ }
+ this._speechBubble.removeFromParent();
+ this._speechBubble = null;
+ }
+ if (this._speechTimeout) clearTimeout(this._speechTimeout);
+
+ const canvas = document.createElement('canvas');
+ canvas.width = 512;
+ canvas.height = 128;
+ const ctx = canvas.getContext('2d');
+ ctx.fillStyle = 'rgba(0,0,0,0.8)';
+ ctx.roundRect ? ctx.roundRect(10, 10, 492, 108, 16) : ctx.fillRect(10, 10, 492, 108);
+ ctx.fill();
+ ctx.font = '24px monospace';
+ ctx.fillStyle = '#00ff88';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ const lines = this._wrapText(ctx, text, 450);
+ lines.forEach((line, i) => {
+ ctx.fillText(line, 256, 40 + i * 30);
+ });
+
+ const texture = new T.CanvasTexture(canvas);
+ const mat = new T.SpriteMaterial({ map: texture, transparent: true, depthTest: false });
+ const sprite = new T.Sprite(mat);
+ sprite.position.set(0, 2.5, 0);
+ sprite.scale.set(3, 0.75, 1);
+ this._avatar.add(sprite);
+ this._speechBubble = sprite;
+ this._avatarMood = 'talking';
+
+ // Auto-clear talking state after text duration
+ setTimeout(() => {
+ if (this._avatarMood === 'talking') this._avatarMood = 'neutral';
+ }, Math.max(2000, text.length * 100));
+ }
+
+ _wrapText(ctx, text, maxWidth) {
+ const words = text.split(' ');
+ const lines = [];
+ let line = '';
+ for (const word of words) {
+ const test = line ? line + ' ' + word : word;
+ if (ctx.measureText(test).width > maxWidth && line) {
+ lines.push(line);
+ line = word;
+ } else {
+ line = test;
+ }
+ }
+ if (line) lines.push(line);
+ return lines.length > 0 ? lines : [''];
+ }
+
+ _avatarEmotion(emotion) {
+ if (!this._avatar) return this.terminal.print('No avatar', 'warning');
+ const parts = this._avatar.userData;
+ if (!parts) return;
+ this._avatarMood = emotion;
+
+ const colors = {
+ happy: 0x00ff88,
+ sad: 0x4488ff,
+ angry: 0xff4444,
+ confused: 0xffaa00,
+ thinking: 0xaa88ff,
+ neutral: 0x00ff88
+ };
+ const color = colors[emotion] || 0x00ff88;
+ if (parts.bodyMat) {
+ parts.bodyMat.color.setHex(color);
+ parts.bodyMat.emissive.setHex(color);
+ }
+ if (parts.leftEye && parts.rightEye) {
+ parts.leftEye.material.color.setHex(color);
+ parts.rightEye.material.color.setHex(color);
+ }
+ this.terminal.print(`Avatar emotion: ${emotion}`, 'success');
+ }
+
// ── 3D Mindmap ──
_renderMindmap(args) {
@@ -639,7 +1540,6 @@ class ThreeModule {
};
layout(root, null, 0, 0, Math.PI * 1.5);
- const nodeMeshes = [];
for (const n of nodes) {
const radius = n.depth === 0 ? 0.6 : 0.3 + (1 / (n.depth + 1)) * 0.4;
const geo = new T.SphereGeometry(radius, 16, 16);
@@ -652,8 +1552,7 @@ class ThreeModule {
mesh.castShadow = true;
mesh.userData = { id: n.id, text: n.text, depth: n.depth };
this._scene.add(mesh);
- nodeMeshes.push(mesh);
- this._objects.push({ mesh, id: this._genId(), label: 'Mindmap: ' + n.text });
+ const _mmid = this._genId(); this._objects.set(_mmid, { mesh, id: _mmid, label: 'Mindmap: ' + n.text });
// Text label sprite
const canvas = document.createElement('canvas');
@@ -670,7 +1569,7 @@ class ThreeModule {
sprite.position.set(n.pos.x, n.pos.y + radius + 0.5, n.pos.z);
sprite.scale.set(2.5, 0.5, 1);
this._scene.add(sprite);
- this._objects.push({ mesh: sprite, id: this._genId(), label: 'Label: ' + n.text });
+ const _slid = this._genId(); this._objects.set(_slid, { mesh: sprite, id: _slid, label: 'Label: ' + n.text });
}
for (const e of edges) {
@@ -685,11 +1584,14 @@ class ThreeModule {
const mat = new T.LineBasicMaterial({ color: 0x444466, transparent: true, opacity: 0.5 });
const line = new T.Line(geo, mat);
this._scene.add(line);
- this._objects.push({ mesh: line, id: this._genId(), label: 'Edge' });
+ const _eid = this._genId(); this._objects.set(_eid, { mesh: line, id: _eid, label: 'Edge' });
}
+ // Auto-frame camera
+ const nodeMeshes = Array.from(this._objects.values()).filter(o => o.mesh.type === 'Mesh').map(o => o.mesh);
if (nodeMeshes.length > 0) {
- const box = new T.Box3().setFromObjects(nodeMeshes);
+ const box = new T.Box3();
+ for (const m of nodeMeshes) box.expandByObject(m);
const center = box.getCenter(new T.Vector3());
const size = box.getSize(new T.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
@@ -714,7 +1616,7 @@ class ThreeModule {
const sunMat = new T.MeshStandardMaterial({ color: 0xffaa00, emissive: 0xff6600, emissiveIntensity: 0.5 });
const sun = new T.Mesh(sunGeo, sunMat);
this._scene.add(sun);
- this._objects.push({ mesh: sun, id: this._genId(), label: 'Sun' });
+ const _sid = this._genId(); this._objects.set(_sid, { mesh: sun, id: _sid, label: 'Sun' });
const planets = [
{ name: 'Mercury', radius: 0.15, orbit: 2.5, color: 0xaaaaaa, speed: 0.02 },
@@ -731,7 +1633,8 @@ class ThreeModule {
mesh.position.x = p.orbit;
mesh.userData = { orbit: p.orbit, speed: p.speed, angle: Math.random() * Math.PI * 2 };
this._scene.add(mesh);
- this._objects.push({ mesh, id: this._genId(), label: p.name, _animate: true, _data: mesh.userData });
+ const _pid = this._genId(); this._objects.set(_pid, { mesh, id: _pid, label: p.name });
+ this._animations[_pid] = { type: 'orbit', speed: p.speed, active: true, angle: Math.random() * Math.PI * 2, orbit: p.orbit };
const ringPoints = [];
for (let i = 0; i <= 64; i++) {
@@ -742,7 +1645,7 @@ class ThreeModule {
const ringMat = new T.LineBasicMaterial({ color: 0x333355, transparent: true, opacity: 0.3 });
const ring = new T.Line(ringGeo, ringMat);
this._scene.add(ring);
- this._objects.push({ mesh: ring, id: this._genId(), label: 'Orbit: ' + p.name });
+ const _rid = this._genId(); this._objects.set(_rid, { mesh: ring, id: _rid, label: 'Orbit: ' + p.name });
}
this.terminal.print('🌌 Solar system preset loaded', 'success');
}
@@ -754,20 +1657,104 @@ class ThreeModule {
const T = this._THREE;
const size = parseInt(args[0]) || 5;
const spacing = parseFloat(args[1]) || 2;
-
+ const count = (size * 2 + 1) ** 2;
+
+ const sharedGeo = new T.BoxGeometry(0.3, 0.3, 0.3);
+ const sharedMat = new T.MeshStandardMaterial({ emissiveIntensity: 0.1 });
+ const instancedMesh = new T.InstancedMesh(sharedGeo, sharedMat, count);
+ const dummy = new T.Object3D();
+ const color = new T.Color();
+ let idx = 0;
for (let x = -size; x <= size; x++) {
for (let z = -size; z <= size; z++) {
- const geo = new T.BoxGeometry(0.3, 0.3, 0.3);
const hue = ((x + size) / (size * 2)) * 0.6 + 0.5;
- const color = new T.Color().setHSL(hue, 0.8, 0.5);
- const mat = new T.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.1 });
- const mesh = new T.Mesh(geo, mat);
- mesh.position.set(x * spacing, 0, z * spacing);
- this._scene.add(mesh);
- this._objects.push({ mesh, id: this._genId(), label: `Grid (${x},${z})` });
+ color.setHSL(hue, 0.8, 0.5);
+ dummy.position.set(x * spacing, 0, z * spacing);
+ dummy.updateMatrix();
+ instancedMesh.setMatrixAt(idx, dummy.matrix);
+ instancedMesh.setColorAt(idx, color);
+ idx++;
}
}
- this.terminal.print(`📐 Grid preset: ${(size*2+1)**2} cubes`, 'success');
+ instancedMesh.instanceMatrix.needsUpdate = true;
+ instancedMesh.instanceColor.needsUpdate = true;
+ instancedMesh.castShadow = true;
+ this._scene.add(instancedMesh);
+ const _gid = this._genId(); this._objects.set(_gid, { mesh: instancedMesh, id: _gid, label: 'Grid' });
+ this.terminal.print(`📐 Grid preset: ${count} cubes (1 draw call)`, 'success');
+ }
+
+ _presetParticleField(args) {
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+ const T = this._THREE;
+ const count = parseInt(args[0]) || 2000;
+ const spread = parseFloat(args[1]) || 30;
+
+ const geo = new T.BufferGeometry();
+ const positions = new Float32Array(count * 3);
+ const colors = new Float32Array(count * 3);
+ for (let i = 0; i < count; i++) {
+ positions[i*3] = (Math.random() - 0.5) * spread;
+ positions[i*3+1] = (Math.random() - 0.5) * spread * 0.5;
+ positions[i*3+2] = (Math.random() - 0.5) * spread;
+ const c = new T.Color().setHSL(Math.random() * 0.3 + 0.4, 0.8, 0.5);
+ colors[i*3] = c.r; colors[i*3+1] = c.g; colors[i*3+2] = c.b;
+ }
+ geo.setAttribute('position', new T.BufferAttribute(positions, 3));
+ geo.setAttribute('color', new T.BufferAttribute(colors, 3));
+ const mat = new T.PointsMaterial({
+ size: 0.2, vertexColors: true, transparent: true, opacity: 0.8,
+ blending: T.AdditiveBlending, sizeAttenuation: true
+ });
+ const points = new T.Points(geo, mat);
+ this._scene.add(points);
+ const _pfid = this._genId(); this._objects.set(_pfid, { mesh: points, id: _pfid, label: 'Particle Field' });
+ this.terminal.print(`✨ Particle field: ${count} stars`, 'success');
+ }
+
+ // ── Import ──
+
+ async _importModel(args) {
+ if (!args[0]) return this.terminal.print('Usage: three import <url>', 'warning');
+ const url = args[0];
+
+ // Check cache
+ if (this._importCache[url]) {
+ const cached = this._importCache[url];
+ const newMesh = cached.clone();
+ this._scene.add(newMesh);
+ const id = this._genId();
+ this._objects.set(id, { mesh: newMesh, id, label: 'Imported: ' + url.split('/').pop() });
+ this.terminal.print(`Loaded cached model (${id})`, 'success');
+ return id;
+ }
+
+ this.terminal.print('📥 Loading model from ' + url + '...', 'info');
+ try {
+ // Load GLTFLoader if not already loaded
+ if (typeof THREE.GLTFLoader === 'undefined') {
+ await this._loadScript('downloads/GLTFLoader.js');
+ }
+ const T = this._THREE;
+ const loader = new T.GLTFLoader();
+ const gltf = await new Promise((resolve, reject) => {
+ loader.load(url, resolve, undefined, reject);
+ });
+ const model = gltf.scene || gltf;
+ if (!this._scene) this._createScene();
+ if (!this._visible) this._showContainer(this._mode);
+ this._scene.add(model);
+ const id = this._genId();
+ this._objects.set(id, { mesh: model, id, label: 'Imported: ' + url.split('/').pop() });
+ this._importCache[url] = model;
+ this.terminal.print('📦 Model imported (' + id + ')', 'success');
+ return id;
+ } catch (e) {
+ this.terminal.print('❌ Import failed: ' + e.message, 'error');
+ return null;
+ }
}
// ── Export ──
@@ -775,11 +1762,11 @@ class ThreeModule {
_export(args) {
if (!this._scene) return this.terminal.print('No scene to export', 'warning');
const rawName = args[0] || 'three-scene';
- const name = rawName.replace(/[<>&"']/g, function(c) { return {'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;'}[c]; });
+ const name = rawName.replace(/[^a-zA-Z0-9_-]/g, '_');
const T = this._THREE;
const objects = [];
- for (const obj of this._objects) {
+ for (const obj of this._objects.values()) {
const m = obj.mesh;
if (m.type === 'Mesh' || m.type === 'Points' || m.type === 'Sprite' || m.type === 'Line') {
const hexColor = m.material && m.material.color ? m.material.color.getHex() : null;
@@ -894,22 +1881,56 @@ class ThreeModule {
_clearScene() {
if (!this._scene) return;
+ const savedObjects = Array.from(this._objects.values());
+ this._pushUndo('clear', savedObjects);
this._disposeAllObjects();
+ this._animations = {};
+ this._clickHandlers = {};
+ this._hoverHandlers = {};
+ this._followTargets = {};
+ this._paths = {};
+ this._pathProgress = {};
+ this._clickableObjects = [];
+ this._timeline = [];
+ this._timelinePlaying = false;
+ this._timelineTime = 0;
+ if (this._avatar) {
+ this._avatar.traverse((child) => {
+ if (child.geometry) child.geometry.dispose();
+ if (child.material) {
+ if (child.material.map) child.material.map.dispose();
+ child.material.dispose();
+ }
+ });
+ this._scene.remove(this._avatar);
+ this._avatar = null;
+ }
+ if (this._speechBubble) {
+ if (this._speechBubble.material) {
+ if (this._speechBubble.material.map) this._speechBubble.material.map.dispose();
+ this._speechBubble.material.dispose();
+ }
+ this._speechBubble = null;
+ }
+ if (this._speechTimeout) clearTimeout(this._speechTimeout);
+ this._speechTimeout = null;
}
_listObjects() {
- if (this._objects.length === 0) return this.terminal.print('No objects in scene', 'info');
- this.terminal.print(`=== 3D Objects (${this._objects.length}) ===`, 'header');
- for (const obj of this._objects) {
+ if (this._objects.size === 0) return this.terminal.print('No objects in scene', 'info');
+ this.terminal.print(`=== 3D Objects (${this._objects.size}) ===`, 'header');
+ for (const obj of this._objects.values()) {
const pos = obj.mesh.position;
- this.terminal.print(` ${obj.id} ${obj.label} @ (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})`);
+ const anim = this._animations[obj.id] ? ' ▶️' : '';
+ const click = this._clickHandlers[obj.id] ? ' 👆' : '';
+ const follow = this._followTargets[obj.id] ? ' 🔗' : '';
+ this.terminal.print(` ${obj.id} ${obj.label} @ (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})${anim}${click}${follow}`);
}
}
_removeObject(id) {
- const idx = this._objects.findIndex(o => o.id === id);
- if (idx === -1) return this.terminal.print('Object not found: ' + id, 'error');
- const obj = this._objects[idx];
+ const obj = this._objects.get(id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
this._scene.remove(obj.mesh);
if (obj.mesh.geometry) obj.mesh.geometry.dispose();
if (obj.mesh.material) {
@@ -919,19 +1940,42 @@ class ThreeModule {
m.dispose();
});
}
- this._objects.splice(idx, 1);
+ this._objects.delete(id);
+ delete this._animations[id];
+ delete this._clickHandlers[id];
+ delete this._hoverHandlers[id];
+ delete this._followTargets[id];
+ delete this._paths[id];
+ delete this._pathProgress[id];
+ this._clickableObjects = this._clickableObjects.filter(c => c !== id);
this.terminal.print(`Removed ${id}`, 'success');
}
_setBgColor(args) {
if (!args[0]) return this.terminal.print('Usage: three bgcolor <hex> (e.g. #0a0a14)', 'warning');
- const color = parseInt(args[0].replace('#', ''), 16);
- if (isNaN(color)) return this.terminal.print('Invalid color', 'warning');
+ const colorStr = String(args[0] || '').replace('#', '');
+ const color = parseInt(colorStr, 16);
+ if (isNaN(color) || colorStr.length < 3) return this.terminal.print('Invalid color', 'warning');
this._backgroundColor = color;
if (this._scene) this._scene.background = new this._THREE.Color(color);
this.terminal.print(`Background set to ${args[0]}`, 'success');
}
+ _takeSnapshot() {
+ if (!this._renderer || !this._scene) return this.terminal.print('No scene to capture', 'warning');
+ try {
+ this._renderer.render(this._scene, this._camera);
+ const dataUrl = this._renderer.domElement.toDataURL('image/png');
+ const link = document.createElement('a');
+ link.download = 'three-snapshot-' + Date.now() + '.png';
+ link.href = dataUrl;
+ link.click();
+ this.terminal.print('📸 Snapshot saved', 'success');
+ } catch (e) {
+ this.terminal.print('Snapshot failed: ' + e.message, 'error');
+ }
+ }
+
// ── Main dispatch ──
async _dispatch(args) {
@@ -973,24 +2017,29 @@ class ThreeModule {
case 'add': {
const type = rest[0];
const params = rest.slice(1);
- if (!type) return this.terminal.print('Usage: three add box|sphere|torus|text|particles|line [params...]', 'warning');
+ if (!type) return this.terminal.print('Usage: three add box|sphere|cylinder|cone|torus|torusknot|text|particles|line [params...]', 'warning');
if (!this._scene) this._createScene();
if (!this._visible) this._showContainer(this._mode);
switch (type) {
case 'box': this._addBox(params); break;
case 'sphere': this._addSphere(params); break;
+ case 'cylinder': this._addCylinder(params); break;
+ case 'cone': this._addCone(params); break;
case 'torus': this._addTorus(params); break;
+ case 'torusknot': this._addTorusKnot(params); break;
case 'text': this._addText(params); break;
case 'particles': this._addParticles(params); break;
case 'line': this._addLine(params); break;
- default: this.terminal.print('Unknown type: ' + type + '. Try: box, sphere, torus, text, particles, line', 'warning');
+ default: this.terminal.print('Unknown type: ' + type + '. Try: box, sphere, cylinder, cone, torus, torusknot, text, particles, line', 'warning');
}
break;
}
case 'camera': {
if (rest.length < 3) return this.terminal.print('Usage: three camera <x> <y> <z>', 'warning');
if (!this._camera) return this.terminal.print('No scene. Create one with three scene first.', 'warning');
- this._camera.position.set(parseFloat(rest[0]), parseFloat(rest[1]), parseFloat(rest[2]));
+ const cx = parseFloat(rest[0]), cy = parseFloat(rest[1]), cz = parseFloat(rest[2]);
+ if ([cx, cy, cz].some(v => isNaN(v))) return this.terminal.print('Invalid number in camera position', 'warning');
+ this._camera.position.set(cx, cy, cz);
this._controls.update();
this.terminal.print(`Camera moved to (${rest[0]}, ${rest[1]}, ${rest[2]})`, 'success');
break;
@@ -1002,6 +2051,110 @@ class ThreeModule {
this.terminal.print(`Orbit controls: ${this._orbitEnabled ? 'ON' : 'OFF'}`, 'info');
break;
}
+ case 'pos': {
+ if (rest.length < 4) return this.terminal.print('Usage: three pos <id> <x> <y> <z>', 'warning');
+ const px = parseFloat(rest[1]), py = parseFloat(rest[2]), pz = parseFloat(rest[3]);
+ if ([px, py, pz].some(v => isNaN(v))) return this.terminal.print('Invalid number in position', 'warning');
+ this._setPosition(rest[0], px, py, pz);
+ break;
+ }
+ case 'rotate': {
+ if (rest.length < 4) return this.terminal.print('Usage: three rotate <id> <x> <y> <z> (radians)', 'warning');
+ const rx = parseFloat(rest[1]), ry = parseFloat(rest[2]), rz = parseFloat(rest[3]);
+ if ([rx, ry, rz].some(v => isNaN(v))) return this.terminal.print('Invalid number in rotation', 'warning');
+ this._setRotation(rest[0], rx, ry, rz);
+ break;
+ }
+ case 'scale': {
+ if (rest.length < 4) return this.terminal.print('Usage: three scale <id> <x> <y> <z>', 'warning');
+ const sx = parseFloat(rest[1]), sy = parseFloat(rest[2]), sz = parseFloat(rest[3]);
+ if ([sx, sy, sz].some(v => isNaN(v))) return this.terminal.print('Invalid number in scale', 'warning');
+ this._setScale(rest[0], sx, sy, sz);
+ break;
+ }
+ case 'color': {
+ if (rest.length < 2) return this.terminal.print('Usage: three color <id> <hex>', 'warning');
+ this._setColor(rest[0], rest[1]);
+ break;
+ }
+ case 'material': {
+ if (rest.length < 3) return this.terminal.print('Usage: three material <id> <key> <value>', 'warning');
+ this._setMaterial(rest[0], rest[1], rest[2]);
+ break;
+ }
+ case 'duplicate': {
+ if (!rest[0]) return this.terminal.print('Usage: three duplicate <id>', 'warning');
+ this._duplicateObject(rest[0]);
+ break;
+ }
+ case 'group': {
+ if (rest.length < 2) return this.terminal.print('Usage: three group <id1> <id2> ...', 'warning');
+ this._groupObjects(rest);
+ break;
+ }
+ case 'ungroup': {
+ if (!rest[0]) return this.terminal.print('Usage: three ungroup <id>', 'warning');
+ this._ungroupObject(rest[0]);
+ break;
+ }
+ case 'animate': {
+ this._animateObject(rest);
+ break;
+ }
+ case 'stop': {
+ this._stopAnimation(rest);
+ break;
+ }
+ case 'timeline': {
+ this._timelineCommand(rest);
+ break;
+ }
+ case 'click': {
+ this._setClickHandler(rest);
+ break;
+ }
+ case 'hover': {
+ this._setHoverHandler(rest);
+ break;
+ }
+ case 'follow': {
+ if (rest.length < 2) return this.terminal.print('Usage: three follow <id> <target-id>', 'warning');
+ this._setFollow(rest[0], rest[1]);
+ break;
+ }
+ case 'unfollow': {
+ if (!rest[0]) return this.terminal.print('Usage: three unfollow <id>', 'warning');
+ this._unfollow(rest[0]);
+ break;
+ }
+ case 'path': {
+ this._setPath(rest);
+ break;
+ }
+ case 'avatar': {
+ this._createAvatar(rest);
+ break;
+ }
+ case 'pose': {
+ if (!rest[0]) return this.terminal.print('Usage: three pose idle|wave|think|point|cross', 'warning');
+ this._avatarPose(rest[0]);
+ break;
+ }
+ case 'look': {
+ if (rest.length < 3) return this.terminal.print('Usage: three look <x> <y> <z>', 'warning');
+ this._avatarLookAt(parseFloat(rest[0]), parseFloat(rest[1]), parseFloat(rest[2]));
+ break;
+ }
+ case 'speak': {
+ if (!rest[0]) return this.terminal.print('Usage: three speak <text...>', 'warning');
+ this._avatarSpeak(rest.join(' '));
+ break;
+ }
+ case 'emotion': {
+ if (!rest[0]) return this.terminal.print('Usage: three emotion happy|sad|angry|confused|thinking|neutral', 'warning');
+ this._avatarEmotion(rest[0]);
+ break;
+ }
case 'clear':
this._clearScene();
this.terminal.print('Scene cleared', 'success');
@@ -1013,39 +2166,95 @@ class ThreeModule {
if (!rest[0]) return this.terminal.print('Usage: three remove <id>', 'warning');
this._removeObject(rest[0]);
break;
- case 'pos':
- if (rest.length < 4) return this.terminal.print('Usage: three pos <id> <x> <y> <z>', 'warning');
- this._setPosition(rest[0], parseFloat(rest[1]), parseFloat(rest[2]), parseFloat(rest[3]));
- break;
case 'mindmap':
this._renderMindmap(rest);
break;
case 'export':
this._export(rest);
break;
+ case 'import':
+ await this._importModel(rest);
+ break;
case 'preset': {
const preset = rest[0] || 'solar';
if (!this._scene) this._createScene();
if (!this._visible) this._showContainer(this._mode);
if (preset === 'solar') this._presetSolarSystem();
else if (preset === 'grid') this._presetGrid(rest.slice(1));
- else this.terminal.print('Presets: solar, grid', 'warning');
+ else if (preset === 'particles' || preset === 'stars') this._presetParticleField(rest.slice(1));
+ else this.terminal.print('Presets: solar, grid, particles', 'warning');
break;
}
case 'bgcolor':
this._setBgColor(rest);
break;
+ case 'bloom':
+ this._bloomEnabled = !this._bloomEnabled;
+ this.terminal.print('Bloom ' + (this._bloomEnabled ? 'ON' : 'OFF'), 'success');
+ break;
+ case 'select':
+ this._selectObject(rest[0]);
+ break;
+ case 'undo':
+ this._undo();
+ break;
+ case 'redo':
+ this._redo();
+ break;
+ case 'rename':
+ if (rest.length < 2) return this.terminal.print('Usage: three rename <id> <newName>', 'warning');
+ this._renameObject(rest[0], rest.slice(1).join(' '));
+ break;
+ case 'batch': {
+ const sub = rest[0];
+ const target = rest[1];
+ const ids = target === 'all' ? Array.from(this._objects.keys()) : [target];
+ if (sub === 'color' && rest[2]) {
+ for (const bid of ids) {
+ const bo = this._objects.get(bid);
+ if (bo && bo.mesh.material && bo.mesh.material.color) {
+ bo.mesh.material.color.set(rest[2]);
+ }
+ }
+ this.terminal.print(`Batch color ${rest[2]} applied to ${ids.length} objects`, 'success');
+ } else if (sub === 'remove') {
+ for (const bid of ids) this._removeObject(bid);
+ this.terminal.print(`Batch removed ${ids.length} objects`, 'success');
+ } else if (sub === 'animate' && rest[2]) {
+ for (const bid of ids) this._animateObject([bid, rest[2], rest[3] || '1']);
+ this.terminal.print(`Batch animated ${ids.length} objects`, 'success');
+ } else if (sub === 'stop') {
+ for (const bid of ids) { delete this._animations[bid]; }
+ this.terminal.print(`Batch stopped ${ids.length} objects`, 'success');
+ } else {
+ this.terminal.print('Usage: three batch color|remove|animate|stop <id|all> [params]', 'warning');
+ }
+ break;
+ }
+ case 'gizmo':
+ if (!this._transformControls) return this.terminal.print('TransformControls not loaded', 'warning');
+ const gmode = rest[0] || 'translate';
+ if (['translate', 'rotate', 'scale'].includes(gmode)) {
+ this._transformControls.setMode(gmode);
+ this.terminal.print('Gizmo mode: ' + gmode, 'success');
+ } else {
+ this.terminal.print('Modes: translate, rotate, scale', 'warning');
+ }
+ break;
case 'hide':
this._hideContainer();
this.terminal.print('3D view closed. Use three scene or three fullscreen to reopen.', 'info');
break;
+ case 'snapshot':
+ this._takeSnapshot();
+ break;
default:
this.terminal.print('Unknown subcommand: ' + sub + '. Try: three help', 'warning');
}
}
_printHelp() {
- this.terminal.print('=== Three.js 3D Module (v4.5.82) ===', 'header');
+ this.terminal.print('=== Three.js 3D Module (v4.5.86) ===', 'header');
this.terminal.print('', '');
this.terminal.print(' ── Scene ──', 'info');
this.terminal.print(' three scene Open 3D in top panel (default)');
@@ -1054,31 +2263,66 @@ class ThreeModule {
this.terminal.print(' three hide Close 3D view (or press Esc)');
this.terminal.print(' three clear Remove all objects');
this.terminal.print(' three bgcolor <hex> Set background color (e.g. #0a0a14)');
+ this.terminal.print(' three snapshot Save PNG screenshot');
this.terminal.print('', '');
this.terminal.print(' ── Objects ──', 'info');
this.terminal.print(' three add box [size] [color] Add a box');
this.terminal.print(' three add sphere [r] [color] Add a sphere');
+ this.terminal.print(' three add cylinder [rt] [rb] [h] [c] Add a cylinder');
+ this.terminal.print(' three add cone [r] [h] [c] Add a cone');
this.terminal.print(' three add torus [r] [tube] [c] Add a torus');
+ this.terminal.print(' three add torusknot [r] [tube] [c] Add a torus knot');
this.terminal.print(' three add text <text> [color] Add 2D text sprite');
this.terminal.print(' three add particles [n] [c] Add particle system');
this.terminal.print(' three add line x1 y1 z1 x2 y2 z2 [color] Add a line');
this.terminal.print(' three list List all objects');
this.terminal.print(' three remove <id> Remove an object');
+ this.terminal.print(' three duplicate <id> Clone an object');
+ this.terminal.print(' three group <id1> <id2> ... Group objects together');
+ this.terminal.print(' three ungroup <id> Ungroup a group');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Transforms ──', 'info');
this.terminal.print(' three pos <id> <x> <y> <z> Move an object');
+ this.terminal.print(' three rotate <id> <x> <y> <z> Rotate (radians)');
+ this.terminal.print(' three scale <id> <x> <y> <z> Scale an object');
+ this.terminal.print(' three color <id> <hex> Change object color');
+ this.terminal.print(' three material <id> <k> <v> Change material (metalness, roughness, opacity)');
this.terminal.print('', '');
this.terminal.print(' ── Camera & Controls ──', 'info');
this.terminal.print(' three camera <x> <y> <z> Set camera position');
this.terminal.print(' three orbit Toggle orbit controls');
this.terminal.print('', '');
+ this.terminal.print(' ── Animations ──', 'info');
+ this.terminal.print(' three animate <id> rotate|bounce|float|orbit [speed] Animate object');
+ this.terminal.print(' three stop [id] Stop animation(s)');
+ this.terminal.print(' three timeline add <id> <t> <x> <y> <z> Add keyframe');
+ this.terminal.print(' three timeline play|pause|stop|clear|list Control timeline');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Interaction ──', 'info');
+ this.terminal.print(' three click <id> <cmd...> Make object clickable (runs command)');
+ this.terminal.print(' three hover <id> <enter> <leave> Hover highlight colors');
+ this.terminal.print(' three follow <id> <target> Make object follow another');
+ this.terminal.print(' three unfollow <id> Stop following');
+ this.terminal.print(' three path <id> <x> <y> <z> [more...] Set waypoint path');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Avatars ──', 'info');
+ this.terminal.print(' three avatar [name] [color] Create an AI avatar');
+ this.terminal.print(' three pose idle|wave|think|point|cross Set avatar pose');
+ this.terminal.print(' three look <x> <y> <z> Avatar looks at point');
+ this.terminal.print(' three speak <text...> Show speech bubble');
+ this.terminal.print(' three emotion happy|sad|angry|confused|thinking|neutral');
+ this.terminal.print('', '');
this.terminal.print(' ── Mindmap ──', 'info');
this.terminal.print(' three mindmap [name] Render current mindmap in 3D');
this.terminal.print('', '');
this.terminal.print(' ── Presets ──', 'info');
this.terminal.print(' three preset solar Solar system');
this.terminal.print(' three preset grid [n] [spc] Color grid of cubes');
+ this.terminal.print(' three preset particles [n] [spread] Star field');
this.terminal.print('', '');
- this.terminal.print(' ── Export ──', 'info');
+ this.terminal.print(' ── Import/Export ──', 'info');
this.terminal.print(' three export [name] Export scene as standalone HTML');
+ this.terminal.print(' three import <url> Import 3D model (GLTF/GLB)');
this.terminal.print('', '');
this.terminal.print(' ── AI Tools ──', 'info');
this.terminal.print(' The AI can create 3D scenes via create_3d_scene tool.');
@@ -1136,6 +2380,11 @@ class ThreeModule {
results.push('torus');
}
+ if (desc.includes('avatar') || desc.includes('character') || desc.includes('robot')) {
+ this._createAvatar(['assistant', '#00ff88']);
+ results.push('avatar');
+ }
+
if (desc.includes('solar') || desc.includes('planet')) {
this._presetSolarSystem();
return 'Created solar system preset';
@@ -1147,10 +2396,10 @@ class ThreeModule {
}
if (results.length === 0) {
- this._addBox(['1', '#818cf8']);
- this._addSphere(['0.6', '#34d399']);
- this._setPosition('obj_1', -2, 0, 0);
- this._setPosition('obj_2', 2, 0, 0);
+ const id1 = this._addBox(['1', '#818cf8']);
+ const id2 = this._addSphere(['0.6', '#34d399']);
+ this._setPosition(id1, -2, 0, 0);
+ this._setPosition(id2, 2, 0, 0);
results.push('default scene');
}
diff --git a/modules/ext/tutorial.js b/modules/ext/tutorial.js
index af4f4fa..e080efd 100644
--- a/modules/ext/tutorial.js
+++ b/modules/ext/tutorial.js
@@ -280,8 +280,4 @@ class TutorialModule {
setTimeout(() => el.remove(), 5000);
}
}
-}
-
-if (typeof window.webx11 !== 'undefined' && window.webx11) {
- webx11.moduleManager.registerModule(new TutorialModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/tutorial.js');
-}
+} \ No newline at end of file
diff --git a/modules/ext/voice.js b/modules/ext/voice.js
index b243ace..284d6f3 100644
--- a/modules/ext/voice.js
+++ b/modules/ext/voice.js
@@ -9,9 +9,29 @@ class VoiceModule {
this._lastError = '';
this._sttEndpoint = '/vmai-api/stt'; // VaultMind STT via nginx proxy
this._recordingDuration = 10000; // 10s auto-stop
+ this._voices = [];
+ this._autoSpeak = false;
+ }
+ init(terminal) {
+ this.terminal = terminal;
+ // Pre-load TTS voices (Chrome loads async)
+ if (typeof speechSynthesis !== 'undefined') {
+ this._voices = speechSynthesis.getVoices() || [];
+ if (this._voices.length === 0) {
+ var self = this;
+ speechSynthesis.onvoiceschanged = function() {
+ self._voices = speechSynthesis.getVoices() || [];
+ };
+ }
+ }
+ }
+ _getBestVoice() {
+ var voices = this._voices.length > 0 ? this._voices : (typeof speechSynthesis !== 'undefined' ? speechSynthesis.getVoices() : []);
+ return voices.find(function(v) { return v.lang && v.lang.startsWith('en') && v.name.includes('Google'); })
+ || voices.find(function(v) { return v.lang && v.lang.startsWith('en'); })
+ || (voices.length > 0 ? voices[0] : null);
}
requirements() {
- // MediaRecorder + getUserMedia work on ALL modern browsers
const hasMic = typeof navigator !== 'undefined' && !!navigator.mediaDevices?.getUserMedia;
const hasRecorder = typeof MediaRecorder !== 'undefined';
if (hasMic && hasRecorder) {
@@ -23,26 +43,20 @@ class VoiceModule {
return { met: false, missing };
}
getHelpInfo() {
- return { 'voice': 'voice [stop] — Start voice recording (Whisper STT), or type "voice stop" to cancel' };
+ return { 'voice': 'voice [stop] — Start voice recording (Whisper STT), or type "voice stop" to cancel', 'speak': 'speak [text] — Text-to-speech. No args = toggle auto-speak for AI responses' };
}
registerCommands() {
this.commands['voice'] = (args) => this.handle(args);
this.commands['speak'] = (args) => this.speak(args);
}
- getHelpInfo() {
- return { 'voice': 'voice [stop] — Start voice recording (Whisper STT), or type "voice stop" to cancel', 'speak': 'speak [text] — Text-to-speech. No args = toggle auto-speak for AI responses' };
- }
-
speak(args) {
const text = args.join(' ').trim();
if (!text) {
- // If no text, check if speechSynthesis is available
if (typeof speechSynthesis === 'undefined') {
this.terminal.print('🔊 TTS not supported in this browser', 'error');
return;
}
- // Toggle auto-speak mode
this._autoSpeak = !this._autoSpeak;
this.terminal.print(this._autoSpeak ? '🔊 Auto-speak ON — AI responses will be spoken' : '🔊 Auto-speak OFF', 'info');
return;
@@ -51,40 +65,59 @@ class VoiceModule {
this.terminal.print('🔊 TTS not supported in this browser', 'error');
return;
}
+ this._doSpeak(text);
+ this.terminal.print('🔊 Speaking: "' + text.substring(0, 80) + (text.length > 80 ? '...' : '') + '"', 'info');
+ }
+
+ _doSpeak(text) {
+ var self = this;
speechSynthesis.cancel();
- const utter = new SpeechSynthesisUtterance(text);
+ var utter = new SpeechSynthesisUtterance(text);
utter.rate = parseFloat(localStorage.getItem('webx11_tts_rate') || '1.0');
utter.pitch = 1.0;
utter.volume = 1.0;
- // Try to use a good English voice
- const voices = speechSynthesis.getVoices();
- const enVoice = voices.find(v => v.lang.startsWith('en') && v.name.includes('Google')) || voices.find(v => v.lang.startsWith('en'));
- if (enVoice) utter.voice = enVoice;
- utter.onstart = () => this._updateButton(true);
- utter.onend = () => this._updateButton(false);
- utter.onerror = () => this._updateButton(false);
- speechSynthesis.speak(utter);
- this.terminal.print('🔊 Speaking: "' + text.substring(0, 80) + (text.length > 80 ? '...' : '') + '"', 'info');
+ var voice = this._getBestVoice();
+ if (voice) utter.voice = voice;
+ utter.onstart = function() { self._updateButton(true); };
+ utter.onend = function() { self._updateButton(false); };
+ utter.onerror = function() { self._updateButton(false); };
+ // If voices not loaded yet, wait for voiceschanged with 2s timeout
+ if (this._voices.length === 0 && typeof speechSynthesis !== 'undefined') {
+ var spoken = false;
+ var onReady = function() {
+ if (spoken) return;
+ spoken = true;
+ var v = self._getBestVoice();
+ if (v) utter.voice = v;
+ speechSynthesis.cancel();
+ speechSynthesis.speak(utter);
+ speechSynthesis.onvoiceschanged = null;
+ };
+ speechSynthesis.onvoiceschanged = onReady;
+ setTimeout(function() {
+ if (!spoken) {
+ spoken = true;
+ speechSynthesis.onvoiceschanged = null;
+ speechSynthesis.cancel();
+ speechSynthesis.speak(utter);
+ }
+ }, 2000);
+ } else {
+ speechSynthesis.speak(utter);
+ }
}
speakText(text) {
- // Called by AI assistant to auto-speak responses
if (!this._autoSpeak || typeof speechSynthesis === 'undefined') return;
speechSynthesis.cancel();
- // Strip markdown/code blocks for cleaner speech
const clean = text.replace(/```[\s\S]*?```/g, ' code block ').replace(/[*_#|`>]/g, '').replace(/\n+/g, '. ').trim();
if (clean.length > 500) {
- // Split long text into chunks
const chunks = clean.match(/.{1,500}/g);
- chunks.forEach(c => {
- const u = new SpeechSynthesisUtterance(c);
- u.rate = parseFloat(localStorage.getItem('webx11_tts_rate') || '1.0');
- speechSynthesis.speak(u);
- });
+ for (var i = 0; i < chunks.length; i++) {
+ this._doSpeak(chunks[i]);
+ }
} else {
- const u = new SpeechSynthesisUtterance(clean);
- u.rate = parseFloat(localStorage.getItem('webx11_tts_rate') || '1.0');
- speechSynthesis.speak(u);
+ this._doSpeak(clean);
}
}
handle(args) {