notoken-core 1.8.0 → 1.8.1

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.
@@ -10,9 +10,11 @@
10
10
  * 6. What's the config state?
11
11
  * 7. Any errors in recent logs?
12
12
  */
13
- import { exec } from "node:child_process";
13
+ import { exec, execSync } from "node:child_process";
14
14
  import { promisify } from "node:util";
15
+ import { resolve } from "node:path";
15
16
  import { discoverInstallations } from "./entityResolver.js";
17
+ import { getUserContext, findFreshestClaudeToken, detectUserMismatch, getAuthProfilesPath } from "./userContext.js";
16
18
  const execAsync = promisify(exec);
17
19
  const c = {
18
20
  reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
@@ -106,6 +108,382 @@ function getClaudeCredsPath() {
106
108
  function getOpenclawHome() {
107
109
  return `${userHome}${isWin ? "\\" : "/"}.openclaw`;
108
110
  }
111
+ /**
112
+ * Check Claude CLI status and sync OAuth token to OpenClaw if needed.
113
+ * Full diagnostic:
114
+ * 1. Is Claude CLI installed?
115
+ * 2. Does Claude have a valid OAuth token?
116
+ * 3. Is OpenClaw's copy stale?
117
+ * 4. Sync if needed.
118
+ */
119
+ /**
120
+ * Full OpenClaw auth refresh — uses expect for proper TTY flow,
121
+ * falls back to direct file write if expect unavailable.
122
+ *
123
+ * Flow:
124
+ * 1. Read fresh token from Claude CLI credentials
125
+ * 2. Try: expect → openclaw models auth paste-token --provider anthropic
126
+ * 3. Fallback: write directly to auth-profiles.json
127
+ * 4. Update lastGood pointer
128
+ */
129
+ export function refreshOpenclawAuth() {
130
+ try {
131
+ const { existsSync: ef, readFileSync: rf, writeFileSync: wf } = require("node:fs");
132
+ // Get fresh token from Claude
133
+ const claudePath = getClaudeCredsPath();
134
+ if (!ef(claudePath))
135
+ return { success: false, method: "none", message: "Claude CLI not found" };
136
+ const claude = JSON.parse(rf(claudePath, "utf-8"));
137
+ const freshToken = claude?.claudeAiOauth?.accessToken;
138
+ const freshExpires = claude?.claudeAiOauth?.expiresAt;
139
+ if (!freshToken)
140
+ return { success: false, method: "none", message: "No Claude OAuth token" };
141
+ // Find Node 22 and openclaw binary
142
+ let node22 = "node";
143
+ const nvmPaths = ["/home/ino/.nvm/versions/node/v22.22.2/bin/node", "/home/ino/.nvm/versions/node/v22.22.1/bin/node"];
144
+ for (const p of nvmPaths) {
145
+ try {
146
+ if (require("node:fs").existsSync(p)) {
147
+ node22 = p;
148
+ break;
149
+ }
150
+ }
151
+ catch { }
152
+ }
153
+ if (node22 === "node") {
154
+ try {
155
+ const found = execSync("ls /home/ino/.nvm/versions/node/v22*/bin/node 2>/dev/null | tail -1", { encoding: "utf-8", timeout: 3000, stdio: "pipe" }).trim();
156
+ if (found)
157
+ node22 = found;
158
+ }
159
+ catch { }
160
+ }
161
+ let ocBin = "openclaw";
162
+ try {
163
+ ocBin = execSync("readlink -f $(which openclaw) 2>/dev/null || which openclaw", { encoding: "utf-8", timeout: 3000, stdio: "pipe" }).trim();
164
+ }
165
+ catch { }
166
+ // Method 1: Try expect for proper OpenClaw registration
167
+ let expectAvailable = false;
168
+ try {
169
+ execSync("which expect 2>/dev/null", { stdio: "pipe" });
170
+ expectAvailable = true;
171
+ }
172
+ catch { }
173
+ if (expectAvailable) {
174
+ try {
175
+ const expectScript = `
176
+ set timeout 15
177
+ spawn ${node22} ${ocBin} models auth paste-token --provider anthropic
178
+ expect "Paste token"
179
+ send "${freshToken}\\r"
180
+ expect eof
181
+ `;
182
+ const result = execSync(`expect -c '${expectScript.replace(/'/g, "'\\''")}'`, {
183
+ encoding: "utf-8", timeout: 20000, stdio: ["pipe", "pipe", "pipe"]
184
+ });
185
+ if (result.includes("Auth profile")) {
186
+ return { success: true, method: "expect", message: "Token registered via OpenClaw auth (proper flow)" };
187
+ }
188
+ }
189
+ catch { /* fall through to direct write */ }
190
+ }
191
+ // Method 2: Direct write to auth-profiles.json
192
+ const authPath = `${getOpenclawHome()}/agents/main/agent/auth-profiles.json`;
193
+ if (!ef(authPath))
194
+ return { success: false, method: "none", message: "OpenClaw auth file not found" };
195
+ const auth = JSON.parse(rf(authPath, "utf-8"));
196
+ if (!auth.profiles)
197
+ auth.profiles = {};
198
+ auth.profiles["anthropic:claude-oauth"] = {
199
+ type: "oauth",
200
+ provider: "anthropic",
201
+ access: freshToken,
202
+ expires: freshExpires,
203
+ };
204
+ if (!auth.lastGood)
205
+ auth.lastGood = {};
206
+ auth.lastGood.anthropic = "anthropic:claude-oauth";
207
+ wf(authPath, JSON.stringify(auth, null, 2));
208
+ return { success: true, method: "direct", message: "Token written directly to auth profiles" };
209
+ }
210
+ catch (err) {
211
+ return { success: false, method: "error", message: err.message };
212
+ }
213
+ }
214
+ /**
215
+ * Sync Codex (OpenAI) OAuth token to OpenClaw.
216
+ * Reads from ~/.codex/auth.json
217
+ */
218
+ /**
219
+ * Parse `openclaw models` output to understand current configuration.
220
+ */
221
+ export function parseOpenclawModels(output) {
222
+ const result = {
223
+ defaultModel: null,
224
+ configuredModels: [],
225
+ providers: [],
226
+ errors: [],
227
+ };
228
+ // Parse default model
229
+ const defaultMatch = output.match(/Default\s*:\s*(\S+)/);
230
+ if (defaultMatch)
231
+ result.defaultModel = defaultMatch[1];
232
+ // Parse configured models
233
+ const modelsMatch = output.match(/Configured models\s*\(\d+\):\s*(.+)/);
234
+ if (modelsMatch) {
235
+ result.configuredModels = modelsMatch[1].split(",").map(s => s.trim().replace(/"/g, "")).filter(Boolean);
236
+ }
237
+ // Parse providers with auth
238
+ const providerLines = output.split("\n").filter(l => l.match(/^- \w+\s+effective=/));
239
+ for (const line of providerLines) {
240
+ const nameMatch = line.match(/^- (\S+)/);
241
+ const hasOAuth = line.includes("OAuth") || line.includes("oauth=1");
242
+ const hasToken = line.includes("token=1") || line.includes("token:");
243
+ const isMissing = line.includes("missing:missing");
244
+ result.providers.push({
245
+ name: nameMatch?.[1] ?? "unknown",
246
+ status: isMissing ? "missing" : "configured",
247
+ hasAuth: hasOAuth || hasToken,
248
+ });
249
+ }
250
+ // Parse errors
251
+ const errorLines = output.split("\n").filter(l => l.includes("Token refresh failed") || l.includes("error") || l.includes("Missing auth"));
252
+ result.errors = errorLines.map(l => l.trim());
253
+ return result;
254
+ }
255
+ /**
256
+ * Parse `openclaw status --deep` for health and channel details.
257
+ */
258
+ export function parseOpenclawDeepStatus(output) {
259
+ const result = {
260
+ health: [],
261
+ channels: [],
262
+ sessions: [],
263
+ };
264
+ // Parse Health table
265
+ const healthRows = output.match(/│\s*(\w+)\s*│\s*(reachable|OK|error|timeout|unreachable)\s*│\s*(.+?)│/g);
266
+ if (healthRows) {
267
+ for (const row of healthRows) {
268
+ const m = row.match(/│\s*(\w+)\s*│\s*(\w+)\s*│\s*(.+?)│/);
269
+ if (m && m[1] !== "Item")
270
+ result.health.push({ item: m[1], status: m[2], detail: m[3].trim() });
271
+ }
272
+ }
273
+ // Parse Channels table
274
+ const chanRows = output.match(/│\s*(\w+)\s*│\s*(ON|OFF)\s*│\s*(OK|error|warn|off)\s*│\s*(.+?)│/g);
275
+ if (chanRows) {
276
+ for (const row of chanRows) {
277
+ const m = row.match(/│\s*(\w+)\s*│\s*(ON|OFF)\s*│\s*(\w+)\s*│\s*(.+?)│/);
278
+ if (m && m[1] !== "Channel")
279
+ result.channels.push({ channel: m[1], enabled: m[2] === "ON", state: m[3], detail: m[4].trim() });
280
+ }
281
+ }
282
+ // Parse Sessions table
283
+ const sessRows = output.match(/│\s*agent:\S+\s*│\s*\w+\s*│\s*\S+\s+ago\s*│\s*\S+\s*│\s*.+?│/g);
284
+ if (sessRows) {
285
+ for (const row of sessRows) {
286
+ const m = row.match(/│\s*(\S+)\s*│\s*(\w+)\s*│\s*(\S+\s+ago)\s*│\s*(\S+)\s*│\s*(.+?)│/);
287
+ if (m)
288
+ result.sessions.push({ key: m[1], kind: m[2], age: m[3], model: m[4], tokens: m[5].trim() });
289
+ }
290
+ }
291
+ return result;
292
+ }
293
+ /**
294
+ * Parse `openclaw status` output to understand gateway and system state.
295
+ */
296
+ export function parseOpenclawStatus(output) {
297
+ const result = {
298
+ dashboard: null,
299
+ gateway: { status: "unknown", url: null, reachable: false, latency: null },
300
+ agents: { count: 0, lastActive: null },
301
+ sessions: { count: 0, defaultModel: null, contextSize: null },
302
+ update: { available: false, version: null },
303
+ security: { critical: 0, warn: 0, info: 0, issues: [] },
304
+ services: { gateway: "unknown", node: "unknown" },
305
+ };
306
+ // Dashboard URL
307
+ const dashMatch = output.match(/Dashboard\s*│\s*(https?:\/\/\S+)/);
308
+ if (dashMatch)
309
+ result.dashboard = dashMatch[1].trim();
310
+ // Gateway
311
+ const gwMatch = output.match(/Gateway\s*│\s*(.+?)│/s);
312
+ if (gwMatch) {
313
+ const gwText = gwMatch[1];
314
+ result.gateway.status = gwText.includes("reachable") ? "running" : "unknown";
315
+ const urlMatch = gwText.match(/(wss?:\/\/\S+)/);
316
+ if (urlMatch)
317
+ result.gateway.url = urlMatch[1];
318
+ result.gateway.reachable = gwText.includes("reachable");
319
+ const latMatch = gwText.match(/reachable\s+(\d+ms)/);
320
+ if (latMatch)
321
+ result.gateway.latency = latMatch[1];
322
+ }
323
+ // Gateway/Node services
324
+ const gwSvcMatch = output.match(/Gateway service\s*│\s*(.+?)│/);
325
+ if (gwSvcMatch)
326
+ result.services.gateway = gwSvcMatch[1].trim();
327
+ const nodeSvcMatch = output.match(/Node service\s*│\s*(.+?)│/);
328
+ if (nodeSvcMatch)
329
+ result.services.node = nodeSvcMatch[1].trim();
330
+ // Agents
331
+ const agentMatch = output.match(/Agents\s*│\s*(\d+)/);
332
+ if (agentMatch)
333
+ result.agents.count = parseInt(agentMatch[1]);
334
+ const activeMatch = output.match(/active\s+(\S+\s+ago)/);
335
+ if (activeMatch)
336
+ result.agents.lastActive = activeMatch[1];
337
+ // Sessions
338
+ const sessMatch = output.match(/Sessions\s*│\s*(\d+)\s+active.*?default\s+(\S+)\s+\((\S+)\s+ctx\)/);
339
+ if (sessMatch) {
340
+ result.sessions.count = parseInt(sessMatch[1]);
341
+ result.sessions.defaultModel = sessMatch[2];
342
+ result.sessions.contextSize = sessMatch[3];
343
+ }
344
+ // Update
345
+ const updateMatch = output.match(/Update\s*│\s*(.+?)│/);
346
+ if (updateMatch) {
347
+ result.update.available = updateMatch[1].includes("available");
348
+ const verMatch = updateMatch[1].match(/(\d{4}\.\d+\.\d+)/);
349
+ if (verMatch)
350
+ result.update.version = verMatch[1];
351
+ }
352
+ // Security
353
+ const secMatch = output.match(/Summary:\s*(\d+)\s*critical.*?(\d+)\s*warn.*?(\d+)\s*info/);
354
+ if (secMatch) {
355
+ result.security.critical = parseInt(secMatch[1]);
356
+ result.security.warn = parseInt(secMatch[2]);
357
+ result.security.info = parseInt(secMatch[3]);
358
+ }
359
+ const critLines = output.match(/CRITICAL\s+.+/g);
360
+ if (critLines)
361
+ result.security.issues = critLines.map(l => l.trim());
362
+ return result;
363
+ }
364
+ function syncCodexToken() {
365
+ try {
366
+ const ctx = getUserContext();
367
+ const { existsSync: ef, readFileSync: rf, writeFileSync: wf } = require("node:fs");
368
+ const codexPath = resolve(ctx.loginHomeDir, ".codex", "auth.json");
369
+ const authPath = getAuthProfilesPath();
370
+ if (!ef(codexPath)) {
371
+ let codexInstalled = "";
372
+ try {
373
+ codexInstalled = execSync("which codex 2>/dev/null || where codex 2>nul", { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
374
+ }
375
+ catch { }
376
+ if (!codexInstalled)
377
+ return { status: "no_claude", message: "Codex CLI not installed. Install: npm install -g @openai/codex" };
378
+ return { status: "no_claude_token", message: "Codex CLI installed but no auth file. Run: codex" };
379
+ }
380
+ const codex = JSON.parse(rf(codexPath, "utf-8"));
381
+ const freshToken = codex?.tokens?.access_token;
382
+ const refreshToken = codex?.tokens?.refresh_token;
383
+ const freshExpires = (codex?.tokens?.expires_at ?? 0) * 1000; // Codex uses seconds, we use ms
384
+ const accountId = codex?.tokens?.account_id;
385
+ if (!freshToken && !refreshToken)
386
+ return { status: "no_claude_token", message: "Codex has no tokens. Run: codex (to authenticate)" };
387
+ if (!ef(authPath))
388
+ return { status: "no_openclaw_auth", message: "OpenClaw auth file not found" };
389
+ const auth = JSON.parse(rf(authPath, "utf-8"));
390
+ const ocProfile = auth?.profiles?.["openai-codex:default"];
391
+ if (!ocProfile) {
392
+ // Create profile
393
+ if (!auth.profiles)
394
+ auth.profiles = {};
395
+ auth.profiles["openai-codex:default"] = { type: "oauth", provider: "openai-codex", access: freshToken, refresh: refreshToken, expires: freshExpires, accountId };
396
+ if (!auth.lastGood)
397
+ auth.lastGood = {};
398
+ auth.lastGood["openai-codex"] = "openai-codex:default";
399
+ wf(authPath, JSON.stringify(auth, null, 2));
400
+ return { status: "synced", message: "Created OpenClaw Codex auth profile from Codex CLI" };
401
+ }
402
+ // Check if stale
403
+ const ocExpires = ocProfile.expires ?? 0;
404
+ const now = Date.now();
405
+ if (ocExpires - now > 3600000) {
406
+ return { status: "already_fresh", message: `Codex token fresh (${((ocExpires - now) / 3600000).toFixed(1)}h remaining)` };
407
+ }
408
+ // Sync — update access token and refresh token
409
+ if (freshToken)
410
+ ocProfile.access = freshToken;
411
+ if (refreshToken)
412
+ ocProfile.refresh = refreshToken;
413
+ ocProfile.expires = freshExpires;
414
+ if (accountId)
415
+ ocProfile.accountId = accountId;
416
+ wf(authPath, JSON.stringify(auth, null, 2));
417
+ return { status: "synced", message: "Codex token synced from ~/.codex/auth.json" };
418
+ }
419
+ catch (err) {
420
+ return { status: "error", message: `Codex sync error: ${err.message}` };
421
+ }
422
+ }
423
+ function syncClaudeToken() {
424
+ try {
425
+ const ctx = getUserContext();
426
+ const { existsSync: ef, readFileSync: rf, writeFileSync: wf } = require("node:fs");
427
+ // 1. Find freshest Claude token across all users (root, ino, etc.)
428
+ const freshest = findFreshestClaudeToken();
429
+ if (!freshest) {
430
+ let claudeInstalled = "";
431
+ try {
432
+ claudeInstalled = execSync("which claude 2>/dev/null || where claude 2>nul", { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
433
+ }
434
+ catch { }
435
+ if (!claudeInstalled)
436
+ return { status: "no_claude", message: "Claude CLI not installed" };
437
+ return { status: "no_claude_token", message: `Claude CLI installed but no valid token found (checked root + ${ctx.loginUser})` };
438
+ }
439
+ const freshToken = freshest.token;
440
+ const freshExpires = freshest.expires;
441
+ // 2. Validate token
442
+ const now = Date.now();
443
+ const claudeHoursLeft = (freshExpires - now) / 3600000;
444
+ if (claudeHoursLeft <= 0) {
445
+ return { status: "no_claude_token", message: `Claude token expired ${Math.abs(claudeHoursLeft).toFixed(1)}h ago (source: ${freshest.source}). Run: claude`, claudeExpires: freshExpires };
446
+ }
447
+ // 3. Check OpenClaw's auth file — use user-aware path
448
+ const authPath = getAuthProfilesPath();
449
+ if (!ef(authPath)) {
450
+ return { status: "no_openclaw_auth", message: "OpenClaw auth not configured. Run: diagnose openclaw", claudeExpires: freshExpires };
451
+ }
452
+ const auth = JSON.parse(rf(authPath, "utf-8"));
453
+ // Check both profile names — openclaw uses "anthropic:claude-oauth" or "anthropic:manual"
454
+ const ocProfile = auth?.profiles?.["anthropic:claude-oauth"] ?? auth?.profiles?.["anthropic:manual"];
455
+ if (!ocProfile) {
456
+ // No anthropic profile at all — create one
457
+ if (!auth.profiles)
458
+ auth.profiles = {};
459
+ auth.profiles["anthropic:claude-oauth"] = { type: "oauth", provider: "anthropic", access: freshToken, expires: freshExpires };
460
+ if (!auth.lastGood)
461
+ auth.lastGood = {};
462
+ auth.lastGood.anthropic = "anthropic:claude-oauth";
463
+ wf(authPath, JSON.stringify(auth, null, 2));
464
+ return { status: "synced", message: `Created OpenClaw auth profile with Claude token (${claudeHoursLeft.toFixed(1)}h remaining)`, claudeExpires: freshExpires };
465
+ }
466
+ // 4. Check if OpenClaw's copy is stale
467
+ const ocExpires = ocProfile.expires ?? 0;
468
+ const ocHoursLeft = (ocExpires - now) / 3600000;
469
+ if (ocHoursLeft > 1) {
470
+ return { status: "already_fresh", message: `OpenClaw token is fresh (${ocHoursLeft.toFixed(1)}h remaining)`, claudeExpires: freshExpires, openclawExpires: ocExpires };
471
+ }
472
+ // 5. Sync fresh token
473
+ ocProfile.access = freshToken;
474
+ ocProfile.expires = freshExpires;
475
+ wf(authPath, JSON.stringify(auth, null, 2));
476
+ return {
477
+ status: "synced",
478
+ message: `Token refreshed — was ${ocHoursLeft > 0 ? `expiring in ${ocHoursLeft.toFixed(1)}h` : `expired ${Math.abs(ocHoursLeft).toFixed(1)}h ago`}, now good for ${claudeHoursLeft.toFixed(1)}h`,
479
+ claudeExpires: freshExpires,
480
+ openclawExpires: freshExpires,
481
+ };
482
+ }
483
+ catch (err) {
484
+ return { status: "error", message: `Token sync error: ${err.message}` };
485
+ }
486
+ }
109
487
  /**
110
488
  * Quick connectivity check — escalates from simplest to most thorough.
111
489
  * Used for "can you talk to openclaw?" / "is openclaw reachable?"
@@ -121,6 +499,101 @@ export async function quickConnectivityCheck(runRemote) {
121
499
  const run = runRemote ?? ((cmd) => runCmd(cmd));
122
500
  const lines = [];
123
501
  lines.push(`\n${c.bold}${c.cyan}── OpenClaw Connectivity Check ──${c.reset}\n`);
502
+ // Show user context
503
+ const ctx = getUserContext();
504
+ const mismatch = detectUserMismatch();
505
+ lines.push(` ${c.dim}User: ${ctx.effectiveUser}${ctx.effectiveUser !== ctx.loginUser ? ` (login: ${ctx.loginUser})` : ""} | Config: ${ctx.openclawHome}${c.reset}`);
506
+ if (mismatch.mismatch)
507
+ lines.push(` ${c.yellow}⚠${c.reset} ${mismatch.message}`);
508
+ // Parse openclaw models to understand current configuration
509
+ try {
510
+ const _tryExec = (cmd) => { try {
511
+ return execSync(cmd, { encoding: "utf-8", timeout: 5000, stdio: "pipe" }).trim();
512
+ }
513
+ catch {
514
+ return "";
515
+ } };
516
+ const _node22 = _tryExec("ls /home/ino/.nvm/versions/node/v22*/bin/node 2>/dev/null | tail -1") || "node";
517
+ const _ocBin = _tryExec(`ls ${_node22.replace('/bin/node', '/lib/node_modules/openclaw/openclaw.mjs')} 2>/dev/null`) || _tryExec("readlink -f $(which openclaw) 2>/dev/null") || "openclaw";
518
+ const [modelsOutput, statusOutput] = await Promise.all([
519
+ run(`${_node22} ${_ocBin} models 2>&1`),
520
+ run(`${_node22} ${_ocBin} status 2>&1`).catch(() => ""),
521
+ ]);
522
+ const ocStatus = parseOpenclawModels(modelsOutput);
523
+ if (ocStatus.defaultModel)
524
+ lines.push(` ${c.bold}Model:${c.reset} ${ocStatus.defaultModel}`);
525
+ if (ocStatus.providers.length > 0) {
526
+ for (const p of ocStatus.providers) {
527
+ const icon = p.hasAuth ? `${c.green}✓` : `${c.yellow}○`;
528
+ lines.push(` ${icon}${c.reset} ${p.name}: ${p.status}${p.hasAuth ? "" : " (no auth)"}`);
529
+ }
530
+ }
531
+ if (ocStatus.errors.length > 0) {
532
+ for (const err of ocStatus.errors.slice(0, 3)) {
533
+ lines.push(` ${c.red}✗${c.reset} ${err}`);
534
+ }
535
+ }
536
+ // Parse status output for gateway/session/security info
537
+ if (statusOutput) {
538
+ const st = parseOpenclawStatus(statusOutput);
539
+ if (st.dashboard)
540
+ lines.push(` ${c.bold}Dashboard:${c.reset} ${st.dashboard}`);
541
+ if (st.gateway.reachable) {
542
+ lines.push(` ${c.green}✓${c.reset} Gateway: ${st.gateway.url ?? "running"} ${st.gateway.latency ? `(${st.gateway.latency})` : ""}`);
543
+ }
544
+ if (st.sessions.defaultModel) {
545
+ lines.push(` ${c.bold}Session:${c.reset} ${st.sessions.defaultModel} (${st.sessions.contextSize ?? "?"} ctx)`);
546
+ }
547
+ if (st.agents.lastActive) {
548
+ lines.push(` ${c.dim}Last active: ${st.agents.lastActive}${c.reset}`);
549
+ }
550
+ if (st.update.available) {
551
+ lines.push(` ${c.yellow}⬆${c.reset} Update available: ${st.update.version}`);
552
+ }
553
+ if (st.security.critical > 0) {
554
+ lines.push(` ${c.red}⚠${c.reset} Security: ${st.security.critical} critical, ${st.security.warn} warnings`);
555
+ for (const issue of st.security.issues.slice(0, 2)) {
556
+ lines.push(` ${c.dim}${issue.substring(0, 80)}${c.reset}`);
557
+ }
558
+ }
559
+ }
560
+ lines.push("");
561
+ }
562
+ catch { /* openclaw CLI not available */ }
563
+ // Auto-sync Claude OAuth token — prevents "unauthorized" errors
564
+ const tokenSync = syncClaudeToken();
565
+ if (tokenSync.status === "synced") {
566
+ lines.push(` ${c.green}✓${c.reset} ${tokenSync.message}`);
567
+ }
568
+ else if (tokenSync.status === "already_fresh") {
569
+ // Silent — all good
570
+ }
571
+ else if (tokenSync.status === "no_claude") {
572
+ lines.push(` ${c.yellow}⚠${c.reset} ${tokenSync.message}`);
573
+ }
574
+ else if (tokenSync.status === "no_claude_token") {
575
+ // Claude exists but token expired — try to refresh by running claude briefly
576
+ let refreshed = "";
577
+ try {
578
+ refreshed = execSync("claude --version 2>/dev/null", { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
579
+ }
580
+ catch { }
581
+ if (refreshed) {
582
+ const retry = syncClaudeToken();
583
+ if (retry.status === "synced")
584
+ lines.push(` ${c.green}✓${c.reset} ${retry.message}`);
585
+ }
586
+ }
587
+ // Auto-sync Codex (OpenAI) token too
588
+ const codexSync = syncCodexToken();
589
+ if (codexSync.status === "synced") {
590
+ lines.push(` ${c.green}✓${c.reset} ${codexSync.message}`);
591
+ }
592
+ else if (codexSync.status !== "already_fresh" && codexSync.status !== "no_claude") {
593
+ // Only warn if codex is installed but has issues
594
+ if (codexSync.status === "no_claude_token")
595
+ lines.push(` ${c.yellow}⚠${c.reset} ${codexSync.message}`);
596
+ }
124
597
  // Auto-discover and register all OpenClaw installations as entities
125
598
  try {
126
599
  await discoverInstallations("openclaw");
@@ -704,6 +1177,33 @@ export async function diagnoseOpenclaw(isRemote, runRemote) {
704
1177
  const steps = [];
705
1178
  const lines = [];
706
1179
  lines.push(`\n${c.bold}${c.cyan}── OpenClaw Diagnostics ──${c.reset}\n`);
1180
+ // Auto-sync Claude OAuth token
1181
+ const diagTokenSync = syncClaudeToken();
1182
+ if (diagTokenSync.status === "synced") {
1183
+ steps.push({ name: "OAuth token", status: "pass", detail: diagTokenSync.message });
1184
+ lines.push(` ${c.green}✓${c.reset} ${diagTokenSync.message}`);
1185
+ }
1186
+ else if (diagTokenSync.status === "already_fresh") {
1187
+ steps.push({ name: "OAuth token", status: "pass", detail: diagTokenSync.message });
1188
+ }
1189
+ else if (diagTokenSync.status === "no_claude") {
1190
+ steps.push({ name: "OAuth token", status: "warn", detail: diagTokenSync.message });
1191
+ }
1192
+ else if (diagTokenSync.status === "no_claude_token") {
1193
+ // Try to auto-refresh by running claude briefly
1194
+ try {
1195
+ execSync("claude --version 2>/dev/null", { timeout: 5000, stdio: "pipe" });
1196
+ }
1197
+ catch { }
1198
+ const retry = syncClaudeToken();
1199
+ if (retry.status === "synced") {
1200
+ steps.push({ name: "OAuth token", status: "pass", detail: retry.message });
1201
+ lines.push(` ${c.green}✓${c.reset} ${retry.message}`);
1202
+ }
1203
+ else {
1204
+ steps.push({ name: "OAuth token", status: "warn", detail: diagTokenSync.message });
1205
+ }
1206
+ }
707
1207
  // ── Environment detection ──
708
1208
  if (isWin) {
709
1209
  steps.push({ name: "Environment", status: "pass", detail: "Windows" });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * OpenClaw Log Parser — understand what's happening from openclaw logs.
3
+ *
4
+ * Parses JSON log lines from `openclaw logs --json` and extracts
5
+ * meaningful events for diagnostics, monitoring, and troubleshooting.
6
+ */
7
+ export interface LogEntry {
8
+ type: "log" | "meta";
9
+ time: string;
10
+ level: "info" | "warn" | "error" | "debug";
11
+ subsystem?: string;
12
+ message: string;
13
+ }
14
+ export interface LogAnalysis {
15
+ /** Total log entries analyzed */
16
+ totalEntries: number;
17
+ /** Time range of logs */
18
+ timeRange: {
19
+ start: string;
20
+ end: string;
21
+ } | null;
22
+ /** Key events extracted */
23
+ events: Array<{
24
+ time: string;
25
+ type: string;
26
+ message: string;
27
+ severity: "info" | "warn" | "error";
28
+ }>;
29
+ /** Discord connection status */
30
+ discord: {
31
+ connected: boolean;
32
+ botName: string | null;
33
+ lastEvent: string | null;
34
+ rateLimited: boolean;
35
+ error4014: boolean;
36
+ };
37
+ /** Gateway health */
38
+ gateway: {
39
+ started: boolean;
40
+ listening: boolean;
41
+ port: number | null;
42
+ model: string | null;
43
+ };
44
+ /** Auth issues */
45
+ auth: {
46
+ errors: string[];
47
+ refreshFailed: boolean;
48
+ tokenExpired: boolean;
49
+ };
50
+ /** Errors and warnings */
51
+ errors: string[];
52
+ warnings: string[];
53
+ }
54
+ /**
55
+ * Parse a single JSON log line.
56
+ */
57
+ export declare function parseLogLine(line: string): LogEntry | null;
58
+ /**
59
+ * Analyze a batch of log lines and extract meaningful events.
60
+ */
61
+ export declare function analyzeLogs(logText: string): LogAnalysis;
62
+ /**
63
+ * Format log analysis for display.
64
+ */
65
+ export declare function formatLogAnalysis(analysis: LogAnalysis): string;