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,826 @@
1
+ /**
2
+ * Discord bot diagnostics, setup, and auto-fix.
3
+ *
4
+ * `diagnoseDiscord()` — runs full checklist and auto-fixes everything:
5
+ * 1. Token valid?
6
+ * 2. Bot in guilds? → auto-invite via patchright
7
+ * 3. Intents enabled in OpenClaw config? → auto-fix
8
+ * 4. DM/group policy correct? → auto-fix
9
+ * 5. OpenClaw version check
10
+ * 6. Restart gateway if config changed
11
+ * 7. Poll gateway connection up to 60s
12
+ * 8. If 4014 error → enable intents via patchright → restart → re-poll
13
+ * 9. Check channels
14
+ * 10. Auto-approve pairing codes
15
+ * 11. Send test message + verify response
16
+ *
17
+ * The full chain runs end-to-end without stopping.
18
+ * User only needs to handle captcha/MFA when patchright prompts.
19
+ */
20
+ import { execSync } from "node:child_process";
21
+ const c = {
22
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
23
+ green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m",
24
+ };
25
+ function tryExec(cmd, timeout = 15_000) {
26
+ try {
27
+ return execSync(cmd, { timeout, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
28
+ }
29
+ catch (e) {
30
+ return e.stdout?.trim?.() ?? "";
31
+ }
32
+ }
33
+ function getNode22() {
34
+ const paths = ["/home/ino/.nvm/versions/node/v22.22.2/bin/node"];
35
+ for (const p of paths) {
36
+ if (tryExec(`ls "${p}" 2>/dev/null`))
37
+ return p;
38
+ }
39
+ const found = tryExec('ls /home/ino/.nvm/versions/node/v22*/bin/node 2>/dev/null | tail -1');
40
+ return found || "node";
41
+ }
42
+ function getOcBin() {
43
+ const node22 = getNode22();
44
+ const nvmOc = tryExec(`ls ${node22.replace('/bin/node', '/lib/node_modules/openclaw/openclaw.mjs')} 2>/dev/null`);
45
+ if (nvmOc)
46
+ return nvmOc;
47
+ return tryExec("readlink -f $(which openclaw) 2>/dev/null") || "openclaw";
48
+ }
49
+ function ocCmd(cmd, timeout = 15_000) {
50
+ return tryExec(`${getNode22()} ${getOcBin()} ${cmd}`, timeout);
51
+ }
52
+ async function discordApi(endpoint, token, method = "GET", body) {
53
+ const headers = { "Authorization": `Bot ${token}` };
54
+ if (body)
55
+ headers["Content-Type"] = "application/json";
56
+ try {
57
+ const response = await fetch(`https://discord.com/api/v10${endpoint}`, {
58
+ method, headers, body,
59
+ });
60
+ if (!response.ok)
61
+ return { error: response.status, message: await response.text() };
62
+ return response.json();
63
+ }
64
+ catch (e) {
65
+ return { error: "fetch_failed", message: e.message };
66
+ }
67
+ }
68
+ function getDiscordToken() {
69
+ const config = tryExec("cat /root/.openclaw/openclaw.json 2>/dev/null");
70
+ if (!config)
71
+ return "";
72
+ try {
73
+ const parsed = JSON.parse(config);
74
+ return parsed?.channels?.discord?.token
75
+ ?? parsed?.channels?.discord?.accounts?.default?.token
76
+ ?? "";
77
+ }
78
+ catch {
79
+ return "";
80
+ }
81
+ }
82
+ /** Sleep helper */
83
+ function sleep(ms) {
84
+ return new Promise(resolve => setTimeout(resolve, ms));
85
+ }
86
+ /**
87
+ * Restart the OpenClaw gateway and return true if health check passes.
88
+ */
89
+ function restartGateway() {
90
+ tryExec("pkill -f openclaw-gateway 2>/dev/null; pkill -f 'openclaw.*gateway' 2>/dev/null");
91
+ tryExec("sleep 2");
92
+ const node22 = getNode22();
93
+ const ocBin = getOcBin();
94
+ tryExec(`bash -c 'OLLAMA_API_KEY="ollama-local" nohup ${node22} ${ocBin} gateway --force --allow-unconfigured > /tmp/openclaw-start.log 2>&1 &'`);
95
+ // Quick health poll — 15s
96
+ for (let i = 0; i < 15; i++) {
97
+ tryExec("sleep 1");
98
+ const health = tryExec("curl -sf http://127.0.0.1:18789/health 2>/dev/null");
99
+ if (health.includes('"ok"'))
100
+ return true;
101
+ }
102
+ return false;
103
+ }
104
+ /**
105
+ * Check gateway logs for current Discord connection state.
106
+ * Returns snapshot — does not poll.
107
+ */
108
+ function checkGatewayLogs() {
109
+ const logFile = tryExec("ls /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log 2>/dev/null");
110
+ if (!logFile)
111
+ return { connected: false, error4014: false, rateLimited: false, retryAfter: 0, awaiting: false, silentRateLimit: false, staleSeconds: 0 };
112
+ const recentLogs = tryExec(`tail -50 "${logFile}" 2>/dev/null`);
113
+ // Check if connected (and it's the most recent state, not stale)
114
+ const lastLoggedIn = recentLogs.lastIndexOf("logged in to discord");
115
+ const lastAwaiting = recentLogs.lastIndexOf("awaiting gateway readiness");
116
+ if (lastLoggedIn > -1 && lastLoggedIn > lastAwaiting) {
117
+ return { connected: true, error4014: false, rateLimited: false, retryAfter: 0, awaiting: false, silentRateLimit: false, staleSeconds: 0 };
118
+ }
119
+ const error4014 = recentLogs.includes("4014");
120
+ const explicitRateLimit = recentLogs.includes("rate limited") || recentLogs.includes("status=429");
121
+ // Extract retry_after if present
122
+ let retryAfter = 0;
123
+ const retryMatch = recentLogs.match(/retry_after[":]*\s*([\d.]+)/);
124
+ if (retryMatch)
125
+ retryAfter = Math.ceil(parseFloat(retryMatch[1]));
126
+ const awaiting = recentLogs.includes("awaiting gateway readiness");
127
+ // Detect silent rate limit: "awaiting gateway readiness" with no new Discord log activity.
128
+ // Discord silently drops the WebSocket — no 429, no error, just never sends READY.
129
+ // This happens after too many gateway restarts in a short period.
130
+ let staleSeconds = 0;
131
+ let silentRateLimit = false;
132
+ if (awaiting && !explicitRateLimit) {
133
+ // Check how long since the last log line was written.
134
+ // Logs may have UTC timestamps (2026-04-03T08:36:11) or local with offset (2026-04-03T01:36:11-07:00).
135
+ // Use file modification time as a reliable staleness indicator.
136
+ const mtime = tryExec(`stat -c %Y "${logFile}" 2>/dev/null`);
137
+ if (mtime) {
138
+ staleSeconds = Math.round(Date.now() / 1000 - parseInt(mtime));
139
+ // If awaiting readiness and no log activity for 2+ minutes, likely silent rate limit
140
+ if (staleSeconds > 120)
141
+ silentRateLimit = true;
142
+ }
143
+ }
144
+ return { connected: false, error4014, rateLimited: explicitRateLimit || silentRateLimit, retryAfter, awaiting, silentRateLimit, staleSeconds };
145
+ }
146
+ /**
147
+ * Poll gateway logs for Discord connection, up to `maxSeconds`.
148
+ * Returns { connected, error4014, stuck, rateLimited }.
149
+ */
150
+ function pollGatewayConnection(maxSeconds = 60) {
151
+ for (let elapsed = 0; elapsed < maxSeconds; elapsed += 3) {
152
+ const status = checkGatewayLogs();
153
+ if (status.connected)
154
+ return { connected: true, error4014: false, stuck: false, rateLimited: false };
155
+ if (status.error4014)
156
+ return { connected: false, error4014: true, stuck: false, rateLimited: false };
157
+ if (status.rateLimited && elapsed % 15 === 0 && elapsed > 0) {
158
+ process.stdout.write(` ${c.yellow}Rate limited by Discord — waiting for cooldown... ${elapsed}s${c.reset}\n`);
159
+ }
160
+ if (elapsed > 0 && elapsed % 15 === 0) {
161
+ process.stdout.write(` ${c.dim}Waiting for Discord connection... ${elapsed}s${c.reset}\n`);
162
+ }
163
+ tryExec("sleep 3");
164
+ }
165
+ // Final check
166
+ const finalStatus = checkGatewayLogs();
167
+ return {
168
+ connected: false,
169
+ error4014: finalStatus.error4014,
170
+ stuck: !finalStatus.rateLimited,
171
+ rateLimited: finalStatus.rateLimited,
172
+ };
173
+ }
174
+ /**
175
+ * Monitor a rate-limited gateway, reporting progress until connected or timeout.
176
+ * Restarts the gateway once after the rate limit cooldown, then monitors.
177
+ * Returns lines to append and whether it connected.
178
+ */
179
+ async function monitorRateLimit(maxMinutes = 5) {
180
+ const out = [];
181
+ const startTime = Date.now();
182
+ const maxMs = maxMinutes * 60_000;
183
+ let restarted = false;
184
+ let lastRestart = 0;
185
+ out.push(` ${c.cyan}Monitoring rate limit recovery (up to ${maxMinutes} min)...${c.reset}`);
186
+ while (Date.now() - startTime < maxMs) {
187
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
188
+ const status = checkGatewayLogs();
189
+ if (status.connected) {
190
+ out.push(` ${c.green}✓${c.reset} Discord connected after ${elapsed}s`);
191
+ return { connected: true, lines: out };
192
+ }
193
+ // If rate limit has cleared (no more 429 in logs) and we haven't restarted recently, restart once
194
+ if (!status.rateLimited && status.awaiting && !restarted && (Date.now() - lastRestart > 60_000)) {
195
+ out.push(` ${c.dim}Rate limit appears cleared — restarting gateway...${c.reset}`);
196
+ restartGateway();
197
+ restarted = true;
198
+ lastRestart = Date.now();
199
+ // Give it time to connect
200
+ tryExec("sleep 10");
201
+ continue;
202
+ }
203
+ // After a restart, check if it connected
204
+ if (restarted && (Date.now() - lastRestart > 30_000)) {
205
+ const postRestart = checkGatewayLogs();
206
+ if (postRestart.connected) {
207
+ out.push(` ${c.green}✓${c.reset} Discord connected after restart (${elapsed}s total)`);
208
+ return { connected: true, lines: out };
209
+ }
210
+ if (postRestart.rateLimited) {
211
+ out.push(` ${c.yellow}Still rate limited after restart — continuing to wait...${c.reset}`);
212
+ restarted = false; // Allow another restart later
213
+ }
214
+ }
215
+ if (elapsed % 30 === 0 && elapsed > 0) {
216
+ const remaining = Math.round((maxMs - (Date.now() - startTime)) / 1000);
217
+ if (status.rateLimited) {
218
+ const retryInfo = status.retryAfter > 0 ? ` (retry_after: ${status.retryAfter}s)` : "";
219
+ out.push(` ${c.yellow}⏳${c.reset} Rate limited${retryInfo} — ${remaining}s remaining`);
220
+ }
221
+ else {
222
+ out.push(` ${c.dim}Still waiting... ${remaining}s remaining${c.reset}`);
223
+ }
224
+ }
225
+ tryExec("sleep 5");
226
+ }
227
+ out.push(` ${c.yellow}⚠${c.reset} Timed out after ${maxMinutes} minutes`);
228
+ return { connected: false, lines: out };
229
+ }
230
+ /**
231
+ * Live Discord connection monitor.
232
+ * Prints real-time status updates as the gateway connects to Discord.
233
+ * Auto-restarts if stuck, auto-waits through rate limits.
234
+ *
235
+ * Usage: "monitor discord" or "watch discord"
236
+ */
237
+ export async function monitorDiscord(maxMinutes = 10) {
238
+ const lines = [];
239
+ const startTime = Date.now();
240
+ const maxMs = maxMinutes * 60_000;
241
+ let restartCount = 0;
242
+ const MAX_RESTARTS = 3;
243
+ lines.push(`\n${c.bold}${c.cyan}── Discord Connection Monitor ──${c.reset}`);
244
+ lines.push(` ${c.dim}Monitoring for up to ${maxMinutes} minutes. Ctrl+C to stop.${c.reset}\n`);
245
+ // Check basics first
246
+ const token = getDiscordToken();
247
+ if (!token) {
248
+ lines.push(` ${c.red}✗ No Discord bot token configured.${c.reset}`);
249
+ lines.push(` ${c.dim}Run: "diagnose discord" to set up.${c.reset}`);
250
+ return lines.join("\n");
251
+ }
252
+ // Verify token is valid
253
+ const me = await discordApi("/users/@me", token);
254
+ if (me.error) {
255
+ lines.push(` ${c.red}✗ Bot token invalid (${me.error}).${c.reset}`);
256
+ return lines.join("\n");
257
+ }
258
+ lines.push(` ${c.green}✓${c.reset} Bot: ${c.bold}${me.username}${c.reset} (${me.id})`);
259
+ // Check if gateway is running
260
+ const health = tryExec("curl -sf http://127.0.0.1:18789/health 2>/dev/null");
261
+ if (!health.includes('"ok"')) {
262
+ lines.push(` ${c.yellow}⚠${c.reset} Gateway not running — starting...`);
263
+ console.log(lines.join("\n"));
264
+ lines.length = 0;
265
+ const started = restartGateway();
266
+ if (!started) {
267
+ lines.push(` ${c.red}✗ Failed to start gateway.${c.reset}`);
268
+ return lines.join("\n");
269
+ }
270
+ lines.push(` ${c.green}✓${c.reset} Gateway started`);
271
+ restartCount++;
272
+ }
273
+ else {
274
+ lines.push(` ${c.green}✓${c.reset} Gateway running`);
275
+ }
276
+ // Print initial status
277
+ console.log(lines.join("\n"));
278
+ lines.length = 0;
279
+ // Monitor loop
280
+ let lastState = "";
281
+ while (Date.now() - startTime < maxMs) {
282
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
283
+ const status = checkGatewayLogs();
284
+ // Build state string for dedup
285
+ const stateStr = `${status.connected}|${status.rateLimited}|${status.error4014}|${status.awaiting}`;
286
+ if (status.connected) {
287
+ if (lastState !== "connected") {
288
+ process.stdout.write(`\r\x1b[2K ${c.green}✓${c.reset} ${c.bold}Discord connected!${c.reset} (${elapsed}s)\n`);
289
+ // Verify with REST API
290
+ const guilds = await discordApi("/users/@me/guilds", token);
291
+ if (Array.isArray(guilds)) {
292
+ process.stdout.write(` ${c.green}✓${c.reset} Receiving events from ${guilds.length} server(s)\n`);
293
+ }
294
+ // Test DM capability
295
+ const canDM = await testBotDM(token, me.id);
296
+ if (canDM) {
297
+ process.stdout.write(` ${c.green}✓${c.reset} Bot can send and receive DMs\n`);
298
+ }
299
+ process.stdout.write(`\n ${c.green}${c.bold}Discord is fully operational.${c.reset}\n`);
300
+ return ""; // Already printed everything
301
+ }
302
+ lastState = "connected";
303
+ await sleep(5000);
304
+ continue;
305
+ }
306
+ if (status.silentRateLimit && !lastState.startsWith("silent")) {
307
+ process.stdout.write(`\r\x1b[2K ${c.yellow}⏳${c.reset} ${c.bold}Silent rate limit detected${c.reset} — Discord isn't sending READY event\n`);
308
+ process.stdout.write(` ${c.dim}No log activity for ${status.staleSeconds}s. This happens after too many gateway restarts.${c.reset}\n`);
309
+ process.stdout.write(` ${c.dim}Discord typically recovers in 5-15 minutes. Don't restart — just wait.${c.reset}\n`);
310
+ lastState = "silent_ratelimit";
311
+ }
312
+ else if (status.rateLimited && !status.silentRateLimit && lastState !== "ratelimited") {
313
+ const retryInfo = status.retryAfter > 0 ? ` (retry_after: ${status.retryAfter}s)` : "";
314
+ process.stdout.write(`\r\x1b[2K ${c.yellow}⏳${c.reset} Rate limited by Discord${retryInfo} — waiting for cooldown...\n`);
315
+ lastState = "ratelimited";
316
+ }
317
+ else if (status.error4014 && lastState !== "error4014") {
318
+ process.stdout.write(`\r\x1b[2K ${c.red}✗${c.reset} Error 4014 — privileged intents not enabled\n`);
319
+ process.stdout.write(` ${c.dim}Run: "diagnose discord" to auto-fix intents${c.reset}\n`);
320
+ return "";
321
+ }
322
+ else if (status.awaiting && !status.rateLimited && !status.silentRateLimit) {
323
+ // Stuck at awaiting readiness but NOT rate limited — safe to restart
324
+ if (stateStr !== lastState) {
325
+ process.stdout.write(`\r\x1b[2K ${c.yellow}⏳${c.reset} Awaiting Discord gateway readiness... (${elapsed}s)\n`);
326
+ }
327
+ // If stuck for more than 90s, restart once — give Discord time
328
+ if (elapsed > 90 && restartCount < MAX_RESTARTS) {
329
+ process.stdout.write(` ${c.dim}Stuck for ${elapsed}s — restarting gateway (attempt ${restartCount + 1}/${MAX_RESTARTS})...${c.reset}\n`);
330
+ restartGateway();
331
+ restartCount++;
332
+ lastState = "";
333
+ await sleep(15_000); // Give gateway time to reconnect
334
+ continue;
335
+ }
336
+ }
337
+ else if (status.silentRateLimit) {
338
+ // Silent rate limit — don't restart, just wait
339
+ if (elapsed % 60 === 0 && elapsed > 0) {
340
+ process.stdout.write(`\r\x1b[2K ${c.yellow}⏳${c.reset} Silent rate limit — waiting... (${Math.round(elapsed / 60)}min, logs stale ${status.staleSeconds}s)\n`);
341
+ }
342
+ }
343
+ // Don't overwrite named states (silent_ratelimit, ratelimited, etc.) with stateStr
344
+ if (!lastState.includes("ratelimit") && !lastState.includes("error4014")) {
345
+ lastState = stateStr;
346
+ }
347
+ // Progress tick every 30s — gentle, no hammering
348
+ // Skip if we already printed a state-specific message this cycle
349
+ if (!status.silentRateLimit && elapsed > 0 && elapsed % 30 === 0) {
350
+ const remaining = Math.round((maxMs - (Date.now() - startTime)) / 1000);
351
+ process.stdout.write(`\r\x1b[2K ${c.dim}⏳ Monitoring... ${elapsed}s elapsed, ${remaining}s remaining${c.reset}`);
352
+ }
353
+ // Poll every 10s — only reading local log files, no API calls
354
+ await sleep(10_000);
355
+ }
356
+ process.stdout.write(`\n ${c.yellow}⚠${c.reset} Timed out after ${maxMinutes} minutes.\n`);
357
+ process.stdout.write(` ${c.dim}Run "diagnose discord" for full diagnostics.${c.reset}\n`);
358
+ return "";
359
+ }
360
+ /**
361
+ * Test if the bot can send a DM via REST API.
362
+ * Sends a test message to a guild member and checks if it went through.
363
+ */
364
+ async function testBotDM(token, botId) {
365
+ try {
366
+ // Get guilds and find a human member to DM
367
+ const guilds = await discordApi("/users/@me/guilds", token);
368
+ if (!Array.isArray(guilds) || guilds.length === 0)
369
+ return false;
370
+ const members = await discordApi(`/guilds/${guilds[0].id}/members?limit=10`, token);
371
+ if (!Array.isArray(members))
372
+ return false;
373
+ const human = members.find((m) => !m.user?.bot);
374
+ if (!human?.user?.id)
375
+ return false;
376
+ // Open DM channel
377
+ const dmChannel = await discordApi("/users/@me/channels", token, "POST", JSON.stringify({ recipient_id: human.user.id }));
378
+ if (!dmChannel?.id)
379
+ return false;
380
+ // Send test message
381
+ const testMsg = await discordApi(`/channels/${dmChannel.id}/messages`, token, "POST", JSON.stringify({ content: `[notoken diagnostic] Bot communication test — ${new Date().toLocaleTimeString()}` }));
382
+ if (testMsg?.id) {
383
+ // Clean up test message
384
+ await discordApi(`/channels/${dmChannel.id}/messages/${testMsg.id}`, token, "DELETE").catch(() => { });
385
+ return true;
386
+ }
387
+ return false;
388
+ }
389
+ catch {
390
+ return false;
391
+ }
392
+ }
393
+ /**
394
+ * Full Discord diagnostic and auto-fix chain.
395
+ */
396
+ export async function diagnoseDiscord() {
397
+ const results = [];
398
+ const lines = [];
399
+ let needsRestart = false;
400
+ let token = "";
401
+ lines.push(`\n${c.bold}${c.cyan}══════════════════════════════════════${c.reset}`);
402
+ lines.push(`${c.bold}${c.cyan} Discord Bot Diagnostics & Auto-Fix${c.reset}`);
403
+ lines.push(`${c.bold}${c.cyan}══════════════════════════════════════${c.reset}\n`);
404
+ // ── 1. Token valid? ──
405
+ token = getDiscordToken();
406
+ if (!token) {
407
+ const savedResult = tryExec("cat /mnt/c/temp/discord-bot-result.json 2>/dev/null");
408
+ try {
409
+ token = JSON.parse(savedResult)?.token ?? "";
410
+ }
411
+ catch { }
412
+ }
413
+ if (!token) {
414
+ results.push({ name: "Bot token", status: "fail", detail: "No token found" });
415
+ lines.push(` ${c.red}✗${c.reset} ${c.bold}Bot token:${c.reset} Not configured`);
416
+ lines.push(` ${c.dim}Run: "setup discord" to create a bot and get a token${c.reset}`);
417
+ return lines.join("\n") + `\n\n ${c.red}Cannot continue without a token.${c.reset}`;
418
+ }
419
+ const botInfo = await discordApi("/users/@me", token);
420
+ if (botInfo.error) {
421
+ results.push({ name: "Bot token", status: "fail", detail: `Invalid: ${botInfo.error}` });
422
+ lines.push(` ${c.red}✗${c.reset} ${c.bold}Bot token:${c.reset} Invalid — ${botInfo.error}`);
423
+ lines.push(` ${c.dim}Run: "setup discord" to get a new token${c.reset}`);
424
+ return lines.join("\n") + `\n\n ${c.red}Cannot continue with invalid token.${c.reset}`;
425
+ }
426
+ const botName = botInfo.username ?? "unknown";
427
+ const appId = botInfo.id;
428
+ results.push({ name: "Bot token", status: "pass", detail: `${botName} (${appId})` });
429
+ lines.push(` ${c.green}✓${c.reset} ${c.bold}Bot token:${c.reset} Valid — ${c.bold}${botName}${c.reset} (${appId})`);
430
+ // ── 2. Bot in guilds? ──
431
+ let guilds = await discordApi("/users/@me/guilds", token);
432
+ if (!Array.isArray(guilds) || guilds.length === 0) {
433
+ results.push({ name: "Guilds", status: "fail", detail: "Not in any servers" });
434
+ lines.push(` ${c.red}✗${c.reset} ${c.bold}Guilds:${c.reset} Bot not in any servers`);
435
+ lines.push(` ${c.yellow}→ Auto-inviting via patchright...${c.reset}`);
436
+ // Auto-fix: invite via patchright
437
+ let authorized = false;
438
+ try {
439
+ const { authorizeDiscordBot } = await import("../automation/discordPatchright.js");
440
+ authorized = await authorizeDiscordBot(appId);
441
+ }
442
+ catch (e) {
443
+ lines.push(` ${c.dim}Patchright unavailable: ${e.message?.substring(0, 50)}${c.reset}`);
444
+ // Fallback: open URL in browser
445
+ const inviteUrl = `https://discord.com/oauth2/authorize?client_id=${appId}&permissions=68608&scope=bot`;
446
+ tryExec(`/mnt/c/Windows/System32/cmd.exe /c "start ${inviteUrl}" 2>/dev/null`);
447
+ lines.push(` ${c.dim}Opened invite URL — add bot to server, then re-run this.${c.reset}`);
448
+ }
449
+ if (authorized) {
450
+ // Wait for guild to appear in API
451
+ lines.push(` ${c.dim}Waiting for guild to register...${c.reset}`);
452
+ for (let i = 0; i < 10; i++) {
453
+ await sleep(3000);
454
+ guilds = await discordApi("/users/@me/guilds", token);
455
+ if (Array.isArray(guilds) && guilds.length > 0)
456
+ break;
457
+ }
458
+ if (Array.isArray(guilds) && guilds.length > 0) {
459
+ results.push({ name: "Guilds (fixed)", status: "fixed", detail: `Joined ${guilds[0].name}` });
460
+ lines.push(` ${c.green}✓${c.reset} ${c.bold}Guilds:${c.reset} Joined ${c.bold}${guilds[0].name}${c.reset}`);
461
+ }
462
+ else {
463
+ lines.push(` ${c.yellow}⚠${c.reset} Authorization succeeded but guild not yet visible — may need a moment.`);
464
+ }
465
+ }
466
+ }
467
+ else {
468
+ results.push({ name: "Guilds", status: "pass", detail: `${guilds.length} server(s)` });
469
+ lines.push(` ${c.green}✓${c.reset} ${c.bold}Guilds:${c.reset} ${guilds.length} server(s)`);
470
+ for (const g of guilds) {
471
+ lines.push(` ${c.cyan}•${c.reset} ${g.name} (${g.id})`);
472
+ }
473
+ }
474
+ // ── 3. OpenClaw intents config ──
475
+ const intentsConfig = tryExec(`${getNode22()} ${getOcBin()} config get channels.discord.intents 2>/dev/null`);
476
+ let intentsOk = false;
477
+ try {
478
+ const intents = JSON.parse(intentsConfig);
479
+ intentsOk = intents.presence === true && intents.guildMembers === true;
480
+ }
481
+ catch { }
482
+ if (intentsOk) {
483
+ results.push({ name: "Intents config", status: "pass", detail: "presence=true, guildMembers=true" });
484
+ lines.push(` ${c.green}✓${c.reset} ${c.bold}Intents config:${c.reset} Correctly configured`);
485
+ }
486
+ else {
487
+ ocCmd('config set channels.discord.intents \'{"presence":true,"guildMembers":true}\' --strict-json');
488
+ results.push({ name: "Intents config", status: "fixed", detail: "Enabled presence + guildMembers" });
489
+ lines.push(` ${c.yellow}⚡${c.reset} ${c.bold}Intents config:${c.reset} Fixed — enabled presence + guildMembers`);
490
+ needsRestart = true;
491
+ }
492
+ // ── 4. DM & group policy ──
493
+ const dmPolicy = tryExec(`${getNode22()} ${getOcBin()} config get channels.discord.dmPolicy 2>/dev/null`).replace(/"/g, "");
494
+ const groupPolicy = tryExec(`${getNode22()} ${getOcBin()} config get channels.discord.groupPolicy 2>/dev/null`).replace(/"/g, "");
495
+ if (dmPolicy === "open" && groupPolicy === "open") {
496
+ results.push({ name: "Policies", status: "pass", detail: "dmPolicy=open, groupPolicy=open" });
497
+ lines.push(` ${c.green}✓${c.reset} ${c.bold}Policies:${c.reset} DM=open, Group=open`);
498
+ }
499
+ else {
500
+ if (dmPolicy !== "open") {
501
+ ocCmd('config set channels.discord.allowFrom \'["*"]\' --strict-json');
502
+ ocCmd('config set channels.discord.dmPolicy \'"open"\' --strict-json');
503
+ }
504
+ if (groupPolicy !== "open") {
505
+ ocCmd('config set channels.discord.groupPolicy \'"open"\' --strict-json');
506
+ }
507
+ results.push({ name: "Policies", status: "fixed", detail: "Set to open" });
508
+ lines.push(` ${c.yellow}⚡${c.reset} ${c.bold}Policies:${c.reset} Fixed — set to open`);
509
+ needsRestart = true;
510
+ }
511
+ // ── 5. OpenClaw version ──
512
+ const ocVersion = ocCmd("--version").replace(/^OpenClaw\s*/i, "").split(" ")[0];
513
+ const npm = getNode22().replace('/bin/node', '/bin/npm');
514
+ const latestVersion = tryExec(`${getNode22()} ${npm} view openclaw version 2>/dev/null`);
515
+ if (ocVersion && latestVersion && ocVersion === latestVersion) {
516
+ results.push({ name: "OpenClaw version", status: "pass", detail: `v${ocVersion}` });
517
+ lines.push(` ${c.green}✓${c.reset} ${c.bold}OpenClaw:${c.reset} v${ocVersion} (latest)`);
518
+ }
519
+ else if (ocVersion) {
520
+ results.push({ name: "OpenClaw version", status: "warn", detail: `v${ocVersion} → ${latestVersion}` });
521
+ lines.push(` ${c.yellow}⚠${c.reset} ${c.bold}OpenClaw:${c.reset} v${ocVersion} (latest: ${latestVersion})`);
522
+ }
523
+ else {
524
+ results.push({ name: "OpenClaw version", status: "fail", detail: "Cannot determine" });
525
+ lines.push(` ${c.red}✗${c.reset} ${c.bold}OpenClaw:${c.reset} Cannot determine version`);
526
+ }
527
+ // ── 6. Restart gateway if config changed ──
528
+ if (needsRestart) {
529
+ lines.push(`\n ${c.cyan}Restarting OpenClaw gateway...${c.reset}`);
530
+ const up = restartGateway();
531
+ lines.push(up
532
+ ? ` ${c.green}✓${c.reset} Gateway restarted`
533
+ : ` ${c.yellow}⚠${c.reset} Gateway may still be starting...`);
534
+ }
535
+ // ── 7. Poll for Discord connection (up to 60s) ──
536
+ lines.push(`\n ${c.dim}Checking Discord connection...${c.reset}`);
537
+ let connStatus = pollGatewayConnection(60);
538
+ // ── 8. If 4014 error → enable intents via patchright → restart → re-poll ──
539
+ if (connStatus.error4014) {
540
+ results.push({ name: "Discord gateway", status: "fail", detail: "Error 4014 — intents not enabled on Developer Portal" });
541
+ lines.push(` ${c.red}✗${c.reset} ${c.bold}Discord gateway:${c.reset} Error 4014 — intents not enabled on Developer Portal`);
542
+ lines.push(` ${c.yellow}→ Auto-enabling intents via patchright...${c.reset}`);
543
+ let intentsFixed = false;
544
+ try {
545
+ const { enableDiscordIntents } = await import("../automation/discordPatchright.js");
546
+ intentsFixed = await enableDiscordIntents(appId);
547
+ }
548
+ catch (e) {
549
+ lines.push(` ${c.dim}Patchright unavailable: ${e.message?.substring(0, 50)}${c.reset}`);
550
+ // Fallback: open portal
551
+ tryExec(`/mnt/c/Windows/System32/cmd.exe /c "start https://discord.com/developers/applications/${appId}/bot" 2>/dev/null`);
552
+ lines.push(` ${c.dim}Opened Developer Portal — enable all Privileged Gateway Intents, then re-run.${c.reset}`);
553
+ }
554
+ if (intentsFixed) {
555
+ lines.push(` ${c.green}✓${c.reset} Intents enabled on Developer Portal`);
556
+ lines.push(` ${c.cyan}Restarting gateway after intent fix...${c.reset}`);
557
+ restartGateway();
558
+ // Re-poll
559
+ connStatus = pollGatewayConnection(60);
560
+ }
561
+ }
562
+ if (connStatus.connected) {
563
+ results.push({ name: "Discord gateway", status: "pass", detail: "Connected" });
564
+ lines.push(` ${c.green}✓${c.reset} ${c.bold}Discord gateway:${c.reset} Connected`);
565
+ }
566
+ else if (connStatus.rateLimited) {
567
+ // ── Rate limited — explain clearly and monitor ──
568
+ const logStatus = checkGatewayLogs();
569
+ lines.push(` ${c.yellow}⚠${c.reset} ${c.bold}Discord gateway:${c.reset} ${c.yellow}Rate limited by Discord${c.reset}`);
570
+ lines.push(``);
571
+ lines.push(` ${c.bold}What happened:${c.reset} Too many gateway restarts triggered Discord's rate limit`);
572
+ lines.push(` on the slash command deployment endpoint (/applications/{id}/commands).`);
573
+ lines.push(` OpenClaw's gateway hangs at "awaiting readiness" because it doesn't`);
574
+ lines.push(` retry after a 429 — it just stops.`);
575
+ if (logStatus.retryAfter > 0) {
576
+ lines.push(` ${c.bold}Discord says:${c.reset} retry after ${c.yellow}${logStatus.retryAfter}s${c.reset}`);
577
+ }
578
+ lines.push(``);
579
+ lines.push(` ${c.bold}What we're doing:${c.reset} Monitoring until the rate limit clears, then`);
580
+ lines.push(` restarting the gateway once. Bot REST API (send DMs, check guilds)`);
581
+ lines.push(` still works — only the WebSocket listener is affected.`);
582
+ lines.push(``);
583
+ // Monitor and wait for it to resolve
584
+ const monitor = await monitorRateLimit(5);
585
+ lines.push(...monitor.lines);
586
+ if (monitor.connected) {
587
+ results.push({ name: "Discord gateway", status: "fixed", detail: "Connected after rate limit cleared" });
588
+ connStatus = { connected: true, error4014: false, stuck: false, rateLimited: false };
589
+ }
590
+ else {
591
+ // Still not connected — verify REST API at least works
592
+ lines.push(` ${c.dim}Testing bot communication via REST API...${c.reset}`);
593
+ const canSendDM = await testBotDM(token, appId);
594
+ if (canSendDM) {
595
+ results.push({ name: "Discord gateway", status: "warn", detail: "Rate limited — REST API works, WebSocket still recovering" });
596
+ lines.push(` ${c.green}✓${c.reset} Bot can still send DMs via REST API`);
597
+ lines.push(` ${c.dim}The rate limit hasn't fully cleared yet. The gateway will recover`);
598
+ lines.push(` on its own — run "diagnose discord" again in a few minutes.${c.reset}`);
599
+ }
600
+ else {
601
+ results.push({ name: "Discord gateway", status: "fail", detail: "Rate limited and REST API not working" });
602
+ lines.push(` ${c.red}✗${c.reset} Rate limited and REST API not responding`);
603
+ }
604
+ }
605
+ }
606
+ else if (connStatus.stuck) {
607
+ // ── Stuck but not rate limited — check if gateway is running ──
608
+ const health = tryExec("curl -sf http://127.0.0.1:18789/health 2>/dev/null");
609
+ const gwUp = health.includes('"ok"');
610
+ if (!gwUp) {
611
+ // Gateway not running — start it
612
+ lines.push(` ${c.red}✗${c.reset} ${c.bold}Discord gateway:${c.reset} Not running — starting...`);
613
+ const started = restartGateway();
614
+ if (started) {
615
+ const retry = pollGatewayConnection(60);
616
+ if (retry.connected) {
617
+ results.push({ name: "Discord gateway", status: "fixed", detail: "Started and connected" });
618
+ lines.push(` ${c.green}✓${c.reset} Gateway started and connected to Discord`);
619
+ connStatus = retry;
620
+ }
621
+ else {
622
+ results.push({ name: "Discord gateway", status: "warn", detail: "Started but still connecting" });
623
+ lines.push(` ${c.yellow}⚠${c.reset} Gateway started but still connecting — may need more time`);
624
+ }
625
+ }
626
+ else {
627
+ results.push({ name: "Discord gateway", status: "fail", detail: "Failed to start" });
628
+ lines.push(` ${c.red}✗${c.reset} Failed to start gateway`);
629
+ }
630
+ }
631
+ else {
632
+ // Gateway running but stuck — check for hidden rate limit in logs
633
+ const logStatus = checkGatewayLogs();
634
+ if (logStatus.rateLimited) {
635
+ // Actually rate limited — redirect to monitor
636
+ lines.push(` ${c.yellow}⚠${c.reset} ${c.bold}Discord gateway:${c.reset} ${c.yellow}Rate limited by Discord${c.reset}`);
637
+ lines.push(` ${c.dim}Monitoring until rate limit clears...${c.reset}`);
638
+ const monitor = await monitorRateLimit(5);
639
+ lines.push(...monitor.lines);
640
+ if (monitor.connected) {
641
+ results.push({ name: "Discord gateway", status: "fixed", detail: "Connected after rate limit cleared" });
642
+ connStatus = { connected: true, error4014: false, stuck: false, rateLimited: false };
643
+ }
644
+ else {
645
+ results.push({ name: "Discord gateway", status: "warn", detail: "Rate limited — waiting for recovery" });
646
+ }
647
+ }
648
+ else {
649
+ // Genuinely stuck — restart once
650
+ lines.push(` ${c.yellow}⚠${c.reset} ${c.bold}Discord gateway:${c.reset} Stuck — restarting...`);
651
+ restartGateway();
652
+ const retry = pollGatewayConnection(45);
653
+ if (retry.connected) {
654
+ results.push({ name: "Discord gateway", status: "fixed", detail: "Connected after restart" });
655
+ lines.push(` ${c.green}✓${c.reset} Connected after restart`);
656
+ connStatus = retry;
657
+ }
658
+ else {
659
+ results.push({ name: "Discord gateway", status: "warn", detail: "Still not connected after restart" });
660
+ lines.push(` ${c.yellow}⚠${c.reset} Still not connected — run "diagnose discord" again in a few minutes`);
661
+ }
662
+ }
663
+ }
664
+ }
665
+ else if (!connStatus.error4014) {
666
+ results.push({ name: "Discord gateway", status: "warn", detail: "Status unclear" });
667
+ lines.push(` ${c.yellow}⚠${c.reset} ${c.bold}Discord gateway:${c.reset} Status unclear — check logs`);
668
+ }
669
+ // ── 9. Channels ──
670
+ if (Array.isArray(guilds) && guilds.length > 0) {
671
+ const guildId = guilds[0].id;
672
+ const channels = await discordApi(`/guilds/${guildId}/channels`, token);
673
+ if (Array.isArray(channels)) {
674
+ const textChannels = channels.filter((ch) => ch.type === 0);
675
+ results.push({ name: "Channels", status: "pass", detail: `${textChannels.length} text channel(s)` });
676
+ lines.push(` ${c.green}✓${c.reset} ${c.bold}Channels:${c.reset} ${textChannels.length} text channel(s)`);
677
+ for (const ch of textChannels) {
678
+ lines.push(` ${c.cyan}#${c.reset} ${ch.name} (${ch.id})`);
679
+ }
680
+ }
681
+ }
682
+ // ── 10. Pairing — find pending codes and auto-approve ──
683
+ if (Array.isArray(guilds) && guilds.length > 0) {
684
+ lines.push(`\n ${c.bold}Checking pairing...${c.reset}`);
685
+ const guildId = guilds[0].id;
686
+ const members = await discordApi(`/guilds/${guildId}/members?limit=10`, token);
687
+ let pairingHandled = false;
688
+ if (Array.isArray(members)) {
689
+ const humans = members.filter((m) => !m.user?.bot);
690
+ for (const member of humans) {
691
+ const userId = member.user?.id;
692
+ if (!userId)
693
+ continue;
694
+ // Open DM channel
695
+ const dmChannel = await discordApi("/users/@me/channels", token, "POST", JSON.stringify({ recipient_id: userId }));
696
+ if (!dmChannel?.id)
697
+ continue;
698
+ // Read recent DMs
699
+ const dms = await discordApi(`/channels/${dmChannel.id}/messages?limit=10`, token);
700
+ if (!Array.isArray(dms))
701
+ continue;
702
+ for (const dm of dms) {
703
+ if (dm.author?.id !== botInfo.id)
704
+ continue;
705
+ const match = dm.content?.match(/(?:Pairing code|pairing code|code):\s*```?\s*(\w{6,12})\s*```?/i)
706
+ ?? dm.content?.match(/\b([A-Z0-9]{6,12})\b/);
707
+ if (!match)
708
+ continue;
709
+ const pairingCode = match[1];
710
+ lines.push(` ${c.yellow}⚡${c.reset} Found pairing code ${c.bold}${pairingCode}${c.reset} for ${member.user.username}`);
711
+ // Auto-approve using correct binary
712
+ const approveResult = ocCmd(`pairing approve discord ${pairingCode}`);
713
+ if (approveResult.toLowerCase().includes("approved") || approveResult.toLowerCase().includes("success") || approveResult === "") {
714
+ // Empty result can mean already approved
715
+ const verifyResult = ocCmd("pairing list discord");
716
+ if (verifyResult.toLowerCase().includes(pairingCode.toLowerCase()) || verifyResult.toLowerCase().includes("approved")) {
717
+ lines.push(` ${c.green}✓${c.reset} Auto-approved pairing for ${member.user.username}`);
718
+ results.push({ name: "Pairing", status: "fixed", detail: `Approved ${pairingCode}` });
719
+ pairingHandled = true;
720
+ }
721
+ else {
722
+ lines.push(` ${c.green}✓${c.reset} Pairing approve sent for ${member.user.username}`);
723
+ pairingHandled = true;
724
+ }
725
+ }
726
+ else {
727
+ lines.push(` ${c.yellow}⚠${c.reset} Approve result: ${approveResult.substring(0, 80)}`);
728
+ results.push({ name: "Pairing", status: "warn", detail: approveResult.substring(0, 60) });
729
+ }
730
+ break;
731
+ }
732
+ if (pairingHandled)
733
+ break;
734
+ }
735
+ }
736
+ if (!pairingHandled) {
737
+ // Check if pairing is already done
738
+ const pairingList = ocCmd("pairing list discord");
739
+ if (pairingList.includes("approved") || pairingList.includes("paired")) {
740
+ results.push({ name: "Pairing", status: "pass", detail: "Already paired" });
741
+ lines.push(` ${c.green}✓${c.reset} ${c.bold}Pairing:${c.reset} Already paired`);
742
+ }
743
+ else {
744
+ lines.push(` ${c.dim}No pending pairing codes found. DM the bot to trigger pairing.${c.reset}`);
745
+ }
746
+ }
747
+ }
748
+ // ── 11. Test message — send and verify response ──
749
+ if (connStatus.connected && Array.isArray(guilds) && guilds.length > 0) {
750
+ const guildId = guilds[0].id;
751
+ const channels = await discordApi(`/guilds/${guildId}/channels`, token);
752
+ if (Array.isArray(channels)) {
753
+ const textChannel = channels.find((ch) => ch.type === 0);
754
+ if (textChannel) {
755
+ lines.push(`\n ${c.bold}Testing bot response...${c.reset}`);
756
+ // Send a test message mentioning the bot
757
+ const testMsg = `<@${appId}> notoken diagnostic ping`;
758
+ const sent = await discordApi(`/channels/${textChannel.id}/messages`, token, "POST", JSON.stringify({ content: testMsg }));
759
+ if (sent?.id) {
760
+ lines.push(` ${c.dim}Sent test ping to #${textChannel.name}...${c.reset}`);
761
+ // Wait a few seconds for bot to respond
762
+ await sleep(5000);
763
+ // Check for response
764
+ const recent = await discordApi(`/channels/${textChannel.id}/messages?limit=5&after=${sent.id}`, token);
765
+ if (Array.isArray(recent) && recent.length > 0) {
766
+ results.push({ name: "Bot response", status: "pass", detail: "Bot responded!" });
767
+ lines.push(` ${c.green}✓${c.reset} ${c.bold}Bot response:${c.reset} Bot is responding in #${textChannel.name}!`);
768
+ }
769
+ else {
770
+ results.push({ name: "Bot response", status: "warn", detail: "No response yet — may need pairing" });
771
+ lines.push(` ${c.yellow}⚠${c.reset} ${c.bold}Bot response:${c.reset} No response yet — may need pairing first`);
772
+ lines.push(` ${c.dim}DM the bot directly to trigger pairing, then re-run diagnostics.${c.reset}`);
773
+ }
774
+ // Clean up test message
775
+ await discordApi(`/channels/${textChannel.id}/messages/${sent.id}`, token, "DELETE").catch(() => { });
776
+ }
777
+ else {
778
+ lines.push(` ${c.dim}Could not send test message — bot may lack permissions.${c.reset}`);
779
+ }
780
+ }
781
+ }
782
+ }
783
+ // ── Summary ──
784
+ lines.push(`\n${c.bold}${c.cyan}── Summary ──${c.reset}\n`);
785
+ const passed = results.filter(r => r.status === "pass").length;
786
+ const fixed = results.filter(r => r.status === "fixed").length;
787
+ const failed = results.filter(r => r.status === "fail").length;
788
+ const warned = results.filter(r => r.status === "warn").length;
789
+ if (failed === 0 && warned === 0) {
790
+ lines.push(` ${c.green}${c.bold}✓ All checks passed!${c.reset} ${fixed > 0 ? `(${fixed} auto-fixed)` : ""}`);
791
+ }
792
+ else if (failed === 0) {
793
+ lines.push(` ${c.yellow}⚠ ${warned} warning(s)${c.reset} ${fixed > 0 ? `, ${fixed} auto-fixed` : ""}`);
794
+ }
795
+ else {
796
+ lines.push(` ${c.red}✗ ${failed} issue(s) need attention${c.reset} ${fixed > 0 ? `, ${fixed} auto-fixed` : ""}`);
797
+ }
798
+ lines.push(` ${c.dim}Passed: ${passed} | Fixed: ${fixed} | Warnings: ${warned} | Failed: ${failed}${c.reset}`);
799
+ return lines.join("\n");
800
+ }
801
+ /**
802
+ * Quick Discord status check.
803
+ */
804
+ export async function quickDiscordCheck() {
805
+ const token = getDiscordToken();
806
+ if (!token)
807
+ return `${c.red}✗${c.reset} Discord not configured. Run: "setup discord"`;
808
+ const bot = await discordApi("/users/@me", token);
809
+ if (bot.error)
810
+ return `${c.red}✗${c.reset} Discord token invalid`;
811
+ const guilds = await discordApi("/users/@me/guilds", token);
812
+ const inGuilds = Array.isArray(guilds) ? guilds.length : 0;
813
+ const health = tryExec("curl -sf http://127.0.0.1:18789/health 2>/dev/null");
814
+ const gwUp = health.includes('"ok"');
815
+ const logFile = tryExec("ls /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log 2>/dev/null");
816
+ const logs = logFile ? tryExec(`tail -20 "${logFile}" 2>/dev/null`) : "";
817
+ const connected = logs.includes("logged in to discord");
818
+ return [
819
+ `\n${c.bold}${c.cyan}── Discord Status ──${c.reset}\n`,
820
+ ` ${c.bold}Bot:${c.reset} ${bot.username} (${bot.id})`,
821
+ ` ${c.bold}Servers:${c.reset} ${inGuilds > 0 ? `${c.green}${inGuilds}${c.reset}` : `${c.red}0${c.reset} — needs invite`}`,
822
+ ` ${c.bold}Gateway:${c.reset} ${gwUp ? `${c.green}✓ running${c.reset}` : `${c.red}✗ down${c.reset}`}`,
823
+ ` ${c.bold}Discord:${c.reset} ${connected ? `${c.green}✓ connected${c.reset}` : `${c.yellow}⚠ not connected${c.reset}`}`,
824
+ `\n ${c.dim}Full check: "diagnose discord"${c.reset}`,
825
+ ].join("\n");
826
+ }