gitnexus 1.6.4-rc.80 → 1.6.4-rc.82

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 (99) hide show
  1. package/dist/cli/analyze.js +27 -0
  2. package/dist/cli/mcp.d.ts +21 -0
  3. package/dist/cli/mcp.js +42 -13
  4. package/dist/cli/optional-grammars.d.ts +47 -0
  5. package/dist/cli/optional-grammars.js +78 -0
  6. package/dist/cli/setup.js +21 -6
  7. package/dist/core/embeddings/embedder.js +8 -8
  8. package/dist/core/embeddings/embedding-pipeline.js +10 -10
  9. package/dist/core/lbug/extension-loader.js +1 -1
  10. package/dist/core/lbug/lbug-adapter.js +3 -3
  11. package/dist/core/lbug/pool-adapter.d.ts +1 -4
  12. package/dist/core/lbug/pool-adapter.js +19 -5
  13. package/dist/core/tree-sitter/parser-loader.js +4 -4
  14. package/dist/mcp/compatible-stdio-transport.js +7 -1
  15. package/dist/mcp/core/lbug-adapter.d.ts +7 -1
  16. package/dist/mcp/core/lbug-adapter.js +7 -1
  17. package/dist/mcp/server.js +18 -7
  18. package/dist/mcp/stdio-capture.d.ts +40 -0
  19. package/dist/mcp/stdio-capture.js +53 -0
  20. package/dist/mcp/stdio-context.d.ts +47 -0
  21. package/dist/mcp/stdio-context.js +145 -0
  22. package/package.json +2 -1
  23. package/scripts/build-tree-sitter-dart.cjs +11 -0
  24. package/scripts/build-tree-sitter-proto.cjs +11 -0
  25. package/web/assets/{agent-BR6JgzUS.js → agent-DGm5BiXg.js} +2 -2
  26. package/web/assets/architecture-YZFGNWBL-S5CXDPWN-CzpqonpT.js +1 -0
  27. package/web/assets/{architectureDiagram-EMZXCZ2Q-DdOrld62.js → architectureDiagram-EMZXCZ2Q-DLWvvvUB.js} +1 -1
  28. package/web/assets/{blockDiagram-IGV67L2C-DBbcDJE6.js → blockDiagram-IGV67L2C-B47EUMKY.js} +1 -1
  29. package/web/assets/{c4Diagram-DFAF54RM-BI-8yWq5.js → c4Diagram-DFAF54RM-DvvVQASF.js} +1 -1
  30. package/web/assets/{chunk-3GS5O3IE-DnMyU1lf.js → chunk-3GS5O3IE-0H2zS7RE.js} +1 -1
  31. package/web/assets/{chunk-3YCYZ6SJ-BZslrNTQ.js → chunk-3YCYZ6SJ-DokVoiwM.js} +1 -1
  32. package/web/assets/{chunk-6NTNNK5N-Y67ka-rU.js → chunk-6NTNNK5N-B9jhOxT0.js} +1 -1
  33. package/web/assets/{chunk-A34GCYZU-ClNpO7ap.js → chunk-A34GCYZU-DfTldqlz.js} +1 -1
  34. package/web/assets/{chunk-DJ7UZH7F-tYXU3fBz.js → chunk-DJ7UZH7F-npvsBmvn.js} +1 -1
  35. package/web/assets/{chunk-DKKBVRCY-DfGJMrSz.js → chunk-DKKBVRCY-DbFEYV2a.js} +2 -2
  36. package/web/assets/{chunk-DU5LTGQ6-qCXmLiFZ.js → chunk-DU5LTGQ6-B1Mj73sD.js} +1 -1
  37. package/web/assets/{chunk-FXACKDTF-D-WiQLyP.js → chunk-FXACKDTF-CNljDI1M.js} +1 -1
  38. package/web/assets/{chunk-H3VCZNTA-D3vbWWyQ.js → chunk-H3VCZNTA-j4ZGeRTZ.js} +1 -1
  39. package/web/assets/{chunk-HN6EAY2L-CQC4-xpr.js → chunk-HN6EAY2L-CEc2xC-J.js} +1 -1
  40. package/web/assets/{chunk-O5ABG6QK-CqQgDXv5.js → chunk-O5ABG6QK-B_00CmQB.js} +1 -1
  41. package/web/assets/{chunk-PK6DOVAG-BCVBr6ZD.js → chunk-PK6DOVAG-lOMJKFOu.js} +1 -1
  42. package/web/assets/{chunk-RNJOYNJ4-Bs1qBdCE.js → chunk-RNJOYNJ4-BKKlhT6X.js} +1 -1
  43. package/web/assets/{chunk-RWUO3TPN-eG5qgc5e.js → chunk-RWUO3TPN-BhUzGpFd.js} +1 -1
  44. package/web/assets/{chunk-TBF5ZNIQ-CzrYvTQC.js → chunk-TBF5ZNIQ-BAsGZFMI.js} +1 -1
  45. package/web/assets/{chunk-TYMNRAUI-BBuddluh.js → chunk-TYMNRAUI-B47lwFjp.js} +1 -1
  46. package/web/assets/{chunk-W7ZLLLMY-JfkpG9pC.js → chunk-W7ZLLLMY-DGb6u0pB.js} +1 -1
  47. package/web/assets/{chunk-WSB5WSVC-BSmELgSM.js → chunk-WSB5WSVC-BiaAMFmd.js} +1 -1
  48. package/web/assets/{chunk-XGPFEOL4-CmPwWVXe.js → chunk-XGPFEOL4-Dqk5iHgi.js} +1 -1
  49. package/web/assets/classDiagram-PPOCWD7C-W4Gq4oYa.js +1 -0
  50. package/web/assets/classDiagram-v2-23LJLIIU-CbyGxpD-.js +1 -0
  51. package/web/assets/{cose-bilkent-PNC4W37J-B2PBKVII.js → cose-bilkent-PNC4W37J-CnOlxR0_.js} +1 -1
  52. package/web/assets/{dagre-E77IOHMT-DOJQpMhU.js → dagre-E77IOHMT-Cy3z5r06.js} +1 -1
  53. package/web/assets/{diagram-H7BISOXX-BwuBSflT.js → diagram-H7BISOXX-ChLdgktB.js} +1 -1
  54. package/web/assets/{diagram-JC5VWROH-RUxO3qvC.js → diagram-JC5VWROH-DdzGpm08.js} +1 -1
  55. package/web/assets/{diagram-LXUTUG65-DL3slkcr.js → diagram-LXUTUG65-Bq_ye_lS.js} +1 -1
  56. package/web/assets/{diagram-WEHSV5V5-D-xIWqCV.js → diagram-WEHSV5V5-WrNO_VoZ.js} +1 -1
  57. package/web/assets/{erDiagram-GCSMX5X6-BsNUcWxA.js → erDiagram-GCSMX5X6-Cx3Aq4SN.js} +1 -1
  58. package/web/assets/{flowDiagram-OTCZ4VVT-Dez0Lj1Y.js → flowDiagram-OTCZ4VVT-DRe-9T0b.js} +1 -1
  59. package/web/assets/{ganttDiagram-MUNLMDZQ-ChSXA0kN.js → ganttDiagram-MUNLMDZQ-CvtFMhi8.js} +1 -1
  60. package/web/assets/gitGraph-7Q5UKJZL-54BCDZD5-DZfbulFG.js +1 -0
  61. package/web/assets/{gitGraphDiagram-3HKGZ4G3-8iHl9xKL.js → gitGraphDiagram-3HKGZ4G3-BxDd7tFR.js} +1 -1
  62. package/web/assets/{index-DnjscxVd.js → index-ChQJsgDb.js} +5 -5
  63. package/web/assets/info-OMHHGYJF-BF2H5H6G-Dehqc8IA.js +1 -0
  64. package/web/assets/infoDiagram-MN7RKWGX-B0B5J4EY.js +2 -0
  65. package/web/assets/{ishikawaDiagram-YMYX4NHK-BPFoHRQ-.js → ishikawaDiagram-YMYX4NHK-TUICrJKY.js} +1 -1
  66. package/web/assets/{journeyDiagram-SO5T7YLQ-DVRJpEUc.js → journeyDiagram-SO5T7YLQ-C4Y1ypn8.js} +1 -1
  67. package/web/assets/{kanban-definition-LJHFXRCJ-DOUCiWuN.js → kanban-definition-LJHFXRCJ--cIG9jHd.js} +1 -1
  68. package/web/assets/{mindmap-definition-2EUWGEK5-BNXeeUBG.js → mindmap-definition-2EUWGEK5-BK54zuWp.js} +1 -1
  69. package/web/assets/packet-4T2RLAQJ-EV4IVRXR-BjetiCTk.js +1 -0
  70. package/web/assets/pie-ZZUOXDRM-N23DN5KN-zkxhGq7v.js +1 -0
  71. package/web/assets/{pieDiagram-3IATQBI2-6ktwo7Jm.js → pieDiagram-3IATQBI2-CMgPhL6Y.js} +1 -1
  72. package/web/assets/{quadrantDiagram-E256RVCF-Ckf5RsLj.js → quadrantDiagram-E256RVCF-CNmFSP0m.js} +1 -1
  73. package/web/assets/radar-PYXPWWZC-P6TP7ZYP-CHxbGrvk.js +1 -0
  74. package/web/assets/{requirementDiagram-M5DCFWZL-OMiwzJCW.js → requirementDiagram-M5DCFWZL-C6e8zmrF.js} +1 -1
  75. package/web/assets/{sankeyDiagram-L3NBLAOT-Bu3q2oR9.js → sankeyDiagram-L3NBLAOT-BDcHIgVE.js} +1 -1
  76. package/web/assets/{sequenceDiagram-ZOUHS735-CCaepW8A.js → sequenceDiagram-ZOUHS735-CGAlp5GL.js} +1 -1
  77. package/web/assets/{stateDiagram-MLPALWAM-D0pPSUeb.js → stateDiagram-MLPALWAM-CCKt0fuW.js} +1 -1
  78. package/web/assets/stateDiagram-v2-B5LQ5ZB2-OC7lFsht.js +1 -0
  79. package/web/assets/{timeline-definition-5SPVSISX-Bnt0tSZA.js → timeline-definition-5SPVSISX-D74jeyVQ.js} +1 -1
  80. package/web/assets/treeView-SZITEDCU-5DXDK3XO-8EO9YZIQ.js +1 -0
  81. package/web/assets/treemap-W4RFUUIX-WYLRDWKO-y8YnclBi.js +1 -0
  82. package/web/assets/{vennDiagram-IE5QUKF5-D1QPXqvM.js → vennDiagram-IE5QUKF5-ChHw0kJO.js} +1 -1
  83. package/web/assets/wardley-RL74JXVD-BCRCBASE-1QZagPhJ.js +1 -0
  84. package/web/assets/{wardleyDiagram-XU3VSMPF-DgVu-8V0.js → wardleyDiagram-XU3VSMPF-D5PsSVSp.js} +1 -1
  85. package/web/assets/{xychartDiagram-ZHJ5623Y-DY2IlISC.js → xychartDiagram-ZHJ5623Y-B7E396Z-.js} +1 -1
  86. package/web/index.html +1 -1
  87. package/web/assets/architecture-YZFGNWBL-S5CXDPWN-DJAeZ1fZ.js +0 -1
  88. package/web/assets/classDiagram-PPOCWD7C-CaCJnkFe.js +0 -1
  89. package/web/assets/classDiagram-v2-23LJLIIU-wdvhnHkq.js +0 -1
  90. package/web/assets/gitGraph-7Q5UKJZL-54BCDZD5-DkYkEmdA.js +0 -1
  91. package/web/assets/info-OMHHGYJF-BF2H5H6G-C3e2WXUC.js +0 -1
  92. package/web/assets/infoDiagram-MN7RKWGX-CIHfQ_Ot.js +0 -2
  93. package/web/assets/packet-4T2RLAQJ-EV4IVRXR-_hVXeTya.js +0 -1
  94. package/web/assets/pie-ZZUOXDRM-N23DN5KN-Cwk3Ms79.js +0 -1
  95. package/web/assets/radar-PYXPWWZC-P6TP7ZYP-BuQ3fcO-.js +0 -1
  96. package/web/assets/stateDiagram-v2-B5LQ5ZB2-DQ0mfMOw.js +0 -1
  97. package/web/assets/treeView-SZITEDCU-5DXDK3XO-B2JHGeuB.js +0 -1
  98. package/web/assets/treemap-W4RFUUIX-WYLRDWKO-BKeOEBXU.js +0 -1
  99. package/web/assets/wardley-RL74JXVD-BCRCBASE-DLAazngR.js +0 -1
@@ -16,6 +16,8 @@ import { getStoragePaths, getGlobalRegistryPath, RegistryNameCollisionError, Ana
16
16
  import { getGitRoot, hasGitDir } from '../storage/git.js';
17
17
  import { runFullAnalysis } from '../core/run-analyze.js';
18
18
  import { getMaxFileSizeBannerMessage } from '../core/ingestion/utils/max-file-size.js';
19
+ import { warnMissingOptionalGrammars } from './optional-grammars.js';
20
+ import { glob } from 'glob';
19
21
  import fs from 'fs/promises';
20
22
  // Capture stderr.write at module load BEFORE anything (LadybugDB native
21
23
  // init, progress bar, console redirection) can monkey-patch it. The
@@ -173,6 +175,31 @@ export const analyzeCommand = async (inputPath, options) => {
173
175
  if (!repoHasGit) {
174
176
  console.log(' Warning: no .git directory found \u2014 commit-tracking and incremental updates disabled.\n');
175
177
  }
178
+ // If the target repo contains files an optional grammar would parse but
179
+ // that grammar's native binding is absent, warn before analysis so users
180
+ // learn why those files end up unparsed instead of silently getting a
181
+ // degraded index.
182
+ try {
183
+ const matches = await glob(['**/*.dart', '**/*.proto'], {
184
+ cwd: repoPath,
185
+ ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
186
+ dot: false,
187
+ nodir: true,
188
+ absolute: false,
189
+ });
190
+ if (matches.length > 0) {
191
+ const present = new Set();
192
+ for (const m of matches) {
193
+ const ext = path.extname(m).toLowerCase();
194
+ if (ext)
195
+ present.add(ext);
196
+ }
197
+ warnMissingOptionalGrammars({ context: 'analyze', relevantExtensions: present });
198
+ }
199
+ }
200
+ catch {
201
+ // Best-effort warning \u2014 never block analyze on the precheck.
202
+ }
176
203
  // KuzuDB migration cleanup is handled by runFullAnalysis internally.
177
204
  // Note: --skills is handled after runFullAnalysis using the returned pipelineResult.
178
205
  if (process.env.GITNEXUS_NO_GITIGNORE) {
package/dist/cli/mcp.d.ts CHANGED
@@ -4,5 +4,26 @@
4
4
  * Starts the MCP server in standalone mode.
5
5
  * Loads all indexed repos from the global registry.
6
6
  * No longer depends on cwd — works from any directory.
7
+ *
8
+ * IMPORTANT: this module's static-import closure is intentionally tiny
9
+ * (one chain: `mcp/stdio-context.js` → `mcp/stdio-capture.js`, which is a
10
+ * leaf with zero non-`node:` imports). All heavy backend modules
11
+ * (`startMCPServer`, `LocalBackend`, `warnMissingOptionalGrammars`) load
12
+ * via `await import(...)` AFTER `installGlobalStdoutSentinel()` runs.
13
+ *
14
+ * This closes the ESM-evaluation-order window where native init banners
15
+ * from `@ladybugdb/core` (or any future heavy import) could reach raw
16
+ * stdout before the sentinel exists. Codex's adversarial review on
17
+ * PR #1383 found that even with the sentinel-install call as the first
18
+ * statement of `mcpCommand`, ESM evaluates static imports of THIS module
19
+ * before the function body runs — so any native side effects during
20
+ * those imports happen before the sentinel can intercept them.
21
+ *
22
+ * If you find yourself adding a static `import` to this file, ask
23
+ * whether the imported module (or anything it transitively imports)
24
+ * touches `process.stdout` or loads a native binding at module init. If
25
+ * either is true, switch it to a dynamic `await import(...)` inside
26
+ * `mcpCommand` after the sentinel install. The regression test at
27
+ * `gitnexus/test/integration/mcp/import-closure.test.ts` enforces this.
7
28
  */
8
29
  export declare const mcpCommand: () => Promise<void>;
package/dist/cli/mcp.js CHANGED
@@ -4,21 +4,50 @@
4
4
  * Starts the MCP server in standalone mode.
5
5
  * Loads all indexed repos from the global registry.
6
6
  * No longer depends on cwd — works from any directory.
7
+ *
8
+ * IMPORTANT: this module's static-import closure is intentionally tiny
9
+ * (one chain: `mcp/stdio-context.js` → `mcp/stdio-capture.js`, which is a
10
+ * leaf with zero non-`node:` imports). All heavy backend modules
11
+ * (`startMCPServer`, `LocalBackend`, `warnMissingOptionalGrammars`) load
12
+ * via `await import(...)` AFTER `installGlobalStdoutSentinel()` runs.
13
+ *
14
+ * This closes the ESM-evaluation-order window where native init banners
15
+ * from `@ladybugdb/core` (or any future heavy import) could reach raw
16
+ * stdout before the sentinel exists. Codex's adversarial review on
17
+ * PR #1383 found that even with the sentinel-install call as the first
18
+ * statement of `mcpCommand`, ESM evaluates static imports of THIS module
19
+ * before the function body runs — so any native side effects during
20
+ * those imports happen before the sentinel can intercept them.
21
+ *
22
+ * If you find yourself adding a static `import` to this file, ask
23
+ * whether the imported module (or anything it transitively imports)
24
+ * touches `process.stdout` or loads a native binding at module init. If
25
+ * either is true, switch it to a dynamic `await import(...)` inside
26
+ * `mcpCommand` after the sentinel install. The regression test at
27
+ * `gitnexus/test/integration/mcp/import-closure.test.ts` enforces this.
7
28
  */
8
- import { startMCPServer } from '../mcp/server.js';
9
- import { LocalBackend } from '../mcp/local/local-backend.js';
29
+ import { installGlobalStdoutSentinel } from '../mcp/stdio-context.js';
10
30
  export const mcpCommand = async () => {
11
- // Prevent unhandled errors from crashing the MCP server process.
12
- // LadybugDB lock conflicts and transient errors should degrade gracefully.
13
- process.on('uncaughtException', (err) => {
14
- console.error(`GitNexus MCP: uncaught exception ${err.message}`);
15
- // Process is in an undefined state after uncaughtException — exit after flushing
16
- setTimeout(() => process.exit(1), 100);
17
- });
18
- process.on('unhandledRejection', (reason) => {
19
- const msg = reason instanceof Error ? reason.message : String(reason);
20
- console.error(`GitNexus MCP: unhandled rejection ${msg}`);
21
- });
31
+ // Install the global stdout sentinel as the very first thing — before
32
+ // ANY other module loads. The static-import closure above is leaf-only
33
+ // (stdio-context → stdio-capture, zero non-`node:` deps), so this is
34
+ // also the first chance any code in this process has to write to stdout.
35
+ installGlobalStdoutSentinel();
36
+ // uncaughtException/unhandledRejection handlers are owned by
37
+ // startMCPServer (gitnexus/src/mcp/server.ts) so the server's shutdown
38
+ // path runs cleanly with full stack traces. Registering duplicates here
39
+ // would only produce noisy double-logging on the same exception.
40
+ // Now safe to dynamically import the heavy backend modules. Anything
41
+ // they emit to stdout during evaluation will route through the sentinel.
42
+ const [{ startMCPServer }, { LocalBackend }] = await Promise.all([
43
+ import('../mcp/server.js'),
44
+ import('../mcp/local/local-backend.js'),
45
+ ]);
46
+ // Missing-optional-grammar warnings are intentionally NOT emitted here.
47
+ // `gitnexus analyze` already warns at index time, filtered by the repo's
48
+ // actual extensions, and a repo can only be served by MCP after analyze
49
+ // has run. Repeating an unconditional warning at every MCP startup is
50
+ // pure noise for users whose indexed repos don't use Dart/Proto.
22
51
  // Initialize multi-repo backend from registry.
23
52
  // The server starts even with 0 repos — tools call refreshRepos() lazily,
24
53
  // so repos indexed after the server starts are discovered automatically.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Optional grammar availability check.
3
+ *
4
+ * tree-sitter-dart and tree-sitter-proto are optionalDependencies that
5
+ * require a `node-gyp rebuild` at install time. The build can be skipped
6
+ * via GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or it can
7
+ * silently soft-fail when the C++ toolchain is missing.
8
+ *
9
+ * Either path produces the same observable: the .node binding is absent
10
+ * at runtime. This helper detects that condition and surfaces a single
11
+ * stderr line per missing grammar so users learn why .dart/.proto support
12
+ * is unavailable instead of silently getting a degraded index.
13
+ */
14
+ export interface MissingGrammar {
15
+ name: string;
16
+ extensions: string[];
17
+ }
18
+ /**
19
+ * Returns the list of optional grammars whose native binding cannot be
20
+ * loaded. Actually `require()`s the package — `require.resolve` would
21
+ * locate the entry path even when the `.node` binding is absent (the
22
+ * `file:` package directory is installed regardless of postinstall
23
+ * outcome), giving false negatives for the exact users we want to warn:
24
+ * those who installed with `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` or whose
25
+ * native rebuild soft-failed for missing toolchain.
26
+ *
27
+ * Node's module cache memoizes `require()` for us — calling this multiple
28
+ * times is cheap. The catch distinguishes "missing" (MODULE_NOT_FOUND or
29
+ * the typical node-gyp-build "could not find any binding" pattern) from
30
+ * "broken" (SyntaxError, EACCES, native crash). Broken bindings surface a
31
+ * separate stderr line so users get an actionable message instead of a
32
+ * misleading "reinstall" hint.
33
+ */
34
+ export declare function detectMissingOptionalGrammars(): MissingGrammar[];
35
+ /**
36
+ * Log a one-line stderr warning for each missing grammar. Safe to call
37
+ * unconditionally — silent if all grammars are present.
38
+ *
39
+ * `relevantExtensions`, if provided, filters the warning to grammars whose
40
+ * extensions appear in the set (e.g. an analyze run can pass the set of
41
+ * extensions actually present in the target repo so users without any
42
+ * .dart/.proto files don't see noise).
43
+ */
44
+ export declare function warnMissingOptionalGrammars(opts?: {
45
+ context?: string;
46
+ relevantExtensions?: ReadonlySet<string>;
47
+ }): void;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Optional grammar availability check.
3
+ *
4
+ * tree-sitter-dart and tree-sitter-proto are optionalDependencies that
5
+ * require a `node-gyp rebuild` at install time. The build can be skipped
6
+ * via GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or it can
7
+ * silently soft-fail when the C++ toolchain is missing.
8
+ *
9
+ * Either path produces the same observable: the .node binding is absent
10
+ * at runtime. This helper detects that condition and surfaces a single
11
+ * stderr line per missing grammar so users learn why .dart/.proto support
12
+ * is unavailable instead of silently getting a degraded index.
13
+ */
14
+ import { createRequire } from 'module';
15
+ const _require = createRequire(import.meta.url);
16
+ const OPTIONAL_GRAMMARS = [
17
+ { name: 'tree-sitter-dart', pkg: 'tree-sitter-dart', extensions: ['.dart'] },
18
+ { name: 'tree-sitter-proto', pkg: 'tree-sitter-proto', extensions: ['.proto'] },
19
+ ];
20
+ /**
21
+ * Returns the list of optional grammars whose native binding cannot be
22
+ * loaded. Actually `require()`s the package — `require.resolve` would
23
+ * locate the entry path even when the `.node` binding is absent (the
24
+ * `file:` package directory is installed regardless of postinstall
25
+ * outcome), giving false negatives for the exact users we want to warn:
26
+ * those who installed with `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` or whose
27
+ * native rebuild soft-failed for missing toolchain.
28
+ *
29
+ * Node's module cache memoizes `require()` for us — calling this multiple
30
+ * times is cheap. The catch distinguishes "missing" (MODULE_NOT_FOUND or
31
+ * the typical node-gyp-build "could not find any binding" pattern) from
32
+ * "broken" (SyntaxError, EACCES, native crash). Broken bindings surface a
33
+ * separate stderr line so users get an actionable message instead of a
34
+ * misleading "reinstall" hint.
35
+ */
36
+ export function detectMissingOptionalGrammars() {
37
+ const missing = [];
38
+ for (const g of OPTIONAL_GRAMMARS) {
39
+ try {
40
+ _require(g.pkg);
41
+ }
42
+ catch (err) {
43
+ const code = err?.code;
44
+ const msg = err instanceof Error ? err.message : String(err);
45
+ const looksMissing = code === 'MODULE_NOT_FOUND' ||
46
+ code === 'ERR_MODULE_NOT_FOUND' ||
47
+ /could not find|no native build|prebuilds/i.test(msg);
48
+ if (!looksMissing) {
49
+ // Present but broken — surface so the user doesn't get a misleading
50
+ // "reinstall" recovery message that wouldn't actually help.
51
+ console.error(`GitNexus: optional grammar "${g.name}" is installed but failed to load (${msg.slice(0, 200)}). ${g.extensions.join('/')} files will not be parsed.`);
52
+ }
53
+ missing.push({ name: g.name, extensions: g.extensions });
54
+ }
55
+ }
56
+ return missing;
57
+ }
58
+ /**
59
+ * Log a one-line stderr warning for each missing grammar. Safe to call
60
+ * unconditionally — silent if all grammars are present.
61
+ *
62
+ * `relevantExtensions`, if provided, filters the warning to grammars whose
63
+ * extensions appear in the set (e.g. an analyze run can pass the set of
64
+ * extensions actually present in the target repo so users without any
65
+ * .dart/.proto files don't see noise).
66
+ */
67
+ export function warnMissingOptionalGrammars(opts) {
68
+ const missing = detectMissingOptionalGrammars();
69
+ if (missing.length === 0)
70
+ return;
71
+ const ctx = opts?.context ? ` [${opts.context}]` : '';
72
+ for (const g of missing) {
73
+ if (opts?.relevantExtensions && !g.extensions.some((e) => opts.relevantExtensions.has(e))) {
74
+ continue;
75
+ }
76
+ console.error(`GitNexus${ctx}: optional grammar "${g.name}" is unavailable — ${g.extensions.join('/')} files will not be parsed. Reinstall without GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (and ensure python3, make, g++) to enable.`);
77
+ }
78
+ }
package/dist/cli/setup.js CHANGED
@@ -9,6 +9,7 @@ import fs from 'fs/promises';
9
9
  import path from 'path';
10
10
  import os from 'os';
11
11
  import { execFile, execFileSync } from 'child_process';
12
+ import { createRequire } from 'module';
12
13
  import { promisify } from 'util';
13
14
  import { fileURLToPath } from 'url';
14
15
  import { glob } from 'glob';
@@ -17,6 +18,18 @@ import { getGlobalDir } from '../storage/repo-manager.js';
17
18
  const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = path.dirname(__filename);
19
20
  const execFileAsync = promisify(execFile);
21
+ // Pin the npx fallback to the installed version. Reason: setup.ts writes
22
+ // a config that persists in the user's editor and is invoked on every MCP
23
+ // connect. Pinning to the installed version means subsequent invocations
24
+ // skip the npm-registry metadata roundtrip (and stay reproducible until
25
+ // the user upgrades). Static configs and READMEs intentionally use
26
+ // `gitnexus@latest` since they're quickstart docs, not persisted state.
27
+ const _require = createRequire(import.meta.url);
28
+ const _pkg = _require('../../package.json');
29
+ if (typeof _pkg.version !== 'string' || !_pkg.version) {
30
+ throw new Error('gitnexus/package.json#version is missing or not a string — cannot generate MCP fallback config.');
31
+ }
32
+ const NPX_REF = `gitnexus@${_pkg.version}`;
20
33
  /**
21
34
  * Resolve the absolute path to the `gitnexus` binary if it's installed
22
35
  * globally (or via npm -g / yarn global). Returns null when not found.
@@ -51,8 +64,10 @@ function resolveGitnexusBin() {
51
64
  * The MCP server entry for all editors.
52
65
  *
53
66
  * Prefers the globally-installed `gitnexus` binary (starts in ~1 s) over
54
- * `npx -y gitnexus@latest` (cold-cache install of native deps can take
55
- * >60 s, exceeding Claude Code's 30 s MCP connection timeout).
67
+ * `npx -y gitnexus@<version>` (cold-cache install of native deps can take
68
+ * >60 s, exceeding Claude Code's 30 s MCP connection timeout). The fallback
69
+ * version is read from gitnexus/package.json#version at module load so the
70
+ * persisted user config matches the installed package.
56
71
  *
57
72
  * Falls back to npx when the binary isn't on PATH — e.g. first-time
58
73
  * users who ran `npx gitnexus analyze` but haven't done `npm i -g`.
@@ -66,12 +81,12 @@ function getMcpEntry() {
66
81
  if (process.platform === 'win32') {
67
82
  return {
68
83
  command: 'cmd',
69
- args: ['/c', 'npx', '-y', 'gitnexus@latest', 'mcp'],
84
+ args: ['/c', 'npx', '-y', NPX_REF, 'mcp'],
70
85
  };
71
86
  }
72
87
  return {
73
88
  command: 'npx',
74
- args: ['-y', 'gitnexus@latest', 'mcp'],
89
+ args: ['-y', NPX_REF, 'mcp'],
75
90
  };
76
91
  }
77
92
  /**
@@ -84,9 +99,9 @@ function getOpenCodeMcpEntry() {
84
99
  return { type: 'local', command: [bin, 'mcp'] };
85
100
  }
86
101
  if (process.platform === 'win32') {
87
- return { type: 'local', command: ['cmd', '/c', 'npx', '-y', 'gitnexus@latest', 'mcp'] };
102
+ return { type: 'local', command: ['cmd', '/c', 'npx', '-y', NPX_REF, 'mcp'] };
88
103
  }
89
- return { type: 'local', command: ['npx', '-y', 'gitnexus@latest', 'mcp'] };
104
+ return { type: 'local', command: ['npx', '-y', NPX_REF, 'mcp'] };
90
105
  }
91
106
  /**
92
107
  * Detect indentation style from file content.
@@ -139,7 +139,7 @@ export const initEmbedder = async (onProgress, config = {}, forceDevice) => {
139
139
  applyHfEnvOverrides(env);
140
140
  const isDev = process.env.NODE_ENV === 'development';
141
141
  if (isDev) {
142
- console.log(`🧠 Loading embedding model: ${finalConfig.modelId}`);
142
+ console.error(`🧠 Loading embedding model: ${finalConfig.modelId}`);
143
143
  }
144
144
  const progressCallback = onProgress
145
145
  ? (data) => {
@@ -161,16 +161,16 @@ export const initEmbedder = async (onProgress, config = {}, forceDevice) => {
161
161
  for (const device of devicesToTry) {
162
162
  try {
163
163
  if (isDev && device === 'dml') {
164
- console.log('🔧 Trying DirectML (DirectX12) GPU backend...');
164
+ console.error('🔧 Trying DirectML (DirectX12) GPU backend...');
165
165
  }
166
166
  else if (isDev && device === 'cuda') {
167
- console.log('🔧 Trying CUDA GPU backend...');
167
+ console.error('🔧 Trying CUDA GPU backend...');
168
168
  }
169
169
  else if (isDev && device === 'cpu') {
170
- console.log('🔧 Using CPU backend...');
170
+ console.error('🔧 Using CPU backend...');
171
171
  }
172
172
  else if (isDev && device === 'wasm') {
173
- console.log('🔧 Using WASM backend (slower)...');
173
+ console.error('🔧 Using WASM backend (slower)...');
174
174
  }
175
175
  embedderInstance = await pipeline('feature-extraction', finalConfig.modelId, {
176
176
  device: device,
@@ -190,15 +190,15 @@ export const initEmbedder = async (onProgress, config = {}, forceDevice) => {
190
190
  : device === 'cuda'
191
191
  ? 'GPU (CUDA)'
192
192
  : device.toUpperCase();
193
- console.log(`✅ Using ${label} backend`);
194
- console.log('✅ Embedding model loaded successfully');
193
+ console.error(`✅ Using ${label} backend`);
194
+ console.error('✅ Embedding model loaded successfully');
195
195
  }
196
196
  return embedderInstance;
197
197
  }
198
198
  catch (deviceError) {
199
199
  if (isDev && (device === 'cuda' || device === 'dml')) {
200
200
  const gpuType = device === 'dml' ? 'DirectML' : 'CUDA';
201
- console.log(`⚠️ ${gpuType} not available, falling back to CPU...`);
201
+ console.error(`⚠️ ${gpuType} not available, falling back to CPU...`);
202
202
  }
203
203
  // Continue to next device in list
204
204
  if (device === devicesToTry[devicesToTry.length - 1]) {
@@ -112,7 +112,7 @@ const queryEmbeddableNodes = async (executeQuery) => {
112
112
  }
113
113
  catch (error) {
114
114
  if (isDev) {
115
- console.warn(`Query for ${label} nodes failed:`, error);
115
+ console.error(`Query for ${label} nodes failed:`, error);
116
116
  }
117
117
  }
118
118
  }
@@ -151,7 +151,7 @@ const createVectorIndex = async (executeQuery) => {
151
151
  }
152
152
  catch (error) {
153
153
  if (isDev) {
154
- console.warn('Vector index creation warning:', error);
154
+ console.error('Vector index creation warning:', error);
155
155
  }
156
156
  return false;
157
157
  }
@@ -176,7 +176,7 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
176
176
  try {
177
177
  const vectorAvailable = await ensureVectorExtensionAvailable();
178
178
  if (!vectorAvailable && isDev)
179
- console.warn(vectorUnavailableMessage);
179
+ console.error(vectorUnavailableMessage);
180
180
  // Phase 1: Load embedding model
181
181
  onProgress({
182
182
  phase: 'loading-model',
@@ -199,7 +199,7 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
199
199
  modelDownloadPercent: 100,
200
200
  });
201
201
  if (isDev) {
202
- console.log('🔍 Querying embeddable nodes...');
202
+ console.error('🔍 Querying embeddable nodes...');
203
203
  }
204
204
  // Phase 2: Query embeddable nodes
205
205
  let nodes = await queryEmbeddableNodes(executeQuery);
@@ -237,7 +237,7 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
237
237
  // (Kuzu forbids SET on vector-indexed properties; DELETE-then-INSERT is the sanctioned pattern)
238
238
  if (staleNodeIds.length > 0) {
239
239
  if (isDev) {
240
- console.log(`🔄 Deleting ${staleNodeIds.length} stale embedding rows for re-embed`);
240
+ console.error(`🔄 Deleting ${staleNodeIds.length} stale embedding rows for re-embed`);
241
241
  }
242
242
  try {
243
243
  await executeWithReusedStatement(`MATCH (e:${EMBEDDING_TABLE_NAME} {nodeId: $nodeId}) DELETE e`, staleNodeIds.map((nodeId) => ({ nodeId })));
@@ -253,12 +253,12 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
253
253
  }
254
254
  }
255
255
  if (isDev) {
256
- console.log(`📦 Incremental embeddings: ${beforeCount} total, ${existingEmbeddings.size} cached, ${staleNodeIds.length} stale, ${nodes.length} to embed`);
256
+ console.error(`📦 Incremental embeddings: ${beforeCount} total, ${existingEmbeddings.size} cached, ${staleNodeIds.length} stale, ${nodes.length} to embed`);
257
257
  }
258
258
  }
259
259
  const totalNodes = nodes.length;
260
260
  if (isDev) {
261
- console.log(`📊 Found ${totalNodes} embeddable nodes`);
261
+ console.error(`📊 Found ${totalNodes} embeddable nodes`);
262
262
  }
263
263
  if (totalNodes === 0) {
264
264
  // Ensure the vector index exists even when no new nodes need embedding.
@@ -324,7 +324,7 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
324
324
  }
325
325
  catch (chunkErr) {
326
326
  if (isDev) {
327
- console.warn(`⚠️ AST chunking failed for ${node.label} "${node.name}" (${node.filePath}), falling back to character-based chunking:`, chunkErr);
327
+ console.error(`⚠️ AST chunking failed for ${node.label} "${node.name}" (${node.filePath}), falling back to character-based chunking:`, chunkErr);
328
328
  }
329
329
  chunks = characterChunk(node.content, startLine, endLine, chunkSize, overlap);
330
330
  }
@@ -382,7 +382,7 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
382
382
  totalNodes,
383
383
  });
384
384
  if (isDev) {
385
- console.log('📇 Creating vector index...');
385
+ console.error('📇 Creating vector index...');
386
386
  }
387
387
  const vectorIndexReady = await createVectorIndex(executeQuery);
388
388
  onProgress({
@@ -392,7 +392,7 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
392
392
  totalNodes,
393
393
  });
394
394
  if (isDev) {
395
- console.log(`✅ Embedding pipeline complete! (${totalChunks} chunks from ${totalNodes} nodes)`);
395
+ console.error(`✅ Embedding pipeline complete! (${totalChunks} chunks from ${totalNodes} nodes)`);
396
396
  }
397
397
  return {
398
398
  nodesProcessed: totalNodes,
@@ -125,7 +125,7 @@ export class ExtensionManager {
125
125
  }
126
126
  const policy = opts.policy ?? this.options.policy ?? resolvePolicyFromEnv();
127
127
  const timeoutMs = opts.installTimeoutMs ?? this.options.installTimeoutMs ?? getExtensionInstallTimeoutMs();
128
- const warn = this.options.warn ?? console.warn;
128
+ const warn = this.options.warn ?? console.error;
129
129
  if (policy === 'never') {
130
130
  this.markUnavailable(name, label, 'extension install policy is "never"', warn);
131
131
  return false;
@@ -274,7 +274,7 @@ const doInitLbug = async (dbPath) => {
274
274
  catch (err) {
275
275
  const msg = err instanceof Error ? err.message : String(err);
276
276
  if (!msg.includes('already exists')) {
277
- console.warn(`⚠️ Schema creation warning: ${msg.slice(0, 120)}`);
277
+ console.error(`[gitnexus:lbug] schema creation warning: ${msg.slice(0, 120)}`);
278
278
  }
279
279
  }
280
280
  }
@@ -889,13 +889,13 @@ export const fetchExistingEmbeddingHashes = async (execQuery) => {
889
889
  if (nodeId)
890
890
  map.set(nodeId, STALE_HASH_SENTINEL);
891
891
  }
892
- console.log(`[embed] ${map.size} nodes in legacy DB (missing chunk-aware columns) — all treated as stale`);
892
+ console.error(`[gitnexus:embed] ${map.size} nodes in legacy DB (missing chunk-aware columns) — all treated as stale`);
893
893
  return map;
894
894
  }
895
895
  catch (fallbackErr) {
896
896
  const fallbackMsg = fallbackErr?.message ?? '';
897
897
  if (isMissingColumnOrTableError(fallbackMsg)) {
898
- console.log(`[embed] CodeEmbedding table not yet present — full embedding run (${fallbackMsg})`);
898
+ console.error(`[gitnexus:embed] CodeEmbedding table not yet present — full embedding run (${fallbackMsg})`);
899
899
  return undefined;
900
900
  }
901
901
  throw fallbackErr;
@@ -31,9 +31,7 @@ type PoolCloseListener = (repoId: string) => void;
31
31
  * listener (handy for tests).
32
32
  */
33
33
  export declare function addPoolCloseListener(listener: PoolCloseListener): () => void;
34
- /** Saved real stdout/stderr write used to silence native module output without race conditions */
35
- export declare const realStdoutWrite: any;
36
- export declare const realStderrWrite: any;
34
+ export { realStdoutWrite, realStderrWrite, setActiveStdoutWrite } from '../../mcp/stdio-capture.js';
37
35
  /**
38
36
  * Touch a repo to reset its idle timeout.
39
37
  * Call this during long-running operations to prevent the connection from being closed.
@@ -90,4 +88,3 @@ export declare const isLbugReady: (repoId: string) => boolean;
90
88
  export declare const CYPHER_WRITE_RE: RegExp;
91
89
  /** Check if a Cypher query contains write operations */
92
90
  export declare function isWriteQuery(query: string): boolean;
93
- export {};
@@ -38,9 +38,20 @@ const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
38
38
  /** Max connections per repo (caps concurrent queries per repo) */
39
39
  const MAX_CONNS_PER_REPO = 8;
40
40
  let idleTimer = null;
41
- /** Saved real stdout/stderr write used to silence native module output without race conditions */
42
- export const realStdoutWrite = process.stdout.write.bind(process.stdout);
43
- export const realStderrWrite = process.stderr.write.bind(process.stderr);
41
+ // Stdout-capture state lives in `gitnexus/src/mcp/stdio-capture.ts`a leaf
42
+ // module with zero non-`node:` imports. We re-export the same symbols here
43
+ // so the existing test mock seam (`gitnexus/src/mcp/core/lbug-adapter.ts`
44
+ // re-exports * from this file, and 8+ test files use that path with
45
+ // `vi.mock(...)`) continues to work without churn. The source of truth is
46
+ // the leaf module; this re-export is a compatibility shim.
47
+ //
48
+ // Why the leaf module exists: Codex's adversarial review on PR #1383 found
49
+ // that putting this state in pool-adapter.ts pulled `@ladybugdb/core` into
50
+ // `cli/mcp.ts`'s static-import closure (via stdio-context → pool-adapter →
51
+ // @ladybugdb/core), corrupting stdout in the pre-sentinel window. Routing
52
+ // through the leaf breaks that chain.
53
+ export { realStdoutWrite, realStderrWrite, setActiveStdoutWrite } from '../../mcp/stdio-capture.js';
54
+ import { getActiveStdoutWrite } from '../../mcp/stdio-capture.js';
44
55
  let stdoutSilenceCount = 0;
45
56
  /** True while pre-warming connections — prevents watchdog from prematurely restoring stdout */
46
57
  let preWarmActive = false;
@@ -155,13 +166,15 @@ let activeQueryCount = 0;
155
166
  */
156
167
  export function silenceStdout() {
157
168
  if (stdoutSilenceCount++ === 0) {
169
+ // eslint-disable-next-line no-restricted-syntax -- silencing infrastructure; replacement is a no-op
158
170
  process.stdout.write = (() => true);
159
171
  }
160
172
  }
161
173
  export function restoreStdout() {
162
174
  if (--stdoutSilenceCount <= 0) {
163
175
  stdoutSilenceCount = 0;
164
- process.stdout.write = realStdoutWrite;
176
+ // eslint-disable-next-line no-restricted-syntax -- restoring the active stdout-write handler is the silencing API contract
177
+ process.stdout.write = getActiveStdoutWrite();
165
178
  }
166
179
  }
167
180
  // Safety watchdog: restore stdout if it gets stuck silenced (e.g. native crash
@@ -171,7 +184,8 @@ export function restoreStdout() {
171
184
  setInterval(() => {
172
185
  if (stdoutSilenceCount > 0 && !preWarmActive && activeQueryCount === 0) {
173
186
  stdoutSilenceCount = 0;
174
- process.stdout.write = realStdoutWrite;
187
+ // eslint-disable-next-line no-restricted-syntax -- watchdog recovery for stuck silencing
188
+ process.stdout.write = getActiveStdoutWrite();
175
189
  }
176
190
  }, 1000).unref();
177
191
  function createConnection(db) {
@@ -114,10 +114,10 @@ const logFailure = (key, result) => {
114
114
  return;
115
115
  logged.add(key);
116
116
  const message = `[gitnexus] ${result.note} (${result.error.message})`;
117
- if (result.severity === 'error')
118
- console.error(message);
119
- else
120
- console.warn(message);
117
+ // Both severities go to stderr — console.warn writes to stderr too, but
118
+ // console.error is the stdout-safe channel we standardize on across
119
+ // MCP-reachable code so the ESLint rule covers this directory.
120
+ console.error(message);
121
121
  };
122
122
  export const resolveLanguageKey = (language, filePath) => language === SupportedLanguages.TypeScript && filePath?.endsWith('.tsx')
123
123
  ? `${language}:tsx`
@@ -1,5 +1,6 @@
1
1
  import process from 'node:process';
2
2
  import { JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
3
+ import { withMcpWrite } from './stdio-context.js';
3
4
  function deserializeMessage(raw) {
4
5
  return JSONRPCMessageSchema.parse(JSON.parse(raw));
5
6
  }
@@ -185,7 +186,12 @@ export class CompatibleStdioServerTransport {
185
186
  reject(error);
186
187
  };
187
188
  this._stdout.on('error', onError);
188
- if (this._stdout.write(payload)) {
189
+ // Tag the write with the MCP transport context so the sentinel
190
+ // (server.ts createStdoutSentinel Proxy) recognizes it as a legitimate
191
+ // JSON-RPC frame and passes it through to the real stdout instead of
192
+ // redirecting to stderr.
193
+ const writeOk = withMcpWrite(() => this._stdout.write(payload));
194
+ if (writeOk) {
189
195
  this._stdout.removeListener('error', onError);
190
196
  resolve();
191
197
  }
@@ -1,5 +1,11 @@
1
1
  /**
2
2
  * LadybugDB connection pool — re-exported from core.
3
- * Prefer importing from `../../core/lbug/pool-adapter.js` in new code.
3
+ *
4
+ * KEEP THIS FILE. It is intentionally a shim re-export of
5
+ * `../../core/lbug/pool-adapter.js`. The MCP test suite uses this path as
6
+ * a vi.mock seam so unit tests can stub LadybugDB without affecting other
7
+ * importers of `core/lbug/pool-adapter.js` (which is shared with the
8
+ * analyze pipeline). New non-test code MAY import from `pool-adapter.js`
9
+ * directly, but the shim must continue to exist for the mock seam to work.
4
10
  */
5
11
  export * from '../../core/lbug/pool-adapter.js';
@@ -1,5 +1,11 @@
1
1
  /**
2
2
  * LadybugDB connection pool — re-exported from core.
3
- * Prefer importing from `../../core/lbug/pool-adapter.js` in new code.
3
+ *
4
+ * KEEP THIS FILE. It is intentionally a shim re-export of
5
+ * `../../core/lbug/pool-adapter.js`. The MCP test suite uses this path as
6
+ * a vi.mock seam so unit tests can stub LadybugDB without affecting other
7
+ * importers of `core/lbug/pool-adapter.js` (which is shared with the
8
+ * analyze pipeline). New non-test code MAY import from `pool-adapter.js`
9
+ * directly, but the shim must continue to exist for the mock seam to work.
4
10
  */
5
11
  export * from '../../core/lbug/pool-adapter.js';