@dotdotgod/claude-code 0.3.10 → 0.3.11

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.
Files changed (2) hide show
  1. package/mcp/cli.mjs +223 -15
  2. package/package.json +2 -2
package/mcp/cli.mjs CHANGED
@@ -1345,12 +1345,18 @@ Checks or writes generated Markdown traceability link sections from canonical fe
1345
1345
  case "graph":
1346
1346
  return `Usage:
1347
1347
  dotdotgod graph impact <root> --changed <path> [--changed <path> ...] [--compact|--json|--yml|--yaml]
1348
+ dotdotgod graph serve <root> --changed <path> [--changed <path> ...] [--host HOST] [--port PORT]
1348
1349
  dotdotgod graph communities <root> [--json]`;
1349
1350
  case "graph impact":
1350
1351
  return `Usage:
1351
1352
  dotdotgod graph impact <root> --changed <path> [--changed <path> ...] [--compact|--json|--yml|--yaml]
1352
1353
 
1353
1354
  Ranks nodes related to one or more changed files. <root> is the project root; repeat --changed with project-relative file paths. Results include a combined ranking and the top five related nodes for each changed file. Use --compact for a short text summary or --yml/--yaml for structured agent-facing output.`;
1355
+ case "graph serve":
1356
+ return `Usage:
1357
+ dotdotgod graph serve <root> --changed <path> [--changed <path> ...] [--host HOST] [--port PORT]
1358
+
1359
+ Starts a local-only impact graph explorer. Changed files are rendered as root nodes and are excluded from related-result lists.`;
1354
1360
  case "graph communities":
1355
1361
  return `Usage:
1356
1362
  dotdotgod graph communities <root> [--json]`;
@@ -1374,6 +1380,7 @@ Commands:
1374
1380
  status Show local index status.
1375
1381
  traceability links Check or write generated traceability links.
1376
1382
  graph impact Find files related to changed files.
1383
+ graph serve Open a local impact graph explorer.
1377
1384
  graph communities Find groups of related project-memory nodes.
1378
1385
 
1379
1386
  Options:
@@ -1711,7 +1718,7 @@ import { spawnSync } from "node:child_process";
1711
1718
  import { basename as basename3, extname as extname2, join as join5 } from "node:path";
1712
1719
 
1713
1720
  // packages/cli/src/index/constants.mjs
1714
- var CACHE_VERSION = 13;
1721
+ var CACHE_VERSION = 14;
1715
1722
  var CACHE_DIR = ".dotdotgod";
1716
1723
  var MANIFEST_FILE = "manifest.json";
1717
1724
 
@@ -2225,7 +2232,7 @@ function mergeIncrementalGraph(previousGraph, changedGraph, changedPaths) {
2225
2232
  if (!previousGraph) return changedGraph;
2226
2233
  const changedSet = new Set(changedPaths);
2227
2234
  const changedNodeIds = new Set(previousGraph.nodes.filter((node2) => nodeOwnedByPath(node2, changedSet)).map((node2) => node2.id));
2228
- for (const node2 of changedGraph.nodes) changedNodeIds.add(node2.id);
2235
+ for (const node2 of changedGraph.nodes) if (nodeOwnedByPath(node2, changedSet)) changedNodeIds.add(node2.id);
2229
2236
  const graph = { nodes: [], edges: [] };
2230
2237
  for (const node2 of previousGraph.nodes) if (!changedNodeIds.has(node2.id) && !nodeOwnedByPath(node2, changedSet)) addNode(graph, node2.id, node2.type, Object.fromEntries(Object.entries(node2).filter(([key]) => key !== "id" && key !== "type")));
2231
2238
  for (const edge of previousGraph.edges) if (!changedNodeIds.has(edge.source) && !changedNodeIds.has(edge.target)) addEdge(graph, edge.source, edge.target, edge.relation, Object.fromEntries(Object.entries(edge).filter(([key]) => key !== "source" && key !== "target" && key !== "relation")));
@@ -3204,15 +3211,15 @@ import { resolve as resolve11 } from "node:path";
3204
3211
  function parse(argv) {
3205
3212
  const [action2 = "status", ...rest] = argv;
3206
3213
  let root = ".";
3207
- let json = false;
3214
+ let json2 = false;
3208
3215
  let confirm = false;
3209
3216
  for (const arg of rest) {
3210
- if (arg === "--json") json = true;
3217
+ if (arg === "--json") json2 = true;
3211
3218
  else if (arg === "--confirm") confirm = true;
3212
3219
  else if (!arg.startsWith("-") && root === ".") root = arg;
3213
3220
  else usage(`Unknown embedding option: ${arg}`, `embedding ${action2}`);
3214
3221
  }
3215
- return { action: action2, root: resolve11(root), json, confirm };
3222
+ return { action: action2, root: resolve11(root), json: json2, confirm };
3216
3223
  }
3217
3224
  function runEmbedding(argv, options = {}) {
3218
3225
  const parsed = parse(argv);
@@ -3339,8 +3346,8 @@ function buildDocumentationMap(projectRoot = ".", { depth = 5 } = {}) {
3339
3346
  }
3340
3347
 
3341
3348
  // packages/cli/src/commands/map.mjs
3342
- function fail(json, code, message) {
3343
- if (json) console.log(JSON.stringify({ ok: false, error: { code, message } }, null, 2));
3349
+ function fail(json2, code, message) {
3350
+ if (json2) console.log(JSON.stringify({ ok: false, error: { code, message } }, null, 2));
3344
3351
  else console.error(message);
3345
3352
  process.exit(2);
3346
3353
  }
@@ -4926,7 +4933,7 @@ function buildCombinedImpactReport(index, changedPaths, limits = {}) {
4926
4933
  const nodeById = new Map(graph.nodes.map((node2) => [node2.id, node2]));
4927
4934
  const seeds = changedPaths.map((path) => `file:${path}`);
4928
4935
  const seedSet = new Set(seeds);
4929
- const maxRelated = Math.max(limits.related ?? 25, seeds.length);
4936
+ const maxRelated = Math.max(limits.related ?? 25, 0);
4930
4937
  const groups = { files: { items: [], omitted: 0 }, docs: { items: [], omitted: 0 }, contracts: { items: [], omitted: 0 }, tests: { items: [], omitted: 0 }, commands: { items: [], omitted: 0 }, events: { items: [], omitted: 0 }, packageResources: { items: [], omitted: 0 }, symbols: { items: [], omitted: 0 } };
4931
4938
  const relatedIds = new Set(seeds);
4932
4939
  const reasons = new Map(seeds.map((seed) => [seed, /* @__PURE__ */ new Set(["changed-file"])]));
@@ -4962,7 +4969,8 @@ function buildCombinedImpactReport(index, changedPaths, limits = {}) {
4962
4969
  const scored = scoreImpactItem({ ...node2, reasons: reasonList, retrieval }, seedSet, changedPaths, policy, pprScores, config);
4963
4970
  return { ...node2, reasons: reasonList, hasCuratedEvidence, ...vectorEvidence.has(id) ? { vectorEvidence: vectorEvidence.get(id) } : {}, retrieval: { ...retrieval, signals: [.../* @__PURE__ */ new Set([...retrieval.signals ?? [], ...reasonSignals])] }, ...scored };
4964
4971
  }).sort(compareImpactItems(seeds));
4965
- const related = selectImpactItems(relatedAll, maxRelated, seeds);
4972
+ const rankedRelated = relatedAll.filter((item) => !seedSet.has(item.id));
4973
+ const related = selectImpactItems(rankedRelated, maxRelated, []);
4966
4974
  for (const item of related) {
4967
4975
  if (item.type === "file") {
4968
4976
  const area2 = docsArea2(item.path, config);
@@ -4984,9 +4992,9 @@ function buildImpactReport(index, changedPaths, limits = {}) {
4984
4992
  aggregate2.perSeed = normalized.map((changed) => {
4985
4993
  const seedId = `file:${changed}`;
4986
4994
  const seedOverlay = limits.overlay ? { ...limits.overlay, edges: (limits.overlay.edges ?? []).filter((edge) => edge.source === seedId) } : void 0;
4987
- const report = buildCombinedImpactReport(index, [changed], { ...limits, overlay: seedOverlay, related: Math.max(limits.related ?? 25, perSeedLimit + 1) });
4988
- const related = report.related.filter((item) => item.id !== `file:${changed}`).slice(0, perSeedLimit);
4989
- return { changed, related, omittedRelated: Math.max(0, report.related.length - 1 - related.length) + report.omittedRelated };
4995
+ const report = buildCombinedImpactReport(index, [changed], { ...limits, overlay: seedOverlay, related: Math.max(limits.related ?? 25, perSeedLimit) });
4996
+ const related = report.related.slice(0, perSeedLimit);
4997
+ return { changed, related, omittedRelated: Math.max(0, report.related.length - related.length) + report.omittedRelated };
4990
4998
  });
4991
4999
  return aggregate2;
4992
5000
  }
@@ -5006,7 +5014,7 @@ function compactImpactGroup(group = { items: [], omitted: 0 }, limit = 5) {
5006
5014
  return { items, omitted: (group.omitted ?? 0) + Math.max(0, (group.items?.length ?? 0) - items.length) };
5007
5015
  }
5008
5016
  function buildCompactImpactReport(impact, limits = {}) {
5009
- const relatedLimit = Math.max(limits.related ?? 10, impact.changedFiles?.length ?? 1);
5017
+ const relatedLimit = limits.related ?? 10;
5010
5018
  const groupLimit = limits.groupItems ?? 5;
5011
5019
  const changedFiles = impact.changedFiles ?? [impact.changed];
5012
5020
  const seedIds = new Set(changedFiles.map((path) => `file:${path}`));
@@ -5321,6 +5329,187 @@ function runTraceability(argv) {
5321
5329
  process.exit(ok ? 0 : 1);
5322
5330
  }
5323
5331
 
5332
+ // packages/cli/src/graph-view/server.mjs
5333
+ import { createServer } from "node:http";
5334
+ import { readFileSync as readFileSync15 } from "node:fs";
5335
+ import { dirname as dirname8, extname as extname7, join as join15 } from "node:path";
5336
+ import { fileURLToPath } from "node:url";
5337
+
5338
+ // packages/cli/src/graph-view/payload.mjs
5339
+ function edgeKey(edge) {
5340
+ return `${edge.source}\0${edge.target}\0${edge.relation}`;
5341
+ }
5342
+ function labelForNode(node2) {
5343
+ if (node2.type === "contract") return node2.title ?? node2.contractId ?? node2.id;
5344
+ return node2.path ?? node2.command ?? node2.name ?? node2.target ?? node2.id;
5345
+ }
5346
+ function adjacencyFor(nodes, edges) {
5347
+ const adjacency = new Map(nodes.map((node2) => [node2.id, /* @__PURE__ */ new Set()]));
5348
+ for (const edge of edges) {
5349
+ adjacency.get(edge.source)?.add(edge.target);
5350
+ adjacency.get(edge.target)?.add(edge.source);
5351
+ }
5352
+ return adjacency;
5353
+ }
5354
+ function rootReachability(nodes, edges, seedIds) {
5355
+ const adjacency = adjacencyFor(nodes, edges);
5356
+ const reachable = /* @__PURE__ */ new Set();
5357
+ const depthByNode = /* @__PURE__ */ new Map();
5358
+ const nearestRootByNode = /* @__PURE__ */ new Map();
5359
+ const queue = [];
5360
+ for (const node2 of nodes) {
5361
+ if (!seedIds.has(node2.id)) continue;
5362
+ reachable.add(node2.id);
5363
+ depthByNode.set(node2.id, 0);
5364
+ nearestRootByNode.set(node2.id, node2.id);
5365
+ queue.push(node2.id);
5366
+ }
5367
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
5368
+ const current = queue[cursor];
5369
+ for (const neighbor of adjacency.get(current) ?? []) {
5370
+ if (reachable.has(neighbor)) continue;
5371
+ reachable.add(neighbor);
5372
+ depthByNode.set(neighbor, (depthByNode.get(current) ?? 0) + 1);
5373
+ nearestRootByNode.set(neighbor, nearestRootByNode.get(current));
5374
+ queue.push(neighbor);
5375
+ }
5376
+ }
5377
+ return { reachable, depthByNode, nearestRootByNode };
5378
+ }
5379
+ function buildImpactGraphPayload(index, impact) {
5380
+ const graph = index?.graph ?? { nodes: [], edges: [] };
5381
+ const config = index?.memoryConfig;
5382
+ const seedIds = new Set((impact.changedFiles ?? [impact.changed]).map((path) => `file:${path}`));
5383
+ const impactItems = new Map((impact.related ?? []).map((item) => [item.id, item]));
5384
+ const isAllowedNode = (node2) => {
5385
+ if (!node2) return false;
5386
+ if (node2.path && resolveMemoryArea(node2.path, config)?.scope === "local") return false;
5387
+ return true;
5388
+ };
5389
+ const sourceNodes = (graph.nodes ?? []).filter(isAllowedNode);
5390
+ const indexedNodes = new Map(sourceNodes.map((node2) => [node2.id, node2]));
5391
+ const seenEdges = /* @__PURE__ */ new Set();
5392
+ const edges = [];
5393
+ for (const edge of graph.edges ?? []) {
5394
+ if (!indexedNodes.has(edge.source) || !indexedNodes.has(edge.target)) continue;
5395
+ const key = edgeKey(edge);
5396
+ if (seenEdges.has(key)) continue;
5397
+ seenEdges.add(key);
5398
+ edges.push({ source: edge.source, target: edge.target, relation: edge.relation, confidence: edge.confidence, weight: edge.weight ?? edge.relationWeight, score: edge.score, traceabilityKey: edge.traceabilityKey });
5399
+ }
5400
+ const { reachable, depthByNode, nearestRootByNode } = rootReachability(sourceNodes, edges, seedIds);
5401
+ const retainedNodes = sourceNodes.filter((node2) => reachable.has(node2.id));
5402
+ const retainedEdges = edges.filter((edge) => reachable.has(edge.source) && reachable.has(edge.target));
5403
+ const nodes = retainedNodes.map((source) => {
5404
+ const item = impactItems.get(source.id);
5405
+ const seed = seedIds.has(source.id);
5406
+ return {
5407
+ id: source.id,
5408
+ type: source.type ?? item?.type ?? "file",
5409
+ path: source.path ?? item?.path,
5410
+ label: labelForNode({ ...source, ...item }),
5411
+ seed,
5412
+ ranked: Boolean(item),
5413
+ depth: depthByNode.get(source.id) ?? 0,
5414
+ nearestRoot: nearestRootByNode.get(source.id),
5415
+ component: 0,
5416
+ seededComponent: true,
5417
+ impactScore: seed ? 100 : item?.impactScore ?? 0,
5418
+ reasons: seed ? ["changed-file"] : item?.reasons ?? [],
5419
+ scoreBreakdown: seed ? { seed: 100 } : item?.scoreBreakdown,
5420
+ retrieval: item?.retrieval ?? source.retrieval,
5421
+ contractId: item?.contractId ?? source.contractId,
5422
+ title: item?.title ?? source.title,
5423
+ sections: item?.sections ?? source.sections,
5424
+ vectorEvidence: item?.vectorEvidence
5425
+ };
5426
+ });
5427
+ return {
5428
+ changed: impact.changed,
5429
+ changedFiles: impact.changedFiles ?? [impact.changed],
5430
+ ranking: impact.ranking,
5431
+ semantic: impact.semantic,
5432
+ complete: true,
5433
+ nodes,
5434
+ edges: retainedEdges,
5435
+ components: retainedNodes.length > 0 ? [{ id: 0, size: retainedNodes.length, seeded: true }] : [],
5436
+ diagnostics: {
5437
+ rootNodes: retainedNodes.filter((node2) => seedIds.has(node2.id)).length,
5438
+ connectedNodes: retainedNodes.length,
5439
+ disconnectedNodesOmitted: sourceNodes.length - retainedNodes.length,
5440
+ maximumDepth: Math.max(0, ...depthByNode.values())
5441
+ },
5442
+ omittedRelated: impact.omittedRelated ?? 0
5443
+ };
5444
+ }
5445
+
5446
+ // packages/cli/src/graph-view/server.mjs
5447
+ var ASSET_ROOT = join15(dirname8(fileURLToPath(import.meta.url)), "assets");
5448
+ var CONTENT_TYPES = { ".css": "text/css; charset=utf-8", ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".map": "application/json; charset=utf-8" };
5449
+ var VENDOR_FILES = {
5450
+ "/vendor/sigma.js": new URL("../../node_modules/sigma/dist/sigma.min.js", import.meta.url),
5451
+ "/vendor/graphology.js": new URL("../../node_modules/graphology/dist/graphology.umd.min.js", import.meta.url),
5452
+ "/vendor/d3.js": new URL("../../node_modules/d3/dist/d3.min.js", import.meta.url)
5453
+ };
5454
+ function json(response, status, value) {
5455
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
5456
+ response.end(JSON.stringify(value));
5457
+ }
5458
+ function asset(response, file) {
5459
+ try {
5460
+ const body = readFileSync15(file);
5461
+ response.writeHead(200, { "content-type": CONTENT_TYPES[extname7(file)] ?? "application/octet-stream", "cache-control": "no-store" });
5462
+ response.end(body);
5463
+ } catch {
5464
+ response.writeHead(404);
5465
+ response.end("Not found");
5466
+ }
5467
+ }
5468
+ async function buildServedImpact(root, changedPaths) {
5469
+ const normalized = [...new Set(changedPaths.map((path) => canonicalizeChangedPath(root, path)).filter(Boolean))];
5470
+ const { status, index, metadata } = readFreshIndex(root);
5471
+ const overlay = await buildVectorImpactOverlay(root, index, normalized);
5472
+ const impact = buildImpactReport(index, normalized, { overlay, verboseSemantic: false, related: 40 });
5473
+ const graph = buildImpactGraphPayload(index, impact);
5474
+ const graphIds = new Set(graph.nodes.map((node2) => node2.id));
5475
+ const explorerImpact = {
5476
+ ...impact,
5477
+ related: impact.related.filter((item) => graphIds.has(item.id))
5478
+ };
5479
+ return { ok: status.ok, status, metadata, impact: explorerImpact, graph };
5480
+ }
5481
+ async function startImpactGraphServer({ root, changed, host = "127.0.0.1", port = 0 }) {
5482
+ let payload = await buildServedImpact(root, changed);
5483
+ const server = createServer(async (request, response) => {
5484
+ const url = new URL(request.url ?? "/", `http://${request.headers.host ?? host}`);
5485
+ if (url.pathname === "/api/impact") return json(response, 200, payload);
5486
+ if (url.pathname === "/api/reroot") {
5487
+ const paths = url.searchParams.getAll("changed");
5488
+ if (paths.length === 0) return json(response, 400, { ok: false, error: { code: "MISSING_CHANGED", message: "At least one changed path is required." } });
5489
+ try {
5490
+ payload = await buildServedImpact(root, paths);
5491
+ return json(response, 200, payload);
5492
+ } catch (error) {
5493
+ return json(response, 500, { ok: false, error: { code: "GRAPH_SERVE_FAILED", message: error instanceof Error ? error.message : String(error) } });
5494
+ }
5495
+ }
5496
+ if (VENDOR_FILES[url.pathname]) return asset(response, fileURLToPath(VENDOR_FILES[url.pathname]));
5497
+ const relative6 = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
5498
+ if (!["index.html", "app.css", "app.js"].includes(relative6)) {
5499
+ response.writeHead(404);
5500
+ return response.end("Not found");
5501
+ }
5502
+ return asset(response, join15(ASSET_ROOT, relative6));
5503
+ });
5504
+ await new Promise((resolve17, reject) => {
5505
+ server.once("error", reject);
5506
+ server.listen(port, host, resolve17);
5507
+ });
5508
+ const address = server.address();
5509
+ const actualPort = typeof address === "object" && address ? address.port : port;
5510
+ return { server, url: `http://${host}:${actualPort}`, payload };
5511
+ }
5512
+
5324
5513
  // packages/cli/src/impact/format.mjs
5325
5514
  function formatCompactImpactGroup(name, group) {
5326
5515
  const items = group?.items ?? [];
@@ -5428,6 +5617,8 @@ function parseGraphOptions(argv) {
5428
5617
  const changed = [];
5429
5618
  let compact = false;
5430
5619
  let yml = false;
5620
+ let host = "127.0.0.1";
5621
+ let port = 0;
5431
5622
  for (let i = 0; i < argv.length; i += 1) {
5432
5623
  if (argv[i] === "--changed") {
5433
5624
  const next = argv[i + 1];
@@ -5437,19 +5628,36 @@ function parseGraphOptions(argv) {
5437
5628
  }
5438
5629
  } else if (argv[i] === "--compact") compact = true;
5439
5630
  else if (argv[i] === "--yml" || argv[i] === "--yaml") yml = true;
5440
- else filtered.push(argv[i]);
5631
+ else if (argv[i] === "--host" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
5632
+ host = argv[i + 1];
5633
+ i += 1;
5634
+ } else if (argv[i] === "--port" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
5635
+ port = Number(argv[i + 1]);
5636
+ i += 1;
5637
+ } else filtered.push(argv[i]);
5441
5638
  }
5442
5639
  const options = parseCommon(filtered);
5443
5640
  options.changed = [...new Set(changed)];
5444
5641
  options.compact = compact;
5445
5642
  options.yml = yml;
5643
+ options.host = host;
5644
+ options.port = Number.isInteger(port) && port >= 0 && port <= 65535 ? port : NaN;
5446
5645
  return options;
5447
5646
  }
5448
5647
  async function runGraph(argv) {
5449
5648
  const sub = argv[0];
5450
5649
  const isImpact = sub === "impact";
5451
- if (!["impact", "communities"].includes(sub)) usage(sub ? `Unknown graph command: ${sub}` : "Missing graph command.", "graph");
5650
+ const isServe = sub === "serve";
5651
+ if (!["impact", "communities", "serve"].includes(sub)) usage(sub ? `Unknown graph command: ${sub}` : "Missing graph command.", "graph");
5452
5652
  const options = parseGraphOptions(argv.slice(1));
5653
+ if (isServe && options.changed.length === 0) usage("Missing required option: --changed <path>.", "graph serve");
5654
+ if (isServe && !Number.isInteger(options.port)) usage("Invalid --port. Use an integer from 0 through 65535.", "graph serve");
5655
+ if (isServe) {
5656
+ options.changed = [...new Set(options.changed.map((path) => canonicalizeChangedPath(options.root, path)).filter(Boolean))];
5657
+ const running = await startImpactGraphServer({ root: options.root, changed: options.changed, host: options.host, port: options.port });
5658
+ console.log(`graph serve: ${running.url}`);
5659
+ return;
5660
+ }
5453
5661
  if (isImpact && [options.json, options.compact, options.yml].filter(Boolean).length > 1) {
5454
5662
  const message = "Choose only one graph impact output mode: --compact, --json, or --yml/--yaml.";
5455
5663
  if (options.json) console.log(JSON.stringify({ ok: false, command: "graph impact", compact: options.compact || void 0, yml: options.yml || void 0, root: options.root, error: { code: "OUTPUT_MODE_CONFLICT", message }, usage: commandUsage("graph impact") }, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdotgod/claude-code",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "description": "Claude Code adapter for dotdotgod project memory workflows.",
5
5
  "type": "module",
6
6
  "license": "Elastic-2.0",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "esbuild": "0.27.3",
28
- "@dotdotgod/context": "0.3.10"
28
+ "@dotdotgod/context": "0.3.11"
29
29
  },
30
30
  "files": [
31
31
  ".claude-plugin",