depwire-cli 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -2
- package/dist/{chunk-VNUOE5VC.js → chunk-65H7HCM4.js} +391 -11
- package/dist/index.js +293 -10
- package/dist/mcpb-entry.js +1 -1
- package/dist/viz/public/temporal.css +397 -0
- package/dist/viz/public/temporal.html +118 -0
- package/dist/viz/public/temporal.js +508 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,8 +40,8 @@ Depwire fixes this by giving AI tools a complete dependency graph of your codeba
|
|
|
40
40
|
- **Live graph, always current** — edit a file and the dependency map updates in real-time. No re-indexing, no waiting.
|
|
41
41
|
- **Works locally, stays private** — zero cloud accounts, zero data leaving your machine. Just `npm install` and go.
|
|
42
42
|
|
|
43
|
-
###
|
|
44
|
-
Depwire isn't just a pretty graph. It's a full context engine with
|
|
43
|
+
### 14 MCP Tools, Not Just Visualization
|
|
44
|
+
Depwire isn't just a pretty graph. It's a full context engine with 14 tools that AI assistants call autonomously — architecture summaries, dependency tracing, symbol search, file context, health scores, temporal evolution, and more. The AI decides which tool to use based on your question.
|
|
45
45
|
|
|
46
46
|
## Installation
|
|
47
47
|
|
|
@@ -66,10 +66,15 @@ depwire viz
|
|
|
66
66
|
depwire parse
|
|
67
67
|
depwire docs
|
|
68
68
|
depwire health
|
|
69
|
+
depwire temporal
|
|
69
70
|
|
|
70
71
|
# Or specify a directory explicitly
|
|
71
72
|
npx depwire-cli viz ./my-project
|
|
72
73
|
npx depwire-cli parse ./my-project
|
|
74
|
+
npx depwire-cli temporal ./my-project
|
|
75
|
+
|
|
76
|
+
# Temporal visualization options
|
|
77
|
+
npx depwire-cli temporal --commits 20 --strategy monthly --verbose --stats
|
|
73
78
|
|
|
74
79
|
# Exclude test files and node_modules
|
|
75
80
|
npx depwire-cli parse --exclude "**/*.test.*" "**/node_modules/**"
|
|
@@ -134,6 +139,7 @@ Settings → Features → Experimental → Enable MCP → Add Server:
|
|
|
134
139
|
| `get_project_docs` | Retrieve auto-generated codebase documentation |
|
|
135
140
|
| `update_project_docs` | Regenerate documentation on demand |
|
|
136
141
|
| `get_health_score` | Get 0-100 dependency health score with recommendations |
|
|
142
|
+
| `get_temporal_graph` | Show how the graph evolved over git history |
|
|
137
143
|
|
|
138
144
|
## Supported Languages
|
|
139
145
|
|
|
@@ -179,6 +185,41 @@ Opens an interactive arc diagram in your browser:
|
|
|
179
185
|
- Export as SVG or PNG
|
|
180
186
|
- **Port collision handling** — Automatically finds an available port if default is in use
|
|
181
187
|
|
|
188
|
+
## Temporal Graph
|
|
189
|
+
|
|
190
|
+
Visualize how your codebase architecture evolved over git history. Scrub through time with an interactive timeline slider.
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Auto-detects project root
|
|
194
|
+
depwire temporal
|
|
195
|
+
|
|
196
|
+
# Sample 20 commits with monthly snapshots
|
|
197
|
+
depwire temporal --commits 20 --strategy monthly
|
|
198
|
+
|
|
199
|
+
# Verbose mode with detailed progress
|
|
200
|
+
depwire temporal --verbose --stats
|
|
201
|
+
|
|
202
|
+
# Custom port
|
|
203
|
+
depwire temporal --port 3335
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Options:**
|
|
207
|
+
- `--commits <number>` — Number of commits to sample (default: 20)
|
|
208
|
+
- `--strategy <type>` — Sampling strategy: `even`, `weekly`, `monthly` (default: `even`)
|
|
209
|
+
- `-p, --port <number>` — Server port (default: 3334)
|
|
210
|
+
- `--output <path>` — Save snapshots to custom path (default: `.depwire/temporal/`)
|
|
211
|
+
- `--verbose` — Show progress for each commit being parsed
|
|
212
|
+
- `--stats` — Show summary statistics at end
|
|
213
|
+
|
|
214
|
+
Opens an interactive temporal visualization in your browser:
|
|
215
|
+
- Timeline slider showing all sampled commits
|
|
216
|
+
- Arc diagram morphing between snapshots
|
|
217
|
+
- Play/pause animation with speed controls (0.5×, 1×, 2×)
|
|
218
|
+
- Statistics panel with growth deltas
|
|
219
|
+
- Evolution chart tracking files/symbols/edges over time
|
|
220
|
+
- Auto-zoom to fit all arcs on snapshot change
|
|
221
|
+
- Search to highlight specific files across time
|
|
222
|
+
|
|
182
223
|
## How It Works
|
|
183
224
|
|
|
184
225
|
1. **Parser** — tree-sitter extracts every symbol and reference
|
|
@@ -360,6 +401,44 @@ depwire health --json
|
|
|
360
401
|
|
|
361
402
|
Health history is stored in `.depwire/health-history.json` (last 50 checks).
|
|
362
403
|
|
|
404
|
+
### `depwire temporal [directory]`
|
|
405
|
+
|
|
406
|
+
Visualize how the dependency graph evolved over git history.
|
|
407
|
+
|
|
408
|
+
**Directory argument is optional** — Auto-detects project root.
|
|
409
|
+
|
|
410
|
+
**Options:**
|
|
411
|
+
- `--commits <number>` — Number of commits to sample (default: 20)
|
|
412
|
+
- `--strategy <type>` — Sampling strategy: `even` (every Nth), `weekly`, `monthly` (default: `even`)
|
|
413
|
+
- `-p, --port <number>` — Server port (default: 3334)
|
|
414
|
+
- `--output <path>` — Save snapshots to custom path (default: `.depwire/temporal/`)
|
|
415
|
+
- `--verbose` — Show progress for each commit being parsed
|
|
416
|
+
- `--stats` — Show summary statistics at end
|
|
417
|
+
|
|
418
|
+
**Examples:**
|
|
419
|
+
```bash
|
|
420
|
+
# Auto-detect and analyze 20 commits
|
|
421
|
+
depwire temporal
|
|
422
|
+
|
|
423
|
+
# Sample 50 commits with monthly snapshots
|
|
424
|
+
depwire temporal --commits 50 --strategy monthly
|
|
425
|
+
|
|
426
|
+
# Verbose mode with stats
|
|
427
|
+
depwire temporal --verbose --stats
|
|
428
|
+
|
|
429
|
+
# Custom output directory
|
|
430
|
+
depwire temporal --output ./temp-snapshots
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Output:**
|
|
434
|
+
- Interactive browser visualization at `http://127.0.0.1:3334`
|
|
435
|
+
- Timeline slider to scrub through git history
|
|
436
|
+
- Arc diagram morphing between snapshots
|
|
437
|
+
- Growth statistics showing files/symbols/edges evolution
|
|
438
|
+
- Auto-zoom to fit full diagram on each snapshot change
|
|
439
|
+
|
|
440
|
+
Snapshots are cached in `.depwire/temporal/` for fast re-rendering.
|
|
441
|
+
|
|
363
442
|
### Error Handling
|
|
364
443
|
|
|
365
444
|
Depwire gracefully handles parse errors:
|
|
@@ -7272,8 +7272,8 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
7272
7272
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7273
7273
|
|
|
7274
7274
|
// src/mcp/tools.ts
|
|
7275
|
-
import { dirname as dirname14, join as
|
|
7276
|
-
import { existsSync as
|
|
7275
|
+
import { dirname as dirname14, join as join15 } from "path";
|
|
7276
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
|
|
7277
7277
|
|
|
7278
7278
|
// src/mcp/connect.ts
|
|
7279
7279
|
import simpleGit from "simple-git";
|
|
@@ -7467,6 +7467,268 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
7467
7467
|
}
|
|
7468
7468
|
}
|
|
7469
7469
|
|
|
7470
|
+
// src/temporal/git.ts
|
|
7471
|
+
import { execSync as execSync2 } from "child_process";
|
|
7472
|
+
async function getCommitLog(dir, limit) {
|
|
7473
|
+
try {
|
|
7474
|
+
const limitArg = limit ? `-n ${limit}` : "";
|
|
7475
|
+
const output = execSync2(
|
|
7476
|
+
`git log ${limitArg} --pretty=format:"%H|%aI|%s|%an"`,
|
|
7477
|
+
{ cwd: dir, encoding: "utf-8" }
|
|
7478
|
+
);
|
|
7479
|
+
if (!output.trim()) {
|
|
7480
|
+
return [];
|
|
7481
|
+
}
|
|
7482
|
+
return output.trim().split("\n").map((line) => {
|
|
7483
|
+
const [hash, date, message, author] = line.split("|");
|
|
7484
|
+
return { hash, date, message, author };
|
|
7485
|
+
});
|
|
7486
|
+
} catch (error) {
|
|
7487
|
+
throw new Error(`Failed to get git log: ${error}`);
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
async function getCurrentBranch(dir) {
|
|
7491
|
+
try {
|
|
7492
|
+
return execSync2("git rev-parse --abbrev-ref HEAD", {
|
|
7493
|
+
cwd: dir,
|
|
7494
|
+
encoding: "utf-8"
|
|
7495
|
+
}).trim();
|
|
7496
|
+
} catch (error) {
|
|
7497
|
+
throw new Error(`Failed to get current branch: ${error}`);
|
|
7498
|
+
}
|
|
7499
|
+
}
|
|
7500
|
+
async function checkoutCommit(dir, hash) {
|
|
7501
|
+
try {
|
|
7502
|
+
execSync2(`git checkout -q ${hash}`, { cwd: dir, stdio: "ignore" });
|
|
7503
|
+
} catch (error) {
|
|
7504
|
+
throw new Error(`Failed to checkout commit ${hash}: ${error}`);
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
async function restoreOriginal(dir, originalBranch) {
|
|
7508
|
+
try {
|
|
7509
|
+
execSync2(`git checkout -q ${originalBranch}`, {
|
|
7510
|
+
cwd: dir,
|
|
7511
|
+
stdio: "ignore"
|
|
7512
|
+
});
|
|
7513
|
+
} catch (error) {
|
|
7514
|
+
throw new Error(`Failed to restore branch ${originalBranch}: ${error}`);
|
|
7515
|
+
}
|
|
7516
|
+
}
|
|
7517
|
+
async function stashChanges(dir) {
|
|
7518
|
+
try {
|
|
7519
|
+
const status = execSync2("git status --porcelain", {
|
|
7520
|
+
cwd: dir,
|
|
7521
|
+
encoding: "utf-8"
|
|
7522
|
+
}).trim();
|
|
7523
|
+
if (status) {
|
|
7524
|
+
execSync2('git stash push -q -m "depwire temporal analysis"', {
|
|
7525
|
+
cwd: dir,
|
|
7526
|
+
stdio: "ignore"
|
|
7527
|
+
});
|
|
7528
|
+
return true;
|
|
7529
|
+
}
|
|
7530
|
+
return false;
|
|
7531
|
+
} catch (error) {
|
|
7532
|
+
throw new Error(`Failed to stash changes: ${error}`);
|
|
7533
|
+
}
|
|
7534
|
+
}
|
|
7535
|
+
async function popStash(dir) {
|
|
7536
|
+
try {
|
|
7537
|
+
execSync2("git stash pop -q", { cwd: dir, stdio: "ignore" });
|
|
7538
|
+
} catch (error) {
|
|
7539
|
+
console.warn("Warning: Failed to restore stashed changes:", error);
|
|
7540
|
+
}
|
|
7541
|
+
}
|
|
7542
|
+
function isGitRepo(dir) {
|
|
7543
|
+
try {
|
|
7544
|
+
execSync2("git rev-parse --git-dir", { cwd: dir, stdio: "ignore" });
|
|
7545
|
+
return true;
|
|
7546
|
+
} catch {
|
|
7547
|
+
return false;
|
|
7548
|
+
}
|
|
7549
|
+
}
|
|
7550
|
+
|
|
7551
|
+
// src/temporal/sampler.ts
|
|
7552
|
+
function sampleCommits(commits, targetCount, strategy) {
|
|
7553
|
+
if (commits.length === 0) {
|
|
7554
|
+
return [];
|
|
7555
|
+
}
|
|
7556
|
+
if (commits.length <= targetCount) {
|
|
7557
|
+
return commits;
|
|
7558
|
+
}
|
|
7559
|
+
switch (strategy) {
|
|
7560
|
+
case "even":
|
|
7561
|
+
return sampleEvenly(commits, targetCount);
|
|
7562
|
+
case "weekly":
|
|
7563
|
+
return sampleWeekly(commits, targetCount);
|
|
7564
|
+
case "monthly":
|
|
7565
|
+
return sampleMonthly(commits, targetCount);
|
|
7566
|
+
default:
|
|
7567
|
+
return sampleEvenly(commits, targetCount);
|
|
7568
|
+
}
|
|
7569
|
+
}
|
|
7570
|
+
function sampleEvenly(commits, targetCount) {
|
|
7571
|
+
if (targetCount >= commits.length) {
|
|
7572
|
+
return commits;
|
|
7573
|
+
}
|
|
7574
|
+
const result = [];
|
|
7575
|
+
const step = (commits.length - 1) / (targetCount - 1);
|
|
7576
|
+
for (let i = 0; i < targetCount; i++) {
|
|
7577
|
+
const index = Math.round(i * step);
|
|
7578
|
+
result.push(commits[index]);
|
|
7579
|
+
}
|
|
7580
|
+
return result;
|
|
7581
|
+
}
|
|
7582
|
+
function sampleWeekly(commits, targetCount) {
|
|
7583
|
+
const result = [];
|
|
7584
|
+
const first = commits[0];
|
|
7585
|
+
const last = commits[commits.length - 1];
|
|
7586
|
+
result.push(first);
|
|
7587
|
+
const weekMap = /* @__PURE__ */ new Map();
|
|
7588
|
+
for (const commit of commits) {
|
|
7589
|
+
const date = new Date(commit.date);
|
|
7590
|
+
const year = date.getFullYear();
|
|
7591
|
+
const week = getWeekNumber(date);
|
|
7592
|
+
const key = `${year}-W${week}`;
|
|
7593
|
+
weekMap.set(key, commit);
|
|
7594
|
+
}
|
|
7595
|
+
const weeklyCommits = Array.from(weekMap.values());
|
|
7596
|
+
if (weeklyCommits.length <= targetCount) {
|
|
7597
|
+
return weeklyCommits;
|
|
7598
|
+
}
|
|
7599
|
+
const step = Math.floor((weeklyCommits.length - 2) / (targetCount - 2));
|
|
7600
|
+
for (let i = 1; i < targetCount - 1; i++) {
|
|
7601
|
+
const index = Math.min(i * step, weeklyCommits.length - 2);
|
|
7602
|
+
if (weeklyCommits[index] !== first && weeklyCommits[index] !== last) {
|
|
7603
|
+
result.push(weeklyCommits[index]);
|
|
7604
|
+
}
|
|
7605
|
+
}
|
|
7606
|
+
if (result[result.length - 1] !== last) {
|
|
7607
|
+
result.push(last);
|
|
7608
|
+
}
|
|
7609
|
+
return result;
|
|
7610
|
+
}
|
|
7611
|
+
function sampleMonthly(commits, targetCount) {
|
|
7612
|
+
const result = [];
|
|
7613
|
+
const first = commits[0];
|
|
7614
|
+
const last = commits[commits.length - 1];
|
|
7615
|
+
result.push(first);
|
|
7616
|
+
const monthMap = /* @__PURE__ */ new Map();
|
|
7617
|
+
for (const commit of commits) {
|
|
7618
|
+
const date = new Date(commit.date);
|
|
7619
|
+
const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
7620
|
+
monthMap.set(key, commit);
|
|
7621
|
+
}
|
|
7622
|
+
const monthlyCommits = Array.from(monthMap.values());
|
|
7623
|
+
if (monthlyCommits.length <= targetCount) {
|
|
7624
|
+
return monthlyCommits;
|
|
7625
|
+
}
|
|
7626
|
+
const step = Math.floor((monthlyCommits.length - 2) / (targetCount - 2));
|
|
7627
|
+
for (let i = 1; i < targetCount - 1; i++) {
|
|
7628
|
+
const index = Math.min(i * step, monthlyCommits.length - 2);
|
|
7629
|
+
if (monthlyCommits[index] !== first && monthlyCommits[index] !== last) {
|
|
7630
|
+
result.push(monthlyCommits[index]);
|
|
7631
|
+
}
|
|
7632
|
+
}
|
|
7633
|
+
if (result[result.length - 1] !== last) {
|
|
7634
|
+
result.push(last);
|
|
7635
|
+
}
|
|
7636
|
+
return result;
|
|
7637
|
+
}
|
|
7638
|
+
function getWeekNumber(date) {
|
|
7639
|
+
const d = new Date(
|
|
7640
|
+
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
|
|
7641
|
+
);
|
|
7642
|
+
const dayNum = d.getUTCDay() || 7;
|
|
7643
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
7644
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
7645
|
+
return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
|
|
7646
|
+
}
|
|
7647
|
+
|
|
7648
|
+
// src/temporal/snapshots.ts
|
|
7649
|
+
import { writeFileSync as writeFileSync4, readFileSync as readFileSync7, mkdirSync as mkdirSync3, existsSync as existsSync11, readdirSync as readdirSync3 } from "fs";
|
|
7650
|
+
import { join as join14 } from "path";
|
|
7651
|
+
function saveSnapshot(snapshot, outputDir) {
|
|
7652
|
+
if (!existsSync11(outputDir)) {
|
|
7653
|
+
mkdirSync3(outputDir, { recursive: true });
|
|
7654
|
+
}
|
|
7655
|
+
const filename = `${snapshot.commitHash.substring(0, 8)}.json`;
|
|
7656
|
+
const filepath = join14(outputDir, filename);
|
|
7657
|
+
writeFileSync4(filepath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
7658
|
+
}
|
|
7659
|
+
function loadSnapshot(commitHash, outputDir) {
|
|
7660
|
+
const shortHash = commitHash.substring(0, 8);
|
|
7661
|
+
const filepath = join14(outputDir, `${shortHash}.json`);
|
|
7662
|
+
if (!existsSync11(filepath)) {
|
|
7663
|
+
return null;
|
|
7664
|
+
}
|
|
7665
|
+
try {
|
|
7666
|
+
const content = readFileSync7(filepath, "utf-8");
|
|
7667
|
+
return JSON.parse(content);
|
|
7668
|
+
} catch {
|
|
7669
|
+
return null;
|
|
7670
|
+
}
|
|
7671
|
+
}
|
|
7672
|
+
function createSnapshot(graph, commitHash, commitDate, commitMessage, commitAuthor) {
|
|
7673
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
7674
|
+
for (const node of graph.nodes) {
|
|
7675
|
+
if (!fileMap.has(node.filePath)) {
|
|
7676
|
+
fileMap.set(node.filePath, { symbols: 0, inbound: 0, outbound: 0 });
|
|
7677
|
+
}
|
|
7678
|
+
fileMap.get(node.filePath).symbols++;
|
|
7679
|
+
}
|
|
7680
|
+
for (const edge of graph.edges) {
|
|
7681
|
+
const sourceNode = graph.nodes.find((n) => n.id === edge.source);
|
|
7682
|
+
const targetNode = graph.nodes.find((n) => n.id === edge.target);
|
|
7683
|
+
if (sourceNode && targetNode && sourceNode.filePath !== targetNode.filePath) {
|
|
7684
|
+
if (fileMap.has(sourceNode.filePath)) {
|
|
7685
|
+
fileMap.get(sourceNode.filePath).outbound++;
|
|
7686
|
+
}
|
|
7687
|
+
if (fileMap.has(targetNode.filePath)) {
|
|
7688
|
+
fileMap.get(targetNode.filePath).inbound++;
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7691
|
+
}
|
|
7692
|
+
const files = Array.from(fileMap.entries()).map(([path2, data]) => ({
|
|
7693
|
+
path: path2,
|
|
7694
|
+
symbols: data.symbols,
|
|
7695
|
+
connections: data.inbound + data.outbound
|
|
7696
|
+
}));
|
|
7697
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
7698
|
+
for (const edge of graph.edges) {
|
|
7699
|
+
const sourceNode = graph.nodes.find((n) => n.id === edge.source);
|
|
7700
|
+
const targetNode = graph.nodes.find((n) => n.id === edge.target);
|
|
7701
|
+
if (sourceNode && targetNode && sourceNode.filePath !== targetNode.filePath) {
|
|
7702
|
+
const key = sourceNode.filePath < targetNode.filePath ? `${sourceNode.filePath}|${targetNode.filePath}` : `${targetNode.filePath}|${sourceNode.filePath}`;
|
|
7703
|
+
edgeMap.set(key, (edgeMap.get(key) || 0) + 1);
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
const edges = Array.from(edgeMap.entries()).map(([key, weight]) => {
|
|
7707
|
+
const [source, target] = key.split("|");
|
|
7708
|
+
return { source, target, weight };
|
|
7709
|
+
});
|
|
7710
|
+
const languages2 = {};
|
|
7711
|
+
for (const file of graph.files) {
|
|
7712
|
+
const ext = file.split(".").pop() || "unknown";
|
|
7713
|
+
const lang = ext === "ts" || ext === "tsx" ? "typescript" : ext === "js" || ext === "jsx" || ext === "mjs" || ext === "cjs" ? "javascript" : ext === "py" ? "python" : ext === "go" ? "go" : "other";
|
|
7714
|
+
languages2[lang] = (languages2[lang] || 0) + 1;
|
|
7715
|
+
}
|
|
7716
|
+
return {
|
|
7717
|
+
commitHash,
|
|
7718
|
+
commitDate,
|
|
7719
|
+
commitMessage,
|
|
7720
|
+
commitAuthor,
|
|
7721
|
+
stats: {
|
|
7722
|
+
totalFiles: graph.files.length,
|
|
7723
|
+
totalSymbols: graph.nodes.length,
|
|
7724
|
+
totalEdges: edges.length,
|
|
7725
|
+
languages: languages2
|
|
7726
|
+
},
|
|
7727
|
+
files,
|
|
7728
|
+
edges
|
|
7729
|
+
};
|
|
7730
|
+
}
|
|
7731
|
+
|
|
7470
7732
|
// src/mcp/tools.ts
|
|
7471
7733
|
function getToolsList() {
|
|
7472
7734
|
return [
|
|
@@ -7647,6 +7909,24 @@ function getToolsList() {
|
|
|
7647
7909
|
type: "object",
|
|
7648
7910
|
properties: {}
|
|
7649
7911
|
}
|
|
7912
|
+
},
|
|
7913
|
+
{
|
|
7914
|
+
name: "get_temporal_graph",
|
|
7915
|
+
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.",
|
|
7916
|
+
inputSchema: {
|
|
7917
|
+
type: "object",
|
|
7918
|
+
properties: {
|
|
7919
|
+
commits: {
|
|
7920
|
+
type: "number",
|
|
7921
|
+
description: "Number of commits to sample (default: 10)"
|
|
7922
|
+
},
|
|
7923
|
+
strategy: {
|
|
7924
|
+
type: "string",
|
|
7925
|
+
enum: ["even", "weekly", "monthly"],
|
|
7926
|
+
description: "Sampling strategy (default: even)"
|
|
7927
|
+
}
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7650
7930
|
}
|
|
7651
7931
|
];
|
|
7652
7932
|
}
|
|
@@ -7700,6 +7980,15 @@ async function handleToolCall(name, args, state) {
|
|
|
7700
7980
|
} else {
|
|
7701
7981
|
result = handleGetHealthScore(state);
|
|
7702
7982
|
}
|
|
7983
|
+
} else if (name === "get_temporal_graph") {
|
|
7984
|
+
if (!isProjectLoaded(state)) {
|
|
7985
|
+
result = {
|
|
7986
|
+
error: "No project loaded",
|
|
7987
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
7988
|
+
};
|
|
7989
|
+
} else {
|
|
7990
|
+
result = await handleGetTemporalGraph(state, args.commits || 10, args.strategy || "even");
|
|
7991
|
+
}
|
|
7703
7992
|
} else {
|
|
7704
7993
|
if (!isProjectLoaded(state)) {
|
|
7705
7994
|
result = {
|
|
@@ -8124,8 +8413,8 @@ The server will keep running until you end the MCP session or press Ctrl+C.`;
|
|
|
8124
8413
|
};
|
|
8125
8414
|
}
|
|
8126
8415
|
async function handleGetProjectDocs(docType, state) {
|
|
8127
|
-
const docsDir =
|
|
8128
|
-
if (!
|
|
8416
|
+
const docsDir = join15(state.projectRoot, ".depwire");
|
|
8417
|
+
if (!existsSync12(docsDir)) {
|
|
8129
8418
|
const errorMessage = `Project documentation has not been generated yet.
|
|
8130
8419
|
|
|
8131
8420
|
Run \`depwire docs ${state.projectRoot}\` to generate codebase documentation.
|
|
@@ -8155,12 +8444,12 @@ Available document types:
|
|
|
8155
8444
|
missing.push(doc);
|
|
8156
8445
|
continue;
|
|
8157
8446
|
}
|
|
8158
|
-
const filePath =
|
|
8159
|
-
if (!
|
|
8447
|
+
const filePath = join15(docsDir, metadata.documents[doc].file);
|
|
8448
|
+
if (!existsSync12(filePath)) {
|
|
8160
8449
|
missing.push(doc);
|
|
8161
8450
|
continue;
|
|
8162
8451
|
}
|
|
8163
|
-
const content =
|
|
8452
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
8164
8453
|
if (docsToReturn.length > 1) {
|
|
8165
8454
|
output += `
|
|
8166
8455
|
|
|
@@ -8185,16 +8474,16 @@ Available document types:
|
|
|
8185
8474
|
}
|
|
8186
8475
|
async function handleUpdateProjectDocs(docType, state) {
|
|
8187
8476
|
const startTime = Date.now();
|
|
8188
|
-
const docsDir =
|
|
8477
|
+
const docsDir = join15(state.projectRoot, ".depwire");
|
|
8189
8478
|
console.error("Regenerating project documentation...");
|
|
8190
8479
|
const parsedFiles = await parseProject(state.projectRoot);
|
|
8191
8480
|
const graph = buildGraph(parsedFiles);
|
|
8192
8481
|
const parseTime = (Date.now() - startTime) / 1e3;
|
|
8193
8482
|
state.graph = graph;
|
|
8194
|
-
const packageJsonPath =
|
|
8195
|
-
const packageJson = JSON.parse(
|
|
8483
|
+
const packageJsonPath = join15(__dirname, "../../package.json");
|
|
8484
|
+
const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf-8"));
|
|
8196
8485
|
const docsToGenerate = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
|
|
8197
|
-
const docsExist =
|
|
8486
|
+
const docsExist = existsSync12(docsDir);
|
|
8198
8487
|
const result = await generateDocs(graph, state.projectRoot, packageJson.version, parseTime, {
|
|
8199
8488
|
outputDir: docsDir,
|
|
8200
8489
|
format: "markdown",
|
|
@@ -8235,6 +8524,86 @@ function handleGetHealthScore(state) {
|
|
|
8235
8524
|
const report = calculateHealthScore(graph, projectRoot);
|
|
8236
8525
|
return report;
|
|
8237
8526
|
}
|
|
8527
|
+
async function handleGetTemporalGraph(state, commits, strategy) {
|
|
8528
|
+
const projectRoot = state.projectRoot;
|
|
8529
|
+
if (!isGitRepo(projectRoot)) {
|
|
8530
|
+
return {
|
|
8531
|
+
error: "Not a git repository",
|
|
8532
|
+
message: "Temporal analysis requires git history"
|
|
8533
|
+
};
|
|
8534
|
+
}
|
|
8535
|
+
try {
|
|
8536
|
+
const allCommits = await getCommitLog(projectRoot);
|
|
8537
|
+
if (allCommits.length === 0) {
|
|
8538
|
+
return {
|
|
8539
|
+
error: "No commits found",
|
|
8540
|
+
message: "Repository has no commit history"
|
|
8541
|
+
};
|
|
8542
|
+
}
|
|
8543
|
+
const sampledCommits = sampleCommits(allCommits, commits, strategy);
|
|
8544
|
+
const snapshots = [];
|
|
8545
|
+
const outputDir = join15(projectRoot, ".depwire", "temporal");
|
|
8546
|
+
for (const commit of sampledCommits) {
|
|
8547
|
+
const existing = loadSnapshot(commit.hash, outputDir);
|
|
8548
|
+
if (existing) {
|
|
8549
|
+
snapshots.push(existing);
|
|
8550
|
+
}
|
|
8551
|
+
}
|
|
8552
|
+
if (snapshots.length === 0) {
|
|
8553
|
+
return {
|
|
8554
|
+
status: "no_snapshots",
|
|
8555
|
+
message: "No temporal snapshots found. Run `depwire temporal` to generate them.",
|
|
8556
|
+
commits_found: allCommits.length,
|
|
8557
|
+
commits_to_sample: sampledCommits.length
|
|
8558
|
+
};
|
|
8559
|
+
}
|
|
8560
|
+
const first = snapshots[0];
|
|
8561
|
+
const last = snapshots[snapshots.length - 1];
|
|
8562
|
+
const growth = {
|
|
8563
|
+
files: last.stats.totalFiles - first.stats.totalFiles,
|
|
8564
|
+
symbols: last.stats.totalSymbols - first.stats.totalSymbols,
|
|
8565
|
+
edges: last.stats.totalEdges - first.stats.totalEdges
|
|
8566
|
+
};
|
|
8567
|
+
const trend = growth.files > 0 ? "Growing" : growth.files < 0 ? "Shrinking" : "Stable";
|
|
8568
|
+
let biggestGrowth = { index: 0, files: 0, date: "", message: "" };
|
|
8569
|
+
for (let i = 1; i < snapshots.length; i++) {
|
|
8570
|
+
const delta = snapshots[i].stats.totalFiles - snapshots[i - 1].stats.totalFiles;
|
|
8571
|
+
if (delta > biggestGrowth.files) {
|
|
8572
|
+
biggestGrowth = {
|
|
8573
|
+
index: i,
|
|
8574
|
+
files: delta,
|
|
8575
|
+
date: snapshots[i].commitDate,
|
|
8576
|
+
message: snapshots[i].commitMessage
|
|
8577
|
+
};
|
|
8578
|
+
}
|
|
8579
|
+
}
|
|
8580
|
+
return {
|
|
8581
|
+
status: "success",
|
|
8582
|
+
time_range: {
|
|
8583
|
+
from: first.commitDate,
|
|
8584
|
+
to: last.commitDate
|
|
8585
|
+
},
|
|
8586
|
+
snapshots: snapshots.map((s) => ({
|
|
8587
|
+
commit: s.commitHash.substring(0, 8),
|
|
8588
|
+
date: s.commitDate,
|
|
8589
|
+
message: s.commitMessage,
|
|
8590
|
+
author: s.commitAuthor,
|
|
8591
|
+
files: s.stats.totalFiles,
|
|
8592
|
+
symbols: s.stats.totalSymbols,
|
|
8593
|
+
edges: s.stats.totalEdges
|
|
8594
|
+
})),
|
|
8595
|
+
growth,
|
|
8596
|
+
trend,
|
|
8597
|
+
biggest_growth_period: biggestGrowth.files > 0 ? biggestGrowth : null,
|
|
8598
|
+
summary: `Analyzed ${snapshots.length} snapshots from ${new Date(first.commitDate).toLocaleDateString()} to ${new Date(last.commitDate).toLocaleDateString()}. Overall trend: ${trend}.`
|
|
8599
|
+
};
|
|
8600
|
+
} catch (error) {
|
|
8601
|
+
return {
|
|
8602
|
+
error: "Failed to analyze temporal graph",
|
|
8603
|
+
message: String(error)
|
|
8604
|
+
};
|
|
8605
|
+
}
|
|
8606
|
+
}
|
|
8238
8607
|
|
|
8239
8608
|
// src/mcp/server.ts
|
|
8240
8609
|
async function startMcpServer(state) {
|
|
@@ -8284,5 +8653,16 @@ export {
|
|
|
8284
8653
|
calculateHealthScore,
|
|
8285
8654
|
getHealthTrend,
|
|
8286
8655
|
generateDocs,
|
|
8656
|
+
getCommitLog,
|
|
8657
|
+
getCurrentBranch,
|
|
8658
|
+
checkoutCommit,
|
|
8659
|
+
restoreOriginal,
|
|
8660
|
+
stashChanges,
|
|
8661
|
+
popStash,
|
|
8662
|
+
isGitRepo,
|
|
8663
|
+
sampleCommits,
|
|
8664
|
+
saveSnapshot,
|
|
8665
|
+
loadSnapshot,
|
|
8666
|
+
createSnapshot,
|
|
8287
8667
|
startMcpServer
|
|
8288
8668
|
};
|