gitnexus 1.2.8 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -186
- package/dist/cli/ai-context.js +71 -71
- package/dist/cli/analyze.js +1 -1
- package/dist/cli/setup.js +8 -1
- package/dist/cli/view.d.ts +13 -0
- package/dist/cli/view.js +59 -0
- package/dist/core/augmentation/engine.js +20 -20
- package/dist/core/embeddings/embedding-pipeline.js +26 -26
- package/dist/core/graph/html-graph-viewer.d.ts +15 -0
- package/dist/core/graph/html-graph-viewer.js +542 -0
- package/dist/core/graph/html-graph-viewer.test.d.ts +1 -0
- package/dist/core/graph/html-graph-viewer.test.js +67 -0
- package/dist/core/ingestion/cluster-enricher.js +16 -16
- package/dist/core/kuzu/kuzu-adapter.js +9 -9
- package/dist/core/kuzu/schema.js +256 -256
- package/dist/core/search/bm25-index.js +5 -5
- package/dist/core/search/hybrid-search.js +3 -3
- package/dist/core/wiki/graph-queries.js +52 -52
- package/dist/core/wiki/html-viewer.js +192 -192
- package/dist/core/wiki/prompts.js +82 -82
- package/dist/mcp/core/embedder.js +8 -4
- package/dist/mcp/local/local-backend.d.ts +6 -0
- package/dist/mcp/local/local-backend.js +224 -117
- package/dist/mcp/resources.js +42 -42
- package/dist/mcp/server.js +16 -16
- package/dist/mcp/tools.js +86 -77
- package/dist/server/api.d.ts +4 -2
- package/dist/server/api.js +253 -83
- package/hooks/claude/gitnexus-hook.cjs +135 -135
- package/hooks/claude/pre-tool-use.sh +78 -78
- package/hooks/claude/session-start.sh +42 -42
- package/package.json +82 -82
- package/skills/debugging.md +85 -85
- package/skills/exploring.md +75 -75
- package/skills/impact-analysis.md +94 -94
- package/skills/refactoring.md +113 -113
- package/vendor/leiden/index.cjs +355 -355
- package/vendor/leiden/utils.cjs +392 -392
package/dist/mcp/server.js
CHANGED
|
@@ -185,14 +185,14 @@ export async function startMCPServer(backend) {
|
|
|
185
185
|
role: 'user',
|
|
186
186
|
content: {
|
|
187
187
|
type: 'text',
|
|
188
|
-
text: `Analyze the impact of my current code changes before committing.
|
|
189
|
-
|
|
190
|
-
Follow these steps:
|
|
191
|
-
1. Run \`detect_changes(${JSON.stringify({ scope, ...(baseRef ? { base_ref: baseRef } : {}) })})\` to find what changed and affected processes
|
|
192
|
-
2. For each changed symbol in critical processes, run \`context({name: "<symbol>"})\` to see its full reference graph
|
|
193
|
-
3. For any high-risk items (many callers or cross-process), run \`impact({target: "<symbol>", direction: "upstream"})\` for blast radius
|
|
194
|
-
4. Summarize: changes, affected processes, risk level, and recommended actions
|
|
195
|
-
|
|
188
|
+
text: `Analyze the impact of my current code changes before committing.
|
|
189
|
+
|
|
190
|
+
Follow these steps:
|
|
191
|
+
1. Run \`detect_changes(${JSON.stringify({ scope, ...(baseRef ? { base_ref: baseRef } : {}) })})\` to find what changed and affected processes
|
|
192
|
+
2. For each changed symbol in critical processes, run \`context({name: "<symbol>"})\` to see its full reference graph
|
|
193
|
+
3. For any high-risk items (many callers or cross-process), run \`impact({target: "<symbol>", direction: "upstream"})\` for blast radius
|
|
194
|
+
4. Summarize: changes, affected processes, risk level, and recommended actions
|
|
195
|
+
|
|
196
196
|
Present the analysis as a clear risk report.`,
|
|
197
197
|
},
|
|
198
198
|
},
|
|
@@ -207,14 +207,14 @@ Present the analysis as a clear risk report.`,
|
|
|
207
207
|
role: 'user',
|
|
208
208
|
content: {
|
|
209
209
|
type: 'text',
|
|
210
|
-
text: `Generate architecture documentation for this codebase using the knowledge graph.
|
|
211
|
-
|
|
212
|
-
Follow these steps:
|
|
213
|
-
1. READ \`gitnexus://repo/${repo || '{name}'}/context\` for codebase stats
|
|
214
|
-
2. READ \`gitnexus://repo/${repo || '{name}'}/clusters\` to see all functional areas
|
|
215
|
-
3. READ \`gitnexus://repo/${repo || '{name}'}/processes\` to see all execution flows
|
|
216
|
-
4. For the top 5 most important processes, READ \`gitnexus://repo/${repo || '{name}'}/process/{name}\` for step-by-step traces
|
|
217
|
-
5. Generate a mermaid architecture diagram showing the major areas and their connections
|
|
210
|
+
text: `Generate architecture documentation for this codebase using the knowledge graph.
|
|
211
|
+
|
|
212
|
+
Follow these steps:
|
|
213
|
+
1. READ \`gitnexus://repo/${repo || '{name}'}/context\` for codebase stats
|
|
214
|
+
2. READ \`gitnexus://repo/${repo || '{name}'}/clusters\` to see all functional areas
|
|
215
|
+
3. READ \`gitnexus://repo/${repo || '{name}'}/processes\` to see all execution flows
|
|
216
|
+
4. For the top 5 most important processes, READ \`gitnexus://repo/${repo || '{name}'}/process/{name}\` for step-by-step traces
|
|
217
|
+
5. Generate a mermaid architecture diagram showing the major areas and their connections
|
|
218
218
|
6. Write an ARCHITECTURE.md file with: overview, functional areas, key execution flows, and the mermaid diagram`,
|
|
219
219
|
},
|
|
220
220
|
},
|
package/dist/mcp/tools.js
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
export const GITNEXUS_TOOLS = [
|
|
8
8
|
{
|
|
9
9
|
name: 'list_repos',
|
|
10
|
-
description: `List all indexed repositories available to GitNexus.
|
|
11
|
-
|
|
12
|
-
Returns each repo's name, path, indexed date, last commit, and stats.
|
|
13
|
-
|
|
14
|
-
WHEN TO USE: First step when multiple repos are indexed, or to discover available repos.
|
|
15
|
-
AFTER THIS: READ gitnexus://repo/{name}/context for the repo you want to work with.
|
|
16
|
-
|
|
17
|
-
When multiple repos are indexed, you MUST specify the "repo" parameter
|
|
10
|
+
description: `List all indexed repositories available to GitNexus.
|
|
11
|
+
|
|
12
|
+
Returns each repo's name, path, indexed date, last commit, and stats.
|
|
13
|
+
|
|
14
|
+
WHEN TO USE: First step when multiple repos are indexed, or to discover available repos.
|
|
15
|
+
AFTER THIS: READ gitnexus://repo/{name}/context for the repo you want to work with.
|
|
16
|
+
|
|
17
|
+
When multiple repos are indexed, you MUST specify the "repo" parameter
|
|
18
18
|
on other tools (query, context, impact, etc.) to target the correct one.`,
|
|
19
19
|
inputSchema: {
|
|
20
20
|
type: 'object',
|
|
@@ -24,17 +24,17 @@ on other tools (query, context, impact, etc.) to target the correct one.`,
|
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
name: 'query',
|
|
27
|
-
description: `Query the code knowledge graph for execution flows related to a concept.
|
|
28
|
-
Returns processes (call chains) ranked by relevance, each with its symbols and file locations.
|
|
29
|
-
|
|
30
|
-
WHEN TO USE: Understanding how code works together. Use this when you need execution flows and relationships, not just file matches. Complements grep/IDE search.
|
|
31
|
-
AFTER THIS: Use context() on a specific symbol for 360-degree view (callers, callees, categorized refs).
|
|
32
|
-
|
|
33
|
-
Returns results grouped by process (execution flow):
|
|
34
|
-
- processes: ranked execution flows with relevance priority
|
|
35
|
-
- process_symbols: all symbols in those flows with file locations
|
|
36
|
-
- definitions: standalone types/interfaces not in any process
|
|
37
|
-
|
|
27
|
+
description: `Query the code knowledge graph for execution flows related to a concept.
|
|
28
|
+
Returns processes (call chains) ranked by relevance, each with its symbols and file locations.
|
|
29
|
+
|
|
30
|
+
WHEN TO USE: Understanding how code works together. Use this when you need execution flows and relationships, not just file matches. Complements grep/IDE search.
|
|
31
|
+
AFTER THIS: Use context() on a specific symbol for 360-degree view (callers, callees, categorized refs).
|
|
32
|
+
|
|
33
|
+
Returns results grouped by process (execution flow):
|
|
34
|
+
- processes: ranked execution flows with relevance priority
|
|
35
|
+
- process_symbols: all symbols in those flows with file locations and module (functional area)
|
|
36
|
+
- definitions: standalone types/interfaces not in any process
|
|
37
|
+
|
|
38
38
|
Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank Fusion.`,
|
|
39
39
|
inputSchema: {
|
|
40
40
|
type: 'object',
|
|
@@ -52,32 +52,34 @@ Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank
|
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
name: 'cypher',
|
|
55
|
-
description: `Execute Cypher query against the code knowledge graph.
|
|
56
|
-
|
|
57
|
-
WHEN TO USE: Complex structural queries that search/explore can't answer. READ gitnexus://repo/{name}/schema first for the full schema.
|
|
58
|
-
AFTER THIS: Use context() on result symbols for deeper context.
|
|
59
|
-
|
|
60
|
-
SCHEMA:
|
|
61
|
-
- Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process
|
|
62
|
-
- Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
|
|
63
|
-
- All edges via single CodeRelation table with 'type' property
|
|
64
|
-
- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS
|
|
65
|
-
- Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
|
|
66
|
-
|
|
67
|
-
EXAMPLES:
|
|
68
|
-
• Find callers of a function:
|
|
69
|
-
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b:Function {name: "validateUser"}) RETURN a.name, a.filePath
|
|
70
|
-
|
|
71
|
-
• Find community members:
|
|
72
|
-
MATCH (f)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) WHERE c.heuristicLabel = "Auth" RETURN f.name
|
|
73
|
-
|
|
74
|
-
• Trace a process:
|
|
75
|
-
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) WHERE p.heuristicLabel = "UserLogin" RETURN s.name, r.step ORDER BY r.step
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
-
|
|
55
|
+
description: `Execute Cypher query against the code knowledge graph.
|
|
56
|
+
|
|
57
|
+
WHEN TO USE: Complex structural queries that search/explore can't answer. READ gitnexus://repo/{name}/schema first for the full schema.
|
|
58
|
+
AFTER THIS: Use context() on result symbols for deeper context.
|
|
59
|
+
|
|
60
|
+
SCHEMA:
|
|
61
|
+
- Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process
|
|
62
|
+
- Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
|
|
63
|
+
- All edges via single CodeRelation table with 'type' property
|
|
64
|
+
- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS
|
|
65
|
+
- Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
|
|
66
|
+
|
|
67
|
+
EXAMPLES:
|
|
68
|
+
• Find callers of a function:
|
|
69
|
+
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b:Function {name: "validateUser"}) RETURN a.name, a.filePath
|
|
70
|
+
|
|
71
|
+
• Find community members:
|
|
72
|
+
MATCH (f)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) WHERE c.heuristicLabel = "Auth" RETURN f.name
|
|
73
|
+
|
|
74
|
+
• Trace a process:
|
|
75
|
+
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) WHERE p.heuristicLabel = "UserLogin" RETURN s.name, r.step ORDER BY r.step
|
|
76
|
+
|
|
77
|
+
OUTPUT: Returns { markdown, row_count } — results formatted as a Markdown table for easy reading.
|
|
78
|
+
|
|
79
|
+
TIPS:
|
|
80
|
+
- All relationships use single CodeRelation table — filter with {type: 'CALLS'} etc.
|
|
81
|
+
- Community = auto-detected functional area (Leiden algorithm)
|
|
82
|
+
- Process = execution flow trace from entry point to terminal
|
|
81
83
|
- Use heuristicLabel (not label) for human-readable community/process names`,
|
|
82
84
|
inputSchema: {
|
|
83
85
|
type: 'object',
|
|
@@ -90,12 +92,12 @@ TIPS:
|
|
|
90
92
|
},
|
|
91
93
|
{
|
|
92
94
|
name: 'context',
|
|
93
|
-
description: `360-degree view of a single code symbol.
|
|
94
|
-
Shows categorized incoming/outgoing references (calls, imports, extends, implements), process participation, and file location.
|
|
95
|
-
|
|
96
|
-
WHEN TO USE: After query() to understand a specific symbol in depth. When you need to know all callers, callees, and what execution flows a symbol participates in.
|
|
97
|
-
AFTER THIS: Use impact() if planning changes, or READ gitnexus://repo/{name}/process/{processName} for full execution trace.
|
|
98
|
-
|
|
95
|
+
description: `360-degree view of a single code symbol.
|
|
96
|
+
Shows categorized incoming/outgoing references (calls, imports, extends, implements), process participation, and file location.
|
|
97
|
+
|
|
98
|
+
WHEN TO USE: After query() to understand a specific symbol in depth. When you need to know all callers, callees, and what execution flows a symbol participates in.
|
|
99
|
+
AFTER THIS: Use impact() if planning changes, or READ gitnexus://repo/{name}/process/{processName} for full execution trace.
|
|
100
|
+
|
|
99
101
|
Handles disambiguation: if multiple symbols share the same name, returns candidates for you to pick from. Use uid param for zero-ambiguity lookup from prior results.`,
|
|
100
102
|
inputSchema: {
|
|
101
103
|
type: 'object',
|
|
@@ -111,12 +113,12 @@ Handles disambiguation: if multiple symbols share the same name, returns candida
|
|
|
111
113
|
},
|
|
112
114
|
{
|
|
113
115
|
name: 'detect_changes',
|
|
114
|
-
description: `Analyze uncommitted git changes and find affected execution flows.
|
|
115
|
-
Maps git diff hunks to indexed symbols, then traces which processes are impacted.
|
|
116
|
-
|
|
117
|
-
WHEN TO USE: Before committing — to understand what your changes affect. Pre-commit review, PR preparation.
|
|
118
|
-
AFTER THIS: Review affected processes. Use context() on high-risk symbols. READ gitnexus://repo/{name}/process/{name} for full traces.
|
|
119
|
-
|
|
116
|
+
description: `Analyze uncommitted git changes and find affected execution flows.
|
|
117
|
+
Maps git diff hunks to indexed symbols, then traces which processes are impacted.
|
|
118
|
+
|
|
119
|
+
WHEN TO USE: Before committing — to understand what your changes affect. Pre-commit review, PR preparation.
|
|
120
|
+
AFTER THIS: Review affected processes. Use context() on high-risk symbols. READ gitnexus://repo/{name}/process/{name} for full traces.
|
|
121
|
+
|
|
120
122
|
Returns: changed symbols, affected processes, and a risk summary.`,
|
|
121
123
|
inputSchema: {
|
|
122
124
|
type: 'object',
|
|
@@ -130,14 +132,14 @@ Returns: changed symbols, affected processes, and a risk summary.`,
|
|
|
130
132
|
},
|
|
131
133
|
{
|
|
132
134
|
name: 'rename',
|
|
133
|
-
description: `Multi-file coordinated rename using the knowledge graph + text search.
|
|
134
|
-
Finds all references via graph (high confidence) and regex text search (lower confidence). Preview by default.
|
|
135
|
-
|
|
136
|
-
WHEN TO USE: Renaming a function, class, method, or variable across the codebase. Safer than find-and-replace.
|
|
137
|
-
AFTER THIS: Run detect_changes() to verify no unexpected side effects.
|
|
138
|
-
|
|
139
|
-
Each edit is tagged with confidence:
|
|
140
|
-
- "graph": found via knowledge graph relationships (high confidence, safe to accept)
|
|
135
|
+
description: `Multi-file coordinated rename using the knowledge graph + text search.
|
|
136
|
+
Finds all references via graph (high confidence) and regex text search (lower confidence). Preview by default.
|
|
137
|
+
|
|
138
|
+
WHEN TO USE: Renaming a function, class, method, or variable across the codebase. Safer than find-and-replace.
|
|
139
|
+
AFTER THIS: Run detect_changes() to verify no unexpected side effects.
|
|
140
|
+
|
|
141
|
+
Each edit is tagged with confidence:
|
|
142
|
+
- "graph": found via knowledge graph relationships (high confidence, safe to accept)
|
|
141
143
|
- "text_search": found via regex text search (lower confidence, review carefully)`,
|
|
142
144
|
inputSchema: {
|
|
143
145
|
type: 'object',
|
|
@@ -154,18 +156,25 @@ Each edit is tagged with confidence:
|
|
|
154
156
|
},
|
|
155
157
|
{
|
|
156
158
|
name: 'impact',
|
|
157
|
-
description: `Analyze the blast radius of changing a code symbol.
|
|
158
|
-
Returns
|
|
159
|
-
|
|
160
|
-
WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break.
|
|
161
|
-
AFTER THIS: Review d=1 items (WILL BREAK).
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
-
|
|
165
|
-
-
|
|
166
|
-
-
|
|
167
|
-
|
|
168
|
-
|
|
159
|
+
description: `Analyze the blast radius of changing a code symbol.
|
|
160
|
+
Returns affected symbols grouped by depth, plus risk assessment, affected execution flows, and affected modules.
|
|
161
|
+
|
|
162
|
+
WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break.
|
|
163
|
+
AFTER THIS: Review d=1 items (WILL BREAK). Use context() on high-risk symbols.
|
|
164
|
+
|
|
165
|
+
Output includes:
|
|
166
|
+
- risk: LOW / MEDIUM / HIGH / CRITICAL
|
|
167
|
+
- summary: direct callers, processes affected, modules affected
|
|
168
|
+
- affected_processes: which execution flows break and at which step
|
|
169
|
+
- affected_modules: which functional areas are hit (direct vs indirect)
|
|
170
|
+
- byDepth: all affected symbols grouped by traversal depth
|
|
171
|
+
|
|
172
|
+
Depth groups:
|
|
173
|
+
- d=1: WILL BREAK (direct callers/importers)
|
|
174
|
+
- d=2: LIKELY AFFECTED (indirect)
|
|
175
|
+
- d=3: MAY NEED TESTING (transitive)
|
|
176
|
+
|
|
177
|
+
EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS
|
|
169
178
|
Confidence: 1.0 = certain, <0.8 = fuzzy match`,
|
|
170
179
|
inputSchema: {
|
|
171
180
|
type: 'object',
|
package/dist/server/api.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HTTP API Server
|
|
2
|
+
* HTTP API Server (Multi-Repo)
|
|
3
3
|
*
|
|
4
|
-
* REST API for browser-based clients to query
|
|
4
|
+
* REST API for browser-based clients to query indexed repositories.
|
|
5
|
+
* Uses LocalBackend for multi-repo support via the global registry —
|
|
6
|
+
* the same backend the MCP server uses.
|
|
5
7
|
*/
|
|
6
8
|
export declare const createServer: (port: number) => Promise<void>;
|
package/dist/server/api.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HTTP API Server
|
|
2
|
+
* HTTP API Server (Multi-Repo)
|
|
3
3
|
*
|
|
4
|
-
* REST API for browser-based clients to query
|
|
4
|
+
* REST API for browser-based clients to query indexed repositories.
|
|
5
|
+
* Uses LocalBackend for multi-repo support via the global registry —
|
|
6
|
+
* the same backend the MCP server uses.
|
|
5
7
|
*/
|
|
6
8
|
import express from 'express';
|
|
7
9
|
import cors from 'cors';
|
|
8
10
|
import path from 'path';
|
|
9
11
|
import fs from 'fs/promises';
|
|
10
|
-
import {
|
|
11
|
-
import { initKuzu, executeQuery } from '../core/kuzu/kuzu-adapter.js';
|
|
12
|
+
import { LocalBackend } from '../mcp/local/local-backend.js';
|
|
12
13
|
import { NODE_TABLES } from '../core/kuzu/schema.js';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const buildGraph = async () => {
|
|
14
|
+
/**
|
|
15
|
+
* Build the full knowledge graph for a repo by querying each node table
|
|
16
|
+
* and all relationships via the backend's cypher tool.
|
|
17
|
+
*/
|
|
18
|
+
const buildGraph = async (backend, repoName) => {
|
|
18
19
|
const nodes = [];
|
|
19
20
|
for (const table of NODE_TABLES) {
|
|
20
21
|
try {
|
|
@@ -34,7 +35,9 @@ const buildGraph = async () => {
|
|
|
34
35
|
else {
|
|
35
36
|
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`;
|
|
36
37
|
}
|
|
37
|
-
const
|
|
38
|
+
const result = await backend.executeCypher(repoName, query);
|
|
39
|
+
// cypher returns the rows directly (array), or { error } on failure
|
|
40
|
+
const rows = Array.isArray(result) ? result : [];
|
|
38
41
|
for (const row of rows) {
|
|
39
42
|
nodes.push({
|
|
40
43
|
id: row.id ?? row[0],
|
|
@@ -62,95 +65,262 @@ const buildGraph = async () => {
|
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
const relationships = [];
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
try {
|
|
69
|
+
const relResult = await backend.executeCypher(repoName, `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`);
|
|
70
|
+
const relRows = Array.isArray(relResult) ? relResult : [];
|
|
71
|
+
for (const row of relRows) {
|
|
72
|
+
relationships.push({
|
|
73
|
+
id: `${row.sourceId}_${row.type}_${row.targetId}`,
|
|
74
|
+
type: row.type,
|
|
75
|
+
sourceId: row.sourceId,
|
|
76
|
+
targetId: row.targetId,
|
|
77
|
+
confidence: row.confidence,
|
|
78
|
+
reason: row.reason,
|
|
79
|
+
step: row.step,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
console.warn('GitNexus: relationship query failed:', err?.message);
|
|
76
85
|
}
|
|
77
86
|
return { nodes, relationships };
|
|
78
87
|
};
|
|
88
|
+
const httpStatus = (err) => {
|
|
89
|
+
const msg = err?.message ?? '';
|
|
90
|
+
if (msg.includes('not found') || msg.includes('No indexed'))
|
|
91
|
+
return 404;
|
|
92
|
+
if (msg.includes('Multiple repositories'))
|
|
93
|
+
return 400;
|
|
94
|
+
return 500;
|
|
95
|
+
};
|
|
79
96
|
export const createServer = async (port) => {
|
|
97
|
+
const backend = new LocalBackend();
|
|
98
|
+
const hasRepos = await backend.init();
|
|
99
|
+
if (!hasRepos) {
|
|
100
|
+
console.warn('GitNexus: No indexed repositories found. The server will start but most endpoints will return errors.');
|
|
101
|
+
console.warn('Run "gitnexus analyze" in a repository to index it first.');
|
|
102
|
+
}
|
|
80
103
|
const app = express();
|
|
81
|
-
app.use(cors(
|
|
104
|
+
app.use(cors({
|
|
105
|
+
origin: (origin, callback) => {
|
|
106
|
+
// Allow requests with no origin (curl, server-to-server), localhost, and the deployed site.
|
|
107
|
+
// The server binds to 127.0.0.1 so only the local machine can reach it — CORS just gates
|
|
108
|
+
// which browser-tab origins may issue the request.
|
|
109
|
+
if (!origin
|
|
110
|
+
|| origin.startsWith('http://localhost:')
|
|
111
|
+
|| origin.startsWith('http://127.0.0.1:')
|
|
112
|
+
|| origin === 'https://gitnexus.vercel.app') {
|
|
113
|
+
callback(null, true);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
callback(new Error('Not allowed by CORS'));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}));
|
|
82
120
|
app.use(express.json({ limit: '10mb' }));
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
// ─── GET /api/repos ─────────────────────────────────────────────
|
|
122
|
+
// List all indexed repositories
|
|
123
|
+
app.get('/api/repos', async (_req, res) => {
|
|
124
|
+
try {
|
|
125
|
+
const repos = await backend.listRepos();
|
|
126
|
+
res.json(repos);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
res.status(500).json({ error: err.message || 'Failed to list repos' });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
// ─── GET /api/repo?repo=X ──────────────────────────────────────
|
|
133
|
+
// Get metadata for a specific repo
|
|
134
|
+
app.get('/api/repo', async (req, res) => {
|
|
135
|
+
try {
|
|
136
|
+
const repoName = req.query.repo;
|
|
137
|
+
const repo = await backend.resolveRepo(repoName);
|
|
138
|
+
res.json({
|
|
139
|
+
name: repo.name,
|
|
140
|
+
path: repo.repoPath,
|
|
141
|
+
indexedAt: repo.indexedAt,
|
|
142
|
+
lastCommit: repo.lastCommit,
|
|
143
|
+
stats: repo.stats || {},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
res.status(httpStatus(err))
|
|
148
|
+
.json({ error: err.message || 'Repository not found' });
|
|
149
|
+
}
|
|
95
150
|
});
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
151
|
+
// ─── GET /api/graph?repo=X ─────────────────────────────────────
|
|
152
|
+
// Full knowledge graph (all nodes + relationships)
|
|
153
|
+
app.get('/api/graph', async (req, res) => {
|
|
154
|
+
try {
|
|
155
|
+
const repoName = req.query.repo;
|
|
156
|
+
// Resolve repo to validate it exists and get the name
|
|
157
|
+
const repo = await backend.resolveRepo(repoName);
|
|
158
|
+
const graph = await buildGraph(backend, repo.name);
|
|
159
|
+
res.json(graph);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
res.status(httpStatus(err))
|
|
163
|
+
.json({ error: err.message || 'Failed to build graph' });
|
|
164
|
+
}
|
|
106
165
|
});
|
|
107
|
-
//
|
|
166
|
+
// ─── POST /api/query ───────────────────────────────────────────
|
|
167
|
+
// Execute a raw Cypher query.
|
|
168
|
+
// This endpoint is intentionally unrestricted (no query validation) because
|
|
169
|
+
// the server binds to 127.0.0.1 only — it exposes full graph query
|
|
170
|
+
// capabilities to local clients by design.
|
|
108
171
|
app.post('/api/query', async (req, res) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
172
|
+
try {
|
|
173
|
+
const repoName = (req.body.repo ?? req.query.repo);
|
|
174
|
+
const cypher = req.body.cypher;
|
|
175
|
+
if (!cypher) {
|
|
176
|
+
res.status(400).json({ error: 'Missing "cypher" in request body' });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const result = await backend.executeCypher(repoName, cypher);
|
|
180
|
+
if (result && !Array.isArray(result) && result.error) {
|
|
181
|
+
res.status(500).json({ error: result.error });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
res.json({ result });
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
res.status(httpStatus(err))
|
|
188
|
+
.json({ error: err.message || 'Query failed' });
|
|
189
|
+
}
|
|
117
190
|
});
|
|
118
|
-
//
|
|
191
|
+
// ─── POST /api/search ──────────────────────────────────────────
|
|
192
|
+
// Process-grouped semantic search
|
|
119
193
|
app.post('/api/search', async (req, res) => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
194
|
+
try {
|
|
195
|
+
const repoName = (req.body.repo ?? req.query.repo);
|
|
196
|
+
const query = (req.body.query ?? '').trim();
|
|
197
|
+
const limit = req.body.limit;
|
|
198
|
+
if (!query) {
|
|
199
|
+
res.status(400).json({ error: 'Missing "query" in request body' });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const results = await backend.callTool('query', {
|
|
203
|
+
repo: repoName,
|
|
204
|
+
query,
|
|
205
|
+
limit,
|
|
206
|
+
});
|
|
130
207
|
res.json({ results });
|
|
131
|
-
return;
|
|
132
208
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
209
|
+
catch (err) {
|
|
210
|
+
res.status(httpStatus(err))
|
|
211
|
+
.json({ error: err.message || 'Search failed' });
|
|
212
|
+
}
|
|
136
213
|
});
|
|
137
|
-
//
|
|
214
|
+
// ─── GET /api/file?repo=X&path=Y ──────────────────────────────
|
|
215
|
+
// Read a file from a resolved repo path on disk
|
|
138
216
|
app.get('/api/file', async (req, res) => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
217
|
+
try {
|
|
218
|
+
const repoName = req.query.repo;
|
|
219
|
+
const filePath = req.query.path;
|
|
220
|
+
if (!filePath) {
|
|
221
|
+
res.status(400).json({ error: 'Missing "path" query parameter' });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const repo = await backend.resolveRepo(repoName);
|
|
225
|
+
// Resolve the full path and validate it stays within the repo root
|
|
226
|
+
const repoRoot = path.resolve(repo.repoPath);
|
|
227
|
+
const fullPath = path.resolve(repoRoot, filePath);
|
|
228
|
+
if (!fullPath.startsWith(repoRoot + path.sep) && fullPath !== repoRoot) {
|
|
229
|
+
res.status(403).json({ error: 'Path traversal denied: path escapes repo root' });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
233
|
+
res.json({ content });
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
if (err.code === 'ENOENT') {
|
|
237
|
+
res.status(404).json({ error: 'File not found' });
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
res.status(httpStatus(err))
|
|
241
|
+
.json({ error: err.message || 'Failed to read file' });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
// ─── GET /api/processes?repo=X ─────────────────────────────────
|
|
246
|
+
// List all processes for a repo
|
|
247
|
+
app.get('/api/processes', async (req, res) => {
|
|
248
|
+
try {
|
|
249
|
+
const repoName = req.query.repo;
|
|
250
|
+
const result = await backend.queryProcesses(repoName);
|
|
251
|
+
res.json(result);
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
res.status(httpStatus(err))
|
|
255
|
+
.json({ error: err.message || 'Failed to query processes' });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
// ─── GET /api/process?repo=X&name=Y ───────────────────────────
|
|
259
|
+
// Get detailed process info including steps
|
|
260
|
+
app.get('/api/process', async (req, res) => {
|
|
261
|
+
try {
|
|
262
|
+
const repoName = req.query.repo;
|
|
263
|
+
const name = req.query.name;
|
|
264
|
+
if (!name) {
|
|
265
|
+
res.status(400).json({ error: 'Missing "name" query parameter' });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const result = await backend.queryProcessDetail(name, repoName);
|
|
269
|
+
if (result.error) {
|
|
270
|
+
res.status(404).json({ error: result.error });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
res.json(result);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
res.status(httpStatus(err))
|
|
277
|
+
.json({ error: err.message || 'Failed to query process detail' });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
// ─── GET /api/clusters?repo=X ─────────────────────────────────
|
|
281
|
+
// List all clusters for a repo
|
|
282
|
+
app.get('/api/clusters', async (req, res) => {
|
|
283
|
+
try {
|
|
284
|
+
const repoName = req.query.repo;
|
|
285
|
+
const result = await backend.queryClusters(repoName);
|
|
286
|
+
res.json(result);
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
res.status(httpStatus(err))
|
|
290
|
+
.json({ error: err.message || 'Failed to query clusters' });
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
// ─── GET /api/cluster?repo=X&name=Y ───────────────────────────
|
|
294
|
+
// Get detailed cluster info including members
|
|
295
|
+
app.get('/api/cluster', async (req, res) => {
|
|
296
|
+
try {
|
|
297
|
+
const repoName = req.query.repo;
|
|
298
|
+
const name = req.query.name;
|
|
299
|
+
if (!name) {
|
|
300
|
+
res.status(400).json({ error: 'Missing "name" query parameter' });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const result = await backend.queryClusterDetail(name, repoName);
|
|
304
|
+
if (result.error) {
|
|
305
|
+
res.status(404).json({ error: result.error });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
res.json(result);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
res.status(httpStatus(err))
|
|
312
|
+
.json({ error: err.message || 'Failed to query cluster detail' });
|
|
313
|
+
}
|
|
152
314
|
});
|
|
153
|
-
app.listen(port, () => {
|
|
315
|
+
const server = app.listen(port, '127.0.0.1', () => {
|
|
154
316
|
console.log(`GitNexus server running on http://localhost:${port}`);
|
|
317
|
+
console.log(`Serving ${hasRepos ? 'all indexed repositories' : 'no repositories (run gitnexus analyze first)'}`);
|
|
155
318
|
});
|
|
319
|
+
const shutdown = async () => {
|
|
320
|
+
server.close();
|
|
321
|
+
await backend.disconnect();
|
|
322
|
+
process.exit(0);
|
|
323
|
+
};
|
|
324
|
+
process.once('SIGINT', shutdown);
|
|
325
|
+
process.once('SIGTERM', shutdown);
|
|
156
326
|
};
|