@plumpslabs/kuma 2.3.23 → 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 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: "affects",
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 INLINE \u2014 When Tracing Flow)",
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: <file1> \u2192 <file2> \u2192 <file3>" })',
8201
+ 'kuma_memory({ action: "arch_flow", content: "domain: <name> | hops: <core_file1> \u2192 <core_file2> \u2192 <core_file3>" })',
8202
8202
  "```",
8203
- "**When:** IMMEDIATELY after tracing EACH hop in a data/control flow. Record INLINE, then continue tracing the next hop.",
8204
- "**Why:** Architecture knowledge dissipates fast. Record each hop before moving to the next file/task.",
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: <file1.tsx> \u2192 <file2.js> \u2192 <file3.ts> | gotchas: <optional> | decisions: <optional>",
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 ` (NOT newlines, NOT commas)",
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
  "",
@@ -8866,33 +8872,24 @@ function handleQuickrefGeneration(root, results) {
8866
8872
  const content = [
8867
8873
  "# Kuma Quick Reference",
8868
8874
  "",
8869
- "_(Auto-generated by `kuma init` \u2014 simplified workflow cheat sheet)_",
8870
- "",
8871
- "## Every Session (Mandatory \u2014 including research-only)",
8875
+ "## Lean Mode (default \u2014 for speed)",
8876
+ "1. `init` \u2192 edit \u2192 record (if needed)",
8872
8877
  "",
8873
- ' 1. `kuma_context({ action: "init" })` \u2014 Load context',
8874
- ' 2. `kuma_safety({ action: "guard" })` \u2014 Safety check',
8875
- ' 3. `kuma_context({ action: "research", scope: "<area>" })` \u2014 Research',
8876
- " 4. *(edit/read using native tools)*",
8877
- ' 5. `kuma_memory({ action: "research_save", ... })` \u2014 After exploring area (creates search cache)',
8878
- ' 6. `kuma_memory({ action: "gotcha" })` \u2014 \u{1F525} IMMEDIATELY when bug found (exponential)',
8879
- ' 7. `kuma_memory({ action: "arch_flow" })` \u2014 \u{1F525} IMMEDIATELY after each flow hop (exponential)',
8880
- ' 8. `kuma_memory({ action: "decision" })` \u2014 When choosing (preserves rationale)',
8881
- ' 9. `kuma_safety({ action: "verify", ... })` \u2014 Run tests / confirm nothing broken',
8882
- ' 10. `kuma_context({ action: "changes" })` \u2014 Review session activity',
8878
+ "## Standard Mode (for safety)",
8879
+ "1. `init` \u2192 `guard` \u2192 `research` \u2192 edit \u2192 `record` \u2192 `verify`",
8883
8880
  "",
8884
- "\u{1F525} `arch_flow` + `gotcha` = EXPONENTIAL value. Each record saves 5-10 files next session.",
8885
- "\u{1F7E2} SKIP recording function/class/component nodes \u2014 grep/glob is faster.",
8886
- "\u{1F9E0} Graph is persistent \u2014 nodes/edges accumulate across sessions.",
8881
+ "## Full Mode (for complex changes)",
8882
+ "1. `init` \u2192 `guard` \u2192 `research` \u2192 `impact` \u2192 edit \u2192 `record` \u2192 `verify` \u2192 `changes`",
8887
8883
  "",
8888
- "\u26D4 Steps 5-10 are mandatory even for research-only sessions!",
8884
+ "## Record Rules",
8885
+ "- **MUST:** decision, gotcha",
8886
+ "- **SKIP:** function, class, import, route",
8889
8887
  "",
8890
8888
  "## Platform Tool Names",
8889
+ "- OpenCode / Antigravity: `kuma_kuma_*`",
8890
+ "- Others: `kuma_*`",
8891
8891
  "",
8892
- "- **OpenCode:** Use `kuma_kuma_*` prefix (e.g. `kuma_kuma_context`)",
8893
- "- **Other platforms:** Use `kuma_*` directly (e.g. `kuma_context`)",
8894
- "",
8895
- "\u{1F4D6} Full rules: `.kuma/init.md`",
8892
+ "\u{1F4D6} Full: `.kuma/init.md` | \u{1F4CA} Modes: `.kuma/MODE.md` | \u{1F7E2} Skip: `.kuma/SKIP_RULES.md`",
8896
8893
  "_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_"
8897
8894
  ].join("\n");
8898
8895
  try {
@@ -8904,11 +8901,9 @@ function handleQuickrefGeneration(root, results) {
8904
8901
  results.push({ type: "claude", filePath: ".kuma/quickref.md", action: "skipped" });
8905
8902
  return;
8906
8903
  }
8907
- results.push({ type: "claude", filePath: ".kuma/quickref.md", action: "skipped" });
8908
- } else {
8909
- fs24.writeFileSync(quickrefPath, content, "utf-8");
8910
- results.push({ type: "claude", filePath: ".kuma/quickref.md", action: "created" });
8911
8904
  }
8905
+ fs24.writeFileSync(quickrefPath, content, "utf-8");
8906
+ results.push({ type: "claude", filePath: ".kuma/quickref.md", action: "created" });
8912
8907
  } catch (err) {
8913
8908
  results.push({
8914
8909
  type: "claude",
@@ -8918,6 +8913,150 @@ function handleQuickrefGeneration(root, results) {
8918
8913
  });
8919
8914
  }
8920
8915
  }
8916
+ function handleModeMdGeneration(root, results) {
8917
+ const filePath = path24.resolve(root, ".kuma/MODE.md");
8918
+ const content = [
8919
+ "# Adaptive Mode Selector",
8920
+ "",
8921
+ "## Mode Definitions",
8922
+ "",
8923
+ "| Mode | When to Use | Token Cost | Steps |",
8924
+ "|------|-------------|------------|-------|",
8925
+ "| **Lean** (default) | Small fixes, familiar code, hotfixes, < 3 files | ~100 | 3 |",
8926
+ "| **Standard** | New features, unfamiliar modules, 3-10 files | ~300 | 5 |",
8927
+ "| **Full** | Cross-module refactors, architecture changes, > 10 files | ~500 | 7 |",
8928
+ "",
8929
+ "## Auto-Detection Rules",
8930
+ "",
8931
+ "```",
8932
+ "IF file_count < 3 AND area_familiar == true:",
8933
+ ' mode = "lean"',
8934
+ "ELSE IF file_count <= 10 OR area_familiar == false:",
8935
+ ' mode = "standard"',
8936
+ "ELSE IF file_count > 10 OR cross_module == true:",
8937
+ ' mode = "full"',
8938
+ "```",
8939
+ "",
8940
+ "## Mode Behaviors",
8941
+ "",
8942
+ "### Lean Mode (Default)",
8943
+ "- Skip: guard, research, verify, changes",
8944
+ "- Record: only if decision/gotcha explicit",
8945
+ "- Focus: speed, minimal overhead",
8946
+ "",
8947
+ "### Standard Mode",
8948
+ "- Include: guard (before unfamiliar), research (before edit)",
8949
+ "- Record: decision, gotcha, arch_flow (if complex)",
8950
+ "- Focus: safety + knowledge capture",
8951
+ "",
8952
+ "### Full Mode",
8953
+ "- Include: all steps including impact, changes",
8954
+ "- Record: everything valuable",
8955
+ "- Focus: completeness, audit trail",
8956
+ "",
8957
+ "_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_"
8958
+ ].join("\n");
8959
+ try {
8960
+ const dir = path24.dirname(filePath);
8961
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
8962
+ if (!fs24.existsSync(filePath)) {
8963
+ fs24.writeFileSync(filePath, content, "utf-8");
8964
+ results.push({ type: "claude", filePath: ".kuma/MODE.md", action: "created" });
8965
+ }
8966
+ } catch (err) {
8967
+ results.push({ type: "claude", filePath: ".kuma/MODE.md", action: "error", error: err instanceof Error ? err.message : String(err) });
8968
+ }
8969
+ }
8970
+ function handleSkipRulesMdGeneration(root, results) {
8971
+ const filePath = path24.resolve(root, ".kuma/SKIP_RULES.md");
8972
+ const content = [
8973
+ "# Skip Rules \u2014 What NOT to Record",
8974
+ "",
8975
+ "## \u{1F7E2} NEVER Record (grep/glob is faster)",
8976
+ "",
8977
+ "| What | Why Skip | Better Tool |",
8978
+ "|------|----------|-------------|",
8979
+ "| Function definitions | `grep funcName(` | Grep |",
8980
+ "| Class definitions | `grep class ClassName` | Grep |",
8981
+ "| Import statements | Read import block | Read |",
8982
+ "| Component definitions | `glob **/*Component*` | Glob |",
8983
+ "| Route definitions | Check router file | Read |",
8984
+ "| Type/interface definitions | `grep interface TypeName` | Grep |",
8985
+ "| Variable/const declarations | `grep const varName` | Grep |",
8986
+ "| Test file locations | `glob **/*.test.*` | Glob |",
8987
+ "",
8988
+ "## \u{1F534} ALWAYS Record (high value)",
8989
+ "",
8990
+ "| What | Why Record | Tool |",
8991
+ "|------|------------|------|",
8992
+ "| Architecture decisions with rationale | Preserves context | `decision` |",
8993
+ "| Bugs/quirks found and fixed | Prevents re-discovery | `gotcha` |",
8994
+ "| Cross-module flow paths | Saves 5-10 files next session | `arch_flow` |",
8995
+ "| Business rules discovered | Domain knowledge | `domain_rules` |",
8996
+ "",
8997
+ "## \u{1F7E1} Record ONLY If Complex",
8998
+ "",
8999
+ "| What | Condition | Tool |",
9000
+ "|------|-----------|------|",
9001
+ "| Research findings | Multi-file exploration (> 3 files) | `research_save` |",
9002
+ "| Architecture flow | Cross-service or > 3 hops | `arch_flow` |",
9003
+ "| Feature definition | High-level feature with owns edges | `feature` |",
9004
+ "",
9005
+ "_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_"
9006
+ ].join("\n");
9007
+ try {
9008
+ const dir = path24.dirname(filePath);
9009
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
9010
+ if (!fs24.existsSync(filePath)) {
9011
+ fs24.writeFileSync(filePath, content, "utf-8");
9012
+ results.push({ type: "claude", filePath: ".kuma/SKIP_RULES.md", action: "created" });
9013
+ }
9014
+ } catch (err) {
9015
+ results.push({ type: "claude", filePath: ".kuma/SKIP_RULES.md", action: "error", error: err instanceof Error ? err.message : String(err) });
9016
+ }
9017
+ }
9018
+ function handleStalenessMdGeneration(root, results) {
9019
+ const filePath = path24.resolve(root, ".kuma/STALENESS.md");
9020
+ const content = [
9021
+ "# Auto-Staleness Detection",
9022
+ "",
9023
+ "## Staleness Signals",
9024
+ "",
9025
+ "| Signal | Detection | Action |",
9026
+ "|--------|-----------|--------|",
9027
+ "| File modified after save | Compare file mtime vs research_save timestamp | Mark STALE, skip cache |",
9028
+ "| Git commit touches file | `git log --since` on researched files | Mark STALE, warn |",
9029
+ '| Age > 7 days | Current time - research_save timestamp | Warn: "Cache may be stale" |',
9030
+ "| Age > 30 days | Current time - research_save timestamp | Auto-invalidate, re-read |",
9031
+ "",
9032
+ "## Recovery Protocol",
9033
+ "",
9034
+ "```",
9035
+ "ON stale_detected:",
9036
+ " 1. Skip cached data",
9037
+ " 2. Re-read file from disk",
9038
+ " 3. Update research_save timestamp",
9039
+ " 4. Continue with fresh data",
9040
+ "",
9041
+ "ON search_conflict:",
9042
+ " 1. Prefer recent research (newer timestamp)",
9043
+ " 2. If same age, prefer live file over cache",
9044
+ " 3. Log conflict for review",
9045
+ "```",
9046
+ "",
9047
+ "_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_"
9048
+ ].join("\n");
9049
+ try {
9050
+ const dir = path24.dirname(filePath);
9051
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
9052
+ if (!fs24.existsSync(filePath)) {
9053
+ fs24.writeFileSync(filePath, content, "utf-8");
9054
+ results.push({ type: "claude", filePath: ".kuma/STALENESS.md", action: "created" });
9055
+ }
9056
+ } catch (err) {
9057
+ results.push({ type: "claude", filePath: ".kuma/STALENESS.md", action: "error", error: err instanceof Error ? err.message : String(err) });
9058
+ }
9059
+ }
8921
9060
  function handleInitMdGeneration(root, results) {
8922
9061
  const initMdPath = path24.resolve(root, ".kuma/init.md");
8923
9062
  try {
@@ -8944,6 +9083,9 @@ function handleInitMdGeneration(root, results) {
8944
9083
  });
8945
9084
  }
8946
9085
  handleQuickrefGeneration(root, results);
9086
+ handleModeMdGeneration(root, results);
9087
+ handleSkipRulesMdGeneration(root, results);
9088
+ handleStalenessMdGeneration(root, results);
8947
9089
  }
8948
9090
  function handleCodewhaleSecondary(root, results) {
8949
9091
  const mcpPath = path24.resolve(root, ".codewhale/mcp.json");
@@ -9681,30 +9823,18 @@ var init_skillGenerator = __esm({
9681
9823
  "use strict";
9682
9824
  BOOTSTRAP = [
9683
9825
  "Kuma MCP tools are available (kuma_context, kuma_memory, kuma_safety).",
9684
- '**Before coding, call `kuma_context({ action: "init" })`** to load project context and session memory.',
9826
+ '**Before coding, call `kuma_context({ action: "init" })`** to load project context.',
9685
9827
  "Project knowledge persists in `.kuma/memories/*.md` across sessions.",
9686
9828
  "",
9687
- "\u26A0\uFE0F **MANDATORY WORKFLOW** \u2014 Follow in order every session (including research-only):",
9829
+ "\u26A1 **ADAPTIVE WORKFLOW (Lean Mode by Default):**",
9830
+ " \u2022 **Lean Mode (Default, < 3 files):** `init` \u2192 edit/read \u2192 record (`decision`/`gotcha` if needed)",
9831
+ " \u2022 **Standard Mode (3-10 files/unfamiliar):** `init` \u2192 `guard` \u2192 `research` \u2192 edit \u2192 `record` \u2192 `verify`",
9832
+ " \u2022 **Full Mode (> 10 files/refactor):** `init` \u2192 `guard` \u2192 `research` \u2192 `impact` \u2192 edit \u2192 `record` \u2192 `verify` \u2192 `changes`",
9688
9833
  "",
9689
- ' 1. `kuma_context({ action: "init" })` \u2014 Load context (START HERE)',
9690
- ' 2. `kuma_safety({ action: "guard" })` \u2014 Safety check before work',
9691
- ' 3. `kuma_context({ action: "research", scope: "<area>" })` \u2014 Research before editing',
9692
- " 4. *(edit/read using native tools)*",
9693
- ' 5. `kuma_memory({ action: "research_save", ... })` \u2014 After exploring area (creates search cache)',
9694
- ' 6. `kuma_memory({ action: "gotcha" })` \u2014 \u{1F525} IMMEDIATELY when bug found (exponential value)',
9695
- ' 7. `kuma_memory({ action: "arch_flow" })` \u2014 \u{1F525} IMMEDIATELY after each flow hop (exponential value)',
9696
- ' 8. `kuma_memory({ action: "decision" })` \u2014 When choosing between options (preserves rationale)',
9697
- ' 9. `kuma_safety({ action: "verify", ... })` \u2014 Run tests / confirm nothing broken',
9698
- ' 10. `kuma_context({ action: "changes" })` \u2014 Review session activity',
9834
+ "\u{1F534} **MUST RECORD:** `decision` (rationale), `gotcha` (bug/quirk), `arch_flow` (complex flow)",
9835
+ "\u{1F7E2} **SKIP RECORDING:** function, class, import, route (use native grep/glob instead)",
9699
9836
  "",
9700
- "\u{1F525} **Power Curve:** `arch_flow` + `gotcha` are EXPONENTIAL \u2014 each record saves 5-10 files next session.",
9701
- " The more you record, the more overpowered the agent becomes. These are the highest-value actions.",
9702
- "\u{1F7E2} **SKIP** recording function/class/component nodes \u2014 grep/glob is faster.",
9703
- "\u{1F9E0} Knowledge graph is persistent \u2014 nodes/edges accumulate across sessions, getting richer over time.",
9704
- "",
9705
- "\u26D4 Do NOT skip steps 5-10 after research \u2014 even if no code was changed.",
9706
- "\u{1F5D1}\uFE0F **Delete/Clear:** `delete_node` (delete node/gotcha/todo/decision) | `clear` (wipe entire graph)",
9707
- "\u{1F4D6} Full rules: `.kuma/init.md`",
9837
+ "\u{1F4D6} Full rules & modes: `.kuma/init.md` | `.kuma/MODE.md` | `.kuma/SKIP_RULES.md`",
9708
9838
  "\u{1F9E0} Tools: `kuma_context` | `kuma_memory` | `kuma_safety`"
9709
9839
  ].join("\n");
9710
9840
  BOOTSTRAP_OPENCODE = [
@@ -9712,27 +9842,15 @@ var init_skillGenerator = __esm({
9712
9842
  '**Before coding, call `kuma_kuma_context({ action: "init" })`** to load project context.',
9713
9843
  "Project knowledge persists in `.kuma/memories/*.md` across sessions.",
9714
9844
  "",
9715
- "\u26A0\uFE0F **MANDATORY WORKFLOW** \u2014 Follow in order every session (including research-only):",
9716
- "",
9717
- ' 1. `kuma_kuma_context({ action: "init" })` \u2014 Load context (START HERE)',
9718
- ' 2. `kuma_kuma_safety({ action: "guard" })` \u2014 Safety check before work',
9719
- ' 3. `kuma_kuma_context({ action: "research", scope: "<area>" })` \u2014 Research before editing',
9720
- " 4. *(edit/read using native tools)*",
9721
- ' 5. `kuma_kuma_memory({ action: "research_save", ... })` \u2014 After exploring area (creates search cache)',
9722
- ' 6. `kuma_kuma_memory({ action: "gotcha" })` \u2014 \u{1F525} IMMEDIATELY when bug found (exponential value)',
9723
- ' 7. `kuma_kuma_memory({ action: "arch_flow" })` \u2014 \u{1F525} IMMEDIATELY after each flow hop (exponential value)',
9724
- ' 8. `kuma_kuma_memory({ action: "decision" })` \u2014 When choosing between options (preserves rationale)',
9725
- ' 9. `kuma_kuma_safety({ action: "verify", ... })` \u2014 Run tests / confirm nothing broken',
9726
- ' 10. `kuma_kuma_context({ action: "changes" })` \u2014 Review session activity',
9845
+ "\u26A1 **ADAPTIVE WORKFLOW (Lean Mode by Default):**",
9846
+ ' \u2022 **Lean Mode (Default, < 3 files):** `kuma_kuma_context({ action: "init" })` \u2192 edit/read \u2192 record (`decision`/`gotcha`)',
9847
+ " \u2022 **Standard Mode (3-10 files/unfamiliar):** `init` \u2192 `guard` \u2192 `research` \u2192 edit \u2192 `record` \u2192 `verify`",
9848
+ " \u2022 **Full Mode (> 10 files/refactor):** `init` \u2192 `guard` \u2192 `research` \u2192 `impact` \u2192 edit \u2192 `record` \u2192 `verify` \u2192 `changes`",
9727
9849
  "",
9728
- "\u{1F525} **Power Curve:** `arch_flow` + `gotcha` are EXPONENTIAL \u2014 each record saves 5-10 files next session.",
9729
- " The more you record, the more overpowered the agent becomes. These are the highest-value actions.",
9730
- "\u{1F7E2} **SKIP** recording function/class/component nodes \u2014 grep/glob is faster.",
9731
- "\u{1F9E0} Knowledge graph is persistent \u2014 nodes/edges accumulate across sessions, getting richer over time.",
9850
+ "\u{1F534} **MUST RECORD:** `decision` (rationale), `gotcha` (bug/quirk), `arch_flow` (complex flow)",
9851
+ "\u{1F7E2} **SKIP RECORDING:** function, class, import, route (use native grep/glob instead)",
9732
9852
  "",
9733
- "\u26D4 Do NOT skip steps 5-10 after research \u2014 even if no code was changed.",
9734
- "\u{1F5D1}\uFE0F **Delete/Clear:** `delete_node` (delete node/gotcha/todo/decision) | `clear` (wipe entire graph)",
9735
- "\u{1F4D6} Full rules: `.kuma/init.md`",
9853
+ "\u{1F4D6} Full rules & modes: `.kuma/init.md` | `.kuma/MODE.md` | `.kuma/SKIP_RULES.md`",
9736
9854
  "\u{1F9E0} Tools: `kuma_kuma_context` | `kuma_kuma_memory` | `kuma_kuma_safety`"
9737
9855
  ].join("\n");
9738
9856
  }
@@ -10265,6 +10383,11 @@ var MEMORY_ALIASES = {
10265
10383
  "read": "session",
10266
10384
  "fetch": "session",
10267
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",
10268
10391
  // Decision synonyms
10269
10392
  "decision": "decision",
10270
10393
  "adr": "decision",
@@ -10296,7 +10419,6 @@ var MEMORY_ALIASES = {
10296
10419
  "task": "todo",
10297
10420
  "todos": "todo",
10298
10421
  "tasks": "todo",
10299
- // Context notes synonyms
10300
10422
  "context": "context",
10301
10423
  "notes": "context",
10302
10424
  "note": "context",
@@ -10401,6 +10523,8 @@ async function handleMemory(params) {
10401
10523
  return handleFeature(params);
10402
10524
  case "delete_node":
10403
10525
  return handleDeleteNode(params);
10526
+ case "graph_health":
10527
+ return handleGraphHealth(params);
10404
10528
  case "clear": {
10405
10529
  const { clearGraph: clearGraph2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
10406
10530
  await clearGraph2();
@@ -10452,14 +10576,26 @@ async function handleResearchSave(params) {
10452
10576
  });
10453
10577
  await saveResearchCache(scope, record, void 0, params.confidence);
10454
10578
  try {
10455
- const { upsertNode: upsertNode2, nodeId: nodeId2 } = await Promise.resolve().then(() => (init_kumaGraph(), kumaGraph_exports));
10456
- await upsertNode2({ id: nodeId2("file", scope), type: "file", name: scope });
10457
- await upsertNode2({
10458
- id: `research::${scope}`,
10459
- type: "research",
10460
- name: `research:${scope}`,
10461
- metadata: { confidence: params.confidence || 0.8 }
10462
- });
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);
10463
10599
  } catch {
10464
10600
  }
10465
10601
  try {
@@ -10675,12 +10811,20 @@ async function handleLayerAction(layer, params) {
10675
10811
  \u274C Wrong: hops: file1.js\\nfile2.js
10676
10812
  \u2705 Correct: domain: <name> | hops: file1.js \u2192 file2.js \u2192 file3.js`;
10677
10813
  }
10678
- const hops = hopsStr ? hopsStr.split("\u2192").map((h) => h.trim()).filter(Boolean).map((h, i, arr) => ({
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) => ({
10679
10823
  from: i === 0 ? domain : arr[i - 1],
10680
10824
  to: h,
10681
10825
  relation: "flows",
10682
10826
  description: h
10683
- })) : [];
10827
+ }));
10684
10828
  if (hops.length === 0) {
10685
10829
  return `\u274C **arch_flow format error:** no hops found after \u2192 separator.
10686
10830
 
@@ -10784,11 +10928,12 @@ async function handleDeleteNode(params) {
10784
10928
  return `\u26A0\uFE0F Unknown scope "${params.scope}". Supported: gotcha, todo, decision, checkpoint`;
10785
10929
  }
10786
10930
  }
10787
- if (params.target) {
10931
+ if (params.target || params.scope) {
10788
10932
  const { flushDb: flushDb2 } = await Promise.resolve().then(() => (init_kumaDb(), kumaDb_exports));
10789
- if (params.target.includes("::") || params.target.includes(":") || params.target.includes("-")) {
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)))) {
10790
10936
  try {
10791
- const targetId = params.target;
10792
10937
  const domainName = targetId.startsWith("feature_domain::") ? targetId.replace("feature_domain::", "") : targetId;
10793
10938
  if (targetId.startsWith("feature_domain::")) {
10794
10939
  db.run("DELETE FROM nodes WHERE id LIKE ? OR id LIKE ? OR id LIKE ?", [
@@ -10802,16 +10947,18 @@ async function handleDeleteNode(params) {
10802
10947
  db.run("DELETE FROM nodes WHERE id = ?", [targetId]);
10803
10948
  }
10804
10949
  flushDb2(db);
10805
- return `\u{1F5D1}\uFE0F **Node & relations deleted instantly from RAM and Disk:** ${params.target}`;
10950
+ return `\u{1F5D1}\uFE0F **Node & relations deleted instantly from RAM and Disk:** ${targetId}`;
10806
10951
  } catch (err) {
10807
10952
  return `\u274C Failed to delete node: ${err}`;
10808
10953
  }
10809
10954
  }
10810
- const id = parseInt(params.target, 10);
10811
- if (!isNaN(id)) {
10812
- db.run("DELETE FROM nodes WHERE rowid = ?", [id]);
10813
- flushDb2(db);
10814
- return `\u{1F5D1}\uFE0F **Node #${id} deleted.**`;
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
+ }
10815
10962
  }
10816
10963
  }
10817
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'";
@@ -10872,6 +11019,68 @@ async function handleFeature(params) {
10872
11019
  \u{1F3F7}\uFE0F Tags: ${tags.length}
10873
11020
  \u26A0\uFE0F Risk: ${risk}`;
10874
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
+ }
10875
11084
  async function handleLayersSummary(_params) {
10876
11085
  const { getLayersSummary: getLayersSummary2 } = await Promise.resolve().then(() => (init_domainRules(), domainRules_exports));
10877
11086
  return getLayersSummary2();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumpslabs/kuma",
3
- "version": "2.3.23",
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",
@@ -234,8 +234,7 @@ header .status .dot.off { background: var(--red); }
234
234
  <div id="graph-container"></div>
235
235
  <div id="search-box"><input id="node-search" type="text" placeholder="Search nodes..." /></div>
236
236
  <div id="graph-controls">
237
- <button onclick="resetGraphFocus()" title="Reset focus"><i data-lucide="x" style="width:12px;height:12px"></i></button>
238
- <button onclick="if(network)network.fit()" title="Fit to viewport"><i data-lucide="maximize-2" style="width:12px;height:12px"></i></button>
237
+ <button onclick="if(network)network.fit()" title="Fit to viewport"><i data-lucide="maximize-2" style="width:12px;height:12px"></i> Fit</button>
239
238
  <div class="ctrl-sep"></div>
240
239
  <button class="depth-btn" data-depth="1" onclick="setDepth(1)">1</button>
241
240
  <button class="depth-btn active" data-depth="2" onclick="setDepth(2)">2</button>
@@ -482,7 +481,7 @@ function renderGraph(data) {
482
481
  stabilization: { iterations: 120, updateInterval: 50, onlyDynamicEdges: false, fit: false },
483
482
  },
484
483
  interaction: { hover: true, tooltipDelay: 300, zoomView: true, dragView: true, dragNodes: true, keyboard: true, multiselect: true, selectable: true, selectConnectedEdges: false },
485
- nodes: { borderWidth: 0, font: { color: '#ffffff', size: 11, face: 'sans-serif', bold: true }, margin: { top: 6, bottom: 6, left: 10, right: 10 }, shapeProperties: { borderRadius: 6 } },
484
+ nodes: { borderWidth: 0, font: { color: '#ffffff', size: 11, face: 'sans-serif' }, margin: { top: 6, bottom: 6, left: 10, right: 10 }, shapeProperties: { borderRadius: 6 } },
486
485
  edges: { smooth: { type: 'continuous', roundness: 0.2 }, width: 1, color: { color: '#4a4a6a', highlight: '#7c3aed' } },
487
486
  configure: { enabled: false },
488
487
  });
@@ -780,7 +779,7 @@ function renderFeatures(data) {
780
779
  var items = data.features || [];
781
780
  var el = document.getElementById("panel-features");
782
781
  if(!el) return;
783
- if(!items.length){el.innerHTML='<div class="empty">No features recorded yet.<br><br>Use:<br><code style="color:var(--accent)">kuma_memory({action:\'feature\', title:\'Auth\'})</code></div>';return;}
782
+ if(!items.length){el.innerHTML='<div class="empty">No features recorded yet.<br><br>Use:<br><code style="color:var(--accent)">kuma_memory</code></div>';return;}
784
783
  el.innerHTML = items.map(function(f){
785
784
  var feat = typeof f === 'string' ? JSON.parse(f) : f;
786
785
  var meta = {};
@@ -829,15 +828,56 @@ function renderHealth(data){
829
828
  }
830
829
 
831
830
  function setupSearch() {
831
+ var searchTimeout = null;
832
832
  document.getElementById("node-search").oninput = function() {
833
- var query = this.value.toLowerCase().trim();
834
- if (!network || !allNodes) return;
835
- if (!query) { network.body.data.nodes.forEach(function(n){ network.body.data.nodes.update({id:n.id,opacity:1.0}); }); return; }
836
- var matched = new Set();
837
- allNodes.forEach(function(n){
838
- if (n.name.toLowerCase().includes(query) || (n.file_path && n.file_path.toLowerCase().includes(query))) matched.add(n.id);
839
- });
840
- network.body.data.nodes.forEach(function(n){ network.body.data.nodes.update({ id: n.id, opacity: matched.has(n.id) ? 1.0 : 0.15 }); });
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);
841
881
  };
842
882
  }
843
883
  </script>