@dreamtree-org/graphify 1.1.2 → 1.2.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.
@@ -332,7 +332,10 @@ function scoreNodes(graph, query) {
332
332
  }
333
333
  if (score > 0) matches.push({ id, label, score });
334
334
  });
335
- matches.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
335
+ const placeholderRank = (id) => id.startsWith("module:") || id.startsWith("external:") ? 1 : 0;
336
+ matches.sort(
337
+ (a, b) => b.score - a.score || placeholderRank(a.id) - placeholderRank(b.id) || a.id.localeCompare(b.id)
338
+ );
336
339
  return matches;
337
340
  }
338
341
  function resolveNode(graph, query) {
@@ -542,6 +545,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
542
545
  if (!target) return null;
543
546
  const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;
544
547
  const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;
548
+ const followed = options.relations ? new Set(options.relations) : DEPENDENCY_RELATIONS;
545
549
  const affected = [];
546
550
  const visited = /* @__PURE__ */ new Set([target.id]);
547
551
  let frontier = [target.id];
@@ -552,7 +556,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
552
556
  graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {
553
557
  if (visited.has(source)) return;
554
558
  const relation = edgeAttrs.relation;
555
- if (!DEPENDENCY_RELATIONS.has(relation)) return;
559
+ if (!followed.has(relation)) return;
556
560
  const confidence = edgeAttrs.confidence;
557
561
  const existing = nextFrontier.get(source);
558
562
  if (existing && CONFIDENCE_RANK2[existing.via.confidence] >= CONFIDENCE_RANK2[confidence]) return;
@@ -611,11 +615,11 @@ var init_impact = __esm({
611
615
  function isTestFile(path18) {
612
616
  return TEST_FILE_PATTERNS.some((p) => p.test(path18));
613
617
  }
614
- function selectionFromImpact(graph, targetIds, targetLabel) {
618
+ function selectionFromImpact(graph, targetIds, targetLabel, relations) {
615
619
  const best = /* @__PURE__ */ new Map();
616
620
  let affectedNodeCount = 0;
617
621
  for (const id of targetIds) {
618
- const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT });
622
+ const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT, relations });
619
623
  if (!impact) continue;
620
624
  affectedNodeCount += impact.affected.length;
621
625
  for (const node of impact.affected) {
@@ -627,11 +631,27 @@ function selectionFromImpact(graph, targetIds, targetLabel) {
627
631
  }
628
632
  }
629
633
  const testFiles = [...best.entries()].map(([file, info]) => ({ file, depth: info.depth, via: info.via })).sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
630
- return { target: targetLabel, testFiles, affectedNodeCount };
634
+ return {
635
+ target: targetLabel,
636
+ testFiles,
637
+ affectedNodeCount,
638
+ precision: relations ? "usage" : "import-reachability"
639
+ };
640
+ }
641
+ function entityRootsOf(graph, id) {
642
+ if (id.includes("::")) return [id];
643
+ const roots = [];
644
+ graph.forEachOutEdge(id, (_edge, attrs, _source, target) => {
645
+ if (String(attrs.relation) === "contains") roots.push(target);
646
+ });
647
+ roots.sort((a, b) => a.localeCompare(b));
648
+ return roots.length > 0 ? roots : [id];
631
649
  }
632
650
  function testsForNode(graph, nodeQuery) {
633
651
  const match = resolveNode(graph, nodeQuery);
634
652
  if (!match) return null;
653
+ const precise = selectionFromImpact(graph, entityRootsOf(graph, match.id), match.id, USAGE_RELATIONS);
654
+ if (precise.testFiles.length > 0) return precise;
635
655
  return selectionFromImpact(graph, [match.id], match.id);
636
656
  }
637
657
  function testsForChangedFiles(graph, changedFiles) {
@@ -644,7 +664,12 @@ function testsForChangedFiles(graph, changedFiles) {
644
664
  }
645
665
  if (graph.hasNode(file)) fileIds.push(file);
646
666
  }
647
- const selection = selectionFromImpact(graph, fileIds, changedFiles.join(", "));
667
+ const label = changedFiles.join(", ");
668
+ const preciseRoots = fileIds.flatMap((f) => entityRootsOf(graph, f));
669
+ let selection = selectionFromImpact(graph, preciseRoots, label, USAGE_RELATIONS);
670
+ if (selection.testFiles.length === 0) {
671
+ selection = selectionFromImpact(graph, fileIds, label);
672
+ }
648
673
  for (const [file, info] of selfSelected) {
649
674
  if (!selection.testFiles.some((t) => t.file === file)) {
650
675
  selection.testFiles.push({ file, depth: info.depth, via: info.via });
@@ -653,7 +678,7 @@ function testsForChangedFiles(graph, changedFiles) {
653
678
  selection.testFiles.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
654
679
  return selection;
655
680
  }
656
- var TEST_FILE_PATTERNS, TEST_REACH_DEPTH, TEST_REACH_LIMIT;
681
+ var TEST_FILE_PATTERNS, TEST_REACH_DEPTH, TEST_REACH_LIMIT, USAGE_RELATIONS;
657
682
  var init_testmap = __esm({
658
683
  "src/testmap.ts"() {
659
684
  "use strict";
@@ -676,6 +701,7 @@ var init_testmap = __esm({
676
701
  ];
677
702
  TEST_REACH_DEPTH = 4;
678
703
  TEST_REACH_LIMIT = 2e3;
704
+ USAGE_RELATIONS = ["calls", "inherits", "implements", "mixes_in", "embeds", "references"];
679
705
  }
680
706
  });
681
707
 
@@ -3213,13 +3239,11 @@ async function extractTypeScript(filePath, displayPath) {
3213
3239
  });
3214
3240
  }
3215
3241
  function importTargetId(binding, property) {
3216
- if (!binding.ref.external) {
3217
- if (binding.kind === "named" && binding.importedName) {
3218
- return entityId(binding.ref.id, binding.importedName);
3219
- }
3220
- if (binding.kind === "namespace" && property) {
3221
- return entityId(binding.ref.id, property);
3222
- }
3242
+ if (binding.kind === "named" && binding.importedName) {
3243
+ return entityId(binding.ref.id, binding.importedName);
3244
+ }
3245
+ if (binding.kind === "namespace" && property) {
3246
+ return entityId(binding.ref.id, property);
3223
3247
  }
3224
3248
  return binding.ref.id;
3225
3249
  }
@@ -3257,24 +3281,35 @@ async function extractTypeScript(filePath, displayPath) {
3257
3281
  }
3258
3282
  }
3259
3283
  }
3284
+ function unwrapExpression(node) {
3285
+ let current = node;
3286
+ while (current.type === "as_expression" || current.type === "satisfies_expression" || current.type === "non_null_expression" || current.type === "parenthesized_expression") {
3287
+ const inner = namedChildren(current)[0];
3288
+ if (!inner) break;
3289
+ current = inner;
3290
+ }
3291
+ return current;
3292
+ }
3260
3293
  function handleTopLevelVariableFunctions(node) {
3294
+ const isExported = node.parent?.type === "export_statement";
3261
3295
  for (const declarator of namedChildren(node)) {
3262
3296
  if (declarator.type !== "variable_declarator") continue;
3263
3297
  const value = declarator.childForFieldName("value");
3264
3298
  if (!value) continue;
3265
- if (value.type === "arrow_function" || value.type === "function_expression") {
3299
+ const core = unwrapExpression(value);
3300
+ if (core.type === "arrow_function" || core.type === "function_expression") {
3266
3301
  const name = declarator.childForFieldName("name")?.text;
3267
3302
  if (!name) continue;
3268
3303
  const id = entityId(sourceFile, name);
3269
3304
  builder.addNode({ id, label: `function ${name}`, sourceFile, sourceLocation: lineOf(declarator) });
3270
3305
  builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
3271
3306
  nameIndex.set(name, id);
3272
- functionNodeMap.set(value.id, id);
3307
+ functionNodeMap.set(core.id, id);
3273
3308
  continue;
3274
3309
  }
3275
- if (value.type === "call_expression") {
3276
- const callee = value.childForFieldName("function");
3277
- const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(value) : null;
3310
+ if (core.type === "call_expression") {
3311
+ const callee = core.childForFieldName("function");
3312
+ const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(core) : null;
3278
3313
  if (specifier) {
3279
3314
  const nameNode = declarator.childForFieldName("name");
3280
3315
  if (nameNode) {
@@ -3282,6 +3317,21 @@ async function extractTypeScript(filePath, displayPath) {
3282
3317
  addModuleNode(ref);
3283
3318
  registerRequireBinding(nameNode, ref);
3284
3319
  }
3320
+ continue;
3321
+ }
3322
+ }
3323
+ if (isExported) {
3324
+ const name = declarator.childForFieldName("name")?.text;
3325
+ if (!name) continue;
3326
+ const id = entityId(sourceFile, name);
3327
+ builder.addNode({ id, label: `const ${name}`, sourceFile, sourceLocation: lineOf(declarator) });
3328
+ builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
3329
+ nameIndex.set(name, id);
3330
+ if (core.type === "identifier") {
3331
+ const aliased = nameIndex.get(core.text);
3332
+ if (aliased !== void 0 && aliased !== id) {
3333
+ builder.addEdge({ source: id, target: aliased, relation: "references", confidence: "EXTRACTED" });
3334
+ }
3285
3335
  }
3286
3336
  }
3287
3337
  }
@@ -3661,13 +3711,12 @@ function candidatePaths(guessed) {
3661
3711
  }
3662
3712
  return candidates.filter((c) => c !== guessed);
3663
3713
  }
3664
- function uniqueMatch(guessed, candidates, realIds) {
3714
+ function uniqueMatch(candidates, real) {
3665
3715
  const matches = /* @__PURE__ */ new Set();
3666
3716
  for (const candidate of candidates) {
3667
- if (realIds.has(candidate)) matches.add(candidate);
3717
+ if (real.has(candidate)) matches.add(candidate);
3668
3718
  }
3669
- if (matches.size !== 1) return null;
3670
- return [...matches][0];
3719
+ return matches.size === 1 ? [...matches][0] : null;
3671
3720
  }
3672
3721
  function resolveCrossFileReferences(extractions, options = {}) {
3673
3722
  const selfNames = options.selfNames ?? [];
@@ -3688,20 +3737,24 @@ function resolveCrossFileReferences(extractions, options = {}) {
3688
3737
  }
3689
3738
  }
3690
3739
  const remap = /* @__PURE__ */ new Map();
3691
- const resolveId = (id) => {
3692
- if (id.startsWith("module:") && selfNames.length > 0) {
3693
- const cachedSelf = remap.get(id);
3694
- if (cachedSelf !== void 0) return cachedSelf;
3695
- const candidates = selfImportCandidates(id.slice("module:".length), selfNames);
3696
- if (candidates !== null) {
3697
- const resolved2 = uniqueMatch(id, candidates, realFiles);
3698
- if (resolved2 !== null) {
3699
- remap.set(id, resolved2);
3700
- return resolved2;
3740
+ let resolvedEndpoints = 0;
3741
+ const applyRemap = (resolveId) => {
3742
+ for (const extraction of extractions) {
3743
+ for (const edge of extraction.edges) {
3744
+ const source = resolveId(edge.source);
3745
+ if (source !== null) {
3746
+ edge.source = source;
3747
+ resolvedEndpoints++;
3748
+ }
3749
+ const target = resolveId(edge.target);
3750
+ if (target !== null) {
3751
+ edge.target = target;
3752
+ resolvedEndpoints++;
3701
3753
  }
3702
3754
  }
3703
- return null;
3704
3755
  }
3756
+ };
3757
+ const resolvePathId = (id) => {
3705
3758
  if (isOpaque(id)) return null;
3706
3759
  const cached = remap.get(id);
3707
3760
  if (cached !== void 0) return cached;
@@ -3711,30 +3764,77 @@ function resolveCrossFileReferences(extractions, options = {}) {
3711
3764
  if (realIds.has(id)) return null;
3712
3765
  const guessedPath = id.slice(0, sep3);
3713
3766
  const name = id.slice(sep3);
3714
- const candidates = candidatePaths(guessedPath).map((p) => `${p}${name}`);
3715
- resolved = uniqueMatch(id, candidates, realIds);
3767
+ resolved = uniqueMatch(candidatePaths(guessedPath).map((p) => `${p}${name}`), realIds);
3716
3768
  } else {
3717
3769
  if (realFiles.has(id)) return null;
3718
- resolved = uniqueMatch(id, candidatePaths(id), realFiles);
3770
+ resolved = uniqueMatch(candidatePaths(id), realFiles);
3719
3771
  }
3720
3772
  if (resolved !== null) remap.set(id, resolved);
3721
3773
  return resolved;
3722
3774
  };
3723
- let resolvedEndpoints = 0;
3775
+ applyRemap(resolvePathId);
3776
+ const namedAlias = /* @__PURE__ */ new Map();
3777
+ const starTargets = /* @__PURE__ */ new Map();
3724
3778
  for (const extraction of extractions) {
3725
3779
  for (const edge of extraction.edges) {
3726
- const source = resolveId(edge.source);
3727
- if (source !== null) {
3728
- edge.source = source;
3729
- resolvedEndpoints++;
3730
- }
3731
- const target = resolveId(edge.target);
3732
- if (target !== null) {
3733
- edge.target = target;
3734
- resolvedEndpoints++;
3780
+ if (edge.relation !== "re_exports" || !realFiles.has(edge.source)) continue;
3781
+ const sep3 = edge.target.lastIndexOf("::");
3782
+ if (sep3 > 0 && realIds.has(edge.target)) {
3783
+ namedAlias.set(`${edge.source}|${edge.target.slice(sep3 + 2)}`, edge.target);
3784
+ } else if (sep3 < 0 && realFiles.has(edge.target)) {
3785
+ const targets = starTargets.get(edge.source) ?? [];
3786
+ targets.push(edge.target);
3787
+ starTargets.set(edge.source, targets);
3735
3788
  }
3736
3789
  }
3737
3790
  }
3791
+ const throughBarrel = (file, name) => {
3792
+ const direct = `${file}::${name}`;
3793
+ if (realIds.has(direct)) return direct;
3794
+ const aliased = namedAlias.get(`${file}|${name}`);
3795
+ if (aliased !== void 0) return aliased;
3796
+ const viaStar = (starTargets.get(file) ?? []).map((sf) => `${sf}::${name}`).filter((id) => realIds.has(id));
3797
+ return viaStar.length === 1 ? viaStar[0] : null;
3798
+ };
3799
+ const resolveBarrelId = (id) => {
3800
+ const cached = remap.get(id);
3801
+ if (cached !== void 0) return cached;
3802
+ if (id.startsWith("module:")) {
3803
+ const spec = id.slice("module:".length);
3804
+ const sep4 = spec.indexOf("::");
3805
+ const moduleSpec = sep4 > 0 ? spec.slice(0, sep4) : spec;
3806
+ const name = sep4 > 0 ? spec.slice(sep4 + 2) : null;
3807
+ const candidates = selfImportCandidates(moduleSpec, selfNames);
3808
+ if (candidates === null) {
3809
+ if (name === null) return null;
3810
+ remap.set(id, `module:${moduleSpec}`);
3811
+ return `module:${moduleSpec}`;
3812
+ }
3813
+ const file = uniqueMatch(candidates, realFiles);
3814
+ let resolved = null;
3815
+ if (file !== null) {
3816
+ resolved = name !== null ? throughBarrel(file, name) ?? file : file;
3817
+ } else if (name !== null) {
3818
+ resolved = `module:${moduleSpec}`;
3819
+ }
3820
+ if (resolved !== null) remap.set(id, resolved);
3821
+ return resolved;
3822
+ }
3823
+ const sep3 = id.indexOf("::");
3824
+ if (sep3 > 0 && !realIds.has(id)) {
3825
+ const guessedPath = id.slice(0, sep3);
3826
+ const name = id.slice(sep3 + 2);
3827
+ const files = [guessedPath, ...candidatePaths(guessedPath)].filter((f) => realFiles.has(f));
3828
+ const resolvedSet = new Set(files.map((f) => throughBarrel(f, name)).filter((r) => r !== null));
3829
+ if (resolvedSet.size === 1) {
3830
+ const resolved = [...resolvedSet][0];
3831
+ remap.set(id, resolved);
3832
+ return resolved;
3833
+ }
3834
+ }
3835
+ return null;
3836
+ };
3837
+ applyRemap(resolveBarrelId);
3738
3838
  let droppedPlaceholders = 0;
3739
3839
  for (const extraction of extractions) {
3740
3840
  const before = extraction.nodes.length;
@@ -4261,7 +4361,8 @@ async function runTests(nodeQuery, options = {}) {
4261
4361
  );
4262
4362
  return;
4263
4363
  }
4264
- console.log(`Test files for ${selection.target} (most direct first):`);
4364
+ const how = selection.precision === "usage" ? "selected by actual usage (calls/inheritance)" : "selected by import reachability \u2014 may over-include files sharing an entry point";
4365
+ console.log(`Test files for ${selection.target} (${how}; most direct first):`);
4265
4366
  for (const test of selection.testFiles) {
4266
4367
  const via = test.depth === 0 ? test.via : `depth ${test.depth}, via ${test.via}`;
4267
4368
  console.log(` ${test.file} (${via})`);
@@ -4677,6 +4778,29 @@ function packageRoot() {
4677
4778
  const packageJsonPath = require4.resolve("@dreamtree-org/graphify/package.json");
4678
4779
  return path15.dirname(packageJsonPath);
4679
4780
  }
4781
+ var MCP_SERVER_ENTRY = {
4782
+ command: "npx",
4783
+ args: ["-y", "@dreamtree-org/graphify", "--mcp"]
4784
+ };
4785
+ async function upsertMcpServer(filePath, serversKey = "mcpServers") {
4786
+ let config = {};
4787
+ try {
4788
+ config = JSON.parse(await fs22.readFile(filePath, "utf-8"));
4789
+ } catch (error) {
4790
+ if (error.code !== "ENOENT") {
4791
+ throw new Error(`cannot update ${filePath} \u2014 it exists but is not valid JSON; fix or remove it and re-run.`, {
4792
+ cause: error
4793
+ });
4794
+ }
4795
+ }
4796
+ const servers = config[serversKey] ?? {};
4797
+ servers["graphify"] = MCP_SERVER_ENTRY;
4798
+ config[serversKey] = servers;
4799
+ await fs22.mkdir(path15.dirname(filePath), { recursive: true });
4800
+ await fs22.writeFile(filePath, `${JSON.stringify(config, null, 2)}
4801
+ `, "utf-8");
4802
+ return filePath;
4803
+ }
4680
4804
  var MD_MARKER_BEGIN = "<!-- >>> graphify >>> -->";
4681
4805
  var MD_MARKER_END = "<!-- <<< graphify <<< -->";
4682
4806
  async function writeOwnedFile(filePath, content) {
@@ -4711,7 +4835,8 @@ var PLATFORMS = {
4711
4835
  claude: async (cwd, content) => {
4712
4836
  const target = path15.join(cwd, ".claude", "skills", "graphify", "SKILL.md");
4713
4837
  await writeOwnedFile(target, content);
4714
- return { host: "Claude Code", path: target };
4838
+ const mcpPath = await upsertMcpServer(path15.join(cwd, ".mcp.json"));
4839
+ return { host: "Claude Code", path: target, mcpPath };
4715
4840
  },
4716
4841
  cursor: async (cwd, content) => {
4717
4842
  const target = path15.join(cwd, ".cursor", "rules", "graphify.mdc");
@@ -4724,7 +4849,8 @@ alwaysApply: false
4724
4849
  ---
4725
4850
  ${body}`
4726
4851
  );
4727
- return { host: "Cursor", path: target };
4852
+ const mcpPath = await upsertMcpServer(path15.join(cwd, ".cursor", "mcp.json"));
4853
+ return { host: "Cursor", path: target, mcpPath };
4728
4854
  },
4729
4855
  windsurf: async (cwd, content) => {
4730
4856
  const target = path15.join(cwd, ".windsurf", "rules", "graphify.md");
@@ -4744,10 +4870,32 @@ ${body}`
4744
4870
  gemini: async (cwd, content) => {
4745
4871
  const target = path15.join(cwd, "GEMINI.md");
4746
4872
  await upsertMarkedBlock(target, content.replace(/^---[\s\S]*?---\n/, ""));
4747
- return { host: "Gemini CLI", path: target };
4873
+ const mcpPath = await upsertMcpServer(path15.join(cwd, ".gemini", "settings.json"));
4874
+ return { host: "Gemini CLI", path: target, mcpPath };
4748
4875
  }
4749
4876
  };
4750
4877
  var SUPPORTED_PLATFORMS = Object.keys(PLATFORMS).sort((a, b) => a.localeCompare(b));
4878
+ var PLATFORM_MARKERS = {
4879
+ cursor: [".cursor"],
4880
+ windsurf: [".windsurf"],
4881
+ cline: [".clinerules"],
4882
+ agents: ["AGENTS.md"],
4883
+ gemini: ["GEMINI.md", ".gemini"]
4884
+ };
4885
+ async function detectPlatforms(cwd = process.cwd()) {
4886
+ const platforms = ["claude"];
4887
+ for (const [platform, markers] of Object.entries(PLATFORM_MARKERS)) {
4888
+ for (const marker of markers) {
4889
+ try {
4890
+ await fs22.access(path15.join(cwd, marker));
4891
+ platforms.push(platform);
4892
+ break;
4893
+ } catch {
4894
+ }
4895
+ }
4896
+ }
4897
+ return platforms;
4898
+ }
4751
4899
  async function runInstall(platforms = ["claude"], cwd = process.cwd()) {
4752
4900
  const sourcePath = path15.join(packageRoot(), "src", "skill", "SKILL.md");
4753
4901
  const content = await fs22.readFile(sourcePath, "utf-8");
@@ -4763,7 +4911,8 @@ async function runInstall(platforms = ["claude"], cwd = process.cwd()) {
4763
4911
  results.push(await installer(cwd, content));
4764
4912
  }
4765
4913
  for (const result of results) {
4766
- console.log(`Installed graphify for ${result.host} -> ${result.path}`);
4914
+ const mcpNote = result.mcpPath ? ` (MCP server registered in ${result.mcpPath})` : "";
4915
+ console.log(`Installed graphify for ${result.host} -> ${result.path}${mcpNote}`);
4767
4916
  }
4768
4917
  return results;
4769
4918
  }
@@ -4879,7 +5028,7 @@ async function watchAndRebuild(root, runOnce) {
4879
5028
  });
4880
5029
  }
4881
5030
  var program = new import_commander.Command();
4882
- program.name("graphify").description("Turn a folder of code/docs/papers into a queryable knowledge graph.").argument("[path]", "path to scan, or a GitHub URL", ".").option("--mode <mode>", "extraction mode (deep for richer INFERRED edges \u2014 reserved, no-op in v1)").option("--update", "incremental rebuild \u2014 reuse cached extractions for unchanged files").option("--watch", "rebuild on file change").option("--cluster-only", "rerun clustering on an existing graph").option("--leiden", "use Leiden instead of Louvain for community detection (v2 \u2014 better community quality)").option("--max-iterations <n>", "Leiden only: cap on outer iterations for very large graphs", Number).option("--no-viz", "skip graph.html").option("--svg", "also export graph.svg (not implemented in v1)").option("--graphml", "also export graph.graphml (not implemented in v1)").option("--neo4j", "generate graphify-out/cypher.txt for Neo4j (not implemented in v1)").option("--mcp", "start MCP stdio server instead of running the pipeline").option("--mysql <dsn>", "also extract a MySQL schema (mysql://user:pass@host:port/db) into the graph").action(async (targetArg, options) => {
5031
+ program.name("graphify").description("Turn a folder of code/docs/papers into a queryable knowledge graph.").argument("[path]", "path to scan, or a GitHub URL", ".").option("--mode <mode>", "extraction mode (deep for richer INFERRED edges \u2014 reserved, no-op in v1)").option("--update", "incremental rebuild \u2014 reuse cached extractions for unchanged files").option("--watch", "rebuild on file change").option("--cluster-only", "rerun clustering on an existing graph").option("--leiden", "use Leiden instead of Louvain for community detection (v2 \u2014 better community quality)").option("--max-iterations <n>", "Leiden only: cap on outer iterations for very large graphs", Number).option("--no-viz", "skip graph.html").option("--svg", "also export graph.svg (not implemented in v1)").option("--graphml", "also export graph.graphml (not implemented in v1)").option("--neo4j", "generate graphify-out/cypher.txt for Neo4j (not implemented in v1)").option("--mcp", "start MCP stdio server instead of running the pipeline").option("--no-install", "skip auto-installing the agent skill + MCP config after the build").option("--mysql <dsn>", "also extract a MySQL schema (mysql://user:pass@host:port/db) into the graph").action(async (targetArg, options) => {
4883
5032
  try {
4884
5033
  if (options.mcp) {
4885
5034
  const outDir = path17.resolve(targetArg === "." ? process.cwd() : targetArg, "graphify-out");
@@ -4888,7 +5037,8 @@ program.name("graphify").description("Turn a folder of code/docs/papers into a q
4888
5037
  return;
4889
5038
  }
4890
5039
  let root = targetArg;
4891
- if (looksLikeGitUrl(targetArg)) {
5040
+ const cloned = looksLikeGitUrl(targetArg);
5041
+ if (cloned) {
4892
5042
  console.error(`Cloning ${targetArg} ...`);
4893
5043
  root = await cloneRepo(targetArg);
4894
5044
  }
@@ -4914,6 +5064,13 @@ program.name("graphify").description("Turn a folder of code/docs/papers into a q
4914
5064
  onProgress: (m) => console.error(m)
4915
5065
  });
4916
5066
  await runOnce();
5067
+ if (options.install && !cloned) {
5068
+ try {
5069
+ await runInstall(await detectPlatforms(root), root);
5070
+ } catch (error) {
5071
+ console.error(`graphify: agent skill/MCP install skipped: ${error.message}`);
5072
+ }
5073
+ }
4917
5074
  if (options.watch) {
4918
5075
  console.error(`Watching ${root} for changes (Ctrl+C to stop) ...`);
4919
5076
  await watchAndRebuild(root, runOnce);
@@ -4995,7 +5152,7 @@ program.command("benchmark [questions...]").description("measure token savings o
4995
5152
  process.exitCode = 1;
4996
5153
  }
4997
5154
  });
4998
- program.command("install").description("install the skill/rules files into local agents (claude, cursor, windsurf, cline, agents, gemini, all)").option("--platform <names...>", "platform(s) to install for", ["claude"]).action(async (options) => {
5155
+ program.command("install").description("install the skill/rules files + project MCP config into local agents (claude, cursor, windsurf, cline, agents, gemini, all)").option("--platform <names...>", "platform(s) to install for", ["claude"]).action(async (options) => {
4999
5156
  try {
5000
5157
  await runInstall(options.platform);
5001
5158
  } catch (error) {