clawfix 0.2.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/clawfix.js +757 -123
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ That's it. ClawFix scans your OpenClaw setup, finds issues, and generates fix sc
|
|
|
24
24
|
- All secrets, tokens, and API keys are **automatically redacted** before leaving your machine
|
|
25
25
|
- Diagnostic data is only sent with your **explicit consent**
|
|
26
26
|
- No telemetry, no tracking, no account required
|
|
27
|
-
- [Source code is open](https://github.com/
|
|
27
|
+
- [Source code is open](https://github.com/arcabotai/clawfix) — verify it yourself
|
|
28
28
|
|
|
29
29
|
## Options
|
|
30
30
|
|
|
@@ -51,8 +51,8 @@ curl -sSL clawfix.dev/fix | bash
|
|
|
51
51
|
## Links
|
|
52
52
|
|
|
53
53
|
- **Website:** [clawfix.dev](https://clawfix.dev)
|
|
54
|
-
- **GitHub:** [
|
|
55
|
-
- **Issues:** [github.com/
|
|
54
|
+
- **GitHub:** [arcabotai/clawfix](https://github.com/arcabotai/clawfix)
|
|
55
|
+
- **Issues:** [github.com/arcabotai/clawfix/issues](https://github.com/arcabotai/clawfix/issues)
|
|
56
56
|
- **Made by:** [Arca](https://arcabot.ai) (arcabot.eth)
|
|
57
57
|
|
|
58
58
|
## License
|
package/bin/clawfix.js
CHANGED
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* ClawFix CLI — AI-powered OpenClaw diagnostic & repair
|
|
5
5
|
* https://clawfix.dev
|
|
6
|
-
*
|
|
7
|
-
* Usage: npx clawfix
|
|
6
|
+
*
|
|
7
|
+
* Usage: npx clawfix (interactive TUI)
|
|
8
|
+
* npx clawfix --scan (one-shot scan, legacy mode)
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { readFile, access, readdir, stat } from 'node:fs/promises';
|
|
11
12
|
import { execSync } from 'node:child_process';
|
|
12
13
|
import { homedir, platform, arch, release, hostname } from 'node:os';
|
|
13
14
|
import { join } from 'node:path';
|
|
14
|
-
import { createHash } from 'node:crypto';
|
|
15
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
16
|
+
import { createInterface } from 'node:readline';
|
|
15
17
|
|
|
16
18
|
// --- Config ---
|
|
17
19
|
const API_URL = process.env.CLAWFIX_API || 'https://clawfix.dev';
|
|
18
|
-
const VERSION = '0.
|
|
20
|
+
const VERSION = '0.6.0';
|
|
19
21
|
|
|
20
22
|
// --- Flags ---
|
|
21
23
|
const args = process.argv.slice(2);
|
|
@@ -23,6 +25,7 @@ const DRY_RUN = args.includes('--dry-run') || args.includes('-n');
|
|
|
23
25
|
const SHOW_DATA = args.includes('--show-data') || args.includes('-d');
|
|
24
26
|
const AUTO_SEND = process.env.CLAWFIX_AUTO === '1' || args.includes('--yes') || args.includes('-y');
|
|
25
27
|
const SHOW_HELP = args.includes('--help') || args.includes('-h');
|
|
28
|
+
const ONE_SHOT = args.includes('--scan') || args.includes('--no-interactive') || DRY_RUN;
|
|
26
29
|
|
|
27
30
|
// --- Colors ---
|
|
28
31
|
const c = {
|
|
@@ -33,6 +36,7 @@ const c = {
|
|
|
33
36
|
cyan: s => `\x1b[36m${s}\x1b[0m`,
|
|
34
37
|
bold: s => `\x1b[1m${s}\x1b[0m`,
|
|
35
38
|
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
39
|
+
magenta: s => `\x1b[35m${s}\x1b[0m`,
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
// --- Helpers ---
|
|
@@ -52,10 +56,9 @@ function hashStr(s) {
|
|
|
52
56
|
return createHash('sha256').update(s).digest('hex').slice(0, 8);
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
// Redact secrets from config
|
|
56
59
|
function sanitizeConfig(config) {
|
|
57
60
|
if (!config || typeof config !== 'object') return config;
|
|
58
|
-
|
|
61
|
+
|
|
59
62
|
const redact = (obj) => {
|
|
60
63
|
if (typeof obj === 'string') {
|
|
61
64
|
if (obj.length > 20 && /^(sk-|xai-|eyJ|ghp_|gho_|npm_|m0-|AIza|ntn_)/.test(obj)) return '***REDACTED***';
|
|
@@ -69,7 +72,7 @@ function sanitizeConfig(config) {
|
|
|
69
72
|
if (/key|token|secret|password|jwt|apikey|accesstoken/i.test(k)) {
|
|
70
73
|
result[k] = '***REDACTED***';
|
|
71
74
|
} else if (k === 'env') {
|
|
72
|
-
continue;
|
|
75
|
+
continue;
|
|
73
76
|
} else {
|
|
74
77
|
result[k] = redact(v);
|
|
75
78
|
}
|
|
@@ -78,74 +81,38 @@ function sanitizeConfig(config) {
|
|
|
78
81
|
}
|
|
79
82
|
return obj;
|
|
80
83
|
};
|
|
81
|
-
|
|
84
|
+
|
|
82
85
|
return redact(config);
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
Usage: npx clawfix [options]
|
|
92
|
-
|
|
93
|
-
Options:
|
|
94
|
-
--dry-run, -n Scan locally only — shows what would be collected, sends nothing
|
|
95
|
-
--show-data, -d Display the full diagnostic payload before asking to send
|
|
96
|
-
--yes, -y Skip confirmation prompt and send automatically
|
|
97
|
-
--help, -h Show this help message
|
|
98
|
-
|
|
99
|
-
Environment:
|
|
100
|
-
CLAWFIX_API Override API URL (default: https://clawfix.dev)
|
|
101
|
-
CLAWFIX_AUTO=1 Same as --yes
|
|
102
|
-
|
|
103
|
-
Security:
|
|
104
|
-
• All API keys, tokens, and passwords are automatically redacted
|
|
105
|
-
• Your hostname is SHA-256 hashed (only first 8 chars sent)
|
|
106
|
-
• No file contents are read (only existence checks)
|
|
107
|
-
• Nothing is sent without your explicit approval (unless --yes)
|
|
108
|
-
• Source code: https://github.com/arcaboteth/clawfix
|
|
109
|
-
|
|
110
|
-
Examples:
|
|
111
|
-
npx clawfix # Interactive scan + optional AI analysis
|
|
112
|
-
npx clawfix --dry-run # See what data would be collected (sends nothing)
|
|
113
|
-
npx clawfix --show-data # Show full payload before asking to send
|
|
114
|
-
npx clawfix --yes # Auto-send for CI/scripting
|
|
115
|
-
`);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
console.log('');
|
|
120
|
-
console.log(c.cyan(`🦞 ClawFix v${VERSION} — AI-Powered OpenClaw Diagnostic`));
|
|
121
|
-
if (DRY_RUN) console.log(c.yellow(' 🔍 DRY RUN MODE — nothing will be sent'));
|
|
122
|
-
console.log(c.cyan('━'.repeat(50)));
|
|
123
|
-
console.log('');
|
|
88
|
+
// ============================================================
|
|
89
|
+
// collectDiagnostics() — reusable scan, returns { diagnostic, issues, summary }
|
|
90
|
+
// ============================================================
|
|
91
|
+
async function collectDiagnostics({ quiet = false } = {}) {
|
|
92
|
+
const log = quiet ? () => {} : (...a) => console.log(...a);
|
|
124
93
|
|
|
125
94
|
// --- Detect OpenClaw ---
|
|
126
95
|
const home = homedir();
|
|
127
96
|
const openclawDir = await exists(join(home, '.openclaw')) ? join(home, '.openclaw') :
|
|
128
97
|
await exists(join(home, '.config', 'openclaw')) ? join(home, '.config', 'openclaw') : null;
|
|
129
|
-
|
|
130
|
-
const openclawBin = run('which openclaw') ||
|
|
98
|
+
|
|
99
|
+
const openclawBin = run('which openclaw') ||
|
|
131
100
|
(await exists('/opt/homebrew/bin/openclaw') ? '/opt/homebrew/bin/openclaw' : '') ||
|
|
132
101
|
(await exists('/usr/local/bin/openclaw') ? '/usr/local/bin/openclaw' : '');
|
|
133
102
|
|
|
134
103
|
const configPath = openclawDir ? join(openclawDir, 'openclaw.json') : null;
|
|
135
104
|
|
|
136
105
|
if (!openclawBin && !openclawDir) {
|
|
137
|
-
|
|
138
|
-
console.log('Make sure OpenClaw is installed: https://openclaw.ai');
|
|
139
|
-
process.exit(1);
|
|
106
|
+
return { error: 'OpenClaw not found on this system.' };
|
|
140
107
|
}
|
|
141
108
|
|
|
142
|
-
|
|
143
|
-
if (openclawBin)
|
|
144
|
-
if (openclawDir)
|
|
109
|
+
log(c.green('✅ OpenClaw found'));
|
|
110
|
+
if (openclawBin) log(` Binary: ${openclawBin}`);
|
|
111
|
+
if (openclawDir) log(` Config: ${openclawDir}`);
|
|
145
112
|
|
|
146
113
|
// --- System Info ---
|
|
147
|
-
|
|
148
|
-
|
|
114
|
+
log('');
|
|
115
|
+
log(c.blue('📋 Collecting system information...'));
|
|
149
116
|
|
|
150
117
|
const osName = platform();
|
|
151
118
|
const osVersion = release();
|
|
@@ -159,13 +126,13 @@ Examples:
|
|
|
159
126
|
ocVersion = run(`"${openclawBin}" --version`);
|
|
160
127
|
}
|
|
161
128
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
129
|
+
log(` OS: ${osName} ${osVersion} (${osArch})`);
|
|
130
|
+
log(` Node: ${nodeVersion}`);
|
|
131
|
+
log(` OpenClaw: ${ocVersion || 'not found'}`);
|
|
165
132
|
|
|
166
133
|
// --- Read Config ---
|
|
167
|
-
|
|
168
|
-
|
|
134
|
+
log('');
|
|
135
|
+
log(c.blue('🔒 Reading config (secrets will be redacted)...'));
|
|
169
136
|
|
|
170
137
|
let config = null;
|
|
171
138
|
let sanitizedConfig = {};
|
|
@@ -173,14 +140,14 @@ Examples:
|
|
|
173
140
|
if (configPath && await exists(configPath)) {
|
|
174
141
|
config = await readJson(configPath);
|
|
175
142
|
sanitizedConfig = sanitizeConfig(config) || {};
|
|
176
|
-
|
|
143
|
+
log(c.green(' ✅ Config read and sanitized'));
|
|
177
144
|
} else {
|
|
178
|
-
|
|
145
|
+
log(c.yellow(' ⚠️ No config file found'));
|
|
179
146
|
}
|
|
180
147
|
|
|
181
148
|
// --- Gateway Status ---
|
|
182
|
-
|
|
183
|
-
|
|
149
|
+
log('');
|
|
150
|
+
log(c.blue('🔌 Checking gateway status...'));
|
|
184
151
|
|
|
185
152
|
let gatewayStatus = 'unknown';
|
|
186
153
|
if (openclawBin) {
|
|
@@ -190,55 +157,125 @@ Examples:
|
|
|
190
157
|
const gatewayPort = config?.gateway?.port || 18789;
|
|
191
158
|
const gatewayPid = run('pgrep -f "openclaw.*gateway"') || '';
|
|
192
159
|
|
|
193
|
-
// Extract the actual status line, not config warnings
|
|
194
160
|
const statusLine = gatewayStatus.split('\n').find(l => /runtime:|listening|running|stopped|not running/i.test(l))
|
|
195
161
|
|| gatewayStatus.split('\n')[0];
|
|
196
|
-
|
|
197
|
-
if (gatewayPid)
|
|
198
|
-
|
|
162
|
+
log(` Status: ${statusLine.trim()}`);
|
|
163
|
+
if (gatewayPid) log(` PID: ${gatewayPid}`);
|
|
164
|
+
log(` Port: ${gatewayPort}`);
|
|
199
165
|
|
|
200
166
|
// --- Logs ---
|
|
201
|
-
|
|
202
|
-
|
|
167
|
+
log('');
|
|
168
|
+
log(c.blue('📜 Reading recent logs...'));
|
|
203
169
|
|
|
204
170
|
let errorLogs = '';
|
|
205
171
|
let stderrLogs = '';
|
|
172
|
+
let gatewayLogTail = '';
|
|
173
|
+
let errLogSizeMB = 0;
|
|
174
|
+
let logSizeMB = 0;
|
|
206
175
|
|
|
207
176
|
const logPath = openclawDir ? join(openclawDir, 'logs', 'gateway.log') : null;
|
|
208
177
|
const errLogPath = openclawDir ? join(openclawDir, 'logs', 'gateway.err.log') : null;
|
|
209
178
|
|
|
210
179
|
if (logPath && await exists(logPath)) {
|
|
211
180
|
try {
|
|
212
|
-
const
|
|
213
|
-
|
|
181
|
+
const logStat = await stat(logPath);
|
|
182
|
+
logSizeMB = Math.round(logStat.size / 1024 / 1024);
|
|
183
|
+
const tailContent = run(`tail -500 "${logPath}" 2>/dev/null`);
|
|
184
|
+
const lines = tailContent.split('\n');
|
|
214
185
|
errorLogs = lines
|
|
215
186
|
.filter(l => /error|warn|fail|crash|EADDRINUSE|EACCES/i.test(l))
|
|
216
187
|
.slice(-30)
|
|
217
188
|
.join('\n');
|
|
218
|
-
|
|
189
|
+
gatewayLogTail = lines
|
|
190
|
+
.filter(l => /signal SIGTERM|listening.*PID|config change detected.*reload|update available/i.test(l))
|
|
191
|
+
.slice(-20)
|
|
192
|
+
.join('\n');
|
|
193
|
+
log(c.green(` ✅ Gateway log found (${logSizeMB}MB, read last 500 lines)`));
|
|
219
194
|
} catch {}
|
|
220
195
|
}
|
|
221
196
|
|
|
222
197
|
if (errLogPath && await exists(errLogPath)) {
|
|
223
198
|
try {
|
|
224
|
-
|
|
225
|
-
|
|
199
|
+
const errStat = await stat(errLogPath);
|
|
200
|
+
errLogSizeMB = Math.round(errStat.size / 1024 / 1024);
|
|
201
|
+
stderrLogs = run(`tail -200 "${errLogPath}" 2>/dev/null`);
|
|
202
|
+
const icon = errLogSizeMB > 50 ? c.yellow('⚠️') : c.green('✅');
|
|
203
|
+
log(` ${icon} Error log found (${errLogSizeMB}MB${errLogSizeMB > 50 ? ' — OVERSIZED!' : ''})`);
|
|
226
204
|
} catch {}
|
|
227
205
|
}
|
|
228
206
|
|
|
207
|
+
// --- Service Health ---
|
|
208
|
+
log('');
|
|
209
|
+
log(c.blue('🔧 Checking service health...'));
|
|
210
|
+
|
|
211
|
+
let serviceHealth = {};
|
|
212
|
+
const isMac = osName === 'darwin';
|
|
213
|
+
const isLinux = osName === 'linux';
|
|
214
|
+
|
|
215
|
+
if (isMac) {
|
|
216
|
+
const uid = run('id -u');
|
|
217
|
+
const launchdInfo = run(`launchctl print gui/${uid}/ai.openclaw.gateway 2>/dev/null`);
|
|
218
|
+
if (launchdInfo) {
|
|
219
|
+
const runsMatch = launchdInfo.match(/runs = (\d+)/);
|
|
220
|
+
const pidMatch = launchdInfo.match(/pid = (\d+)/);
|
|
221
|
+
const stateMatch = launchdInfo.match(/state = (running|waiting|not running)/);
|
|
222
|
+
const exitCodeMatch = launchdInfo.match(/last exit code = (\d+)/);
|
|
223
|
+
serviceHealth = {
|
|
224
|
+
manager: 'launchd',
|
|
225
|
+
runs: runsMatch ? parseInt(runsMatch[1]) : 0,
|
|
226
|
+
pid: pidMatch ? parseInt(pidMatch[1]) : 0,
|
|
227
|
+
state: stateMatch ? stateMatch[1] : 'unknown',
|
|
228
|
+
lastExitCode: exitCodeMatch ? parseInt(exitCodeMatch[1]) : null,
|
|
229
|
+
};
|
|
230
|
+
if (serviceHealth.pid) {
|
|
231
|
+
const elapsed = run(`ps -p ${serviceHealth.pid} -o etime= 2>/dev/null`).trim();
|
|
232
|
+
serviceHealth.uptimeStr = elapsed;
|
|
233
|
+
const parts = elapsed.replace(/-/g, ':').split(':').reverse().map(Number);
|
|
234
|
+
serviceHealth.uptimeSeconds = (parts[0] || 0) + (parts[1] || 0) * 60 + (parts[2] || 0) * 3600 + (parts[3] || 0) * 86400;
|
|
235
|
+
}
|
|
236
|
+
const runsIcon = serviceHealth.runs > 2 ? c.yellow('⚠️') : c.green('✅');
|
|
237
|
+
log(` ${runsIcon} LaunchAgent: ${serviceHealth.state} (${serviceHealth.runs} run(s), PID ${serviceHealth.pid || 'none'})`);
|
|
238
|
+
if (serviceHealth.uptimeStr) log(` Uptime: ${serviceHealth.uptimeStr}`);
|
|
239
|
+
if (serviceHealth.runs > 2) log(c.yellow(` ⚠️ Multiple restarts detected — possible crash loop`));
|
|
240
|
+
} else {
|
|
241
|
+
log(c.dim(' LaunchAgent not found'));
|
|
242
|
+
}
|
|
243
|
+
} else if (isLinux) {
|
|
244
|
+
const systemdInfo = run('systemctl show openclaw-gateway --property=NRestarts,ActiveState,SubState,ExecMainPID,ExecMainStartTimestamp 2>/dev/null');
|
|
245
|
+
if (systemdInfo) {
|
|
246
|
+
const props = {};
|
|
247
|
+
systemdInfo.split('\n').forEach(l => {
|
|
248
|
+
const [k, v] = l.split('=', 2);
|
|
249
|
+
if (k && v) props[k.trim()] = v.trim();
|
|
250
|
+
});
|
|
251
|
+
serviceHealth = {
|
|
252
|
+
manager: 'systemd',
|
|
253
|
+
nRestarts: parseInt(props.NRestarts) || 0,
|
|
254
|
+
state: props.ActiveState || 'unknown',
|
|
255
|
+
subState: props.SubState || 'unknown',
|
|
256
|
+
pid: parseInt(props.ExecMainPID) || 0,
|
|
257
|
+
};
|
|
258
|
+
log(` systemd: ${serviceHealth.state}/${serviceHealth.subState} (${serviceHealth.nRestarts} restart(s))`);
|
|
259
|
+
} else {
|
|
260
|
+
log(c.dim(' systemd service not found'));
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
log(c.dim(' Service manager detection not available on this OS'));
|
|
264
|
+
}
|
|
265
|
+
|
|
229
266
|
// --- Plugins ---
|
|
230
|
-
|
|
231
|
-
|
|
267
|
+
log('');
|
|
268
|
+
log(c.blue('🔌 Checking plugins...'));
|
|
232
269
|
|
|
233
270
|
const plugins = config?.plugins?.entries || {};
|
|
234
271
|
for (const [name, cfg] of Object.entries(plugins)) {
|
|
235
272
|
const icon = cfg.enabled === false ? '❌' : '✅';
|
|
236
|
-
|
|
273
|
+
log(` ${icon} ${name}`);
|
|
237
274
|
}
|
|
238
275
|
|
|
239
276
|
// --- Workspace ---
|
|
240
|
-
|
|
241
|
-
|
|
277
|
+
log('');
|
|
278
|
+
log(c.blue('📁 Checking workspace...'));
|
|
242
279
|
|
|
243
280
|
const workspaceDir = config?.agents?.defaults?.workspace || '';
|
|
244
281
|
let mdFiles = 0;
|
|
@@ -263,25 +300,28 @@ Examples:
|
|
|
263
300
|
} catch {}
|
|
264
301
|
}
|
|
265
302
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
303
|
+
log(` Path: ${workspaceDir}`);
|
|
304
|
+
log(` Files: ${mdFiles} .md files`);
|
|
305
|
+
log(` Memory: ${memoryFiles} daily notes`);
|
|
306
|
+
log(` SOUL.md: ${hasSoul}`);
|
|
307
|
+
log(` AGENTS.md: ${hasAgents}`);
|
|
271
308
|
}
|
|
272
309
|
|
|
273
310
|
// --- Check Ports ---
|
|
274
|
-
|
|
275
|
-
|
|
311
|
+
log('');
|
|
312
|
+
log(c.blue('🔗 Checking port availability...'));
|
|
276
313
|
|
|
314
|
+
const portResults = {};
|
|
277
315
|
const checkPort = (port, name) => {
|
|
278
316
|
const inUse = run(`lsof -i :${port} 2>/dev/null | grep LISTEN`) ||
|
|
279
317
|
run(`ss -tlnp 2>/dev/null | grep :${port}`);
|
|
280
318
|
if (inUse) {
|
|
281
|
-
|
|
319
|
+
log(c.yellow(` ⚠️ Port ${port} (${name}) — IN USE`));
|
|
320
|
+
portResults[port] = true;
|
|
282
321
|
return true;
|
|
283
322
|
} else {
|
|
284
|
-
|
|
323
|
+
log(c.green(` ✅ Port ${port} (${name}) — available`));
|
|
324
|
+
portResults[port] = false;
|
|
285
325
|
return false;
|
|
286
326
|
}
|
|
287
327
|
};
|
|
@@ -291,15 +331,8 @@ Examples:
|
|
|
291
331
|
checkPort(18791, 'browser control');
|
|
292
332
|
|
|
293
333
|
// --- Local Issue Detection ---
|
|
294
|
-
console.log('');
|
|
295
|
-
console.log(c.cyan('━'.repeat(50)));
|
|
296
|
-
console.log(c.bold('📊 Diagnostic Summary'));
|
|
297
|
-
console.log(c.cyan('━'.repeat(50)));
|
|
298
|
-
console.log('');
|
|
299
|
-
|
|
300
334
|
const issues = [];
|
|
301
335
|
|
|
302
|
-
// Check actual gateway status — ignore config warnings in output
|
|
303
336
|
const gatewayRunning = /running.*pid|state active|listening/i.test(gatewayStatus);
|
|
304
337
|
const gatewayFailed = /not running|failed to start|stopped|inactive/i.test(gatewayStatus);
|
|
305
338
|
if (gatewayFailed || (!gatewayRunning && !/warning/i.test(gatewayStatus))) {
|
|
@@ -308,6 +341,40 @@ Examples:
|
|
|
308
341
|
if (/EADDRINUSE/i.test(errorLogs)) {
|
|
309
342
|
issues.push({ severity: 'critical', text: 'Port conflict detected' });
|
|
310
343
|
}
|
|
344
|
+
|
|
345
|
+
const sigtermCount = (gatewayLogTail.match(/signal SIGTERM/gi) || []).length;
|
|
346
|
+
const restartCount = (gatewayLogTail.match(/listening.*PID/gi) || []).length;
|
|
347
|
+
if (config?.update?.auto?.enabled === true && (sigtermCount >= 2 || restartCount >= 3)) {
|
|
348
|
+
issues.push({ severity: 'critical', text: 'Auto-update causing gateway restart loop' });
|
|
349
|
+
} else if (config?.update?.auto?.enabled === true) {
|
|
350
|
+
issues.push({ severity: 'medium', text: 'Auto-update enabled (risk of restart loops)' });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const reloadCount = (gatewayLogTail.match(/config change detected.*evaluating reload/gi) || []).length;
|
|
354
|
+
if (reloadCount >= 3) {
|
|
355
|
+
issues.push({ severity: 'high', text: `Config reload cascade detected (${reloadCount} reloads in recent logs)` });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (serviceHealth.runs > 2 && (serviceHealth.uptimeSeconds || 0) < 300) {
|
|
359
|
+
issues.push({ severity: 'critical', text: `Gateway crash loop — ${serviceHealth.runs} restarts, only ${serviceHealth.uptimeStr} uptime` });
|
|
360
|
+
} else if ((serviceHealth.nRestarts || 0) > 0) {
|
|
361
|
+
issues.push({ severity: 'high', text: `Gateway has restarted ${serviceHealth.nRestarts} time(s) (systemd)` });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const handshakeSpam = (stderrLogs.match(/invalid handshake.*chrome-extension|closed before connect.*chrome-extension/gi) || []).length;
|
|
365
|
+
if (handshakeSpam >= 5) {
|
|
366
|
+
issues.push({ severity: 'medium', text: 'Browser Relay extension spamming invalid handshakes' });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (errLogSizeMB > 50) {
|
|
370
|
+
issues.push({ severity: 'medium', text: `Error log is ${errLogSizeMB}MB (should be <50MB)` });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const matrixTimeouts = (stderrLogs.match(/ESOCKETTIMEDOUT/gi) || []).length;
|
|
374
|
+
if (matrixTimeouts >= 3) {
|
|
375
|
+
issues.push({ severity: 'low', text: 'Matrix sync timeouts spamming error log' });
|
|
376
|
+
}
|
|
377
|
+
|
|
311
378
|
if (config?.plugins?.entries?.['openclaw-mem0']?.config?.enableGraph === true) {
|
|
312
379
|
issues.push({ severity: 'high', text: 'Mem0 enableGraph requires Pro plan (will silently fail)' });
|
|
313
380
|
}
|
|
@@ -327,23 +394,6 @@ Examples:
|
|
|
327
394
|
issues.push({ severity: 'low', text: 'No memory files found' });
|
|
328
395
|
}
|
|
329
396
|
|
|
330
|
-
if (issues.length === 0) {
|
|
331
|
-
console.log(c.green('✅ No issues detected! Your OpenClaw looks healthy.'));
|
|
332
|
-
} else {
|
|
333
|
-
console.log(c.red(`Found ${issues.length} issue(s):`));
|
|
334
|
-
console.log('');
|
|
335
|
-
for (const issue of issues) {
|
|
336
|
-
const icon = issue.severity === 'critical' ? c.red('❌') :
|
|
337
|
-
issue.severity === 'high' ? c.red('❌') :
|
|
338
|
-
c.yellow('⚠️');
|
|
339
|
-
console.log(` ${icon} [${issue.severity.toUpperCase()}] ${issue.text}`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
console.log('');
|
|
344
|
-
console.log(c.cyan('━'.repeat(50)));
|
|
345
|
-
console.log('');
|
|
346
|
-
|
|
347
397
|
// --- Build Payload ---
|
|
348
398
|
const diagnostic = {
|
|
349
399
|
version: VERSION,
|
|
@@ -368,7 +418,11 @@ Examples:
|
|
|
368
418
|
logs: {
|
|
369
419
|
errors: errorLogs,
|
|
370
420
|
stderr: stderrLogs,
|
|
421
|
+
gatewayLog: gatewayLogTail,
|
|
422
|
+
errLogSizeMB,
|
|
423
|
+
logSizeMB,
|
|
371
424
|
},
|
|
425
|
+
service: serviceHealth,
|
|
372
426
|
workspace: {
|
|
373
427
|
path: workspaceDir || 'unknown',
|
|
374
428
|
mdFiles,
|
|
@@ -381,6 +435,72 @@ Examples:
|
|
|
381
435
|
},
|
|
382
436
|
};
|
|
383
437
|
|
|
438
|
+
// Build summary for TUI display
|
|
439
|
+
const gatewayIcon = gatewayRunning ? c.green('✓') : c.red('✗');
|
|
440
|
+
const gatewayLabel = gatewayRunning
|
|
441
|
+
? `running (pid ${gatewayPid || '?'}, port ${gatewayPort})`
|
|
442
|
+
: 'not running';
|
|
443
|
+
const configIcon = config ? c.green('✓') : c.yellow('⚠');
|
|
444
|
+
const configLabel = config ? 'loaded' : 'not found';
|
|
445
|
+
const issueIcon = issues.length === 0 ? c.green('✓') : c.yellow('⚠');
|
|
446
|
+
const issueLabel = issues.length === 0 ? 'No issues' : `${issues.length} issue(s) detected`;
|
|
447
|
+
|
|
448
|
+
const summary = {
|
|
449
|
+
gateway: { icon: gatewayIcon, label: gatewayLabel },
|
|
450
|
+
config: { icon: configIcon, label: configLabel },
|
|
451
|
+
issues: { icon: issueIcon, label: issueLabel },
|
|
452
|
+
node: nodeVersion,
|
|
453
|
+
os: `${osName === 'darwin' ? 'macOS' : osName} ${osVersion}`,
|
|
454
|
+
ocVersion: ocVersion || 'unknown',
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
return { diagnostic, issues, summary };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ============================================================
|
|
461
|
+
// One-shot mode (legacy: --scan, --dry-run, --no-interactive)
|
|
462
|
+
// ============================================================
|
|
463
|
+
async function runOneShotMode() {
|
|
464
|
+
console.log('');
|
|
465
|
+
console.log(c.cyan(`🦞 ClawFix v${VERSION} — AI-Powered OpenClaw Diagnostic`));
|
|
466
|
+
if (DRY_RUN) console.log(c.yellow(' 🔍 DRY RUN MODE — nothing will be sent'));
|
|
467
|
+
console.log(c.cyan('━'.repeat(50)));
|
|
468
|
+
console.log('');
|
|
469
|
+
|
|
470
|
+
const result = await collectDiagnostics();
|
|
471
|
+
|
|
472
|
+
if (result.error) {
|
|
473
|
+
console.log(c.red(`❌ ${result.error}`));
|
|
474
|
+
console.log('Make sure OpenClaw is installed: https://openclaw.ai');
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const { diagnostic, issues } = result;
|
|
479
|
+
|
|
480
|
+
// --- Display issues ---
|
|
481
|
+
console.log('');
|
|
482
|
+
console.log(c.cyan('━'.repeat(50)));
|
|
483
|
+
console.log(c.bold('📊 Diagnostic Summary'));
|
|
484
|
+
console.log(c.cyan('━'.repeat(50)));
|
|
485
|
+
console.log('');
|
|
486
|
+
|
|
487
|
+
if (issues.length === 0) {
|
|
488
|
+
console.log(c.green('✅ No issues detected! Your OpenClaw looks healthy.'));
|
|
489
|
+
} else {
|
|
490
|
+
console.log(c.red(`Found ${issues.length} issue(s):`));
|
|
491
|
+
console.log('');
|
|
492
|
+
for (const issue of issues) {
|
|
493
|
+
const icon = issue.severity === 'critical' ? c.red('❌') :
|
|
494
|
+
issue.severity === 'high' ? c.red('❌') :
|
|
495
|
+
c.yellow('⚠️');
|
|
496
|
+
console.log(` ${icon} [${issue.severity.toUpperCase()}] ${issue.text}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
console.log('');
|
|
501
|
+
console.log(c.cyan('━'.repeat(50)));
|
|
502
|
+
console.log('');
|
|
503
|
+
|
|
384
504
|
// --- Show collected data ---
|
|
385
505
|
if (DRY_RUN || SHOW_DATA) {
|
|
386
506
|
console.log('');
|
|
@@ -398,18 +518,17 @@ Examples:
|
|
|
398
518
|
console.log(c.cyan(' npx clawfix'));
|
|
399
519
|
console.log('');
|
|
400
520
|
console.log(c.cyan('🦞 ClawFix — made by Arca (arcabot.eth)'));
|
|
401
|
-
console.log(c.cyan(' https://clawfix.dev | https://x.com/
|
|
521
|
+
console.log(c.cyan(' https://clawfix.dev | https://x.com/arcabotai'));
|
|
402
522
|
console.log('');
|
|
403
523
|
return;
|
|
404
524
|
}
|
|
405
525
|
|
|
406
|
-
// --- Send for AI analysis ---
|
|
407
526
|
if (issues.length === 0) {
|
|
408
527
|
console.log(c.green('Your OpenClaw is looking good! No fixes needed.'));
|
|
409
528
|
console.log(`If you're still having issues, run with --show-data to see what would be collected.`);
|
|
410
529
|
console.log('');
|
|
411
530
|
console.log(c.cyan(`🦞 ClawFix — made by Arca (arcabot.eth)`));
|
|
412
|
-
console.log(c.cyan(` https://clawfix.dev | https://x.com/
|
|
531
|
+
console.log(c.cyan(` https://clawfix.dev | https://x.com/arcabotai`));
|
|
413
532
|
console.log('');
|
|
414
533
|
return;
|
|
415
534
|
}
|
|
@@ -423,8 +542,7 @@ Examples:
|
|
|
423
542
|
|
|
424
543
|
let shouldSend = AUTO_SEND;
|
|
425
544
|
if (!shouldSend) {
|
|
426
|
-
const
|
|
427
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
545
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
428
546
|
const answer = await new Promise(resolve => {
|
|
429
547
|
rl.question('Send diagnostic for AI analysis? [y/N] ', resolve);
|
|
430
548
|
});
|
|
@@ -461,7 +579,6 @@ Examples:
|
|
|
461
579
|
console.log(c.green(`✅ Diagnosis complete! Found ${result.issuesFound} issue(s).`));
|
|
462
580
|
console.log('');
|
|
463
581
|
|
|
464
|
-
// Show known issues
|
|
465
582
|
if (result.knownIssues) {
|
|
466
583
|
for (const issue of result.knownIssues) {
|
|
467
584
|
console.log(` ${issue.severity.toUpperCase()} — ${issue.title}: ${issue.description}`);
|
|
@@ -473,7 +590,6 @@ Examples:
|
|
|
473
590
|
console.log(result.analysis || 'Pattern matching only (no AI configured)');
|
|
474
591
|
console.log('');
|
|
475
592
|
|
|
476
|
-
// Save fix script
|
|
477
593
|
if (result.fixScript) {
|
|
478
594
|
const { writeFile } = await import('node:fs/promises');
|
|
479
595
|
const fixPath = `/tmp/clawfix-${fixId}.sh`;
|
|
@@ -499,8 +615,526 @@ Examples:
|
|
|
499
615
|
|
|
500
616
|
console.log('');
|
|
501
617
|
console.log(c.cyan('🦞 ClawFix — made by Arca (arcabot.eth)'));
|
|
502
|
-
console.log(c.cyan(' https://clawfix.dev | https://x.com/
|
|
618
|
+
console.log(c.cyan(' https://clawfix.dev | https://x.com/arcabotai'));
|
|
619
|
+
console.log('');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ============================================================
|
|
623
|
+
// Interactive TUI mode (default)
|
|
624
|
+
// ============================================================
|
|
625
|
+
async function runInteractiveMode() {
|
|
626
|
+
const conversationId = randomUUID();
|
|
627
|
+
let diagnosticId = null;
|
|
628
|
+
let issues = [];
|
|
629
|
+
let diagnostic = null;
|
|
630
|
+
let summary = null;
|
|
631
|
+
let serverIssues = null; // issues returned from server after /api/diagnose
|
|
632
|
+
|
|
633
|
+
// --- Clear screen and show header ---
|
|
634
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
635
|
+
|
|
636
|
+
console.log('');
|
|
637
|
+
console.log(c.cyan(`🦞 ClawFix v${VERSION}`));
|
|
638
|
+
console.log(c.cyan('━'.repeat(48)));
|
|
639
|
+
console.log('');
|
|
640
|
+
console.log(c.dim('Scanning your OpenClaw installation...'));
|
|
641
|
+
console.log('');
|
|
642
|
+
|
|
643
|
+
// --- Auto-scan on startup ---
|
|
644
|
+
const scanResult = await collectDiagnostics({ quiet: true });
|
|
645
|
+
|
|
646
|
+
if (scanResult.error) {
|
|
647
|
+
console.log(c.red(`❌ ${scanResult.error}`));
|
|
648
|
+
console.log('Make sure OpenClaw is installed: https://openclaw.ai');
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
diagnostic = scanResult.diagnostic;
|
|
653
|
+
issues = scanResult.issues;
|
|
654
|
+
summary = scanResult.summary;
|
|
655
|
+
|
|
656
|
+
// --- Send diagnostic to server for AI context ---
|
|
657
|
+
try {
|
|
658
|
+
const resp = await fetch(`${API_URL}/api/diagnose`, {
|
|
659
|
+
method: 'POST',
|
|
660
|
+
headers: { 'Content-Type': 'application/json' },
|
|
661
|
+
body: JSON.stringify(diagnostic),
|
|
662
|
+
});
|
|
663
|
+
if (resp.ok) {
|
|
664
|
+
const data = await resp.json();
|
|
665
|
+
diagnosticId = data.fixId;
|
|
666
|
+
serverIssues = data.knownIssues || [];
|
|
667
|
+
}
|
|
668
|
+
} catch {
|
|
669
|
+
// Server unavailable — continue in local-only mode
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// --- Render TUI ---
|
|
673
|
+
renderStatus(summary, issues, serverIssues);
|
|
674
|
+
|
|
675
|
+
// --- Start interactive prompt ---
|
|
676
|
+
const rl = createInterface({
|
|
677
|
+
input: process.stdin,
|
|
678
|
+
output: process.stdout,
|
|
679
|
+
prompt: `${c.cyan('clawfix')}${c.dim('>')} `,
|
|
680
|
+
terminal: true,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
rl.prompt();
|
|
684
|
+
|
|
685
|
+
rl.on('line', async (line) => {
|
|
686
|
+
const input = line.trim();
|
|
687
|
+
|
|
688
|
+
if (!input) {
|
|
689
|
+
// Empty enter → show issues summary
|
|
690
|
+
renderIssues(issues, serverIssues);
|
|
691
|
+
rl.prompt();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// --- Built-in commands ---
|
|
696
|
+
if (/^(exit|quit|q)$/i.test(input)) {
|
|
697
|
+
console.log('');
|
|
698
|
+
console.log(c.cyan('🦞 ClawFix — made by Arca (arcabot.eth)'));
|
|
699
|
+
console.log(c.cyan(' https://clawfix.dev'));
|
|
700
|
+
console.log('');
|
|
701
|
+
process.exit(0);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (/^(help|\?)$/i.test(input)) {
|
|
705
|
+
renderHelp();
|
|
706
|
+
rl.prompt();
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (/^(scan|rescan)$/i.test(input)) {
|
|
711
|
+
console.log('');
|
|
712
|
+
console.log(c.dim('Rescanning...'));
|
|
713
|
+
console.log('');
|
|
714
|
+
const result = await collectDiagnostics({ quiet: true });
|
|
715
|
+
if (!result.error) {
|
|
716
|
+
diagnostic = result.diagnostic;
|
|
717
|
+
issues = result.issues;
|
|
718
|
+
summary = result.summary;
|
|
719
|
+
|
|
720
|
+
// Re-send to server
|
|
721
|
+
try {
|
|
722
|
+
const resp = await fetch(`${API_URL}/api/diagnose`, {
|
|
723
|
+
method: 'POST',
|
|
724
|
+
headers: { 'Content-Type': 'application/json' },
|
|
725
|
+
body: JSON.stringify(diagnostic),
|
|
726
|
+
});
|
|
727
|
+
if (resp.ok) {
|
|
728
|
+
const data = await resp.json();
|
|
729
|
+
diagnosticId = data.fixId;
|
|
730
|
+
serverIssues = data.knownIssues || [];
|
|
731
|
+
}
|
|
732
|
+
} catch {}
|
|
733
|
+
}
|
|
734
|
+
renderStatus(summary, issues, serverIssues);
|
|
735
|
+
rl.prompt();
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (/^issues?$/i.test(input)) {
|
|
740
|
+
renderIssues(issues, serverIssues);
|
|
741
|
+
rl.prompt();
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (/^status$/i.test(input)) {
|
|
746
|
+
renderStatus(summary, issues, serverIssues);
|
|
747
|
+
rl.prompt();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// fix <id> — show details about a detected issue
|
|
752
|
+
const fixMatch = input.match(/^fix\s+(\d+)$/i);
|
|
753
|
+
if (fixMatch) {
|
|
754
|
+
const idx = parseInt(fixMatch[1]) - 1;
|
|
755
|
+
const allIssues = mergeIssues(issues, serverIssues);
|
|
756
|
+
if (idx < 0 || idx >= allIssues.length) {
|
|
757
|
+
console.log(c.red(` No issue #${fixMatch[1]}. Use ${c.cyan('issues')} to see the list.`));
|
|
758
|
+
} else {
|
|
759
|
+
const issue = allIssues[idx];
|
|
760
|
+
console.log('');
|
|
761
|
+
console.log(c.bold(` Issue #${idx + 1}: ${issue.title || issue.text}`));
|
|
762
|
+
console.log(` Severity: ${severityColor(issue.severity)}`);
|
|
763
|
+
if (issue.description) console.log(` ${issue.description}`);
|
|
764
|
+
if (issue.fix) {
|
|
765
|
+
console.log('');
|
|
766
|
+
console.log(c.dim(' Fix script:'));
|
|
767
|
+
console.log(c.dim(' ─────────────────────────────'));
|
|
768
|
+
for (const line of issue.fix.split('\n').slice(0, 15)) {
|
|
769
|
+
console.log(` ${c.dim(line)}`);
|
|
770
|
+
}
|
|
771
|
+
if (issue.fix.split('\n').length > 15) {
|
|
772
|
+
console.log(c.dim(` ... (${issue.fix.split('\n').length - 15} more lines)`));
|
|
773
|
+
}
|
|
774
|
+
console.log(c.dim(' ─────────────────────────────'));
|
|
775
|
+
console.log('');
|
|
776
|
+
console.log(` Run ${c.cyan(`apply ${idx + 1}`)} to apply this fix.`);
|
|
777
|
+
}
|
|
778
|
+
console.log('');
|
|
779
|
+
}
|
|
780
|
+
rl.prompt();
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// apply <id> — run fix with confirmation
|
|
785
|
+
const applyMatch = input.match(/^apply\s+(\d+)$/i);
|
|
786
|
+
if (applyMatch) {
|
|
787
|
+
const idx = parseInt(applyMatch[1]) - 1;
|
|
788
|
+
const allIssues = mergeIssues(issues, serverIssues);
|
|
789
|
+
if (idx < 0 || idx >= allIssues.length) {
|
|
790
|
+
console.log(c.red(` No issue #${applyMatch[1]}. Use ${c.cyan('issues')} to see the list.`));
|
|
791
|
+
rl.prompt();
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const issue = allIssues[idx];
|
|
796
|
+
if (!issue.fix) {
|
|
797
|
+
console.log(c.yellow(` No automatic fix available for this issue.`));
|
|
798
|
+
console.log(` Try asking about it: ${c.dim(`"how do I fix ${issue.title || issue.text}?"`)}`);
|
|
799
|
+
rl.prompt();
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
console.log('');
|
|
804
|
+
console.log(c.bold(` Applying fix for: ${issue.title || issue.text}`));
|
|
805
|
+
console.log('');
|
|
806
|
+
for (const line of issue.fix.split('\n').slice(0, 10)) {
|
|
807
|
+
console.log(` ${c.dim(line)}`);
|
|
808
|
+
}
|
|
809
|
+
if (issue.fix.split('\n').length > 10) {
|
|
810
|
+
console.log(c.dim(` ... (${issue.fix.split('\n').length - 10} more lines)`));
|
|
811
|
+
}
|
|
812
|
+
console.log('');
|
|
813
|
+
|
|
814
|
+
const answer = await new Promise(resolve => {
|
|
815
|
+
rl.question(` ${c.yellow('Apply this fix?')} [y/N] `, resolve);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
if (/^y(es)?$/i.test(answer.trim())) {
|
|
819
|
+
console.log('');
|
|
820
|
+
console.log(c.blue(' Running fix...'));
|
|
821
|
+
try {
|
|
822
|
+
const output = execSync(`bash -c ${JSON.stringify(issue.fix)}`, {
|
|
823
|
+
encoding: 'utf8',
|
|
824
|
+
timeout: 30000,
|
|
825
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
826
|
+
});
|
|
827
|
+
if (output.trim()) {
|
|
828
|
+
for (const line of output.trim().split('\n')) {
|
|
829
|
+
console.log(` ${line}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
console.log(c.green(' ✅ Fix applied successfully.'));
|
|
833
|
+
console.log(c.dim(' Run "rescan" to verify.'));
|
|
834
|
+
} catch (err) {
|
|
835
|
+
console.log(c.red(` ❌ Fix failed: ${err.message}`));
|
|
836
|
+
}
|
|
837
|
+
} else {
|
|
838
|
+
console.log(c.dim(' Cancelled.'));
|
|
839
|
+
}
|
|
840
|
+
console.log('');
|
|
841
|
+
rl.prompt();
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// --- Natural language → send to /chat ---
|
|
846
|
+
console.log('');
|
|
847
|
+
await streamChat(input, diagnosticId, conversationId, rl);
|
|
848
|
+
console.log('');
|
|
849
|
+
rl.prompt();
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
rl.on('close', () => {
|
|
853
|
+
console.log('');
|
|
854
|
+
console.log(c.cyan('🦞 ClawFix — made by Arca (arcabot.eth)'));
|
|
855
|
+
console.log(c.cyan(' https://clawfix.dev'));
|
|
856
|
+
console.log('');
|
|
857
|
+
process.exit(0);
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// ============================================================
|
|
862
|
+
// TUI Rendering helpers
|
|
863
|
+
// ============================================================
|
|
864
|
+
|
|
865
|
+
function renderStatus(summary, issues, serverIssues) {
|
|
866
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
867
|
+
console.log('');
|
|
868
|
+
console.log(c.cyan(`🦞 ClawFix v${VERSION}`));
|
|
869
|
+
console.log(c.cyan('━'.repeat(48)));
|
|
503
870
|
console.log('');
|
|
871
|
+
console.log(c.bold('System Status:'));
|
|
872
|
+
console.log(` ${summary.gateway.icon} Gateway: ${summary.gateway.label}`);
|
|
873
|
+
console.log(` ${summary.config.icon} Config: ${summary.config.label}`);
|
|
874
|
+
console.log(` ${summary.issues.icon} ${summary.issues.label}`);
|
|
875
|
+
console.log(` ${c.green('✓')} Node: ${summary.node} | OS: ${summary.os}`);
|
|
876
|
+
console.log('');
|
|
877
|
+
|
|
878
|
+
renderIssues(issues, serverIssues);
|
|
879
|
+
|
|
880
|
+
console.log(c.cyan('━'.repeat(48)));
|
|
881
|
+
console.log(c.dim(' Type naturally to chat, or: fix <#> | scan | apply <#> | help | exit'));
|
|
882
|
+
console.log('');
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function renderIssues(issues, serverIssues) {
|
|
886
|
+
const all = mergeIssues(issues, serverIssues);
|
|
887
|
+
|
|
888
|
+
if (all.length === 0) {
|
|
889
|
+
console.log(c.green(' ✅ No issues detected — looking healthy!'));
|
|
890
|
+
console.log('');
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
console.log(c.bold('Detected Issues:'));
|
|
895
|
+
for (let i = 0; i < all.length; i++) {
|
|
896
|
+
const issue = all[i];
|
|
897
|
+
const sev = issue.severity || 'medium';
|
|
898
|
+
const label = sev === 'critical' || sev === 'high'
|
|
899
|
+
? c.red(`[${sev.toUpperCase()}]`)
|
|
900
|
+
: sev === 'medium'
|
|
901
|
+
? c.yellow(`[${sev.toUpperCase()}]`)
|
|
902
|
+
: c.dim(`[${sev.toUpperCase()}]`);
|
|
903
|
+
console.log(` ${c.dim(`${i + 1}.`)} ${label} ${issue.title || issue.text}`);
|
|
904
|
+
}
|
|
905
|
+
console.log('');
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function renderHelp() {
|
|
909
|
+
console.log('');
|
|
910
|
+
console.log(c.bold('Commands:'));
|
|
911
|
+
console.log(` ${c.cyan('fix <#>')} Show details + fix script for issue #`);
|
|
912
|
+
console.log(` ${c.cyan('apply <#>')} Apply the fix for issue # (with confirmation)`);
|
|
913
|
+
console.log(` ${c.cyan('scan')} Re-run diagnostics`);
|
|
914
|
+
console.log(` ${c.cyan('issues')} Show detected issues`);
|
|
915
|
+
console.log(` ${c.cyan('status')} Show system status`);
|
|
916
|
+
console.log(` ${c.cyan('help')} Show this help`);
|
|
917
|
+
console.log(` ${c.cyan('exit')} Quit ClawFix`);
|
|
918
|
+
console.log('');
|
|
919
|
+
console.log(c.bold('Chat:'));
|
|
920
|
+
console.log(` Just type naturally — e.g. ${c.dim('"my discord bot isn\'t responding"')}`);
|
|
921
|
+
console.log(` ClawFix AI will analyze using your diagnostic context.`);
|
|
922
|
+
console.log('');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Merge local CLI-detected issues with server-detected known issues.
|
|
927
|
+
* Server issues (from known-issues.js pattern matching) include fix scripts.
|
|
928
|
+
* Local issues are simpler {severity, text} objects.
|
|
929
|
+
* Deduplicate by rough text matching.
|
|
930
|
+
*/
|
|
931
|
+
function mergeIssues(localIssues, serverIssues) {
|
|
932
|
+
const merged = [];
|
|
933
|
+
const seen = new Set();
|
|
934
|
+
|
|
935
|
+
// Server issues first (they have fix scripts)
|
|
936
|
+
if (serverIssues) {
|
|
937
|
+
for (const si of serverIssues) {
|
|
938
|
+
merged.push(si);
|
|
939
|
+
seen.add((si.title || '').toLowerCase());
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Then local issues that aren't duplicated
|
|
944
|
+
for (const li of localIssues) {
|
|
945
|
+
const key = (li.text || '').toLowerCase();
|
|
946
|
+
const isDup = [...seen].some(s =>
|
|
947
|
+
s.includes(key.slice(0, 20)) || key.includes(s.slice(0, 20))
|
|
948
|
+
);
|
|
949
|
+
if (!isDup) {
|
|
950
|
+
merged.push(li);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return merged;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function severityColor(sev) {
|
|
958
|
+
if (sev === 'critical') return c.red(c.bold('CRITICAL'));
|
|
959
|
+
if (sev === 'high') return c.red('HIGH');
|
|
960
|
+
if (sev === 'medium') return c.yellow('MEDIUM');
|
|
961
|
+
return c.dim('LOW');
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// ============================================================
|
|
965
|
+
// Chat streaming — SSE from /api/chat
|
|
966
|
+
// ============================================================
|
|
967
|
+
async function streamChat(message, diagnosticId, conversationId, rl) {
|
|
968
|
+
// Pause readline so it doesn't interfere with output
|
|
969
|
+
rl.pause();
|
|
970
|
+
|
|
971
|
+
process.stdout.write(c.dim(' thinking...'));
|
|
972
|
+
|
|
973
|
+
try {
|
|
974
|
+
const resp = await fetch(`${API_URL}/api/chat`, {
|
|
975
|
+
method: 'POST',
|
|
976
|
+
headers: { 'Content-Type': 'application/json' },
|
|
977
|
+
body: JSON.stringify({ diagnosticId, message, conversationId }),
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
// Non-SSE fallback (e.g. AI not available)
|
|
981
|
+
const contentType = resp.headers.get('content-type') || '';
|
|
982
|
+
if (contentType.includes('application/json')) {
|
|
983
|
+
const data = await resp.json();
|
|
984
|
+
// Clear "thinking..."
|
|
985
|
+
process.stdout.write('\r\x1b[K');
|
|
986
|
+
if (data.error) {
|
|
987
|
+
console.log(c.red(` ${data.error}`));
|
|
988
|
+
} else {
|
|
989
|
+
wrapPrint(data.response || 'No response from AI.');
|
|
990
|
+
}
|
|
991
|
+
rl.resume();
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// SSE streaming
|
|
996
|
+
process.stdout.write('\r\x1b[K');
|
|
997
|
+
process.stdout.write(' ');
|
|
998
|
+
|
|
999
|
+
const reader = resp.body.getReader();
|
|
1000
|
+
const decoder = new TextDecoder();
|
|
1001
|
+
let buffer = '';
|
|
1002
|
+
let col = 2; // Current column (2 for indent)
|
|
1003
|
+
|
|
1004
|
+
while (true) {
|
|
1005
|
+
const { done, value } = await reader.read();
|
|
1006
|
+
if (done) break;
|
|
1007
|
+
|
|
1008
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1009
|
+
const lines = buffer.split('\n');
|
|
1010
|
+
buffer = lines.pop() || '';
|
|
1011
|
+
|
|
1012
|
+
for (const line of lines) {
|
|
1013
|
+
const trimmed = line.trim();
|
|
1014
|
+
if (!trimmed || !trimmed.startsWith('data: ')) continue;
|
|
1015
|
+
|
|
1016
|
+
const data = trimmed.slice(6);
|
|
1017
|
+
if (data === '[DONE]') break;
|
|
1018
|
+
|
|
1019
|
+
try {
|
|
1020
|
+
const parsed = JSON.parse(data);
|
|
1021
|
+
if (parsed.error) {
|
|
1022
|
+
process.stdout.write(c.red(parsed.error));
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
if (parsed.content) {
|
|
1026
|
+
// Word-wrap at ~76 cols
|
|
1027
|
+
for (const ch of parsed.content) {
|
|
1028
|
+
if (ch === '\n') {
|
|
1029
|
+
process.stdout.write('\n ');
|
|
1030
|
+
col = 2;
|
|
1031
|
+
} else {
|
|
1032
|
+
process.stdout.write(ch);
|
|
1033
|
+
col++;
|
|
1034
|
+
if (col > 76 && ch === ' ') {
|
|
1035
|
+
process.stdout.write('\n ');
|
|
1036
|
+
col = 2;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
} catch {}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
process.stdout.write('\n');
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
process.stdout.write('\r\x1b[K');
|
|
1048
|
+
if (err.code === 'ECONNREFUSED' || err.cause?.code === 'ECONNREFUSED') {
|
|
1049
|
+
console.log(c.yellow(' ClawFix server is unreachable. Chat requires an internet connection.'));
|
|
1050
|
+
console.log(c.dim(' Local commands still work: fix <#>, apply <#>, scan, issues'));
|
|
1051
|
+
} else {
|
|
1052
|
+
console.log(c.red(` Connection error: ${err.message}`));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
rl.resume();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Print text with 2-space indent and word wrapping.
|
|
1061
|
+
*/
|
|
1062
|
+
function wrapPrint(text) {
|
|
1063
|
+
const width = 76;
|
|
1064
|
+
for (const paragraph of text.split('\n')) {
|
|
1065
|
+
if (!paragraph.trim()) {
|
|
1066
|
+
console.log('');
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const words = paragraph.split(' ');
|
|
1070
|
+
let line = ' ';
|
|
1071
|
+
for (const word of words) {
|
|
1072
|
+
if (line.length + word.length + 1 > width && line.trim()) {
|
|
1073
|
+
console.log(line);
|
|
1074
|
+
line = ' ';
|
|
1075
|
+
}
|
|
1076
|
+
line += (line.trim() ? ' ' : '') + word;
|
|
1077
|
+
}
|
|
1078
|
+
if (line.trim()) console.log(line);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// ============================================================
|
|
1083
|
+
// Main entry point
|
|
1084
|
+
// ============================================================
|
|
1085
|
+
async function main() {
|
|
1086
|
+
if (SHOW_HELP) {
|
|
1087
|
+
console.log(`
|
|
1088
|
+
🦞 ClawFix v${VERSION} — AI-Powered OpenClaw Diagnostic
|
|
1089
|
+
|
|
1090
|
+
Usage: npx clawfix [options]
|
|
1091
|
+
|
|
1092
|
+
Modes:
|
|
1093
|
+
(default) Interactive TUI — scan + chat + fix
|
|
1094
|
+
--scan One-shot scan (legacy mode)
|
|
1095
|
+
--no-interactive Same as --scan
|
|
1096
|
+
|
|
1097
|
+
Options:
|
|
1098
|
+
--dry-run, -n Scan locally only — shows what would be collected, sends nothing
|
|
1099
|
+
--show-data, -d Display the full diagnostic payload before asking to send
|
|
1100
|
+
--yes, -y Skip confirmation prompt and send automatically
|
|
1101
|
+
--help, -h Show this help message
|
|
1102
|
+
|
|
1103
|
+
Environment:
|
|
1104
|
+
CLAWFIX_API Override API URL (default: https://clawfix.dev)
|
|
1105
|
+
CLAWFIX_AUTO=1 Same as --yes
|
|
1106
|
+
|
|
1107
|
+
Interactive Commands:
|
|
1108
|
+
fix <#> Show details + fix script for a detected issue
|
|
1109
|
+
apply <#> Apply the fix (with confirmation)
|
|
1110
|
+
scan Re-run diagnostics
|
|
1111
|
+
issues Show detected issues
|
|
1112
|
+
help Show help
|
|
1113
|
+
exit Quit
|
|
1114
|
+
|
|
1115
|
+
Or just type naturally to chat with ClawFix AI.
|
|
1116
|
+
|
|
1117
|
+
Security:
|
|
1118
|
+
• All API keys, tokens, and passwords are automatically redacted
|
|
1119
|
+
• Your hostname is SHA-256 hashed (only first 8 chars sent)
|
|
1120
|
+
• No file contents are read (only existence checks)
|
|
1121
|
+
• Nothing is sent without your explicit approval (unless --yes)
|
|
1122
|
+
• Source code: https://github.com/arcabotai/clawfix
|
|
1123
|
+
|
|
1124
|
+
Examples:
|
|
1125
|
+
npx clawfix # Interactive TUI (default)
|
|
1126
|
+
npx clawfix --scan # One-shot scan + AI analysis
|
|
1127
|
+
npx clawfix --dry-run # See what data would be collected
|
|
1128
|
+
npx clawfix --yes --scan # Auto-send for CI/scripting
|
|
1129
|
+
`);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (ONE_SHOT) {
|
|
1134
|
+
await runOneShotMode();
|
|
1135
|
+
} else {
|
|
1136
|
+
await runInteractiveMode();
|
|
1137
|
+
}
|
|
504
1138
|
}
|
|
505
1139
|
|
|
506
1140
|
main().catch(err => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawfix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "AI-powered diagnostic and repair for OpenClaw installations",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clawfix": "./bin/clawfix.js"
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
"homepage": "https://clawfix.dev",
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/
|
|
21
|
+
"url": "https://github.com/arcabotai/clawfix"
|
|
22
22
|
},
|
|
23
23
|
"bugs": {
|
|
24
|
-
"url": "https://github.com/
|
|
24
|
+
"url": "https://github.com/arcabotai/clawfix/issues"
|
|
25
25
|
},
|
|
26
26
|
"author": "Arca <arca@arcabot.ai> (https://arcabot.ai)",
|
|
27
27
|
"license": "MIT",
|