@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.
Files changed (2) hide show
  1. package/dist/index.js +456 -135
  2. 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 { query, targetFolder, maxResults = 30, extensions } = params;
91
- if (!query || query.length < 1) {
92
- return "Error: 'query' parameter is required.";
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 = `${query}:${targetFolder ?? "root"}:${maxResults}:${extensions ? extensions.join(",") : ""}`;
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 searchPattern = targetFolder ? path.join(targetFolder, "**/*").replace(/\\/g, "/") : "**/*";
103
- const targetDepth = targetFolder ? targetFolder.split("/").filter(Boolean).length : 0;
104
- const maxDepth = targetFolder ? Math.min(targetDepth + 5, 20) : 10;
105
- let entries = await fg(searchPattern, {
106
- cwd: projectRoot,
107
- ignore: IGNORE_PATTERNS,
108
- onlyFiles: true,
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 (extensions && extensions.length > 0) {
115
- const normalizedExts = extensions.map(
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
- if (entries.length === 0) {
124
- const msg = `No files found${targetFolder ? ` in folder "${targetFolder}"` : ""}${extensions ? ` with extensions [${extensions.join(", ")}]` : ""}.`;
125
- grepCache.set(cacheKey, { results: msg, timestamp: Date.now() });
126
- return msg;
205
+ return formatRipgrepOutput(output, patterns, opts);
206
+ } catch (err) {
207
+ if (err.status === 1) {
208
+ return formatEmpty(patterns[0], opts);
127
209
  }
128
- const regex = createRegex(query);
129
- const results = [];
130
- let filesScanned = 0;
131
- for (const entry of entries) {
132
- if (results.length >= maxResults) break;
133
- filesScanned++;
134
- try {
135
- const fullPath = path.join(projectRoot, entry);
136
- const stat = fs.statSync(fullPath);
137
- if (stat.size > 5e5) continue;
138
- if (isBinaryFile(fullPath)) continue;
139
- const content = fs.readFileSync(fullPath, "utf-8");
140
- const lines = content.split("\n");
141
- for (let i = 0; i < lines.length; i++) {
142
- if (results.length >= maxResults) break;
143
- const line = lines[i];
144
- if (regex.test(line)) {
145
- const contextLines = [];
146
- if (i > 0)
147
- contextLines.push(`${i}: ${lines[i - 1].substring(0, 200)}`);
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
- const formatted = formatResults(results, query, filesScanned);
163
- grepCache.set(cacheKey, { results: formatted, timestamp: Date.now() });
164
- sessionMemory.recordToolCall("smart_grep", {
165
- query,
166
- matchCount: results.length,
167
- filesScanned
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
- sessionMemory.addSearchResult(
170
- query,
171
- results.map((r) => r.file)
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
- return formatted;
174
- } catch (err) {
175
- return `Error searching "${query}": ${err instanceof Error ? err.message : String(err)}`;
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 lines = [
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
- const filePath = r.file;
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(lines.join("\n"), 150);
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 { filePath, startLine, endLine, chunkStrategy = "smart" } = params;
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 "Error: " + validation.error.message;
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 = "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.";
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 'Error: File not found: "' + filePath + '".\n' + suggestion;
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 "Error: File too large (" + (stat.size / 1024 / 1024).toFixed(1) + "MB). Max 1MB.\nUse smart_grep to find specific content instead.";
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 'Error reading file "' + filePath + '": ' + (err instanceof Error ? err.message : String(err));
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
- "File: " + filePath,
287
- totalLines + " total lines",
288
- truncated ? "Showing " + lines.length + " lines (file >" + CHUNK_THRESHOLD + " lines). Use startLine/endLine for a specific range." : "",
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, " ") + " | " + line;
603
+ return `${String(lineNum).padStart(4, " ")} | ${line}`;
294
604
  }).join("\n");
295
- const full = header + "\n" + body;
296
- return truncateToTokenLimit(full, 2e3);
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
- "File: " + filePath,
315
- totalLines + " total lines (OUTLINE MODE - signatures and imports only)",
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) => " " + l.substring(0, 150)),
319
- importLines.length > 30 ? " ...and " + (importLines.length - 30) + " more imports" : "",
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) => " [L" + e.line + "] " + e.text),
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: " ... " + (tailStart - headerEnd) + " lines hidden ..."
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
- "File: " + filePath,
358
- totalLines + " total lines (SMART MODE - header + signatures + tail)",
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, " ") + " | " + s.text;
674
+ return `${String(s.line).padStart(4, " ")} | ${s.text}`;
366
675
  }).join("\n");
367
- const full = header + "\n" + body;
368
- return truncateToTokenLimit(full, 3e3);
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: execSync10 } = await import("child_process");
1199
+ const { execSync: execSync11 } = await import("child_process");
893
1200
  const root = getProjectRoot();
894
1201
  if (version === "list") {
895
- const log = execSync10("git log --oneline -20", { cwd: root, encoding: "utf-8" });
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 = execSync10("git status --porcelain", { cwd: root, encoding: "utf-8" }).trim();
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
- execSync10(`git checkout ${target} -- "${filePath}"`, { cwd: root, encoding: "utf-8" });
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
- execSync10(`git checkout ${target}`, { cwd: root, encoding: "utf-8" });
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 = execSync("git status --porcelain", {
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 execSync2 } from "child_process";
3456
+ import { execSync as execSync3 } from "child_process";
3150
3457
  function runGitCommand(command) {
3151
3458
  const root = getProjectRoot();
3152
- return execSync2(command, {
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 execSync3 } from "child_process";
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 execSync3("git diff --stat", {
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 execSync4 } from "child_process";
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 = execSync4("git status --porcelain", {
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 = execSync4("git diff --name-only --diff-filter=D HEAD", {
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 execSync5 } from "child_process";
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 = execSync5("git diff --stat", {
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 execSync6 } from "child_process";
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 = execSync6(
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 = execSync6(
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 execSync7 } from "child_process";
7623
+ import { execSync as execSync8 } from "child_process";
7317
7624
  function getBlameForFile(filePath) {
7318
7625
  const root = getProjectRoot();
7319
7626
  try {
7320
- const output = execSync7(
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 = execSync7(
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 = execSync7(`git show HEAD:"${filePath}"`, {
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 = execSync7(
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 execSync8 } from "child_process";
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 = execSync8(
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 execSync9 } from "child_process";
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
- execSync9(`npm install --no-save ${npmPackage}`, {
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
- return await handleSmartGrep(params);
11847
- case "read":
11848
- return await handleSmartFilePicker(params);
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.4",
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",