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.
- package/config/file-hints.json +255 -0
- package/config/hosts.json +14 -0
- package/config/intents.json +3920 -0
- package/config/playbooks.json +112 -0
- package/config/rules.json +100 -0
- package/dist/agents/agentSpawner.d.ts +56 -0
- package/dist/agents/agentSpawner.js +180 -0
- package/dist/agents/planner.d.ts +40 -0
- package/dist/agents/planner.js +175 -0
- package/dist/agents/playbookRunner.d.ts +45 -0
- package/dist/agents/playbookRunner.js +120 -0
- package/dist/agents/taskRunner.d.ts +61 -0
- package/dist/agents/taskRunner.js +142 -0
- package/dist/context/history.d.ts +36 -0
- package/dist/context/history.js +115 -0
- package/dist/conversation/coreference.d.ts +27 -0
- package/dist/conversation/coreference.js +147 -0
- package/dist/conversation/secrets.d.ts +43 -0
- package/dist/conversation/secrets.js +129 -0
- package/dist/conversation/store.d.ts +94 -0
- package/dist/conversation/store.js +184 -0
- package/dist/execution/git.d.ts +11 -0
- package/dist/execution/git.js +146 -0
- package/dist/execution/ssh.d.ts +2 -0
- package/dist/execution/ssh.js +17 -0
- package/dist/handlers/executor.d.ts +8 -0
- package/dist/handlers/executor.js +216 -0
- package/dist/healing/claudeHealer.d.ts +17 -0
- package/dist/healing/claudeHealer.js +300 -0
- package/dist/healing/patchPromoter.d.ts +25 -0
- package/dist/healing/patchPromoter.js +118 -0
- package/dist/healing/ruleBuilder.d.ts +5 -0
- package/dist/healing/ruleBuilder.js +111 -0
- package/dist/healing/ruleRepairer.d.ts +8 -0
- package/dist/healing/ruleRepairer.js +29 -0
- package/dist/healing/ruleValidator.d.ts +22 -0
- package/dist/healing/ruleValidator.js +145 -0
- package/dist/healing/runHealer.d.ts +11 -0
- package/dist/healing/runHealer.js +74 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +62 -0
- package/dist/intents/catalog.d.ts +4 -0
- package/dist/intents/catalog.js +7 -0
- package/dist/nlp/disambiguate.d.ts +2 -0
- package/dist/nlp/disambiguate.js +46 -0
- package/dist/nlp/fuzzyResolver.d.ts +14 -0
- package/dist/nlp/fuzzyResolver.js +108 -0
- package/dist/nlp/llmFallback.d.ts +63 -0
- package/dist/nlp/llmFallback.js +338 -0
- package/dist/nlp/llmParser.d.ts +8 -0
- package/dist/nlp/llmParser.js +118 -0
- package/dist/nlp/multiClassifier.d.ts +39 -0
- package/dist/nlp/multiClassifier.js +181 -0
- package/dist/nlp/parseIntent.d.ts +2 -0
- package/dist/nlp/parseIntent.js +34 -0
- package/dist/nlp/ruleParser.d.ts +2 -0
- package/dist/nlp/ruleParser.js +234 -0
- package/dist/nlp/semantic.d.ts +104 -0
- package/dist/nlp/semantic.js +419 -0
- package/dist/nlp/uncertainty.d.ts +42 -0
- package/dist/nlp/uncertainty.js +103 -0
- package/dist/parsers/apacheParser.d.ts +50 -0
- package/dist/parsers/apacheParser.js +152 -0
- package/dist/parsers/bindParser.d.ts +40 -0
- package/dist/parsers/bindParser.js +189 -0
- package/dist/parsers/envFile.d.ts +39 -0
- package/dist/parsers/envFile.js +128 -0
- package/dist/parsers/fileFinder.d.ts +30 -0
- package/dist/parsers/fileFinder.js +226 -0
- package/dist/parsers/index.d.ts +27 -0
- package/dist/parsers/index.js +193 -0
- package/dist/parsers/jsonParser.d.ts +16 -0
- package/dist/parsers/jsonParser.js +57 -0
- package/dist/parsers/nginxParser.d.ts +47 -0
- package/dist/parsers/nginxParser.js +161 -0
- package/dist/parsers/passwd.d.ts +25 -0
- package/dist/parsers/passwd.js +41 -0
- package/dist/parsers/shadow.d.ts +23 -0
- package/dist/parsers/shadow.js +50 -0
- package/dist/parsers/yamlParser.d.ts +13 -0
- package/dist/parsers/yamlParser.js +54 -0
- package/dist/policy/confirm.d.ts +2 -0
- package/dist/policy/confirm.js +29 -0
- package/dist/policy/safety.d.ts +4 -0
- package/dist/policy/safety.js +32 -0
- package/dist/types/intent.d.ts +205 -0
- package/dist/types/intent.js +32 -0
- package/dist/types/rules.d.ts +237 -0
- package/dist/types/rules.js +50 -0
- package/dist/utils/analysis.d.ts +25 -0
- package/dist/utils/analysis.js +307 -0
- package/dist/utils/autoBackup.d.ts +43 -0
- package/dist/utils/autoBackup.js +144 -0
- package/dist/utils/config.d.ts +11 -0
- package/dist/utils/config.js +32 -0
- package/dist/utils/dirAnalysis.d.ts +23 -0
- package/dist/utils/dirAnalysis.js +192 -0
- package/dist/utils/explain.d.ts +8 -0
- package/dist/utils/explain.js +145 -0
- package/dist/utils/logger.d.ts +5 -0
- package/dist/utils/logger.js +29 -0
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +26 -0
- package/dist/utils/paths.d.ts +26 -0
- package/dist/utils/paths.js +47 -0
- package/dist/utils/permissions.d.ts +64 -0
- package/dist/utils/permissions.js +298 -0
- package/dist/utils/platform.d.ts +53 -0
- package/dist/utils/platform.js +253 -0
- package/dist/utils/smartFile.d.ts +29 -0
- package/dist/utils/smartFile.js +188 -0
- package/dist/utils/spinner.d.ts +53 -0
- package/dist/utils/spinner.js +140 -0
- package/dist/utils/verbose.d.ts +27 -0
- package/dist/utils/verbose.js +131 -0
- package/dist/utils/wslPaths.d.ts +31 -0
- package/dist/utils/wslPaths.js +145 -0
- 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,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,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;
|