gitnexus 1.4.6 → 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.
- package/README.md +22 -1
- package/dist/cli/ai-context.d.ts +1 -1
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +54 -21
- package/dist/cli/index.js +2 -1
- package/dist/cli/setup.js +78 -1
- package/dist/config/supported-languages.d.ts +30 -0
- package/dist/config/supported-languages.js +30 -0
- package/dist/core/embeddings/embedder.d.ts +6 -1
- package/dist/core/embeddings/embedder.js +65 -5
- package/dist/core/embeddings/embedding-pipeline.js +11 -9
- package/dist/core/embeddings/http-client.d.ts +31 -0
- package/dist/core/embeddings/http-client.js +179 -0
- package/dist/core/embeddings/index.d.ts +1 -0
- package/dist/core/embeddings/index.js +1 -0
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/types.d.ts +4 -3
- package/dist/core/ingestion/ast-helpers.d.ts +80 -0
- package/dist/core/ingestion/ast-helpers.js +738 -0
- package/dist/core/ingestion/call-analysis.d.ts +73 -0
- package/dist/core/ingestion/call-analysis.js +490 -0
- package/dist/core/ingestion/call-processor.d.ts +55 -2
- package/dist/core/ingestion/call-processor.js +673 -108
- package/dist/core/ingestion/call-routing.d.ts +23 -2
- package/dist/core/ingestion/call-routing.js +21 -0
- package/dist/core/ingestion/entry-point-scoring.js +36 -26
- package/dist/core/ingestion/framework-detection.d.ts +10 -2
- package/dist/core/ingestion/framework-detection.js +49 -12
- package/dist/core/ingestion/heritage-processor.js +47 -49
- package/dist/core/ingestion/import-processor.d.ts +1 -1
- package/dist/core/ingestion/import-processor.js +103 -194
- package/dist/core/ingestion/import-resolution.d.ts +101 -0
- package/dist/core/ingestion/import-resolution.js +251 -0
- package/dist/core/ingestion/language-config.d.ts +3 -0
- package/dist/core/ingestion/language-config.js +13 -0
- package/dist/core/ingestion/markdown-processor.d.ts +17 -0
- package/dist/core/ingestion/markdown-processor.js +124 -0
- package/dist/core/ingestion/mro-processor.js +8 -3
- package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
- package/dist/core/ingestion/named-binding-extraction.js +89 -79
- package/dist/core/ingestion/parsing-processor.d.ts +3 -2
- package/dist/core/ingestion/parsing-processor.js +27 -60
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/ingestion/pipeline.js +425 -4
- package/dist/core/ingestion/resolution-context.d.ts +5 -0
- package/dist/core/ingestion/resolution-context.js +7 -4
- package/dist/core/ingestion/resolvers/index.d.ts +1 -1
- package/dist/core/ingestion/resolvers/index.js +1 -1
- package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
- package/dist/core/ingestion/resolvers/jvm.js +25 -9
- package/dist/core/ingestion/resolvers/php.d.ts +14 -0
- package/dist/core/ingestion/resolvers/php.js +43 -3
- package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
- package/dist/core/ingestion/resolvers/utils.js +16 -0
- package/dist/core/ingestion/symbol-table.d.ts +29 -3
- package/dist/core/ingestion/symbol-table.js +42 -9
- package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
- package/dist/core/ingestion/tree-sitter-queries.js +243 -2
- package/dist/core/ingestion/type-env.d.ts +28 -1
- package/dist/core/ingestion/type-env.js +451 -72
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +146 -2
- package/dist/core/ingestion/type-extractors/csharp.js +189 -16
- package/dist/core/ingestion/type-extractors/go.js +45 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
- package/dist/core/ingestion/type-extractors/index.js +1 -1
- package/dist/core/ingestion/type-extractors/jvm.js +244 -69
- package/dist/core/ingestion/type-extractors/php.js +31 -4
- package/dist/core/ingestion/type-extractors/python.js +89 -17
- package/dist/core/ingestion/type-extractors/ruby.js +17 -2
- package/dist/core/ingestion/type-extractors/rust.js +72 -4
- package/dist/core/ingestion/type-extractors/shared.d.ts +12 -2
- package/dist/core/ingestion/type-extractors/shared.js +115 -13
- package/dist/core/ingestion/type-extractors/swift.js +7 -6
- package/dist/core/ingestion/type-extractors/types.d.ts +54 -11
- package/dist/core/ingestion/type-extractors/typescript.js +171 -9
- package/dist/core/ingestion/utils.d.ts +2 -95
- package/dist/core/ingestion/utils.js +3 -892
- package/dist/core/ingestion/workers/parse-worker.d.ts +36 -11
- package/dist/core/ingestion/workers/parse-worker.js +116 -95
- package/dist/core/lbug/csv-generator.js +18 -1
- package/dist/core/lbug/lbug-adapter.d.ts +12 -0
- package/dist/core/lbug/lbug-adapter.js +71 -4
- package/dist/core/lbug/schema.d.ts +6 -4
- package/dist/core/lbug/schema.js +27 -3
- package/dist/mcp/core/embedder.js +11 -3
- package/dist/mcp/core/lbug-adapter.d.ts +22 -0
- package/dist/mcp/core/lbug-adapter.js +178 -23
- package/dist/mcp/local/local-backend.d.ts +22 -0
- package/dist/mcp/local/local-backend.js +136 -32
- package/dist/mcp/resources.js +13 -0
- package/dist/mcp/server.js +26 -4
- package/dist/mcp/tools.js +17 -7
- package/dist/server/api.d.ts +19 -1
- package/dist/server/api.js +66 -6
- package/dist/storage/git.d.ts +12 -0
- package/dist/storage/git.js +21 -0
- package/package.json +12 -4
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
|
[](https://www.npmjs.com/package/gitnexus)
|
|
8
8
|
[](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.
|
package/dist/cli/ai-context.d.ts
CHANGED
|
@@ -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';
|
package/dist/cli/ai-context.js
CHANGED
|
@@ -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';
|
package/dist/cli/analyze.d.ts
CHANGED
|
@@ -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>;
|
package/dist/cli/analyze.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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'
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
26
|
-
*
|
|
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
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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[
|
|
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[]>;
|