gitnexus 1.2.7 → 1.2.9

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 CHANGED
@@ -39,6 +39,12 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up
39
39
 
40
40
  > **Claude Code** gets the deepest integration: MCP tools + agent skills + PreToolUse hooks that automatically enrich grep/glob/bash calls with knowledge graph context.
41
41
 
42
+ ### Community Integrations
43
+
44
+ | Agent | Install | Source |
45
+ |-------|---------|--------|
46
+ | [pi](https://pi.dev) | `pi install npm:pi-gitnexus` | [pi-gitnexus](https://github.com/tintinweb/pi-gitnexus) |
47
+
42
48
  ## MCP Setup (manual)
43
49
 
44
50
  If you prefer to configure manually instead of using `gitnexus setup`:
@@ -135,7 +141,7 @@ gitnexus analyze [path] # Index a repository (or update stale index)
135
141
  gitnexus analyze --force # Force full re-index
136
142
  gitnexus analyze --skip-embeddings # Skip embedding generation (faster)
137
143
  gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
138
- gitnexus serve # Start HTTP server for web UI
144
+ gitnexus serve # Start local HTTP server (multi-repo) for web UI
139
145
  gitnexus list # List all indexed repositories
140
146
  gitnexus status # Show index status for current repo
141
147
  gitnexus clean # Delete index for current repo
@@ -179,6 +185,8 @@ Installed automatically by both `gitnexus analyze` (per-repo) and `gitnexus setu
179
185
 
180
186
  GitNexus also has a browser-based UI at [gitnexus.vercel.app](https://gitnexus.vercel.app) — 100% client-side, your code never leaves the browser.
181
187
 
188
+ **Local Backend Mode:** Run `gitnexus serve` and open the web UI locally — it auto-detects the server and shows all your indexed repos, with full AI chat support. No need to re-upload or re-index. The agent's tools (Cypher queries, search, code navigation) route through the backend HTTP API automatically.
189
+
182
190
  ## License
183
191
 
184
192
  [PolyForm Noncommercial 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0/)
@@ -5,6 +5,6 @@
5
5
  */
6
6
  export interface AnalyzeOptions {
7
7
  force?: boolean;
8
- skipEmbeddings?: boolean;
8
+ embeddings?: boolean;
9
9
  }
10
10
  export declare const analyzeCommand: (inputPath?: string, options?: AnalyzeOptions) => Promise<void>;
@@ -8,7 +8,7 @@ import cliProgress from 'cli-progress';
8
8
  import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
9
9
  import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu, createFTSIndex, loadCachedEmbeddings } from '../core/kuzu/kuzu-adapter.js';
10
10
  import { runEmbeddingPipeline } from '../core/embeddings/embedding-pipeline.js';
11
- import { disposeEmbedder } from '../core/embeddings/embedder.js';
11
+ // disposeEmbedder intentionally not called — ONNX Runtime segfaults on cleanup (see #38)
12
12
  import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath } from '../storage/repo-manager.js';
13
13
  import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
14
14
  import { generateAIContextFiles } from './ai-context.js';
@@ -70,11 +70,29 @@ export const analyzeCommand = async (inputPath, options) => {
70
70
  stopOnComplete: false,
71
71
  }, cliProgress.Presets.shades_grey);
72
72
  bar.start(100, 0, { phase: 'Initializing...' });
73
+ // Route all console output through bar.log() so the bar doesn't stamp itself
74
+ // multiple times when other code writes to stdout/stderr mid-render.
75
+ const origLog = console.log.bind(console);
76
+ const origWarn = console.warn.bind(console);
77
+ const origError = console.error.bind(console);
78
+ const barLog = (...args) => origLog(args.map(a => (typeof a === 'string' ? a : String(a))).join(' '));
79
+ console.log = barLog;
80
+ console.warn = barLog;
81
+ console.error = barLog;
82
+ // Show elapsed seconds for phases that run longer than 3s
83
+ let lastPhaseLabel = 'Initializing...';
84
+ let phaseStart = Date.now();
85
+ const elapsedTimer = setInterval(() => {
86
+ const elapsed = Math.round((Date.now() - phaseStart) / 1000);
87
+ if (elapsed >= 3) {
88
+ bar.update({ phase: `${lastPhaseLabel} (${elapsed}s)` });
89
+ }
90
+ }, 1000);
73
91
  const t0Global = Date.now();
74
92
  // ── Cache embeddings from existing index before rebuild ────────────
75
93
  let cachedEmbeddingNodeIds = new Set();
76
94
  let cachedEmbeddings = [];
77
- if (existingMeta && !options?.force) {
95
+ if (options?.embeddings && existingMeta && !options?.force) {
78
96
  try {
79
97
  bar.update(0, { phase: 'Caching embeddings...' });
80
98
  await initKuzu(kuzuPath);
@@ -94,10 +112,16 @@ export const analyzeCommand = async (inputPath, options) => {
94
112
  const pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
95
113
  const phaseLabel = PHASE_LABELS[progress.phase] || progress.phase;
96
114
  const scaled = Math.round(progress.percent * 0.6);
115
+ if (phaseLabel !== lastPhaseLabel) {
116
+ lastPhaseLabel = phaseLabel;
117
+ phaseStart = Date.now();
118
+ }
97
119
  bar.update(scaled, { phase: phaseLabel });
98
120
  });
99
121
  // ── Phase 2: KuzuDB (60–85%) ──────────────────────────────────────
100
- bar.update(60, { phase: 'Loading into KuzuDB...' });
122
+ lastPhaseLabel = 'Loading into KuzuDB...';
123
+ phaseStart = Date.now();
124
+ bar.update(60, { phase: lastPhaseLabel });
101
125
  await closeKuzu();
102
126
  const kuzuFiles = [kuzuPath, `${kuzuPath}.wal`, `${kuzuPath}.lock`];
103
127
  for (const f of kuzuFiles) {
@@ -117,7 +141,9 @@ export const analyzeCommand = async (inputPath, options) => {
117
141
  const kuzuTime = ((Date.now() - t0Kuzu) / 1000).toFixed(1);
118
142
  const kuzuWarnings = kuzuResult.warnings;
119
143
  // ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
120
- bar.update(85, { phase: 'Creating search indexes...' });
144
+ lastPhaseLabel = 'Creating search indexes...';
145
+ phaseStart = Date.now();
146
+ bar.update(85, { phase: lastPhaseLabel });
121
147
  const t0Fts = Date.now();
122
148
  try {
123
149
  await createFTSIndex('File', 'file_fts', ['name', 'content']);
@@ -146,22 +172,28 @@ export const analyzeCommand = async (inputPath, options) => {
146
172
  // ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
147
173
  const stats = await getKuzuStats();
148
174
  let embeddingTime = '0.0';
149
- let embeddingSkipped = false;
150
- let embeddingSkipReason = '';
151
- if (options?.skipEmbeddings) {
152
- embeddingSkipped = true;
153
- embeddingSkipReason = 'skipped (--skip-embeddings)';
154
- }
155
- else if (stats.nodes > EMBEDDING_NODE_LIMIT) {
156
- embeddingSkipped = true;
157
- embeddingSkipReason = `skipped (${stats.nodes.toLocaleString()} nodes > ${EMBEDDING_NODE_LIMIT.toLocaleString()} limit)`;
175
+ let embeddingSkipped = true;
176
+ let embeddingSkipReason = 'off (use --embeddings to enable)';
177
+ if (options?.embeddings) {
178
+ if (stats.nodes > EMBEDDING_NODE_LIMIT) {
179
+ embeddingSkipReason = `skipped (${stats.nodes.toLocaleString()} nodes > ${EMBEDDING_NODE_LIMIT.toLocaleString()} limit)`;
180
+ }
181
+ else {
182
+ embeddingSkipped = false;
183
+ }
158
184
  }
159
185
  if (!embeddingSkipped) {
160
- bar.update(90, { phase: 'Loading embedding model...' });
186
+ lastPhaseLabel = 'Loading embedding model...';
187
+ phaseStart = Date.now();
188
+ bar.update(90, { phase: lastPhaseLabel });
161
189
  const t0Emb = Date.now();
162
190
  await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (progress) => {
163
191
  const scaled = 90 + Math.round((progress.percent / 100) * 8);
164
192
  const label = progress.phase === 'loading-model' ? 'Loading embedding model...' : `Embedding ${progress.nodesProcessed || 0}/${progress.totalNodes || '?'}`;
193
+ if (label !== lastPhaseLabel) {
194
+ lastPhaseLabel = label;
195
+ phaseStart = Date.now();
196
+ }
165
197
  bar.update(scaled, { phase: label });
166
198
  }, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined);
167
199
  embeddingTime = ((Date.now() - t0Emb) / 1000).toFixed(1);
@@ -203,8 +235,14 @@ export const analyzeCommand = async (inputPath, options) => {
203
235
  processes: pipelineResult.processResult?.stats.totalProcesses,
204
236
  });
205
237
  await closeKuzu();
206
- await disposeEmbedder();
238
+ // Note: we intentionally do NOT call disposeEmbedder() here.
239
+ // ONNX Runtime's native cleanup segfaults on macOS and some Linux configs.
240
+ // Since the process exits immediately after, Node.js reclaims everything.
207
241
  const totalTime = ((Date.now() - t0Global) / 1000).toFixed(1);
242
+ clearInterval(elapsedTimer);
243
+ console.log = origLog;
244
+ console.warn = origWarn;
245
+ console.error = origError;
208
246
  bar.update(100, { phase: 'Done' });
209
247
  bar.stop();
210
248
  // ── Summary ───────────────────────────────────────────────────────
@@ -233,4 +271,10 @@ export const analyzeCommand = async (inputPath, options) => {
233
271
  console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
234
272
  }
235
273
  console.log('');
274
+ // ONNX Runtime registers native atexit hooks that segfault during process
275
+ // shutdown on macOS (#38) and some Linux configs (#40). Force-exit to
276
+ // bypass them when embeddings were loaded.
277
+ if (!embeddingSkipped) {
278
+ process.exit(0);
279
+ }
236
280
  };
package/dist/cli/index.js CHANGED
@@ -24,7 +24,7 @@ program
24
24
  .command('analyze [path]')
25
25
  .description('Index a repository (full analysis)')
26
26
  .option('-f, --force', 'Force full re-index even if up to date')
27
- .option('--skip-embeddings', 'Skip embedding generation (faster)')
27
+ .option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
28
28
  .action(analyzeCommand);
29
29
  program
30
30
  .command('serve')
package/dist/cli/setup.js CHANGED
@@ -13,9 +13,16 @@ import { getGlobalDir } from '../storage/repo-manager.js';
13
13
  const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = path.dirname(__filename);
15
15
  /**
16
- * The MCP server entry for all editors
16
+ * The MCP server entry for all editors.
17
+ * On Windows, npx must be invoked via cmd /c since it's a .cmd script.
17
18
  */
18
19
  function getMcpEntry() {
20
+ if (process.platform === 'win32') {
21
+ return {
22
+ command: 'cmd',
23
+ args: ['/c', 'npx', '-y', 'gitnexus@latest', 'mcp'],
24
+ };
25
+ }
19
26
  return {
20
27
  command: 'npx',
21
28
  args: ['-y', 'gitnexus@latest', 'mcp'],
@@ -0,0 +1,13 @@
1
+ /**
2
+ * View Command
3
+ *
4
+ * Generates a self-contained graph.html from the KuzuDB index and
5
+ * opens it in the default browser.
6
+ *
7
+ * Usage: gitnexus view [path] [--no-open]
8
+ */
9
+ export interface ViewCommandOptions {
10
+ noOpen?: boolean;
11
+ output?: string;
12
+ }
13
+ export declare const viewCommand: (inputPath?: string, options?: ViewCommandOptions) => Promise<void>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * View Command
3
+ *
4
+ * Generates a self-contained graph.html from the KuzuDB index and
5
+ * opens it in the default browser.
6
+ *
7
+ * Usage: gitnexus view [path] [--no-open]
8
+ */
9
+ import path from 'path';
10
+ import fs from 'fs/promises';
11
+ import { exec } from 'child_process';
12
+ import { findRepo } from '../storage/repo-manager.js';
13
+ import { initKuzu } from '../core/kuzu/kuzu-adapter.js';
14
+ import { buildGraph } from '../server/api.js';
15
+ import { generateHTMLGraphViewer } from '../core/graph/html-graph-viewer.js';
16
+ import { getCurrentCommit } from '../storage/git.js';
17
+ function openInBrowser(filePath) {
18
+ const url = `file://${filePath}`;
19
+ let cmd;
20
+ if (process.platform === 'darwin') {
21
+ cmd = `open "${url}"`;
22
+ }
23
+ else if (process.platform === 'win32') {
24
+ cmd = `start "" "${url}"`;
25
+ }
26
+ else {
27
+ cmd = `xdg-open "${url}"`;
28
+ }
29
+ exec(cmd, (err) => {
30
+ if (err)
31
+ console.error('Failed to open browser:', err.message);
32
+ });
33
+ }
34
+ export const viewCommand = async (inputPath, options) => {
35
+ console.log('⚠ Experimental: gitnexus view is under active development.\n');
36
+ const repoPath = inputPath ? path.resolve(inputPath) : process.cwd();
37
+ const repo = await findRepo(repoPath);
38
+ if (!repo) {
39
+ console.error('No index found. Run: gitnexus analyze');
40
+ process.exit(1);
41
+ }
42
+ const currentCommit = getCurrentCommit(repo.repoPath);
43
+ if (currentCommit !== repo.meta.lastCommit) {
44
+ console.warn('Index is stale — showing last indexed state. Run: gitnexus analyze\n');
45
+ }
46
+ await initKuzu(repo.kuzuPath);
47
+ const { nodes, relationships } = await buildGraph();
48
+ const projectName = path.basename(repo.repoPath);
49
+ const outputPath = options?.output
50
+ ? path.resolve(options.output)
51
+ : path.join(repo.storagePath, 'graph.html');
52
+ const html = generateHTMLGraphViewer(nodes, relationships, projectName);
53
+ await fs.writeFile(outputPath, html, 'utf-8');
54
+ console.log(`Graph written to: ${outputPath}`);
55
+ console.log(`Nodes: ${nodes.length} Edges: ${relationships.length}`);
56
+ if (!options?.noOpen) {
57
+ openInBrowser(outputPath);
58
+ }
59
+ };
@@ -89,6 +89,7 @@ export const initEmbedder = async (onProgress, config = {}, forceDevice) => {
89
89
  device: device,
90
90
  dtype: 'fp32',
91
91
  progress_callback: progressCallback,
92
+ session_options: { logSeverityLevel: 3 },
92
93
  });
93
94
  currentDevice = device;
94
95
  if (isDev) {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * HTML Graph Viewer Generator
3
+ *
4
+ * Produces a self-contained graph.html that renders the knowledge graph
5
+ * using Sigma.js v2 + graphology (both from CDN).
6
+ *
7
+ * Critical: node `content` fields are stripped before embedding to prevent
8
+ * </script> injection from source code breaking the HTML parser.
9
+ */
10
+ import { GraphNode, GraphRelationship } from './types.js';
11
+ /**
12
+ * Generate a self-contained HTML file that renders the knowledge graph.
13
+ * Strips large/unsafe fields from nodes before embedding.
14
+ */
15
+ export declare function generateHTMLGraphViewer(nodes: GraphNode[], relationships: GraphRelationship[], projectName: string): string;