clawfix 0.2.0 → 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 +762 -122
- 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,52 +157,125 @@ Examples:
|
|
|
190
157
|
const gatewayPort = config?.gateway?.port || 18789;
|
|
191
158
|
const gatewayPid = run('pgrep -f "openclaw.*gateway"') || '';
|
|
192
159
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
160
|
+
const statusLine = gatewayStatus.split('\n').find(l => /runtime:|listening|running|stopped|not running/i.test(l))
|
|
161
|
+
|| gatewayStatus.split('\n')[0];
|
|
162
|
+
log(` Status: ${statusLine.trim()}`);
|
|
163
|
+
if (gatewayPid) log(` PID: ${gatewayPid}`);
|
|
164
|
+
log(` Port: ${gatewayPort}`);
|
|
196
165
|
|
|
197
166
|
// --- Logs ---
|
|
198
|
-
|
|
199
|
-
|
|
167
|
+
log('');
|
|
168
|
+
log(c.blue('📜 Reading recent logs...'));
|
|
200
169
|
|
|
201
170
|
let errorLogs = '';
|
|
202
171
|
let stderrLogs = '';
|
|
172
|
+
let gatewayLogTail = '';
|
|
173
|
+
let errLogSizeMB = 0;
|
|
174
|
+
let logSizeMB = 0;
|
|
203
175
|
|
|
204
176
|
const logPath = openclawDir ? join(openclawDir, 'logs', 'gateway.log') : null;
|
|
205
177
|
const errLogPath = openclawDir ? join(openclawDir, 'logs', 'gateway.err.log') : null;
|
|
206
178
|
|
|
207
179
|
if (logPath && await exists(logPath)) {
|
|
208
180
|
try {
|
|
209
|
-
const
|
|
210
|
-
|
|
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');
|
|
211
185
|
errorLogs = lines
|
|
212
186
|
.filter(l => /error|warn|fail|crash|EADDRINUSE|EACCES/i.test(l))
|
|
213
187
|
.slice(-30)
|
|
214
188
|
.join('\n');
|
|
215
|
-
|
|
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)`));
|
|
216
194
|
} catch {}
|
|
217
195
|
}
|
|
218
196
|
|
|
219
197
|
if (errLogPath && await exists(errLogPath)) {
|
|
220
198
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
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!' : ''})`);
|
|
223
204
|
} catch {}
|
|
224
205
|
}
|
|
225
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
|
+
|
|
226
266
|
// --- Plugins ---
|
|
227
|
-
|
|
228
|
-
|
|
267
|
+
log('');
|
|
268
|
+
log(c.blue('🔌 Checking plugins...'));
|
|
229
269
|
|
|
230
270
|
const plugins = config?.plugins?.entries || {};
|
|
231
271
|
for (const [name, cfg] of Object.entries(plugins)) {
|
|
232
272
|
const icon = cfg.enabled === false ? '❌' : '✅';
|
|
233
|
-
|
|
273
|
+
log(` ${icon} ${name}`);
|
|
234
274
|
}
|
|
235
275
|
|
|
236
276
|
// --- Workspace ---
|
|
237
|
-
|
|
238
|
-
|
|
277
|
+
log('');
|
|
278
|
+
log(c.blue('📁 Checking workspace...'));
|
|
239
279
|
|
|
240
280
|
const workspaceDir = config?.agents?.defaults?.workspace || '';
|
|
241
281
|
let mdFiles = 0;
|
|
@@ -260,25 +300,28 @@ Examples:
|
|
|
260
300
|
} catch {}
|
|
261
301
|
}
|
|
262
302
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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}`);
|
|
268
308
|
}
|
|
269
309
|
|
|
270
310
|
// --- Check Ports ---
|
|
271
|
-
|
|
272
|
-
|
|
311
|
+
log('');
|
|
312
|
+
log(c.blue('🔗 Checking port availability...'));
|
|
273
313
|
|
|
314
|
+
const portResults = {};
|
|
274
315
|
const checkPort = (port, name) => {
|
|
275
316
|
const inUse = run(`lsof -i :${port} 2>/dev/null | grep LISTEN`) ||
|
|
276
317
|
run(`ss -tlnp 2>/dev/null | grep :${port}`);
|
|
277
318
|
if (inUse) {
|
|
278
|
-
|
|
319
|
+
log(c.yellow(` ⚠️ Port ${port} (${name}) — IN USE`));
|
|
320
|
+
portResults[port] = true;
|
|
279
321
|
return true;
|
|
280
322
|
} else {
|
|
281
|
-
|
|
323
|
+
log(c.green(` ✅ Port ${port} (${name}) — available`));
|
|
324
|
+
portResults[port] = false;
|
|
282
325
|
return false;
|
|
283
326
|
}
|
|
284
327
|
};
|
|
@@ -288,20 +331,50 @@ Examples:
|
|
|
288
331
|
checkPort(18791, 'browser control');
|
|
289
332
|
|
|
290
333
|
// --- Local Issue Detection ---
|
|
291
|
-
console.log('');
|
|
292
|
-
console.log(c.cyan('━'.repeat(50)));
|
|
293
|
-
console.log(c.bold('📊 Diagnostic Summary'));
|
|
294
|
-
console.log(c.cyan('━'.repeat(50)));
|
|
295
|
-
console.log('');
|
|
296
|
-
|
|
297
334
|
const issues = [];
|
|
298
335
|
|
|
299
|
-
|
|
336
|
+
const gatewayRunning = /running.*pid|state active|listening/i.test(gatewayStatus);
|
|
337
|
+
const gatewayFailed = /not running|failed to start|stopped|inactive/i.test(gatewayStatus);
|
|
338
|
+
if (gatewayFailed || (!gatewayRunning && !/warning/i.test(gatewayStatus))) {
|
|
300
339
|
issues.push({ severity: 'critical', text: 'Gateway is not running' });
|
|
301
340
|
}
|
|
302
341
|
if (/EADDRINUSE/i.test(errorLogs)) {
|
|
303
342
|
issues.push({ severity: 'critical', text: 'Port conflict detected' });
|
|
304
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
|
+
|
|
305
378
|
if (config?.plugins?.entries?.['openclaw-mem0']?.config?.enableGraph === true) {
|
|
306
379
|
issues.push({ severity: 'high', text: 'Mem0 enableGraph requires Pro plan (will silently fail)' });
|
|
307
380
|
}
|
|
@@ -321,23 +394,6 @@ Examples:
|
|
|
321
394
|
issues.push({ severity: 'low', text: 'No memory files found' });
|
|
322
395
|
}
|
|
323
396
|
|
|
324
|
-
if (issues.length === 0) {
|
|
325
|
-
console.log(c.green('✅ No issues detected! Your OpenClaw looks healthy.'));
|
|
326
|
-
} else {
|
|
327
|
-
console.log(c.red(`Found ${issues.length} issue(s):`));
|
|
328
|
-
console.log('');
|
|
329
|
-
for (const issue of issues) {
|
|
330
|
-
const icon = issue.severity === 'critical' ? c.red('❌') :
|
|
331
|
-
issue.severity === 'high' ? c.red('❌') :
|
|
332
|
-
c.yellow('⚠️');
|
|
333
|
-
console.log(` ${icon} [${issue.severity.toUpperCase()}] ${issue.text}`);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
console.log('');
|
|
338
|
-
console.log(c.cyan('━'.repeat(50)));
|
|
339
|
-
console.log('');
|
|
340
|
-
|
|
341
397
|
// --- Build Payload ---
|
|
342
398
|
const diagnostic = {
|
|
343
399
|
version: VERSION,
|
|
@@ -362,7 +418,11 @@ Examples:
|
|
|
362
418
|
logs: {
|
|
363
419
|
errors: errorLogs,
|
|
364
420
|
stderr: stderrLogs,
|
|
421
|
+
gatewayLog: gatewayLogTail,
|
|
422
|
+
errLogSizeMB,
|
|
423
|
+
logSizeMB,
|
|
365
424
|
},
|
|
425
|
+
service: serviceHealth,
|
|
366
426
|
workspace: {
|
|
367
427
|
path: workspaceDir || 'unknown',
|
|
368
428
|
mdFiles,
|
|
@@ -375,6 +435,72 @@ Examples:
|
|
|
375
435
|
},
|
|
376
436
|
};
|
|
377
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
|
+
|
|
378
504
|
// --- Show collected data ---
|
|
379
505
|
if (DRY_RUN || SHOW_DATA) {
|
|
380
506
|
console.log('');
|
|
@@ -392,18 +518,17 @@ Examples:
|
|
|
392
518
|
console.log(c.cyan(' npx clawfix'));
|
|
393
519
|
console.log('');
|
|
394
520
|
console.log(c.cyan('🦞 ClawFix — made by Arca (arcabot.eth)'));
|
|
395
|
-
console.log(c.cyan(' https://clawfix.dev | https://x.com/
|
|
521
|
+
console.log(c.cyan(' https://clawfix.dev | https://x.com/arcabotai'));
|
|
396
522
|
console.log('');
|
|
397
523
|
return;
|
|
398
524
|
}
|
|
399
525
|
|
|
400
|
-
// --- Send for AI analysis ---
|
|
401
526
|
if (issues.length === 0) {
|
|
402
527
|
console.log(c.green('Your OpenClaw is looking good! No fixes needed.'));
|
|
403
528
|
console.log(`If you're still having issues, run with --show-data to see what would be collected.`);
|
|
404
529
|
console.log('');
|
|
405
530
|
console.log(c.cyan(`🦞 ClawFix — made by Arca (arcabot.eth)`));
|
|
406
|
-
console.log(c.cyan(` https://clawfix.dev | https://x.com/
|
|
531
|
+
console.log(c.cyan(` https://clawfix.dev | https://x.com/arcabotai`));
|
|
407
532
|
console.log('');
|
|
408
533
|
return;
|
|
409
534
|
}
|
|
@@ -417,8 +542,7 @@ Examples:
|
|
|
417
542
|
|
|
418
543
|
let shouldSend = AUTO_SEND;
|
|
419
544
|
if (!shouldSend) {
|
|
420
|
-
const
|
|
421
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
545
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
422
546
|
const answer = await new Promise(resolve => {
|
|
423
547
|
rl.question('Send diagnostic for AI analysis? [y/N] ', resolve);
|
|
424
548
|
});
|
|
@@ -455,7 +579,6 @@ Examples:
|
|
|
455
579
|
console.log(c.green(`✅ Diagnosis complete! Found ${result.issuesFound} issue(s).`));
|
|
456
580
|
console.log('');
|
|
457
581
|
|
|
458
|
-
// Show known issues
|
|
459
582
|
if (result.knownIssues) {
|
|
460
583
|
for (const issue of result.knownIssues) {
|
|
461
584
|
console.log(` ${issue.severity.toUpperCase()} — ${issue.title}: ${issue.description}`);
|
|
@@ -467,7 +590,6 @@ Examples:
|
|
|
467
590
|
console.log(result.analysis || 'Pattern matching only (no AI configured)');
|
|
468
591
|
console.log('');
|
|
469
592
|
|
|
470
|
-
// Save fix script
|
|
471
593
|
if (result.fixScript) {
|
|
472
594
|
const { writeFile } = await import('node:fs/promises');
|
|
473
595
|
const fixPath = `/tmp/clawfix-${fixId}.sh`;
|
|
@@ -493,8 +615,526 @@ Examples:
|
|
|
493
615
|
|
|
494
616
|
console.log('');
|
|
495
617
|
console.log(c.cyan('🦞 ClawFix — made by Arca (arcabot.eth)'));
|
|
496
|
-
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)));
|
|
497
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
|
+
}
|
|
498
1138
|
}
|
|
499
1139
|
|
|
500
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",
|