@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/dist/cli/index.cjs
CHANGED
|
@@ -332,7 +332,10 @@ function scoreNodes(graph, query) {
|
|
|
332
332
|
}
|
|
333
333
|
if (score > 0) matches.push({ id, label, score });
|
|
334
334
|
});
|
|
335
|
-
|
|
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 (!
|
|
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 {
|
|
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
|
|
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 (
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
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
|
-
|
|
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(
|
|
3307
|
+
functionNodeMap.set(core.id, id);
|
|
3273
3308
|
continue;
|
|
3274
3309
|
}
|
|
3275
|
-
if (
|
|
3276
|
-
const callee =
|
|
3277
|
-
const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(
|
|
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(
|
|
3714
|
+
function uniqueMatch(candidates, real) {
|
|
3665
3715
|
const matches = /* @__PURE__ */ new Set();
|
|
3666
3716
|
for (const candidate of candidates) {
|
|
3667
|
-
if (
|
|
3717
|
+
if (real.has(candidate)) matches.add(candidate);
|
|
3668
3718
|
}
|
|
3669
|
-
|
|
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
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
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
|
-
|
|
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(
|
|
3770
|
+
resolved = uniqueMatch(candidatePaths(id), realFiles);
|
|
3719
3771
|
}
|
|
3720
3772
|
if (resolved !== null) remap.set(id, resolved);
|
|
3721
3773
|
return resolved;
|
|
3722
3774
|
};
|
|
3723
|
-
|
|
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
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
}
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
edge.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|