clawkit-doctor 2.0.1 → 2.2.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 (3) hide show
  1. package/README.md +72 -12
  2. package/bin/index.js +663 -353
  3. package/package.json +36 -23
package/README.md CHANGED
@@ -1,28 +1,88 @@
1
1
  # ClawKit Doctor
2
2
 
3
- > Instant environment diagnostic tool for OpenClaw.
3
+ Fast diagnostics and safe auto-repair for OpenClaw environments.
4
+
5
+ Maintainer: **@branzoom**
4
6
 
5
7
  ## Usage
6
8
 
7
- Simply run:
9
+ ```bash
10
+ npx clawkit-doctor@latest
11
+ ```
12
+
13
+ ### Common modes
8
14
 
9
15
  ```bash
16
+ # Diagnose only
10
17
  npx clawkit-doctor@latest
18
+
19
+ # Diagnose + safe auto-repair
20
+ npx clawkit-doctor@latest --fix
21
+
22
+ # Paste an error for instant recognition + fix steps
23
+ npx clawkit-doctor@latest --paste "Telegram getUpdates conflict 409"
24
+
25
+ # Continue to full environment checks after paste diagnosis
26
+ npx clawkit-doctor@latest --paste "token mismatch" --full
27
+
28
+ # Language selection
29
+ npx clawkit-doctor@latest --lang zh
30
+
31
+ # Skip 0/1 follow-up prompts
32
+ npx clawkit-doctor@latest --no-interactive
33
+
34
+ # Machine-readable output
35
+ npx clawkit-doctor@latest --json
11
36
  ```
12
37
 
13
- No installation required.
38
+ ## Flags
39
+
40
+ - `--fix`: auto-repair supported issues (missing config dir, stale locks, token alignment, permissions)
41
+ - `--paste "<error text>"`: detect known issues from pasted logs/errors and show steps
42
+ - `--full`: with `--paste`, continue into full environment checks
43
+ - `--lang en|zh|ja`: switch UI language
44
+ - `--no-interactive`: disable the y/n resolution follow-up
45
+ - `--json`: print full report JSON for CI/automation
46
+ - `--quiet`: reduce console output
47
+ - `--no-report`: skip shareable report URL
48
+ - `--help`, `--version`
14
49
 
15
50
  ## What it checks
16
51
 
17
- 1. **Node.js Version**: Ensures you are running Node.js v18+.
18
- 2. **Config Directory**: Checks if `~/.openclaw` exists.
19
- 3. **Config File**: Checks if `clawhub.json` is present.
20
- 4. **Permissions**: Verifies write access to the config directory.
21
- 5. **Connectivity**: Checks if the local agent is running on port 3000.
52
+ 1. Node.js version (`v22+` required)
53
+ 2. Git availability
54
+ 3. Docker availability
55
+ 4. OpenClaw config directory (`~/.openclaw`)
56
+ 5. Config file syntax (`openclaw.json`)
57
+ 6. Directory write permissions
58
+ 7. Gateway token alignment (`auth`, `remote`, env override)
59
+ 8. Stale lock files under `~/.openclaw/sessions`
60
+ 9. Port `18789` (gateway)
61
+ 10. Port `3000` (agent)
62
+ 11. Port `18800` (browser CDP)
63
+
64
+ ## Resolution loop (best-practice UX)
65
+
66
+ When `--paste` is used in an interactive terminal, Doctor asks:
67
+
68
+ 1. `y/n` whether the issue is solved (also accepts `1/0` for compatibility)
69
+ 2. If not solved, reason selection (`command`, `permission`, `timeout/network`, `not sure`)
70
+ 3. Provides fallback steps and asks `y/n` again
71
+ 4. Only after repeated failure, shows escalation links (issue details, troubleshooting index, contact)
22
72
 
23
- ## Publishing (For Maintainers)
73
+ This avoids hard-sell behavior and keeps flow focused on incident recovery.
24
74
 
25
- To publish a new version:
75
+ ## Built-in links (contextual)
26
76
 
27
- 1. Update the version in `package.json`.
28
- 2. Run `npm publish --access public`.
77
+ Doctor links users naturally based on state:
78
+
79
+ - Matched issue details page (`/docs/troubleshooting/...`)
80
+ - Troubleshooting index: `https://getclawkit.com/docs/troubleshooting`
81
+ - Prevention guide (post-resolution): `https://getclawkit.com/docs/guides/stable-ops`
82
+
83
+ ## Maintainer publish
84
+
85
+ ```bash
86
+ cd packages/clawkit-doctor
87
+ npm publish --access public
88
+ ```
package/bin/index.js CHANGED
@@ -5,431 +5,741 @@ const path = require('path');
5
5
  const os = require('os');
6
6
  const chalk = require('chalk');
7
7
  const ora = require('ora');
8
- const http = require('http');
9
8
  const net = require('net');
9
+ const readline = require('readline');
10
10
  const { execSync } = require('child_process');
11
11
 
12
- // --- Help & Version ---
13
- if (process.argv.includes('--help') || process.argv.includes('-h')) {
14
- console.log(`
15
- ${chalk.cyan.bold('šŸ¦ž ClawKit Doctor v2.0')} — Diagnose & fix your OpenClaw environment
12
+ const pkg = require('../package.json');
16
13
 
17
- ${chalk.bold('Usage:')}
18
- npx clawkit-doctor@latest Run all diagnostics (read-only)
19
- npx clawkit-doctor@latest --fix Run diagnostics + auto-repair issues
20
- npx clawkit-doctor@latest --help Show this help message
21
- npx clawkit-doctor@latest --version Show version
22
-
23
- ${chalk.bold('What it checks (11 items):')}
24
- ${chalk.green('āœ“')} Node.js version (v18+ required)
25
- ${chalk.green('āœ“')} Git installed and in PATH
26
- ${chalk.green('āœ“')} Docker installed and in PATH
27
- ${chalk.green('āœ“')} Config directory (~/.openclaw)
28
- ${chalk.green('āœ“')} Config file validity (clawhub.json JSON syntax)
29
- ${chalk.green('āœ“')} Directory write permissions
30
- ${chalk.green('āœ“')} Gateway token alignment (auth vs remote vs env)
31
- ${chalk.green('āœ“')} Stale lock files
32
- ${chalk.green('āœ“')} Gateway port 18789
33
- ${chalk.green('āœ“')} Agent port 3000
34
- ${chalk.green('āœ“')} Browser CDP port 18800
35
-
36
- ${chalk.bold('What --fix auto-repairs:')}
37
- ${chalk.yellow('⚔')} Creates missing config directory
38
- ${chalk.yellow('⚔')} Removes stale session lock files
39
- ${chalk.yellow('⚔')} Aligns mismatched gateway tokens
40
- ${chalk.yellow('⚔')} Fixes directory ownership (macOS/Linux)
41
-
42
- ${chalk.bold('Report:')}
43
- After running, a shareable report URL is generated.
44
- Paste it in GitHub issues or Discord for quick support.
45
-
46
- ${chalk.gray('More info: https://getclawkit.com/tools/doctor')}
47
- `);
48
- process.exit(0);
49
- }
50
-
51
- if (process.argv.includes('--version') || process.argv.includes('-v')) {
52
- console.log('clawkit-doctor v2.0.0');
53
- process.exit(0);
54
- }
55
-
56
- // --- Configuration ---
57
14
  const SITE_URL = 'https://getclawkit.com';
58
15
  const CONFIG_DIR = path.join(os.homedir(), '.openclaw');
59
- const CONFIG_FILE = path.join(CONFIG_DIR, 'clawhub.json');
16
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'openclaw.json');
60
17
  const SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
61
- const FIX_MODE = process.argv.includes('--fix');
62
18
 
63
- // --- Report collector ---
19
+ const argv = process.argv.slice(2);
20
+ const args = new Set(argv);
21
+ function getArgValue(flag) {
22
+ for (let i = 0; i < argv.length; i += 1) {
23
+ const token = argv[i];
24
+ if (token === flag && i + 1 < argv.length) return argv[i + 1];
25
+ if (token.startsWith(`${flag}=`)) return token.slice(flag.length + 1);
26
+ }
27
+ return '';
28
+ }
29
+
30
+ const requestedLang = (getArgValue('--lang') || process.env.LANG || 'en').toLowerCase();
31
+ const LANG = requestedLang.startsWith('zh') ? 'zh' : requestedLang.startsWith('ja') ? 'ja' : 'en';
32
+ const PASTE_TEXT = (getArgValue('--paste') || '').trim();
33
+ const FULL_MODE = args.has('--full');
34
+ const FIX_MODE = args.has('--fix');
35
+ const JSON_MODE = args.has('--json');
36
+ const QUIET_MODE = args.has('--quiet') || JSON_MODE;
37
+ const INTERACTIVE_MODE = !args.has('--no-interactive') && !!process.stdin.isTTY && !JSON_MODE;
38
+ const NO_REPORT = args.has('--no-report');
39
+
64
40
  const report = {
65
- ts: new Date().toISOString(),
66
- os: `${os.platform()} ${os.arch()} ${os.release()}`,
67
- node: process.version,
68
- fix: FIX_MODE,
69
- checks: [],
41
+ ts: new Date().toISOString(),
42
+ version: pkg.version,
43
+ os: `${os.platform()} ${os.arch()} ${os.release()}`,
44
+ node: process.version,
45
+ fix: FIX_MODE,
46
+ checks: [],
47
+ totals: {},
48
+ resolution: null,
49
+ };
50
+
51
+ const i18n = {
52
+ en: {
53
+ subtitle: 'Reliable OpenClaw diagnostics and repair',
54
+ docs: 'Troubleshooting index',
55
+ prevent: 'Prevention guide',
56
+ explore: 'Explore more skills & workflows',
57
+ support: 'Support channel',
58
+ summary: 'Summary',
59
+ tipFix: 'Tip: run with --fix for safe auto-repairs where supported.',
60
+ blocking: 'Blocking issues',
61
+ share: 'Shareable report',
62
+ pasteTitle: 'Pasted error diagnosis',
63
+ pasteMatched: 'Matched issue',
64
+ pasteNoMatch: 'No direct match found',
65
+ pasteNext: 'Next step',
66
+ pasteDetails: 'See detailed fix',
67
+ resolvedAsk: 'Did this fix restore your setup? (y=yes, n=no)',
68
+ resolvedAskAgain: 'After the fallback steps, is it solved now? (y=yes, n=no)',
69
+ resolvedDone: 'Nice. Incident closed.',
70
+ resolvedDoneDetail: 'System is back online.',
71
+ unresolvedReason: 'Why not solved? 1=command error 2=permission issue 3=timeout/network 4=not sure',
72
+ retrySteps: 'Recommended fallback steps',
73
+ escalate: 'Escalation',
74
+ askFull: 'Run full environment diagnostics now? (y=yes, n=no)',
75
+ nonInteractive: 'Interactive follow-up skipped (non-TTY).',
76
+ },
77
+ zh: {
78
+ subtitle: 'OpenClaw ēŽÆå¢ƒčÆŠę–­äøŽå®‰å…Øäæ®å¤',
79
+ docs: 'ę•…éšœęŽ’ęŸ„ę€»č§ˆ',
80
+ prevent: 'é¢„é˜²å»ŗč®®',
81
+ explore: 'ē»§ē»­ęŽ¢ē“¢ skills äøŽ workflows',
82
+ support: 'ę”ÆęŒęø é“',
83
+ summary: 'čÆŠę–­ę€»ē»“',
84
+ tipFix: 'ęē¤ŗļ¼šåÆē”Ø --fix č‡ŖåŠØäæ®å¤åÆå®‰å…Øå¤„ē†ēš„é—®é¢˜ć€‚',
85
+ blocking: '阻唞问题',
86
+ share: 'åÆåˆ†äŗ«ęŠ„å‘Š',
87
+ pasteTitle: 'ē²˜č““é”™čÆÆčÆŠę–­',
88
+ pasteMatched: '匹配到问题',
89
+ pasteNoMatch: 'ęœŖē›“ęŽ„åŒ¹é…åˆ°å·²ēŸ„é—®é¢˜',
90
+ pasteNext: '下一歄',
91
+ pasteDetails: 'ęŸ„ēœ‹čÆ„é—®é¢˜ēš„čÆ¦ē»†äæ®å¤',
92
+ resolvedAsk: 'ę‰§č”ŒåŽę˜Æå¦ę¢å¤ę­£åøøļ¼Ÿ(y=是, n=否)',
93
+ resolvedAskAgain: 'ę‰§č”Œč”„ę•‘ę­„éŖ¤åŽļ¼ŒēŽ°åœØę˜Æå¦å·²č§£å†³ļ¼Ÿ(y=是, n=否)',
94
+ resolvedDone: 'ęžå®šļ¼Œę•…éšœå·²å…³é—­ć€‚',
95
+ resolvedDoneDetail: 'ē³»ē»Ÿå·²ę¢å¤åÆē”Øć€‚',
96
+ unresolvedReason: 'ęœŖč§£å†³ēš„äø»č¦åŽŸå› ļ¼Ÿ1=å‘½ä»¤ęŠ„é”™ 2=ęƒé™é—®é¢˜ 3=č¶…ę—¶/ē½‘ē»œ 4=äøē”®å®š',
97
+ retrySteps: 'å»ŗč®®ēš„č”„ę•‘ę­„éŖ¤',
98
+ escalate: 'å‡ēŗ§å¤„ē†',
99
+ askFull: 'ę˜Æå¦ē»§ē»­ę‰§č”Œå®Œę•“ēŽÆå¢ƒčÆŠę–­ļ¼Ÿ(y=是, n=否)',
100
+ nonInteractive: 'å½“å‰äøę˜Æäŗ¤äŗ’ē»ˆē«Æļ¼Œå·²č·³čæ‡ 0/1 追问。',
101
+ },
102
+ ja: {
103
+ subtitle: 'OpenClaw ē’°å¢ƒć®čØŗę–­ćØå®‰å…ØćŖäæ®å¾©',
104
+ docs: 'ćƒˆćƒ©ćƒ–ćƒ«ć‚·ćƒ„ćƒ¼ćƒ†ć‚£ćƒ³ć‚°äø€č¦§',
105
+ prevent: 'å†ē™ŗé˜²ę­¢ć‚¬ć‚¤ćƒ‰',
106
+ explore: 'skills と workflows ć‚’ęŽ¢ć™',
107
+ support: 'ć‚µćƒćƒ¼ćƒˆēŖ“å£',
108
+ summary: 'čØŗę–­ć‚µćƒžćƒŖćƒ¼',
109
+ tipFix: '--fix ć§å®‰å…Øć«č‡Ŗå‹•äæ®å¾©ć§ćć¾ć™ć€‚',
110
+ blocking: 'ćƒ–ćƒ­ćƒƒć‚­ćƒ³ć‚°å•é”Œ',
111
+ share: 'å…±ęœ‰ćƒ¬ćƒćƒ¼ćƒˆ',
112
+ pasteTitle: 'č²¼ć‚Šä»˜ć‘ć‚Øćƒ©ćƒ¼čØŗę–­',
113
+ pasteMatched: 'äø€č‡“ć—ćŸå•é”Œ',
114
+ pasteNoMatch: 'ę—¢ēŸ„ćƒ‘ć‚æćƒ¼ćƒ³ć«äø€č‡“ć—ć¾ć›ć‚“ć§ć—ćŸ',
115
+ pasteNext: 'ę¬”ć®ć‚¹ćƒ†ćƒƒćƒ—',
116
+ pasteDetails: 'ć“ć®å•é”Œć®č©³ē“°ćŖäæ®ę­£ę‰‹é †',
117
+ resolvedAsk: 'ć“ć®äæ®ę­£ć§å¾©ę—§ć—ć¾ć—ćŸć‹ļ¼Ÿ (y=はい, n=恄恄恈)',
118
+ resolvedAskAgain: 'čæ½åŠ ę‰‹é †ć®å¾Œć€č§£ę±ŗć—ć¾ć—ćŸć‹ļ¼Ÿ (y=はい, n=恄恄恈)',
119
+ resolvedDone: 'č§£ę±ŗå®Œäŗ†ć€‚ć‚¤ćƒ³ć‚·ćƒ‡ćƒ³ćƒˆć‚’ć‚Æćƒ­ćƒ¼ć‚ŗć—ć¾ć—ćŸć€‚',
120
+ resolvedDoneDetail: 'ć‚·ć‚¹ćƒ†ćƒ ćÆę­£åøøć«ęˆ»ć‚Šć¾ć—ćŸć€‚',
121
+ unresolvedReason: 'ęœŖč§£ę±ŗć®ē†ē”±ćÆļ¼Ÿ 1=ć‚³ćƒžćƒ³ćƒ‰å¤±ę•— 2=ęØ©é™å•é”Œ 3=ć‚æć‚¤ćƒ ć‚¢ć‚¦ćƒˆ/ćƒćƒƒćƒˆćƒÆćƒ¼ć‚Æ 4=äøę˜Ž',
122
+ retrySteps: 'ęŽØå„Øćƒ•ć‚©ćƒ¼ćƒ«ćƒćƒƒć‚Æę‰‹é †',
123
+ escalate: 'ć‚Øć‚¹ć‚«ćƒ¬ćƒ¼ć‚·ćƒ§ćƒ³',
124
+ askFull: 'ćƒ•ćƒ«ē’°å¢ƒčØŗę–­ć‚’ē¶šč”Œć—ć¾ć™ć‹ļ¼Ÿ (y=はい, n=恄恄恈)',
125
+ nonInteractive: 'åÆ¾č©±ē«Æęœ«ć§ćÆćŖć„ćŸć‚čæ½č·”č³Ŗå•ć‚’ć‚¹ć‚­ćƒƒćƒ—ć—ć¾ć—ćŸć€‚',
126
+ },
70
127
  };
128
+ const t = i18n[LANG];
71
129
 
72
- function record(id, status, message, helpUrl) {
73
- report.checks.push({ id, status, message });
74
- return { id, status, message, helpUrl };
130
+ if (args.has('--help') || args.has('-h')) {
131
+ console.log(`
132
+ ${chalk.cyan.bold('ClawKit Doctor')} ${chalk.gray(`v${pkg.version}`)}
133
+ ${chalk.gray('by @branzoom (GitHub)')}
134
+
135
+ ${chalk.bold('Usage:')}
136
+ npx clawkit-doctor@latest
137
+ npx clawkit-doctor@latest --fix
138
+ npx clawkit-doctor@latest --paste "your error text"
139
+ npx clawkit-doctor@latest --json
140
+
141
+ ${chalk.bold('Flags:')}
142
+ --fix Auto-repair supported issues
143
+ --paste Diagnose a pasted error and print fix steps
144
+ --full Run full environment checks after --paste
145
+ --lang Language: en | zh | ja
146
+ --no-interactive Skip 0/1 resolution follow-up prompts
147
+ --json Output machine-readable JSON report
148
+ --quiet Suppress banners and non-essential logs
149
+ --no-report Do not print shareable report URL
150
+ --version Print version
151
+ --help Show this help
152
+
153
+ ${chalk.bold('Checks:')}
154
+ Node.js (v22+), Git, Docker, OpenClaw config, permissions,
155
+ gateway token alignment, stale lock files, and key local ports.
156
+
157
+ ${chalk.gray(`Docs: ${SITE_URL}/docs/troubleshooting`)}`);
158
+ process.exit(0);
159
+ }
160
+
161
+ if (args.has('--version') || args.has('-v')) {
162
+ console.log(`clawkit-doctor v${pkg.version}`);
163
+ process.exit(0);
164
+ }
165
+
166
+ function createSpinner(text) {
167
+ if (QUIET_MODE) {
168
+ return {
169
+ text,
170
+ start() { return this; },
171
+ succeed() {},
172
+ fail() {},
173
+ warn() {},
174
+ info() {},
175
+ };
176
+ }
177
+ return ora(text).start();
178
+ }
179
+
180
+ function logLine(message) {
181
+ if (!QUIET_MODE) console.log(message);
75
182
  }
76
183
 
77
- // --- Helpers ---
78
184
  function commandExists(cmd) {
79
- try {
80
- const where = os.platform() === 'win32' ? 'where' : 'which';
81
- execSync(`${where} ${cmd}`, { stdio: 'ignore' });
82
- return true;
83
- } catch {
84
- return false;
85
- }
185
+ try {
186
+ const where = os.platform() === 'win32' ? 'where' : 'which';
187
+ execSync(`${where} ${cmd}`, { stdio: 'ignore' });
188
+ return true;
189
+ } catch {
190
+ return false;
191
+ }
86
192
  }
87
193
 
88
194
  function getCommandVersion(cmd) {
89
- try {
90
- return execSync(`${cmd} --version`, { encoding: 'utf8', timeout: 5000 }).trim().split('\n')[0];
91
- } catch {
92
- return null;
93
- }
195
+ try {
196
+ return execSync(`${cmd} --version`, { encoding: 'utf8', timeout: 5000 }).trim().split('\n')[0];
197
+ } catch {
198
+ return null;
199
+ }
94
200
  }
95
201
 
96
202
  function checkPort(port) {
97
- return new Promise((resolve) => {
98
- const server = net.createServer();
99
- server.listen(port, '127.0.0.1', () => {
100
- server.close(() => resolve(false)); // port is free
101
- });
102
- server.on('error', () => resolve(true)); // port is in use
203
+ return new Promise((resolve) => {
204
+ const server = net.createServer();
205
+ server.listen(port, '127.0.0.1', () => {
206
+ server.close(() => resolve(false));
103
207
  });
208
+ server.on('error', () => resolve(true));
209
+ });
104
210
  }
105
211
 
106
212
  function readConfig() {
107
- try {
108
- const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
109
- return JSON.parse(raw);
110
- } catch {
111
- return null;
112
- }
213
+ try {
214
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
215
+ } catch {
216
+ return null;
217
+ }
113
218
  }
114
219
 
115
220
  function getNestedValue(obj, keyPath) {
116
- return keyPath.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj);
221
+ return keyPath.split('.').reduce((acc, segment) => (acc && acc[segment] !== undefined ? acc[segment] : undefined), obj);
117
222
  }
118
223
 
119
- // ============================================================
120
- // Checks
121
- // ============================================================
122
-
123
- async function checkNodeVersion() {
124
- const spinner = ora('Checking Node.js version...').start();
125
- const major = parseInt(process.version.substring(1).split('.')[0], 10);
224
+ function setNestedValue(obj, keyPath, value) {
225
+ const keys = keyPath.split('.');
226
+ let current = obj;
227
+ for (let i = 0; i < keys.length - 1; i += 1) {
228
+ const key = keys[i];
229
+ if (!current[key] || typeof current[key] !== 'object') current[key] = {};
230
+ current = current[key];
231
+ }
232
+ current[keys[keys.length - 1]] = value;
233
+ }
126
234
 
127
- if (major >= 18) {
128
- spinner.succeed(chalk.green(`Node.js ${process.version}`));
129
- return record('node', 'pass', `Node.js ${process.version}`);
130
- } else {
131
- spinner.fail(chalk.red(`Node.js ${process.version} is too old. Install Node.js v18+.`));
132
- return record('node', 'fail', `Node.js ${process.version} — requires v18+`);
133
- }
235
+ function recordResult(id, status, message, detail, helpUrl, elapsedMs) {
236
+ const item = {
237
+ id,
238
+ status,
239
+ message,
240
+ detail: detail || null,
241
+ helpUrl: helpUrl || null,
242
+ elapsedMs,
243
+ };
244
+ report.checks.push(item);
245
+ return item;
134
246
  }
135
247
 
136
- async function checkGit() {
137
- const spinner = ora('Checking Git...').start();
138
- if (commandExists('git')) {
139
- const ver = getCommandVersion('git') || 'found';
140
- spinner.succeed(chalk.green(`Git: ${ver}`));
141
- return record('git', 'pass', ver);
142
- } else {
143
- const helpUrl = `${SITE_URL}/docs/troubleshooting/spawn-git-enoent`;
144
- spinner.fail(chalk.red(`Git not found. npm packages that use Git will fail.`));
145
- console.log(chalk.gray(` Fix: ${helpUrl}`));
146
- if (FIX_MODE && os.platform() === 'win32') {
147
- console.log(chalk.yellow(' Auto-fix: running winget install Git.Git ...'));
148
- try { execSync('winget install Git.Git', { stdio: 'inherit' }); } catch { /* ignore */ }
149
- }
150
- return record('git', 'fail', 'Git not installed or not in PATH', helpUrl);
151
- }
248
+ async function runCheck(id, title, fn) {
249
+ const start = Date.now();
250
+ const spinner = createSpinner(title);
251
+ try {
252
+ const result = await fn(spinner);
253
+ return recordResult(id, result.status, result.message, result.detail, result.helpUrl, Date.now() - start);
254
+ } catch (error) {
255
+ spinner.fail('Unexpected check error');
256
+ return recordResult(id, 'fail', 'Unexpected check error', String(error.message || error), null, Date.now() - start);
257
+ }
152
258
  }
153
259
 
154
- async function checkDocker() {
155
- const spinner = ora('Checking Docker...').start();
156
- if (commandExists('docker')) {
157
- const ver = getCommandVersion('docker') || 'found';
158
- spinner.succeed(chalk.green(`Docker: ${ver}`));
159
- return record('docker', 'pass', ver);
160
- } else {
161
- const helpUrl = `${SITE_URL}/docs/troubleshooting/spawn-docker-enoent`;
162
- spinner.warn(chalk.yellow('Docker not found. Agent sandbox mode will fail.'));
163
- console.log(chalk.gray(` Fix: Install Docker or disable sandbox: openclaw config set agents.defaults.sandbox.mode off`));
164
- console.log(chalk.gray(` Guide: ${helpUrl}`));
165
- return record('docker', 'warn', 'Docker not installed (sandbox mode will fail)', helpUrl);
166
- }
260
+ async function checkNodeVersion(spinner) {
261
+ const major = Number.parseInt(process.version.slice(1).split('.')[0], 10);
262
+ if (major >= 22) {
263
+ spinner.succeed(chalk.green(`Node.js ${process.version}`));
264
+ return { status: 'pass', message: `Node.js ${process.version}` };
265
+ }
266
+ spinner.fail(chalk.red(`Node.js ${process.version} is too old. Install Node.js v22+.`));
267
+ return {
268
+ status: 'fail',
269
+ message: `Node.js ${process.version} (requires v22+)`,
270
+ helpUrl: `${SITE_URL}/docs/troubleshooting/windows-npm-install-errors`,
271
+ };
167
272
  }
168
273
 
169
- async function checkConfigDir() {
170
- const spinner = ora('Checking config directory...').start();
171
- try {
172
- await fs.promises.access(CONFIG_DIR);
173
- spinner.succeed(chalk.green(`Config directory: ${CONFIG_DIR}`));
174
- return record('config_dir', 'pass', CONFIG_DIR);
175
- } catch {
176
- spinner.fail(chalk.red(`Config directory missing: ${CONFIG_DIR}`));
177
- console.log(chalk.gray(` Fix: Run 'openclaw init' or use ${SITE_URL}/tools/config`));
178
- if (FIX_MODE) {
179
- console.log(chalk.yellow(' Auto-fix: creating directory...'));
180
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
181
- console.log(chalk.green(' Created.'));
182
- }
183
- return record('config_dir', 'fail', `Missing: ${CONFIG_DIR}`);
184
- }
274
+ async function checkGit(spinner) {
275
+ if (!commandExists('git')) {
276
+ const helpUrl = `${SITE_URL}/docs/troubleshooting/spawn-git-enoent`;
277
+ spinner.fail(chalk.red('Git not found in PATH.'));
278
+ logLine(chalk.gray(` Fix: ${helpUrl}`));
279
+ return { status: 'fail', message: 'Git missing', helpUrl };
280
+ }
281
+ const version = getCommandVersion('git') || 'git found';
282
+ spinner.succeed(chalk.green(`Git: ${version}`));
283
+ return { status: 'pass', message: version };
185
284
  }
186
285
 
187
- async function checkConfigFile() {
188
- const spinner = ora('Checking config file (clawhub.json)...').start();
189
- try {
190
- await fs.promises.access(CONFIG_FILE);
191
- } catch {
192
- const helpUrl = `${SITE_URL}/tools/config`;
193
- spinner.fail(chalk.yellow('Config file missing (clawhub.json).'));
194
- console.log(chalk.gray(` Generate one: ${helpUrl}`));
195
- return record('config_file', 'fail', 'clawhub.json not found', helpUrl);
196
- }
286
+ async function checkDocker(spinner) {
287
+ if (!commandExists('docker')) {
288
+ const helpUrl = `${SITE_URL}/docs/troubleshooting/spawn-docker-enoent`;
289
+ spinner.warn(chalk.yellow('Docker not found. Sandbox mode may fail.'));
290
+ logLine(chalk.gray(` Guide: ${helpUrl}`));
291
+ return { status: 'warn', message: 'Docker missing (sandbox mode risk)', helpUrl };
292
+ }
293
+ const version = getCommandVersion('docker') || 'docker found';
294
+ spinner.succeed(chalk.green(`Docker: ${version}`));
295
+ return { status: 'pass', message: version };
296
+ }
197
297
 
198
- // Validate JSON syntax
199
- try {
200
- const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
201
- JSON.parse(raw);
202
- spinner.succeed(chalk.green('Config file found and valid JSON.'));
203
- return record('config_file', 'pass', 'clawhub.json valid');
204
- } catch (e) {
205
- const helpUrl = `${SITE_URL}/docs/troubleshooting/json-parse-errors`;
206
- spinner.fail(chalk.red(`Config file has invalid JSON: ${e.message}`));
207
- console.log(chalk.gray(` Fix: ${helpUrl}`));
208
- return record('config_file', 'fail', `Invalid JSON: ${e.message}`, helpUrl);
209
- }
298
+ async function checkConfigDir(spinner) {
299
+ if (fs.existsSync(CONFIG_DIR)) {
300
+ spinner.succeed(chalk.green(`Config directory: ${CONFIG_DIR}`));
301
+ return { status: 'pass', message: CONFIG_DIR };
302
+ }
303
+
304
+ if (FIX_MODE) {
305
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
306
+ spinner.warn(chalk.yellow(`Config directory created: ${CONFIG_DIR}`));
307
+ return { status: 'warn', message: `Created missing directory: ${CONFIG_DIR}` };
308
+ }
309
+
310
+ spinner.fail(chalk.red(`Missing config directory: ${CONFIG_DIR}`));
311
+ return {
312
+ status: 'fail',
313
+ message: `Missing directory: ${CONFIG_DIR}`,
314
+ helpUrl: `${SITE_URL}/tools/config`,
315
+ };
210
316
  }
211
317
 
212
- async function checkPermissions() {
213
- const spinner = ora('Checking directory permissions...').start();
214
- try {
215
- if (!fs.existsSync(CONFIG_DIR)) {
216
- spinner.info(chalk.gray('Skipped (directory missing).'));
217
- return record('permissions', 'skip', 'Directory missing');
218
- }
219
- await fs.promises.access(CONFIG_DIR, fs.constants.W_OK);
220
- spinner.succeed(chalk.green('Write permission OK for ~/.openclaw'));
221
- return record('permissions', 'pass', 'Write permission OK');
222
- } catch {
223
- spinner.fail(chalk.red('No write permission for ~/.openclaw'));
224
- console.log(chalk.gray(' Fix: sudo chown -R $(whoami) ~/.openclaw'));
225
- if (FIX_MODE && os.platform() !== 'win32') {
226
- console.log(chalk.yellow(' Auto-fix: fixing ownership...'));
227
- try { execSync(`chown -R $(whoami) "${CONFIG_DIR}"`, { stdio: 'inherit' }); } catch { /* ignore */ }
228
- }
229
- return record('permissions', 'fail', 'No write permission');
230
- }
318
+ async function checkConfigFile(spinner) {
319
+ if (!fs.existsSync(CONFIG_FILE)) {
320
+ spinner.fail(chalk.red('openclaw.json not found.'));
321
+ return {
322
+ status: 'fail',
323
+ message: 'Missing openclaw.json',
324
+ helpUrl: `${SITE_URL}/tools/config`,
325
+ };
326
+ }
327
+
328
+ try {
329
+ JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
330
+ spinner.succeed(chalk.green('openclaw.json syntax valid.'));
331
+ return { status: 'pass', message: 'openclaw.json valid' };
332
+ } catch (error) {
333
+ const helpUrl = `${SITE_URL}/docs/troubleshooting/json-parse-errors`;
334
+ spinner.fail(chalk.red('openclaw.json has invalid JSON syntax.'));
335
+ logLine(chalk.gray(` Fix: ${helpUrl}`));
336
+ return {
337
+ status: 'fail',
338
+ message: 'Invalid JSON in openclaw.json',
339
+ detail: String(error.message || error),
340
+ helpUrl,
341
+ };
342
+ }
231
343
  }
232
344
 
233
- async function checkGatewayTokens() {
234
- const spinner = ora('Checking gateway token alignment...').start();
235
- const config = readConfig();
236
- if (!config) {
237
- spinner.info(chalk.gray('Skipped (no valid config).'));
238
- return record('tokens', 'skip', 'No config to check');
345
+ async function checkPermissions(spinner) {
346
+ if (!fs.existsSync(CONFIG_DIR)) {
347
+ spinner.info(chalk.gray('Skipped: config directory missing.'));
348
+ return { status: 'skip', message: 'Config directory missing' };
349
+ }
350
+
351
+ try {
352
+ await fs.promises.access(CONFIG_DIR, fs.constants.W_OK);
353
+ spinner.succeed(chalk.green('Write permission OK for ~/.openclaw'));
354
+ return { status: 'pass', message: 'Write permission OK' };
355
+ } catch {
356
+ const fixCmd = `chown -R $(whoami) "${CONFIG_DIR}"`;
357
+ if (FIX_MODE && os.platform() !== 'win32') {
358
+ try {
359
+ execSync(fixCmd, { stdio: 'ignore' });
360
+ spinner.warn(chalk.yellow('Permission repaired with chown.'));
361
+ return { status: 'warn', message: 'Permission repaired by --fix', detail: fixCmd };
362
+ } catch {
363
+ spinner.fail(chalk.red('No write permission for ~/.openclaw'));
364
+ }
365
+ } else {
366
+ spinner.fail(chalk.red('No write permission for ~/.openclaw'));
239
367
  }
240
368
 
241
- const authToken = getNestedValue(config, 'gateway.auth.token');
242
- const remoteToken = getNestedValue(config, 'gateway.remote.token');
243
- const envToken = process.env.OPENCLAW_GATEWAY_TOKEN;
244
-
245
- if (!authToken && !remoteToken) {
246
- spinner.info(chalk.gray('No gateway tokens configured (will auto-generate).'));
247
- return record('tokens', 'info', 'No tokens configured');
248
- }
369
+ return {
370
+ status: 'fail',
371
+ message: 'No write permission for ~/.openclaw',
372
+ detail: `Run: sudo ${fixCmd}`,
373
+ };
374
+ }
375
+ }
249
376
 
250
- // Check env override
251
- if (envToken && authToken && envToken !== authToken) {
252
- const helpUrl = `${SITE_URL}/docs/troubleshooting/gateway-token-mismatch`;
253
- spinner.fail(chalk.red('OPENCLAW_GATEWAY_TOKEN env var overrides config! Tokens will mismatch.'));
254
- console.log(chalk.gray(` Env: ${envToken.substring(0, 8)}... Config: ${authToken.substring(0, 8)}...`));
255
- console.log(chalk.gray(` Fix: ${helpUrl}`));
256
- return record('tokens', 'fail', 'Env var overrides gateway.auth.token', helpUrl);
377
+ async function checkGatewayTokens(spinner) {
378
+ const config = readConfig();
379
+ if (!config) {
380
+ spinner.info(chalk.gray('Skipped: no readable openclaw.json'));
381
+ return { status: 'skip', message: 'No valid config' };
382
+ }
383
+
384
+ const authToken = getNestedValue(config, 'gateway.auth.token');
385
+ const remoteToken = getNestedValue(config, 'gateway.remote.token');
386
+ const envToken = process.env.OPENCLAW_GATEWAY_TOKEN;
387
+
388
+ if (!authToken && !remoteToken && !envToken) {
389
+ spinner.info(chalk.gray('No gateway tokens configured.'));
390
+ return { status: 'info', message: 'No tokens configured' };
391
+ }
392
+
393
+ if (envToken && authToken && envToken !== authToken) {
394
+ const helpUrl = `${SITE_URL}/docs/troubleshooting/gateway-token-mismatch`;
395
+ spinner.fail(chalk.red('OPENCLAW_GATEWAY_TOKEN conflicts with config token.'));
396
+ return {
397
+ status: 'fail',
398
+ message: 'Env token overrides gateway.auth.token',
399
+ detail: `env=${envToken.slice(0, 8)}... config=${authToken.slice(0, 8)}...`,
400
+ helpUrl,
401
+ };
402
+ }
403
+
404
+ if (authToken && remoteToken && authToken !== remoteToken) {
405
+ const helpUrl = `${SITE_URL}/docs/troubleshooting/gateway-token-mismatch`;
406
+ if (FIX_MODE) {
407
+ setNestedValue(config, 'gateway.remote.token', authToken);
408
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
409
+ spinner.warn(chalk.yellow('Token mismatch fixed (remote token aligned).'));
410
+ return { status: 'warn', message: 'Aligned gateway.remote.token with gateway.auth.token', helpUrl };
257
411
  }
258
412
 
259
- if (authToken && remoteToken && authToken !== remoteToken) {
260
- const helpUrl = `${SITE_URL}/docs/troubleshooting/gateway-token-mismatch`;
261
- spinner.fail(chalk.red('gateway.auth.token ≠ gateway.remote.token — clients cannot connect.'));
262
- console.log(chalk.gray(` Auth: ${authToken.substring(0, 8)}...`));
263
- console.log(chalk.gray(` Remote: ${remoteToken.substring(0, 8)}...`));
264
- console.log(chalk.gray(` Fix: ${helpUrl}`));
265
- if (FIX_MODE) {
266
- console.log(chalk.yellow(' Auto-fix: aligning remote token to match auth token...'));
267
- config.gateway.remote.token = authToken;
268
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
269
- console.log(chalk.green(' Fixed. Restart gateway: openclaw gateway restart'));
270
- }
271
- return record('tokens', 'fail', 'auth.token ≠ remote.token', helpUrl);
272
- }
413
+ spinner.fail(chalk.red('gateway.auth.token and gateway.remote.token mismatch.'));
414
+ return {
415
+ status: 'fail',
416
+ message: 'auth.token != remote.token',
417
+ detail: `auth=${authToken.slice(0, 8)}... remote=${remoteToken.slice(0, 8)}...`,
418
+ helpUrl,
419
+ };
420
+ }
421
+
422
+ spinner.succeed(chalk.green('Gateway tokens aligned.'));
423
+ return { status: 'pass', message: 'Tokens aligned' };
424
+ }
273
425
 
274
- spinner.succeed(chalk.green('Gateway tokens aligned.'));
275
- return record('tokens', 'pass', 'Tokens aligned');
426
+ async function checkStaleLocks(spinner) {
427
+ if (!fs.existsSync(SESSIONS_DIR)) {
428
+ spinner.info(chalk.gray('No sessions directory.'));
429
+ return { status: 'skip', message: 'No sessions directory' };
430
+ }
431
+
432
+ const lockFiles = fs.readdirSync(SESSIONS_DIR).filter((name) => name.endsWith('.lock'));
433
+ if (!lockFiles.length) {
434
+ spinner.succeed(chalk.green('No stale lock files.'));
435
+ return { status: 'pass', message: 'No lock files' };
436
+ }
437
+
438
+ if (FIX_MODE) {
439
+ lockFiles.forEach((file) => fs.unlinkSync(path.join(SESSIONS_DIR, file)));
440
+ spinner.warn(chalk.yellow(`Removed ${lockFiles.length} lock file(s).`));
441
+ return { status: 'warn', message: `Removed ${lockFiles.length} stale lock(s)` };
442
+ }
443
+
444
+ const helpUrl = `${SITE_URL}/docs/troubleshooting/gateway-lock-timeout`;
445
+ spinner.warn(chalk.yellow(`Found ${lockFiles.length} stale lock(s).`));
446
+ return {
447
+ status: 'warn',
448
+ message: `${lockFiles.length} stale lock file(s) found`,
449
+ detail: lockFiles.join(', '),
450
+ helpUrl,
451
+ };
276
452
  }
277
453
 
278
- async function checkStaleLocks() {
279
- const spinner = ora('Checking for stale lock files...').start();
280
- if (!fs.existsSync(SESSIONS_DIR)) {
281
- spinner.info(chalk.gray('No sessions directory.'));
282
- return record('locks', 'skip', 'No sessions directory');
454
+ async function checkPortUsage(id, title, port, expectedInUse) {
455
+ return runCheck(id, title, async (spinner) => {
456
+ const inUse = await checkPort(port);
457
+ if (expectedInUse && inUse) {
458
+ spinner.succeed(chalk.green(`Port ${port} is in use.`));
459
+ return { status: 'pass', message: `Port ${port} in use` };
283
460
  }
284
-
285
- try {
286
- const files = fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.lock'));
287
- if (files.length === 0) {
288
- spinner.succeed(chalk.green('No stale lock files.'));
289
- return record('locks', 'pass', 'No lock files');
290
- }
291
-
292
- const helpUrl = `${SITE_URL}/docs/troubleshooting/gateway-lock-timeout`;
293
- spinner.warn(chalk.yellow(`Found ${files.length} lock file(s): ${files.join(', ')}`));
294
- console.log(chalk.gray(` This may cause "gateway already running" errors.`));
295
- console.log(chalk.gray(` Fix: ${helpUrl}`));
296
-
297
- if (FIX_MODE) {
298
- console.log(chalk.yellow(' Auto-fix: removing stale locks...'));
299
- for (const f of files) {
300
- fs.unlinkSync(path.join(SESSIONS_DIR, f));
301
- }
302
- console.log(chalk.green(` Removed ${files.length} lock file(s).`));
303
- }
304
- return record('locks', 'warn', `${files.length} lock file(s) found`, helpUrl);
305
- } catch (e) {
306
- spinner.info(chalk.gray(`Could not check locks: ${e.message}`));
307
- return record('locks', 'skip', e.message);
461
+ if (expectedInUse && !inUse) {
462
+ spinner.info(chalk.gray(`Port ${port} is free.`));
463
+ return { status: 'info', message: `Port ${port} free` };
308
464
  }
309
- }
310
-
311
- async function checkGatewayPort() {
312
- const spinner = ora('Checking Gateway port (18789)...').start();
313
- const inUse = await checkPort(18789);
314
- if (inUse) {
315
- spinner.succeed(chalk.green('Gateway port 18789 is in use (gateway likely running).'));
316
- return record('gateway_port', 'pass', 'Port 18789 in use (gateway running)');
317
- } else {
318
- spinner.info(chalk.gray('Gateway port 18789 is free (gateway not running).'));
319
- return record('gateway_port', 'info', 'Port 18789 free (gateway not running)');
465
+ if (!expectedInUse && inUse) {
466
+ spinner.info(chalk.gray(`Port ${port} is in use.`));
467
+ return { status: 'info', message: `Port ${port} in use` };
320
468
  }
469
+ spinner.succeed(chalk.green(`Port ${port} is free.`));
470
+ return { status: 'pass', message: `Port ${port} free` };
471
+ });
321
472
  }
322
473
 
323
- async function checkAgentPort() {
324
- const spinner = ora('Checking Agent port (3000)...').start();
325
- const inUse = await checkPort(3000);
326
- if (inUse) {
327
- spinner.succeed(chalk.green('Agent port 3000 is in use (agent likely running).'));
328
- return record('agent_port', 'pass', 'Port 3000 in use');
329
- } else {
330
- spinner.info(chalk.gray('Agent port 3000 is free (agent not running).'));
331
- return record('agent_port', 'info', 'Port 3000 free');
332
- }
474
+ function generateReportUrl() {
475
+ const payload = {
476
+ t: report.ts,
477
+ v: report.version,
478
+ o: report.os,
479
+ n: report.node,
480
+ f: report.fix,
481
+ c: report.checks.map((c) => [c.id, c.status[0], c.message.slice(0, 100)]),
482
+ };
483
+ return `${SITE_URL}/tools/doctor?r=${Buffer.from(JSON.stringify(payload)).toString('base64url')}`;
333
484
  }
334
485
 
335
- async function checkCDPPort() {
336
- const spinner = ora('Checking Browser CDP port (18800)...').start();
337
- const inUse = await checkPort(18800);
338
- if (inUse) {
339
- spinner.info(chalk.gray('CDP port 18800 in use (browser control active or stray process).'));
340
- return record('cdp_port', 'info', 'Port 18800 in use');
341
- } else {
342
- spinner.succeed(chalk.green('CDP port 18800 is free.'));
343
- return record('cdp_port', 'pass', 'Port 18800 free');
344
- }
486
+ function diagnosePastedError(text) {
487
+ const normalized = text.toLowerCase();
488
+ const rules = [
489
+ {
490
+ id: 'gateway-args',
491
+ pattern: /too many arguments.*gateway/,
492
+ title: "Too many arguments for 'gateway'",
493
+ docUrl: `${SITE_URL}/docs/troubleshooting/gateway-lock-timeout`,
494
+ steps: ['Run: openclaw gateway --help', 'Update command flags to current syntax', 'Retry after removing deprecated args'],
495
+ },
496
+ {
497
+ id: 'allowed-origins',
498
+ pattern: /non-loopback.*allowedorigins|allowedorigins/i,
499
+ title: 'Control UI requires allowedOrigins',
500
+ docUrl: `${SITE_URL}/docs/troubleshooting/device-token-mismatch`,
501
+ steps: ['Set explicit allowedOrigins in openclaw.json', 'Restart gateway service', 'Validate via localhost first'],
502
+ },
503
+ {
504
+ id: 'docker-access',
505
+ pattern: /docker pull access denied.*openclaw\/openclaw|pull access denied/i,
506
+ title: 'Docker image pull access denied',
507
+ docUrl: `${SITE_URL}/docs/troubleshooting/docker-macos-errors`,
508
+ steps: ['Check image name/tag', 'Run: docker login', 'Use mirrored/public image per docs'],
509
+ },
510
+ {
511
+ id: 'err-empty-response',
512
+ pattern: /err_empty_response/i,
513
+ title: 'ERR_EMPTY_RESPONSE',
514
+ docUrl: `${SITE_URL}/docs/troubleshooting/connection-errors`,
515
+ steps: ['Check gateway/agent process status', 'Verify port 18789/3000 are reachable', 'Rotate token if mismatch suspected'],
516
+ },
517
+ {
518
+ id: 'systemctl-user',
519
+ pattern: /systemctl\s+--user.*unavailable|eacces/i,
520
+ title: 'systemctl --user unavailable',
521
+ docUrl: `${SITE_URL}/docs/troubleshooting/windows-gateway-errors`,
522
+ steps: ['Use foreground run mode or service manager for your OS', 'Avoid systemctl --user on unsupported environments', 'Apply OS-specific startup instructions'],
523
+ },
524
+ {
525
+ id: 'provider-400',
526
+ pattern: /400 provider returned error|provider returned error/i,
527
+ title: 'Provider returned 400',
528
+ docUrl: `${SITE_URL}/docs/troubleshooting/api-key-problems`,
529
+ steps: ['Verify API key and model name', 'Check provider request payload schema', 'Reduce unsupported params and retry'],
530
+ },
531
+ {
532
+ id: 'telegram-409',
533
+ pattern: /telegram.*409|getupdates conflict/i,
534
+ title: 'Telegram getUpdates conflict 409',
535
+ docUrl: `${SITE_URL}/docs/troubleshooting/message-ordering-conflict`,
536
+ steps: ['Keep only one active polling consumer', 'Stop duplicate bot sessions', 'Restart bot once with a single token'],
537
+ },
538
+ {
539
+ id: 'run-timeout',
540
+ pattern: /embedded run timeout|run timeout/i,
541
+ title: 'Embedded run timeout',
542
+ docUrl: `${SITE_URL}/docs/troubleshooting/ollama-timeout-errors`,
543
+ steps: ['Increase timeout in config', 'Reduce tool chain depth', 'Validate provider latency and retry'],
544
+ },
545
+ {
546
+ id: 'token-mismatch',
547
+ pattern: /token mismatch|unauthorized.*device|pairing required/i,
548
+ title: 'Gateway token mismatch',
549
+ docUrl: `${SITE_URL}/docs/troubleshooting/gateway-token-mismatch`,
550
+ steps: ['Align gateway.auth.token and gateway.remote.token', 'Unset conflicting OPENCLAW_GATEWAY_TOKEN', 'Restart gateway and agent'],
551
+ },
552
+ ];
553
+
554
+ const matched = rules.find((rule) => rule.pattern.test(normalized));
555
+ if (!matched) return null;
556
+ return matched;
345
557
  }
346
558
 
347
- // ============================================================
348
- // Report Generator
349
- // ============================================================
350
-
351
- function generateReportUrl() {
352
- const compressed = JSON.stringify({
353
- t: report.ts,
354
- o: report.os,
355
- n: report.node,
356
- f: report.fix,
357
- c: report.checks.map(c => [c.id, c.status[0], c.message.substring(0, 80)]),
559
+ function ask(question) {
560
+ const rl = readline.createInterface({
561
+ input: process.stdin,
562
+ output: process.stdout,
563
+ });
564
+ return new Promise((resolve) => {
565
+ rl.question(`${question}\n> `, (answer) => {
566
+ rl.close();
567
+ resolve((answer || '').trim());
358
568
  });
359
- const encoded = Buffer.from(compressed).toString('base64url');
360
- return `${SITE_URL}/tools/doctor?r=${encoded}`;
569
+ });
361
570
  }
362
571
 
363
- // ============================================================
364
- // Main
365
- // ============================================================
366
-
367
- const steps = [
368
- checkNodeVersion,
369
- checkGit,
370
- checkDocker,
371
- checkConfigDir,
372
- checkConfigFile,
373
- checkPermissions,
374
- checkGatewayTokens,
375
- checkStaleLocks,
376
- checkGatewayPort,
377
- checkAgentPort,
378
- checkCDPPort,
379
- ];
380
-
381
- async function run() {
382
- console.clear();
383
- console.log(chalk.cyan.bold('\nšŸ¦ž ClawKit Doctor v2.0\n'));
384
-
385
- if (FIX_MODE) {
386
- console.log(chalk.yellow.bold('⚔ Fix mode enabled — will auto-repair issues where possible.\n'));
387
- }
388
-
389
- for (const step of steps) {
390
- await step();
391
- }
392
-
393
- // Summary
394
- const fails = report.checks.filter(c => c.status === 'fail');
395
- const warns = report.checks.filter(c => c.status === 'warn');
396
- const passes = report.checks.filter(c => c.status === 'pass');
572
+ async function askBinary(question, fallback = 'n') {
573
+ if (!INTERACTIVE_MODE) return fallback;
574
+ const answer = (await ask(question)).toLowerCase();
575
+ if (answer === 'y' || answer === 'yes' || answer === '1') return 'y';
576
+ if (answer === 'n' || answer === 'no' || answer === '0') return 'n';
577
+ return fallback;
578
+ }
397
579
 
398
- console.log('');
399
- console.log(chalk.bold('─'.repeat(50)));
580
+ function getFallbackSteps(reason, match) {
581
+ if (reason === '1') {
582
+ return [
583
+ 'Re-run the command with verbose output and copy the first 20 error lines.',
584
+ 'Verify CLI syntax with: openclaw gateway --help',
585
+ `Open issue-specific details: ${match.docUrl}`,
586
+ ];
587
+ }
588
+ if (reason === '2') {
589
+ return [
590
+ `Fix ownership: sudo chown -R $(whoami) "${CONFIG_DIR}"`,
591
+ 'Retry with: npx clawkit-doctor@latest --fix',
592
+ `If still blocked, open: ${SITE_URL}/docs/troubleshooting/windows-gateway-errors`,
593
+ ];
594
+ }
595
+ if (reason === '3') {
596
+ return [
597
+ 'Check service status and ports: 18789 / 3000',
598
+ 'Retry with stable network or increased timeout settings',
599
+ `Timeout-focused guide: ${SITE_URL}/docs/troubleshooting/ollama-timeout-errors`,
600
+ ];
601
+ }
602
+ return [
603
+ 'Run full diagnostics: npx clawkit-doctor@latest --full',
604
+ `Open troubleshooting index: ${SITE_URL}/docs/troubleshooting`,
605
+ `Then open matched details: ${match.docUrl}`,
606
+ ];
607
+ }
400
608
 
401
- if (fails.length === 0 && warns.length === 0) {
402
- console.log(chalk.green.bold('\nāœ… All checks passed! Your environment looks healthy.\n'));
609
+ async function run() {
610
+ if (!QUIET_MODE) {
611
+ console.log(chalk.cyan.bold('\nClawKit Doctor'));
612
+ console.log(chalk.gray(`v${pkg.version} • @branzoom (GitHub)`));
613
+ console.log(chalk.gray(`${t.subtitle}\n`));
614
+ }
615
+
616
+ let shouldContinueToFull = FULL_MODE;
617
+ if (PASTE_TEXT) {
618
+ const match = diagnosePastedError(PASTE_TEXT);
619
+ report.pastedError = { input: PASTE_TEXT.slice(0, 600), matchId: match?.id || null };
620
+
621
+ if (JSON_MODE) {
622
+ report.pasteDiagnosis = match
623
+ ? { issueId: match.id, title: match.title, steps: match.steps, docUrl: match.docUrl }
624
+ : { issueId: null, title: null, steps: [], docUrl: `${SITE_URL}/tools/doctor` };
625
+ if (!FULL_MODE) {
626
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
627
+ return;
628
+ }
403
629
  } else {
404
- console.log(chalk.yellow.bold(`\nšŸ“‹ Summary: ${passes.length} passed, ${warns.length} warnings, ${fails.length} failed\n`));
405
-
406
- if (fails.length > 0) {
407
- console.log(chalk.red.bold(' Issues to fix:'));
408
- for (const f of fails) {
409
- console.log(chalk.red(` āœ— ${f.message}`));
630
+ console.log(chalk.bold(`\n${t.pasteTitle}`));
631
+ if (match) {
632
+ console.log(chalk.green(` ${t.pasteMatched}: ${match.title}`));
633
+ match.steps.forEach((step, index) => {
634
+ console.log(chalk.gray(` ${index + 1}. ${step}`));
635
+ });
636
+ console.log(chalk.gray(` ${t.pasteDetails}: ${match.docUrl}`));
637
+
638
+ if (INTERACTIVE_MODE) {
639
+ const firstResolved = await askBinary(t.resolvedAsk, 'n');
640
+ if (firstResolved === 'y') {
641
+ report.resolution = { solved: 1, round: 1, reason: null };
642
+ console.log(chalk.green(` ${t.resolvedDone}`));
643
+ console.log(chalk.gray(` ${t.resolvedDoneDetail}`));
644
+ console.log(chalk.gray(` ${t.prevent}: ${SITE_URL}/docs/guides/stable-ops`));
645
+ console.log(chalk.cyan(` ${t.explore}: ${SITE_URL}/skills`));
646
+ } else {
647
+ const reason = await ask(t.unresolvedReason);
648
+ const normalizedReason = ['1', '2', '3', '4'].includes(reason) ? reason : '4';
649
+ const fallbackSteps = getFallbackSteps(normalizedReason, match);
650
+ console.log(chalk.yellow(` ${t.retrySteps}:`));
651
+ fallbackSteps.forEach((step, index) => {
652
+ console.log(chalk.gray(` ${index + 1}. ${step}`));
653
+ });
654
+
655
+ const secondResolved = await askBinary(t.resolvedAskAgain, 'n');
656
+ if (secondResolved === 'y') {
657
+ report.resolution = { solved: 1, round: 2, reason: normalizedReason };
658
+ console.log(chalk.green(` ${t.resolvedDone}`));
659
+ console.log(chalk.gray(` ${t.resolvedDoneDetail}`));
660
+ console.log(chalk.gray(` ${t.prevent}: ${SITE_URL}/docs/guides/stable-ops`));
661
+ console.log(chalk.cyan(` ${t.explore}: ${SITE_URL}/skills`));
662
+ } else {
663
+ report.resolution = { solved: 0, round: 2, reason: normalizedReason };
664
+ console.log(chalk.red(` ${t.escalate}:`));
665
+ console.log(chalk.gray(` ${t.pasteDetails}: ${match.docUrl}`));
666
+ console.log(chalk.gray(` ${t.docs}: ${SITE_URL}/docs/troubleshooting`));
667
+ console.log(chalk.gray(` ${t.support}: ${SITE_URL}/contact`));
668
+ shouldContinueToFull = shouldContinueToFull || (await askBinary(t.askFull, 'n')) === 'y';
410
669
  }
670
+ }
671
+ } else {
672
+ console.log(chalk.gray(` ${t.nonInteractive}`));
411
673
  }
412
- if (warns.length > 0) {
413
- console.log(chalk.yellow.bold(' Warnings:'));
414
- for (const w of warns) {
415
- console.log(chalk.yellow(` ⚠ ${w.message}`));
416
- }
674
+ } else {
675
+ console.log(chalk.yellow(` ${t.pasteNoMatch}`));
676
+ console.log(chalk.gray(` ${t.pasteNext}: ${SITE_URL}/tools/doctor`));
677
+ console.log(chalk.gray(` ${t.docs}: ${SITE_URL}/docs/troubleshooting`));
678
+ if (INTERACTIVE_MODE) {
679
+ shouldContinueToFull = shouldContinueToFull || (await askBinary(t.askFull, 'y')) === 'y';
417
680
  }
681
+ }
418
682
 
419
- if (!FIX_MODE && fails.length > 0) {
420
- console.log(chalk.cyan(`\n šŸ’” Run with --fix to auto-repair: npx clawkit-doctor@latest --fix\n`));
421
- }
683
+ if (!shouldContinueToFull) return;
684
+ console.log('');
422
685
  }
686
+ }
687
+
688
+ await runCheck('node', 'Checking Node.js version...', checkNodeVersion);
689
+ await runCheck('git', 'Checking Git...', checkGit);
690
+ await runCheck('docker', 'Checking Docker...', checkDocker);
691
+ await runCheck('config_dir', 'Checking OpenClaw config directory...', checkConfigDir);
692
+ await runCheck('config_file', 'Checking openclaw.json...', checkConfigFile);
693
+ await runCheck('permissions', 'Checking directory permissions...', checkPermissions);
694
+ await runCheck('tokens', 'Checking gateway token alignment...', checkGatewayTokens);
695
+ await runCheck('locks', 'Checking stale lock files...', checkStaleLocks);
696
+ await checkPortUsage('gateway_port', 'Checking gateway port (18789)...', 18789, true);
697
+ await checkPortUsage('agent_port', 'Checking agent port (3000)...', 3000, true);
698
+ await checkPortUsage('cdp_port', 'Checking browser CDP port (18800)...', 18800, false);
699
+
700
+ const totals = {
701
+ pass: report.checks.filter((x) => x.status === 'pass').length,
702
+ warn: report.checks.filter((x) => x.status === 'warn').length,
703
+ fail: report.checks.filter((x) => x.status === 'fail').length,
704
+ info: report.checks.filter((x) => x.status === 'info').length,
705
+ skip: report.checks.filter((x) => x.status === 'skip').length,
706
+ };
707
+ report.totals = totals;
708
+
709
+ if (JSON_MODE) {
710
+ if (!NO_REPORT) report.reportUrl = generateReportUrl();
711
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
712
+ return;
713
+ }
714
+
715
+ console.log(chalk.bold('\n' + '-'.repeat(56)));
716
+ console.log(chalk.bold(t.summary));
717
+ console.log(` Passed: ${chalk.green(totals.pass)} Warnings: ${chalk.yellow(totals.warn)} Failed: ${chalk.red(totals.fail)} Info/Skip: ${totals.info + totals.skip}`);
718
+
719
+ if (totals.fail > 0 && !FIX_MODE) {
720
+ console.log(chalk.cyan(`\n${t.tipFix}`));
721
+ console.log(chalk.gray(' npx clawkit-doctor@latest --fix'));
722
+ }
723
+
724
+ const blocking = report.checks.filter((c) => c.status === 'fail');
725
+ if (blocking.length) {
726
+ console.log(chalk.red.bold(`\n${t.blocking}`));
727
+ blocking.forEach((item) => {
728
+ console.log(chalk.red(` - [${item.id}] ${item.message}`));
729
+ if (item.helpUrl) console.log(chalk.gray(` ${item.helpUrl}`));
730
+ });
731
+ }
732
+
733
+ if (!NO_REPORT) {
734
+ console.log(chalk.bold(`\n${t.share}`));
735
+ console.log(` ${generateReportUrl()}`);
736
+ }
423
737
 
424
- // Report URL
425
- const reportUrl = generateReportUrl();
426
- console.log(chalk.gray('─'.repeat(50)));
427
- console.log(chalk.cyan.bold('\nšŸ“‹ Share this report:'));
428
- console.log(chalk.white(` ${reportUrl}\n`));
429
- console.log(chalk.gray(`For full troubleshooting guides: ${SITE_URL}/docs/troubleshooting\n`));
738
+ console.log(chalk.gray(`\n${t.docs}: ${SITE_URL}/docs/troubleshooting`));
739
+ console.log(chalk.gray(`${t.prevent}: ${SITE_URL}/docs/guides/stable-ops\n`));
430
740
  }
431
741
 
432
- run().catch(err => {
433
- console.error(chalk.red('\nāŒ Unexpected Error:'), err.message);
434
- process.exit(1);
742
+ run().catch((error) => {
743
+ console.error(chalk.red('\nUnexpected Error:'), error.message || error);
744
+ process.exit(1);
435
745
  });
package/package.json CHANGED
@@ -1,24 +1,37 @@
1
1
  {
2
- "name": "clawkit-doctor",
3
- "version": "2.0.1",
4
- "description": "Diagnostic and auto-fix tool for OpenClaw environment",
5
- "main": "bin/index.js",
6
- "bin": {
7
- "clawkit-doctor": "bin/index.js"
8
- },
9
- "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
11
- },
12
- "keywords": [
13
- "openclaw",
14
- "diagnostic",
15
- "doctor",
16
- "cli"
17
- ],
18
- "author": "OpenClaw Team",
19
- "license": "MIT",
20
- "dependencies": {
21
- "chalk": "^4.1.2",
22
- "ora": "^5.4.1"
23
- }
24
- }
2
+ "name": "clawkit-doctor",
3
+ "version": "2.2.0",
4
+ "description": "Guided OpenClaw diagnostic CLI with issue-first recovery flow",
5
+ "main": "bin/index.js",
6
+ "bin": {
7
+ "clawkit-doctor": "bin/index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node bin/index.js --json --no-report"
11
+ },
12
+ "keywords": [
13
+ "openclaw",
14
+ "diagnostic",
15
+ "doctor",
16
+ "cli",
17
+ "troubleshooting"
18
+ ],
19
+ "author": "branzoom",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/branzoom/getclawkit-web.git",
24
+ "directory": "packages/clawkit-doctor"
25
+ },
26
+ "homepage": "https://getclawkit.com/tools/doctor",
27
+ "bugs": {
28
+ "url": "https://github.com/branzoom/getclawkit-web/issues"
29
+ },
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
33
+ "dependencies": {
34
+ "chalk": "^4.1.2",
35
+ "ora": "^5.4.1"
36
+ }
37
+ }