@plumpslabs/kuma 2.1.2 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-P5Y7IAUC.js +912 -0
- package/dist/index.js +939 -1237
- package/dist/init-ZRT7JP5F.js +14 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ALL_CONFIG_TYPES,
|
|
4
|
+
ensureBackupDir,
|
|
5
|
+
formatInitResults,
|
|
6
|
+
getBackupPath,
|
|
7
|
+
getProjectRoot,
|
|
8
|
+
runInit,
|
|
9
|
+
validateFileExtension,
|
|
10
|
+
validateFilePath
|
|
11
|
+
} from "./chunk-P5Y7IAUC.js";
|
|
2
12
|
|
|
3
13
|
// src/index.ts
|
|
4
14
|
import { readFileSync } from "fs";
|
|
@@ -11,157 +21,8 @@ import { z } from "zod";
|
|
|
11
21
|
|
|
12
22
|
// src/tools/smartGrep.ts
|
|
13
23
|
import fg from "fast-glob";
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
// src/utils/pathValidator.ts
|
|
18
|
-
import path from "path";
|
|
19
|
-
import fs from "fs";
|
|
20
|
-
function validateFilePath(filePath, projectRoot) {
|
|
21
|
-
try {
|
|
22
|
-
const root = projectRoot ?? getProjectRoot();
|
|
23
|
-
const resolvedRoot = path.resolve(root);
|
|
24
|
-
const normalizedRoot = path.normalize(resolvedRoot).toLowerCase();
|
|
25
|
-
let resolvedPath;
|
|
26
|
-
if (!path.isAbsolute(filePath)) {
|
|
27
|
-
const cwdPath = path.resolve(process.cwd(), filePath);
|
|
28
|
-
const normalizedCwdPath = path.normalize(cwdPath).toLowerCase();
|
|
29
|
-
if (fs.existsSync(cwdPath) && normalizedCwdPath.startsWith(normalizedRoot)) {
|
|
30
|
-
resolvedPath = cwdPath;
|
|
31
|
-
} else {
|
|
32
|
-
resolvedPath = path.resolve(resolvedRoot, filePath);
|
|
33
|
-
}
|
|
34
|
-
} else {
|
|
35
|
-
resolvedPath = path.resolve(resolvedRoot, filePath);
|
|
36
|
-
}
|
|
37
|
-
const normalizedPath = path.normalize(resolvedPath).toLowerCase();
|
|
38
|
-
if (!normalizedPath.startsWith(normalizedRoot)) {
|
|
39
|
-
return {
|
|
40
|
-
valid: false,
|
|
41
|
-
error: new Error(
|
|
42
|
-
`PATH_TRAVERSAL: Access denied. Path "${filePath}" resolves outside project root "${resolvedRoot}".`
|
|
43
|
-
)
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
if (normalizedPath.includes("..")) {
|
|
47
|
-
return {
|
|
48
|
-
valid: false,
|
|
49
|
-
error: new Error(
|
|
50
|
-
`PATH_TRAVERSAL: Path traversal detected in "${filePath}". Relative paths with ".." are not allowed.`
|
|
51
|
-
)
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
const blockedPatterns = [
|
|
55
|
-
"/etc/",
|
|
56
|
-
"/sys/",
|
|
57
|
-
"/proc/",
|
|
58
|
-
"/dev/",
|
|
59
|
-
"/boot/",
|
|
60
|
-
"/usr/",
|
|
61
|
-
"C:\\Windows",
|
|
62
|
-
"C:\\Program Files",
|
|
63
|
-
"C:\\Program Files (x86)",
|
|
64
|
-
"C:\\System32",
|
|
65
|
-
"~/.ssh",
|
|
66
|
-
"/root/"
|
|
67
|
-
];
|
|
68
|
-
for (const pattern of blockedPatterns) {
|
|
69
|
-
if (normalizedPath.includes(pattern.toLowerCase())) {
|
|
70
|
-
return {
|
|
71
|
-
valid: false,
|
|
72
|
-
error: new Error(
|
|
73
|
-
`PATH_TRAVERSAL: Access to system directory "${pattern}" is blocked.`
|
|
74
|
-
)
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
try {
|
|
79
|
-
if (fs.existsSync(resolvedPath)) {
|
|
80
|
-
const realPath = fs.realpathSync(resolvedPath);
|
|
81
|
-
const normalizedRealPath = path.normalize(realPath).toLowerCase();
|
|
82
|
-
if (!normalizedRealPath.startsWith(normalizedRoot)) {
|
|
83
|
-
return {
|
|
84
|
-
valid: false,
|
|
85
|
-
error: new Error(
|
|
86
|
-
`PATH_TRAVERSAL: Symlink escape detected. Path "${filePath}" resolves to "${realPath}" which is outside project root "${resolvedRoot}".`
|
|
87
|
-
)
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
92
|
-
}
|
|
93
|
-
if (normalizedPath.includes("node_modules")) {
|
|
94
|
-
return {
|
|
95
|
-
valid: true,
|
|
96
|
-
resolvedPath
|
|
97
|
-
// Warning will be handled by caller
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
return { valid: true, resolvedPath };
|
|
101
|
-
} catch (err) {
|
|
102
|
-
return {
|
|
103
|
-
valid: false,
|
|
104
|
-
error: err instanceof Error ? err : new Error(`Path validation error: ${String(err)}`)
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
function validateFileExtension(filePath) {
|
|
109
|
-
const allowedExtensions = [
|
|
110
|
-
".ts",
|
|
111
|
-
".tsx",
|
|
112
|
-
".js",
|
|
113
|
-
".jsx",
|
|
114
|
-
".mjs",
|
|
115
|
-
".cjs",
|
|
116
|
-
".json",
|
|
117
|
-
".md",
|
|
118
|
-
".css",
|
|
119
|
-
".html",
|
|
120
|
-
".htm",
|
|
121
|
-
".env",
|
|
122
|
-
".env.example",
|
|
123
|
-
".env.local",
|
|
124
|
-
".yml",
|
|
125
|
-
".yaml",
|
|
126
|
-
".toml",
|
|
127
|
-
".svg",
|
|
128
|
-
".png",
|
|
129
|
-
".jpg",
|
|
130
|
-
".gif",
|
|
131
|
-
".sh",
|
|
132
|
-
".bat",
|
|
133
|
-
".ps1",
|
|
134
|
-
".sql",
|
|
135
|
-
".graphql",
|
|
136
|
-
".gql",
|
|
137
|
-
".vue",
|
|
138
|
-
".svelte",
|
|
139
|
-
".astro",
|
|
140
|
-
".prisma",
|
|
141
|
-
".proto",
|
|
142
|
-
".txt",
|
|
143
|
-
".log"
|
|
144
|
-
];
|
|
145
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
146
|
-
return allowedExtensions.includes(ext);
|
|
147
|
-
}
|
|
148
|
-
function getProjectRoot() {
|
|
149
|
-
return process.env.AGENT_PROJECT_ROOT ?? process.cwd();
|
|
150
|
-
}
|
|
151
|
-
function getBackupPath(filePath, timestamp) {
|
|
152
|
-
const ts = timestamp ?? Date.now();
|
|
153
|
-
const root = getProjectRoot();
|
|
154
|
-
const relativePath = path.relative(root, filePath);
|
|
155
|
-
return path.join(root, ".agent-backups", String(ts), relativePath);
|
|
156
|
-
}
|
|
157
|
-
function ensureBackupDir() {
|
|
158
|
-
const root = getProjectRoot();
|
|
159
|
-
const backupDir = path.join(root, ".agent-backups");
|
|
160
|
-
if (!fs.existsSync(backupDir)) {
|
|
161
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
162
|
-
}
|
|
163
|
-
return backupDir;
|
|
164
|
-
}
|
|
24
|
+
import fs2 from "fs";
|
|
25
|
+
import path2 from "path";
|
|
165
26
|
|
|
166
27
|
// src/utils/tokenCounter.ts
|
|
167
28
|
function estimateTokens(text) {
|
|
@@ -186,33 +47,33 @@ function limitLines(text, maxLines) {
|
|
|
186
47
|
}
|
|
187
48
|
|
|
188
49
|
// src/engine/sessionMemory.ts
|
|
189
|
-
import
|
|
190
|
-
import
|
|
50
|
+
import fs from "fs";
|
|
51
|
+
import path from "path";
|
|
191
52
|
var SessionMemory = class {
|
|
192
53
|
state;
|
|
193
54
|
initialized = false;
|
|
194
55
|
init(config) {
|
|
195
|
-
const kumaDir =
|
|
196
|
-
const sessionFile =
|
|
197
|
-
const oldFile =
|
|
198
|
-
if (
|
|
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)) {
|
|
199
60
|
try {
|
|
200
|
-
|
|
61
|
+
fs.renameSync(oldFile, sessionFile);
|
|
201
62
|
console.error("[SessionMemory] Migrated .kuma-memory.json \u2192 memory.json");
|
|
202
63
|
} catch {
|
|
203
64
|
}
|
|
204
65
|
}
|
|
205
|
-
const oldSessionFile =
|
|
206
|
-
if (
|
|
66
|
+
const oldSessionFile = path.join(kumaDir, "session.json");
|
|
67
|
+
if (fs.existsSync(oldSessionFile) && !fs.existsSync(sessionFile)) {
|
|
207
68
|
try {
|
|
208
|
-
|
|
69
|
+
fs.renameSync(oldSessionFile, sessionFile);
|
|
209
70
|
console.error("[SessionMemory] Migrated session.json \u2192 memory.json");
|
|
210
71
|
} catch {
|
|
211
72
|
}
|
|
212
73
|
}
|
|
213
|
-
if (
|
|
74
|
+
if (fs.existsSync(sessionFile)) {
|
|
214
75
|
try {
|
|
215
|
-
const raw =
|
|
76
|
+
const raw = fs.readFileSync(sessionFile, "utf-8");
|
|
216
77
|
const parsed = JSON.parse(raw);
|
|
217
78
|
this.state = {
|
|
218
79
|
projectRoot: parsed.projectRoot || config.projectRoot,
|
|
@@ -250,23 +111,23 @@ var SessionMemory = class {
|
|
|
250
111
|
console.error(`[SessionMemory] Initialized at ${new Date(config.startTime).toISOString()}`);
|
|
251
112
|
}
|
|
252
113
|
memoriesDir() {
|
|
253
|
-
return
|
|
114
|
+
return path.join(this.state.projectRoot, ".kuma", "memories");
|
|
254
115
|
}
|
|
255
116
|
ensureMemoriesDir() {
|
|
256
117
|
const dir = this.memoriesDir();
|
|
257
|
-
if (!
|
|
258
|
-
|
|
118
|
+
if (!fs.existsSync(dir)) {
|
|
119
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
259
120
|
}
|
|
260
121
|
}
|
|
261
122
|
memoryFilePath(topic) {
|
|
262
|
-
return
|
|
123
|
+
return path.join(this.memoriesDir(), `${topic}.md`);
|
|
263
124
|
}
|
|
264
125
|
getMemoryContent(topic) {
|
|
265
126
|
this.ensureInit();
|
|
266
127
|
this.ensureMemoriesDir();
|
|
267
128
|
const filePath = this.memoryFilePath(topic);
|
|
268
|
-
if (
|
|
269
|
-
return
|
|
129
|
+
if (fs.existsSync(filePath)) {
|
|
130
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
270
131
|
}
|
|
271
132
|
return this.generateMemory(topic);
|
|
272
133
|
}
|
|
@@ -277,7 +138,7 @@ var SessionMemory = class {
|
|
|
277
138
|
}
|
|
278
139
|
writeMemoryFile(topic, content) {
|
|
279
140
|
const filePath = this.memoryFilePath(topic);
|
|
280
|
-
|
|
141
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
281
142
|
}
|
|
282
143
|
generateMemory(topic) {
|
|
283
144
|
switch (topic) {
|
|
@@ -376,9 +237,9 @@ No unresolved issues.`;
|
|
|
376
237
|
save() {
|
|
377
238
|
if (!this.initialized || !this.state) return;
|
|
378
239
|
try {
|
|
379
|
-
const kumaDir =
|
|
380
|
-
if (!
|
|
381
|
-
|
|
240
|
+
const kumaDir = path.join(this.state.projectRoot, ".kuma");
|
|
241
|
+
if (!fs.existsSync(kumaDir)) {
|
|
242
|
+
fs.mkdirSync(kumaDir, { recursive: true });
|
|
382
243
|
}
|
|
383
244
|
const serialized = {
|
|
384
245
|
projectRoot: this.state.projectRoot,
|
|
@@ -392,7 +253,7 @@ No unresolved issues.`;
|
|
|
392
253
|
toolCalls: this.state.toolCalls,
|
|
393
254
|
conventions: this.state.conventions
|
|
394
255
|
};
|
|
395
|
-
|
|
256
|
+
fs.writeFileSync(path.join(kumaDir, "memory.json"), JSON.stringify(serialized, null, 2), "utf-8");
|
|
396
257
|
} catch (err) {
|
|
397
258
|
console.error(`[SessionMemory] Failed to save session: ${err}`);
|
|
398
259
|
}
|
|
@@ -507,6 +368,68 @@ No unresolved issues.`;
|
|
|
507
368
|
// ============================================================
|
|
508
369
|
// KEYWORD SEARCH — Search session memory content
|
|
509
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
|
+
}
|
|
510
433
|
/**
|
|
511
434
|
* Search through tool call history, memory files, search results,
|
|
512
435
|
* errors, and file modifications for a keyword.
|
|
@@ -562,14 +485,14 @@ No unresolved issues.`;
|
|
|
562
485
|
}
|
|
563
486
|
}
|
|
564
487
|
const memoriesDir = this.memoriesDir();
|
|
565
|
-
if (
|
|
488
|
+
if (fs.existsSync(memoriesDir)) {
|
|
566
489
|
try {
|
|
567
|
-
const files =
|
|
490
|
+
const files = fs.readdirSync(memoriesDir);
|
|
568
491
|
for (const file of files) {
|
|
569
492
|
if (!file.endsWith(".md")) continue;
|
|
570
|
-
const filePath =
|
|
493
|
+
const filePath = path.join(memoriesDir, file);
|
|
571
494
|
try {
|
|
572
|
-
const content =
|
|
495
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
573
496
|
const lines = content.split("\n");
|
|
574
497
|
for (let i = 0; i < lines.length; i++) {
|
|
575
498
|
if (lines[i].toLowerCase().includes(q)) {
|
|
@@ -625,11 +548,11 @@ No unresolved issues.`;
|
|
|
625
548
|
unresolvedFailures.push({ task: f.task, error });
|
|
626
549
|
}
|
|
627
550
|
}
|
|
628
|
-
const hasMemories =
|
|
551
|
+
const hasMemories = fs.existsSync(this.memoriesDir());
|
|
629
552
|
let availableMemories = [];
|
|
630
553
|
if (hasMemories) {
|
|
631
554
|
try {
|
|
632
|
-
availableMemories =
|
|
555
|
+
availableMemories = fs.readdirSync(this.memoriesDir()).filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
|
|
633
556
|
} catch {
|
|
634
557
|
}
|
|
635
558
|
}
|
|
@@ -771,7 +694,7 @@ async function handleSmartGrep(params) {
|
|
|
771
694
|
}
|
|
772
695
|
const projectRoot = getProjectRoot();
|
|
773
696
|
try {
|
|
774
|
-
const searchPattern = targetFolder ?
|
|
697
|
+
const searchPattern = targetFolder ? path2.join(targetFolder, "**/*").replace(/\\/g, "/") : "**/*";
|
|
775
698
|
let entries = await fg(searchPattern, {
|
|
776
699
|
cwd: projectRoot,
|
|
777
700
|
ignore: IGNORE_PATTERNS,
|
|
@@ -787,7 +710,7 @@ async function handleSmartGrep(params) {
|
|
|
787
710
|
(e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`
|
|
788
711
|
);
|
|
789
712
|
entries = entries.filter((entry) => {
|
|
790
|
-
const ext =
|
|
713
|
+
const ext = path2.extname(entry).toLowerCase();
|
|
791
714
|
return normalizedExts.includes(ext);
|
|
792
715
|
});
|
|
793
716
|
}
|
|
@@ -803,11 +726,11 @@ async function handleSmartGrep(params) {
|
|
|
803
726
|
if (results.length >= maxResults) break;
|
|
804
727
|
filesScanned++;
|
|
805
728
|
try {
|
|
806
|
-
const fullPath =
|
|
807
|
-
const stat =
|
|
729
|
+
const fullPath = path2.join(projectRoot, entry);
|
|
730
|
+
const stat = fs2.statSync(fullPath);
|
|
808
731
|
if (stat.size > 5e5) continue;
|
|
809
732
|
if (isBinaryFile(fullPath)) continue;
|
|
810
|
-
const content =
|
|
733
|
+
const content = fs2.readFileSync(fullPath, "utf-8");
|
|
811
734
|
const lines = content.split("\n");
|
|
812
735
|
for (let i = 0; i < lines.length; i++) {
|
|
813
736
|
if (results.length >= maxResults) break;
|
|
@@ -876,9 +799,9 @@ ${r.content.split("\n").map((l) => ` ${l}`).join("\n")}`;
|
|
|
876
799
|
function isBinaryFile(filePath) {
|
|
877
800
|
try {
|
|
878
801
|
const buffer = Buffer.alloc(512);
|
|
879
|
-
const fd =
|
|
880
|
-
const bytesRead =
|
|
881
|
-
|
|
802
|
+
const fd = fs2.openSync(filePath, "r");
|
|
803
|
+
const bytesRead = fs2.readSync(fd, buffer, 0, 512, 0);
|
|
804
|
+
fs2.closeSync(fd);
|
|
882
805
|
for (let i = 0; i < bytesRead; i++) {
|
|
883
806
|
if (buffer[i] === 0) return true;
|
|
884
807
|
}
|
|
@@ -889,8 +812,8 @@ function isBinaryFile(filePath) {
|
|
|
889
812
|
}
|
|
890
813
|
|
|
891
814
|
// src/tools/smartFilePicker.ts
|
|
892
|
-
import
|
|
893
|
-
import
|
|
815
|
+
import fs3 from "fs";
|
|
816
|
+
import path3 from "path";
|
|
894
817
|
var MAX_FILE_SIZE = 1e6;
|
|
895
818
|
var CHUNK_THRESHOLD = 300;
|
|
896
819
|
async function handleSmartFilePicker(params) {
|
|
@@ -900,23 +823,23 @@ async function handleSmartFilePicker(params) {
|
|
|
900
823
|
return "Error: " + validation.error.message;
|
|
901
824
|
}
|
|
902
825
|
const resolvedPath = validation.resolvedPath;
|
|
903
|
-
if (!
|
|
826
|
+
if (!fs3.existsSync(resolvedPath)) {
|
|
904
827
|
const projectRoot = getProjectRoot();
|
|
905
828
|
let suggestion;
|
|
906
|
-
if (!
|
|
907
|
-
const cwdPath =
|
|
829
|
+
if (!path3.isAbsolute(filePath)) {
|
|
830
|
+
const cwdPath = path3.resolve(process.cwd(), filePath);
|
|
908
831
|
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.";
|
|
909
832
|
} else {
|
|
910
833
|
suggestion = "File does not exist. Try smart_grep to locate it.";
|
|
911
834
|
}
|
|
912
835
|
return 'Error: File not found: "' + filePath + '".\n' + suggestion;
|
|
913
836
|
}
|
|
914
|
-
const stat =
|
|
837
|
+
const stat = fs3.statSync(resolvedPath);
|
|
915
838
|
if (stat.size > MAX_FILE_SIZE) {
|
|
916
839
|
return "Error: File too large (" + (stat.size / 1024 / 1024).toFixed(1) + "MB). Max 1MB.\nUse smart_grep to find specific content instead.";
|
|
917
840
|
}
|
|
918
841
|
try {
|
|
919
|
-
const content =
|
|
842
|
+
const content = fs3.readFileSync(resolvedPath, "utf-8");
|
|
920
843
|
const lines = content.split("\n");
|
|
921
844
|
const totalLines = lines.length;
|
|
922
845
|
sessionMemory.recordToolCall("smart_file_picker", {
|
|
@@ -1066,8 +989,8 @@ function extractKeyDeclarations(lines) {
|
|
|
1066
989
|
}
|
|
1067
990
|
|
|
1068
991
|
// src/tools/preciseDiffEditor.ts
|
|
1069
|
-
import
|
|
1070
|
-
import
|
|
992
|
+
import fs4 from "fs";
|
|
993
|
+
import path4 from "path";
|
|
1071
994
|
|
|
1072
995
|
// src/utils/errorHandler.ts
|
|
1073
996
|
var CircuitBreakerStore = class {
|
|
@@ -1142,12 +1065,12 @@ async function handlePreciseDiffEditor(params) {
|
|
|
1142
1065
|
|
|
1143
1066
|
Try reading the file first with smart_file_picker to verify current content.`;
|
|
1144
1067
|
}
|
|
1145
|
-
if (!
|
|
1068
|
+
if (!fs4.existsSync(resolvedPath)) {
|
|
1146
1069
|
return `Error: File not found: "${filePath}".
|
|
1147
1070
|
Use batch_file_writer to create a new file.`;
|
|
1148
1071
|
}
|
|
1149
1072
|
try {
|
|
1150
|
-
const originalContent =
|
|
1073
|
+
const originalContent = fs4.readFileSync(resolvedPath, "utf-8");
|
|
1151
1074
|
let currentContent = originalContent;
|
|
1152
1075
|
const results = [];
|
|
1153
1076
|
for (let i = 0; i < edits.length; i++) {
|
|
@@ -1155,7 +1078,7 @@ Use batch_file_writer to create a new file.`;
|
|
|
1155
1078
|
const result = applyEdit(currentContent, edit, resolvedPath, i, dryRun);
|
|
1156
1079
|
if (result.success) {
|
|
1157
1080
|
if (!dryRun) {
|
|
1158
|
-
|
|
1081
|
+
fs4.writeFileSync(resolvedPath, result.details, "utf-8");
|
|
1159
1082
|
sessionMemory.recordToolCall("precise_diff_editor", {
|
|
1160
1083
|
filePath,
|
|
1161
1084
|
editIndex: i,
|
|
@@ -1345,9 +1268,9 @@ function findNearestLine(content, search) {
|
|
|
1345
1268
|
function createBackup(filePath) {
|
|
1346
1269
|
ensureBackupDir();
|
|
1347
1270
|
const backupPath = getBackupPath(filePath);
|
|
1348
|
-
const backupDir =
|
|
1349
|
-
|
|
1350
|
-
|
|
1271
|
+
const backupDir = path4.dirname(backupPath);
|
|
1272
|
+
fs4.mkdirSync(backupDir, { recursive: true });
|
|
1273
|
+
fs4.copyFileSync(filePath, backupPath);
|
|
1351
1274
|
return backupPath;
|
|
1352
1275
|
}
|
|
1353
1276
|
function formatDryRunResult(results, filePath, originalContent) {
|
|
@@ -1433,7 +1356,7 @@ function formatDiffResult(results, filePath) {
|
|
|
1433
1356
|
if (r.success) {
|
|
1434
1357
|
lines.push(`[${i + 1}] \u2705 Matched: ${r.matched}x, Replaced: ${r.replaced}x`);
|
|
1435
1358
|
if (r.backupPath) {
|
|
1436
|
-
const relativePath =
|
|
1359
|
+
const relativePath = path4.relative(getProjectRoot(), r.backupPath);
|
|
1437
1360
|
lines.push(` Backup: ${relativePath}`);
|
|
1438
1361
|
}
|
|
1439
1362
|
} else {
|
|
@@ -1468,15 +1391,15 @@ async function handleRollbackEdit(params) {
|
|
|
1468
1391
|
}
|
|
1469
1392
|
const resolvedPath = validation.resolvedPath;
|
|
1470
1393
|
const root = getProjectRoot();
|
|
1471
|
-
const relativePath =
|
|
1472
|
-
const backupRoot =
|
|
1473
|
-
if (!
|
|
1394
|
+
const relativePath = path4.relative(root, resolvedPath);
|
|
1395
|
+
const backupRoot = path4.join(root, ".agent-backups");
|
|
1396
|
+
if (!fs4.existsSync(backupRoot)) {
|
|
1474
1397
|
return `Error: No backup folder (.agent-backups) found in project root.`;
|
|
1475
1398
|
}
|
|
1476
1399
|
try {
|
|
1477
|
-
const dirs =
|
|
1478
|
-
const fullPath =
|
|
1479
|
-
return
|
|
1400
|
+
const dirs = fs4.readdirSync(backupRoot).filter((name) => {
|
|
1401
|
+
const fullPath = path4.join(backupRoot, name);
|
|
1402
|
+
return fs4.statSync(fullPath).isDirectory() && /^\d+$/.test(name);
|
|
1480
1403
|
});
|
|
1481
1404
|
if (dirs.length === 0) {
|
|
1482
1405
|
return `Error: No valid backup found in .agent-backups folder.`;
|
|
@@ -1484,8 +1407,8 @@ async function handleRollbackEdit(params) {
|
|
|
1484
1407
|
dirs.sort((a, b) => Number(b) - Number(a));
|
|
1485
1408
|
const backupVersions = [];
|
|
1486
1409
|
for (const dir of dirs) {
|
|
1487
|
-
const potentialBackupPath =
|
|
1488
|
-
if (
|
|
1410
|
+
const potentialBackupPath = path4.join(backupRoot, dir, relativePath);
|
|
1411
|
+
if (fs4.existsSync(potentialBackupPath)) {
|
|
1489
1412
|
backupVersions.push({ dir, backupPath: potentialBackupPath });
|
|
1490
1413
|
}
|
|
1491
1414
|
}
|
|
@@ -1512,14 +1435,14 @@ async function handleRollbackEdit(params) {
|
|
|
1512
1435
|
selectedIndex = version - 1;
|
|
1513
1436
|
}
|
|
1514
1437
|
const selected = backupVersions[selectedIndex];
|
|
1515
|
-
|
|
1438
|
+
fs4.copyFileSync(selected.backupPath, resolvedPath);
|
|
1516
1439
|
sessionMemory.recordToolCall("rollback_last_edit", {
|
|
1517
1440
|
filePath,
|
|
1518
1441
|
backupTimestamp: selected.dir,
|
|
1519
1442
|
version: version ?? 1,
|
|
1520
1443
|
success: true
|
|
1521
1444
|
});
|
|
1522
|
-
const relBackupPath =
|
|
1445
|
+
const relBackupPath = path4.relative(root, selected.backupPath);
|
|
1523
1446
|
return `\u2705 Rollback Successful!
|
|
1524
1447
|
File "${filePath}" restored from backup: "${relBackupPath}".`;
|
|
1525
1448
|
} catch (err) {
|
|
@@ -1528,8 +1451,8 @@ File "${filePath}" restored from backup: "${relBackupPath}".`;
|
|
|
1528
1451
|
}
|
|
1529
1452
|
|
|
1530
1453
|
// src/tools/batchFileWriter.ts
|
|
1531
|
-
import
|
|
1532
|
-
import
|
|
1454
|
+
import fs5 from "fs";
|
|
1455
|
+
import path5 from "path";
|
|
1533
1456
|
async function handleBatchFileWriter(params) {
|
|
1534
1457
|
const { files } = params;
|
|
1535
1458
|
if (files.length > 15) {
|
|
@@ -1556,22 +1479,22 @@ async function handleBatchFileWriter(params) {
|
|
|
1556
1479
|
results.push({
|
|
1557
1480
|
filePath: file.filePath,
|
|
1558
1481
|
success: false,
|
|
1559
|
-
error: `File extension not allowed: "${
|
|
1482
|
+
error: `File extension not allowed: "${path5.extname(file.filePath)}". Allowed extensions: .ts, .js, .tsx, .jsx, .json, .md, .css, .html, .yml, .yaml, .toml, .sh, .env`
|
|
1560
1483
|
});
|
|
1561
1484
|
continue;
|
|
1562
1485
|
}
|
|
1563
|
-
if (
|
|
1486
|
+
if (fs5.existsSync(resolvedPath)) {
|
|
1564
1487
|
ensureBackupDir();
|
|
1565
1488
|
const backupPath = getBackupPath(file.filePath);
|
|
1566
|
-
const backupDir =
|
|
1567
|
-
|
|
1568
|
-
|
|
1489
|
+
const backupDir = path5.dirname(backupPath);
|
|
1490
|
+
fs5.mkdirSync(backupDir, { recursive: true });
|
|
1491
|
+
fs5.copyFileSync(resolvedPath, backupPath);
|
|
1569
1492
|
}
|
|
1570
|
-
const dir =
|
|
1571
|
-
if (!
|
|
1572
|
-
|
|
1493
|
+
const dir = path5.dirname(resolvedPath);
|
|
1494
|
+
if (!fs5.existsSync(dir)) {
|
|
1495
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1573
1496
|
}
|
|
1574
|
-
|
|
1497
|
+
fs5.writeFileSync(resolvedPath, file.content, "utf-8");
|
|
1575
1498
|
sessionMemory.recordToolCall("batch_file_writer", {
|
|
1576
1499
|
filePath: file.filePath,
|
|
1577
1500
|
instructions: file.instructions,
|
|
@@ -1614,12 +1537,12 @@ function formatBatchResult(results, totalRequested) {
|
|
|
1614
1537
|
}
|
|
1615
1538
|
|
|
1616
1539
|
// src/tools/safeTerminalExec.ts
|
|
1617
|
-
import fs8 from "fs";
|
|
1618
|
-
import path8 from "path";
|
|
1619
|
-
|
|
1620
|
-
// src/utils/conventionsDetector.ts
|
|
1621
1540
|
import fs7 from "fs";
|
|
1622
1541
|
import path7 from "path";
|
|
1542
|
+
|
|
1543
|
+
// src/utils/conventionsDetector.ts
|
|
1544
|
+
import fs6 from "fs";
|
|
1545
|
+
import path6 from "path";
|
|
1623
1546
|
var cachedConventions = null;
|
|
1624
1547
|
async function detectConventions(forceRescan = false) {
|
|
1625
1548
|
if (cachedConventions && !forceRescan) {
|
|
@@ -1645,7 +1568,7 @@ async function detectConventions(forceRescan = false) {
|
|
|
1645
1568
|
return conventions;
|
|
1646
1569
|
}
|
|
1647
1570
|
function detectFramework(root) {
|
|
1648
|
-
const pkg = readJsonSafe(
|
|
1571
|
+
const pkg = readJsonSafe(path6.join(root, "package.json"));
|
|
1649
1572
|
if (pkg) {
|
|
1650
1573
|
const pkgDeps = pkg.dependencies ?? {};
|
|
1651
1574
|
const pkgDevDeps = pkg.devDependencies ?? {};
|
|
@@ -1690,11 +1613,11 @@ function detectFramework(root) {
|
|
|
1690
1613
|
function scanSubProjectFrameworks(root) {
|
|
1691
1614
|
const frameworks = [];
|
|
1692
1615
|
try {
|
|
1693
|
-
const entries =
|
|
1616
|
+
const entries = fs6.readdirSync(root, { withFileTypes: true });
|
|
1694
1617
|
for (const entry of entries) {
|
|
1695
1618
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
1696
|
-
const subDir =
|
|
1697
|
-
const subPkg = readJsonSafe(
|
|
1619
|
+
const subDir = path6.join(root, entry.name);
|
|
1620
|
+
const subPkg = readJsonSafe(path6.join(subDir, "package.json"));
|
|
1698
1621
|
if (!subPkg) continue;
|
|
1699
1622
|
const subDeps = { ...subPkg.dependencies ?? {}, ...subPkg.devDependencies ?? {} };
|
|
1700
1623
|
if (subDeps.next) frameworks.push("Next.js");
|
|
@@ -1713,7 +1636,7 @@ function scanSubProjectFrameworks(root) {
|
|
|
1713
1636
|
return frameworks;
|
|
1714
1637
|
}
|
|
1715
1638
|
function detectProjectType(root) {
|
|
1716
|
-
const pkg = readJsonSafe(
|
|
1639
|
+
const pkg = readJsonSafe(path6.join(root, "package.json"));
|
|
1717
1640
|
if (pkg) {
|
|
1718
1641
|
const pkgDeps = pkg.dependencies ?? {};
|
|
1719
1642
|
const pkgDevDeps = pkg.devDependencies ?? {};
|
|
@@ -1737,8 +1660,8 @@ function detectProjectType(root) {
|
|
|
1737
1660
|
}
|
|
1738
1661
|
function detectWorkspaces(root) {
|
|
1739
1662
|
const results = [];
|
|
1740
|
-
const pkg = readJsonSafe(
|
|
1741
|
-
const pnpmWorkspace = readYamlLite(
|
|
1663
|
+
const pkg = readJsonSafe(path6.join(root, "package.json"));
|
|
1664
|
+
const pnpmWorkspace = readYamlLite(path6.join(root, "pnpm-workspace.yaml"));
|
|
1742
1665
|
const patterns = /* @__PURE__ */ new Set();
|
|
1743
1666
|
const pkgWorkspaces = pkg?.workspaces;
|
|
1744
1667
|
if (Array.isArray(pkgWorkspaces)) {
|
|
@@ -1758,22 +1681,22 @@ function detectWorkspaces(root) {
|
|
|
1758
1681
|
for (const pattern of patterns) {
|
|
1759
1682
|
const match = pattern.match(/^([^*]+)\/\*$/);
|
|
1760
1683
|
if (!match) continue;
|
|
1761
|
-
const dir =
|
|
1762
|
-
if (!
|
|
1684
|
+
const dir = path6.join(root, match[1]);
|
|
1685
|
+
if (!fs6.existsSync(dir)) continue;
|
|
1763
1686
|
let entries = [];
|
|
1764
1687
|
try {
|
|
1765
|
-
entries =
|
|
1688
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
1766
1689
|
} catch {
|
|
1767
1690
|
continue;
|
|
1768
1691
|
}
|
|
1769
1692
|
for (const entry of entries) {
|
|
1770
1693
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
1771
|
-
const pkgPath =
|
|
1694
|
+
const pkgPath = path6.join(dir, entry.name, "package.json");
|
|
1772
1695
|
const subPkg = readJsonSafe(pkgPath);
|
|
1773
1696
|
if (!subPkg) continue;
|
|
1774
|
-
const workspacePath =
|
|
1697
|
+
const workspacePath = path6.join(dir, entry.name);
|
|
1775
1698
|
results.push({
|
|
1776
|
-
path:
|
|
1699
|
+
path: path6.relative(root, workspacePath),
|
|
1777
1700
|
name: subPkg.name ?? entry.name,
|
|
1778
1701
|
framework: detectFramework(workspacePath),
|
|
1779
1702
|
packageManager: detectPackageManagerForDir(workspacePath)
|
|
@@ -1789,8 +1712,8 @@ function detectWorkspaces(root) {
|
|
|
1789
1712
|
}
|
|
1790
1713
|
function readYamlLite(filePath) {
|
|
1791
1714
|
try {
|
|
1792
|
-
if (!
|
|
1793
|
-
const text =
|
|
1715
|
+
if (!fs6.existsSync(filePath)) return null;
|
|
1716
|
+
const text = fs6.readFileSync(filePath, "utf-8");
|
|
1794
1717
|
const packages = [];
|
|
1795
1718
|
let inPackages = false;
|
|
1796
1719
|
for (const rawLine of text.split("\n")) {
|
|
@@ -1811,7 +1734,7 @@ function readYamlLite(filePath) {
|
|
|
1811
1734
|
}
|
|
1812
1735
|
}
|
|
1813
1736
|
function detectTestRunner(root) {
|
|
1814
|
-
const pkg = readJsonSafe(
|
|
1737
|
+
const pkg = readJsonSafe(path6.join(root, "package.json"));
|
|
1815
1738
|
if (!pkg) return "unknown";
|
|
1816
1739
|
const pkgDeps2 = pkg.dependencies ?? {};
|
|
1817
1740
|
const pkgDevDeps2 = pkg.devDependencies ?? {};
|
|
@@ -1832,7 +1755,7 @@ function detectTestRunner(root) {
|
|
|
1832
1755
|
return "unknown";
|
|
1833
1756
|
}
|
|
1834
1757
|
function detectStyling(root) {
|
|
1835
|
-
const pkg = readJsonSafe(
|
|
1758
|
+
const pkg = readJsonSafe(path6.join(root, "package.json"));
|
|
1836
1759
|
if (!pkg) return "unknown";
|
|
1837
1760
|
const pkgDeps3 = pkg.dependencies ?? {};
|
|
1838
1761
|
const pkgDevDeps3 = pkg.devDependencies ?? {};
|
|
@@ -1845,22 +1768,22 @@ function detectStyling(root) {
|
|
|
1845
1768
|
if (deps.less) return "Less";
|
|
1846
1769
|
if (deps["@vanilla-extract/css"]) return "Vanilla Extract";
|
|
1847
1770
|
if (deps.cssmodules || hasCSSModules(root)) return "CSS Modules";
|
|
1848
|
-
const srcDir =
|
|
1849
|
-
if (
|
|
1771
|
+
const srcDir = path6.join(root, "src");
|
|
1772
|
+
if (fs6.existsSync(srcDir)) {
|
|
1850
1773
|
const cssFiles = findFiles(srcDir, [".css", ".scss", ".less"]);
|
|
1851
1774
|
if (cssFiles.length > 0) return "Plain CSS/SCSS";
|
|
1852
1775
|
}
|
|
1853
1776
|
return "unknown";
|
|
1854
1777
|
}
|
|
1855
1778
|
function detectImportAlias(root) {
|
|
1856
|
-
const tsconfig = readJsonSafe(
|
|
1779
|
+
const tsconfig = readJsonSafe(path6.join(root, "tsconfig.json"));
|
|
1857
1780
|
const tsconfigPaths = tsconfig?.compilerOptions;
|
|
1858
1781
|
if (tsconfigPaths?.paths) {
|
|
1859
1782
|
const paths = tsconfigPaths.paths;
|
|
1860
1783
|
const alias = Object.keys(paths)[0];
|
|
1861
1784
|
if (alias) return alias.replace("/*", "");
|
|
1862
1785
|
}
|
|
1863
|
-
const jsconfig = readJsonSafe(
|
|
1786
|
+
const jsconfig = readJsonSafe(path6.join(root, "jsconfig.json"));
|
|
1864
1787
|
const jsconfigPaths = jsconfig?.compilerOptions;
|
|
1865
1788
|
if (jsconfigPaths?.paths) {
|
|
1866
1789
|
const paths = jsconfigPaths.paths;
|
|
@@ -1871,15 +1794,15 @@ function detectImportAlias(root) {
|
|
|
1871
1794
|
}
|
|
1872
1795
|
function detectLintRules(root) {
|
|
1873
1796
|
const rules = [];
|
|
1874
|
-
if (
|
|
1875
|
-
if (
|
|
1876
|
-
if (
|
|
1877
|
-
if (
|
|
1878
|
-
if (
|
|
1879
|
-
if (
|
|
1880
|
-
if (
|
|
1881
|
-
if (
|
|
1882
|
-
if (
|
|
1797
|
+
if (fs6.existsSync(path6.join(root, ".eslintrc"))) rules.push("ESLint (.eslintrc)");
|
|
1798
|
+
if (fs6.existsSync(path6.join(root, ".eslintrc.js"))) rules.push("ESLint (.eslintrc.js)");
|
|
1799
|
+
if (fs6.existsSync(path6.join(root, ".eslintrc.json"))) rules.push("ESLint (.eslintrc.json)");
|
|
1800
|
+
if (fs6.existsSync(path6.join(root, "eslint.config.js"))) rules.push("ESLint (flat config)");
|
|
1801
|
+
if (fs6.existsSync(path6.join(root, ".prettierrc"))) rules.push("Prettier");
|
|
1802
|
+
if (fs6.existsSync(path6.join(root, ".prettierrc.json"))) rules.push("Prettier");
|
|
1803
|
+
if (fs6.existsSync(path6.join(root, ".prettierrc.js"))) rules.push("Prettier");
|
|
1804
|
+
if (fs6.existsSync(path6.join(root, ".stylelintrc"))) rules.push("StyleLint");
|
|
1805
|
+
if (fs6.existsSync(path6.join(root, ".stylelintrc.json"))) rules.push("StyleLint");
|
|
1883
1806
|
return rules;
|
|
1884
1807
|
}
|
|
1885
1808
|
function detectPackageManager() {
|
|
@@ -1888,26 +1811,26 @@ function detectPackageManager() {
|
|
|
1888
1811
|
}
|
|
1889
1812
|
function detectPackageManagerForDir(dir) {
|
|
1890
1813
|
const root = getProjectRoot();
|
|
1891
|
-
let currentDir =
|
|
1814
|
+
let currentDir = path6.resolve(dir);
|
|
1892
1815
|
while (currentDir.startsWith(root)) {
|
|
1893
|
-
if (
|
|
1894
|
-
if (
|
|
1895
|
-
if (
|
|
1896
|
-
if (
|
|
1816
|
+
if (fs6.existsSync(path6.join(currentDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
1817
|
+
if (fs6.existsSync(path6.join(currentDir, "yarn.lock"))) return "yarn";
|
|
1818
|
+
if (fs6.existsSync(path6.join(currentDir, "package-lock.json"))) return "npm";
|
|
1819
|
+
if (fs6.existsSync(path6.join(currentDir, "bun.lockb"))) return "bun";
|
|
1897
1820
|
if (currentDir === root) break;
|
|
1898
|
-
currentDir =
|
|
1821
|
+
currentDir = path6.dirname(currentDir);
|
|
1899
1822
|
}
|
|
1900
1823
|
return "npm";
|
|
1901
1824
|
}
|
|
1902
1825
|
function detectModuleSystem(root) {
|
|
1903
|
-
const pkg = readJsonSafe(
|
|
1826
|
+
const pkg = readJsonSafe(path6.join(root, "package.json"));
|
|
1904
1827
|
if (pkg?.type === "module") return "esm";
|
|
1905
|
-
const srcDir =
|
|
1906
|
-
if (
|
|
1828
|
+
const srcDir = path6.join(root, "src");
|
|
1829
|
+
if (fs6.existsSync(srcDir)) {
|
|
1907
1830
|
const tsFiles = findFiles(srcDir, [".ts", ".tsx", ".js", ".jsx", ".mjs"]);
|
|
1908
1831
|
for (const file of tsFiles.slice(0, 20)) {
|
|
1909
1832
|
try {
|
|
1910
|
-
const content =
|
|
1833
|
+
const content = fs6.readFileSync(file, "utf-8");
|
|
1911
1834
|
if (content.includes("import ") || content.includes("export ")) return "esm";
|
|
1912
1835
|
} catch {
|
|
1913
1836
|
continue;
|
|
@@ -1917,9 +1840,9 @@ function detectModuleSystem(root) {
|
|
|
1917
1840
|
return "cjs";
|
|
1918
1841
|
}
|
|
1919
1842
|
function detectLanguage(root) {
|
|
1920
|
-
if (
|
|
1921
|
-
const srcDir =
|
|
1922
|
-
if (
|
|
1843
|
+
if (fs6.existsSync(path6.join(root, "tsconfig.json"))) return "typescript";
|
|
1844
|
+
const srcDir = path6.join(root, "src");
|
|
1845
|
+
if (fs6.existsSync(srcDir)) {
|
|
1923
1846
|
const tsFiles = findFiles(srcDir, [".ts", ".tsx"]);
|
|
1924
1847
|
if (tsFiles.length > 0) return "typescript";
|
|
1925
1848
|
}
|
|
@@ -1927,7 +1850,7 @@ function detectLanguage(root) {
|
|
|
1927
1850
|
}
|
|
1928
1851
|
function detectFeatures(root) {
|
|
1929
1852
|
const features = [];
|
|
1930
|
-
const pkg = readJsonSafe(
|
|
1853
|
+
const pkg = readJsonSafe(path6.join(root, "package.json"));
|
|
1931
1854
|
if (!pkg) return features;
|
|
1932
1855
|
const pkgDeps4 = pkg.dependencies ?? {};
|
|
1933
1856
|
const pkgDevDeps4 = pkg.devDependencies ?? {};
|
|
@@ -1951,25 +1874,25 @@ function detectFeatures(root) {
|
|
|
1951
1874
|
}
|
|
1952
1875
|
function readJsonSafe(filePath) {
|
|
1953
1876
|
try {
|
|
1954
|
-
if (
|
|
1955
|
-
return JSON.parse(
|
|
1877
|
+
if (fs6.existsSync(filePath)) {
|
|
1878
|
+
return JSON.parse(fs6.readFileSync(filePath, "utf-8"));
|
|
1956
1879
|
}
|
|
1957
1880
|
} catch {
|
|
1958
1881
|
}
|
|
1959
1882
|
return null;
|
|
1960
1883
|
}
|
|
1961
1884
|
function hasCSSModules(root) {
|
|
1962
|
-
const srcDir =
|
|
1963
|
-
if (!
|
|
1885
|
+
const srcDir = path6.join(root, "src");
|
|
1886
|
+
if (!fs6.existsSync(srcDir)) return false;
|
|
1964
1887
|
const files = findFiles(srcDir, [".module.css", ".module.scss", ".module.less"]);
|
|
1965
1888
|
return files.length > 0;
|
|
1966
1889
|
}
|
|
1967
1890
|
function findFiles(dir, extensions) {
|
|
1968
1891
|
const results = [];
|
|
1969
1892
|
try {
|
|
1970
|
-
const entries =
|
|
1893
|
+
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
1971
1894
|
for (const entry of entries) {
|
|
1972
|
-
const fullPath =
|
|
1895
|
+
const fullPath = path6.join(dir, entry.name);
|
|
1973
1896
|
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
1974
1897
|
results.push(...findFiles(fullPath, extensions));
|
|
1975
1898
|
} else if (entry.isFile()) {
|
|
@@ -2160,7 +2083,7 @@ async function handleSafeTerminalExec(params) {
|
|
|
2160
2083
|
const workspaces = conventions?.workspaces;
|
|
2161
2084
|
const matched = workspaces?.find((w) => w.name === workspace || w.path === workspace);
|
|
2162
2085
|
if (matched) {
|
|
2163
|
-
workingDir =
|
|
2086
|
+
workingDir = path7.resolve(projectRoot, matched.path);
|
|
2164
2087
|
resolvedFrom = `workspace "${matched.name}" \u2192 ${matched.path}`;
|
|
2165
2088
|
} else {
|
|
2166
2089
|
return `\u26A0\uFE0F Workspace "${workspace}" not found. Run project_conventions first to detect workspaces, or use 'cwd' parameter with a direct path.
|
|
@@ -2168,13 +2091,13 @@ async function handleSafeTerminalExec(params) {
|
|
|
2168
2091
|
Available workspaces: ${workspaces?.map((w) => `"${w.name}" (${w.path})`).join(", ") || "none detected"}`;
|
|
2169
2092
|
}
|
|
2170
2093
|
} else if (inputCwd) {
|
|
2171
|
-
const resolved =
|
|
2172
|
-
const normalizedResolved =
|
|
2173
|
-
const normalizedRoot =
|
|
2094
|
+
const resolved = path7.resolve(projectRoot, inputCwd);
|
|
2095
|
+
const normalizedResolved = path7.normalize(resolved).toLowerCase();
|
|
2096
|
+
const normalizedRoot = path7.normalize(projectRoot).toLowerCase();
|
|
2174
2097
|
if (!normalizedResolved.startsWith(normalizedRoot)) {
|
|
2175
2098
|
return `\u{1F6AB} BLOCKED: Path "${inputCwd}" resolves outside project root "${projectRoot}".`;
|
|
2176
2099
|
}
|
|
2177
|
-
if (!
|
|
2100
|
+
if (!fs7.existsSync(resolved)) {
|
|
2178
2101
|
return `\u26A0\uFE0F Directory "${inputCwd}" does not exist at "${resolved}".`;
|
|
2179
2102
|
}
|
|
2180
2103
|
workingDir = resolved;
|
|
@@ -2260,8 +2183,8 @@ function formatTimeoutResult(command, timeout) {
|
|
|
2260
2183
|
}
|
|
2261
2184
|
|
|
2262
2185
|
// src/agents/codeReviewer.ts
|
|
2263
|
-
import
|
|
2264
|
-
import
|
|
2186
|
+
import fs8 from "fs";
|
|
2187
|
+
import path8 from "path";
|
|
2265
2188
|
import { execSync } from "child_process";
|
|
2266
2189
|
var CODE_EXTENSIONS = [
|
|
2267
2190
|
".ts",
|
|
@@ -2275,7 +2198,19 @@ var CODE_EXTENSIONS = [
|
|
|
2275
2198
|
".rs",
|
|
2276
2199
|
".php",
|
|
2277
2200
|
".cs",
|
|
2278
|
-
".java"
|
|
2201
|
+
".java",
|
|
2202
|
+
".json",
|
|
2203
|
+
".jsonc",
|
|
2204
|
+
".yaml",
|
|
2205
|
+
".yml",
|
|
2206
|
+
".toml"
|
|
2207
|
+
];
|
|
2208
|
+
var CONFIG_EXTENSIONS = [
|
|
2209
|
+
".json",
|
|
2210
|
+
".jsonc",
|
|
2211
|
+
".yaml",
|
|
2212
|
+
".yml",
|
|
2213
|
+
".toml"
|
|
2279
2214
|
];
|
|
2280
2215
|
function getGitChangedFiles() {
|
|
2281
2216
|
try {
|
|
@@ -2293,7 +2228,7 @@ function getGitChangedFiles() {
|
|
|
2293
2228
|
if (filePath.startsWith('"') && filePath.endsWith('"')) {
|
|
2294
2229
|
filePath = filePath.substring(1, filePath.length - 1);
|
|
2295
2230
|
}
|
|
2296
|
-
const ext =
|
|
2231
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
2297
2232
|
if (CODE_EXTENSIONS.includes(ext)) files.push(filePath);
|
|
2298
2233
|
}
|
|
2299
2234
|
return files.slice(0, 10);
|
|
@@ -2303,7 +2238,7 @@ function getGitChangedFiles() {
|
|
|
2303
2238
|
}
|
|
2304
2239
|
}
|
|
2305
2240
|
async function handleCodeReviewer(params) {
|
|
2306
|
-
const { files: inputFiles, focus = "correctness", customCriteria, format = "text" } = params;
|
|
2241
|
+
const { files: inputFiles, focus = "correctness", customCriteria, format = "text", convention } = params;
|
|
2307
2242
|
let files = inputFiles ?? [];
|
|
2308
2243
|
let isAutoDetected = false;
|
|
2309
2244
|
if (files.length === 0) {
|
|
@@ -2330,7 +2265,7 @@ Pass an explicit 'files' array to review specific files.`;
|
|
|
2330
2265
|
continue;
|
|
2331
2266
|
}
|
|
2332
2267
|
const resolvedPath = validation.resolvedPath;
|
|
2333
|
-
if (!
|
|
2268
|
+
if (!fs8.existsSync(resolvedPath)) {
|
|
2334
2269
|
allIssues.push({
|
|
2335
2270
|
file: filePath,
|
|
2336
2271
|
line: 0,
|
|
@@ -2342,7 +2277,7 @@ Pass an explicit 'files' array to review specific files.`;
|
|
|
2342
2277
|
continue;
|
|
2343
2278
|
}
|
|
2344
2279
|
try {
|
|
2345
|
-
const content =
|
|
2280
|
+
const content = fs8.readFileSync(resolvedPath, "utf-8");
|
|
2346
2281
|
filesReviewed++;
|
|
2347
2282
|
checkGeneral(filePath, content, allIssues);
|
|
2348
2283
|
switch (focus) {
|
|
@@ -2362,6 +2297,9 @@ Pass an explicit 'files' array to review specific files.`;
|
|
|
2362
2297
|
checkOverEngineering(filePath, content, allIssues);
|
|
2363
2298
|
break;
|
|
2364
2299
|
}
|
|
2300
|
+
if (convention === "matcha") {
|
|
2301
|
+
checkMatchaConventions(filePath, content, allIssues);
|
|
2302
|
+
}
|
|
2365
2303
|
} catch (err) {
|
|
2366
2304
|
allIssues.push({
|
|
2367
2305
|
file: filePath,
|
|
@@ -2389,6 +2327,7 @@ Pass an explicit 'files' array to review specific files.`;
|
|
|
2389
2327
|
filesReviewed,
|
|
2390
2328
|
filesRequested: files.length,
|
|
2391
2329
|
focus,
|
|
2330
|
+
convention,
|
|
2392
2331
|
autoDetected: isAutoDetected,
|
|
2393
2332
|
...customCriteria ? { customCriteria } : {}
|
|
2394
2333
|
};
|
|
@@ -2436,11 +2375,12 @@ function isTestFile(filePath) {
|
|
|
2436
2375
|
return /\.(test|spec)\.[a-z]+$/i.test(filePath) || /__tests__/.test(filePath);
|
|
2437
2376
|
}
|
|
2438
2377
|
function isTsLike(filePath) {
|
|
2439
|
-
const ext =
|
|
2378
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
2440
2379
|
return ext === ".ts" || ext === ".tsx";
|
|
2441
2380
|
}
|
|
2442
2381
|
function checkGeneral(filePath, content, issues) {
|
|
2443
2382
|
const lines = content.split("\n");
|
|
2383
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
2444
2384
|
if (content.length > 0 && !content.endsWith("\n")) {
|
|
2445
2385
|
issues.push({
|
|
2446
2386
|
file: filePath,
|
|
@@ -2461,6 +2401,9 @@ function checkGeneral(filePath, content, issues) {
|
|
|
2461
2401
|
suggestion: "Extract cohesive sections into separate modules"
|
|
2462
2402
|
});
|
|
2463
2403
|
}
|
|
2404
|
+
if (CONFIG_EXTENSIONS.includes(ext)) {
|
|
2405
|
+
checkConfigFile(filePath, content, issues);
|
|
2406
|
+
}
|
|
2464
2407
|
}
|
|
2465
2408
|
function checkCorrectness(filePath, content, issues) {
|
|
2466
2409
|
const lines = content.split("\n");
|
|
@@ -2623,6 +2566,282 @@ function checkCorrectness(filePath, content, issues) {
|
|
|
2623
2566
|
});
|
|
2624
2567
|
}
|
|
2625
2568
|
}
|
|
2569
|
+
function stripJsonComments(text) {
|
|
2570
|
+
let result = text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
2571
|
+
const lines = result.split("\n");
|
|
2572
|
+
const cleaned = lines.map((line) => {
|
|
2573
|
+
const commentIdx = line.indexOf("//");
|
|
2574
|
+
if (commentIdx === -1) return line;
|
|
2575
|
+
const before = line.substring(0, commentIdx);
|
|
2576
|
+
const quoteCount = (before.match(/"/g) || []).length;
|
|
2577
|
+
if (quoteCount % 2 === 0) {
|
|
2578
|
+
return before;
|
|
2579
|
+
}
|
|
2580
|
+
return line;
|
|
2581
|
+
});
|
|
2582
|
+
return cleaned.join("\n");
|
|
2583
|
+
}
|
|
2584
|
+
function checkConfigFile(filePath, content, issues) {
|
|
2585
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
2586
|
+
const lines = content.split("\n");
|
|
2587
|
+
if (ext === ".json" || ext === ".jsonc") {
|
|
2588
|
+
checkJson(filePath, content, lines, issues);
|
|
2589
|
+
} else if (ext === ".yaml" || ext === ".yml") {
|
|
2590
|
+
checkYaml(filePath, content, lines, issues);
|
|
2591
|
+
} else if (ext === ".toml") {
|
|
2592
|
+
checkToml(filePath, content, lines, issues);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
function checkJson(filePath, content, lines, issues) {
|
|
2596
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
2597
|
+
const isJsonc = ext === ".jsonc";
|
|
2598
|
+
const contentToParse = isJsonc ? stripJsonComments(content) : content;
|
|
2599
|
+
try {
|
|
2600
|
+
JSON.parse(contentToParse);
|
|
2601
|
+
} catch (err) {
|
|
2602
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2603
|
+
if (isJsonc && /\/\/|\/\*/.test(content)) {
|
|
2604
|
+
issues.push({
|
|
2605
|
+
file: filePath,
|
|
2606
|
+
line: 1,
|
|
2607
|
+
severity: "info",
|
|
2608
|
+
rule: "jsonc/comments-stripped",
|
|
2609
|
+
message: "JSON with Comments \u2014 comments were stripped for validation. Check the remaining syntax.",
|
|
2610
|
+
suggestion: "If issues remain, check quotes, commas, braces after comment removal"
|
|
2611
|
+
});
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
const posMatch = msg.match(/position\s+(\d+)/i);
|
|
2615
|
+
const lineMatch = msg.match(/line\s+(\d+)/i);
|
|
2616
|
+
let errorLine = 1;
|
|
2617
|
+
if (lineMatch) {
|
|
2618
|
+
errorLine = parseInt(lineMatch[1], 10);
|
|
2619
|
+
} else if (posMatch) {
|
|
2620
|
+
const pos = parseInt(posMatch[1], 10);
|
|
2621
|
+
let charCount = 0;
|
|
2622
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2623
|
+
charCount += lines[i].length + 1;
|
|
2624
|
+
if (charCount >= pos) {
|
|
2625
|
+
errorLine = i + 1;
|
|
2626
|
+
break;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
issues.push({
|
|
2631
|
+
file: filePath,
|
|
2632
|
+
line: errorLine,
|
|
2633
|
+
severity: "error",
|
|
2634
|
+
rule: "json/parse-error",
|
|
2635
|
+
message: `Invalid JSON: ${msg.substring(0, 120)}`,
|
|
2636
|
+
suggestion: "Fix the syntax error \u2014 check quotes, commas, braces, and brackets"
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2640
|
+
const trimmed = lines[i].trim();
|
|
2641
|
+
if (!/,\s*$/.test(trimmed)) continue;
|
|
2642
|
+
let nextNonEmpty = "";
|
|
2643
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
2644
|
+
const t = lines[j].trim();
|
|
2645
|
+
if (t && !t.startsWith("//") && !t.startsWith("*") && !t.startsWith("#")) {
|
|
2646
|
+
nextNonEmpty = t;
|
|
2647
|
+
break;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
if (nextNonEmpty === "]" || nextNonEmpty === "}" || nextNonEmpty === "]," || nextNonEmpty === "},") {
|
|
2651
|
+
issues.push({
|
|
2652
|
+
file: filePath,
|
|
2653
|
+
line: i + 1,
|
|
2654
|
+
severity: "error",
|
|
2655
|
+
rule: "json/trailing-comma",
|
|
2656
|
+
message: "Trailing comma before closing bracket/brace",
|
|
2657
|
+
suggestion: "Remove the trailing comma after the last element"
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
try {
|
|
2662
|
+
const parsed = JSON.parse(content);
|
|
2663
|
+
findDuplicateKeys(parsed, "", filePath, issues, lines);
|
|
2664
|
+
} catch {
|
|
2665
|
+
}
|
|
2666
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2667
|
+
const line = lines[i];
|
|
2668
|
+
if ((/\s+'[a-zA-Z_][a-zA-Z0-9_]*'\s*:/.test(line) || /:\s*'[^']*'\s*[,}\]$]/.test(line)) && !/\/\//.test(line)) {
|
|
2669
|
+
issues.push({
|
|
2670
|
+
file: filePath,
|
|
2671
|
+
line: i + 1,
|
|
2672
|
+
severity: "warning",
|
|
2673
|
+
rule: "json/single-quote",
|
|
2674
|
+
message: "Single quotes used \u2014 JSON requires double quotes",
|
|
2675
|
+
suggestion: `Replace ' with "`
|
|
2676
|
+
});
|
|
2677
|
+
break;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
function findDuplicateKeys(obj, nestedPath, filePath, issues, lines) {
|
|
2682
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) return;
|
|
2683
|
+
const keys = Object.keys(obj);
|
|
2684
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2685
|
+
for (const key of keys) {
|
|
2686
|
+
if (seen.has(key)) {
|
|
2687
|
+
const searchStr = `"${key}"`;
|
|
2688
|
+
let lineNum = 1;
|
|
2689
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2690
|
+
if (lines[i].includes(searchStr)) {
|
|
2691
|
+
lineNum = i + 1;
|
|
2692
|
+
break;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
issues.push({
|
|
2696
|
+
file: filePath,
|
|
2697
|
+
line: lineNum,
|
|
2698
|
+
severity: "warning",
|
|
2699
|
+
rule: "json/duplicate-key",
|
|
2700
|
+
message: `Duplicate key "${key}" in ${nestedPath || "root"}`,
|
|
2701
|
+
suggestion: "Remove or rename the duplicate \u2014 last value wins in JSON"
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
seen.set(key, 1);
|
|
2705
|
+
}
|
|
2706
|
+
for (const key of keys) {
|
|
2707
|
+
const deeperPath = nestedPath ? `${nestedPath}.${key}` : key;
|
|
2708
|
+
findDuplicateKeys(obj[key], deeperPath, filePath, issues, lines);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
function checkYaml(filePath, _content, lines, issues) {
|
|
2712
|
+
let hasTabs = false;
|
|
2713
|
+
let hasSpaces = false;
|
|
2714
|
+
let inconsistentIndent = false;
|
|
2715
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2716
|
+
const rawLine = lines[i];
|
|
2717
|
+
const trimmed = rawLine.trim();
|
|
2718
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2719
|
+
if (rawLine.startsWith(" ")) hasTabs = true;
|
|
2720
|
+
const spaceMatch = rawLine.match(/^( +)/);
|
|
2721
|
+
if (spaceMatch) {
|
|
2722
|
+
hasSpaces = true;
|
|
2723
|
+
const indent = spaceMatch[1].length;
|
|
2724
|
+
if (indent % 2 !== 0 && indent !== 0) {
|
|
2725
|
+
inconsistentIndent = true;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
if (hasTabs) {
|
|
2730
|
+
issues.push({
|
|
2731
|
+
file: filePath,
|
|
2732
|
+
line: 1,
|
|
2733
|
+
severity: "error",
|
|
2734
|
+
rule: "yaml/tab-indent",
|
|
2735
|
+
message: "YAML uses spaces for indentation \u2014 tabs are not allowed",
|
|
2736
|
+
suggestion: "Replace tabs with spaces (2 spaces per indent level)"
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
if (inconsistentIndent) {
|
|
2740
|
+
issues.push({
|
|
2741
|
+
file: filePath,
|
|
2742
|
+
line: 1,
|
|
2743
|
+
severity: "warning",
|
|
2744
|
+
rule: "yaml/inconsistent-indent",
|
|
2745
|
+
message: "Indentation depth is not a multiple of 2 \u2014 may cause parsing issues",
|
|
2746
|
+
suggestion: "Use consistent 2-space indentation throughout the file"
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2750
|
+
const trimmed = lines[i].trim();
|
|
2751
|
+
if (/^https?:\/\//.test(trimmed) || /:\s*https?:\/\//.test(trimmed)) {
|
|
2752
|
+
issues.push({
|
|
2753
|
+
file: filePath,
|
|
2754
|
+
line: i + 1,
|
|
2755
|
+
severity: "info",
|
|
2756
|
+
rule: "yaml/unquoted-url",
|
|
2757
|
+
message: "URL should be quoted to avoid colon parsing ambiguity",
|
|
2758
|
+
suggestion: 'Wrap URL in quotes: "https://..."'
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
if (/^\s*\w+:\s*(yes|no|on|off)\s*$/.test(lines[i])) {
|
|
2762
|
+
issues.push({
|
|
2763
|
+
file: filePath,
|
|
2764
|
+
line: i + 1,
|
|
2765
|
+
severity: "info",
|
|
2766
|
+
rule: "yaml/ambiguous-bool",
|
|
2767
|
+
message: "Ambiguous boolean value \u2014 YAML interprets yes/no/on/off as booleans",
|
|
2768
|
+
suggestion: "Use true/false instead for clarity, or quote the value"
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
if (hasTabs && hasSpaces) {
|
|
2773
|
+
issues.push({
|
|
2774
|
+
file: filePath,
|
|
2775
|
+
line: 1,
|
|
2776
|
+
severity: "error",
|
|
2777
|
+
rule: "yaml/mixed-indent",
|
|
2778
|
+
message: "Mixed tabs and spaces in indentation",
|
|
2779
|
+
suggestion: "Use spaces only (2 spaces per level)"
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
function checkToml(filePath, _content, lines, issues) {
|
|
2784
|
+
const seenKeys = /* @__PURE__ */ new Map();
|
|
2785
|
+
const arrayTables = /* @__PURE__ */ new Set();
|
|
2786
|
+
let currentTable = "";
|
|
2787
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2788
|
+
const trimmed = lines[i].trim();
|
|
2789
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2790
|
+
const isArrayTable = trimmed.startsWith("[[");
|
|
2791
|
+
const tableMatch = trimmed.match(/^\[\[?(.+?)\]?\]$/);
|
|
2792
|
+
if (tableMatch) {
|
|
2793
|
+
currentTable = tableMatch[1];
|
|
2794
|
+
if (isArrayTable) {
|
|
2795
|
+
arrayTables.add(currentTable);
|
|
2796
|
+
}
|
|
2797
|
+
continue;
|
|
2798
|
+
}
|
|
2799
|
+
const kvMatch = trimmed.match(/^(\w+)\s*=/);
|
|
2800
|
+
if (kvMatch) {
|
|
2801
|
+
const fullKey = currentTable ? `${currentTable}.${kvMatch[1]}` : kvMatch[1];
|
|
2802
|
+
const parentTable = currentTable.split(".").slice(0, -1).join(".");
|
|
2803
|
+
const isInArrayTable = arrayTables.has(currentTable) || arrayTables.has(parentTable);
|
|
2804
|
+
if (seenKeys.has(fullKey)) {
|
|
2805
|
+
if (!isInArrayTable) {
|
|
2806
|
+
const prevLine = seenKeys.get(fullKey);
|
|
2807
|
+
issues.push({
|
|
2808
|
+
file: filePath,
|
|
2809
|
+
line: i + 1,
|
|
2810
|
+
severity: "warning",
|
|
2811
|
+
rule: "toml/duplicate-key",
|
|
2812
|
+
message: `Duplicate key "${fullKey}" (first defined at line ${prevLine})`,
|
|
2813
|
+
suggestion: "Remove or rename the duplicate \u2014 only last value is kept"
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
} else {
|
|
2817
|
+
seenKeys.set(fullKey, i + 1);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
if (/\{[^}]*\}/.test(trimmed)) {
|
|
2821
|
+
const inlineContent = trimmed.match(/\{([^}]*)\}/);
|
|
2822
|
+
if (inlineContent) {
|
|
2823
|
+
const inlineKeys = inlineContent[1].match(/(\w+)\s*=/g);
|
|
2824
|
+
if (inlineKeys) {
|
|
2825
|
+
const inlineSeen = /* @__PURE__ */ new Set();
|
|
2826
|
+
for (const k of inlineKeys) {
|
|
2827
|
+
const keyName = k.replace(/\s*=\s*$/, "").trim();
|
|
2828
|
+
if (inlineSeen.has(keyName)) {
|
|
2829
|
+
issues.push({
|
|
2830
|
+
file: filePath,
|
|
2831
|
+
line: i + 1,
|
|
2832
|
+
severity: "warning",
|
|
2833
|
+
rule: "toml/duplicate-inline-key",
|
|
2834
|
+
message: `Duplicate key "${keyName}" in inline table`,
|
|
2835
|
+
suggestion: "Remove the duplicate key"
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
inlineSeen.add(keyName);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2626
2845
|
function checkConventions(filePath, content, issues) {
|
|
2627
2846
|
const lines = content.split("\n");
|
|
2628
2847
|
let hasTabs = false;
|
|
@@ -2915,8 +3134,8 @@ function collectBlockLines(lines, startIdx) {
|
|
|
2915
3134
|
let depth = 0;
|
|
2916
3135
|
let started = false;
|
|
2917
3136
|
const block = [];
|
|
2918
|
-
for (let
|
|
2919
|
-
const line = lines[
|
|
3137
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
3138
|
+
const line = lines[i];
|
|
2920
3139
|
block.push(line);
|
|
2921
3140
|
for (const ch of line) {
|
|
2922
3141
|
if (ch === "{") {
|
|
@@ -2933,6 +3152,73 @@ function extractBodyAfter(lines, match) {
|
|
|
2933
3152
|
if (startIdx < 0) return "";
|
|
2934
3153
|
return collectBlockLines(lines, startIdx).join("\n");
|
|
2935
3154
|
}
|
|
3155
|
+
function checkMatchaConventions(filePath, content, issues) {
|
|
3156
|
+
const lines = content.split("\n");
|
|
3157
|
+
const text = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
3158
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3159
|
+
const line = stripCommentsAndStrings(lines[i]);
|
|
3160
|
+
const lineNum = i + 1;
|
|
3161
|
+
if (/\b(?:const|let|var)\s+\w+\s*=\s*(?:[2-9]|\d{2,})\b/.test(line)) {
|
|
3162
|
+
if (!/eslint-disable/.test(lines[i])) {
|
|
3163
|
+
issues.push({
|
|
3164
|
+
file: filePath,
|
|
3165
|
+
line: lineNum,
|
|
3166
|
+
severity: "info",
|
|
3167
|
+
rule: "matcha/no-hardcoded-values",
|
|
3168
|
+
message: "Possible hardcoded numeric value (magic number)",
|
|
3169
|
+
suggestion: "Extract to a named constant"
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
const envMatch = line.match(/process\.env\.([A-Z0-9_]+)/);
|
|
3174
|
+
if (envMatch) {
|
|
3175
|
+
const envName = envMatch[1];
|
|
3176
|
+
if (!envName.includes("_")) {
|
|
3177
|
+
issues.push({
|
|
3178
|
+
file: filePath,
|
|
3179
|
+
line: lineNum,
|
|
3180
|
+
severity: "warning",
|
|
3181
|
+
rule: "matcha/env-prefix",
|
|
3182
|
+
message: `Environment variable "${envName}" doesn't seem to have a prefix`,
|
|
3183
|
+
suggestion: "Use an APPNAME_ prefix for environment variables"
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
const interfaceNames = [];
|
|
3189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3190
|
+
const m = lines[i].match(/^\s*(export\s+)?interface\s+(\w+)/);
|
|
3191
|
+
if (m) interfaceNames.push(m[2]);
|
|
3192
|
+
}
|
|
3193
|
+
for (const iface of interfaceNames) {
|
|
3194
|
+
const implCount = (text.match(new RegExp(`implements\\s+.*\\b${iface}\\b`, "g")) || []).length;
|
|
3195
|
+
if (implCount <= 1) {
|
|
3196
|
+
const lineNum = lines.findIndex((l) => l.includes(`interface ${iface}`)) + 1;
|
|
3197
|
+
issues.push({
|
|
3198
|
+
file: filePath,
|
|
3199
|
+
line: lineNum,
|
|
3200
|
+
severity: "info",
|
|
3201
|
+
rule: "matcha/no-premature-abstraction",
|
|
3202
|
+
message: `Interface "${iface}" has only 1 implementation \u2014 premature abstraction?`,
|
|
3203
|
+
suggestion: "Wait for a 2nd use case before abstracting"
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
const funcBodies = findBlockBodies(lines, /^\s*(export\s+)?(async\s+)?function\s+\w+/);
|
|
3208
|
+
for (const { body, nameLine, name } of funcBodies) {
|
|
3209
|
+
const nonEmptyLines = body.split("\n").filter((l) => l.trim()).length;
|
|
3210
|
+
if (nonEmptyLines > 40) {
|
|
3211
|
+
issues.push({
|
|
3212
|
+
file: filePath,
|
|
3213
|
+
line: nameLine,
|
|
3214
|
+
severity: "warning",
|
|
3215
|
+
rule: "matcha/single-responsibility",
|
|
3216
|
+
message: `Function "${name}" is ${nonEmptyLines} lines long \u2014 might be doing >1 thing`,
|
|
3217
|
+
suggestion: "Extract into smaller, focused functions"
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
2936
3222
|
function formatReviewOutput(issues, filesReviewed, totalFiles, focus, customCriteria, autoDetected) {
|
|
2937
3223
|
const errors = issues.filter((i) => i.severity === "error");
|
|
2938
3224
|
const warnings = issues.filter((i) => i.severity === "warning");
|
|
@@ -3164,9 +3450,9 @@ async function handleGitDiff(params) {
|
|
|
3164
3450
|
}
|
|
3165
3451
|
|
|
3166
3452
|
// src/tools/projectStructure.ts
|
|
3167
|
-
import
|
|
3168
|
-
import
|
|
3169
|
-
var DEFAULT_IGNORE = [
|
|
3453
|
+
import fs9 from "fs";
|
|
3454
|
+
import path9 from "path";
|
|
3455
|
+
var DEFAULT_IGNORE = [
|
|
3170
3456
|
"node_modules",
|
|
3171
3457
|
".git",
|
|
3172
3458
|
".kuma",
|
|
@@ -3202,7 +3488,7 @@ async function handleProjectStructure(params) {
|
|
|
3202
3488
|
sessionMemory.recordToolCall("project_structure", { depth: clampedDepth, folderOnly });
|
|
3203
3489
|
try {
|
|
3204
3490
|
const tree = buildTree(root, root, clampedDepth, 0, folderOnly, includePattern, excludePattern);
|
|
3205
|
-
const projectName =
|
|
3491
|
+
const projectName = path9.basename(root);
|
|
3206
3492
|
const lines = [
|
|
3207
3493
|
"[Project Structure] - " + projectName,
|
|
3208
3494
|
"Depth: " + clampedDepth + " | " + (folderOnly ? "Folders only" : "Files and folders"),
|
|
@@ -3225,7 +3511,7 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
|
|
|
3225
3511
|
const lines = [];
|
|
3226
3512
|
let entries = [];
|
|
3227
3513
|
try {
|
|
3228
|
-
entries =
|
|
3514
|
+
entries = fs9.readdirSync(currentDir, { withFileTypes: true });
|
|
3229
3515
|
} catch {
|
|
3230
3516
|
return lines;
|
|
3231
3517
|
}
|
|
@@ -3234,12 +3520,12 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
|
|
|
3234
3520
|
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
3235
3521
|
return a.name.localeCompare(b.name);
|
|
3236
3522
|
});
|
|
3237
|
-
const relativeDir =
|
|
3523
|
+
const relativeDir = path9.relative(root, currentDir) || ".";
|
|
3238
3524
|
const prefix = getPrefix(currentDepth);
|
|
3239
3525
|
for (const entry of entries) {
|
|
3240
3526
|
if (shouldIgnore(entry.name, relativeDir, root)) continue;
|
|
3241
|
-
const fullPath =
|
|
3242
|
-
const relativePath =
|
|
3527
|
+
const fullPath = path9.join(currentDir, entry.name);
|
|
3528
|
+
const relativePath = path9.relative(root, fullPath);
|
|
3243
3529
|
if (includePattern && !entry.name.includes(includePattern) && !relativePath.includes(includePattern)) {
|
|
3244
3530
|
if (!entry.isDirectory()) continue;
|
|
3245
3531
|
}
|
|
@@ -3250,11 +3536,11 @@ function buildTree(root, currentDir, maxDepth, currentDepth, folderOnly, include
|
|
|
3250
3536
|
lines.push(prefix + "[D] " + entry.name + "/");
|
|
3251
3537
|
let subEntries = [];
|
|
3252
3538
|
try {
|
|
3253
|
-
subEntries =
|
|
3539
|
+
subEntries = fs9.readdirSync(fullPath, { withFileTypes: true });
|
|
3254
3540
|
} catch {
|
|
3255
3541
|
}
|
|
3256
3542
|
const hasVisibleContent = subEntries.some(
|
|
3257
|
-
(e) => !shouldIgnore(e.name,
|
|
3543
|
+
(e) => !shouldIgnore(e.name, path9.relative(root, fullPath), root)
|
|
3258
3544
|
);
|
|
3259
3545
|
if (hasVisibleContent) {
|
|
3260
3546
|
const subLines = buildTree(root, fullPath, maxDepth, currentDepth + 1, folderOnly, includePattern, excludePattern);
|
|
@@ -3285,7 +3571,7 @@ function getPrefix(depth) {
|
|
|
3285
3571
|
}
|
|
3286
3572
|
function getFileSize(fullPath) {
|
|
3287
3573
|
try {
|
|
3288
|
-
const stat =
|
|
3574
|
+
const stat = fs9.statSync(fullPath);
|
|
3289
3575
|
if (stat.size < 1024) return stat.size + "B";
|
|
3290
3576
|
if (stat.size < 1024 * 1024) return (stat.size / 1024).toFixed(0) + "KB";
|
|
3291
3577
|
const sizeMB = (stat.size / (1024 * 1024)).toFixed(1);
|
|
@@ -3296,8 +3582,8 @@ function getFileSize(fullPath) {
|
|
|
3296
3582
|
}
|
|
3297
3583
|
|
|
3298
3584
|
// src/tools/staticAnalysis.ts
|
|
3299
|
-
import
|
|
3300
|
-
import
|
|
3585
|
+
import fs10 from "fs";
|
|
3586
|
+
import path10 from "path";
|
|
3301
3587
|
async function handleStaticAnalysis(params) {
|
|
3302
3588
|
const { tool = "all", files, autoFix = false, timeout = 60 } = params;
|
|
3303
3589
|
const root = getProjectRoot();
|
|
@@ -3315,7 +3601,26 @@ async function handleStaticAnalysis(params) {
|
|
|
3315
3601
|
for (const t of toolsToRun) {
|
|
3316
3602
|
const output = await runTool(t, root, files, autoFix, timeout);
|
|
3317
3603
|
if (output === null) {
|
|
3318
|
-
|
|
3604
|
+
const cmd = buildToolCommand(t, files, autoFix);
|
|
3605
|
+
const reason = cmd === null ? `Could not build command for tool "${t}" (unknown tool type)` : `Command "${cmd}" returned no output (tool may not be installed)`;
|
|
3606
|
+
console.error(`[StaticAnalysis] ${reason}`);
|
|
3607
|
+
allIssues.push({
|
|
3608
|
+
file: `(${t} error)`,
|
|
3609
|
+
line: 0,
|
|
3610
|
+
column: 0,
|
|
3611
|
+
severity: "error",
|
|
3612
|
+
message: reason,
|
|
3613
|
+
rule: t,
|
|
3614
|
+
source: t
|
|
3615
|
+
});
|
|
3616
|
+
const result2 = {
|
|
3617
|
+
tool: t,
|
|
3618
|
+
exitCode: -1,
|
|
3619
|
+
issues: [{ file: `(${t} error)`, line: 0, column: 0, severity: "error", message: reason, rule: t, source: t }],
|
|
3620
|
+
summary: { errors: 1, warnings: 0, info: 0 }
|
|
3621
|
+
};
|
|
3622
|
+
results.push(result2);
|
|
3623
|
+
sessionMemory.addFailedFile("static_analysis:" + t, reason);
|
|
3319
3624
|
continue;
|
|
3320
3625
|
}
|
|
3321
3626
|
const issues = parseToolOutput(t, output.stdout, output.stderr, root);
|
|
@@ -3331,6 +3636,22 @@ async function handleStaticAnalysis(params) {
|
|
|
3331
3636
|
};
|
|
3332
3637
|
results.push(result);
|
|
3333
3638
|
allIssues.push(...issues);
|
|
3639
|
+
if (result.exitCode !== 0 && issues.length === 0 && output.stderr.trim()) {
|
|
3640
|
+
const stderrLines = output.stderr.trim().split("\n").slice(0, 8);
|
|
3641
|
+
console.error(`[StaticAnalysis] ${t} exited with code ${result.exitCode} (no parseable output):`);
|
|
3642
|
+
for (const errLine of stderrLines) {
|
|
3643
|
+
console.error(`[StaticAnalysis] ${errLine}`);
|
|
3644
|
+
}
|
|
3645
|
+
allIssues.push({
|
|
3646
|
+
file: `(${t} error)`,
|
|
3647
|
+
line: 0,
|
|
3648
|
+
column: 0,
|
|
3649
|
+
severity: "error",
|
|
3650
|
+
message: `Tool "${t}" exited with code ${result.exitCode}. Check stderr for details.`,
|
|
3651
|
+
rule: t,
|
|
3652
|
+
source: t
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3334
3655
|
if (result.summary.errors > 0 || result.summary.warnings > 0) {
|
|
3335
3656
|
const errorMsg = result.summary.errors > 0 ? result.summary.errors + " error(s)" : result.summary.warnings + " warning(s)";
|
|
3336
3657
|
sessionMemory.addFailedFile("static_analysis:" + t, errorMsg + " found");
|
|
@@ -3352,11 +3673,11 @@ function detectAvailableTools(root) {
|
|
|
3352
3673
|
"eslint.config.js",
|
|
3353
3674
|
"eslint.config.mjs"
|
|
3354
3675
|
];
|
|
3355
|
-
const hasEslintConfig = eslintConfigs.some((cfg) =>
|
|
3676
|
+
const hasEslintConfig = eslintConfigs.some((cfg) => fs10.existsSync(path10.join(root, cfg)));
|
|
3356
3677
|
if (hasEslintConfig && depNames.has("eslint")) {
|
|
3357
3678
|
tools.push("eslint");
|
|
3358
3679
|
}
|
|
3359
|
-
if (
|
|
3680
|
+
if (fs10.existsSync(path10.join(root, "tsconfig.json")) && depNames.has("typescript")) {
|
|
3360
3681
|
tools.push("tsc");
|
|
3361
3682
|
}
|
|
3362
3683
|
const prettierConfigs = [
|
|
@@ -3367,20 +3688,20 @@ function detectAvailableTools(root) {
|
|
|
3367
3688
|
".prettierrc.toml",
|
|
3368
3689
|
"prettier.config.js"
|
|
3369
3690
|
];
|
|
3370
|
-
const hasPrettierConfig = prettierConfigs.some((cfg) =>
|
|
3691
|
+
const hasPrettierConfig = prettierConfigs.some((cfg) => fs10.existsSync(path10.join(root, cfg)));
|
|
3371
3692
|
if (hasPrettierConfig && depNames.has("prettier")) {
|
|
3372
3693
|
tools.push("prettier");
|
|
3373
3694
|
}
|
|
3374
|
-
if (
|
|
3695
|
+
if (fs10.existsSync(path10.join(root, "ruff.toml")) || fs10.existsSync(path10.join(root, ".ruff.toml"))) {
|
|
3375
3696
|
tools.push("ruff");
|
|
3376
3697
|
}
|
|
3377
3698
|
return tools;
|
|
3378
3699
|
}
|
|
3379
3700
|
function readPackageJson(root) {
|
|
3380
3701
|
try {
|
|
3381
|
-
const pkgPath =
|
|
3382
|
-
if (
|
|
3383
|
-
return JSON.parse(
|
|
3702
|
+
const pkgPath = path10.join(root, "package.json");
|
|
3703
|
+
if (fs10.existsSync(pkgPath)) {
|
|
3704
|
+
return JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
|
|
3384
3705
|
}
|
|
3385
3706
|
} catch {
|
|
3386
3707
|
}
|
|
@@ -3400,10 +3721,16 @@ async function runTool(tool, cwd, files, autoFix, timeoutSeconds) {
|
|
|
3400
3721
|
}
|
|
3401
3722
|
function buildToolCommand(tool, files, autoFix) {
|
|
3402
3723
|
const pm = detectPackageManagerPrefix();
|
|
3724
|
+
const root = getProjectRoot();
|
|
3403
3725
|
switch (tool) {
|
|
3404
3726
|
case "eslint": {
|
|
3405
3727
|
const fixFlag = autoFix ? " --fix" : "";
|
|
3406
3728
|
const target = files ? files.join(" ") : ".";
|
|
3729
|
+
const eslintBin = findBinary(root, "eslint");
|
|
3730
|
+
if (!eslintBin) {
|
|
3731
|
+
console.error(`[StaticAnalysis] eslint not found locally. Trying npx (auto-install).`);
|
|
3732
|
+
return "npx eslint" + fixFlag + " " + target + " --format unix";
|
|
3733
|
+
}
|
|
3407
3734
|
return pm + "eslint" + fixFlag + " " + target + " --format unix";
|
|
3408
3735
|
}
|
|
3409
3736
|
case "tsc": {
|
|
@@ -3412,6 +3739,11 @@ function buildToolCommand(tool, files, autoFix) {
|
|
|
3412
3739
|
case "prettier": {
|
|
3413
3740
|
const fixFlag = autoFix ? " --write" : " --check";
|
|
3414
3741
|
const target = files ? files.join(" ") : ".";
|
|
3742
|
+
const prettierBin = findBinary(root, "prettier");
|
|
3743
|
+
if (!prettierBin) {
|
|
3744
|
+
console.error(`[StaticAnalysis] prettier not found locally. Trying npx (auto-install).`);
|
|
3745
|
+
return "npx prettier" + fixFlag + " " + target;
|
|
3746
|
+
}
|
|
3415
3747
|
return pm + "prettier" + fixFlag + " " + target;
|
|
3416
3748
|
}
|
|
3417
3749
|
case "ruff": {
|
|
@@ -3425,10 +3757,25 @@ function buildToolCommand(tool, files, autoFix) {
|
|
|
3425
3757
|
}
|
|
3426
3758
|
function detectPackageManagerPrefix() {
|
|
3427
3759
|
const root = getProjectRoot();
|
|
3428
|
-
if (
|
|
3429
|
-
if (
|
|
3760
|
+
if (fs10.existsSync(path10.join(root, "pnpm-lock.yaml"))) return "pnpm ";
|
|
3761
|
+
if (fs10.existsSync(path10.join(root, "yarn.lock"))) return "yarn ";
|
|
3430
3762
|
return "npx --no-install ";
|
|
3431
3763
|
}
|
|
3764
|
+
function findBinary(root, binName) {
|
|
3765
|
+
const candidates = [
|
|
3766
|
+
path10.join(root, "node_modules", ".bin", binName),
|
|
3767
|
+
path10.join(root, "..", "node_modules", ".bin", binName)
|
|
3768
|
+
];
|
|
3769
|
+
for (const candidate of candidates) {
|
|
3770
|
+
try {
|
|
3771
|
+
if (fs10.existsSync(candidate)) {
|
|
3772
|
+
return candidate;
|
|
3773
|
+
}
|
|
3774
|
+
} catch {
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
return null;
|
|
3778
|
+
}
|
|
3432
3779
|
function parseToolOutput(tool, stdout, stderr, projectRoot) {
|
|
3433
3780
|
switch (tool) {
|
|
3434
3781
|
case "eslint":
|
|
@@ -3445,7 +3792,7 @@ function parseToolOutput(tool, stdout, stderr, projectRoot) {
|
|
|
3445
3792
|
}
|
|
3446
3793
|
function resolveToolPath(filePath, projectRoot) {
|
|
3447
3794
|
const trimmed = filePath.trim();
|
|
3448
|
-
return
|
|
3795
|
+
return path10.isAbsolute(trimmed) ? path10.relative(projectRoot, trimmed) : trimmed.replace(/^\.\//, "");
|
|
3449
3796
|
}
|
|
3450
3797
|
function parseEslintOutput(output, projectRoot) {
|
|
3451
3798
|
const issues = [];
|
|
@@ -3558,10 +3905,13 @@ function formatResults2(results, allIssues, toolsRun) {
|
|
|
3558
3905
|
if (!hasIssues) {
|
|
3559
3906
|
const failedTools = results.filter((r) => r.exitCode !== 0);
|
|
3560
3907
|
if (failedTools.length > 0) {
|
|
3561
|
-
lines.push("Tool(s) executed but
|
|
3562
|
-
|
|
3908
|
+
lines.push("Tool(s) executed but failed:");
|
|
3909
|
+
for (const ft of failedTools) {
|
|
3910
|
+
lines.push(` - ${ft.tool} (exit code: ${ft.exitCode})`);
|
|
3911
|
+
}
|
|
3563
3912
|
lines.push("");
|
|
3564
|
-
lines.push("The
|
|
3913
|
+
lines.push("The tools may not be installed correctly or encountered an error.");
|
|
3914
|
+
lines.push("Run the tool manually to see the full output.");
|
|
3565
3915
|
} else {
|
|
3566
3916
|
lines.push("All checks passed \u2014 no issues found.");
|
|
3567
3917
|
}
|
|
@@ -3603,13 +3953,13 @@ function formatNoToolsDetected() {
|
|
|
3603
3953
|
"No linters or checkers detected for this project.",
|
|
3604
3954
|
"",
|
|
3605
3955
|
"Detected tools include:",
|
|
3606
|
-
" - ESLint
|
|
3607
|
-
" -
|
|
3608
|
-
" - Prettier
|
|
3609
|
-
" - Ruff
|
|
3956
|
+
" - ESLint \u2014 check .eslintrc or eslint.config.*",
|
|
3957
|
+
" - tsc \u2014 check tsconfig.json",
|
|
3958
|
+
" - Prettier \u2014 check .prettierrc",
|
|
3959
|
+
" - Ruff \u2014 check ruff.toml",
|
|
3610
3960
|
"",
|
|
3611
|
-
"
|
|
3612
|
-
"
|
|
3961
|
+
"Each needs both a config file AND the npm package installed.",
|
|
3962
|
+
"Install a linter, then run project_conventions to refresh."
|
|
3613
3963
|
].join("\n");
|
|
3614
3964
|
}
|
|
3615
3965
|
function formatToolNotAvailable(requested, available) {
|
|
@@ -3722,8 +4072,8 @@ async function handleReflect(params) {
|
|
|
3722
4072
|
import { execSync as execSync6 } from "child_process";
|
|
3723
4073
|
|
|
3724
4074
|
// src/guards/antiPatternDetector.ts
|
|
3725
|
-
import
|
|
3726
|
-
import
|
|
4075
|
+
import fs11 from "fs";
|
|
4076
|
+
import path11 from "path";
|
|
3727
4077
|
import { execSync as execSync4 } from "child_process";
|
|
3728
4078
|
var SCRIPT_PATCH_PATTERNS = [
|
|
3729
4079
|
"writeFileSync",
|
|
@@ -3750,20 +4100,20 @@ function findPatchScripts(projectRoot) {
|
|
|
3750
4100
|
const warnings = [];
|
|
3751
4101
|
const recentFiles = scanRecentFiles(projectRoot);
|
|
3752
4102
|
for (const file of recentFiles) {
|
|
3753
|
-
const ext =
|
|
4103
|
+
const ext = path11.extname(file).toLowerCase();
|
|
3754
4104
|
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
3755
|
-
const relativePath =
|
|
4105
|
+
const relativePath = path11.relative(projectRoot, file);
|
|
3756
4106
|
if (relativePath.startsWith("src") || relativePath.startsWith("test") || relativePath.startsWith("node_modules") || relativePath.startsWith(".")) {
|
|
3757
4107
|
continue;
|
|
3758
4108
|
}
|
|
3759
4109
|
try {
|
|
3760
|
-
const content =
|
|
4110
|
+
const content = fs11.readFileSync(file, "utf-8").toLowerCase();
|
|
3761
4111
|
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
3762
4112
|
if (matchedPattern) {
|
|
3763
4113
|
warnings.push({
|
|
3764
4114
|
severity: "high",
|
|
3765
4115
|
pattern: "script-patching",
|
|
3766
|
-
message: `Created script file that modifies other files: ${
|
|
4116
|
+
message: `Created script file that modifies other files: ${path11.basename(file)}`,
|
|
3767
4117
|
suggestion: "Use **precise_diff_editor** instead \u2014 it has fuzzy matching, auto-backup, and rollback support",
|
|
3768
4118
|
evidence: `File: ${relativePath} contains '${matchedPattern}'`,
|
|
3769
4119
|
filePath: relativePath
|
|
@@ -3797,15 +4147,15 @@ function detectBashGrepUsage() {
|
|
|
3797
4147
|
function scanRecentFiles(projectRoot) {
|
|
3798
4148
|
const recent = [];
|
|
3799
4149
|
try {
|
|
3800
|
-
const entries =
|
|
4150
|
+
const entries = fs11.readdirSync(projectRoot, { withFileTypes: true });
|
|
3801
4151
|
const now = Date.now();
|
|
3802
4152
|
const recentThreshold = 30 * 60 * 1e3;
|
|
3803
4153
|
for (const entry of entries) {
|
|
3804
4154
|
if (!entry.isFile()) continue;
|
|
3805
4155
|
try {
|
|
3806
|
-
const stat =
|
|
4156
|
+
const stat = fs11.statSync(path11.join(projectRoot, entry.name));
|
|
3807
4157
|
if (now - stat.mtimeMs < recentThreshold || now - stat.ctimeMs < recentThreshold) {
|
|
3808
|
-
recent.push(
|
|
4158
|
+
recent.push(path11.join(projectRoot, entry.name));
|
|
3809
4159
|
}
|
|
3810
4160
|
} catch {
|
|
3811
4161
|
continue;
|
|
@@ -3829,14 +4179,14 @@ function detectGitPatchScripts(projectRoot) {
|
|
|
3829
4179
|
const prefix = line.substring(0, 2);
|
|
3830
4180
|
if (prefix !== "??" && prefix !== "A ") continue;
|
|
3831
4181
|
const file = line.substring(3).trim();
|
|
3832
|
-
const ext =
|
|
4182
|
+
const ext = path11.extname(file).toLowerCase();
|
|
3833
4183
|
if (!SCRIPT_EXTENSIONS.includes(ext)) continue;
|
|
3834
4184
|
const isRootLevel = !file.includes("/");
|
|
3835
4185
|
const isScriptsDir = file.startsWith("scripts/") || file.startsWith("patches/");
|
|
3836
4186
|
if (!isRootLevel && !isScriptsDir) continue;
|
|
3837
|
-
const fullPath =
|
|
3838
|
-
if (
|
|
3839
|
-
const content =
|
|
4187
|
+
const fullPath = path11.join(projectRoot, file);
|
|
4188
|
+
if (fs11.existsSync(fullPath)) {
|
|
4189
|
+
const content = fs11.readFileSync(fullPath, "utf-8").toLowerCase();
|
|
3840
4190
|
const matchedPattern = SCRIPT_PATCH_PATTERNS.find((p) => content.includes(p.toLowerCase()));
|
|
3841
4191
|
if (matchedPattern) {
|
|
3842
4192
|
warnings.push({
|
|
@@ -3859,7 +4209,7 @@ function detectGitPatchScripts(projectRoot) {
|
|
|
3859
4209
|
if (deletedStdout) {
|
|
3860
4210
|
const deletedFiles = deletedStdout.split("\n").filter(Boolean);
|
|
3861
4211
|
for (const file of deletedFiles) {
|
|
3862
|
-
const ext =
|
|
4212
|
+
const ext = path11.extname(file).toLowerCase();
|
|
3863
4213
|
if (SCRIPT_EXTENSIONS.includes(ext)) {
|
|
3864
4214
|
warnings.push({
|
|
3865
4215
|
severity: "high",
|
|
@@ -3919,21 +4269,21 @@ function detectAllAntiPatterns() {
|
|
|
3919
4269
|
}
|
|
3920
4270
|
|
|
3921
4271
|
// src/engine/contextSnapshot.ts
|
|
3922
|
-
import
|
|
3923
|
-
import
|
|
4272
|
+
import fs12 from "fs";
|
|
4273
|
+
import path12 from "path";
|
|
3924
4274
|
import { execSync as execSync5 } from "child_process";
|
|
3925
4275
|
var SNAPSHOT_DIR = "context-snapshots";
|
|
3926
4276
|
function snapshotDir() {
|
|
3927
|
-
return
|
|
4277
|
+
return path12.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
|
|
3928
4278
|
}
|
|
3929
4279
|
function ensureSnapshotDir() {
|
|
3930
4280
|
const dir = snapshotDir();
|
|
3931
|
-
if (!
|
|
3932
|
-
|
|
4281
|
+
if (!fs12.existsSync(dir)) {
|
|
4282
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
3933
4283
|
}
|
|
3934
4284
|
}
|
|
3935
4285
|
function snapshotFilePath(timestamp) {
|
|
3936
|
-
return
|
|
4286
|
+
return path12.join(snapshotDir(), `${timestamp}.json`);
|
|
3937
4287
|
}
|
|
3938
4288
|
function saveSnapshot(goal) {
|
|
3939
4289
|
try {
|
|
@@ -3954,7 +4304,7 @@ function saveSnapshot(goal) {
|
|
|
3954
4304
|
hasConventions: !!summary.hasConventions
|
|
3955
4305
|
};
|
|
3956
4306
|
try {
|
|
3957
|
-
|
|
4307
|
+
fs12.writeFileSync(snapshotFilePath(Date.now()), JSON.stringify(snapshot, null, 2), "utf-8");
|
|
3958
4308
|
} catch {
|
|
3959
4309
|
}
|
|
3960
4310
|
sessionMemory.recordToolCall("kuma_context", { action: "save", goal: snapshot.goal });
|
|
@@ -3962,12 +4312,12 @@ function saveSnapshot(goal) {
|
|
|
3962
4312
|
}
|
|
3963
4313
|
function listSnapshots() {
|
|
3964
4314
|
const dir = snapshotDir();
|
|
3965
|
-
if (!
|
|
4315
|
+
if (!fs12.existsSync(dir)) return [];
|
|
3966
4316
|
try {
|
|
3967
|
-
const files =
|
|
4317
|
+
const files = fs12.readdirSync(dir).filter((f) => f.endsWith(".json")).sort((a, b) => b.localeCompare(a));
|
|
3968
4318
|
return files.map((f) => {
|
|
3969
4319
|
try {
|
|
3970
|
-
const content =
|
|
4320
|
+
const content = fs12.readFileSync(path12.join(dir, f), "utf-8");
|
|
3971
4321
|
return JSON.parse(content);
|
|
3972
4322
|
} catch {
|
|
3973
4323
|
return null;
|
|
@@ -4173,6 +4523,68 @@ async function handleKumaContext(params) {
|
|
|
4173
4523
|
}
|
|
4174
4524
|
}
|
|
4175
4525
|
|
|
4526
|
+
// src/tools/kumaInit.ts
|
|
4527
|
+
import fs13 from "fs";
|
|
4528
|
+
import path13 from "path";
|
|
4529
|
+
async function handleKumaInit(params) {
|
|
4530
|
+
const root = params.projectRoot ?? getProjectRoot();
|
|
4531
|
+
const kumaDir = path13.join(root, ".kuma");
|
|
4532
|
+
const memoriesDir = path13.join(kumaDir, "memories");
|
|
4533
|
+
const initMdPath = path13.join(kumaDir, "init.md");
|
|
4534
|
+
sessionMemory.recordToolCall("kuma_init", { projectRoot: root });
|
|
4535
|
+
let rules = "";
|
|
4536
|
+
if (fs13.existsSync(initMdPath)) {
|
|
4537
|
+
rules = fs13.readFileSync(initMdPath, "utf-8");
|
|
4538
|
+
}
|
|
4539
|
+
const memories = [];
|
|
4540
|
+
if (fs13.existsSync(memoriesDir)) {
|
|
4541
|
+
try {
|
|
4542
|
+
const files = fs13.readdirSync(memoriesDir).filter((f) => f.endsWith(".md"));
|
|
4543
|
+
for (const file of files) {
|
|
4544
|
+
try {
|
|
4545
|
+
const content = fs13.readFileSync(path13.join(memoriesDir, file), "utf-8");
|
|
4546
|
+
memories.push({ topic: file.replace(/\.md$/, ""), content });
|
|
4547
|
+
} catch {
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
} catch {
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
const sessionLoad = sessionMemory.loadSession();
|
|
4554
|
+
const sections = [
|
|
4555
|
+
"\u{1F9E0} **Kuma Context Loaded**",
|
|
4556
|
+
"\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"
|
|
4557
|
+
];
|
|
4558
|
+
const rulesLineCount = rules ? rules.split("\n").filter((l) => l.trim()).length : 0;
|
|
4559
|
+
sections.push(`\u{1F4CB} **Rules:** ${rulesLineCount} lines from .kuma/init.md`);
|
|
4560
|
+
if (memories.length > 0) {
|
|
4561
|
+
const memoryList = memories.map((m) => ` \u2022 **${m.topic}** \u2014 ${m.content.split("\n").filter((l) => l.trim()).length} lines`).join("\n");
|
|
4562
|
+
sections.push(`\u{1F4C1} **Memories (${memories.length}):**
|
|
4563
|
+
${memoryList}`);
|
|
4564
|
+
} else {
|
|
4565
|
+
sections.push("\u{1F4C1} **Memories:** none yet \u2014 run project_conventions() to generate");
|
|
4566
|
+
}
|
|
4567
|
+
if (sessionLoad.hasPrevSession) {
|
|
4568
|
+
sections.push(
|
|
4569
|
+
"\u{1F4CA} **Previous Session:**",
|
|
4570
|
+
` \u2022 \u{1F6E0}\uFE0F ${sessionLoad.toolCallCount} tool calls`,
|
|
4571
|
+
` \u2022 ${sessionLoad.hasConventions ? "\u2705 conventions detected" : "\u274C no conventions"}`
|
|
4572
|
+
);
|
|
4573
|
+
} else {
|
|
4574
|
+
sections.push("\u{1F4CA} **Previous Session:** none \u2014 first session for this project");
|
|
4575
|
+
}
|
|
4576
|
+
sections.push(
|
|
4577
|
+
"\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
|
|
4578
|
+
"",
|
|
4579
|
+
"\u{1F4A1} **Next Steps:**",
|
|
4580
|
+
" \u2022 Previous session context has been loaded into session memory",
|
|
4581
|
+
' \u2022 Call `get_session_memory({topic: "..."})` for detailed memory',
|
|
4582
|
+
" \u2022 If project hasn't been scanned, run `project_conventions()`",
|
|
4583
|
+
" \u2022 Or continue working \u2014 session state is ready."
|
|
4584
|
+
);
|
|
4585
|
+
return sections.join("\n");
|
|
4586
|
+
}
|
|
4587
|
+
|
|
4176
4588
|
// src/engine/lspClient.ts
|
|
4177
4589
|
import { spawn as spawn2 } from "child_process";
|
|
4178
4590
|
import path14 from "path";
|
|
@@ -4295,7 +4707,8 @@ var LSPClient = class {
|
|
|
4295
4707
|
}
|
|
4296
4708
|
} catch {
|
|
4297
4709
|
}
|
|
4298
|
-
|
|
4710
|
+
console.error(`[LSP] typescript-language-server not found locally. Trying npx fallback.`);
|
|
4711
|
+
return "npx --yes typescript-language-server";
|
|
4299
4712
|
}
|
|
4300
4713
|
toUri(filePath) {
|
|
4301
4714
|
return `file://${filePath}`;
|
|
@@ -4541,19 +4954,22 @@ async function handleFindReferences(params) {
|
|
|
4541
4954
|
}
|
|
4542
4955
|
if (!lspClient.isAvailable()) {
|
|
4543
4956
|
sessionMemory.recordToolCall("lsp_query", { action: "refs", filePath, line, character, fallback: "regex" });
|
|
4957
|
+
console.error(`[LSP] typescript-language-server not found. Using regex fallback for find_references.`);
|
|
4544
4958
|
const symbolName = extractSymbolAtPosition(resolvedPath, line, character);
|
|
4545
4959
|
if (!symbolName) {
|
|
4546
|
-
return
|
|
4960
|
+
return `**Find References** \u2014 "${filePath}:${line + 1}:${character + 1}"
|
|
4961
|
+
Could not read symbol at this position.
|
|
4547
4962
|
|
|
4548
|
-
|
|
4963
|
+
Try selecting a smaller/cleaner position, or install typescript-language-server for better results:
|
|
4964
|
+
npm install typescript-language-server --save-dev`;
|
|
4549
4965
|
}
|
|
4550
4966
|
return fallbackGrepReferences(symbolName, resolvedPath, line, character);
|
|
4551
4967
|
}
|
|
4552
4968
|
try {
|
|
4553
4969
|
const references = await lspClient.findReferences(resolvedPath, line, character);
|
|
4554
4970
|
if (references.length === 0) {
|
|
4555
|
-
return
|
|
4556
|
-
|
|
4971
|
+
return `**Find References** \u2014 "${filePath}:${line + 1}:${character + 1}"
|
|
4972
|
+
No references found for symbol at this position.`;
|
|
4557
4973
|
}
|
|
4558
4974
|
const enrichedRefs = references.map((ref) => {
|
|
4559
4975
|
let lineContent = "";
|
|
@@ -4573,7 +4989,7 @@ async function handleFindReferences(params) {
|
|
|
4573
4989
|
}
|
|
4574
4990
|
const projectRoot = getProjectRoot();
|
|
4575
4991
|
const lines = [
|
|
4576
|
-
|
|
4992
|
+
`**Find References** \u2014 ${enrichedRefs.length} references found`,
|
|
4577
4993
|
`\u{1F4CD} File: ${path15.relative(projectRoot, resolvedPath)}:${line + 1}:${character + 1}`,
|
|
4578
4994
|
""
|
|
4579
4995
|
];
|
|
@@ -4589,7 +5005,7 @@ async function handleFindReferences(params) {
|
|
|
4589
5005
|
lines.push("\u{1F4A1} Use smart_file_picker to read specific files.");
|
|
4590
5006
|
return lines.join("\n");
|
|
4591
5007
|
} catch (err) {
|
|
4592
|
-
return
|
|
5008
|
+
return `**Find References** \u2014 error during lookup: ${err instanceof Error ? err.message : String(err)}`;
|
|
4593
5009
|
}
|
|
4594
5010
|
}
|
|
4595
5011
|
async function handleGoToDefinition(params) {
|
|
@@ -4604,19 +5020,22 @@ async function handleGoToDefinition(params) {
|
|
|
4604
5020
|
}
|
|
4605
5021
|
if (!lspClient.isAvailable()) {
|
|
4606
5022
|
sessionMemory.recordToolCall("lsp_query", { action: "def", filePath, line, character, fallback: "regex" });
|
|
5023
|
+
console.error(`[LSP] typescript-language-server not found. Using regex fallback for go_to_definition.`);
|
|
4607
5024
|
const symbolName = extractSymbolAtPosition(resolvedPath, line, character);
|
|
4608
5025
|
if (!symbolName) {
|
|
4609
|
-
return
|
|
5026
|
+
return `**Go to Definition** \u2014 "${filePath}:${line + 1}:${character + 1}"
|
|
5027
|
+
Could not read symbol at this position.
|
|
4610
5028
|
|
|
4611
|
-
|
|
5029
|
+
Try selecting a smaller/cleaner position, or install typescript-language-server for better results:
|
|
5030
|
+
npm install typescript-language-server --save-dev`;
|
|
4612
5031
|
}
|
|
4613
5032
|
return fallbackGrepDefinition(symbolName);
|
|
4614
5033
|
}
|
|
4615
5034
|
try {
|
|
4616
5035
|
const definition = await lspClient.goToDefinition(resolvedPath, line, character);
|
|
4617
5036
|
if (!definition) {
|
|
4618
|
-
return
|
|
4619
|
-
|
|
5037
|
+
return `**Go to Definition** \u2014 "${filePath}:${line + 1}:${character + 1}"
|
|
5038
|
+
Cannot find definition for symbol at this position.`;
|
|
4620
5039
|
}
|
|
4621
5040
|
const projectRoot = getProjectRoot();
|
|
4622
5041
|
const relPath = path15.relative(projectRoot, definition.filePath);
|
|
@@ -4641,7 +5060,7 @@ async function handleGoToDefinition(params) {
|
|
|
4641
5060
|
];
|
|
4642
5061
|
return lines.join("\n");
|
|
4643
5062
|
} catch (err) {
|
|
4644
|
-
return
|
|
5063
|
+
return `**Go to Definition** \u2014 error during lookup: ${err instanceof Error ? err.message : String(err)}`;
|
|
4645
5064
|
}
|
|
4646
5065
|
}
|
|
4647
5066
|
async function handleRenameSymbol(params) {
|
|
@@ -4659,11 +5078,13 @@ async function handleRenameSymbol(params) {
|
|
|
4659
5078
|
}
|
|
4660
5079
|
if (!lspClient.isAvailable()) {
|
|
4661
5080
|
sessionMemory.recordToolCall("lsp_query", { action: "rename", filePath, line, character, newName, fallback: "none" });
|
|
4662
|
-
|
|
4663
|
-
Rename
|
|
4664
|
-
|
|
5081
|
+
console.error(`[LSP] typescript-language-server not found. Rename requires LSP server.`);
|
|
5082
|
+
return `**Rename Symbol** \u2014 cannot rename without language server.
|
|
5083
|
+
Rename needs typescript-language-server to track references across all files.
|
|
5084
|
+
|
|
5085
|
+
Install: npm install typescript-language-server --save-dev
|
|
4665
5086
|
|
|
4666
|
-
Meanwhile, use smart_grep to find all references, then precise_diff_editor
|
|
5087
|
+
Meanwhile, use smart_grep to find all references, then precise_diff_editor to rename manually.`;
|
|
4667
5088
|
}
|
|
4668
5089
|
try {
|
|
4669
5090
|
const result = await lspClient.renameSymbol(resolvedPath, line, character, newName);
|
|
@@ -4676,7 +5097,7 @@ Make sure:
|
|
|
4676
5097
|
\`\`\``;
|
|
4677
5098
|
}
|
|
4678
5099
|
if (result.changes.length === 0) {
|
|
4679
|
-
return "
|
|
5100
|
+
return "No changes needed.";
|
|
4680
5101
|
}
|
|
4681
5102
|
const projectRoot = getProjectRoot();
|
|
4682
5103
|
let totalEdits = 0;
|
|
@@ -4732,12 +5153,13 @@ async function handleGetTypeInfo(params) {
|
|
|
4732
5153
|
}
|
|
4733
5154
|
if (!lspClient.isAvailable()) {
|
|
4734
5155
|
sessionMemory.recordToolCall("lsp_query", { action: "type", filePath, line, character, fallback: "line-context" });
|
|
5156
|
+
console.error(`[LSP] typescript-language-server not found. Using file-read fallback for type info.`);
|
|
4735
5157
|
try {
|
|
4736
5158
|
const fileContent = fs15.readFileSync(resolvedPath, "utf-8");
|
|
4737
5159
|
const fileLines = fileContent.split("\n");
|
|
4738
5160
|
const targetLine = fileLines[line];
|
|
4739
5161
|
if (!targetLine) {
|
|
4740
|
-
return
|
|
5162
|
+
return `**Type Info** \u2014 Line ${line + 1} does not exist in "${filePath}".`;
|
|
4741
5163
|
}
|
|
4742
5164
|
const symbolName = extractSymbolAtPosition(resolvedPath, line, character);
|
|
4743
5165
|
const projectRoot = getProjectRoot();
|
|
@@ -4754,29 +5176,26 @@ async function handleGetTypeInfo(params) {
|
|
|
4754
5176
|
}
|
|
4755
5177
|
}
|
|
4756
5178
|
const resultLines = [
|
|
4757
|
-
`\u{1F4CB} **Type Info**
|
|
4758
|
-
`\u{
|
|
4759
|
-
symbolName ? `\u{1F524} **Symbol:** \`${symbolName}\`` : `\u26A0\uFE0F Could not extract symbol at this position.`,
|
|
5179
|
+
`\u{1F4CB} **Type Info** \u2014 \`${relPath}:${line + 1}:${character + 1}\``,
|
|
5180
|
+
symbolName ? `\u{1F524} **Symbol:** \`${symbolName}\`` : `Could not extract symbol at this position.`,
|
|
4760
5181
|
"",
|
|
4761
5182
|
"\u{1F4CE} Context:",
|
|
4762
5183
|
"```",
|
|
4763
5184
|
...contextLines,
|
|
4764
5185
|
"```",
|
|
4765
5186
|
"",
|
|
4766
|
-
|
|
4767
|
-
`\u{1F4A1} Install: npm install typescript-language-server --save-dev`,
|
|
4768
|
-
`\u{1F4A1} Use smart_grep or smart_file_picker to read the file for more context.`
|
|
5187
|
+
"Use smart_grep or smart_file_picker to read more context."
|
|
4769
5188
|
];
|
|
4770
5189
|
return resultLines.join("\n");
|
|
4771
5190
|
} catch (err) {
|
|
4772
|
-
return
|
|
5191
|
+
return `**Type Info** \u2014 could not read file for context: ${err instanceof Error ? err.message : String(err)}`;
|
|
4773
5192
|
}
|
|
4774
5193
|
}
|
|
4775
5194
|
try {
|
|
4776
5195
|
const hoverInfo = await lspClient.getTypeInfo(resolvedPath, line, character);
|
|
4777
5196
|
if (!hoverInfo || !hoverInfo.contents) {
|
|
4778
|
-
return
|
|
4779
|
-
|
|
5197
|
+
return `**Type Info** \u2014 "${filePath}:${line + 1}:${character + 1}"
|
|
5198
|
+
No type info for this position.`;
|
|
4780
5199
|
}
|
|
4781
5200
|
const projectRoot = getProjectRoot();
|
|
4782
5201
|
const relPath = path15.relative(projectRoot, resolvedPath);
|
|
@@ -4864,8 +5283,8 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
|
|
|
4864
5283
|
}
|
|
4865
5284
|
}
|
|
4866
5285
|
if (results.length === 0) {
|
|
4867
|
-
return
|
|
4868
|
-
|
|
5286
|
+
return `**Find References** \u2014 "${symbolName}"
|
|
5287
|
+
No references found. Symbol may not be used in other files.`;
|
|
4869
5288
|
}
|
|
4870
5289
|
const grouped = /* @__PURE__ */ new Map();
|
|
4871
5290
|
for (const r of results) {
|
|
@@ -4875,17 +5294,14 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
|
|
|
4875
5294
|
}
|
|
4876
5295
|
const projectRoot = getProjectRoot();
|
|
4877
5296
|
const lines = [
|
|
4878
|
-
|
|
5297
|
+
`**Find References** \u2014 ${results.length} references found`,
|
|
4879
5298
|
`\u{1F4CD} Symbol: "${symbolName}"`,
|
|
4880
|
-
`\u26A0\uFE0F Regex results may be less accurate than LSP (includes comments/strings).`,
|
|
4881
5299
|
""
|
|
4882
5300
|
];
|
|
4883
5301
|
if (grouped.size > 1) {
|
|
4884
5302
|
const fileList = [...grouped.keys()].map((f) => path15.relative(projectRoot, f)).join(", ");
|
|
4885
|
-
lines.push(
|
|
4886
|
-
lines.push(`
|
|
4887
|
-
lines.push(` If you intended only one scope, clarify which file or location you mean.`);
|
|
4888
|
-
lines.push(` \u{1F4A1} Install typescript-language-server for scope-aware disambiguation.`);
|
|
5303
|
+
lines.push(`**Ambiguity note:** Found matches across ${grouped.size} different files (${fileList}).`);
|
|
5304
|
+
lines.push(` Verify each match is the right scope \u2014 results may include same-named symbols.`);
|
|
4889
5305
|
lines.push("");
|
|
4890
5306
|
} else if (results.length > 1) {
|
|
4891
5307
|
const filePath = [...grouped.keys()][0];
|
|
@@ -4894,9 +5310,8 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
|
|
|
4894
5310
|
const minLine = Math.min(...matchLines);
|
|
4895
5311
|
const maxLine = Math.max(...matchLines);
|
|
4896
5312
|
if (maxLine - minLine > 15) {
|
|
4897
|
-
lines.push(
|
|
4898
|
-
lines.push(`
|
|
4899
|
-
lines.push(` \u{1F4A1} Verify each match is the intended symbol before editing.`);
|
|
5313
|
+
lines.push(`**Ambiguity note:** Found ${results.length} matches for "${symbolName}" spanning lines ${minLine}-${maxLine} in the same file.`);
|
|
5314
|
+
lines.push(` Verify each match is the intended symbol.`);
|
|
4900
5315
|
lines.push("");
|
|
4901
5316
|
}
|
|
4902
5317
|
}
|
|
@@ -4908,7 +5323,7 @@ async function fallbackGrepReferences(symbolName, _filePath, _line, _character)
|
|
|
4908
5323
|
}
|
|
4909
5324
|
lines.push("");
|
|
4910
5325
|
}
|
|
4911
|
-
|
|
5326
|
+
console.error(`[LSP] Regex fallback: found ${results.length} references for "${symbolName}".`);
|
|
4912
5327
|
return lines.join("\n");
|
|
4913
5328
|
} catch (err) {
|
|
4914
5329
|
return `Error in fallback grep references: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -4944,12 +5359,10 @@ async function fallbackGrepDefinition(symbolName) {
|
|
|
4944
5359
|
const projectRoot = getProjectRoot();
|
|
4945
5360
|
const relPath = path15.relative(projectRoot, file);
|
|
4946
5361
|
return [
|
|
4947
|
-
`\u{1F4CD} **Go to Definition
|
|
5362
|
+
`\u{1F4CD} **Go to Definition**`,
|
|
4948
5363
|
`\u{1F4C4} File: \`${relPath}\``,
|
|
4949
5364
|
`\u{1F4CF} Line: ${i + 1}`,
|
|
4950
|
-
`\u2514 ${trimmed.substring(0, 120)}
|
|
4951
|
-
"",
|
|
4952
|
-
`\u{1F4A1} Install typescript-language-server for more accurate results.`
|
|
5365
|
+
`\u2514 ${trimmed.substring(0, 120)}`
|
|
4953
5366
|
].join("\n");
|
|
4954
5367
|
}
|
|
4955
5368
|
}
|
|
@@ -4958,9 +5371,9 @@ async function fallbackGrepDefinition(symbolName) {
|
|
|
4958
5371
|
continue;
|
|
4959
5372
|
}
|
|
4960
5373
|
}
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
5374
|
+
console.error(`[LSP] Regex fallback: no definition found for "${symbolName}".`);
|
|
5375
|
+
return `**Go to Definition** \u2014 "${symbolName}"
|
|
5376
|
+
Cannot find definition.`;
|
|
4964
5377
|
} catch (err) {
|
|
4965
5378
|
return `Error in fallback grep definition: ${err instanceof Error ? err.message : String(err)}`;
|
|
4966
5379
|
}
|
|
@@ -5290,811 +5703,22 @@ function registerAllTools(server) {
|
|
|
5290
5703
|
}
|
|
5291
5704
|
}
|
|
5292
5705
|
);
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
import path16 from "path";
|
|
5299
|
-
var ALL_CONFIG_TYPES = [
|
|
5300
|
-
"claude",
|
|
5301
|
-
"cursor",
|
|
5302
|
-
"windsurf",
|
|
5303
|
-
"copilot",
|
|
5304
|
-
"cline",
|
|
5305
|
-
"aider",
|
|
5306
|
-
"antigravity",
|
|
5307
|
-
"opencode",
|
|
5308
|
-
"codex",
|
|
5309
|
-
"qwen",
|
|
5310
|
-
"kiro",
|
|
5311
|
-
"openclaw",
|
|
5312
|
-
"codewhale"
|
|
5313
|
-
];
|
|
5314
|
-
var CONFIG_LABELS = {
|
|
5315
|
-
claude: "Claude Code (CLAUDE.md / plugin)",
|
|
5316
|
-
cursor: "Cursor (.cursor/rules/*.mdc)",
|
|
5317
|
-
windsurf: "Windsurf (.windsurfrules)",
|
|
5318
|
-
copilot: "GitHub Copilot Editor (AGENTS.md + Skill)",
|
|
5319
|
-
cline: "Cline (.clinerules/*.md)",
|
|
5320
|
-
aider: "Aider (CONVENTIONS.md via .aider.conf.yml)",
|
|
5321
|
-
antigravity: "Antigravity CLI (.agents/skills/)",
|
|
5322
|
-
opencode: "OpenCode (opencode.json)",
|
|
5323
|
-
codex: "Codex CLI (AGENTS.md + .codex/config.toml)",
|
|
5324
|
-
qwen: "Qwen Code (AGENTS.md + settings.json)",
|
|
5325
|
-
kiro: "Kiro (.kiro/steering/*.md)",
|
|
5326
|
-
openclaw: "OpenClaw (skills/)",
|
|
5327
|
-
codewhale: "CodeWhale (skills/ + .codewhale/mcp.json)"
|
|
5328
|
-
};
|
|
5329
|
-
function configFilePath(type) {
|
|
5330
|
-
switch (type) {
|
|
5331
|
-
case "claude":
|
|
5332
|
-
return "CLAUDE.md";
|
|
5333
|
-
case "cursor":
|
|
5334
|
-
return ".cursor/rules/kuma.mdc";
|
|
5335
|
-
case "windsurf":
|
|
5336
|
-
return ".windsurfrules";
|
|
5337
|
-
case "copilot":
|
|
5338
|
-
return "AGENTS.md";
|
|
5339
|
-
case "cline":
|
|
5340
|
-
return ".clinerules/kuma.md";
|
|
5341
|
-
case "aider":
|
|
5342
|
-
return "CONVENTIONS.md";
|
|
5343
|
-
case "antigravity":
|
|
5344
|
-
return ".agents/skills/kuma/SKILL.md";
|
|
5345
|
-
case "opencode":
|
|
5346
|
-
return "opencode.json";
|
|
5347
|
-
case "codex":
|
|
5348
|
-
return "AGENTS.md";
|
|
5349
|
-
case "qwen":
|
|
5350
|
-
return "AGENTS.md";
|
|
5351
|
-
case "kiro":
|
|
5352
|
-
return ".kiro/steering/kuma.md";
|
|
5353
|
-
case "openclaw":
|
|
5354
|
-
return "skills/kuma/SKILL.md";
|
|
5355
|
-
case "codewhale":
|
|
5356
|
-
return "skills/kuma/SKILL.md";
|
|
5357
|
-
}
|
|
5358
|
-
}
|
|
5359
|
-
var CORE_RULES = [
|
|
5360
|
-
"## AI Agent Usage Guidelines",
|
|
5361
|
-
"",
|
|
5362
|
-
"Kuma MCP tools are available. Use them correctly:",
|
|
5363
|
-
"",
|
|
5364
|
-
"### Code Search",
|
|
5365
|
-
"- Use the **smart_grep** tool to search code - NOT bash grep/ripgrep manually",
|
|
5366
|
-
"- smart_grep returns line numbers + context, caches results, respects .gitignore",
|
|
5367
|
-
`- **Example:** smart_grep({ query: "function handleAuth", extensions: ['ts'] })`,
|
|
5368
|
-
"",
|
|
5369
|
-
"### Reading Code",
|
|
5370
|
-
"- Use the **smart_file_picker** tool to read files with smart chunking",
|
|
5371
|
-
"- For large files, use startLine/endLine to read specific ranges",
|
|
5372
|
-
'- **Example:** smart_file_picker({ filePath: "src/index.ts", chunkStrategy: "outline" })',
|
|
5373
|
-
'- **Example:** smart_file_picker({ filePath: "src/index.ts", startLine: 10, endLine: 30 })',
|
|
5374
|
-
"",
|
|
5375
|
-
"### Editing Code",
|
|
5376
|
-
"- Use the **precise_diff_editor** tool to edit files (fuzzy matching + auto-backup)",
|
|
5377
|
-
"- DO NOT create Python/Node scripts to patch files; use precise_diff_editor directly",
|
|
5378
|
-
"- DO NOT use bash sed/cat/awk to modify source files",
|
|
5379
|
-
'- **Example:** precise_diff_editor({ filePath: "src/app.ts", edits: [{ searchBlock: "old code", replaceBlock: "new code" }] })',
|
|
5380
|
-
'- **Example:** precise_diff_editor({ filePath: "src/app.ts", dryRun: true, edits: [...] })',
|
|
5381
|
-
'- **Example:** precise_diff_editor({ filePath: "src/app.ts", action: "rollback" })',
|
|
5382
|
-
"",
|
|
5383
|
-
"### Creating Files",
|
|
5384
|
-
"- Use the **batch_file_writer** tool to create new files (up to 15 at once)",
|
|
5385
|
-
'- **Example:** batch_file_writer({ files: [{ filePath: "src/util.ts", content: "// code", instructions: "reason for creating" }] })',
|
|
5386
|
-
"",
|
|
5387
|
-
"### Running Tasks",
|
|
5388
|
-
"- Use the **execute_safe_test** tool for test/build/lint/typecheck",
|
|
5389
|
-
"- Always run typecheck after editing TypeScript files",
|
|
5390
|
-
'- **Example:** execute_safe_test({ task: "typecheck" })',
|
|
5391
|
-
'- **Example:** execute_safe_test({ task: "custom", customCommand: "npm run lint" })',
|
|
5392
|
-
"",
|
|
5393
|
-
"### Code Review",
|
|
5394
|
-
"- Use the **code_reviewer** tool after changes",
|
|
5395
|
-
"- Supports focus: correctness, security, performance, over-engineering",
|
|
5396
|
-
'- **Example:** code_reviewer({ focus: "security" })',
|
|
5397
|
-
'- **Example:** code_reviewer({ files: ["src/auth.ts"], format: "json" })',
|
|
5398
|
-
"",
|
|
5399
|
-
"### Git Operations",
|
|
5400
|
-
"- Use the **git_diff** tool for structured diff output",
|
|
5401
|
-
"- Use the **git_log** tool for commit history",
|
|
5402
|
-
"- **Example:** git_log({ maxCount: 5 })",
|
|
5403
|
-
"- **Example:** git_diff({ staged: true })",
|
|
5404
|
-
"",
|
|
5405
|
-
"### Session Awareness",
|
|
5406
|
-
"- Use the **kuma_reflect** tool to check on-track/drift/loops",
|
|
5407
|
-
"- Use the **kuma_guard** tool for deeper safety checks (anti-patterns, auto-detection)",
|
|
5408
|
-
"- Use the **get_session_memory** tool to recall session state",
|
|
5409
|
-
'- **Example:** kuma_reflect({ goal: "refactor auth" })',
|
|
5410
|
-
'- **Example:** kuma_guard({ check: "all", goal: "refactor auth" })',
|
|
5411
|
-
"",
|
|
5412
|
-
"### LSP / Code Intelligence",
|
|
5413
|
-
"- Use the **lsp_query** tool for go-to-definition, find references, type info",
|
|
5414
|
-
'- **Example:** lsp_query({ filePath: "src/index.ts", line: 5, character: 10, action: "def" })',
|
|
5415
|
-
'- **Example:** lsp_query({ filePath: "src/index.ts", line: 5, character: 10, action: "refs" })',
|
|
5416
|
-
"",
|
|
5417
|
-
"### Static Analysis",
|
|
5418
|
-
"- Use the **static_analysis** tool to run ESLint/TSC/Prettier/Ruff",
|
|
5419
|
-
'- **Example:** static_analysis({ tool: "eslint", autoFix: true })',
|
|
5420
|
-
"",
|
|
5421
|
-
"### Project Structure",
|
|
5422
|
-
"- Use the **project_structure** tool to see project layout",
|
|
5423
|
-
"- **Example:** project_structure({ depth: 2, folderOnly: true })",
|
|
5424
|
-
"",
|
|
5425
|
-
"### Write Memory",
|
|
5426
|
-
"- Use the **write_memory** tool to persist decisions and glossary",
|
|
5427
|
-
'- **Example:** write_memory({ topic: "decisions", content: "## Reason for using X" })',
|
|
5428
|
-
"",
|
|
5429
|
-
"### General Rules",
|
|
5430
|
-
"- When you error, READ the error carefully before acting",
|
|
5431
|
-
"- After 3+ edits without running tests, stop and verify",
|
|
5432
|
-
"- If a tool fails, check the message - don't retry blindly",
|
|
5433
|
-
"- Detect conventions first with the **project_conventions** tool",
|
|
5434
|
-
"- **Example:** project_conventions({ forceRescan: true })"
|
|
5435
|
-
].join("\n");
|
|
5436
|
-
var KUMA_CORE_INSTRUCTIONS = CORE_RULES;
|
|
5437
|
-
function claudeTemplate() {
|
|
5438
|
-
return [
|
|
5439
|
-
"# Kuma AI Agent Guidelines",
|
|
5440
|
-
"",
|
|
5441
|
-
KUMA_CORE_INSTRUCTIONS,
|
|
5442
|
-
"",
|
|
5443
|
-
"## Workflow Pipeline",
|
|
5444
|
-
"",
|
|
5445
|
-
"For best results:",
|
|
5446
|
-
"1. **project_conventions** - detect stack",
|
|
5447
|
-
"2. **smart_grep** / **smart_file_picker** - understand code",
|
|
5448
|
-
"3. **precise_diff_editor** / **batch_file_writer** - make changes",
|
|
5449
|
-
"4. **execute_safe_test** - verify (typecheck + test)",
|
|
5450
|
-
"5. **code_reviewer** - review changes"
|
|
5451
|
-
].join("\n");
|
|
5452
|
-
}
|
|
5453
|
-
function cursorRulesTemplate() {
|
|
5454
|
-
return [
|
|
5455
|
-
"---",
|
|
5456
|
-
"description: Kuma MCP tool usage rules for AI coding agents",
|
|
5457
|
-
"alwaysApply: true",
|
|
5458
|
-
"---",
|
|
5459
|
-
"",
|
|
5460
|
-
"You are an expert engineer. Kuma MCP tools are available.",
|
|
5461
|
-
"",
|
|
5462
|
-
"## Critical Rules",
|
|
5463
|
-
"",
|
|
5464
|
-
KUMA_CORE_INSTRUCTIONS,
|
|
5465
|
-
"",
|
|
5466
|
-
"## NEVER",
|
|
5467
|
-
"- Never create Python/Node scripts to patch code",
|
|
5468
|
-
"- Never use bash sed/cat/awk to edit source files",
|
|
5469
|
-
"- Never run git push/git commit through bash"
|
|
5470
|
-
].join("\n");
|
|
5471
|
-
}
|
|
5472
|
-
function windsurfRulesTemplate() {
|
|
5473
|
-
return [
|
|
5474
|
-
"# Windsurf Cascade Rules with Kuma",
|
|
5475
|
-
"",
|
|
5476
|
-
KUMA_CORE_INSTRUCTIONS
|
|
5477
|
-
].join("\n");
|
|
5478
|
-
}
|
|
5479
|
-
function copilotTemplate() {
|
|
5480
|
-
return [
|
|
5481
|
-
"## GitHub Copilot Editor",
|
|
5482
|
-
"",
|
|
5483
|
-
"Kuma MCP tools are available. Use them correctly:",
|
|
5484
|
-
"",
|
|
5485
|
-
KUMA_CORE_INSTRUCTIONS,
|
|
5486
|
-
"",
|
|
5487
|
-
"### Copilot Editor-Specific",
|
|
5488
|
-
"- Copilot Editor reads AGENTS.md at project root for persistent instructions",
|
|
5489
|
-
"- Configure MCP servers via VS Code settings (cmd+shift+P \u2192 Developer: Reload Window after adding Kuma)",
|
|
5490
|
-
"- Use kuma_guard periodically to check for anti-patterns"
|
|
5491
|
-
].join("\n");
|
|
5492
|
-
}
|
|
5493
|
-
function clineRulesTemplate() {
|
|
5494
|
-
return [
|
|
5495
|
-
"---",
|
|
5496
|
-
"description: Kuma MCP tool usage rules for AI coding agents",
|
|
5497
|
-
"paths:",
|
|
5498
|
-
' - "*"',
|
|
5499
|
-
"---",
|
|
5500
|
-
"",
|
|
5501
|
-
KUMA_CORE_INSTRUCTIONS
|
|
5502
|
-
].join("\n");
|
|
5503
|
-
}
|
|
5504
|
-
function aiderTemplate() {
|
|
5505
|
-
return [
|
|
5506
|
-
"# Kuma MCP - Aider Coding Conventions",
|
|
5507
|
-
"",
|
|
5508
|
-
"These conventions are loaded by Aider via the `read:` field in .aider.conf.yml",
|
|
5509
|
-
"",
|
|
5510
|
-
KUMA_CORE_INSTRUCTIONS
|
|
5511
|
-
].join("\n");
|
|
5512
|
-
}
|
|
5513
|
-
function opencodeTemplate() {
|
|
5514
|
-
const config = {
|
|
5515
|
-
mcp: {
|
|
5516
|
-
kuma: {
|
|
5517
|
-
type: "local",
|
|
5518
|
-
command: ["npx", "-y", "@plumpslabs/kuma"],
|
|
5519
|
-
enabled: true
|
|
5520
|
-
}
|
|
5706
|
+
server.tool(
|
|
5707
|
+
"kuma_init",
|
|
5708
|
+
"**Call this FIRST** in every new session. Loads project context: rules from .kuma/init.md, memories from .kuma/memories/, and previous session state. After this, you can work without re-detecting conventions.",
|
|
5709
|
+
{
|
|
5710
|
+
projectRoot: z.string().optional().describe("Project root path (auto-detected if omitted)")
|
|
5521
5711
|
},
|
|
5522
|
-
|
|
5523
|
-
};
|
|
5524
|
-
const header = [
|
|
5525
|
-
"// Generated by Kuma MCP - https://github.com/plumpslabs/kuma",
|
|
5526
|
-
"// OpenCode config with Kuma MCP tools. Edit opencode.json to customize.",
|
|
5527
|
-
""
|
|
5528
|
-
].join("\n");
|
|
5529
|
-
return header + JSON.stringify(config, null, 2) + "\n";
|
|
5530
|
-
}
|
|
5531
|
-
function codexTemplate() {
|
|
5532
|
-
return [
|
|
5533
|
-
"## Codex CLI (OpenAI)",
|
|
5534
|
-
"",
|
|
5535
|
-
"Kuma MCP tools are available. Use them correctly:",
|
|
5536
|
-
"",
|
|
5537
|
-
KUMA_CORE_INSTRUCTIONS,
|
|
5538
|
-
"",
|
|
5539
|
-
"### Codex-Specific",
|
|
5540
|
-
"- Codex uses cascading AGENTS.md files (global ~/.codex/AGENTS.md -> project AGENTS.md)",
|
|
5541
|
-
"- MCP config is in .codex/config.toml (auto-generated by kuma init)",
|
|
5542
|
-
"- Use kuma_guard periodically to check for anti-patterns"
|
|
5543
|
-
].join("\n");
|
|
5544
|
-
}
|
|
5545
|
-
function codexConfigTomlTemplate() {
|
|
5546
|
-
return [
|
|
5547
|
-
"# Generated by Kuma MCP - https://github.com/plumpslabs/kuma",
|
|
5548
|
-
"# Kuma MCP server config for Codex CLI",
|
|
5549
|
-
"",
|
|
5550
|
-
"[mcp_servers.kuma]",
|
|
5551
|
-
'command = "npx"',
|
|
5552
|
-
'args = ["-y", "@plumpslabs/kuma"]',
|
|
5553
|
-
""
|
|
5554
|
-
].join("\n");
|
|
5555
|
-
}
|
|
5556
|
-
function qwenTemplate() {
|
|
5557
|
-
return [
|
|
5558
|
-
"## Qwen Code",
|
|
5559
|
-
"",
|
|
5560
|
-
"Kuma MCP tools are available. Use them correctly:",
|
|
5561
|
-
"",
|
|
5562
|
-
KUMA_CORE_INSTRUCTIONS,
|
|
5563
|
-
"",
|
|
5564
|
-
"### Qwen-Specific",
|
|
5565
|
-
"- Qwen reads AGENTS.md at project root for persistent instructions",
|
|
5566
|
-
"- MCP config is in settings.json (auto-generated by kuma init)",
|
|
5567
|
-
"- Use kuma_guard periodically to check for anti-patterns"
|
|
5568
|
-
].join("\n");
|
|
5569
|
-
}
|
|
5570
|
-
function qwenSettingsTemplate() {
|
|
5571
|
-
const config = {
|
|
5572
|
-
mcpServers: {
|
|
5573
|
-
kuma: {
|
|
5574
|
-
command: "npx",
|
|
5575
|
-
args: ["-y", "@plumpslabs/kuma"],
|
|
5576
|
-
env: {}
|
|
5577
|
-
}
|
|
5578
|
-
}
|
|
5579
|
-
};
|
|
5580
|
-
return JSON.stringify(config, null, 2) + "\n";
|
|
5581
|
-
}
|
|
5582
|
-
function kiroRulesTemplate() {
|
|
5583
|
-
return [
|
|
5584
|
-
"---",
|
|
5585
|
-
"name: kuma-mcp",
|
|
5586
|
-
"description: Kuma safety toolkit - use smart_grep for search, precise_diff_editor for edits",
|
|
5587
|
-
"inclusion: always",
|
|
5588
|
-
"---",
|
|
5589
|
-
"",
|
|
5590
|
-
"# Kuma MCP - Kiro Steering",
|
|
5591
|
-
"",
|
|
5592
|
-
KUMA_CORE_INSTRUCTIONS,
|
|
5593
|
-
"",
|
|
5594
|
-
"## Kiro-Specific",
|
|
5595
|
-
"- Kiro reads steering files from .kiro/steering/ for project instructions",
|
|
5596
|
-
"- Configure MCP servers via IDE settings or global Kiro config",
|
|
5597
|
-
"- Use kuma_guard periodically to check for anti-patterns"
|
|
5598
|
-
].join("\n");
|
|
5599
|
-
}
|
|
5600
|
-
function openclawSkillTemplate() {
|
|
5601
|
-
return [
|
|
5602
|
-
"---",
|
|
5603
|
-
"name: kuma-mcp",
|
|
5604
|
-
"description: Kuma safety toolkit for AI coding agents. Use smart_grep for search, precise_diff_editor for edits, execute_safe_test for verification.",
|
|
5605
|
-
"---",
|
|
5606
|
-
"",
|
|
5607
|
-
"# Kuma MCP - OpenClaw Skill",
|
|
5608
|
-
"",
|
|
5609
|
-
KUMA_CORE_INSTRUCTIONS,
|
|
5610
|
-
"",
|
|
5611
|
-
"## OpenClaw-Specific",
|
|
5612
|
-
"- OpenClaw loads skills/ from workspace root or ~/.openclaw/skills for global",
|
|
5613
|
-
"- Configure Kuma MCP server via ~/.openclaw/openclaw.json or agents standard",
|
|
5614
|
-
"- Use kuma_guard periodically to check for anti-patterns",
|
|
5615
|
-
"",
|
|
5616
|
-
"## Verification",
|
|
5617
|
-
"- After edits, run execute_safe_test to verify no breakage",
|
|
5618
|
-
"- Use code_reviewer for correctness/security review",
|
|
5619
|
-
"- Check kuma_reflect to confirm on-track"
|
|
5620
|
-
].join("\n");
|
|
5621
|
-
}
|
|
5622
|
-
function codewhaleTemplate() {
|
|
5623
|
-
return [
|
|
5624
|
-
"---",
|
|
5625
|
-
"name: kuma-mcp",
|
|
5626
|
-
"description: Kuma safety toolkit for AI coding agents. Use smart_grep for search, precise_diff_editor for edits, execute_safe_test for verification.",
|
|
5627
|
-
"---",
|
|
5628
|
-
"",
|
|
5629
|
-
"# Kuma MCP - CodeWhale Skill",
|
|
5630
|
-
"",
|
|
5631
|
-
KUMA_CORE_INSTRUCTIONS,
|
|
5632
|
-
"",
|
|
5633
|
-
"## CodeWhale-Specific",
|
|
5634
|
-
"- CodeWhale loads SKILL.md from skills/ (workspace-local), .agents/skills/, or ~/.codewhale/skills/",
|
|
5635
|
-
"- MCP config is in ~/.codewhale/mcp.json or ~/.deepseek/mcp.json",
|
|
5636
|
-
"- Use kuma_guard periodically to check for anti-patterns",
|
|
5637
|
-
"",
|
|
5638
|
-
"## Verification",
|
|
5639
|
-
"- After edits, run execute_safe_test to verify no breakage",
|
|
5640
|
-
"- Use code_reviewer for correctness/security review",
|
|
5641
|
-
"- Check kuma_reflect to confirm on-track"
|
|
5642
|
-
].join("\n");
|
|
5643
|
-
}
|
|
5644
|
-
function antigravitySkillTemplate() {
|
|
5645
|
-
return [
|
|
5646
|
-
"---",
|
|
5647
|
-
"name: kuma-mcp",
|
|
5648
|
-
"description: Kuma safety toolkit for AI coding agents. Use smart_grep for search, precise_diff_editor for edits, execute_safe_test for verification.",
|
|
5649
|
-
"---",
|
|
5650
|
-
"",
|
|
5651
|
-
"# Kuma MCP - Antigravity Skill",
|
|
5652
|
-
"",
|
|
5653
|
-
KUMA_CORE_INSTRUCTIONS,
|
|
5654
|
-
"",
|
|
5655
|
-
"## Verification",
|
|
5656
|
-
"- After edits, run execute_safe_test to verify no breakage",
|
|
5657
|
-
"- Use code_reviewer for correctness/security review",
|
|
5658
|
-
"- Check kuma_reflect to confirm on-track"
|
|
5659
|
-
].join("\n");
|
|
5660
|
-
}
|
|
5661
|
-
function antigravityMcpConfigTemplate() {
|
|
5662
|
-
const config = {
|
|
5663
|
-
mcpServers: {
|
|
5664
|
-
kuma: {
|
|
5665
|
-
command: "npx",
|
|
5666
|
-
args: ["-y", "@plumpslabs/kuma"],
|
|
5667
|
-
env: {}
|
|
5668
|
-
}
|
|
5669
|
-
}
|
|
5670
|
-
};
|
|
5671
|
-
return JSON.stringify(config, null, 2) + "\n";
|
|
5672
|
-
}
|
|
5673
|
-
var TEMPLATES = {
|
|
5674
|
-
claude: claudeTemplate,
|
|
5675
|
-
cursor: cursorRulesTemplate,
|
|
5676
|
-
windsurf: windsurfRulesTemplate,
|
|
5677
|
-
copilot: copilotTemplate,
|
|
5678
|
-
cline: clineRulesTemplate,
|
|
5679
|
-
aider: aiderTemplate,
|
|
5680
|
-
antigravity: antigravitySkillTemplate,
|
|
5681
|
-
opencode: opencodeTemplate,
|
|
5682
|
-
codex: codexTemplate,
|
|
5683
|
-
qwen: qwenTemplate,
|
|
5684
|
-
kiro: kiroRulesTemplate,
|
|
5685
|
-
openclaw: openclawSkillTemplate,
|
|
5686
|
-
codewhale: codewhaleTemplate
|
|
5687
|
-
};
|
|
5688
|
-
var APPEND_SEPARATOR = "\n\n---\n_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_\n\n";
|
|
5689
|
-
function handleOpencodeSecondary(root, results) {
|
|
5690
|
-
const claudePath = path16.resolve(root, "CLAUDE.md");
|
|
5691
|
-
if (!fs16.existsSync(claudePath)) {
|
|
5692
|
-
try {
|
|
5693
|
-
fs16.writeFileSync(claudePath, [
|
|
5694
|
-
"# Kuma MCP - OpenCode Instructions",
|
|
5695
|
-
"",
|
|
5696
|
-
KUMA_CORE_INSTRUCTIONS
|
|
5697
|
-
].join("\n"), "utf-8");
|
|
5698
|
-
results.push({ type: "opencode", filePath: "CLAUDE.md", action: "created" });
|
|
5699
|
-
} catch (err) {
|
|
5700
|
-
results.push({
|
|
5701
|
-
type: "opencode",
|
|
5702
|
-
filePath: "CLAUDE.md",
|
|
5703
|
-
action: "error",
|
|
5704
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5705
|
-
});
|
|
5706
|
-
}
|
|
5707
|
-
}
|
|
5708
|
-
}
|
|
5709
|
-
function handleCodexSecondary(root, results) {
|
|
5710
|
-
const tomlPath = path16.resolve(root, ".codex/config.toml");
|
|
5711
|
-
if (results.some((r) => r.filePath === ".codex/config.toml")) return;
|
|
5712
|
-
try {
|
|
5713
|
-
const dir = path16.dirname(tomlPath);
|
|
5714
|
-
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5715
|
-
if (fs16.existsSync(tomlPath)) {
|
|
5716
|
-
const existingContent = fs16.readFileSync(tomlPath, "utf-8");
|
|
5717
|
-
if (existingContent.includes("kuma")) {
|
|
5718
|
-
results.push({ type: "codex", filePath: ".codex/config.toml", action: "skipped" });
|
|
5719
|
-
return;
|
|
5720
|
-
}
|
|
5721
|
-
fs16.writeFileSync(tomlPath, existingContent.trimEnd() + "\n\n" + codexConfigTomlTemplate(), "utf-8");
|
|
5722
|
-
results.push({ type: "codex", filePath: ".codex/config.toml", action: "appended" });
|
|
5723
|
-
} else {
|
|
5724
|
-
fs16.writeFileSync(tomlPath, codexConfigTomlTemplate(), "utf-8");
|
|
5725
|
-
results.push({ type: "codex", filePath: ".codex/config.toml", action: "created" });
|
|
5726
|
-
}
|
|
5727
|
-
} catch (err) {
|
|
5728
|
-
results.push({
|
|
5729
|
-
type: "codex",
|
|
5730
|
-
filePath: ".codex/config.toml",
|
|
5731
|
-
action: "error",
|
|
5732
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5733
|
-
});
|
|
5734
|
-
}
|
|
5735
|
-
}
|
|
5736
|
-
function handleQwenSecondary(root, results) {
|
|
5737
|
-
const settingsPath = path16.resolve(root, "settings.json");
|
|
5738
|
-
if (results.some((r) => r.filePath === "settings.json")) return;
|
|
5739
|
-
try {
|
|
5740
|
-
if (fs16.existsSync(settingsPath)) {
|
|
5741
|
-
const existingContent = fs16.readFileSync(settingsPath, "utf-8");
|
|
5742
|
-
if (existingContent.includes("kuma")) {
|
|
5743
|
-
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5744
|
-
try {
|
|
5745
|
-
const parsed = JSON.parse(existingContent);
|
|
5746
|
-
parsed.mcpServers = parsed.mcpServers || {};
|
|
5747
|
-
if (!parsed.mcpServers.kuma) {
|
|
5748
|
-
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5749
|
-
fs16.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5750
|
-
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
5751
|
-
return;
|
|
5752
|
-
}
|
|
5753
|
-
} catch {
|
|
5754
|
-
}
|
|
5755
|
-
}
|
|
5756
|
-
results.push({ type: "qwen", filePath: "settings.json", action: "skipped" });
|
|
5757
|
-
return;
|
|
5758
|
-
}
|
|
5712
|
+
async (params) => {
|
|
5759
5713
|
try {
|
|
5760
|
-
const
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
fs16.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5765
|
-
results.push({ type: "qwen", filePath: "settings.json", action: "appended" });
|
|
5766
|
-
}
|
|
5767
|
-
} catch {
|
|
5768
|
-
}
|
|
5769
|
-
} else {
|
|
5770
|
-
fs16.writeFileSync(settingsPath, qwenSettingsTemplate(), "utf-8");
|
|
5771
|
-
results.push({ type: "qwen", filePath: "settings.json", action: "created" });
|
|
5772
|
-
}
|
|
5773
|
-
} catch (err) {
|
|
5774
|
-
results.push({
|
|
5775
|
-
type: "qwen",
|
|
5776
|
-
filePath: "settings.json",
|
|
5777
|
-
action: "error",
|
|
5778
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5779
|
-
});
|
|
5780
|
-
}
|
|
5781
|
-
}
|
|
5782
|
-
var AGENTS_MD_TYPES = ["codex", "qwen", "copilot"];
|
|
5783
|
-
function getAgentsMdHeader() {
|
|
5784
|
-
return [
|
|
5785
|
-
"# Kuma MCP - Combined Agent Instructions",
|
|
5786
|
-
"",
|
|
5787
|
-
"This file contains instructions for AI coding agents that read AGENTS.md.",
|
|
5788
|
-
"Each section applies to a specific agent. Unused sections can be safely removed.",
|
|
5789
|
-
"",
|
|
5790
|
-
"---",
|
|
5791
|
-
"_Generated by Kuma MCP - https://github.com/plumpslabs/kuma_",
|
|
5792
|
-
""
|
|
5793
|
-
].join("\n");
|
|
5794
|
-
}
|
|
5795
|
-
function getCombinedAgentsMd(selectedTypes) {
|
|
5796
|
-
const sections = [getAgentsMdHeader()];
|
|
5797
|
-
const agentOrder = ["codex", "qwen", "copilot"];
|
|
5798
|
-
for (const t of agentOrder) {
|
|
5799
|
-
if (selectedTypes.has(t)) {
|
|
5800
|
-
sections.push(TEMPLATES[t]());
|
|
5801
|
-
}
|
|
5802
|
-
}
|
|
5803
|
-
return sections.join("\n\n---\n\n");
|
|
5804
|
-
}
|
|
5805
|
-
function handleAntigravityMcpConfig(root, results) {
|
|
5806
|
-
const mcpPath = path16.resolve(root, ".agents/mcp_config.json");
|
|
5807
|
-
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
5808
|
-
try {
|
|
5809
|
-
const mcpDir = path16.dirname(mcpPath);
|
|
5810
|
-
if (fs16.existsSync(mcpPath)) {
|
|
5811
|
-
const existingContent = fs16.readFileSync(mcpPath, "utf-8");
|
|
5812
|
-
if (existingContent.includes("kuma")) {
|
|
5813
|
-
if (!existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5814
|
-
const trimmed = existingContent.trimEnd();
|
|
5815
|
-
if (trimmed.endsWith("}")) {
|
|
5816
|
-
const updated = trimmed.slice(0, -1).trimEnd() + ',\n "_kuma_note": "Kuma MCP - Generated by kuma init"\n}\n';
|
|
5817
|
-
fs16.writeFileSync(mcpPath, updated, "utf-8");
|
|
5818
|
-
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5819
|
-
}
|
|
5820
|
-
}
|
|
5821
|
-
return;
|
|
5822
|
-
}
|
|
5823
|
-
const parsed = JSON.parse(existingContent);
|
|
5824
|
-
parsed.mcpServers = parsed.mcpServers || {};
|
|
5825
|
-
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5826
|
-
fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5827
|
-
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5828
|
-
} else {
|
|
5829
|
-
if (!fs16.existsSync(mcpDir)) fs16.mkdirSync(mcpDir, { recursive: true });
|
|
5830
|
-
fs16.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
5831
|
-
results.push({ type: "antigravity", filePath: ".agents/mcp_config.json", action: "created" });
|
|
5832
|
-
}
|
|
5833
|
-
} catch (err) {
|
|
5834
|
-
results.push({
|
|
5835
|
-
type: "antigravity",
|
|
5836
|
-
filePath: ".agents/mcp_config.json",
|
|
5837
|
-
action: "error",
|
|
5838
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5839
|
-
});
|
|
5840
|
-
}
|
|
5841
|
-
}
|
|
5842
|
-
function handleAiderSecondary(root, results) {
|
|
5843
|
-
const ymlPath = path16.resolve(root, ".aider.conf.yml");
|
|
5844
|
-
if (results.some((r) => r.filePath === ".aider.conf.yml")) return;
|
|
5845
|
-
try {
|
|
5846
|
-
const conventionsRef = "read: CONVENTIONS.md";
|
|
5847
|
-
if (fs16.existsSync(ymlPath)) {
|
|
5848
|
-
const existingContent = fs16.readFileSync(ymlPath, "utf-8");
|
|
5849
|
-
if (existingContent.includes("CONVENTIONS.md") || existingContent.includes("kuma")) {
|
|
5850
|
-
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "skipped" });
|
|
5851
|
-
return;
|
|
5852
|
-
}
|
|
5853
|
-
const newContent = existingContent.trimEnd() + "\n\n# Kuma MCP conventions\n" + conventionsRef + "\n";
|
|
5854
|
-
fs16.writeFileSync(ymlPath, newContent, "utf-8");
|
|
5855
|
-
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "appended" });
|
|
5856
|
-
} else {
|
|
5857
|
-
const content = [
|
|
5858
|
-
"# Generated by Kuma MCP - https://github.com/plumpslabs/kuma",
|
|
5859
|
-
"# Aider will read CONVENTIONS.md for coding conventions",
|
|
5860
|
-
"",
|
|
5861
|
-
conventionsRef,
|
|
5862
|
-
""
|
|
5863
|
-
].join("\n");
|
|
5864
|
-
fs16.writeFileSync(ymlPath, content, "utf-8");
|
|
5865
|
-
results.push({ type: "aider", filePath: ".aider.conf.yml", action: "created" });
|
|
5866
|
-
}
|
|
5867
|
-
} catch (err) {
|
|
5868
|
-
results.push({
|
|
5869
|
-
type: "aider",
|
|
5870
|
-
filePath: ".aider.conf.yml",
|
|
5871
|
-
action: "error",
|
|
5872
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5873
|
-
});
|
|
5874
|
-
}
|
|
5875
|
-
}
|
|
5876
|
-
function handleCopilotSecondary(root, results) {
|
|
5877
|
-
const skillPath = path16.resolve(root, ".github/skills/kuma/SKILL.md");
|
|
5878
|
-
if (results.some((r) => r.filePath === ".github/skills/kuma/SKILL.md")) return;
|
|
5879
|
-
try {
|
|
5880
|
-
const dir = path16.dirname(skillPath);
|
|
5881
|
-
const content = [
|
|
5882
|
-
"---",
|
|
5883
|
-
"name: kuma-mcp",
|
|
5884
|
-
"description: Kuma safety toolkit for AI coding agents. Use smart_grep for search, precise_diff_editor for edits, execute_safe_test for verification.",
|
|
5885
|
-
"---",
|
|
5886
|
-
"",
|
|
5887
|
-
"# Kuma MCP - Copilot Editor Skill",
|
|
5888
|
-
"",
|
|
5889
|
-
KUMA_CORE_INSTRUCTIONS
|
|
5890
|
-
].join("\n");
|
|
5891
|
-
if (fs16.existsSync(skillPath)) {
|
|
5892
|
-
const existingContent = fs16.readFileSync(skillPath, "utf-8");
|
|
5893
|
-
if (existingContent.includes("kuma")) {
|
|
5894
|
-
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "skipped" });
|
|
5895
|
-
return;
|
|
5896
|
-
}
|
|
5897
|
-
fs16.writeFileSync(skillPath, existingContent.trimEnd() + "\n\n" + content, "utf-8");
|
|
5898
|
-
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "appended" });
|
|
5899
|
-
} else {
|
|
5900
|
-
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5901
|
-
fs16.writeFileSync(skillPath, content, "utf-8");
|
|
5902
|
-
results.push({ type: "copilot", filePath: ".github/skills/kuma/SKILL.md", action: "created" });
|
|
5903
|
-
}
|
|
5904
|
-
} catch (err) {
|
|
5905
|
-
results.push({
|
|
5906
|
-
type: "copilot",
|
|
5907
|
-
filePath: ".github/skills/kuma/SKILL.md",
|
|
5908
|
-
action: "error",
|
|
5909
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5910
|
-
});
|
|
5911
|
-
}
|
|
5912
|
-
}
|
|
5913
|
-
function handleOpenclawSecondary(root, results) {
|
|
5914
|
-
const mcpPath = path16.resolve(root, ".agents/mcp_config.json");
|
|
5915
|
-
if (results.some((r) => r.filePath === ".agents/mcp_config.json")) return;
|
|
5916
|
-
try {
|
|
5917
|
-
const dir = path16.dirname(mcpPath);
|
|
5918
|
-
if (fs16.existsSync(mcpPath)) {
|
|
5919
|
-
const existingContent = fs16.readFileSync(mcpPath, "utf-8");
|
|
5920
|
-
if (existingContent.includes("kuma")) return;
|
|
5921
|
-
const parsed = JSON.parse(existingContent);
|
|
5922
|
-
parsed.mcpServers = parsed.mcpServers || {};
|
|
5923
|
-
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5924
|
-
fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5925
|
-
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "appended" });
|
|
5926
|
-
} else {
|
|
5927
|
-
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5928
|
-
fs16.writeFileSync(mcpPath, antigravityMcpConfigTemplate(), "utf-8");
|
|
5929
|
-
results.push({ type: "openclaw", filePath: ".agents/mcp_config.json", action: "created" });
|
|
5930
|
-
}
|
|
5931
|
-
} catch (err) {
|
|
5932
|
-
results.push({
|
|
5933
|
-
type: "openclaw",
|
|
5934
|
-
filePath: ".agents/mcp_config.json",
|
|
5935
|
-
action: "error",
|
|
5936
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5937
|
-
});
|
|
5938
|
-
}
|
|
5939
|
-
}
|
|
5940
|
-
function handleCodewhaleSecondary(root, results) {
|
|
5941
|
-
const mcpPath = path16.resolve(root, ".codewhale/mcp.json");
|
|
5942
|
-
if (results.some((r) => r.filePath === ".codewhale/mcp.json")) return;
|
|
5943
|
-
try {
|
|
5944
|
-
const dir = path16.dirname(mcpPath);
|
|
5945
|
-
if (fs16.existsSync(mcpPath)) {
|
|
5946
|
-
const existingContent = fs16.readFileSync(mcpPath, "utf-8");
|
|
5947
|
-
if (existingContent.includes("kuma")) return;
|
|
5948
|
-
const parsed = JSON.parse(existingContent);
|
|
5949
|
-
parsed.mcpServers = parsed.mcpServers || {};
|
|
5950
|
-
parsed.mcpServers.kuma = { command: "npx", args: ["-y", "@plumpslabs/kuma"], env: {} };
|
|
5951
|
-
fs16.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
5952
|
-
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "appended" });
|
|
5953
|
-
} else {
|
|
5954
|
-
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
5955
|
-
const config = {
|
|
5956
|
-
mcpServers: {
|
|
5957
|
-
kuma: {
|
|
5958
|
-
command: "npx",
|
|
5959
|
-
args: ["-y", "@plumpslabs/kuma"],
|
|
5960
|
-
env: {}
|
|
5961
|
-
}
|
|
5962
|
-
}
|
|
5963
|
-
};
|
|
5964
|
-
fs16.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5965
|
-
results.push({ type: "codewhale", filePath: ".codewhale/mcp.json", action: "created" });
|
|
5966
|
-
}
|
|
5967
|
-
} catch (err) {
|
|
5968
|
-
results.push({
|
|
5969
|
-
type: "codewhale",
|
|
5970
|
-
filePath: ".codewhale/mcp.json",
|
|
5971
|
-
action: "error",
|
|
5972
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5973
|
-
});
|
|
5974
|
-
}
|
|
5975
|
-
}
|
|
5976
|
-
function runInit(types, projectRoot) {
|
|
5977
|
-
const root = projectRoot ?? getProjectRoot();
|
|
5978
|
-
const selected = types.length > 0 ? types : ALL_CONFIG_TYPES;
|
|
5979
|
-
const results = [];
|
|
5980
|
-
const selectedSet = new Set(selected);
|
|
5981
|
-
const agentsMdSelected = AGENTS_MD_TYPES.filter((t) => selectedSet.has(t));
|
|
5982
|
-
let agentsMdHandled = false;
|
|
5983
|
-
for (const type of selected) {
|
|
5984
|
-
const relativePath = configFilePath(type);
|
|
5985
|
-
const fullPath = path16.resolve(root, relativePath);
|
|
5986
|
-
const getTemplate = TEMPLATES[type];
|
|
5987
|
-
try {
|
|
5988
|
-
if (AGENTS_MD_TYPES.includes(type) && !agentsMdHandled) {
|
|
5989
|
-
agentsMdHandled = true;
|
|
5990
|
-
const combinedContent = getCombinedAgentsMd(new Set(agentsMdSelected));
|
|
5991
|
-
if (fs16.existsSync(fullPath)) {
|
|
5992
|
-
const existingContent = fs16.readFileSync(fullPath, "utf-8");
|
|
5993
|
-
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
5994
|
-
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
5995
|
-
} else {
|
|
5996
|
-
fs16.writeFileSync(fullPath, existingContent.trimEnd() + "\n\n" + combinedContent, "utf-8");
|
|
5997
|
-
results.push({ type, filePath: relativePath, action: "appended" });
|
|
5998
|
-
}
|
|
5999
|
-
} else {
|
|
6000
|
-
const dir = path16.dirname(fullPath);
|
|
6001
|
-
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
6002
|
-
fs16.writeFileSync(fullPath, combinedContent, "utf-8");
|
|
6003
|
-
results.push({ type, filePath: relativePath, action: "created" });
|
|
6004
|
-
}
|
|
6005
|
-
if (selectedSet.has("codex")) handleCodexSecondary(root, results);
|
|
6006
|
-
if (selectedSet.has("qwen")) handleQwenSecondary(root, results);
|
|
6007
|
-
if (selectedSet.has("copilot")) handleCopilotSecondary(root, results);
|
|
6008
|
-
} else if (AGENTS_MD_TYPES.includes(type) && agentsMdHandled) {
|
|
6009
|
-
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
6010
|
-
continue;
|
|
6011
|
-
} else {
|
|
6012
|
-
const template = getTemplate();
|
|
6013
|
-
if (fs16.existsSync(fullPath)) {
|
|
6014
|
-
const existingContent = fs16.readFileSync(fullPath, "utf-8");
|
|
6015
|
-
if (existingContent.includes("_Generated by Kuma MCP_")) {
|
|
6016
|
-
if (type === "antigravity") {
|
|
6017
|
-
handleAntigravityMcpConfig(root, results);
|
|
6018
|
-
} else if (type === "openclaw") {
|
|
6019
|
-
handleOpenclawSecondary(root, results);
|
|
6020
|
-
} else if (type === "codewhale") {
|
|
6021
|
-
handleCodewhaleSecondary(root, results);
|
|
6022
|
-
}
|
|
6023
|
-
results.push({ type, filePath: relativePath, action: "skipped" });
|
|
6024
|
-
continue;
|
|
6025
|
-
}
|
|
6026
|
-
const newContent = existingContent.trimEnd() + APPEND_SEPARATOR + template;
|
|
6027
|
-
fs16.writeFileSync(fullPath, newContent, "utf-8");
|
|
6028
|
-
results.push({ type, filePath: relativePath, action: "appended" });
|
|
6029
|
-
} else {
|
|
6030
|
-
const dir = path16.dirname(fullPath);
|
|
6031
|
-
if (!fs16.existsSync(dir)) {
|
|
6032
|
-
fs16.mkdirSync(dir, { recursive: true });
|
|
6033
|
-
}
|
|
6034
|
-
fs16.writeFileSync(fullPath, template, "utf-8");
|
|
6035
|
-
results.push({ type, filePath: relativePath, action: "created" });
|
|
6036
|
-
}
|
|
6037
|
-
if (type === "antigravity") {
|
|
6038
|
-
handleAntigravityMcpConfig(root, results);
|
|
6039
|
-
} else if (type === "openclaw") {
|
|
6040
|
-
handleOpenclawSecondary(root, results);
|
|
6041
|
-
} else if (type === "codewhale") {
|
|
6042
|
-
handleCodewhaleSecondary(root, results);
|
|
6043
|
-
} else if (type === "aider") {
|
|
6044
|
-
handleAiderSecondary(root, results);
|
|
6045
|
-
} else if (type === "opencode") {
|
|
6046
|
-
handleOpencodeSecondary(root, results);
|
|
6047
|
-
}
|
|
5714
|
+
const result = await handleKumaInit(params);
|
|
5715
|
+
return { content: [{ type: "text", text: result }] };
|
|
5716
|
+
} catch (err) {
|
|
5717
|
+
return { content: [{ type: "text", text: `Error in kuma_init: ${err}` }], isError: true };
|
|
6048
5718
|
}
|
|
6049
|
-
} catch (err) {
|
|
6050
|
-
results.push({
|
|
6051
|
-
type,
|
|
6052
|
-
filePath: relativePath,
|
|
6053
|
-
action: "error",
|
|
6054
|
-
error: err instanceof Error ? err.message : String(err)
|
|
6055
|
-
});
|
|
6056
5719
|
}
|
|
6057
|
-
}
|
|
6058
|
-
return results;
|
|
6059
|
-
}
|
|
6060
|
-
function formatInitResults(results) {
|
|
6061
|
-
const lines = [
|
|
6062
|
-
"\u{1F43B} **Kuma Init - AI Agent Config Generator**",
|
|
6063
|
-
""
|
|
6064
|
-
];
|
|
6065
|
-
for (const r of results) {
|
|
6066
|
-
const label = CONFIG_LABELS[r.type];
|
|
6067
|
-
switch (r.action) {
|
|
6068
|
-
case "created":
|
|
6069
|
-
lines.push(" \u2705 " + label);
|
|
6070
|
-
lines.push(" \u2192 Created: " + r.filePath);
|
|
6071
|
-
break;
|
|
6072
|
-
case "appended":
|
|
6073
|
-
lines.push(" \u2795 " + label);
|
|
6074
|
-
lines.push(" \u2192 Appended to: " + r.filePath);
|
|
6075
|
-
break;
|
|
6076
|
-
case "skipped":
|
|
6077
|
-
lines.push(" \u23ED " + label);
|
|
6078
|
-
lines.push(" \u2192 Skipped (already has Kuma): " + r.filePath);
|
|
6079
|
-
break;
|
|
6080
|
-
case "error":
|
|
6081
|
-
lines.push(" \u274C " + label);
|
|
6082
|
-
lines.push(" \u2192 Error: " + (r.error ?? "unknown"));
|
|
6083
|
-
break;
|
|
6084
|
-
}
|
|
6085
|
-
}
|
|
6086
|
-
const created = results.filter((r) => r.action === "created").length;
|
|
6087
|
-
const appended = results.filter((r) => r.action === "appended").length;
|
|
6088
|
-
const skipped = results.filter((r) => r.action === "skipped").length;
|
|
6089
|
-
const errors = results.filter((r) => r.action === "error").length;
|
|
6090
|
-
lines.push(
|
|
6091
|
-
"",
|
|
6092
|
-
"\u{1F4CA} Summary: " + created + " created, " + appended + " appended, " + skipped + " skipped, " + errors + " errors",
|
|
6093
|
-
"",
|
|
6094
|
-
"\u{1F4A1} Config files teach your AI how to use Kuma tools.",
|
|
6095
|
-
"\u{1F4A1} Run again to generate additional config files anytime."
|
|
6096
5720
|
);
|
|
6097
|
-
|
|
5721
|
+
console.error("[Manifest] Registered 18 tools.");
|
|
6098
5722
|
}
|
|
6099
5723
|
|
|
6100
5724
|
// src/index.ts
|
|
@@ -6110,6 +5734,8 @@ Usage:
|
|
|
6110
5734
|
npx @plumpslabs/kuma Start MCP server (default)
|
|
6111
5735
|
npx @plumpslabs/kuma init Generate AI agent config files
|
|
6112
5736
|
npx @plumpslabs/kuma init --all Generate ALL config files
|
|
5737
|
+
npx @plumpslabs/kuma init --merge Append to existing files (default)
|
|
5738
|
+
npx @plumpslabs/kuma init --skip-existing Skip generation if file exists
|
|
6113
5739
|
npx @plumpslabs/kuma init --claude --cursor Generate specific files
|
|
6114
5740
|
npx @plumpslabs/kuma init --help Show this help
|
|
6115
5741
|
|
|
@@ -6178,20 +5804,63 @@ async function main() {
|
|
|
6178
5804
|
}
|
|
6179
5805
|
}
|
|
6180
5806
|
if (selectedTypes.length === 0) {
|
|
6181
|
-
console.error(
|
|
5807
|
+
console.error(
|
|
5808
|
+
"\u26A0\uFE0F No valid flags provided. Use --help to see options."
|
|
5809
|
+
);
|
|
6182
5810
|
process.exit(1);
|
|
6183
5811
|
}
|
|
6184
5812
|
}
|
|
6185
5813
|
}
|
|
6186
|
-
const
|
|
5814
|
+
const skipExisting = requestedFlags.includes("--skip-existing");
|
|
5815
|
+
const results = runInit({
|
|
5816
|
+
types: selectedTypes,
|
|
5817
|
+
projectRoot: process.cwd(),
|
|
5818
|
+
skipExisting
|
|
5819
|
+
});
|
|
6187
5820
|
const output = formatInitResults(results);
|
|
6188
5821
|
console.log(output);
|
|
5822
|
+
const fs16 = await import("fs");
|
|
5823
|
+
const path16 = await import("path");
|
|
5824
|
+
const matchaSkills = path16.resolve(process.cwd(), "skills/matcha/SKILL.md");
|
|
5825
|
+
const matchaAgents = path16.resolve(
|
|
5826
|
+
process.cwd(),
|
|
5827
|
+
".agents/skills/matcha/SKILL.md"
|
|
5828
|
+
);
|
|
5829
|
+
const matchaCursor = path16.resolve(
|
|
5830
|
+
process.cwd(),
|
|
5831
|
+
".cursor/rules/matcha.mdc"
|
|
5832
|
+
);
|
|
5833
|
+
const matchaWindsurf = path16.resolve(
|
|
5834
|
+
process.cwd(),
|
|
5835
|
+
".windsurf/rules/matcha.md"
|
|
5836
|
+
);
|
|
5837
|
+
if (fs16.existsSync(matchaSkills) || fs16.existsSync(matchaAgents) || fs16.existsSync(matchaCursor) || fs16.existsSync(matchaWindsurf)) {
|
|
5838
|
+
console.error(
|
|
5839
|
+
"\n\u{1F375} Hey, I see matcha is installed \u2014 they pair well together!"
|
|
5840
|
+
);
|
|
5841
|
+
}
|
|
6189
5842
|
process.exit(0);
|
|
6190
5843
|
}
|
|
6191
5844
|
sessionMemory.init({
|
|
6192
5845
|
projectRoot: process.cwd(),
|
|
6193
5846
|
startTime: Date.now()
|
|
6194
5847
|
});
|
|
5848
|
+
(async () => {
|
|
5849
|
+
try {
|
|
5850
|
+
const { generateInitMdContent } = await import("./init-ZRT7JP5F.js");
|
|
5851
|
+
const fs16 = await import("fs");
|
|
5852
|
+
const path16 = await import("path");
|
|
5853
|
+
const initMdPath = path16.resolve(process.cwd(), ".kuma/init.md");
|
|
5854
|
+
if (!fs16.existsSync(initMdPath)) {
|
|
5855
|
+
const kumaDir = path16.dirname(initMdPath);
|
|
5856
|
+
if (!fs16.existsSync(kumaDir)) fs16.mkdirSync(kumaDir, { recursive: true });
|
|
5857
|
+
fs16.writeFileSync(initMdPath, generateInitMdContent(), "utf-8");
|
|
5858
|
+
console.error(`[${SERVER_NAME}] Auto-generated .kuma/init.md`);
|
|
5859
|
+
}
|
|
5860
|
+
} catch (err) {
|
|
5861
|
+
console.error(`[${SERVER_NAME}] Failed to auto-generate .kuma/init.md: ${err}`);
|
|
5862
|
+
}
|
|
5863
|
+
})();
|
|
6195
5864
|
const server = new McpServer(
|
|
6196
5865
|
{
|
|
6197
5866
|
name: SERVER_NAME,
|
|
@@ -6211,6 +5880,12 @@ async function main() {
|
|
|
6211
5880
|
console.error(
|
|
6212
5881
|
`[${SERVER_NAME}] Session started: ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
6213
5882
|
);
|
|
5883
|
+
console.error(
|
|
5884
|
+
`[${SERVER_NAME}] Kuma Core v3 \u2014 .kuma/init.md is the single source of truth`
|
|
5885
|
+
);
|
|
5886
|
+
console.error(
|
|
5887
|
+
`[${SERVER_NAME}] \u{1F9E0} Call kuma_init() at session start to load project context`
|
|
5888
|
+
);
|
|
6214
5889
|
await server.connect(transport);
|
|
6215
5890
|
console.error(
|
|
6216
5891
|
`[${SERVER_NAME}] Server connected via stdio. Waiting for requests...`
|
|
@@ -6219,18 +5894,42 @@ async function main() {
|
|
|
6219
5894
|
function interactiveSelect() {
|
|
6220
5895
|
const labels = [
|
|
6221
5896
|
{ type: "claude", label: "1) Claude Code (CLAUDE.md)" },
|
|
6222
|
-
{
|
|
5897
|
+
{
|
|
5898
|
+
type: "cursor",
|
|
5899
|
+
label: "2) Cursor (.cursor/rules/kuma.mdc)"
|
|
5900
|
+
},
|
|
6223
5901
|
{ type: "windsurf", label: "3) Windsurf (.windsurfrules)" },
|
|
6224
|
-
{
|
|
5902
|
+
{
|
|
5903
|
+
type: "copilot",
|
|
5904
|
+
label: "4) GitHub Copilot Editor (AGENTS.md + Skill)"
|
|
5905
|
+
},
|
|
6225
5906
|
{ type: "cline", label: "5) Cline (.clinerules/kuma.md)" },
|
|
6226
|
-
{
|
|
6227
|
-
|
|
5907
|
+
{
|
|
5908
|
+
type: "aider",
|
|
5909
|
+
label: "6) Aider (CONVENTIONS.md via .aider.conf.yml)"
|
|
5910
|
+
},
|
|
5911
|
+
{
|
|
5912
|
+
type: "antigravity",
|
|
5913
|
+
label: "7) Antigravity CLI (.agents/skills/)"
|
|
5914
|
+
},
|
|
6228
5915
|
{ type: "opencode", label: "8) OpenCode (opencode.json)" },
|
|
6229
|
-
{
|
|
6230
|
-
|
|
5916
|
+
{
|
|
5917
|
+
type: "codex",
|
|
5918
|
+
label: "9) Codex CLI - OpenAI (AGENTS.md + .codex/config.toml)"
|
|
5919
|
+
},
|
|
5920
|
+
{
|
|
5921
|
+
type: "qwen",
|
|
5922
|
+
label: "10) Qwen Code (AGENTS.md + settings.json)"
|
|
5923
|
+
},
|
|
6231
5924
|
{ type: "kiro", label: "11) Kiro (.kiro/steering/kuma.md)" },
|
|
6232
|
-
{
|
|
6233
|
-
|
|
5925
|
+
{
|
|
5926
|
+
type: "openclaw",
|
|
5927
|
+
label: "12) OpenClaw (skills/kuma/SKILL.md)"
|
|
5928
|
+
},
|
|
5929
|
+
{
|
|
5930
|
+
type: "codewhale",
|
|
5931
|
+
label: "13) CodeWhale (skills/kuma/SKILL.md + .codewhale/mcp.json)"
|
|
5932
|
+
}
|
|
6234
5933
|
];
|
|
6235
5934
|
const rl = readline.createInterface({
|
|
6236
5935
|
input: process.stdin,
|
|
@@ -6242,38 +5941,41 @@ function interactiveSelect() {
|
|
|
6242
5941
|
console.error(l.label);
|
|
6243
5942
|
}
|
|
6244
5943
|
console.error("");
|
|
6245
|
-
rl.question(
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
const typeMap = {
|
|
6254
|
-
1: "claude",
|
|
6255
|
-
2: "cursor",
|
|
6256
|
-
3: "windsurf",
|
|
6257
|
-
4: "copilot",
|
|
6258
|
-
5: "cline",
|
|
6259
|
-
6: "aider",
|
|
6260
|
-
7: "antigravity",
|
|
6261
|
-
8: "opencode",
|
|
6262
|
-
9: "codex",
|
|
6263
|
-
10: "qwen",
|
|
6264
|
-
11: "kiro",
|
|
6265
|
-
12: "openclaw",
|
|
6266
|
-
13: "codewhale"
|
|
6267
|
-
};
|
|
6268
|
-
const selected = [];
|
|
6269
|
-
for (const n of nums) {
|
|
6270
|
-
const t = typeMap[n];
|
|
6271
|
-
if (t && !selected.includes(t)) {
|
|
6272
|
-
selected.push(t);
|
|
5944
|
+
rl.question(
|
|
5945
|
+
"Enter numbers separated by space (e.g. '1 3 5'), or 'all': ",
|
|
5946
|
+
(answer) => {
|
|
5947
|
+
rl.close();
|
|
5948
|
+
const input = answer.trim().toLowerCase();
|
|
5949
|
+
if (input === "all") {
|
|
5950
|
+
resolve(ALL_CONFIG_TYPES);
|
|
5951
|
+
return;
|
|
6273
5952
|
}
|
|
5953
|
+
const nums = input.split(/\s+/).map(Number).filter((n) => n >= 1 && n <= 13);
|
|
5954
|
+
const typeMap = {
|
|
5955
|
+
1: "claude",
|
|
5956
|
+
2: "cursor",
|
|
5957
|
+
3: "windsurf",
|
|
5958
|
+
4: "copilot",
|
|
5959
|
+
5: "cline",
|
|
5960
|
+
6: "aider",
|
|
5961
|
+
7: "antigravity",
|
|
5962
|
+
8: "opencode",
|
|
5963
|
+
9: "codex",
|
|
5964
|
+
10: "qwen",
|
|
5965
|
+
11: "kiro",
|
|
5966
|
+
12: "openclaw",
|
|
5967
|
+
13: "codewhale"
|
|
5968
|
+
};
|
|
5969
|
+
const selected = [];
|
|
5970
|
+
for (const n of nums) {
|
|
5971
|
+
const t = typeMap[n];
|
|
5972
|
+
if (t && !selected.includes(t)) {
|
|
5973
|
+
selected.push(t);
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
resolve(selected);
|
|
6274
5977
|
}
|
|
6275
|
-
|
|
6276
|
-
});
|
|
5978
|
+
);
|
|
6277
5979
|
});
|
|
6278
5980
|
}
|
|
6279
5981
|
main().catch((err) => {
|