mason-context 0.3.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -62
- package/dist/mason-drift.js +541 -0
- package/dist/mason-drift.js.map +1 -0
- package/dist/mason-mcp.js +4268 -0
- package/dist/mason-mcp.js.map +1 -0
- package/dist/mason.js +19 -0
- package/dist/mason.js.map +1 -0
- package/package.json +11 -11
- package/dist/bin/mason-mcp.js +0 -1412
- package/dist/bin/mason-mcp.js.map +0 -1
- package/dist/bin/mason.js +0 -2338
- package/dist/bin/mason.js.map +0 -1
- package/dist/src/cli.js +0 -2337
- package/dist/src/cli.js.map +0 -1
package/dist/bin/mason.js
DELETED
|
@@ -1,2338 +0,0 @@
|
|
|
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/analyzers/base.ts
|
|
13
|
-
import fs from "fs/promises";
|
|
14
|
-
import fg from "fast-glob";
|
|
15
|
-
var BaseAnalyzer;
|
|
16
|
-
var init_base = __esm({
|
|
17
|
-
"src/analyzers/base.ts"() {
|
|
18
|
-
"use strict";
|
|
19
|
-
BaseAnalyzer = class {
|
|
20
|
-
async findFiles(patterns, root) {
|
|
21
|
-
return fg(patterns, {
|
|
22
|
-
cwd: root,
|
|
23
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
|
|
24
|
-
absolute: true
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
async readFile(filePath) {
|
|
28
|
-
return fs.readFile(filePath, "utf-8");
|
|
29
|
-
}
|
|
30
|
-
createFinding(partial) {
|
|
31
|
-
return {
|
|
32
|
-
analyzer: this.name,
|
|
33
|
-
category: partial.category,
|
|
34
|
-
confidence: partial.confidence,
|
|
35
|
-
summary: partial.summary,
|
|
36
|
-
evidence: partial.evidence ?? [],
|
|
37
|
-
ruleCandidate: partial.ruleCandidate ?? null
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
createResult(findings, gaps, startTime) {
|
|
41
|
-
return {
|
|
42
|
-
analyzer: this.name,
|
|
43
|
-
findings,
|
|
44
|
-
gaps,
|
|
45
|
-
durationMs: Date.now() - startTime
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// src/analyzers/git-history.ts
|
|
53
|
-
import { execFile } from "child_process";
|
|
54
|
-
import { promisify } from "util";
|
|
55
|
-
var exec, GitHistoryAnalyzer;
|
|
56
|
-
var init_git_history = __esm({
|
|
57
|
-
"src/analyzers/git-history.ts"() {
|
|
58
|
-
"use strict";
|
|
59
|
-
init_base();
|
|
60
|
-
exec = promisify(execFile);
|
|
61
|
-
GitHistoryAnalyzer = class extends BaseAnalyzer {
|
|
62
|
-
name = "git-history";
|
|
63
|
-
async analyze(context) {
|
|
64
|
-
const startTime = Date.now();
|
|
65
|
-
const findings = [];
|
|
66
|
-
const gaps = [];
|
|
67
|
-
if (!context.gitAvailable) {
|
|
68
|
-
return this.createResult([], [], startTime);
|
|
69
|
-
}
|
|
70
|
-
const [staleFindings, staleGaps] = await this.findStaleDirectories(context);
|
|
71
|
-
findings.push(...staleFindings);
|
|
72
|
-
gaps.push(...staleGaps);
|
|
73
|
-
const hotFindings = await this.findHotFiles(context);
|
|
74
|
-
findings.push(...hotFindings);
|
|
75
|
-
const commitFindings = await this.analyzeCommitPatterns(context);
|
|
76
|
-
findings.push(...commitFindings);
|
|
77
|
-
return this.createResult(findings, gaps, startTime);
|
|
78
|
-
}
|
|
79
|
-
async git(args, cwd) {
|
|
80
|
-
try {
|
|
81
|
-
const { stdout } = await exec("git", args, { cwd, maxBuffer: 1e7 });
|
|
82
|
-
return stdout.trim();
|
|
83
|
-
} catch {
|
|
84
|
-
return "";
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
async findStaleDirectories(context) {
|
|
88
|
-
const findings = [];
|
|
89
|
-
const gaps = [];
|
|
90
|
-
const output = await this.git(
|
|
91
|
-
["log", "--all", "--format=%ci", "--name-only", "--diff-filter=AMCR", "-n", "500"],
|
|
92
|
-
context.rootDir
|
|
93
|
-
);
|
|
94
|
-
if (!output) return [findings, gaps];
|
|
95
|
-
const dirLastTouch = /* @__PURE__ */ new Map();
|
|
96
|
-
let currentDate = null;
|
|
97
|
-
for (const line of output.split("\n")) {
|
|
98
|
-
if (!line) continue;
|
|
99
|
-
if (/^\d{4}-\d{2}-\d{2}/.test(line)) {
|
|
100
|
-
currentDate = new Date(line);
|
|
101
|
-
} else if (currentDate) {
|
|
102
|
-
const topDir = line.split("/")[0];
|
|
103
|
-
if (topDir && !topDir.startsWith(".") && !topDir.includes("node_modules")) {
|
|
104
|
-
const existing = dirLastTouch.get(topDir);
|
|
105
|
-
if (!existing || currentDate > existing) {
|
|
106
|
-
dirLastTouch.set(topDir, currentDate);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const sixMonthsAgo = /* @__PURE__ */ new Date();
|
|
112
|
-
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
|
|
113
|
-
for (const [dir, lastTouch] of dirLastTouch) {
|
|
114
|
-
if (lastTouch < sixMonthsAgo) {
|
|
115
|
-
const monthsStale = Math.floor(
|
|
116
|
-
(Date.now() - lastTouch.getTime()) / (1e3 * 60 * 60 * 24 * 30)
|
|
117
|
-
);
|
|
118
|
-
findings.push(
|
|
119
|
-
this.createFinding({
|
|
120
|
-
category: "risk",
|
|
121
|
-
confidence: 0.7,
|
|
122
|
-
summary: `Directory "${dir}" hasn't been modified in ${monthsStale} months`,
|
|
123
|
-
evidence: [
|
|
124
|
-
{ filePath: dir, detail: `Last commit: ${lastTouch.toISOString().split("T")[0]}` }
|
|
125
|
-
],
|
|
126
|
-
ruleCandidate: `Do not refactor or modify files in "${dir}/" unless explicitly asked \u2014 this area has been stable for ${monthsStale} months and may be legacy code.`
|
|
127
|
-
})
|
|
128
|
-
);
|
|
129
|
-
gaps.push({
|
|
130
|
-
analyzer: this.name,
|
|
131
|
-
question: `Directory "${dir}" hasn't been touched in ${monthsStale} months. Is it deprecated, stable, or legacy?`,
|
|
132
|
-
context: `Last modified: ${lastTouch.toISOString().split("T")[0]}`,
|
|
133
|
-
answerKey: `stale-dir-${dir}`
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return [findings, gaps];
|
|
138
|
-
}
|
|
139
|
-
async findHotFiles(context) {
|
|
140
|
-
const findings = [];
|
|
141
|
-
const output = await this.git(
|
|
142
|
-
["log", "--since=3 months ago", "--format=", "--name-only"],
|
|
143
|
-
context.rootDir
|
|
144
|
-
);
|
|
145
|
-
if (!output) return findings;
|
|
146
|
-
const fileCounts = /* @__PURE__ */ new Map();
|
|
147
|
-
for (const line of output.split("\n")) {
|
|
148
|
-
if (!line || line.startsWith(".") || line.includes("node_modules")) continue;
|
|
149
|
-
fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
|
|
150
|
-
}
|
|
151
|
-
const sorted = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
152
|
-
if (sorted.length > 0 && sorted[0][1] >= 5) {
|
|
153
|
-
const hotFiles = sorted.filter(([, count]) => count >= 5);
|
|
154
|
-
if (hotFiles.length > 0) {
|
|
155
|
-
findings.push(
|
|
156
|
-
this.createFinding({
|
|
157
|
-
category: "risk",
|
|
158
|
-
confidence: 0.8,
|
|
159
|
-
summary: `${hotFiles.length} files changed frequently in the last 3 months`,
|
|
160
|
-
evidence: hotFiles.map(([file, count]) => ({
|
|
161
|
-
filePath: file,
|
|
162
|
-
detail: `${count} commits`
|
|
163
|
-
})),
|
|
164
|
-
ruleCandidate: `These files change frequently and are high-risk for conflicts: ${hotFiles.map(([f]) => f).join(", ")}. Take extra care when modifying them.`
|
|
165
|
-
})
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return findings;
|
|
170
|
-
}
|
|
171
|
-
async analyzeCommitPatterns(context) {
|
|
172
|
-
const findings = [];
|
|
173
|
-
const output = await this.git(
|
|
174
|
-
["log", "--format=%s", "-n", "100"],
|
|
175
|
-
context.rootDir
|
|
176
|
-
);
|
|
177
|
-
if (!output) return findings;
|
|
178
|
-
const messages = output.split("\n").filter(Boolean);
|
|
179
|
-
const conventionalPattern = /^(feat|fix|chore|docs|style|refactor|test|perf|ci|build|revert)(\(.+\))?:/;
|
|
180
|
-
const conventionalCount = messages.filter(
|
|
181
|
-
(m) => conventionalPattern.test(m)
|
|
182
|
-
).length;
|
|
183
|
-
const conventionalRatio = conventionalCount / messages.length;
|
|
184
|
-
if (conventionalRatio > 0.5) {
|
|
185
|
-
findings.push(
|
|
186
|
-
this.createFinding({
|
|
187
|
-
category: "convention",
|
|
188
|
-
confidence: Math.min(conventionalRatio + 0.1, 1),
|
|
189
|
-
summary: `${Math.round(conventionalRatio * 100)}% of recent commits use conventional commit format`,
|
|
190
|
-
evidence: [
|
|
191
|
-
{
|
|
192
|
-
filePath: ".git",
|
|
193
|
-
detail: `${conventionalCount} of ${messages.length} commits match`
|
|
194
|
-
}
|
|
195
|
-
],
|
|
196
|
-
ruleCandidate: "Use conventional commit format: type(scope): description (e.g., feat(auth): add login endpoint)"
|
|
197
|
-
})
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
const ticketPattern = /[A-Z]+-\d+|#\d+/;
|
|
201
|
-
const ticketCount = messages.filter((m) => ticketPattern.test(m)).length;
|
|
202
|
-
const ticketRatio = ticketCount / messages.length;
|
|
203
|
-
if (ticketRatio > 0.3) {
|
|
204
|
-
findings.push(
|
|
205
|
-
this.createFinding({
|
|
206
|
-
category: "convention",
|
|
207
|
-
confidence: ticketRatio,
|
|
208
|
-
summary: `${Math.round(ticketRatio * 100)}% of commits reference issue/ticket IDs`,
|
|
209
|
-
evidence: [
|
|
210
|
-
{
|
|
211
|
-
filePath: ".git",
|
|
212
|
-
detail: `${ticketCount} of ${messages.length} commits have ticket refs`
|
|
213
|
-
}
|
|
214
|
-
],
|
|
215
|
-
ruleCandidate: "Include issue/ticket references in commit messages when applicable."
|
|
216
|
-
})
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
return findings;
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// src/analyzers/index.ts
|
|
226
|
-
async function runAll(context) {
|
|
227
|
-
return Promise.all(analyzers.map((a) => a.analyze(context)));
|
|
228
|
-
}
|
|
229
|
-
var analyzers;
|
|
230
|
-
var init_analyzers = __esm({
|
|
231
|
-
"src/analyzers/index.ts"() {
|
|
232
|
-
"use strict";
|
|
233
|
-
init_git_history();
|
|
234
|
-
analyzers = [new GitHistoryAnalyzer()];
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
// src/utils/git.ts
|
|
239
|
-
import { execFile as execFile2 } from "child_process";
|
|
240
|
-
import { promisify as promisify2 } from "util";
|
|
241
|
-
async function isGitRepo(dir) {
|
|
242
|
-
try {
|
|
243
|
-
await exec2("git", ["rev-parse", "--git-dir"], { cwd: dir });
|
|
244
|
-
return true;
|
|
245
|
-
} catch {
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
var exec2;
|
|
250
|
-
var init_git = __esm({
|
|
251
|
-
"src/utils/git.ts"() {
|
|
252
|
-
"use strict";
|
|
253
|
-
exec2 = promisify2(execFile2);
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// src/llm/config.ts
|
|
258
|
-
import fs2 from "fs/promises";
|
|
259
|
-
import path from "path";
|
|
260
|
-
import os from "os";
|
|
261
|
-
import { execFile as execFile3 } from "child_process";
|
|
262
|
-
import { promisify as promisify3 } from "util";
|
|
263
|
-
async function loadConfig() {
|
|
264
|
-
try {
|
|
265
|
-
const raw = await fs2.readFile(CONFIG_FILE, "utf-8");
|
|
266
|
-
return JSON.parse(raw);
|
|
267
|
-
} catch {
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
async function saveConfig(config) {
|
|
272
|
-
await fs2.mkdir(CONFIG_DIR, { recursive: true });
|
|
273
|
-
await fs2.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
274
|
-
}
|
|
275
|
-
function getDefaultModel(provider) {
|
|
276
|
-
return DEFAULT_MODELS[provider];
|
|
277
|
-
}
|
|
278
|
-
function validateProvider(value) {
|
|
279
|
-
const valid = ["claude", "gemini", "openai", "ollama"];
|
|
280
|
-
if (!valid.includes(value)) {
|
|
281
|
-
throw new Error(
|
|
282
|
-
`Invalid provider "${value}". Must be one of: ${valid.join(", ")}`
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
return value;
|
|
286
|
-
}
|
|
287
|
-
async function detectCLI(provider) {
|
|
288
|
-
const cliName = provider === "claude" ? "claude" : provider === "gemini" ? "gemini" : provider === "ollama" ? "ollama" : null;
|
|
289
|
-
if (!cliName) return { available: false };
|
|
290
|
-
try {
|
|
291
|
-
const { stdout } = await exec3(cliName, ["--version"]);
|
|
292
|
-
return { available: true, version: stdout.trim() };
|
|
293
|
-
} catch {
|
|
294
|
-
return { available: false };
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
function needsApiKey(provider) {
|
|
298
|
-
return provider === "openai";
|
|
299
|
-
}
|
|
300
|
-
var exec3, CONFIG_DIR, CONFIG_FILE, DEFAULT_MODELS;
|
|
301
|
-
var init_config = __esm({
|
|
302
|
-
"src/llm/config.ts"() {
|
|
303
|
-
"use strict";
|
|
304
|
-
exec3 = promisify3(execFile3);
|
|
305
|
-
CONFIG_DIR = path.join(os.homedir(), ".mason");
|
|
306
|
-
CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
307
|
-
DEFAULT_MODELS = {
|
|
308
|
-
claude: "claude-sonnet-4-20250514",
|
|
309
|
-
gemini: "gemini-2.5-flash",
|
|
310
|
-
openai: "gpt-4o",
|
|
311
|
-
ollama: "llama3"
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// src/llm/providers.ts
|
|
317
|
-
import { execFile as execFile4, spawn } from "child_process";
|
|
318
|
-
import { promisify as promisify4 } from "util";
|
|
319
|
-
async function callLLM(config, userMessage, systemPrompt) {
|
|
320
|
-
const model = config.model ?? getDefaultModel(config.provider);
|
|
321
|
-
const system = systemPrompt ?? CLAUDE_MD_SYSTEM_PROMPT;
|
|
322
|
-
switch (config.provider) {
|
|
323
|
-
case "claude":
|
|
324
|
-
if (config.apiKey) {
|
|
325
|
-
return {
|
|
326
|
-
type: "response",
|
|
327
|
-
text: await callClaudeAPI(config.apiKey, model, system, userMessage)
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
return {
|
|
331
|
-
type: "response",
|
|
332
|
-
text: await callClaudeCLI(system, userMessage)
|
|
333
|
-
};
|
|
334
|
-
case "ollama":
|
|
335
|
-
return {
|
|
336
|
-
type: "response",
|
|
337
|
-
text: await callOllamaCLI(
|
|
338
|
-
config.ollamaHost ?? "http://localhost:11434",
|
|
339
|
-
model,
|
|
340
|
-
system,
|
|
341
|
-
userMessage
|
|
342
|
-
)
|
|
343
|
-
};
|
|
344
|
-
case "gemini":
|
|
345
|
-
if (config.apiKey) {
|
|
346
|
-
return {
|
|
347
|
-
type: "response",
|
|
348
|
-
text: await callGeminiAPI(config.apiKey, model, system, userMessage)
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
return {
|
|
352
|
-
type: "response",
|
|
353
|
-
text: await callGeminiCLI(system, userMessage)
|
|
354
|
-
};
|
|
355
|
-
case "openai":
|
|
356
|
-
if (config.apiKey) {
|
|
357
|
-
return {
|
|
358
|
-
type: "response",
|
|
359
|
-
text: await callOpenAIAPI(config.apiKey, model, system, userMessage)
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
return {
|
|
363
|
-
type: "prompt",
|
|
364
|
-
text: formatPromptForCopy(system, userMessage)
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
function formatPromptForCopy(system, userMessage) {
|
|
369
|
-
return `${system}
|
|
370
|
-
|
|
371
|
-
---
|
|
372
|
-
|
|
373
|
-
${userMessage}`;
|
|
374
|
-
}
|
|
375
|
-
function spawnWithStdin(command, args, input) {
|
|
376
|
-
return new Promise((resolve, reject) => {
|
|
377
|
-
const proc = spawn(command, args, {
|
|
378
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
379
|
-
timeout: 3e5
|
|
380
|
-
});
|
|
381
|
-
const onSigint = () => proc.kill("SIGINT");
|
|
382
|
-
process.on("SIGINT", onSigint);
|
|
383
|
-
let stdout = "";
|
|
384
|
-
let stderr = "";
|
|
385
|
-
proc.stdout.on("data", (data) => {
|
|
386
|
-
stdout += data.toString();
|
|
387
|
-
});
|
|
388
|
-
proc.stderr.on("data", (data) => {
|
|
389
|
-
stderr += data.toString();
|
|
390
|
-
});
|
|
391
|
-
proc.on("close", (code) => {
|
|
392
|
-
process.off("SIGINT", onSigint);
|
|
393
|
-
if (code === 0) {
|
|
394
|
-
resolve(stdout.trim());
|
|
395
|
-
} else {
|
|
396
|
-
reject(new Error(`${command} exited with code ${code}: ${stderr}`));
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
proc.on("error", (err) => {
|
|
400
|
-
process.off("SIGINT", onSigint);
|
|
401
|
-
reject(err);
|
|
402
|
-
});
|
|
403
|
-
proc.stdin.write(input);
|
|
404
|
-
proc.stdin.end();
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
async function callClaudeCLI(system, userMessage) {
|
|
408
|
-
return spawnWithStdin("claude", ["-p", "--system-prompt", system], userMessage);
|
|
409
|
-
}
|
|
410
|
-
async function callGeminiCLI(system, userMessage) {
|
|
411
|
-
const prompt = `<system>
|
|
412
|
-
${system}
|
|
413
|
-
</system>
|
|
414
|
-
|
|
415
|
-
${userMessage}`;
|
|
416
|
-
return spawnWithStdin("gemini", ["-p", ""], prompt);
|
|
417
|
-
}
|
|
418
|
-
async function callOllamaCLI(host, model, system, userMessage) {
|
|
419
|
-
const response = await fetch(`${host}/api/chat`, {
|
|
420
|
-
method: "POST",
|
|
421
|
-
headers: { "Content-Type": "application/json" },
|
|
422
|
-
body: JSON.stringify({
|
|
423
|
-
model,
|
|
424
|
-
stream: false,
|
|
425
|
-
messages: [
|
|
426
|
-
{ role: "system", content: system },
|
|
427
|
-
{ role: "user", content: userMessage }
|
|
428
|
-
]
|
|
429
|
-
})
|
|
430
|
-
});
|
|
431
|
-
const result = await response.json();
|
|
432
|
-
return result.message?.content ?? "";
|
|
433
|
-
}
|
|
434
|
-
async function callClaudeAPI(apiKey, model, system, userMessage) {
|
|
435
|
-
const { default: Anthropic } = await import("@anthropic-ai/sdk");
|
|
436
|
-
const client = new Anthropic({ apiKey });
|
|
437
|
-
const response = await client.messages.create({
|
|
438
|
-
model,
|
|
439
|
-
max_tokens: 8192,
|
|
440
|
-
system,
|
|
441
|
-
messages: [{ role: "user", content: userMessage }]
|
|
442
|
-
});
|
|
443
|
-
const textBlock = response.content.find((b) => b.type === "text");
|
|
444
|
-
return textBlock?.text ?? "";
|
|
445
|
-
}
|
|
446
|
-
async function callGeminiAPI(apiKey, model, system, userMessage) {
|
|
447
|
-
const { default: OpenAI } = await import("openai");
|
|
448
|
-
const client = new OpenAI({
|
|
449
|
-
apiKey,
|
|
450
|
-
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/"
|
|
451
|
-
});
|
|
452
|
-
const response = await client.chat.completions.create({
|
|
453
|
-
model,
|
|
454
|
-
max_tokens: 8192,
|
|
455
|
-
messages: [
|
|
456
|
-
{ role: "system", content: system },
|
|
457
|
-
{ role: "user", content: userMessage }
|
|
458
|
-
]
|
|
459
|
-
});
|
|
460
|
-
return response.choices[0]?.message?.content ?? "";
|
|
461
|
-
}
|
|
462
|
-
async function callOpenAIAPI(apiKey, model, system, userMessage) {
|
|
463
|
-
const { default: OpenAI } = await import("openai");
|
|
464
|
-
const client = new OpenAI({ apiKey });
|
|
465
|
-
const response = await client.chat.completions.create({
|
|
466
|
-
model,
|
|
467
|
-
max_tokens: 8192,
|
|
468
|
-
messages: [
|
|
469
|
-
{ role: "system", content: system },
|
|
470
|
-
{ role: "user", content: userMessage }
|
|
471
|
-
]
|
|
472
|
-
});
|
|
473
|
-
return response.choices[0]?.message?.content ?? "";
|
|
474
|
-
}
|
|
475
|
-
var exec4, CLAUDE_MD_SYSTEM_PROMPT;
|
|
476
|
-
var init_providers = __esm({
|
|
477
|
-
"src/llm/providers.ts"() {
|
|
478
|
-
"use strict";
|
|
479
|
-
init_config();
|
|
480
|
-
exec4 = promisify4(execFile4);
|
|
481
|
-
CLAUDE_MD_SYSTEM_PROMPT = `You are Mason, a context engineering tool. You've been given a comprehensive analysis of a codebase including:
|
|
482
|
-
- Git history stats (commit patterns, frequently changed files, stale directories)
|
|
483
|
-
- Project structure (directory layout, file counts by type)
|
|
484
|
-
- Curated code samples (key architectural files with previews)
|
|
485
|
-
- Test-to-source file mapping
|
|
486
|
-
|
|
487
|
-
Your job: write a COMPLETE CLAUDE.md file from scratch based ONLY on the analysis data provided below. Do NOT read any existing files in the project. Do NOT reference or preserve any existing CLAUDE.md. Generate the entire document fresh.
|
|
488
|
-
|
|
489
|
-
CRITICAL: Output ONLY the raw markdown content. No preamble, no summary, no "Here's the CLAUDE.md:", no explanation, no questions, no commentary. Start directly with "# CLAUDE.md" and end with the last line of content. Your entire response will be written directly to a file.
|
|
490
|
-
|
|
491
|
-
The CLAUDE.md should include:
|
|
492
|
-
- Project overview (what it is, tech stack, architecture)
|
|
493
|
-
- Module/package structure and boundaries
|
|
494
|
-
- Code conventions and patterns you observe in the samples
|
|
495
|
-
- Testing conventions and coverage
|
|
496
|
-
- Build and development commands
|
|
497
|
-
- Important files and hot spots
|
|
498
|
-
- Any warnings or gotchas
|
|
499
|
-
|
|
500
|
-
Be specific and actionable. Reference actual file paths. Don't be generic \u2014 every rule should be grounded in what you see in the data.`;
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
// src/mcp/sampler.ts
|
|
505
|
-
import fs3 from "fs/promises";
|
|
506
|
-
import path2 from "path";
|
|
507
|
-
import { execFile as execFile5 } from "child_process";
|
|
508
|
-
import { promisify as promisify5 } from "util";
|
|
509
|
-
import fg2 from "fast-glob";
|
|
510
|
-
async function loadProjectConfig(rootDir) {
|
|
511
|
-
try {
|
|
512
|
-
const raw = await fs3.readFile(
|
|
513
|
-
path2.join(rootDir, ".mason", "config.json"),
|
|
514
|
-
"utf-8"
|
|
515
|
-
);
|
|
516
|
-
return JSON.parse(raw);
|
|
517
|
-
} catch {
|
|
518
|
-
return {};
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
async function getTrackedFiles(rootDir) {
|
|
522
|
-
try {
|
|
523
|
-
const { stdout } = await exec5("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
524
|
-
cwd: rootDir,
|
|
525
|
-
maxBuffer: 1e7
|
|
526
|
-
});
|
|
527
|
-
return new Set(stdout.trim().split("\n").filter(Boolean));
|
|
528
|
-
} catch {
|
|
529
|
-
return null;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
async function sampleFiles(rootDir, maxFiles = 25) {
|
|
533
|
-
const selected = /* @__PURE__ */ new Map();
|
|
534
|
-
const projectConfig = await loadProjectConfig(rootDir);
|
|
535
|
-
const ignorePatterns = [...IGNORE_PATTERNS, ...projectConfig.ignore ?? []];
|
|
536
|
-
const trackedFiles = await getTrackedFiles(rootDir);
|
|
537
|
-
for (const filePath of projectConfig.alwaysInclude ?? []) {
|
|
538
|
-
if (selected.size >= maxFiles) break;
|
|
539
|
-
const resolvedPath = path2.resolve(rootDir, filePath);
|
|
540
|
-
if (!resolvedPath.startsWith(path2.resolve(rootDir))) continue;
|
|
541
|
-
selected.set(filePath, "always-include (project config)");
|
|
542
|
-
}
|
|
543
|
-
let configCount = 0;
|
|
544
|
-
for (const pattern of CONFIG_FILES) {
|
|
545
|
-
if (configCount >= 5) break;
|
|
546
|
-
const matches = await fg2(pattern, {
|
|
547
|
-
cwd: rootDir,
|
|
548
|
-
ignore: ignorePatterns,
|
|
549
|
-
deep: 3
|
|
550
|
-
});
|
|
551
|
-
for (const match of matches) {
|
|
552
|
-
if (configCount >= 5 || selected.size >= maxFiles) break;
|
|
553
|
-
selected.set(match, "config file");
|
|
554
|
-
configCount++;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
const moduleBuildPatterns = [
|
|
558
|
-
// Gradle
|
|
559
|
-
"**/build.gradle.kts",
|
|
560
|
-
"**/build.gradle",
|
|
561
|
-
// Cargo workspace members
|
|
562
|
-
"**/Cargo.toml",
|
|
563
|
-
// Node workspaces
|
|
564
|
-
"**/package.json",
|
|
565
|
-
// Go sub-modules
|
|
566
|
-
"**/go.mod"
|
|
567
|
-
];
|
|
568
|
-
let moduleBuildCount = 0;
|
|
569
|
-
for (const pattern of moduleBuildPatterns) {
|
|
570
|
-
const matches = await fg2(pattern, {
|
|
571
|
-
cwd: rootDir,
|
|
572
|
-
ignore: ignorePatterns,
|
|
573
|
-
deep: 4
|
|
574
|
-
});
|
|
575
|
-
const subMatches = matches.filter((m) => m.includes("/"));
|
|
576
|
-
for (const match of subMatches) {
|
|
577
|
-
if (moduleBuildCount >= 4 || selected.size >= maxFiles) break;
|
|
578
|
-
if (!selected.has(match)) {
|
|
579
|
-
selected.set(match, "module build file (reveals dependency graph)");
|
|
580
|
-
moduleBuildCount++;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
if (moduleBuildCount >= 4) break;
|
|
584
|
-
}
|
|
585
|
-
let entryCount = 0;
|
|
586
|
-
for (const pattern of ENTRY_POINT_PATTERNS) {
|
|
587
|
-
if (entryCount >= 2) break;
|
|
588
|
-
const matches = await fg2(pattern, {
|
|
589
|
-
cwd: rootDir,
|
|
590
|
-
ignore: ignorePatterns,
|
|
591
|
-
deep: 5
|
|
592
|
-
});
|
|
593
|
-
for (const match of matches) {
|
|
594
|
-
if (entryCount >= 2 || selected.size >= maxFiles) break;
|
|
595
|
-
if (!selected.has(match)) {
|
|
596
|
-
selected.set(match, "entry point");
|
|
597
|
-
entryCount++;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
try {
|
|
602
|
-
const { stdout } = await exec5(
|
|
603
|
-
"git",
|
|
604
|
-
["log", "--since=3 months ago", "--format=", "--name-only"],
|
|
605
|
-
{ cwd: rootDir, maxBuffer: 5e6 }
|
|
606
|
-
);
|
|
607
|
-
const fileCounts = /* @__PURE__ */ new Map();
|
|
608
|
-
for (const line of stdout.split("\n")) {
|
|
609
|
-
if (!line) continue;
|
|
610
|
-
if (line.includes("node_modules") || line.includes("/build/") || line.includes(".gradle") || line.includes("/generated/"))
|
|
611
|
-
continue;
|
|
612
|
-
const ext = path2.extname(line).slice(1);
|
|
613
|
-
if (!SOURCE_EXTENSIONS.includes(ext)) continue;
|
|
614
|
-
fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
|
|
615
|
-
}
|
|
616
|
-
const hotFiles = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
617
|
-
for (const [file, count] of hotFiles) {
|
|
618
|
-
if (selected.size >= maxFiles) break;
|
|
619
|
-
if (!selected.has(file)) {
|
|
620
|
-
selected.set(file, `frequently changed (${count} commits in 3 months)`);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
} catch {
|
|
624
|
-
}
|
|
625
|
-
const seenCategories = /* @__PURE__ */ new Set();
|
|
626
|
-
let patternCount = 0;
|
|
627
|
-
for (const pattern of ARCHITECTURAL_PATTERNS) {
|
|
628
|
-
if (patternCount >= 8 || selected.size >= maxFiles) break;
|
|
629
|
-
if (seenCategories.has(pattern.category)) continue;
|
|
630
|
-
const matches = await fg2(pattern.glob, {
|
|
631
|
-
cwd: rootDir,
|
|
632
|
-
ignore: ignorePatterns
|
|
633
|
-
});
|
|
634
|
-
if (matches.length > 0) {
|
|
635
|
-
for (const match of matches) {
|
|
636
|
-
if (!selected.has(match)) {
|
|
637
|
-
selected.set(match, pattern.reason);
|
|
638
|
-
seenCategories.add(pattern.category);
|
|
639
|
-
patternCount++;
|
|
640
|
-
break;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
for (const customGlob of projectConfig.patterns ?? []) {
|
|
646
|
-
if (selected.size >= maxFiles) break;
|
|
647
|
-
const matches = await fg2(customGlob, {
|
|
648
|
-
cwd: rootDir,
|
|
649
|
-
ignore: ignorePatterns
|
|
650
|
-
});
|
|
651
|
-
for (const match of matches) {
|
|
652
|
-
if (selected.size >= maxFiles) break;
|
|
653
|
-
if (!selected.has(match)) {
|
|
654
|
-
selected.set(match, "custom pattern (project config)");
|
|
655
|
-
break;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
const testPatternGroups = [
|
|
660
|
-
// JS/TS tests
|
|
661
|
-
{ patterns: ["**/*.test.*", "**/*.spec.*"], label: "JS/TS test" },
|
|
662
|
-
// JVM tests
|
|
663
|
-
{ patterns: ["**/*Test.kt", "**/*Test.java"], label: "JVM test" },
|
|
664
|
-
// Python tests
|
|
665
|
-
{ patterns: ["**/test_*.py", "**/*_test.py"], label: "Python test" },
|
|
666
|
-
// Go tests
|
|
667
|
-
{ patterns: ["**/*_test.go"], label: "Go test" },
|
|
668
|
-
// Swift tests
|
|
669
|
-
{ patterns: ["**/*Tests.swift", "**/*Test.swift"], label: "Swift test" },
|
|
670
|
-
// Rust tests
|
|
671
|
-
{ patterns: ["**/*_test.rs"], label: "Rust test" }
|
|
672
|
-
];
|
|
673
|
-
let testCount = 0;
|
|
674
|
-
for (const group of testPatternGroups) {
|
|
675
|
-
if (testCount >= 3 || selected.size >= maxFiles) break;
|
|
676
|
-
const testFiles = await fg2(group.patterns, {
|
|
677
|
-
cwd: rootDir,
|
|
678
|
-
ignore: ignorePatterns
|
|
679
|
-
});
|
|
680
|
-
if (testFiles.length > 0) {
|
|
681
|
-
for (const file of testFiles) {
|
|
682
|
-
if (!selected.has(file)) {
|
|
683
|
-
selected.set(file, `test example (${group.label})`);
|
|
684
|
-
testCount++;
|
|
685
|
-
break;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
const sourceGlobs = SOURCE_EXTENSIONS.map((ext) => `**/*.${ext}`);
|
|
691
|
-
const allSourceFiles = await fg2(sourceGlobs, {
|
|
692
|
-
cwd: rootDir,
|
|
693
|
-
ignore: ignorePatterns
|
|
694
|
-
});
|
|
695
|
-
const dirRepresentatives = /* @__PURE__ */ new Map();
|
|
696
|
-
const boringFiles = /\.(gradle|gradle\.kts|json|toml|yaml|yml|xml|properties)$/;
|
|
697
|
-
for (const file of allSourceFiles) {
|
|
698
|
-
const topDir = file.split("/")[0];
|
|
699
|
-
if (!dirRepresentatives.has(topDir) && !boringFiles.test(file)) {
|
|
700
|
-
dirRepresentatives.set(topDir, file);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
for (const [, file] of dirRepresentatives) {
|
|
704
|
-
if (selected.size >= maxFiles) break;
|
|
705
|
-
if (!selected.has(file)) {
|
|
706
|
-
selected.set(file, "directory representative");
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
const results = [];
|
|
710
|
-
for (const [filePath, reason] of selected) {
|
|
711
|
-
try {
|
|
712
|
-
const fullPath = path2.resolve(rootDir, filePath);
|
|
713
|
-
if (!fullPath.startsWith(path2.resolve(rootDir))) continue;
|
|
714
|
-
if (isSensitiveFile(filePath)) continue;
|
|
715
|
-
if (trackedFiles && !trackedFiles.has(filePath)) continue;
|
|
716
|
-
const stat = await fs3.stat(fullPath);
|
|
717
|
-
if (stat.size > 1e5) continue;
|
|
718
|
-
const content = await fs3.readFile(fullPath, "utf-8");
|
|
719
|
-
const lines = content.split("\n");
|
|
720
|
-
const preview = lines.slice(0, PREVIEW_LINES).join("\n");
|
|
721
|
-
results.push({
|
|
722
|
-
path: filePath,
|
|
723
|
-
preview,
|
|
724
|
-
totalLines: lines.length,
|
|
725
|
-
sizeBytes: stat.size,
|
|
726
|
-
reason
|
|
727
|
-
});
|
|
728
|
-
} catch {
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
return results;
|
|
732
|
-
}
|
|
733
|
-
function isSensitiveFile(filePath) {
|
|
734
|
-
const basename = path2.basename(filePath);
|
|
735
|
-
return SENSITIVE_PATTERNS.some((p) => p.test(basename));
|
|
736
|
-
}
|
|
737
|
-
async function readFullFile(rootDir, filePath) {
|
|
738
|
-
try {
|
|
739
|
-
const fullPath = path2.join(path2.resolve(rootDir), filePath);
|
|
740
|
-
if (!fullPath.startsWith(path2.resolve(rootDir))) return null;
|
|
741
|
-
if (isSensitiveFile(filePath)) return null;
|
|
742
|
-
const content = await fs3.readFile(fullPath, "utf-8");
|
|
743
|
-
return {
|
|
744
|
-
path: filePath,
|
|
745
|
-
content,
|
|
746
|
-
totalLines: content.split("\n").length
|
|
747
|
-
};
|
|
748
|
-
} catch {
|
|
749
|
-
return null;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
var exec5, SOURCE_EXTENSIONS, CONFIG_FILES, ENTRY_POINT_PATTERNS, ARCHITECTURAL_PATTERNS, IGNORE_PATTERNS, PREVIEW_LINES, SENSITIVE_PATTERNS;
|
|
753
|
-
var init_sampler = __esm({
|
|
754
|
-
"src/mcp/sampler.ts"() {
|
|
755
|
-
"use strict";
|
|
756
|
-
exec5 = promisify5(execFile5);
|
|
757
|
-
SOURCE_EXTENSIONS = [
|
|
758
|
-
"ts",
|
|
759
|
-
"tsx",
|
|
760
|
-
"js",
|
|
761
|
-
"jsx",
|
|
762
|
-
"mts",
|
|
763
|
-
"mjs",
|
|
764
|
-
"kt",
|
|
765
|
-
"kts",
|
|
766
|
-
"java",
|
|
767
|
-
"py",
|
|
768
|
-
"go",
|
|
769
|
-
"rs",
|
|
770
|
-
"swift",
|
|
771
|
-
"rb",
|
|
772
|
-
"cs",
|
|
773
|
-
"cpp",
|
|
774
|
-
"c",
|
|
775
|
-
"h",
|
|
776
|
-
"dart"
|
|
777
|
-
];
|
|
778
|
-
CONFIG_FILES = [
|
|
779
|
-
// Build & project config
|
|
780
|
-
"package.json",
|
|
781
|
-
"tsconfig.json",
|
|
782
|
-
"build.gradle.kts",
|
|
783
|
-
"build.gradle",
|
|
784
|
-
"settings.gradle.kts",
|
|
785
|
-
"settings.gradle",
|
|
786
|
-
"Cargo.toml",
|
|
787
|
-
"go.mod",
|
|
788
|
-
"pyproject.toml",
|
|
789
|
-
"Gemfile",
|
|
790
|
-
"*.csproj",
|
|
791
|
-
// Version catalogs & dependency locks
|
|
792
|
-
"gradle/libs.versions.toml",
|
|
793
|
-
// Code quality & formatting
|
|
794
|
-
".editorconfig",
|
|
795
|
-
".eslintrc.*",
|
|
796
|
-
"eslint.config.*",
|
|
797
|
-
".prettierrc",
|
|
798
|
-
"rustfmt.toml",
|
|
799
|
-
".swiftlint.yml",
|
|
800
|
-
// CI/CD
|
|
801
|
-
".github/workflows/*.yml",
|
|
802
|
-
".gitlab-ci.yml",
|
|
803
|
-
"Jenkinsfile",
|
|
804
|
-
// Containerization
|
|
805
|
-
"Dockerfile",
|
|
806
|
-
"docker-compose.yml",
|
|
807
|
-
"docker-compose.yaml"
|
|
808
|
-
];
|
|
809
|
-
ENTRY_POINT_PATTERNS = [
|
|
810
|
-
"src/main.*",
|
|
811
|
-
"src/index.*",
|
|
812
|
-
"src/app.*",
|
|
813
|
-
"main.*",
|
|
814
|
-
"index.*",
|
|
815
|
-
"app.*",
|
|
816
|
-
"App.*",
|
|
817
|
-
"**/Main.kt",
|
|
818
|
-
"**/Application.kt",
|
|
819
|
-
"**/main.py",
|
|
820
|
-
"**/main.go",
|
|
821
|
-
"**/main.rs",
|
|
822
|
-
"**/lib.rs",
|
|
823
|
-
"**/Program.cs"
|
|
824
|
-
];
|
|
825
|
-
ARCHITECTURAL_PATTERNS = [
|
|
826
|
-
// State/data flow
|
|
827
|
-
{ glob: "**/*ViewModel.*", category: "state", reason: "viewmodel (state management)" },
|
|
828
|
-
{ glob: "**/*Store.*", category: "state", reason: "store (state management)" },
|
|
829
|
-
{ glob: "**/*Reducer.*", category: "state", reason: "reducer (state management)" },
|
|
830
|
-
// Data layer — interface
|
|
831
|
-
{ glob: "**/*Repository.*", category: "data-interface", reason: "repository interface (data layer contract)" },
|
|
832
|
-
{ glob: "**/*Dao.*", category: "data-interface", reason: "DAO (data access)" },
|
|
833
|
-
{ glob: "**/*DataSource.*", category: "data-interface", reason: "data source" },
|
|
834
|
-
// Data layer — implementation (where actual patterns live: mappers, retry, IO dispatchers)
|
|
835
|
-
{ glob: "**/*RepositoryImpl.*", category: "data-impl", reason: "repository implementation (data layer patterns)" },
|
|
836
|
-
{ glob: "**/*ServiceImpl.*", category: "data-impl", reason: "service implementation" },
|
|
837
|
-
{ glob: "**/*Impl.*", category: "data-impl", reason: "implementation (concrete patterns)" },
|
|
838
|
-
// Data transformation
|
|
839
|
-
{ glob: "**/*Mapper.*", category: "transform", reason: "mapper (data transformation)" },
|
|
840
|
-
{ glob: "**/*Converter.*", category: "transform", reason: "converter (data transformation)" },
|
|
841
|
-
{ glob: "**/*Adapter.*", category: "transform", reason: "adapter (interface adaptation)" },
|
|
842
|
-
// Dependency injection / wiring
|
|
843
|
-
{ glob: "**/*Module.*", category: "di", reason: "module (DI/wiring)" },
|
|
844
|
-
{ glob: "**/*Provider.*", category: "di", reason: "provider (DI/wiring)" },
|
|
845
|
-
{ glob: "**/*Container.*", category: "di", reason: "container (DI/wiring)" },
|
|
846
|
-
{ glob: "**/*Factory.*", category: "di", reason: "factory (object creation)" },
|
|
847
|
-
// API / network
|
|
848
|
-
{ glob: "**/*Service.*", category: "api", reason: "service (business/API layer)" },
|
|
849
|
-
{ glob: "**/*Client.*", category: "api", reason: "client (API/network layer)" },
|
|
850
|
-
{ glob: "**/*Api.*", category: "api", reason: "API interface definition" },
|
|
851
|
-
// Interface contracts / protocols
|
|
852
|
-
{ glob: "**/*Interface.*", category: "contract", reason: "interface definition" },
|
|
853
|
-
{ glob: "**/*Protocol.*", category: "contract", reason: "protocol definition" },
|
|
854
|
-
{ glob: "**/*Trait.*", category: "contract", reason: "trait definition" },
|
|
855
|
-
// Routing / navigation
|
|
856
|
-
{ glob: "**/*Router.*", category: "routing", reason: "router (navigation/routing)" },
|
|
857
|
-
{ glob: "**/*Route.*", category: "routing", reason: "route definition" },
|
|
858
|
-
{ glob: "**/*NavHost.*", category: "routing", reason: "navigation host" },
|
|
859
|
-
{ glob: "**/*Controller.*", category: "routing", reason: "controller (request handling)" },
|
|
860
|
-
{ glob: "**/*Handler.*", category: "routing", reason: "handler (request handling)" },
|
|
861
|
-
// Middleware / interceptors
|
|
862
|
-
{ glob: "**/*Middleware.*", category: "middleware", reason: "middleware (request pipeline)" },
|
|
863
|
-
{ glob: "**/*Interceptor.*", category: "middleware", reason: "interceptor (cross-cutting)" },
|
|
864
|
-
{ glob: "**/*Plugin.*", category: "middleware", reason: "plugin (extensibility)" },
|
|
865
|
-
// Models / types
|
|
866
|
-
{ glob: "**/*Model.*", category: "model", reason: "model (domain types)" },
|
|
867
|
-
{ glob: "**/*Entity.*", category: "model", reason: "entity (persistence types)" },
|
|
868
|
-
{ glob: "**/*Dto.*", category: "model", reason: "DTO (data transfer types)" },
|
|
869
|
-
{ glob: "**/*Schema.*", category: "model", reason: "schema (data validation)" },
|
|
870
|
-
// Use cases / commands
|
|
871
|
-
{ glob: "**/*UseCase.*", category: "usecase", reason: "use case (business logic)" },
|
|
872
|
-
{ glob: "**/*Interactor.*", category: "usecase", reason: "interactor (business logic)" },
|
|
873
|
-
{ glob: "**/*Command.*", category: "usecase", reason: "command (CQRS pattern)" }
|
|
874
|
-
];
|
|
875
|
-
IGNORE_PATTERNS = [
|
|
876
|
-
"**/node_modules/**",
|
|
877
|
-
"**/dist/**",
|
|
878
|
-
"**/build/**",
|
|
879
|
-
"**/.gradle/**",
|
|
880
|
-
"**/target/**",
|
|
881
|
-
"**/.git/**",
|
|
882
|
-
"**/vendor/**",
|
|
883
|
-
"**/__pycache__/**",
|
|
884
|
-
"**/venv/**",
|
|
885
|
-
"**/.venv/**",
|
|
886
|
-
"**/*.min.*",
|
|
887
|
-
"**/*.map",
|
|
888
|
-
"**/package-lock.json",
|
|
889
|
-
"**/yarn.lock",
|
|
890
|
-
"**/pnpm-lock.yaml",
|
|
891
|
-
"**/*.lock",
|
|
892
|
-
"**/*.generated.*",
|
|
893
|
-
"**/generated/**",
|
|
894
|
-
"**/R.java",
|
|
895
|
-
"**/BuildConfig.java"
|
|
896
|
-
];
|
|
897
|
-
PREVIEW_LINES = 60;
|
|
898
|
-
SENSITIVE_PATTERNS = [
|
|
899
|
-
/^\.env$/,
|
|
900
|
-
/^\.env\./,
|
|
901
|
-
/\.pem$/,
|
|
902
|
-
/\.key$/,
|
|
903
|
-
/\.p12$/,
|
|
904
|
-
/\.pfx$/,
|
|
905
|
-
/\.jks$/,
|
|
906
|
-
/id_rsa/,
|
|
907
|
-
/id_ed25519/,
|
|
908
|
-
/credentials\./,
|
|
909
|
-
/secret/i,
|
|
910
|
-
/\.keystore$/,
|
|
911
|
-
/local\.properties$/
|
|
912
|
-
];
|
|
913
|
-
}
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
// src/test-map.ts
|
|
917
|
-
var test_map_exports = {};
|
|
918
|
-
__export(test_map_exports, {
|
|
919
|
-
buildTestMap: () => buildTestMap
|
|
920
|
-
});
|
|
921
|
-
import path3 from "path";
|
|
922
|
-
import fg3 from "fast-glob";
|
|
923
|
-
async function buildTestMap(dir) {
|
|
924
|
-
const rootDir = path3.resolve(dir);
|
|
925
|
-
const testPatterns = [
|
|
926
|
-
"**/*.test.*",
|
|
927
|
-
"**/*.spec.*",
|
|
928
|
-
"**/*Test.kt",
|
|
929
|
-
"**/*Test.java",
|
|
930
|
-
"**/*Tests.kt",
|
|
931
|
-
"**/*Tests.java",
|
|
932
|
-
"**/test_*.py",
|
|
933
|
-
"**/*_test.py",
|
|
934
|
-
"**/*_test.go",
|
|
935
|
-
"**/*Tests.swift",
|
|
936
|
-
"**/*Test.swift",
|
|
937
|
-
"**/*_test.rs"
|
|
938
|
-
];
|
|
939
|
-
const testFiles = await fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
|
|
940
|
-
const sourceFiles = await fg3(
|
|
941
|
-
"**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}",
|
|
942
|
-
{ cwd: rootDir, ignore: IGNORE }
|
|
943
|
-
);
|
|
944
|
-
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
945
|
-
for (const file of sourceFiles) {
|
|
946
|
-
if (testFiles.includes(file)) continue;
|
|
947
|
-
const baseName = path3.basename(file).replace(/\.[^.]+$/, "");
|
|
948
|
-
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
949
|
-
existing.push(file);
|
|
950
|
-
sourceByBaseName.set(baseName, existing);
|
|
951
|
-
}
|
|
952
|
-
const paired = [];
|
|
953
|
-
const unmatched = [];
|
|
954
|
-
for (const testFile of testFiles) {
|
|
955
|
-
const testBaseName = path3.basename(testFile).replace(/\.[^.]+$/, "");
|
|
956
|
-
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
957
|
-
if (!sourceName) {
|
|
958
|
-
unmatched.push(testFile);
|
|
959
|
-
continue;
|
|
960
|
-
}
|
|
961
|
-
const candidates = sourceByBaseName.get(sourceName);
|
|
962
|
-
if (candidates && candidates.length > 0) {
|
|
963
|
-
const testDir = path3.dirname(testFile);
|
|
964
|
-
const bestMatch = candidates.reduce((best, candidate) => {
|
|
965
|
-
const candidateDir = path3.dirname(candidate);
|
|
966
|
-
const bestDir = path3.dirname(best);
|
|
967
|
-
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
968
|
-
const bestOverlap = commonSegments(testDir, bestDir);
|
|
969
|
-
return candidateOverlap > bestOverlap ? candidate : best;
|
|
970
|
-
});
|
|
971
|
-
paired.push({
|
|
972
|
-
test: testFile,
|
|
973
|
-
source: bestMatch,
|
|
974
|
-
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
975
|
-
});
|
|
976
|
-
} else {
|
|
977
|
-
unmatched.push(testFile);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
return { totalTestFiles: testFiles.length, paired, unmatched };
|
|
981
|
-
}
|
|
982
|
-
function commonSegments(pathA, pathB) {
|
|
983
|
-
const segsA = pathA.split("/");
|
|
984
|
-
const segsB = pathB.split("/");
|
|
985
|
-
let count = 0;
|
|
986
|
-
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
987
|
-
if (segsA[i] === segsB[i]) count++;
|
|
988
|
-
else break;
|
|
989
|
-
}
|
|
990
|
-
return count;
|
|
991
|
-
}
|
|
992
|
-
var IGNORE;
|
|
993
|
-
var init_test_map = __esm({
|
|
994
|
-
"src/test-map.ts"() {
|
|
995
|
-
"use strict";
|
|
996
|
-
IGNORE = [
|
|
997
|
-
"**/node_modules/**",
|
|
998
|
-
"**/dist/**",
|
|
999
|
-
"**/build/**",
|
|
1000
|
-
"**/.gradle/**",
|
|
1001
|
-
"**/target/**",
|
|
1002
|
-
"**/.git/**",
|
|
1003
|
-
"**/vendor/**",
|
|
1004
|
-
"**/__pycache__/**",
|
|
1005
|
-
"**/venv/**",
|
|
1006
|
-
"**/.venv/**",
|
|
1007
|
-
"**/*.min.*",
|
|
1008
|
-
"**/*.map"
|
|
1009
|
-
];
|
|
1010
|
-
}
|
|
1011
|
-
});
|
|
1012
|
-
|
|
1013
|
-
// src/snapshot/prompt.ts
|
|
1014
|
-
function buildSnapshotPrompt(files, testPairs) {
|
|
1015
|
-
const fileBlocks = files.map(
|
|
1016
|
-
(f) => `=== ${f.path} ===
|
|
1017
|
-
${f.content.slice(0, 3e3)}${f.content.length > 3e3 ? "\n... (truncated)" : ""}`
|
|
1018
|
-
).join("\n\n");
|
|
1019
|
-
let prompt = `Create a concept-to-files map for this codebase. Here are the key source files:
|
|
1020
|
-
|
|
1021
|
-
${fileBlocks}`;
|
|
1022
|
-
if (testPairs && testPairs.length > 0) {
|
|
1023
|
-
const testBlock = testPairs.map((p) => `${p.test} \u2192 ${p.source}`).join("\n");
|
|
1024
|
-
prompt += `
|
|
1025
|
-
|
|
1026
|
-
Here are the test-to-source file mappings. Use these to populate the "tests" field for each feature:
|
|
1027
|
-
|
|
1028
|
-
${testBlock}`;
|
|
1029
|
-
}
|
|
1030
|
-
return prompt;
|
|
1031
|
-
}
|
|
1032
|
-
function buildIncrementalPrompt(files, existingSnapshot) {
|
|
1033
|
-
const fileBlocks = files.map(
|
|
1034
|
-
(f) => `=== ${f.path} ===
|
|
1035
|
-
${f.content.slice(0, 3e3)}${f.content.length > 3e3 ? "\n... (truncated)" : ""}`
|
|
1036
|
-
).join("\n\n");
|
|
1037
|
-
return `Here is the existing concept map for this project:
|
|
1038
|
-
${JSON.stringify(existingSnapshot, null, 2)}
|
|
1039
|
-
|
|
1040
|
-
These files have been added or changed. Update the concept map to incorporate them. Return the FULL updated map (not just the changes).
|
|
1041
|
-
|
|
1042
|
-
Changed/new files:
|
|
1043
|
-
${fileBlocks}`;
|
|
1044
|
-
}
|
|
1045
|
-
var SNAPSHOT_SYSTEM_PROMPT;
|
|
1046
|
-
var init_prompt = __esm({
|
|
1047
|
-
"src/snapshot/prompt.ts"() {
|
|
1048
|
-
"use strict";
|
|
1049
|
-
SNAPSHOT_SYSTEM_PROMPT = `You are Mason, a context engineering tool. You're given source files from a codebase. Your job is to create a concept-to-files map that helps an AI coding assistant instantly find the right files for any task.
|
|
1050
|
-
|
|
1051
|
-
Respond with ONLY a JSON object. No markdown, no explanation, no code fences. Just the raw JSON.
|
|
1052
|
-
|
|
1053
|
-
The JSON must have two keys: "features" and "flows".
|
|
1054
|
-
|
|
1055
|
-
"features" maps user-facing feature names or concepts to the files that implement them. Group files by what a developer would naturally ask about. Use plain language names ("home screen", not "HomeScreenModule").
|
|
1056
|
-
|
|
1057
|
-
"flows" maps data/action flows to ordered chains of files showing how data moves through the system. These help when someone asks "what happens when X?"
|
|
1058
|
-
|
|
1059
|
-
Example output:
|
|
1060
|
-
{
|
|
1061
|
-
"features": {
|
|
1062
|
-
"user authentication": {
|
|
1063
|
-
"description": "Login, signup, token refresh, and session management",
|
|
1064
|
-
"files": ["src/services/AuthService.ts", "src/middleware/AuthMiddleware.ts", "src/models/User.ts", "src/routes/auth.ts"],
|
|
1065
|
-
"tests": ["tests/auth.test.ts"]
|
|
1066
|
-
},
|
|
1067
|
-
"payment processing": {
|
|
1068
|
-
"description": "Stripe integration for subscriptions and one-time payments",
|
|
1069
|
-
"files": ["src/services/PaymentService.ts", "src/webhooks/stripe.ts", "src/models/Subscription.ts"],
|
|
1070
|
-
"tests": ["tests/payment.test.ts"]
|
|
1071
|
-
}
|
|
1072
|
-
},
|
|
1073
|
-
"flows": {
|
|
1074
|
-
"user login": {
|
|
1075
|
-
"description": "User submits credentials, gets JWT token",
|
|
1076
|
-
"chain": ["src/routes/auth.ts", "src/services/AuthService.ts", "src/models/User.ts"]
|
|
1077
|
-
},
|
|
1078
|
-
"process payment": {
|
|
1079
|
-
"description": "User initiates payment, Stripe charges card, webhook confirms",
|
|
1080
|
-
"chain": ["src/routes/payment.ts", "src/services/PaymentService.ts", "src/webhooks/stripe.ts"]
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
Rules:
|
|
1086
|
-
- Use the FULL relative file paths exactly as given in the input
|
|
1087
|
-
- Group by what a human would naturally ask about, not by technical structure
|
|
1088
|
-
- Each feature should have 2-8 files \u2014 not too granular, not too broad
|
|
1089
|
-
- Flows should show the actual call chain order
|
|
1090
|
-
- Include test files in the "tests" field when they exist
|
|
1091
|
-
- Cover ALL the files you're given \u2014 don't skip any`;
|
|
1092
|
-
}
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
// src/snapshot/snapshot.ts
|
|
1096
|
-
var snapshot_exports = {};
|
|
1097
|
-
__export(snapshot_exports, {
|
|
1098
|
-
createSnapshot: () => createSnapshot,
|
|
1099
|
-
getCurrentGitHash: () => getCurrentGitHash,
|
|
1100
|
-
installHook: () => installHook,
|
|
1101
|
-
loadSnapshot: () => loadSnapshot,
|
|
1102
|
-
saveSnapshot: () => saveSnapshot,
|
|
1103
|
-
updateSnapshot: () => updateSnapshot
|
|
1104
|
-
});
|
|
1105
|
-
import fs4 from "fs/promises";
|
|
1106
|
-
import path4 from "path";
|
|
1107
|
-
import { execFile as execFile6 } from "child_process";
|
|
1108
|
-
import { promisify as promisify6 } from "util";
|
|
1109
|
-
import fg4 from "fast-glob";
|
|
1110
|
-
function snapshotDir(rootDir) {
|
|
1111
|
-
return path4.join(rootDir, ".mason");
|
|
1112
|
-
}
|
|
1113
|
-
function snapshotPath(rootDir) {
|
|
1114
|
-
return path4.join(snapshotDir(rootDir), "snapshot.json");
|
|
1115
|
-
}
|
|
1116
|
-
async function loadSnapshot(rootDir) {
|
|
1117
|
-
try {
|
|
1118
|
-
const raw = await fs4.readFile(snapshotPath(rootDir), "utf-8");
|
|
1119
|
-
const parsed = JSON.parse(raw);
|
|
1120
|
-
if (parsed.version !== 2) return null;
|
|
1121
|
-
return parsed;
|
|
1122
|
-
} catch {
|
|
1123
|
-
return null;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
async function saveSnapshot(rootDir, snapshot) {
|
|
1127
|
-
await fs4.mkdir(snapshotDir(rootDir), { recursive: true });
|
|
1128
|
-
await fs4.writeFile(
|
|
1129
|
-
snapshotPath(rootDir),
|
|
1130
|
-
JSON.stringify(snapshot, null, 2),
|
|
1131
|
-
"utf-8"
|
|
1132
|
-
);
|
|
1133
|
-
}
|
|
1134
|
-
async function getCurrentGitHash(rootDir) {
|
|
1135
|
-
try {
|
|
1136
|
-
const { stdout } = await exec6("git", ["rev-parse", "HEAD"], {
|
|
1137
|
-
cwd: rootDir
|
|
1138
|
-
});
|
|
1139
|
-
return stdout.trim();
|
|
1140
|
-
} catch {
|
|
1141
|
-
return "unknown";
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
function parseSnapshotResponse(raw) {
|
|
1145
|
-
let cleaned = raw.trim();
|
|
1146
|
-
if (cleaned.startsWith("```")) {
|
|
1147
|
-
cleaned = cleaned.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
1148
|
-
}
|
|
1149
|
-
try {
|
|
1150
|
-
const parsed = JSON.parse(cleaned);
|
|
1151
|
-
return {
|
|
1152
|
-
features: parsed.features ?? {},
|
|
1153
|
-
flows: parsed.flows ?? {}
|
|
1154
|
-
};
|
|
1155
|
-
} catch {
|
|
1156
|
-
const match = raw.match(/\{[\s\S]*\}/);
|
|
1157
|
-
if (match) {
|
|
1158
|
-
try {
|
|
1159
|
-
const parsed = JSON.parse(match[0]);
|
|
1160
|
-
return {
|
|
1161
|
-
features: parsed.features ?? {},
|
|
1162
|
-
flows: parsed.flows ?? {}
|
|
1163
|
-
};
|
|
1164
|
-
} catch {
|
|
1165
|
-
return { features: {}, flows: {} };
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
return { features: {}, flows: {} };
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
async function createSnapshot(rootDir, config) {
|
|
1172
|
-
const resolvedRoot = path4.resolve(rootDir);
|
|
1173
|
-
const allFiles = await fg4(
|
|
1174
|
-
"**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}",
|
|
1175
|
-
{
|
|
1176
|
-
cwd: resolvedRoot,
|
|
1177
|
-
ignore: [
|
|
1178
|
-
"**/node_modules/**",
|
|
1179
|
-
"**/dist/**",
|
|
1180
|
-
"**/build/**",
|
|
1181
|
-
"**/.gradle/**",
|
|
1182
|
-
"**/target/**",
|
|
1183
|
-
"**/.git/**",
|
|
1184
|
-
"**/vendor/**",
|
|
1185
|
-
"**/__pycache__/**",
|
|
1186
|
-
"**/venv/**",
|
|
1187
|
-
"**/.venv/**",
|
|
1188
|
-
"**/*.min.*",
|
|
1189
|
-
"**/*.map",
|
|
1190
|
-
"**/generated/**",
|
|
1191
|
-
"**/R.java",
|
|
1192
|
-
"**/BuildConfig.java"
|
|
1193
|
-
]
|
|
1194
|
-
}
|
|
1195
|
-
);
|
|
1196
|
-
const sampleCount = Math.min(80, Math.max(20, Math.round(allFiles.length * 0.15)));
|
|
1197
|
-
const sampled = await sampleFiles(resolvedRoot, sampleCount);
|
|
1198
|
-
const filesWithContent = [];
|
|
1199
|
-
for (const sample of sampled) {
|
|
1200
|
-
const full = await readFullFile(resolvedRoot, sample.path);
|
|
1201
|
-
if (full) {
|
|
1202
|
-
filesWithContent.push({ path: full.path, content: full.content });
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
const gitHash = await getCurrentGitHash(resolvedRoot);
|
|
1206
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1207
|
-
if (filesWithContent.length === 0) {
|
|
1208
|
-
return {
|
|
1209
|
-
version: 2,
|
|
1210
|
-
createdAt: now,
|
|
1211
|
-
updatedAt: now,
|
|
1212
|
-
gitHash,
|
|
1213
|
-
features: {},
|
|
1214
|
-
flows: {}
|
|
1215
|
-
};
|
|
1216
|
-
}
|
|
1217
|
-
const testMap = await buildTestMap(resolvedRoot);
|
|
1218
|
-
const userMessage = buildSnapshotPrompt(filesWithContent, testMap.paired);
|
|
1219
|
-
const result = await callLLM(config, userMessage, SNAPSHOT_SYSTEM_PROMPT);
|
|
1220
|
-
const resultText = typeof result === "string" ? result : result.type === "response" ? result.text : "";
|
|
1221
|
-
if (!resultText) {
|
|
1222
|
-
throw new Error(
|
|
1223
|
-
"No CLI or API key available for this provider. Use claude or ollama (no key needed), or provide an API key."
|
|
1224
|
-
);
|
|
1225
|
-
}
|
|
1226
|
-
const { features, flows } = parseSnapshotResponse(resultText);
|
|
1227
|
-
const snapshot = {
|
|
1228
|
-
version: 2,
|
|
1229
|
-
createdAt: now,
|
|
1230
|
-
updatedAt: now,
|
|
1231
|
-
gitHash,
|
|
1232
|
-
features,
|
|
1233
|
-
flows
|
|
1234
|
-
};
|
|
1235
|
-
await saveSnapshot(resolvedRoot, snapshot);
|
|
1236
|
-
return snapshot;
|
|
1237
|
-
}
|
|
1238
|
-
async function updateSnapshot(rootDir, config) {
|
|
1239
|
-
const resolvedRoot = path4.resolve(rootDir);
|
|
1240
|
-
const existing = await loadSnapshot(resolvedRoot);
|
|
1241
|
-
if (!existing) {
|
|
1242
|
-
const snapshot = await createSnapshot(rootDir, config);
|
|
1243
|
-
const featureCount = Object.keys(snapshot.features).length;
|
|
1244
|
-
const flowCount = Object.keys(snapshot.flows).length;
|
|
1245
|
-
return {
|
|
1246
|
-
status: "created",
|
|
1247
|
-
details: `New snapshot: ${featureCount} features, ${flowCount} flows`
|
|
1248
|
-
};
|
|
1249
|
-
}
|
|
1250
|
-
let changedFiles = [];
|
|
1251
|
-
try {
|
|
1252
|
-
const { stdout } = await exec6(
|
|
1253
|
-
"git",
|
|
1254
|
-
["diff", "--name-only", existing.gitHash, "HEAD"],
|
|
1255
|
-
{ cwd: resolvedRoot }
|
|
1256
|
-
);
|
|
1257
|
-
changedFiles = stdout.trim().split("\n").filter((f) => f.length > 0);
|
|
1258
|
-
} catch {
|
|
1259
|
-
const snapshot = await createSnapshot(rootDir, config);
|
|
1260
|
-
const featureCount = Object.keys(snapshot.features).length;
|
|
1261
|
-
return { status: "rebuilt", details: `${featureCount} features` };
|
|
1262
|
-
}
|
|
1263
|
-
if (changedFiles.length === 0) {
|
|
1264
|
-
return { status: "up-to-date", details: "No changes since last snapshot" };
|
|
1265
|
-
}
|
|
1266
|
-
const sampled = await sampleFiles(resolvedRoot, 30);
|
|
1267
|
-
const sampledPaths = new Set(sampled.map((s) => s.path));
|
|
1268
|
-
const snapshotFiles = /* @__PURE__ */ new Set();
|
|
1269
|
-
for (const feature of Object.values(existing.features)) {
|
|
1270
|
-
for (const f of feature.files) snapshotFiles.add(f);
|
|
1271
|
-
for (const t of feature.tests ?? []) snapshotFiles.add(t);
|
|
1272
|
-
}
|
|
1273
|
-
for (const flow of Object.values(existing.flows)) {
|
|
1274
|
-
for (const f of flow.chain) snapshotFiles.add(f);
|
|
1275
|
-
}
|
|
1276
|
-
const relevantChanges = changedFiles.filter(
|
|
1277
|
-
(f) => sampledPaths.has(f) || snapshotFiles.has(f)
|
|
1278
|
-
);
|
|
1279
|
-
if (relevantChanges.length === 0) {
|
|
1280
|
-
existing.gitHash = await getCurrentGitHash(resolvedRoot);
|
|
1281
|
-
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1282
|
-
await saveSnapshot(resolvedRoot, existing);
|
|
1283
|
-
return {
|
|
1284
|
-
status: "unchanged",
|
|
1285
|
-
details: `${changedFiles.length} files changed but none affect the concept map`
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
const filesWithContent = [];
|
|
1289
|
-
for (const filePath of relevantChanges) {
|
|
1290
|
-
const full = await readFullFile(resolvedRoot, filePath);
|
|
1291
|
-
if (full) {
|
|
1292
|
-
filesWithContent.push({ path: full.path, content: full.content });
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
if (filesWithContent.length === 0) {
|
|
1296
|
-
return { status: "unchanged", details: "Changed files could not be read" };
|
|
1297
|
-
}
|
|
1298
|
-
const userMessage = buildIncrementalPrompt(filesWithContent, {
|
|
1299
|
-
features: existing.features,
|
|
1300
|
-
flows: existing.flows
|
|
1301
|
-
});
|
|
1302
|
-
const result = await callLLM(config, userMessage, SNAPSHOT_SYSTEM_PROMPT);
|
|
1303
|
-
const resultText = typeof result === "string" ? result : result.type === "response" ? result.text : "";
|
|
1304
|
-
if (!resultText) {
|
|
1305
|
-
throw new Error("No CLI or API key available for this provider.");
|
|
1306
|
-
}
|
|
1307
|
-
const { features, flows } = parseSnapshotResponse(resultText);
|
|
1308
|
-
const gitHash = await getCurrentGitHash(resolvedRoot);
|
|
1309
|
-
existing.features = features;
|
|
1310
|
-
existing.flows = flows;
|
|
1311
|
-
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1312
|
-
existing.gitHash = gitHash;
|
|
1313
|
-
await saveSnapshot(resolvedRoot, existing);
|
|
1314
|
-
return {
|
|
1315
|
-
status: "updated",
|
|
1316
|
-
details: `${Object.keys(features).length} features, ${Object.keys(flows).length} flows (${relevantChanges.length} files changed)`
|
|
1317
|
-
};
|
|
1318
|
-
}
|
|
1319
|
-
async function installHook(rootDir) {
|
|
1320
|
-
const resolvedRoot = path4.resolve(rootDir);
|
|
1321
|
-
const hooksDir = path4.join(resolvedRoot, ".git", "hooks");
|
|
1322
|
-
try {
|
|
1323
|
-
await fs4.access(hooksDir);
|
|
1324
|
-
} catch {
|
|
1325
|
-
throw new Error("Not a git repository (no .git/hooks directory)");
|
|
1326
|
-
}
|
|
1327
|
-
const hookPath = path4.join(hooksDir, "post-commit");
|
|
1328
|
-
const hookContent = `#!/bin/sh
|
|
1329
|
-
# Mason: auto-update project snapshot after commit
|
|
1330
|
-
# Runs in background so it doesn't block your workflow
|
|
1331
|
-
mason snapshot-update "$(git rev-parse --show-toplevel)" &
|
|
1332
|
-
`;
|
|
1333
|
-
try {
|
|
1334
|
-
const existing = await fs4.readFile(hookPath, "utf-8");
|
|
1335
|
-
if (existing.includes("mason snapshot-update")) {
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1338
|
-
await fs4.appendFile(hookPath, "\n" + hookContent);
|
|
1339
|
-
} catch {
|
|
1340
|
-
await fs4.writeFile(hookPath, hookContent, { mode: 493 });
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
var exec6;
|
|
1344
|
-
var init_snapshot = __esm({
|
|
1345
|
-
"src/snapshot/snapshot.ts"() {
|
|
1346
|
-
"use strict";
|
|
1347
|
-
init_sampler();
|
|
1348
|
-
init_test_map();
|
|
1349
|
-
init_providers();
|
|
1350
|
-
init_prompt();
|
|
1351
|
-
exec6 = promisify6(execFile6);
|
|
1352
|
-
}
|
|
1353
|
-
});
|
|
1354
|
-
|
|
1355
|
-
// src/impact/impact.ts
|
|
1356
|
-
var impact_exports = {};
|
|
1357
|
-
__export(impact_exports, {
|
|
1358
|
-
analyzeImpact: () => analyzeImpact
|
|
1359
|
-
});
|
|
1360
|
-
import fs5 from "fs/promises";
|
|
1361
|
-
import path5 from "path";
|
|
1362
|
-
import { execFile as execFile7 } from "child_process";
|
|
1363
|
-
import { promisify as promisify7 } from "util";
|
|
1364
|
-
import fg5 from "fast-glob";
|
|
1365
|
-
async function analyzeImpact(rootDir, targetFiles) {
|
|
1366
|
-
const resolvedRoot = path5.resolve(rootDir);
|
|
1367
|
-
const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
|
|
1368
|
-
const [cochange, references, tests] = await Promise.all([
|
|
1369
|
-
getCochangeFiles(resolvedRoot, resolvedTargets),
|
|
1370
|
-
getReferences(resolvedRoot, resolvedTargets),
|
|
1371
|
-
getRelatedTests(resolvedRoot, resolvedTargets)
|
|
1372
|
-
]);
|
|
1373
|
-
return {
|
|
1374
|
-
targetFiles: resolvedTargets,
|
|
1375
|
-
cochange,
|
|
1376
|
-
references,
|
|
1377
|
-
tests
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
async function resolveTargetFiles(rootDir, targets) {
|
|
1381
|
-
const resolved = [];
|
|
1382
|
-
for (const target of targets) {
|
|
1383
|
-
if (target.includes("/")) {
|
|
1384
|
-
resolved.push(target);
|
|
1385
|
-
continue;
|
|
1386
|
-
}
|
|
1387
|
-
const matches = await fg5(`**/${target}`, {
|
|
1388
|
-
cwd: rootDir,
|
|
1389
|
-
ignore: IGNORE2
|
|
1390
|
-
});
|
|
1391
|
-
if (matches.length > 0) {
|
|
1392
|
-
resolved.push(matches[0]);
|
|
1393
|
-
} else {
|
|
1394
|
-
const noExt = target.replace(/\.[^.]+$/, "");
|
|
1395
|
-
const extMatches = await fg5(`**/${noExt}.*`, {
|
|
1396
|
-
cwd: rootDir,
|
|
1397
|
-
ignore: IGNORE2
|
|
1398
|
-
});
|
|
1399
|
-
if (extMatches.length > 0) {
|
|
1400
|
-
resolved.push(extMatches[0]);
|
|
1401
|
-
} else {
|
|
1402
|
-
resolved.push(target);
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
return resolved;
|
|
1407
|
-
}
|
|
1408
|
-
async function getCochangeFiles(rootDir, targetFiles) {
|
|
1409
|
-
const cochangeCounts = /* @__PURE__ */ new Map();
|
|
1410
|
-
let totalTargetCommits = 0;
|
|
1411
|
-
for (const targetFile of targetFiles) {
|
|
1412
|
-
try {
|
|
1413
|
-
const { stdout: commitLog } = await exec7(
|
|
1414
|
-
"git",
|
|
1415
|
-
["log", "--format=%H", "-n", "500", "--", targetFile],
|
|
1416
|
-
{ cwd: rootDir, maxBuffer: 5e6 }
|
|
1417
|
-
);
|
|
1418
|
-
const commits = commitLog.trim().split("\n").filter(Boolean);
|
|
1419
|
-
totalTargetCommits += commits.length;
|
|
1420
|
-
if (commits.length === 0) continue;
|
|
1421
|
-
for (const commit of commits) {
|
|
1422
|
-
try {
|
|
1423
|
-
const { stdout: filesInCommit } = await exec7(
|
|
1424
|
-
"git",
|
|
1425
|
-
["diff-tree", "--no-commit-id", "--name-only", "-r", commit],
|
|
1426
|
-
{ cwd: rootDir }
|
|
1427
|
-
);
|
|
1428
|
-
const files = filesInCommit.trim().split("\n").filter(Boolean);
|
|
1429
|
-
for (const file of files) {
|
|
1430
|
-
if (targetFiles.includes(file)) continue;
|
|
1431
|
-
cochangeCounts.set(file, (cochangeCounts.get(file) ?? 0) + 1);
|
|
1432
|
-
}
|
|
1433
|
-
} catch {
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
} catch {
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
if (totalTargetCommits === 0) return [];
|
|
1440
|
-
return [...cochangeCounts.entries()].map(([file, count]) => ({
|
|
1441
|
-
file,
|
|
1442
|
-
cochangeRate: Math.round(count / totalTargetCommits * 100) / 100,
|
|
1443
|
-
sharedCommits: count
|
|
1444
|
-
})).filter((e) => e.cochangeRate >= 0.3 || e.sharedCommits >= 3).sort((a, b) => b.cochangeRate - a.cochangeRate).slice(0, 20);
|
|
1445
|
-
}
|
|
1446
|
-
async function getReferences(rootDir, targetFiles) {
|
|
1447
|
-
const searchNames = /* @__PURE__ */ new Set();
|
|
1448
|
-
for (const target of targetFiles) {
|
|
1449
|
-
const basename = path5.basename(target).replace(/\.[^.]+$/, "");
|
|
1450
|
-
searchNames.add(basename);
|
|
1451
|
-
}
|
|
1452
|
-
const allSourceFiles = await fg5(`**/${SOURCE_EXTENSIONS2}`, {
|
|
1453
|
-
cwd: rootDir,
|
|
1454
|
-
ignore: IGNORE2
|
|
1455
|
-
});
|
|
1456
|
-
const targetSet = new Set(targetFiles);
|
|
1457
|
-
const filesToSearch = allSourceFiles.filter((f) => !targetSet.has(f));
|
|
1458
|
-
const results = /* @__PURE__ */ new Map();
|
|
1459
|
-
const batchSize = 50;
|
|
1460
|
-
for (let i = 0; i < filesToSearch.length; i += batchSize) {
|
|
1461
|
-
const batch = filesToSearch.slice(i, i + batchSize);
|
|
1462
|
-
await Promise.all(
|
|
1463
|
-
batch.map(async (file) => {
|
|
1464
|
-
try {
|
|
1465
|
-
const content = await fs5.readFile(
|
|
1466
|
-
path5.join(rootDir, file),
|
|
1467
|
-
"utf-8"
|
|
1468
|
-
);
|
|
1469
|
-
for (const name of searchNames) {
|
|
1470
|
-
const regex = new RegExp(`\\b${escapeRegex(name)}\\b`);
|
|
1471
|
-
if (regex.test(content)) {
|
|
1472
|
-
if (!results.has(file)) results.set(file, /* @__PURE__ */ new Set());
|
|
1473
|
-
results.get(file).add(name);
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
} catch {
|
|
1477
|
-
}
|
|
1478
|
-
})
|
|
1479
|
-
);
|
|
1480
|
-
}
|
|
1481
|
-
return [...results.entries()].map(([file, matches]) => ({
|
|
1482
|
-
file,
|
|
1483
|
-
matches: [...matches]
|
|
1484
|
-
})).sort((a, b) => b.matches.length - a.matches.length);
|
|
1485
|
-
}
|
|
1486
|
-
async function getRelatedTests(rootDir, targetFiles) {
|
|
1487
|
-
const testPatterns = [
|
|
1488
|
-
"**/*.test.*",
|
|
1489
|
-
"**/*.spec.*",
|
|
1490
|
-
"**/*Test.kt",
|
|
1491
|
-
"**/*Test.java",
|
|
1492
|
-
"**/*Tests.kt",
|
|
1493
|
-
"**/*Tests.java",
|
|
1494
|
-
"**/test_*.py",
|
|
1495
|
-
"**/*_test.py",
|
|
1496
|
-
"**/*_test.go",
|
|
1497
|
-
"**/*Tests.swift",
|
|
1498
|
-
"**/*Test.swift",
|
|
1499
|
-
"**/*_test.rs"
|
|
1500
|
-
];
|
|
1501
|
-
const testFiles = await fg5(testPatterns, { cwd: rootDir, ignore: IGNORE2 });
|
|
1502
|
-
const results = [];
|
|
1503
|
-
for (const target of targetFiles) {
|
|
1504
|
-
const targetBaseName = path5.basename(target).replace(/\.[^.]+$/, "");
|
|
1505
|
-
for (const testFile of testFiles) {
|
|
1506
|
-
const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
|
|
1507
|
-
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
1508
|
-
if (sourceName === targetBaseName) {
|
|
1509
|
-
results.push({
|
|
1510
|
-
file: testFile,
|
|
1511
|
-
confidence: "exact"
|
|
1512
|
-
});
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
return results;
|
|
1517
|
-
}
|
|
1518
|
-
function escapeRegex(str) {
|
|
1519
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1520
|
-
}
|
|
1521
|
-
var exec7, IGNORE2, SOURCE_EXTENSIONS2;
|
|
1522
|
-
var init_impact = __esm({
|
|
1523
|
-
"src/impact/impact.ts"() {
|
|
1524
|
-
"use strict";
|
|
1525
|
-
exec7 = promisify7(execFile7);
|
|
1526
|
-
IGNORE2 = [
|
|
1527
|
-
"**/node_modules/**",
|
|
1528
|
-
"**/dist/**",
|
|
1529
|
-
"**/build/**",
|
|
1530
|
-
"**/.gradle/**",
|
|
1531
|
-
"**/target/**",
|
|
1532
|
-
"**/.git/**",
|
|
1533
|
-
"**/vendor/**",
|
|
1534
|
-
"**/__pycache__/**",
|
|
1535
|
-
"**/venv/**",
|
|
1536
|
-
"**/.venv/**",
|
|
1537
|
-
"**/generated/**"
|
|
1538
|
-
];
|
|
1539
|
-
SOURCE_EXTENSIONS2 = "*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,h,dart,gradle.kts,gradle}";
|
|
1540
|
-
}
|
|
1541
|
-
});
|
|
1542
|
-
|
|
1543
|
-
// src/mcp/tools.ts
|
|
1544
|
-
import fs6 from "fs/promises";
|
|
1545
|
-
import path6 from "path";
|
|
1546
|
-
import { execFile as execFile8 } from "child_process";
|
|
1547
|
-
import { promisify as promisify8 } from "util";
|
|
1548
|
-
import fg6 from "fast-glob";
|
|
1549
|
-
async function buildContext(dir) {
|
|
1550
|
-
return {
|
|
1551
|
-
rootDir: dir,
|
|
1552
|
-
gitAvailable: await isGitRepo(dir)
|
|
1553
|
-
};
|
|
1554
|
-
}
|
|
1555
|
-
async function analyzeProject(dir) {
|
|
1556
|
-
const rootDir = path6.resolve(dir);
|
|
1557
|
-
const context = await buildContext(rootDir);
|
|
1558
|
-
const results = await runAll(context);
|
|
1559
|
-
const projectSnapshot = await detectProjectSnapshot(rootDir);
|
|
1560
|
-
const output = {
|
|
1561
|
-
project: projectSnapshot,
|
|
1562
|
-
analyzers: results.map((r) => ({
|
|
1563
|
-
name: r.analyzer,
|
|
1564
|
-
durationMs: r.durationMs,
|
|
1565
|
-
findings: r.findings.map((f) => ({
|
|
1566
|
-
category: f.category,
|
|
1567
|
-
confidence: f.confidence,
|
|
1568
|
-
summary: f.summary,
|
|
1569
|
-
evidence: f.evidence,
|
|
1570
|
-
suggestedRule: f.ruleCandidate
|
|
1571
|
-
})),
|
|
1572
|
-
gaps: r.gaps.map((g) => ({
|
|
1573
|
-
question: g.question,
|
|
1574
|
-
context: g.context
|
|
1575
|
-
}))
|
|
1576
|
-
}))
|
|
1577
|
-
};
|
|
1578
|
-
return JSON.stringify(output, null, 2);
|
|
1579
|
-
}
|
|
1580
|
-
async function detectProjectSnapshot(rootDir) {
|
|
1581
|
-
const buildFiles = [
|
|
1582
|
-
"package.json",
|
|
1583
|
-
"tsconfig.json",
|
|
1584
|
-
"build.gradle.kts",
|
|
1585
|
-
"build.gradle",
|
|
1586
|
-
"settings.gradle.kts",
|
|
1587
|
-
"settings.gradle",
|
|
1588
|
-
"gradle/libs.versions.toml",
|
|
1589
|
-
"Cargo.toml",
|
|
1590
|
-
"go.mod",
|
|
1591
|
-
"go.sum",
|
|
1592
|
-
"pyproject.toml",
|
|
1593
|
-
"setup.py",
|
|
1594
|
-
"requirements.txt",
|
|
1595
|
-
"Pipfile",
|
|
1596
|
-
"Gemfile",
|
|
1597
|
-
"Package.swift",
|
|
1598
|
-
"Makefile",
|
|
1599
|
-
"CMakeLists.txt",
|
|
1600
|
-
"Dockerfile",
|
|
1601
|
-
"docker-compose.yml",
|
|
1602
|
-
"docker-compose.yaml",
|
|
1603
|
-
".github/workflows",
|
|
1604
|
-
".gitlab-ci.yml",
|
|
1605
|
-
"Jenkinsfile"
|
|
1606
|
-
];
|
|
1607
|
-
const present = [];
|
|
1608
|
-
for (const file of buildFiles) {
|
|
1609
|
-
try {
|
|
1610
|
-
await fs6.access(path6.join(rootDir, file));
|
|
1611
|
-
present.push(file);
|
|
1612
|
-
} catch {
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
const testDirs = [
|
|
1616
|
-
"test",
|
|
1617
|
-
"tests",
|
|
1618
|
-
"__tests__",
|
|
1619
|
-
"spec",
|
|
1620
|
-
"src/test",
|
|
1621
|
-
"src/tests",
|
|
1622
|
-
"**/src/test",
|
|
1623
|
-
"**/src/androidTest",
|
|
1624
|
-
"**/src/iosTest"
|
|
1625
|
-
];
|
|
1626
|
-
const testInfo = {};
|
|
1627
|
-
for (const pattern of testDirs) {
|
|
1628
|
-
const files = await fg6(`${pattern}/**/*`, {
|
|
1629
|
-
cwd: rootDir,
|
|
1630
|
-
ignore: IGNORE3,
|
|
1631
|
-
onlyFiles: true
|
|
1632
|
-
});
|
|
1633
|
-
if (files.length > 0) {
|
|
1634
|
-
testInfo[pattern] = files.length;
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
const testFilePatterns = [
|
|
1638
|
-
{ pattern: "**/*.test.*", label: "*.test.*" },
|
|
1639
|
-
{ pattern: "**/*.spec.*", label: "*.spec.*" },
|
|
1640
|
-
{ pattern: "**/*Test.kt", label: "*Test.kt" },
|
|
1641
|
-
{ pattern: "**/*Test.java", label: "*Test.java" },
|
|
1642
|
-
{ pattern: "**/test_*.py", label: "test_*.py" },
|
|
1643
|
-
{ pattern: "**/*_test.go", label: "*_test.go" },
|
|
1644
|
-
{ pattern: "**/*Tests.swift", label: "*Tests.swift" },
|
|
1645
|
-
{ pattern: "**/*_test.rs", label: "*_test.rs" }
|
|
1646
|
-
];
|
|
1647
|
-
for (const { pattern, label } of testFilePatterns) {
|
|
1648
|
-
const files = await fg6(pattern, { cwd: rootDir, ignore: IGNORE3 });
|
|
1649
|
-
if (files.length > 0) {
|
|
1650
|
-
testInfo[label] = files.length;
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
const sourceFiles = await fg6("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
|
|
1654
|
-
cwd: rootDir,
|
|
1655
|
-
ignore: IGNORE3
|
|
1656
|
-
});
|
|
1657
|
-
const fileCounts = {};
|
|
1658
|
-
for (const file of sourceFiles) {
|
|
1659
|
-
const ext = path6.extname(file).slice(1);
|
|
1660
|
-
fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
|
|
1661
|
-
}
|
|
1662
|
-
return {
|
|
1663
|
-
configFilesPresent: present,
|
|
1664
|
-
sourceFileCounts: fileCounts,
|
|
1665
|
-
totalSourceFiles: sourceFiles.length,
|
|
1666
|
-
testInfo: Object.keys(testInfo).length > 0 ? testInfo : void 0
|
|
1667
|
-
};
|
|
1668
|
-
}
|
|
1669
|
-
async function getCodeSamples(dir, count = 15) {
|
|
1670
|
-
const rootDir = path6.resolve(dir);
|
|
1671
|
-
const samples = await sampleFiles(rootDir, count);
|
|
1672
|
-
const output = {
|
|
1673
|
-
note: "These are previews (first ~60 lines). Use get_file_content to read the full file if needed.",
|
|
1674
|
-
files: samples.map((s) => ({
|
|
1675
|
-
path: s.path,
|
|
1676
|
-
reason: s.reason,
|
|
1677
|
-
totalLines: s.totalLines,
|
|
1678
|
-
sizeBytes: s.sizeBytes,
|
|
1679
|
-
preview: s.preview
|
|
1680
|
-
}))
|
|
1681
|
-
};
|
|
1682
|
-
return JSON.stringify(output, null, 2);
|
|
1683
|
-
}
|
|
1684
|
-
async function getProjectStructure(dir) {
|
|
1685
|
-
const rootDir = path6.resolve(dir);
|
|
1686
|
-
const allFiles = await fg6("**/*", {
|
|
1687
|
-
cwd: rootDir,
|
|
1688
|
-
ignore: IGNORE3,
|
|
1689
|
-
onlyFiles: true
|
|
1690
|
-
});
|
|
1691
|
-
const dirInfo = /* @__PURE__ */ new Map();
|
|
1692
|
-
for (const file of allFiles) {
|
|
1693
|
-
const parts = file.split("/");
|
|
1694
|
-
for (let depth = 1; depth <= Math.min(parts.length, 2); depth++) {
|
|
1695
|
-
const dirPath = parts.slice(0, depth).join("/");
|
|
1696
|
-
if (!dirInfo.has(dirPath)) {
|
|
1697
|
-
dirInfo.set(dirPath, { fileCount: 0, extensions: /* @__PURE__ */ new Map() });
|
|
1698
|
-
}
|
|
1699
|
-
const info2 = dirInfo.get(dirPath);
|
|
1700
|
-
info2.fileCount++;
|
|
1701
|
-
const ext = path6.extname(file).slice(1);
|
|
1702
|
-
if (ext) {
|
|
1703
|
-
info2.extensions.set(ext, (info2.extensions.get(ext) ?? 0) + 1);
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
const directories = [...dirInfo.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([dirPath, info2]) => {
|
|
1708
|
-
const extensions = {};
|
|
1709
|
-
for (const [ext, count] of info2.extensions) {
|
|
1710
|
-
extensions[ext] = count;
|
|
1711
|
-
}
|
|
1712
|
-
return { path: dirPath, fileCount: info2.fileCount, extensions };
|
|
1713
|
-
});
|
|
1714
|
-
const topLevelFiles = allFiles.filter((f) => !f.includes("/"));
|
|
1715
|
-
const output = {
|
|
1716
|
-
totalFiles: allFiles.length,
|
|
1717
|
-
topLevelFiles,
|
|
1718
|
-
directories
|
|
1719
|
-
};
|
|
1720
|
-
return JSON.stringify(output, null, 2);
|
|
1721
|
-
}
|
|
1722
|
-
async function getTestMap(dir) {
|
|
1723
|
-
const { buildTestMap: buildTestMap2 } = await Promise.resolve().then(() => (init_test_map(), test_map_exports));
|
|
1724
|
-
const result = await buildTestMap2(dir);
|
|
1725
|
-
return JSON.stringify(result, null, 2);
|
|
1726
|
-
}
|
|
1727
|
-
async function getSnapshot(dir) {
|
|
1728
|
-
const rootDir = path6.resolve(dir);
|
|
1729
|
-
const snapshot = await loadSnapshot(rootDir);
|
|
1730
|
-
if (!snapshot) {
|
|
1731
|
-
return JSON.stringify({
|
|
1732
|
-
exists: false,
|
|
1733
|
-
message: "No concept map found. Run 'mason snapshot' to create one, or call save_snapshot with features and flows."
|
|
1734
|
-
});
|
|
1735
|
-
}
|
|
1736
|
-
const currentHash = await getCurrentGitHash(rootDir);
|
|
1737
|
-
const isStale = snapshot.gitHash !== currentHash && snapshot.gitHash !== "unknown";
|
|
1738
|
-
const seenFiles = /* @__PURE__ */ new Set();
|
|
1739
|
-
const compactFeatures = {};
|
|
1740
|
-
for (const [name, feat] of Object.entries(snapshot.features)) {
|
|
1741
|
-
const unique = feat.files.filter((f) => !seenFiles.has(f));
|
|
1742
|
-
if (unique.length === 0) continue;
|
|
1743
|
-
for (const f of unique) seenFiles.add(f);
|
|
1744
|
-
const entry = { files: unique };
|
|
1745
|
-
if (feat.tests && feat.tests.length > 0) {
|
|
1746
|
-
entry.tests = feat.tests;
|
|
1747
|
-
}
|
|
1748
|
-
compactFeatures[name] = entry;
|
|
1749
|
-
}
|
|
1750
|
-
const compactFlows = {};
|
|
1751
|
-
for (const [name, flow] of Object.entries(snapshot.flows)) {
|
|
1752
|
-
compactFlows[name] = flow.chain;
|
|
1753
|
-
}
|
|
1754
|
-
const output = {
|
|
1755
|
-
exists: true,
|
|
1756
|
-
updatedAt: snapshot.updatedAt,
|
|
1757
|
-
features: compactFeatures,
|
|
1758
|
-
flows: compactFlows,
|
|
1759
|
-
stale: isStale
|
|
1760
|
-
};
|
|
1761
|
-
if (isStale) {
|
|
1762
|
-
output.message = "Snapshot is behind HEAD. Run 'mason snapshot-update' or call save_snapshot to refresh.";
|
|
1763
|
-
}
|
|
1764
|
-
return JSON.stringify(output);
|
|
1765
|
-
}
|
|
1766
|
-
async function fullAnalysis(dir) {
|
|
1767
|
-
const rootDir = path6.resolve(dir);
|
|
1768
|
-
const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
|
|
1769
|
-
analyzeProject(dir),
|
|
1770
|
-
getProjectStructure(dir),
|
|
1771
|
-
getCodeSamples(dir, 25),
|
|
1772
|
-
getTestMap(dir),
|
|
1773
|
-
loadSnapshot(rootDir)
|
|
1774
|
-
]);
|
|
1775
|
-
const output = {
|
|
1776
|
-
note: "Full project analysis. Code samples are previews (~60 lines). Use get_file_content to read any file in full.",
|
|
1777
|
-
analysis: JSON.parse(analysis),
|
|
1778
|
-
structure: JSON.parse(structure),
|
|
1779
|
-
codeSamples: JSON.parse(samples),
|
|
1780
|
-
testMap: JSON.parse(testMap)
|
|
1781
|
-
};
|
|
1782
|
-
if (snapshot) {
|
|
1783
|
-
output.conceptMap = {
|
|
1784
|
-
updatedAt: snapshot.updatedAt,
|
|
1785
|
-
features: snapshot.features,
|
|
1786
|
-
flows: snapshot.flows
|
|
1787
|
-
};
|
|
1788
|
-
output.note = "Full project analysis with concept map. The concept map shows which files implement each feature and how data flows through them. Use it to jump straight to relevant files instead of exploring. Use get_file_content to read specific files.";
|
|
1789
|
-
}
|
|
1790
|
-
return JSON.stringify(output, null, 2);
|
|
1791
|
-
}
|
|
1792
|
-
function sanitizePaths(rootDir, files) {
|
|
1793
|
-
return files.filter((f) => {
|
|
1794
|
-
const resolved = path6.resolve(rootDir, f);
|
|
1795
|
-
return resolved.startsWith(rootDir) && !f.startsWith("/") && !f.includes("..");
|
|
1796
|
-
});
|
|
1797
|
-
}
|
|
1798
|
-
async function saveSnapshotData(dir, features, flows) {
|
|
1799
|
-
const rootDir = path6.resolve(dir);
|
|
1800
|
-
const gitHash = await getCurrentGitHash(rootDir);
|
|
1801
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1802
|
-
for (const feat of Object.values(features)) {
|
|
1803
|
-
feat.files = sanitizePaths(rootDir, feat.files);
|
|
1804
|
-
if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
|
|
1805
|
-
}
|
|
1806
|
-
for (const flow of Object.values(flows)) {
|
|
1807
|
-
flow.chain = sanitizePaths(rootDir, flow.chain);
|
|
1808
|
-
}
|
|
1809
|
-
const existing = await loadSnapshot(rootDir);
|
|
1810
|
-
if (existing) {
|
|
1811
|
-
existing.features = { ...existing.features, ...features };
|
|
1812
|
-
existing.flows = { ...existing.flows, ...flows };
|
|
1813
|
-
existing.updatedAt = now;
|
|
1814
|
-
existing.gitHash = gitHash;
|
|
1815
|
-
await saveSnapshot(rootDir, existing);
|
|
1816
|
-
return JSON.stringify({
|
|
1817
|
-
status: "updated",
|
|
1818
|
-
features: Object.keys(existing.features).length,
|
|
1819
|
-
flows: Object.keys(existing.flows).length
|
|
1820
|
-
});
|
|
1821
|
-
}
|
|
1822
|
-
const snapshot = {
|
|
1823
|
-
version: 2,
|
|
1824
|
-
createdAt: now,
|
|
1825
|
-
updatedAt: now,
|
|
1826
|
-
gitHash,
|
|
1827
|
-
features,
|
|
1828
|
-
flows
|
|
1829
|
-
};
|
|
1830
|
-
await saveSnapshot(rootDir, snapshot);
|
|
1831
|
-
return JSON.stringify({
|
|
1832
|
-
status: "created",
|
|
1833
|
-
features: Object.keys(features).length,
|
|
1834
|
-
flows: Object.keys(flows).length
|
|
1835
|
-
});
|
|
1836
|
-
}
|
|
1837
|
-
async function getImpact(dir, files) {
|
|
1838
|
-
const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
|
|
1839
|
-
const rootDir = path6.resolve(dir);
|
|
1840
|
-
const result = await analyzeImpact2(rootDir, files);
|
|
1841
|
-
return JSON.stringify(result, null, 2);
|
|
1842
|
-
}
|
|
1843
|
-
var exec8, IGNORE3;
|
|
1844
|
-
var init_tools = __esm({
|
|
1845
|
-
"src/mcp/tools.ts"() {
|
|
1846
|
-
"use strict";
|
|
1847
|
-
init_analyzers();
|
|
1848
|
-
init_git();
|
|
1849
|
-
init_sampler();
|
|
1850
|
-
init_snapshot();
|
|
1851
|
-
exec8 = promisify8(execFile8);
|
|
1852
|
-
IGNORE3 = [
|
|
1853
|
-
"**/node_modules/**",
|
|
1854
|
-
"**/dist/**",
|
|
1855
|
-
"**/build/**",
|
|
1856
|
-
"**/.gradle/**",
|
|
1857
|
-
"**/target/**",
|
|
1858
|
-
"**/.git/**",
|
|
1859
|
-
"**/vendor/**",
|
|
1860
|
-
"**/__pycache__/**",
|
|
1861
|
-
"**/venv/**",
|
|
1862
|
-
"**/.venv/**",
|
|
1863
|
-
"**/*.min.*",
|
|
1864
|
-
"**/*.map"
|
|
1865
|
-
];
|
|
1866
|
-
}
|
|
1867
|
-
});
|
|
1868
|
-
|
|
1869
|
-
// src/mcp/server.ts
|
|
1870
|
-
var server_exports = {};
|
|
1871
|
-
__export(server_exports, {
|
|
1872
|
-
createMcpServer: () => createMcpServer,
|
|
1873
|
-
startMcpServer: () => startMcpServer
|
|
1874
|
-
});
|
|
1875
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1876
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1877
|
-
import { z } from "zod";
|
|
1878
|
-
function createMcpServer() {
|
|
1879
|
-
const server = new McpServer(
|
|
1880
|
-
{
|
|
1881
|
-
name: "mason",
|
|
1882
|
-
version: "0.3.7"
|
|
1883
|
-
},
|
|
1884
|
-
{
|
|
1885
|
-
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."
|
|
1886
|
-
}
|
|
1887
|
-
);
|
|
1888
|
-
server.tool(
|
|
1889
|
-
"full_analysis",
|
|
1890
|
-
"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.",
|
|
1891
|
-
{
|
|
1892
|
-
dir: z.string().describe("Absolute path to the project root directory")
|
|
1893
|
-
},
|
|
1894
|
-
async ({ dir }) => {
|
|
1895
|
-
const result = await fullAnalysis(dir);
|
|
1896
|
-
return {
|
|
1897
|
-
content: [{ type: "text", text: result }]
|
|
1898
|
-
};
|
|
1899
|
-
}
|
|
1900
|
-
);
|
|
1901
|
-
server.tool(
|
|
1902
|
-
"analyze_project",
|
|
1903
|
-
"Run git history analysis on a codebase. Returns commit convention patterns, stale directories, and frequently changed files. These are aggregate stats across hundreds of commits that would be expensive to compute manually.",
|
|
1904
|
-
{
|
|
1905
|
-
dir: z.string().describe("Absolute path to the project root directory")
|
|
1906
|
-
},
|
|
1907
|
-
async ({ dir }) => {
|
|
1908
|
-
const result = await analyzeProject(dir);
|
|
1909
|
-
return {
|
|
1910
|
-
content: [{ type: "text", text: result }]
|
|
1911
|
-
};
|
|
1912
|
-
}
|
|
1913
|
-
);
|
|
1914
|
-
server.tool(
|
|
1915
|
-
"get_code_samples",
|
|
1916
|
-
"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.",
|
|
1917
|
-
{
|
|
1918
|
-
dir: z.string().describe("Absolute path to the project root directory"),
|
|
1919
|
-
count: z.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
|
|
1920
|
-
},
|
|
1921
|
-
async ({ dir, count }) => {
|
|
1922
|
-
const result = await getCodeSamples(dir, count);
|
|
1923
|
-
return {
|
|
1924
|
-
content: [{ type: "text", text: result }]
|
|
1925
|
-
};
|
|
1926
|
-
}
|
|
1927
|
-
);
|
|
1928
|
-
server.tool(
|
|
1929
|
-
"get_snapshot",
|
|
1930
|
-
"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.",
|
|
1931
|
-
{
|
|
1932
|
-
dir: z.string().describe("Absolute path to the project root directory")
|
|
1933
|
-
},
|
|
1934
|
-
async ({ dir }) => {
|
|
1935
|
-
const result = await getSnapshot(dir);
|
|
1936
|
-
return {
|
|
1937
|
-
content: [{ type: "text", text: result }]
|
|
1938
|
-
};
|
|
1939
|
-
}
|
|
1940
|
-
);
|
|
1941
|
-
server.tool(
|
|
1942
|
-
"save_snapshot",
|
|
1943
|
-
"Save a concept-to-files map as a persistent project snapshot. Maps feature names and data flows to the files that implement them. Persists across conversations \u2014 future sessions can call get_snapshot to instantly find relevant files. No API key needed \u2014 you are the LLM generating the map.",
|
|
1944
|
-
{
|
|
1945
|
-
dir: z.string().describe("Absolute path to the project root directory"),
|
|
1946
|
-
features: z.record(
|
|
1947
|
-
z.object({
|
|
1948
|
-
description: z.string().describe("One-line description of the feature"),
|
|
1949
|
-
files: z.array(z.string()).describe("File paths that implement this feature"),
|
|
1950
|
-
tests: z.array(z.string()).optional().describe("Test file paths for this feature")
|
|
1951
|
-
})
|
|
1952
|
-
).describe("Map of feature names to their implementing files"),
|
|
1953
|
-
flows: z.record(
|
|
1954
|
-
z.object({
|
|
1955
|
-
description: z.string().describe("One-line description of the flow"),
|
|
1956
|
-
chain: z.array(z.string()).describe("Ordered list of file paths showing data/call flow")
|
|
1957
|
-
})
|
|
1958
|
-
).describe("Map of flow names to ordered file chains")
|
|
1959
|
-
},
|
|
1960
|
-
async ({ dir, features, flows }) => {
|
|
1961
|
-
const result = await saveSnapshotData(dir, features, flows);
|
|
1962
|
-
return {
|
|
1963
|
-
content: [{ type: "text", text: result }]
|
|
1964
|
-
};
|
|
1965
|
-
}
|
|
1966
|
-
);
|
|
1967
|
-
server.tool(
|
|
1968
|
-
"get_impact",
|
|
1969
|
-
"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.",
|
|
1970
|
-
{
|
|
1971
|
-
dir: z.string().describe("Absolute path to the project root directory"),
|
|
1972
|
-
files: z.array(z.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
|
|
1973
|
-
},
|
|
1974
|
-
async ({ dir, files }) => {
|
|
1975
|
-
const result = await getImpact(dir, files);
|
|
1976
|
-
return {
|
|
1977
|
-
content: [{ type: "text", text: result }]
|
|
1978
|
-
};
|
|
1979
|
-
}
|
|
1980
|
-
);
|
|
1981
|
-
return server;
|
|
1982
|
-
}
|
|
1983
|
-
async function startMcpServer() {
|
|
1984
|
-
const server = createMcpServer();
|
|
1985
|
-
const transport = new StdioServerTransport();
|
|
1986
|
-
await server.connect(transport);
|
|
1987
|
-
}
|
|
1988
|
-
var init_server = __esm({
|
|
1989
|
-
"src/mcp/server.ts"() {
|
|
1990
|
-
"use strict";
|
|
1991
|
-
init_tools();
|
|
1992
|
-
}
|
|
1993
|
-
});
|
|
1994
|
-
|
|
1995
|
-
// src/cli.ts
|
|
1996
|
-
init_analyzers();
|
|
1997
|
-
init_git();
|
|
1998
|
-
init_config();
|
|
1999
|
-
init_providers();
|
|
2000
|
-
init_tools();
|
|
2001
|
-
import { Command } from "commander";
|
|
2002
|
-
import fs7 from "fs/promises";
|
|
2003
|
-
import path7 from "path";
|
|
2004
|
-
import ora from "ora";
|
|
2005
|
-
import chalk2 from "chalk";
|
|
2006
|
-
|
|
2007
|
-
// src/utils/logger.ts
|
|
2008
|
-
import chalk from "chalk";
|
|
2009
|
-
var verbose = false;
|
|
2010
|
-
function info(msg) {
|
|
2011
|
-
console.log(chalk.blue("\u2139"), msg);
|
|
2012
|
-
}
|
|
2013
|
-
function success(msg) {
|
|
2014
|
-
console.log(chalk.green("\u2714"), msg);
|
|
2015
|
-
}
|
|
2016
|
-
function error(msg) {
|
|
2017
|
-
console.error(chalk.red("\u2716"), msg);
|
|
2018
|
-
}
|
|
2019
|
-
function debug(msg) {
|
|
2020
|
-
if (verbose) {
|
|
2021
|
-
console.log(chalk.gray("\u22EF"), msg);
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
// src/cli.ts
|
|
2026
|
-
async function buildContext2(dir) {
|
|
2027
|
-
return {
|
|
2028
|
-
rootDir: dir,
|
|
2029
|
-
gitAvailable: await isGitRepo(dir)
|
|
2030
|
-
};
|
|
2031
|
-
}
|
|
2032
|
-
function printFindings(results) {
|
|
2033
|
-
for (const result of results) {
|
|
2034
|
-
if (result.findings.length === 0) {
|
|
2035
|
-
debug(`${result.analyzer}: no findings`);
|
|
2036
|
-
continue;
|
|
2037
|
-
}
|
|
2038
|
-
console.log(
|
|
2039
|
-
chalk2.bold(`
|
|
2040
|
-
\u{1F4CB} ${result.analyzer}`) + chalk2.gray(` (${result.durationMs}ms)`)
|
|
2041
|
-
);
|
|
2042
|
-
for (const finding of result.findings) {
|
|
2043
|
-
const conf = chalk2.gray(`[${Math.round(finding.confidence * 100)}%]`);
|
|
2044
|
-
console.log(` ${conf} ${finding.summary}`);
|
|
2045
|
-
for (const ev of finding.evidence) {
|
|
2046
|
-
console.log(chalk2.gray(` ${ev.filePath}: ${ev.detail}`));
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
function extractMarkdown(raw) {
|
|
2052
|
-
const trimmed = raw.trim();
|
|
2053
|
-
if (trimmed.startsWith("# ")) return trimmed;
|
|
2054
|
-
const fenceMatch = trimmed.match(/```(?:markdown|md)?\n([\s\S]*?)```/);
|
|
2055
|
-
if (fenceMatch) return fenceMatch[1].trim();
|
|
2056
|
-
const headingIndex = trimmed.search(/^# /m);
|
|
2057
|
-
if (headingIndex >= 0) return trimmed.slice(headingIndex).trim();
|
|
2058
|
-
const subheadingIndex = trimmed.search(/^## /m);
|
|
2059
|
-
if (subheadingIndex >= 0) return trimmed.slice(subheadingIndex).trim();
|
|
2060
|
-
return trimmed;
|
|
2061
|
-
}
|
|
2062
|
-
function createCLI() {
|
|
2063
|
-
process.on("SIGINT", () => process.exit(130));
|
|
2064
|
-
const program2 = new Command();
|
|
2065
|
-
program2.name("mason").description(
|
|
2066
|
-
"Context engineering CLI & MCP server \u2014 generates intelligent CLAUDE.md files"
|
|
2067
|
-
).version("0.3.7");
|
|
2068
|
-
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) => {
|
|
2069
|
-
const { execFile: execFile9 } = await import("child_process");
|
|
2070
|
-
const { promisify: promisify9 } = await import("util");
|
|
2071
|
-
const exec9 = promisify9(execFile9);
|
|
2072
|
-
try {
|
|
2073
|
-
await exec9("claude", ["--version"]);
|
|
2074
|
-
} catch {
|
|
2075
|
-
error(
|
|
2076
|
-
"Claude Code CLI not found. Install it from https://claude.ai/code"
|
|
2077
|
-
);
|
|
2078
|
-
process.exit(1);
|
|
2079
|
-
}
|
|
2080
|
-
try {
|
|
2081
|
-
const args = [
|
|
2082
|
-
"mcp",
|
|
2083
|
-
"add",
|
|
2084
|
-
"mason",
|
|
2085
|
-
"--scope",
|
|
2086
|
-
opts.scope,
|
|
2087
|
-
"--",
|
|
2088
|
-
"npx",
|
|
2089
|
-
"mason-ai",
|
|
2090
|
-
"mcp"
|
|
2091
|
-
];
|
|
2092
|
-
await exec9("claude", args);
|
|
2093
|
-
success("Mason registered with Claude Code.");
|
|
2094
|
-
info("Restart Claude Code to start using Mason's tools.");
|
|
2095
|
-
} catch (err) {
|
|
2096
|
-
error(
|
|
2097
|
-
`Failed to register: ${err instanceof Error ? err.message : String(err)}`
|
|
2098
|
-
);
|
|
2099
|
-
process.exit(1);
|
|
2100
|
-
}
|
|
2101
|
-
});
|
|
2102
|
-
program2.command("set-llm").description("Configure the LLM provider for standalone generation").argument("<provider>", "LLM provider: claude, gemini, openai, or ollama").argument("[api-key]", "API key (not needed for claude or ollama)").option("--model <model>", "Override the default model").option("--ollama-host <host>", "Ollama server URL", "http://localhost:11434").action(
|
|
2103
|
-
async (provider, apiKey, opts) => {
|
|
2104
|
-
const validProvider = validateProvider(provider);
|
|
2105
|
-
if (needsApiKey(validProvider) && !apiKey) {
|
|
2106
|
-
error(
|
|
2107
|
-
`API key is required for ${validProvider}. Usage: mason set-llm ${validProvider} <api-key>`
|
|
2108
|
-
);
|
|
2109
|
-
process.exit(1);
|
|
2110
|
-
}
|
|
2111
|
-
if (!apiKey && !needsApiKey(validProvider)) {
|
|
2112
|
-
const cli = await detectCLI(validProvider);
|
|
2113
|
-
if (!cli.available) {
|
|
2114
|
-
const hints = {
|
|
2115
|
-
claude: "Claude Code CLI not found. Install it from https://claude.ai/code, or provide an API key: mason set-llm claude <api-key>",
|
|
2116
|
-
gemini: "Gemini CLI not found. Install it from https://ai.google.dev/gemini-api/docs/cli, or provide an API key: mason set-llm gemini <api-key>",
|
|
2117
|
-
ollama: "Ollama not found. Install it from https://ollama.ai"
|
|
2118
|
-
};
|
|
2119
|
-
error(hints[validProvider] ?? "CLI not found for this provider.");
|
|
2120
|
-
process.exit(1);
|
|
2121
|
-
}
|
|
2122
|
-
info(
|
|
2123
|
-
`Found ${validProvider} CLI (${cli.version ?? "installed"}). No API key needed.`
|
|
2124
|
-
);
|
|
2125
|
-
}
|
|
2126
|
-
const config = {
|
|
2127
|
-
provider: validProvider,
|
|
2128
|
-
apiKey,
|
|
2129
|
-
model: opts.model,
|
|
2130
|
-
ollamaHost: validProvider === "ollama" ? opts.ollamaHost : void 0
|
|
2131
|
-
};
|
|
2132
|
-
await saveConfig(config);
|
|
2133
|
-
const model = config.model ?? getDefaultModel(validProvider);
|
|
2134
|
-
success(
|
|
2135
|
-
`Configured ${validProvider} (model: ${model}). Run "mason generate" to create a CLAUDE.md.`
|
|
2136
|
-
);
|
|
2137
|
-
}
|
|
2138
|
-
);
|
|
2139
|
-
program2.command("generate").description("Analyze codebase and generate CLAUDE.md using configured LLM").argument("[dir]", "Directory to analyze", ".").option("--model <model>", "Override the configured model for this run").action(async (dir, opts) => {
|
|
2140
|
-
const config = await loadConfig();
|
|
2141
|
-
if (!config) {
|
|
2142
|
-
error(
|
|
2143
|
-
'No LLM configured. Run "mason set-llm <provider> <api-key>" first.'
|
|
2144
|
-
);
|
|
2145
|
-
process.exit(1);
|
|
2146
|
-
}
|
|
2147
|
-
const rootDir = path7.resolve(dir);
|
|
2148
|
-
const runConfig = opts.model ? { ...config, model: opts.model } : config;
|
|
2149
|
-
const spinner = ora({ discardStdin: false, text: "Analyzing codebase..." }).start();
|
|
2150
|
-
const analysisData = await fullAnalysis(rootDir);
|
|
2151
|
-
spinner.text = `Generating CLAUDE.md with ${runConfig.provider}...`;
|
|
2152
|
-
try {
|
|
2153
|
-
const result = await callLLM(
|
|
2154
|
-
runConfig,
|
|
2155
|
-
`Here is the full project analysis. Write a CLAUDE.md based on this data:
|
|
2156
|
-
|
|
2157
|
-
${analysisData}`
|
|
2158
|
-
);
|
|
2159
|
-
spinner.stop();
|
|
2160
|
-
if (result.type === "prompt") {
|
|
2161
|
-
console.log(
|
|
2162
|
-
chalk2.bold("\nNo API key or CLI available. Copy this prompt into your LLM:\n")
|
|
2163
|
-
);
|
|
2164
|
-
console.log(chalk2.gray("\u2500".repeat(60)));
|
|
2165
|
-
console.log(result.text);
|
|
2166
|
-
console.log(chalk2.gray("\u2500".repeat(60)));
|
|
2167
|
-
console.log(
|
|
2168
|
-
chalk2.gray("\nPaste the LLM's response into CLAUDE.md manually.")
|
|
2169
|
-
);
|
|
2170
|
-
return;
|
|
2171
|
-
}
|
|
2172
|
-
const markdown = extractMarkdown(result.text);
|
|
2173
|
-
if (!markdown.trim()) {
|
|
2174
|
-
error("LLM returned empty response.");
|
|
2175
|
-
process.exit(1);
|
|
2176
|
-
}
|
|
2177
|
-
const claudeDir = path7.join(rootDir, ".claude");
|
|
2178
|
-
await fs7.mkdir(claudeDir, { recursive: true });
|
|
2179
|
-
const outPath = path7.join(claudeDir, "CLAUDE.md");
|
|
2180
|
-
await fs7.writeFile(outPath, markdown, "utf-8");
|
|
2181
|
-
success(`Generated ${outPath}`);
|
|
2182
|
-
try {
|
|
2183
|
-
const { createSnapshot: createSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
2184
|
-
const spinner2 = ora({ discardStdin: false, text: "Building concept map..." }).start();
|
|
2185
|
-
const snapshot = await createSnapshot2(rootDir, runConfig);
|
|
2186
|
-
spinner2.stop();
|
|
2187
|
-
const featureCount = Object.keys(snapshot.features).length;
|
|
2188
|
-
const flowCount = Object.keys(snapshot.flows).length;
|
|
2189
|
-
success(
|
|
2190
|
-
`Concept map created: ${featureCount} features, ${flowCount} flows`
|
|
2191
|
-
);
|
|
2192
|
-
} catch {
|
|
2193
|
-
}
|
|
2194
|
-
const hookPath = path7.join(rootDir, ".git", "hooks", "post-commit");
|
|
2195
|
-
try {
|
|
2196
|
-
const hookContent = await fs7.readFile(hookPath, "utf-8");
|
|
2197
|
-
if (!hookContent.includes("mason snapshot-update")) {
|
|
2198
|
-
console.log(
|
|
2199
|
-
chalk2.gray(
|
|
2200
|
-
'\nTip: Run "mason snapshot --install-hook" to keep the concept map updated automatically.'
|
|
2201
|
-
)
|
|
2202
|
-
);
|
|
2203
|
-
}
|
|
2204
|
-
} catch {
|
|
2205
|
-
console.log(
|
|
2206
|
-
chalk2.gray(
|
|
2207
|
-
'\nTip: Run "mason snapshot --install-hook" to keep the concept map updated automatically.'
|
|
2208
|
-
)
|
|
2209
|
-
);
|
|
2210
|
-
}
|
|
2211
|
-
} catch (err) {
|
|
2212
|
-
spinner.stop();
|
|
2213
|
-
error(
|
|
2214
|
-
`Failed to generate: ${err instanceof Error ? err.message : String(err)}`
|
|
2215
|
-
);
|
|
2216
|
-
process.exit(1);
|
|
2217
|
-
}
|
|
2218
|
-
});
|
|
2219
|
-
program2.command("snapshot").description("Generate a persistent project snapshot using LLM").argument("[dir]", "Directory to analyze", ".").option("--install-hook", "Install a post-commit git hook to auto-update").action(async (dir, opts) => {
|
|
2220
|
-
const {
|
|
2221
|
-
createSnapshot: createSnapshot2,
|
|
2222
|
-
installHook: installHook2
|
|
2223
|
-
} = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
2224
|
-
const rootDir = path7.resolve(dir);
|
|
2225
|
-
if (opts.installHook) {
|
|
2226
|
-
try {
|
|
2227
|
-
await installHook2(rootDir);
|
|
2228
|
-
success("Post-commit hook installed. Snapshot will auto-update on each commit.");
|
|
2229
|
-
} catch (err) {
|
|
2230
|
-
error(
|
|
2231
|
-
`Failed to install hook: ${err instanceof Error ? err.message : String(err)}`
|
|
2232
|
-
);
|
|
2233
|
-
}
|
|
2234
|
-
return;
|
|
2235
|
-
}
|
|
2236
|
-
const config = await loadConfig();
|
|
2237
|
-
if (!config) {
|
|
2238
|
-
error(
|
|
2239
|
-
'No LLM configured. Run "mason set-llm <provider> <api-key>" first.'
|
|
2240
|
-
);
|
|
2241
|
-
process.exit(1);
|
|
2242
|
-
}
|
|
2243
|
-
const spinner = ora({ discardStdin: false, text: "Building project snapshot..." }).start();
|
|
2244
|
-
try {
|
|
2245
|
-
const snapshot = await createSnapshot2(rootDir, config);
|
|
2246
|
-
spinner.stop();
|
|
2247
|
-
const featureCount = Object.keys(snapshot.features).length;
|
|
2248
|
-
const flowCount = Object.keys(snapshot.flows).length;
|
|
2249
|
-
success(
|
|
2250
|
-
`Concept map created: ${featureCount} features, ${flowCount} flows \u2192 .mason/snapshot.json`
|
|
2251
|
-
);
|
|
2252
|
-
} catch (err) {
|
|
2253
|
-
spinner.stop();
|
|
2254
|
-
error(
|
|
2255
|
-
`Failed to create snapshot: ${err instanceof Error ? err.message : String(err)}`
|
|
2256
|
-
);
|
|
2257
|
-
process.exit(1);
|
|
2258
|
-
}
|
|
2259
|
-
});
|
|
2260
|
-
program2.command("snapshot-update").description("Incrementally update snapshot with recent changes").argument("[dir]", "Directory to update", ".").action(async (dir) => {
|
|
2261
|
-
const { updateSnapshot: updateSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
2262
|
-
const rootDir = path7.resolve(dir);
|
|
2263
|
-
const config = await loadConfig();
|
|
2264
|
-
if (!config) return;
|
|
2265
|
-
try {
|
|
2266
|
-
const result = await updateSnapshot2(rootDir, config);
|
|
2267
|
-
if (result.status === "up-to-date" || result.status === "unchanged") {
|
|
2268
|
-
return;
|
|
2269
|
-
}
|
|
2270
|
-
success(`Concept map ${result.status}: ${result.details}`);
|
|
2271
|
-
} catch {
|
|
2272
|
-
}
|
|
2273
|
-
});
|
|
2274
|
-
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) => {
|
|
2275
|
-
const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
|
|
2276
|
-
const rootDir = path7.resolve(opts.dir);
|
|
2277
|
-
const spinner = ora({ discardStdin: false, text: "Analyzing impact..." }).start();
|
|
2278
|
-
const result = await analyzeImpact2(rootDir, files);
|
|
2279
|
-
spinner.stop();
|
|
2280
|
-
console.log(chalk2.bold(`
|
|
2281
|
-
Impact analysis for: ${result.targetFiles.join(", ")}
|
|
2282
|
-
`));
|
|
2283
|
-
if (result.cochange.length > 0) {
|
|
2284
|
-
console.log(chalk2.bold(" Co-change (files that historically change together):"));
|
|
2285
|
-
for (const entry of result.cochange) {
|
|
2286
|
-
const rate = chalk2.gray(`${Math.round(entry.cochangeRate * 100)}%`);
|
|
2287
|
-
console.log(` ${rate} ${entry.file} ${chalk2.gray(`(${entry.sharedCommits} shared commits)`)}`);
|
|
2288
|
-
}
|
|
2289
|
-
console.log();
|
|
2290
|
-
}
|
|
2291
|
-
if (result.references.length > 0) {
|
|
2292
|
-
console.log(chalk2.bold(" References (files that mention the target):"));
|
|
2293
|
-
for (const entry of result.references) {
|
|
2294
|
-
console.log(` ${entry.file} ${chalk2.gray(`[${entry.matches.join(", ")}]`)}`);
|
|
2295
|
-
}
|
|
2296
|
-
console.log();
|
|
2297
|
-
}
|
|
2298
|
-
if (result.tests.length > 0) {
|
|
2299
|
-
console.log(chalk2.bold(" Related tests:"));
|
|
2300
|
-
for (const entry of result.tests) {
|
|
2301
|
-
console.log(` ${entry.file} ${chalk2.gray(`(${entry.confidence})`)}`);
|
|
2302
|
-
}
|
|
2303
|
-
console.log();
|
|
2304
|
-
}
|
|
2305
|
-
const total = result.cochange.length + result.references.length + result.tests.length;
|
|
2306
|
-
if (total === 0) {
|
|
2307
|
-
info("No impact detected \u2014 this file may be independent.");
|
|
2308
|
-
} else {
|
|
2309
|
-
console.log(chalk2.bold(` ${total} related file(s) found`));
|
|
2310
|
-
}
|
|
2311
|
-
});
|
|
2312
|
-
program2.command("analyze").description("Analyze the codebase and print findings").argument("[dir]", "Directory to analyze", ".").action(async (dir) => {
|
|
2313
|
-
const rootDir = path7.resolve(dir);
|
|
2314
|
-
const spinner = ora({ discardStdin: false, text: "Analyzing codebase..." }).start();
|
|
2315
|
-
const context = await buildContext2(rootDir);
|
|
2316
|
-
const results = await runAll(context);
|
|
2317
|
-
spinner.stop();
|
|
2318
|
-
printFindings(results);
|
|
2319
|
-
const totalFindings = results.reduce(
|
|
2320
|
-
(sum, r) => sum + r.findings.length,
|
|
2321
|
-
0
|
|
2322
|
-
);
|
|
2323
|
-
console.log(
|
|
2324
|
-
chalk2.bold(`
|
|
2325
|
-
${totalFindings} findings from ${results.length} analyzers`)
|
|
2326
|
-
);
|
|
2327
|
-
});
|
|
2328
|
-
program2.command("mcp").description("Start the MCP server (stdio transport)").action(async () => {
|
|
2329
|
-
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
2330
|
-
await startMcpServer2();
|
|
2331
|
-
});
|
|
2332
|
-
return program2;
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
|
-
// bin/mason.ts
|
|
2336
|
-
var program = createCLI();
|
|
2337
|
-
program.parse();
|
|
2338
|
-
//# sourceMappingURL=mason.js.map
|