callgraph-mcp 1.3.0 → 1.4.1
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 +47 -21
- package/dist/index.js +7 -3
- package/dist/server.js +71 -0
- package/dist/tools/analyzeFile.js +109 -0
- package/dist/tools/analyzeWorkspace.js +95 -0
- package/dist/tools/findCycles.d.ts +2 -0
- package/dist/tools/findCycles.js +177 -0
- package/dist/tools/findDuplicates.d.ts +2 -0
- package/dist/tools/findDuplicates.js +254 -0
- package/dist/tools/findOrphans.js +100 -0
- package/dist/tools/getCallees.js +116 -0
- package/dist/tools/getCallers.js +117 -0
- package/dist/tools/getFlow.js +140 -0
- package/dist/tools/listEntryPoints.js +96 -0
- package/dist/utils/analysis.js +113 -0
- package/dist/utils/cache.js +28 -0
- package/dist/utils/fileDiscovery.js +94 -0
- package/dist/utils/formatGraph.js +38 -0
- package/dist/utils/toolHelper.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerFindDuplicates = registerFindDuplicates;
|
|
37
|
+
const zod_1 = require("zod");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const analysis_1 = require("../utils/analysis");
|
|
40
|
+
const toolHelper_1 = require("../utils/toolHelper");
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Jaccard similarity between two sets of strings
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
function jaccard(a, b) {
|
|
45
|
+
if (a.size === 0 && b.size === 0)
|
|
46
|
+
return 1;
|
|
47
|
+
let intersection = 0;
|
|
48
|
+
for (const v of a)
|
|
49
|
+
if (b.has(v))
|
|
50
|
+
intersection++;
|
|
51
|
+
const union = a.size + b.size - intersection;
|
|
52
|
+
return union === 0 ? 0 : intersection / union;
|
|
53
|
+
}
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Union-Find to cluster similar functions
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
class UnionFind {
|
|
58
|
+
parent = new Map();
|
|
59
|
+
find(x) {
|
|
60
|
+
if (this.parent.get(x) !== x) {
|
|
61
|
+
this.parent.set(x, this.find(this.parent.get(x)));
|
|
62
|
+
}
|
|
63
|
+
return this.parent.get(x);
|
|
64
|
+
}
|
|
65
|
+
union(x, y) {
|
|
66
|
+
if (!this.parent.has(x))
|
|
67
|
+
this.parent.set(x, x);
|
|
68
|
+
if (!this.parent.has(y))
|
|
69
|
+
this.parent.set(y, y);
|
|
70
|
+
const rx = this.find(x);
|
|
71
|
+
const ry = this.find(y);
|
|
72
|
+
if (rx !== ry)
|
|
73
|
+
this.parent.set(rx, ry);
|
|
74
|
+
}
|
|
75
|
+
init(id) {
|
|
76
|
+
if (!this.parent.has(id))
|
|
77
|
+
this.parent.set(id, id);
|
|
78
|
+
}
|
|
79
|
+
clusters() {
|
|
80
|
+
const map = new Map();
|
|
81
|
+
for (const id of this.parent.keys()) {
|
|
82
|
+
const root = this.find(id);
|
|
83
|
+
if (!map.has(root))
|
|
84
|
+
map.set(root, []);
|
|
85
|
+
map.get(root).push(id);
|
|
86
|
+
}
|
|
87
|
+
return map;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function envThreshold() {
|
|
91
|
+
const v = parseFloat(process.env.FLOWMAP_DUP_THRESHOLD ?? '');
|
|
92
|
+
return isFinite(v) && v >= 0 && v <= 1 ? v : 0.75;
|
|
93
|
+
}
|
|
94
|
+
function envMinCallees() {
|
|
95
|
+
const v = parseInt(process.env.FLOWMAP_DUP_MIN_CALLEES ?? '', 10);
|
|
96
|
+
return isFinite(v) && v >= 1 ? v : 2;
|
|
97
|
+
}
|
|
98
|
+
function registerFindDuplicates(server) {
|
|
99
|
+
(0, toolHelper_1.registerTool)(server, 'flowmap_find_duplicates', 'Identify functionally duplicate functions — different names, potentially in different files or components, but calling the same set of dependencies (same business logic). Uses callee-set Jaccard similarity: two functions are flagged as duplicates when the overlap of what they call exceeds the similarity threshold. Results are grouped into clusters so you can see when 3+ functions are all doing the same thing. Use this to find refactoring opportunities and candidates for a shared utility. Default thresholds can be tuned via FLOWMAP_DUP_THRESHOLD and FLOWMAP_DUP_MIN_CALLEES environment variables.', {
|
|
100
|
+
workspacePath: zod_1.z.string().describe('Absolute path to the repository root'),
|
|
101
|
+
similarityThreshold: zod_1.z.number().min(0).max(1).optional().describe('Jaccard similarity threshold (0–1). Default: 0.75 (or FLOWMAP_DUP_THRESHOLD env var). Lower = more matches, higher = stricter. 1.0 = identical callee sets.'),
|
|
102
|
+
minCallees: zod_1.z.number().int().min(1).optional().describe('Minimum number of distinct callees a function must have to be considered. Default: 2 (or FLOWMAP_DUP_MIN_CALLEES env var). Raising this avoids matching trivial one-liner wrappers.'),
|
|
103
|
+
exclude: zod_1.z.string().optional().describe('Comma-separated glob patterns to exclude. Defaults: node_modules,dist,.git,__pycache__,*.test.*,*.spec.*'),
|
|
104
|
+
}, async ({ workspacePath, similarityThreshold, minCallees, exclude }) => {
|
|
105
|
+
const resolvedThreshold = similarityThreshold ?? envThreshold();
|
|
106
|
+
const resolvedMinCallees = minCallees ?? envMinCallees();
|
|
107
|
+
try {
|
|
108
|
+
if (!fs.existsSync(workspacePath)) {
|
|
109
|
+
return {
|
|
110
|
+
content: [{
|
|
111
|
+
type: 'text',
|
|
112
|
+
text: JSON.stringify({
|
|
113
|
+
error: true,
|
|
114
|
+
code: 'WORKSPACE_NOT_FOUND',
|
|
115
|
+
message: `Directory does not exist: ${workspacePath}`,
|
|
116
|
+
workspacePath,
|
|
117
|
+
}),
|
|
118
|
+
}],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const DEFAULT_EXCLUDES = ['node_modules', 'dist', '.git', '__pycache__', '*.test.*', '*.spec.*'];
|
|
122
|
+
const excludeList = exclude
|
|
123
|
+
? exclude.split(',').map(s => s.trim()).filter(Boolean)
|
|
124
|
+
: DEFAULT_EXCLUDES;
|
|
125
|
+
const graph = await (0, analysis_1.analyzeWorkspace)(workspacePath, { exclude: excludeList });
|
|
126
|
+
const similarityThreshold = resolvedThreshold;
|
|
127
|
+
const minCallees = resolvedMinCallees;
|
|
128
|
+
// Build callee-name set per function
|
|
129
|
+
// Using callee *names* (not IDs) so the comparison is semantic, not
|
|
130
|
+
// identity-based — two functions calling `validateInput` both call the
|
|
131
|
+
// same concept even if the resolved IDs differ across files.
|
|
132
|
+
const calleeNames = new Map();
|
|
133
|
+
const nodeById = new Map(graph.nodes.map(n => [n.id, n]));
|
|
134
|
+
for (const node of graph.nodes) {
|
|
135
|
+
calleeNames.set(node.id, new Set());
|
|
136
|
+
}
|
|
137
|
+
for (const edge of graph.edges) {
|
|
138
|
+
if (edge.from === edge.to)
|
|
139
|
+
continue; // skip self-loops
|
|
140
|
+
const calleeNode = nodeById.get(edge.to);
|
|
141
|
+
const calleeName = calleeNode?.name ?? edge.to;
|
|
142
|
+
calleeNames.get(edge.from)?.add(calleeName);
|
|
143
|
+
}
|
|
144
|
+
// Filter to functions with enough callees to be meaningful
|
|
145
|
+
const candidates = graph.nodes.filter(n => (calleeNames.get(n.id)?.size ?? 0) >= minCallees);
|
|
146
|
+
// O(n²) pair comparison — guarded by minCallees filter
|
|
147
|
+
const uf = new UnionFind();
|
|
148
|
+
for (const node of candidates)
|
|
149
|
+
uf.init(node.id);
|
|
150
|
+
// Track best similarity per pair for reporting
|
|
151
|
+
const pairSimilarity = new Map();
|
|
152
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
153
|
+
const a = candidates[i];
|
|
154
|
+
const setA = calleeNames.get(a.id);
|
|
155
|
+
for (let j = i + 1; j < candidates.length; j++) {
|
|
156
|
+
const b = candidates[j];
|
|
157
|
+
// Quick skip: if names are identical and in the same file, not a duplication concern
|
|
158
|
+
if (a.name === b.name && a.filePath === b.filePath)
|
|
159
|
+
continue;
|
|
160
|
+
const setB = calleeNames.get(b.id);
|
|
161
|
+
const sim = jaccard(setA, setB);
|
|
162
|
+
if (sim >= similarityThreshold) {
|
|
163
|
+
uf.union(a.id, b.id);
|
|
164
|
+
const key = [a.id, b.id].sort().join('|||');
|
|
165
|
+
pairSimilarity.set(key, sim);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Build clusters — only keep clusters with >1 member
|
|
170
|
+
const rawClusters = uf.clusters();
|
|
171
|
+
const duplicateClusters = [];
|
|
172
|
+
let clusterIndex = 1;
|
|
173
|
+
for (const [, memberIds] of rawClusters) {
|
|
174
|
+
if (memberIds.length < 2)
|
|
175
|
+
continue;
|
|
176
|
+
const members = memberIds.map(id => {
|
|
177
|
+
const n = nodeById.get(id);
|
|
178
|
+
const callees = [...(calleeNames.get(id) ?? [])].sort();
|
|
179
|
+
return {
|
|
180
|
+
id,
|
|
181
|
+
name: n?.name ?? 'unknown',
|
|
182
|
+
filePath: n?.filePath ?? 'unknown',
|
|
183
|
+
startLine: n?.startLine ?? 0,
|
|
184
|
+
language: n?.language ?? 'unknown',
|
|
185
|
+
calleeCount: callees.length,
|
|
186
|
+
callees,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
// Shared callees across all members in the cluster
|
|
190
|
+
const allCalleeSets = memberIds.map(id => calleeNames.get(id));
|
|
191
|
+
const sharedCallees = [...allCalleeSets[0]].filter(c => allCalleeSets.every(s => s.has(c))).sort();
|
|
192
|
+
// Compute min/max similarity across all pairs in this cluster
|
|
193
|
+
let minSim = 1;
|
|
194
|
+
let maxSim = 0;
|
|
195
|
+
for (let i = 0; i < memberIds.length; i++) {
|
|
196
|
+
for (let j = i + 1; j < memberIds.length; j++) {
|
|
197
|
+
const key = [memberIds[i], memberIds[j]].sort().join('|||');
|
|
198
|
+
const sim = pairSimilarity.get(key) ?? jaccard(calleeNames.get(memberIds[i]), calleeNames.get(memberIds[j]));
|
|
199
|
+
if (sim < minSim)
|
|
200
|
+
minSim = sim;
|
|
201
|
+
if (sim > maxSim)
|
|
202
|
+
maxSim = sim;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Derive a suggestion
|
|
206
|
+
const uniqueFiles = new Set(members.map(m => m.filePath)).size;
|
|
207
|
+
const suggestion = uniqueFiles > 1
|
|
208
|
+
? `These ${members.length} functions across ${uniqueFiles} files share the same core logic. Consider extracting a shared utility that accepts parameters for any behavioural differences.`
|
|
209
|
+
: `These ${members.length} functions in the same file appear to duplicate logic. Consider merging them or extracting a private helper.`;
|
|
210
|
+
duplicateClusters.push({
|
|
211
|
+
clusterIndex: clusterIndex++,
|
|
212
|
+
size: members.length,
|
|
213
|
+
members,
|
|
214
|
+
sharedCallees,
|
|
215
|
+
minSimilarity: Math.round(minSim * 100) / 100,
|
|
216
|
+
maxSimilarity: Math.round(maxSim * 100) / 100,
|
|
217
|
+
suggestion,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// Sort by cluster size descending (largest duplication opportunity first)
|
|
221
|
+
duplicateClusters.sort((a, b) => b.size - a.size || b.sharedCallees.length - a.sharedCallees.length);
|
|
222
|
+
return {
|
|
223
|
+
content: [{
|
|
224
|
+
type: 'text',
|
|
225
|
+
text: JSON.stringify({
|
|
226
|
+
duplicateClusters,
|
|
227
|
+
totalClusters: duplicateClusters.length,
|
|
228
|
+
totalFunctionsInvolved: duplicateClusters.reduce((s, c) => s + c.size, 0),
|
|
229
|
+
parameters: { similarityThreshold, minCallees, envOverrides: { FLOWMAP_DUP_THRESHOLD: process.env.FLOWMAP_DUP_THRESHOLD ?? null, FLOWMAP_DUP_MIN_CALLEES: process.env.FLOWMAP_DUP_MIN_CALLEES ?? null } },
|
|
230
|
+
durationMs: graph.durationMs,
|
|
231
|
+
scannedFiles: graph.scannedFiles,
|
|
232
|
+
note: duplicateClusters.length === 0
|
|
233
|
+
? 'No functionally duplicate functions detected at the current threshold. Try lowering similarityThreshold or minCallees.'
|
|
234
|
+
: `${duplicateClusters.length} duplicate cluster(s) found. Each cluster is a group of functions that call the same logical dependencies and are candidates for generalisation.`,
|
|
235
|
+
}),
|
|
236
|
+
}],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
241
|
+
return {
|
|
242
|
+
content: [{
|
|
243
|
+
type: 'text',
|
|
244
|
+
text: JSON.stringify({
|
|
245
|
+
error: true,
|
|
246
|
+
code: 'PARSE_ERROR',
|
|
247
|
+
message,
|
|
248
|
+
workspacePath,
|
|
249
|
+
}),
|
|
250
|
+
}],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerFindOrphans = registerFindOrphans;
|
|
37
|
+
const zod_1 = require("zod");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const analysis_1 = require("../utils/analysis");
|
|
40
|
+
const toolHelper_1 = require("../utils/toolHelper");
|
|
41
|
+
function registerFindOrphans(server) {
|
|
42
|
+
(0, toolHelper_1.registerTool)(server, 'flowmap_find_orphans', 'Return all functions that are never called from any entry point — potential dead code. Use this during refactoring to identify code that can safely be removed.', {
|
|
43
|
+
workspacePath: zod_1.z.string().describe('Absolute path to the repository root'),
|
|
44
|
+
}, async ({ workspacePath }) => {
|
|
45
|
+
try {
|
|
46
|
+
if (!fs.existsSync(workspacePath)) {
|
|
47
|
+
return {
|
|
48
|
+
content: [{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: JSON.stringify({
|
|
51
|
+
error: true,
|
|
52
|
+
code: 'WORKSPACE_NOT_FOUND',
|
|
53
|
+
message: `Directory does not exist: ${workspacePath}`,
|
|
54
|
+
workspacePath,
|
|
55
|
+
}),
|
|
56
|
+
}],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const graph = await (0, analysis_1.analyzeWorkspace)(workspacePath);
|
|
60
|
+
const orphans = graph.orphans.map(orphanId => {
|
|
61
|
+
const node = graph.nodes.find(n => n.id === orphanId);
|
|
62
|
+
return node
|
|
63
|
+
? {
|
|
64
|
+
id: node.id,
|
|
65
|
+
name: node.name,
|
|
66
|
+
filePath: node.filePath,
|
|
67
|
+
startLine: node.startLine,
|
|
68
|
+
language: node.language,
|
|
69
|
+
isExported: node.isExported,
|
|
70
|
+
}
|
|
71
|
+
: { id: orphanId, name: 'unknown', filePath: 'unknown', startLine: 0 };
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
content: [{
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: JSON.stringify({
|
|
77
|
+
orphans,
|
|
78
|
+
count: orphans.length,
|
|
79
|
+
durationMs: graph.durationMs,
|
|
80
|
+
note: 'Exported functions may be used by external consumers — verify before deleting.',
|
|
81
|
+
}),
|
|
82
|
+
}],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
87
|
+
return {
|
|
88
|
+
content: [{
|
|
89
|
+
type: 'text',
|
|
90
|
+
text: JSON.stringify({
|
|
91
|
+
error: true,
|
|
92
|
+
code: 'PARSE_ERROR',
|
|
93
|
+
message,
|
|
94
|
+
workspacePath,
|
|
95
|
+
}),
|
|
96
|
+
}],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerGetCallees = registerGetCallees;
|
|
37
|
+
const zod_1 = require("zod");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const analysis_1 = require("../utils/analysis");
|
|
40
|
+
const toolHelper_1 = require("../utils/toolHelper");
|
|
41
|
+
function registerGetCallees(server) {
|
|
42
|
+
(0, toolHelper_1.registerTool)(server, 'flowmap_get_callees', 'Return all functions directly called by the named function. Use this to understand what a function depends on.', {
|
|
43
|
+
functionName: zod_1.z.string().describe('The function name to find callees of'),
|
|
44
|
+
workspacePath: zod_1.z.string().describe('Absolute path to the repository root'),
|
|
45
|
+
}, async ({ functionName, workspacePath }) => {
|
|
46
|
+
try {
|
|
47
|
+
if (!fs.existsSync(workspacePath)) {
|
|
48
|
+
return {
|
|
49
|
+
content: [{
|
|
50
|
+
type: 'text',
|
|
51
|
+
text: JSON.stringify({
|
|
52
|
+
error: true,
|
|
53
|
+
code: 'WORKSPACE_NOT_FOUND',
|
|
54
|
+
message: `Directory does not exist: ${workspacePath}`,
|
|
55
|
+
workspacePath,
|
|
56
|
+
}),
|
|
57
|
+
}],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const graph = await (0, analysis_1.analyzeWorkspace)(workspacePath);
|
|
61
|
+
const targets = graph.nodes.filter(n => n.name === functionName);
|
|
62
|
+
if (targets.length === 0) {
|
|
63
|
+
return {
|
|
64
|
+
content: [{
|
|
65
|
+
type: 'text',
|
|
66
|
+
text: JSON.stringify({
|
|
67
|
+
error: true,
|
|
68
|
+
code: 'FUNCTION_NOT_FOUND',
|
|
69
|
+
message: `No function named "${functionName}" found in the codebase.`,
|
|
70
|
+
workspacePath,
|
|
71
|
+
}),
|
|
72
|
+
}],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const target = targets[0];
|
|
76
|
+
const targetIds = new Set(targets.map(t => t.id));
|
|
77
|
+
// Find edges where from is one of the target IDs
|
|
78
|
+
const calleeEdges = graph.edges.filter(e => targetIds.has(e.from));
|
|
79
|
+
const callees = calleeEdges.map(edge => {
|
|
80
|
+
const calleeNode = graph.nodes.find(n => n.id === edge.to);
|
|
81
|
+
return {
|
|
82
|
+
id: edge.to,
|
|
83
|
+
name: calleeNode?.name ?? 'unknown',
|
|
84
|
+
filePath: calleeNode?.filePath ?? 'unknown',
|
|
85
|
+
startLine: calleeNode?.startLine ?? 0,
|
|
86
|
+
callLine: edge.line,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
content: [{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: JSON.stringify({
|
|
93
|
+
target: functionName,
|
|
94
|
+
targetId: target.id,
|
|
95
|
+
callees,
|
|
96
|
+
count: callees.length,
|
|
97
|
+
}),
|
|
98
|
+
}],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
103
|
+
return {
|
|
104
|
+
content: [{
|
|
105
|
+
type: 'text',
|
|
106
|
+
text: JSON.stringify({
|
|
107
|
+
error: true,
|
|
108
|
+
code: 'PARSE_ERROR',
|
|
109
|
+
message,
|
|
110
|
+
workspacePath,
|
|
111
|
+
}),
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerGetCallers = registerGetCallers;
|
|
37
|
+
const zod_1 = require("zod");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const analysis_1 = require("../utils/analysis");
|
|
40
|
+
const toolHelper_1 = require("../utils/toolHelper");
|
|
41
|
+
function registerGetCallers(server) {
|
|
42
|
+
(0, toolHelper_1.registerTool)(server, 'flowmap_get_callers', 'Return all functions that directly call the named function. Use this for impact analysis — to understand what breaks if you change a function\'s signature.', {
|
|
43
|
+
functionName: zod_1.z.string().describe('The function name to find callers of'),
|
|
44
|
+
workspacePath: zod_1.z.string().describe('Absolute path to the repository root'),
|
|
45
|
+
}, async ({ functionName, workspacePath }) => {
|
|
46
|
+
try {
|
|
47
|
+
if (!fs.existsSync(workspacePath)) {
|
|
48
|
+
return {
|
|
49
|
+
content: [{
|
|
50
|
+
type: 'text',
|
|
51
|
+
text: JSON.stringify({
|
|
52
|
+
error: true,
|
|
53
|
+
code: 'WORKSPACE_NOT_FOUND',
|
|
54
|
+
message: `Directory does not exist: ${workspacePath}`,
|
|
55
|
+
workspacePath,
|
|
56
|
+
}),
|
|
57
|
+
}],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const graph = await (0, analysis_1.analyzeWorkspace)(workspacePath);
|
|
61
|
+
// Find target function(s) by name
|
|
62
|
+
const targets = graph.nodes.filter(n => n.name === functionName);
|
|
63
|
+
if (targets.length === 0) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: JSON.stringify({
|
|
68
|
+
error: true,
|
|
69
|
+
code: 'FUNCTION_NOT_FOUND',
|
|
70
|
+
message: `No function named "${functionName}" found in the codebase.`,
|
|
71
|
+
workspacePath,
|
|
72
|
+
}),
|
|
73
|
+
}],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const target = targets[0];
|
|
77
|
+
const targetIds = new Set(targets.map(t => t.id));
|
|
78
|
+
// Find edges where to is one of the target IDs
|
|
79
|
+
const callerEdges = graph.edges.filter(e => targetIds.has(e.to));
|
|
80
|
+
const callers = callerEdges.map(edge => {
|
|
81
|
+
const callerNode = graph.nodes.find(n => n.id === edge.from);
|
|
82
|
+
return {
|
|
83
|
+
id: edge.from,
|
|
84
|
+
name: callerNode?.name ?? 'unknown',
|
|
85
|
+
filePath: callerNode?.filePath ?? 'unknown',
|
|
86
|
+
startLine: callerNode?.startLine ?? 0,
|
|
87
|
+
callLine: edge.line,
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
content: [{
|
|
92
|
+
type: 'text',
|
|
93
|
+
text: JSON.stringify({
|
|
94
|
+
target: functionName,
|
|
95
|
+
targetId: target.id,
|
|
96
|
+
callers,
|
|
97
|
+
count: callers.length,
|
|
98
|
+
}),
|
|
99
|
+
}],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: JSON.stringify({
|
|
108
|
+
error: true,
|
|
109
|
+
code: 'PARSE_ERROR',
|
|
110
|
+
message,
|
|
111
|
+
workspacePath,
|
|
112
|
+
}),
|
|
113
|
+
}],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|