mason-context 0.1.0 → 0.2.1

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/dist/bin/mason.js CHANGED
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
4
10
  var __esm = (fn, res) => function __init() {
5
11
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
12
  };
@@ -372,48 +378,44 @@ function formatPromptForCopy(system, userMessage) {
372
378
 
373
379
  ${userMessage}`;
374
380
  }
381
+ function spawnWithStdin(command, args, input) {
382
+ const { spawn } = __require("child_process");
383
+ return new Promise((resolve, reject) => {
384
+ const proc = spawn(command, args, {
385
+ stdio: ["pipe", "pipe", "pipe"],
386
+ timeout: 3e5
387
+ });
388
+ let stdout = "";
389
+ let stderr = "";
390
+ proc.stdout.on("data", (data) => {
391
+ stdout += data.toString();
392
+ });
393
+ proc.stderr.on("data", (data) => {
394
+ stderr += data.toString();
395
+ });
396
+ proc.on("close", (code) => {
397
+ if (code === 0) {
398
+ resolve(stdout.trim());
399
+ } else {
400
+ reject(new Error(`${command} exited with code ${code}: ${stderr}`));
401
+ }
402
+ });
403
+ proc.on("error", reject);
404
+ proc.stdin.write(input);
405
+ proc.stdin.end();
406
+ });
407
+ }
375
408
  async function callClaudeCLI(system, userMessage) {
376
- const fs7 = await import("fs/promises");
377
- const os2 = await import("os");
378
- const path6 = await import("path");
379
409
  const prompt = `${system}
380
410
 
381
411
  ${userMessage}`;
382
- const tmpFile = path6.join(os2.tmpdir(), `mason-prompt-${Date.now()}.txt`);
383
- try {
384
- await fs7.writeFile(tmpFile, prompt, "utf-8");
385
- const promptContent = await fs7.readFile(tmpFile, "utf-8");
386
- const { stdout } = await exec4(
387
- "sh",
388
- ["-c", `cat "${tmpFile}" | claude -p`],
389
- { maxBuffer: 1e7, timeout: 3e5 }
390
- );
391
- return stdout.trim();
392
- } finally {
393
- await fs7.unlink(tmpFile).catch(() => {
394
- });
395
- }
412
+ return spawnWithStdin("claude", ["-p"], prompt);
396
413
  }
397
414
  async function callGeminiCLI(system, userMessage) {
398
- const fs7 = await import("fs/promises");
399
- const os2 = await import("os");
400
- const path6 = await import("path");
401
415
  const prompt = `${system}
402
416
 
403
417
  ${userMessage}`;
404
- const tmpFile = path6.join(os2.tmpdir(), `mason-prompt-${Date.now()}.txt`);
405
- try {
406
- await fs7.writeFile(tmpFile, prompt, "utf-8");
407
- const { stdout } = await exec4(
408
- "sh",
409
- ["-c", `cat "${tmpFile}" | gemini -p ""`],
410
- { maxBuffer: 1e7, timeout: 3e5 }
411
- );
412
- return stdout.trim();
413
- } finally {
414
- await fs7.unlink(tmpFile).catch(() => {
415
- });
416
- }
418
+ return spawnWithStdin("gemini", ["-p", ""], prompt);
417
419
  }
418
420
  async function callOllamaCLI(host, model, system, userMessage) {
419
421
  const response = await fetch(`${host}/api/chat`, {
@@ -524,6 +526,8 @@ async function sampleFiles(rootDir, maxFiles = 25) {
524
526
  const ignorePatterns = [...IGNORE_PATTERNS, ...projectConfig.ignore ?? []];
525
527
  for (const filePath of projectConfig.alwaysInclude ?? []) {
526
528
  if (selected.size >= maxFiles) break;
529
+ const resolvedPath = path2.resolve(rootDir, filePath);
530
+ if (!resolvedPath.startsWith(path2.resolve(rootDir))) continue;
527
531
  selected.set(filePath, "always-include (project config)");
528
532
  }
529
533
  let configCount = 0;
@@ -695,7 +699,9 @@ async function sampleFiles(rootDir, maxFiles = 25) {
695
699
  const results = [];
696
700
  for (const [filePath, reason] of selected) {
697
701
  try {
698
- const fullPath = path2.join(rootDir, filePath);
702
+ const fullPath = path2.resolve(rootDir, filePath);
703
+ if (!fullPath.startsWith(path2.resolve(rootDir))) continue;
704
+ if (isSensitiveFile(filePath)) continue;
699
705
  const stat = await fs3.stat(fullPath);
700
706
  if (stat.size > 1e5) continue;
701
707
  const content = await fs3.readFile(fullPath, "utf-8");
@@ -713,10 +719,15 @@ async function sampleFiles(rootDir, maxFiles = 25) {
713
719
  }
714
720
  return results;
715
721
  }
722
+ function isSensitiveFile(filePath) {
723
+ const basename = path2.basename(filePath);
724
+ return SENSITIVE_PATTERNS.some((p) => p.test(basename));
725
+ }
716
726
  async function readFullFile(rootDir, filePath) {
717
727
  try {
718
728
  const fullPath = path2.join(path2.resolve(rootDir), filePath);
719
729
  if (!fullPath.startsWith(path2.resolve(rootDir))) return null;
730
+ if (isSensitiveFile(filePath)) return null;
720
731
  const content = await fs3.readFile(fullPath, "utf-8");
721
732
  return {
722
733
  path: filePath,
@@ -727,7 +738,7 @@ async function readFullFile(rootDir, filePath) {
727
738
  return null;
728
739
  }
729
740
  }
730
- var exec5, SOURCE_EXTENSIONS, CONFIG_FILES, ENTRY_POINT_PATTERNS, ARCHITECTURAL_PATTERNS, IGNORE_PATTERNS, PREVIEW_LINES;
741
+ var exec5, SOURCE_EXTENSIONS, CONFIG_FILES, ENTRY_POINT_PATTERNS, ARCHITECTURAL_PATTERNS, IGNORE_PATTERNS, PREVIEW_LINES, SENSITIVE_PATTERNS;
731
742
  var init_sampler = __esm({
732
743
  "src/mcp/sampler.ts"() {
733
744
  "use strict";
@@ -873,6 +884,21 @@ var init_sampler = __esm({
873
884
  "**/BuildConfig.java"
874
885
  ];
875
886
  PREVIEW_LINES = 60;
887
+ SENSITIVE_PATTERNS = [
888
+ /^\.env$/,
889
+ /^\.env\./,
890
+ /\.pem$/,
891
+ /\.key$/,
892
+ /\.p12$/,
893
+ /\.pfx$/,
894
+ /\.jks$/,
895
+ /id_rsa/,
896
+ /id_ed25519/,
897
+ /credentials\./,
898
+ /secret/i,
899
+ /\.keystore$/,
900
+ /local\.properties$/
901
+ ];
876
902
  }
877
903
  });
878
904
 
@@ -1182,12 +1208,200 @@ var init_snapshot = __esm({
1182
1208
  }
1183
1209
  });
1184
1210
 
1185
- // src/mcp/tools.ts
1211
+ // src/impact/impact.ts
1212
+ var impact_exports = {};
1213
+ __export(impact_exports, {
1214
+ analyzeImpact: () => analyzeImpact
1215
+ });
1186
1216
  import fs5 from "fs/promises";
1187
1217
  import path4 from "path";
1188
1218
  import { execFile as execFile7 } from "child_process";
1189
1219
  import { promisify as promisify7 } from "util";
1190
1220
  import fg3 from "fast-glob";
1221
+ async function analyzeImpact(rootDir, targetFiles) {
1222
+ const resolvedRoot = path4.resolve(rootDir);
1223
+ const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
1224
+ const [cochange, references, tests] = await Promise.all([
1225
+ getCochangeFiles(resolvedRoot, resolvedTargets),
1226
+ getReferences(resolvedRoot, resolvedTargets),
1227
+ getRelatedTests(resolvedRoot, resolvedTargets)
1228
+ ]);
1229
+ return {
1230
+ targetFiles: resolvedTargets,
1231
+ cochange,
1232
+ references,
1233
+ tests
1234
+ };
1235
+ }
1236
+ async function resolveTargetFiles(rootDir, targets) {
1237
+ const resolved = [];
1238
+ for (const target of targets) {
1239
+ if (target.includes("/")) {
1240
+ resolved.push(target);
1241
+ continue;
1242
+ }
1243
+ const matches = await fg3(`**/${target}`, {
1244
+ cwd: rootDir,
1245
+ ignore: IGNORE
1246
+ });
1247
+ if (matches.length > 0) {
1248
+ resolved.push(matches[0]);
1249
+ } else {
1250
+ const noExt = target.replace(/\.[^.]+$/, "");
1251
+ const extMatches = await fg3(`**/${noExt}.*`, {
1252
+ cwd: rootDir,
1253
+ ignore: IGNORE
1254
+ });
1255
+ if (extMatches.length > 0) {
1256
+ resolved.push(extMatches[0]);
1257
+ } else {
1258
+ resolved.push(target);
1259
+ }
1260
+ }
1261
+ }
1262
+ return resolved;
1263
+ }
1264
+ async function getCochangeFiles(rootDir, targetFiles) {
1265
+ const cochangeCounts = /* @__PURE__ */ new Map();
1266
+ let totalTargetCommits = 0;
1267
+ for (const targetFile of targetFiles) {
1268
+ try {
1269
+ const { stdout: commitLog } = await exec7(
1270
+ "git",
1271
+ ["log", "--format=%H", "-n", "500", "--", targetFile],
1272
+ { cwd: rootDir, maxBuffer: 5e6 }
1273
+ );
1274
+ const commits = commitLog.trim().split("\n").filter(Boolean);
1275
+ totalTargetCommits += commits.length;
1276
+ if (commits.length === 0) continue;
1277
+ for (const commit of commits) {
1278
+ try {
1279
+ const { stdout: filesInCommit } = await exec7(
1280
+ "git",
1281
+ ["diff-tree", "--no-commit-id", "--name-only", "-r", commit],
1282
+ { cwd: rootDir }
1283
+ );
1284
+ const files = filesInCommit.trim().split("\n").filter(Boolean);
1285
+ for (const file of files) {
1286
+ if (targetFiles.includes(file)) continue;
1287
+ cochangeCounts.set(file, (cochangeCounts.get(file) ?? 0) + 1);
1288
+ }
1289
+ } catch {
1290
+ }
1291
+ }
1292
+ } catch {
1293
+ }
1294
+ }
1295
+ if (totalTargetCommits === 0) return [];
1296
+ return [...cochangeCounts.entries()].map(([file, count]) => ({
1297
+ file,
1298
+ cochangeRate: Math.round(count / totalTargetCommits * 100) / 100,
1299
+ sharedCommits: count
1300
+ })).filter((e) => e.cochangeRate >= 0.3 || e.sharedCommits >= 3).sort((a, b) => b.cochangeRate - a.cochangeRate).slice(0, 20);
1301
+ }
1302
+ async function getReferences(rootDir, targetFiles) {
1303
+ const searchNames = /* @__PURE__ */ new Set();
1304
+ for (const target of targetFiles) {
1305
+ const basename = path4.basename(target).replace(/\.[^.]+$/, "");
1306
+ searchNames.add(basename);
1307
+ }
1308
+ const allSourceFiles = await fg3(`**/${SOURCE_EXTENSIONS2}`, {
1309
+ cwd: rootDir,
1310
+ ignore: IGNORE
1311
+ });
1312
+ const targetSet = new Set(targetFiles);
1313
+ const filesToSearch = allSourceFiles.filter((f) => !targetSet.has(f));
1314
+ const results = /* @__PURE__ */ new Map();
1315
+ const batchSize = 50;
1316
+ for (let i = 0; i < filesToSearch.length; i += batchSize) {
1317
+ const batch = filesToSearch.slice(i, i + batchSize);
1318
+ await Promise.all(
1319
+ batch.map(async (file) => {
1320
+ try {
1321
+ const content = await fs5.readFile(
1322
+ path4.join(rootDir, file),
1323
+ "utf-8"
1324
+ );
1325
+ for (const name of searchNames) {
1326
+ const regex = new RegExp(`\\b${escapeRegex(name)}\\b`);
1327
+ if (regex.test(content)) {
1328
+ if (!results.has(file)) results.set(file, /* @__PURE__ */ new Set());
1329
+ results.get(file).add(name);
1330
+ }
1331
+ }
1332
+ } catch {
1333
+ }
1334
+ })
1335
+ );
1336
+ }
1337
+ return [...results.entries()].map(([file, matches]) => ({
1338
+ file,
1339
+ matches: [...matches]
1340
+ })).sort((a, b) => b.matches.length - a.matches.length);
1341
+ }
1342
+ async function getRelatedTests(rootDir, targetFiles) {
1343
+ const testPatterns = [
1344
+ "**/*.test.*",
1345
+ "**/*.spec.*",
1346
+ "**/*Test.kt",
1347
+ "**/*Test.java",
1348
+ "**/*Tests.kt",
1349
+ "**/*Tests.java",
1350
+ "**/test_*.py",
1351
+ "**/*_test.py",
1352
+ "**/*_test.go",
1353
+ "**/*Tests.swift",
1354
+ "**/*Test.swift",
1355
+ "**/*_test.rs"
1356
+ ];
1357
+ const testFiles = await fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
1358
+ const results = [];
1359
+ for (const target of targetFiles) {
1360
+ const targetBaseName = path4.basename(target).replace(/\.[^.]+$/, "");
1361
+ for (const testFile of testFiles) {
1362
+ const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
1363
+ const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
1364
+ if (sourceName === targetBaseName) {
1365
+ results.push({
1366
+ file: testFile,
1367
+ confidence: "exact"
1368
+ });
1369
+ }
1370
+ }
1371
+ }
1372
+ return results;
1373
+ }
1374
+ function escapeRegex(str) {
1375
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1376
+ }
1377
+ var exec7, IGNORE, SOURCE_EXTENSIONS2;
1378
+ var init_impact = __esm({
1379
+ "src/impact/impact.ts"() {
1380
+ "use strict";
1381
+ exec7 = promisify7(execFile7);
1382
+ IGNORE = [
1383
+ "**/node_modules/**",
1384
+ "**/dist/**",
1385
+ "**/build/**",
1386
+ "**/.gradle/**",
1387
+ "**/target/**",
1388
+ "**/.git/**",
1389
+ "**/vendor/**",
1390
+ "**/__pycache__/**",
1391
+ "**/venv/**",
1392
+ "**/.venv/**",
1393
+ "**/generated/**"
1394
+ ];
1395
+ SOURCE_EXTENSIONS2 = "*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,h,dart,gradle.kts,gradle}";
1396
+ }
1397
+ });
1398
+
1399
+ // src/mcp/tools.ts
1400
+ import fs6 from "fs/promises";
1401
+ import path5 from "path";
1402
+ import { execFile as execFile8 } from "child_process";
1403
+ import { promisify as promisify8 } from "util";
1404
+ import fg4 from "fast-glob";
1191
1405
  async function buildContext(dir) {
1192
1406
  return {
1193
1407
  rootDir: dir,
@@ -1195,7 +1409,7 @@ async function buildContext(dir) {
1195
1409
  };
1196
1410
  }
1197
1411
  async function analyzeProject(dir) {
1198
- const rootDir = path4.resolve(dir);
1412
+ const rootDir = path5.resolve(dir);
1199
1413
  const context = await buildContext(rootDir);
1200
1414
  const results = await runAll(context);
1201
1415
  const projectSnapshot = await detectProjectSnapshot(rootDir);
@@ -1249,7 +1463,7 @@ async function detectProjectSnapshot(rootDir) {
1249
1463
  const present = [];
1250
1464
  for (const file of buildFiles) {
1251
1465
  try {
1252
- await fs5.access(path4.join(rootDir, file));
1466
+ await fs6.access(path5.join(rootDir, file));
1253
1467
  present.push(file);
1254
1468
  } catch {
1255
1469
  }
@@ -1267,9 +1481,9 @@ async function detectProjectSnapshot(rootDir) {
1267
1481
  ];
1268
1482
  const testInfo = {};
1269
1483
  for (const pattern of testDirs) {
1270
- const files = await fg3(`${pattern}/**/*`, {
1484
+ const files = await fg4(`${pattern}/**/*`, {
1271
1485
  cwd: rootDir,
1272
- ignore: IGNORE,
1486
+ ignore: IGNORE2,
1273
1487
  onlyFiles: true
1274
1488
  });
1275
1489
  if (files.length > 0) {
@@ -1287,18 +1501,18 @@ async function detectProjectSnapshot(rootDir) {
1287
1501
  { pattern: "**/*_test.rs", label: "*_test.rs" }
1288
1502
  ];
1289
1503
  for (const { pattern, label } of testFilePatterns) {
1290
- const files = await fg3(pattern, { cwd: rootDir, ignore: IGNORE });
1504
+ const files = await fg4(pattern, { cwd: rootDir, ignore: IGNORE2 });
1291
1505
  if (files.length > 0) {
1292
1506
  testInfo[label] = files.length;
1293
1507
  }
1294
1508
  }
1295
- const sourceFiles = await fg3("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
1509
+ const sourceFiles = await fg4("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
1296
1510
  cwd: rootDir,
1297
- ignore: IGNORE
1511
+ ignore: IGNORE2
1298
1512
  });
1299
1513
  const fileCounts = {};
1300
1514
  for (const file of sourceFiles) {
1301
- const ext = path4.extname(file).slice(1);
1515
+ const ext = path5.extname(file).slice(1);
1302
1516
  fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
1303
1517
  }
1304
1518
  return {
@@ -1309,7 +1523,7 @@ async function detectProjectSnapshot(rootDir) {
1309
1523
  };
1310
1524
  }
1311
1525
  async function getCodeSamples(dir, count = 15) {
1312
- const rootDir = path4.resolve(dir);
1526
+ const rootDir = path5.resolve(dir);
1313
1527
  const samples = await sampleFiles(rootDir, count);
1314
1528
  const output = {
1315
1529
  note: "These are previews (first ~60 lines). Use get_file_content to read the full file if needed.",
@@ -1323,19 +1537,11 @@ async function getCodeSamples(dir, count = 15) {
1323
1537
  };
1324
1538
  return JSON.stringify(output, null, 2);
1325
1539
  }
1326
- async function getFileContent(dir, filePath) {
1327
- const rootDir = path4.resolve(dir);
1328
- const result = await readFullFile(rootDir, filePath);
1329
- if (!result) {
1330
- return JSON.stringify({ error: `Could not read file: ${filePath}` });
1331
- }
1332
- return JSON.stringify(result, null, 2);
1333
- }
1334
1540
  async function getProjectStructure(dir) {
1335
- const rootDir = path4.resolve(dir);
1336
- const allFiles = await fg3("**/*", {
1541
+ const rootDir = path5.resolve(dir);
1542
+ const allFiles = await fg4("**/*", {
1337
1543
  cwd: rootDir,
1338
- ignore: IGNORE,
1544
+ ignore: IGNORE2,
1339
1545
  onlyFiles: true
1340
1546
  });
1341
1547
  const dirInfo = /* @__PURE__ */ new Map();
@@ -1348,7 +1554,7 @@ async function getProjectStructure(dir) {
1348
1554
  }
1349
1555
  const info2 = dirInfo.get(dirPath);
1350
1556
  info2.fileCount++;
1351
- const ext = path4.extname(file).slice(1);
1557
+ const ext = path5.extname(file).slice(1);
1352
1558
  if (ext) {
1353
1559
  info2.extensions.set(ext, (info2.extensions.get(ext) ?? 0) + 1);
1354
1560
  }
@@ -1370,7 +1576,7 @@ async function getProjectStructure(dir) {
1370
1576
  return JSON.stringify(output, null, 2);
1371
1577
  }
1372
1578
  async function getTestMap(dir) {
1373
- const rootDir = path4.resolve(dir);
1579
+ const rootDir = path5.resolve(dir);
1374
1580
  const testPatterns = [
1375
1581
  "**/*.test.*",
1376
1582
  "**/*.spec.*",
@@ -1385,15 +1591,15 @@ async function getTestMap(dir) {
1385
1591
  "**/*Test.swift",
1386
1592
  "**/*_test.rs"
1387
1593
  ];
1388
- const testFiles = await fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
1389
- const sourceFiles = await fg3(
1594
+ const testFiles = await fg4(testPatterns, { cwd: rootDir, ignore: IGNORE2 });
1595
+ const sourceFiles = await fg4(
1390
1596
  "**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}",
1391
- { cwd: rootDir, ignore: IGNORE }
1597
+ { cwd: rootDir, ignore: IGNORE2 }
1392
1598
  );
1393
1599
  const sourceByBaseName = /* @__PURE__ */ new Map();
1394
1600
  for (const file of sourceFiles) {
1395
1601
  if (testFiles.includes(file)) continue;
1396
- const baseName = path4.basename(file).replace(/\.[^.]+$/, "");
1602
+ const baseName = path5.basename(file).replace(/\.[^.]+$/, "");
1397
1603
  const existing = sourceByBaseName.get(baseName) ?? [];
1398
1604
  existing.push(file);
1399
1605
  sourceByBaseName.set(baseName, existing);
@@ -1401,7 +1607,7 @@ async function getTestMap(dir) {
1401
1607
  const pairs = [];
1402
1608
  const unmatched = [];
1403
1609
  for (const testFile of testFiles) {
1404
- const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
1610
+ const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
1405
1611
  const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
1406
1612
  if (!sourceName) {
1407
1613
  unmatched.push(testFile);
@@ -1409,10 +1615,10 @@ async function getTestMap(dir) {
1409
1615
  }
1410
1616
  const candidates = sourceByBaseName.get(sourceName);
1411
1617
  if (candidates && candidates.length > 0) {
1412
- const testDir = path4.dirname(testFile);
1618
+ const testDir = path5.dirname(testFile);
1413
1619
  const bestMatch = candidates.reduce((best, candidate) => {
1414
- const candidateDir = path4.dirname(candidate);
1415
- const bestDir = path4.dirname(best);
1620
+ const candidateDir = path5.dirname(candidate);
1621
+ const bestDir = path5.dirname(best);
1416
1622
  const candidateOverlap = commonSegments(testDir, candidateDir);
1417
1623
  const bestOverlap = commonSegments(testDir, bestDir);
1418
1624
  return candidateOverlap > bestOverlap ? candidate : best;
@@ -1444,7 +1650,7 @@ function commonSegments(pathA, pathB) {
1444
1650
  return count;
1445
1651
  }
1446
1652
  async function getSnapshot(dir) {
1447
- const rootDir = path4.resolve(dir);
1653
+ const rootDir = path5.resolve(dir);
1448
1654
  const snapshot = await loadSnapshot(rootDir);
1449
1655
  if (!snapshot) {
1450
1656
  return JSON.stringify({
@@ -1454,23 +1660,31 @@ async function getSnapshot(dir) {
1454
1660
  }
1455
1661
  const currentHash = await getCurrentGitHash(rootDir);
1456
1662
  const isStale = snapshot.gitHash !== currentHash && snapshot.gitHash !== "unknown";
1663
+ const seenFiles = /* @__PURE__ */ new Set();
1664
+ const compactFeatures = {};
1665
+ for (const [name, feat] of Object.entries(snapshot.features)) {
1666
+ const unique = feat.files.filter((f) => !seenFiles.has(f));
1667
+ if (unique.length === 0) continue;
1668
+ for (const f of unique) seenFiles.add(f);
1669
+ compactFeatures[name] = unique;
1670
+ }
1671
+ const compactFlows = {};
1672
+ for (const [name, flow] of Object.entries(snapshot.flows)) {
1673
+ compactFlows[name] = flow.chain;
1674
+ }
1457
1675
  const output = {
1458
1676
  exists: true,
1459
- createdAt: snapshot.createdAt,
1460
- updatedAt: snapshot.updatedAt,
1461
- featureCount: Object.keys(snapshot.features).length,
1462
- flowCount: Object.keys(snapshot.flows).length,
1463
- features: snapshot.features,
1464
- flows: snapshot.flows,
1677
+ features: compactFeatures,
1678
+ flows: compactFlows,
1465
1679
  stale: isStale
1466
1680
  };
1467
1681
  if (isStale) {
1468
- output.message = "Snapshot is behind HEAD. Some features/flows may reference changed files. Run 'mason snapshot-update' or call save_snapshot to refresh.";
1682
+ output.message = "Snapshot is behind HEAD. Run 'mason snapshot-update' or call save_snapshot to refresh.";
1469
1683
  }
1470
- return JSON.stringify(output, null, 2);
1684
+ return JSON.stringify(output);
1471
1685
  }
1472
1686
  async function fullAnalysis(dir) {
1473
- const rootDir = path4.resolve(dir);
1687
+ const rootDir = path5.resolve(dir);
1474
1688
  const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
1475
1689
  analyzeProject(dir),
1476
1690
  getProjectStructure(dir),
@@ -1496,7 +1710,7 @@ async function fullAnalysis(dir) {
1496
1710
  return JSON.stringify(output, null, 2);
1497
1711
  }
1498
1712
  async function saveSnapshotData(dir, features, flows) {
1499
- const rootDir = path4.resolve(dir);
1713
+ const rootDir = path5.resolve(dir);
1500
1714
  const gitHash = await getCurrentGitHash(rootDir);
1501
1715
  const now = (/* @__PURE__ */ new Date()).toISOString();
1502
1716
  const existing = await loadSnapshot(rootDir);
@@ -1527,28 +1741,13 @@ async function saveSnapshotData(dir, features, flows) {
1527
1741
  flows: Object.keys(flows).length
1528
1742
  });
1529
1743
  }
1530
- async function configureProject(dir, config) {
1531
- const rootDir = path4.resolve(dir);
1532
- const configDir = path4.join(rootDir, ".mason");
1533
- const configPath = path4.join(configDir, "config.json");
1534
- let existing = {};
1535
- try {
1536
- const raw = await fs5.readFile(configPath, "utf-8");
1537
- existing = JSON.parse(raw);
1538
- } catch {
1539
- }
1540
- if (config.patterns) existing.patterns = config.patterns;
1541
- if (config.alwaysInclude) existing.alwaysInclude = config.alwaysInclude;
1542
- if (config.ignore) existing.ignore = config.ignore;
1543
- await fs5.mkdir(configDir, { recursive: true });
1544
- await fs5.writeFile(configPath, JSON.stringify(existing, null, 2), "utf-8");
1545
- return JSON.stringify({
1546
- status: "saved",
1547
- path: configPath,
1548
- config: existing
1549
- });
1744
+ async function getImpact(dir, files) {
1745
+ const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
1746
+ const rootDir = path5.resolve(dir);
1747
+ const result = await analyzeImpact2(rootDir, files);
1748
+ return JSON.stringify(result, null, 2);
1550
1749
  }
1551
- var exec7, IGNORE;
1750
+ var exec8, IGNORE2;
1552
1751
  var init_tools = __esm({
1553
1752
  "src/mcp/tools.ts"() {
1554
1753
  "use strict";
@@ -1556,8 +1755,8 @@ var init_tools = __esm({
1556
1755
  init_git();
1557
1756
  init_sampler();
1558
1757
  init_snapshot();
1559
- exec7 = promisify7(execFile7);
1560
- IGNORE = [
1758
+ exec8 = promisify8(execFile8);
1759
+ IGNORE2 = [
1561
1760
  "**/node_modules/**",
1562
1761
  "**/dist/**",
1563
1762
  "**/build/**",
@@ -1587,15 +1786,15 @@ function createMcpServer() {
1587
1786
  const server = new McpServer(
1588
1787
  {
1589
1788
  name: "mason",
1590
- version: "0.1.0"
1789
+ version: "0.2.1"
1591
1790
  },
1592
1791
  {
1593
- 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."
1792
+ 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."
1594
1793
  }
1595
1794
  );
1596
1795
  server.tool(
1597
1796
  "full_analysis",
1598
- "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.",
1797
+ "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.",
1599
1798
  {
1600
1799
  dir: z.string().describe("Absolute path to the project root directory")
1601
1800
  },
@@ -1621,7 +1820,7 @@ function createMcpServer() {
1621
1820
  );
1622
1821
  server.tool(
1623
1822
  "get_code_samples",
1624
- "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.",
1823
+ "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.",
1625
1824
  {
1626
1825
  dir: z.string().describe("Absolute path to the project root directory"),
1627
1826
  count: z.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
@@ -1633,46 +1832,6 @@ function createMcpServer() {
1633
1832
  };
1634
1833
  }
1635
1834
  );
1636
- server.tool(
1637
- "get_file_content",
1638
- "Read the full content of a specific file. Use this after get_code_samples to drill into files you want to understand fully.",
1639
- {
1640
- dir: z.string().describe("Absolute path to the project root directory"),
1641
- file_path: z.string().describe("Relative path to the file within the project (e.g., 'src/main.ts')")
1642
- },
1643
- async ({ dir, file_path }) => {
1644
- const result = await getFileContent(dir, file_path);
1645
- return {
1646
- content: [{ type: "text", text: result }]
1647
- };
1648
- }
1649
- );
1650
- server.tool(
1651
- "get_project_structure",
1652
- "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.",
1653
- {
1654
- dir: z.string().describe("Absolute path to the project root directory")
1655
- },
1656
- async ({ dir }) => {
1657
- const result = await getProjectStructure(dir);
1658
- return {
1659
- content: [{ type: "text", text: result }]
1660
- };
1661
- }
1662
- );
1663
- server.tool(
1664
- "get_test_map",
1665
- "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.",
1666
- {
1667
- dir: z.string().describe("Absolute path to the project root directory")
1668
- },
1669
- async ({ dir }) => {
1670
- const result = await getTestMap(dir);
1671
- return {
1672
- content: [{ type: "text", text: result }]
1673
- };
1674
- }
1675
- );
1676
1835
  server.tool(
1677
1836
  "get_snapshot",
1678
1837
  "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.",
@@ -1713,20 +1872,14 @@ function createMcpServer() {
1713
1872
  }
1714
1873
  );
1715
1874
  server.tool(
1716
- "configure_project",
1717
- "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.",
1875
+ "get_impact",
1876
+ "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.",
1718
1877
  {
1719
1878
  dir: z.string().describe("Absolute path to the project root directory"),
1720
- patterns: z.array(z.string()).optional().describe("Custom glob patterns for architecturally important files (e.g., '**/*Gateway.*', '**/*Bloc.*')"),
1721
- alwaysInclude: z.array(z.string()).optional().describe("Specific file paths to always include in samples (e.g., 'src/core/config.ts')"),
1722
- ignore: z.array(z.string()).optional().describe("Additional glob patterns to ignore (e.g., '**/fixtures/**')")
1879
+ files: z.array(z.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
1723
1880
  },
1724
- async ({ dir, patterns, alwaysInclude, ignore }) => {
1725
- const result = await configureProject(dir, {
1726
- patterns,
1727
- alwaysInclude,
1728
- ignore
1729
- });
1881
+ async ({ dir, files }) => {
1882
+ const result = await getImpact(dir, files);
1730
1883
  return {
1731
1884
  content: [{ type: "text", text: result }]
1732
1885
  };
@@ -1753,8 +1906,8 @@ init_config();
1753
1906
  init_providers();
1754
1907
  init_tools();
1755
1908
  import { Command } from "commander";
1756
- import fs6 from "fs/promises";
1757
- import path5 from "path";
1909
+ import fs7 from "fs/promises";
1910
+ import path6 from "path";
1758
1911
  import ora from "ora";
1759
1912
  import chalk2 from "chalk";
1760
1913
 
@@ -1819,11 +1972,11 @@ function createCLI() {
1819
1972
  "Context engineering CLI & MCP server \u2014 generates intelligent CLAUDE.md files"
1820
1973
  ).version("0.1.0");
1821
1974
  program2.command("setup").description("Register Mason as an MCP server with Claude Code").option("--scope <scope>", "Config scope: user or project", "user").action(async (opts) => {
1822
- const { execFile: execFile8 } = await import("child_process");
1823
- const { promisify: promisify8 } = await import("util");
1824
- const exec8 = promisify8(execFile8);
1975
+ const { execFile: execFile9 } = await import("child_process");
1976
+ const { promisify: promisify9 } = await import("util");
1977
+ const exec9 = promisify9(execFile9);
1825
1978
  try {
1826
- await exec8("claude", ["--version"]);
1979
+ await exec9("claude", ["--version"]);
1827
1980
  } catch {
1828
1981
  error(
1829
1982
  "Claude Code CLI not found. Install it from https://claude.ai/code"
@@ -1842,7 +1995,7 @@ function createCLI() {
1842
1995
  "mason-ai",
1843
1996
  "mcp"
1844
1997
  ];
1845
- await exec8("claude", args);
1998
+ await exec9("claude", args);
1846
1999
  success("Mason registered with Claude Code.");
1847
2000
  info("Restart Claude Code to start using Mason's tools.");
1848
2001
  } catch (err) {
@@ -1897,7 +2050,7 @@ function createCLI() {
1897
2050
  );
1898
2051
  process.exit(1);
1899
2052
  }
1900
- const rootDir = path5.resolve(dir);
2053
+ const rootDir = path6.resolve(dir);
1901
2054
  const runConfig = opts.model ? { ...config, model: opts.model } : config;
1902
2055
  const spinner = ora("Analyzing codebase...").start();
1903
2056
  const analysisData = await fullAnalysis(rootDir);
@@ -1927,11 +2080,40 @@ ${analysisData}`
1927
2080
  error("LLM returned empty response.");
1928
2081
  process.exit(1);
1929
2082
  }
1930
- const claudeDir = path5.join(rootDir, ".claude");
1931
- await fs6.mkdir(claudeDir, { recursive: true });
1932
- const outPath = path5.join(claudeDir, "CLAUDE.md");
1933
- await fs6.writeFile(outPath, markdown, "utf-8");
2083
+ const claudeDir = path6.join(rootDir, ".claude");
2084
+ await fs7.mkdir(claudeDir, { recursive: true });
2085
+ const outPath = path6.join(claudeDir, "CLAUDE.md");
2086
+ await fs7.writeFile(outPath, markdown, "utf-8");
1934
2087
  success(`Generated ${outPath}`);
2088
+ try {
2089
+ const { createSnapshot: createSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
2090
+ const spinner2 = ora("Building concept map...").start();
2091
+ const snapshot = await createSnapshot2(rootDir, runConfig);
2092
+ spinner2.stop();
2093
+ const featureCount = Object.keys(snapshot.features).length;
2094
+ const flowCount = Object.keys(snapshot.flows).length;
2095
+ success(
2096
+ `Concept map created: ${featureCount} features, ${flowCount} flows`
2097
+ );
2098
+ } catch {
2099
+ }
2100
+ const hookPath = path6.join(rootDir, ".git", "hooks", "post-commit");
2101
+ try {
2102
+ const hookContent = await fs7.readFile(hookPath, "utf-8");
2103
+ if (!hookContent.includes("mason snapshot-update")) {
2104
+ console.log(
2105
+ chalk2.gray(
2106
+ '\nTip: Run "mason snapshot --install-hook" to keep the concept map updated automatically.'
2107
+ )
2108
+ );
2109
+ }
2110
+ } catch {
2111
+ console.log(
2112
+ chalk2.gray(
2113
+ '\nTip: Run "mason snapshot --install-hook" to keep the concept map updated automatically.'
2114
+ )
2115
+ );
2116
+ }
1935
2117
  } catch (err) {
1936
2118
  spinner.stop();
1937
2119
  error(
@@ -1945,7 +2127,7 @@ ${analysisData}`
1945
2127
  createSnapshot: createSnapshot2,
1946
2128
  installHook: installHook2
1947
2129
  } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
1948
- const rootDir = path5.resolve(dir);
2130
+ const rootDir = path6.resolve(dir);
1949
2131
  if (opts.installHook) {
1950
2132
  try {
1951
2133
  await installHook2(rootDir);
@@ -1983,7 +2165,7 @@ ${analysisData}`
1983
2165
  });
1984
2166
  program2.command("snapshot-update").description("Incrementally update snapshot with recent changes").argument("[dir]", "Directory to update", ".").action(async (dir) => {
1985
2167
  const { updateSnapshot: updateSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
1986
- const rootDir = path5.resolve(dir);
2168
+ const rootDir = path6.resolve(dir);
1987
2169
  const config = await loadConfig();
1988
2170
  if (!config) return;
1989
2171
  try {
@@ -1995,8 +2177,46 @@ ${analysisData}`
1995
2177
  } catch {
1996
2178
  }
1997
2179
  });
2180
+ program2.command("impact").description("Show files affected by changes to a given file").argument("<files...>", "File paths or names to analyze").option("-d, --dir <dir>", "Project directory", ".").action(async (files, opts) => {
2181
+ const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
2182
+ const rootDir = path6.resolve(opts.dir);
2183
+ const spinner = ora("Analyzing impact...").start();
2184
+ const result = await analyzeImpact2(rootDir, files);
2185
+ spinner.stop();
2186
+ console.log(chalk2.bold(`
2187
+ Impact analysis for: ${result.targetFiles.join(", ")}
2188
+ `));
2189
+ if (result.cochange.length > 0) {
2190
+ console.log(chalk2.bold(" Co-change (files that historically change together):"));
2191
+ for (const entry of result.cochange) {
2192
+ const rate = chalk2.gray(`${Math.round(entry.cochangeRate * 100)}%`);
2193
+ console.log(` ${rate} ${entry.file} ${chalk2.gray(`(${entry.sharedCommits} shared commits)`)}`);
2194
+ }
2195
+ console.log();
2196
+ }
2197
+ if (result.references.length > 0) {
2198
+ console.log(chalk2.bold(" References (files that mention the target):"));
2199
+ for (const entry of result.references) {
2200
+ console.log(` ${entry.file} ${chalk2.gray(`[${entry.matches.join(", ")}]`)}`);
2201
+ }
2202
+ console.log();
2203
+ }
2204
+ if (result.tests.length > 0) {
2205
+ console.log(chalk2.bold(" Related tests:"));
2206
+ for (const entry of result.tests) {
2207
+ console.log(` ${entry.file} ${chalk2.gray(`(${entry.confidence})`)}`);
2208
+ }
2209
+ console.log();
2210
+ }
2211
+ const total = result.cochange.length + result.references.length + result.tests.length;
2212
+ if (total === 0) {
2213
+ info("No impact detected \u2014 this file may be independent.");
2214
+ } else {
2215
+ console.log(chalk2.bold(` ${total} related file(s) found`));
2216
+ }
2217
+ });
1998
2218
  program2.command("analyze").description("Analyze the codebase and print findings").argument("[dir]", "Directory to analyze", ".").action(async (dir) => {
1999
- const rootDir = path5.resolve(dir);
2219
+ const rootDir = path6.resolve(dir);
2000
2220
  const spinner = ora("Analyzing codebase...").start();
2001
2221
  const context = await buildContext2(rootDir);
2002
2222
  const results = await runAll(context);