gitnexus 1.6.9 → 1.6.10-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 (91) hide show
  1. package/README.md +126 -41
  2. package/dist/cli/ai-context.d.ts +1 -1
  3. package/dist/cli/ai-context.js +1 -1
  4. package/dist/cli/analyze.js +106 -3
  5. package/dist/cli/cli-message.d.ts +1 -1
  6. package/dist/cli/doctor.d.ts +5 -0
  7. package/dist/cli/doctor.js +41 -3
  8. package/dist/cli/editor-targets.d.ts +17 -2
  9. package/dist/cli/editor-targets.js +44 -0
  10. package/dist/cli/embeddings.d.ts +12 -0
  11. package/dist/cli/embeddings.js +49 -0
  12. package/dist/cli/help-i18n.js +4 -0
  13. package/dist/cli/i18n/en.d.ts +5 -1
  14. package/dist/cli/i18n/en.js +5 -1
  15. package/dist/cli/i18n/resources.d.ts +9 -1
  16. package/dist/cli/i18n/zh-CN.d.ts +4 -0
  17. package/dist/cli/i18n/zh-CN.js +5 -1
  18. package/dist/cli/index.js +12 -1
  19. package/dist/cli/setup.js +204 -28
  20. package/dist/cli/uninstall.js +70 -16
  21. package/dist/core/embeddings/embedder.js +16 -2
  22. package/dist/core/embeddings/http-client.d.ts +28 -1
  23. package/dist/core/embeddings/http-client.js +107 -15
  24. package/dist/core/embeddings/node-module-compat.d.ts +23 -0
  25. package/dist/core/embeddings/node-module-compat.js +23 -0
  26. package/dist/core/embeddings/onnxruntime-common-resolver.js +6 -3
  27. package/dist/core/embeddings/onnxruntime-node-resolver.js +31 -5
  28. package/dist/core/embeddings/runtime-install.d.ts +119 -0
  29. package/dist/core/embeddings/runtime-install.js +372 -0
  30. package/dist/core/embeddings/runtime-support.d.ts +48 -17
  31. package/dist/core/embeddings/runtime-support.js +96 -0
  32. package/dist/core/group/extractors/http-patterns/python.js +246 -108
  33. package/dist/core/group/extractors/manifest-extractor.js +4 -0
  34. package/dist/core/ingestion/cobol/jcl-processor.js +9 -8
  35. package/dist/core/ingestion/cobol-processor.js +27 -26
  36. package/dist/core/ingestion/community-processor.d.ts +51 -1
  37. package/dist/core/ingestion/community-processor.js +316 -63
  38. package/dist/core/ingestion/emit-references.js +3 -2
  39. package/dist/core/ingestion/markdown-processor.js +3 -2
  40. package/dist/core/ingestion/parsing-processor.d.ts +3 -1
  41. package/dist/core/ingestion/parsing-processor.js +4 -0
  42. package/dist/core/ingestion/pipeline-phases/parse-impl.js +42 -0
  43. package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +83 -0
  44. package/dist/core/ingestion/route-extractors/constant-resolver.js +131 -0
  45. package/dist/core/ingestion/route-extractors/python-const-resolver.d.ts +75 -0
  46. package/dist/core/ingestion/route-extractors/python-const-resolver.js +299 -0
  47. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
  48. package/dist/core/ingestion/tree-sitter-queries.js +12 -1
  49. package/dist/core/ingestion/utils/line-base.d.ts +20 -0
  50. package/dist/core/ingestion/utils/line-base.js +20 -0
  51. package/dist/core/ingestion/utils/symbol-labels.d.ts +21 -0
  52. package/dist/core/ingestion/utils/symbol-labels.js +45 -0
  53. package/dist/core/ingestion/workers/parse-worker.d.ts +34 -0
  54. package/dist/core/ingestion/workers/parse-worker.js +39 -4
  55. package/dist/core/ingestion/workers/result-merge.js +4 -0
  56. package/dist/core/lbug/csv-generator.js +8 -2
  57. package/dist/core/lbug/extension-load-error.d.ts +67 -0
  58. package/dist/core/lbug/extension-load-error.js +320 -0
  59. package/dist/core/lbug/extension-loader.d.ts +17 -2
  60. package/dist/core/lbug/extension-loader.js +38 -13
  61. package/dist/core/lbug/lbug-adapter.js +7 -16
  62. package/dist/core/lbug/native-check.d.ts +22 -0
  63. package/dist/core/lbug/native-check.js +66 -0
  64. package/dist/core/lbug/pool-adapter.js +10 -1
  65. package/dist/core/lbug/sidecar-recovery.d.ts +32 -0
  66. package/dist/core/lbug/sidecar-recovery.js +91 -7
  67. package/dist/core/platform/capabilities.js +27 -1
  68. package/dist/core/run-analyze.js +33 -6
  69. package/dist/core/search/fts-indexes.d.ts +7 -0
  70. package/dist/core/search/fts-indexes.js +37 -0
  71. package/dist/mcp/core/embedder.js +16 -2
  72. package/dist/mcp/local/line-display.d.ts +22 -0
  73. package/dist/mcp/local/line-display.js +3 -0
  74. package/dist/mcp/local/local-backend.d.ts +14 -0
  75. package/dist/mcp/local/local-backend.js +63 -19
  76. package/dist/mcp/local/pdg-impact.d.ts +5 -3
  77. package/dist/mcp/local/pdg-impact.js +5 -2
  78. package/dist/mcp/resources.js +1 -0
  79. package/dist/mcp/tools.js +1 -1
  80. package/dist/server/api.js +2 -2
  81. package/dist/storage/parse-cache.js +1 -1
  82. package/dist/storage/repo-manager.d.ts +6 -1
  83. package/dist/storage/repo-manager.js +6 -1
  84. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +50 -3
  85. package/hooks/claude/gitnexus-hook.cjs +59 -9
  86. package/package.json +5 -3
  87. package/scripts/cross-platform-tests.ts +26 -0
  88. package/scripts/ensure-fts.ts +32 -0
  89. package/scripts/install-duckdb-extension.mjs +85 -24
  90. package/scripts/run-cross-platform.ts +46 -6
  91. package/scripts/shard-arg.ts +30 -0
@@ -0,0 +1,12 @@
1
+ export interface EmbeddingsInstallOptions {
2
+ cuda?: boolean;
3
+ force?: boolean;
4
+ }
5
+ /**
6
+ * `gitnexus embeddings install [--cuda] [--force]` — fetch the optional local
7
+ * embedding stack on demand (#2370). Goes through the user's npm registry
8
+ * config (mirrors/proxies apply); with --cuda it additionally runs
9
+ * onnxruntime-node's postinstall to download the CUDA GPU binaries from NuGet
10
+ * (set GLOBAL_AGENT_HTTPS_PROXY behind a proxy).
11
+ */
12
+ export declare const embeddingsInstallCommand: (options?: EmbeddingsInstallOptions) => Promise<void>;
@@ -0,0 +1,49 @@
1
+ import { cliError, cliInfo, cliWarn } from './cli-message.js';
2
+ import { getEmbeddingRuntimeDir, getEmbeddingStackSpecs, installEmbeddingRuntime, isPrefixRuntimeLoadable, resolveEmbeddingRuntime, } from '../core/embeddings/runtime-install.js';
3
+ import { localEmbeddingPrefixUnloadableMessage } from '../core/embeddings/runtime-support.js';
4
+ /**
5
+ * `gitnexus embeddings install [--cuda] [--force]` — fetch the optional local
6
+ * embedding stack on demand (#2370). Goes through the user's npm registry
7
+ * config (mirrors/proxies apply); with --cuda it additionally runs
8
+ * onnxruntime-node's postinstall to download the CUDA GPU binaries from NuGet
9
+ * (set GLOBAL_AGENT_HTTPS_PROXY behind a proxy).
10
+ */
11
+ export const embeddingsInstallCommand = async (options = {}) => {
12
+ const resolved = resolveEmbeddingRuntime();
13
+ if (resolved?.source === 'package' && !options.force) {
14
+ cliInfo('The embedding stack is already installed with gitnexus itself — nothing to do.\n' +
15
+ '(Use --force to install a copy into the runtime prefix anyway.)');
16
+ return;
17
+ }
18
+ const specs = Object.entries(getEmbeddingStackSpecs())
19
+ .map(([name, spec]) => `${name}@${spec}`)
20
+ .join(', ');
21
+ cliInfo(`Installing ${specs} into ${getEmbeddingRuntimeDir()} …`);
22
+ cliInfo(options.cuda
23
+ ? 'CUDA mode: onnxruntime-node will download GPU binaries from NuGet ' +
24
+ '(set GLOBAL_AGENT_HTTPS_PROXY=<proxy-url> behind a proxy).'
25
+ : 'CPU mode: install scripts are skipped — only your npm registry is contacted.');
26
+ try {
27
+ await installEmbeddingRuntime({ cuda: options.cuda, onOutput: (line) => cliInfo(` ${line}`) });
28
+ }
29
+ catch (err) {
30
+ cliError(`${err instanceof Error ? err.message : String(err)}\n`, {
31
+ recoveryHint: 'local-embedding-stack-missing',
32
+ });
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ const postInstall = resolveEmbeddingRuntime();
37
+ if (postInstall === null) {
38
+ cliInfo('✗ Install completed but the stack still does not resolve — check the output above.');
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+ if (postInstall.source === 'runtime-prefix' && !isPrefixRuntimeLoadable()) {
43
+ // The packages are in the prefix, but this Node has no module.registerHooks
44
+ // to load them — don't claim readiness the loader can't honour.
45
+ cliWarn(`${localEmbeddingPrefixUnloadableMessage()}\n`);
46
+ return;
47
+ }
48
+ cliInfo('✓ Embedding runtime installed. `gitnexus analyze --embeddings` is ready.');
49
+ };
@@ -17,6 +17,8 @@ const COMMAND_DESCRIPTION_KEYS = {
17
17
  list: 'help.command.list.description',
18
18
  status: 'help.command.status.description',
19
19
  doctor: 'help.command.doctor.description',
20
+ embeddings: 'help.command.embeddings.description',
21
+ 'embeddings install': 'help.command.embeddings.install.description',
20
22
  clean: 'help.command.clean.description',
21
23
  remove: 'help.command.remove.description',
22
24
  wiki: 'help.command.wiki.description',
@@ -143,6 +145,8 @@ const OPTION_DESCRIPTION_KEYS = {
143
145
  'eval-server|-p, --port <port>': 'help.option.port',
144
146
  'eval-server|--host <host>': 'help.option.evalServer.host',
145
147
  'eval-server|--idle-timeout <seconds>': 'help.option.evalServer.idleTimeout',
148
+ 'embeddings install|--cuda': 'help.option.embeddings.install.cuda',
149
+ 'embeddings install|--force': 'help.option.embeddings.install.force',
146
150
  'group create|--force': 'help.option.group.create.force',
147
151
  'group sync|--skip-embeddings': 'help.option.group.sync.skipEmbeddings',
148
152
  'group sync|--exact-only': 'help.option.group.sync.exactOnly',
@@ -108,7 +108,7 @@ export declare const en: {
108
108
  readonly 'help.command.help.description': "display help for command";
109
109
  readonly 'help.option.help': "display help for command";
110
110
  readonly 'help.option.version': "output the version number";
111
- readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex";
111
+ readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex";
112
112
  readonly 'help.command.uninstall.description': "Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors";
113
113
  readonly 'help.command.analyze.description': "Index a repository (full analysis)";
114
114
  readonly 'help.command.index.description': "Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)";
@@ -117,6 +117,8 @@ export declare const en: {
117
117
  readonly 'help.command.list.description': "List all indexed repositories";
118
118
  readonly 'help.command.status.description': "Show index status for current repo";
119
119
  readonly 'help.command.doctor.description': "Show runtime platform capabilities and embedding configuration";
120
+ readonly 'help.command.embeddings.description': "Manage the on-demand local embedding runtime";
121
+ readonly 'help.command.embeddings.install.description': "Install the local embedding stack (@huggingface/transformers + onnxruntime-node) on demand. Heals installs where npm skipped the optional packages (e.g. behind an HTTP proxy, #2370). Downloads only from your configured npm registry — mirrors and proxies apply.";
120
122
  readonly 'help.command.clean.description': "Delete GitNexus index for current repo";
121
123
  readonly 'help.command.remove.description': "Delete the GitNexus index for a registered repo (by alias, name, or absolute path). Unlike `clean`, does not require being inside the repo. Idempotent on unknown targets.";
122
124
  readonly 'help.command.wiki.description': "Generate repository wiki from knowledge graph";
@@ -220,6 +222,8 @@ export declare const en: {
220
222
  readonly 'help.option.check.cycles': "Detect circular imports and fail when any are found";
221
223
  readonly 'help.option.evalServer.host': "Bind address (default: 127.0.0.1, use 0.0.0.0 to expose to all interfaces)";
222
224
  readonly 'help.option.evalServer.idleTimeout': "Auto-shutdown after N seconds idle (0 = disabled)";
225
+ readonly 'help.option.embeddings.install.cuda': "Also download the CUDA GPU binaries (runs onnxruntime-node's NuGet postinstall; set GLOBAL_AGENT_HTTPS_PROXY behind a proxy)";
226
+ readonly 'help.option.embeddings.install.force': "Install into the runtime prefix even when the stack already resolves";
223
227
  readonly 'help.option.group.create.force': "Overwrite existing group";
224
228
  readonly 'help.option.group.sync.skipEmbeddings': "Exact + BM25 only (no embedding fallback)";
225
229
  readonly 'help.option.group.sync.exactOnly': "Exact match only";
@@ -108,7 +108,7 @@ export const en = {
108
108
  'help.command.help.description': 'display help for command',
109
109
  'help.option.help': 'display help for command',
110
110
  'help.option.version': 'output the version number',
111
- 'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex',
111
+ 'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex',
112
112
  'help.command.uninstall.description': 'Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors',
113
113
  'help.command.analyze.description': 'Index a repository (full analysis)',
114
114
  'help.command.index.description': 'Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)',
@@ -117,6 +117,8 @@ export const en = {
117
117
  'help.command.list.description': 'List all indexed repositories',
118
118
  'help.command.status.description': 'Show index status for current repo',
119
119
  'help.command.doctor.description': 'Show runtime platform capabilities and embedding configuration',
120
+ 'help.command.embeddings.description': 'Manage the on-demand local embedding runtime',
121
+ 'help.command.embeddings.install.description': 'Install the local embedding stack (@huggingface/transformers + onnxruntime-node) on demand. Heals installs where npm skipped the optional packages (e.g. behind an HTTP proxy, #2370). Downloads only from your configured npm registry — mirrors and proxies apply.',
120
122
  'help.command.clean.description': 'Delete GitNexus index for current repo',
121
123
  'help.command.remove.description': 'Delete the GitNexus index for a registered repo (by alias, name, or absolute path). Unlike `clean`, does not require being inside the repo. Idempotent on unknown targets.',
122
124
  'help.command.wiki.description': 'Generate repository wiki from knowledge graph',
@@ -220,6 +222,8 @@ export const en = {
220
222
  'help.option.check.cycles': 'Detect circular imports and fail when any are found',
221
223
  'help.option.evalServer.host': 'Bind address (default: 127.0.0.1, use 0.0.0.0 to expose to all interfaces)',
222
224
  'help.option.evalServer.idleTimeout': 'Auto-shutdown after N seconds idle (0 = disabled)',
225
+ 'help.option.embeddings.install.cuda': "Also download the CUDA GPU binaries (runs onnxruntime-node's NuGet postinstall; set GLOBAL_AGENT_HTTPS_PROXY behind a proxy)",
226
+ 'help.option.embeddings.install.force': 'Install into the runtime prefix even when the stack already resolves',
223
227
  'help.option.group.create.force': 'Overwrite existing group',
224
228
  'help.option.group.sync.skipEmbeddings': 'Exact + BM25 only (no embedding fallback)',
225
229
  'help.option.group.sync.exactOnly': 'Exact match only',
@@ -109,7 +109,7 @@ export declare const cliResources: {
109
109
  readonly 'help.command.help.description': "display help for command";
110
110
  readonly 'help.option.help': "display help for command";
111
111
  readonly 'help.option.version': "output the version number";
112
- readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex";
112
+ readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex";
113
113
  readonly 'help.command.uninstall.description': "Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors";
114
114
  readonly 'help.command.analyze.description': "Index a repository (full analysis)";
115
115
  readonly 'help.command.index.description': "Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)";
@@ -118,6 +118,8 @@ export declare const cliResources: {
118
118
  readonly 'help.command.list.description': "List all indexed repositories";
119
119
  readonly 'help.command.status.description': "Show index status for current repo";
120
120
  readonly 'help.command.doctor.description': "Show runtime platform capabilities and embedding configuration";
121
+ readonly 'help.command.embeddings.description': "Manage the on-demand local embedding runtime";
122
+ readonly 'help.command.embeddings.install.description': "Install the local embedding stack (@huggingface/transformers + onnxruntime-node) on demand. Heals installs where npm skipped the optional packages (e.g. behind an HTTP proxy, #2370). Downloads only from your configured npm registry — mirrors and proxies apply.";
121
123
  readonly 'help.command.clean.description': "Delete GitNexus index for current repo";
122
124
  readonly 'help.command.remove.description': "Delete the GitNexus index for a registered repo (by alias, name, or absolute path). Unlike `clean`, does not require being inside the repo. Idempotent on unknown targets.";
123
125
  readonly 'help.command.wiki.description': "Generate repository wiki from knowledge graph";
@@ -221,6 +223,8 @@ export declare const cliResources: {
221
223
  readonly 'help.option.check.cycles': "Detect circular imports and fail when any are found";
222
224
  readonly 'help.option.evalServer.host': "Bind address (default: 127.0.0.1, use 0.0.0.0 to expose to all interfaces)";
223
225
  readonly 'help.option.evalServer.idleTimeout': "Auto-shutdown after N seconds idle (0 = disabled)";
226
+ readonly 'help.option.embeddings.install.cuda': "Also download the CUDA GPU binaries (runs onnxruntime-node's NuGet postinstall; set GLOBAL_AGENT_HTTPS_PROXY behind a proxy)";
227
+ readonly 'help.option.embeddings.install.force': "Install into the runtime prefix even when the stack already resolves";
224
228
  readonly 'help.option.group.create.force': "Overwrite existing group";
225
229
  readonly 'help.option.group.sync.skipEmbeddings': "Exact + BM25 only (no embedding fallback)";
226
230
  readonly 'help.option.group.sync.exactOnly': "Exact match only";
@@ -360,6 +364,8 @@ export declare const cliResources: {
360
364
  'help.command.list.description': string;
361
365
  'help.command.status.description': string;
362
366
  'help.command.doctor.description': string;
367
+ 'help.command.embeddings.description': string;
368
+ 'help.command.embeddings.install.description': string;
363
369
  'help.command.clean.description': string;
364
370
  'help.command.remove.description': string;
365
371
  'help.command.wiki.description': string;
@@ -463,6 +469,8 @@ export declare const cliResources: {
463
469
  'help.option.check.cycles': string;
464
470
  'help.option.evalServer.host': string;
465
471
  'help.option.evalServer.idleTimeout': string;
472
+ 'help.option.embeddings.install.cuda': string;
473
+ 'help.option.embeddings.install.force': string;
466
474
  'help.option.group.create.force': string;
467
475
  'help.option.group.sync.skipEmbeddings': string;
468
476
  'help.option.group.sync.exactOnly': string;
@@ -117,6 +117,8 @@ export declare const zhCN: {
117
117
  'help.command.list.description': string;
118
118
  'help.command.status.description': string;
119
119
  'help.command.doctor.description': string;
120
+ 'help.command.embeddings.description': string;
121
+ 'help.command.embeddings.install.description': string;
120
122
  'help.command.clean.description': string;
121
123
  'help.command.remove.description': string;
122
124
  'help.command.wiki.description': string;
@@ -220,6 +222,8 @@ export declare const zhCN: {
220
222
  'help.option.check.cycles': string;
221
223
  'help.option.evalServer.host': string;
222
224
  'help.option.evalServer.idleTimeout': string;
225
+ 'help.option.embeddings.install.cuda': string;
226
+ 'help.option.embeddings.install.force': string;
223
227
  'help.option.group.create.force': string;
224
228
  'help.option.group.sync.skipEmbeddings': string;
225
229
  'help.option.group.sync.exactOnly': string;
@@ -108,7 +108,7 @@ export const zhCN = {
108
108
  'help.command.help.description': '显示命令帮助',
109
109
  'help.option.help': '显示命令帮助',
110
110
  'help.option.version': '输出版本号',
111
- 'help.command.setup.description': '一次性设置:为 Cursor、Claude Code、OpenCode、Codex 配置 MCP',
111
+ 'help.command.setup.description': '一次性设置:为 Cursor、Claude Code、Antigravity、OpenCode、CodeBuddy、Qoder、Codex 配置 MCP',
112
112
  'help.command.uninstall.description': '撤销 `setup`:从所有检测到的编辑器中移除 GitNexus 的 MCP 配置、技能和钩子',
113
113
  'help.command.analyze.description': '索引仓库(完整分析)',
114
114
  'help.command.index.description': '将现有 .gitnexus/ 文件夹注册到全局注册表(无需重新分析)',
@@ -117,6 +117,8 @@ export const zhCN = {
117
117
  'help.command.list.description': '列出所有已索引仓库',
118
118
  'help.command.status.description': '显示当前仓库的索引状态',
119
119
  'help.command.doctor.description': '显示运行平台能力和嵌入配置',
120
+ 'help.command.embeddings.description': '管理按需安装的本地嵌入运行时',
121
+ 'help.command.embeddings.install.description': '按需安装本地嵌入组件(@huggingface/transformers + onnxruntime-node)。修复 npm 跳过可选包的安装(例如在 HTTP 代理后,#2370)。仅从你配置的 npm registry 下载 — 镜像和代理均生效。',
120
122
  'help.command.clean.description': '删除当前仓库的 GitNexus 索引',
121
123
  'help.command.remove.description': '删除已注册仓库的 GitNexus 索引(按别名、名称或绝对路径)。与 `clean` 不同,不要求位于仓库内;未知目标会幂等处理。',
122
124
  'help.command.wiki.description': '从知识图谱生成仓库 Wiki',
@@ -220,6 +222,8 @@ export const zhCN = {
220
222
  'help.option.check.cycles': '检测循环导入,并在发现循环时失败',
221
223
  'help.option.evalServer.host': '绑定地址(默认:127.0.0.1;用 0.0.0.0 暴露到所有网卡)',
222
224
  'help.option.evalServer.idleTimeout': '空闲 N 秒后自动关闭(0 = 禁用)',
225
+ 'help.option.embeddings.install.cuda': '同时下载 CUDA GPU 二进制文件(运行 onnxruntime-node 的 NuGet postinstall;代理后请设置 GLOBAL_AGENT_HTTPS_PROXY)',
226
+ 'help.option.embeddings.install.force': '即使嵌入组件已可解析,也强制安装到运行时目录',
223
227
  'help.option.group.create.force': '覆盖现有仓库组',
224
228
  'help.option.group.sync.skipEmbeddings': '仅使用 exact + BM25(不使用嵌入回退)',
225
229
  'help.option.group.sync.exactOnly': '仅精确匹配',
package/dist/cli/index.js CHANGED
@@ -17,7 +17,7 @@ function collectCodingAgents(value, previous) {
17
17
  program.name('gitnexus').description('GitNexus local CLI and MCP server').version(pkg.version);
18
18
  program
19
19
  .command('setup')
20
- .description('One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, Codex')
20
+ .description('One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex')
21
21
  .option('-c, --coding-agent <agents>', 'Configure only these coding agents (comma-separated or repeatable)', collectCodingAgents)
22
22
  .action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
23
23
  program
@@ -153,6 +153,17 @@ program
153
153
  .command('doctor')
154
154
  .description('Show runtime platform capabilities and embedding configuration')
155
155
  .action(createLazyAction(() => import('./doctor.js'), 'doctorCommand'));
156
+ program
157
+ .command('embeddings')
158
+ .description('Manage the on-demand local embedding runtime')
159
+ .command('install')
160
+ .description('Install the local embedding stack (@huggingface/transformers + onnxruntime-node) on demand. ' +
161
+ 'Heals installs where npm skipped the optional packages (e.g. behind an HTTP proxy, #2370). ' +
162
+ 'Downloads only from your configured npm registry — mirrors and proxies apply.')
163
+ .option('--cuda', "Also download the CUDA GPU binaries (runs onnxruntime-node's NuGet postinstall; " +
164
+ 'set GLOBAL_AGENT_HTTPS_PROXY behind a proxy)')
165
+ .option('--force', 'Install into the runtime prefix even when the stack already resolves')
166
+ .action(createLazyAction(() => import('./embeddings.js'), 'embeddingsInstallCommand'));
156
167
  program
157
168
  .command('clean')
158
169
  .description('Delete GitNexus index for current repo')
package/dist/cli/setup.js CHANGED
@@ -14,7 +14,7 @@ import { promisify } from 'util';
14
14
  import { fileURLToPath } from 'url';
15
15
  import { parseTree, modify, applyEdits, parse as parseJsonc } from 'jsonc-parser';
16
16
  import { getGlobalDir } from '../storage/repo-manager.js';
17
- import { getEditorTargets, mcpTarget, skillTarget, hookTarget, detectIndentation, } from './editor-targets.js';
17
+ import { getEditorTargets, mcpTarget, skillTarget, hookTarget, detectIndentation, isEnoent, } from './editor-targets.js';
18
18
  const __filename = fileURLToPath(import.meta.url);
19
19
  const __dirname = path.dirname(__filename);
20
20
  const execFileAsync = promisify(execFile);
@@ -66,6 +66,8 @@ const CODING_AGENT_IDS = {
66
66
  claude: 'claude',
67
67
  antigravity: 'antigravity',
68
68
  opencode: 'opencode',
69
+ codebuddy: 'codebuddy',
70
+ qoder: 'qoder',
69
71
  codex: 'codex',
70
72
  };
71
73
  const SUPPORTED_CODING_AGENTS = Object.values(CODING_AGENT_IDS);
@@ -173,7 +175,13 @@ async function mergeJsoncFile(filePath, keyPath, value) {
173
175
  try {
174
176
  raw = await fs.readFile(filePath, 'utf-8');
175
177
  }
176
- catch {
178
+ catch (err) {
179
+ // Only an absent file means "start fresh". Any other read failure (EACCES,
180
+ // EIO, cloud-placeholder faults) must not be treated as empty — the write
181
+ // below would replace the user's existing config with a gitnexus-only
182
+ // document and report success. Rethrow into the per-editor catch instead.
183
+ if (!isEnoent(err))
184
+ throw err;
177
185
  raw = '';
178
186
  }
179
187
  if (raw.trim().length === 0) {
@@ -207,6 +215,35 @@ async function dirExists(dirPath) {
207
215
  return false;
208
216
  }
209
217
  }
218
+ /**
219
+ * Detection probe: is there a non-empty regular file at this path?
220
+ * Swallows ALL errors (like dirExists) — detection gates run outside the
221
+ * per-editor try blocks, so a rethrowing probe would abort setup for every
222
+ * remaining editor. Size > 0 keeps detection aligned with the config-chain
223
+ * resolver: an empty config file is not evidence of an install, and treating
224
+ * it as one would route the write to a fresh file whose mkdir manufactures
225
+ * the editor's directory.
226
+ */
227
+ async function isNonEmptyFile(filePath) {
228
+ try {
229
+ const stat = await fs.stat(filePath);
230
+ return stat.isFile() && stat.size > 0;
231
+ }
232
+ catch {
233
+ return false;
234
+ }
235
+ }
236
+ /**
237
+ * Detection probe: does any file in the target's MCP config chain look like an
238
+ * install trace? Always walks [file, ...legacyFiles] so an editor gaining
239
+ * legacyFiles later is automatically covered (CodeBuddy and Qoder share this —
240
+ * a per-editor copy is how the root-config-only detection gap crept in, see
241
+ * PR #2368 review I4).
242
+ */
243
+ async function anyChainConfigFile(target) {
244
+ const hits = await Promise.all([target.file, ...(target.legacyFiles ?? [])].map(isNonEmptyFile));
245
+ return hits.includes(true);
246
+ }
210
247
  // ─── Editor-specific setup ─────────────────────────────────────────
211
248
  async function setupCursor(result) {
212
249
  const cursorDir = path.join(os.homedir(), '.cursor');
@@ -287,7 +324,12 @@ async function mergeHooksJsonc(filePath, entries) {
287
324
  try {
288
325
  raw = await fs.readFile(filePath, 'utf-8');
289
326
  }
290
- catch {
327
+ catch (err) {
328
+ // Same contract as mergeJsoncFile: an unreadable (non-ENOENT) settings
329
+ // file must not be rewritten as hooks-only — that would destroy every
330
+ // user setting in it. Rethrow into the hook installer's catch.
331
+ if (!isEnoent(err))
332
+ throw err;
291
333
  raw = '';
292
334
  }
293
335
  if (raw.trim().length === 0) {
@@ -365,20 +407,26 @@ export async function copyHookHelpers(srcDir, destDir, label, result) {
365
407
  return failedRequired;
366
408
  }
367
409
  /**
368
- * Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
369
- * Merges hook config without overwriting existing hooks, preserving
370
- * comments and formatting in the JSONC file.
410
+ * Install GitNexus hooks for editors that use Claude Code's hooks schema.
411
+ *
412
+ * Claude Code registers hooks in ~/.claude/settings.json; Codex uses a
413
+ * dedicated ~/.codex/hooks.json with the identical {hooks: {Event: [...]}}
414
+ * JSON shape, stdin payload, and hookSpecificOutput response contract
415
+ * (https://developers.openai.com/codex/hooks), so both runtimes share this
416
+ * installer and the same bundled adapter script. Merges hook config without
417
+ * overwriting existing hooks, preserving comments and formatting.
371
418
  */
372
- async function installClaudeCodeHooks(result) {
373
- const claudeDir = path.join(os.homedir(), '.claude');
374
- if (!(await dirExists(claudeDir)))
419
+ async function installClaudeSchemaHooks(result, id) {
420
+ const hookCfg = hookTarget(id);
421
+ const settingsPath = hookCfg.settingsFile;
422
+ const label = `${hookCfg.label} hooks`;
423
+ // Gate on the editor's own config dir (~/.claude, ~/.codex) existing.
424
+ if (!(await dirExists(path.dirname(settingsPath))))
375
425
  return;
376
- const claudeHook = hookTarget('claude');
377
- const settingsPath = claudeHook.settingsFile;
378
426
  // Source hooks bundled within the gitnexus package (hooks/claude/)
379
427
  const pluginHooksPath = path.join(__dirname, '..', '..', 'hooks', 'claude');
380
- // Copy unified hook script to ~/.claude/hooks/gitnexus/
381
- const destHooksDir = claudeHook.scriptDir;
428
+ // Copy unified hook script to the editor's hooks/gitnexus/ dir
429
+ const destHooksDir = hookCfg.scriptDir;
382
430
  try {
383
431
  await fs.mkdir(destHooksDir, { recursive: true });
384
432
  const src = path.join(pluginHooksPath, 'gitnexus-hook.cjs');
@@ -389,7 +437,7 @@ async function installClaudeCodeHooks(result) {
389
437
  const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
390
438
  const jsonCli = JSON.stringify(normalizedCli);
391
439
  if (!content.includes(CLI_PATH_SOURCE_LITERAL)) {
392
- result.errors.push('Claude Code hooks: gitnexus-hook.cjs no longer contains the cliPath literal to patch — the installed hook may fail to resolve the CLI. Update CLI_PATH_SOURCE_LITERAL in setup.ts.');
440
+ result.errors.push(`${label}: gitnexus-hook.cjs no longer contains the cliPath literal to patch — the installed hook may fail to resolve the CLI. Update CLI_PATH_SOURCE_LITERAL in setup.ts.`);
393
441
  }
394
442
  content = content.replace(CLI_PATH_SOURCE_LITERAL, `let cliPath = ${jsonCli};`);
395
443
  await fs.writeFile(dest, content, 'utf-8');
@@ -404,12 +452,12 @@ async function installClaudeCodeHooks(result) {
404
452
  await fs.access(dest);
405
453
  }
406
454
  catch {
407
- result.errors.push('Claude Code hooks: adapter script was not installed — skipping hook registration');
455
+ result.errors.push(`${label}: adapter script was not installed — skipping hook registration`);
408
456
  return;
409
457
  }
410
- const failedRequired = await copyHookHelpers(pluginHooksPath, destHooksDir, 'Claude Code hooks', result);
458
+ const failedRequired = await copyHookHelpers(pluginHooksPath, destHooksDir, label, result);
411
459
  if (failedRequired.length > 0) {
412
- result.errors.push(`Claude Code hooks: required helper(s) ${failedRequired.join(', ')} failed to copy — skipping hook registration`);
460
+ result.errors.push(`${label}: required helper(s) ${failedRequired.join(', ')} failed to copy — skipping hook registration`);
413
461
  return;
414
462
  }
415
463
  const hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/');
@@ -425,9 +473,10 @@ async function installClaudeCodeHooks(result) {
425
473
  }
426
474
  })();
427
475
  const hookEntries = [];
428
- // NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576).
429
- // Session context is delivered via CLAUDE.md / skills instead.
430
- if (!hasGitnexusHook(parsed?.hooks, 'PreToolUse', claudeHook.needle)) {
476
+ // NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576),
477
+ // and Codex reads AGENTS.md natively. Session context is delivered via
478
+ // CLAUDE.md / AGENTS.md / skills instead.
479
+ if (!hasGitnexusHook(parsed?.hooks, 'PreToolUse', hookCfg.needle)) {
431
480
  hookEntries.push({
432
481
  eventName: 'PreToolUse',
433
482
  value: {
@@ -443,7 +492,7 @@ async function installClaudeCodeHooks(result) {
443
492
  },
444
493
  });
445
494
  }
446
- if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse', claudeHook.needle)) {
495
+ if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse', hookCfg.needle)) {
447
496
  hookEntries.push({
448
497
  eventName: 'PostToolUse',
449
498
  value: {
@@ -460,19 +509,19 @@ async function installClaudeCodeHooks(result) {
460
509
  });
461
510
  }
462
511
  if (hookEntries.length === 0) {
463
- result.configured.push('Claude Code hooks (already configured)');
512
+ result.configured.push(`${label} (already configured)`);
464
513
  return;
465
514
  }
466
515
  const ok = await mergeHooksJsonc(settingsPath, hookEntries);
467
516
  if (ok) {
468
- result.configured.push('Claude Code hooks (PreToolUse, PostToolUse)');
517
+ result.configured.push(`${label} (PreToolUse, PostToolUse)`);
469
518
  }
470
519
  else {
471
- result.errors.push('Claude Code hooks: settings.json is corrupt — skipping to preserve existing content');
520
+ result.errors.push(`${label}: ${path.basename(settingsPath)} is corrupt — skipping to preserve existing content`);
472
521
  }
473
522
  }
474
523
  catch (err) {
475
- result.errors.push(`Claude Code hooks: ${err.message}`);
524
+ result.errors.push(`${label}: ${err.message}`);
476
525
  }
477
526
  }
478
527
  // ─── Antigravity (Google) ──────────────────────────────────────────
@@ -660,6 +709,118 @@ async function setupOpenCode(result) {
660
709
  result.errors.push(`OpenCode: ${err.message}`);
661
710
  }
662
711
  }
712
+ /**
713
+ * Resolve which config file in a target's [file, ...legacyFiles] priority
714
+ * chain setup should write into: the first that exists, else the recommended
715
+ * `file`. CodeBuddy reads only the first existing file in its chain, so
716
+ * creating the recommended file above a populated deprecated one would shadow
717
+ * the user's existing MCP servers.
718
+ */
719
+ async function resolveMcpConfigFile(target) {
720
+ for (const candidate of [target.file, ...(target.legacyFiles ?? [])]) {
721
+ try {
722
+ const stat = await fs.stat(candidate);
723
+ // Non-empty regular files only: a 0-byte recommended file must not
724
+ // shadow a populated deprecated one (mergeJsoncFile treats empty as a
725
+ // fresh document anyway), and directories are never config candidates.
726
+ if (stat.isFile() && stat.size > 0)
727
+ return candidate;
728
+ }
729
+ catch (err) {
730
+ // ENOENT = candidate absent — try the next one. Anything else (EACCES
731
+ // on the file or a parent) is surfaced: silently skipping could route
732
+ // the write to a lower-priority file the editor never reads.
733
+ if (!isEnoent(err))
734
+ throw err;
735
+ }
736
+ }
737
+ return target.file;
738
+ }
739
+ async function setupCodeBuddy(result) {
740
+ const codebuddyDir = path.join(os.homedir(), '.codebuddy');
741
+ const target = mcpTarget('codebuddy');
742
+ // Installed = the config dir exists OR any registered MCP config file does.
743
+ // A user whose only trace is a root-level config (e.g. a legacy
744
+ // ~/.codebuddy.json) still gets configured — uninstall already handles that
745
+ // shape, so setup skipping it was an asymmetry (PR #2368 review I4).
746
+ if (!(await dirExists(codebuddyDir)) && !(await anyChainConfigFile(target))) {
747
+ result.skipped.push('CodeBuddy (not installed)');
748
+ return;
749
+ }
750
+ try {
751
+ const configFile = await resolveMcpConfigFile(target);
752
+ const ok = await mergeJsoncFile(configFile, target.keyPath, getMcpEntry());
753
+ if (ok) {
754
+ result.configured.push('CodeBuddy');
755
+ }
756
+ else {
757
+ result.errors.push(`CodeBuddy: ${path.basename(configFile)} is corrupt — skipping to preserve existing content`);
758
+ }
759
+ }
760
+ catch (err) {
761
+ result.errors.push(`CodeBuddy: ${err instanceof Error ? err.message : String(err)}`);
762
+ }
763
+ }
764
+ async function setupQoder(result) {
765
+ const qoderDir = path.join(os.homedir(), '.qoder');
766
+ const target = mcpTarget('qoder');
767
+ const { file: mcpPath, keyPath } = target;
768
+ // Same chain-aware detection as CodeBuddy: ~/.qoder.json alone counts.
769
+ if (!(await dirExists(qoderDir)) && !(await anyChainConfigFile(target))) {
770
+ result.skipped.push('Qoder (not installed)');
771
+ return;
772
+ }
773
+ try {
774
+ const ok = await mergeJsoncFile(mcpPath, keyPath, getMcpEntry());
775
+ if (ok) {
776
+ result.configured.push('Qoder');
777
+ }
778
+ else {
779
+ result.errors.push('Qoder: .qoder.json is corrupt — skipping to preserve existing content');
780
+ }
781
+ }
782
+ catch (err) {
783
+ result.errors.push(`Qoder: ${err instanceof Error ? err.message : String(err)}`);
784
+ }
785
+ }
786
+ /**
787
+ * Install global CodeBuddy skills to ~/.codebuddy/skills/
788
+ * (https://www.codebuddy.ai/docs/cli/skills — same SKILL.md layout as Claude Code).
789
+ */
790
+ async function installCodeBuddySkills(result) {
791
+ const codebuddyDir = path.join(os.homedir(), '.codebuddy');
792
+ if (!(await dirExists(codebuddyDir)))
793
+ return;
794
+ const skillsDir = skillTarget('codebuddy').dir;
795
+ try {
796
+ const installed = await installSkillsTo(skillsDir);
797
+ if (installed.length > 0) {
798
+ result.configured.push(`CodeBuddy skills (${installed.length} skills → ~/.codebuddy/skills/)`);
799
+ }
800
+ }
801
+ catch (err) {
802
+ result.errors.push(`CodeBuddy skills: ${err instanceof Error ? err.message : String(err)}`);
803
+ }
804
+ }
805
+ /**
806
+ * Install global Qoder skills to ~/.qoder/skills/
807
+ * (https://docs.qoder.com/extensions/skills — same SKILL.md layout as Claude Code).
808
+ */
809
+ async function installQoderSkills(result) {
810
+ const qoderDir = path.join(os.homedir(), '.qoder');
811
+ if (!(await dirExists(qoderDir)))
812
+ return;
813
+ const skillsDir = skillTarget('qoder').dir;
814
+ try {
815
+ const installed = await installSkillsTo(skillsDir);
816
+ if (installed.length > 0) {
817
+ result.configured.push(`Qoder skills (${installed.length} skills → ~/.qoder/skills/)`);
818
+ }
819
+ }
820
+ catch (err) {
821
+ result.errors.push(`Qoder skills: ${err instanceof Error ? err.message : String(err)}`);
822
+ }
823
+ }
663
824
  /**
664
825
  * Build a TOML section for Codex MCP config (~/.codex/config.toml).
665
826
  */
@@ -677,7 +838,12 @@ async function upsertCodexConfigToml(configPath) {
677
838
  try {
678
839
  existing = await fs.readFile(configPath, 'utf-8');
679
840
  }
680
- catch {
841
+ catch (err) {
842
+ // TOML variant of the mergeJsoncFile contract: treating a non-ENOENT read
843
+ // failure as an empty config would rewrite config.toml with only the
844
+ // gitnexus section. Rethrow into setupCodex's catch.
845
+ if (!isEnoent(err))
846
+ throw err;
681
847
  existing = '';
682
848
  }
683
849
  if (existing.includes(`[${getEditorTargets().codex.tomlSection}]`)) {
@@ -885,12 +1051,16 @@ export const setupCommand = async (options) => {
885
1051
  await setupAntigravity(result);
886
1052
  if (selected.has('opencode'))
887
1053
  await setupOpenCode(result);
1054
+ if (selected.has('codebuddy'))
1055
+ await setupCodeBuddy(result);
1056
+ if (selected.has('qoder'))
1057
+ await setupQoder(result);
888
1058
  if (selected.has('codex'))
889
1059
  await setupCodex(result);
890
1060
  // Install global skills for platforms that support them
891
1061
  if (selected.has('claude')) {
892
1062
  await installClaudeCodeSkills(result);
893
- await installClaudeCodeHooks(result);
1063
+ await installClaudeSchemaHooks(result, 'claude');
894
1064
  }
895
1065
  if (selected.has('antigravity')) {
896
1066
  await installAntigravitySkills(result);
@@ -900,8 +1070,14 @@ export const setupCommand = async (options) => {
900
1070
  await installCursorSkills(result);
901
1071
  if (selected.has('opencode'))
902
1072
  await installOpenCodeSkills(result);
903
- if (selected.has('codex'))
1073
+ if (selected.has('codebuddy'))
1074
+ await installCodeBuddySkills(result);
1075
+ if (selected.has('qoder'))
1076
+ await installQoderSkills(result);
1077
+ if (selected.has('codex')) {
904
1078
  await installCodexSkills(result);
1079
+ await installClaudeSchemaHooks(result, 'codex');
1080
+ }
905
1081
  // Print results
906
1082
  if (result.configured.length > 0) {
907
1083
  console.log(' Configured:');