@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
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ claude mcp list
|
|
|
126
126
|
|
|
127
127
|
## Tools
|
|
128
128
|
|
|
129
|
-
### `
|
|
129
|
+
### `explore_codebase`
|
|
130
130
|
|
|
131
131
|
Analyzes code structure, dependencies, and relationships across a repository. Use this to understand unfamiliar codebases, plan refactorings, assess change impact, or map system architecture.
|
|
132
132
|
|
|
@@ -148,40 +148,54 @@ Analyzes code structure, dependencies, and relationships across a repository. Us
|
|
|
148
148
|
|
|
149
149
|
| Argument | Type | Required | Description |
|
|
150
150
|
|----------|------|----------|-------------|
|
|
151
|
-
| `
|
|
151
|
+
| `directory` | string | Yes* | Path to repository directory (automatic zipping) |
|
|
152
|
+
| `file` | string | Yes* | Path to pre-zipped archive (deprecated) |
|
|
152
153
|
| `Idempotency-Key` | string | Yes | Cache key in format `{repo}:{type}:{hash}` |
|
|
153
|
-
| `
|
|
154
|
+
| `query` | string | No | Query type (summary, search, list_nodes, etc.) |
|
|
155
|
+
| `jq_filter` | string | No | jq filter for custom data extraction |
|
|
156
|
+
|
|
157
|
+
\* Either `directory` (recommended) or `file` must be provided
|
|
154
158
|
|
|
155
159
|
**Quick start:**
|
|
156
160
|
|
|
157
161
|
```bash
|
|
158
|
-
# 1.
|
|
159
|
-
git archive -o /tmp/repo.zip HEAD
|
|
160
|
-
|
|
161
|
-
# 2. Get commit hash for cache key
|
|
162
|
+
# 1. Get commit hash for cache key
|
|
162
163
|
git rev-parse --short HEAD
|
|
163
164
|
# Output: abc123
|
|
164
165
|
|
|
165
|
-
#
|
|
166
|
-
# "Analyze the codebase at /
|
|
166
|
+
# 2. Ask Claude to analyze (no manual zipping needed!)
|
|
167
|
+
# "Analyze the codebase at /path/to/repo using key myproject:supermodel:abc123"
|
|
167
168
|
```
|
|
168
169
|
|
|
169
170
|
**Example prompts:**
|
|
170
|
-
- "Analyze the codebase at
|
|
171
|
-
- "Before I refactor the authentication module, analyze
|
|
172
|
-
- "What's the structure of the codebase in /
|
|
171
|
+
- "Analyze the codebase at . to understand its architecture"
|
|
172
|
+
- "Before I refactor the authentication module, analyze this repo to show me what depends on it"
|
|
173
|
+
- "What's the structure of the codebase in /Users/me/project?"
|
|
174
|
+
|
|
175
|
+
**Automatic features:**
|
|
176
|
+
- Respects `.gitignore` patterns automatically
|
|
177
|
+
- Excludes sensitive files (`.env`, `*.pem`, credentials, etc.)
|
|
178
|
+
- Skips dependencies (`node_modules`, `venv`, `vendor`)
|
|
179
|
+
- Removes build outputs (`dist`, `build`, `out`)
|
|
180
|
+
- Cleans up temporary files automatically
|
|
181
|
+
- Cross-platform compatible
|
|
173
182
|
|
|
174
183
|
## Troubleshooting
|
|
175
184
|
|
|
176
185
|
Debug logs go to stderr:
|
|
177
186
|
|
|
178
187
|
- `[DEBUG] Server configuration:` - Startup config
|
|
188
|
+
- `[DEBUG] Auto-zipping directory:` - Starting zip creation
|
|
189
|
+
- `[DEBUG] Auto-zip complete:` - Zip stats (file count, size)
|
|
179
190
|
- `[DEBUG] Making API request` - Request details
|
|
180
191
|
- `[ERROR] API call failed:` - Error details with HTTP status
|
|
181
192
|
|
|
182
193
|
**Common issues:**
|
|
183
194
|
- 401: Check `SUPERMODEL_API_KEY` is set
|
|
184
|
-
- ZIP too large:
|
|
195
|
+
- ZIP too large: Directory contains too many files/dependencies. Ensure `.gitignore` is configured properly
|
|
196
|
+
- Permission denied: Check read permissions on the directory
|
|
197
|
+
- Insufficient disk space: Free up space in your system's temp directory
|
|
198
|
+
- Directory does not exist: Verify the path is correct and absolute
|
|
185
199
|
|
|
186
200
|
## Links
|
|
187
201
|
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* LRU Cache for indexed graphs
|
|
4
|
+
* Stores raw API responses + derived indexes for fast query execution
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.graphCache = exports.GraphCache = void 0;
|
|
8
|
+
exports.buildIndexes = buildIndexes;
|
|
9
|
+
exports.normalizePath = normalizePath;
|
|
10
|
+
exports.toNodeDescriptor = toNodeDescriptor;
|
|
11
|
+
/**
|
|
12
|
+
* Build indexes from raw SupermodelIR response
|
|
13
|
+
*/
|
|
14
|
+
function buildIndexes(raw, cacheKey) {
|
|
15
|
+
const nodes = raw.graph?.nodes || [];
|
|
16
|
+
const relationships = raw.graph?.relationships || [];
|
|
17
|
+
// Initialize indexes
|
|
18
|
+
const nodeById = new Map();
|
|
19
|
+
const labelIndex = new Map();
|
|
20
|
+
const pathIndex = new Map();
|
|
21
|
+
const dirIndex = new Map();
|
|
22
|
+
const nameIndex = new Map();
|
|
23
|
+
const callAdj = new Map();
|
|
24
|
+
const importAdj = new Map();
|
|
25
|
+
const domainIndex = new Map();
|
|
26
|
+
// Build node indexes
|
|
27
|
+
for (const node of nodes) {
|
|
28
|
+
const id = node.id;
|
|
29
|
+
const props = node.properties || {};
|
|
30
|
+
const labels = node.labels || [];
|
|
31
|
+
// nodeById
|
|
32
|
+
nodeById.set(id, node);
|
|
33
|
+
// labelIndex
|
|
34
|
+
for (const label of labels) {
|
|
35
|
+
if (!labelIndex.has(label)) {
|
|
36
|
+
labelIndex.set(label, []);
|
|
37
|
+
}
|
|
38
|
+
labelIndex.get(label).push(id);
|
|
39
|
+
}
|
|
40
|
+
// nameIndex (lowercase for case-insensitive search)
|
|
41
|
+
const name = props.name;
|
|
42
|
+
if (name) {
|
|
43
|
+
const lowerName = name.toLowerCase();
|
|
44
|
+
if (!nameIndex.has(lowerName)) {
|
|
45
|
+
nameIndex.set(lowerName, []);
|
|
46
|
+
}
|
|
47
|
+
nameIndex.get(lowerName).push(id);
|
|
48
|
+
}
|
|
49
|
+
// pathIndex - track what's defined in each file
|
|
50
|
+
const filePath = props.filePath;
|
|
51
|
+
if (filePath) {
|
|
52
|
+
const normalized = normalizePath(filePath);
|
|
53
|
+
if (!pathIndex.has(normalized)) {
|
|
54
|
+
pathIndex.set(normalized, { fileId: '', classIds: [], functionIds: [], typeIds: [] });
|
|
55
|
+
}
|
|
56
|
+
const entry = pathIndex.get(normalized);
|
|
57
|
+
const primaryLabel = labels[0];
|
|
58
|
+
if (primaryLabel === 'File') {
|
|
59
|
+
entry.fileId = id;
|
|
60
|
+
}
|
|
61
|
+
else if (primaryLabel === 'Class') {
|
|
62
|
+
entry.classIds.push(id);
|
|
63
|
+
}
|
|
64
|
+
else if (primaryLabel === 'Function') {
|
|
65
|
+
entry.functionIds.push(id);
|
|
66
|
+
}
|
|
67
|
+
else if (primaryLabel === 'Type') {
|
|
68
|
+
entry.typeIds.push(id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// dirIndex - build directory tree
|
|
72
|
+
if (labels[0] === 'Directory') {
|
|
73
|
+
const dirPath = normalizePath(props.path || props.name || '');
|
|
74
|
+
if (!dirIndex.has(dirPath)) {
|
|
75
|
+
dirIndex.set(dirPath, []);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Initialize adjacency lists for functions and files
|
|
79
|
+
if (labels[0] === 'Function') {
|
|
80
|
+
callAdj.set(id, { out: [], in: [] });
|
|
81
|
+
}
|
|
82
|
+
if (labels[0] === 'File' || labels[0] === 'LocalModule' || labels[0] === 'ExternalModule') {
|
|
83
|
+
importAdj.set(id, { out: [], in: [] });
|
|
84
|
+
}
|
|
85
|
+
// domainIndex
|
|
86
|
+
if (labels[0] === 'Domain' || labels[0] === 'Subdomain') {
|
|
87
|
+
domainIndex.set(name || id, { memberIds: [], relationships: [] });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Build relationship indexes
|
|
91
|
+
for (const rel of relationships) {
|
|
92
|
+
const { type, startNode, endNode } = rel;
|
|
93
|
+
// Call adjacency
|
|
94
|
+
if (type === 'calls') {
|
|
95
|
+
if (callAdj.has(startNode)) {
|
|
96
|
+
callAdj.get(startNode).out.push(endNode);
|
|
97
|
+
}
|
|
98
|
+
if (callAdj.has(endNode)) {
|
|
99
|
+
callAdj.get(endNode).in.push(startNode);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Import adjacency
|
|
103
|
+
if (type === 'IMPORTS') {
|
|
104
|
+
// Some graphs emit IMPORTS edges from non-File nodes (e.g. Function -> Module).
|
|
105
|
+
// Create adjacency lazily for any node that participates.
|
|
106
|
+
let startAdj = importAdj.get(startNode);
|
|
107
|
+
if (!startAdj) {
|
|
108
|
+
startAdj = { out: [], in: [] };
|
|
109
|
+
importAdj.set(startNode, startAdj);
|
|
110
|
+
}
|
|
111
|
+
startAdj.out.push(endNode);
|
|
112
|
+
let endAdj = importAdj.get(endNode);
|
|
113
|
+
if (!endAdj) {
|
|
114
|
+
endAdj = { out: [], in: [] };
|
|
115
|
+
importAdj.set(endNode, endAdj);
|
|
116
|
+
}
|
|
117
|
+
endAdj.in.push(startNode);
|
|
118
|
+
}
|
|
119
|
+
// Directory contains
|
|
120
|
+
if (type === 'CONTAINS_FILE' || type === 'CHILD_DIRECTORY') {
|
|
121
|
+
const startNode_ = nodeById.get(startNode);
|
|
122
|
+
if (startNode_ && startNode_.labels?.[0] === 'Directory') {
|
|
123
|
+
const dirPath = normalizePath(startNode_.properties?.path || startNode_.properties?.name || '');
|
|
124
|
+
if (dirIndex.has(dirPath)) {
|
|
125
|
+
dirIndex.get(dirPath).push(endNode);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Domain membership
|
|
130
|
+
if (type === 'belongsTo') {
|
|
131
|
+
const targetNode = nodeById.get(endNode);
|
|
132
|
+
if (targetNode) {
|
|
133
|
+
const domainName = targetNode.properties?.name;
|
|
134
|
+
if (domainIndex.has(domainName)) {
|
|
135
|
+
domainIndex.get(domainName).memberIds.push(startNode);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Domain relationships
|
|
140
|
+
const startNodeData = nodeById.get(startNode);
|
|
141
|
+
const endNodeData = nodeById.get(endNode);
|
|
142
|
+
if (startNodeData?.labels?.[0] === 'Domain' && endNodeData?.labels?.[0] === 'Domain') {
|
|
143
|
+
const domainName = startNodeData.properties?.name;
|
|
144
|
+
if (domainIndex.has(domainName)) {
|
|
145
|
+
domainIndex.get(domainName).relationships.push(rel);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Compute summary
|
|
150
|
+
const summary = {
|
|
151
|
+
filesProcessed: raw.summary?.filesProcessed || labelIndex.get('File')?.length || 0,
|
|
152
|
+
classes: raw.summary?.classes || labelIndex.get('Class')?.length || 0,
|
|
153
|
+
functions: raw.summary?.functions || labelIndex.get('Function')?.length || 0,
|
|
154
|
+
types: raw.summary?.types || labelIndex.get('Type')?.length || 0,
|
|
155
|
+
domains: raw.summary?.domains || labelIndex.get('Domain')?.length || 0,
|
|
156
|
+
primaryLanguage: raw.summary?.primaryLanguage || 'unknown',
|
|
157
|
+
nodeCount: nodes.length,
|
|
158
|
+
relationshipCount: relationships.length,
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
raw,
|
|
162
|
+
nodeById,
|
|
163
|
+
labelIndex,
|
|
164
|
+
pathIndex,
|
|
165
|
+
dirIndex,
|
|
166
|
+
nameIndex,
|
|
167
|
+
callAdj,
|
|
168
|
+
importAdj,
|
|
169
|
+
domainIndex,
|
|
170
|
+
summary,
|
|
171
|
+
cachedAt: new Date().toISOString(),
|
|
172
|
+
cacheKey,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Normalize file paths for consistent matching
|
|
177
|
+
*/
|
|
178
|
+
function normalizePath(path) {
|
|
179
|
+
return path.replace(/\\/g, '/');
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Convert full node to lightweight descriptor
|
|
183
|
+
*/
|
|
184
|
+
function toNodeDescriptor(node) {
|
|
185
|
+
const props = node.properties || {};
|
|
186
|
+
return {
|
|
187
|
+
id: node.id,
|
|
188
|
+
labels: node.labels || [],
|
|
189
|
+
name: props.name,
|
|
190
|
+
filePath: props.filePath,
|
|
191
|
+
startLine: props.startLine,
|
|
192
|
+
endLine: props.endLine,
|
|
193
|
+
kind: props.kind,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* LRU Cache for indexed graphs
|
|
198
|
+
*/
|
|
199
|
+
class GraphCache {
|
|
200
|
+
cache = new Map();
|
|
201
|
+
maxGraphs;
|
|
202
|
+
maxNodes;
|
|
203
|
+
currentNodes = 0;
|
|
204
|
+
constructor(options) {
|
|
205
|
+
this.maxGraphs = options?.maxGraphs || 20;
|
|
206
|
+
this.maxNodes = options?.maxNodes || 1000000;
|
|
207
|
+
}
|
|
208
|
+
get(cacheKey) {
|
|
209
|
+
const entry = this.cache.get(cacheKey);
|
|
210
|
+
if (entry) {
|
|
211
|
+
// Update access time (LRU)
|
|
212
|
+
entry.lastAccessed = Date.now();
|
|
213
|
+
return entry.graph;
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
set(cacheKey, graph) {
|
|
218
|
+
const nodeCount = graph.summary.nodeCount;
|
|
219
|
+
// Evict if needed
|
|
220
|
+
while ((this.cache.size >= this.maxGraphs || this.currentNodes + nodeCount > this.maxNodes) &&
|
|
221
|
+
this.cache.size > 0) {
|
|
222
|
+
this.evictOldest();
|
|
223
|
+
}
|
|
224
|
+
// Store
|
|
225
|
+
this.cache.set(cacheKey, {
|
|
226
|
+
graph,
|
|
227
|
+
nodeCount,
|
|
228
|
+
lastAccessed: Date.now(),
|
|
229
|
+
});
|
|
230
|
+
this.currentNodes += nodeCount;
|
|
231
|
+
}
|
|
232
|
+
has(cacheKey) {
|
|
233
|
+
return this.cache.has(cacheKey);
|
|
234
|
+
}
|
|
235
|
+
evictOldest() {
|
|
236
|
+
let oldestKey = null;
|
|
237
|
+
let oldestTime = Infinity;
|
|
238
|
+
for (const [key, entry] of this.cache) {
|
|
239
|
+
if (entry.lastAccessed < oldestTime) {
|
|
240
|
+
oldestTime = entry.lastAccessed;
|
|
241
|
+
oldestKey = key;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (oldestKey) {
|
|
245
|
+
const entry = this.cache.get(oldestKey);
|
|
246
|
+
this.currentNodes -= entry.nodeCount;
|
|
247
|
+
this.cache.delete(oldestKey);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
status() {
|
|
251
|
+
return {
|
|
252
|
+
graphs: this.cache.size,
|
|
253
|
+
nodes: this.currentNodes,
|
|
254
|
+
keys: Array.from(this.cache.keys()),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
exports.GraphCache = GraphCache;
|
|
259
|
+
// Global cache instance
|
|
260
|
+
exports.graphCache = new GraphCache();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cache module exports
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
17
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
__exportStar(require("./graph-types"), exports);
|
|
21
|
+
__exportStar(require("./graph-cache"), exports);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discovery queries: get_node, search, list_nodes
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getNode = getNode;
|
|
7
|
+
exports.search = search;
|
|
8
|
+
exports.listNodes = listNodes;
|
|
9
|
+
const graph_cache_1 = require("../cache/graph-cache");
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
const DEFAULT_LIMIT = 200;
|
|
12
|
+
/**
|
|
13
|
+
* get_node - Return full details for a specific node ID
|
|
14
|
+
*/
|
|
15
|
+
function getNode(params, graph, source) {
|
|
16
|
+
if (!params.targetId) {
|
|
17
|
+
return (0, types_1.createError)('INVALID_PARAMS', 'targetId is required for get_node query');
|
|
18
|
+
}
|
|
19
|
+
const node = graph.nodeById.get(params.targetId);
|
|
20
|
+
if (!node) {
|
|
21
|
+
return (0, types_1.createError)('NOT_FOUND', `Node with id '${params.targetId}' not found`, {
|
|
22
|
+
detail: 'Use search or list_nodes to discover valid node IDs',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const result = {
|
|
26
|
+
node: (0, graph_cache_1.toNodeDescriptor)(node),
|
|
27
|
+
};
|
|
28
|
+
// Only include raw if explicitly requested (reduces response size)
|
|
29
|
+
if (params.includeRaw) {
|
|
30
|
+
result.raw = node;
|
|
31
|
+
}
|
|
32
|
+
return (0, types_1.createResponse)('get_node', graph.cacheKey, source, graph.cachedAt, result);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* search - Simple substring search across node names
|
|
36
|
+
*/
|
|
37
|
+
function search(params, graph, source) {
|
|
38
|
+
if (!params.searchText) {
|
|
39
|
+
return (0, types_1.createError)('INVALID_PARAMS', 'searchText is required for search query');
|
|
40
|
+
}
|
|
41
|
+
const searchLower = params.searchText.toLowerCase();
|
|
42
|
+
const limit = params.limit || DEFAULT_LIMIT;
|
|
43
|
+
const labels = params.labels;
|
|
44
|
+
const filePrefix = params.filePathPrefix ? (0, graph_cache_1.normalizePath)(params.filePathPrefix) : null;
|
|
45
|
+
const results = [];
|
|
46
|
+
let scanned = 0;
|
|
47
|
+
// Scan through nameIndex for matches
|
|
48
|
+
for (const [name, ids] of graph.nameIndex) {
|
|
49
|
+
if (!name.includes(searchLower))
|
|
50
|
+
continue;
|
|
51
|
+
for (const id of ids) {
|
|
52
|
+
if (results.length >= limit)
|
|
53
|
+
break;
|
|
54
|
+
const node = graph.nodeById.get(id);
|
|
55
|
+
if (!node)
|
|
56
|
+
continue;
|
|
57
|
+
// Filter by labels if specified
|
|
58
|
+
if (labels && labels.length > 0) {
|
|
59
|
+
const nodeLabel = node.labels?.[0];
|
|
60
|
+
if (!nodeLabel || !labels.includes(nodeLabel))
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// Filter by file path prefix if specified
|
|
64
|
+
if (filePrefix) {
|
|
65
|
+
const filePath = node.properties?.filePath;
|
|
66
|
+
if (!filePath || !(0, graph_cache_1.normalizePath)(filePath).startsWith(filePrefix))
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
results.push((0, graph_cache_1.toNodeDescriptor)(node));
|
|
70
|
+
scanned++;
|
|
71
|
+
}
|
|
72
|
+
if (results.length >= limit)
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
const hasMore = results.length >= limit;
|
|
76
|
+
return (0, types_1.createResponse)('search', graph.cacheKey, source, graph.cachedAt, { nodes: results }, {
|
|
77
|
+
page: { limit, hasMore },
|
|
78
|
+
warnings: hasMore ? [`Results limited to ${limit}. Use more specific searchText or add filters.`] : undefined,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* list_nodes - Filtered list of nodes by labels, namePattern, filePathPrefix
|
|
83
|
+
*/
|
|
84
|
+
function listNodes(params, graph, source) {
|
|
85
|
+
const limit = params.limit || DEFAULT_LIMIT;
|
|
86
|
+
const labels = params.labels;
|
|
87
|
+
const filePrefix = params.filePathPrefix ? (0, graph_cache_1.normalizePath)(params.filePathPrefix) : null;
|
|
88
|
+
const searchText = params.searchText?.toLowerCase();
|
|
89
|
+
const namePattern = params.namePattern;
|
|
90
|
+
// Compile regex if namePattern provided
|
|
91
|
+
let regex = null;
|
|
92
|
+
if (namePattern) {
|
|
93
|
+
try {
|
|
94
|
+
regex = new RegExp(namePattern, 'i');
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
return (0, types_1.createError)('INVALID_PARAMS', `Invalid namePattern regex: ${namePattern}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const results = [];
|
|
101
|
+
// Determine which nodes to scan
|
|
102
|
+
let candidateIds;
|
|
103
|
+
if (labels && labels.length > 0) {
|
|
104
|
+
// Start with nodes matching specified labels (use Set to dedupe)
|
|
105
|
+
candidateIds = new Set();
|
|
106
|
+
for (const label of labels) {
|
|
107
|
+
const ids = graph.labelIndex.get(label) || [];
|
|
108
|
+
for (const id of ids) {
|
|
109
|
+
candidateIds.add(id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// Scan all nodes
|
|
115
|
+
candidateIds = new Set(graph.nodeById.keys());
|
|
116
|
+
}
|
|
117
|
+
for (const id of candidateIds) {
|
|
118
|
+
if (results.length >= limit)
|
|
119
|
+
break;
|
|
120
|
+
const node = graph.nodeById.get(id);
|
|
121
|
+
if (!node)
|
|
122
|
+
continue;
|
|
123
|
+
const props = node.properties || {};
|
|
124
|
+
const name = props.name;
|
|
125
|
+
const filePath = props.filePath;
|
|
126
|
+
// Filter by file path prefix
|
|
127
|
+
if (filePrefix) {
|
|
128
|
+
if (!filePath || !(0, graph_cache_1.normalizePath)(filePath).startsWith(filePrefix))
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Filter by search text (simple substring)
|
|
132
|
+
if (searchText) {
|
|
133
|
+
if (!name || !name.toLowerCase().includes(searchText))
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// Filter by name pattern (regex)
|
|
137
|
+
if (regex) {
|
|
138
|
+
if (!name || !regex.test(name))
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
results.push((0, graph_cache_1.toNodeDescriptor)(node));
|
|
142
|
+
}
|
|
143
|
+
const hasMore = results.length >= limit;
|
|
144
|
+
return (0, types_1.createResponse)('list_nodes', graph.cacheKey, source, graph.cachedAt, { nodes: results }, {
|
|
145
|
+
page: { limit, hasMore },
|
|
146
|
+
warnings: hasMore ? [`Results limited to ${limit}. Add filters to narrow results.`] : undefined,
|
|
147
|
+
});
|
|
148
|
+
}
|