mason-context 0.1.0 → 0.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 +70 -108
- package/dist/bin/mason-mcp.js +281 -136
- package/dist/bin/mason-mcp.js.map +1 -1
- package/dist/bin/mason.js +389 -169
- package/dist/bin/mason.js.map +1 -1
- package/dist/src/cli.js +389 -169
- package/dist/src/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/mason-mcp.js
CHANGED
|
@@ -1,4 +1,201 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/impact/impact.ts
|
|
13
|
+
var impact_exports = {};
|
|
14
|
+
__export(impact_exports, {
|
|
15
|
+
analyzeImpact: () => analyzeImpact
|
|
16
|
+
});
|
|
17
|
+
import fs5 from "fs/promises";
|
|
18
|
+
import path4 from "path";
|
|
19
|
+
import { execFile as execFile7 } from "child_process";
|
|
20
|
+
import { promisify as promisify7 } from "util";
|
|
21
|
+
import fg3 from "fast-glob";
|
|
22
|
+
async function analyzeImpact(rootDir, targetFiles) {
|
|
23
|
+
const resolvedRoot = path4.resolve(rootDir);
|
|
24
|
+
const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
|
|
25
|
+
const [cochange, references, tests] = await Promise.all([
|
|
26
|
+
getCochangeFiles(resolvedRoot, resolvedTargets),
|
|
27
|
+
getReferences(resolvedRoot, resolvedTargets),
|
|
28
|
+
getRelatedTests(resolvedRoot, resolvedTargets)
|
|
29
|
+
]);
|
|
30
|
+
return {
|
|
31
|
+
targetFiles: resolvedTargets,
|
|
32
|
+
cochange,
|
|
33
|
+
references,
|
|
34
|
+
tests
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async function resolveTargetFiles(rootDir, targets) {
|
|
38
|
+
const resolved = [];
|
|
39
|
+
for (const target of targets) {
|
|
40
|
+
if (target.includes("/")) {
|
|
41
|
+
resolved.push(target);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const matches = await fg3(`**/${target}`, {
|
|
45
|
+
cwd: rootDir,
|
|
46
|
+
ignore: IGNORE
|
|
47
|
+
});
|
|
48
|
+
if (matches.length > 0) {
|
|
49
|
+
resolved.push(matches[0]);
|
|
50
|
+
} else {
|
|
51
|
+
const noExt = target.replace(/\.[^.]+$/, "");
|
|
52
|
+
const extMatches = await fg3(`**/${noExt}.*`, {
|
|
53
|
+
cwd: rootDir,
|
|
54
|
+
ignore: IGNORE
|
|
55
|
+
});
|
|
56
|
+
if (extMatches.length > 0) {
|
|
57
|
+
resolved.push(extMatches[0]);
|
|
58
|
+
} else {
|
|
59
|
+
resolved.push(target);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return resolved;
|
|
64
|
+
}
|
|
65
|
+
async function getCochangeFiles(rootDir, targetFiles) {
|
|
66
|
+
const cochangeCounts = /* @__PURE__ */ new Map();
|
|
67
|
+
let totalTargetCommits = 0;
|
|
68
|
+
for (const targetFile of targetFiles) {
|
|
69
|
+
try {
|
|
70
|
+
const { stdout: commitLog } = await exec7(
|
|
71
|
+
"git",
|
|
72
|
+
["log", "--format=%H", "-n", "500", "--", targetFile],
|
|
73
|
+
{ cwd: rootDir, maxBuffer: 5e6 }
|
|
74
|
+
);
|
|
75
|
+
const commits = commitLog.trim().split("\n").filter(Boolean);
|
|
76
|
+
totalTargetCommits += commits.length;
|
|
77
|
+
if (commits.length === 0) continue;
|
|
78
|
+
for (const commit of commits) {
|
|
79
|
+
try {
|
|
80
|
+
const { stdout: filesInCommit } = await exec7(
|
|
81
|
+
"git",
|
|
82
|
+
["diff-tree", "--no-commit-id", "--name-only", "-r", commit],
|
|
83
|
+
{ cwd: rootDir }
|
|
84
|
+
);
|
|
85
|
+
const files = filesInCommit.trim().split("\n").filter(Boolean);
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
if (targetFiles.includes(file)) continue;
|
|
88
|
+
cochangeCounts.set(file, (cochangeCounts.get(file) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (totalTargetCommits === 0) return [];
|
|
97
|
+
return [...cochangeCounts.entries()].map(([file, count]) => ({
|
|
98
|
+
file,
|
|
99
|
+
cochangeRate: Math.round(count / totalTargetCommits * 100) / 100,
|
|
100
|
+
sharedCommits: count
|
|
101
|
+
})).filter((e) => e.cochangeRate >= 0.3 || e.sharedCommits >= 3).sort((a, b) => b.cochangeRate - a.cochangeRate).slice(0, 20);
|
|
102
|
+
}
|
|
103
|
+
async function getReferences(rootDir, targetFiles) {
|
|
104
|
+
const searchNames = /* @__PURE__ */ new Set();
|
|
105
|
+
for (const target of targetFiles) {
|
|
106
|
+
const basename = path4.basename(target).replace(/\.[^.]+$/, "");
|
|
107
|
+
searchNames.add(basename);
|
|
108
|
+
}
|
|
109
|
+
const allSourceFiles = await fg3(`**/${SOURCE_EXTENSIONS2}`, {
|
|
110
|
+
cwd: rootDir,
|
|
111
|
+
ignore: IGNORE
|
|
112
|
+
});
|
|
113
|
+
const targetSet = new Set(targetFiles);
|
|
114
|
+
const filesToSearch = allSourceFiles.filter((f) => !targetSet.has(f));
|
|
115
|
+
const results = /* @__PURE__ */ new Map();
|
|
116
|
+
const batchSize = 50;
|
|
117
|
+
for (let i = 0; i < filesToSearch.length; i += batchSize) {
|
|
118
|
+
const batch = filesToSearch.slice(i, i + batchSize);
|
|
119
|
+
await Promise.all(
|
|
120
|
+
batch.map(async (file) => {
|
|
121
|
+
try {
|
|
122
|
+
const content = await fs5.readFile(
|
|
123
|
+
path4.join(rootDir, file),
|
|
124
|
+
"utf-8"
|
|
125
|
+
);
|
|
126
|
+
for (const name of searchNames) {
|
|
127
|
+
const regex = new RegExp(`\\b${escapeRegex(name)}\\b`);
|
|
128
|
+
if (regex.test(content)) {
|
|
129
|
+
if (!results.has(file)) results.set(file, /* @__PURE__ */ new Set());
|
|
130
|
+
results.get(file).add(name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return [...results.entries()].map(([file, matches]) => ({
|
|
139
|
+
file,
|
|
140
|
+
matches: [...matches]
|
|
141
|
+
})).sort((a, b) => b.matches.length - a.matches.length);
|
|
142
|
+
}
|
|
143
|
+
async function getRelatedTests(rootDir, targetFiles) {
|
|
144
|
+
const testPatterns = [
|
|
145
|
+
"**/*.test.*",
|
|
146
|
+
"**/*.spec.*",
|
|
147
|
+
"**/*Test.kt",
|
|
148
|
+
"**/*Test.java",
|
|
149
|
+
"**/*Tests.kt",
|
|
150
|
+
"**/*Tests.java",
|
|
151
|
+
"**/test_*.py",
|
|
152
|
+
"**/*_test.py",
|
|
153
|
+
"**/*_test.go",
|
|
154
|
+
"**/*Tests.swift",
|
|
155
|
+
"**/*Test.swift",
|
|
156
|
+
"**/*_test.rs"
|
|
157
|
+
];
|
|
158
|
+
const testFiles = await fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
|
|
159
|
+
const results = [];
|
|
160
|
+
for (const target of targetFiles) {
|
|
161
|
+
const targetBaseName = path4.basename(target).replace(/\.[^.]+$/, "");
|
|
162
|
+
for (const testFile of testFiles) {
|
|
163
|
+
const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
|
|
164
|
+
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
165
|
+
if (sourceName === targetBaseName) {
|
|
166
|
+
results.push({
|
|
167
|
+
file: testFile,
|
|
168
|
+
confidence: "exact"
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
function escapeRegex(str) {
|
|
176
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
177
|
+
}
|
|
178
|
+
var exec7, IGNORE, SOURCE_EXTENSIONS2;
|
|
179
|
+
var init_impact = __esm({
|
|
180
|
+
"src/impact/impact.ts"() {
|
|
181
|
+
"use strict";
|
|
182
|
+
exec7 = promisify7(execFile7);
|
|
183
|
+
IGNORE = [
|
|
184
|
+
"**/node_modules/**",
|
|
185
|
+
"**/dist/**",
|
|
186
|
+
"**/build/**",
|
|
187
|
+
"**/.gradle/**",
|
|
188
|
+
"**/target/**",
|
|
189
|
+
"**/.git/**",
|
|
190
|
+
"**/vendor/**",
|
|
191
|
+
"**/__pycache__/**",
|
|
192
|
+
"**/venv/**",
|
|
193
|
+
"**/.venv/**",
|
|
194
|
+
"**/generated/**"
|
|
195
|
+
];
|
|
196
|
+
SOURCE_EXTENSIONS2 = "*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,h,dart,gradle.kts,gradle}";
|
|
197
|
+
}
|
|
198
|
+
});
|
|
2
199
|
|
|
3
200
|
// src/mcp/server.ts
|
|
4
201
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -6,11 +203,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
203
|
import { z } from "zod";
|
|
7
204
|
|
|
8
205
|
// src/mcp/tools.ts
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import { execFile as
|
|
12
|
-
import { promisify as
|
|
13
|
-
import
|
|
206
|
+
import fs6 from "fs/promises";
|
|
207
|
+
import path5 from "path";
|
|
208
|
+
import { execFile as execFile8 } from "child_process";
|
|
209
|
+
import { promisify as promisify8 } from "util";
|
|
210
|
+
import fg4 from "fast-glob";
|
|
14
211
|
|
|
15
212
|
// src/analyzers/git-history.ts
|
|
16
213
|
import { execFile } from "child_process";
|
|
@@ -398,6 +595,8 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
398
595
|
const ignorePatterns = [...IGNORE_PATTERNS, ...projectConfig.ignore ?? []];
|
|
399
596
|
for (const filePath of projectConfig.alwaysInclude ?? []) {
|
|
400
597
|
if (selected.size >= maxFiles) break;
|
|
598
|
+
const resolvedPath = path.resolve(rootDir, filePath);
|
|
599
|
+
if (!resolvedPath.startsWith(path.resolve(rootDir))) continue;
|
|
401
600
|
selected.set(filePath, "always-include (project config)");
|
|
402
601
|
}
|
|
403
602
|
let configCount = 0;
|
|
@@ -569,7 +768,9 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
569
768
|
const results = [];
|
|
570
769
|
for (const [filePath, reason] of selected) {
|
|
571
770
|
try {
|
|
572
|
-
const fullPath = path.
|
|
771
|
+
const fullPath = path.resolve(rootDir, filePath);
|
|
772
|
+
if (!fullPath.startsWith(path.resolve(rootDir))) continue;
|
|
773
|
+
if (isSensitiveFile(filePath)) continue;
|
|
573
774
|
const stat = await fs2.stat(fullPath);
|
|
574
775
|
if (stat.size > 1e5) continue;
|
|
575
776
|
const content = await fs2.readFile(fullPath, "utf-8");
|
|
@@ -587,19 +788,24 @@ async function sampleFiles(rootDir, maxFiles = 25) {
|
|
|
587
788
|
}
|
|
588
789
|
return results;
|
|
589
790
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
791
|
+
var SENSITIVE_PATTERNS = [
|
|
792
|
+
/^\.env$/,
|
|
793
|
+
/^\.env\./,
|
|
794
|
+
/\.pem$/,
|
|
795
|
+
/\.key$/,
|
|
796
|
+
/\.p12$/,
|
|
797
|
+
/\.pfx$/,
|
|
798
|
+
/\.jks$/,
|
|
799
|
+
/id_rsa/,
|
|
800
|
+
/id_ed25519/,
|
|
801
|
+
/credentials\./,
|
|
802
|
+
/secret/i,
|
|
803
|
+
/\.keystore$/,
|
|
804
|
+
/local\.properties$/
|
|
805
|
+
];
|
|
806
|
+
function isSensitiveFile(filePath) {
|
|
807
|
+
const basename = path.basename(filePath);
|
|
808
|
+
return SENSITIVE_PATTERNS.some((p) => p.test(basename));
|
|
603
809
|
}
|
|
604
810
|
|
|
605
811
|
// src/snapshot/snapshot.ts
|
|
@@ -663,8 +869,8 @@ async function getCurrentGitHash(rootDir) {
|
|
|
663
869
|
}
|
|
664
870
|
|
|
665
871
|
// src/mcp/tools.ts
|
|
666
|
-
var
|
|
667
|
-
var
|
|
872
|
+
var exec8 = promisify8(execFile8);
|
|
873
|
+
var IGNORE2 = [
|
|
668
874
|
"**/node_modules/**",
|
|
669
875
|
"**/dist/**",
|
|
670
876
|
"**/build/**",
|
|
@@ -685,7 +891,7 @@ async function buildContext(dir) {
|
|
|
685
891
|
};
|
|
686
892
|
}
|
|
687
893
|
async function analyzeProject(dir) {
|
|
688
|
-
const rootDir =
|
|
894
|
+
const rootDir = path5.resolve(dir);
|
|
689
895
|
const context = await buildContext(rootDir);
|
|
690
896
|
const results = await runAll(context);
|
|
691
897
|
const projectSnapshot = await detectProjectSnapshot(rootDir);
|
|
@@ -739,7 +945,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
739
945
|
const present = [];
|
|
740
946
|
for (const file of buildFiles) {
|
|
741
947
|
try {
|
|
742
|
-
await
|
|
948
|
+
await fs6.access(path5.join(rootDir, file));
|
|
743
949
|
present.push(file);
|
|
744
950
|
} catch {
|
|
745
951
|
}
|
|
@@ -757,9 +963,9 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
757
963
|
];
|
|
758
964
|
const testInfo = {};
|
|
759
965
|
for (const pattern of testDirs) {
|
|
760
|
-
const files = await
|
|
966
|
+
const files = await fg4(`${pattern}/**/*`, {
|
|
761
967
|
cwd: rootDir,
|
|
762
|
-
ignore:
|
|
968
|
+
ignore: IGNORE2,
|
|
763
969
|
onlyFiles: true
|
|
764
970
|
});
|
|
765
971
|
if (files.length > 0) {
|
|
@@ -777,18 +983,18 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
777
983
|
{ pattern: "**/*_test.rs", label: "*_test.rs" }
|
|
778
984
|
];
|
|
779
985
|
for (const { pattern, label } of testFilePatterns) {
|
|
780
|
-
const files = await
|
|
986
|
+
const files = await fg4(pattern, { cwd: rootDir, ignore: IGNORE2 });
|
|
781
987
|
if (files.length > 0) {
|
|
782
988
|
testInfo[label] = files.length;
|
|
783
989
|
}
|
|
784
990
|
}
|
|
785
|
-
const sourceFiles = await
|
|
991
|
+
const sourceFiles = await fg4("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
|
|
786
992
|
cwd: rootDir,
|
|
787
|
-
ignore:
|
|
993
|
+
ignore: IGNORE2
|
|
788
994
|
});
|
|
789
995
|
const fileCounts = {};
|
|
790
996
|
for (const file of sourceFiles) {
|
|
791
|
-
const ext =
|
|
997
|
+
const ext = path5.extname(file).slice(1);
|
|
792
998
|
fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
|
|
793
999
|
}
|
|
794
1000
|
return {
|
|
@@ -799,7 +1005,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
799
1005
|
};
|
|
800
1006
|
}
|
|
801
1007
|
async function getCodeSamples(dir, count = 15) {
|
|
802
|
-
const rootDir =
|
|
1008
|
+
const rootDir = path5.resolve(dir);
|
|
803
1009
|
const samples = await sampleFiles(rootDir, count);
|
|
804
1010
|
const output = {
|
|
805
1011
|
note: "These are previews (first ~60 lines). Use get_file_content to read the full file if needed.",
|
|
@@ -813,19 +1019,11 @@ async function getCodeSamples(dir, count = 15) {
|
|
|
813
1019
|
};
|
|
814
1020
|
return JSON.stringify(output, null, 2);
|
|
815
1021
|
}
|
|
816
|
-
async function getFileContent(dir, filePath) {
|
|
817
|
-
const rootDir = path4.resolve(dir);
|
|
818
|
-
const result = await readFullFile(rootDir, filePath);
|
|
819
|
-
if (!result) {
|
|
820
|
-
return JSON.stringify({ error: `Could not read file: ${filePath}` });
|
|
821
|
-
}
|
|
822
|
-
return JSON.stringify(result, null, 2);
|
|
823
|
-
}
|
|
824
1022
|
async function getProjectStructure(dir) {
|
|
825
|
-
const rootDir =
|
|
826
|
-
const allFiles = await
|
|
1023
|
+
const rootDir = path5.resolve(dir);
|
|
1024
|
+
const allFiles = await fg4("**/*", {
|
|
827
1025
|
cwd: rootDir,
|
|
828
|
-
ignore:
|
|
1026
|
+
ignore: IGNORE2,
|
|
829
1027
|
onlyFiles: true
|
|
830
1028
|
});
|
|
831
1029
|
const dirInfo = /* @__PURE__ */ new Map();
|
|
@@ -838,7 +1036,7 @@ async function getProjectStructure(dir) {
|
|
|
838
1036
|
}
|
|
839
1037
|
const info = dirInfo.get(dirPath);
|
|
840
1038
|
info.fileCount++;
|
|
841
|
-
const ext =
|
|
1039
|
+
const ext = path5.extname(file).slice(1);
|
|
842
1040
|
if (ext) {
|
|
843
1041
|
info.extensions.set(ext, (info.extensions.get(ext) ?? 0) + 1);
|
|
844
1042
|
}
|
|
@@ -860,7 +1058,7 @@ async function getProjectStructure(dir) {
|
|
|
860
1058
|
return JSON.stringify(output, null, 2);
|
|
861
1059
|
}
|
|
862
1060
|
async function getTestMap(dir) {
|
|
863
|
-
const rootDir =
|
|
1061
|
+
const rootDir = path5.resolve(dir);
|
|
864
1062
|
const testPatterns = [
|
|
865
1063
|
"**/*.test.*",
|
|
866
1064
|
"**/*.spec.*",
|
|
@@ -875,15 +1073,15 @@ async function getTestMap(dir) {
|
|
|
875
1073
|
"**/*Test.swift",
|
|
876
1074
|
"**/*_test.rs"
|
|
877
1075
|
];
|
|
878
|
-
const testFiles = await
|
|
879
|
-
const sourceFiles = await
|
|
1076
|
+
const testFiles = await fg4(testPatterns, { cwd: rootDir, ignore: IGNORE2 });
|
|
1077
|
+
const sourceFiles = await fg4(
|
|
880
1078
|
"**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}",
|
|
881
|
-
{ cwd: rootDir, ignore:
|
|
1079
|
+
{ cwd: rootDir, ignore: IGNORE2 }
|
|
882
1080
|
);
|
|
883
1081
|
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
884
1082
|
for (const file of sourceFiles) {
|
|
885
1083
|
if (testFiles.includes(file)) continue;
|
|
886
|
-
const baseName =
|
|
1084
|
+
const baseName = path5.basename(file).replace(/\.[^.]+$/, "");
|
|
887
1085
|
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
888
1086
|
existing.push(file);
|
|
889
1087
|
sourceByBaseName.set(baseName, existing);
|
|
@@ -891,7 +1089,7 @@ async function getTestMap(dir) {
|
|
|
891
1089
|
const pairs = [];
|
|
892
1090
|
const unmatched = [];
|
|
893
1091
|
for (const testFile of testFiles) {
|
|
894
|
-
const testBaseName =
|
|
1092
|
+
const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
|
|
895
1093
|
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
896
1094
|
if (!sourceName) {
|
|
897
1095
|
unmatched.push(testFile);
|
|
@@ -899,10 +1097,10 @@ async function getTestMap(dir) {
|
|
|
899
1097
|
}
|
|
900
1098
|
const candidates = sourceByBaseName.get(sourceName);
|
|
901
1099
|
if (candidates && candidates.length > 0) {
|
|
902
|
-
const testDir =
|
|
1100
|
+
const testDir = path5.dirname(testFile);
|
|
903
1101
|
const bestMatch = candidates.reduce((best, candidate) => {
|
|
904
|
-
const candidateDir =
|
|
905
|
-
const bestDir =
|
|
1102
|
+
const candidateDir = path5.dirname(candidate);
|
|
1103
|
+
const bestDir = path5.dirname(best);
|
|
906
1104
|
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
907
1105
|
const bestOverlap = commonSegments(testDir, bestDir);
|
|
908
1106
|
return candidateOverlap > bestOverlap ? candidate : best;
|
|
@@ -934,7 +1132,7 @@ function commonSegments(pathA, pathB) {
|
|
|
934
1132
|
return count;
|
|
935
1133
|
}
|
|
936
1134
|
async function getSnapshot(dir) {
|
|
937
|
-
const rootDir =
|
|
1135
|
+
const rootDir = path5.resolve(dir);
|
|
938
1136
|
const snapshot = await loadSnapshot(rootDir);
|
|
939
1137
|
if (!snapshot) {
|
|
940
1138
|
return JSON.stringify({
|
|
@@ -944,23 +1142,31 @@ async function getSnapshot(dir) {
|
|
|
944
1142
|
}
|
|
945
1143
|
const currentHash = await getCurrentGitHash(rootDir);
|
|
946
1144
|
const isStale = snapshot.gitHash !== currentHash && snapshot.gitHash !== "unknown";
|
|
1145
|
+
const seenFiles = /* @__PURE__ */ new Set();
|
|
1146
|
+
const compactFeatures = {};
|
|
1147
|
+
for (const [name, feat] of Object.entries(snapshot.features)) {
|
|
1148
|
+
const unique = feat.files.filter((f) => !seenFiles.has(f));
|
|
1149
|
+
if (unique.length === 0) continue;
|
|
1150
|
+
for (const f of unique) seenFiles.add(f);
|
|
1151
|
+
compactFeatures[name] = unique;
|
|
1152
|
+
}
|
|
1153
|
+
const compactFlows = {};
|
|
1154
|
+
for (const [name, flow] of Object.entries(snapshot.flows)) {
|
|
1155
|
+
compactFlows[name] = flow.chain;
|
|
1156
|
+
}
|
|
947
1157
|
const output = {
|
|
948
1158
|
exists: true,
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
featureCount: Object.keys(snapshot.features).length,
|
|
952
|
-
flowCount: Object.keys(snapshot.flows).length,
|
|
953
|
-
features: snapshot.features,
|
|
954
|
-
flows: snapshot.flows,
|
|
1159
|
+
features: compactFeatures,
|
|
1160
|
+
flows: compactFlows,
|
|
955
1161
|
stale: isStale
|
|
956
1162
|
};
|
|
957
1163
|
if (isStale) {
|
|
958
|
-
output.message = "Snapshot is behind HEAD.
|
|
1164
|
+
output.message = "Snapshot is behind HEAD. Run 'mason snapshot-update' or call save_snapshot to refresh.";
|
|
959
1165
|
}
|
|
960
|
-
return JSON.stringify(output
|
|
1166
|
+
return JSON.stringify(output);
|
|
961
1167
|
}
|
|
962
1168
|
async function fullAnalysis(dir) {
|
|
963
|
-
const rootDir =
|
|
1169
|
+
const rootDir = path5.resolve(dir);
|
|
964
1170
|
const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
|
|
965
1171
|
analyzeProject(dir),
|
|
966
1172
|
getProjectStructure(dir),
|
|
@@ -986,7 +1192,7 @@ async function fullAnalysis(dir) {
|
|
|
986
1192
|
return JSON.stringify(output, null, 2);
|
|
987
1193
|
}
|
|
988
1194
|
async function saveSnapshotData(dir, features, flows) {
|
|
989
|
-
const rootDir =
|
|
1195
|
+
const rootDir = path5.resolve(dir);
|
|
990
1196
|
const gitHash = await getCurrentGitHash(rootDir);
|
|
991
1197
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
992
1198
|
const existing = await loadSnapshot(rootDir);
|
|
@@ -1017,26 +1223,11 @@ async function saveSnapshotData(dir, features, flows) {
|
|
|
1017
1223
|
flows: Object.keys(flows).length
|
|
1018
1224
|
});
|
|
1019
1225
|
}
|
|
1020
|
-
async function
|
|
1021
|
-
const
|
|
1022
|
-
const
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
try {
|
|
1026
|
-
const raw = await fs5.readFile(configPath, "utf-8");
|
|
1027
|
-
existing = JSON.parse(raw);
|
|
1028
|
-
} catch {
|
|
1029
|
-
}
|
|
1030
|
-
if (config.patterns) existing.patterns = config.patterns;
|
|
1031
|
-
if (config.alwaysInclude) existing.alwaysInclude = config.alwaysInclude;
|
|
1032
|
-
if (config.ignore) existing.ignore = config.ignore;
|
|
1033
|
-
await fs5.mkdir(configDir, { recursive: true });
|
|
1034
|
-
await fs5.writeFile(configPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
1035
|
-
return JSON.stringify({
|
|
1036
|
-
status: "saved",
|
|
1037
|
-
path: configPath,
|
|
1038
|
-
config: existing
|
|
1039
|
-
});
|
|
1226
|
+
async function getImpact(dir, files) {
|
|
1227
|
+
const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
|
|
1228
|
+
const rootDir = path5.resolve(dir);
|
|
1229
|
+
const result = await analyzeImpact2(rootDir, files);
|
|
1230
|
+
return JSON.stringify(result, null, 2);
|
|
1040
1231
|
}
|
|
1041
1232
|
|
|
1042
1233
|
// src/mcp/server.ts
|
|
@@ -1047,12 +1238,12 @@ function createMcpServer() {
|
|
|
1047
1238
|
version: "0.1.0"
|
|
1048
1239
|
},
|
|
1049
1240
|
{
|
|
1050
|
-
instructions: "Mason is a context engineering tool. Always call get_snapshot before using Explore agents, Glob, or Grep to understand the codebase. The snapshot is a concept map that maps features and flows to their implementing files \u2014 it eliminates the need to search. This applies to ANY question about architecture, features, flows, how things work, cross-feature interactions, or bug investigation. Workflow: 1) Call get_snapshot first. 2) If no snapshot, call full_analysis
|
|
1241
|
+
instructions: "Mason is a context engineering tool. Always call get_snapshot before using Explore agents, Glob, or Grep to understand the codebase. The snapshot is a concept map that maps features and flows to their implementing files \u2014 it eliminates the need to search. This applies to ANY question about architecture, features, flows, how things work, cross-feature interactions, or bug investigation. Workflow: 1) Call get_snapshot first. 2) If no snapshot, call full_analysis and then save_snapshot to create one. 3) If the snapshot is stale, tell the user and offer to update it. 4) Use your native file reading tool to read files the snapshot points to. 5) Before modifying a file, call get_impact to check what else might be affected. 6) After making significant changes (new features, refactors, architecture changes), call save_snapshot to update the concept map."
|
|
1051
1242
|
}
|
|
1052
1243
|
);
|
|
1053
1244
|
server.tool(
|
|
1054
1245
|
"full_analysis",
|
|
1055
|
-
"Run a complete project analysis in one call. Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source file mapping. This is the recommended starting point \u2014 call this first, then
|
|
1246
|
+
"Run a complete project analysis in one call. Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source file mapping. This is the recommended starting point \u2014 call this first, then read specific files natively for full content.",
|
|
1056
1247
|
{
|
|
1057
1248
|
dir: z.string().describe("Absolute path to the project root directory")
|
|
1058
1249
|
},
|
|
@@ -1078,7 +1269,7 @@ function createMcpServer() {
|
|
|
1078
1269
|
);
|
|
1079
1270
|
server.tool(
|
|
1080
1271
|
"get_code_samples",
|
|
1081
|
-
"Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth.
|
|
1272
|
+
"Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth. Read files natively for full content.",
|
|
1082
1273
|
{
|
|
1083
1274
|
dir: z.string().describe("Absolute path to the project root directory"),
|
|
1084
1275
|
count: z.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
|
|
@@ -1090,46 +1281,6 @@ function createMcpServer() {
|
|
|
1090
1281
|
};
|
|
1091
1282
|
}
|
|
1092
1283
|
);
|
|
1093
|
-
server.tool(
|
|
1094
|
-
"get_file_content",
|
|
1095
|
-
"Read the full content of a specific file. Use this after get_code_samples to drill into files you want to understand fully.",
|
|
1096
|
-
{
|
|
1097
|
-
dir: z.string().describe("Absolute path to the project root directory"),
|
|
1098
|
-
file_path: z.string().describe("Relative path to the file within the project (e.g., 'src/main.ts')")
|
|
1099
|
-
},
|
|
1100
|
-
async ({ dir, file_path }) => {
|
|
1101
|
-
const result = await getFileContent(dir, file_path);
|
|
1102
|
-
return {
|
|
1103
|
-
content: [{ type: "text", text: result }]
|
|
1104
|
-
};
|
|
1105
|
-
}
|
|
1106
|
-
);
|
|
1107
|
-
server.tool(
|
|
1108
|
-
"get_project_structure",
|
|
1109
|
-
"Get the directory structure of a project with file counts and extension breakdown per directory. Shows top-level files and annotated directory listing up to 2 levels deep. Useful for understanding project layout before diving into code.",
|
|
1110
|
-
{
|
|
1111
|
-
dir: z.string().describe("Absolute path to the project root directory")
|
|
1112
|
-
},
|
|
1113
|
-
async ({ dir }) => {
|
|
1114
|
-
const result = await getProjectStructure(dir);
|
|
1115
|
-
return {
|
|
1116
|
-
content: [{ type: "text", text: result }]
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
);
|
|
1120
|
-
server.tool(
|
|
1121
|
-
"get_test_map",
|
|
1122
|
-
"Map test files to their corresponding source files by name matching. Shows which source files have tests and which don't. Useful for understanding test coverage patterns and test organization conventions.",
|
|
1123
|
-
{
|
|
1124
|
-
dir: z.string().describe("Absolute path to the project root directory")
|
|
1125
|
-
},
|
|
1126
|
-
async ({ dir }) => {
|
|
1127
|
-
const result = await getTestMap(dir);
|
|
1128
|
-
return {
|
|
1129
|
-
content: [{ type: "text", text: result }]
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
);
|
|
1133
1284
|
server.tool(
|
|
1134
1285
|
"get_snapshot",
|
|
1135
1286
|
"Get the project's concept map \u2014 a lookup table from features and flows to the files that implement them. Use this to jump straight to relevant files instead of exploring. Example: 'home screen' \u2192 [HomeScreen.kt, HomeViewModel.kt, HomeModule.kt]. If stale, run 'mason snapshot-update' to refresh.",
|
|
@@ -1170,20 +1321,14 @@ function createMcpServer() {
|
|
|
1170
1321
|
}
|
|
1171
1322
|
);
|
|
1172
1323
|
server.tool(
|
|
1173
|
-
"
|
|
1174
|
-
"
|
|
1324
|
+
"get_impact",
|
|
1325
|
+
"Analyze the impact of changing specific files. Returns three signals: git co-change (files that historically change together), references (files that mention the target by name), and related tests. Use this before editing a file to understand what else might need updating.",
|
|
1175
1326
|
{
|
|
1176
1327
|
dir: z.string().describe("Absolute path to the project root directory"),
|
|
1177
|
-
|
|
1178
|
-
alwaysInclude: z.array(z.string()).optional().describe("Specific file paths to always include in samples (e.g., 'src/core/config.ts')"),
|
|
1179
|
-
ignore: z.array(z.string()).optional().describe("Additional glob patterns to ignore (e.g., '**/fixtures/**')")
|
|
1328
|
+
files: z.array(z.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
|
|
1180
1329
|
},
|
|
1181
|
-
async ({ dir,
|
|
1182
|
-
const result = await
|
|
1183
|
-
patterns,
|
|
1184
|
-
alwaysInclude,
|
|
1185
|
-
ignore
|
|
1186
|
-
});
|
|
1330
|
+
async ({ dir, files }) => {
|
|
1331
|
+
const result = await getImpact(dir, files);
|
|
1187
1332
|
return {
|
|
1188
1333
|
content: [{ type: "text", text: result }]
|
|
1189
1334
|
};
|