gitnexus 1.6.6-rc.33 → 1.6.6-rc.35

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.
@@ -73,6 +73,12 @@ interface RepoHandle {
73
73
  * and cannot be changed mid-process.
74
74
  */
75
75
  export declare function resolveWorktreeCwd(repoPath: string, launchCwd: string): string;
76
+ /**
77
+ * Length of the base64url path hash appended to a colliding repo id.
78
+ * Exported so tests can pin the suffix shape without re-deriving the
79
+ * literal; see `repoId()` and the hashed-id resolution tier (#1658).
80
+ */
81
+ export declare const REPO_ID_HASH_LENGTH = 6;
76
82
  export declare class LocalBackend {
77
83
  private repos;
78
84
  private contextCache;
@@ -128,8 +134,20 @@ export declare class LocalBackend {
128
134
  resolveRepo(repoParam?: string): Promise<RepoHandle>;
129
135
  /**
130
136
  * Try to resolve a repo from the in-memory cache. Returns null on miss.
137
+ * Throws {@link RegistryAmbiguousTargetError} when `repoParam` matches
138
+ * multiple handles by name and cwd cannot disambiguate (#1658).
131
139
  */
132
140
  private resolveRepoFromCache;
141
+ /**
142
+ * Prefer the indexed repo whose path matches the git root of process.cwd().
143
+ *
144
+ * In MCP stdio server mode, `process.cwd()` is the server's launch directory,
145
+ * not the agent client's cwd. If the server was started from an unrelated
146
+ * directory, `getGitRoot` returns null and duplicate-name resolution throws
147
+ * {@link RegistryAmbiguousTargetError} — callers should pass an absolute path.
148
+ */
149
+ private pickRepoHandleForCwd;
150
+ private handleToRegistryEntry;
133
151
  private ensureInitialized;
134
152
  /**
135
153
  * Get context for a specific repo (or the single repo if only one).
@@ -16,7 +16,7 @@ import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../../core/lbug/l
16
16
  // import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
17
17
  import { parseDiffHunks, getCanonicalRepoRoot, getGitRoot, } from '../../storage/git.js';
18
18
  import { realpathSync } from 'fs';
19
- import { listRegisteredRepos, cleanupOldKuzuFiles, } from '../../storage/repo-manager.js';
19
+ import { listRegisteredRepos, cleanupOldKuzuFiles, canonicalizePath, RegistryAmbiguousTargetError, } from '../../storage/repo-manager.js';
20
20
  import { GroupService } from '../../core/group/service.js';
21
21
  import { resolveAtGroupMemberRepoPath } from '../../core/group/resolve-at-member.js';
22
22
  import { collectBestChunks } from '../../core/embeddings/types.js';
@@ -233,6 +233,12 @@ export function resolveWorktreeCwd(repoPath, launchCwd) {
233
233
  }
234
234
  return repoPath;
235
235
  }
236
+ /**
237
+ * Length of the base64url path hash appended to a colliding repo id.
238
+ * Exported so tests can pin the suffix shape without re-deriving the
239
+ * literal; see `repoId()` and the hashed-id resolution tier (#1658).
240
+ */
241
+ export const REPO_ID_HASH_LENGTH = 6;
236
242
  export class LocalBackend {
237
243
  repos = new Map();
238
244
  contextCache = new Map();
@@ -345,7 +351,13 @@ export class LocalBackend {
345
351
  for (const [id, handle] of this.repos) {
346
352
  if (id === base && handle.repoPath !== path.resolve(repoPath)) {
347
353
  // Collision — use path hash
348
- const hash = Buffer.from(repoPath).toString('base64url').slice(0, 6);
354
+ // Lowercase the hash so it survives the `paramLower` lookup in
355
+ // resolveRepoFromCache — base64url retains mixed case, but the id
356
+ // tier compares against `repoParam.toLowerCase()` (#1658 follow-up).
357
+ const hash = Buffer.from(repoPath)
358
+ .toString('base64url')
359
+ .slice(0, REPO_ID_HASH_LENGTH)
360
+ .toLowerCase();
349
361
  return `${base}-${hash}`;
350
362
  }
351
363
  }
@@ -362,7 +374,20 @@ export class LocalBackend {
362
374
  * while the MCP server was running.
363
375
  */
364
376
  async resolveRepo(repoParam) {
365
- const result = this.resolveRepoFromCache(repoParam);
377
+ let refreshedAfterAmbiguity = false;
378
+ let result;
379
+ try {
380
+ result = this.resolveRepoFromCache(repoParam);
381
+ }
382
+ catch (err) {
383
+ if (!(err instanceof RegistryAmbiguousTargetError))
384
+ throw err;
385
+ // Stale in-memory duplicate siblings can linger after unregister; refresh
386
+ // once before re-throwing so a resolved registry can disambiguate (#1658).
387
+ await this.refreshRepos();
388
+ refreshedAfterAmbiguity = true;
389
+ result = this.resolveRepoFromCache(repoParam);
390
+ }
366
391
  if (result) {
367
392
  // Issue: silent graph drift across sibling clones.
368
393
  // If the caller's cwd lives in a *different* on-disk clone of
@@ -375,8 +400,10 @@ export class LocalBackend {
375
400
  });
376
401
  return result;
377
402
  }
378
- // Miss — refresh registry and try once more
379
- await this.refreshRepos();
403
+ // Miss — refresh registry and try once more (skip if already refreshed above)
404
+ if (!refreshedAfterAmbiguity) {
405
+ await this.refreshRepos();
406
+ }
380
407
  const retried = this.resolveRepoFromCache(repoParam);
381
408
  if (retried) {
382
409
  this.maybeWarnSiblingDrift(retried).catch(() => { });
@@ -403,31 +430,57 @@ export class LocalBackend {
403
430
  }
404
431
  /**
405
432
  * Try to resolve a repo from the in-memory cache. Returns null on miss.
433
+ * Throws {@link RegistryAmbiguousTargetError} when `repoParam` matches
434
+ * multiple handles by name and cwd cannot disambiguate (#1658).
406
435
  */
407
436
  resolveRepoFromCache(repoParam) {
408
437
  if (this.repos.size === 0)
409
438
  return null;
410
439
  if (repoParam) {
411
440
  const paramLower = repoParam.toLowerCase();
412
- // Match by id
441
+ const looksLikePath = path.isAbsolute(repoParam) || repoParam.includes(path.sep) || repoParam.includes('/');
442
+ const resolvePathMatch = () => {
443
+ const canonicalTarget = canonicalizePath(repoParam);
444
+ return [...this.repos.values()].find((handle) => {
445
+ const stored = canonicalizePath(handle.repoPath);
446
+ return process.platform === 'win32'
447
+ ? stored.toLowerCase() === canonicalTarget.toLowerCase()
448
+ : stored === canonicalTarget;
449
+ });
450
+ };
451
+ // Path-like params first (absolute or contains separators) — aligns with
452
+ // resolveRegistryEntry (#829). Bare aliases such as ".tmp-repro-mini" must
453
+ // not be resolved via path.resolve(cwd) before duplicate-name handling.
454
+ if (looksLikePath) {
455
+ const pathMatch = resolvePathMatch();
456
+ if (pathMatch)
457
+ return pathMatch;
458
+ }
459
+ // Exact name before id — the first duplicate sibling keeps id === name
460
+ // (e.g. id "shared"), so a name lookup must not be captured by the id tier.
461
+ const nameMatches = [...this.repos.values()].filter((handle) => handle.name.toLowerCase() === paramLower);
462
+ if (nameMatches.length === 1)
463
+ return nameMatches[0];
464
+ if (nameMatches.length > 1) {
465
+ const cwdPick = this.pickRepoHandleForCwd(nameMatches);
466
+ if (cwdPick)
467
+ return cwdPick;
468
+ throw new RegistryAmbiguousTargetError(repoParam, nameMatches.map((h) => this.handleToRegistryEntry(h)));
469
+ }
470
+ // Stable hashed id (e.g. "shared-abc123") from repoId() collision suffix
413
471
  if (this.repos.has(paramLower))
414
472
  return this.repos.get(paramLower);
415
- // Match by name (case-insensitive)
416
- for (const handle of this.repos.values()) {
417
- if (handle.name.toLowerCase() === paramLower)
418
- return handle;
419
- }
420
- // Match by path (substring)
421
- const resolved = path.resolve(repoParam);
422
- for (const handle of this.repos.values()) {
423
- if (handle.repoPath === resolved)
424
- return handle;
425
- }
426
- // Match by partial name
427
- for (const handle of this.repos.values()) {
428
- if (handle.name.toLowerCase().includes(paramLower))
429
- return handle;
430
- }
473
+ // Bare name resolved as a cwd-relative path (e.g. "myrepo" against process.cwd()),
474
+ // after name/id tiers. Path-like strings with separators were handled at the top.
475
+ if (!looksLikePath) {
476
+ const pathMatch = resolvePathMatch();
477
+ if (pathMatch)
478
+ return pathMatch;
479
+ }
480
+ // Partial name only when unambiguous
481
+ const partialMatches = [...this.repos.values()].filter((handle) => handle.name.toLowerCase().includes(paramLower));
482
+ if (partialMatches.length === 1)
483
+ return partialMatches[0];
431
484
  return null;
432
485
  }
433
486
  if (this.repos.size === 1) {
@@ -435,6 +488,38 @@ export class LocalBackend {
435
488
  }
436
489
  return null; // Multiple repos, no param — ambiguous
437
490
  }
491
+ /**
492
+ * Prefer the indexed repo whose path matches the git root of process.cwd().
493
+ *
494
+ * In MCP stdio server mode, `process.cwd()` is the server's launch directory,
495
+ * not the agent client's cwd. If the server was started from an unrelated
496
+ * directory, `getGitRoot` returns null and duplicate-name resolution throws
497
+ * {@link RegistryAmbiguousTargetError} — callers should pass an absolute path.
498
+ */
499
+ pickRepoHandleForCwd(candidates) {
500
+ const cwdRoot = getGitRoot(process.cwd());
501
+ if (!cwdRoot)
502
+ return null;
503
+ const canonicalCwd = canonicalizePath(cwdRoot);
504
+ const cwdMatches = candidates.filter((handle) => {
505
+ const stored = canonicalizePath(handle.repoPath);
506
+ return process.platform === 'win32'
507
+ ? stored.toLowerCase() === canonicalCwd.toLowerCase()
508
+ : stored === canonicalCwd;
509
+ });
510
+ return cwdMatches.length === 1 ? cwdMatches[0] : null;
511
+ }
512
+ handleToRegistryEntry(handle) {
513
+ return {
514
+ name: handle.name,
515
+ path: handle.repoPath,
516
+ storagePath: handle.storagePath,
517
+ indexedAt: handle.indexedAt,
518
+ lastCommit: handle.lastCommit,
519
+ stats: handle.stats,
520
+ remoteUrl: handle.remoteUrl,
521
+ };
522
+ }
438
523
  // ─── Lazy LadybugDB Init ────────────────────────────────────────────
439
524
  async ensureInitialized(repoId) {
440
525
  // If a reinit is already in progress for this repo, wait for it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.33",
3
+ "version": "1.6.6-rc.35",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",