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.
- package/config/entities.json +3 -3
- package/config/intents.json +453 -58
- package/dist/automation/discordPatchright.js +16 -3
- package/dist/handlers/executor.js +441 -6
- package/dist/index.d.ts +4 -1
- package/dist/index.js +5 -1
- package/dist/nlp/llmFallback.d.ts +47 -0
- package/dist/nlp/llmFallback.js +147 -35
- package/dist/nlp/llmParser.d.ts +5 -1
- package/dist/nlp/llmParser.js +43 -24
- package/dist/nlp/parseIntent.js +20 -2
- package/dist/nlp/ruleParser.js +32 -1
- package/dist/utils/discordDiag.js +20 -12
- package/dist/utils/openclawDiag.d.ts +98 -0
- package/dist/utils/openclawDiag.js +501 -1
- package/dist/utils/openclawLogParser.d.ts +65 -0
- package/dist/utils/openclawLogParser.js +168 -0
- package/dist/utils/userContext.d.ts +57 -0
- package/dist/utils/userContext.js +133 -0
- package/package.json +1 -1
|
@@ -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;
|