gitnexus 1.1.8 → 1.2.0

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 (76) hide show
  1. package/README.md +50 -59
  2. package/dist/cli/ai-context.js +9 -9
  3. package/dist/cli/analyze.js +139 -47
  4. package/dist/cli/augment.d.ts +13 -0
  5. package/dist/cli/augment.js +33 -0
  6. package/dist/cli/claude-hooks.d.ts +22 -0
  7. package/dist/cli/claude-hooks.js +97 -0
  8. package/dist/cli/eval-server.d.ts +30 -0
  9. package/dist/cli/eval-server.js +372 -0
  10. package/dist/cli/index.js +56 -1
  11. package/dist/cli/mcp.js +9 -0
  12. package/dist/cli/setup.js +184 -5
  13. package/dist/cli/tool.d.ts +37 -0
  14. package/dist/cli/tool.js +91 -0
  15. package/dist/cli/wiki.d.ts +13 -0
  16. package/dist/cli/wiki.js +199 -0
  17. package/dist/core/augmentation/engine.d.ts +26 -0
  18. package/dist/core/augmentation/engine.js +213 -0
  19. package/dist/core/embeddings/embedder.d.ts +2 -2
  20. package/dist/core/embeddings/embedder.js +11 -11
  21. package/dist/core/embeddings/embedding-pipeline.d.ts +2 -1
  22. package/dist/core/embeddings/embedding-pipeline.js +13 -5
  23. package/dist/core/embeddings/types.d.ts +2 -2
  24. package/dist/core/ingestion/call-processor.d.ts +7 -0
  25. package/dist/core/ingestion/call-processor.js +61 -23
  26. package/dist/core/ingestion/community-processor.js +34 -26
  27. package/dist/core/ingestion/filesystem-walker.js +15 -10
  28. package/dist/core/ingestion/heritage-processor.d.ts +6 -0
  29. package/dist/core/ingestion/heritage-processor.js +68 -5
  30. package/dist/core/ingestion/import-processor.d.ts +22 -0
  31. package/dist/core/ingestion/import-processor.js +215 -20
  32. package/dist/core/ingestion/parsing-processor.d.ts +8 -1
  33. package/dist/core/ingestion/parsing-processor.js +66 -25
  34. package/dist/core/ingestion/pipeline.js +104 -40
  35. package/dist/core/ingestion/process-processor.js +1 -1
  36. package/dist/core/ingestion/workers/parse-worker.d.ts +58 -0
  37. package/dist/core/ingestion/workers/parse-worker.js +451 -0
  38. package/dist/core/ingestion/workers/worker-pool.d.ts +22 -0
  39. package/dist/core/ingestion/workers/worker-pool.js +65 -0
  40. package/dist/core/kuzu/kuzu-adapter.d.ts +15 -1
  41. package/dist/core/kuzu/kuzu-adapter.js +177 -63
  42. package/dist/core/kuzu/schema.d.ts +1 -1
  43. package/dist/core/kuzu/schema.js +3 -0
  44. package/dist/core/search/bm25-index.js +13 -15
  45. package/dist/core/wiki/generator.d.ts +96 -0
  46. package/dist/core/wiki/generator.js +674 -0
  47. package/dist/core/wiki/graph-queries.d.ts +80 -0
  48. package/dist/core/wiki/graph-queries.js +238 -0
  49. package/dist/core/wiki/html-viewer.d.ts +10 -0
  50. package/dist/core/wiki/html-viewer.js +297 -0
  51. package/dist/core/wiki/llm-client.d.ts +36 -0
  52. package/dist/core/wiki/llm-client.js +111 -0
  53. package/dist/core/wiki/prompts.d.ts +53 -0
  54. package/dist/core/wiki/prompts.js +174 -0
  55. package/dist/mcp/core/embedder.js +4 -2
  56. package/dist/mcp/core/kuzu-adapter.d.ts +2 -1
  57. package/dist/mcp/core/kuzu-adapter.js +35 -15
  58. package/dist/mcp/local/local-backend.d.ts +54 -1
  59. package/dist/mcp/local/local-backend.js +716 -171
  60. package/dist/mcp/resources.d.ts +1 -1
  61. package/dist/mcp/resources.js +111 -73
  62. package/dist/mcp/server.d.ts +1 -1
  63. package/dist/mcp/server.js +91 -22
  64. package/dist/mcp/tools.js +80 -61
  65. package/dist/storage/git.d.ts +0 -1
  66. package/dist/storage/git.js +1 -8
  67. package/dist/storage/repo-manager.d.ts +17 -0
  68. package/dist/storage/repo-manager.js +26 -0
  69. package/hooks/claude/gitnexus-hook.cjs +135 -0
  70. package/hooks/claude/pre-tool-use.sh +78 -0
  71. package/hooks/claude/session-start.sh +42 -0
  72. package/package.json +4 -2
  73. package/skills/debugging.md +24 -22
  74. package/skills/exploring.md +26 -24
  75. package/skills/impact-analysis.md +19 -13
  76. package/skills/refactoring.md +37 -26
package/README.md CHANGED
@@ -18,31 +18,37 @@ AI coding tools don't understand your codebase structure. They edit a function w
18
18
  ## Quick Start
19
19
 
20
20
  ```bash
21
- # Install
22
- npm install -g gitnexus
21
+ # Index your repo (run from repo root)
22
+ npx gitnexus analyze
23
+ ```
23
24
 
24
- # One-time: configure MCP for your editors
25
- gitnexus setup
25
+ That's it. This indexes the codebase, installs agent skills, registers Claude Code hooks, and creates `AGENTS.md` / `CLAUDE.md` context files — all in one command.
26
26
 
27
- # Index your repository (run from repo root)
28
- gitnexus analyze
27
+ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up manually below.
29
28
 
30
- # Done! Open your editor MCP connects automatically.
31
- ```
29
+ `gitnexus setup` auto-detects your editors and writes the correct global MCP config. You only need to run it once.
32
30
 
33
- Or without installing globally:
31
+ ### Editor Support
34
32
 
35
- ```bash
36
- npx gitnexus setup # one-time
37
- npx gitnexus analyze # per repo
38
- ```
33
+ | Editor | MCP | Skills | Hooks (auto-augment) | Support |
34
+ |--------|-----|--------|---------------------|---------|
35
+ | **Claude Code** | Yes | Yes | Yes (PreToolUse) | **Full** |
36
+ | **Cursor** | Yes | Yes | — | MCP + Skills |
37
+ | **Windsurf** | Yes | — | — | MCP |
38
+ | **OpenCode** | Yes | Yes | — | MCP + Skills |
39
39
 
40
- The `setup` command auto-detects Cursor, Claude Code, and OpenCode, then writes the correct global MCP config. You only run it once.
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
42
  ## MCP Setup (manual)
43
43
 
44
44
  If you prefer to configure manually instead of using `gitnexus setup`:
45
45
 
46
+ ### Claude Code (full support — MCP + skills + hooks)
47
+
48
+ ```bash
49
+ claude mcp add gitnexus -- npx -y gitnexus@latest mcp
50
+ ```
51
+
46
52
  ### Cursor / Windsurf
47
53
 
48
54
  Add to `~/.cursor/mcp.json` (global — works for all projects):
@@ -58,12 +64,6 @@ Add to `~/.cursor/mcp.json` (global — works for all projects):
58
64
  }
59
65
  ```
60
66
 
61
- ### Claude Code
62
-
63
- ```bash
64
- claude mcp add gitnexus -- npx -y gitnexus@latest mcp
65
- ```
66
-
67
67
  ### OpenCode
68
68
 
69
69
  Add to `~/.config/opencode/config.json`:
@@ -100,68 +100,59 @@ Your AI agent gets these tools automatically:
100
100
  | Tool | What It Does | `repo` Param |
101
101
  |------|-------------|--------------|
102
102
  | `list_repos` | Discover all indexed repositories | — |
103
- | `search` | Hybrid search (BM25 + semantic) with cluster context | Optional |
104
- | `overview` | List all clusters and processes | Optional |
105
- | `explore` | Deep dive on a symbol, cluster, or process | Optional |
106
- | `impact` | Blast radius analysis | Optional |
103
+ | `query` | Process-grouped hybrid search (BM25 + semantic + RRF) | Optional |
104
+ | `context` | 360-degree symbol view categorized refs, process participation | Optional |
105
+ | `impact` | Blast radius analysis with depth grouping and confidence | Optional |
106
+ | `detect_changes` | Git-diff impact maps changed lines to affected processes | Optional |
107
+ | `rename` | Multi-file coordinated rename with graph + text search | Optional |
107
108
  | `cypher` | Raw Cypher graph queries | Optional |
108
- | `analyze` | Index or re-index a repository | Optional |
109
109
 
110
- > With one indexed repo, the `repo` param is optional. With multiple, specify which: `search({query: "auth", repo: "my-app"})`.
110
+ > With one indexed repo, the `repo` param is optional. With multiple, specify which: `query({query: "auth", repo: "my-app"})`.
111
111
 
112
112
  ## MCP Resources
113
113
 
114
114
  | Resource | Purpose |
115
115
  |----------|---------|
116
116
  | `gitnexus://repos` | List all indexed repositories (read first) |
117
- | `gitnexus://repo/{name}/context` | Codebase stats and overview |
118
- | `gitnexus://repo/{name}/clusters` | All functional clusters |
117
+ | `gitnexus://repo/{name}/context` | Codebase stats, staleness check, and available tools |
118
+ | `gitnexus://repo/{name}/clusters` | All functional clusters with cohesion scores |
119
119
  | `gitnexus://repo/{name}/cluster/{name}` | Cluster members and details |
120
120
  | `gitnexus://repo/{name}/processes` | All execution flows |
121
- | `gitnexus://repo/{name}/process/{name}` | Full process trace |
121
+ | `gitnexus://repo/{name}/process/{name}` | Full process trace with steps |
122
122
  | `gitnexus://repo/{name}/schema` | Graph schema for Cypher queries |
123
123
 
124
+ ## MCP Prompts
125
+
126
+ | Prompt | What It Does |
127
+ |--------|-------------|
128
+ | `detect_impact` | Pre-commit change analysis — scope, affected processes, risk level |
129
+ | `generate_map` | Architecture documentation from the knowledge graph with mermaid diagrams |
130
+
124
131
  ## CLI Commands
125
132
 
126
133
  ```bash
127
- gitnexus setup # Configure MCP for your editors (one-time)
128
- gitnexus analyze [path] # Index a repository (or update stale index)
129
- gitnexus analyze --force # Force full re-index
130
- gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
131
- gitnexus serve # Start HTTP server for web UI
132
- gitnexus list # List all indexed repositories
133
- gitnexus status # Show index status for current repo
134
- gitnexus clean # Delete index for current repo
135
- gitnexus clean --all # Delete all indexes
134
+ gitnexus setup # Configure MCP for your editors (one-time)
135
+ gitnexus analyze [path] # Index a repository (or update stale index)
136
+ gitnexus analyze --force # Force full re-index
137
+ gitnexus analyze --skip-embeddings # Skip embedding generation (faster)
138
+ gitnexus mcp # Start MCP server (stdio) serves all indexed repos
139
+ gitnexus serve # Start HTTP server for web UI
140
+ gitnexus list # List all indexed repositories
141
+ gitnexus status # Show index status for current repo
142
+ gitnexus clean # Delete index for current repo
143
+ gitnexus clean --all --force # Delete all indexes
144
+ gitnexus wiki [path] # Generate LLM-powered docs from knowledge graph
145
+ gitnexus wiki --model <model> # Wiki with custom LLM model (default: gpt-4o-mini)
136
146
  ```
137
147
 
138
148
  ## Multi-Repo Support
139
149
 
140
- 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 with lazy KuzuDB connections.
150
+ 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 with lazy KuzuDB connections (max 5 concurrent, evicted after 5 minutes idle).
141
151
 
142
152
  ## Supported Languages
143
153
 
144
154
  TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust
145
155
 
146
- ## How Impact Analysis Works
147
-
148
- ```
149
- gitnexus_impact({target: "UserService", direction: "upstream", repo: "my-app"})
150
-
151
- TARGET: Class UserService (src/services/user.ts)
152
-
153
- UPSTREAM (what depends on this):
154
- Depth 1 (direct callers):
155
- handleLogin [CALLS 90%] → src/api/auth.ts:45
156
- handleRegister [CALLS 90%] → src/api/auth.ts:78
157
- Depth 2:
158
- authRouter [IMPORTS] → src/routes/auth.ts
159
-
160
- 8 files affected, 3 clusters touched
161
- ```
162
-
163
- Options: `maxDepth`, `minConfidence`, `relationTypes`, `includeTests`
164
-
165
156
  ## Agent Skills
166
157
 
167
158
  GitNexus ships with skill files that teach AI agents how to use the tools effectively:
@@ -171,7 +162,7 @@ GitNexus ships with skill files that teach AI agents how to use the tools effect
171
162
  - **Impact Analysis** — Analyze blast radius before changes
172
163
  - **Refactoring** — Plan safe refactors using dependency mapping
173
164
 
174
- These are installed automatically to `.claude/skills/` when you run `gitnexus analyze`.
165
+ Installed automatically by both `gitnexus analyze` (per-repo) and `gitnexus setup` (global).
175
166
 
176
167
  ## Requirements
177
168
 
@@ -24,13 +24,12 @@ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
24
24
  * - Tools/Resources sections are labeled "Reference" — agents treat them as lookup, not workflow
25
25
  */
26
26
  function generateGitNexusContent(projectName, stats) {
27
- const clusterCount = stats.clusters || stats.communities || 0;
28
27
  return `${GITNEXUS_START_MARKER}
29
28
  # GitNexus MCP
30
29
 
31
- This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${clusterCount} clusters, ${stats.processes || 0} processes).
30
+ This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows).
32
31
 
33
- GitNexus provides a knowledge graph over this codebase — clusters, call chains, blast radius, execution flows, and semantic search.
32
+ GitNexus provides a knowledge graph over this codebase — call chains, blast radius, execution flows, and semantic search.
34
33
 
35
34
  ## Always Start Here
36
35
 
@@ -55,10 +54,11 @@ For any task involving code understanding, debugging, impact analysis, or refact
55
54
 
56
55
  | Tool | What it gives you |
57
56
  |------|-------------------|
58
- | \`search\` | Semantic + keyword code search with cluster context |
59
- | \`explore\` | Symbol deep divecallers, callees, cluster membership, processes |
60
- | \`impact\` | Blast radius — what breaks at depth 1/2/3 with confidence scores |
61
- | \`overview\` | All clusters and processes at a glance |
57
+ | \`query\` | Process-grouped code intelligence execution flows related to a concept |
58
+ | \`context\` | 360-degree symbol viewcategorized refs, processes it participates in |
59
+ | \`impact\` | Symbol blast radius — what breaks at depth 1/2/3 with confidence |
60
+ | \`detect_changes\` | Git-diff impact what do your current changes affect |
61
+ | \`rename\` | Multi-file coordinated rename with confidence-tagged edits |
62
62
  | \`cypher\` | Raw graph queries (read \`gitnexus://repo/{name}/schema\` first) |
63
63
  | \`list_repos\` | Discover indexed repos |
64
64
 
@@ -69,8 +69,8 @@ Lightweight reads (~100-500 tokens) for navigation:
69
69
  | Resource | Content |
70
70
  |----------|---------|
71
71
  | \`gitnexus://repo/{name}/context\` | Stats, staleness check |
72
- | \`gitnexus://repo/{name}/clusters\` | All clusters with cohesion scores |
73
- | \`gitnexus://repo/{name}/cluster/{clusterName}\` | Cluster members |
72
+ | \`gitnexus://repo/{name}/clusters\` | All functional areas with cohesion scores |
73
+ | \`gitnexus://repo/{name}/cluster/{clusterName}\` | Area members |
74
74
  | \`gitnexus://repo/{name}/processes\` | All execution flows |
75
75
  | \`gitnexus://repo/{name}/process/{processName}\` | Step-by-step trace |
76
76
  | \`gitnexus://repo/{name}/schema\` | Graph schema for Cypher |
@@ -4,17 +4,35 @@
4
4
  * Indexes a repository and stores the knowledge graph in .gitnexus/
5
5
  */
6
6
  import path from 'path';
7
- import ora from 'ora';
7
+ import cliProgress from 'cli-progress';
8
8
  import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
9
- import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu, createFTSIndex } from '../core/kuzu/kuzu-adapter.js';
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
12
  import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath } from '../storage/repo-manager.js';
12
13
  import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
13
14
  import { generateAIContextFiles } from './ai-context.js';
14
15
  import fs from 'fs/promises';
16
+ import { registerClaudeHook } from './claude-hooks.js';
17
+ /** Threshold: auto-skip embeddings for repos with more nodes than this */
18
+ const EMBEDDING_NODE_LIMIT = 50_000;
19
+ const PHASE_LABELS = {
20
+ extracting: 'Scanning files',
21
+ structure: 'Building structure',
22
+ parsing: 'Parsing code',
23
+ imports: 'Resolving imports',
24
+ calls: 'Tracing calls',
25
+ heritage: 'Extracting inheritance',
26
+ communities: 'Detecting communities',
27
+ processes: 'Detecting processes',
28
+ complete: 'Pipeline complete',
29
+ kuzu: 'Loading into KuzuDB',
30
+ fts: 'Creating search indexes',
31
+ embeddings: 'Generating embeddings',
32
+ done: 'Done',
33
+ };
15
34
  export const analyzeCommand = async (inputPath, options) => {
16
- const spinner = ora('Checking repository...').start();
17
- // If path provided, use it directly. Otherwise, find git root from cwd.
35
+ console.log('\n GitNexus Analyzer\n');
18
36
  let repoPath;
19
37
  if (inputPath) {
20
38
  repoPath = path.resolve(inputPath);
@@ -22,49 +40,85 @@ export const analyzeCommand = async (inputPath, options) => {
22
40
  else {
23
41
  const gitRoot = getGitRoot(process.cwd());
24
42
  if (!gitRoot) {
25
- spinner.fail('Not inside a git repository');
43
+ console.log(' Not inside a git repository\n');
26
44
  process.exitCode = 1;
27
45
  return;
28
46
  }
29
47
  repoPath = gitRoot;
30
48
  }
31
49
  if (!isGitRepo(repoPath)) {
32
- spinner.fail('Not a git repository');
50
+ console.log(' Not a git repository\n');
33
51
  process.exitCode = 1;
34
52
  return;
35
53
  }
36
54
  const { storagePath, kuzuPath } = getStoragePaths(repoPath);
37
55
  const currentCommit = getCurrentCommit(repoPath);
38
56
  const existingMeta = await loadMeta(storagePath);
39
- // Skip if already indexed at same commit
40
57
  if (existingMeta && !options?.force && existingMeta.lastCommit === currentCommit) {
41
- spinner.succeed('Repository already up to date');
58
+ console.log(' Already up to date\n');
42
59
  return;
43
60
  }
44
- // Run ingestion pipeline
45
- spinner.text = 'Running ingestion pipeline...';
61
+ // Single progress bar for entire pipeline
62
+ const bar = new cliProgress.SingleBar({
63
+ format: ' {bar} {percentage}% | {phase}',
64
+ barCompleteChar: '\u2588',
65
+ barIncompleteChar: '\u2591',
66
+ hideCursor: true,
67
+ barGlue: '',
68
+ autopadding: true,
69
+ clearOnComplete: false,
70
+ stopOnComplete: false,
71
+ }, cliProgress.Presets.shades_grey);
72
+ bar.start(100, 0, { phase: 'Initializing...' });
73
+ const t0Global = Date.now();
74
+ // ── Cache embeddings from existing index before rebuild ────────────
75
+ let cachedEmbeddingNodeIds = new Set();
76
+ let cachedEmbeddings = [];
77
+ if (existingMeta && !options?.force) {
78
+ try {
79
+ bar.update(0, { phase: 'Caching embeddings...' });
80
+ await initKuzu(kuzuPath);
81
+ const cached = await loadCachedEmbeddings();
82
+ cachedEmbeddingNodeIds = cached.embeddingNodeIds;
83
+ cachedEmbeddings = cached.embeddings;
84
+ await closeKuzu();
85
+ }
86
+ catch {
87
+ try {
88
+ await closeKuzu();
89
+ }
90
+ catch { }
91
+ }
92
+ }
93
+ // ── Phase 1: Full Pipeline (0–60%) ─────────────────────────────────
46
94
  const pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
47
- spinner.text = `${progress.phase}: ${progress.percent}%`;
95
+ const phaseLabel = PHASE_LABELS[progress.phase] || progress.phase;
96
+ const scaled = Math.round(progress.percent * 0.6);
97
+ bar.update(scaled, { phase: phaseLabel });
48
98
  });
49
- // Load graph into KuzuDB
50
- // Always start fresh - remove existing kuzu DB to avoid stale/corrupt data
51
- spinner.text = 'Loading graph into KuzuDB...';
99
+ // ── Phase 2: KuzuDB (60–85%) ──────────────────────────────────────
100
+ bar.update(60, { phase: 'Loading into KuzuDB...' });
52
101
  await closeKuzu();
53
- // Kuzu 0.11 stores databases as: <name> (main file) + <name>.wal (WAL file)
54
- // BOTH must be deleted or kuzu will find the orphaned WAL and corrupt the database
55
- const fsClean = await import('fs/promises');
56
102
  const kuzuFiles = [kuzuPath, `${kuzuPath}.wal`, `${kuzuPath}.lock`];
57
103
  for (const f of kuzuFiles) {
58
104
  try {
59
- await fsClean.rm(f, { recursive: true, force: true });
105
+ await fs.rm(f, { recursive: true, force: true });
60
106
  }
61
- catch { /* may not exist */ }
107
+ catch { }
62
108
  }
109
+ const t0Kuzu = Date.now();
63
110
  await initKuzu(kuzuPath);
64
- await loadGraphToKuzu(pipelineResult.graph, pipelineResult.fileContents, storagePath);
65
- // Create FTS indexes for keyword search
66
- // Indexes searchable content on: File, Function, Class, Method
67
- spinner.text = 'Creating FTS indexes...';
111
+ let kuzuMsgCount = 0;
112
+ const kuzuResult = await loadGraphToKuzu(pipelineResult.graph, pipelineResult.fileContents, storagePath, (msg) => {
113
+ kuzuMsgCount++;
114
+ const progress = Math.min(84, 60 + Math.round((kuzuMsgCount / (kuzuMsgCount + 10)) * 24));
115
+ bar.update(progress, { phase: msg });
116
+ });
117
+ const kuzuTime = ((Date.now() - t0Kuzu) / 1000).toFixed(1);
118
+ const kuzuWarnings = kuzuResult.warnings;
119
+ // ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
120
+ bar.update(85, { phase: 'Creating search indexes...' });
121
+ const t0Fts = Date.now();
68
122
  try {
69
123
  await createFTSIndex('File', 'file_fts', ['name', 'content']);
70
124
  await createFTSIndex('Function', 'function_fts', ['name', 'content']);
@@ -73,18 +127,47 @@ export const analyzeCommand = async (inputPath, options) => {
73
127
  await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
74
128
  }
75
129
  catch (e) {
76
- // FTS index creation may fail if tables are empty (no data for that type)
77
- console.error('Note: Some FTS indexes may not have been created:', e.message);
130
+ // Non-fatal FTS is best-effort
78
131
  }
79
- // Generate embeddings
80
- if (!options?.skipEmbeddings) {
81
- spinner.text = 'Generating embeddings...';
82
- await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (progress) => {
83
- spinner.text = `Embeddings: ${progress.percent}%`;
84
- });
132
+ const ftsTime = ((Date.now() - t0Fts) / 1000).toFixed(1);
133
+ // ── Phase 3.5: Re-insert cached embeddings ────────────────────────
134
+ if (cachedEmbeddings.length > 0) {
135
+ bar.update(88, { phase: `Restoring ${cachedEmbeddings.length} cached embeddings...` });
136
+ const EMBED_BATCH = 200;
137
+ for (let i = 0; i < cachedEmbeddings.length; i += EMBED_BATCH) {
138
+ const batch = cachedEmbeddings.slice(i, i + EMBED_BATCH);
139
+ const paramsList = batch.map(e => ({ nodeId: e.nodeId, embedding: e.embedding }));
140
+ try {
141
+ await executeWithReusedStatement(`CREATE (e:CodeEmbedding {nodeId: $nodeId, embedding: $embedding})`, paramsList);
142
+ }
143
+ catch { /* some may fail if node was removed, that's fine */ }
144
+ }
85
145
  }
86
- // Save metadata
146
+ // ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
87
147
  const stats = await getKuzuStats();
148
+ 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)`;
158
+ }
159
+ if (!embeddingSkipped) {
160
+ bar.update(90, { phase: 'Loading embedding model...' });
161
+ const t0Emb = Date.now();
162
+ await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (progress) => {
163
+ const scaled = 90 + Math.round((progress.percent / 100) * 8);
164
+ const label = progress.phase === 'loading-model' ? 'Loading embedding model...' : `Embedding ${progress.nodesProcessed || 0}/${progress.totalNodes || '?'}`;
165
+ bar.update(scaled, { phase: label });
166
+ }, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined);
167
+ embeddingTime = ((Date.now() - t0Emb) / 1000).toFixed(1);
168
+ }
169
+ // ── Phase 5: Finalize (98–100%) ───────────────────────────────────
170
+ bar.update(98, { phase: 'Saving metadata...' });
88
171
  const meta = {
89
172
  repoPath,
90
173
  lastCommit: currentCommit,
@@ -98,14 +181,10 @@ export const analyzeCommand = async (inputPath, options) => {
98
181
  },
99
182
  };
100
183
  await saveMeta(storagePath, meta);
101
- // Register in global registry
102
184
  await registerRepo(repoPath, meta);
103
- // Add .gitnexus to .gitignore
104
185
  await addToGitignore(repoPath);
105
- // Generate AI context files
186
+ const hookResult = await registerClaudeHook();
106
187
  const projectName = path.basename(repoPath);
107
- // Compute aggregated cluster count (grouped by heuristicLabel, >=5 symbols)
108
- // This matches the aggregation logic in local-backend.ts for tool output consistency.
109
188
  let aggregatedClusterCount = 0;
110
189
  if (pipelineResult.communityResult?.communities) {
111
190
  const groups = new Map();
@@ -123,22 +202,35 @@ export const analyzeCommand = async (inputPath, options) => {
123
202
  clusters: aggregatedClusterCount,
124
203
  processes: pipelineResult.processResult?.stats.totalProcesses,
125
204
  });
126
- // Close database
127
205
  await closeKuzu();
128
- spinner.succeed('Repository indexed successfully');
129
- console.log(` Path: ${repoPath}`);
130
- console.log(` Storage: ${storagePath}`);
131
- console.log(` Stats: ${stats.nodes} nodes, ${stats.edges} edges`);
206
+ await disposeEmbedder();
207
+ const totalTime = ((Date.now() - t0Global) / 1000).toFixed(1);
208
+ bar.update(100, { phase: 'Done' });
209
+ bar.stop();
210
+ // ── Summary ───────────────────────────────────────────────────────
211
+ const embeddingsCached = cachedEmbeddings.length > 0;
212
+ console.log(`\n Repository indexed successfully (${totalTime}s)${embeddingsCached ? ` [${cachedEmbeddings.length} embeddings cached]` : ''}\n`);
213
+ console.log(` ${stats.nodes.toLocaleString()} nodes | ${stats.edges.toLocaleString()} edges | ${pipelineResult.communityResult?.stats.totalCommunities || 0} clusters | ${pipelineResult.processResult?.stats.totalProcesses || 0} flows`);
214
+ console.log(` KuzuDB ${kuzuTime}s | FTS ${ftsTime}s | Embeddings ${embeddingSkipped ? embeddingSkipReason : embeddingTime + 's'}`);
215
+ console.log(` ${repoPath}`);
132
216
  if (aiContext.files.length > 0) {
133
- console.log(` AI Context: ${aiContext.files.join(', ')}`);
217
+ console.log(` Context: ${aiContext.files.join(', ')}`);
218
+ }
219
+ if (hookResult.registered) {
220
+ console.log(` Hooks: ${hookResult.message}`);
221
+ }
222
+ // Show warnings (missing schema pairs, etc.) after the clean output
223
+ if (kuzuWarnings.length > 0) {
224
+ console.log(`\n Warnings (${kuzuWarnings.length}):`);
225
+ for (const w of kuzuWarnings) {
226
+ console.log(` ${w}`);
227
+ }
134
228
  }
135
- // Hint about setup if it hasn't been run
136
229
  try {
137
230
  await fs.access(getGlobalRegistryPath());
138
231
  }
139
232
  catch {
140
- // Registry didn't exist before this run suggest setup
141
- console.log('');
142
- console.log(' Tip: Run `gitnexus setup` to configure MCP for your editor.');
233
+ console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
143
234
  }
235
+ console.log('');
144
236
  };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Augment CLI Command
3
+ *
4
+ * Fast-path command for platform hooks.
5
+ * Shells out from Claude Code PreToolUse / Cursor beforeShellExecution hooks.
6
+ *
7
+ * Usage: gitnexus augment <pattern>
8
+ * Returns enriched text to stdout.
9
+ *
10
+ * Performance: Must cold-start fast (<500ms).
11
+ * Skips unnecessary initialization (no web server, no full DB warmup).
12
+ */
13
+ export declare function augmentCommand(pattern: string): Promise<void>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Augment CLI Command
3
+ *
4
+ * Fast-path command for platform hooks.
5
+ * Shells out from Claude Code PreToolUse / Cursor beforeShellExecution hooks.
6
+ *
7
+ * Usage: gitnexus augment <pattern>
8
+ * Returns enriched text to stdout.
9
+ *
10
+ * Performance: Must cold-start fast (<500ms).
11
+ * Skips unnecessary initialization (no web server, no full DB warmup).
12
+ */
13
+ import { augment } from '../core/augmentation/engine.js';
14
+ export async function augmentCommand(pattern) {
15
+ if (!pattern || pattern.length < 3) {
16
+ process.exit(0);
17
+ }
18
+ try {
19
+ const result = await augment(pattern, process.cwd());
20
+ if (result) {
21
+ // IMPORTANT: Write to stderr, NOT stdout.
22
+ // KuzuDB's native module captures stdout fd at OS level during init,
23
+ // which makes stdout permanently broken in subprocess contexts.
24
+ // stderr is never captured, so it works reliably everywhere.
25
+ // The hook reads from the subprocess's stderr.
26
+ process.stderr.write(result + '\n');
27
+ }
28
+ }
29
+ catch {
30
+ // Graceful failure — never break the calling hook
31
+ process.exit(0);
32
+ }
33
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Claude Code Hook Registration
3
+ *
4
+ * Registers the GitNexus PreToolUse hook in ~/.claude/hooks.json
5
+ * so that grep/glob/bash calls are automatically augmented with
6
+ * knowledge graph context.
7
+ *
8
+ * Idempotent — safe to call multiple times.
9
+ */
10
+ /**
11
+ * Register (or verify) the GitNexus hook in Claude Code's global hooks.json.
12
+ *
13
+ * - Creates ~/.claude/ and hooks.json if they don't exist
14
+ * - Preserves existing hooks from other tools
15
+ * - Skips if GitNexus hook is already registered
16
+ *
17
+ * Returns a status message for the CLI output.
18
+ */
19
+ export declare function registerClaudeHook(): Promise<{
20
+ registered: boolean;
21
+ message: string;
22
+ }>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Claude Code Hook Registration
3
+ *
4
+ * Registers the GitNexus PreToolUse hook in ~/.claude/hooks.json
5
+ * so that grep/glob/bash calls are automatically augmented with
6
+ * knowledge graph context.
7
+ *
8
+ * Idempotent — safe to call multiple times.
9
+ */
10
+ import fs from 'fs/promises';
11
+ import path from 'path';
12
+ import os from 'os';
13
+ import { fileURLToPath } from 'url';
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ /**
17
+ * Get the absolute path to the gitnexus-hook.js file.
18
+ * Works for both local dev and npm-installed packages.
19
+ */
20
+ function getHookScriptPath() {
21
+ // From dist/cli/claude-hooks.js → hooks/claude/gitnexus-hook.js
22
+ const packageRoot = path.resolve(__dirname, '..', '..');
23
+ return path.join(packageRoot, 'hooks', 'claude', 'gitnexus-hook.cjs');
24
+ }
25
+ /**
26
+ * Register (or verify) the GitNexus hook in Claude Code's global hooks.json.
27
+ *
28
+ * - Creates ~/.claude/ and hooks.json if they don't exist
29
+ * - Preserves existing hooks from other tools
30
+ * - Skips if GitNexus hook is already registered
31
+ *
32
+ * Returns a status message for the CLI output.
33
+ */
34
+ export async function registerClaudeHook() {
35
+ const claudeDir = path.join(os.homedir(), '.claude');
36
+ const hooksFile = path.join(claudeDir, 'hooks.json');
37
+ const hookScript = getHookScriptPath();
38
+ // Check if the hook script exists
39
+ try {
40
+ await fs.access(hookScript);
41
+ }
42
+ catch {
43
+ return { registered: false, message: 'Hook script not found (package may be incomplete)' };
44
+ }
45
+ // Build the hook command — use node + absolute path for reliability
46
+ const hookCommand = `node "${hookScript}"`;
47
+ // Check if ~/.claude/ exists (user has Claude Code installed)
48
+ try {
49
+ await fs.access(claudeDir);
50
+ }
51
+ catch {
52
+ // No Claude Code installation — skip silently
53
+ return { registered: false, message: 'Claude Code not detected (~/.claude/ not found)' };
54
+ }
55
+ // Read existing hooks.json or start fresh
56
+ let hooksConfig = {};
57
+ try {
58
+ const existing = await fs.readFile(hooksFile, 'utf-8');
59
+ hooksConfig = JSON.parse(existing);
60
+ }
61
+ catch {
62
+ // File doesn't exist or is invalid — we'll create it
63
+ }
64
+ // Ensure the hooks structure exists
65
+ if (!hooksConfig.hooks) {
66
+ hooksConfig.hooks = {};
67
+ }
68
+ if (!Array.isArray(hooksConfig.hooks.PreToolUse)) {
69
+ hooksConfig.hooks.PreToolUse = [];
70
+ }
71
+ // Check if GitNexus hook is already registered
72
+ const existingEntry = hooksConfig.hooks.PreToolUse.find((entry) => {
73
+ if (!entry.hooks || !Array.isArray(entry.hooks))
74
+ return false;
75
+ return entry.hooks.some((h) => h.command && (h.command.includes('gitnexus-hook') ||
76
+ h.command.includes('gitnexus augment')));
77
+ });
78
+ if (existingEntry) {
79
+ return { registered: true, message: 'Claude Code hook already registered' };
80
+ }
81
+ // Add the GitNexus hook entry
82
+ hooksConfig.hooks.PreToolUse.push({
83
+ matcher: {
84
+ tool_name: "Grep|Glob|Bash"
85
+ },
86
+ hooks: [
87
+ {
88
+ type: "command",
89
+ command: hookCommand,
90
+ timeout: 8000
91
+ }
92
+ ]
93
+ });
94
+ // Write back
95
+ await fs.writeFile(hooksFile, JSON.stringify(hooksConfig, null, 2) + '\n', 'utf-8');
96
+ return { registered: true, message: 'Claude Code hook registered' };
97
+ }