depgraph-core 1.8.0 → 1.9.0

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 CHANGED
@@ -311,18 +311,18 @@ DepGraph ships an **MCP server** (`depgraph-mcp`) that lets Claude analyze your
311
311
 
312
312
  ### Quick setup
313
313
 
314
- **1. Build the MCP server**
314
+ **If the package is published to npm** (zero-install, always up to date):
315
315
  ```bash
316
- npm install
317
- npm run release:all # compiles + bundles both CLI and MCP server
316
+ claude mcp add depgraph -- npx -p depgraph-core depgraph-mcp
318
317
  ```
319
318
 
320
- **2. Register with Claude Code** (run once)
319
+ **If running from a local build** (clone the repo first):
321
320
  ```bash
322
- claude mcp add depgraph -- node /absolute/path/to/depgraph/depgraph-mcp.js
321
+ npm install && npm run release:all
322
+ npm run mcp:register
323
323
  ```
324
324
 
325
- **3. Use it** — open Claude Code in any project and ask naturally:
325
+ **Then use it** — open Claude Code in any project and ask naturally:
326
326
  ```
327
327
  Summarize the dependency graph of /Users/me/my-app
328
328
  What is the risk of changing getUserById in /Users/me/my-app?
package/depgraph-mcp.js CHANGED
@@ -24110,28 +24110,79 @@ function buildPipeline(projectDir) {
24110
24110
  const metrics = computeMetrics(graph);
24111
24111
  return { files, parsed, graph, metrics };
24112
24112
  }
24113
- function analyzeProject(projectDir) {
24113
+ function buildSubgraph(graph, focusName, maxDepth) {
24114
+ const focusNode = [...graph.nodes.values()].find((n) => n.name === focusName);
24115
+ if (!focusNode) return { nodes: /* @__PURE__ */ new Map(), edges: [] };
24116
+ const included = /* @__PURE__ */ new Set();
24117
+ const queue = [{ id: focusNode.id, depth: 0 }];
24118
+ while (queue.length > 0) {
24119
+ const { id, depth } = queue.shift();
24120
+ if (included.has(id)) continue;
24121
+ included.add(id);
24122
+ if (depth >= maxDepth) continue;
24123
+ for (const edge of graph.edges) {
24124
+ if (edge.from === id && !included.has(edge.to)) queue.push({ id: edge.to, depth: depth + 1 });
24125
+ if (edge.to === id && !included.has(edge.from)) queue.push({ id: edge.from, depth: depth + 1 });
24126
+ }
24127
+ }
24128
+ const nodes = /* @__PURE__ */ new Map();
24129
+ for (const id of included) {
24130
+ const node = graph.nodes.get(id);
24131
+ if (node) nodes.set(id, node);
24132
+ }
24133
+ return {
24134
+ nodes,
24135
+ edges: graph.edges.filter((e) => included.has(e.from) && included.has(e.to))
24136
+ };
24137
+ }
24138
+ function buildProseSummary(summary) {
24139
+ const s = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
24140
+ const shortIds = (ids) => ids.slice(0, 3).map((id) => id.split("__")[0]).join(", ") + (ids.length > 3 ? ` (+${ids.length - 3} more)` : "");
24141
+ const parts = [
24142
+ `${s(summary.totalFiles, "file")}, ${s(summary.totalNodes, "node")}, ${s(summary.totalEdges, "edge")}.`
24143
+ ];
24144
+ if (summary.criticalNodes.length > 0) {
24145
+ parts.push(
24146
+ `${s(summary.criticalNodes.length, "critical node")} \u2014 change carefully: ${shortIds(summary.criticalNodes)}.`
24147
+ );
24148
+ }
24149
+ if (summary.entryPoints.length > 0) {
24150
+ parts.push(`${s(summary.entryPoints.length, "entry point")}: ${shortIds(summary.entryPoints)}.`);
24151
+ }
24152
+ if (summary.leafNodes.length > 0) {
24153
+ parts.push(`${s(summary.leafNodes.length, "leaf node")} (pure utilities with no outgoing deps).`);
24154
+ }
24155
+ if (summary.isolatedNodes.length > 0) {
24156
+ parts.push(`${s(summary.isolatedNodes.length, "isolated node")} \u2014 potential dead code: ${shortIds(summary.isolatedNodes)}.`);
24157
+ }
24158
+ return parts.join(" ");
24159
+ }
24160
+ function analyzeProject(projectDir, opts = {}) {
24161
+ const { verbosity = "full", focus, depth = 3 } = opts;
24114
24162
  const { files, parsed, metrics } = buildPipeline(projectDir);
24115
24163
  const totalLines = parsed.reduce((sum, f) => sum + f.lines, 0);
24116
- return {
24117
- meta: {
24118
- version: VERSION,
24119
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
24120
- totalFiles: parsed.length,
24121
- totalLines
24122
- },
24123
- summary: {
24124
- totalNodes: metrics.nodes.size,
24125
- totalEdges: metrics.edges.length,
24126
- entryPoints: getEntryPoints(metrics),
24127
- leafNodes: getLeafNodes(metrics),
24128
- isolatedNodes: getIsolatedNodes(metrics),
24129
- criticalNodes: getCriticalNodes(metrics)
24130
- },
24131
- nodes: [...metrics.nodes.values()],
24132
- edges: metrics.edges,
24133
- files: parsed
24164
+ const graph = focus ? buildSubgraph(metrics, focus, depth) : metrics;
24165
+ const meta = {
24166
+ version: VERSION,
24167
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
24168
+ totalFiles: parsed.length,
24169
+ totalLines
24170
+ };
24171
+ const summary = {
24172
+ totalNodes: graph.nodes.size,
24173
+ totalEdges: graph.edges.length,
24174
+ entryPoints: getEntryPoints(graph),
24175
+ leafNodes: getLeafNodes(graph),
24176
+ isolatedNodes: getIsolatedNodes(graph),
24177
+ criticalNodes: getCriticalNodes(graph)
24134
24178
  };
24179
+ if (verbosity === "sketch") {
24180
+ return { meta, summary, nodes: [], edges: [], files: [] };
24181
+ }
24182
+ if (verbosity === "overview") {
24183
+ return { meta, summary, nodes: [...graph.nodes.values()], edges: [], files: [] };
24184
+ }
24185
+ return { meta, summary, nodes: [...graph.nodes.values()], edges: graph.edges, files: parsed };
24135
24186
  }
24136
24187
  function analyzeImpact(projectDir, targetNode, changeDescription) {
24137
24188
  const { metrics } = buildPipeline(projectDir);
@@ -24149,16 +24200,68 @@ function analyzeGitImpact(options) {
24149
24200
  }));
24150
24201
  return { changedEntities, impacts };
24151
24202
  }
24152
- function getGraphSummary(projectDir) {
24203
+ function getGraphSummary(projectDir, verbosity = "overview") {
24153
24204
  const { files, parsed, metrics } = buildPipeline(projectDir);
24154
- return {
24205
+ const base = {
24155
24206
  totalFiles: files.length,
24156
24207
  totalNodes: metrics.nodes.size,
24157
24208
  totalEdges: metrics.edges.length,
24209
+ criticalNodes: getCriticalNodes(metrics)
24210
+ };
24211
+ if (verbosity === "sketch") {
24212
+ return { ...base, entryPoints: [], leafNodes: [], isolatedNodes: [] };
24213
+ }
24214
+ return {
24215
+ ...base,
24158
24216
  entryPoints: getEntryPoints(metrics),
24159
24217
  leafNodes: getLeafNodes(metrics),
24160
- isolatedNodes: getIsolatedNodes(metrics),
24161
- criticalNodes: getCriticalNodes(metrics)
24218
+ isolatedNodes: getIsolatedNodes(metrics)
24219
+ };
24220
+ }
24221
+ function describeNode(projectDir, nodeName) {
24222
+ const { metrics } = buildPipeline(projectDir);
24223
+ const node = [...metrics.nodes.values()].find((n) => n.name === nodeName);
24224
+ if (!node) {
24225
+ return {
24226
+ found: false,
24227
+ name: nodeName,
24228
+ file: "",
24229
+ line: 0,
24230
+ type: "",
24231
+ lang: "",
24232
+ centralityScore: 0,
24233
+ inDegree: 0,
24234
+ outDegree: 0,
24235
+ importedBy: [],
24236
+ imports: [],
24237
+ role: "not found"
24238
+ };
24239
+ }
24240
+ const importedBy = [...new Set(
24241
+ metrics.edges.filter((e) => e.to === node.id).map((e) => metrics.nodes.get(e.from)?.name).filter((n) => Boolean(n))
24242
+ )];
24243
+ const imports = [...new Set(
24244
+ metrics.edges.filter((e) => e.from === node.id).map((e) => metrics.nodes.get(e.to)?.name).filter((n) => Boolean(n))
24245
+ )];
24246
+ let role;
24247
+ if (node.centralityScore > 20) role = "critical shared dependency";
24248
+ else if (node.inDegree === 0 && node.outDegree > 0) role = "entry point";
24249
+ else if (node.outDegree === 0 && node.inDegree > 0) role = "leaf";
24250
+ else if (node.inDegree === 0 && node.outDegree === 0) role = "isolated \u2014 potential dead code";
24251
+ else role = "connector";
24252
+ return {
24253
+ found: true,
24254
+ name: node.name,
24255
+ file: node.file,
24256
+ line: node.line,
24257
+ type: node.type,
24258
+ lang: node.lang,
24259
+ centralityScore: node.centralityScore,
24260
+ inDegree: node.inDegree,
24261
+ outDegree: node.outDegree,
24262
+ importedBy,
24263
+ imports,
24264
+ role
24162
24265
  };
24163
24266
  }
24164
24267
 
@@ -24168,16 +24271,17 @@ var server = new McpServer({
24168
24271
  version: VERSION
24169
24272
  });
24170
24273
  server.tool(
24171
- "analyze_project",
24172
- "Scan a project directory and return its full dependency graph \u2014 nodes, edges, entry points, leaf nodes, isolated nodes, critical nodes, and per-file metadata. Use this to understand the full structure of a codebase before making changes.",
24274
+ "describe_node",
24275
+ 'PREFERRED first tool for questions about a specific function, class, or entity. Returns what depends on it, what it depends on, its file/line, type, and role \u2014 all in ~50 tokens. Use this INSTEAD of analyze_project when the question is "tell me about X" or "what uses X" or "what does X import". Much cheaper than loading the full graph.',
24173
24276
  {
24174
- projectDir: external_exports.string().describe(
24175
- "Absolute path to the project directory to analyze (e.g. /Users/me/my-app)"
24277
+ projectDir: external_exports.string().describe("Absolute path to the project directory"),
24278
+ nodeName: external_exports.string().describe(
24279
+ 'Exact name of the entity to look up (e.g. "getUserById", "UserService", "AuthMiddleware")'
24176
24280
  )
24177
24281
  },
24178
- async ({ projectDir }) => {
24282
+ async ({ projectDir, nodeName }) => {
24179
24283
  try {
24180
- const result = analyzeProject(projectDir);
24284
+ const result = describeNode(projectDir, nodeName);
24181
24285
  return {
24182
24286
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
24183
24287
  };
@@ -24191,15 +24295,22 @@ server.tool(
24191
24295
  );
24192
24296
  server.tool(
24193
24297
  "get_graph_summary",
24194
- "Get a lightweight summary of the dependency graph \u2014 file/node/edge counts, entry points, leaf nodes, isolated nodes, and critical nodes \u2014 without returning the full graph data. Faster than analyze_project when you only need the overview.",
24298
+ 'Get a project-wide overview \u2014 file/node/edge counts, critical nodes, entry points, leaf nodes, and isolated nodes. Use verbosity "sketch" (~50 tokens) for a quick orientation, "overview" for the full summary. Use format "prose" to get a single readable sentence (~100 tokens) instead of JSON. Call this before analyze_project or when you only need counts and structure, not full node data.',
24195
24299
  {
24196
- projectDir: external_exports.string().describe("Absolute path to the project directory")
24300
+ projectDir: external_exports.string().describe("Absolute path to the project directory"),
24301
+ verbosity: external_exports.enum(["sketch", "overview", "full"]).optional().describe(
24302
+ '"sketch" = counts + critical nodes only (~50 tokens). "overview" = full summary with entry/leaf/isolated lists (default). "full" = same as overview.'
24303
+ ),
24304
+ format: external_exports.enum(["json", "prose"]).optional().describe(
24305
+ '"json" = structured JSON object (default). "prose" = single readable paragraph (~100 tokens) \u2014 use when you want compact context without JSON overhead.'
24306
+ )
24197
24307
  },
24198
- async ({ projectDir }) => {
24308
+ async ({ projectDir, verbosity, format }) => {
24199
24309
  try {
24200
- const result = getGraphSummary(projectDir);
24310
+ const result = getGraphSummary(projectDir, verbosity ?? "overview");
24311
+ const text = format === "prose" ? buildProseSummary(result) : JSON.stringify(result, null, 2);
24201
24312
  return {
24202
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
24313
+ content: [{ type: "text", text }]
24203
24314
  };
24204
24315
  } catch (err) {
24205
24316
  return {
@@ -24211,16 +24322,14 @@ server.tool(
24211
24322
  );
24212
24323
  server.tool(
24213
24324
  "simulate_impact",
24214
- "Simulate the cascading downstream impact of changing a specific function, class, or entity. Returns a risk score (0-100), risk level (LOW/MEDIUM/HIGH/CRITICAL), a list of all affected nodes with their change requirements, a testing plan, and actionable recommendations.",
24325
+ 'PREFERRED tool when the question is impact-oriented: "what breaks if I change X?". Use this INSTEAD of analyze_project \u2014 it requires no graph preload and costs ~200 tokens vs ~80k for the full graph. Returns risk score (0-100), risk level (LOW/MEDIUM/HIGH/CRITICAL), all affected nodes, a testing plan, and recommendations.',
24215
24326
  {
24216
- projectDir: external_exports.string().describe(
24217
- "Absolute path to the project directory"
24218
- ),
24327
+ projectDir: external_exports.string().describe("Absolute path to the project directory"),
24219
24328
  targetNode: external_exports.string().describe(
24220
24329
  'Name of the function, class, or entity to simulate changing (e.g. "getUserById")'
24221
24330
  ),
24222
24331
  changeDescription: external_exports.string().describe(
24223
- 'Human-readable description of the proposed change (e.g. "removing the userId parameter")'
24332
+ 'Description of the proposed change (e.g. "removing the userId parameter")'
24224
24333
  )
24225
24334
  },
24226
24335
  async ({ projectDir, targetNode, changeDescription }) => {
@@ -24237,9 +24346,40 @@ server.tool(
24237
24346
  }
24238
24347
  }
24239
24348
  );
24349
+ server.tool(
24350
+ "analyze_project",
24351
+ 'Return the dependency graph. EXPENSIVE at full verbosity \u2014 use verbosity "sketch" or "overview", or narrow scope with "focus" + "depth" to avoid token overload. Prefer describe_node for single-entity questions, simulate_impact for risk questions, and get_graph_summary for project-wide orientation.',
24352
+ {
24353
+ projectDir: external_exports.string().describe(
24354
+ "Absolute path to the project directory to analyze (e.g. /Users/me/my-app)"
24355
+ ),
24356
+ verbosity: external_exports.enum(["sketch", "overview", "full"]).optional().describe(
24357
+ '"sketch" = meta + summary only, no nodes/edges (~100 tokens). "overview" = meta + summary + nodes, no edges. "full" = complete graph incl. edges and file detail (default, can be very large).'
24358
+ ),
24359
+ focus: external_exports.string().optional().describe(
24360
+ "Entity name to centre the graph on \u2014 returns only the subgraph within `depth` hops of this node (bidirectional). Use to scope the result to one module instead of the whole project."
24361
+ ),
24362
+ depth: external_exports.number().optional().describe(
24363
+ "Max BFS hops from the focus node (default 3). Only used when focus is set."
24364
+ )
24365
+ },
24366
+ async ({ projectDir, verbosity, focus, depth }) => {
24367
+ try {
24368
+ const result = analyzeProject(projectDir, { verbosity, focus, depth });
24369
+ return {
24370
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
24371
+ };
24372
+ } catch (err) {
24373
+ return {
24374
+ content: [{ type: "text", text: `Error: ${err.message}` }],
24375
+ isError: true
24376
+ };
24377
+ }
24378
+ }
24379
+ );
24240
24380
  server.tool(
24241
24381
  "git_impact",
24242
- "Detect which functions/classes changed in a git diff and simulate their downstream impact on the whole project. Supports three modes: uncommitted changes, a specific commit, or a branch comparison.",
24382
+ 'PREFERRED tool for "what did this commit/branch change and what does it break?". Detects changed functions/classes from a git diff and simulates their downstream impact \u2014 no manual entity name needed. Supports uncommitted changes, a specific commit SHA, or a branch comparison.',
24243
24383
  {
24244
24384
  projectDir: external_exports.string().describe(
24245
24385
  "Absolute path to the project directory (must be a git repository)"
package/depgraph.js CHANGED
@@ -4173,7 +4173,7 @@ function getFlag(flag) {
4173
4173
  }
4174
4174
  function printHelp() {
4175
4175
  console.log(`
4176
- ${bold("DepGraph")} ${dim("v1.8.0")}
4176
+ ${bold("DepGraph")} ${dim("v1.9.0")}
4177
4177
  ${dim("Dependency mapping \xB7 Impact simulation \xB7 Developer intelligence")}
4178
4178
 
4179
4179
  ${bold("USAGE")}
@@ -4222,7 +4222,7 @@ ${bold("GIT EXAMPLES")}
4222
4222
  function printBanner() {
4223
4223
  console.log(`
4224
4224
  ${bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501")}
4225
- ${bold(" DepGraph")} ${dim("v1.8.0")}
4225
+ ${bold(" DepGraph")} ${dim("v1.9.0")}
4226
4226
  ${bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501")}
4227
4227
  `);
4228
4228
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depgraph-core",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Dependency mapping and impact simulation for JS/TS projects",
5
5
  "main": "depgraph.js",
6
6
  "bin": {