@duytransipher/gitnexus 1.4.6-sipher.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 (224) hide show
  1. package/LICENSE +73 -0
  2. package/README.md +261 -0
  3. package/dist/cli/ai-context.d.ts +23 -0
  4. package/dist/cli/ai-context.js +265 -0
  5. package/dist/cli/analyze.d.ts +12 -0
  6. package/dist/cli/analyze.js +345 -0
  7. package/dist/cli/augment.d.ts +13 -0
  8. package/dist/cli/augment.js +33 -0
  9. package/dist/cli/clean.d.ts +10 -0
  10. package/dist/cli/clean.js +60 -0
  11. package/dist/cli/eval-server.d.ts +37 -0
  12. package/dist/cli/eval-server.js +389 -0
  13. package/dist/cli/index.d.ts +2 -0
  14. package/dist/cli/index.js +137 -0
  15. package/dist/cli/lazy-action.d.ts +6 -0
  16. package/dist/cli/lazy-action.js +18 -0
  17. package/dist/cli/list.d.ts +6 -0
  18. package/dist/cli/list.js +30 -0
  19. package/dist/cli/mcp.d.ts +8 -0
  20. package/dist/cli/mcp.js +36 -0
  21. package/dist/cli/serve.d.ts +4 -0
  22. package/dist/cli/serve.js +6 -0
  23. package/dist/cli/setup.d.ts +8 -0
  24. package/dist/cli/setup.js +367 -0
  25. package/dist/cli/sipher-patched.d.ts +2 -0
  26. package/dist/cli/sipher-patched.js +77 -0
  27. package/dist/cli/skill-gen.d.ts +26 -0
  28. package/dist/cli/skill-gen.js +549 -0
  29. package/dist/cli/status.d.ts +6 -0
  30. package/dist/cli/status.js +36 -0
  31. package/dist/cli/tool.d.ts +60 -0
  32. package/dist/cli/tool.js +180 -0
  33. package/dist/cli/wiki.d.ts +15 -0
  34. package/dist/cli/wiki.js +365 -0
  35. package/dist/config/ignore-service.d.ts +26 -0
  36. package/dist/config/ignore-service.js +284 -0
  37. package/dist/config/supported-languages.d.ts +15 -0
  38. package/dist/config/supported-languages.js +16 -0
  39. package/dist/core/augmentation/engine.d.ts +26 -0
  40. package/dist/core/augmentation/engine.js +240 -0
  41. package/dist/core/embeddings/embedder.d.ts +60 -0
  42. package/dist/core/embeddings/embedder.js +251 -0
  43. package/dist/core/embeddings/embedding-pipeline.d.ts +51 -0
  44. package/dist/core/embeddings/embedding-pipeline.js +356 -0
  45. package/dist/core/embeddings/index.d.ts +9 -0
  46. package/dist/core/embeddings/index.js +9 -0
  47. package/dist/core/embeddings/text-generator.d.ts +24 -0
  48. package/dist/core/embeddings/text-generator.js +182 -0
  49. package/dist/core/embeddings/types.d.ts +87 -0
  50. package/dist/core/embeddings/types.js +32 -0
  51. package/dist/core/graph/graph.d.ts +2 -0
  52. package/dist/core/graph/graph.js +66 -0
  53. package/dist/core/graph/types.d.ts +66 -0
  54. package/dist/core/graph/types.js +1 -0
  55. package/dist/core/ingestion/ast-cache.d.ts +11 -0
  56. package/dist/core/ingestion/ast-cache.js +35 -0
  57. package/dist/core/ingestion/call-processor.d.ts +23 -0
  58. package/dist/core/ingestion/call-processor.js +793 -0
  59. package/dist/core/ingestion/call-routing.d.ts +68 -0
  60. package/dist/core/ingestion/call-routing.js +129 -0
  61. package/dist/core/ingestion/cluster-enricher.d.ts +38 -0
  62. package/dist/core/ingestion/cluster-enricher.js +170 -0
  63. package/dist/core/ingestion/community-processor.d.ts +39 -0
  64. package/dist/core/ingestion/community-processor.js +312 -0
  65. package/dist/core/ingestion/constants.d.ts +16 -0
  66. package/dist/core/ingestion/constants.js +16 -0
  67. package/dist/core/ingestion/entry-point-scoring.d.ts +40 -0
  68. package/dist/core/ingestion/entry-point-scoring.js +353 -0
  69. package/dist/core/ingestion/export-detection.d.ts +18 -0
  70. package/dist/core/ingestion/export-detection.js +231 -0
  71. package/dist/core/ingestion/filesystem-walker.d.ts +28 -0
  72. package/dist/core/ingestion/filesystem-walker.js +81 -0
  73. package/dist/core/ingestion/framework-detection.d.ts +54 -0
  74. package/dist/core/ingestion/framework-detection.js +411 -0
  75. package/dist/core/ingestion/heritage-processor.d.ts +28 -0
  76. package/dist/core/ingestion/heritage-processor.js +251 -0
  77. package/dist/core/ingestion/import-processor.d.ts +34 -0
  78. package/dist/core/ingestion/import-processor.js +398 -0
  79. package/dist/core/ingestion/language-config.d.ts +46 -0
  80. package/dist/core/ingestion/language-config.js +167 -0
  81. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  82. package/dist/core/ingestion/mro-processor.js +369 -0
  83. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  84. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  85. package/dist/core/ingestion/parsing-processor.d.ts +19 -0
  86. package/dist/core/ingestion/parsing-processor.js +315 -0
  87. package/dist/core/ingestion/pipeline.d.ts +6 -0
  88. package/dist/core/ingestion/pipeline.js +401 -0
  89. package/dist/core/ingestion/process-processor.d.ts +51 -0
  90. package/dist/core/ingestion/process-processor.js +315 -0
  91. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  92. package/dist/core/ingestion/resolution-context.js +132 -0
  93. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  94. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  95. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  96. package/dist/core/ingestion/resolvers/go.js +42 -0
  97. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  98. package/dist/core/ingestion/resolvers/index.js +13 -0
  99. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  100. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  101. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  102. package/dist/core/ingestion/resolvers/php.js +35 -0
  103. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  104. package/dist/core/ingestion/resolvers/python.js +52 -0
  105. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  106. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  107. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  108. package/dist/core/ingestion/resolvers/rust.js +73 -0
  109. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  110. package/dist/core/ingestion/resolvers/standard.js +123 -0
  111. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  112. package/dist/core/ingestion/resolvers/utils.js +122 -0
  113. package/dist/core/ingestion/structure-processor.d.ts +2 -0
  114. package/dist/core/ingestion/structure-processor.js +36 -0
  115. package/dist/core/ingestion/symbol-table.d.ts +63 -0
  116. package/dist/core/ingestion/symbol-table.js +85 -0
  117. package/dist/core/ingestion/tree-sitter-queries.d.ts +15 -0
  118. package/dist/core/ingestion/tree-sitter-queries.js +888 -0
  119. package/dist/core/ingestion/type-env.d.ts +49 -0
  120. package/dist/core/ingestion/type-env.js +613 -0
  121. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  122. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  123. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  124. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  125. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  126. package/dist/core/ingestion/type-extractors/go.js +467 -0
  127. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  128. package/dist/core/ingestion/type-extractors/index.js +31 -0
  129. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  130. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  131. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  132. package/dist/core/ingestion/type-extractors/php.js +549 -0
  133. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  134. package/dist/core/ingestion/type-extractors/python.js +455 -0
  135. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  136. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  137. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  138. package/dist/core/ingestion/type-extractors/rust.js +456 -0
  139. package/dist/core/ingestion/type-extractors/shared.d.ts +145 -0
  140. package/dist/core/ingestion/type-extractors/shared.js +810 -0
  141. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  142. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  143. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  144. package/dist/core/ingestion/type-extractors/types.js +1 -0
  145. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  146. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  147. package/dist/core/ingestion/utils.d.ts +138 -0
  148. package/dist/core/ingestion/utils.js +1290 -0
  149. package/dist/core/ingestion/workers/parse-worker.d.ts +122 -0
  150. package/dist/core/ingestion/workers/parse-worker.js +1126 -0
  151. package/dist/core/ingestion/workers/worker-pool.d.ts +16 -0
  152. package/dist/core/ingestion/workers/worker-pool.js +128 -0
  153. package/dist/core/lbug/csv-generator.d.ts +33 -0
  154. package/dist/core/lbug/csv-generator.js +366 -0
  155. package/dist/core/lbug/lbug-adapter.d.ts +103 -0
  156. package/dist/core/lbug/lbug-adapter.js +769 -0
  157. package/dist/core/lbug/schema.d.ts +53 -0
  158. package/dist/core/lbug/schema.js +430 -0
  159. package/dist/core/search/bm25-index.d.ts +23 -0
  160. package/dist/core/search/bm25-index.js +96 -0
  161. package/dist/core/search/hybrid-search.d.ts +49 -0
  162. package/dist/core/search/hybrid-search.js +118 -0
  163. package/dist/core/tree-sitter/parser-loader.d.ts +5 -0
  164. package/dist/core/tree-sitter/parser-loader.js +63 -0
  165. package/dist/core/wiki/generator.d.ts +120 -0
  166. package/dist/core/wiki/generator.js +939 -0
  167. package/dist/core/wiki/graph-queries.d.ts +80 -0
  168. package/dist/core/wiki/graph-queries.js +238 -0
  169. package/dist/core/wiki/html-viewer.d.ts +10 -0
  170. package/dist/core/wiki/html-viewer.js +297 -0
  171. package/dist/core/wiki/llm-client.d.ts +43 -0
  172. package/dist/core/wiki/llm-client.js +186 -0
  173. package/dist/core/wiki/prompts.d.ts +53 -0
  174. package/dist/core/wiki/prompts.js +174 -0
  175. package/dist/lib/utils.d.ts +1 -0
  176. package/dist/lib/utils.js +3 -0
  177. package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
  178. package/dist/mcp/compatible-stdio-transport.js +200 -0
  179. package/dist/mcp/core/embedder.d.ts +27 -0
  180. package/dist/mcp/core/embedder.js +108 -0
  181. package/dist/mcp/core/lbug-adapter.d.ts +57 -0
  182. package/dist/mcp/core/lbug-adapter.js +455 -0
  183. package/dist/mcp/local/local-backend.d.ts +181 -0
  184. package/dist/mcp/local/local-backend.js +1722 -0
  185. package/dist/mcp/resources.d.ts +31 -0
  186. package/dist/mcp/resources.js +411 -0
  187. package/dist/mcp/server.d.ts +23 -0
  188. package/dist/mcp/server.js +296 -0
  189. package/dist/mcp/staleness.d.ts +15 -0
  190. package/dist/mcp/staleness.js +29 -0
  191. package/dist/mcp/tools.d.ts +24 -0
  192. package/dist/mcp/tools.js +292 -0
  193. package/dist/server/api.d.ts +10 -0
  194. package/dist/server/api.js +344 -0
  195. package/dist/server/mcp-http.d.ts +13 -0
  196. package/dist/server/mcp-http.js +100 -0
  197. package/dist/storage/git.d.ts +6 -0
  198. package/dist/storage/git.js +35 -0
  199. package/dist/storage/repo-manager.d.ts +138 -0
  200. package/dist/storage/repo-manager.js +299 -0
  201. package/dist/types/pipeline.d.ts +32 -0
  202. package/dist/types/pipeline.js +18 -0
  203. package/dist/unreal/bridge.d.ts +4 -0
  204. package/dist/unreal/bridge.js +113 -0
  205. package/dist/unreal/config.d.ts +6 -0
  206. package/dist/unreal/config.js +55 -0
  207. package/dist/unreal/types.d.ts +105 -0
  208. package/dist/unreal/types.js +1 -0
  209. package/hooks/claude/gitnexus-hook.cjs +238 -0
  210. package/hooks/claude/pre-tool-use.sh +79 -0
  211. package/hooks/claude/session-start.sh +42 -0
  212. package/package.json +100 -0
  213. package/scripts/ensure-cli-executable.cjs +21 -0
  214. package/scripts/patch-tree-sitter-swift.cjs +74 -0
  215. package/scripts/setup-unreal-gitnexus.ps1 +191 -0
  216. package/skills/gitnexus-cli.md +82 -0
  217. package/skills/gitnexus-debugging.md +89 -0
  218. package/skills/gitnexus-exploring.md +78 -0
  219. package/skills/gitnexus-guide.md +64 -0
  220. package/skills/gitnexus-impact-analysis.md +97 -0
  221. package/skills/gitnexus-pr-review.md +163 -0
  222. package/skills/gitnexus-refactoring.md +121 -0
  223. package/vendor/leiden/index.cjs +355 -0
  224. package/vendor/leiden/utils.cjs +392 -0
@@ -0,0 +1,344 @@
1
+ /**
2
+ * HTTP API Server
3
+ *
4
+ * REST API for browser-based clients to query the local .gitnexus/ index.
5
+ * Also hosts the MCP server over StreamableHTTP for remote AI tool access.
6
+ *
7
+ * Security: binds to 127.0.0.1 by default (use --host to override).
8
+ * CORS is restricted to localhost and the deployed site.
9
+ */
10
+ import express from 'express';
11
+ import cors from 'cors';
12
+ import path from 'path';
13
+ import fs from 'fs/promises';
14
+ import { loadMeta, listRegisteredRepos } from '../storage/repo-manager.js';
15
+ import { executeQuery, closeLbug, withLbugDb } from '../core/lbug/lbug-adapter.js';
16
+ import { NODE_TABLES } from '../core/lbug/schema.js';
17
+ import { searchFTSFromLbug } from '../core/search/bm25-index.js';
18
+ import { hybridSearch } from '../core/search/hybrid-search.js';
19
+ // Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
20
+ // at server startup — crashes on unsupported Node ABI versions (#89)
21
+ import { LocalBackend } from '../mcp/local/local-backend.js';
22
+ import { mountMCPEndpoints } from './mcp-http.js';
23
+ const buildGraph = async () => {
24
+ const nodes = [];
25
+ for (const table of NODE_TABLES) {
26
+ try {
27
+ let query = '';
28
+ if (table === 'File') {
29
+ query = `MATCH (n:File) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.content AS content`;
30
+ }
31
+ else if (table === 'Folder') {
32
+ query = `MATCH (n:Folder) RETURN n.id AS id, n.name AS name, n.filePath AS filePath`;
33
+ }
34
+ else if (table === 'Community') {
35
+ query = `MATCH (n:Community) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.cohesion AS cohesion, n.symbolCount AS symbolCount`;
36
+ }
37
+ else if (table === 'Process') {
38
+ query = `MATCH (n:Process) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.processType AS processType, n.stepCount AS stepCount, n.communities AS communities, n.entryPointId AS entryPointId, n.terminalId AS terminalId`;
39
+ }
40
+ else {
41
+ query = `MATCH (n:${table}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine, n.content AS content`;
42
+ }
43
+ const rows = await executeQuery(query);
44
+ for (const row of rows) {
45
+ nodes.push({
46
+ id: row.id ?? row[0],
47
+ label: table,
48
+ properties: {
49
+ name: row.name ?? row.label ?? row[1],
50
+ filePath: row.filePath ?? row[2],
51
+ startLine: row.startLine,
52
+ endLine: row.endLine,
53
+ content: row.content,
54
+ heuristicLabel: row.heuristicLabel,
55
+ cohesion: row.cohesion,
56
+ symbolCount: row.symbolCount,
57
+ processType: row.processType,
58
+ stepCount: row.stepCount,
59
+ communities: row.communities,
60
+ entryPointId: row.entryPointId,
61
+ terminalId: row.terminalId,
62
+ },
63
+ });
64
+ }
65
+ }
66
+ catch {
67
+ // ignore empty tables
68
+ }
69
+ }
70
+ const relationships = [];
71
+ const relRows = await executeQuery(`MATCH (a)-[r:CodeRelation]->(b) RETURN a.id AS sourceId, b.id AS targetId, r.type AS type, r.confidence AS confidence, r.reason AS reason, r.step AS step`);
72
+ for (const row of relRows) {
73
+ relationships.push({
74
+ id: `${row.sourceId}_${row.type}_${row.targetId}`,
75
+ type: row.type,
76
+ sourceId: row.sourceId,
77
+ targetId: row.targetId,
78
+ confidence: row.confidence,
79
+ reason: row.reason,
80
+ step: row.step,
81
+ });
82
+ }
83
+ return { nodes, relationships };
84
+ };
85
+ const statusFromError = (err) => {
86
+ const msg = String(err?.message ?? '');
87
+ if (msg.includes('No indexed repositories') || msg.includes('not found'))
88
+ return 404;
89
+ if (msg.includes('Multiple repositories'))
90
+ return 400;
91
+ return 500;
92
+ };
93
+ const requestedRepo = (req) => {
94
+ const fromQuery = typeof req.query.repo === 'string' ? req.query.repo : undefined;
95
+ if (fromQuery)
96
+ return fromQuery;
97
+ if (req.body && typeof req.body === 'object' && typeof req.body.repo === 'string') {
98
+ return req.body.repo;
99
+ }
100
+ return undefined;
101
+ };
102
+ export const createServer = async (port, host = '127.0.0.1') => {
103
+ const app = express();
104
+ // CORS: only allow localhost origins and the deployed site.
105
+ // Non-browser requests (curl, server-to-server) have no origin and are allowed.
106
+ app.use(cors({
107
+ origin: (origin, callback) => {
108
+ if (!origin
109
+ || origin.startsWith('http://localhost:')
110
+ || origin.startsWith('http://127.0.0.1:')
111
+ || origin === 'https://gitnexus.vercel.app') {
112
+ callback(null, true);
113
+ }
114
+ else {
115
+ callback(new Error('Not allowed by CORS'));
116
+ }
117
+ }
118
+ }));
119
+ app.use(express.json({ limit: '10mb' }));
120
+ // Initialize MCP backend (multi-repo, shared across all MCP sessions)
121
+ const backend = new LocalBackend();
122
+ await backend.init();
123
+ const cleanupMcp = mountMCPEndpoints(app, backend);
124
+ // Helper: resolve a repo by name from the global registry, or default to first
125
+ const resolveRepo = async (repoName) => {
126
+ const repos = await listRegisteredRepos();
127
+ if (repos.length === 0)
128
+ return null;
129
+ if (repoName)
130
+ return repos.find(r => r.name === repoName) || null;
131
+ return repos[0]; // default to first
132
+ };
133
+ // List all registered repos
134
+ app.get('/api/repos', async (_req, res) => {
135
+ try {
136
+ const repos = await listRegisteredRepos();
137
+ res.json(repos.map(r => ({
138
+ name: r.name, path: r.path, indexedAt: r.indexedAt,
139
+ lastCommit: r.lastCommit, stats: r.stats,
140
+ })));
141
+ }
142
+ catch (err) {
143
+ res.status(500).json({ error: err.message || 'Failed to list repos' });
144
+ }
145
+ });
146
+ // Get repo info
147
+ app.get('/api/repo', async (req, res) => {
148
+ try {
149
+ const entry = await resolveRepo(requestedRepo(req));
150
+ if (!entry) {
151
+ res.status(404).json({ error: 'Repository not found. Run: gitnexus analyze' });
152
+ return;
153
+ }
154
+ const meta = await loadMeta(entry.storagePath);
155
+ res.json({
156
+ name: entry.name,
157
+ repoPath: entry.path,
158
+ indexedAt: meta?.indexedAt ?? entry.indexedAt,
159
+ stats: meta?.stats ?? entry.stats ?? {},
160
+ });
161
+ }
162
+ catch (err) {
163
+ res.status(500).json({ error: err.message || 'Failed to get repo info' });
164
+ }
165
+ });
166
+ // Get full graph
167
+ app.get('/api/graph', async (req, res) => {
168
+ try {
169
+ const entry = await resolveRepo(requestedRepo(req));
170
+ if (!entry) {
171
+ res.status(404).json({ error: 'Repository not found' });
172
+ return;
173
+ }
174
+ const lbugPath = path.join(entry.storagePath, 'lbug');
175
+ const graph = await withLbugDb(lbugPath, async () => buildGraph());
176
+ res.json(graph);
177
+ }
178
+ catch (err) {
179
+ res.status(500).json({ error: err.message || 'Failed to build graph' });
180
+ }
181
+ });
182
+ // Execute Cypher query
183
+ app.post('/api/query', async (req, res) => {
184
+ try {
185
+ const cypher = req.body.cypher;
186
+ if (!cypher) {
187
+ res.status(400).json({ error: 'Missing "cypher" in request body' });
188
+ return;
189
+ }
190
+ const entry = await resolveRepo(requestedRepo(req));
191
+ if (!entry) {
192
+ res.status(404).json({ error: 'Repository not found' });
193
+ return;
194
+ }
195
+ const lbugPath = path.join(entry.storagePath, 'lbug');
196
+ const result = await withLbugDb(lbugPath, () => executeQuery(cypher));
197
+ res.json({ result });
198
+ }
199
+ catch (err) {
200
+ res.status(500).json({ error: err.message || 'Query failed' });
201
+ }
202
+ });
203
+ // Search
204
+ app.post('/api/search', async (req, res) => {
205
+ try {
206
+ const query = (req.body.query ?? '').trim();
207
+ if (!query) {
208
+ res.status(400).json({ error: 'Missing "query" in request body' });
209
+ return;
210
+ }
211
+ const entry = await resolveRepo(requestedRepo(req));
212
+ if (!entry) {
213
+ res.status(404).json({ error: 'Repository not found' });
214
+ return;
215
+ }
216
+ const lbugPath = path.join(entry.storagePath, 'lbug');
217
+ const parsedLimit = Number(req.body.limit ?? 10);
218
+ const limit = Number.isFinite(parsedLimit)
219
+ ? Math.max(1, Math.min(100, Math.trunc(parsedLimit)))
220
+ : 10;
221
+ const results = await withLbugDb(lbugPath, async () => {
222
+ const { isEmbedderReady } = await import('../core/embeddings/embedder.js');
223
+ if (isEmbedderReady()) {
224
+ const { semanticSearch } = await import('../core/embeddings/embedding-pipeline.js');
225
+ return hybridSearch(query, limit, executeQuery, semanticSearch);
226
+ }
227
+ // FTS-only fallback when embeddings aren't loaded
228
+ return searchFTSFromLbug(query, limit);
229
+ });
230
+ res.json({ results });
231
+ }
232
+ catch (err) {
233
+ res.status(500).json({ error: err.message || 'Search failed' });
234
+ }
235
+ });
236
+ // Read file — with path traversal guard
237
+ app.get('/api/file', async (req, res) => {
238
+ try {
239
+ const entry = await resolveRepo(requestedRepo(req));
240
+ if (!entry) {
241
+ res.status(404).json({ error: 'Repository not found' });
242
+ return;
243
+ }
244
+ const filePath = req.query.path;
245
+ if (!filePath) {
246
+ res.status(400).json({ error: 'Missing path' });
247
+ return;
248
+ }
249
+ // Prevent path traversal — resolve and verify the path stays within the repo root
250
+ const repoRoot = path.resolve(entry.path);
251
+ const fullPath = path.resolve(repoRoot, filePath);
252
+ if (!fullPath.startsWith(repoRoot + path.sep) && fullPath !== repoRoot) {
253
+ res.status(403).json({ error: 'Path traversal denied' });
254
+ return;
255
+ }
256
+ const content = await fs.readFile(fullPath, 'utf-8');
257
+ res.json({ content });
258
+ }
259
+ catch (err) {
260
+ if (err.code === 'ENOENT') {
261
+ res.status(404).json({ error: 'File not found' });
262
+ }
263
+ else {
264
+ res.status(500).json({ error: err.message || 'Failed to read file' });
265
+ }
266
+ }
267
+ });
268
+ // List all processes
269
+ app.get('/api/processes', async (req, res) => {
270
+ try {
271
+ const result = await backend.queryProcesses(requestedRepo(req));
272
+ res.json(result);
273
+ }
274
+ catch (err) {
275
+ res.status(statusFromError(err)).json({ error: err.message || 'Failed to query processes' });
276
+ }
277
+ });
278
+ // Process detail
279
+ app.get('/api/process', async (req, res) => {
280
+ try {
281
+ const name = String(req.query.name ?? '').trim();
282
+ if (!name) {
283
+ res.status(400).json({ error: 'Missing "name" query parameter' });
284
+ return;
285
+ }
286
+ const result = await backend.queryProcessDetail(name, requestedRepo(req));
287
+ if (result?.error) {
288
+ res.status(404).json({ error: result.error });
289
+ return;
290
+ }
291
+ res.json(result);
292
+ }
293
+ catch (err) {
294
+ res.status(statusFromError(err)).json({ error: err.message || 'Failed to query process detail' });
295
+ }
296
+ });
297
+ // List all clusters
298
+ app.get('/api/clusters', async (req, res) => {
299
+ try {
300
+ const result = await backend.queryClusters(requestedRepo(req));
301
+ res.json(result);
302
+ }
303
+ catch (err) {
304
+ res.status(statusFromError(err)).json({ error: err.message || 'Failed to query clusters' });
305
+ }
306
+ });
307
+ // Cluster detail
308
+ app.get('/api/cluster', async (req, res) => {
309
+ try {
310
+ const name = String(req.query.name ?? '').trim();
311
+ if (!name) {
312
+ res.status(400).json({ error: 'Missing "name" query parameter' });
313
+ return;
314
+ }
315
+ const result = await backend.queryClusterDetail(name, requestedRepo(req));
316
+ if (result?.error) {
317
+ res.status(404).json({ error: result.error });
318
+ return;
319
+ }
320
+ res.json(result);
321
+ }
322
+ catch (err) {
323
+ res.status(statusFromError(err)).json({ error: err.message || 'Failed to query cluster detail' });
324
+ }
325
+ });
326
+ // Global error handler — catch anything the route handlers miss
327
+ app.use((err, _req, res, _next) => {
328
+ console.error('Unhandled error:', err);
329
+ res.status(500).json({ error: 'Internal server error' });
330
+ });
331
+ const server = app.listen(port, host, () => {
332
+ console.log(`GitNexus server running on http://${host}:${port}`);
333
+ });
334
+ // Graceful shutdown — close Express + LadybugDB cleanly
335
+ const shutdown = async () => {
336
+ server.close();
337
+ await cleanupMcp();
338
+ await closeLbug();
339
+ await backend.disconnect();
340
+ process.exit(0);
341
+ };
342
+ process.once('SIGINT', shutdown);
343
+ process.once('SIGTERM', shutdown);
344
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * MCP over HTTP
3
+ *
4
+ * Mounts the GitNexus MCP server on Express using StreamableHTTP transport.
5
+ * Each connecting client gets its own stateful session; the LocalBackend
6
+ * is shared across all sessions (thread-safe — lazy LadybugDB per repo).
7
+ *
8
+ * Sessions are cleaned up on explicit close or after SESSION_TTL_MS of inactivity
9
+ * (guards against network drops that never trigger onclose).
10
+ */
11
+ import type { Express } from 'express';
12
+ import type { LocalBackend } from '../mcp/local/local-backend.js';
13
+ export declare function mountMCPEndpoints(app: Express, backend: LocalBackend): () => Promise<void>;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * MCP over HTTP
3
+ *
4
+ * Mounts the GitNexus MCP server on Express using StreamableHTTP transport.
5
+ * Each connecting client gets its own stateful session; the LocalBackend
6
+ * is shared across all sessions (thread-safe — lazy LadybugDB per repo).
7
+ *
8
+ * Sessions are cleaned up on explicit close or after SESSION_TTL_MS of inactivity
9
+ * (guards against network drops that never trigger onclose).
10
+ */
11
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
12
+ import { createMCPServer } from '../mcp/server.js';
13
+ import { randomUUID } from 'crypto';
14
+ /** Idle sessions are evicted after 30 minutes */
15
+ const SESSION_TTL_MS = 30 * 60 * 1000;
16
+ /** Cleanup sweep runs every 5 minutes */
17
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
18
+ export function mountMCPEndpoints(app, backend) {
19
+ const sessions = new Map();
20
+ // Periodic cleanup of idle sessions (guards against network drops)
21
+ const cleanupTimer = setInterval(() => {
22
+ const now = Date.now();
23
+ for (const [id, session] of sessions) {
24
+ if (now - session.lastActivity > SESSION_TTL_MS) {
25
+ try {
26
+ session.server.close();
27
+ }
28
+ catch { }
29
+ sessions.delete(id);
30
+ }
31
+ }
32
+ }, CLEANUP_INTERVAL_MS);
33
+ if (cleanupTimer && typeof cleanupTimer === 'object' && 'unref' in cleanupTimer) {
34
+ cleanupTimer.unref();
35
+ }
36
+ const handleMcpRequest = async (req, res) => {
37
+ const sessionId = req.headers['mcp-session-id'];
38
+ if (sessionId && sessions.has(sessionId)) {
39
+ // Existing session — delegate to its transport
40
+ const session = sessions.get(sessionId);
41
+ session.lastActivity = Date.now();
42
+ await session.transport.handleRequest(req, res, req.body);
43
+ }
44
+ else if (sessionId) {
45
+ // Unknown/expired session ID — tell client to re-initialize (per MCP spec)
46
+ res.status(404).json({
47
+ jsonrpc: '2.0',
48
+ error: { code: -32001, message: 'Session not found. Re-initialize.' },
49
+ id: null,
50
+ });
51
+ }
52
+ else if (req.method === 'POST') {
53
+ // No session ID — new client initializing
54
+ const transport = new StreamableHTTPServerTransport({
55
+ sessionIdGenerator: () => randomUUID(),
56
+ });
57
+ const server = createMCPServer(backend);
58
+ await server.connect(transport);
59
+ await transport.handleRequest(req, res, req.body);
60
+ if (transport.sessionId) {
61
+ sessions.set(transport.sessionId, { server, transport, lastActivity: Date.now() });
62
+ transport.onclose = () => {
63
+ sessions.delete(transport.sessionId);
64
+ };
65
+ }
66
+ }
67
+ else {
68
+ res.status(400).json({
69
+ jsonrpc: '2.0',
70
+ error: { code: -32000, message: 'No valid session. Send a POST to initialize.' },
71
+ id: null,
72
+ });
73
+ }
74
+ };
75
+ app.all('/api/mcp', (req, res) => {
76
+ void handleMcpRequest(req, res).catch((err) => {
77
+ console.error('MCP HTTP request failed:', err);
78
+ if (res.headersSent)
79
+ return;
80
+ res.status(500).json({
81
+ jsonrpc: '2.0',
82
+ error: { code: -32000, message: 'Internal MCP server error' },
83
+ id: null,
84
+ });
85
+ });
86
+ });
87
+ const cleanup = async () => {
88
+ clearInterval(cleanupTimer);
89
+ const closers = [...sessions.values()].map(async (session) => {
90
+ try {
91
+ await Promise.resolve(session.server.close());
92
+ }
93
+ catch { }
94
+ });
95
+ sessions.clear();
96
+ await Promise.allSettled(closers);
97
+ };
98
+ console.log('MCP HTTP endpoints mounted at /api/mcp');
99
+ return cleanup;
100
+ }
@@ -0,0 +1,6 @@
1
+ export declare const isGitRepo: (repoPath: string) => boolean;
2
+ export declare const getCurrentCommit: (repoPath: string) => string;
3
+ /**
4
+ * Find the git repository root from any path inside the repo
5
+ */
6
+ export declare const getGitRoot: (fromPath: string) => string | null;
@@ -0,0 +1,35 @@
1
+ import { execSync } from 'child_process';
2
+ import path from 'path';
3
+ // Git utilities for repository detection, commit tracking, and diff analysis
4
+ export const isGitRepo = (repoPath) => {
5
+ try {
6
+ execSync('git rev-parse --is-inside-work-tree', { cwd: repoPath, stdio: 'ignore' });
7
+ return true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ };
13
+ export const getCurrentCommit = (repoPath) => {
14
+ try {
15
+ return execSync('git rev-parse HEAD', { cwd: repoPath }).toString().trim();
16
+ }
17
+ catch {
18
+ return '';
19
+ }
20
+ };
21
+ /**
22
+ * Find the git repository root from any path inside the repo
23
+ */
24
+ export const getGitRoot = (fromPath) => {
25
+ try {
26
+ const raw = execSync('git rev-parse --show-toplevel', { cwd: fromPath })
27
+ .toString()
28
+ .trim();
29
+ // On Windows, git returns /d/Projects/Foo — path.resolve normalizes to D:\Projects\Foo
30
+ return path.resolve(raw);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Repository Manager
3
+ *
4
+ * Manages GitNexus index storage in .gitnexus/ at repo root.
5
+ * Also maintains a global registry at ~/.gitnexus/registry.json
6
+ * so the MCP server can discover indexed repos from any cwd.
7
+ */
8
+ export interface RepoMeta {
9
+ repoPath: string;
10
+ lastCommit: string;
11
+ indexedAt: string;
12
+ stats?: {
13
+ files?: number;
14
+ nodes?: number;
15
+ edges?: number;
16
+ communities?: number;
17
+ processes?: number;
18
+ embeddings?: number;
19
+ };
20
+ }
21
+ export interface IndexedRepo {
22
+ repoPath: string;
23
+ storagePath: string;
24
+ lbugPath: string;
25
+ metaPath: string;
26
+ meta: RepoMeta;
27
+ }
28
+ /**
29
+ * Shape of an entry in the global registry (~/.gitnexus/registry.json)
30
+ */
31
+ export interface RegistryEntry {
32
+ name: string;
33
+ path: string;
34
+ storagePath: string;
35
+ indexedAt: string;
36
+ lastCommit: string;
37
+ stats?: RepoMeta['stats'];
38
+ }
39
+ /**
40
+ * Get the .gitnexus storage path for a repository
41
+ */
42
+ export declare const getStoragePath: (repoPath: string) => string;
43
+ /**
44
+ * Get paths to key storage files
45
+ */
46
+ export declare const getStoragePaths: (repoPath: string) => {
47
+ storagePath: string;
48
+ lbugPath: string;
49
+ metaPath: string;
50
+ };
51
+ /**
52
+ * Check whether a KuzuDB index exists in the given storage path.
53
+ * Non-destructive — safe to call from status commands.
54
+ */
55
+ export declare const hasKuzuIndex: (storagePath: string) => Promise<boolean>;
56
+ /**
57
+ * Clean up stale KuzuDB files after migration to LadybugDB.
58
+ *
59
+ * Returns:
60
+ * found — true if .gitnexus/kuzu existed and was deleted
61
+ * needsReindex — true if kuzu existed but lbug does not (re-analyze required)
62
+ *
63
+ * Callers own the user-facing messaging; this function only deletes files.
64
+ */
65
+ export declare const cleanupOldKuzuFiles: (storagePath: string) => Promise<{
66
+ found: boolean;
67
+ needsReindex: boolean;
68
+ }>;
69
+ /**
70
+ * Load metadata from an indexed repo
71
+ */
72
+ export declare const loadMeta: (storagePath: string) => Promise<RepoMeta | null>;
73
+ /**
74
+ * Save metadata to storage
75
+ */
76
+ export declare const saveMeta: (storagePath: string, meta: RepoMeta) => Promise<void>;
77
+ /**
78
+ * Check if a path has a GitNexus index
79
+ */
80
+ export declare const hasIndex: (repoPath: string) => Promise<boolean>;
81
+ /**
82
+ * Load an indexed repo from a path
83
+ */
84
+ export declare const loadRepo: (repoPath: string) => Promise<IndexedRepo | null>;
85
+ /**
86
+ * Find .gitnexus by walking up from a starting path
87
+ */
88
+ export declare const findRepo: (startPath: string) => Promise<IndexedRepo | null>;
89
+ /**
90
+ * Add .gitnexus to .gitignore if not already present
91
+ */
92
+ export declare const addToGitignore: (repoPath: string) => Promise<void>;
93
+ /**
94
+ * Get the path to the global GitNexus directory
95
+ */
96
+ export declare const getGlobalDir: () => string;
97
+ /**
98
+ * Get the path to the global registry file
99
+ */
100
+ export declare const getGlobalRegistryPath: () => string;
101
+ /**
102
+ * Read the global registry. Returns empty array if not found.
103
+ */
104
+ export declare const readRegistry: () => Promise<RegistryEntry[]>;
105
+ /**
106
+ * Register (add or update) a repo in the global registry.
107
+ * Called after `gitnexus analyze` completes.
108
+ */
109
+ export declare const registerRepo: (repoPath: string, meta: RepoMeta) => Promise<void>;
110
+ /**
111
+ * Remove a repo from the global registry.
112
+ * Called after `gitnexus clean`.
113
+ */
114
+ export declare const unregisterRepo: (repoPath: string) => Promise<void>;
115
+ /**
116
+ * List all registered repos from the global registry.
117
+ * Optionally validates that each entry's .gitnexus/ still exists.
118
+ */
119
+ export declare const listRegisteredRepos: (opts?: {
120
+ validate?: boolean;
121
+ }) => Promise<RegistryEntry[]>;
122
+ export interface CLIConfig {
123
+ apiKey?: string;
124
+ model?: string;
125
+ baseUrl?: string;
126
+ }
127
+ /**
128
+ * Get the path to the global CLI config file
129
+ */
130
+ export declare const getGlobalConfigPath: () => string;
131
+ /**
132
+ * Load CLI config from ~/.gitnexus/config.json
133
+ */
134
+ export declare const loadCLIConfig: () => Promise<CLIConfig>;
135
+ /**
136
+ * Save CLI config to ~/.gitnexus/config.json
137
+ */
138
+ export declare const saveCLIConfig: (config: CLIConfig) => Promise<void>;