@plumpslabs/kuma 2.3.0 → 2.3.2

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.
@@ -89,16 +89,95 @@ function validateFilePath(filePath, projectRoot) {
89
89
  };
90
90
  }
91
91
  }
92
+ function validateFileExtension(filePath) {
93
+ const allowedExtensions = [
94
+ ".ts",
95
+ ".tsx",
96
+ ".js",
97
+ ".jsx",
98
+ ".mjs",
99
+ ".cjs",
100
+ ".json",
101
+ ".md",
102
+ ".css",
103
+ ".html",
104
+ ".htm",
105
+ ".env",
106
+ ".env.example",
107
+ ".env.local",
108
+ ".yml",
109
+ ".yaml",
110
+ ".toml",
111
+ ".svg",
112
+ ".png",
113
+ ".jpg",
114
+ ".gif",
115
+ ".sh",
116
+ ".bat",
117
+ ".ps1",
118
+ ".sql",
119
+ ".graphql",
120
+ ".gql",
121
+ ".vue",
122
+ ".svelte",
123
+ ".astro",
124
+ ".prisma",
125
+ ".proto",
126
+ ".txt",
127
+ ".log"
128
+ ];
129
+ const ext = path.extname(filePath).toLowerCase();
130
+ return allowedExtensions.includes(ext);
131
+ }
92
132
  function getProjectRoot() {
93
133
  return process.env.AGENT_PROJECT_ROOT ?? process.cwd();
94
134
  }
135
+ function isReadable(filePath) {
136
+ try {
137
+ fs.accessSync(filePath, fs.constants.R_OK);
138
+ return true;
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+ function isWritable(dirPath) {
144
+ try {
145
+ fs.accessSync(dirPath, fs.constants.W_OK);
146
+ return true;
147
+ } catch {
148
+ return false;
149
+ }
150
+ }
151
+ function getBackupPath(filePath, timestamp) {
152
+ const ts = timestamp ?? Date.now();
153
+ const root = getProjectRoot();
154
+ const relativePath = path.relative(root, filePath);
155
+ return path.join(root, ".kuma", "backups", String(ts), relativePath);
156
+ }
157
+ function ensureBackupDir() {
158
+ const root = getProjectRoot();
159
+ const backupDir = path.join(root, ".kuma", "backups");
160
+ if (!fs.existsSync(backupDir)) {
161
+ fs.mkdirSync(backupDir, { recursive: true });
162
+ }
163
+ return backupDir;
164
+ }
95
165
  function getKumaDir() {
96
166
  const root = getProjectRoot();
97
167
  return path.join(root, ".kuma");
98
168
  }
169
+ function getKumaBackupsDir() {
170
+ return path.join(getKumaDir(), "backups");
171
+ }
99
172
 
100
173
  export {
101
174
  validateFilePath,
175
+ validateFileExtension,
102
176
  getProjectRoot,
103
- getKumaDir
177
+ isReadable,
178
+ isWritable,
179
+ getBackupPath,
180
+ ensureBackupDir,
181
+ getKumaDir,
182
+ getKumaBackupsDir
104
183
  };
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  sessionMemory
3
- } from "./chunk-TT37TE4P.js";
3
+ } from "./chunk-SU7BTOND.js";
4
4
  import {
5
5
  getProjectRoot
6
- } from "./chunk-IXMWW5WA.js";
6
+ } from "./chunk-E2KFPEBT.js";
7
7
 
8
8
  // src/engine/kumaMemory.ts
9
9
  import fs from "fs";
@@ -73,11 +73,52 @@ function recordDecision(decision) {
73
73
  const existing = sessionMemory.getMemoryContent("decisions");
74
74
  sessionMemory.writeMemory("decisions", existing + entry);
75
75
  sessionMemory.recordToolCall("kuma_decision", { title: decision.title });
76
+ recordDecisionToGraph(decision).catch(() => {
77
+ });
76
78
  return `\u2705 Decision "${decision.title}" recorded.`;
77
79
  } catch (err) {
78
80
  return `Error recording decision: ${err}`;
79
81
  }
80
82
  }
83
+ async function recordDecisionToGraph(decision) {
84
+ try {
85
+ const { upsertNode, addEdge } = await import("./kumaGraph-5IZ4FDZA.js");
86
+ const decisionId = `decision::${decision.title.replace(/[^a-zA-Z0-9_\-\s]/g, "").trim().replace(/\s+/g, "-")}`;
87
+ await upsertNode({
88
+ id: decisionId,
89
+ type: "variable",
90
+ name: `ADR: ${decision.title.substring(0, 80)}`,
91
+ metadata: {
92
+ type: "architectural-decision",
93
+ context: decision.context.substring(0, 200),
94
+ rationale: decision.rationale.substring(0, 200),
95
+ outcome: decision.outcome,
96
+ timestamp: decision.timestamp
97
+ }
98
+ });
99
+ const filePathMatches = decision.context.matchAll(/["']?([\w./-]+\.\w+)["']?/g);
100
+ for (const match of filePathMatches) {
101
+ const possiblePath = match[1];
102
+ if (possiblePath.includes("/") && (possiblePath.endsWith(".ts") || possiblePath.endsWith(".js") || possiblePath.endsWith(".json") || possiblePath.endsWith(".md"))) {
103
+ try {
104
+ const fileId = `file::${possiblePath}`;
105
+ await upsertNode({ id: fileId, type: "file", name: possiblePath });
106
+ await addEdge({ sourceId: decisionId, targetId: fileId, type: "depends_on", metadata: { reason: "adr-context" } });
107
+ } catch {
108
+ }
109
+ }
110
+ }
111
+ if (decision.outcome && decision.outcome !== "implemented") {
112
+ try {
113
+ const outcomeId = `decision-outcome::${decision.outcome.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase()}`;
114
+ await upsertNode({ id: outcomeId, type: "variable", name: `Outcome: ${decision.outcome.substring(0, 60)}` });
115
+ await addEdge({ sourceId: decisionId, targetId: outcomeId, type: "depends_on" });
116
+ } catch {
117
+ }
118
+ }
119
+ } catch {
120
+ }
121
+ }
81
122
  var decisionCooldown = 0;
82
123
  function shouldRecordDecision() {
83
124
  const history = sessionMemory.getToolCallHistory(30);
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  getDb,
3
3
  saveDb
4
- } from "./chunk-6NEMSUKP.js";
4
+ } from "./chunk-5N6KXACT.js";
5
5
  import {
6
6
  getProjectRoot
7
- } from "./chunk-IXMWW5WA.js";
7
+ } from "./chunk-E2KFPEBT.js";
8
8
 
9
9
  // src/engine/kumaSelfHeal.ts
10
10
  import { execSync } from "child_process";
@@ -458,7 +458,7 @@ The two nodes may not be connected in the knowledge graph yet. Use more tools to
458
458
  }
459
459
  async function buildFromSessionMemory() {
460
460
  try {
461
- const { sessionMemory } = await import("./sessionMemory-52ECPXZ6.js");
461
+ const { sessionMemory } = await import("./sessionMemory-LI4FIAMF.js");
462
462
  const toolCalls = sessionMemory.getToolCallHistory(50);
463
463
  let edgeCount = 0;
464
464
  for (const call of toolCalls) {
@@ -532,7 +532,7 @@ async function searchGraph(query, limit = 20) {
532
532
  }
533
533
  stmt.free();
534
534
  if (results.length === 0) {
535
- return `\u{1F50D} **Graph Search** \u2014 No results for "${query}". Try a different search term.`;
535
+ return await codebaseSearchFallback(query, limit);
536
536
  }
537
537
  const lines = [
538
538
  `\u{1F50D} **Graph Search** \u2014 ${results.length} result(s) for "${query}"`,
@@ -546,6 +546,89 @@ async function searchGraph(query, limit = 20) {
546
546
  return `Error searching graph: ${err}`;
547
547
  }
548
548
  }
549
+ async function codebaseSearchFallback(query, limit = 20) {
550
+ try {
551
+ const fg = (await import("fast-glob")).default;
552
+ const { getProjectRoot: getProjectRoot2 } = await import("./pathValidator-V4DC6U6Z.js");
553
+ const root = getProjectRoot2();
554
+ const patterns = [
555
+ `**/*${query}*`,
556
+ `**/*${query}*/**`,
557
+ query.includes(".") ? `**/${query}` : null
558
+ ].filter(Boolean);
559
+ const ignorePatterns = ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/.kuma/**"];
560
+ const files = await fg(patterns, {
561
+ cwd: root,
562
+ ignore: ignorePatterns,
563
+ onlyFiles: true,
564
+ deep: 6,
565
+ dot: false
566
+ });
567
+ if (files.length === 0) {
568
+ return `\u{1F50D} **Codebase Search** \u2014 No results for "${query}". Try a different search term or research the topic first.`;
569
+ }
570
+ const lines = [
571
+ `\u{1F50D} **Codebase Search** \u2014 ${files.length} result(s) for "${query}" (graph fallback)`,
572
+ `\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`,
573
+ ""
574
+ ];
575
+ for (const file of files.slice(0, limit)) {
576
+ lines.push(` \u{1F4C4} **${file}**`);
577
+ }
578
+ if (files.length > limit) {
579
+ lines.push(` ... and ${files.length - limit} more`);
580
+ }
581
+ lines.push(
582
+ "",
583
+ "\u{1F4A1} The knowledge graph is empty or has no matches.",
584
+ "\u{1F4A1} Run research or use more tools to build the graph for richer results."
585
+ );
586
+ return lines.join("\n");
587
+ } catch {
588
+ try {
589
+ let walk2 = function(dir) {
590
+ if (results.length >= limit) return;
591
+ let entries = [];
592
+ try {
593
+ entries = fs2.readdirSync(dir);
594
+ } catch {
595
+ return;
596
+ }
597
+ for (const entry of entries) {
598
+ if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "build") continue;
599
+ const full = path2.join(dir, entry);
600
+ try {
601
+ const stat = fs2.statSync(full);
602
+ if (stat.isDirectory()) {
603
+ walk2(full);
604
+ } else if (entry.toLowerCase().includes(query.toLowerCase())) {
605
+ results.push(path2.relative(root, full));
606
+ }
607
+ } catch {
608
+ }
609
+ }
610
+ };
611
+ var walk = walk2;
612
+ const fs2 = await import("fs");
613
+ const path2 = await import("path");
614
+ const root = process.cwd();
615
+ const results = [];
616
+ walk2(root);
617
+ if (results.length === 0) {
618
+ return `\u{1F50D} **Codebase Search** \u2014 No results for "${query}". Try a different search term.`;
619
+ }
620
+ return [
621
+ `\u{1F50D} **Codebase Search** \u2014 ${results.length} result(s) for "${query}" (basic fallback)`,
622
+ "",
623
+ ...results.slice(0, limit).map((f) => ` \u{1F4C4} **${f}**`),
624
+ "",
625
+ "\u{1F4A1} Install fast-glob for faster, more comprehensive codebase searches."
626
+ ].join("\n");
627
+ } catch {
628
+ return `\u{1F50D} **Search** \u2014 No results for "${query}". The knowledge graph is empty. Try researching first.`;
629
+ }
630
+ }
631
+ }
549
632
  async function getGraphStats() {
550
633
  try {
551
634
  const db = await getDb();
@@ -589,6 +672,10 @@ async function getGraphStats() {
589
672
  async function analyzeImpact(target) {
590
673
  try {
591
674
  const db = await getDb();
675
+ const nodeCount = db.exec("SELECT COUNT(*) as c FROM nodes")[0]?.values[0][0] ?? 0;
676
+ if (nodeCount === 0) {
677
+ return await codebaseImpactFallback(target);
678
+ }
592
679
  const nodeStmt = db.prepare(`
593
680
  SELECT id, name, type, file_path FROM nodes
594
681
  WHERE name LIKE ? OR file_path = ? OR id = ?
@@ -604,7 +691,7 @@ async function analyzeImpact(target) {
604
691
  }
605
692
  nodeStmt.free();
606
693
  if (!nodeId2) {
607
- return { symbol: target, references: 0, files: 0, testFiles: 0, entryPoints: [], risk: "low" };
694
+ return await codebaseImpactFallback(target);
608
695
  }
609
696
  const refStmt = db.prepare(`
610
697
  SELECT COUNT(*) as cnt, COUNT(DISTINCT CASE WHEN e.source_id != ? THEN e.source_id ELSE e.target_id END) as file_count
@@ -651,6 +738,34 @@ async function analyzeImpact(target) {
651
738
  return { symbol: target, references: 0, files: 0, testFiles: 0, entryPoints: [], risk: "low" };
652
739
  }
653
740
  }
741
+ async function codebaseImpactFallback(target) {
742
+ try {
743
+ const { execSync: execSync2 } = await import("child_process");
744
+ const root = process.cwd();
745
+ const grepResult = execSync2(
746
+ `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`,
747
+ { cwd: root, encoding: "utf-8", timeout: 5e3, maxBuffer: 1024 * 1024 }
748
+ ).trim();
749
+ const files = grepResult ? grepResult.split("\n").filter(Boolean).filter((f) => !f.startsWith("./")) : [];
750
+ if (files.length === 0) {
751
+ return { symbol: target, references: 0, files: 0, testFiles: 0, entryPoints: [], risk: "low" };
752
+ }
753
+ const fileCount = files.length;
754
+ const testFiles = files.filter((f) => f.includes("test") || f.includes("spec") || f.includes("__tests__")).length;
755
+ const risk = fileCount > 20 ? "high" : fileCount > 5 ? "medium" : "low";
756
+ return {
757
+ symbol: target,
758
+ references: fileCount,
759
+ // file-level count since we're approximating
760
+ files: fileCount,
761
+ testFiles,
762
+ entryPoints: files.slice(0, 5).map((f) => `${f} (grep match)`),
763
+ risk
764
+ };
765
+ } catch {
766
+ return { symbol: target, references: 0, files: 0, testFiles: 0, entryPoints: [], risk: "low" };
767
+ }
768
+ }
654
769
  function formatImpact(result) {
655
770
  const icon = result.risk === "high" ? "\u{1F534}" : result.risk === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
656
771
  const lines = [
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  sessionMemory
3
- } from "./chunk-TT37TE4P.js";
3
+ } from "./chunk-SU7BTOND.js";
4
4
  import {
5
5
  getProjectRoot
6
- } from "./chunk-IXMWW5WA.js";
6
+ } from "./chunk-E2KFPEBT.js";
7
7
 
8
8
  // src/utils/kumaShared.ts
9
9
  import { execSync } from "child_process";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getProjectRoot
3
- } from "./chunk-IXMWW5WA.js";
3
+ } from "./chunk-E2KFPEBT.js";
4
4
 
5
5
  // src/cli/init.ts
6
6
  import fs from "fs";
@@ -270,6 +270,7 @@ function generateInitMdContent() {
270
270
  "",
271
271
  '- `kuma_memory({ action: "research_save", scope: "...", ... })` \u2014 Save research results',
272
272
  '- `kuma_memory({ action: "decision", decisionAction: "record", ... })` \u2014 ADR-style decision',
273
+ '- **`kuma_memory({ action: "mine", scope: "..." })` \u2014 Mine historical decisions from git log & comments**',
273
274
  '- `kuma_memory({ action: "session" })` \u2014 Session summary',
274
275
  '- `kuma_memory({ action: "heal" })` \u2014 Self-heal knowledge graph',
275
276
  '- `kuma_memory({ action: "search", query: "..." })` \u2014 Search memories + graph',
@@ -278,6 +279,7 @@ function generateInitMdContent() {
278
279
  "### \u{1F6E1}\uFE0F kuma_safety \u2014 Safety & Policy",
279
280
  "",
280
281
  '- `kuma_safety({ action: "guard", guardGoal: "..." })` \u2014 Anti-pattern, drift, loop detection',
282
+ '- **`kuma_safety({ action: "verify", scope: "..." })` \u2014 Auto-run scoped tests & verify correctness**',
281
283
  '- `kuma_safety({ action: "check", ... })` \u2014 Pre-execution safety check',
282
284
  '- `kuma_safety({ action: "audit" })` \u2014 Query safety audit trail',
283
285
  '- `kuma_safety({ action: "lock", lockAction: "acquire", ... })` \u2014 Multi-agent file lock',
@@ -300,15 +300,41 @@ No unresolved issues.`;
300
300
  }
301
301
  recordToolCall(toolName, params) {
302
302
  this.ensureInit();
303
+ const timestamp = Date.now();
303
304
  this.state.toolCalls.push({
304
305
  toolName,
305
306
  params,
306
- timestamp: Date.now()
307
+ timestamp
307
308
  });
308
309
  if (this.state.toolCalls.length > 100) {
309
310
  this.state.toolCalls = this.state.toolCalls.slice(-100);
310
311
  }
311
312
  this.save();
313
+ this.autoTrackToDb(toolName, params, timestamp).catch(() => {
314
+ });
315
+ }
316
+ /**
317
+ * Auto-track tool calls to the database sessions + tool_calls + experiences tables.
318
+ * This is the passive observability pipeline — every tool call gets recorded
319
+ * automatically without requiring the tool to explicitly write to the DB.
320
+ */
321
+ async autoTrackToDb(toolName, params, timestamp) {
322
+ try {
323
+ const { getDb, saveDb } = await import("./kumaDb-RC2EO3OB.js");
324
+ const db = await getDb();
325
+ db.run(
326
+ `INSERT INTO tool_calls (tool_name, params, success, duration_ms, created_at) VALUES (?, ?, 1, 0, ?)`,
327
+ [toolName, JSON.stringify(params), Math.floor(timestamp / 1e3)]
328
+ );
329
+ const paramsHash = simpleHash(JSON.stringify(params));
330
+ const filePath = params.filePath || params.file_path || params.target || null;
331
+ db.run(
332
+ `INSERT INTO experiences (tool_name, params_hash, success, context_file, context_action, created_at) VALUES (?, ?, 1, ?, ?, ?)`,
333
+ [toolName, paramsHash, filePath, params.action || null, Math.floor(timestamp / 1e3)]
334
+ );
335
+ saveDb(db);
336
+ } catch {
337
+ }
312
338
  }
313
339
  setConventions(conventions) {
314
340
  this.ensureInit();
@@ -571,7 +597,7 @@ No unresolved issues.`;
571
597
  this.addModifiedFile(filePath);
572
598
  }
573
599
  try {
574
- const { recordChange } = await import("./kumaDb-KN7Z4B5V.js");
600
+ const { recordChange } = await import("./kumaDb-RC2EO3OB.js");
575
601
  const gitHash = this.getGitHead();
576
602
  await recordChange({ filePath, changeType, symbol, gitCommitHash: gitHash || void 0 });
577
603
  } catch {
@@ -608,6 +634,15 @@ No unresolved issues.`;
608
634
  }
609
635
  }
610
636
  };
637
+ function simpleHash(str) {
638
+ let hash = 0;
639
+ for (let i = 0; i < str.length; i++) {
640
+ const char = str.charCodeAt(i);
641
+ hash = (hash << 5) - hash + char;
642
+ hash = hash & hash;
643
+ }
644
+ return Math.abs(hash).toString(36);
645
+ }
611
646
  var sessionMemory = new SessionMemory();
612
647
 
613
648
  export {