@plumpslabs/kuma 2.2.8 → 2.3.0

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,77 +89,16 @@ 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
- }
132
92
  function getProjectRoot() {
133
93
  return process.env.AGENT_PROJECT_ROOT ?? process.cwd();
134
94
  }
135
- function getBackupPath(filePath, timestamp) {
136
- const ts = timestamp ?? Date.now();
137
- const root = getProjectRoot();
138
- const relativePath = path.relative(root, filePath);
139
- return path.join(root, ".kuma", "backups", String(ts), relativePath);
140
- }
141
- function ensureBackupDir() {
142
- const root = getProjectRoot();
143
- const backupDir = path.join(root, ".kuma", "backups");
144
- if (!fs.existsSync(backupDir)) {
145
- fs.mkdirSync(backupDir, { recursive: true });
146
- }
147
- return backupDir;
148
- }
149
95
  function getKumaDir() {
150
96
  const root = getProjectRoot();
151
97
  return path.join(root, ".kuma");
152
98
  }
153
- function getKumaBackupsDir() {
154
- return path.join(getKumaDir(), "backups");
155
- }
156
99
 
157
100
  export {
158
101
  validateFilePath,
159
- validateFileExtension,
160
102
  getProjectRoot,
161
- getBackupPath,
162
- ensureBackupDir,
163
- getKumaDir,
164
- getKumaBackupsDir
103
+ getKumaDir
165
104
  };
@@ -0,0 +1,131 @@
1
+ import {
2
+ sessionMemory
3
+ } from "./chunk-TT37TE4P.js";
4
+ import {
5
+ getProjectRoot
6
+ } from "./chunk-IXMWW5WA.js";
7
+
8
+ // src/engine/kumaMemory.ts
9
+ import fs from "fs";
10
+ import path from "path";
11
+ var MEMORY_DIR = ".kuma/memories";
12
+ function scoreMemoryRelevance(context, limit = 5) {
13
+ const results = [];
14
+ const kumaDir = path.join(getProjectRoot(), MEMORY_DIR);
15
+ if (!fs.existsSync(kumaDir)) return [];
16
+ const terms = context.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
17
+ if (terms.length === 0) return [];
18
+ try {
19
+ const files = fs.readdirSync(kumaDir).filter((f) => f.endsWith(".md"));
20
+ for (const file of files) {
21
+ try {
22
+ const content = fs.readFileSync(path.join(kumaDir, file), "utf-8");
23
+ const lower = content.toLowerCase();
24
+ let matchCount = 0;
25
+ for (const term of terms) {
26
+ if (lower.includes(term)) matchCount++;
27
+ }
28
+ const score = Math.round(matchCount / terms.length * 100);
29
+ if (score > 0) {
30
+ const topic = file.replace(/\.md$/, "");
31
+ const firstLine = content.split("\n").slice(0, 3).join(" ").substring(0, 150);
32
+ results.push({
33
+ topic,
34
+ content: firstLine,
35
+ score,
36
+ reason: `${matchCount}/${terms.length} terms matched`
37
+ });
38
+ }
39
+ } catch {
40
+ }
41
+ }
42
+ } catch {
43
+ }
44
+ return results.sort((a, b) => b.score - a.score).slice(0, limit);
45
+ }
46
+ function formatScoredMemories(memories, context) {
47
+ if (memories.length === 0) return "";
48
+ const lines = [
49
+ `\u{1F9E0} **Relevant Memories** (for "${context.substring(0, 40)}")`,
50
+ "\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"
51
+ ];
52
+ for (const m of memories) {
53
+ const bar = "\u2588".repeat(Math.round(m.score / 10)) + "\u2591".repeat(Math.round(10 - m.score / 10));
54
+ lines.push(` **${m.topic}** \u2014 ${bar} ${m.score}%`);
55
+ lines.push(` ${m.content.substring(0, 100)}`);
56
+ lines.push(` \u{1F4A1} ${m.reason}`);
57
+ lines.push("");
58
+ }
59
+ return lines.join("\n");
60
+ }
61
+ function recordDecision(decision) {
62
+ try {
63
+ const entry = [
64
+ "",
65
+ `## ${decision.title}`,
66
+ `- **Date:** ${decision.timestamp || (/* @__PURE__ */ new Date()).toISOString()}`,
67
+ `- **Context:** ${decision.context}`,
68
+ `- **Options:** ${decision.options.join(", ")}`,
69
+ `- **Rationale:** ${decision.rationale}`,
70
+ `- **Outcome:** ${decision.outcome}`,
71
+ ""
72
+ ].join("\n");
73
+ const existing = sessionMemory.getMemoryContent("decisions");
74
+ sessionMemory.writeMemory("decisions", existing + entry);
75
+ sessionMemory.recordToolCall("kuma_decision", { title: decision.title });
76
+ return `\u2705 Decision "${decision.title}" recorded.`;
77
+ } catch (err) {
78
+ return `Error recording decision: ${err}`;
79
+ }
80
+ }
81
+ var decisionCooldown = 0;
82
+ function shouldRecordDecision() {
83
+ const history = sessionMemory.getToolCallHistory(30);
84
+ const edits = history.filter((c) => c.toolName === "precise_diff_editor");
85
+ if (edits.length > decisionCooldown + 10 && edits.length >= 10) {
86
+ decisionCooldown = edits.length;
87
+ return {
88
+ worth: true,
89
+ title: `Significant edits (${edits.length} changes)`
90
+ };
91
+ }
92
+ return { worth: false };
93
+ }
94
+ function getProactiveMemories() {
95
+ const summary = sessionMemory.getSummary();
96
+ const goal = summary.currentGoal || "";
97
+ const modifiedFiles = summary.modifiedFiles || [];
98
+ const context = [goal, ...modifiedFiles.map((f) => f.filePath || "")].join(" ");
99
+ if (!context.trim()) return "";
100
+ const memories = scoreMemoryRelevance(context, 3);
101
+ return formatScoredMemories(memories, goal || "current context");
102
+ }
103
+ function formatDecisionTemplate() {
104
+ return [
105
+ "\u{1F4DD} **Decision Recording Template**",
106
+ "\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",
107
+ "",
108
+ "Use kuma_decision({ action: 'record', ... }) to log important decisions:",
109
+ "",
110
+ "```",
111
+ "kuma_decision({",
112
+ " action: 'record',",
113
+ " title: 'Why Redis instead of in-memory cache',",
114
+ " context: 'Need stateless auth for mobile clients',",
115
+ " options: ['Redis', 'In-memory', 'Database'],",
116
+ " rationale: 'Latency <10ms with persistence required',",
117
+ " outcome: 'Redis chosen'",
118
+ "})",
119
+ "```",
120
+ "",
121
+ "\u{1F4A1} Call kuma_decision({ action: 'suggest' }) to check if now is a good time to record."
122
+ ].join("\n");
123
+ }
124
+
125
+ export {
126
+ scoreMemoryRelevance,
127
+ recordDecision,
128
+ shouldRecordDecision,
129
+ getProactiveMemories,
130
+ formatDecisionTemplate
131
+ };
@@ -1,6 +1,7 @@
1
1
  // src/engine/sessionMemory.ts
2
2
  import fs from "fs";
3
3
  import path from "path";
4
+ import { execSync } from "child_process";
4
5
  var SessionMemory = class {
5
6
  state;
6
7
  initialized = false;
@@ -557,6 +558,47 @@ No unresolved issues.`;
557
558
  }
558
559
  return { isLooping: false };
559
560
  }
561
+ // ============================================================
562
+ // V3: Change Tracking (Selective Undo)
563
+ // ============================================================
564
+ /**
565
+ * Track a file change with symbol-level detail.
566
+ * Stores in memory.json AND writes to change_log in DB.
567
+ */
568
+ async trackChange(filePath, changeType, symbol) {
569
+ this.ensureInit();
570
+ if (changeType === "modified" || changeType === "created") {
571
+ this.addModifiedFile(filePath);
572
+ }
573
+ try {
574
+ const { recordChange } = await import("./kumaDb-KN7Z4B5V.js");
575
+ const gitHash = this.getGitHead();
576
+ await recordChange({ filePath, changeType, symbol, gitCommitHash: gitHash || void 0 });
577
+ } catch {
578
+ }
579
+ this.save();
580
+ }
581
+ /**
582
+ * Get the current git HEAD hash.
583
+ */
584
+ getGitHead() {
585
+ try {
586
+ return execSync("git rev-parse HEAD", { encoding: "utf-8", timeout: 3e3 }).trim();
587
+ } catch {
588
+ return null;
589
+ }
590
+ }
591
+ /**
592
+ * Get changes for selective undo, grouped by session.
593
+ */
594
+ getChangesGrouped() {
595
+ this.ensureInit();
596
+ return [{
597
+ sessionId: String(this.state.startTime),
598
+ files: Array.from(this.state.modifiedFiles.keys()),
599
+ goal: this.state.currentGoal
600
+ }];
601
+ }
560
602
  ensureInit() {
561
603
  if (!this.initialized) {
562
604
  this.init({
@@ -567,46 +609,7 @@ No unresolved issues.`;
567
609
  }
568
610
  };
569
611
  var sessionMemory = new SessionMemory();
570
- function getSessionMemory(topic) {
571
- return sessionMemory.getSummary(topic);
572
- }
573
- function searchSessionMemory(params) {
574
- const { query, limit = 20 } = params;
575
- sessionMemory.recordToolCall("search_session_memory", { query, limit });
576
- const results = sessionMemory.searchMemory(query, limit);
577
- if (results.length === 0) {
578
- return `\u{1F50D} **Search Memory** \u2014 No results for "${query}".`;
579
- }
580
- const lines = [
581
- `\u{1F50D} **Search Memory** \u2014 ${results.length} results for "${query}"`,
582
- ""
583
- ];
584
- for (const r of results) {
585
- const icon = r.type.startsWith("tool:") ? "\u{1F6E0}\uFE0F" : r.type.startsWith("file:") ? r.type.includes("created") ? "\u2728" : "\u{1F4DD}" : r.type.startsWith("failure") ? "\u274C" : r.type.startsWith("memory") ? "\u{1F9E0}" : r.type === "search" ? "\u{1F50E}" : r.type === "dependency" ? "\u{1F517}" : "\u{1F4C4}";
586
- const timeStr = r.timestamp ? new Date(r.timestamp).toLocaleTimeString() : "";
587
- lines.push(`${icon} ${timeStr ? `[${timeStr}] ` : ""}${r.content}`);
588
- }
589
- lines.push("", `\u{1F4A1} Use get_session_memory({topic: "..."}) to load a specific memory topic.`);
590
- return lines.join("\n");
591
- }
592
- function handleWriteMemory(params) {
593
- const { topic, content, mode = "append" } = params;
594
- const existing = sessionMemory.getMemoryContent(topic);
595
- let finalContent = content;
596
- if (mode === "prepend") {
597
- finalContent = content + "\n\n" + existing;
598
- } else if (mode === "append") {
599
- finalContent = existing + "\n\n" + content;
600
- }
601
- sessionMemory.writeMemory(topic, finalContent);
602
- sessionMemory.recordToolCall("write_memory", { topic, mode });
603
- return `\u2705 Memory "${topic}" updated (mode: ${mode}).`;
604
- }
605
612
 
606
613
  export {
607
- SessionMemory,
608
- sessionMemory,
609
- getSessionMemory,
610
- searchSessionMemory,
611
- handleWriteMemory
614
+ sessionMemory
612
615
  };
@@ -0,0 +1,71 @@
1
+ import {
2
+ sessionMemory
3
+ } from "./chunk-TT37TE4P.js";
4
+ import {
5
+ getProjectRoot
6
+ } from "./chunk-IXMWW5WA.js";
7
+
8
+ // src/utils/kumaShared.ts
9
+ import { execSync } from "child_process";
10
+ function getSessionStats(inputGoal) {
11
+ const summary = sessionMemory.getSummary();
12
+ const goal = inputGoal || summary.currentGoal || "";
13
+ const modifiedFiles = sessionMemory.getModifiedFiles();
14
+ const toolCalls = sessionMemory.getToolCallHistory(50);
15
+ const failedFiles = sessionMemory.getFailedFiles();
16
+ const loop = sessionMemory.detectLoop();
17
+ return {
18
+ goal,
19
+ modifiedFiles,
20
+ toolCalls,
21
+ toolCallCount: toolCalls.length,
22
+ failedFiles,
23
+ hasLoop: loop.isLooping,
24
+ loopMessage: loop.message,
25
+ hasRunTests: toolCalls.some((c) => c.toolName === "execute_safe_test")
26
+ };
27
+ }
28
+ function getGitDiffStat(timeout = 3e3) {
29
+ try {
30
+ const root = getProjectRoot();
31
+ return execSync("git diff --stat", {
32
+ cwd: root,
33
+ encoding: "utf-8",
34
+ timeout
35
+ }).trim();
36
+ } catch {
37
+ return "";
38
+ }
39
+ }
40
+ function getUnresolvedCount(failedFiles) {
41
+ let count = 0;
42
+ for (const f of failedFiles) {
43
+ for (const ff of f.failures) {
44
+ if (!ff.resolved) count++;
45
+ }
46
+ }
47
+ return count;
48
+ }
49
+ function buildDriftMessages(modifiedFiles, hasRunTests, unresolvedCount, gitStat, loopMessage) {
50
+ const drifts = [];
51
+ if (modifiedFiles > 0 && !hasRunTests) {
52
+ drifts.push(`${modifiedFiles} file(s) edited but no test run`);
53
+ }
54
+ if (loopMessage) {
55
+ drifts.push(loopMessage);
56
+ }
57
+ if (unresolvedCount > 0) {
58
+ drifts.push(`${unresolvedCount} unresolved failure(s)`);
59
+ }
60
+ if (gitStat) {
61
+ drifts.push(`Git diff: ${gitStat}`);
62
+ }
63
+ return drifts;
64
+ }
65
+
66
+ export {
67
+ getSessionStats,
68
+ getGitDiffStat,
69
+ getUnresolvedCount,
70
+ buildDriftMessages
71
+ };