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,6 +10,18 @@
10
10
  import { execSync } from "node:child_process";
11
11
  import { loadIntents } from "../utils/config.js";
12
12
  import { detectLocalPlatform } from "../utils/platform.js";
13
+ /** Conversation history for multi-turn LLM disambiguation */
14
+ const _llmConversation = [];
15
+ /** Add a turn to the LLM conversation for multi-turn context */
16
+ export function addLLMContext(role, content) {
17
+ _llmConversation.push({ role, content });
18
+ if (_llmConversation.length > 10)
19
+ _llmConversation.splice(0, _llmConversation.length - 10);
20
+ }
21
+ /** Clear LLM conversation when topic changes */
22
+ export function clearLLMContext() { _llmConversation.length = 0; }
23
+ /** Get conversation for context in multi-turn */
24
+ export function getLLMContext() { return [..._llmConversation]; }
13
25
  /**
14
26
  * Check if any LLM is configured.
15
27
  */
@@ -98,6 +110,55 @@ export async function llmFallback(rawText, context) {
98
110
  }
99
111
  return null;
100
112
  }
113
+ /**
114
+ * Multi-turn LLM disambiguation.
115
+ *
116
+ * 1. Ask the LLM what to do
117
+ * 2. If it needs more info → run gatherCommands → feed results back
118
+ * 3. Repeat up to maxTurns times
119
+ * 4. Return the final result
120
+ */
121
+ export async function llmMultiTurn(rawText, context, options) {
122
+ const maxTurns = options?.maxTurns ?? 3;
123
+ const onProgress = options?.onProgress ?? (() => { });
124
+ // Turn 1: initial LLM call
125
+ onProgress("Asking LLM to interpret...");
126
+ let result = await llmFallback(rawText, context);
127
+ if (!result)
128
+ return null;
129
+ // Multi-turn loop: if LLM needs more info, run commands and ask again
130
+ for (let turn = 1; turn < maxTurns && result?.needsMoreInfo && result.gatherCommands?.length; turn++) {
131
+ const commands = result.gatherCommands;
132
+ onProgress(`Turn ${turn + 1}: Running ${commands.length} command(s) to gather info...`);
133
+ // Run each gather command
134
+ const commandResults = [];
135
+ for (const cmd of commands) {
136
+ onProgress(` Running: ${cmd.command}`);
137
+ try {
138
+ const { execSync } = await import("node:child_process");
139
+ const output = execSync(cmd.command, {
140
+ encoding: "utf-8",
141
+ timeout: 15_000,
142
+ stdio: ["pipe", "pipe", "pipe"],
143
+ }).trim();
144
+ commandResults.push({ ...cmd, output: output.substring(0, 1000) });
145
+ }
146
+ catch (err) {
147
+ const e = err;
148
+ commandResults.push({ ...cmd, output: `ERROR: ${e.message?.split("\n")[0] ?? "failed"}` });
149
+ }
150
+ }
151
+ // Build follow-up prompt with command outputs
152
+ const outputSummary = commandResults.map(r => `Command: ${r.command}\nPurpose: ${r.purpose}\nOutput:\n${r.output}`).join("\n\n");
153
+ const followUpText = `${rawText}\n\n--- COMMAND OUTPUTS ---\n${outputSummary}\n\nBased on these results, complete this JSON template. Replace FILL with values:\n\`\`\`json\n{"understood": FILL, "restatement": "FILL", "suggestedIntents": [{"intent": "FILL", "fields": {}, "confidence": FILL, "reasoning": "FILL"}], "needsMoreInfo": false}\n\`\`\``;
154
+ onProgress("Analyzing results...");
155
+ result = await llmFallback(followUpText, context);
156
+ // Track conversation
157
+ addLLMContext("user", rawText);
158
+ addLLMContext("assistant", JSON.stringify(commandResults));
159
+ }
160
+ return result;
161
+ }
101
162
  async function tryLLMCli(rawText, context) {
102
163
  const cli = process.env.NOTOKEN_LLM_CLI;
103
164
  if (!cli)
@@ -193,16 +254,21 @@ async function tryOllama(rawText, context) {
193
254
  const prompt = buildPrompt(rawText, context);
194
255
  const model = process.env.NOTOKEN_OLLAMA_MODEL ?? "llama3.2";
195
256
  try {
196
- const response = await fetch("http://localhost:11434/api/generate", {
257
+ const controller = new AbortController();
258
+ const timeout = setTimeout(() => controller.abort(), 90_000);
259
+ // Use 127.0.0.1 explicitly — Node 18 fetch resolves localhost to IPv6 ::1 which Ollama doesn't listen on
260
+ const response = await fetch("http://127.0.0.1:11434/api/generate", {
197
261
  method: "POST",
198
262
  headers: { "Content-Type": "application/json" },
263
+ signal: controller.signal,
199
264
  body: JSON.stringify({
200
265
  model,
201
266
  prompt,
202
267
  stream: false,
203
- options: { temperature: 0.1, num_predict: 1024 },
268
+ options: { temperature: 0.1, num_predict: 512 },
204
269
  }),
205
270
  });
271
+ clearTimeout(timeout);
206
272
  if (!response.ok)
207
273
  return null;
208
274
  const data = (await response.json());
@@ -211,7 +277,8 @@ async function tryOllama(rawText, context) {
211
277
  return null;
212
278
  return parseResponse(text);
213
279
  }
214
- catch {
280
+ catch (err) {
281
+ console.error(`\x1b[2m[llm-ollama] ${err.message?.substring(0, 100)}\x1b[0m`);
215
282
  return null;
216
283
  }
217
284
  }
@@ -234,7 +301,7 @@ export function isOllamaInstalled() {
234
301
  */
235
302
  export async function getOllamaModels() {
236
303
  try {
237
- const response = await fetch("http://localhost:11434/api/tags");
304
+ const response = await fetch("http://127.0.0.1:11434/api/tags");
238
305
  if (!response.ok)
239
306
  return [];
240
307
  const data = (await response.json());
@@ -246,50 +313,95 @@ export async function getOllamaModels() {
246
313
  }
247
314
  function buildPrompt(rawText, context) {
248
315
  const intents = loadIntents();
249
- const intentSummary = intents.map((i) => {
250
- const fields = Object.entries(i.fields)
251
- .map(([k, v]) => `${k}:${v.type}${v.required ? "*" : ""}`)
252
- .join(", ");
253
- return ` ${i.name}: ${i.description} [${fields}]`;
254
- }).join("\n");
255
316
  const platform = detectLocalPlatform();
256
- return `You are a server operations CLI assistant. The user said something I couldn't parse with my rule-based system.
317
+ // Group intents by domain much shorter than listing all 298
318
+ const domains = new Map();
319
+ for (const i of intents) {
320
+ const domain = i.name.split(".")[0];
321
+ if (!domains.has(domain))
322
+ domains.set(domain, []);
323
+ domains.get(domain).push(`${i.name}: ${i.description}`);
324
+ }
325
+ // Only include top-level summary + relevant domains based on user input
326
+ const relevantDomains = [];
327
+ const inputLower = rawText.toLowerCase();
328
+ for (const [domain, items] of domains) {
329
+ // Include domains that might be relevant to the query
330
+ const domainKeywords = {
331
+ service: ["service", "restart", "start", "stop", "status", "running"],
332
+ server: ["server", "cpu", "memory", "disk", "load", "uptime"],
333
+ docker: ["docker", "container", "image", "compose"],
334
+ network: ["network", "ip", "port", "ping", "dns", "curl", "speed"],
335
+ git: ["git", "commit", "push", "pull", "branch", "merge"],
336
+ deploy: ["deploy", "release", "rollback"],
337
+ logs: ["log", "error", "tail", "search"],
338
+ security: ["security", "attack", "firewall", "scan", "block"],
339
+ disk: ["disk", "space", "cleanup", "scan", "drive"],
340
+ db: ["database", "mysql", "postgres", "query", "sql"],
341
+ openclaw: ["openclaw", "claw", "gateway", "discord"],
342
+ ollama: ["ollama", "llm", "model"],
343
+ ai: ["image", "generate", "stable diffusion"],
344
+ files: ["file", "find", "copy", "move", "delete"],
345
+ process: ["process", "kill", "pid"],
346
+ user: ["user", "who", "login"],
347
+ backup: ["backup", "restore", "snapshot"],
348
+ notoken: ["notoken", "status", "version", "update", "help"],
349
+ };
350
+ const keywords = domainKeywords[domain] ?? [domain];
351
+ if (keywords.some(k => inputLower.includes(k)) || items.length <= 5) {
352
+ relevantDomains.push(`\n [${domain}] (${items.length} commands)\n${items.slice(0, 8).map(i => ` ${i}`).join("\n")}`);
353
+ }
354
+ }
355
+ // If no relevant domains found, include a general summary
356
+ if (relevantDomains.length === 0) {
357
+ for (const [domain, items] of [...domains].slice(0, 10)) {
358
+ relevantDomains.push(` [${domain}]: ${items.slice(0, 3).map(i => i.split(":")[0]).join(", ")}...`);
359
+ }
360
+ }
361
+ // Recent conversation context
362
+ const recentIntents = context.recentIntents ?? [];
363
+ const recentContext = recentIntents.length > 0
364
+ ? `\nRECENT COMMANDS (what the user has been doing):\n ${recentIntents.slice(0, 5).join(", ")}\n`
365
+ : "";
366
+ // Known entities
367
+ const entities = context.knownEntities ?? [];
368
+ const entityContext = entities.length > 0
369
+ ? `\nKNOWN ENTITIES:\n ${entities.slice(0, 10).map(e => `${e.entity} (${e.type})`).join(", ")}\n`
370
+ : "";
371
+ return `You are NoToken, a server operations CLI assistant. The user said something my NLP couldn't parse. Help me understand what they want.
257
372
 
258
373
  ENVIRONMENT:
259
- OS: ${platform.distro}${platform.isWSL ? " (WSL)" : ""}
260
- Kernel: ${platform.kernel}
261
- Arch: ${platform.arch}
262
- Shell: ${platform.shell}
263
- Package manager: ${platform.packageManager}
264
- Init system: ${platform.initSystem}
265
-
374
+ OS: ${platform.distro}${platform.isWSL ? " (WSL)" : ""} | Shell: ${platform.shell}
375
+ Package manager: ${platform.packageManager} | Init: ${platform.initSystem}
376
+ ${recentContext}${entityContext}
266
377
  USER INPUT: "${rawText}"
267
378
 
268
- CONTEXT:
269
- ${JSON.stringify(context, null, 2)}
379
+ AVAILABLE COMMANDS (grouped by domain — ${intents.length} total):
380
+ ${relevantDomains.join("\n")}
270
381
 
271
- AVAILABLE INTENTS (these are the tools I can execute):
272
- ${intentSummary}
382
+ Complete this JSON template. Replace every FILL with actual values based on the user's input. Remove fields you don't need. Output ONLY the completed JSON:
273
383
 
274
- Respond with ONLY a JSON object:
384
+ \`\`\`json
275
385
  {
276
- "understood": true/false,
277
- "restatement": "In plain English, what the user wants to do",
386
+ "understood": FILL_true_or_false,
387
+ "restatement": "FILL_what_user_wants_in_plain_english",
278
388
  "suggestedIntents": [
279
- {
280
- "intent": "intent.name from list above",
281
- "fields": { "field": "value" },
282
- "confidence": 0.0-1.0,
283
- "reasoning": "why this intent"
284
- }
389
+ {"intent": "FILL_best_matching_command_from_list_above", "fields": {}, "confidence": FILL_0_to_1, "reasoning": "FILL_why_this_command"}
285
390
  ],
286
- "todoSteps": [
287
- { "step": 1, "description": "what to do first", "intent": "optional intent name" }
391
+ "needsMoreInfo": FILL_true_if_you_need_to_run_commands_first,
392
+ "gatherCommands": [
393
+ {"command": "FILL_shell_command_to_run", "purpose": "FILL_why_run_this"}
288
394
  ],
289
- "missingInfo": ["things I'd need to ask the user"]
395
+ "shellCommands": ["FILL_raw_shell_command_if_no_intent_fits"],
396
+ "missingInfo": ["FILL_question_to_ask_user_if_unclear"]
290
397
  }
398
+ \`\`\`
291
399
 
292
- Return ONLY JSON.`;
400
+ Rules:
401
+ - If one of my commands fits, use suggestedIntents and set needsMoreInfo=false.
402
+ - If you need to investigate first, set needsMoreInfo=true and list gatherCommands with real shell commands (uptime, df -h, ps aux, curl, etc).
403
+ - I will run those commands and send you the output for a second round.
404
+ - Remove empty arrays. Output ONLY the JSON.`;
293
405
  }
294
406
  function parseResponse(raw) {
295
407
  try {
@@ -5,4 +5,8 @@ import type { DynamicIntent } from "../types/intent.js";
5
5
  * Sends the raw text + context to an LLM and asks for structured JSON.
6
6
  * Set NOTOKEN_LLM_ENDPOINT and optionally NOTOKEN_LLM_API_KEY in env.
7
7
  */
8
- export declare function parseByLLM(rawText: string): Promise<DynamicIntent | null>;
8
+ export declare function parseByLLM(rawText: string, nearMisses?: Array<{
9
+ intent: string;
10
+ score: number;
11
+ source: string;
12
+ }>): Promise<DynamicIntent | null>;
@@ -7,14 +7,14 @@ import { loadRules } from "../utils/config.js";
7
7
  * Sends the raw text + context to an LLM and asks for structured JSON.
8
8
  * Set NOTOKEN_LLM_ENDPOINT and optionally NOTOKEN_LLM_API_KEY in env.
9
9
  */
10
- export async function parseByLLM(rawText) {
10
+ export async function parseByLLM(rawText, nearMisses) {
11
11
  const endpoint = process.env.NOTOKEN_LLM_ENDPOINT;
12
12
  if (!endpoint)
13
13
  return null;
14
14
  const apiKey = process.env.NOTOKEN_LLM_API_KEY ?? "";
15
15
  const rules = loadRules();
16
16
  const intents = loadIntents();
17
- const systemPrompt = buildSystemPrompt(intents, rules);
17
+ const systemPrompt = buildSystemPrompt(intents, rules, nearMisses);
18
18
  const userPrompt = `Parse this command into structured intent JSON:\n\n"${rawText}"`;
19
19
  try {
20
20
  const response = await fetch(endpoint, {
@@ -59,35 +59,54 @@ export async function parseByLLM(rawText) {
59
59
  return null;
60
60
  }
61
61
  }
62
- function buildSystemPrompt(intents, rules) {
63
- const intentList = intents
64
- .map((i) => {
65
- const fields = Object.entries(i.fields)
66
- .map(([k, v]) => `${k}(${v.type}${v.required ? ",required" : ""})`)
67
- .join(", ");
68
- return `- ${i.name}: ${i.description} [${fields}]`;
69
- })
70
- .join("\n");
62
+ function buildSystemPrompt(intents, rules, nearMisses) {
63
+ // Build a concise intent list grouped by domain
64
+ const domains = new Map();
65
+ for (const i of intents) {
66
+ const domain = i.name.split(".")[0];
67
+ if (!domains.has(domain))
68
+ domains.set(domain, []);
69
+ const fields = Object.entries(i.fields).map(([k, v]) => `${k}:${v.type}`).join(", ");
70
+ domains.get(domain).push(`${i.name}${fields ? ` [${fields}]` : ""}`);
71
+ }
72
+ // If we have near-misses, show their full details + related intents
73
+ let nearMissSection = "";
74
+ if (nearMisses && nearMisses.length > 0) {
75
+ const nearMissDetails = nearMisses
76
+ .filter((v, i, a) => a.findIndex(x => x.intent === v.intent) === i) // dedup
77
+ .slice(0, 5)
78
+ .map(nm => {
79
+ const def = intents.find(i => i.name === nm.intent);
80
+ const fields = def ? Object.entries(def.fields).map(([k, v]) => `${k}:${v.type}`).join(", ") : "";
81
+ return ` - ${nm.intent} (${(nm.score * 100).toFixed(0)}% from ${nm.source}): ${def?.description ?? ""}${fields ? ` [${fields}]` : ""}`;
82
+ }).join("\n");
83
+ // Also include related intents from the same domains
84
+ const nearDomains = new Set(nearMisses.map(nm => nm.intent.split(".")[0]));
85
+ const relatedIntents = [...nearDomains].flatMap(d => (domains.get(d) ?? []).slice(0, 5)).join("\n ");
86
+ nearMissSection = `\nNEAR MATCHES (my classifiers think it might be one of these — pick the best or suggest another):
87
+ ${nearMissDetails}
88
+
89
+ RELATED COMMANDS in those domains:
90
+ ${relatedIntents}\n`;
91
+ }
92
+ // Compact domain summary for everything else
93
+ const domainSummary = [...domains].map(([d, items]) => ` [${d}]: ${items.slice(0, 4).join(", ")}${items.length > 4 ? ` +${items.length - 4} more` : ""}`).join("\n");
71
94
  const envs = Object.keys(rules.environmentAliases).join(", ");
72
95
  const services = Object.keys(rules.serviceAliases).join(", ");
73
- return `You are a command parser for a server operations CLI.
74
- Parse the user's natural language command into a JSON object.
75
-
76
- Supported intents:
77
- ${intentList}
96
+ return `You are NoToken, a server operations CLI command parser.
97
+ Parse the user's natural language into a structured JSON intent.
98
+ ${nearMissSection}
99
+ ALL AVAILABLE COMMANDS (${intents.length} total):
100
+ ${domainSummary}
78
101
 
79
102
  Known environments: ${envs}
80
103
  Known services: ${services}
81
104
 
82
- Return ONLY valid JSON with:
83
- - "intent": one of the intent names above, or "unknown"
84
- - "confidence": 0.0 to 1.0
85
- - "fields": object with all relevant fields for that intent
86
-
87
- Example: {"intent": "service.restart", "confidence": 0.9, "fields": {"service": "nginx", "environment": "prod"}}
105
+ Return ONLY valid JSON:
106
+ {"intent": "domain.command", "confidence": 0.0-1.0, "fields": {"field": "value"}}
88
107
 
89
- If you cannot determine the intent, return: {"intent": "unknown", "confidence": 0.1, "fields": {"reason": "..."}}
90
- Return ONLY the JSON object, no markdown.`;
108
+ If unclear, return: {"intent": "unknown", "confidence": 0.1, "fields": {"reason": "...", "clarification": "What did you mean? Did you want to..."}}
109
+ Return ONLY JSON, no markdown.`;
91
110
  }
92
111
  function extractContent(data) {
93
112
  if (data.choices && Array.isArray(data.choices)) {
@@ -153,8 +153,26 @@ export async function parseIntent(rawText) {
153
153
  }
154
154
  }
155
155
  catch { /* semantic similarity not available */ }
156
- // Stage 3: LLM fallback
157
- const llmResult = await parseByLLM(rawText);
156
+ // Stage 3: LLM fallback — pass near-miss candidates as context
157
+ // Gather partial matches from classifiers for the LLM to consider
158
+ const nearMisses = [];
159
+ if (multiResult.best)
160
+ nearMisses.push({ intent: multiResult.best.intent, score: multiResult.best.score, source: "classifier" });
161
+ // Include runner-up from classifier votes if available
162
+ const allScores = multiResult.votes.reduce((acc, v) => { acc[v.intent] = Math.max(acc[v.intent] ?? 0, v.confidence); return acc; }, {});
163
+ const sortedIntents = Object.entries(allScores).sort((a, b) => b[1] - a[1]);
164
+ if (sortedIntents.length > 1)
165
+ nearMisses.push({ intent: sortedIntents[1][0], score: sortedIntents[1][1], source: "classifier" });
166
+ try {
167
+ const { findSimilarIntents } = await import("./semanticSimilarity.js");
168
+ for (const s of findSimilarIntents(rawText, 3)) {
169
+ nearMisses.push({ intent: s.intent, score: s.score, source: "similarity" });
170
+ }
171
+ }
172
+ catch { }
173
+ if (ruleResult)
174
+ nearMisses.push({ intent: ruleResult.intent, score: ruleResult.confidence, source: "rules" });
175
+ const llmResult = await parseByLLM(rawText, nearMisses);
158
176
  if (llmResult && llmResult.confidence >= 0.5) {
159
177
  return disambiguate(llmResult);
160
178
  }
@@ -232,13 +232,44 @@ export function parseByRules(rawText) {
232
232
  const intentName = isDirList ? "dir.list" : "project.detect";
233
233
  return { intent: intentName, confidence: 0.9, rawText, fields: { path: "." } };
234
234
  }
235
- // Pre-check: "how is openclaw doing" / "how is discord doing" *.status
235
+ // Pre-check: "how is openclaw doing" / "status of openclaw" / "can you talk to openclaw"
236
236
  const howIsMatch = text.match(/^how(?:'s| is| are) (openclaw|claw|discord|ollama|notoken) (?:doing|going|running|working)/);
237
237
  if (howIsMatch) {
238
238
  const target = howIsMatch[1] === "claw" ? "openclaw" : howIsMatch[1];
239
239
  const intentName = target === "notoken" ? "notoken.status" : `${target}.status`;
240
240
  return { intent: intentName, confidence: 0.9, rawText, fields: {} };
241
241
  }
242
+ // "status of X" / "can you talk to X" / "diagnose X" / "check X"
243
+ // Also catches "can you talk to it" / "are you able to talk to it" with "it" passthrough
244
+ const statusOfMatch = text.match(/(?:status of|check on|talk to|communicate with|connect to|reach|diagnos\w*)\s+(openclaw|claw|discord|ollama|notoken)\b/);
245
+ if (statusOfMatch) {
246
+ const target = statusOfMatch[1] === "claw" ? "openclaw" : statusOfMatch[1];
247
+ const intentName = target === "notoken" ? "notoken.status" : `${target}.status`;
248
+ return { intent: intentName, confidence: 0.9, rawText, fields: {} };
249
+ }
250
+ // Pre-check: "uninstall ollama" → ollama.uninstall (not generic package.uninstall)
251
+ if (/\b(uninstall|remove|delete|get rid of)\s+ollama\b/i.test(text)) {
252
+ return { intent: "ollama.uninstall", confidence: 0.95, rawText, fields: {} };
253
+ }
254
+ // Pre-check: "do we have X" / "is X installed" → tool.info or specific status
255
+ const haveMatch = text.match(/\b(do we have|is|are)\s+(ollama|docker|nginx|node|python|git)\s+(installed|running|available|there|set ?up)\b/i)
256
+ ?? text.match(/\b(do we have|do i have)\s+(ollama|docker|nginx|node|python|git)\b/i);
257
+ if (haveMatch) {
258
+ const tool = haveMatch[2].toLowerCase();
259
+ if (tool === "ollama")
260
+ return { intent: "ollama.status", confidence: 0.9, rawText, fields: {} };
261
+ if (tool === "docker")
262
+ return { intent: "docker.list", confidence: 0.9, rawText, fields: {} };
263
+ return { intent: "tool.info", confidence: 0.9, rawText, fields: { tool } };
264
+ }
265
+ // Pre-check: file organization
266
+ if (/\b(organize|sort|tidy|clean ?up|arrange|categorize)\b.*\b(files?|folder|directory|downloads?|this)\b/i.test(text)
267
+ || /\b(files?|folder|directory|downloads?)\b.*\b(organize|sort|tidy|arrange|categorize)\b/i.test(text)) {
268
+ return { intent: "files.organize", confidence: 0.9, rawText, fields: {} };
269
+ }
270
+ if (/\bwhere\s+(should|do|can)\s+i\s+put\b/i.test(text) || /\bwhere\s+does\s+this\s+(go|belong)\b/i.test(text)) {
271
+ return { intent: "files.place", confidence: 0.9, rawText, fields: {} };
272
+ }
242
273
  // Match intent by synonyms defined in intents.json
243
274
  const matched = matchIntent(text, intents);
244
275
  if (!matched)
@@ -18,6 +18,7 @@
18
18
  * User only needs to handle captcha/MFA when patchright prompts.
19
19
  */
20
20
  import { execSync } from "node:child_process";
21
+ import { readFileSync, existsSync } from "node:fs";
21
22
  const c = {
22
23
  reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
23
24
  green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m",
@@ -66,18 +67,19 @@ async function discordApi(endpoint, token, method = "GET", body) {
66
67
  }
67
68
  }
68
69
  function getDiscordToken() {
69
- const config = tryExec("cat /root/.openclaw/openclaw.json 2>/dev/null");
70
- if (!config)
71
- return "";
70
+ const home = process.env.USERPROFILE || process.env.HOME || "/root";
71
+ const sep = process.platform === "win32" ? "\\" : "/";
72
+ const configPath = `${home}${sep}.openclaw${sep}openclaw.json`;
72
73
  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 "";
74
+ if (existsSync(configPath)) {
75
+ const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
76
+ return parsed?.channels?.discord?.token
77
+ ?? parsed?.channels?.discord?.accounts?.default?.token
78
+ ?? "";
79
+ }
80
80
  }
81
+ catch { }
82
+ return "";
81
83
  }
82
84
  /** Sleep helper */
83
85
  function sleep(ms) {
@@ -404,7 +406,8 @@ export async function diagnoseDiscord() {
404
406
  // ── 1. Token valid? ──
405
407
  token = getDiscordToken();
406
408
  if (!token) {
407
- const savedResult = tryExec("cat /mnt/c/temp/discord-bot-result.json 2>/dev/null");
409
+ const resultPath = process.platform === "win32" ? "C:\\temp\\discord-bot-result.json" : "/mnt/c/temp/discord-bot-result.json";
410
+ const savedResult = tryExec(`cat "${resultPath}" 2>/dev/null`);
408
411
  try {
409
412
  token = JSON.parse(savedResult)?.token ?? "";
410
413
  }
@@ -443,7 +446,12 @@ export async function diagnoseDiscord() {
443
446
  lines.push(` ${c.dim}Patchright unavailable: ${e.message?.substring(0, 50)}${c.reset}`);
444
447
  // Fallback: open URL in browser
445
448
  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`);
449
+ if (process.platform === "win32") {
450
+ tryExec(`powershell -Command "Start-Process '${inviteUrl}'" 2>/dev/null`);
451
+ }
452
+ else {
453
+ tryExec(`/mnt/c/Windows/System32/cmd.exe /c "start ${inviteUrl}" 2>/dev/null`);
454
+ }
447
455
  lines.push(` ${c.dim}Opened invite URL — add bot to server, then re-run this.${c.reset}`);
448
456
  }
449
457
  if (authorized) {
@@ -10,6 +10,104 @@
10
10
  * 6. What's the config state?
11
11
  * 7. Any errors in recent logs?
12
12
  */
13
+ /**
14
+ * Check Claude CLI status and sync OAuth token to OpenClaw if needed.
15
+ * Full diagnostic:
16
+ * 1. Is Claude CLI installed?
17
+ * 2. Does Claude have a valid OAuth token?
18
+ * 3. Is OpenClaw's copy stale?
19
+ * 4. Sync if needed.
20
+ */
21
+ /**
22
+ * Full OpenClaw auth refresh — uses expect for proper TTY flow,
23
+ * falls back to direct file write if expect unavailable.
24
+ *
25
+ * Flow:
26
+ * 1. Read fresh token from Claude CLI credentials
27
+ * 2. Try: expect → openclaw models auth paste-token --provider anthropic
28
+ * 3. Fallback: write directly to auth-profiles.json
29
+ * 4. Update lastGood pointer
30
+ */
31
+ export declare function refreshOpenclawAuth(): {
32
+ success: boolean;
33
+ method: string;
34
+ message: string;
35
+ };
36
+ /**
37
+ * Sync Codex (OpenAI) OAuth token to OpenClaw.
38
+ * Reads from ~/.codex/auth.json
39
+ */
40
+ /**
41
+ * Parse `openclaw models` output to understand current configuration.
42
+ */
43
+ export declare function parseOpenclawModels(output: string): {
44
+ defaultModel: string | null;
45
+ configuredModels: string[];
46
+ providers: Array<{
47
+ name: string;
48
+ status: string;
49
+ hasAuth: boolean;
50
+ }>;
51
+ errors: string[];
52
+ };
53
+ /**
54
+ * Parse `openclaw status --deep` for health and channel details.
55
+ */
56
+ export declare function parseOpenclawDeepStatus(output: string): {
57
+ health: Array<{
58
+ item: string;
59
+ status: string;
60
+ detail: string;
61
+ }>;
62
+ channels: Array<{
63
+ channel: string;
64
+ enabled: boolean;
65
+ state: string;
66
+ detail: string;
67
+ }>;
68
+ sessions: Array<{
69
+ key: string;
70
+ kind: string;
71
+ age: string;
72
+ model: string;
73
+ tokens: string;
74
+ }>;
75
+ };
76
+ /**
77
+ * Parse `openclaw status` output to understand gateway and system state.
78
+ */
79
+ export declare function parseOpenclawStatus(output: string): {
80
+ dashboard: string | null;
81
+ gateway: {
82
+ status: string;
83
+ url: string | null;
84
+ reachable: boolean;
85
+ latency: string | null;
86
+ };
87
+ agents: {
88
+ count: number;
89
+ lastActive: string | null;
90
+ };
91
+ sessions: {
92
+ count: number;
93
+ defaultModel: string | null;
94
+ contextSize: string | null;
95
+ };
96
+ update: {
97
+ available: boolean;
98
+ version: string | null;
99
+ };
100
+ security: {
101
+ critical: number;
102
+ warn: number;
103
+ info: number;
104
+ issues: string[];
105
+ };
106
+ services: {
107
+ gateway: string;
108
+ node: string;
109
+ };
110
+ };
13
111
  /**
14
112
  * Quick connectivity check — escalates from simplest to most thorough.
15
113
  * Used for "can you talk to openclaw?" / "is openclaw reachable?"