depwire-cli 0.9.19 → 0.9.21
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 +105 -33
- package/dist/{chunk-S3NZMIIU.js → chunk-QHVWDUSX.js} +382 -1874
- package/dist/chunk-XBCQPU63.js +2002 -0
- package/dist/index.js +172 -28
- package/dist/mcpb-entry.js +5 -3
- package/dist/sdk.d.ts +237 -0
- package/dist/sdk.js +32 -0
- package/package.json +16 -4
|
@@ -0,0 +1,2002 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SimulationEngine,
|
|
3
|
+
analyzeDeadCode,
|
|
4
|
+
buildGraph,
|
|
5
|
+
calculateHealthScore,
|
|
6
|
+
findSymbols,
|
|
7
|
+
generateDocs,
|
|
8
|
+
getArchitectureSummary,
|
|
9
|
+
getCrossFileEdges,
|
|
10
|
+
getDependencies,
|
|
11
|
+
getDependents,
|
|
12
|
+
getFileSummary,
|
|
13
|
+
getImpact,
|
|
14
|
+
loadMetadata,
|
|
15
|
+
parseProject,
|
|
16
|
+
parseTypeScriptFile,
|
|
17
|
+
searchSymbols
|
|
18
|
+
} from "./chunk-QHVWDUSX.js";
|
|
19
|
+
|
|
20
|
+
// src/viz/data.ts
|
|
21
|
+
import { basename } from "path";
|
|
22
|
+
function prepareVizData(graph, projectRoot) {
|
|
23
|
+
const fileSummary = getFileSummary(graph);
|
|
24
|
+
const crossFileEdges = getCrossFileEdges(graph);
|
|
25
|
+
const files = fileSummary.map((f) => ({
|
|
26
|
+
path: f.filePath,
|
|
27
|
+
directory: f.filePath.includes("/") ? f.filePath.substring(0, f.filePath.lastIndexOf("/")) : ".",
|
|
28
|
+
symbolCount: f.symbolCount,
|
|
29
|
+
incomingCount: f.incomingRefs,
|
|
30
|
+
outgoingCount: f.outgoingRefs
|
|
31
|
+
}));
|
|
32
|
+
files.sort((a, b) => {
|
|
33
|
+
if (a.directory !== b.directory) {
|
|
34
|
+
return a.directory.localeCompare(b.directory);
|
|
35
|
+
}
|
|
36
|
+
return a.path.localeCompare(b.path);
|
|
37
|
+
});
|
|
38
|
+
const arcMap = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const edge of crossFileEdges) {
|
|
40
|
+
const key = `${edge.sourceFile}::${edge.targetFile}`;
|
|
41
|
+
if (arcMap.has(key)) {
|
|
42
|
+
const arc = arcMap.get(key);
|
|
43
|
+
arc.edgeCount++;
|
|
44
|
+
if (!arc.edgeKinds.includes(edge.kind)) {
|
|
45
|
+
arc.edgeKinds.push(edge.kind);
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
arcMap.set(key, {
|
|
49
|
+
sourceFile: edge.sourceFile,
|
|
50
|
+
targetFile: edge.targetFile,
|
|
51
|
+
edgeCount: 1,
|
|
52
|
+
edgeKinds: [edge.kind]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const arcs = Array.from(arcMap.values());
|
|
57
|
+
const projectName = basename(projectRoot);
|
|
58
|
+
return {
|
|
59
|
+
files,
|
|
60
|
+
arcs,
|
|
61
|
+
stats: {
|
|
62
|
+
totalFiles: files.length,
|
|
63
|
+
totalSymbols: graph.order,
|
|
64
|
+
totalEdges: graph.size,
|
|
65
|
+
totalCrossFileEdges: arcs.reduce((sum, arc) => sum + arc.edgeCount, 0)
|
|
66
|
+
},
|
|
67
|
+
projectName
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/watcher.ts
|
|
72
|
+
import chokidar from "chokidar";
|
|
73
|
+
function watchProject(projectRoot, callbacks) {
|
|
74
|
+
console.error(`[Watcher] Creating watcher for: ${projectRoot}`);
|
|
75
|
+
const watcherOptions = {
|
|
76
|
+
ignored: [
|
|
77
|
+
"**/node_modules/**",
|
|
78
|
+
"**/vendor/**",
|
|
79
|
+
// Go dependencies
|
|
80
|
+
"**/.git/**",
|
|
81
|
+
"**/dist/**",
|
|
82
|
+
"**/build/**",
|
|
83
|
+
"**/coverage/**",
|
|
84
|
+
"**/.next/**",
|
|
85
|
+
"**/.turbo/**",
|
|
86
|
+
"**/.DS_Store",
|
|
87
|
+
// macOS metadata
|
|
88
|
+
"**/.env",
|
|
89
|
+
// Environment files
|
|
90
|
+
"**/.env.*",
|
|
91
|
+
// Environment variants
|
|
92
|
+
"**/.eslintcache",
|
|
93
|
+
// ESLint cache
|
|
94
|
+
"**/.vscode/**",
|
|
95
|
+
// VS Code settings
|
|
96
|
+
"**/.idea/**"
|
|
97
|
+
// IntelliJ IDEA settings
|
|
98
|
+
],
|
|
99
|
+
ignoreInitial: true,
|
|
100
|
+
// Don't fire events for existing files
|
|
101
|
+
persistent: true,
|
|
102
|
+
followSymlinks: false,
|
|
103
|
+
usePolling: true,
|
|
104
|
+
// Use polling for macOS reliability
|
|
105
|
+
interval: 1e3,
|
|
106
|
+
// Poll every second
|
|
107
|
+
atomic: true,
|
|
108
|
+
// Handle atomic writes (VS Code, Sublime, etc.)
|
|
109
|
+
awaitWriteFinish: {
|
|
110
|
+
stabilityThreshold: 300,
|
|
111
|
+
// Wait 300ms after last change before firing
|
|
112
|
+
pollInterval: 100
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const watcher = chokidar.watch(projectRoot, watcherOptions);
|
|
116
|
+
console.error("[Watcher] Attaching event listeners...");
|
|
117
|
+
watcher.on("change", (absolutePath) => {
|
|
118
|
+
const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".c", ".h"];
|
|
119
|
+
if (!validExtensions.some((ext) => absolutePath.endsWith(ext))) return;
|
|
120
|
+
if (absolutePath.endsWith("_test.go")) return;
|
|
121
|
+
const relativePath = absolutePath.replace(projectRoot + "/", "");
|
|
122
|
+
console.error(`[Watcher] Change event: ${relativePath}`);
|
|
123
|
+
callbacks.onFileChanged(relativePath);
|
|
124
|
+
});
|
|
125
|
+
watcher.on("add", (absolutePath) => {
|
|
126
|
+
const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".c", ".h"];
|
|
127
|
+
if (!validExtensions.some((ext) => absolutePath.endsWith(ext))) return;
|
|
128
|
+
if (absolutePath.endsWith("_test.go")) return;
|
|
129
|
+
const relativePath = absolutePath.replace(projectRoot + "/", "");
|
|
130
|
+
console.error(`[Watcher] Add event: ${relativePath}`);
|
|
131
|
+
callbacks.onFileAdded(relativePath);
|
|
132
|
+
});
|
|
133
|
+
watcher.on("unlink", (absolutePath) => {
|
|
134
|
+
const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".c", ".h"];
|
|
135
|
+
if (!validExtensions.some((ext) => absolutePath.endsWith(ext))) return;
|
|
136
|
+
if (absolutePath.endsWith("_test.go")) return;
|
|
137
|
+
const relativePath = absolutePath.replace(projectRoot + "/", "");
|
|
138
|
+
console.error(`[Watcher] Unlink event: ${relativePath}`);
|
|
139
|
+
callbacks.onFileDeleted(relativePath);
|
|
140
|
+
});
|
|
141
|
+
watcher.on("error", (error) => {
|
|
142
|
+
console.error("[Watcher] Error:", error);
|
|
143
|
+
});
|
|
144
|
+
watcher.on("ready", () => {
|
|
145
|
+
console.error("[Watcher] Ready \u2014 watching for changes");
|
|
146
|
+
const watched = watcher.getWatched();
|
|
147
|
+
const dirs = Object.keys(watched);
|
|
148
|
+
let fileCount = 0;
|
|
149
|
+
for (const dir of dirs) {
|
|
150
|
+
const files = watched[dir];
|
|
151
|
+
fileCount += files.filter(
|
|
152
|
+
(f) => f.endsWith(".ts") || f.endsWith(".tsx") || f.endsWith(".js") || f.endsWith(".jsx") || f.endsWith(".mjs") || f.endsWith(".cjs") || f.endsWith(".py") || f.endsWith(".go") && !f.endsWith("_test.go") || f.endsWith(".rs") || f.endsWith(".c") || f.endsWith(".h")
|
|
153
|
+
).length;
|
|
154
|
+
}
|
|
155
|
+
console.error(`[Watcher] Watching ${fileCount} TypeScript/JavaScript/Python/Go/Rust/C files in ${dirs.length} directories`);
|
|
156
|
+
});
|
|
157
|
+
return watcher;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/viz/server.ts
|
|
161
|
+
import express from "express";
|
|
162
|
+
import open from "open";
|
|
163
|
+
import { fileURLToPath } from "url";
|
|
164
|
+
import { dirname, join } from "path";
|
|
165
|
+
import { WebSocketServer } from "ws";
|
|
166
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
167
|
+
var __dirname2 = dirname(__filename);
|
|
168
|
+
var activeServer = null;
|
|
169
|
+
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
170
|
+
const net = await import("net");
|
|
171
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
172
|
+
const testPort = startPort + attempt;
|
|
173
|
+
const isAvailable = await new Promise((resolve2) => {
|
|
174
|
+
const server = net.createServer();
|
|
175
|
+
server.once("error", () => {
|
|
176
|
+
resolve2(false);
|
|
177
|
+
});
|
|
178
|
+
server.once("listening", () => {
|
|
179
|
+
server.close();
|
|
180
|
+
resolve2(true);
|
|
181
|
+
});
|
|
182
|
+
server.listen(testPort, "127.0.0.1");
|
|
183
|
+
});
|
|
184
|
+
if (isAvailable) {
|
|
185
|
+
if (attempt > 0) {
|
|
186
|
+
console.error(`Port ${startPort} in use, using port ${testPort} instead`);
|
|
187
|
+
}
|
|
188
|
+
return testPort;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`No available ports found between ${startPort} and ${startPort + maxAttempts - 1}`);
|
|
192
|
+
}
|
|
193
|
+
async function startVizServer(initialVizData, graph, projectRoot, port = 3333, shouldOpen = true, options) {
|
|
194
|
+
if (activeServer) {
|
|
195
|
+
console.error(`Visualization server already running at ${activeServer.url}`);
|
|
196
|
+
return {
|
|
197
|
+
server: activeServer.server,
|
|
198
|
+
url: activeServer.url,
|
|
199
|
+
alreadyRunning: true
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const availablePort = await findAvailablePort(port);
|
|
203
|
+
const app = express();
|
|
204
|
+
let vizData = initialVizData;
|
|
205
|
+
const publicDir = join(__dirname2, "viz", "public");
|
|
206
|
+
app.use(express.static(publicDir));
|
|
207
|
+
app.get("/api/graph", (req, res) => {
|
|
208
|
+
res.json(vizData);
|
|
209
|
+
});
|
|
210
|
+
const server = app.listen(availablePort, "127.0.0.1", () => {
|
|
211
|
+
const url2 = `http://127.0.0.1:${availablePort}`;
|
|
212
|
+
console.error(`
|
|
213
|
+
Depwire visualization running at ${url2}`);
|
|
214
|
+
console.error("Press Ctrl+C to stop\n");
|
|
215
|
+
activeServer = { server, port: availablePort, url: url2 };
|
|
216
|
+
if (shouldOpen) {
|
|
217
|
+
open(url2);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
const wss = new WebSocketServer({ server });
|
|
221
|
+
wss.on("connection", (ws) => {
|
|
222
|
+
console.error("Browser connected to WebSocket");
|
|
223
|
+
ws.on("close", () => {
|
|
224
|
+
console.error("Browser disconnected from WebSocket");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
function broadcastRefresh() {
|
|
228
|
+
wss.clients.forEach((client) => {
|
|
229
|
+
if (client.readyState === 1) {
|
|
230
|
+
client.send(JSON.stringify({ type: "refresh" }));
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
console.error("Starting file watcher...");
|
|
235
|
+
const watcher = watchProject(projectRoot, {
|
|
236
|
+
onFileChanged: async (filePath) => {
|
|
237
|
+
console.error(`File changed: ${filePath} \u2014 re-parsing project...`);
|
|
238
|
+
try {
|
|
239
|
+
const parsedFiles = await parseProject(projectRoot, options);
|
|
240
|
+
const newGraph = buildGraph(parsedFiles);
|
|
241
|
+
graph.clear();
|
|
242
|
+
newGraph.forEachNode((node, attrs) => {
|
|
243
|
+
graph.addNode(node, attrs);
|
|
244
|
+
});
|
|
245
|
+
newGraph.forEachEdge((edge, attrs, source, target) => {
|
|
246
|
+
graph.addEdge(source, target, attrs);
|
|
247
|
+
});
|
|
248
|
+
vizData = prepareVizData(graph, projectRoot);
|
|
249
|
+
broadcastRefresh();
|
|
250
|
+
console.error(`Graph updated (${vizData.stats.totalSymbols} symbols, ${vizData.stats.totalCrossFileEdges} edges)`);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error(`Failed to update graph for ${filePath}:`, error);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
onFileAdded: async (filePath) => {
|
|
256
|
+
console.error(`File added: ${filePath} \u2014 re-parsing project...`);
|
|
257
|
+
try {
|
|
258
|
+
const parsedFiles = await parseProject(projectRoot, options);
|
|
259
|
+
const newGraph = buildGraph(parsedFiles);
|
|
260
|
+
graph.clear();
|
|
261
|
+
newGraph.forEachNode((node, attrs) => {
|
|
262
|
+
graph.addNode(node, attrs);
|
|
263
|
+
});
|
|
264
|
+
newGraph.forEachEdge((edge, attrs, source, target) => {
|
|
265
|
+
graph.addEdge(source, target, attrs);
|
|
266
|
+
});
|
|
267
|
+
vizData = prepareVizData(graph, projectRoot);
|
|
268
|
+
broadcastRefresh();
|
|
269
|
+
console.error(`Graph updated (${vizData.stats.totalSymbols} symbols, ${vizData.stats.totalCrossFileEdges} edges)`);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error(`Failed to update graph for ${filePath}:`, error);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
onFileDeleted: async (filePath) => {
|
|
275
|
+
console.error(`File deleted: ${filePath} \u2014 re-parsing project...`);
|
|
276
|
+
try {
|
|
277
|
+
const parsedFiles = await parseProject(projectRoot, options);
|
|
278
|
+
const newGraph = buildGraph(parsedFiles);
|
|
279
|
+
graph.clear();
|
|
280
|
+
newGraph.forEachNode((node, attrs) => {
|
|
281
|
+
graph.addNode(node, attrs);
|
|
282
|
+
});
|
|
283
|
+
newGraph.forEachEdge((edge, attrs, source, target) => {
|
|
284
|
+
graph.addEdge(source, target, attrs);
|
|
285
|
+
});
|
|
286
|
+
vizData = prepareVizData(graph, projectRoot);
|
|
287
|
+
broadcastRefresh();
|
|
288
|
+
console.error(`Graph updated (${vizData.stats.totalSymbols} symbols, ${vizData.stats.totalCrossFileEdges} edges)`);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error(`Failed to remove ${filePath} from graph:`, error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
process.on("SIGINT", () => {
|
|
295
|
+
console.error("\nShutting down visualization server...");
|
|
296
|
+
activeServer = null;
|
|
297
|
+
watcher.close();
|
|
298
|
+
wss.close();
|
|
299
|
+
server.close(() => {
|
|
300
|
+
process.exit(0);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
const url = `http://127.0.0.1:${availablePort}`;
|
|
304
|
+
return { server, url, alreadyRunning: false };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/mcp/state.ts
|
|
308
|
+
function createEmptyState() {
|
|
309
|
+
return {
|
|
310
|
+
graph: null,
|
|
311
|
+
projectRoot: null,
|
|
312
|
+
projectName: null,
|
|
313
|
+
watcher: null
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function isProjectLoaded(state) {
|
|
317
|
+
return state.graph !== null && state.projectRoot !== null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/graph/updater.ts
|
|
321
|
+
import { join as join2 } from "path";
|
|
322
|
+
function removeFileFromGraph(graph, filePath) {
|
|
323
|
+
const nodesToRemove = [];
|
|
324
|
+
graph.forEachNode((node, attrs) => {
|
|
325
|
+
if (attrs.filePath === filePath) {
|
|
326
|
+
nodesToRemove.push(node);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
nodesToRemove.forEach((node) => {
|
|
330
|
+
try {
|
|
331
|
+
graph.dropNode(node);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function addFileToGraph(graph, parsedFile) {
|
|
337
|
+
for (const symbol of parsedFile.symbols) {
|
|
338
|
+
const nodeId = `${parsedFile.filePath}::${symbol.name}`;
|
|
339
|
+
try {
|
|
340
|
+
graph.addNode(nodeId, {
|
|
341
|
+
name: symbol.name,
|
|
342
|
+
kind: symbol.kind,
|
|
343
|
+
filePath: parsedFile.filePath,
|
|
344
|
+
startLine: symbol.location.startLine,
|
|
345
|
+
endLine: symbol.location.endLine,
|
|
346
|
+
exported: symbol.exported,
|
|
347
|
+
scope: symbol.scope
|
|
348
|
+
});
|
|
349
|
+
} catch (error) {
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const edge of parsedFile.edges) {
|
|
353
|
+
try {
|
|
354
|
+
graph.mergeEdge(edge.source, edge.target, {
|
|
355
|
+
kind: edge.kind,
|
|
356
|
+
sourceFile: edge.sourceFile,
|
|
357
|
+
targetFile: edge.targetFile
|
|
358
|
+
});
|
|
359
|
+
} catch (error) {
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function updateFileInGraph(graph, projectRoot, relativeFilePath) {
|
|
364
|
+
removeFileFromGraph(graph, relativeFilePath);
|
|
365
|
+
const absolutePath = join2(projectRoot, relativeFilePath);
|
|
366
|
+
try {
|
|
367
|
+
const parsedFile = parseTypeScriptFile(absolutePath, relativeFilePath);
|
|
368
|
+
addFileToGraph(graph, parsedFile);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.error(`Failed to parse file ${relativeFilePath}:`, error);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/mcp/server.ts
|
|
375
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
376
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
377
|
+
|
|
378
|
+
// src/mcp/tools.ts
|
|
379
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
380
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
381
|
+
|
|
382
|
+
// src/mcp/connect.ts
|
|
383
|
+
import simpleGit from "simple-git";
|
|
384
|
+
import { existsSync } from "fs";
|
|
385
|
+
import { join as join3, basename as basename2, resolve } from "path";
|
|
386
|
+
import { tmpdir, homedir } from "os";
|
|
387
|
+
function validateProjectPath(source) {
|
|
388
|
+
const resolved = resolve(source);
|
|
389
|
+
const blockedPaths = [
|
|
390
|
+
"/etc",
|
|
391
|
+
"/var",
|
|
392
|
+
"/usr",
|
|
393
|
+
"/bin",
|
|
394
|
+
"/sbin",
|
|
395
|
+
"/boot",
|
|
396
|
+
"/proc",
|
|
397
|
+
"/sys",
|
|
398
|
+
join3(homedir(), ".ssh"),
|
|
399
|
+
join3(homedir(), ".gnupg"),
|
|
400
|
+
join3(homedir(), ".aws"),
|
|
401
|
+
join3(homedir(), ".config"),
|
|
402
|
+
join3(homedir(), ".env")
|
|
403
|
+
];
|
|
404
|
+
for (const blocked of blockedPaths) {
|
|
405
|
+
if (resolved.startsWith(blocked)) {
|
|
406
|
+
return { valid: false, error: `Access denied: ${blocked} is a protected path` };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return { valid: true };
|
|
410
|
+
}
|
|
411
|
+
async function connectToRepo(source, subdirectory, state) {
|
|
412
|
+
try {
|
|
413
|
+
let projectRoot;
|
|
414
|
+
let projectName;
|
|
415
|
+
const isGitHub = source.startsWith("https://github.com/") || source.startsWith("git@github.com:");
|
|
416
|
+
if (isGitHub) {
|
|
417
|
+
const match = source.match(/[\/:]([^\/]+?)(?:\.git)?$/);
|
|
418
|
+
if (!match) {
|
|
419
|
+
return {
|
|
420
|
+
error: "Invalid GitHub URL",
|
|
421
|
+
message: "Could not parse repository name from URL"
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
projectName = match[1];
|
|
425
|
+
const reposDir = join3(tmpdir(), "depwire-repos");
|
|
426
|
+
const cloneDir = join3(reposDir, projectName);
|
|
427
|
+
console.error(`Connecting to GitHub repo: ${source}`);
|
|
428
|
+
const git = simpleGit();
|
|
429
|
+
if (existsSync(cloneDir)) {
|
|
430
|
+
console.error(`Repo already cloned at ${cloneDir}, pulling latest changes...`);
|
|
431
|
+
try {
|
|
432
|
+
await git.cwd(cloneDir).pull();
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error(`Pull failed, using existing clone: ${error}`);
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
console.error(`Cloning ${source} to ${cloneDir}...`);
|
|
438
|
+
try {
|
|
439
|
+
await git.clone(source, cloneDir, ["--depth", "1", "--no-recurse-submodules", "--single-branch"]);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
return {
|
|
442
|
+
error: "Failed to clone repository",
|
|
443
|
+
message: `Git clone failed: ${error}. Ensure git is installed and the URL is correct.`
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
projectRoot = subdirectory ? join3(cloneDir, subdirectory) : cloneDir;
|
|
448
|
+
} else {
|
|
449
|
+
const validation2 = validateProjectPath(source);
|
|
450
|
+
if (!validation2.valid) {
|
|
451
|
+
return {
|
|
452
|
+
error: "Access denied",
|
|
453
|
+
message: validation2.error
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (!existsSync(source)) {
|
|
457
|
+
return {
|
|
458
|
+
error: "Directory not found",
|
|
459
|
+
message: `Directory does not exist: ${source}`
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
projectRoot = subdirectory ? join3(source, subdirectory) : source;
|
|
463
|
+
projectName = basename2(projectRoot);
|
|
464
|
+
}
|
|
465
|
+
const validation = validateProjectPath(projectRoot);
|
|
466
|
+
if (!validation.valid) {
|
|
467
|
+
return {
|
|
468
|
+
error: "Access denied",
|
|
469
|
+
message: validation.error
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (!existsSync(projectRoot)) {
|
|
473
|
+
return {
|
|
474
|
+
error: "Project root not found",
|
|
475
|
+
message: `Directory does not exist: ${projectRoot}`
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
console.error(`Parsing project at ${projectRoot}...`);
|
|
479
|
+
if (state.watcher) {
|
|
480
|
+
console.error("Stopping previous file watcher...");
|
|
481
|
+
await state.watcher.close();
|
|
482
|
+
state.watcher = null;
|
|
483
|
+
}
|
|
484
|
+
const parsedFiles = await parseProject(projectRoot);
|
|
485
|
+
if (parsedFiles.length === 0) {
|
|
486
|
+
return {
|
|
487
|
+
error: "No source files found",
|
|
488
|
+
message: `No supported source files (.ts, .tsx, .js, .jsx, .py, .go) found in ${projectRoot}`
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const graph = buildGraph(parsedFiles);
|
|
492
|
+
state.graph = graph;
|
|
493
|
+
state.projectRoot = projectRoot;
|
|
494
|
+
state.projectName = projectName;
|
|
495
|
+
console.error(`Parsed ${parsedFiles.length} files`);
|
|
496
|
+
console.error("Starting file watcher...");
|
|
497
|
+
state.watcher = watchProject(projectRoot, {
|
|
498
|
+
onFileChanged: async (filePath) => {
|
|
499
|
+
console.error(`File changed: ${filePath}`);
|
|
500
|
+
try {
|
|
501
|
+
await updateFileInGraph(state.graph, projectRoot, filePath);
|
|
502
|
+
console.error(`Graph updated for ${filePath}`);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error(`Failed to update graph for ${filePath}: ${error}`);
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
onFileAdded: async (filePath) => {
|
|
508
|
+
console.error(`File added: ${filePath}`);
|
|
509
|
+
try {
|
|
510
|
+
await updateFileInGraph(state.graph, projectRoot, filePath);
|
|
511
|
+
console.error(`Graph updated for ${filePath}`);
|
|
512
|
+
} catch (error) {
|
|
513
|
+
console.error(`Failed to update graph for ${filePath}: ${error}`);
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
onFileDeleted: (filePath) => {
|
|
517
|
+
console.error(`File deleted: ${filePath}`);
|
|
518
|
+
try {
|
|
519
|
+
const fileNodes = state.graph.filterNodes(
|
|
520
|
+
(node, attrs) => attrs.filePath === filePath
|
|
521
|
+
);
|
|
522
|
+
fileNodes.forEach((node) => state.graph.dropNode(node));
|
|
523
|
+
console.error(`Removed ${filePath} from graph`);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
console.error(`Failed to remove ${filePath} from graph: ${error}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
const summary = getArchitectureSummary(graph);
|
|
530
|
+
const mostConnected = summary.mostConnectedFiles.slice(0, 3);
|
|
531
|
+
const languageBreakdown = {};
|
|
532
|
+
parsedFiles.forEach((file) => {
|
|
533
|
+
const ext = file.filePath.toLowerCase();
|
|
534
|
+
let lang;
|
|
535
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
|
|
536
|
+
lang = "typescript";
|
|
537
|
+
} else if (ext.endsWith(".py")) {
|
|
538
|
+
lang = "python";
|
|
539
|
+
} else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
|
|
540
|
+
lang = "javascript";
|
|
541
|
+
} else if (ext.endsWith(".go")) {
|
|
542
|
+
lang = "go";
|
|
543
|
+
} else {
|
|
544
|
+
lang = "other";
|
|
545
|
+
}
|
|
546
|
+
languageBreakdown[lang] = (languageBreakdown[lang] || 0) + 1;
|
|
547
|
+
});
|
|
548
|
+
return {
|
|
549
|
+
connected: true,
|
|
550
|
+
projectRoot,
|
|
551
|
+
projectName,
|
|
552
|
+
stats: {
|
|
553
|
+
files: summary.totalFiles,
|
|
554
|
+
symbols: summary.totalSymbols,
|
|
555
|
+
edges: summary.totalEdges,
|
|
556
|
+
crossFileEdges: summary.crossFileEdges,
|
|
557
|
+
languages: languageBreakdown
|
|
558
|
+
},
|
|
559
|
+
mostConnectedFiles: mostConnected.map((f) => ({
|
|
560
|
+
path: f.filePath,
|
|
561
|
+
connections: f.incomingCount + f.outgoingCount
|
|
562
|
+
})),
|
|
563
|
+
summary: `Connected to ${projectName}. Found ${summary.totalFiles} files with ${summary.totalSymbols} symbols and ${summary.crossFileEdges} cross-file edges.`
|
|
564
|
+
};
|
|
565
|
+
} catch (error) {
|
|
566
|
+
console.error("Error in connectToRepo:", error);
|
|
567
|
+
return {
|
|
568
|
+
error: "Connection failed",
|
|
569
|
+
message: String(error)
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/temporal/git.ts
|
|
575
|
+
import { execSync } from "child_process";
|
|
576
|
+
async function getCommitLog(dir, limit) {
|
|
577
|
+
try {
|
|
578
|
+
const limitArg = limit ? `-n ${limit}` : "";
|
|
579
|
+
const output = execSync(
|
|
580
|
+
`git log ${limitArg} --pretty=format:"%H|%aI|%s|%an"`,
|
|
581
|
+
{ cwd: dir, encoding: "utf-8" }
|
|
582
|
+
);
|
|
583
|
+
if (!output.trim()) {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
return output.trim().split("\n").map((line) => {
|
|
587
|
+
const [hash, date, message, author] = line.split("|");
|
|
588
|
+
return { hash, date, message, author };
|
|
589
|
+
});
|
|
590
|
+
} catch (error) {
|
|
591
|
+
throw new Error(`Failed to get git log: ${error}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
async function getCurrentBranch(dir) {
|
|
595
|
+
try {
|
|
596
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
597
|
+
cwd: dir,
|
|
598
|
+
encoding: "utf-8"
|
|
599
|
+
}).trim();
|
|
600
|
+
} catch (error) {
|
|
601
|
+
throw new Error(`Failed to get current branch: ${error}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async function checkoutCommit(dir, hash) {
|
|
605
|
+
try {
|
|
606
|
+
execSync(`git checkout -q ${hash}`, { cwd: dir, stdio: "ignore" });
|
|
607
|
+
} catch (error) {
|
|
608
|
+
throw new Error(`Failed to checkout commit ${hash}: ${error}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async function restoreOriginal(dir, originalBranch) {
|
|
612
|
+
try {
|
|
613
|
+
execSync(`git checkout -q ${originalBranch}`, {
|
|
614
|
+
cwd: dir,
|
|
615
|
+
stdio: "ignore"
|
|
616
|
+
});
|
|
617
|
+
} catch (error) {
|
|
618
|
+
throw new Error(`Failed to restore branch ${originalBranch}: ${error}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async function stashChanges(dir) {
|
|
622
|
+
try {
|
|
623
|
+
const status = execSync("git status --porcelain", {
|
|
624
|
+
cwd: dir,
|
|
625
|
+
encoding: "utf-8"
|
|
626
|
+
}).trim();
|
|
627
|
+
if (status) {
|
|
628
|
+
execSync('git stash push -q -m "depwire temporal analysis"', {
|
|
629
|
+
cwd: dir,
|
|
630
|
+
stdio: "ignore"
|
|
631
|
+
});
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
return false;
|
|
635
|
+
} catch (error) {
|
|
636
|
+
throw new Error(`Failed to stash changes: ${error}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async function popStash(dir) {
|
|
640
|
+
try {
|
|
641
|
+
const stashList = execSync("git stash list", {
|
|
642
|
+
cwd: dir,
|
|
643
|
+
encoding: "utf-8",
|
|
644
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
645
|
+
// Suppress stderr
|
|
646
|
+
}).trim();
|
|
647
|
+
if (stashList) {
|
|
648
|
+
execSync("git stash pop -q", { cwd: dir, stdio: "ignore" });
|
|
649
|
+
}
|
|
650
|
+
} catch (error) {
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function isGitRepo(dir) {
|
|
654
|
+
try {
|
|
655
|
+
execSync("git rev-parse --git-dir", { cwd: dir, stdio: "ignore" });
|
|
656
|
+
return true;
|
|
657
|
+
} catch {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/temporal/sampler.ts
|
|
663
|
+
function sampleCommits(commits, targetCount, strategy) {
|
|
664
|
+
if (commits.length === 0) {
|
|
665
|
+
return [];
|
|
666
|
+
}
|
|
667
|
+
if (commits.length <= targetCount) {
|
|
668
|
+
return commits;
|
|
669
|
+
}
|
|
670
|
+
switch (strategy) {
|
|
671
|
+
case "even":
|
|
672
|
+
return sampleEvenly(commits, targetCount);
|
|
673
|
+
case "weekly":
|
|
674
|
+
return sampleWeekly(commits, targetCount);
|
|
675
|
+
case "monthly":
|
|
676
|
+
return sampleMonthly(commits, targetCount);
|
|
677
|
+
default:
|
|
678
|
+
return sampleEvenly(commits, targetCount);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function sampleEvenly(commits, targetCount) {
|
|
682
|
+
if (targetCount >= commits.length) {
|
|
683
|
+
return commits;
|
|
684
|
+
}
|
|
685
|
+
const result = [];
|
|
686
|
+
const step = (commits.length - 1) / (targetCount - 1);
|
|
687
|
+
for (let i = 0; i < targetCount; i++) {
|
|
688
|
+
const index = Math.round(i * step);
|
|
689
|
+
result.push(commits[index]);
|
|
690
|
+
}
|
|
691
|
+
return result;
|
|
692
|
+
}
|
|
693
|
+
function sampleWeekly(commits, targetCount) {
|
|
694
|
+
const result = [];
|
|
695
|
+
const first = commits[0];
|
|
696
|
+
const last = commits[commits.length - 1];
|
|
697
|
+
result.push(first);
|
|
698
|
+
const weekMap = /* @__PURE__ */ new Map();
|
|
699
|
+
for (const commit of commits) {
|
|
700
|
+
const date = new Date(commit.date);
|
|
701
|
+
const year = date.getFullYear();
|
|
702
|
+
const week = getWeekNumber(date);
|
|
703
|
+
const key = `${year}-W${week}`;
|
|
704
|
+
weekMap.set(key, commit);
|
|
705
|
+
}
|
|
706
|
+
const weeklyCommits = Array.from(weekMap.values());
|
|
707
|
+
if (weeklyCommits.length <= targetCount) {
|
|
708
|
+
return weeklyCommits;
|
|
709
|
+
}
|
|
710
|
+
const step = Math.floor((weeklyCommits.length - 2) / (targetCount - 2));
|
|
711
|
+
for (let i = 1; i < targetCount - 1; i++) {
|
|
712
|
+
const index = Math.min(i * step, weeklyCommits.length - 2);
|
|
713
|
+
if (weeklyCommits[index] !== first && weeklyCommits[index] !== last) {
|
|
714
|
+
result.push(weeklyCommits[index]);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (result[result.length - 1] !== last) {
|
|
718
|
+
result.push(last);
|
|
719
|
+
}
|
|
720
|
+
return result;
|
|
721
|
+
}
|
|
722
|
+
function sampleMonthly(commits, targetCount) {
|
|
723
|
+
const result = [];
|
|
724
|
+
const first = commits[0];
|
|
725
|
+
const last = commits[commits.length - 1];
|
|
726
|
+
result.push(first);
|
|
727
|
+
const monthMap = /* @__PURE__ */ new Map();
|
|
728
|
+
for (const commit of commits) {
|
|
729
|
+
const date = new Date(commit.date);
|
|
730
|
+
const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
731
|
+
monthMap.set(key, commit);
|
|
732
|
+
}
|
|
733
|
+
const monthlyCommits = Array.from(monthMap.values());
|
|
734
|
+
if (monthlyCommits.length <= targetCount) {
|
|
735
|
+
return monthlyCommits;
|
|
736
|
+
}
|
|
737
|
+
const step = Math.floor((monthlyCommits.length - 2) / (targetCount - 2));
|
|
738
|
+
for (let i = 1; i < targetCount - 1; i++) {
|
|
739
|
+
const index = Math.min(i * step, monthlyCommits.length - 2);
|
|
740
|
+
if (monthlyCommits[index] !== first && monthlyCommits[index] !== last) {
|
|
741
|
+
result.push(monthlyCommits[index]);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (result[result.length - 1] !== last) {
|
|
745
|
+
result.push(last);
|
|
746
|
+
}
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
function getWeekNumber(date) {
|
|
750
|
+
const d = new Date(
|
|
751
|
+
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
|
|
752
|
+
);
|
|
753
|
+
const dayNum = d.getUTCDay() || 7;
|
|
754
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
755
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
756
|
+
return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/temporal/snapshots.ts
|
|
760
|
+
import { writeFileSync, readFileSync, mkdirSync, existsSync as existsSync2, readdirSync } from "fs";
|
|
761
|
+
import { join as join4 } from "path";
|
|
762
|
+
function saveSnapshot(snapshot, outputDir) {
|
|
763
|
+
if (!existsSync2(outputDir)) {
|
|
764
|
+
mkdirSync(outputDir, { recursive: true });
|
|
765
|
+
}
|
|
766
|
+
const filename = `${snapshot.commitHash.substring(0, 8)}.json`;
|
|
767
|
+
const filepath = join4(outputDir, filename);
|
|
768
|
+
writeFileSync(filepath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
769
|
+
}
|
|
770
|
+
function loadSnapshot(commitHash, outputDir) {
|
|
771
|
+
const shortHash = commitHash.substring(0, 8);
|
|
772
|
+
const filepath = join4(outputDir, `${shortHash}.json`);
|
|
773
|
+
if (!existsSync2(filepath)) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
const content = readFileSync(filepath, "utf-8");
|
|
778
|
+
return JSON.parse(content);
|
|
779
|
+
} catch {
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function createSnapshot(graph, commitHash, commitDate, commitMessage, commitAuthor) {
|
|
784
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
785
|
+
for (const node of graph.nodes) {
|
|
786
|
+
if (!fileMap.has(node.filePath)) {
|
|
787
|
+
fileMap.set(node.filePath, { symbols: 0, inbound: 0, outbound: 0 });
|
|
788
|
+
}
|
|
789
|
+
fileMap.get(node.filePath).symbols++;
|
|
790
|
+
}
|
|
791
|
+
for (const edge of graph.edges) {
|
|
792
|
+
const sourceNode = graph.nodes.find((n) => n.id === edge.source);
|
|
793
|
+
const targetNode = graph.nodes.find((n) => n.id === edge.target);
|
|
794
|
+
if (sourceNode && targetNode && sourceNode.filePath !== targetNode.filePath) {
|
|
795
|
+
if (fileMap.has(sourceNode.filePath)) {
|
|
796
|
+
fileMap.get(sourceNode.filePath).outbound++;
|
|
797
|
+
}
|
|
798
|
+
if (fileMap.has(targetNode.filePath)) {
|
|
799
|
+
fileMap.get(targetNode.filePath).inbound++;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const files = Array.from(fileMap.entries()).map(([path, data]) => ({
|
|
804
|
+
path,
|
|
805
|
+
symbols: data.symbols,
|
|
806
|
+
connections: data.inbound + data.outbound
|
|
807
|
+
}));
|
|
808
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
809
|
+
for (const edge of graph.edges) {
|
|
810
|
+
const sourceNode = graph.nodes.find((n) => n.id === edge.source);
|
|
811
|
+
const targetNode = graph.nodes.find((n) => n.id === edge.target);
|
|
812
|
+
if (sourceNode && targetNode && sourceNode.filePath !== targetNode.filePath) {
|
|
813
|
+
const key = sourceNode.filePath < targetNode.filePath ? `${sourceNode.filePath}|${targetNode.filePath}` : `${targetNode.filePath}|${sourceNode.filePath}`;
|
|
814
|
+
edgeMap.set(key, (edgeMap.get(key) || 0) + 1);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const edges = Array.from(edgeMap.entries()).map(([key, weight]) => {
|
|
818
|
+
const [source, target] = key.split("|");
|
|
819
|
+
return { source, target, weight };
|
|
820
|
+
});
|
|
821
|
+
const languages = {};
|
|
822
|
+
for (const file of graph.files) {
|
|
823
|
+
const ext = file.split(".").pop() || "unknown";
|
|
824
|
+
const lang = ext === "ts" || ext === "tsx" ? "typescript" : ext === "js" || ext === "jsx" || ext === "mjs" || ext === "cjs" ? "javascript" : ext === "py" ? "python" : ext === "go" ? "go" : "other";
|
|
825
|
+
languages[lang] = (languages[lang] || 0) + 1;
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
commitHash,
|
|
829
|
+
commitDate,
|
|
830
|
+
commitMessage,
|
|
831
|
+
commitAuthor,
|
|
832
|
+
stats: {
|
|
833
|
+
totalFiles: graph.files.length,
|
|
834
|
+
totalSymbols: graph.nodes.length,
|
|
835
|
+
totalEdges: edges.length,
|
|
836
|
+
languages
|
|
837
|
+
},
|
|
838
|
+
files,
|
|
839
|
+
edges
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/mcp/tools.ts
|
|
844
|
+
function getToolsList() {
|
|
845
|
+
return [
|
|
846
|
+
{
|
|
847
|
+
name: "connect_repo",
|
|
848
|
+
description: "Connect Depwire to a codebase for analysis. Accepts a local directory path or a GitHub repository URL. If a GitHub URL is provided, the repo will be cloned automatically. This replaces the currently loaded project.",
|
|
849
|
+
inputSchema: {
|
|
850
|
+
type: "object",
|
|
851
|
+
properties: {
|
|
852
|
+
source: {
|
|
853
|
+
type: "string",
|
|
854
|
+
description: "Local directory path (e.g., '/Users/me/project') or GitHub URL (e.g., 'https://github.com/vercel/next.js')"
|
|
855
|
+
},
|
|
856
|
+
subdirectory: {
|
|
857
|
+
type: "string",
|
|
858
|
+
description: "Subdirectory within the repo to analyze (optional, e.g., 'packages/core/src')"
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
required: ["source"]
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
name: "get_symbol_info",
|
|
866
|
+
description: "Look up detailed information about a symbol (function, class, variable, type, etc.) by name. Pass a symbol name (e.g., 'Router') or a fully qualified ID (e.g., 'src/router.ts::Router') for exact matching. If multiple symbols share the same name, returns all matches for disambiguation.",
|
|
867
|
+
inputSchema: {
|
|
868
|
+
type: "object",
|
|
869
|
+
properties: {
|
|
870
|
+
name: {
|
|
871
|
+
type: "string",
|
|
872
|
+
description: "The symbol name to look up (e.g., 'UserService') or full ID (e.g., 'src/services/UserService.ts::UserService')"
|
|
873
|
+
}
|
|
874
|
+
},
|
|
875
|
+
required: ["name"]
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
name: "get_dependencies",
|
|
880
|
+
description: "Get all symbols that a given symbol depends on (what does this symbol use/import/call?). Pass a symbol name (e.g., 'Router') or a fully qualified ID (e.g., 'src/router.ts::Router') for exact matching. If multiple symbols share the same name, returns all matches for disambiguation.",
|
|
881
|
+
inputSchema: {
|
|
882
|
+
type: "object",
|
|
883
|
+
properties: {
|
|
884
|
+
symbol: {
|
|
885
|
+
type: "string",
|
|
886
|
+
description: "Symbol name (e.g., 'Router') or full ID (e.g., 'src/router.ts::Router')"
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
required: ["symbol"]
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
name: "get_dependents",
|
|
894
|
+
description: "Get all symbols that depend on a given symbol (what uses this symbol?). Pass a symbol name (e.g., 'Router') or a fully qualified ID (e.g., 'src/router.ts::Router') for exact matching. If multiple symbols share the same name, returns all matches for disambiguation.",
|
|
895
|
+
inputSchema: {
|
|
896
|
+
type: "object",
|
|
897
|
+
properties: {
|
|
898
|
+
symbol: {
|
|
899
|
+
type: "string",
|
|
900
|
+
description: "Symbol name (e.g., 'Router') or full ID (e.g., 'src/router.ts::Router')"
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
required: ["symbol"]
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
name: "impact_analysis",
|
|
908
|
+
description: "Analyze what would break if a symbol is changed, renamed, or removed. Shows direct dependents, transitive dependents (chain reaction), and all affected files. Pass a symbol name (e.g., 'Router') or a fully qualified ID (e.g., 'src/router.ts::Router') for exact matching. If multiple symbols share the same name, returns all matches for disambiguation. Use this before making changes to understand the blast radius.",
|
|
909
|
+
inputSchema: {
|
|
910
|
+
type: "object",
|
|
911
|
+
properties: {
|
|
912
|
+
symbol: {
|
|
913
|
+
type: "string",
|
|
914
|
+
description: "Symbol name (e.g., 'Router') or full ID (e.g., 'src/router.ts::Router')"
|
|
915
|
+
},
|
|
916
|
+
file: {
|
|
917
|
+
type: "string",
|
|
918
|
+
description: "Optional: File path to disambiguate when multiple symbols have the same name (e.g., 'src/router.ts')"
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
required: ["symbol"]
|
|
922
|
+
}
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
name: "get_file_context",
|
|
926
|
+
description: "Get complete context about a file \u2014 all symbols defined in it, all imports, all exports, and all files that import from it.",
|
|
927
|
+
inputSchema: {
|
|
928
|
+
type: "object",
|
|
929
|
+
properties: {
|
|
930
|
+
filePath: {
|
|
931
|
+
type: "string",
|
|
932
|
+
description: "Relative file path (e.g., 'services/UserService.ts')"
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
required: ["filePath"]
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
name: "search_symbols",
|
|
940
|
+
description: "Search for symbols by name across the entire codebase. Supports partial matching.",
|
|
941
|
+
inputSchema: {
|
|
942
|
+
type: "object",
|
|
943
|
+
properties: {
|
|
944
|
+
query: {
|
|
945
|
+
type: "string",
|
|
946
|
+
description: "Search query (case-insensitive substring match)"
|
|
947
|
+
},
|
|
948
|
+
limit: {
|
|
949
|
+
type: "number",
|
|
950
|
+
description: "Maximum results to return (default: 20)"
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
required: ["query"]
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
name: "get_architecture_summary",
|
|
958
|
+
description: "Get a high-level overview of the project's architecture \u2014 file count, symbol count, most connected files, dependency hotspots, and orphan files.",
|
|
959
|
+
inputSchema: {
|
|
960
|
+
type: "object",
|
|
961
|
+
properties: {}
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
name: "list_files",
|
|
966
|
+
description: "List all files in the project with basic stats.",
|
|
967
|
+
inputSchema: {
|
|
968
|
+
type: "object",
|
|
969
|
+
properties: {
|
|
970
|
+
directory: {
|
|
971
|
+
type: "string",
|
|
972
|
+
description: "Filter to a specific subdirectory (optional)"
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
name: "visualize_graph",
|
|
979
|
+
description: "Render an interactive arc diagram visualization of the current codebase's cross-reference graph. Shows files as bars along the bottom and dependency arcs connecting them, colored by distance. The visualization appears inline in the conversation.",
|
|
980
|
+
inputSchema: {
|
|
981
|
+
type: "object",
|
|
982
|
+
properties: {
|
|
983
|
+
highlight: {
|
|
984
|
+
type: "string",
|
|
985
|
+
description: "File or symbol name to highlight in the visualization (optional)"
|
|
986
|
+
},
|
|
987
|
+
maxFiles: {
|
|
988
|
+
type: "number",
|
|
989
|
+
description: "Limit to top N most connected files (optional, default: all)"
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
name: "get_project_docs",
|
|
996
|
+
description: "Retrieve auto-generated codebase documentation. Returns architecture overview, code conventions, dependency maps, and onboarding guides. Documentation must be generated first with `depwire docs` command.",
|
|
997
|
+
inputSchema: {
|
|
998
|
+
type: "object",
|
|
999
|
+
properties: {
|
|
1000
|
+
doc_type: {
|
|
1001
|
+
type: "string",
|
|
1002
|
+
description: "Document type to retrieve: 'architecture', 'conventions', 'dependencies', 'onboarding', or 'all' (default: 'all')"
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
name: "update_project_docs",
|
|
1009
|
+
description: "Regenerate codebase documentation with the latest changes. If docs don't exist, generates them for the first time. Use this after significant code changes to keep documentation up-to-date.",
|
|
1010
|
+
inputSchema: {
|
|
1011
|
+
type: "object",
|
|
1012
|
+
properties: {
|
|
1013
|
+
doc_type: {
|
|
1014
|
+
type: "string",
|
|
1015
|
+
description: "Document type to update: 'architecture', 'conventions', 'dependencies', 'onboarding', or 'all' (default: 'all')"
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
name: "get_health_score",
|
|
1022
|
+
description: "Get a 0-100 health score for the project's dependency architecture. Scores coupling, cohesion, circular dependencies, god files, orphan files, and dependency depth. Returns overall score, per-dimension breakdown, and actionable recommendations.",
|
|
1023
|
+
inputSchema: {
|
|
1024
|
+
type: "object",
|
|
1025
|
+
properties: {}
|
|
1026
|
+
}
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
name: "get_temporal_graph",
|
|
1030
|
+
description: "Show how the dependency graph evolved over git history. Returns snapshots at sampled commits showing file counts, symbol counts, edge counts, and structural changes over time.",
|
|
1031
|
+
inputSchema: {
|
|
1032
|
+
type: "object",
|
|
1033
|
+
properties: {
|
|
1034
|
+
commits: {
|
|
1035
|
+
type: "number",
|
|
1036
|
+
description: "Number of commits to sample (default: 10)"
|
|
1037
|
+
},
|
|
1038
|
+
strategy: {
|
|
1039
|
+
type: "string",
|
|
1040
|
+
enum: ["even", "weekly", "monthly"],
|
|
1041
|
+
description: "Sampling strategy (default: even)"
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
name: "find_dead_code",
|
|
1048
|
+
description: "Find potentially dead code \u2014 symbols that are defined but never referenced anywhere in the codebase. Returns symbols categorized by confidence level (high, medium, low). High confidence means definitely unused. Use this to identify cleanup opportunities.",
|
|
1049
|
+
inputSchema: {
|
|
1050
|
+
type: "object",
|
|
1051
|
+
properties: {
|
|
1052
|
+
confidence: {
|
|
1053
|
+
type: "string",
|
|
1054
|
+
enum: ["high", "medium", "low"],
|
|
1055
|
+
description: "Minimum confidence level to return (default: medium)",
|
|
1056
|
+
default: "medium"
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
name: "simulate_change",
|
|
1063
|
+
description: `Simulate an architectural change before touching any code. Returns health score delta, broken imports, and affected nodes. Zero file I/O \u2014 pure in-memory simulation.
|
|
1064
|
+
|
|
1065
|
+
Operations:
|
|
1066
|
+
- delete: Simulate deleting a file. Shows every file that would break and the full blast radius.
|
|
1067
|
+
- move: Simulate moving a file to a new path. Shows broken imports and edge changes.
|
|
1068
|
+
- rename: Simulate renaming a file. Shows all affected imports and nodes.
|
|
1069
|
+
- split: Simulate splitting a file by moving specified symbols to a new file.
|
|
1070
|
+
- merge: Simulate merging two files into one. Fails fast on symbol name collision.
|
|
1071
|
+
|
|
1072
|
+
Always run this before any refactor that touches file structure.`,
|
|
1073
|
+
inputSchema: {
|
|
1074
|
+
type: "object",
|
|
1075
|
+
properties: {
|
|
1076
|
+
operation: {
|
|
1077
|
+
type: "string",
|
|
1078
|
+
enum: ["move", "delete", "rename", "split", "merge"],
|
|
1079
|
+
description: "Type of change to simulate"
|
|
1080
|
+
},
|
|
1081
|
+
target: {
|
|
1082
|
+
type: "string",
|
|
1083
|
+
description: "Relative file path of the primary target"
|
|
1084
|
+
},
|
|
1085
|
+
destination: {
|
|
1086
|
+
type: "string",
|
|
1087
|
+
description: "Required for move and rename \u2014 the new file path"
|
|
1088
|
+
},
|
|
1089
|
+
symbols: {
|
|
1090
|
+
type: "array",
|
|
1091
|
+
items: { type: "string" },
|
|
1092
|
+
description: "Required for split \u2014 symbol names to move to new file"
|
|
1093
|
+
},
|
|
1094
|
+
mergeTarget: {
|
|
1095
|
+
type: "string",
|
|
1096
|
+
description: "Required for merge \u2014 the file to merge into target"
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
required: ["operation", "target"]
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
];
|
|
1103
|
+
}
|
|
1104
|
+
async function handleToolCall(name, args, state) {
|
|
1105
|
+
try {
|
|
1106
|
+
let result;
|
|
1107
|
+
if (name === "connect_repo") {
|
|
1108
|
+
result = await connectToRepo(args.source, args.subdirectory, state);
|
|
1109
|
+
} else if (name === "get_architecture_summary") {
|
|
1110
|
+
if (!isProjectLoaded(state)) {
|
|
1111
|
+
result = {
|
|
1112
|
+
status: "no_project",
|
|
1113
|
+
message: "No project loaded. Use connect_repo to analyze a codebase."
|
|
1114
|
+
};
|
|
1115
|
+
} else {
|
|
1116
|
+
result = handleGetArchitectureSummary(state.graph);
|
|
1117
|
+
}
|
|
1118
|
+
} else if (name === "visualize_graph") {
|
|
1119
|
+
if (!isProjectLoaded(state)) {
|
|
1120
|
+
result = {
|
|
1121
|
+
error: "No project loaded",
|
|
1122
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
1123
|
+
};
|
|
1124
|
+
} else {
|
|
1125
|
+
result = await handleVisualizeGraph(args.highlight, args.maxFiles, state);
|
|
1126
|
+
}
|
|
1127
|
+
} else if (name === "get_project_docs") {
|
|
1128
|
+
if (!isProjectLoaded(state)) {
|
|
1129
|
+
result = {
|
|
1130
|
+
error: "No project loaded",
|
|
1131
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
1132
|
+
};
|
|
1133
|
+
} else {
|
|
1134
|
+
result = await handleGetProjectDocs(args.doc_type || "all", state);
|
|
1135
|
+
}
|
|
1136
|
+
} else if (name === "update_project_docs") {
|
|
1137
|
+
if (!isProjectLoaded(state)) {
|
|
1138
|
+
result = {
|
|
1139
|
+
error: "No project loaded",
|
|
1140
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
1141
|
+
};
|
|
1142
|
+
} else {
|
|
1143
|
+
result = await handleUpdateProjectDocs(args.doc_type || "all", state);
|
|
1144
|
+
}
|
|
1145
|
+
} else if (name === "get_health_score") {
|
|
1146
|
+
if (!isProjectLoaded(state)) {
|
|
1147
|
+
result = {
|
|
1148
|
+
error: "No project loaded",
|
|
1149
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
1150
|
+
};
|
|
1151
|
+
} else {
|
|
1152
|
+
result = handleGetHealthScore(state);
|
|
1153
|
+
}
|
|
1154
|
+
} else if (name === "get_temporal_graph") {
|
|
1155
|
+
if (!isProjectLoaded(state)) {
|
|
1156
|
+
result = {
|
|
1157
|
+
error: "No project loaded",
|
|
1158
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
1159
|
+
};
|
|
1160
|
+
} else {
|
|
1161
|
+
result = await handleGetTemporalGraph(state, args.commits || 10, args.strategy || "even");
|
|
1162
|
+
}
|
|
1163
|
+
} else if (name === "find_dead_code") {
|
|
1164
|
+
if (!isProjectLoaded(state)) {
|
|
1165
|
+
result = {
|
|
1166
|
+
error: "No project loaded",
|
|
1167
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
1168
|
+
};
|
|
1169
|
+
} else {
|
|
1170
|
+
result = handleFindDeadCode(state, args.confidence || "medium");
|
|
1171
|
+
}
|
|
1172
|
+
} else if (name === "simulate_change") {
|
|
1173
|
+
if (!isProjectLoaded(state)) {
|
|
1174
|
+
result = {
|
|
1175
|
+
error: true,
|
|
1176
|
+
message: "No project loaded. Use connect_repo to connect to a codebase first.",
|
|
1177
|
+
operation: args.operation,
|
|
1178
|
+
target: args.target
|
|
1179
|
+
};
|
|
1180
|
+
} else {
|
|
1181
|
+
result = handleSimulateChange(args, state);
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1184
|
+
if (!isProjectLoaded(state)) {
|
|
1185
|
+
result = {
|
|
1186
|
+
error: "No project loaded",
|
|
1187
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
1188
|
+
};
|
|
1189
|
+
} else {
|
|
1190
|
+
const graph = state.graph;
|
|
1191
|
+
switch (name) {
|
|
1192
|
+
case "get_symbol_info":
|
|
1193
|
+
result = handleGetSymbolInfo(args.name, graph);
|
|
1194
|
+
break;
|
|
1195
|
+
case "get_dependencies":
|
|
1196
|
+
result = handleGetDependencies(args.symbol, graph);
|
|
1197
|
+
break;
|
|
1198
|
+
case "get_dependents":
|
|
1199
|
+
result = handleGetDependents(args.symbol, graph);
|
|
1200
|
+
break;
|
|
1201
|
+
case "impact_analysis":
|
|
1202
|
+
result = handleImpactAnalysis(args.symbol, graph, args.file);
|
|
1203
|
+
break;
|
|
1204
|
+
case "get_file_context":
|
|
1205
|
+
result = handleGetFileContext(args.filePath, graph);
|
|
1206
|
+
break;
|
|
1207
|
+
case "search_symbols":
|
|
1208
|
+
result = handleSearchSymbols(args.query, args.limit || 20, graph);
|
|
1209
|
+
break;
|
|
1210
|
+
case "list_files":
|
|
1211
|
+
result = handleListFiles(args.directory, graph);
|
|
1212
|
+
break;
|
|
1213
|
+
default:
|
|
1214
|
+
result = { error: `Unknown tool: ${name}` };
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (result && typeof result === "object" && "_mcpAppResponse" in result) {
|
|
1219
|
+
const appResult = result;
|
|
1220
|
+
return {
|
|
1221
|
+
content: [
|
|
1222
|
+
{
|
|
1223
|
+
type: "text",
|
|
1224
|
+
text: appResult.text
|
|
1225
|
+
},
|
|
1226
|
+
{
|
|
1227
|
+
type: "resource",
|
|
1228
|
+
resource: {
|
|
1229
|
+
uri: "ui://depwire/arc-diagram",
|
|
1230
|
+
mimeType: "text/html;profile=mcp-app",
|
|
1231
|
+
text: appResult.html
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
]
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
if (result && typeof result === "object" && "content" in result && Array.isArray(result.content)) {
|
|
1238
|
+
return result;
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
content: [
|
|
1242
|
+
{
|
|
1243
|
+
type: "text",
|
|
1244
|
+
text: JSON.stringify(result, null, 2)
|
|
1245
|
+
}
|
|
1246
|
+
]
|
|
1247
|
+
};
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
console.error("Error handling tool call:", error);
|
|
1250
|
+
return {
|
|
1251
|
+
content: [
|
|
1252
|
+
{
|
|
1253
|
+
type: "text",
|
|
1254
|
+
text: JSON.stringify({ error: String(error) }, null, 2)
|
|
1255
|
+
}
|
|
1256
|
+
]
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function createDisambiguationResponse(matches, queryName) {
|
|
1261
|
+
const suggestion = matches.length > 0 ? matches[0].id : "";
|
|
1262
|
+
const exampleFile = matches.length > 0 ? matches[0].filePath : "";
|
|
1263
|
+
return {
|
|
1264
|
+
ambiguous: true,
|
|
1265
|
+
message: `Found ${matches.length} symbols named '${queryName}'. Disambiguate by:
|
|
1266
|
+
1. Using full ID: '${suggestion}'
|
|
1267
|
+
2. Or adding file parameter: { symbol: '${queryName}', file: '${exampleFile}' }`,
|
|
1268
|
+
matches: matches.map((m, index) => ({
|
|
1269
|
+
id: m.id,
|
|
1270
|
+
kind: m.kind,
|
|
1271
|
+
filePath: m.filePath,
|
|
1272
|
+
line: m.startLine,
|
|
1273
|
+
dependents: m.dependentCount,
|
|
1274
|
+
hint: index === 0 && m.dependentCount > 0 ? "Most dependents \u2014 likely the one you want" : ""
|
|
1275
|
+
})),
|
|
1276
|
+
suggestion
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
function handleGetSymbolInfo(name, graph) {
|
|
1280
|
+
const matches = findSymbols(graph, name);
|
|
1281
|
+
if (matches.length === 0) {
|
|
1282
|
+
const fuzzyMatches = searchSymbols(graph, name).slice(0, 10);
|
|
1283
|
+
return {
|
|
1284
|
+
error: `Symbol '${name}' not found`,
|
|
1285
|
+
suggestion: fuzzyMatches.length > 0 ? `Did you mean: ${fuzzyMatches.map((m) => m.name).join(", ")}?` : "Try using search_symbols to find available symbols",
|
|
1286
|
+
fuzzyMatches: fuzzyMatches.map((m) => ({
|
|
1287
|
+
id: m.id,
|
|
1288
|
+
name: m.name,
|
|
1289
|
+
kind: m.kind,
|
|
1290
|
+
filePath: m.filePath
|
|
1291
|
+
}))
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
matches: matches.map((m) => ({
|
|
1296
|
+
id: m.id,
|
|
1297
|
+
name: m.name,
|
|
1298
|
+
kind: m.kind,
|
|
1299
|
+
filePath: m.filePath,
|
|
1300
|
+
startLine: m.startLine,
|
|
1301
|
+
endLine: m.endLine,
|
|
1302
|
+
exported: m.exported,
|
|
1303
|
+
scope: m.scope,
|
|
1304
|
+
dependents: m.dependentCount
|
|
1305
|
+
})),
|
|
1306
|
+
count: matches.length
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function handleGetDependencies(symbol, graph) {
|
|
1310
|
+
const matches = findSymbols(graph, symbol);
|
|
1311
|
+
if (matches.length === 0) {
|
|
1312
|
+
const fuzzyMatches = searchSymbols(graph, symbol).slice(0, 10);
|
|
1313
|
+
return {
|
|
1314
|
+
error: `Symbol '${symbol}' not found`,
|
|
1315
|
+
suggestion: fuzzyMatches.length > 0 ? `Did you mean: ${fuzzyMatches.map((m) => m.name).join(", ")}?` : "Try using search_symbols to find available symbols"
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
if (matches.length > 1) {
|
|
1319
|
+
return createDisambiguationResponse(matches, symbol);
|
|
1320
|
+
}
|
|
1321
|
+
const target = matches[0];
|
|
1322
|
+
const deps = getDependencies(graph, target.id);
|
|
1323
|
+
const grouped = {};
|
|
1324
|
+
graph.forEachOutEdge(target.id, (edge, attrs, source, targetNode) => {
|
|
1325
|
+
const kind = attrs.kind;
|
|
1326
|
+
if (!grouped[kind]) {
|
|
1327
|
+
grouped[kind] = [];
|
|
1328
|
+
}
|
|
1329
|
+
const targetAttrs = graph.getNodeAttributes(targetNode);
|
|
1330
|
+
grouped[kind].push({
|
|
1331
|
+
name: targetAttrs.name,
|
|
1332
|
+
filePath: targetAttrs.filePath,
|
|
1333
|
+
kind: targetAttrs.kind
|
|
1334
|
+
});
|
|
1335
|
+
});
|
|
1336
|
+
const totalCount = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
|
|
1337
|
+
return {
|
|
1338
|
+
symbol: target.id,
|
|
1339
|
+
dependencies: grouped,
|
|
1340
|
+
totalCount
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
function handleGetDependents(symbol, graph) {
|
|
1344
|
+
const matches = findSymbols(graph, symbol);
|
|
1345
|
+
if (matches.length === 0) {
|
|
1346
|
+
const fuzzyMatches = searchSymbols(graph, symbol).slice(0, 10);
|
|
1347
|
+
return {
|
|
1348
|
+
error: `Symbol '${symbol}' not found`,
|
|
1349
|
+
suggestion: fuzzyMatches.length > 0 ? `Did you mean: ${fuzzyMatches.map((m) => m.name).join(", ")}?` : "Try using search_symbols to find available symbols"
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
if (matches.length > 1) {
|
|
1353
|
+
return createDisambiguationResponse(matches, symbol);
|
|
1354
|
+
}
|
|
1355
|
+
const target = matches[0];
|
|
1356
|
+
const deps = getDependents(graph, target.id);
|
|
1357
|
+
const grouped = {};
|
|
1358
|
+
graph.forEachInEdge(target.id, (edge, attrs, source, targetNode) => {
|
|
1359
|
+
const kind = attrs.kind;
|
|
1360
|
+
if (!grouped[kind]) {
|
|
1361
|
+
grouped[kind] = [];
|
|
1362
|
+
}
|
|
1363
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
1364
|
+
grouped[kind].push({
|
|
1365
|
+
name: sourceAttrs.name,
|
|
1366
|
+
filePath: sourceAttrs.filePath,
|
|
1367
|
+
kind: sourceAttrs.kind
|
|
1368
|
+
});
|
|
1369
|
+
});
|
|
1370
|
+
const totalCount = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
|
|
1371
|
+
return {
|
|
1372
|
+
symbol: target.id,
|
|
1373
|
+
dependents: grouped,
|
|
1374
|
+
totalCount
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
function handleImpactAnalysis(symbol, graph, file) {
|
|
1378
|
+
const matches = findSymbols(graph, symbol);
|
|
1379
|
+
if (matches.length === 0) {
|
|
1380
|
+
const fuzzyMatches = searchSymbols(graph, symbol).slice(0, 10);
|
|
1381
|
+
return {
|
|
1382
|
+
error: `Symbol '${symbol}' not found`,
|
|
1383
|
+
suggestion: fuzzyMatches.length > 0 ? `Did you mean: ${fuzzyMatches.map((m) => m.name).join(", ")}?` : "Try using search_symbols to find available symbols"
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
let filteredMatches = matches;
|
|
1387
|
+
if (file) {
|
|
1388
|
+
filteredMatches = matches.filter((m) => m.filePath === file || m.filePath.endsWith(file));
|
|
1389
|
+
if (filteredMatches.length === 0) {
|
|
1390
|
+
return {
|
|
1391
|
+
error: `Symbol '${symbol}' not found in file '${file}'`,
|
|
1392
|
+
availableFiles: matches.map((m) => m.filePath),
|
|
1393
|
+
suggestion: `The symbol exists in: ${matches.map((m) => m.filePath).join(", ")}`
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (filteredMatches.length > 1) {
|
|
1398
|
+
return createDisambiguationResponse(filteredMatches, symbol);
|
|
1399
|
+
}
|
|
1400
|
+
const target = filteredMatches[0];
|
|
1401
|
+
const impact = getImpact(graph, target.id);
|
|
1402
|
+
const directWithKinds = impact.directDependents.map((dep) => {
|
|
1403
|
+
let relationship = "unknown";
|
|
1404
|
+
graph.forEachEdge(dep.id, target.id, (edge, attrs) => {
|
|
1405
|
+
relationship = attrs.kind;
|
|
1406
|
+
});
|
|
1407
|
+
return {
|
|
1408
|
+
name: dep.name,
|
|
1409
|
+
filePath: dep.filePath,
|
|
1410
|
+
kind: dep.kind,
|
|
1411
|
+
relationship
|
|
1412
|
+
};
|
|
1413
|
+
});
|
|
1414
|
+
const transitiveFormatted = impact.transitiveDependents.filter((dep) => !impact.directDependents.some((d) => d.id === dep.id)).map((dep) => ({
|
|
1415
|
+
name: dep.name,
|
|
1416
|
+
filePath: dep.filePath,
|
|
1417
|
+
kind: dep.kind
|
|
1418
|
+
}));
|
|
1419
|
+
const summary = `Changing ${target.name} would directly affect ${impact.directDependents.length} symbol(s) and transitively affect ${transitiveFormatted.length} more, across ${impact.affectedFiles.length} file(s).`;
|
|
1420
|
+
return {
|
|
1421
|
+
symbol: {
|
|
1422
|
+
id: target.id,
|
|
1423
|
+
name: target.name,
|
|
1424
|
+
filePath: target.filePath,
|
|
1425
|
+
kind: target.kind
|
|
1426
|
+
},
|
|
1427
|
+
impact: {
|
|
1428
|
+
directDependents: directWithKinds,
|
|
1429
|
+
transitiveDependents: transitiveFormatted,
|
|
1430
|
+
affectedFiles: impact.affectedFiles,
|
|
1431
|
+
summary
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
function handleGetFileContext(filePath, graph) {
|
|
1436
|
+
const fileSymbols = [];
|
|
1437
|
+
graph.forEachNode((nodeId, attrs) => {
|
|
1438
|
+
if (attrs.filePath === filePath) {
|
|
1439
|
+
fileSymbols.push({
|
|
1440
|
+
name: attrs.name,
|
|
1441
|
+
kind: attrs.kind,
|
|
1442
|
+
exported: attrs.exported,
|
|
1443
|
+
startLine: attrs.startLine,
|
|
1444
|
+
endLine: attrs.endLine,
|
|
1445
|
+
scope: attrs.scope
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
if (fileSymbols.length === 0) {
|
|
1450
|
+
return {
|
|
1451
|
+
error: `File '${filePath}' not found`,
|
|
1452
|
+
suggestion: "Use list_files to see available files"
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
const importsMap = /* @__PURE__ */ new Map();
|
|
1456
|
+
graph.forEachNode((nodeId, attrs) => {
|
|
1457
|
+
if (attrs.filePath === filePath) {
|
|
1458
|
+
graph.forEachOutEdge(nodeId, (edge, edgeAttrs, source, target) => {
|
|
1459
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
1460
|
+
if (targetAttrs.filePath !== filePath) {
|
|
1461
|
+
if (!importsMap.has(targetAttrs.filePath)) {
|
|
1462
|
+
importsMap.set(targetAttrs.filePath, /* @__PURE__ */ new Set());
|
|
1463
|
+
}
|
|
1464
|
+
importsMap.get(targetAttrs.filePath).add(targetAttrs.name);
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
const imports = Array.from(importsMap.entries()).map(([file, symbols]) => ({
|
|
1470
|
+
from: file,
|
|
1471
|
+
symbols: Array.from(symbols)
|
|
1472
|
+
}));
|
|
1473
|
+
const importedByMap = /* @__PURE__ */ new Map();
|
|
1474
|
+
graph.forEachNode((nodeId, attrs) => {
|
|
1475
|
+
if (attrs.filePath === filePath) {
|
|
1476
|
+
graph.forEachInEdge(nodeId, (edge, edgeAttrs, source, target) => {
|
|
1477
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
1478
|
+
if (sourceAttrs.filePath !== filePath) {
|
|
1479
|
+
if (!importedByMap.has(sourceAttrs.filePath)) {
|
|
1480
|
+
importedByMap.set(sourceAttrs.filePath, /* @__PURE__ */ new Set());
|
|
1481
|
+
}
|
|
1482
|
+
importedByMap.get(sourceAttrs.filePath).add(attrs.name);
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
const importedBy = Array.from(importedByMap.entries()).map(([file, symbols]) => ({
|
|
1488
|
+
file,
|
|
1489
|
+
symbols: Array.from(symbols)
|
|
1490
|
+
}));
|
|
1491
|
+
const summary = `${filePath} defines ${fileSymbols.length} symbol(s), imports from ${imports.length} file(s), and is imported by ${importedBy.length} file(s).`;
|
|
1492
|
+
return {
|
|
1493
|
+
filePath,
|
|
1494
|
+
symbols: fileSymbols,
|
|
1495
|
+
imports,
|
|
1496
|
+
importedBy,
|
|
1497
|
+
summary
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function handleSearchSymbols(query, limit, graph) {
|
|
1501
|
+
const results = searchSymbols(graph, query);
|
|
1502
|
+
const queryLower = query.toLowerCase();
|
|
1503
|
+
results.sort((a, b) => {
|
|
1504
|
+
const aName = a.name.toLowerCase();
|
|
1505
|
+
const bName = b.name.toLowerCase();
|
|
1506
|
+
if (aName === queryLower && bName !== queryLower) return -1;
|
|
1507
|
+
if (bName === queryLower && aName !== queryLower) return 1;
|
|
1508
|
+
const aStarts = aName.startsWith(queryLower);
|
|
1509
|
+
const bStarts = bName.startsWith(queryLower);
|
|
1510
|
+
if (aStarts && !bStarts) return -1;
|
|
1511
|
+
if (bStarts && !aStarts) return 1;
|
|
1512
|
+
return aName.localeCompare(bName);
|
|
1513
|
+
});
|
|
1514
|
+
const showing = Math.min(limit, results.length);
|
|
1515
|
+
return {
|
|
1516
|
+
query,
|
|
1517
|
+
results: results.slice(0, limit).map((r) => ({
|
|
1518
|
+
name: r.name,
|
|
1519
|
+
kind: r.kind,
|
|
1520
|
+
filePath: r.filePath,
|
|
1521
|
+
exported: r.exported,
|
|
1522
|
+
scope: r.scope
|
|
1523
|
+
})),
|
|
1524
|
+
totalMatches: results.length,
|
|
1525
|
+
showing
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
function handleGetArchitectureSummary(graph) {
|
|
1529
|
+
const summary = getArchitectureSummary(graph);
|
|
1530
|
+
const fileSummary = getFileSummary(graph);
|
|
1531
|
+
const dirMap = /* @__PURE__ */ new Map();
|
|
1532
|
+
const languageBreakdown = {};
|
|
1533
|
+
fileSummary.forEach((f) => {
|
|
1534
|
+
const dir = f.filePath.includes("/") ? dirname2(f.filePath) : ".";
|
|
1535
|
+
if (!dirMap.has(dir)) {
|
|
1536
|
+
dirMap.set(dir, { fileCount: 0, symbolCount: 0 });
|
|
1537
|
+
}
|
|
1538
|
+
const entry = dirMap.get(dir);
|
|
1539
|
+
entry.fileCount++;
|
|
1540
|
+
entry.symbolCount += f.symbolCount;
|
|
1541
|
+
const ext = f.filePath.toLowerCase();
|
|
1542
|
+
let lang;
|
|
1543
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
|
|
1544
|
+
lang = "typescript";
|
|
1545
|
+
} else if (ext.endsWith(".py")) {
|
|
1546
|
+
lang = "python";
|
|
1547
|
+
} else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
|
|
1548
|
+
lang = "javascript";
|
|
1549
|
+
} else {
|
|
1550
|
+
lang = "other";
|
|
1551
|
+
}
|
|
1552
|
+
languageBreakdown[lang] = (languageBreakdown[lang] || 0) + 1;
|
|
1553
|
+
});
|
|
1554
|
+
const directories = Array.from(dirMap.entries()).map(([name, stats]) => ({ name, ...stats })).sort((a, b) => b.symbolCount - a.symbolCount);
|
|
1555
|
+
const summaryText = `Project has ${summary.fileCount} files with ${summary.symbolCount} symbols and ${summary.edgeCount} edges. The most connected file is ${summary.mostConnectedFiles[0]?.filePath || "N/A"} with ${summary.mostConnectedFiles[0]?.connections || 0} connections.`;
|
|
1556
|
+
return {
|
|
1557
|
+
overview: {
|
|
1558
|
+
totalFiles: summary.fileCount,
|
|
1559
|
+
totalSymbols: summary.symbolCount,
|
|
1560
|
+
totalEdges: summary.edgeCount,
|
|
1561
|
+
languages: languageBreakdown
|
|
1562
|
+
},
|
|
1563
|
+
mostConnectedFiles: summary.mostConnectedFiles.slice(0, 10),
|
|
1564
|
+
directories: directories.slice(0, 10),
|
|
1565
|
+
orphanFiles: summary.orphanFiles,
|
|
1566
|
+
summary: summaryText
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
function handleListFiles(directory, graph) {
|
|
1570
|
+
const fileSummary = getFileSummary(graph);
|
|
1571
|
+
let filtered = fileSummary;
|
|
1572
|
+
if (directory) {
|
|
1573
|
+
filtered = fileSummary.filter((f) => f.filePath.startsWith(directory));
|
|
1574
|
+
}
|
|
1575
|
+
const files = filtered.map((f) => ({
|
|
1576
|
+
path: f.filePath,
|
|
1577
|
+
symbolCount: f.symbolCount,
|
|
1578
|
+
connections: f.incomingRefs + f.outgoingRefs
|
|
1579
|
+
}));
|
|
1580
|
+
return {
|
|
1581
|
+
files,
|
|
1582
|
+
totalFiles: files.length
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
async function handleVisualizeGraph(highlight, maxFiles, state) {
|
|
1586
|
+
const vizData = prepareVizData(state.graph, state.projectRoot);
|
|
1587
|
+
const { url, alreadyRunning } = await startVizServer(
|
|
1588
|
+
vizData,
|
|
1589
|
+
state.graph,
|
|
1590
|
+
state.projectRoot,
|
|
1591
|
+
3456,
|
|
1592
|
+
// Use different port from CLI default to avoid conflicts
|
|
1593
|
+
false
|
|
1594
|
+
// Don't auto-open browser from MCP
|
|
1595
|
+
);
|
|
1596
|
+
const fileCount = maxFiles && maxFiles < vizData.files.length ? maxFiles : vizData.files.length;
|
|
1597
|
+
const arcCount = vizData.arcs.filter((a) => {
|
|
1598
|
+
if (!maxFiles || maxFiles >= vizData.files.length) return true;
|
|
1599
|
+
const topFiles = vizData.files.sort((a2, b) => b.incomingCount + b.outgoingCount - (a2.incomingCount + a2.outgoingCount)).slice(0, maxFiles).map((f) => f.path);
|
|
1600
|
+
return topFiles.includes(a.sourceFile) && topFiles.includes(a.targetFile);
|
|
1601
|
+
}).length;
|
|
1602
|
+
const statusMessage = alreadyRunning ? "Visualization server is already running." : "Visualization server started.";
|
|
1603
|
+
const message = `${statusMessage}
|
|
1604
|
+
|
|
1605
|
+
Interactive arc diagram: ${url}
|
|
1606
|
+
|
|
1607
|
+
The diagram shows ${fileCount} files and ${arcCount} cross-file dependencies.${highlight ? ` Highlighted: ${highlight}` : ""}
|
|
1608
|
+
|
|
1609
|
+
Features:
|
|
1610
|
+
\u2022 Hover over arcs to see source \u2192 target details
|
|
1611
|
+
\u2022 Click files to filter connections
|
|
1612
|
+
\u2022 Search for specific files
|
|
1613
|
+
\u2022 Export as SVG or PNG
|
|
1614
|
+
|
|
1615
|
+
The server will keep running until you end the MCP session or press Ctrl+C.`;
|
|
1616
|
+
return {
|
|
1617
|
+
content: [{ type: "text", text: message }]
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
async function handleGetProjectDocs(docType, state) {
|
|
1621
|
+
const docsDir = join5(state.projectRoot, ".depwire");
|
|
1622
|
+
if (!existsSync3(docsDir)) {
|
|
1623
|
+
const errorMessage = `Project documentation has not been generated yet.
|
|
1624
|
+
|
|
1625
|
+
Run \`depwire docs ${state.projectRoot}\` to generate codebase documentation.
|
|
1626
|
+
|
|
1627
|
+
Once generated, this tool will return the requested documentation.
|
|
1628
|
+
|
|
1629
|
+
Available document types:
|
|
1630
|
+
- architecture: High-level structural overview
|
|
1631
|
+
- conventions: Auto-detected coding patterns
|
|
1632
|
+
- dependencies: Complete dependency mapping
|
|
1633
|
+
- onboarding: Guide for new developers`;
|
|
1634
|
+
return {
|
|
1635
|
+
content: [{ type: "text", text: errorMessage }]
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
const metadata = loadMetadata(docsDir);
|
|
1639
|
+
if (!metadata) {
|
|
1640
|
+
return {
|
|
1641
|
+
content: [{ type: "text", text: "Documentation directory exists but metadata is missing. Please regenerate with `depwire docs`." }]
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
const docsToReturn = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
|
|
1645
|
+
let output = "";
|
|
1646
|
+
const missing = [];
|
|
1647
|
+
for (const doc of docsToReturn) {
|
|
1648
|
+
if (!metadata.documents[doc]) {
|
|
1649
|
+
missing.push(doc);
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
const filePath = join5(docsDir, metadata.documents[doc].file);
|
|
1653
|
+
if (!existsSync3(filePath)) {
|
|
1654
|
+
missing.push(doc);
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
1658
|
+
if (docsToReturn.length > 1) {
|
|
1659
|
+
output += `
|
|
1660
|
+
|
|
1661
|
+
---
|
|
1662
|
+
|
|
1663
|
+
# ${doc.toUpperCase()}
|
|
1664
|
+
|
|
1665
|
+
`;
|
|
1666
|
+
}
|
|
1667
|
+
output += content;
|
|
1668
|
+
}
|
|
1669
|
+
if (missing.length > 0) {
|
|
1670
|
+
output += `
|
|
1671
|
+
|
|
1672
|
+
---
|
|
1673
|
+
|
|
1674
|
+
**Note:** The following documents are missing: ${missing.join(", ")}. Run \`depwire docs ${state.projectRoot} --update\` to generate them.`;
|
|
1675
|
+
}
|
|
1676
|
+
return {
|
|
1677
|
+
content: [{ type: "text", text: output }]
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
async function handleUpdateProjectDocs(docType, state) {
|
|
1681
|
+
const startTime = Date.now();
|
|
1682
|
+
const docsDir = join5(state.projectRoot, ".depwire");
|
|
1683
|
+
console.error("Regenerating project documentation...");
|
|
1684
|
+
const parsedFiles = await parseProject(state.projectRoot);
|
|
1685
|
+
const graph = buildGraph(parsedFiles);
|
|
1686
|
+
const parseTime = (Date.now() - startTime) / 1e3;
|
|
1687
|
+
state.graph = graph;
|
|
1688
|
+
const packageJsonPath = join5(__dirname, "../../package.json");
|
|
1689
|
+
const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
1690
|
+
const docsToGenerate = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
|
|
1691
|
+
const docsExist = existsSync3(docsDir);
|
|
1692
|
+
const result = await generateDocs(graph, state.projectRoot, packageJson.version, parseTime, {
|
|
1693
|
+
outputDir: docsDir,
|
|
1694
|
+
format: "markdown",
|
|
1695
|
+
include: docsToGenerate,
|
|
1696
|
+
update: docsExist,
|
|
1697
|
+
only: docsExist ? docsToGenerate : void 0,
|
|
1698
|
+
verbose: false,
|
|
1699
|
+
stats: false
|
|
1700
|
+
});
|
|
1701
|
+
const elapsed = (Date.now() - startTime) / 1e3;
|
|
1702
|
+
if (result.success) {
|
|
1703
|
+
const fileCount = /* @__PURE__ */ new Set();
|
|
1704
|
+
graph.forEachNode((node, attrs) => {
|
|
1705
|
+
fileCount.add(attrs.filePath);
|
|
1706
|
+
});
|
|
1707
|
+
return {
|
|
1708
|
+
status: "success",
|
|
1709
|
+
message: `Updated ${result.generated.join(", ")} (${fileCount.size} files, ${graph.order} symbols, ${elapsed.toFixed(1)}s)`,
|
|
1710
|
+
generated: result.generated,
|
|
1711
|
+
stats: {
|
|
1712
|
+
files: fileCount.size,
|
|
1713
|
+
symbols: graph.order,
|
|
1714
|
+
edges: graph.size,
|
|
1715
|
+
time: elapsed
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
} else {
|
|
1719
|
+
return {
|
|
1720
|
+
status: "error",
|
|
1721
|
+
message: `Failed to update documentation: ${result.errors.join(", ")}`,
|
|
1722
|
+
errors: result.errors
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
function handleGetHealthScore(state) {
|
|
1727
|
+
const graph = state.graph;
|
|
1728
|
+
const projectRoot = state.projectRoot;
|
|
1729
|
+
const report = calculateHealthScore(graph, projectRoot);
|
|
1730
|
+
return report;
|
|
1731
|
+
}
|
|
1732
|
+
async function handleGetTemporalGraph(state, commits, strategy) {
|
|
1733
|
+
const projectRoot = state.projectRoot;
|
|
1734
|
+
if (!isGitRepo(projectRoot)) {
|
|
1735
|
+
return {
|
|
1736
|
+
error: "Not a git repository",
|
|
1737
|
+
message: "Temporal analysis requires git history"
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
try {
|
|
1741
|
+
const allCommits = await getCommitLog(projectRoot);
|
|
1742
|
+
if (allCommits.length === 0) {
|
|
1743
|
+
return {
|
|
1744
|
+
error: "No commits found",
|
|
1745
|
+
message: "Repository has no commit history"
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
const sampledCommits = sampleCommits(allCommits, commits, strategy);
|
|
1749
|
+
const snapshots = [];
|
|
1750
|
+
const outputDir = join5(projectRoot, ".depwire", "temporal");
|
|
1751
|
+
for (const commit of sampledCommits) {
|
|
1752
|
+
const existing = loadSnapshot(commit.hash, outputDir);
|
|
1753
|
+
if (existing) {
|
|
1754
|
+
snapshots.push(existing);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (snapshots.length === 0) {
|
|
1758
|
+
return {
|
|
1759
|
+
status: "no_snapshots",
|
|
1760
|
+
message: "No temporal snapshots found. Run `depwire temporal` to generate them.",
|
|
1761
|
+
commits_found: allCommits.length,
|
|
1762
|
+
commits_to_sample: sampledCommits.length
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
const first = snapshots[0];
|
|
1766
|
+
const last = snapshots[snapshots.length - 1];
|
|
1767
|
+
const growth = {
|
|
1768
|
+
files: last.stats.totalFiles - first.stats.totalFiles,
|
|
1769
|
+
symbols: last.stats.totalSymbols - first.stats.totalSymbols,
|
|
1770
|
+
edges: last.stats.totalEdges - first.stats.totalEdges
|
|
1771
|
+
};
|
|
1772
|
+
const trend = growth.files > 0 ? "Growing" : growth.files < 0 ? "Shrinking" : "Stable";
|
|
1773
|
+
let biggestGrowth = { index: 0, files: 0, date: "", message: "" };
|
|
1774
|
+
for (let i = 1; i < snapshots.length; i++) {
|
|
1775
|
+
const delta = snapshots[i].stats.totalFiles - snapshots[i - 1].stats.totalFiles;
|
|
1776
|
+
if (delta > biggestGrowth.files) {
|
|
1777
|
+
biggestGrowth = {
|
|
1778
|
+
index: i,
|
|
1779
|
+
files: delta,
|
|
1780
|
+
date: snapshots[i].commitDate,
|
|
1781
|
+
message: snapshots[i].commitMessage
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return {
|
|
1786
|
+
status: "success",
|
|
1787
|
+
time_range: {
|
|
1788
|
+
from: first.commitDate,
|
|
1789
|
+
to: last.commitDate
|
|
1790
|
+
},
|
|
1791
|
+
snapshots: snapshots.map((s) => ({
|
|
1792
|
+
commit: s.commitHash.substring(0, 8),
|
|
1793
|
+
date: s.commitDate,
|
|
1794
|
+
message: s.commitMessage,
|
|
1795
|
+
author: s.commitAuthor,
|
|
1796
|
+
files: s.stats.totalFiles,
|
|
1797
|
+
symbols: s.stats.totalSymbols,
|
|
1798
|
+
edges: s.stats.totalEdges
|
|
1799
|
+
})),
|
|
1800
|
+
growth,
|
|
1801
|
+
trend,
|
|
1802
|
+
biggest_growth_period: biggestGrowth.files > 0 ? biggestGrowth : null,
|
|
1803
|
+
summary: `Analyzed ${snapshots.length} snapshots from ${new Date(first.commitDate).toLocaleDateString()} to ${new Date(last.commitDate).toLocaleDateString()}. Overall trend: ${trend}.`
|
|
1804
|
+
};
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
return {
|
|
1807
|
+
error: "Failed to analyze temporal graph",
|
|
1808
|
+
message: String(error)
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
function handleFindDeadCode(state, confidence) {
|
|
1813
|
+
if (!state.graph || !state.projectRoot) {
|
|
1814
|
+
return {
|
|
1815
|
+
error: "No project loaded",
|
|
1816
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
try {
|
|
1820
|
+
const report = analyzeDeadCode(state.graph, state.projectRoot, {
|
|
1821
|
+
confidence,
|
|
1822
|
+
includeTests: false,
|
|
1823
|
+
verbose: false,
|
|
1824
|
+
stats: false,
|
|
1825
|
+
json: true
|
|
1826
|
+
});
|
|
1827
|
+
return {
|
|
1828
|
+
status: "success",
|
|
1829
|
+
totalSymbols: report.totalSymbols,
|
|
1830
|
+
deadSymbols: report.deadSymbols,
|
|
1831
|
+
deadPercentage: report.deadPercentage,
|
|
1832
|
+
byConfidence: report.byConfidence,
|
|
1833
|
+
symbols: report.symbols.map((s) => ({
|
|
1834
|
+
name: s.name,
|
|
1835
|
+
kind: s.kind,
|
|
1836
|
+
file: s.file,
|
|
1837
|
+
line: s.line,
|
|
1838
|
+
exported: s.exported,
|
|
1839
|
+
dependents: s.dependents,
|
|
1840
|
+
confidence: s.confidence,
|
|
1841
|
+
reason: s.reason
|
|
1842
|
+
})),
|
|
1843
|
+
summary: `Found ${report.deadSymbols} potentially dead symbols (${report.byConfidence.high} high, ${report.byConfidence.medium} medium, ${report.byConfidence.low} low confidence) out of ${report.totalSymbols} total symbols (${report.deadPercentage.toFixed(1)}% dead code).`
|
|
1844
|
+
};
|
|
1845
|
+
} catch (error) {
|
|
1846
|
+
return {
|
|
1847
|
+
error: "Failed to analyze dead code",
|
|
1848
|
+
message: String(error)
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
function handleSimulateChange(args, state) {
|
|
1853
|
+
const { operation, target, destination, symbols, mergeTarget } = args;
|
|
1854
|
+
const graph = state.graph;
|
|
1855
|
+
if ((operation === "move" || operation === "rename") && !destination) {
|
|
1856
|
+
return {
|
|
1857
|
+
error: true,
|
|
1858
|
+
message: "destination is required for move and rename operations",
|
|
1859
|
+
operation,
|
|
1860
|
+
target
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
if (operation === "split" && (!symbols || symbols.length === 0)) {
|
|
1864
|
+
return {
|
|
1865
|
+
error: true,
|
|
1866
|
+
message: "symbols is required for split operations and must not be empty",
|
|
1867
|
+
operation,
|
|
1868
|
+
target
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
if (operation === "merge" && !mergeTarget) {
|
|
1872
|
+
return {
|
|
1873
|
+
error: true,
|
|
1874
|
+
message: "mergeTarget is required for merge operations",
|
|
1875
|
+
operation,
|
|
1876
|
+
target
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
const targetNodes = graph.filterNodes(
|
|
1880
|
+
(_node, attrs) => {
|
|
1881
|
+
const fp = attrs.filePath?.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1882
|
+
const t = target.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1883
|
+
return fp === t || fp?.endsWith("/" + t) || t.endsWith("/" + fp);
|
|
1884
|
+
}
|
|
1885
|
+
);
|
|
1886
|
+
if (targetNodes.length === 0) {
|
|
1887
|
+
return {
|
|
1888
|
+
error: true,
|
|
1889
|
+
message: `Target file '${target}' not found in the dependency graph`,
|
|
1890
|
+
operation,
|
|
1891
|
+
target
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
let action;
|
|
1895
|
+
switch (operation) {
|
|
1896
|
+
case "move":
|
|
1897
|
+
action = { type: "move", target, destination };
|
|
1898
|
+
break;
|
|
1899
|
+
case "delete":
|
|
1900
|
+
action = { type: "delete", target };
|
|
1901
|
+
break;
|
|
1902
|
+
case "rename":
|
|
1903
|
+
action = { type: "rename", target, newName: destination };
|
|
1904
|
+
break;
|
|
1905
|
+
case "split":
|
|
1906
|
+
action = { type: "split", target, newFile: destination || target.replace(/(\.\w+)$/, ".split$1"), symbols };
|
|
1907
|
+
break;
|
|
1908
|
+
case "merge":
|
|
1909
|
+
action = { type: "merge", target, source: mergeTarget };
|
|
1910
|
+
break;
|
|
1911
|
+
default:
|
|
1912
|
+
return {
|
|
1913
|
+
error: true,
|
|
1914
|
+
message: `Unknown operation: ${operation}`,
|
|
1915
|
+
operation,
|
|
1916
|
+
target
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
try {
|
|
1920
|
+
const engine = new SimulationEngine(graph);
|
|
1921
|
+
const result = engine.simulate(action);
|
|
1922
|
+
const brokenImportCount = result.diff.brokenImports.length;
|
|
1923
|
+
const affectedNodeCount = result.diff.affectedNodes.length;
|
|
1924
|
+
const removedEdgeCount = result.diff.removedEdges.length;
|
|
1925
|
+
return {
|
|
1926
|
+
operation,
|
|
1927
|
+
target,
|
|
1928
|
+
healthBefore: result.healthDelta.before,
|
|
1929
|
+
healthAfter: result.healthDelta.after,
|
|
1930
|
+
healthDelta: result.healthDelta.delta,
|
|
1931
|
+
affectedNodes: affectedNodeCount,
|
|
1932
|
+
brokenImports: result.diff.brokenImports.map((bi) => ({
|
|
1933
|
+
file: bi.file,
|
|
1934
|
+
importedSymbol: bi.importedSymbol
|
|
1935
|
+
})),
|
|
1936
|
+
removedEdges: removedEdgeCount,
|
|
1937
|
+
circularDepsIntroduced: result.diff.circularDepsIntroduced.length,
|
|
1938
|
+
circularDepsResolved: result.diff.circularDepsResolved.length,
|
|
1939
|
+
summary: `${operation.charAt(0).toUpperCase() + operation.slice(1)}ing ${target} would ${result.healthDelta.delta >= 0 ? "improve" : "reduce"} health score from ${result.healthDelta.before} to ${result.healthDelta.after} (${result.healthDelta.delta >= 0 ? "+" : ""}${result.healthDelta.delta}), breaking ${brokenImportCount} import${brokenImportCount !== 1 ? "s" : ""} across ${affectedNodeCount} affected node${affectedNodeCount !== 1 ? "s" : ""}.`
|
|
1940
|
+
};
|
|
1941
|
+
} catch (err) {
|
|
1942
|
+
return {
|
|
1943
|
+
error: true,
|
|
1944
|
+
message: err.message,
|
|
1945
|
+
operation,
|
|
1946
|
+
target
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// src/mcp/server.ts
|
|
1952
|
+
async function startMcpServer(state) {
|
|
1953
|
+
const server = new Server(
|
|
1954
|
+
{
|
|
1955
|
+
name: "depwire",
|
|
1956
|
+
version: "0.1.0"
|
|
1957
|
+
},
|
|
1958
|
+
{
|
|
1959
|
+
capabilities: {
|
|
1960
|
+
tools: {}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
);
|
|
1964
|
+
const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
1965
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1966
|
+
return {
|
|
1967
|
+
tools: getToolsList()
|
|
1968
|
+
};
|
|
1969
|
+
});
|
|
1970
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1971
|
+
const { name, arguments: args } = request.params;
|
|
1972
|
+
return await handleToolCall(name, args || {}, state);
|
|
1973
|
+
});
|
|
1974
|
+
const transport = new StdioServerTransport();
|
|
1975
|
+
await server.connect(transport);
|
|
1976
|
+
console.error("Depwire MCP server started");
|
|
1977
|
+
if (state.projectRoot) {
|
|
1978
|
+
console.error(`Project: ${state.projectRoot}`);
|
|
1979
|
+
} else {
|
|
1980
|
+
console.error("No project loaded. Use connect_repo to connect to a codebase.");
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
export {
|
|
1985
|
+
prepareVizData,
|
|
1986
|
+
watchProject,
|
|
1987
|
+
startVizServer,
|
|
1988
|
+
createEmptyState,
|
|
1989
|
+
updateFileInGraph,
|
|
1990
|
+
getCommitLog,
|
|
1991
|
+
getCurrentBranch,
|
|
1992
|
+
checkoutCommit,
|
|
1993
|
+
restoreOriginal,
|
|
1994
|
+
stashChanges,
|
|
1995
|
+
popStash,
|
|
1996
|
+
isGitRepo,
|
|
1997
|
+
sampleCommits,
|
|
1998
|
+
saveSnapshot,
|
|
1999
|
+
loadSnapshot,
|
|
2000
|
+
createSnapshot,
|
|
2001
|
+
startMcpServer
|
|
2002
|
+
};
|