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.
- package/README.md +72 -12
- package/bin/index.js +663 -353
- package/package.json +36 -23
package/README.md
CHANGED
|
@@ -1,28 +1,88 @@
|
|
|
1
1
|
# ClawKit Doctor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Fast diagnostics and safe auto-repair for OpenClaw environments.
|
|
4
|
+
|
|
5
|
+
Maintainer: **@branzoom**
|
|
4
6
|
|
|
5
7
|
## Usage
|
|
6
8
|
|
|
7
|
-
|
|
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
|
-
|
|
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.
|
|
18
|
-
2.
|
|
19
|
-
3.
|
|
20
|
-
4.
|
|
21
|
-
5.
|
|
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
|
-
|
|
73
|
+
This avoids hard-sell behavior and keeps flow focused on incident recovery.
|
|
24
74
|
|
|
25
|
-
|
|
75
|
+
## Built-in links (contextual)
|
|
26
76
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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, '
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
221
|
+
return keyPath.split('.').reduce((acc, segment) => (acc && acc[segment] !== undefined ? acc[segment] : undefined), obj);
|
|
117
222
|
}
|
|
118
223
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
369
|
+
return {
|
|
370
|
+
status: 'fail',
|
|
371
|
+
message: 'No write permission for ~/.openclaw',
|
|
372
|
+
detail: `Run: sudo ${fixCmd}`,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
249
376
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
360
|
-
return `${SITE_URL}/tools/doctor?r=${encoded}`;
|
|
569
|
+
});
|
|
361
570
|
}
|
|
362
571
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
399
|
-
|
|
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
|
-
|
|
402
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
425
|
-
|
|
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(
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
}
|