mason-context 0.2.2 → 0.3.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/dist/bin/mason-mcp.js +151 -117
- package/dist/bin/mason-mcp.js.map +1 -1
- package/dist/bin/mason.js +177 -140
- package/dist/bin/mason.js.map +1 -1
- package/dist/src/cli.js +177 -140
- package/dist/src/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/mason.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
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
|
-
});
|
|
10
4
|
var __esm = (fn, res) => function __init() {
|
|
11
5
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
6
|
};
|
|
@@ -320,7 +314,7 @@ var init_config = __esm({
|
|
|
320
314
|
});
|
|
321
315
|
|
|
322
316
|
// src/llm/providers.ts
|
|
323
|
-
import { execFile as execFile4 } from "child_process";
|
|
317
|
+
import { execFile as execFile4, spawn } from "child_process";
|
|
324
318
|
import { promisify as promisify4 } from "util";
|
|
325
319
|
async function callLLM(config, userMessage, systemPrompt) {
|
|
326
320
|
const model = config.model ?? getDefaultModel(config.provider);
|
|
@@ -379,7 +373,6 @@ function formatPromptForCopy(system, userMessage) {
|
|
|
379
373
|
${userMessage}`;
|
|
380
374
|
}
|
|
381
375
|
function spawnWithStdin(command, args, input) {
|
|
382
|
-
const { spawn } = __require("child_process");
|
|
383
376
|
return new Promise((resolve, reject) => {
|
|
384
377
|
const proc = spawn(command, args, {
|
|
385
378
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -902,15 +895,121 @@ var init_sampler = __esm({
|
|
|
902
895
|
}
|
|
903
896
|
});
|
|
904
897
|
|
|
898
|
+
// src/test-map.ts
|
|
899
|
+
var test_map_exports = {};
|
|
900
|
+
__export(test_map_exports, {
|
|
901
|
+
buildTestMap: () => buildTestMap
|
|
902
|
+
});
|
|
903
|
+
import path3 from "path";
|
|
904
|
+
import fg3 from "fast-glob";
|
|
905
|
+
async function buildTestMap(dir) {
|
|
906
|
+
const rootDir = path3.resolve(dir);
|
|
907
|
+
const testPatterns = [
|
|
908
|
+
"**/*.test.*",
|
|
909
|
+
"**/*.spec.*",
|
|
910
|
+
"**/*Test.kt",
|
|
911
|
+
"**/*Test.java",
|
|
912
|
+
"**/*Tests.kt",
|
|
913
|
+
"**/*Tests.java",
|
|
914
|
+
"**/test_*.py",
|
|
915
|
+
"**/*_test.py",
|
|
916
|
+
"**/*_test.go",
|
|
917
|
+
"**/*Tests.swift",
|
|
918
|
+
"**/*Test.swift",
|
|
919
|
+
"**/*_test.rs"
|
|
920
|
+
];
|
|
921
|
+
const testFiles = await fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
|
|
922
|
+
const sourceFiles = await fg3(
|
|
923
|
+
"**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}",
|
|
924
|
+
{ cwd: rootDir, ignore: IGNORE }
|
|
925
|
+
);
|
|
926
|
+
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
927
|
+
for (const file of sourceFiles) {
|
|
928
|
+
if (testFiles.includes(file)) continue;
|
|
929
|
+
const baseName = path3.basename(file).replace(/\.[^.]+$/, "");
|
|
930
|
+
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
931
|
+
existing.push(file);
|
|
932
|
+
sourceByBaseName.set(baseName, existing);
|
|
933
|
+
}
|
|
934
|
+
const paired = [];
|
|
935
|
+
const unmatched = [];
|
|
936
|
+
for (const testFile of testFiles) {
|
|
937
|
+
const testBaseName = path3.basename(testFile).replace(/\.[^.]+$/, "");
|
|
938
|
+
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
939
|
+
if (!sourceName) {
|
|
940
|
+
unmatched.push(testFile);
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const candidates = sourceByBaseName.get(sourceName);
|
|
944
|
+
if (candidates && candidates.length > 0) {
|
|
945
|
+
const testDir = path3.dirname(testFile);
|
|
946
|
+
const bestMatch = candidates.reduce((best, candidate) => {
|
|
947
|
+
const candidateDir = path3.dirname(candidate);
|
|
948
|
+
const bestDir = path3.dirname(best);
|
|
949
|
+
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
950
|
+
const bestOverlap = commonSegments(testDir, bestDir);
|
|
951
|
+
return candidateOverlap > bestOverlap ? candidate : best;
|
|
952
|
+
});
|
|
953
|
+
paired.push({
|
|
954
|
+
test: testFile,
|
|
955
|
+
source: bestMatch,
|
|
956
|
+
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
957
|
+
});
|
|
958
|
+
} else {
|
|
959
|
+
unmatched.push(testFile);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return { totalTestFiles: testFiles.length, paired, unmatched };
|
|
963
|
+
}
|
|
964
|
+
function commonSegments(pathA, pathB) {
|
|
965
|
+
const segsA = pathA.split("/");
|
|
966
|
+
const segsB = pathB.split("/");
|
|
967
|
+
let count = 0;
|
|
968
|
+
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
969
|
+
if (segsA[i] === segsB[i]) count++;
|
|
970
|
+
else break;
|
|
971
|
+
}
|
|
972
|
+
return count;
|
|
973
|
+
}
|
|
974
|
+
var IGNORE;
|
|
975
|
+
var init_test_map = __esm({
|
|
976
|
+
"src/test-map.ts"() {
|
|
977
|
+
"use strict";
|
|
978
|
+
IGNORE = [
|
|
979
|
+
"**/node_modules/**",
|
|
980
|
+
"**/dist/**",
|
|
981
|
+
"**/build/**",
|
|
982
|
+
"**/.gradle/**",
|
|
983
|
+
"**/target/**",
|
|
984
|
+
"**/.git/**",
|
|
985
|
+
"**/vendor/**",
|
|
986
|
+
"**/__pycache__/**",
|
|
987
|
+
"**/venv/**",
|
|
988
|
+
"**/.venv/**",
|
|
989
|
+
"**/*.min.*",
|
|
990
|
+
"**/*.map"
|
|
991
|
+
];
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
|
|
905
995
|
// src/snapshot/prompt.ts
|
|
906
|
-
function buildSnapshotPrompt(files) {
|
|
996
|
+
function buildSnapshotPrompt(files, testPairs) {
|
|
907
997
|
const fileBlocks = files.map(
|
|
908
998
|
(f) => `=== ${f.path} ===
|
|
909
999
|
${f.content.slice(0, 3e3)}${f.content.length > 3e3 ? "\n... (truncated)" : ""}`
|
|
910
1000
|
).join("\n\n");
|
|
911
|
-
|
|
1001
|
+
let prompt = `Create a concept-to-files map for this codebase. Here are the key source files:
|
|
912
1002
|
|
|
913
1003
|
${fileBlocks}`;
|
|
1004
|
+
if (testPairs && testPairs.length > 0) {
|
|
1005
|
+
const testBlock = testPairs.map((p) => `${p.test} \u2192 ${p.source}`).join("\n");
|
|
1006
|
+
prompt += `
|
|
1007
|
+
|
|
1008
|
+
Here are the test-to-source file mappings. Use these to populate the "tests" field for each feature:
|
|
1009
|
+
|
|
1010
|
+
${testBlock}`;
|
|
1011
|
+
}
|
|
1012
|
+
return prompt;
|
|
914
1013
|
}
|
|
915
1014
|
function buildIncrementalPrompt(files, existingSnapshot) {
|
|
916
1015
|
const fileBlocks = files.map(
|
|
@@ -986,14 +1085,14 @@ __export(snapshot_exports, {
|
|
|
986
1085
|
updateSnapshot: () => updateSnapshot
|
|
987
1086
|
});
|
|
988
1087
|
import fs4 from "fs/promises";
|
|
989
|
-
import
|
|
1088
|
+
import path4 from "path";
|
|
990
1089
|
import { execFile as execFile6 } from "child_process";
|
|
991
1090
|
import { promisify as promisify6 } from "util";
|
|
992
1091
|
function snapshotDir(rootDir) {
|
|
993
|
-
return
|
|
1092
|
+
return path4.join(rootDir, ".mason");
|
|
994
1093
|
}
|
|
995
1094
|
function snapshotPath(rootDir) {
|
|
996
|
-
return
|
|
1095
|
+
return path4.join(snapshotDir(rootDir), "snapshot.json");
|
|
997
1096
|
}
|
|
998
1097
|
async function loadSnapshot(rootDir) {
|
|
999
1098
|
try {
|
|
@@ -1051,7 +1150,7 @@ function parseSnapshotResponse(raw) {
|
|
|
1051
1150
|
}
|
|
1052
1151
|
}
|
|
1053
1152
|
async function createSnapshot(rootDir, config) {
|
|
1054
|
-
const resolvedRoot =
|
|
1153
|
+
const resolvedRoot = path4.resolve(rootDir);
|
|
1055
1154
|
const sampled = await sampleFiles(resolvedRoot, 25);
|
|
1056
1155
|
const filesWithContent = [];
|
|
1057
1156
|
for (const sample of sampled) {
|
|
@@ -1072,7 +1171,8 @@ async function createSnapshot(rootDir, config) {
|
|
|
1072
1171
|
flows: {}
|
|
1073
1172
|
};
|
|
1074
1173
|
}
|
|
1075
|
-
const
|
|
1174
|
+
const testMap = await buildTestMap(resolvedRoot);
|
|
1175
|
+
const userMessage = buildSnapshotPrompt(filesWithContent, testMap.paired);
|
|
1076
1176
|
const result = await callLLM(config, userMessage, SNAPSHOT_SYSTEM_PROMPT);
|
|
1077
1177
|
const resultText = typeof result === "string" ? result : result.type === "response" ? result.text : "";
|
|
1078
1178
|
if (!resultText) {
|
|
@@ -1093,7 +1193,7 @@ async function createSnapshot(rootDir, config) {
|
|
|
1093
1193
|
return snapshot;
|
|
1094
1194
|
}
|
|
1095
1195
|
async function updateSnapshot(rootDir, config) {
|
|
1096
|
-
const resolvedRoot =
|
|
1196
|
+
const resolvedRoot = path4.resolve(rootDir);
|
|
1097
1197
|
const existing = await loadSnapshot(resolvedRoot);
|
|
1098
1198
|
if (!existing) {
|
|
1099
1199
|
const snapshot = await createSnapshot(rootDir, config);
|
|
@@ -1174,14 +1274,14 @@ async function updateSnapshot(rootDir, config) {
|
|
|
1174
1274
|
};
|
|
1175
1275
|
}
|
|
1176
1276
|
async function installHook(rootDir) {
|
|
1177
|
-
const resolvedRoot =
|
|
1178
|
-
const hooksDir =
|
|
1277
|
+
const resolvedRoot = path4.resolve(rootDir);
|
|
1278
|
+
const hooksDir = path4.join(resolvedRoot, ".git", "hooks");
|
|
1179
1279
|
try {
|
|
1180
1280
|
await fs4.access(hooksDir);
|
|
1181
1281
|
} catch {
|
|
1182
1282
|
throw new Error("Not a git repository (no .git/hooks directory)");
|
|
1183
1283
|
}
|
|
1184
|
-
const hookPath =
|
|
1284
|
+
const hookPath = path4.join(hooksDir, "post-commit");
|
|
1185
1285
|
const hookContent = `#!/bin/sh
|
|
1186
1286
|
# Mason: auto-update project snapshot after commit
|
|
1187
1287
|
# Runs in background so it doesn't block your workflow
|
|
@@ -1202,6 +1302,7 @@ var init_snapshot = __esm({
|
|
|
1202
1302
|
"src/snapshot/snapshot.ts"() {
|
|
1203
1303
|
"use strict";
|
|
1204
1304
|
init_sampler();
|
|
1305
|
+
init_test_map();
|
|
1205
1306
|
init_providers();
|
|
1206
1307
|
init_prompt();
|
|
1207
1308
|
exec6 = promisify6(execFile6);
|
|
@@ -1214,12 +1315,12 @@ __export(impact_exports, {
|
|
|
1214
1315
|
analyzeImpact: () => analyzeImpact
|
|
1215
1316
|
});
|
|
1216
1317
|
import fs5 from "fs/promises";
|
|
1217
|
-
import
|
|
1318
|
+
import path5 from "path";
|
|
1218
1319
|
import { execFile as execFile7 } from "child_process";
|
|
1219
1320
|
import { promisify as promisify7 } from "util";
|
|
1220
|
-
import
|
|
1321
|
+
import fg4 from "fast-glob";
|
|
1221
1322
|
async function analyzeImpact(rootDir, targetFiles) {
|
|
1222
|
-
const resolvedRoot =
|
|
1323
|
+
const resolvedRoot = path5.resolve(rootDir);
|
|
1223
1324
|
const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
|
|
1224
1325
|
const [cochange, references, tests] = await Promise.all([
|
|
1225
1326
|
getCochangeFiles(resolvedRoot, resolvedTargets),
|
|
@@ -1240,17 +1341,17 @@ async function resolveTargetFiles(rootDir, targets) {
|
|
|
1240
1341
|
resolved.push(target);
|
|
1241
1342
|
continue;
|
|
1242
1343
|
}
|
|
1243
|
-
const matches = await
|
|
1344
|
+
const matches = await fg4(`**/${target}`, {
|
|
1244
1345
|
cwd: rootDir,
|
|
1245
|
-
ignore:
|
|
1346
|
+
ignore: IGNORE2
|
|
1246
1347
|
});
|
|
1247
1348
|
if (matches.length > 0) {
|
|
1248
1349
|
resolved.push(matches[0]);
|
|
1249
1350
|
} else {
|
|
1250
1351
|
const noExt = target.replace(/\.[^.]+$/, "");
|
|
1251
|
-
const extMatches = await
|
|
1352
|
+
const extMatches = await fg4(`**/${noExt}.*`, {
|
|
1252
1353
|
cwd: rootDir,
|
|
1253
|
-
ignore:
|
|
1354
|
+
ignore: IGNORE2
|
|
1254
1355
|
});
|
|
1255
1356
|
if (extMatches.length > 0) {
|
|
1256
1357
|
resolved.push(extMatches[0]);
|
|
@@ -1302,12 +1403,12 @@ async function getCochangeFiles(rootDir, targetFiles) {
|
|
|
1302
1403
|
async function getReferences(rootDir, targetFiles) {
|
|
1303
1404
|
const searchNames = /* @__PURE__ */ new Set();
|
|
1304
1405
|
for (const target of targetFiles) {
|
|
1305
|
-
const basename =
|
|
1406
|
+
const basename = path5.basename(target).replace(/\.[^.]+$/, "");
|
|
1306
1407
|
searchNames.add(basename);
|
|
1307
1408
|
}
|
|
1308
|
-
const allSourceFiles = await
|
|
1409
|
+
const allSourceFiles = await fg4(`**/${SOURCE_EXTENSIONS2}`, {
|
|
1309
1410
|
cwd: rootDir,
|
|
1310
|
-
ignore:
|
|
1411
|
+
ignore: IGNORE2
|
|
1311
1412
|
});
|
|
1312
1413
|
const targetSet = new Set(targetFiles);
|
|
1313
1414
|
const filesToSearch = allSourceFiles.filter((f) => !targetSet.has(f));
|
|
@@ -1319,7 +1420,7 @@ async function getReferences(rootDir, targetFiles) {
|
|
|
1319
1420
|
batch.map(async (file) => {
|
|
1320
1421
|
try {
|
|
1321
1422
|
const content = await fs5.readFile(
|
|
1322
|
-
|
|
1423
|
+
path5.join(rootDir, file),
|
|
1323
1424
|
"utf-8"
|
|
1324
1425
|
);
|
|
1325
1426
|
for (const name of searchNames) {
|
|
@@ -1354,12 +1455,12 @@ async function getRelatedTests(rootDir, targetFiles) {
|
|
|
1354
1455
|
"**/*Test.swift",
|
|
1355
1456
|
"**/*_test.rs"
|
|
1356
1457
|
];
|
|
1357
|
-
const testFiles = await
|
|
1458
|
+
const testFiles = await fg4(testPatterns, { cwd: rootDir, ignore: IGNORE2 });
|
|
1358
1459
|
const results = [];
|
|
1359
1460
|
for (const target of targetFiles) {
|
|
1360
|
-
const targetBaseName =
|
|
1461
|
+
const targetBaseName = path5.basename(target).replace(/\.[^.]+$/, "");
|
|
1361
1462
|
for (const testFile of testFiles) {
|
|
1362
|
-
const testBaseName =
|
|
1463
|
+
const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
|
|
1363
1464
|
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
1364
1465
|
if (sourceName === targetBaseName) {
|
|
1365
1466
|
results.push({
|
|
@@ -1374,12 +1475,12 @@ async function getRelatedTests(rootDir, targetFiles) {
|
|
|
1374
1475
|
function escapeRegex(str) {
|
|
1375
1476
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1376
1477
|
}
|
|
1377
|
-
var exec7,
|
|
1478
|
+
var exec7, IGNORE2, SOURCE_EXTENSIONS2;
|
|
1378
1479
|
var init_impact = __esm({
|
|
1379
1480
|
"src/impact/impact.ts"() {
|
|
1380
1481
|
"use strict";
|
|
1381
1482
|
exec7 = promisify7(execFile7);
|
|
1382
|
-
|
|
1483
|
+
IGNORE2 = [
|
|
1383
1484
|
"**/node_modules/**",
|
|
1384
1485
|
"**/dist/**",
|
|
1385
1486
|
"**/build/**",
|
|
@@ -1398,10 +1499,10 @@ var init_impact = __esm({
|
|
|
1398
1499
|
|
|
1399
1500
|
// src/mcp/tools.ts
|
|
1400
1501
|
import fs6 from "fs/promises";
|
|
1401
|
-
import
|
|
1502
|
+
import path6 from "path";
|
|
1402
1503
|
import { execFile as execFile8 } from "child_process";
|
|
1403
1504
|
import { promisify as promisify8 } from "util";
|
|
1404
|
-
import
|
|
1505
|
+
import fg5 from "fast-glob";
|
|
1405
1506
|
async function buildContext(dir) {
|
|
1406
1507
|
return {
|
|
1407
1508
|
rootDir: dir,
|
|
@@ -1409,7 +1510,7 @@ async function buildContext(dir) {
|
|
|
1409
1510
|
};
|
|
1410
1511
|
}
|
|
1411
1512
|
async function analyzeProject(dir) {
|
|
1412
|
-
const rootDir =
|
|
1513
|
+
const rootDir = path6.resolve(dir);
|
|
1413
1514
|
const context = await buildContext(rootDir);
|
|
1414
1515
|
const results = await runAll(context);
|
|
1415
1516
|
const projectSnapshot = await detectProjectSnapshot(rootDir);
|
|
@@ -1463,7 +1564,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
1463
1564
|
const present = [];
|
|
1464
1565
|
for (const file of buildFiles) {
|
|
1465
1566
|
try {
|
|
1466
|
-
await fs6.access(
|
|
1567
|
+
await fs6.access(path6.join(rootDir, file));
|
|
1467
1568
|
present.push(file);
|
|
1468
1569
|
} catch {
|
|
1469
1570
|
}
|
|
@@ -1481,9 +1582,9 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
1481
1582
|
];
|
|
1482
1583
|
const testInfo = {};
|
|
1483
1584
|
for (const pattern of testDirs) {
|
|
1484
|
-
const files = await
|
|
1585
|
+
const files = await fg5(`${pattern}/**/*`, {
|
|
1485
1586
|
cwd: rootDir,
|
|
1486
|
-
ignore:
|
|
1587
|
+
ignore: IGNORE3,
|
|
1487
1588
|
onlyFiles: true
|
|
1488
1589
|
});
|
|
1489
1590
|
if (files.length > 0) {
|
|
@@ -1501,18 +1602,18 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
1501
1602
|
{ pattern: "**/*_test.rs", label: "*_test.rs" }
|
|
1502
1603
|
];
|
|
1503
1604
|
for (const { pattern, label } of testFilePatterns) {
|
|
1504
|
-
const files = await
|
|
1605
|
+
const files = await fg5(pattern, { cwd: rootDir, ignore: IGNORE3 });
|
|
1505
1606
|
if (files.length > 0) {
|
|
1506
1607
|
testInfo[label] = files.length;
|
|
1507
1608
|
}
|
|
1508
1609
|
}
|
|
1509
|
-
const sourceFiles = await
|
|
1610
|
+
const sourceFiles = await fg5("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
|
|
1510
1611
|
cwd: rootDir,
|
|
1511
|
-
ignore:
|
|
1612
|
+
ignore: IGNORE3
|
|
1512
1613
|
});
|
|
1513
1614
|
const fileCounts = {};
|
|
1514
1615
|
for (const file of sourceFiles) {
|
|
1515
|
-
const ext =
|
|
1616
|
+
const ext = path6.extname(file).slice(1);
|
|
1516
1617
|
fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
|
|
1517
1618
|
}
|
|
1518
1619
|
return {
|
|
@@ -1523,7 +1624,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
1523
1624
|
};
|
|
1524
1625
|
}
|
|
1525
1626
|
async function getCodeSamples(dir, count = 15) {
|
|
1526
|
-
const rootDir =
|
|
1627
|
+
const rootDir = path6.resolve(dir);
|
|
1527
1628
|
const samples = await sampleFiles(rootDir, count);
|
|
1528
1629
|
const output = {
|
|
1529
1630
|
note: "These are previews (first ~60 lines). Use get_file_content to read the full file if needed.",
|
|
@@ -1538,10 +1639,10 @@ async function getCodeSamples(dir, count = 15) {
|
|
|
1538
1639
|
return JSON.stringify(output, null, 2);
|
|
1539
1640
|
}
|
|
1540
1641
|
async function getProjectStructure(dir) {
|
|
1541
|
-
const rootDir =
|
|
1542
|
-
const allFiles = await
|
|
1642
|
+
const rootDir = path6.resolve(dir);
|
|
1643
|
+
const allFiles = await fg5("**/*", {
|
|
1543
1644
|
cwd: rootDir,
|
|
1544
|
-
ignore:
|
|
1645
|
+
ignore: IGNORE3,
|
|
1545
1646
|
onlyFiles: true
|
|
1546
1647
|
});
|
|
1547
1648
|
const dirInfo = /* @__PURE__ */ new Map();
|
|
@@ -1554,7 +1655,7 @@ async function getProjectStructure(dir) {
|
|
|
1554
1655
|
}
|
|
1555
1656
|
const info2 = dirInfo.get(dirPath);
|
|
1556
1657
|
info2.fileCount++;
|
|
1557
|
-
const ext =
|
|
1658
|
+
const ext = path6.extname(file).slice(1);
|
|
1558
1659
|
if (ext) {
|
|
1559
1660
|
info2.extensions.set(ext, (info2.extensions.get(ext) ?? 0) + 1);
|
|
1560
1661
|
}
|
|
@@ -1576,81 +1677,12 @@ async function getProjectStructure(dir) {
|
|
|
1576
1677
|
return JSON.stringify(output, null, 2);
|
|
1577
1678
|
}
|
|
1578
1679
|
async function getTestMap(dir) {
|
|
1579
|
-
const
|
|
1580
|
-
const
|
|
1581
|
-
|
|
1582
|
-
"**/*.spec.*",
|
|
1583
|
-
"**/*Test.kt",
|
|
1584
|
-
"**/*Test.java",
|
|
1585
|
-
"**/*Tests.kt",
|
|
1586
|
-
"**/*Tests.java",
|
|
1587
|
-
"**/test_*.py",
|
|
1588
|
-
"**/*_test.py",
|
|
1589
|
-
"**/*_test.go",
|
|
1590
|
-
"**/*Tests.swift",
|
|
1591
|
-
"**/*Test.swift",
|
|
1592
|
-
"**/*_test.rs"
|
|
1593
|
-
];
|
|
1594
|
-
const testFiles = await fg4(testPatterns, { cwd: rootDir, ignore: IGNORE2 });
|
|
1595
|
-
const sourceFiles = await fg4(
|
|
1596
|
-
"**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}",
|
|
1597
|
-
{ cwd: rootDir, ignore: IGNORE2 }
|
|
1598
|
-
);
|
|
1599
|
-
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
1600
|
-
for (const file of sourceFiles) {
|
|
1601
|
-
if (testFiles.includes(file)) continue;
|
|
1602
|
-
const baseName = path5.basename(file).replace(/\.[^.]+$/, "");
|
|
1603
|
-
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
1604
|
-
existing.push(file);
|
|
1605
|
-
sourceByBaseName.set(baseName, existing);
|
|
1606
|
-
}
|
|
1607
|
-
const pairs = [];
|
|
1608
|
-
const unmatched = [];
|
|
1609
|
-
for (const testFile of testFiles) {
|
|
1610
|
-
const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
|
|
1611
|
-
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
1612
|
-
if (!sourceName) {
|
|
1613
|
-
unmatched.push(testFile);
|
|
1614
|
-
continue;
|
|
1615
|
-
}
|
|
1616
|
-
const candidates = sourceByBaseName.get(sourceName);
|
|
1617
|
-
if (candidates && candidates.length > 0) {
|
|
1618
|
-
const testDir = path5.dirname(testFile);
|
|
1619
|
-
const bestMatch = candidates.reduce((best, candidate) => {
|
|
1620
|
-
const candidateDir = path5.dirname(candidate);
|
|
1621
|
-
const bestDir = path5.dirname(best);
|
|
1622
|
-
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
1623
|
-
const bestOverlap = commonSegments(testDir, bestDir);
|
|
1624
|
-
return candidateOverlap > bestOverlap ? candidate : best;
|
|
1625
|
-
});
|
|
1626
|
-
pairs.push({
|
|
1627
|
-
test: testFile,
|
|
1628
|
-
source: bestMatch,
|
|
1629
|
-
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
1630
|
-
});
|
|
1631
|
-
} else {
|
|
1632
|
-
unmatched.push(testFile);
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
const output = {
|
|
1636
|
-
totalTestFiles: testFiles.length,
|
|
1637
|
-
paired: pairs,
|
|
1638
|
-
unmatched
|
|
1639
|
-
};
|
|
1640
|
-
return JSON.stringify(output, null, 2);
|
|
1641
|
-
}
|
|
1642
|
-
function commonSegments(pathA, pathB) {
|
|
1643
|
-
const segsA = pathA.split("/");
|
|
1644
|
-
const segsB = pathB.split("/");
|
|
1645
|
-
let count = 0;
|
|
1646
|
-
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
1647
|
-
if (segsA[i] === segsB[i]) count++;
|
|
1648
|
-
else break;
|
|
1649
|
-
}
|
|
1650
|
-
return count;
|
|
1680
|
+
const { buildTestMap: buildTestMap2 } = await Promise.resolve().then(() => (init_test_map(), test_map_exports));
|
|
1681
|
+
const result = await buildTestMap2(dir);
|
|
1682
|
+
return JSON.stringify(result, null, 2);
|
|
1651
1683
|
}
|
|
1652
1684
|
async function getSnapshot(dir) {
|
|
1653
|
-
const rootDir =
|
|
1685
|
+
const rootDir = path6.resolve(dir);
|
|
1654
1686
|
const snapshot = await loadSnapshot(rootDir);
|
|
1655
1687
|
if (!snapshot) {
|
|
1656
1688
|
return JSON.stringify({
|
|
@@ -1666,7 +1698,11 @@ async function getSnapshot(dir) {
|
|
|
1666
1698
|
const unique = feat.files.filter((f) => !seenFiles.has(f));
|
|
1667
1699
|
if (unique.length === 0) continue;
|
|
1668
1700
|
for (const f of unique) seenFiles.add(f);
|
|
1669
|
-
|
|
1701
|
+
const entry = { files: unique };
|
|
1702
|
+
if (feat.tests && feat.tests.length > 0) {
|
|
1703
|
+
entry.tests = feat.tests;
|
|
1704
|
+
}
|
|
1705
|
+
compactFeatures[name] = entry;
|
|
1670
1706
|
}
|
|
1671
1707
|
const compactFlows = {};
|
|
1672
1708
|
for (const [name, flow] of Object.entries(snapshot.flows)) {
|
|
@@ -1674,6 +1710,7 @@ async function getSnapshot(dir) {
|
|
|
1674
1710
|
}
|
|
1675
1711
|
const output = {
|
|
1676
1712
|
exists: true,
|
|
1713
|
+
updatedAt: snapshot.updatedAt,
|
|
1677
1714
|
features: compactFeatures,
|
|
1678
1715
|
flows: compactFlows,
|
|
1679
1716
|
stale: isStale
|
|
@@ -1684,7 +1721,7 @@ async function getSnapshot(dir) {
|
|
|
1684
1721
|
return JSON.stringify(output);
|
|
1685
1722
|
}
|
|
1686
1723
|
async function fullAnalysis(dir) {
|
|
1687
|
-
const rootDir =
|
|
1724
|
+
const rootDir = path6.resolve(dir);
|
|
1688
1725
|
const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
|
|
1689
1726
|
analyzeProject(dir),
|
|
1690
1727
|
getProjectStructure(dir),
|
|
@@ -1710,7 +1747,7 @@ async function fullAnalysis(dir) {
|
|
|
1710
1747
|
return JSON.stringify(output, null, 2);
|
|
1711
1748
|
}
|
|
1712
1749
|
async function saveSnapshotData(dir, features, flows) {
|
|
1713
|
-
const rootDir =
|
|
1750
|
+
const rootDir = path6.resolve(dir);
|
|
1714
1751
|
const gitHash = await getCurrentGitHash(rootDir);
|
|
1715
1752
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1716
1753
|
const existing = await loadSnapshot(rootDir);
|
|
@@ -1743,11 +1780,11 @@ async function saveSnapshotData(dir, features, flows) {
|
|
|
1743
1780
|
}
|
|
1744
1781
|
async function getImpact(dir, files) {
|
|
1745
1782
|
const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
|
|
1746
|
-
const rootDir =
|
|
1783
|
+
const rootDir = path6.resolve(dir);
|
|
1747
1784
|
const result = await analyzeImpact2(rootDir, files);
|
|
1748
1785
|
return JSON.stringify(result, null, 2);
|
|
1749
1786
|
}
|
|
1750
|
-
var exec8,
|
|
1787
|
+
var exec8, IGNORE3;
|
|
1751
1788
|
var init_tools = __esm({
|
|
1752
1789
|
"src/mcp/tools.ts"() {
|
|
1753
1790
|
"use strict";
|
|
@@ -1756,7 +1793,7 @@ var init_tools = __esm({
|
|
|
1756
1793
|
init_sampler();
|
|
1757
1794
|
init_snapshot();
|
|
1758
1795
|
exec8 = promisify8(execFile8);
|
|
1759
|
-
|
|
1796
|
+
IGNORE3 = [
|
|
1760
1797
|
"**/node_modules/**",
|
|
1761
1798
|
"**/dist/**",
|
|
1762
1799
|
"**/build/**",
|
|
@@ -1786,7 +1823,7 @@ function createMcpServer() {
|
|
|
1786
1823
|
const server = new McpServer(
|
|
1787
1824
|
{
|
|
1788
1825
|
name: "mason",
|
|
1789
|
-
version: "0.
|
|
1826
|
+
version: "0.3.0"
|
|
1790
1827
|
},
|
|
1791
1828
|
{
|
|
1792
1829
|
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."
|
|
@@ -1907,7 +1944,7 @@ init_providers();
|
|
|
1907
1944
|
init_tools();
|
|
1908
1945
|
import { Command } from "commander";
|
|
1909
1946
|
import fs7 from "fs/promises";
|
|
1910
|
-
import
|
|
1947
|
+
import path7 from "path";
|
|
1911
1948
|
import ora from "ora";
|
|
1912
1949
|
import chalk2 from "chalk";
|
|
1913
1950
|
|
|
@@ -1970,7 +2007,7 @@ function createCLI() {
|
|
|
1970
2007
|
const program2 = new Command();
|
|
1971
2008
|
program2.name("mason").description(
|
|
1972
2009
|
"Context engineering CLI & MCP server \u2014 generates intelligent CLAUDE.md files"
|
|
1973
|
-
).version("0.
|
|
2010
|
+
).version("0.3.0");
|
|
1974
2011
|
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) => {
|
|
1975
2012
|
const { execFile: execFile9 } = await import("child_process");
|
|
1976
2013
|
const { promisify: promisify9 } = await import("util");
|
|
@@ -2050,7 +2087,7 @@ function createCLI() {
|
|
|
2050
2087
|
);
|
|
2051
2088
|
process.exit(1);
|
|
2052
2089
|
}
|
|
2053
|
-
const rootDir =
|
|
2090
|
+
const rootDir = path7.resolve(dir);
|
|
2054
2091
|
const runConfig = opts.model ? { ...config, model: opts.model } : config;
|
|
2055
2092
|
const spinner = ora("Analyzing codebase...").start();
|
|
2056
2093
|
const analysisData = await fullAnalysis(rootDir);
|
|
@@ -2080,9 +2117,9 @@ ${analysisData}`
|
|
|
2080
2117
|
error("LLM returned empty response.");
|
|
2081
2118
|
process.exit(1);
|
|
2082
2119
|
}
|
|
2083
|
-
const claudeDir =
|
|
2120
|
+
const claudeDir = path7.join(rootDir, ".claude");
|
|
2084
2121
|
await fs7.mkdir(claudeDir, { recursive: true });
|
|
2085
|
-
const outPath =
|
|
2122
|
+
const outPath = path7.join(claudeDir, "CLAUDE.md");
|
|
2086
2123
|
await fs7.writeFile(outPath, markdown, "utf-8");
|
|
2087
2124
|
success(`Generated ${outPath}`);
|
|
2088
2125
|
try {
|
|
@@ -2097,7 +2134,7 @@ ${analysisData}`
|
|
|
2097
2134
|
);
|
|
2098
2135
|
} catch {
|
|
2099
2136
|
}
|
|
2100
|
-
const hookPath =
|
|
2137
|
+
const hookPath = path7.join(rootDir, ".git", "hooks", "post-commit");
|
|
2101
2138
|
try {
|
|
2102
2139
|
const hookContent = await fs7.readFile(hookPath, "utf-8");
|
|
2103
2140
|
if (!hookContent.includes("mason snapshot-update")) {
|
|
@@ -2127,7 +2164,7 @@ ${analysisData}`
|
|
|
2127
2164
|
createSnapshot: createSnapshot2,
|
|
2128
2165
|
installHook: installHook2
|
|
2129
2166
|
} = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
2130
|
-
const rootDir =
|
|
2167
|
+
const rootDir = path7.resolve(dir);
|
|
2131
2168
|
if (opts.installHook) {
|
|
2132
2169
|
try {
|
|
2133
2170
|
await installHook2(rootDir);
|
|
@@ -2165,7 +2202,7 @@ ${analysisData}`
|
|
|
2165
2202
|
});
|
|
2166
2203
|
program2.command("snapshot-update").description("Incrementally update snapshot with recent changes").argument("[dir]", "Directory to update", ".").action(async (dir) => {
|
|
2167
2204
|
const { updateSnapshot: updateSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
2168
|
-
const rootDir =
|
|
2205
|
+
const rootDir = path7.resolve(dir);
|
|
2169
2206
|
const config = await loadConfig();
|
|
2170
2207
|
if (!config) return;
|
|
2171
2208
|
try {
|
|
@@ -2179,7 +2216,7 @@ ${analysisData}`
|
|
|
2179
2216
|
});
|
|
2180
2217
|
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
2218
|
const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
|
|
2182
|
-
const rootDir =
|
|
2219
|
+
const rootDir = path7.resolve(opts.dir);
|
|
2183
2220
|
const spinner = ora("Analyzing impact...").start();
|
|
2184
2221
|
const result = await analyzeImpact2(rootDir, files);
|
|
2185
2222
|
spinner.stop();
|
|
@@ -2216,7 +2253,7 @@ Impact analysis for: ${result.targetFiles.join(", ")}
|
|
|
2216
2253
|
}
|
|
2217
2254
|
});
|
|
2218
2255
|
program2.command("analyze").description("Analyze the codebase and print findings").argument("[dir]", "Directory to analyze", ".").action(async (dir) => {
|
|
2219
|
-
const rootDir =
|
|
2256
|
+
const rootDir = path7.resolve(dir);
|
|
2220
2257
|
const spinner = ora("Analyzing codebase...").start();
|
|
2221
2258
|
const context = await buildContext2(rootDir);
|
|
2222
2259
|
const results = await runAll(context);
|