diff options
Diffstat (limited to '.bkp-fix-20261023_143429')
| -rw-r--r-- | .bkp-fix-20261023_143429/clusters.php | 98 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/config.php | 59 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/dashboard.php | 200 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/error.php | 36 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/errors.php | 43 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/filemgr.js | 1150 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/health.php | 20 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/help.html | 2147 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/ingest.php | 123 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/main.html | 1484 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/module-debug-supervisor.js | 889 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/module-loader.js | 84 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/module-memory-graph.js | 663 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/module-world-model.js | 600 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/report.php | 112 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/skill-pack-core.js | 1150 | ||||
| -rw-r--r-- | .bkp-fix-20261023_143429/stats.php | 55 |
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 · <?= date('Y-m-d H:i:s') ?> · <a href="stats.php">JSON Stats</a> · <a href="clusters.php">Clusters</a> · <a href="errors.php">Raw Errors</a> · <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, '&') + .replace(/</g, '<') + .replace(/>/g, '>'); + // 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, '&').replace(/</g, '<').replace(/>/g, '>') + '</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, '"').replace(/'/g, ''')) + '" ' + + '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 || ' ') + '</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); + } + + _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 <command></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 <path></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 'N,Mp' | 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 <request></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 <question></code></td><td>Ask the assistant (short alias of <code>webxai</code>)</td></tr> + <tr><td><code>webxai <question></code></td><td>Ask the assistant a one-off question</td></tr> + <tr><td><code>wx <question></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 <path></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 <name> <display> <systemPrompt></code></td><td>Create a context profile</td></tr> + <tr><td><code>context-delete <name></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 <question></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 <name></code></td><td>Switch to a named profile</td></tr> + <tr><td><code>agent-loops new <name> <desc></code></td><td>Create a new profile from defaults</td></tr> + <tr><td><code>agent-loops edit <name> <key> <val></code></td><td>Edit a profile setting (dot-notation, e.g. <code>loop.maxTurns 12</code>)</td></tr> + <tr><td><code>agent-loops delete <name></code></td><td>Delete a profile (not default)</td></tr> + <tr><td><code>agent-loops clone <src> <dst></code></td><td>Clone a profile</td></tr> + <tr><td><code>agent-loops export <name></code></td><td>Export profile as JSON</td></tr> + <tr><td><code>agent-loops reset <name></code></td><td>Reset profile to defaults</td></tr> + <tr><td><code>bench <profile> [--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 <p1> <p2></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 <topic></code></td><td>Learn a new skill from a topic</td></tr> + <tr><td><code>skill-validate <name></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 <name></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 <query></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 <text></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 <file> [--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 <px></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 <path> <passphrase></code></td><td>AES-256-GCM encrypt a VFS file</td></tr> + <tr><td><code>decrypt <path.enc> <passphrase></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 <prices|portfolio|transactions|copy-trade|chat></code></td><td>Query VaultMind</td></tr> + <tr><td><code>wallet-lookup <address></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 <url></code></td><td>Load module from URL</td></tr> + <tr><td><code>removemod <name></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 <expr></code></td><td>Evaluate JavaScript in page context</td></tr> + <tr><td><code>js-run <path></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 <name></code></td><td>Load a module by name</td></tr> + <tr><td><code>module-unload <name></code></td><td>Unload a module</td></tr> + <tr><td><code>module-enable <name></code></td><td>Enable a module for startup</td></tr> + <tr><td><code>module-reset <name></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/<name>.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 <lessonId> | course list</code></td><td>Course browser</td></tr> + <tr><td><code>kanban add <task> | list | done <id></code></td><td>Task board</td></tr> + <tr><td><code>three add text <text> | reset | screenshot</code></td><td>3D scene commands</td></tr> + <tr><td><code>gui-run <command></code></td><td>Run a command and render its output in the GUI panel</td></tr> + <tr><td><code>gui-text <content></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 <path></code></td><td>Compress a VFS file to .gz</td></tr> + <tr><td><code>wget <url> [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 & 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>></code> <code>>></code> <code><</code></h3> + <pre><span class="cmt"># Write output to file</span> +cal > /tmp/calendar.txt + +<span class="cmt"># Append to file</span> +echo "new line" >> log.txt + +<span class="cmt"># Read file as stdin</span> +grep error < log.txt</pre> + + <h3>Command Chaining</h3> + <pre><span class="cmt"># AND: next runs only if previous succeeds</span> +touch test.txt && 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' > greet.sh +echo 'echo "Hello, $NAME!"' >> 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 <var>=<val></code></td><td>Set a shell variable</td></tr> + <tr><td><code>unset <var></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 <path.sh></code></td><td>Run a VFS script file</td></tr> + <tr><td><code>sh <path.sh></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 <name></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 <term>]</code></td><td>Command history, optionally filtered</td></tr> + <tr><td><code>run <script></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 <name></code></td><td>Print a module's source</td></tr> + <tr><td><code>node <script.js></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 <id> <label> <command></code></td><td>Spawn a floating button that runs a terminal command</td></tr> + <tr><td><code>floating-add menu <id> <label> btn:Sub|cmd ...</code></td><td>Spawn a collapsible floating menu with sub-buttons</td></tr> + <tr><td><code>floating-add panel <id> <title> <content></code></td><td>Spawn a floating text panel</td></tr> + <tr><td><code>floating-remove <id></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 <id> <x|auto> <y|auto></code></td><td>Reposition a floating element</td></tr> + <tr><td><code>floating-toggle <id></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 <id> <severity> <pattern> <tools...></code></td><td>Add a custom guardrail rule (severity: deny/warn)</td></tr> + <tr><td><code>guardrail-remove <id></code></td><td>Remove a guardrail rule</td></tr> + <tr><td><code>shadow <command></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 <url> <token></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 <local> [remote]</code></td><td>Upload file to cloud</td></tr> + <tr><td><code>webdav-pull <remote> [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 <path></code></td><td>Delete remote file/directory</td></tr> + <tr><td><code>webdav-mkdir <path></code></td><td>Create remote directory</td></tr> + <tr><td><code>webdav-rename <old> <new></code></td><td>Rename remote file</td></tr> + <tr><td><code>webdav-move <src> <dst></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 <user></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 <id></code></td><td>Show detailed status for a specific agent</td></tr> + <tr><td><code>agent-register <name> <model></code></td><td>Register a new AI agent</td></tr> + <tr><td><code>agent-unregister <id></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 <task></code></td><td>Delegate multi-step task to agent orchestrator</td></tr> + <tr><td><code>orchestrate-parallel <task></code></td><td>Decompose and run sub-tasks in parallel (auto-splits on "and"/"then")</td></tr> + <tr><td><code>orchestrate-decompose <task></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 <role> [caps...]</code></td><td>Spawn a new worker agent</td></tr> + <tr><td><code>orchestrator-sleep <id></code></td><td>Suspend agent to VFS (survives reload)</td></tr> + <tr><td><code>orchestrator-wake <id></code></td><td>Restore agent from VFS sleep state</td></tr> + <tr><td><code>orchestrator-kill <id></code></td><td>Terminate an agent and revoke leases</td></tr> + <tr><td><code>orchestrator-purge</code></td><td>Remove dead agents (>60s silent)</td></tr> + <tr><td><code>orchestrator-config <key> <value></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 <id> <policy></code></td><td>Set per-agent restart policy: on-failure, always, never</td></tr> + <tr><td><code>consensus <question></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 <test|deploy|verify|history|config></code></td><td>Autonomous CI/CD: test agents/modules, deploy (test→auto-fix→verify), history, config</td></tr> + <tr><td><code>lease-create <agent-id> [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 <id></code></td><td>Revoke an active lease</td></tr> + <tr><td><code>lease-renew <id> <seconds></code></td><td>Extend an active lease by N seconds</td></tr> + <tr><td><code>orchestrator-schedule <role> <interval_s> <task...></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 <id></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 <agent-id> <json></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 <role> [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 <to> <type> <capability> <payload></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 <id></code></td><td>Acknowledge a message</td></tr> + <tr><td><code>a2a-reject <id></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 <q></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 <concept></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 <name> <url></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 <name></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 <task></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 <k> <v></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 <url></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 <k> <v></code></td><td>Configure max-workers, min-workers</td></tr> + <tr><td><code>sandbox-run <task></code></td><td>Execute a task in the sandbox pool</td></tr> + </table></div> + <p><code>orchestrate --sandboxed <task></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 <q></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 (>30d, <3 uses)</td></tr> + <tr><td><code>memgraph-learn <entity> <fact></code></td><td>Add a fact to the memory graph</td></tr> + <tr><td><code>memgraph-agent <agentId></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 <days> <min_uses></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 <q></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 <k> <v></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 <0-3></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 <cmd></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 <cmd></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 <cmd></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 <flag></code></td><td>Add/remove suspicious flag (persisted, -- prefix optional)</td></tr> + <tr><td><code>cmd-allow <command></code></td><td>Add command to allowlist (bypasses restrictions)</td></tr> + <tr><td><code>cmd-deny <command></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 <dir></span><span class="desc">Change directory</span></div> + <div class="quick-ref-item"><span class="cmd">cat <f></span><span class="desc">View file</span></div> + <div class="quick-ref-item"><span class="cmd">edit <f></span><span class="desc">Edit file</span></div> + <div class="quick-ref-item"><span class="cmd">ai <msg></span><span class="desc">Chat with AI</span></div> + <div class="quick-ref-item"><span class="cmd">search <q></span><span class="desc">Web search</span></div> + <div class="quick-ref-item"><span class="cmd">find <q></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">| pipe</span><span class="desc">Chain commands</span></div> + <div class="quick-ref-item"><span class="cmd">> 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 <entity> <fact></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><script></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 <text></code> searches all node text + tags (case-insensitive). Returns matching IDs and depth.</li> + <li><strong>Move nodes</strong> — <code>mindmap move <id> <new-parent-id></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 <id> <name></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 <name></code> still deletes a FILE (backward compat). New <code>mindmap remove <id></code> removes a NODE. <code>mindmap delete-file</code> is the explicit form.</li> + <li><strong>Import</strong> — <code>mindmap import <file></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 <name> [--into=<id>]</code> adds a saved mindmap under the current root (or specified parent). Conflict-free: just appends.</li> + <li><strong>Diff</strong> — <code>mindmap diff <name1> <name2></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 [<tag>]</code> shows all tags + counts, or all nodes with a specific tag.</li> + <li><strong>Clone</strong> — <code>mindmap clone <id></code> duplicates a node and its children with new IDs. Text gets " (copy)" suffix.</li> + <li><strong>Archive</strong> — <code>mindmap archive <name></code> saves to <code>/mindmaps/archive/<name>-<ts>.json</code>. Auto-cleans to last 20.</li> + <li><strong>Weight</strong> — <code>mindmap weight <id> <1-5></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 <file> > 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=<pat> --flat</code> flags, and shows aggregate summary (size, count, failed, disabled).</li> + <li><strong><code>module-info <name></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-<timestamp>.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 <url></code> for AI summary, or <code>nvfs-resolve <concept></code> to find related files".</li> + <li><strong>Backend script/style strip</strong> — Extract's regex fallback now strips <code><script></code>, <code><style></code>, <code><noscript></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&body=...&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 <expression></code> command evals JavaScript in page context. <code>js-run <path></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 <src-dir> <out.tar[.gz]></code> bundles a VFS directory; <code>untar <path.tar[.gz]> [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 & 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 <seconds></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 <name></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>></code>, <code>>></code>, <code><</code>), chain (<code>&&</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 <path></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 <query></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'), +]); |