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.
- package/config/chat-responses.json +767 -0
- package/config/concept-clusters.json +31 -0
- package/config/entities.json +93 -0
- package/config/image-prompts.json +20 -0
- package/config/intent-vectors.json +1 -0
- package/config/intents.json +4946 -83
- package/config/ollama-models.json +193 -0
- package/config/rules.json +32 -1
- package/dist/automation/discordPatchright.d.ts +35 -0
- package/dist/automation/discordPatchright.js +424 -0
- package/dist/automation/discordSetup.d.ts +31 -0
- package/dist/automation/discordSetup.js +338 -0
- package/dist/conversation/coreference.js +44 -4
- package/dist/conversation/pendingActions.d.ts +55 -0
- package/dist/conversation/pendingActions.js +127 -0
- package/dist/conversation/store.d.ts +72 -0
- package/dist/conversation/store.js +140 -1
- package/dist/conversation/topicTracker.d.ts +36 -0
- package/dist/conversation/topicTracker.js +141 -0
- package/dist/execution/ssh.d.ts +42 -1
- package/dist/execution/ssh.js +532 -3
- package/dist/handlers/executor.js +3981 -16
- package/dist/index.d.ts +25 -3
- package/dist/index.js +36 -2
- package/dist/nlp/batchParser.d.ts +30 -0
- package/dist/nlp/batchParser.js +77 -0
- package/dist/nlp/conceptExpansion.d.ts +54 -0
- package/dist/nlp/conceptExpansion.js +136 -0
- package/dist/nlp/conceptRouter.d.ts +49 -0
- package/dist/nlp/conceptRouter.js +302 -0
- package/dist/nlp/confidenceCalibrator.d.ts +62 -0
- package/dist/nlp/confidenceCalibrator.js +116 -0
- package/dist/nlp/correctionLearner.d.ts +45 -0
- package/dist/nlp/correctionLearner.js +207 -0
- package/dist/nlp/entitySpellCorrect.d.ts +35 -0
- package/dist/nlp/entitySpellCorrect.js +141 -0
- package/dist/nlp/knowledgeGraph.d.ts +70 -0
- package/dist/nlp/knowledgeGraph.js +380 -0
- package/dist/nlp/llmFallback.js +28 -1
- package/dist/nlp/multiClassifier.js +91 -6
- package/dist/nlp/multiIntent.d.ts +43 -0
- package/dist/nlp/multiIntent.js +154 -0
- package/dist/nlp/parseIntent.d.ts +6 -1
- package/dist/nlp/parseIntent.js +180 -5
- package/dist/nlp/ruleParser.js +315 -0
- package/dist/nlp/semanticSimilarity.d.ts +30 -0
- package/dist/nlp/semanticSimilarity.js +174 -0
- package/dist/nlp/vocabularyBuilder.d.ts +43 -0
- package/dist/nlp/vocabularyBuilder.js +224 -0
- package/dist/nlp/wikidata.d.ts +49 -0
- package/dist/nlp/wikidata.js +228 -0
- package/dist/policy/confirm.d.ts +10 -0
- package/dist/policy/confirm.js +39 -0
- package/dist/policy/safety.js +6 -4
- package/dist/utils/aliases.d.ts +5 -0
- package/dist/utils/aliases.js +39 -0
- package/dist/utils/analysis.js +71 -15
- package/dist/utils/browser.d.ts +64 -0
- package/dist/utils/browser.js +364 -0
- package/dist/utils/commandHistory.d.ts +20 -0
- package/dist/utils/commandHistory.js +108 -0
- package/dist/utils/completer.d.ts +17 -0
- package/dist/utils/completer.js +79 -0
- package/dist/utils/config.js +32 -2
- package/dist/utils/dbQuery.d.ts +25 -0
- package/dist/utils/dbQuery.js +248 -0
- package/dist/utils/discordDiag.d.ts +35 -0
- package/dist/utils/discordDiag.js +826 -0
- package/dist/utils/diskCleanup.d.ts +36 -0
- package/dist/utils/diskCleanup.js +775 -0
- package/dist/utils/entityResolver.d.ts +107 -0
- package/dist/utils/entityResolver.js +468 -0
- package/dist/utils/imageGen.d.ts +92 -0
- package/dist/utils/imageGen.js +2031 -0
- package/dist/utils/installTracker.d.ts +57 -0
- package/dist/utils/installTracker.js +160 -0
- package/dist/utils/multiExec.d.ts +21 -0
- package/dist/utils/multiExec.js +141 -0
- package/dist/utils/openclawDiag.d.ts +29 -0
- package/dist/utils/openclawDiag.js +1035 -0
- package/dist/utils/output.js +4 -0
- package/dist/utils/platform.js +2 -1
- package/dist/utils/progressReporter.d.ts +50 -0
- package/dist/utils/progressReporter.js +58 -0
- package/dist/utils/projectDetect.d.ts +44 -0
- package/dist/utils/projectDetect.js +319 -0
- package/dist/utils/projectScanner.d.ts +44 -0
- package/dist/utils/projectScanner.js +312 -0
- package/dist/utils/shellCompat.d.ts +78 -0
- package/dist/utils/shellCompat.js +186 -0
- package/dist/utils/smartArchive.d.ts +16 -0
- package/dist/utils/smartArchive.js +172 -0
- package/dist/utils/smartRetry.d.ts +26 -0
- package/dist/utils/smartRetry.js +114 -0
- package/dist/utils/updater.d.ts +1 -0
- package/dist/utils/updater.js +1 -1
- package/dist/utils/version.d.ts +20 -0
- package/dist/utils/version.js +212 -0
- 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
|
+
]);
|
package/dist/policy/confirm.d.ts
CHANGED
|
@@ -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>;
|
package/dist/policy/confirm.js
CHANGED
|
@@ -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 {
|
package/dist/policy/safety.js
CHANGED
|
@@ -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
|
|
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 (!
|
|
18
|
-
errors.push(`${fieldName} "${value}" is not in the allowlist: ${
|
|
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
|
+
}
|