gitnexus 1.4.1 → 1.4.6

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 (169) hide show
  1. package/README.md +215 -194
  2. package/dist/cli/ai-context.d.ts +2 -1
  3. package/dist/cli/ai-context.js +117 -90
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +57 -30
  6. package/dist/cli/augment.js +1 -1
  7. package/dist/cli/eval-server.d.ts +1 -1
  8. package/dist/cli/eval-server.js +14 -6
  9. package/dist/cli/index.js +18 -25
  10. package/dist/cli/lazy-action.d.ts +6 -0
  11. package/dist/cli/lazy-action.js +18 -0
  12. package/dist/cli/mcp.js +1 -1
  13. package/dist/cli/setup.js +42 -32
  14. package/dist/cli/skill-gen.d.ts +26 -0
  15. package/dist/cli/skill-gen.js +549 -0
  16. package/dist/cli/status.js +13 -4
  17. package/dist/cli/tool.d.ts +3 -2
  18. package/dist/cli/tool.js +48 -13
  19. package/dist/cli/wiki.js +2 -2
  20. package/dist/config/ignore-service.d.ts +25 -0
  21. package/dist/config/ignore-service.js +76 -0
  22. package/dist/config/supported-languages.d.ts +1 -0
  23. package/dist/config/supported-languages.js +1 -1
  24. package/dist/core/augmentation/engine.js +99 -72
  25. package/dist/core/embeddings/embedder.d.ts +1 -1
  26. package/dist/core/embeddings/embedder.js +1 -1
  27. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  28. package/dist/core/embeddings/embedding-pipeline.js +74 -47
  29. package/dist/core/embeddings/types.d.ts +1 -1
  30. package/dist/core/graph/types.d.ts +5 -2
  31. package/dist/core/ingestion/ast-cache.js +3 -2
  32. package/dist/core/ingestion/call-processor.d.ts +5 -7
  33. package/dist/core/ingestion/call-processor.js +430 -283
  34. package/dist/core/ingestion/call-routing.d.ts +53 -0
  35. package/dist/core/ingestion/call-routing.js +108 -0
  36. package/dist/core/ingestion/cluster-enricher.js +16 -16
  37. package/dist/core/ingestion/constants.d.ts +16 -0
  38. package/dist/core/ingestion/constants.js +16 -0
  39. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  40. package/dist/core/ingestion/entry-point-scoring.js +94 -24
  41. package/dist/core/ingestion/export-detection.d.ts +18 -0
  42. package/dist/core/ingestion/export-detection.js +231 -0
  43. package/dist/core/ingestion/filesystem-walker.js +4 -3
  44. package/dist/core/ingestion/framework-detection.d.ts +5 -1
  45. package/dist/core/ingestion/framework-detection.js +48 -8
  46. package/dist/core/ingestion/heritage-processor.d.ts +13 -5
  47. package/dist/core/ingestion/heritage-processor.js +109 -55
  48. package/dist/core/ingestion/import-processor.d.ts +16 -20
  49. package/dist/core/ingestion/import-processor.js +202 -696
  50. package/dist/core/ingestion/language-config.d.ts +46 -0
  51. package/dist/core/ingestion/language-config.js +167 -0
  52. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  53. package/dist/core/ingestion/mro-processor.js +369 -0
  54. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  55. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  56. package/dist/core/ingestion/parsing-processor.d.ts +3 -11
  57. package/dist/core/ingestion/parsing-processor.js +85 -181
  58. package/dist/core/ingestion/pipeline.d.ts +5 -1
  59. package/dist/core/ingestion/pipeline.js +192 -116
  60. package/dist/core/ingestion/process-processor.js +2 -1
  61. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  62. package/dist/core/ingestion/resolution-context.js +132 -0
  63. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  64. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  65. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  66. package/dist/core/ingestion/resolvers/go.js +42 -0
  67. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  68. package/dist/core/ingestion/resolvers/index.js +13 -0
  69. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  70. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  71. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  72. package/dist/core/ingestion/resolvers/php.js +35 -0
  73. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  74. package/dist/core/ingestion/resolvers/python.js +52 -0
  75. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  76. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  77. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  78. package/dist/core/ingestion/resolvers/rust.js +73 -0
  79. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  80. package/dist/core/ingestion/resolvers/standard.js +123 -0
  81. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  82. package/dist/core/ingestion/resolvers/utils.js +122 -0
  83. package/dist/core/ingestion/symbol-table.d.ts +21 -1
  84. package/dist/core/ingestion/symbol-table.js +40 -12
  85. package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -11
  86. package/dist/core/ingestion/tree-sitter-queries.js +642 -485
  87. package/dist/core/ingestion/type-env.d.ts +49 -0
  88. package/dist/core/ingestion/type-env.js +611 -0
  89. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  90. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  91. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  92. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  93. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  94. package/dist/core/ingestion/type-extractors/go.js +467 -0
  95. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  96. package/dist/core/ingestion/type-extractors/index.js +31 -0
  97. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  98. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  99. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  100. package/dist/core/ingestion/type-extractors/php.js +549 -0
  101. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  102. package/dist/core/ingestion/type-extractors/python.js +406 -0
  103. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  104. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  105. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  106. package/dist/core/ingestion/type-extractors/rust.js +449 -0
  107. package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
  108. package/dist/core/ingestion/type-extractors/shared.js +703 -0
  109. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  110. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  111. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  112. package/dist/core/ingestion/type-extractors/types.js +1 -0
  113. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  114. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  115. package/dist/core/ingestion/utils.d.ts +98 -0
  116. package/dist/core/ingestion/utils.js +1064 -9
  117. package/dist/core/ingestion/workers/parse-worker.d.ts +38 -4
  118. package/dist/core/ingestion/workers/parse-worker.js +251 -359
  119. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  120. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +1 -1
  121. package/dist/core/{kuzu → lbug}/csv-generator.js +20 -4
  122. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +19 -19
  123. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +82 -82
  124. package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
  125. package/dist/core/{kuzu → lbug}/schema.js +304 -289
  126. package/dist/core/search/bm25-index.d.ts +4 -4
  127. package/dist/core/search/bm25-index.js +17 -16
  128. package/dist/core/search/hybrid-search.d.ts +2 -2
  129. package/dist/core/search/hybrid-search.js +9 -9
  130. package/dist/core/tree-sitter/parser-loader.js +9 -2
  131. package/dist/core/wiki/generator.d.ts +4 -52
  132. package/dist/core/wiki/generator.js +53 -552
  133. package/dist/core/wiki/graph-queries.d.ts +4 -46
  134. package/dist/core/wiki/graph-queries.js +103 -282
  135. package/dist/core/wiki/html-viewer.js +192 -192
  136. package/dist/core/wiki/llm-client.js +11 -73
  137. package/dist/core/wiki/prompts.d.ts +8 -52
  138. package/dist/core/wiki/prompts.js +86 -200
  139. package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
  140. package/dist/mcp/compatible-stdio-transport.js +200 -0
  141. package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +7 -9
  142. package/dist/mcp/core/{kuzu-adapter.js → lbug-adapter.js} +77 -79
  143. package/dist/mcp/local/local-backend.d.ts +7 -6
  144. package/dist/mcp/local/local-backend.js +176 -147
  145. package/dist/mcp/resources.js +42 -42
  146. package/dist/mcp/server.js +18 -19
  147. package/dist/mcp/tools.js +103 -104
  148. package/dist/server/api.js +12 -12
  149. package/dist/server/mcp-http.d.ts +1 -1
  150. package/dist/server/mcp-http.js +1 -1
  151. package/dist/storage/repo-manager.d.ts +20 -2
  152. package/dist/storage/repo-manager.js +55 -1
  153. package/dist/types/pipeline.d.ts +1 -1
  154. package/hooks/claude/gitnexus-hook.cjs +238 -155
  155. package/hooks/claude/pre-tool-use.sh +79 -79
  156. package/hooks/claude/session-start.sh +42 -42
  157. package/package.json +99 -96
  158. package/scripts/patch-tree-sitter-swift.cjs +74 -74
  159. package/skills/gitnexus-cli.md +82 -82
  160. package/skills/gitnexus-debugging.md +89 -89
  161. package/skills/gitnexus-exploring.md +78 -78
  162. package/skills/gitnexus-guide.md +64 -64
  163. package/skills/gitnexus-impact-analysis.md +97 -97
  164. package/skills/gitnexus-pr-review.md +163 -163
  165. package/skills/gitnexus-refactoring.md +121 -121
  166. package/vendor/leiden/index.cjs +355 -355
  167. package/vendor/leiden/utils.cjs +392 -392
  168. package/dist/core/wiki/diagrams.d.ts +0 -27
  169. package/dist/core/wiki/diagrams.js +0 -163
@@ -23,10 +23,64 @@ export const getStoragePaths = (repoPath) => {
23
23
  const storagePath = getStoragePath(repoPath);
24
24
  return {
25
25
  storagePath,
26
- kuzuPath: path.join(storagePath, 'kuzu'),
26
+ lbugPath: path.join(storagePath, 'lbug'),
27
27
  metaPath: path.join(storagePath, 'meta.json'),
28
28
  };
29
29
  };
30
+ /**
31
+ * Check whether a KuzuDB index exists in the given storage path.
32
+ * Non-destructive — safe to call from status commands.
33
+ */
34
+ export const hasKuzuIndex = async (storagePath) => {
35
+ try {
36
+ await fs.stat(path.join(storagePath, 'kuzu'));
37
+ return true;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ };
43
+ /**
44
+ * Clean up stale KuzuDB files after migration to LadybugDB.
45
+ *
46
+ * Returns:
47
+ * found — true if .gitnexus/kuzu existed and was deleted
48
+ * needsReindex — true if kuzu existed but lbug does not (re-analyze required)
49
+ *
50
+ * Callers own the user-facing messaging; this function only deletes files.
51
+ */
52
+ export const cleanupOldKuzuFiles = async (storagePath) => {
53
+ const oldPath = path.join(storagePath, 'kuzu');
54
+ const newPath = path.join(storagePath, 'lbug');
55
+ try {
56
+ await fs.stat(oldPath);
57
+ // Old kuzu file/dir exists — determine if lbug is already present
58
+ let needsReindex = false;
59
+ try {
60
+ await fs.stat(newPath);
61
+ }
62
+ catch {
63
+ needsReindex = true;
64
+ }
65
+ // Delete kuzu database file and its sidecars (.wal, .lock)
66
+ for (const suffix of ['', '.wal', '.lock']) {
67
+ try {
68
+ await fs.unlink(oldPath + suffix);
69
+ }
70
+ catch { }
71
+ }
72
+ // Also handle the case where kuzu was stored as a directory
73
+ try {
74
+ await fs.rm(oldPath, { recursive: true, force: true });
75
+ }
76
+ catch { }
77
+ return { found: true, needsReindex };
78
+ }
79
+ catch {
80
+ // Old path doesn't exist — nothing to do
81
+ return { found: false, needsReindex: false };
82
+ }
83
+ };
30
84
  /**
31
85
  * Load metadata from an indexed repo
32
86
  */
@@ -15,7 +15,7 @@ export interface PipelineProgress {
15
15
  }
16
16
  export interface PipelineResult {
17
17
  graph: KnowledgeGraph;
18
- /** Absolute path to the repo root — used for lazy file reads during KuzuDB loading */
18
+ /** Absolute path to the repo root — used for lazy file reads during LadybugDB loading */
19
19
  repoPath: string;
20
20
  /** Total files scanned (for stats) */
21
21
  totalFileCount: number;
@@ -1,155 +1,238 @@
1
- #!/usr/bin/env node
2
- /**
3
- * GitNexus Claude Code Hook
4
- *
5
- * PreToolUse handler — intercepts Grep/Glob/Bash searches
6
- * and augments with graph context from the GitNexus index.
7
- *
8
- * NOTE: SessionStart hooks are broken on Windows (Claude Code bug).
9
- * Session context is injected via CLAUDE.md / skills instead.
10
- */
11
-
12
- const fs = require('fs');
13
- const path = require('path');
14
- const { execFileSync } = require('child_process');
15
-
16
- /**
17
- * Read JSON input from stdin synchronously.
18
- */
19
- function readInput() {
20
- try {
21
- const data = fs.readFileSync(0, 'utf-8');
22
- return JSON.parse(data);
23
- } catch {
24
- return {};
25
- }
26
- }
27
-
28
- /**
29
- * Check if a directory (or ancestor) has a .gitnexus index.
30
- */
31
- function findGitNexusIndex(startDir) {
32
- let dir = startDir || process.cwd();
33
- for (let i = 0; i < 5; i++) {
34
- if (fs.existsSync(path.join(dir, '.gitnexus'))) {
35
- return true;
36
- }
37
- const parent = path.dirname(dir);
38
- if (parent === dir) break;
39
- dir = parent;
40
- }
41
- return false;
42
- }
43
-
44
- /**
45
- * Extract search pattern from tool input.
46
- */
47
- function extractPattern(toolName, toolInput) {
48
- if (toolName === 'Grep') {
49
- return toolInput.pattern || null;
50
- }
51
-
52
- if (toolName === 'Glob') {
53
- const raw = toolInput.pattern || '';
54
- const match = raw.match(/[*\/]([a-zA-Z][a-zA-Z0-9_-]{2,})/);
55
- return match ? match[1] : null;
56
- }
57
-
58
- if (toolName === 'Bash') {
59
- const cmd = toolInput.command || '';
60
- if (!/\brg\b|\bgrep\b/.test(cmd)) return null;
61
-
62
- const tokens = cmd.split(/\s+/);
63
- let foundCmd = false;
64
- let skipNext = false;
65
- const flagsWithValues = new Set(['-e', '-f', '-m', '-A', '-B', '-C', '-g', '--glob', '-t', '--type', '--include', '--exclude']);
66
-
67
- for (const token of tokens) {
68
- if (skipNext) { skipNext = false; continue; }
69
- if (!foundCmd) {
70
- if (/\brg$|\bgrep$/.test(token)) foundCmd = true;
71
- continue;
72
- }
73
- if (token.startsWith('-')) {
74
- if (flagsWithValues.has(token)) skipNext = true;
75
- continue;
76
- }
77
- const cleaned = token.replace(/['"]/g, '');
78
- return cleaned.length >= 3 ? cleaned : null;
79
- }
80
- return null;
81
- }
82
-
83
- return null;
84
- }
85
-
86
- function main() {
87
- try {
88
- const input = readInput();
89
- const hookEvent = input.hook_event_name || '';
90
-
91
- if (hookEvent !== 'PreToolUse') return;
92
-
93
- const cwd = input.cwd || process.cwd();
94
- if (!findGitNexusIndex(cwd)) return;
95
-
96
- const toolName = input.tool_name || '';
97
- const toolInput = input.tool_input || {};
98
-
99
- if (toolName !== 'Grep' && toolName !== 'Glob' && toolName !== 'Bash') return;
100
-
101
- const pattern = extractPattern(toolName, toolInput);
102
- if (!pattern || pattern.length < 3) return;
103
-
104
- // Resolve CLI path — try multiple strategies:
105
- // 1. Relative path (works when script is inside npm package)
106
- // 2. require.resolve (works when gitnexus is globally installed)
107
- // 3. Fall back to npx (works when neither is available)
108
- let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');
109
- if (!fs.existsSync(cliPath)) {
110
- try {
111
- cliPath = require.resolve('gitnexus/dist/cli/index.js');
112
- } catch {
113
- cliPath = ''; // will use npx fallback
114
- }
115
- }
116
-
117
- // augment CLI writes result to stderr (KuzuDB's native module captures
118
- // stdout fd at OS level, making it unusable in subprocess contexts).
119
- const { spawnSync } = require('child_process');
120
- let result = '';
121
- try {
122
- let child;
123
- if (cliPath) {
124
- child = spawnSync(
125
- process.execPath,
126
- [cliPath, 'augment', pattern],
127
- { encoding: 'utf-8', timeout: 8000, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
128
- );
129
- } else {
130
- // npx fallback
131
- const cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
132
- child = spawnSync(
133
- cmd,
134
- ['-y', 'gitnexus', 'augment', pattern],
135
- { encoding: 'utf-8', timeout: 15000, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
136
- );
137
- }
138
- result = child.stderr || '';
139
- } catch { /* graceful failure */ }
140
-
141
- if (result && result.trim()) {
142
- console.log(JSON.stringify({
143
- hookSpecificOutput: {
144
- hookEventName: 'PreToolUse',
145
- additionalContext: result.trim()
146
- }
147
- }));
148
- }
149
- } catch (err) {
150
- // Graceful failure log to stderr for debugging
151
- console.error('GitNexus hook error:', err.message);
152
- }
153
- }
154
-
155
- main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GitNexus Claude Code Hook
4
+ *
5
+ * PreToolUse — intercepts Grep/Glob/Bash searches and augments
6
+ * with graph context from the GitNexus index.
7
+ * PostToolUse — detects stale index after git mutations and notifies
8
+ * the agent to reindex.
9
+ *
10
+ * NOTE: SessionStart hooks are broken on Windows (Claude Code bug).
11
+ * Session context is injected via CLAUDE.md / skills instead.
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { spawnSync } = require('child_process');
17
+
18
+ /**
19
+ * Read JSON input from stdin synchronously.
20
+ */
21
+ function readInput() {
22
+ try {
23
+ const data = fs.readFileSync(0, 'utf-8');
24
+ return JSON.parse(data);
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Find the .gitnexus directory by walking up from startDir.
32
+ * Returns the path to .gitnexus/ or null if not found.
33
+ */
34
+ function findGitNexusDir(startDir) {
35
+ let dir = startDir || process.cwd();
36
+ for (let i = 0; i < 5; i++) {
37
+ const candidate = path.join(dir, '.gitnexus');
38
+ if (fs.existsSync(candidate)) return candidate;
39
+ const parent = path.dirname(dir);
40
+ if (parent === dir) break;
41
+ dir = parent;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Extract search pattern from tool input.
48
+ */
49
+ function extractPattern(toolName, toolInput) {
50
+ if (toolName === 'Grep') {
51
+ return toolInput.pattern || null;
52
+ }
53
+
54
+ if (toolName === 'Glob') {
55
+ const raw = toolInput.pattern || '';
56
+ const match = raw.match(/[*\/]([a-zA-Z][a-zA-Z0-9_-]{2,})/);
57
+ return match ? match[1] : null;
58
+ }
59
+
60
+ if (toolName === 'Bash') {
61
+ const cmd = toolInput.command || '';
62
+ if (!/\brg\b|\bgrep\b/.test(cmd)) return null;
63
+
64
+ const tokens = cmd.split(/\s+/);
65
+ let foundCmd = false;
66
+ let skipNext = false;
67
+ const flagsWithValues = new Set(['-e', '-f', '-m', '-A', '-B', '-C', '-g', '--glob', '-t', '--type', '--include', '--exclude']);
68
+
69
+ for (const token of tokens) {
70
+ if (skipNext) { skipNext = false; continue; }
71
+ if (!foundCmd) {
72
+ if (/\brg$|\bgrep$/.test(token)) foundCmd = true;
73
+ continue;
74
+ }
75
+ if (token.startsWith('-')) {
76
+ if (flagsWithValues.has(token)) skipNext = true;
77
+ continue;
78
+ }
79
+ const cleaned = token.replace(/['"]/g, '');
80
+ return cleaned.length >= 3 ? cleaned : null;
81
+ }
82
+ return null;
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ /**
89
+ * Resolve the gitnexus CLI path.
90
+ * 1. Relative path (works when script is inside npm package)
91
+ * 2. require.resolve (works when gitnexus is globally installed)
92
+ * 3. Fall back to npx (returns empty string)
93
+ */
94
+ function resolveCliPath() {
95
+ let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');
96
+ if (!fs.existsSync(cliPath)) {
97
+ try {
98
+ cliPath = require.resolve('gitnexus/dist/cli/index.js');
99
+ } catch {
100
+ cliPath = '';
101
+ }
102
+ }
103
+ return cliPath;
104
+ }
105
+
106
+ /**
107
+ * Spawn a gitnexus CLI command synchronously.
108
+ * Returns the stderr output (KuzuDB captures stdout at OS level).
109
+ */
110
+ function runGitNexusCli(cliPath, args, cwd, timeout) {
111
+ const isWin = process.platform === 'win32';
112
+ if (cliPath) {
113
+ return spawnSync(
114
+ process.execPath,
115
+ [cliPath, ...args],
116
+ { encoding: 'utf-8', timeout, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
117
+ );
118
+ }
119
+ // On Windows, invoke npx.cmd directly (no shell needed)
120
+ return spawnSync(
121
+ isWin ? 'npx.cmd' : 'npx',
122
+ ['-y', 'gitnexus', ...args],
123
+ { encoding: 'utf-8', timeout: timeout + 5000, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
124
+ );
125
+ }
126
+
127
+ /**
128
+ * PreToolUse handler — augment searches with graph context.
129
+ */
130
+ function handlePreToolUse(input) {
131
+ const cwd = input.cwd || process.cwd();
132
+ if (!path.isAbsolute(cwd)) return;
133
+ if (!findGitNexusDir(cwd)) return;
134
+
135
+ const toolName = input.tool_name || '';
136
+ const toolInput = input.tool_input || {};
137
+
138
+ if (toolName !== 'Grep' && toolName !== 'Glob' && toolName !== 'Bash') return;
139
+
140
+ const pattern = extractPattern(toolName, toolInput);
141
+ if (!pattern || pattern.length < 3) return;
142
+
143
+ const cliPath = resolveCliPath();
144
+ let result = '';
145
+ try {
146
+ const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
147
+ if (!child.error && child.status === 0) {
148
+ result = child.stderr || '';
149
+ }
150
+ } catch { /* graceful failure */ }
151
+
152
+ if (result && result.trim()) {
153
+ sendHookResponse('PreToolUse', result.trim());
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Emit a PostToolUse hook response with additional context for the agent.
159
+ */
160
+ function sendHookResponse(hookEventName, message) {
161
+ console.log(JSON.stringify({
162
+ hookSpecificOutput: { hookEventName, additionalContext: message }
163
+ }));
164
+ }
165
+
166
+ /**
167
+ * PostToolUse handler — detect index staleness after git mutations.
168
+ *
169
+ * Instead of spawning a full `gitnexus analyze` synchronously (which blocks
170
+ * the agent for up to 120s and risks KuzuDB corruption on timeout), we do a
171
+ * lightweight staleness check: compare `git rev-parse HEAD` against the
172
+ * lastCommit stored in `.gitnexus/meta.json`. If they differ, notify the
173
+ * agent so it can decide when to reindex.
174
+ */
175
+ function handlePostToolUse(input) {
176
+ const toolName = input.tool_name || '';
177
+ if (toolName !== 'Bash') return;
178
+
179
+ const command = (input.tool_input || {}).command || '';
180
+ if (!/\bgit\s+(commit|merge|rebase|cherry-pick|pull)(\s|$)/.test(command)) return;
181
+
182
+ // Only proceed if the command succeeded
183
+ const toolOutput = input.tool_output || {};
184
+ if (toolOutput.exit_code !== undefined && toolOutput.exit_code !== 0) return;
185
+
186
+ const cwd = input.cwd || process.cwd();
187
+ if (!path.isAbsolute(cwd)) return;
188
+ const gitNexusDir = findGitNexusDir(cwd);
189
+ if (!gitNexusDir) return;
190
+
191
+ // Compare HEAD against last indexed commit — skip if unchanged
192
+ let currentHead = '';
193
+ try {
194
+ const headResult = spawnSync('git', ['rev-parse', 'HEAD'], {
195
+ encoding: 'utf-8', timeout: 3000, cwd, stdio: ['pipe', 'pipe', 'pipe'],
196
+ });
197
+ currentHead = (headResult.stdout || '').trim();
198
+ } catch { return; }
199
+
200
+ if (!currentHead) return;
201
+
202
+ let lastCommit = '';
203
+ let hadEmbeddings = false;
204
+ try {
205
+ const meta = JSON.parse(fs.readFileSync(path.join(gitNexusDir, 'meta.json'), 'utf-8'));
206
+ lastCommit = meta.lastCommit || '';
207
+ hadEmbeddings = (meta.stats && meta.stats.embeddings > 0);
208
+ } catch { /* no meta — treat as stale */ }
209
+
210
+ // If HEAD matches last indexed commit, no reindex needed
211
+ if (currentHead && currentHead === lastCommit) return;
212
+
213
+ const analyzeCmd = `npx gitnexus analyze${hadEmbeddings ? ' --embeddings' : ''}`;
214
+ sendHookResponse('PostToolUse',
215
+ `GitNexus index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` +
216
+ `Run \`${analyzeCmd}\` to update the knowledge graph.`
217
+ );
218
+ }
219
+
220
+ // Dispatch map for hook events
221
+ const handlers = {
222
+ PreToolUse: handlePreToolUse,
223
+ PostToolUse: handlePostToolUse,
224
+ };
225
+
226
+ function main() {
227
+ try {
228
+ const input = readInput();
229
+ const handler = handlers[input.hook_event_name || ''];
230
+ if (handler) handler(input);
231
+ } catch (err) {
232
+ if (process.env.GITNEXUS_DEBUG) {
233
+ console.error('GitNexus hook error:', (err.message || '').slice(0, 200));
234
+ }
235
+ }
236
+ }
237
+
238
+ main();
@@ -1,79 +1,79 @@
1
- #!/bin/bash
2
- # GitNexus PreToolUse hook for Claude Code
3
- # Intercepts Grep/Glob/Bash searches and augments with graph context.
4
- # Receives JSON on stdin with { tool_name, tool_input, cwd, ... }
5
- # Returns JSON with additionalContext for graph-enriched results.
6
-
7
- INPUT=$(cat)
8
-
9
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
10
- CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
11
-
12
- # Extract search pattern based on tool type
13
- PATTERN=""
14
-
15
- case "$TOOL_NAME" in
16
- Grep)
17
- PATTERN=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null)
18
- ;;
19
- Glob)
20
- # Glob patterns are file paths, not search terms — extract meaningful part
21
- RAW=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null)
22
- # Strip glob syntax to get the meaningful name (e.g., "**/*.ts" → skip, "auth*.ts" → "auth")
23
- PATTERN=$(echo "$RAW" | sed -n 's/.*[*\/]\([a-zA-Z][a-zA-Z0-9_-]*\).*/\1/p')
24
- ;;
25
- Bash)
26
- CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
27
- # Only augment grep/rg commands
28
- if echo "$CMD" | grep -qE '\brg\b|\bgrep\b'; then
29
- # Extract pattern from rg/grep
30
- if echo "$CMD" | grep -qE '\brg\b'; then
31
- PATTERN=$(echo "$CMD" | sed -n "s/.*\brg\s\+\(--[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p")
32
- elif echo "$CMD" | grep -qE '\bgrep\b'; then
33
- PATTERN=$(echo "$CMD" | sed -n "s/.*\bgrep\s\+\(-[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p")
34
- fi
35
- fi
36
- ;;
37
- *)
38
- # Not a search tool — skip
39
- exit 0
40
- ;;
41
- esac
42
-
43
- # Skip if pattern too short or empty
44
- if [ -z "$PATTERN" ] || [ ${#PATTERN} -lt 3 ]; then
45
- exit 0
46
- fi
47
-
48
- # Check if we're in a GitNexus-indexed repo
49
- dir="${CWD:-$PWD}"
50
- found=false
51
- for i in 1 2 3 4 5; do
52
- if [ -d "$dir/.gitnexus" ]; then
53
- found=true
54
- break
55
- fi
56
- parent="$(dirname "$dir")"
57
- [ "$parent" = "$dir" ] && break
58
- dir="$parent"
59
- done
60
-
61
- if [ "$found" = false ]; then
62
- exit 0
63
- fi
64
-
65
- # Run gitnexus augment — must be fast (<500ms target)
66
- # augment writes to stderr (KuzuDB captures stdout at OS level), so capture stderr and discard stdout
67
- RESULT=$(cd "$CWD" && npx -y gitnexus augment "$PATTERN" 2>&1 1>/dev/null)
68
-
69
- if [ -n "$RESULT" ]; then
70
- ESCAPED=$(echo "$RESULT" | jq -Rs .)
71
- jq -n --argjson ctx "$ESCAPED" '{
72
- hookSpecificOutput: {
73
- hookEventName: "PreToolUse",
74
- additionalContext: $ctx
75
- }
76
- }'
77
- else
78
- exit 0
79
- fi
1
+ #!/bin/bash
2
+ # GitNexus PreToolUse hook for Claude Code
3
+ # Intercepts Grep/Glob/Bash searches and augments with graph context.
4
+ # Receives JSON on stdin with { tool_name, tool_input, cwd, ... }
5
+ # Returns JSON with additionalContext for graph-enriched results.
6
+
7
+ INPUT=$(cat)
8
+
9
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
10
+ CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
11
+
12
+ # Extract search pattern based on tool type
13
+ PATTERN=""
14
+
15
+ case "$TOOL_NAME" in
16
+ Grep)
17
+ PATTERN=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null)
18
+ ;;
19
+ Glob)
20
+ # Glob patterns are file paths, not search terms — extract meaningful part
21
+ RAW=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null)
22
+ # Strip glob syntax to get the meaningful name (e.g., "**/*.ts" → skip, "auth*.ts" → "auth")
23
+ PATTERN=$(echo "$RAW" | sed -n 's/.*[*\/]\([a-zA-Z][a-zA-Z0-9_-]*\).*/\1/p')
24
+ ;;
25
+ Bash)
26
+ CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
27
+ # Only augment grep/rg commands
28
+ if echo "$CMD" | grep -qE '\brg\b|\bgrep\b'; then
29
+ # Extract pattern from rg/grep
30
+ if echo "$CMD" | grep -qE '\brg\b'; then
31
+ PATTERN=$(echo "$CMD" | sed -n "s/.*\brg\s\+\(--[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p")
32
+ elif echo "$CMD" | grep -qE '\bgrep\b'; then
33
+ PATTERN=$(echo "$CMD" | sed -n "s/.*\bgrep\s\+\(-[^ ]*\s\+\)*['\"]\\?\([^'\";\| >]*\\).*/\2/p")
34
+ fi
35
+ fi
36
+ ;;
37
+ *)
38
+ # Not a search tool — skip
39
+ exit 0
40
+ ;;
41
+ esac
42
+
43
+ # Skip if pattern too short or empty
44
+ if [ -z "$PATTERN" ] || [ ${#PATTERN} -lt 3 ]; then
45
+ exit 0
46
+ fi
47
+
48
+ # Check if we're in a GitNexus-indexed repo
49
+ dir="${CWD:-$PWD}"
50
+ found=false
51
+ for i in 1 2 3 4 5; do
52
+ if [ -d "$dir/.gitnexus" ]; then
53
+ found=true
54
+ break
55
+ fi
56
+ parent="$(dirname "$dir")"
57
+ [ "$parent" = "$dir" ] && break
58
+ dir="$parent"
59
+ done
60
+
61
+ if [ "$found" = false ]; then
62
+ exit 0
63
+ fi
64
+
65
+ # Run gitnexus augment — must be fast (<500ms target)
66
+ # augment writes to stderr (KuzuDB captures stdout at OS level), so capture stderr and discard stdout
67
+ RESULT=$(cd "$CWD" && npx -y gitnexus augment "$PATTERN" 2>&1 1>/dev/null)
68
+
69
+ if [ -n "$RESULT" ]; then
70
+ ESCAPED=$(echo "$RESULT" | jq -Rs .)
71
+ jq -n --argjson ctx "$ESCAPED" '{
72
+ hookSpecificOutput: {
73
+ hookEventName: "PreToolUse",
74
+ additionalContext: $ctx
75
+ }
76
+ }'
77
+ else
78
+ exit 0
79
+ fi