@plumpslabs/kuma 2.4.17 → 2.4.20
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/dist/index.js +117 -10
- package/package.json +1 -1
- package/plugin.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3366,6 +3366,33 @@ async function scanCodebase(options = {}) {
|
|
|
3366
3366
|
}
|
|
3367
3367
|
}
|
|
3368
3368
|
const db = await getDb();
|
|
3369
|
+
const ensureFileNode = async (relPath) => {
|
|
3370
|
+
const fid = nodeId("file", relPath);
|
|
3371
|
+
try {
|
|
3372
|
+
const chk = db.prepare(`SELECT 1 FROM nodes WHERE id = ? LIMIT 1`);
|
|
3373
|
+
chk.bind([fid]);
|
|
3374
|
+
const exists = chk.step();
|
|
3375
|
+
chk.free();
|
|
3376
|
+
if (!exists) {
|
|
3377
|
+
await upsertNode({ id: fid, type: "file", name: relPath, filePath: relPath, metadata: { backbone: true } });
|
|
3378
|
+
result.nodeCount++;
|
|
3379
|
+
}
|
|
3380
|
+
} catch {
|
|
3381
|
+
}
|
|
3382
|
+
return fid;
|
|
3383
|
+
};
|
|
3384
|
+
const nodeExists = (id) => {
|
|
3385
|
+
try {
|
|
3386
|
+
const chk = db.prepare(`SELECT 1 FROM nodes WHERE id = ? LIMIT 1`);
|
|
3387
|
+
chk.bind([id]);
|
|
3388
|
+
const exists = chk.step();
|
|
3389
|
+
chk.free();
|
|
3390
|
+
return exists;
|
|
3391
|
+
} catch {
|
|
3392
|
+
return false;
|
|
3393
|
+
}
|
|
3394
|
+
};
|
|
3395
|
+
const pendingCalls = [];
|
|
3369
3396
|
for (const parsed of parsedFiles) {
|
|
3370
3397
|
const { filePath, symbols, imports, calledSymbols, isTest } = parsed;
|
|
3371
3398
|
const fileId = nodeId("file", filePath);
|
|
@@ -3421,14 +3448,16 @@ async function scanCodebase(options = {}) {
|
|
|
3421
3448
|
if (sym.extends) {
|
|
3422
3449
|
const parentFile = symbolMap.get(sym.extends) || filePath;
|
|
3423
3450
|
const parentId = `class::${parentFile}::${sym.extends}`;
|
|
3424
|
-
|
|
3425
|
-
|
|
3451
|
+
if (nodeExists(parentId)) {
|
|
3452
|
+
await addEdge({ sourceId: symId, targetId: parentId, type: "extends" });
|
|
3453
|
+
result.edgeCount++;
|
|
3454
|
+
}
|
|
3426
3455
|
}
|
|
3427
3456
|
}
|
|
3428
3457
|
for (const imp of imports) {
|
|
3429
3458
|
const resolved = resolveImportPath(filePath, imp.source, root);
|
|
3430
3459
|
if (resolved) {
|
|
3431
|
-
const targetFileId =
|
|
3460
|
+
const targetFileId = await ensureFileNode(resolved);
|
|
3432
3461
|
await addEdge({
|
|
3433
3462
|
sourceId: fileId,
|
|
3434
3463
|
targetId: targetFileId,
|
|
@@ -3449,17 +3478,17 @@ async function scanCodebase(options = {}) {
|
|
|
3449
3478
|
for (const callee of calledSymbols) {
|
|
3450
3479
|
const calleeFile = symbolMap.get(callee);
|
|
3451
3480
|
if (calleeFile && calleeFile !== filePath) {
|
|
3452
|
-
|
|
3453
|
-
await addEdge({
|
|
3454
|
-
sourceId: fileId,
|
|
3455
|
-
targetId: calleeId,
|
|
3456
|
-
type: "calls"
|
|
3457
|
-
});
|
|
3458
|
-
result.edgeCount++;
|
|
3481
|
+
pendingCalls.push({ fileId, callee, calleeFile, filePath });
|
|
3459
3482
|
}
|
|
3460
3483
|
}
|
|
3461
3484
|
result.filesScanned++;
|
|
3462
3485
|
}
|
|
3486
|
+
for (const p of pendingCalls) {
|
|
3487
|
+
const calleeId = `function::${p.calleeFile}::${p.callee}`;
|
|
3488
|
+
if (!nodeExists(calleeId)) continue;
|
|
3489
|
+
await addEdge({ sourceId: p.fileId, targetId: calleeId, type: "calls" });
|
|
3490
|
+
result.edgeCount++;
|
|
3491
|
+
}
|
|
3463
3492
|
saveDb(db);
|
|
3464
3493
|
return result;
|
|
3465
3494
|
}
|
|
@@ -5771,6 +5800,7 @@ __export(kumaGraph_exports, {
|
|
|
5771
5800
|
getGraphStats: () => getGraphStats,
|
|
5772
5801
|
nodeId: () => nodeId,
|
|
5773
5802
|
propagateImpact: () => propagateImpact,
|
|
5803
|
+
pruneOrphans: () => pruneOrphans,
|
|
5774
5804
|
queryGraph: () => queryGraph,
|
|
5775
5805
|
recordAffects: () => recordAffects,
|
|
5776
5806
|
recordApiRoute: () => recordApiRoute,
|
|
@@ -5847,6 +5877,48 @@ async function pruneGraphIfNeeded() {
|
|
|
5847
5877
|
return { pruned: false, removedNodes: 0, removedEdges: 0 };
|
|
5848
5878
|
}
|
|
5849
5879
|
}
|
|
5880
|
+
async function pruneOrphans(opts = {}) {
|
|
5881
|
+
const olderThanSec = opts.olderThanSec ?? 3600;
|
|
5882
|
+
const dryRun = opts.dryRun ?? false;
|
|
5883
|
+
let orphanNodes = 0;
|
|
5884
|
+
let danglingEdges = 0;
|
|
5885
|
+
try {
|
|
5886
|
+
const db = await getDb();
|
|
5887
|
+
const cutoff = Math.floor(Date.now() / 1e3) - olderThanSec;
|
|
5888
|
+
const dangling = db.exec(`
|
|
5889
|
+
SELECT COUNT(*) FROM edges e
|
|
5890
|
+
WHERE NOT EXISTS (SELECT 1 FROM nodes n WHERE n.id = e.target_id)
|
|
5891
|
+
OR NOT EXISTS (SELECT 1 FROM nodes n WHERE n.id = e.source_id)
|
|
5892
|
+
`);
|
|
5893
|
+
danglingEdges = Number(dangling[0]?.values?.[0]?.[0]) || 0;
|
|
5894
|
+
if (!dryRun && danglingEdges > 0) {
|
|
5895
|
+
db.exec(`
|
|
5896
|
+
DELETE FROM edges
|
|
5897
|
+
WHERE NOT EXISTS (SELECT 1 FROM nodes n WHERE n.id = edges.target_id)
|
|
5898
|
+
OR NOT EXISTS (SELECT 1 FROM nodes n WHERE n.id = edges.source_id)
|
|
5899
|
+
`);
|
|
5900
|
+
}
|
|
5901
|
+
const orphans = db.exec(`
|
|
5902
|
+
SELECT COUNT(*) FROM nodes n
|
|
5903
|
+
WHERE n.type IN ('file','function','class','method','interface','test','feature_domain','arch_flow')
|
|
5904
|
+
AND n.updated_at < ${cutoff}
|
|
5905
|
+
AND NOT EXISTS (SELECT 1 FROM edges e WHERE e.source_id = n.id OR e.target_id = n.id)
|
|
5906
|
+
`);
|
|
5907
|
+
orphanNodes = Number(orphans[0]?.values?.[0]?.[0]) || 0;
|
|
5908
|
+
if (!dryRun && orphanNodes > 0) {
|
|
5909
|
+
db.exec(`
|
|
5910
|
+
DELETE FROM nodes
|
|
5911
|
+
WHERE type IN ('file','function','class','method','interface','test','feature_domain','arch_flow')
|
|
5912
|
+
AND updated_at < ${cutoff}
|
|
5913
|
+
AND id NOT IN (SELECT source_id FROM edges UNION SELECT target_id FROM edges)
|
|
5914
|
+
`);
|
|
5915
|
+
}
|
|
5916
|
+
if (!dryRun && (orphanNodes > 0 || danglingEdges > 0)) saveDb(db);
|
|
5917
|
+
} catch (err) {
|
|
5918
|
+
console.error(`[KumaGraph] pruneOrphans failed: ${err}`);
|
|
5919
|
+
}
|
|
5920
|
+
return { orphanNodes, danglingEdges };
|
|
5921
|
+
}
|
|
5850
5922
|
async function upsertNode(node) {
|
|
5851
5923
|
try {
|
|
5852
5924
|
const db = await getDb();
|
|
@@ -5961,6 +6033,9 @@ async function recordApiRoute(route, handler) {
|
|
|
5961
6033
|
async function recordDomainFlow(params) {
|
|
5962
6034
|
let nodeCount = 0;
|
|
5963
6035
|
let edgeCount = 0;
|
|
6036
|
+
if (!params.hops || params.hops.length === 0) {
|
|
6037
|
+
return { nodeCount, edgeCount };
|
|
6038
|
+
}
|
|
5964
6039
|
try {
|
|
5965
6040
|
const domainId = nodeId("feature_domain", params.domain);
|
|
5966
6041
|
let fileHashes = {};
|
|
@@ -11397,6 +11472,30 @@ function opencodePluginTemplate() {
|
|
|
11397
11472
|
" },",
|
|
11398
11473
|
" };",
|
|
11399
11474
|
"};",
|
|
11475
|
+
"",
|
|
11476
|
+
"// Server-runtime definition (required by newer OpenCode server).",
|
|
11477
|
+
"// Reuses the same hooks via the named export above.",
|
|
11478
|
+
"const kumaHooks = {",
|
|
11479
|
+
' "tool.execute.before": async (input, output) => {',
|
|
11480
|
+
" const hooks = await KumaPlugin();",
|
|
11481
|
+
' return hooks["tool.execute.before"](input, output);',
|
|
11482
|
+
" },",
|
|
11483
|
+
' "file.edited": async (input) => {',
|
|
11484
|
+
" const hooks = await KumaPlugin();",
|
|
11485
|
+
' return hooks["file.edited"](input);',
|
|
11486
|
+
" },",
|
|
11487
|
+
' "experimental.session.compacting": async (input, output) => {',
|
|
11488
|
+
" const hooks = await KumaPlugin();",
|
|
11489
|
+
' return hooks["experimental.session.compacting"](input, output);',
|
|
11490
|
+
" },",
|
|
11491
|
+
"};",
|
|
11492
|
+
"",
|
|
11493
|
+
"export default {",
|
|
11494
|
+
' id: "kuma",',
|
|
11495
|
+
" // NOTE: setup must return hooks synchronously \u2014 never an async",
|
|
11496
|
+
" // effect/event-loop. An async effect never resolves and hangs load.",
|
|
11497
|
+
" setup: () => kumaHooks,",
|
|
11498
|
+
"};",
|
|
11400
11499
|
""
|
|
11401
11500
|
].join("\n");
|
|
11402
11501
|
}
|
|
@@ -12935,6 +13034,14 @@ async function daemonTick(root) {
|
|
|
12935
13034
|
notes.push(`drift-report: ${items} item(s) \u2192 .kuma/drift.json`);
|
|
12936
13035
|
} catch {
|
|
12937
13036
|
}
|
|
13037
|
+
try {
|
|
13038
|
+
const { pruneOrphans: pruneOrphans2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
|
|
13039
|
+
const { orphanNodes, danglingEdges } = await pruneOrphans2({ olderThanSec: 3600 });
|
|
13040
|
+
if (orphanNodes > 0 || danglingEdges > 0) {
|
|
13041
|
+
notes.push(`orphans: -${orphanNodes}n/-${danglingEdges}e pruned`);
|
|
13042
|
+
}
|
|
13043
|
+
} catch {
|
|
13044
|
+
}
|
|
12938
13045
|
try {
|
|
12939
13046
|
const { clearDirty: clearDirty2 } = await Promise.resolve().then(() => (init_cacheFreshness(), cacheFreshness_exports));
|
|
12940
13047
|
clearDirty2();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plumpslabs/kuma",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.20",
|
|
4
4
|
"description": "Safety-first context & orchestration engine for AI coding agents. MCP server with mandatory research pipeline, knowledge graph, impact analysis, decision memory, and safety guard — works with any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
|
|
3
3
|
"name": "kuma",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.20",
|
|
5
5
|
"description": "Safety-first context & orchestration engine for AI coding agents. Shadow memory, gotcha lifecycle, blast radius impact analysis, and test verification.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "plumpslabs",
|