depwire-cli 0.9.20 → 0.9.22

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.
@@ -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
+ };