@plumpslabs/kuma 2.3.20 → 2.3.23

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 (39) hide show
  1. package/README.md +18 -0
  2. package/dist/index.js +10316 -380
  3. package/package.json +2 -2
  4. package/packages/ide/studio/dist/index.js +90 -9
  5. package/packages/ide/studio/public/index.html +528 -233
  6. package/dist/agentDetector-YOWQVJFR.js +0 -186
  7. package/dist/chunk-3BRBJZ7P.js +0 -1055
  8. package/dist/chunk-3OHYYXYN.js +0 -71
  9. package/dist/chunk-ABKE45T4.js +0 -1264
  10. package/dist/chunk-E2KFPEBT.js +0 -183
  11. package/dist/chunk-FKRSI5U5.js +0 -282
  12. package/dist/chunk-GFLSAXAH.js +0 -155
  13. package/dist/chunk-L7F67KUP.js +0 -172
  14. package/dist/chunk-LVKOGXLC.js +0 -658
  15. package/dist/chunk-PRUTTZBS.js +0 -1113
  16. package/dist/contextDigest-QB5XHPXE.js +0 -177
  17. package/dist/domainRules-QLPAQASB.js +0 -20
  18. package/dist/init-PL4XL662.js +0 -15
  19. package/dist/kumaAstValidator-CNM7FHYA.js +0 -150
  20. package/dist/kumaCheckpoint-J2LDQMEO.js +0 -207
  21. package/dist/kumaCodeScanner-J6B2EHGI.js +0 -563
  22. package/dist/kumaContractEngine-KX27T4N7.js +0 -305
  23. package/dist/kumaDb-4XZ5S2LH.js +0 -65
  24. package/dist/kumaDriftDetector-TOORILSZ.js +0 -237
  25. package/dist/kumaGotchas-XRGFFBTA.js +0 -151
  26. package/dist/kumaGraph-UMXZNGYF.js +0 -44
  27. package/dist/kumaMemory-FBJMV77G.js +0 -16
  28. package/dist/kumaMiner-XJETL7TL.js +0 -176
  29. package/dist/kumaPolicyEngine-2QDJDLM7.js +0 -311
  30. package/dist/kumaProgressiveContext-MWEDRXOH.js +0 -231
  31. package/dist/kumaSearch-PV4QTKE7.js +0 -321
  32. package/dist/kumaTrajectory-7NOAVNG2.js +0 -460
  33. package/dist/kumaVerifier-6YEGC77M.js +0 -265
  34. package/dist/kumaVisualize-264OEBGJ.js +0 -264
  35. package/dist/pathValidator-V4DC6U6Z.js +0 -22
  36. package/dist/safetyAudit-O45SPNTS.js +0 -12
  37. package/dist/safetyScore-TMMRD2MV.js +0 -333
  38. package/dist/sessionMemory-YPKVIOMV.js +0 -6
  39. package/dist/skillGenerator-PWEJKZNX.js +0 -304
@@ -1,1113 +0,0 @@
1
- import {
2
- getDb,
3
- saveDb
4
- } from "./chunk-3BRBJZ7P.js";
5
- import {
6
- getProjectRoot
7
- } from "./chunk-E2KFPEBT.js";
8
-
9
- // src/engine/kumaSelfHeal.ts
10
- import { execSync } from "child_process";
11
- import fs from "fs";
12
- import path from "path";
13
- import crypto from "crypto";
14
- function contentFingerprint(filePath) {
15
- try {
16
- const fullPath = path.join(getProjectRoot(), filePath);
17
- if (!fs.existsSync(fullPath)) return null;
18
- const stat = fs.statSync(fullPath);
19
- if (!stat.isFile()) return null;
20
- const fd = fs.openSync(fullPath, "r");
21
- try {
22
- const size = stat.size;
23
- const readSize = Math.min(1024, size);
24
- const head = Buffer.alloc(readSize);
25
- fs.readSync(fd, head, 0, readSize, 0);
26
- let tail = Buffer.alloc(0);
27
- if (size > 2048) {
28
- tail = Buffer.alloc(1024);
29
- fs.readSync(fd, tail, 0, 1024, size - 1024);
30
- }
31
- const hash = crypto.createHash("md5");
32
- hash.update(head);
33
- hash.update(tail);
34
- hash.update(String(size));
35
- return hash.digest("hex");
36
- } finally {
37
- fs.closeSync(fd);
38
- }
39
- } catch {
40
- return null;
41
- }
42
- }
43
- function findByFingerprint(fingerprint, oldName) {
44
- try {
45
- const root = getProjectRoot();
46
- const ext = path.extname(oldName);
47
- const result = execSync(
48
- `find . -name "*${ext}" -type f -not -path "./node_modules/*" -not -path "./.git/*" -not -path "./dist/*" -not -path "./build/*" 2>/dev/null | head -200`,
49
- { cwd: root, encoding: "utf-8", maxBuffer: 1024 * 1024, timeout: 1e4 }
50
- ).trim();
51
- if (!result) return null;
52
- const files = result.split("\n").filter(Boolean);
53
- for (const file of files) {
54
- const relativePath = file.startsWith("./") ? file.slice(2) : file;
55
- const fp = contentFingerprint(relativePath);
56
- if (fp === fingerprint) {
57
- return relativePath;
58
- }
59
- }
60
- return null;
61
- } catch {
62
- return null;
63
- }
64
- }
65
- async function detectStaleNodes() {
66
- const stale = [];
67
- try {
68
- const db = await getDb();
69
- const root = getProjectRoot();
70
- const stmt = db.prepare(`
71
- SELECT id, type, name, file_path FROM nodes
72
- WHERE file_path IS NOT NULL
73
- AND length(file_path) > 0
74
- ORDER BY updated_at DESC LIMIT 500
75
- `);
76
- while (stmt.step()) {
77
- const row = stmt.getAsObject();
78
- const filePath = row.file_path;
79
- if (filePath.startsWith("search::") || filePath.startsWith("api_route::")) continue;
80
- const fullPath = path.join(root, filePath);
81
- if (!fs.existsSync(fullPath)) {
82
- const newPath = findRenamedPath(filePath);
83
- if (newPath) {
84
- stale.push({
85
- nodeId: row.id,
86
- type: row.type,
87
- name: row.name,
88
- oldPath: filePath,
89
- newPath,
90
- issue: "path-changed"
91
- });
92
- } else {
93
- const oldFingerprint = contentFingerprint(filePath);
94
- const contentMatch = oldFingerprint ? findByFingerprint(oldFingerprint, path.basename(filePath)) : null;
95
- stale.push({
96
- nodeId: row.id,
97
- type: row.type,
98
- name: row.name,
99
- oldPath: filePath,
100
- newPath: contentMatch,
101
- issue: contentMatch ? "path-changed" : "file-missing"
102
- });
103
- }
104
- }
105
- }
106
- stmt.free();
107
- } catch (err) {
108
- console.error(`[KumaSelfHeal] Failed to detect stale nodes: ${err}`);
109
- }
110
- return stale;
111
- }
112
- function findRenamedPath(oldPath) {
113
- try {
114
- const root = getProjectRoot();
115
- const output = execSync(
116
- `git log --follow --diff-filter=R --name-only --format="" -1 -- "${oldPath}"`,
117
- { cwd: root, encoding: "utf-8", maxBuffer: 1024 * 1024, timeout: 5e3 }
118
- ).trim();
119
- if (output) {
120
- const lines = output.split("\n").filter(Boolean);
121
- return lines[lines.length - 1] || null;
122
- }
123
- return null;
124
- } catch {
125
- return null;
126
- }
127
- }
128
- async function cascadeStaleEdges(nodeIds) {
129
- try {
130
- const db = await getDb();
131
- let count = 0;
132
- for (const nodeId2 of nodeIds) {
133
- db.run(
134
- `UPDATE edges SET weight = MAX(weight * 0.1, 0.01), metadata = json_set(COALESCE(NULLIF(metadata,''), '{}'), '$.stale', 1)
135
- WHERE (source_id = ? OR target_id = ?) AND weight > 0`,
136
- [nodeId2, nodeId2]
137
- );
138
- count += db.getRowsModified();
139
- }
140
- saveDb();
141
- return count;
142
- } catch (err) {
143
- console.error(`[KumaSelfHeal] Failed to cascade stale edges: ${err}`);
144
- return 0;
145
- }
146
- }
147
- async function healStaleNode(entry) {
148
- try {
149
- const db = await getDb();
150
- if (entry.newPath) {
151
- db.run(
152
- `UPDATE nodes SET file_path = ?, updated_at = strftime('%s','now') WHERE id = ?`,
153
- [entry.newPath, entry.nodeId]
154
- );
155
- db.run(
156
- `UPDATE edges SET metadata = json_set(COALESCE(NULLIF(metadata,''), '{}'), '$.healed', 1) WHERE source_id = ? OR target_id = ?`,
157
- [entry.nodeId, entry.nodeId]
158
- );
159
- db.run(
160
- `UPDATE edges SET metadata = json_set(COALESCE(NULLIF(metadata,''), '{}'), '$.healed', 1, '$.newPath', ?)
161
- WHERE json_extract(metadata, '$.filePath') = ?`,
162
- [entry.newPath, entry.oldPath]
163
- );
164
- saveDb();
165
- return true;
166
- }
167
- db.run(
168
- `UPDATE nodes SET metadata = json_set(COALESCE(NULLIF(metadata,''), '{}'), '$.stale', 1) WHERE id = ?`,
169
- [entry.nodeId]
170
- );
171
- saveDb();
172
- return false;
173
- } catch (err) {
174
- console.error(`[KumaSelfHeal] Failed to heal node ${entry.nodeId}: ${err}`);
175
- return false;
176
- }
177
- }
178
- async function autoHeal() {
179
- const stale = await detectStaleNodes();
180
- let healed = 0;
181
- let missing = 0;
182
- for (const entry of stale) {
183
- const success = await healStaleNode(entry);
184
- if (success) healed++;
185
- else missing++;
186
- }
187
- const staleNodeIds = stale.filter((e) => !e.newPath).map((e) => e.nodeId);
188
- const cascadedEdges = await cascadeStaleEdges(staleNodeIds);
189
- return { healed, missing, total: stale.length, cascadedEdges };
190
- }
191
- async function healOnQuery(filePaths) {
192
- if (filePaths.length === 0) return { healed: 0 };
193
- try {
194
- const stalePaths = filePaths.filter((fp) => {
195
- if (!fp || fp.startsWith("search::") || fp.startsWith("api_route::")) return false;
196
- const fullPath = path.join(getProjectRoot(), fp);
197
- return !fs.existsSync(fullPath);
198
- });
199
- if (stalePaths.length === 0) return { healed: 0 };
200
- let healed = 0;
201
- for (const oldPath of stalePaths) {
202
- const newPath = findRenamedPath(oldPath);
203
- if (newPath) {
204
- const db = await getDb();
205
- db.run(
206
- `UPDATE nodes SET file_path = ?, updated_at = strftime('%s','now') WHERE file_path = ?`,
207
- [newPath, oldPath]
208
- );
209
- saveDb();
210
- healed++;
211
- }
212
- }
213
- return { healed };
214
- } catch {
215
- return { healed: 0 };
216
- }
217
- }
218
- function formatHealReport(result) {
219
- if (result.total === 0) {
220
- return "\u2705 **Self-Heal Check** \u2014 No stale entries found. Knowledge Graph is fresh.";
221
- }
222
- const lines = [
223
- `\u{1FA7A} **Self-Heal Report**`,
224
- `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
225
- "",
226
- `\u{1F4CA} ${result.total} stale entr${result.total > 1 ? "ies" : "y"} found`,
227
- `\u2705 ${result.healed} healed (file renamed \u2192 path updated)`,
228
- `\u274C ${result.missing} missing (file deleted \u2014 marked as stale)`,
229
- result.cascadedEdges > 0 ? `\u{1F517} ${result.cascadedEdges} cascade edges updated` : "",
230
- ""
231
- ].filter(Boolean);
232
- if (result.healed > 0) {
233
- lines.push("\u{1F4A1} Healed entries had their file paths updated via git rename detection or content hash matching.");
234
- }
235
- if (result.missing > 0) {
236
- lines.push("\u26A0\uFE0F Missing entries were marked as stale. The Knowledge Graph will deprioritize them.");
237
- lines.push("\u{1F4A1} Run incremental healing after making changes to keep the graph fresh.");
238
- }
239
- return lines.join("\n");
240
- }
241
-
242
- // src/engine/kumaGraph.ts
243
- function nodeId(type, name) {
244
- return `${type}::${name}`;
245
- }
246
- async function upsertNode(node) {
247
- try {
248
- const db = await getDb();
249
- const id = node.id || nodeId(node.type, node.name);
250
- const metadata = JSON.stringify(node.metadata ?? {});
251
- db.run(`
252
- INSERT INTO nodes (id, type, name, file_path, metadata, updated_at)
253
- VALUES (?, ?, ?, ?, ?, strftime('%s','now'))
254
- ON CONFLICT(id) DO UPDATE SET
255
- type = excluded.type,
256
- name = excluded.name,
257
- file_path = COALESCE(excluded.file_path, nodes.file_path),
258
- metadata = excluded.metadata,
259
- updated_at = strftime('%s','now')
260
- `, [id, node.type, node.name, node.filePath || null, metadata]);
261
- try {
262
- db.run(`INSERT INTO nodes_fts (rowid, name, metadata) VALUES (last_insert_rowid(), ?, ?)`, [node.name, metadata]);
263
- } catch {
264
- }
265
- saveDb(db);
266
- } catch (err) {
267
- console.error(`[KumaGraph] Failed to upsert node: ${err}`);
268
- }
269
- }
270
- async function addEdge(edge) {
271
- try {
272
- const db = await getDb();
273
- const weight = edge.weight ?? 1;
274
- const metadata = JSON.stringify(edge.metadata ?? {});
275
- db.run(`
276
- INSERT INTO edges (source_id, target_id, type, weight, metadata)
277
- VALUES (?, ?, ?, ?, ?)
278
- ON CONFLICT(source_id, target_id, type) DO UPDATE SET
279
- weight = edges.weight + 1,
280
- metadata = excluded.metadata
281
- `, [edge.sourceId, edge.targetId, edge.type, weight, metadata]);
282
- saveDb(db);
283
- } catch (err) {
284
- console.error(`[KumaGraph] Failed to add edge: ${err}`);
285
- }
286
- }
287
- async function recordFunctionCall(caller, callee, filePath) {
288
- const callerId = nodeId("function", caller);
289
- const calleeId = nodeId("function", callee);
290
- await upsertNode({ id: callerId, type: "function", name: caller, filePath });
291
- await upsertNode({ id: calleeId, type: "function", name: callee });
292
- await addEdge({ sourceId: callerId, targetId: calleeId, type: "calls" });
293
- }
294
- async function recordFileDefinition(filePath, symbol, symbolType = "function") {
295
- const fileId = nodeId("file", filePath);
296
- const symbolId = nodeId(symbolType, symbol);
297
- await upsertNode({ id: fileId, type: "file", name: filePath });
298
- await upsertNode({ id: symbolId, type: symbolType, name: symbol, filePath });
299
- await addEdge({ sourceId: fileId, targetId: symbolId, type: "defines", metadata: { filePath } });
300
- }
301
- async function recordImport(fromFile, toFile) {
302
- const fromId = nodeId("file", fromFile);
303
- const toId = nodeId("file", toFile);
304
- await upsertNode({ id: fromId, type: "file", name: fromFile });
305
- await upsertNode({ id: toId, type: "file", name: toFile });
306
- await addEdge({ sourceId: fromId, targetId: toId, type: "imports" });
307
- }
308
- async function recordTestRelation(testFile, functionName) {
309
- const testId = nodeId("test", testFile);
310
- const funcId = nodeId("function", functionName);
311
- await upsertNode({ id: testId, type: "test", name: testFile });
312
- await upsertNode({ id: funcId, type: "function", name: functionName });
313
- await addEdge({ sourceId: testId, targetId: funcId, type: "tests" });
314
- }
315
- async function recordApiRoute(route, handler) {
316
- const routeId = nodeId("api_route", route);
317
- const handlerId = nodeId("function", handler);
318
- await upsertNode({ id: routeId, type: "api_route", name: route });
319
- await upsertNode({ id: handlerId, type: "function", name: handler });
320
- await addEdge({ sourceId: routeId, targetId: handlerId, type: "routes" });
321
- }
322
- async function recordDomainFlow(params) {
323
- let nodeCount = 0;
324
- let edgeCount = 0;
325
- try {
326
- const domainId = nodeId("feature_domain", params.domain);
327
- await upsertNode({
328
- id: domainId,
329
- type: "feature_domain",
330
- name: params.domain,
331
- metadata: {
332
- hops: params.hops.length,
333
- gotchas: params.gotchas?.length || 0,
334
- decisions: params.decisions?.length || 0,
335
- filePaths: params.filePaths || []
336
- }
337
- });
338
- nodeCount++;
339
- for (let i = 0; i < params.hops.length; i++) {
340
- const hop = params.hops[i];
341
- const fromId = nodeId("cross_service_link", `${params.domain}::${hop.from}`);
342
- await upsertNode({
343
- id: fromId,
344
- type: "cross_service_link",
345
- name: hop.from,
346
- metadata: {
347
- domain: params.domain,
348
- relation: hop.relation,
349
- description: hop.description || "",
350
- hopIndex: i
351
- }
352
- });
353
- nodeCount++;
354
- const toId = nodeId("cross_service_link", `${params.domain}::${hop.to}`);
355
- await upsertNode({
356
- id: toId,
357
- type: "cross_service_link",
358
- name: hop.to,
359
- metadata: {
360
- domain: params.domain,
361
- relation: hop.relation,
362
- description: hop.description || "",
363
- hopIndex: i
364
- }
365
- });
366
- nodeCount++;
367
- await addEdge({
368
- sourceId: fromId,
369
- targetId: toId,
370
- type: "flows_through",
371
- metadata: { relation: hop.relation, description: hop.description }
372
- });
373
- edgeCount++;
374
- await addEdge({
375
- sourceId: domainId,
376
- targetId: fromId,
377
- type: "contains",
378
- metadata: { hopIndex: i }
379
- });
380
- edgeCount++;
381
- await addEdge({
382
- sourceId: domainId,
383
- targetId: toId,
384
- type: "contains",
385
- metadata: { hopIndex: i }
386
- });
387
- edgeCount++;
388
- }
389
- if (params.gotchas) {
390
- for (const gotcha of params.gotchas) {
391
- const gotchaId = `gotcha::${params.domain}::${gotcha.substring(0, 40)}`;
392
- try {
393
- await upsertNode({
394
- id: gotchaId,
395
- type: "variable",
396
- name: `gotcha:${gotcha.substring(0, 60)}`,
397
- metadata: { domain: params.domain, gotcha, source: "arch_flow" }
398
- });
399
- nodeCount++;
400
- await addEdge({
401
- sourceId: domainId,
402
- targetId: gotchaId,
403
- type: "depends_on",
404
- metadata: { type: "gotcha" }
405
- });
406
- edgeCount++;
407
- } catch {
408
- }
409
- }
410
- }
411
- if (params.decisions) {
412
- for (const decision of params.decisions) {
413
- const decisionId = `decision::${params.domain}::${decision.substring(0, 40)}`;
414
- try {
415
- await upsertNode({
416
- id: decisionId,
417
- type: "variable",
418
- name: `decision:${decision.substring(0, 60)}`,
419
- metadata: { domain: params.domain, decision, source: "arch_flow" }
420
- });
421
- nodeCount++;
422
- await addEdge({
423
- sourceId: domainId,
424
- targetId: decisionId,
425
- type: "depends_on",
426
- metadata: { type: "decision" }
427
- });
428
- edgeCount++;
429
- } catch {
430
- }
431
- }
432
- }
433
- if (params.filePaths) {
434
- for (const fp of params.filePaths) {
435
- const fileId = nodeId("file", fp);
436
- try {
437
- await upsertNode({
438
- id: fileId,
439
- type: "file",
440
- name: fp
441
- });
442
- nodeCount++;
443
- await addEdge({
444
- sourceId: domainId,
445
- targetId: fileId,
446
- type: "owns",
447
- metadata: { domain: params.domain }
448
- });
449
- edgeCount++;
450
- } catch {
451
- }
452
- }
453
- }
454
- return { nodeCount, edgeCount };
455
- } catch (err) {
456
- console.error(`[KumaGraph] Failed to record domain flow: ${err}`);
457
- return { nodeCount, edgeCount };
458
- }
459
- }
460
- async function queryGraph(params) {
461
- try {
462
- const db = await getDb();
463
- const { type = "nodes", query, limit = 20 } = params;
464
- if (query && !query.includes("::")) {
465
- try {
466
- await healOnQuery([query]);
467
- } catch {
468
- }
469
- }
470
- if (type === "nodes") {
471
- const stmt = db.prepare(`
472
- SELECT id, type, name, file_path, metadata FROM nodes
473
- WHERE name LIKE ? OR type = ?
474
- ORDER BY updated_at DESC
475
- LIMIT ?
476
- `);
477
- stmt.bind([`%${query}%`, query, limit]);
478
- const results = [];
479
- while (stmt.step()) {
480
- const row = stmt.getAsObject();
481
- results.push(row);
482
- }
483
- stmt.free();
484
- if (results.length === 0) {
485
- return `\u{1F50D} **Graph Query** \u2014 No nodes found for "${query}".
486
-
487
- Try a different search term or check if the graph has been built by using tools like smart_grep or lsp_query.`;
488
- }
489
- const lines = [
490
- `\u{1F50D} **Graph Nodes** \u2014 ${results.length} result(s) for "${query}"`,
491
- ""
492
- ];
493
- for (const r of results) {
494
- const typeEmoji = r.type === "function" ? "\u{1F527}" : r.type === "file" ? "\u{1F4C4}" : r.type === "test" ? "\u{1F9EA}" : r.type === "api_route" ? "\u{1F310}" : r.type === "db_table" ? "\u{1F5C4}\uFE0F" : r.type === "class" ? "\u{1F3D7}\uFE0F" : r.type === "feature_domain" ? "\u{1F3DB}\uFE0F" : r.type === "workflow" ? "\u{1F504}" : r.type === "cross_service_link" ? "\u{1F517}" : "\u{1F4CC}";
495
- lines.push(`${typeEmoji} **${r.name}** (${r.type})`);
496
- if (r.file_path) lines.push(` \u{1F4CD} ${r.file_path}`);
497
- }
498
- lines.push("", "\u{1F4A1} Use kuma_context({ action: 'research', scope: '<name>' }) to research a scope.");
499
- return lines.join("\n");
500
- } else if (type === "edges") {
501
- const stmt = db.prepare(`
502
- SELECT e.type, e.weight,
503
- CASE WHEN e.source_id = ? THEN e.target_id ELSE e.source_id END AS connected_node,
504
- n.type AS connected_type, n.name AS connected_name, n.file_path AS connected_path
505
- FROM edges e
506
- LEFT JOIN nodes n ON n.id = (CASE WHEN e.source_id = ? THEN e.target_id ELSE e.source_id END)
507
- WHERE e.source_id = ? OR e.target_id = ?
508
- ORDER BY e.weight DESC
509
- LIMIT ?
510
- `);
511
- stmt.bind([query, query, query, query, limit]);
512
- const results = [];
513
- while (stmt.step()) {
514
- const row = stmt.getAsObject();
515
- results.push(row);
516
- }
517
- stmt.free();
518
- if (results.length === 0) {
519
- return `\u{1F50D} **Graph Edges** \u2014 No connections found for node "${query}".
520
-
521
- Try kuma_graph_query({ type: 'nodes', query: '<search>' }) to find node IDs first.`;
522
- }
523
- const lines = [
524
- `\u{1F517} **Graph Edges** \u2014 ${results.length} connection(s) for "${query}"`,
525
- ""
526
- ];
527
- for (const r of results) {
528
- const edgeEmoji = r.type === "calls" ? "\u27A1\uFE0F" : r.type === "imports" ? "\u{1F4E5}" : r.type === "defines" ? "\u{1F4DD}" : r.type === "tests" ? "\u{1F9EA}" : r.type === "routes" ? "\u{1F310}" : "\u{1F517}";
529
- const typeLabel = r.connected_type ? ` (${r.connected_type})` : "";
530
- lines.push(`${edgeEmoji} **${r.connected_node}**${typeLabel} \u2014 ${r.type} (weight: ${r.weight})`);
531
- if (r.connected_path) lines.push(` \u{1F4CD} ${r.connected_path}`);
532
- }
533
- return lines.join("\n");
534
- } else if (type === "paths") {
535
- const parts = query.split("\u2192").map((s) => s.trim());
536
- if (parts.length !== 2) {
537
- return `\u26A0\uFE0F For path queries, use format: "sourceNodeID \u2192 targetNodeID"
538
- Example: kuma_graph_query({ type: 'paths', query: 'function::login \u2192 function::validatePassword' })`;
539
- }
540
- const [sourceId, targetId] = parts;
541
- const visited = /* @__PURE__ */ new Set();
542
- const maxDepth = 10;
543
- const queue = [{ nodeId: sourceId, path: [sourceId] }];
544
- visited.add(sourceId);
545
- let foundPath = null;
546
- while (queue.length > 0 && !foundPath && queue[0].path.length <= maxDepth) {
547
- const current = queue.shift();
548
- const stmt = db.prepare(`
549
- SELECT CASE WHEN source_id = ? THEN target_id ELSE source_id END AS neighbor
550
- FROM edges WHERE (source_id = ? OR target_id = ?)
551
- `);
552
- stmt.bind([current.nodeId, current.nodeId, current.nodeId]);
553
- while (stmt.step()) {
554
- const row = stmt.getAsObject();
555
- const neighbor = row.neighbor;
556
- if (neighbor === targetId) {
557
- foundPath = [...current.path, neighbor];
558
- break;
559
- }
560
- if (!visited.has(neighbor)) {
561
- visited.add(neighbor);
562
- queue.push({ nodeId: neighbor, path: [...current.path, neighbor] });
563
- }
564
- }
565
- stmt.free();
566
- }
567
- if (!foundPath) {
568
- return `\u{1F50D} **Path Query** \u2014 No path found between "${sourceId}" and "${targetId}".
569
-
570
- The two nodes may not be connected in the knowledge graph yet. Use more tools to build the graph.`;
571
- }
572
- const names = [];
573
- for (const nodeId2 of foundPath) {
574
- const stmt2 = db.prepare("SELECT name, type FROM nodes WHERE id = ?");
575
- stmt2.bind([nodeId2]);
576
- if (stmt2.step()) {
577
- const row = stmt2.getAsObject();
578
- names.push(`${row.name} (${row.type})`);
579
- } else {
580
- names.push(nodeId2);
581
- }
582
- stmt2.free();
583
- }
584
- return [
585
- `\u{1F6E4}\uFE0F **Graph Path** \u2014 ${names.length} node(s)`,
586
- "",
587
- ...names.map((n, i) => ` ${i > 0 ? "\u2192" : " "} ${n}`),
588
- "",
589
- "\u{1F4A1} This path was discovered by traversing the knowledge graph built from AI tool calls."
590
- ].join("\n");
591
- }
592
- return `\u26A0\uFE0F Unknown query type "${type}". Use "nodes", "edges", or "paths".`;
593
- } catch (err) {
594
- return `Error querying graph: ${err}`;
595
- }
596
- }
597
- async function buildFromSessionMemory() {
598
- try {
599
- const { sessionMemory } = await import("./sessionMemory-YPKVIOMV.js");
600
- const toolCalls = sessionMemory.getToolCallHistory(50);
601
- let edgeCount = 0;
602
- for (const call of toolCalls) {
603
- const params = call.params;
604
- if (call.toolName === "kuma_research" || call.toolName === "research") {
605
- const scope = params.scope;
606
- if (scope) {
607
- await upsertNode({
608
- id: `research::${scope}`,
609
- type: "variable",
610
- name: `research:${scope}`,
611
- metadata: { confidence: params.confidence || 0 }
612
- });
613
- edgeCount++;
614
- }
615
- }
616
- const filePath = params.filePath || params.files?.[0];
617
- if (filePath) {
618
- await upsertNode({
619
- id: nodeId("file", filePath),
620
- type: "file",
621
- name: filePath
622
- });
623
- edgeCount++;
624
- }
625
- }
626
- return edgeCount;
627
- } catch {
628
- return 0;
629
- }
630
- }
631
- async function searchGraph(query, limit = 20) {
632
- try {
633
- const db = await getDb();
634
- try {
635
- const stmt2 = db.prepare(`
636
- SELECT n.id, n.type, n.name, n.file_path
637
- FROM nodes_fts f
638
- JOIN nodes n ON n.rowid = f.rowid
639
- WHERE nodes_fts MATCH ?
640
- LIMIT ?
641
- `);
642
- stmt2.bind([query, limit]);
643
- const results2 = [];
644
- while (stmt2.step()) {
645
- results2.push(stmt2.getAsObject());
646
- }
647
- stmt2.free();
648
- if (results2.length > 0) {
649
- const lines2 = [
650
- `\u{1F50D} **Graph Search** \u2014 ${results2.length} result(s) for "${query}"`,
651
- ""
652
- ];
653
- for (const r of results2) {
654
- lines2.push(` \u2022 **${r.name}** (${r.type})${r.file_path ? ` \u2014 ${r.file_path}` : ""}`);
655
- }
656
- return lines2.join("\n");
657
- }
658
- } catch {
659
- }
660
- const stmt = db.prepare(`
661
- SELECT id, type, name, file_path FROM nodes
662
- WHERE name LIKE ? OR file_path LIKE ?
663
- ORDER BY updated_at DESC
664
- LIMIT ?
665
- `);
666
- stmt.bind([`%${query}%`, `%${query}%`, limit]);
667
- const results = [];
668
- while (stmt.step()) {
669
- results.push(stmt.getAsObject());
670
- }
671
- stmt.free();
672
- if (results.length === 0) {
673
- return await codebaseSearchFallback(query, limit);
674
- }
675
- const lines = [
676
- `\u{1F50D} **Graph Search** \u2014 ${results.length} result(s) for "${query}"`,
677
- ""
678
- ];
679
- for (const r of results) {
680
- lines.push(` \u2022 **${r.name}** (${r.type})${r.file_path ? ` \u2014 ${r.file_path}` : ""}`);
681
- }
682
- return lines.join("\n");
683
- } catch (err) {
684
- return `Error searching graph: ${err}`;
685
- }
686
- }
687
- async function codebaseSearchFallback(query, limit = 20) {
688
- try {
689
- const fg = (await import("fast-glob")).default;
690
- const { getProjectRoot: getProjectRoot2 } = await import("./pathValidator-V4DC6U6Z.js");
691
- const root = getProjectRoot2();
692
- const patterns = [
693
- `**/*${query}*`,
694
- `**/*${query}*/**`,
695
- query.includes(".") ? `**/${query}` : null
696
- ].filter(Boolean);
697
- const ignorePatterns = ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/.kuma/**"];
698
- const files = await fg(patterns, {
699
- cwd: root,
700
- ignore: ignorePatterns,
701
- onlyFiles: true,
702
- deep: 6,
703
- dot: false
704
- });
705
- if (files.length === 0) {
706
- return `\u{1F50D} **Codebase Search** \u2014 No results for "${query}". Try a different search term or research the topic first.`;
707
- }
708
- const lines = [
709
- `\u{1F50D} **Codebase Search** \u2014 ${files.length} result(s) for "${query}" (graph fallback)`,
710
- `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
711
- ""
712
- ];
713
- for (const file of files.slice(0, limit)) {
714
- lines.push(` \u{1F4C4} **${file}**`);
715
- }
716
- if (files.length > limit) {
717
- lines.push(` ... and ${files.length - limit} more`);
718
- }
719
- lines.push(
720
- "",
721
- "\u{1F4A1} The knowledge graph is empty or has no matches.",
722
- "\u{1F4A1} Run research or use more tools to build the graph for richer results."
723
- );
724
- return lines.join("\n");
725
- } catch {
726
- try {
727
- let walk2 = function(dir, depth = 0) {
728
- if (results.length >= limit || depth > MAX_WALK_DEPTH) return;
729
- let entries = [];
730
- try {
731
- entries = fs2.readdirSync(dir);
732
- } catch {
733
- return;
734
- }
735
- for (const entry of entries) {
736
- if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "build") continue;
737
- const full = path2.join(dir, entry);
738
- try {
739
- const stat = fs2.statSync(full);
740
- if (stat.isDirectory()) {
741
- walk2(full, depth + 1);
742
- } else if (entry.toLowerCase().includes(query.toLowerCase())) {
743
- results.push(path2.relative(root, full));
744
- }
745
- } catch {
746
- }
747
- }
748
- };
749
- var walk = walk2;
750
- const fs2 = await import("fs");
751
- const path2 = await import("path");
752
- const root = process.cwd();
753
- const results = [];
754
- const MAX_WALK_DEPTH = 8;
755
- walk2(root, 0);
756
- if (results.length === 0) {
757
- return `\u{1F50D} **Codebase Search** \u2014 No results for "${query}". Try a different search term.`;
758
- }
759
- return [
760
- `\u{1F50D} **Codebase Search** \u2014 ${results.length} result(s) for "${query}" (basic fallback)`,
761
- "",
762
- ...results.slice(0, limit).map((f) => ` \u{1F4C4} **${f}**`),
763
- "",
764
- "\u{1F4A1} Install fast-glob for faster, more comprehensive codebase searches."
765
- ].join("\n");
766
- } catch {
767
- return `\u{1F50D} **Search** \u2014 No results for "${query}". The knowledge graph is empty. Try researching first.`;
768
- }
769
- }
770
- }
771
- async function resolveFederatedNode(uri) {
772
- try {
773
- if (!uri.startsWith("kuma://")) {
774
- return `\u26A0\uFE0F Invalid URI scheme: "${uri.substring(0, 20)}...". Use kuma://project/node-id`;
775
- }
776
- const pathPart = uri.replace("kuma://", "");
777
- const firstSlash = pathPart.indexOf("/");
778
- if (firstSlash === -1) {
779
- return `\u26A0\uFE0F Invalid federated URI: "${uri}". Format: kuma://<project>/<node-id>`;
780
- }
781
- const projectName = pathPart.substring(0, firstSlash);
782
- const nodeRef = pathPart.substring(firstSlash + 1);
783
- const db = await getDb();
784
- const stmt = db.prepare(`
785
- SELECT id, type, name, file_path, metadata FROM nodes
786
- WHERE id = ? OR name LIKE ?
787
- LIMIT 5
788
- `);
789
- stmt.bind([nodeRef, `%${nodeRef}%`]);
790
- const results = [];
791
- while (stmt.step()) results.push(stmt.getAsObject());
792
- stmt.free();
793
- if (results.length > 0) {
794
- const lines = [
795
- `\u{1F517} **Federated Node Resolved** (local)`,
796
- `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
797
- `\u{1F4C1} Project: ${projectName}`,
798
- `\u{1F517} URI: ${uri}`,
799
- ""
800
- ];
801
- for (const r of results) {
802
- lines.push(` \u2022 **${r.name}** (${r.type}) \u2014 ${r.file_path || "no path"}`);
803
- }
804
- lines.push("", "\u{1F4A1} Federated cross-repo resolution is active. Remote resolution via HTTP coming soon.");
805
- return lines.join("\n");
806
- }
807
- const federatedNodes = await registerFederatedReference(uri, projectName, nodeRef);
808
- return [
809
- `\u{1F517} **Federated Reference**`,
810
- `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
811
- `\u{1F4C1} Remote project: ${projectName}`,
812
- `\u{1F517} URI: ${uri}`,
813
- `\u{1F4CC} Node ref: ${nodeRef}`,
814
- "",
815
- `\u{1F4DD} Registered as federated reference for future resolution.`,
816
- `\u{1F4A1} Remote graph resolution will be available when the target project is reachable.`,
817
- federatedNodes > 0 ? `\u2705 Created ${federatedNodes} federated node(s) in local graph.` : ""
818
- ].filter(Boolean).join("\n");
819
- } catch (err) {
820
- return `\u274C Federated resolution failed: ${err}`;
821
- }
822
- }
823
- async function registerFederatedReference(uri, projectName, nodeRef) {
824
- try {
825
- await upsertNode({
826
- id: `federated::${uri}`,
827
- type: "module",
828
- name: `[${projectName}] ${nodeRef}`,
829
- metadata: {
830
- federated: true,
831
- uri,
832
- project: projectName,
833
- ref: nodeRef,
834
- resolved: false
835
- }
836
- });
837
- return 1;
838
- } catch {
839
- return 0;
840
- }
841
- }
842
- async function listFederatedReferences() {
843
- try {
844
- const db = await getDb();
845
- const stmt = db.prepare(`
846
- SELECT id, name, metadata FROM nodes
847
- WHERE metadata LIKE '%"federated":true%'
848
- ORDER BY updated_at DESC
849
- LIMIT 20
850
- `);
851
- const results = [];
852
- while (stmt.step()) results.push(stmt.getAsObject());
853
- stmt.free();
854
- if (results.length === 0) {
855
- return "\u{1F517} **No federated references.** Use kuma_memory({ action: 'federated', uri: 'kuma://project/node-id' }) to add one.";
856
- }
857
- const lines = [
858
- "\u{1F517} **Federated References**",
859
- "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
860
- ""
861
- ];
862
- for (const r of results) {
863
- const meta = JSON.parse(r.metadata || "{}");
864
- lines.push(` \u{1F517} **${r.name}**`);
865
- lines.push(` URI: ${meta.uri || "unknown"}`);
866
- lines.push(` Resolved: ${meta.resolved ? "\u2705" : "\u23F3"}`);
867
- lines.push("");
868
- }
869
- return lines.join("\n");
870
- } catch (err) {
871
- return `Error: ${err}`;
872
- }
873
- }
874
- async function getGraphStats() {
875
- try {
876
- const db = await getDb();
877
- const nodeCount = db.exec("SELECT COUNT(*) as c FROM nodes")[0]?.values[0][0] ?? 0;
878
- const edgeCount = db.exec("SELECT COUNT(*) as c FROM edges")[0]?.values[0][0] ?? 0;
879
- const typeCounts = [];
880
- try {
881
- const stmt = db.prepare("SELECT type, COUNT(*) as cnt FROM nodes GROUP BY type ORDER BY cnt DESC");
882
- while (stmt.step()) {
883
- typeCounts.push(stmt.getAsObject());
884
- }
885
- stmt.free();
886
- } catch {
887
- }
888
- const lines = [
889
- `\u{1F4CA} **Knowledge Graph Stats**`,
890
- `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
891
- "",
892
- `\u{1F4CA} **${nodeCount} nodes** | **${edgeCount} edges**`,
893
- ""
894
- ];
895
- if (typeCounts.length > 0) {
896
- lines.push("**Node Types:**");
897
- for (const t of typeCounts) {
898
- const emoji = t.type === "function" ? "\u{1F527}" : t.type === "file" ? "\u{1F4C4}" : t.type === "test" ? "\u{1F9EA}" : t.type === "api_route" ? "\u{1F310}" : t.type === "db_table" ? "\u{1F5C4}\uFE0F" : t.type === "class" ? "\u{1F3D7}\uFE0F" : t.type === "feature_domain" ? "\u{1F3DB}\uFE0F" : t.type === "workflow" ? "\u{1F504}" : t.type === "cross_service_link" ? "\u{1F517}" : "\u{1F4CC}";
899
- lines.push(` ${emoji} ${t.type}: ${t.cnt}`);
900
- }
901
- }
902
- lines.push(
903
- "",
904
- "\u{1F4A1} Nodes are built from research and code changes:",
905
- " \u2022 kuma_context research \u2192 research nodes",
906
- " \u2022 File modifications \u2192 file change edges",
907
- " \u2022 Graph queries \u2192 connection edges"
908
- );
909
- return lines.join("\n");
910
- } catch (err) {
911
- return `Error getting graph stats: ${err}`;
912
- }
913
- }
914
- async function analyzeImpact(target) {
915
- try {
916
- const db = await getDb();
917
- const nodeCount = db.exec("SELECT COUNT(*) as c FROM nodes")[0]?.values[0][0] ?? 0;
918
- if (nodeCount === 0) {
919
- return await codebaseImpactFallback(target);
920
- }
921
- const nodeStmt = db.prepare(`
922
- SELECT id, name, type, file_path FROM nodes
923
- WHERE name LIKE ? OR file_path = ? OR id = ?
924
- LIMIT 1
925
- `);
926
- nodeStmt.bind([`%${target}%`, target, target]);
927
- let nodeId2 = "";
928
- let nodeName = "";
929
- if (nodeStmt.step()) {
930
- const row = nodeStmt.getAsObject();
931
- nodeId2 = row.id;
932
- nodeName = row.name;
933
- }
934
- nodeStmt.free();
935
- if (!nodeId2) {
936
- return await codebaseImpactFallback(target);
937
- }
938
- const refStmt = db.prepare(`
939
- SELECT COUNT(*) as cnt, COUNT(DISTINCT CASE WHEN e.source_id != ? THEN e.source_id ELSE e.target_id END) as file_count
940
- FROM edges e WHERE e.source_id = ? OR e.target_id = ?
941
- `);
942
- refStmt.bind([nodeId2, nodeId2, nodeId2]);
943
- let references = 0;
944
- let files = 0;
945
- if (refStmt.step()) {
946
- const row = refStmt.getAsObject();
947
- references = row.cnt || 0;
948
- files = row.file_count || 0;
949
- }
950
- refStmt.free();
951
- let testFiles = 0;
952
- try {
953
- const testStmt = db.prepare(
954
- `SELECT COUNT(*) as cnt FROM edges e JOIN nodes n ON n.id = e.source_id WHERE (e.source_id = ? OR e.target_id = ?) AND n.type = 'test'`
955
- );
956
- testStmt.bind([nodeId2, nodeId2]);
957
- if (testStmt.step()) {
958
- testFiles = testStmt.getAsObject().cnt || 0;
959
- }
960
- testStmt.free();
961
- } catch {
962
- }
963
- const entryStmt = db.prepare(
964
- `SELECT n.name, COUNT(*) as cnt FROM edges e JOIN nodes n ON n.id = e.target_id WHERE e.target_id = ? GROUP BY n.name ORDER BY cnt DESC LIMIT 5`
965
- );
966
- entryStmt.bind([nodeId2]);
967
- const entryRows = [];
968
- while (entryStmt.step()) {
969
- entryRows.push(entryStmt.getAsObject());
970
- }
971
- entryStmt.free();
972
- const entryPoints = [];
973
- for (const row of entryRows) {
974
- entryPoints.push(`${row.name} (${row.cnt} refs)`);
975
- }
976
- const risk = references > 20 ? "high" : references > 5 ? "medium" : "low";
977
- return { symbol: nodeName, references, files, testFiles, entryPoints, risk };
978
- } catch (err) {
979
- console.error(`[KumaGraph] Impact analysis failed: ${err}`);
980
- return { symbol: target, references: 0, files: 0, testFiles: 0, entryPoints: [], risk: "low" };
981
- }
982
- }
983
- async function codebaseImpactFallback(target) {
984
- try {
985
- const { execSync: execSync2 } = await import("child_process");
986
- const root = process.cwd();
987
- const grepResult = execSync2(
988
- `grep -rn --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.json" -l "${target.replace(/"/g, '\\"')}" . 2>/dev/null | grep -v node_modules | grep -v .git | grep -v dist | grep -v .kuma | head -50`,
989
- { cwd: root, encoding: "utf-8", timeout: 5e3, maxBuffer: 256 * 1024 }
990
- ).trim();
991
- const files = grepResult ? grepResult.split("\n").filter(Boolean).filter((f) => !f.startsWith("./")) : [];
992
- if (files.length === 0) {
993
- return { symbol: target, references: 0, files: 0, testFiles: 0, entryPoints: [], risk: "low" };
994
- }
995
- const fileCount = files.length;
996
- const testFiles = files.filter((f) => f.includes("test") || f.includes("spec") || f.includes("__tests__")).length;
997
- const risk = fileCount > 20 ? "high" : fileCount > 5 ? "medium" : "low";
998
- return {
999
- symbol: target,
1000
- references: fileCount,
1001
- // file-level count since we're approximating
1002
- files: fileCount,
1003
- testFiles,
1004
- entryPoints: files.slice(0, 5).map((f) => `${f} (grep match)`),
1005
- risk
1006
- };
1007
- } catch {
1008
- return { symbol: target, references: 0, files: 0, testFiles: 0, entryPoints: [], risk: "low" };
1009
- }
1010
- }
1011
- function formatImpact(result) {
1012
- const icon = result.risk === "high" ? "\u{1F534}" : result.risk === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
1013
- const lines = [
1014
- `${icon} **Impact Analysis** \u2014 ${result.symbol}`,
1015
- `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1016
- "",
1017
- `\u{1F4CA} ${result.references} reference(s) across ${result.files} file(s)`,
1018
- `\u{1F9EA} ${result.testFiles} test file(s)`,
1019
- result.entryPoints.length > 0 ? `
1020
- \u{1F3AF} Entry Points:
1021
- ${result.entryPoints.join("\n ")}` : "",
1022
- "",
1023
- `\u26A0\uFE0F Risk Level: **${result.risk.toUpperCase()}**`,
1024
- result.risk === "high" ? "\u{1F4A1} High impact \u2014 review thoroughly before changing." : ""
1025
- ].filter(Boolean);
1026
- return lines.join("\n");
1027
- }
1028
- async function traceFlow(entryPoint, maxDepth = 10) {
1029
- const steps = [];
1030
- try {
1031
- const db = await getDb();
1032
- const nodeStmt = db.prepare("SELECT id FROM nodes WHERE name LIKE ? OR id = ? LIMIT 1");
1033
- nodeStmt.bind([`%${entryPoint}%`, entryPoint]);
1034
- let nodeId2 = "";
1035
- if (nodeStmt.step()) {
1036
- nodeId2 = nodeStmt.getAsObject().id;
1037
- }
1038
- nodeStmt.free();
1039
- if (!nodeId2) return steps;
1040
- const visited = /* @__PURE__ */ new Set();
1041
- const queue = [{ id: nodeId2, depth: 0 }];
1042
- visited.add(nodeId2);
1043
- while (queue.length > 0) {
1044
- const current = queue.shift();
1045
- if (current.depth >= maxDepth) continue;
1046
- const edgeStmt = db.prepare(`
1047
- SELECT e.type, n.id, n.name FROM edges e
1048
- JOIN nodes n ON n.id = e.target_id
1049
- WHERE e.source_id = ? AND e.type IN ('calls','routes','imports')
1050
- ORDER BY e.weight DESC LIMIT 10
1051
- `);
1052
- edgeStmt.bind([current.id]);
1053
- while (edgeStmt.step()) {
1054
- const row = edgeStmt.getAsObject();
1055
- const targetId = row.id;
1056
- const targetName = row.name;
1057
- const edgeType = row.type;
1058
- if (!visited.has(targetId)) {
1059
- visited.add(targetId);
1060
- steps.push({ from: current.id, to: targetName, edgeType });
1061
- queue.push({ id: targetId, depth: current.depth + 1 });
1062
- }
1063
- }
1064
- edgeStmt.free();
1065
- }
1066
- return steps;
1067
- } catch (err) {
1068
- console.error(`[KumaGraph] Flow trace failed: ${err}`);
1069
- return steps;
1070
- }
1071
- }
1072
- function formatFlow(entryPoint, steps) {
1073
- if (steps.length === 0) {
1074
- return `\u{1F50D} No flow found for "${entryPoint}". Research the scope first with kuma_context({ action: 'research' }).`;
1075
- }
1076
- const lines = [
1077
- `\u{1F6E4}\uFE0F **Flow: ${entryPoint}**`,
1078
- `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1079
- ""
1080
- ];
1081
- const edgeEmojis = { calls: "\u27A1\uFE0F", routes: "\u{1F310}", imports: "\u{1F4E5}" };
1082
- for (const step of steps) {
1083
- const emoji = edgeEmojis[step.edgeType] || "\u{1F517}";
1084
- lines.push(` ${emoji} ${step.to} (${step.edgeType})`);
1085
- }
1086
- lines.push("", `\u{1F4A1} ${steps.length} step(s) traversed.`);
1087
- return lines.join("\n");
1088
- }
1089
-
1090
- export {
1091
- detectStaleNodes,
1092
- autoHeal,
1093
- formatHealReport,
1094
- nodeId,
1095
- upsertNode,
1096
- addEdge,
1097
- recordFunctionCall,
1098
- recordFileDefinition,
1099
- recordImport,
1100
- recordTestRelation,
1101
- recordApiRoute,
1102
- recordDomainFlow,
1103
- queryGraph,
1104
- buildFromSessionMemory,
1105
- searchGraph,
1106
- resolveFederatedNode,
1107
- listFederatedReferences,
1108
- getGraphStats,
1109
- analyzeImpact,
1110
- formatImpact,
1111
- traceFlow,
1112
- formatFlow
1113
- };