gitnexus 1.4.7 → 1.4.8

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 (92) hide show
  1. package/README.md +22 -1
  2. package/dist/cli/ai-context.d.ts +1 -1
  3. package/dist/cli/ai-context.js +1 -1
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +54 -21
  6. package/dist/cli/index.js +2 -1
  7. package/dist/cli/setup.js +78 -1
  8. package/dist/config/supported-languages.d.ts +30 -0
  9. package/dist/config/supported-languages.js +30 -0
  10. package/dist/core/embeddings/embedder.d.ts +6 -1
  11. package/dist/core/embeddings/embedder.js +65 -5
  12. package/dist/core/embeddings/embedding-pipeline.js +11 -9
  13. package/dist/core/embeddings/http-client.d.ts +31 -0
  14. package/dist/core/embeddings/http-client.js +179 -0
  15. package/dist/core/embeddings/index.d.ts +1 -0
  16. package/dist/core/embeddings/index.js +1 -0
  17. package/dist/core/embeddings/types.d.ts +1 -1
  18. package/dist/core/graph/types.d.ts +2 -1
  19. package/dist/core/ingestion/ast-helpers.d.ts +80 -0
  20. package/dist/core/ingestion/ast-helpers.js +738 -0
  21. package/dist/core/ingestion/call-analysis.d.ts +73 -0
  22. package/dist/core/ingestion/call-analysis.js +490 -0
  23. package/dist/core/ingestion/call-processor.d.ts +48 -1
  24. package/dist/core/ingestion/call-processor.js +368 -7
  25. package/dist/core/ingestion/call-routing.d.ts +6 -0
  26. package/dist/core/ingestion/entry-point-scoring.js +36 -26
  27. package/dist/core/ingestion/framework-detection.d.ts +10 -2
  28. package/dist/core/ingestion/framework-detection.js +49 -12
  29. package/dist/core/ingestion/heritage-processor.js +47 -49
  30. package/dist/core/ingestion/import-processor.d.ts +1 -1
  31. package/dist/core/ingestion/import-processor.js +103 -194
  32. package/dist/core/ingestion/import-resolution.d.ts +101 -0
  33. package/dist/core/ingestion/import-resolution.js +251 -0
  34. package/dist/core/ingestion/language-config.d.ts +3 -0
  35. package/dist/core/ingestion/language-config.js +13 -0
  36. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  37. package/dist/core/ingestion/markdown-processor.js +124 -0
  38. package/dist/core/ingestion/mro-processor.js +8 -3
  39. package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
  40. package/dist/core/ingestion/named-binding-extraction.js +89 -79
  41. package/dist/core/ingestion/parsing-processor.d.ts +2 -2
  42. package/dist/core/ingestion/parsing-processor.js +14 -73
  43. package/dist/core/ingestion/pipeline.d.ts +10 -0
  44. package/dist/core/ingestion/pipeline.js +421 -4
  45. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  46. package/dist/core/ingestion/resolution-context.js +7 -4
  47. package/dist/core/ingestion/resolvers/index.d.ts +1 -1
  48. package/dist/core/ingestion/resolvers/index.js +1 -1
  49. package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
  50. package/dist/core/ingestion/resolvers/jvm.js +25 -9
  51. package/dist/core/ingestion/resolvers/php.d.ts +14 -0
  52. package/dist/core/ingestion/resolvers/php.js +43 -3
  53. package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
  54. package/dist/core/ingestion/resolvers/utils.js +16 -0
  55. package/dist/core/ingestion/symbol-table.d.ts +16 -0
  56. package/dist/core/ingestion/symbol-table.js +20 -6
  57. package/dist/core/ingestion/tree-sitter-queries.d.ts +4 -4
  58. package/dist/core/ingestion/tree-sitter-queries.js +43 -2
  59. package/dist/core/ingestion/type-env.d.ts +28 -1
  60. package/dist/core/ingestion/type-env.js +419 -96
  61. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  62. package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
  63. package/dist/core/ingestion/type-extractors/csharp.js +149 -16
  64. package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
  65. package/dist/core/ingestion/type-extractors/index.js +1 -1
  66. package/dist/core/ingestion/type-extractors/jvm.js +169 -66
  67. package/dist/core/ingestion/type-extractors/rust.js +35 -1
  68. package/dist/core/ingestion/type-extractors/shared.d.ts +0 -2
  69. package/dist/core/ingestion/type-extractors/shared.js +5 -10
  70. package/dist/core/ingestion/type-extractors/swift.js +7 -6
  71. package/dist/core/ingestion/type-extractors/types.d.ts +37 -7
  72. package/dist/core/ingestion/type-extractors/typescript.js +141 -9
  73. package/dist/core/ingestion/utils.d.ts +2 -120
  74. package/dist/core/ingestion/utils.js +3 -1051
  75. package/dist/core/ingestion/workers/parse-worker.d.ts +13 -4
  76. package/dist/core/ingestion/workers/parse-worker.js +66 -87
  77. package/dist/core/lbug/csv-generator.js +18 -1
  78. package/dist/core/lbug/lbug-adapter.d.ts +10 -0
  79. package/dist/core/lbug/lbug-adapter.js +69 -4
  80. package/dist/core/lbug/schema.d.ts +5 -3
  81. package/dist/core/lbug/schema.js +26 -2
  82. package/dist/mcp/core/embedder.js +11 -3
  83. package/dist/mcp/core/lbug-adapter.js +12 -1
  84. package/dist/mcp/local/local-backend.d.ts +22 -0
  85. package/dist/mcp/local/local-backend.js +133 -29
  86. package/dist/mcp/resources.js +2 -0
  87. package/dist/mcp/tools.js +2 -2
  88. package/dist/server/api.d.ts +19 -1
  89. package/dist/server/api.js +66 -6
  90. package/dist/storage/git.d.ts +12 -0
  91. package/dist/storage/git.js +21 -0
  92. package/package.json +10 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Graph-powered code intelligence for AI agents.** Index any codebase into a knowledge graph, then query it via MCP or CLI.
4
4
 
5
- Works with **Cursor**, **Claude Code**, **Windsurf**, **Cline**, **OpenCode**, and any MCP-compatible tool.
5
+ Works with **Cursor**, **Claude Code**, **Codex**, **Windsurf**, **Cline**, **OpenCode**, and any MCP-compatible tool.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/gitnexus.svg)](https://www.npmjs.com/package/gitnexus)
8
8
  [![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm%20Noncommercial-blue.svg)](https://polyformproject.org/licenses/noncommercial/1.0.0/)
@@ -34,6 +34,7 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up
34
34
  |--------|-----|--------|---------------------|---------|
35
35
  | **Claude Code** | Yes | Yes | Yes (PreToolUse) | **Full** |
36
36
  | **Cursor** | Yes | Yes | — | MCP + Skills |
37
+ | **Codex** | Yes | Yes | — | MCP + Skills |
37
38
  | **Windsurf** | Yes | — | — | MCP |
38
39
  | **OpenCode** | Yes | Yes | — | MCP + Skills |
39
40
 
@@ -55,6 +56,12 @@ If you prefer to configure manually instead of using `gitnexus setup`:
55
56
  claude mcp add gitnexus -- npx -y gitnexus@latest mcp
56
57
  ```
57
58
 
59
+ ### Codex (full support — MCP + skills)
60
+
61
+ ```bash
62
+ codex mcp add gitnexus -- npx -y gitnexus@latest mcp
63
+ ```
64
+
58
65
  ### Cursor / Windsurf
59
66
 
60
67
  Add to `~/.cursor/mcp.json` (global — works for all projects):
@@ -151,6 +158,20 @@ gitnexus wiki [path] # Generate LLM-powered docs from knowledge grap
151
158
  gitnexus wiki --model <model> # Wiki with custom LLM model (default: gpt-4o-mini)
152
159
  ```
153
160
 
161
+ ## Remote Embeddings
162
+
163
+ Set these env vars to use a remote OpenAI-compatible `/v1/embeddings` endpoint instead of the local model:
164
+
165
+ ```bash
166
+ export GITNEXUS_EMBEDDING_URL=http://your-server:8080/v1
167
+ export GITNEXUS_EMBEDDING_MODEL=BAAI/bge-large-en-v1.5
168
+ export GITNEXUS_EMBEDDING_DIMS=1024 # optional, default 384
169
+ export GITNEXUS_EMBEDDING_API_KEY=your-key # optional, default: "unused"
170
+ gitnexus analyze . --embeddings
171
+ ```
172
+
173
+ Works with Infinity, vLLM, TEI, llama.cpp, Ollama, LM Studio, or OpenAI. When unset, local embeddings are used unchanged.
174
+
154
175
  ## Multi-Repo Support
155
176
 
156
177
  GitNexus supports indexing multiple repositories. Each `gitnexus analyze` registers the repo in a global registry (`~/.gitnexus/registry.json`). The MCP server serves all indexed repos automatically.
@@ -2,7 +2,7 @@
2
2
  * AI Context Generator
3
3
  *
4
4
  * Creates AGENTS.md and CLAUDE.md with full inline GitNexus context.
5
- * AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Cline, etc.
5
+ * AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, etc.
6
6
  * CLAUDE.md is for Claude Code which only reads that file.
7
7
  */
8
8
  import { type GeneratedSkillInfo } from './skill-gen.js';
@@ -2,7 +2,7 @@
2
2
  * AI Context Generator
3
3
  *
4
4
  * Creates AGENTS.md and CLAUDE.md with full inline GitNexus context.
5
- * AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Cline, etc.
5
+ * AGENTS.md is the standard read by Cursor, Windsurf, OpenCode, Codex, Cline, etc.
6
6
  * CLAUDE.md is for Claude Code which only reads that file.
7
7
  */
8
8
  import fs from 'fs/promises';
@@ -8,5 +8,7 @@ export interface AnalyzeOptions {
8
8
  embeddings?: boolean;
9
9
  skills?: boolean;
10
10
  verbose?: boolean;
11
+ /** Index the folder even when no .git directory is present. */
12
+ skipGit?: boolean;
11
13
  }
12
14
  export declare const analyzeCommand: (inputPath?: string, options?: AnalyzeOptions) => Promise<void>;
@@ -14,7 +14,7 @@ import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReuse
14
14
  // versions whose ABI is not yet supported by the native binary (#89).
15
15
  // disposeEmbedder intentionally not called — ONNX Runtime segfaults on cleanup (see #38)
16
16
  import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath, cleanupOldKuzuFiles } from '../storage/repo-manager.js';
17
- import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
17
+ import { getCurrentCommit, getGitRoot, hasGitDir } from '../storage/git.js';
18
18
  import { generateAIContextFiles } from './ai-context.js';
19
19
  import { generateSkillFiles } from './skill-gen.js';
20
20
  import fs from 'fs/promises';
@@ -70,17 +70,27 @@ export const analyzeCommand = async (inputPath, options) => {
70
70
  else {
71
71
  const gitRoot = getGitRoot(process.cwd());
72
72
  if (!gitRoot) {
73
- console.log(' Not inside a git repository\n');
74
- process.exitCode = 1;
75
- return;
73
+ if (!options?.skipGit) {
74
+ console.log(' Not inside a git repository.\n Tip: pass --skip-git to index any folder without a .git directory.\n');
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+ // --skip-git: fall back to cwd as the root
79
+ repoPath = path.resolve(process.cwd());
80
+ }
81
+ else {
82
+ repoPath = gitRoot;
76
83
  }
77
- repoPath = gitRoot;
78
84
  }
79
- if (!isGitRepo(repoPath)) {
80
- console.log(' Not a git repository\n');
85
+ const repoHasGit = hasGitDir(repoPath);
86
+ if (!repoHasGit && !options?.skipGit) {
87
+ console.log(' Not a git repository.\n Tip: pass --skip-git to index any folder without a .git directory.\n');
81
88
  process.exitCode = 1;
82
89
  return;
83
90
  }
91
+ if (!repoHasGit) {
92
+ console.log(' Warning: no .git directory found \u2014 commit-tracking and incremental updates disabled.\n');
93
+ }
84
94
  const { storagePath, lbugPath } = getStoragePaths(repoPath);
85
95
  // Clean up stale KuzuDB files from before the LadybugDB migration.
86
96
  // If kuzu existed but lbug doesn't, we're doing a migration re-index — say so.
@@ -88,11 +98,14 @@ export const analyzeCommand = async (inputPath, options) => {
88
98
  if (kuzuResult.found && kuzuResult.needsReindex) {
89
99
  console.log(' Migrating from KuzuDB to LadybugDB — rebuilding index...\n');
90
100
  }
91
- const currentCommit = getCurrentCommit(repoPath);
101
+ const currentCommit = repoHasGit ? getCurrentCommit(repoPath) : '';
92
102
  const existingMeta = await loadMeta(storagePath);
93
103
  if (existingMeta && !options?.force && !options?.skills && existingMeta.lastCommit === currentCommit) {
94
- console.log(' Already up to date\n');
95
- return;
104
+ // Non-git folders have currentCommit = '' always rebuild since we can't detect changes
105
+ if (currentCommit !== '') {
106
+ console.log(' Already up to date\n');
107
+ return;
108
+ }
96
109
  }
97
110
  if (process.env.GITNEXUS_NO_GITIGNORE) {
98
111
  console.log(' GITNEXUS_NO_GITIGNORE is set — skipping .gitignore (still reading .gitnexusignore)\n');
@@ -218,15 +231,26 @@ export const analyzeCommand = async (inputPath, options) => {
218
231
  const ftsTime = ((Date.now() - t0Fts) / 1000).toFixed(1);
219
232
  // ── Phase 3.5: Re-insert cached embeddings ────────────────────────
220
233
  if (cachedEmbeddings.length > 0) {
221
- updateBar(88, `Restoring ${cachedEmbeddings.length} cached embeddings...`);
222
- const EMBED_BATCH = 200;
223
- for (let i = 0; i < cachedEmbeddings.length; i += EMBED_BATCH) {
224
- const batch = cachedEmbeddings.slice(i, i + EMBED_BATCH);
225
- const paramsList = batch.map(e => ({ nodeId: e.nodeId, embedding: e.embedding }));
226
- try {
227
- await executeWithReusedStatement(`CREATE (e:CodeEmbedding {nodeId: $nodeId, embedding: $embedding})`, paramsList);
234
+ // Check if cached embedding dimensions match current schema
235
+ const cachedDims = cachedEmbeddings[0].embedding.length;
236
+ const { EMBEDDING_DIMS } = await import('../core/lbug/schema.js');
237
+ if (cachedDims !== EMBEDDING_DIMS) {
238
+ // Dimensions changed (e.g. switched embedding model) discard cache and re-embed all
239
+ console.error(`⚠️ Embedding dimensions changed (${cachedDims}d → ${EMBEDDING_DIMS}d), discarding cache`);
240
+ cachedEmbeddings = [];
241
+ cachedEmbeddingNodeIds = new Set();
242
+ }
243
+ else {
244
+ updateBar(88, `Restoring ${cachedEmbeddings.length} cached embeddings...`);
245
+ const EMBED_BATCH = 200;
246
+ for (let i = 0; i < cachedEmbeddings.length; i += EMBED_BATCH) {
247
+ const batch = cachedEmbeddings.slice(i, i + EMBED_BATCH);
248
+ const paramsList = batch.map(e => ({ nodeId: e.nodeId, embedding: e.embedding }));
249
+ try {
250
+ await executeWithReusedStatement(`CREATE (e:CodeEmbedding {nodeId: $nodeId, embedding: $embedding})`, paramsList);
251
+ }
252
+ catch { /* some may fail if node was removed, that's fine */ }
228
253
  }
229
- catch { /* some may fail if node was removed, that's fine */ }
230
254
  }
231
255
  }
232
256
  // ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
@@ -243,12 +267,16 @@ export const analyzeCommand = async (inputPath, options) => {
243
267
  }
244
268
  }
245
269
  if (!embeddingSkipped) {
246
- updateBar(90, 'Loading embedding model...');
270
+ const { isHttpMode } = await import('../core/embeddings/http-client.js');
271
+ const httpMode = isHttpMode();
272
+ updateBar(90, httpMode ? 'Connecting to embedding endpoint...' : 'Loading embedding model...');
247
273
  const t0Emb = Date.now();
248
274
  const { runEmbeddingPipeline } = await import('../core/embeddings/embedding-pipeline.js');
249
275
  await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (progress) => {
250
276
  const scaled = 90 + Math.round((progress.percent / 100) * 8);
251
- const label = progress.phase === 'loading-model' ? 'Loading embedding model...' : `Embedding ${progress.nodesProcessed || 0}/${progress.totalNodes || '?'}`;
277
+ const label = progress.phase === 'loading-model'
278
+ ? (httpMode ? 'Connecting to embedding endpoint...' : 'Loading embedding model...')
279
+ : `Embedding ${progress.nodesProcessed || 0}/${progress.totalNodes || '?'}`;
252
280
  updateBar(scaled, label);
253
281
  }, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined);
254
282
  embeddingTime = ((Date.now() - t0Emb) / 1000).toFixed(1);
@@ -277,7 +305,12 @@ export const analyzeCommand = async (inputPath, options) => {
277
305
  };
278
306
  await saveMeta(storagePath, meta);
279
307
  await registerRepo(repoPath, meta);
280
- await addToGitignore(repoPath);
308
+ // Only attempt to update .gitignore when a .git directory is present.
309
+ // Use hasGitDir (filesystem check) rather than git CLI subprocess
310
+ // so we skip correctly for --skip-git folders even if git CLI is available.
311
+ if (hasGitDir(repoPath)) {
312
+ await addToGitignore(repoPath);
313
+ }
281
314
  const projectName = path.basename(repoPath);
282
315
  let aggregatedClusterCount = 0;
283
316
  if (pipelineResult.communityResult?.communities) {
package/dist/cli/index.js CHANGED
@@ -13,7 +13,7 @@ program
13
13
  .version(pkg.version);
14
14
  program
15
15
  .command('setup')
16
- .description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode')
16
+ .description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex')
17
17
  .action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
18
18
  program
19
19
  .command('analyze [path]')
@@ -21,6 +21,7 @@ program
21
21
  .option('-f, --force', 'Force full re-index even if up to date')
22
22
  .option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
23
23
  .option('--skills', 'Generate repo-specific skill files from detected communities')
24
+ .option('--skip-git', 'Index a folder without requiring a .git directory')
24
25
  .option('-v, --verbose', 'Enable verbose ingestion warnings (default: false)')
25
26
  .addHelpText('after', '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)')
26
27
  .action(createLazyAction(() => import('./analyze.js'), 'analyzeCommand'));
package/dist/cli/setup.js CHANGED
@@ -8,11 +8,14 @@
8
8
  import fs from 'fs/promises';
9
9
  import path from 'path';
10
10
  import os from 'os';
11
+ import { execFile } from 'child_process';
12
+ import { promisify } from 'util';
11
13
  import { fileURLToPath } from 'url';
12
14
  import { glob } from 'glob';
13
15
  import { getGlobalDir } from '../storage/repo-manager.js';
14
16
  const __filename = fileURLToPath(import.meta.url);
15
17
  const __dirname = path.dirname(__filename);
18
+ const execFileAsync = promisify(execFile);
16
19
  /**
17
20
  * The MCP server entry for all editors.
18
21
  * On Windows, npx must be invoked via cmd /c since it's a .cmd script.
@@ -201,11 +204,65 @@ async function setupOpenCode(result) {
201
204
  result.errors.push(`OpenCode: ${err.message}`);
202
205
  }
203
206
  }
207
+ /**
208
+ * Build a TOML section for Codex MCP config (~/.codex/config.toml).
209
+ */
210
+ function getCodexMcpTomlSection() {
211
+ const entry = getMcpEntry();
212
+ const command = JSON.stringify(entry.command);
213
+ const args = `[${entry.args.map(arg => JSON.stringify(arg)).join(', ')}]`;
214
+ return `[mcp_servers.gitnexus]\ncommand = ${command}\nargs = ${args}\n`;
215
+ }
216
+ /**
217
+ * Append GitNexus MCP server config to Codex's config.toml if missing.
218
+ */
219
+ async function upsertCodexConfigToml(configPath) {
220
+ let existing = '';
221
+ try {
222
+ existing = await fs.readFile(configPath, 'utf-8');
223
+ }
224
+ catch {
225
+ existing = '';
226
+ }
227
+ if (existing.includes('[mcp_servers.gitnexus]')) {
228
+ return;
229
+ }
230
+ const section = getCodexMcpTomlSection();
231
+ const nextContent = existing.trim().length > 0
232
+ ? `${existing.trimEnd()}\n\n${section}`
233
+ : section;
234
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
235
+ await fs.writeFile(configPath, `${nextContent.trimEnd()}\n`, 'utf-8');
236
+ }
237
+ async function setupCodex(result) {
238
+ const codexDir = path.join(os.homedir(), '.codex');
239
+ if (!(await dirExists(codexDir))) {
240
+ result.skipped.push('Codex (not installed)');
241
+ return;
242
+ }
243
+ try {
244
+ const entry = getMcpEntry();
245
+ await execFileAsync('codex', ['mcp', 'add', 'gitnexus', '--', entry.command, ...entry.args], { shell: process.platform === 'win32' });
246
+ result.configured.push('Codex');
247
+ return;
248
+ }
249
+ catch {
250
+ // Fallback for environments where `codex` binary isn't on PATH.
251
+ }
252
+ try {
253
+ const configPath = path.join(codexDir, 'config.toml');
254
+ await upsertCodexConfigToml(configPath);
255
+ result.configured.push('Codex (MCP added to ~/.codex/config.toml)');
256
+ }
257
+ catch (err) {
258
+ result.errors.push(`Codex: ${err.message}`);
259
+ }
260
+ }
204
261
  // ─── Skill Installation ───────────────────────────────────────────
205
262
  /**
206
263
  * Install GitNexus skills to a target directory.
207
264
  * Each skill is installed as {targetDir}/gitnexus-{skillName}/SKILL.md
208
- * following the Agent Skills standard (both Cursor and Claude Code).
265
+ * following the Agent Skills standard (Cursor, Claude Code, and Codex).
209
266
  *
210
267
  * Supports two source layouts:
211
268
  * - Flat file: skills/{name}.md → copied as SKILL.md
@@ -310,6 +367,24 @@ async function installOpenCodeSkills(result) {
310
367
  result.errors.push(`OpenCode skills: ${err.message}`);
311
368
  }
312
369
  }
370
+ /**
371
+ * Install global Codex skills to ~/.agents/skills/gitnexus/
372
+ */
373
+ async function installCodexSkills(result) {
374
+ const codexDir = path.join(os.homedir(), '.codex');
375
+ if (!(await dirExists(codexDir)))
376
+ return;
377
+ const skillsDir = path.join(os.homedir(), '.agents', 'skills');
378
+ try {
379
+ const installed = await installSkillsTo(skillsDir);
380
+ if (installed.length > 0) {
381
+ result.configured.push(`Codex skills (${installed.length} skills → ~/.agents/skills/)`);
382
+ }
383
+ }
384
+ catch (err) {
385
+ result.errors.push(`Codex skills: ${err.message}`);
386
+ }
387
+ }
313
388
  // ─── Main command ──────────────────────────────────────────────────
314
389
  export const setupCommand = async () => {
315
390
  console.log('');
@@ -328,11 +403,13 @@ export const setupCommand = async () => {
328
403
  await setupCursor(result);
329
404
  await setupClaudeCode(result);
330
405
  await setupOpenCode(result);
406
+ await setupCodex(result);
331
407
  // Install global skills for platforms that support them
332
408
  await installClaudeCodeSkills(result);
333
409
  await installClaudeCodeHooks(result);
334
410
  await installCursorSkills(result);
335
411
  await installOpenCodeSkills(result);
412
+ await installCodexSkills(result);
336
413
  // Print results
337
414
  if (result.configured.length > 0) {
338
415
  console.log(' Configured:');
@@ -1,3 +1,33 @@
1
+ /**
2
+ * HOW TO ADD A NEW LANGUAGE:
3
+ *
4
+ * 1. Add the enum member below (e.g., Scala = 'scala')
5
+ * 2. Run `tsc --noEmit` — compiler errors guide you to every dispatch table
6
+ * 3. Use this checklist for each file:
7
+ *
8
+ * FILE | WHAT TO ADD | DEFAULT (simple languages)
9
+ * ----------------------------------|------------------------------------------|---------------------------
10
+ * tree-sitter-queries.ts | Query string + LANGUAGE_QUERIES entry | (required)
11
+ * export-detection.ts | ExportChecker function + table entry | (required)
12
+ * import-resolution.ts | Resolver in importResolvers | resolveStandard(...)
13
+ * import-resolution.ts | namedBindingExtractors entry | undefined
14
+ * call-routing.ts | callRouters entry | noRouting
15
+ * entry-point-scoring.ts | ENTRY_POINT_PATTERNS entry | []
16
+ * framework-detection.ts | AST_FRAMEWORK_PATTERNS entry | []
17
+ * type-extractors/<lang>.ts | New file + index.ts import | (required)
18
+ * resolvers/<lang>.ts | Resolver file (if non-standard) | (only if resolveStandard insufficient)
19
+ * named-binding-extraction.ts | Extractor (if named imports) | (only if language has named imports)
20
+ *
21
+ * 4. Also check these files for language-specific if-checks (no compile-time guard):
22
+ * - mro-processor.ts (MRO strategy selection)
23
+ * - heritage-processor.ts (extends/implements handling)
24
+ * - parse-worker.ts (AST edge cases)
25
+ * - parsing-processor.ts (node label normalization)
26
+ *
27
+ * 5. Add tree-sitter-<lang> to package.json dependencies
28
+ * 6. Add file extension mapping in utils.ts getLanguageFromFilename()
29
+ * 7. Run full test suite
30
+ */
1
31
  export declare enum SupportedLanguages {
2
32
  JavaScript = "javascript",
3
33
  TypeScript = "typescript",
@@ -1,3 +1,33 @@
1
+ /**
2
+ * HOW TO ADD A NEW LANGUAGE:
3
+ *
4
+ * 1. Add the enum member below (e.g., Scala = 'scala')
5
+ * 2. Run `tsc --noEmit` — compiler errors guide you to every dispatch table
6
+ * 3. Use this checklist for each file:
7
+ *
8
+ * FILE | WHAT TO ADD | DEFAULT (simple languages)
9
+ * ----------------------------------|------------------------------------------|---------------------------
10
+ * tree-sitter-queries.ts | Query string + LANGUAGE_QUERIES entry | (required)
11
+ * export-detection.ts | ExportChecker function + table entry | (required)
12
+ * import-resolution.ts | Resolver in importResolvers | resolveStandard(...)
13
+ * import-resolution.ts | namedBindingExtractors entry | undefined
14
+ * call-routing.ts | callRouters entry | noRouting
15
+ * entry-point-scoring.ts | ENTRY_POINT_PATTERNS entry | []
16
+ * framework-detection.ts | AST_FRAMEWORK_PATTERNS entry | []
17
+ * type-extractors/<lang>.ts | New file + index.ts import | (required)
18
+ * resolvers/<lang>.ts | Resolver file (if non-standard) | (only if resolveStandard insufficient)
19
+ * named-binding-extraction.ts | Extractor (if named imports) | (only if language has named imports)
20
+ *
21
+ * 4. Also check these files for language-specific if-checks (no compile-time guard):
22
+ * - mro-processor.ts (MRO strategy selection)
23
+ * - heritage-processor.ts (extends/implements handling)
24
+ * - parse-worker.ts (AST edge cases)
25
+ * - parsing-processor.ts (node label normalization)
26
+ *
27
+ * 5. Add tree-sitter-<lang> to package.json dependencies
28
+ * 6. Add file extension mapping in utils.ts getLanguageFromFilename()
29
+ * 7. Run full test suite
30
+ */
1
31
  export var SupportedLanguages;
2
32
  (function (SupportedLanguages) {
3
33
  SupportedLanguages["JavaScript"] = "javascript";
@@ -30,6 +30,11 @@ export declare const initEmbedder: (onProgress?: ModelProgressCallback, config?:
30
30
  * Check if the embedder is initialized and ready
31
31
  */
32
32
  export declare const isEmbedderReady: () => boolean;
33
+ /**
34
+ * Get the effective embedding dimensions.
35
+ * In HTTP mode, uses GITNEXUS_EMBEDDING_DIMS if set, otherwise the default.
36
+ */
37
+ export declare const getEmbeddingDimensions: () => number;
33
38
  /**
34
39
  * Get the embedder instance (throws if not initialized)
35
40
  */
@@ -38,7 +43,7 @@ export declare const getEmbedder: () => FeatureExtractionPipeline;
38
43
  * Embed a single text string
39
44
  *
40
45
  * @param text - Text to embed
41
- * @returns Float32Array of embedding vector (384 dimensions)
46
+ * @returns Float32Array of embedding vector
42
47
  */
43
48
  export declare const embedText: (text: string) => Promise<Float32Array>;
44
49
  /**
@@ -15,17 +15,53 @@ if (!process.env.ORT_LOG_LEVEL) {
15
15
  import { pipeline, env } from '@huggingface/transformers';
16
16
  import { existsSync } from 'fs';
17
17
  import { execFileSync } from 'child_process';
18
- import { join } from 'path';
18
+ import { join, dirname } from 'path';
19
+ import { createRequire } from 'module';
19
20
  import { DEFAULT_EMBEDDING_CONFIG } from './types.js';
21
+ import { isHttpMode, getHttpDimensions, httpEmbed } from './http-client.js';
22
+ /**
23
+ * Check whether the onnxruntime-node package that @huggingface/transformers
24
+ * will actually load at runtime ships the CUDA execution provider.
25
+ *
26
+ * Critical: we resolve from transformers' own module scope, NOT from ours.
27
+ * npm may install two copies — a top-level 1.24.x (our dep) and a nested
28
+ * 1.21.0 (transformers' pinned dep). The guard must inspect whichever copy
29
+ * transformers.js will dlopen, otherwise the check is meaningless.
30
+ */
31
+ function hasOrtCudaProvider() {
32
+ try {
33
+ const require = createRequire(import.meta.url);
34
+ // Resolve from @huggingface/transformers' scope so we find the same
35
+ // onnxruntime-node binary that transformers.js will use at runtime
36
+ const transformersDir = dirname(require.resolve('@huggingface/transformers/package.json'));
37
+ const ortRequire = createRequire(join(transformersDir, 'package.json'));
38
+ const ortPath = dirname(ortRequire.resolve('onnxruntime-node/package.json'));
39
+ // ORT 1.24.x only ships CUDA binaries for linux/x64 (downloaded from NuGet
40
+ // at postinstall). arm64 will correctly return false here until ORT adds support.
41
+ const arch = process.arch;
42
+ return existsSync(join(ortPath, 'bin', 'napi-v6', 'linux', arch, 'libonnxruntime_providers_cuda.so'));
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
20
48
  /**
21
49
  * Check whether CUDA libraries are actually available on this system.
22
50
  * ONNX Runtime's native layer crashes (uncatchable) if we attempt CUDA
23
51
  * without the required shared libraries, so we probe first.
24
52
  *
25
- * Checks the dynamic linker cache (ldconfig) which covers all architectures
26
- * and install paths, then falls back to CUDA_PATH / LD_LIBRARY_PATH env vars.
53
+ * Checks both:
54
+ * 1. That system CUDA libraries (libcublasLt) are present
55
+ * 2. That onnxruntime-node ships the CUDA execution provider binary
56
+ *
57
+ * Both conditions must be true — system CUDA libs alone are not enough
58
+ * if onnxruntime-node is a CPU-only build (versions < 1.24.0).
27
59
  */
28
60
  function isCudaAvailable() {
61
+ // First, verify onnxruntime-node has the CUDA provider binary.
62
+ // Without this, requesting CUDA causes an uncatchable native crash.
63
+ if (!hasOrtCudaProvider())
64
+ return false;
29
65
  // Primary: query the dynamic linker cache — covers all architectures,
30
66
  // distro layouts, and custom install paths registered with ldconfig
31
67
  try {
@@ -70,6 +106,10 @@ export const getCurrentDevice = () => currentDevice;
70
106
  * @returns Promise resolving to the embedder pipeline
71
107
  */
72
108
  export const initEmbedder = async (onProgress, config = {}, forceDevice) => {
109
+ if (isHttpMode()) {
110
+ throw new Error('initEmbedder() should not be called in HTTP mode. ' +
111
+ 'Use embedText()/embedBatch() which handle HTTP transparently.');
112
+ }
73
113
  // Return existing instance if available
74
114
  if (embedderInstance) {
75
115
  return embedderInstance;
@@ -169,12 +209,25 @@ export const initEmbedder = async (onProgress, config = {}, forceDevice) => {
169
209
  * Check if the embedder is initialized and ready
170
210
  */
171
211
  export const isEmbedderReady = () => {
172
- return embedderInstance !== null;
212
+ return isHttpMode() || embedderInstance !== null;
213
+ };
214
+ /**
215
+ * Get the effective embedding dimensions.
216
+ * In HTTP mode, uses GITNEXUS_EMBEDDING_DIMS if set, otherwise the default.
217
+ */
218
+ export const getEmbeddingDimensions = () => {
219
+ if (isHttpMode()) {
220
+ return getHttpDimensions() ?? DEFAULT_EMBEDDING_CONFIG.dimensions;
221
+ }
222
+ return DEFAULT_EMBEDDING_CONFIG.dimensions;
173
223
  };
174
224
  /**
175
225
  * Get the embedder instance (throws if not initialized)
176
226
  */
177
227
  export const getEmbedder = () => {
228
+ if (isHttpMode()) {
229
+ throw new Error('getEmbedder() is not available in HTTP embedding mode. Use embedText()/embedBatch() instead.');
230
+ }
178
231
  if (!embedderInstance) {
179
232
  throw new Error('Embedder not initialized. Call initEmbedder() first.');
180
233
  }
@@ -184,9 +237,13 @@ export const getEmbedder = () => {
184
237
  * Embed a single text string
185
238
  *
186
239
  * @param text - Text to embed
187
- * @returns Float32Array of embedding vector (384 dimensions)
240
+ * @returns Float32Array of embedding vector
188
241
  */
189
242
  export const embedText = async (text) => {
243
+ if (isHttpMode()) {
244
+ const [vec] = await httpEmbed([text]);
245
+ return vec;
246
+ }
190
247
  const embedder = getEmbedder();
191
248
  const result = await embedder(text, {
192
249
  pooling: 'mean',
@@ -206,6 +263,9 @@ export const embedBatch = async (texts) => {
206
263
  if (texts.length === 0) {
207
264
  return [];
208
265
  }
266
+ if (isHttpMode()) {
267
+ return httpEmbed(texts);
268
+ }
209
269
  const embedder = getEmbedder();
210
270
  // Process batch
211
271
  const result = await embedder(texts, {
@@ -121,14 +121,16 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
121
121
  percent: 0,
122
122
  modelDownloadPercent: 0,
123
123
  });
124
- await initEmbedder((modelProgress) => {
125
- const downloadPercent = modelProgress.progress ?? 0;
126
- onProgress({
127
- phase: 'loading-model',
128
- percent: Math.round(downloadPercent * 0.2),
129
- modelDownloadPercent: downloadPercent,
130
- });
131
- }, finalConfig);
124
+ if (!isEmbedderReady()) {
125
+ await initEmbedder((modelProgress) => {
126
+ const downloadPercent = modelProgress.progress ?? 0;
127
+ onProgress({
128
+ phase: 'loading-model',
129
+ percent: Math.round(downloadPercent * 0.2),
130
+ modelDownloadPercent: downloadPercent,
131
+ });
132
+ }, finalConfig);
133
+ }
132
134
  onProgress({
133
135
  phase: 'loading-model',
134
136
  percent: 20,
@@ -255,7 +257,7 @@ export const semanticSearch = async (executeQuery, query, k = 10, maxDistance =
255
257
  // Query the vector index on CodeEmbedding to get nodeIds and distances
256
258
  const vectorQuery = `
257
259
  CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
258
- CAST(${queryVecStr} AS FLOAT[384]), ${k})
260
+ CAST(${queryVecStr} AS FLOAT[${queryVec.length}]), ${k})
259
261
  YIELD node AS emb, distance
260
262
  WITH emb, distance
261
263
  WHERE distance < ${maxDistance}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * HTTP Embedding Client
3
+ *
4
+ * Shared fetch+retry logic for OpenAI-compatible /v1/embeddings endpoints.
5
+ * Imported by both the core embedder (batch) and MCP embedder (query).
6
+ */
7
+ /**
8
+ * Check whether HTTP embedding mode is active (env vars are set).
9
+ */
10
+ export declare const isHttpMode: () => boolean;
11
+ /**
12
+ * Return the configured embedding dimensions for HTTP mode, or undefined
13
+ * if HTTP mode is not active or no explicit dimensions are set.
14
+ */
15
+ export declare const getHttpDimensions: () => number | undefined;
16
+ /**
17
+ * Embed texts via the HTTP backend, splitting into batches.
18
+ * Reads config from env vars on every call.
19
+ *
20
+ * @param texts - Array of texts to embed
21
+ * @returns Array of Float32Array embedding vectors
22
+ */
23
+ export declare const httpEmbed: (texts: string[]) => Promise<Float32Array[]>;
24
+ /**
25
+ * Embed a single query text via the HTTP backend.
26
+ * Convenience for MCP search where only one vector is needed.
27
+ *
28
+ * @param text - Query text to embed
29
+ * @returns Embedding vector as number array
30
+ */
31
+ export declare const httpEmbedQuery: (text: string) => Promise<number[]>;