notoken-core 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/config/chat-responses.json +767 -0
  2. package/config/concept-clusters.json +31 -0
  3. package/config/entities.json +93 -0
  4. package/config/image-prompts.json +20 -0
  5. package/config/intent-vectors.json +1 -0
  6. package/config/intents.json +5023 -65
  7. package/config/ollama-models.json +193 -0
  8. package/config/rules.json +32 -1
  9. package/dist/automation/discordPatchright.d.ts +35 -0
  10. package/dist/automation/discordPatchright.js +424 -0
  11. package/dist/automation/discordSetup.d.ts +31 -0
  12. package/dist/automation/discordSetup.js +338 -0
  13. package/dist/conversation/coreference.js +44 -4
  14. package/dist/conversation/pendingActions.d.ts +55 -0
  15. package/dist/conversation/pendingActions.js +127 -0
  16. package/dist/conversation/store.d.ts +72 -0
  17. package/dist/conversation/store.js +140 -1
  18. package/dist/conversation/topicTracker.d.ts +36 -0
  19. package/dist/conversation/topicTracker.js +141 -0
  20. package/dist/execution/ssh.d.ts +42 -1
  21. package/dist/execution/ssh.js +532 -3
  22. package/dist/handlers/executor.js +3981 -16
  23. package/dist/index.d.ts +25 -3
  24. package/dist/index.js +36 -2
  25. package/dist/nlp/batchParser.d.ts +30 -0
  26. package/dist/nlp/batchParser.js +77 -0
  27. package/dist/nlp/conceptExpansion.d.ts +54 -0
  28. package/dist/nlp/conceptExpansion.js +136 -0
  29. package/dist/nlp/conceptRouter.d.ts +49 -0
  30. package/dist/nlp/conceptRouter.js +302 -0
  31. package/dist/nlp/confidenceCalibrator.d.ts +62 -0
  32. package/dist/nlp/confidenceCalibrator.js +116 -0
  33. package/dist/nlp/correctionLearner.d.ts +45 -0
  34. package/dist/nlp/correctionLearner.js +207 -0
  35. package/dist/nlp/entitySpellCorrect.d.ts +35 -0
  36. package/dist/nlp/entitySpellCorrect.js +141 -0
  37. package/dist/nlp/knowledgeGraph.d.ts +70 -0
  38. package/dist/nlp/knowledgeGraph.js +380 -0
  39. package/dist/nlp/llmFallback.js +28 -1
  40. package/dist/nlp/multiClassifier.js +91 -6
  41. package/dist/nlp/multiIntent.d.ts +43 -0
  42. package/dist/nlp/multiIntent.js +154 -0
  43. package/dist/nlp/parseIntent.d.ts +6 -1
  44. package/dist/nlp/parseIntent.js +180 -5
  45. package/dist/nlp/ruleParser.js +315 -0
  46. package/dist/nlp/semanticSimilarity.d.ts +30 -0
  47. package/dist/nlp/semanticSimilarity.js +174 -0
  48. package/dist/nlp/vocabularyBuilder.d.ts +43 -0
  49. package/dist/nlp/vocabularyBuilder.js +224 -0
  50. package/dist/nlp/wikidata.d.ts +49 -0
  51. package/dist/nlp/wikidata.js +228 -0
  52. package/dist/policy/confirm.d.ts +10 -0
  53. package/dist/policy/confirm.js +39 -0
  54. package/dist/policy/safety.js +6 -4
  55. package/dist/utils/aliases.d.ts +5 -0
  56. package/dist/utils/aliases.js +39 -0
  57. package/dist/utils/analysis.js +71 -15
  58. package/dist/utils/browser.d.ts +64 -0
  59. package/dist/utils/browser.js +364 -0
  60. package/dist/utils/commandHistory.d.ts +20 -0
  61. package/dist/utils/commandHistory.js +108 -0
  62. package/dist/utils/completer.d.ts +17 -0
  63. package/dist/utils/completer.js +79 -0
  64. package/dist/utils/config.js +32 -2
  65. package/dist/utils/dbQuery.d.ts +25 -0
  66. package/dist/utils/dbQuery.js +248 -0
  67. package/dist/utils/discordDiag.d.ts +35 -0
  68. package/dist/utils/discordDiag.js +826 -0
  69. package/dist/utils/diskCleanup.d.ts +36 -0
  70. package/dist/utils/diskCleanup.js +775 -0
  71. package/dist/utils/entityResolver.d.ts +107 -0
  72. package/dist/utils/entityResolver.js +468 -0
  73. package/dist/utils/imageGen.d.ts +92 -0
  74. package/dist/utils/imageGen.js +2031 -0
  75. package/dist/utils/installTracker.d.ts +57 -0
  76. package/dist/utils/installTracker.js +160 -0
  77. package/dist/utils/multiExec.d.ts +21 -0
  78. package/dist/utils/multiExec.js +141 -0
  79. package/dist/utils/openclawDiag.d.ts +29 -0
  80. package/dist/utils/openclawDiag.js +1035 -0
  81. package/dist/utils/output.js +4 -0
  82. package/dist/utils/platform.js +2 -1
  83. package/dist/utils/progressReporter.d.ts +50 -0
  84. package/dist/utils/progressReporter.js +58 -0
  85. package/dist/utils/projectDetect.d.ts +44 -0
  86. package/dist/utils/projectDetect.js +319 -0
  87. package/dist/utils/projectScanner.d.ts +44 -0
  88. package/dist/utils/projectScanner.js +312 -0
  89. package/dist/utils/shellCompat.d.ts +78 -0
  90. package/dist/utils/shellCompat.js +186 -0
  91. package/dist/utils/smartArchive.d.ts +16 -0
  92. package/dist/utils/smartArchive.js +172 -0
  93. package/dist/utils/smartRetry.d.ts +26 -0
  94. package/dist/utils/smartRetry.js +114 -0
  95. package/dist/utils/updater.d.ts +1 -0
  96. package/dist/utils/updater.js +1 -1
  97. package/dist/utils/version.d.ts +20 -0
  98. package/dist/utils/version.js +212 -0
  99. package/package.json +6 -3
@@ -0,0 +1,1035 @@
1
+ /**
2
+ * OpenClaw advanced diagnostics.
3
+ *
4
+ * Runs a multi-step check:
5
+ * 1. Is the gateway process running?
6
+ * 2. Is the HTTP health endpoint responding?
7
+ * 3. Can we reach the WebSocket gateway?
8
+ * 4. What channels are configured (Telegram, Discord, Matrix)?
9
+ * 5. Are channels connected and healthy?
10
+ * 6. What's the config state?
11
+ * 7. Any errors in recent logs?
12
+ */
13
+ import { exec } from "node:child_process";
14
+ import { promisify } from "node:util";
15
+ import { discoverInstallations } from "./entityResolver.js";
16
+ const execAsync = promisify(exec);
17
+ const c = {
18
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
19
+ green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m",
20
+ };
21
+ /** Extract the text reply from openclaw agent JSON output. */
22
+ function extractAgentReply(output) {
23
+ try {
24
+ // Output may have log lines before JSON — find the JSON
25
+ const jsonStart = output.indexOf("{");
26
+ if (jsonStart < 0)
27
+ return null;
28
+ const json = JSON.parse(output.substring(jsonStart));
29
+ // openclaw agent --json returns: { result: { payloads: [{ text: "..." }] } }
30
+ const text = json?.result?.payloads?.[0]?.text
31
+ ?? json?.reply
32
+ ?? json?.text
33
+ ?? json?.content;
34
+ return text || null;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ const isWin = process.platform === "win32";
41
+ const userHome = process.env.HOME ?? process.env.USERPROFILE ?? (isWin ? "C:\\Users\\Default" : "/root");
42
+ async function runCmd(cmd, timeout = 15_000) {
43
+ try {
44
+ const shell = isWin ? "bash" : undefined;
45
+ const { stdout, stderr } = await execAsync(cmd, { timeout, shell });
46
+ return (stdout + stderr).trim();
47
+ }
48
+ catch (err) {
49
+ return err?.stdout?.trim() ?? err.message.split("\n")[0];
50
+ }
51
+ }
52
+ /** Cross-platform: check if a command exists */
53
+ async function cmdExists(run, cmd) {
54
+ if (isWin) {
55
+ const out = await run(`command -v ${cmd} 2>/dev/null || where ${cmd} 2>/dev/null`);
56
+ return out && !out.includes("not found") && !out.includes("Could not find") ? out.trim() : "";
57
+ }
58
+ const out = await run(`which ${cmd} 2>/dev/null`);
59
+ return out && out.includes(cmd) ? out.trim() : "";
60
+ }
61
+ /** Cross-platform: check if openclaw gateway process is running */
62
+ async function isGatewayRunning(run) {
63
+ if (isWin) {
64
+ // Use WMI — Get-Process doesn't populate CommandLine on older Windows (Server 2016)
65
+ const wmiOut = await run(`powershell -Command "Get-WmiObject Win32_Process -Filter \\"Name='node.exe'\\" | Where-Object { \\$_.CommandLine -match 'openclaw.*gateway' } | Select-Object -First 1 ProcessId" 2>/dev/null`);
66
+ if (wmiOut && /\d+/.test(wmiOut)) {
67
+ const pid = wmiOut.match(/(\d+)/)?.[1] ?? "?";
68
+ return { running: true, pid };
69
+ }
70
+ // Fallback: check if health endpoint responds (gateway running but can't find process)
71
+ const healthCheck = await run("curl -sf http://127.0.0.1:18789/health 2>/dev/null");
72
+ if (healthCheck && healthCheck.includes('"ok"')) {
73
+ return { running: true, pid: "?" };
74
+ }
75
+ return { running: false, pid: "" };
76
+ }
77
+ const psOut = await run("ps aux | grep openclaw-gateway | grep -v grep | head -1");
78
+ if (psOut && psOut.includes("openclaw")) {
79
+ const pidMatch = psOut.match(/\S+\s+(\d+)/);
80
+ return { running: true, pid: pidMatch?.[1] ?? "?" };
81
+ }
82
+ return { running: false, pid: "" };
83
+ }
84
+ /** Cross-platform: nvm prefix for running openclaw with Node 22 */
85
+ function getNvmPrefix() {
86
+ if (isWin) {
87
+ // On Windows with bash (Git Bash/MSYS), nvm might be nvm-windows which doesn't need sourcing
88
+ // Just try to use node directly — if Node 22 was installed it should be on PATH
89
+ return "";
90
+ }
91
+ return `for d in "$HOME/.nvm" "/home/"*"/.nvm" "/root/.nvm"; do [ -s "$d/nvm.sh" ] && export NVM_DIR="$d" && . "$d/nvm.sh" && break; done 2>/dev/null; nvm use 22 > /dev/null 2>&1;`;
92
+ }
93
+ /** Cross-platform: wrap an openclaw CLI command with the right Node version */
94
+ function wrapOcCmd(cmd, nvmPrefix) {
95
+ if (isWin) {
96
+ // On Windows, just run directly — Node version managers update PATH globally
97
+ return `${cmd} 2>&1`;
98
+ }
99
+ return `bash -c '${nvmPrefix} ${cmd} 2>&1'`;
100
+ }
101
+ /** Cross-platform: get Claude credentials path */
102
+ function getClaudeCredsPath() {
103
+ return `${userHome}${isWin ? "\\" : "/"}.claude${isWin ? "\\" : "/"}.credentials.json`;
104
+ }
105
+ /** Cross-platform: get openclaw config base path */
106
+ function getOpenclawHome() {
107
+ return `${userHome}${isWin ? "\\" : "/"}.openclaw`;
108
+ }
109
+ /**
110
+ * Quick connectivity check — escalates from simplest to most thorough.
111
+ * Used for "can you talk to openclaw?" / "is openclaw reachable?"
112
+ *
113
+ * Steps:
114
+ * 1. Is the process alive? (ps aux — instant)
115
+ * 2. Does the health endpoint respond? (curl — fast)
116
+ * 3. Can the CLI communicate? (openclaw health — slower)
117
+ *
118
+ * Stops at the first failure and reports what's wrong.
119
+ */
120
+ export async function quickConnectivityCheck(runRemote) {
121
+ const run = runRemote ?? ((cmd) => runCmd(cmd));
122
+ const lines = [];
123
+ lines.push(`\n${c.bold}${c.cyan}── OpenClaw Connectivity Check ──${c.reset}\n`);
124
+ // Auto-discover and register all OpenClaw installations as entities
125
+ try {
126
+ await discoverInstallations("openclaw");
127
+ }
128
+ catch { /* non-critical */ }
129
+ // Detect environment
130
+ let hostGatewayRunning = false;
131
+ if (isWin) {
132
+ lines.push(` ${c.dim}Environment: Windows${c.reset}`);
133
+ }
134
+ else {
135
+ const wslCheck = await run("grep -qi microsoft /proc/version 2>/dev/null && echo wsl || echo native");
136
+ const inWSL = wslCheck.trim() === "wsl";
137
+ if (inWSL) {
138
+ lines.push(` ${c.dim}Environment: WSL${c.reset}`);
139
+ // Check for OpenClaw on Windows host — use PowerShell to get command lines (tasklist doesn't show script args)
140
+ const hostPs = await run("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -Command \"Get-WmiObject Win32_Process -Filter \\\"Name='node.exe'\\\" | Select -Exp CommandLine\" 2>/dev/null");
141
+ const hostHasOpenclaw = hostPs.includes("openclaw");
142
+ const hostNodeCheck = await run("cmd.exe /c 'where openclaw' 2>/dev/null");
143
+ const hostInstalled = hostNodeCheck.includes("openclaw");
144
+ if (hostHasOpenclaw) {
145
+ lines.push(` ${c.green}✓${c.reset} OpenClaw detected on ${c.bold}Windows host${c.reset}`);
146
+ // Check if it's a gateway process
147
+ if (hostPs.includes("gateway")) {
148
+ hostGatewayRunning = true;
149
+ lines.push(` ${c.green}✓${c.reset} Windows gateway is running on port 18789`);
150
+ const hostHealth = await run("curl -sf http://127.0.0.1:18789/health 2>/dev/null");
151
+ if (hostHealth.includes('"ok"')) {
152
+ lines.push(` ${c.green}✓${c.reset} Windows gateway health: OK`);
153
+ }
154
+ else {
155
+ lines.push(` ${c.yellow}⚠${c.reset} Windows gateway running but health check failed`);
156
+ }
157
+ const winConfig = await run("cmd.exe /c 'type \"%USERPROFILE%\\.openclaw\\openclaw.json\"' 2>/dev/null");
158
+ const winModelMatch = winConfig.match(/"primary"\s*:\s*"([^"]+)"/);
159
+ if (winModelMatch) {
160
+ lines.push(` ${c.dim} Windows model: ${winModelMatch[1]}${c.reset}`);
161
+ }
162
+ }
163
+ }
164
+ else if (hostInstalled) {
165
+ lines.push(` ${c.yellow}○${c.reset} OpenClaw installed on Windows host but ${c.bold}not running${c.reset}`);
166
+ }
167
+ }
168
+ }
169
+ // Step 1: Is process running?
170
+ console.error(`${c.dim}Checking if openclaw is running...${c.reset}`);
171
+ const gwStatus = await isGatewayRunning(run);
172
+ // If Windows host gateway is already running, don't try to start another one in WSL
173
+ if (hostGatewayRunning && !gwStatus.running) {
174
+ lines.push(`\n ${c.green}✓${c.reset} Using Windows host gateway (WSL gateway not needed — same port 18789)`);
175
+ const nvmPrefix = getNvmPrefix();
176
+ // Test if WSL CLI can talk to Windows gateway
177
+ console.error(`${c.dim}Testing WSL CLI → Windows gateway...${c.reset}`);
178
+ const cliOut = await run(wrapOcCmd("openclaw health 2>&1 | head -5", nvmPrefix));
179
+ if (cliOut.includes("Agents:") || cliOut.includes("Heartbeat") || cliOut.includes("Session store")) {
180
+ lines.push(` ${c.green}✓${c.reset} WSL CLI can communicate with Windows gateway`);
181
+ }
182
+ else {
183
+ lines.push(` ${c.yellow}⚠${c.reset} WSL CLI cannot reach Windows gateway — may need matching config`);
184
+ lines.push(` ${c.dim} WSL config: ~/.openclaw/openclaw.json${c.reset}`);
185
+ lines.push(` ${c.dim} Windows config: %USERPROFILE%\\.openclaw\\openclaw.json${c.reset}`);
186
+ }
187
+ await auditOpenclawComponents(run, nvmPrefix, lines);
188
+ return lines.join("\n");
189
+ }
190
+ if (!gwStatus.running) {
191
+ lines.push(` ${c.yellow}✗${c.reset} Gateway is not running. ${c.bold}Starting now...${c.reset}`);
192
+ // Check Node version — openclaw needs 22+
193
+ const nodeOk = await ensureNodeVersion(run, lines);
194
+ if (!nodeOk) {
195
+ return lines.join("\n");
196
+ }
197
+ // Start the gateway with the right Node
198
+ console.error(`${c.dim}→ Starting openclaw gateway...${c.reset}`);
199
+ const startCmd = await buildStartCommand(run);
200
+ const startOut = await run(`${startCmd} sleep 8 && curl -sf http://127.0.0.1:18789/health 2>/dev/null || echo STARTING`);
201
+ if (startOut.includes('"ok":true') || startOut.includes('"status"')) {
202
+ lines.push(` ${c.green}✓${c.reset} ${c.bold}Gateway started successfully.${c.reset}`);
203
+ }
204
+ else if (startOut.includes("STARTING")) {
205
+ lines.push(` ${c.yellow}⚠${c.reset} Gateway starting... may take a few more seconds.`);
206
+ // Try one more time after a brief wait
207
+ const retryOut = await run("sleep 3 && curl -sf http://127.0.0.1:18789/health 2>/dev/null || echo FAIL");
208
+ if (retryOut.includes('"ok"')) {
209
+ lines.push(` ${c.green}✓${c.reset} ${c.bold}Gateway is now running.${c.reset}`);
210
+ }
211
+ else {
212
+ lines.push(` ${c.dim}Still starting — check: openclaw health${c.reset}`);
213
+ }
214
+ }
215
+ else {
216
+ lines.push(` ${c.red}✗${c.reset} Failed to start gateway.`);
217
+ lines.push(` ${c.dim}Try manually: openclaw gateway --force${c.reset}`);
218
+ return lines.join("\n");
219
+ }
220
+ }
221
+ lines.push(` ${c.green}✓${c.reset} Gateway process running ${c.dim}(PID ${gwStatus.pid})${c.reset}`);
222
+ // Step 2: Health endpoint
223
+ console.error(`${c.dim}Checking health endpoint...${c.reset}`);
224
+ const healthOut = await run("curl -sf http://127.0.0.1:18789/health 2>/dev/null || echo FAIL");
225
+ if (healthOut === "FAIL" || !healthOut.includes('"ok"')) {
226
+ lines.push(` ${c.red}✗${c.reset} ${c.bold}Health endpoint not responding.${c.reset}`);
227
+ lines.push(` ${c.dim}Process is running but HTTP port 18789 isn't accepting connections.${c.reset}`);
228
+ lines.push(`\n ${c.bold}Try:${c.reset} ${c.cyan}restart openclaw${c.reset}`);
229
+ return lines.join("\n");
230
+ }
231
+ lines.push(` ${c.green}✓${c.reset} Health endpoint OK ${c.dim}(http://127.0.0.1:18789)${c.reset}`);
232
+ // Step 3: CLI communication (use nvm if needed for correct Node)
233
+ console.error(`${c.dim}Testing CLI communication...${c.reset}`);
234
+ const nvmPrefix = getNvmPrefix();
235
+ const cliOut = await run(wrapOcCmd("openclaw health 2>&1 | head -5", nvmPrefix));
236
+ if (cliOut.includes("Agents:") || cliOut.includes("Heartbeat") || cliOut.includes("Session store")) {
237
+ lines.push(` ${c.green}✓${c.reset} CLI can communicate with gateway`);
238
+ const agentMatch = cliOut.match(/Agents:\s*(.+)/);
239
+ const heartbeatMatch = cliOut.match(/Heartbeat interval:\s*(.+)/);
240
+ const sessionMatch = cliOut.match(/- (.+ago)/);
241
+ if (agentMatch)
242
+ lines.push(` ${c.dim} Agent: ${agentMatch[1]}${c.reset}`);
243
+ if (heartbeatMatch)
244
+ lines.push(` ${c.dim} Heartbeat: ${heartbeatMatch[1]}${c.reset}`);
245
+ if (sessionMatch)
246
+ lines.push(` ${c.dim} Last session: ${sessionMatch[1]}${c.reset}`);
247
+ // Step 4: Try to actually send a message to the agent
248
+ console.error(`${c.dim}Sending test message to agent...${c.reset}`);
249
+ const agentOut = await run(wrapOcCmd(`${isWin ? "" : "timeout 30 "}openclaw agent --agent main --message "hi" --json`, nvmPrefix));
250
+ const agentReply = extractAgentReply(agentOut);
251
+ if (agentReply) {
252
+ lines.push(` ${c.green}✓${c.reset} Agent responded!`);
253
+ lines.push(` ${c.bold} OpenClaw says:${c.reset} ${agentReply}`);
254
+ }
255
+ else if (agentOut.includes("No API key") || agentOut.includes("auth") || !agentReply) {
256
+ // Agent didn't respond — likely missing LLM auth. Try to auto-configure.
257
+ // Try to auto-detect API keys from environment
258
+ const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
259
+ const hasOpenAIKey = !!process.env.OPENAI_API_KEY;
260
+ // Try to auto-configure auth — escalating methods:
261
+ // 1. Claude CLI OAuth token sync (frictionless — uses existing login)
262
+ // 2. Environment variables (ANTHROPIC_API_KEY / OPENAI_API_KEY)
263
+ // 3. Suggest manual setup
264
+ let authFixed = false;
265
+ // Method 1: Read Claude Code's OAuth token directly from ~/.claude/.credentials.json
266
+ const claudeCredsPath = getClaudeCredsPath();
267
+ let claudeToken = null;
268
+ try {
269
+ const { readFileSync: readFS, existsSync: existsFS } = await import("node:fs");
270
+ if (existsFS(claudeCredsPath)) {
271
+ const creds = JSON.parse(readFS(claudeCredsPath, "utf-8"));
272
+ claudeToken = creds?.claudeAiOauth?.accessToken ?? null;
273
+ }
274
+ }
275
+ catch { }
276
+ if (claudeToken) {
277
+ lines.push(` ${c.cyan}Found Claude Code OAuth token — configuring openclaw...${c.reset}`);
278
+ // Write directly into openclaw's auth-profiles.json
279
+ const authProfilePath = `${getOpenclawHome()}${isWin ? "\\" : "/"}agents${isWin ? "\\" : "/"}main${isWin ? "\\" : "/"}agent${isWin ? "\\" : "/"}auth-profiles.json`;
280
+ try {
281
+ const { readFileSync: readFS, writeFileSync: writeFS, existsSync: existsFS, mkdirSync: mkdirFS } = await import("node:fs");
282
+ const { dirname: dirnameFS } = await import("node:path");
283
+ let profiles = { version: 1, profiles: {} };
284
+ if (existsFS(authProfilePath)) {
285
+ profiles = JSON.parse(readFS(authProfilePath, "utf-8"));
286
+ }
287
+ else {
288
+ mkdirFS(dirnameFS(authProfilePath), { recursive: true });
289
+ }
290
+ // Add/update the anthropic profile with the Claude Code OAuth token
291
+ profiles.profiles["anthropic:claude-oauth"] = {
292
+ type: "oauth",
293
+ provider: "anthropic",
294
+ access: claudeToken,
295
+ expires: Date.now() + 86400000, // 24h — token will be refreshed by Claude
296
+ };
297
+ writeFS(authProfilePath, JSON.stringify(profiles, null, 2));
298
+ lines.push(` ${c.green}✓${c.reset} Claude OAuth token injected into openclaw auth`);
299
+ // Reload secrets so the gateway picks up the new token
300
+ await run(wrapOcCmd("openclaw secrets reload 2>&1 || true", nvmPrefix));
301
+ authFixed = true;
302
+ }
303
+ catch (e) {
304
+ lines.push(` ${c.yellow}⚠${c.reset} Could not write auth profile: ${e.message?.split("\n")[0]}`);
305
+ }
306
+ }
307
+ // Method 1b: Try openclaw's built-in setup-token sync as fallback
308
+ if (!authFixed) {
309
+ const claudeInstalled = await cmdExists(run, "claude");
310
+ if (claudeInstalled) {
311
+ lines.push(` ${c.cyan}Trying Claude CLI token sync...${c.reset}`);
312
+ const syncOut = await run(wrapOcCmd("openclaw models auth setup-token --provider anthropic --yes", nvmPrefix));
313
+ if (!syncOut.includes("error") && !syncOut.includes("Error")) {
314
+ lines.push(` ${c.green}✓${c.reset} Claude token synced`);
315
+ authFixed = true;
316
+ }
317
+ }
318
+ }
319
+ // Method 2: Environment API keys
320
+ if (!authFixed) {
321
+ const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
322
+ const hasOpenAIKey = !!process.env.OPENAI_API_KEY;
323
+ if (hasAnthropicKey || hasOpenAIKey) {
324
+ const provider = hasAnthropicKey ? "anthropic" : "openai";
325
+ const key = hasAnthropicKey ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
326
+ lines.push(` ${c.cyan}Found ${provider.toUpperCase()} key — configuring...${c.reset}`);
327
+ const pasteOut = await run(wrapOcCmd(`openclaw models auth paste-token --provider ${provider} <<< "${key}"`, nvmPrefix));
328
+ if (!pasteOut.includes("error")) {
329
+ lines.push(` ${c.green}✓${c.reset} API key configured`);
330
+ authFixed = true;
331
+ }
332
+ }
333
+ }
334
+ // Method 3: Check if Codex CLI is available (OpenAI Codex OAuth)
335
+ if (!authFixed) {
336
+ const codexCheck = await cmdExists(run, "codex");
337
+ if (codexCheck) {
338
+ lines.push(` ${c.cyan}Found Codex CLI — syncing OAuth token...${c.reset}`);
339
+ const syncOut = await run(wrapOcCmd("openclaw models auth setup-token --provider openai-codex --yes", nvmPrefix));
340
+ if (!syncOut.includes("error") && !syncOut.includes("Error")) {
341
+ lines.push(` ${c.green}✓${c.reset} Codex OAuth token synced to openclaw`);
342
+ authFixed = true;
343
+ }
344
+ }
345
+ }
346
+ // Retry the message if auth was fixed
347
+ if (authFixed) {
348
+ console.error(`${c.dim}Retrying message to agent...${c.reset}`);
349
+ const retryOut = await run(wrapOcCmd(`${isWin ? "" : "timeout 30 "}openclaw agent --agent main --message "hi" --json`, nvmPrefix));
350
+ const retryReply = extractAgentReply(retryOut);
351
+ if (retryReply) {
352
+ lines.push(` ${c.green}✓${c.reset} Agent responded!`);
353
+ lines.push(` ${c.bold} OpenClaw says:${c.reset} ${retryReply}`);
354
+ }
355
+ else if (retryOut.includes("No API key") || retryOut.includes("auth")) {
356
+ lines.push(` ${c.yellow}⚠${c.reset} Auth configured but agent still needs setup`);
357
+ lines.push(` ${c.dim} Run: openclaw configure --section model${c.reset}`);
358
+ }
359
+ else {
360
+ lines.push(` ${c.yellow}⚠${c.reset} Agent didn't reply — may need gateway restart`);
361
+ lines.push(` ${c.dim} Try: restart openclaw${c.reset}`);
362
+ }
363
+ }
364
+ else {
365
+ lines.push(` ${c.yellow}⚠${c.reset} No API key found — need Claude CLI or ANTHROPIC_API_KEY`);
366
+ lines.push(` ${c.dim} Install Claude: npm install -g @anthropic-ai/claude-code${c.reset}`);
367
+ lines.push(` ${c.dim} Then: claude login${c.reset}`);
368
+ lines.push(` ${c.dim} notoken will sync the token to openclaw automatically.${c.reset}`);
369
+ }
370
+ }
371
+ else if (agentOut.includes("pairing required")) {
372
+ lines.push(` ${c.yellow}⚠${c.reset} Gateway needs pairing`);
373
+ lines.push(` ${c.dim} Run: openclaw setup${c.reset}`);
374
+ }
375
+ else {
376
+ lines.push(` ${c.yellow}⚠${c.reset} Agent didn't respond: ${c.dim}${agentOut.split("\n")[0].substring(0, 60)}${c.reset}`);
377
+ }
378
+ lines.push(`\n ${c.green}${c.bold}✓ OpenClaw gateway is running and reachable.${c.reset}`);
379
+ lines.push(` ${c.dim}Dashboard: http://127.0.0.1:18789/${c.reset}`);
380
+ lines.push(` ${c.dim}TUI: openclaw tui${c.reset}`);
381
+ }
382
+ else if (cliOut.includes("error") || cliOut.includes("failed")) {
383
+ lines.push(` ${c.yellow}⚠${c.reset} CLI returned errors: ${c.dim}${cliOut.split("\n")[0]}${c.reset}`);
384
+ lines.push(`\n ${c.bold}Try:${c.reset} ${c.cyan}diagnose openclaw${c.reset} ${c.dim}for full diagnostics${c.reset}`);
385
+ }
386
+ else {
387
+ lines.push(` ${c.green}✓${c.reset} CLI responded ${c.dim}(${cliOut.split("\n")[0].substring(0, 60)})${c.reset}`);
388
+ lines.push(`\n ${c.green}${c.bold}✓ OpenClaw is running and reachable.${c.reset}`);
389
+ }
390
+ // ── Component audit — what's available for openclaw ──
391
+ await auditOpenclawComponents(run, nvmPrefix, lines);
392
+ return lines.join("\n");
393
+ }
394
+ /**
395
+ * Audit all optional components that openclaw can use.
396
+ * Reports what's detected, what's optional, and what's needed.
397
+ */
398
+ async function auditOpenclawComponents(run, nvmPrefix, lines) {
399
+ lines.push(`\n${c.bold}${c.cyan}── OpenClaw Components ──${c.reset}`);
400
+ lines.push(`${c.dim} Each is optional — need at least one LLM or one channel.${c.reset}\n`);
401
+ let hasLLM = false;
402
+ let hasChannel = false;
403
+ // ── LLM / AI Providers ──
404
+ lines.push(` ${c.bold}LLM Providers:${c.reset}`);
405
+ // Claude CLI
406
+ const claudeVer = await run("claude --version 2>/dev/null | head -1");
407
+ if (claudeVer && !claudeVer.includes("not found")) {
408
+ lines.push(` ${c.green}✓${c.reset} Claude Code: ${c.dim}${claudeVer.trim()}${c.reset}`);
409
+ // Check OAuth token
410
+ const claudeCredsPath = getClaudeCredsPath();
411
+ try {
412
+ const { existsSync: ef, readFileSync: rf } = await import("node:fs");
413
+ if (ef(claudeCredsPath)) {
414
+ const creds = JSON.parse(rf(claudeCredsPath, "utf-8"));
415
+ if (creds?.claudeAiOauth?.accessToken) {
416
+ lines.push(` ${c.green}✓${c.reset} OAuth token present`);
417
+ hasLLM = true;
418
+ }
419
+ }
420
+ }
421
+ catch { }
422
+ if (!hasLLM) {
423
+ lines.push(` ${c.yellow}○${c.reset} Not logged in — run: ${c.cyan}claude login${c.reset}`);
424
+ }
425
+ }
426
+ else {
427
+ lines.push(` ${c.dim}○${c.reset} Claude Code: not installed ${c.dim}(optional — npm install -g @anthropic-ai/claude-code)${c.reset}`);
428
+ }
429
+ // ANTHROPIC_API_KEY
430
+ if (process.env.ANTHROPIC_API_KEY) {
431
+ lines.push(` ${c.green}✓${c.reset} ANTHROPIC_API_KEY: set in environment`);
432
+ hasLLM = true;
433
+ }
434
+ // OPENAI_API_KEY
435
+ if (process.env.OPENAI_API_KEY) {
436
+ lines.push(` ${c.green}✓${c.reset} OPENAI_API_KEY: set in environment`);
437
+ hasLLM = true;
438
+ }
439
+ // Codex CLI (OpenAI Codex)
440
+ const codexCheck = await cmdExists(run, "codex");
441
+ if (codexCheck) {
442
+ const codexVer = await run("codex --version 2>/dev/null | head -1");
443
+ lines.push(` ${c.green}✓${c.reset} Codex CLI: ${codexVer?.trim() ?? "installed"}`);
444
+ // Check if openai-codex auth is in openclaw
445
+ const authCheck = await run(wrapOcCmd("openclaw models status", nvmPrefix));
446
+ if (authCheck.includes("openai-codex")) {
447
+ lines.push(` ${c.green}✓${c.reset} OAuth synced to openclaw`);
448
+ hasLLM = true;
449
+ }
450
+ else {
451
+ lines.push(` ${c.yellow}○${c.reset} Not synced — run: ${c.cyan}openclaw models auth setup-token --provider openai-codex${c.reset}`);
452
+ }
453
+ }
454
+ else {
455
+ lines.push(` ${c.dim}○${c.reset} Codex CLI: not installed ${c.dim}(optional — npm install -g @openai/codex)${c.reset}`);
456
+ }
457
+ // Ollama (local LLM)
458
+ const ollamaVer = await run("ollama --version 2>/dev/null | head -1");
459
+ if (ollamaVer && !ollamaVer.includes("not found")) {
460
+ const ollamaRunning = await run("curl -sf http://localhost:11434/api/tags 2>/dev/null | head -1");
461
+ if (ollamaRunning && ollamaRunning.includes("models")) {
462
+ lines.push(` ${c.green}✓${c.reset} Ollama: running (local LLM — no API key needed)`);
463
+ hasLLM = true;
464
+ }
465
+ else {
466
+ lines.push(` ${c.yellow}○${c.reset} Ollama: installed but not running — ${c.cyan}ollama serve${c.reset}`);
467
+ }
468
+ }
469
+ else {
470
+ lines.push(` ${c.dim}○${c.reset} Ollama: not installed ${c.dim}(optional — local LLM, no tokens needed)${c.reset}`);
471
+ }
472
+ // ── Chat Channels ──
473
+ lines.push(`\n ${c.bold}Chat Channels:${c.reset}`);
474
+ // TUI (terminal — always available if gateway is running)
475
+ const gwUp = await run("curl -sf http://127.0.0.1:18789/health 2>/dev/null || echo FAIL");
476
+ if (gwUp.includes('"ok"')) {
477
+ lines.push(` ${c.green}✓${c.reset} Terminal (TUI): available — ${c.cyan}openclaw tui${c.reset}`);
478
+ lines.push(` ${c.green}✓${c.reset} notoken can talk to it: ${c.cyan}tell openclaw <message>${c.reset}`);
479
+ hasChannel = true;
480
+ }
481
+ else {
482
+ lines.push(` ${c.yellow}○${c.reset} Terminal (TUI): gateway not running`);
483
+ }
484
+ const channelsOut = await run(wrapOcCmd("openclaw channels list", nvmPrefix));
485
+ // Telegram
486
+ if (channelsOut.toLowerCase().includes("telegram")) {
487
+ const configured = channelsOut.toLowerCase().includes("telegram") && channelsOut.includes("configured");
488
+ lines.push(` ${configured ? `${c.green}✓` : `${c.yellow}○`}${c.reset} Telegram: ${configured ? "configured" : "detected but not configured"}`);
489
+ if (configured)
490
+ hasChannel = true;
491
+ }
492
+ else {
493
+ lines.push(` ${c.dim}○${c.reset} Telegram: not configured ${c.dim}(optional — openclaw configure --section channels)${c.reset}`);
494
+ }
495
+ // Discord
496
+ if (channelsOut.toLowerCase().includes("discord")) {
497
+ const configured = channelsOut.includes("configured");
498
+ lines.push(` ${configured ? `${c.green}✓` : `${c.yellow}○`}${c.reset} Discord: ${configured ? "configured" : "detected but not configured"}`);
499
+ if (configured)
500
+ hasChannel = true;
501
+ }
502
+ else {
503
+ lines.push(` ${c.dim}○${c.reset} Discord: not configured ${c.dim}(optional)${c.reset}`);
504
+ }
505
+ // Matrix
506
+ if (channelsOut.toLowerCase().includes("matrix")) {
507
+ const errored = channelsOut.includes("failed") || channelsOut.includes("Blocked");
508
+ const configured = channelsOut.includes("configured");
509
+ if (configured && !errored) {
510
+ lines.push(` ${c.green}✓${c.reset} Matrix: configured and running`);
511
+ hasChannel = true;
512
+ }
513
+ else if (configured) {
514
+ lines.push(` ${c.yellow}⚠${c.reset} Matrix: configured but has errors — run: ${c.cyan}openclaw doctor --fix${c.reset}`);
515
+ }
516
+ }
517
+ else {
518
+ lines.push(` ${c.dim}○${c.reset} Matrix: not configured ${c.dim}(optional — can run locally, no account needed)${c.reset}`);
519
+ }
520
+ // WhatsApp
521
+ if (channelsOut.toLowerCase().includes("whatsapp")) {
522
+ lines.push(` ${c.green}✓${c.reset} WhatsApp: configured`);
523
+ hasChannel = true;
524
+ }
525
+ else {
526
+ lines.push(` ${c.dim}○${c.reset} WhatsApp: not configured ${c.dim}(optional)${c.reset}`);
527
+ }
528
+ // ── Summary ──
529
+ lines.push("");
530
+ if (hasLLM && hasChannel) {
531
+ lines.push(` ${c.green}${c.bold}✓ Fully operational:${c.reset} LLM + chat channel available.`);
532
+ }
533
+ else if (hasLLM) {
534
+ lines.push(` ${c.green}✓${c.reset} LLM available — agent can respond.`);
535
+ lines.push(` ${c.dim} Add a channel (Telegram/Discord/Matrix) to chat from your phone.${c.reset}`);
536
+ }
537
+ else if (hasChannel) {
538
+ lines.push(` ${c.green}✓${c.reset} Chat channel available.`);
539
+ lines.push(` ${c.yellow} Need an LLM: set ANTHROPIC_API_KEY, install Claude, or run Ollama.${c.reset}`);
540
+ }
541
+ else {
542
+ lines.push(` ${c.yellow}${c.bold}⚠ Need at least one of:${c.reset}`);
543
+ lines.push(` ${c.cyan}1.${c.reset} LLM: Claude Code (${c.cyan}claude login${c.reset}), API key, or Ollama (local)`);
544
+ lines.push(` ${c.cyan}2.${c.reset} Channel: Telegram, Discord, or Matrix (${c.cyan}openclaw configure${c.reset})`);
545
+ }
546
+ }
547
+ /**
548
+ * Ensure Node.js 22+ is available. Installs via nvm if needed.
549
+ * Works in WSL, Linux, and Windows (via nvm-windows, fnm, or direct download).
550
+ */
551
+ async function ensureNodeVersion(run, lines) {
552
+ // Check current Node version
553
+ const nodeVer = await run("node --version 2>/dev/null");
554
+ const major = parseInt(nodeVer.replace("v", ""));
555
+ if (major >= 22) {
556
+ return true; // Already good
557
+ }
558
+ lines.push(` ${c.dim}Current Node: ${nodeVer.trim()} (need 22+)${c.reset}`);
559
+ // ── Windows: try nvm-windows, fnm, or direct download ──
560
+ if (isWin) {
561
+ // Check for nvm-windows
562
+ const nvmWinCheck = await run("nvm version 2>/dev/null");
563
+ if (nvmWinCheck && /\d+\.\d+/.test(nvmWinCheck)) {
564
+ const nvmList = await run("nvm list 2>/dev/null");
565
+ if (nvmList.includes("22.")) {
566
+ lines.push(` ${c.green}✓${c.reset} Node 22 available via nvm-windows`);
567
+ await run("nvm use 22 2>&1");
568
+ return true;
569
+ }
570
+ lines.push(` ${c.cyan}Installing Node 22 via nvm-windows...${c.reset}`);
571
+ console.error(`${c.dim}→ nvm install 22${c.reset}`);
572
+ const installOut = await run("nvm install 22 2>&1");
573
+ if (installOut.includes("22.") || installOut.toLowerCase().includes("installed")) {
574
+ await run("nvm use 22 2>&1");
575
+ lines.push(` ${c.green}✓${c.reset} Node 22 installed via nvm-windows`);
576
+ return true;
577
+ }
578
+ }
579
+ // Check for fnm
580
+ const fnmCheck = await run("fnm --version 2>/dev/null");
581
+ if (fnmCheck && fnmCheck.includes("fnm")) {
582
+ lines.push(` ${c.cyan}Installing Node 22 via fnm...${c.reset}`);
583
+ const fnmOut = await run("fnm install 22 && fnm use 22 2>&1");
584
+ if (fnmOut.includes("installed") || fnmOut.includes("v22")) {
585
+ lines.push(` ${c.green}✓${c.reset} Node 22 installed via fnm`);
586
+ return true;
587
+ }
588
+ }
589
+ // Direct download via PowerShell as last resort — requires admin
590
+ const adminCheck = await run(`powershell -Command "& { ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) }" 2>&1`);
591
+ const hasAdmin = adminCheck.trim() === "True";
592
+ if (!hasAdmin) {
593
+ lines.push(` ${c.red}✗${c.reset} Cannot install Node 22 — admin privileges required.`);
594
+ lines.push(` ${c.dim}Run as Administrator, or install manually:${c.reset}`);
595
+ lines.push(` ${c.dim} • nvm-windows: https://github.com/coreybutler/nvm-windows${c.reset}`);
596
+ lines.push(` ${c.dim} • Node.js: https://nodejs.org/${c.reset}`);
597
+ return false;
598
+ }
599
+ lines.push(` ${c.cyan}Downloading Node 22 installer (admin)...${c.reset}`);
600
+ console.error(`${c.dim}→ Downloading Node.js 22 for Windows${c.reset}`);
601
+ const msiUrl = "https://nodejs.org/dist/v22.15.0/node-v22.15.0-x64.msi";
602
+ // Resolve temp dir: use Windows %TEMP% for PowerShell, bash-compatible path for curl
603
+ const winTemp = (await run(`powershell -Command 'Write-Output $env:TEMP' 2>/dev/null`)).trim() || "C:\\Windows\\Temp";
604
+ const msiWinPath = `${winTemp}\\node22.msi`;
605
+ const msiBashPath = `$(cygpath '${winTemp}')/node22.msi`;
606
+ // Try curl first — Invoke-WebRequest doesn't work on Windows Server 2016 (old TLS/IE engine)
607
+ const curlCheck = await run("curl --version 2>/dev/null");
608
+ if (curlCheck && curlCheck.includes("curl")) {
609
+ await run(`curl -fsSL -o "${msiBashPath}" "${msiUrl}" 2>&1`);
610
+ }
611
+ else {
612
+ await run(`powershell -Command "& { Invoke-WebRequest -Uri '${msiUrl}' -OutFile '${msiWinPath}' }" 2>&1`);
613
+ }
614
+ const dlOut = await run(`powershell -Command "Start-Process msiexec.exe -ArgumentList '/i','${msiWinPath}','/qn' -Wait; Remove-Item '${msiWinPath}'" 2>&1`);
615
+ // Verify after install
616
+ const recheck = await run("node --version 2>/dev/null");
617
+ const recheckMajor = parseInt(recheck.replace("v", ""));
618
+ if (recheckMajor >= 22) {
619
+ lines.push(` ${c.green}✓${c.reset} Node ${recheck.trim()} installed`);
620
+ return true;
621
+ }
622
+ lines.push(` ${c.red}✗${c.reset} Node 22 installer ran but could not verify. You may need to restart your terminal.`);
623
+ lines.push(` ${c.dim}Try: node --version (if still old, restart terminal or download from https://nodejs.org/)${c.reset}`);
624
+ return false;
625
+ }
626
+ // ── Linux/WSL: try nvm, fnm, or install nvm ──
627
+ const nvmSourceCmd = `for d in "$HOME/.nvm" "/home/"*"/.nvm" "/root/.nvm"; do [ -s "$d/nvm.sh" ] && export NVM_DIR="$d" && . "$d/nvm.sh" && break; done`;
628
+ const nvmCheck = await run(`bash -c '${nvmSourceCmd} 2>/dev/null && nvm --version' 2>/dev/null`);
629
+ const hasNvm = nvmCheck && !nvmCheck.includes("not found") && /\d+\.\d+/.test(nvmCheck);
630
+ if (hasNvm) {
631
+ const nvmList = await run(`bash -c '${nvmSourceCmd} 2>/dev/null && nvm ls 22 2>/dev/null'`);
632
+ if (nvmList.includes("v22")) {
633
+ lines.push(` ${c.green}✓${c.reset} Node 22 available via nvm`);
634
+ return true;
635
+ }
636
+ lines.push(` ${c.cyan}Installing Node 22 via nvm...${c.reset}`);
637
+ console.error(`${c.dim}→ nvm install 22${c.reset}`);
638
+ const installOut = await run(`bash -c '${nvmSourceCmd} && nvm install 22' 2>&1`);
639
+ if (installOut.includes("v22") || installOut.includes("Now using")) {
640
+ lines.push(` ${c.green}✓${c.reset} Node 22 installed via nvm`);
641
+ return true;
642
+ }
643
+ else {
644
+ lines.push(` ${c.red}✗${c.reset} nvm install failed: ${installOut.split("\n")[0]}`);
645
+ return false;
646
+ }
647
+ }
648
+ // Check for fnm
649
+ const fnmCheck = await run("fnm --version 2>/dev/null");
650
+ if (fnmCheck && fnmCheck.includes("fnm")) {
651
+ lines.push(` ${c.cyan}Installing Node 22 via fnm...${c.reset}`);
652
+ const fnmOut = await run("fnm install 22 && fnm use 22 2>&1");
653
+ if (fnmOut.includes("installed") || fnmOut.includes("v22")) {
654
+ lines.push(` ${c.green}✓${c.reset} Node 22 installed via fnm`);
655
+ return true;
656
+ }
657
+ }
658
+ // No version manager — install nvm first
659
+ lines.push(` ${c.cyan}Installing nvm...${c.reset}`);
660
+ console.error(`${c.dim}→ Installing nvm + Node 22${c.reset}`);
661
+ const nvmInstall = await run("curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh 2>/dev/null | bash 2>&1; " +
662
+ `bash -c '${nvmSourceCmd} && nvm install 22' 2>&1`);
663
+ if (nvmInstall.includes("v22") || nvmInstall.includes("Now using")) {
664
+ lines.push(` ${c.green}✓${c.reset} nvm + Node 22 installed`);
665
+ return true;
666
+ }
667
+ lines.push(` ${c.red}✗${c.reset} Could not install Node 22 automatically.`);
668
+ lines.push(` ${c.dim}Install manually: nvm install 22 (or download from nodejs.org)${c.reset}`);
669
+ return false;
670
+ }
671
+ /**
672
+ * Build the command to start openclaw with the correct Node version.
673
+ * Uses nvm if the system Node is too old. Cross-platform.
674
+ */
675
+ async function buildStartCommand(run) {
676
+ const nodeVer = await run("node --version 2>/dev/null");
677
+ const major = parseInt(nodeVer.replace("v", ""));
678
+ if (major >= 22) {
679
+ if (isWin) {
680
+ return `powershell -Command "Start-Process -WindowStyle Hidden -FilePath node -ArgumentList (Get-Command openclaw).Source,'gateway','--force','--allow-unconfigured'"`;
681
+ }
682
+ return "openclaw gateway --force --allow-unconfigured &";
683
+ }
684
+ if (isWin) {
685
+ // On Windows, nvm-windows/fnm update PATH globally — just try to run
686
+ return `powershell -Command "Start-Process -WindowStyle Hidden -FilePath node -ArgumentList (Get-Command openclaw).Source,'gateway','--force','--allow-unconfigured'"`;
687
+ }
688
+ // Try nvm — find it wherever it lives
689
+ const nvmSource = `for d in "$HOME/.nvm" "/home/"*"/.nvm" "/root/.nvm"; do [ -s "$d/nvm.sh" ] && export NVM_DIR="$d" && . "$d/nvm.sh" && break; done`;
690
+ const nvmNode22 = await run(`bash -c '${nvmSource} 2>/dev/null && nvm which 22 2>/dev/null'`);
691
+ if (nvmNode22 && nvmNode22.includes("/node")) {
692
+ return `bash -c '${nvmSource} && nvm use 22 && openclaw gateway --force --allow-unconfigured' &`;
693
+ }
694
+ // Try fnm
695
+ const fnmCheck = await run("fnm --version 2>/dev/null");
696
+ if (fnmCheck && fnmCheck.includes("fnm")) {
697
+ return `bash -c 'eval "$(fnm env)" && fnm use 22 && openclaw gateway --force --allow-unconfigured' &`;
698
+ }
699
+ // Fallback — just try
700
+ return "nohup openclaw gateway --force > /dev/null 2>&1";
701
+ }
702
+ export async function diagnoseOpenclaw(isRemote, runRemote) {
703
+ const run = runRemote ?? ((cmd) => runCmd(cmd));
704
+ const steps = [];
705
+ const lines = [];
706
+ lines.push(`\n${c.bold}${c.cyan}── OpenClaw Diagnostics ──${c.reset}\n`);
707
+ // ── Environment detection ──
708
+ if (isWin) {
709
+ steps.push({ name: "Environment", status: "pass", detail: "Windows" });
710
+ const localOC = await cmdExists(run, "openclaw");
711
+ if (localOC) {
712
+ const localVer = await run("openclaw --version 2>/dev/null || echo error");
713
+ if (localVer.includes("error")) {
714
+ steps.push({ name: "Install", status: "warn", detail: `Binary exists at ${localOC} but --version failed (may need Node 22+)` });
715
+ }
716
+ else {
717
+ steps.push({ name: "Install", status: "pass", detail: `${localVer.trim()} at ${localOC}` });
718
+ }
719
+ }
720
+ else {
721
+ steps.push({ name: "Install", status: "fail", detail: "OpenClaw not installed — npm install -g openclaw" });
722
+ }
723
+ }
724
+ else {
725
+ const wslDiag = await run("grep -qi microsoft /proc/version 2>/dev/null && echo wsl || echo native");
726
+ const diagInWSL = wslDiag.trim() === "wsl";
727
+ if (diagInWSL) {
728
+ steps.push({ name: "Environment", status: "pass", detail: "WSL (Windows Subsystem for Linux)" });
729
+ // Check OpenClaw on Windows host
730
+ const hostOC = await run("cmd.exe /c 'where openclaw' 2>/dev/null");
731
+ if (hostOC.includes("openclaw")) {
732
+ const hostRunning = await run("cmd.exe /c 'tasklist /FI \"IMAGENAME eq node.exe\" /V /NH' 2>/dev/null");
733
+ if (hostRunning.includes("openclaw")) {
734
+ steps.push({ name: "Windows host", status: "pass", detail: "OpenClaw running on Windows host" });
735
+ }
736
+ else {
737
+ steps.push({ name: "Windows host", status: "warn", detail: "OpenClaw installed on Windows but not running" });
738
+ }
739
+ }
740
+ else {
741
+ steps.push({ name: "Windows host", status: "skip", detail: "OpenClaw not installed on Windows host (checking WSL only)" });
742
+ }
743
+ // Check WSL OpenClaw install
744
+ const wslOC = await cmdExists(run, "openclaw");
745
+ if (wslOC) {
746
+ const wslVer = await run("openclaw --version 2>/dev/null || echo error");
747
+ if (wslVer.includes("error")) {
748
+ steps.push({ name: "WSL install", status: "warn", detail: `Binary exists at ${wslOC} but --version failed (may need Node 22+)` });
749
+ }
750
+ else {
751
+ steps.push({ name: "WSL install", status: "pass", detail: `${wslVer.trim()} at ${wslOC}` });
752
+ }
753
+ }
754
+ else {
755
+ steps.push({ name: "WSL install", status: "fail", detail: "OpenClaw not installed in WSL — npm install -g openclaw" });
756
+ }
757
+ }
758
+ else {
759
+ steps.push({ name: "Environment", status: "pass", detail: "Native Linux" });
760
+ const localOC = await cmdExists(run, "openclaw");
761
+ if (localOC) {
762
+ const localVer = await run("openclaw --version 2>/dev/null || echo error");
763
+ if (localVer.includes("error")) {
764
+ steps.push({ name: "Install", status: "warn", detail: `Binary exists at ${localOC} but --version failed (may need Node 22+)` });
765
+ }
766
+ else {
767
+ steps.push({ name: "Install", status: "pass", detail: `${localVer.trim()} at ${localOC}` });
768
+ }
769
+ }
770
+ else {
771
+ steps.push({ name: "Install", status: "fail", detail: "OpenClaw not installed — npm install -g openclaw" });
772
+ }
773
+ }
774
+ }
775
+ // ── Step 0: Node version ──
776
+ const nodeVer = await run("node --version 2>/dev/null");
777
+ const nodeMajor = parseInt(nodeVer.replace("v", ""));
778
+ if (nodeMajor >= 22) {
779
+ steps.push({ name: "Node.js", status: "pass", detail: nodeVer.trim() });
780
+ }
781
+ else {
782
+ steps.push({ name: "Node.js", status: "warn", detail: `${nodeVer.trim()} (need 22+) — will auto-install if needed` });
783
+ // Try to ensure Node 22 is available
784
+ const fixLines = [];
785
+ const nodeOk = await ensureNodeVersion(run, fixLines);
786
+ if (nodeOk) {
787
+ steps[steps.length - 1] = { name: "Node.js", status: "pass", detail: `${nodeVer.trim()} → Node 22 available via nvm` };
788
+ }
789
+ }
790
+ // ── Step 1: Is the process running? ──
791
+ const diagGw = await isGatewayRunning(run);
792
+ if (diagGw.running) {
793
+ steps.push({ name: "Gateway process", status: "pass", detail: `Running (PID ${diagGw.pid})` });
794
+ }
795
+ else {
796
+ // Try to auto-start
797
+ const startCmd = await buildStartCommand(run);
798
+ await run(`${startCmd}${isWin ? "" : " & sleep 4"}`);
799
+ if (isWin)
800
+ await run("powershell -Command Start-Sleep -Seconds 4");
801
+ const retryGw = await isGatewayRunning(run);
802
+ if (retryGw.running) {
803
+ steps.push({ name: "Gateway process", status: "pass", detail: `Started automatically (PID ${retryGw.pid})` });
804
+ }
805
+ else {
806
+ steps.push({ name: "Gateway process", status: "fail", detail: "Not running — auto-start failed" });
807
+ }
808
+ }
809
+ // ── Step 2: HTTP health endpoint ──
810
+ const healthOut = await run("curl -sf http://127.0.0.1:18789/health 2>/dev/null || echo FAIL");
811
+ if (healthOut.includes('"ok":true') || healthOut.includes('"status":"live"')) {
812
+ steps.push({ name: "Health endpoint", status: "pass", detail: "http://127.0.0.1:18789/health → OK" });
813
+ }
814
+ else if (healthOut === "FAIL") {
815
+ steps.push({ name: "Health endpoint", status: "fail", detail: "Not responding on port 18789" });
816
+ }
817
+ else {
818
+ steps.push({ name: "Health endpoint", status: "warn", detail: healthOut.substring(0, 80) });
819
+ }
820
+ // ── Step 3: Gateway RPC / WebSocket ──
821
+ const gatewayOut = await run("openclaw gateway status 2>&1 | head -5");
822
+ if (gatewayOut.includes("running") || gatewayOut.includes("RPC probe: ok")) {
823
+ steps.push({ name: "Gateway RPC", status: "pass", detail: "WebSocket gateway responding" });
824
+ }
825
+ else if (gatewayOut.includes("not installed") || gatewayOut.includes("not found")) {
826
+ steps.push({ name: "Gateway RPC", status: "fail", detail: "openclaw CLI not found" });
827
+ }
828
+ else {
829
+ steps.push({ name: "Gateway RPC", status: "warn", detail: gatewayOut.split("\n")[0] });
830
+ }
831
+ // ── Step 4: TUI connectivity test ──
832
+ // The TUI connects via WebSocket — if gateway is up, TUI would work
833
+ if (steps.find(s => s.name === "Health endpoint")?.status === "pass") {
834
+ steps.push({ name: "TUI connectivity", status: "pass", detail: "Gateway reachable — TUI can connect (openclaw tui)" });
835
+ }
836
+ else {
837
+ steps.push({ name: "TUI connectivity", status: "fail", detail: "Gateway not reachable — TUI will fail" });
838
+ }
839
+ // ── Step 5: Channels ──
840
+ const channelsOut = await run("openclaw channels list 2>&1");
841
+ const channelLines = channelsOut.split("\n");
842
+ // Parse channel list
843
+ const channels = [];
844
+ // Check for Telegram
845
+ if (channelsOut.toLowerCase().includes("telegram")) {
846
+ const teleLine = channelLines.find(l => l.toLowerCase().includes("telegram"));
847
+ const configured = teleLine?.includes("configured") ?? false;
848
+ channels.push({ name: "Telegram", type: "telegram", status: configured ? "configured" : "not configured" });
849
+ }
850
+ // Check for Discord
851
+ if (channelsOut.toLowerCase().includes("discord")) {
852
+ const discLine = channelLines.find(l => l.toLowerCase().includes("discord"));
853
+ const configured = discLine?.includes("configured") ?? false;
854
+ channels.push({ name: "Discord", type: "discord", status: configured ? "configured" : "not configured" });
855
+ }
856
+ // Check for Matrix
857
+ if (channelsOut.toLowerCase().includes("matrix")) {
858
+ const matLine = channelLines.find(l => l.toLowerCase().includes("matrix"));
859
+ const configured = matLine?.includes("configured") ?? false;
860
+ const errored = matLine?.includes("failed") || matLine?.includes("error") || channelsOut.includes("Blocked hostname");
861
+ channels.push({
862
+ name: "Matrix",
863
+ type: "matrix",
864
+ status: errored ? "configured but errored" : configured ? "configured" : "not configured",
865
+ });
866
+ }
867
+ // Check for WhatsApp
868
+ if (channelsOut.toLowerCase().includes("whatsapp")) {
869
+ const waLine = channelLines.find(l => l.toLowerCase().includes("whatsapp"));
870
+ const configured = waLine?.includes("configured") ?? false;
871
+ channels.push({ name: "WhatsApp", type: "whatsapp", status: configured ? "configured" : "not configured" });
872
+ }
873
+ if (channels.length > 0) {
874
+ for (const ch of channels) {
875
+ const status = ch.status === "configured" ? "pass" :
876
+ ch.status.includes("error") ? "warn" : "skip";
877
+ steps.push({ name: `Channel: ${ch.name}`, status, detail: ch.status });
878
+ }
879
+ }
880
+ else {
881
+ steps.push({ name: "Channels", status: "warn", detail: "No channels configured — run: openclaw configure" });
882
+ }
883
+ // ── Step 5b: LLM auth — check if any provider is configured, auto-setup if not ──
884
+ const modelsOut = await run("openclaw models status 2>&1");
885
+ const hasProviderAuth = modelsOut.includes("Providers w/ OAuth/tokens (0)") === false
886
+ && !modelsOut.includes("Providers w/ OAuth/tokens (0)");
887
+ const missingAuth = modelsOut.includes("Missing auth");
888
+ if (hasProviderAuth && !missingAuth) {
889
+ steps.push({ name: "LLM auth", status: "pass", detail: "Provider auth configured" });
890
+ }
891
+ else {
892
+ // No LLM auth — try to auto-configure from Claude Code credentials
893
+ let authFixed = false;
894
+ const claudeCredsPath = getClaudeCredsPath();
895
+ try {
896
+ const { readFileSync: readFS, existsSync: existsFS, writeFileSync: writeFS, mkdirSync: mkdirFS } = await import("node:fs");
897
+ const { dirname: dirnameFS } = await import("node:path");
898
+ if (existsFS(claudeCredsPath)) {
899
+ const creds = JSON.parse(readFS(claudeCredsPath, "utf-8"));
900
+ const claudeToken = creds?.claudeAiOauth?.accessToken;
901
+ if (claudeToken) {
902
+ // Write directly into openclaw's auth-profiles.json
903
+ const authProfilePath = `${getOpenclawHome()}${isWin ? "\\" : "/"}agents${isWin ? "\\" : "/"}main${isWin ? "\\" : "/"}agent${isWin ? "\\" : "/"}auth-profiles.json`;
904
+ let profiles = { version: 1, profiles: {} };
905
+ if (existsFS(authProfilePath)) {
906
+ profiles = JSON.parse(readFS(authProfilePath, "utf-8"));
907
+ }
908
+ else {
909
+ mkdirFS(dirnameFS(authProfilePath), { recursive: true });
910
+ }
911
+ profiles.profiles["anthropic:claude-oauth"] = {
912
+ type: "oauth",
913
+ provider: "anthropic",
914
+ access: claudeToken,
915
+ expires: Date.now() + 86400000,
916
+ };
917
+ writeFS(authProfilePath, JSON.stringify(profiles, null, 2));
918
+ steps.push({ name: "LLM auth", status: "pass", detail: "Auto-configured from Claude Code OAuth token" });
919
+ authFixed = true;
920
+ }
921
+ }
922
+ }
923
+ catch { }
924
+ if (!authFixed && process.env.ANTHROPIC_API_KEY) {
925
+ steps.push({ name: "LLM auth", status: "pass", detail: "ANTHROPIC_API_KEY found in environment" });
926
+ authFixed = true;
927
+ }
928
+ if (!authFixed && process.env.OPENAI_API_KEY) {
929
+ steps.push({ name: "LLM auth", status: "pass", detail: "OPENAI_API_KEY found in environment" });
930
+ authFixed = true;
931
+ }
932
+ if (!authFixed) {
933
+ steps.push({ name: "LLM auth", status: "fail", detail: "No LLM provider configured — install Claude Code and run: claude login" });
934
+ }
935
+ }
936
+ // ── Step 6: Config ──
937
+ const configFileOut = await run("openclaw config file 2>&1");
938
+ if (configFileOut && !configFileOut.includes("error") && !configFileOut.includes("not found")) {
939
+ steps.push({ name: "Config file", status: "pass", detail: configFileOut.trim() });
940
+ }
941
+ else {
942
+ steps.push({ name: "Config file", status: "fail", detail: "No config — run: openclaw setup" });
943
+ }
944
+ // ── Step 7: Recent errors in logs ──
945
+ const logErrors = await run("openclaw logs 2>&1 | grep -i 'error\\|fail\\|fatal\\|crash' | tail -5");
946
+ if (logErrors && logErrors.trim().length > 0) {
947
+ const errorCount = logErrors.split("\n").length;
948
+ steps.push({ name: "Recent log errors", status: "warn", detail: `${errorCount} error(s) in recent logs` });
949
+ }
950
+ else {
951
+ steps.push({ name: "Recent log errors", status: "pass", detail: "No recent errors" });
952
+ }
953
+ // ── Step 8: Version ──
954
+ const versionOut = await run("openclaw --version 2>&1");
955
+ if (versionOut) {
956
+ steps.push({ name: "Version", status: "pass", detail: versionOut.trim() });
957
+ }
958
+ // ── Render results ──
959
+ for (const step of steps) {
960
+ const icon = step.status === "pass" ? `${c.green}✓${c.reset}` :
961
+ step.status === "warn" ? `${c.yellow}⚠${c.reset}` :
962
+ step.status === "fail" ? `${c.red}✗${c.reset}` :
963
+ `${c.dim}○${c.reset}`;
964
+ lines.push(` ${icon} ${c.bold}${step.name}${c.reset} ${c.dim}${step.detail}${c.reset}`);
965
+ }
966
+ // ── Summary ──
967
+ const passCount = steps.filter(s => s.status === "pass").length;
968
+ const warnCount = steps.filter(s => s.status === "warn").length;
969
+ const failCount = steps.filter(s => s.status === "fail").length;
970
+ lines.push("");
971
+ if (failCount === 0 && warnCount === 0) {
972
+ lines.push(` ${c.green}${c.bold}✓ All checks passed.${c.reset}`);
973
+ }
974
+ else {
975
+ lines.push(` ${c.bold}Results:${c.reset} ${c.green}${passCount} passed${c.reset} | ${c.yellow}${warnCount} warnings${c.reset} | ${c.red}${failCount} failed${c.reset}`);
976
+ }
977
+ // ── Auto-fix ──
978
+ if (failCount > 0 || warnCount > 0) {
979
+ const fixes = [];
980
+ if (steps.find(s => s.name === "Gateway process" && s.status === "fail")) {
981
+ fixes.push({ issue: "Gateway not running", command: "openclaw gateway --force", description: "Start the gateway (kills stale port binds)" });
982
+ }
983
+ if (steps.find(s => s.name === "LLM auth" && s.status === "fail")) {
984
+ fixes.push({ issue: "No LLM provider", command: "npm install -g @anthropic-ai/claude-code && claude login", description: "Install Claude Code and login — notoken will sync the token to openclaw" });
985
+ }
986
+ if (steps.find(s => s.name.includes("Matrix") && s.status === "warn")) {
987
+ fixes.push({ issue: "Matrix plugin errors", command: "openclaw doctor --fix", description: "Install missing plugin dependencies" });
988
+ }
989
+ if (steps.find(s => s.name === "Config file" && s.status === "fail")) {
990
+ fixes.push({ issue: "No configuration", command: "openclaw setup", description: "Run interactive setup" });
991
+ }
992
+ if (steps.find(s => s.name === "Channels" && s.status === "warn" && s.detail.includes("No channels"))) {
993
+ fixes.push({ issue: "No channels configured", command: "openclaw configure --section channels", description: "Set up Telegram/Discord/Matrix" });
994
+ }
995
+ if (fixes.length > 0) {
996
+ lines.push(`\n ${c.bold}Available fixes:${c.reset}`);
997
+ for (let i = 0; i < fixes.length; i++) {
998
+ lines.push(` ${c.yellow}${i + 1}.${c.reset} ${fixes[i].issue}`);
999
+ lines.push(` ${c.cyan}${fixes[i].command}${c.reset} ${c.dim}— ${fixes[i].description}${c.reset}`);
1000
+ }
1001
+ // Store fixes for auto-fix
1002
+ globalThis.__openclawFixes = fixes;
1003
+ lines.push(`\n ${c.dim}Run "fix openclaw" to apply these fixes automatically.${c.reset}`);
1004
+ }
1005
+ }
1006
+ return lines.join("\n");
1007
+ }
1008
+ /**
1009
+ * Apply auto-fixes from the last diagnosis.
1010
+ */
1011
+ export async function autoFixOpenclaw(runRemote) {
1012
+ const run = runRemote ?? ((cmd) => runCmd(cmd));
1013
+ const fixes = globalThis.__openclawFixes;
1014
+ if (!fixes || fixes.length === 0) {
1015
+ return `${c.dim}No fixes pending. Run "diagnose openclaw" first.${c.reset}`;
1016
+ }
1017
+ const lines = [];
1018
+ lines.push(`\n${c.bold}${c.cyan}── OpenClaw Auto-Fix ──${c.reset}\n`);
1019
+ for (const fix of fixes) {
1020
+ lines.push(` ${c.cyan}Fixing:${c.reset} ${fix.issue}`);
1021
+ lines.push(` ${c.dim}→ ${fix.command}${c.reset}`);
1022
+ const result = await run(fix.command + " 2>&1");
1023
+ const firstLine = result.split("\n")[0];
1024
+ if (result.includes("error") || result.includes("FAIL")) {
1025
+ lines.push(` ${c.red}✗ ${firstLine}${c.reset}\n`);
1026
+ }
1027
+ else {
1028
+ lines.push(` ${c.green}✓ Done${c.reset}\n`);
1029
+ }
1030
+ }
1031
+ // Clear fixes
1032
+ globalThis.__openclawFixes = undefined;
1033
+ lines.push(` ${c.dim}Run "diagnose openclaw" again to verify.${c.reset}`);
1034
+ return lines.join("\n");
1035
+ }