notoken-core 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/config/chat-responses.json +767 -0
  2. package/config/concept-clusters.json +31 -0
  3. package/config/entities.json +93 -0
  4. package/config/image-prompts.json +20 -0
  5. package/config/intent-vectors.json +1 -0
  6. package/config/intents.json +4946 -83
  7. package/config/ollama-models.json +193 -0
  8. package/config/rules.json +32 -1
  9. package/dist/automation/discordPatchright.d.ts +35 -0
  10. package/dist/automation/discordPatchright.js +424 -0
  11. package/dist/automation/discordSetup.d.ts +31 -0
  12. package/dist/automation/discordSetup.js +338 -0
  13. package/dist/conversation/coreference.js +44 -4
  14. package/dist/conversation/pendingActions.d.ts +55 -0
  15. package/dist/conversation/pendingActions.js +127 -0
  16. package/dist/conversation/store.d.ts +72 -0
  17. package/dist/conversation/store.js +140 -1
  18. package/dist/conversation/topicTracker.d.ts +36 -0
  19. package/dist/conversation/topicTracker.js +141 -0
  20. package/dist/execution/ssh.d.ts +42 -1
  21. package/dist/execution/ssh.js +532 -3
  22. package/dist/handlers/executor.js +3981 -16
  23. package/dist/index.d.ts +25 -3
  24. package/dist/index.js +36 -2
  25. package/dist/nlp/batchParser.d.ts +30 -0
  26. package/dist/nlp/batchParser.js +77 -0
  27. package/dist/nlp/conceptExpansion.d.ts +54 -0
  28. package/dist/nlp/conceptExpansion.js +136 -0
  29. package/dist/nlp/conceptRouter.d.ts +49 -0
  30. package/dist/nlp/conceptRouter.js +302 -0
  31. package/dist/nlp/confidenceCalibrator.d.ts +62 -0
  32. package/dist/nlp/confidenceCalibrator.js +116 -0
  33. package/dist/nlp/correctionLearner.d.ts +45 -0
  34. package/dist/nlp/correctionLearner.js +207 -0
  35. package/dist/nlp/entitySpellCorrect.d.ts +35 -0
  36. package/dist/nlp/entitySpellCorrect.js +141 -0
  37. package/dist/nlp/knowledgeGraph.d.ts +70 -0
  38. package/dist/nlp/knowledgeGraph.js +380 -0
  39. package/dist/nlp/llmFallback.js +28 -1
  40. package/dist/nlp/multiClassifier.js +91 -6
  41. package/dist/nlp/multiIntent.d.ts +43 -0
  42. package/dist/nlp/multiIntent.js +154 -0
  43. package/dist/nlp/parseIntent.d.ts +6 -1
  44. package/dist/nlp/parseIntent.js +180 -5
  45. package/dist/nlp/ruleParser.js +315 -0
  46. package/dist/nlp/semanticSimilarity.d.ts +30 -0
  47. package/dist/nlp/semanticSimilarity.js +174 -0
  48. package/dist/nlp/vocabularyBuilder.d.ts +43 -0
  49. package/dist/nlp/vocabularyBuilder.js +224 -0
  50. package/dist/nlp/wikidata.d.ts +49 -0
  51. package/dist/nlp/wikidata.js +228 -0
  52. package/dist/policy/confirm.d.ts +10 -0
  53. package/dist/policy/confirm.js +39 -0
  54. package/dist/policy/safety.js +6 -4
  55. package/dist/utils/aliases.d.ts +5 -0
  56. package/dist/utils/aliases.js +39 -0
  57. package/dist/utils/analysis.js +71 -15
  58. package/dist/utils/browser.d.ts +64 -0
  59. package/dist/utils/browser.js +364 -0
  60. package/dist/utils/commandHistory.d.ts +20 -0
  61. package/dist/utils/commandHistory.js +108 -0
  62. package/dist/utils/completer.d.ts +17 -0
  63. package/dist/utils/completer.js +79 -0
  64. package/dist/utils/config.js +32 -2
  65. package/dist/utils/dbQuery.d.ts +25 -0
  66. package/dist/utils/dbQuery.js +248 -0
  67. package/dist/utils/discordDiag.d.ts +35 -0
  68. package/dist/utils/discordDiag.js +826 -0
  69. package/dist/utils/diskCleanup.d.ts +36 -0
  70. package/dist/utils/diskCleanup.js +775 -0
  71. package/dist/utils/entityResolver.d.ts +107 -0
  72. package/dist/utils/entityResolver.js +468 -0
  73. package/dist/utils/imageGen.d.ts +92 -0
  74. package/dist/utils/imageGen.js +2031 -0
  75. package/dist/utils/installTracker.d.ts +57 -0
  76. package/dist/utils/installTracker.js +160 -0
  77. package/dist/utils/multiExec.d.ts +21 -0
  78. package/dist/utils/multiExec.js +141 -0
  79. package/dist/utils/openclawDiag.d.ts +29 -0
  80. package/dist/utils/openclawDiag.js +1035 -0
  81. package/dist/utils/output.js +4 -0
  82. package/dist/utils/platform.js +2 -1
  83. package/dist/utils/progressReporter.d.ts +50 -0
  84. package/dist/utils/progressReporter.js +58 -0
  85. package/dist/utils/projectDetect.d.ts +44 -0
  86. package/dist/utils/projectDetect.js +319 -0
  87. package/dist/utils/projectScanner.d.ts +44 -0
  88. package/dist/utils/projectScanner.js +312 -0
  89. package/dist/utils/shellCompat.d.ts +78 -0
  90. package/dist/utils/shellCompat.js +186 -0
  91. package/dist/utils/smartArchive.d.ts +16 -0
  92. package/dist/utils/smartArchive.js +172 -0
  93. package/dist/utils/smartRetry.d.ts +26 -0
  94. package/dist/utils/smartRetry.js +114 -0
  95. package/dist/utils/updater.d.ts +1 -0
  96. package/dist/utils/updater.js +1 -1
  97. package/dist/utils/version.d.ts +20 -0
  98. package/dist/utils/version.js +212 -0
  99. package/package.json +6 -3
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Multi-intent parser.
3
+ *
4
+ * Splits compound sentences into individual intents and creates a plan.
5
+ *
6
+ * "check if the firewall is blocking port 443 and also check dns for my domain"
7
+ * → Step 1: firewall.list (check port 443)
8
+ * Step 2: dns.lookup (check domain)
9
+ *
10
+ * "show me disk usage, check memory, and list running containers"
11
+ * → Step 1: server.check_disk
12
+ * Step 2: server.check_memory
13
+ * Step 3: docker.list
14
+ *
15
+ * Splitting rules:
16
+ * - Split on: "and", "also", "then", "after that", ",", ";"
17
+ * - But NOT inside quoted strings or after "and" that joins nouns ("cats and dogs")
18
+ * - Each part is parsed independently through rule parser + concept router
19
+ * - Only creates a plan if 2+ distinct intents are found
20
+ */
21
+ import { parseByRules } from "./ruleParser.js";
22
+ import { routeByConcepts } from "./conceptRouter.js";
23
+ import { getIntentDef } from "../utils/config.js";
24
+ const c = {
25
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
26
+ green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m",
27
+ };
28
+ // ─── Sentence Splitting ────────────────────────────────────────────────────
29
+ // Conjunctions that typically join separate commands
30
+ const SPLIT_PATTERNS = [
31
+ /\s+and\s+(?:also\s+)?(?:then\s+)?(?:can\s+you\s+)?/i,
32
+ /\s+also\s+/i,
33
+ /\s+then\s+/i,
34
+ /\s+after\s+that\s+/i,
35
+ /\s*;\s*/,
36
+ /\s*,\s+(?:and\s+)?(?:also\s+)?(?:then\s+)?(?:can\s+you\s+)?/i,
37
+ /\s+but\s+(?:first\s+)?(?:also\s+)?/i,
38
+ ];
39
+ // Don't split on "and" that joins nouns (e.g., "cats and dogs", "videos and photos")
40
+ const NOUN_AND_PATTERN = /^[a-z]+\s+and\s+[a-z]+$/i;
41
+ /**
42
+ * Split a compound sentence into parts.
43
+ */
44
+ export function splitCompoundSentence(text) {
45
+ let parts = [text.trim()];
46
+ for (const pattern of SPLIT_PATTERNS) {
47
+ const newParts = [];
48
+ for (const part of parts) {
49
+ const splits = part.split(pattern).map(s => s.trim()).filter(s => s.length > 2);
50
+ if (splits.length > 1) {
51
+ // Check if this is just noun joining (don't split "videos and photos")
52
+ const isNounJoin = splits.length === 2 && NOUN_AND_PATTERN.test(part);
53
+ if (isNounJoin) {
54
+ newParts.push(part);
55
+ }
56
+ else {
57
+ newParts.push(...splits);
58
+ }
59
+ }
60
+ else {
61
+ newParts.push(part);
62
+ }
63
+ }
64
+ parts = newParts;
65
+ }
66
+ // Clean up: remove leading filler words
67
+ return parts.map(p => p.replace(/^(can you|could you|please|will you|would you)\s+/i, "").trim()).filter(p => p.length > 2);
68
+ }
69
+ // ─── Multi-Intent Parsing ──────────────────────────────────────────────────
70
+ /**
71
+ * Parse a potentially compound sentence into a multi-step plan.
72
+ * Returns a single-step plan if only one intent is found.
73
+ */
74
+ export function parseMultiIntent(rawText) {
75
+ const parts = splitCompoundSentence(rawText);
76
+ // If only one part, it's a single intent
77
+ if (parts.length <= 1) {
78
+ const intent = resolveIntent(rawText);
79
+ return {
80
+ steps: intent ? [intentToStep(intent, rawText)] : [],
81
+ originalText: rawText,
82
+ isSingleIntent: true,
83
+ };
84
+ }
85
+ // Parse each part independently
86
+ const steps = [];
87
+ const seenIntents = new Set();
88
+ for (const part of parts) {
89
+ const intent = resolveIntent(part);
90
+ if (intent && !seenIntents.has(intent.intent)) {
91
+ steps.push(intentToStep(intent, part));
92
+ seenIntents.add(intent.intent);
93
+ }
94
+ }
95
+ return {
96
+ steps,
97
+ originalText: rawText,
98
+ isSingleIntent: steps.length <= 1,
99
+ };
100
+ }
101
+ function resolveIntent(text) {
102
+ // Try rule parser first
103
+ const rule = parseByRules(text);
104
+ if (rule && rule.confidence >= 0.6)
105
+ return rule;
106
+ // Then concept router
107
+ const concept = routeByConcepts(text);
108
+ if (concept && concept.confidence >= 0.5) {
109
+ return {
110
+ intent: concept.intent,
111
+ rawText: text,
112
+ confidence: concept.confidence,
113
+ fields: {},
114
+ };
115
+ }
116
+ return null;
117
+ }
118
+ function intentToStep(intent, rawText) {
119
+ const def = getIntentDef(intent.intent);
120
+ return {
121
+ intent: intent.intent,
122
+ rawText,
123
+ confidence: intent.confidence,
124
+ description: def?.description ?? intent.intent,
125
+ requiresConfirmation: def?.requiresConfirmation ?? false,
126
+ riskLevel: def?.riskLevel ?? "low",
127
+ };
128
+ }
129
+ // ─── Plan Formatting ───────────────────────────────────────────────────────
130
+ export function formatPlanSteps(plan) {
131
+ const lines = [];
132
+ if (plan.steps.length === 0) {
133
+ return `${c.dim}Could not create a plan from: "${plan.originalText}"${c.reset}`;
134
+ }
135
+ if (plan.isSingleIntent) {
136
+ return ""; // Don't show plan for single intents
137
+ }
138
+ const hasWrite = plan.steps.some(s => s.requiresConfirmation || s.riskLevel !== "low");
139
+ lines.push(`${c.bold}${c.cyan}Plan (${plan.steps.length} steps):${c.reset}`);
140
+ if (hasWrite) {
141
+ lines.push(`${c.yellow}⚠ Some steps modify your system — confirmation required${c.reset}`);
142
+ }
143
+ lines.push("");
144
+ for (let i = 0; i < plan.steps.length; i++) {
145
+ const step = plan.steps[i];
146
+ const num = `${c.cyan}${i + 1}.${c.reset}`;
147
+ const risk = step.riskLevel !== "low" ? ` ${c.yellow}[${step.riskLevel}]${c.reset}` : "";
148
+ const confirm = step.requiresConfirmation ? ` ${c.yellow}(needs confirmation)${c.reset}` : "";
149
+ lines.push(` ${num} ${c.bold}${step.intent}${c.reset}${risk}${confirm}`);
150
+ lines.push(` ${c.dim}${step.description}${c.reset}`);
151
+ lines.push(` ${c.dim}"${step.rawText}"${c.reset}`);
152
+ }
153
+ return lines.join("\n");
154
+ }
@@ -1,2 +1,7 @@
1
1
  import type { ParsedCommand } from "../types/intent.js";
2
- export declare function parseIntent(rawText: string): Promise<ParsedCommand>;
2
+ import { type MultiIntentPlan } from "./multiIntent.js";
3
+ /** Result from parseIntent — may contain a multi-step plan */
4
+ export type { MultiIntentPlan };
5
+ export declare function parseIntent(rawText: string): Promise<ParsedCommand & {
6
+ plan?: MultiIntentPlan;
7
+ }>;
@@ -2,22 +2,194 @@ import { parseByRules } from "./ruleParser.js";
2
2
  import { parseByLLM } from "./llmParser.js";
3
3
  import { disambiguate } from "./disambiguate.js";
4
4
  import { logFailure } from "../utils/logger.js";
5
+ import { classifyMulti } from "./multiClassifier.js";
6
+ import { lookupUnknownNouns } from "./wikidata.js";
7
+ import { routeByConcepts } from "./conceptRouter.js";
8
+ import { parseMultiIntent } from "./multiIntent.js";
9
+ import { isAffirmation, consumePendingAction, isRedirectingPendingAction } from "../conversation/pendingActions.js";
5
10
  export async function parseIntent(rawText) {
6
- // Stage 1: deterministic rule parser
11
+ // Stage -2: Knowledge graph reference resolution
12
+ // Only resolve if coreference hasn't already handled it (avoid double resolution).
13
+ // Coreference runs in interactive mode (interactive.ts), knowledge graph here covers one-shot mode.
14
+ // Stage -2: Knowledge graph reference resolution with candidate scoring
15
+ const hasPronouns = /\b(it|that|this)\b/i.test(rawText) && !/\b(it's|that's|this is)\b/i.test(rawText);
16
+ if (hasPronouns) {
17
+ try {
18
+ const { resolveCandidates } = await import("./knowledgeGraph.js");
19
+ const { getOrCreateConversation, getRecentEntities } = await import("../conversation/store.js");
20
+ const conv = getOrCreateConversation(process.cwd());
21
+ const recentEnts = getRecentEntities(conv, 5).map((e) => e.entity);
22
+ const words = rawText.split(/\s+/);
23
+ let resolved = rawText;
24
+ for (const word of words) {
25
+ if (/^(it|that|this)$/i.test(word)) {
26
+ const candidates = resolveCandidates(word, recentEnts);
27
+ if (candidates.length > 0) {
28
+ const best = candidates[0];
29
+ // Only resolve if confident (score > 0.5) or clear winner (gap > 0.2)
30
+ const gap = candidates.length > 1 ? best.score - candidates[1].score : 1;
31
+ if (best.score > 0.5 || gap > 0.2) {
32
+ resolved = resolved.replace(new RegExp(`\\b${word}\\b`, "i"), best.entity.name);
33
+ // Show candidates if close (for transparency)
34
+ if (candidates.length > 1 && gap < 0.3) {
35
+ const alt = candidates.slice(0, 3).map(c => `${c.entity.name} (${(c.score * 100).toFixed(0)}%)`).join(", ");
36
+ console.error(`\x1b[2mResolved "${word}" → ${best.entity.name} (candidates: ${alt})\x1b[0m`);
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ if (resolved !== rawText) {
43
+ const resolvedResult = parseByRules(resolved);
44
+ if (resolvedResult && resolvedResult.confidence >= 0.7) {
45
+ resolvedResult.rawText = rawText;
46
+ return disambiguate(resolvedResult);
47
+ }
48
+ }
49
+ }
50
+ catch { /* knowledge graph not available */ }
51
+ }
52
+ // Stage -1a: check if user is redirecting a pending action ("put it on F drive")
53
+ const redirect = isRedirectingPendingAction(rawText);
54
+ if (redirect) {
55
+ consumePendingAction();
56
+ // Re-parse with the new location context
57
+ const reParsed = parseByRules(redirect);
58
+ if (reParsed && reParsed.confidence >= 0.5)
59
+ return disambiguate(reParsed);
60
+ // Fall through to normal parsing with the redirected text
61
+ rawText = redirect;
62
+ }
63
+ // Stage -1b: check if user is affirming a pending action ("ok", "try it", "do it")
64
+ if (isAffirmation(rawText)) {
65
+ const pending = consumePendingAction();
66
+ if (pending) {
67
+ if (pending.type === "intent") {
68
+ const reParsed = parseByRules(pending.action);
69
+ if (reParsed && reParsed.confidence >= 0.5) {
70
+ return disambiguate(reParsed);
71
+ }
72
+ }
73
+ return disambiguate({
74
+ intent: pending.action.includes(".") ? pending.action : "unknown",
75
+ rawText: pending.action,
76
+ confidence: 0.8,
77
+ fields: pending.fields ?? {},
78
+ });
79
+ }
80
+ }
81
+ // Stage 0: check for compound sentences (multi-intent)
82
+ // "check disk and show me containers and list crontabs"
83
+ const multiPlan = parseMultiIntent(rawText);
84
+ if (!multiPlan.isSingleIntent && multiPlan.steps.length >= 2) {
85
+ // Check if one step is just a location modifier (not a real separate command)
86
+ // "do it offline and put files on D drive" → single intent with drive modifier
87
+ const locationStep = multiPlan.steps.find(s => /\b(put|place|install|store)\b.*\b(on|in|at)\s+[a-z]\s*drive\b/i.test(s.rawText)
88
+ || /\b(on|in|at)\s+\/\S+/i.test(s.rawText));
89
+ const actionStep = multiPlan.steps.find(s => s !== locationStep);
90
+ if (locationStep && actionStep && multiPlan.steps.length === 2) {
91
+ // Merge: use the action step's intent but the full original text
92
+ // so the executor can extract the drive from "on D drive"
93
+ const result = disambiguate({
94
+ intent: actionStep.intent,
95
+ rawText, // full original text
96
+ confidence: actionStep.confidence,
97
+ fields: {},
98
+ });
99
+ return result; // single intent, no plan
100
+ }
101
+ // Real multi-intent plan
102
+ const firstStep = multiPlan.steps[0];
103
+ const result = disambiguate({
104
+ intent: firstStep.intent,
105
+ rawText, // full original text
106
+ confidence: firstStep.confidence,
107
+ fields: {},
108
+ });
109
+ return { ...result, plan: multiPlan };
110
+ }
111
+ // Stage 1: deterministic rule parser (synonym matching + spell correction)
7
112
  const ruleResult = parseByRules(rawText);
8
113
  if (ruleResult && ruleResult.confidence >= 0.7) {
9
114
  return disambiguate(ruleResult);
10
115
  }
11
- // Stage 2: LLM fallback
116
+ // Stage 2: concept router — understands topics/domains, not just phrases
117
+ // Handles: "is this offline or cloud", "check my crontabs", etc.
118
+ const conceptResult = routeByConcepts(rawText);
119
+ if (conceptResult && conceptResult.confidence >= 0.6) {
120
+ return disambiguate({
121
+ intent: conceptResult.intent,
122
+ rawText,
123
+ confidence: conceptResult.confidence,
124
+ fields: {},
125
+ });
126
+ }
127
+ // Stage 2.5: multi-classifier (synonym + semantic + vector + fuzzy voting)
128
+ const multiResult = classifyMulti(rawText);
129
+ if (multiResult.best && multiResult.best.score >= 0.6 && !multiResult.ambiguous) {
130
+ const mFields = ruleResult?.fields ?? {};
131
+ return disambiguate({
132
+ intent: multiResult.best.intent,
133
+ rawText,
134
+ confidence: Math.min(0.95, multiResult.best.score),
135
+ fields: mFields,
136
+ });
137
+ }
138
+ // Stage 2.75: semantic similarity — catches paraphrases that exact matching misses
139
+ try {
140
+ const { findSimilarIntents } = await import("./semanticSimilarity.js");
141
+ const similar = findSimilarIntents(rawText, 3);
142
+ if (similar.length > 0 && similar[0].score >= 0.4) {
143
+ // Only use if it's clearly the best match (gap > 0.1 from second)
144
+ const gap = similar.length > 1 ? similar[0].score - similar[1].score : 1;
145
+ if (gap > 0.08 || similar[0].score >= 0.6) {
146
+ return disambiguate({
147
+ intent: similar[0].intent,
148
+ rawText,
149
+ confidence: Math.min(0.85, similar[0].score + 0.3),
150
+ fields: {},
151
+ });
152
+ }
153
+ }
154
+ }
155
+ catch { /* semantic similarity not available */ }
156
+ // Stage 3: LLM fallback
12
157
  const llmResult = await parseByLLM(rawText);
13
158
  if (llmResult && llmResult.confidence >= 0.5) {
14
159
  return disambiguate(llmResult);
15
160
  }
16
- // Stage 3: if rule parser had a low-confidence result, use it anyway
161
+ // Stage 4: if rule parser had a low-confidence result, use it anyway
17
162
  if (ruleResult) {
18
163
  return disambiguate(ruleResult);
19
164
  }
20
- // Stage 4: unknown log the failure for auto-learning
165
+ // Stage 4: check if the input looks like a "what is X" knowledge query
166
+ // If it contains unknown nouns, try Wikidata lookup and route to knowledge.lookup
167
+ const looksLikeQuestion = /^(what|who|tell|define|explain|info|facts|learn)\b/i.test(rawText.trim());
168
+ if (looksLikeQuestion) {
169
+ const topic = rawText.replace(/^(what|who)\s+(is|are|was|were)\s+/i, "")
170
+ .replace(/^(tell me about|define|explain|info about|facts about|learn about)\s+/i, "")
171
+ .replace(/\?$/, "").trim();
172
+ if (topic.length >= 2) {
173
+ return disambiguate({
174
+ intent: "knowledge.lookup",
175
+ rawText,
176
+ confidence: 0.6,
177
+ fields: { topic },
178
+ });
179
+ }
180
+ }
181
+ // Stage 5: for completely unknown input, try to identify unknown nouns via Wikidata
182
+ // and attach the context to the unknown result so the user gets useful info
183
+ const words = rawText.toLowerCase().split(/\s+/);
184
+ let wikiContext;
185
+ try {
186
+ const entities = await lookupUnknownNouns(words);
187
+ if (entities.length > 0) {
188
+ wikiContext = entities.map(e => `${e.label}: ${e.description}`).join("; ");
189
+ }
190
+ }
191
+ catch { }
192
+ // Stage 6: unknown — log the failure for auto-learning
21
193
  logFailure({
22
194
  rawText,
23
195
  timestamp: new Date().toISOString(),
@@ -29,6 +201,9 @@ export async function parseIntent(rawText) {
29
201
  intent: "unknown",
30
202
  rawText,
31
203
  confidence: 0,
32
- fields: { reason: "No supported intent matched" },
204
+ fields: {
205
+ reason: "No supported intent matched",
206
+ ...(wikiContext ? { wikiContext } : {}),
207
+ },
33
208
  });
34
209
  }