gitnexus 1.6.4 → 1.6.5-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 (72) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/ai-context.d.ts +1 -0
  3. package/dist/cli/ai-context.js +30 -12
  4. package/dist/cli/analyze.d.ts +18 -0
  5. package/dist/cli/analyze.js +33 -4
  6. package/dist/cli/augment.d.ts +1 -1
  7. package/dist/cli/augment.js +1 -1
  8. package/dist/cli/index.js +6 -1
  9. package/dist/core/augmentation/engine.d.ts +2 -2
  10. package/dist/core/augmentation/engine.js +2 -2
  11. package/dist/core/embeddings/ast-utils.js +2 -1
  12. package/dist/core/embeddings/http-client.js +22 -6
  13. package/dist/core/group/extractors/grpc-extractor.js +2 -1
  14. package/dist/core/group/extractors/http-patterns/python.js +175 -0
  15. package/dist/core/group/extractors/http-route-extractor.js +2 -1
  16. package/dist/core/group/extractors/include-extractor.js +2 -1
  17. package/dist/core/group/extractors/thrift-extractor.js +2 -1
  18. package/dist/core/group/extractors/tree-sitter-scanner.js +2 -1
  19. package/dist/core/ingestion/call-processor.js +3 -2
  20. package/dist/core/ingestion/heritage-processor.js +3 -2
  21. package/dist/core/ingestion/import-processor.js +2 -1
  22. package/dist/core/ingestion/languages/c/arity-metadata.d.ts +14 -0
  23. package/dist/core/ingestion/languages/c/arity-metadata.js +94 -0
  24. package/dist/core/ingestion/languages/c/arity.d.ts +6 -0
  25. package/dist/core/ingestion/languages/c/arity.js +18 -0
  26. package/dist/core/ingestion/languages/c/captures.d.ts +2 -0
  27. package/dist/core/ingestion/languages/c/captures.js +105 -0
  28. package/dist/core/ingestion/languages/c/header-scan.d.ts +7 -0
  29. package/dist/core/ingestion/languages/c/header-scan.js +55 -0
  30. package/dist/core/ingestion/languages/c/import-decomposer.d.ts +8 -0
  31. package/dist/core/ingestion/languages/c/import-decomposer.js +50 -0
  32. package/dist/core/ingestion/languages/c/import-target.d.ts +14 -0
  33. package/dist/core/ingestion/languages/c/import-target.js +57 -0
  34. package/dist/core/ingestion/languages/c/index.d.ts +11 -0
  35. package/dist/core/ingestion/languages/c/index.js +11 -0
  36. package/dist/core/ingestion/languages/c/interpret.d.ts +14 -0
  37. package/dist/core/ingestion/languages/c/interpret.js +48 -0
  38. package/dist/core/ingestion/languages/c/merge-bindings.d.ts +7 -0
  39. package/dist/core/ingestion/languages/c/merge-bindings.js +23 -0
  40. package/dist/core/ingestion/languages/c/query.d.ts +3 -0
  41. package/dist/core/ingestion/languages/c/query.js +161 -0
  42. package/dist/core/ingestion/languages/c/scope-resolver.d.ts +13 -0
  43. package/dist/core/ingestion/languages/c/scope-resolver.js +60 -0
  44. package/dist/core/ingestion/languages/c/simple-hooks.d.ts +14 -0
  45. package/dist/core/ingestion/languages/c/simple-hooks.js +19 -0
  46. package/dist/core/ingestion/languages/c/static-linkage.d.ts +13 -0
  47. package/dist/core/ingestion/languages/c/static-linkage.js +57 -0
  48. package/dist/core/ingestion/languages/c-cpp.js +10 -0
  49. package/dist/core/ingestion/languages/csharp/captures.js +2 -1
  50. package/dist/core/ingestion/languages/csharp/namespace-siblings.js +3 -2
  51. package/dist/core/ingestion/languages/go/captures.js +2 -1
  52. package/dist/core/ingestion/languages/go/range-binding.js +2 -1
  53. package/dist/core/ingestion/languages/python/captures.js +2 -1
  54. package/dist/core/ingestion/languages/typescript/captures.js +2 -1
  55. package/dist/core/ingestion/parsing-processor.js +2 -1
  56. package/dist/core/ingestion/registry-primary-flag.js +1 -0
  57. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +11 -0
  58. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +2 -1
  59. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +13 -2
  60. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
  61. package/dist/core/ingestion/scope-resolution/pipeline/run.js +4 -1
  62. package/dist/core/ingestion/workers/parse-worker.js +2 -1
  63. package/dist/core/lbug/lbug-adapter.js +8 -0
  64. package/dist/core/lbug/pool-adapter.js +19 -2
  65. package/dist/core/run-analyze.d.ts +2 -0
  66. package/dist/core/run-analyze.js +5 -1
  67. package/dist/core/tree-sitter/safe-parse.d.ts +6 -0
  68. package/dist/core/tree-sitter/safe-parse.js +32 -0
  69. package/dist/server/git-clone.js +10 -13
  70. package/dist/storage/git.d.ts +17 -6
  71. package/dist/storage/git.js +46 -12
  72. package/package.json +1 -1
@@ -348,7 +348,18 @@ async function doInitLbug(repoId, dbPath) {
348
348
  // install; analyze owns extension installation. If LOAD fails, search
349
349
  // features degrade gracefully and the user-facing query path proceeds.
350
350
  if (!shared.ftsLoaded) {
351
- shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
351
+ // Windows guard: LOAD EXTENSION fts crashes with SIGSEGV on Windows when
352
+ // the FTS extension binary is not installed locally (@ladybugdb/core native
353
+ // bug — the extension loader hits an unhandled error path that signals SIGSEGV
354
+ // rather than throwing a JS exception, so try/catch cannot protect here).
355
+ // Skip the load on Windows; bm25-index.js catches the resulting Kuzu catalog
356
+ // errors and returns empty BM25 results gracefully. Graph queries are unaffected.
357
+ if (process.platform === 'win32') {
358
+ shared.ftsLoaded = true;
359
+ }
360
+ else {
361
+ shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
362
+ }
352
363
  }
353
364
  // Register pool entry only after all connections are pre-warmed and FTS is
354
365
  // loaded. Concurrent executeQuery calls see either "not initialized"
@@ -404,8 +415,14 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
404
415
  // Load FTS extension if not already loaded on this Database.
405
416
  // policy: 'load-only' — same contract as initLbug above; the read pool
406
417
  // must not block on a network install during query execution.
418
+ // Windows guard: same SIGSEGV risk as doInitLbug above — skip on Windows.
407
419
  if (!shared.ftsLoaded) {
408
- shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
420
+ if (process.platform === 'win32') {
421
+ shared.ftsLoaded = true;
422
+ }
423
+ else {
424
+ shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
425
+ }
409
426
  }
410
427
  pool.set(repoId, {
411
428
  db: existingDb,
@@ -42,6 +42,8 @@ export interface AnalyzeOptions {
42
42
  skipAgentsMd?: boolean;
43
43
  /** Omit volatile symbol/relationship counts from AGENTS.md and CLAUDE.md. */
44
44
  noStats?: boolean;
45
+ /** Skip installing standard GitNexus skill files to .claude/skills/gitnexus/. */
46
+ skipSkills?: boolean;
45
47
  /**
46
48
  * User-provided alias for the registry `name` (#829). When set,
47
49
  * forwarded to `registerRepo` so the indexed repo is stored under
@@ -350,7 +350,11 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
350
350
  communities: pipelineResult.communityResult?.stats.totalCommunities,
351
351
  clusters: aggregatedClusterCount,
352
352
  processes: pipelineResult.processResult?.stats.totalProcesses,
353
- }, undefined, { skipAgentsMd: options.skipAgentsMd, noStats: options.noStats });
353
+ }, undefined, {
354
+ skipAgentsMd: options.skipAgentsMd,
355
+ skipSkills: options.skipSkills,
356
+ noStats: options.noStats,
357
+ });
354
358
  }
355
359
  catch {
356
360
  // Best-effort — don't fail the entire analysis for context file issues
@@ -0,0 +1,6 @@
1
+ import type Parser from 'tree-sitter';
2
+ /**
3
+ * Parse `sourceText` safely on every platform. See {@link SAFE_PARSE_CHUNK_CHARS}
4
+ * for the underlying tree-sitter binding bug this works around.
5
+ */
6
+ export declare function parseSourceSafe(parser: Parser, sourceText: string, oldTree?: Parser.Tree, options?: Parser.Options): Parser.Tree;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * tree-sitter 0.21.x's Node native binding crashes (SIGSEGV) on Windows when
3
+ * `parser.parse(string, …)` is handed a JS string longer than 32 767 chars.
4
+ * The crash happens inside the binding's V8 string-to-buffer conversion and
5
+ * cannot be intercepted from JavaScript. The callback (`Parser.Input`) overload
6
+ * pulls source in fixed-size chunks via repeated callback invocations and
7
+ * bypasses that conversion path entirely.
8
+ *
9
+ * Chunk size is comfortably below the boundary; any value < 32 767 works.
10
+ */
11
+ const SAFE_PARSE_CHUNK_CHARS = 16 * 1024;
12
+ /**
13
+ * Files at or below this length skip the callback machinery and use the
14
+ * direct string overload — the bug only manifests above the int16 boundary,
15
+ * so small inputs save the cost of N callback invocations per parse.
16
+ */
17
+ const DIRECT_PARSE_LIMIT_CHARS = 16 * 1024;
18
+ /**
19
+ * Parse `sourceText` safely on every platform. See {@link SAFE_PARSE_CHUNK_CHARS}
20
+ * for the underlying tree-sitter binding bug this works around.
21
+ */
22
+ export function parseSourceSafe(parser, sourceText, oldTree, options) {
23
+ if (sourceText.length <= DIRECT_PARSE_LIMIT_CHARS) {
24
+ return parser.parse(sourceText, oldTree, options);
25
+ }
26
+ const input = (index) => {
27
+ if (index >= sourceText.length)
28
+ return null;
29
+ return sourceText.slice(index, index + SAFE_PARSE_CHUNK_CHARS);
30
+ };
31
+ return parser.parse(input, oldTree, options);
32
+ }
@@ -10,6 +10,7 @@ import os from 'os';
10
10
  import fs from 'fs/promises';
11
11
  import { isIP } from 'net';
12
12
  import { logger } from '../core/logger.js';
13
+ import { parseRepoNameFromUrl } from '../storage/git.js';
13
14
  /** Root directory for all cloned repositories. Targets must resolve inside this. */
14
15
  const CLONE_ROOT = path.resolve(path.join(os.homedir(), '.gitnexus', 'repos'));
15
16
  // A valid git repository name is filesystem-safe: alphanumerics plus `. _ -`.
@@ -25,19 +26,15 @@ const REPO_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
25
26
  * clone root via path traversal.
26
27
  */
27
28
  export function extractRepoName(url) {
28
- // Strip trailing slashes without a regex to avoid polynomial-ReDoS on
29
- // pathological inputs like `https://x.com/y` + '/'.repeat(1e6). CodeQL's
30
- // js/polynomial-redos flagged `/\/+$/` here.
31
- let end = url.length;
32
- while (end > 0 && url.charCodeAt(end - 1) === 47 /* '/' */)
33
- end--;
34
- const cleaned = url.slice(0, end);
35
- const lastSegment = cleaned.split(/[/:]/).pop() || '';
36
- const stripped = lastSegment.endsWith('.git') ? lastSegment.slice(0, -4) : lastSegment;
37
- if (!stripped || stripped === '.' || stripped === '..' || !REPO_NAME_PATTERN.test(stripped)) {
29
+ const name = parseRepoNameFromUrl(url);
30
+ if (!name ||
31
+ name === '.' ||
32
+ name === '..' ||
33
+ name === 'unknown' ||
34
+ !REPO_NAME_PATTERN.test(name)) {
38
35
  throw new Error('Could not extract a valid repository name from URL');
39
36
  }
40
- return stripped;
37
+ return name;
41
38
  }
42
39
  /** Get the clone target directory for a repo name. */
43
40
  export function getCloneDir(repoName) {
@@ -347,8 +344,8 @@ export async function cloneOrPull(url, targetDir, onProgress) {
347
344
  throw new Error(`Clone target must be a subdirectory of ${CLONE_ROOT}`);
348
345
  }
349
346
  // Always validate the requested URL — the prior shape only ran this in
350
- // the clone branch, leaving the pull branch as an SSRF / blocked-host
351
- // bypass when an existing clone shared the basename of an attacker URL.
347
+ // the code path where the repo was cloned. Now it runs unconditionally,
348
+ // preventing SSRF / blocked-host bypasses even when targetDir already exists.
352
349
  validateGitUrl(url);
353
350
  const exists = await fs.access(path.join(safeTarget, '.git')).then(() => true, () => false);
354
351
  if (exists) {
@@ -108,13 +108,24 @@ export declare const hasGitDir: (dirPath: string) => boolean;
108
108
  */
109
109
  export declare const getRemoteOriginUrl: (repoPath: string) => string | null;
110
110
  /**
111
- * Parse a repository name out of a git remote URL. Handles the common
112
- * SSH (`git@host:owner/repo.git`), HTTPS (`https://host/owner/repo.git`),
113
- * `git://`, `ssh://`, and `file://` shapes. Returns `null` for empty /
114
- * unparseable input.
111
+ * Sanitize a repository name to prevent argument injection and ensure
112
+ * cross-platform filesystem compatibility.
115
113
  *
116
- * The heuristic: strip a trailing `.git` and trailing slashes, then
117
- * take the segment after the last `/` or `:`.
114
+ * 1. Strips leading dashes to prevent git command-line argument injection
115
+ * (e.g., --upload-pack=evil).
116
+ * 2. Replaces characters that are unsafe for directory names across
117
+ * platforms (Windows/macOS/Linux) with underscores.
118
+ * 3. Blocks path traversal segments ("." and "..") and Windows reserved
119
+ * names (e.g., CON, NUL) to prevent directory escape.
120
+ */
121
+ export declare const sanitizeRepoName: (name: string) => string;
122
+ /**
123
+ * Parse a repository name out of a git remote URL. Handles common shapes
124
+ * including SSH (git@host:owner/repo.git) and HTTPS (https://host/owner/repo.git).
125
+ *
126
+ * Returns a sanitized, filesystem-safe name or null if no name could be inferred.
127
+ * Returning null (rather than 'unknown') allows callers to use ?? null-coalescing
128
+ * for fallbacks without risk of registry collisions on 'unknown'.
118
129
  */
119
130
  export declare const parseRepoNameFromUrl: (url: string | null | undefined) => string | null;
120
131
  /**
@@ -257,13 +257,36 @@ export const getRemoteOriginUrl = (repoPath) => {
257
257
  }
258
258
  };
259
259
  /**
260
- * Parse a repository name out of a git remote URL. Handles the common
261
- * SSH (`git@host:owner/repo.git`), HTTPS (`https://host/owner/repo.git`),
262
- * `git://`, `ssh://`, and `file://` shapes. Returns `null` for empty /
263
- * unparseable input.
260
+ * Sanitize a repository name to prevent argument injection and ensure
261
+ * cross-platform filesystem compatibility.
264
262
  *
265
- * The heuristic: strip a trailing `.git` and trailing slashes, then
266
- * take the segment after the last `/` or `:`.
263
+ * 1. Strips leading dashes to prevent git command-line argument injection
264
+ * (e.g., --upload-pack=evil).
265
+ * 2. Replaces characters that are unsafe for directory names across
266
+ * platforms (Windows/macOS/Linux) with underscores.
267
+ * 3. Blocks path traversal segments ("." and "..") and Windows reserved
268
+ * names (e.g., CON, NUL) to prevent directory escape.
269
+ */
270
+ export const sanitizeRepoName = (name) => {
271
+ // 1. Prevent argument injection by stripping leading dashes.
272
+ // 2. Remove characters that are not alphanumerics, dots, underscores, or dashes.
273
+ const sanitized = name.replace(/^-+/, '').replace(/[^a-zA-Z0-9._-]/g, '_');
274
+ // 3. Block path traversal segments and Windows reserved names.
275
+ // Windows reserved names like CON, PRN, AUX, NUL, COM1-9, LPT1-9 cannot
276
+ // be used as directory names on Windows even if they have an extension.
277
+ const reserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$/i;
278
+ if (!sanitized || sanitized === '.' || sanitized === '..' || reserved.test(sanitized)) {
279
+ return 'unknown';
280
+ }
281
+ return sanitized;
282
+ };
283
+ /**
284
+ * Parse a repository name out of a git remote URL. Handles common shapes
285
+ * including SSH (git@host:owner/repo.git) and HTTPS (https://host/owner/repo.git).
286
+ *
287
+ * Returns a sanitized, filesystem-safe name or null if no name could be inferred.
288
+ * Returning null (rather than 'unknown') allows callers to use ?? null-coalescing
289
+ * for fallbacks without risk of registry collisions on 'unknown'.
267
290
  */
268
291
  export const parseRepoNameFromUrl = (url) => {
269
292
  if (!url)
@@ -271,12 +294,23 @@ export const parseRepoNameFromUrl = (url) => {
271
294
  const trimmed = url.trim();
272
295
  if (!trimmed)
273
296
  return null;
274
- // Strip `.git` suffix (case-insensitive) and any trailing slashes.
275
- const withoutSuffix = trimmed.replace(/\.git\/*$/i, '').replace(/\/+$/, '');
276
- // Last path segment, splitting on either `/` or `:` (covers SSH form).
277
- const m = withoutSuffix.match(/[/:]([^/:]+)$/);
278
- const candidate = m ? m[1] : withoutSuffix;
279
- return candidate || null;
297
+ // Strip trailing slashes without a regex to avoid polynomial-ReDoS on
298
+ // pathological inputs like `https://x.com/y` + '/'.repeat(1e6).
299
+ let end = trimmed.length;
300
+ while (end > 0 && trimmed.charCodeAt(end - 1) === 47 /* '/' */)
301
+ end--;
302
+ let cleaned = trimmed.slice(0, end);
303
+ // Strip trailing .git (case-insensitive)
304
+ if (cleaned.toLowerCase().endsWith('.git')) {
305
+ cleaned = cleaned.slice(0, -4);
306
+ }
307
+ // Last path segment, handling colons for SSH URLs and path traversal.
308
+ // Split on both / and : to consistently extract the last part.
309
+ const candidate = cleaned.split(/[/:]/).pop() || '';
310
+ if (!candidate)
311
+ return null;
312
+ const safe = sanitizeRepoName(candidate);
313
+ return safe === 'unknown' ? null : safe;
280
314
  };
281
315
  /**
282
316
  * Convenience wrapper: derive a registry-friendly name from the repo's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.4",
3
+ "version": "1.6.5-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",