grepmax 0.17.18 → 0.17.20

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
@@ -46,6 +46,7 @@ exports.dead = void 0;
46
46
  const commander_1 = require("commander");
47
47
  const graph_builder_1 = require("../lib/graph/graph-builder");
48
48
  const vector_db_1 = require("../lib/store/vector-db");
49
+ const agent_errors_1 = require("../lib/utils/agent-errors");
49
50
  const exit_1 = require("../lib/utils/exit");
50
51
  const filter_builder_1 = require("../lib/utils/filter-builder");
51
52
  const project_registry_1 = require("../lib/utils/project-registry");
@@ -134,7 +135,7 @@ exports.dead = new commander_1.Command("dead")
134
135
  .limit(1)
135
136
  .toArray();
136
137
  if (defRows.length === 0) {
137
- console.log(opts.agent ? "(not found)" : `Symbol not found: ${symbol}`);
138
+ console.log((0, agent_errors_1.symbolNotFoundLines)(symbol, { agent: opts.agent }).join("\n"));
138
139
  process.exitCode = 1;
139
140
  return;
140
141
  }
@@ -46,6 +46,7 @@ exports.extract = void 0;
46
46
  const fs = __importStar(require("node:fs"));
47
47
  const commander_1 = require("commander");
48
48
  const vector_db_1 = require("../lib/store/vector-db");
49
+ const agent_errors_1 = require("../lib/utils/agent-errors");
49
50
  const exit_1 = require("../lib/utils/exit");
50
51
  const filter_builder_1 = require("../lib/utils/filter-builder");
51
52
  const import_extractor_1 = require("../lib/utils/import-extractor");
@@ -130,13 +131,11 @@ exports.extract = new commander_1.Command("extract")
130
131
  const where = buildScopeWhere(scope, `array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}')`);
131
132
  const chunks = yield findSymbolChunks(vectorDb, where);
132
133
  if (chunks.length === 0) {
133
- const lines = [
134
- `Symbol not found: ${opts.agent ? symbol : style.bold(symbol)}`,
135
- ];
136
- if (!opts.agent) {
137
- lines.push("", style.dim("Possible reasons:"), style.dim(" \u2022 The symbol doesn't exist in any indexed project"), style.dim(" \u2022 The containing file hasn't been indexed yet"), style.dim(" \u2022 The name is spelled differently in the source"), "", style.dim("Try:"), style.dim(" gmax status \u2014 see which projects are indexed"), style.dim(" gmax search <name> \u2014 fuzzy search for similar symbols"));
138
- }
139
- console.log(lines.join("\n"));
134
+ console.log((0, agent_errors_1.symbolNotFoundLines)(symbol, {
135
+ agent: opts.agent,
136
+ dim: style.dim,
137
+ bold: style.bold,
138
+ }).join("\n"));
140
139
  process.exitCode = 1;
141
140
  return;
142
141
  }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.helpAgent = void 0;
4
+ const commander_1 = require("commander");
5
+ const agent_cheatsheet_1 = require("../lib/help/agent-cheatsheet");
6
+ exports.helpAgent = new commander_1.Command("help-agent")
7
+ .description("Print the agent command cheatsheet (re-summon the SessionStart hint on demand)")
8
+ .addHelpText("after", `
9
+ The same survey injected at SessionStart. Useful when a long session has
10
+ compacted away the original hint and you need to re-discover the commands.`)
11
+ .action(() => {
12
+ process.stdout.write(`${agent_cheatsheet_1.AGENT_CHEATSHEET}\n`);
13
+ });
@@ -47,6 +47,7 @@ const path = __importStar(require("node:path"));
47
47
  const commander_1 = require("commander");
48
48
  const impact_1 = require("../lib/graph/impact");
49
49
  const vector_db_1 = require("../lib/store/vector-db");
50
+ const agent_errors_1 = require("../lib/utils/agent-errors");
50
51
  const exit_1 = require("../lib/utils/exit");
51
52
  const project_registry_1 = require("../lib/utils/project-registry");
52
53
  const project_root_1 = require("../lib/utils/project-root");
@@ -73,7 +74,7 @@ exports.impact = new commander_1.Command("impact")
73
74
  if (symbols.length === 0) {
74
75
  console.log(resolvedAsFile
75
76
  ? `No symbols found in file: ${target}`
76
- : `Symbol not found: ${target}`);
77
+ : (0, agent_errors_1.symbolNotFoundLines)(target, { agent: opts.agent }).join("\n"));
77
78
  process.exitCode = 1;
78
79
  return;
79
80
  }
@@ -47,6 +47,7 @@ const fs = __importStar(require("node:fs"));
47
47
  const commander_1 = require("commander");
48
48
  const graph_builder_1 = require("../lib/graph/graph-builder");
49
49
  const vector_db_1 = require("../lib/store/vector-db");
50
+ const agent_errors_1 = require("../lib/utils/agent-errors");
50
51
  const exit_1 = require("../lib/utils/exit");
51
52
  const filter_builder_1 = require("../lib/utils/filter-builder");
52
53
  const language_1 = require("../lib/utils/language");
@@ -152,13 +153,11 @@ exports.peek = new commander_1.Command("peek")
152
153
  const graphBuilder = new graph_builder_1.GraphBuilder(vectorDb, scope.pathPrefix, scope.excludePrefixes);
153
154
  const graph = yield graphBuilder.buildGraph(symbol);
154
155
  if (!graph.center) {
155
- const lines = [
156
- `Symbol not found: ${opts.agent ? symbol : style.bold(symbol)}`,
157
- ];
158
- if (!opts.agent) {
159
- lines.push("", style.dim("Possible reasons:"), style.dim(" \u2022 The symbol doesn't exist in any indexed project"), style.dim(" \u2022 The containing file hasn't been indexed yet"), style.dim(" \u2022 The name is spelled differently in the source"), "", style.dim("Try:"), style.dim(" gmax status \u2014 see which projects are indexed"), style.dim(" gmax search <name> \u2014 fuzzy search for similar symbols"));
160
- }
161
- console.log(lines.join("\n"));
156
+ console.log((0, agent_errors_1.symbolNotFoundLines)(symbol, {
157
+ agent: opts.agent,
158
+ dim: style.dim,
159
+ bold: style.bold,
160
+ }).join("\n"));
162
161
  process.exitCode = 1;
163
162
  return;
164
163
  }
@@ -46,6 +46,7 @@ exports.related = void 0;
46
46
  const path = __importStar(require("node:path"));
47
47
  const commander_1 = require("commander");
48
48
  const vector_db_1 = require("../lib/store/vector-db");
49
+ const agent_errors_1 = require("../lib/utils/agent-errors");
49
50
  const filter_builder_1 = require("../lib/utils/filter-builder");
50
51
  const exit_1 = require("../lib/utils/exit");
51
52
  const project_registry_1 = require("../lib/utils/project-registry");
@@ -85,8 +86,7 @@ exports.related = new commander_1.Command("related")
85
86
  .where(`path = '${(0, filter_builder_1.escapeSqlString)(absPath)}'`)
86
87
  .toArray();
87
88
  if (fileChunks.length === 0) {
88
- console.log(`File not found in index: ${file}`);
89
- console.log("\nCheck that the path is relative to the project root. Run `gmax status` to see indexed projects.");
89
+ console.log((0, agent_errors_1.fileNotFoundLines)(file, { agent: opts.agent }).join("\n"));
90
90
  process.exitCode = 1;
91
91
  return;
92
92
  }
@@ -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, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
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;
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
- // Check for running server
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 = (0, server_registry_1.getServerForProject)(projectRootForServer);
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
- const pathFilter = options.in && options.in.length > 0
556
- ? scope.pathPrefix
557
- : exec_path
558
- ? (() => {
559
- const p = path.resolve(exec_path);
560
- return p.endsWith("/") ? p : `${p}/`;
561
- })()
562
- : scope.pathPrefix;
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 (scope.inPrefixes.length > 0)
571
- searchFilters.inPrefixes = scope.inPrefixes;
572
- if (scope.excludePrefixes.length > 0)
573
- searchFilters.excludePrefixes = scope.excludePrefixes;
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: ${(_d = resp.error) !== null && _d !== void 0 ? _d : "unknown"}`);
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 && !((_e = process.env.NODE_ENV) === null || _e === void 0 ? void 0 : _e.includes("test"))) {
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 && ((_f = searchResult.warnings) === null || _f === void 0 ? void 0 : _f.length)) {
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 (_0) {
802
+ catch (_1) {
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 (_1) { }
930
+ catch (_2) { }
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
@@ -836,9 +965,9 @@ Examples:
836
965
  let shown = 0;
837
966
  console.log(resultCountHeader(filteredData, parseInt(options.m, 10)));
838
967
  for (const r of filteredData) {
839
- const absP = (_j = (_g = r.path) !== null && _g !== void 0 ? _g : (_h = r.metadata) === null || _h === void 0 ? void 0 : _h.path) !== null && _j !== void 0 ? _j : "";
840
- const startLine = (_o = (_l = (_k = r.startLine) !== null && _k !== void 0 ? _k : r.start_line) !== null && _l !== void 0 ? _l : (_m = r.generated_metadata) === null || _m === void 0 ? void 0 : _m.start_line) !== null && _o !== void 0 ? _o : 0;
841
- const endLine = (_s = (_q = (_p = r.endLine) !== null && _p !== void 0 ? _p : r.end_line) !== null && _q !== void 0 ? _q : (_r = r.generated_metadata) === null || _r === void 0 ? void 0 : _r.end_line) !== null && _s !== void 0 ? _s : startLine;
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;
842
971
  const relPath = absP.startsWith(projectRoot)
843
972
  ? absP.slice(projectRoot.length + 1)
844
973
  : absP;
@@ -871,7 +1000,7 @@ Examples:
871
1000
  tokensUsed += blobTokens;
872
1001
  shown++;
873
1002
  }
874
- catch (_2) {
1003
+ catch (_3) {
875
1004
  console.log(`\n--- ${relPath} (file not readable) ---`);
876
1005
  shown++;
877
1006
  }
@@ -888,7 +1017,7 @@ Examples:
888
1017
  if (options.imports) {
889
1018
  const seenFiles = new Set();
890
1019
  for (const r of filteredData) {
891
- const absP = (_v = (_t = r.path) !== null && _t !== void 0 ? _t : (_u = r.metadata) === null || _u === void 0 ? void 0 : _u.path) !== null && _v !== void 0 ? _v : "";
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 : "";
892
1021
  if (absP && !seenFiles.has(absP)) {
893
1022
  seenFiles.add(absP);
894
1023
  const imports = getImportsForFile(absP);
@@ -915,7 +1044,7 @@ Examples:
915
1044
  for (const r of filteredData) {
916
1045
  const b = r.scoreBreakdown;
917
1046
  if (b) {
918
- const absP = (_y = (_w = r.path) !== null && _w !== void 0 ? _w : (_x = r.metadata) === null || _x === void 0 ? void 0 : _x.path) !== null && _y !== void 0 ? _y : "";
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 : "";
919
1048
  const relPath = absP.startsWith(projectRoot)
920
1049
  ? absP.slice(projectRoot.length + 1)
921
1050
  : absP;
@@ -977,7 +1106,7 @@ Examples:
977
1106
  console.log(lines.join("\n"));
978
1107
  }
979
1108
  }
980
- catch (_3) {
1109
+ catch (_4) {
981
1110
  // Trace failed — skip silently
982
1111
  }
983
1112
  }
@@ -997,13 +1126,13 @@ Examples:
997
1126
  source: "cli",
998
1127
  tool: "search",
999
1128
  query: pattern,
1000
- project: (_z = (0, project_root_1.findProjectRoot)(root)) !== null && _z !== void 0 ? _z : root,
1129
+ project: (_0 = (0, project_root_1.findProjectRoot)(root)) !== null && _0 !== void 0 ? _0 : root,
1001
1130
  results: _searchResultCount,
1002
1131
  ms: Date.now() - _searchStartMs,
1003
1132
  error: _searchError,
1004
1133
  });
1005
1134
  }
1006
- catch (_4) { }
1135
+ catch (_5) { }
1007
1136
  if (vectorDb) {
1008
1137
  try {
1009
1138
  yield vectorDb.close();
@@ -46,6 +46,7 @@ exports.similar = void 0;
46
46
  const path = __importStar(require("node:path"));
47
47
  const commander_1 = require("commander");
48
48
  const vector_db_1 = require("../lib/store/vector-db");
49
+ const agent_errors_1 = require("../lib/utils/agent-errors");
49
50
  const filter_builder_1 = require("../lib/utils/filter-builder");
50
51
  const exit_1 = require("../lib/utils/exit");
51
52
  const project_registry_1 = require("../lib/utils/project-registry");
@@ -96,9 +97,9 @@ exports.similar = new commander_1.Command("similar")
96
97
  .toArray();
97
98
  }
98
99
  if (sourceRows.length === 0) {
99
- console.log(isFile
100
- ? `File not found in index: ${target}`
101
- : `Symbol not found: ${target}`);
100
+ console.log((isFile
101
+ ? (0, agent_errors_1.fileNotFoundLines)(target, { agent: opts.agent })
102
+ : (0, agent_errors_1.symbolNotFoundLines)(target, { agent: opts.agent })).join("\n"));
102
103
  process.exitCode = 1;
103
104
  return;
104
105
  }
@@ -48,6 +48,7 @@ const commander_1 = require("commander");
48
48
  const graph_builder_1 = require("../lib/graph/graph-builder");
49
49
  const formatter_1 = require("../lib/output/formatter");
50
50
  const vector_db_1 = require("../lib/store/vector-db");
51
+ const agent_errors_1 = require("../lib/utils/agent-errors");
51
52
  const exit_1 = require("../lib/utils/exit");
52
53
  const project_registry_1 = require("../lib/utils/project-registry");
53
54
  const project_root_1 = require("../lib/utils/project-root");
@@ -221,7 +222,7 @@ exports.trace = new commander_1.Command("trace")
221
222
  const graph = yield graphBuilder.buildGraphMultiHop(symbol, depth);
222
223
  if (opts.inbound) {
223
224
  if (!graph.center) {
224
- console.log(opts.agent ? "(not found)" : `Symbol not found: ${symbol}`);
225
+ console.log((0, agent_errors_1.symbolNotFoundLines)(symbol, { agent: opts.agent }).join("\n"));
225
226
  process.exitCode = 1;
226
227
  }
227
228
  else {
package/dist/index.js CHANGED
@@ -48,6 +48,7 @@ const codex_1 = require("./commands/codex");
48
48
  const config_1 = require("./commands/config");
49
49
  const doctor_1 = require("./commands/doctor");
50
50
  const extract_1 = require("./commands/extract");
51
+ const help_agent_1 = require("./commands/help-agent");
51
52
  const impact_1 = require("./commands/impact");
52
53
  const droid_1 = require("./commands/droid");
53
54
  const index_1 = require("./commands/index");
@@ -110,6 +111,7 @@ commander_1.program.addCommand(add_1.add);
110
111
  commander_1.program.addCommand(remove_1.remove);
111
112
  commander_1.program.addCommand(index_1.index);
112
113
  commander_1.program.addCommand(status_1.status);
114
+ commander_1.program.addCommand(help_agent_1.helpAgent);
113
115
  commander_1.program.addCommand(list_1.list);
114
116
  commander_1.program.addCommand(skeleton_1.skeleton);
115
117
  commander_1.program.addCommand(symbols_1.symbols);
@@ -355,8 +355,9 @@ class Daemon {
355
355
  // index — this is the dominant cost)
356
356
  // - FTS index "already exists" round-trip
357
357
  // - Two parallel encodeQuery calls so the worker pool spawns + warms
358
- // two workers (the reaper keeps min 2 alive). With one worker busy
359
- // on a long indexing batch, the second is always free for searches.
358
+ // workers ahead of the first real search. The reaper floor is
359
+ // MIN_KEEP_WORKERS = 1 (pool.ts), so only one worker stays resident
360
+ // long-term — but the prewarm still pays the model-load cost up front.
360
361
  // Fire-and-forget; failures are non-fatal — the next real search just
361
362
  // pays the cost once. Delay a few seconds so we don't compete with the
362
363
  // catchup scans dispatched on startup.
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * Canonical agent cheatsheet — the single source of truth for the gmax command
4
+ * survey shown to agent sessions. Consumed by two places:
5
+ * - `gmax help-agent` (re-summon the survey on demand, e.g. after a session
6
+ * has compacted away the original SessionStart injection)
7
+ * - the SessionStart hook (`plugins/grepmax/hooks/start.js`), which loads this
8
+ * compiled module from the installed package and falls back to an inline
9
+ * copy only if the require fails — so the two can never silently drift.
10
+ *
11
+ * Keep this file as plain string data with NO imports so the hook can cheaply
12
+ * `require()` the compiled CommonJS output.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SESSION_START_HINT = exports.AGENT_CHEATSHEET = exports.SESSION_START_PREFIX = void 0;
16
+ /** Intro line shown only at SessionStart (not part of the on-demand command). */
17
+ exports.SESSION_START_PREFIX = "gmax ready. Add --agent to any command for compact output (~89% fewer tokens).";
18
+ /** The command survey — Find / Understand / Survey + scope/roles/recovery. */
19
+ exports.AGENT_CHEATSHEET = `Find:
20
+ gmax "topic" semantic search
21
+ gmax similar <symbol> similar code
22
+
23
+ Understand:
24
+ gmax peek <symbol> signature + callers + callees + tests
25
+ gmax extract <symbol> full body + tests
26
+ gmax trace <symbol> call graph (--inbound = callers + snippets)
27
+ gmax test <symbol> tests for symbol
28
+ gmax impact <symbol> blast radius
29
+ gmax related <file> file deps + dependents
30
+
31
+ Survey:
32
+ gmax project codebase overview (langs, structure, key symbols)
33
+ gmax skeleton <file> file structure (file path, NOT a directory)
34
+ gmax context "topic-or-path" --budget 4000 topic summary or deterministic file/dir context
35
+ gmax log <path-or-symbol> git commits (replaces recent/diff)
36
+ gmax status indexed projects
37
+
38
+ Scope flags: --root <name|path>, --in <subpath>, --exclude <subpath>.
39
+ Roles in results: [DEFI] [ORCH] [IMPL] [DOCS].
40
+ Recovery: "not added yet" → gmax add; stale → gmax index; broken → gmax doctor --fix.`;
41
+ /** Full SessionStart context = prefix + cheatsheet. */
42
+ exports.SESSION_START_HINT = `${exports.SESSION_START_PREFIX}\n\n${exports.AGENT_CHEATSHEET}`;
@@ -304,9 +304,6 @@ class Searcher {
304
304
  adjusted *= boostFactor;
305
305
  }
306
306
  }
307
- if (record.role === "DOCS") {
308
- adjusted *= 0.6;
309
- }
310
307
  const pathStr = (record.path || "").toLowerCase();
311
308
  // Use path-segment and filename patterns to avoid false positives like "latest"
312
309
  const isTestPath = /(^|\/)(__tests__|tests?|specs?|benchmark)(\/|$)/i.test(pathStr) ||
@@ -315,14 +312,21 @@ class Searcher {
315
312
  const testPenalty = Number.parseFloat((_c = process.env.GMAX_TEST_PENALTY) !== null && _c !== void 0 ? _c : "") || 0.5;
316
313
  adjusted *= testPenalty;
317
314
  }
318
- if (pathStr.endsWith(".md") ||
315
+ // Downweight docs/data — applied ONCE whether the chunk is classified DOCS
316
+ // by role OR lives in a doc/data file. GMAX_DOC_PENALTY tunes both. (S2: the
317
+ // role branch was previously a hardcoded 0.6 that stacked with the path
318
+ // branch, double-penalizing a DOCS-role chunk in a .md / /docs/ path to
319
+ // 0.36 — and the role half was not env-tunable.)
320
+ const isDocOrData = record.role === "DOCS" ||
321
+ pathStr.endsWith(".md") ||
319
322
  pathStr.endsWith(".mdx") ||
320
323
  pathStr.endsWith(".txt") ||
321
324
  pathStr.endsWith(".json") ||
322
325
  pathStr.endsWith(".lock") ||
323
- pathStr.includes("/docs/")) {
326
+ pathStr.includes("/docs/");
327
+ if (isDocOrData) {
324
328
  const docPenalty = Number.parseFloat((_d = process.env.GMAX_DOC_PENALTY) !== null && _d !== void 0 ? _d : "") || 0.6;
325
- adjusted *= docPenalty; // Downweight docs/data
329
+ adjusted *= docPenalty;
326
330
  }
327
331
  // Import-only penalty
328
332
  if ((record.content || "").length < 50 && !record.is_exported) {
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * Shared "not found" / empty-result rendering for the symbol and file lookup
4
+ * commands (peek/extract/trace/dead/impact/similar/related). Two jobs:
5
+ *
6
+ * 1. Unify the format — previously each command emitted its own variant
7
+ * ("Symbol not found: X", "(not found)", bare one-liners), and only
8
+ * peek/extract carried the rich human "Possible reasons / Try:" block.
9
+ * 2. Stop discarding recovery hints under `--agent` — human mode keeps the
10
+ * full block; agent mode now gets a compact trailing `next:` line instead
11
+ * of a bare, dead-end error.
12
+ *
13
+ * Returns lines (caller joins with "\n"). `dim`/`bold` default to identity so
14
+ * the helper stays color-agnostic; commands pass their own `style.*` to
15
+ * preserve existing human coloring.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.symbolNotFoundLines = symbolNotFoundLines;
19
+ exports.fileNotFoundLines = fileNotFoundLines;
20
+ const identity = (s) => s;
21
+ /** `Symbol not found: X` — symbol lookup miss. */
22
+ function symbolNotFoundLines(symbol, opts = {}) {
23
+ const { agent = false, dim = identity, bold = identity } = opts;
24
+ if (agent) {
25
+ return [
26
+ `Symbol not found: ${symbol}`,
27
+ `next: gmax status (indexed projects) · gmax search ${symbol} (fuzzy match)`,
28
+ ];
29
+ }
30
+ return [
31
+ `Symbol not found: ${bold(symbol)}`,
32
+ "",
33
+ dim("Possible reasons:"),
34
+ dim(" • The symbol doesn't exist in any indexed project"),
35
+ dim(" • The containing file hasn't been indexed yet"),
36
+ dim(" • The name is spelled differently in the source"),
37
+ "",
38
+ dim("Try:"),
39
+ dim(" gmax status — see which projects are indexed"),
40
+ dim(" gmax search <name> — fuzzy search for similar symbols"),
41
+ ];
42
+ }
43
+ /** `File not found in index: X` — file lookup miss. */
44
+ function fileNotFoundLines(file, opts = {}) {
45
+ const { agent = false, dim = identity, bold = identity } = opts;
46
+ if (agent) {
47
+ return [
48
+ `File not found in index: ${file}`,
49
+ `next: use a path relative to the project root · gmax status · gmax add <project> (if untracked)`,
50
+ ];
51
+ }
52
+ return [
53
+ `File not found in index: ${bold(file)}`,
54
+ "",
55
+ dim("Try:"),
56
+ dim(" ensure the path is relative to the project root"),
57
+ dim(" gmax status — see which projects are indexed"),
58
+ dim(" gmax add <project> — index a project that isn't tracked yet"),
59
+ ];
60
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.18",
3
+ "version": "0.17.20",
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.18",
3
+ "version": "0.17.20",
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,65 @@ function findMlxServerDir() {
42
42
  return null;
43
43
  }
44
44
 
45
+ // Inline fallback so SessionStart context is never empty if the installed
46
+ // gmax package can't be resolved (e.g. gmax not yet on PATH). The canonical
47
+ // copy lives in src/lib/help/agent-cheatsheet.ts; getSessionStartHint() prefers
48
+ // it and only uses this if the require fails. Keep the two in sync.
49
+ const FALLBACK_SESSION_START_HINT = `gmax ready. Add --agent to any command for compact output (~89% fewer tokens).
50
+
51
+ Find:
52
+ gmax "topic" semantic search
53
+ gmax similar <symbol> similar code
54
+
55
+ Understand:
56
+ gmax peek <symbol> signature + callers + callees + tests
57
+ gmax extract <symbol> full body + tests
58
+ gmax trace <symbol> call graph (--inbound = callers + snippets)
59
+ gmax test <symbol> tests for symbol
60
+ gmax impact <symbol> blast radius
61
+ gmax related <file> file deps + dependents
62
+
63
+ Survey:
64
+ gmax project codebase overview (langs, structure, key symbols)
65
+ gmax skeleton <file> file structure (file path, NOT a directory)
66
+ gmax context "topic-or-path" --budget 4000 topic summary or deterministic file/dir context
67
+ gmax log <path-or-symbol> git commits (replaces recent/diff)
68
+ gmax status indexed projects
69
+
70
+ Scope flags: --root <name|path>, --in <subpath>, --exclude <subpath>.
71
+ Roles in results: [DEFI] [ORCH] [IMPL] [DOCS].
72
+ Recovery: "not added yet" → gmax add; stale → gmax index; broken → gmax doctor --fix.`;
73
+
74
+ // Load the canonical cheatsheet from the installed gmax package (single source
75
+ // of truth shared with `gmax help-agent`). Tries the npm-resolved package root
76
+ // first, then the dev checkout, then the inline fallback above.
77
+ function getSessionStartHint() {
78
+ const roots = [];
79
+ try {
80
+ const gmaxPath = execFileSync("which", ["gmax"], {
81
+ encoding: "utf-8",
82
+ }).trim();
83
+ if (gmaxPath) {
84
+ const realPath = fs.realpathSync(gmaxPath);
85
+ roots.push(_path.resolve(_path.dirname(realPath), ".."));
86
+ }
87
+ } catch {}
88
+ // dev mode — plugin lives at <repo>/plugins/grepmax/hooks
89
+ roots.push(_path.resolve(__dirname, "../../.."));
90
+
91
+ for (const root of roots) {
92
+ try {
93
+ const mod = require(
94
+ _path.join(root, "dist", "lib", "help", "agent-cheatsheet.js"),
95
+ );
96
+ if (mod && typeof mod.SESSION_START_HINT === "string") {
97
+ return mod.SESSION_START_HINT;
98
+ }
99
+ } catch {}
100
+ }
101
+ return FALLBACK_SESSION_START_HINT;
102
+ }
103
+
45
104
  function startPythonServer(serverDir, scriptName, logName, processName) {
46
105
  if (!serverDir) return;
47
106
 
@@ -175,30 +234,7 @@ async function main() {
175
234
  const response = {
176
235
  hookSpecificOutput: {
177
236
  hookEventName: "SessionStart",
178
- additionalContext: `gmax ready. Add --agent to any command for compact output (~89% fewer tokens).
179
-
180
- Find:
181
- gmax "topic" semantic search
182
- gmax similar <symbol> similar code
183
-
184
- Understand:
185
- gmax peek <symbol> signature + callers + callees + tests
186
- gmax extract <symbol> full body + tests
187
- gmax trace <symbol> call graph (--inbound = callers + snippets)
188
- gmax test <symbol> tests for symbol
189
- gmax impact <symbol> blast radius
190
- gmax related <file> file deps + dependents
191
-
192
- Survey:
193
- gmax project codebase overview (langs, structure, key symbols)
194
- gmax skeleton <file> file structure (file path, NOT a directory)
195
- gmax context "topic-or-path" --budget 4000 topic summary or deterministic file/dir context
196
- gmax log <path-or-symbol> git commits (replaces recent/diff)
197
- gmax status indexed projects
198
-
199
- Scope flags: --root <name|path>, --in <subpath>, --exclude <subpath>.
200
- Roles in results: [DEFI] [ORCH] [IMPL] [DOCS].
201
- Recovery: "not added yet" → gmax add; stale → gmax index; broken → gmax doctor --fix.`,
237
+ additionalContext: getSessionStartHint(),
202
238
  },
203
239
  };
204
240
  process.stdout.write(JSON.stringify(response));