@plumpslabs/kuma 2.1.5 → 2.2.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.
package/dist/index.js CHANGED
@@ -1,14 +1,23 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ getSessionMemory,
4
+ handleWriteMemory,
5
+ searchSessionMemory,
6
+ sessionMemory
7
+ } from "./chunk-QPVPFUCQ.js";
2
8
  import {
3
9
  ALL_CONFIG_TYPES,
4
- ensureBackupDir,
5
10
  formatInitResults,
11
+ runInit
12
+ } from "./chunk-EM6IK2TB.js";
13
+ import {
14
+ ensureBackupDir,
6
15
  getBackupPath,
16
+ getKumaDir,
7
17
  getProjectRoot,
8
- runInit,
9
18
  validateFileExtension,
10
19
  validateFilePath
11
- } from "./chunk-2BFTJMBP.js";
20
+ } from "./chunk-7Q3YUJSM.js";
12
21
 
13
22
  // src/index.ts
14
23
  import { readFileSync } from "fs";
@@ -21,8 +30,8 @@ import { z } from "zod";
21
30
 
22
31
  // src/tools/smartGrep.ts
23
32
  import fg from "fast-glob";
24
- import fs2 from "fs";
25
- import path2 from "path";
33
+ import fs from "fs";
34
+ import path from "path";
26
35
 
27
36
  // src/utils/tokenCounter.ts
28
37
  function estimateTokens(text) {
@@ -46,611 +55,6 @@ function limitLines(text, maxLines) {
46
55
  [...${lines.length - maxLines} more lines truncated]`;
47
56
  }
48
57
 
49
- // src/engine/sessionMemory.ts
50
- import fs from "fs";
51
- import path from "path";
52
- var SessionMemory = class {
53
- state;
54
- initialized = false;
55
- init(config) {
56
- const kumaDir = path.join(config.projectRoot, ".kuma");
57
- const sessionFile = path.join(kumaDir, "memory.json");
58
- const oldFile = path.join(kumaDir, ".kuma-memory.json");
59
- if (fs.existsSync(oldFile) && !fs.existsSync(sessionFile)) {
60
- try {
61
- fs.renameSync(oldFile, sessionFile);
62
- console.error("[SessionMemory] Migrated .kuma-memory.json \u2192 memory.json");
63
- } catch {
64
- }
65
- }
66
- const oldSessionFile = path.join(kumaDir, "session.json");
67
- if (fs.existsSync(oldSessionFile) && !fs.existsSync(sessionFile)) {
68
- try {
69
- fs.renameSync(oldSessionFile, sessionFile);
70
- console.error("[SessionMemory] Migrated session.json \u2192 memory.json");
71
- } catch {
72
- }
73
- }
74
- if (fs.existsSync(sessionFile)) {
75
- try {
76
- const raw = fs.readFileSync(sessionFile, "utf-8");
77
- const parsed = JSON.parse(raw);
78
- this.state = {
79
- projectRoot: parsed.projectRoot || config.projectRoot,
80
- startTime: parsed.startTime || config.startTime,
81
- currentGoal: parsed.currentGoal || "",
82
- completedSteps: parsed.completedSteps || [],
83
- modifiedFiles: new Map(parsed.modifiedFiles || []),
84
- failedFiles: new Map(parsed.failedFiles || []),
85
- searchResults: new Map(parsed.searchResults || []),
86
- dependencyGraph: new Map(parsed.dependencyGraph || []),
87
- toolCalls: parsed.toolCalls || [],
88
- conventions: parsed.conventions
89
- };
90
- this.initialized = true;
91
- console.error(`[SessionMemory] Loaded persistent session memory.json`);
92
- return;
93
- } catch (err) {
94
- console.error(`[SessionMemory] Failed to load persistent session: ${err}. Re-initializing.`);
95
- }
96
- }
97
- this.state = {
98
- projectRoot: config.projectRoot,
99
- startTime: config.startTime,
100
- currentGoal: "",
101
- completedSteps: [],
102
- modifiedFiles: /* @__PURE__ */ new Map(),
103
- failedFiles: /* @__PURE__ */ new Map(),
104
- searchResults: /* @__PURE__ */ new Map(),
105
- dependencyGraph: /* @__PURE__ */ new Map(),
106
- toolCalls: []
107
- };
108
- this.initialized = true;
109
- this.save();
110
- this.ensureMemoriesDir();
111
- console.error(`[SessionMemory] Initialized at ${new Date(config.startTime).toISOString()}`);
112
- }
113
- memoriesDir() {
114
- return path.join(this.state.projectRoot, ".kuma", "memories");
115
- }
116
- ensureMemoriesDir() {
117
- const dir = this.memoriesDir();
118
- if (!fs.existsSync(dir)) {
119
- fs.mkdirSync(dir, { recursive: true });
120
- }
121
- }
122
- memoryFilePath(topic) {
123
- return path.join(this.memoriesDir(), `${topic}.md`);
124
- }
125
- getMemoryContent(topic) {
126
- this.ensureInit();
127
- this.ensureMemoriesDir();
128
- const filePath = this.memoryFilePath(topic);
129
- if (fs.existsSync(filePath)) {
130
- return fs.readFileSync(filePath, "utf-8");
131
- }
132
- return this.generateMemory(topic);
133
- }
134
- writeMemory(topic, content) {
135
- this.ensureInit();
136
- this.ensureMemoriesDir();
137
- this.writeMemoryFile(topic, content);
138
- }
139
- writeMemoryFile(topic, content) {
140
- const filePath = this.memoryFilePath(topic);
141
- fs.writeFileSync(filePath, content, "utf-8");
142
- }
143
- generateMemory(topic) {
144
- switch (topic) {
145
- case "architecture":
146
- return this.generateArchitectureMd();
147
- case "conventions":
148
- return this.generateConventionsMd();
149
- case "known-issues":
150
- return this.generateKnownIssuesMd();
151
- default:
152
- return `# ${topic}
153
-
154
- (empty)`;
155
- }
156
- }
157
- generateArchitectureMd() {
158
- const conv = this.state.conventions;
159
- if (!conv) return `# Architecture
160
-
161
- Run project_conventions first to auto-generate.`;
162
- const stackKeys = ["framework", "projectType", "monorepo", "buildTool", "packageManager"];
163
- const lines = [
164
- "# Architecture",
165
- "",
166
- `Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
167
- "",
168
- "## Stack",
169
- ""
170
- ];
171
- for (const key of stackKeys) {
172
- if (conv[key] !== void 0) {
173
- lines.push(`- **${key}**: ${JSON.stringify(conv[key])}`);
174
- }
175
- }
176
- lines.push("", "## Project Layout", "");
177
- if (conv.workspaces) {
178
- lines.push(`- Monorepo workspaces: ${JSON.stringify(conv.workspaces)}`);
179
- }
180
- lines.push(`- Source root: ${conv.srcDir || "src/"}`);
181
- return lines.join("\n");
182
- }
183
- generateConventionsMd() {
184
- const conv = this.state.conventions;
185
- if (!conv) return `# Conventions
186
-
187
- Run project_conventions first to auto-generate.`;
188
- const styleKeys = ["testRunner", "styling", "importAlias", "lintConfig", "codeStyle"];
189
- const lines = [
190
- "# Conventions",
191
- "",
192
- `Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
193
- "",
194
- "## Code Style",
195
- ""
196
- ];
197
- for (const key of styleKeys) {
198
- if (conv[key] !== void 0) {
199
- lines.push(`- **${key}**: ${JSON.stringify(conv[key])}`);
200
- }
201
- }
202
- if (conv.testRunner) {
203
- lines.push("", `## Testing
204
-
205
- - Test runner: ${conv.testRunner}`);
206
- }
207
- if (conv.importAlias) {
208
- lines.push("", `## Imports
209
-
210
- - Alias: \`${conv.importAlias}\``);
211
- }
212
- return lines.join("\n");
213
- }
214
- generateKnownIssuesMd() {
215
- const allFailures = this.getFailedFiles();
216
- const unresolved = allFailures.filter((f) => f.failures.some((ff) => !ff.resolved));
217
- if (unresolved.length === 0) return `# Known Issues
218
-
219
- No unresolved issues.`;
220
- const lines = [
221
- "# Known Issues",
222
- "",
223
- `Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
224
- ""
225
- ];
226
- for (const f of unresolved) {
227
- lines.push(`## ${f.task}`);
228
- for (const ff of f.failures) {
229
- if (!ff.resolved) {
230
- lines.push(`- ${new Date(ff.timestamp).toISOString()}: ${ff.error}`);
231
- }
232
- }
233
- lines.push("");
234
- }
235
- return lines.join("\n");
236
- }
237
- save() {
238
- if (!this.initialized || !this.state) return;
239
- try {
240
- const kumaDir = path.join(this.state.projectRoot, ".kuma");
241
- if (!fs.existsSync(kumaDir)) {
242
- fs.mkdirSync(kumaDir, { recursive: true });
243
- }
244
- const serialized = {
245
- projectRoot: this.state.projectRoot,
246
- startTime: this.state.startTime,
247
- currentGoal: this.state.currentGoal,
248
- completedSteps: this.state.completedSteps,
249
- modifiedFiles: Array.from(this.state.modifiedFiles.entries()),
250
- failedFiles: Array.from(this.state.failedFiles.entries()),
251
- searchResults: Array.from(this.state.searchResults.entries()),
252
- dependencyGraph: Array.from(this.state.dependencyGraph.entries()),
253
- toolCalls: this.state.toolCalls,
254
- conventions: this.state.conventions
255
- };
256
- fs.writeFileSync(path.join(kumaDir, "memory.json"), JSON.stringify(serialized, null, 2), "utf-8");
257
- } catch (err) {
258
- console.error(`[SessionMemory] Failed to save session: ${err}`);
259
- }
260
- }
261
- setGoal(goal) {
262
- this.ensureInit();
263
- this.state.currentGoal = goal;
264
- this.save();
265
- }
266
- addCompletedStep(step) {
267
- this.ensureInit();
268
- if (!this.state.completedSteps.includes(step)) {
269
- this.state.completedSteps.push(step);
270
- this.save();
271
- }
272
- }
273
- addModifiedFile(filePath) {
274
- this.ensureInit();
275
- const existing = this.state.modifiedFiles.get(filePath);
276
- if (existing) {
277
- existing.modifiedAt = Date.now();
278
- existing.status = "modified";
279
- } else {
280
- this.state.modifiedFiles.set(filePath, {
281
- filePath,
282
- modifiedAt: Date.now(),
283
- status: "modified"
284
- });
285
- }
286
- this.save();
287
- }
288
- addCreatedFile(filePath) {
289
- this.ensureInit();
290
- this.state.modifiedFiles.set(filePath, {
291
- filePath,
292
- modifiedAt: Date.now(),
293
- status: "created"
294
- });
295
- this.save();
296
- }
297
- addFailedFile(task, error) {
298
- this.ensureInit();
299
- const trimmedError = error?.trim() ?? "";
300
- if (!trimmedError) return;
301
- const truncatedError = trimmedError.substring(0, 500);
302
- const failures = this.state.failedFiles.get(task) ?? [];
303
- const lastUnresolved = [...failures].reverse().find((f) => !f.resolved);
304
- if (lastUnresolved && lastUnresolved.error === truncatedError) return;
305
- failures.push({
306
- task,
307
- error: truncatedError,
308
- timestamp: Date.now(),
309
- resolved: false
310
- });
311
- this.state.failedFiles.set(task, failures);
312
- this.save();
313
- this.ensureMemoriesDir();
314
- this.writeMemoryFile("known-issues", this.generateKnownIssuesMd());
315
- }
316
- markFailureResolved(task) {
317
- this.ensureInit();
318
- const failures = this.state.failedFiles.get(task);
319
- if (failures) {
320
- let changed = false;
321
- for (const f of failures) {
322
- if (!f.resolved) {
323
- f.resolved = true;
324
- changed = true;
325
- }
326
- }
327
- if (changed) {
328
- this.save();
329
- this.ensureMemoriesDir();
330
- this.writeMemoryFile("known-issues", this.generateKnownIssuesMd());
331
- }
332
- }
333
- }
334
- addSearchResult(query, files) {
335
- this.ensureInit();
336
- this.state.searchResults.set(query, files);
337
- this.save();
338
- }
339
- addDependency(file, dependsOn) {
340
- this.ensureInit();
341
- const deps = this.state.dependencyGraph.get(file) ?? [];
342
- if (!deps.includes(dependsOn)) {
343
- deps.push(dependsOn);
344
- this.state.dependencyGraph.set(file, deps);
345
- this.save();
346
- }
347
- }
348
- recordToolCall(toolName, params) {
349
- this.ensureInit();
350
- this.state.toolCalls.push({
351
- toolName,
352
- params,
353
- timestamp: Date.now()
354
- });
355
- if (this.state.toolCalls.length > 100) {
356
- this.state.toolCalls = this.state.toolCalls.slice(-100);
357
- }
358
- this.save();
359
- }
360
- setConventions(conventions) {
361
- this.ensureInit();
362
- this.state.conventions = conventions;
363
- this.save();
364
- this.ensureMemoriesDir();
365
- this.writeMemoryFile("architecture", this.generateArchitectureMd());
366
- this.writeMemoryFile("conventions", this.generateConventionsMd());
367
- }
368
- // ============================================================
369
- // KEYWORD SEARCH — Search session memory content
370
- // ============================================================
371
- /**
372
- * Load previous session data into current session.
373
- * Called by kuma_init() to hydrate conventions, failures, and modified files
374
- * from a prior session stored in memory.json.
375
- */
376
- loadSession() {
377
- this.ensureInit();
378
- const kumaDir = path.join(this.state.projectRoot, ".kuma");
379
- const sessionFile = path.join(kumaDir, "memory.json");
380
- if (!fs.existsSync(sessionFile)) {
381
- return { hasPrevSession: false, toolCallCount: 0, hasConventions: !!this.state.conventions };
382
- }
383
- try {
384
- const raw = fs.readFileSync(sessionFile, "utf-8");
385
- const parsed = JSON.parse(raw);
386
- if (!this.state.conventions && parsed.conventions) {
387
- this.state.conventions = parsed.conventions;
388
- }
389
- const prevFailed = parsed.failedFiles;
390
- if (prevFailed) {
391
- for (const [task, failures] of prevFailed) {
392
- for (const f of failures) {
393
- if (!f.resolved) {
394
- const existing = this.state.failedFiles.get(task);
395
- if (!existing?.some((ef) => ef.error === f.error)) {
396
- this.state.failedFiles.set(task, [...existing || [], f]);
397
- }
398
- }
399
- }
400
- }
401
- }
402
- const prevModified = parsed.modifiedFiles;
403
- if (prevModified) {
404
- for (const [filePath, mod] of prevModified) {
405
- if (!this.state.modifiedFiles.has(filePath)) {
406
- this.state.modifiedFiles.set(filePath, {
407
- filePath,
408
- modifiedAt: mod.modifiedAt,
409
- status: mod.status
410
- });
411
- }
412
- }
413
- }
414
- const prevSearch = parsed.searchResults;
415
- if (prevSearch) {
416
- for (const [query, files] of prevSearch) {
417
- if (!this.state.searchResults.has(query)) {
418
- this.state.searchResults.set(query, files);
419
- }
420
- }
421
- }
422
- const toolCallCount = parsed.toolCalls?.length ?? 0;
423
- this.save();
424
- return {
425
- hasPrevSession: true,
426
- toolCallCount,
427
- hasConventions: !!this.state.conventions
428
- };
429
- } catch {
430
- return { hasPrevSession: false, toolCallCount: 0, hasConventions: !!this.state.conventions };
431
- }
432
- }
433
- /**
434
- * Search through tool call history, memory files, search results,
435
- * errors, and file modifications for a keyword.
436
- */
437
- searchMemory(query, limit = 20) {
438
- this.ensureInit();
439
- const results = [];
440
- const q = query.toLowerCase();
441
- for (const call of this.state.toolCalls) {
442
- const paramStr = JSON.stringify(call.params);
443
- if (call.toolName.toLowerCase().includes(q) || paramStr.toLowerCase().includes(q)) {
444
- results.push({
445
- type: `tool:${call.toolName}`,
446
- content: `${call.toolName}(${JSON.stringify(call.params).substring(0, 200)})`,
447
- timestamp: call.timestamp
448
- });
449
- }
450
- }
451
- for (const [queryStr, files] of this.state.searchResults) {
452
- if (queryStr.toLowerCase().includes(q) || files.some((f) => f.toLowerCase().includes(q))) {
453
- results.push({
454
- type: "search",
455
- content: `Query: "${queryStr}" \u2192 ${files.slice(0, 5).join(", ")}${files.length > 5 ? ` (+${files.length - 5} more)` : ""}`
456
- });
457
- }
458
- }
459
- for (const [, mod] of this.state.modifiedFiles) {
460
- if (mod.filePath.toLowerCase().includes(q)) {
461
- results.push({
462
- type: `file:${mod.status}`,
463
- content: `${mod.status.toUpperCase()}: ${mod.filePath}`,
464
- timestamp: mod.modifiedAt
465
- });
466
- }
467
- }
468
- for (const [task, failures] of this.state.failedFiles) {
469
- for (const f of failures) {
470
- if (task.toLowerCase().includes(q) || f.error.toLowerCase().includes(q)) {
471
- results.push({
472
- type: `failure:${task}`,
473
- content: `[${f.resolved ? "RESOLVED" : "UNRESOLVED"}] ${task}: ${f.error.substring(0, 200)}`,
474
- timestamp: f.timestamp
475
- });
476
- }
477
- }
478
- }
479
- for (const [file, deps] of this.state.dependencyGraph) {
480
- if (file.toLowerCase().includes(q)) {
481
- results.push({
482
- type: "dependency",
483
- content: `${file} depends on: ${deps.join(", ")}`
484
- });
485
- }
486
- }
487
- const memoriesDir = this.memoriesDir();
488
- if (fs.existsSync(memoriesDir)) {
489
- try {
490
- const files = fs.readdirSync(memoriesDir);
491
- for (const file of files) {
492
- if (!file.endsWith(".md")) continue;
493
- const filePath = path.join(memoriesDir, file);
494
- try {
495
- const content = fs.readFileSync(filePath, "utf-8");
496
- const lines = content.split("\n");
497
- for (let i = 0; i < lines.length; i++) {
498
- if (lines[i].toLowerCase().includes(q)) {
499
- const topicName = file.replace(/\.md$/, "");
500
- results.push({
501
- type: `memory:${topicName}`,
502
- content: `[${topicName}] L${i + 1}: ${lines[i].trim().substring(0, 150)}`
503
- });
504
- if (results.filter((r) => r.type.startsWith("memory")).length >= 5) break;
505
- }
506
- }
507
- } catch {
508
- }
509
- }
510
- } catch {
511
- }
512
- }
513
- results.sort((a, b) => {
514
- if (a.timestamp && b.timestamp) return b.timestamp - a.timestamp;
515
- if (a.timestamp) return -1;
516
- if (b.timestamp) return 1;
517
- return 0;
518
- });
519
- return results.slice(0, limit);
520
- }
521
- getConventions() {
522
- this.ensureInit();
523
- return this.state.conventions;
524
- }
525
- pruneMemory() {
526
- this.ensureInit();
527
- this.state.toolCalls = this.state.toolCalls.slice(-3);
528
- this.state.searchResults.clear();
529
- this.state.completedSteps = [];
530
- this.save();
531
- }
532
- getSummary(topic) {
533
- this.ensureInit();
534
- if (topic) {
535
- const content = this.getMemoryContent(topic);
536
- return { topic, content };
537
- }
538
- const unresolvedFailures = [];
539
- const seen = /* @__PURE__ */ new Set();
540
- for (const [, failures] of this.state.failedFiles) {
541
- for (const f of failures) {
542
- if (f.resolved) continue;
543
- const error = f.error.substring(0, 200);
544
- if (!error.trim()) continue;
545
- const key = `${f.task}::${error}`;
546
- if (seen.has(key)) continue;
547
- seen.add(key);
548
- unresolvedFailures.push({ task: f.task, error });
549
- }
550
- }
551
- const hasMemories = fs.existsSync(this.memoriesDir());
552
- let availableMemories = [];
553
- if (hasMemories) {
554
- try {
555
- availableMemories = fs.readdirSync(this.memoriesDir()).filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
556
- } catch {
557
- }
558
- }
559
- return {
560
- projectRoot: this.state.projectRoot,
561
- sessionDuration: Math.round((Date.now() - this.state.startTime) / 1e3) + "s",
562
- currentGoal: this.state.currentGoal,
563
- completedSteps: this.state.completedSteps,
564
- modifiedFiles: Array.from(this.state.modifiedFiles.values()),
565
- unresolvedFailures,
566
- toolCallCount: this.state.toolCalls.length,
567
- recentSearches: Array.from(this.state.searchResults.keys()).slice(-5),
568
- hasConventions: !!this.state.conventions,
569
- ...availableMemories.length > 0 ? { availableMemories } : {},
570
- ...availableMemories.length > 0 ? { hint: `Use get_session_memory({ topic: "<name>" }) to load a specific memory. Topics: ${availableMemories.join(", ")}` } : {}
571
- };
572
- }
573
- getModifiedFiles() {
574
- this.ensureInit();
575
- return Array.from(this.state.modifiedFiles.values());
576
- }
577
- getFailedFiles() {
578
- this.ensureInit();
579
- const result = [];
580
- for (const [task, failures] of this.state.failedFiles) {
581
- result.push({ task, failures });
582
- }
583
- return result;
584
- }
585
- getToolCallHistory(limit = 10) {
586
- this.ensureInit();
587
- return this.state.toolCalls.slice(-limit);
588
- }
589
- detectLoop() {
590
- this.ensureInit();
591
- const recentCalls = this.state.toolCalls.slice(-10);
592
- if (recentCalls.length < 6) return { isLooping: false };
593
- const toolCounts = /* @__PURE__ */ new Map();
594
- for (const call of recentCalls) {
595
- toolCounts.set(call.toolName, (toolCounts.get(call.toolName) ?? 0) + 1);
596
- }
597
- for (const [toolName, count] of toolCounts) {
598
- if (count >= 4) {
599
- return {
600
- isLooping: true,
601
- toolName,
602
- message: `Detected potential loop: "${toolName}" called ${count} times in last ${recentCalls.length} tool calls. Consider a different approach.`
603
- };
604
- }
605
- }
606
- return { isLooping: false };
607
- }
608
- ensureInit() {
609
- if (!this.initialized) {
610
- this.init({
611
- projectRoot: process.cwd(),
612
- startTime: Date.now()
613
- });
614
- }
615
- }
616
- };
617
- var sessionMemory = new SessionMemory();
618
- function getSessionMemory(topic) {
619
- return sessionMemory.getSummary(topic);
620
- }
621
- function searchSessionMemory(params) {
622
- const { query, limit = 20 } = params;
623
- sessionMemory.recordToolCall("search_session_memory", { query, limit });
624
- const results = sessionMemory.searchMemory(query, limit);
625
- if (results.length === 0) {
626
- return `\u{1F50D} **Search Memory** \u2014 No results for "${query}".`;
627
- }
628
- const lines = [
629
- `\u{1F50D} **Search Memory** \u2014 ${results.length} results for "${query}"`,
630
- ""
631
- ];
632
- for (const r of results) {
633
- 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}";
634
- const timeStr = r.timestamp ? new Date(r.timestamp).toLocaleTimeString() : "";
635
- lines.push(`${icon} ${timeStr ? `[${timeStr}] ` : ""}${r.content}`);
636
- }
637
- lines.push("", `\u{1F4A1} Use get_session_memory({topic: "..."}) to load a specific memory topic.`);
638
- return lines.join("\n");
639
- }
640
- function handleWriteMemory(params) {
641
- const { topic, content, mode = "append" } = params;
642
- const existing = sessionMemory.getMemoryContent(topic);
643
- let finalContent = content;
644
- if (mode === "prepend") {
645
- finalContent = content + "\n\n" + existing;
646
- } else if (mode === "append") {
647
- finalContent = existing + "\n\n" + content;
648
- }
649
- sessionMemory.writeMemory(topic, finalContent);
650
- sessionMemory.recordToolCall("write_memory", { topic, mode });
651
- return `\u2705 Memory "${topic}" updated (mode: ${mode}).`;
652
- }
653
-
654
58
  // src/tools/smartGrep.ts
655
59
  var IGNORE_PATTERNS = [
656
60
  "**/node_modules/**",
@@ -659,7 +63,7 @@ var IGNORE_PATTERNS = [
659
63
  "**/build/**",
660
64
  "**/.git/**",
661
65
  "**/.cache/**",
662
- "**/.agent-backups/**",
66
+ "**/.kuma/backups/**",
663
67
  "**/*.min.js",
664
68
  "**/*.bundle.js",
665
69
  "**/*.map",
@@ -694,7 +98,7 @@ async function handleSmartGrep(params) {
694
98
  }
695
99
  const projectRoot = getProjectRoot();
696
100
  try {
697
- const searchPattern = targetFolder ? path2.join(targetFolder, "**/*").replace(/\\/g, "/") : "**/*";
101
+ const searchPattern = targetFolder ? path.join(targetFolder, "**/*").replace(/\\/g, "/") : "**/*";
698
102
  let entries = await fg(searchPattern, {
699
103
  cwd: projectRoot,
700
104
  ignore: IGNORE_PATTERNS,
@@ -710,7 +114,7 @@ async function handleSmartGrep(params) {
710
114
  (e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`
711
115
  );
712
116
  entries = entries.filter((entry) => {
713
- const ext = path2.extname(entry).toLowerCase();
117
+ const ext = path.extname(entry).toLowerCase();
714
118
  return normalizedExts.includes(ext);
715
119
  });
716
120
  }
@@ -726,11 +130,11 @@ async function handleSmartGrep(params) {
726
130
  if (results.length >= maxResults) break;
727
131
  filesScanned++;
728
132
  try {
729
- const fullPath = path2.join(projectRoot, entry);
730
- const stat = fs2.statSync(fullPath);
133
+ const fullPath = path.join(projectRoot, entry);
134
+ const stat = fs.statSync(fullPath);
731
135
  if (stat.size > 5e5) continue;
732
136
  if (isBinaryFile(fullPath)) continue;
733
- const content = fs2.readFileSync(fullPath, "utf-8");
137
+ const content = fs.readFileSync(fullPath, "utf-8");
734
138
  const lines = content.split("\n");
735
139
  for (let i = 0; i < lines.length; i++) {
736
140
  if (results.length >= maxResults) break;
@@ -799,9 +203,9 @@ ${r.content.split("\n").map((l) => ` ${l}`).join("\n")}`;
799
203
  function isBinaryFile(filePath) {
800
204
  try {
801
205
  const buffer = Buffer.alloc(512);
802
- const fd = fs2.openSync(filePath, "r");
803
- const bytesRead = fs2.readSync(fd, buffer, 0, 512, 0);
804
- fs2.closeSync(fd);
206
+ const fd = fs.openSync(filePath, "r");
207
+ const bytesRead = fs.readSync(fd, buffer, 0, 512, 0);
208
+ fs.closeSync(fd);
805
209
  for (let i = 0; i < bytesRead; i++) {
806
210
  if (buffer[i] === 0) return true;
807
211
  }
@@ -812,8 +216,8 @@ function isBinaryFile(filePath) {
812
216
  }
813
217
 
814
218
  // src/tools/smartFilePicker.ts
815
- import fs3 from "fs";
816
- import path3 from "path";
219
+ import fs2 from "fs";
220
+ import path2 from "path";
817
221
  var MAX_FILE_SIZE = 1e6;
818
222
  var CHUNK_THRESHOLD = 300;
819
223
  async function handleSmartFilePicker(params) {
@@ -823,23 +227,23 @@ async function handleSmartFilePicker(params) {
823
227
  return "Error: " + validation.error.message;
824
228
  }
825
229
  const resolvedPath = validation.resolvedPath;
826
- if (!fs3.existsSync(resolvedPath)) {
230
+ if (!fs2.existsSync(resolvedPath)) {
827
231
  const projectRoot = getProjectRoot();
828
232
  let suggestion;
829
- if (!path3.isAbsolute(filePath)) {
830
- const cwdPath = path3.resolve(process.cwd(), filePath);
233
+ if (!path2.isAbsolute(filePath)) {
234
+ const cwdPath = path2.resolve(process.cwd(), filePath);
831
235
  suggestion = "Path resolved to: " + resolvedPath + "\nCWD: " + process.cwd() + "\nProject root: " + projectRoot + "\nHint: Try path relative to project root, or relative to CWD (" + cwdPath + ")\nTry smart_grep first to locate the correct file.";
832
236
  } else {
833
237
  suggestion = "File does not exist. Try smart_grep to locate it.";
834
238
  }
835
239
  return 'Error: File not found: "' + filePath + '".\n' + suggestion;
836
240
  }
837
- const stat = fs3.statSync(resolvedPath);
241
+ const stat = fs2.statSync(resolvedPath);
838
242
  if (stat.size > MAX_FILE_SIZE) {
839
243
  return "Error: File too large (" + (stat.size / 1024 / 1024).toFixed(1) + "MB). Max 1MB.\nUse smart_grep to find specific content instead.";
840
244
  }
841
245
  try {
842
- const content = fs3.readFileSync(resolvedPath, "utf-8");
246
+ const content = fs2.readFileSync(resolvedPath, "utf-8");
843
247
  const lines = content.split("\n");
844
248
  const totalLines = lines.length;
845
249
  sessionMemory.recordToolCall("smart_file_picker", {
@@ -989,8 +393,8 @@ function extractKeyDeclarations(lines) {
989
393
  }
990
394
 
991
395
  // src/tools/preciseDiffEditor.ts
992
- import fs4 from "fs";
993
- import path4 from "path";
396
+ import fs3 from "fs";
397
+ import path3 from "path";
994
398
 
995
399
  // src/utils/errorHandler.ts
996
400
  var CircuitBreakerStore = class {
@@ -1046,10 +450,13 @@ var CircuitBreakerStore = class {
1046
450
  var circuitBreaker = new CircuitBreakerStore();
1047
451
 
1048
452
  // src/tools/preciseDiffEditor.ts
453
+ function generateEditId() {
454
+ return `edit-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
455
+ }
1049
456
  async function handlePreciseDiffEditor(params) {
1050
- const { filePath, edits, dryRun = false, action } = params;
457
+ const { filePath, edits, dryRun = false, action, scope } = params;
1051
458
  if (action === "rollback") {
1052
- return handleRollbackEdit({ filePath, version: params.version });
459
+ return handleRollbackEdit({ filePath, version: params.version, scope: scope || "file", editId: params.editId });
1053
460
  }
1054
461
  if (!edits || edits.length === 0) {
1055
462
  return "Error: 'edits' required for edit mode, or use action: 'rollback' for rollback.";
@@ -1065,12 +472,12 @@ async function handlePreciseDiffEditor(params) {
1065
472
 
1066
473
  Try reading the file first with smart_file_picker to verify current content.`;
1067
474
  }
1068
- if (!fs4.existsSync(resolvedPath)) {
475
+ if (!fs3.existsSync(resolvedPath)) {
1069
476
  return `Error: File not found: "${filePath}".
1070
477
  Use batch_file_writer to create a new file.`;
1071
478
  }
1072
479
  try {
1073
- const originalContent = fs4.readFileSync(resolvedPath, "utf-8");
480
+ const originalContent = fs3.readFileSync(resolvedPath, "utf-8");
1074
481
  let currentContent = originalContent;
1075
482
  const results = [];
1076
483
  for (let i = 0; i < edits.length; i++) {
@@ -1078,10 +485,16 @@ Use batch_file_writer to create a new file.`;
1078
485
  const result = applyEdit(currentContent, edit, resolvedPath, i, dryRun);
1079
486
  if (result.success) {
1080
487
  if (!dryRun) {
1081
- fs4.writeFileSync(resolvedPath, result.details, "utf-8");
488
+ const editId = generateEditId();
489
+ result.editId = editId;
490
+ fs3.writeFileSync(resolvedPath, result.details, "utf-8");
491
+ if (result.backupPath) {
492
+ updateBackupManifest(result.backupPath, editId, filePath, i);
493
+ }
1082
494
  sessionMemory.recordToolCall("precise_diff_editor", {
1083
495
  filePath,
1084
496
  editIndex: i,
497
+ editId,
1085
498
  success: true,
1086
499
  matched: result.matched
1087
500
  });
@@ -1268,11 +681,29 @@ function findNearestLine(content, search) {
1268
681
  function createBackup(filePath) {
1269
682
  ensureBackupDir();
1270
683
  const backupPath = getBackupPath(filePath);
1271
- const backupDir = path4.dirname(backupPath);
1272
- fs4.mkdirSync(backupDir, { recursive: true });
1273
- fs4.copyFileSync(filePath, backupPath);
684
+ const backupDir = path3.dirname(backupPath);
685
+ fs3.mkdirSync(backupDir, { recursive: true });
686
+ fs3.copyFileSync(filePath, backupPath);
1274
687
  return backupPath;
1275
688
  }
689
+ function updateBackupManifest(backupPath, editId, filePath, editIndex) {
690
+ try {
691
+ const backupDir = path3.dirname(backupPath);
692
+ const manifestPath = path3.join(backupDir, "backup_manifest.json");
693
+ let manifest;
694
+ if (fs3.existsSync(manifestPath)) {
695
+ const existing = fs3.readFileSync(manifestPath, "utf-8");
696
+ manifest = JSON.parse(existing);
697
+ } else {
698
+ const tsDir = path3.basename(backupDir);
699
+ manifest = { timestamp: Number(tsDir) || Date.now(), edits: [] };
700
+ }
701
+ manifest.edits.push({ editId, filePath, editIndex });
702
+ fs3.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
703
+ } catch (err) {
704
+ console.error(`[BackupManifest] Failed to update manifest: ${err}`);
705
+ }
706
+ }
1276
707
  function formatDryRunResult(results, filePath, originalContent) {
1277
708
  const lines = [
1278
709
  `\u{1F50D} **DRY RUN** \u2014 ${filePath}`,
@@ -1356,9 +787,12 @@ function formatDiffResult(results, filePath) {
1356
787
  if (r.success) {
1357
788
  lines.push(`[${i + 1}] \u2705 Matched: ${r.matched}x, Replaced: ${r.replaced}x`);
1358
789
  if (r.backupPath) {
1359
- const relativePath = path4.relative(getProjectRoot(), r.backupPath);
790
+ const relativePath = path3.relative(getProjectRoot(), r.backupPath);
1360
791
  lines.push(` Backup: ${relativePath}`);
1361
792
  }
793
+ if (r.editId) {
794
+ lines.push(` editId: \`${r.editId}\``);
795
+ }
1362
796
  } else {
1363
797
  lines.push(`[${i + 1}] \u274C ${r.error}`);
1364
798
  if (r.details) {
@@ -1369,7 +803,8 @@ function formatDiffResult(results, filePath) {
1369
803
  lines.push(
1370
804
  "",
1371
805
  `\u{1F4A1} Use smart_file_picker to read the file and verify edit results.`,
1372
- `\u{1F4A1} Or execute_safe_test({task: "typecheck"}) to check if edits broke anything.`
806
+ `\u{1F4A1} Or execute_safe_test({task: "typecheck"}) to check if edits broke anything.`,
807
+ `\u{1F4A1} Rollback options: { action: "rollback", version: <N> } | { scope: "edit-id", editId: "<id>" } | { scope: "dir", ... }`
1373
808
  );
1374
809
  return lines.join("\n");
1375
810
  }
@@ -1384,31 +819,39 @@ function formatRelativeTime(timestamp) {
1384
819
  return `${days}d ago`;
1385
820
  }
1386
821
  async function handleRollbackEdit(params) {
1387
- const { filePath, version } = params;
822
+ const { filePath, version, scope = "file", editId } = params;
823
+ if (scope === "commit") {
824
+ return handleCommitRollback(filePath, version);
825
+ }
826
+ if (scope === "edit-id") {
827
+ return handleEditIdRollback(editId, version);
828
+ }
829
+ if (scope === "dir") {
830
+ return handleDirRollback(filePath, version);
831
+ }
832
+ if (!filePath) {
833
+ return "Error: 'filePath' is required for file-scoped rollback.";
834
+ }
1388
835
  const validation = validateFilePath(filePath);
1389
836
  if (!validation.valid) {
1390
837
  return `Error: ${validation.error.message}`;
1391
838
  }
1392
839
  const resolvedPath = validation.resolvedPath;
1393
840
  const root = getProjectRoot();
1394
- const relativePath = path4.relative(root, resolvedPath);
1395
- const backupRoot = path4.join(root, ".agent-backups");
1396
- if (!fs4.existsSync(backupRoot)) {
1397
- return `Error: No backup folder (.agent-backups) found in project root.`;
841
+ const relativePath = path3.relative(root, resolvedPath);
842
+ const backupRoot = path3.join(root, ".kuma", "backups");
843
+ if (!fs3.existsSync(backupRoot)) {
844
+ return `Error: No backup folder (.kuma/backups) found in project root.`;
1398
845
  }
1399
846
  try {
1400
- const dirs = fs4.readdirSync(backupRoot).filter((name) => {
1401
- const fullPath = path4.join(backupRoot, name);
1402
- return fs4.statSync(fullPath).isDirectory() && /^\d+$/.test(name);
1403
- });
847
+ const dirs = getBackupTimestamps(backupRoot);
1404
848
  if (dirs.length === 0) {
1405
- return `Error: No valid backup found in .agent-backups folder.`;
849
+ return `Error: No valid backup found in .kuma/backups folder.`;
1406
850
  }
1407
- dirs.sort((a, b) => Number(b) - Number(a));
1408
851
  const backupVersions = [];
1409
852
  for (const dir of dirs) {
1410
- const potentialBackupPath = path4.join(backupRoot, dir, relativePath);
1411
- if (fs4.existsSync(potentialBackupPath)) {
853
+ const potentialBackupPath = path3.join(backupRoot, dir, relativePath);
854
+ if (fs3.existsSync(potentialBackupPath)) {
1412
855
  backupVersions.push({ dir, backupPath: potentialBackupPath });
1413
856
  }
1414
857
  }
@@ -1416,16 +859,7 @@ async function handleRollbackEdit(params) {
1416
859
  return `Error: No backup history found for file "${filePath}".`;
1417
860
  }
1418
861
  if (version === "list") {
1419
- const lines = [`\u{1F4CB} Backup Versions for "${filePath}":`];
1420
- backupVersions.forEach((v, i) => {
1421
- const ts = Number(v.dir);
1422
- const date = new Date(ts).toISOString();
1423
- const relative = formatRelativeTime(ts);
1424
- lines.push(` [${i + 1}] ${date} (${relative})`);
1425
- });
1426
- lines.push("");
1427
- lines.push(`\u{1F4A1} Use rollback_last_edit({ filePath: "${filePath}", version: <N> }) to restore a specific version.`);
1428
- return lines.join("\n");
862
+ return formatBackupVersionList(backupVersions, filePath);
1429
863
  }
1430
864
  let selectedIndex = 0;
1431
865
  if (typeof version === "number") {
@@ -1435,24 +869,253 @@ async function handleRollbackEdit(params) {
1435
869
  selectedIndex = version - 1;
1436
870
  }
1437
871
  const selected = backupVersions[selectedIndex];
1438
- fs4.copyFileSync(selected.backupPath, resolvedPath);
872
+ fs3.copyFileSync(selected.backupPath, resolvedPath);
1439
873
  sessionMemory.recordToolCall("rollback_last_edit", {
1440
874
  filePath,
1441
875
  backupTimestamp: selected.dir,
1442
876
  version: version ?? 1,
877
+ scope: "file",
1443
878
  success: true
1444
879
  });
1445
- const relBackupPath = path4.relative(root, selected.backupPath);
880
+ const relBackupPath = path3.relative(root, selected.backupPath);
1446
881
  return `\u2705 Rollback Successful!
1447
882
  File "${filePath}" restored from backup: "${relBackupPath}".`;
1448
883
  } catch (err) {
1449
884
  return `Error performing rollback for "${filePath}": ${err instanceof Error ? err.message : String(err)}`;
1450
885
  }
1451
886
  }
887
+ async function handleCommitRollback(filePath, version) {
888
+ try {
889
+ const { execSync: execSync6 } = await import("child_process");
890
+ const root = getProjectRoot();
891
+ if (version === "list") {
892
+ const log = execSync6("git log --oneline -20", { cwd: root, encoding: "utf-8" });
893
+ const lines = log.trim().split("\n").map((l) => ` ${l}`).join("\n");
894
+ return [
895
+ `\u{1F4CB} **Recent Commits:**`,
896
+ "",
897
+ lines,
898
+ "",
899
+ `\u{1F4A1} Use { action: "rollback", scope: "commit", version: <N> } to restore a specific commit.`
900
+ ].join("\n");
901
+ }
902
+ const status = execSync6("git status --porcelain", { cwd: root, encoding: "utf-8" }).trim();
903
+ if (status) {
904
+ return [
905
+ `\u26A0\uFE0F **Uncommitted changes detected.**`,
906
+ `Running \`git checkout HEAD~N\` would destroy these changes.`,
907
+ "",
908
+ `Options:`,
909
+ ` 1. Stash changes first: run \`git stash\``,
910
+ ` 2. Commit changes: run \`git commit -m "..."\``,
911
+ ` 3. Use file-scoped rollback instead: { action: "rollback", scope: "file", ... }`,
912
+ "",
913
+ `\u{1F4A1} Run \`git stash\` first, then retry the commit rollback.`
914
+ ].join("\n");
915
+ }
916
+ let target = "";
917
+ if (typeof version === "number" && version >= 1) {
918
+ target = `HEAD~${version}`;
919
+ } else {
920
+ target = "HEAD~1";
921
+ }
922
+ if (filePath) {
923
+ execSync6(`git checkout ${target} -- "${filePath}"`, { cwd: root, encoding: "utf-8" });
924
+ return `\u2705 **Commit Rollback** \u2014 File "${filePath}" restored from ${target}.`;
925
+ } else {
926
+ execSync6(`git checkout ${target}`, { cwd: root, encoding: "utf-8" });
927
+ return `\u2705 **Commit Rollback** \u2014 Project restored to ${target}.`;
928
+ }
929
+ } catch (err) {
930
+ return `Error performing git rollback: ${err instanceof Error ? err.message : String(err)}`;
931
+ }
932
+ }
933
+ async function handleEditIdRollback(editId, version) {
934
+ if (!editId && version !== "list") {
935
+ return "Error: 'editId' is required for edit-id scoped rollback.";
936
+ }
937
+ const root = getProjectRoot();
938
+ const backupRoot = path3.join(root, ".kuma", "backups");
939
+ const dirs = getBackupTimestamps(backupRoot);
940
+ if (dirs.length === 0) {
941
+ return "Error: No backups found.";
942
+ }
943
+ const matchingEdits = [];
944
+ const allEdits = [];
945
+ for (const dir of dirs) {
946
+ const manifestPath = path3.join(backupRoot, dir, "backup_manifest.json");
947
+ if (!fs3.existsSync(manifestPath)) continue;
948
+ try {
949
+ const manifest = JSON.parse(fs3.readFileSync(manifestPath, "utf-8"));
950
+ for (const edit of manifest.edits) {
951
+ allEdits.push({ dir, editId: edit.editId, filePath: edit.filePath });
952
+ if (edit.editId === editId) {
953
+ matchingEdits.push({ dir, editId: edit.editId, filePath: edit.filePath });
954
+ }
955
+ }
956
+ } catch {
957
+ }
958
+ }
959
+ if (version === "list") {
960
+ if (allEdits.length === 0) {
961
+ return "\u{1F4CB} **Edit IDs** \u2014 No edit IDs found in backup manifests.\n\nNew edits will be tracked with editId going forward.";
962
+ }
963
+ const lines = ["\u{1F4CB} **Tracked Edit IDs:**", ""];
964
+ for (const e of allEdits.slice(0, 30)) {
965
+ const ts = new Date(Number(e.dir)).toISOString();
966
+ lines.push(` \u2022 \`${e.editId}\` \u2014 ${e.filePath} (${ts})`);
967
+ }
968
+ if (allEdits.length > 30) lines.push(` ... +${allEdits.length - 30} more`);
969
+ lines.push("", `\u{1F4A1} Use { action: "rollback", scope: "edit-id", editId: "<id>" } to restore a specific edit.`);
970
+ return lines.join("\n");
971
+ }
972
+ if (matchingEdits.length === 0) {
973
+ return `\u26A0\uFE0F **Edit ID not found** \u2014 "${editId}" not in backup manifests.
974
+
975
+ Edit IDs are tracked for new edits. To rollback by file, use scope: "file".`;
976
+ }
977
+ const results = [];
978
+ for (const match of matchingEdits) {
979
+ const backupFilePath = path3.join(backupRoot, match.dir, match.filePath);
980
+ if (fs3.existsSync(backupFilePath)) {
981
+ const resolvedPath = path3.join(root, match.filePath);
982
+ fs3.copyFileSync(backupFilePath, resolvedPath);
983
+ results.push(` \u2705 \`${match.filePath}\` restored`);
984
+ }
985
+ }
986
+ sessionMemory.recordToolCall("rollback_last_edit", {
987
+ editId,
988
+ filesRestored: matchingEdits.length,
989
+ success: true
990
+ });
991
+ return [
992
+ `\u2705 **Edit Rollback** \u2014 Restored ${matchingEdits.length} file(s) for edit "${editId}":`,
993
+ "",
994
+ ...results
995
+ ].join("\n");
996
+ }
997
+ async function handleDirRollback(dirPath, version) {
998
+ if (!dirPath) {
999
+ return "Error: 'filePath' (directory path) is required for dir-scoped rollback.";
1000
+ }
1001
+ const root = getProjectRoot();
1002
+ const backupRoot = path3.join(root, ".kuma", "backups");
1003
+ const dirs = getBackupTimestamps(backupRoot);
1004
+ if (dirs.length === 0) {
1005
+ return "Error: No backups found.";
1006
+ }
1007
+ const normalizedDir = dirPath.replace(/\\/g, "/").replace(/\/$/, "");
1008
+ const backupFiles = [];
1009
+ for (const dir of dirs) {
1010
+ const backupDirPath = path3.join(backupRoot, dir);
1011
+ const files = walkBackupDir(backupDirPath);
1012
+ for (const file of files) {
1013
+ const relative = path3.relative(backupDirPath, file);
1014
+ if (relative.startsWith(normalizedDir) || relative.startsWith(normalizedDir.replace(/^\//, ""))) {
1015
+ backupFiles.push({ dir, relativePath: relative, fullPath: file });
1016
+ }
1017
+ }
1018
+ }
1019
+ if (version === "list") {
1020
+ const timestampFiles = /* @__PURE__ */ new Map();
1021
+ for (const bf of backupFiles) {
1022
+ if (!timestampFiles.has(bf.dir)) timestampFiles.set(bf.dir, []);
1023
+ timestampFiles.get(bf.dir).push(bf.relativePath);
1024
+ }
1025
+ const lines = [`\u{1F4CB} **Backup Snapshots for directory "${dirPath}":**`, ""];
1026
+ for (const [ts, files] of timestampFiles) {
1027
+ const date = new Date(Number(ts)).toISOString();
1028
+ lines.push(` \u{1F4C1} ${date} \u2014 ${files.length} file(s)`);
1029
+ for (const f of files.slice(0, 5)) {
1030
+ lines.push(` \u2022 ${f}`);
1031
+ }
1032
+ if (files.length > 5) lines.push(` ... +${files.length - 5} more`);
1033
+ }
1034
+ lines.push("", `\u{1F4A1} Use { action: "rollback", scope: "dir", filePath: "${dirPath}", version: <N> } to restore.`);
1035
+ return lines.join("\n");
1036
+ }
1037
+ const byTimestamp = /* @__PURE__ */ new Map();
1038
+ for (const bf of backupFiles) {
1039
+ if (!byTimestamp.has(bf.dir)) byTimestamp.set(bf.dir, []);
1040
+ byTimestamp.get(bf.dir).push(bf);
1041
+ }
1042
+ const sortedTimestamps = [...byTimestamp.keys()].sort((a, b) => Number(b) - Number(a));
1043
+ let selectedTs;
1044
+ if (typeof version === "number" && version >= 1) {
1045
+ if (version > sortedTimestamps.length) {
1046
+ return `Error: Version ${version} is invalid. ${sortedTimestamps.length} snapshots available (1-${sortedTimestamps.length}).`;
1047
+ }
1048
+ selectedTs = sortedTimestamps[version - 1];
1049
+ } else {
1050
+ selectedTs = sortedTimestamps[0];
1051
+ }
1052
+ const filesToRestore = byTimestamp.get(selectedTs) || [];
1053
+ let restoredCount = 0;
1054
+ for (const bf of filesToRestore) {
1055
+ const targetPath = path3.join(root, bf.relativePath);
1056
+ try {
1057
+ fs3.copyFileSync(bf.fullPath, targetPath);
1058
+ restoredCount++;
1059
+ } catch {
1060
+ }
1061
+ }
1062
+ sessionMemory.recordToolCall("rollback_last_edit", {
1063
+ scope: "dir",
1064
+ directory: dirPath,
1065
+ filesRestored: restoredCount,
1066
+ backupTimestamp: selectedTs,
1067
+ success: true
1068
+ });
1069
+ return [
1070
+ `\u2705 **Directory Rollback** \u2014 Restored ${restoredCount} file(s) in "${dirPath}" from snapshot ${new Date(Number(selectedTs)).toISOString()}:`
1071
+ ].join("\n");
1072
+ }
1073
+ function walkBackupDir(dirPath) {
1074
+ const results = [];
1075
+ try {
1076
+ const entries = fs3.readdirSync(dirPath, { withFileTypes: true });
1077
+ for (const entry of entries) {
1078
+ const fullPath = path3.join(dirPath, entry.name);
1079
+ if (entry.name === "backup_manifest.json") continue;
1080
+ if (entry.isDirectory()) {
1081
+ results.push(...walkBackupDir(fullPath));
1082
+ } else {
1083
+ results.push(fullPath);
1084
+ }
1085
+ }
1086
+ } catch {
1087
+ }
1088
+ return results;
1089
+ }
1090
+ function getBackupTimestamps(backupRoot) {
1091
+ try {
1092
+ const dirs = fs3.readdirSync(backupRoot).filter((name) => {
1093
+ const fullPath = path3.join(backupRoot, name);
1094
+ return fs3.statSync(fullPath).isDirectory() && /^\d+$/.test(name);
1095
+ });
1096
+ return dirs.sort((a, b) => Number(b) - Number(a));
1097
+ } catch {
1098
+ return [];
1099
+ }
1100
+ }
1101
+ function formatBackupVersionList(backupVersions, filePath) {
1102
+ const lines = [`\u{1F4CB} Backup Versions for "${filePath}":`];
1103
+ backupVersions.forEach((v, i) => {
1104
+ const ts = Number(v.dir);
1105
+ const date = new Date(ts).toISOString();
1106
+ const relative = formatRelativeTime(ts);
1107
+ lines.push(` [${i + 1}] ${date} (${relative})`);
1108
+ });
1109
+ lines.push("");
1110
+ lines.push(`\u{1F4A1} Use { action: "rollback", version: <N>, filePath: "${filePath}" } to restore a specific version.`);
1111
+ lines.push(`\u{1F4A1} Or use { scope: "dir", filePath: "<directory>" } to rollback a whole directory.`);
1112
+ lines.push(`\u{1F4A1} Or use { scope: "edit-id", editId: "<id>" } to rollback by edit ID.`);
1113
+ return lines.join("\n");
1114
+ }
1452
1115
 
1453
1116
  // src/tools/batchFileWriter.ts
1454
- import fs5 from "fs";
1455
- import path5 from "path";
1117
+ import fs4 from "fs";
1118
+ import path4 from "path";
1456
1119
  async function handleBatchFileWriter(params) {
1457
1120
  const { files } = params;
1458
1121
  if (files.length > 15) {
@@ -1479,22 +1142,22 @@ async function handleBatchFileWriter(params) {
1479
1142
  results.push({
1480
1143
  filePath: file.filePath,
1481
1144
  success: false,
1482
- error: `File extension not allowed: "${path5.extname(file.filePath)}". Allowed extensions: .ts, .js, .tsx, .jsx, .json, .md, .css, .html, .yml, .yaml, .toml, .sh, .env`
1145
+ error: `File extension not allowed: "${path4.extname(file.filePath)}". Allowed extensions: .ts, .js, .tsx, .jsx, .json, .md, .css, .html, .yml, .yaml, .toml, .sh, .env`
1483
1146
  });
1484
1147
  continue;
1485
1148
  }
1486
- if (fs5.existsSync(resolvedPath)) {
1149
+ if (fs4.existsSync(resolvedPath)) {
1487
1150
  ensureBackupDir();
1488
1151
  const backupPath = getBackupPath(file.filePath);
1489
- const backupDir = path5.dirname(backupPath);
1490
- fs5.mkdirSync(backupDir, { recursive: true });
1491
- fs5.copyFileSync(resolvedPath, backupPath);
1152
+ const backupDir = path4.dirname(backupPath);
1153
+ fs4.mkdirSync(backupDir, { recursive: true });
1154
+ fs4.copyFileSync(resolvedPath, backupPath);
1492
1155
  }
1493
- const dir = path5.dirname(resolvedPath);
1494
- if (!fs5.existsSync(dir)) {
1495
- fs5.mkdirSync(dir, { recursive: true });
1156
+ const dir = path4.dirname(resolvedPath);
1157
+ if (!fs4.existsSync(dir)) {
1158
+ fs4.mkdirSync(dir, { recursive: true });
1496
1159
  }
1497
- fs5.writeFileSync(resolvedPath, file.content, "utf-8");
1160
+ fs4.writeFileSync(resolvedPath, file.content, "utf-8");
1498
1161
  sessionMemory.recordToolCall("batch_file_writer", {
1499
1162
  filePath: file.filePath,
1500
1163
  instructions: file.instructions,
@@ -1537,12 +1200,12 @@ function formatBatchResult(results, totalRequested) {
1537
1200
  }
1538
1201
 
1539
1202
  // src/tools/safeTerminalExec.ts
1540
- import fs7 from "fs";
1541
- import path7 from "path";
1542
-
1543
- // src/utils/conventionsDetector.ts
1544
1203
  import fs6 from "fs";
1545
1204
  import path6 from "path";
1205
+
1206
+ // src/utils/conventionsDetector.ts
1207
+ import fs5 from "fs";
1208
+ import path5 from "path";
1546
1209
  var cachedConventions = null;
1547
1210
  async function detectConventions(forceRescan = false) {
1548
1211
  if (cachedConventions && !forceRescan) {
@@ -1568,7 +1231,7 @@ async function detectConventions(forceRescan = false) {
1568
1231
  return conventions;
1569
1232
  }
1570
1233
  function detectFramework(root) {
1571
- const pkg = readJsonSafe(path6.join(root, "package.json"));
1234
+ const pkg = readJsonSafe(path5.join(root, "package.json"));
1572
1235
  if (pkg) {
1573
1236
  const pkgDeps = pkg.dependencies ?? {};
1574
1237
  const pkgDevDeps = pkg.devDependencies ?? {};
@@ -1613,11 +1276,11 @@ function detectFramework(root) {
1613
1276
  function scanSubProjectFrameworks(root) {
1614
1277
  const frameworks = [];
1615
1278
  try {
1616
- const entries = fs6.readdirSync(root, { withFileTypes: true });
1279
+ const entries = fs5.readdirSync(root, { withFileTypes: true });
1617
1280
  for (const entry of entries) {
1618
1281
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
1619
- const subDir = path6.join(root, entry.name);
1620
- const subPkg = readJsonSafe(path6.join(subDir, "package.json"));
1282
+ const subDir = path5.join(root, entry.name);
1283
+ const subPkg = readJsonSafe(path5.join(subDir, "package.json"));
1621
1284
  if (!subPkg) continue;
1622
1285
  const subDeps = { ...subPkg.dependencies ?? {}, ...subPkg.devDependencies ?? {} };
1623
1286
  if (subDeps.next) frameworks.push("Next.js");
@@ -1636,7 +1299,7 @@ function scanSubProjectFrameworks(root) {
1636
1299
  return frameworks;
1637
1300
  }
1638
1301
  function detectProjectType(root) {
1639
- const pkg = readJsonSafe(path6.join(root, "package.json"));
1302
+ const pkg = readJsonSafe(path5.join(root, "package.json"));
1640
1303
  if (pkg) {
1641
1304
  const pkgDeps = pkg.dependencies ?? {};
1642
1305
  const pkgDevDeps = pkg.devDependencies ?? {};
@@ -1660,8 +1323,8 @@ function detectProjectType(root) {
1660
1323
  }
1661
1324
  function detectWorkspaces(root) {
1662
1325
  const results = [];
1663
- const pkg = readJsonSafe(path6.join(root, "package.json"));
1664
- const pnpmWorkspace = readYamlLite(path6.join(root, "pnpm-workspace.yaml"));
1326
+ const pkg = readJsonSafe(path5.join(root, "package.json"));
1327
+ const pnpmWorkspace = readYamlLite(path5.join(root, "pnpm-workspace.yaml"));
1665
1328
  const patterns = /* @__PURE__ */ new Set();
1666
1329
  const pkgWorkspaces = pkg?.workspaces;
1667
1330
  if (Array.isArray(pkgWorkspaces)) {
@@ -1681,22 +1344,22 @@ function detectWorkspaces(root) {
1681
1344
  for (const pattern of patterns) {
1682
1345
  const match = pattern.match(/^([^*]+)\/\*$/);
1683
1346
  if (!match) continue;
1684
- const dir = path6.join(root, match[1]);
1685
- if (!fs6.existsSync(dir)) continue;
1347
+ const dir = path5.join(root, match[1]);
1348
+ if (!fs5.existsSync(dir)) continue;
1686
1349
  let entries = [];
1687
1350
  try {
1688
- entries = fs6.readdirSync(dir, { withFileTypes: true });
1351
+ entries = fs5.readdirSync(dir, { withFileTypes: true });
1689
1352
  } catch {
1690
1353
  continue;
1691
1354
  }
1692
1355
  for (const entry of entries) {
1693
1356
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
1694
- const pkgPath = path6.join(dir, entry.name, "package.json");
1357
+ const pkgPath = path5.join(dir, entry.name, "package.json");
1695
1358
  const subPkg = readJsonSafe(pkgPath);
1696
1359
  if (!subPkg) continue;
1697
- const workspacePath = path6.join(dir, entry.name);
1360
+ const workspacePath = path5.join(dir, entry.name);
1698
1361
  results.push({
1699
- path: path6.relative(root, workspacePath),
1362
+ path: path5.relative(root, workspacePath),
1700
1363
  name: subPkg.name ?? entry.name,
1701
1364
  framework: detectFramework(workspacePath),
1702
1365
  packageManager: detectPackageManagerForDir(workspacePath)
@@ -1712,8 +1375,8 @@ function detectWorkspaces(root) {
1712
1375
  }
1713
1376
  function readYamlLite(filePath) {
1714
1377
  try {
1715
- if (!fs6.existsSync(filePath)) return null;
1716
- const text = fs6.readFileSync(filePath, "utf-8");
1378
+ if (!fs5.existsSync(filePath)) return null;
1379
+ const text = fs5.readFileSync(filePath, "utf-8");
1717
1380
  const packages = [];
1718
1381
  let inPackages = false;
1719
1382
  for (const rawLine of text.split("\n")) {
@@ -1734,7 +1397,7 @@ function readYamlLite(filePath) {
1734
1397
  }
1735
1398
  }
1736
1399
  function detectTestRunner(root) {
1737
- const pkg = readJsonSafe(path6.join(root, "package.json"));
1400
+ const pkg = readJsonSafe(path5.join(root, "package.json"));
1738
1401
  if (!pkg) return "unknown";
1739
1402
  const pkgDeps2 = pkg.dependencies ?? {};
1740
1403
  const pkgDevDeps2 = pkg.devDependencies ?? {};
@@ -1755,7 +1418,7 @@ function detectTestRunner(root) {
1755
1418
  return "unknown";
1756
1419
  }
1757
1420
  function detectStyling(root) {
1758
- const pkg = readJsonSafe(path6.join(root, "package.json"));
1421
+ const pkg = readJsonSafe(path5.join(root, "package.json"));
1759
1422
  if (!pkg) return "unknown";
1760
1423
  const pkgDeps3 = pkg.dependencies ?? {};
1761
1424
  const pkgDevDeps3 = pkg.devDependencies ?? {};
@@ -1768,22 +1431,22 @@ function detectStyling(root) {
1768
1431
  if (deps.less) return "Less";
1769
1432
  if (deps["@vanilla-extract/css"]) return "Vanilla Extract";
1770
1433
  if (deps.cssmodules || hasCSSModules(root)) return "CSS Modules";
1771
- const srcDir = path6.join(root, "src");
1772
- if (fs6.existsSync(srcDir)) {
1434
+ const srcDir = path5.join(root, "src");
1435
+ if (fs5.existsSync(srcDir)) {
1773
1436
  const cssFiles = findFiles(srcDir, [".css", ".scss", ".less"]);
1774
1437
  if (cssFiles.length > 0) return "Plain CSS/SCSS";
1775
1438
  }
1776
1439
  return "unknown";
1777
1440
  }
1778
1441
  function detectImportAlias(root) {
1779
- const tsconfig = readJsonSafe(path6.join(root, "tsconfig.json"));
1442
+ const tsconfig = readJsonSafe(path5.join(root, "tsconfig.json"));
1780
1443
  const tsconfigPaths = tsconfig?.compilerOptions;
1781
1444
  if (tsconfigPaths?.paths) {
1782
1445
  const paths = tsconfigPaths.paths;
1783
1446
  const alias = Object.keys(paths)[0];
1784
1447
  if (alias) return alias.replace("/*", "");
1785
1448
  }
1786
- const jsconfig = readJsonSafe(path6.join(root, "jsconfig.json"));
1449
+ const jsconfig = readJsonSafe(path5.join(root, "jsconfig.json"));
1787
1450
  const jsconfigPaths = jsconfig?.compilerOptions;
1788
1451
  if (jsconfigPaths?.paths) {
1789
1452
  const paths = jsconfigPaths.paths;
@@ -1794,15 +1457,15 @@ function detectImportAlias(root) {
1794
1457
  }
1795
1458
  function detectLintRules(root) {
1796
1459
  const rules = [];
1797
- if (fs6.existsSync(path6.join(root, ".eslintrc"))) rules.push("ESLint (.eslintrc)");
1798
- if (fs6.existsSync(path6.join(root, ".eslintrc.js"))) rules.push("ESLint (.eslintrc.js)");
1799
- if (fs6.existsSync(path6.join(root, ".eslintrc.json"))) rules.push("ESLint (.eslintrc.json)");
1800
- if (fs6.existsSync(path6.join(root, "eslint.config.js"))) rules.push("ESLint (flat config)");
1801
- if (fs6.existsSync(path6.join(root, ".prettierrc"))) rules.push("Prettier");
1802
- if (fs6.existsSync(path6.join(root, ".prettierrc.json"))) rules.push("Prettier");
1803
- if (fs6.existsSync(path6.join(root, ".prettierrc.js"))) rules.push("Prettier");
1804
- if (fs6.existsSync(path6.join(root, ".stylelintrc"))) rules.push("StyleLint");
1805
- if (fs6.existsSync(path6.join(root, ".stylelintrc.json"))) rules.push("StyleLint");
1460
+ if (fs5.existsSync(path5.join(root, ".eslintrc"))) rules.push("ESLint (.eslintrc)");
1461
+ if (fs5.existsSync(path5.join(root, ".eslintrc.js"))) rules.push("ESLint (.eslintrc.js)");
1462
+ if (fs5.existsSync(path5.join(root, ".eslintrc.json"))) rules.push("ESLint (.eslintrc.json)");
1463
+ if (fs5.existsSync(path5.join(root, "eslint.config.js"))) rules.push("ESLint (flat config)");
1464
+ if (fs5.existsSync(path5.join(root, ".prettierrc"))) rules.push("Prettier");
1465
+ if (fs5.existsSync(path5.join(root, ".prettierrc.json"))) rules.push("Prettier");
1466
+ if (fs5.existsSync(path5.join(root, ".prettierrc.js"))) rules.push("Prettier");
1467
+ if (fs5.existsSync(path5.join(root, ".stylelintrc"))) rules.push("StyleLint");
1468
+ if (fs5.existsSync(path5.join(root, ".stylelintrc.json"))) rules.push("StyleLint");
1806
1469
  return rules;
1807
1470
  }
1808
1471
  function detectPackageManager() {
@@ -1811,26 +1474,26 @@ function detectPackageManager() {
1811
1474
  }
1812
1475
  function detectPackageManagerForDir(dir) {
1813
1476
  const root = getProjectRoot();
1814
- let currentDir = path6.resolve(dir);
1477
+ let currentDir = path5.resolve(dir);
1815
1478
  while (currentDir.startsWith(root)) {
1816
- if (fs6.existsSync(path6.join(currentDir, "pnpm-lock.yaml"))) return "pnpm";
1817
- if (fs6.existsSync(path6.join(currentDir, "yarn.lock"))) return "yarn";
1818
- if (fs6.existsSync(path6.join(currentDir, "package-lock.json"))) return "npm";
1819
- if (fs6.existsSync(path6.join(currentDir, "bun.lockb"))) return "bun";
1479
+ if (fs5.existsSync(path5.join(currentDir, "pnpm-lock.yaml"))) return "pnpm";
1480
+ if (fs5.existsSync(path5.join(currentDir, "yarn.lock"))) return "yarn";
1481
+ if (fs5.existsSync(path5.join(currentDir, "package-lock.json"))) return "npm";
1482
+ if (fs5.existsSync(path5.join(currentDir, "bun.lockb"))) return "bun";
1820
1483
  if (currentDir === root) break;
1821
- currentDir = path6.dirname(currentDir);
1484
+ currentDir = path5.dirname(currentDir);
1822
1485
  }
1823
1486
  return "npm";
1824
1487
  }
1825
1488
  function detectModuleSystem(root) {
1826
- const pkg = readJsonSafe(path6.join(root, "package.json"));
1489
+ const pkg = readJsonSafe(path5.join(root, "package.json"));
1827
1490
  if (pkg?.type === "module") return "esm";
1828
- const srcDir = path6.join(root, "src");
1829
- if (fs6.existsSync(srcDir)) {
1491
+ const srcDir = path5.join(root, "src");
1492
+ if (fs5.existsSync(srcDir)) {
1830
1493
  const tsFiles = findFiles(srcDir, [".ts", ".tsx", ".js", ".jsx", ".mjs"]);
1831
1494
  for (const file of tsFiles.slice(0, 20)) {
1832
1495
  try {
1833
- const content = fs6.readFileSync(file, "utf-8");
1496
+ const content = fs5.readFileSync(file, "utf-8");
1834
1497
  if (content.includes("import ") || content.includes("export ")) return "esm";
1835
1498
  } catch {
1836
1499
  continue;
@@ -1840,9 +1503,9 @@ function detectModuleSystem(root) {
1840
1503
  return "cjs";
1841
1504
  }
1842
1505
  function detectLanguage(root) {
1843
- if (fs6.existsSync(path6.join(root, "tsconfig.json"))) return "typescript";
1844
- const srcDir = path6.join(root, "src");
1845
- if (fs6.existsSync(srcDir)) {
1506
+ if (fs5.existsSync(path5.join(root, "tsconfig.json"))) return "typescript";
1507
+ const srcDir = path5.join(root, "src");
1508
+ if (fs5.existsSync(srcDir)) {
1846
1509
  const tsFiles = findFiles(srcDir, [".ts", ".tsx"]);
1847
1510
  if (tsFiles.length > 0) return "typescript";
1848
1511
  }
@@ -1850,7 +1513,7 @@ function detectLanguage(root) {
1850
1513
  }
1851
1514
  function detectFeatures(root) {
1852
1515
  const features = [];
1853
- const pkg = readJsonSafe(path6.join(root, "package.json"));
1516
+ const pkg = readJsonSafe(path5.join(root, "package.json"));
1854
1517
  if (!pkg) return features;
1855
1518
  const pkgDeps4 = pkg.dependencies ?? {};
1856
1519
  const pkgDevDeps4 = pkg.devDependencies ?? {};
@@ -1874,25 +1537,25 @@ function detectFeatures(root) {
1874
1537
  }
1875
1538
  function readJsonSafe(filePath) {
1876
1539
  try {
1877
- if (fs6.existsSync(filePath)) {
1878
- return JSON.parse(fs6.readFileSync(filePath, "utf-8"));
1540
+ if (fs5.existsSync(filePath)) {
1541
+ return JSON.parse(fs5.readFileSync(filePath, "utf-8"));
1879
1542
  }
1880
1543
  } catch {
1881
1544
  }
1882
1545
  return null;
1883
1546
  }
1884
1547
  function hasCSSModules(root) {
1885
- const srcDir = path6.join(root, "src");
1886
- if (!fs6.existsSync(srcDir)) return false;
1548
+ const srcDir = path5.join(root, "src");
1549
+ if (!fs5.existsSync(srcDir)) return false;
1887
1550
  const files = findFiles(srcDir, [".module.css", ".module.scss", ".module.less"]);
1888
1551
  return files.length > 0;
1889
1552
  }
1890
1553
  function findFiles(dir, extensions) {
1891
1554
  const results = [];
1892
1555
  try {
1893
- const entries = fs6.readdirSync(dir, { withFileTypes: true });
1556
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
1894
1557
  for (const entry of entries) {
1895
- const fullPath = path6.join(dir, entry.name);
1558
+ const fullPath = path5.join(dir, entry.name);
1896
1559
  if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
1897
1560
  results.push(...findFiles(fullPath, extensions));
1898
1561
  } else if (entry.isFile()) {
@@ -2083,7 +1746,7 @@ async function handleSafeTerminalExec(params) {
2083
1746
  const workspaces = conventions?.workspaces;
2084
1747
  const matched = workspaces?.find((w) => w.name === workspace || w.path === workspace);
2085
1748
  if (matched) {
2086
- workingDir = path7.resolve(projectRoot, matched.path);
1749
+ workingDir = path6.resolve(projectRoot, matched.path);
2087
1750
  resolvedFrom = `workspace "${matched.name}" \u2192 ${matched.path}`;
2088
1751
  } else {
2089
1752
  return `\u26A0\uFE0F Workspace "${workspace}" not found. Run project_conventions first to detect workspaces, or use 'cwd' parameter with a direct path.
@@ -2091,13 +1754,13 @@ async function handleSafeTerminalExec(params) {
2091
1754
  Available workspaces: ${workspaces?.map((w) => `"${w.name}" (${w.path})`).join(", ") || "none detected"}`;
2092
1755
  }
2093
1756
  } else if (inputCwd) {
2094
- const resolved = path7.resolve(projectRoot, inputCwd);
2095
- const normalizedResolved = path7.normalize(resolved).toLowerCase();
2096
- const normalizedRoot = path7.normalize(projectRoot).toLowerCase();
1757
+ const resolved = path6.resolve(projectRoot, inputCwd);
1758
+ const normalizedResolved = path6.normalize(resolved).toLowerCase();
1759
+ const normalizedRoot = path6.normalize(projectRoot).toLowerCase();
2097
1760
  if (!normalizedResolved.startsWith(normalizedRoot)) {
2098
1761
  return `\u{1F6AB} BLOCKED: Path "${inputCwd}" resolves outside project root "${projectRoot}".`;
2099
1762
  }
2100
- if (!fs7.existsSync(resolved)) {
1763
+ if (!fs6.existsSync(resolved)) {
2101
1764
  return `\u26A0\uFE0F Directory "${inputCwd}" does not exist at "${resolved}".`;
2102
1765
  }
2103
1766
  workingDir = resolved;
@@ -2183,8 +1846,8 @@ function formatTimeoutResult(command, timeout) {
2183
1846
  }
2184
1847
 
2185
1848
  // src/agents/codeReviewer.ts
2186
- import fs8 from "fs";
2187
- import path8 from "path";
1849
+ import fs7 from "fs";
1850
+ import path7 from "path";
2188
1851
  import { execSync } from "child_process";
2189
1852
  var CODE_EXTENSIONS = [
2190
1853
  ".ts",
@@ -2228,7 +1891,7 @@ function getGitChangedFiles() {
2228
1891
  if (filePath.startsWith('"') && filePath.endsWith('"')) {
2229
1892
  filePath = filePath.substring(1, filePath.length - 1);
2230
1893
  }
2231
- const ext = path8.extname(filePath).toLowerCase();
1894
+ const ext = path7.extname(filePath).toLowerCase();
2232
1895
  if (CODE_EXTENSIONS.includes(ext)) files.push(filePath);
2233
1896
  }
2234
1897
  return files.slice(0, 10);
@@ -2265,7 +1928,7 @@ Pass an explicit 'files' array to review specific files.`;
2265
1928
  continue;
2266
1929
  }
2267
1930
  const resolvedPath = validation.resolvedPath;
2268
- if (!fs8.existsSync(resolvedPath)) {
1931
+ if (!fs7.existsSync(resolvedPath)) {
2269
1932
  allIssues.push({
2270
1933
  file: filePath,
2271
1934
  line: 0,
@@ -2277,7 +1940,7 @@ Pass an explicit 'files' array to review specific files.`;
2277
1940
  continue;
2278
1941
  }
2279
1942
  try {
2280
- const content = fs8.readFileSync(resolvedPath, "utf-8");
1943
+ const content = fs7.readFileSync(resolvedPath, "utf-8");
2281
1944
  filesReviewed++;
2282
1945
  checkGeneral(filePath, content, allIssues);
2283
1946
  switch (focus) {
@@ -2375,12 +2038,12 @@ function isTestFile(filePath) {
2375
2038
  return /\.(test|spec)\.[a-z]+$/i.test(filePath) || /__tests__/.test(filePath);
2376
2039
  }
2377
2040
  function isTsLike(filePath) {
2378
- const ext = path8.extname(filePath).toLowerCase();
2041
+ const ext = path7.extname(filePath).toLowerCase();
2379
2042
  return ext === ".ts" || ext === ".tsx";
2380
2043
  }
2381
2044
  function checkGeneral(filePath, content, issues) {
2382
2045
  const lines = content.split("\n");
2383
- const ext = path8.extname(filePath).toLowerCase();
2046
+ const ext = path7.extname(filePath).toLowerCase();
2384
2047
  if (content.length > 0 && !content.endsWith("\n")) {
2385
2048
  issues.push({
2386
2049
  file: filePath,
@@ -2582,7 +2245,7 @@ function stripJsonComments(text) {
2582
2245
  return cleaned.join("\n");
2583
2246
  }
2584
2247
  function checkConfigFile(filePath, content, issues) {
2585
- const ext = path8.extname(filePath).toLowerCase();
2248
+ const ext = path7.extname(filePath).toLowerCase();
2586
2249
  const lines = content.split("\n");
2587
2250
  if (ext === ".json" || ext === ".jsonc") {
2588
2251
  checkJson(filePath, content, lines, issues);
@@ -2593,7 +2256,7 @@ function checkConfigFile(filePath, content, issues) {
2593
2256
  }
2594
2257
  }
2595
2258
  function checkJson(filePath, content, lines, issues) {
2596
- const ext = path8.extname(filePath).toLowerCase();
2259
+ const ext = path7.extname(filePath).toLowerCase();
2597
2260
  const isJsonc = ext === ".jsonc";
2598
2261
  const contentToParse = isJsonc ? stripJsonComments(content) : content;
2599
2262
  try {
@@ -3450,13 +3113,13 @@ async function handleGitDiff(params) {
3450
3113
  }
3451
3114
 
3452
3115
  // src/tools/projectStructure.ts
3453
- import fs9 from "fs";
3454
- import path9 from "path";
3116
+ import fs8 from "fs";
3117
+ import path8 from "path";
3455
3118
  var DEFAULT_IGNORE = [
3456
3119
  "node_modules",
3457
3120
  ".git",
3458
3121
  ".kuma",
3459
- ".agent-backups",
3122
+ ".kuma/backups",
3460
3123
  "dist",
3461
3124
  ".next",
3462
3125
  "build",
@@ -3488,7 +3151,7 @@ async function handleProjectStructure(params) {
3488
3151
  sessionMemory.recordToolCall("project_structure", { depth: clampedDepth, folderOnly });
3489
3152
  try {
3490
3153
  const tree = buildTree(root, root, clampedDepth, 0, folderOnly, includePattern, excludePattern);
3491
- const projectName = path9.basename(root);
3154
+ const projectName = path8.basename(root);
3492
3155
  const lines = [
3493
3156
  "[Project Structure] - " + projectName,
3494
3157
  "Depth: " + clampedDepth + " | " + (folderOnly ? "Folders only" : "Files and folders"),
@@ -3511,7 +3174,7 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
3511
3174
  const lines = [];
3512
3175
  let entries = [];
3513
3176
  try {
3514
- entries = fs9.readdirSync(currentDir, { withFileTypes: true });
3177
+ entries = fs8.readdirSync(currentDir, { withFileTypes: true });
3515
3178
  } catch {
3516
3179
  return lines;
3517
3180
  }
@@ -3520,12 +3183,12 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
3520
3183
  if (!a.isDirectory() && b.isDirectory()) return 1;
3521
3184
  return a.name.localeCompare(b.name);
3522
3185
  });
3523
- const relativeDir = path9.relative(root, currentDir) || ".";
3186
+ const relativeDir = path8.relative(root, currentDir) || ".";
3524
3187
  const prefix = getPrefix(currentDepth);
3525
3188
  for (const entry of entries) {
3526
3189
  if (shouldIgnore(entry.name, relativeDir, root)) continue;
3527
- const fullPath = path9.join(currentDir, entry.name);
3528
- const relativePath = path9.relative(root, fullPath);
3190
+ const fullPath = path8.join(currentDir, entry.name);
3191
+ const relativePath = path8.relative(root, fullPath);
3529
3192
  if (includePattern && !entry.name.includes(includePattern) && !relativePath.includes(includePattern)) {
3530
3193
  if (!entry.isDirectory()) continue;
3531
3194
  }
@@ -3536,11 +3199,11 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
3536
3199
  lines.push(prefix + "[D] " + entry.name + "/");
3537
3200
  let subEntries = [];
3538
3201
  try {
3539
- subEntries = fs9.readdirSync(fullPath, { withFileTypes: true });
3202
+ subEntries = fs8.readdirSync(fullPath, { withFileTypes: true });
3540
3203
  } catch {
3541
3204
  }
3542
3205
  const hasVisibleContent = subEntries.some(
3543
- (e) => !shouldIgnore(e.name, path9.relative(root, fullPath), root)
3206
+ (e) => !shouldIgnore(e.name, path8.relative(root, fullPath), root)
3544
3207
  );
3545
3208
  if (hasVisibleContent) {
3546
3209
  const subLines = buildTree(root, fullPath, maxDepth, currentDepth + 1, folderOnly, includePattern, excludePattern);
@@ -3571,7 +3234,7 @@ function getPrefix(depth) {
3571
3234
  }
3572
3235
  function getFileSize(fullPath) {
3573
3236
  try {
3574
- const stat = fs9.statSync(fullPath);
3237
+ const stat = fs8.statSync(fullPath);
3575
3238
  if (stat.size < 1024) return stat.size + "B";
3576
3239
  if (stat.size < 1024 * 1024) return (stat.size / 1024).toFixed(0) + "KB";
3577
3240
  const sizeMB = (stat.size / (1024 * 1024)).toFixed(1);
@@ -3582,8 +3245,8 @@ function getFileSize(fullPath) {
3582
3245
  }
3583
3246
 
3584
3247
  // src/tools/staticAnalysis.ts
3585
- import fs10 from "fs";
3586
- import path10 from "path";
3248
+ import fs9 from "fs";
3249
+ import path9 from "path";
3587
3250
  async function handleStaticAnalysis(params) {
3588
3251
  const { tool = "all", files, autoFix = false, timeout = 60 } = params;
3589
3252
  const root = getProjectRoot();
@@ -3662,7 +3325,10 @@ async function handleStaticAnalysis(params) {
3662
3325
  function detectAvailableTools(root) {
3663
3326
  const tools = [];
3664
3327
  const pkg = readPackageJson(root);
3665
- const allDeps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
3328
+ const allDeps = {
3329
+ ...pkg?.dependencies ?? {},
3330
+ ...pkg?.devDependencies ?? {}
3331
+ };
3666
3332
  const depNames = new Set(Object.keys(allDeps));
3667
3333
  const eslintConfigs = [
3668
3334
  ".eslintrc",
@@ -3673,11 +3339,11 @@ function detectAvailableTools(root) {
3673
3339
  "eslint.config.js",
3674
3340
  "eslint.config.mjs"
3675
3341
  ];
3676
- const hasEslintConfig = eslintConfigs.some((cfg) => fs10.existsSync(path10.join(root, cfg)));
3342
+ const hasEslintConfig = eslintConfigs.some((cfg) => fs9.existsSync(path9.join(root, cfg)));
3677
3343
  if (hasEslintConfig && depNames.has("eslint")) {
3678
3344
  tools.push("eslint");
3679
3345
  }
3680
- if (fs10.existsSync(path10.join(root, "tsconfig.json")) && depNames.has("typescript")) {
3346
+ if (fs9.existsSync(path9.join(root, "tsconfig.json")) && depNames.has("typescript")) {
3681
3347
  tools.push("tsc");
3682
3348
  }
3683
3349
  const prettierConfigs = [
@@ -3688,20 +3354,20 @@ function detectAvailableTools(root) {
3688
3354
  ".prettierrc.toml",
3689
3355
  "prettier.config.js"
3690
3356
  ];
3691
- const hasPrettierConfig = prettierConfigs.some((cfg) => fs10.existsSync(path10.join(root, cfg)));
3357
+ const hasPrettierConfig = prettierConfigs.some((cfg) => fs9.existsSync(path9.join(root, cfg)));
3692
3358
  if (hasPrettierConfig && depNames.has("prettier")) {
3693
3359
  tools.push("prettier");
3694
3360
  }
3695
- if (fs10.existsSync(path10.join(root, "ruff.toml")) || fs10.existsSync(path10.join(root, ".ruff.toml"))) {
3361
+ if (fs9.existsSync(path9.join(root, "ruff.toml")) || fs9.existsSync(path9.join(root, ".ruff.toml"))) {
3696
3362
  tools.push("ruff");
3697
3363
  }
3698
3364
  return tools;
3699
3365
  }
3700
3366
  function readPackageJson(root) {
3701
3367
  try {
3702
- const pkgPath = path10.join(root, "package.json");
3703
- if (fs10.existsSync(pkgPath)) {
3704
- return JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
3368
+ const pkgPath = path9.join(root, "package.json");
3369
+ if (fs9.existsSync(pkgPath)) {
3370
+ return JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
3705
3371
  }
3706
3372
  } catch {
3707
3373
  }
@@ -3757,18 +3423,18 @@ function buildToolCommand(tool, files, autoFix) {
3757
3423
  }
3758
3424
  function detectPackageManagerPrefix() {
3759
3425
  const root = getProjectRoot();
3760
- if (fs10.existsSync(path10.join(root, "pnpm-lock.yaml"))) return "pnpm ";
3761
- if (fs10.existsSync(path10.join(root, "yarn.lock"))) return "yarn ";
3426
+ if (fs9.existsSync(path9.join(root, "pnpm-lock.yaml"))) return "pnpm ";
3427
+ if (fs9.existsSync(path9.join(root, "yarn.lock"))) return "yarn ";
3762
3428
  return "npx --no-install ";
3763
3429
  }
3764
3430
  function findBinary(root, binName) {
3765
3431
  const candidates = [
3766
- path10.join(root, "node_modules", ".bin", binName),
3767
- path10.join(root, "..", "node_modules", ".bin", binName)
3432
+ path9.join(root, "node_modules", ".bin", binName),
3433
+ path9.join(root, "..", "node_modules", ".bin", binName)
3768
3434
  ];
3769
3435
  for (const candidate of candidates) {
3770
3436
  try {
3771
- if (fs10.existsSync(candidate)) {
3437
+ if (fs9.existsSync(candidate)) {
3772
3438
  return candidate;
3773
3439
  }
3774
3440
  } catch {
@@ -3792,7 +3458,7 @@ function parseToolOutput(tool, stdout, stderr, projectRoot) {
3792
3458
  }
3793
3459
  function resolveToolPath(filePath, projectRoot) {
3794
3460
  const trimmed = filePath.trim();
3795
- return path10.isAbsolute(trimmed) ? path10.relative(projectRoot, trimmed) : trimmed.replace(/^\.\//, "");
3461
+ return path9.isAbsolute(trimmed) ? path9.relative(projectRoot, trimmed) : trimmed.replace(/^\.\//, "");
3796
3462
  }
3797
3463
  function parseEslintOutput(output, projectRoot) {
3798
3464
  const issues = [];
@@ -3974,80 +3640,140 @@ function formatToolNotAvailable(requested, available) {
3974
3640
  ].join("\n");
3975
3641
  }
3976
3642
 
3977
- // src/tools/kumaReflect.ts
3643
+ // src/utils/kumaShared.ts
3978
3644
  import { execSync as execSync3 } from "child_process";
3979
- async function handleReflect(params) {
3645
+ function getSessionStats(inputGoal) {
3980
3646
  const summary = sessionMemory.getSummary();
3981
- const goal = params.goal || summary.currentGoal || "";
3647
+ const goal = inputGoal || summary.currentGoal || "";
3982
3648
  const modifiedFiles = sessionMemory.getModifiedFiles();
3983
3649
  const toolCalls = sessionMemory.getToolCallHistory(50);
3984
3650
  const failedFiles = sessionMemory.getFailedFiles();
3985
3651
  const loop = sessionMemory.detectLoop();
3986
- const unresolved = [];
3987
- for (const f of failedFiles) {
3988
- for (const ff of f.failures) {
3989
- if (!ff.resolved) {
3990
- unresolved.push({ task: f.task, error: ff.error.substring(0, 200) });
3991
- }
3992
- }
3993
- }
3994
- const hasRunTests = toolCalls.some(
3995
- (c) => c.toolName === "execute_safe_test"
3996
- );
3997
- let gitStat = "";
3652
+ return {
3653
+ goal,
3654
+ modifiedFiles,
3655
+ toolCalls,
3656
+ toolCallCount: toolCalls.length,
3657
+ failedFiles,
3658
+ hasLoop: loop.isLooping,
3659
+ loopMessage: loop.message,
3660
+ hasRunTests: toolCalls.some((c) => c.toolName === "execute_safe_test")
3661
+ };
3662
+ }
3663
+ function getGitDiffStat(timeout = 3e3) {
3998
3664
  try {
3999
3665
  const root = getProjectRoot();
4000
- gitStat = execSync3("git diff --stat", {
3666
+ return execSync3("git diff --stat", {
4001
3667
  cwd: root,
4002
3668
  encoding: "utf-8",
4003
- timeout: 5e3
3669
+ timeout
4004
3670
  }).trim();
4005
3671
  } catch {
3672
+ return "";
4006
3673
  }
4007
- const drifts = [];
4008
- if (modifiedFiles.length > 0 && !hasRunTests) {
4009
- drifts.push(`${modifiedFiles.length} file(s) edited but no test run`);
4010
- }
4011
- if (loop.isLooping) {
4012
- drifts.push(loop.message);
4013
- }
4014
- if (unresolved.length > 0) {
4015
- drifts.push(`${unresolved.length} unresolved failure(s)`);
3674
+ }
3675
+ function getUnresolvedCount(failedFiles) {
3676
+ let count = 0;
3677
+ for (const f of failedFiles) {
3678
+ for (const ff of f.failures) {
3679
+ if (!ff.resolved) count++;
3680
+ }
4016
3681
  }
4017
- if (gitStat) {
4018
- drifts.push(`Git diff: ${gitStat}`);
3682
+ return count;
3683
+ }
3684
+ function getUnresolvedDetails(failedFiles) {
3685
+ const result = [];
3686
+ for (const f of failedFiles) {
3687
+ for (const ff of f.failures) {
3688
+ if (!ff.resolved) {
3689
+ result.push({ task: f.task, error: ff.error.substring(0, 200) });
3690
+ }
3691
+ }
4019
3692
  }
4020
- const ladderViolations = [];
3693
+ return result;
3694
+ }
3695
+ function checkLadderViolations(toolCalls, modifiedFiles, hasRunTests) {
3696
+ const violations = [];
4021
3697
  const editCalls = toolCalls.filter(
4022
3698
  (c) => c.toolName === "precise_diff_editor" || c.toolName === "batch_file_writer"
4023
3699
  ).length;
4024
3700
  if (editCalls > 5) {
4025
- ladderViolations.push(
4026
- `${editCalls} file ops in a row \u2014 consider if all are needed`
4027
- );
3701
+ violations.push(`${editCalls} file ops in a row \u2014 consider if all are needed`);
4028
3702
  }
4029
3703
  if (modifiedFiles.length > 5 && !hasRunTests) {
4030
- ladderViolations.push(
4031
- `${modifiedFiles.length} files modified without verification`
4032
- );
3704
+ violations.push(`${modifiedFiles.length} files modified without verification`);
3705
+ }
3706
+ return violations;
3707
+ }
3708
+ function buildDriftMessages(modifiedFiles, hasRunTests, unresolvedCount, gitStat, loopMessage) {
3709
+ const drifts = [];
3710
+ if (modifiedFiles > 0 && !hasRunTests) {
3711
+ drifts.push(`${modifiedFiles} file(s) edited but no test run`);
3712
+ }
3713
+ if (loopMessage) {
3714
+ drifts.push(loopMessage);
3715
+ }
3716
+ if (unresolvedCount > 0) {
3717
+ drifts.push(`${unresolvedCount} unresolved failure(s)`);
3718
+ }
3719
+ if (gitStat) {
3720
+ drifts.push(`Git diff: ${gitStat}`);
3721
+ }
3722
+ return drifts;
3723
+ }
3724
+ function getPrioritySuggestion(goal, warnings, hasLoop, unresolvedCount, modifiedFiles, hasRunTests, editCalls) {
3725
+ if (warnings.some((w) => w.severity === "high" && w.pattern === "script-patching")) {
3726
+ return "Remove patch scripts and use precise_diff_editor for all file modifications";
3727
+ }
3728
+ if (hasLoop) {
3729
+ return "Switch approach \u2014 current tool is not making progress";
3730
+ }
3731
+ if (warnings.some((w) => w.pattern === "no-test-after-edit") || modifiedFiles > 0 && !hasRunTests) {
3732
+ return "Run tests to verify your changes before continuing";
3733
+ }
3734
+ if (unresolvedCount > 0) {
3735
+ return "Fix unresolved failures before continuing";
3736
+ }
3737
+ if (warnings.some((w) => w.pattern === "bash-grep")) {
3738
+ return "Use smart_grep for code search instead of bash grep";
3739
+ }
3740
+ if (warnings.some((w) => w.pattern === "excessive-edits") || editCalls > 10) {
3741
+ return "Consider if refactoring can be simplified \u2014 fewer files = fewer bugs";
4033
3742
  }
4034
- const onTrack = !loop.isLooping && unresolved.length === 0 && (modifiedFiles.length === 0 || hasRunTests);
4035
- let suggestion;
4036
3743
  if (!goal) {
4037
- suggestion = "No goal set \u2014 use goal parameter or setGoal";
4038
- } else if (loop.isLooping) {
4039
- suggestion = "Switch approach \u2014 current tool is not making progress";
4040
- } else if (unresolved.length > 0) {
4041
- suggestion = "Fix unresolved failures before continuing";
4042
- } else if (modifiedFiles.length > 0 && !hasRunTests) {
4043
- suggestion = "Run tests to verify changes";
4044
- } else if (modifiedFiles.length === 0 && !toolCalls.some((c) => ["smart_file_picker", "smart_grep"].includes(c.toolName))) {
4045
- suggestion = "Start by exploring what exists before writing code";
4046
- } else if (editCalls > 10) {
4047
- suggestion = "Consider if refactoring can be simplified \u2014 fewer files = fewer bugs";
4048
- } else {
4049
- suggestion = "On track";
3744
+ return "No goal set \u2014 use goal parameter or setGoal to track intent";
4050
3745
  }
3746
+ if (modifiedFiles === 0) {
3747
+ return "Start by exploring what exists before writing code";
3748
+ }
3749
+ return "On track \u2014 continue with current approach";
3750
+ }
3751
+
3752
+ // src/tools/kumaReflect.ts
3753
+ async function handleReflect(params) {
3754
+ const stats = getSessionStats(params.goal);
3755
+ const unresolved = getUnresolvedDetails(stats.failedFiles);
3756
+ const gitStat = getGitDiffStat(5e3);
3757
+ const ladderViolations = checkLadderViolations(stats.toolCalls, stats.modifiedFiles, stats.hasRunTests);
3758
+ const drifts = buildDriftMessages(
3759
+ stats.modifiedFiles.length,
3760
+ stats.hasRunTests,
3761
+ unresolved.length,
3762
+ gitStat,
3763
+ stats.loopMessage
3764
+ );
3765
+ const onTrack = !stats.hasLoop && unresolved.length === 0 && (stats.modifiedFiles.length === 0 || stats.hasRunTests);
3766
+ const suggestion = getPrioritySuggestion(
3767
+ stats.goal,
3768
+ [],
3769
+ stats.hasLoop,
3770
+ unresolved.length,
3771
+ stats.modifiedFiles.length,
3772
+ stats.hasRunTests,
3773
+ stats.toolCalls.filter(
3774
+ (c) => c.toolName === "precise_diff_editor" || c.toolName === "batch_file_writer"
3775
+ ).length
3776
+ );
4051
3777
  return JSON.stringify(
4052
3778
  {
4053
3779
  onTrack,
@@ -4055,12 +3781,12 @@ async function handleReflect(params) {
4055
3781
  ...ladderViolations.length > 0 ? { ladderViolations } : {},
4056
3782
  suggestion,
4057
3783
  stats: {
4058
- goal,
4059
- modifiedFiles: modifiedFiles.length,
4060
- toolCalls: toolCalls.length,
3784
+ goal: stats.goal,
3785
+ modifiedFiles: stats.modifiedFiles.length,
3786
+ toolCalls: stats.toolCallCount,
4061
3787
  unresolvedFailures: unresolved.length,
4062
- hasLoop: loop.isLooping,
4063
- hasRunTests
3788
+ hasLoop: stats.hasLoop,
3789
+ hasRunTests: stats.hasRunTests
4064
3790
  }
4065
3791
  },
4066
3792
  null,
@@ -4068,12 +3794,9 @@ async function handleReflect(params) {
4068
3794
  );
4069
3795
  }
4070
3796
 
4071
- // src/tools/kumaGuard.ts
4072
- import { execSync as execSync6 } from "child_process";
4073
-
4074
3797
  // src/guards/antiPatternDetector.ts
4075
- import fs11 from "fs";
4076
- import path11 from "path";
3798
+ import fs10 from "fs";
3799
+ import path10 from "path";
4077
3800
  import { execSync as execSync4 } from "child_process";
4078
3801
  var SCRIPT_PATCH_PATTERNS = [
4079
3802
  "writeFileSync",
@@ -4100,20 +3823,20 @@ function findPatchScripts(projectRoot) {
4100
3823
  const warnings = [];
4101
3824
  const recentFiles = scanRecentFiles(projectRoot);
4102
3825
  for (const file of recentFiles) {
4103
- const ext = path11.extname(file).toLowerCase();
3826
+ const ext = path10.extname(file).toLowerCase();
4104
3827
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
4105
- const relativePath = path11.relative(projectRoot, file);
3828
+ const relativePath = path10.relative(projectRoot, file);
4106
3829
  if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".")) {
4107
3830
  continue;
4108
3831
  }
4109
3832
  try {
4110
- const content = fs11.readFileSync(file, "utf-8").toLowerCase();
3833
+ const content = fs10.readFileSync(file, "utf-8").toLowerCase();
4111
3834
  const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
4112
3835
  if (matchedPattern) {
4113
3836
  warnings.push({
4114
3837
  severity: "high",
4115
3838
  pattern: "script-patching",
4116
- message: `Created script file that modifies other files: ${path11.basename(file)}`,
3839
+ message: `Created script file that modifies other files: ${path10.basename(file)}`,
4117
3840
  suggestion: "Use **precise_diff_editor** instead \u2014 it has fuzzy matching, auto-backup, and rollback support",
4118
3841
  evidence: `File: ${relativePath} contains '${matchedPattern}'`,
4119
3842
  filePath: relativePath
@@ -4147,15 +3870,15 @@ function detectBashGrepUsage() {
4147
3870
  function scanRecentFiles(projectRoot) {
4148
3871
  const recent = [];
4149
3872
  try {
4150
- const entries = fs11.readdirSync(projectRoot, { withFileTypes: true });
3873
+ const entries = fs10.readdirSync(projectRoot, { withFileTypes: true });
4151
3874
  const now = Date.now();
4152
3875
  const recentThreshold = 30 * 60 * 1e3;
4153
3876
  for (const entry of entries) {
4154
3877
  if (!entry.isFile()) continue;
4155
3878
  try {
4156
- const stat = fs11.statSync(path11.join(projectRoot, entry.name));
3879
+ const stat = fs10.statSync(path10.join(projectRoot, entry.name));
4157
3880
  if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
4158
- recent.push(path11.join(projectRoot, entry.name));
3881
+ recent.push(path10.join(projectRoot, entry.name));
4159
3882
  }
4160
3883
  } catch {
4161
3884
  continue;
@@ -4179,14 +3902,14 @@ function detectGitPatchScripts(projectRoot) {
4179
3902
  const prefix = line.substring(0, 2);
4180
3903
  if (prefix !== "??" && prefix !== "A ") continue;
4181
3904
  const file = line.substring(3).trim();
4182
- const ext = path11.extname(file).toLowerCase();
3905
+ const ext = path10.extname(file).toLowerCase();
4183
3906
  if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
4184
3907
  const isRootLevel = !file.includes("/");
4185
3908
  const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
4186
3909
  if (!isRootLevel && !isScriptsDir) continue;
4187
- const fullPath = path11.join(projectRoot, file);
4188
- if (fs11.existsSync(fullPath)) {
4189
- const content = fs11.readFileSync(fullPath, "utf-8").toLowerCase();
3910
+ const fullPath = path10.join(projectRoot, file);
3911
+ if (fs10.existsSync(fullPath)) {
3912
+ const content = fs10.readFileSync(fullPath, "utf-8").toLowerCase();
4190
3913
  const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
4191
3914
  if (matchedPattern) {
4192
3915
  warnings.push({
@@ -4209,7 +3932,7 @@ function detectGitPatchScripts(projectRoot) {
4209
3932
  if (deletedStdout) {
4210
3933
  const deletedFiles = deletedStdout.split("\n").filter(Boolean);
4211
3934
  for (const file of deletedFiles) {
4212
- const ext = path11.extname(file).toLowerCase();
3935
+ const ext = path10.extname(file).toLowerCase();
4213
3936
  if (SCRIPT_EXTENSIONS.includes(ext)) {
4214
3937
  warnings.push({
4215
3938
  severity: "high",
@@ -4269,21 +3992,21 @@ function detectAllAntiPatterns() {
4269
3992
  }
4270
3993
 
4271
3994
  // src/engine/contextSnapshot.ts
4272
- import fs12 from "fs";
4273
- import path12 from "path";
3995
+ import fs11 from "fs";
3996
+ import path11 from "path";
4274
3997
  import { execSync as execSync5 } from "child_process";
4275
3998
  var SNAPSHOT_DIR = "context-snapshots";
4276
3999
  function snapshotDir() {
4277
- return path12.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
4000
+ return path11.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
4278
4001
  }
4279
4002
  function ensureSnapshotDir() {
4280
4003
  const dir = snapshotDir();
4281
- if (!fs12.existsSync(dir)) {
4282
- fs12.mkdirSync(dir, { recursive: true });
4004
+ if (!fs11.existsSync(dir)) {
4005
+ fs11.mkdirSync(dir, { recursive: true });
4283
4006
  }
4284
4007
  }
4285
4008
  function snapshotFilePath(timestamp) {
4286
- return path12.join(snapshotDir(), `${timestamp}.json`);
4009
+ return path11.join(snapshotDir(), `${timestamp}.json`);
4287
4010
  }
4288
4011
  function saveSnapshot(goal) {
4289
4012
  try {
@@ -4299,12 +4022,12 @@ function saveSnapshot(goal) {
4299
4022
  modifiedFiles: summary.modifiedFiles || [],
4300
4023
  unresolvedFailures: summary.unresolvedFailures || [],
4301
4024
  toolCallCount: summary.toolCallCount || 0,
4302
- gitDiffStat: getGitDiffStat(),
4025
+ gitDiffStat: getGitDiffStat2(),
4303
4026
  completedSteps: summary.completedSteps || [],
4304
4027
  hasConventions: !!summary.hasConventions
4305
4028
  };
4306
4029
  try {
4307
- fs12.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
4030
+ fs11.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
4308
4031
  } catch {
4309
4032
  }
4310
4033
  sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
@@ -4312,12 +4035,12 @@ function saveSnapshot(goal) {
4312
4035
  }
4313
4036
  function listSnapshots() {
4314
4037
  const dir = snapshotDir();
4315
- if (!fs12.existsSync(dir)) return [];
4038
+ if (!fs11.existsSync(dir)) return [];
4316
4039
  try {
4317
- const files = fs12.readdirSync(dir).filter((f) => f.endsWith(".json")).sort((a, b) => b.localeCompare(a));
4040
+ const files = fs11.readdirSync(dir).filter((f) => f.endsWith(".json")).sort((a, b) => b.localeCompare(a));
4318
4041
  return files.map((f) => {
4319
4042
  try {
4320
- const content = fs12.readFileSync(path12.join(dir, f), "utf-8");
4043
+ const content = fs11.readFileSync(path11.join(dir, f), "utf-8");
4321
4044
  return JSON.parse(content);
4322
4045
  } catch {
4323
4046
  return null;
@@ -4357,7 +4080,7 @@ function formatSnapshot(snapshot) {
4357
4080
  lines.push('\u{1F4A1} Use kuma_guard({ check: "context" }) to create a new snapshot.');
4358
4081
  return lines.join("\n");
4359
4082
  }
4360
- function getGitDiffStat() {
4083
+ function getGitDiffStat2() {
4361
4084
  try {
4362
4085
  const root = getProjectRoot();
4363
4086
  const stdout = execSync5("git diff --stat", {
@@ -4374,9 +4097,8 @@ function getGitDiffStat() {
4374
4097
  // src/tools/kumaGuard.ts
4375
4098
  async function handleKumaGuard(params) {
4376
4099
  const { check = "all", goal: inputGoal } = params;
4377
- const summary = sessionMemory.getSummary();
4378
- const goal = inputGoal || summary.currentGoal || "";
4379
- sessionMemory.recordToolCall("kuma_guard", { check, goal });
4100
+ sessionMemory.recordToolCall("kuma_guard", { check, goal: inputGoal });
4101
+ const stats = getSessionStats(inputGoal);
4380
4102
  const warnings = [];
4381
4103
  if (check === "all" || check === "anti-pattern") {
4382
4104
  warnings.push(...detectAllAntiPatterns());
@@ -4391,44 +4113,26 @@ async function handleKumaGuard(params) {
4391
4113
  });
4392
4114
  }
4393
4115
  const drifts = [];
4394
- const toolCalls = sessionMemory.getToolCallHistory(50);
4395
- const hasRunTests = toolCalls.some((c) => c.toolName === "execute_safe_test");
4396
4116
  if (check === "all" || check === "drift") {
4397
- const modifiedFiles = sessionMemory.getModifiedFiles();
4398
- const failedFiles = sessionMemory.getFailedFiles();
4399
- let unresolvedCount = 0;
4400
- for (const f of failedFiles) {
4401
- for (const ff of f.failures) {
4402
- if (!ff.resolved) unresolvedCount++;
4403
- }
4404
- }
4405
- if (modifiedFiles.length > 0 && !hasRunTests) {
4406
- drifts.push(`${modifiedFiles.length} file(s) edited but no test run`);
4117
+ const unresolvedCount = getUnresolvedCount(stats.failedFiles);
4118
+ const gitStat = getGitDiffStat();
4119
+ const editCalls = stats.toolCalls.filter(
4120
+ (c) => c.toolName === "precise_diff_editor" || c.toolName === "batch_file_writer"
4121
+ ).length;
4122
+ drifts.push(...buildDriftMessages(
4123
+ stats.modifiedFiles.length,
4124
+ stats.hasRunTests,
4125
+ unresolvedCount,
4126
+ gitStat
4127
+ ));
4128
+ if (stats.modifiedFiles.length > 0 && !stats.hasRunTests) {
4407
4129
  warnings.push({
4408
4130
  severity: "medium",
4409
4131
  pattern: "no-test-after-edit",
4410
- message: `${modifiedFiles.length} file(s) modified without running tests`,
4132
+ message: `${stats.modifiedFiles.length} file(s) modified without running tests`,
4411
4133
  suggestion: 'Run execute_safe_test({ task: "typecheck" }) to verify changes'
4412
4134
  });
4413
4135
  }
4414
- if (unresolvedCount > 0) {
4415
- drifts.push(`${unresolvedCount} unresolved failure(s)`);
4416
- }
4417
- try {
4418
- const root = getProjectRoot();
4419
- const gitStat = execSync6("git diff --stat", {
4420
- cwd: root,
4421
- encoding: "utf-8",
4422
- timeout: 3e3
4423
- }).trim();
4424
- if (gitStat) {
4425
- drifts.push(`Git diff: ${gitStat}`);
4426
- }
4427
- } catch {
4428
- }
4429
- const editCalls = toolCalls.filter(
4430
- (c) => c.toolName === "precise_diff_editor" || c.toolName === "batch_file_writer"
4431
- ).length;
4432
4136
  if (editCalls > 5) {
4433
4137
  warnings.push({
4434
4138
  severity: "low",
@@ -4439,7 +4143,7 @@ async function handleKumaGuard(params) {
4439
4143
  }
4440
4144
  }
4441
4145
  if (check === "context") {
4442
- const snapshot = saveSnapshot(goal);
4146
+ const snapshot = saveSnapshot(stats.goal);
4443
4147
  if (!snapshot) {
4444
4148
  return "\u26A0\uFE0F Could not create context snapshot. The .kuma directory might not be accessible.";
4445
4149
  }
@@ -4459,7 +4163,7 @@ async function handleKumaGuard(params) {
4459
4163
  suggestion = "Use smart_grep for code search instead of bash grep";
4460
4164
  } else if (warnings.some((w) => w.pattern === "excessive-edits")) {
4461
4165
  suggestion = "Pause and review: are all these edits necessary?";
4462
- } else if (!goal) {
4166
+ } else if (!stats.goal) {
4463
4167
  suggestion = "No goal set \u2014 use goal parameter or setGoal to track intent";
4464
4168
  } else {
4465
4169
  suggestion = "On track \u2014 continue with current approach";
@@ -4471,12 +4175,12 @@ async function handleKumaGuard(params) {
4471
4175
  drifts,
4472
4176
  suggestion,
4473
4177
  stats: {
4474
- goal,
4475
- modifiedFiles: summary.modifiedFiles.length,
4476
- toolCalls: summary.toolCallCount ?? 0,
4477
- unresolvedFailures: summary.unresolvedFailures ? summary.unresolvedFailures.length : 0,
4178
+ goal: stats.goal,
4179
+ modifiedFiles: stats.modifiedFiles.length,
4180
+ toolCalls: stats.toolCallCount,
4181
+ unresolvedFailures: getUnresolvedCount(stats.failedFiles),
4478
4182
  hasLoop: loop.isLooping,
4479
- hasRunTests
4183
+ hasRunTests: stats.hasRunTests
4480
4184
  }
4481
4185
  };
4482
4186
  return JSON.stringify(report, null, 2);
@@ -4524,25 +4228,25 @@ async function handleKumaContext(params) {
4524
4228
  }
4525
4229
 
4526
4230
  // src/tools/kumaInit.ts
4527
- import fs13 from "fs";
4528
- import path13 from "path";
4231
+ import fs12 from "fs";
4232
+ import path12 from "path";
4529
4233
  async function handleKumaInit(params) {
4530
4234
  const root = params.projectRoot ?? getProjectRoot();
4531
- const kumaDir = path13.join(root, ".kuma");
4532
- const memoriesDir = path13.join(kumaDir, "memories");
4533
- const initMdPath = path13.join(kumaDir, "init.md");
4235
+ const kumaDir = path12.join(root, ".kuma");
4236
+ const memoriesDir = path12.join(kumaDir, "memories");
4237
+ const initMdPath = path12.join(kumaDir, "init.md");
4534
4238
  sessionMemory.recordToolCall("kuma_init", { projectRoot: root });
4535
4239
  let rules = "";
4536
- if (fs13.existsSync(initMdPath)) {
4537
- rules = fs13.readFileSync(initMdPath, "utf-8");
4240
+ if (fs12.existsSync(initMdPath)) {
4241
+ rules = fs12.readFileSync(initMdPath, "utf-8");
4538
4242
  }
4539
4243
  const memories = [];
4540
- if (fs13.existsSync(memoriesDir)) {
4244
+ if (fs12.existsSync(memoriesDir)) {
4541
4245
  try {
4542
- const files = fs13.readdirSync(memoriesDir).filter((f) => f.endsWith(".md"));
4246
+ const files = fs12.readdirSync(memoriesDir).filter((f) => f.endsWith(".md"));
4543
4247
  for (const file of files) {
4544
4248
  try {
4545
- const content = fs13.readFileSync(path13.join(memoriesDir, file), "utf-8");
4249
+ const content = fs12.readFileSync(path12.join(memoriesDir, file), "utf-8");
4546
4250
  memories.push({ topic: file.replace(/\.md$/, ""), content });
4547
4251
  } catch {
4548
4252
  }
@@ -4587,8 +4291,8 @@ ${memoryList}`);
4587
4291
 
4588
4292
  // src/engine/lspClient.ts
4589
4293
  import { spawn as spawn2 } from "child_process";
4590
- import path14 from "path";
4591
- import fs14 from "fs";
4294
+ import path13 from "path";
4295
+ import fs13 from "fs";
4592
4296
  var LSPClient = class {
4593
4297
  process = null;
4594
4298
  requestId = 0;
@@ -4626,8 +4330,8 @@ var LSPClient = class {
4626
4330
  console.error(`[LSP] typescript-language-server not found. LSP features will fallback to regex.`);
4627
4331
  return;
4628
4332
  }
4629
- const tsconfigPath = path14.join(root, "tsconfig.json");
4630
- if (!fs14.existsSync(tsconfigPath)) {
4333
+ const tsconfigPath = path13.join(root, "tsconfig.json");
4334
+ if (!fs13.existsSync(tsconfigPath)) {
4631
4335
  console.error(`[LSP] Warning: tsconfig.json not found at "${root}". Running in implicit project mode.`);
4632
4336
  }
4633
4337
  this.process = spawn2(lspBinary, ["--stdio", "--log-level=4"], {
@@ -4689,19 +4393,19 @@ var LSPClient = class {
4689
4393
  /** Resolve the typescript-language-server binary from local or global install */
4690
4394
  resolveLspBinary(projectRoot) {
4691
4395
  const candidates = [
4692
- path14.join(projectRoot, "node_modules", ".bin", "typescript-language-server"),
4693
- path14.join(projectRoot, "..", "node_modules", ".bin", "typescript-language-server")
4396
+ path13.join(projectRoot, "node_modules", ".bin", "typescript-language-server"),
4397
+ path13.join(projectRoot, "..", "node_modules", ".bin", "typescript-language-server")
4694
4398
  ];
4695
4399
  for (const candidate of candidates) {
4696
- if (fs14.existsSync(candidate)) {
4400
+ if (fs13.existsSync(candidate)) {
4697
4401
  return candidate;
4698
4402
  }
4699
4403
  }
4700
4404
  try {
4701
4405
  const envPath = process.env.PATH ?? "";
4702
- for (const dir of envPath.split(path14.delimiter)) {
4703
- const binPath = path14.join(dir, "typescript-language-server");
4704
- if (fs14.existsSync(binPath)) {
4406
+ for (const dir of envPath.split(path13.delimiter)) {
4407
+ const binPath = path13.join(dir, "typescript-language-server");
4408
+ if (fs13.existsSync(binPath)) {
4705
4409
  return binPath;
4706
4410
  }
4707
4411
  }
@@ -4723,7 +4427,7 @@ var LSPClient = class {
4723
4427
  this.openDocuments.add(filePath);
4724
4428
  let content;
4725
4429
  try {
4726
- content = fs14.readFileSync(filePath, "utf-8");
4430
+ content = fs13.readFileSync(filePath, "utf-8");
4727
4431
  } catch {
4728
4432
  content = "";
4729
4433
  }
@@ -4940,8 +4644,8 @@ var LSPClient = class {
4940
4644
  var lspClient = new LSPClient();
4941
4645
 
4942
4646
  // src/tools/lspTools.ts
4943
- import fs15 from "fs";
4944
- import path15 from "path";
4647
+ import fs14 from "fs";
4648
+ import path14 from "path";
4945
4649
  async function handleFindReferences(params) {
4946
4650
  const { filePath, line, character } = params;
4947
4651
  const validation = validateFilePath(filePath);
@@ -4949,7 +4653,7 @@ async function handleFindReferences(params) {
4949
4653
  return `Error: ${validation.error.message}`;
4950
4654
  }
4951
4655
  const resolvedPath = validation.resolvedPath;
4952
- if (!fs15.existsSync(resolvedPath)) {
4656
+ if (!fs14.existsSync(resolvedPath)) {
4953
4657
  return `Error: File not found: "${filePath}".`;
4954
4658
  }
4955
4659
  if (!lspClient.isAvailable()) {
@@ -4974,7 +4678,7 @@ No references found for symbol at this position.`;
4974
4678
  const enrichedRefs = references.map((ref) => {
4975
4679
  let lineContent = "";
4976
4680
  try {
4977
- const content = fs15.readFileSync(ref.filePath, "utf-8");
4681
+ const content = fs14.readFileSync(ref.filePath, "utf-8");
4978
4682
  const lines2 = content.split("\n");
4979
4683
  lineContent = lines2[ref.line]?.trim() ?? "";
4980
4684
  } catch {
@@ -4990,11 +4694,11 @@ No references found for symbol at this position.`;
4990
4694
  const projectRoot = getProjectRoot();
4991
4695
  const lines = [
4992
4696
  `**Find References** \u2014 ${enrichedRefs.length} references found`,
4993
- `\u{1F4CD} File: ${path15.relative(projectRoot, resolvedPath)}:${line + 1}:${character + 1}`,
4697
+ `\u{1F4CD} File: ${path14.relative(projectRoot, resolvedPath)}:${line + 1}:${character + 1}`,
4994
4698
  ""
4995
4699
  ];
4996
4700
  for (const [file, refs] of grouped) {
4997
- const relPath = path15.relative(projectRoot, file);
4701
+ const relPath = path14.relative(projectRoot, file);
4998
4702
  lines.push(`**\u{1F4C4} ${relPath}:**`);
4999
4703
  for (const ref of refs) {
5000
4704
  const loc = `L${ref.line + 1}:${ref.character + 1}`;
@@ -5015,7 +4719,7 @@ async function handleGoToDefinition(params) {
5015
4719
  return `Error: ${validation.error.message}`;
5016
4720
  }
5017
4721
  const resolvedPath = validation.resolvedPath;
5018
- if (!fs15.existsSync(resolvedPath)) {
4722
+ if (!fs14.existsSync(resolvedPath)) {
5019
4723
  return `Error: File not found: "${filePath}".`;
5020
4724
  }
5021
4725
  if (!lspClient.isAvailable()) {
@@ -5038,10 +4742,10 @@ Try selecting a smaller/cleaner position, or install typescript-language-server
5038
4742
  Cannot find definition for symbol at this position.`;
5039
4743
  }
5040
4744
  const projectRoot = getProjectRoot();
5041
- const relPath = path15.relative(projectRoot, definition.filePath);
4745
+ const relPath = path14.relative(projectRoot, definition.filePath);
5042
4746
  let lineContent = "";
5043
4747
  try {
5044
- const content = fs15.readFileSync(definition.filePath, "utf-8");
4748
+ const content = fs14.readFileSync(definition.filePath, "utf-8");
5045
4749
  const lines2 = content.split("\n");
5046
4750
  lineContent = lines2[definition.line]?.trim() ?? "";
5047
4751
  } catch {
@@ -5073,7 +4777,7 @@ async function handleRenameSymbol(params) {
5073
4777
  return `Error: ${validation.error.message}`;
5074
4778
  }
5075
4779
  const resolvedPath = validation.resolvedPath;
5076
- if (!fs15.existsSync(resolvedPath)) {
4780
+ if (!fs14.existsSync(resolvedPath)) {
5077
4781
  return `Error: File not found: "${filePath}".`;
5078
4782
  }
5079
4783
  if (!lspClient.isAvailable()) {
@@ -5104,7 +4808,7 @@ Make sure:
5104
4808
  const fileChanges = [];
5105
4809
  for (const change of result.changes) {
5106
4810
  try {
5107
- const content = fs15.readFileSync(change.filePath, "utf-8");
4811
+ const content = fs14.readFileSync(change.filePath, "utf-8");
5108
4812
  const lines2 = content.split("\n");
5109
4813
  const sortedEdits = [...change.edits].sort((a, b) => {
5110
4814
  if (b.line !== a.line) return b.line - a.line;
@@ -5118,10 +4822,10 @@ Make sure:
5118
4822
  lines2[edit.line] = before + edit.newText + after;
5119
4823
  }
5120
4824
  }
5121
- fs15.writeFileSync(change.filePath, lines2.join("\n"), "utf-8");
4825
+ fs14.writeFileSync(change.filePath, lines2.join("\n"), "utf-8");
5122
4826
  totalEdits += change.edits.length;
5123
4827
  fileChanges.push({
5124
- filePath: path15.relative(projectRoot, change.filePath),
4828
+ filePath: path14.relative(projectRoot, change.filePath),
5125
4829
  editCount: change.edits.length
5126
4830
  });
5127
4831
  } catch (err) {
@@ -5148,14 +4852,14 @@ async function handleGetTypeInfo(params) {
5148
4852
  return `Error: ${validation.error.message}`;
5149
4853
  }
5150
4854
  const resolvedPath = validation.resolvedPath;
5151
- if (!fs15.existsSync(resolvedPath)) {
4855
+ if (!fs14.existsSync(resolvedPath)) {
5152
4856
  return `Error: File not found: "${filePath}".`;
5153
4857
  }
5154
4858
  if (!lspClient.isAvailable()) {
5155
4859
  sessionMemory.recordToolCall("lsp_query", { action: "type", filePath, line, character, fallback: "line-context" });
5156
4860
  console.error(`[LSP] typescript-language-server not found. Using file-read fallback for type info.`);
5157
4861
  try {
5158
- const fileContent = fs15.readFileSync(resolvedPath, "utf-8");
4862
+ const fileContent = fs14.readFileSync(resolvedPath, "utf-8");
5159
4863
  const fileLines = fileContent.split("\n");
5160
4864
  const targetLine = fileLines[line];
5161
4865
  if (!targetLine) {
@@ -5163,7 +4867,7 @@ async function handleGetTypeInfo(params) {
5163
4867
  }
5164
4868
  const symbolName = extractSymbolAtPosition(resolvedPath, line, character);
5165
4869
  const projectRoot = getProjectRoot();
5166
- const relPath = path15.relative(projectRoot, resolvedPath);
4870
+ const relPath = path14.relative(projectRoot, resolvedPath);
5167
4871
  const contextStart = Math.max(0, line - 3);
5168
4872
  const contextEnd = Math.min(fileLines.length, line + 4);
5169
4873
  const contextLines = [];
@@ -5198,7 +4902,7 @@ async function handleGetTypeInfo(params) {
5198
4902
  No type info for this position.`;
5199
4903
  }
5200
4904
  const projectRoot = getProjectRoot();
5201
- const relPath = path15.relative(projectRoot, resolvedPath);
4905
+ const relPath = path14.relative(projectRoot, resolvedPath);
5202
4906
  const lines = [
5203
4907
  `\u{1F4CB} **Type Info** \u2014 \`${relPath}:${line + 1}:${character + 1}\``,
5204
4908
  "",
@@ -5239,7 +4943,7 @@ async function handleLspQuery(params) {
5239
4943
  }
5240
4944
  function extractSymbolAtPosition(filePath, line, character) {
5241
4945
  try {
5242
- const content = fs15.readFileSync(filePath, "utf-8");
4946
+ const content = fs14.readFileSync(filePath, "utf-8");
5243
4947
  const lines = content.split("\n");
5244
4948
  const targetLine = lines[line];
5245
4949
  if (!targetLine) return null;
@@ -5270,7 +4974,7 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
5270
4974
  const regex = new RegExp(`\\b${escapedSymbol}\\b`, "g");
5271
4975
  for (const file of tsFiles.slice(0, 100)) {
5272
4976
  try {
5273
- const content = fs15.readFileSync(file, "utf-8");
4977
+ const content = fs14.readFileSync(file, "utf-8");
5274
4978
  const lines2 = content.split("\n");
5275
4979
  for (let i = 0; i < lines2.length; i++) {
5276
4980
  if (regex.test(lines2[i])) {
@@ -5299,7 +5003,7 @@ No references found. Symbol may not be used in other files.`;
5299
5003
  ""
5300
5004
  ];
5301
5005
  if (grouped.size > 1) {
5302
- const fileList = [...grouped.keys()].map((f) => path15.relative(projectRoot, f)).join(", ");
5006
+ const fileList = [...grouped.keys()].map((f) => path14.relative(projectRoot, f)).join(", ");
5303
5007
  lines.push(`**Ambiguity note:** Found matches across ${grouped.size} different files (${fileList}).`);
5304
5008
  lines.push(` Verify each match is the right scope \u2014 results may include same-named symbols.`);
5305
5009
  lines.push("");
@@ -5316,7 +5020,7 @@ No references found. Symbol may not be used in other files.`;
5316
5020
  }
5317
5021
  }
5318
5022
  for (const [file, refs] of grouped) {
5319
- const relPath = path15.relative(projectRoot, file);
5023
+ const relPath = path14.relative(projectRoot, file);
5320
5024
  lines.push(`**\u{1F4C4} ${relPath}:**`);
5321
5025
  for (const ref of refs) {
5322
5026
  lines.push(` \u2514 L${ref.line} \u2014 ${ref.content}`);
@@ -5350,14 +5054,14 @@ async function fallbackGrepDefinition(symbolName) {
5350
5054
  ];
5351
5055
  for (const file of tsFiles) {
5352
5056
  try {
5353
- const content = fs15.readFileSync(file, "utf-8");
5057
+ const content = fs14.readFileSync(file, "utf-8");
5354
5058
  const lines = content.split("\n");
5355
5059
  for (let i = 0; i < lines.length; i++) {
5356
5060
  const trimmed = lines[i].trim();
5357
5061
  for (const pattern of declPatterns) {
5358
5062
  if (pattern.test(trimmed)) {
5359
5063
  const projectRoot = getProjectRoot();
5360
- const relPath = path15.relative(projectRoot, file);
5064
+ const relPath = path14.relative(projectRoot, file);
5361
5065
  return [
5362
5066
  `\u{1F4CD} **Go to Definition**`,
5363
5067
  `\u{1F4C4} File: \`${relPath}\``,
@@ -5379,6 +5083,543 @@ Cannot find definition.`;
5379
5083
  }
5380
5084
  }
5381
5085
 
5086
+ // src/tools/kumaPolicy.ts
5087
+ import fs15 from "fs";
5088
+ import path15 from "path";
5089
+ var DEFAULT_POLICY = {
5090
+ never_touch: [
5091
+ ".env",
5092
+ ".env.local",
5093
+ ".env.production",
5094
+ ".env.development",
5095
+ "package-lock.json",
5096
+ "yarn.lock",
5097
+ "pnpm-lock.yaml",
5098
+ "node_modules/**"
5099
+ ],
5100
+ block_commands: [
5101
+ "rm -rf",
5102
+ "rm -fr",
5103
+ "git push --force",
5104
+ "git push -f",
5105
+ "npm publish",
5106
+ "yarn publish",
5107
+ "pnpm publish",
5108
+ "curl | bash",
5109
+ "curl | sh"
5110
+ ]
5111
+ };
5112
+ function loadPolicy() {
5113
+ const root = getProjectRoot();
5114
+ const ymlPath = path15.join(root, ".kuma", "policy.yml");
5115
+ const yamlPath = path15.join(root, ".kuma", "policy.yaml");
5116
+ let policyContent = null;
5117
+ let policyPath = null;
5118
+ if (fs15.existsSync(ymlPath)) {
5119
+ policyContent = fs15.readFileSync(ymlPath, "utf-8");
5120
+ policyPath = ymlPath;
5121
+ } else if (fs15.existsSync(yamlPath)) {
5122
+ policyContent = fs15.readFileSync(yamlPath, "utf-8");
5123
+ policyPath = yamlPath;
5124
+ }
5125
+ if (!policyContent) {
5126
+ return { ...DEFAULT_POLICY };
5127
+ }
5128
+ try {
5129
+ return parseSimpleYaml(policyContent);
5130
+ } catch (err) {
5131
+ console.error(`[Policy] Failed to parse ${policyPath}: ${err}. Using defaults.`);
5132
+ return { ...DEFAULT_POLICY };
5133
+ }
5134
+ }
5135
+ function parseSimpleYaml(content) {
5136
+ const policy = {};
5137
+ const lines = content.split("\n");
5138
+ let currentKey = null;
5139
+ let currentArray = [];
5140
+ for (const rawLine of lines) {
5141
+ const line = rawLine.trim();
5142
+ if (!line || line.startsWith("#")) continue;
5143
+ const keyMatch = line.match(/^(\w[\w_-]*):\s*(.*)/);
5144
+ if (keyMatch) {
5145
+ if (currentKey && currentArray.length > 0) {
5146
+ setPolicyValue(policy, currentKey, currentArray);
5147
+ currentArray = [];
5148
+ }
5149
+ currentKey = keyMatch[1];
5150
+ const value = keyMatch[2].trim();
5151
+ if (value === "" || value === "|") {
5152
+ currentArray = [];
5153
+ } else {
5154
+ if (value.startsWith("[") && value.endsWith("]")) {
5155
+ const items = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, ""));
5156
+ setPolicyValue(policy, currentKey, items);
5157
+ } else {
5158
+ setPolicyValue(policy, currentKey, value.replace(/^['"]|['"]$/g, ""));
5159
+ }
5160
+ currentKey = null;
5161
+ }
5162
+ } else if (currentKey && line.startsWith("- ")) {
5163
+ currentArray.push(line.slice(2).trim().replace(/^['"]|['"]$/g, ""));
5164
+ }
5165
+ }
5166
+ if (currentKey && currentArray.length > 0) {
5167
+ setPolicyValue(policy, currentKey, currentArray);
5168
+ }
5169
+ return policy;
5170
+ }
5171
+ function setPolicyValue(policy, key, value) {
5172
+ switch (key) {
5173
+ case "never_touch":
5174
+ policy.never_touch = value;
5175
+ break;
5176
+ case "require_review":
5177
+ policy.require_review = value;
5178
+ break;
5179
+ case "require_tests":
5180
+ policy.require_tests = value;
5181
+ break;
5182
+ case "max_file_size":
5183
+ policy.max_file_size = Number(value);
5184
+ break;
5185
+ case "block_commands":
5186
+ policy.block_commands = value;
5187
+ break;
5188
+ }
5189
+ }
5190
+ function matchesGlob(pattern, filePath) {
5191
+ const normalizedPattern = pattern.replace(/\\/g, "/");
5192
+ const normalizedPath = filePath.replace(/\\/g, "/");
5193
+ let regexStr = "^";
5194
+ for (let i = 0; i < normalizedPattern.length; i++) {
5195
+ const ch = normalizedPattern[i];
5196
+ if (ch === "*" && normalizedPattern[i + 1] === "*" && normalizedPattern[i + 2] === "/") {
5197
+ regexStr += "(.+/)?";
5198
+ i += 2;
5199
+ } else if (ch === "*") {
5200
+ regexStr += "[^/]*";
5201
+ } else if (ch === "?") {
5202
+ regexStr += "[^/]";
5203
+ } else {
5204
+ regexStr += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
5205
+ }
5206
+ }
5207
+ regexStr += "$";
5208
+ try {
5209
+ return new RegExp(regexStr).test(normalizedPath);
5210
+ } catch {
5211
+ return false;
5212
+ }
5213
+ }
5214
+ function checkFilePathPolicy(filePath, policy) {
5215
+ const violations = [];
5216
+ const warnings = [];
5217
+ if (policy.never_touch) {
5218
+ for (const pattern of policy.never_touch) {
5219
+ if (matchesGlob(pattern, filePath)) {
5220
+ violations.push({
5221
+ rule: "never_touch",
5222
+ pattern,
5223
+ message: `File "${filePath}" matches never_touch pattern "${pattern}"`,
5224
+ severity: "error"
5225
+ });
5226
+ }
5227
+ }
5228
+ }
5229
+ if (policy.max_file_size && policy.max_file_size > 0) {
5230
+ try {
5231
+ const root = getProjectRoot();
5232
+ const fullPath = path15.join(root, filePath);
5233
+ if (fs15.existsSync(fullPath)) {
5234
+ const sizeKB = fs15.statSync(fullPath).size / 1024;
5235
+ if (sizeKB > policy.max_file_size) {
5236
+ violations.push({
5237
+ rule: "max_file_size",
5238
+ pattern: `${policy.max_file_size}KB`,
5239
+ message: `File "${filePath}" is ${Math.round(sizeKB)}KB, exceeds max_file_size of ${policy.max_file_size}KB`,
5240
+ severity: "error"
5241
+ });
5242
+ }
5243
+ }
5244
+ } catch {
5245
+ }
5246
+ }
5247
+ if (policy.require_review) {
5248
+ for (const pattern of policy.require_review) {
5249
+ if (matchesGlob(pattern, filePath)) {
5250
+ warnings.push({
5251
+ rule: "require_review",
5252
+ message: `File "${filePath}" requires review (matches "${pattern}")`
5253
+ });
5254
+ }
5255
+ }
5256
+ }
5257
+ return { violations, warnings };
5258
+ }
5259
+
5260
+ // src/engine/kumaDb.ts
5261
+ import initSqlJs from "sql.js";
5262
+ import fs16 from "fs";
5263
+ import path16 from "path";
5264
+ var DB_FILENAME = "kuma.db";
5265
+ var dbInstance = null;
5266
+ var initPromise = null;
5267
+ async function getDb() {
5268
+ if (dbInstance) return dbInstance;
5269
+ if (initPromise) return initPromise;
5270
+ initPromise = initDb();
5271
+ return initPromise;
5272
+ }
5273
+ async function initDb() {
5274
+ const SQL = await initSqlJs();
5275
+ const kumaDir = getKumaDir();
5276
+ const dbPath = path16.join(kumaDir, DB_FILENAME);
5277
+ if (!fs16.existsSync(kumaDir)) {
5278
+ fs16.mkdirSync(kumaDir, { recursive: true });
5279
+ }
5280
+ let db;
5281
+ if (fs16.existsSync(dbPath)) {
5282
+ const buffer = fs16.readFileSync(dbPath);
5283
+ db = new SQL.Database(buffer);
5284
+ } else {
5285
+ db = new SQL.Database();
5286
+ }
5287
+ db.run("PRAGMA journal_mode=WAL");
5288
+ db.run("PRAGMA synchronous=NORMAL");
5289
+ createSchema(db);
5290
+ saveDb(db);
5291
+ dbInstance = db;
5292
+ return db;
5293
+ }
5294
+ function saveDb(db) {
5295
+ const d = db ?? dbInstance;
5296
+ if (!d) return;
5297
+ try {
5298
+ const kumaDir = getKumaDir();
5299
+ const dbPath = path16.join(kumaDir, DB_FILENAME);
5300
+ const data = d.export();
5301
+ const buffer = Buffer.from(data);
5302
+ fs16.writeFileSync(dbPath, buffer);
5303
+ } catch (err) {
5304
+ console.error(`[KumaDB] Failed to save database: ${err}`);
5305
+ }
5306
+ }
5307
+ function createSchema(db) {
5308
+ db.run(`
5309
+ CREATE TABLE IF NOT EXISTS nodes (
5310
+ id TEXT PRIMARY KEY,
5311
+ type TEXT NOT NULL CHECK(type IN ('function','file','api_route','db_table','test','class','interface','type','module','variable')),
5312
+ name TEXT NOT NULL,
5313
+ file_path TEXT,
5314
+ metadata TEXT DEFAULT '{}',
5315
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
5316
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
5317
+ )
5318
+ `);
5319
+ db.run(`
5320
+ CREATE TABLE IF NOT EXISTS edges (
5321
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5322
+ source_id TEXT NOT NULL REFERENCES nodes(id),
5323
+ target_id TEXT NOT NULL REFERENCES nodes(id),
5324
+ type TEXT NOT NULL CHECK(type IN ('calls','imports','defines','tests','routes','implements','extends','depends_on','owns','modified_by')),
5325
+ weight REAL DEFAULT 1.0,
5326
+ metadata TEXT DEFAULT '{}',
5327
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
5328
+ UNIQUE(source_id, target_id, type)
5329
+ )
5330
+ `);
5331
+ db.run(`
5332
+ CREATE TABLE IF NOT EXISTS sessions (
5333
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5334
+ started_at INTEGER NOT NULL,
5335
+ ended_at INTEGER,
5336
+ goal TEXT,
5337
+ tool_calls INTEGER DEFAULT 0,
5338
+ edits INTEGER DEFAULT 0,
5339
+ rollbacks INTEGER DEFAULT 0,
5340
+ failures INTEGER DEFAULT 0,
5341
+ safety_score INTEGER
5342
+ )
5343
+ `);
5344
+ db.run(`
5345
+ CREATE TABLE IF NOT EXISTS tool_calls (
5346
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5347
+ session_id INTEGER REFERENCES sessions(id),
5348
+ tool_name TEXT NOT NULL,
5349
+ params TEXT,
5350
+ success INTEGER DEFAULT 1,
5351
+ duration_ms INTEGER,
5352
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
5353
+ )
5354
+ `);
5355
+ db.run(`
5356
+ CREATE TABLE IF NOT EXISTS experiences (
5357
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5358
+ tool_name TEXT NOT NULL,
5359
+ params_hash TEXT NOT NULL,
5360
+ success INTEGER NOT NULL DEFAULT 1,
5361
+ duration_ms INTEGER,
5362
+ error_pattern TEXT,
5363
+ context_file TEXT,
5364
+ context_action TEXT,
5365
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
5366
+ )
5367
+ `);
5368
+ db.run(`
5369
+ CREATE TABLE IF NOT EXISTS experience_patterns (
5370
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5371
+ antecedent_tool TEXT NOT NULL,
5372
+ antecedent_hash TEXT NOT NULL,
5373
+ consequent_tool TEXT NOT NULL,
5374
+ confidence REAL DEFAULT 0.0,
5375
+ count INTEGER DEFAULT 1,
5376
+ avg_duration_ms INTEGER DEFAULT 0,
5377
+ success_rate REAL DEFAULT 1.0,
5378
+ last_seen_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
5379
+ UNIQUE(antecedent_tool, antecedent_hash, consequent_tool)
5380
+ )
5381
+ `);
5382
+ try {
5383
+ db.run(`
5384
+ CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
5385
+ name,
5386
+ metadata,
5387
+ content='nodes',
5388
+ content_rowid='rowid'
5389
+ )
5390
+ `);
5391
+ } catch {
5392
+ console.warn("[KumaDB] FTS5 not available, full-text search disabled");
5393
+ }
5394
+ db.run(`CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type)`);
5395
+ db.run(`CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name)`);
5396
+ db.run(`CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id)`);
5397
+ db.run(`CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id)`);
5398
+ db.run(`CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type)`);
5399
+ db.run(`CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id)`);
5400
+ db.run(`CREATE INDEX IF NOT EXISTS idx_tool_calls_created ON tool_calls(created_at)`);
5401
+ db.run(`CREATE INDEX IF NOT EXISTS idx_experiences_tool ON experiences(tool_name)`);
5402
+ db.run(`CREATE INDEX IF NOT EXISTS idx_experiences_hash ON experiences(params_hash)`);
5403
+ db.run(`CREATE INDEX IF NOT EXISTS idx_experiences_created ON experiences(created_at)`);
5404
+ db.run(`CREATE INDEX IF NOT EXISTS idx_patterns_antecedent ON experience_patterns(antecedent_tool)`);
5405
+ db.run(`CREATE INDEX IF NOT EXISTS idx_patterns_confidence ON experience_patterns(confidence DESC)`);
5406
+ }
5407
+
5408
+ // src/engine/safetyAudit.ts
5409
+ var SCHEMA_SQL = `
5410
+ CREATE TABLE IF NOT EXISTS safety_audit (
5411
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5412
+ timestamp INTEGER NOT NULL,
5413
+ tool_name TEXT NOT NULL,
5414
+ action TEXT NOT NULL,
5415
+ file_path TEXT,
5416
+ risk_level TEXT NOT NULL DEFAULT 'low',
5417
+ policy_violations INTEGER DEFAULT 0,
5418
+ allowed INTEGER NOT NULL DEFAULT 1,
5419
+ duration_ms INTEGER DEFAULT 0,
5420
+ metadata TEXT DEFAULT '{}'
5421
+ );
5422
+ CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON safety_audit(timestamp);
5423
+ CREATE INDEX IF NOT EXISTS idx_audit_tool ON safety_audit(tool_name);
5424
+ CREATE INDEX IF NOT EXISTS idx_audit_risk ON safety_audit(risk_level);
5425
+ CREATE INDEX IF NOT EXISTS idx_audit_allowed ON safety_audit(allowed);
5426
+ `;
5427
+ var schemaEnsured = false;
5428
+ async function ensureSchema() {
5429
+ if (schemaEnsured) return;
5430
+ try {
5431
+ const db = await getDb();
5432
+ db.exec(SCHEMA_SQL);
5433
+ saveDb();
5434
+ schemaEnsured = true;
5435
+ } catch (err) {
5436
+ console.error(`[SafetyAudit] Failed to ensure schema: ${err}`);
5437
+ }
5438
+ }
5439
+ async function recordAudit(entry) {
5440
+ try {
5441
+ await ensureSchema();
5442
+ const db = await getDb();
5443
+ db.run(`
5444
+ INSERT INTO safety_audit (timestamp, tool_name, action, file_path, risk_level, policy_violations, allowed, duration_ms, metadata)
5445
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
5446
+ `, [
5447
+ entry.timestamp,
5448
+ entry.toolName,
5449
+ entry.action,
5450
+ entry.filePath || null,
5451
+ entry.riskLevel,
5452
+ entry.policyViolations,
5453
+ entry.allowed ? 1 : 0,
5454
+ entry.durationMs,
5455
+ JSON.stringify(entry.metadata || {})
5456
+ ]);
5457
+ saveDb();
5458
+ } catch (err) {
5459
+ console.error(`[SafetyAudit] Failed to record audit: ${err}`);
5460
+ }
5461
+ }
5462
+
5463
+ // src/engine/kumaSafetyProxy.ts
5464
+ async function preCheck(toolName, params, opts = {}) {
5465
+ const messages = [];
5466
+ let policyViolations = 0;
5467
+ let highestRisk = "low";
5468
+ const blockThreshold = opts.blockRiskThreshold || "high";
5469
+ const filePath = opts.extractFilePath?.(params);
5470
+ if (filePath) {
5471
+ const validation = validateFilePath(filePath);
5472
+ if (!validation.valid) {
5473
+ messages.push(`\u{1F6AB} Path blocked: ${validation.error.message}`);
5474
+ policyViolations++;
5475
+ highestRisk = "critical";
5476
+ }
5477
+ }
5478
+ const policy = loadPolicy();
5479
+ if (filePath) {
5480
+ const { violations, warnings } = checkFilePathPolicy(filePath, policy);
5481
+ policyViolations += violations.length;
5482
+ for (const v of violations) {
5483
+ messages.push(`\u{1F4DC} Policy violation (${v.rule}): ${v.message}`);
5484
+ highestRisk = "critical";
5485
+ }
5486
+ for (const w of warnings) {
5487
+ messages.push(`\u26A0\uFE0F Policy warning (${w.rule}): ${w.message}`);
5488
+ if (highestRisk === "low") highestRisk = "medium";
5489
+ }
5490
+ }
5491
+ const command = opts.extractCommand?.(params);
5492
+ if (command) {
5493
+ const dangerousPatterns = [
5494
+ "rm -rf",
5495
+ "rm -fr",
5496
+ "git push",
5497
+ "git commit",
5498
+ "npm publish",
5499
+ "yarn publish",
5500
+ "pnpm publish",
5501
+ "| bash",
5502
+ "| sh",
5503
+ "eval ",
5504
+ "exec ",
5505
+ "mkfs",
5506
+ "dd if=",
5507
+ "shred"
5508
+ ];
5509
+ const matched = dangerousPatterns.find((p) => command.toLowerCase().includes(p));
5510
+ if (matched) {
5511
+ messages.push(`\u{1F6AB} Command blocked: contains dangerous pattern "${matched}"`);
5512
+ policyViolations++;
5513
+ highestRisk = "critical";
5514
+ }
5515
+ }
5516
+ try {
5517
+ const db = await getDb();
5518
+ const recentFailures = db.exec("SELECT COUNT(*) as c FROM failure_kb WHERE resolved = 0")[0]?.values[0][0] ?? 0;
5519
+ if (recentFailures > 3 && (toolName.includes("edit") || toolName === "precise_diff_editor")) {
5520
+ messages.push(`\u26A0\uFE0F ${recentFailures} unresolved failures \u2014 consider fixing them before editing`);
5521
+ if (highestRisk === "low") highestRisk = "medium";
5522
+ }
5523
+ } catch {
5524
+ }
5525
+ try {
5526
+ const summary = sessionMemory.getSummary();
5527
+ const goal = summary.currentGoal;
5528
+ if (!goal && (toolName === "precise_diff_editor" || toolName === "batch_file_writer")) {
5529
+ messages.push("\u{1F4A1} No goal set \u2014 consider setting a goal with setGoal() to track intent");
5530
+ }
5531
+ } catch {
5532
+ }
5533
+ const riskLevel = highestRisk;
5534
+ const thresholdLevels = ["low", "medium", "high", "critical"];
5535
+ const thresholdIndex = thresholdLevels.indexOf(blockThreshold);
5536
+ const riskIndex = thresholdLevels.indexOf(riskLevel);
5537
+ const allowed = opts.blockOnViolation !== false ? riskIndex < thresholdIndex : policyViolations === 0;
5538
+ return { allowed, riskLevel, policyViolations, messages };
5539
+ }
5540
+ function wrapWithSafety(toolName, handler, opts = {}) {
5541
+ return async (params) => {
5542
+ const startTime = Date.now();
5543
+ const p = params;
5544
+ const action = p.action || "execute";
5545
+ try {
5546
+ const verdict = await preCheck(toolName, p, opts);
5547
+ const filePath = opts.extractFilePath?.(p);
5548
+ if (!verdict.allowed) {
5549
+ await recordAudit({
5550
+ timestamp: Math.floor(startTime / 1e3),
5551
+ toolName,
5552
+ action,
5553
+ filePath,
5554
+ riskLevel: verdict.riskLevel,
5555
+ policyViolations: verdict.policyViolations,
5556
+ allowed: false,
5557
+ durationMs: Date.now() - startTime,
5558
+ metadata: { blocked: true, messages: verdict.messages }
5559
+ });
5560
+ return formatBlocked(verdict, toolName);
5561
+ }
5562
+ const result = await handler(params);
5563
+ const durationMs = Date.now() - startTime;
5564
+ await recordAudit({
5565
+ timestamp: Math.floor(startTime / 1e3),
5566
+ toolName,
5567
+ action,
5568
+ filePath,
5569
+ riskLevel: verdict.riskLevel,
5570
+ policyViolations: verdict.policyViolations,
5571
+ allowed: true,
5572
+ durationMs,
5573
+ metadata: { success: true }
5574
+ });
5575
+ if (verdict.messages.length > 0) {
5576
+ const warnings = verdict.messages.filter((m) => m.startsWith("\u26A0\uFE0F") || m.startsWith("\u{1F4A1}")).join("\n");
5577
+ if (warnings) {
5578
+ return `${result}
5579
+
5580
+ \u{1F6E1}\uFE0F **Safety Context:**
5581
+ ${warnings}`;
5582
+ }
5583
+ }
5584
+ return result;
5585
+ } catch (err) {
5586
+ const durationMs = Date.now() - startTime;
5587
+ await recordAudit({
5588
+ timestamp: Math.floor(startTime / 1e3),
5589
+ toolName,
5590
+ action,
5591
+ riskLevel: "critical",
5592
+ policyViolations: 0,
5593
+ allowed: false,
5594
+ durationMs,
5595
+ metadata: { error: String(err) }
5596
+ });
5597
+ return `\u26A0\uFE0F **Safety Proxy Error** \u2014 ${toolName} failed safety check:
5598
+ ${err}`;
5599
+ }
5600
+ };
5601
+ }
5602
+ function formatBlocked(verdict, toolName) {
5603
+ const icon = verdict.riskLevel === "critical" ? "\u{1F534}" : verdict.riskLevel === "high" ? "\u{1F7E0}" : "\u{1F7E1}";
5604
+ const lines = [
5605
+ `${icon} **Safety AI Layer \u2014 \u26D4 Blocked**`,
5606
+ `\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`,
5607
+ "",
5608
+ `\u{1F3AF} Tool: **${toolName}**`,
5609
+ `\u26A0\uFE0F Risk Level: **${verdict.riskLevel.toUpperCase()}**`,
5610
+ `\u{1F4DC} Policy Violations: ${verdict.policyViolations}`,
5611
+ "",
5612
+ ...verdict.messages,
5613
+ "",
5614
+ "\u{1F4A1} **Resolution steps:**",
5615
+ " 1. Review the policy violations above",
5616
+ " 2. If editing: use precise_diff_editor with proper params",
5617
+ " 3. If command: avoid dangerous patterns",
5618
+ " 4. To bypass: kuma_safety({ action: 'override', tool: '...' })"
5619
+ ];
5620
+ return lines.join("\n");
5621
+ }
5622
+
5382
5623
  // src/manifest.ts
5383
5624
  var MEMORY_TOPICS = ["decisions", "glossary", "architecture", "conventions", "known-issues"];
5384
5625
  function registerAllTools(server) {
@@ -5418,6 +5659,9 @@ function registerAllTools(server) {
5418
5659
  }
5419
5660
  }
5420
5661
  );
5662
+ const safeEditHandler = wrapWithSafety("precise_diff_editor", handlePreciseDiffEditor, {
5663
+ extractFilePath: (p) => p.filePath
5664
+ });
5421
5665
  server.tool(
5422
5666
  "precise_diff_editor",
5423
5667
  "Edit code with safety net. Search-and-replace with fuzzy fallback + automatic versioned backup. Use action:'rollback' to undo edits.",
@@ -5433,11 +5677,13 @@ function registerAllTools(server) {
5433
5677
  invalid_type_error: 'precise_diff_editor: "edits" must be an ARRAY of { searchBlock, replaceBlock } objects.\n\n\u2705 Correct format:\n{\n edits: [\n { searchBlock: "old code", replaceBlock: "new code" }\n ]\n}'
5434
5678
  }).min(1).max(10).optional().describe("Array of edits (max 10). Each edit has: searchBlock (code to find), replaceBlock (new code)"),
5435
5679
  dryRun: z.boolean().optional().default(false).describe("Preview changes without writing to disk. Set dryRun: true to preview first."),
5436
- version: z.union([z.number().min(1), z.literal("list")]).optional().describe("Backup version to restore (1=newest, omit=latest, 'list'=show versions)")
5680
+ version: z.union([z.number().min(1), z.literal("list")]).optional().describe("Backup version to restore (1=newest, omit=latest, 'list'=show versions)"),
5681
+ scope: z.enum(["file", "dir", "edit-id", "commit"]).optional().describe("Rollback scope: file (default), dir (directory). Requires filePath. edit-id (by edit ID). Requires editId. commit (git-based)."),
5682
+ editId: z.string().optional().describe("Edit ID for edit-id scoped rollback. Use scope:'edit-id' with version:'list' to see all tracked edit IDs.")
5437
5683
  },
5438
5684
  async (params) => {
5439
5685
  try {
5440
- const result = await handlePreciseDiffEditor(params);
5686
+ const result = await safeEditHandler(params);
5441
5687
  return { content: [{ type: "text", text: result }] };
5442
5688
  } catch (err) {
5443
5689
  return { content: [{ type: "text", text: `Error in precise_diff_editor: ${err}` }], isError: true };
@@ -5718,7 +5964,7 @@ function registerAllTools(server) {
5718
5964
  }
5719
5965
  }
5720
5966
  );
5721
- console.error("[Manifest] Registered 18 tools.");
5967
+ console.error("[Manifest] Registered 19 tools.");
5722
5968
  }
5723
5969
 
5724
5970
  // src/index.ts
@@ -5819,22 +6065,23 @@ async function main() {
5819
6065
  });
5820
6066
  const output = formatInitResults(results);
5821
6067
  console.log(output);
5822
- const fs16 = await import("fs");
5823
- const path16 = await import("path");
5824
- const matchaSkills = path16.resolve(process.cwd(), "skills/matcha/SKILL.md");
5825
- const matchaAgents = path16.resolve(
6068
+ const fs17 = await import("fs");
6069
+ const path17 = await import("path");
6070
+ const matchaSkills = path17.resolve(process.cwd(), "skills/matcha/SKILL.md");
6071
+ const matchaAgents = path17.resolve(
5826
6072
  process.cwd(),
5827
6073
  ".agents/skills/matcha/SKILL.md"
5828
6074
  );
5829
- const matchaCursor = path16.resolve(
6075
+ const matchaRootSkills = path17.resolve(
5830
6076
  process.cwd(),
5831
- ".cursor/rules/matcha.mdc"
6077
+ "skills/matcha/SKILL.md"
5832
6078
  );
5833
- const matchaWindsurf = path16.resolve(
6079
+ const matchaAgentsMd = path17.resolve(process.cwd(), "AGENTS.md");
6080
+ const matchaWindsurfRules = path17.resolve(
5834
6081
  process.cwd(),
5835
- ".windsurf/rules/matcha.md"
6082
+ ".windsurfrules"
5836
6083
  );
5837
- if (fs16.existsSync(matchaSkills) || fs16.existsSync(matchaAgents) || fs16.existsSync(matchaCursor) || fs16.existsSync(matchaWindsurf)) {
6084
+ if (fs17.existsSync(matchaSkills) || fs17.existsSync(matchaAgents) || fs17.existsSync(matchaRootSkills) || fs17.existsSync(matchaAgentsMd) || fs17.existsSync(matchaWindsurfRules)) {
5838
6085
  console.error(
5839
6086
  "\n\u{1F375} Hey, I see matcha is installed \u2014 they pair well together!"
5840
6087
  );
@@ -5847,20 +6094,56 @@ async function main() {
5847
6094
  });
5848
6095
  (async () => {
5849
6096
  try {
5850
- const { generateInitMdContent } = await import("./init-AMRMKI4X.js");
5851
- const fs16 = await import("fs");
5852
- const path16 = await import("path");
5853
- const initMdPath = path16.resolve(process.cwd(), ".kuma/init.md");
5854
- if (!fs16.existsSync(initMdPath)) {
5855
- const kumaDir = path16.dirname(initMdPath);
5856
- if (!fs16.existsSync(kumaDir)) fs16.mkdirSync(kumaDir, { recursive: true });
5857
- fs16.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
6097
+ const { generateInitMdContent } = await import("./init-WZADRE7F.js");
6098
+ const fs17 = await import("fs");
6099
+ const path17 = await import("path");
6100
+ const initMdPath = path17.resolve(process.cwd(), ".kuma/init.md");
6101
+ if (!fs17.existsSync(initMdPath)) {
6102
+ const kumaDir = path17.dirname(initMdPath);
6103
+ if (!fs17.existsSync(kumaDir)) fs17.mkdirSync(kumaDir, { recursive: true });
6104
+ fs17.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
5858
6105
  console.error(`[${SERVER_NAME}] Auto-generated .kuma/init.md`);
5859
6106
  }
5860
6107
  } catch (err) {
5861
6108
  console.error(`[${SERVER_NAME}] Failed to auto-generate .kuma/init.md: ${err}`);
5862
6109
  }
5863
6110
  })();
6111
+ (async () => {
6112
+ try {
6113
+ const { detectAgent, getSkillPath, getAgentLabel } = await import("./agentDetector-AHKFSZGQ.js");
6114
+ const { generateSkill, getSecondaryFiles } = await import("./skillGenerator-F6YO4H5D.js");
6115
+ const fs17 = await import("fs");
6116
+ const path17 = await import("path");
6117
+ const detection = detectAgent();
6118
+ if (!detection.primary) {
6119
+ console.error(`[${SERVER_NAME}] No AI agent detected \u2014 skipping auto-skill creation`);
6120
+ return;
6121
+ }
6122
+ const agentType = detection.primary;
6123
+ const skillPath = getSkillPath(agentType);
6124
+ const fullPath = path17.resolve(process.cwd(), skillPath);
6125
+ if (fs17.existsSync(fullPath)) {
6126
+ console.error(`[${SERVER_NAME}] Skill exists for ${getAgentLabel(agentType)} \u2014 skipping`);
6127
+ return;
6128
+ }
6129
+ const dir = path17.dirname(fullPath);
6130
+ if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
6131
+ fs17.writeFileSync(fullPath, generateSkill(agentType), "utf-8");
6132
+ console.error(`[${SERVER_NAME}] Auto-created ${skillPath} for ${getAgentLabel(agentType)}`);
6133
+ const secondaryFiles = getSecondaryFiles(agentType);
6134
+ for (const sf of secondaryFiles) {
6135
+ const sfPath = path17.resolve(process.cwd(), sf.path);
6136
+ const sfDir = path17.dirname(sfPath);
6137
+ if (!fs17.existsSync(sfPath)) {
6138
+ if (!fs17.existsSync(sfDir)) fs17.mkdirSync(sfDir, { recursive: true });
6139
+ fs17.writeFileSync(sfPath, sf.content, "utf-8");
6140
+ console.error(`[${SERVER_NAME}] Auto-created ${sf.path}`);
6141
+ }
6142
+ }
6143
+ } catch (err) {
6144
+ console.error(`[${SERVER_NAME}] Failed to auto-create skill file: ${err}`);
6145
+ }
6146
+ })();
5864
6147
  const server = new McpServer(
5865
6148
  {
5866
6149
  name: SERVER_NAME,