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.
@@ -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, _d, _e;
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 bodySection = ["\n## Key Functions"];
210
- for (const r of topChunks) {
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 = (_e = (_d = (0, arrow_1.toArr)(r.defined_symbols)) === null || _d === void 0 ? void 0 : _d[0]) !== null && _e !== void 0 ? _e : "";
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
- const blob = `\n--- ${relPath(projectRoot, absP)}:${startLine + 1} ${sym} ---\n${body}`;
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 (_f) {
229
- // File not readable — skip
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
- break;
268
+ continue;
254
269
  skelSection.push(blob);
255
270
  tokensUsed += blobTokens;
256
271
  }
257
- catch (_g) {
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 (_h) { }
333
+ catch (_e) { }
319
334
  }
320
335
  yield (0, exit_1.gracefulExit)();
321
336
  }
@@ -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, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
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 (_1) {
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 (_2) { }
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
- for (const r of filteredData) {
968
- 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 : "";
969
- const startLine = (_p = (_m = (_l = r.startLine) !== null && _l !== void 0 ? _l : r.start_line) !== null && _m !== void 0 ? _m : (_o = r.generated_metadata) === null || _o === void 0 ? void 0 : _o.start_line) !== null && _p !== void 0 ? _p : 0;
970
- const endLine = (_t = (_r = (_q = r.endLine) !== null && _q !== void 0 ? _q : r.end_line) !== null && _r !== void 0 ? _r : (_s = r.generated_metadata) === null || _s === void 0 ? void 0 : _s.end_line) !== null && _t !== void 0 ? _t : startLine;
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
- const blobText = blob.join("\n");
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 (_3) {
1004
- console.log(`\n--- ${relPath} (file not readable) ---`);
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 = (_w = (_u = r.path) !== null && _u !== void 0 ? _u : (_v = r.metadata) === null || _v === void 0 ? void 0 : _v.path) !== null && _w !== void 0 ? _w : "";
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 = (_z = (_x = r.path) !== null && _x !== void 0 ? _x : (_y = r.metadata) === null || _y === void 0 ? void 0 : _y.path) !== null && _z !== void 0 ? _z : "";
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 (_4) {
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: (_0 = (0, project_root_1.findProjectRoot)(root)) !== null && _0 !== void 0 ? _0 : root,
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 (_5) { }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.20",
3
+ "version": "0.17.21",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.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.17.20",
3
+ "version": "0.17.21",
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",