aboutsummaryrefslogtreecommitdiff
path: root/.bkp-fix-20261023_143429
diff options
context:
space:
mode:
Diffstat (limited to '.bkp-fix-20261023_143429')
-rw-r--r--.bkp-fix-20261023_143429/clusters.php98
-rw-r--r--.bkp-fix-20261023_143429/config.php59
-rw-r--r--.bkp-fix-20261023_143429/dashboard.php200
-rw-r--r--.bkp-fix-20261023_143429/error.php36
-rw-r--r--.bkp-fix-20261023_143429/errors.php43
-rw-r--r--.bkp-fix-20261023_143429/filemgr.js1150
-rw-r--r--.bkp-fix-20261023_143429/health.php20
-rw-r--r--.bkp-fix-20261023_143429/help.html2147
-rw-r--r--.bkp-fix-20261023_143429/ingest.php123
-rw-r--r--.bkp-fix-20261023_143429/main.html1484
-rw-r--r--.bkp-fix-20261023_143429/module-debug-supervisor.js889
-rw-r--r--.bkp-fix-20261023_143429/module-loader.js84
-rw-r--r--.bkp-fix-20261023_143429/module-memory-graph.js663
-rw-r--r--.bkp-fix-20261023_143429/module-world-model.js600
-rw-r--r--.bkp-fix-20261023_143429/report.php112
-rw-r--r--.bkp-fix-20261023_143429/skill-pack-core.js1150
-rw-r--r--.bkp-fix-20261023_143429/stats.php55
17 files changed, 8913 insertions, 0 deletions
diff --git a/.bkp-fix-20261023_143429/clusters.php b/.bkp-fix-20261023_143429/clusters.php
new file mode 100644
index 0000000..5174317
--- /dev/null
+++ b/.bkp-fix-20261023_143429/clusters.php
@@ -0,0 +1,98 @@
+<?php
+require_once __DIR__ . '/config.php';
+
+$errors = load_json(ERRORS_FILE);
+$rebuildFlag = DEBUG_DATA_DIR . '/.rebuild_clusters';
+
+// Check if cached clusters exist and are fresh
+if (file_exists(CLUSTERS_FILE) && !file_exists($rebuildFlag)) {
+ $clusters = load_json(CLUSTERS_FILE);
+ json_response([
+ 'success' => true,
+ 'cached' => true,
+ 'clusters' => $clusters,
+ 'total_errors' => count($errors),
+ ]);
+}
+
+// Rebuild clusters
+$normalized = [];
+foreach ($errors as $e) {
+ $msg = strtolower($e['message'] ?? '');
+ $msg = preg_replace('/\b0x[a-f0-9]+\b/', '0xHEX', $msg);
+ $msg = preg_replace('/\b\d+\b/', 'N', $msg);
+ $msg = preg_replace('/["\'].*?["\']/', '"STR"', $msg);
+ $msg = preg_replace('/[^\w\s]/', ' ', $msg);
+ $msg = preg_replace('/\s+/', ' ', $msg);
+ $normalized[] = trim($msg);
+}
+
+$clusters = [];
+$assigned = [];
+$threshold = CLUSTER_SIMILARITY_THRESHOLD;
+
+for ($i = 0; $i < count($errors); $i++) {
+ if (isset($assigned[$i])) continue;
+
+ $cluster = [
+ 'errors' => [$i],
+ 'messages' => [$errors[$i]['message']],
+ 'count' => $errors[$i]['count'] ?? 1,
+ 'types' => [$errors[$i]['type'] ?? 'unknown'],
+ 'categories' => [$errors[$i]['category'] ?? 'runtime'],
+ 'label' => $errors[$i]['message'] ?? '',
+ ];
+ $assigned[$i] = true;
+
+ for ($j = $i + 1; $j < count($errors); $j++) {
+ if (isset($assigned[$j])) continue;
+
+ // Jaccard similarity
+ $tokensA = array_unique(array_filter(explode(' ', $normalized[$i])));
+ $tokensB = array_unique(array_filter(explode(' ', $normalized[$j])));
+ if (empty($tokensA) && empty($tokensB)) {
+ $similarity = 1;
+ } else {
+ $intersection = count(array_intersect($tokensA, $tokensB));
+ $union = count($tokensA) + count($tokensB) - $intersection;
+ $similarity = $union > 0 ? $intersection / $union : 0;
+ }
+
+ if ($similarity >= $threshold) {
+ $cluster['errors'][] = $j;
+ $cluster['messages'][] = $errors[$j]['message'];
+ $cluster['count'] += $errors[$j]['count'] ?? 1;
+ if (!in_array($errors[$j]['type'] ?? 'unknown', $cluster['types'])) {
+ $cluster['types'][] = $errors[$j]['type'] ?? 'unknown';
+ }
+ if (!in_array($errors[$j]['category'] ?? 'runtime', $cluster['categories'])) {
+ $cluster['categories'][] = $errors[$j]['category'] ?? 'runtime';
+ }
+ $assigned[$j] = true;
+ }
+ }
+
+ // Most frequent message as label
+ $freq = array_count_values($cluster['messages']);
+ arsort($freq);
+ $cluster['label'] = substr(key($freq) ?: $cluster['label'], 0, 200);
+ $cluster['error_count'] = count($cluster['errors']);
+ unset($cluster['errors']);
+ unset($cluster['messages']);
+
+ $clusters[] = $cluster;
+}
+
+usort($clusters, fn($a, $b) => $b['count'] - $a['count']);
+
+// Cache
+save_json(CLUSTERS_FILE, $clusters);
+if (file_exists($rebuildFlag)) unlink($rebuildFlag);
+
+json_response([
+ 'success' => true,
+ 'cached' => false,
+ 'clusters' => array_slice($clusters, 0, 50),
+ 'total_errors' => count($errors),
+ 'total_clusters' => count($clusters),
+]);
diff --git a/.bkp-fix-20261023_143429/config.php b/.bkp-fix-20261023_143429/config.php
new file mode 100644
index 0000000..9abc262
--- /dev/null
+++ b/.bkp-fix-20261023_143429/config.php
@@ -0,0 +1,59 @@
+<?php
+// api/debug/config.php — Shared config for debug backend
+header('Content-Type: application/json; charset=utf-8');
+header('Access-Control-Allow-Origin: https://webx11.duckdns.org');
+header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
+header('Access-Control-Allow-Headers: Content-Type');
+header('Access-Control-Max-Age: 86400');
+
+// Handle preflight
+if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
+ http_response_code(204);
+ exit;
+}
+
+date_default_timezone_set('UTC');
+
+define('DEBUG_DATA_DIR', __DIR__ . '/../../data/debug');
+define('MAX_ERRORS_PER_BATCH', 100);
+define('MAX_BATCH_SIZE_KB', 1024); // raised 64->1024 (2026-10-05): error batches with stacks legitimately exceed 64KB, causing endless 413s
+define('CLUSTER_SIMILARITY_THRESHOLD', 0.35);
+define('ERRORS_FILE', DEBUG_DATA_DIR . '/errors.json');
+define('SESSIONS_FILE', DEBUG_DATA_DIR . '/sessions.json');
+define('CLUSTERS_FILE', DEBUG_DATA_DIR . '/clusters.json');
+
+if (!is_dir(DEBUG_DATA_DIR)) {
+ mkdir(DEBUG_DATA_DIR, 0755, true);
+}
+
+function load_json($path) {
+ if (!file_exists($path)) return [];
+ $data = json_decode(file_get_contents($path), true);
+ return is_array($data) ? $data : [];
+}
+
+function save_json($path, $data) {
+ $encoded = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ $fp = fopen($path, 'c');
+ if ($fp && flock($fp, LOCK_EX)) {
+ ftruncate($fp, 0);
+ fwrite($fp, $encoded);
+ fflush($fp);
+ flock($fp, LOCK_UN);
+ fclose($fp);
+ } else {
+ // Fallback: write without lock
+ file_put_contents($path, $encoded);
+ }
+ @chmod($path, 0664);
+}
+
+function json_response($data, $code = 200) {
+ http_response_code($code);
+ echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ exit;
+}
+
+function error_response($msg, $code = 400) {
+ json_response(['success' => false, 'error' => $msg], $code);
+}
diff --git a/.bkp-fix-20261023_143429/dashboard.php b/.bkp-fix-20261023_143429/dashboard.php
new file mode 100644
index 0000000..c235ec0
--- /dev/null
+++ b/.bkp-fix-20261023_143429/dashboard.php
@@ -0,0 +1,200 @@
+<?php
+require_once __DIR__ . '/config.php';
+header('Content-Type: text/html; charset=utf-8');
+
+$errors = load_json(ERRORS_FILE);
+$sessions = load_json(SESSIONS_FILE);
+
+$totalErrors = count($errors);
+$totalSessions = count($sessions);
+$byType = [];
+$bySeverity = [];
+$byCategory = [];
+$byModule = [];
+$byDay = [];
+$topErrors = [];
+
+foreach ($errors as $e) {
+ $t = $e['type'] ?? 'unknown';
+ $byType[$t] = ($byType[$t] ?? 0) + ($e['count'] ?? 1);
+ $s = $e['severity'] ?? 'error';
+ $bySeverity[$s] = ($bySeverity[$s] ?? 0) + ($e['count'] ?? 1);
+ $m = $e['module'] ?? 'unknown';
+ $byModule[$m] = ($byModule[$m] ?? 0) + ($e['count'] ?? 1);
+ $c = $e['category'] ?? 'runtime';
+ $byCategory[$c] = ($byCategory[$c] ?? 0) + ($e['count'] ?? 1);
+ $day = substr($e['ingested_at'] ?? $e['last_seen'], 0, 10);
+ $byDay[$day] = ($byDay[$day] ?? 0) + ($e['count'] ?? 1);
+
+ $hash = $e['hash'] ?? '';
+ if ($hash) {
+ if (!isset($topErrors[$hash])) {
+ $topErrors[$hash] = ['hash' => $hash, 'message' => $e['message'] ?? '', 'count' => 0, 'type' => $t];
+ }
+ $topErrors[$hash]['count'] += $e['count'] ?? 1;
+ }
+}
+usort($topErrors, fn($a, $b) => $b['count'] - $a['count']);
+$topErrors = array_slice($topErrors, 0, 10);
+ksort($byDay);
+
+$recentSessions = array_slice($sessions, -10, 10, true);
+?>
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>newOS4 Debug Dashboard</title>
+<style>
+ :root { --bg: #0d0d12; --surface: #16161e; --border: #1e1e2a; --text: #e8e8f0; --muted: #888; --accent: #00e5c8; --error: #ff5555; --warn: #ffaa00; --info: #00aaff; }
+ * { box-sizing: border-box; margin: 0; padding: 0; }
+ body { background: var(--bg); color: var(--text); font-family: 'Courier New', monospace; padding: 20px; }
+ h1 { color: var(--accent); font-size: 1.4em; margin-bottom: 4px; }
+ .subtitle { color: var(--muted); font-size: 0.85em; margin-bottom: 20px; }
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; margin-bottom: 20px; }
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px; }
+ .card h3 { font-size: 0.8em; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
+ .stat-value { font-size: 2em; font-weight: bold; }
+ .stat-value.error { color: var(--error); }
+ .stat-value.warn { color: var(--warn); }
+ .stat-value.ok { color: var(--accent); }
+ table { width: 100%; border-collapse: collapse; font-size: 0.85em; }
+ th { text-align: left; color: var(--muted); padding: 6px 8px; border-bottom: 1px solid var(--border); font-size: 0.8em; text-transform: uppercase; }
+ td { padding: 6px 8px; border-bottom: 1px solid var(--border); }
+ .bar { display: inline-block; height: 6px; border-radius: 3px; background: var(--accent); margin-right: 6px; vertical-align: middle; }
+ .bar.error { background: var(--error); }
+ .bar.warn { background: var(--warn); }
+ .tag { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 0.75em; background: var(--border); }
+ .tag.error { background: #3a1010; color: var(--error); }
+ .tag.warn { background: #3a2a00; color: var(--warn); }
+ .tag.ok { background: #002a20; color: var(--accent); }
+ .refresh { color: var(--muted); font-size: 0.8em; margin-top: 20px; text-align: center; }
+ a { color: var(--accent); text-decoration: none; }
+ a:hover { text-decoration: underline; }
+ .max-w-300 { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+</style>
+</head>
+<body>
+<h1>🔍 newOS4 Debug Dashboard</h1>
+<div class="subtitle">Server-side error aggregation &middot; <?= date('Y-m-d H:i:s') ?> &middot; <a href="stats.php">JSON Stats</a> &middot; <a href="clusters.php">Clusters</a> &middot; <a href="errors.php">Raw Errors</a> &middot; <a href="health.php">Health</a></div>
+
+<?php
+// Find last sync time
+$lastSync = null;
+if (!empty($errors)) {
+ $lastSync = end($errors)['ingested_at'] ?? null;
+}
+if ($lastSync): ?>
+<div style="color:var(--muted);font-size:0.8em;margin-bottom:16px;">Last sync: <?= htmlspecialchars(substr($lastSync, 0, 19)) ?> UTC</div>
+<?php endif; ?>
+
+<div class="grid">
+ <div class="card">
+ <h3>Total Errors</h3>
+ <div class="stat-value <?= $totalErrors > 0 ? 'error' : 'ok' ?>"><?= $totalErrors ?></div>
+ </div>
+ <div class="card">
+ <h3>Sessions</h3>
+ <div class="stat-value"><?= $totalSessions ?></div>
+ </div>
+ <div class="card">
+ <h3>Error Types</h3>
+ <div class="stat-value"><?= count($byType) ?></div>
+ </div>
+ <div class="card">
+ <h3>Modules Affected</h3>
+ <div class="stat-value"><?= count($byModule) ?></div>
+ </div>
+</div>
+
+<div class="grid">
+ <div class="card">
+ <h3>By Severity</h3>
+ <table>
+ <?php foreach (['fatal' => 'error', 'error' => 'error', 'warning' => 'warn'] as $sev => $cls): ?>
+ <tr><td><span class="tag <?= $cls ?>"><?= $sev ?></span></td><td><?= $bySeverity[$sev] ?? 0 ?></td></tr>
+ <?php endforeach; ?>
+ </table>
+ </div>
+ <div class="card">
+ <h3>By Category</h3>
+ <table>
+ <?php foreach (['network', 'module', 'runtime', 'vfs', 'security', 'tool'] as $cat): ?>
+ <?php if (($byCategory[$cat] ?? 0) > 0): ?>
+ <tr><td><?= $cat ?></td><td><?= $byCategory[$cat] ?? 0 ?></td></tr>
+ <?php endif; ?>
+ <?php endforeach; ?>
+ </table>
+ </div>
+ <div class="card">
+ <h3>By Module</h3>
+ <table>
+ <?php
+ arsort($byModule);
+ $i = 0;
+ foreach (array_slice($byModule, 0, 8) as $mod => $count):
+ if ($i++ > 5) break;
+ ?>
+ <tr><td class="max-w-300"><?= htmlspecialchars($mod) ?></td><td><?= $count ?></td></tr>
+ <?php endforeach; ?>
+ </table>
+ </div>
+</div>
+
+<div class="card" style="margin-bottom:20px;">
+ <h3>Top Errors</h3>
+ <table>
+ <tr><th>#</th><th>Message</th><th>Type</th><th>Count</th></tr>
+ <?php $i = 1; foreach ($topErrors as $e): ?>
+ <tr>
+ <td><?= $i++ ?></td>
+ <td class="max-w-300"><a href="error.php?hash=<?= urlencode($e['hash'] ?? '') ?>" style="color:var(--text);text-decoration:none;" title="View details"><?= htmlspecialchars(substr($e['message'] ?? '', 0, 80)) ?></a></td>
+ <td><span class="tag"><?= htmlspecialchars($e['type'] ?? '?') ?></span></td>
+ <td><?= (int)($e['count'] ?? 0) ?></td>
+ </tr>
+ <?php endforeach; ?>
+ <?php if (empty($topErrors)): ?>
+ <tr><td colspan="4" style="color:var(--muted);text-align:center;">No errors recorded</td></tr>
+ <?php endif; ?>
+ </table>
+</div>
+
+<div class="card" style="margin-bottom:20px;">
+ <h3>Daily Trend (last 14 days)</h3>
+ <table>
+ <tr><th>Day</th><th>Errors</th><th></th></tr>
+ <?php
+ $maxDay = max($byDay) ?: 1;
+ $days = array_slice($byDay, -14, 14, true);
+ foreach ($days as $day => $count):
+ $pct = round($count / $maxDay * 100, 0);
+ ?>
+ <tr>
+ <td><?= $day ?></td>
+ <td><?= $count ?></td>
+ <td><div class="bar" style="width:<?= $pct ?>%;"></div></td>
+ </tr>
+ <?php endforeach; ?>
+ </table>
+</div>
+
+<div class="card">
+ <h3>Recent Sessions</h3>
+ <table>
+ <tr><th>Session</th><th>Version</th><th>Errors</th><th>Last Seen</th></tr>
+ <?php foreach ($recentSessions as $sid => $s): ?>
+ <tr>
+ <td class="max-w-300"><a href="errors.php?session=<?= urlencode($sid) ?>" style="color:var(--text);text-decoration:none;" title="View this session's errors"><?= htmlspecialchars($sid) ?></a></td>
+ <td><?= htmlspecialchars($s['version'] ?? '?') ?></td>
+ <td><?= $s['error_count'] ?? 0 ?></td>
+ <td><?= htmlspecialchars(substr($s['last_seen'] ?? '', 0, 16)) ?></td>
+ </tr>
+ <?php endforeach; ?>
+ </table>
+</div>
+
+<div class="refresh">Auto-refresh every 30s</div>
+<script>setTimeout(() => location.reload(), 30000);</script>
+</body>
+</html>
diff --git a/.bkp-fix-20261023_143429/error.php b/.bkp-fix-20261023_143429/error.php
new file mode 100644
index 0000000..242dfca
--- /dev/null
+++ b/.bkp-fix-20261023_143429/error.php
@@ -0,0 +1,36 @@
+<?php
+require_once __DIR__ . '/config.php';
+
+$hash = $_GET['hash'] ?? '';
+if (!$hash) error_response('Missing hash parameter');
+
+$errors = load_json(ERRORS_FILE);
+$sessions = load_json(SESSIONS_FILE);
+
+$matches = array_filter($errors, fn($e) => ($e['hash'] ?? '') === $hash);
+$matches = array_values($matches);
+
+if (empty($matches)) {
+ error_response('Error not found', 404);
+}
+
+$totalCount = array_sum(array_column($matches, 'count'));
+$uniqueSessions = array_unique(array_column($matches, 'session_id'));
+$firstSeen = min(array_column($matches, 'first_seen'));
+$lastSeen = max(array_column($matches, 'last_seen'));
+
+json_response([
+ 'success' => true,
+ 'hash' => $hash,
+ 'total_occurrences' => count($matches),
+ 'total_count' => $totalCount,
+ 'unique_sessions' => count($uniqueSessions),
+ 'first_seen' => $firstSeen,
+ 'last_seen' => $lastSeen,
+ 'type' => $matches[0]['type'] ?? 'unknown',
+ 'severity' => $matches[0]['severity'] ?? 'error',
+ 'category' => $matches[0]['category'] ?? 'runtime',
+ 'sample_message' => $matches[0]['message'] ?? '',
+ 'sample_stack' => $matches[0]['stack'] ?? null,
+ 'occurrences' => array_slice($matches, 0, 20),
+]);
diff --git a/.bkp-fix-20261023_143429/errors.php b/.bkp-fix-20261023_143429/errors.php
new file mode 100644
index 0000000..1ed9a2f
--- /dev/null
+++ b/.bkp-fix-20261023_143429/errors.php
@@ -0,0 +1,43 @@
+<?php
+require_once __DIR__ . '/config.php';
+
+$errors = load_json(ERRORS_FILE);
+
+// Filters
+$type = $_GET['type'] ?? null;
+$severity = $_GET['severity'] ?? null;
+$module = $_GET['module'] ?? null;
+$category = $_GET['category'] ?? null;
+$search = $_GET['search'] ?? null;
+$from = $_GET['from'] ?? null;
+$to = $_GET['to'] ?? null;
+$session = $_GET['session'] ?? null;
+$page = max(1, (int)($_GET['page'] ?? 1));
+$perPage = min(100, max(1, (int)($_GET['per_page'] ?? 50)));
+
+$filtered = array_filter($errors, function($e) use ($type, $severity, $module, $category, $search, $from, $to, $session) {
+ if ($type && ($e['type'] ?? '') !== $type) return false;
+ if ($severity && ($e['severity'] ?? '') !== $severity) return false;
+ if ($module && ($e['module'] ?? '') !== $module) return false;
+ if ($category && ($e['category'] ?? '') !== $category) return false;
+ if ($session && ($e['session_id'] ?? '') !== $session) return false;
+ if ($search && stripos($e['message'] ?? '', $search) === false) return false;
+ if ($from && ($e['ingested_at'] ?? '') < $from) return false;
+ if ($to && ($e['ingested_at'] ?? '') > $to) return false;
+ return true;
+});
+
+$filtered = array_values($filtered);
+$total = count($filtered);
+$totalPages = max(1, ceil($total / $perPage));
+$offset = ($page - 1) * $perPage;
+$pageErrors = array_slice($filtered, $offset, $perPage);
+
+json_response([
+ 'success' => true,
+ 'total' => $total,
+ 'page' => $page,
+ 'per_page' => $perPage,
+ 'total_pages' => $totalPages,
+ 'errors' => $pageErrors,
+]);
diff --git a/.bkp-fix-20261023_143429/filemgr.js b/.bkp-fix-20261023_143429/filemgr.js
new file mode 100644
index 0000000..01d1da9
--- /dev/null
+++ b/.bkp-fix-20261023_143429/filemgr.js
@@ -0,0 +1,1150 @@
+class FileManagerModule {
+ constructor() {
+ this.commands = {};
+ this.commandsMeta = {
+ 'files': { argType: 'file' }, 'preview': { argType: 'file' },
+ };
+ this.terminal = null;
+ this.currentPath = [];
+ this._overlay = null;
+ this._selectedFile = null;
+ this._multiSelect = new Set(); // v4.5.78: multi-select for batch ops
+ this._selectMode = false; // v4.6.35: tap-to-select mode (touch devices)
+ this._itemCount = 0; // items rendered in the current directory/filter
+ this._clipboard = { mode: null, paths: [] }; // 'copy' or 'cut'
+ this._searchFilter = '';
+ this._rawMode = false;
+ this._inlineHtml = false;
+ }
+
+ requirements() {
+ return { met: true, missing: [] };
+ }
+
+ getHelpInfo() {
+ return {
+ 'files': 'files — Open the file manager overlay with VFS directory listing, preview, encrypt, zip',
+ 'preview': 'preview <path> — Preview a specific file',
+ };
+ }
+
+ registerCommands() {
+ this.commands['files'] = () => this._openOverlay();
+ this.commands['preview'] = (args) => this._previewCommand(args);
+ }
+
+ _previewCommand(args) {
+ if (!args[0]) return this.terminal.print('Usage: preview <path>', 'warning');
+ const path = args.join(' ');
+ this._openOverlay();
+ this._navigateToPathFromParts(path);
+ const content = this.terminal.fs.readFile(path);
+ if (content === null || content === false) {
+ this._renderPreviewError('File not found: ' + path);
+ return;
+ }
+ this._selectedFile = path;
+ this._renderPreview(path, content);
+ this._highlightSelected();
+ }
+
+ _navigateToPathFromParts(path) {
+ const parts = path.split('/').filter(p => p);
+ if (parts.length <= 1) {
+ this.currentPath = [];
+ return;
+ }
+ this.currentPath = parts.slice(0, -1);
+ }
+
+ _detectType(filePath) {
+ const ext = filePath.split('.').pop().toLowerCase();
+ if (!ext || ext === filePath) return 'other';
+ const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico'];
+ const videoExts = ['mp4', 'webm', 'avi', 'mov', 'mkv'];
+ const audioExts = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
+ const codeExts = ['js', 'py', 'css', 'yaml', 'yml', 'toml'];
+ if (ext === 'md') return 'md';
+ if (imageExts.includes(ext)) return 'image';
+ if (videoExts.includes(ext)) return 'video';
+ if (audioExts.includes(ext)) return 'audio';
+ if (codeExts.includes(ext)) return 'code';
+ if (ext === 'html') return 'html';
+ if (ext === 'json') return 'json';
+ if (ext === 'csv') return 'csv';
+ if (ext === 'pdf') return 'pdf';
+ if (ext === 'svg') return 'svg';
+ if (ext === 'txt') return 'text';
+ return 'other';
+ }
+
+ _renderMarkdown(text) {
+ if (!text) return '';
+ try {
+ let html = text
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+ // code blocks first
+ html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
+ // inline code
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
+ // headers
+ html = html.replace(/^### (.+)$/gm, '<h3 style="color:var(--accent-color);margin:8px 0 4px">$1</h3>');
+ html = html.replace(/^## (.+)$/gm, '<h2 style="color:var(--accent-color);margin:10px 0 4px">$1</h2>');
+ html = html.replace(/^# (.+)$/gm, '<h1 style="color:var(--accent-color);margin:12px 0 6px">$1</h1>');
+ // bold
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
+ // italic
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
+ // links — v4.5.67: sanitize URL to prevent javascript: scheme injection
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
+ const safeUrl = /^(https?:\/\/|mailto:|\/|#)/.test(url) ? url : '#';
+ return `<a href="${safeUrl}" target="_blank" style="color:var(--info-color)">${text}</a>`;
+ });
+ // horizontal rules
+ html = html.replace(/^---$/gm, '<hr style="border-color:var(--border-color);margin:8px 0">');
+ // unordered lists
+ html = html.replace(/^[\s]*[-*]\s+(.+)$/gm, '<li style="margin-left:16px;list-style:disc">$1</li>');
+ // paragraphs - double newlines
+ html = html.replace(/\n\n/g, '</p><p style="margin:6px 0">');
+ html = '<p style="margin:6px 0">' + html + '</p>';
+ return html;
+ } catch (e) {
+ return '<pre style="white-space:pre-wrap">' + text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</pre>';
+ }
+ }
+
+ _getPreviewHtml(path, content) {
+ if (content === null || content === false) {
+ return '<div style="color:var(--error-color);padding:20px">File not found: ' + this._escapeHtml(path) + '</div>';
+ }
+
+ const type = this._detectType(path);
+ const ext = path.split('.').pop().toLowerCase();
+ const fileName = path.split('/').pop() || path;
+
+ // Handle binary detection for media types
+ if (typeof content !== 'string') {
+ return '<div style="padding:20px;text-align:center;color:var(--text-muted)">Cannot preview: non-string content</div>';
+ }
+ const isLikelyBinary = content.length > 0 && !content.startsWith('\n') && !content.startsWith('\r') && !content.startsWith(' ') && !content.startsWith('<') && !content.startsWith('{') && !content.startsWith('[') && content.charCodeAt(0) < 32 && content.charCodeAt(0) !== 9 && content.charCodeAt(0) !== 10 && content.charCodeAt(0) !== 13;
+
+ const isTextContent = typeof content === 'string' && (content.startsWith('{') || content.startsWith('[') || content.startsWith('<') || content.startsWith('#') || /^[\w\s\n\r\t.,;:!?()\-=+*@#$%^&"'\/\\|`~]/.test(content.substring(0, 100)));
+
+ if (type === 'md' && isTextContent) {
+ return '<div class="markdown-preview" style="padding:12px;font-family:var(--font-primary);line-height:1.5">' + this._renderMarkdown(content) + '</div>';
+ }
+
+ if (type === 'html') {
+ // Render HTML in an inline sandbox frame
+ const safeHtml = content
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
+ .replace(/on\w+\s*=\s*"[^"]*"/gi, '')
+ .replace(/on\w+\s*=\s*'[^']*'/gi, '');
+ return '<div style="padding:0;width:100%;height:60vh;border:none;overflow:hidden">' +
+ '<iframe srcdoc="' + this._escapeHtml(safeHtml.replace(/"/g, '&quot;').replace(/'/g, '&#39;')) + '" ' +
+ 'style="width:100%;height:100%;border:none;background:#fff" sandbox="allow-same-origin"></iframe></div>';
+ }
+
+ if (type === 'text') {
+ const truncated = content.substring(0, 2000);
+ const extra = content.length > 2000 ? '<div style="color:var(--text-muted);margin-top:8px">(truncated to 2000 chars)</div>' : '';
+ return '<pre style="white-space:pre-wrap;padding:12px;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto">' + this._escapeHtml(truncated) + extra + '</pre>';
+ }
+
+ if (type === 'json') {
+ try {
+ const parsed = JSON.parse(content);
+ const formatted = JSON.stringify(parsed, null, 2);
+ const html = this._syntaxHighlightJson(formatted);
+ return '<pre style="padding:12px;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto;line-height:1.4">' + html + '</pre>';
+ } catch (e) {
+ return '<pre style="padding:12px;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto">' + this._escapeHtml(content.substring(0, 5000)) + '</pre>';
+ }
+ }
+
+ if (type === 'code') {
+ const lines = content.split('\n');
+ const totalLines = lines.length;
+ const maxLines = 150;
+ const displayLines = lines.slice(0, maxLines);
+ let html = '<table style="border-collapse:collapse;width:100%;font-family:var(--font-primary);font-size:0.85em;margin:0"><tbody>';
+ displayLines.forEach((line, i) => {
+ const lineNum = i + 1;
+ const escaped = this._escapeHtml(line);
+ html += '<tr><td style="color:var(--text-muted);padding:0 8px 0 4px;text-align:right;user-select:none;border-right:1px solid var(--border-color);vertical-align:top;width:40px">' + lineNum + '</td>';
+ html += '<td style="padding:0 8px;white-space:pre;width:100%">' + (escaped || '&nbsp;') + '</td></tr>';
+ });
+ html += '</tbody></table>';
+ if (totalLines > maxLines) {
+ html += '<div style="color:var(--text-muted);padding:8px;border-top:1px solid var(--border-color)">(showing ' + maxLines + ' of ' + totalLines + ' lines)</div>';
+ }
+ return html;
+ }
+
+ if (type === 'image') {
+ // VFS stores images as base64 strings — detect and render inline
+ const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100));
+ if (isBase64) {
+ const mimeTypes = {jpg:'image/jpeg',jpeg:'image/jpeg',png:'image/png',gif:'image/gif',webp:'image/webp',bmp:'image/bmp',ico:'image/x-icon'};
+ const mime = mimeTypes[ext] || 'image/png';
+ return '<div style="padding:12px;text-align:center;max-width:100%"><img src="data:' + mime + ';base64,' + content + '" style="max-width:100%;max-height:70vh;border-radius:4px;border:1px solid var(--border-color)" alt="' + this._escapeHtml(fileName) + '"></div>';
+ }
+ return '<div style="padding:20px;text-align:center;color:var(--text-muted)">📷 Image — cannot render, file may be stored as binary</div>';
+ }
+
+ if (type === 'svg') {
+ try {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(content, 'text/html');
+ const scripts = doc.querySelectorAll('script');
+ scripts.forEach(s => s.remove());
+ // Remove event handlers from svg elements
+ const allEls = doc.querySelectorAll('*');
+ allEls.forEach(el => {
+ for (let i = el.attributes.length - 1; i >= 0; i--) {
+ const attr = el.attributes[i].name;
+ if (attr.startsWith('on')) el.removeAttribute(attr);
+ }
+ });
+ const cleanSvg = doc.body.innerHTML;
+ return '<div style="padding:12px;text-align:center;max-width:100%;overflow:auto">' + cleanSvg + '</div>';
+ } catch (e) {
+ return '<pre style="padding:12px;font-family:var(--font-primary);font-size:0.9em;overflow-x:auto">' + this._escapeHtml(content.substring(0, 5000)) + '</pre>';
+ }
+ }
+
+ if (type === 'video') {
+ const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100));
+ if (isBase64) {
+ const mimeTypes = {mp4:'video/mp4',webm:'video/webm',avi:'video/x-msvideo',mov:'video/quicktime',mkv:'video/x-matroska'};
+ const mime = mimeTypes[ext] || 'video/mp4';
+ return '<div style="padding:12px;text-align:center"><video controls style="max-width:90%;max-height:65vh;border-radius:4px"><source src="data:' + mime + ';base64,' + content + '" type="' + mime + '"></video></div>';
+ }
+ return '<div style="padding:20px;text-align:center;color:var(--text-muted)">🎬 Video — cannot render</div>';
+ }
+
+ if (type === 'audio') {
+ const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100));
+ if (isBase64) {
+ const mimeTypes = {mp3:'audio/mpeg',wav:'audio/wav',ogg:'audio/ogg',flac:'audio/flac',aac:'audio/aac',m4a:'audio/mp4'};
+ const mime = mimeTypes[ext] || 'audio/mpeg';
+ return '<div style="padding:12px;text-align:center"><audio controls style="max-width:90%"><source src="data:' + mime + ';base64,' + content + '" type="' + mime + '"></audio></div>';
+ }
+ return '<div style="padding:20px;text-align:center;color:var(--text-muted)">🎵 Audio — cannot render</div>';
+ }
+
+ if (type === 'pdf') {
+ const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100));
+ if (isBase64) {
+ return '<div style="padding:12px;text-align:center;height:70vh"><embed src="data:application/pdf;base64,' + content + '" type="application/pdf" style="width:100%;height:100%;border:1px solid var(--border-color);border-radius:4px"></div>';
+ }
+ return '<div style="padding:20px;text-align:center;color:var(--text-muted)">📄 PDF — cannot render</div>';
+ }
+
+ if (type === 'csv') {
+ try {
+ const lines = content.split('\n').filter(l => l.trim());
+ const maxRows = 50;
+ const rows = lines.slice(0, maxRows).map(l => l.split(',').map(c => c.trim().replace(/^"|"$/g, '')));
+ let html = '<table class="csv-table" style="border-collapse:collapse;width:100%;font-family:var(--font-primary);font-size:0.85em;margin:0;background:var(--bg-secondary)">';
+ if (rows.length > 0) {
+ html += '<thead><tr>';
+ rows[0].forEach(cell => {
+ html += '<th style="border:1px solid var(--border-color);padding:6px 8px;text-align:left;background:var(--bg-tertiary);color:var(--accent-color);font-weight:bold">' + this._escapeHtml(cell) + '</th>';
+ });
+ html += '</tr></thead><tbody>';
+ rows.slice(1).forEach(row => {
+ html += '<tr>';
+ row.forEach(cell => {
+ html += '<td style="border:1px solid var(--border-color);padding:4px 8px">' + this._escapeHtml(cell) + '</td>';
+ });
+ html += '</tr>';
+ });
+ html += '</tbody></table>';
+ if (lines.length > maxRows) {
+ html += '<div style="color:var(--text-muted);padding:8px">(showing ' + maxRows + ' of ' + lines.length + ' rows)</div>';
+ }
+ }
+ return html;
+ } catch (e) {
+ return '<pre style="padding:12px;font-family:var(--font-primary);font-size:0.9em;overflow-x:auto">' + this._escapeHtml(content.substring(0, 5000)) + '</pre>';
+ }
+ }
+
+ // 'other' — raw text fallback
+ if (typeof content === 'string' && isTextContent) {
+ const lines = content.split('\n');
+ const maxLines = 80;
+ const displayLines = lines.slice(0, maxLines);
+ const truncated = lines.length > maxLines;
+ let html = '<pre style="white-space:pre-wrap;padding:12px;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto">';
+ displayLines.forEach((line, i) => {
+ html += this._escapeHtml(line) + '\n';
+ });
+ html += '</pre>';
+ if (truncated) {
+ html += '<div style="color:var(--text-muted);padding:8px;border-top:1px solid var(--border-color)">(showing ' + maxLines + ' of ' + lines.length + ' lines)</div>';
+ }
+ return html;
+ }
+
+ return '<div style="padding:20px;text-align:center;color:var(--text-muted)">📁 Preview not available for this file type</div>';
+ }
+
+ _syntaxHighlightJson(jsonStr) {
+ return jsonStr.replace(/("(?:\\.|[^"\\])*")\s*:/g, '<span style="color:var(--info-color)">$1</span>:')
+ .replace(/:\s*("(?:\\.|[^"\\])*")/g, ': <span style="color:var(--success-color)">$1</span>')
+ .replace(/:\s*(\d+\.?\d*)/g, ': <span style="color:var(--warning-color)">$1</span>')
+ .replace(/:\s*(true|false|null)/g, ': <span style="color:var(--error-color)">$1</span>');
+ }
+
+ _escapeHtml(str) {
+ if (typeof str !== 'string') return '';
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
+ }
+
+ _formatSize(size) {
+ if (!size && size !== 0) return '';
+ if (size < 1024) return size + ' B';
+ if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB';
+ return (size / (1024 * 1024)).toFixed(1) + ' MB';
+ }
+
+ _createOverlay() {
+ if (this._overlay) return;
+
+ const overlay = document.createElement('div');
+ overlay.id = 'filemgr-overlay';
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:1800;display:flex;flex-direction:column;background:var(--bg-color);color:var(--text-color);font-family:var(--font-primary);font-size:14px;height:100vh;';
+
+ // Inject responsive styles for the overlay
+ const styleId = 'filemgr-styles';
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ #filemgr-overlay.responsive .filemgr-main {
+ flex-direction: column !important;
+ }
+ #filemgr-overlay.responsive #filemgr-sidebar {
+ width: 100% !important;
+ max-height: 40vh;
+ }
+ #filemgr-overlay.responsive #filemgr-preview {
+ width: 100% !important;
+ }
+ #filemgr-overlay .filemgr-main {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+ }
+ @media (max-width: 600px) {
+ #filemgr-overlay .filemgr-main {
+ flex-direction: column !important;
+ }
+ #filemgr-overlay #filemgr-sidebar {
+ width: 100% !important;
+ max-height: 40vh;
+ border-right: none !important;
+ border-bottom: 1px solid var(--border-color);
+ }
+ #filemgr-overlay #filemgr-preview {
+ width: 100% !important;
+ flex: 1 !important;
+ min-height: 0;
+ }
+ #filemgr-action-bar {
+ flex-wrap: wrap;
+ gap: 6px;
+ }
+ #filemgr-action-bar button {
+ min-width: 44px;
+ min-height: 44px;
+ font-size: 0.9em;
+ }
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ // Toolbar
+ const toolbar = document.createElement('div');
+ toolbar.style.cssText = 'display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0;gap:8px;flex-wrap:wrap;';
+
+ const closeBtn = document.createElement('button');
+ closeBtn.textContent = '✕';
+ closeBtn.style.cssText = 'background:none;border:1px solid var(--border-color);color:var(--text-color);cursor:pointer;padding:4px 10px;border-radius:4px;font-family:inherit;font-size:1em;min-height:44px;touch-action:manipulation;';
+ closeBtn.onclick = () => this._closeOverlay();
+ toolbar.appendChild(closeBtn);
+
+ const titleSpan = document.createElement('span');
+ titleSpan.textContent = '📁 File Manager';
+ titleSpan.style.cssText = 'font-weight:bold;margin-right:8px;';
+ toolbar.appendChild(titleSpan);
+
+ // Breadcrumbs
+ const breadcrumbs = document.createElement('div');
+ breadcrumbs.style.cssText = 'display:flex;align-items:center;gap:4px;flex-wrap:wrap;font-size:0.9em;';
+ breadcrumbs.id = 'filemgr-breadcrumbs';
+
+ // Root breadcrumb
+ const rootSeg = document.createElement('span');
+ rootSeg.textContent = '/';
+ rootSeg.style.cssText = 'color:var(--accent-color);cursor:pointer;padding:2px 4px;border-radius:3px;';
+ rootSeg.onmouseenter = () => { rootSeg.style.background = 'var(--bg-tertiary)'; };
+ rootSeg.onmouseleave = () => { rootSeg.style.background = 'transparent'; };
+ rootSeg.onclick = () => this._navigateTo([]);
+ breadcrumbs.appendChild(rootSeg);
+
+ toolbar.appendChild(breadcrumbs);
+
+ // Search input
+ const searchInput = document.createElement('input');
+ searchInput.type = 'text';
+ searchInput.placeholder = '🔍 Filter files...';
+ searchInput.style.cssText = 'flex:1;min-width:120px;max-width:300px;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:4px;color:var(--text-color);font-family:var(--font-primary);font-size:0.9em;outline:none;';
+ searchInput.oninput = (e) => {
+ this._searchFilter = e.target.value.toLowerCase();
+ this._refreshFileList();
+ };
+ toolbar.appendChild(searchInput);
+
+ // v4.5.78: Batch operation buttons
+ const batchBar = document.createElement('div');
+ batchBar.style.cssText = 'display:flex;align-items:center;gap:4px;flex-wrap:wrap;';
+ const mkBtn = (label, title, action) => {
+ const b = document.createElement('button');
+ b.textContent = label;
+ b.title = title;
+ b.style.cssText = 'background:none;border:1px solid var(--border-color);color:var(--text-color);cursor:pointer;padding:4px 8px;border-radius:4px;font-family:inherit;font-size:0.85em;min-height:32px;touch-action:manipulation;';
+ b.onclick = () => action();
+ return b;
+ };
+ batchBar.appendChild(mkBtn('📋 Copy', 'Copy selected files', () => this._batchCopy()));
+ batchBar.appendChild(mkBtn('✂️ Cut', 'Cut selected files', () => this._batchCut()));
+ batchBar.appendChild(mkBtn('📥 Paste', 'Paste from clipboard', () => this._batchPaste()));
+ batchBar.appendChild(mkBtn('🗑️ Del', 'Delete selected files', () => this._batchDelete()));
+ batchBar.appendChild(mkBtn('📝 Rename', 'Rename selected file', () => this._batchRename()));
+ batchBar.appendChild(mkBtn('☐ All', 'Toggle select all', () => this._toggleSelectAll()));
+ // v4.6.35: tap-to-select mode — the only usable multi-select path on touch devices
+ const selModeBtn = mkBtn('☑ Select', 'Toggle tap-to-select mode (touch)', () => {
+ this._selectMode = !this._selectMode;
+ selModeBtn.style.background = this._selectMode ? 'var(--accent-color)' : 'none';
+ selModeBtn.style.color = this._selectMode ? '#fff' : 'var(--text-color)';
+ this._updateStatus(this._selectMode ? 'Select mode ON — tap rows to select' : 'Select mode OFF');
+ });
+ selModeBtn.setAttribute('aria-pressed', 'false');
+ batchBar.appendChild(selModeBtn);
+ toolbar.appendChild(batchBar);
+
+ // 2026-10-21: publish a VFS file to the user's WebDAV space was terminal-only.
+ const wdavBtn = document.createElement('button');
+ wdavBtn.textContent = '☁ WebDAV';
+ wdavBtn.title = 'Upload the selected/current file to your WebDAV space and get its public link';
+ wdavBtn.style.cssText = 'background:var(--bg-tertiary);border:1px solid var(--border-color);' +
+ 'color:var(--text-color);cursor:pointer;padding:4px 10px;border-radius:4px;' +
+ 'font-family:inherit;font-size:0.9em;min-height:32px;touch-action:manipulation;';
+ wdavBtn.onclick = () => this._uploadSelectionToWebdav();
+ toolbar.appendChild(wdavBtn);
+
+ overlay.appendChild(toolbar);
+
+ // Main area — wraps sidebar + preview in a responsive flex container
+ const mainArea = document.createElement('div');
+ mainArea.id = 'filemgr-main-container';
+ mainArea.className = 'filemgr-main';
+ mainArea.style.cssText = 'flex:1;overflow:hidden;display:flex;min-height:0;';
+
+ // Sidebar
+ const sidebar = document.createElement('div');
+ sidebar.id = 'filemgr-sidebar';
+ sidebar.style.cssText = 'width:25%;min-width:150px;overflow-y:auto;background:var(--bg-secondary);border-right:1px solid var(--border-color);';
+
+ mainArea.appendChild(sidebar);
+
+ // Preview pane
+ const preview = document.createElement('div');
+ preview.id = 'filemgr-preview';
+ preview.style.cssText = 'flex:1;overflow-y:auto;padding:0;';
+
+ mainArea.appendChild(preview);
+
+ overlay.appendChild(mainArea);
+
+ // Action bar
+ const actionBar = document.createElement('div');
+ actionBar.id = 'filemgr-action-bar';
+ actionBar.style.cssText = 'display:flex;align-items:center;padding:8px 12px;border-top:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0;gap:8px;flex-wrap:wrap;';
+
+ const statusSpan = document.createElement('span');
+ statusSpan.id = 'filemgr-status';
+ statusSpan.style.cssText = 'flex:1;color:var(--text-muted);font-size:0.85em;';
+ statusSpan.textContent = 'No file selected';
+ actionBar.appendChild(statusSpan);
+
+ const encryptBtn = document.createElement('button');
+ encryptBtn.textContent = '🔐 Encrypt';
+ encryptBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--warning-color);color:var(--warning-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ encryptBtn.onclick = () => this._actionEncrypt();
+ actionBar.appendChild(encryptBtn);
+
+ const compressBtn = document.createElement('button');
+ compressBtn.textContent = '📦 Zip';
+ compressBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--info-color);color:var(--info-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ compressBtn.onclick = () => this._actionCompress();
+ actionBar.appendChild(compressBtn);
+
+ const deleteBtn = document.createElement('button');
+ deleteBtn.textContent = '🗑 Delete';
+ deleteBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--error-color);color:var(--error-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ deleteBtn.onclick = () => this._actionDelete();
+ actionBar.appendChild(deleteBtn);
+
+ const downloadBtn = document.createElement('button');
+ downloadBtn.textContent = '⬇ Download';
+ downloadBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ downloadBtn.onclick = () => this._actionDownload();
+ actionBar.appendChild(downloadBtn);
+
+ const editBtn = document.createElement('button');
+ editBtn.textContent = '✏ Edit';
+ editBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--accent-color);color:var(--accent-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ editBtn.onclick = () => this._actionEdit();
+ actionBar.appendChild(editBtn);
+
+ const newFileBtn = document.createElement('button');
+ newFileBtn.textContent = '📄 New';
+ newFileBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--success-color);color:var(--success-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ newFileBtn.onclick = () => this._actionNewFile();
+ actionBar.appendChild(newFileBtn);
+
+ const previewToggleBtn = document.createElement('button');
+ previewToggleBtn.id = 'filemgr-toggle-raw';
+ previewToggleBtn.textContent = '📄 Raw';
+ previewToggleBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-muted);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ previewToggleBtn.onclick = () => this._togglePreviewMode();
+ actionBar.appendChild(previewToggleBtn);
+
+ overlay.appendChild(actionBar);
+
+ this._overlay = overlay;
+ return overlay;
+ }
+
+ _openOverlay() {
+ if (this._overlay) {
+ this._overlay.style.display = 'flex';
+ this._refreshFileList();
+ return;
+ }
+ const overlay = this._createOverlay();
+ document.body.appendChild(overlay);
+ this._navigateTo(this.currentPath);
+ }
+
+ _closeOverlay() {
+ if (this._overlay) {
+ this._overlay.style.display = 'none';
+ }
+ this._multiSelect.clear();
+ this._selectedFile = null;
+ this._selectMode = false;
+ }
+
+ _clearOverlay() {
+ if (this._overlay && this._overlay.parentNode) {
+ this._overlay.parentNode.removeChild(this._overlay);
+ }
+ this._overlay = null;
+ this._selectedFile = null;
+ this._multiSelect.clear();
+ this._selectMode = false;
+ this._searchFilter = '';
+ this._rawMode = false;
+ }
+
+ _navigateTo(pathParts) {
+ this.currentPath = pathParts || [];
+ this._selectedFile = null;
+ this._multiSelect.clear();
+ this._searchFilter = '';
+ const searchInput = this._overlay?.querySelector('input[type="text"]');
+ if (searchInput) searchInput.value = '';
+ this._updateBreadcrumbs();
+ this._refreshFileList();
+ this._clearPreview();
+ this._updateStatus('No file selected');
+ }
+
+ _updateBreadcrumbs() {
+ const breadcrumbs = document.getElementById('filemgr-breadcrumbs');
+ if (!breadcrumbs) return;
+
+ // Keep root segment, remove rest
+ while (breadcrumbs.children.length > 1) {
+ breadcrumbs.removeChild(breadcrumbs.lastChild);
+ }
+
+ let accumulated = '';
+ this.currentPath.forEach((part, i) => {
+ const sep = document.createElement('span');
+ sep.textContent = ' › ';
+ sep.style.cssText = 'color:var(--text-muted);margin:0 2px;';
+ breadcrumbs.appendChild(sep);
+
+ const seg = document.createElement('span');
+ seg.textContent = part;
+ seg.style.cssText = 'cursor:pointer;padding:2px 4px;border-radius:3px;color:var(--text-color);';
+ if (i === this.currentPath.length - 1) {
+ seg.style.color = 'var(--accent-color)';
+ seg.style.fontWeight = 'bold';
+ }
+ seg.onmouseenter = () => { seg.style.background = 'var(--bg-tertiary)'; };
+ seg.onmouseleave = () => { seg.style.background = 'transparent'; };
+ const pathUpTo = this.currentPath.slice(0, i + 1);
+ seg.onclick = () => this._navigateTo(pathUpTo);
+ breadcrumbs.appendChild(seg);
+ });
+ }
+
+ _refreshFileList() {
+ const sidebar = document.getElementById('filemgr-sidebar');
+ if (!sidebar) return;
+
+ // Read directory
+ const pathStr = '/' + this.currentPath.join('/');
+ const entries = this.terminal.fs.ls(pathStr) || [];
+
+ // Separate dirs and files
+ const dirs = entries.filter(e => e.type === 'dir');
+ const files = entries.filter(e => e.type !== 'dir');
+
+ // Prune selections that no longer exist here (deleted/moved elsewhere)
+ try {
+ const _live = new Set();
+ entries.forEach(e => _live.add(pathStr === '/' ? '/' + e.name : pathStr + '/' + e.name));
+ [...this._multiSelect].forEach(p => { if (!_live.has(p)) this._multiSelect.delete(p); });
+ } catch (_e) {}
+
+ // Sort alphabetically
+ dirs.sort((a, b) => a.name.localeCompare(b.name));
+ files.sort((a, b) => a.name.localeCompare(b.name));
+
+ // Apply search filter
+ let filteredDirs = dirs;
+ let filteredFiles = files;
+ if (this._searchFilter) {
+ const q = this._searchFilter;
+ filteredDirs = dirs.filter(e => e.name.toLowerCase().includes(q));
+ filteredFiles = files.filter(e => e.name.toLowerCase().includes(q));
+ }
+
+ const _scrollTop = sidebar.scrollTop;
+ sidebar.innerHTML = '';
+
+ if (filteredDirs.length === 0 && filteredFiles.length === 0) {
+ const emptyMsg = document.createElement('div');
+ emptyMsg.style.cssText = 'padding:20px;text-align:center;color:var(--text-muted);';
+ emptyMsg.textContent = this._searchFilter ? 'No matching files' : '(empty)';
+ sidebar.appendChild(emptyMsg);
+ this._itemCount = 0;
+ this._updateSelectionStatus();
+ return;
+ }
+
+ // Add directory items
+ filteredDirs.forEach(entry => {
+ const item = document.createElement('div');
+ const fullPath = pathStr === '/' ? '/' + entry.name : pathStr + '/' + entry.name;
+ item.dataset.path = fullPath;
+ item.dataset.type = 'dir';
+ item.style.cssText = 'display:flex;align-items:center;padding:6px 10px;cursor:pointer;border-bottom:1px solid var(--border-color);gap:6px;border-left:3px solid transparent;';
+ item.onmouseenter = () => { if (!item.dataset.selected) item.style.background = 'var(--bg-tertiary)'; };
+ item.onmouseleave = () => { if (!item.dataset.selected) item.style.background = 'transparent'; };
+ item.onclick = (e) => {
+ if (e.ctrlKey || e.metaKey || this._selectMode) { e.preventDefault(); this._toggleMulti(fullPath); return; }
+ const newPath = [...this.currentPath, entry.name];
+ this._navigateTo(newPath);
+ };
+
+ item.appendChild(this._makeCheckbox(fullPath, entry.name + '/'));
+
+ const icon = document.createElement('span');
+ icon.textContent = '📁';
+ item.appendChild(icon);
+
+ const nameSpan = document.createElement('span');
+ nameSpan.textContent = entry.name + '/';
+ nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
+ item.appendChild(nameSpan);
+
+ sidebar.appendChild(item);
+ });
+
+ // Add file items
+ filteredFiles.forEach(entry => {
+ const item = document.createElement('div');
+ const fullPath = pathStr === '/' ? '/' + entry.name : pathStr + '/' + entry.name;
+ const isSelected = this._selectedFile === fullPath;
+ const isMulti = this._multiSelect.has(fullPath);
+ item.dataset.path = fullPath;
+ item.dataset.type = 'file';
+ item.dataset.selected = (isSelected || isMulti) ? 'true' : 'false';
+ const bgColor = isMulti ? 'var(--bg-tertiary)' : 'transparent';
+ item.style.cssText = 'display:flex;align-items:center;padding:6px 10px;cursor:pointer;border-bottom:1px solid var(--border-color);gap:6px;background:' + bgColor + ';border-left:3px solid ' + (isSelected ? 'var(--accent-color)' : 'transparent') + ';';
+ item.onmouseenter = () => { if (!item.dataset.selected) item.style.background = 'var(--bg-tertiary)'; };
+ item.onmouseleave = () => { if (!item.dataset.selected) item.style.background = 'transparent'; };
+ item.onclick = (e) => {
+ // Ctrl/Cmd or Select-mode → toggle in the batch set (no full re-render)
+ if (e.ctrlKey || e.metaKey || this._selectMode) { e.preventDefault(); this._toggleMulti(fullPath); return; }
+ // Plain click → preview AND make it the single selected item
+ this._multiSelect.clear();
+ this._multiSelect.add(fullPath);
+ this._selectedFile = fullPath;
+ this._highlightSelected();
+ this._renderPreview(fullPath, this.terminal.fs.readFile(fullPath));
+ this._updateSelectionStatus();
+ };
+
+ item.appendChild(this._makeCheckbox(fullPath, entry.name));
+
+ const icon = document.createElement('span');
+ icon.textContent = '📄';
+ item.appendChild(icon);
+
+ const nameSpan = document.createElement('span');
+ nameSpan.textContent = entry.name;
+ nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
+ item.appendChild(nameSpan);
+
+ const sizeSpan = document.createElement('span');
+ sizeSpan.textContent = this._formatSize(entry.size);
+ sizeSpan.style.cssText = 'color:var(--text-muted);font-size:0.8em;white-space:nowrap;margin-left:4px;';
+ item.appendChild(sizeSpan);
+
+ sidebar.appendChild(item);
+ });
+
+ this._itemCount = filteredDirs.length + filteredFiles.length;
+ sidebar.scrollTop = _scrollTop;
+ this._highlightSelected();
+ this._updateSelectionStatus();
+ }
+
+ _makeCheckbox(path, label) {
+ const cb = document.createElement('input');
+ cb.type = 'checkbox';
+ cb.checked = this._multiSelect.has(path);
+ cb.setAttribute('aria-label', 'Select ' + label);
+ cb.style.cssText = 'width:20px;height:20px;min-width:20px;margin:0 6px 0 0;accent-color:var(--accent-color);cursor:pointer;touch-action:manipulation;';
+ cb.onclick = (e) => e.stopPropagation();
+ cb.onchange = () => this._toggleMulti(path);
+ return cb;
+ }
+
+ _toggleMulti(path) {
+ if (this._multiSelect.has(path)) this._multiSelect.delete(path);
+ else this._multiSelect.add(path);
+ this._highlightSelected();
+ this._updateSelectionStatus();
+ }
+
+ _updateSelectionStatus() {
+ const n = this._multiSelect.size;
+ if (n) { this._updateStatus(n + ' selected'); return; }
+ if (this._selectedFile) { this._updateStatus('Preview: ' + this._selectedFile.split('/').pop()); return; }
+ const total = this._itemCount || 0;
+ this._updateStatus(total + ' item' + (total === 1 ? '' : 's'));
+ }
+
+ _exists(path) {
+ const fs = this.terminal.fs;
+ try {
+ if (fs.readFile(path) !== null) return true;
+ if (typeof fs.readDir === 'function' && fs.readDir(path) !== null) return true;
+ if (typeof fs.stat === 'function' && fs.stat(path)) return true;
+ } catch (_e) {}
+ return false;
+ }
+
+ _highlightSelected() {
+ const sidebar = document.getElementById('filemgr-sidebar');
+ if (!sidebar) return;
+ const items = sidebar.querySelectorAll('[data-path]');
+ items.forEach(item => {
+ const path = item.dataset.path;
+ const inBatch = this._multiSelect.has(path);
+ const preview = this._selectedFile === path;
+ item.dataset.selected = (inBatch || preview) ? 'true' : 'false';
+ item.style.background = inBatch ? 'var(--bg-tertiary)' : 'transparent';
+ item.style.borderLeft = '3px solid ' + (preview ? 'var(--accent-color)' : 'transparent');
+ const cb = item.querySelector('input[type="checkbox"]');
+ if (cb) cb.checked = inBatch;
+ });
+ }
+
+ // v4.5.78: Batch operations
+ _getSelectedPaths() {
+ return [...this._multiSelect];
+ }
+
+ _batchCopy() {
+ const paths = this._getSelectedPaths();
+ if (!paths.length) { this._updateStatus('No files selected'); return; }
+ this._clipboard = { mode: 'copy', paths: [...paths] };
+ this._updateStatus('📋 Copied ' + paths.length + ' item(s)');
+ }
+
+ _batchCut() {
+ const paths = this._getSelectedPaths();
+ if (!paths.length) { this._updateStatus('No files selected'); return; }
+ this._clipboard = { mode: 'cut', paths: [...paths] };
+ this._updateStatus('✂️ Cut ' + paths.length + ' item(s)');
+ }
+
+ _batchPaste() {
+ if (!this._clipboard || !this._clipboard.paths.length) { this._updateStatus('Clipboard empty'); return; }
+ const fs = this.terminal.fs;
+ const dest = this.currentPath.length ? '/' + this.currentPath.join('/') : '/';
+ let count = 0, skipped = 0;
+ const failed = [];
+ this._clipboard.paths.forEach(src => {
+ const name = src.split('/').pop();
+ const destPath = dest === '/' ? '/' + name : dest + '/' + name;
+ if (destPath === src || destPath.startsWith(src + '/')) { skipped++; return; }
+ // Directory → existing directory would REPLACE the whole subtree silently; skip it.
+ let srcIsDir = false;
+ try { srcIsDir = typeof fs.readDir === 'function' && fs.readDir(src) !== null; } catch (_e) {}
+ if (this._exists(destPath) && srcIsDir) { skipped++; return; }
+ if (this._exists(destPath) && !confirm('Overwrite ' + destPath + '?')) { skipped++; return; }
+ try {
+ const ok = this._clipboard.mode === 'copy' ? fs.copy(src, destPath) : fs.move(src, destPath);
+ if (ok) count++; else failed.push(src);
+ } catch (_e) { failed.push(src); }
+ });
+ if (this._clipboard.mode === 'cut') {
+ this._clipboard = failed.length ? { mode: 'cut', paths: failed } : { mode: null, paths: [] };
+ }
+ this._multiSelect.clear();
+ this._selectedFile = null;
+ this._refreshFileList();
+ this._updateStatus('📥 Pasted ' + count + ' item(s)' + (skipped ? ' — ' + skipped + ' skipped' : '') + (failed.length ? ' — ' + failed.length + ' failed' : ''));
+ }
+
+ _batchDelete() {
+ const paths = this._getSelectedPaths();
+ if (!paths.length) { this._updateStatus('No files selected'); return false; }
+ if (!confirm('Delete ' + paths.length + ' item(s)?')) return false;
+ const fs = this.terminal.fs;
+ let count = 0, failed = 0;
+ paths.forEach(p => {
+ try {
+ const ok = typeof fs.rm === 'function' ? fs.rm(p) : fs.deleteFile(p);
+ if (ok) count++; else failed++;
+ } catch (_e) { failed++; }
+ });
+ this._multiSelect.clear();
+ this._selectedFile = null;
+ this._refreshFileList();
+ this._updateStatus('🗑️ Deleted ' + count + ' item(s)' + (failed ? ' — ' + failed + ' failed' : ''));
+ return count > 0;
+ }
+
+ _batchRename() {
+ const paths = this._getSelectedPaths();
+ if (!paths.length) { this._updateStatus('No item selected'); return; }
+ const fs = this.terminal.fs;
+ if (paths.length === 1) {
+ const path = paths[0];
+ const oldName = path.split('/').pop();
+ const newName = (prompt('Rename to:', oldName) || '').trim();
+ if (!newName || newName === oldName) return;
+ if (newName === '.' || newName === '..' || newName.includes('/') || newName.includes('\\')) {
+ this._updateStatus('❌ Invalid file name'); return;
+ }
+ const dir = path.substring(0, path.lastIndexOf('/')) || '/';
+ const newPath = dir === '/' ? '/' + newName : dir + '/' + newName;
+ if (this._exists(newPath)) { this._updateStatus('⚠ Destination already exists'); return; }
+ if (!fs.rename(path, newPath)) { this._updateStatus('❌ Rename failed'); return; }
+ this._selectedFile = newPath;
+ this._multiSelect.clear();
+ this._multiSelect.add(newPath);
+ this._refreshFileList();
+ this._updateStatus('📝 Renamed to ' + newName);
+ return;
+ }
+ const prefix = (prompt('Rename ' + paths.length + ' items — new prefix:', 'file') || '').trim();
+ if (!prefix) return;
+ let i = 0, ok = 0, skip = 0;
+ paths.forEach(p => {
+ const ext = (p.match(/\.[^./]+$/) || [''])[0];
+ const dir = p.substring(0, p.lastIndexOf('/')) || '/';
+ const dest = (dir === '/' ? '' : dir) + '/' + prefix + (++i) + ext;
+ if (dest === p || this._exists(dest)) { skip++; return; }
+ try { if (fs.rename(p, dest)) ok++; else skip++; } catch (_e) { skip++; }
+ });
+ this._multiSelect.clear();
+ this._selectedFile = null;
+ this._refreshFileList();
+ this._updateStatus('📝 Renamed ' + ok + ' item(s)' + (skip ? ' — ' + skip + ' skipped' : ''));
+ }
+
+ _toggleSelectAll() {
+ const sidebar = document.getElementById('filemgr-sidebar');
+ if (!sidebar) return;
+ const paths = [...sidebar.querySelectorAll('[data-path]')].map(i => i.dataset.path);
+ if (!paths.length) { this._updateSelectionStatus(); return; }
+ const all = paths.every(p => this._multiSelect.has(p));
+ if (all) {
+ paths.forEach(p => this._multiSelect.delete(p));
+ if (this._selectedFile && paths.includes(this._selectedFile)) this._selectedFile = null;
+ } else {
+ paths.forEach(p => this._multiSelect.add(p));
+ }
+ this._refreshFileList();
+ }
+
+ _renderPreview(path, content) {
+ const preview = document.getElementById('filemgr-preview');
+ if (!preview) return;
+
+ // Re-read content on each preview call
+ if (content === undefined) {
+ content = this.terminal.fs.readFile(path);
+ }
+
+ // For raw mode, show raw text regardless of type
+ if (this._rawMode && typeof content === 'string') {
+ preview.innerHTML = this._previewShell(path, '<div style="padding:12px"><div style="color:var(--text-muted);font-size:0.85em;margin-bottom:8px">RAW: ' + this._escapeHtml(path) + '</div><pre style="white-space:pre-wrap;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto">' + this._escapeHtml(content.substring(0, 10000)) + '</pre></div>');
+ this._wirePreviewToolbar(path);
+ this._updateStatus('Preview: ' + path);
+ return;
+ }
+
+ // Check file size for text files
+ const type = this._detectType(path);
+ const textTypes = ['md', 'text', 'json', 'code', 'csv', 'other'];
+ if (textTypes.includes(type) && typeof content === 'string' && content.length > 51200) {
+ content = content.substring(0, 51200);
+ preview.innerHTML = this._previewShell(path,
+ this._getPreviewHtml(path, content) +
+ '<div style="color:var(--warning-color);padding:8px 12px;border-top:1px solid var(--border-color);font-size:0.85em;">(truncated — file exceeds 50KB)</div>');
+ this._wirePreviewToolbar(path);
+ this._updateStatus('Preview: ' + path);
+ return;
+ }
+
+ preview.innerHTML = this._previewShell(path, this._getPreviewHtml(path, content));
+ this._wirePreviewToolbar(path);
+ this._updateStatus('Preview: ' + path);
+ }
+
+ // 2026-10-21: preview pane had no fullscreen and no share action.
+ _previewShell(path, innerHtml) {
+ return '<div class="fm-preview-toolbar" style="display:flex;gap:6px;justify-content:flex-end;' +
+ 'padding:6px 8px;border-bottom:1px solid var(--border-color);flex-shrink:0">' +
+ '<button class="fm-btn-fs" title="Fullscreen preview" aria-label="Fullscreen preview" style="background:var(--bg-tertiary);' +
+ 'border:1px solid var(--border-color);color:var(--text-color);padding:2px 8px;border-radius:4px;' +
+ 'cursor:pointer;font-size:0.85em;min-height:24px;">⛶</button>' +
+ '<button class="fm-btn-share" title="Copy share link" aria-label="Copy share link" style="background:var(--bg-tertiary);' +
+ 'border:1px solid var(--border-color);color:var(--text-color);padding:2px 8px;border-radius:4px;' +
+ 'cursor:pointer;font-size:0.85em;min-height:24px;">🔗</button>' +
+ '</div><div class="fm-preview-body" style="flex:1;min-height:0;overflow:auto">' + innerHtml + '</div>';
+ }
+
+ _previewShareUrl(path) {
+ const wdav = this.terminal && this.terminal.moduleManager && this.terminal.moduleManager.modules
+ ? this.terminal.moduleManager.modules.WebDAVModule : null;
+ const space = (wdav && wdav._resolveSpaceName && wdav._resolveSpaceName()) || '';
+ const clean = String(path).replace(/^\/+/, '');
+ return 'https://webx11.duckdns.org/spaces/' + space + '/' +
+ clean.split('/').map(encodeURIComponent).join('/');
+ }
+
+ // 2026-10-21: publish VFS file(s) to WebDAV (was terminal-only: webdav-push)
+ async _uploadSelectionToWebdav() {
+ let targets = this._getSelectedPaths ? this._getSelectedPaths() : [];
+ if (!targets || !targets.length) targets = this._selectedFile ? [this._selectedFile] : [];
+ if (!targets.length) { this._updateStatus('☁ Select a file first'); return; }
+ const wdav = this.terminal && this.terminal.moduleManager && this.terminal.moduleManager.modules
+ ? this.terminal.moduleManager.modules.WebDAVModule : null;
+ if (!wdav) { this._updateStatus('☁ WebDAV module not loaded'); return; }
+ if (!wdav.config || !wdav.config.url) { try { await wdav._autoBootstrap(); } catch (e) {} }
+ if (!wdav.config || !wdav.config.url) { this._updateStatus('☁ WebDAV not configured — run webdav-config'); return; }
+ for (const p of targets) {
+ const clean = String(p).replace(/^\/+/, '');
+ try {
+ await this.terminal.security.checkAndRun('webdav-push ' + clean);
+ const shareUrl = this._previewShareUrl(clean);
+ this._updateStatus('☁ Uploaded ' + clean + ' → ' + shareUrl);
+ if (this.terminal.print) this.terminal.print('☁ WebDAV: ' + shareUrl, 'success');
+ } catch (e) {
+ this._updateStatus('☁ Upload failed: ' + (e && e.message ? e.message : e));
+ }
+ }
+ }
+
+ _wirePreviewToolbar(path) {
+ const pane = document.getElementById('filemgr-preview');
+ if (!pane) return;
+ const fsBtn = pane.querySelector('.fm-btn-fs');
+ const shBtn = pane.querySelector('.fm-btn-share');
+ if (fsBtn) fsBtn.onclick = () => {
+ const body = pane.querySelector('.fm-preview-body') || pane;
+ if (document.fullscreenElement) { document.exitFullscreen(); return; }
+ if (body.requestFullscreen) body.requestFullscreen().catch(() => this._updateStatus('⛶ Fullscreen blocked by this browser'));
+ else if (body.webkitRequestFullscreen) body.webkitRequestFullscreen();
+ else this._updateStatus('⛶ Fullscreen not supported here');
+ };
+ if (shBtn) shBtn.onclick = () => {
+ const url = this._previewShareUrl(path);
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(url)
+ .then(() => this._updateStatus('🔗 Link copied: ' + url))
+ .catch(() => prompt('Copy share link:', url));
+ } else {
+ prompt('Copy share link:', url);
+ }
+ };
+ }
+
+ _renderPreviewError(msg) {
+ const preview = document.getElementById('filemgr-preview');
+ if (!preview) return;
+ preview.innerHTML = '<div style="color:var(--error-color);padding:20px;font-size:1em">' + this._escapeHtml(msg) + '</div>';
+ this._updateStatus('Error');
+ }
+
+ _clearPreview() {
+ const preview = document.getElementById('filemgr-preview');
+ if (preview) preview.innerHTML = '';
+ }
+
+ _updateStatus(msg) {
+ const status = document.getElementById('filemgr-status');
+ if (status) status.textContent = msg;
+ }
+
+ _actionDelete() {
+ // Delegate to the batch path so checkbox selections are honoured, not just the previewed file.
+ if (!this._getSelectedPaths().length && this._selectedFile) {
+ this._multiSelect.add(this._selectedFile);
+ }
+ // Only clear the preview when something was actually deleted (not on cancel).
+ if (this._batchDelete()) this._clearPreview();
+ }
+
+ _actionDownload() {
+ if (!this._selectedFile) {
+ this._updateStatus('No file selected to download');
+ return;
+ }
+ const content = this.terminal.fs.readFile(this._selectedFile);
+ if (content === null || content === false) {
+ this._updateStatus('❌ File not found');
+ return;
+ }
+ const fileName = this._selectedFile.split('/').pop() || 'file';
+ const blob = new Blob([content], { type: 'application/octet-stream' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = fileName;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ this._updateStatus('⬇ Downloaded: ' + fileName);
+ }
+
+ _actionEdit() {
+ if (!this._selectedFile) {
+ this._updateStatus('No file selected to edit');
+ return;
+ }
+ const path = this._selectedFile;
+ // Close file manager first so code editor modal (z-index:1000) is visible above
+ this._closeOverlay();
+ if (typeof webx11 !== 'undefined' && webx11 && webx11.dispatchCommand) {
+ setTimeout(() => {
+ webx11.dispatchCommand('edit', [path]);
+ }, 100);
+ this._updateStatus('✏ Opened in editor: ' + path);
+ } else {
+ this._updateStatus('❌ Editor not available');
+ }
+ }
+
+ _togglePreviewMode() {
+ this._rawMode = !this._rawMode;
+ const btn = document.getElementById('filemgr-toggle-raw');
+ if (btn) {
+ btn.textContent = this._rawMode ? '📄 Rendered' : '📄 Raw';
+ btn.style.borderColor = this._rawMode ? 'var(--accent-color)' : 'var(--border-color)';
+ btn.style.color = this._rawMode ? 'var(--accent-color)' : 'var(--text-muted)';
+ }
+ if (this._selectedFile) {
+ const content = this.terminal.fs.readFile(this._selectedFile);
+ this._renderPreview(this._selectedFile, content);
+ }
+ }
+
+ _actionEncrypt() {
+ if (!this._selectedFile) {
+ this._updateStatus('No file selected');
+ return;
+ }
+ if (this._selectedFile.endsWith('.enc')) {
+ this._updateStatus('⚠ Already encrypted (.enc)');
+ return;
+ }
+ const passphrase = prompt('Enter passphrase for AES-256-GCM encryption:');
+ if (!passphrase || passphrase.length < 4) {
+ this._updateStatus('❌ Passphrase too short (min 4 chars)');
+ return;
+ }
+ // Delegate to cryptogzip module's encrypt command
+ if (typeof webx11 !== 'undefined' && webx11.executeCommand) {
+ webx11.executeCommand('encrypt ' + this._selectedFile + ' ' + passphrase);
+ this._updateStatus('🔐 Encrypted: ' + this._selectedFile + '.enc');
+ this._refreshFileList();
+ }
+ }
+
+ _actionCompress() {
+ if (!this._selectedFile) {
+ this._updateStatus('No file selected');
+ return;
+ }
+ if (this._selectedFile.endsWith('.gz')) {
+ this._updateStatus('⚠ Already compressed (.gz)');
+ return;
+ }
+ if (typeof webx11 !== 'undefined' && webx11.executeCommand) {
+ webx11.executeCommand('gzip ' + this._selectedFile);
+ this._updateStatus('📦 Compressed: ' + this._selectedFile + '.gz');
+ this._refreshFileList();
+ }
+ }
+
+ _actionNewFile() {
+ const name = prompt('New file name (e.g. myfile.txt):');
+ if (!name) return;
+ const pathStr = '/' + this.currentPath.join('/');
+ const cleanPath = pathStr === '/' ? '/' + name : pathStr + '/' + name;
+ const content = prompt('Enter file content (optional):') || '';
+ const ok = this.terminal.fs.writeFile(cleanPath, content);
+ if (ok) {
+ this._updateStatus('✅ Created: ' + cleanPath + ' (' + content.length + ' chars)');
+ this._refreshFileList();
+ } else {
+ this._updateStatus('❌ Failed to create file');
+ }
+ }
+}
+
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new FileManagerModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/filemgr.js');
+}
diff --git a/.bkp-fix-20261023_143429/health.php b/.bkp-fix-20261023_143429/health.php
new file mode 100644
index 0000000..5a0048f
--- /dev/null
+++ b/.bkp-fix-20261023_143429/health.php
@@ -0,0 +1,20 @@
+<?php
+require_once __DIR__ . '/config.php';
+
+$errors = load_json(ERRORS_FILE);
+$sessions = load_json(SESSIONS_FILE);
+
+$lastError = !empty($errors) ? end($errors) : null;
+
+json_response([
+ 'success' => true,
+ 'status' => 'ok',
+ 'uptime' => filemtime(__FILE__) ? (time() - filemtime(__FILE__)) : 0,
+ 'total_errors' => count($errors),
+ 'total_sessions' => count($sessions),
+ 'errors_file_size_kb' => file_exists(ERRORS_FILE) ? round(filesize(ERRORS_FILE) / 1024, 1) : 0,
+ 'sessions_file_size_kb' => file_exists(SESSIONS_FILE) ? round(filesize(SESSIONS_FILE) / 1024, 1) : 0,
+ 'last_ingest' => $lastError ? ($lastError['ingested_at'] ?? null) : null,
+ 'last_ingest_message' => $lastError ? substr($lastError['message'] ?? '', 0, 100) : null,
+ 'generated_at' => date('c'),
+]);
diff --git a/.bkp-fix-20261023_143429/help.html b/.bkp-fix-20261023_143429/help.html
new file mode 100644
index 0000000..0d70e08
--- /dev/null
+++ b/.bkp-fix-20261023_143429/help.html
@@ -0,0 +1,2147 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<meta name="theme-color" content="#0a0a14">
+<title>WebX11 — Help</title>
+<script>!function(){var v=localStorage.getItem('webx11_cache_ver');if(!v)return;var m=v.match(/v([\d.]+)/);if(!m)return;var ver='v'+m[1];document.title='WebX11 '+ver+' \u2014 Help';var els=document.querySelectorAll('.badge-v');for(var i=0;i<els.length;i++)els[i].textContent=ver;var qr=document.querySelector('.quick-ref-title');if(qr)qr.textContent='WebX11 '+ver;var ov=document.querySelector('#overview p');if(ov)ov.textContent=ov.textContent.replace(/v[\d.]+/,ver)}()</script>
+<style>
+/* ── Reset & Base ── */
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
+:root{
+ --bg:#0a0a14;
+ --bg2:#141428;
+ --bg3:#1c1c36;
+ --bg4:#24244a;
+ --text:#e8e8ff;
+ --text2:#b0b0cc;
+ --accent:#818cf8;
+ --accent2:#a78bfa;
+ --green:#34d399;
+ --cyan:#22d3ee;
+ --amber:#fbbf24;
+ --rose:#fb7185;
+ --pink:#e879f9;
+ --border:#3a3a5a;
+ --radius:12px;
+ --font-mono:'JetBrains Mono','Fira Code','Cascadia Code','Courier New',monospace;
+ --font-sans:-apple-system,BlinkMacSystemFont,'Segoe UI','Inter','SF Pro',Roboto,sans-serif
+}
+html{scroll-behavior:smooth}
+body{background:var(--bg);color:var(--text);font-family:var(--font-sans);line-height:1.8;font-size:17px;-webkit-font-smoothing:antialiased}
+@media(max-width:600px){body{font-size:15px}}
+
+/* ── Header ── */
+header{
+ position:sticky;top:0;z-index:100;
+ background:rgba(10,10,20,.85);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
+ border-bottom:1px solid var(--border);padding:0
+}
+header::after{
+ content:'';position:absolute;bottom:-1px;left:10%;right:10%;height:1px;
+ background:linear-gradient(90deg,transparent,var(--accent),transparent);opacity:.5
+}
+.header-inner{max-width:960px;margin:0 auto;padding:20px 24px 16px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px}
+.header-left{display:flex;align-items:center;gap:14px}
+.header-logo{width:36px;height:36px;border-radius:8px;background:linear-gradient(135deg,var(--accent),var(--pink));display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff;flex-shrink:0}
+h1{font-size:1.35em;font-weight:700;letter-spacing:-.02em;color:#fff}
+h1 span{color:var(--accent2)}
+.header-meta{display:flex;align-items:center;gap:10px;font-size:.8em;color:var(--text2)}
+.badge{
+ display:inline-block;padding:3px 10px;border-radius:20px;font-size:.7em;font-weight:600;
+ letter-spacing:.03em;text-transform:uppercase
+}
+.badge-v{background:rgba(124,92,252,.2);color:var(--accent2);border:1px solid rgba(124,92,252,.3)}
+.badge-new{background:rgba(52,211,153,.15);color:var(--green);border:1px solid rgba(52,211,153,.25)}
+
+/* ── Nav ── */
+nav{
+ position:sticky;top:74px;z-index:99;
+ background:rgba(10,10,20,.7);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
+ border-bottom:1px solid var(--border);padding:0;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none
+}
+nav::-webkit-scrollbar{display:none}
+.nav-inner{max-width:960px;margin:0 auto;padding:8px 24px;display:flex;gap:4px;flex-wrap:nowrap}
+.nav-link{
+ flex-shrink:0;color:var(--text2);text-decoration:none;padding:6px 14px;border-radius:8px;
+ font-size:.78em;font-weight:500;transition:all .15s;white-space:nowrap
+}
+.nav-link:hover{color:#fff;background:rgba(124,92,252,.12)}
+
+/* ── Main ── */
+main{max-width:960px;margin:0 auto;padding:32px 24px 80px}
+
+/* ── Search ── */
+.search-box{
+ display:flex;gap:8px;margin-bottom:40px;
+ background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);
+ padding:4px;transition:border-color .2s
+}
+.search-box:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px rgba(124,92,252,.15)}
+.search-box input{
+ flex:1;background:transparent;border:none;color:var(--text);font-family:var(--font-mono);
+ font-size:.9em;outline:none;padding:10px 14px
+}
+.search-box input::placeholder{color:var(--text2);opacity:.6}
+.search-box button{
+ background:var(--accent);color:#fff;border:none;padding:8px 20px;border-radius:8px;
+ font-size:.8em;font-weight:600;cursor:pointer;transition:opacity .15s;font-family:inherit
+}
+.search-box button:hover{opacity:.85}
+.search-result-msg{color:var(--amber);font-size:.85em;margin-top:8px;display:none}
+
+/* ── Sections ── */
+section{margin-bottom:56px;scroll-margin-top:130px}
+h2{
+ font-size:1.35em;font-weight:700;color:#fff;margin-bottom:20px;letter-spacing:-.01em;
+ display:flex;align-items:center;gap:10px
+}
+h2::after{content:'';flex:1;height:1px;background:linear-gradient(90deg,var(--border),transparent)}
+h3{
+ font-size:1.05em;font-weight:600;color:var(--text);margin:28px 0 12px;
+ display:flex;align-items:center;gap:8px
+}
+h4{font-size:.92em;font-weight:600;color:var(--accent2);margin:20px 0 8px}
+p{margin-bottom:14px;color:var(--text);line-height:1.8}
+ul,ol{margin-bottom:14px;padding-left:22px;color:var(--text)}
+li{margin-bottom:6px}
+a{color:var(--accent2);text-decoration:none;transition:color .15s}
+a:hover{color:#fff}
+
+/* ── Tables ── */
+.table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;margin:16px 0;border-radius:var(--radius);border:1px solid var(--border)}
+table{width:100%;border-collapse:collapse;font-size:.88em}
+th,td{padding:10px 14px;text-align:left;border-bottom:1px solid var(--border)}
+th{background:var(--bg3);color:var(--accent2);font-weight:600;font-size:.82em;text-transform:uppercase;letter-spacing:.04em}
+tr:last-child td{border-bottom:none}
+tr:hover td{background:rgba(124,92,252,.04)}
+code{
+ font-family:var(--font-mono);font-size:.88em;padding:2px 7px;border-radius:5px;
+ background:rgba(124,92,252,.12);color:var(--accent2);border:1px solid rgba(124,92,252,.15)
+}
+pre code{background:transparent;border:none;padding:0;color:var(--text);font-size:.88em}
+
+/* ── Pre / Code Blocks ── */
+pre{
+ background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);
+ padding:16px 20px;overflow-x:auto;margin:16px 0;font-family:var(--font-mono);font-size:.85em;line-height:1.6
+}
+pre .cmt{color:var(--text2);opacity:.6} /* comment style */
+
+/* ── Callouts ── */
+.callout{
+ padding:16px 20px;border-radius:var(--radius);margin:16px 0;
+ border-left:3px solid;position:relative
+}
+.callout::before{content:'';position:absolute;inset:0;border-radius:var(--radius);opacity:.06;pointer-events:none}
+.callout-success{border-color:var(--green)}.callout-success::before{background:var(--green)}
+.callout-info{border-color:var(--cyan)}.callout-info::before{background:var(--cyan)}
+.callout-warning{border-color:var(--amber)}.callout-warning::before{background:var(--amber)}
+.callout-error{border-color:var(--rose)}.callout-error::before{background:var(--rose)}
+.callout-ollama{border-color:var(--pink)}.callout-ollama::before{background:var(--pink)}
+.callout-title{font-weight:700;margin-bottom:6px;display:block;font-size:.92em}
+
+/* ── Feature Grid ── */
+.feature-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;margin:16px 0}
+.feature-card{
+ background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);
+ padding:16px;transition:border-color .2s,transform .15s
+}
+.feature-card:hover{border-color:rgba(124,92,252,.4);transform:translateY(-1px)}
+.feature-card h4{margin:0 0 6px;font-size:.88em}
+.feature-card p{font-size:.82em;color:var(--text2);margin:0;line-height:1.5}
+
+/* ── Quick Ref ── */
+.quick-ref{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:24px;margin:16px 0}
+.quick-ref-title{color:var(--accent2);font-weight:700;text-align:center;margin-bottom:16px;font-size:.95em;letter-spacing:.02em;text-transform:uppercase}
+.quick-ref-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px}
+.quick-ref-item{background:var(--bg3);padding:10px 12px;border-radius:8px;border:1px solid var(--border)}
+.quick-ref-item .cmd{color:var(--accent2);font-family:var(--font-mono);font-weight:600;font-size:.85em;display:block}
+.quick-ref-item .desc{color:var(--text2);font-size:.78em;margin-top:3px}
+
+/* ── Version Items ── */
+.version-item{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin:12px 0;transition:border-color .2s}
+.version-item:hover{border-color:rgba(124,92,252,.3)}
+.version-item .v-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;flex-wrap:wrap;gap:6px}
+.version-item .v-num{color:var(--accent2);font-weight:700;font-size:.95em}
+.version-item .v-date{color:var(--text2);font-size:.82em}
+.version-item ul{margin:0;padding-left:18px}
+.version-item li{margin-bottom:4px;font-size:.88em;color:var(--text)}
+
+/* ── Footer ── */
+footer{
+ text-align:center;padding:32px 24px;border-top:1px solid var(--border);
+ color:var(--text2);font-size:.82em
+}
+footer .links{display:flex;justify-content:center;gap:20px;margin-bottom:14px;flex-wrap:wrap}
+footer .links a{color:var(--text);font-weight:500}
+footer .links a:hover{color:var(--accent2)}
+footer .tagline{color:var(--accent2);margin-top:10px;font-weight:500}
+
+/* ── Back to Top ── */
+.back-top{
+ position:fixed;bottom:24px;right:24px;
+ background:var(--bg3);border:1px solid var(--border);color:var(--text2);
+ width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;
+ font-size:18px;cursor:pointer;opacity:0;pointer-events:none;transition:all .2s;z-index:50;
+ backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);text-decoration:none
+}
+.back-top.visible{opacity:1;pointer-events:auto}
+.back-top:hover{background:var(--accent);color:#fff;border-color:var(--accent);transform:translateY(-2px)}
+
+/* ── Responsive ── */
+@media(max-width:600px){
+ .header-inner{padding:16px 16px 12px}
+ .header-meta{display:none}
+ .nav-inner{padding:6px 12px;gap:2px}
+ .nav-link{padding:5px 10px;font-size:.74em}
+ main{padding:20px 16px 60px}
+ h2{font-size:1.15em}
+ .feature-grid{grid-template-columns:1fr}
+ .quick-ref-grid{grid-template-columns:1fr 1fr}
+ pre{font-size:.8em;padding:12px 14px}
+ th,td{padding:8px 10px;font-size:.82em}
+}
+@media(max-width:400px){
+ .quick-ref-grid{grid-template-columns:1fr}
+ .badge-new{display:none}
+}
+
+/* ── Doc Tabs ── */
+.doc-tabs{display:flex;gap:0;margin-bottom:32px;border-bottom:1px solid var(--border);flex-wrap:wrap}
+.doc-tabs input{display:none}
+.doc-tabs label{
+ flex-shrink:0;padding:12px 20px;cursor:pointer;font-size:.82em;font-weight:600;
+ color:var(--text2);border-bottom:2px solid transparent;transition:all .15s;
+ font-family:var(--font-sans);user-select:none;-webkit-user-select:none
+}
+.doc-tabs label:hover{color:var(--text)}
+.doc-tabs input:checked+label{color:var(--accent2);border-color:var(--accent2)}
+.doc-tab{display:none}
+.doc-tab.active{display:block}
+
+</style>
+</head>
+<body>
+
+<header>
+ <div class="header-inner">
+ <div class="header-left">
+ <div class="header-logo">⚡</div>
+ <h1>WebX11 <span>Docs</span></h1>
+ </div>
+ <div class="header-meta">
+ <span class="badge badge-v">v4.5.22</span>
+ <span class="badge badge-new">38 core · ext</span>
+ <span style="color:var(--text2)">160+ commands</span>
+ </div>
+ </div>
+</header>
+
+<nav>
+ <div class="nav-inner">
+ <a href="#overview" class="nav-link">Overview</a>
+ <a href="#start" class="nav-link">Quick Start</a>
+ <a href="#commands" class="nav-link">Commands</a>
+ <a href="#filesystem" class="nav-link">Filesystem</a>
+ <a href="#ai" class="nav-link">AI Agent</a>
+ <a href="skills.html" class="nav-link">Skills → dynamic</a>
+ <a href="#ollama" class="nav-link">Ollama</a>
+ <a href="#web" class="nav-link">Search & Web</a>
+ <a href="#agents" class="nav-link">Agents</a>
+ <a href="#a2a-contracts" class="nav-link">A2A</a>
+ <a href="#neural-vfs" class="nav-link">Neural VFS</a>
+ <a href="#server-monitor" class="nav-link">Monitor</a>
+ <a href="#debug-panel" class="nav-link">Debug</a>
+ <a href="#sandbox" class="nav-link">Sandbox</a>
+ <a href="#memory-graph" class="nav-link">Memory</a>
+ <a href="#world-model" class="nav-link">World Model</a>
+ <a href="#security" class="nav-link">Security</a>
+ <a href="#trouble" class="nav-link">Troubleshoot</a>
+ <a href="#ref" class="nav-link">Quick Ref</a>
+ <a href="#history" class="nav-link">Changelog</a>
+ </div>
+</nav>
+
+<main>
+<div class="doc-tabs">
+ <input type="radio" name="doc-layer" id="tab-basics" checked>
+ <label for="tab-basics">🌱 Start in 5 Min</label>
+ <input type="radio" name="doc-layer" id="tab-guide">
+ <label for="tab-guide">📘 User Guide</label>
+ <input type="radio" name="doc-layer" id="tab-dev">
+ <label for="tab-dev">🔧 Dev Reference</label>
+</div>
+<div class="doc-tab active" id="content-basics">
+ <section id="basics-overview">
+ <h2>🚀 Welcome to newOS4</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">🎮 New here?</span>
+ Type <code>tutorial</code> in the terminal to start a 2-minute interactive tour!
+ </div>
+ <div class="feature-grid">
+ <div class="feature-card"><h4>🖥️ Terminal OS</h4><p>Full virtual filesystem with ls, cd, cat, touch, edit, find, grep and 50+ Unix-like commands.</p></div>
+ <div class="feature-card"><h4>🤖 AI Assistant</h4><p>Built-in AI agent with tool calling, skills, memory, and 28 tools. Just type <code>ai "your question"</code>.</p></div>
+ <div class="feature-card"><h4>🧩 Module System</h4><p>50 modules across 3 layers — <a href="modules.html">Browse on Modules page →</a> (dynamic, searchable, live status)</p></div>
+ <div class="feature-card"><h4>📚 Skills</h4><p>Browse live on the <a href="skills.html">Skills page →</a> (searchable, filterable, no hardcoded lists).</p></div>
+ </div>
+ </section>
+
+ <section id="basics-first">
+ <h3>🔤 First 6 Commands</h3>
+ <div class="quick-ref">
+ <div class="quick-ref-grid">
+ <div class="quick-ref-item"><span class="cmd">help</span><span class="desc">Show all commands</span></div>
+ <div class="quick-ref-item"><span class="cmd">ls</span><span class="desc">List files</span></div>
+ <div class="quick-ref-item"><span class="cmd">cat</span><span class="desc">View file content</span></div>
+ <div class="quick-ref-item"><span class="cmd">clear</span><span class="desc">Clear terminal</span></div>
+ <div class="quick-ref-item"><span class="cmd">touch</span><span class="desc">Create a file</span></div>
+ <div class="quick-ref-item"><span class="cmd">ai</span><span class="desc">Ask the AI assistant</span></div>
+ </div>
+ </div>
+ <h3>📋 Workflow</h3>
+ <ol style="margin-bottom:14px;padding-left:22px;color:var(--text)">
+ <li>Type <code>help</code> — see every available command</li>
+ <li>Type <code>ls</code> — browse files in your virtual drive</li>
+ <li>Type <code>touch hello.txt</code> — create a file</li>
+ <li>Type <code>edit hello.txt</code> — write something and save</li>
+ <li>Type <code>ai "summarize this"</code> — let AI analyze your files</li>
+ <li>Type <code>status</code> — check system health</li>
+ </ol>
+ </section>
+
+ <section>
+ <h3>🔧 Quick Start</h3>
+ <p>Type <code>help</code> to see all 130+ commands grouped by category. Use <code>help &lt;command&gt;</code> for details on a specific one.</p>
+ <p>The <strong>📋 Quick Commands</strong> panel (Ctrl+K) has clickable cards for common tasks.</p>
+ <p>For the full reference, switch to the <strong>📘 User Guide</strong> tab — it covers every module, filesystem, AI, skills, configuration, and all extended modules in detail.</p>
+ </section>
+</div>
+ <div class="search-box">
+ <input type="text" id="search-input" placeholder="Search docs…" spellcheck="false">
+ <button onclick="searchDocs()">Search</button>
+ </div>
+ <div class="search-result-msg" id="search-result-msg"></div>
+
+ <div class="doc-tab" id="content-dev">
+ <section id="dev-api">
+ <h2>🔧 Module Development API</h2>
+ <h3>Ext Module Structure</h3>
+ <p>Modules are ES6 classes with 3 required and 1 optional method:</p>
+ <pre><code>class MyModule {
+ constructor() { this.commands = {}; this.terminal = null; }
+ requirements() { return { met: true, missing: [] }; }
+ getHelpInfo() { return { 'mycmd': 'mycmd — What it does' }; }
+ registerCommands() { this.commands['mycmd'] = (args) => { /* ... */ }; }
+}
+// Auto-register:
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new MyModule(), 'url');
+}</code></pre>
+
+ <h3>VFS (Virtual FileSystem) API</h3>
+ <p>Access via <code>this.terminal.fs</code>:</p>
+ <div class="table-wrap"><table>
+ <tr><th>Method</th><th>Returns</th><th>Description</th></tr>
+ <tr><td><code>readFile(path)</code></td><td>string or null</td><td>Read file content</td></tr>
+ <tr><td><code>writeFile(path, content)</code></td><td>boolean</td><td>Write string to file</td></tr>
+ <tr><td><code>ls(path)</code></td><td>array</td><td>List directory entries</td></tr>
+ <tr><td><code>mkdir(path)</code></td><td>boolean</td><td>Create directory</td></tr>
+ <tr><td><code>rm(path)</code></td><td>boolean</td><td>Remove file</td></tr>
+ <tr><td><code>rmdir(path)</code></td><td>boolean</td><td>Remove empty directory</td></tr>
+ <tr><td><code>touch(path)</code></td><td>boolean</td><td>Create empty file</td></tr>
+ <tr><td><code>cd(path)</code></td><td>boolean</td><td>Change current directory</td></tr>
+ <tr><td><code>pwd()</code></td><td>string</td><td>Current directory path</td></tr>
+ <tr><td><code>mv(old, new)</code></td><td>boolean</td><td>Move/rename file</td></tr>
+ <tr><td><code>cp(src, dst)</code></td><td>boolean</td><td>Copy file</td></tr>
+ </table></div>
+
+ <h3>AI Tool Definition Format</h3>
+ <p>Add tools to <code>module-aiassist.js</code> in <code>getToolDefinitions()</code> (around line 300):</p>
+ <pre><code>{
+ type: 'function',
+ function: {
+ name: 'my_tool',
+ description: 'What it does — AI reads this to decide when to call it.',
+ parameters: { type: 'object', properties: {
+ paramName: { type: 'string', description: 'What this param does' }
+ }, required: ['paramName'] }
+ }
+}</code></pre>
+ <p>Then add a <code>case 'my_tool':</code> handler in <code>runTool()</code> (around line 1000). Return <code>JSON.stringify(...)</code>.</p>
+ </section>
+
+ <section id="dev-skills">
+ <h3>Skill Format</h3>
+ <p>Skills are markdown files with YAML frontmatter, stored in <code>/skills/</code> in VFS:</p>
+ <pre><code>---
+name: my-skill
+description: Brief description
+tags: [tag1, tag2]
+---
+# Skill Body
+
+Instructions, examples, and workflows here.</code></pre>
+ </section>
+
+ <section id="dev-ollama">
+ <h3>OllamaManager API</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Property/Method</th><th>Description</th></tr>
+ <tr><td><code>.endpoint</code></td><td>Current Ollama API URL</td></tr>
+ <tr><td><code>.currentModel</code></td><td>Active model name</td></tr>
+ <tr><td><code>.models</code></td><td>Array of available models</td></tr>
+ <tr><td><code>.connected</code></td><td>Boolean connection state</td></tr>
+ <tr><td><code>.connect()</code></td><td>Connect to Ollama, load models</td></tr>
+ <tr><td><code>.chatWithTools()</code></td><td>AI chat with tool calling support</td></tr>
+ </table></div>
+ </section>
+
+ <section id="dev-global">
+ <h3>window.webx11 Global</h3>
+ <p>The terminal instance is exposed globally as <code>window.webx11</code> (aliased as <code>webx11</code>). This gives access to everything:</p>
+ <pre><code>webx11.fs // VFS instance
+webx11.moduleManager // Module registry
+webx11.ollamaManager // Ollama connection
+webx11.security // Security manager
+webx11.executeCommand(cmd) // Run a terminal command programmatically
+webx11.print(text, type) // Print to terminal output
+webx11.tutorialModule // Tutorial progress (if active)
+webx11.inputField // DOM input element</code></pre>
+ </section>
+</div>
+<div class="doc-tab" id="content-guide">
+ <!-- ── Overview ── -->
+ <section id="overview">
+ <h2>⛅ Overview</h2>
+ <p>WebX11 v4.5.24 is a browser-based Unix-like terminal — dynamic modules, AI assistant (Ollama), persistent virtual filesystem, web search, voice input, code editor, and a full skill system. Everything runs client-side in your browser.</p>
+ <div class="feature-grid">
+ <div class="feature-card"><h4>🧩 18 Core + 14 Ext</h4><p>Modules loaded at startup. Shell with pipes, redirects, vars, and scripts.</p></div>
+ <div class="feature-card"><h4>🤖 Agent with Tools</h4><p>28 tools — file ops, web search, skills, modules. Destructive action confirmations with inline bar + countdown.</p></div>
+ <div class="feature-card"><h4>📚 Skills System</h4><p>Persistent procedural memory. Create, patch, search, export skills for repeatable workflows.</p></div>
+ <div class="feature-card"><h4>💾 VFS + Sessions</h4><p>Virtual filesystem persists in localStorage. Save/load/export sessions.</p></div>
+ <div class="feature-card"><h4>🎤 Voice Input</h4><p>Speech recognition in the terminal. Works in Chrome, Edge, Safari.</p></div>
+ <div class="feature-card"><h4>🌐 Web Search</h4><p>SearXNG + DuckDuckGo fallback + URL fetch/extract. All built-in.</p></div>
+ <div class="feature-card"><h4>☁️ WebDAV Sync</h4><p>Push, pull, backup VFS to any WebDAV server.</p></div>
+ <div class="feature-card"><h4>📱 Mobile First</h4><p>Touch-optimized. <code>srcdoc</code> HTML preview works on mobile.</p></div>
+ <div class="feature-card"><h4>✏️ Text Editor</h4><p>Inline VFS editor — <code>edit &lt;path&gt;</code> opens a modal with save/cancel.</p></div>
+ <div class="feature-card"><h4>🔗 Aliases</h4><p><code>ll</code>, <code>h</code>, <code>cls</code> built-in. Define custom aliases with <code>alias x=y</code>.</p></div>
+ </div>
+ </section>
+
+ <!-- ── Quick Start ── -->
+ <section id="start">
+ <h2>🚀 Quick Start</h2>
+ <ol style="margin-bottom:8px">
+ <li><strong>Open</strong> <code>index.html</code> in Chrome/Edge</li>
+ <li>Watch the splash screen load modules</li>
+ <li>Type <code>help</code> + Enter</li>
+ <li>Use the quick-action buttons above the input area</li>
+ </ol>
+
+ <h3>First Commands</h3>
+ <pre><span class="cmt"># List VFS files</span>
+ls
+
+<span class="cmt"># List files faster</span>
+ll
+
+<span class="cmt"># Connect to Ollama AI</span>
+ollamaconnect
+
+<span class="cmt"># Chat with AI</span>
+ai what's in this directory?
+
+<span class="cmt"># Create a file</span>
+put hello.txt Hello World
+
+<span class="cmt"># Edit a file</span>
+edit hello.txt
+
+<span class="cmt"># Search the web</span>
+search quantum computing
+
+<span class="cmt"># Find files</span>
+find hello
+
+<span class="cmt"># Compare two files</span>
+diff a.txt b.txt</pre>
+
+ <div class="callout callout-info">
+ <span class="callout-title">⌨️ Keyboard Shortcuts</span>
+ <ul style="margin:0">
+ <li><strong>Enter</strong> — Execute</li>
+ <li><strong>Tab</strong> — Cycle autocomplete</li>
+ <li><strong>↑/↓</strong> — Command history</li>
+ <li><strong>Esc</strong> — Hide autocomplete / stop AI</li>
+ <li><strong>|</strong> — Pipe: <code>ls | grep hello</code></li>
+ </ul>
+ </div>
+ </section>
+
+ <!-- ── Commands ── -->
+ <section id="commands">
+ <h2>📖 Full Command Reference</h2>
+
+ <h3>System</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th><th>Example</th></tr>
+ <tr><td><code>help</code></td><td>Show all commands</td><td><code>help</code></td></tr>
+ <tr><td><code>clear</code> / <code>cls</code></td><td>Clear terminal</td><td><code>cls</code></td></tr>
+ <tr><td><code>echo</code></td><td>Print text</td><td><code>echo Hello</code></td></tr>
+ <tr><td><code>mem</code></td><td>Show memory usage</td><td><code>mem</code></td></tr>
+ <tr><td><code>config</code></td><td>View/set terminal config (max-lines, history, pipe-buffer)</td><td><code>config terminal-max-lines 500</code></td></tr>
+ <tr><td><code>status</code></td><td>System status</td><td><code>status</code></td></tr>
+ <tr><td><code>font-size</code></td><td>Change font size (8–32)</td><td><code>font-size 16</code></td></tr>
+ <tr><td><code>load-local</code></td><td>Load module from VFS or URL</td><td><code>load-local modules/ext/calc.js</code></td></tr>
+ <tr><td><code>startup-modules</code></td><td>Open startup module selector</td><td><code>startup-modules</code></td></tr>
+ <tr><td><code>preview</code></td><td>Preview HTML in iframe</td><td><code>preview test.html</code></td></tr>
+ <tr><td><code>reset mem</code></td><td>Clear AI memory (context, patterns, profile)</td><td><code>reset mem --confirm</code></td></tr>
+ <tr><td><code>reset vfs</code></td><td>Clear virtual filesystem to empty root</td><td><code>reset vfs --confirm</code></td></tr>
+ <tr><td><code>reset config</code></td><td>Reset all config to defaults</td><td><code>reset config --confirm</code></td></tr>
+ <tr><td><code>reset all</code></td><td>Wipe everything (nuclear)</td><td><code>reset all --confirm</code></td></tr>
+ <tr><td><code>logrotate</code></td><td>Rotate current.log to dated archive</td><td><code>logrotate</code></td></tr>
+ <tr><td><code>health-check</code></td><td>Audit VFS, modules, logs, trash, skills</td><td><code>health-check</code></td></tr>
+ <tr><td><code>memory-map-export</code></td><td>Generate memory map from /memory/index.json</td><td><code>memory-map-export</code></td></tr>
+ <tr><td><code>checkpoint</code></td><td>Create VFS snapshot</td><td><code>checkpoint</code></td></tr>
+ <tr><td><code>checkpoint-list</code></td><td>List VFS checkpoints</td><td><code>checkpoint-list</code></td></tr>
+ <tr><td><code>rollback</code></td><td>Restore VFS from checkpoint</td><td><code>rollback</code></td></tr>
+ <tr><td><code>checkpoint-del</code></td><td>Delete a checkpoint</td><td><code>checkpoint-del</code></td></tr>
+ <tr><td><code>smart-prompt</code></td><td>Toggle context-aware prompt</td><td><code>smart-prompt</code></td></tr>
+ <tr><td><code>test</code></td><td>Run test suite</td><td><code>test auto</code></td></tr>
+ </table></div>
+
+ <h3>AI Assistant</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th><th>Example</th></tr>
+ <tr><td><code>ai</code> / <code>ask</code></td><td>Chat with AI (tool-calling)</td><td><code>ai create a notes file</code></td></tr>
+ <tr><td><code>ai-clear</code></td><td>Reset conversation context</td><td><code>ai-clear</code></td></tr>
+ <tr><td><code>ai-system</code></td><td>View/set system prompt</td><td><code>ai-system</code></td></tr>
+ <tr><td><code>ai-context</code></td><td>Show conversation memory</td><td><code>ai-context</code></td></tr>
+ <tr><td><code>ai-allow</code></td><td>Allow pending destructive action</td><td><code>ai-allow</code></td></tr>
+ <tr><td><code>ai-deny</code></td><td>Deny pending destructive action</td><td><code>ai-deny</code></td></tr>
+ <tr><td><code>ai-stop</code></td><td>Cancel running AI request</td><td><code>ai-stop</code></td></tr>
+ <tr><td><code>ai-compress</code></td><td>Manually compress conversation context</td><td><code>ai-compress</code></td></tr>
+ <tr><td><code>ai-config</code></td><td>View/set AI loop config (max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization)</td><td><code>ai-config max-turns 12</code></td></tr>
+ <tr><td><code>debug</code></td><td>Toggle AI debug mode (ext module)</td><td><code>debug</code></td></tr>
+ <tr><td><code>debug-ai</code></td><td>Toggle AI tool-call debug panel</td><td><code>debug-ai</code></td></tr>
+ <tr><td><code>debug-ai-last</code></td><td>Show last AI response (content, tool calls, errors)</td><td><code>debug-ai-last</code></td></tr>
+ <tr><td><code>debug-ai-send</code></td><td>Show what was sent to the AI (messages with roles)</td><td><code>debug-ai-send</code></td></tr>
+ <tr><td><code>debug-ai-log</code></td><td>Dump full tool call log for current session</td><td><code>debug-ai-log</code></td></tr>
+ <tr><td><code>archive-list</code></td><td>List archived session snapshots</td><td><code>archive-list</code></td></tr>
+ <tr><td><code>archive-read</code></td><td>View an archive by filename</td><td><code>archive-read 2026-06-25-14-30.json</code></td></tr>
+ <tr><td><code>mem-debug</code></td><td>Memory diagnostic (tool call log, heap, localStorage sizes)</td><td><code>mem-debug</code></td></tr>
+ <tr><td><code>reset-mem</code></td><td>Clear AI internal logs without touching VFS/config</td><td><code>reset-mem</code></td></tr>
+ </table></div>
+
+ <h3>Debug Supervisor</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th><th>Example</th></tr>
+ <tr><td><code>debug-stats</code></td><td>Error summary (total, unique, clusters, top issues)</td><td><code>debug-stats</code></td></tr>
+ <tr><td><code>debug-history</code></td><td>Show last N errors with hash, type, severity</td><td><code>debug-history 10</code></td></tr>
+ <tr><td><code>debug-analyze</code></td><td>Deep analysis of a specific error (root cause, fix suggestions)</td><td><code>debug-analyze a1b2c3d4</code></td></tr>
+ <tr><td><code>debug-trends</code></td><td>Error frequency over time, grouped by day and type</td><td><code>debug-trends 7</code></td></tr>
+ <tr><td><code>debug-fix</code></td><td>Suggest and optionally auto-apply fix for an error</td><td><code>debug-fix a1b2c3d4</code></td></tr>
+ <tr><td><code>debug-health</code></td><td>Check DebugSupervisorModule health status</td><td><code>debug-health</code></td></tr>
+ </table></div>
+
+
+
+ <h3>Skills</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th><th>Example</th></tr>
+ <tr><td><code>skill-list</code></td><td>List all skills</td><td><code>skill-list</code></td></tr>
+ <tr><td><code>skill-view</code></td><td>View skill content</td><td><code>skill-view web-scraper</code></td></tr>
+ <tr><td><code>skill-create</code></td><td>Create a skill</td><td><code>skill-create name desc "steps" tags</code></td></tr>
+ <tr><td><code>skill-patch</code></td><td>Update a skill</td><td><code>skill-patch name "new content"</code></td></tr>
+ <tr><td><code>skill-delete</code></td><td>Delete a skill</td><td><code>skill-delete oldskill</code></td></tr>
+ <tr><td><code>skill-export</code></td><td>Export skill to JSON</td><td><code>skill-export myskill</code></td></tr>
+ <tr><td><code>skill-import</code></td><td>Import skill from JSON</td><td><code>skill-import</code></td></tr>
+ <tr><td><code>skill-sync</code></td><td>Rebuild skill index</td><td><code>skill-sync</code></td></tr>
+ <tr><td><code>skill-webdav-sync</code></td><td>Push all skills to WebDAV as JSON</td><td><code>skill-webdav-sync</code></td></tr>
+ </table></div>
+
+ <h3>Config</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th><th>Example</th></tr>
+ <tr><td><code>config</code></td><td>Get a config value</td><td><code>config</code></td></tr>
+ <tr><td><code>config-set</code></td><td>Set a config value</td><td><code>config-set key value</code></td></tr>
+ <tr><td><code>config-list</code></td><td>List all config</td><td><code>config-list</code></td></tr>
+ <tr><td><code>config-delete</code></td><td>Delete a config entry</td><td><code>config-delete key</code></td></tr>
+ <tr><td><code>config-export</code></td><td>Export config as JSON</td><td><code>config-export</code></td></tr>
+ <tr><td><code>config-reset</code></td><td>Reset all config</td><td><code>config-reset</code></td></tr>
+ </table></div>
+ </section>
+
+ <!-- ── Filesystem ── -->
+ <section id="filesystem">
+ <h2>📁 Filesystem</h2>
+ <p>All VFS files persist in <code>localStorage</code>. <code>ls</code>, <code>cd</code>, <code>pwd</code> work like a real terminal. Clickable output — tap a file to <code>cat</code>, tap a dir to <code>cd</code>.</p>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th><th>Example</th></tr>
+ <tr><td><code>ls</code> / <code>ll</code></td><td>List files</td><td><code>ll</code></td></tr>
+ <tr><td><code>cd</code></td><td>Change directory</td><td><code>cd notes</code></td></tr>
+ <tr><td><code>pwd</code></td><td>Print working directory</td><td><code>pwd</code></td></tr>
+ <tr><td><code>cat</code></td><td>View file</td><td><code>cat readme.txt</code></td></tr>
+ <tr><td><code>put</code></td><td>Create file with content</td><td><code>put hello.txt Hello World</code></td></tr>
+ <tr><td><code>edit</code></td><td>Open in modal editor</td><td><code>edit notes.md</code></td></tr>
+ <tr><td><code>rm</code></td><td>Delete file</td><td><code>rm temp.txt</code></td></tr>
+ <tr><td><code>mkdir</code></td><td>Create directory</td><td><code>mkdir projects</code></td></tr>
+ <tr><td><code>rmdir</code></td><td>Remove directory</td><td><code>rmdir empty</code></td></tr>
+ <tr><td><code>trash</code></td><td>Move file to trash</td><td><code>trash temp.txt</code></td></tr>
+ <tr><td><code>trash-dir</code></td><td>Move directory to trash</td><td><code>trash-dir olddir</code></td></tr>
+ <tr><td><code>ls-trash</code></td><td>List items in trash</td><td><code>ls-trash</code></td></tr>
+ <tr><td><code>empty-trash</code></td><td>Permanently delete all trash items</td><td><code>empty-trash</code></td></tr>
+ <tr><td><code>mv</code></td><td>Move/rename</td><td><code>mv old.txt new.txt</code></td></tr>
+ <tr><td><code>cp</code></td><td>Copy file</td><td><code>cp a.txt b.txt</code></td></tr>
+ <tr><td><code>touch</code></td><td>Create empty file</td><td><code>touch newfile.txt</code></td></tr>
+ <tr><td><code>tree</code></td><td>Directory tree</td><td><code>tree</code></td></tr>
+ <tr><td><code>find</code></td><td>Recursive file search</td><td><code>find .txt</code></td></tr>
+ <tr><td><code>diff</code></td><td>Compare two files</td><td><code>diff a.txt b.txt</code></td></tr>
+ <tr><td><code>grep</code></td><td>Search file contents</td><td><code>grep hello *.txt</code></td></tr>
+ <tr><td><code>head</code></td><td>First N lines</td><td><code>head file.txt 10</code></td></tr>
+ <tr><td><code>tail</code></td><td>Last N lines</td><td><code>tail file.txt 10</code></td></tr>
+ <tr><td><code>sort</code></td><td>Sort file lines</td><td><code>sort file.txt</code></td></tr>
+ <tr><td><code>wc</code></td><td>Count lines/words/chars</td><td><code>wc file.txt</code></td></tr>
+ <tr><td><code>du</code></td><td>Disk usage</td><td><code>du</code></td></tr>
+ <tr><td><code>upload</code></td><td>Upload file from device</td><td><code>upload</code></td></tr>
+ <tr><td><code>download</code></td><td>Download file</td><td><code>download file.txt</code></td></tr>
+ <tr><td><code>vfs-size</code></td><td>Show VFS size report (top files, system dirs)</td><td><code>vfs-size</code></td></tr>
+ <tr><td><code>vfs-clean</code></td><td>Nuke snapshots, sessions, and trash</td><td><code>vfs-clean</code></td></tr>
+ <tr><td><code>vfs-export</code></td><td>Download VFS as JSON</td><td><code>vfs-export</code></td></tr>
+ <tr><td><code>vfs-import</code></td><td>Import VFS from JSON</td><td><code>vfs-import</code></td></tr>
+ <tr><td><code>session-save</code></td><td>Save a session snapshot</td><td><code>session-save</code></td></tr>
+ <tr><td><code>session-load</code></td><td>Load a session snapshot</td><td><code>session-load mysession</code></td></tr>
+ <tr><td><code>session-list</code></td><td>List all saved sessions</td><td><code>session-list</code></td></tr>
+ <tr><td><code>session-delete</code></td><td>Delete a session</td><td><code>session-delete old</code></td></tr>
+ <tr><td><code>session-export</code></td><td>Export session as JSON download</td><td><code>session-export mysession</code></td></tr>
+ <tr><td><code>session-import</code></td><td>Import a session from file</td><td><code>session-import</code></td></tr>
+ <tr><td><code>sed -n &#x27;N,Mp&#x27; | s/a/b/ | /pat/d | -i</code></td><td>Print or edit VFS file lines</td></tr>
+ <tr><td><code>files</code></td><td>Open the Files panel</td></tr>
+</table></div>
+ </section>
+
+ <!-- ── AI Agent ── -->
+ <section id="ai">
+ <h2>🤖 AI Assistant</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">Tool-Calling Agent</span>
+ The AI runs a full agent loop with 28 tools: filesystem, web search, URL fetch, skills, WPM, session memory, user profile, module introspection.
+ </div>
+
+ <h3>How It Works</h3>
+ <ol>
+ <li>Type <code>ai &lt;request&gt;</code></li>
+ <li>AI loads relevant skills + recent session memory</li>
+ <li>Decides which tools to call (up to 8 turns)</li>
+ <li><strong>Destructive tools</strong> trigger an inline confirm bar — no dark overlay</li>
+ <li>120s countdown: yellow → bold red at 30s → pulsing at 10s</li>
+ <li>Type <code>ai-allow</code> / <code>ai-deny</code> or tap the buttons</li>
+ <li>After complex tasks, AI suggests creating a skill</li>
+ </ol>
+
+ <div class="callout callout-warning">
+ <span class="callout-title">⚠️ Destructive Tools</span>
+ <code>write_file</code>, <code>delete_file</code>, <code>make_directory</code>, <code>edit_module</code> require confirmation before executing. <code>run_command</code> only requires confirmation for destructive commands (rm, kill, format) — safe-read commands like <code>ls</code>, <code>find</code>, <code>tree</code> are auto-approved.
+ </div>
+
+ <h3>Available Tools</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Tool</th><th>Description</th><th>Destructive?</th></tr>
+ <tr><td><code>list_files</code></td><td>List VFS directory</td><td>❌</td></tr>
+ <tr><td><code>read_file</code></td><td>Read file (8k trunc)</td><td>❌</td></tr>
+ <tr><td><code>write_file</code></td><td>Write to file</td><td>✅</td></tr>
+ <tr><td><code>make_directory</code></td><td>Create directory</td><td>✅</td></tr>
+ <tr><td><code>delete_file</code></td><td>Delete file/dir</td><td>✅</td></tr>
+ <tr><td><code>get_status</code></td><td>System status</td><td>❌</td></tr>
+ <tr><td><code>get_help</code></td><td>List commands</td><td>❌</td></tr>
+ <tr><td><code>web_search</code></td><td>SearXNG + DDG</td><td>❌</td></tr>
+ <tr><td><code>web_fetch</code></td><td>Extract URL content</td><td>❌</td></tr>
+ <tr><td><code>run_command</code></td><td>Execute terminal cmd (safe-read auto-approved)</td><td>❌ (only destructive cmds)</td></tr>
+ <tr><td><code>load_skills</code></td><td>Search relevant skills</td><td>❌</td></tr>
+ <tr><td><code>create/patch/list_skills</code></td><td>Skill management</td><td>❌</td></tr>
+ <tr><td><code>view/edit/test/reload</code></td><td>Module operations</td><td>edit ✅</td></tr>
+ <tr><td><code>user_get/set/list/forget</code></td><td>User profile</td><td>❌</td></tr>
+ <tr><td><code>session_save/load/recent/stats</code></td><td>Session memory</td><td>❌</td></tr>
+ <tr><td><code>vaultmind_connect</code></td><td>Connect wallet to VaultMind AI (reads key from SecretVault)</td><td>❌</td></tr>
+ <tr><td><code>vaultmind_status</code></td><td>Check VaultMind session status</td><td>❌</td></tr>
+ <tr><td><code>vaultmind_query</code></td><td>Query VaultMind: prices, portfolio, chat</td><td>❌</td></tr>
+ <tr><td><code>webx &lt;question&gt;</code></td><td>Ask the assistant (short alias of <code>webxai</code>)</td></tr>
+ <tr><td><code>webxai &lt;question&gt;</code></td><td>Ask the assistant a one-off question</td></tr>
+ <tr><td><code>wx &lt;question&gt;</code></td><td>Alias of <code>webxai</code></td></tr>
+ <tr><td><code>ai-model [name]</code></td><td>Show or set the active model</td></tr>
+ <tr><td><code>ai-ignore &lt;path&gt;</code></td><td>Exclude a path from AI context</td></tr>
+ <tr><td><code>commit-status</code></td><td>Show the pending AI file-commit batch</td></tr>
+ <tr><td><code>commit-approve</code></td><td>Apply the pending AI file commits</td></tr>
+ <tr><td><code>commit-deny</code></td><td>Discard the pending AI file commits</td></tr>
+ <tr><td><code>context</code></td><td>Show the active AI context profile</td></tr>
+ <tr><td><code>contexts</code></td><td>List context profiles</td></tr>
+ <tr><td><code>context-new &lt;name&gt; &lt;display&gt; &lt;systemPrompt&gt;</code></td><td>Create a context profile</td></tr>
+ <tr><td><code>context-delete &lt;name&gt;</code></td><td>Delete a context profile</td></tr>
+ <tr><td><code>context-auto on|off</code></td><td>Auto-switch contexts</td></tr>
+ <tr><td><code>smart-ask &lt;question&gt;</code></td><td>Ask with automatic context selection</td></tr>
+</table></div>
+
+ <h3>Sound Notification</h3>
+ <p>A short <strong>"ding"</strong> plays via WebAudio when AI finishes. Toggle with the <strong>🔊 Sound</strong> button in quick-actions. Persists in localStorage.</p>
+ </section>
+
+ <!-- ── Agent Loop Studio ── -->
+ <section id="agent-loops">
+ <h2>🔄 Agent Loop Studio</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">Pluggable Agent Loops</span>
+ Switch between profiles that control max turns, tool sets, temperature, system prompt directives, and loop behavior — without restarting.
+ </div>
+
+ <h3>Commands</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>agent-loops</code></td><td>List all profiles with turns, tools, and benchmark results</td></tr>
+ <tr><td><code>agent-loops &lt;name&gt;</code></td><td>Switch to a named profile</td></tr>
+ <tr><td><code>agent-loops new &lt;name&gt; &lt;desc&gt;</code></td><td>Create a new profile from defaults</td></tr>
+ <tr><td><code>agent-loops edit &lt;name&gt; &lt;key&gt; &lt;val&gt;</code></td><td>Edit a profile setting (dot-notation, e.g. <code>loop.maxTurns 12</code>)</td></tr>
+ <tr><td><code>agent-loops delete &lt;name&gt;</code></td><td>Delete a profile (not default)</td></tr>
+ <tr><td><code>agent-loops clone &lt;src&gt; &lt;dst&gt;</code></td><td>Clone a profile</td></tr>
+ <tr><td><code>agent-loops export &lt;name&gt;</code></td><td>Export profile as JSON</td></tr>
+ <tr><td><code>agent-loops reset &lt;name&gt;</code></td><td>Reset profile to defaults</td></tr>
+ <tr><td><code>bench &lt;profile&gt; [--tasks=ids]</code></td><td>Run benchmark suite on a profile (6 tasks: fs-navigate, fs-create-read, nvfs-resolve, web-search, multi-tool, code-edit)</td></tr>
+ <tr><td><code>bench-compare &lt;p1&gt; &lt;p2&gt;</code></td><td>Compare benchmark results of two profiles</td></tr>
+ </table></div>
+
+ <h3>Default Profiles</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Profile</th><th>Turns</th><th>Tools</th><th>Temp</th><th>Use Case</th></tr>
+ <tr><td><code>default</code></td><td>8</td><td>all</td><td>inherit</td><td>Balanced general-purpose</td></tr>
+ <tr><td><code>minimal</code></td><td>4</td><td>4 (custom)</td><td>0.3</td><td>Fast, low-token simple tasks</td></tr>
+ <tr><td><code>research</code></td><td>6</td><td>7 (custom)</td><td>0.5</td><td>Read-only web + file exploration</td></tr>
+ <tr><td><code>safe</code></td><td>8</td><td>all (destructive disabled)</td><td>0.5</td><td>No destructive tools</td></tr>
+ <tr><td><code>creative</code></td><td>12</td><td>all</td><td>0.9</td><td>More turns, higher creativity</td></tr>
+ <tr><td><code>coder</code></td><td>10</td><td>all</td><td>0.2</td><td>Code-focused with append directive</td></tr>
+ </table></div>
+
+ <h3>System Prompt Layering</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Effect</th></tr>
+ <tr><td><code>ai-system +"directive"</code></td><td>Prepend a directive to the active profile's system prompt (persists to profile)</td></tr>
+ <tr><td><code>ai-system -"directive"</code></td><td>Append a directive to the active profile's system prompt (persists to profile)</td></tr>
+ <tr><td><code>ai-system "text"</code></td><td>Full replace (persists to localStorage, overrides profile)</td></tr>
+ <tr><td><code>ai-system reset</code></td><td>Restore default system prompt</td></tr>
+ </table></div>
+
+ <h3>Self-Improvement</h3>
+ <p>After each AI run, <code>_postRunReview()</code> detects anomalies: near-max-turns, high token usage, tool failures, and above-benchmark consumption. Lessons are auto-saved as VFS skills under <code>/skills/loop-lesson-*</code>.</p>
+
+ <h3>AI Tools</h3>
+ <p>The AI can also manage profiles via tools: <code>agent_loop_list</code>, <code>agent_loop_switch</code>, <code>agent_loop_create</code>, <code>agent_loop_benchmark</code>.</p>
+ </section>
+
+ <!-- ── Skills ── -->
+ <section id="skills">
+ <h2>📚 Skills System</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">Dynamic Browser</span>
+ Skills are now in a <strong><a href="skills.html" target="_blank">dedicated dynamic page →</a></strong>. Search, filter by category, browse all 17 skills with descriptions and tags — no more hardcoded lists.
+ </div>
+ <div class="feature-grid" style="margin-top:14px">
+ <div class="feature-card"><h4>📄 <a href="skills.html">skills.html</a></h4><p>Dynamic skill browser — searchable, filterable card grid with tag badges, category pills, and update dates. Zero hardcoded data.</p></div>
+ <div class="feature-card"><h4>📘 <a href="guide.html">guide.html</a></h4><p>Quick user guide with anchor-navigable sections, module refs, and command examples.</p></div>
+ </div>
+ <h3>Terminal Commands</h3>
+ <pre><span class="cmt"># List all skills</span>
+skill-list
+
+<span class="cmt"># Create a skill</span>
+skill-create my-skill "Does X" "Steps to do X" "tag1,tag2"
+
+<span class="cmt"># View / patch / delete</span>
+skill-view my-skill
+skill-patch my-skill "Updated steps"
+skill-delete my-skill
+
+<span class="cmt"># Export / Import</span>
+skill-export my-skill
+skill-import</pre>
+
+ <h3>Auto-Creation</h3>
+ <p>When the AI encounters the same tool error 3+ times, a workaround skill is auto-created. Every 20 messages, skills are audited for staleness.</p>
+ <table>
+ <thead><tr><th>Command</th><th>Description</th></tr></thead>
+ <tbody>
+ <tr><td><code>learn-skill &lt;topic&gt;</code></td><td>Learn a new skill from a topic</td></tr>
+ <tr><td><code>skill-validate &lt;name&gt;</code></td><td>Validate a skill file</td></tr>
+ <tr><td><code>skill-audit</code></td><td>Audit all installed skills</td></tr>
+ <tr><td><code>skill-restore &lt;name&gt;</code></td><td>Restore a skill from its backup</td></tr>
+ <tr><td><code>skill-pack-core</code></td><td>Install / verify the core skill pack</td></tr>
+ </tbody>
+ </table>
+
+</section>
+
+ <!-- ── Ollama ── -->
+ <section id="ollama">
+ <h2>🤖 Ollama</h2>
+ <div class="callout callout-ollama">
+ <span class="callout-title">Local or Remote AI</span>
+ WebX11 connects to any Ollama instance. All tool-calling and chat features require a connection.
+ </div>
+
+ <h3>Setup</h3>
+ <pre><span class="cmt"># Install & pull a model</span>
+curl -fsSL https://ollama.com/install.sh | sh
+ollama pull llama2
+
+<span class="cmt"># Start Ollama</span>
+ollama serve
+
+<span class="cmt"># In WebX11</span>
+ollamaconnect</pre>
+
+ <h3>Commands</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>ollamaconnect</code></td><td>Connect to Ollama</td></tr>
+ <tr><td><code>ollamasetproxy</code></td><td>Set remote endpoint</td></tr>
+ <tr><td><code>ollamamodels</code></td><td>List models</td></tr>
+ <tr><td><code>ollamasetmodel</code></td><td>Set active model</td></tr>
+ <tr><td><code>ollamadisconnect</code></td><td>Disconnect</td></tr>
+ <tr><td><code>ollama</code></td><td>Open Ollama Manager UI</td></tr>
+ <tr><td><code>models</code></td><td>List all models</td></tr>
+ <tr><td><code>model-info</code></td><td>Model details</td></tr>
+ <tr><td><code>model-pull</code></td><td>Pull a model</td></tr>
+ <tr><td><code>model-delete</code></td><td>Delete a model</td></tr>
+ <tr><td><code>model-run</code></td><td>Run model with prompt</td></tr>
+ </table></div>
+
+ <div class="callout callout-warning">
+ <span class="callout-title">🔌 CORS for Remote</span>
+ <code>OLLAMA_ORIGINS="*" ollama serve</code>
+ </div>
+ </section>
+
+ <!-- ── Search & Web ── -->
+ <section id="web">
+ <h2>🌐 Search & Web</h2>
+ <div class="callout callout-info">
+ <span class="callout-title">SearXNG + DDG + URL Fetcher</span>
+ Three modules provide comprehensive web capabilities via the proxy API.
+ </div>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Module</th><th>Description</th><th>Example</th></tr>
+ <tr><td><code>search</code> / <code>s</code></td><td>SearXNG</td><td>Web search</td><td><code>search raspberry pi</code></td></tr>
+
+ <tr><td><code>fetch</code> / <code>web</code></td><td>Fetcher</td><td>Fetch URL content</td><td><code>fetch https://example.com</code></td></tr>
+ <tr><td><code>extract</code></td><td>Fetcher</td><td>Clean readable text</td><td><code>extract https://news.article</code></td></tr>
+ <tr><td><code>images</code></td><td>SearXNG</td><td>Image search</td><td><code>images cats</code></td></tr>
+ <tr><td><code>videos &lt;query&gt;</code></td><td>Video search via SearXNG</td></tr>
+</table></div>
+ <p>The AI agent also has <code>web_search</code> and <code>web_fetch</code> tools using the same APIs.</p>
+ </section>
+
+ <!-- ── Voice & Sound ── -->
+ <section id="voice">
+ <h2>🎤 Voice & Sound</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">Speech Recognition</span>
+ A <strong>🎤 button</strong> in the input area activates SpeechRecognition. Chrome, Edge, Safari 14.1+. Not Firefox.
+ </div>
+ <ul>
+ <li>Tap 🎤 to start listening — button pulses green</li>
+ <li>Say your command — recognized text fills the input</li>
+ <li>Tap again or wait to stop</li>
+ <li>Red flash indicates failure</li>
+ </ul>
+ <h3>Sound</h3>
+ <p>Toggle AI response "ding" with the <strong>🔊 Sound</strong> button. Persists in localStorage.</p>
+ <table>
+ <thead><tr><th>Command</th><th>Description</th></tr></thead>
+ <tbody>
+ <tr><td><code>speak &lt;text&gt;</code></td><td>Speak text aloud (TTS)</td></tr>
+ <tr><td><code>voice</code></td><td>Voice settings and microphone test</td></tr>
+ <tr><td><code>analyze &lt;file&gt; [--frames N] [--spectrogram] [--beats] [--scene]</code></td><td>Analyze an audio or video file</td></tr>
+ </tbody>
+ </table>
+
+</section>
+
+ <!-- ── Config ── -->
+ <section id="config">
+ <h2>⚙️ Configuration</h2>
+ <p>All config values stored as <code>webx11_config_*</code> in localStorage. Use the ⚙ Config button or CLI commands.</p>
+ <h3>Notable Keys</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Key</th><th>Description</th><th>Values</th></tr>
+ <tr><td><code>webx11_config_ai_sound</code></td><td>AI sound toggle</td><td><code>on</code>, <code>off</code></td></tr>
+ <tr><td><code>webx11_config_voice_visible</code></td><td>Mic button visibility</td><td><code>on</code>, <code>off</code></td></tr>
+ </table></div>
+ <h3>Persisted State</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Key</th><th>Description</th></tr>
+ <tr><td><code>webx11_filesystem</code></td><td>VFS JSON</td></tr>
+ <tr><td><code>webx11_modules</code></td><td>Loaded module URLs</td></tr>
+ <tr><td><code>webx11_ollama_endpoint</code></td><td>Ollama API URL</td></tr>
+ <tr><td><code>webx11_ollama_model</code></td><td>Selected model</td></tr>
+ <tr><td><code>webx11_startup_modules</code></td><td>Startup selection</td></tr>
+ <tr><td><code>webx11_font_size</code></td><td>Font size (8–32)</td></tr>
+ <tr><td><code>webx11_command_history</code></td><td>Command history</td></tr>
+ <tr><td><code>webx11_terminal_output</code></td><td>Persisted output</td></tr>
+ <tr><td><code>webx11_ai_context</code></td><td>AI conversation history</td></tr>
+ <tr><td><code>webx11_user_profile</code></td><td>AI user profile</td></tr>
+ <tr><td><code>webx11_aliases</code></td><td>User-defined aliases</td></tr>
+ <tr><td><code>bug</code></td><td>Generate a bug report from the current session</td></tr>
+ <tr><td><code>factory-reset-old --confirm</code></td><td>Legacy full reset (prefer <code>factory-reset</code>)</td></tr>
+ <tr><td><code>setfont &lt;px&gt;</code></td><td>Set terminal font size (alias of <code>font-size</code>)</td></tr>
+</table></div>
+ </section>
+
+ <!-- ── Secret Vault ── -->
+ <section id="vault">
+ <h2>🔒 Secret Vault</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">Encrypted Secrets Manager</span>
+ AES-256-GCM encrypted key-value storage in localStorage. Lock/unlock with a PIN — secrets are never stored in plain text. Vault status persists across sessions.
+ </div>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>secret-set</code></td><td>Encrypt and store a secret</td></tr>
+ <tr><td><code>secret-get</code></td><td>Decrypt and display a secret value</td></tr>
+ <tr><td><code>secret-list</code></td><td>List all stored secret names</td></tr>
+ <tr><td><code>secret-del</code></td><td>Remove a stored secret</td></tr>
+ <tr><td><code>secret-lock</code></td><td>Lock the vault (clears key from memory)</td></tr>
+ <tr><td><code>secret-unlock</code></td><td>Unlock the vault with PIN</td></tr>
+ <tr><td><code>secret-setpin</code></td><td>Set or change vault PIN</td></tr>
+ <tr><td><code>secret-status</code></td><td>Show vault status (locked/unlocked/pin set)</td></tr>
+ <tr><td><code>encrypt &lt;path&gt; &lt;passphrase&gt;</code></td><td>AES-256-GCM encrypt a VFS file</td></tr>
+ <tr><td><code>decrypt &lt;path.enc&gt; &lt;passphrase&gt;</code></td><td>Decrypt a .enc file</td></tr>
+</table></div>
+ </section>
+
+ <!-- ── VaultMind AI ── -->
+ <section id="vaultmind">
+ <h2>🧠 VaultMind AI</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">Hyperliquid AI Assistant</span>
+ VaultMind is an AI-powered wallet assistant for Hyperliquid. It provides market data, portfolio analysis, transaction analysis, and AI chat agents.
+ </div>
+
+ <h3>Setup</h3>
+ <ol>
+ <li>Store your Hyperliquid private key: <code>secret-set hyperliquid_key 0x...</code></li>
+ <li>Ask the AI to connect: <code>ai connect to vaultmind</code> or use the <code>vaultmind_connect</code> tool</li>
+ <li>Check status: <code>ai check vaultmind status</code> or <code>vaultmind_status</code></li>
+ </ol>
+
+ <h3>AI Tools</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Tool</th><th>Description</th></tr>
+ <tr><td><code>vaultmind_connect</code></td><td>Reads <code>hyperliquid_key</code> from SecretVault, authenticates with VaultMind backend, stores session token</td></tr>
+ <tr><td><code>vaultmind_status</code></td><td>Checks if a valid VaultMind session token exists in localStorage</td></tr>
+ <tr><td><code>vaultmind_query</code></td><td>Query endpoints: <code>prices</code>, <code>portfolio</code>, <code>transactions</code>, <code>copy-trade</code>, or <code>chat</code> with an AI agent</td></tr>
+ </table></div>
+
+ <h3>API Endpoints (proxied via nginx)</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Path</th><th>Backend</th></tr>
+ <tr><td><code>/vmai/</code></td><td>Proxied to VaultMind backend at <code>127.0.0.1:8000</code></td></tr>
+ <tr><td><code>/vmai-api/</code></td><td>Alias for frontend chat — also proxied to <code>127.0.0.1:8000</code></td></tr>
+ <tr><td><code>vaultmind-connect</code></td><td>Connect VaultMind to the trading API</td></tr>
+ <tr><td><code>vaultmind-status</code></td><td>VaultMind connection and balance status</td></tr>
+ <tr><td><code>vaultmind-query &lt;prices|portfolio|transactions|copy-trade|chat&gt;</code></td><td>Query VaultMind</td></tr>
+ <tr><td><code>wallet-lookup &lt;address&gt;</code></td><td>Look up a wallet address</td></tr>
+</table></div>
+ </section>
+
+ <!-- ── Modules ── -->
+ <section id="modules">
+ <h2>🧩 Module System</h2>
+ <div class="callout callout-info">
+ <span class="callout-title">25 Core + 17 Ext</span>
+ Modules loaded in dependency order at startup. Toggle ext modules with ⚙ gear on splash.
+ </div>
+ <h3>Commands</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>modules</code></td><td>Open Module Manager UI</td></tr>
+ <tr><td><code>loadmod &lt;url&gt;</code></td><td>Load module from URL</td></tr>
+ <tr><td><code>removemod &lt;name&gt;</code></td><td>Remove a module</td></tr>
+ <tr><td><code>listmods</code></td><td>List all loaded modules</td></tr>
+ <tr><td><code>load-local</code></td><td>Load module from VFS or URL</td></tr>
+ <tr><td><code>loadlocal</code></td><td>Alias for load-local</td></tr>
+ <tr><td><code>js &lt;expr&gt;</code></td><td>Evaluate JavaScript in page context</td></tr>
+ <tr><td><code>js-run &lt;path&gt;</code></td><td>Load and execute a JS file from VFS</td></tr>
+ <tr><td><code>startup-modules</code></td><td>Configure boot modules</td></tr>
+ <tr><td><code>module-load &lt;name&gt;</code></td><td>Load a module by name</td></tr>
+ <tr><td><code>module-unload &lt;name&gt;</code></td><td>Unload a module</td></tr>
+ <tr><td><code>module-enable &lt;name&gt;</code></td><td>Enable a module for startup</td></tr>
+ <tr><td><code>module-reset &lt;name&gt;</code></td><td>Reset a module to its shipped source</td></tr>
+</table></div>
+
+ <h3>Boot Order</h3>
+ <ol>
+ <li>Splash screen with ⚙ gear</li>
+ <li>Foundation: <code>virtual-fs.js</code> → <code>module-manager.js</code> → <code>ollama-manager.js</code> → <code>terminal.js</code></li>
+ <li>Built-in modules loaded async</li>
+ <li>Ext modules loaded if selected</li>
+ <li>Splash hides, terminal ready</li>
+ </ol>
+
+ <h3>Code Editor</h3>
+ <p>The AI can <code>edit_module(name)</code> to copy source into the Code Editor modal. Edit inline, then <strong>💾 Save & Test</strong> to load live from <code>/_mods/</code>. Use <code>test_module</code> without reloading the page.</p>
+ <p>You can also type <code>edit modules/ext/foo.js</code> — the editor now mirrors saves to <code>_mods/&lt;name&gt;.js</code> so <code>test_module</code> works from either path.</p>
+ </section>
+
+ <!-- ── Extras ── -->
+ <section id="extras">
+ <h2>🧰 Extended Modules</h2>
+
+ <h3>WebDAV Sync</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>webdav-config</code></td><td>Configure server</td></tr>
+ <tr><td><code>webdav-status</code></td><td>Connection status</td></tr>
+ <tr><td><code>webdav-ls</code></td><td>List remote dir</td></tr>
+ <tr><td><code>webdav-push</code></td><td>Push file</td></tr>
+ <tr><td><code>webdav-pull</code></td><td>Pull file</td></tr>
+ <tr><td><code>webdav-backup</code></td><td>Backup VFS</td></tr>
+ <tr><td><code>webdav-restore</code></td><td>Restore VFS</td></tr>
+ <tr><td><code>webdav-backup-list</code></td><td>List backups</td></tr>
+ <tr><td><code>webdav-browser</code></td><td>Visual file explorer modal</td></tr>
+ </table></div>
+
+ <h3>Calculator & Cron</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Module</th><th>Description</th></tr>
+ <tr><td><code>calc</code></td><td>calc.js</td><td>Evaluate math / unit conversion</td></tr>
+ <tr><td><code>cron-every</code></td><td>cron.js</td><td>Schedule a command</td></tr>
+ <tr><td><code>cron-list</code></td><td>cron.js</td><td>List scheduled jobs</td></tr>
+ <tr><td><code>cron-stop</code></td><td>cron.js</td><td>Stop a job</td></tr>
+ <tr><td><code>cron-clear</code></td><td>cron.js</td><td>Clear all jobs</td></tr>
+ </table></div>
+
+ <h3>Theme, WPM & Mindmap</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Module</th><th>Description</th></tr>
+ <tr><td><code>theme</code></td><td>theme.js</td><td>Set color theme (dark, light, amber, cyber, …)</td></tr>
+ <tr><td><code>wpm</code></td><td>wpm.js</td><td>Package manager</td></tr>
+ <tr><td><code>mindmap</code></td><td>mindmap.js</td><td>Show file structure as mindmap</td></tr>
+ </table></div>
+
+ <h3>Server Monitor</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>server</code></td><td>Add server to monitor</td></tr>
+ <tr><td><code>servers</code></td><td>List monitored servers</td></tr>
+ <tr><td><code>server-check</code></td><td>Ping a server</td></tr>
+ <tr><td><code>server-remove</code></td><td>Remove server</td></tr>
+ <tr><td><code>monitor</code></td><td>Toggle auto-monitoring</td></tr>
+ <tr><td><code>course open &lt;lessonId&gt; | course list</code></td><td>Course browser</td></tr>
+ <tr><td><code>kanban add &lt;task&gt; | list | done &lt;id&gt;</code></td><td>Task board</td></tr>
+ <tr><td><code>three add text &lt;text&gt; | reset | screenshot</code></td><td>3D scene commands</td></tr>
+ <tr><td><code>gui-run &lt;command&gt;</code></td><td>Run a command and render its output in the GUI panel</td></tr>
+ <tr><td><code>gui-text &lt;content&gt;</code></td><td>Render text in the GUI panel</td></tr>
+ <tr><td><code>gui-clear</code></td><td>Clear the GUI panel</td></tr>
+ <tr><td><code>gzip &lt;path&gt;</code></td><td>Compress a VFS file to .gz</td></tr>
+ <tr><td><code>wget &lt;url&gt; [filename]</code></td><td>Download a URL and save it to the VFS</td></tr>
+</table></div>
+
+
+
+ <h3>Aliases</h3>
+ <p>Built-in: <code>ll</code> = <code>ls</code>, <code>h</code> = <code>help</code>, <code>cls</code> = <code>clear</code>. Define custom: <code>alias x=ls -la</code>. Remove: <code>unalias x</code>. Persists in localStorage.</p>
+ </section>
+
+ <!-- ── BusyBox ── -->
+ <section id="busybox">
+ <h2>🔧 BusyBox Utilities</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">16 Unix Utility Commands</span>
+ Lightweight utility commands for everyday terminal work — identity, path, text, and system info tools. All work standalone and with shell pipes.
+ </div>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th><th>Example</th></tr>
+ <tr><td><code>which</code></td><td>Locate command in module registry + fuzzy suggestions</td><td><code>which ls</code></td></tr>
+ <tr><td><code>env</code></td><td>List local env vars (webx11_, ollama, user profile)</td><td><code>env</code></td></tr>
+ <tr><td><code>date</code></td><td>Current time (local, ISO, UTC, Unix timestamp)</td><td><code>date</code></td></tr>
+ <tr><td><code>cal</code></td><td>ASCII calendar for any month/year</td><td><code>cal 12 2026</code></td></tr>
+ <tr><td><code>yes</code></td><td>Output repeated string (capped at 50)</td><td><code>yes test</code></td></tr>
+ <tr><td><code>seq</code></td><td>Generate number sequence</td><td><code>seq 1 5</code></td></tr>
+ <tr><td><code>base64</code></td><td>Base64 encode/decode</td><td><code>base64 -d aGVsbG8=</code></td></tr>
+ <tr><td><code>printf</code></td><td>Formatted print (%s, %d, %f)</td><td><code>printf "hello %s" world</code></td></tr>
+ <tr><td><code>cut</code></td><td>Extract column from file</td><td><code>cut -f2 -d, data.csv</code></td></tr>
+ <tr><td><code>uniq</code></td><td>Filter adjacent duplicates</td><td><code>uniq lines.txt</code></td></tr>
+ <tr><td><code>hostname</code></td><td>Show host/platform info</td><td><code>hostname</code></td></tr>
+ <tr><td><code>whoami</code></td><td>Show current user</td><td><code>whoami</code></td></tr>
+ <tr><td><code>basename</code></td><td>Strip directory from path</td><td><code>basename /a/b/c.txt</code></td></tr>
+ <tr><td><code>dirname</code></td><td>Strip filename from path</td><td><code>dirname /a/b/c.txt</code></td></tr>
+ <tr><td><code>sleep</code></td><td>Countdown delay (1-30s)</td><td><code>sleep 3</code></td></tr>
+ <tr><td><code>expand</code></td><td>Convert tabs to spaces</td><td><code>expand code.txt</code></td></tr>
+ </table></div>
+ <p>All file-reading commands (<code>cut</code>, <code>uniq</code>, <code>expand</code>) work with shell pipes: <code>cat data.csv | cut -f2 -d, | uniq</code>.</p>
+ </section>
+
+ <!-- ── Floating ── -->
+ <section id="floating">
+ <h2>🪟 Floating UI Elements</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">Buttons, Menus &amp; Panels</span>
+ Create persistent floating UI elements that survive page reload — buttons that run commands, collapsible menus with sub-buttons, and informational panels.
+ </div>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th><th>Example</th></tr>
+ <tr><td><code>floating-add</code></td><td>Add a floating element (btn, menu, or panel)</td><td><code>floating-add btn quick-status ls -la</code></td></tr>
+ <tr><td><code>floating-remove</code></td><td>Remove a floating element</td><td><code>floating-remove quick-status</code></td></tr>
+ <tr><td><code>floating-list</code></td><td>List all floating elements</td><td><code>floating-list</code></td></tr>
+ <tr><td><code>floating-pos</code></td><td>Reposition an element (x y in px, or auto)</td><td><code>floating-pos menu1 100 200</code></td></tr>
+ <tr><td><code>floating-clear</code></td><td>Remove all floating elements</td><td><code>floating-clear</code></td></tr>
+ <tr><td><code>floating-toggle</code></td><td>Show/hide a floating element</td><td><code>floating-toggle menu1</code></td></tr>
+ </table></div>
+ <h3>Usage Examples</h3>
+ <pre><span class="cmt"># Simple button — click to run a command</span>
+floating-add btn quick ls -la
+
+<span class="cmt"># Menu with sub-buttons (btn:Label:command)</span>
+floating-add menu tools Tools btn:Status:status btn:Help:help btn:Check:health-check
+
+<span class="cmt"># Panel with info text</span>
+floating-add panel info "Server: production | Uptime: 12d"
+
+<span class="cmt"># You can drag buttons and menu headers to reposition them</span>
+<span class="cmt"># Position persists across page reloads</span></pre>
+ </section>
+
+ <!-- ── Runtime Debug /sys/ ── -->
+ <section id="runtime-debug">
+ <h2>🔍 Runtime Debug — /sys/</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">Virtual /sys/ filesystem</span>
+ Live runtime state mounted as browsable VFS files. Cat, grep, tree, find all work — no special commands needed. Never persisted to localStorage.
+ </div>
+ <h3>Path Reference</h3>
+ <pre><span class="cmt"># Overview</span>
+cat /sys/info
+
+<span class="cmt"># Memory</span>
+cat /sys/heap/stats
+cat /sys/heap/breakdown
+
+<span class="cmt"># AI Internal Logs</span>
+cat /sys/logs/tool-calls
+cat /sys/logs/errors
+cat /sys/logs/commands
+
+<span class="cmt"># VFS Stats</span>
+cat /sys/vfs/stats
+cat /sys/vfs/largest
+
+<span class="cmt"># Module Info</span>
+ls /sys/modules/
+cat /sys/modules/calc/info
+
+<span class="cmt"># DOM / Config</span>
+cat /sys/dom/node-count
+cat /sys/config/ai
+cat /sys/config/version</pre>
+ <p>Also available: <code>mem-debug</code> (one-shot text output), <code>reset-mem</code> (clear AI internal logs without touching VFS or config), and the <strong>DebugSupervisorModule</strong> (<code>debug-stats</code>, <code>debug-history</code>, <code>debug-analyze</code>, <code>debug-trends</code>, <code>debug-fix</code>, <code>debug-health</code>) for persistent error tracking and root-cause analysis.</p>
+ </section>
+
+ <!-- ── Shell ── -->
+ <section id="shell">
+ <h2>🐚 Shell</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">Pipes · Redirects · Variables · Scripts</span>
+ The shell module sits on top of every command — pipes, redirections, variable substitution, command chaining, and VFS script execution work automatically without any prefix.
+ </div>
+
+ <h3>Pipes <code>|</code></h3>
+ <p>Chain commands by piping stdout to the next command's stdin. Pipe-aware commands (<code>grep</code>, <code>head</code>, <code>tail</code>, <code>sort</code>, <code>wc</code>, <code>cut</code>, <code>uniq</code>, <code>expand</code>) auto-read from pipe when no file argument is given.</p>
+ <pre><span class="cmt"># Filter file listing</span>
+ls | grep .md
+
+<span class="cmt"># Count lines matching pattern</span>
+cat notes.txt | grep error | wc
+
+<span class="cmt"># Pipeline: filter → sort → head</span>
+cat data.csv | grep 2026 | sort | head -5</pre>
+
+ <h3>Redirects <code>&gt;</code> <code>&gt;&gt;</code> <code>&lt;</code></h3>
+ <pre><span class="cmt"># Write output to file</span>
+cal &gt; /tmp/calendar.txt
+
+<span class="cmt"># Append to file</span>
+echo "new line" &gt;&gt; log.txt
+
+<span class="cmt"># Read file as stdin</span>
+grep error &lt; log.txt</pre>
+
+ <h3>Command Chaining</h3>
+ <pre><span class="cmt"># AND: next runs only if previous succeeds</span>
+touch test.txt &amp;&amp; echo "created"
+
+<span class="cmt"># OR: next runs only if previous fails</span>
+rm nonexistent || echo "failed"
+
+<span class="cmt"># Sequence: runs in order regardless</span>
+cd /skills; ls</pre>
+
+ <h3>Variables</h3>
+ <pre><span class="cmt"># Set a variable</span>
+export GREET=hello
+
+<span class="cmt"># Reference in any command</span>
+echo $GREET
+
+<span class="cmt"># List all vars</span>
+set
+
+<span class="cmt"># Remove a var</span>
+unset GREET</pre>
+
+ <h3>Scripts</h3>
+ <p>VFS files with <code>.sh</code> extension can be executed line by line. Variables persist across lines.</p>
+ <pre><span class="cmt"># Create a script in VFS</span>
+echo 'export NAME=WebX11' &gt; greet.sh
+echo 'echo "Hello, $NAME!"' &gt;&gt; greet.sh
+
+<span class="cmt"># Run it</span>
+script greet.sh
+sh greet.sh</pre>
+
+ <h3>Shell Commands</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>export &lt;var&gt;=&lt;val&gt;</code></td><td>Set a shell variable</td></tr>
+ <tr><td><code>unset &lt;var&gt;</code></td><td>Remove a shell variable</td></tr>
+ <tr><td><code>set</code></td><td>List all shell variables</td></tr>
+ <tr><td><code>script &lt;path.sh&gt;</code></td><td>Run a VFS script file</td></tr>
+ <tr><td><code>sh &lt;path.sh&gt;</code></td><td>Alias for <code>script</code> — run a VFS script file</td></tr>
+ <tr><td><code>alias [name=value]</code></td><td>List aliases or set a custom alias name=command</td></tr>
+ <tr><td><code>unalias &lt;name&gt;</code></td><td>Remove a custom alias</td></tr>
+ <tr><td><code>h</code> · <code>s</code> · <code>ll</code></td><td>Short aliases: <code>h</code> = help, <code>s</code> = web search, <code>ll</code> = detailed listing</td></tr>
+ <tr><td><code>dashboard [--watch|--stop]</code></td><td>Collect live telemetry and open the dashboard page; --watch re-collects every 5 s</td></tr>
+ <tr><td><code>usage [--json] [--reset]</code></td><td>LLM token usage report for this browser (per model, cost estimate)</td></tr>
+ <tr><td><code>df</code></td><td>Virtual filesystem usage</td></tr>
+ <tr><td><code>free</code></td><td>JS heap / memory snapshot</td></tr>
+ <tr><td><code>top</code></td><td>Busiest commands and modules by activity</td></tr>
+ <tr><td><code>ps</code></td><td>Commands and follows currently running</td></tr>
+ <tr><td><code>history [--search &lt;term&gt;]</code></td><td>Command history, optionally filtered</td></tr>
+ <tr><td><code>run &lt;script&gt;</code></td><td>Execute a saved script from the VFS</td></tr>
+ <tr><td><code>sysmsg [on|off]</code></td><td>Toggle system messages</td></tr>
+ <tr><td><code>view_module &lt;name&gt;</code></td><td>Print a module's source</td></tr>
+ <tr><td><code>node &lt;script.js&gt;</code></td><td>Run a JS file from the VFS (alias of <code>js-run</code>)</td></tr>
+</table></div>
+ </section>
+
+ <!-- ── Floating UI ── -->
+ <section id="floating">
+ <h2>🪄 Floating UI</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">Remixable floating buttons · Menus · Panels</span>
+ The Floating Module lets you or the AI spawn persistent floating UI elements that survive page reloads. All state is kept in localStorage.
+ </div>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>floating-add btn &lt;id&gt; &lt;label&gt; &lt;command&gt;</code></td><td>Spawn a floating button that runs a terminal command</td></tr>
+ <tr><td><code>floating-add menu &lt;id&gt; &lt;label&gt; btn:Sub|cmd ...</code></td><td>Spawn a collapsible floating menu with sub-buttons</td></tr>
+ <tr><td><code>floating-add panel &lt;id&gt; &lt;title&gt; &lt;content&gt;</code></td><td>Spawn a floating text panel</td></tr>
+ <tr><td><code>floating-remove &lt;id&gt;</code></td><td>Remove a floating element</td></tr>
+ <tr><td><code>floating-list</code></td><td>Show all floating elements with IDs and positions</td></tr>
+ <tr><td><code>floating-pos &lt;id&gt; &lt;x|auto&gt; &lt;y|auto&gt;</code></td><td>Reposition a floating element</td></tr>
+ <tr><td><code>floating-toggle &lt;id&gt;</code></td><td>Show/hide a floating element</td></tr>
+ <tr><td><code>floating-clear</code></td><td>Remove all floating elements</td></tr>
+ </table></div>
+ <pre><span class="cmt"># Create a button</span>
+floating-add btn files "📁 Files" ls
+
+<span class="cmt"># Create a collapsible menu</span>
+floating-add menu tools "🔧 Tools" btn:Help|help btn:Clear|clear
+
+<span class="cmt"># Create a status panel</span>
+floating-add panel status "📊 Status" "Ready"
+
+<span class="cmt"># List all elements</span>
+floating-list
+
+<span class="cmt"># Hide without removing</span>
+floating-toggle files</pre>
+ </section>
+
+ <!-- ── Security ── -->
+ <section id="security">
+ <h2>🔒 Security & Guardrails</h2>
+ <div class="callout callout-warning">
+ <span class="callout-title">Protecting Your Terminal</span>
+ Three layered protections — guardrails block dangerous patterns, confidence escalation requires AI to self-assess, shadow VFS previews filesystem impact.
+ </div>
+
+ <h3>Guardrail Policy</h3>
+ <p>Pre-execution guardrails intercept AI tool calls and block or warn on patterns matching dangerous operations:</p>
+ <div class="table-wrap"><table>
+ <tr><th>Rule</th><th>Description</th><th>Severity</th></tr>
+ <tr><td><code>rm-rf-block</code></td><td>Prevent recursive force deletes</td><td>🔴 deny</td></tr>
+ <tr><td><code>curl-pipe-bash</code></td><td>Block curl-to-bash execution</td><td>🔴 deny</td></tr>
+ <tr><td><code>rm-outer-vfs</code></td><td>Block deletion of sensitive metadata files</td><td>🔴 deny</td></tr>
+ <tr><td><code>mass-delete</code></td><td>Block mass-delete operations</td><td>🔴 deny</td></tr>
+ <tr><td><code>chmod-777</code></td><td>Block world-writable permissions</td><td>🔴 deny</td></tr>
+ <tr><td><code>eval-danger</code></td><td>Block dynamic code execution (eval/exec/system)</td><td>🔴 deny</td></tr>
+ <tr><td><code>external-dl</code></td><td>External file download warning</td><td>🟡 warn</td></tr>
+ <tr><td><code>env-overwrite</code></td><td>Modifying environment variables</td><td>🟡 warn</td></tr>
+ </table></div>
+
+ <h3>Commands</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>guardrail</code></td><td>Show guardrail policy rules and violation count</td></tr>
+ <tr><td><code>guardrail-add &lt;id&gt; &lt;severity&gt; &lt;pattern&gt; &lt;tools...&gt;</code></td><td>Add a custom guardrail rule (severity: deny/warn)</td></tr>
+ <tr><td><code>guardrail-remove &lt;id&gt;</code></td><td>Remove a guardrail rule</td></tr>
+ <tr><td><code>shadow &lt;command&gt;</code></td><td>Execute command in Shadow VFS — snapshot, run, diff changes</td></tr>
+ <tr><td><code>shadow-commit</code></td><td>Accept shadow VFS changes</td></tr>
+ <tr><td><code>shadow-rollback</code></td><td>Undo shadow VFS changes</td></tr>
+ </table></div>
+
+ <h3>Confidence Escalation</h3>
+ <p>When the AI requests destructive actions, it must declare <code>Confidence: N%</code> in its reasoning. Below 90%, manual approval is required. If no confidence is declared, manual approval is required by default. This prevents the AI from making low-confidence destructive changes.</p>
+ </section>
+
+ <!-- ── Sessions ── -->
+ <section id="sessions">
+ <h2>💬 Sessions</h2>
+ <p>Sessions persist AI conversation logs in VFS <code>/memory/</code>. The AI auto-loads recent sessions at start for continuity.</p>
+ <h3>AI Tools</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Tool</th><th>Description</th></tr>
+ <tr><td><code>session_save</code></td><td>Save summary</td></tr>
+ <tr><td><code>session_load</code></td><td>Load by date</td></tr>
+ <tr><td><code>session_recent</code></td><td>Show last N</td></tr>
+ <tr><td><code>session_stats</code></td><td>Aggregate statistics</td></tr>
+ <tr><td><code>session-new</code></td><td>Start a fresh AI session (clears context, keeps history)</td></tr>
+</table></div>
+ <p>The terminal auto-saves session state every 60s and auto-restores on page reload.</p>
+ </section>
+
+ <!-- ── Storage ── -->
+ <section id="storage">
+ <h2>💾 Storage</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">localStorage Persistence</span>
+ Everything survives hard refreshes via 15+ localStorage keys.
+ </div>
+ <pre><span class="cmt"># Clear all WebX11 data</span>
+localStorage.clear()
+location.reload()
+
+<span class="cmt"># Back up / restore VFS</span>
+vfs-export
+vfs-import</pre>
+ <p>~5–10MB per domain. Only URLs (not code) are stored for modules.</p>
+ </section>
+
+ <!-- ── WebDAV ── -->
+ <section id="webdav">
+ <h2>☁️ WebDAV</h2>
+ <p>Cloud sync with your WebX11 space. File-tree browser UI with bulk actions.</p>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>webdav-config &lt;url&gt; &lt;token&gt;</code></td><td>Configure WebDAV connection</td></tr>
+ <tr><td><code>webdav-status</code></td><td>Show sync status and connection health</td></tr>
+ <tr><td><code>webdav-ls [path]</code></td><td>List remote files</td></tr>
+ <tr><td><code>webdav-push &lt;local&gt; [remote]</code></td><td>Upload file to cloud</td></tr>
+ <tr><td><code>webdav-pull &lt;remote&gt; [local]</code></td><td>Download file from cloud</td></tr>
+ <tr><td><code>webdav-browser</code></td><td>Open file-tree browser UI with checkboxes, context menu, inline rename, per-file preview 👁 / open-in-new-tab ⛶ buttons, minimize-to-floating and share-URL header buttons</td></tr>
+ <tr><td><code>webdav-rm &lt;path&gt;</code></td><td>Delete remote file/directory</td></tr>
+ <tr><td><code>webdav-mkdir &lt;path&gt;</code></td><td>Create remote directory</td></tr>
+ <tr><td><code>webdav-rename &lt;old&gt; &lt;new&gt;</code></td><td>Rename remote file</td></tr>
+ <tr><td><code>webdav-move &lt;src&gt; &lt;dst&gt;</code></td><td>Move remote file (alias for rename)</td></tr>
+ <tr><td><code>webdav-backup</code></td><td>Backup entire space</td></tr>
+ <tr><td><code>webdav-login &lt;user&gt;</code></td><td>Store WebDAV credentials for this session</td></tr>
+</table></div>
+ <h3>Browser Features</h3>
+ <ul>
+ <li>Checkbox select + Select All toggle</li>
+ <li>Bulk action bar: Delete, Move, Download selected files</li>
+ <li>Inline rename: double-click filename</li>
+ <li>Right-click context menu with all operations</li>
+ <li>Breadcrumb navigation</li>
+ <li>Create-directory button</li>
+ <li>Keyboard shortcuts: <code>j</code>/<code>k</code> navigate, <code>Space</code> select, <code>Enter</code> open</li>
+ </ul>
+ </section>
+
+ <!-- ── Agents ── -->
+ <section id="agents">
+ <h2>🤖 Agent Orchestration</h2>
+ <p>Register, list, and delegate tasks to AI agents with lease-based lifecycle management.</p>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>agent-list</code></td><td>List all registered agents with status</td></tr>
+ <tr><td><code>agent-status &lt;id&gt;</code></td><td>Show detailed status for a specific agent</td></tr>
+ <tr><td><code>agent-register &lt;name&gt; &lt;model&gt;</code></td><td>Register a new AI agent</td></tr>
+ <tr><td><code>agent-unregister &lt;id&gt;</code></td><td>Remove an agent from the registry</td></tr>
+ <tr><td><code>lease-list</code></td><td>Show active leases — agent tasks with status and TTL</td></tr>
+ <tr><td><code>orchestrate &lt;task&gt;</code></td><td>Delegate multi-step task to agent orchestrator</td></tr>
+ <tr><td><code>orchestrate-parallel &lt;task&gt;</code></td><td>Decompose and run sub-tasks in parallel (auto-splits on "and"/"then")</td></tr>
+ <tr><td><code>orchestrate-decompose &lt;task&gt;</code></td><td>Preview how a task would be split into sub-tasks</td></tr>
+ <tr><td><code>orchestrate-result</code></td><td>Show results of the last parallel batch</td></tr>
+ <tr><td><code>orchestrator-status</code></td><td>Show orchestrator agent pool status</td></tr>
+ <tr><td><code>orchestrator-list</code></td><td>List all spawned agents with full details</td></tr>
+ <tr><td><code>orchestrator-spawn &lt;role&gt; [caps...]</code></td><td>Spawn a new worker agent</td></tr>
+ <tr><td><code>orchestrator-sleep &lt;id&gt;</code></td><td>Suspend agent to VFS (survives reload)</td></tr>
+ <tr><td><code>orchestrator-wake &lt;id&gt;</code></td><td>Restore agent from VFS sleep state</td></tr>
+ <tr><td><code>orchestrator-kill &lt;id&gt;</code></td><td>Terminate an agent and revoke leases</td></tr>
+ <tr><td><code>orchestrator-purge</code></td><td>Remove dead agents (&gt;60s silent)</td></tr>
+ <tr><td><code>orchestrator-config &lt;key&gt; &lt;value&gt;</code></td><td>Tune: spawn-limit, spawn-strategy, heartbeat-timeout, idle-timeout, restart-policy, a2a-polling (on/off), a2a-poll-interval, heartbeat-check, idle-sweep-interval, dead-purge-timeout, consensus-size, consensus-strategy, consensus-timeout, pipeline-auto-fix, pipeline-stage-timeout (persisted)</td></tr>
+ <tr><td><code>orchestrator-restart-policy &lt;id&gt; &lt;policy&gt;</code></td><td>Set per-agent restart policy: on-failure, always, never</td></tr>
+ <tr><td><code>consensus &lt;question&gt;</code></td><td>Consensus: spawn N agents, debate, resolve by majority vote</td></tr>
+ <tr><td><code>consensus-status</code></td><td>Show current/active consensus round status</td></tr>
+ <tr><td><code>consensus-history</code></td><td>Show past consensus rounds and resolutions</td></tr>
+ <tr><td><code>pipeline &lt;test|deploy|verify|history|config&gt;</code></td><td>Autonomous CI/CD: test agents/modules, deploy (test→auto-fix→verify), history, config</td></tr>
+ <tr><td><code>lease-create &lt;agent-id&gt; [duration] [task]</code></td><td>Create a time-bound lease for a worker agent</td></tr>
+ <tr><td><code>lease-list</code></td><td>Show active leases — agent tasks with status and TTL</td></tr>
+ <tr><td><code>lease-revoke &lt;id&gt;</code></td><td>Revoke an active lease</td></tr>
+ <tr><td><code>lease-renew &lt;id&gt; &lt;seconds&gt;</code></td><td>Extend an active lease by N seconds</td></tr>
+ <tr><td><code>orchestrator-schedule &lt;role&gt; &lt;interval_s&gt; &lt;task...&gt;</code></td><td>Schedule a recurring task for a role</td></tr>
+ <tr><td><code>orchestrator-schedules</code></td><td>List scheduled tasks</td></tr>
+ <tr><td><code>orchestrator-unschedule &lt;id&gt;</code></td><td>Remove a scheduled task</td></tr>
+</table></div>
+ </section>
+
+ <!-- ── A2A Contracts ── -->
+ <section id="a2a-contracts">
+ <h2>📜 A2A Contracts</h2>
+ <p>Define and enforce agent-to-agent contracts with input/output schema validation. Contracts are stored in <code>/.contracts/</code> and validated during orchestrator delegation.</p>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>a2a-send &lt;agent-id&gt; &lt;json&gt;</code></td><td>Send an A2A message to an agent's VFS inbox</td></tr>
+ <tr><td><code>a2a-list</code></td><td>Show all A2A messages with pending status</td></tr>
+ <tr><td><code>contract-register &lt;role&gt; [fields...] --output [fields...]</code></td><td>Register a contract with input/output schema</td></tr>
+ <tr><td><code>contract-list</code></td><td>List all registered contracts</td></tr>
+ <tr><td><code>a2a-post &lt;to&gt; &lt;type&gt; &lt;capability&gt; &lt;payload&gt;</code></td><td>Post a message to another agent's mailbox</td></tr>
+ <tr><td><code>a2a-poll</code></td><td>Read your own inbox</td></tr>
+ <tr><td><code>a2a-ack &lt;id&gt;</code></td><td>Acknowledge a message</td></tr>
+ <tr><td><code>a2a-reject &lt;id&gt;</code></td><td>Reject a message</td></tr>
+ <tr><td><code>a2a-status</code></td><td>Mailbox counters per agent</td></tr>
+ <tr><td><code>a2a-prune</code></td><td>Drop old and acknowledged messages</td></tr>
+</table></div>
+ </section>
+
+ <!-- ── Neural VFS ── -->
+ <section id="neural-vfs">
+ <h2>🧠 Neural VFS</h2>
+ <p>Semantic concept-indexing layer over the hierarchical VFS. Scans files and builds a concept → path mapping using filenames, tags, and headers.</p>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>memgraph-status</code></td><td>Memory graph node/edge counts</td></tr>
+ <tr><td><code>memgraph-search &lt;q&gt;</code></td><td>Find nodes by label or type</td></tr>
+ <tr><td><code>memgraph-stats</code></td><td>Most connected nodes, strongest edges</td></tr>
+ <tr><td><code>memgraph-export</code></td><td>Export graph as JSON</td></tr>
+ <tr><td><code>memgraph-prune</code></td><td>Remove stale low-weight edges</td></tr>
+ <tr><td><code>nvfs-status</code></td><td>Show index stats and last scan time</td></tr>
+ <tr><td><code>nvfs-resolve &lt;concept&gt;</code></td><td>Find files related to a concept (exact + fuzzy)</td></tr>
+ <tr><td><code>nvfs-tree [concept]</code></td><td>Show concept-to-file mapping tree</td></tr>
+<tr><td><code>nvfs-config [key] [value]</code></td><td>View/set config (fuzzy-limit, tree-files, tree-concepts, resolve-files, deep-index, deep-model)</td></tr>
+ <tr><td><code>nvfs-alias list|add|remove|clear</code></td><td>Manage concept aliases</td></tr>
+ <tr><td><code>nvfs-sync [--deep]</code></td><td>Incremental background sync dirty files</td></tr>
+ <tr><td><code>nvfs-save</code></td><td>Force-save semantic index to VFS disk</td></tr>
+ <tr><td><code>nvfs-cancel</code></td><td>Cancel running index operation</td></tr>
+ <tr><td><code>nvfs-index-graph [--deep]</code></td><td>Build the NVFS index dependency graph</td></tr>
+</table></div>
+ </section>
+
+ <!-- ── Server Monitor ── -->
+ <section id="server-monitor">
+ <h2>📊 Server Monitor</h2>
+ <p>Uptime monitoring for external services + self-optimization telemetry tracking tool latency and error rates.</p>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>server &lt;name&gt; &lt;url&gt;</code></td><td>Add a server to monitor</td></tr>
+ <tr><td><code>servers</code></td><td>List all monitored servers</td></tr>
+ <tr><td><code>server-check &lt;name&gt;</code></td><td>Check a server's current status</td></tr>
+ <tr><td><code>monitor</code></td><td>Toggle auto-monitoring (60s interval)</td></tr>
+<tr><td><code>system-health</code></td><td>Tool latency, VFS stats, modules, memory, error rates</td></tr>
+ <tr><td><code>predict &lt;task&gt;</code></td><td>Predict task duration, tool turns, and confidence</td></tr>
+ <tr><td><code>predictor-status</code></td><td>Prediction model accuracy and data points</td></tr>
+ <tr><td><code>predictor-config &lt;k&gt; &lt;v&gt;</code></td><td>Tune confidence-threshold, min-data-points</td></tr>
+ </table></div>
+ </section>
+ <section id="debug-panel">
+ <h2>🔧 Debug Panel</h2>
+ <p>Toggle the debug overlay to monitor system internals in real-time.</p>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>debug</code></td><td>Toggle debug panel on/off</td></tr>
+ <tr><td><code>debug-sync</code></td><td>Push debug records to the configured sync endpoint</td></tr>
+ <tr><td><code>debug-sync-url &lt;url&gt;</code></td><td>Show or set the sync endpoint</td></tr>
+</table></div>
+ <h3>Debug Panel Shows</h3>
+ <ul>
+ <li><strong>Memory</strong> — localStorage usage, VFS size, session context size</li>
+ <li><strong>Events</strong> — registered event listeners, module event bindings</li>
+ <li><strong>Errors</strong> — recent JS errors with stack traces</li>
+ <li><strong>Modules</strong> — loaded count, broken references, cache state</li>
+ </ul>
+ <p>Useful for troubleshooting module load failures, VFS corruption, and AI context bloat.</p>
+ </section>
+
+ <!-- ── Sandbox ── -->
+ <section id="sandbox">
+ <h2>🧪 Sandboxed Execution</h2>
+ <p>WebWorkers provide isolated contexts for task execution. Each worker gets a VFS snapshot so operations don't interfere with the main terminal.</p>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>sandbox-status</code></td><td>Show worker pool health and task metrics</td></tr>
+ <tr><td><code>sandbox-config &lt;k&gt; &lt;v&gt;</code></td><td>Configure max-workers, min-workers</td></tr>
+ <tr><td><code>sandbox-run &lt;task&gt;</code></td><td>Execute a task in the sandbox pool</td></tr>
+ </table></div>
+ <p><code>orchestrate --sandboxed &lt;task&gt;</code> routes delegation through the sandbox for full isolation.</p>
+ </section>
+
+ <!-- ── Memory Graph ── -->
+ <section id="memory-graph">
+ <h2>🧠 Memory Graph</h2>
+ <p>Cross-session persistent graph database tracking files, concepts, tasks, and tools. Nodes store metadata, edges record relationships with weighted strength. Survives page reloads via VFS persistence.</p>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>memgraph-status</code></td><td>Show node/edge counts and graph health</td></tr>
+ <tr><td><code>memgraph-search &lt;q&gt;</code></td><td>Find nodes by label, type, or id</td></tr>
+ <tr><td><code>memgraph-stats</code></td><td>Most connected nodes and strongest edges</td></tr>
+ <tr><td><code>memgraph-export</code></td><td>Export full graph as JSON to VFS</td></tr>
+<tr><td><code>memgraph-prune</code></td><td>Remove stale low-weight edges (&gt;30d, &lt;3 uses)</td></tr>
+ <tr><td><code>memgraph-learn &lt;entity&gt; &lt;fact&gt;</code></td><td>Add a fact to the memory graph</td></tr>
+ <tr><td><code>memgraph-agent &lt;agentId&gt;</code></td><td>Show one agent's memory subgraph</td></tr>
+ <tr><td><code>memgraph-session [YYYY-MM-DD]</code></td><td>Show a day's session graph</td></tr>
+ <tr><td><code>memgraph-decay-config &lt;days&gt; &lt;min_uses&gt;</code></td><td>Tune memory decay</td></tr>
+</table></div>
+ <p>Context is automatically injected into AI prompts for cross-session awareness.</p>
+ </section>
+
+ <!-- ── World Model ── -->
+ <section id="world-model">
+ <h2>🌐 World Model</h2>
+ <p>System self-awareness engine. Synthesizes data from memory graph, module system, skills index, telemetry, and agent pool into a unified model at <code>/system/world-model.json</code>. Auto-refreshes every 5 minutes.</p>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>world-model</code></td><td>Show full world model breakdown</td></tr>
+ <tr><td><code>world-model-summary</code></td><td>One-line system status summary</td></tr>
+ <tr><td><code>world-model-ask &lt;q&gt;</code></td><td>Query modules, skills, agents, health, or changes</td></tr>
+ <tr><td><code>world-model-refresh</code></td><td>Force re-synthesis immediately</td></tr>
+ <tr><td><code>world-model-config &lt;k&gt; &lt;v&gt;</code></td><td>Tune refresh-interval and max-projects</td></tr>
+ </table></div>
+ </section>
+
+ <!-- ── Security ── -->
+ <section id="security">
+ <h2>🔒 Security</h2>
+ <div class="table-wrap"><table>
+ <tr><th>Feature</th><th>Status</th></tr>
+ <tr><td>Input Validation</td><td>✅</td></tr>
+ <tr><td>Output Escaping</td><td>✅</td></tr>
+ <tr><td>No eval() on User Input</td><td>✅</td></tr>
+ <tr><td>Path Traversal Prevention</td><td>✅</td></tr>
+ <tr><td>Sandboxed File System</td><td>✅</td></tr>
+ <tr><td>AI Destructive Confirmations</td><td>✅ Required + 120s countdown</td></tr>
+ <tr><td>Iframe Preview Sandbox</td><td>✅ allow-same-origin allow-scripts</td></tr>
+ </table></div>
+ <h3>Security Levels</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Level</th><th>Behavior</th></tr>
+ <tr><td><code>0</code></td><td>Unrestricted — all commands allowed without confirmation</td></tr>
+ <tr><td><code>1</code> (default)</td><td>Confirm destructive actions (rm, overwrite, etc.)</td></tr>
+ <tr><td><code>2</code></td><td>Deny sensitive commands (rm -rf, encrypt operations)</td></tr>
+ <tr><td><code>3</code></td><td>Deny all dangerous commands (requires deliberate override)</td></tr>
+ </table></div>
+ <div class="table-wrap"><table>
+ <tr><th>Command</th><th>Description</th></tr>
+ <tr><td><code>security-level &lt;0-3&gt;</code></td><td>Set active security level</td></tr>
+ <tr><td><code>security</code></td><td>Show full status: level, blocked count, destructive/system/whitelist sets, suspicious flags, registered commands</td></tr>
+ <tr><td><code>security-destructive list</code></td><td>Show destructive command set</td></tr>
+ <tr><td><code>security-destructive add|remove &lt;cmd&gt;</code></td><td>Add/remove command from destructive set (persisted)</td></tr>
+ <tr><td><code>security-system list</code></td><td>Show system command set</td></tr>
+ <tr><td><code>security-system add|remove &lt;cmd&gt;</code></td><td>Add/remove command from system set (persisted)</td></tr>
+ <tr><td><code>security-whitelist list</code></td><td>Show whitelist command set</td></tr>
+ <tr><td><code>security-whitelist add|remove &lt;cmd&gt;</code></td><td>Add/remove command from whitelist (persisted)</td></tr>
+ <tr><td><code>security-flag list</code></td><td>Show suspicious flags</td></tr>
+ <tr><td><code>security-flag add|remove &lt;flag&gt;</code></td><td>Add/remove suspicious flag (persisted, -- prefix optional)</td></tr>
+ <tr><td><code>cmd-allow &lt;command&gt;</code></td><td>Add command to allowlist (bypasses restrictions)</td></tr>
+ <tr><td><code>cmd-deny &lt;command&gt;</code></td><td>Add command to denylist (always blocked)</td></tr>
+ </table></div>
+ <div class="callout callout-warning">
+ <span class="callout-title">⚠️ Module Loading</span>
+ Only load modules from trusted sources — they have full terminal access.
+ </div>
+ </section>
+
+ <!-- ── Troubleshooting ── -->
+ <section id="trouble">
+ <h2>🔧 Troubleshooting</h2>
+ <div class="table-wrap"><table>
+ <tr><th>Issue</th><th>Solution</th></tr>
+ <tr><td>AI not responding</td><td>Run <code>ollamaconnect</code> first, ensure Ollama is running</td></tr>
+ <tr><td>Voice not working</td><td>Chrome/Edge/Safari only. Grant mic permission</td></tr>
+ <tr><td>Module fails to load</td><td>Check CORS on the hosting server</td></tr>
+ <tr><td>Data lost after refresh</td><td>Check localStorage is enabled. Private mode clears data</td></tr>
+ <tr><td>AI actions not confirming</td><td>Confirm bar auto-denies after 120s. Type <code>ai-allow</code></td></tr>
+ <tr><td>Sound not playing</td><td>Browser may need first user gesture. Toggle off/on</td></tr>
+ <tr><td>Ollama CORS error</td><td><code>OLLAMA_ORIGINS="*" ollama serve</code></td></tr>
+ </table></div>
+ <h3>Debug</h3>
+ <pre><span class="cmt"># Console</span>
+F12 → Console
+
+<span class="cmt"># View AI state</span>
+ai-system
+ai-context
+
+<span class="cmt"># Module status</span>
+listmods</pre>
+ </section>
+
+ <!-- ── Quick Ref ── -->
+ <section id="ref">
+ <h2>⚡ Quick Reference</h2>
+ <div class="quick-ref">
+ <div class="quick-ref-title">WebX11 v4.5.11</div>
+ <div class="quick-ref-grid">
+ <div class="quick-ref-item"><span class="cmd">help</span><span class="desc">Show all commands</span></div>
+ <div class="quick-ref-item"><span class="cmd">cls</span><span class="desc">Clear terminal</span></div>
+ <div class="quick-ref-item"><span class="cmd">ll</span><span class="desc">List files</span></div>
+ <div class="quick-ref-item"><span class="cmd">cd &lt;dir&gt;</span><span class="desc">Change directory</span></div>
+ <div class="quick-ref-item"><span class="cmd">cat &lt;f&gt;</span><span class="desc">View file</span></div>
+ <div class="quick-ref-item"><span class="cmd">edit &lt;f&gt;</span><span class="desc">Edit file</span></div>
+ <div class="quick-ref-item"><span class="cmd">ai &lt;msg&gt;</span><span class="desc">Chat with AI</span></div>
+ <div class="quick-ref-item"><span class="cmd">search &lt;q&gt;</span><span class="desc">Web search</span></div>
+ <div class="quick-ref-item"><span class="cmd">find &lt;q&gt;</span><span class="desc">File search</span></div>
+ <div class="quick-ref-item"><span class="cmd">diff a b</span><span class="desc">Compare files</span></div>
+ <div class="quick-ref-item"><span class="cmd">ollamaconnect</span><span class="desc">Connect AI</span></div>
+ <div class="quick-ref-item"><span class="cmd">ai-allow</span><span class="desc">Allow destructive</span></div>
+ <div class="quick-ref-item"><span class="cmd">ai-deny</span><span class="desc">Deny destructive</span></div>
+ <div class="quick-ref-item"><span class="cmd">modules</span><span class="desc">Module Manager</span></div>
+ <div class="quick-ref-item"><span class="cmd">skill-list</span><span class="desc">List skills</span></div>
+ <div class="quick-ref-item"><span class="cmd">alias</span><span class="desc">List aliases</span></div>
+ <div class="quick-ref-item"><span class="cmd">preview</span><span class="desc">HTML preview</span></div>
+ <div class="quick-ref-item"><span class="cmd">upload</span><span class="desc">Upload file</span></div>
+ <div class="quick-ref-item"><span class="cmd">&#124; pipe</span><span class="desc">Chain commands</span></div>
+ <div class="quick-ref-item"><span class="cmd">&gt; file</span><span class="desc">Redirect output</span></div>
+ <div class="quick-ref-item"><span class="cmd">which</span><span class="desc">Locate command</span></div>
+ <div class="quick-ref-item"><span class="cmd">env</span><span class="desc">Show env vars</span></div>
+ <div class="quick-ref-item"><span class="cmd">script</span><span class="desc">Run .sh file</span></div>
+ <div class="quick-ref-item"><span class="cmd">date</span><span class="desc">Show time</span></div>
+ <div class="quick-ref-item"><span class="cmd">cal</span><span class="desc">Show calendar</span></div>
+ <div class="quick-ref-item"><span class="cmd">base64</span><span class="desc">Encode/decode</span></div>
+ <div class="quick-ref-item"><span class="cmd">nvfs-index</span><span class="desc">Build semantic index</span></div>
+ <div class="quick-ref-item"><span class="cmd">nvfs-resolve</span><span class="desc">Find files by concept</span></div>
+ <div class="quick-ref-item"><span class="cmd">nvfs-sync</span><span class="desc">Sync dirty files</span></div>
+ <div class="quick-ref-item"><span class="cmd">nvfs-save</span><span class="desc">Save index to disk</span></div>
+ <div class="quick-ref-item"><span class="cmd">nvfs-cancel</span><span class="desc">Cancel index op</span></div>
+ <div class="quick-ref-item"><span class="cmd">js</span><span class="desc">Eval JS expression</span></div>
+ <div class="quick-ref-item"><span class="cmd">js-run</span><span class="desc">Run JS file</span></div>
+ <div class="quick-ref-item"><span class="cmd">vfs-size</span><span class="desc">VFS size report</span></div>
+ <div class="quick-ref-item"><span class="cmd">vfs-clean</span><span class="desc">Nuke system dirs</span></div>
+ <div class="quick-ref-item"><span class="cmd">mindmap svg</span><span class="desc">Render mindmap as SVG</span></div>
+ <div class="quick-ref-item"><span class="cmd">gui</span><span class="desc">Simple GUI builder</span></div>
+ <div class="quick-ref-item"><span class="cmd">contract-list</span><span class="desc">List A2A contracts</span></div>
+ <div class="quick-ref-item"><span class="cmd">modules-list</span><span class="desc">List all 45 modules</span></div>
+ <div class="quick-ref-item"><span class="cmd">orchestrate</span><span class="desc">Delegate to agent</span></div>
+ <div class="quick-ref-item"><span class="cmd">system-health</span><span class="desc">System diagnostics</span></div>
+ <div class="quick-ref-item"><span class="cmd">seq</span><span class="desc">Number sequence</span></div>
+ <div class="quick-ref-item"><span class="cmd">whoami</span><span class="desc">Show user</span></div>
+ <div class="quick-ref-item"><span class="cmd">basename</span><span class="desc">Strip dir path</span></div>
+ </div>
+ </div>
+ </section>
+
+ <div style="text-align:center;margin-bottom:32px;">
+ <p>📄 One-page <a href="cheatsheet.html" style="color:var(--accent2);font-weight:600;">Quick Reference cheat sheet</a> — printable, dark mode, all commands.</p>
+ </div>
+
+ <!-- ── Changelog ── -->
+ <section id="history">
+ <h2>📋 Changelog</h2>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.6.52 — October 2026</span><span class="v-date">2026-10-22</span></div>
+ <ul>
+ <li><strong>Documentation backlog cleared</strong> — 74 commands that the code registered but this page never documented are now in their own sections: <code>dashboard</code> (with <code>--watch</code>/<code>--stop</code>), <code>usage</code>, <code>ps</code>/<code>top</code>/<code>df</code>/<code>free</code>, the six <code>a2a-*</code> mailbox commands, <code>webxai</code>/<code>webx</code>/<code>wx</code>, <code>context</code>/<code>contexts</code>/<code>context-new</code>/<code>context-delete</code>/<code>context-auto</code>/<code>smart-ask</code>, the <code>commit-*</code> approval trio, <code>module-load|unload|enable|reset</code>, <code>memgraph-agent|session|decay-config</code>, <code>orchestrator-schedule|schedules|unschedule</code>, <code>skill-validate|audit|restore|pack-core</code>, <code>learn-skill</code>, <code>encrypt</code>/<code>decrypt</code>, <code>gzip</code>/<code>wget</code>, <code>kanban</code>, <code>three</code>, <code>course</code>, <code>videos</code>, <code>speak</code>/<code>voice</code>/<code>analyze</code>, <code>gui-run|text|clear</code>, the <code>vaultmind-*</code> set, <code>debug-sync</code>/<code>debug-sync-url</code>, <code>setfont</code>, <code>bug</code>, <code>session-new</code>, <code>files</code>, <code>node</code>, <code>sed</code>, <code>nvfs-index-graph</code>, <code>wallet-lookup</code>, <code>ai-model</code>, <code>ai-ignore</code>, <code>webdav-login</code>, <code>view_module</code>, <code>run</code>, <code>sysmsg</code>, <code>history</code>.</li>
+ <li><strong>Ghost rows removed</strong> — <code>commands</code>, <code>check</code>, <code>sandbox-kill</code> and <code>webdav-explorer</code> were documented here but are not registered anywhere in the code. Their rows are gone; the sandbox prose no longer promises <code>sandbox-kill</code>.</li>
+ <li><strong>Audit method</strong> — the command list is now derived from <code>terminal.js</code> (<code>_baseWhitelist</code>, <code>internals</code>, <code>cmd ===</code> dispatch) plus every <code>commands['…']</code> registration in <code>modules/</code>, so the next gap can be found mechanically.</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.86 — July 2026</span><span class="v-date">2026-07-04</span></div>
+ <ul>
+ <li><strong>Cold module activation</strong> — Three modules (Memory Graph, Predictor, World Model) were loaded but produced zero actionable data. All now wired into the AI agent loop.</li>
+ <li><strong>Predictor feedback loop</strong> — <code>predictor.recordResult()</code> now called after multi-turn AI tasks (toolTurns ≥ 2). Predictor accumulates historical data for duration/turn predictions. Previously had zero call sites — module was dead.</li>
+ <li><strong>New AI tools</strong> — <code>predict_task</code> (estimate task duration before starting) and <code>world_model_query</code> (query system self-awareness: modules, skills, files, agents, health, changes, projects). AI now has 51 tools (was 49).</li>
+ <li><strong>Memory graph noise suppression</strong> — <code>getContextForPrompt()</code> returns empty string when graph has ≤2 nodes and 0 edges. Eliminates useless <code>[Memory Graph Context] 1 nodes, 0 edges</code> token waste on every AI turn.</li>
+ <li><strong>Memory graph task recording</strong> — <code>recordTask()</code> now called for direct AI conversations, not just orchestrator tasks. Graph learns about all AI interactions.</li>
+ <li><strong><code>memgraph-learn</code> command</strong> — Documented in help.html but never implemented. Now creates concept+fact nodes with weighted edge. <code>memgraph-learn &lt;entity&gt; &lt;fact&gt;</code>.</li>
+ <li><strong><code>getStats()</code> alias</strong> — Orchestrator's <code>_pipelineVerify()</code> called <code>memgraph.getStats()</code> but the method was <code>showStats()</code>. Added <code>getStats()</code> alias returning <code>{nodes, edges, types, totalAccesses}</code>.</li>
+ <li><strong>World Model summary fix</strong> — <code>_getTotalModules()</code> returned registry count (7) instead of <code>max(loaded, registry)</code> (43). Summary now shows <code>43/43</code> not <code>43/7</code>.</li>
+ <li><strong>Dashboard World Model panel</strong> — New section showing version, last synthesized time, module/skill/file/agent counts, memory graph stats, health, active projects, recent changes.</li>
+ <li><strong>System prompt update</strong> — Now mentions <code>predict_task</code> and <code>world_model_query</code> capabilities.</li>
+ <li>Cache: <code>2026-07-04-v4.5.86</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.64–v4.5.67 — June 2026</span><span class="v-date">2026-06-18</span></div>
+ <ul>
+ <li><strong>Orphan removal</strong> — Removed 5 dead modules from core-bundle: <code>MyModule</code>, <code>ModuleIOModule</code>, <code>DynamicPromptsModule</code>, <code>SmartContextModule</code>, <code>DuckDuckGoModule</code>. 454 lines, 23KB saved. 12+ ghost commands cleaned from help.html. <code>modules-index.json</code> 50→45 entries. <code>rebuild-bundles.py</code> updated to prevent resurrection.</li>
+ <li><strong>Unified <code>reset</code> command</strong> — Supersedes <code>factory-reset</code>, <code>reset-vfs</code>, <code>reset-mem</code>, <code>config-reset</code>. Subcommands: <code>reset mem</code> (AI state), <code>reset vfs</code> (filesystem), <code>reset config</code> (settings), <code>reset all</code> (nuclear). Old commands kept as aliases.</li>
+ <li><strong>Agent loop detection</strong> — <code>agenticLoop</code> now tracks repeated tool calls. Same tool+args 3× triggers loop break with system message. Prevents AI from wasting 8 turns calling <code>ls</code>.</li>
+ <li><strong>Agent optimization default OFF</strong> — <code>trivial-optimization</code> was always ON by default (<code>!== 'false'</code>). Changed to default OFF (<code>=== 'true'</code>). Use <code>ai-config trivial-optimization on</code> to enable.</li>
+ <li><strong>JS Console fix</strong> — <code>js</code> / <code>js-run</code> commands were not in the security whitelist. At level 1+ they were classified "unknown" and routed to AI instead of dispatching. Added to whitelist — now works.</li>
+ <li><strong>Telemetry throttle</strong> — ServerMonitor persisted tool-latency logs every 20 calls. Throttled to every 100 calls or 5 minutes. ~80% fewer VFS writes.</li>
+ <li><strong>Pipe security fix</strong> — Pipe chains (<code>cmd1 | cmd2</code>) bypassed ALL security classification. <code>rm foo | cat</code> ran <code>rm</code> with zero checks even at Paranoid level. Each pipe stage now passes through <code>_classify()</code> — destructive and system commands blocked at level 1+.</li>
+ <li>Cache: <code>2026-06-18-v4.5.67</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.45–v4.5.63 — June 2026</span><span class="v-date">2026-06-12 to 2026-06-17</span></div>
+ <ul>
+ <li><strong>Critical ext bundle</strong> — 7 command-critical ext modules (editor, shell, alias, finder, filemgr, diff, busybox) promoted into <code>critical-ext-bundle.js</code> (2,365 lines). Solves HTTP 503 request-storm during boot.</li>
+ <li><strong>Core module bundle</strong> — All core modules concatenated into <code>core-bundle.js</code>. Single <code>&lt;script&gt;</code> tag replaces 30 individual loads. <code>rebuild-bundles.py</code> script added.</li>
+ <li><strong>VFS session bloat fixes</strong> — v2 metadata-only sessions (no VFS tree). Checkpoints strip system dirs, capped at 3. <code>_purgeTrash()</code> on boot (24h TTL). WebDAV backup deep-clones + strips.</li>
+ <li><strong>New modules</strong> — <code>module-agent-registry.js</code> (A2A transport), <code>module-leases.js</code> (agent lifecycle), <code>module-aiasm.js</code> (assembly), <code>module-duckduckgo.js</code> (search, later orphaned), <code>debug-panel.js</code>, <code>skill-pack-core.js</code>.</li>
+ <li><strong>A2A scratchpad architecture</strong> — Orchestrator agent-to-agent transport with prune timers, heartbeat checks, idle sweep. 6 <code>a2a-*</code> commands.</li>
+ <li><strong>Orchestrator rewrite</strong> — <code>delegateTask</code> re-enters full agentic loop. <code>spawnAgent</code>, <code>orchestrate-parallel</code>, <code>orchestrate-decompose</code>, consensus protocol.</li>
+ <li><strong>Deep architecture audit</strong> — 3 parallel subagent investigation of AI subsystem, VFS/state, security/dispatch. Found pipe security bypass, 5 orphaned modules, 6 duplicate localStorage keys, 19 unregistered intervals, 47 bare fetch() calls.</li>
+ <li>Cache: various — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.44 — June 2026</span><span class="v-date">2026-06-11</span></div>
+ <ul>
+ <li><strong>Mindmap organization overhaul</strong> — Pain point #5 shipped. The <code>mindmap</code> command went from 12 subcommands to <strong>40</strong>. <code>mindmap.js</code> grew from 321 to 1127 lines.</li>
+ <li><strong>Multi-mindmap support</strong> — No more losing work when switching. <code>mindmap new foo</code> + <code>mindmap new bar</code> creates two in-memory mindmaps. <code>mindmap switch foo</code> to jump between them. <code>mindmap list-loaded</code> shows all in-memory. Current is marked with <code>*</code>, dirty mindmaps with <code>[DIRTY]</code>.</li>
+ <li><strong>Dirty-flag warning on load</strong> — <code>mindmap load foo</code> now warns if the current mindmap has unsaved changes. Use <code>--force</code> to override.</li>
+ <li><strong>Find by text</strong> — <code>mindmap find &lt;text&gt;</code> searches all node text + tags (case-insensitive). Returns matching IDs and depth.</li>
+ <li><strong>Move nodes</strong> — <code>mindmap move &lt;id&gt; &lt;new-parent-id&gt;</code> reparents a node (with all its children). Validates against cycles (can't move under self or descendant).</li>
+ <li><strong>Undo</strong> — Stack of last 10 destructive ops. <code>mindmap undo</code> reverses: add, remove, rename, move, color, tag, weight, merge.</li>
+ <li><strong>Human-readable aliases</strong> — <code>mindmap alias &lt;id&gt; &lt;name&gt;</code> gives a memorable name. All commands that take an ID (<code>edit</code>, <code>move</code>, <code>color</code>, etc.) now accept either ID or alias.</li>
+ <li><strong>Disambiguated delete</strong> — <code>mindmap delete &lt;name&gt;</code> still deletes a FILE (backward compat). New <code>mindmap remove &lt;id&gt;</code> removes a NODE. <code>mindmap delete-file</code> is the explicit form.</li>
+ <li><strong>Import</strong> — <code>mindmap import &lt;file&gt;</code> reads from VFS, auto-detects format (JSON/Mermaid/Markdown) by extension or content. Re-generates IDs to avoid collisions.</li>
+ <li><strong>Merge</strong> — <code>mindmap merge &lt;name&gt; [--into=&lt;id&gt;]</code> adds a saved mindmap under the current root (or specified parent). Conflict-free: just appends.</li>
+ <li><strong>Diff</strong> — <code>mindmap diff &lt;name1&gt; &lt;name2&gt;</code> shows added/removed/modified nodes between two saved mindmaps.</li>
+ <li><strong>Stats</strong> — <code>mindmap stats</code> shows total nodes, max depth, nodes per level, colors used, top 10 tags, dirty status, last save time.</li>
+ <li><strong>Tags cross-node</strong> — <code>mindmap tags-list [&lt;tag&gt;]</code> shows all tags + counts, or all nodes with a specific tag.</li>
+ <li><strong>Clone</strong> — <code>mindmap clone &lt;id&gt;</code> duplicates a node and its children with new IDs. Text gets " (copy)" suffix.</li>
+ <li><strong>Archive</strong> — <code>mindmap archive &lt;name&gt;</code> saves to <code>/mindmaps/archive/&lt;name&gt;-&lt;ts&gt;.json</code>. Auto-cleans to last 20.</li>
+ <li><strong>Weight</strong> — <code>mindmap weight &lt;id&gt; &lt;1-5&gt;</code> scales node size in SVG output (1=small, 5=large, default=3).</li>
+ <li><strong>Mermaid export</strong> — <code>mindmap mermaid</code> (or <code>mindmap export mermaid</code>) prints Mermaid mindmap syntax. Auto-escapes parentheses, brackets, ampersands.</li>
+ <li><strong>JSON export</strong> — <code>mindmap json</code> prints the internal JSON (with aliases). Both Markdown and JSON get copied to clipboard.</li>
+ <li><strong>Refresh HTML</strong> — <code>mindmap refresh-html</code> regenerates the standalone architecture page from <code>modules-index.json</code>. Writes to <code>/mindmaps/_generated-architecture.html</code> in VFS, prints preview. Deploy with <code>cat &lt;file&gt; &gt; mindmap.html</code>.</li>
+ <li><strong>SVG layout fix</strong> — New <code>_layoutTree()</code> algorithm computes subtree widths first, then positions. Handles 30+ node mindmaps without going off-screen. <code>weight</code> affects node size.</li>
+ <li>Cache: <code>2026-06-11-v4.5.44</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.43 — June 2026</span><span class="v-date">2026-06-11</span></div>
+ <ul>
+ <li><strong>Module management overhaul</strong> — Pain point #3 finally shipped. <code>module-list</code> now groups by category (CORE/EXTENSION/FOUNDATION), supports <code>--core --ext --foundation --lazy --eager --loaded --unloaded --failed --disabled --search=&lt;pat&gt; --flat</code> flags, and shows aggregate summary (size, count, failed, disabled).</li>
+ <li><strong><code>module-info &lt;name&gt;</code></strong> — Shows module details: status, category, type, path, size, lazy flag, <strong>and the full list of commands it provides</strong> (the biggest gap). Listed in 2 columns.</li>
+ <li><strong><code>module-stats</code></strong> — Aggregate view: total/loaded/unloaded/lazy/eager/failed/disabled + total size + total commands.</li>
+ <li><strong><code>module-disable / module-enable</code></strong> — Persistently disable a module from auto-loading. Stored in <code>localStorage</code> <code>webx11_module_disabled</code>. Survives refresh. <code>module-loader.js</code> now checks this list and skips disabled modules with a <code>⊘</code> marker in the splash.</li>
+ <li><strong><code>module-reload / module-reset</code></strong> — Reload shows old→new diff. Reset clears module's localStorage state and reloads.</li>
+ <li><strong>System troubleshooting commands</strong> — Pain point #4.</li>
+ <li><strong><code>version</code></strong> — Single command shows: version, build date (parsed from script tag), cache key, browser engine, online status.</li>
+ <li><strong><code>uptime</code></strong> — Time since page load (formatted <code>Xh Ym Zs</code>) + last save time.</li>
+ <li><strong><code>errors [n]</code></strong> — Alias for <code>logs [n] ERROR</code>. Shows last N errors. Says "No errors found" when logs are clean.</li>
+ <li><strong><code>doctor</code></strong> — Runs 8 health checks (network, ollama, VFS size, heap, logs, recent errors, localStorage quota, disabled modules), ranks by severity (🔴/🟡/🟢), shows top 5, and gives recommended actions.</li>
+ <li><strong><code>bug-report</code></strong> — Generates a markdown report with system info, recent errors, recent commands, disabled modules, top-level VFS listing. Saves to <code>/tmp/bug-report-&lt;timestamp&gt;.md</code>. Send this file when reporting a bug.</li>
+ <li><strong><code>test-net</code></strong> — Checks browser online status, proxy API (<code>/api/stats</code>), Ollama connection, and internet (HEAD to duckduckgo.com). Green/red status for each.</li>
+ <li><strong><code>mem</code> enhanced</strong> — Now shows JS heap + localStorage size + sessionStorage size + VFS in-memory size + total. Color-coded thresholds (green/yellow/red).</li>
+ <li><strong>Trust violations fixed</strong> — Removed ghost commands <code>commands</code> and <code>check</code> from <code>getHelpInfo()</code> (they were in help but never registered). Fixed <code>memory-map-export</code> to actually save to <code>/memory/reference.md</code> (was: print to terminal).</li>
+ <li>Cache: <code>2026-06-11-v4.5.43</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.41 — June 2026</span><span class="v-date">2026-06-11</span></div>
+ <ul>
+ <li><strong>Search result quality fix</strong> — Default engines changed from <code>google,bing,brave,wikipedia</code> to <code>mojeek,bing</code>. Previous default had 3/4 engines broken (Google/Brave/Wikipedia block datacenter IPs); Bing worked but returned French-localized results due to server's Amsterdam IP. <strong>Mojeek</strong> is a privacy-focused crawler-based search that returns English results regardless of server location.</li>
+ <li><strong>Result deduplication</strong> — <code>/api/search</code> now dedupes results by domain and <strong>prefers Mojeek</strong> over Bing (Mojeek is more relevant in this environment). Two-pass: Mojeek first, then Bing for domains not already seen.</li>
+ <li><strong>Verified</strong>: <code>search quantum computing</code> now returns "Quantum computing - Wikipedia" as the #1 result (was "iPhone - Apple"). <code>search how to cook risotto</code> now returns actual risotto recipes (was "Cook, les épices bio" - a French spice site).</li>
+ <li>Cache: <code>2026-06-11-v4.5.41</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.40 — June 2026</span><span class="v-date">2026-06-10</span></div>
+ <ul>
+ <li><strong>AI assistant POST support</strong> — <code>web_fetch</code> tool now accepts <code>method=POST</code>, <code>body</code>, and <code>content_type</code> params. AI can now fetch POST-only APIs (Hyperliquid, financial APIs) without telling the user to "use curl". POST routes through <code>/api/fetch</code>; GET still uses <code>/api/extract</code> for cleaner output.</li>
+ <li><strong>AI assistant error parsing</strong> — <code>web_search</code> and <code>web_fetch</code> now parse JSON errors from proxy and surface the real reason (was just "HTTP 500" with no details).</li>
+ <li><strong>30s frontend timeout</strong> — <code>fetch</code>, <code>extract</code>, and <code>search</code> now abort after 30s with a helpful message. Replaces 15s silent freeze. Implementation: <code>AbortController</code>.</li>
+ <li><strong>Loading indicator for <code>--summarize</code></strong> — Shows "⏳ AI overview will be generated (1-3s)..." before the LLM call, so user knows the terminal is alive.</li>
+ <li><strong>Extract follow-up hint</strong> — After <code>extract</code>, shows: "💡 Try <code>summarize &lt;url&gt;</code> for AI summary, or <code>nvfs-resolve &lt;concept&gt;</code> to find related files".</li>
+ <li><strong>Backend script/style strip</strong> — Extract's regex fallback now strips <code>&lt;script&gt;</code>, <code>&lt;style&gt;</code>, <code>&lt;noscript&gt;</code> and HTML comments. Prevents raw JS leaking into extracted text on BS4 failures.</li>
+ <li><strong><code>web</code> alias removed</strong> — Was a duplicate of <code>fetch</code> that didn't appear in autocomplete. Confusing. Just use <code>fetch</code>.</li>
+ <li>Cache: <code>2026-06-10-v4.5.40</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.39 — June 2026</span><span class="v-date">2026-06-10</span></div>
+ <ul>
+ <li><strong>POST support in <code>/api/fetch</code></strong> — Proxy was GET-only, blocking any API that requires POST (Hyperliquid <code>/info</code>, etc). Now accepts <code>method=POST&amp;body=...&amp;content_type=...</code> query params with 10KB body cap and method-aware cache key.</li>
+ <li><strong><code>fetch</code> command gains 3 flags</strong> — <code>--post=BODY</code> sends a POST request with the given body, <code>--type=CT</code> sets the Content-Type header (defaults to <code>application/json</code> if <code>--post</code> is set), <code>--raw</code> shows untruncated response. Example: <code>fetch https://api.hyperliquid.xyz/info --post='{"type":"l2Book","coin":"HYPE"}' --type=application/json</code></li>
+ <li><strong>Better error messages</strong> — Frontend <code>fetch</code> now parses JSON error from proxy and shows the real reason (was just "HTTP 502" with no details).</li>
+ <li>Cache: <code>2026-06-10-v4.5.39</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.38 — June 2026</span><span class="v-date">2026-06-10</span></div>
+ <ul>
+ <li><strong>Keyboard shortcuts</strong> — <code>Ctrl+L</code> clears the output area, <code>Ctrl+C</code> cancels the current input line (prints <code>^C</code>), <code>Ctrl+U</code> clears the input field. Standard terminal conventions now work.</li>
+ <li><strong>Shift+Tab cycles backward</strong> — autocomplete suggestions can be cycled in both directions.</li>
+ <li><strong>Send button → 48px tappable</strong> — was 31px (fails mobile/tablet tap target). Now <code>min-width: 48px; min-height: 48px</code> per Android/iOS standards.</li>
+ <li><strong>Autocomplete argType fixes</strong> — <code>nvfs-config</code>, <code>nvfs-alias</code>, <code>skill-list</code> now show enum values (config keys, alias actions, skill categories) instead of file paths. <code>nvfs-sync</code>/<code>nvfs-save</code>/<code>nvfs-cancel</code>/<code>nvfs-status</code> marked as <code>argType: 'none'</code> so they don't show file suggestions.</li>
+ <li><strong>Stale autocomplete cleared</strong> — Tab suggestions from prior input no longer linger when you start typing something new.</li>
+ <li><strong>History cap on startup</strong> — <code>terminal-cmd-history</code> config now applies on page load, not "next save" (which never happened).</li>
+ <li>Cache: <code>2026-06-10-v4.5.38</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.37 — June 2026</span><span class="v-date">2026-06-10</span></div>
+ <ul>
+ <li><strong>/skills/index.json re-index loop FIXED</strong> — <code>skill-pack-core.seed()</code> was rewriting the <code>updated</code> timestamp for all 8 core skills on every page refresh, causing NVFS to see a hash mismatch and re-queue the 7KB JSON for LLM extraction every run. Now <code>seed()</code> compares existing file content to bundled content and only writes the index if something actually changed. Eliminates the 5-concepts-from-1-file loop on every refresh.</li>
+ <li><strong>One-time data fix</strong> — After deploying, the existing stale <code>updated</code> timestamp in your VFS will be re-evaluated on next refresh. If it matches the bundled timestamp, the loop stops immediately. If not, one final write happens and the loop is dead thereafter.</li>
+ <li>Cache: <code>2026-06-10-v4.5.37</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.36 — June 2026</span><span class="v-date">2026-06-10</span></div>
+ <ul>
+ <li><strong>Deep pass output shows file paths</strong> — Was "5 LLM concepts across 1 file" (no path). Now lists up to 20 files with their concept counts. Makes it obvious if a single file keeps re-processing every refresh (loop detection).</li>
+ <li><strong>Auto-sync output shows file paths</strong> — Same treatment: "re-indexed N file(s)" now lists the actual paths so you can see what's looping.</li>
+ <li><strong>nvfs-status shows LLM/fast breakdown</strong> — Two new lines: "Fast concepts: N" and "LLM concepts: N (across M files)". Proves persistence is working without needing DevTools inspection.</li>
+ <li><strong>Deep pass message clearer</strong> — "added N new LLM concepts (M files). Total in index: X" — distinguishes the per-pass delta from the persisted total.</li>
+ <li>Cache: <code>2026-06-10-v4.5.36</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.35 — June 2026</span><span class="v-date">2026-06-10</span></div>
+ <ul>
+ <li><strong>NVFS bug fix audit (7 bugs)</strong> — Stale <code>/sys/...</code> entries from pre-v4.5.34 builds cause permanent re-index loops. <code>init()</code> now auto-purges them on first run with a one-time warning.</li>
+ <li><strong>Cancel responsive</strong> — <code>nvfs-cancel</code> now checks between every LLM call (was: between every batch of 3). Stop within one 30s call instead of waiting up to 90s.</li>
+ <li><strong>Save race fixed</strong> — <code>nvfs-save</code> and end of deep pass now await in-flight VFS save instead of clobbering the debounce flag. Prevents silent LLM concept loss on concurrent writes.</li>
+ <li><strong>init() now awaits VFS load</strong> — Was fire-and-forget; first <code>nvfs-status</code> could return empty even when an index existed. <code>module-manager.js</code> already awaits init(), so making it async is safe.</li>
+ <li><strong>Deep-mode warning shown once</strong> — Was spammy on every <code>nvfs-index</code> call. Tracked by session flag <code>_warnedAboutLLMStrip</code>.</li>
+ <li><strong>Tag extraction fixed</strong> — <code>_extractConcepts</code> used <code>tagMatches[0]</code> (whole match) instead of capture group, so inline <code>#tag1 #tag2</code> only captured <code>#tag1</code>. Now correctly splits on commas or captures all hash tags.</li>
+ <li><strong>Filter defensive</strong> — <code>_isSystemFile</code> split into clean two-check structure. <code>node_modules</code> check now uses normalized path so <code>node_modules/foo.js</code> (no leading slash) also gets filtered.</li>
+ <li>Cache: <code>2026-06-10-v4.5.35</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.34 — June 2026</span><span class="v-date">2026-06-10</span></div>
+ <ul>
+ <li><strong>NVFS deep index persistence</strong> — LLM-generated concepts now survive page refresh. <code>_processDeepQueue()</code> calls <code>_saveIndex()</code>. Deep concepts written to disk on completion.</li>
+ <li><strong>nvfs-save command</strong> — <code>nvfs-save</code> force-saves the semantic index to VFS disk immediately. Useful before hard refresh.</li>
+ <li><strong>Auto-sync deep mode</strong> — <code>nvfs-sync --deep</code> or <code>nvfs-config deep-index on</code> now triggers LLM analysis for dirty files, not just fast filename concepts.</li>
+ <li><strong>Safety guard</strong> — Running <code>nvfs-index</code> without <code>--deep</code> when saved index contains LLM concepts prints a warning: they'll be stripped from re-indexed files.</li>
+ <li><strong>JS Console module</strong> — New <code>js &lt;expression&gt;</code> command evals JavaScript in page context. <code>js-run &lt;path&gt;</code> loads and executes a VFS script file. Auto-loads at startup.</li>
+ <li><strong>Module registration</strong> — <code>console</code> added to packages.json, terminal.js ext loader, and skipList. Auto-loads on next refresh.</li>
+ <li>Cache: <code>2026-06-10-v4.5.34</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.24 — June 2026</span><span class="v-date">2026-06-08</span></div>
+ <ul>
+ <li><strong>tar / untar commands</strong> — New pure-JS tarball parser in cryptogzip.js. <code>tar &lt;src-dir&gt; &lt;out.tar[.gz]&gt;</code> bundles a VFS directory; <code>untar &lt;path.tar[.gz]&gt; [dest-dir]</code> extracts back. Auto-detects gzip wrapper. Fixes the "downloaded newos4.tar.gz but can't extract" problem.</li>
+ <li><strong>Binary-safe gzip</strong> — <code>gunzip</code> now preserves binary content (was UTF-8 only). Updated to use base64 roundtrip for binary files.</li>
+ <li><strong>POSIX ustar format</strong> — Reads &amp; writes standard ustar tarballs (compatible with GNU tar, BSD tar, macOS tar).</li>
+ <li><strong>Cheatsheet updated</strong> — Added 8 entries under 🔧 Tools for tar/untar/gzip/gunzip/encrypt/decrypt/diff/edit</li>
+ <li><strong>System prompt</strong> — tar/untar added to [ALL OTHERS] command list</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.23 — June 2026</span><span class="v-date">2026-06-08</span></div>
+ <ul>
+ <li><strong>Doc version sync</strong> — Help page title, overview, and footer updated to v4.5.22 to match deployed module-loader.js</li>
+ <li><strong>Whitepaper v4.5.22</strong> — Bumped from v4.3.0-beta; Last Updated 2026-06-08</li>
+ <li><strong>Deprecated module-modmgr</strong> — Removed from BUILTIN array (was duplicate of module-manager.js). Set auto_load=false, layer=deprecated. File retained for one release for rollback safety; deletion scheduled v4.6.0</li>
+ <li><strong>Fixed modules-index class</strong> — <code>module-loader</code> class changed from filename string <code>"module-loader.js"</code> to <code>null</code> (foundation file, not a module class)</li>
+ <li><strong>packages.json cleanup</strong> — Renamed <code>neural-vfs</code> to <code>neural-vfs-core</code>, category → <code>core</code>, added note that NVFS is built-in not a WPM package</li>
+ <li><strong>Cheatsheet coverage</strong> — Added 4 missing commands: <code>sandbox-config</code>, <code>memgraph-export</code>, <code>predictor-config</code>, <code>world-model-config</code></li>
+ <li><strong>Audit script fix</strong> — <code>audit-newos4-filesystem.py</code> now respects <code>skills/index.json</code> for orphan detection (no more 26 false-positive warnings on valid skill JSONs)</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5 — June 2026</span><span class="v-date">2026-06-06</span></div>
+ <ul>
+<li><strong>Security Config CLI</strong> — <code>security-destructive</code>, <code>security-system</code>, <code>security-whitelist</code>, <code>security-flag</code> add|remove|list with localStorage persistence and boot loading</li>
+ <li><strong>Enhanced security command</strong> — Shows all sets (destructive, system, whitelist, flags) plus registered command count</li>
+ <li><strong>AI loop config</strong> — <code>ai-config max-turns|max-context|trivial-optimization</code> with persistence across sessions</li>
+ <li><strong>Unified config CLI</strong> — <code>config</code> command with 4 terminal tuning keys (max-lines, history, pipe-buffer), all persisted in localStorage</li>
+ <li><strong>Extended ai-config</strong> — New keys: <code>max-context-tokens</code>, <code>confirm-timeout</code>, <code>chat-lock-timeout</code>, <code>skill-debounce</code> with range validation</li>
+ <li><strong>Safe-read run_command</strong> — <code>run_command</code> now command-aware: <code>ls</code>, <code>find</code>, <code>tree</code>, <code>cat</code> auto-approved; only mutating commands require confirmation</li>
+ <li><strong>Whitelist expanded (50+ cmds)</strong> — find, grep, tree, head, tail, wc, sort, du, diff, which, hostname, and all BusyBox read-only utils now whitelisted</li>
+ <li><strong>Security-bypass fix</strong> — <code>run_command</code> AI tool now routes through <code>checkAndRun()</code> security layer instead of bypassing</li>
+ <li><strong>list_files root fallback</strong> — relative paths retry from root if cwd-relative lookup fails</li>
+ <li><strong>Confirm timeout configurable</strong> — <code>ai-config confirm-timeout &lt;seconds&gt;</code> accepts 0+ (0 = no confirm / auto-approve all, no upper cap). Dynamic label on confirm bar shows actual timeout.</li>
+ <li><strong>Auto-sync Neural VFS</strong> — <code>write_file</code>/<code>mkdir</code>/<code>rm</code>/<code>mv</code>/<code>cp</code> automatically mark index dirty; auto-reindexes after 10s debounce. <code>nvfs-alias</code> command for concept aliases. Hybrid discovery fallback in <code>nvfs-resolve</code> (live VFS scan when index empty). Staleness warning in <code>nvfs-status</code> if index >4h old.</li>
+ <li><strong>5 modules whitelisted</strong> — system-health, server, servers, server-check, monitor run directly</li>
+ <li><strong>OllamaModule consolidated</strong> — Removed duplicate <code>ollamamodels</code> code path (redirects to <code>models</code>), centralized endpoint via <code>ollamaManager.endpoint</code></li>
+ <li><strong>Neural VFS async indexing</strong> — Chunked processing with progress percentage, no UI freeze on large VFS. Improved <code>_summarize</code> (section heading → paragraph → sentence → fallback)</li>
+ <li><strong>Neural VFS display-limit config</strong> — <code>nvfs-config</code> command with 4 keys (fuzzy-limit, tree-files, tree-concepts, resolve-files), persisted</li>
+ <li><strong>Orchestrator event bus</strong> — In-memory pub/sub for instant A2A delivery (VFS polling remains as fallback). Improved <code>_deriveCapabilities</code> with 14 keyword mappings</li>
+ <li><strong>Orchestrator extended config</strong> — 4 new interval keys (a2a-poll-interval, heartbeat-check, idle-sweep-interval, dead-purge-timeout) + all config now persists across sessions</li>
+ <li>Cache: <code>2026-06-06-v4.5.11</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.12 — June 2026</span><span class="v-date">2026-06-06</span></div>
+ <ul>
+ <li><strong>Tiered Indexing toggle</strong> — <code>nvfs-config deep-index on|off</code> switches between Fast (metadata-only, instant) and Deep (LLM-assisted, semantic) modes. Off by default. <code>nvfs-index --deep</code> or <code>nvfs-sync --deep</code> forces deep mode per-run.</li>
+ <li><strong>Configurable deep model</strong> — <code>nvfs-config deep-model &lt;name&gt;</code> selects the LLM for deep indexing. Default: <code>gemma4:31b-cloud</code> for speed. Example: <code>nvfs-config deep-model gemma4:31b-cloud</code> for deeper analysis.</li>
+ <li><strong>Status bar indicator</strong> — Real-time 🧠 status in terminal bar: concept count + 🌊 wave for deep mode, 🟡 amber for pending auto-sync, 🧠 Deep Indexing progress percentage during LLM processing.</li>
+ <li><strong>nvfs-sync command</strong> — <code>nvfs-sync [--deep]</code> for incremental background sync of dirty files.</li>
+ <li><strong>Deep concept extraction</strong> — LLM generates 2-5 semantic concepts per file (configurable model via <code>nvfs-config deep-model</code>, default <code>gemma4:31b-cloud</code>, 2000 chars, 3-file batches with UI yield). Fast filename-concepts stored immediately, LLM concepts augment in background queue.</li>
+ <li><strong>nvfs-status mode display</strong> — Shows 🌊 Deep or ⚡ Fast mode.</li>
+ <li><strong>LLM self-healing</strong> — If Ollama disconnected, deep mode silently falls back to fast metadata-only.</li>
+ <li>Cache: <code>2026-06-06-v4.5.12</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.2 — June 2026</span><span class="v-date">2026-06-06</span></div>
+ <ul>
+ <li><strong>VFS Size Diagnostics</strong> — <code>vfs-size</code> shows total VFS size, top 5 largest files, and system dir sizes</li>
+ <li><strong>VFS Clean</strong> — <code>vfs-clean</code> / <code>vfs clean</code> — nuke snapshots, sessions, trash in one shot</li>
+ <li><strong>Fractal checkpoint fix</strong> — <code>checkpoint()</code> no longer deep-clones its own <code>/_snapshots/</code>, eliminating recursive bloat. Same for <code>session-save</code></li>
+ <li><strong>Ghost commands wired</strong> — <code>checkpoint</code>, <code>checkpoint-list</code>, <code>checkpoint-del</code>, <code>rollback</code> had help text but no handlers — now actually work</li>
+ <li><strong>Autocomplete fix</strong> — Built-in commands (vfs-clean, vfs-size) now appear in dropdown</li>
+ <li><strong>Editor→_mods bridge</strong> — <code>edit path/to/module.js</code> now mirrors to <code>_mods/</code> for test_module compatibility</li>
+ <li><strong>Skill WebDAV Sync</strong> — <code>skill-webdav-sync</code> pushes all skills to WebDAV as JSON files</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.5.11 — June 2026</span><span class="v-date">2026-06-06</span></div>
+ <ul>
+ <li><strong>init(terminal) fix</strong> — <code>loadModuleFromCode</code> and <code>loadModuleFromURL</code> now pass <code>this.terminal</code> to <code>init()</code>, fixing PredictorModule fatal error</li>
+ <li><strong>Save & Test returns value</strong> — <code>saveCodeEditor</code> checks <code>loadModuleFromCode</code> return before printing success</li>
+ <li><strong>StateSyncModule recovered</strong> — Extracted from backup, saved to <code>~/.hermes/scripts/</code></li>
+ <li><strong>PredictorModule hasSync</strong> — Feature detection for sync/recall tasks with tuned duration/turn prediction</li>
+ <li>Cache: <code>2026-06-06-v4.5.11</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.4 — June 2026</span><span class="v-date">2026-06-05</span></div>
+ <ul>
+ <li><strong>v4.4 complete</strong> — 46 modules, 24 skills, full A2A lifecycle, sandboxed execution, memory graph, world model, health monitoring</li>
+ <li><strong>A2A Mailbox Transport</strong> — <code>a2a-send</code>, <code>a2a-list</code>, 500ms polling with envelope routing (request/response/broadcast/heartbeat)</li>
+ <li><strong>Autonomous sub-agent spawning</strong> — auto-role derivation, sleep/reanimate VFS serialization</li>
+ <li><strong>Sandboxed execution</strong> — WebWorker pool <code>sandbox-run</code> (there is no <code>sandbox-kill</code>; tasks end on timeout) with inline fallback for headless</li>
+ <li><strong>A2A Protocol</strong> — Agent contract system: <code>contract-register</code>, <code>contract-list</code>, schema validation in orchestrator</li>
+ <li><strong>Self-Optimization Telemetry</strong> — <code>system-health</code> command showing tool latency, VFS stats, error rates</li>
+ <li><strong>AI tool: nvfs_resolve</strong> — AI can search VFS by concept</li>
+ <li><strong>Skills consolidated</strong> — 9 → 4 unified; index rebuilt minified</li>
+ <li><strong>Docs sync</strong> — vision.html and whitepaper updated</li>
+ <li>Cache: <code>2026-06-03-neuralvfs</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.3.2 — June 2026</span><span class="v-date">2026-06-04</span></div>
+ <ul>
+ <li><strong>mindmap svg</strong> — Inline SVG rendering with colored rects + bezier curves</li>
+ <li><strong>GUI Builder</strong> — <code>gui</code>, <code>gui-button</code>, <code>gui-input</code> — SimpleGUIModule registered</li>
+ <li><strong>mermaid.min.js</strong> — Downloaded for webx11_mindmap.html</li>
+ <li><strong>get_module_status fixed</strong> — Shows all 34 loaded modules (was only HTTP-loaded)</li>
+ <li><strong>view_module fixed</strong> — Uses modules-index.json to resolve class→file paths</li>
+ <li><strong>module-ollamamodels merged</strong> — Into module-ollama (13 commands total)</li>
+ <li><strong>concept:// URIs</strong> — <code>cat concept://skill</code> resolves via Neural VFS</li>
+ <li><strong>Module categorization</strong> — <code>modules-list</code> shows [core], [ext], [infra] layer tags</li>
+ <li><strong>5 modules whitelisted</strong> — system-health, server, servers, server-check, monitor run directly</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.3 — June 2026</span><span class="v-date">2026-06-02</span></div>
+ <ul>
+ <li><strong>WebDAV Browser</strong> — Modern file-tree UI with checkboxes, Select All, bulk actions (Delete, Move, Download), inline rename (double-click), context menu, breadcrumbs, keyboard shortcuts</li>
+ <li><strong>WebDAV commands</strong> — <code>webdav-browser</code>, <code>webdav-rm</code>, <code>webdav-mkdir</code>, <code>webdav-rename</code>, <code>webdav-move</code>, <code>webdav-backup</code></li>
+ <li><strong>Skill Pack System</strong> — Auto-loads 12 skills on startup, seed/reload support, skill-pack-core.js manages in-memory index</li>
+ <li><strong>Agent Orchestration</strong> — <code>agent-list</code>, <code>agent-register</code>, <code>lease-list</code>, <code>orchestrate</code> for multi-step delegation</li>
+ <li><strong>Security Layer</strong> — <code>security-level 0-3</code> with granular restrictions, <code>cmd-allow</code>/<code>cmd-deny</code> per-command access control</li>
+ <li><strong>Debug Panel</strong> — <code>debug</code> toggle shows memory, events, errors, module health</li>
+ <li><strong>Chat Cleanup</strong> — Removed <code>chat-setapi</code>, <code>chat-setmodel</code>, <code>chat-ask</code>; single <code>chat</code> command toggles overlay</li>
+ <li>Module count refactored: 18 core + 14 ext (was 25+17)</li>
+ <li><strong>Download fix</strong> — switched from <code>resp.text()</code> to <code>resp.blob()</code> for binary-safe downloads; same fix for image preview (FileReader DataURL)</li>
+ <li>Cache: <code>v=40</code> — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.2.1 — May 2026</span><span class="v-date">2026-05-27</span></div>
+ <ul>
+ <li><strong>BusyBox</strong> — 16 new utility commands: <code>which</code>, <code>env</code>, <code>date</code>, <code>cal</code>, <code>yes</code>, <code>seq</code>, <code>base64</code>, <code>printf</code>, <code>cut</code>, <code>uniq</code>, <code>hostname</code>, <code>whoami</code>, <code>basename</code>, <code>dirname</code>, <code>sleep</code>, <code>expand</code></li>
+ <li><strong>Shell</strong> — Full pipe (<code>|</code>), redirect (<code>&gt;</code>, <code>&gt;&gt;</code>, <code>&lt;</code>), chain (<code>&amp;&amp;</code>, <code>||</code>, <code>;</code>), variable (<code>$VAR</code>), and VFS script execution (<code>script</code>)</li>
+ <li>Pipe-aware commands (<code>grep</code>, <code>head</code>, <code>tail</code>, <code>sort</code>, <code>wc</code>, <code>cut</code>, <code>uniq</code>, <code>expand</code>) auto-read from pipe via temp file injection</li>
+ <li><code>export</code>, <code>unset</code>, <code>set</code> — shell variable management</li>
+ <li>Multiple ASI and VFS API bug fixes (<code>fs.fsWrite</code> → <code>fs.writeFile</code> across 3 modules)</li>
+ <li><strong>Test Suite</strong> — 32 automated regression tests. Run with <code>load-local modules/test/test-suite.js</code> then <code>test</code>, <code>test auto</code>, or <code>test list</code></li>
+ <li>Cache v34 — hard refresh required</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.2 — May 2026</span><span class="v-date">2026-05-25</span></div>
+ <ul>
+ <li><strong>Editor</strong> — <code>edit &lt;path&gt;</code> opens inline VFS editor modal</li>
+ <li><strong>Diff</strong> — <code>diff a b</code> LCS-based comparison + <code>diff --module</code></li>
+ <li><strong>Aliases</strong> — <code>ll</code>, <code>h</code>, <code>cls</code> defaults + custom alias definition</li>
+ <li><strong>File Finder</strong> — <code>find &lt;query&gt;</code> recursive VFS search</li>
+ <li>Agent loop: capped at 8 turns (was 1), <code>edit_module</code> destructive confirmation, model auto-validation</li>
+ <li><code>ai-stop</code> now cancels chat + stream, not just tool calls</li>
+ <li>Unknown commands awaited, "Command not found" suppressed on AI route</li>
+ <li><code>loopResult</code> scope fix — no more undefined error on AI fail</li>
+ <li>Preview iframe allows JS (<code>allow-scripts</code>), cache bump <code>v=18</code></li>
+ <li>Splash fallback + startup modal list all 12 ext modules</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.1</span><span class="v-date">2026-05-23</span></div>
+ <ul>
+ <li>AI Agent with full tool-calling loop (28 tools)</li>
+ <li>Inline destructive confirmation bar with 120s countdown timer</li>
+ <li>Skills system (8 CLI commands + UI modal)</li>
+ <li>Voice input (🎤 button, SpeechRecognition, show/hide toggle)</li>
+ <li>Sound notification (🔊 toggle, WebAudio ding)</li>
+ <li>AI spinner in status bar</li>
+ <li>Session system (save/load/recent/stats)</li>
+ <li>WebDAV sync (8 commands)</li>
+ <li>Web search (SearXNG + DDG + URL fetch/extract)</li>
+ <li>Config manager (6 commands + UI)</li>
+ <li>Startup modules selector (⚙ gear on splash)</li>
+ <li>Code editor modal (module editing, save & test live)</li>
+ <li>HTML preview via srcdoc (mobile)</li>
+ <li>File/dir autocomplete, pipe support</li>
+ <li>Smart context profiles, dynamic prompts with variables</li>
+ <li>Server monitor, ChatStudio overlay</li>
+ <li>Modular architecture: 23 JS modules, async loader</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
+ <div class="v-header"><span class="v-num">v4.0</span><span class="v-date">2026-05-20</span></div>
+ <ul>
+ <li>Monolith refactored into core/ext modules</li>
+ <li>Async module loader with splash screen</li>
+ <li>Config manager modal</li>
+ <li>Bootloader fix for disabled modules</li>
+ </ul>
+ </div>
+ </section>
+</div>
+</main>
+
+<footer>
+ <div class="links">
+ <a href="./main.html">Launch WebX11</a>
+ <a href="https://ollama.com" target="_blank">Ollama</a>
+ </div>
+ <p>WebX11 v4.5.22 · 2026</p>
+ <p class="tagline">Happy Hacking 🖥️🤖🎤</p>
+</footer>
+
+<a href="#" class="back-top" id="backTop">↑</a>
+
+<script>
+/* ── Back to Top ── */
+const bt = document.getElementById('backTop');
+window.addEventListener('scroll',()=>{bt.classList.toggle('visible',window.scrollY>300)});
+
+/* ── Search ── */
+function searchDocs(){
+ const q=document.getElementById('search-input').value.toLowerCase().trim();
+ const msg=document.getElementById('search-result-msg');
+ if(!q){msg.style.display='none';return}
+ // Search in ALL tabs, switch to the one with results
+ const tabs = document.querySelectorAll('.doc-tab');
+ let found = false;
+ tabs.forEach(tab => {
+ if (tab.textContent.toLowerCase().includes(q)) {
+ // Found a match in this tab — switch to it
+ const tabId = tab.id.replace('content-', 'tab-');
+ const radio = document.getElementById(tabId);
+ if (radio) {
+ radio.checked = true;
+ radio.dispatchEvent(new Event('change'));
+ }
+ // Highlight the matching element
+ const sections = tab.querySelectorAll('section, .quick-ref-item, .feature-card, .callout');
+ sections.forEach(s => {
+ if (s.textContent.toLowerCase().includes(q)) {
+ s.scrollIntoView({behavior:'smooth', block:'center'});
+ s.style.border='2px solid var(--accent)'; s.style.borderRadius='12px';
+ s.style.padding='0 20px'; s.style.transition='border .3s';
+ setTimeout(() => { s.style.border='none'; s.style.padding='0'; }, 2000);
+ found = true;
+ }
+ });
+ }
+ });
+ if(!found){
+ msg.textContent='No results for "'+q+'"';
+ msg.style.display='block';
+ setTimeout(()=>{msg.style.display='none'},3000);
+ }else{msg.style.display='none'}
+}
+document.getElementById('search-input').addEventListener('keypress',e=>{if(e.key==='Enter')searchDocs()});
+
+/* ── Smooth scroll nav — switch to User Guide tab first ── */
+document.querySelectorAll('.nav-link').forEach(l=>{
+ l.addEventListener('click',e=>{
+ e.preventDefault();
+ const href = l.getAttribute('href');
+ // Switch to User Guide tab first
+ const guideTab = document.getElementById('tab-guide');
+ if (guideTab) {
+ guideTab.checked = true;
+ guideTab.dispatchEvent(new Event('change'));
+ }
+ // Then scroll after a short delay so tab content becomes visible
+ setTimeout(() => {
+ const t = document.querySelector(href);
+ if (t) t.scrollIntoView({behavior:'smooth', block:'start'});
+ }, 100);
+ });
+});
+/* ── Doc Tabs ── */
+document.querySelectorAll('.doc-tabs input').forEach(radio=>{
+ radio.addEventListener('change',function(){
+ document.querySelectorAll('.doc-tab').forEach(t=>t.classList.remove('active'));
+ const tab=document.getElementById('content-'+this.id.replace('tab-',''));
+ if(tab)tab.classList.add('active');
+ localStorage.setItem('webx11_help_tab',this.id);
+ });
+});
+// Restore last tab
+(function(){
+ const saved = localStorage.getItem('webx11_help_tab');
+ if(saved){
+ const el = document.getElementById(saved);
+ if(el){ el.checked = true; el.dispatchEvent(new Event('change')); }
+ }
+})();
+
+console.log('WebX11 v4.3 Docs — Modern');
+</script>
+</body>
+</html>
diff --git a/.bkp-fix-20261023_143429/ingest.php b/.bkp-fix-20261023_143429/ingest.php
new file mode 100644
index 0000000..b513593
--- /dev/null
+++ b/.bkp-fix-20261023_143429/ingest.php
@@ -0,0 +1,123 @@
+<?php
+require_once __DIR__ . '/config.php';
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ error_response('Method not allowed', 405);
+}
+
+$raw = file_get_contents('php://input');
+if (!$raw) error_response('Empty request body');
+
+$payload = json_decode($raw, true);
+if (!$payload || !isset($payload['errors']) || !is_array($payload['errors'])) {
+ error_response('Invalid payload: expected { errors: [...], session: {...} }');
+}
+
+$errors = $payload['errors'];
+$session = $payload['session'] ?? [];
+$clientVersion = $payload['version'] ?? 'unknown';
+
+// Client-supplied counts are untrusted: clamp so a bad client cannot store 38,495× rows.
+function clamp_count($v) {
+ $n = (int)$v;
+ return $n < 1 ? 1 : ($n > 1000 ? 1000 : $n);
+}
+
+// Validate batch size
+if (count($errors) > MAX_ERRORS_PER_BATCH) {
+ $errors = array_slice($errors, 0, MAX_ERRORS_PER_BATCH);
+}
+if (strlen($raw) > MAX_BATCH_SIZE_KB * 1024) {
+ error_response('Payload too large (max ' . MAX_BATCH_SIZE_KB . 'KB)', 413);
+}
+
+// Load existing data
+$allErrors = load_json(ERRORS_FILE);
+$sessions = load_json(SESSIONS_FILE);
+
+$sessionId = $session['id'] ?? ('anon_' . bin2hex(random_bytes(4)));
+$ingested = 0;
+$now = date('c');
+
+// Update session metadata
+if (!isset($sessions[$sessionId])) {
+ $sessions[$sessionId] = [
+ 'id' => $sessionId,
+ 'first_seen' => $now,
+ 'version' => $clientVersion,
+ 'modules' => $session['modules'] ?? [],
+ 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
+ 'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
+ 'error_count' => 0,
+ ];
+}
+$sessions[$sessionId]['last_seen'] = $now;
+$sessions[$sessionId]['error_count'] += count($errors);
+$sessions[$sessionId]['version'] = $clientVersion;
+
+// Build a lookup of existing hashes for dedup
+$existingHashes = [];
+foreach ($allErrors as $idx => $existing) {
+ $h = $existing['hash'] ?? '';
+ if ($h) {
+ $existingHashes[$h] = $idx;
+ }
+}
+
+// Ingest each error (dedup by hash)
+foreach ($errors as $e) {
+ $hash = $e['hash'] ?? md5(($e['message'] ?? '') . '|' . ($e['source'] ?? '') . '|' . ($e['type'] ?? ''));
+
+ if (isset($existingHashes[$hash])) {
+ // Update existing entry
+ $idx = $existingHashes[$hash];
+ $allErrors[$idx]['count'] += clamp_count($e['count'] ?? 1);
+ $allErrors[$idx]['last_seen'] = $e['lastSeen'] ?? $now;
+ $allErrors[$idx]['ingested_at'] = $now;
+ if (!empty($e['stack'])) {
+ $allErrors[$idx]['stack'] = substr($e['stack'], 0, 2000);
+ }
+ $ingested++;
+ continue;
+ }
+
+ $entry = [
+ 'hash' => $hash,
+ 'message' => substr($e['message'] ?? '', 0, 500),
+ 'type' => $e['type'] ?? 'unknown',
+ 'severity' => $e['severity'] ?? 'error',
+ 'category' => $e['category'] ?? 'runtime',
+ 'source' => $e['source'] ?? '',
+ 'module' => $e['module'] ?? null,
+ 'stack' => isset($e['stack']) ? substr($e['stack'], 0, 2000) : null,
+ 'session_id' => $sessionId,
+ 'count' => clamp_count($e['count'] ?? 1),
+ 'first_seen' => $e['firstSeen'] ?? $now,
+ 'last_seen' => $e['lastSeen'] ?? $now,
+ 'ingested_at' => $now,
+ ];
+
+ $allErrors[] = $entry;
+ $existingHashes[$hash] = count($allErrors) - 1;
+ $ingested++;
+}
+
+// Trim to last 10,000 errors (ring buffer)
+if (count($allErrors) > 10000) {
+ $allErrors = array_slice($allErrors, -10000);
+}
+
+save_json(ERRORS_FILE, $allErrors);
+save_json(SESSIONS_FILE, $sessions);
+
+// Rebuild clusters asynchronously (flag file)
+$rebuildFlag = DEBUG_DATA_DIR . '/.rebuild_clusters';
+file_put_contents($rebuildFlag, time());
+
+json_response([
+ 'success' => true,
+ 'ingested' => $ingested,
+ 'session_id' => $sessionId,
+ 'total_errors' => count($allErrors),
+ 'total_sessions' => count($sessions),
+]);
diff --git a/.bkp-fix-20261023_143429/main.html b/.bkp-fix-20261023_143429/main.html
new file mode 100644
index 0000000..f41b12d
--- /dev/null
+++ b/.bkp-fix-20261023_143429/main.html
@@ -0,0 +1,1484 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+ <meta name="theme-color" content="#000000">
+ <title>WebX11 Module System</title>
+ <style>
+ :root {
+ --bg-color: #000000;
+ --bg-secondary: #0a0a0a;
+ --bg-tertiary: #111111;
+ --text-color: #00ff00;
+ --text-muted: #888888;
+ --accent-color: #00aa00;
+ --border-color: #005500;
+ --error-color: #ff5555;
+ --warning-color: #ffaa00;
+ --info-color: #00aaff;
+ --success-color: #00ff00;
+ --ai-text-color: #aaddff;
+ --ai-bg-color: #001a26;
+ --font-primary: 'Courier New', Courier, monospace;
+ }
+ * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
+ body {
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ font-family: var(--font-primary);
+ font-size: var(--term-font-size, 16px);
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+ @media (max-width: 600px) { body { font-size: var(--term-font-size, 12px); } }
+ header {
+ border-bottom: 1px solid var(--border-color);
+ padding: 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--bg-secondary);
+ flex-shrink: 0;
+ }
+ .header-title { font-weight: bold; font-size: 1.1em; }
+ .header-version { font-size: 0.7em; color: var(--text-muted); }
+ .status-pill {
+ font-size: 0.8em;
+ padding: 2px 6px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ background: var(--bg-tertiary);
+ }
+ #terminal-container {
+ flex-grow: 1;
+ overflow-y: auto;
+ padding: 10px;
+ scroll-behavior: smooth;
+ -webkit-overflow-scrolling: touch;
+ transition: padding 0.2s;
+ }
+ #terminal-container.terminal-expanded {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100vw;
+ height: 100vh;
+ z-index: 1000;
+ background: var(--bg-color);
+ padding: 16px;
+ }
+ #terminal-container.terminal-expanded + #input-area,
+ #terminal-container.terminal-expanded ~ #input-area {
+ display: none;
+ }
+ .line-copy-btn {
+ float: right;
+ font-size: 0.7em;
+ padding: 2px 6px;
+ margin-left: 6px;
+ border: 1px solid var(--border-color);
+ background: var(--bg-tertiary);
+ color: var(--text-muted);
+ border-radius: 4px;
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.15s;
+ }
+ .output-line:hover .line-copy-btn { opacity: 1; }
+ .line-copy-btn:active { background: var(--accent-color); color: #000; }
+ #ai-confirm-area {
+ display: none;
+ flex-shrink: 0;
+ padding: 10px;
+ margin: 0 8px 4px;
+ border: 1px solid var(--warning-color);
+ border-radius: 6px;
+ background: var(--bg-secondary);
+ z-index: 200;
+ font-size: 0.95em;
+ min-height: 48px;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: center;
+ pointer-events: auto;
+ }
+ #ai-confirm-area.visible { display: flex; }
+ #ai-confirm-area.hidden { display: none !important; }
+ #ai-confirm-area .confirm-desc {
+ flex: 1;
+ min-width: 120px;
+ color: var(--warning-color);
+ word-break: break-word;
+ }
+ #ai-confirm-area .confirm-btn {
+ padding: 12px 24px;
+ border: 1px solid var(--success-color);
+ color: var(--success-color);
+ background: transparent;
+ border-radius: 6px;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 1.1em;
+ font-weight: bold;
+ touch-action: manipulation;
+ min-height: 48px;
+ min-width: 90px;
+ user-select: none;
+ -webkit-user-select: none;
+ transition: background 0.15s;
+ }
+ #ai-confirm-area .confirm-btn:active {
+ background: rgba(0, 255, 0, 0.15);
+ }
+ #ai-confirm-area .confirm-btn.deny {
+ border-color: var(--error-color);
+ color: var(--error-color);
+ }
+ #ai-confirm-area .confirm-timer {
+ font-size: 0.8em;
+ color: var(--warning-color);
+ flex-shrink: 0;
+ }
+ #ai-confirm-area .confirm-timer.urgent {
+ color: var(--error-color);
+ font-weight: bold;
+ }
+ #ai-confirm-area .confirm-timer.critical {
+ color: #ff0000;
+ font-weight: bold;
+ animation: pulse 1s infinite;
+ }
+ /* ── Startup Modules Modal ── */
+ .output-line { white-space: pre-wrap; word-break: break-word; line-height: 1.4; }
+ .output-line.command-prompt { color: var(--text-color); font-weight: bold; }
+ .output-line.error { color: var(--error-color); }
+ .output-line.warning { color: var(--warning-color); }
+ .output-line.info { color: var(--info-color); }
+ .output-line.success { color: var(--success-color); }
+ .output-line.text-muted { color: var(--text-muted); }
+ .output-line.ollama {
+ color: var(--ai-text-color);
+ background: var(--ai-bg-color);
+ padding: 8px;
+ border-radius: 4px;
+ border: 1px solid #003344;
+ margin-bottom: 10px;
+ position: relative;
+ }
+ .output-line.user-prompt-line {
+ display: flex;
+ align-items: flex-start;
+ gap: 6px;
+ flex-wrap: wrap;
+ padding: 4px 0;
+ }
+ .output-line.user-prompt-line .prompt-prefix {
+ color: var(--accent-color);
+ white-space: nowrap;
+ flex-shrink: 0;
+ }
+ .output-line.user-prompt-line .prompt-text {
+ color: var(--text-color);
+ word-break: break-word;
+ flex: 1;
+ min-width: 0;
+ }
+ .copy-btn, .edit-btn {
+ font-size: 0.7em;
+ padding: 2px 8px;
+ cursor: pointer;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ color: var(--text-muted);
+ touch-action: manipulation;
+ flex-shrink: 0;
+ transition: background 0.15s;
+ }
+ .copy-btn:hover, .edit-btn:hover,
+ .copy-btn:active, .edit-btn:active { background: var(--accent-color); color: var(--bg-color); }
+ #input-area {
+ border-top: 1px solid var(--border-color);
+ padding: 8px 10px;
+ background: var(--bg-secondary);
+ display: flex;
+ align-items: flex-start;
+ flex-shrink: 0;
+ position: relative;
+ }
+ .prompt-label { margin-right: 8px; color: var(--accent-color); white-space: nowrap; margin-top: 6px; }
+ #command-input {
+ flex-grow: 1;
+ background: transparent;
+ border: none;
+ color: var(--text-color);
+ font-family: var(--font-primary);
+ font-size: inherit;
+ outline: none;
+ resize: none;
+ overflow-y: auto;
+ max-height: 120px;
+ line-height: 1.4;
+ padding: 4px 0;
+ }
+ #send-btn {
+ flex-shrink: 0;
+ min-width: 48px;
+ min-height: 48px;
+ padding: 8px 16px;
+ font-size: 1em;
+ touch-action: manipulation;
+ margin-top: 2px;
+ }
+ #quick-actions { scrollbar-width: none; -ms-overflow-style: none; }
+ #quick-actions::-webkit-scrollbar { display: none; }
+ @media (max-width: 480px) {
+ #input-area { padding: 6px; }
+ #command-input { font-size: 0.9em; }
+ .prompt-label { font-size: 0.8em; max-width: 80px; overflow: hidden; text-overflow: ellipsis; }
+ }
+ #autocomplete-dropdown {
+ position: absolute;
+ bottom: 100%;
+ left: 0;
+ right: 0;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-top: none;
+ max-height: 200px;
+ overflow-y: auto;
+ display: none;
+ z-index: 1001;
+ }
+ @media (min-height: 700px) {
+ #autocomplete-dropdown {
+ bottom: auto;
+ top: 100%;
+ border-top: 1px solid var(--border-color);
+ border-bottom: none;
+ }
+ }
+ .autocomplete-item {
+ padding: 8px 12px;
+ cursor: pointer;
+ border-bottom: 1px solid #111;
+ }
+ .autocomplete-item:hover, .autocomplete-item.selected {
+ background: var(--bg-tertiary);
+ color: var(--success-color);
+ }
+ .autocomplete-item .cmd-name { color: var(--text-color); font-weight: bold; }
+ .autocomplete-item .cmd-desc { color: var(--text-muted); font-size: 0.85em; }
+ #quick-actions {
+ display: flex;
+ gap: 8px;
+ padding: 8px 10px;
+ overflow-x: auto;
+ border-top: 1px solid #111;
+ background: var(--bg-tertiary);
+ flex-shrink: 0;
+ }
+ .qa-btn {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ color: var(--text-color);
+ padding: 8px 12px;
+ border-radius: 4px;
+ font-family: inherit;
+ font-size: 0.9em;
+ cursor: pointer;
+ white-space: nowrap;
+ touch-action: manipulation;
+ }
+ .qa-btn:active { background: var(--border-color); }
+ @keyframes confetti-fall {
+ 0% { transform: translateY(-10vh) rotate(0deg); opacity: 1; }
+ 100% { transform: translateY(110vh) rotate(720deg); opacity: 0; }
+ }
+ .confetti-piece {
+ position: fixed; top: -5vh; z-index: 9999;
+ font-size: 24px; pointer-events: none; user-select: none;
+ animation: confetti-fall 3s ease-in forwards;
+ }
+ #status-bar {
+ border-top: 1px solid var(--border-color);
+ padding: 4px 10px;
+ font-size: 0.8em;
+ display: flex;
+ justify-content: space-between;
+ color: var(--text-muted);
+ flex-shrink: 0;
+ }
+ .modal-overlay {
+ position: fixed;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(0,0,0,0.85);
+ display: none;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+ }
+ .modal-content {
+ background: var(--bg-color);
+ border: 1px solid var(--border-color);
+ width: 90%;
+ max-width: 500px;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ border-radius: 4px;
+ }
+ .modal-header {
+ padding: 10px;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ font-weight: bold;
+ }
+ .modal-body { padding: 10px; overflow-y: auto; flex-grow: 1; }
+ .modal-footer {
+ padding: 10px;
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ }
+ .btn {
+ padding: 6px 12px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ color: var(--text-color);
+ cursor: pointer;
+ border-radius: 3px;
+ }
+ .btn:hover { background: var(--bg-tertiary); }
+ .btn-danger { border-color: var(--error-color); color: var(--error-color); }
+ .file-item, .module-item, .model-item {
+ padding: 8px;
+ border-bottom: 1px solid #111;
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+ .file-item:hover, .module-item:hover, .model-item:hover { background: var(--bg-tertiary); }
+ .file-item.dir { color: var(--info-color); }
+ .module-item.active { border-left: 3px solid var(--success-color); }
+ .module-item.disabled { opacity: 0.5; }
+ .model-item.connected { color: var(--success-color); }
+ .hidden { display: none !important; }
+ .text-center { text-align: center; }
+ .clickable-file, .clickable-dir {
+ cursor: pointer;
+ display: inline-block;
+ padding: 1px 3px;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ margin: 1px 0;
+ }
+ .clickable-file:hover { background: rgba(0,255,0,0.1); border-color: var(--border-color); }
+ .module-url-input {
+ width: 100%;
+ padding: 8px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-color);
+ font-family: var(--font-primary);
+ margin-bottom: 10px;
+ }
+ .ollama-status {
+ padding: 10px;
+ margin-bottom: 10px;
+ border-radius: 4px;
+ }
+ .ollama-status.connected { background: rgba(0, 255, 0, 0.1); border: 1px solid var(--success-color); }
+ .ollama-status.disconnected { background: rgba(255, 85, 85, 0.1); border: 1px solid var(--error-color); }
+ /* Toggle slider styles */
+ #ollama-optimization-slider::before, #ollama-validator-slider::before, #ollama-stream-slider::before {
+ content: "";
+ position: absolute;
+ height: 20px;
+ width: 20px;
+ left: 2px;
+ bottom: 2px;
+ background: var(--text-color);
+ border-radius: 50%;
+ transition: 0.3s;
+ }
+ #ollama-optimization-toggle:checked + #ollama-optimization-slider::before,
+ #ollama-validator-toggle:checked + #ollama-validator-slider::before,
+ #ollama-stream-toggle:checked + #ollama-stream-slider::before {
+ transform: translateX(22px);
+ }
+ #ollama-optimization-toggle:checked + #ollama-optimization-slider,
+ #ollama-validator-toggle:checked + #ollama-validator-slider,
+ #ollama-stream-toggle:checked + #ollama-stream-slider {
+ border-color: var(--success-color);
+ }
+ /* ── Card Layout Styles ── */
+ .msg-card {
+ margin-bottom: 8px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ overflow: hidden;
+ background: var(--bg-secondary);
+ }
+ .msg-card.user { border-left: 3px solid var(--success-color); }
+ .msg-card.ai { border-left: 3px solid var(--ai-text-color); background: var(--ai-bg-color); }
+ .msg-card.system { border-left: 3px solid var(--text-muted); }
+ .msg-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 6px 10px;
+ border-bottom: 1px solid var(--border-color);
+ background: var(--bg-tertiary);
+ }
+ .msg-role { font-weight: bold; font-size: 0.85em; white-space: nowrap; }
+ .msg-buttons { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
+ .msg-card-body {
+ padding: 10px;
+ white-space: pre-wrap;
+ word-break: break-word;
+ line-height: 1.4;
+ font-size: 0.9em;
+ }
+ .card-btn {
+ display: inline-flex;
+ padding: 4px 12px;
+ font-size: 0.75em;
+ font-family: inherit;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-muted);
+ border-radius: 4px;
+ cursor: pointer;
+ touch-action: manipulation;
+ transition: background 0.15s;
+ white-space: nowrap;
+ line-height: 1.4;
+ }
+ .card-btn:hover, .card-btn:active { background: var(--accent-color); color: var(--bg-color); }
+ .msg-card.ai .ai-render-container {
+ margin: 0;
+ border: 1px solid var(--border-color);
+ }
+ .msg-card.ai .ai-content {
+ display: inline;
+ }
+ .msg-card.system .msg-card-body {
+ color: inherit;
+ }
+ /* ── End Card Layout ── */
+ .ai-render-container {
+ margin: 10px 0;
+ border-radius: 6px;
+ overflow: hidden;
+ border: 1px solid var(--border-color);
+ background: #000;
+ }
+ .ai-render-iframe {
+ width: 100%;
+ border: none;
+ display: block;
+ background: #000;
+ min-height: 200px;
+ }
+ .module-badge {
+ display: inline-block;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-size: 0.75em;
+ margin-left: 5px;
+ }
+ .module-badge.builtin { background: var(--bg-tertiary); border: 1px solid var(--border-color); }
+ .fetch-content {
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ padding: 10px;
+ margin: 5px 0;
+ border-radius: 4px;
+ max-height: 300px;
+ overflow-y: auto;
+ font-size: 0.85em;
+ }
+ .search-result {
+ margin-bottom: 10px;
+ padding: 8px;
+ background: var(--bg-tertiary);
+ border-radius: 4px;
+ border-left: 3px solid var(--info-color);
+ }
+ .search-result a {
+ color: var(--info-color);
+ text-decoration: none;
+ }
+ .search-result a:hover {
+ text-decoration: underline;
+ }
+ .search-result-image, .search-result-video {
+ margin-bottom: 10px;
+ padding: 8px;
+ background: var(--bg-tertiary);
+ border-radius: 4px;
+ }
+ .search-result-image img, .search-result-video img {
+ max-width: 200px;
+ max-height: 150px;
+ border-radius: 4px;
+ object-fit: cover;
+ }
+
+ /* ── Splash Screen ── */
+ #splash-overlay {
+ position: fixed;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background: var(--bg-color);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ z-index: 2000;
+ transition: opacity 0.3s ease;
+ }
+ #splash-overlay.hidden {
+ opacity: 0;
+ pointer-events: none;
+ }
+ #splash-title {
+ font-size: 1.8em;
+ font-weight: bold;
+ margin-bottom: 4px;
+ }
+ #splash-version {
+ font-size: 0.8em;
+ color: var(--text-muted);
+ margin-bottom: 24px;
+ }
+ #splash-progress-track {
+ width: 260px;
+ max-width: 70vw;
+ height: 8px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ overflow: hidden;
+ margin-bottom: 12px;
+ }
+ #splash-progress-fill {
+ height: 100%;
+ width: 0%;
+ background: var(--accent-color);
+ transition: width 0.15s ease;
+ border-radius: 3px;
+ }
+ #splash-phase {
+ font-size: 0.75em;
+ color: var(--text-muted);
+ margin-bottom: 4px;
+ }
+ #splash-filename {
+ font-size: 0.85em;
+ color: var(--info-color);
+ margin-bottom: 16px;
+ }
+ #splash-cursor {
+ display: inline-block;
+ width: 8px;
+ height: 14px;
+ background: var(--text-color);
+ animation: blink 1s step-end infinite;
+ }
+ @keyframes blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+ }
+ #splash-gear {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ font-size: 1.4em;
+ cursor: pointer;
+ opacity: 0.5;
+ transition: opacity 0.2s;
+ touch-action: manipulation;
+ padding: 8px;
+ }
+ #splash-gear:hover { opacity: 1; }
+ #splash-error {
+ color: var(--error-color);
+ font-size: 0.85em;
+ margin-top: 12px;
+ text-align: center;
+ display: none;
+ }
+ #splash-error.show { display: block; }
+
+ /* ── Startup Modules Modal ── */
+ .mod-card {
+ display: flex;
+ align-items: center;
+ padding: 10px 8px;
+ border-bottom: 1px solid #111;
+ min-height: 44px;
+ touch-action: manipulation;
+ }
+ .mod-card:last-child { border-bottom: none; }
+ .mod-card.disabled { opacity: 0.4; pointer-events: none; }
+ .mod-card label {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-grow: 1;
+ cursor: pointer;
+ }
+ .mod-card input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ accent-color: var(--accent-color);
+ flex-shrink: 0;
+ }
+ .mod-card .mod-name {
+ font-size: 0.9em;
+ flex-shrink: 0;
+ }
+ .mod-card .mod-badge {
+ font-size: 0.7em;
+ padding: 2px 6px;
+ border-radius: 3px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-muted);
+ margin-left: auto;
+ }
+ .mod-card .mod-badge.foundation {
+ background: #001a00;
+ border-color: var(--accent-color);
+ color: var(--accent-color);
+ }
+ .mod-card .mod-badge.ext {
+ background: #1a001a;
+ border-color: #550055;
+ color: #aa55ff;
+ }
+ .mod-section-header {
+ padding: 8px 8px 4px;
+ font-size: 0.75em;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ border-bottom: 1px solid var(--border-color);
+ background: var(--bg-secondary);
+ }
+ @media (max-width: 400px) {
+ .mod-card { padding: 10px 6px; }
+ .mod-card .mod-name { font-size: 0.82em; }
+ }
+ /* ── Command Cards Panel ── */
+ #cmd-cards {
+ position: fixed; right: 0; top: 48px; bottom: 0; width: 280px;
+ background: var(--bg-secondary); border-left: 1px solid var(--border-color);
+ z-index: 999; display: none; flex-direction: column;
+ backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
+ overflow-y: auto;
+ }
+ #cmd-cards.visible { display: flex; }
+ #cmd-cards-header {
+ display: flex; justify-content: space-between; align-items: center;
+ padding: 12px 14px; border-bottom: 1px solid var(--border-color);
+ font-weight: bold; font-size: 0.9em;
+ position: sticky; top: 0; background: var(--bg-secondary);
+ }
+ .cmd-card-section { padding: 10px 14px; }
+ .cmd-card-section + .cmd-card-section { border-top: 1px solid var(--border-color); }
+ .cmd-card-section-title { font-size: 0.75em; color: var(--text-muted); margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; }
+ .cmd-card {
+ display: flex; align-items: center; gap: 10px;
+ padding: 10px 12px; margin-bottom: 4px;
+ background: var(--bg-tertiary); border: 1px solid var(--border-color);
+ border-radius: 6px; cursor: pointer; transition: border-color .15s;
+ min-height: 44px; touch-action: manipulation;
+ }
+ .cmd-card:hover { border-color: var(--accent-color); }
+ .cmd-card .cmd { font-family: var(--font-primary); font-weight: bold; font-size: 0.85em; color: var(--accent-color); flex-shrink: 0; min-width: 60px; }
+ .cmd-card .desc { font-size: 0.78em; color: var(--text-muted); }
+ #cmd-cards-toggle { cursor: pointer; font-size: 1.1em; line-height: 1; }
+
+ /* ── Floating UI Module ── */
+ #floating-container {
+ position: fixed;
+ bottom: calc(56px + 36px + 12px);
+ right: 12px;
+ z-index: 500;
+ display: flex;
+ flex-direction: column-reverse;
+ align-items: flex-end;
+ gap: 6px;
+ pointer-events: none;
+ max-width: 280px;
+ }
+ .floating-btn {
+ pointer-events: auto;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-color);
+ padding: 8px 14px;
+ border-radius: 8px;
+ font-family: inherit;
+ font-size: 0.9em;
+ cursor: grab;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ white-space: nowrap;
+ touch-action: manipulation;
+ min-height: 44px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.4);
+ transition: border-color 0.15s, background 0.15s;
+ user-select: none;
+ }
+ .floating-btn:hover,
+ .floating-btn:active {
+ border-color: var(--accent-color);
+ background: #0a1a0a;
+ }
+ .floating-btn:active {
+ cursor: grabbing;
+ }
+ .floating-menu {
+ pointer-events: auto;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ overflow: hidden;
+ min-width: 140px;
+ box-shadow: 0 2px 12px rgba(0,0,0,0.5);
+ }
+ .floating-menu-header {
+ padding: 8px 14px;
+ cursor: grab;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 0.9em;
+ min-height: 44px;
+ touch-action: none;
+ user-select: none;
+ border-bottom: 1px solid var(--border-color);
+ }
+ .floating-menu-header:active {
+ cursor: grabbing;
+ }
+ .floating-menu-header:hover {
+ background: #0a1a0a;
+ }
+ .floating-menu-header .arrow {
+ transition: transform 0.2s;
+ font-size: 0.7em;
+ }
+ .floating-menu-header .arrow.collapsed {
+ transform: rotate(-90deg);
+ }
+ .floating-menu-body {
+ overflow: hidden;
+ transition: max-height 0.2s ease-out;
+ max-height: 0;
+ }
+ .floating-menu-body.expanded {
+ max-height: 300px;
+ }
+ .floating-menu-btn {
+ display: block;
+ width: 100%;
+ text-align: left;
+ padding: 8px 14px 8px 24px;
+ background: transparent;
+ border: none;
+ color: var(--text-muted);
+ font-family: inherit;
+ font-size: 0.85em;
+ cursor: pointer;
+ touch-action: manipulation;
+ min-height: 40px;
+ }
+ .floating-menu-btn:hover,
+ .floating-menu-btn:active {
+ background: #0a1a0a;
+ color: var(--text-color);
+ }
+ .floating-panel {
+ pointer-events: auto;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ font-size: 0.8em;
+ min-width: 120px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.4);
+ overflow: hidden;
+ }
+ .floating-panel-title {
+ padding: 6px 10px;
+ font-weight: bold;
+ color: var(--text-muted);
+ font-size: 0.9em;
+ cursor: grab;
+ touch-action: none;
+ user-select: none;
+ border-bottom: 1px solid var(--border-color);
+ background: rgba(0,0,0,0.15);
+ }
+ .floating-panel-title:active {
+ cursor: grabbing;
+ }
+ .floating-panel-content {
+ padding: 6px 10px;
+ color: var(--text-color);
+ line-height: 1.4;
+ }
+ @keyframes float-in {
+ from { opacity: 0; transform: translateY(8px) scale(0.95); }
+ to { opacity: 1; transform: translateY(0) scale(1); }
+ }
+ .floating-btn,
+ .floating-menu,
+ .floating-panel {
+ animation: float-in 0.15s ease-out;
+ }
+ </style>
+ <!-- Early persistence restore — apply saved theme + font-size BEFORE bundle loads to prevent FOUC -->
+ <script>
+ (function() {
+ try {
+ // Restore font size
+ var fs = localStorage.getItem('webx11_font_size');
+ if (fs) { document.documentElement.style.fontSize = fs + 'px'; }
+ // Restore theme
+ var theme = localStorage.getItem('webx11_active_theme');
+ if (theme) {
+ var themesRaw = localStorage.getItem('webx11_themes');
+ var 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) { try { Object.assign(defaults, JSON.parse(themesRaw)); } catch(e){} }
+ var t = defaults[theme];
+ if (t) { for (var k in t) { document.documentElement.style.setProperty(k, t[k]); } }
+ }
+ } catch(e) {}
+ })();
+ </script>
+</head>
+<body>
+ <header>
+ <div>
+ <span class="header-title">WebX11</span>
+ <span class="header-version"></span>
+ </div>
+ <div id="header-status" class="status-pill">Mem: --</div>
+ <span id="cmd-cards-toggle" onclick="if(webx11)webx11.toggleCmdCards()" title="Quick Commands (Ctrl+K)">📋</span>
+ </header>
+
+ <!-- Command Cards Panel -->
+ <div id="cmd-cards">
+ <div id="cmd-cards-header">
+ <span>📋 Quick Commands</span>
+ <span onclick="if(webx11)webx11.toggleCmdCards()" style="cursor:pointer;font-size:1.2em;">✕</span>
+ </div>
+ <div class="cmd-card-section">
+ <div class="cmd-card-section-title">🟢 Beginners</div>
+ <div class="cmd-card" onclick="webx11?.executeCommand('help')"><span class="cmd">help</span><span class="desc">Show all commands</span></div>
+ <div class="cmd-card" onclick="webx11?.executeCommand('ls')"><span class="cmd">ls</span><span class="desc">List files</span></div>
+ <div class="cmd-card" onclick="webx11?.executeCommand('clear')"><span class="cmd">clear</span><span class="desc">Clear terminal</span></div>
+ <div class="cmd-card" onclick="webx11?.executeCommand('status')"><span class="cmd">status</span><span class="desc">System health</span></div>
+ </div>
+ <div class="cmd-card-section">
+ <div class="cmd-card-section-title">🤖 AI</div>
+ <div class="cmd-card" onclick="webx11?.executeCommand('ai \"what can you do?\"')"><span class="cmd">ai "..."</span><span class="desc">Ask AI assistant</span></div>
+ <div class="cmd-card" onclick="webx11?.executeCommand('ai-model')"><span class="cmd">ai-model</span><span class="desc">Set AI model</span></div>
+ </div>
+ <div class="cmd-card-section">
+ <div class="cmd-card-section-title">🔍 Explore</div>
+ <div class="cmd-card" onclick="webx11?.executeCommand('modules')"><span class="cmd">modules</span><span class="desc">List loaded modules</span></div>
+ <div class="cmd-card" onclick="webx11?.executeCommand('tutorial')"><span class="cmd">tutorial</span><span class="desc">Interactive tour</span></div>
+ </div>
+ <div class="cmd-card-section">
+ <div class="cmd-card-section-title">🚀 Advanced</div>
+ <div class="cmd-card" onclick="webx11?.executeCommand('cron-every 60 echo hello')"><span class="cmd">cron</span><span class="desc">Schedule tasks</span></div>
+ <div class="cmd-card" onclick="webx11?.executeCommand('webdav-status')"><span class="cmd">webdav</span><span class="desc">Cloud sync</span></div>
+ <div class="cmd-card" onclick="webx11?.executeCommand('help.html')"><span class="cmd">help</span><span class="desc">Full reference</span></div>
+ </div>
+ </div>
+
+ <!-- Terminal Container -->
+ <div id="terminal-container">
+ <div class="output-line info" id="terminal-welcome">WebX11 Module System - Unix-like Terminal</div>
+ <div class="output-line info">Commands: ls, cd, pwd, cat, echo, rm, mkdir, mv, cp, touch, tree, find, grep</div>
+ <div class="output-line info">Type 'help' for all commands</div>
+ <div class="output-line"></div>
+ <div class="output-line info loading" id="loading-indicator">Loading modules...</div>
+ </div>
+
+ <!-- Floating UI Container — populated by FloatingModule -->
+ <div id="floating-container"></div>
+
+ <!-- AI Confirm Area — fixed outside scrollable output, always visible -->
+ <div id="ai-confirm-area" class="hidden"></div>
+
+ <div id="quick-actions">
+ <button class="qa-btn" onclick="webx11.executeCommand('ls')">📁 Files</button>
+ <button class="qa-btn" onclick="webx11.executeCommand('help')">📖 Help</button>
+ <button class="qa-btn" onclick="webx11.executeCommand('modules')">🧩 Modules</button>
+ <button class="qa-btn" onclick="webx11.openOllamaManager()">🤖 Ollama</button>
+ <button class="qa-btn" onclick="webx11.openSkillsManager()">📚 Skills</button>
+ <button class="qa-btn" onclick="webx11.openConfigManager()">⚙ Config</button>
+ <button class="qa-btn" onclick="webx11.executeCommand('clear')">🧹 Clear</button>
+ <button class="qa-btn" id="voice-toggle-btn" onclick="webx11.toggleMicVisibility()">🎤 Mic</button>
+ <button class="qa-btn" onclick="webx11.toggleDebugPanel()">🐞 Debug</button>
+ <button class="qa-btn" id="sound-toggle-btn">🔊 Sound</button>
+ </div>
+
+ <div id="input-area">
+ <span class="prompt-label">$</span>
+ <textarea id="command-input" rows="1" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" placeholder="Type a command..." style="resize:none; overflow-y:auto; max-height:120px;"></textarea>
+ <button id="send-btn" onclick="var i=document.getElementById('command-input');webx11.executeCommand(i.value);i.value='';i.style.height='auto'" style="background:var(--bg-tertiary); border:1px solid var(--border-color); color:var(--success-color); padding:6px 12px; border-radius:4px; cursor:pointer; font-family:inherit; font-size:0.9em; margin-left:6px; white-space:nowrap; flex-shrink:0;">⏎ Send</button>
+ <div id="autocomplete-dropdown"></div>
+ </div>
+
+ <div id="status-bar">
+ <span id="status-text">Ready</span>
+ <span id="network-status">📶 Online</span>
+ <span id="stream-status" style="font-size:0.75em; margin-left:4px; color:var(--success-color);">▶ Stream</span>
+ <button id="ai-cancel-btn" style="display:none; background:var(--error-color); color:#fff; border:none; border-radius:4px; padding:2px 10px; cursor:pointer; font-family:inherit; font-size:0.8em; touch-action:manipulation; min-height:28px;" onclick="webx11.executeCommand('ai-stop')">⏹ Stop</button>
+ </div>
+
+ <!-- File Browser Modal -->
+ <div id="file-browser" class="modal-overlay">
+ <div class="modal-content">
+ <div class="modal-header">
+ <span>File Browser</span>
+ <span style="cursor:pointer" onclick="webx11.closeFileBrowser()">✕</span>
+ </div>
+ <div class="modal-body" id="fb-content"></div>
+ <div class="modal-footer">
+ <button class="btn" onclick="document.getElementById('file-upload-input').click()">⬆️ Upload</button>
+ <button class="btn" onclick="webx11.closeFileBrowser()">Close</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- Module Manager Modal -->
+ <div id="module-manager" class="modal-overlay">
+ <div class="modal-content" style="max-width: 600px;">
+ <div class="modal-header">
+ <span>Module Manager</span>
+ <span style="cursor:pointer" onclick="webx11.closeModuleManager()">✕</span>
+ </div>
+ <div class="modal-body">
+ <div style="margin-bottom: 15px;">
+ <input type="text" id="module-url-input" class="module-url-input" placeholder="Enter module URL (https://example.com/module.js)">
+ <button class="btn" onclick="webx11.loadModuleFromURL()" style="width: 100%;">Load Module from URL</button>
+ </div>
+ <h4 style="color: var(--info-color); margin-bottom: 10px;">Loaded Modules</h4>
+ <div id="module-list"></div>
+ </div>
+ <div class="modal-footer">
+ <button class="btn btn-danger" onclick="webx11.clearAllModules()">Clear All</button>
+ <button class="btn" onclick="webx11.closeModuleManager()">Close</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- Ollama Manager Modal -->
+ <div id="ollama-manager" class="modal-overlay">
+ <div class="modal-content" style="max-width: 600px;">
+ <div class="modal-header">
+ <span>Ollama AI Manager</span>
+ <span style="cursor:pointer" onclick="webx11.closeOllamaManager()">✕</span>
+ </div>
+ <div class="modal-body">
+ <div id="ollama-status" class="ollama-status disconnected">
+ <strong>Status:</strong> <span id="ollama-status-text">Disconnected</span>
+ </div>
+ <div style="margin-bottom: 15px;">
+ <label style="display: block; margin-bottom: 5px; color: var(--text-muted);">Ollama Endpoint:</label>
+ <input type="text" id="ollama-endpoint" class="module-url-input" value="http://localhost:11434" placeholder="http://localhost:11434">
+ <button class="btn" onclick="webx11.connectOllama()" style="width: 100%;">Connect</button>
+ </div>
+ <h4 style="color: var(--info-color); margin-bottom: 10px;">Available Models</h4>
+ <input type="text" id="ollama-search" class="module-url-input" placeholder="Search models..." oninput="webx11.renderOllamaModels()" style="margin-bottom: 8px;">
+ <div id="ollama-model-count" style="font-size: 0.8em; color: var(--text-muted); margin-bottom: 6px;"></div>
+ <div id="ollama-models" style="max-height: 300px; overflow-y: auto;"></div>
+ <div style="margin-top: 15px;">
+ <label style="display: block; margin-bottom: 5px; color: var(--text-muted);">Current Model:</label>
+ <select id="ollama-current-model" class="module-url-input">
+ <option value="">Select a model</option>
+ </select>
+ <button class="btn" onclick="webx11.setOllamaModel()" style="width: 100%;">Set Model</button>
+ </div>
+ <hr style="border-color:var(--border-color); margin: 15px 0;">
+ <h4 style="color: var(--accent-color); margin-bottom: 10px;">⚡ AI Agent Optimization</h4>
+ <div style="display:flex; align-items:center; gap:10px; margin-bottom: 12px;">
+ <span style="color:var(--text-muted); font-size:0.85em;">Agent Loop Optimizations:</span>
+ <label style="position:relative; display:inline-block; width:48px; height:26px;">
+ <input type="checkbox" id="ollama-optimization-toggle" onchange="webx11.toggleOptimization()" style="opacity:0; width:0; height:0;">
+ <span id="ollama-optimization-slider" style="position:absolute; cursor:pointer; inset:0; background:var(--bg-tertiary); border:1px solid var(--border-color); border-radius:26px; transition:0.3s;"></span>
+ </label>
+ <span id="ollama-optimization-label" style="font-size:0.85em; color:var(--success-color);">Enabled</span>
+ </div>
+ <div style="font-size:0.8em; color:var(--text-muted); margin-bottom: 10px;">
+ ⚡ Tool def caching, trivial query skip, parallel safe tools, debounced reflection<br>
+ <span style="color:var(--warning-color);">Disable for debugging or if model behaves unexpectedly. Reload after toggle.</span>
+ </div>
+ <hr style="border-color:var(--border-color); margin: 15px 0;">
+ <h4 style="color: var(--accent-color); margin-bottom: 10px;">⚡ Response Streaming</h4>
+ <div style="display:flex; align-items:center; gap:10px; margin-bottom: 6px;">
+ <span style="color:var(--text-muted); font-size:0.85em;">Stream Tokens:</span>
+ <label style="position:relative; display:inline-block; width:48px; height:26px;">
+ <input type="checkbox" id="ollama-stream-toggle" onchange="webx11.toggleStream()" style="opacity:0; width:0; height:0;">
+ <span id="ollama-stream-slider" style="position:absolute; cursor:pointer; inset:0; background:var(--bg-tertiary); border:1px solid var(--border-color); border-radius:26px; transition:0.3s;"></span>
+ </label>
+ <span id="ollama-stream-label" style="font-size:0.85em; color:var(--success-color);">Enabled</span>
+ </div>
+ <div style="font-size:0.8em; color:var(--text-muted); margin-bottom: 10px;">
+ Show AI responses token-by-token. Disable for instant full responses and reduced crash risk.<br>
+ <span style="color:var(--warning-color);">Disable on slow connections or when browser becomes unresponsive during long outputs.</span>
+ </div>
+ <hr style="border-color:var(--border-color); margin: 15px 0;">
+ <h4 style="color: var(--info-color); margin-bottom: 10px;">🔍 Validator Agent</h4>
+ <div style="font-size:0.85em; color:var(--text-muted); margin-bottom: 10px;">
+ A separate lightweight AI that validates the main agent's tool results. Has its own
+ endpoint and model — independent from your active AI model.<br>
+ <span style="color:var(--success-color);">⚡ Best validator model: <strong>qwen2.5:0.5b</strong> (1.94s, 352MB)</span>
+ </div>
+ <div style="display:flex; align-items:center; gap:10px; margin-bottom: 10px;">
+ <span style="color:var(--text-muted); font-size:0.85em;">Enable Validator:</span>
+ <label style="position:relative; display:inline-block; width:48px; height:26px;">
+ <input type="checkbox" id="ollama-validator-toggle" onchange="webx11.toggleValidator()" style="opacity:0; width:0; height:0;">
+ <span id="ollama-validator-slider" style="position:absolute; cursor:pointer; inset:0; background:var(--bg-tertiary); border:1px solid var(--border-color); border-radius:26px; transition:0.3s;"></span>
+ </label>
+ <span id="ollama-validator-label" style="font-size:0.85em; color:var(--text-muted);">Disabled</span>
+ </div>
+ <div style="margin-bottom: 8px;">
+ <label style="display: block; margin-bottom: 3px; color: var(--text-muted); font-size:0.85em;">Validator Endpoint:</label>
+ <input type="text" id="validator-endpoint-input" class="module-url-input" value="http://localhost:11434" placeholder="http://localhost:11434" style="margin-bottom:6px;">
+ <button class="btn" onclick="webx11.connectValidatorEndpoint()" style="width:100%; border-color:var(--info-color); color:var(--info-color);">🔌 Connect & Load Models</button>
+ <div id="validator-connection-status" style="font-size:0.8em; margin-top:4px;"></div>
+ </div>
+ <div style="margin-bottom: 10px;">
+ <label style="display: block; margin-bottom: 3px; color: var(--text-muted); font-size:0.85em;">Validator Model:</label>
+ <select id="validator-model-select" class="module-url-input" style="margin-bottom:6px;" disabled>
+ <option value="">Connect to load models</option>
+ </select>
+ <button class="btn" onclick="webx11.saveValidatorConfig()" style="width:100%; border-color:var(--info-color); color:var(--info-color);">Save Validator Config</button>
+ </div>
+ <div style="font-size:0.8em; color:var(--warning-color); border:1px solid var(--border-color); border-radius:4px; padding:10px; margin-top:10px;">
+ <strong>⚠️ Connection Notes</strong><br>
+ • Cloud models (deepseek-v4*, gemma4:*b-cloud) via Ollama proxy often <strong>ignore tool calls</strong>
+ and answer from training data instead. Use local models for tool calling.<br>
+ • To connect to local Ollama from a web page: set <code>OLLAMA_ORIGINS</code> on the server:
+ <code>export OLLAMA_ORIGINS="https://webx11.duckdns.org"</code> then restart Ollama.<br>
+ • Test local connection: open a terminal and run <code>curl http://localhost:11434/api/tags</code><br>
+ • If you see <strong>403</strong> errors in console: the <code>OLLAMA_ORIGINS</code> env var isn't matching your page URL.
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button class="btn" onclick="webx11.closeOllamaManager()">Close</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- Splash Screen -->
+ <div id="splash-overlay">
+ <span id="splash-gear" onclick="openStartupModulesSplash(event)" title="Select startup modules">⚙</span>
+ <div id="splash-title">⚡ WebX11</div>
+ <div id="splash-version"></div>
+ <div id="splash-progress-track"><div id="splash-progress-fill"></div></div>
+ <div id="splash-phase">Initializing...</div>
+ <div id="splash-filename"></div>
+ <div><span id="splash-cursor"></span></div>
+ <div id="splash-error"></div>
+ </div>
+
+ <!-- Skills Manager Modal -->
+ <div id="skills-manager" class="modal-overlay">
+ <div class="modal-content" style="max-width: 750px; max-height: 90vh;">
+ <div class="modal-header">
+ <span>📚 Skills Manager</span>
+ <span style="cursor:pointer" onclick="webx11.closeSkillsManager()">✕</span>
+ </div>
+ <div class="modal-body" style="display:flex; flex-direction:column; gap:8px;">
+ <!-- Stats header -->
+ <div id="skills-stats" style="display:flex; gap:12px; flex-wrap:wrap; font-size:0.85em; color:var(--text-muted); padding-bottom:6px; border-bottom:1px solid var(--border-color);"></div>
+ <!-- Search + action bar -->
+ <div style="display:flex; gap:6px; align-items:center; flex-shrink:0;">
+ <input type="text" id="skills-search-input" class="module-url-input" placeholder="🔍 Search skills..." style="flex:1;" oninput="webx11.refreshSkillsList()">
+ <button class="btn" onclick="webx11.exportAllSkillsUI()" style="padding:4px 10px; font-size:0.8em">📤 Export All</button>
+ <button class="btn" onclick="document.getElementById('skills-import-input').click()" style="padding:4px 10px; font-size:0.8em">📥 Import</button>
+ <input type="file" id="skills-import-input" accept=".json" style="display:none" onchange="webx11.importSkillsUI(event)">
+ <button class="btn" onclick="webx11.syncSkillsToWebdavUI()" style="padding:4px 10px; font-size:0.8em">☁️ Sync</button>
+ </div>
+ <!-- Skills list with category groups -->
+ <div id="skills-list" style="max-height:300px;overflow-y:auto; flex-shrink:0;"></div>
+ <!-- Preview (collapsible) -->
+ <div id="skill-preview" style="display:none; flex-shrink:0; background:var(--bg-tertiary); border:1px solid var(--border-color); border-radius:4px; padding:10px; overflow-y:auto; white-space:pre-wrap; font-family:var(--font-primary); font-size:0.85em; color:var(--text-color);">
+ <div style="display:flex; justify-content:space-between; margin-bottom:6px;">
+ <span id="skill-preview-title" style="color:var(--success-color); font-weight:bold;"></span>
+ <div style="display:flex; gap:6px;">
+ <button class="btn" id="skill-preview-expand-btn" onclick="webx11.toggleSkillPreviewSize()" style="padding:2px 6px; font-size:0.75em">⛶ Expand</button>
+ <span style="cursor:pointer; color:var(--text-muted); font-size:1.2em;" onclick="webx11.closeSkillPreview()">✕</span>
+ </div>
+ </div>
+ <div id="skill-preview-content"></div>
+ </div>
+ <!-- Create form -->
+ <div style="flex-shrink:0; border-top:1px solid var(--border-color); padding-top:8px;">
+ <h4 style="color: var(--info-color); margin-bottom: 6px;">Create New Skill</h4>
+ <div style="display:flex; gap:6px; flex-wrap:wrap;">
+ <input type="text" id="skill-name-input" class="module-url-input" placeholder="Name (kebab-case)" style="flex:2; min-width:140px;">
+ <input type="text" id="skill-desc-input" class="module-url-input" placeholder="Description" style="flex:3; min-width:180px;">
+ <select id="skill-category-input" class="module-url-input" style="flex:1; min-width:100px;">
+ <option value="general">general</option>
+ <option value="system">system</option>
+ <option value="research">research</option>
+ <option value="dev">dev</option>
+ <option value="productivity">productivity</option>
+ <option value="ui">ui</option>
+ <option value="security">security</option>
+ </select>
+ </div>
+ <div style="display:flex; gap:6px; margin-top:6px;">
+ <input type="text" id="skill-tags-input" class="module-url-input" placeholder="Tags (comma-separated)" style="flex:1;">
+ <button class="btn" onclick="webx11.createSkillFromUI()" style="border-color:var(--success-color);color:var(--success-color); flex-shrink:0;">Create</button>
+ </div>
+ <textarea id="skill-content-input" class="module-url-input" style="margin-top:6px; height:80px; resize:vertical; font-family:var(--font-primary)" placeholder="Markdown content (optional — will use template)"></textarea>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button class="btn" onclick="webx11.refreshSkillsList()">🔄 Refresh</button>
+ <button class="btn" onclick="webx11.closeSkillsManager()">Close</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- Preview Modal -->
+ <div id="preview-modal" class="modal-overlay">
+ <div class="modal-content" style="max-width: 95%; max-height: 90vh; width: 95%;">
+ <div class="modal-header">
+ <span id="preview-title">📄 Preview</span>
+ <span style="display:flex;gap:10px;align-items:center">
+ <button class="btn btn-sm" onclick="var f=document.getElementById('preview-iframe');if(f&&f.srcdoc){var w=window.open('','_blank');if(w){w.document.open();w.document.write(f.srcdoc);w.document.close();}}" title="Open in new tab">⛶</button>
+ <span style="cursor:pointer" onclick="webx11.closePreview()">✕</span>
+ </span>
+ </div>
+ <div class="modal-body" style="padding:0; overflow:hidden; height:75vh;">
+ <iframe id="preview-iframe" style="width:100%; height:100%; border:none; background:var(--bg-color);" sandbox="allow-same-origin allow-scripts"></iframe>
+ </div>
+ <div class="modal-footer">
+ <button class="btn" onclick="webx11.closePreview()">Close</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- Config Manager Modal -->
+ <div id="config-manager" class="modal-overlay">
+ <div class="modal-content" style="max-width: 550px; max-height: 80vh;">
+ <div class="modal-header">
+ <span>⚙ Configuration</span>
+ <span style="cursor:pointer" onclick="webx11.closeConfigManager()">✕</span>
+ </div>
+ <div class="modal-body" id="config-manager-body">
+ <div id="config-list" style="margin-bottom:16px;"></div>
+ <div style="border-top:1px solid var(--border-color); padding-top:12px;">
+ <h4 style="color:var(--info-color); margin-bottom:8px;">Add / Update Config</h4>
+ <div style="display:flex; gap:6px; flex-wrap:wrap;">
+ <input type="text" id="config-key-input" class="module-url-input" placeholder="Key" style="flex:1; min-width:100px;">
+ <input type="text" id="config-value-input" class="module-url-input" placeholder="Value" style="flex:3; min-width:150px;">
+ <button class="btn" onclick="webx11.addConfigFromUI()" style="border-color:var(--success-color);color:var(--success-color); padding:6px 14px;">Add</button>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button class="btn" onclick="webx11.exportConfigFromUI()">📤 Export</button>
+ <button class="btn btn-danger" onclick="webx11.resetConfigFromUI()">🗑 Reset All</button>
+ <button class="btn" onclick="webx11.closeConfigManager()">Close</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- Welcome Tour Modal -->
+ <div id="welcome-modal" class="modal-overlay">
+ <div class="modal-content" style="max-width:500px;text-align:center;">
+ <div class="modal-header">
+ <span>🚀 Welcome to newOS4</span>
+ </div>
+ <div class="modal-body" style="padding:24px;">
+ <div style="font-size:3em;margin-bottom:12px;">🖥️</div>
+ <h2 style="margin:0 0 8px;color:var(--accent-color);">Let's Get Started</h2>
+ <p style="color:var(--text-muted);margin-bottom:16px;line-height:1.6;">
+ newOS4 is a terminal-centric browser OS with AI, filesystem, and modules.
+ Take the <strong>2-minute guided tour</strong> to learn the basics.
+ </p>
+ <div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;">
+ <button class="btn" onclick="var m=document.getElementById('welcome-modal');if(m)m.style.display='none';if(webx11&&webx11.tutorialModule)webx11.tutorialModule.startTutorial()"
+ style="border-color:var(--success-color);color:var(--success-color);padding:12px 28px;font-size:1em;">
+ 🎮 Start Tutorial
+ </button>
+ <button class="btn" onclick="var m=document.getElementById('welcome-modal');if(m)m.style.display='none';location.href='course.html'"
+ style="border-color:var(--info-color);color:var(--info-color);padding:12px 28px;font-size:1em;">
+ 📚 Full Course
+ </button>
+ <button class="btn" onclick="if(webx11)webx11.dismissWelcome()"
+ style="border-color:var(--text-muted);color:var(--text-muted);padding:12px 28px;font-size:1em;">
+ Skip — I'll explore
+ </button>
+ </div>
+ <label style="display:flex;align-items:center;justify-content:center;gap:6px;margin-top:14px;font-size:0.78em;color:var(--text-muted);cursor:pointer;">
+ <input type="checkbox" id="welcome-dont-show" onchange="if(this.checked)localStorage.setItem('webx11_welcome_dismissed','1');else localStorage.removeItem('webx11_welcome_dismissed')">
+ Don't show this again
+ </label>
+ <p style="font-size:0.78em;color:var(--text-muted);margin-top:16px;">
+ Your progress is saved. Type <code>tutorial</code> anytime to resume.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <!-- Startup Modules Modal -->
+ <div id="startup-modules" class="modal-overlay">
+ <div class="modal-content" style="max-width: 500px;">
+ <div class="modal-header">
+ <span>⚙ Startup Modules</span>
+ <span style="cursor:pointer" onclick="var w=window.webx11;if(w&&typeof w.closeStartupModules==='function')w.closeStartupModules();else document.getElementById('startup-modules').style.display='none'">✕</span>
+ </div>
+ <div class="modal-body" id="startup-modules-body"></div>
+ <div class="modal-footer">
+ <button class="btn btn-danger" onclick="webx11.closeStartupModules()">Cancel</button>
+ <button class="btn" onclick="webx11.saveStartupModules()" style="border-color:var(--info-color);color:var(--info-color)">Save & Reload</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- Code Editor Modal -->
+ <div id="code-editor" class="modal-overlay">
+ <div class="modal-content" style="max-width: 90vw; max-height: 90vh; width: 900px; height: 80vh;">
+ <div class="modal-header">
+ <span id="code-editor-title">📝 Edit Module</span>
+ <span style="cursor:pointer" onclick="if(webx11) webx11.closeCodeEditor()">✕</span>
+ </div>
+ <div class="modal-body" style="padding:0; display:flex; flex-direction:column; height:calc(100% - 100px);">
+ <div id="code-editor-info" style="padding:6px 10px; font-size:0.8em; color:var(--text-muted); border-bottom:1px solid var(--border-color); flex-shrink:0; display:flex; justify-content:space-between; align-items:center;">
+ <span id="code-editor-status">Ready</span>
+ <span><button class="btn" style="font-size:0.7em;padding:2px 8px" onclick="window._editorToggleFullscreen()">⤢ Fullscreen</button></span>
+ </div>
+ <div id="code-editor-wrapper" style="flex:1; display:flex; overflow:hidden; position:relative;">
+ <div id="code-editor-gutter" style="flex-shrink:0; width:40px; background:var(--bg-secondary); color:var(--text-muted); font-family:monospace; font-size:13px; line-height:20px; padding:10px 4px; text-align:right; overflow:hidden; user-select:none; border-right:1px solid var(--border-color);"></div>
+ <textarea id="code-editor-textarea" style="flex:1; border:none; background:var(--bg-tertiary); color:var(--text-color); font-family:monospace; font-size:13px; line-height:20px; padding:10px; resize:none; outline:none; tab-size:2; white-space:pre; overflow:auto; display:block;" spellcheck="false"></textarea>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button class="btn btn-danger" onclick="if(webx11) webx11.closeCodeEditor()">Cancel</button>
+ <button class="btn" onclick="(async()=>{if(webx11)await webx11.saveCodeEditor()})()" style="border-color:var(--info-color);color:var(--info-color)">💾 Save & Test</button>
+ </div>
+ </div>
+ </div>
+
+ <input type="file" id="file-upload-input" style="display:none">
+
+
+ <!-- Gear splash function — works before webx11 initializes -->
+ <script>
+ function openStartupModulesSplash(event) {
+ event.stopPropagation();
+ var w = window.webx11;
+ if (w && typeof w.openStartupModules === 'function') {
+ w.openStartupModules();
+ } else {
+ var overlay = document.getElementById('startup-modules');
+ if (!overlay) return;
+ // Bare-bones render for when webx11 isn't ready yet
+ var body = document.getElementById('startup-modules-body');
+ if (body && !body.children.length) {
+ var mods = [
+ ['Foundation (always loaded)', ['virtual-fs.js','module-manager.js','ollama-manager.js','terminal.js'], true],
+ ['Built-in modules', ['module-system.js','module-fs.js','module-ollama.js','module-my.js','module-searxng.js','module-servermon.js','module-ollamamodels.js','module-config.js','module-simplegui.js','module-io.js','module-dynamicprompts.js','module-aiassist.js','module-aiasm.js','module-smartctx.js','module-duckduckgo.js','module-fetcher.js','module-chatstudio.js'], false],
+ ['Extensions', ['calc.js','voice.js','cron.js','theme.js','wpm.js','mindmap.js','webdav.js','skill-pack-core.js','editor.js','diff.js','alias.js','finder.js','filemgr.js','cryptogzip.js','tutorial.js','shell.js','busybox.js','floating.js','console.js','kanban.js','course.js'], false]
+ ];
+ var isSel = function(name) { return true; }; // default: all selected
+ mods.forEach(function(section) {
+ var h = document.createElement('div');
+ h.className = 'mod-section-header';
+ h.textContent = section[0];
+ body.appendChild(h);
+ section[1].forEach(function(name) {
+ var card = document.createElement('div');
+ card.className = 'mod-card' + (section[2] ? ' disabled' : '');
+ var label = document.createElement('label');
+ var cb = document.createElement('input');
+ cb.type = 'checkbox';
+ cb.checked = section[2] || isSel(name);
+ cb.disabled = section[2];
+ cb.dataset.name = name;
+ var nameSpan = document.createElement('span');
+ nameSpan.className = 'mod-name';
+ nameSpan.textContent = name;
+ var badgeSpan = document.createElement('span');
+ badgeSpan.className = 'mod-badge' + (section[2] ? ' foundation' : '');
+ badgeSpan.textContent = section[2] ? 'foundation' : 'built-in';
+ label.appendChild(cb);
+ label.appendChild(nameSpan);
+ card.appendChild(label);
+ card.appendChild(badgeSpan);
+ body.appendChild(card);
+ });
+ });
+ }
+ overlay.style.display = 'flex';
+ }
+ }
+ </script>
+
+ <!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
+<script src="modules/core/core-bundle.js?v=v4.6.51"></script>
+
+ <!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
+<script src="modules/core/module-loader.js?v=v4.6.51"></script>
+
+ <!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.51"></script>
+
+<script>
+// ═══ ONE TRUE VERSION SOURCE ═══
+// Reads from module-loader.js src attribute (single source of truth)
+(function(){
+ var version = 'v4.0'; // fallback
+ var scripts = document.getElementsByTagName('script');
+ for (var i = 0; i < scripts.length; i++) {
+ var m = scripts[i].src && scripts[i].src.match(/module-loader\.js\?v=([v\d\.]+)/);
+ if (m) { version = m[1]; break; }
+ }
+ window.NEWOS4_VERSION = version;
+ var el = document.getElementById('splash-version');
+ if (el) el.textContent = version;
+ el = document.querySelector('.header-version');
+ if (el) el.textContent = version;
+ el = document.getElementById('terminal-welcome');
+ if (el) el.textContent = 'WebX11 Module System ' + version + ' - Unix-like Terminal';
+ document.title = 'WebX11 Module System ' + version;
+})();
+</script>
+
+<!-- Phase 6: Boot Watchdog — auto-revert if recent commit broke the system -->
+<script>
+(function() {
+ // Wait for modules to load, then check health
+ var watchdogTimer = null;
+ var watchdogStarted = false;
+
+ function checkBoot() {
+ if (watchdogStarted) return;
+ watchdogStarted = true;
+
+ // Check if core modules loaded
+ if (typeof webx11 === 'undefined' || !webx11.moduleManager) {
+ // Critical: webx11 object doesn't exist — system is broken
+ handleBootFailure('webx11 object not found');
+ return;
+ }
+
+ var mm = webx11.moduleManager;
+ var loadedModules = Object.keys(mm.modules).length;
+
+ // If less than 50% of expected modules loaded, boot failed
+ // Expected: ~19 modules at full load
+ if (loadedModules < 10) {
+ handleBootFailure('Only ' + loadedModules + ' modules loaded (expected ≥10)');
+ return;
+ }
+
+ // Boot OK — save current state as last-good
+ try {
+ var lastGood = {
+ version: window.NEWOS4_VERSION || 'unknown',
+ moduleCount: loadedModules,
+ timestamp: Date.now()
+ };
+ localStorage.setItem('newos4_last_good_boot', JSON.stringify(lastGood));
+ } catch (e) {}
+ }
+
+ function handleBootFailure(reason) {
+ console.error('[Boot Watchdog] Boot failure detected: ' + reason);
+
+ // Check if there was a recent commit (within last 30 min)
+ var commitLog = null;
+ try {
+ // Check localStorage for recent commit info
+ var lastCommit = localStorage.getItem('newos4_last_commit');
+ if (lastCommit) {
+ var commit = JSON.parse(lastCommit);
+ if (commit && commit.timestamp && (Date.now() - commit.timestamp < 1800000)) {
+ console.warn('[Boot Watchdog] Recent commit detected (version ' + commit.version + '), attempting auto-revert...');
+ attemptRevert(commit);
+ return;
+ }
+ }
+ } catch (e) {}
+
+ // No recent commit or no revert info — just alert
+ console.error('[Boot Watchdog] No recent commit to revert. System may need manual intervention.');
+ var banner = document.createElement('div');
+ banner.style.cssText = 'position:fixed;top:0;left:0;right:0;background:#ff4444;color:#fff;padding:10px;font-family:monospace;font-size:12px;z-index:99999;text-align:center;';
+ banner.textContent = '⚠️ Boot Watchdog: System failed to load (' + reason + '). Try hard refresh or clear localStorage.';
+ document.body.insertBefore(banner, document.body.firstChild);
+ }
+
+ function attemptRevert(commit) {
+ // Try to restore the previous version by setting the version back
+ if (commit.previousVersion) {
+ console.warn('[Boot Watchdog] Reverting to version ' + commit.previousVersion);
+ // Replace all ?v= in script tags with previous version
+ var scripts = document.getElementsByTagName('script');
+ var reverted = 0;
+ for (var i = 0; i < scripts.length; i++) {
+ var src = scripts[i].src || '';
+ if (src.indexOf('?v=') !== -1) {
+ scripts[i].src = src.replace(/\?v=[^&"']*/, '?v=' + commit.previousVersion);
+ reverted++;
+ }
+ }
+ console.log('[Boot Watchdog] Reverted ' + reverted + ' script tags to ' + commit.previousVersion);
+
+ // Show warning banner
+ var banner = document.createElement('div');
+ banner.style.cssText = 'position:fixed;top:0;left:0;right:0;background:#ffaa00;color:#000;padding:10px;font-family:monospace;font-size:12px;z-index:99999;text-align:center;';
+ banner.textContent = '⚠️ Boot Watchdog: Auto-reverted from ' + commit.version + ' to ' + commit.previousVersion + '. Refresh page to load reverted version.';
+ document.body.insertBefore(banner, document.body.firstChild);
+
+ // Force reload after 3 seconds
+ setTimeout(function() {
+ // Update URL with previous version to force cache bypass
+ window.location.href = window.location.pathname + '?v=' + commit.previousVersion + '&watchdog=1';
+ }, 3000);
+ } else {
+ console.error('[Boot Watchdog] No previous version stored. Manual intervention required.');
+ }
+ }
+
+ // Start watchdog after a delay to allow modules to load
+ watchdogTimer = setTimeout(checkBoot, 8000);
+})();
+</script>
+
+</body>
+</html>
diff --git a/.bkp-fix-20261023_143429/module-debug-supervisor.js b/.bkp-fix-20261023_143429/module-debug-supervisor.js
new file mode 100644
index 0000000..959d162
--- /dev/null
+++ b/.bkp-fix-20261023_143429/module-debug-supervisor.js
@@ -0,0 +1,889 @@
+// module-debug-supervisor.js — Persistent error capture, clustering, root-cause analysis, and auto-fix
+// Commands: debug-stats, debug-history, debug-analyze, debug-trends, debug-fix, debug-health
+class DebugSupervisorModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._healthy = true;
+ this._errorDB = { errors: {}, clusters: {}, patterns: {}, sessionId: null };
+ this._maxErrors = 500;
+ this._ttlDays = 7;
+ this._writeDebounce = null;
+ this._pendingWrites = 0;
+ this._originalConsole = {};
+ this._originalOnError = null;
+ this._originalOnRejection = null;
+ this._originalFetch = null;
+ this._interceptorInstalled = false;
+ this._sessionId = this._generateSessionId();
+ }
+
+ _generateSessionId() {
+ try {
+ let sid = localStorage.getItem('webx11_debug_session');
+ if (!sid) {
+ sid = Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8);
+ localStorage.setItem('webx11_debug_session', sid);
+ }
+ return sid;
+ } catch (e) {
+ return Date.now().toString(36);
+ }
+ }
+
+ getHelpInfo() {
+ return {
+ 'debug-stats': 'debug-stats — Show error summary (total, unique, clusters, top issues)',
+ 'debug-history': 'debug-history [n] — Show last N errors (default: 20)',
+ 'debug-analyze': 'debug-analyze <hash> — Deep analysis of a specific error',
+ 'debug-trends': 'debug-trends [days] — Error frequency over time (default: 7)',
+ 'debug-fix': 'debug-fix <hash> — Suggest and optionally apply fix for an error',
+ 'debug-health': 'debug-health — Check supervisor module health status',
+ 'debug-sync': 'debug-sync [url] — Sync errors to backend API (or set sync URL)',
+ 'debug-sync-url': 'debug-sync-url [url] — View or set the backend sync endpoint',
+ };
+ }
+
+ registerCommands() {
+ this.commands['debug-stats'] = () => this._cmdStats();
+ this.commands['debug-history'] = (args) => this._cmdHistory(args);
+ this.commands['debug-analyze'] = (args) => this._cmdAnalyze(args);
+ this.commands['debug-trends'] = (args) => this._cmdTrends(args);
+ this.commands['debug-fix'] = (args) => this._cmdFix(args);
+ this.commands['debug-health'] = () => this._cmdHealth();
+ this.commands['debug-sync'] = (args) => this._cmdSync(args);
+ this.commands['debug-sync-url'] = (args) => this._cmdSyncUrl(args);
+ }
+
+ seed() {
+ // seed() is not called automatically by module manager — interceptors go in init()
+ }
+
+ init() {
+ this._loadDB();
+ this._installInterceptors();
+ // Start periodic sync (every 5 minutes)
+ this._syncInterval = setInterval(() => this._syncToBackend(), 300000);
+ // Also sync on first load after a short delay
+ setTimeout(() => this._syncToBackend(), 5000);
+ }
+
+ destroy() {
+ // Clear sync interval
+ if (this._syncInterval) {
+ clearInterval(this._syncInterval);
+ this._syncInterval = null;
+ }
+ // Restore original console.error
+ if (this._originalConsole.error) {
+ console.error = this._originalConsole.error;
+ }
+ // Restore original window.onerror
+ if (this._originalOnError) {
+ window.onerror = this._originalOnError;
+ }
+ // Restore original fetch
+ if (this._originalFetch) {
+ window.fetch = this._originalFetch;
+ }
+ // Remove unhandledrejection listener (stored for cleanup)
+ if (this._rejectionHandler) {
+ window.removeEventListener('unhandledrejection', this._rejectionHandler);
+ }
+ this._interceptorInstalled = false;
+ this._healthy = false;
+ }
+
+ // ── Error Database ──
+
+ _loadDB() {
+ try {
+ const raw = localStorage.getItem('webx11_debug_db');
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ this._errorDB = parsed;
+ this._pruneExpired();
+ }
+ } catch (e) {
+ // Corrupted DB — start fresh
+ this._errorDB = { errors: {}, clusters: {}, patterns: {}, sessionId: this._sessionId };
+ }
+ }
+
+ _saveDB() {
+ try {
+ localStorage.setItem('webx11_debug_db', JSON.stringify(this._errorDB));
+ } catch (e) {
+ // Quota exceeded — evict oldest 20% and retry
+ this._evictLRU(0.2);
+ try {
+ localStorage.setItem('webx11_debug_db', JSON.stringify(this._errorDB));
+ } catch (e2) {
+ // Still failing — supervisor degrades gracefully
+ console.warn('[DebugSupervisor] DB write failed, errors may be lost');
+ }
+ }
+ }
+
+ _scheduleSave() {
+ this._pendingWrites++;
+ if (this._writeDebounce) return;
+ this._writeDebounce = setTimeout(() => {
+ this._writeDebounce = null;
+ if (this._pendingWrites > 0) {
+ this._saveDB();
+ this._pendingWrites = 0;
+ }
+ }, 2000);
+ }
+
+ _pruneExpired() {
+ const now = Date.now();
+ const maxAge = this._ttlDays * 86400000;
+ let changed = false;
+ for (const [hash, entry] of Object.entries(this._errorDB.errors)) {
+ if (now - entry.lastSeen > maxAge) {
+ delete this._errorDB.errors[hash];
+ changed = true;
+ }
+ }
+ if (changed) this._saveDB();
+ }
+
+ _evictLRU(fraction) {
+ const entries = Object.entries(this._errorDB.errors);
+ const sorted = entries.sort((a, b) => a[1].lastSeen - b[1].lastSeen);
+ const toRemove = Math.ceil(sorted.length * fraction);
+ for (let i = 0; i < toRemove; i++) {
+ delete this._errorDB.errors[sorted[i][0]];
+ }
+ }
+
+ _checkQuota() {
+ try {
+ // Test localStorage is writable
+ const testKey = '__quota_test__';
+ localStorage.setItem(testKey, '1');
+ localStorage.removeItem(testKey);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ // ── Error Classification ──
+
+ _classifyError(message, source, stack) {
+ const type = this._detectType(message, stack);
+ const severity = this._detectSeverity(type, message);
+ const category = this._detectCategory(message, source, type);
+ return { type, severity, category };
+ }
+
+ _detectType(message, stack) {
+ if (!message) return 'unknown';
+ if (message.includes('ReferenceError') || message.includes('is not defined')) return 'ReferenceError';
+ if (message.includes('TypeError') || message.includes('is not a function') || message.includes('cannot read property')) return 'TypeError';
+ if (message.includes('SyntaxError') || message.includes('Unexpected token')) return 'SyntaxError';
+ if (message.includes('HTTP') || message.includes('NetworkError') || message.includes('Failed to fetch')) return 'HTTP';
+ if (message.includes('timeout') || message.includes('Timeout') || message.includes('AbortError')) return 'timeout';
+ if (message.includes('QuotaExceededError') || message.includes('quota')) return 'quota';
+ if (message.includes('SecurityError') || message.includes('permission')) return 'security';
+ if (stack && stack.includes('tool_')) return 'tool';
+ return 'runtime';
+ }
+
+ _detectSeverity(type, message) {
+ if (type === 'SyntaxError' || type === 'ReferenceError' || type === 'quota') return 'fatal';
+ if (type === 'HTTP' && /\b5\d{2}\b/.test(message)) return 'error';
+ if (type === 'timeout' || type === 'security') return 'error';
+ if (type === 'tool') return 'warning';
+ return 'error';
+ }
+
+ _detectCategory(message, source, type) {
+ if (type === 'HTTP' || type === 'timeout') return 'network';
+ if (type === 'quota') return 'vfs';
+ if (type === 'security') return 'security';
+ if (source && (source.includes('module-') || source.includes('ext/'))) return 'module';
+ if (type === 'tool') return 'tool';
+ return 'runtime';
+ }
+
+ _extractModule(source) {
+ if (!source) return null;
+ const m = source.match(/(module-[a-z-]+)\.js/);
+ return m ? m[1] : null;
+ }
+
+ _computeHash(message, source, type) {
+ const raw = (message || '') + '|' + (source || '') + '|' + (type || '');
+ // Simple hash (Fowler-Noll-Vo)
+ let hash = 0x811c9dc5;
+ for (let i = 0; i < raw.length; i++) {
+ hash ^= raw.charCodeAt(i);
+ hash = Math.imul(hash, 0x01000193);
+ }
+ return (hash >>> 0).toString(16);
+ }
+
+ // ── Error Interception ──
+
+ _installInterceptors() {
+ if (this._interceptorInstalled) return;
+ const self = this;
+
+ // Console.error
+ this._originalConsole.error = console.error;
+ console.error = function() {
+ const args = Array.prototype.slice.call(arguments);
+ const msg = args.map(a => typeof a === 'object' ? (a.message || JSON.stringify(a)) : String(a)).join(' ');
+ self._recordError(msg, 'console.error', null);
+ return self._originalConsole.error.apply(console, args);
+ };
+
+ // window.onerror
+ this._originalOnError = window.onerror;
+ window.onerror = function(msg, src, line, col, err) {
+ const fullMsg = msg + (err ? ' — ' + err.message : '') + ' (' + src + ':' + line + ':' + col + ')';
+ self._recordError(fullMsg, src || 'window.onerror', err ? err.stack : null);
+ if (self._originalOnError) return self._originalOnError.call(window, msg, src, line, col, err);
+ };
+
+ // Unhandled rejections
+ this._originalOnRejection = window.onunhandledrejection;
+ this._rejectionHandler = function(e) {
+ const msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled Promise rejection';
+ self._recordError(msg, 'unhandledrejection', e.reason ? e.reason.stack : null);
+ };
+ window.addEventListener('unhandledrejection', this._rejectionHandler);
+
+ // Fetch failures
+ this._originalFetch = window.fetch;
+ window.fetch = function() {
+ const args = arguments;
+ const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '?');
+ // Never record failures of the debug-sync endpoint itself: that produced a
+ // self-referential loop (38,495 identical "Fetch failed …/ingest.php" entries).
+ if (url.indexOf('/api/debug/') !== -1) return self._originalFetch.apply(window, args);
+ return self._originalFetch.apply(window, args).catch(function(err) {
+ self._recordError('Fetch failed: ' + url + ' — ' + (err.message || err), url, err.stack);
+ throw err;
+ });
+ };
+
+ this._interceptorInstalled = true;
+ }
+
+ // ── Record Error ──
+
+ recordToolError(toolName, args, errorMessage) {
+ this._recordError(errorMessage, 'tool:' + toolName, null);
+ }
+
+ _recordError(message, source, stack) {
+ if (!this._healthy) return;
+ const classification = this._classifyError(message, source, stack);
+ const hash = this._computeHash(message, source, classification.type);
+ const now = Date.now();
+
+ if (this._errorDB.errors[hash]) {
+ this._errorDB.errors[hash].count++;
+ this._errorDB.errors[hash].lastSeen = now;
+ if (this._errorDB.errors[hash].sessions.indexOf(this._sessionId) === -1) {
+ this._errorDB.errors[hash].sessions.push(this._sessionId);
+ }
+ } else {
+ // LRU eviction if at cap
+ const keys = Object.keys(this._errorDB.errors);
+ if (keys.length >= this._maxErrors) {
+ this._evictLRU(0.1);
+ }
+ this._errorDB.errors[hash] = {
+ count: 1,
+ firstSeen: now,
+ lastSeen: now,
+ message: message,
+ type: classification.type,
+ severity: classification.severity,
+ category: classification.category,
+ source: source,
+ stack: stack ? stack.split('\n').slice(0, 10).join('\n') : null,
+ module: this._extractModule(source),
+ sessions: [this._sessionId],
+ };
+ }
+
+ this._scheduleSave();
+
+ // v4.6.38 (A5): auto-file fatal/error-class failures, max 3 per day, deduped by hash.
+ if (classification.severity === 'fatal' || classification.severity === 'error') {
+ this._maybeAutoFile({ hash, message, source, type: classification.type, severity: classification.severity });
+ }
+ }
+
+ _maybeAutoFile(e) {
+ try { this._maybeAutoFileInner(e); } catch (_e) {}
+ }
+
+ _maybeAutoFileInner(e) {
+ try { if (localStorage.getItem('webx11_autoreport') === 'off') return; } catch (_e) {}
+
+ const today = new Date().toISOString().slice(0, 10);
+ let state = { day: today, count: 0, hashes: [] };
+ try {
+ const raw = JSON.parse(localStorage.getItem('webx11_autoreport_day') || 'null');
+ if (raw && raw.day === today && Array.isArray(raw.hashes)) state = raw;
+ } catch (_e) {}
+
+ if (state.count >= 3) return; // daily quota
+ if (state.hashes.indexOf(e.hash) !== -1) return; // one report per distinct error
+ state.count++;
+ state.hashes.push(e.hash);
+ try { localStorage.setItem('webx11_autoreport_day', JSON.stringify(state)); } catch (_e) {}
+
+ const payload = {
+ title: 'Auto: ' + String(e.message || 'unknown error').slice(0, 120),
+ body: 'Automatically filed by the debug supervisor (no user action).',
+ diagnostics: {
+ version: (typeof window !== 'undefined' && window.NEWOS4_VERSION) || 'unknown',
+ url: (typeof location !== 'undefined') ? location.href : '',
+ ua: (typeof navigator !== 'undefined') ? navigator.userAgent : '',
+ auto: true,
+ errors: [{
+ message: String(e.message || '').slice(0, 300),
+ source: String(e.source || '').slice(0, 200),
+ type: e.type,
+ severity: e.severity,
+ count: 1,
+ }],
+ },
+ };
+ try {
+ // /api/debug/* is excluded from the fetch interceptor, so a failure here cannot loop.
+ fetch('/spaces/newos4/api/debug/report.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ }).catch(function () {});
+ } catch (_e) {}
+ }
+
+ // ── Clustering (Jaccard similarity) ──
+
+ _normalizeMessage(msg) {
+ if (!msg) return '';
+ return msg
+ .toLowerCase()
+ .replace(/\b0x[a-f0-9]+\b/g, '0xHEX')
+ .replace(/\b\d+\b/g, 'N')
+ .replace(/["'].*?["']/g, '"STR"')
+ .replace(/[^\w\s]/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+ }
+
+ _jaccardSimilarity(a, b) {
+ const setA = new Set(a.split(' ').filter(Boolean));
+ const setB = new Set(b.split(' ').filter(Boolean));
+ if (setA.size === 0 && setB.size === 0) return 1;
+ let intersection = 0;
+ for (const token of setA) {
+ if (setB.has(token)) intersection++;
+ }
+ const union = setA.size + setB.size - intersection;
+ return union === 0 ? 0 : intersection / union;
+ }
+
+ _clusterErrors() {
+ const errors = Object.entries(this._errorDB.errors);
+ if (errors.length === 0) return [];
+
+ const clusters = [];
+ const assigned = new Set();
+
+ for (let i = 0; i < errors.length; i++) {
+ if (assigned.has(i)) continue;
+ const [hashI, entryI] = errors[i];
+ const normI = this._normalizeMessage(entryI.message);
+ const cluster = { hashes: [hashI], messages: [entryI.message], count: entryI.count, types: new Set([entryI.type]), categories: new Set([entryI.category]) };
+ assigned.add(i);
+
+ for (let j = i + 1; j < errors.length; j++) {
+ if (assigned.has(j)) continue;
+ const [hashJ, entryJ] = errors[j];
+ const normJ = this._normalizeMessage(entryJ.message);
+ if (this._jaccardSimilarity(normI, normJ) >= 0.35) {
+ cluster.hashes.push(hashJ);
+ cluster.messages.push(entryJ.message);
+ cluster.count += entryJ.count;
+ cluster.types.add(entryJ.type);
+ cluster.categories.add(entryJ.category);
+ assigned.add(j);
+ }
+ }
+
+ cluster.label = this._mostFrequent(cluster.messages);
+ cluster.typeCount = cluster.types.size;
+ cluster.categoryCount = cluster.categories.size;
+ delete cluster.types;
+ delete cluster.categories;
+ clusters.push(cluster);
+ }
+
+ clusters.sort((a, b) => b.count - a.count);
+ return clusters;
+ }
+
+ _mostFrequent(arr) {
+ const freq = {};
+ let best = arr[0], bestCount = 1;
+ for (const item of arr) {
+ freq[item] = (freq[item] || 0) + 1;
+ if (freq[item] > bestCount) {
+ best = item;
+ bestCount = freq[item];
+ }
+ }
+ return best;
+ }
+
+ // ── Root-Cause Analysis ──
+
+ _analyzeRootCause(hash) {
+ const entry = this._errorDB.errors[hash];
+ if (!entry) return { found: false };
+
+ const findings = [];
+ const fixes = [];
+
+ // Tool failure → check module
+ if (entry.source && entry.source.startsWith('tool:')) {
+ const toolName = entry.source.replace('tool:', '');
+ const mm = this.terminal?.moduleManager;
+ if (mm) {
+ // Check if any module has this command
+ let found = false;
+ for (const [modName, mod] of Object.entries(mm.modules)) {
+ if (mod.commands && mod.commands[toolName]) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ findings.push({ type: 'module_not_loaded', detail: 'No module provides command "' + toolName + '"' });
+ fixes.push({ action: 'loadmod', params: toolName, description: 'Load module providing ' + toolName });
+ }
+ }
+ }
+
+ // HTTP error → check network
+ if (entry.type === 'HTTP' || entry.type === 'timeout') {
+ findings.push({ type: 'network_issue', detail: 'Network request failed: ' + entry.message.substring(0, 100) });
+ fixes.push({ action: 'test-net', params: null, description: 'Run network diagnostics' });
+ }
+
+ // ReferenceError → check bundle
+ if (entry.type === 'ReferenceError') {
+ findings.push({ type: 'missing_class', detail: 'ReferenceError suggests a class or variable is not defined' });
+ fixes.push({ action: 'reload', params: null, description: 'Reload the page to re-initialize all modules' });
+ }
+
+ // Module error → check module loader
+ if (entry.category === 'module') {
+ findings.push({ type: 'module_error', detail: 'Error in module: ' + (entry.module || 'unknown') });
+ fixes.push({ action: 'loadmod', params: entry.module, description: 'Reload module: ' + entry.module });
+ }
+
+ return { found: findings.length > 0, findings, fixes };
+ }
+
+ // ── Fix Registry ──
+
+ _buildFixSuggestion(hash) {
+ const entry = this._errorDB.errors[hash];
+ if (!entry) return null;
+
+ const rca = this._analyzeRootCause(hash);
+ if (!rca.found) {
+ return { confidence: 0, suggestion: 'No known fix pattern for this error', autoApply: false };
+ }
+
+ // Calculate confidence based on frequency
+ let confidence = Math.min(0.5, entry.count / 10);
+ if (entry.count >= 3) confidence = 0.7;
+ if (entry.count >= 10) confidence = 0.9;
+
+ return {
+ confidence: confidence,
+ suggestion: rca.findings.map(f => f.detail).join('; '),
+ fixes: rca.fixes,
+ autoApply: confidence >= 0.9,
+ };
+ }
+
+ // ── Terminal Commands ──
+
+ _cmdStats() {
+ const db = this._errorDB.errors;
+ const keys = Object.keys(db);
+ if (keys.length === 0) {
+ this.terminal.print('📊 Debug Stats: No errors recorded', 'success');
+ return;
+ }
+
+ const totalCount = keys.reduce((sum, k) => sum + db[k].count, 0);
+ const bySeverity = {};
+ const byCategory = {};
+ for (const k of keys) {
+ const e = db[k];
+ bySeverity[e.severity] = (bySeverity[e.severity] || 0) + e.count;
+ byCategory[e.category] = (byCategory[e.category] || 0) + e.count;
+ }
+
+ const clusters = this._clusterErrors();
+ const topClusters = clusters.slice(0, 5);
+
+ this.terminal.print('📊 Debug Stats', 'header');
+ this.terminal.print(' Total errors: ' + totalCount, 'info');
+ this.terminal.print(' Unique errors: ' + keys.length, 'info');
+ this.terminal.print(' Clusters: ' + clusters.length, 'info');
+ this.terminal.print(' Session: ' + this._sessionId, 'text-muted');
+ this.terminal.print(' Supervisor: ' + (this._healthy ? '✅ healthy' : '🔴 degraded'), this._healthy ? 'success' : 'error');
+
+ this.terminal.print('── By Severity ──', 'info');
+ for (const [sev, count] of Object.entries(bySeverity)) {
+ const icon = sev === 'fatal' ? '🔴' : sev === 'error' ? '🟡' : '🟢';
+ this.terminal.print(' ' + icon + ' ' + sev + ': ' + count, sev === 'fatal' ? 'error' : 'warning');
+ }
+
+ this.terminal.print('── By Category ──', 'info');
+ for (const [cat, count] of Object.entries(byCategory)) {
+ this.terminal.print(' • ' + cat + ': ' + count, 'text-muted');
+ }
+
+ if (topClusters.length > 0) {
+ this.terminal.print('── Top Clusters ──', 'info');
+ for (const c of topClusters) {
+ this.terminal.print(' [' + c.hashes.length + ' errors ×' + c.count + '] ' + c.label.substring(0, 80), 'warning');
+ }
+ this.terminal.print(' Run debug-analyze <hash> for details', 'text-muted');
+ }
+ }
+
+ _cmdHistory(args) {
+ const n = (args && args[0]) ? parseInt(args[0]) : 20;
+ const entries = Object.entries(this._errorDB.errors)
+ .sort((a, b) => b[1].lastSeen - a[1].lastSeen)
+ .slice(0, n);
+
+ if (entries.length === 0) {
+ this.terminal.print('No errors recorded', 'success');
+ return;
+ }
+
+ this.terminal.print('── Error History (last ' + entries.length + ') ──', 'header');
+ for (const [hash, e] of entries) {
+ const time = new Date(e.lastSeen).toLocaleTimeString();
+ const icon = e.severity === 'fatal' ? '🔴' : e.severity === 'error' ? '🟡' : '🟢';
+ const msg = e.message.substring(0, 80);
+ this.terminal.print(' ' + icon + ' [' + time + '] ' + msg, e.severity === 'fatal' ? 'error' : 'text-muted');
+ this.terminal.print(' hash=' + hash + ' type=' + e.type + ' cat=' + e.category + ' ×' + e.count, 'text-muted');
+ }
+ this.terminal.print('── End ──', 'info');
+ this.terminal.print('Run debug-analyze <hash> for details', 'text-muted');
+ }
+
+ _cmdAnalyze(args) {
+ const hash = (args && args[0]) || '';
+ if (!hash) {
+ this.terminal.print('Usage: debug-analyze <hash>', 'warning');
+ return;
+ }
+
+ const entry = this._errorDB.errors[hash];
+ if (!entry) {
+ this.terminal.print('Error not found: ' + hash, 'error');
+ return;
+ }
+
+ this.terminal.print('── Error Analysis: ' + hash + ' ──', 'header');
+ this.terminal.print(' Message: ' + entry.message, 'info');
+ this.terminal.print(' Type: ' + entry.type + ' | Severity: ' + entry.severity + ' | Category: ' + entry.category, 'info');
+ this.terminal.print(' Source: ' + (entry.source || '?'), 'text-muted');
+ this.terminal.print(' Module: ' + (entry.module || 'unknown'), 'text-muted');
+ this.terminal.print(' Count: ' + entry.count + ' | First: ' + new Date(entry.firstSeen).toLocaleString() + ' | Last: ' + new Date(entry.lastSeen).toLocaleString(), 'info');
+ this.terminal.print(' Sessions: ' + (entry.sessions || []).length, 'text-muted');
+ if (entry.stack) {
+ this.terminal.print('── Stack Trace (top 10) ──', 'info');
+ this.terminal.print(entry.stack, 'text-muted');
+ }
+
+ // Root-cause analysis
+ const rca = this._analyzeRootCause(hash);
+ if (rca.found) {
+ this.terminal.print('── Root Cause Analysis ──', 'warning');
+ for (const f of rca.findings) {
+ this.terminal.print(' • [' + f.type + '] ' + f.detail, 'warning');
+ }
+ if (rca.fixes.length > 0) {
+ this.terminal.print('── Suggested Fixes ──', 'success');
+ for (const fix of rca.fixes) {
+ this.terminal.print(' → ' + fix.action + (fix.params ? ' ' + fix.params : '') + ' — ' + fix.description, 'success');
+ }
+ }
+ } else {
+ this.terminal.print(' No root cause identified', 'text-muted');
+ }
+
+ // Fix suggestion
+ const fix = this._buildFixSuggestion(hash);
+ if (fix && fix.confidence > 0) {
+ this.terminal.print('── Auto-Fix Confidence: ' + Math.round(fix.confidence * 100) + '% ──', fix.autoApply ? 'success' : 'warning');
+ this.terminal.print(' ' + fix.suggestion, 'info');
+ if (fix.autoApply) {
+ this.terminal.print(' ⚡ Confidence ≥ 90% — fix can be auto-applied with debug-fix ' + hash, 'success');
+ }
+ }
+ }
+
+ _cmdTrends(args) {
+ const days = (args && args[0]) ? parseInt(args[0]) : 7;
+ const cutoff = Date.now() - days * 86400000;
+
+ const entries = Object.values(this._errorDB.errors).filter(e => e.lastSeen >= cutoff);
+ if (entries.length === 0) {
+ this.terminal.print('No errors in the last ' + days + ' days', 'success');
+ return;
+ }
+
+ // Group by day
+ const byDay = {};
+ for (const e of entries) {
+ const day = new Date(e.lastSeen).toISOString().split('T')[0];
+ if (!byDay[day]) byDay[day] = { total: 0, byType: {} };
+ byDay[day].total += e.count;
+ byDay[day].byType[e.type] = (byDay[day].byType[e.type] || 0) + e.count;
+ }
+
+ const sortedDays = Object.keys(byDay).sort();
+ this.terminal.print('── Error Trends (last ' + days + ' days) ──', 'header');
+ for (const day of sortedDays) {
+ const d = byDay[day];
+ const types = Object.entries(d.byType).map(([t, c]) => t + ':' + c).join(', ');
+ this.terminal.print(' ' + day + ': ' + d.total + ' errors (' + types + ')', d.total > 5 ? 'warning' : 'text-muted');
+ }
+
+ // Top error types
+ const typeTotals = {};
+ for (const e of entries) {
+ typeTotals[e.type] = (typeTotals[e.type] || 0) + e.count;
+ }
+ this.terminal.print('── By Type ──', 'info');
+ for (const [type, count] of Object.entries(typeTotals).sort((a, b) => b[1] - a[1])) {
+ this.terminal.print(' • ' + type + ': ' + count, 'text-muted');
+ }
+ }
+
+ _cmdFix(args) {
+ const hash = (args && args[0]) || '';
+ if (!hash) {
+ this.terminal.print('Usage: debug-fix <hash>', 'warning');
+ return;
+ }
+
+ const fix = this._buildFixSuggestion(hash);
+ if (!fix || fix.confidence === 0) {
+ this.terminal.print('No fix available for error: ' + hash, 'warning');
+ return;
+ }
+
+ this.terminal.print('── Fix Suggestion for ' + hash + ' ──', 'header');
+ this.terminal.print(' Confidence: ' + Math.round(fix.confidence * 100) + '%', fix.autoApply ? 'success' : 'warning');
+ this.terminal.print(' Suggestion: ' + fix.suggestion, 'info');
+
+ if (fix.fixes && fix.fixes.length > 0) {
+ this.terminal.print('── Available Fixes ──', 'info');
+ for (const f of fix.fixes) {
+ this.terminal.print(' → ' + f.action + (f.params ? ' ' + f.params : ''), 'success');
+ this.terminal.print(' ' + f.description, 'text-muted');
+ }
+ }
+
+ if (fix.autoApply) {
+ this.terminal.print('⚡ Auto-applying fix...', 'info');
+ this._applyFix(hash, fix);
+ } else {
+ this.terminal.print('Confidence below auto-apply threshold. Run the suggested command manually.', 'warning');
+ }
+ }
+
+ _applyFix(hash, fix) {
+ if (!fix.fixes || fix.fixes.length === 0) return;
+ const firstFix = fix.fixes[0];
+ try {
+ // Sanitize params: only allow alphanumeric, hyphens, underscores, dots
+ const safeParams = (firstFix.params || '').replace(/[^a-zA-Z0-9_\-\.]/g, '');
+ if (firstFix.action === 'loadmod' && this.terminal) {
+ this.terminal.executeCommand('loadmod ' + safeParams);
+ this.terminal.print(' ✅ Applied: loadmod ' + safeParams, 'success');
+ } else if (firstFix.action === 'test-net' && this.terminal) {
+ this.terminal.executeCommand('test-net');
+ this.terminal.print(' ✅ Applied: test-net', 'success');
+ } else if (firstFix.action === 'reload') {
+ this.terminal.print(' ℹ️ Manual action needed: reload the page', 'warning');
+ } else {
+ this.terminal.print(' ℹ️ Cannot auto-apply: ' + firstFix.action, 'warning');
+ }
+ } catch (e) {
+ this.terminal.print(' ❌ Fix failed: ' + e.message, 'error');
+ }
+ }
+
+ _cmdHealth() {
+ this.terminal.print('── DebugSupervisor Health ──', 'header');
+ this.terminal.print(' Status: ' + (this._healthy ? '✅ Healthy' : '🔴 Degraded'), this._healthy ? 'success' : 'error');
+ this.terminal.print(' Session: ' + this._sessionId, 'text-muted');
+ this.terminal.print(' Errors tracked: ' + Object.keys(this._errorDB.errors).length + ' unique', 'info');
+ this.terminal.print(' Max errors: ' + this._maxErrors, 'text-muted');
+ this.terminal.print(' TTL: ' + this._ttlDays + ' days', 'text-muted');
+ this.terminal.print(' Interceptors: ' + (this._interceptorInstalled ? '✅ active' : '🔴 inactive'), this._interceptorInstalled ? 'success' : 'error');
+ this.terminal.print(' Quota: ' + (this._checkQuota() ? '✅ available' : '🔴 low'), this._checkQuota() ? 'success' : 'error');
+ }
+
+ // ── Sync to Backend ──
+
+ _getSyncUrl() {
+ try {
+ return localStorage.getItem('webx11_debug_sync_url') || 'https://webx11.duckdns.org/spaces/newos4/api/debug/ingest.php';
+ } catch (e) {
+ return 'https://webx11.duckdns.org/spaces/newos4/api/debug/ingest.php';
+ }
+ }
+
+ async _syncToBackend() {
+ if (!this._healthy) return;
+ const db = this._errorDB.errors;
+ const keys = Object.keys(db);
+ if (keys.length === 0) return;
+
+ const payload = {
+ version: window.NEWOS4_VERSION || 'v4.x',
+ session: {
+ id: this._sessionId,
+ modules: this.terminal?.moduleManager ? Object.keys(this.terminal.moduleManager.modules) : [],
+ },
+ errors: keys.map(k => ({
+ hash: k,
+ message: db[k].message,
+ type: db[k].type,
+ severity: db[k].severity,
+ category: db[k].category,
+ source: db[k].source,
+ module: db[k].module,
+ stack: db[k].stack,
+ count: db[k].count,
+ firstSeen: new Date(db[k].firstSeen).toISOString(),
+ lastSeen: new Date(db[k].lastSeen).toISOString(),
+ })),
+ };
+
+ try {
+ const resp = await fetch(this._getSyncUrl(), {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ signal: AbortSignal.timeout(10000),
+ });
+ if (resp.ok) {
+ const result = await resp.json();
+ if (result.success) {
+ const sent = keys.length;
+ const accepted = result.ingested || 0;
+ if (accepted < sent) {
+ console.warn('[DebugSupervisor] Sync truncated: sent ' + sent + ' errors, server accepted ' + accepted + ' (max ' + (result.total_errors || '?') + ' total)');
+ } else {
+ console.log('[DebugSupervisor] Synced ' + accepted + ' errors to backend');
+ }
+ this._lastSyncOk = Date.now();
+ return true;
+ }
+ }
+ console.warn('[DebugSupervisor] Sync returned HTTP ' + resp.status);
+ return false;
+ } catch (e) {
+ console.warn('[DebugSupervisor] Sync failed: ' + e.message);
+ return false;
+ }
+ }
+
+ _cmdSync(args) {
+ const url = (args && args[0]) ? args[0] : null;
+ if (url) {
+ try {
+ localStorage.setItem('webx11_debug_sync_url', url);
+ this.terminal.print('✅ Sync URL set to: ' + url, 'success');
+ } catch (e) {
+ this.terminal.print('❌ Failed to save sync URL', 'error');
+ }
+ return;
+ }
+ this.terminal.print('🔄 Syncing errors to backend...', 'info');
+ this._syncToBackend().then(ok => {
+ if (ok) {
+ this.terminal.print('✅ Sync complete', 'success');
+ } else {
+ this.terminal.print('❌ Sync failed', 'error');
+ }
+ });
+ }
+
+ _cmdSyncUrl(args) {
+ if (args && args[0]) {
+ try {
+ localStorage.setItem('webx11_debug_sync_url', args[0]);
+ this.terminal.print('✅ Sync URL set to: ' + args[0], 'success');
+ } catch (e) {
+ this.terminal.print('❌ Failed to save sync URL', 'error');
+ }
+ } else {
+ this.terminal.print('Current sync URL: ' + this._getSyncUrl(), 'info');
+ }
+ }
+
+ // ── Dashboard Data ──
+
+ getDashboardData() {
+ const db = this._errorDB.errors;
+ const keys = Object.keys(db);
+ const totalCount = keys.reduce((sum, k) => sum + db[k].count, 0);
+ const clusters = this._clusterErrors().slice(0, 5);
+
+ return {
+ totalUnique: keys.length,
+ totalCount: totalCount,
+ clusters: clusters.map(c => ({ label: c.label.substring(0, 60), count: c.count, hashes: c.hashes.length })),
+ healthy: this._healthy,
+ topType: this._getTopType(db),
+ topCategory: this._getTopCategory(db),
+ };
+ }
+
+ _getTopType(db) {
+ const freq = {};
+ for (const e of Object.values(db)) {
+ freq[e.type] = (freq[e.type] || 0) + e.count;
+ }
+ const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]);
+ return sorted.length > 0 ? sorted[0][0] : null;
+ }
+
+ _getTopCategory(db) {
+ const freq = {};
+ for (const e of Object.values(db)) {
+ freq[e.category] = (freq[e.category] || 0) + e.count;
+ }
+ const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]);
+ return sorted.length > 0 ? sorted[0][0] : null;
+ }
+}
diff --git a/.bkp-fix-20261023_143429/module-loader.js b/.bkp-fix-20261023_143429/module-loader.js
new file mode 100644
index 0000000..46aad17
--- /dev/null
+++ b/.bkp-fix-20261023_143429/module-loader.js
@@ -0,0 +1,84 @@
+(function() {
+ 'use strict';
+
+ // Core module bundle loaded synchronously in main.html.
+ // This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
+
+ const LATEST_VER = 'v4.6.51';
+ const CACHE = '?v=' + LATEST_VER;
+ const stored = localStorage.getItem('webx11_cache_ver');
+ if (stored !== LATEST_VER) {
+ localStorage.setItem('webx11_cache_ver', LATEST_VER);
+ }
+
+ function setSplash(phase, current, total, filename) {
+ const fill = document.getElementById('splash-progress-fill');
+ const phaseEl = document.getElementById('splash-phase');
+ const fileEl = document.getElementById('splash-filename');
+ if (fill && total > 0) {
+ fill.style.width = Math.round((current / total) * 100) + '%';
+ }
+ if (phaseEl) phaseEl.textContent = phase;
+ if (fileEl) fileEl.textContent = filename || '';
+ }
+
+ function hideSplash() {
+ const overlay = document.getElementById('splash-overlay');
+ if (overlay) {
+ overlay.classList.add('hidden');
+ setTimeout(() => { overlay.style.display = 'none'; }, 350);
+ }
+ }
+
+ function showSplashError(msg) {
+ const el = document.getElementById('splash-error');
+ const fill = document.getElementById('splash-progress-fill');
+ if (el) { el.textContent = msg; el.classList.add('show'); }
+ if (fill) fill.style.background = 'var(--error-color)';
+ }
+
+ // Create webx11 synchronously so critical-ext-bundle.js (loaded right after us)
+ // can call registerModule() without waiting for DOMContentLoaded
+ const webx11 = new WebX11Terminal();
+ window.webx11 = webx11;
+
+ async function init() {
+ try {
+ const total = 3;
+ let count = 0;
+
+ setSplash('Core bundle loaded (1/3)', ++count, total, 'core-bundle.js');
+ setSplash('Critical ext bundle loaded (2/3)', ++count, total, 'critical-ext-bundle.js');
+
+ setSplash('Initializing terminal (3/3)', total, total, '');
+ // webx11 already created synchronously above — just init ext modules
+ if (typeof webx11._loadExtModules === 'function') {
+ let extKeys = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console', 'media', 'three', 'kanban', 'course'];
+ const saved = localStorage.getItem('webx11_startup_modules');
+ let selected = null;
+ try { if (saved) selected = JSON.parse(saved); } catch (e) {}
+ if (selected && Array.isArray(selected)) {
+ extKeys = extKeys.filter(k => selected.includes(k + '.js'));
+ }
+ let disabledMods = [];
+ try { disabledMods = JSON.parse(localStorage.getItem('webx11_module_disabled') || '[]'); } catch (e) {}
+ extKeys = extKeys.filter(k => !disabledMods.includes(k));
+ if (extKeys.length > 0) {
+ setTimeout(() => webx11._loadExtModules(extKeys), 800);
+ }
+ }
+
+ hideSplash();
+ console.log('[ModuleLoader] Core loader initialized');
+ } catch (err) {
+ console.error('[ModuleLoader] Fatal error:', err);
+ showSplashError('Loading failed — ' + err.message);
+ }
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/.bkp-fix-20261023_143429/module-memory-graph.js b/.bkp-fix-20261023_143429/module-memory-graph.js
new file mode 100644
index 0000000..ffc79e0
--- /dev/null
+++ b/.bkp-fix-20261023_143429/module-memory-graph.js
@@ -0,0 +1,663 @@
+// module-memory-graph.js — Cross-session persistent memory graph
+// Nodes: files, concepts, tasks, patterns, users
+// Edges: relationships with weight, last_used
+// Commands: memgraph-status, memgraph-search, memgraph-stats, memgraph-export, memgraph-prune
+class MemoryGraphModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._nodes = {};
+ this._edges = [];
+ this._indexPath = '/.memory_graph/';
+ this._debounceTimer = null;
+ this._initialized = false;
+ this._totalNodeAccesses = 0;
+ // v4.5.89: Decay configuration
+ this._decayDays = 30;
+ this._decayMinUses = 3;
+ this._decayRate = 0.01; // Daily decay rate
+ this._lastDecayRun = 0;
+ }
+
+ getHelpInfo() {
+ return {
+ 'memgraph-status': 'memgraph-status — Show node/edge counts, last access',
+ 'memgraph-search': 'memgraph-search <query> — Find nodes by label or type',
+ 'memgraph-stats': 'memgraph-stats — Most connected nodes, strongest edges, cluster info',
+ 'memgraph-export': 'memgraph-export — Export graph as JSON',
+ 'memgraph-prune': 'memgraph-prune — Remove stale low-weight edges (>30d, <3 accesses)',
+ 'memgraph-learn': 'memgraph-learn <entity> <fact> — Add a fact to the memory graph',
+ 'memgraph-session': 'memgraph-session [date] — Show graph nodes for a session (default: today)',
+ 'memgraph-agent': 'memgraph-agent <agentId> — Show namespaced nodes for an agent',
+ 'memgraph-decay-config': 'memgraph-decay-config <days> <min_uses> — Configure decay policy (default: 30d, 3 uses)',
+ };
+ }
+
+ registerCommands() {
+ this.commands['memgraph-status'] = () => this.showStatus();
+ this.commands['memgraph-search'] = (args) => this.search(args.join(' '));
+ this.commands['memgraph-stats'] = () => this.showStats();
+ this.commands['memgraph-export'] = () => this.exportGraph();
+ this.commands['memgraph-prune'] = () => this.prune();
+ this.commands['memgraph-learn'] = (args) => this.learn(args);
+ this.commands['memgraph-session'] = (args) => this.showSession(args.join(' '));
+ this.commands['memgraph-agent'] = (args) => this.showAgent(args.join(' '));
+ // v4.5.89: Decay configuration
+ this.commands['memgraph-decay-config'] = (args) => this.decayConfig(args);
+ }
+
+ init(terminal) {
+ this.terminal = terminal;
+ this._loadFromVFS();
+ this._initialized = true;
+ this.terminal.print('🧠 Memory Graph loaded — cross-session context active', 'info');
+ // Log startup for graph
+ // v4.5.89 fix: Use day-floored key to match edge source keys (P0-1 from review)
+ this.addNode('session', `session-${Math.floor(Date.now() / 86400000)}`, `Session ${new Date().toLocaleDateString()}`);
+ }
+
+ // ─── Node API ───
+
+ addNode(type, id, label, metadata = {}, agentId = null) {
+ // v4.5.88: Support agent namespacing with dual-lookup
+ const ns = agentId ? `agent:${agentId}` : 'global';
+ const key = agentId ? `${ns}:${type}:${id}` : `${type}:${id}`;
+ if (!this._nodes[key]) {
+ this._nodes[key] = {
+ id: key,
+ type,
+ label: label || id,
+ metadata: metadata || {},
+ created: new Date().toISOString(),
+ updated: new Date().toISOString(),
+ access_count: 1,
+ namespace: ns,
+ source_session: `session-${Math.floor(Date.now() / 86400000)}`
+ };
+ } else {
+ this._nodes[key].updated = new Date().toISOString();
+ this._nodes[key].access_count++;
+ if (metadata && Object.keys(metadata).length > 0) {
+ Object.assign(this._nodes[key].metadata, metadata);
+ }
+ }
+ this._totalNodeAccesses++;
+ this._debounceSave();
+ return key;
+ }
+
+ // v4.5.88: Dual-lookup — try namespaced key first, fall back to global
+ getNode(type, id, agentId = null) {
+ if (agentId) {
+ const nsKey = `agent:${agentId}:${type}:${id}`;
+ if (this._nodes[nsKey]) return this._nodes[nsKey];
+ }
+ const globalKey = `${type}:${id}`;
+ return this._nodes[globalKey] || null;
+ }
+
+ findNodes(query) {
+ const q = query.toLowerCase();
+ return Object.values(this._nodes).filter(n =>
+ n.label.toLowerCase().includes(q) ||
+ n.type.toLowerCase().includes(q) ||
+ (n.id && n.id.toLowerCase().includes(q))
+ );
+ }
+
+ // ─── Edge API ───
+
+ // v4.5.88: addEdge tracks source_agents for multi-agent attribution
+ addEdge(sourceKey, targetKey, type, weight = 1, agentId = null) {
+ // Check if edge already exists
+ let edge = this._edges.find(e =>
+ e.source === sourceKey && e.target === targetKey && e.type === type
+ );
+ if (edge) {
+ edge.weight = (edge.weight + weight) / 2;
+ edge.last_used = new Date().toISOString();
+ edge.use_count++;
+ if (agentId) edge.source_agents = [...new Set([...(edge.source_agents || []), agentId])];
+ } else {
+ this._edges.push({
+ source: sourceKey,
+ target: targetKey,
+ type,
+ weight,
+ created: new Date().toISOString(),
+ last_used: new Date().toISOString(),
+ use_count: 1,
+ source_agents: agentId ? [agentId] : []
+ });
+ }
+ this._debounceSave();
+ }
+
+ // ─── Event-based Learning ───
+
+ // v4.5.89 fix: Thread agentId through record* methods (P1-3 from review)
+ recordFileAccess(path, agentId = null) {
+ const name = path.split('/').pop() || path;
+ const nodeKey = this.addNode('file', path, name, {}, agentId);
+ const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
+ this.addEdge(sessionKey, nodeKey, 'accessed', 0.5, agentId);
+
+ // Extract file extension edge
+ const ext = path.split('.').pop();
+ if (ext && ext.length < 10) {
+ this.addNode('type', ext, `.${ext} files`);
+ this.addEdge(nodeKey, `type:${ext}`, 'has_type', 0.3);
+ }
+ }
+
+ recordToolCall(toolName, success, agentId = null) {
+ const nodeKey = this.addNode('tool', toolName, toolName, {}, agentId);
+ const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
+ this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8, agentId);
+ }
+
+ recordConcept(concept, filePath) {
+ const conceptKey = this.addNode('concept', concept, concept);
+ const fileKey = this.addNode('file', filePath, filePath.split('/').pop());
+ this.addEdge(conceptKey, fileKey, 'maps_to', 0.7);
+ this.addEdge(fileKey, conceptKey, 'tagged_as', 0.7);
+ }
+
+ // v4.5.88: recordTask now passes agentId for namespacing + adds source_session link
+ recordTask(task, result, agentId) {
+ const taskId = `task-${Date.now()}`;
+ const taskKey = this.addNode('task', taskId, task.substring(0, 60), {
+ full: task,
+ result: result?.substring(0, 100),
+ agent: agentId
+ }, agentId);
+ const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
+ this.addEdge(sessionKey, taskKey, 'executed', 0.6, agentId);
+
+ // Extract keywords as concepts
+ const words = task.toLowerCase().split(/\s+/).filter(w => w.length > 3);
+ for (const word of [...new Set(words)].slice(0, 5)) {
+ this.addNode('concept', word, word);
+ this.addEdge(taskKey, `concept:${word}`, 'involves', 0.4);
+ }
+
+ // Link to tools used (if any found in task)
+ const knownTools = ['list_files', 'read_file', 'write_file', 'web_search', 'web_fetch',
+ 'edit_module', 'view_module', 'run_command', 'orchestrate'];
+ for (const tool of knownTools) {
+ if (task.toLowerCase().includes(tool.replace('_', ' '))) {
+ this.recordToolCall(tool, true);
+ }
+ }
+ }
+
+ // ─── Context Injection ───
+
+ getContextSummary() {
+ // Get top active nodes from recent session
+ const now = Date.now();
+ const today = `session:session-${Math.floor(now / 86400000)}`;
+ const yesterday = `session:session-${Math.floor(now / 86400000) - 1}`;
+
+ const lines = [];
+ const totalNodes = Object.keys(this._nodes).length;
+ const totalEdges = this._edges.length;
+
+ lines.push(`Memory Graph: ${totalNodes} nodes, ${totalEdges} edges`);
+
+ // Recent task context
+ const recentTasks = Object.values(this._nodes)
+ .filter(n => n.type === 'task')
+ .sort((a, b) => new Date(b.updated) - new Date(a.updated))
+ .slice(0, 3);
+
+ if (recentTasks.length > 0) {
+ lines.push('Recent tasks:');
+ for (const t of recentTasks) {
+ lines.push(` - ${t.label.substring(0, 50)}`);
+ }
+ }
+
+ // Most accessed files
+ const topFiles = Object.values(this._nodes)
+ .filter(n => n.type === 'file')
+ .sort((a, b) => b.access_count - a.access_count)
+ .slice(0, 3);
+
+ if (topFiles.length > 0) {
+ lines.push('Most accessed files:');
+ for (const f of topFiles) {
+ lines.push(` - ${f.label} (${f.access_count}x)`);
+ }
+ }
+
+ return lines.join('\n');
+ }
+
+ getContextForPrompt() {
+ const summary = this.getContextSummary();
+ if (!summary) return '';
+ // Suppress noise: don't inject when graph is nearly empty
+ const totalNodes = Object.keys(this._nodes).length;
+ const totalEdges = this._edges.length;
+ if (totalNodes <= 2 && totalEdges === 0) return '';
+ return `[Memory Graph Context]\n${summary}\n`;
+ }
+
+ // Alias for orchestrator compatibility (orchestrator calls getStats, not showStats)
+ getStats() {
+ const types = {};
+ for (const n of Object.values(this._nodes)) {
+ types[n.type] = (types[n.type] || 0) + 1;
+ }
+ return {
+ nodes: Object.keys(this._nodes).length,
+ edges: this._edges.length,
+ types: types,
+ totalAccesses: this._totalNodeAccesses
+ };
+ }
+
+ // ─── Commands ───
+
+ showStatus() {
+ // v4.5.89: Lazy decay — run applyDecay() on status check
+ this.applyDecay();
+ const types = {};
+ for (const n of Object.values(this._nodes)) {
+ types[n.type] = (types[n.type] || 0) + 1;
+ }
+ const typeStr = Object.entries(types)
+ .sort((a, b) => b[1] - a[1])
+ .map(([t, c]) => `${t}: ${c}`)
+ .join(', ');
+
+ this.terminal.print('🧠 Memory Graph', 'header');
+ this.terminal.print(` Nodes: ${Object.keys(this._nodes).length} (${typeStr})`, 'info');
+ this.terminal.print(` Edges: ${this._edges.length}`, 'info');
+ this.terminal.print(` Total accesses: ${this._totalNodeAccesses}`, 'info');
+ this.terminal.print(` Path: ${this._indexPath}`, 'info');
+ // v4.5.89: Show decay config
+ this.terminal.print(` Decay: ${this._decayDays}d threshold, ${this._decayMinUses} min uses, rate ${this._decayRate}/day`, 'text-muted');
+ }
+
+ search(query) {
+ if (!query) return this.terminal.print('Usage: memgraph-search <query>', 'error');
+ const results = this.findNodes(query);
+ if (results.length === 0) return this.terminal.print(`No nodes matching "${query}"`, 'info');
+
+ this.terminal.print(`🔍 Found ${results.length} nodes:`, 'header');
+ for (const n of results.slice(0, 20)) {
+ const edges = this._edges.filter(e => e.source === n.id || e.target === n.id);
+ this.terminal.print(` ${n.type}:${n.label} (${edges.length} edges, ${n.access_count}x)`, 'info');
+ for (const e of edges.slice(0, 3)) {
+ const other = e.source === n.id ? e.target : e.source;
+ const otherLabel = (this._nodes[other] || {}).label || other;
+ this.terminal.print(` └─ ${e.type} → ${otherLabel} (w:${e.weight.toFixed(1)})`, 'text-muted');
+ }
+ }
+ if (results.length > 20) this.terminal.print(` ... and ${results.length - 20} more`, 'info');
+ }
+
+ showStats() {
+ if (Object.keys(this._nodes).length === 0) {
+ return this.terminal.print('No graph data yet. Use commands normally to build it.', 'info');
+ }
+
+ // Most connected nodes
+ const connected = {};
+ for (const e of this._edges) {
+ connected[e.source] = (connected[e.source] || 0) + 1;
+ connected[e.target] = (connected[e.target] || 0) + 1;
+ }
+ const top = Object.entries(connected)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 5);
+
+ this.terminal.print('📊 Memory Graph Stats', 'header');
+ this.terminal.print(' Most connected:', 'info');
+ for (const [key, count] of top) {
+ const label = (this._nodes[key] || {}).label || key;
+ this.terminal.print(` ${label} — ${count} connections`, 'info');
+ }
+
+ // Strongest edges
+ const strong = [...this._edges].sort((a, b) => b.weight - a.weight).slice(0, 3);
+ if (strong.length > 0) {
+ this.terminal.print(' Strongest relationships:', 'info');
+ for (const e of strong) {
+ const sLabel = (this._nodes[e.source] || {}).label || e.source;
+ const tLabel = (this._nodes[e.target] || {}).label || e.target;
+ this.terminal.print(` ${sLabel} → ${tLabel} (${e.type}, w:${e.weight.toFixed(2)}, used ${e.use_count}x)`, 'info');
+ }
+ }
+
+ // Cluster density
+ const density = this._edges.length / Math.max(Object.keys(this._nodes).length, 1);
+ this.terminal.print(` Density: ${density.toFixed(2)} edges/node`, 'info');
+ }
+
+ exportGraph() {
+ const exportData = {
+ version: 1,
+ exported: new Date().toISOString(),
+ nodes: Object.values(this._nodes),
+ edges: this._edges,
+ stats: {
+ totalNodes: Object.keys(this._nodes).length,
+ totalEdges: this._edges.length,
+ totalAccesses: this._totalNodeAccesses
+ }
+ };
+ const json = JSON.stringify(exportData, null, 2);
+ // Save to VFS
+ try {
+ if (!this.terminal.fs.readDir(this._indexPath)) {
+ this.terminal.fs.mkdirRecursive(this._indexPath);
+ }
+ this.terminal.fs.writeFile(`${this._indexPath}export.json`, json);
+ this.terminal.print(`✅ Graph exported (${json.length} bytes) to ${this._indexPath}export.json`, 'success');
+ } catch (e) {
+ this.terminal.print(`❌ Export failed: ${e.message}`, 'error');
+ }
+ }
+
+ prune() {
+ const now = Date.now();
+ const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;
+
+ const before = this._edges.length;
+ this._edges = this._edges.filter(e => {
+ const age = now - new Date(e.last_used).getTime();
+ // Keep if: used recently (<30d) OR used 3+ times regardless of age
+ if (age < THIRTY_DAYS) return true;
+ if (e.use_count >= 3) return true;
+ // Or if weight is high
+ if (e.weight > 0.8) return true;
+ return false;
+ });
+ const pruned = before - this._edges.length;
+
+ // Also prune orphan nodes (no edges)
+ const connectedNodes = new Set();
+ for (const e of this._edges) {
+ connectedNodes.add(e.source);
+ connectedNodes.add(e.target);
+ }
+ const nodeBefore = Object.keys(this._nodes).length;
+ for (const key of Object.keys(this._nodes)) {
+ if (!connectedNodes.has(key)) {
+ // Keep session and high-access nodes
+ if (this._nodes[key].access_count >= 5) continue;
+ delete this._nodes[key];
+ }
+ }
+ const nodesPruned = nodeBefore - Object.keys(this._nodes).length;
+
+ this._saveNow();
+ this.terminal.print(`🧹 Pruned ${pruned} edges and ${nodesPruned} orphan nodes`, nodesPruned > 0 ? 'warning' : 'info');
+ }
+
+ // v4.5.89: Apply decay scores to nodes based on age and access patterns
+ applyDecay() {
+ const now = Date.now();
+ if (now - this._lastDecayRun < 3600000) return;
+ this._lastDecayRun = now;
+
+ // v4.5.89b fix: Compute decay rate from _decayDays (P0 from review)
+ const effectiveRate = 0.9 / this._decayDays;
+ let decayed = 0;
+ let removed = 0;
+
+ for (const key of Object.keys(this._nodes)) {
+ const node = this._nodes[key];
+ const ageDays = (now - new Date(node.updated || node.created).getTime()) / 86400000;
+ node.decay_score = Math.max(0, 1.0 - (ageDays * effectiveRate));
+ if (node.decay_score < 0.1 && node.access_count < this._decayMinUses) {
+ if (node.type === 'session') continue;
+ if (node.namespace && node.namespace.startsWith('agent:')) continue;
+ delete this._nodes[key];
+ removed++;
+ continue;
+ }
+ decayed++;
+ }
+
+ this._edges = this._edges.filter(e => {
+ const ageDays = (now - new Date(e.last_used).getTime()) / 86400000;
+ e.decay_score = Math.max(0, 1.0 - (ageDays * effectiveRate));
+ return !(e.decay_score < 0.1 && (e.use_count || 1) < this._decayMinUses);
+ });
+
+ if (removed > 0) {
+ this._debounceSave();
+ console.log('[MemoryGraph] Decay: ' + decayed + ' scored, ' + removed + ' auto-removed');
+ }
+
+ // v4.5.90: Check for stale skills
+ var staleSkills = this.getStaleSkills();
+ if (staleSkills.length > 0) {
+ this.terminal.print('🧠 Stale skills: ' + staleSkills.length + ' (' + staleSkills.map(function(s) { return s.name; }).join(', ') + ')', 'warning');
+ }
+ }
+
+ // v4.5.90: Record skill usage for decay tracking
+ recordSkillUse(name, success) {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ if (!index[name]) index[name] = { useCount: 0, successCount: 0 };
+ index[name].lastUsed = new Date().toISOString();
+ index[name].useCount = (index[name].useCount || 0) + 1;
+ if (success !== false) {
+ index[name].successCount = (index[name].successCount || 0) + 1;
+ }
+ fs.fsWriteJSON('/skills/index.json', index);
+ } catch (e) {
+ console.warn('[MemoryGraph] Skill use tracking error:', e.message);
+ }
+ }
+
+ // v4.5.90: Get stale skills (unused >_decayDays, useCount == 0, not immune)
+ getStaleSkills() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return [];
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const now = Date.now();
+ // v4.5.90b fix: Use configurable _decayDays (P1 from review)
+ const staleThreshold = this._decayDays * 24 * 60 * 60 * 1000;
+ var stale = [];
+ for (var name of Object.keys(index)) {
+ var stats = index[name] || {};
+ if (stats.immune) continue;
+ var useCount = stats.useCount || 0;
+ var lastUsed = stats.lastUsed ? new Date(stats.lastUsed).getTime() : 0;
+ if (useCount === 0 && (now - lastUsed > staleThreshold || lastUsed === 0)) {
+ var daysUnused = lastUsed > 0 ? Math.floor((now - lastUsed) / 86400000) : 999;
+ stale.push({ name: name, daysUnused: daysUnused });
+ }
+ }
+ return stale;
+ } catch (e) {
+ return [];
+ }
+ }
+
+ // v4.5.89: Configure decay policy
+ decayConfig(args) {
+ if (args.length < 2) {
+ this.terminal.print('Usage: memgraph-decay-config <days> <min_uses>', 'error');
+ this.terminal.print(' Current: ' + this._decayDays + 'd threshold, ' + this._decayMinUses + ' min uses', 'info');
+ this.terminal.print(' Decay rate: ' + this._decayRate + '/day (0.01 = 1% per day)', 'info');
+ return;
+ }
+ const days = parseInt(args[0]);
+ const minUses = parseInt(args[1]);
+ if (isNaN(days) || days < 1) return this.terminal.print('Days must be >= 1', 'error');
+ if (isNaN(minUses) || minUses < 0) return this.terminal.print('Min uses must be >= 0', 'error');
+ this._decayDays = days;
+ this._decayMinUses = minUses;
+ this._debounceSave();
+ this.terminal.print('✅ Decay config: ' + days + 'd threshold, ' + minUses + ' min uses', 'success');
+ }
+
+ learn(args) {
+ if (!args || args.length < 2) return this.terminal.print('Usage: memgraph-learn <entity> <fact>', 'error');
+ const entity = args[0];
+ const fact = args.slice(1).join(' ');
+ this.addNode('concept', entity, entity);
+ const factKey = this.addNode('fact', entity + '-' + Date.now(), fact);
+ const conceptKey = `concept:${entity}`;
+ this.addEdge(conceptKey, factKey, 'has_fact', 0.9);
+ this.terminal.print(`✅ Learned: ${entity} → ${fact}`, 'success');
+ // v4.5.89b fix: Index only the new fact, not all facts (P2 from review — O(n) → O(1))
+ const nvfs = this.terminal.moduleManager.modules['NeuralVFSModule'];
+ if (nvfs && typeof nvfs.indexGraphFacts === 'function') {
+ nvfs.indexSingleFact(factKey, entity, fact);
+ }
+ }
+
+ // v4.5.88: Show graph nodes for a specific session
+ showSession(dateStr) {
+ let sessionKey;
+ if (dateStr) {
+ const d = new Date(dateStr);
+ if (isNaN(d.getTime())) return this.terminal.print('Usage: memgraph-session [YYYY-MM-DD]', 'error');
+ sessionKey = `session-${Math.floor(d.getTime() / 86400000)}`;
+ } else {
+ sessionKey = `session-${Math.floor(Date.now() / 86400000)}`;
+ }
+
+ const sessionNodes = Object.values(this._nodes).filter(function(n) {
+ return n.source_session === sessionKey || n.type === 'session' && n.id.includes(sessionKey);
+ });
+
+ this.terminal.print('📅 Session Graph: ' + sessionKey, 'header');
+ if (sessionNodes.length === 0) {
+ this.terminal.print(' No nodes found for this session', 'info');
+ return;
+ }
+ this.terminal.print(' ' + sessionNodes.length + ' nodes:', 'info');
+ for (var n of sessionNodes.slice(0, 15)) {
+ var ns = n.namespace || 'global';
+ this.terminal.print(' ' + n.type + ':' + n.label + ' (' + ns + ', ' + n.access_count + 'x)', 'info');
+ }
+ var sessionEdges = this._edges.filter(function(e) {
+ return sessionNodes.some(function(n) { return n.id === e.source || n.id === e.target; });
+ });
+ this.terminal.print(' ' + sessionEdges.length + ' edges', 'info');
+ }
+
+ // v4.5.88: Show namespaced nodes for a specific agent
+ showAgent(agentId) {
+ if (!agentId) return this.terminal.print('Usage: memgraph-agent <agentId>', 'error');
+ var ns = 'agent:' + agentId;
+ var agentNodes = Object.values(this._nodes).filter(function(n) {
+ return n.namespace === ns;
+ });
+ var agentEdges = this._edges.filter(function(e) {
+ return (e.source_agents || []).includes(agentId);
+ });
+
+ this.terminal.print('🤖 Agent Memory: ' + agentId, 'header');
+ if (agentNodes.length === 0) {
+ this.terminal.print(' No namespaced nodes for this agent', 'info');
+ this.terminal.print(' (Agent may have used global namespace)', 'text-muted');
+ return;
+ }
+ this.terminal.print(' ' + agentNodes.length + ' nodes, ' + agentEdges.length + ' edges:', 'info');
+ for (var n of agentNodes.slice(0, 15)) {
+ this.terminal.print(' ' + n.type + ':' + n.label + ' (' + n.access_count + 'x)', 'info');
+ }
+ }
+
+ // ─── Persistence ───
+
+ _loadFromVFS() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+
+ // Try loading from export first
+ const exportData = fs.fsReadJSON(`${this._indexPath}export.json`);
+ if (exportData && exportData.nodes) {
+ for (const n of exportData.nodes) {
+ // v4.5.89 fix: Migrate old-format nodes (P1-1 from review)
+ if (!n.namespace) n.namespace = 'global';
+ if (!n.source_session) {
+ const createdMs = new Date(n.created).getTime();
+ n.source_session = isNaN(createdMs) ? null : `session-${Math.floor(createdMs / 86400000)}`;
+ }
+ this._nodes[n.id] = n;
+ }
+ this._edges = exportData.edges || [];
+ for (const e of this._edges) {
+ if (!e.source_agents) e.source_agents = [];
+ }
+ this._totalNodeAccesses = (exportData.stats?.totalAccesses) || 0;
+ return;
+ }
+
+ // Fallback: load nodes and edges separately
+ const nodes = fs.fsReadJSON(`${this._indexPath}nodes.json`);
+ if (nodes) {
+ this._nodes = nodes;
+ for (const n of Object.values(this._nodes)) {
+ if (!n.namespace) n.namespace = 'global';
+ if (!n.source_session) {
+ const createdMs = new Date(n.created).getTime();
+ n.source_session = isNaN(createdMs) ? null : `session-${Math.floor(createdMs / 86400000)}`;
+ }
+ }
+ }
+ const edges = fs.fsReadJSON(`${this._indexPath}edges.json`);
+ if (edges) {
+ this._edges = edges;
+ for (const e of this._edges) {
+ if (!e.source_agents) e.source_agents = [];
+ }
+ }
+ // v4.5.89b fix: Load persisted decay config (P1 from review)
+ const config = fs.fsReadJSON(`${this._indexPath}config.json`);
+ if (config) {
+ if (typeof config.decayDays === 'number') this._decayDays = config.decayDays;
+ if (typeof config.decayMinUses === 'number') this._decayMinUses = config.decayMinUses;
+ if (typeof config.lastDecayRun === 'number') this._lastDecayRun = config.lastDecayRun;
+ }
+ } catch (e) {
+ console.warn('[MemoryGraph] Load error (fresh start):', e.message);
+ }
+ }
+
+ _saveNow() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ if (!fs.readDir(this._indexPath)) {
+ fs.mkdirRecursive(this._indexPath);
+ }
+ fs.fsWriteJSON(`${this._indexPath}nodes.json`, this._nodes);
+ fs.fsWriteJSON(`${this._indexPath}edges.json`, this._edges);
+ // v4.5.89b fix: Persist decay config (P1 from review)
+ fs.fsWriteJSON(`${this._indexPath}config.json`, {
+ decayDays: this._decayDays,
+ decayMinUses: this._decayMinUses,
+ lastDecayRun: this._lastDecayRun
+ });
+ } catch (e) {
+ console.error('[MemoryGraph] Save error:', e.message);
+ }
+ }
+
+ _debounceSave() {
+ if (this._debounceTimer) clearTimeout(this._debounceTimer);
+ this._debounceTimer = setTimeout(() => this._saveNow(), 2000);
+ }
+}
+
+// Required for module system
+if (typeof window !== 'undefined') {
+ window.MemoryGraphModule = MemoryGraphModule;
+} \ No newline at end of file
diff --git a/.bkp-fix-20261023_143429/module-world-model.js b/.bkp-fix-20261023_143429/module-world-model.js
new file mode 100644
index 0000000..cbfd1fb
--- /dev/null
+++ b/.bkp-fix-20261023_143429/module-world-model.js
@@ -0,0 +1,600 @@
+// module-world-model.js — Global World Model: system self-awareness synthesis engine
+// Aggregates from memory graph, neural VFS, skills index, telemetry
+// Commands: world-model, world-model-summary, world-model-ask (LLM), world-model-refresh, world-model-config
+class WorldModelModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._model = null;
+ this._lastRefresh = null;
+ this._REFRESH_INTERVAL = 300000; // 5 minutes (fallback)
+ this._MAX_PROJECTS = 10;
+ this._refreshTimer = null;
+ // v4.5.89: Event-driven sync state
+ this._syncDebounce = null;
+ this._syncing = false;
+ this._lastEventSync = 0;
+ this._EVENT_DEBOUNCE_MS = 500;
+ }
+
+ getHelpInfo() {
+ return {
+ 'world-model': 'world-model — Show full world model as structured output',
+ 'world-model-summary': 'world-model-summary — One-line system self-awareness statement',
+'world-model-ask': 'world-model-ask <question> — Ask the LLM about this system (grounded in the world model; --facts for the instant keyword lookup)',
+ 'world-model-refresh': 'world-model-refresh — Force re-synthesis from all subsystems',
+ 'world-model-config': 'world-model-config <key> <value> — Tune: refresh-interval, max-projects',
+ };
+ }
+
+ registerCommands() {
+ this.commands['world-model'] = () => this.showModel();
+ this.commands['world-model-summary'] = () => this.showSummary();
+ this.commands['world-model-ask'] = (args) => this.ask(args.join(' '));
+ this.commands['world-model-refresh'] = () => this.refresh();
+ this.commands['world-model-config'] = (args) => this.setConfig(args);
+ }
+
+ 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);
+ this.terminal.print('🌐 World Model loaded — system self-awareness active', 'info');
+ }
+
+ // v4.5.89: Event-driven sync — debounced, recursion-guarded
+ _scheduleSync() {
+ if (this._syncing) return; // Prevent recursion during active synthesis
+ if (this._syncDebounce) clearTimeout(this._syncDebounce);
+ const now = Date.now();
+ // Skip if we synced < 2s ago (rate limit beyond debounce)
+ if (now - this._lastEventSync < 2000) return;
+ this._syncDebounce = setTimeout(() => {
+ this._syncDebounce = null;
+ this._syncing = true;
+ try {
+ this._synthesize();
+ this._lastEventSync = Date.now();
+ } finally {
+ this._syncing = false;
+ }
+ }, this._EVENT_DEBOUNCE_MS);
+ }
+
+ // ─── Synthesis Engine ───
+
+ _synthesize() {
+ try {
+ 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: liveVersion,
+ synthesized: new Date().toISOString(),
+ system: this._gatherSystemInfo(fs, mInfo.loaded, liveVersion),
+ modules: mInfo,
+ files: fileInfo,
+ skills: skillInfo,
+ memory: this._gatherMemoryInfo(),
+ agents: agentInfo,
+ telemetry: this._gatherTelemetry(fs),
+ projects: this._gatherProjects(fs),
+ recent: this._gatherRecent(fs)
+ };
+
+ this._model = model;
+ this._lastRefresh = model.synthesized;
+ this._saveToVFS(fs);
+
+ // Log synthesis summary
+ const mCount = model.modules?.loaded || '?';
+ const sCount = model.skills?.count || '?';
+ const fCount = model.files?.total || '?';
+ console.log(`[WorldModel] Synthesis complete: ${mCount} modules, ${sCount} skills, ${fCount} files`);
+ } catch (e) {
+ console.error('[WorldModel] Synthesis error:', e.message);
+ }
+ }
+
+ _deriveVersion(mInfo) {
+ // Single source of truth: the constant set at bootstrap
+ if (window.NEWOS4_VERSION) return window.NEWOS4_VERSION;
+ // Fallback: try to extract from script tag ?v= query param or LATEST_VER constant
+ try {
+ const scripts = document.querySelectorAll('script[src*="module-loader"]');
+ for (const s of scripts) {
+ const match = s.src.match(/[?&]v=([^&]+)/);
+ if (match) return match[1];
+ }
+ // Try to find LATEST_VER in any inline script
+ const allScripts = document.querySelectorAll('script:not([src])');
+ for (const s of allScripts) {
+ const match = s.textContent.match(/LATEST_VER\s*=\s*['"]([^'"]+)['"]/);
+ if (match) return match[1];
+ }
+ } catch (e) {
+ console.warn('[WorldModel] Version extraction error:', e.message);
+ }
+ return 'v?';
+ }
+
+ _getTotalModules() {
+ try {
+ const mm = this.terminal?.moduleManager;
+ if (!mm) return 0;
+ // Use max of registry vs loaded so the ratio is never >1 (e.g. 43/7 → 43/43)
+ const loadedCount = Object.keys(mm.modules || {}).length;
+ const registryCount = (mm.moduleRegistry || []).length;
+ return Math.max(loadedCount, registryCount);
+ } catch (e) {
+ console.warn('[WorldModel] Total modules gather error:', e.message);
+ return Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0;
+ }
+ }
+
+ _gatherSystemInfo(fs, moduleCount, version) {
+ // Get real health data from ServerMonitorModule's _latencyLog and DebugSupervisorModule
+ const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
+ const ds = this.terminal?.moduleManager?.modules?.['DebugSupervisorModule'];
+ const errorCount = ds ? Object.keys(ds._errorDB?.errors || {}).length : 0;
+ const latencyErrors = sm?._latencyLog ? Object.values(sm._latencyLog).filter(e => e.status === 'error').length : 0;
+ const totalErrors = errorCount + latencyErrors;
+ return {
+ 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
+ ? `${(performance.memory.usedJSHeapSize / 1048576).toFixed(1)}MB`
+ : 'N/A',
+ health: totalErrors > 0 ? `⚠️ ${totalErrors} errors` : 'OK',
+ lastHealthCheck: new Date().toISOString()
+ };
+ }
+
+ _gatherModuleInfo() {
+ const mm = this.terminal?.moduleManager;
+ if (!mm) return { loaded: 0, modified: [] };
+ const modules = Object.keys(mm.modules || {}).map(name => {
+ const mod = mm.modules[name];
+ const clsName = mod?.constructor?.name || name;
+ return { name: clsName, hasCommands: !!mod?.commands };
+ });
+ // Check for local modifications
+ const modsDir = this.terminal.fs?.readDir('/_mods/');
+ const modified = modsDir
+ ? modsDir.filter(e => e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js'))
+ .map(e => e.name.replace('.js', ''))
+ : [];
+ return {
+ loaded: modules.length,
+ total: this._getTotalModules(),
+ modules,
+ modified,
+ modifiedCount: modified.length
+ };
+ }
+
+ _gatherFileInfo(fs) {
+ if (!fs) return { total: 0, size: '0KB' };
+ let fileCount = 0, dirCount = 0;
+ const walk = (path) => {
+ const entries = fs.ls(path);
+ if (!entries) return;
+ for (const e of entries) {
+ if (e.type === 'file') fileCount++;
+ else { dirCount++; walk(path === '/' ? '/' + e.name : path + '/' + e.name); }
+ }
+ };
+ walk('/');
+ return {
+ total: fileCount,
+ directories: dirCount,
+ size: `${(JSON.stringify(fs.root).length / 1024).toFixed(1)}KB`
+ };
+ }
+
+ _gatherSkillInfo(fs) {
+ try {
+ const idx = fs.fsReadJSON('/skills/index.json');
+ const count = idx ? Object.keys(idx).length : 0;
+ return { count, skills: idx ? Object.keys(idx) : [] };
+ } catch (e) {
+ console.warn('[WorldModel] Skill info gather error:', e.message);
+ return { count: 0, skills: [] };
+ }
+ }
+
+ _gatherMemoryInfo() {
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ if (mg && mg._nodes) {
+ const nodes = Object.keys(mg._nodes).length;
+ const edges = (mg._edges || []).length;
+ return { nodes, edges, active: true, fallback: false };
+ }
+ // Fallback: count memory files
+ try {
+ const memIndex = this.terminal.fs?.fsReadJSON('/memory/index.json');
+ return { nodes: memIndex ? Object.keys(memIndex).length : 0, edges: 0, active: false, fallback: true };
+ } catch (e) {
+ console.warn('[WorldModel] Memory info gather error:', e.message);
+ return { nodes: 0, edges: 0, active: false, fallback: true };
+ }
+ }
+
+ _gatherAgentInfo() {
+ const orch = this.terminal?.moduleManager?.modules?.['OrchestratorModule'];
+ if (!orch || !orch._agents) return { agents: -1, active: -1, error: 'Orchestrator not loaded' };
+ const agents = Object.keys(orch._agents).length;
+ const active = Object.values(orch._agents).filter(a => a.status === 'busy').length;
+ return { agents, active, totalLeases: Object.keys(orch._a2aMessages || {}).length };
+ }
+
+ _gatherTelemetry(fs) {
+ try {
+ // Get real latency data from ServerMonitorModule and errors from DebugSupervisorModule
+ const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
+ const ds = this.terminal?.moduleManager?.modules?.['DebugSupervisorModule'];
+ const latencyDataPoints = sm?._latencyLog ? Object.keys(sm._latencyLog).length : 0;
+ const recentErrors = ds ? Object.keys(ds._errorDB?.errors || {}).length : 0;
+ return {
+ toolLatencyDataPoints: latencyDataPoints,
+ recentErrors: recentErrors,
+ };
+ } catch (e) {
+ console.warn('[WorldModel] Telemetry gather error:', e.message);
+ return { toolLatencyDataPoints: 0, recentErrors: 0 };
+ }
+ }
+
+ _gatherProjects(fs) {
+ try {
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ if (mg && mg._nodes) {
+ // Find task clusters as "projects"
+ const tasks = Object.values(mg._nodes).filter(n => n.type === 'task');
+ const recentFiles = Object.values(mg._nodes)
+ .filter(n => n.type === 'file')
+ .sort((a, b) => (b.access_count || 0) - (a.access_count || 0))
+ .slice(0, this._MAX_PROJECTS);
+ return {
+ totalTasks: tasks.length,
+ activeProjects: recentFiles.slice(0, 5).map(f => ({
+ name: f.label,
+ accesses: f.access_count || 0,
+ lastModified: f.updated
+ }))
+ };
+ }
+ return { totalTasks: 0, activeProjects: [] };
+ } catch (e) {
+ console.warn('[WorldModel] Projects gather error:', e.message);
+ return { totalTasks: 0, activeProjects: [] };
+ }
+ }
+
+ _gatherRecent(fs) {
+ const recent = [];
+ try {
+ // Check modified modules
+ const modsDir = fs.readDir('/_mods/');
+ if (modsDir) {
+ for (const e of modsDir) {
+ if (e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js') && !e.name.endsWith('.meta.json')) {
+ recent.push(`Modified module: ${e.name.replace('.js', '')}`);
+ }
+ }
+ }
+ // Check skills activity
+ const skills = fs.ls('/skills/');
+ if (skills && skills.length > 0) {
+ recent.push(`Skills directory: ${skills.filter(s => s.type === 'file').length} files`);
+ }
+ // Check memory directory for recent changes
+ const memDir = fs.readDir('/memory/');
+ if (memDir && memDir.length > 0) {
+ const memFiles = memDir.filter(s => s.type === 'file');
+ if (memFiles.length > 0) {
+ recent.push(`Memory: ${memFiles.length} files in /memory/`);
+ }
+ }
+ // Check system config changes
+ const sysDir = fs.readDir('/system/');
+ if (sysDir && sysDir.length > 0) {
+ const configFiles = sysDir.filter(s => s.type === 'file' && s.name.endsWith('.json'));
+ if (configFiles.length > 0) {
+ recent.push(`System config: ${configFiles.length} config files`);
+ }
+ }
+ } catch (e) {
+ console.warn('[WorldModel] Recent gather error:', e.message);
+ }
+ return recent;
+ }
+
+ // ─── Commands ───
+
+ showModel() {
+ if (!this._model) return this.terminal.print('No model yet — run world-model-refresh', 'warning');
+ const m = this._model;
+
+ this.terminal.print('🌐 World Model', 'header');
+ this.terminal.print(` Version: ${m.version} (last synthsized: ${new Date(m.synthesized).toLocaleTimeString()})`, 'info');
+
+ this.terminal.print(` ⚙️ System — ${m.system.modules} modules loaded, ${m.system.vfsSize} VFS, ${m.system.memory}`, 'info');
+ this.terminal.print(` 🧩 Modules — ${m.modules.loaded} loaded (${m.modules.modifiedCount} modified)`, 'info');
+ this.terminal.print(` 📁 Files — ${m.files.total} files, ${m.files.directories} dirs, ${m.files.size}`, 'info');
+ this.terminal.print(` 📚 Skills — ${m.skills.count} installed`, 'info');
+ this.terminal.print(` 🧠 Memory Graph — ${m.memory.nodes} nodes, ${m.memory.edges} edges${m.memory.active ? ' ✅' : ''}`, 'info');
+ this.terminal.print(` 🤖 Agents — ${m.agents.agents} registered (${m.agents.active} active)`, 'info');
+ this.terminal.print(` 📊 Telemetry — ${m.telemetry.toolLatencyDataPoints} tool latency points, ${m.telemetry.recentErrors} recent errors`, 'info');
+
+ if (m.projects.activeProjects.length > 0) {
+ this.terminal.print(' Active context:', 'header');
+ for (const p of m.projects.activeProjects.slice(0, 3)) {
+ this.terminal.print(` 📌 ${p.name} (${p.accesses}x accesses)`, 'info');
+ }
+ }
+
+ if (m.recent.length > 0) {
+ this.terminal.print(' Recent changes:', 'header');
+ for (const r of m.recent.slice(0, 3)) {
+ this.terminal.print(` ${r}`, 'info');
+ }
+ }
+ }
+
+ showSummary() {
+ if (!this._model) return this.terminal.print('No model yet', 'warning');
+ const m = this._model;
+ const summary = `newOS4 ${m.version} · ${m.modules.loaded}/${m.modules.total} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents · ${m.memory.nodes} graph nodes · ${m.telemetry.recentErrors > 0 ? `⚠️ ${m.telemetry.recentErrors} errors` : '✅ OK'}`;
+ this.terminal.print(summary, 'info');
+ return summary;
+ }
+
+ async ask(question) {
+ if (!question) return this.terminal.print('Usage: world-model-ask <question> (--facts = fast keyword lookup)', 'error');
+ const factsOnly = /(^|\s)--facts\b/.test(question);
+ if (factsOnly) question = question.replace(/--facts\b/, '').trim();
+ if (!this._model) return this.terminal.print('No model — run world-model-refresh first', 'warning');
+
+ this.terminal.print(`❓ ${question}`, 'info');
+
+ if (!factsOnly) {
+ const om = this.terminal.ollamaManager;
+ try {
+ if (om && !om.connected) { try { await om.connect(); } catch (e) {} }
+ if (om && om.connected) {
+ const answer = await om.chat(this._buildAskPrompt(question));
+ if (answer && String(answer).trim()) {
+ this.terminal.print(`💡 ${String(answer).trim()}`, 'success');
+ return;
+ }
+ }
+ } catch (e) { /* fall through to the facts path */ }
+ this.terminal.print('⚠️ LLM unavailable — showing the factual lookup instead', 'warning');
+ }
+
+ this.terminal.print(`💡 ${this._answerQuery(question)}`, 'success');
+ }
+
+ // Shared query routing — used by both ask() (terminal) and world_model_query (AI tool)
+ _answerQuery(question) {
+ if (!this._model) return 'World model not synthesized yet — run world-model-refresh';
+ const q = (question || '').toLowerCase();
+ const m = this._model;
+
+ if (q.includes('module') || q.includes('loaded')) {
+ return `Loaded ${m.modules.loaded} modules (${m.modules.modifiedCount} modified: ${m.modules.modified.join(', ') || 'none'})`;
+ } else if (q.includes('skill')) {
+ return `Installed ${m.skills.count} skills: ${m.skills.skills.join(', ') || 'none'}`;
+ } else if (q.includes('file') || q.includes('vfs')) {
+ return `${m.files.total} files in ${m.files.directories} directories, total ${m.files.size}`;
+ } else if (q.includes('agent') || q.includes('orchestrat')) {
+ return `${m.agents.agents} agents (${m.agents.active} active)`;
+ } else if (q.includes('health') || q.includes('status')) {
+ return `Health: ${m.system.health} · Memory: ${m.system.memory} · ${m.telemetry.recentErrors > 0 ? `${m.telemetry.recentErrors} recent errors` : 'No errors'}`;
+ } else if (q.includes('memory') || q.includes('graph')) {
+ return `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`;
+ } else if (q.includes('change') || q.includes('yesterday') || q.includes('recent')) {
+ const diff = this._diffSincePrev();
+ const recent = m.recent || [];
+ const parts = [];
+ if (diff.length) parts.push('Since the last snapshot: ' + diff.join('; '));
+ if (recent.length) parts.push('Current: ' + recent.join('; '));
+ return parts.length ? parts.join(' · ') : 'No previous snapshot yet — run world-model-refresh twice';
+ } else if (q.includes('project') || q.includes('work')) {
+ const projects = m.projects.activeProjects;
+ return projects.length > 0
+ ? `Active projects: ${projects.map(p => `${p.name} (${p.accesses}x)`).join(', ')}`
+ : 'No active projects detected';
+ } else if (q.includes('telemetry') || q.includes('latency')) {
+ return `${m.telemetry.toolLatencyDataPoints} tool latency data points tracked, ${m.telemetry.recentErrors} recent errors`;
+ } else if (q.includes('summary') || q.includes('overview') || q.includes('all')) {
+ return `newOS4 ${m.version} · ${m.modules.loaded}/${m.modules.total} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents · ${m.memory.nodes} graph nodes · ${m.telemetry.recentErrors > 0 ? `⚠️ ${m.telemetry.recentErrors} errors` : '✅ OK'}`;
+ } else {
+ return `Unknown query. Try: modules, skills, files, agents, health, memory, changes, projects, telemetry, or summary.`;
+ }
+ }
+
+ // 2026-10-21: ask() used to be a keyword table only, so any real question got
+ // "Unknown query". Whitelist the fields we are willing to send to a model
+ // (never the raw model object: it is persisted alongside other VFS data).
+ _modelDigest(maxChars) {
+ const m = this._model;
+ if (!m) return '';
+ const cap = maxChars || 4000;
+ const pick = {
+ version: m.version,
+ synthesized: m.synthesized,
+ system: m.system,
+ modules: m.modules,
+ files: m.files,
+ skills: { count: m.skills && m.skills.count, skills: ((m.skills && m.skills.skills) || []).slice(0, 40) },
+ memory: m.memory,
+ agents: m.agents,
+ telemetry: m.telemetry,
+ projects: m.projects,
+ recent: (m.recent || []).slice(0, 20),
+ // 2026-10-21: the model only ever exposed a COUNT ("9 recent errors") with no detail,
+ // so neither the user nor the LLM could say what failed. Carry the last few.
+ recentErrorSample: this._errorSample ? this._errorSample() : [],
+ changesSinceLastSnapshot: this._diffSincePrev ? this._diffSincePrev() : []
+ };
+ let json = JSON.stringify(pick);
+ if (json.length > cap) json = json.slice(0, cap) + '…(digest truncated)';
+ return json;
+ }
+
+ // Last few tool/agent errors with their tool name, for the digest (bounded).
+ _errorSample() {
+ try {
+ const ds = this.terminal.moduleManager.modules['DebugSupervisorModule'];
+ const db = ds && ds._errorDB && ds._errorDB.errors;
+ if (!db) return [];
+ return Object.keys(db).slice(-5).map(k => {
+ const e = db[k] || {};
+ return String(e.tool || e.module || k).slice(0, 40) + ': ' + String(e.error || e.message || '').slice(0, 80);
+ });
+ } catch (e) { return []; }
+ }
+
+ _buildAskPrompt(question) {
+ return [
+ 'You are the newOS4 world model interface. Answer ONLY from the JSON snapshot below.',
+ 'If the snapshot does not contain the answer, say what is missing and name the command that',
+ 'would produce it (world-model-refresh, world-model, modules, test). Never invent numbers.',
+ 'Answer in 1-3 short sentences, plain text, no markdown headers.',
+ '',
+ 'SNAPSHOT: ' + this._modelDigest(),
+ '',
+ 'QUESTION: ' + String(question || '').trim()
+ ].join('\n');
+ }
+
+ _diffSincePrev() {
+ try {
+ const fs = this.terminal.fs;
+ const prev = fs && fs.fsReadJSON('/system/world-model.prev.json');
+ const cur = this._model;
+ if (!prev || !cur) return [];
+ const out = [];
+ if (prev.version && cur.version && prev.version !== cur.version) out.push(`version ${prev.version} → ${cur.version}`);
+ const fields = [['modules', 'loaded'], ['skills', 'count'], ['files', 'total'], ['agents', 'agents'], ['memory', 'nodes']];
+ fields.forEach(function (f) {
+ const a = prev[f[0]] && prev[f[0]][f[1]];
+ const b = cur[f[0]] && cur[f[0]][f[1]];
+ if (typeof a === 'number' && typeof b === 'number' && a !== b) out.push(`${f[0]} ${a} → ${b}`);
+ });
+ if (prev.synthesized) out.push('previous snapshot: ' + prev.synthesized);
+ return out;
+ } catch (e) { return []; }
+ }
+
+ refresh() {
+ this._synthesize();
+ this.terminal.print(`✅ World Model refreshed at ${new Date().toLocaleTimeString()}`, 'success');
+ }
+
+ setConfig(args) {
+ if (args.length < 2) return this.terminal.print('Usage: world-model-config <key> <value>', 'error');
+ const key = args[0];
+ const val = args[1];
+
+ const configMap = {
+ 'refresh-interval': () => {
+ const n = parseInt(val) * 1000;
+ if (isNaN(n) || n < 10000) return 'Must be >= 10 seconds';
+ this._REFRESH_INTERVAL = n;
+ if (this._refreshTimer) clearInterval(this._refreshTimer);
+ this._refreshTimer = setInterval(() => this._synthesize(), n);
+ return `refresh-interval set to ${val}s`;
+ },
+ 'max-projects': () => {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 1) return 'Must be >= 1';
+ this._MAX_PROJECTS = n;
+ return `max-projects set to ${n}`;
+ },
+ };
+
+ if (!configMap[key]) return this.terminal.print(`Unknown: ${key}. Valid: ${Object.keys(configMap).join(', ')}`, 'error');
+ const msg = configMap[key]();
+ this.terminal.print(`✅ ${msg}`, 'success');
+ }
+
+ // ─── Context Injection ───
+
+ getContextForPrompt() {
+ if (!this._model) return '';
+ const m = this._model;
+ const agentStr = m.agents.error
+ ? 'agents: N/A'
+ : `${m.agents.agents} agents`;
+ return `[World Model Context]\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
+ }
+
+ // ─── Persistence ───
+
+ _loadFromVFS() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ const data = fs.fsReadJSON('/system/world-model.json');
+ if (data) {
+ this._model = data;
+ this._lastRefresh = data.synthesized;
+ }
+ } catch (e) {
+ console.warn('[WorldModel] Load error (fresh start):', e.message);
+ }
+ }
+
+ // v4.5.89: Atomic write — temp file + rename to prevent corruption
+ _saveToVFS(fs) {
+ try {
+ if (!fs.readDir('/system')) fs.mkdir('system');
+ // 2026-10-21: keep one previous model so "what changed" can be a real diff.
+ const prevModel = fs.fsReadJSON('/system/world-model.json');
+ if (prevModel) { try { fs.fsWriteJSON('/system/world-model.prev.json', prevModel); } catch (_) {} }
+ // v4.5.89b fix: Clean stale temp from previous crash (P1 from review)
+ try { fs.deleteFile('/system/world-model.tmp.json'); } catch (_) {}
+ fs.fsWriteJSON('/system/world-model.tmp.json', this._model);
+ const tmp = fs.fsReadJSON('/system/world-model.tmp.json');
+ if (tmp) {
+ fs.fsWriteJSON('/system/world-model.json', tmp);
+ fs.deleteFile('/system/world-model.tmp.json');
+ } else {
+ console.error('[WorldModel] Save failed: temp file corrupt after write');
+ }
+ } catch (e) {
+ console.error('[WorldModel] Save error:', e.message);
+ }
+ }
+}
+
+// Required for module system
+if (typeof window !== 'undefined') {
+ window.WorldModelModule = WorldModelModule;
+} \ No newline at end of file
diff --git a/.bkp-fix-20261023_143429/report.php b/.bkp-fix-20261023_143429/report.php
new file mode 100644
index 0000000..cefcae6
--- /dev/null
+++ b/.bkp-fix-20261023_143429/report.php
@@ -0,0 +1,112 @@
+<?php
+// api/debug/report.php — structured bug-report intake (Part A)
+// POST { title, body?, diagnostics{...} } -> { success, id, duplicate, url }
+require_once __DIR__ . '/config.php';
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ error_response('Method not allowed', 405);
+}
+
+define('REPORTS_DIR', __DIR__ . '/../../data/reports');
+define('RATE_FILE', DEBUG_DATA_DIR . '/report_rate.json');
+define('MAX_REPORT_KB', 512);
+define('RATE_MAX', 5); // reports per IP …
+define('RATE_WINDOW', 600); // … per 10 minutes
+
+$raw = file_get_contents('php://input');
+if (!$raw) error_response('Empty request body');
+if (strlen($raw) > MAX_REPORT_KB * 1024) {
+ error_response('Payload too large (max ' . MAX_REPORT_KB . 'KB)', 413);
+}
+
+$payload = json_decode($raw, true);
+if (!is_array($payload)) error_response('Invalid JSON');
+
+$title = trim((string)($payload['title'] ?? ''));
+if ($title === '') error_response('title is required');
+if (mb_strlen($title) > 160) $title = mb_substr($title, 0, 160);
+$body = (string)($payload['body'] ?? '');
+if (mb_strlen($body) > 4000) $body = mb_substr($body, 0, 4000);
+$diag = (isset($payload['diagnostics']) && is_array($payload['diagnostics'])) ? $payload['diagnostics'] : [];
+
+// Storage guard: data/ is not writable by php-fpm, so never pretend we wrote a report.
+if (!is_dir(REPORTS_DIR)) @mkdir(REPORTS_DIR, 0775, true);
+if (!is_dir(REPORTS_DIR) || !is_writable(REPORTS_DIR)) {
+ error_response('Report storage unavailable', 503);
+}
+
+// Per-IP rate limit
+$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
+$now = time();
+$rate = load_json(RATE_FILE);
+$hits = array_values(array_filter($rate[$ip] ?? [], function ($t) use ($now) {
+ return $t > $now - RATE_WINDOW;
+}));
+if (count($hits) >= RATE_MAX) {
+ header('Retry-After: ' . RATE_WINDOW);
+ error_response('Rate limit exceeded (max ' . RATE_MAX . ' reports per 10 minutes)', 429);
+}
+$hits[] = $now;
+$rate[$ip] = $hits;
+foreach ($rate as $k => $v) {
+ $v = array_values(array_filter($v, function ($t) use ($now) { return $t > $now - 86400; }));
+ if ($v) $rate[$k] = $v; else unset($rate[$k]);
+}
+save_json(RATE_FILE, $rate);
+
+// Dedupe: same title + error set + version within 24h collapses onto the existing report.
+$fingerprint = sha1($title . '|' . json_encode($diag['errors'] ?? []) . '|' . (string)($diag['version'] ?? ''));
+$existing = glob(REPORTS_DIR . '/RPT-*.json') ?: [];
+foreach (array_slice(array_reverse($existing), 0, 100) as $f) {
+ $d = json_decode(@file_get_contents($f), true);
+ if (!is_array($d)) continue;
+ if (($d['fingerprint'] ?? '') === $fingerprint && strtotime($d['created_at'] ?? '') > $now - 86400) {
+ // Don't discard user content on a duplicate: keep it as an appended note.
+ $d['duplicate_count'] = (int)($d['duplicate_count'] ?? 0) + 1;
+ if ($body !== '' && ($d['body'] ?? '') !== $body) {
+ $d['body'] = trim((string)($d['body'] ?? '') . "\n---\n[duplicate " . gmdate('c') . "] " . $body);
+ $json = json_encode($d, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ if ($json !== false) { @file_put_contents($f, $json); @chmod($f, 0664); }
+ }
+ json_response([
+ 'success' => true,
+ 'id' => $d['id'],
+ 'duplicate' => true,
+ 'url' => '/spaces/newos4/downloads/bugs/BUG-REPORT.html',
+ ]);
+ }
+}
+
+$id = 'RPT-' . gmdate('Ymd-His') . '-' . substr(bin2hex(random_bytes(4)), 0, 8);
+$record = [
+ 'id' => $id,
+ 'created_at' => gmdate('c'),
+ 'fingerprint' => $fingerprint,
+ 'title' => $title,
+ 'body' => $body,
+ 'diagnostics' => $diag,
+ 'meta' => [
+ 'ip' => $ip,
+ 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
+ ],
+];
+
+$file = REPORTS_DIR . '/' . $id . '.json';
+$json = json_encode($record, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+if ($json === false || file_put_contents($file, $json) === false) {
+ error_response('Failed to persist report', 500);
+}
+@chmod($file, 0664);
+
+// Regenerate the public page immediately so the URL we print is never stale.
+$generator = realpath(__DIR__ . '/../../scripts/gen-bug-report.py');
+if ($generator) {
+ @exec('/usr/bin/python3 ' . escapeshellarg($generator) . ' 2>&1', $genOut, $genRc);
+}
+
+json_response([
+ 'success' => true,
+ 'id' => $id,
+ 'duplicate' => false,
+ 'url' => '/spaces/newos4/downloads/bugs/BUG-REPORT.html',
+], 201);
diff --git a/.bkp-fix-20261023_143429/skill-pack-core.js b/.bkp-fix-20261023_143429/skill-pack-core.js
new file mode 100644
index 0000000..f204e70
--- /dev/null
+++ b/.bkp-fix-20261023_143429/skill-pack-core.js
@@ -0,0 +1,1150 @@
+class SkillPackCoreModule {
+ constructor() {
+ this.commands = {};
+ this.commandsMeta = {
+ 'skill-list': { argType: 'enum', enumFn: '_getSkillCategories' }, 'skill-view': { argType: 'file' },
+ 'skill-create': { argType: 'dir' }, 'skill-patch': { argType: 'file' },
+ 'skill-delete': { argType: 'file' }, 'skill-export': { argType: 'file' },
+ 'skill-sync': { argType: 'file' },
+ };
+ this.terminal = null;
+ this._seeded = false;
+ this._skills = this._buildSkills();
+ }
+
+ requirements() {
+ try {
+ const fs = this.terminal && this.terminal.fs;
+ if (fs && fs.readFile('/skills/_seeded')) {
+ return { met: true, missing: [] };
+ }
+ return { met: false, missing: ['Skills directory not initialized. Will be created on first use.'] };
+ } catch (e) {
+ return { met: true, missing: [] };
+ }
+ }
+
+ getHelpInfo() {
+ return {
+ 'skill-pack-core': 'Seed and manage built-in core skills (navigation, memory, search, modules, backup, agents, webdav, security, etc.)',
+ 'skill-list': 'skill-list [category] — List all skills, optionally filtered by category',
+ 'skill-view': 'skill-view <name> — View a skill\'s full content',
+ 'skill-create': 'skill-create <name> <desc> "<steps>" [tags] — Create a new skill',
+ 'skill-patch': 'skill-patch <name> "<content>" — Replace a skill\'s full content',
+ 'skill-delete': 'skill-delete <name> — Delete a skill',
+ 'skill-export': 'skill-export <name> — Export a skill as JSON',
+ 'skill-import': 'skill-import <json> — Import a skill from JSON string',
+ 'skill-sync': 'skill-sync — Rebuild skills index from /skills/ directory',
+ 'learn-skill': 'learn-skill <topic> — Reflect on topic, draft a skill from experience',
+ 'skill-validate': 'skill-validate <name> — Validate skill structure and get confidence score',
+ 'skill-audit': 'skill-audit — Audit skills for stale, duplicate, and broken references',
+ 'skill-restore': 'skill-restore <name> — Restore an archived skill',
+ 'skill-webdav-sync': 'skill-webdav-sync — Push all skills to WebDAV as JSON files',
+ };
+ }
+
+ _buildSkills() {
+ const s = (name, desc, tags, cat, steps) => ({
+ name, description: desc, tags, category: cat,
+ content: '---\nname: ' + name + '\ndescription: ' + desc + '\ntags: [' + tags.join(', ') + ']\ncategory: ' + cat + '\n---\n## Steps\n' + steps
+ });
+
+ const sk = (name, desc, tags, cat, content) => ({
+ name, description: desc, tags, category: cat,
+ content: '---\nname: ' + name + '\ndescription: ' + desc + '\ntags: [' + tags.join(', ') + ']\ncategory: ' + cat + '\n---\n' + content
+ });
+
+ return [
+ s('navigating-the-terminal', 'How to explore the WebX11 terminal, find commands, check status, and navigate the filesystem', ['terminal', 'navigation', 'help', 'filesystem'], 'system',
+`1. **Get an overview of available commands:**
+ - Type "help" for a full command list sorted by category
+ - Type "status" to see filesystem state, loaded modules, Ollama status
+ - For a specific command, use help <command>
+ - Type "commands" for a compact list with module source info
+ - Type "check" for system health including module requirements
+
+2. **Explore the filesystem:**
+ - Use ls, ll, tree to browse directories
+ - Use cd <dir> to navigate, pwd to show current path
+ - Use cat or edit to view file contents
+ - Use find or grep to search files
+ - Use du to see disk usage
+
+3. **Understand system state:**
+ - mem — memory usage (auto-shown in header on boot)
+ - status — network, files, modules, Ollama
+ - modules — module manager GUI
+ - health-check — VFS size, modules, logs, trash, skills, tool failures
+ - checkpoint-list — VFS checkpoint snapshots
+
+4. **Agent / Orchestration state:**
+ - agent-list — registered agents (type, capabilities, status)
+ - orchestrate — list active orchestrated agents
+ - lease-list — show active leases with TTL
+
+5. **When user asks a vague question:**
+ - First run status and help to understand the current state
+ - Then ask clarifying questions based on what you found`),
+
+ s('memory-and-profile', 'How to use the user profile system, session memory, and pattern tracking for persistent context', ['memory', 'profile', 'session', 'persistence'], 'system',
+`1. **User Profile (persists across sessions):**
+ - user_get(key) — read any profile field (e.g. "preferredModel", "fontSize")
+ - user_set(key, value) — write to profile, auto-persists to localStorage
+ - user_list — show all stored profile keys
+ - user_forget(key) — delete a profile key
+ - Dot notation supported: user_set knownFacts.name "Alex"
+
+2. **Session Memory (VFS-based daily logs):**
+ - session_save(summary) — save a summary of what was accomplished
+ - session_load(date) — read a specific day's log (format: YYYY-MM-DD)
+ - session_recent(count) — show last N session summaries
+ - session_stats — aggregate totals across all sessions
+ - Recent sessions auto-load into context on first message
+ - systemd-style journal: use logs and logrotate commands for persistent logging
+
+3. **Conversation Patterns:**
+ - Pattern tracking runs automatically — frequent topics surface as hints
+ - my-set, my-get, my-list store additional personal data
+ - AI conversation context persists within a session (use ai-clear to reset)
+ - debug command toggles real-time debug panel with Copy/Export/VFS buttons
+
+4. **Checkpoints (VFS snapshots):**
+ - checkpoint [name] — snapshot current VFS state
+ - checkpoint-list — list all checkpoints
+ - rollback <name> — restore from checkpoint
+ - checkpoint-del <name> — delete a checkpoint
+
+5. **When to use:**
+ - At start: call session_recent(3) to see what was worked on before
+ - After identifying user preferences: call user_set to store them
+ - After complex tasks: call session_save with a brief summary
+ - Before risky edits: call checkpoint to create a restore point`),
+
+ s('web-search-and-fetch', 'How to search the web and fetch URL content using the WebX11 proxy API', ['search', 'fetch', 'web', 'proxy', 'api'], 'research',
+`1. **Search the web:**
+ - Use search <query> or web_search(query) — uses /api/search which queries SearXNG + DDG fallback
+ - Results include: title, url, content snippet, source engine
+ - Always available (same-origin, no CORS issues)
+ - 5-minute server-side cache for repeated queries
+ - Alternative: use ddg or duck <query> for DuckDuckGo direct, or s <query> as alias
+
+2. **Fetch URL content:**
+ - Use fetch <url> or web_fetch(url) — uses /api/extract which strips HTML, extracts clean text
+ - Returns: title, content (up to 4000 chars), word_count, extraction method
+ - SSRF protection blocks internal network addresses
+ - 15-minute server-side cache
+
+3. **Search flags (via terminal search command):**
+ - search <query> --summarize — adds AI-generated overview via Ollama
+ - search <query> --fetch — fetches content snippets for top 3 results
+
+4. **When to use:**
+ - User asks about current events, facts, research → use search <query>
+ - User says "read this article" or provides a URL → use fetch <url>
+ - For in-depth analysis: search first, then fetch top results`),
+
+ s('module-management', 'How to view, edit, test, reload modules and manage startup configuration', ['modules', 'editing', 'code', 'self-improvement'], 'dev',
+`1. **View module source:**
+ - view_module(name) — fetches original source or reads from _mods/ if edited
+ - Supports all loaded modules: SystemModule, FileSystemModule, SearXNGModule, etc.
+ - Or use modules command for GUI module manager
+
+2. **Edit a module:**
+ - edit_module(name) — copies source to _mods/<name>.js and opens code editor
+ - Editor shows the full source in a textarea
+ - Click "Save & Test" to save and auto-load the edited version
+ - Checkpoint before editing: checkpoint mod-edit-<name>
+
+3. **Test and reload:**
+ - test_module(name) — loads edited module from _mods/ for live testing
+ - reload_module(name) — discards edits and reloads from original URL
+ - list_modified_modules — shows all modules with pending edits
+
+4. **Check for updates:**
+ - check_updates — HEAD-requests all loaded module URLs to verify reachability
+
+5. **Manage startup:**
+ - startup-modules — opens the Startup Modules UI to select which modules load
+ - load-local <path> — load a module from VFS or URL without editing
+ - loadmod <url> — alternative: load module from URL
+ - removemod <name> — remove a loaded module
+
+6. **Agent modules (Phase 2):**
+ - module-agent-registry.js — Agent registry with agent-list, agent-register, agent-unregister, agent-status
+ - module-leases.js — Time-bound contracts with 5s watchdog, auto-expiry
+ - module-orchestrator.js — Spawn, delegate, kill, purge with heartbeat monitoring
+
+7. **When to use:**
+ - User reports a bug in a module → view_module, edit_module, test_module
+ - User asks "can you add X feature?" → same workflow
+ - Always save edits before testing. If test fails, the original module stays loaded.`),
+
+ s('system-maintenance', 'How to reset, troubleshoot, and maintain the WebX11 terminal', ['system', 'maintenance', 'reset', 'troubleshoot', 'config'], 'system',
+`1. **Factory reset (wipes everything):**
+ - factory-reset --confirm — clears all webx11_ localStorage keys, resets VFS, reloads
+ - Use only when the system is in an unrecoverable state
+
+2. **Soft VFS reset:**
+ - reset-vfs --confirm — clears only the filesystem, preserves settings/profile/modules
+
+3. **Config management:**
+ - config-list — show all config keys and values
+ - config-set <key> <value> — set a config value
+ - config-delete <key> — remove a config key
+ - config-export — dump all config as JSON
+ - config-reset — restore defaults (use before factory-reset if you want to save data)
+
+4. **Security level:**
+ - security-level <0-3> — set security level (0=Permissive, 1=Standard, 2=Paranoid, 3=Audit)
+ - security — show current security level, blocked count, and stats
+ - cmd-allow <cmd> — whitelist a command from security checks
+ - cmd-deny <cmd> — blacklist a command
+
+5. **Health checks (run automatically after complex tasks):**
+ - Check module loading status: loaded vs total
+ - Check for pending module edits
+ - Check memory usage (high if >100MB)
+ - Check skill count (0 skills after 2+ tool calls → suggest create_skill)
+ - health-check — full audit: VFS size, modules, logs, trash, skills, tool failures
+
+6. **Font size and display:**
+ - font-size <8-32> — set terminal font size, persists across sessions
+ - setfont — alias for font-size
+
+7. **Checkpoints:**
+ - checkpoint [name] — snapshot current VFS state
+ - checkpoint-list — list all checkpoints
+ - rollback <name> — restore from checkpoint
+
+8. **When to use:**
+ - System is slow or modules fail to load → check check_updates, mem
+ - User says "reset everything" → factory-reset --confirm
+ - User says "clear my files but keep settings" → reset-vfs --confirm
+ - User reports security concerns → security-level or cmd-allow/cmd-deny`),
+
+ s('backup-and-restore', 'How to backup, restore, and export WebX11 data', ['backup', 'restore', 'export', 'import', 'persistence'], 'system',
+`1. **Backup VFS:**
+ - vfs-export — downloads the entire virtual filesystem as a dated JSON file
+ - backup — creates a full backup including VFS, modules, config (saves to _backups/)
+
+2. **WebDAV cloud backup:**
+ - webdav-backup — push VFS snapshot to WebDAV server (auto-archive)
+ - webdav-backup-list — list remote WebDAV backups
+ - webdav-restore <file> — restore VFS from remote WebDAV snapshot
+ - webdav-push <local> <remote> — push file to WebDAV space
+ - webdav-pull <remote> <local> — pull file from WebDAV space
+ - webdav-ls [path] — list remote WebDAV directory contents
+ - webdav-status — check WebDAV connection and space usage
+ - webdav-explorer — GUI file browser for WebDAV
+ - webdav-browser — simpler file listing for WebDAV
+ - webdav-config — configure WebDAV endpoint and credentials
+
+3. **Export/Import personal data:**
+ - my-export — prints all my-set data as formatted JSON
+ - my-import <json> — bulk-import personal data from JSON string
+
+4. **Export/Import modules:**
+ - export-mods — exports module registry as JSON
+ - import-mods <json> — imports modules from JSON backup
+
+5. **Restore:**
+ - restore <json> — restores terminal state from a backup JSON
+ - Session logs are saved to /memory/ automatically on page unload
+
+6. **When to use:**
+ - Before factory-reset: call my-export, vfs-export, export-mods to save data
+ - When switching browsers or devices: export all, then import
+ - User says "backup my data" or "save my files"
+ - For cloud backup: use webdav-backup (requires WebDAV server configured)`),
+
+ s('mindmap-workflow', 'How to create, view, edit, and manage brainstorming mindmaps', ['mindmap', 'brainstorming', 'planning', 'organization'], 'productivity',
+`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 from 7-color palette
+
+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:**
+ - 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, auto-copies to clipboard
+
+7. **When to use:**
+ - Map out project structure, brainstorm features, organize ideas
+ - Break down complex tasks into subtrees
+ - User says "brainstorm", "diagram", "organize thoughts", "plan"`),
+
+ s('skills-system-guide', 'How to use the skills system: load, create, patch, list, and audit skills for persistent procedural memory', ['skills', 'system', 'meta'], 'system',
+`1. **Always load skills at task start:**
+ - At the START of every task, type or call "skill-view <name>" or "skill-list"
+ - Example: skill-view "module-management" — returns matching skill content
+ - Example: skill-list system — browse skills by category
+
+2. **Create skills after complex tasks:**
+ - After any task with 3+ tool calls, call "skill-create"
+ - Required fields: name (kebab-case), description, content (markdown with YAML frontmatter)
+ - Optional: tags (comma-separated), category (e.g. research, system, dev)
+ - Skills persist in /skills/ as .md files with /skills/index.json for fast lookup
+ - Skill pack auto-seeds 8 core skills on first boot
+
+3. **Patch skills when they're outdated:**
+ - If a stored procedure has wrong steps, call "skill-patch <name>"
+ - This replaces the entire skill content and updates the index
+ - Do NOT create a duplicate — always patch the existing skill
+
+4. **Auto-created error skills:**
+ - If the same tool fails 3+ times, a workaround skill is created automatically
+ - Named tool-<name>-workaround (e.g. tool-write_file-workaround)
+
+5. **Audit and maintenance:**
+ - skill-list <category?> — browse available skills, optionally filter
+ - skill-export <name> — download a skill as markdown
+ - skill-import <json> — bulk import skills
+ - skill-sync — re-index skills from /skills/ directory
+ - Skills are audited every 20 messages for staleness (empty content, >30 days old)
+
+6. **YAML frontmatter format:**
+ - Each skill file must start with: ---\\nname: <name>\\ndescription: <text>\\ntags: [...]\\ncategory: <cat>\\n---\\n
+ - Steps follow under ## Steps heading
+ - Use numbered lists for sequential steps`),
+
+ sk('agent-orchestration', 'How to use the agent registry, leases, and orchestrator for multi-agent workflows', ['agents', 'orchestration', 'delegation', 'leases'], 'system',
+`## Steps
+1. **Agent Registry:**
+ - agent-list — list all registered agents (ID, type, capabilities, status)
+ - agent-register <type> <capabilities> — register a new agent (capabilities: comma-separated like "code,research")
+ - agent-unregister <id> — remove an agent
+ - agent-status <id> — detailed status including uptime, task history
+
+2. **Leases (time-bound contracts):**
+ - lease-create <agent-id> <ttl-seconds> — create a lease with watchdog timer
+ - lease-list — show active leases with remaining TTL
+ - lease-revoke <lease-id> — terminate a lease early
+ - lease-renew <lease-id> <ttl-seconds> — extend a lease
+
+3. **Orchestrator (spawn, delegate, kill):**
+ - orchestrator-spawn <type> <capabilities> — spawn a new agent automatically (registers + creates lease)
+ - orchestrator-kill <agent-id> — kill an agent (revokes lease + unregisters)
+ - orchestrate — list all active agents with their orchestrator state
+ - orchestrator-purge — kill all idle/expired agents
+ - orchestrator-status — full system status: active, idle, dead agents
+
+4. **AI tool integration:**
+ - AI tools list_agents, delegate_task, spawn_agent are available in the AI system prompt
+ - The AI can auto-spawn worker agents for sub-tasks
+ - Each agent gets its own VFS workspace at /agents/<agent-id>/
+
+5. **When to use:**
+ - User needs parallel task execution → orchestrator-spawn coder + researcher
+ - Time-bound automation → lease-create with short TTL
+ - System cleanup → orchestrator-purge to kill stale agents`),
+
+ sk('debug-panel-and-logs', 'How to use the debug panel, session logging, and terminal diagnostics', ['debug', 'logging', 'diagnostics', 'troubleshooting'], 'system',
+`## Steps
+1. **Toggle debug panel:**
+ - debug — opens real-time debug panel showing last AI tool call, response time, and context
+ - Debug panel includes Copy, Export, and Write to VFS buttons
+ - Export: saves tool call log as timestamped JSON to downloads
+ - Write to VFS: appends debug data to /debug.log (dedup by turn ID)
+
+2. **Session logging:**
+ - logs [n] [LEVEL] — view last n log entries (optionally filtered by INFO/WARN/ERROR)
+ - logrotate — rotate current.log to dated archive (auto-cleans old logs beyond 10)
+ - Logs stored in /logs/ directory: current.log + dated archives
+ - Health check runs automatically after complex tasks
+
+3. **Diagnostic commands:**
+ - health-check — full audit: VFS size, modules, logs, trash, skills
+ - check — quick system health including module requirement status
+ - mem — real-time memory usage (also in header)
+ - status — network, files, modules, Ollama connectivity
+
+4. **System journals:**
+ - debug.log — appended by debug panel VFS button (dedup by turn ID)
+ - current.log — system activity log with rotation
+ - Auto-save: terminal state auto-saves every 60s
+
+5. **When to use:**
+ - AI is behaving unexpectedly → debug to see tool calls
+ - User reports errors → logs 50 ERROR to see error entries
+ - Memory concerns → mem to check, health-check for full audit`),
+
+ sk('webdav-operations', 'How to use WebDAV cloud storage for backup, file sync, and remote file access', ['webdav', 'cloud', 'backup', 'sync', 'storage'], 'system',
+`## Steps
+1. **Configuration:**
+ - webdav-config — configure WebDAV endpoint URL and credentials
+ - Connection auto-tested on config; webdav-status shows connection health
+ - Uses per-user token stored in .token file on the server side
+
+2. **File operations:**
+ - webdav-ls [path] — list remote directory contents
+ - webdav-push <local-path> <remote-path> — upload file to WebDAV
+ - webdav-pull <remote-path> [local-path] — download file from WebDAV
+ - webdav-explorer — GUI file browser for WebDAV
+ - webdav-browser — CLI file browser for WebDAV
+
+3. **Backup & Restore:**
+ - webdav-backup — push full VFS snapshot to WebDAV (creates /backups/newos4-<date>.json)
+ - webdav-backup-list — list all remote backup snapshots
+ - webdav-restore <filename> — restore VFS from a remote backup snapshot
+
+4. **Status & Diagnostics:**
+ - webdav-status — connection status, last sync, space information
+ - If uploads fail: check webdav-config, ensure .token file is valid
+ - WebDAV runs on the same server (nginx with WebDAV module), no external dependency
+
+5. **When to use:**
+ - Before factory-reset: webdav-backup to store VFS remotely
+ - Cross-browser: webdav-push/pull to move files between instances
+ - User says "cloud backup" or "remote save" → webdav-backup`),
+
+ sk('security-and-safety', 'How to use WebX11 security levels, command blocking, and safety features', ['security', 'safety', 'blocking', 'permissions'], 'system',
+`## Steps
+1. **Security levels:**
+ - security-level <0-3> — set security level:
+ 0 = Permissive (no blocks, no confirms — anything runs)
+ 1 = Standard (unknown cmds → AI route, destructive → confirm, system → block)
+ 2 = Paranoid (unknown cmds blocked entirely, destructive blocked)
+ 3 = Audit (everything allowed, everything logged)
+ - Default: level 1
+ - security — show current level, blocked count, and stats
+
+2. **Command control:**
+ - cmd-allow <cmd> — whitelist a command from security checks
+ - cmd-deny <cmd> — blacklist/block a command entirely
+ - whitelist built-in: help, clear, ls, cat, pwd, whoami, df, free, top, ps, echo, date, env, history
+
+3. **What gets blocked at each level:**
+ - Unknown commands: Level 1 → AI routed, Level 2+ → blocked
+ - Destructive commands (delete, write, save, format, etc.): Level 1 → confirmation, Level 2+ → blocked
+ - System commands (reboot, shutdown, format, kill): Level 1+ → blocked
+ - Suspicious flags (--execute, --eval, --shell): Level 1+ → blocked
+
+4. **Safety checklist:**
+ - Check security: security shows your current level
+ - Before running risky commands: create a checkpoint first
+ - For temporary relax: security-level 0, then restore to 1
+ - For total lockdown before leaving: security-level 2
+
+5. **When to use:**
+ - User wants to lock down the terminal → security-level 2
+ - User wants to allow a blocked command → cmd-allow <cmd>
+ - Debugging strange behavior → verify security-level (user may have set it higher)`),
+
+ sk('mindmap-user-guide', 'How to create, view, and manage brainstorming mindmaps using the mindmap terminal module', ['mindmap', 'brainstorming', 'organization', 'planning'], '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`),
+
+ sk('floating-ui', 'How to create, manage, and remove floating UI elements — buttons, collapsible menus, and status panels that persist across page reloads', ['floating', 'ui', 'buttons', 'menu', 'gui'], 'ui',
+`## Commands
+
+1. **Create a floating button:**
+ - floating-add btn <id> <label> <command>
+ - Example: floating-add btn files "📁 Files" ls
+ - Button appears at bottom-right, persists across reloads
+
+2. **Create a collapsible menu:**
+ - floating-add menu <id> <label> btn:SubLabel|command btn:Sub2|cmd2 ...
+ - Example: floating-add menu tools "🔧 Tools" btn:Help|help btn:Clear|clear
+ - Click header to expand/collapse. State persists.
+
+3. **Create a text panel:**
+ - floating-add panel <id> <title> <content>
+ - Example: floating-add panel status "📊 Status" "CPU: 2%"
+
+4. **Manage elements:**
+ - floating-list — show all elements with IDs and positions
+ - floating-remove <id> — delete by ID
+ - floating-toggle <id> — show/hide without deleting
+ - floating-pos <id> x|auto y|auto — reposition or reset to auto
+ - floating-clear — remove everything
+
+5. **Persistence:**
+ - All elements auto-save to localStorage (webx11_floating_elements)
+ - Survive page reload, browser restart
+ - Run floating-clear to start fresh
+
+## When to use
+- User wants quick-access buttons instead of typing commands
+- User says "add a button for X", "make a shortcut", "floating menu"
+- Creating a dashboard with status panels and action buttons`)
+ ];
+ }
+ registerCommands() {
+ this.commands['skill-pack-core'] = () => {
+ const count = this._skills.length;
+ this.terminal.print(`Skill Pack: Core — ${count} skills available`, 'info');
+ this.terminal.print('Skills will auto-seed on load', 'info');
+ };
+ this.commands['skill-list'] = (args) => this._cmdSkillList(args);
+ this.commands['skill-view'] = (args) => this._cmdSkillView(args);
+ this.commands['skill-create'] = (args) => this._cmdSkillCreate(args);
+ this.commands['skill-patch'] = (args) => this._cmdSkillPatch(args);
+ this.commands['skill-delete'] = (args) => this._cmdSkillDelete(args);
+ this.commands['skill-export'] = (args) => this._cmdSkillExport(args);
+ this.commands['skill-import'] = (args) => this._cmdSkillImport(args);
+ this.commands['skill-sync'] = () => this._cmdSkillSync();
+ // v4.5.90: Agent Skills — learn, validate, audit, restore
+ this.commands['learn-skill'] = (args) => this._cmdLearnSkill(args);
+ this.commands['skill-validate'] = (args) => this._cmdSkillValidate(args);
+ this.commands['skill-audit'] = () => this._cmdSkillAudit();
+ this.commands['skill-restore'] = (args) => this._cmdSkillRestore(args);
+ this.commands['skill-webdav-sync'] = () => this._cmdSkillWebdavSync();
+ }
+ // ── Skill Commands ──
+
+ _cmdSkillList(args) {
+ const fs = this.terminal.fs;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const entries = Object.values(index);
+ const filter = args[0] ? args[0].toLowerCase() : null;
+ const filtered = filter ? entries.filter(s => s.category === filter || s.category?.toLowerCase() === filter) : entries;
+ if (filtered.length === 0) {
+ this.terminal.print(filter ? `No skills in category: ${args[0]}` : 'No skills installed.', 'info');
+ return;
+ }
+ this.terminal.print(`📚 Skills (${filtered.length} of ${entries.length} total):`, 'info');
+ // Group by category
+ const groups = {};
+ for (const s of filtered) {
+ const cat = s.category || 'uncategorized';
+ if (!groups[cat]) groups[cat] = [];
+ groups[cat].push(s);
+ }
+ for (const [cat, skills] of Object.entries(groups)) {
+ this.terminal.print(` ${cat}:`, '');
+ for (const s of skills) {
+ const tags = s.tags?.length ? ` (${s.tags.join(', ')})` : '';
+ this.terminal.print(` 📄 ${s.name} — ${s.description}${tags}`, '');
+ }
+ }
+ }
+
+ _cmdSkillView(args) {
+ if (!args[0]) { this.terminal.print('Usage: skill-view <name>', 'warning'); return; }
+ const name = args[0].replace(/\.md$/, '');
+ const fs = this.terminal.fs;
+ const content = fs.readFile(`/skills/${name}.md`);
+ if (content === null || content === undefined) {
+ this.terminal.print(`Skill not found: ${name}`, 'error');
+ return;
+ }
+ this.terminal.print(`=== ${name} ===`, 'info');
+ const lines = content.split('\n');
+ for (const line of lines) {
+ this.terminal.print(line, '');
+ }
+ this.terminal.print(`=== End ${name} ===`, 'info');
+ // v4.5.90: Record skill use for decay tracking
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ if (mg && typeof mg.recordSkillUse === 'function') mg.recordSkillUse(name, true);
+ }
+
+ _cmdSkillCreate(args) {
+ if (args.length < 3) {
+ this.terminal.print('Usage: skill-create <name> <description> "<markdown content>" [tags]', 'warning');
+ return;
+ }
+ const name = args[0].replace(/[^a-z0-9-]/g, '').toLowerCase();
+ if (!name) { this.terminal.print('Name must be kebab-case (letters, numbers, hyphens only)', 'error'); return; }
+ const desc = args[1];
+ const content = args.slice(2).join(' ');
+ const tags = [];
+ // Parse tags from the last argument if it looks like a comma-separated list
+ const lastArg = args[args.length - 1];
+ if (lastArg && !lastArg.startsWith('"') && lastArg.includes(',')) {
+ tags.push(...lastArg.split(',').map(t => t.trim()).filter(Boolean));
+ }
+ const fs = this.terminal.fs;
+ // Check for duplicate
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ if (index[name]) {
+ this.terminal.print(`Skill "${name}" already exists. Use skill-patch to update.`, 'warning');
+ return;
+ }
+ const now = new Date().toISOString();
+ const skillContent = `---\nname: ${name}\ndescription: ${desc}\ntags: [${tags.join(', ')}]\ncategory: general\n---\n${content}`;
+ fs.writeFile(`/skills/${name}.md`, skillContent);
+ index[name] = { name, description: desc, tags, category: 'general', created: now, updated: now };
+ fs.fsWriteJSON('/skills/index.json', index);
+ this.terminal.print(`✅ Skill "${name}" created (${(skillContent.length / 1024).toFixed(1)}KB)`, 'success');
+ }
+
+ _cmdSkillPatch(args) {
+ if (args.length < 2) {
+ this.terminal.print('Usage: skill-patch <name> "<full content>"', 'warning');
+ return;
+ }
+ const name = args[0].replace(/\.md$/, '');
+ const content = args.slice(1).join(' ');
+ const fs = this.terminal.fs;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ if (!index[name]) {
+ this.terminal.print(`Skill not found: ${name}`, 'error');
+ return;
+ }
+ fs.writeFile(`/skills/${name}.md`, content);
+ index[name].updated = new Date().toISOString();
+ // Re-parse YAML frontmatter to update index
+ const headerMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
+ if (headerMatch) {
+ const header = headerMatch[1];
+ const d = header.match(/description:\s*(.+)/);
+ const c = header.match(/category:\s*(.+)/);
+ const t = header.match(/tags:\s*\[(.*?)\]/);
+ if (d) index[name].description = d[1].trim();
+ if (c) index[name].category = c[1].trim();
+ if (t) index[name].tags = t[1].split(',').map(x => x.trim()).filter(Boolean);
+ }
+ fs.fsWriteJSON('/skills/index.json', index);
+ this.terminal.print(`✅ Skill "${name}" updated`, 'success');
+ }
+
+ _cmdSkillDelete(args) {
+ if (!args[0]) { this.terminal.print('Usage: skill-delete <name>', 'warning'); return; }
+ const name = args[0].replace(/\.md$/, '');
+ const fs = this.terminal.fs;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ if (!index[name]) {
+ this.terminal.print(`Skill not found: ${name}`, 'error');
+ return;
+ }
+ fs.deleteFile(`/skills/${name}.md`);
+ delete index[name];
+ fs.fsWriteJSON('/skills/index.json', index);
+ this.terminal.print(`🗑️ Skill "${name}" deleted`, 'success');
+ }
+
+ _cmdSkillExport(args) {
+ if (!args[0]) { this.terminal.print('Usage: skill-export <name>', 'warning'); return; }
+ const name = args[0].replace(/\.md$/, '');
+ const fs = this.terminal.fs;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ if (!index[name]) {
+ this.terminal.print(`Skill not found: ${name}`, 'error');
+ return;
+ }
+ const content = fs.readFile(`/skills/${name}.md`);
+ const exportObj = {
+ ...index[name],
+ content: content || ''
+ };
+ this.terminal.print(JSON.stringify(exportObj, null, 2), '');
+ }
+
+ _cmdSkillImport(args) {
+ if (args.length === 0) { this.terminal.print('Usage: skill-import <json_string> or pipe a JSON object', 'warning'); return; }
+ const jsonStr = args.join(' ');
+ let data;
+ try { data = JSON.parse(jsonStr); } catch (e) {
+ this.terminal.print('Invalid JSON. Please provide a valid skill object.', 'error');
+ return;
+ }
+ if (!data.name || !data.content) {
+ this.terminal.print('JSON must include "name" and "content" fields', 'error');
+ return;
+ }
+ const fs = this.terminal.fs;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const now = new Date().toISOString();
+ const existing = index[data.name];
+ fs.writeFile(`/skills/${data.name}.md`, data.content);
+ index[data.name] = {
+ name: data.name,
+ description: data.description || data.name,
+ tags: data.tags || [],
+ category: data.category || 'general',
+ created: existing?.created || now,
+ updated: now
+ };
+ fs.fsWriteJSON('/skills/index.json', index);
+ this.terminal.print(`✅ Skill "${data.name}" imported (${existing ? 'updated' : 'created'})`, 'success');
+ }
+
+ _cmdSkillSync() {
+ const fs = this.terminal.fs;
+ const files = fs.ls('/skills') || [];
+ const mdFiles = files.filter(f => f.type === 'file' && f.name.endsWith('.md') && f.name !== 'index.json');
+ let synced = 0;
+ const oldIndex = fs.fsReadJSON('/skills/index.json') || {};
+ const index = {};
+ for (const f of mdFiles) {
+ const content = fs.readFile(`/skills/${f.name}`);
+ if (!content) continue;
+ const name = f.name.replace(/\.md$/, '');
+ const headerMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
+ let description = name;
+ let tags = [];
+ let category = 'general';
+ if (headerMatch) {
+ const h = headerMatch[1];
+ const d = h.match(/description:\s*(.+)/);
+ const c = h.match(/category:\s*(.+)/);
+ const t = h.match(/tags:\s*\[(.*?)\]/);
+ if (d) description = d[1].trim();
+ if (c) category = c[1].trim();
+ if (t) tags = t[1].split(',').map(x => x.trim()).filter(Boolean);
+ }
+ index[name] = {
+ name,
+ description,
+ tags,
+ category,
+ created: (oldIndex[name] && oldIndex[name].created) || new Date().toISOString(),
+ updated: new Date().toISOString()
+ };
+ synced++;
+ }
+ fs.fsWriteJSON('/skills/index.json', index);
+ this.terminal.print(`📚 Skill index rebuilt: ${synced} skills synced`, 'success');
+ }
+
+ async _cmdSkillWebdavSync() {
+ const fs = this.terminal.fs;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const names = Object.keys(index);
+ if (names.length === 0) {
+ this.terminal.print('No skills to sync', 'info');
+ return;
+ }
+ // Find the webdav module
+ const webdavMod = this.terminal.moduleManager.modules['WebDAVModule'];
+ if (!webdavMod || !webdavMod._webdavRequest) {
+ this.terminal.print('WebDAV module not loaded. Configure webdav first with webdav-config', 'warning');
+ return;
+ }
+ this.terminal.print(`📤 Syncing ${names.length} skills to WebDAV...`, 'info');
+ let ok = 0, fail = 0;
+ await webdavMod._ensureDir('skills');
+ for (const name of names) {
+ const content = fs.readFile(`/skills/${name}.md`);
+ if (content === null) { fail++; continue; }
+ const meta = index[name];
+ const exportObj = { ...meta, content };
+ const json = JSON.stringify(exportObj, null, 2);
+ try {
+ const resp = await webdavMod._webdavRequest('PUT', 'skills/' + name + '.json', {
+ body: json,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ if (resp.ok || resp.status === 201 || resp.status === 204) {
+ ok++;
+ } else {
+ fail++;
+ }
+ } catch (e) {
+ fail++;
+ }
+ }
+ this.terminal.print(`📤 Skill WebDAV sync complete: ${ok} pushed, ${fail} failed`, fail > 0 ? 'warning' : 'success');
+ }
+
+ init() {
+ this.seed();
+ }
+
+ _getSkillCategories() {
+ // Read from /skills/index.json and return unique categories
+ const fs = this.terminal?.fs;
+ if (!fs) return ['system', 'research', 'devops', 'general'];
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const cats = new Set();
+ for (const skill of Object.values(index)) {
+ if (skill.category) cats.add(skill.category);
+ }
+ return cats.size > 0 ? [...cats] : ['system', 'research', 'devops', 'general'];
+ }
+
+ async seed() {
+ console.log('[SkillPackCore] seed() called, _seeded=' + this._seeded);
+ if (this._seeded) return;
+ const fs = this.terminal.fs;
+ if (!fs) { console.warn('[SkillPackCore] this.terminal.fs not available'); return; }
+ fs.mkdirRecursive('/skills');
+ let index = fs.fsReadJSON('/skills/index.json') || {};
+ console.log('[SkillPackCore] existing index keys:', Object.keys(index));
+ let seeded = 0;
+ let updated = 0;
+ for (const skill of this._skills) {
+ const existing = index[skill.name];
+ if (!existing) {
+ fs.writeFile('/skills/' + skill.name + '.md', skill.content);
+ index[skill.name] = {
+ name: skill.name,
+ description: skill.description,
+ tags: skill.tags,
+ category: skill.category,
+ created: new Date().toISOString(),
+ updated: new Date().toISOString()
+ };
+ seeded++;
+ } else {
+ // Only rewrite the .md and update index if the bundled content has actually changed.
+ // This prevents a permanent re-index loop on /skills/index.json: every page refresh
+ // would otherwise bump the `updated` timestamp → hash mismatch → NVFS re-indexes
+ // the 7KB JSON file → LLM re-extracts 5 concepts → token waste.
+ const existingContent = fs.readFile('/skills/' + skill.name + '.md');
+ if (existingContent !== skill.content) {
+ fs.writeFile('/skills/' + skill.name + '.md', skill.content);
+ index[skill.name] = {
+ ...existing,
+ description: skill.description,
+ tags: skill.tags,
+ category: skill.category,
+ updated: new Date().toISOString()
+ };
+ updated++;
+ }
+ }
+ }
+ if (seeded > 0) {
+ this.terminal.print('📚 Seeded ' + seeded + ' new skill(s)', 'success');
+ }
+ if (updated > 0) {
+ this.terminal.print('📚 Updated ' + updated + ' skill(s) from new bundle', 'info');
+ }
+ // Only write index.json if something actually changed — otherwise we cause a
+ // hash mismatch on every refresh and NVFS re-indexes this 7KB file forever.
+ if (seeded > 0 || updated > 0) {
+ fs.fsWriteJSON('/skills/index.json', index);
+ }
+ this._seeded = true;
+ }
+
+ // ── v4.5.90: Agent Skills ──
+
+ _cmdLearnSkill(args) {
+ if (!args || args.length === 0) return this.terminal.print('Usage: learn-skill <topic>', 'warning');
+ const topic = args.join(' ');
+ this.terminal.print('🧠 Reflecting on: ' + topic + '...', 'info');
+
+ // 1. Pull context from memgraph
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ let graphNodes = [];
+ if (mg) {
+ graphNodes = mg.findNodes(topic) || [];
+ this.terminal.print(' 📊 Found ' + graphNodes.length + ' graph nodes matching topic', 'info');
+ }
+
+ // 2. Pull recent sessions
+ const fs = this.terminal.fs;
+ let sessionInsights = [];
+ try {
+ const memDir = fs.readDir('/memory/') || [];
+ // v4.5.90b fix: readDir returns objects, filter to .json files (P0 from review)
+ const recent = memDir.filter(function(e) { return e.type === 'file' && e.name.endsWith('.json'); }).slice(-3);
+ for (const f of recent) {
+ const data = fs.fsReadJSON('/memory/' + f.name);
+ if (data && data.summary) sessionInsights.push(data.summary);
+ }
+ } catch (_) {}
+ this.terminal.print(' 📅 Loaded ' + sessionInsights.length + ' recent sessions', 'info');
+
+ // 3. Pull existing skills
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const skillNames = Object.keys(index).filter(function(n) {
+ return n.toLowerCase().includes(topic.toLowerCase().split(' ')[0]);
+ });
+ this.terminal.print(' 📚 Found ' + skillNames.length + ' related skills', 'info');
+
+ // 4. Draft skill
+ const draft = this._draftSkill(topic, graphNodes, sessionInsights, skillNames);
+ this.terminal.print('', '');
+ this.terminal.print('📝 Draft skill:', 'header');
+ const lines = draft.split('\n');
+ for (const line of lines) this.terminal.print(line, '');
+ this.terminal.print('', '');
+
+ // 5. Validate
+ const validation = this._validateSkillContent(draft);
+ if (validation.errors.length > 0) {
+ this.terminal.print('❌ Validation errors:', 'error');
+ for (const e of validation.errors) this.terminal.print(' • ' + e, 'error');
+ this.terminal.print('Fix the issues and use skill-create to save manually.', 'info');
+ return;
+ }
+ this.terminal.print('✅ Validation passed. Confidence: ' + validation.confidence.toFixed(2), 'success');
+
+ // 6. Propose
+ this.terminal.print('Ship this skill? Use: skill-create ' + topic.replace(/\s+/g, '-') + ' "<desc>" "<content>"', 'info');
+ this.terminal.print('Or discard and continue.', 'text-muted');
+ }
+
+ _draftSkill(topic, graphNodes, sessions, existingSkills) {
+ var lines = [];
+ lines.push('---');
+ lines.push('name: ' + topic.toLowerCase().replace(/\s+/g, '-'));
+ lines.push('description: ' + topic + ' — auto-drafted from agent experience');
+ lines.push('tags: [auto-learned, ' + topic.toLowerCase().split(' ')[0] + ']');
+ lines.push('---');
+ lines.push('');
+ lines.push('# ' + topic);
+ lines.push('');
+ lines.push('## Steps');
+ lines.push('');
+ // Generate steps from graph node patterns
+ var stepNum = 1;
+ if (graphNodes && graphNodes.length > 0) {
+ for (var n of graphNodes.slice(0, 5)) {
+ lines.push(stepNum + '. ' + n.label + ' (type: ' + n.type + ', uses: ' + (n.access_count || 1) + ')');
+ stepNum++;
+ }
+ } else {
+ lines.push('1. Identify the core ' + topic + ' workflow');
+ lines.push('2. Check for existing tools and modules');
+ lines.push('3. Execute the task step by step');
+ lines.push('4. Record results in memory graph');
+ }
+ if (sessions && sessions.length > 0) {
+ lines.push('');
+ lines.push('## Insights from Recent Sessions');
+ for (var s of sessions.slice(0, 3)) {
+ lines.push('- ' + s.substring(0, 100));
+ }
+ }
+ if (existingSkills && existingSkills.length > 0) {
+ lines.push('');
+ lines.push('## Related Skills');
+ for (var sk of existingSkills) lines.push('- skill-view ' + sk);
+ }
+ lines.push('');
+ lines.push('## Pitfalls');
+ lines.push('- This skill was auto-drafted — validate before relying on it');
+ return lines.join('\n');
+ }
+
+ _cmdSkillValidate(args) {
+ if (!args || args.length === 0) return this.terminal.print('Usage: skill-validate <name>', 'warning');
+ const name = args[0].replace(/\.md$/, '');
+ const fs = this.terminal.fs;
+ const content = fs.readFile('/skills/' + name + '.md');
+ if (!content) return this.terminal.print('Skill not found: ' + name, 'error');
+
+ const result = this._validateSkillContent(content);
+ this.terminal.print('📊 Validation: ' + name, 'header');
+ if (result.errors.length === 0) {
+ this.terminal.print(' ✅ Valid', 'success');
+ } else {
+ this.terminal.print(' ❌ ' + result.errors.length + ' error(s):', 'error');
+ for (var e of result.errors) this.terminal.print(' • ' + e, 'error');
+ }
+ if (result.warnings.length > 0) {
+ this.terminal.print(' ⚠️ ' + result.warnings.length + ' warning(s):', 'warning');
+ for (var w of result.warnings) this.terminal.print(' • ' + w, 'warning');
+ }
+ this.terminal.print(' 📈 Confidence: ' + result.confidence.toFixed(2), 'info');
+ }
+
+ _validateSkillContent(content) {
+ var errors = [];
+ var warnings = [];
+ var confidence = 0;
+
+ // v4.5.90b fix: Normalize CRLF to LF (P2 from review)
+ content = content.replace(/\r\n/g, '\n');
+
+ // Check YAML frontmatter
+ var fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
+ if (!fmMatch) {
+ errors.push('Missing YAML frontmatter (--- ... ---)');
+ } else {
+ var fm = fmMatch[1];
+ if (!fm.match(/^name:\s*\S+/m)) errors.push('Missing "name" field in frontmatter');
+ if (!fm.match(/^description:\s*\S+/m)) errors.push('Missing "description" field in frontmatter');
+ var nameMatch = fm.match(/^name:\s*(\S+)/m);
+ if (nameMatch && !nameMatch[1].match(/^[a-z0-9-]+$/)) {
+ errors.push('Name must be kebab-case (lowercase, hyphens, numbers only)');
+ }
+ var tagsMatch = fm.match(/^tags:\s*\[(.+)\]/m);
+ if (tagsMatch && !tagsMatch[1].trim()) warnings.push('Tags field is empty');
+ }
+
+ // Check for ## Steps heading
+ if (!content.match(/##\s+Steps/i)) {
+ errors.push('Missing "## Steps" heading');
+ } else {
+ // Check for at least 1 numbered step
+ var steps = content.match(/^\d+\.\s+/gm);
+ if (!steps || steps.length === 0) errors.push('No numbered steps found under ## Steps');
+ }
+
+ // Check broken skill-view references
+ var refMatches = content.match(/skill-view\s+(\S+)/g) || [];
+ var fs = this.terminal.fs;
+ for (var ref of refMatches) {
+ var refName = ref.replace(/skill-view\s+/, '').replace(/\.md$/, '');
+ if (fs && !fs.readFile('/skills/' + refName + '.md')) {
+ warnings.push('Broken skill-view reference: ' + refName);
+ }
+ }
+
+ // v4.5.90b fix: Confidence = success_rate × (uses / (uses + 5)) — saturates at ~5 perfect uses, discriminates better (P2 from review)
+ var index = fs ? (fs.fsReadJSON('/skills/index.json') || {}) : {};
+ var fmName = fmMatch ? fmMatch[1].match(/^name:\s*(\S+)/m) : null;
+ var skillName = fmName ? fmName[1] : '';
+ var stats = index[skillName] || {};
+ var uses = stats.useCount || 0;
+ var successes = stats.successCount || 0;
+ if (uses > 0) {
+ var successRate = successes / uses;
+ confidence = successRate * (uses / (uses + 5));
+ }
+
+ return { valid: errors.length === 0, errors: errors, warnings: warnings, confidence: Math.min(confidence, 1.0) };
+ }
+
+ _cmdSkillAudit() {
+ const fs = this.terminal.fs;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const allNames = Object.keys(index);
+ const now = Date.now();
+
+ var stale = [];
+ var duplicates = [];
+ var brokenRefs = 0;
+
+ // 1. Stale check
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ var staleSkills = [];
+ if (mg && typeof mg.getStaleSkills === 'function') {
+ staleSkills = mg.getStaleSkills();
+ }
+ for (var s of staleSkills) {
+ if (!index[s.name] || index[s.name].immune) continue;
+ stale.push(s.name + ' (' + s.daysUnused + 'd unused)');
+ }
+
+ // 2. Duplicate check (simple Levenshtein on name)
+ const auditCap = 200;
+ const auditNames = allNames.slice(0, auditCap);
+ for (var i = 0; i < auditNames.length; i++) {
+ for (var j = i + 1; j < auditNames.length; j++) {
+ if (this._levenshtein(auditNames[i], auditNames[j]) <= 2) {
+ duplicates.push(auditNames[i] + ' ≈ ' + auditNames[j]);
+ }
+ }
+ }
+ if (allNames.length > auditCap) {
+ warnings.push('Duplicate check only scanned first ' + auditCap + ' of ' + allNames.length + ' skills');
+ }
+
+ // 3. Reference check
+ for (var name of allNames) {
+ var content = fs.readFile('/skills/' + name + '.md');
+ if (!content) continue;
+ var refs = content.match(/skill-view\s+(\S+)/g) || [];
+ for (var ref of refs) {
+ var refName = ref.replace(/skill-view\s+/, '').replace(/\.md$/, '');
+ if (!fs.readFile('/skills/' + refName + '.md')) brokenRefs++;
+ }
+ }
+
+ var healthy = allNames.length - stale.length;
+ this.terminal.print('📊 Skill Audit', 'header');
+ this.terminal.print('────────────────────────────────', 'text-muted');
+ this.terminal.print('Total skills: ' + allNames.length, 'info');
+ this.terminal.print('Healthy: ' + healthy, 'success');
+ this.terminal.print('Stale: ' + stale.length + (stale.length > 0 ? ' (' + stale.join(', ') + ')' : ''), stale.length > 0 ? 'warning' : 'info');
+ this.terminal.print('Duplicates: ' + duplicates.length + (duplicates.length > 0 ? ' (' + duplicates.join(', ') + ')' : ''), duplicates.length > 0 ? 'warning' : 'info');
+ this.terminal.print('Broken refs: ' + brokenRefs, brokenRefs > 0 ? 'error' : 'info');
+ this.terminal.print('', '');
+
+ if (stale.length > 0 || duplicates.length > 0 || brokenRefs > 0) {
+ this.terminal.print('Suggestions:', 'info');
+ for (var st of stale) this.terminal.print('• Archive ' + st, 'warning');
+ for (var dup of duplicates) this.terminal.print('• Merge ' + dup, 'warning');
+ if (brokenRefs > 0) this.terminal.print('• Fix ' + brokenRefs + ' broken reference(s)', 'warning');
+ } else {
+ this.terminal.print('✅ All skills healthy', 'success');
+ }
+ }
+
+ _cmdSkillRestore(args) {
+ if (!args || args.length === 0) return this.terminal.print('Usage: skill-restore <name>', 'warning');
+ const name = args[0].replace(/\.md$/, '');
+ const fs = this.terminal.fs;
+ if (!fs.readDir('/skills/.archive/')) return this.terminal.print('No archive directory found', 'error');
+ const content = fs.readFile('/skills/.archive/' + name + '.md');
+ if (!content) return this.terminal.print('Skill not found in archive: ' + name, 'error');
+ // v4.5.90b fix: Don't overwrite a live skill (P1 from review)
+ const existing = fs.readFile('/skills/' + name + '.md');
+ if (existing) return this.terminal.print('Skill "' + name + '" already exists live. Use skill-patch or delete it first.', 'warning');
+ fs.writeFile('/skills/' + name + '.md', content);
+ fs.deleteFile('/skills/.archive/' + name + '.md');
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ index[name] = index[name] || {};
+ index[name].lastUsed = new Date().toISOString();
+ index[name].useCount = index[name].useCount || 0;
+ fs.fsWriteJSON('/skills/index.json', index);
+ this.terminal.print('✅ Restored: ' + name, 'success');
+ }
+
+ _levenshtein(a, b) {
+ if (a.length === 0) return b.length;
+ if (b.length === 0) return a.length;
+ var matrix = [];
+ for (var i = 0; i <= b.length; i++) matrix[i] = [i];
+ for (var j = 0; j <= a.length; j++) matrix[0][j] = j;
+ for (var i = 1; i <= b.length; i++) {
+ for (var j = 1; j <= a.length; j++) {
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
+ matrix[i][j] = matrix[i - 1][j - 1];
+ } else {
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
+ }
+ }
+ }
+ return matrix[b.length][a.length];
+ }
+}
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new SkillPackCoreModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/skill-pack-core.js');
+} \ No newline at end of file
diff --git a/.bkp-fix-20261023_143429/stats.php b/.bkp-fix-20261023_143429/stats.php
new file mode 100644
index 0000000..dd0b0db
--- /dev/null
+++ b/.bkp-fix-20261023_143429/stats.php
@@ -0,0 +1,55 @@
+<?php
+require_once __DIR__ . '/config.php';
+
+$errors = load_json(ERRORS_FILE);
+$sessions = load_json(SESSIONS_FILE);
+
+$total = count($errors);
+$byType = [];
+$bySeverity = [];
+$byCategory = [];
+$byModule = [];
+$byDay = [];
+$topErrors = [];
+
+foreach ($errors as $e) {
+ $t = $e['type'] ?? 'unknown';
+ $byType[$t] = ($byType[$t] ?? 0) + ($e['count'] ?? 1);
+
+ $s = $e['severity'] ?? 'error';
+ $bySeverity[$s] = ($bySeverity[$s] ?? 0) + ($e['count'] ?? 1);
+
+ $c = $e['category'] ?? 'runtime';
+ $byCategory[$c] = ($byCategory[$c] ?? 0) + ($e['count'] ?? 1);
+
+ $m = $e['module'] ?? 'unknown';
+ $byModule[$m] = ($byModule[$m] ?? 0) + ($e['count'] ?? 1);
+
+ $day = substr($e['ingested_at'] ?? $e['last_seen'], 0, 10);
+ $byDay[$day] = ($byDay[$day] ?? 0) + ($e['count'] ?? 1);
+
+ $hash = $e['hash'] ?? '';
+ if ($hash) {
+ if (!isset($topErrors[$hash])) {
+ $topErrors[$hash] = ['hash' => $hash, 'message' => $e['message'] ?? '', 'count' => 0, 'type' => $t];
+ }
+ $topErrors[$hash]['count'] += $e['count'] ?? 1;
+ }
+}
+
+usort($topErrors, fn($a, $b) => $b['count'] - $a['count']);
+$topErrors = array_slice($topErrors, 0, 20);
+ksort($byDay);
+
+json_response([
+ 'success' => true,
+ 'total_errors' => $total,
+ 'total_sessions' => count($sessions),
+ 'by_type' => $byType,
+ 'by_severity' => $bySeverity,
+ 'by_category' => $byCategory,
+ 'by_module' => $byModule,
+ 'by_day' => $byDay,
+ 'top_errors' => $topErrors,
+ 'generated_at' => date('c'),
+]);