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.
@@ -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 fs5 from "fs/promises";
10
- import path4 from "path";
11
- import { execFile as execFile7 } from "child_process";
12
- import { promisify as promisify7 } from "util";
13
- import fg3 from "fast-glob";
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.join(rootDir, filePath);
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
- async function readFullFile(rootDir, filePath) {
591
- try {
592
- const fullPath = path.join(path.resolve(rootDir), filePath);
593
- if (!fullPath.startsWith(path.resolve(rootDir))) return null;
594
- const content = await fs2.readFile(fullPath, "utf-8");
595
- return {
596
- path: filePath,
597
- content,
598
- totalLines: content.split("\n").length
599
- };
600
- } catch {
601
- return null;
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 exec7 = promisify7(execFile7);
667
- var IGNORE = [
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 = path4.resolve(dir);
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 fs5.access(path4.join(rootDir, file));
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 fg3(`${pattern}/**/*`, {
966
+ const files = await fg4(`${pattern}/**/*`, {
761
967
  cwd: rootDir,
762
- ignore: 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 fg3(pattern, { cwd: rootDir, ignore: IGNORE });
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 fg3("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
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: IGNORE
993
+ ignore: IGNORE2
788
994
  });
789
995
  const fileCounts = {};
790
996
  for (const file of sourceFiles) {
791
- const ext = path4.extname(file).slice(1);
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 = path4.resolve(dir);
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 = path4.resolve(dir);
826
- const allFiles = await fg3("**/*", {
1023
+ const rootDir = path5.resolve(dir);
1024
+ const allFiles = await fg4("**/*", {
827
1025
  cwd: rootDir,
828
- ignore: 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 = path4.extname(file).slice(1);
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 = path4.resolve(dir);
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 fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
879
- const sourceFiles = await fg3(
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: 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 = path4.basename(file).replace(/\.[^.]+$/, "");
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 = path4.basename(testFile).replace(/\.[^.]+$/, "");
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 = path4.dirname(testFile);
1100
+ const testDir = path5.dirname(testFile);
903
1101
  const bestMatch = candidates.reduce((best, candidate) => {
904
- const candidateDir = path4.dirname(candidate);
905
- const bestDir = path4.dirname(best);
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 = path4.resolve(dir);
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
- createdAt: snapshot.createdAt,
950
- updatedAt: snapshot.updatedAt,
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. Some features/flows may reference changed files. Run 'mason snapshot-update' or call save_snapshot to refresh.";
1164
+ output.message = "Snapshot is behind HEAD. Run 'mason snapshot-update' or call save_snapshot to refresh.";
959
1165
  }
960
- return JSON.stringify(output, null, 2);
1166
+ return JSON.stringify(output);
961
1167
  }
962
1168
  async function fullAnalysis(dir) {
963
- const rootDir = path4.resolve(dir);
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 = path4.resolve(dir);
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 configureProject(dir, config) {
1021
- const rootDir = path4.resolve(dir);
1022
- const configDir = path4.join(rootDir, ".mason");
1023
- const configPath = path4.join(configDir, "config.json");
1024
- let existing = {};
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 instead. 3) Use get_file_content to read the files the snapshot points to. 4) Call save_snapshot to persist your understanding for future sessions. 5) Call write_claude_md for documentation."
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 use get_file_content to read specific files in full.",
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. Use get_file_content to read the full content of any file that looks interesting.",
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
- "configure_project",
1174
- "Configure Mason for this project. Add custom file patterns to sample, files to always include, or paths to ignore. Saved to .mason/config.json. Use this when the default architectural patterns miss important files in the project.",
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
- patterns: z.array(z.string()).optional().describe("Custom glob patterns for architecturally important files (e.g., '**/*Gateway.*', '**/*Bloc.*')"),
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, patterns, alwaysInclude, ignore }) => {
1182
- const result = await configureProject(dir, {
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
  };