grepmax 0.7.4 → 0.7.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.
@@ -61,6 +61,7 @@ const searcher_1 = require("../lib/search/searcher");
61
61
  const retriever_1 = require("../lib/skeleton/retriever");
62
62
  const skeletonizer_1 = require("../lib/skeleton/skeletonizer");
63
63
  const vector_db_1 = require("../lib/store/vector-db");
64
+ const file_utils_1 = require("../lib/utils/file-utils");
64
65
  const filter_builder_1 = require("../lib/utils/filter-builder");
65
66
  const project_registry_1 = require("../lib/utils/project-registry");
66
67
  const project_root_1 = require("../lib/utils/project-root");
@@ -103,6 +104,14 @@ const TOOLS = [
103
104
  type: "number",
104
105
  description: "Max results per file (default: no cap). Useful to get diversity across files.",
105
106
  },
107
+ file: {
108
+ type: "string",
109
+ description: "Filter to files matching this name (e.g. 'syncer.ts'). Matches the filename, not the full path.",
110
+ },
111
+ exclude: {
112
+ type: "string",
113
+ description: "Exclude files under this path prefix (e.g. 'tests/' or 'dist/').",
114
+ },
106
115
  },
107
116
  required: ["query"],
108
117
  },
@@ -133,19 +142,31 @@ const TOOLS = [
133
142
  type: "number",
134
143
  description: "Max results per file (default: no cap).",
135
144
  },
145
+ file: {
146
+ type: "string",
147
+ description: "Filter to files matching this name (e.g. 'syncer.ts').",
148
+ },
149
+ exclude: {
150
+ type: "string",
151
+ description: "Exclude files under this path prefix (e.g. 'tests/').",
152
+ },
136
153
  },
137
154
  required: ["query"],
138
155
  },
139
156
  },
140
157
  {
141
158
  name: "code_skeleton",
142
- description: "Show the structure of a source fileall function/class/method signatures with bodies collapsed. Useful for understanding large files without reading every line. Returns ~4x fewer tokens than the full file.",
159
+ description: "Show the structure of source files — signatures with bodies collapsed (~4x fewer tokens). Accepts a file path, a directory path, or comma-separated file paths.",
143
160
  inputSchema: {
144
161
  type: "object",
145
162
  properties: {
146
163
  target: {
147
164
  type: "string",
148
- description: "File path relative to project root (e.g. 'src/services/booking.ts')",
165
+ description: "File path, directory path (e.g. 'src/lib/search/'), or comma-separated files (e.g. 'src/a.ts,src/b.ts'). Relative to project root.",
166
+ },
167
+ limit: {
168
+ type: "number",
169
+ description: "Max files for directory mode (default 10, max 20). Ignored for single files.",
149
170
  },
150
171
  },
151
172
  required: ["target"],
@@ -422,7 +443,14 @@ exports.mcp = new commander_1.Command("mcp")
422
443
  pathPrefix += "/";
423
444
  }
424
445
  }
425
- const result = yield searcher.search(query, limit, { rerank: true }, undefined, pathPrefix);
446
+ const filters = {};
447
+ if (typeof args.file === "string" && args.file) {
448
+ filters.file = args.file;
449
+ }
450
+ if (typeof args.exclude === "string" && args.exclude) {
451
+ filters.exclude = args.exclude;
452
+ }
453
+ const result = yield searcher.search(query, limit, { rerank: true }, Object.keys(filters).length > 0 ? filters : undefined, pathPrefix);
426
454
  if (!result.data || result.data.length === 0) {
427
455
  return ok("No matches found.");
428
456
  }
@@ -510,36 +538,71 @@ exports.mcp = new commander_1.Command("mcp")
510
538
  const target = String(args.target || "");
511
539
  if (!target)
512
540
  return err("Missing required parameter: target");
513
- const absPath = path.resolve(projectRoot, target);
514
- if (!fs.existsSync(absPath)) {
515
- return err(`File not found: ${target}`);
541
+ const fileLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 20);
542
+ // Determine targets: comma-separated, directory, or single file
543
+ let targets;
544
+ if (target.includes(",")) {
545
+ targets = target
546
+ .split(",")
547
+ .map((t) => t.trim())
548
+ .filter(Boolean);
516
549
  }
517
- // Try cached skeleton first (stored with absolute path)
518
- try {
519
- const db = getVectorDb();
520
- const cached = yield (0, retriever_1.getStoredSkeleton)(db, absPath);
521
- if (cached) {
522
- const tokens = Math.ceil(cached.length / 4);
523
- return ok(`// ${target} (~${tokens} tokens)\n\n${cached}`);
550
+ else {
551
+ const absPath = path.resolve(projectRoot, target);
552
+ if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) {
553
+ const entries = fs.readdirSync(absPath, { withFileTypes: true });
554
+ targets = entries
555
+ .filter((e) => e.isFile() &&
556
+ (0, file_utils_1.isIndexableFile)(path.join(absPath, e.name)))
557
+ .map((e) => path.relative(projectRoot, path.join(absPath, e.name)))
558
+ .slice(0, fileLimit);
559
+ if (targets.length === 0) {
560
+ return err(`No indexable files found in ${target}`);
561
+ }
524
562
  }
525
- }
526
- catch (_a) {
527
- // Index may not exist yet — fall through to live generation
528
- }
529
- // Generate skeleton from file
530
- try {
531
- const content = fs.readFileSync(absPath, "utf-8");
532
- const skel = yield getSkeletonizer();
533
- const result = yield skel.skeletonizeFile(absPath, content);
534
- if (!result.success && result.error) {
535
- return err(`Skeleton generation failed: ${result.error}`);
563
+ else {
564
+ targets = [target];
536
565
  }
537
- return ok(`// ${target} (~${result.tokenEstimate} tokens)\n\n${result.skeleton}`);
538
566
  }
539
- catch (e) {
540
- const msg = e instanceof Error ? e.message : String(e);
541
- return err(`Skeleton failed: ${msg}`);
567
+ // Generate skeletons for all targets
568
+ const parts = [];
569
+ const skel = yield getSkeletonizer();
570
+ for (const t of targets) {
571
+ const absPath = path.resolve(projectRoot, t);
572
+ if (!fs.existsSync(absPath)) {
573
+ parts.push(`// ${t} — file not found`);
574
+ continue;
575
+ }
576
+ // Try cached skeleton first
577
+ try {
578
+ const db = getVectorDb();
579
+ const cached = yield (0, retriever_1.getStoredSkeleton)(db, absPath);
580
+ if (cached) {
581
+ const tokens = Math.ceil(cached.length / 4);
582
+ parts.push(`// ${t} (~${tokens} tokens)\n\n${cached}`);
583
+ continue;
584
+ }
585
+ }
586
+ catch (_a) {
587
+ // Index may not exist yet — fall through to live generation
588
+ }
589
+ // Generate live
590
+ try {
591
+ const content = fs.readFileSync(absPath, "utf-8");
592
+ const result = yield skel.skeletonizeFile(absPath, content);
593
+ if (result.success) {
594
+ parts.push(`// ${t} (~${result.tokenEstimate} tokens)\n\n${result.skeleton}`);
595
+ }
596
+ else {
597
+ parts.push(`// ${t} — skeleton failed: ${result.error}`);
598
+ }
599
+ }
600
+ catch (e) {
601
+ const msg = e instanceof Error ? e.message : String(e);
602
+ parts.push(`// ${t} — error: ${msg}`);
603
+ }
542
604
  }
605
+ return ok(parts.join("\n\n---\n\n"));
543
606
  });
544
607
  }
545
608
  function handleTraceCalls(args) {
@@ -570,11 +633,23 @@ exports.mcp = new commander_1.Command("mcp")
570
633
  else {
571
634
  lines.push("Callers: none");
572
635
  }
573
- // Callees (cap at 15)
636
+ // Callees with file paths
574
637
  if (graph.callees.length > 0) {
575
- const capped = graph.callees.slice(0, 15);
576
- const suffix = graph.callees.length > 15 ? ` (+${graph.callees.length - 15} more)` : "";
577
- lines.push(`Calls: ${capped.join(", ")}${suffix}`);
638
+ lines.push("Calls:");
639
+ for (const callee of graph.callees.slice(0, 15)) {
640
+ if (callee.file) {
641
+ const rel = callee.file.startsWith(projectRoot)
642
+ ? callee.file.slice(projectRoot.length + 1)
643
+ : callee.file;
644
+ lines.push(` -> ${callee.symbol} ${rel}:${callee.line + 1}`);
645
+ }
646
+ else {
647
+ lines.push(` -> ${callee.symbol} (not indexed)`);
648
+ }
649
+ }
650
+ if (graph.callees.length > 15) {
651
+ lines.push(` (+${graph.callees.length - 15} more)`);
652
+ }
578
653
  }
579
654
  else {
580
655
  lines.push("Calls: none");
@@ -695,7 +770,23 @@ exports.mcp = new commander_1.Command("mcp")
695
770
  indexingLine,
696
771
  "",
697
772
  "Indexed directories:",
698
- ...projects.map((p) => { var _a; return ` ${p.name}\t${p.root}\t${(_a = p.lastIndexed) !== null && _a !== void 0 ? _a : "unknown"}`; }),
773
+ ...(yield Promise.all(projects.map((p) => __awaiter(this, void 0, void 0, function* () {
774
+ var _a, _b;
775
+ const prefix = p.root.endsWith("/") ? p.root : `${p.root}/`;
776
+ try {
777
+ const table = yield db.ensureTable();
778
+ const rows = yield table
779
+ .query()
780
+ .select(["id"])
781
+ .where(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
782
+ .limit(100000)
783
+ .toArray();
784
+ return ` ${p.name}\t${p.root}\t${(_a = p.lastIndexed) !== null && _a !== void 0 ? _a : "unknown"}\t(${rows.length} chunks)`;
785
+ }
786
+ catch (_c) {
787
+ return ` ${p.name}\t${p.root}\t${(_b = p.lastIndexed) !== null && _b !== void 0 ? _b : "unknown"}`;
788
+ }
789
+ })))),
699
790
  ].filter(Boolean);
700
791
  return ok(lines.join("\n"));
701
792
  }
@@ -69,9 +69,40 @@ class GraphBuilder {
69
69
  : null;
70
70
  // 2. Get Callers
71
71
  const callers = yield this.getCallers(symbol);
72
- // 3. Get Callees (from center)
73
- const callees = center ? center.calls : [];
74
- return { center, callers, callees };
72
+ // 3. Get Callees resolve each to a GraphNode with file:line
73
+ const calleeNames = center ? center.calls.slice(0, 15) : [];
74
+ const calleeNodes = [];
75
+ for (const name of calleeNames) {
76
+ const esc = (0, filter_builder_1.escapeSqlString)(name);
77
+ const rows = yield table
78
+ .query()
79
+ .where(`array_contains(defined_symbols, '${esc}')`)
80
+ .select([
81
+ "path",
82
+ "start_line",
83
+ "defined_symbols",
84
+ "referenced_symbols",
85
+ "role",
86
+ "parent_symbol",
87
+ "complexity",
88
+ ])
89
+ .limit(1)
90
+ .toArray();
91
+ if (rows.length > 0) {
92
+ calleeNodes.push(this.mapRowToNode(rows[0], name, "center"));
93
+ }
94
+ else {
95
+ calleeNodes.push({
96
+ symbol: name,
97
+ file: "",
98
+ line: 0,
99
+ role: "",
100
+ calls: [],
101
+ calledBy: [],
102
+ });
103
+ }
104
+ }
105
+ return { center, callers, callees: calleeNodes };
75
106
  });
76
107
  }
77
108
  mapRowToNode(row, targetSymbol, type) {
@@ -151,7 +151,12 @@ function formatTrace(graph) {
151
151
  if (graph.callees.length > 0) {
152
152
  lines.push(style.bold("Callees (What does this call?):"));
153
153
  graph.callees.forEach((callee) => {
154
- lines.push(` ${style.cyan("↓")} ${callee}`);
154
+ if (callee.file) {
155
+ lines.push(` ${style.cyan("↓")} ${style.green(callee.symbol)} ${style.dim(`(${callee.file}:${callee.line})`)}`);
156
+ }
157
+ else {
158
+ lines.push(` ${style.cyan("↓")} ${callee.symbol} ${style.dim("(not indexed)")}`);
159
+ }
155
160
  });
156
161
  }
157
162
  else {
@@ -277,6 +277,19 @@ class Searcher {
277
277
  if (pathPrefix) {
278
278
  whereClauseParts.push(`path LIKE '${(0, filter_builder_1.escapeSqlString)((0, filter_builder_1.normalizePath)(pathPrefix))}%'`);
279
279
  }
280
+ // Handle file name filter
281
+ const fileFilter = _filters === null || _filters === void 0 ? void 0 : _filters.file;
282
+ if (typeof fileFilter === "string" && fileFilter) {
283
+ whereClauseParts.push(`path LIKE '%/${(0, filter_builder_1.escapeSqlString)(fileFilter)}'`);
284
+ }
285
+ // Handle exclude filter
286
+ const excludeFilter = _filters === null || _filters === void 0 ? void 0 : _filters.exclude;
287
+ if (typeof excludeFilter === "string" && excludeFilter) {
288
+ const absExclude = pathPrefix
289
+ ? (0, filter_builder_1.normalizePath)(pathPrefix + excludeFilter)
290
+ : excludeFilter;
291
+ whereClauseParts.push(`path NOT LIKE '${(0, filter_builder_1.escapeSqlString)(absExclude)}%'`);
292
+ }
280
293
  // Handle --def (definition) filter
281
294
  const defFilter = _filters === null || _filters === void 0 ? void 0 : _filters.def;
282
295
  if (typeof defFilter === "string" && defFilter) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "author": "Robert Owens <robowens@me.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",
@@ -42,6 +42,8 @@ Parameters:
42
42
  - `detail` (optional): `"pointer"` (default) or `"code"`
43
43
  - `min_score` (optional): Filter by minimum relevance score (0-1)
44
44
  - `max_per_file` (optional): Cap results per file for diversity
45
+ - `file` (optional): Filter to files matching this name (e.g. "syncer.ts"). Matches filename, not full path.
46
+ - `exclude` (optional): Exclude files under this path prefix (e.g. "tests/" or "dist/")
45
47
 
46
48
  **When to use which mode:**
47
49
  - `pointer` — navigation, finding locations, understanding architecture
@@ -53,11 +55,12 @@ Search ALL indexed code across every directory. Same parameters as semantic_sear
53
55
  Use sparingly. Prefer `semantic_search` when you know which directory to search.
54
56
 
55
57
  ### code_skeleton
56
- File structure — signatures with bodies collapsed (~4x fewer tokens).
57
- - `target` (required): File path relative to project root
58
+ File or directory structure — signatures with bodies collapsed (~4x fewer tokens).
59
+ - `target` (required): File path, directory path (e.g. "src/lib/search/"), or comma-separated files
60
+ - `limit` (optional): Max files for directory mode (default 10, max 20)
58
61
 
59
62
  ### trace_calls
60
- Call graph — who calls a symbol and what it calls. Unscoped — follows calls across all indexed directories.
63
+ Call graph — who calls a symbol and what it calls. Callers and callees include file:line locations. Unscoped — follows calls across all indexed directories.
61
64
  - `symbol` (required): Function/method/class name
62
65
 
63
66
  ### list_symbols