@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.
package/README.md CHANGED
@@ -31,6 +31,14 @@ npm install -g @dreamtree-org/graphify
31
31
  npx @dreamtree-org/graphify .
32
32
  ```
33
33
 
34
+ A default run does more than build the graph — it also makes it instantly usable by the AI
35
+ agents your project already has: it installs the graphify skill/rules files and registers the
36
+ project-scoped MCP server. Claude Code is always set up (`.claude/skills/graphify/SKILL.md` +
37
+ `.mcp.json`); Cursor, Windsurf, Cline, AGENTS.md hosts, and Gemini CLI are set up when their
38
+ config dir/file is detected in the project. All writes are idempotent and never touch your
39
+ existing config (`.mcp.json` entries for other servers, user content in `AGENTS.md`, ...).
40
+ Opt out with `--no-install`.
41
+
34
42
  ## Build the graph
35
43
 
36
44
  ```bash
@@ -43,6 +51,7 @@ graphify . --leiden # Leiden instead of Louvain community detection
43
51
  graphify . --watch # rebuild on file change
44
52
  graphify . --mysql <dsn> # also extract a MySQL schema (mysql://user:pass@host:port/db) into the same graph
45
53
  graphify . --mcp # start the MCP stdio server instead of running the pipeline
54
+ graphify . --no-install # skip the automatic agent skill + MCP config install
46
55
  ```
47
56
 
48
57
  Outputs land in `graphify-out/`: `graph.json` (GraphRAG-ready), `graph.html` (interactive,
@@ -79,8 +88,10 @@ graphify save-result --question Q --answer A --nodes id... --outcome useful|dead
79
88
  graphify reflect # aggregate results into graphify-out/reflections/LESSONS.md
80
89
  # (recency-weighted; agents read it at session start)
81
90
  graphify hook install # auto-rebuild (incremental) after every commit and pull
82
- graphify install --platform all # install the agent skill/rules: claude, cursor, windsurf, cline,
83
- # agents (AGENTS.md: Codex/opencode/Amp), gemini
91
+ graphify install --platform all # install the agent skill/rules + project MCP config: claude,
92
+ # cursor, windsurf, cline, agents (AGENTS.md: Codex/opencode/Amp),
93
+ # gemini — a plain `graphify .` run already does this for
94
+ # detected hosts
84
95
  ```
85
96
 
86
97
  With hooks installed the MCP server always serves a fresh graph — it re-reads `graph.json`
@@ -102,6 +113,11 @@ so repos that use the same packages are automatically bridged in the merged grap
102
113
  `graphify --mcp` (or the `graphify-mcp` binary) exposes `context`, `query`, `path`,
103
114
  `explain`, `affected`, and `tests` as MCP tools over stdio. No network listener.
104
115
 
116
+ You normally never start it by hand: the default pipeline run registers it in the project's
117
+ MCP config (`.mcp.json` for Claude Code, `.cursor/mcp.json` for Cursor, `.gemini/settings.json`
118
+ for Gemini CLI) as `npx -y @dreamtree-org/graphify --mcp`, so the host launches it on demand —
119
+ and teammates who pull the repo get it with zero setup.
120
+
105
121
  ## Guardrails file
106
122
 
107
123
  `graphify.rules.json` at the repo root (this repo dogfoods its own):
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  affectedBy,
3
3
  testsForNode
4
- } from "./chunk-YT7B6DOD.js";
4
+ } from "./chunk-ZK3TA6FW.js";
5
5
  import {
6
6
  escapeHtml,
7
7
  sanitizeLabel,
@@ -1609,13 +1609,11 @@ async function extractTypeScript(filePath, displayPath) {
1609
1609
  });
1610
1610
  }
1611
1611
  function importTargetId(binding, property) {
1612
- if (!binding.ref.external) {
1613
- if (binding.kind === "named" && binding.importedName) {
1614
- return entityId(binding.ref.id, binding.importedName);
1615
- }
1616
- if (binding.kind === "namespace" && property) {
1617
- return entityId(binding.ref.id, property);
1618
- }
1612
+ if (binding.kind === "named" && binding.importedName) {
1613
+ return entityId(binding.ref.id, binding.importedName);
1614
+ }
1615
+ if (binding.kind === "namespace" && property) {
1616
+ return entityId(binding.ref.id, property);
1619
1617
  }
1620
1618
  return binding.ref.id;
1621
1619
  }
@@ -1653,24 +1651,35 @@ async function extractTypeScript(filePath, displayPath) {
1653
1651
  }
1654
1652
  }
1655
1653
  }
1654
+ function unwrapExpression(node) {
1655
+ let current = node;
1656
+ while (current.type === "as_expression" || current.type === "satisfies_expression" || current.type === "non_null_expression" || current.type === "parenthesized_expression") {
1657
+ const inner = namedChildren(current)[0];
1658
+ if (!inner) break;
1659
+ current = inner;
1660
+ }
1661
+ return current;
1662
+ }
1656
1663
  function handleTopLevelVariableFunctions(node) {
1664
+ const isExported = node.parent?.type === "export_statement";
1657
1665
  for (const declarator of namedChildren(node)) {
1658
1666
  if (declarator.type !== "variable_declarator") continue;
1659
1667
  const value = declarator.childForFieldName("value");
1660
1668
  if (!value) continue;
1661
- if (value.type === "arrow_function" || value.type === "function_expression") {
1669
+ const core = unwrapExpression(value);
1670
+ if (core.type === "arrow_function" || core.type === "function_expression") {
1662
1671
  const name = declarator.childForFieldName("name")?.text;
1663
1672
  if (!name) continue;
1664
1673
  const id = entityId(sourceFile, name);
1665
1674
  builder.addNode({ id, label: `function ${name}`, sourceFile, sourceLocation: lineOf(declarator) });
1666
1675
  builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
1667
1676
  nameIndex.set(name, id);
1668
- functionNodeMap.set(value.id, id);
1677
+ functionNodeMap.set(core.id, id);
1669
1678
  continue;
1670
1679
  }
1671
- if (value.type === "call_expression") {
1672
- const callee = value.childForFieldName("function");
1673
- const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(value) : null;
1680
+ if (core.type === "call_expression") {
1681
+ const callee = core.childForFieldName("function");
1682
+ const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(core) : null;
1674
1683
  if (specifier) {
1675
1684
  const nameNode = declarator.childForFieldName("name");
1676
1685
  if (nameNode) {
@@ -1678,6 +1687,21 @@ async function extractTypeScript(filePath, displayPath) {
1678
1687
  addModuleNode(ref);
1679
1688
  registerRequireBinding(nameNode, ref);
1680
1689
  }
1690
+ continue;
1691
+ }
1692
+ }
1693
+ if (isExported) {
1694
+ const name = declarator.childForFieldName("name")?.text;
1695
+ if (!name) continue;
1696
+ const id = entityId(sourceFile, name);
1697
+ builder.addNode({ id, label: `const ${name}`, sourceFile, sourceLocation: lineOf(declarator) });
1698
+ builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
1699
+ nameIndex.set(name, id);
1700
+ if (core.type === "identifier") {
1701
+ const aliased = nameIndex.get(core.text);
1702
+ if (aliased !== void 0 && aliased !== id) {
1703
+ builder.addEdge({ source: id, target: aliased, relation: "references", confidence: "EXTRACTED" });
1704
+ }
1681
1705
  }
1682
1706
  }
1683
1707
  }
@@ -2077,13 +2101,12 @@ function candidatePaths(guessed) {
2077
2101
  }
2078
2102
  return candidates.filter((c) => c !== guessed);
2079
2103
  }
2080
- function uniqueMatch(guessed, candidates, realIds) {
2104
+ function uniqueMatch(candidates, real) {
2081
2105
  const matches = /* @__PURE__ */ new Set();
2082
2106
  for (const candidate of candidates) {
2083
- if (realIds.has(candidate)) matches.add(candidate);
2107
+ if (real.has(candidate)) matches.add(candidate);
2084
2108
  }
2085
- if (matches.size !== 1) return null;
2086
- return [...matches][0];
2109
+ return matches.size === 1 ? [...matches][0] : null;
2087
2110
  }
2088
2111
  function resolveCrossFileReferences(extractions, options = {}) {
2089
2112
  const selfNames = options.selfNames ?? [];
@@ -2104,20 +2127,24 @@ function resolveCrossFileReferences(extractions, options = {}) {
2104
2127
  }
2105
2128
  }
2106
2129
  const remap = /* @__PURE__ */ new Map();
2107
- const resolveId = (id) => {
2108
- if (id.startsWith("module:") && selfNames.length > 0) {
2109
- const cachedSelf = remap.get(id);
2110
- if (cachedSelf !== void 0) return cachedSelf;
2111
- const candidates = selfImportCandidates(id.slice("module:".length), selfNames);
2112
- if (candidates !== null) {
2113
- const resolved2 = uniqueMatch(id, candidates, realFiles);
2114
- if (resolved2 !== null) {
2115
- remap.set(id, resolved2);
2116
- return resolved2;
2130
+ let resolvedEndpoints = 0;
2131
+ const applyRemap = (resolveId) => {
2132
+ for (const extraction of extractions) {
2133
+ for (const edge of extraction.edges) {
2134
+ const source = resolveId(edge.source);
2135
+ if (source !== null) {
2136
+ edge.source = source;
2137
+ resolvedEndpoints++;
2138
+ }
2139
+ const target = resolveId(edge.target);
2140
+ if (target !== null) {
2141
+ edge.target = target;
2142
+ resolvedEndpoints++;
2117
2143
  }
2118
2144
  }
2119
- return null;
2120
2145
  }
2146
+ };
2147
+ const resolvePathId = (id) => {
2121
2148
  if (isOpaque(id)) return null;
2122
2149
  const cached = remap.get(id);
2123
2150
  if (cached !== void 0) return cached;
@@ -2127,30 +2154,77 @@ function resolveCrossFileReferences(extractions, options = {}) {
2127
2154
  if (realIds.has(id)) return null;
2128
2155
  const guessedPath = id.slice(0, sep2);
2129
2156
  const name = id.slice(sep2);
2130
- const candidates = candidatePaths(guessedPath).map((p) => `${p}${name}`);
2131
- resolved = uniqueMatch(id, candidates, realIds);
2157
+ resolved = uniqueMatch(candidatePaths(guessedPath).map((p) => `${p}${name}`), realIds);
2132
2158
  } else {
2133
2159
  if (realFiles.has(id)) return null;
2134
- resolved = uniqueMatch(id, candidatePaths(id), realFiles);
2160
+ resolved = uniqueMatch(candidatePaths(id), realFiles);
2135
2161
  }
2136
2162
  if (resolved !== null) remap.set(id, resolved);
2137
2163
  return resolved;
2138
2164
  };
2139
- let resolvedEndpoints = 0;
2165
+ applyRemap(resolvePathId);
2166
+ const namedAlias = /* @__PURE__ */ new Map();
2167
+ const starTargets = /* @__PURE__ */ new Map();
2140
2168
  for (const extraction of extractions) {
2141
2169
  for (const edge of extraction.edges) {
2142
- const source = resolveId(edge.source);
2143
- if (source !== null) {
2144
- edge.source = source;
2145
- resolvedEndpoints++;
2146
- }
2147
- const target = resolveId(edge.target);
2148
- if (target !== null) {
2149
- edge.target = target;
2150
- resolvedEndpoints++;
2170
+ if (edge.relation !== "re_exports" || !realFiles.has(edge.source)) continue;
2171
+ const sep2 = edge.target.lastIndexOf("::");
2172
+ if (sep2 > 0 && realIds.has(edge.target)) {
2173
+ namedAlias.set(`${edge.source}|${edge.target.slice(sep2 + 2)}`, edge.target);
2174
+ } else if (sep2 < 0 && realFiles.has(edge.target)) {
2175
+ const targets = starTargets.get(edge.source) ?? [];
2176
+ targets.push(edge.target);
2177
+ starTargets.set(edge.source, targets);
2151
2178
  }
2152
2179
  }
2153
2180
  }
2181
+ const throughBarrel = (file, name) => {
2182
+ const direct = `${file}::${name}`;
2183
+ if (realIds.has(direct)) return direct;
2184
+ const aliased = namedAlias.get(`${file}|${name}`);
2185
+ if (aliased !== void 0) return aliased;
2186
+ const viaStar = (starTargets.get(file) ?? []).map((sf) => `${sf}::${name}`).filter((id) => realIds.has(id));
2187
+ return viaStar.length === 1 ? viaStar[0] : null;
2188
+ };
2189
+ const resolveBarrelId = (id) => {
2190
+ const cached = remap.get(id);
2191
+ if (cached !== void 0) return cached;
2192
+ if (id.startsWith("module:")) {
2193
+ const spec = id.slice("module:".length);
2194
+ const sep3 = spec.indexOf("::");
2195
+ const moduleSpec = sep3 > 0 ? spec.slice(0, sep3) : spec;
2196
+ const name = sep3 > 0 ? spec.slice(sep3 + 2) : null;
2197
+ const candidates = selfImportCandidates(moduleSpec, selfNames);
2198
+ if (candidates === null) {
2199
+ if (name === null) return null;
2200
+ remap.set(id, `module:${moduleSpec}`);
2201
+ return `module:${moduleSpec}`;
2202
+ }
2203
+ const file = uniqueMatch(candidates, realFiles);
2204
+ let resolved = null;
2205
+ if (file !== null) {
2206
+ resolved = name !== null ? throughBarrel(file, name) ?? file : file;
2207
+ } else if (name !== null) {
2208
+ resolved = `module:${moduleSpec}`;
2209
+ }
2210
+ if (resolved !== null) remap.set(id, resolved);
2211
+ return resolved;
2212
+ }
2213
+ const sep2 = id.indexOf("::");
2214
+ if (sep2 > 0 && !realIds.has(id)) {
2215
+ const guessedPath = id.slice(0, sep2);
2216
+ const name = id.slice(sep2 + 2);
2217
+ const files = [guessedPath, ...candidatePaths(guessedPath)].filter((f) => realFiles.has(f));
2218
+ const resolvedSet = new Set(files.map((f) => throughBarrel(f, name)).filter((r) => r !== null));
2219
+ if (resolvedSet.size === 1) {
2220
+ const resolved = [...resolvedSet][0];
2221
+ remap.set(id, resolved);
2222
+ return resolved;
2223
+ }
2224
+ }
2225
+ return null;
2226
+ };
2227
+ applyRemap(resolveBarrelId);
2154
2228
  let droppedPlaceholders = 0;
2155
2229
  for (const extraction of extractions) {
2156
2230
  const before = extraction.nodes.length;
@@ -3046,4 +3120,4 @@ export {
3046
3120
  validateRules,
3047
3121
  checkRules
3048
3122
  };
3049
- //# sourceMappingURL=chunk-PXVI2L45.js.map
3123
+ //# sourceMappingURL=chunk-NS5HB65S.js.map