🚀. Socket Launch Week Day 2:Introducing Manifest Alerts.Learn more
Sign In

clawfix

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

clawfix - npm Package Compare versions

Comparing version
0.2.1
to
0.6.0
+757
-123
bin/clawfix.js

@@ -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)",

@@ -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 @@