@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 +18 -2
- package/dist/{chunk-PXVI2L45.js → chunk-NS5HB65S.js} +116 -42
- package/dist/chunk-NS5HB65S.js.map +1 -0
- package/dist/{chunk-YT7B6DOD.js → chunk-ZK3TA6FW.js} +33 -7
- package/dist/chunk-ZK3TA6FW.js.map +1 -0
- package/dist/cli/index.cjs +212 -55
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +67 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +147 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +2 -2
- package/dist/mcp/server.cjs +26 -5
- package/dist/mcp/server.cjs.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/package.json +1 -1
- package/src/skill/SKILL.md +9 -4
- package/dist/chunk-PXVI2L45.js.map +0 -1
- package/dist/chunk-YT7B6DOD.js.map +0 -1
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
|
|
83
|
-
# agents (AGENTS.md: Codex/opencode/Amp),
|
|
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-
|
|
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 (
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
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
|
-
|
|
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(
|
|
1677
|
+
functionNodeMap.set(core.id, id);
|
|
1669
1678
|
continue;
|
|
1670
1679
|
}
|
|
1671
|
-
if (
|
|
1672
|
-
const callee =
|
|
1673
|
-
const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(
|
|
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(
|
|
2104
|
+
function uniqueMatch(candidates, real) {
|
|
2081
2105
|
const matches = /* @__PURE__ */ new Set();
|
|
2082
2106
|
for (const candidate of candidates) {
|
|
2083
|
-
if (
|
|
2107
|
+
if (real.has(candidate)) matches.add(candidate);
|
|
2084
2108
|
}
|
|
2085
|
-
|
|
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
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
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
|
-
|
|
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(
|
|
2160
|
+
resolved = uniqueMatch(candidatePaths(id), realFiles);
|
|
2135
2161
|
}
|
|
2136
2162
|
if (resolved !== null) remap.set(id, resolved);
|
|
2137
2163
|
return resolved;
|
|
2138
2164
|
};
|
|
2139
|
-
|
|
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
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
edge.
|
|
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-
|
|
3123
|
+
//# sourceMappingURL=chunk-NS5HB65S.js.map
|