@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/README.md +334 -20
- package/dist/agentDetector-AHKFSZGQ.js +186 -0
- package/dist/chunk-7Q3YUJSM.js +161 -0
- package/dist/{chunk-2BFTJMBP.js → chunk-EM6IK2TB.js} +73 -225
- package/dist/chunk-QPVPFUCQ.js +612 -0
- package/dist/index.js +1241 -958
- package/dist/{init-AMRMKI4X.js → init-WZADRE7F.js} +2 -1
- package/dist/sessionMemory-LGQFVHHE.js +14 -0
- package/dist/skillGenerator-F6YO4H5D.js +278 -0
- package/package.json +7 -3
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-
|
|
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
|
|
25
|
-
import
|
|
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
|
-
"**/.
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
730
|
-
const stat =
|
|
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 =
|
|
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 =
|
|
803
|
-
const bytesRead =
|
|
804
|
-
|
|
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
|
|
816
|
-
import
|
|
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 (!
|
|
230
|
+
if (!fs2.existsSync(resolvedPath)) {
|
|
827
231
|
const projectRoot = getProjectRoot();
|
|
828
232
|
let suggestion;
|
|
829
|
-
if (!
|
|
830
|
-
const cwdPath =
|
|
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 =
|
|
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 =
|
|
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
|
|
993
|
-
import
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
1272
|
-
|
|
1273
|
-
|
|
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 =
|
|
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 =
|
|
1395
|
-
const backupRoot =
|
|
1396
|
-
if (!
|
|
1397
|
-
return `Error: No backup folder (.
|
|
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 =
|
|
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 .
|
|
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 =
|
|
1411
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1455
|
-
import
|
|
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: "${
|
|
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 (
|
|
1149
|
+
if (fs4.existsSync(resolvedPath)) {
|
|
1487
1150
|
ensureBackupDir();
|
|
1488
1151
|
const backupPath = getBackupPath(file.filePath);
|
|
1489
|
-
const backupDir =
|
|
1490
|
-
|
|
1491
|
-
|
|
1152
|
+
const backupDir = path4.dirname(backupPath);
|
|
1153
|
+
fs4.mkdirSync(backupDir, { recursive: true });
|
|
1154
|
+
fs4.copyFileSync(resolvedPath, backupPath);
|
|
1492
1155
|
}
|
|
1493
|
-
const dir =
|
|
1494
|
-
if (!
|
|
1495
|
-
|
|
1156
|
+
const dir = path4.dirname(resolvedPath);
|
|
1157
|
+
if (!fs4.existsSync(dir)) {
|
|
1158
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1496
1159
|
}
|
|
1497
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
1620
|
-
const subPkg = readJsonSafe(
|
|
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(
|
|
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(
|
|
1664
|
-
const pnpmWorkspace = readYamlLite(
|
|
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 =
|
|
1685
|
-
if (!
|
|
1347
|
+
const dir = path5.join(root, match[1]);
|
|
1348
|
+
if (!fs5.existsSync(dir)) continue;
|
|
1686
1349
|
let entries = [];
|
|
1687
1350
|
try {
|
|
1688
|
-
entries =
|
|
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 =
|
|
1357
|
+
const pkgPath = path5.join(dir, entry.name, "package.json");
|
|
1695
1358
|
const subPkg = readJsonSafe(pkgPath);
|
|
1696
1359
|
if (!subPkg) continue;
|
|
1697
|
-
const workspacePath =
|
|
1360
|
+
const workspacePath = path5.join(dir, entry.name);
|
|
1698
1361
|
results.push({
|
|
1699
|
-
path:
|
|
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 (!
|
|
1716
|
-
const text =
|
|
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(
|
|
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(
|
|
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 =
|
|
1772
|
-
if (
|
|
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(
|
|
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(
|
|
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 (
|
|
1798
|
-
if (
|
|
1799
|
-
if (
|
|
1800
|
-
if (
|
|
1801
|
-
if (
|
|
1802
|
-
if (
|
|
1803
|
-
if (
|
|
1804
|
-
if (
|
|
1805
|
-
if (
|
|
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 =
|
|
1477
|
+
let currentDir = path5.resolve(dir);
|
|
1815
1478
|
while (currentDir.startsWith(root)) {
|
|
1816
|
-
if (
|
|
1817
|
-
if (
|
|
1818
|
-
if (
|
|
1819
|
-
if (
|
|
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 =
|
|
1484
|
+
currentDir = path5.dirname(currentDir);
|
|
1822
1485
|
}
|
|
1823
1486
|
return "npm";
|
|
1824
1487
|
}
|
|
1825
1488
|
function detectModuleSystem(root) {
|
|
1826
|
-
const pkg = readJsonSafe(
|
|
1489
|
+
const pkg = readJsonSafe(path5.join(root, "package.json"));
|
|
1827
1490
|
if (pkg?.type === "module") return "esm";
|
|
1828
|
-
const srcDir =
|
|
1829
|
-
if (
|
|
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 =
|
|
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 (
|
|
1844
|
-
const srcDir =
|
|
1845
|
-
if (
|
|
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(
|
|
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 (
|
|
1878
|
-
return JSON.parse(
|
|
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 =
|
|
1886
|
-
if (!
|
|
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 =
|
|
1556
|
+
const entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
1894
1557
|
for (const entry of entries) {
|
|
1895
|
-
const fullPath =
|
|
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 =
|
|
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 =
|
|
2095
|
-
const normalizedResolved =
|
|
2096
|
-
const normalizedRoot =
|
|
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 (!
|
|
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
|
|
2187
|
-
import
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
3454
|
-
import
|
|
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
|
-
".
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3528
|
-
const relativePath =
|
|
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 =
|
|
3202
|
+
subEntries = fs8.readdirSync(fullPath, { withFileTypes: true });
|
|
3540
3203
|
} catch {
|
|
3541
3204
|
}
|
|
3542
3205
|
const hasVisibleContent = subEntries.some(
|
|
3543
|
-
(e) => !shouldIgnore(e.name,
|
|
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 =
|
|
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
|
|
3586
|
-
import
|
|
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 = {
|
|
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) =>
|
|
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 (
|
|
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) =>
|
|
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 (
|
|
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 =
|
|
3703
|
-
if (
|
|
3704
|
-
return JSON.parse(
|
|
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 (
|
|
3761
|
-
if (
|
|
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
|
-
|
|
3767
|
-
|
|
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 (
|
|
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
|
|
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/
|
|
3643
|
+
// src/utils/kumaShared.ts
|
|
3978
3644
|
import { execSync as execSync3 } from "child_process";
|
|
3979
|
-
|
|
3645
|
+
function getSessionStats(inputGoal) {
|
|
3980
3646
|
const summary = sessionMemory.getSummary();
|
|
3981
|
-
const goal =
|
|
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
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
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
|
-
|
|
3666
|
+
return execSync3("git diff --stat", {
|
|
4001
3667
|
cwd: root,
|
|
4002
3668
|
encoding: "utf-8",
|
|
4003
|
-
timeout
|
|
3669
|
+
timeout
|
|
4004
3670
|
}).trim();
|
|
4005
3671
|
} catch {
|
|
3672
|
+
return "";
|
|
4006
3673
|
}
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
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
|
-
|
|
4018
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4031
|
-
|
|
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
|
-
|
|
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:
|
|
3784
|
+
goal: stats.goal,
|
|
3785
|
+
modifiedFiles: stats.modifiedFiles.length,
|
|
3786
|
+
toolCalls: stats.toolCallCount,
|
|
4061
3787
|
unresolvedFailures: unresolved.length,
|
|
4062
|
-
hasLoop:
|
|
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
|
|
4076
|
-
import
|
|
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 =
|
|
3826
|
+
const ext = path10.extname(file).toLowerCase();
|
|
4104
3827
|
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
4105
|
-
const relativePath =
|
|
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 =
|
|
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: ${
|
|
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 =
|
|
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 =
|
|
3879
|
+
const stat = fs10.statSync(path10.join(projectRoot, entry.name));
|
|
4157
3880
|
if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
|
|
4158
|
-
recent.push(
|
|
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 =
|
|
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 =
|
|
4188
|
-
if (
|
|
4189
|
-
const content =
|
|
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 =
|
|
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
|
|
4273
|
-
import
|
|
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
|
|
4000
|
+
return path11.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
|
|
4278
4001
|
}
|
|
4279
4002
|
function ensureSnapshotDir() {
|
|
4280
4003
|
const dir = snapshotDir();
|
|
4281
|
-
if (!
|
|
4282
|
-
|
|
4004
|
+
if (!fs11.existsSync(dir)) {
|
|
4005
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
4283
4006
|
}
|
|
4284
4007
|
}
|
|
4285
4008
|
function snapshotFilePath(timestamp) {
|
|
4286
|
-
return
|
|
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:
|
|
4025
|
+
gitDiffStat: getGitDiffStat2(),
|
|
4303
4026
|
completedSteps: summary.completedSteps || [],
|
|
4304
4027
|
hasConventions: !!summary.hasConventions
|
|
4305
4028
|
};
|
|
4306
4029
|
try {
|
|
4307
|
-
|
|
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 (!
|
|
4038
|
+
if (!fs11.existsSync(dir)) return [];
|
|
4316
4039
|
try {
|
|
4317
|
-
const files =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
4378
|
-
const
|
|
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
|
|
4398
|
-
const
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
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:
|
|
4476
|
-
toolCalls:
|
|
4477
|
-
unresolvedFailures:
|
|
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
|
|
4528
|
-
import
|
|
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 =
|
|
4532
|
-
const memoriesDir =
|
|
4533
|
-
const initMdPath =
|
|
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 (
|
|
4537
|
-
rules =
|
|
4240
|
+
if (fs12.existsSync(initMdPath)) {
|
|
4241
|
+
rules = fs12.readFileSync(initMdPath, "utf-8");
|
|
4538
4242
|
}
|
|
4539
4243
|
const memories = [];
|
|
4540
|
-
if (
|
|
4244
|
+
if (fs12.existsSync(memoriesDir)) {
|
|
4541
4245
|
try {
|
|
4542
|
-
const files =
|
|
4246
|
+
const files = fs12.readdirSync(memoriesDir).filter((f) => f.endsWith(".md"));
|
|
4543
4247
|
for (const file of files) {
|
|
4544
4248
|
try {
|
|
4545
|
-
const content =
|
|
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
|
|
4591
|
-
import
|
|
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 =
|
|
4630
|
-
if (!
|
|
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
|
-
|
|
4693
|
-
|
|
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 (
|
|
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(
|
|
4703
|
-
const binPath =
|
|
4704
|
-
if (
|
|
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 =
|
|
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
|
|
4944
|
-
import
|
|
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 (!
|
|
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 =
|
|
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: ${
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
4745
|
+
const relPath = path14.relative(projectRoot, definition.filePath);
|
|
5042
4746
|
let lineContent = "";
|
|
5043
4747
|
try {
|
|
5044
|
-
const content =
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
4825
|
+
fs14.writeFileSync(change.filePath, lines2.join("\n"), "utf-8");
|
|
5122
4826
|
totalEdits += change.edits.length;
|
|
5123
4827
|
fileChanges.push({
|
|
5124
|
-
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
5823
|
-
const
|
|
5824
|
-
const matchaSkills =
|
|
5825
|
-
const matchaAgents =
|
|
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
|
|
6075
|
+
const matchaRootSkills = path17.resolve(
|
|
5830
6076
|
process.cwd(),
|
|
5831
|
-
"
|
|
6077
|
+
"skills/matcha/SKILL.md"
|
|
5832
6078
|
);
|
|
5833
|
-
const
|
|
6079
|
+
const matchaAgentsMd = path17.resolve(process.cwd(), "AGENTS.md");
|
|
6080
|
+
const matchaWindsurfRules = path17.resolve(
|
|
5834
6081
|
process.cwd(),
|
|
5835
|
-
".
|
|
6082
|
+
".windsurfrules"
|
|
5836
6083
|
);
|
|
5837
|
-
if (
|
|
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-
|
|
5851
|
-
const
|
|
5852
|
-
const
|
|
5853
|
-
const initMdPath =
|
|
5854
|
-
if (!
|
|
5855
|
-
const kumaDir =
|
|
5856
|
-
if (!
|
|
5857
|
-
|
|
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,
|