grepmax 0.17.2 → 0.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/commands/context.js +118 -21
- package/dist/commands/mcp.js +305 -118
- package/dist/commands/search.js +40 -102
- package/dist/eval-graph-sanity.js +225 -0
- package/dist/eval-graph-spotcheck.js +83 -0
- package/dist/eval-graph-totals.js +131 -0
- package/dist/eval-oss.js +244 -0
- package/dist/eval.js +6 -1
- package/dist/lib/output/agent-search-formatter.js +163 -0
- package/dist/lib/search/pagerank.js +267 -0
- package/dist/lib/search/searcher.js +44 -4
- package/mlx-embed-server/server.py +24 -0
- package/package.json +3 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/skills/grepmax/SKILL.md +15 -5
package/README.md
CHANGED
|
@@ -64,6 +64,7 @@ gmax log src/lib/auth.ts # Git commit history for a path or symb
|
|
|
64
64
|
gmax test handleAuth # Find tests via reverse call graph
|
|
65
65
|
gmax impact handleAuth # Dependents + affected tests
|
|
66
66
|
gmax similar handleAuth # Find similar code patterns
|
|
67
|
+
gmax dead handleAuth # Unused-symbol check via call graph (DEAD / PUBLIC EXPORT / LIVE)
|
|
67
68
|
gmax context "auth system" --budget 4000 # Token-budgeted topic summary
|
|
68
69
|
```
|
|
69
70
|
|
|
@@ -121,6 +122,7 @@ Plugins auto-update when you run `npm install -g grepmax@latest` — no need to
|
|
|
121
122
|
| `trace_calls` | Call graph: importers, callers (multi-hop), callees with file:line. |
|
|
122
123
|
| `extract_symbol` | Complete function/class body by symbol name. |
|
|
123
124
|
| `peek_symbol` | Compact overview: signature + callers + callees. |
|
|
125
|
+
| `dead` | Unused-symbol check via call graph. Returns `DEAD`, `PUBLIC EXPORT`, or `LIVE` with caller count. |
|
|
124
126
|
| `list_symbols` | Indexed symbols with role and export status. |
|
|
125
127
|
| `index_status` | Index health: chunks, files, projects, watcher status. |
|
|
126
128
|
| `summarize_project` | Project overview: languages, structure, key symbols, entry points. |
|
|
@@ -277,6 +279,7 @@ fixtures/
|
|
|
277
279
|
| `GMAX_WORKER_THREADS` | Worker threads for embedding | 50% of cores |
|
|
278
280
|
| `GMAX_DEBUG` | Debug logging | Off |
|
|
279
281
|
| `GMAX_SUMMARIZER` | Enable summarizer auto-start (`1`) | Off |
|
|
282
|
+
| `GMAX_RERANK` | Enable ColBERT rerank (`1`) — off by default since v0.17.1 ([why](docs/known-limitations.md)) | Off |
|
|
280
283
|
|
|
281
284
|
## Troubleshooting
|
|
282
285
|
|
|
@@ -293,6 +296,20 @@ gmax watch stop && gmax watch --daemon -b # Restart daemon
|
|
|
293
296
|
|
|
294
297
|
See [CLAUDE.md](CLAUDE.md) for development setup, commands, and architecture details.
|
|
295
298
|
|
|
299
|
+
### Benchmarks
|
|
300
|
+
|
|
301
|
+
Two evaluation harnesses live in the repo. Both emit stable JSON via `:json` variants.
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
pnpm bench:recall # 97-case internal eval against gmax's own repo
|
|
305
|
+
pnpm bench:recall:json
|
|
306
|
+
pnpm bench:oss # P1 definition-lookup across express, lodash, platform (sverklo-bench fixtures)
|
|
307
|
+
pnpm bench:oss:json
|
|
308
|
+
GMAX_EVAL_RERANK=1 pnpm bench:oss # toggle ColBERT rerank
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The OSS bench requires the fixture repos to be indexed first — see [`docs/known-limitations.md`](docs/known-limitations.md) for the most recent rerank-on-vs-off comparison across 4 datasets / 131 cases.
|
|
312
|
+
|
|
296
313
|
## Attribution
|
|
297
314
|
|
|
298
315
|
grepmax is built upon the foundation of [mgrep](https://github.com/mixedbread-ai/mgrep) by MixedBread. See the [NOTICE](NOTICE) file for details.
|
package/dist/commands/context.js
CHANGED
|
@@ -44,18 +44,111 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.context = void 0;
|
|
46
46
|
const fs = __importStar(require("node:fs"));
|
|
47
|
+
const path = __importStar(require("node:path"));
|
|
47
48
|
const commander_1 = require("commander");
|
|
48
49
|
const searcher_1 = require("../lib/search/searcher");
|
|
49
50
|
const skeleton_1 = require("../lib/skeleton");
|
|
50
51
|
const vector_db_1 = require("../lib/store/vector-db");
|
|
51
|
-
const
|
|
52
|
+
const arrow_1 = require("../lib/utils/arrow");
|
|
52
53
|
const exit_1 = require("../lib/utils/exit");
|
|
54
|
+
const filter_builder_1 = require("../lib/utils/filter-builder");
|
|
53
55
|
const project_registry_1 = require("../lib/utils/project-registry");
|
|
54
56
|
const project_root_1 = require("../lib/utils/project-root");
|
|
55
|
-
const arrow_1 = require("../lib/utils/arrow");
|
|
56
57
|
function estimateTokens(text) {
|
|
57
58
|
return Math.ceil(text.length / 4);
|
|
58
59
|
}
|
|
60
|
+
function addSection(state, text, budget) {
|
|
61
|
+
const tokens = estimateTokens(text);
|
|
62
|
+
if (state.tokensUsed + tokens > budget)
|
|
63
|
+
return false;
|
|
64
|
+
state.sections.push(text);
|
|
65
|
+
state.tokensUsed += tokens;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
function relPath(projectRoot, p) {
|
|
69
|
+
return p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
|
|
70
|
+
}
|
|
71
|
+
function chunkPath(chunk) {
|
|
72
|
+
const metadata = chunk.metadata;
|
|
73
|
+
return String(chunk.path || (metadata === null || metadata === void 0 ? void 0 : metadata.path) || "");
|
|
74
|
+
}
|
|
75
|
+
function chunkStartLine(chunk) {
|
|
76
|
+
var _a, _b, _c, _d;
|
|
77
|
+
return Number((_d = (_b = (_a = chunk.start_line) !== null && _a !== void 0 ? _a : chunk.startLine) !== null && _b !== void 0 ? _b : (_c = chunk.generated_metadata) === null || _c === void 0 ? void 0 : _c.start_line) !== null && _d !== void 0 ? _d : 0);
|
|
78
|
+
}
|
|
79
|
+
function chunkEndLine(chunk) {
|
|
80
|
+
var _a, _b, _c, _d;
|
|
81
|
+
const start = chunkStartLine(chunk);
|
|
82
|
+
return Number((_d = (_b = (_a = chunk.end_line) !== null && _a !== void 0 ? _a : chunk.endLine) !== null && _b !== void 0 ? _b : (_c = chunk.generated_metadata) === null || _c === void 0 ? void 0 : _c.end_line) !== null && _d !== void 0 ? _d : start);
|
|
83
|
+
}
|
|
84
|
+
function resolveExistingPath(target, root, projectRoot) {
|
|
85
|
+
const candidates = [
|
|
86
|
+
path.isAbsolute(target) ? target : path.resolve(root, target),
|
|
87
|
+
path.resolve(projectRoot, target),
|
|
88
|
+
];
|
|
89
|
+
for (const candidate of candidates) {
|
|
90
|
+
if (fs.existsSync(candidate))
|
|
91
|
+
return candidate;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function renderPathContext(target, absPath, projectRoot, budget) {
|
|
96
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
const state = {
|
|
98
|
+
sections: [],
|
|
99
|
+
tokensUsed: 0,
|
|
100
|
+
};
|
|
101
|
+
const header = `=== Context: "${target}" ===`;
|
|
102
|
+
addSection(state, header, budget);
|
|
103
|
+
const stat = fs.statSync(absPath);
|
|
104
|
+
const targetSection = [
|
|
105
|
+
"\n## Target",
|
|
106
|
+
`${relPath(projectRoot, absPath)} [${stat.isDirectory() ? "directory" : "file"}]`,
|
|
107
|
+
].join("\n");
|
|
108
|
+
addSection(state, targetSection, budget);
|
|
109
|
+
if (stat.isDirectory()) {
|
|
110
|
+
const entries = fs
|
|
111
|
+
.readdirSync(absPath, { withFileTypes: true })
|
|
112
|
+
.filter((entry) => !entry.name.startsWith("."))
|
|
113
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
114
|
+
.slice(0, 40)
|
|
115
|
+
.map((entry) => `${entry.isDirectory() ? "dir " : "file"} ${entry.name}`);
|
|
116
|
+
if (entries.length > 0) {
|
|
117
|
+
addSection(state, ["\n## Directory Entries", ...entries].join("\n"), budget);
|
|
118
|
+
}
|
|
119
|
+
return state;
|
|
120
|
+
}
|
|
121
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
122
|
+
const skeletonizer = new skeleton_1.Skeletonizer();
|
|
123
|
+
yield skeletonizer.init();
|
|
124
|
+
if (skeletonizer.isSupported(absPath).supported) {
|
|
125
|
+
try {
|
|
126
|
+
const result = yield skeletonizer.skeletonizeFile(absPath, content);
|
|
127
|
+
if (result.success) {
|
|
128
|
+
addSection(state, [
|
|
129
|
+
"\n## File Structure",
|
|
130
|
+
`--- ${relPath(projectRoot, absPath)} (skeleton, ~${result.tokenEstimate} tokens) ---`,
|
|
131
|
+
result.skeleton,
|
|
132
|
+
].join("\n"), budget);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (_a) {
|
|
136
|
+
// Skeleton is a convenience in path mode; fall through to excerpt.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const lines = content.split("\n");
|
|
140
|
+
const excerptLines = lines.slice(0, Math.min(lines.length, 120));
|
|
141
|
+
const omitted = lines.length > excerptLines.length
|
|
142
|
+
? `\n... (+${lines.length - excerptLines.length} more lines)`
|
|
143
|
+
: "";
|
|
144
|
+
addSection(state, [
|
|
145
|
+
"\n## File Excerpt",
|
|
146
|
+
`--- ${relPath(projectRoot, absPath)}:1 ---`,
|
|
147
|
+
`${excerptLines.join("\n")}${omitted}`,
|
|
148
|
+
].join("\n"), budget);
|
|
149
|
+
return state;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
59
152
|
exports.context = new commander_1.Command("context")
|
|
60
153
|
.description("Generate a token-budgeted topic summary (search + skeleton + extract)")
|
|
61
154
|
.argument("<topic>", "Natural language topic or directory path")
|
|
@@ -64,7 +157,7 @@ exports.context = new commander_1.Command("context")
|
|
|
64
157
|
.option("--root <dir>", "Project root directory")
|
|
65
158
|
.option("--agent", "Compact output for AI agents", false)
|
|
66
159
|
.action((topic, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
67
|
-
var _a, _b, _c, _d, _e
|
|
160
|
+
var _a, _b, _c, _d, _e;
|
|
68
161
|
const budget = Number.parseInt(opts.budget || "4000", 10) || 4000;
|
|
69
162
|
const maxResults = Number.parseInt(opts.maxResults || "10", 10) || 10;
|
|
70
163
|
let vectorDb = null;
|
|
@@ -75,8 +168,14 @@ exports.context = new commander_1.Command("context")
|
|
|
75
168
|
const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
|
|
76
169
|
const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
|
|
77
170
|
vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
|
|
171
|
+
const pathTarget = resolveExistingPath(topic, root, projectRoot);
|
|
172
|
+
if (pathTarget) {
|
|
173
|
+
const rendered = yield renderPathContext(topic, pathTarget, projectRoot, budget);
|
|
174
|
+
rendered.sections.push(`\n(~${rendered.tokensUsed}/${budget} tokens used)`);
|
|
175
|
+
console.log(rendered.sections.join("\n"));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
78
178
|
const searcher = new searcher_1.Searcher(vectorDb);
|
|
79
|
-
const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
|
|
80
179
|
// Phase 1: Semantic search
|
|
81
180
|
const response = yield searcher.search(topic, maxResults, { rerank: true }, {}, projectRoot);
|
|
82
181
|
if (response.data.length === 0) {
|
|
@@ -94,11 +193,11 @@ exports.context = new commander_1.Command("context")
|
|
|
94
193
|
const entryPoints = orchestrators.length > 0 ? orchestrators : response.data.slice(0, 3);
|
|
95
194
|
const epSection = ["\n## Entry Points"];
|
|
96
195
|
for (const r of entryPoints.slice(0, 5)) {
|
|
97
|
-
const p =
|
|
98
|
-
const line =
|
|
99
|
-
const sym = (
|
|
196
|
+
const p = chunkPath(r);
|
|
197
|
+
const line = chunkStartLine(r);
|
|
198
|
+
const sym = (_c = (_b = (0, arrow_1.toArr)(r.defined_symbols)) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : "";
|
|
100
199
|
const role = String(r.role || "IMPLEMENTATION");
|
|
101
|
-
epSection.push(`${
|
|
200
|
+
epSection.push(`${relPath(projectRoot, p)}:${line + 1} ${sym} [${role}]`);
|
|
102
201
|
}
|
|
103
202
|
const epText = epSection.join("\n");
|
|
104
203
|
if (tokensUsed + estimateTokens(epText) <= budget) {
|
|
@@ -109,24 +208,24 @@ exports.context = new commander_1.Command("context")
|
|
|
109
208
|
const topChunks = entryPoints.slice(0, 3);
|
|
110
209
|
const bodySection = ["\n## Key Functions"];
|
|
111
210
|
for (const r of topChunks) {
|
|
112
|
-
const absP =
|
|
113
|
-
const startLine =
|
|
114
|
-
const endLine =
|
|
115
|
-
const sym = (
|
|
211
|
+
const absP = chunkPath(r);
|
|
212
|
+
const startLine = chunkStartLine(r);
|
|
213
|
+
const endLine = chunkEndLine(r);
|
|
214
|
+
const sym = (_e = (_d = (0, arrow_1.toArr)(r.defined_symbols)) === null || _d === void 0 ? void 0 : _d[0]) !== null && _e !== void 0 ? _e : "";
|
|
116
215
|
try {
|
|
117
216
|
const content = fs.readFileSync(absP, "utf-8");
|
|
118
217
|
const allLines = content.split("\n");
|
|
119
218
|
const body = allLines
|
|
120
219
|
.slice(startLine, Math.min(endLine + 1, allLines.length))
|
|
121
220
|
.join("\n");
|
|
122
|
-
const blob = `\n--- ${
|
|
221
|
+
const blob = `\n--- ${relPath(projectRoot, absP)}:${startLine + 1} ${sym} ---\n${body}`;
|
|
123
222
|
const blobTokens = estimateTokens(blob);
|
|
124
223
|
if (tokensUsed + blobTokens > budget)
|
|
125
224
|
break;
|
|
126
225
|
bodySection.push(blob);
|
|
127
226
|
tokensUsed += blobTokens;
|
|
128
227
|
}
|
|
129
|
-
catch (
|
|
228
|
+
catch (_f) {
|
|
130
229
|
// File not readable — skip
|
|
131
230
|
}
|
|
132
231
|
}
|
|
@@ -135,9 +234,7 @@ exports.context = new commander_1.Command("context")
|
|
|
135
234
|
}
|
|
136
235
|
// Phase 4: File skeletons for unique files
|
|
137
236
|
const uniqueFiles = [
|
|
138
|
-
...new Set(response.data
|
|
139
|
-
.map((r) => String(r.path || ""))
|
|
140
|
-
.filter(Boolean)),
|
|
237
|
+
...new Set(response.data.map((r) => chunkPath(r)).filter(Boolean)),
|
|
141
238
|
].slice(0, 5);
|
|
142
239
|
const skelSection = ["\n## File Structure"];
|
|
143
240
|
const skeletonizer = new skeleton_1.Skeletonizer();
|
|
@@ -150,14 +247,14 @@ exports.context = new commander_1.Command("context")
|
|
|
150
247
|
const result = yield skeletonizer.skeletonizeFile(absP, content);
|
|
151
248
|
if (!result.success)
|
|
152
249
|
continue;
|
|
153
|
-
const blob = `\n--- ${
|
|
250
|
+
const blob = `\n--- ${relPath(projectRoot, absP)} (skeleton, ~${result.tokenEstimate} tokens) ---\n${result.skeleton}`;
|
|
154
251
|
const blobTokens = estimateTokens(blob);
|
|
155
252
|
if (tokensUsed + blobTokens > budget)
|
|
156
253
|
break;
|
|
157
254
|
skelSection.push(blob);
|
|
158
255
|
tokensUsed += blobTokens;
|
|
159
256
|
}
|
|
160
|
-
catch (
|
|
257
|
+
catch (_g) {
|
|
161
258
|
// Skip unreadable files
|
|
162
259
|
}
|
|
163
260
|
}
|
|
@@ -195,7 +292,7 @@ exports.context = new commander_1.Command("context")
|
|
|
195
292
|
if (topRelated.length > 0) {
|
|
196
293
|
const relSection = ["\n## Related Files"];
|
|
197
294
|
for (const [p, count] of topRelated) {
|
|
198
|
-
relSection.push(`${
|
|
295
|
+
relSection.push(`${relPath(projectRoot, p)} — ${count} shared symbol${count > 1 ? "s" : ""}`);
|
|
199
296
|
}
|
|
200
297
|
const relText = relSection.join("\n");
|
|
201
298
|
if (tokensUsed + estimateTokens(relText) <= budget) {
|
|
@@ -218,7 +315,7 @@ exports.context = new commander_1.Command("context")
|
|
|
218
315
|
try {
|
|
219
316
|
yield vectorDb.close();
|
|
220
317
|
}
|
|
221
|
-
catch (
|
|
318
|
+
catch (_h) { }
|
|
222
319
|
}
|
|
223
320
|
yield (0, exit_1.gracefulExit)();
|
|
224
321
|
}
|