@kernel.chat/kbot 2.7.0 → 2.9.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 (57) hide show
  1. package/dist/agent-protocol.d.ts +97 -0
  2. package/dist/agent-protocol.d.ts.map +1 -0
  3. package/dist/agent-protocol.js +618 -0
  4. package/dist/agent-protocol.js.map +1 -0
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +25 -1
  7. package/dist/agent.js.map +1 -1
  8. package/dist/architect.d.ts +44 -0
  9. package/dist/architect.d.ts.map +1 -0
  10. package/dist/architect.js +403 -0
  11. package/dist/architect.js.map +1 -0
  12. package/dist/cli.js +210 -2
  13. package/dist/cli.js.map +1 -1
  14. package/dist/confidence.d.ts +102 -0
  15. package/dist/confidence.d.ts.map +1 -0
  16. package/dist/confidence.js +693 -0
  17. package/dist/confidence.js.map +1 -0
  18. package/dist/graph-memory.d.ts +98 -0
  19. package/dist/graph-memory.d.ts.map +1 -0
  20. package/dist/graph-memory.js +926 -0
  21. package/dist/graph-memory.js.map +1 -0
  22. package/dist/ide/acp-server.js +2 -2
  23. package/dist/ide/acp-server.js.map +1 -1
  24. package/dist/intentionality.d.ts +139 -0
  25. package/dist/intentionality.d.ts.map +1 -0
  26. package/dist/intentionality.js +1092 -0
  27. package/dist/intentionality.js.map +1 -0
  28. package/dist/lsp-client.d.ts +167 -0
  29. package/dist/lsp-client.d.ts.map +1 -0
  30. package/dist/lsp-client.js +679 -0
  31. package/dist/lsp-client.js.map +1 -0
  32. package/dist/mcp-plugins.d.ts +62 -0
  33. package/dist/mcp-plugins.d.ts.map +1 -0
  34. package/dist/mcp-plugins.js +551 -0
  35. package/dist/mcp-plugins.js.map +1 -0
  36. package/dist/reasoning.d.ts +100 -0
  37. package/dist/reasoning.d.ts.map +1 -0
  38. package/dist/reasoning.js +1292 -0
  39. package/dist/reasoning.js.map +1 -0
  40. package/dist/temporal.d.ts +133 -0
  41. package/dist/temporal.d.ts.map +1 -0
  42. package/dist/temporal.js +778 -0
  43. package/dist/temporal.js.map +1 -0
  44. package/dist/tools/e2b-sandbox.d.ts +2 -0
  45. package/dist/tools/e2b-sandbox.d.ts.map +1 -0
  46. package/dist/tools/e2b-sandbox.js +460 -0
  47. package/dist/tools/e2b-sandbox.js.map +1 -0
  48. package/dist/tools/index.d.ts.map +1 -1
  49. package/dist/tools/index.js +19 -1
  50. package/dist/tools/index.js.map +1 -1
  51. package/dist/tools/lsp-tools.d.ts +2 -0
  52. package/dist/tools/lsp-tools.d.ts.map +1 -0
  53. package/dist/tools/lsp-tools.js +268 -0
  54. package/dist/tools/lsp-tools.js.map +1 -0
  55. package/dist/ui.js +1 -1
  56. package/dist/ui.js.map +1 -1
  57. package/package.json +2 -2
@@ -0,0 +1,926 @@
1
+ // K:BOT Graph Memory — Knowledge graph for entity-relationship reasoning
2
+ //
3
+ // Extends flat memory (memory.ts, memory-tools.ts) with a graph structure:
4
+ // entities connected by typed relationships so the agent can reason about
5
+ // connections between concepts, files, people, bugs, decisions, and patterns.
6
+ //
7
+ // Stored at ~/.kbot/graph.json as a single file.
8
+ // No external dependencies — fuzzy search uses simple heuristics.
9
+ import { homedir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
12
+ import { randomBytes } from 'node:crypto';
13
+ import { registerTool } from './tools/index.js';
14
+ // ── Constants ──
15
+ const KBOT_DIR = join(homedir(), '.kbot');
16
+ const GRAPH_FILE = join(KBOT_DIR, 'graph.json');
17
+ const MAX_NODES = 1000;
18
+ const MAX_EDGES = 5000;
19
+ const ID_LENGTH = 8;
20
+ // ── Module state ──
21
+ let graph = {
22
+ nodes: new Map(),
23
+ edges: [],
24
+ };
25
+ // ── ID generation ──
26
+ function generateId() {
27
+ return randomBytes(ID_LENGTH / 2).toString('hex');
28
+ }
29
+ // ── Persistence ──
30
+ function ensureDir() {
31
+ if (!existsSync(KBOT_DIR)) {
32
+ mkdirSync(KBOT_DIR, { recursive: true });
33
+ }
34
+ }
35
+ /** Load graph from ~/.kbot/graph.json */
36
+ export function load() {
37
+ ensureDir();
38
+ if (!existsSync(GRAPH_FILE)) {
39
+ graph = { nodes: new Map(), edges: [] };
40
+ return;
41
+ }
42
+ try {
43
+ const raw = JSON.parse(readFileSync(GRAPH_FILE, 'utf-8'));
44
+ graph = {
45
+ nodes: new Map(raw.nodes || []),
46
+ edges: raw.edges || [],
47
+ };
48
+ }
49
+ catch {
50
+ graph = { nodes: new Map(), edges: [] };
51
+ }
52
+ }
53
+ /** Save graph to ~/.kbot/graph.json */
54
+ export function save() {
55
+ ensureDir();
56
+ const data = {
57
+ nodes: Array.from(graph.nodes.entries()),
58
+ edges: graph.edges,
59
+ };
60
+ writeFileSync(GRAPH_FILE, JSON.stringify(data, null, 2), 'utf-8');
61
+ }
62
+ // ── Fuzzy search ──
63
+ /**
64
+ * Compute a similarity score between two strings (0-1).
65
+ * Combines substring match and character-level edit distance.
66
+ * No external dependencies.
67
+ */
68
+ function fuzzyScore(query, target) {
69
+ const q = query.toLowerCase();
70
+ const t = target.toLowerCase();
71
+ // Exact match
72
+ if (q === t)
73
+ return 1.0;
74
+ // Substring containment — strong signal
75
+ if (t.includes(q)) {
76
+ return 0.7 + 0.3 * (q.length / t.length);
77
+ }
78
+ if (q.includes(t)) {
79
+ return 0.6 + 0.2 * (t.length / q.length);
80
+ }
81
+ // Simple Levenshtein-like distance ratio
82
+ const distance = levenshtein(q, t);
83
+ const maxLen = Math.max(q.length, t.length);
84
+ if (maxLen === 0)
85
+ return 1.0;
86
+ const ratio = 1 - distance / maxLen;
87
+ return Math.max(0, ratio);
88
+ }
89
+ /** Levenshtein edit distance — standard DP implementation */
90
+ function levenshtein(a, b) {
91
+ const m = a.length;
92
+ const n = b.length;
93
+ // Optimization: use single-row DP
94
+ let prev = new Array(n + 1);
95
+ let curr = new Array(n + 1);
96
+ for (let j = 0; j <= n; j++)
97
+ prev[j] = j;
98
+ for (let i = 1; i <= m; i++) {
99
+ curr[0] = i;
100
+ for (let j = 1; j <= n; j++) {
101
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
102
+ curr[j] = Math.min(prev[j] + 1, // deletion
103
+ curr[j - 1] + 1, // insertion
104
+ prev[j - 1] + cost // substitution
105
+ );
106
+ }
107
+ ;
108
+ [prev, curr] = [curr, prev];
109
+ }
110
+ return prev[n];
111
+ }
112
+ // ── Core API ──
113
+ /** Add a node to the graph. Returns the new node. */
114
+ export function addNode(type, name, properties = {}) {
115
+ // Check for existing node with same type and name to avoid duplicates
116
+ for (const existing of Array.from(graph.nodes.values())) {
117
+ if (existing.type === type && existing.name.toLowerCase() === name.toLowerCase()) {
118
+ existing.lastAccessed = new Date().toISOString();
119
+ existing.accessCount++;
120
+ // Merge properties
121
+ Object.assign(existing.properties, properties);
122
+ return existing;
123
+ }
124
+ }
125
+ // Enforce max nodes — prune lowest-weight if at capacity
126
+ if (graph.nodes.size >= MAX_NODES) {
127
+ pruneToCapacity();
128
+ }
129
+ const now = new Date().toISOString();
130
+ const node = {
131
+ id: generateId(),
132
+ type,
133
+ name,
134
+ properties,
135
+ created: now,
136
+ lastAccessed: now,
137
+ accessCount: 1,
138
+ };
139
+ graph.nodes.set(node.id, node);
140
+ return node;
141
+ }
142
+ /** Add an edge between two nodes. Returns true if successful. */
143
+ export function addEdge(sourceId, targetId, relation, weight = 0.5) {
144
+ if (!graph.nodes.has(sourceId) || !graph.nodes.has(targetId)) {
145
+ return false;
146
+ }
147
+ // Clamp weight to [0, 1]
148
+ const clampedWeight = Math.max(0, Math.min(1, weight));
149
+ // Check for existing edge with same source, target, and relation
150
+ const existing = graph.edges.find(e => e.source === sourceId && e.target === targetId && e.relation === relation);
151
+ if (existing) {
152
+ // Strengthen existing edge
153
+ existing.weight = Math.min(1, existing.weight + 0.1);
154
+ return true;
155
+ }
156
+ // Enforce max edges — prune lowest-weight if at capacity
157
+ if (graph.edges.length >= MAX_EDGES) {
158
+ graph.edges.sort((a, b) => a.weight - b.weight);
159
+ graph.edges = graph.edges.slice(Math.floor(MAX_EDGES * 0.1)); // Drop bottom 10%
160
+ }
161
+ graph.edges.push({
162
+ source: sourceId,
163
+ target: targetId,
164
+ relation,
165
+ weight: clampedWeight,
166
+ created: new Date().toISOString(),
167
+ });
168
+ return true;
169
+ }
170
+ /** Fuzzy search nodes by name. Returns matches sorted by relevance. */
171
+ export function findNode(query) {
172
+ const threshold = 0.3;
173
+ const scored = [];
174
+ for (const node of Array.from(graph.nodes.values())) {
175
+ // Score against name and property values
176
+ let bestScore = fuzzyScore(query, node.name);
177
+ for (const val of Object.values(node.properties)) {
178
+ const propScore = fuzzyScore(query, val) * 0.8; // Properties slightly less relevant
179
+ bestScore = Math.max(bestScore, propScore);
180
+ }
181
+ if (bestScore >= threshold) {
182
+ scored.push({ node, score: bestScore });
183
+ }
184
+ }
185
+ // Sort by score descending, then by access count as tiebreaker
186
+ scored.sort((a, b) => b.score - a.score || b.node.accessCount - a.node.accessCount);
187
+ // Touch accessed nodes
188
+ const now = new Date().toISOString();
189
+ for (const { node } of scored.slice(0, 20)) {
190
+ node.lastAccessed = now;
191
+ node.accessCount++;
192
+ }
193
+ return scored.slice(0, 20).map(s => s.node);
194
+ }
195
+ /** Get connected nodes up to a given depth (default 1, max 3). */
196
+ export function getNeighbors(nodeId, depth = 1) {
197
+ const clampedDepth = Math.max(1, Math.min(3, depth));
198
+ const visitedIds = new Set([nodeId]);
199
+ const resultEdges = [];
200
+ let frontier = new Set([nodeId]);
201
+ for (let d = 0; d < clampedDepth; d++) {
202
+ const nextFrontier = new Set();
203
+ for (const edge of graph.edges) {
204
+ if (frontier.has(edge.source) && !visitedIds.has(edge.target)) {
205
+ visitedIds.add(edge.target);
206
+ nextFrontier.add(edge.target);
207
+ resultEdges.push(edge);
208
+ }
209
+ if (frontier.has(edge.target) && !visitedIds.has(edge.source)) {
210
+ visitedIds.add(edge.source);
211
+ nextFrontier.add(edge.source);
212
+ resultEdges.push(edge);
213
+ }
214
+ }
215
+ frontier = nextFrontier;
216
+ if (frontier.size === 0)
217
+ break;
218
+ }
219
+ const resultNodes = [];
220
+ for (const id of Array.from(visitedIds)) {
221
+ const node = graph.nodes.get(id);
222
+ if (node)
223
+ resultNodes.push(node);
224
+ }
225
+ return { nodes: resultNodes, edges: resultEdges };
226
+ }
227
+ /** Get a subgraph containing the given node IDs and all edges between them. */
228
+ export function getSubgraph(nodeIds) {
229
+ const idSet = new Set(nodeIds);
230
+ const nodes = [];
231
+ for (const id of Array.from(idSet)) {
232
+ const node = graph.nodes.get(id);
233
+ if (node)
234
+ nodes.push(node);
235
+ }
236
+ const edges = graph.edges.filter(e => idSet.has(e.source) && idSet.has(e.target));
237
+ return { nodes, edges };
238
+ }
239
+ /** BFS shortest path between two nodes. Returns node IDs in order, or empty array if no path. */
240
+ export function shortestPath(fromId, toId) {
241
+ if (!graph.nodes.has(fromId) || !graph.nodes.has(toId))
242
+ return [];
243
+ if (fromId === toId)
244
+ return [fromId];
245
+ // Build adjacency list (undirected)
246
+ const adj = new Map();
247
+ for (const nodeKey of Array.from(graph.nodes.keys())) {
248
+ adj.set(nodeKey, []);
249
+ }
250
+ for (const edge of graph.edges) {
251
+ const srcList = adj.get(edge.source);
252
+ if (srcList && srcList.indexOf(edge.target) === -1)
253
+ srcList.push(edge.target);
254
+ const tgtList = adj.get(edge.target);
255
+ if (tgtList && tgtList.indexOf(edge.source) === -1)
256
+ tgtList.push(edge.source);
257
+ }
258
+ // BFS
259
+ const visited = new Set();
260
+ visited.add(fromId);
261
+ const parent = new Map();
262
+ const queue = [fromId];
263
+ while (queue.length > 0) {
264
+ const current = queue.shift();
265
+ if (current === toId) {
266
+ // Reconstruct path
267
+ const path = [toId];
268
+ let step = toId;
269
+ while (parent.has(step)) {
270
+ step = parent.get(step);
271
+ path.unshift(step);
272
+ }
273
+ return path;
274
+ }
275
+ const neighbors = adj.get(current);
276
+ if (!neighbors)
277
+ continue;
278
+ for (const neighbor of neighbors) {
279
+ if (!visited.has(neighbor)) {
280
+ visited.add(neighbor);
281
+ parent.set(neighbor, current);
282
+ queue.push(neighbor);
283
+ }
284
+ }
285
+ }
286
+ return []; // No path found
287
+ }
288
+ /** Get all edges of a given relation type. */
289
+ export function queryRelation(relation) {
290
+ const lowerRelation = relation.toLowerCase();
291
+ return graph.edges.filter(e => e.relation.toLowerCase() === lowerRelation);
292
+ }
293
+ /** Reduce weight of nodes unaccessed for more than `days` days. */
294
+ export function decayUnused(days) {
295
+ const cutoff = new Date();
296
+ cutoff.setDate(cutoff.getDate() - days);
297
+ const cutoffISO = cutoff.toISOString();
298
+ let decayed = 0;
299
+ for (const node of Array.from(graph.nodes.values())) {
300
+ if (node.lastAccessed < cutoffISO) {
301
+ // Decay edges connected to this node
302
+ for (const edge of graph.edges) {
303
+ if (edge.source === node.id || edge.target === node.id) {
304
+ edge.weight = Math.max(0, edge.weight - 0.1);
305
+ }
306
+ }
307
+ decayed++;
308
+ }
309
+ }
310
+ return { decayed };
311
+ }
312
+ /** Remove nodes with accessCount below a threshold, and their dangling edges. */
313
+ export function prune(minWeight) {
314
+ const edgesBefore = graph.edges.length;
315
+ // Remove low-weight edges
316
+ graph.edges = graph.edges.filter(e => e.weight >= minWeight);
317
+ // Find nodes that have no edges and low access count
318
+ const connectedIds = new Set();
319
+ for (const edge of graph.edges) {
320
+ connectedIds.add(edge.source);
321
+ connectedIds.add(edge.target);
322
+ }
323
+ let removedNodes = 0;
324
+ for (const [id, node] of Array.from(graph.nodes.entries())) {
325
+ // Remove disconnected nodes with weight proxy (accessCount as standin) below threshold
326
+ // A node's effective weight: accessCount normalized, capped at 1
327
+ const effectiveWeight = Math.min(1, node.accessCount / 10);
328
+ if (!connectedIds.has(id) && effectiveWeight < minWeight) {
329
+ graph.nodes.delete(id);
330
+ removedNodes++;
331
+ }
332
+ }
333
+ return {
334
+ removedNodes,
335
+ removedEdges: edgesBefore - graph.edges.length,
336
+ };
337
+ }
338
+ /**
339
+ * Serialize relevant subgraph into a compact, readable format for LLM context injection.
340
+ *
341
+ * Format:
342
+ * [file:auth.ts] --uses--> [entity:JWT] --implements--> [decision:use RS256]
343
+ * [person:Isaac] --authored--> [file:agent.ts]
344
+ */
345
+ export function toContext(maxTokens = 2000) {
346
+ if (graph.nodes.size === 0)
347
+ return '[No graph memory]';
348
+ // Prioritize recently accessed, high-access-count nodes
349
+ const sortedNodes = Array.from(graph.nodes.values()).sort((a, b) => {
350
+ // Primary: lastAccessed descending
351
+ const timeDiff = b.lastAccessed.localeCompare(a.lastAccessed);
352
+ if (timeDiff !== 0)
353
+ return timeDiff;
354
+ // Secondary: accessCount descending
355
+ return b.accessCount - a.accessCount;
356
+ });
357
+ // Build lines from edges, referencing the most relevant nodes
358
+ const relevantIds = new Set(sortedNodes.slice(0, 100).map(n => n.id));
359
+ const lines = [];
360
+ const usedNodeIds = new Set();
361
+ // Format edges between relevant nodes
362
+ for (const edge of graph.edges) {
363
+ if (!relevantIds.has(edge.source) && !relevantIds.has(edge.target))
364
+ continue;
365
+ const sourceNode = graph.nodes.get(edge.source);
366
+ const targetNode = graph.nodes.get(edge.target);
367
+ if (!sourceNode || !targetNode)
368
+ continue;
369
+ const line = `[${sourceNode.type}:${sourceNode.name}] --${edge.relation}--> [${targetNode.type}:${targetNode.name}]`;
370
+ lines.push(line);
371
+ usedNodeIds.add(edge.source);
372
+ usedNodeIds.add(edge.target);
373
+ }
374
+ // Add isolated relevant nodes not in any edge
375
+ for (const node of sortedNodes.slice(0, 50)) {
376
+ if (!usedNodeIds.has(node.id)) {
377
+ const props = Object.entries(node.properties)
378
+ .map(([k, v]) => `${k}=${v}`)
379
+ .join(', ');
380
+ const propStr = props ? ` (${props})` : '';
381
+ lines.push(`[${node.type}:${node.name}]${propStr}`);
382
+ usedNodeIds.add(node.id);
383
+ }
384
+ }
385
+ // Truncate to approximate token budget (rough: 4 chars ~ 1 token)
386
+ const charLimit = maxTokens * 4;
387
+ const result = [];
388
+ let charCount = 0;
389
+ for (const line of lines) {
390
+ if (charCount + line.length > charLimit)
391
+ break;
392
+ result.push(line);
393
+ charCount += line.length + 1;
394
+ }
395
+ if (result.length === 0)
396
+ return '[No graph memory]';
397
+ const header = `[Graph Memory — ${graph.nodes.size} nodes, ${graph.edges.length} edges]`;
398
+ return `${header}\n${result.join('\n')}`;
399
+ }
400
+ // ── Capacity management ──
401
+ function pruneToCapacity() {
402
+ // Remove the least-accessed, oldest nodes to get back under MAX_NODES
403
+ const sorted = Array.from(graph.nodes.values()).sort((a, b) => {
404
+ // Sort ascending by access count, then by lastAccessed
405
+ if (a.accessCount !== b.accessCount)
406
+ return a.accessCount - b.accessCount;
407
+ return a.lastAccessed.localeCompare(b.lastAccessed);
408
+ });
409
+ const toRemove = sorted.slice(0, Math.floor(MAX_NODES * 0.1)); // Remove bottom 10%
410
+ for (const node of toRemove) {
411
+ graph.nodes.delete(node.id);
412
+ // Remove orphaned edges
413
+ graph.edges = graph.edges.filter(e => e.source !== node.id && e.target !== node.id);
414
+ }
415
+ }
416
+ // ── Auto-extraction ──
417
+ /** File path pattern: Unix and Windows paths */
418
+ const FILE_PATH_RE = /(?:\/[\w./-]+\.[\w]+|[A-Z]:\\[\w.\\-]+\.[\w]+)/g;
419
+ /** GitHub issue/PR pattern: org/repo#123 or #123 */
420
+ const GITHUB_REF_RE = /(?:[\w-]+\/[\w-]+)?#(\d+)/g;
421
+ /** Function/class name pattern: common code identifiers */
422
+ const IDENTIFIER_RE = /\b(?:function|class|const|let|var|def|fn)\s+([A-Za-z_]\w{2,})/g;
423
+ /** Decision pattern: "let's use X", "we chose X", "decided to X", "going with X" */
424
+ const DECISION_RE = /(?:let'?s?\s+use|we\s+chose|decided?\s+(?:to|on)|going\s+with|switched?\s+to)\s+([^\n,.;]+)/gi;
425
+ /** Pattern detection: "always do X", "never Y", "when X then Y" */
426
+ const PATTERN_RE = /(?:always\s+|never\s+|when\s+\S+.*?\s+then\s+)([^\n,.;]+)/gi;
427
+ /**
428
+ * Extract entities from a user message and agent response using simple heuristics.
429
+ * Returns the newly created nodes.
430
+ */
431
+ export function extractEntities(userMessage, agentResponse) {
432
+ const combined = `${userMessage}\n${agentResponse}`;
433
+ const newNodes = [];
434
+ // File paths → 'file' nodes
435
+ const filePaths = new Set();
436
+ let match;
437
+ FILE_PATH_RE.lastIndex = 0;
438
+ while ((match = FILE_PATH_RE.exec(combined)) !== null) {
439
+ filePaths.add(match[0]);
440
+ }
441
+ for (const fp of Array.from(filePaths)) {
442
+ const name = fp.split('/').pop() || fp;
443
+ const node = addNode('file', name, { path: fp });
444
+ newNodes.push(node);
445
+ }
446
+ // GitHub issues/PRs → 'bug' nodes
447
+ const ghRefs = new Set();
448
+ GITHUB_REF_RE.lastIndex = 0;
449
+ while ((match = GITHUB_REF_RE.exec(combined)) !== null) {
450
+ ghRefs.add(match[0]);
451
+ }
452
+ for (const ref of Array.from(ghRefs)) {
453
+ const node = addNode('bug', ref, { reference: ref });
454
+ newNodes.push(node);
455
+ }
456
+ // Function/class names → 'entity' nodes
457
+ const identifiers = new Set();
458
+ IDENTIFIER_RE.lastIndex = 0;
459
+ while ((match = IDENTIFIER_RE.exec(combined)) !== null) {
460
+ // Skip very short or very common names
461
+ const name = match[1];
462
+ if (name.length > 2 && !['the', 'get', 'set', 'new', 'var', 'let'].includes(name.toLowerCase())) {
463
+ identifiers.add(name);
464
+ }
465
+ }
466
+ for (const ident of Array.from(identifiers)) {
467
+ const node = addNode('entity', ident, { kind: 'code-identifier' });
468
+ newNodes.push(node);
469
+ }
470
+ // Decisions → 'decision' nodes
471
+ DECISION_RE.lastIndex = 0;
472
+ while ((match = DECISION_RE.exec(combined)) !== null) {
473
+ const decision = match[1].trim();
474
+ if (decision.length > 3 && decision.length < 200) {
475
+ const node = addNode('decision', decision, { source: 'conversation' });
476
+ newNodes.push(node);
477
+ }
478
+ }
479
+ // Patterns → 'pattern' nodes
480
+ PATTERN_RE.lastIndex = 0;
481
+ while ((match = PATTERN_RE.exec(combined)) !== null) {
482
+ const pattern = match[1].trim();
483
+ if (pattern.length > 5 && pattern.length < 200) {
484
+ const node = addNode('pattern', pattern, { source: 'conversation' });
485
+ newNodes.push(node);
486
+ }
487
+ }
488
+ return newNodes;
489
+ }
490
+ /**
491
+ * Auto-create edges between a node and other nodes that were extracted
492
+ * from the same conversation turn. Nodes sharing a conversation turn
493
+ * are assumed to be related.
494
+ */
495
+ export function autoConnect(nodeId) {
496
+ const node = graph.nodes.get(nodeId);
497
+ if (!node)
498
+ return 0;
499
+ // Find nodes created or accessed at the same time (within 2 seconds)
500
+ const nodeTime = new Date(node.lastAccessed).getTime();
501
+ let connected = 0;
502
+ for (const other of Array.from(graph.nodes.values())) {
503
+ if (other.id === nodeId)
504
+ continue;
505
+ const otherTime = new Date(other.lastAccessed).getTime();
506
+ if (Math.abs(nodeTime - otherTime) < 2000) {
507
+ // Determine relation type based on node types
508
+ const relation = inferRelation(node, other);
509
+ if (addEdge(nodeId, other.id, relation, 0.3)) {
510
+ connected++;
511
+ }
512
+ }
513
+ }
514
+ return connected;
515
+ }
516
+ /** Infer a sensible relation type between two nodes based on their types. */
517
+ function inferRelation(a, b) {
518
+ const pair = `${a.type}:${b.type}`;
519
+ switch (pair) {
520
+ case 'person:file': return 'authored';
521
+ case 'file:person': return 'authored_by';
522
+ case 'file:entity': return 'contains';
523
+ case 'entity:file': return 'defined_in';
524
+ case 'file:bug': return 'affected_by';
525
+ case 'bug:file': return 'affects';
526
+ case 'decision:entity': return 'chose';
527
+ case 'entity:decision': return 'chosen_by';
528
+ case 'decision:file': return 'applies_to';
529
+ case 'file:decision': return 'governed_by';
530
+ case 'pattern:entity': return 'applies_to';
531
+ case 'entity:pattern': return 'follows';
532
+ case 'bug:entity': return 'involves';
533
+ case 'entity:bug': return 'involved_in';
534
+ case 'file:file': return 'related_to';
535
+ case 'entity:entity': return 'related_to';
536
+ case 'concept:entity': return 'describes';
537
+ case 'entity:concept': return 'described_by';
538
+ default: return 'related_to';
539
+ }
540
+ }
541
+ // ── Import from flat memory ──
542
+ /**
543
+ * Bootstrap graph from existing flat memory files (from memory-tools.ts).
544
+ * Reads JSON memory files in ~/.kbot/memory/{category}/ and converts them
545
+ * into graph nodes and edges.
546
+ */
547
+ export function importFromMemory(memoryDir) {
548
+ const baseDir = memoryDir || join(homedir(), '.kbot', 'memory');
549
+ if (!existsSync(baseDir))
550
+ return { imported: 0, edges: 0 };
551
+ const categories = ['fact', 'preference', 'pattern', 'solution'];
552
+ const importedNodes = [];
553
+ let edgeCount = 0;
554
+ for (const category of categories) {
555
+ const catDir = join(baseDir, category);
556
+ if (!existsSync(catDir))
557
+ continue;
558
+ let files;
559
+ try {
560
+ files = readdirSync(catDir).filter(f => f.endsWith('.json'));
561
+ }
562
+ catch {
563
+ continue;
564
+ }
565
+ for (const file of files) {
566
+ try {
567
+ const raw = JSON.parse(readFileSync(join(catDir, file), 'utf-8'));
568
+ const nodeType = categoryToNodeType(category);
569
+ const node = addNode(nodeType, raw.key || file.replace('.json', ''), {
570
+ content: typeof raw.content === 'string' ? raw.content.slice(0, 500) : '',
571
+ category,
572
+ originalCreated: raw.created_at || '',
573
+ });
574
+ importedNodes.push(node);
575
+ }
576
+ catch {
577
+ // Skip malformed files
578
+ }
579
+ }
580
+ }
581
+ // Also check for context.md — extract any top-level sections as concept nodes
582
+ const contextFile = join(baseDir, 'context.md');
583
+ if (existsSync(contextFile)) {
584
+ try {
585
+ const content = readFileSync(contextFile, 'utf-8');
586
+ const sections = content.split(/^## /m).filter(s => s.trim());
587
+ for (const section of sections.slice(0, 50)) {
588
+ const firstLine = section.split('\n')[0].trim();
589
+ if (firstLine && firstLine.length > 2) {
590
+ const node = addNode('concept', firstLine, {
591
+ source: 'context.md',
592
+ excerpt: section.slice(0, 300),
593
+ });
594
+ importedNodes.push(node);
595
+ }
596
+ }
597
+ }
598
+ catch {
599
+ // Skip if context.md is unreadable
600
+ }
601
+ }
602
+ // Auto-connect imported nodes that share content keywords
603
+ for (let i = 0; i < importedNodes.length; i++) {
604
+ for (let j = i + 1; j < importedNodes.length; j++) {
605
+ const a = importedNodes[i];
606
+ const b = importedNodes[j];
607
+ const similarity = fuzzyScore(a.name, b.name);
608
+ if (similarity > 0.5) {
609
+ addEdge(a.id, b.id, 'related_to', similarity * 0.5);
610
+ edgeCount++;
611
+ }
612
+ }
613
+ }
614
+ return { imported: importedNodes.length, edges: edgeCount };
615
+ }
616
+ function categoryToNodeType(category) {
617
+ switch (category) {
618
+ case 'pattern': return 'pattern';
619
+ case 'solution': return 'entity';
620
+ case 'preference': return 'decision';
621
+ case 'fact':
622
+ default: return 'concept';
623
+ }
624
+ }
625
+ // ── Graph statistics ──
626
+ export function getStats() {
627
+ const nodesByType = {};
628
+ for (const node of Array.from(graph.nodes.values())) {
629
+ nodesByType[node.type] = (nodesByType[node.type] || 0) + 1;
630
+ }
631
+ const topNodes = Array.from(graph.nodes.values())
632
+ .sort((a, b) => b.accessCount - a.accessCount)
633
+ .slice(0, 10)
634
+ .map(n => ({ name: n.name, type: n.type, accessCount: n.accessCount }));
635
+ return {
636
+ nodeCount: graph.nodes.size,
637
+ edgeCount: graph.edges.length,
638
+ nodesByType,
639
+ topNodes,
640
+ };
641
+ }
642
+ /** Get a node by ID (for internal use and testing). */
643
+ export function getNode(id) {
644
+ return graph.nodes.get(id);
645
+ }
646
+ /** Get the raw graph (for testing / advanced usage). */
647
+ export function getGraph() {
648
+ return graph;
649
+ }
650
+ // ── Tool registration ──
651
+ export function registerGraphMemoryTools() {
652
+ // ── graph_add ──
653
+ registerTool({
654
+ name: 'graph_add',
655
+ description: 'Add an entity to the knowledge graph with optional edges to existing nodes. Use this to build connections between concepts, files, people, bugs, decisions, and patterns the user works with.',
656
+ parameters: {
657
+ type: {
658
+ type: 'string',
659
+ description: 'Node type: "entity", "concept", "file", "person", "bug", "decision", or "pattern"',
660
+ required: true,
661
+ },
662
+ name: {
663
+ type: 'string',
664
+ description: 'Name of the entity (e.g. "auth.ts", "Isaac", "JWT", "use RS256")',
665
+ required: true,
666
+ },
667
+ properties: {
668
+ type: 'string',
669
+ description: 'JSON object of key-value properties (e.g. \'{"path":"/src/auth.ts","language":"typescript"}\')',
670
+ },
671
+ connect_to: {
672
+ type: 'string',
673
+ description: 'Comma-separated node IDs to connect this node to',
674
+ },
675
+ relation: {
676
+ type: 'string',
677
+ description: 'Relation type for edges created via connect_to (default: "related_to")',
678
+ },
679
+ },
680
+ tier: 'free',
681
+ timeout: 10_000,
682
+ async execute(args) {
683
+ const type = String(args.type || '').trim();
684
+ const name = String(args.name || '').trim();
685
+ const validTypes = ['entity', 'concept', 'file', 'person', 'bug', 'decision', 'pattern'];
686
+ if (!validTypes.includes(type)) {
687
+ return `Error: type must be one of: ${validTypes.join(', ')}`;
688
+ }
689
+ if (!name)
690
+ return 'Error: name is required.';
691
+ let properties = {};
692
+ if (args.properties) {
693
+ try {
694
+ properties = JSON.parse(String(args.properties));
695
+ }
696
+ catch {
697
+ return 'Error: properties must be valid JSON.';
698
+ }
699
+ }
700
+ load();
701
+ const node = addNode(type, name, properties);
702
+ // Handle optional edge connections
703
+ let edgesCreated = 0;
704
+ if (args.connect_to) {
705
+ const targetIds = String(args.connect_to).split(',').map(s => s.trim()).filter(Boolean);
706
+ const relation = String(args.relation || 'related_to').trim();
707
+ for (const targetId of targetIds) {
708
+ if (addEdge(node.id, targetId, relation)) {
709
+ edgesCreated++;
710
+ }
711
+ }
712
+ }
713
+ save();
714
+ const edgeNote = edgesCreated > 0 ? ` + ${edgesCreated} edge(s) created` : '';
715
+ return `Added [${type}:${name}] (id: ${node.id})${edgeNote}. Graph: ${graph.nodes.size} nodes, ${graph.edges.length} edges.`;
716
+ },
717
+ });
718
+ // ── graph_query ──
719
+ registerTool({
720
+ name: 'graph_query',
721
+ description: 'Search the knowledge graph for entities by name or find all edges of a given relation type. Use this to recall known entities and their connections.',
722
+ parameters: {
723
+ query: {
724
+ type: 'string',
725
+ description: 'Search term to fuzzy-match against node names',
726
+ },
727
+ relation: {
728
+ type: 'string',
729
+ description: 'Find all edges of this relation type (e.g. "uses", "authored_by", "fixes")',
730
+ },
731
+ },
732
+ tier: 'free',
733
+ timeout: 10_000,
734
+ async execute(args) {
735
+ load();
736
+ const results = [];
737
+ if (args.query) {
738
+ const query = String(args.query).trim();
739
+ const matches = findNode(query);
740
+ if (matches.length === 0) {
741
+ results.push(`No nodes found matching "${query}".`);
742
+ }
743
+ else {
744
+ results.push(`Found ${matches.length} node(s) matching "${query}":`);
745
+ for (const node of matches) {
746
+ const props = Object.entries(node.properties)
747
+ .map(([k, v]) => `${k}=${v}`)
748
+ .join(', ');
749
+ const propStr = props ? ` (${props})` : '';
750
+ results.push(` [${node.type}:${node.name}] id=${node.id} accessed=${node.accessCount}x${propStr}`);
751
+ }
752
+ }
753
+ }
754
+ if (args.relation) {
755
+ const relation = String(args.relation).trim();
756
+ const edges = queryRelation(relation);
757
+ if (edges.length === 0) {
758
+ results.push(`No edges found with relation "${relation}".`);
759
+ }
760
+ else {
761
+ results.push(`\nFound ${edges.length} edge(s) with relation "${relation}":`);
762
+ for (const edge of edges.slice(0, 20)) {
763
+ const src = graph.nodes.get(edge.source);
764
+ const tgt = graph.nodes.get(edge.target);
765
+ if (src && tgt) {
766
+ results.push(` [${src.type}:${src.name}] --${edge.relation}(${edge.weight.toFixed(2)})--> [${tgt.type}:${tgt.name}]`);
767
+ }
768
+ }
769
+ if (edges.length > 20) {
770
+ results.push(` ... and ${edges.length - 20} more`);
771
+ }
772
+ }
773
+ }
774
+ if (!args.query && !args.relation) {
775
+ // Show graph stats
776
+ const stats = getStats();
777
+ results.push(`Graph: ${stats.nodeCount} nodes, ${stats.edgeCount} edges`);
778
+ results.push('Node types:');
779
+ for (const [type, count] of Object.entries(stats.nodesByType)) {
780
+ results.push(` ${type}: ${count}`);
781
+ }
782
+ if (stats.topNodes.length > 0) {
783
+ results.push('Most accessed:');
784
+ for (const n of stats.topNodes) {
785
+ results.push(` [${n.type}:${n.name}] — ${n.accessCount}x`);
786
+ }
787
+ }
788
+ }
789
+ save();
790
+ return results.join('\n');
791
+ },
792
+ });
793
+ // ── graph_connect ──
794
+ registerTool({
795
+ name: 'graph_connect',
796
+ description: 'Create a typed relationship edge between two nodes in the knowledge graph. Use this to link entities that are related (e.g. a file uses a library, a person authored a module).',
797
+ parameters: {
798
+ source: {
799
+ type: 'string',
800
+ description: 'Source node ID',
801
+ required: true,
802
+ },
803
+ target: {
804
+ type: 'string',
805
+ description: 'Target node ID',
806
+ required: true,
807
+ },
808
+ relation: {
809
+ type: 'string',
810
+ description: 'Relation type (e.g. "uses", "depends_on", "authored_by", "fixes", "related_to", "implements")',
811
+ required: true,
812
+ },
813
+ weight: {
814
+ type: 'string',
815
+ description: 'Edge weight 0-1 (default: 0.5). Higher = stronger relationship.',
816
+ },
817
+ },
818
+ tier: 'free',
819
+ timeout: 10_000,
820
+ async execute(args) {
821
+ const sourceId = String(args.source || '').trim();
822
+ const targetId = String(args.target || '').trim();
823
+ const relation = String(args.relation || '').trim();
824
+ const weight = args.weight ? parseFloat(String(args.weight)) : 0.5;
825
+ if (!sourceId || !targetId || !relation) {
826
+ return 'Error: source, target, and relation are all required.';
827
+ }
828
+ load();
829
+ const sourceNode = graph.nodes.get(sourceId);
830
+ const targetNode = graph.nodes.get(targetId);
831
+ if (!sourceNode)
832
+ return `Error: source node "${sourceId}" not found.`;
833
+ if (!targetNode)
834
+ return `Error: target node "${targetId}" not found.`;
835
+ if (addEdge(sourceId, targetId, relation, weight)) {
836
+ save();
837
+ return `Connected [${sourceNode.type}:${sourceNode.name}] --${relation}(${weight.toFixed(2)})--> [${targetNode.type}:${targetNode.name}]`;
838
+ }
839
+ return 'Error: failed to create edge.';
840
+ },
841
+ });
842
+ // ── graph_view ──
843
+ registerTool({
844
+ name: 'graph_view',
845
+ description: 'View the subgraph around a specific node — its neighbors and connections. Use this to understand how an entity relates to others in the knowledge graph.',
846
+ parameters: {
847
+ node_id: {
848
+ type: 'string',
849
+ description: 'The node ID to center the view on',
850
+ required: true,
851
+ },
852
+ depth: {
853
+ type: 'string',
854
+ description: 'How many hops from the center node to include (1-3, default: 1)',
855
+ },
856
+ },
857
+ tier: 'free',
858
+ timeout: 10_000,
859
+ async execute(args) {
860
+ const nodeId = String(args.node_id || '').trim();
861
+ const depth = args.depth ? Math.max(1, Math.min(3, parseInt(String(args.depth), 10) || 1)) : 1;
862
+ if (!nodeId)
863
+ return 'Error: node_id is required.';
864
+ load();
865
+ const centerNode = graph.nodes.get(nodeId);
866
+ if (!centerNode)
867
+ return `Error: node "${nodeId}" not found.`;
868
+ const { nodes, edges } = getNeighbors(nodeId, depth);
869
+ const lines = [];
870
+ lines.push(`Subgraph around [${centerNode.type}:${centerNode.name}] (depth ${depth}):`);
871
+ lines.push(` ${nodes.length} node(s), ${edges.length} edge(s)`);
872
+ lines.push('');
873
+ // List nodes
874
+ lines.push('Nodes:');
875
+ for (const node of nodes) {
876
+ const marker = node.id === nodeId ? ' ★' : '';
877
+ lines.push(` [${node.type}:${node.name}] id=${node.id}${marker}`);
878
+ }
879
+ // List edges
880
+ if (edges.length > 0) {
881
+ lines.push('');
882
+ lines.push('Edges:');
883
+ for (const edge of edges) {
884
+ const src = graph.nodes.get(edge.source);
885
+ const tgt = graph.nodes.get(edge.target);
886
+ if (src && tgt) {
887
+ lines.push(` [${src.type}:${src.name}] --${edge.relation}(${edge.weight.toFixed(2)})--> [${tgt.type}:${tgt.name}]`);
888
+ }
889
+ }
890
+ }
891
+ save();
892
+ return lines.join('\n');
893
+ },
894
+ });
895
+ // ── graph_context ──
896
+ registerTool({
897
+ name: 'graph_context',
898
+ description: 'Get relevant knowledge graph context for the current task. Returns a compact, human-readable summary of the most relevant entities and their connections. Inject this into the system prompt to give the agent structural awareness.',
899
+ parameters: {
900
+ max_tokens: {
901
+ type: 'string',
902
+ description: 'Max approximate tokens for the context output (default: 2000)',
903
+ },
904
+ focus: {
905
+ type: 'string',
906
+ description: 'Optional: a search term to focus the context around specific entities',
907
+ },
908
+ },
909
+ tier: 'free',
910
+ timeout: 10_000,
911
+ async execute(args) {
912
+ const maxTokens = args.max_tokens ? parseInt(String(args.max_tokens), 10) || 2000 : 2000;
913
+ load();
914
+ // If a focus query is provided, boost those nodes first
915
+ if (args.focus) {
916
+ const focusQuery = String(args.focus).trim();
917
+ // Touch focus-related nodes to boost their recency
918
+ findNode(focusQuery); // Side effect: updates lastAccessed on matches
919
+ }
920
+ const context = toContext(maxTokens);
921
+ save();
922
+ return context;
923
+ },
924
+ });
925
+ }
926
+ //# sourceMappingURL=graph-memory.js.map