clawkit-doctor 1.0.0 → 2.0.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.
Files changed (2) hide show
  1. package/bin/index.js +343 -70
  2. package/package.json +4 -4
package/bin/index.js CHANGED
@@ -6,113 +6,386 @@ const os = require('os');
6
6
  const chalk = require('chalk');
7
7
  const ora = require('ora');
8
8
  const http = require('http');
9
+ const net = require('net');
10
+ const { execSync } = require('child_process');
9
11
 
10
- console.clear();
11
- console.log(chalk.cyan.bold('\nšŸ¦ž ClawKit Doctor starting...\n'));
12
+ // --- Configuration ---
13
+ const SITE_URL = 'https://getclawkit.com';
14
+ const CONFIG_DIR = path.join(os.homedir(), '.openclaw');
15
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'clawhub.json');
16
+ const SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
17
+ const FIX_MODE = process.argv.includes('--fix');
12
18
 
13
- const steps = [
14
- checkNodeVersion,
15
- checkConfigDir,
16
- checkConfigFile,
17
- checkPermissions,
18
- checkAgentConnection,
19
- ];
19
+ // --- Report collector ---
20
+ const report = {
21
+ ts: new Date().toISOString(),
22
+ os: `${os.platform()} ${os.arch()} ${os.release()}`,
23
+ node: process.version,
24
+ fix: FIX_MODE,
25
+ checks: [],
26
+ };
20
27
 
21
- async function run() {
22
- for (const step of steps) {
23
- await step();
28
+ function record(id, status, message, helpUrl) {
29
+ report.checks.push({ id, status, message });
30
+ return { id, status, message, helpUrl };
31
+ }
32
+
33
+ // --- Helpers ---
34
+ function commandExists(cmd) {
35
+ try {
36
+ const where = os.platform() === 'win32' ? 'where' : 'which';
37
+ execSync(`${where} ${cmd}`, { stdio: 'ignore' });
38
+ return true;
39
+ } catch {
40
+ return false;
24
41
  }
25
- console.log(chalk.cyan.bold('\nāœ… Diagnosis Complete. Screenshot this if you need help!\n'));
26
- console.log(chalk.gray('For more help, visit: https://getclawkit.com/docs\n'));
27
42
  }
28
43
 
29
- run().catch(err => {
30
- console.error(chalk.red('\nāŒ Unexpected Error:'), err);
31
- process.exit(1);
32
- });
44
+ function getCommandVersion(cmd) {
45
+ try {
46
+ return execSync(`${cmd} --version`, { encoding: 'utf8', timeout: 5000 }).trim().split('\n')[0];
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function checkPort(port) {
53
+ return new Promise((resolve) => {
54
+ const server = net.createServer();
55
+ server.listen(port, '127.0.0.1', () => {
56
+ server.close(() => resolve(false)); // port is free
57
+ });
58
+ server.on('error', () => resolve(true)); // port is in use
59
+ });
60
+ }
61
+
62
+ function readConfig() {
63
+ try {
64
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
65
+ return JSON.parse(raw);
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ function getNestedValue(obj, keyPath) {
72
+ return keyPath.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj);
73
+ }
33
74
 
34
- // --- Checks ---
75
+ // ============================================================
76
+ // Checks
77
+ // ============================================================
35
78
 
36
79
  async function checkNodeVersion() {
37
80
  const spinner = ora('Checking Node.js version...').start();
38
- const version = process.version;
39
- const major = parseInt(version.substring(1).split('.')[0], 10);
81
+ const major = parseInt(process.version.substring(1).split('.')[0], 10);
40
82
 
41
83
  if (major >= 18) {
42
- spinner.succeed(chalk.green(`Node.js found: ${version}`));
84
+ spinner.succeed(chalk.green(`Node.js ${process.version}`));
85
+ return record('node', 'pass', `Node.js ${process.version}`);
43
86
  } else {
44
- spinner.fail(chalk.red(`Node.js ${version} is too old. Please install Node.js v18+.`));
87
+ spinner.fail(chalk.red(`Node.js ${process.version} is too old. Install Node.js v18+.`));
88
+ return record('node', 'fail', `Node.js ${process.version} — requires v18+`);
45
89
  }
46
90
  }
47
91
 
48
- async function checkConfigDir() {
49
- const spinner = ora('Checking Config Directory...').start();
50
- const homeDir = os.homedir();
51
- const configPath = path.join(homeDir, '.openclaw');
92
+ async function checkGit() {
93
+ const spinner = ora('Checking Git...').start();
94
+ if (commandExists('git')) {
95
+ const ver = getCommandVersion('git') || 'found';
96
+ spinner.succeed(chalk.green(`Git: ${ver}`));
97
+ return record('git', 'pass', ver);
98
+ } else {
99
+ const helpUrl = `${SITE_URL}/docs/troubleshooting/spawn-git-enoent`;
100
+ spinner.fail(chalk.red(`Git not found. npm packages that use Git will fail.`));
101
+ console.log(chalk.gray(` Fix: ${helpUrl}`));
102
+ if (FIX_MODE && os.platform() === 'win32') {
103
+ console.log(chalk.yellow(' Auto-fix: running winget install Git.Git ...'));
104
+ try { execSync('winget install Git.Git', { stdio: 'inherit' }); } catch { /* ignore */ }
105
+ }
106
+ return record('git', 'fail', 'Git not installed or not in PATH', helpUrl);
107
+ }
108
+ }
109
+
110
+ async function checkDocker() {
111
+ const spinner = ora('Checking Docker...').start();
112
+ if (commandExists('docker')) {
113
+ const ver = getCommandVersion('docker') || 'found';
114
+ spinner.succeed(chalk.green(`Docker: ${ver}`));
115
+ return record('docker', 'pass', ver);
116
+ } else {
117
+ const helpUrl = `${SITE_URL}/docs/troubleshooting/spawn-docker-enoent`;
118
+ spinner.warn(chalk.yellow('Docker not found. Agent sandbox mode will fail.'));
119
+ console.log(chalk.gray(` Fix: Install Docker or disable sandbox: openclaw config set agents.defaults.sandbox.mode off`));
120
+ console.log(chalk.gray(` Guide: ${helpUrl}`));
121
+ return record('docker', 'warn', 'Docker not installed (sandbox mode will fail)', helpUrl);
122
+ }
123
+ }
52
124
 
125
+ async function checkConfigDir() {
126
+ const spinner = ora('Checking config directory...').start();
53
127
  try {
54
- await fs.promises.access(configPath);
55
- spinner.succeed(chalk.green(`Config directory exists at: ${configPath}`));
56
- } catch (error) {
57
- spinner.fail(chalk.red(`Config directory missing at ${configPath}. Run 'clawhub init' first.`));
128
+ await fs.promises.access(CONFIG_DIR);
129
+ spinner.succeed(chalk.green(`Config directory: ${CONFIG_DIR}`));
130
+ return record('config_dir', 'pass', CONFIG_DIR);
131
+ } catch {
132
+ spinner.fail(chalk.red(`Config directory missing: ${CONFIG_DIR}`));
133
+ console.log(chalk.gray(` Fix: Run 'openclaw init' or use ${SITE_URL}/tools/config`));
134
+ if (FIX_MODE) {
135
+ console.log(chalk.yellow(' Auto-fix: creating directory...'));
136
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
137
+ console.log(chalk.green(' Created.'));
138
+ }
139
+ return record('config_dir', 'fail', `Missing: ${CONFIG_DIR}`);
58
140
  }
59
141
  }
60
142
 
61
143
  async function checkConfigFile() {
62
- const spinner = ora('Checking Config File...').start();
63
- const homeDir = os.homedir();
64
- const configFilePath = path.join(homeDir, '.openclaw', 'clawhub.json');
144
+ const spinner = ora('Checking config file (clawhub.json)...').start();
145
+ try {
146
+ await fs.promises.access(CONFIG_FILE);
147
+ } catch {
148
+ const helpUrl = `${SITE_URL}/tools/config`;
149
+ spinner.fail(chalk.yellow('Config file missing (clawhub.json).'));
150
+ console.log(chalk.gray(` Generate one: ${helpUrl}`));
151
+ return record('config_file', 'fail', 'clawhub.json not found', helpUrl);
152
+ }
65
153
 
154
+ // Validate JSON syntax
66
155
  try {
67
- await fs.promises.access(configFilePath);
68
- spinner.succeed(chalk.green('Config file found (clawhub.json).'));
69
- } catch (error) {
70
- spinner.fail(chalk.yellow('Config file missing (clawhub.json). Use Config Generator to create one.'));
156
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
157
+ JSON.parse(raw);
158
+ spinner.succeed(chalk.green('Config file found and valid JSON.'));
159
+ return record('config_file', 'pass', 'clawhub.json valid');
160
+ } catch (e) {
161
+ const helpUrl = `${SITE_URL}/docs/troubleshooting/json-parse-errors`;
162
+ spinner.fail(chalk.red(`Config file has invalid JSON: ${e.message}`));
163
+ console.log(chalk.gray(` Fix: ${helpUrl}`));
164
+ return record('config_file', 'fail', `Invalid JSON: ${e.message}`, helpUrl);
71
165
  }
72
166
  }
73
167
 
74
168
  async function checkPermissions() {
75
- const spinner = ora('Checking Directory Permissions...').start();
76
- const homeDir = os.homedir();
77
- const configPath = path.join(homeDir, '.openclaw');
78
-
169
+ const spinner = ora('Checking directory permissions...').start();
79
170
  try {
80
- // Only check if dir exists
81
- if (fs.existsSync(configPath)) {
82
- await fs.promises.access(configPath, fs.constants.W_OK);
83
- spinner.succeed(chalk.green('Write permission OK for ~/.openclaw'));
84
- } else {
85
- spinner.info(chalk.gray('Skipping permission check (directory missing).'));
171
+ if (!fs.existsSync(CONFIG_DIR)) {
172
+ spinner.info(chalk.gray('Skipped (directory missing).'));
173
+ return record('permissions', 'skip', 'Directory missing');
86
174
  }
175
+ await fs.promises.access(CONFIG_DIR, fs.constants.W_OK);
176
+ spinner.succeed(chalk.green('Write permission OK for ~/.openclaw'));
177
+ return record('permissions', 'pass', 'Write permission OK');
178
+ } catch {
179
+ spinner.fail(chalk.red('No write permission for ~/.openclaw'));
180
+ console.log(chalk.gray(' Fix: sudo chown -R $(whoami) ~/.openclaw'));
181
+ if (FIX_MODE && os.platform() !== 'win32') {
182
+ console.log(chalk.yellow(' Auto-fix: fixing ownership...'));
183
+ try { execSync(`chown -R $(whoami) "${CONFIG_DIR}"`, { stdio: 'inherit' }); } catch { /* ignore */ }
184
+ }
185
+ return record('permissions', 'fail', 'No write permission');
186
+ }
187
+ }
188
+
189
+ async function checkGatewayTokens() {
190
+ const spinner = ora('Checking gateway token alignment...').start();
191
+ const config = readConfig();
192
+ if (!config) {
193
+ spinner.info(chalk.gray('Skipped (no valid config).'));
194
+ return record('tokens', 'skip', 'No config to check');
195
+ }
196
+
197
+ const authToken = getNestedValue(config, 'gateway.auth.token');
198
+ const remoteToken = getNestedValue(config, 'gateway.remote.token');
199
+ const envToken = process.env.OPENCLAW_GATEWAY_TOKEN;
200
+
201
+ if (!authToken && !remoteToken) {
202
+ spinner.info(chalk.gray('No gateway tokens configured (will auto-generate).'));
203
+ return record('tokens', 'info', 'No tokens configured');
204
+ }
205
+
206
+ // Check env override
207
+ if (envToken && authToken && envToken !== authToken) {
208
+ const helpUrl = `${SITE_URL}/docs/troubleshooting/gateway-token-mismatch`;
209
+ spinner.fail(chalk.red('OPENCLAW_GATEWAY_TOKEN env var overrides config! Tokens will mismatch.'));
210
+ console.log(chalk.gray(` Env: ${envToken.substring(0, 8)}... Config: ${authToken.substring(0, 8)}...`));
211
+ console.log(chalk.gray(` Fix: ${helpUrl}`));
212
+ return record('tokens', 'fail', 'Env var overrides gateway.auth.token', helpUrl);
213
+ }
87
214
 
88
- } catch (error) {
89
- spinner.fail(chalk.red('No write permission for ~/.openclaw. Fix ownership with: sudo chown -R $(whoami) ~/.openclaw'));
215
+ if (authToken && remoteToken && authToken !== remoteToken) {
216
+ const helpUrl = `${SITE_URL}/docs/troubleshooting/gateway-token-mismatch`;
217
+ spinner.fail(chalk.red('gateway.auth.token ≠ gateway.remote.token — clients cannot connect.'));
218
+ console.log(chalk.gray(` Auth: ${authToken.substring(0, 8)}...`));
219
+ console.log(chalk.gray(` Remote: ${remoteToken.substring(0, 8)}...`));
220
+ console.log(chalk.gray(` Fix: ${helpUrl}`));
221
+ if (FIX_MODE) {
222
+ console.log(chalk.yellow(' Auto-fix: aligning remote token to match auth token...'));
223
+ config.gateway.remote.token = authToken;
224
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
225
+ console.log(chalk.green(' Fixed. Restart gateway: openclaw gateway restart'));
226
+ }
227
+ return record('tokens', 'fail', 'auth.token ≠ remote.token', helpUrl);
90
228
  }
229
+
230
+ spinner.succeed(chalk.green('Gateway tokens aligned.'));
231
+ return record('tokens', 'pass', 'Tokens aligned');
91
232
  }
92
233
 
93
- async function checkAgentConnection() {
94
- const spinner = ora('Checking Local Agent (Port 3000)...').start();
234
+ async function checkStaleLocks() {
235
+ const spinner = ora('Checking for stale lock files...').start();
236
+ if (!fs.existsSync(SESSIONS_DIR)) {
237
+ spinner.info(chalk.gray('No sessions directory.'));
238
+ return record('locks', 'skip', 'No sessions directory');
239
+ }
95
240
 
96
- return new Promise((resolve) => {
97
- const req = http.get('http://localhost:3000', (res) => {
98
- spinner.succeed(chalk.green('Local Agent Port (3000) is OPEN.'));
99
- resolve();
100
- });
241
+ try {
242
+ const files = fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.lock'));
243
+ if (files.length === 0) {
244
+ spinner.succeed(chalk.green('No stale lock files.'));
245
+ return record('locks', 'pass', 'No lock files');
246
+ }
247
+
248
+ const helpUrl = `${SITE_URL}/docs/troubleshooting/gateway-lock-timeout`;
249
+ spinner.warn(chalk.yellow(`Found ${files.length} lock file(s): ${files.join(', ')}`));
250
+ console.log(chalk.gray(` This may cause "gateway already running" errors.`));
251
+ console.log(chalk.gray(` Fix: ${helpUrl}`));
101
252
 
102
- req.on('error', (e) => {
103
- if (e.code === 'ECONNREFUSED') {
104
- spinner.info(chalk.gray('Port 3000 is free (Agent not running).'));
105
- } else {
106
- spinner.info(chalk.gray(`Network check skipped: ${e.message}`));
253
+ if (FIX_MODE) {
254
+ console.log(chalk.yellow(' Auto-fix: removing stale locks...'));
255
+ for (const f of files) {
256
+ fs.unlinkSync(path.join(SESSIONS_DIR, f));
107
257
  }
108
- resolve();
109
- });
258
+ console.log(chalk.green(` Removed ${files.length} lock file(s).`));
259
+ }
260
+ return record('locks', 'warn', `${files.length} lock file(s) found`, helpUrl);
261
+ } catch (e) {
262
+ spinner.info(chalk.gray(`Could not check locks: ${e.message}`));
263
+ return record('locks', 'skip', e.message);
264
+ }
265
+ }
110
266
 
111
- req.setTimeout(2000, () => {
112
- message = 'Connection timed out';
113
- req.destroy();
114
- spinner.info(chalk.gray('Network check timed out.'));
115
- resolve();
116
- });
267
+ async function checkGatewayPort() {
268
+ const spinner = ora('Checking Gateway port (18789)...').start();
269
+ const inUse = await checkPort(18789);
270
+ if (inUse) {
271
+ spinner.succeed(chalk.green('Gateway port 18789 is in use (gateway likely running).'));
272
+ return record('gateway_port', 'pass', 'Port 18789 in use (gateway running)');
273
+ } else {
274
+ spinner.info(chalk.gray('Gateway port 18789 is free (gateway not running).'));
275
+ return record('gateway_port', 'info', 'Port 18789 free (gateway not running)');
276
+ }
277
+ }
278
+
279
+ async function checkAgentPort() {
280
+ const spinner = ora('Checking Agent port (3000)...').start();
281
+ const inUse = await checkPort(3000);
282
+ if (inUse) {
283
+ spinner.succeed(chalk.green('Agent port 3000 is in use (agent likely running).'));
284
+ return record('agent_port', 'pass', 'Port 3000 in use');
285
+ } else {
286
+ spinner.info(chalk.gray('Agent port 3000 is free (agent not running).'));
287
+ return record('agent_port', 'info', 'Port 3000 free');
288
+ }
289
+ }
290
+
291
+ async function checkCDPPort() {
292
+ const spinner = ora('Checking Browser CDP port (18800)...').start();
293
+ const inUse = await checkPort(18800);
294
+ if (inUse) {
295
+ spinner.info(chalk.gray('CDP port 18800 in use (browser control active or stray process).'));
296
+ return record('cdp_port', 'info', 'Port 18800 in use');
297
+ } else {
298
+ spinner.succeed(chalk.green('CDP port 18800 is free.'));
299
+ return record('cdp_port', 'pass', 'Port 18800 free');
300
+ }
301
+ }
302
+
303
+ // ============================================================
304
+ // Report Generator
305
+ // ============================================================
306
+
307
+ function generateReportUrl() {
308
+ const compressed = JSON.stringify({
309
+ t: report.ts,
310
+ o: report.os,
311
+ n: report.node,
312
+ f: report.fix,
313
+ c: report.checks.map(c => [c.id, c.status[0], c.message.substring(0, 80)]),
117
314
  });
315
+ const encoded = Buffer.from(compressed).toString('base64url');
316
+ return `${SITE_URL}/tools/doctor?r=${encoded}`;
317
+ }
318
+
319
+ // ============================================================
320
+ // Main
321
+ // ============================================================
322
+
323
+ const steps = [
324
+ checkNodeVersion,
325
+ checkGit,
326
+ checkDocker,
327
+ checkConfigDir,
328
+ checkConfigFile,
329
+ checkPermissions,
330
+ checkGatewayTokens,
331
+ checkStaleLocks,
332
+ checkGatewayPort,
333
+ checkAgentPort,
334
+ checkCDPPort,
335
+ ];
336
+
337
+ async function run() {
338
+ console.clear();
339
+ console.log(chalk.cyan.bold('\nšŸ¦ž ClawKit Doctor v2.0\n'));
340
+
341
+ if (FIX_MODE) {
342
+ console.log(chalk.yellow.bold('⚔ Fix mode enabled — will auto-repair issues where possible.\n'));
343
+ }
344
+
345
+ for (const step of steps) {
346
+ await step();
347
+ }
348
+
349
+ // Summary
350
+ const fails = report.checks.filter(c => c.status === 'fail');
351
+ const warns = report.checks.filter(c => c.status === 'warn');
352
+ const passes = report.checks.filter(c => c.status === 'pass');
353
+
354
+ console.log('');
355
+ console.log(chalk.bold('─'.repeat(50)));
356
+
357
+ if (fails.length === 0 && warns.length === 0) {
358
+ console.log(chalk.green.bold('\nāœ… All checks passed! Your environment looks healthy.\n'));
359
+ } else {
360
+ console.log(chalk.yellow.bold(`\nšŸ“‹ Summary: ${passes.length} passed, ${warns.length} warnings, ${fails.length} failed\n`));
361
+
362
+ if (fails.length > 0) {
363
+ console.log(chalk.red.bold(' Issues to fix:'));
364
+ for (const f of fails) {
365
+ console.log(chalk.red(` āœ— ${f.message}`));
366
+ }
367
+ }
368
+ if (warns.length > 0) {
369
+ console.log(chalk.yellow.bold(' Warnings:'));
370
+ for (const w of warns) {
371
+ console.log(chalk.yellow(` ⚠ ${w.message}`));
372
+ }
373
+ }
374
+
375
+ if (!FIX_MODE && fails.length > 0) {
376
+ console.log(chalk.cyan(`\n šŸ’” Run with --fix to auto-repair: npx clawkit-doctor@latest --fix\n`));
377
+ }
378
+ }
379
+
380
+ // Report URL
381
+ const reportUrl = generateReportUrl();
382
+ console.log(chalk.gray('─'.repeat(50)));
383
+ console.log(chalk.cyan.bold('\nšŸ“‹ Share this report:'));
384
+ console.log(chalk.white(` ${reportUrl}\n`));
385
+ console.log(chalk.gray(`For full troubleshooting guides: ${SITE_URL}/docs/troubleshooting\n`));
118
386
  }
387
+
388
+ run().catch(err => {
389
+ console.error(chalk.red('\nāŒ Unexpected Error:'), err.message);
390
+ process.exit(1);
391
+ });
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "clawkit-doctor",
3
- "version": "1.0.0",
4
- "description": "Diagnostic tool for OpenClaw environment",
3
+ "version": "2.0.0",
4
+ "description": "Diagnostic and auto-fix tool for OpenClaw environment",
5
5
  "main": "bin/index.js",
6
6
  "bin": {
7
- "clawkit-doctor": "./bin/index.js"
7
+ "clawkit-doctor": "bin/index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -21,4 +21,4 @@
21
21
  "chalk": "^4.1.2",
22
22
  "ora": "^5.4.1"
23
23
  }
24
- }
24
+ }