aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/module-aiassist.js503
-rw-r--r--modules/core/module-chatstudio.js2
-rw-r--r--modules/core/module-fs.js52
-rw-r--r--modules/core/module-loader.js5
-rw-r--r--modules/core/module-neural-vfs.js265
-rw-r--r--modules/core/module-ollama.js9
-rw-r--r--modules/core/module-orchestrator.js108
-rw-r--r--modules/core/module-sandbox.js62
-rw-r--r--modules/core/module-simplegui.js3
-rw-r--r--modules/core/module-skills.js295
-rw-r--r--modules/core/module-system.js53
-rw-r--r--modules/core/module-world-model.js58
-rw-r--r--modules/core/sandbox-worker.js119
-rw-r--r--modules/core/terminal.js350
-rw-r--r--modules/core/virtual-fs.js32
15 files changed, 1386 insertions, 530 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index b93b12d..5a4341a 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -20,6 +20,16 @@
this._toolDefs = null;
this._toolCallLog = [];
this._debugPanelEnabled = false;
+ // ๐Ÿ”’ Guardrail Layer
+ this._guardrailPolicy = null;
+ this._shadowSnapshot = null;
+ this._guardrailViolations = {};
+ this._confirmTimeoutMs = 120000;
+ this._skillDebounceMs = 1800000;
+ this._chatLockTimeoutMs = 10000;
+ this._loadGuardrailPolicy();
+ // Restore AI config from localStorage (maxContext, MAX_TOOL_TURNS, etc.)
+ try { const cfg = localStorage.getItem('webx11_ai_config'); if (cfg) { const p = JSON.parse(cfg); if (p.maxTurns) this.MAX_TOOL_TURNS = p.maxTurns; if (p.maxContext) this.maxContext = p.maxContext; if (p.maxContextTokens) this.maxContextTokens = p.maxContextTokens; if (p.confirmTimeoutMs) this._confirmTimeoutMs = p.confirmTimeoutMs; if (p.skillDebounceMs) this._skillDebounceMs = p.skillDebounceMs; if (p.chatLockTimeoutMs) this._chatLockTimeoutMs = p.chatLockTimeoutMs; } } catch (e) {}
try { const s = localStorage.getItem('webx11_ai_patterns'); if (s) this._commandFreq = JSON.parse(s); } catch (e) {}
try { const p = localStorage.getItem('webx11_user_profile'); if (p) this._userProfile = JSON.parse(p); } catch (e) {}
// Restore AI conversation context from localStorage
@@ -455,11 +465,11 @@ RULES:
required: []
}
}
- }, {
+}, {
type: 'function',
function: {
name: 'smart_context_set',
- description: 'Set the active AI context profile. Changes the AI\'s behavior (temperature, style) for subsequent responses.',
+ description: "Set the active AI context profile. Changes the AI's behavior (temperature, style) for subsequent responses.",
parameters: {
type: 'object',
properties: {
@@ -468,6 +478,20 @@ RULES:
required: ['name']
}
}
+ }, {
+ type: 'function',
+ function: {
+ name: 'create_mindmap',
+ description: 'Create a mindmap with the given topic and child nodes, then render it as inline SVG in the terminal. Creates root and all children in one call. Use this when the user asks for a mindmap or visual architecture.',
+ parameters: {
+ type: 'object',
+ properties: {
+ topic: { type: 'string', description: 'Root topic of the mindmap (e.g. "WebX11 Architecture")' },
+ children: { type: 'string', description: 'Comma-separated list of child node names (e.g. "AI Engine, VFS System, Modules")' }
+ },
+ required: ['topic']
+ }
+ }
}];
}
getHelpInfo() {
@@ -481,7 +505,8 @@ RULES:
'ai-deny': 'ai-deny โ€” Deny pending AI action',
'ai-ignore': 'ai-ignore โ€” Suppress validator warnings for this session',
'ai-stop': 'ai-stop โ€” Cancel current AI operation',
- 'ai-model': 'ai-model [model] โ€” Show or set AI model'
+ 'ai-model': 'ai-model [model] โ€” Show or set AI model',
+ 'ai-config': 'ai-config [key] [value] โ€” View or set AI loop config (max-turns, max-context)'
};
}
registerCommands() {
@@ -496,7 +521,14 @@ RULES:
this.commands['ai-stop'] = () => this.cancelAi();
this.commands['ai-model'] = (args) => this.showModel(args);
this.commands['ai-compress'] = () => this._manualCompress();
+ this.commands['ai-config'] = (args) => this.aiConfig(args);
this.commands['debug'] = () => this.toggleDebug();
+ 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]);
+ this.commands['shadow'] = (args) => this.shadowExec(args);
+ this.commands['shadow-commit'] = () => this.shadowCommit();
+ this.commands['shadow-rollback'] = () => this.shadowRollback();
}
async chat(args) {
const userMessage = args.join(' ');
@@ -507,7 +539,7 @@ RULES:
this.terminal.setAiBusy(true);
const lockStart = Date.now();
while (this._chatLock) {
- if (Date.now() - lockStart > 10000) {
+ if (Date.now() - lockStart > this._chatLockTimeoutMs) {
this._chatLock = false;
this.terminal.print('โš ๏ธ Chat lock timed out โ€” force released', 'warning');
break;
@@ -528,13 +560,6 @@ RULES:
this.context.slice().forEach(msg => messages.push(msg));
messages.push({ role: 'user', content: userMessage });
this.terminal.print('๐Ÿค– WebX11AI thinking...', 'info');
- // Ensure skills directory exists and seed defaults on first run
- this.terminal.fs.mkdirRecursive('/skills');
- if (!this.terminal.fs.readFile('/skills/_seeded')) {
- try {
- await this._seedDefaultSkills();
- } catch (e) { /* non-critical */ }
- }
// Auto-load skills relevant to the user's query (awaited, not fire-and-forget)
// Skip skill loading for trivial queries (< 10 chars, no actionable keywords)
const optEnabled = this.terminal?.ollamaManager?.optimizationEnabled !== false;
@@ -775,6 +800,17 @@ RULES:
}));
for (const { fn, toolResult, t1 } of safeResults) {
this.terminal.print(`๐Ÿ”ง Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
+ // Memory graph: record tool call + file access
+ try {
+ const _mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ if (_mg) {
+ _mg.recordToolCall(fn.name, !this._isToolError(toolResult));
+ if (fn.name === 'read_file' || fn.name === 'write_file' || fn.name === 'view_module' || fn.name === 'edit_module') {
+ const _path = (typeof fn.arguments === 'object' ? (fn.arguments?.path || '') : '');
+ if (_path) _mg.recordFileAccess(_path);
+ }
+ }
+ } catch (_e) { /* memory graph not available */ }
const obs = this._summarizeObservation(toolResult, 150);
const err = this._isToolError(toolResult);
if (err) {
@@ -793,7 +829,20 @@ RULES:
}
// Only block on destructive tools
if (destructiveTools.length > 0) {
- const approved = await this.confirmActionMulti(destructiveTools);
+ // ๐ŸŽฏ CONFIDENCE ESCALATION โ€” auto-approve only if confidence >= 90%
+ let autoApprove = false;
+ if (pendingText) {
+ const confidence = this._parseConfidence(pendingText);
+ if (confidence !== null && confidence >= 90) {
+ autoApprove = true;
+ this.terminal.print(`๐ŸŽฏ AI confidence: ${confidence}% โ€” auto-approved`, 'success');
+ } else if (confidence !== null) {
+ this.terminal.print(`๐ŸŽฏ AI confidence: ${confidence}% โ€” below 90% threshold, requiring manual approval`, 'warning');
+ } else {
+ this.terminal.print('๐ŸŽฏ No confidence declared for destructive action โ€” requiring manual approval', 'warning');
+ }
+ }
+ const approved = autoApprove || await this.confirmActionMulti(destructiveTools);
if (approved) {
for (const tc of destructiveTools) {
const fn = tc.function;
@@ -813,6 +862,17 @@ RULES:
const sm2 = this.terminal.moduleManager.modules['ServerMonitorModule'];
if (sm2 && sm2.logToolCall) sm2.logToolCall(fn.name, (t1 * 1000) || 1, this._isToolError(toolResult));
if (this._debugPanelEnabled) this._updateDebugPanel();
+ // Memory graph: record destructive tool call
+ try {
+ const _mg2 = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ if (_mg2) {
+ _mg2.recordToolCall(fn.name, !this._isToolError(toolResult));
+ if (fn.name === 'write_file' || fn.name === 'edit_module') {
+ const _path2 = (typeof fn.arguments === 'object' ? (fn.arguments?.path || '') : '');
+ if (_path2) _mg2.recordFileAccess(_path2);
+ }
+ }
+ } catch (_e2) { /* memory graph not available */ }
const dObs = this._summarizeObservation(toolResult, 150);
const dErr = this._isToolError(toolResult);
if (dErr) {
@@ -865,7 +925,7 @@ RULES:
async afterActionReflection(query, toolTurns) {
// Debounce: only suggest skill creation once per 30 minutes
const now = Date.now();
- if (this._lastSkillSuggestion && (now - this._lastSkillSuggestion) < 1800000) {
+ if (this._lastSkillSuggestion && (now - this._lastSkillSuggestion) < this._skillDebounceMs) {
return;
}
const suggestions = [];
@@ -955,7 +1015,7 @@ RULES:
// Countdown timer updates with escalating visibility
const timerInterval = setInterval(() => {
const elapsed = Math.round((Date.now() - startTime) / 1000);
- const remaining = Math.max(0, 120 - elapsed);
+ const remaining = Math.max(0, (this._confirmTimeoutMs / 1000) - elapsed);
const timerEl = document.getElementById('ai-confirm-timer');
if (timerEl) {
timerEl.textContent = `โฑ ${remaining}s`;
@@ -984,7 +1044,7 @@ RULES:
this._cleanupConfirmBar();
this.terminal.print('โฑ Confirmation timed out โ€” tool not executed', 'warning');
}
- }, 120000);
+ }, this._confirmTimeoutMs);
});
}
_resolveConfirm(approved) {
@@ -1065,12 +1125,288 @@ RULES:
this.terminal.print('๐Ÿ”ง Debug panel disabled', 'info');
}
}
+ // ๐Ÿ”’ === GUARDRAIL LAYER ===
+ _loadGuardrailPolicy() {
+ try {
+ const raw = localStorage.getItem('webx11_guardrail_policy');
+ if (raw) {
+ this._guardrailPolicy = JSON.parse(raw);
+ } else {
+ // Default policy โ€” seed on first run
+ this._guardrailPolicy = {
+ version: 1,
+ rules: [
+ {
+ id: 'rm-rf-block',
+ pattern: '/rm\\s+-rf/',
+ description: 'Prevent recursive force deletes outside trash',
+ severity: 'deny',
+ tools: ['run_command', 'write_file']
+ },
+ {
+ id: 'curl-pipe-bash',
+ pattern: '/(curl|wget)\\s+.*\\|\\s*(bash|sh)/',
+ description: 'Block curl-to-bash execution',
+ severity: 'deny',
+ tools: ['run_command']
+ },
+ {
+ id: 'rm-outer-vfs',
+ pattern: '/delete_file.*\\.(user\\.json|token|session|key)/',
+ description: 'Block deletion of sensitive metadata files',
+ severity: 'deny',
+ tools: ['delete_file']
+ },
+ {
+ id: 'mass-delete',
+ pattern: '/rm\\s+-rf\\s+\\/|delete_file.*\\*|\\.*\\/\\*/',
+ description: 'Block mass-delete operations',
+ severity: 'deny',
+ tools: ['run_command', 'delete_file']
+ },
+ {
+ id: 'chmod-777',
+ pattern: '/chmod\\s+777/',
+ description: 'Block world-writable permissions',
+ severity: 'deny',
+ tools: ['run_command']
+ },
+ {
+ id: 'external-dl',
+ pattern: '/(wget|curl)\\s+-[oO]\\s+/',
+ description: 'External file download โ€” requires high confidence',
+ severity: 'warn',
+ tools: ['run_command']
+ },
+ {
+ id: 'env-overwrite',
+ pattern: '/export\\s+\\w+\\s*=\\s*/',
+ description: 'Modifying environment variables',
+ severity: 'warn',
+ tools: ['run_command']
+ },
+ {
+ id: 'eval-danger',
+ pattern: '/(eval|exec|system)\\s*\\(/',
+ description: 'Dynamic code execution',
+ severity: 'deny',
+ tools: ['run_command']
+ }
+ ],
+ descriptions: {}
+ };
+ localStorage.setItem('webx11_guardrail_policy', JSON.stringify(this._guardrailPolicy));
+ }
+ } catch (e) {
+ this._guardrailPolicy = { version: 1, rules: [] };
+ }
+ }
+ _saveGuardrailPolicy() {
+ try {
+ localStorage.setItem('webx11_guardrail_policy', JSON.stringify(this._guardrailPolicy));
+ } catch (e) {}
+ }
+ checkGuardrail(toolName, args) {
+ if (!this._guardrailPolicy || !this._guardrailPolicy.rules) return null;
+ const argsStr = typeof args === 'object' ? JSON.stringify(args) : String(args);
+ for (const rule of this._guardrailPolicy.rules) {
+ if (!rule.tools.includes(toolName)) continue;
+ try {
+ const re = new RegExp(rule.pattern.slice(1, -1), 'i');
+ if (re.test(argsStr)) {
+ this._guardrailViolations[rule.id] = (this._guardrailViolations[rule.id] || 0) + 1;
+ return rule; // { id, pattern, description, severity, tools }
+ }
+ } catch (e) { /* skip malformed regex */ }
+ }
+ return null;
+ }
+ // ๐ŸŽฏ === CONFIDENCE ESCALATION ===
+ _parseConfidence(text) {
+ if (!text || typeof text !== 'string') return null;
+ // Look for patterns like "Confidence: 85%" or "confidence=95%" or "I am 72% confident"
+ const patterns = [
+ /confidence[:\s]*(\d+)%/i,
+ /confidence[=:]?\s*(\d+)\s*%/i,
+ /(\d+)%\s*confiden/i,
+ /confiden[ts]\s+(\d+)%/i
+ ];
+ for (const re of patterns) {
+ const m = text.match(re);
+ if (m) return parseInt(m[1], 10);
+ }
+ return null;
+ }
+ // ๐Ÿ“ธ === SHADOW VFS (Sandboxed Execution) ===
+ _snapshotShadowVFS() {
+ // Deep-clone the VFS root state into a snapshot
+ try {
+ const root = this.terminal.fs.root;
+ this._shadowSnapshot = JSON.parse(JSON.stringify(root));
+ return true;
+ } catch (e) {
+ this.terminal.print('โš ๏ธ Shadow VFS snapshot failed: ' + e.message, 'error');
+ return false;
+ }
+ }
+ _diffShadowVFS(consume = true) {
+ if (!this._shadowSnapshot) return null;
+ try {
+ const current = this.terminal.fs.root;
+ const changes = [];
+ this._walkDiff(this._shadowSnapshot, current, '', changes);
+ if (consume) {
+ this._shadowSnapshot = null;
+ }
+ return changes;
+ } catch (e) {
+ this._shadowSnapshot = null;
+ return [{ type: 'diff-error', message: e.message }];
+ }
+ }
+ _walkDiff(snapshot, live, path, changes) {
+ const snapKeys = snapshot && snapshot.content ? Object.keys(snapshot.content) : [];
+ const liveKeys = live && live.content ? Object.keys(live.content) : [];
+ // New files in live
+ for (const key of liveKeys) {
+ const livePath = path ? path + '/' + key : key;
+ const liveEntry = live.content[key];
+ const snapEntry = snapshot.content ? snapshot.content[key] : undefined;
+ if (!snapEntry) {
+ const entrySize = liveEntry.type === 'file' ? (liveEntry.content || '').length : 0;
+ changes.push({ type: 'created', path: livePath, entryType: liveEntry.type, size: entrySize });
+ } else if (liveEntry.type === 'file' && liveEntry.content !== snapEntry.content) {
+ const oldLen = (snapEntry.content || '').length;
+ const newLen = (liveEntry.content || '').length;
+ changes.push({ type: 'modified', path: livePath, sizeBefore: oldLen, sizeAfter: newLen });
+ }
+ // Recurse into directories
+ if (liveEntry.type === 'dir' && snapEntry && snapEntry.type === 'dir') {
+ this._walkDiff(snapEntry, liveEntry, livePath, changes);
+ }
+ }
+ // Deleted files
+ for (const key of snapKeys) {
+ if (!liveKeys.includes(key)) {
+ changes.push({ type: 'deleted', path: path ? path + '/' + key : key });
+ }
+ }
+ }
+ // ๐Ÿ”’ === GUARDRAIL COMMANDS ===
+ guardrailCmd(args) {
+ if (args.length === 0) {
+ this.terminal.print('=== Guardrail Policy ===', 'info');
+ if (!this._guardrailPolicy) {
+ this.terminal.print('No guardrail policy loaded', 'warning');
+ return;
+ }
+ this.terminal.print(`Rules: ${this._guardrailPolicy.rules.length}`, 'info');
+ for (const rule of this._guardrailPolicy.rules) {
+ const icon = rule.severity === 'deny' ? '๐Ÿ”ด' : '๐ŸŸก';
+ this.terminal.print(`${icon} ${rule.id}: ${rule.description}`, 'info');
+ }
+ const violations = Object.values(this._guardrailViolations).reduce((a, b) => a + b, 0);
+ if (violations > 0) {
+ this.terminal.print(`โš ๏ธ ${violations} violation(s) this session`, 'warning');
+ }
+ this.terminal.print('Usage: guardrail-add <id> <severity> <pattern> <tools...> | guardrail-remove <id>', 'info');
+ return;
+ }
+ const sub = args[0].toLowerCase();
+ if (sub === 'add' && args.length >= 4) {
+ const id = args[1];
+ const severity = args[2];
+ const pattern = args[3];
+ const tools = args.slice(4);
+ this._guardrailPolicy.rules.push({ id, severity, pattern, tools, description: 'Custom rule' });
+ this._saveGuardrailPolicy();
+ this.terminal.print(`โœ… Guardrail rule '${id}' added`, 'success');
+ } else if (sub === 'remove' && args.length >= 2) {
+ const idx = this._guardrailPolicy.rules.findIndex(r => r.id === args[1]);
+ if (idx >= 0) {
+ this._guardrailPolicy.rules.splice(idx, 1);
+ this._saveGuardrailPolicy();
+ this.terminal.print(`โœ… Guardrail rule '${args[1]}' removed`, 'success');
+ } else {
+ this.terminal.print(`โŒ Rule '${args[1]}' not found`, 'error');
+ }
+ } else {
+ this.terminal.print('Usage: guardrail-add <id> <severity> <pattern> <tools...> | guardrail-remove <id>', 'warning');
+ }
+ }
+ // ๐ŸŽฏ SHADOW COMMAND
+ shadowExec(args) {
+ // Usage: shadow <command>
+ // Snapshot VFS, run command, diff output
+ if (args.length === 0) {
+ this.terminal.print('Usage: shadow <command> โ€” Execute command in shadow VFS (sandbox preview)', 'warning');
+ return;
+ }
+ const cmd = args.join(' ');
+ this._snapshotShadowVFS();
+ const cmdName = cmd.split(' ')[0].toLowerCase();
+ const cmdArgs = cmd.split(' ').slice(1);
+ const originalPrint = this.terminal.print.bind(this.terminal);
+ let output = '';
+ this.terminal.print = (text, type) => { output += text + '\n'; };
+ this.terminal.dispatchCommand(cmdName, cmdArgs).then(() => {
+ this.terminal.print = originalPrint;
+ const diff = this._diffShadowVFS(false);
+ if (!diff || diff.length === 0) {
+ this.terminal.print('๐Ÿ“ฆ Shadow: No filesystem changes detected.', 'success');
+ } else {
+ this.terminal.print(`๐Ÿ“ฆ Shadow: ${diff.length} change(s) detected:`, 'info');
+ for (const c of diff) {
+ const icon = c.type === 'created' ? 'โž•' : c.type === 'modified' ? 'โœ๏ธ' : '๐Ÿ—‘๏ธ';
+ const sizeStr = c.size !== undefined ? c.size + 'B' : '';
+ const detail = c.type === 'created' ? `(${c.entryType}${sizeStr ? ', ' + sizeStr : ''})` :
+ c.type === 'modified' ? `(${c.sizeBefore}โ†’${c.sizeAfter}B)` : '';
+ this.terminal.print(`${icon} ${c.path} ${detail}`, c.type === 'deleted' ? 'warning' : 'info');
+ }
+ // Ask to commit or rollback
+ this.terminal.print('๐Ÿ”’ VFS modified โ€” type "shadow-commit" to accept or "shadow-rollback" to undo.', 'warning');
+ }
+ }).catch(e => {
+ this.terminal.print = originalPrint;
+ this.terminal.print('โš ๏ธ Shadow command error: ' + e.message, 'error');
+ });
+ }
+ shadowCommit() {
+ this._shadowSnapshot = null;
+ this.terminal.print('โœ… Shadow changes committed to VFS', 'success');
+ }
+ shadowRollback() {
+ if (!this._shadowSnapshot) {
+ this.terminal.print('โŒ No shadow snapshot to rollback to', 'error');
+ return;
+ }
+ try {
+ this.terminal.fs.root = JSON.parse(JSON.stringify(this._shadowSnapshot));
+ this.terminal.fs.saveToLocalStorage();
+ this._shadowSnapshot = null;
+ this.terminal.print('โ†ฉ๏ธ VFS rolled back to pre-shadow state', 'success');
+ } catch (e) {
+ this.terminal.print('โš ๏ธ Rollback failed: ' + e.message, 'error');
+ }
+ }
+ // ๐Ÿ”’ OVERRIDES for isDestructive โ€” adds runTool intercept
isDestructive(name) {
return name === 'write_file' || name === 'delete_file' || name === 'run_command' || name === 'make_directory' || name === 'edit_module';
}
async runTool(name, argsStr) {
let a = {};
try {
+ // ๐Ÿ”’ GUARDRAIL CHECK โ€” intercept before any tool executes
+ const guardrail = this.checkGuardrail(name, argsStr);
+ if (guardrail && guardrail.severity === 'deny') {
+ const errMsg = `๐Ÿ”’ GUARDRAIL BLOCKED: ${guardrail.description} (rule: ${guardrail.id})`;
+ this.terminal.print(errMsg, 'error');
+ this.terminal.log('WARN', 'GUARDRAIL', errMsg);
+ return JSON.stringify({ error: errMsg });
+ }
+ if (guardrail && guardrail.severity === 'warn') {
+ this.terminal.print(`๐ŸŸก GUARDRAIL WARNING: ${guardrail.description} (rule: ${guardrail.id})`, 'warning');
+ }
a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {});
switch (name) {
case 'list_files': {
@@ -1200,10 +1536,19 @@ RULES:
case 'run_command': {
const cmd = a.command || '';
if (!cmd) return JSON.stringify({ error: 'No command provided' });
+ // ๐Ÿ“ธ Shadow VFS auto-snapshot before run_command
+ this._snapshotShadowVFS();
const originalPrint = this.terminal.print.bind(this.terminal);
let output = '';
this.terminal.print = (text, type) => { output += text + '\n'; };
try { await this.terminal.dispatchCommand(cmd.split(' ')[0].toLowerCase(), cmd.split(' ').slice(1)); } finally { this.terminal.print = originalPrint; }
+ // ๐Ÿ“ธ Diff and append shadow report
+ const shadowDiff = this._diffShadowVFS();
+ if (shadowDiff && shadowDiff.length > 0) {
+ const diffSummary = shadowDiff.map(c => `${c.type}:${c.path}`).join(', ');
+ output += `\n๐Ÿ“ฆ [Shadow VFS: ${shadowDiff.length} change(s) โ€” ${diffSummary}]`;
+ this._shadowSnapshot = null; // Changes considered "committed" since AI explicitly ran command
+ }
return output.trim() || '(no output)';
}
case 'check_updates': {
@@ -1486,6 +1831,27 @@ RULES:
const profile = scm.contextProfiles[name];
return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature });
}
+ case 'create_mindmap': {
+ const mindmap = this.terminal.moduleManager.modules['MindmapModule'];
+ if (!mindmap) return JSON.stringify({ error: 'MindmapModule not loaded' });
+ const topic = a.topic || 'Mindmap';
+ const children = a.children || '';
+ mindmap.root = null;
+ const args = topic.split(' ');
+ mindmap._new(args);
+ if (!mindmap.root) return JSON.stringify({ error: 'Failed to create root' });
+ const rootId = mindmap.root.id;
+ const added = [];
+ if (children) {
+ const childNames = children.split(',').map(c => c.trim()).filter(Boolean);
+ for (const child of childNames) {
+ mindmap._add([rootId, child]);
+ added.push(child);
+ }
+ }
+ mindmap._svg([]);
+ return JSON.stringify({ success: true, topic, children: added.length ? added : 'none', nodes: mindmap._countNodes(mindmap.root), rendered: 'inline SVG in terminal' });
+ }
case 'view_module':
case 'edit_module':
case 'test_module':
@@ -1731,6 +2097,64 @@ RULES:
this.terminal.print('No models available. Use ollamaconnect first.', 'warning');
}
}
+ aiConfig(args) {
+ if (args.length === 0) {
+ this.terminal.print('=== AI Config ===', 'info');
+ this.terminal.print(`max-turns: ${this.MAX_TOOL_TURNS} (default: 8)`, '');
+ this.terminal.print(`max-context: ${this.maxContext} (default: 10)`, '');
+ this.terminal.print(`max-context-tokens: ${this.maxContextTokens} (default: 2000)`, '');
+ this.terminal.print(`trivial-optimization: ${(this.terminal?.ollamaManager?.optimizationEnabled !== false) ? 'on' : 'off'}`, '');
+ this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs / 1000}s (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)`, '');
+ this.terminal.print('Usage: ai-config <key> <value>', 'info');
+ this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization', 'info');
+ return;
+ }
+ const key = args[0];
+ const val = args[1];
+ if (key === 'max-turns') {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 1 || n > 50) return this.terminal.print('max-turns must be 1-50', 'warning');
+ this.MAX_TOOL_TURNS = n;
+ } else if (key === 'max-context') {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 1 || n > 100) return this.terminal.print('max-context must be 1-100', 'warning');
+ this.maxContext = n;
+ } else if (key === 'max-context-tokens') {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 100 || n > 10000) return this.terminal.print('max-context-tokens must be 100-10000', 'warning');
+ this.maxContextTokens = n;
+ } else if (key === 'confirm-timeout') {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 5 || n > 600) return this.terminal.print('confirm-timeout must be 5-600 seconds', 'warning');
+ this._confirmTimeoutMs = n * 1000;
+ } else if (key === 'chat-lock-timeout') {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 1 || n > 60) return this.terminal.print('chat-lock-timeout must be 1-60 seconds', 'warning');
+ this._chatLockTimeoutMs = n * 1000;
+ } else if (key === 'skill-debounce') {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 1 || n > 240) return this.terminal.print('skill-debounce must be 1-240 minutes', 'warning');
+ this._skillDebounceMs = n * 60000;
+ } else if (key === 'trivial-optimization') {
+ if (this.terminal?.ollamaManager) {
+ this.terminal.ollamaManager.optimizationEnabled = val === 'on';
+ }
+ } 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`, 'warning');
+ }
+ // Persist
+ localStorage.setItem('webx11_ai_config', JSON.stringify({
+ maxTurns: this.MAX_TOOL_TURNS,
+ maxContext: this.maxContext,
+ maxContextTokens: this.maxContextTokens,
+ confirmTimeoutMs: this._confirmTimeoutMs,
+ chatLockTimeoutMs: this._chatLockTimeoutMs,
+ skillDebounceMs: this._skillDebounceMs
+ }));
+ this.terminal.print(`ai-config ${key} set to ${val}`, 'success');
+ }
showContext() {
if (this.context.length === 0) {
this.terminal.print('No conversation context', 'info'); return;
@@ -1767,55 +2191,6 @@ RULES:
try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {}
}
}
- async _seedDefaultSkills() {
- const fs = this.terminal.fs;
- const mindmapSkill = `---
-name: mindmap-user-guide
-description: How to create, view, and manage brainstorming mindmaps using the mindmap terminal module
-tags: [mindmap, brainstorming, organization, planning]
-category: productivity
----
-## Steps
-
-1. **Create a new mindmap:**
- - \`mindmap new "<topic>" -t tag1,tag2\` โ€” creates a root node
- - Tags are optional (e.g. planning, research, dev)
- - Root gets auto-assigned a color
-
-2. **Add child nodes:**
- - \`mindmap add <parent-id> "<idea>" -t tag\` โ€” add a child
- - Get the parent-id from \`mindmap show\`
- - IDs are 4-char random strings (e.g. a3f8, x7k2)
-
-3. **View the mindmap:**
- - \`mindmap show\` โ€” full tree with Unicode box-drawing
- - \`mindmap show <id>\` โ€” subtree starting at that node
-
-4. **Edit nodes:**
- - \`mindmap rename <id> "<new text>"\` โ€” change text
- - \`mindmap color <id> <color>\` โ€” set color (red/green/yellow/blue/cyan/magenta/white)
- - \`mindmap tag <id> <tag>\` โ€” add a tag
-
-5. **Persistence:**
- - \`mindmap save <name>\` โ€” save to localStorage
- - \`mindmap load <name>\` โ€” restore
- - \`mindmap list\` โ€” show saved mindmaps
- - \`mindmap delete <name>\` โ€” remove saved
-
-6. **Export:**
- - \`mindmap export [id]\` โ€” Markdown bullet list output
-
-## When to use
-- User wants to brainstorm, organize ideas, plan features
-- User says "map out", "diagram", "brainstorm", "organize thoughts"
-- Visualizing project structure or task decomposition`;
-
- fs.writeFile('/skills/mindmap-user-guide.md', mindmapSkill);
- const existingIndex = fs.fsReadJSON('/skills/index.json') || {};
- existingIndex['mindmap-user-guide'] = { name: 'mindmap-user-guide', description: 'How to create, view, and manage brainstorming mindmaps using the mindmap terminal module', tags: ['mindmap', 'brainstorming', 'organization', 'planning'], category: 'productivity', created: existingIndex['mindmap-user-guide']?.created || new Date().toISOString(), updated: new Date().toISOString() };
- fs.fsWriteJSON('/skills/index.json', existingIndex);
- fs.writeFile('/skills/_seeded', '1');
- }
_trackPattern(userMessage) {
const lower = userMessage.toLowerCase();
const patterns = ['font size','bigger text','smaller text','export','backup','save','install','load module','search','find','color','theme','skill','learn','remember','mindmap','brainstorm'];
diff --git a/modules/core/module-chatstudio.js b/modules/core/module-chatstudio.js
index 9643fff..1ca5220 100644
--- a/modules/core/module-chatstudio.js
+++ b/modules/core/module-chatstudio.js
@@ -1,5 +1,5 @@
/*
- * ChatStudio Overlay Module for newOS4 v4.2
+ * ChatStudio Overlay Module for newOS4 โ€” reads window.NEWOS4_VERSION at runtime
* Integrates ChatStudio Lite's full chat UI as an overlay launched via `chat` command.
* All Lite engine functions are inlined from lite/cslite-core.js, adapted to use
* the terminal's OllamaManager for endpoint configuration.
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js
index 160dccc..c2f29c4 100644
--- a/modules/core/module-fs.js
+++ b/modules/core/module-fs.js
@@ -351,15 +351,65 @@ class FileSystemModule {
setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 2000);
this.terminal.print('Exported VFS to webx11-filesystem-' + dateStr + '.json', 'success');
};
+ this.commands['vfs-size'] = () => {
+ try {
+ const json = JSON.stringify(this.terminal.fs.root);
+ const totalSize = (json.length / 1024 / 1024).toFixed(2);
+ const root = this.terminal.fs.root;
+ const sizeMap = [];
+ function walk(node, path) {
+ if (!node || !node.content) return;
+ for (const [name, child] of Object.entries(node.content)) {
+ const fullPath = path + '/' + name;
+ if (child.type === 'file') {
+ const size = (child.content ? child.content.length : 0);
+ sizeMap.push({ path: fullPath, size, sizeKB: (size / 1024).toFixed(1) });
+ } else if (child.type === 'dir') {
+ walk(child, fullPath);
+ }
+ }
+ }
+ walk(root, '');
+ sizeMap.sort((a, b) => b.size - a.size);
+ const totalFiles = sizeMap.length;
+ const top5 = sizeMap.slice(0, 5);
+ this.terminal.print('๐Ÿ“Š VFS Size Report', 'text-muted');
+ this.terminal.print(' Total: ' + totalSize + ' MB across ' + totalFiles + ' files');
+ this.terminal.print(' Largest:');
+ top5.forEach((f, i) => {
+ this.terminal.print(' ' + (i + 1) + '. ' + f.path + ' โ€” ' + f.sizeKB + ' KB');
+ });
+ // Show system dir sizes
+ const sysDirs = ['/_snapshots', '/_sessions', '/.trash'];
+ for (const dir of sysDirs) {
+ const dirNode = this.terminal.fs.readDir(dir);
+ if (dirNode) {
+ const cnt = dirNode.length;
+ // estimate dir size
+ const dirSize = JSON.stringify(dirNode).length;
+ this.terminal.print(' ' + dir + ': ' + cnt + ' files, ~' + (dirSize / 1024).toFixed(1) + ' KB', dirSize > 102400 ? 'warning' : 'text-muted');
+ }
+ }
+ } catch (e) {
+ this.terminal.print('vfs-size error: ' + e.message, 'error');
+ }
+ };
// Session commands (save/load/export/import)
this.commands['session-save'] = (args) => {
if (!args[0]) return this.terminal.print('Usage: session-save <name>', 'warning');
const name = args[0].replace(/[^a-zA-Z0-9_-]/g, '_');
+ // Strip system dirs to avoid recursive bloat in snapshots
+ const fsRoot = JSON.parse(JSON.stringify(this.terminal.fs.root));
+ if (fsRoot && fsRoot.content) {
+ delete fsRoot.content['_sessions'];
+ delete fsRoot.content['_snapshots'];
+ delete fsRoot.content['.trash'];
+ }
const snapshot = {
type: 'session',
savedAt: new Date().toISOString(),
- fsRoot: this.terminal.fs.root,
+ fsRoot: fsRoot,
currentPath: this.terminal.fs.currentPath,
version: 1
};
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index b32577d..c5237ad 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -27,7 +27,6 @@
'module-smartctx.js',
'module-duckduckgo.js',
'module-fetcher.js',
- 'module-skills.js',
'secret-vault.js',
'module-chatstudio.js',
'module-agent-registry.js',
@@ -106,7 +105,7 @@
window.__moduleLoaderReload = reloadWithSelection;
-const LATEST_VER = '2026-06-05-v4.4.0';
+const LATEST_VER = '2026-06-06-v4.5.6';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
@@ -150,7 +149,7 @@ const CACHE = '?v=' + LATEST_VER;
setTimeout(async () => {
try {
if (webx11.ollamaManager && !webx11.ollamaManager.connected) {
- webx11.log('INFO', 'System', 'WebX11 v4.2 started');
+ webx11.log('INFO', 'System', 'WebX11 ' + (window.NEWOS4_VERSION || 'v4.0') + ' started');
webx11.log('INFO', 'System', 'Default model: ' + webx11.ollamaManager.currentModel);
await webx11.ollamaManager.connect();
if (typeof webx11.updateOllamaStatus === 'function') {
diff --git a/modules/core/module-neural-vfs.js b/modules/core/module-neural-vfs.js
index e9ffafb..2644e85 100644
--- a/modules/core/module-neural-vfs.js
+++ b/modules/core/module-neural-vfs.js
@@ -4,6 +4,20 @@ class NeuralVFSModule {
this.terminal = null;
this.index = {};
this.indexPath = '/.semantic_index/map.json';
+ this.hashCachePath = '/.semantic_index/hash_cache.json';
+ this._hashCache = {};
+ // Configurable display limits
+ this._fuzzyLimit = 10;
+ this._treeFiles = 5;
+ this._treeConcepts = 15;
+ this._resolveFiles = 20;
+ this._loadConfig();
+ }
+ _loadConfig() {
+ try { const c = localStorage.getItem('webx11_nvfs_config'); if (c) { const p = JSON.parse(c); if (p.fuzzyLimit) this._fuzzyLimit = p.fuzzyLimit; if (p.treeFiles) this._treeFiles = p.treeFiles; if (p.treeConcepts) this._treeConcepts = p.treeConcepts; if (p.resolveFiles) this._resolveFiles = p.resolveFiles; } } catch (e) {}
+ }
+ _saveConfig() {
+ localStorage.setItem('webx11_nvfs_config', JSON.stringify({ fuzzyLimit: this._fuzzyLimit, treeFiles: this._treeFiles, treeConcepts: this._treeConcepts, resolveFiles: this._resolveFiles }));
}
getHelpInfo() {
@@ -11,7 +25,8 @@ class NeuralVFSModule {
'nvfs-index': 'nvfs-index โ€” Scan VFS and build semantic concept index',
'nvfs-status': 'nvfs-status โ€” Show semantic index stats and last scan time',
'nvfs-resolve': 'nvfs-resolve <concept> โ€” Find files related to a concept',
- 'nvfs-tree': 'nvfs-tree [concept] โ€” Show concept-to-file mapping as a tree'
+ 'nvfs-tree': 'nvfs-tree [concept] โ€” Show concept-to-file mapping as a tree',
+ 'nvfs-config': 'nvfs-config [key] [value] โ€” View or set Neural VFS display limits (fuzzy-limit, tree-files, tree-concepts, resolve-files)'
};
}
@@ -20,56 +35,133 @@ class NeuralVFSModule {
this.commands['nvfs-status'] = (args) => this.showStatus();
this.commands['nvfs-resolve'] = (args) => this.resolve(args);
this.commands['nvfs-tree'] = (args) => this.tree(args);
+ this.commands['nvfs-config'] = (args) => this.nvfsConfig(args);
}
- // โ”€โ”€ Core Index Builder โ”€โ”€
+ // โ”€โ”€ Core Index Builder (async, chunked) โ”€โ”€
- buildIndex() {
+ async buildIndex() {
const fs = this.terminal.fs;
if (!fs) return this.terminal.print('VFS not available', 'error');
this.terminal.print('๐Ÿ” Scanning VFS for semantic concepts...', 'info');
const start = Date.now();
- const newIndex = {};
+ let newIndex = {};
let fileCount = 0;
let conceptCount = 0;
+ let cachedCount = 0;
- // Walk VFS recursively โ€” skip system dirs
- this._walk('/', fs, newIndex, (path, node) => {
- if (node.type !== 'file') return;
- if (this._isSystemFile(path)) return;
-
- fileCount++;
- const concepts = this._extractConcepts(path, node.content || '');
- for (const concept of concepts) {
- if (!newIndex[concept]) {
- newIndex[concept] = [];
- conceptCount++;
- }
- // Avoid dupes
- if (!newIndex[concept].find(e => e.path === path)) {
- newIndex[concept].push({ path, summary: this._summarize(node.content) });
- }
- }
- });
+ // Load previous index and hash cache for incremental diffing
+ const prevIndex = fs.fsReadJSON(this.indexPath) || {};
+ this._loadHashCache(fs);
+
+ // Collect current file list
+ const fileList = [];
+ this._collectFiles('/', fs, fileList);
- // Also index skills
- this._indexSkills(fs, newIndex);
+ if (fileList.length === 0) {
+ return this.terminal.print('No files found to index', 'warning');
+ }
+
+ // Detect deleted files โ€” strip orphaned entries from prevIndex
+ const currentPaths = new Set(fileList.map(f => f.path));
+ let deletedCount = 0;
+ for (const [concept, entries] of Object.entries(prevIndex)) {
+ const remaining = entries.filter(e => currentPaths.has(e.path));
+ if (remaining.length !== entries.length) deletedCount++;
+ if (remaining.length > 0) prevIndex[concept] = remaining;
+ else delete prevIndex[concept];
+ }
+
+ // Warm start: clone prevIndex and count live paths for stats
+ newIndex = JSON.parse(JSON.stringify(prevIndex));
+ const livePaths = new Set();
+ for (const entries of Object.values(newIndex)) {
+ for (const e of entries) livePaths.add(e.path);
+ }
+ conceptCount = Object.keys(newIndex).length;
+
+ // Chunked async processing โ€” yields to UI between chunks
+ const chunkSize = 10;
+ let i = 0;
+ const processChunk = () => {
+ const end = Math.min(i + chunkSize, fileList.length);
+ for (let j = i; j < end; j++) {
+ const { path, node } = fileList[j];
+ fileCount++;
+ const content = node.content || '';
+ const hash = this._contentHash(content);
+
+ // Skip if hash unchanged โ€” content is same as last scan
+ const cached = this._getCached(path, hash);
+ if (cached) {
+ cachedCount++;
+ if (!livePaths.has(path)) {
+ // File was in cache but somehow missing from prevIndex
+ this._reindexFile(path, content, newIndex);
+ }
+ continue;
+ }
- this.index = newIndex;
- this.lastScan = new Date().toISOString();
- this.fileCount = fileCount;
- this.conceptCount = conceptCount;
+ // Changed or new file โ€” extract concepts fresh
+ this._reindexFile(path, content, newIndex);
+ this._hashCache[path] = { hash };
+ }
+ i = end;
+ if (i < fileList.length) {
+ const pct = Math.round(i / fileList.length * 100);
+ const cMsg = cachedCount > 0 ? ` (${cachedCount} cached)` : '';
+ this.terminal.print(`โณ Indexing... ${pct}% (${i}/${fileList.length} files${cMsg})`, 'info');
+ setTimeout(processChunk, 0);
+ } else {
+ // Also index skills
+ this._indexSkills(fs, newIndex);
+ this.index = newIndex;
+ this.lastScan = new Date().toISOString();
+ this.fileCount = fileCount;
+ this.conceptCount = Object.keys(newIndex).length;
+ this._saveIndex(fs);
+ if (fs._clearConceptCache) fs._clearConceptCache();
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
+ const processedMsg = cachedCount > 0 ? ` (${cachedCount} cached, ${fileCount - cachedCount} reindexed)` : '';
+ this.terminal.print(`โœ… Indexed ${fileCount} files โ†’ ${Object.keys(newIndex).length} concepts in ${elapsed}s${processedMsg}`, 'success');
+ if (deletedCount > 0) this.terminal.print(` Purged ${deletedCount} orphaned concepts from deleted files`, 'info');
+ this.terminal.print(` Index saved to ${this.indexPath}`, 'info');
+ }
+ };
+ processChunk();
+ }
- // Save to VFS
- this._saveIndex(fs);
- // Clear VFS concept cache so concept:// URIs reflect the new index
- if (fs._clearConceptCache) fs._clearConceptCache();
+ _reindexFile(path, content, index) {
+ const concepts = this._extractConcepts(path, content);
+ // Remove old entries for this path
+ for (const entries of Object.values(index)) {
+ const idx = entries.findIndex(e => e.path === path);
+ if (idx !== -1) entries.splice(idx, 1);
+ }
+ // Add new entries
+ for (const concept of concepts) {
+ if (!index[concept]) {
+ index[concept] = [];
+ this.conceptCount++;
+ }
+ index[concept].push({ path, summary: this._summarize(content) });
+ }
+ }
- const elapsed = ((Date.now() - start) / 1000).toFixed(1);
- this.terminal.print(`โœ… Indexed ${fileCount} files โ†’ ${conceptCount} concepts in ${elapsed}s`, 'success');
- this.terminal.print(` Index saved to ${this.indexPath}`, 'info');
+ _collectFiles(path, fs, list) {
+ const entries = fs.readDir(path);
+ if (!entries) return;
+ for (const entry of entries) {
+ const fullPath = path === '/' ? '/' + entry.name : path + '/' + entry.name;
+ if (entry.type === 'file') {
+ if (this._isSystemFile(fullPath)) continue;
+ list.push({ path: fullPath, node: { type: 'file', content: fs.readFile(fullPath) || '' } });
+ } else if (entry.type === 'dir') {
+ this._collectFiles(fullPath, fs, list);
+ }
+ }
}
// โ”€โ”€ Resolve โ”€โ”€
@@ -91,17 +183,17 @@ class NeuralVFSModule {
if (matches.length > 0) {
this.terminal.print(`๐Ÿ“‚ "${concept}" โ†’ ${matches.length} file(s):`, 'info');
- for (const m of matches.slice(0, 20)) {
+ for (const m of matches.slice(0, this._resolveFiles)) {
this.terminal.print(` ${m.path}`, 'success');
if (m.summary) this.terminal.print(` ${m.summary}`, '')
}
- if (matches.length > 20) {
- this.terminal.print(` ... and ${matches.length - 20} more`, 'info');
+ if (matches.length > this._resolveFiles) {
+ this.terminal.print(` ... and ${matches.length - this._resolveFiles} more`, 'info');
}
}
if (fuzzy.length > 0) {
- this.terminal.print(`\n๐Ÿ”€ Similar concepts: ${fuzzy.slice(0, 10).join(', ')}`, 'info');
+ this.terminal.print(`\n๐Ÿ”€ Similar concepts: ${fuzzy.slice(0, this._fuzzyLimit).join(', ')}`, 'info');
}
}
@@ -145,20 +237,45 @@ class NeuralVFSModule {
const filtered = filter ? concepts.filter(c => c.includes(filter)) : concepts;
this.terminal.print(`๐ŸŒณ Concept Tree${filter ? ` (filter: "${filter}")` : ''}:`, 'info');
- for (const concept of filtered.slice(0, 15)) {
+ for (const concept of filtered.slice(0, this._treeConcepts)) {
this.terminal.print(` ๐Ÿ“ ${concept}`, 'success');
- for (const entry of this.index[concept].slice(0, 5)) {
+ for (const entry of this.index[concept].slice(0, this._treeFiles)) {
this.terminal.print(` โ””โ”€ ${entry.path}`, '');
}
- if (this.index[concept].length > 5) {
- this.terminal.print(` โ””โ”€ ... (+${this.index[concept].length - 5} more)`, '');
+ if (this.index[concept].length > this._treeFiles) {
+ this.terminal.print(` โ””โ”€ ... (+${this.index[concept].length - this._treeFiles} more)`, '');
}
}
- if (filtered.length > 15) {
- this.terminal.print(` ... and ${filtered.length - 15} more concepts`, 'info');
+ if (filtered.length > this._treeConcepts) {
+ this.terminal.print(` ... and ${filtered.length - this._treeConcepts} more concepts`, 'info');
}
}
+ // โ”€โ”€ Nvfs Config โ”€โ”€
+
+ nvfsConfig(args) {
+ if (args.length === 0) {
+ this.terminal.print('=== Neural VFS Config ===', 'info');
+ this.terminal.print(`fuzzy-limit: ${this._fuzzyLimit} (default: 10)`, '');
+ this.terminal.print(`tree-files: ${this._treeFiles} (default: 5)`, '');
+ this.terminal.print(`tree-concepts: ${this._treeConcepts} (default: 15)`, '');
+ this.terminal.print(`resolve-files: ${this._resolveFiles} (default: 20)`, '');
+ this.terminal.print('Usage: nvfs-config <key> <value>', 'info');
+ this.terminal.print('Keys: fuzzy-limit, tree-files, tree-concepts, resolve-files', 'info');
+ return;
+ }
+ const key = args[0];
+ const val = parseInt(args[1]);
+ if (isNaN(val) || val < 1 || val > 200) return this.terminal.print('Value must be 1-200', 'warning');
+ if (key === 'fuzzy-limit') { this._fuzzyLimit = val; }
+ else if (key === 'tree-files') { this._treeFiles = val; }
+ else if (key === 'tree-concepts') { this._treeConcepts = val; }
+ else if (key === 'resolve-files') { this._resolveFiles = val; }
+ else { return this.terminal.print(`Unknown key: ${key}. Valid: fuzzy-limit, tree-files, tree-concepts, resolve-files`, 'warning'); }
+ this._saveConfig();
+ this.terminal.print(`nvfs-config ${key} set to ${val}`, 'success');
+ }
+
// โ”€โ”€ Private Helpers โ”€โ”€
_walk(path, fs, index, cb) {
@@ -179,6 +296,27 @@ class NeuralVFSModule {
return sysPrefixes.some(p => path.startsWith(p) || path.includes('/node_modules'));
}
+ _contentHash(str) {
+ if (!str) return '';
+ let hash = 5381;
+ for (let i = 0; i < str.length; i++) {
+ hash = ((hash << 5) + hash) + str.charCodeAt(i);
+ hash = hash & hash;
+ }
+ return Math.abs(hash).toString(36).slice(0, 12);
+ }
+
+ _loadHashCache(fs) {
+ const saved = fs.fsReadJSON(this.hashCachePath);
+ if (saved && typeof saved === 'object') this._hashCache = saved;
+ }
+
+ _getCached(path, hash) {
+ const c = this._hashCache[path];
+ if (c && c.hash === hash) return c;
+ return null;
+ }
+
_extractConcepts(path, content) {
const concepts = new Set();
const name = path.split('/').pop() || '';
@@ -235,10 +373,39 @@ class NeuralVFSModule {
_summarize(content) {
if (!content || content.length < 10) return '';
+ // Strip YAML frontmatter
const cleaned = content.replace(/---[\s\S]*?---/, '').trim();
- const firstLine = cleaned.split('\n').find(l => l.trim().length > 20);
- if (!firstLine) return '';
- return firstLine.trim().slice(0, 100) + (firstLine.length > 100 ? '...' : '');
+
+ // 1. Try first ## section heading
+ const sectionMatch = cleaned.match(/^##\s+(.+)/m);
+ if (sectionMatch && sectionMatch[1].trim().length > 5) {
+ const s = sectionMatch[1].trim();
+ return s.slice(0, 100) + (s.length > 100 ? '...' : '');
+ }
+
+ // 2. Try first paragraph > 40 chars after frontmatter
+ const lines = cleaned.split('\n').filter(l => l.trim().length > 0);
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (trimmed.length > 40 && !trimmed.startsWith('#') && !trimmed.startsWith('//')) {
+ return trimmed.slice(0, 100) + (trimmed.length > 100 ? '...' : '');
+ }
+ }
+
+ // 3. First meaningful sentence (> 30 chars)
+ const sentence = cleaned.match(/[^.!?\n]+[.!?]/);
+ if (sentence && sentence[0].trim().length > 30) {
+ const s = sentence[0].trim();
+ return s.slice(0, 100) + (s.length > 100 ? '...' : '');
+ }
+
+ // 4. Fallback: first line > 20 chars (original behavior)
+ const firstLine = lines.find(l => l.trim().length > 20);
+ if (firstLine) {
+ const tl = firstLine.trim();
+ return tl.slice(0, 100) + (tl.length > 100 ? '...' : '');
+ }
+ return '';
}
_indexSkills(fs, index) {
@@ -259,6 +426,8 @@ class NeuralVFSModule {
fs.mkdirRecursive('/.semantic_index');
}
fs.fsWriteJSON(this.indexPath, this.index);
+ // Also persist content hash cache for incremental diffing
+ fs.fsWriteJSON(this.hashCachePath, this._hashCache);
} catch (e) {
console.error('NeuralVFS save error:', e);
}
diff --git a/modules/core/module-ollama.js b/modules/core/module-ollama.js
index ba8ad57..aec52d4 100644
--- a/modules/core/module-ollama.js
+++ b/modules/core/module-ollama.js
@@ -30,9 +30,8 @@ class OllamaModule {
this.terminal.updateOllamaStatus();
};
this.commands['ollamamodels'] = () => {
- const models = this.terminal.ollamaManager.getModels();
- if(models.length === 0) return this.terminal.print("No models found. Connect first.");
- models.forEach(m => this.terminal.print(`${m.name} (${(m.size/1e9).toFixed(1)}GB)`));
+ // Redirect to the consolidated models command
+ this.listModels();
};
this.commands['ollamasetmodel'] = (args) => {
if(!args[0]) return this.terminal.print("Usage: ollamasetmodel <name>", 'warning');
@@ -62,7 +61,7 @@ class OllamaModule {
this.commands['model-run'] = (args) => this.runModel(args);
}
async listModels() {
- const endpoint = localStorage.getItem('webx11_ollama_endpoint') || 'http://localhost:11434';
+ const endpoint = this.terminal?.ollamaManager?.endpoint || localStorage.getItem('webx11_ollama_endpoint') || 'http://localhost:11434';
try {
const response = await fetch(`${endpoint}/api/tags`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
@@ -95,7 +94,7 @@ class OllamaModule {
if (!args[0]) {
this.terminal.print("Usage: model-info <model-name>", "warning"); return;
}
- const endpoint = localStorage.getItem('webx11_ollama_endpoint') || 'http://localhost:11434';
+ const endpoint = this.terminal?.ollamaManager?.endpoint || localStorage.getItem('webx11_ollama_endpoint') || 'http://localhost:11434';
try {
const response = await fetch(`${endpoint}/api/show`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js
index ec385bb..05e1598 100644
--- a/modules/core/module-orchestrator.js
+++ b/modules/core/module-orchestrator.js
@@ -13,10 +13,32 @@ class OrchestratorModule {
this._IDLE_TIMEOUT = 120000;
this._RESTART_POLICY = 'never';
this._A2A_POLLING = true;
+ this._A2A_POLL_INTERVAL = 500;
+ this._HEARTBEAT_CHECK = 3000;
+ this._IDLE_SWEEP_INTERVAL = 30000;
+ this._DEAD_PURGE_TIMEOUT = 60000;
this._a2aPollTimer = null;
this._heartbeatInterval = null;
this._a2aMessages = {};
this._restartBackoffs = {};
+ this._eventBus = {};
+ this._idleSweepInterval = null;
+ this._loadConfig();
+ }
+ _loadConfig() {
+ try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; } } catch (e) {}
+ }
+ _saveConfig() {
+ localStorage.setItem('webx11_orchestrator_config', JSON.stringify({
+ spawnLimit: this._SPAWN_LIMIT,
+ heartbeatTimeout: this._HEARTBEAT_TIMEOUT,
+ idleTimeout: this._IDLE_TIMEOUT,
+ restartPolicy: this._RESTART_POLICY,
+ a2aPollInterval: this._A2A_POLL_INTERVAL,
+ heartbeatCheck: this._HEARTBEAT_CHECK,
+ idleSweepInterval: this._IDLE_SWEEP_INTERVAL,
+ deadPurgeTimeout: this._DEAD_PURGE_TIMEOUT
+ }));
}
registerCommands() {
this.commands['orchestrate'] = (args) => this.delegateTask(args.join(' '));
@@ -39,7 +61,7 @@ class OrchestratorModule {
'orchestrator-spawn': 'orchestrator-spawn [role] [capabilities...] โ€” Manually spawn a new agent',
'orchestrator-kill': 'orchestrator-kill <agent-id> โ€” Terminate an agent and revoke leases',
'orchestrator-purge': 'orchestrator-purge โ€” Remove dead agents (>60s since heartbeat)',
- 'orchestrator-config': 'orchestrator-config <key> <value> โ€” Tune: spawn-limit, spawn-strategy, heartbeat-timeout, idle-timeout, restart-policy, a2a-polling',
+ 'orchestrator-config': 'orchestrator-config <key> <value> โ€” Tune: spawn-limit, spawn-strategy, heartbeat-timeout, idle-timeout, restart-policy, a2a-polling, a2a-poll-interval, heartbeat-check, idle-sweep-interval, dead-purge-timeout',
'orchestrator-list': 'orchestrator-list โ€” List all spawned agents with full details',
'orchestrator-sleep': 'orchestrator-sleep <agent-id> โ€” Suspend agent to VFS',
'orchestrator-wake': 'orchestrator-wake <agent-id> โ€” Restore agent from VFS sleep',
@@ -59,13 +81,39 @@ class OrchestratorModule {
this._reanimateFromVFS();
// A2A mailbox polling loop (500ms)
if (this._A2A_POLLING) {
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), 500);
+ this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
}
- // Heartbeat watchdog every 3 seconds
- this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), 3000);
- // Idle timeout sweep every 30s
- setInterval(() => this._sweepIdle(), 30000);
+ // Heartbeat watchdog
+ this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), this._HEARTBEAT_CHECK);
+ // Idle timeout sweep
+ this._idleSweepInterval = setInterval(() => this._sweepIdle(), this._IDLE_SWEEP_INTERVAL);
this.terminal.print('๐Ÿง  Orchestrator loaded โ€” A2A mailbox transport active', 'success');
+ // 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]) {
+ this._agents[msg.from].lastHeartbeat = Date.now();
+ }
+ if (msg.type === 'response' && msg.from && this._agents[msg.from]) {
+ this._agents[msg.from].status = 'idle';
+ this._agents[msg.from].currentTask = null;
+ }
+ });
+ }
+
+ // โ”€โ”€โ”€ Event Bus (in-memory pub/sub for agent communication) โ”€โ”€โ”€
+
+ _on(event, callback) {
+ if (!this._eventBus[event]) this._eventBus[event] = [];
+ this._eventBus[event].push(callback);
+ return () => { // Return unsubscribe function
+ this._eventBus[event] = this._eventBus[event].filter(cb => cb !== callback);
+ };
+ }
+
+ _emit(event, data) {
+ for (const cb of (this._eventBus[event] || [])) {
+ try { cb(data); } catch (e) { /* subscriber error */ }
+ }
}
// โ”€โ”€โ”€ A2A Mailbox Transport โ”€โ”€โ”€
@@ -104,6 +152,10 @@ class OrchestratorModule {
this._a2aMessages[agentId].push({ ...msg, _processed: false });
this.terminal.print(`๐Ÿ“จ A2A message sent to ${agentId} (${msg.type}, id: ${msg.msg_id})`, 'success');
+
+ // Emit via event bus for in-memory delivery (bypasses VFS polling)
+ this._emit('a2a:message', msg);
+ this._emit(`a2a:message:${agentId}`, msg);
}
listA2AMessages() {
@@ -178,8 +230,12 @@ class OrchestratorModule {
// โ”€โ”€โ”€ Agent Lifecycle โ”€โ”€โ”€
+ _sanitizeId(name) {
+ return (name || 'worker').replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 32) || 'worker';
+ }
spawnAgent(role, capabilities) {
- const id = `agent-${Object.keys(this._agents).length + 1}-${role || 'worker'}`;
+ const safeRole = this._sanitizeId(role);
+ const id = `agent-${Object.keys(this._agents).length + 1}-${safeRole}`;
if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) {
this.terminal.print(`โŒ Agent spawn limit (${this._SPAWN_LIMIT}) reached`, 'error');
return null;
@@ -343,13 +399,26 @@ class OrchestratorModule {
_deriveCapabilities(task) {
const caps = ['list_files', 'read_file'];
const lower = task.toLowerCase();
- if (lower.includes('search') || lower.includes('find') || lower.includes('look up') || lower.includes('web')) caps.push('web_search');
- if (lower.includes('write') || lower.includes('create') || lower.includes('save') || lower.includes('edit')) caps.push('write_file');
- if (lower.includes('delete') || lower.includes('remove') || lower.includes('rm ')) caps.push('delete_file');
- if (lower.includes('install') || lower.includes('package') || lower.includes('wpm')) caps.push('wpm_install');
- if (lower.includes('module') || lower.includes('view') || lower.includes('edit')) caps.push('view_module', 'edit_module');
- if (lower.includes('list') || lower.includes('status') || lower.includes('health')) caps.push('get_status');
- if (lower.includes('fetch') || lower.includes('url') || lower.includes('http')) caps.push('web_fetch');
+ const CAP_MAP = [
+ { patterns: ['search', 'find', 'research', 'lookup', 'query', 'look up'], caps: ['web_search'] },
+ { patterns: ['write', 'create', 'generate', 'compose', 'draft', 'save'], caps: ['write_file'] },
+ { patterns: ['delete', 'remove', 'trash', 'rm '], caps: ['delete_file'] },
+ { patterns: ['read', 'view', 'cat', 'show', 'display', 'inspect', 'preview'], caps: [] }, // already has read_file
+ { patterns: ['edit', 'modify', 'update', 'patch', 'change', 'fix'], caps: ['write_file'] },
+ { patterns: ['install', 'package', 'wpm', 'setup'], caps: ['wpm_install'] },
+ { patterns: ['module', 'plugin', 'ext'], caps: ['view_module', 'edit_module'] },
+ { patterns: ['list', 'status', 'health', 'audit', 'report', 'check'], caps: ['get_status'] },
+ { patterns: ['fetch', 'url', 'http', 'scrape'], caps: ['web_fetch'] },
+ { patterns: ['code', 'program', 'script', 'build', 'develop', 'compile'], caps: ['list_files', 'read_file', 'write_file'] },
+ { patterns: ['analyze', 'analyse', 'summarize', 'review', 'investigate'], caps: ['web_search', 'list_files', 'read_file'] },
+ { patterns: ['deploy', 'publish', 'upload'], caps: ['write_file', 'webdav_upload'] },
+ { patterns: ['monitor', 'watch', 'poll', 'track', 'observe'], caps: ['get_status'] },
+ ];
+ for (const entry of CAP_MAP) {
+ if (entry.patterns.some(p => lower.includes(p))) {
+ entry.caps.forEach(c => caps.push(c));
+ }
+ }
return [...new Set(caps)];
}
@@ -381,13 +450,17 @@ class OrchestratorModule {
'a2a-polling': () => {
this._A2A_POLLING = val === 'on' || val === 'true';
if (this._A2A_POLLING && !this._a2aPollTimer) {
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), 500);
+ this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
} else if (!this._A2A_POLLING && this._a2aPollTimer) {
clearInterval(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`; },
+ '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`; }
};
if (!configMap[key]) {
@@ -395,6 +468,7 @@ class OrchestratorModule {
}
const result = configMap[key]();
this.terminal.print(`โœ… ${result}`, 'success');
+ this._saveConfig();
}
// โ”€โ”€โ”€ Task Delegation โ”€โ”€โ”€
@@ -522,7 +596,7 @@ class OrchestratorModule {
const now = Date.now();
let count = 0;
Object.entries(this._agents).forEach(([id, agent]) => {
- if (now - agent.lastHeartbeat > 60000) {
+ if (now - agent.lastHeartbeat > this._DEAD_PURGE_TIMEOUT) {
this.killAgent(id);
count++;
}
diff --git a/modules/core/module-sandbox.js b/modules/core/module-sandbox.js
index dc8dc76..67b8357 100644
--- a/modules/core/module-sandbox.js
+++ b/modules/core/module-sandbox.js
@@ -33,7 +33,7 @@ class SandboxModule {
init(terminal) {
this.terminal = terminal;
- this.workerUrl = '/spaces/newos4/modules/core/sandbox-worker.js';
+ this.workerUrl = '/spaces/newos4/modules/core/sandbox-worker.js?v=2026-06-05-v4.4.0';
// Detect Worker availability
if (typeof Worker === 'undefined') {
@@ -41,6 +41,19 @@ class SandboxModule {
this.terminal.print('โš ๏ธ WebWorkers not available โ€” sandbox using inline fallback', 'warning');
} else {
this._initPool();
+ // Deferred check: workers may fail asynchronously (headless browsers)
+ // If no worker proves alive within 1.5s, switch to inline fallback
+ this._workerCheckTimer = setTimeout(() => {
+ if (!this._fallbackMode && this.workers.length > 0) {
+ const alive = this.workers.some(w => !w._failed && this._workerReady[w._sandboxId]);
+ if (!alive) {
+ this._fallbackMode = true;
+ this.workers.forEach(w => w.terminate());
+ this.workers = [];
+ this.terminal.print('โš ๏ธ WebWorkers unavailable โ€” switching to inline fallback', 'warning');
+ }
+ }
+ }, 1500);
}
this.terminal.print('๐Ÿงช Sandbox module loaded', 'info');
@@ -49,44 +62,65 @@ class SandboxModule {
_initPool() {
// Start with MIN_WORKERS, grow on demand up to MAX_WORKERS
for (let i = 0; i < this.MIN_WORKERS; i++) {
- this._spawnWorker();
+ const w = this._spawnWorker();
+ if (!w) break; // first failure kills the pool
}
}
_spawnWorker() {
if (this.workers.length >= this.MAX_WORKERS) return null;
+ if (this._fallbackMode) return null;
try {
const worker = new Worker(this.workerUrl);
const id = `w${this.workers.length + 1}`;
worker._sandboxId = id;
worker._busy = false;
+ worker._failed = false;
- worker.onmessage = (e) => this._handleResult(e.data, id);
+ // Detect immediate worker failure (headless browsers, restricted contexts)
+ const failTimer = setTimeout(() => {
+ if (!worker._failed) {
+ // Worker survived 2 seconds - likely functional
+ }
+ }, 2000);
+
+ worker.onmessage = (e) => {
+ clearTimeout(failTimer);
+ if (e.data && e.data.type === 'ready') {
+ this._workerReady[id] = true;
+ } else {
+ this._handleResult(e.data, id);
+ }
+ };
worker.onerror = (e) => {
- console.error(`[Sandbox] Worker ${id} error:`, e.message);
- this._totalErrors++;
- this._replaceWorker(id);
+ clearTimeout(failTimer);
+ worker._failed = true;
+ worker.terminate();
+ console.warn(`[Sandbox] Worker ${id} failed (headless env) โ€” switching to inline`);
+ this._fallbackMode = true;
+ this.workers.forEach(w => w.terminate());
+ this.workers = [];
+ this.terminal.print('โš ๏ธ WebWorkers unavailable in this environment โ€” using inline fallback', 'warning');
};
this.workers.push(worker);
- this._workerReady[id] = true;
return worker;
} catch (e) {
- console.error('[Sandbox] Failed to spawn worker:', e);
+ console.warn('[Sandbox] Failed to spawn worker:', e);
this._fallbackMode = true;
return null;
}
}
_replaceWorker(id) {
+ // In headless environments, workers don't work โ€” don't retry
+ if (this._fallbackMode) return;
const idx = this.workers.findIndex(w => w._sandboxId === id);
if (idx !== -1) {
this.workers[idx].terminate();
this.workers.splice(idx, 1);
delete this._workerReady[id];
}
- // Re-spawn after a delay
- setTimeout(() => this._spawnWorker(), 1000);
}
_getIdleWorker() {
@@ -104,7 +138,13 @@ class SandboxModule {
async runInSandbox(task, capabilities) {
if (!task) return this.terminal.print('Usage: sandbox-run <task>', 'error');
- if (this._fallbackMode) {
+ // Check if any worker is actually alive and ready
+ const hasAliveWorker = this.workers.some(w =>
+ !w._failed && this._workerReady[w._sandboxId]
+ );
+ const useInline = this._fallbackMode || this.workers.length === 0 || !hasAliveWorker;
+
+ if (useInline) {
return this._runInline(task, capabilities);
}
diff --git a/modules/core/module-simplegui.js b/modules/core/module-simplegui.js
index 78fa9b5..09d172a 100644
--- a/modules/core/module-simplegui.js
+++ b/modules/core/module-simplegui.js
@@ -29,8 +29,9 @@
}
const label = args[0], command = args.slice(1).join(' ');
const id = 'gui-btn-' + Date.now();
+ const safeCmd = command.replace(/'/g, "\\'");
this.terminal.printHTML(`<div style="margin: 5px 0;">
- <button class="qa-btn" onclick="${command}" id="${id}">${label}</button>
+ <button class="qa-btn" onclick="(window.webx11||window).executeCommand('${safeCmd}')" id="${id}">${label}</button>
</div>`);
this.guiElements[id] = { type: 'button', label, command };
this.terminal.print(`Button '${label}' created`, "success");
diff --git a/modules/core/module-skills.js b/modules/core/module-skills.js
deleted file mode 100644
index 9d0adec..0000000
--- a/modules/core/module-skills.js
+++ /dev/null
@@ -1,295 +0,0 @@
-// Extracted from newOS4 index.html โ€” SkillsModule (v4.2 โ€” Self-improving AI)
- class SkillsModule {
- constructor() {
- this.commands = {};
- this.terminal = null;
- this._skillIndexCache = null;
- this._skillIndexDirty = false;
- this._skillIndexFlushTimer = null;
- }
- _readSkillIndex() {
- if (this._skillIndexCache !== null) return this._skillIndexCache;
- const fs = this.terminal.fs;
- this._skillIndexCache = fs.fsReadJSON('/skills/index.json') || {};
- return this._skillIndexCache;
- }
- _markSkillIndexDirty() {
- this._skillIndexDirty = true;
- if (this._skillIndexFlushTimer) clearTimeout(this._skillIndexFlushTimer);
- this._skillIndexFlushTimer = setTimeout(() => this._flushSkillIndex(), 2000);
- }
- _flushSkillIndex() {
- if (!this._skillIndexDirty || this._skillIndexCache === null) return;
- const fs = this.terminal.fs;
- fs.fsWriteJSON('/skills/index.json', this._skillIndexCache);
- this._skillIndexDirty = false;
- this._skillIndexFlushTimer = null;
- }
- getHelpInfo() {
- return {
- 'skill-list': 'skill-list โ€” List all installed skills',
- 'skill-view': 'skill-view <name> โ€” View the content of a skill',
- 'skill-create': 'skill-create <name> [desc] [category] [tags] โ€” Create a new skill',
- 'skill-patch': 'skill-patch <name> [content...] โ€” Update skill content (no args to view current)',
- 'skill-delete': 'skill-delete <name> โ€” Delete a skill',
- 'skill-export': 'skill-export <name> โ€” Export a skill to WebDAV',
- 'skill-import': 'skill-import <name> โ€” Import a skill from WebDAV',
- 'skill-sync': 'skill-sync โ€” Bidirectional sync all skills with WebDAV',
- };
- }
- ensureSkillsDir() {
- if (this.terminal && this.terminal.fs) {
- const fs = this.terminal.fs;
- // Ensure /skills/ directory exists
- if (!fs.root.content['skills']) {
- fs.root.content['skills'] = { type: 'dir', content: {} };
- fs.saveToStorage();
- }
- }
- }
- registerCommands() {
- this.commands['skill-list'] = () => this.listSkills();
- this.commands['skill-view'] = (args) => this.viewSkill(args);
- this.commands['skill-create'] = (args) => this.createSkill(args);
- this.commands['skill-patch'] = (args) => this.patchSkill(args);
- this.commands['skill-delete'] = (args) => this.deleteSkill(args);
- this.commands['skill-export'] = (args) => this.exportSkill(args);
- this.commands['skill-import'] = (args) => this.importSkill(args);
- this.commands['skill-sync'] = () => this.syncSkills();
- }
- listSkills() {
- this.ensureSkillsDir();
- const index = this._readSkillIndex();
- const names = Object.keys(index);
- if (names.length === 0) {
- this.terminal.print('No skills found.', 'info');
- return;
- }
- this.terminal.print(`=== Skills (${names.length}) ===`, 'info');
- names.forEach(name => {
- const s = index[name];
- const tags = (s.tags || []).join(', ');
- this.terminal.print(`๐Ÿ“˜ ${name} โ€” ${s.description} [${s.category}] ${tags ? '(' + tags + ')' : ''}`);
- });
- }
- viewSkill(args) {
- const name = (args[0] || '').replace(/\.md$/, '');
- if (!name) {
- this.terminal.print('Usage: skill-view <name>', 'warning');
- return;
- }
- const content = this.terminal.fs.readFile(`/skills/${name}.md`);
- if (!content) {
- this.terminal.print(`Skill '${name}' not found`, 'error');
- return;
- }
- this.terminal.print(`=== ${name} ===`, 'info');
- this.terminal.print(content, 'info');
- }
- createSkill(args) {
- this.ensureSkillsDir();
- const name = args[0];
- if (!name) {
- this.terminal.print('Usage: skill-create <name> [description] [category] [tag1,tag2,...]', 'warning');
- return;
- }
- const description = args[1] || 'No description';
- const category = args[2] || 'general';
- const tags = (args[3] || '').split(',').filter(Boolean);
- const fs = this.terminal.fs;
- if (fs.readFile(`/skills/${name}.md`)) {
- this.terminal.print(`Skill '${name}' already exists. Use skill-patch to modify it.`, 'warning');
- return;
- }
- const content = '---\nname: ' + name + '\ndescription: ' + description + '\ntags: [' + tags.join(', ') + ']\ncategory: ' + category + '\n---\n## Steps\n1. (to be filled)\n';
- fs.writeFile(`/skills/${name}.md`, content);
- const index = this._readSkillIndex();
- index[name] = { name, description, category, tags };
- this._markSkillIndexDirty();
- this.terminal.print(`Skill '${name}' created. Edit its content with skill-patch ${name}`, 'success');
- }
- patchSkill(args) {
- const name = args[0];
- if (!name) {
- this.terminal.print('Usage: skill-patch <name> [content...]', 'warning');
- return;
- }
- const fs = this.terminal.fs;
- const existing = fs.readFile(`/skills/${name}.md`);
- if (!existing) {
- this.terminal.print(`Skill '${name}' not found. Use skill-create first.`, 'error');
- return;
- }
- if (args.length < 2) {
- // No new content โ€” show the skill so user can copy-paste
- this.terminal.print(`=== ${name} (current content) ===`, 'info');
- this.terminal.print(existing, 'info');
- this.terminal.print('---', 'info');
- this.terminal.print('Pass new content as additional arguments: skill-patch <name> <full content>', 'warning');
- return;
- }
- const newContent = args.slice(1).join(' ');
- fs.writeFile(`/skills/${name}.md`, newContent);
- this.terminal.print(`Skill '${name}' patched`, 'success');
- }
- deleteSkill(args) {
- const name = (args[0] || '').replace(/\.md$/, '');
- if (!name) {
- this.terminal.print('Usage: skill-delete <name>', 'warning');
- return;
- }
- const fs = this.terminal.fs;
- if (!fs.readFile(`/skills/${name}.md`)) {
- this.terminal.print(`Skill '${name}' not found`, 'error');
- return;
- }
- fs.writeFile(`/skills/${name}.md`, '');
- const index = this._readSkillIndex();
- delete index[name];
- this._markSkillIndexDirty();
- this.terminal.print(`Skill '${name}' deleted`, 'success');
- }
- getWebDAVModule() {
- return this.terminal && this.terminal.moduleManager.modules['WebDAVModule'];
- }
- async exportSkill(args) {
- const name = (args[0] || '').replace(/\.md$/, '');
- if (!name) {
- this.terminal.print('Usage: skill-export <name>', 'warning');
- return;
- }
- const wd = this.getWebDAVModule();
- if (!wd || !wd.config || !wd.config.url) {
- this.terminal.print('WebDAV not configured. Use webdav-config first.', 'warning');
- return;
- }
- this.ensureSkillsDir();
- const fs = this.terminal.fs;
- const content = fs.readFile(`/skills/${name}.md`);
- if (!content) {
- this.terminal.print(`Skill '${name}' not found in VFS`, 'error');
- return;
- }
- const index = this._readSkillIndex();
- const meta = index[name];
- try {
- // Ensure remote skills dir
- if (typeof wd._ensureDir === 'function') await wd._ensureDir('skills');
- // Push the skill file
- const resp1 = await wd._webdavRequest('PUT', 'skills/' + name + '.md', {
- body: content,
- headers: { 'Content-Type': 'text/markdown' }
- });
- if (!resp1.ok && resp1.status !== 201 && resp1.status !== 204) {
- throw new Error('Upload failed: HTTP ' + resp1.status);
- }
- // Push updated index
- const indexResp = await wd._webdavRequest('PUT', 'skills/index.json', {
- body: JSON.stringify(index, null, 2),
- headers: { 'Content-Type': 'application/json' }
- });
- if (indexResp.ok || indexResp.status === 201 || indexResp.status === 204) {
- this.terminal.print(`Skill '${name}' exported to WebDAV skills/`, 'success');
- }
- } catch (e) {
- this.terminal.print('Export failed: ' + e.message, 'error');
- }
- }
- async importSkill(args) {
- const name = (args[0] || '').replace(/\.md$/, '');
- if (!name) {
- this.terminal.print('Usage: skill-import <name>', 'warning');
- return;
- }
- const wd = this.getWebDAVModule();
- if (!wd || !wd.config || !wd.config.url) {
- this.terminal.print('WebDAV not configured. Use webdav-config first.', 'warning');
- return;
- }
- try {
- const resp = await wd._webdavRequest('GET', 'skills/' + name + '.md');
- if (!resp.ok) throw new Error('HTTP ' + resp.status);
- const content = await resp.text();
- this.ensureSkillsDir();
- const fs = this.terminal.fs;
- fs.writeFile(`/skills/${name}.md`, content);
- // Try to pull the index to get metadata; if fails, create minimal entry
- let meta = { name, description: 'Imported from WebDAV', category: 'general', tags: [] };
- try {
- const indexResp = await wd._webdavRequest('GET', 'skills/index.json');
- if (indexResp.ok) {
- const remoteIndex = JSON.parse(await indexResp.text());
- if (remoteIndex && remoteIndex[name]) {
- meta = remoteIndex[name];
- }
- }
- } catch (e) { /* use default meta */ }
- const index = this._readSkillIndex();
- index[name] = meta;
- this._markSkillIndexDirty();
- this.terminal.print(`Skill '${name}' imported from WebDAV (${content.length} bytes)`, 'success');
- } catch (e) {
- this.terminal.print('Import failed: ' + e.message, 'error');
- }
- }
- async syncSkills() {
- const wd = this.getWebDAVModule();
- if (!wd || !wd.config || !wd.config.url) {
- this.terminal.print('WebDAV not configured. Use webdav-config first.', 'warning');
- return;
- }
- this.ensureSkillsDir();
- const fs = this.terminal.fs;
- const localIndex = this._readSkillIndex();
- const localNames = new Set(Object.keys(localIndex));
- this.terminal.print('Syncing skills with WebDAV...', 'info');
- let imported = 0, exported = 0;
- try {
- // Try to get remote index
- let remoteIndex = {};
- try {
- const resp = await wd._webdavRequest('GET', 'skills/index.json');
- if (resp.ok) {
- remoteIndex = JSON.parse(await resp.text()) || {};
- }
- } catch (e) { /* no remote index yet */ }
- const remoteNames = new Set(Object.keys(remoteIndex));
- // Export local-only skills to remote
- for (const name of localNames) {
- if (!remoteNames.has(name)) {
- const content = fs.readFile(`/skills/${name}.md`);
- if (content) {
- await wd._ensureDir('skills');
- await wd._webdavRequest('PUT', 'skills/' + name + '.md', {
- body: content,
- headers: { 'Content-Type': 'text/markdown' }
- });
- exported++;
- }
- }
- }
- // Import remote-only skills to local
- for (const name of remoteNames) {
- if (!localNames.has(name)) {
- const resp = await wd._webdavRequest('GET', 'skills/' + name + '.md');
- if (resp.ok) {
- const content = await resp.text();
- fs.writeFile(`/skills/${name}.md`, content);
- localIndex[name] = remoteIndex[name];
- imported++;
- }
- }
- }
- // Push updated local index back to remote
- this._markSkillIndexDirty();
- await wd._ensureDir('skills');
- await wd._webdavRequest('PUT', 'skills/index.json', {
- body: JSON.stringify(localIndex, null, 2),
- headers: { 'Content-Type': 'application/json' }
- });
- this.terminal.print(`Sync complete: ${exported} exported, ${imported} imported`, 'success');
- } catch (e) {
- this.terminal.print('Sync failed: ' + e.message, 'error');
- }
- }
- }
diff --git a/modules/core/module-system.js b/modules/core/module-system.js
index c0dbe14..c2115c1 100644
--- a/modules/core/module-system.js
+++ b/modules/core/module-system.js
@@ -44,7 +44,7 @@
}
}, 500);
this.commands['status'] = () => {
- this.terminal.print(`System: WebX11 v4.2`);
+ this.terminal.print(`System: WebX11 ${window.NEWOS4_VERSION || 'v4.0'}`);
this.terminal.print(`Network: ${navigator.onLine ? 'Online' : 'Offline'}`);
this.terminal.print(`Files: ${this.terminal.fs.ls().length} items`);
this.terminal.print(`Modules: ${Object.keys(this.terminal.moduleManager.modules).length} loaded`);
@@ -134,6 +134,55 @@
this.terminal.print(`Font size set to ${val}px (saved) ๐Ÿ‘†`, 'success');
};
this.commands['setfont'] = this.commands['font-size'];
+
+ // Checkpoint commands (delegated to virtual-fs.js)
+ this.commands['checkpoint'] = (args) => {
+ try {
+ const name = args[0] || '';
+ this.terminal.fs.checkpoint(name);
+ const displayName = name || new Date().toISOString();
+ this.terminal.print('โœ… Checkpoint created: ' + displayName, 'success');
+ } catch (e) {
+ this.terminal.print('Checkpoint failed: ' + e.message, 'error');
+ }
+ };
+ this.commands['checkpoint-list'] = () => {
+ try {
+ const list = this.terminal.fs.listCheckpoints();
+ if (!list || list.length === 0) {
+ this.terminal.print('No checkpoints found', 'info');
+ return;
+ }
+ this.terminal.print('๐Ÿ“‹ Checkpoints:', 'text-muted');
+ list.forEach(c => this.terminal.print(' โ€ข ' + c));
+ } catch (e) {
+ this.terminal.print('checkpoint-list failed: ' + e.message, 'error');
+ }
+ };
+ this.commands['checkpoint-del'] = (args) => {
+ if (!args[0]) return this.terminal.print('Usage: checkpoint-del <name>', 'warning');
+ try {
+ if (this.terminal.fs.deleteCheckpoint(args[0])) {
+ this.terminal.print('Deleted checkpoint: ' + args[0], 'success');
+ } else {
+ this.terminal.print('Checkpoint not found: ' + args[0], 'error');
+ }
+ } catch (e) {
+ this.terminal.print('checkpoint-del failed: ' + e.message, 'error');
+ }
+ };
+ this.commands['rollback'] = (args) => {
+ if (!args[0]) return this.terminal.print('Usage: rollback <name>', 'warning');
+ try {
+ if (this.terminal.fs.rollback(args[0])) {
+ this.terminal.print('โ†ป Rolled back to checkpoint: ' + args[0], 'success');
+ } else {
+ this.terminal.print('Rollback failed: checkpoint not found or invalid', 'error');
+ }
+ } catch (e) {
+ this.terminal.print('Rollback failed: ' + e.message, 'error');
+ }
+ };
}
_autoRotateLogs(force = false) {
try {
@@ -253,7 +302,7 @@
skills:'๐Ÿ“š Skills', config:'โš™๏ธ Config', webx:'๐ŸŒ WebX', data:'๐Ÿ’พ Data',
ext:'๐Ÿงฐ Extras', gui:'๐Ÿ–ฅ๏ธ GUI', vault:'๐Ÿ”’ Vault'
};
- let result = 'โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—\nโ•‘ ๐Ÿ”ท WebX11 v4.2 โ€” All Commands โ•‘\nโ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n\n';
+ let result = 'โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—\nโ•‘ ๐Ÿ”ท WebX11 ' + (window.NEWOS4_VERSION || 'v4.0') + ' โ€” All Commands โ•‘\nโ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n\n';
const total = Object.keys(helpMap).length;
result += `๐Ÿ“Š ${total} commands loaded\n\n`;
for (const [key, cmds] of Object.entries(cats)) {
diff --git a/modules/core/module-world-model.js b/modules/core/module-world-model.js
index b7bb28f..e8e3405 100644
--- a/modules/core/module-world-model.js
+++ b/modules/core/module-world-model.js
@@ -33,13 +33,21 @@ class WorldModelModule {
init(terminal) {
this.terminal = terminal;
this._loadFromVFS();
+ // Deferred initial synthesis โ€” wait for ext modules (SkillPackCore etc) to fully load
+ setTimeout(() => {
+ this._synthesize();
+ this.terminal.print('๐ŸŒ World Model synthesized โ€” system self-awareness active', 'info');
+ // Second pass after ext modules definitely finished loading
+ setTimeout(() => {
+ this._synthesize();
+ this.terminal.print('๐ŸŒ World Model refresh โ€” full module load confirmed', 'info');
+ }, 7000);
+ }, 5000);
// Auto-refresh on a timer
this._refreshTimer = setInterval(() => {
const age = Date.now() - (this._lastRefresh ? new Date(this._lastRefresh).getTime() : 0);
if (age > this._REFRESH_INTERVAL) this._synthesize();
}, this._REFRESH_INTERVAL);
- // Initial synthesis
- this._synthesize();
this.terminal.print('๐ŸŒ World Model loaded โ€” system self-awareness active', 'info');
}
@@ -50,15 +58,22 @@ class WorldModelModule {
const fs = this.terminal.fs;
if (!fs) return;
+ // Derive version from actual system state โ€” no hardcoded strings
+ const mInfo = this._gatherModuleInfo();
+ const agentInfo = this._gatherAgentInfo();
+ const fileInfo = this._gatherFileInfo(fs);
+ const skillInfo = this._gatherSkillInfo(fs);
+ const liveVersion = this._deriveVersion(mInfo);
+
const model = {
- version: 'v4.4',
+ version: liveVersion,
synthesized: new Date().toISOString(),
- system: this._gatherSystemInfo(fs),
- modules: this._gatherModuleInfo(),
- files: this._gatherFileInfo(fs),
- skills: this._gatherSkillInfo(fs),
+ system: this._gatherSystemInfo(fs, mInfo.loaded, liveVersion),
+ modules: mInfo,
+ files: fileInfo,
+ skills: skillInfo,
memory: this._gatherMemoryInfo(),
- agents: this._gatherAgentInfo(),
+ agents: agentInfo,
telemetry: this._gatherTelemetry(fs),
projects: this._gatherProjects(fs),
recent: this._gatherRecent(fs)
@@ -78,11 +93,30 @@ class WorldModelModule {
}
}
- _gatherSystemInfo(fs) {
+ _deriveVersion(mInfo) {
+ // Single source of truth: the constant set at bootstrap
+ if (window.NEWOS4_VERSION) return window.NEWOS4_VERSION;
+ // No fallback version string โ€” if the constant is missing, report unknown
+ return 'v?';
+ }
+
+ _getTotalModules() {
+ 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 + 5; // +5 for expected unloaded
+ } catch (e) {
+ return Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0;
+ }
+ }
+
+ _gatherSystemInfo(fs, moduleCount, version) {
const healthData = fs.fsReadJSON('/system/metrics/health.json');
return {
- version: 'newOS4 v4.4',
- modules: Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0,
+ version: `newOS4 ${version}`,
+ modules: moduleCount || Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0,
vfsSize: JSON.stringify(fs?.root || {}).length,
online: typeof navigator !== 'undefined' ? navigator.onLine : true,
memory: typeof performance !== 'undefined' && performance.memory
@@ -109,7 +143,7 @@ class WorldModelModule {
: [];
return {
loaded: modules.length,
- total: 49,
+ total: this._getTotalModules(),
modules,
modified,
modifiedCount: modified.length
diff --git a/modules/core/sandbox-worker.js b/modules/core/sandbox-worker.js
index 5841582..696c510 100644
--- a/modules/core/sandbox-worker.js
+++ b/modules/core/sandbox-worker.js
@@ -5,11 +5,16 @@
let _vfs = null;
let _apiProxyOrigin = null;
-self.onmessage = async (e) => {
- const { taskId, task, capabilities, vfsSnapshot, allowedCommands, apiProxy } = e.data;
- _apiProxyOrigin = apiProxy ? new URL(apiProxy).origin : null;
+// Signal ready immediately
+self.postMessage({ type: 'ready' });
- const startTime = performance.now();
+self.addEventListener('message', async (e) => {
+ const { taskId, task, capabilities, vfsSnapshot, allowedCommands, apiProxy } = e.data || {};
+
+ // Must have a taskId to respond to
+ if (!taskId) return;
+
+ const startTime = self.performance.now();
let toolCalls = 0;
let result = '';
let error = null;
@@ -17,18 +22,13 @@ self.onmessage = async (e) => {
try {
// Rebuild VFS from snapshot
_vfs = vfsSnapshot ? JSON.parse(vfsSnapshot) : { root: { type: 'dir', content: {} }, currentPath: [] };
-
- // Log start
- console.log(`[Sandbox ${taskId}] Starting task: "${task.substring(0, 60)}"`);
+ _apiProxyOrigin = apiProxy ? new URL(apiProxy).origin : null;
// Parse the task for tool use
- const toolResults = {};
+ const parts = (task || '').trim().split(/\s+/);
+ const cmd = parts.length > 0 ? parts[0].toLowerCase() : '';
const commandWhitelist = allowedCommands || ['ls', 'cat', 'echo', 'find', 'grep', 'help', 'status'];
- // Execute basic commands
- const parts = task.trim().split(/\s+/);
- const cmd = parts[0].toLowerCase();
-
if (cmd === 'ls' || cmd === 'list') {
result = sandbox_ls(parts[1] || '/');
toolCalls++;
@@ -50,117 +50,115 @@ self.onmessage = async (e) => {
} else if (cmd === 'status') {
result = sandbox_status();
toolCalls++;
- } else if (capabilities.includes('run_command') && commandWhitelist.includes(cmd)) {
+ } else if (capabilities && capabilities.includes('run_command') && commandWhitelist.includes(cmd)) {
result = sandbox_run(cmd, parts.slice(1));
toolCalls++;
} else {
// If the task is descriptive (not a command), run smart handling
- if (capabilities.includes('list_files') || capabilities.includes('read_file')) {
- // Smart mode: interpret task
- const lower = task.toLowerCase();
+ if (capabilities && (capabilities.includes('list_files') || capabilities.includes('read_file'))) {
+ const lower = (task || '').toLowerCase();
if (lower.includes('list') || lower.includes('file')) {
const dir = parts.find(p => p.startsWith('/') || p.startsWith('.')) || '/';
result = sandbox_ls(dir);
} else if (lower.includes('find') || lower.includes('search')) {
- const q = task.replace(/find|search/i, '').trim();
+ const q = (task || '').replace(/find|search/i, '').trim();
result = sandbox_find(q || '');
} else if (lower.includes('count') || lower.includes('how many')) {
result = sandbox_ls('/');
const count = (result.match(/\[file\]|\[dir\]/g) || []).length;
- result = `Found ${count} items in total.\\n${result}`;
+ result = 'Found ' + count + ' items in total.\n' + result;
} else if (lower.includes('status') || lower.includes('health')) {
result = sandbox_status();
} else {
- result = `Task interpreted: "${task}"\\nCapabilities: ${capabilities.join(', ')}\\nTip: Be more specific (e.g. "list files in /")`;
+ result = 'Task interpreted: "' + (task || '') + '"\nCapabilities: ' + (capabilities || []).join(', ') + '\nTip: Be more specific (e.g. "list files in /")';
}
toolCalls++;
} else {
- result = `Task: "${task}"\\nNo matching capability for command "${cmd}"`;
+ result = 'Task: "' + (task || '') + '"\nNo matching capability for command "' + cmd + '"';
}
}
} catch (e) {
- error = e.message;
- console.error(`[Sandbox ${taskId}] Error:`, e);
+ error = e.message || String(e);
}
- const duration = ((performance.now() - startTime) / 1000).toFixed(2);
+ const duration = ((self.performance.now() - startTime) / 1000).toFixed(2);
self.postMessage({
- taskId,
- result,
- error,
- toolCalls,
+ taskId: taskId,
+ result: result,
+ error: error,
+ toolCalls: toolCalls,
duration: parseFloat(duration),
vfsSnapshot: JSON.stringify(_vfs)
});
-};
+});
// โ”€โ”€โ”€ Sandboxed VFS Operations โ”€โ”€โ”€
function sandbox_ls(path) {
path = path || '/';
const entries = _vfs_get_entries(path);
- if (!entries) return `ls: ${path}: No such directory`;
- const lines = entries.map(e => {
+ if (!entries) return 'ls: ' + path + ': No such directory';
+ const lines = entries.map(function(e) {
const icon = e.type === 'dir' ? '[dir]' : '[file]';
- const size = e.size !== undefined ? ` (${e.size}B)` : '';
- return `${icon} ${e.name}${size}`;
+ const size = e.size !== undefined ? ' (' + e.size + 'B)' : '';
+ return icon + ' ' + e.name + size;
});
- return lines.join('\\n') || '(empty)';
+ return lines.join('\n') || '(empty)';
}
function sandbox_cat(path) {
const content = _vfs_get_content(path);
- if (content === null) return `cat: ${path}: No such file`;
- if (typeof content === 'object') return `cat: ${path}: Is a directory`;
- if (content.length > 8000) return content.substring(0, 8000) + '\\n... (truncated at 8KB)';
+ if (content === null) return 'cat: ' + path + ': No such file';
+ if (typeof content === 'object') return 'cat: ' + path + ': Is a directory';
+ if (content.length > 8000) return content.substring(0, 8000) + '\n... (truncated at 8KB)';
return content;
}
function sandbox_find(query) {
const results = [];
const q = query ? query.toLowerCase() : '';
- _vfs_walk('/', (path, node) => {
+ _vfs_walk('/', function(path, node) {
if (node.type === 'file') {
if (!q || path.toLowerCase().includes(q)) {
results.push(path);
}
}
});
- if (results.length === 0) return `No files found${query ? ` matching "${query}"` : ''}`;
- return results.join('\\n');
+ if (results.length === 0) return 'No files found' + (query ? ' matching "' + query + '"' : '');
+ return results.join('\n');
}
function sandbox_grep(query) {
if (!query) return 'grep: missing pattern';
const q = query.toLowerCase();
const results = [];
- _vfs_walk('/', (path, node) => {
+ _vfs_walk('/', function(path, node) {
if (node.type === 'file' && typeof node.content === 'string') {
- const lines = node.content.split('\\n');
+ const lines = node.content.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].toLowerCase().includes(q)) {
- results.push(`${path}:${i + 1}: ${lines[i].trim().substring(0, 100)}`);
+ results.push(path + ':' + (i + 1) + ': ' + lines[i].trim().substring(0, 100));
}
}
}
});
- if (results.length === 0) return `grep: no matches for "${query}"`;
- return results.slice(0, 50).join('\\n') + (results.length > 50 ? `\\n... (+${results.length - 50} more)` : '');
+ if (results.length === 0) return 'grep: no matches for "' + query + '"';
+ return results.slice(0, 50).join('\n') + (results.length > 50 ? '\n... (+' + (results.length - 50) + ' more)' : '');
}
function sandbox_status() {
let fileCount = 0, dirCount = 0;
- _vfs_walk('/', (path, node) => {
+ _vfs_walk('/', function(path, node) {
if (node.type === 'file') fileCount++;
else dirCount++;
});
const vfsSize = JSON.stringify(_vfs).length;
- return `Sandbox VFS Status:\\n Files: ${fileCount}\\n Directories: ${dirCount}\\n VFS Size: ${(vfsSize / 1024).toFixed(1)}KB`;
+ return 'Sandbox VFS Status:\n Files: ' + fileCount + '\n Directories: ' + dirCount + '\n VFS Size: ' + (vfsSize / 1024).toFixed(1) + 'KB';
}
function sandbox_run(cmd, args) {
- return `[sandbox] ${cmd} ${args.join(' ')}\\n(Command execution limited in sandbox mode)`;
+ return '[sandbox] ' + cmd + ' ' + args.join(' ') + '\n(Command execution limited in sandbox mode)';
}
// โ”€โ”€โ”€ VFS Helpers โ”€โ”€โ”€
@@ -168,11 +166,15 @@ function sandbox_run(cmd, args) {
function _vfs_get_entries(path) {
const dir = _vfs_resolve(path);
if (!dir || dir.type !== 'dir') return null;
- return Object.entries(dir.content || {}).map(([name, node]) => ({
- name,
- type: node.type || 'file',
- size: typeof node.content === 'string' ? node.content.length : undefined
- })).sort((a, b) => a.name.localeCompare(b.name));
+ return Object.entries(dir.content || {}).map(function(entry) {
+ const name = entry[0];
+ const node = entry[1];
+ return {
+ name: name,
+ type: node.type || 'file',
+ size: typeof node.content === 'string' ? node.content.length : undefined
+ };
+ }).sort(function(a, b) { return a.name.localeCompare(b.name); });
}
function _vfs_get_content(path) {
@@ -184,11 +186,11 @@ function _vfs_get_content(path) {
function _vfs_resolve(path) {
if (!_vfs || !_vfs.root) return null;
if (path === '/' || path === '') return _vfs.root;
- const parts = path.split('/').filter(p => p);
+ const parts = path.split('/').filter(function(p) { return p; });
let current = _vfs.root;
- for (const part of parts) {
- if (!current.content || !current.content[part]) return null;
- current = current.content[part];
+ for (let i = 0; i < parts.length; i++) {
+ if (!current.content || !current.content[parts[i]]) return null;
+ current = current.content[parts[i]];
}
return current;
}
@@ -198,8 +200,9 @@ function _vfs_walk(path, cb) {
if (!node) return;
cb(path, node);
if (node.type === 'dir' && node.content) {
- for (const [name, child] of Object.entries(node.content)) {
- _vfs_walk(path === '/' ? '/' + name : path + '/' + name, cb);
+ const names = Object.keys(node.content);
+ for (let i = 0; i < names.length; i++) {
+ _vfs_walk(path === '/' ? '/' + names[i] : path + '/' + names[i], cb);
}
}
} \ No newline at end of file
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index d916ae6..be57c05 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -11,10 +11,15 @@
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']);
+ // 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._registerBuiltIn();
}
_registerBuiltIn() {
- const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor'];
+ 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'];
internals.forEach(c => this.registerCommand(c, { category: 'builtin' }));
}
setLevel(n) {
@@ -73,8 +78,25 @@
}
return best ? { cmd: best, dist: bestDist } : null;
}
+ _parseArgs(input) {
+ const args = [];
+ let current = '';
+ let inDouble = false;
+ let inSingle = false;
+ for (const ch of input) {
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
+ if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
+ if (ch === ' ' && !inDouble && !inSingle) {
+ if (current) { args.push(current); current = ''; }
+ } else {
+ current += ch;
+ }
+ }
+ if (current) args.push(current);
+ return args;
+ }
_classify(raw) {
- const parts = raw.trim().split(/\s+/);
+ const parts = this._parseArgs(raw.trim());
const cmd = parts[0].toLowerCase();
const args = parts.slice(1);
// Internal commands always allowed at terminal level, but security still classifies
@@ -199,12 +221,23 @@
this.moduleManager = new ModuleManager(this);
this.security = new WebX11SecurityManager(this, parseInt(localStorage.getItem('webx11_security_level')) || 1);
this.ollamaManager = new OllamaManager(this);
+ // Config registry โ€” modules can register keys for the unified `config` command
+ this._configRegistry = {};
+ this._configDefaults = {};
+ this._configSetters = {};
+ this._loadTerminalConfig();
+ // Register built-in terminal config keys
+ this.registerConfigItem('terminal-max-lines', 200, (v) => { this.MAX_OUTPUT_LINES = v; });
+ this.registerConfigItem('terminal-hist-output', 50, (v) => { this._historyMaxLines = v; });
+ this.registerConfigItem('terminal-cmd-history', 200, (v) => { /* applied on next save */ });
+ this.registerConfigItem('terminal-pipe-buffer', 5000, (v) => { this._pipeBufferSize = v; });
this._pipeInput = null;
this._pipeBufferSize = 5000;
this._historyBuffer = [];
this._historyMaxLines = 50;
this._historySavePending = false;
this.MAX_OUTPUT_LINES = 200;
+ this._historyCap = 200;
this._aiBusy = false;
this._dirty = false;
this.history = JSON.parse(localStorage.getItem('webx11_command_history') || '[]');
@@ -221,6 +254,8 @@
// Auto-restore session
this._autoRestoreSession();
this._restoreFontSize();
+ // Restore saved theme on boot
+ this._restoreTheme();
// Init auto-save timer โ€” 60s after boot, then every 60s
setTimeout(() => this._initAutoSave(), 30000);
// Init sound toggle button text
@@ -231,6 +266,10 @@
setTimeout(() => this.inputField.focus(), 100);
// First-run welcome check โ€” fires after ext modules are loaded
setTimeout(() => this._checkFirstRun(), 2000);
+ // Init all registered modules (safe now โ€” constructor is done)
+ this._initRegisteredModules();
+ // Apply config overrides after everything is initialized
+ this._applyTerminalConfig();
}
initEventListeners() {
this.inputField.addEventListener('keydown', (e) => {
@@ -244,7 +283,14 @@
}
}
const cmd = this.inputField.value;
- this.history.push(cmd);
+ // Dedup: don't push if same as last entry
+ if (this.history.length === 0 || this.history[this.history.length - 1] !== cmd) {
+ this.history.push(cmd);
+ }
+ // Trim to max 200 entries
+ if (this.history.length > this._historyCap) {
+ this.history = this.history.slice(-this._historyCap);
+ }
this.historyIndex = this.history.length;
this._saveCommandHistory();
this.executeCommand(cmd);
@@ -739,7 +785,7 @@
var rootClone;
try {
const json = JSON.stringify(this.fs.root);
- if (json.length > 5242880) { // 5MB cap
+ if (json.length > 52428800) { // 50MB cap
rootClone = { type: 'directory', name: '/', content: {}, _truncated: true };
this.print('โš  Auto-save skipped (VFS too large)', 'warning');
return;
@@ -761,7 +807,7 @@
terminalLines: this._historyBuffer.slice(-50)
};
const snapshotJson = JSON.stringify(snapshot);
- if (snapshotJson.length > 5242880) {
+ if (snapshotJson.length > 52428800) {
this.print('โš  Auto-save snapshot too large, skipping', 'warning');
return;
}
@@ -851,9 +897,14 @@
.map(e => e.type === 'dir' ? prefix + e.name + '/' : prefix + e.name);
}
showAutocomplete() {
+ if (!this.moduleManager || !this.security) return;
const input = this.inputField.value;
const trimmed = input.trim();
- const commands = this.moduleManager.getAllCommands();
+ const commands = this.moduleManager.getAllCommands() || {};
+ // Merge built-in internals into autocomplete (they're handled via if-chain, not module commands)
+ for (const c of this.security.commandMeta.keys()) {
+ if (!(c in commands)) commands[c] = 'builtin';
+ }
this.suggestions = [];
this.selectedSuggestion = -1;
this._isCdAutocomplete = false;
@@ -1020,7 +1071,6 @@
this._safeRegister('WebX11AIAssistantModule', 'AI assistant');
this._safeRegister('WebFetcherModule', 'web fetcher');
this._safeRegister('WebXAIAliasModule', 'AI alias');
- this._safeRegister('SkillsModule', 'skills');
this._safeRegister('SecretVault', 'secret vault');
this.reloadPersistedModules();
this._safeRegister('ChatStudioModule', 'chat studio');
@@ -1035,6 +1085,15 @@
this._safeRegister('ServerMonitorModule', 'server monitor');
this._safeRegister('SimpleGUIModule', 'simple GUI builder');
}
+ _initRegisteredModules() {
+ for (const [name, mod] of Object.entries(this.moduleManager.modules)) {
+ if (typeof mod.init === 'function') {
+ try { mod.init(this); } catch (e) {
+ console.warn(`[Init] ${name} failed: ${e.message}`);
+ }
+ }
+ }
+ }
_safeRegister(className, label) {
let cls;
try {
@@ -1050,6 +1109,56 @@
try { this.moduleManager.registerModule(new cls()); }
catch (e) { console.warn(`[SafeRegister] Skipped ${label}: ${e.message}`); }
}
+ // โ”€โ”€ Config Registry โ”€โ”€
+ registerConfigItem(key, defaultValue, setterFn) {
+ this._configRegistry[key] = { key, defaultValue };
+ this._configDefaults[key] = defaultValue;
+ this._configSetters[key] = setterFn;
+ // Apply persisted override if present
+ const saved = localStorage.getItem('webx11_terminal_config');
+ if (saved) {
+ try {
+ const parsed = JSON.parse(saved);
+ if (parsed[key] !== undefined && parsed[key] !== defaultValue) {
+ setterFn(parsed[key]);
+ }
+ } catch (e) {}
+ }
+ }
+ _loadTerminalConfig() {
+ try {
+ const saved = localStorage.getItem('webx11_terminal_config');
+ if (!saved) return;
+ const parsed = JSON.parse(saved);
+ // Apply registered config items from persistence
+ for (const [key, val] of Object.entries(parsed)) {
+ if (this._configSetters[key] && val !== this._configDefaults[key]) {
+ this._configSetters[key](val);
+ }
+ }
+ } catch (e) {}
+ }
+ _saveTerminalConfig() {
+ const values = {};
+ for (const key of Object.keys(this._configRegistry)) {
+ const actual = this[key] !== undefined ? this[key] : this._configDefaults[key];
+ if (actual !== this._configDefaults[key]) {
+ values[key] = actual;
+ }
+ }
+ localStorage.setItem('webx11_terminal_config', JSON.stringify(values));
+ }
+ _applyTerminalConfig() {
+ try {
+ const saved = localStorage.getItem('webx11_terminal_config');
+ if (!saved) return;
+ const parsed = JSON.parse(saved);
+ if (parsed['terminal-max-lines'] !== undefined) this.MAX_OUTPUT_LINES = parsed['terminal-max-lines'];
+ if (parsed['terminal-hist-output'] !== undefined) this._historyMaxLines = parsed['terminal-hist-output'];
+ if (parsed['terminal-cmd-history'] !== undefined) this._historyCap = parsed['terminal-cmd-history'];
+ if (parsed['terminal-pipe-buffer'] !== undefined) this._pipeBufferSize = parsed['terminal-pipe-buffer'];
+ } catch (e) {}
+ }
async _loadExtModules(filterKeys) {
const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox'];
const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
@@ -1083,6 +1192,16 @@
async executeCommand(rawInput) {
if (!rawInput.trim()) return;
this.markDirty();
+ // Push to command history (dedup + trim)
+ const cmd = rawInput.trim();
+ if (this.history.length === 0 || this.history[this.history.length - 1] !== cmd) {
+ this.history.push(cmd);
+ }
+ if (this.history.length > this._historyCap) {
+ this.history = this.history.slice(-this._historyCap);
+ }
+ this.historyIndex = this.history.length;
+ this._saveCommandHistory();
this.printUserPrompt(rawInput);
const pipeParts = this.splitPipe(rawInput.trim());
if (pipeParts.length > 1) {
@@ -1108,7 +1227,7 @@
let pipeInput = null;
for (let i = 0; i < pipeParts.length; i++) {
const cmdText = pipeParts[i];
- const parts = cmdText.split(/\s+/);
+ const parts = this.security._parseArgs(cmdText);
const cmd = parts[0].toLowerCase();
const args = parts.slice(1);
this._pipeInput = pipeInput;
@@ -1139,9 +1258,40 @@
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 === 'config') {
+ if (args.length === 0) {
+ this.print('=== Terminal Config ===', 'info');
+ this.print(`terminal-max-lines: ${this.MAX_OUTPUT_LINES} (default: 200)`, '');
+ this.print(`terminal-hist-output: ${this._historyMaxLines} (default: 50)`, '');
+ this.print(`terminal-cmd-history: ${this._historyCap} (default: 200)`, '');
+ this.print(`terminal-pipe-buffer: ${this._pipeBufferSize} (default: 5000)`, '');
+ this.print('Usage: config <key> <value>', 'info');
+ this.print('Keys: terminal-max-lines, terminal-hist-output, terminal-cmd-history, terminal-pipe-buffer', 'info');
+ return;
+ }
+ const key = args[0];
+ const val = args[1];
+ if (val === undefined) { this.print('Usage: config <key> <value>', 'warning'); return; }
+ const n = parseInt(val);
+ if (isNaN(n) || n < 1) { this.print('Value must be a positive number', 'warning'); return; }
+ if (key === 'terminal-max-lines') { this.MAX_OUTPUT_LINES = n; }
+ else if (key === 'terminal-hist-output') { this._historyMaxLines = n; }
+ else if (key === 'terminal-cmd-history') { this._historyCap = n; }
+ else if (key === 'terminal-pipe-buffer') { this._pipeBufferSize = n; }
+ else { this.print(`Unknown key: ${key}. Valid: terminal-max-lines, terminal-hist-output, terminal-cmd-history, terminal-pipe-buffer`, 'warning'); return; }
+ this._saveTerminalConfig();
+ this.print(`config ${key} set to ${val}`, 'success');
+ return;
+ }
if (cmd === 'security') {
this.print(`Security level: ${this.security.getLevel()} (${this.security.getLevelName()})`, 'info');
this.print(`Blocked commands: ${this.security.blockedCount}`, 'info');
+ this.print(`Destructive set: ${[...this.security.destructiveCmds].sort().join(', ')}`, '');
+ this.print(`System set: ${[...this.security.systemCmds].sort().join(', ')}`, '');
+ this.print(`Suspicious flags: ${[...this.security.suspiciousFlags].sort().join(', ')}`, '');
+ this.print(`Whitelist: ${[...this.security.whitelist].sort().join(', ')}`, '');
+ const all = this.security._collectAllCommands();
+ this.print(`Registered commands: ${all.length}`, '');
return;
}
if (cmd === 'security-level') {
@@ -1151,14 +1301,160 @@
localStorage.setItem('webx11_security_level', lv);
return;
}
+ if (cmd === 'security-destructive') {
+ const action = args[0];
+ if (!action) { this.print('Usage: security-destructive add|remove|list [cmd]', 'warning'); return; }
+ if (action === 'list') {
+ this.print('Destructive commands:', 'info');
+ for (const c of this.security.destructiveCmds) this.print(` ${c}`, '');
+ return;
+ }
+ if (!args[1]) { this.print('Usage: security-destructive add|remove <cmd>', 'warning'); return; }
+ 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.print(`Updated destructive set (${this.security.destructiveCmds.size} commands)`, 'success');
+ return;
+ }
+ if (cmd === 'security-system') {
+ const action = args[0];
+ if (!action) { this.print('Usage: security-system add|remove|list [cmd]', 'warning'); return; }
+ if (action === 'list') {
+ this.print('System commands:', 'info');
+ for (const c of this.security.systemCmds) this.print(` ${c}`, '');
+ return;
+ }
+ if (!args[1]) { this.print('Usage: security-system add|remove <cmd>', 'warning'); return; }
+ 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.print(`Updated system set (${this.security.systemCmds.size} commands)`, 'success');
+ return;
+ }
+ if (cmd === 'security-whitelist') {
+ const action = args[0];
+ if (!action) { this.print('Usage: security-whitelist add|remove|list [cmd]', 'warning'); return; }
+ if (action === 'list') {
+ this.print('Whitelist commands:', 'info');
+ for (const c of this.security.whitelist) this.print(` ${c}`, '');
+ return;
+ }
+ if (!args[1]) { this.print('Usage: security-whitelist add|remove <cmd>', 'warning'); return; }
+ 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.print(`Updated whitelist (${this.security.whitelist.size} commands)`, 'success');
+ return;
+ }
+ if (cmd === 'security-flag') {
+ const action = args[0];
+ if (!action) { this.print('Usage: security-flag add|remove|list [flag]', 'warning'); return; }
+ if (action === 'list') {
+ this.print('Suspicious flags:', 'info');
+ for (const f of this.security.suspiciousFlags) this.print(` ${f}`, '');
+ return;
+ }
+ if (!args[1]) { this.print('Usage: security-flag add|remove <flag>', 'warning'); return; }
+ const flag = args[1].startsWith('--') ? args[1] : '--' + args[1];
+ 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.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');
+ return;
+ }
+ this.fs.reset();
+ this.print('VFS reset โ€” all files deleted.', 'success');
+ return;
+ }
+ if (cmd === 'vfs' && args[0] === 'clean') {
+ let count = 0;
+ const sysDirs = ['_snapshots', '_sessions', '.trash'];
+ for (const dir of sysDirs) {
+ if (this.fs.root.content && this.fs.root.content[dir]) {
+ delete this.fs.root.content[dir];
+ count++;
+ }
+ }
+ if (count > 0) {
+ this.fs.saveToStorage();
+ this.print('๐Ÿงน Cleaned ' + count + ' system dirs: ' + sysDirs.join(', '), 'success');
+ } else {
+ this.print('Nothing to clean โ€” system dirs already empty.', 'info');
+ }
+ return;
+ }
+ if (cmd === 'vfs-clean') {
+ let count = 0;
+ const sysDirs = ['_snapshots', '_sessions', '.trash'];
+ for (const dir of sysDirs) {
+ if (this.fs.root.content && this.fs.root.content[dir]) {
+ delete this.fs.root.content[dir];
+ count++;
+ }
+ }
+ if (count > 0) {
+ this.fs.saveToStorage();
+ this.print('๐Ÿงน Cleaned ' + count + ' system dirs: ' + sysDirs.join(', '), 'success');
+ } else {
+ this.print('Nothing to clean โ€” system dirs already empty.', 'info');
+ }
+ 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');
+ return;
+ }
+ if (this.fs.emptyTrash()) {
+ this.print('Trash emptied.', 'success');
+ } else {
+ this.print('Trash is already empty.', 'info');
+ }
+ return;
+ }
+ if (cmd === 'history') {
+ const search = args.join(' ').toLowerCase();
+ const entries = this.history;
+ if (!entries.length) { this.print('No command history.', 'info'); return; }
+ if (search) {
+ const filtered = entries.map((c, i) => ({ i, c })).filter(e => e.c.toLowerCase().includes(search));
+ if (!filtered.length) { this.print(`No history entries matching: ${search}`, 'info'); return; }
+ this.print(`=== History (matching: ${search}) ===`, 'info');
+ filtered.forEach(e => this.print(` ${e.i + 1} ${e.c}`));
+ } else {
+ this.print(`=== Command History (${entries.length} entries) ===`, 'info');
+ const start = Math.max(0, entries.length - 50);
+ for (let i = start; i < entries.length; i++) {
+ this.print(` ${i + 1} ${entries[i]}`);
+ }
+ if (entries.length > 50) this.print(`... (${entries.length - 50} older entries hidden, use history <search> to find)`);
+ }
+ return;
+ }
for (const modName in this.moduleManager.modules) {
const mod = this.moduleManager.modules[modName];
if (mod.commands && mod.commands[cmd]) {
try { await mod.commands[cmd](args);
// Notify tutorial module for quest tracking
if (this.tutorialModule && cmd !== 'tutorial') this.tutorialModule.onCommandExecuted(cmd, args);
+ // Memory graph: record file access for read/file commands
+ if (cmd === 'cat' || cmd === 'view_module' || cmd === 'edit_module') {
+ try {
+ const _mg = this.moduleManager.modules['MemoryGraphModule'];
+ if (_mg && args[0]) _mg.recordFileAccess(args[0]);
+ } catch (_e) {}
+ }
return; }
catch (err) { this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); return; }
}
@@ -1336,8 +1632,7 @@
openSkillsManager() {
const modal = document.getElementById('skills-manager');
// Ensure /skills/ directory exists
- const skillsMod = this.moduleManager.modules['SkillsModule'];
- if (skillsMod && skillsMod.ensureSkillsDir) skillsMod.ensureSkillsDir();
+ this.fs.mkdirRecursive('/skills');
this.refreshSkillsList();
modal.style.display = 'flex';
}
@@ -1453,12 +1748,16 @@
if (!confirm(`Delete skill "${name}"?`)) return;
const fs = this.fs;
fs.mkdirRecursive('/skills');
- fs.writeFile(`/skills/${name}.md`, '');
+ // Actually remove from VFS tree
+ if (fs.root && fs.root.content && fs.root.content['skills'] && fs.root.content['skills'].content) {
+ delete fs.root.content['skills'].content[`${name}.md`];
+ }
const index = fs.fsReadJSON('/skills/index.json') || {};
delete index[name];
fs.fsWriteJSON('/skills/index.json', index);
this.print(`Skill '${name}' deleted`, 'success');
this.refreshSkillsList();
+ fs.saveToStorage();
}
createSkillFromUI() {
const name = document.getElementById('skill-name-input').value.trim().replace(/\s+/g, '-').toLowerCase();
@@ -1890,6 +2189,35 @@
}
} catch (e) {}
}
+ _restoreTheme() {
+ try {
+ const savedTheme = localStorage.getItem('webx11_active_theme');
+ if (savedTheme) {
+ const themesRaw = localStorage.getItem('webx11_themes');
+ const defaults = {
+ 'dark': { '--bg-color': '#000000', '--bg-secondary': '#0a0a0a', '--bg-tertiary': '#111111', '--text-color': '#00ff00', '--text-muted': '#888888', '--accent-color': '#00aa00', '--border-color': '#005500' },
+ 'light': { '--bg-color': '#ffffff', '--bg-secondary': '#f5f5f5', '--bg-tertiary': '#eeeeee', '--text-color': '#222222', '--text-muted': '#888888', '--accent-color': '#0066cc', '--border-color': '#cccccc' },
+ 'amber': { '--bg-color': '#1a0f00', '--bg-secondary': '#2a1a00', '--bg-tertiary': '#332200', '--text-color': '#ffaa00', '--text-muted': '#886600', '--accent-color': '#ff8800', '--border-color': '#663300' },
+ 'cyber': { '--bg-color': '#000a14', '--bg-secondary': '#001a2e', '--bg-tertiary': '#002240', '--text-color': '#00ffff', '--text-muted': '#0088aa', '--accent-color': '#ff00ff', '--border-color': '#004466' },
+ 'ocean': { '--bg-color': '#0a1628', '--bg-secondary': '#0f2240', '--bg-tertiary': '#142e54', '--text-color': '#88ddff', '--text-muted': '#5599bb', '--accent-color': '#00aaff', '--border-color': '#1a4470' },
+ 'forest': { '--bg-color': '#0a1a0e', '--bg-secondary': '#0f2814', '--bg-tertiary': '#14361c', '--text-color': '#88dd88', '--text-muted': '#559955', '--accent-color': '#33cc33', '--border-color': '#1a4a22' },
+ 'monochrome': { '--bg-color': '#0a0a0a', '--bg-secondary': '#181818', '--bg-tertiary': '#222222', '--text-color': '#cccccc', '--text-muted': '#666666', '--accent-color': '#999999', '--border-color': '#333333' },
+ 'sunset': { '--bg-color': '#1a0a14', '--bg-secondary': '#2a1220', '--bg-tertiary': '#3a1a2c', '--text-color': '#ffbb99', '--text-muted': '#cc7755', '--accent-color': '#ff6644', '--border-color': '#552233' },
+ 'midnight': { '--bg-color': '#050510', '--bg-secondary': '#0a0a20', '--bg-tertiary': '#101030', '--text-color': '#aaaaee', '--text-muted': '#555588', '--accent-color': '#6666ff', '--border-color': '#1a1a3a' }
+ };
+ if (themesRaw) {
+ const themes = JSON.parse(themesRaw);
+ Object.assign(defaults, themes);
+ }
+ const t = defaults[savedTheme];
+ if (t) {
+ for (const [k, v] of Object.entries(t)) {
+ document.documentElement.style.setProperty(k, v);
+ }
+ }
+ }
+ } catch (e) { console.warn('[Theme] restore error:', e); }
+ }
// โ”€โ”€ First-run welcome โ”€โ”€
_checkFirstRun() {
if (localStorage.getItem('webx11_tutorial_complete') || localStorage.getItem('webx11_tutorial_skipped')) return;
diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js
index 3ab25b1..59ab18b 100644
--- a/modules/core/virtual-fs.js
+++ b/modules/core/virtual-fs.js
@@ -520,15 +520,25 @@ class VirtualFileSystem {
checkpoint(name) {
if (!name) name = new Date().toISOString();
const createdAt = new Date().toISOString();
+ // Strip system dirs to avoid fractal bloat โ€” snapshots/sessions/trash
+ // don't belong inside a checkpoint of user data
+ const root = JSON.parse(JSON.stringify(this.root));
+ if (root && root.content) {
+ delete root.content['_snapshots'];
+ delete root.content['_sessions'];
+ delete root.content['.trash'];
+ }
const data = {
version: 1,
createdAt,
- root: JSON.parse(JSON.stringify(this.root)),
+ root,
currentPath: [...this.currentPath]
};
if (!this.readDir('/_snapshots')) {
this.mkdirRecursive('/_snapshots');
}
+ // Strip _snapshots from the serialized data too (redundant safety)
+ data.root = root;
this.fsWriteJSON('/_snapshots/checkpoint-' + name + '.json', data);
this.saveToStorage();
// Auto-manage: keep max 10 checkpoints, delete oldest
@@ -568,6 +578,26 @@ class VirtualFileSystem {
}
return false;
}
+ // Reset VFS โ€” wipes everything, removes localStorage keys
+ reset() {
+ this.root = { type: 'dir', content: {} };
+ this.currentPath = [];
+ this._conceptCache = null;
+ try {
+ localStorage.removeItem('webx11_filesystem_v2');
+ localStorage.removeItem('webx11_filesystem');
+ } catch (e) {}
+ return true;
+ }
+ // Empty trash โ€” permanently delete all trashed files
+ emptyTrash() {
+ if (this.root.content['.trash']) {
+ delete this.root.content['.trash'];
+ this.saveToStorage();
+ return true;
+ }
+ return false;
+ }
// Old-compat aliases for agent tools and SkillsModule
read(path) { return this.readFile(path); }
write(path, content) { return this.writeFile(path, content); }