notoken-core 1.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 (118) hide show
  1. package/config/file-hints.json +255 -0
  2. package/config/hosts.json +14 -0
  3. package/config/intents.json +3920 -0
  4. package/config/playbooks.json +112 -0
  5. package/config/rules.json +100 -0
  6. package/dist/agents/agentSpawner.d.ts +56 -0
  7. package/dist/agents/agentSpawner.js +180 -0
  8. package/dist/agents/planner.d.ts +40 -0
  9. package/dist/agents/planner.js +175 -0
  10. package/dist/agents/playbookRunner.d.ts +45 -0
  11. package/dist/agents/playbookRunner.js +120 -0
  12. package/dist/agents/taskRunner.d.ts +61 -0
  13. package/dist/agents/taskRunner.js +142 -0
  14. package/dist/context/history.d.ts +36 -0
  15. package/dist/context/history.js +115 -0
  16. package/dist/conversation/coreference.d.ts +27 -0
  17. package/dist/conversation/coreference.js +147 -0
  18. package/dist/conversation/secrets.d.ts +43 -0
  19. package/dist/conversation/secrets.js +129 -0
  20. package/dist/conversation/store.d.ts +94 -0
  21. package/dist/conversation/store.js +184 -0
  22. package/dist/execution/git.d.ts +11 -0
  23. package/dist/execution/git.js +146 -0
  24. package/dist/execution/ssh.d.ts +2 -0
  25. package/dist/execution/ssh.js +17 -0
  26. package/dist/handlers/executor.d.ts +8 -0
  27. package/dist/handlers/executor.js +216 -0
  28. package/dist/healing/claudeHealer.d.ts +17 -0
  29. package/dist/healing/claudeHealer.js +300 -0
  30. package/dist/healing/patchPromoter.d.ts +25 -0
  31. package/dist/healing/patchPromoter.js +118 -0
  32. package/dist/healing/ruleBuilder.d.ts +5 -0
  33. package/dist/healing/ruleBuilder.js +111 -0
  34. package/dist/healing/ruleRepairer.d.ts +8 -0
  35. package/dist/healing/ruleRepairer.js +29 -0
  36. package/dist/healing/ruleValidator.d.ts +22 -0
  37. package/dist/healing/ruleValidator.js +145 -0
  38. package/dist/healing/runHealer.d.ts +11 -0
  39. package/dist/healing/runHealer.js +74 -0
  40. package/dist/index.d.ts +51 -0
  41. package/dist/index.js +62 -0
  42. package/dist/intents/catalog.d.ts +4 -0
  43. package/dist/intents/catalog.js +7 -0
  44. package/dist/nlp/disambiguate.d.ts +2 -0
  45. package/dist/nlp/disambiguate.js +46 -0
  46. package/dist/nlp/fuzzyResolver.d.ts +14 -0
  47. package/dist/nlp/fuzzyResolver.js +108 -0
  48. package/dist/nlp/llmFallback.d.ts +63 -0
  49. package/dist/nlp/llmFallback.js +338 -0
  50. package/dist/nlp/llmParser.d.ts +8 -0
  51. package/dist/nlp/llmParser.js +118 -0
  52. package/dist/nlp/multiClassifier.d.ts +39 -0
  53. package/dist/nlp/multiClassifier.js +181 -0
  54. package/dist/nlp/parseIntent.d.ts +2 -0
  55. package/dist/nlp/parseIntent.js +34 -0
  56. package/dist/nlp/ruleParser.d.ts +2 -0
  57. package/dist/nlp/ruleParser.js +234 -0
  58. package/dist/nlp/semantic.d.ts +104 -0
  59. package/dist/nlp/semantic.js +419 -0
  60. package/dist/nlp/uncertainty.d.ts +42 -0
  61. package/dist/nlp/uncertainty.js +103 -0
  62. package/dist/parsers/apacheParser.d.ts +50 -0
  63. package/dist/parsers/apacheParser.js +152 -0
  64. package/dist/parsers/bindParser.d.ts +40 -0
  65. package/dist/parsers/bindParser.js +189 -0
  66. package/dist/parsers/envFile.d.ts +39 -0
  67. package/dist/parsers/envFile.js +128 -0
  68. package/dist/parsers/fileFinder.d.ts +30 -0
  69. package/dist/parsers/fileFinder.js +226 -0
  70. package/dist/parsers/index.d.ts +27 -0
  71. package/dist/parsers/index.js +193 -0
  72. package/dist/parsers/jsonParser.d.ts +16 -0
  73. package/dist/parsers/jsonParser.js +57 -0
  74. package/dist/parsers/nginxParser.d.ts +47 -0
  75. package/dist/parsers/nginxParser.js +161 -0
  76. package/dist/parsers/passwd.d.ts +25 -0
  77. package/dist/parsers/passwd.js +41 -0
  78. package/dist/parsers/shadow.d.ts +23 -0
  79. package/dist/parsers/shadow.js +50 -0
  80. package/dist/parsers/yamlParser.d.ts +13 -0
  81. package/dist/parsers/yamlParser.js +54 -0
  82. package/dist/policy/confirm.d.ts +2 -0
  83. package/dist/policy/confirm.js +29 -0
  84. package/dist/policy/safety.d.ts +4 -0
  85. package/dist/policy/safety.js +32 -0
  86. package/dist/types/intent.d.ts +205 -0
  87. package/dist/types/intent.js +32 -0
  88. package/dist/types/rules.d.ts +237 -0
  89. package/dist/types/rules.js +50 -0
  90. package/dist/utils/analysis.d.ts +25 -0
  91. package/dist/utils/analysis.js +307 -0
  92. package/dist/utils/autoBackup.d.ts +43 -0
  93. package/dist/utils/autoBackup.js +144 -0
  94. package/dist/utils/config.d.ts +11 -0
  95. package/dist/utils/config.js +32 -0
  96. package/dist/utils/dirAnalysis.d.ts +23 -0
  97. package/dist/utils/dirAnalysis.js +192 -0
  98. package/dist/utils/explain.d.ts +8 -0
  99. package/dist/utils/explain.js +145 -0
  100. package/dist/utils/logger.d.ts +5 -0
  101. package/dist/utils/logger.js +29 -0
  102. package/dist/utils/output.d.ts +2 -0
  103. package/dist/utils/output.js +26 -0
  104. package/dist/utils/paths.d.ts +26 -0
  105. package/dist/utils/paths.js +47 -0
  106. package/dist/utils/permissions.d.ts +64 -0
  107. package/dist/utils/permissions.js +298 -0
  108. package/dist/utils/platform.d.ts +53 -0
  109. package/dist/utils/platform.js +253 -0
  110. package/dist/utils/smartFile.d.ts +29 -0
  111. package/dist/utils/smartFile.js +188 -0
  112. package/dist/utils/spinner.d.ts +53 -0
  113. package/dist/utils/spinner.js +140 -0
  114. package/dist/utils/verbose.d.ts +27 -0
  115. package/dist/utils/verbose.js +131 -0
  116. package/dist/utils/wslPaths.d.ts +31 -0
  117. package/dist/utils/wslPaths.js +145 -0
  118. package/package.json +39 -0
@@ -0,0 +1,181 @@
1
+ import { loadIntents, loadRules } from "../utils/config.js";
2
+ import { semanticParse, fuzzyMatch } from "./semantic.js";
3
+ import { parseByRules } from "./ruleParser.js";
4
+ const CLASSIFIER_WEIGHTS = {
5
+ synonym: 1.0,
6
+ semantic: 0.8,
7
+ context: 0.6,
8
+ fuzzy: 0.5,
9
+ };
10
+ /**
11
+ * Run all classifiers and merge results.
12
+ */
13
+ export function classifyMulti(rawText, recentIntents) {
14
+ const votes = [];
15
+ // 1. Synonym classifier (existing rule parser)
16
+ votes.push(...classifySynonym(rawText));
17
+ // 2. Semantic classifier (compromise-powered)
18
+ votes.push(...classifySemantic(rawText));
19
+ // 3. Context classifier (recent history)
20
+ if (recentIntents && recentIntents.length > 0) {
21
+ votes.push(...classifyContext(rawText, recentIntents));
22
+ }
23
+ // 4. Fuzzy classifier (keyboard distance)
24
+ votes.push(...classifyFuzzy(rawText));
25
+ // Merge votes into weighted scores
26
+ const scoreMap = new Map();
27
+ for (const vote of votes) {
28
+ const weight = CLASSIFIER_WEIGHTS[vote.classifier] ?? 1.0;
29
+ const existing = scoreMap.get(vote.intent) ?? { total: 0, count: 0 };
30
+ existing.total += vote.confidence * weight;
31
+ existing.count += 1;
32
+ scoreMap.set(vote.intent, existing);
33
+ }
34
+ const scores = Array.from(scoreMap.entries())
35
+ .map(([intent, { total, count }]) => ({
36
+ intent,
37
+ score: total / count,
38
+ votes: count,
39
+ }))
40
+ .sort((a, b) => b.score - a.score);
41
+ const best = scores[0] ?? null;
42
+ const second = scores[1];
43
+ const ambiguous = !!(best && second && best.score - second.score < 0.15);
44
+ return {
45
+ votes,
46
+ scores: scores.map((s) => ({ intent: s.intent, score: Math.round(s.score * 100) / 100, votes: s.votes })),
47
+ best: best ? { intent: best.intent, score: Math.round(best.score * 100) / 100 } : null,
48
+ ambiguous,
49
+ };
50
+ }
51
+ // ─── Individual Classifiers ──────────────────────────────────────────────────
52
+ function classifySynonym(rawText) {
53
+ const result = parseByRules(rawText);
54
+ if (!result || result.intent === "unknown")
55
+ return [];
56
+ return [{
57
+ classifier: "synonym",
58
+ intent: result.intent,
59
+ confidence: result.confidence,
60
+ reason: "Matched synonym in rules",
61
+ }];
62
+ }
63
+ function classifySemantic(rawText) {
64
+ const rules = loadRules();
65
+ const intents = loadIntents();
66
+ const services = Object.keys(rules.serviceAliases);
67
+ const envs = Object.keys(rules.environmentAliases);
68
+ const parse = semanticParse(rawText, services, envs);
69
+ const votes = [];
70
+ if (!parse.action)
71
+ return votes;
72
+ // Match action verb to intent
73
+ for (const def of intents) {
74
+ const actionScore = scoreActionMatch(parse.action, def);
75
+ if (actionScore > 0) {
76
+ // Boost if entities also match expected fields
77
+ const entityBoost = scoreEntityMatch(parse, def);
78
+ const confidence = Math.min(0.95, actionScore * 0.6 + entityBoost * 0.4);
79
+ votes.push({
80
+ classifier: "semantic",
81
+ intent: def.name,
82
+ confidence,
83
+ reason: `Verb "${parse.action}" matches ${def.name}`,
84
+ });
85
+ }
86
+ }
87
+ return votes;
88
+ }
89
+ function classifyContext(rawText, recentIntents) {
90
+ const votes = [];
91
+ // If recent intents are heavily weighted toward one area, boost it slightly
92
+ const intentCounts = new Map();
93
+ for (const i of recentIntents) {
94
+ intentCounts.set(i, (intentCounts.get(i) ?? 0) + 1);
95
+ }
96
+ // Get the domain prefix of recent intents (e.g., "service", "git", "logs")
97
+ const domainCounts = new Map();
98
+ for (const [intent, count] of intentCounts) {
99
+ const domain = intent.split(".")[0];
100
+ domainCounts.set(domain, (domainCounts.get(domain) ?? 0) + count);
101
+ }
102
+ // If the raw text matches a recent domain, give a small boost
103
+ const intents = loadIntents();
104
+ for (const def of intents) {
105
+ const domain = def.name.split(".")[0];
106
+ const domainFreq = domainCounts.get(domain) ?? 0;
107
+ if (domainFreq > 0) {
108
+ // Check if any synonym partially matches
109
+ const hasPartialMatch = def.synonyms.some((s) => rawText.toLowerCase().includes(s.split(" ")[0]));
110
+ if (hasPartialMatch) {
111
+ votes.push({
112
+ classifier: "context",
113
+ intent: def.name,
114
+ confidence: Math.min(0.7, 0.3 + domainFreq * 0.1),
115
+ reason: `Recent context favors ${domain} domain`,
116
+ });
117
+ }
118
+ }
119
+ }
120
+ return votes;
121
+ }
122
+ function classifyFuzzy(rawText) {
123
+ const intents = loadIntents();
124
+ const votes = [];
125
+ const words = rawText.toLowerCase().split(/\s+/);
126
+ for (const def of intents) {
127
+ for (const word of words) {
128
+ for (const synonym of def.synonyms) {
129
+ const synonymWords = synonym.split(" ");
130
+ for (const sw of synonymWords) {
131
+ if (sw.length < 3)
132
+ continue;
133
+ const match = fuzzyMatch(word, [sw], 1.5);
134
+ if (match && match.distance > 0 && match.distance <= 1.5) {
135
+ votes.push({
136
+ classifier: "fuzzy",
137
+ intent: def.name,
138
+ confidence: Math.max(0.3, 0.7 - match.distance * 0.3),
139
+ reason: `Fuzzy: "${word}" ≈ "${sw}" (dist: ${match.distance})`,
140
+ });
141
+ break;
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+ return votes;
148
+ }
149
+ // ─── Scoring Helpers ─────────────────────────────────────────────────────────
150
+ function scoreActionMatch(action, def) {
151
+ const actionLower = action.toLowerCase();
152
+ // Direct synonym match
153
+ if (def.synonyms.some((s) => s.includes(actionLower)))
154
+ return 0.9;
155
+ // Check if action is part of the intent name
156
+ if (def.name.includes(actionLower))
157
+ return 0.7;
158
+ // Check examples
159
+ if (def.examples.some((e) => e.toLowerCase().includes(actionLower)))
160
+ return 0.5;
161
+ return 0;
162
+ }
163
+ function scoreEntityMatch(parse, def) {
164
+ let matches = 0;
165
+ let total = Object.keys(def.fields).length;
166
+ if (total === 0)
167
+ return 0.5;
168
+ for (const [fieldName, fieldDef] of Object.entries(def.fields)) {
169
+ if (fieldDef.type === "environment" && parse.location)
170
+ matches++;
171
+ if (fieldDef.type === "service" && parse.entities.some((e) => e.type === "SERVICE"))
172
+ matches++;
173
+ if (fieldDef.type === "number" && parse.quantity !== undefined)
174
+ matches++;
175
+ if (fieldName === "destination" && parse.destination)
176
+ matches++;
177
+ if (fieldName === "source" && parse.source)
178
+ matches++;
179
+ }
180
+ return matches / total;
181
+ }
@@ -0,0 +1,2 @@
1
+ import type { ParsedCommand } from "../types/intent.js";
2
+ export declare function parseIntent(rawText: string): Promise<ParsedCommand>;
@@ -0,0 +1,34 @@
1
+ import { parseByRules } from "./ruleParser.js";
2
+ import { parseByLLM } from "./llmParser.js";
3
+ import { disambiguate } from "./disambiguate.js";
4
+ import { logFailure } from "../utils/logger.js";
5
+ export async function parseIntent(rawText) {
6
+ // Stage 1: deterministic rule parser
7
+ const ruleResult = parseByRules(rawText);
8
+ if (ruleResult && ruleResult.confidence >= 0.7) {
9
+ return disambiguate(ruleResult);
10
+ }
11
+ // Stage 2: LLM fallback
12
+ const llmResult = await parseByLLM(rawText);
13
+ if (llmResult && llmResult.confidence >= 0.5) {
14
+ return disambiguate(llmResult);
15
+ }
16
+ // Stage 3: if rule parser had a low-confidence result, use it anyway
17
+ if (ruleResult) {
18
+ return disambiguate(ruleResult);
19
+ }
20
+ // Stage 4: unknown — log the failure for auto-learning
21
+ logFailure({
22
+ rawText,
23
+ timestamp: new Date().toISOString(),
24
+ parsedIntent: null,
25
+ confidence: 0,
26
+ error: "No parser matched",
27
+ });
28
+ return disambiguate({
29
+ intent: "unknown",
30
+ rawText,
31
+ confidence: 0,
32
+ fields: { reason: "No supported intent matched" },
33
+ });
34
+ }
@@ -0,0 +1,2 @@
1
+ import type { DynamicIntent } from "../types/intent.js";
2
+ export declare function parseByRules(rawText: string): DynamicIntent | null;
@@ -0,0 +1,234 @@
1
+ import { loadRules, loadIntents } from "../utils/config.js";
2
+ import { normalizePath } from "../utils/wslPaths.js";
3
+ export function parseByRules(rawText) {
4
+ const rules = loadRules();
5
+ const intents = loadIntents();
6
+ const text = rawText.trim().toLowerCase();
7
+ // Match intent by synonyms defined in intents.json
8
+ const matched = matchIntent(text, intents);
9
+ if (!matched)
10
+ return null;
11
+ const { def, matchedPhrase } = matched;
12
+ // Extract fields based on the intent's field definitions
13
+ const fields = {};
14
+ let allRequiredFound = true;
15
+ // First pass: extract typed fields (environment, service, number, branch)
16
+ for (const [fieldName, fieldDef] of Object.entries(def.fields)) {
17
+ let value = undefined;
18
+ switch (fieldDef.type) {
19
+ case "environment":
20
+ value = extractEnvironment(text, rules.environmentAliases);
21
+ break;
22
+ case "service":
23
+ value = extractService(text, rules.serviceAliases, matchedPhrase);
24
+ break;
25
+ case "number":
26
+ value = extractNumber(text);
27
+ break;
28
+ case "branch":
29
+ value = extractBranch(text);
30
+ break;
31
+ }
32
+ if (value !== undefined) {
33
+ fields[fieldName] = value;
34
+ }
35
+ }
36
+ // Second pass: extract string fields using context-aware extraction
37
+ const stringFields = Object.entries(def.fields).filter(([, fd]) => fd.type === "string");
38
+ if (stringFields.length > 0) {
39
+ const extracted = extractStringFields(rawText, text, matchedPhrase, stringFields.map(([n]) => n), fields);
40
+ for (const [k, v] of Object.entries(extracted)) {
41
+ if (v !== undefined)
42
+ fields[k] = v;
43
+ }
44
+ }
45
+ // Apply defaults for missing fields
46
+ for (const [fieldName, fieldDef] of Object.entries(def.fields)) {
47
+ if (fields[fieldName] === undefined && fieldDef.default !== undefined) {
48
+ fields[fieldName] = fieldDef.default;
49
+ }
50
+ if (fields[fieldName] === undefined && fieldDef.required) {
51
+ allRequiredFound = false;
52
+ }
53
+ }
54
+ // Resolve logPaths if the intent uses them
55
+ if (def.logPaths && fields.service) {
56
+ const logPath = def.logPaths[fields.service];
57
+ if (logPath)
58
+ fields.logPath = logPath;
59
+ }
60
+ // Confidence scoring
61
+ let confidence = 0.7;
62
+ if (allRequiredFound)
63
+ confidence += 0.15;
64
+ if (matchedPhrase.length > 4)
65
+ confidence += 0.05;
66
+ confidence = Math.min(confidence, 0.95);
67
+ return {
68
+ intent: def.name,
69
+ confidence,
70
+ rawText,
71
+ fields,
72
+ };
73
+ }
74
+ /**
75
+ * Extract string fields from natural language using preposition patterns.
76
+ *
77
+ * Handles patterns like:
78
+ * "copy nginx.conf to /root" → source=nginx.conf, destination=/root
79
+ * "move app.log to /backup" → source=app.log, destination=/backup
80
+ * "grep error in /var/log" → query=error, path=/var/log
81
+ * "find *.conf in /etc" → pattern=*.conf, path=/etc
82
+ */
83
+ function extractStringFields(rawText, lowerText, matchedPhrase, fieldNames, alreadyExtracted) {
84
+ const result = {};
85
+ // Remove the matched intent phrase and known extracted values from text
86
+ let remaining = lowerText.replace(matchedPhrase, " ");
87
+ for (const [, v] of Object.entries(alreadyExtracted)) {
88
+ if (typeof v === "string") {
89
+ remaining = remaining.replace(v, " ");
90
+ }
91
+ }
92
+ remaining = remaining.replace(/\s+/g, " ").trim();
93
+ // Check for quoted strings first
94
+ const quoted = rawText.match(/["']([^"']+)["']/g);
95
+ if (quoted) {
96
+ for (let i = 0; i < Math.min(quoted.length, fieldNames.length); i++) {
97
+ result[fieldNames[i]] = quoted[i].replace(/["']/g, "");
98
+ }
99
+ return result;
100
+ }
101
+ // For source/destination patterns (copy X to Y, move X to Y)
102
+ if (fieldNames.includes("source") && fieldNames.includes("destination")) {
103
+ const toMatch = remaining.match(/(.+?)\s+to\s+(.+)/);
104
+ if (toMatch) {
105
+ result.source = extractPathOrFilename(toMatch[1].trim());
106
+ result.destination = extractPathOrFilename(toMatch[2].trim());
107
+ return result;
108
+ }
109
+ }
110
+ // For query/path patterns (grep X in Y, search X in Y, Y for X)
111
+ if (fieldNames.includes("query") && fieldNames.includes("path")) {
112
+ // "X in Y"
113
+ const inMatch = remaining.match(/(.+?)\s+in\s+(.+)/);
114
+ if (inMatch) {
115
+ result.query = inMatch[1].trim().replace(/^(for|the)\s+/, "");
116
+ result.path = extractPathOrFilename(inMatch[2].trim());
117
+ return result;
118
+ }
119
+ // "Y for X" (path first, query second)
120
+ const forMatch = remaining.match(/(.+?)\s+for\s+(.+)/);
121
+ if (forMatch) {
122
+ const left = forMatch[1].trim();
123
+ const right = forMatch[2].trim();
124
+ // If left looks like a path, it's path+query. Otherwise query+path.
125
+ if (left.includes("/") || left.includes(".")) {
126
+ result.path = extractPathOrFilename(left);
127
+ result.query = right.replace(/^(the)\s+/, "");
128
+ }
129
+ else {
130
+ result.query = left.replace(/^(the)\s+/, "");
131
+ result.path = extractPathOrFilename(right);
132
+ }
133
+ return result;
134
+ }
135
+ // Split by path-like token: anything with / or . is path, rest is query
136
+ const words = remaining.split(/\s+/).filter((w) => !isStopWord(w));
137
+ const pathWord = words.find((w) => w.includes("/") || (w.includes(".") && w.length > 3));
138
+ if (pathWord) {
139
+ result.path = pathWord;
140
+ result.query = words.filter((w) => w !== pathWord).join(" ") || undefined;
141
+ return result;
142
+ }
143
+ if (words.length > 0) {
144
+ result.query = words[0];
145
+ }
146
+ return result;
147
+ }
148
+ // For pattern/path patterns (find X in Y)
149
+ if (fieldNames.includes("pattern") && fieldNames.includes("path")) {
150
+ const inMatch = remaining.match(/(.+?)\s+in\s+(.+)/);
151
+ if (inMatch) {
152
+ result.pattern = extractPathOrFilename(inMatch[1].trim());
153
+ result.path = extractPathOrFilename(inMatch[2].trim());
154
+ return result;
155
+ }
156
+ }
157
+ // For single target field (delete X, kill X)
158
+ if (fieldNames.length === 1) {
159
+ const words = remaining.split(/\s+/).filter((w) => !isStopWord(w));
160
+ const pathLike = words.find((w) => w.includes("/") || w.includes("."));
161
+ result[fieldNames[0]] = pathLike ?? words[0];
162
+ return result;
163
+ }
164
+ // Generic: assign remaining words to fields in order
165
+ const words = remaining.split(/\s+/).filter((w) => !isStopWord(w));
166
+ for (let i = 0; i < Math.min(words.length, fieldNames.length); i++) {
167
+ result[fieldNames[i]] = words[i];
168
+ }
169
+ return result;
170
+ }
171
+ function extractPathOrFilename(text) {
172
+ const cleaned = text.replace(/^(the|this|that|a|an|file|directory|dir|folder)\s+/gi, "").trim();
173
+ const words = cleaned.split(/\s+/);
174
+ // Find the most path-like word (Linux or Windows paths)
175
+ const pathWord = words.find((w) => w.includes("/") || w.includes("\\") || w.includes(".") || /^[A-Za-z]:/.test(w));
176
+ const result = pathWord ?? words[0] ?? cleaned;
177
+ // Normalize Windows paths to Linux in WSL
178
+ return normalizePath(result);
179
+ }
180
+ function isStopWord(word) {
181
+ return ["the", "a", "an", "this", "that", "on", "in", "at", "for", "from", "with", "of", "file", "files"].includes(word);
182
+ }
183
+ function matchIntent(text, intents) {
184
+ let best = null;
185
+ for (const def of intents) {
186
+ for (const phrase of def.synonyms) {
187
+ if (text.includes(phrase)) {
188
+ if (!best || phrase.length > best.length) {
189
+ best = { def, matchedPhrase: phrase, length: phrase.length };
190
+ }
191
+ }
192
+ }
193
+ }
194
+ return best ? { def: best.def, matchedPhrase: best.matchedPhrase } : null;
195
+ }
196
+ function extractEnvironment(text, aliases) {
197
+ for (const [canonical, aliasList] of Object.entries(aliases)) {
198
+ for (const alias of aliasList) {
199
+ const pattern = new RegExp(`\\b${escapeRegex(alias)}\\b`);
200
+ if (pattern.test(text))
201
+ return canonical;
202
+ }
203
+ }
204
+ return undefined;
205
+ }
206
+ function extractService(text, aliases, intentPhrase) {
207
+ const cleaned = text.replace(intentPhrase, " ").trim();
208
+ for (const [canonical, aliasList] of Object.entries(aliases)) {
209
+ for (const alias of aliasList) {
210
+ const pattern = new RegExp(`\\b${escapeRegex(alias)}\\b`);
211
+ if (pattern.test(cleaned))
212
+ return canonical;
213
+ }
214
+ }
215
+ for (const [canonical, aliasList] of Object.entries(aliases)) {
216
+ for (const alias of aliasList) {
217
+ const pattern = new RegExp(`\\b${escapeRegex(alias)}\\b`);
218
+ if (pattern.test(text))
219
+ return canonical;
220
+ }
221
+ }
222
+ return undefined;
223
+ }
224
+ function extractNumber(text) {
225
+ const match = text.match(/\b(\d+)\b/);
226
+ return match ? Number(match[1]) : undefined;
227
+ }
228
+ function extractBranch(text) {
229
+ const match = text.match(/\b(main|master|develop|release\/[a-z0-9._-]+)\b/);
230
+ return match?.[1];
231
+ }
232
+ function escapeRegex(str) {
233
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
234
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Semantic NLP layer — powered by compromise.
3
+ *
4
+ * Uses compromise for:
5
+ * - Tokenization
6
+ * - POS tagging (Verb, Noun, Adjective, Preposition, etc.)
7
+ * - Normalization and root form extraction
8
+ * - Sentence/clause structure
9
+ *
10
+ * Layers on top:
11
+ * - Domain entity recognition (services, environments, paths)
12
+ * - Adjacent keyboard typo correction
13
+ * - Dependency parsing (SVO + prepositional)
14
+ * - Concept graph builder
15
+ * - Knowledge graph for entity relationships
16
+ */
17
+ export declare function keyboardDistance(a: string, b: string): number;
18
+ export declare function fuzzyMatch(word: string, candidates: string[], maxDistance?: number): {
19
+ match: string;
20
+ distance: number;
21
+ } | null;
22
+ export type TokenTag = "VERB" | "NOUN" | "PATH" | "ENV" | "SERVICE" | "NUMBER" | "PREP" | "DET" | "ADJ" | "ADV" | "CONJ" | "UNKNOWN";
23
+ export interface Token {
24
+ text: string;
25
+ tag: TokenTag;
26
+ index: number;
27
+ normalized?: string;
28
+ /** POS tags from compromise */
29
+ compromiseTags?: string[];
30
+ /** Root/infinitive form from compromise */
31
+ root?: string;
32
+ }
33
+ /**
34
+ * Use compromise to get rich POS tagging and normalization,
35
+ * then overlay domain-specific entity detection and typo correction.
36
+ *
37
+ * Pre-processes input to protect path-like tokens (e.g. /var/log)
38
+ * from being split by compromise's tokenizer.
39
+ */
40
+ export declare function tokenize(text: string, knownServices: string[], knownEnvironments: string[]): Token[];
41
+ export interface Dependency {
42
+ head: Token;
43
+ dependent: Token;
44
+ relation: "subject" | "object" | "modifier" | "location" | "destination" | "source" | "quantity";
45
+ }
46
+ export declare function parseDependencies(tokens: Token[]): Dependency[];
47
+ export interface ConceptNode {
48
+ id: string;
49
+ label: string;
50
+ type: "action" | "entity" | "property" | "location" | "quantity";
51
+ aliases: string[];
52
+ }
53
+ export interface ConceptEdge {
54
+ from: string;
55
+ to: string;
56
+ relation: "acts_on" | "located_at" | "has_property" | "quantity_of" | "destination" | "source";
57
+ }
58
+ export interface ConceptGraph {
59
+ nodes: ConceptNode[];
60
+ edges: ConceptEdge[];
61
+ }
62
+ export declare function buildConceptGraph(tokens: Token[], deps: Dependency[]): ConceptGraph;
63
+ /**
64
+ * Extract verbs in their root/infinitive form using compromise.
65
+ */
66
+ export declare function extractVerbs(text: string): string[];
67
+ /**
68
+ * Extract nouns using compromise.
69
+ */
70
+ export declare function extractNouns(text: string): string[];
71
+ /**
72
+ * Normalize text — lowercase, expand contractions, normalize whitespace.
73
+ */
74
+ export declare function normalizeText(text: string): string;
75
+ /**
76
+ * Get sentence structure analysis from compromise.
77
+ */
78
+ export declare function analyzeSentence(text: string): {
79
+ verbs: string[];
80
+ nouns: string[];
81
+ adjectives: string[];
82
+ prepositions: string[];
83
+ isQuestion: boolean;
84
+ isNegative: boolean;
85
+ tense: string;
86
+ };
87
+ export interface SemanticParse {
88
+ tokens: Token[];
89
+ dependencies: Dependency[];
90
+ graph: ConceptGraph;
91
+ action?: string;
92
+ actionRoot?: string;
93
+ entities: Array<{
94
+ text: string;
95
+ type: string;
96
+ normalized?: string;
97
+ }>;
98
+ location?: string;
99
+ destination?: string;
100
+ source?: string;
101
+ quantity?: number;
102
+ sentence: ReturnType<typeof analyzeSentence>;
103
+ }
104
+ export declare function semanticParse(text: string, knownServices: string[], knownEnvironments: string[]): SemanticParse;