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
package/dist/nlp/llmFallback.js
CHANGED
|
@@ -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
|
|
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:
|
|
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://
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
269
|
-
${
|
|
379
|
+
AVAILABLE COMMANDS (grouped by domain — ${intents.length} total):
|
|
380
|
+
${relevantDomains.join("\n")}
|
|
270
381
|
|
|
271
|
-
|
|
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
|
-
|
|
384
|
+
\`\`\`json
|
|
275
385
|
{
|
|
276
|
-
"understood":
|
|
277
|
-
"restatement": "
|
|
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
|
-
"
|
|
287
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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 {
|
package/dist/nlp/llmParser.d.ts
CHANGED
|
@@ -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
|
|
8
|
+
export declare function parseByLLM(rawText: string, nearMisses?: Array<{
|
|
9
|
+
intent: string;
|
|
10
|
+
score: number;
|
|
11
|
+
source: string;
|
|
12
|
+
}>): Promise<DynamicIntent | null>;
|
package/dist/nlp/llmParser.js
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.
|
|
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
|
|
74
|
-
Parse the user's natural language
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
${
|
|
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
|
|
83
|
-
|
|
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
|
|
90
|
-
Return ONLY
|
|
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)) {
|
package/dist/nlp/parseIntent.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/nlp/ruleParser.js
CHANGED
|
@@ -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" / "
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
-
|
|
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?"
|