@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.
- package/LICENSE +73 -0
- package/README.md +261 -0
- package/dist/cli/ai-context.d.ts +23 -0
- package/dist/cli/ai-context.js +265 -0
- package/dist/cli/analyze.d.ts +12 -0
- package/dist/cli/analyze.js +345 -0
- package/dist/cli/augment.d.ts +13 -0
- package/dist/cli/augment.js +33 -0
- package/dist/cli/clean.d.ts +10 -0
- package/dist/cli/clean.js +60 -0
- package/dist/cli/eval-server.d.ts +37 -0
- package/dist/cli/eval-server.js +389 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +137 -0
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +18 -0
- package/dist/cli/list.d.ts +6 -0
- package/dist/cli/list.js +30 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +36 -0
- package/dist/cli/serve.d.ts +4 -0
- package/dist/cli/serve.js +6 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +367 -0
- package/dist/cli/sipher-patched.d.ts +2 -0
- package/dist/cli/sipher-patched.js +77 -0
- package/dist/cli/skill-gen.d.ts +26 -0
- package/dist/cli/skill-gen.js +549 -0
- package/dist/cli/status.d.ts +6 -0
- package/dist/cli/status.js +36 -0
- package/dist/cli/tool.d.ts +60 -0
- package/dist/cli/tool.js +180 -0
- package/dist/cli/wiki.d.ts +15 -0
- package/dist/cli/wiki.js +365 -0
- package/dist/config/ignore-service.d.ts +26 -0
- package/dist/config/ignore-service.js +284 -0
- package/dist/config/supported-languages.d.ts +15 -0
- package/dist/config/supported-languages.js +16 -0
- package/dist/core/augmentation/engine.d.ts +26 -0
- package/dist/core/augmentation/engine.js +240 -0
- package/dist/core/embeddings/embedder.d.ts +60 -0
- package/dist/core/embeddings/embedder.js +251 -0
- package/dist/core/embeddings/embedding-pipeline.d.ts +51 -0
- package/dist/core/embeddings/embedding-pipeline.js +356 -0
- package/dist/core/embeddings/index.d.ts +9 -0
- package/dist/core/embeddings/index.js +9 -0
- package/dist/core/embeddings/text-generator.d.ts +24 -0
- package/dist/core/embeddings/text-generator.js +182 -0
- package/dist/core/embeddings/types.d.ts +87 -0
- package/dist/core/embeddings/types.js +32 -0
- package/dist/core/graph/graph.d.ts +2 -0
- package/dist/core/graph/graph.js +66 -0
- package/dist/core/graph/types.d.ts +66 -0
- package/dist/core/graph/types.js +1 -0
- package/dist/core/ingestion/ast-cache.d.ts +11 -0
- package/dist/core/ingestion/ast-cache.js +35 -0
- package/dist/core/ingestion/call-processor.d.ts +23 -0
- package/dist/core/ingestion/call-processor.js +793 -0
- package/dist/core/ingestion/call-routing.d.ts +68 -0
- package/dist/core/ingestion/call-routing.js +129 -0
- package/dist/core/ingestion/cluster-enricher.d.ts +38 -0
- package/dist/core/ingestion/cluster-enricher.js +170 -0
- package/dist/core/ingestion/community-processor.d.ts +39 -0
- package/dist/core/ingestion/community-processor.js +312 -0
- package/dist/core/ingestion/constants.d.ts +16 -0
- package/dist/core/ingestion/constants.js +16 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +40 -0
- package/dist/core/ingestion/entry-point-scoring.js +353 -0
- package/dist/core/ingestion/export-detection.d.ts +18 -0
- package/dist/core/ingestion/export-detection.js +231 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +28 -0
- package/dist/core/ingestion/filesystem-walker.js +81 -0
- package/dist/core/ingestion/framework-detection.d.ts +54 -0
- package/dist/core/ingestion/framework-detection.js +411 -0
- package/dist/core/ingestion/heritage-processor.d.ts +28 -0
- package/dist/core/ingestion/heritage-processor.js +251 -0
- package/dist/core/ingestion/import-processor.d.ts +34 -0
- package/dist/core/ingestion/import-processor.js +398 -0
- package/dist/core/ingestion/language-config.d.ts +46 -0
- package/dist/core/ingestion/language-config.js +167 -0
- package/dist/core/ingestion/mro-processor.d.ts +45 -0
- package/dist/core/ingestion/mro-processor.js +369 -0
- package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
- package/dist/core/ingestion/named-binding-extraction.js +363 -0
- package/dist/core/ingestion/parsing-processor.d.ts +19 -0
- package/dist/core/ingestion/parsing-processor.js +315 -0
- package/dist/core/ingestion/pipeline.d.ts +6 -0
- package/dist/core/ingestion/pipeline.js +401 -0
- package/dist/core/ingestion/process-processor.d.ts +51 -0
- package/dist/core/ingestion/process-processor.js +315 -0
- package/dist/core/ingestion/resolution-context.d.ts +53 -0
- package/dist/core/ingestion/resolution-context.js +132 -0
- package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
- package/dist/core/ingestion/resolvers/csharp.js +109 -0
- package/dist/core/ingestion/resolvers/go.d.ts +19 -0
- package/dist/core/ingestion/resolvers/go.js +42 -0
- package/dist/core/ingestion/resolvers/index.d.ts +18 -0
- package/dist/core/ingestion/resolvers/index.js +13 -0
- package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
- package/dist/core/ingestion/resolvers/jvm.js +87 -0
- package/dist/core/ingestion/resolvers/php.d.ts +15 -0
- package/dist/core/ingestion/resolvers/php.js +35 -0
- package/dist/core/ingestion/resolvers/python.d.ts +19 -0
- package/dist/core/ingestion/resolvers/python.js +52 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
- package/dist/core/ingestion/resolvers/ruby.js +15 -0
- package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
- package/dist/core/ingestion/resolvers/rust.js +73 -0
- package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
- package/dist/core/ingestion/resolvers/standard.js +123 -0
- package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
- package/dist/core/ingestion/resolvers/utils.js +122 -0
- package/dist/core/ingestion/structure-processor.d.ts +2 -0
- package/dist/core/ingestion/structure-processor.js +36 -0
- package/dist/core/ingestion/symbol-table.d.ts +63 -0
- package/dist/core/ingestion/symbol-table.js +85 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +15 -0
- package/dist/core/ingestion/tree-sitter-queries.js +888 -0
- package/dist/core/ingestion/type-env.d.ts +49 -0
- package/dist/core/ingestion/type-env.js +613 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/csharp.js +383 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/go.js +467 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
- package/dist/core/ingestion/type-extractors/index.js +31 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
- package/dist/core/ingestion/type-extractors/jvm.js +681 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/php.js +549 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/python.js +455 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/ruby.js +389 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/rust.js +456 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +145 -0
- package/dist/core/ingestion/type-extractors/shared.js +810 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/swift.js +137 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
- package/dist/core/ingestion/type-extractors/types.js +1 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/typescript.js +494 -0
- package/dist/core/ingestion/utils.d.ts +138 -0
- package/dist/core/ingestion/utils.js +1290 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +122 -0
- package/dist/core/ingestion/workers/parse-worker.js +1126 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +16 -0
- package/dist/core/ingestion/workers/worker-pool.js +128 -0
- package/dist/core/lbug/csv-generator.d.ts +33 -0
- package/dist/core/lbug/csv-generator.js +366 -0
- package/dist/core/lbug/lbug-adapter.d.ts +103 -0
- package/dist/core/lbug/lbug-adapter.js +769 -0
- package/dist/core/lbug/schema.d.ts +53 -0
- package/dist/core/lbug/schema.js +430 -0
- package/dist/core/search/bm25-index.d.ts +23 -0
- package/dist/core/search/bm25-index.js +96 -0
- package/dist/core/search/hybrid-search.d.ts +49 -0
- package/dist/core/search/hybrid-search.js +118 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +5 -0
- package/dist/core/tree-sitter/parser-loader.js +63 -0
- package/dist/core/wiki/generator.d.ts +120 -0
- package/dist/core/wiki/generator.js +939 -0
- package/dist/core/wiki/graph-queries.d.ts +80 -0
- package/dist/core/wiki/graph-queries.js +238 -0
- package/dist/core/wiki/html-viewer.d.ts +10 -0
- package/dist/core/wiki/html-viewer.js +297 -0
- package/dist/core/wiki/llm-client.d.ts +43 -0
- package/dist/core/wiki/llm-client.js +186 -0
- package/dist/core/wiki/prompts.d.ts +53 -0
- package/dist/core/wiki/prompts.js +174 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +3 -0
- package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
- package/dist/mcp/compatible-stdio-transport.js +200 -0
- package/dist/mcp/core/embedder.d.ts +27 -0
- package/dist/mcp/core/embedder.js +108 -0
- package/dist/mcp/core/lbug-adapter.d.ts +57 -0
- package/dist/mcp/core/lbug-adapter.js +455 -0
- package/dist/mcp/local/local-backend.d.ts +181 -0
- package/dist/mcp/local/local-backend.js +1722 -0
- package/dist/mcp/resources.d.ts +31 -0
- package/dist/mcp/resources.js +411 -0
- package/dist/mcp/server.d.ts +23 -0
- package/dist/mcp/server.js +296 -0
- package/dist/mcp/staleness.d.ts +15 -0
- package/dist/mcp/staleness.js +29 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/mcp/tools.js +292 -0
- package/dist/server/api.d.ts +10 -0
- package/dist/server/api.js +344 -0
- package/dist/server/mcp-http.d.ts +13 -0
- package/dist/server/mcp-http.js +100 -0
- package/dist/storage/git.d.ts +6 -0
- package/dist/storage/git.js +35 -0
- package/dist/storage/repo-manager.d.ts +138 -0
- package/dist/storage/repo-manager.js +299 -0
- package/dist/types/pipeline.d.ts +32 -0
- package/dist/types/pipeline.js +18 -0
- package/dist/unreal/bridge.d.ts +4 -0
- package/dist/unreal/bridge.js +113 -0
- package/dist/unreal/config.d.ts +6 -0
- package/dist/unreal/config.js +55 -0
- package/dist/unreal/types.d.ts +105 -0
- package/dist/unreal/types.js +1 -0
- package/hooks/claude/gitnexus-hook.cjs +238 -0
- package/hooks/claude/pre-tool-use.sh +79 -0
- package/hooks/claude/session-start.sh +42 -0
- package/package.json +100 -0
- package/scripts/ensure-cli-executable.cjs +21 -0
- package/scripts/patch-tree-sitter-swift.cjs +74 -0
- package/scripts/setup-unreal-gitnexus.ps1 +191 -0
- package/skills/gitnexus-cli.md +82 -0
- package/skills/gitnexus-debugging.md +89 -0
- package/skills/gitnexus-exploring.md +78 -0
- package/skills/gitnexus-guide.md +64 -0
- package/skills/gitnexus-impact-analysis.md +97 -0
- package/skills/gitnexus-pr-review.md +163 -0
- package/skills/gitnexus-refactoring.md +121 -0
- package/vendor/leiden/index.cjs +355 -0
- 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>;
|