grepmax 0.26.2 → 0.26.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.26.2",
13
+ "version": "0.26.4",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
@@ -45,7 +45,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.context = void 0;
46
46
  exports.findEnclosingSignature = findEnclosingSignature;
47
47
  const fs = __importStar(require("node:fs"));
48
- const path = __importStar(require("node:path"));
49
48
  const commander_1 = require("commander");
50
49
  const searcher_1 = require("../lib/search/searcher");
51
50
  const skeleton_1 = require("../lib/skeleton");
@@ -102,20 +101,6 @@ function findEnclosingSignature(lines, startLine, parentSymbol) {
102
101
  }
103
102
  return null;
104
103
  }
105
- function resolveExistingPath(target, root, projectRoot) {
106
- const candidates = [
107
- path.isAbsolute(target) ? target : path.resolve(root, target),
108
- path.resolve(projectRoot, target),
109
- ];
110
- for (const candidate of candidates) {
111
- if (!fs.existsSync(candidate))
112
- continue;
113
- return (0, path_containment_1.resolveContainedPath)(projectRoot, candidate, {
114
- verifyExistingTarget: true,
115
- });
116
- }
117
- return null;
118
- }
119
104
  function renderPathContext(target, absPath, projectRoot, budget) {
120
105
  return __awaiter(this, void 0, void 0, function* () {
121
106
  const state = {
@@ -192,7 +177,10 @@ exports.context = new commander_1.Command("context")
192
177
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
193
178
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
194
179
  vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
195
- const pathTarget = resolveExistingPath(topic, root, projectRoot);
180
+ const pathTarget = (0, path_containment_1.resolveContainedExistingPath)(projectRoot, topic, {
181
+ cwd: root,
182
+ onOutside: "throw",
183
+ });
196
184
  if (pathTarget) {
197
185
  const rendered = yield renderPathContext(topic, pathTarget, projectRoot, budget);
198
186
  rendered.sections.push(`\n(~${rendered.tokensUsed}/${budget} tokens used)`);
@@ -50,6 +50,7 @@ const agent_errors_1 = require("../lib/utils/agent-errors");
50
50
  const arrow_1 = require("../lib/utils/arrow");
51
51
  const exit_1 = require("../lib/utils/exit");
52
52
  const filter_builder_1 = require("../lib/utils/filter-builder");
53
+ const path_containment_1 = require("../lib/utils/path-containment");
53
54
  const project_registry_1 = require("../lib/utils/project-registry");
54
55
  const project_root_1 = require("../lib/utils/project-root");
55
56
  const query_timeout_1 = require("../lib/utils/query-timeout");
@@ -63,7 +64,7 @@ exports.related = new commander_1.Command("related")
63
64
  .option("--exclude <subpath>", "Exclude a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
64
65
  .option("--agent", "Compact output for AI agents", false)
65
66
  .action((file, opts) => __awaiter(void 0, void 0, void 0, function* () {
66
- var _a;
67
+ var _a, _b;
67
68
  const limit = Math.min(Math.max(Number.parseInt(opts.limit || "10", 10), 1), 25);
68
69
  let vectorDb = null;
69
70
  try {
@@ -75,7 +76,7 @@ exports.related = new commander_1.Command("related")
75
76
  (0, stale_hint_1.maybeWarnStaleEmbedding)(projectRoot, { agent: opts.agent });
76
77
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
77
78
  vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
78
- const absPath = path.resolve(projectRoot, file);
79
+ const absPath = (_b = (0, path_containment_1.resolveContainedExistingPath)(projectRoot, file)) !== null && _b !== void 0 ? _b : path.resolve(projectRoot, file);
79
80
  const table = yield vectorDb.ensureTable();
80
81
  const { resolveScope, buildScopeWhere } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/scope-filter")));
81
82
  const scope = resolveScope({
@@ -267,7 +268,7 @@ exports.related = new commander_1.Command("related")
267
268
  try {
268
269
  yield vectorDb.close();
269
270
  }
270
- catch (_b) { }
271
+ catch (_c) { }
271
272
  }
272
273
  yield (0, exit_1.gracefulExit)();
273
274
  }
@@ -43,10 +43,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.search = void 0;
46
+ const path = __importStar(require("node:path"));
46
47
  const commander_1 = require("commander");
47
48
  const setup_helpers_1 = require("../lib/setup/setup-helpers");
48
49
  const cross_project_1 = require("../lib/utils/cross-project");
49
50
  const exit_1 = require("../lib/utils/exit");
51
+ const path_containment_1 = require("../lib/utils/path-containment");
50
52
  const project_registry_1 = require("../lib/utils/project-registry");
51
53
  const project_root_1 = require("../lib/utils/project-root");
52
54
  const scope_filter_1 = require("../lib/utils/scope-filter");
@@ -99,7 +101,7 @@ Examples:
99
101
  gmax "auth middleware" --projects api,gateway --plain
100
102
  `)
101
103
  .action((pattern, exec_path, _options, cmd) => __awaiter(void 0, void 0, void 0, function* () {
102
- var _a, _b, _c, _d, _e, _f;
104
+ var _a, _b, _c, _d, _e, _f, _g;
103
105
  const options = cmd.optsWithGlobals();
104
106
  const root = process.cwd();
105
107
  const minScore = Number.isFinite(Number.parseFloat(options.minScore))
@@ -161,6 +163,17 @@ Examples:
161
163
  }
162
164
  const cwdProjectRoot = (_d = (0, project_root_1.findProjectRoot)(root)) !== null && _d !== void 0 ? _d : root;
163
165
  const projectRootForServer = resolvedOptionRoot !== null && resolvedOptionRoot !== void 0 ? resolvedOptionRoot : cwdProjectRoot;
166
+ // A cwd inside a nested git repo covered by a registered umbrella project
167
+ // resolves to the umbrella (findProjectRoot). Scope the search to the
168
+ // subrepo and resolve relative subpaths against it — that's what a caller
169
+ // inside the subrepo means by "this project". --root overrides this.
170
+ const cwdGitRoot = crossProject.active ? null : (0, project_root_1.findGitRoot)(root);
171
+ const nestedScopeRoot = !options.root &&
172
+ cwdGitRoot &&
173
+ cwdGitRoot !== cwdProjectRoot &&
174
+ (0, path_containment_1.isPathWithin)(cwdProjectRoot, cwdGitRoot)
175
+ ? cwdGitRoot
176
+ : undefined;
164
177
  if (exec_path && options.in && options.in.length > 0) {
165
178
  console.warn("Warning: --in overrides positional [path]; using --in.");
166
179
  }
@@ -168,11 +181,14 @@ Examples:
168
181
  try {
169
182
  scope = (0, scope_filter_1.resolveScope)({
170
183
  projectRoot: projectRootForServer,
184
+ base: nestedScopeRoot,
171
185
  in: options.in && options.in.length > 0
172
186
  ? options.in
173
187
  : exec_path
174
188
  ? [exec_path]
175
- : undefined,
189
+ : nestedScopeRoot
190
+ ? [nestedScopeRoot]
191
+ : undefined,
176
192
  exclude: options.exclude,
177
193
  });
178
194
  }
@@ -182,6 +198,13 @@ Examples:
182
198
  yield (0, exit_1.gracefulExit)(1);
183
199
  return;
184
200
  }
201
+ // Only announce the scope when it was implied by the cwd; an explicit
202
+ // --in/[path] already states what the caller scoped to.
203
+ if (nestedScopeRoot && !((_e = options.in) === null || _e === void 0 ? void 0 : _e.length) && !exec_path) {
204
+ const subName = path.relative(cwdProjectRoot, nestedScopeRoot);
205
+ const projName = path.basename(cwdProjectRoot);
206
+ console.error(`Scoped to ${subName}/ within ${projName}; pass --root ${projName} to search the whole project.`);
207
+ }
185
208
  const containedExecPath = exec_path && !(options.in && options.in.length > 0)
186
209
  ? scope.pathPrefix.replace(/\/$/, "")
187
210
  : undefined;
@@ -212,7 +235,7 @@ Examples:
212
235
  : (0, project_registry_1.resolveRootOrExit)(options.root);
213
236
  if (resolved === null)
214
237
  return;
215
- checkRoot = (_e = (0, project_root_1.findProjectRoot)(resolved)) !== null && _e !== void 0 ? _e : resolved;
238
+ checkRoot = (_f = (0, project_root_1.findProjectRoot)(resolved)) !== null && _f !== void 0 ? _f : resolved;
216
239
  }
217
240
  const projectRoot = crossProject.active ? cwdProjectRoot : checkRoot;
218
241
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
@@ -320,13 +343,13 @@ Examples:
320
343
  source: "cli",
321
344
  tool: "search",
322
345
  query: pattern,
323
- project: (_f = (0, project_root_1.findProjectRoot)(root)) !== null && _f !== void 0 ? _f : root,
346
+ project: (_g = (0, project_root_1.findProjectRoot)(root)) !== null && _g !== void 0 ? _g : root,
324
347
  results: _searchResultCount,
325
348
  ms: Date.now() - _searchStartMs,
326
349
  error: _searchError,
327
350
  });
328
351
  }
329
- catch (_g) { }
352
+ catch (_h) { }
330
353
  if (vectorDb) {
331
354
  try {
332
355
  yield vectorDb.close();
@@ -128,7 +128,7 @@ Examples:
128
128
  gmax skeleton "auth logic" Search, skeletonize top matches
129
129
  `)
130
130
  .action((target, options, _cmd) => __awaiter(void 0, void 0, void 0, function* () {
131
- var _a, _b;
131
+ var _a, _b, _c, _d, _e;
132
132
  let vectorDb = null;
133
133
  try {
134
134
  // Initialize
@@ -166,8 +166,10 @@ Examples:
166
166
  const skeletonOpts = {
167
167
  includeSummary: !options.noSummary,
168
168
  };
169
- // Determine mode based on target
170
- const resolvedTarget = (0, path_containment_1.resolveContainedPath)(projectRoot, target, {
169
+ // Determine mode based on target. Prefer an existing cwd-relative
170
+ // match so callers inside a subdirectory (or nested subrepo) can pass
171
+ // paths relative to where they are.
172
+ const resolvedTarget = (_b = (0, path_containment_1.resolveContainedExistingPath)(projectRoot, target)) !== null && _b !== void 0 ? _b : (0, path_containment_1.resolveContainedPath)(projectRoot, target, {
171
173
  verifyExistingTarget: true,
172
174
  });
173
175
  // Directory mode is unsupported. Auto-picking files from a directory
@@ -191,7 +193,7 @@ Examples:
191
193
  .map((t) => t.trim())
192
194
  .filter(Boolean);
193
195
  for (const t of targets) {
194
- const filePath = (0, path_containment_1.resolveContainedPath)(projectRoot, t, {
196
+ const filePath = (_c = (0, path_containment_1.resolveContainedExistingPath)(projectRoot, t)) !== null && _c !== void 0 ? _c : (0, path_containment_1.resolveContainedPath)(projectRoot, t, {
195
197
  verifyExistingTarget: true,
196
198
  });
197
199
  if (!fs.existsSync(filePath)) {
@@ -206,7 +208,7 @@ Examples:
206
208
  }
207
209
  if (isFilePath(target)) {
208
210
  // === FILE MODE ===
209
- const filePath = (0, path_containment_1.resolveContainedPath)(projectRoot, target, {
211
+ const filePath = (_d = (0, path_containment_1.resolveContainedExistingPath)(projectRoot, target)) !== null && _d !== void 0 ? _d : (0, path_containment_1.resolveContainedPath)(projectRoot, target, {
210
212
  verifyExistingTarget: true,
211
213
  });
212
214
  if (!fs.existsSync(filePath)) {
@@ -275,7 +277,7 @@ Examples:
275
277
  const seenPaths = new Set();
276
278
  const filePaths = [];
277
279
  for (const result of searchResults.data) {
278
- const resultPath = (_b = result.metadata) === null || _b === void 0 ? void 0 : _b.path;
280
+ const resultPath = (_e = result.metadata) === null || _e === void 0 ? void 0 : _e.path;
279
281
  if (resultPath && !seenPaths.has(resultPath)) {
280
282
  seenPaths.add(resultPath);
281
283
  filePaths.push(resultPath);
@@ -293,7 +295,7 @@ Examples:
293
295
  verifyExistingTarget: true,
294
296
  });
295
297
  }
296
- catch (_c) {
298
+ catch (_f) {
297
299
  results.push({
298
300
  file: filePath,
299
301
  skeleton: `// File outside selected project: ${filePath}`,
@@ -352,7 +354,7 @@ Examples:
352
354
  try {
353
355
  yield vectorDb.close();
354
356
  }
355
- catch (_d) {
357
+ catch (_g) {
356
358
  // Ignore close errors
357
359
  }
358
360
  }
@@ -121,12 +121,14 @@ class WatcherManager {
121
121
  try {
122
122
  const filePolicy = new file_policy_1.ProjectFilePolicy(root);
123
123
  let processor;
124
+ let lastReindexAt;
124
125
  processor = new batch_processor_1.ProjectBatchProcessor(Object.assign(Object.assign({ projectRoot: root, vectorDb,
125
126
  metaCache }, (workerPool ? { workerPool } : {})), { filePolicy, onPolicyChange: () => {
126
127
  void this.runCatchup(root, processor).catch((err) => {
127
128
  console.error(`[daemon:${path.basename(root)}] Policy reconciliation failed:`, err);
128
129
  });
129
130
  }, onReindex: (files, ms) => __awaiter(this, void 0, void 0, function* () {
131
+ lastReindexAt = Date.now();
130
132
  console.log(`[daemon:${path.basename(root)}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(ms / 1000).toFixed(1)}s)`);
131
133
  // Update project registry so gmax status shows fresh data
132
134
  const proj = (0, project_registry_1.getProject)(root);
@@ -140,16 +142,13 @@ class WatcherManager {
140
142
  }
141
143
  (0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { lastIndexed: new Date().toISOString(), chunkCount }));
142
144
  }
143
- // Back to watching after batch completes
144
- (0, watcher_store_1.registerWatcher)({
145
- pid: process.pid,
146
- projectRoot: root,
147
- startTime: Date.now(),
148
- status: this.isRootDegraded(root) ? "degraded" : "watching",
149
- lastHeartbeat: Date.now(),
150
- lastReindex: Date.now(),
151
- });
152
- }), onActivity: () => {
145
+ }), onBatchSettled: () => {
146
+ (0, watcher_store_1.registerWatcher)(Object.assign({ pid: process.pid, projectRoot: root, startTime: Date.now(), status: this.isRootDegraded(root)
147
+ ? "degraded"
148
+ : processor.progress.pendingFiles > 0
149
+ ? "syncing"
150
+ : "watching", lastHeartbeat: Date.now() }, (lastReindexAt ? { lastReindex: lastReindexAt } : {})));
151
+ }, onActivity: () => {
153
152
  this.deps.touchActivity();
154
153
  // Mark as syncing while processing
155
154
  (0, watcher_store_1.registerWatcher)({
@@ -78,6 +78,7 @@ class ProjectBatchProcessor {
78
78
  this.workerPool = (_a = opts.workerPool) !== null && _a !== void 0 ? _a : (0, pool_1.getWorkerPool)();
79
79
  this.onReindex = opts.onReindex;
80
80
  this.onActivity = opts.onActivity;
81
+ this.onBatchSettled = opts.onBatchSettled;
81
82
  this.onPolicyChange = opts.onPolicyChange;
82
83
  this.onTerminalFailure = opts.onTerminalFailure;
83
84
  this.onPathSuccess = opts.onPathSuccess;
@@ -190,7 +191,7 @@ class ProjectBatchProcessor {
190
191
  }
191
192
  processBatch(operationSignal) {
192
193
  return __awaiter(this, void 0, void 0, function* () {
193
- var _a, _b, _c, _d, _e;
194
+ var _a, _b, _c, _d, _e, _f;
194
195
  if (this.closed || this.processing || this.pending.size === 0)
195
196
  return;
196
197
  const diskPressure = this.vectorDb.checkDiskPressure();
@@ -574,6 +575,8 @@ class ProjectBatchProcessor {
574
575
  this.schedulePendingBatch();
575
576
  }
576
577
  }
578
+ if (!this.closed)
579
+ (_f = this.onBatchSettled) === null || _f === void 0 ? void 0 : _f.call(this);
577
580
  }
578
581
  });
579
582
  }
@@ -195,6 +195,7 @@ function startWatcher(opts) {
195
195
  processor = new batch_processor_1.ProjectBatchProcessor(Object.assign(Object.assign({}, opts), { filePolicy, onPolicyChange: reconcile, onReindex: (files, durationMs) => {
196
196
  var _a;
197
197
  (_a = opts.onReindex) === null || _a === void 0 ? void 0 : _a.call(opts, files, durationMs);
198
+ }, onBatchSettled: () => {
198
199
  reportHealthyIfSettled();
199
200
  }, onTerminalFailure: (absPath) => {
200
201
  var _a;
@@ -96,6 +96,10 @@ class VectorDB {
96
96
  this.maintenancePromise = null;
97
97
  this.maintenanceTimer = null;
98
98
  this.maintenanceRunner = (fn) => fn();
99
+ /** Latched when a from-scratch FTS rebuild failed to stop an optimize
100
+ * panic. Stops the (expensive) rebuild from re-running every maintenance
101
+ * tick for a panic it cannot fix; cleared when an optimize succeeds. */
102
+ this.ftsPanicRecoveryExhausted = false;
99
103
  this.lastCorruptionLogMs = 0;
100
104
  this.diskPressure = "ok";
101
105
  this.lastDiskCheckMs = 0;
@@ -781,6 +785,7 @@ class VectorDB {
781
785
  try {
782
786
  const table = yield this.ensureTableUnsafe();
783
787
  const cutoff = new Date(Date.now() - retentionMs);
788
+ let rebuiltFts = false;
784
789
  for (let attempt = 1; attempt <= retries; attempt++) {
785
790
  yield this.drainWrites();
786
791
  try {
@@ -790,6 +795,7 @@ class VectorDB {
790
795
  deleteUnverified: true,
791
796
  });
792
797
  done();
798
+ this.ftsPanicRecoveryExhausted = false;
793
799
  const { compaction, prune } = stats;
794
800
  if (compaction.fragmentsRemoved > 0 ||
795
801
  prune.oldVersionsRemoved > 0 ||
@@ -822,6 +828,34 @@ class VectorDB {
822
828
  yield new Promise((r) => setTimeout(r, delay));
823
829
  continue;
824
830
  }
831
+ // A Rust panic here is the lance-index inverted (FTS) merge bug
832
+ // (out-of-bounds in scalar/inverted/builder.rs): the incremental
833
+ // merge trips on inconsistent index state, and because optimize()
834
+ // is all-or-nothing it blocks compaction and pruning entirely.
835
+ // Rebuild the FTS index from scratch once and retry. Must use the
836
+ // Unsafe variant: the public createFTSIndex waits on
837
+ // compactingPromise, which we hold.
838
+ if (msg.includes("Panic")) {
839
+ if (attempt < retries &&
840
+ !rebuiltFts &&
841
+ !this.ftsPanicRecoveryExhausted) {
842
+ rebuiltFts = true;
843
+ (0, logger_1.log)("vectordb", "Optimize panicked (likely corrupt FTS merge) — rebuilding FTS index from scratch and retrying");
844
+ try {
845
+ yield this.createFTSIndexUnsafe(true);
846
+ continue;
847
+ }
848
+ catch (rebuildErr) {
849
+ this.ftsPanicRecoveryExhausted = true;
850
+ (0, logger_1.log)("vectordb", `FTS rebuild failed: ${rebuildErr}`);
851
+ return;
852
+ }
853
+ }
854
+ if (rebuiltFts) {
855
+ this.ftsPanicRecoveryExhausted = true;
856
+ (0, logger_1.log)("vectordb", "Optimize still panicking after FTS rebuild — disabling auto-rebuild until an optimize succeeds");
857
+ }
858
+ }
825
859
  (0, logger_1.log)("vectordb", `Optimize failed: ${msg}`);
826
860
  return;
827
861
  }
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.PathContainmentError = void 0;
37
37
  exports.isPathWithin = isPathWithin;
38
38
  exports.resolveContainedPath = resolveContainedPath;
39
+ exports.resolveContainedExistingPath = resolveContainedExistingPath;
39
40
  exports.resolveContainedFile = resolveContainedFile;
40
41
  const fs = __importStar(require("node:fs"));
41
42
  const path = __importStar(require("node:path"));
@@ -92,6 +93,43 @@ function resolveContainedPath(root, input, options = {}) {
92
93
  }
93
94
  return candidate;
94
95
  }
96
+ /**
97
+ * Resolve a target that may be cwd-relative or root-relative, preferring an
98
+ * existing cwd-relative match — what a caller sitting in a subdirectory (or
99
+ * a nested subrepo of an umbrella project) means by the path. A candidate
100
+ * that exists but lies outside the root is skipped so the root-relative
101
+ * interpretation can still win. Returns null when no contained candidate
102
+ * exists; with onOutside: "throw", an existing-but-outside match raises
103
+ * PathContainmentError instead (for callers that must loudly reject it).
104
+ */
105
+ function resolveContainedExistingPath(root, target, options = {}) {
106
+ var _a;
107
+ const cwd = (_a = options.cwd) !== null && _a !== void 0 ? _a : process.cwd();
108
+ const candidates = [
109
+ path.isAbsolute(target) ? path.resolve(target) : path.resolve(cwd, target),
110
+ path.resolve(root, target),
111
+ ];
112
+ let outside = null;
113
+ for (const candidate of candidates) {
114
+ if (!fs.existsSync(candidate))
115
+ continue;
116
+ try {
117
+ return resolveContainedPath(root, candidate, {
118
+ verifyExistingTarget: true,
119
+ });
120
+ }
121
+ catch (err) {
122
+ if (err instanceof PathContainmentError) {
123
+ outside !== null && outside !== void 0 ? outside : (outside = err);
124
+ continue;
125
+ }
126
+ throw err;
127
+ }
128
+ }
129
+ if (options.onOutside === "throw" && outside)
130
+ throw outside;
131
+ return null;
132
+ }
95
133
  /** Resolve an existing regular file and return its canonical, contained path. */
96
134
  function resolveContainedFile(root, input) {
97
135
  const candidate = resolveContainedPath(root, input);
@@ -33,30 +33,61 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.findGitRoot = findGitRoot;
36
37
  exports.findProjectRoot = findProjectRoot;
37
38
  exports.ensureProjectPaths = ensureProjectPaths;
38
39
  const fs = __importStar(require("node:fs"));
39
40
  const path = __importStar(require("node:path"));
40
41
  const config_1 = require("../../config");
42
+ const path_containment_1 = require("./path-containment");
43
+ const project_registry_1 = require("./project-registry");
41
44
  /**
42
- * Find the project root for a given directory.
43
- * Looks for .git to determine the project boundary.
44
- * Falls back to the directory itself if no .git found.
45
+ * Walk up from a directory to the nearest ancestor containing .git.
46
+ * Returns null when no .git exists anywhere up the tree.
45
47
  */
46
- function findProjectRoot(startDir = process.cwd()) {
47
- const start = path.resolve(startDir);
48
- // Walk up to find .git
49
- let dir = start;
48
+ function findGitRoot(startDir = process.cwd()) {
49
+ let dir = path.resolve(startDir);
50
50
  while (true) {
51
51
  if (fs.existsSync(path.join(dir, ".git")))
52
52
  return dir;
53
53
  const parent = path.dirname(dir);
54
54
  if (parent === dir)
55
- break; // reached filesystem root
55
+ return null; // reached filesystem root
56
56
  dir = parent;
57
57
  }
58
- // No .git found — treat startDir as root
59
- return start;
58
+ }
59
+ /**
60
+ * Find the project root for a given directory.
61
+ *
62
+ * The nearest .git is only a candidate boundary: a registered project may be
63
+ * an umbrella directory with no .git of its own whose children are separate
64
+ * git repos (so the .git walk lands on an unregistered subrepo), or the
65
+ * nearest .git may sit above the registered root (e.g. a git-tracked home
66
+ * dir). Resolution order:
67
+ * 1. the nearest .git root, when it is itself a registered project
68
+ * 2. the deepest registered project containing startDir
69
+ * 3. the nearest .git root, else startDir (unregistered fallback)
70
+ */
71
+ function findProjectRoot(startDir = process.cwd()) {
72
+ const start = path.resolve(startDir);
73
+ const gitRoot = findGitRoot(start);
74
+ let registered = [];
75
+ try {
76
+ registered = (0, project_registry_1.listProjects)();
77
+ }
78
+ catch (_a) {
79
+ // A corrupt registry shouldn't break root detection; commands that need
80
+ // the registry surface the read error themselves.
81
+ }
82
+ const roots = registered.map((p) => path.resolve(p.root));
83
+ if (gitRoot && roots.includes(gitRoot))
84
+ return gitRoot;
85
+ const ancestor = roots
86
+ .filter((root) => (0, path_containment_1.isPathWithin)(root, start))
87
+ .sort((a, b) => b.length - a.length)[0];
88
+ if (ancestor)
89
+ return ancestor;
90
+ return gitRoot !== null && gitRoot !== void 0 ? gitRoot : start;
60
91
  }
61
92
  /**
62
93
  * Returns centralized paths for storage.
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.resolveScope = resolveScope;
37
37
  exports.buildScopeWhere = buildScopeWhere;
38
+ const fs = __importStar(require("node:fs"));
38
39
  const path = __importStar(require("node:path"));
39
40
  const filter_builder_1 = require("./filter-builder");
40
41
  const path_containment_1 = require("./path-containment");
@@ -49,19 +50,28 @@ function toArray(value) {
49
50
  .map((v) => v.trim())
50
51
  .filter(Boolean);
51
52
  }
52
- function joinSubpath(projectRoot, sub) {
53
- const resolved = (0, path_containment_1.resolveContainedPath)(projectRoot, sub);
53
+ function joinSubpath(projectRoot, base, sub) {
54
+ let candidate = path.isAbsolute(sub) ? sub : path.resolve(base, sub);
55
+ if (!path.isAbsolute(sub) &&
56
+ path.resolve(base) !== path.resolve(projectRoot) &&
57
+ !fs.existsSync(candidate) &&
58
+ fs.existsSync(path.resolve(projectRoot, sub))) {
59
+ candidate = path.resolve(projectRoot, sub);
60
+ }
61
+ const resolved = (0, path_containment_1.resolveContainedPath)(projectRoot, candidate);
54
62
  return resolved.endsWith(path.sep) ? resolved : `${resolved}${path.sep}`;
55
63
  }
56
64
  function resolveScope(opts) {
65
+ var _a;
57
66
  const { projectRoot } = opts;
67
+ const base = (_a = opts.base) !== null && _a !== void 0 ? _a : projectRoot;
58
68
  const ins = toArray(opts.in);
59
69
  const excludes = toArray(opts.exclude);
60
70
  const projectPrefix = projectRoot.endsWith("/")
61
71
  ? projectRoot
62
72
  : `${projectRoot}/`;
63
- const inPrefixesAll = ins.map((v) => joinSubpath(projectRoot, v));
64
- const excludePrefixes = excludes.map((v) => joinSubpath(projectRoot, v));
73
+ const inPrefixesAll = ins.map((v) => joinSubpath(projectRoot, base, v));
74
+ const excludePrefixes = excludes.map((v) => joinSubpath(projectRoot, base, v));
65
75
  // Collapse a single --in into pathPrefix to keep WHERE clauses simple.
66
76
  if (inPrefixesAll.length === 1) {
67
77
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.2",
3
+ "version": "0.26.4",
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.26.2",
3
+ "version": "0.26.4",
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",