@supermodeltools/mcp-server 0.4.3 → 0.4.4
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 +27 -13
- package/dist/cache/graph-cache.js +260 -0
- package/dist/cache/graph-types.js +6 -0
- package/dist/cache/index.js +21 -0
- package/dist/queries/discovery.js +148 -0
- package/dist/queries/index.js +241 -0
- package/dist/queries/summary.js +36 -0
- package/dist/queries/traversal.js +392 -0
- package/dist/queries/types.js +38 -0
- package/dist/server.js +38 -1
- package/dist/tools/create-supermodel-graph.js +500 -29
- package/dist/utils/zip-repository.js +332 -0
- package/package.json +4 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Query dispatcher - routes queries to appropriate handlers
|
|
4
|
+
* Central entry point for the query engine
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
18
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
19
|
+
};
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.graphCache = void 0;
|
|
25
|
+
exports.executeQuery = executeQuery;
|
|
26
|
+
exports.getAvailableQueries = getAvailableQueries;
|
|
27
|
+
// @ts-ignore - jq-web doesn't have type declarations
|
|
28
|
+
const jq_web_1 = __importDefault(require("jq-web"));
|
|
29
|
+
const graph_cache_1 = require("../cache/graph-cache");
|
|
30
|
+
const types_1 = require("./types");
|
|
31
|
+
const summary_1 = require("./summary");
|
|
32
|
+
const discovery_1 = require("./discovery");
|
|
33
|
+
const traversal_1 = require("./traversal");
|
|
34
|
+
// jq import moved to top of file
|
|
35
|
+
/**
|
|
36
|
+
* Execute a query against a graph
|
|
37
|
+
* Handles caching, graph loading, and query dispatch
|
|
38
|
+
*/
|
|
39
|
+
async function executeQuery(params, apiResponse) {
|
|
40
|
+
// Cache by idempotencyKey only - file path shouldn't affect cache hits
|
|
41
|
+
const cacheKey = params.idempotencyKey;
|
|
42
|
+
// Try to get from cache first
|
|
43
|
+
let graph = graph_cache_1.graphCache.get(cacheKey);
|
|
44
|
+
let source = 'cache';
|
|
45
|
+
// If not cached and we have API response, build indexes and cache
|
|
46
|
+
if (!graph && apiResponse) {
|
|
47
|
+
graph = (0, graph_cache_1.buildIndexes)(apiResponse, cacheKey);
|
|
48
|
+
graph_cache_1.graphCache.set(cacheKey, graph);
|
|
49
|
+
source = 'api';
|
|
50
|
+
}
|
|
51
|
+
// Special case: graph_status works even without a cached graph
|
|
52
|
+
if (params.query === 'graph_status') {
|
|
53
|
+
return (0, summary_1.graphStatus)(params, graph);
|
|
54
|
+
}
|
|
55
|
+
// All other queries require a graph
|
|
56
|
+
if (!graph) {
|
|
57
|
+
return (0, types_1.createError)('CACHE_MISS', `Graph not found in cache for key '${cacheKey}'`, {
|
|
58
|
+
retryable: true,
|
|
59
|
+
detail: 'Re-call with the same file and idempotencyKey to fetch from API',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// Dispatch to appropriate handler
|
|
63
|
+
return dispatchQuery(params, graph, source);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Dispatch query to the appropriate handler function
|
|
67
|
+
*/
|
|
68
|
+
function dispatchQuery(params, graph, source) {
|
|
69
|
+
switch (params.query) {
|
|
70
|
+
// Summary queries
|
|
71
|
+
case 'summary':
|
|
72
|
+
return (0, summary_1.summary)(params, graph, source);
|
|
73
|
+
// Discovery queries
|
|
74
|
+
case 'get_node':
|
|
75
|
+
return (0, discovery_1.getNode)(params, graph, source);
|
|
76
|
+
case 'search':
|
|
77
|
+
return (0, discovery_1.search)(params, graph, source);
|
|
78
|
+
case 'list_nodes':
|
|
79
|
+
return (0, discovery_1.listNodes)(params, graph, source);
|
|
80
|
+
// Traversal queries (v1 MVP)
|
|
81
|
+
case 'function_calls_in':
|
|
82
|
+
return (0, traversal_1.functionCallsIn)(params, graph, source);
|
|
83
|
+
case 'function_calls_out':
|
|
84
|
+
return (0, traversal_1.functionCallsOut)(params, graph, source);
|
|
85
|
+
case 'definitions_in_file':
|
|
86
|
+
return (0, traversal_1.definitionsInFile)(params, graph, source);
|
|
87
|
+
// Traversal queries (v1.1)
|
|
88
|
+
case 'file_imports':
|
|
89
|
+
return (0, traversal_1.fileImports)(params, graph, source);
|
|
90
|
+
case 'domain_map':
|
|
91
|
+
return (0, traversal_1.domainMap)(params, graph, source);
|
|
92
|
+
case 'domain_membership':
|
|
93
|
+
return (0, traversal_1.domainMembership)(params, graph, source);
|
|
94
|
+
case 'neighborhood':
|
|
95
|
+
return (0, traversal_1.neighborhood)(params, graph, source);
|
|
96
|
+
// Escape hatch
|
|
97
|
+
case 'jq':
|
|
98
|
+
return executeJqQuery(params, graph, source);
|
|
99
|
+
// Not implemented yet
|
|
100
|
+
case 'uses_in_file':
|
|
101
|
+
case 'list_files_in_dir':
|
|
102
|
+
return (0, types_1.createError)('INVALID_QUERY', `Query type '${params.query}' is not yet implemented`);
|
|
103
|
+
default:
|
|
104
|
+
return (0, types_1.createError)('INVALID_QUERY', `Unknown query type: ${params.query}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Execute a raw jq query against the graph
|
|
109
|
+
* Escape hatch for queries not covered by canned types
|
|
110
|
+
*/
|
|
111
|
+
async function executeJqQuery(params, graph, source) {
|
|
112
|
+
if (!params.jq_filter) {
|
|
113
|
+
return (0, types_1.createError)('INVALID_PARAMS', 'jq_filter is required for jq query');
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
// Execute jq using jq-web (in-memory, no temp file needed)
|
|
117
|
+
const jqInstance = await jq_web_1.default;
|
|
118
|
+
const result = jqInstance.json(graph.raw, params.jq_filter);
|
|
119
|
+
return {
|
|
120
|
+
query: 'jq',
|
|
121
|
+
cacheKey: graph.cacheKey,
|
|
122
|
+
source,
|
|
123
|
+
cachedAt: graph.cachedAt,
|
|
124
|
+
result,
|
|
125
|
+
warnings: [
|
|
126
|
+
'jq is an escape hatch. Consider using a canned query type for better performance.',
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
return (0, types_1.createError)('BAD_JQ', `jq filter error: ${message}`, {
|
|
133
|
+
detail: params.jq_filter,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get list of available query types with descriptions
|
|
139
|
+
* Useful for agent discovery
|
|
140
|
+
*/
|
|
141
|
+
function getAvailableQueries() {
|
|
142
|
+
return [
|
|
143
|
+
// v1 MVP
|
|
144
|
+
{
|
|
145
|
+
query: 'graph_status',
|
|
146
|
+
description: 'Check if graph is cached and get summary stats',
|
|
147
|
+
requiredParams: ['file', 'idempotencyKey'],
|
|
148
|
+
optionalParams: [],
|
|
149
|
+
phase: 'v1',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
query: 'summary',
|
|
153
|
+
description: 'Get high-level stats about the codebase',
|
|
154
|
+
requiredParams: ['file', 'idempotencyKey'],
|
|
155
|
+
optionalParams: [],
|
|
156
|
+
phase: 'v1',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
query: 'get_node',
|
|
160
|
+
description: 'Get full details for a specific node by ID',
|
|
161
|
+
requiredParams: ['file', 'idempotencyKey', 'targetId'],
|
|
162
|
+
optionalParams: [],
|
|
163
|
+
phase: 'v1',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
query: 'search',
|
|
167
|
+
description: 'Search nodes by name substring',
|
|
168
|
+
requiredParams: ['file', 'idempotencyKey', 'searchText'],
|
|
169
|
+
optionalParams: ['labels', 'filePathPrefix', 'limit'],
|
|
170
|
+
phase: 'v1',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
query: 'list_nodes',
|
|
174
|
+
description: 'List nodes with filters (labels, namePattern, filePathPrefix)',
|
|
175
|
+
requiredParams: ['file', 'idempotencyKey'],
|
|
176
|
+
optionalParams: ['labels', 'namePattern', 'filePathPrefix', 'searchText', 'limit'],
|
|
177
|
+
phase: 'v1',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
query: 'function_calls_in',
|
|
181
|
+
description: 'Find all callers of a function',
|
|
182
|
+
requiredParams: ['file', 'idempotencyKey', 'targetId'],
|
|
183
|
+
optionalParams: ['limit'],
|
|
184
|
+
phase: 'v1',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
query: 'function_calls_out',
|
|
188
|
+
description: 'Find all functions called by a function',
|
|
189
|
+
requiredParams: ['file', 'idempotencyKey', 'targetId'],
|
|
190
|
+
optionalParams: ['limit'],
|
|
191
|
+
phase: 'v1',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
query: 'definitions_in_file',
|
|
195
|
+
description: 'Get all classes, functions, types defined in a file',
|
|
196
|
+
requiredParams: ['file', 'idempotencyKey'],
|
|
197
|
+
optionalParams: ['targetId', 'filePathPrefix', 'limit'],
|
|
198
|
+
phase: 'v1',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
query: 'jq',
|
|
202
|
+
description: 'Execute raw jq filter (escape hatch)',
|
|
203
|
+
requiredParams: ['file', 'idempotencyKey', 'jq_filter'],
|
|
204
|
+
optionalParams: [],
|
|
205
|
+
phase: 'v1',
|
|
206
|
+
},
|
|
207
|
+
// v1.1
|
|
208
|
+
{
|
|
209
|
+
query: 'file_imports',
|
|
210
|
+
description: 'Get imports for a file (outgoing and incoming)',
|
|
211
|
+
requiredParams: ['file', 'idempotencyKey', 'targetId'],
|
|
212
|
+
optionalParams: ['limit'],
|
|
213
|
+
phase: 'v1.1',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
query: 'domain_map',
|
|
217
|
+
description: 'List all domains with relationships',
|
|
218
|
+
requiredParams: ['file', 'idempotencyKey'],
|
|
219
|
+
optionalParams: [],
|
|
220
|
+
phase: 'v1.1',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
query: 'domain_membership',
|
|
224
|
+
description: 'Get members of a domain',
|
|
225
|
+
requiredParams: ['file', 'idempotencyKey'],
|
|
226
|
+
optionalParams: ['targetId', 'searchText', 'limit'],
|
|
227
|
+
phase: 'v1.1',
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
query: 'neighborhood',
|
|
231
|
+
description: 'Get ego graph around a node',
|
|
232
|
+
requiredParams: ['file', 'idempotencyKey', 'targetId'],
|
|
233
|
+
optionalParams: ['depth', 'relationshipTypes', 'limit'],
|
|
234
|
+
phase: 'v1.1',
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
// Re-export types for convenience
|
|
239
|
+
__exportStar(require("./types"), exports);
|
|
240
|
+
var graph_cache_2 = require("../cache/graph-cache");
|
|
241
|
+
Object.defineProperty(exports, "graphCache", { enumerable: true, get: function () { return graph_cache_2.graphCache; } });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Summary queries: graph_status, summary
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.graphStatus = graphStatus;
|
|
7
|
+
exports.summary = summary;
|
|
8
|
+
const graph_cache_1 = require("../cache/graph-cache");
|
|
9
|
+
const types_1 = require("./types");
|
|
10
|
+
/**
|
|
11
|
+
* graph_status - Return cache status and summary if available
|
|
12
|
+
*/
|
|
13
|
+
function graphStatus(params, graph) {
|
|
14
|
+
const cacheKey = params.idempotencyKey;
|
|
15
|
+
const cacheStats = graph_cache_1.graphCache.status();
|
|
16
|
+
if (!graph) {
|
|
17
|
+
return (0, types_1.createResponse)('graph_status', cacheKey, 'cache', new Date().toISOString(), {
|
|
18
|
+
cached: false,
|
|
19
|
+
cacheKey,
|
|
20
|
+
cacheStats,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return (0, types_1.createResponse)('graph_status', cacheKey, 'cache', graph.cachedAt, {
|
|
24
|
+
cached: true,
|
|
25
|
+
cacheKey: graph.cacheKey,
|
|
26
|
+
cachedAt: graph.cachedAt,
|
|
27
|
+
summary: graph.summary,
|
|
28
|
+
cacheStats,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* summary - Return high-level stats about the graph
|
|
33
|
+
*/
|
|
34
|
+
function summary(params, graph, source) {
|
|
35
|
+
return (0, types_1.createResponse)('summary', graph.cacheKey, source, graph.cachedAt, graph.summary);
|
|
36
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Traversal queries: function_calls_in, function_calls_out, definitions_in_file
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.functionCallsIn = functionCallsIn;
|
|
7
|
+
exports.functionCallsOut = functionCallsOut;
|
|
8
|
+
exports.definitionsInFile = definitionsInFile;
|
|
9
|
+
exports.fileImports = fileImports;
|
|
10
|
+
exports.domainMap = domainMap;
|
|
11
|
+
exports.domainMembership = domainMembership;
|
|
12
|
+
exports.neighborhood = neighborhood;
|
|
13
|
+
const graph_cache_1 = require("../cache/graph-cache");
|
|
14
|
+
const types_1 = require("./types");
|
|
15
|
+
const DEFAULT_LIMIT = 200;
|
|
16
|
+
/**
|
|
17
|
+
* function_calls_in - Find all callers of a function
|
|
18
|
+
*/
|
|
19
|
+
function functionCallsIn(params, graph, source) {
|
|
20
|
+
if (!params.targetId) {
|
|
21
|
+
return (0, types_1.createError)('INVALID_PARAMS', 'targetId is required for function_calls_in query');
|
|
22
|
+
}
|
|
23
|
+
// Verify target exists and is a function
|
|
24
|
+
const targetNode = graph.nodeById.get(params.targetId);
|
|
25
|
+
if (!targetNode) {
|
|
26
|
+
return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`, {
|
|
27
|
+
detail: 'Use search or list_nodes with labels=["Function"] to discover function IDs',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (targetNode.labels?.[0] !== 'Function') {
|
|
31
|
+
return (0, types_1.createError)('INVALID_PARAMS', `Node '${params.targetId}' is not a Function (got ${targetNode.labels?.[0]})`);
|
|
32
|
+
}
|
|
33
|
+
const adj = graph.callAdj.get(params.targetId);
|
|
34
|
+
if (!adj) {
|
|
35
|
+
return (0, types_1.createResponse)('function_calls_in', graph.cacheKey, source, graph.cachedAt, { nodes: [], edges: [] });
|
|
36
|
+
}
|
|
37
|
+
const limit = params.limit || DEFAULT_LIMIT;
|
|
38
|
+
const callerIds = adj.in.slice(0, limit);
|
|
39
|
+
const nodes = [];
|
|
40
|
+
const edges = [];
|
|
41
|
+
for (const callerId of callerIds) {
|
|
42
|
+
const callerNode = graph.nodeById.get(callerId);
|
|
43
|
+
if (callerNode) {
|
|
44
|
+
nodes.push((0, graph_cache_1.toNodeDescriptor)(callerNode));
|
|
45
|
+
edges.push({
|
|
46
|
+
type: 'calls',
|
|
47
|
+
from: callerId,
|
|
48
|
+
to: params.targetId,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const hasMore = adj.in.length > limit;
|
|
53
|
+
return (0, types_1.createResponse)('function_calls_in', graph.cacheKey, source, graph.cachedAt, { nodes, edges }, {
|
|
54
|
+
page: { limit, hasMore },
|
|
55
|
+
warnings: hasMore ? [`${adj.in.length - limit} more callers not shown. Increase limit to see more.`] : undefined,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* function_calls_out - Find all functions called by a function
|
|
60
|
+
*/
|
|
61
|
+
function functionCallsOut(params, graph, source) {
|
|
62
|
+
if (!params.targetId) {
|
|
63
|
+
return (0, types_1.createError)('INVALID_PARAMS', 'targetId is required for function_calls_out query');
|
|
64
|
+
}
|
|
65
|
+
// Verify target exists and is a function
|
|
66
|
+
const targetNode = graph.nodeById.get(params.targetId);
|
|
67
|
+
if (!targetNode) {
|
|
68
|
+
return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`, {
|
|
69
|
+
detail: 'Use search or list_nodes with labels=["Function"] to discover function IDs',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (targetNode.labels?.[0] !== 'Function') {
|
|
73
|
+
return (0, types_1.createError)('INVALID_PARAMS', `Node '${params.targetId}' is not a Function (got ${targetNode.labels?.[0]})`);
|
|
74
|
+
}
|
|
75
|
+
const adj = graph.callAdj.get(params.targetId);
|
|
76
|
+
if (!adj) {
|
|
77
|
+
return (0, types_1.createResponse)('function_calls_out', graph.cacheKey, source, graph.cachedAt, { nodes: [], edges: [] });
|
|
78
|
+
}
|
|
79
|
+
const limit = params.limit || DEFAULT_LIMIT;
|
|
80
|
+
const calleeIds = adj.out.slice(0, limit);
|
|
81
|
+
const nodes = [];
|
|
82
|
+
const edges = [];
|
|
83
|
+
for (const calleeId of calleeIds) {
|
|
84
|
+
const calleeNode = graph.nodeById.get(calleeId);
|
|
85
|
+
if (calleeNode) {
|
|
86
|
+
nodes.push((0, graph_cache_1.toNodeDescriptor)(calleeNode));
|
|
87
|
+
edges.push({
|
|
88
|
+
type: 'calls',
|
|
89
|
+
from: params.targetId,
|
|
90
|
+
to: calleeId,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const hasMore = adj.out.length > limit;
|
|
95
|
+
return (0, types_1.createResponse)('function_calls_out', graph.cacheKey, source, graph.cachedAt, { nodes, edges }, {
|
|
96
|
+
page: { limit, hasMore },
|
|
97
|
+
warnings: hasMore ? [`${adj.out.length - limit} more callees not shown. Increase limit to see more.`] : undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* definitions_in_file - Get all classes, functions, types defined in a file
|
|
102
|
+
*/
|
|
103
|
+
function definitionsInFile(params, graph, source) {
|
|
104
|
+
// Accept either targetId (file node ID) or filePathPrefix (file path)
|
|
105
|
+
let filePath = null;
|
|
106
|
+
if (params.targetId) {
|
|
107
|
+
const targetNode = graph.nodeById.get(params.targetId);
|
|
108
|
+
if (!targetNode) {
|
|
109
|
+
return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`);
|
|
110
|
+
}
|
|
111
|
+
if (targetNode.labels?.[0] !== 'File') {
|
|
112
|
+
return (0, types_1.createError)('INVALID_PARAMS', `Node '${params.targetId}' is not a File (got ${targetNode.labels?.[0]})`);
|
|
113
|
+
}
|
|
114
|
+
filePath = targetNode.properties?.filePath || targetNode.properties?.path;
|
|
115
|
+
}
|
|
116
|
+
else if (params.filePathPrefix) {
|
|
117
|
+
filePath = params.filePathPrefix;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
return (0, types_1.createError)('INVALID_PARAMS', 'Either targetId (file node ID) or filePathPrefix is required');
|
|
121
|
+
}
|
|
122
|
+
if (!filePath) {
|
|
123
|
+
return (0, types_1.createError)('INVALID_PARAMS', 'Could not determine file path');
|
|
124
|
+
}
|
|
125
|
+
const normalizedPath = (0, graph_cache_1.normalizePath)(filePath);
|
|
126
|
+
const pathEntry = graph.pathIndex.get(normalizedPath);
|
|
127
|
+
let resolvedPath = normalizedPath;
|
|
128
|
+
if (!pathEntry) {
|
|
129
|
+
// Try to find by partial match
|
|
130
|
+
for (const [path] of graph.pathIndex) {
|
|
131
|
+
if (path.endsWith(normalizedPath) || normalizedPath.endsWith(path)) {
|
|
132
|
+
resolvedPath = path;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (resolvedPath === normalizedPath) {
|
|
137
|
+
return (0, types_1.createError)('NOT_FOUND', `No definitions found for file '${filePath}'`, {
|
|
138
|
+
detail: 'File may not exist in the analyzed codebase or has no class/function/type definitions',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const entry = pathEntry || graph.pathIndex.get(resolvedPath);
|
|
143
|
+
if (!entry) {
|
|
144
|
+
return (0, types_1.createResponse)('definitions_in_file', graph.cacheKey, source, graph.cachedAt, { file: null, definitions: { nodes: [] } });
|
|
145
|
+
}
|
|
146
|
+
const limit = params.limit || DEFAULT_LIMIT;
|
|
147
|
+
const nodes = [];
|
|
148
|
+
// Get file node
|
|
149
|
+
let fileNode = null;
|
|
150
|
+
if (entry.fileId) {
|
|
151
|
+
const file = graph.nodeById.get(entry.fileId);
|
|
152
|
+
if (file) {
|
|
153
|
+
fileNode = (0, graph_cache_1.toNodeDescriptor)(file);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Collect all definitions
|
|
157
|
+
const allIds = [...entry.classIds, ...entry.functionIds, ...entry.typeIds];
|
|
158
|
+
for (const id of allIds.slice(0, limit)) {
|
|
159
|
+
const node = graph.nodeById.get(id);
|
|
160
|
+
if (node) {
|
|
161
|
+
nodes.push((0, graph_cache_1.toNodeDescriptor)(node));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const hasMore = allIds.length > limit;
|
|
165
|
+
return (0, types_1.createResponse)('definitions_in_file', graph.cacheKey, source, graph.cachedAt, { file: fileNode, definitions: { nodes } }, {
|
|
166
|
+
page: { limit, hasMore },
|
|
167
|
+
warnings: hasMore ? [`${allIds.length - limit} more definitions not shown.`] : undefined,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* file_imports - Get imports for a file (both outgoing and incoming)
|
|
172
|
+
* v1.1 query
|
|
173
|
+
*/
|
|
174
|
+
function fileImports(params, graph, source) {
|
|
175
|
+
if (!params.targetId) {
|
|
176
|
+
return (0, types_1.createError)('INVALID_PARAMS', 'targetId is required for file_imports query');
|
|
177
|
+
}
|
|
178
|
+
const targetNode = graph.nodeById.get(params.targetId);
|
|
179
|
+
if (!targetNode) {
|
|
180
|
+
return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`);
|
|
181
|
+
}
|
|
182
|
+
const label = targetNode.labels?.[0];
|
|
183
|
+
if (label !== 'File' && label !== 'LocalModule' && label !== 'ExternalModule') {
|
|
184
|
+
return (0, types_1.createError)('INVALID_PARAMS', `Node '${params.targetId}' is not a File/Module (got ${label})`);
|
|
185
|
+
}
|
|
186
|
+
const limit = params.limit || DEFAULT_LIMIT;
|
|
187
|
+
const outIds = new Set();
|
|
188
|
+
const addOutgoingImports = (nodeId) => {
|
|
189
|
+
const adj = graph.importAdj.get(nodeId);
|
|
190
|
+
if (!adj)
|
|
191
|
+
return;
|
|
192
|
+
for (const outId of adj.out) {
|
|
193
|
+
outIds.add(outId);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
// Always include imports directly attached to the target node.
|
|
197
|
+
addOutgoingImports(params.targetId);
|
|
198
|
+
// If the target is a File node, also aggregate IMPORTS edges attached to
|
|
199
|
+
// definitions within the file. This handles graphs where IMPORTS edges
|
|
200
|
+
// are modeled as Function -> Module rather than File -> Module.
|
|
201
|
+
if (label === 'File') {
|
|
202
|
+
const filePathRaw = targetNode.properties?.filePath ||
|
|
203
|
+
targetNode.properties?.path ||
|
|
204
|
+
'';
|
|
205
|
+
const filePath = (0, graph_cache_1.normalizePath)(filePathRaw);
|
|
206
|
+
const entry = graph.pathIndex.get(filePath);
|
|
207
|
+
if (entry) {
|
|
208
|
+
for (const id of entry.functionIds)
|
|
209
|
+
addOutgoingImports(id);
|
|
210
|
+
for (const id of entry.classIds)
|
|
211
|
+
addOutgoingImports(id);
|
|
212
|
+
for (const id of entry.typeIds)
|
|
213
|
+
addOutgoingImports(id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const imports = [];
|
|
217
|
+
for (const id of outIds) {
|
|
218
|
+
if (imports.length >= limit)
|
|
219
|
+
break;
|
|
220
|
+
const node = graph.nodeById.get(id);
|
|
221
|
+
if (node) {
|
|
222
|
+
imports.push((0, graph_cache_1.toNodeDescriptor)(node));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const hasMore = outIds.size > limit;
|
|
226
|
+
return (0, types_1.createResponse)('file_imports', graph.cacheKey, source, graph.cachedAt, { imports: { nodes: imports } }, {
|
|
227
|
+
page: { limit, hasMore },
|
|
228
|
+
warnings: label === 'File' && outIds.size === 0
|
|
229
|
+
? [
|
|
230
|
+
'No IMPORTS edges found directly on the File node or on its contained definitions. This may reflect graph modeling choices for this repository.',
|
|
231
|
+
]
|
|
232
|
+
: undefined,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* domain_map - List all domains with their relationships
|
|
237
|
+
* v1.1 query
|
|
238
|
+
*/
|
|
239
|
+
function domainMap(params, graph, source) {
|
|
240
|
+
const domains = [];
|
|
241
|
+
const relationships = [];
|
|
242
|
+
// Get domain nodes
|
|
243
|
+
const domainIds = graph.labelIndex.get('Domain') || [];
|
|
244
|
+
for (const id of domainIds) {
|
|
245
|
+
const node = graph.nodeById.get(id);
|
|
246
|
+
if (node) {
|
|
247
|
+
const name = node.properties?.name;
|
|
248
|
+
const description = node.properties?.description;
|
|
249
|
+
const domainEntry = graph.domainIndex.get(name);
|
|
250
|
+
domains.push({
|
|
251
|
+
name,
|
|
252
|
+
description,
|
|
253
|
+
memberCount: domainEntry?.memberIds.length || 0,
|
|
254
|
+
});
|
|
255
|
+
// Collect relationships
|
|
256
|
+
if (domainEntry) {
|
|
257
|
+
for (const rel of domainEntry.relationships) {
|
|
258
|
+
const targetNode = graph.nodeById.get(rel.endNode);
|
|
259
|
+
if (targetNode) {
|
|
260
|
+
relationships.push({
|
|
261
|
+
from: name,
|
|
262
|
+
to: targetNode.properties?.name,
|
|
263
|
+
type: rel.type,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return (0, types_1.createResponse)('domain_map', graph.cacheKey, source, graph.cachedAt, { domains, relationships });
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* domain_membership - Get members of a domain
|
|
274
|
+
* v1.1 query
|
|
275
|
+
*/
|
|
276
|
+
function domainMembership(params, graph, source) {
|
|
277
|
+
if (!params.searchText && !params.targetId) {
|
|
278
|
+
return (0, types_1.createError)('INVALID_PARAMS', 'searchText (domain name) or targetId (domain node ID) is required');
|
|
279
|
+
}
|
|
280
|
+
let domainName;
|
|
281
|
+
if (params.targetId) {
|
|
282
|
+
const node = graph.nodeById.get(params.targetId);
|
|
283
|
+
if (!node) {
|
|
284
|
+
return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`);
|
|
285
|
+
}
|
|
286
|
+
domainName = node.properties?.name;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
domainName = params.searchText;
|
|
290
|
+
}
|
|
291
|
+
const domainEntry = graph.domainIndex.get(domainName);
|
|
292
|
+
if (!domainEntry) {
|
|
293
|
+
return (0, types_1.createError)('NOT_FOUND', `Domain '${domainName}' not found`, {
|
|
294
|
+
detail: 'Use domain_map to list available domains',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const limit = params.limit || DEFAULT_LIMIT;
|
|
298
|
+
const members = [];
|
|
299
|
+
for (const id of domainEntry.memberIds.slice(0, limit)) {
|
|
300
|
+
const node = graph.nodeById.get(id);
|
|
301
|
+
if (node)
|
|
302
|
+
members.push((0, graph_cache_1.toNodeDescriptor)(node));
|
|
303
|
+
}
|
|
304
|
+
const hasMore = domainEntry.memberIds.length > limit;
|
|
305
|
+
return (0, types_1.createResponse)('domain_membership', graph.cacheKey, source, graph.cachedAt, { domain: domainName, members: { nodes: members } }, {
|
|
306
|
+
page: { limit, hasMore },
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* neighborhood - Get ego graph around a node
|
|
311
|
+
* v1.1 query
|
|
312
|
+
*/
|
|
313
|
+
function neighborhood(params, graph, source) {
|
|
314
|
+
if (!params.targetId) {
|
|
315
|
+
return (0, types_1.createError)('INVALID_PARAMS', 'targetId is required for neighborhood query');
|
|
316
|
+
}
|
|
317
|
+
const targetNode = graph.nodeById.get(params.targetId);
|
|
318
|
+
if (!targetNode) {
|
|
319
|
+
return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`);
|
|
320
|
+
}
|
|
321
|
+
const depth = Math.min(params.depth || 1, 3); // Cap at 3 to prevent explosion
|
|
322
|
+
const limit = params.limit || 100;
|
|
323
|
+
// Only include relationship types we actually implement
|
|
324
|
+
const relationshipTypes = params.relationshipTypes || ['calls', 'IMPORTS'];
|
|
325
|
+
const visited = new Set();
|
|
326
|
+
const nodes = [];
|
|
327
|
+
const edges = [];
|
|
328
|
+
// BFS
|
|
329
|
+
let frontier = [params.targetId];
|
|
330
|
+
visited.add(params.targetId);
|
|
331
|
+
nodes.push((0, graph_cache_1.toNodeDescriptor)(targetNode));
|
|
332
|
+
for (let d = 0; d < depth && nodes.length < limit; d++) {
|
|
333
|
+
const nextFrontier = [];
|
|
334
|
+
for (const nodeId of frontier) {
|
|
335
|
+
if (nodes.length >= limit)
|
|
336
|
+
break;
|
|
337
|
+
// Get adjacent nodes via call graph
|
|
338
|
+
if (relationshipTypes.includes('calls')) {
|
|
339
|
+
const callAdj = graph.callAdj.get(nodeId);
|
|
340
|
+
if (callAdj) {
|
|
341
|
+
for (const adjId of [...callAdj.out, ...callAdj.in]) {
|
|
342
|
+
if (!visited.has(adjId) && nodes.length < limit) {
|
|
343
|
+
visited.add(adjId);
|
|
344
|
+
const adjNode = graph.nodeById.get(adjId);
|
|
345
|
+
if (adjNode) {
|
|
346
|
+
nodes.push((0, graph_cache_1.toNodeDescriptor)(adjNode));
|
|
347
|
+
nextFrontier.push(adjId);
|
|
348
|
+
// Add edge
|
|
349
|
+
if (callAdj.out.includes(adjId)) {
|
|
350
|
+
edges.push({ type: 'calls', from: nodeId, to: adjId });
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
edges.push({ type: 'calls', from: adjId, to: nodeId });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Get adjacent nodes via import graph
|
|
361
|
+
if (relationshipTypes.includes('IMPORTS')) {
|
|
362
|
+
const importAdj = graph.importAdj.get(nodeId);
|
|
363
|
+
if (importAdj) {
|
|
364
|
+
for (const adjId of [...importAdj.out, ...importAdj.in]) {
|
|
365
|
+
if (!visited.has(adjId) && nodes.length < limit) {
|
|
366
|
+
visited.add(adjId);
|
|
367
|
+
const adjNode = graph.nodeById.get(adjId);
|
|
368
|
+
if (adjNode) {
|
|
369
|
+
nodes.push((0, graph_cache_1.toNodeDescriptor)(adjNode));
|
|
370
|
+
nextFrontier.push(adjId);
|
|
371
|
+
if (importAdj.out.includes(adjId)) {
|
|
372
|
+
edges.push({ type: 'IMPORTS', from: nodeId, to: adjId });
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
edges.push({ type: 'IMPORTS', from: adjId, to: nodeId });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
frontier = nextFrontier;
|
|
384
|
+
if (frontier.length === 0)
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
const hasMore = nodes.length >= limit;
|
|
388
|
+
return (0, types_1.createResponse)('neighborhood', graph.cacheKey, source, graph.cachedAt, { nodes, edges }, {
|
|
389
|
+
page: { limit, hasMore },
|
|
390
|
+
warnings: hasMore ? [`Neighborhood truncated at ${limit} nodes. Decrease depth or increase limit.`] : undefined,
|
|
391
|
+
});
|
|
392
|
+
}
|