gitnexus 1.6.6-rc.32 → 1.6.6-rc.34

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.
@@ -5,7 +5,12 @@ import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-s
5
5
  * - FastAPI `@app.get("/path")` provider decorators
6
6
  * - `requests.get/post/...("url")` consumer calls
7
7
  * - Generic `requests.request("METHOD", "url")` consumer calls
8
- * - `httpx.AsyncClient` instances calling `.get/.post/...("url")`
8
+ * - `httpx.AsyncClient` instances calling `.get/.post/...("url")`, including
9
+ * aliased imports such as `import httpx as hx`,
10
+ * `from httpx import AsyncClient`, and
11
+ * `from httpx import AsyncClient as HttpxAsyncClient`.
12
+ * Locally rebound names (e.g. `AsyncClient = mock_factory()` inside a
13
+ * function) are excluded to avoid false-positive consumer contracts.
9
14
  */
10
15
  const FASTAPI_VERBS = {
11
16
  get: 'GET',
@@ -67,12 +72,48 @@ const REQUESTS_GENERIC_PATTERNS = compilePatterns({
67
72
  ],
68
73
  });
69
74
  // ─── Consumer: httpx.AsyncClient assignments ────────────────────────
70
- // NOTE: This targeted detector only tracks explicit `httpx.AsyncClient(...)`
71
- // construction. Direct imports (`from httpx import AsyncClient`) and module
72
- // aliases (`import httpx as hx`) and annotated assignments (`client: httpx.AsyncClient = ...`)
73
- // are intentionally left for a follow-up. Module-scope clients are only matched
75
+ // Module-scope clients are only matched
74
76
  // at module scope; calls inside functions require a function/class-local tracked
75
77
  // client to avoid false positives from same-name local variables.
78
+ const HTTPX_MODULE_IMPORT_PATTERNS = compilePatterns({
79
+ name: 'python-httpx-module-imports',
80
+ language: Python,
81
+ patterns: [
82
+ {
83
+ meta: {},
84
+ query: `
85
+ (import_statement
86
+ name: (aliased_import
87
+ name: (dotted_name (identifier) @module)
88
+ alias: (identifier) @alias))
89
+ `,
90
+ },
91
+ ],
92
+ });
93
+ const HTTPX_ASYNC_CLIENT_IMPORT_PATTERNS = compilePatterns({
94
+ name: 'python-httpx-async-client-imports',
95
+ language: Python,
96
+ patterns: [
97
+ {
98
+ meta: {},
99
+ query: `
100
+ (import_from_statement
101
+ module_name: (dotted_name (identifier) @module)
102
+ name: (dotted_name (identifier) @client_class))
103
+ `,
104
+ },
105
+ {
106
+ meta: {},
107
+ query: `
108
+ (import_from_statement
109
+ module_name: (dotted_name (identifier) @module)
110
+ name: (aliased_import
111
+ name: (dotted_name (identifier) @client_class)
112
+ alias: (identifier) @alias))
113
+ `,
114
+ },
115
+ ],
116
+ });
76
117
  const HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS = compilePatterns({
77
118
  name: 'python-httpx-async-client-assign',
78
119
  language: Python,
@@ -84,8 +125,23 @@ const HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS = compilePatterns({
84
125
  left: (_) @client
85
126
  right: (call
86
127
  function: (attribute
87
- object: (identifier) @module (#eq? @module "httpx")
88
- attribute: (identifier) @client_class (#eq? @client_class "AsyncClient"))))
128
+ object: (identifier) @module
129
+ attribute: (identifier) @client_class)))
130
+ `,
131
+ },
132
+ ],
133
+ });
134
+ const HTTPX_ASYNC_CLIENT_DIRECT_ASSIGN_PATTERNS = compilePatterns({
135
+ name: 'python-httpx-async-client-direct-assign',
136
+ language: Python,
137
+ patterns: [
138
+ {
139
+ meta: {},
140
+ query: `
141
+ (assignment
142
+ left: (_) @client
143
+ right: (call
144
+ function: (identifier) @client_class))
89
145
  `,
90
146
  },
91
147
  ],
@@ -101,8 +157,23 @@ const HTTPX_ASYNC_CLIENT_WITH_ALIAS_PATTERNS = compilePatterns({
101
157
  (as_pattern
102
158
  (call
103
159
  function: (attribute
104
- object: (identifier) @module (#eq? @module "httpx")
105
- attribute: (identifier) @client_class (#eq? @client_class "AsyncClient")))
160
+ object: (identifier) @module
161
+ attribute: (identifier) @client_class))
162
+ (as_pattern_target (identifier) @client))
163
+ `,
164
+ },
165
+ ],
166
+ });
167
+ const HTTPX_ASYNC_CLIENT_DIRECT_WITH_ALIAS_PATTERNS = compilePatterns({
168
+ name: 'python-httpx-async-client-direct-with-alias',
169
+ language: Python,
170
+ patterns: [
171
+ {
172
+ meta: {},
173
+ query: `
174
+ (as_pattern
175
+ (call
176
+ function: (identifier) @client_class)
106
177
  (as_pattern_target (identifier) @client))
107
178
  `,
108
179
  },
@@ -131,14 +202,120 @@ function trackedClientScopeKey(clientNode) {
131
202
  return getScopeKey(clientNode.parent, clientNode.text.includes('.'));
132
203
  }
133
204
  function callScopeKeys(clientNode) {
134
- const keys = new Set();
135
- const preferClass = clientNode.text.includes('.');
136
- const nearestScope = getScopeKey(clientNode.parent, preferClass);
137
- keys.add(nearestScope);
138
- return [...keys];
205
+ return [getScopeKey(clientNode.parent, clientNode.text.includes('.'))];
206
+ }
207
+ // Returns the scope key that a rebind of an imported alias would shadow under
208
+ // Python LEGB rules, or `null` when the rebind does not shadow anything that
209
+ // could produce a false-positive consumer detection.
210
+ // - Rebind inside a function/method → that function's scope.
211
+ // - Rebind at module top level → 'module' (shadows the whole file).
212
+ // - Rebind in a class body without an enclosing function → null. Python
213
+ // class attributes do not shadow bare-name lookups inside methods (methods
214
+ // see the module binding, not the class attribute), so we must not poison
215
+ // them.
216
+ function shadowScopeKey(node) {
217
+ let current = node;
218
+ let passedThroughClass = false;
219
+ while (current) {
220
+ if (current.type === 'function_definition') {
221
+ // Reuse getScopeKey's key format so the two helpers cannot drift apart.
222
+ return getScopeKey(current);
223
+ }
224
+ if (current.type === 'class_definition') {
225
+ passedThroughClass = true;
226
+ }
227
+ current = current.parent;
228
+ }
229
+ return passedThroughClass ? null : 'module';
230
+ }
231
+ function collectHttpxImportAliases(tree) {
232
+ const moduleAliases = new Set(['httpx']);
233
+ const asyncClientAliases = new Set();
234
+ // The @module capture is a single identifier inside a `dotted_name`, so for
235
+ // `import package.httpx as hx` the pattern would match the inner `httpx`
236
+ // segment. Check the full `dotted_name` text via `parent` to anchor the match.
237
+ for (const match of runCompiledPatterns(HTTPX_MODULE_IMPORT_PATTERNS, tree)) {
238
+ const moduleNode = match.captures.module;
239
+ const aliasNode = match.captures.alias;
240
+ if (moduleNode?.parent?.text === 'httpx' && aliasNode)
241
+ moduleAliases.add(aliasNode.text);
242
+ }
243
+ for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_IMPORT_PATTERNS, tree)) {
244
+ const moduleNode = match.captures.module;
245
+ const classNode = match.captures.client_class;
246
+ if (moduleNode?.parent?.text !== 'httpx' || classNode?.text !== 'AsyncClient')
247
+ continue;
248
+ asyncClientAliases.add(match.captures.alias?.text ?? classNode.text);
249
+ }
250
+ return { moduleAliases, asyncClientAliases };
251
+ }
252
+ // Tracks local rebindings (`AsyncClient = ...`, `hx = ...`) that shadow an
253
+ // imported alias. We treat the whole enclosing scope (module, class, or
254
+ // function) as shadowed for that alias name, so subsequent constructions in
255
+ // that scope are not falsely detected as httpx consumers. Covers bare-identifier
256
+ // targets and the common tuple / list destructuring shapes.
257
+ const ALIAS_SHADOW_PATTERNS = compilePatterns({
258
+ name: 'python-httpx-alias-shadow',
259
+ language: Python,
260
+ patterns: [
261
+ {
262
+ meta: {},
263
+ query: `(assignment left: (identifier) @name)`,
264
+ },
265
+ {
266
+ meta: {},
267
+ query: `(assignment left: (pattern_list (identifier) @name))`,
268
+ },
269
+ {
270
+ meta: {},
271
+ query: `(assignment left: (tuple_pattern (identifier) @name))`,
272
+ },
273
+ {
274
+ meta: {},
275
+ query: `(assignment left: (list_pattern (identifier) @name))`,
276
+ },
277
+ ],
278
+ });
279
+ function collectAliasShadowScopes(tree, aliases) {
280
+ const shadowed = new Map();
281
+ if (aliases.size === 0)
282
+ return shadowed;
283
+ for (const match of runCompiledPatterns(ALIAS_SHADOW_PATTERNS, tree)) {
284
+ const nameNode = match.captures.name;
285
+ if (!nameNode || !aliases.has(nameNode.text))
286
+ continue;
287
+ const scopeKey = shadowScopeKey(nameNode.parent);
288
+ if (scopeKey === null)
289
+ continue;
290
+ const set = shadowed.get(nameNode.text) ?? new Set();
291
+ set.add(scopeKey);
292
+ shadowed.set(nameNode.text, set);
293
+ }
294
+ return shadowed;
295
+ }
296
+ function isAliasShadowed(shadowed, aliasName, node) {
297
+ const scopes = shadowed.get(aliasName);
298
+ if (!scopes || scopes.size === 0)
299
+ return false;
300
+ let current = node.parent;
301
+ while (current) {
302
+ if (current.type === 'function_definition') {
303
+ // Reuse getScopeKey's key format so the two helpers cannot drift apart.
304
+ if (scopes.has(getScopeKey(current)))
305
+ return true;
306
+ }
307
+ current = current.parent;
308
+ }
309
+ // A module-level rebind shadows the alias for the entire file.
310
+ return scopes.has('module');
139
311
  }
140
312
  function collectHttpxAsyncClients(tree) {
141
313
  const clients = new Map();
314
+ const { moduleAliases, asyncClientAliases } = collectHttpxImportAliases(tree);
315
+ // Module aliases (`hx`) and AsyncClient aliases (`AsyncClient`,
316
+ // `HttpxAsyncClient`) share disjoint name spaces, so one shadow map keyed by
317
+ // alias name serves both lookups and we only walk the tree for rebinds once.
318
+ const shadowed = collectAliasShadowScopes(tree, new Set([...moduleAliases, ...asyncClientAliases]));
142
319
  const addClient = (clientNode) => {
143
320
  if (!clientNode)
144
321
  return;
@@ -149,9 +326,41 @@ function collectHttpxAsyncClients(tree) {
149
326
  clients.set(clientText, scopes);
150
327
  };
151
328
  for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_ASSIGN_PATTERNS, tree)) {
329
+ const moduleNode = match.captures.module;
330
+ const classNode = match.captures.client_class;
331
+ if (!moduleNode || !classNode)
332
+ continue;
333
+ if (!moduleAliases.has(moduleNode.text) || classNode.text !== 'AsyncClient')
334
+ continue;
335
+ if (isAliasShadowed(shadowed, moduleNode.text, moduleNode))
336
+ continue;
337
+ addClient(match.captures.client);
338
+ }
339
+ for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_DIRECT_ASSIGN_PATTERNS, tree)) {
340
+ const classNode = match.captures.client_class;
341
+ if (!classNode || !asyncClientAliases.has(classNode.text))
342
+ continue;
343
+ if (isAliasShadowed(shadowed, classNode.text, classNode))
344
+ continue;
152
345
  addClient(match.captures.client);
153
346
  }
154
347
  for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_WITH_ALIAS_PATTERNS, tree)) {
348
+ const moduleNode = match.captures.module;
349
+ const classNode = match.captures.client_class;
350
+ if (!moduleNode || !classNode)
351
+ continue;
352
+ if (!moduleAliases.has(moduleNode.text) || classNode.text !== 'AsyncClient')
353
+ continue;
354
+ if (isAliasShadowed(shadowed, moduleNode.text, moduleNode))
355
+ continue;
356
+ addClient(match.captures.client);
357
+ }
358
+ for (const match of runCompiledPatterns(HTTPX_ASYNC_CLIENT_DIRECT_WITH_ALIAS_PATTERNS, tree)) {
359
+ const classNode = match.captures.client_class;
360
+ if (!classNode || !asyncClientAliases.has(classNode.text))
361
+ continue;
362
+ if (isAliasShadowed(shadowed, classNode.text, classNode))
363
+ continue;
155
364
  addClient(match.captures.client);
156
365
  }
157
366
  return clients;
@@ -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.32",
3
+ "version": "1.6.6-rc.34",
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",