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,224 @@
1
+ /**
2
+ * Vocabulary Builder — learns vocabulary from Wikidata lookups.
3
+ *
4
+ * After every successful Wikidata entity lookup, this module:
5
+ * 1. Extracts instanceOf labels and maps them to intent domains
6
+ * 2. Collects aliases as synonyms for future matching
7
+ * 3. Adds related concepts to the concept router map
8
+ * 4. Persists learned vocabulary to ~/.notoken/learned-vocabulary.json
9
+ *
10
+ * On startup, loads learned vocabulary and merges it into the
11
+ * concept router's CONCEPT_DOMAINS so future queries benefit.
12
+ */
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
14
+ import { resolve } from "node:path";
15
+ import { USER_HOME } from "../utils/paths.js";
16
+ import { mergeConceptDomains } from "./conceptRouter.js";
17
+ const VOCAB_FILE = resolve(USER_HOME, "learned-vocabulary.json");
18
+ // ─── Default domain mapping rules ─────────────────────────────────────────
19
+ // When we encounter an instanceOf label from Wikidata, map it to likely
20
+ // intent domains. This is the heuristic that turns Wikidata types into
21
+ // actionable routing information.
22
+ const INSTANCE_OF_TO_DOMAINS = {
23
+ // Software / services
24
+ "web server": ["service.status", "service.restart"],
25
+ "reverse proxy": ["service.status", "service.restart"],
26
+ "http server": ["service.status", "service.restart"],
27
+ "web framework": ["knowledge.lookup"],
28
+ "application server": ["service.status", "service.restart"],
29
+ "database management system": ["service.status", "service.restart"],
30
+ "relational database management system": ["service.status", "service.restart"],
31
+ "nosql database": ["service.status", "service.restart"],
32
+ "message broker": ["service.status", "service.restart"],
33
+ "caching system": ["service.status", "service.restart"],
34
+ "search engine software": ["service.status", "service.restart"],
35
+ // Container / orchestration
36
+ "container orchestrator": ["docker.ps", "docker.restart"],
37
+ "containerization": ["docker.ps", "docker.restart"],
38
+ "container platform": ["docker.ps", "docker.restart"],
39
+ // Programming languages
40
+ "programming language": ["knowledge.lookup"],
41
+ "scripting language": ["knowledge.lookup"],
42
+ "markup language": ["knowledge.lookup"],
43
+ // Operating systems
44
+ "operating system": ["system.kernel", "system.hostname"],
45
+ "linux distribution": ["system.kernel", "package.audit"],
46
+ // Networking
47
+ "network protocol": ["network.connections", "network.ports"],
48
+ "communication protocol": ["network.connections"],
49
+ // Version control
50
+ "version control system": ["git.status", "git.branch"],
51
+ // Package management
52
+ "package manager": ["package.audit"],
53
+ // General software
54
+ "free software": ["knowledge.lookup"],
55
+ "open-source software": ["knowledge.lookup"],
56
+ "software": ["knowledge.lookup"],
57
+ };
58
+ // ─── In-memory state ──────────────────────────────────────────────────────
59
+ let vocabulary = {
60
+ concepts: {},
61
+ domainMappings: {},
62
+ learnedAt: new Date().toISOString(),
63
+ };
64
+ let loaded = false;
65
+ // ─── Persistence ──────────────────────────────────────────────────────────
66
+ function readVocabFile() {
67
+ try {
68
+ if (existsSync(VOCAB_FILE)) {
69
+ const raw = JSON.parse(readFileSync(VOCAB_FILE, "utf-8"));
70
+ if (raw && typeof raw === "object" && raw.concepts && raw.domainMappings) {
71
+ return raw;
72
+ }
73
+ }
74
+ }
75
+ catch { /* corrupted file — start fresh */ }
76
+ return null;
77
+ }
78
+ function saveVocabFile() {
79
+ try {
80
+ mkdirSync(USER_HOME, { recursive: true });
81
+ vocabulary.learnedAt = new Date().toISOString();
82
+ writeFileSync(VOCAB_FILE, JSON.stringify(vocabulary, null, 2));
83
+ }
84
+ catch { /* best-effort persistence */ }
85
+ }
86
+ // ─── Core logic ───────────────────────────────────────────────────────────
87
+ /**
88
+ * Enrich vocabulary from a Wikidata entity.
89
+ *
90
+ * Called after every successful Wikidata lookup. Extracts:
91
+ * - instanceOf labels → domain mappings
92
+ * - aliases → concept synonyms
93
+ * - related concepts → concept router entries
94
+ */
95
+ export function enrichVocabularyFromWiki(entity) {
96
+ ensureLoaded();
97
+ const label = entity.label.toLowerCase();
98
+ const existingAliases = vocabulary.concepts[label] ?? [];
99
+ const newAliases = new Set(existingAliases);
100
+ // 1. Collect aliases as synonyms
101
+ for (const alias of entity.aliases) {
102
+ const lower = alias.toLowerCase();
103
+ if (lower !== label) {
104
+ newAliases.add(lower);
105
+ }
106
+ }
107
+ // 2. Add instanceOf labels as synonyms too (they describe what it is)
108
+ for (const inst of entity.instanceOf) {
109
+ const lower = inst.toLowerCase();
110
+ if (lower !== label) {
111
+ newAliases.add(lower);
112
+ }
113
+ }
114
+ vocabulary.concepts[label] = [...newAliases];
115
+ // 3. Map instanceOf labels to intent domains
116
+ const entityDomains = new Set();
117
+ for (const inst of entity.instanceOf) {
118
+ const lower = inst.toLowerCase();
119
+ // Check our built-in heuristic mapping
120
+ const mapped = INSTANCE_OF_TO_DOMAINS[lower];
121
+ if (mapped) {
122
+ for (const d of mapped)
123
+ entityDomains.add(d);
124
+ // Persist the mapping so it's available next time
125
+ if (!vocabulary.domainMappings[lower]) {
126
+ vocabulary.domainMappings[lower] = [...mapped];
127
+ }
128
+ }
129
+ // Also check previously learned domain mappings
130
+ const learned = vocabulary.domainMappings[lower];
131
+ if (learned) {
132
+ for (const d of learned)
133
+ entityDomains.add(d);
134
+ }
135
+ }
136
+ // 4. Merge into concept router: entity label + aliases → discovered domains
137
+ const domainsArray = [...entityDomains];
138
+ if (domainsArray.length > 0) {
139
+ // Map the entity label itself
140
+ mergeConceptDomains({ [label]: domainsArray });
141
+ // Map each alias to the same domains
142
+ for (const alias of newAliases) {
143
+ mergeConceptDomains({ [alias]: domainsArray });
144
+ }
145
+ }
146
+ // 5. Add related concepts to concept router with the same domains (lower weight)
147
+ if (domainsArray.length > 0) {
148
+ for (const rel of entity.related) {
149
+ const lower = rel.toLowerCase();
150
+ mergeConceptDomains({ [lower]: domainsArray });
151
+ }
152
+ }
153
+ // Persist
154
+ saveVocabFile();
155
+ }
156
+ /**
157
+ * Load learned vocabulary from disk and merge into the concept router.
158
+ *
159
+ * Should be called on startup so that previously learned vocabulary
160
+ * is available for intent routing from the first query.
161
+ */
162
+ export function loadLearnedVocabulary() {
163
+ const saved = readVocabFile();
164
+ if (saved) {
165
+ vocabulary = saved;
166
+ }
167
+ // Merge all learned concepts into the concept router
168
+ const toMerge = {};
169
+ for (const [label, aliases] of Object.entries(vocabulary.concepts)) {
170
+ // Determine domains for this label from domain mappings
171
+ const domains = resolveDomains(label, aliases);
172
+ if (domains.length > 0) {
173
+ toMerge[label] = domains;
174
+ for (const alias of aliases) {
175
+ toMerge[alias] = domains;
176
+ }
177
+ }
178
+ }
179
+ if (Object.keys(toMerge).length > 0) {
180
+ mergeConceptDomains(toMerge);
181
+ }
182
+ loaded = true;
183
+ }
184
+ /**
185
+ * Get the current enriched concepts map (merged vocabulary).
186
+ *
187
+ * Returns a combined view of hardcoded concepts and learned vocabulary.
188
+ */
189
+ export function getEnrichedConcepts() {
190
+ ensureLoaded();
191
+ return {
192
+ concepts: { ...vocabulary.concepts },
193
+ domainMappings: { ...vocabulary.domainMappings },
194
+ learnedAt: vocabulary.learnedAt,
195
+ };
196
+ }
197
+ // ─── Helpers ──────────────────────────────────────────────────────────────
198
+ function ensureLoaded() {
199
+ if (!loaded) {
200
+ loadLearnedVocabulary();
201
+ }
202
+ }
203
+ /**
204
+ * Resolve domains for a concept by checking its instanceOf-style labels
205
+ * against both the built-in heuristic map and learned domain mappings.
206
+ */
207
+ function resolveDomains(label, aliases) {
208
+ const domains = new Set();
209
+ // Check if the label itself is a known instanceOf category
210
+ const directMap = INSTANCE_OF_TO_DOMAINS[label] ?? vocabulary.domainMappings[label];
211
+ if (directMap) {
212
+ for (const d of directMap)
213
+ domains.add(d);
214
+ }
215
+ // Check aliases — some may be instanceOf labels
216
+ for (const alias of aliases) {
217
+ const aliasMap = INSTANCE_OF_TO_DOMAINS[alias] ?? vocabulary.domainMappings[alias];
218
+ if (aliasMap) {
219
+ for (const d of aliasMap)
220
+ domains.add(d);
221
+ }
222
+ }
223
+ return [...domains];
224
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Wikidata Knowledge Base.
3
+ *
4
+ * When the NLP parser encounters unknown nouns, queries Wikidata to:
5
+ * 1. Identify what the entity is (person, software, company, concept)
6
+ * 2. Pull key facts (description, instance-of, subclass-of)
7
+ * 3. Build semantic relationships (related topics, categories)
8
+ * 4. Cache results locally for offline use
9
+ *
10
+ * Uses the Wikidata REST API (no auth needed).
11
+ * Results are cached in ~/.notoken/wikidata-cache.json
12
+ */
13
+ export interface WikiEntity {
14
+ id: string;
15
+ label: string;
16
+ description: string;
17
+ aliases: string[];
18
+ instanceOf: string[];
19
+ subclassOf: string[];
20
+ related: string[];
21
+ url: string;
22
+ wikipedia?: string;
23
+ cachedAt: string;
24
+ }
25
+ export interface WikiLookupResult {
26
+ found: boolean;
27
+ entity?: WikiEntity;
28
+ suggestions?: Array<{
29
+ id: string;
30
+ label: string;
31
+ description: string;
32
+ }>;
33
+ error?: string;
34
+ }
35
+ /**
36
+ * Search Wikidata for an entity by name.
37
+ */
38
+ export declare function searchWikidata(query: string): Promise<WikiLookupResult>;
39
+ export declare function formatWikiEntity(entity: WikiEntity): string;
40
+ export declare function formatWikiSuggestions(suggestions: Array<{
41
+ id: string;
42
+ label: string;
43
+ description: string;
44
+ }>): string;
45
+ /**
46
+ * Look up unknown nouns via Wikidata.
47
+ * Called when the parser can't match an intent and has unknown words.
48
+ */
49
+ export declare function lookupUnknownNouns(words: string[]): Promise<WikiEntity[]>;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Wikidata Knowledge Base.
3
+ *
4
+ * When the NLP parser encounters unknown nouns, queries Wikidata to:
5
+ * 1. Identify what the entity is (person, software, company, concept)
6
+ * 2. Pull key facts (description, instance-of, subclass-of)
7
+ * 3. Build semantic relationships (related topics, categories)
8
+ * 4. Cache results locally for offline use
9
+ *
10
+ * Uses the Wikidata REST API (no auth needed).
11
+ * Results are cached in ~/.notoken/wikidata-cache.json
12
+ */
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
14
+ import { resolve } from "node:path";
15
+ import { USER_HOME } from "../utils/paths.js";
16
+ import { enrichVocabularyFromWiki } from "./vocabularyBuilder.js";
17
+ const CACHE_FILE = resolve(USER_HOME, "wikidata-cache.json");
18
+ const CACHE_TTL = 7 * 24 * 3600_000; // 7 days
19
+ const WIKIDATA_API = "https://www.wikidata.org/w/api.php";
20
+ const SEARCH_URL = "https://www.wikidata.org/w/api.php?action=wbsearchentities&format=json&language=en&limit=3&search=";
21
+ const c = {
22
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
23
+ green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m", magenta: "\x1b[35m",
24
+ };
25
+ let cache = {};
26
+ function loadCache() {
27
+ try {
28
+ if (existsSync(CACHE_FILE)) {
29
+ cache = JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
30
+ }
31
+ }
32
+ catch {
33
+ cache = {};
34
+ }
35
+ }
36
+ function saveCache() {
37
+ try {
38
+ mkdirSync(USER_HOME, { recursive: true });
39
+ // Prune expired entries
40
+ const now = Date.now();
41
+ for (const [key, entry] of Object.entries(cache)) {
42
+ if (now - entry.timestamp > CACHE_TTL)
43
+ delete cache[key];
44
+ }
45
+ writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
46
+ }
47
+ catch { }
48
+ }
49
+ function getCached(key) {
50
+ loadCache();
51
+ const entry = cache[key.toLowerCase()];
52
+ if (!entry)
53
+ return null;
54
+ if (Date.now() - entry.timestamp > CACHE_TTL)
55
+ return null;
56
+ return entry.entity;
57
+ }
58
+ function setCache(key, entity) {
59
+ cache[key.toLowerCase()] = { entity, timestamp: Date.now() };
60
+ saveCache();
61
+ }
62
+ // ─── API ───────────────────────────────────────────────────────────────────
63
+ async function fetchJson(url) {
64
+ try {
65
+ const response = await fetch(url, {
66
+ signal: AbortSignal.timeout(8000),
67
+ headers: { "User-Agent": "NoToken-CLI/1.0 (https://notoken.sh)" },
68
+ });
69
+ if (!response.ok)
70
+ return null;
71
+ return response.json();
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ /**
78
+ * Search Wikidata for an entity by name.
79
+ */
80
+ export async function searchWikidata(query) {
81
+ // Check cache first
82
+ const cached = getCached(query);
83
+ if (cached)
84
+ return { found: true, entity: cached };
85
+ // Search Wikidata
86
+ const searchData = await fetchJson(`${SEARCH_URL}${encodeURIComponent(query)}`);
87
+ if (!searchData?.search?.length) {
88
+ return { found: false, error: "No results found on Wikidata" };
89
+ }
90
+ const suggestions = searchData.search.map(s => ({
91
+ id: s.id, label: s.label, description: s.description,
92
+ }));
93
+ // Fetch full entity data for top result
94
+ const topId = searchData.search[0].id;
95
+ const entity = await fetchEntity(topId, searchData.search[0].label, searchData.search[0].description);
96
+ if (entity) {
97
+ setCache(query, entity);
98
+ // Enrich vocabulary from this lookup for future NLP matching
99
+ try {
100
+ enrichVocabularyFromWiki(entity);
101
+ }
102
+ catch { }
103
+ return { found: true, entity, suggestions };
104
+ }
105
+ return { found: false, suggestions };
106
+ }
107
+ /**
108
+ * Fetch full entity details from Wikidata by Q-number.
109
+ */
110
+ async function fetchEntity(qid, label, description) {
111
+ const url = `${WIKIDATA_API}?action=wbgetentities&format=json&ids=${qid}&props=labels|descriptions|aliases|claims|sitelinks&languages=en`;
112
+ const data = await fetchJson(url);
113
+ if (!data?.entities?.[qid])
114
+ return null;
115
+ const e = data.entities[qid];
116
+ const claims = e.claims ?? {};
117
+ // Extract instance-of (P31) and subclass-of (P279) labels
118
+ const instanceOf = await resolveClaimLabels(claims["P31"]);
119
+ const subclassOf = await resolveClaimLabels(claims["P279"]);
120
+ // Get related entities from "part of" (P361), "has use" (P366), "field of work" (P101)
121
+ const relatedClaims = [
122
+ ...(claims["P361"] ?? []), // part of
123
+ ...(claims["P366"] ?? []), // has use
124
+ ...(claims["P101"] ?? []), // field of work
125
+ ...(claims["P1535"] ?? []), // used by
126
+ ];
127
+ const related = await resolveClaimLabels(relatedClaims);
128
+ const aliases = (e.aliases?.en ?? []).map(a => a.value).slice(0, 10);
129
+ const wikipedia = e.sitelinks?.enwiki?.url;
130
+ return {
131
+ id: qid,
132
+ label: e.labels?.en?.value ?? label,
133
+ description: e.descriptions?.en?.value ?? description,
134
+ aliases,
135
+ instanceOf,
136
+ subclassOf,
137
+ related,
138
+ url: `https://www.wikidata.org/wiki/${qid}`,
139
+ wikipedia,
140
+ cachedAt: new Date().toISOString(),
141
+ };
142
+ }
143
+ async function resolveClaimLabels(claims) {
144
+ if (!claims?.length)
145
+ return [];
146
+ const qids = [];
147
+ for (const c of claims.slice(0, 8)) {
148
+ const val = c.mainsnak?.datavalue?.value;
149
+ if (typeof val === "object" && val && "id" in val && val.id) {
150
+ qids.push(val.id);
151
+ }
152
+ }
153
+ if (qids.length === 0)
154
+ return [];
155
+ // Batch fetch labels
156
+ const url = `${WIKIDATA_API}?action=wbgetentities&format=json&ids=${qids.join("|")}&props=labels&languages=en`;
157
+ const data = await fetchJson(url);
158
+ if (!data?.entities)
159
+ return [];
160
+ return qids
161
+ .map(q => data.entities?.[q]?.labels?.en?.value)
162
+ .filter((l) => !!l);
163
+ }
164
+ // ─── Formatting ────────────────────────────────────────────────────────────
165
+ export function formatWikiEntity(entity) {
166
+ const lines = [];
167
+ lines.push(`${c.bold}${c.cyan}${entity.label}${c.reset}`);
168
+ lines.push(`${c.dim}${entity.description}${c.reset}\n`);
169
+ if (entity.instanceOf.length > 0) {
170
+ lines.push(` ${c.bold}Type:${c.reset} ${entity.instanceOf.join(", ")}`);
171
+ }
172
+ if (entity.subclassOf.length > 0) {
173
+ lines.push(` ${c.bold}Category:${c.reset} ${entity.subclassOf.join(", ")}`);
174
+ }
175
+ if (entity.aliases.length > 0) {
176
+ lines.push(` ${c.bold}Also known as:${c.reset} ${entity.aliases.join(", ")}`);
177
+ }
178
+ if (entity.related.length > 0) {
179
+ lines.push(` ${c.bold}Related:${c.reset} ${entity.related.join(", ")}`);
180
+ }
181
+ lines.push("");
182
+ if (entity.wikipedia) {
183
+ lines.push(` ${c.dim}Wikipedia: ${entity.wikipedia}${c.reset}`);
184
+ }
185
+ lines.push(` ${c.dim}Wikidata: ${entity.url}${c.reset}`);
186
+ return lines.join("\n");
187
+ }
188
+ export function formatWikiSuggestions(suggestions) {
189
+ const lines = [];
190
+ lines.push(`${c.bold}Did you mean:${c.reset}\n`);
191
+ for (const s of suggestions) {
192
+ lines.push(` ${c.cyan}${s.label}${c.reset} — ${c.dim}${s.description}${c.reset}`);
193
+ }
194
+ return lines.join("\n");
195
+ }
196
+ // ─── Integration with NLP ──────────────────────────────────────────────────
197
+ /**
198
+ * Look up unknown nouns via Wikidata.
199
+ * Called when the parser can't match an intent and has unknown words.
200
+ */
201
+ export async function lookupUnknownNouns(words) {
202
+ const results = [];
203
+ // Filter to likely nouns (capitalized, or multi-char words not in common stop words)
204
+ const candidates = words.filter(w => w.length >= 3 &&
205
+ !STOP_WORDS.has(w.toLowerCase()));
206
+ for (const word of candidates.slice(0, 3)) {
207
+ const result = await searchWikidata(word);
208
+ if (result.found && result.entity) {
209
+ results.push(result.entity);
210
+ }
211
+ }
212
+ return results;
213
+ }
214
+ const STOP_WORDS = new Set([
215
+ "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
216
+ "have", "has", "had", "do", "does", "did", "will", "would", "shall",
217
+ "should", "may", "might", "must", "can", "could", "and", "but", "or",
218
+ "if", "then", "else", "when", "where", "how", "what", "which", "who",
219
+ "that", "this", "these", "those", "it", "its", "my", "your", "his",
220
+ "her", "our", "their", "not", "no", "yes", "all", "any", "each",
221
+ "every", "some", "many", "much", "more", "most", "very", "just",
222
+ "about", "above", "after", "again", "before", "below", "between",
223
+ "both", "down", "during", "for", "from", "here", "in", "into",
224
+ "of", "off", "on", "out", "over", "own", "same", "so", "than",
225
+ "then", "there", "through", "to", "too", "under", "until", "up",
226
+ "with", "check", "show", "get", "list", "find", "run", "start",
227
+ "stop", "restart", "install", "please", "help", "me", "tell",
228
+ ]);
@@ -1,2 +1,12 @@
1
1
  export declare function askForConfirmation(message: string): Promise<boolean>;
2
+ /**
3
+ * Require the user to type an exact phrase to confirm a dangerous action.
4
+ * Returns true only if the typed text matches exactly.
5
+ */
6
+ export declare function askForStrictConfirmation(message: string, requiredPhrase: string): Promise<boolean>;
7
+ /**
8
+ * Ask with extended responses: y/n plus control flow.
9
+ * Returns: "yes" | "no" | "all" | "stop"
10
+ */
11
+ export declare function askWithControl(message: string): Promise<"yes" | "no" | "all" | "stop">;
2
12
  export declare function askForChoice(message: string, choices: string[]): Promise<string | null>;
@@ -10,6 +10,45 @@ export async function askForConfirmation(message) {
10
10
  rl.close();
11
11
  }
12
12
  }
13
+ /**
14
+ * Require the user to type an exact phrase to confirm a dangerous action.
15
+ * Returns true only if the typed text matches exactly.
16
+ */
17
+ export async function askForStrictConfirmation(message, requiredPhrase) {
18
+ const rl = readline.createInterface({ input, output });
19
+ try {
20
+ const answer = await rl.question(`${message}\n Type "${requiredPhrase}" to confirm: `);
21
+ return answer.trim() === requiredPhrase;
22
+ }
23
+ finally {
24
+ rl.close();
25
+ }
26
+ }
27
+ /**
28
+ * Ask with extended responses: y/n plus control flow.
29
+ * Returns: "yes" | "no" | "all" | "stop"
30
+ */
31
+ export async function askWithControl(message) {
32
+ const rl = readline.createInterface({ input, output });
33
+ try {
34
+ const answer = await rl.question(`${message} [y/N/all/stop] `);
35
+ const trimmed = answer.trim().toLowerCase();
36
+ // Yes
37
+ if (/^y(es)?$/.test(trimmed))
38
+ return "yes";
39
+ // All / do everything / keep going / clean everything
40
+ if (/^(all|yes.?all|do.?all|everything|clean.?(all|everything)|keep.?going|do.?it|go.?ahead)$/.test(trimmed))
41
+ return "all";
42
+ // Stop / abort / quit / enough / don't / no more / stop right there
43
+ if (/^(stop|abort|quit|enough|no.?more|that.?s?.?enough|stop.?right.?there|cancel|done|don.?t|nah|exit)$/.test(trimmed))
44
+ return "stop";
45
+ // Default: no
46
+ return "no";
47
+ }
48
+ finally {
49
+ rl.close();
50
+ }
51
+ }
13
52
  export async function askForChoice(message, choices) {
14
53
  const rl = readline.createInterface({ input, output });
15
54
  try {
@@ -1,4 +1,4 @@
1
- import { getIntentDef } from "../utils/config.js";
1
+ import { getIntentDef, loadRules } from "../utils/config.js";
2
2
  export function validateIntent(intent) {
3
3
  const def = getIntentDef(intent.intent);
4
4
  if (!def)
@@ -9,13 +9,15 @@ export function validateIntent(intent) {
9
9
  errors.push(`Missing required field: ${fieldName}`);
10
10
  }
11
11
  }
12
- // Check allowlist if defined
12
+ // Check allowlist services from rules.json serviceAliases are always allowed
13
13
  if (def.allowlist && def.allowlist.length > 0) {
14
+ const rules = loadRules();
15
+ const knownServices = new Set([...def.allowlist, ...Object.keys(rules.serviceAliases)]);
14
16
  for (const [fieldName, fieldDef] of Object.entries(def.fields)) {
15
17
  if (fieldDef.type === "service" && intent.fields[fieldName]) {
16
18
  const value = intent.fields[fieldName];
17
- if (!def.allowlist.includes(value)) {
18
- errors.push(`${fieldName} "${value}" is not in the allowlist: ${def.allowlist.join(", ")}`);
19
+ if (!knownServices.has(value)) {
20
+ errors.push(`${fieldName} "${value}" is not in the allowlist: ${[...knownServices].join(", ")}`);
19
21
  }
20
22
  }
21
23
  }
@@ -0,0 +1,5 @@
1
+ export declare function loadAliases(): Record<string, string>;
2
+ export declare function resolveAlias(text: string): string;
3
+ export declare function saveAlias(name: string, command: string): void;
4
+ export declare function removeAlias(name: string): boolean;
5
+ export declare function listAliases(): Record<string, string>;
@@ -0,0 +1,39 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { USER_HOME } from "./paths.js";
4
+ const ALIASES_FILE = resolve(USER_HOME, "aliases.json");
5
+ function ensureDir() {
6
+ if (!existsSync(USER_HOME))
7
+ mkdirSync(USER_HOME, { recursive: true });
8
+ }
9
+ export function loadAliases() {
10
+ try {
11
+ const raw = readFileSync(ALIASES_FILE, "utf-8");
12
+ return JSON.parse(raw);
13
+ }
14
+ catch {
15
+ return {};
16
+ }
17
+ }
18
+ export function resolveAlias(text) {
19
+ const aliases = loadAliases();
20
+ return aliases[text] ?? text;
21
+ }
22
+ export function saveAlias(name, command) {
23
+ ensureDir();
24
+ const aliases = loadAliases();
25
+ aliases[name] = command;
26
+ writeFileSync(ALIASES_FILE, JSON.stringify(aliases, null, 2) + "\n", "utf-8");
27
+ }
28
+ export function removeAlias(name) {
29
+ const aliases = loadAliases();
30
+ if (!(name in aliases))
31
+ return false;
32
+ delete aliases[name];
33
+ ensureDir();
34
+ writeFileSync(ALIASES_FILE, JSON.stringify(aliases, null, 2) + "\n", "utf-8");
35
+ return true;
36
+ }
37
+ export function listAliases() {
38
+ return loadAliases();
39
+ }