grepmax 0.17.20 → 0.17.21
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/commands/context.js
CHANGED
|
@@ -50,6 +50,7 @@ const searcher_1 = require("../lib/search/searcher");
|
|
|
50
50
|
const skeleton_1 = require("../lib/skeleton");
|
|
51
51
|
const vector_db_1 = require("../lib/store/vector-db");
|
|
52
52
|
const arrow_1 = require("../lib/utils/arrow");
|
|
53
|
+
const budget_pack_1 = require("../lib/utils/budget-pack");
|
|
53
54
|
const exit_1 = require("../lib/utils/exit");
|
|
54
55
|
const filter_builder_1 = require("../lib/utils/filter-builder");
|
|
55
56
|
const project_registry_1 = require("../lib/utils/project-registry");
|
|
@@ -157,7 +158,7 @@ exports.context = new commander_1.Command("context")
|
|
|
157
158
|
.option("--root <dir>", "Project root directory")
|
|
158
159
|
.option("--agent", "Compact output for AI agents", false)
|
|
159
160
|
.action((topic, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
160
|
-
var _a, _b, _c
|
|
161
|
+
var _a, _b, _c;
|
|
161
162
|
const budget = Number.parseInt(opts.budget || "4000", 10) || 4000;
|
|
162
163
|
const maxResults = Number.parseInt(opts.maxResults || "10", 10) || 10;
|
|
163
164
|
let vectorDb = null;
|
|
@@ -204,33 +205,45 @@ exports.context = new commander_1.Command("context")
|
|
|
204
205
|
sections.push(epText);
|
|
205
206
|
tokensUsed += estimateTokens(epText);
|
|
206
207
|
}
|
|
207
|
-
// Phase 3: Key function bodies (top 2-3 results)
|
|
208
|
+
// Phase 3: Key function bodies (top 2-3 results). Token-aware packing
|
|
209
|
+
// (knapsack-continue): an oversized body is skipped so a smaller, still-
|
|
210
|
+
// relevant one can fill the remaining budget instead of aborting the rest.
|
|
208
211
|
const topChunks = entryPoints.slice(0, 3);
|
|
209
|
-
const
|
|
210
|
-
|
|
212
|
+
const bodyBlobs = topChunks.map((r) => {
|
|
213
|
+
var _a, _b;
|
|
211
214
|
const absP = chunkPath(r);
|
|
212
215
|
const startLine = chunkStartLine(r);
|
|
213
216
|
const endLine = chunkEndLine(r);
|
|
214
|
-
const sym = (
|
|
217
|
+
const sym = (_b = (_a = (0, arrow_1.toArr)(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
|
|
215
218
|
try {
|
|
216
219
|
const content = fs.readFileSync(absP, "utf-8");
|
|
217
220
|
const allLines = content.split("\n");
|
|
218
221
|
const body = allLines
|
|
219
222
|
.slice(startLine, Math.min(endLine + 1, allLines.length))
|
|
220
223
|
.join("\n");
|
|
221
|
-
|
|
222
|
-
const blobTokens = estimateTokens(blob);
|
|
223
|
-
if (tokensUsed + blobTokens > budget)
|
|
224
|
-
break;
|
|
225
|
-
bodySection.push(blob);
|
|
226
|
-
tokensUsed += blobTokens;
|
|
224
|
+
return `\n--- ${relPath(projectRoot, absP)}:${startLine + 1} ${sym} ---\n${body}`;
|
|
227
225
|
}
|
|
228
|
-
catch (
|
|
229
|
-
// File not readable —
|
|
226
|
+
catch (_c) {
|
|
227
|
+
return null; // File not readable — drop
|
|
230
228
|
}
|
|
229
|
+
});
|
|
230
|
+
const bodyCandidates = bodyBlobs.map((blob, idx) => ({
|
|
231
|
+
tokens: blob ? estimateTokens(blob) : Number.POSITIVE_INFINITY,
|
|
232
|
+
score: topChunks.length - idx, // preserve relevance order
|
|
233
|
+
}));
|
|
234
|
+
const bodyPack = (0, budget_pack_1.packByBudget)(bodyCandidates, budget - tokensUsed, {
|
|
235
|
+
atLeastOne: false,
|
|
236
|
+
});
|
|
237
|
+
const bodySection = ["\n## Key Functions"];
|
|
238
|
+
for (const i of bodyPack.selected) {
|
|
239
|
+
const blob = bodyBlobs[i];
|
|
240
|
+
if (!blob)
|
|
241
|
+
continue;
|
|
242
|
+
bodySection.push(blob);
|
|
231
243
|
}
|
|
232
244
|
if (bodySection.length > 1) {
|
|
233
245
|
sections.push(bodySection.join(""));
|
|
246
|
+
tokensUsed += bodyPack.tokensUsed;
|
|
234
247
|
}
|
|
235
248
|
// Phase 4: File skeletons for unique files
|
|
236
249
|
const uniqueFiles = [
|
|
@@ -249,12 +262,14 @@ exports.context = new commander_1.Command("context")
|
|
|
249
262
|
continue;
|
|
250
263
|
const blob = `\n--- ${relPath(projectRoot, absP)} (skeleton, ~${result.tokenEstimate} tokens) ---\n${result.skeleton}`;
|
|
251
264
|
const blobTokens = estimateTokens(blob);
|
|
265
|
+
// Skip an oversized skeleton but keep trying smaller ones (a verbose
|
|
266
|
+
// file shouldn't starve the rest of the budget).
|
|
252
267
|
if (tokensUsed + blobTokens > budget)
|
|
253
|
-
|
|
268
|
+
continue;
|
|
254
269
|
skelSection.push(blob);
|
|
255
270
|
tokensUsed += blobTokens;
|
|
256
271
|
}
|
|
257
|
-
catch (
|
|
272
|
+
catch (_d) {
|
|
258
273
|
// Skip unreadable files
|
|
259
274
|
}
|
|
260
275
|
}
|
|
@@ -315,7 +330,7 @@ exports.context = new commander_1.Command("context")
|
|
|
315
330
|
try {
|
|
316
331
|
yield vectorDb.close();
|
|
317
332
|
}
|
|
318
|
-
catch (
|
|
333
|
+
catch (_e) { }
|
|
319
334
|
}
|
|
320
335
|
yield (0, exit_1.gracefulExit)();
|
|
321
336
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -397,7 +397,7 @@ Examples:
|
|
|
397
397
|
gmax "auth middleware" --projects api,gateway --plain
|
|
398
398
|
`)
|
|
399
399
|
.action((pattern, exec_path, _options, cmd) => __awaiter(void 0, void 0, void 0, function* () {
|
|
400
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p
|
|
400
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
401
401
|
const options = cmd.optsWithGlobals();
|
|
402
402
|
const root = process.cwd();
|
|
403
403
|
const minScore = Number.isFinite(Number.parseFloat(options.minScore))
|
|
@@ -799,7 +799,7 @@ Examples:
|
|
|
799
799
|
return defs.some((d) => regex.test(d));
|
|
800
800
|
});
|
|
801
801
|
}
|
|
802
|
-
catch (
|
|
802
|
+
catch (_q) {
|
|
803
803
|
// Invalid regex — skip
|
|
804
804
|
}
|
|
805
805
|
}
|
|
@@ -927,7 +927,7 @@ Examples:
|
|
|
927
927
|
}
|
|
928
928
|
}
|
|
929
929
|
}
|
|
930
|
-
catch (
|
|
930
|
+
catch (_r) { }
|
|
931
931
|
}
|
|
932
932
|
// Partial-index footer last, so it's the final line the agent reads —
|
|
933
933
|
// and emitted even on "(none)", where an empty result may just mean the
|
|
@@ -960,14 +960,18 @@ Examples:
|
|
|
960
960
|
if (options.contextForLlm) {
|
|
961
961
|
const fs = yield Promise.resolve().then(() => __importStar(require("node:fs")));
|
|
962
962
|
const { extractImportsFromContent } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/import-extractor")));
|
|
963
|
+
const { packByBudget } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/budget-pack")));
|
|
963
964
|
const budget = parseInt(options.budget, 10) || 8000;
|
|
964
|
-
let tokensUsed = 0;
|
|
965
|
-
let shown = 0;
|
|
966
965
|
console.log(resultCountHeader(filteredData, parseInt(options.m, 10)));
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
966
|
+
// Build every candidate blob up front (token cost needs the rendered
|
|
967
|
+
// text), then pack to budget. Token-aware packing skips an oversized
|
|
968
|
+
// chunk and keeps filling with smaller, still-relevant ones rather than
|
|
969
|
+
// aborting the loop — recovering budget the old greedy `break` wasted.
|
|
970
|
+
const candidates = filteredData.map((r, idx) => {
|
|
971
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
972
|
+
const absP = (_c = (_a = r.path) !== null && _a !== void 0 ? _a : (_b = r.metadata) === null || _b === void 0 ? void 0 : _b.path) !== null && _c !== void 0 ? _c : "";
|
|
973
|
+
const startLine = (_g = (_e = (_d = r.startLine) !== null && _d !== void 0 ? _d : r.start_line) !== null && _e !== void 0 ? _e : (_f = r.generated_metadata) === null || _f === void 0 ? void 0 : _f.start_line) !== null && _g !== void 0 ? _g : 0;
|
|
974
|
+
const endLine = (_l = (_j = (_h = r.endLine) !== null && _h !== void 0 ? _h : r.end_line) !== null && _j !== void 0 ? _j : (_k = r.generated_metadata) === null || _k === void 0 ? void 0 : _k.end_line) !== null && _l !== void 0 ? _l : startLine;
|
|
971
975
|
const relPath = absP.startsWith(projectRoot)
|
|
972
976
|
? absP.slice(projectRoot.length + 1)
|
|
973
977
|
: absP;
|
|
@@ -976,6 +980,7 @@ Examples:
|
|
|
976
980
|
r.defined_symbols.length > 0
|
|
977
981
|
? r.defined_symbols[0]
|
|
978
982
|
: "";
|
|
983
|
+
let blobText;
|
|
979
984
|
try {
|
|
980
985
|
const content = fs.readFileSync(absP, "utf-8");
|
|
981
986
|
const allLines = content.split("\n");
|
|
@@ -986,24 +991,27 @@ Examples:
|
|
|
986
991
|
const blob = [
|
|
987
992
|
`--- ${relPath}:${startLine + 1}${symbol ? ` ${symbol}` : ""} [${role}] ---`,
|
|
988
993
|
];
|
|
989
|
-
if (imports)
|
|
994
|
+
if (imports)
|
|
990
995
|
blob.push("[imports]", imports, "");
|
|
991
|
-
}
|
|
992
996
|
blob.push("[body]", body);
|
|
993
|
-
|
|
994
|
-
const blobTokens = Math.ceil(blobText.length / 4);
|
|
995
|
-
if (tokensUsed + blobTokens > budget && shown > 0) {
|
|
996
|
-
console.log(`\n(budget exhausted at ~${tokensUsed} tokens, ${filteredData.length - shown} more results not shown)`);
|
|
997
|
-
break;
|
|
998
|
-
}
|
|
999
|
-
console.log(`\n${blobText}`);
|
|
1000
|
-
tokensUsed += blobTokens;
|
|
1001
|
-
shown++;
|
|
997
|
+
blobText = blob.join("\n");
|
|
1002
998
|
}
|
|
1003
|
-
catch (
|
|
1004
|
-
|
|
1005
|
-
shown++;
|
|
999
|
+
catch (_m) {
|
|
1000
|
+
blobText = `--- ${relPath} (file not readable) ---`;
|
|
1006
1001
|
}
|
|
1002
|
+
// Preserve relevance order when scores are absent (rank-derived
|
|
1003
|
+
// fallback) so the density tiebreaker never reshuffles arbitrarily.
|
|
1004
|
+
const score = typeof r.score === "number"
|
|
1005
|
+
? r.score
|
|
1006
|
+
: (filteredData.length - idx) / filteredData.length;
|
|
1007
|
+
return { blobText, tokens: Math.ceil(blobText.length / 4), score };
|
|
1008
|
+
});
|
|
1009
|
+
const pack = packByBudget(candidates.map((c) => ({ tokens: c.tokens, score: c.score })), budget);
|
|
1010
|
+
for (const i of pack.selected) {
|
|
1011
|
+
console.log(`\n${candidates[i].blobText}`);
|
|
1012
|
+
}
|
|
1013
|
+
if (pack.dropped > 0) {
|
|
1014
|
+
console.log(`\n(budget: ~${pack.tokensUsed}/${budget} tokens, ${pack.dropped} lower-density result${pack.dropped > 1 ? "s" : ""} not shown)`);
|
|
1007
1015
|
}
|
|
1008
1016
|
return;
|
|
1009
1017
|
}
|
|
@@ -1017,7 +1025,7 @@ Examples:
|
|
|
1017
1025
|
if (options.imports) {
|
|
1018
1026
|
const seenFiles = new Set();
|
|
1019
1027
|
for (const r of filteredData) {
|
|
1020
|
-
const absP = (
|
|
1028
|
+
const absP = (_k = (_h = r.path) !== null && _h !== void 0 ? _h : (_j = r.metadata) === null || _j === void 0 ? void 0 : _j.path) !== null && _k !== void 0 ? _k : "";
|
|
1021
1029
|
if (absP && !seenFiles.has(absP)) {
|
|
1022
1030
|
seenFiles.add(absP);
|
|
1023
1031
|
const imports = getImportsForFile(absP);
|
|
@@ -1044,7 +1052,7 @@ Examples:
|
|
|
1044
1052
|
for (const r of filteredData) {
|
|
1045
1053
|
const b = r.scoreBreakdown;
|
|
1046
1054
|
if (b) {
|
|
1047
|
-
const absP = (
|
|
1055
|
+
const absP = (_o = (_l = r.path) !== null && _l !== void 0 ? _l : (_m = r.metadata) === null || _m === void 0 ? void 0 : _m.path) !== null && _o !== void 0 ? _o : "";
|
|
1048
1056
|
const relPath = absP.startsWith(projectRoot)
|
|
1049
1057
|
? absP.slice(projectRoot.length + 1)
|
|
1050
1058
|
: absP;
|
|
@@ -1106,7 +1114,7 @@ Examples:
|
|
|
1106
1114
|
console.log(lines.join("\n"));
|
|
1107
1115
|
}
|
|
1108
1116
|
}
|
|
1109
|
-
catch (
|
|
1117
|
+
catch (_s) {
|
|
1110
1118
|
// Trace failed — skip silently
|
|
1111
1119
|
}
|
|
1112
1120
|
}
|
|
@@ -1126,13 +1134,13 @@ Examples:
|
|
|
1126
1134
|
source: "cli",
|
|
1127
1135
|
tool: "search",
|
|
1128
1136
|
query: pattern,
|
|
1129
|
-
project: (
|
|
1137
|
+
project: (_p = (0, project_root_1.findProjectRoot)(root)) !== null && _p !== void 0 ? _p : root,
|
|
1130
1138
|
results: _searchResultCount,
|
|
1131
1139
|
ms: Date.now() - _searchStartMs,
|
|
1132
1140
|
error: _searchError,
|
|
1133
1141
|
});
|
|
1134
1142
|
}
|
|
1135
|
-
catch (
|
|
1143
|
+
catch (_t) { }
|
|
1136
1144
|
if (vectorDb) {
|
|
1137
1145
|
try {
|
|
1138
1146
|
yield vectorDb.close();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Token-aware budget packing (Phase 4).
|
|
4
|
+
*
|
|
5
|
+
* The budget-oriented output modes (`gmax context`, `gmax search
|
|
6
|
+
* --context-for-llm --budget`) greedily emit results in relevance order until
|
|
7
|
+
* the next chunk would overflow the token budget — then they STOP. That wastes
|
|
8
|
+
* the tail: a single mid-ranked verbose chunk that busts the budget aborts the
|
|
9
|
+
* whole loop, even when smaller, still-relevant chunks further down would fit.
|
|
10
|
+
*
|
|
11
|
+
* `packByBudget` fixes that with a knapsack-style greedy fill (skip the
|
|
12
|
+
* oversized chunk, keep trying the rest) plus a *conservative* density
|
|
13
|
+
* tiebreaker: among chunks whose relevance scores are within `tieEpsilon`, the
|
|
14
|
+
* denser (fewer-token) chunk is preferred. The tiebreaker only reorders
|
|
15
|
+
* near-ties, so it never buries a clearly-more-relevant chunk beneath a small
|
|
16
|
+
* tangential one. Selected items are returned in their original relevance order
|
|
17
|
+
* for display — packing is a selection concern, not a presentation one.
|
|
18
|
+
*
|
|
19
|
+
* This lives entirely in the presentation layer; it does NOT touch
|
|
20
|
+
* `searcher.ts` ranking, so search relevance / the bench are unaffected.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.packByBudget = packByBudget;
|
|
24
|
+
function packByBudget(candidates, budget, options = {}) {
|
|
25
|
+
var _a, _b;
|
|
26
|
+
const eps = (_a = options.tieEpsilon) !== null && _a !== void 0 ? _a : 0.02;
|
|
27
|
+
const atLeastOne = (_b = options.atLeastOne) !== null && _b !== void 0 ? _b : true;
|
|
28
|
+
if (candidates.length === 0) {
|
|
29
|
+
return { selected: [], tokensUsed: 0, dropped: 0 };
|
|
30
|
+
}
|
|
31
|
+
// Selection order: higher score first, but bucket near-ties (within eps) so
|
|
32
|
+
// the denser candidate wins inside a bucket. Bucketing keeps the comparator a
|
|
33
|
+
// valid total order (a raw |Δscore|<eps test would be intransitive).
|
|
34
|
+
const order = candidates
|
|
35
|
+
.map((c, i) => ({ i, tokens: Math.max(0, c.tokens), score: c.score }))
|
|
36
|
+
.sort((a, b) => {
|
|
37
|
+
const ba = Math.round(a.score / eps);
|
|
38
|
+
const bb = Math.round(b.score / eps);
|
|
39
|
+
if (ba !== bb)
|
|
40
|
+
return bb - ba; // higher score bucket first
|
|
41
|
+
if (a.tokens !== b.tokens)
|
|
42
|
+
return a.tokens - b.tokens; // denser first on tie
|
|
43
|
+
return a.i - b.i; // stable
|
|
44
|
+
});
|
|
45
|
+
const selected = [];
|
|
46
|
+
let used = 0;
|
|
47
|
+
for (const o of order) {
|
|
48
|
+
if (used + o.tokens > budget)
|
|
49
|
+
continue; // skip oversized, keep filling
|
|
50
|
+
selected.push(o.i);
|
|
51
|
+
used += o.tokens;
|
|
52
|
+
}
|
|
53
|
+
if (selected.length === 0 && atLeastOne) {
|
|
54
|
+
// Nothing fit — emit the single most relevant candidate anyway.
|
|
55
|
+
let best = 0;
|
|
56
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
57
|
+
if (candidates[i].score > candidates[best].score)
|
|
58
|
+
best = i;
|
|
59
|
+
}
|
|
60
|
+
selected.push(best);
|
|
61
|
+
used = Math.max(0, candidates[best].tokens);
|
|
62
|
+
}
|
|
63
|
+
selected.sort((x, y) => x - y); // back to relevance/display order
|
|
64
|
+
return {
|
|
65
|
+
selected,
|
|
66
|
+
tokensUsed: used,
|
|
67
|
+
dropped: candidates.length - selected.length,
|
|
68
|
+
};
|
|
69
|
+
}
|
package/package.json
CHANGED