@plumpslabs/kuma 2.4.10 → 2.4.12
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/README.md +7 -4
- package/dist/index.js +2980 -1766
- package/package.json +3 -3
- package/packages/ide/studio/dist/index.js +209 -4
- package/packages/ide/studio/public/index.html +925 -142
- package/plugin.json +1 -1
- package/skills/kuma/SKILL.md +80 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plumpslabs/kuma",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.12",
|
|
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",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.7.0",
|
|
28
28
|
"fast-glob": "^3.3.2",
|
|
29
29
|
"sql.js": "^1.14.1",
|
|
30
|
+
"typescript": "^5.9.3",
|
|
30
31
|
"zod": "^3.24.0"
|
|
31
32
|
},
|
|
32
33
|
"optionalDependencies": {
|
|
@@ -40,8 +41,7 @@
|
|
|
40
41
|
"jest-mock": "^30.4.1",
|
|
41
42
|
"ts-jest": "^29.2.0",
|
|
42
43
|
"ts-node": "^10.9.2",
|
|
43
|
-
"tsup": "^8.3.0"
|
|
44
|
-
"typescript": "^5.9.3"
|
|
44
|
+
"tsup": "^8.3.0"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=18.0.0"
|
|
@@ -73,12 +73,51 @@ function readSessionMetrics(dbPath) {
|
|
|
73
73
|
return empty;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
function searchByBasename(dir, baseName, maxDepth) {
|
|
77
|
+
if (maxDepth < 0) return null;
|
|
78
|
+
try {
|
|
79
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
|
|
82
|
+
const full = path.join(dir, entry.name);
|
|
83
|
+
if (entry.isFile() && entry.name.toLowerCase() === baseName.toLowerCase()) return full;
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
const found = searchByBasename(full, baseName, maxDepth - 1);
|
|
86
|
+
if (found) return found;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function findFileInMonorepo(root, targetPath) {
|
|
94
|
+
const direct = path.join(root, targetPath);
|
|
95
|
+
if (fs.existsSync(direct) && fs.statSync(direct).isFile()) return direct;
|
|
96
|
+
const targetBase = path.basename(targetPath);
|
|
97
|
+
try {
|
|
98
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
99
|
+
for (const ent of entries) {
|
|
100
|
+
if (ent.isDirectory() && !ent.name.startsWith(".") && ent.name !== "node_modules" && ent.name !== "dist" && ent.name !== "build") {
|
|
101
|
+
const dirPath = path.join(root, ent.name);
|
|
102
|
+
const relCand = path.join(dirPath, targetPath);
|
|
103
|
+
if (fs.existsSync(relCand) && fs.statSync(relCand).isFile()) return relCand;
|
|
104
|
+
const relSrcCand = path.join(dirPath, "src", targetPath);
|
|
105
|
+
if (fs.existsSync(relSrcCand) && fs.statSync(relSrcCand).isFile()) return relSrcCand;
|
|
106
|
+
const found = searchByBasename(dirPath, targetBase, 3);
|
|
107
|
+
if (found) return found;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
76
114
|
function detectStaleNodes(db, dbPath) {
|
|
77
115
|
try {
|
|
78
116
|
const root = projectRootFromDb(dbPath);
|
|
79
117
|
const stmt = db.prepare(
|
|
80
|
-
`SELECT id, file_path, metadata, COUNT(*) as cnt FROM nodes
|
|
118
|
+
`SELECT id, file_path, type, metadata, COUNT(*) as cnt FROM nodes
|
|
81
119
|
WHERE file_path IS NOT NULL AND file_path != '' AND file_path NOT LIKE '%::%'
|
|
120
|
+
AND type NOT IN ('decision', 'arch_flow', 'feature_domain', 'feature', 'workflow', 'cross_service_link', 'research')
|
|
82
121
|
GROUP BY file_path ORDER BY cnt DESC LIMIT 500`
|
|
83
122
|
);
|
|
84
123
|
const missing = [];
|
|
@@ -86,10 +125,19 @@ function detectStaleNodes(db, dbPath) {
|
|
|
86
125
|
let staleNodes = 0;
|
|
87
126
|
while (stmt.step()) {
|
|
88
127
|
const row = stmt.getAsObject();
|
|
89
|
-
const fp = row.file_path;
|
|
128
|
+
const fp = (row.file_path || "").trim();
|
|
90
129
|
const metadata = row.metadata || "{}";
|
|
130
|
+
if (!fp || /\s/.test(fp) || !path.extname(fp) || fp.includes("::")) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
91
133
|
checked++;
|
|
92
|
-
|
|
134
|
+
let fullPath = path.join(root, fp);
|
|
135
|
+
if (!fs.existsSync(fullPath)) {
|
|
136
|
+
const monorepoPath = findFileInMonorepo(root, fp);
|
|
137
|
+
if (monorepoPath) {
|
|
138
|
+
fullPath = monorepoPath;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
93
141
|
if (!fs.existsSync(fullPath)) {
|
|
94
142
|
const count = Number(row.cnt ?? 1);
|
|
95
143
|
missing.push({ filePath: fp, nodeCount: count, reason: "file_missing" });
|
|
@@ -343,7 +391,118 @@ async function getNodeDetail(nodeId) {
|
|
|
343
391
|
return null;
|
|
344
392
|
}
|
|
345
393
|
}).filter(Boolean);
|
|
346
|
-
|
|
394
|
+
const inDegree = incoming.length;
|
|
395
|
+
const outDegree = outgoing.length;
|
|
396
|
+
const isHub = inDegree >= 4;
|
|
397
|
+
return { node, outgoing, incoming, gotchas, centrality: { inDegree, outDegree, isHub } };
|
|
398
|
+
} finally {
|
|
399
|
+
db.close();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function saveDbToDisk(db) {
|
|
403
|
+
const dbPath = findKumaDb();
|
|
404
|
+
if (!dbPath) throw new Error("No .kuma/kuma.db found");
|
|
405
|
+
const data = db.export();
|
|
406
|
+
const buffer = Buffer.from(data);
|
|
407
|
+
const tmpPath = `${dbPath}.tmp.${process.pid}.${Date.now()}`;
|
|
408
|
+
fs.writeFileSync(tmpPath, buffer);
|
|
409
|
+
try {
|
|
410
|
+
fs.renameSync(tmpPath, dbPath);
|
|
411
|
+
} catch {
|
|
412
|
+
fs.writeFileSync(dbPath, buffer);
|
|
413
|
+
try {
|
|
414
|
+
fs.unlinkSync(tmpPath);
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async function upsertStudioNode(data) {
|
|
420
|
+
const db = await openDb();
|
|
421
|
+
try {
|
|
422
|
+
const type = data.type || "feature_domain";
|
|
423
|
+
const name = (data.name || "").trim();
|
|
424
|
+
const filePath = (data.file_path || "").trim();
|
|
425
|
+
let id = data.id;
|
|
426
|
+
if (!id) {
|
|
427
|
+
if (type === "file" && filePath) {
|
|
428
|
+
id = `file::${filePath}`;
|
|
429
|
+
} else if (type === "function" && filePath) {
|
|
430
|
+
id = `function::${filePath}::${name}`;
|
|
431
|
+
} else {
|
|
432
|
+
id = `${type}::${name}`;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const meta = data.metadata || {};
|
|
436
|
+
if (data.description) {
|
|
437
|
+
meta.description = data.description;
|
|
438
|
+
}
|
|
439
|
+
const metaStr = JSON.stringify(meta);
|
|
440
|
+
db.run(
|
|
441
|
+
`INSERT INTO nodes (id, type, name, file_path, metadata, severity, confidence, updated_at)
|
|
442
|
+
VALUES (?, ?, ?, ?, ?, 'medium', 0.8, strftime('%s','now'))
|
|
443
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
444
|
+
type = excluded.type,
|
|
445
|
+
name = excluded.name,
|
|
446
|
+
file_path = COALESCE(excluded.file_path, nodes.file_path),
|
|
447
|
+
metadata = excluded.metadata,
|
|
448
|
+
updated_at = strftime('%s','now')`,
|
|
449
|
+
[id, type, name, filePath || null, metaStr]
|
|
450
|
+
);
|
|
451
|
+
try {
|
|
452
|
+
const terms = `${name} ${filePath} ${metaStr}`.toLowerCase().split(/[\s,.;:!?()\[\]{}"'\/\\|@#$%^&*+=<>~`_-]+/).filter((t) => t.length > 2 && t.length < 40);
|
|
453
|
+
const uniqueTerms = Array.from(new Set(terms));
|
|
454
|
+
db.run(`DELETE FROM node_tokens WHERE node_id = ?`, [id]);
|
|
455
|
+
for (const term of uniqueTerms.slice(0, 30)) {
|
|
456
|
+
db.run(`INSERT OR IGNORE INTO node_tokens (token, node_id) VALUES (?, ?)`, [term, id]);
|
|
457
|
+
}
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
saveDbToDisk(db);
|
|
461
|
+
return { id, type, name, file_path: filePath, metadata: meta };
|
|
462
|
+
} finally {
|
|
463
|
+
db.close();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function deleteStudioNode(id) {
|
|
467
|
+
const db = await openDb();
|
|
468
|
+
try {
|
|
469
|
+
db.run(`DELETE FROM nodes WHERE id = ?`, [id]);
|
|
470
|
+
db.run(`DELETE FROM edges WHERE source_id = ? OR target_id = ?`, [id, id]);
|
|
471
|
+
try {
|
|
472
|
+
db.run(`DELETE FROM node_tokens WHERE node_id = ?`, [id]);
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
saveDbToDisk(db);
|
|
476
|
+
} finally {
|
|
477
|
+
db.close();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async function createStudioEdge(edge) {
|
|
481
|
+
const db = await openDb();
|
|
482
|
+
try {
|
|
483
|
+
const weight = edge.weight ?? 1;
|
|
484
|
+
const metadata = JSON.stringify(edge.metadata ?? {});
|
|
485
|
+
db.run(
|
|
486
|
+
`INSERT INTO edges (source_id, target_id, type, weight, metadata)
|
|
487
|
+
VALUES (?, ?, ?, ?, ?)
|
|
488
|
+
ON CONFLICT(source_id, target_id, type) DO UPDATE SET
|
|
489
|
+
weight = excluded.weight,
|
|
490
|
+
metadata = excluded.metadata`,
|
|
491
|
+
[edge.source, edge.target, edge.type, weight, metadata]
|
|
492
|
+
);
|
|
493
|
+
saveDbToDisk(db);
|
|
494
|
+
} finally {
|
|
495
|
+
db.close();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function deleteStudioEdge(source, target, type) {
|
|
499
|
+
const db = await openDb();
|
|
500
|
+
try {
|
|
501
|
+
db.run(
|
|
502
|
+
`DELETE FROM edges WHERE source_id = ? AND target_id = ? AND type = ?`,
|
|
503
|
+
[source, target, type]
|
|
504
|
+
);
|
|
505
|
+
saveDbToDisk(db);
|
|
347
506
|
} finally {
|
|
348
507
|
db.close();
|
|
349
508
|
}
|
|
@@ -409,6 +568,52 @@ api.get("/node/:id", async (c) => {
|
|
|
409
568
|
return c.json({ error: e.message }, 500);
|
|
410
569
|
}
|
|
411
570
|
});
|
|
571
|
+
api.post("/node", async (c) => {
|
|
572
|
+
try {
|
|
573
|
+
const body = await c.req.json();
|
|
574
|
+
if (!body || !body.name || !body.type) {
|
|
575
|
+
return c.json({ error: "Missing required fields: name and type are required" }, 400);
|
|
576
|
+
}
|
|
577
|
+
const node = await upsertStudioNode(body);
|
|
578
|
+
return c.json({ ok: true, node });
|
|
579
|
+
} catch (e) {
|
|
580
|
+
return c.json({ error: e.message }, 500);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
api.delete("/node/:id", async (c) => {
|
|
584
|
+
try {
|
|
585
|
+
const id = c.req.param("id");
|
|
586
|
+
if (!id) return c.json({ error: "Node id required" }, 400);
|
|
587
|
+
await deleteStudioNode(id);
|
|
588
|
+
return c.json({ ok: true, id });
|
|
589
|
+
} catch (e) {
|
|
590
|
+
return c.json({ error: e.message }, 500);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
api.post("/edge", async (c) => {
|
|
594
|
+
try {
|
|
595
|
+
const body = await c.req.json();
|
|
596
|
+
if (!body || !body.source || !body.target || !body.type) {
|
|
597
|
+
return c.json({ error: "Missing required fields: source, target, and type are required" }, 400);
|
|
598
|
+
}
|
|
599
|
+
await createStudioEdge(body);
|
|
600
|
+
return c.json({ ok: true, edge: body });
|
|
601
|
+
} catch (e) {
|
|
602
|
+
return c.json({ error: e.message }, 500);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
api.delete("/edge", async (c) => {
|
|
606
|
+
try {
|
|
607
|
+
const body = await c.req.json();
|
|
608
|
+
if (!body || !body.source || !body.target || !body.type) {
|
|
609
|
+
return c.json({ error: "Missing required fields: source, target, and type are required" }, 400);
|
|
610
|
+
}
|
|
611
|
+
await deleteStudioEdge(body.source, body.target, body.type);
|
|
612
|
+
return c.json({ ok: true, edge: body });
|
|
613
|
+
} catch (e) {
|
|
614
|
+
return c.json({ error: e.message }, 500);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
412
617
|
api.post("/reset-db", (c) => {
|
|
413
618
|
const blocked = confirmGuard(c);
|
|
414
619
|
if (blocked) return blocked;
|