@plumpslabs/kuma 2.3.24 → 2.3.25
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 +2 -0
- package/dist/index.js +124 -27
- package/package.json +1 -1
- package/packages/ide/studio/public/index.html +49 -8
package/README.md
CHANGED
|
@@ -142,6 +142,8 @@ Kuma consolidates everything into **3 coarse-grained tools**. Each action trigge
|
|
|
142
142
|
| `arch_flow` | Layer 2 — Architecture flow mapping (Issue #17). |
|
|
143
143
|
| `gotcha` | Layer 3 — Known gotchas & anti-regression shield (Issue #17/#21). |
|
|
144
144
|
| `layers` | Show all 3 memory layers summary. |
|
|
145
|
+
| `delete_node` | Delete specific node or record from disk & RAM immediately. |
|
|
146
|
+
| `graph_health` | Monitor knowledge graph health, orphan nodes, and duplicate groups. |
|
|
145
147
|
|
|
146
148
|
### 🛡️ `kuma_safety` — Safety & Policy
|
|
147
149
|
|
package/dist/index.js
CHANGED
|
@@ -2558,7 +2558,7 @@ async function recordDomainFlow(params) {
|
|
|
2558
2558
|
await addEdge({
|
|
2559
2559
|
sourceId: fromId,
|
|
2560
2560
|
targetId: toId,
|
|
2561
|
-
type: "
|
|
2561
|
+
type: "depends_on",
|
|
2562
2562
|
weight: 0.8,
|
|
2563
2563
|
metadata: { reason: "arch_flow", domain: params.domain }
|
|
2564
2564
|
});
|
|
@@ -8196,19 +8196,25 @@ function generateInitMdContent() {
|
|
|
8196
8196
|
"- `status` \u2014 severity: `low` | `medium` | `high` | `critical` (default: `medium`)",
|
|
8197
8197
|
"- `description` \u2014 optional workaround/fix description",
|
|
8198
8198
|
"",
|
|
8199
|
-
"### STEP 7: Record Arch Flow (RECORD
|
|
8199
|
+
"### STEP 7: Record Arch Flow (RECORD COMPLETE FLOW \u2014 Core Logic Only)",
|
|
8200
8200
|
"```",
|
|
8201
|
-
'kuma_memory({ action: "arch_flow", content: "domain: <name> | hops: <
|
|
8201
|
+
'kuma_memory({ action: "arch_flow", content: "domain: <name> | hops: <core_file1> \u2192 <core_file2> \u2192 <core_file3>" })',
|
|
8202
8202
|
"```",
|
|
8203
|
-
"**When:**
|
|
8204
|
-
"**Why:**
|
|
8203
|
+
"**When:** AFTER tracing a COMPLETE architecture flow (not per hop).",
|
|
8204
|
+
"**Why:** Record flow architecture for future sessions without cluttering the graph.",
|
|
8205
|
+
"",
|
|
8206
|
+
"**Rules:**",
|
|
8207
|
+
"1. MAX 5 files per flow \u2014 record core business logic files only.",
|
|
8208
|
+
"2. SKIP UI components (*.tsx, *.jsx), Controllers (*Controller.js), Schemas (*Schema.js), and simple CRUD services.",
|
|
8209
|
+
"3. INCLUDE files with complex business logic, decision points, state mutations, or error handling.",
|
|
8210
|
+
"4. Format hops separated by ` \u2192 ` (e.g. `domain: LeadDistributionFlow | hops: LeadRotationService.js \u2192 ProspectAssignmentService.js \u2192 ActivityLogService.js`).",
|
|
8205
8211
|
"",
|
|
8206
8212
|
"**\u26A0\uFE0F CRITICAL FORMAT \u2014 Content MUST use this exact syntax:**",
|
|
8207
8213
|
"```",
|
|
8208
|
-
"domain: <DomainName> | hops: <
|
|
8214
|
+
"domain: <DomainName> | hops: <core_file1.js> \u2192 <core_file2.js> \u2192 <core_file3.js> | gotchas: <optional> | decisions: <optional>",
|
|
8209
8215
|
"```",
|
|
8210
8216
|
"- `domain:` \u2014 short domain name (e.g. `InviteUserFlow`, `LeadManagementFlow`)",
|
|
8211
|
-
"- `hops:` \u2014 file names separated by ` \u2192 `
|
|
8217
|
+
"- `hops:` \u2014 max 5 core file names separated by ` \u2192 `",
|
|
8212
8218
|
"- `gotchas:` \u2014 optional comma-separated gotchas",
|
|
8213
8219
|
"- `decisions:` \u2014 optional comma-separated decisions",
|
|
8214
8220
|
"",
|
|
@@ -10377,6 +10383,11 @@ var MEMORY_ALIASES = {
|
|
|
10377
10383
|
"read": "session",
|
|
10378
10384
|
"fetch": "session",
|
|
10379
10385
|
"current": "session",
|
|
10386
|
+
// Graph health synonyms
|
|
10387
|
+
"graph_health": "graph_health",
|
|
10388
|
+
"graph-health": "graph_health",
|
|
10389
|
+
"health-check": "graph_health",
|
|
10390
|
+
"graph-status": "graph_health",
|
|
10380
10391
|
// Decision synonyms
|
|
10381
10392
|
"decision": "decision",
|
|
10382
10393
|
"adr": "decision",
|
|
@@ -10408,7 +10419,6 @@ var MEMORY_ALIASES = {
|
|
|
10408
10419
|
"task": "todo",
|
|
10409
10420
|
"todos": "todo",
|
|
10410
10421
|
"tasks": "todo",
|
|
10411
|
-
// Context notes synonyms
|
|
10412
10422
|
"context": "context",
|
|
10413
10423
|
"notes": "context",
|
|
10414
10424
|
"note": "context",
|
|
@@ -10513,6 +10523,8 @@ async function handleMemory(params) {
|
|
|
10513
10523
|
return handleFeature(params);
|
|
10514
10524
|
case "delete_node":
|
|
10515
10525
|
return handleDeleteNode(params);
|
|
10526
|
+
case "graph_health":
|
|
10527
|
+
return handleGraphHealth(params);
|
|
10516
10528
|
case "clear": {
|
|
10517
10529
|
const { clearGraph: clearGraph2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
|
|
10518
10530
|
await clearGraph2();
|
|
@@ -10564,14 +10576,26 @@ async function handleResearchSave(params) {
|
|
|
10564
10576
|
});
|
|
10565
10577
|
await saveResearchCache(scope, record, void 0, params.confidence);
|
|
10566
10578
|
try {
|
|
10567
|
-
const {
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10579
|
+
const { getDb: getDb2, flushDb: flushDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
10580
|
+
const db = await getDb2();
|
|
10581
|
+
const fileId = `file::${scope}`;
|
|
10582
|
+
const researchId = `research::${scope}`;
|
|
10583
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
10584
|
+
db.run(`
|
|
10585
|
+
INSERT INTO nodes (id, type, name, file_path, metadata, updated_at)
|
|
10586
|
+
VALUES (?, 'file', ?, ?, ?, ?)
|
|
10587
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
10588
|
+
metadata = json_patch(COALESCE(nodes.metadata, '{}'), excluded.metadata),
|
|
10589
|
+
updated_at = excluded.updated_at
|
|
10590
|
+
`, [fileId, scope, scope, JSON.stringify({ findings: [params.content || ""], confidence: params.confidence || 0.8 }), now]);
|
|
10591
|
+
db.run(`
|
|
10592
|
+
INSERT INTO nodes (id, type, name, file_path, metadata, updated_at)
|
|
10593
|
+
VALUES (?, 'research', ?, null, ?, ?)
|
|
10594
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
10595
|
+
metadata = json_patch(COALESCE(nodes.metadata, '{}'), excluded.metadata),
|
|
10596
|
+
updated_at = excluded.updated_at
|
|
10597
|
+
`, [researchId, `research:${scope}`, JSON.stringify({ confidence: params.confidence || 0.8, last_findings: params.content || "" }), now]);
|
|
10598
|
+
flushDb2(db);
|
|
10575
10599
|
} catch {
|
|
10576
10600
|
}
|
|
10577
10601
|
try {
|
|
@@ -10787,12 +10811,20 @@ async function handleLayerAction(layer, params) {
|
|
|
10787
10811
|
\u274C Wrong: hops: file1.js\\nfile2.js
|
|
10788
10812
|
\u2705 Correct: domain: <name> | hops: file1.js \u2192 file2.js \u2192 file3.js`;
|
|
10789
10813
|
}
|
|
10790
|
-
const
|
|
10814
|
+
const rawHops = hopsStr ? hopsStr.split("\u2192").map((h) => h.trim()).filter(Boolean) : [];
|
|
10815
|
+
const coreHops = rawHops.filter((file) => {
|
|
10816
|
+
if (file.endsWith(".tsx") || file.endsWith(".jsx")) return false;
|
|
10817
|
+
if (file.includes("Controller")) return false;
|
|
10818
|
+
if (file.includes("Schema") || file.includes("RequestSchema")) return false;
|
|
10819
|
+
return true;
|
|
10820
|
+
});
|
|
10821
|
+
const limitedHopsList = coreHops.slice(0, 5);
|
|
10822
|
+
const hops = limitedHopsList.map((h, i, arr) => ({
|
|
10791
10823
|
from: i === 0 ? domain : arr[i - 1],
|
|
10792
10824
|
to: h,
|
|
10793
10825
|
relation: "flows",
|
|
10794
10826
|
description: h
|
|
10795
|
-
}))
|
|
10827
|
+
}));
|
|
10796
10828
|
if (hops.length === 0) {
|
|
10797
10829
|
return `\u274C **arch_flow format error:** no hops found after \u2192 separator.
|
|
10798
10830
|
|
|
@@ -10896,11 +10928,12 @@ async function handleDeleteNode(params) {
|
|
|
10896
10928
|
return `\u26A0\uFE0F Unknown scope "${params.scope}". Supported: gotcha, todo, decision, checkpoint`;
|
|
10897
10929
|
}
|
|
10898
10930
|
}
|
|
10899
|
-
if (params.target) {
|
|
10931
|
+
if (params.target || params.scope) {
|
|
10900
10932
|
const { flushDb: flushDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
10901
|
-
|
|
10933
|
+
const targetStr = params.target || "";
|
|
10934
|
+
const targetId = params.scope && !targetStr.includes("::") ? `${targetStr}::${params.scope}` : targetStr;
|
|
10935
|
+
if (targetId && (targetId.includes("::") || targetId.includes(":") || targetId.includes("-") || isNaN(parseInt(targetId, 10)))) {
|
|
10902
10936
|
try {
|
|
10903
|
-
const targetId = params.target;
|
|
10904
10937
|
const domainName = targetId.startsWith("feature_domain::") ? targetId.replace("feature_domain::", "") : targetId;
|
|
10905
10938
|
if (targetId.startsWith("feature_domain::")) {
|
|
10906
10939
|
db.run("DELETE FROM nodes WHERE id LIKE ? OR id LIKE ? OR id LIKE ?", [
|
|
@@ -10914,16 +10947,18 @@ async function handleDeleteNode(params) {
|
|
|
10914
10947
|
db.run("DELETE FROM nodes WHERE id = ?", [targetId]);
|
|
10915
10948
|
}
|
|
10916
10949
|
flushDb2(db);
|
|
10917
|
-
return `\u{1F5D1}\uFE0F **Node & relations deleted instantly from RAM and Disk:** ${
|
|
10950
|
+
return `\u{1F5D1}\uFE0F **Node & relations deleted instantly from RAM and Disk:** ${targetId}`;
|
|
10918
10951
|
} catch (err) {
|
|
10919
10952
|
return `\u274C Failed to delete node: ${err}`;
|
|
10920
10953
|
}
|
|
10921
10954
|
}
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
10955
|
+
if (targetId) {
|
|
10956
|
+
const id = parseInt(targetId, 10);
|
|
10957
|
+
if (!isNaN(id)) {
|
|
10958
|
+
db.run("DELETE FROM nodes WHERE rowid = ?", [id]);
|
|
10959
|
+
flushDb2(db);
|
|
10960
|
+
return `\u{1F5D1}\uFE0F **Node #${id} deleted.**`;
|
|
10961
|
+
}
|
|
10927
10962
|
}
|
|
10928
10963
|
}
|
|
10929
10964
|
return "\u26A0\uFE0F Provide `target` (node ID) to delete, or `scope` + `target` (numeric ID) for gotcha/todo/decision/checkpoint.\n\nExamples:\n- `delete_node`, target: 'function::sendMessage'\n- `delete_node`, scope: 'gotcha', target: '42'\n- `delete_node`, scope: 'todo', target: '7'\n- `delete_node`, scope: 'decision', target: '3'\n- `delete_node`, scope: 'checkpoint', target: '1'";
|
|
@@ -10984,6 +11019,68 @@ async function handleFeature(params) {
|
|
|
10984
11019
|
\u{1F3F7}\uFE0F Tags: ${tags.length}
|
|
10985
11020
|
\u26A0\uFE0F Risk: ${risk}`;
|
|
10986
11021
|
}
|
|
11022
|
+
async function handleGraphHealth(_params) {
|
|
11023
|
+
const { getDb: getDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
|
|
11024
|
+
const db = await getDb2();
|
|
11025
|
+
const getRows = (sql) => {
|
|
11026
|
+
try {
|
|
11027
|
+
const res = db.exec(sql);
|
|
11028
|
+
if (!res.length) return [];
|
|
11029
|
+
const cols = res[0].columns;
|
|
11030
|
+
return res[0].values.map((val) => {
|
|
11031
|
+
const obj = {};
|
|
11032
|
+
cols.forEach((c, idx) => obj[c] = val[idx]);
|
|
11033
|
+
return obj;
|
|
11034
|
+
});
|
|
11035
|
+
} catch {
|
|
11036
|
+
return [];
|
|
11037
|
+
}
|
|
11038
|
+
};
|
|
11039
|
+
const totalNodesRow = getRows("SELECT COUNT(*) as count FROM nodes")[0];
|
|
11040
|
+
const totalNodes = totalNodesRow ? totalNodesRow.count : 0;
|
|
11041
|
+
const totalEdgesRow = getRows("SELECT COUNT(*) as count FROM edges")[0];
|
|
11042
|
+
const totalEdges = totalEdgesRow ? totalEdgesRow.count : 0;
|
|
11043
|
+
const orphanRows = getRows(`
|
|
11044
|
+
SELECT COUNT(*) as count FROM nodes
|
|
11045
|
+
WHERE id NOT IN (SELECT DISTINCT source_id FROM edges UNION SELECT DISTINCT target_id FROM edges)
|
|
11046
|
+
`);
|
|
11047
|
+
const totalOrphans = orphanRows[0] ? orphanRows[0].count : 0;
|
|
11048
|
+
const duplicateGroups = getRows(`
|
|
11049
|
+
SELECT name, COUNT(*) as count FROM nodes GROUP BY name HAVING count > 1
|
|
11050
|
+
`);
|
|
11051
|
+
const typeStats = getRows(`
|
|
11052
|
+
SELECT type, COUNT(*) as count FROM nodes GROUP BY type ORDER BY count DESC
|
|
11053
|
+
`);
|
|
11054
|
+
const lines = [
|
|
11055
|
+
"\u{1F4CA} **Kuma Knowledge Graph Health Report**",
|
|
11056
|
+
"\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\u2501\u2501\u2501\u2501",
|
|
11057
|
+
`\u2022 **Total Nodes:** ${totalNodes}`,
|
|
11058
|
+
`\u2022 **Total Edges:** ${totalEdges}`,
|
|
11059
|
+
`\u2022 **Orphan Nodes:** ${totalOrphans}`,
|
|
11060
|
+
`\u2022 **Duplicate Groups:** ${duplicateGroups.length}`,
|
|
11061
|
+
"",
|
|
11062
|
+
"**Nodes by Type:**",
|
|
11063
|
+
...typeStats.map((s) => ` - \`${s.type}\`: ${s.count}`),
|
|
11064
|
+
"",
|
|
11065
|
+
"**Recommendations:**"
|
|
11066
|
+
];
|
|
11067
|
+
const recs = [];
|
|
11068
|
+
const crossLinks = typeStats.find((s) => s.type === "cross_service_link");
|
|
11069
|
+
if (crossLinks && crossLinks.count > 50) {
|
|
11070
|
+
recs.push("\u26A0\uFE0F Too many `cross_service_link` nodes. Run cleanup or delete obsolete feature domains.");
|
|
11071
|
+
}
|
|
11072
|
+
if (duplicateGroups.length > 0) {
|
|
11073
|
+
recs.push(`\u26A0\uFE0F ${duplicateGroups.length} duplicate node groups detected. Delete duplicates using \`delete_node\`.`);
|
|
11074
|
+
}
|
|
11075
|
+
if (totalOrphans > 10) {
|
|
11076
|
+
recs.push(`\u26A0\uFE0F ${totalOrphans} orphan nodes found (0 edges). Consider running \`heal\` or \`clear\`.`);
|
|
11077
|
+
}
|
|
11078
|
+
if (recs.length === 0) {
|
|
11079
|
+
recs.push("\u2705 Graph health is optimal! Low noise, good signal-to-noise ratio.");
|
|
11080
|
+
}
|
|
11081
|
+
lines.push(...recs.map((r) => ` ${r}`));
|
|
11082
|
+
return lines.join("\n");
|
|
11083
|
+
}
|
|
10987
11084
|
async function handleLayersSummary(_params) {
|
|
10988
11085
|
const { getLayersSummary: getLayersSummary2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
|
|
10989
11086
|
return getLayersSummary2();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plumpslabs/kuma",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.25",
|
|
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",
|
|
@@ -828,15 +828,56 @@ function renderHealth(data){
|
|
|
828
828
|
}
|
|
829
829
|
|
|
830
830
|
function setupSearch() {
|
|
831
|
+
var searchTimeout = null;
|
|
831
832
|
document.getElementById("node-search").oninput = function() {
|
|
832
|
-
var
|
|
833
|
-
if (
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
833
|
+
var self = this;
|
|
834
|
+
if (searchTimeout) clearTimeout(searchTimeout);
|
|
835
|
+
searchTimeout = setTimeout(function() {
|
|
836
|
+
var query = self.value.toLowerCase().trim();
|
|
837
|
+
if (!network || !allNodes) return;
|
|
838
|
+
|
|
839
|
+
if (!query) {
|
|
840
|
+
resetHighlight();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
var matched = new Set();
|
|
845
|
+
var firstMatchedId = null;
|
|
846
|
+
allNodes.forEach(function(n) {
|
|
847
|
+
if (n.name.toLowerCase().includes(query) || (n.file_path && n.file_path.toLowerCase().includes(query))) {
|
|
848
|
+
matched.add(n.id);
|
|
849
|
+
if (!firstMatchedId) firstMatchedId = n.id;
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
// Update Nodes
|
|
854
|
+
var nodeBatchUpdates = [];
|
|
855
|
+
network.body.data.nodes.forEach(function(n) {
|
|
856
|
+
var hit = matched.has(n.id);
|
|
857
|
+
nodeBatchUpdates.push({ id: n.id, opacity: hit ? 1.0 : 0.08, font: { color: hit ? '#ffffff' : 'rgba(255,255,255,0.04)', bold: hit } });
|
|
858
|
+
});
|
|
859
|
+
network.body.data.nodes.update(nodeBatchUpdates);
|
|
860
|
+
|
|
861
|
+
// Update Edges (Dim edges whose endpoints are both not in search results)
|
|
862
|
+
var edgeBatchUpdates = [];
|
|
863
|
+
network.body.data.edges.forEach(function(e) {
|
|
864
|
+
var hit = matched.has(e.from) || matched.has(e.to);
|
|
865
|
+
var def = edgeDefaults[e.id] || {};
|
|
866
|
+
var strokeColor = hit ? (def.color || '#64748b') : 'rgba(100,116,139,0.05)';
|
|
867
|
+
var fontColor = hit ? '#94a3b8' : 'rgba(148,163,184,0.03)';
|
|
868
|
+
edgeBatchUpdates.push({
|
|
869
|
+
id: e.id,
|
|
870
|
+
color: { color: strokeColor, highlight: '#c084fc', hover: '#38bdf8', opacity: hit ? 0.95 : 0.05 },
|
|
871
|
+
font: { color: fontColor, strokeWidth: 0, background: 'transparent' },
|
|
872
|
+
width: hit ? (def.width || 1.5) : 0.2
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
network.body.data.edges.update(edgeBatchUpdates);
|
|
876
|
+
|
|
877
|
+
if (firstMatchedId && matched.size < 10) {
|
|
878
|
+
network.focus(firstMatchedId, { scale: 1.2, animation: { duration: 300 } });
|
|
879
|
+
}
|
|
880
|
+
}, 150);
|
|
840
881
|
};
|
|
841
882
|
}
|
|
842
883
|
</script>
|