grepmax 0.17.19 → 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/README.md
CHANGED
|
@@ -163,6 +163,9 @@ gmax "query" [options]
|
|
|
163
163
|
| `--explain` | Show scoring breakdown per result. | `false` |
|
|
164
164
|
| `-C <n>` | Context lines before/after. | `0` |
|
|
165
165
|
| `--root <dir>` | Search a different project. | cwd |
|
|
166
|
+
| `--all-projects` | Search every indexed project; results grouped by project. | `false` |
|
|
167
|
+
| `--projects <list>` | Search only these projects (comma-separated names). | — |
|
|
168
|
+
| `--exclude-projects <list>` | With `--all-projects`, skip these projects. | — |
|
|
166
169
|
| `--min-score <n>` | Minimum relevance score. | `0` |
|
|
167
170
|
|
|
168
171
|
## Background Daemon
|
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
|
@@ -56,6 +56,7 @@ const setup_helpers_1 = require("../lib/setup/setup-helpers");
|
|
|
56
56
|
const skeleton_1 = require("../lib/skeleton");
|
|
57
57
|
const retriever_1 = require("../lib/skeleton/retriever");
|
|
58
58
|
const vector_db_1 = require("../lib/store/vector-db");
|
|
59
|
+
const cross_project_1 = require("../lib/utils/cross-project");
|
|
59
60
|
const exit_1 = require("../lib/utils/exit");
|
|
60
61
|
const formatter_1 = require("../lib/utils/formatter");
|
|
61
62
|
const import_extractor_1 = require("../lib/utils/import-extractor");
|
|
@@ -370,6 +371,9 @@ exports.search = new commander_1.Command("search")
|
|
|
370
371
|
.option("--file <name>", "Filter to files matching this name (e.g. 'syncer.ts')")
|
|
371
372
|
.option("--in <subpath>", "Restrict to a sub-path of the project (repeatable; comma-separated also accepted)", (value, prev) => prev ? [...prev, value] : [value])
|
|
372
373
|
.option("--exclude <subpath>", "Exclude a sub-path of the project (repeatable; e.g. 'tests/')", (value, prev) => prev ? [...prev, value] : [value])
|
|
374
|
+
.option("--all-projects", "Search across every indexed project, not just the current one", false)
|
|
375
|
+
.option("--projects <list>", "Search only these indexed projects (comma-separated names)")
|
|
376
|
+
.option("--exclude-projects <list>", "With --all-projects, skip these projects (comma-separated names)")
|
|
373
377
|
.option("--lang <ext>", "Filter by file extension (e.g. 'ts', 'py')")
|
|
374
378
|
.option("--role <role>", "Filter by role: ORCHESTRATION, DEFINITION, IMPLEMENTATION")
|
|
375
379
|
.option("--symbol", "Append call graph after search results", false)
|
|
@@ -389,9 +393,11 @@ Examples:
|
|
|
389
393
|
gmax "VectorDB" --symbol --plain
|
|
390
394
|
gmax "error handling" -C 5 --imports --plain
|
|
391
395
|
gmax "handler" --name "handle.*" --exclude tests/
|
|
396
|
+
gmax "rate limiter" --all-projects --agent
|
|
397
|
+
gmax "auth middleware" --projects api,gateway --plain
|
|
392
398
|
`)
|
|
393
399
|
.action((pattern, exec_path, _options, cmd) => __awaiter(void 0, void 0, void 0, function* () {
|
|
394
|
-
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;
|
|
395
401
|
const options = cmd.optsWithGlobals();
|
|
396
402
|
const root = process.cwd();
|
|
397
403
|
const minScore = Number.isFinite(Number.parseFloat(options.minScore))
|
|
@@ -401,10 +407,46 @@ Examples:
|
|
|
401
407
|
const _searchStartMs = Date.now();
|
|
402
408
|
let _searchResultCount = 0;
|
|
403
409
|
let _searchError;
|
|
404
|
-
//
|
|
410
|
+
// Cross-project scope (Phase 6): --all-projects / --projects / --exclude-projects.
|
|
411
|
+
// When active, single-project path scoping is dropped in favor of the
|
|
412
|
+
// project_roots filter clauses, and results are grouped by owning project.
|
|
413
|
+
const crossProject = (0, cross_project_1.resolveCrossProjectScope)({
|
|
414
|
+
allProjects: options.allProjects,
|
|
415
|
+
projects: options.projects,
|
|
416
|
+
excludeProjects: options.excludeProjects,
|
|
417
|
+
});
|
|
418
|
+
if (crossProject.active) {
|
|
419
|
+
// These modifiers are inherently single-project (one skeleton root, one
|
|
420
|
+
// call-graph center, one budget rollup). Reject the combination up front
|
|
421
|
+
// rather than emit confusing cross-root output.
|
|
422
|
+
const conflict = options.skeleton
|
|
423
|
+
? "--skeleton"
|
|
424
|
+
: options.contextForLlm
|
|
425
|
+
? "--context-for-llm"
|
|
426
|
+
: options.symbol
|
|
427
|
+
? "--symbol"
|
|
428
|
+
: null;
|
|
429
|
+
if (conflict) {
|
|
430
|
+
console.error(`${conflict} is single-project; drop --all-projects/--projects or ${conflict}.`);
|
|
431
|
+
process.exitCode = 1;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
for (const w of crossProject.warnings)
|
|
435
|
+
console.warn(`Warning: ${w}`);
|
|
436
|
+
if (!crossProject.roots.length) {
|
|
437
|
+
console.error("No matching indexed projects. Run `gmax status` to list them.");
|
|
438
|
+
process.exitCode = 1;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Check for running server. The per-project HTTP server can't answer
|
|
443
|
+
// cross-project queries, so cross-project mode skips it and uses the
|
|
444
|
+
// daemon-mediated / in-process path (both query the shared table).
|
|
405
445
|
const execPathForServer = exec_path ? path.resolve(exec_path) : root;
|
|
406
446
|
const projectRootForServer = (_a = (0, project_root_1.findProjectRoot)(execPathForServer)) !== null && _a !== void 0 ? _a : execPathForServer;
|
|
407
|
-
const server =
|
|
447
|
+
const server = crossProject.active
|
|
448
|
+
? null
|
|
449
|
+
: (0, server_registry_1.getServerForProject)(projectRootForServer);
|
|
408
450
|
if (server) {
|
|
409
451
|
try {
|
|
410
452
|
const response = yield fetch(`http://localhost:${server.port}/search`, {
|
|
@@ -552,14 +594,22 @@ Examples:
|
|
|
552
594
|
in: options.in,
|
|
553
595
|
exclude: options.exclude,
|
|
554
596
|
});
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
597
|
+
// Cross-project mode drops the single-project path prefix (and any
|
|
598
|
+
// --in/[path] sub-scoping, which is meaningless across roots) in favor of
|
|
599
|
+
// the project_roots filter clauses computed below.
|
|
600
|
+
if (crossProject.active && (exec_path || ((_d = options.in) === null || _d === void 0 ? void 0 : _d.length))) {
|
|
601
|
+
console.warn("Warning: --in / [path] are single-project; ignored under --all-projects/--projects.");
|
|
602
|
+
}
|
|
603
|
+
const pathFilter = crossProject.active
|
|
604
|
+
? undefined
|
|
605
|
+
: options.in && options.in.length > 0
|
|
606
|
+
? scope.pathPrefix
|
|
607
|
+
: exec_path
|
|
608
|
+
? (() => {
|
|
609
|
+
const p = path.resolve(exec_path);
|
|
610
|
+
return p.endsWith("/") ? p : `${p}/`;
|
|
611
|
+
})()
|
|
612
|
+
: scope.pathPrefix;
|
|
563
613
|
const searchFilters = {};
|
|
564
614
|
if (options.file)
|
|
565
615
|
searchFilters.file = options.file;
|
|
@@ -567,10 +617,19 @@ Examples:
|
|
|
567
617
|
searchFilters.language = options.lang;
|
|
568
618
|
if (options.role)
|
|
569
619
|
searchFilters.role = options.role;
|
|
570
|
-
if (
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
620
|
+
if (crossProject.active) {
|
|
621
|
+
if (crossProject.projectRootsCsv)
|
|
622
|
+
searchFilters.project_roots = crossProject.projectRootsCsv;
|
|
623
|
+
if (crossProject.excludeProjectRootsCsv)
|
|
624
|
+
searchFilters.exclude_project_roots =
|
|
625
|
+
crossProject.excludeProjectRootsCsv;
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
if (scope.inPrefixes.length > 0)
|
|
629
|
+
searchFilters.inPrefixes = scope.inPrefixes;
|
|
630
|
+
if (scope.excludePrefixes.length > 0)
|
|
631
|
+
searchFilters.excludePrefixes = scope.excludePrefixes;
|
|
632
|
+
}
|
|
574
633
|
// Aider-style seeding: --seed-file / --seed-symbol (repeatable, also
|
|
575
634
|
// comma-separated) bias candidate generation toward the caller's working
|
|
576
635
|
// context. Absent → undefined → inert.
|
|
@@ -621,7 +680,7 @@ Examples:
|
|
|
621
680
|
indexState = resp.indexState;
|
|
622
681
|
}
|
|
623
682
|
else if (process.env.GMAX_DEBUG === "1") {
|
|
624
|
-
console.error(`[search] daemon path unavailable: ${(
|
|
683
|
+
console.error(`[search] daemon path unavailable: ${(_e = resp.error) !== null && _e !== void 0 ? _e : "unknown"}`);
|
|
625
684
|
}
|
|
626
685
|
}
|
|
627
686
|
}
|
|
@@ -703,7 +762,7 @@ Examples:
|
|
|
703
762
|
}
|
|
704
763
|
}
|
|
705
764
|
// Ensure a watcher is running for live reindexing
|
|
706
|
-
if (!process.env.VITEST && !((
|
|
765
|
+
if (!process.env.VITEST && !((_f = process.env.NODE_ENV) === null || _f === void 0 ? void 0 : _f.includes("test"))) {
|
|
707
766
|
const { launchWatcher } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/watcher-launcher")));
|
|
708
767
|
const launched = yield launchWatcher(projectRoot);
|
|
709
768
|
if (!launched.ok && launched.reason === "spawn-failed") {
|
|
@@ -715,7 +774,7 @@ Examples:
|
|
|
715
774
|
? searchFilters
|
|
716
775
|
: undefined, pathFilter);
|
|
717
776
|
} // end if (!searchResult) — in-process fallback
|
|
718
|
-
if (!options.agent && ((
|
|
777
|
+
if (!options.agent && ((_g = searchResult.warnings) === null || _g === void 0 ? void 0 : _g.length)) {
|
|
719
778
|
for (const w of searchResult.warnings) {
|
|
720
779
|
console.warn(`Warning: ${w}`);
|
|
721
780
|
}
|
|
@@ -740,7 +799,7 @@ Examples:
|
|
|
740
799
|
return defs.some((d) => regex.test(d));
|
|
741
800
|
});
|
|
742
801
|
}
|
|
743
|
-
catch (
|
|
802
|
+
catch (_q) {
|
|
744
803
|
// Invalid regex — skip
|
|
745
804
|
}
|
|
746
805
|
}
|
|
@@ -757,6 +816,76 @@ Examples:
|
|
|
757
816
|
};
|
|
758
817
|
// Agent mode: ultra-compact one-line-per-result output
|
|
759
818
|
_searchResultCount = filteredData.length;
|
|
819
|
+
// Cross-project (Phase 6): render grouped by owning project so idioms
|
|
820
|
+
// from different stacks don't blur into one flat list. Only the
|
|
821
|
+
// string-formatter modes reach here — skeleton/context-for-llm/symbol
|
|
822
|
+
// were rejected up front.
|
|
823
|
+
if (crossProject.active) {
|
|
824
|
+
const emitFooter = () => {
|
|
825
|
+
const footer = (0, index_state_footer_1.formatIndexStateFooter)(indexState, {
|
|
826
|
+
agent: !!options.agent,
|
|
827
|
+
});
|
|
828
|
+
if (footer) {
|
|
829
|
+
if (options.agent)
|
|
830
|
+
console.log(footer);
|
|
831
|
+
else
|
|
832
|
+
console.warn(footer);
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
if (!filteredData.length) {
|
|
836
|
+
console.log(options.agent ? "(none)" : "No matches found.");
|
|
837
|
+
process.exitCode = 1;
|
|
838
|
+
emitFooter();
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
const getPath = (r) => {
|
|
842
|
+
var _a, _b, _c;
|
|
843
|
+
return String((_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 : "");
|
|
844
|
+
};
|
|
845
|
+
const groups = (0, cross_project_1.groupResultsByProject)(filteredData, crossProject.roots, getPath);
|
|
846
|
+
const isTTY = process.stdout.isTTY;
|
|
847
|
+
const shouldBePlain = options.plain || !isTTY;
|
|
848
|
+
const blocks = [];
|
|
849
|
+
for (const g of groups) {
|
|
850
|
+
let body;
|
|
851
|
+
if (options.agent) {
|
|
852
|
+
body = (0, agent_search_formatter_1.formatAgentSearchResults)(g.items, g.root, {
|
|
853
|
+
includeImports: options.imports,
|
|
854
|
+
getImportsForFile,
|
|
855
|
+
explain: options.explain,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
else if (options.compact) {
|
|
859
|
+
body = formatCompactTable(toCompactHits(g.items), g.root, pattern, {
|
|
860
|
+
isTTY: !!isTTY,
|
|
861
|
+
plain: !!options.plain,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
else if (shouldBePlain) {
|
|
865
|
+
body = (0, formatter_1.formatTextResults)(toTextResults(g.items), pattern, g.root, {
|
|
866
|
+
isPlain: true,
|
|
867
|
+
compact: options.compact,
|
|
868
|
+
content: options.content,
|
|
869
|
+
perFile: parseInt(options.perFile, 10),
|
|
870
|
+
showScores: options.scores,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
const { formatResults } = yield Promise.resolve().then(() => __importStar(require("../lib/output/formatter")));
|
|
875
|
+
body = formatResults(g.items, g.root, {
|
|
876
|
+
content: options.content,
|
|
877
|
+
explain: options.explain,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
const header = options.agent
|
|
881
|
+
? `## ${g.name} (${g.items.length})`
|
|
882
|
+
: `=== ${g.name} (${g.items.length}) ===`;
|
|
883
|
+
blocks.push(`${header}\n${body}`);
|
|
884
|
+
}
|
|
885
|
+
console.log(blocks.join("\n\n"));
|
|
886
|
+
emitFooter();
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
760
889
|
if (options.agent) {
|
|
761
890
|
if (!filteredData.length) {
|
|
762
891
|
console.log("(none)");
|
|
@@ -798,7 +927,7 @@ Examples:
|
|
|
798
927
|
}
|
|
799
928
|
}
|
|
800
929
|
}
|
|
801
|
-
catch (
|
|
930
|
+
catch (_r) { }
|
|
802
931
|
}
|
|
803
932
|
// Partial-index footer last, so it's the final line the agent reads —
|
|
804
933
|
// and emitted even on "(none)", where an empty result may just mean the
|
|
@@ -831,14 +960,18 @@ Examples:
|
|
|
831
960
|
if (options.contextForLlm) {
|
|
832
961
|
const fs = yield Promise.resolve().then(() => __importStar(require("node:fs")));
|
|
833
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")));
|
|
834
964
|
const budget = parseInt(options.budget, 10) || 8000;
|
|
835
|
-
let tokensUsed = 0;
|
|
836
|
-
let shown = 0;
|
|
837
965
|
console.log(resultCountHeader(filteredData, parseInt(options.m, 10)));
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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;
|
|
842
975
|
const relPath = absP.startsWith(projectRoot)
|
|
843
976
|
? absP.slice(projectRoot.length + 1)
|
|
844
977
|
: absP;
|
|
@@ -847,6 +980,7 @@ Examples:
|
|
|
847
980
|
r.defined_symbols.length > 0
|
|
848
981
|
? r.defined_symbols[0]
|
|
849
982
|
: "";
|
|
983
|
+
let blobText;
|
|
850
984
|
try {
|
|
851
985
|
const content = fs.readFileSync(absP, "utf-8");
|
|
852
986
|
const allLines = content.split("\n");
|
|
@@ -857,24 +991,27 @@ Examples:
|
|
|
857
991
|
const blob = [
|
|
858
992
|
`--- ${relPath}:${startLine + 1}${symbol ? ` ${symbol}` : ""} [${role}] ---`,
|
|
859
993
|
];
|
|
860
|
-
if (imports)
|
|
994
|
+
if (imports)
|
|
861
995
|
blob.push("[imports]", imports, "");
|
|
862
|
-
}
|
|
863
996
|
blob.push("[body]", body);
|
|
864
|
-
|
|
865
|
-
const blobTokens = Math.ceil(blobText.length / 4);
|
|
866
|
-
if (tokensUsed + blobTokens > budget && shown > 0) {
|
|
867
|
-
console.log(`\n(budget exhausted at ~${tokensUsed} tokens, ${filteredData.length - shown} more results not shown)`);
|
|
868
|
-
break;
|
|
869
|
-
}
|
|
870
|
-
console.log(`\n${blobText}`);
|
|
871
|
-
tokensUsed += blobTokens;
|
|
872
|
-
shown++;
|
|
997
|
+
blobText = blob.join("\n");
|
|
873
998
|
}
|
|
874
|
-
catch (
|
|
875
|
-
|
|
876
|
-
shown++;
|
|
999
|
+
catch (_m) {
|
|
1000
|
+
blobText = `--- ${relPath} (file not readable) ---`;
|
|
877
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)`);
|
|
878
1015
|
}
|
|
879
1016
|
return;
|
|
880
1017
|
}
|
|
@@ -888,7 +1025,7 @@ Examples:
|
|
|
888
1025
|
if (options.imports) {
|
|
889
1026
|
const seenFiles = new Set();
|
|
890
1027
|
for (const r of filteredData) {
|
|
891
|
-
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 : "";
|
|
892
1029
|
if (absP && !seenFiles.has(absP)) {
|
|
893
1030
|
seenFiles.add(absP);
|
|
894
1031
|
const imports = getImportsForFile(absP);
|
|
@@ -915,7 +1052,7 @@ Examples:
|
|
|
915
1052
|
for (const r of filteredData) {
|
|
916
1053
|
const b = r.scoreBreakdown;
|
|
917
1054
|
if (b) {
|
|
918
|
-
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 : "";
|
|
919
1056
|
const relPath = absP.startsWith(projectRoot)
|
|
920
1057
|
? absP.slice(projectRoot.length + 1)
|
|
921
1058
|
: absP;
|
|
@@ -977,7 +1114,7 @@ Examples:
|
|
|
977
1114
|
console.log(lines.join("\n"));
|
|
978
1115
|
}
|
|
979
1116
|
}
|
|
980
|
-
catch (
|
|
1117
|
+
catch (_s) {
|
|
981
1118
|
// Trace failed — skip silently
|
|
982
1119
|
}
|
|
983
1120
|
}
|
|
@@ -997,13 +1134,13 @@ Examples:
|
|
|
997
1134
|
source: "cli",
|
|
998
1135
|
tool: "search",
|
|
999
1136
|
query: pattern,
|
|
1000
|
-
project: (
|
|
1137
|
+
project: (_p = (0, project_root_1.findProjectRoot)(root)) !== null && _p !== void 0 ? _p : root,
|
|
1001
1138
|
results: _searchResultCount,
|
|
1002
1139
|
ms: Date.now() - _searchStartMs,
|
|
1003
1140
|
error: _searchError,
|
|
1004
1141
|
});
|
|
1005
1142
|
}
|
|
1006
|
-
catch (
|
|
1143
|
+
catch (_t) { }
|
|
1007
1144
|
if (vectorDb) {
|
|
1008
1145
|
try {
|
|
1009
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
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cross-project search scoping (Phase 6).
|
|
4
|
+
*
|
|
5
|
+
* The shared LanceDB table holds chunks from every indexed project, scoped by
|
|
6
|
+
* absolute-path prefix. Single-project search pins a `pathPrefix`; cross-project
|
|
7
|
+
* search drops the prefix and instead scopes with the `project_roots` /
|
|
8
|
+
* `exclude_project_roots` filter clauses (an OR-group of `path LIKE` prefixes —
|
|
9
|
+
* see buildWhereClause in searcher.ts). This module resolves the CLI flags
|
|
10
|
+
* (`--all-projects` / `--projects` / `--exclude-projects`) to those filter
|
|
11
|
+
* values and groups results back by owning project for display.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.resolveCrossProjectScope = resolveCrossProjectScope;
|
|
15
|
+
exports.projectForPath = projectForPath;
|
|
16
|
+
exports.groupResultsByProject = groupResultsByProject;
|
|
17
|
+
const project_registry_1 = require("./project-registry");
|
|
18
|
+
function resolveCrossProjectScope(opts) {
|
|
19
|
+
const active = !!(opts.allProjects || opts.projects);
|
|
20
|
+
if (!active) {
|
|
21
|
+
return { active: false, roots: [], warnings: [] };
|
|
22
|
+
}
|
|
23
|
+
// Ignore "error"-status projects: the daemon won't search them anyway.
|
|
24
|
+
const all = (0, project_registry_1.listProjects)().filter((p) => p.status !== "error");
|
|
25
|
+
const byName = new Map(all.map((p) => [p.name, p]));
|
|
26
|
+
const warnings = [];
|
|
27
|
+
const resolveNames = (csv) => {
|
|
28
|
+
const names = (csv !== null && csv !== void 0 ? csv : "")
|
|
29
|
+
.split(",")
|
|
30
|
+
.map((s) => s.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
const found = [];
|
|
33
|
+
const missing = [];
|
|
34
|
+
for (const n of names) {
|
|
35
|
+
const p = byName.get(n);
|
|
36
|
+
if (p)
|
|
37
|
+
found.push(p);
|
|
38
|
+
else
|
|
39
|
+
missing.push(n);
|
|
40
|
+
}
|
|
41
|
+
return { found, missing };
|
|
42
|
+
};
|
|
43
|
+
const excluded = resolveNames(opts.excludeProjects);
|
|
44
|
+
const excludedRoots = new Set(excluded.found.map((p) => p.root));
|
|
45
|
+
if (excluded.missing.length) {
|
|
46
|
+
warnings.push(`Unknown --exclude-projects: ${excluded.missing.join(", ")}`);
|
|
47
|
+
}
|
|
48
|
+
let included;
|
|
49
|
+
let projectRootsCsv;
|
|
50
|
+
let excludeProjectRootsCsv;
|
|
51
|
+
if (opts.projects) {
|
|
52
|
+
const r = resolveNames(opts.projects);
|
|
53
|
+
if (r.missing.length) {
|
|
54
|
+
warnings.push(`Unknown --projects: ${r.missing.join(", ")}. Available: ${all
|
|
55
|
+
.map((p) => p.name)
|
|
56
|
+
.join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
included = r.found.filter((p) => !excludedRoots.has(p.root));
|
|
59
|
+
// Narrowed to an explicit subset → scope with project_roots.
|
|
60
|
+
projectRootsCsv = included.length
|
|
61
|
+
? included.map((p) => p.root).join(",")
|
|
62
|
+
: undefined;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// --all-projects: search the whole shared table. No project_roots clause
|
|
66
|
+
// (its absence IS "everything"); only carve out --exclude-projects.
|
|
67
|
+
included = all.filter((p) => !excludedRoots.has(p.root));
|
|
68
|
+
if (excludedRoots.size) {
|
|
69
|
+
excludeProjectRootsCsv = [...excludedRoots].join(",");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
active: true,
|
|
74
|
+
roots: included.map((p) => ({ root: p.root, name: p.name })),
|
|
75
|
+
projectRootsCsv,
|
|
76
|
+
excludeProjectRootsCsv,
|
|
77
|
+
warnings,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Longest-prefix match of an absolute path against the in-scope project roots. */
|
|
81
|
+
function projectForPath(absPath, roots) {
|
|
82
|
+
let best = null;
|
|
83
|
+
let bestLen = -1;
|
|
84
|
+
for (const r of roots) {
|
|
85
|
+
const prefix = r.root.endsWith("/") ? r.root : `${r.root}/`;
|
|
86
|
+
if (absPath === r.root || absPath.startsWith(prefix)) {
|
|
87
|
+
if (prefix.length > bestLen) {
|
|
88
|
+
best = r;
|
|
89
|
+
bestLen = prefix.length;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return best;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Bucket ranked results by owning project, preserving rank order: groups appear
|
|
97
|
+
* in order of their best-ranked member, items keep their original order.
|
|
98
|
+
*/
|
|
99
|
+
function groupResultsByProject(results, roots, getPath) {
|
|
100
|
+
var _a, _b, _c;
|
|
101
|
+
const order = [];
|
|
102
|
+
const buckets = new Map();
|
|
103
|
+
for (const r of results) {
|
|
104
|
+
const owner = projectForPath(getPath(r), roots);
|
|
105
|
+
const key = (_a = owner === null || owner === void 0 ? void 0 : owner.root) !== null && _a !== void 0 ? _a : "(unknown)";
|
|
106
|
+
let bucket = buckets.get(key);
|
|
107
|
+
if (!bucket) {
|
|
108
|
+
bucket = { name: (_b = owner === null || owner === void 0 ? void 0 : owner.name) !== null && _b !== void 0 ? _b : "(unknown)", root: (_c = owner === null || owner === void 0 ? void 0 : owner.root) !== null && _c !== void 0 ? _c : "", items: [] };
|
|
109
|
+
buckets.set(key, bucket);
|
|
110
|
+
order.push(key);
|
|
111
|
+
}
|
|
112
|
+
bucket.items.push(r);
|
|
113
|
+
}
|
|
114
|
+
return order.map((k) => buckets.get(k));
|
|
115
|
+
}
|
package/package.json
CHANGED