@plumpslabs/kuma 2.2.7 → 2.2.8

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 +88 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -104,12 +104,89 @@ function isRipgrepAvailable() {
104
104
  function quotePath(p) {
105
105
  return `"${p.replace(/"/g, '\\"')}"`;
106
106
  }
107
+ async function searchInSingleFile(filePath, patterns, opts) {
108
+ const { maxResults, contextLines, filenamesOnly, countOnly, outputMode, compact } = opts;
109
+ const validation = validateFilePath(filePath);
110
+ if (!validation.valid) {
111
+ return compact ? `ERR:${filePath} - invalid path` : `Error: ${validation.error.message}`;
112
+ }
113
+ const resolvedPath = validation.resolvedPath;
114
+ if (!fs.existsSync(resolvedPath)) {
115
+ return compact ? `ERR:not found ${filePath}` : `Error: File not found: ${filePath}`;
116
+ }
117
+ const stat = fs.statSync(resolvedPath);
118
+ if (stat.size > 5e5) {
119
+ return compact ? `ERR:too large ${filePath}` : `Error: File too large (${(stat.size / 1024).toFixed(0)}KB). Use a more specific query.`;
120
+ }
121
+ if (isBinaryFile(resolvedPath)) {
122
+ return compact ? `ERR:binary ${filePath}` : `Error: Cannot search binary file: ${filePath}`;
123
+ }
124
+ const projectRoot = getProjectRoot();
125
+ const relativePath = resolvedPath.startsWith(projectRoot + "/") ? resolvedPath.slice(projectRoot.length + 1) : resolvedPath;
126
+ try {
127
+ const content = fs.readFileSync(resolvedPath, "utf-8");
128
+ const lines = content.split("\n");
129
+ const regex = createCombinedRegex(patterns);
130
+ const results = [];
131
+ const maxFileResults = Math.min(maxResults, 100);
132
+ let matchCount = 0;
133
+ for (let i = 0; i < lines.length; i++) {
134
+ if (results.length >= maxFileResults) break;
135
+ if (regex.test(lines[i])) {
136
+ matchCount++;
137
+ if (filenamesOnly) {
138
+ return relativePath;
139
+ }
140
+ if (countOnly) continue;
141
+ const ctxLines = [];
142
+ const startCtx = Math.max(0, i - contextLines);
143
+ const endCtx = Math.min(lines.length - 1, i + contextLines);
144
+ for (let ci = startCtx; ci <= endCtx; ci++) {
145
+ const prefix = ci === i ? ">" : " ";
146
+ ctxLines.push(`${prefix}${ci + 1}: ${lines[ci].substring(0, 200)}`);
147
+ }
148
+ results.push({
149
+ file: relativePath,
150
+ line: i + 1,
151
+ content: ctxLines.join("\n")
152
+ });
153
+ }
154
+ }
155
+ if (countOnly) {
156
+ const counts = `${filePath}:${matchCount}`;
157
+ if (outputMode === "json") return JSON.stringify({ [filePath]: matchCount });
158
+ if (compact) return counts;
159
+ return `\u{1F4CA} Count results for "${patterns[0]}":
160
+ ${counts}`;
161
+ }
162
+ if (matchCount === 0) {
163
+ if (outputMode === "json") return JSON.stringify({ query: patterns[0], matches: 0 });
164
+ if (compact) return `0:${patterns[0]}`;
165
+ return `\u{1F50D} No matches for "${patterns[0]}" in ${filePath}.`;
166
+ }
167
+ if (compact || outputMode === "raw") {
168
+ return results.map((r) => `${r.file}:${r.line}:${r.content.split("\n")[0].replace(/^[>\s]+\d+:\s*/, "")}`).join("\n");
169
+ }
170
+ if (outputMode === "json") {
171
+ return JSON.stringify(results);
172
+ }
173
+ const header = `\u{1F50D} Smart Grep: "${patterns[0]}" \u2014 ${filePath}
174
+ \u{1F4C1} ${results.length} matches
175
+ `;
176
+ const body = results.map((r, i) => `[${i + 1}] \u{1F4C4} ${r.file}:${r.line}
177
+ ${r.content}`).join("\n");
178
+ return limitLines(`${header}${body}`, 150);
179
+ } catch (err) {
180
+ return compact ? `ERR:reading ${filePath}` : `Error reading file "${filePath}": ${err instanceof Error ? err.message : String(err)}`;
181
+ }
182
+ }
107
183
  async function handleSmartGrep(params) {
108
184
  const {
109
185
  query,
110
186
  queries,
111
187
  targetFolder,
112
188
  maxResults = 30,
189
+ filePath,
113
190
  extensions,
114
191
  contextLines = 1,
115
192
  filenamesOnly = false,
@@ -121,7 +198,7 @@ async function handleSmartGrep(params) {
121
198
  if (patterns.length === 0 || patterns[0].length < 1) {
122
199
  return compact ? "ERR:query required" : "Error: 'query' or 'queries' parameter is required.";
123
200
  }
124
- const cacheKey = `${patterns.join("||")}:${targetFolder ?? "root"}:${maxResults}:${contextLines}:${filenamesOnly}:${countOnly}:${outputMode}:${extensions ? extensions.join(",") : ""}`;
201
+ const cacheKey = `${patterns.join("||")}:${filePath ?? ""}:${targetFolder ?? "root"}:${maxResults}:${contextLines}:${filenamesOnly}:${countOnly}:${outputMode}:${extensions ? extensions.join(",") : ""}`;
125
202
  const cached = grepCache.get(cacheKey);
126
203
  if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
127
204
  sessionMemory.recordToolCall("smart_grep", { query: patterns[0], cached: true });
@@ -129,7 +206,16 @@ async function handleSmartGrep(params) {
129
206
  }
130
207
  const projectRoot = getProjectRoot();
131
208
  let result;
132
- if (isRipgrepAvailable()) {
209
+ if (filePath) {
210
+ result = await searchInSingleFile(filePath, patterns, {
211
+ maxResults,
212
+ contextLines,
213
+ filenamesOnly,
214
+ countOnly,
215
+ outputMode,
216
+ compact
217
+ });
218
+ } else if (isRipgrepAvailable()) {
133
219
  result = await tryRipgrep(patterns, {
134
220
  projectRoot,
135
221
  targetFolder,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumpslabs/kuma",
3
- "version": "2.2.7",
3
+ "version": "2.2.8",
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",