@plumpslabs/kuma 2.2.4 → 2.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +456 -135
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import { z } from "zod";
|
|
|
33
33
|
import fg from "fast-glob";
|
|
34
34
|
import fs from "fs";
|
|
35
35
|
import path from "path";
|
|
36
|
+
import { execSync } from "child_process";
|
|
36
37
|
|
|
37
38
|
// src/utils/tokenCounter.ts
|
|
38
39
|
function estimateTokens(text) {
|
|
@@ -86,94 +87,315 @@ var IGNORE_PATTERNS = [
|
|
|
86
87
|
];
|
|
87
88
|
var grepCache = /* @__PURE__ */ new Map();
|
|
88
89
|
var CACHE_TTL_MS = 3e4;
|
|
90
|
+
var rgAvailable = null;
|
|
91
|
+
function isRipgrepAvailable() {
|
|
92
|
+
if (process.env.KUMA_DISABLE_RG === "1" || process.env.KUMA_DISABLE_RG === "true") {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (rgAvailable !== null) return rgAvailable;
|
|
96
|
+
try {
|
|
97
|
+
execSync("rg --version", { stdio: "ignore", timeout: 2e3 });
|
|
98
|
+
rgAvailable = true;
|
|
99
|
+
} catch {
|
|
100
|
+
rgAvailable = false;
|
|
101
|
+
}
|
|
102
|
+
return rgAvailable;
|
|
103
|
+
}
|
|
104
|
+
function quotePath(p) {
|
|
105
|
+
return `"${p.replace(/"/g, '\\"')}"`;
|
|
106
|
+
}
|
|
89
107
|
async function handleSmartGrep(params) {
|
|
90
|
-
const {
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
const {
|
|
109
|
+
query,
|
|
110
|
+
queries,
|
|
111
|
+
targetFolder,
|
|
112
|
+
maxResults = 30,
|
|
113
|
+
extensions,
|
|
114
|
+
contextLines = 1,
|
|
115
|
+
filenamesOnly = false,
|
|
116
|
+
countOnly = false,
|
|
117
|
+
outputMode = "rich",
|
|
118
|
+
compact = false
|
|
119
|
+
} = params;
|
|
120
|
+
const patterns = queries && queries.length > 0 ? queries : query ? [query] : [];
|
|
121
|
+
if (patterns.length === 0 || patterns[0].length < 1) {
|
|
122
|
+
return compact ? "ERR:query required" : "Error: 'query' or 'queries' parameter is required.";
|
|
93
123
|
}
|
|
94
|
-
const cacheKey = `${
|
|
124
|
+
const cacheKey = `${patterns.join("||")}:${targetFolder ?? "root"}:${maxResults}:${contextLines}:${filenamesOnly}:${countOnly}:${outputMode}:${extensions ? extensions.join(",") : ""}`;
|
|
95
125
|
const cached = grepCache.get(cacheKey);
|
|
96
126
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
97
|
-
sessionMemory.recordToolCall("smart_grep", { query, cached: true });
|
|
127
|
+
sessionMemory.recordToolCall("smart_grep", { query: patterns[0], cached: true });
|
|
98
128
|
return cached.results;
|
|
99
129
|
}
|
|
100
130
|
const projectRoot = getProjectRoot();
|
|
131
|
+
let result;
|
|
132
|
+
if (isRipgrepAvailable()) {
|
|
133
|
+
result = await tryRipgrep(patterns, {
|
|
134
|
+
projectRoot,
|
|
135
|
+
targetFolder,
|
|
136
|
+
maxResults,
|
|
137
|
+
extensions,
|
|
138
|
+
contextLines,
|
|
139
|
+
filenamesOnly,
|
|
140
|
+
countOnly,
|
|
141
|
+
outputMode,
|
|
142
|
+
compact
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
result = await jsGrep(patterns, {
|
|
146
|
+
projectRoot,
|
|
147
|
+
targetFolder,
|
|
148
|
+
maxResults,
|
|
149
|
+
extensions,
|
|
150
|
+
contextLines,
|
|
151
|
+
filenamesOnly,
|
|
152
|
+
countOnly,
|
|
153
|
+
outputMode,
|
|
154
|
+
compact
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
grepCache.set(cacheKey, { results: result, timestamp: Date.now() });
|
|
158
|
+
sessionMemory.recordToolCall("smart_grep", {
|
|
159
|
+
query: patterns[0],
|
|
160
|
+
matchCount: result.length,
|
|
161
|
+
engine: rgAvailable ? "ripgrep" : "js"
|
|
162
|
+
});
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
async function tryRipgrep(patterns, opts) {
|
|
166
|
+
const args = [];
|
|
167
|
+
args.push("rg", "--no-heading", "--color", "never", "-i");
|
|
168
|
+
if (opts.extensions && opts.extensions.length > 0) {
|
|
169
|
+
for (const ext of opts.extensions) {
|
|
170
|
+
const clean = ext.startsWith(".") ? ext.slice(1) : ext;
|
|
171
|
+
args.push("--type-add", `kuma:*.${clean}`);
|
|
172
|
+
args.push("--type", "kuma");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (opts.contextLines > 0 && !opts.filenamesOnly && !opts.countOnly) {
|
|
176
|
+
args.push("-C", String(opts.contextLines));
|
|
177
|
+
}
|
|
178
|
+
if (!opts.filenamesOnly && !opts.countOnly) {
|
|
179
|
+
const perFileLimit = Math.min(opts.maxResults, 50);
|
|
180
|
+
args.push("-m", String(perFileLimit));
|
|
181
|
+
}
|
|
182
|
+
if (opts.filenamesOnly) {
|
|
183
|
+
args.push("-l");
|
|
184
|
+
}
|
|
185
|
+
if (opts.countOnly) {
|
|
186
|
+
args.push("-c");
|
|
187
|
+
}
|
|
188
|
+
for (const p of patterns) {
|
|
189
|
+
args.push("-e", p);
|
|
190
|
+
}
|
|
191
|
+
const searchDir = opts.targetFolder ? path.join(opts.projectRoot, opts.targetFolder) : opts.projectRoot;
|
|
192
|
+
args.push(quotePath(searchDir));
|
|
101
193
|
try {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
absolute: false,
|
|
110
|
-
deep: maxDepth,
|
|
111
|
-
dot: false
|
|
112
|
-
// Skip dotfiles
|
|
194
|
+
const output = execSync(args.join(" "), {
|
|
195
|
+
cwd: opts.projectRoot,
|
|
196
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
197
|
+
// 10MB
|
|
198
|
+
timeout: 15e3,
|
|
199
|
+
encoding: "utf-8",
|
|
200
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
113
201
|
});
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
(e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`
|
|
117
|
-
);
|
|
118
|
-
entries = entries.filter((entry) => {
|
|
119
|
-
const ext = path.extname(entry).toLowerCase();
|
|
120
|
-
return normalizedExts.includes(ext);
|
|
121
|
-
});
|
|
202
|
+
if (!output || output.trim().length === 0) {
|
|
203
|
+
return formatEmpty(patterns[0], opts);
|
|
122
204
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return
|
|
205
|
+
return formatRipgrepOutput(output, patterns, opts);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (err.status === 1) {
|
|
208
|
+
return formatEmpty(patterns[0], opts);
|
|
127
209
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
contextLines.push(`${i + 1}: ${line.substring(0, 200)}`);
|
|
149
|
-
if (i < lines.length - 1)
|
|
150
|
-
contextLines.push(`${i + 2}: ${lines[i + 1].substring(0, 200)}`);
|
|
151
|
-
results.push({
|
|
152
|
-
file: entry,
|
|
153
|
-
line: i + 1,
|
|
154
|
-
content: contextLines.join("\n")
|
|
155
|
-
});
|
|
156
|
-
}
|
|
210
|
+
return await jsGrep(patterns, opts);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function formatRipgrepOutput(output, patterns, opts) {
|
|
214
|
+
const lines = output.trim().split("\n");
|
|
215
|
+
if (opts.countOnly) {
|
|
216
|
+
const counts = lines.map((l) => {
|
|
217
|
+
const parts = l.split(":");
|
|
218
|
+
if (parts.length >= 2) {
|
|
219
|
+
const count = parts.pop();
|
|
220
|
+
return `${parts.join(":")}:${count}`;
|
|
221
|
+
}
|
|
222
|
+
return l;
|
|
223
|
+
});
|
|
224
|
+
if (opts.outputMode === "json") {
|
|
225
|
+
const obj = {};
|
|
226
|
+
for (const c of counts) {
|
|
227
|
+
const idx = c.lastIndexOf(":");
|
|
228
|
+
if (idx > 0) {
|
|
229
|
+
obj[c.slice(0, idx)] = parseInt(c.slice(idx + 1), 10);
|
|
157
230
|
}
|
|
158
|
-
} catch {
|
|
159
|
-
continue;
|
|
160
231
|
}
|
|
232
|
+
return JSON.stringify(obj);
|
|
161
233
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
234
|
+
if (opts.compact) return counts.join("\n");
|
|
235
|
+
return `\u{1F4CA} Count results for "${patterns[0]}":
|
|
236
|
+
${counts.join("\n")}`;
|
|
237
|
+
}
|
|
238
|
+
if (opts.filenamesOnly) {
|
|
239
|
+
if (opts.outputMode === "json") return JSON.stringify(lines);
|
|
240
|
+
if (opts.compact) return lines.join("\n");
|
|
241
|
+
return `\u{1F4C1} Files matching "${patterns[0]}":
|
|
242
|
+
${lines.join("\n")}`;
|
|
243
|
+
}
|
|
244
|
+
const results = parseRipgrepOutput(output, opts.projectRoot).slice(0, opts.maxResults);
|
|
245
|
+
if (results.length === 0) {
|
|
246
|
+
return formatEmpty(patterns[0], opts);
|
|
247
|
+
}
|
|
248
|
+
if (opts.compact || opts.outputMode === "raw") {
|
|
249
|
+
return results.map((r) => `${r.file}:${r.line}:${r.content}`).join("\n");
|
|
250
|
+
}
|
|
251
|
+
if (opts.outputMode === "json") {
|
|
252
|
+
return JSON.stringify(results);
|
|
253
|
+
}
|
|
254
|
+
const header = `\u{1F50D} Smart Grep: "${patterns[0]}"
|
|
255
|
+
\u{1F4C1} ${results.length} matches
|
|
256
|
+
`;
|
|
257
|
+
const body = results.map((r, i) => `[${i + 1}] \u{1F4C4} ${r.file}:${r.line}
|
|
258
|
+
${r.content}`).join("\n");
|
|
259
|
+
return limitLines(`${header}${body}`, 150);
|
|
260
|
+
}
|
|
261
|
+
function parseRipgrepOutput(output, projectRoot) {
|
|
262
|
+
const results = [];
|
|
263
|
+
const lines = output.trim().split("\n");
|
|
264
|
+
const rootPrefix = projectRoot ? projectRoot.replace(/\/+$/, "") + "/" : "";
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
const lastColonIdx = line.lastIndexOf(":");
|
|
267
|
+
if (lastColonIdx <= 0) continue;
|
|
268
|
+
const beforeLastColon = line.slice(0, lastColonIdx);
|
|
269
|
+
const afterLastColon = line.slice(lastColonIdx + 1);
|
|
270
|
+
if (!afterLastColon.trim()) continue;
|
|
271
|
+
const secondLastColonIdx = beforeLastColon.lastIndexOf(":");
|
|
272
|
+
if (secondLastColonIdx <= 0) continue;
|
|
273
|
+
let potentialFile = beforeLastColon.slice(0, secondLastColonIdx);
|
|
274
|
+
const potentialLineStr = beforeLastColon.slice(secondLastColonIdx + 1);
|
|
275
|
+
const potentialLine = parseInt(potentialLineStr, 10);
|
|
276
|
+
if (isNaN(potentialLine) || potentialLine <= 0) continue;
|
|
277
|
+
if (rootPrefix && potentialFile.startsWith(rootPrefix)) {
|
|
278
|
+
potentialFile = potentialFile.slice(rootPrefix.length);
|
|
279
|
+
}
|
|
280
|
+
results.push({
|
|
281
|
+
file: potentialFile,
|
|
282
|
+
line: potentialLine,
|
|
283
|
+
content: afterLastColon.trim()
|
|
168
284
|
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
285
|
+
}
|
|
286
|
+
return results;
|
|
287
|
+
}
|
|
288
|
+
async function jsGrep(patterns, opts) {
|
|
289
|
+
const { projectRoot, targetFolder, maxResults, extensions, contextLines, filenamesOnly, countOnly, outputMode, compact } = opts;
|
|
290
|
+
const searchPattern = targetFolder ? path.join(targetFolder, "**/*").replace(/\\/g, "/") : "**/*";
|
|
291
|
+
const targetDepth = targetFolder ? targetFolder.split("/").filter(Boolean).length : 0;
|
|
292
|
+
const maxDepth = targetFolder ? Math.min(targetDepth + 5, 20) : 10;
|
|
293
|
+
let entries = await fg(searchPattern, {
|
|
294
|
+
cwd: projectRoot,
|
|
295
|
+
ignore: IGNORE_PATTERNS,
|
|
296
|
+
onlyFiles: true,
|
|
297
|
+
absolute: false,
|
|
298
|
+
deep: maxDepth,
|
|
299
|
+
dot: false
|
|
300
|
+
});
|
|
301
|
+
if (extensions && extensions.length > 0) {
|
|
302
|
+
const normalizedExts = extensions.map(
|
|
303
|
+
(e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`
|
|
172
304
|
);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
305
|
+
entries = entries.filter((entry) => {
|
|
306
|
+
const ext = path.extname(entry).toLowerCase();
|
|
307
|
+
return normalizedExts.includes(ext);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (entries.length === 0) {
|
|
311
|
+
return formatEmpty(patterns[0], opts);
|
|
176
312
|
}
|
|
313
|
+
const regex = createCombinedRegex(patterns);
|
|
314
|
+
const results = [];
|
|
315
|
+
const fileCounts = /* @__PURE__ */ new Map();
|
|
316
|
+
const matchedFiles = /* @__PURE__ */ new Set();
|
|
317
|
+
let filesScanned = 0;
|
|
318
|
+
for (const entry of entries) {
|
|
319
|
+
if (!filenamesOnly && results.length >= maxResults) break;
|
|
320
|
+
filesScanned++;
|
|
321
|
+
try {
|
|
322
|
+
const fullPath = path.join(projectRoot, entry);
|
|
323
|
+
const stat = fs.statSync(fullPath);
|
|
324
|
+
if (stat.size > 5e5) continue;
|
|
325
|
+
if (isBinaryFile(fullPath)) continue;
|
|
326
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
327
|
+
const lines = content.split("\n");
|
|
328
|
+
let fileMatchCount = 0;
|
|
329
|
+
for (let i = 0; i < lines.length; i++) {
|
|
330
|
+
if (!filenamesOnly && results.length >= maxResults) break;
|
|
331
|
+
if (regex.test(lines[i])) {
|
|
332
|
+
fileMatchCount++;
|
|
333
|
+
matchedFiles.add(entry);
|
|
334
|
+
if (filenamesOnly) {
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
if (countOnly) {
|
|
338
|
+
fileCounts.set(entry, (fileCounts.get(entry) || 0) + 1);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const contextLinesArr = [];
|
|
342
|
+
const startCtx = Math.max(0, i - contextLines);
|
|
343
|
+
const endCtx = Math.min(lines.length - 1, i + contextLines);
|
|
344
|
+
for (let ci = startCtx; ci <= endCtx; ci++) {
|
|
345
|
+
const prefix = ci === i ? ">" : " ";
|
|
346
|
+
contextLinesArr.push(`${prefix}${ci + 1}: ${lines[ci].substring(0, 200)}`);
|
|
347
|
+
}
|
|
348
|
+
results.push({
|
|
349
|
+
file: entry,
|
|
350
|
+
line: i + 1,
|
|
351
|
+
content: contextLinesArr.join("\n")
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (matchedFiles.size === 0) {
|
|
360
|
+
return formatEmpty(patterns[0], opts);
|
|
361
|
+
}
|
|
362
|
+
if (filenamesOnly) {
|
|
363
|
+
const files = Array.from(matchedFiles);
|
|
364
|
+
if (outputMode === "json") return JSON.stringify(files);
|
|
365
|
+
if (compact) return files.join("\n");
|
|
366
|
+
return `\u{1F4C1} Files matching "${patterns[0]}":
|
|
367
|
+
${files.join("\n")}`;
|
|
368
|
+
}
|
|
369
|
+
if (countOnly) {
|
|
370
|
+
const counts = Array.from(fileCounts.entries()).map(([f, c]) => `${f}:${c}`);
|
|
371
|
+
if (outputMode === "json") return JSON.stringify(Object.fromEntries(fileCounts));
|
|
372
|
+
if (compact) return counts.join("\n");
|
|
373
|
+
return `\u{1F4CA} Count results for "${patterns[0]}":
|
|
374
|
+
${counts.join("\n")}`;
|
|
375
|
+
}
|
|
376
|
+
if (compact || outputMode === "raw") {
|
|
377
|
+
return results.map((r) => `${r.file}:${r.line}:${r.content.split("\n")[0].replace(/^[>\s]+\d+:\s*/, "")}`).join("\n");
|
|
378
|
+
}
|
|
379
|
+
if (outputMode === "json") {
|
|
380
|
+
return JSON.stringify(results);
|
|
381
|
+
}
|
|
382
|
+
return formatResults(results, patterns[0], filesScanned);
|
|
383
|
+
}
|
|
384
|
+
function createCombinedRegex(patterns) {
|
|
385
|
+
if (patterns.length === 1) {
|
|
386
|
+
return createRegex(patterns[0]);
|
|
387
|
+
}
|
|
388
|
+
const combined = patterns.map((p) => {
|
|
389
|
+
try {
|
|
390
|
+
if (/[.\\+*?[\](){}^$|]/.test(p)) {
|
|
391
|
+
return `(?:${p})`;
|
|
392
|
+
}
|
|
393
|
+
return `(?:${p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`;
|
|
394
|
+
} catch {
|
|
395
|
+
return p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
396
|
+
}
|
|
397
|
+
}).join("|");
|
|
398
|
+
return new RegExp(combined, "i");
|
|
177
399
|
}
|
|
178
400
|
function createRegex(query) {
|
|
179
401
|
const normalized = query.replace(/\\\|/g, "|");
|
|
@@ -185,23 +407,27 @@ function createRegex(query) {
|
|
|
185
407
|
}
|
|
186
408
|
return new RegExp(normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
|
|
187
409
|
}
|
|
410
|
+
function formatEmpty(pattern, opts) {
|
|
411
|
+
if (opts.outputMode === "json") return JSON.stringify({ query: pattern, matches: 0, results: [] });
|
|
412
|
+
if (opts.compact) return `0:${pattern}`;
|
|
413
|
+
return `\u{1F50D} No matches for "${pattern}".`;
|
|
414
|
+
}
|
|
188
415
|
function formatResults(results, query, filesScanned) {
|
|
189
416
|
if (results.length === 0) {
|
|
190
417
|
return `\u{1F50D} No matches for "${query}" (${filesScanned} files scanned).`;
|
|
191
418
|
}
|
|
192
|
-
const
|
|
419
|
+
const lines_out = [
|
|
193
420
|
`\u{1F50D} Smart Grep: "${query}"`,
|
|
194
421
|
`\u{1F4C1} ${results.length} matches from ${filesScanned} files scanned`,
|
|
195
422
|
"",
|
|
196
423
|
...results.map((r, i) => {
|
|
197
|
-
|
|
198
|
-
return `[${i + 1}] \u{1F4C4} ${filePath}:${r.line}
|
|
424
|
+
return `[${i + 1}] \u{1F4C4} ${r.file}:${r.line}
|
|
199
425
|
${r.content.split("\n").map((l) => ` ${l}`).join("\n")}`;
|
|
200
426
|
}),
|
|
201
427
|
"",
|
|
202
428
|
`\u{1F4A1} Use smart_file_picker to open a specific file.`
|
|
203
429
|
];
|
|
204
|
-
return limitLines(
|
|
430
|
+
return limitLines(lines_out.join("\n"), 150);
|
|
205
431
|
}
|
|
206
432
|
function isBinaryFile(filePath) {
|
|
207
433
|
try {
|
|
@@ -224,10 +450,54 @@ import path2 from "path";
|
|
|
224
450
|
var MAX_FILE_SIZE = 1e6;
|
|
225
451
|
var CHUNK_THRESHOLD = 300;
|
|
226
452
|
async function handleSmartFilePicker(params) {
|
|
227
|
-
const {
|
|
453
|
+
const {
|
|
454
|
+
filePath,
|
|
455
|
+
files,
|
|
456
|
+
startLine,
|
|
457
|
+
endLine,
|
|
458
|
+
contextLines = 5,
|
|
459
|
+
chunkStrategy = "smart",
|
|
460
|
+
outputMode = "rich",
|
|
461
|
+
compact = false
|
|
462
|
+
} = params;
|
|
463
|
+
const filePaths = [];
|
|
464
|
+
if (files && files.length > 0) {
|
|
465
|
+
filePaths.push(...files);
|
|
466
|
+
} else if (filePath) {
|
|
467
|
+
filePaths.push(filePath);
|
|
468
|
+
} else {
|
|
469
|
+
return compact ? "ERR:filePath required" : "Error: 'filePath' or 'files' parameter is required.";
|
|
470
|
+
}
|
|
471
|
+
const results = [];
|
|
472
|
+
for (const fp of filePaths) {
|
|
473
|
+
const result = await readSingleFile(fp, {
|
|
474
|
+
startLine,
|
|
475
|
+
endLine,
|
|
476
|
+
contextLines,
|
|
477
|
+
chunkStrategy,
|
|
478
|
+
outputMode,
|
|
479
|
+
compact
|
|
480
|
+
});
|
|
481
|
+
results.push(result);
|
|
482
|
+
}
|
|
483
|
+
if (results.length === 1) return results[0];
|
|
484
|
+
if (compact || outputMode === "raw") {
|
|
485
|
+
return results.join("\n---\n");
|
|
486
|
+
}
|
|
487
|
+
return results.join("\n\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n");
|
|
488
|
+
}
|
|
489
|
+
async function readSingleFile(filePath, opts) {
|
|
490
|
+
const {
|
|
491
|
+
startLine,
|
|
492
|
+
endLine,
|
|
493
|
+
contextLines,
|
|
494
|
+
chunkStrategy,
|
|
495
|
+
outputMode,
|
|
496
|
+
compact
|
|
497
|
+
} = opts;
|
|
228
498
|
const validation = validateFilePath(filePath);
|
|
229
499
|
if (!validation.valid) {
|
|
230
|
-
return
|
|
500
|
+
return compact ? `ERR:${filePath} - ${validation.error.message}` : `Error: ${validation.error.message}`;
|
|
231
501
|
}
|
|
232
502
|
const resolvedPath = validation.resolvedPath;
|
|
233
503
|
if (!fs2.existsSync(resolvedPath)) {
|
|
@@ -235,15 +505,21 @@ async function handleSmartFilePicker(params) {
|
|
|
235
505
|
let suggestion;
|
|
236
506
|
if (!path2.isAbsolute(filePath)) {
|
|
237
507
|
const cwdPath = path2.resolve(process.cwd(), filePath);
|
|
238
|
-
suggestion =
|
|
508
|
+
suggestion = `Path resolved to: ${resolvedPath}
|
|
509
|
+
CWD: ${process.cwd()}
|
|
510
|
+
Project root: ${projectRoot}
|
|
511
|
+
Hint: Try path relative to project root, or relative to CWD (${cwdPath})
|
|
512
|
+
Try smart_grep first to locate the correct file.`;
|
|
239
513
|
} else {
|
|
240
514
|
suggestion = "File does not exist. Try smart_grep to locate it.";
|
|
241
515
|
}
|
|
242
|
-
return
|
|
516
|
+
return compact ? `ERR:not found "${filePath}"` : `Error: File not found: "${filePath}".
|
|
517
|
+
${suggestion}`;
|
|
243
518
|
}
|
|
244
519
|
const stat = fs2.statSync(resolvedPath);
|
|
245
520
|
if (stat.size > MAX_FILE_SIZE) {
|
|
246
|
-
return
|
|
521
|
+
return compact ? `ERR:too large ${filePath} (${(stat.size / 1024 / 1024).toFixed(1)}MB)` : `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 1MB.
|
|
522
|
+
Use smart_grep to find specific content instead.`;
|
|
247
523
|
}
|
|
248
524
|
try {
|
|
249
525
|
const content = fs2.readFileSync(resolvedPath, "utf-8");
|
|
@@ -254,6 +530,26 @@ async function handleSmartFilePicker(params) {
|
|
|
254
530
|
chunkStrategy,
|
|
255
531
|
totalLines
|
|
256
532
|
});
|
|
533
|
+
if (compact || outputMode === "raw") {
|
|
534
|
+
if (startLine !== void 0 || endLine !== void 0) {
|
|
535
|
+
const start = startLine ?? 1;
|
|
536
|
+
const end = endLine ?? totalLines;
|
|
537
|
+
return lines.slice(start - 1, end).join("\n");
|
|
538
|
+
}
|
|
539
|
+
if (totalLines <= CHUNK_THRESHOLD || chunkStrategy === "full") {
|
|
540
|
+
return content;
|
|
541
|
+
}
|
|
542
|
+
const tailStart = Math.max(totalLines - 50, 0);
|
|
543
|
+
return lines.slice(0, 50).join("\n") + `
|
|
544
|
+
... ${totalLines - 100} lines hidden ...
|
|
545
|
+
` + lines.slice(tailStart).join("\n");
|
|
546
|
+
}
|
|
547
|
+
if (startLine !== void 0 && endLine === void 0) {
|
|
548
|
+
const ctxStart = Math.max(0, startLine - 1 - contextLines);
|
|
549
|
+
const ctxEnd = Math.min(totalLines, startLine - 1 + contextLines);
|
|
550
|
+
const selectedLines = lines.slice(ctxStart, ctxEnd);
|
|
551
|
+
return formatJumpOutput(filePath, selectedLines, ctxStart + 1, startLine, totalLines);
|
|
552
|
+
}
|
|
257
553
|
if (startLine !== void 0 || endLine !== void 0) {
|
|
258
554
|
const start = startLine ?? 1;
|
|
259
555
|
const end = endLine ?? totalLines;
|
|
@@ -278,30 +574,43 @@ async function handleSmartFilePicker(params) {
|
|
|
278
574
|
);
|
|
279
575
|
}
|
|
280
576
|
} catch (err) {
|
|
281
|
-
return
|
|
577
|
+
return compact ? `ERR:reading "${filePath}"` : `Error reading file "${filePath}": ${err instanceof Error ? err.message : String(err)}`;
|
|
282
578
|
}
|
|
283
579
|
}
|
|
580
|
+
function formatJumpOutput(filePath, lines, startLine, targetLine, totalLines) {
|
|
581
|
+
const header = [
|
|
582
|
+
`File: ${filePath}`,
|
|
583
|
+
`${totalLines} total lines`,
|
|
584
|
+
`Jumped to line ${targetLine} (\xB1context)`,
|
|
585
|
+
""
|
|
586
|
+
].join("\n");
|
|
587
|
+
const body = lines.map((line, i) => {
|
|
588
|
+
const lineNum = startLine + i;
|
|
589
|
+
const marker = lineNum === targetLine ? ">" : " ";
|
|
590
|
+
return `${marker} ${String(lineNum).padStart(4, " ")} | ${line}`;
|
|
591
|
+
}).join("\n");
|
|
592
|
+
return header + "\n" + body;
|
|
593
|
+
}
|
|
284
594
|
function formatOutput(filePath, lines, startLine, totalLines, truncated) {
|
|
285
595
|
const header = [
|
|
286
|
-
|
|
287
|
-
totalLines
|
|
288
|
-
truncated ?
|
|
596
|
+
`File: ${filePath}`,
|
|
597
|
+
`${totalLines} total lines`,
|
|
598
|
+
truncated ? `Showing ${lines.length} lines (file >${CHUNK_THRESHOLD} lines). Use startLine/endLine for a specific range.` : "",
|
|
289
599
|
""
|
|
290
600
|
].filter(Boolean).join("\n");
|
|
291
601
|
const body = lines.map((line, i) => {
|
|
292
602
|
const lineNum = startLine + i;
|
|
293
|
-
return String(lineNum).padStart(4, " ")
|
|
603
|
+
return `${String(lineNum).padStart(4, " ")} | ${line}`;
|
|
294
604
|
}).join("\n");
|
|
295
|
-
|
|
296
|
-
|
|
605
|
+
return truncateToTokenLimit(`${header}
|
|
606
|
+
${body}`, 2e3);
|
|
297
607
|
}
|
|
298
608
|
async function handleOutlineStrategy(filePath, lines, totalLines) {
|
|
299
609
|
const importLines = [];
|
|
300
610
|
const exportLines = [];
|
|
301
611
|
for (let i = 0; i < lines.length; i++) {
|
|
302
612
|
const line = lines[i].trim();
|
|
303
|
-
if (line.startsWith("//") || line.startsWith("#") || line.startsWith("/*") || line.startsWith("*"))
|
|
304
|
-
continue;
|
|
613
|
+
if (line.startsWith("//") || line.startsWith("#") || line.startsWith("/*") || line.startsWith("*")) continue;
|
|
305
614
|
if (line.startsWith("import ") || line.startsWith("from ") || line.startsWith("require(")) {
|
|
306
615
|
importLines.push(line);
|
|
307
616
|
continue;
|
|
@@ -311,15 +620,15 @@ async function handleOutlineStrategy(filePath, lines, totalLines) {
|
|
|
311
620
|
}
|
|
312
621
|
}
|
|
313
622
|
const result = [
|
|
314
|
-
|
|
315
|
-
totalLines
|
|
623
|
+
`File: ${filePath}`,
|
|
624
|
+
`${totalLines} total lines (OUTLINE MODE - signatures and imports only)`,
|
|
316
625
|
"",
|
|
317
626
|
"Imports:",
|
|
318
|
-
...importLines.slice(0, 30).map((l) =>
|
|
319
|
-
importLines.length > 30 ?
|
|
627
|
+
...importLines.slice(0, 30).map((l) => ` ${l.substring(0, 150)}`),
|
|
628
|
+
importLines.length > 30 ? ` ...and ${importLines.length - 30} more imports` : "",
|
|
320
629
|
"",
|
|
321
630
|
"Exports & Declarations:",
|
|
322
|
-
...exportLines.map((e) =>
|
|
631
|
+
...exportLines.map((e) => ` [L${e.line}] ${e.text}`),
|
|
323
632
|
"",
|
|
324
633
|
"Pass startLine/endLine to read a specific range.",
|
|
325
634
|
'Or use chunkStrategy: "full" to read the entire file.'
|
|
@@ -337,7 +646,7 @@ async function handleSmartStrategy(filePath, lines, totalLines) {
|
|
|
337
646
|
smartLines.push({ line: -1, text: "" });
|
|
338
647
|
smartLines.push({
|
|
339
648
|
line: -1,
|
|
340
|
-
text:
|
|
649
|
+
text: ` ... ${tailStart - headerEnd} lines hidden ...`
|
|
341
650
|
});
|
|
342
651
|
smartLines.push({ line: -1, text: "" });
|
|
343
652
|
}
|
|
@@ -354,18 +663,18 @@ async function handleSmartStrategy(filePath, lines, totalLines) {
|
|
|
354
663
|
smartLines.push({ line: i + 1, text: lines[i] });
|
|
355
664
|
}
|
|
356
665
|
const header = [
|
|
357
|
-
|
|
358
|
-
totalLines
|
|
666
|
+
`File: ${filePath}`,
|
|
667
|
+
`${totalLines} total lines (SMART MODE - header + signatures + tail)`,
|
|
359
668
|
"Pass startLine/endLine to read a specific range.",
|
|
360
669
|
'Or use chunkStrategy: "full" to read the entire file.',
|
|
361
670
|
""
|
|
362
671
|
].join("\n");
|
|
363
672
|
const body = smartLines.map((s) => {
|
|
364
673
|
if (s.line === -1) return s.text;
|
|
365
|
-
return String(s.line).padStart(4, " ")
|
|
674
|
+
return `${String(s.line).padStart(4, " ")} | ${s.text}`;
|
|
366
675
|
}).join("\n");
|
|
367
|
-
|
|
368
|
-
|
|
676
|
+
return truncateToTokenLimit(`${header}
|
|
677
|
+
${body}`, 3e3);
|
|
369
678
|
}
|
|
370
679
|
function findHeaderEnd(lines) {
|
|
371
680
|
let lastImportLine = 0;
|
|
@@ -386,9 +695,7 @@ function extractKeyDeclarations(lines) {
|
|
|
386
695
|
for (let i = 0; i < lines.length; i++) {
|
|
387
696
|
const line = lines[i].trim();
|
|
388
697
|
if (!line || line.startsWith("//") || line.startsWith("#")) continue;
|
|
389
|
-
if (/^(export\s+)?(async\s+)?(function|class|interface|type|enum|const|let|var|def)\s/.test(
|
|
390
|
-
line
|
|
391
|
-
)) {
|
|
698
|
+
if (/^(export\s+)?(async\s+)?(function|class|interface|type|enum|const|let|var|def)\s/.test(line)) {
|
|
392
699
|
decls.push({ line: i, text: line.substring(0, 150) });
|
|
393
700
|
}
|
|
394
701
|
}
|
|
@@ -889,10 +1196,10 @@ File "${filePath}" restored from backup: "${relBackupPath}".`;
|
|
|
889
1196
|
}
|
|
890
1197
|
async function handleCommitRollback(filePath, version) {
|
|
891
1198
|
try {
|
|
892
|
-
const { execSync:
|
|
1199
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
893
1200
|
const root = getProjectRoot();
|
|
894
1201
|
if (version === "list") {
|
|
895
|
-
const log =
|
|
1202
|
+
const log = execSync11("git log --oneline -20", { cwd: root, encoding: "utf-8" });
|
|
896
1203
|
const lines = log.trim().split("\n").map((l) => ` ${l}`).join("\n");
|
|
897
1204
|
return [
|
|
898
1205
|
`\u{1F4CB} **Recent Commits:**`,
|
|
@@ -902,7 +1209,7 @@ async function handleCommitRollback(filePath, version) {
|
|
|
902
1209
|
`\u{1F4A1} Use { action: "rollback", scope: "commit", version: <N> } to restore a specific commit.`
|
|
903
1210
|
].join("\n");
|
|
904
1211
|
}
|
|
905
|
-
const status =
|
|
1212
|
+
const status = execSync11("git status --porcelain", { cwd: root, encoding: "utf-8" }).trim();
|
|
906
1213
|
if (status) {
|
|
907
1214
|
return [
|
|
908
1215
|
`\u26A0\uFE0F **Uncommitted changes detected.**`,
|
|
@@ -923,10 +1230,10 @@ async function handleCommitRollback(filePath, version) {
|
|
|
923
1230
|
target = "HEAD~1";
|
|
924
1231
|
}
|
|
925
1232
|
if (filePath) {
|
|
926
|
-
|
|
1233
|
+
execSync11(`git checkout ${target} -- "${filePath}"`, { cwd: root, encoding: "utf-8" });
|
|
927
1234
|
return `\u2705 **Commit Rollback** \u2014 File "${filePath}" restored from ${target}.`;
|
|
928
1235
|
} else {
|
|
929
|
-
|
|
1236
|
+
execSync11(`git checkout ${target}`, { cwd: root, encoding: "utf-8" });
|
|
930
1237
|
return `\u2705 **Commit Rollback** \u2014 Project restored to ${target}.`;
|
|
931
1238
|
}
|
|
932
1239
|
} catch (err) {
|
|
@@ -2012,7 +2319,7 @@ function formatTimeoutResult(command, timeout) {
|
|
|
2012
2319
|
// src/agents/codeReviewer.ts
|
|
2013
2320
|
import fs7 from "fs";
|
|
2014
2321
|
import path7 from "path";
|
|
2015
|
-
import { execSync } from "child_process";
|
|
2322
|
+
import { execSync as execSync2 } from "child_process";
|
|
2016
2323
|
var CODE_EXTENSIONS = [
|
|
2017
2324
|
".ts",
|
|
2018
2325
|
".tsx",
|
|
@@ -2042,7 +2349,7 @@ var CONFIG_EXTENSIONS = [
|
|
|
2042
2349
|
function getGitChangedFiles() {
|
|
2043
2350
|
try {
|
|
2044
2351
|
const root = getProjectRoot();
|
|
2045
|
-
const stdout =
|
|
2352
|
+
const stdout = execSync2("git status --porcelain", {
|
|
2046
2353
|
cwd: root,
|
|
2047
2354
|
encoding: "utf-8"
|
|
2048
2355
|
});
|
|
@@ -3146,10 +3453,10 @@ function formatConventionsOutput(conventions) {
|
|
|
3146
3453
|
}
|
|
3147
3454
|
|
|
3148
3455
|
// src/utils/gitUtils.ts
|
|
3149
|
-
import { execSync as
|
|
3456
|
+
import { execSync as execSync3 } from "child_process";
|
|
3150
3457
|
function runGitCommand(command) {
|
|
3151
3458
|
const root = getProjectRoot();
|
|
3152
|
-
return
|
|
3459
|
+
return execSync3(command, {
|
|
3153
3460
|
cwd: root,
|
|
3154
3461
|
encoding: "utf-8",
|
|
3155
3462
|
maxBuffer: 2 * 1024 * 1024
|
|
@@ -3805,7 +4112,7 @@ function formatToolNotAvailable(requested, available) {
|
|
|
3805
4112
|
}
|
|
3806
4113
|
|
|
3807
4114
|
// src/utils/kumaShared.ts
|
|
3808
|
-
import { execSync as
|
|
4115
|
+
import { execSync as execSync4 } from "child_process";
|
|
3809
4116
|
function getSessionStats(inputGoal) {
|
|
3810
4117
|
const summary = sessionMemory.getSummary();
|
|
3811
4118
|
const goal = inputGoal || summary.currentGoal || "";
|
|
@@ -3827,7 +4134,7 @@ function getSessionStats(inputGoal) {
|
|
|
3827
4134
|
function getGitDiffStat(timeout = 3e3) {
|
|
3828
4135
|
try {
|
|
3829
4136
|
const root = getProjectRoot();
|
|
3830
|
-
return
|
|
4137
|
+
return execSync4("git diff --stat", {
|
|
3831
4138
|
cwd: root,
|
|
3832
4139
|
encoding: "utf-8",
|
|
3833
4140
|
timeout
|
|
@@ -4219,7 +4526,7 @@ function formatHealthDashboard(r) {
|
|
|
4219
4526
|
// src/guards/antiPatternDetector.ts
|
|
4220
4527
|
import fs11 from "fs";
|
|
4221
4528
|
import path11 from "path";
|
|
4222
|
-
import { execSync as
|
|
4529
|
+
import { execSync as execSync5 } from "child_process";
|
|
4223
4530
|
var SCRIPT_PATCH_PATTERNS = [
|
|
4224
4531
|
"writeFileSync",
|
|
4225
4532
|
"writeFile",
|
|
@@ -4313,7 +4620,7 @@ function scanRecentFiles(projectRoot) {
|
|
|
4313
4620
|
function detectGitPatchScripts(projectRoot) {
|
|
4314
4621
|
const warnings = [];
|
|
4315
4622
|
try {
|
|
4316
|
-
const statusStdout =
|
|
4623
|
+
const statusStdout = execSync5("git status --porcelain", {
|
|
4317
4624
|
cwd: projectRoot,
|
|
4318
4625
|
encoding: "utf-8",
|
|
4319
4626
|
timeout: 3e3
|
|
@@ -4346,7 +4653,7 @@ function detectGitPatchScripts(projectRoot) {
|
|
|
4346
4653
|
}
|
|
4347
4654
|
}
|
|
4348
4655
|
}
|
|
4349
|
-
const deletedStdout =
|
|
4656
|
+
const deletedStdout = execSync5("git diff --name-only --diff-filter=D HEAD", {
|
|
4350
4657
|
cwd: projectRoot,
|
|
4351
4658
|
encoding: "utf-8",
|
|
4352
4659
|
timeout: 3e3
|
|
@@ -4416,7 +4723,7 @@ function detectAllAntiPatterns() {
|
|
|
4416
4723
|
// src/engine/contextSnapshot.ts
|
|
4417
4724
|
import fs12 from "fs";
|
|
4418
4725
|
import path12 from "path";
|
|
4419
|
-
import { execSync as
|
|
4726
|
+
import { execSync as execSync6 } from "child_process";
|
|
4420
4727
|
var SNAPSHOT_DIR = "context-snapshots";
|
|
4421
4728
|
function snapshotDir() {
|
|
4422
4729
|
return path12.join(getProjectRoot(), ".kuma", SNAPSHOT_DIR);
|
|
@@ -4505,7 +4812,7 @@ function formatSnapshot(snapshot) {
|
|
|
4505
4812
|
function getGitDiffStat2() {
|
|
4506
4813
|
try {
|
|
4507
4814
|
const root = getProjectRoot();
|
|
4508
|
-
const stdout =
|
|
4815
|
+
const stdout = execSync6("git diff --stat", {
|
|
4509
4816
|
cwd: root,
|
|
4510
4817
|
encoding: "utf-8",
|
|
4511
4818
|
timeout: 3e3
|
|
@@ -5804,7 +6111,7 @@ function createSchema(db) {
|
|
|
5804
6111
|
}
|
|
5805
6112
|
|
|
5806
6113
|
// src/engine/kumaSelfHeal.ts
|
|
5807
|
-
import { execSync as
|
|
6114
|
+
import { execSync as execSync7 } from "child_process";
|
|
5808
6115
|
import fs19 from "fs";
|
|
5809
6116
|
import path19 from "path";
|
|
5810
6117
|
import crypto from "crypto";
|
|
@@ -5841,7 +6148,7 @@ function findByFingerprint(fingerprint, oldName) {
|
|
|
5841
6148
|
try {
|
|
5842
6149
|
const root = getProjectRoot();
|
|
5843
6150
|
const ext = path19.extname(oldName);
|
|
5844
|
-
const result =
|
|
6151
|
+
const result = execSync7(
|
|
5845
6152
|
`find . -name "*${ext}" -type f -not -path "./node_modules/*" -not -path "./.git/*" -not -path "./dist/*" -not -path "./build/*" 2>/dev/null | head -200`,
|
|
5846
6153
|
{ cwd: root, encoding: "utf-8", maxBuffer: 1024 * 1024, timeout: 1e4 }
|
|
5847
6154
|
).trim();
|
|
@@ -5909,7 +6216,7 @@ async function detectStaleNodes() {
|
|
|
5909
6216
|
function findRenamedPath(oldPath) {
|
|
5910
6217
|
try {
|
|
5911
6218
|
const root = getProjectRoot();
|
|
5912
|
-
const output =
|
|
6219
|
+
const output = execSync7(
|
|
5913
6220
|
`git log --follow --diff-filter=R --name-only --format="" -1 -- "${oldPath}"`,
|
|
5914
6221
|
{ cwd: root, encoding: "utf-8", maxBuffer: 1024 * 1024, timeout: 5e3 }
|
|
5915
6222
|
).trim();
|
|
@@ -7313,11 +7620,11 @@ Cannot find definition.`;
|
|
|
7313
7620
|
}
|
|
7314
7621
|
|
|
7315
7622
|
// src/engine/kumaTimeMachine.ts
|
|
7316
|
-
import { execSync as
|
|
7623
|
+
import { execSync as execSync8 } from "child_process";
|
|
7317
7624
|
function getBlameForFile(filePath) {
|
|
7318
7625
|
const root = getProjectRoot();
|
|
7319
7626
|
try {
|
|
7320
|
-
const output =
|
|
7627
|
+
const output = execSync8(
|
|
7321
7628
|
`git blame --line-porcelain -- "${filePath}"`,
|
|
7322
7629
|
{ cwd: root, encoding: "utf-8", maxBuffer: 2 * 1024 * 1024, timeout: 1e4 }
|
|
7323
7630
|
);
|
|
@@ -7363,7 +7670,7 @@ function getBlameForFile(filePath) {
|
|
|
7363
7670
|
function getFileHistory(filePath, maxCount = 20) {
|
|
7364
7671
|
const root = getProjectRoot();
|
|
7365
7672
|
try {
|
|
7366
|
-
const output =
|
|
7673
|
+
const output = execSync8(
|
|
7367
7674
|
`git log --follow --format="%H||%an||%ai||%s" --shortstat -- "${filePath}"`,
|
|
7368
7675
|
{ cwd: root, encoding: "utf-8", maxBuffer: 2 * 1024 * 1024, timeout: 1e4 }
|
|
7369
7676
|
);
|
|
@@ -7398,7 +7705,7 @@ async function getSymbolTimeline(symbolName, filePath, symbolType = "function")
|
|
|
7398
7705
|
const blame = getBlameForFile(filePath);
|
|
7399
7706
|
const pattern = getSymbolPattern(symbolName, symbolType);
|
|
7400
7707
|
const root = getProjectRoot();
|
|
7401
|
-
const content =
|
|
7708
|
+
const content = execSync8(`git show HEAD:"${filePath}"`, {
|
|
7402
7709
|
cwd: root,
|
|
7403
7710
|
encoding: "utf-8",
|
|
7404
7711
|
maxBuffer: 2 * 1024 * 1024,
|
|
@@ -7425,7 +7732,7 @@ async function getSymbolTimeline(symbolName, filePath, symbolType = "function")
|
|
|
7425
7732
|
const entries = [];
|
|
7426
7733
|
for (const [hash, data] of commitMap) {
|
|
7427
7734
|
try {
|
|
7428
|
-
const msg =
|
|
7735
|
+
const msg = execSync8(
|
|
7429
7736
|
`git log -1 --format="%s" ${hash}`,
|
|
7430
7737
|
{ cwd: root, encoding: "utf-8", maxBuffer: 1024 * 1024, timeout: 3e3 }
|
|
7431
7738
|
).trim();
|
|
@@ -8128,7 +8435,7 @@ function formatDecisionTemplate() {
|
|
|
8128
8435
|
}
|
|
8129
8436
|
|
|
8130
8437
|
// src/engine/kumaContextEngine.ts
|
|
8131
|
-
import { execSync as
|
|
8438
|
+
import { execSync as execSync9 } from "child_process";
|
|
8132
8439
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
8133
8440
|
"the",
|
|
8134
8441
|
"this",
|
|
@@ -8245,7 +8552,7 @@ async function searchNodesFts(db, terms) {
|
|
|
8245
8552
|
function getFileRecencySeconds(filePath) {
|
|
8246
8553
|
try {
|
|
8247
8554
|
const escapedPath = filePath.replace(/"/g, '\\"');
|
|
8248
|
-
const result =
|
|
8555
|
+
const result = execSync9(
|
|
8249
8556
|
'git log -1 --format=%ct -- "' + escapedPath + '" 2>/dev/null || echo 0',
|
|
8250
8557
|
{ encoding: "utf-8", timeout: 2e3 }
|
|
8251
8558
|
);
|
|
@@ -11061,7 +11368,7 @@ function formatCollectivePatterns(patterns) {
|
|
|
11061
11368
|
}
|
|
11062
11369
|
|
|
11063
11370
|
// src/engine/kumaMarketplace.ts
|
|
11064
|
-
import { execSync as
|
|
11371
|
+
import { execSync as execSync10 } from "child_process";
|
|
11065
11372
|
import fs27 from "fs";
|
|
11066
11373
|
import path28 from "path";
|
|
11067
11374
|
var BUILT_IN_TEMPLATES = [
|
|
@@ -11447,7 +11754,7 @@ async function tryInstallFromNpm(templateId) {
|
|
|
11447
11754
|
try {
|
|
11448
11755
|
const npmPackage = `@kuma-templates/${name}`;
|
|
11449
11756
|
const root = getProjectRoot();
|
|
11450
|
-
|
|
11757
|
+
execSync10(`npm install --no-save ${npmPackage}`, {
|
|
11451
11758
|
cwd: root,
|
|
11452
11759
|
encoding: "utf-8",
|
|
11453
11760
|
timeout: 3e4,
|
|
@@ -11842,10 +12149,17 @@ async function handleInit(action, params) {
|
|
|
11842
12149
|
}
|
|
11843
12150
|
async function handleCore(action, params) {
|
|
11844
12151
|
switch (action) {
|
|
11845
|
-
case "grep":
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
12152
|
+
case "grep": {
|
|
12153
|
+
const p = { ...params, outputMode: params.grepOutputMode || params.outputMode || "rich" };
|
|
12154
|
+
return await handleSmartGrep(p);
|
|
12155
|
+
}
|
|
12156
|
+
case "read": {
|
|
12157
|
+
const readParams = { ...params, outputMode: params.readOutputMode || params.outputMode || "rich" };
|
|
12158
|
+
if (params.filePaths) {
|
|
12159
|
+
readParams.files = params.filePaths;
|
|
12160
|
+
}
|
|
12161
|
+
return await handleSmartFilePicker(readParams);
|
|
12162
|
+
}
|
|
11849
12163
|
case "edit":
|
|
11850
12164
|
return await handlePreciseDiffEditor(params);
|
|
11851
12165
|
case "batch":
|
|
@@ -12242,14 +12556,21 @@ function registerAllTools(server) {
|
|
|
12242
12556
|
action: z.enum(["grep", "read", "edit", "batch", "lsp"]).describe("Action: grep=search code, read=open file, edit=edit with safety, batch=create files, lsp=semantic analysis"),
|
|
12243
12557
|
// grep params
|
|
12244
12558
|
query: z.string().optional().describe("Regex pattern for grep action"),
|
|
12559
|
+
queries: z.array(z.string()).optional().describe("Multiple regex patterns (OR'd together, single pass)"),
|
|
12245
12560
|
targetFolder: z.string().optional().describe("Target folder for grep"),
|
|
12246
12561
|
maxResults: z.number().min(1).max(100).optional().default(30).describe("Max results"),
|
|
12247
12562
|
extensions: z.array(z.string()).optional().describe("File extensions filter"),
|
|
12563
|
+
contextLines: z.number().min(0).max(20).optional().default(1).describe("Context lines around match (grep) or jump-to-line (read)"),
|
|
12564
|
+
filenamesOnly: z.boolean().optional().default(false).describe("Only show matching filenames (like grep -l)"),
|
|
12565
|
+
countOnly: z.boolean().optional().default(false).describe("Only show match count per file (like grep -c)"),
|
|
12566
|
+
grepOutputMode: z.enum(["rich", "raw", "json"]).optional().default("rich").describe("Output format for grep: rich=emojis, raw=compact, json=parseable"),
|
|
12248
12567
|
// read params
|
|
12249
12568
|
filePath: z.string().optional().describe("File path for read/edit/batch actions"),
|
|
12569
|
+
filePaths: z.array(z.string()).optional().describe("Multiple file paths to read in one call"),
|
|
12250
12570
|
startLine: z.number().min(1).optional().describe("Start line (1-indexed) for read"),
|
|
12251
12571
|
endLine: z.number().min(1).optional().describe("End line (1-indexed) for read"),
|
|
12252
12572
|
chunkStrategy: z.enum(["full", "smart", "outline"]).optional().default("smart").describe("Read strategy"),
|
|
12573
|
+
readOutputMode: z.enum(["rich", "raw"]).optional().default("rich").describe("Output format for read: rich=line numbers, raw=content only"),
|
|
12253
12574
|
// edit params
|
|
12254
12575
|
edits: z.array(z.object({
|
|
12255
12576
|
searchBlock: z.string().min(1).describe("Code to replace"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plumpslabs/kuma",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.6",
|
|
4
4
|
"description": "Zero-setup safety toolkit for AI coding agents. MCP server with code review, git analysis, file editing, session memory, and static analysis — works with Claude Code, Cursor, Gemini CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|