gitnexus 1.6.5 → 1.6.6-rc.10

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.
Files changed (81) hide show
  1. package/dist/_shared/index.d.ts +2 -2
  2. package/dist/_shared/index.d.ts.map +1 -1
  3. package/dist/_shared/index.js.map +1 -1
  4. package/dist/_shared/scope-resolution/registries/context.d.ts +37 -0
  5. package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -1
  6. package/dist/_shared/scope-resolution/registries/context.js.map +1 -1
  7. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +4 -2
  8. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -1
  9. package/dist/_shared/scope-resolution/registries/lookup-core.js +8 -23
  10. package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -1
  11. package/dist/_shared/scope-resolution/symbol-definition.d.ts +20 -0
  12. package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
  13. package/dist/cli/analyze.js +65 -3
  14. package/dist/cli/eval-server.d.ts +14 -3
  15. package/dist/cli/eval-server.js +79 -9
  16. package/dist/cli/index.js +3 -1
  17. package/dist/cli/serve.js +6 -1
  18. package/dist/cli/wiki.d.ts +1 -0
  19. package/dist/cli/wiki.js +32 -8
  20. package/dist/core/ingestion/language-provider.d.ts +29 -0
  21. package/dist/core/ingestion/languages/c-cpp.js +46 -0
  22. package/dist/core/ingestion/languages/cpp/arity-metadata.d.ts +17 -0
  23. package/dist/core/ingestion/languages/cpp/arity-metadata.js +51 -2
  24. package/dist/core/ingestion/languages/cpp/arity.d.ts +4 -1
  25. package/dist/core/ingestion/languages/cpp/arity.js +4 -1
  26. package/dist/core/ingestion/languages/cpp/captures.js +73 -0
  27. package/dist/core/ingestion/languages/cpp/constraint-extractor.d.ts +73 -0
  28. package/dist/core/ingestion/languages/cpp/constraint-extractor.js +308 -0
  29. package/dist/core/ingestion/languages/cpp/constraint-filter.d.ts +31 -0
  30. package/dist/core/ingestion/languages/cpp/constraint-filter.js +135 -0
  31. package/dist/core/ingestion/languages/cpp/scope-resolver.js +6 -0
  32. package/dist/core/ingestion/languages/cpp/type-classifier.d.ts +26 -0
  33. package/dist/core/ingestion/languages/cpp/type-classifier.js +49 -0
  34. package/dist/core/ingestion/model/field-registry.d.ts +17 -3
  35. package/dist/core/ingestion/model/field-registry.js +19 -4
  36. package/dist/core/ingestion/model/owned-members-lookup.d.ts +19 -0
  37. package/dist/core/ingestion/model/owned-members-lookup.js +43 -0
  38. package/dist/core/ingestion/model/symbol-table.d.ts +2 -1
  39. package/dist/core/ingestion/model/symbol-table.js +3 -0
  40. package/dist/core/ingestion/model/type-registry.d.ts +9 -0
  41. package/dist/core/ingestion/model/type-registry.js +18 -0
  42. package/dist/core/ingestion/parsing-processor.js +35 -2
  43. package/dist/core/ingestion/resolve-references.d.ts +3 -1
  44. package/dist/core/ingestion/resolve-references.js +5 -1
  45. package/dist/core/ingestion/scope-extractor.js +63 -0
  46. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +21 -1
  47. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +1 -0
  48. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +14 -0
  49. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +12 -0
  50. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +11 -1
  51. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +83 -28
  52. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +30 -7
  53. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +49 -18
  54. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +1 -1
  55. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +10 -4
  56. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +4 -2
  57. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +48 -9
  58. package/dist/core/ingestion/scope-resolution/pipeline/run.js +3 -0
  59. package/dist/core/ingestion/utils/template-arguments.d.ts +19 -0
  60. package/dist/core/ingestion/utils/template-arguments.js +30 -0
  61. package/dist/core/ingestion/workers/parse-worker.d.ts +2 -1
  62. package/dist/core/ingestion/workers/parse-worker.js +1 -0
  63. package/dist/core/lbug/lbug-adapter.d.ts +3 -1
  64. package/dist/core/lbug/lbug-adapter.js +33 -19
  65. package/dist/core/lbug/lbug-config.d.ts +1 -1
  66. package/dist/core/lbug/lbug-config.js +1 -1
  67. package/dist/core/lbug/pool-adapter.d.ts +0 -7
  68. package/dist/core/lbug/pool-adapter.js +10 -35
  69. package/dist/core/lbug/query-params.d.ts +1 -0
  70. package/dist/core/lbug/query-params.js +21 -0
  71. package/dist/core/search/bm25-index.js +4 -6
  72. package/dist/core/wiki/generator.d.ts +13 -0
  73. package/dist/core/wiki/generator.js +45 -4
  74. package/dist/core/wiki/llm-client.d.ts +1 -1
  75. package/dist/core/wiki/llm-client.js +23 -6
  76. package/dist/mcp/local/local-backend.d.ts +18 -3
  77. package/dist/mcp/local/local-backend.js +105 -11
  78. package/dist/mcp/tools.js +10 -0
  79. package/dist/server/api.d.ts +3 -0
  80. package/dist/server/api.js +36 -24
  81. package/package.json +1 -1
@@ -7,14 +7,15 @@
7
7
  */
8
8
  import fs from 'fs/promises';
9
9
  import path from 'path';
10
- import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady, isWriteQuery, } from '../../core/lbug/pool-adapter.js';
10
+ import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady, } from '../../core/lbug/pool-adapter.js';
11
+ import { isValidQueryParams } from '../../core/lbug/query-params.js';
11
12
  import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../../core/lbug/lbug-config.js';
12
- export { isWriteQuery };
13
13
  // Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
14
14
  // at MCP server startup — crashes on unsupported Node ABI versions (#89)
15
15
  // git utilities available if needed
16
16
  // import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
17
- import { parseDiffHunks } from '../../storage/git.js';
17
+ import { parseDiffHunks, getCanonicalRepoRoot, getGitRoot, } from '../../storage/git.js';
18
+ import { realpathSync } from 'fs';
18
19
  import { listRegisteredRepos, cleanupOldKuzuFiles, } from '../../storage/repo-manager.js';
19
20
  import { GroupService } from '../../core/group/service.js';
20
21
  import { resolveAtGroupMemberRepoPath } from '../../core/group/resolve-at-member.js';
@@ -141,6 +142,7 @@ function logQueryError(context, err) {
141
142
  const msg = err instanceof Error ? err.message : String(err);
142
143
  logger.error({ context, err: msg }, 'GitNexus query failed');
143
144
  }
145
+ const isReadOnlyDbError = (err) => /read-only database/i.test(err instanceof Error ? err.message : String(err));
144
146
  /**
145
147
  * Per-query latency telemetry for production aggregation (#553).
146
148
  *
@@ -160,6 +162,55 @@ function logQueryTiming(query, phases) {
160
162
  const truncated = query.length > 80 ? `${query.slice(0, 80)}…` : query;
161
163
  logger.debug({ query: truncated, totalMs, phases }, 'GitNexus query timing');
162
164
  }
165
+ /** Resolve symlinks for path comparison; falls back to path.resolve on error.
166
+ * Uses `realpathSync.native` (not the pure-JS `realpathSync`) so that Windows
167
+ * 8.3 short names (e.g. RUNNER~1 → runneradmin) are expanded to long form,
168
+ * matching the output of `git rev-parse --show-toplevel`. */
169
+ function tryRealpath(p) {
170
+ try {
171
+ return realpathSync.native(p);
172
+ }
173
+ catch {
174
+ return path.resolve(p);
175
+ }
176
+ }
177
+ /**
178
+ * Resolve the git diff cwd for detect_changes, auto-detecting linked worktrees.
179
+ *
180
+ * When `launchCwd` is a linked worktree of the same canonical repository as
181
+ * `repoPath` (i.e. `getGitRoot(launchCwd)` differs from `repoPath` but both
182
+ * share the same `getCanonicalRepoRoot`), returns the worktree's git root so
183
+ * that `git diff` sees the correct working directory and index.
184
+ *
185
+ * Returns `repoPath` unchanged in all other cases (non-worktree, git
186
+ * unavailable, unrelated repo).
187
+ *
188
+ * Extracted as a module-level export so tests can pass any `launchCwd` instead
189
+ * of relying on `process.cwd()`, which is fixed to the server launch directory
190
+ * and cannot be changed mid-process.
191
+ */
192
+ export function resolveWorktreeCwd(repoPath, launchCwd) {
193
+ try {
194
+ const launchGitRoot = getGitRoot(launchCwd);
195
+ if (launchGitRoot) {
196
+ // Normalise via realpathSync before comparing so macOS /var → /private/var
197
+ // symlinks (and Windows 8.3 short names) don't create false mismatches.
198
+ const realLaunch = tryRealpath(launchGitRoot);
199
+ const realRepo = tryRealpath(repoPath);
200
+ if (realLaunch !== realRepo) {
201
+ const launchCanonical = getCanonicalRepoRoot(launchCwd);
202
+ const repoCanonical = getCanonicalRepoRoot(repoPath);
203
+ if (launchCanonical && repoCanonical && launchCanonical === repoCanonical) {
204
+ return launchGitRoot;
205
+ }
206
+ }
207
+ }
208
+ }
209
+ catch {
210
+ // Best-effort; fall through to repoPath.
211
+ }
212
+ return repoPath;
213
+ }
163
214
  export class LocalBackend {
164
215
  repos = new Map();
165
216
  contextCache = new Map();
@@ -1017,27 +1068,31 @@ export class LocalBackend {
1017
1068
  return [];
1018
1069
  }
1019
1070
  }
1020
- async executeCypher(repoName, query) {
1071
+ async executeCypher(repoName, query, params = {}) {
1021
1072
  const repo = await this.resolveRepo(repoName);
1022
- return this.cypher(repo, { query });
1073
+ return this.cypher(repo, { query, params });
1023
1074
  }
1024
- async cypher(repo, params) {
1075
+ async cypher(repo, request) {
1025
1076
  await this.ensureInitialized(repo.id);
1026
1077
  if (!isLbugReady(repo.id)) {
1027
1078
  return { error: 'LadybugDB not ready. Index may be corrupted.' };
1028
1079
  }
1029
- // Block write operations (defense-in-depth — DB is already read-only)
1030
- if (isWriteQuery(params.query)) {
1080
+ if (request.params !== undefined && !isValidQueryParams(request.params)) {
1031
1081
  return {
1032
- error: 'Write operations (CREATE, DELETE, SET, MERGE, REMOVE, DROP, ALTER, COPY, DETACH) are not allowed. The knowledge graph is read-only.',
1082
+ error: '"params" must be a plain object with scalar values (string/number/boolean/null).',
1033
1083
  };
1034
1084
  }
1035
1085
  try {
1036
- const result = await executeQuery(repo.id, params.query);
1086
+ const result = await executeParameterized(repo.id, request.query, request.params ?? {});
1037
1087
  return result;
1038
1088
  }
1039
1089
  catch (err) {
1040
1090
  const msg = err.message || 'Query failed';
1091
+ if (isReadOnlyDbError(err)) {
1092
+ return {
1093
+ error: 'Write operations (CREATE, DELETE, SET, MERGE, REMOVE, DROP, ALTER, COPY, DETACH) are not allowed. The knowledge graph is read-only.',
1094
+ };
1095
+ }
1041
1096
  if (isWalCorruptionError(err)) {
1042
1097
  return {
1043
1098
  error: msg,
@@ -1770,11 +1825,50 @@ export class LocalBackend {
1770
1825
  }
1771
1826
  let diffOutput;
1772
1827
  try {
1828
+ // Resolve the cwd for git diff.
1829
+ //
1830
+ // In a linked worktree (e.g. /repo/wt-feature/), the user's staged and
1831
+ // unstaged changes live in that worktree's separate working directory and
1832
+ // index. Running `git diff` from the canonical repo root sees a different
1833
+ // working tree and returns empty output.
1834
+ //
1835
+ // Resolution order (see resolveWorktreeCwd for details):
1836
+ // 1. params.worktree — explicit override, validated against the
1837
+ // registered repo's canonical root.
1838
+ // 2. Auto-detect — if the server's launch cwd (process.cwd()) is a
1839
+ // linked worktree of the same canonical repo, use its git root.
1840
+ // 3. repo.repoPath — fallback (original behaviour, handled inside
1841
+ // resolveWorktreeCwd when no worktree is detected).
1842
+ //
1843
+ // Start with the auto-detected value; override with the validated
1844
+ // explicit param when provided. This avoids a dead initial assignment.
1845
+ let diffCwd = resolveWorktreeCwd(repo.repoPath, process.cwd());
1846
+ if (params.worktree) {
1847
+ if (!path.isAbsolute(params.worktree)) {
1848
+ return {
1849
+ error: `worktree must be an absolute path, got: "${params.worktree}"`,
1850
+ };
1851
+ }
1852
+ const providedResolved = path.resolve(params.worktree);
1853
+ const repoCanonical = getCanonicalRepoRoot(repo.repoPath);
1854
+ if (!repoCanonical) {
1855
+ return {
1856
+ error: `Could not determine canonical root for repo "${repo.repoPath}". Is git available?`,
1857
+ };
1858
+ }
1859
+ const worktreeCanonical = getCanonicalRepoRoot(providedResolved);
1860
+ if (!worktreeCanonical || tryRealpath(worktreeCanonical) !== tryRealpath(repoCanonical)) {
1861
+ return {
1862
+ error: `worktree "${params.worktree}" is not a worktree of repo "${repo.repoPath}". Ensure the path is inside the same git repository.`,
1863
+ };
1864
+ }
1865
+ diffCwd = providedResolved;
1866
+ }
1773
1867
  // maxBuffer raised from Node's 1MB default to 256MB to avoid ENOBUFS on
1774
1868
  // repos with large unstaged/untracked diffs (e.g. unignored build folders).
1775
1869
  // See issue: spawnSync git ENOBUFS in detect_changes(scope="unstaged").
1776
1870
  diffOutput = execFileSync('git', diffArgs, {
1777
- cwd: repo.repoPath,
1871
+ cwd: diffCwd,
1778
1872
  encoding: 'utf-8',
1779
1873
  maxBuffer: 256 * 1024 * 1024,
1780
1874
  });
package/dist/mcp/tools.js CHANGED
@@ -155,6 +155,10 @@ TIPS:
155
155
  type: 'object',
156
156
  properties: {
157
157
  query: { type: 'string', description: 'Cypher query to execute' },
158
+ params: {
159
+ type: 'object',
160
+ description: 'Optional query parameters for placeholders (e.g. $name) to execute via prepared statement binding.',
161
+ },
158
162
  repo: {
159
163
  type: 'string',
160
164
  description: 'Repository name or path. Omit if only one repo is indexed.',
@@ -218,6 +222,8 @@ Maps git diff hunks to indexed symbols, then traces which processes are impacted
218
222
  WHEN TO USE: Before committing — to understand what your changes affect. Pre-commit review, PR preparation.
219
223
  AFTER THIS: Review affected processes. Use context() on high-risk symbols. READ gitnexus://repo/{name}/process/{name} for full traces.
220
224
 
225
+ GIT WORKTREE SUPPORT: GitNexus automatically detects when the MCP server was launched from inside a linked git worktree and runs git diff against that worktree — no extra parameters needed in the common case. Pass "worktree" explicitly only when the server was started from a different directory than the worktree you are editing (e.g., the server runs from the canonical root but your changes are in a linked worktree at a different path).
226
+
221
227
  Returns: changed symbols, affected processes, and a risk summary.`,
222
228
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
223
229
  inputSchema: {
@@ -233,6 +239,10 @@ Returns: changed symbols, affected processes, and a risk summary.`,
233
239
  type: 'string',
234
240
  description: 'Branch/commit for "compare" scope (e.g., "main")',
235
241
  },
242
+ worktree: {
243
+ type: 'string',
244
+ description: 'Absolute path to a linked git worktree. Pass this when your changes are in a worktree (the .git entry at that path is a file, not a directory). GitNexus will run git diff from that worktree so staged/unstaged changes are correctly detected.',
245
+ },
236
246
  repo: {
237
247
  type: 'string',
238
248
  description: 'Repository name or path. Omit if only one repo is indexed.',
@@ -68,5 +68,8 @@ export declare const handleFileRequest: (req: {
68
68
  };
69
69
  json: (body: any) => void;
70
70
  }, repoPath: string) => Promise<void>;
71
+ export declare const handleQueryRequest: (req: express.Request, res: express.Response, resolveRepo: (repoName?: string) => Promise<{
72
+ storagePath: string;
73
+ } | undefined>) => Promise<void>;
71
74
  export declare const createServer: (port: number, host?: string) => Promise<void>;
72
75
  export {};
@@ -13,8 +13,8 @@ import path from 'path';
13
13
  import fs from 'fs/promises';
14
14
  import { createRequire } from 'node:module';
15
15
  import { loadMeta, listRegisteredRepos, getStoragePath } from '../storage/repo-manager.js';
16
- import { executeQuery, executePrepared, executeWithReusedStatement, streamQuery, flushWAL, closeLbug, withLbugDb, } from '../core/lbug/lbug-adapter.js';
17
- import { isWriteQuery } from '../core/lbug/pool-adapter.js';
16
+ import { executeQuery, executePrepared, executeWithReusedStatement, streamQuery, flushWAL, closeLbug, withLbugDb, isReadOnlyDbError, } from '../core/lbug/lbug-adapter.js';
17
+ import { isValidQueryParams } from '../core/lbug/query-params.js';
18
18
  import { NODE_TABLES } from '../_shared/index.js';
19
19
  import { searchFTSFromLbug } from '../core/search/bm25-index.js';
20
20
  import { hybridSearch } from '../core/search/hybrid-search.js';
@@ -533,6 +533,39 @@ export const handleFileRequest = async (req, res, repoPath) => {
533
533
  }
534
534
  }
535
535
  };
536
+ export const handleQueryRequest = async (req, res, resolveRepo) => {
537
+ try {
538
+ const cypher = req.body.cypher;
539
+ if (!cypher) {
540
+ res.status(400).json({ error: 'Missing "cypher" in request body' });
541
+ return;
542
+ }
543
+ const queryParams = req.body.params;
544
+ if (queryParams !== undefined && !isValidQueryParams(queryParams)) {
545
+ res.status(400).json({
546
+ error: '"params" must be a plain object with scalar values (string/number/boolean/null)',
547
+ });
548
+ return;
549
+ }
550
+ const entry = await resolveRepo(requestedRepo(req));
551
+ if (!entry) {
552
+ res.status(404).json({ error: 'Repository not found' });
553
+ return;
554
+ }
555
+ const lbugPath = path.join(entry.storagePath, 'lbug');
556
+ const result = await withLbugDb(lbugPath, () => executePrepared(cypher, queryParams ?? {}), {
557
+ readOnly: true,
558
+ });
559
+ res.json({ result });
560
+ }
561
+ catch (err) {
562
+ if (isReadOnlyDbError(err)) {
563
+ res.status(403).json({ error: 'Write queries are not allowed via the HTTP API' });
564
+ return;
565
+ }
566
+ res.status(500).json({ error: err.message || 'Query failed' });
567
+ }
568
+ };
536
569
  export const createServer = async (port, host = '127.0.0.1') => {
537
570
  const app = express();
538
571
  app.disable('x-powered-by');
@@ -892,28 +925,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
892
925
  });
893
926
  // Execute Cypher query
894
927
  app.post('/api/query', async (req, res) => {
895
- try {
896
- const cypher = req.body.cypher;
897
- if (!cypher) {
898
- res.status(400).json({ error: 'Missing "cypher" in request body' });
899
- return;
900
- }
901
- if (isWriteQuery(cypher)) {
902
- res.status(403).json({ error: 'Write queries are not allowed via the HTTP API' });
903
- return;
904
- }
905
- const entry = await resolveRepo(requestedRepo(req));
906
- if (!entry) {
907
- res.status(404).json({ error: 'Repository not found' });
908
- return;
909
- }
910
- const lbugPath = path.join(entry.storagePath, 'lbug');
911
- const result = await withLbugDb(lbugPath, () => executeQuery(cypher));
912
- res.json({ result });
913
- }
914
- catch (err) {
915
- res.status(500).json({ error: err.message || 'Query failed' });
916
- }
928
+ await handleQueryRequest(req, res, resolveRepo);
917
929
  });
918
930
  // Search (supports mode: 'hybrid' | 'semantic' | 'bm25', and optional enrichment)
919
931
  app.post('/api/search', async (req, res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.5",
3
+ "version": "1.6.6-rc.10",
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",