notoken-core 1.6.0 → 1.8.1
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/ascii-art.json +12 -0
- package/config/chat-responses.json +1019 -0
- package/config/cheat-sheets.json +94 -0
- package/config/concept-clusters.json +31 -0
- package/config/daily-tips.json +105 -0
- package/config/entities.json +93 -0
- package/config/history-today.json +9762 -0
- package/config/image-prompts.json +20 -0
- package/config/intent-vectors.json +1 -0
- package/config/intents.json +5749 -85
- package/config/ollama-models.json +193 -0
- package/config/rules.json +32 -1
- package/config/startup-quotes.json +45 -0
- package/dist/automation/discordPatchright.d.ts +35 -0
- package/dist/automation/discordPatchright.js +437 -0
- package/dist/automation/discordSetup.d.ts +31 -0
- package/dist/automation/discordSetup.js +338 -0
- package/dist/automation/smAutomation.d.ts +82 -0
- package/dist/automation/smAutomation.js +448 -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 +538 -3
- package/dist/handlers/executor.d.ts +2 -0
- package/dist/handlers/executor.js +4669 -31
- package/dist/index.d.ts +39 -5
- package/dist/index.js +56 -4
- 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.d.ts +47 -0
- package/dist/nlp/llmFallback.js +175 -36
- package/dist/nlp/llmParser.d.ts +5 -1
- package/dist/nlp/llmParser.js +43 -24
- 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 +199 -6
- package/dist/nlp/ruleParser.js +348 -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/types/intent.d.ts +8 -0
- package/dist/types/intent.js +1 -0
- package/dist/utils/achievements.d.ts +38 -0
- package/dist/utils/achievements.js +126 -0
- 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/bookmarks.d.ts +13 -0
- package/dist/utils/bookmarks.js +51 -0
- 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/devTools.d.ts +35 -0
- package/dist/utils/devTools.js +95 -0
- package/dist/utils/discordDiag.d.ts +35 -0
- package/dist/utils/discordDiag.js +834 -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 +127 -0
- package/dist/utils/openclawDiag.js +1535 -0
- package/dist/utils/openclawLogParser.d.ts +65 -0
- package/dist/utils/openclawLogParser.js +168 -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/snippets.d.ts +13 -0
- package/dist/utils/snippets.js +53 -0
- package/dist/utils/stabilityMatrixManager.d.ts +80 -0
- package/dist/utils/stabilityMatrixManager.js +268 -0
- package/dist/utils/teachMode.d.ts +41 -0
- package/dist/utils/teachMode.js +100 -0
- package/dist/utils/timer.d.ts +22 -0
- package/dist/utils/timer.js +52 -0
- package/dist/utils/updater.d.ts +1 -0
- package/dist/utils/updater.js +1 -1
- package/dist/utils/userContext.d.ts +57 -0
- package/dist/utils/userContext.js +133 -0
- package/dist/utils/version.d.ts +20 -0
- package/dist/utils/version.js +212 -0
- package/package.json +6 -3
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Graph — persistent entity-relationship store.
|
|
3
|
+
* Persists to ~/.notoken/knowledge-graph.json.
|
|
4
|
+
* Auto-populates from entities.json, rules.json, and running system state.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { loadEntities } from "../utils/entityResolver.js";
|
|
10
|
+
// ─── Persistence ────────────────────────────────────────────────────────────
|
|
11
|
+
const GRAPH_DIR = join(homedir(), ".notoken");
|
|
12
|
+
const GRAPH_PATH = join(GRAPH_DIR, "knowledge-graph.json");
|
|
13
|
+
let _graph = null;
|
|
14
|
+
export function loadKnowledgeGraph() {
|
|
15
|
+
if (_graph)
|
|
16
|
+
return _graph;
|
|
17
|
+
if (existsSync(GRAPH_PATH)) {
|
|
18
|
+
try {
|
|
19
|
+
_graph = JSON.parse(readFileSync(GRAPH_PATH, "utf-8"));
|
|
20
|
+
return _graph;
|
|
21
|
+
}
|
|
22
|
+
catch { /* rebuild */ }
|
|
23
|
+
}
|
|
24
|
+
_graph = buildGraph();
|
|
25
|
+
saveKnowledgeGraph(_graph);
|
|
26
|
+
return _graph;
|
|
27
|
+
}
|
|
28
|
+
export function saveKnowledgeGraph(graph) {
|
|
29
|
+
const g = graph ?? _graph;
|
|
30
|
+
if (!g)
|
|
31
|
+
return;
|
|
32
|
+
if (!existsSync(GRAPH_DIR))
|
|
33
|
+
mkdirSync(GRAPH_DIR, { recursive: true });
|
|
34
|
+
writeFileSync(GRAPH_PATH, JSON.stringify(g, null, 2) + "\n");
|
|
35
|
+
_graph = g;
|
|
36
|
+
}
|
|
37
|
+
// ─── Mutation ───────────────────────────────────────────────────────────────
|
|
38
|
+
export function addEntity(name, type, aliases = [], properties = {}) {
|
|
39
|
+
const g = loadKnowledgeGraph();
|
|
40
|
+
const entity = { name, type, aliases, properties };
|
|
41
|
+
g.entities[name] = entity;
|
|
42
|
+
return entity;
|
|
43
|
+
}
|
|
44
|
+
export function addRelation(from, to, relation, properties) {
|
|
45
|
+
const g = loadKnowledgeGraph();
|
|
46
|
+
const rel = { from, to, relation, properties };
|
|
47
|
+
if (!g.relations.some((r) => r.from === from && r.to === to && r.relation === relation))
|
|
48
|
+
g.relations.push(rel);
|
|
49
|
+
return rel;
|
|
50
|
+
}
|
|
51
|
+
// ─── Queries ────────────────────────────────────────────────────────────────
|
|
52
|
+
/** Find an entity by exact name, alias, or prefix (min 3 chars). */
|
|
53
|
+
export function getEntity(name) {
|
|
54
|
+
const g = loadKnowledgeGraph();
|
|
55
|
+
const lower = name.toLowerCase();
|
|
56
|
+
if (g.entities[name])
|
|
57
|
+
return g.entities[name];
|
|
58
|
+
for (const [key, ent] of Object.entries(g.entities)) {
|
|
59
|
+
if (key.toLowerCase() === lower)
|
|
60
|
+
return ent;
|
|
61
|
+
}
|
|
62
|
+
for (const ent of Object.values(g.entities)) {
|
|
63
|
+
if (ent.aliases.some((a) => a.toLowerCase() === lower))
|
|
64
|
+
return ent;
|
|
65
|
+
}
|
|
66
|
+
if (lower.length >= 3) {
|
|
67
|
+
for (const ent of Object.values(g.entities)) {
|
|
68
|
+
if (ent.name.toLowerCase().startsWith(lower))
|
|
69
|
+
return ent;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
/** Find all entities related to entityName, optionally filtered by relation type. */
|
|
75
|
+
export function getRelated(entityName, relation) {
|
|
76
|
+
const g = loadKnowledgeGraph();
|
|
77
|
+
const results = [];
|
|
78
|
+
for (const rel of g.relations) {
|
|
79
|
+
if (relation && rel.relation !== relation)
|
|
80
|
+
continue;
|
|
81
|
+
if (rel.from === entityName && g.entities[rel.to])
|
|
82
|
+
results.push({ entity: g.entities[rel.to], relation: rel, direction: "outgoing" });
|
|
83
|
+
else if (rel.to === entityName && g.entities[rel.from])
|
|
84
|
+
results.push({ entity: g.entities[rel.from], relation: rel, direction: "incoming" });
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve "it", "the server", "that service" using graph context + recent entities.
|
|
90
|
+
* Returns the best candidate. Use resolveCandidates() for all scored options.
|
|
91
|
+
*/
|
|
92
|
+
export function resolveReference(text, recentEntities) {
|
|
93
|
+
const candidates = resolveCandidates(text, recentEntities);
|
|
94
|
+
return candidates.length > 0 ? candidates[0].entity : null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get all resolution candidates, scored and ranked.
|
|
98
|
+
* Scores: recent entity = 1.0 - (0.1 * position), type match = +0.2, relation match = +0.15
|
|
99
|
+
*/
|
|
100
|
+
export function resolveCandidates(text, recentEntities) {
|
|
101
|
+
const lower = text.toLowerCase().trim();
|
|
102
|
+
const g = loadKnowledgeGraph();
|
|
103
|
+
const candidates = [];
|
|
104
|
+
// Direct match — highest confidence
|
|
105
|
+
const direct = getEntity(lower);
|
|
106
|
+
if (direct)
|
|
107
|
+
return [{ entity: direct, score: 1.0, reason: "direct match" }];
|
|
108
|
+
// Determine what type we're looking for
|
|
109
|
+
let wantType = null;
|
|
110
|
+
const typed = lower.match(/^(?:the|that|this)\s+(server|service|database|container|port|package|llm|channel|path|user)$/);
|
|
111
|
+
if (typed)
|
|
112
|
+
wantType = typed[1];
|
|
113
|
+
// For "it"/"that"/"this" — prefer services and containers (actionable things)
|
|
114
|
+
const isAnaphoric = /^(it|that|this)$/.test(lower);
|
|
115
|
+
if (isAnaphoric)
|
|
116
|
+
wantType = null; // consider all types
|
|
117
|
+
// Score recent entities
|
|
118
|
+
for (let i = 0; i < recentEntities.length; i++) {
|
|
119
|
+
const ent = g.entities[recentEntities[i]];
|
|
120
|
+
if (!ent)
|
|
121
|
+
continue;
|
|
122
|
+
let score = 1.0 - (i * 0.15); // Recency: most recent = 1.0, then 0.85, 0.7, ...
|
|
123
|
+
let reason = `recent entity (#${i + 1})`;
|
|
124
|
+
// Type match bonus
|
|
125
|
+
if (wantType && ent.type === wantType) {
|
|
126
|
+
score += 0.2;
|
|
127
|
+
reason += `, type match (${wantType})`;
|
|
128
|
+
}
|
|
129
|
+
// For "it" — prefer services/containers over servers/ports
|
|
130
|
+
if (isAnaphoric) {
|
|
131
|
+
if (ent.type === "service" || ent.type === "container") {
|
|
132
|
+
score += 0.15;
|
|
133
|
+
reason += ", actionable type";
|
|
134
|
+
}
|
|
135
|
+
else if (ent.type === "server") {
|
|
136
|
+
score += 0.05;
|
|
137
|
+
reason += ", server";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Relationship bonus — if this entity is related to other recent entities
|
|
141
|
+
for (const other of recentEntities.slice(0, 3)) {
|
|
142
|
+
if (other === recentEntities[i])
|
|
143
|
+
continue;
|
|
144
|
+
const rels = g.relations.filter(r => (r.from === ent.name && r.to === other) || (r.to === ent.name && r.from === other));
|
|
145
|
+
if (rels.length > 0) {
|
|
146
|
+
score += 0.1;
|
|
147
|
+
reason += `, related to ${other}`;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
candidates.push({ entity: ent, score: Math.min(score, 1.0), reason });
|
|
152
|
+
}
|
|
153
|
+
// If no recent entities matched and we want a type, search all entities of that type
|
|
154
|
+
if (candidates.length === 0 && wantType) {
|
|
155
|
+
for (const ent of Object.values(g.entities)) {
|
|
156
|
+
if (ent.type === wantType) {
|
|
157
|
+
candidates.push({ entity: ent, score: 0.3, reason: `type match (${wantType}), no recency` });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return candidates.sort((a, b) => b.score - a.score);
|
|
162
|
+
}
|
|
163
|
+
/** Use relationships to infer intent context from tokens. Resolves anaphora and finds target/location. */
|
|
164
|
+
export function inferIntent(tokens, recentEntities = []) {
|
|
165
|
+
const resolvedEntities = [];
|
|
166
|
+
const impliedRelations = [];
|
|
167
|
+
let target, location;
|
|
168
|
+
for (const token of tokens) {
|
|
169
|
+
const resolved = resolveReference(token, recentEntities) ?? getEntity(token);
|
|
170
|
+
if (!resolved)
|
|
171
|
+
continue;
|
|
172
|
+
resolvedEntities.push({ token, entity: resolved });
|
|
173
|
+
if (!target && (resolved.type === "service" || resolved.type === "container" || resolved.type === "package"))
|
|
174
|
+
target = resolved;
|
|
175
|
+
else if (!location && resolved.type === "server")
|
|
176
|
+
location = resolved;
|
|
177
|
+
}
|
|
178
|
+
if (target && location) {
|
|
179
|
+
const g = loadKnowledgeGraph();
|
|
180
|
+
for (const rel of g.relations) {
|
|
181
|
+
if ((rel.from === target.name && rel.to === location.name) || (rel.from === location.name && rel.to === target.name)) {
|
|
182
|
+
impliedRelations.push(rel);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { resolvedEntities, impliedRelations, target, location };
|
|
187
|
+
}
|
|
188
|
+
/** General-purpose query — find entities by type and/or property filter. */
|
|
189
|
+
export function queryGraph(filter) {
|
|
190
|
+
const g = loadKnowledgeGraph();
|
|
191
|
+
return Object.values(g.entities).filter((ent) => {
|
|
192
|
+
if (filter.type && ent.type !== filter.type)
|
|
193
|
+
return false;
|
|
194
|
+
if (filter.property !== undefined) {
|
|
195
|
+
const val = ent.properties[filter.property];
|
|
196
|
+
if (val === undefined)
|
|
197
|
+
return false;
|
|
198
|
+
if (filter.value !== undefined && val !== filter.value)
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// ─── Graph Builder ──────────────────────────────────────────────────────────
|
|
205
|
+
function loadRulesConfig() {
|
|
206
|
+
for (const p of [
|
|
207
|
+
join(import.meta.url.replace("file://", "").replace(/\/[^/]+\/[^/]+$/, ""), "../config/rules.json"),
|
|
208
|
+
join(process.cwd(), "packages/core/config/rules.json"),
|
|
209
|
+
join(process.cwd(), "config/rules.json"),
|
|
210
|
+
]) {
|
|
211
|
+
if (existsSync(p)) {
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(readFileSync(p, "utf-8")).serviceAliases ?? {};
|
|
214
|
+
}
|
|
215
|
+
catch { /* skip */ }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {};
|
|
219
|
+
}
|
|
220
|
+
function populateFromEntities(g, ents) {
|
|
221
|
+
for (const [name, srv] of Object.entries(ents.servers)) {
|
|
222
|
+
g.entities[name] = { name, type: "server", aliases: srv.aliases ?? [],
|
|
223
|
+
properties: { host: srv.host, ...(srv.user ? { user: srv.user } : {}), ...(srv.description ? { description: srv.description } : {}) } };
|
|
224
|
+
if (srv.host)
|
|
225
|
+
g.relations.push({ from: name, to: `ip:${srv.host}`, relation: "has_ip" });
|
|
226
|
+
}
|
|
227
|
+
for (const [name, db] of Object.entries(ents.databases)) {
|
|
228
|
+
g.entities[name] = { name, type: "database", aliases: db.aliases ?? [],
|
|
229
|
+
properties: { dbType: db.type, host: db.host, dbName: db.name, ...(db.port ? { port: db.port } : {}), ...(db.user ? { user: db.user } : {}) } };
|
|
230
|
+
if (db.port)
|
|
231
|
+
g.relations.push({ from: name, to: `port:${db.port}`, relation: "has_port" });
|
|
232
|
+
}
|
|
233
|
+
for (const [id, inst] of Object.entries(ents.installations ?? {})) {
|
|
234
|
+
const props = { service: inst.service, environment: inst.environment };
|
|
235
|
+
if (inst.path)
|
|
236
|
+
props.path = inst.path;
|
|
237
|
+
if (inst.version)
|
|
238
|
+
props.version = inst.version;
|
|
239
|
+
if (inst.port)
|
|
240
|
+
props.port = inst.port;
|
|
241
|
+
if (inst.model)
|
|
242
|
+
props.model = inst.model;
|
|
243
|
+
if (inst.status)
|
|
244
|
+
props.status = inst.status;
|
|
245
|
+
g.entities[id] = { name: id, type: "service", aliases: inst.aliases ?? [], properties: props };
|
|
246
|
+
if (inst.port)
|
|
247
|
+
g.relations.push({ from: id, to: `port:${inst.port}`, relation: "has_port" });
|
|
248
|
+
if (inst.model) {
|
|
249
|
+
const llmName = `llm:${inst.model}`;
|
|
250
|
+
if (!g.entities[llmName])
|
|
251
|
+
g.entities[llmName] = { name: llmName, type: "llm", aliases: [inst.model.split("/").pop()], properties: { model: inst.model } };
|
|
252
|
+
g.relations.push({ from: id, to: llmName, relation: "uses" });
|
|
253
|
+
}
|
|
254
|
+
const serverName = Object.keys(ents.servers).find((s) => inst.environment === s || inst.aliases.some((a) => a.includes(s)));
|
|
255
|
+
if (serverName)
|
|
256
|
+
g.relations.push({ from: id, to: serverName, relation: "runs_on" });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function populateFromRules(g, serviceAliases) {
|
|
260
|
+
for (const [svc, aliases] of Object.entries(serviceAliases)) {
|
|
261
|
+
if (!g.entities[svc]) {
|
|
262
|
+
g.entities[svc] = { name: svc, type: "service", aliases, properties: {} };
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
for (const a of aliases) {
|
|
266
|
+
if (!g.entities[svc].aliases.includes(a))
|
|
267
|
+
g.entities[svc].aliases.push(a);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function populateFromSystem(g) {
|
|
273
|
+
let execSync;
|
|
274
|
+
try {
|
|
275
|
+
execSync = require("node:child_process").execSync;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const tryExec = (cmd) => { try {
|
|
281
|
+
return execSync(cmd, { timeout: 5_000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return "";
|
|
285
|
+
} };
|
|
286
|
+
// Docker containers
|
|
287
|
+
const dockerPs = tryExec("docker ps --format '{{.Names}}\\t{{.Image}}\\t{{.Ports}}' 2>/dev/null");
|
|
288
|
+
for (const line of (dockerPs || "").split("\n").filter(Boolean)) {
|
|
289
|
+
const [name, image, ports] = line.split("\t");
|
|
290
|
+
if (name)
|
|
291
|
+
g.entities[`container:${name}`] = { name: `container:${name}`, type: "container", aliases: [name], properties: { image: image ?? "", ports: ports ?? "" } };
|
|
292
|
+
}
|
|
293
|
+
// Listening ports
|
|
294
|
+
const ssOut = tryExec("ss -tlnp 2>/dev/null | tail -n +2");
|
|
295
|
+
for (const line of (ssOut || "").split("\n").filter(Boolean)) {
|
|
296
|
+
const portM = line.match(/:(\d+)\s/), procM = line.match(/users:\(\("([^"]+)"/);
|
|
297
|
+
if (!portM)
|
|
298
|
+
continue;
|
|
299
|
+
const key = `port:${portM[1]}`, proc = procM?.[1] ?? "unknown";
|
|
300
|
+
if (!g.entities[key])
|
|
301
|
+
g.entities[key] = { name: key, type: "port", aliases: [`port ${portM[1]}`], properties: { port: Number(portM[1]), process: proc } };
|
|
302
|
+
if (proc !== "unknown" && g.entities[proc])
|
|
303
|
+
g.relations.push({ from: proc, to: key, relation: "has_port" });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** Build the full knowledge graph from all sources. */
|
|
307
|
+
export function buildGraph() {
|
|
308
|
+
const g = { entities: {}, relations: [], lastBuilt: new Date().toISOString() };
|
|
309
|
+
populateFromEntities(g, loadEntities(true));
|
|
310
|
+
populateFromRules(g, loadRulesConfig());
|
|
311
|
+
populateFromSystem(g);
|
|
312
|
+
return g;
|
|
313
|
+
}
|
|
314
|
+
/** Force a rebuild of the graph from all sources and persist. */
|
|
315
|
+
export function rebuildGraph() {
|
|
316
|
+
_graph = buildGraph();
|
|
317
|
+
saveKnowledgeGraph(_graph);
|
|
318
|
+
return _graph;
|
|
319
|
+
}
|
|
320
|
+
// ─── Learning from execution ──────────────────────────────────────────────
|
|
321
|
+
/**
|
|
322
|
+
* Learn from a successfully executed intent.
|
|
323
|
+
* Grows the graph over time by recording:
|
|
324
|
+
* - Services that were restarted/checked (type: service)
|
|
325
|
+
* - Servers that were targeted (type: server)
|
|
326
|
+
* - Relationships discovered (service → runs_on → server)
|
|
327
|
+
* - New entities mentioned by the user
|
|
328
|
+
*
|
|
329
|
+
* Called after every successful execution. Persists periodically.
|
|
330
|
+
*/
|
|
331
|
+
let _learnCount = 0;
|
|
332
|
+
export function learnFromExecution(intent, fields, rawText) {
|
|
333
|
+
const g = loadKnowledgeGraph();
|
|
334
|
+
const service = fields.service;
|
|
335
|
+
const environment = fields.environment;
|
|
336
|
+
const path = fields.path;
|
|
337
|
+
const target = fields.target;
|
|
338
|
+
// Learn services
|
|
339
|
+
if (service && !g.entities[service]) {
|
|
340
|
+
const type = intent.startsWith("docker.") ? "container" : "service";
|
|
341
|
+
g.entities[service] = { name: service, type, aliases: [], properties: {} };
|
|
342
|
+
}
|
|
343
|
+
// Learn servers/environments
|
|
344
|
+
if (environment && environment !== "local" && environment !== "localhost" && environment !== "dev") {
|
|
345
|
+
if (!g.entities[environment]) {
|
|
346
|
+
g.entities[environment] = { name: environment, type: "server", aliases: [], properties: {} };
|
|
347
|
+
}
|
|
348
|
+
// Learn relationship: service runs_on environment
|
|
349
|
+
if (service) {
|
|
350
|
+
const rel = { from: service, to: environment, relation: "runs_on" };
|
|
351
|
+
if (!g.relations.some(r => r.from === rel.from && r.to === rel.to && r.relation === rel.relation)) {
|
|
352
|
+
g.relations.push(rel);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Learn paths
|
|
357
|
+
if (path && path !== "." && !g.entities[`path:${path}`]) {
|
|
358
|
+
g.entities[`path:${path}`] = { name: `path:${path}`, type: "path", aliases: [path], properties: { path } };
|
|
359
|
+
}
|
|
360
|
+
// Learn targets (from disk.scan, file operations, etc.)
|
|
361
|
+
if (target && !g.entities[target]) {
|
|
362
|
+
g.entities[target] = { name: target, type: "service", aliases: [], properties: {} };
|
|
363
|
+
}
|
|
364
|
+
// Learn from specific intents
|
|
365
|
+
if (intent === "entity.define") {
|
|
366
|
+
// User taught us a new entity — already handled by entityResolver
|
|
367
|
+
// but mark the graph as needing a rebuild next time
|
|
368
|
+
g.lastBuilt = undefined;
|
|
369
|
+
}
|
|
370
|
+
// Persist every 5 learn calls (not every single one — too much I/O)
|
|
371
|
+
_learnCount++;
|
|
372
|
+
if (_learnCount % 5 === 0) {
|
|
373
|
+
saveKnowledgeGraph(g);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** Flush any pending graph changes to disk. */
|
|
377
|
+
export function flushGraph() {
|
|
378
|
+
if (_graph)
|
|
379
|
+
saveKnowledgeGraph(_graph);
|
|
380
|
+
}
|
|
@@ -22,8 +22,27 @@ export interface LLMFallbackResult {
|
|
|
22
22
|
intent?: string;
|
|
23
23
|
command?: string;
|
|
24
24
|
}>;
|
|
25
|
+
/** Raw shell commands to run if no intent matches */
|
|
26
|
+
shellCommands?: string[];
|
|
27
|
+
/** Questions to ask the user for clarification */
|
|
25
28
|
missingInfo?: string[];
|
|
29
|
+
/** Commands to run first to gather info before answering (multi-turn) */
|
|
30
|
+
gatherCommands?: Array<{
|
|
31
|
+
command: string;
|
|
32
|
+
purpose: string;
|
|
33
|
+
}>;
|
|
34
|
+
/** Whether the LLM needs more info from command outputs before it can answer */
|
|
35
|
+
needsMoreInfo?: boolean;
|
|
26
36
|
}
|
|
37
|
+
/** Add a turn to the LLM conversation for multi-turn context */
|
|
38
|
+
export declare function addLLMContext(role: "user" | "assistant", content: string): void;
|
|
39
|
+
/** Clear LLM conversation when topic changes */
|
|
40
|
+
export declare function clearLLMContext(): void;
|
|
41
|
+
/** Get conversation for context in multi-turn */
|
|
42
|
+
export declare function getLLMContext(): Array<{
|
|
43
|
+
role: "user" | "assistant";
|
|
44
|
+
content: string;
|
|
45
|
+
}>;
|
|
27
46
|
/**
|
|
28
47
|
* Check if any LLM is configured.
|
|
29
48
|
*/
|
|
@@ -47,6 +66,34 @@ export declare function llmFallback(rawText: string, context: {
|
|
|
47
66
|
type: string;
|
|
48
67
|
}>;
|
|
49
68
|
uncertainTokens?: string[];
|
|
69
|
+
nearMisses?: Array<{
|
|
70
|
+
intent: string;
|
|
71
|
+
score: number;
|
|
72
|
+
source: string;
|
|
73
|
+
}>;
|
|
74
|
+
}): Promise<LLMFallbackResult | null>;
|
|
75
|
+
/**
|
|
76
|
+
* Multi-turn LLM disambiguation.
|
|
77
|
+
*
|
|
78
|
+
* 1. Ask the LLM what to do
|
|
79
|
+
* 2. If it needs more info → run gatherCommands → feed results back
|
|
80
|
+
* 3. Repeat up to maxTurns times
|
|
81
|
+
* 4. Return the final result
|
|
82
|
+
*/
|
|
83
|
+
export declare function llmMultiTurn(rawText: string, context: {
|
|
84
|
+
recentIntents?: string[];
|
|
85
|
+
knownEntities?: Array<{
|
|
86
|
+
entity: string;
|
|
87
|
+
type: string;
|
|
88
|
+
}>;
|
|
89
|
+
nearMisses?: Array<{
|
|
90
|
+
intent: string;
|
|
91
|
+
score: number;
|
|
92
|
+
source: string;
|
|
93
|
+
}>;
|
|
94
|
+
}, options?: {
|
|
95
|
+
maxTurns?: number;
|
|
96
|
+
onProgress?: (msg: string) => void;
|
|
50
97
|
}): Promise<LLMFallbackResult | null>;
|
|
51
98
|
/**
|
|
52
99
|
* Check if Ollama is installed (not just running).
|