+757
-123
@@ -6,4 +6,5 @@ #!/usr/bin/env node | ||
| * https://clawfix.dev | ||
| * | ||
| * Usage: npx clawfix | ||
| * | ||
| * Usage: npx clawfix (interactive TUI) | ||
| * npx clawfix --scan (one-shot scan, legacy mode) | ||
| */ | ||
@@ -15,7 +16,8 @@ | ||
| import { join } from 'node:path'; | ||
| import { createHash } from 'node:crypto'; | ||
| import { createHash, randomUUID } from 'node:crypto'; | ||
| import { createInterface } from 'node:readline'; | ||
| // --- Config --- | ||
| const API_URL = process.env.CLAWFIX_API || 'https://clawfix.dev'; | ||
| const VERSION = '0.2.0'; | ||
| const VERSION = '0.6.0'; | ||
@@ -28,2 +30,3 @@ // --- Flags --- | ||
| const SHOW_HELP = args.includes('--help') || args.includes('-h'); | ||
| const ONE_SHOT = args.includes('--scan') || args.includes('--no-interactive') || DRY_RUN; | ||
@@ -39,2 +42,3 @@ // --- Colors --- | ||
| dim: s => `\x1b[2m${s}\x1b[0m`, | ||
| magenta: s => `\x1b[35m${s}\x1b[0m`, | ||
| }; | ||
@@ -59,6 +63,5 @@ | ||
| // Redact secrets from config | ||
| function sanitizeConfig(config) { | ||
| if (!config || typeof config !== 'object') return config; | ||
| const redact = (obj) => { | ||
@@ -77,3 +80,3 @@ if (typeof obj === 'string') { | ||
| } else if (k === 'env') { | ||
| continue; // Skip env block entirely | ||
| continue; | ||
| } else { | ||
@@ -87,46 +90,12 @@ result[k] = redact(v); | ||
| }; | ||
| return redact(config); | ||
| } | ||
| // --- Main --- | ||
| async function main() { | ||
| if (SHOW_HELP) { | ||
| console.log(` | ||
| 🦞 ClawFix v${VERSION} — AI-Powered OpenClaw Diagnostic | ||
| // ============================================================ | ||
| // collectDiagnostics() — reusable scan, returns { diagnostic, issues, summary } | ||
| // ============================================================ | ||
| async function collectDiagnostics({ quiet = false } = {}) { | ||
| const log = quiet ? () => {} : (...a) => console.log(...a); | ||
| Usage: npx clawfix [options] | ||
| Options: | ||
| --dry-run, -n Scan locally only — shows what would be collected, sends nothing | ||
| --show-data, -d Display the full diagnostic payload before asking to send | ||
| --yes, -y Skip confirmation prompt and send automatically | ||
| --help, -h Show this help message | ||
| Environment: | ||
| CLAWFIX_API Override API URL (default: https://clawfix.dev) | ||
| CLAWFIX_AUTO=1 Same as --yes | ||
| Security: | ||
| • All API keys, tokens, and passwords are automatically redacted | ||
| • Your hostname is SHA-256 hashed (only first 8 chars sent) | ||
| • No file contents are read (only existence checks) | ||
| • Nothing is sent without your explicit approval (unless --yes) | ||
| • Source code: https://github.com/arcaboteth/clawfix | ||
| Examples: | ||
| npx clawfix # Interactive scan + optional AI analysis | ||
| npx clawfix --dry-run # See what data would be collected (sends nothing) | ||
| npx clawfix --show-data # Show full payload before asking to send | ||
| npx clawfix --yes # Auto-send for CI/scripting | ||
| `); | ||
| return; | ||
| } | ||
| console.log(''); | ||
| console.log(c.cyan(`🦞 ClawFix v${VERSION} — AI-Powered OpenClaw Diagnostic`)); | ||
| if (DRY_RUN) console.log(c.yellow(' 🔍 DRY RUN MODE — nothing will be sent')); | ||
| console.log(c.cyan('━'.repeat(50))); | ||
| console.log(''); | ||
| // --- Detect OpenClaw --- | ||
@@ -136,4 +105,4 @@ const home = homedir(); | ||
| await exists(join(home, '.config', 'openclaw')) ? join(home, '.config', 'openclaw') : null; | ||
| const openclawBin = run('which openclaw') || | ||
| const openclawBin = run('which openclaw') || | ||
| (await exists('/opt/homebrew/bin/openclaw') ? '/opt/homebrew/bin/openclaw' : '') || | ||
@@ -145,14 +114,12 @@ (await exists('/usr/local/bin/openclaw') ? '/usr/local/bin/openclaw' : ''); | ||
| if (!openclawBin && !openclawDir) { | ||
| console.log(c.red('❌ OpenClaw not found on this system.')); | ||
| console.log('Make sure OpenClaw is installed: https://openclaw.ai'); | ||
| process.exit(1); | ||
| return { error: 'OpenClaw not found on this system.' }; | ||
| } | ||
| console.log(c.green('✅ OpenClaw found')); | ||
| if (openclawBin) console.log(` Binary: ${openclawBin}`); | ||
| if (openclawDir) console.log(` Config: ${openclawDir}`); | ||
| log(c.green('✅ OpenClaw found')); | ||
| if (openclawBin) log(` Binary: ${openclawBin}`); | ||
| if (openclawDir) log(` Config: ${openclawDir}`); | ||
| // --- System Info --- | ||
| console.log(''); | ||
| console.log(c.blue('📋 Collecting system information...')); | ||
| log(''); | ||
| log(c.blue('📋 Collecting system information...')); | ||
@@ -171,9 +138,9 @@ const osName = platform(); | ||
| console.log(` OS: ${osName} ${osVersion} (${osArch})`); | ||
| console.log(` Node: ${nodeVersion}`); | ||
| console.log(` OpenClaw: ${ocVersion || 'not found'}`); | ||
| log(` OS: ${osName} ${osVersion} (${osArch})`); | ||
| log(` Node: ${nodeVersion}`); | ||
| log(` OpenClaw: ${ocVersion || 'not found'}`); | ||
| // --- Read Config --- | ||
| console.log(''); | ||
| console.log(c.blue('🔒 Reading config (secrets will be redacted)...')); | ||
| log(''); | ||
| log(c.blue('🔒 Reading config (secrets will be redacted)...')); | ||
@@ -186,10 +153,10 @@ let config = null; | ||
| sanitizedConfig = sanitizeConfig(config) || {}; | ||
| console.log(c.green(' ✅ Config read and sanitized')); | ||
| log(c.green(' ✅ Config read and sanitized')); | ||
| } else { | ||
| console.log(c.yellow(' ⚠️ No config file found')); | ||
| log(c.yellow(' ⚠️ No config file found')); | ||
| } | ||
| // --- Gateway Status --- | ||
| console.log(''); | ||
| console.log(c.blue('🔌 Checking gateway status...')); | ||
| log(''); | ||
| log(c.blue('🔌 Checking gateway status...')); | ||
@@ -204,15 +171,17 @@ let gatewayStatus = 'unknown'; | ||
| // Extract the actual status line, not config warnings | ||
| const statusLine = gatewayStatus.split('\n').find(l => /runtime:|listening|running|stopped|not running/i.test(l)) | ||
| || gatewayStatus.split('\n')[0]; | ||
| console.log(` Status: ${statusLine.trim()}`); | ||
| if (gatewayPid) console.log(` PID: ${gatewayPid}`); | ||
| console.log(` Port: ${gatewayPort}`); | ||
| log(` Status: ${statusLine.trim()}`); | ||
| if (gatewayPid) log(` PID: ${gatewayPid}`); | ||
| log(` Port: ${gatewayPort}`); | ||
| // --- Logs --- | ||
| console.log(''); | ||
| console.log(c.blue('📜 Reading recent logs...')); | ||
| log(''); | ||
| log(c.blue('📜 Reading recent logs...')); | ||
| let errorLogs = ''; | ||
| let stderrLogs = ''; | ||
| let gatewayLogTail = ''; | ||
| let errLogSizeMB = 0; | ||
| let logSizeMB = 0; | ||
@@ -224,4 +193,6 @@ const logPath = openclawDir ? join(openclawDir, 'logs', 'gateway.log') : null; | ||
| try { | ||
| const logContent = await readFile(logPath, 'utf8'); | ||
| const lines = logContent.split('\n'); | ||
| const logStat = await stat(logPath); | ||
| logSizeMB = Math.round(logStat.size / 1024 / 1024); | ||
| const tailContent = run(`tail -500 "${logPath}" 2>/dev/null`); | ||
| const lines = tailContent.split('\n'); | ||
| errorLogs = lines | ||
@@ -231,3 +202,7 @@ .filter(l => /error|warn|fail|crash|EADDRINUSE|EACCES/i.test(l)) | ||
| .join('\n'); | ||
| console.log(c.green(` ✅ Gateway log found (${lines.length} lines)`)); | ||
| gatewayLogTail = lines | ||
| .filter(l => /signal SIGTERM|listening.*PID|config change detected.*reload|update available/i.test(l)) | ||
| .slice(-20) | ||
| .join('\n'); | ||
| log(c.green(` ✅ Gateway log found (${logSizeMB}MB, read last 500 lines)`)); | ||
| } catch {} | ||
@@ -238,10 +213,72 @@ } | ||
| try { | ||
| stderrLogs = (await readFile(errLogPath, 'utf8')).split('\n').slice(-50).join('\n'); | ||
| console.log(c.green(' ✅ Error log found')); | ||
| const errStat = await stat(errLogPath); | ||
| errLogSizeMB = Math.round(errStat.size / 1024 / 1024); | ||
| stderrLogs = run(`tail -200 "${errLogPath}" 2>/dev/null`); | ||
| const icon = errLogSizeMB > 50 ? c.yellow('⚠️') : c.green('✅'); | ||
| log(` ${icon} Error log found (${errLogSizeMB}MB${errLogSizeMB > 50 ? ' — OVERSIZED!' : ''})`); | ||
| } catch {} | ||
| } | ||
| // --- Service Health --- | ||
| log(''); | ||
| log(c.blue('🔧 Checking service health...')); | ||
| let serviceHealth = {}; | ||
| const isMac = osName === 'darwin'; | ||
| const isLinux = osName === 'linux'; | ||
| if (isMac) { | ||
| const uid = run('id -u'); | ||
| const launchdInfo = run(`launchctl print gui/${uid}/ai.openclaw.gateway 2>/dev/null`); | ||
| if (launchdInfo) { | ||
| const runsMatch = launchdInfo.match(/runs = (\d+)/); | ||
| const pidMatch = launchdInfo.match(/pid = (\d+)/); | ||
| const stateMatch = launchdInfo.match(/state = (running|waiting|not running)/); | ||
| const exitCodeMatch = launchdInfo.match(/last exit code = (\d+)/); | ||
| serviceHealth = { | ||
| manager: 'launchd', | ||
| runs: runsMatch ? parseInt(runsMatch[1]) : 0, | ||
| pid: pidMatch ? parseInt(pidMatch[1]) : 0, | ||
| state: stateMatch ? stateMatch[1] : 'unknown', | ||
| lastExitCode: exitCodeMatch ? parseInt(exitCodeMatch[1]) : null, | ||
| }; | ||
| if (serviceHealth.pid) { | ||
| const elapsed = run(`ps -p ${serviceHealth.pid} -o etime= 2>/dev/null`).trim(); | ||
| serviceHealth.uptimeStr = elapsed; | ||
| const parts = elapsed.replace(/-/g, ':').split(':').reverse().map(Number); | ||
| serviceHealth.uptimeSeconds = (parts[0] || 0) + (parts[1] || 0) * 60 + (parts[2] || 0) * 3600 + (parts[3] || 0) * 86400; | ||
| } | ||
| const runsIcon = serviceHealth.runs > 2 ? c.yellow('⚠️') : c.green('✅'); | ||
| log(` ${runsIcon} LaunchAgent: ${serviceHealth.state} (${serviceHealth.runs} run(s), PID ${serviceHealth.pid || 'none'})`); | ||
| if (serviceHealth.uptimeStr) log(` Uptime: ${serviceHealth.uptimeStr}`); | ||
| if (serviceHealth.runs > 2) log(c.yellow(` ⚠️ Multiple restarts detected — possible crash loop`)); | ||
| } else { | ||
| log(c.dim(' LaunchAgent not found')); | ||
| } | ||
| } else if (isLinux) { | ||
| const systemdInfo = run('systemctl show openclaw-gateway --property=NRestarts,ActiveState,SubState,ExecMainPID,ExecMainStartTimestamp 2>/dev/null'); | ||
| if (systemdInfo) { | ||
| const props = {}; | ||
| systemdInfo.split('\n').forEach(l => { | ||
| const [k, v] = l.split('=', 2); | ||
| if (k && v) props[k.trim()] = v.trim(); | ||
| }); | ||
| serviceHealth = { | ||
| manager: 'systemd', | ||
| nRestarts: parseInt(props.NRestarts) || 0, | ||
| state: props.ActiveState || 'unknown', | ||
| subState: props.SubState || 'unknown', | ||
| pid: parseInt(props.ExecMainPID) || 0, | ||
| }; | ||
| log(` systemd: ${serviceHealth.state}/${serviceHealth.subState} (${serviceHealth.nRestarts} restart(s))`); | ||
| } else { | ||
| log(c.dim(' systemd service not found')); | ||
| } | ||
| } else { | ||
| log(c.dim(' Service manager detection not available on this OS')); | ||
| } | ||
| // --- Plugins --- | ||
| console.log(''); | ||
| console.log(c.blue('🔌 Checking plugins...')); | ||
| log(''); | ||
| log(c.blue('🔌 Checking plugins...')); | ||
@@ -251,8 +288,8 @@ const plugins = config?.plugins?.entries || {}; | ||
| const icon = cfg.enabled === false ? '❌' : '✅'; | ||
| console.log(` ${icon} ${name}`); | ||
| log(` ${icon} ${name}`); | ||
| } | ||
| // --- Workspace --- | ||
| console.log(''); | ||
| console.log(c.blue('📁 Checking workspace...')); | ||
| log(''); | ||
| log(c.blue('📁 Checking workspace...')); | ||
@@ -282,13 +319,14 @@ const workspaceDir = config?.agents?.defaults?.workspace || ''; | ||
| console.log(` Path: ${workspaceDir}`); | ||
| console.log(` Files: ${mdFiles} .md files`); | ||
| console.log(` Memory: ${memoryFiles} daily notes`); | ||
| console.log(` SOUL.md: ${hasSoul}`); | ||
| console.log(` AGENTS.md: ${hasAgents}`); | ||
| log(` Path: ${workspaceDir}`); | ||
| log(` Files: ${mdFiles} .md files`); | ||
| log(` Memory: ${memoryFiles} daily notes`); | ||
| log(` SOUL.md: ${hasSoul}`); | ||
| log(` AGENTS.md: ${hasAgents}`); | ||
| } | ||
| // --- Check Ports --- | ||
| console.log(''); | ||
| console.log(c.blue('🔗 Checking port availability...')); | ||
| log(''); | ||
| log(c.blue('🔗 Checking port availability...')); | ||
| const portResults = {}; | ||
| const checkPort = (port, name) => { | ||
@@ -298,6 +336,8 @@ const inUse = run(`lsof -i :${port} 2>/dev/null | grep LISTEN`) || | ||
| if (inUse) { | ||
| console.log(c.yellow(` ⚠️ Port ${port} (${name}) — IN USE`)); | ||
| log(c.yellow(` ⚠️ Port ${port} (${name}) — IN USE`)); | ||
| portResults[port] = true; | ||
| return true; | ||
| } else { | ||
| console.log(c.green(` ✅ Port ${port} (${name}) — available`)); | ||
| log(c.green(` ✅ Port ${port} (${name}) — available`)); | ||
| portResults[port] = false; | ||
| return false; | ||
@@ -312,11 +352,4 @@ } | ||
| // --- Local Issue Detection --- | ||
| console.log(''); | ||
| console.log(c.cyan('━'.repeat(50))); | ||
| console.log(c.bold('📊 Diagnostic Summary')); | ||
| console.log(c.cyan('━'.repeat(50))); | ||
| console.log(''); | ||
| const issues = []; | ||
| // Check actual gateway status — ignore config warnings in output | ||
| const gatewayRunning = /running.*pid|state active|listening/i.test(gatewayStatus); | ||
@@ -330,2 +363,36 @@ const gatewayFailed = /not running|failed to start|stopped|inactive/i.test(gatewayStatus); | ||
| } | ||
| const sigtermCount = (gatewayLogTail.match(/signal SIGTERM/gi) || []).length; | ||
| const restartCount = (gatewayLogTail.match(/listening.*PID/gi) || []).length; | ||
| if (config?.update?.auto?.enabled === true && (sigtermCount >= 2 || restartCount >= 3)) { | ||
| issues.push({ severity: 'critical', text: 'Auto-update causing gateway restart loop' }); | ||
| } else if (config?.update?.auto?.enabled === true) { | ||
| issues.push({ severity: 'medium', text: 'Auto-update enabled (risk of restart loops)' }); | ||
| } | ||
| const reloadCount = (gatewayLogTail.match(/config change detected.*evaluating reload/gi) || []).length; | ||
| if (reloadCount >= 3) { | ||
| issues.push({ severity: 'high', text: `Config reload cascade detected (${reloadCount} reloads in recent logs)` }); | ||
| } | ||
| if (serviceHealth.runs > 2 && (serviceHealth.uptimeSeconds || 0) < 300) { | ||
| issues.push({ severity: 'critical', text: `Gateway crash loop — ${serviceHealth.runs} restarts, only ${serviceHealth.uptimeStr} uptime` }); | ||
| } else if ((serviceHealth.nRestarts || 0) > 0) { | ||
| issues.push({ severity: 'high', text: `Gateway has restarted ${serviceHealth.nRestarts} time(s) (systemd)` }); | ||
| } | ||
| const handshakeSpam = (stderrLogs.match(/invalid handshake.*chrome-extension|closed before connect.*chrome-extension/gi) || []).length; | ||
| if (handshakeSpam >= 5) { | ||
| issues.push({ severity: 'medium', text: 'Browser Relay extension spamming invalid handshakes' }); | ||
| } | ||
| if (errLogSizeMB > 50) { | ||
| issues.push({ severity: 'medium', text: `Error log is ${errLogSizeMB}MB (should be <50MB)` }); | ||
| } | ||
| const matrixTimeouts = (stderrLogs.match(/ESOCKETTIMEDOUT/gi) || []).length; | ||
| if (matrixTimeouts >= 3) { | ||
| issues.push({ severity: 'low', text: 'Matrix sync timeouts spamming error log' }); | ||
| } | ||
| if (config?.plugins?.entries?.['openclaw-mem0']?.config?.enableGraph === true) { | ||
@@ -350,19 +417,2 @@ issues.push({ severity: 'high', text: 'Mem0 enableGraph requires Pro plan (will silently fail)' }); | ||
| if (issues.length === 0) { | ||
| console.log(c.green('✅ No issues detected! Your OpenClaw looks healthy.')); | ||
| } else { | ||
| console.log(c.red(`Found ${issues.length} issue(s):`)); | ||
| console.log(''); | ||
| for (const issue of issues) { | ||
| const icon = issue.severity === 'critical' ? c.red('❌') : | ||
| issue.severity === 'high' ? c.red('❌') : | ||
| c.yellow('⚠️'); | ||
| console.log(` ${icon} [${issue.severity.toUpperCase()}] ${issue.text}`); | ||
| } | ||
| } | ||
| console.log(''); | ||
| console.log(c.cyan('━'.repeat(50))); | ||
| console.log(''); | ||
| // --- Build Payload --- | ||
@@ -392,3 +442,7 @@ const diagnostic = { | ||
| stderr: stderrLogs, | ||
| gatewayLog: gatewayLogTail, | ||
| errLogSizeMB, | ||
| logSizeMB, | ||
| }, | ||
| service: serviceHealth, | ||
| workspace: { | ||
@@ -406,2 +460,68 @@ path: workspaceDir || 'unknown', | ||
| // Build summary for TUI display | ||
| const gatewayIcon = gatewayRunning ? c.green('✓') : c.red('✗'); | ||
| const gatewayLabel = gatewayRunning | ||
| ? `running (pid ${gatewayPid || '?'}, port ${gatewayPort})` | ||
| : 'not running'; | ||
| const configIcon = config ? c.green('✓') : c.yellow('⚠'); | ||
| const configLabel = config ? 'loaded' : 'not found'; | ||
| const issueIcon = issues.length === 0 ? c.green('✓') : c.yellow('⚠'); | ||
| const issueLabel = issues.length === 0 ? 'No issues' : `${issues.length} issue(s) detected`; | ||
| const summary = { | ||
| gateway: { icon: gatewayIcon, label: gatewayLabel }, | ||
| config: { icon: configIcon, label: configLabel }, | ||
| issues: { icon: issueIcon, label: issueLabel }, | ||
| node: nodeVersion, | ||
| os: `${osName === 'darwin' ? 'macOS' : osName} ${osVersion}`, | ||
| ocVersion: ocVersion || 'unknown', | ||
| }; | ||
| return { diagnostic, issues, summary }; | ||
| } | ||
| // ============================================================ | ||
| // One-shot mode (legacy: --scan, --dry-run, --no-interactive) | ||
| // ============================================================ | ||
| async function runOneShotMode() { | ||
| console.log(''); | ||
| console.log(c.cyan(`🦞 ClawFix v${VERSION} — AI-Powered OpenClaw Diagnostic`)); | ||
| if (DRY_RUN) console.log(c.yellow(' 🔍 DRY RUN MODE — nothing will be sent')); | ||
| console.log(c.cyan('━'.repeat(50))); | ||
| console.log(''); | ||
| const result = await collectDiagnostics(); | ||
| if (result.error) { | ||
| console.log(c.red(`❌ ${result.error}`)); | ||
| console.log('Make sure OpenClaw is installed: https://openclaw.ai'); | ||
| process.exit(1); | ||
| } | ||
| const { diagnostic, issues } = result; | ||
| // --- Display issues --- | ||
| console.log(''); | ||
| console.log(c.cyan('━'.repeat(50))); | ||
| console.log(c.bold('📊 Diagnostic Summary')); | ||
| console.log(c.cyan('━'.repeat(50))); | ||
| console.log(''); | ||
| if (issues.length === 0) { | ||
| console.log(c.green('✅ No issues detected! Your OpenClaw looks healthy.')); | ||
| } else { | ||
| console.log(c.red(`Found ${issues.length} issue(s):`)); | ||
| console.log(''); | ||
| for (const issue of issues) { | ||
| const icon = issue.severity === 'critical' ? c.red('❌') : | ||
| issue.severity === 'high' ? c.red('❌') : | ||
| c.yellow('⚠️'); | ||
| console.log(` ${icon} [${issue.severity.toUpperCase()}] ${issue.text}`); | ||
| } | ||
| } | ||
| console.log(''); | ||
| console.log(c.cyan('━'.repeat(50))); | ||
| console.log(''); | ||
| // --- Show collected data --- | ||
@@ -424,3 +544,3 @@ if (DRY_RUN || SHOW_DATA) { | ||
| console.log(c.cyan('🦞 ClawFix — made by Arca (arcabot.eth)')); | ||
| console.log(c.cyan(' https://clawfix.dev | https://x.com/arcaboteth')); | ||
| console.log(c.cyan(' https://clawfix.dev | https://x.com/arcabotai')); | ||
| console.log(''); | ||
@@ -430,3 +550,2 @@ return; | ||
| // --- Send for AI analysis --- | ||
| if (issues.length === 0) { | ||
@@ -437,3 +556,3 @@ console.log(c.green('Your OpenClaw is looking good! No fixes needed.')); | ||
| console.log(c.cyan(`🦞 ClawFix — made by Arca (arcabot.eth)`)); | ||
| console.log(c.cyan(` https://clawfix.dev | https://x.com/arcaboteth`)); | ||
| console.log(c.cyan(` https://clawfix.dev | https://x.com/arcabotai`)); | ||
| console.log(''); | ||
@@ -452,4 +571,3 @@ return; | ||
| if (!shouldSend) { | ||
| const readline = await import('node:readline'); | ||
| const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); | ||
| const rl = createInterface({ input: process.stdin, output: process.stdout }); | ||
| const answer = await new Promise(resolve => { | ||
@@ -491,3 +609,2 @@ rl.question('Send diagnostic for AI analysis? [y/N] ', resolve); | ||
| // Show known issues | ||
| if (result.knownIssues) { | ||
@@ -504,3 +621,2 @@ for (const issue of result.knownIssues) { | ||
| // Save fix script | ||
| if (result.fixScript) { | ||
@@ -531,6 +647,524 @@ const { writeFile } = await import('node:fs/promises'); | ||
| console.log(c.cyan('🦞 ClawFix — made by Arca (arcabot.eth)')); | ||
| console.log(c.cyan(' https://clawfix.dev | https://x.com/arcaboteth')); | ||
| console.log(c.cyan(' https://clawfix.dev | https://x.com/arcabotai')); | ||
| console.log(''); | ||
| } | ||
| // ============================================================ | ||
| // Interactive TUI mode (default) | ||
| // ============================================================ | ||
| async function runInteractiveMode() { | ||
| const conversationId = randomUUID(); | ||
| let diagnosticId = null; | ||
| let issues = []; | ||
| let diagnostic = null; | ||
| let summary = null; | ||
| let serverIssues = null; // issues returned from server after /api/diagnose | ||
| // --- Clear screen and show header --- | ||
| process.stdout.write('\x1b[2J\x1b[H'); | ||
| console.log(''); | ||
| console.log(c.cyan(`🦞 ClawFix v${VERSION}`)); | ||
| console.log(c.cyan('━'.repeat(48))); | ||
| console.log(''); | ||
| console.log(c.dim('Scanning your OpenClaw installation...')); | ||
| console.log(''); | ||
| // --- Auto-scan on startup --- | ||
| const scanResult = await collectDiagnostics({ quiet: true }); | ||
| if (scanResult.error) { | ||
| console.log(c.red(`❌ ${scanResult.error}`)); | ||
| console.log('Make sure OpenClaw is installed: https://openclaw.ai'); | ||
| process.exit(1); | ||
| } | ||
| diagnostic = scanResult.diagnostic; | ||
| issues = scanResult.issues; | ||
| summary = scanResult.summary; | ||
| // --- Send diagnostic to server for AI context --- | ||
| try { | ||
| const resp = await fetch(`${API_URL}/api/diagnose`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(diagnostic), | ||
| }); | ||
| if (resp.ok) { | ||
| const data = await resp.json(); | ||
| diagnosticId = data.fixId; | ||
| serverIssues = data.knownIssues || []; | ||
| } | ||
| } catch { | ||
| // Server unavailable — continue in local-only mode | ||
| } | ||
| // --- Render TUI --- | ||
| renderStatus(summary, issues, serverIssues); | ||
| // --- Start interactive prompt --- | ||
| const rl = createInterface({ | ||
| input: process.stdin, | ||
| output: process.stdout, | ||
| prompt: `${c.cyan('clawfix')}${c.dim('>')} `, | ||
| terminal: true, | ||
| }); | ||
| rl.prompt(); | ||
| rl.on('line', async (line) => { | ||
| const input = line.trim(); | ||
| if (!input) { | ||
| // Empty enter → show issues summary | ||
| renderIssues(issues, serverIssues); | ||
| rl.prompt(); | ||
| return; | ||
| } | ||
| // --- Built-in commands --- | ||
| if (/^(exit|quit|q)$/i.test(input)) { | ||
| console.log(''); | ||
| console.log(c.cyan('🦞 ClawFix — made by Arca (arcabot.eth)')); | ||
| console.log(c.cyan(' https://clawfix.dev')); | ||
| console.log(''); | ||
| process.exit(0); | ||
| } | ||
| if (/^(help|\?)$/i.test(input)) { | ||
| renderHelp(); | ||
| rl.prompt(); | ||
| return; | ||
| } | ||
| if (/^(scan|rescan)$/i.test(input)) { | ||
| console.log(''); | ||
| console.log(c.dim('Rescanning...')); | ||
| console.log(''); | ||
| const result = await collectDiagnostics({ quiet: true }); | ||
| if (!result.error) { | ||
| diagnostic = result.diagnostic; | ||
| issues = result.issues; | ||
| summary = result.summary; | ||
| // Re-send to server | ||
| try { | ||
| const resp = await fetch(`${API_URL}/api/diagnose`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(diagnostic), | ||
| }); | ||
| if (resp.ok) { | ||
| const data = await resp.json(); | ||
| diagnosticId = data.fixId; | ||
| serverIssues = data.knownIssues || []; | ||
| } | ||
| } catch {} | ||
| } | ||
| renderStatus(summary, issues, serverIssues); | ||
| rl.prompt(); | ||
| return; | ||
| } | ||
| if (/^issues?$/i.test(input)) { | ||
| renderIssues(issues, serverIssues); | ||
| rl.prompt(); | ||
| return; | ||
| } | ||
| if (/^status$/i.test(input)) { | ||
| renderStatus(summary, issues, serverIssues); | ||
| rl.prompt(); | ||
| return; | ||
| } | ||
| // fix <id> — show details about a detected issue | ||
| const fixMatch = input.match(/^fix\s+(\d+)$/i); | ||
| if (fixMatch) { | ||
| const idx = parseInt(fixMatch[1]) - 1; | ||
| const allIssues = mergeIssues(issues, serverIssues); | ||
| if (idx < 0 || idx >= allIssues.length) { | ||
| console.log(c.red(` No issue #${fixMatch[1]}. Use ${c.cyan('issues')} to see the list.`)); | ||
| } else { | ||
| const issue = allIssues[idx]; | ||
| console.log(''); | ||
| console.log(c.bold(` Issue #${idx + 1}: ${issue.title || issue.text}`)); | ||
| console.log(` Severity: ${severityColor(issue.severity)}`); | ||
| if (issue.description) console.log(` ${issue.description}`); | ||
| if (issue.fix) { | ||
| console.log(''); | ||
| console.log(c.dim(' Fix script:')); | ||
| console.log(c.dim(' ─────────────────────────────')); | ||
| for (const line of issue.fix.split('\n').slice(0, 15)) { | ||
| console.log(` ${c.dim(line)}`); | ||
| } | ||
| if (issue.fix.split('\n').length > 15) { | ||
| console.log(c.dim(` ... (${issue.fix.split('\n').length - 15} more lines)`)); | ||
| } | ||
| console.log(c.dim(' ─────────────────────────────')); | ||
| console.log(''); | ||
| console.log(` Run ${c.cyan(`apply ${idx + 1}`)} to apply this fix.`); | ||
| } | ||
| console.log(''); | ||
| } | ||
| rl.prompt(); | ||
| return; | ||
| } | ||
| // apply <id> — run fix with confirmation | ||
| const applyMatch = input.match(/^apply\s+(\d+)$/i); | ||
| if (applyMatch) { | ||
| const idx = parseInt(applyMatch[1]) - 1; | ||
| const allIssues = mergeIssues(issues, serverIssues); | ||
| if (idx < 0 || idx >= allIssues.length) { | ||
| console.log(c.red(` No issue #${applyMatch[1]}. Use ${c.cyan('issues')} to see the list.`)); | ||
| rl.prompt(); | ||
| return; | ||
| } | ||
| const issue = allIssues[idx]; | ||
| if (!issue.fix) { | ||
| console.log(c.yellow(` No automatic fix available for this issue.`)); | ||
| console.log(` Try asking about it: ${c.dim(`"how do I fix ${issue.title || issue.text}?"`)}`); | ||
| rl.prompt(); | ||
| return; | ||
| } | ||
| console.log(''); | ||
| console.log(c.bold(` Applying fix for: ${issue.title || issue.text}`)); | ||
| console.log(''); | ||
| for (const line of issue.fix.split('\n').slice(0, 10)) { | ||
| console.log(` ${c.dim(line)}`); | ||
| } | ||
| if (issue.fix.split('\n').length > 10) { | ||
| console.log(c.dim(` ... (${issue.fix.split('\n').length - 10} more lines)`)); | ||
| } | ||
| console.log(''); | ||
| const answer = await new Promise(resolve => { | ||
| rl.question(` ${c.yellow('Apply this fix?')} [y/N] `, resolve); | ||
| }); | ||
| if (/^y(es)?$/i.test(answer.trim())) { | ||
| console.log(''); | ||
| console.log(c.blue(' Running fix...')); | ||
| try { | ||
| const output = execSync(`bash -c ${JSON.stringify(issue.fix)}`, { | ||
| encoding: 'utf8', | ||
| timeout: 30000, | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| }); | ||
| if (output.trim()) { | ||
| for (const line of output.trim().split('\n')) { | ||
| console.log(` ${line}`); | ||
| } | ||
| } | ||
| console.log(c.green(' ✅ Fix applied successfully.')); | ||
| console.log(c.dim(' Run "rescan" to verify.')); | ||
| } catch (err) { | ||
| console.log(c.red(` ❌ Fix failed: ${err.message}`)); | ||
| } | ||
| } else { | ||
| console.log(c.dim(' Cancelled.')); | ||
| } | ||
| console.log(''); | ||
| rl.prompt(); | ||
| return; | ||
| } | ||
| // --- Natural language → send to /chat --- | ||
| console.log(''); | ||
| await streamChat(input, diagnosticId, conversationId, rl); | ||
| console.log(''); | ||
| rl.prompt(); | ||
| }); | ||
| rl.on('close', () => { | ||
| console.log(''); | ||
| console.log(c.cyan('🦞 ClawFix — made by Arca (arcabot.eth)')); | ||
| console.log(c.cyan(' https://clawfix.dev')); | ||
| console.log(''); | ||
| process.exit(0); | ||
| }); | ||
| } | ||
| // ============================================================ | ||
| // TUI Rendering helpers | ||
| // ============================================================ | ||
| function renderStatus(summary, issues, serverIssues) { | ||
| process.stdout.write('\x1b[2J\x1b[H'); | ||
| console.log(''); | ||
| console.log(c.cyan(`🦞 ClawFix v${VERSION}`)); | ||
| console.log(c.cyan('━'.repeat(48))); | ||
| console.log(''); | ||
| console.log(c.bold('System Status:')); | ||
| console.log(` ${summary.gateway.icon} Gateway: ${summary.gateway.label}`); | ||
| console.log(` ${summary.config.icon} Config: ${summary.config.label}`); | ||
| console.log(` ${summary.issues.icon} ${summary.issues.label}`); | ||
| console.log(` ${c.green('✓')} Node: ${summary.node} | OS: ${summary.os}`); | ||
| console.log(''); | ||
| renderIssues(issues, serverIssues); | ||
| console.log(c.cyan('━'.repeat(48))); | ||
| console.log(c.dim(' Type naturally to chat, or: fix <#> | scan | apply <#> | help | exit')); | ||
| console.log(''); | ||
| } | ||
| function renderIssues(issues, serverIssues) { | ||
| const all = mergeIssues(issues, serverIssues); | ||
| if (all.length === 0) { | ||
| console.log(c.green(' ✅ No issues detected — looking healthy!')); | ||
| console.log(''); | ||
| return; | ||
| } | ||
| console.log(c.bold('Detected Issues:')); | ||
| for (let i = 0; i < all.length; i++) { | ||
| const issue = all[i]; | ||
| const sev = issue.severity || 'medium'; | ||
| const label = sev === 'critical' || sev === 'high' | ||
| ? c.red(`[${sev.toUpperCase()}]`) | ||
| : sev === 'medium' | ||
| ? c.yellow(`[${sev.toUpperCase()}]`) | ||
| : c.dim(`[${sev.toUpperCase()}]`); | ||
| console.log(` ${c.dim(`${i + 1}.`)} ${label} ${issue.title || issue.text}`); | ||
| } | ||
| console.log(''); | ||
| } | ||
| function renderHelp() { | ||
| console.log(''); | ||
| console.log(c.bold('Commands:')); | ||
| console.log(` ${c.cyan('fix <#>')} Show details + fix script for issue #`); | ||
| console.log(` ${c.cyan('apply <#>')} Apply the fix for issue # (with confirmation)`); | ||
| console.log(` ${c.cyan('scan')} Re-run diagnostics`); | ||
| console.log(` ${c.cyan('issues')} Show detected issues`); | ||
| console.log(` ${c.cyan('status')} Show system status`); | ||
| console.log(` ${c.cyan('help')} Show this help`); | ||
| console.log(` ${c.cyan('exit')} Quit ClawFix`); | ||
| console.log(''); | ||
| console.log(c.bold('Chat:')); | ||
| console.log(` Just type naturally — e.g. ${c.dim('"my discord bot isn\'t responding"')}`); | ||
| console.log(` ClawFix AI will analyze using your diagnostic context.`); | ||
| console.log(''); | ||
| } | ||
| /** | ||
| * Merge local CLI-detected issues with server-detected known issues. | ||
| * Server issues (from known-issues.js pattern matching) include fix scripts. | ||
| * Local issues are simpler {severity, text} objects. | ||
| * Deduplicate by rough text matching. | ||
| */ | ||
| function mergeIssues(localIssues, serverIssues) { | ||
| const merged = []; | ||
| const seen = new Set(); | ||
| // Server issues first (they have fix scripts) | ||
| if (serverIssues) { | ||
| for (const si of serverIssues) { | ||
| merged.push(si); | ||
| seen.add((si.title || '').toLowerCase()); | ||
| } | ||
| } | ||
| // Then local issues that aren't duplicated | ||
| for (const li of localIssues) { | ||
| const key = (li.text || '').toLowerCase(); | ||
| const isDup = [...seen].some(s => | ||
| s.includes(key.slice(0, 20)) || key.includes(s.slice(0, 20)) | ||
| ); | ||
| if (!isDup) { | ||
| merged.push(li); | ||
| } | ||
| } | ||
| return merged; | ||
| } | ||
| function severityColor(sev) { | ||
| if (sev === 'critical') return c.red(c.bold('CRITICAL')); | ||
| if (sev === 'high') return c.red('HIGH'); | ||
| if (sev === 'medium') return c.yellow('MEDIUM'); | ||
| return c.dim('LOW'); | ||
| } | ||
| // ============================================================ | ||
| // Chat streaming — SSE from /api/chat | ||
| // ============================================================ | ||
| async function streamChat(message, diagnosticId, conversationId, rl) { | ||
| // Pause readline so it doesn't interfere with output | ||
| rl.pause(); | ||
| process.stdout.write(c.dim(' thinking...')); | ||
| try { | ||
| const resp = await fetch(`${API_URL}/api/chat`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ diagnosticId, message, conversationId }), | ||
| }); | ||
| // Non-SSE fallback (e.g. AI not available) | ||
| const contentType = resp.headers.get('content-type') || ''; | ||
| if (contentType.includes('application/json')) { | ||
| const data = await resp.json(); | ||
| // Clear "thinking..." | ||
| process.stdout.write('\r\x1b[K'); | ||
| if (data.error) { | ||
| console.log(c.red(` ${data.error}`)); | ||
| } else { | ||
| wrapPrint(data.response || 'No response from AI.'); | ||
| } | ||
| rl.resume(); | ||
| return; | ||
| } | ||
| // SSE streaming | ||
| process.stdout.write('\r\x1b[K'); | ||
| process.stdout.write(' '); | ||
| const reader = resp.body.getReader(); | ||
| const decoder = new TextDecoder(); | ||
| let buffer = ''; | ||
| let col = 2; // Current column (2 for indent) | ||
| while (true) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
| buffer += decoder.decode(value, { stream: true }); | ||
| const lines = buffer.split('\n'); | ||
| buffer = lines.pop() || ''; | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || !trimmed.startsWith('data: ')) continue; | ||
| const data = trimmed.slice(6); | ||
| if (data === '[DONE]') break; | ||
| try { | ||
| const parsed = JSON.parse(data); | ||
| if (parsed.error) { | ||
| process.stdout.write(c.red(parsed.error)); | ||
| break; | ||
| } | ||
| if (parsed.content) { | ||
| // Word-wrap at ~76 cols | ||
| for (const ch of parsed.content) { | ||
| if (ch === '\n') { | ||
| process.stdout.write('\n '); | ||
| col = 2; | ||
| } else { | ||
| process.stdout.write(ch); | ||
| col++; | ||
| if (col > 76 && ch === ' ') { | ||
| process.stdout.write('\n '); | ||
| col = 2; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } catch {} | ||
| } | ||
| } | ||
| process.stdout.write('\n'); | ||
| } catch (err) { | ||
| process.stdout.write('\r\x1b[K'); | ||
| if (err.code === 'ECONNREFUSED' || err.cause?.code === 'ECONNREFUSED') { | ||
| console.log(c.yellow(' ClawFix server is unreachable. Chat requires an internet connection.')); | ||
| console.log(c.dim(' Local commands still work: fix <#>, apply <#>, scan, issues')); | ||
| } else { | ||
| console.log(c.red(` Connection error: ${err.message}`)); | ||
| } | ||
| } | ||
| rl.resume(); | ||
| } | ||
| /** | ||
| * Print text with 2-space indent and word wrapping. | ||
| */ | ||
| function wrapPrint(text) { | ||
| const width = 76; | ||
| for (const paragraph of text.split('\n')) { | ||
| if (!paragraph.trim()) { | ||
| console.log(''); | ||
| continue; | ||
| } | ||
| const words = paragraph.split(' '); | ||
| let line = ' '; | ||
| for (const word of words) { | ||
| if (line.length + word.length + 1 > width && line.trim()) { | ||
| console.log(line); | ||
| line = ' '; | ||
| } | ||
| line += (line.trim() ? ' ' : '') + word; | ||
| } | ||
| if (line.trim()) console.log(line); | ||
| } | ||
| } | ||
| // ============================================================ | ||
| // Main entry point | ||
| // ============================================================ | ||
| async function main() { | ||
| if (SHOW_HELP) { | ||
| console.log(` | ||
| 🦞 ClawFix v${VERSION} — AI-Powered OpenClaw Diagnostic | ||
| Usage: npx clawfix [options] | ||
| Modes: | ||
| (default) Interactive TUI — scan + chat + fix | ||
| --scan One-shot scan (legacy mode) | ||
| --no-interactive Same as --scan | ||
| Options: | ||
| --dry-run, -n Scan locally only — shows what would be collected, sends nothing | ||
| --show-data, -d Display the full diagnostic payload before asking to send | ||
| --yes, -y Skip confirmation prompt and send automatically | ||
| --help, -h Show this help message | ||
| Environment: | ||
| CLAWFIX_API Override API URL (default: https://clawfix.dev) | ||
| CLAWFIX_AUTO=1 Same as --yes | ||
| Interactive Commands: | ||
| fix <#> Show details + fix script for a detected issue | ||
| apply <#> Apply the fix (with confirmation) | ||
| scan Re-run diagnostics | ||
| issues Show detected issues | ||
| help Show help | ||
| exit Quit | ||
| Or just type naturally to chat with ClawFix AI. | ||
| Security: | ||
| • All API keys, tokens, and passwords are automatically redacted | ||
| • Your hostname is SHA-256 hashed (only first 8 chars sent) | ||
| • No file contents are read (only existence checks) | ||
| • Nothing is sent without your explicit approval (unless --yes) | ||
| • Source code: https://github.com/arcabotai/clawfix | ||
| Examples: | ||
| npx clawfix # Interactive TUI (default) | ||
| npx clawfix --scan # One-shot scan + AI analysis | ||
| npx clawfix --dry-run # See what data would be collected | ||
| npx clawfix --yes --scan # Auto-send for CI/scripting | ||
| `); | ||
| return; | ||
| } | ||
| if (ONE_SHOT) { | ||
| await runOneShotMode(); | ||
| } else { | ||
| await runInteractiveMode(); | ||
| } | ||
| } | ||
| main().catch(err => { | ||
@@ -537,0 +1171,0 @@ console.error(c.red(`Fatal error: ${err.message}`)); |
+3
-3
| { | ||
| "name": "clawfix", | ||
| "version": "0.2.1", | ||
| "version": "0.6.0", | ||
| "description": "AI-powered diagnostic and repair for OpenClaw installations", | ||
@@ -21,6 +21,6 @@ "bin": { | ||
| "type": "git", | ||
| "url": "https://github.com/arcaboteth/clawfix" | ||
| "url": "https://github.com/arcabotai/clawfix" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/arcaboteth/clawfix/issues" | ||
| "url": "https://github.com/arcabotai/clawfix/issues" | ||
| }, | ||
@@ -27,0 +27,0 @@ "author": "Arca <arca@arcabot.ai> (https://arcabot.ai)", |
+3
-3
@@ -27,3 +27,3 @@ # 🦞 ClawFix | ||
| - No telemetry, no tracking, no account required | ||
| - [Source code is open](https://github.com/arcaboteth/clawfix) — verify it yourself | ||
| - [Source code is open](https://github.com/arcabotai/clawfix) — verify it yourself | ||
@@ -55,4 +55,4 @@ ## Options | ||
| - **Website:** [clawfix.dev](https://clawfix.dev) | ||
| - **GitHub:** [arcaboteth/clawfix](https://github.com/arcaboteth/clawfix) | ||
| - **Issues:** [github.com/arcaboteth/clawfix/issues](https://github.com/arcaboteth/clawfix/issues) | ||
| - **GitHub:** [arcabotai/clawfix](https://github.com/arcabotai/clawfix) | ||
| - **Issues:** [github.com/arcabotai/clawfix/issues](https://github.com/arcabotai/clawfix/issues) | ||
| - **Made by:** [Arca](https://arcabot.ai) (arcabot.eth) | ||
@@ -59,0 +59,0 @@ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
42047
106.16%995
128.74%5
400%