mason-context 0.1.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.
@@ -0,0 +1,2024 @@
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 } 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
+ async function callClaudeCLI(system, userMessage) {
376
+ const fs7 = await import("fs/promises");
377
+ const os2 = await import("os");
378
+ const path6 = await import("path");
379
+ const prompt = `${system}
380
+
381
+ ${userMessage}`;
382
+ const tmpFile = path6.join(os2.tmpdir(), `mason-prompt-${Date.now()}.txt`);
383
+ try {
384
+ await fs7.writeFile(tmpFile, prompt, "utf-8");
385
+ const promptContent = await fs7.readFile(tmpFile, "utf-8");
386
+ const { stdout } = await exec4(
387
+ "sh",
388
+ ["-c", `cat "${tmpFile}" | claude -p`],
389
+ { maxBuffer: 1e7, timeout: 3e5 }
390
+ );
391
+ return stdout.trim();
392
+ } finally {
393
+ await fs7.unlink(tmpFile).catch(() => {
394
+ });
395
+ }
396
+ }
397
+ async function callGeminiCLI(system, userMessage) {
398
+ const fs7 = await import("fs/promises");
399
+ const os2 = await import("os");
400
+ const path6 = await import("path");
401
+ const prompt = `${system}
402
+
403
+ ${userMessage}`;
404
+ const tmpFile = path6.join(os2.tmpdir(), `mason-prompt-${Date.now()}.txt`);
405
+ try {
406
+ await fs7.writeFile(tmpFile, prompt, "utf-8");
407
+ const { stdout } = await exec4(
408
+ "sh",
409
+ ["-c", `cat "${tmpFile}" | gemini -p ""`],
410
+ { maxBuffer: 1e7, timeout: 3e5 }
411
+ );
412
+ return stdout.trim();
413
+ } finally {
414
+ await fs7.unlink(tmpFile).catch(() => {
415
+ });
416
+ }
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 sampleFiles(rootDir, maxFiles = 25) {
522
+ const selected = /* @__PURE__ */ new Map();
523
+ const projectConfig = await loadProjectConfig(rootDir);
524
+ const ignorePatterns = [...IGNORE_PATTERNS, ...projectConfig.ignore ?? []];
525
+ for (const filePath of projectConfig.alwaysInclude ?? []) {
526
+ if (selected.size >= maxFiles) break;
527
+ selected.set(filePath, "always-include (project config)");
528
+ }
529
+ let configCount = 0;
530
+ for (const pattern of CONFIG_FILES) {
531
+ if (configCount >= 5) break;
532
+ const matches = await fg2(pattern, {
533
+ cwd: rootDir,
534
+ ignore: ignorePatterns,
535
+ deep: 3
536
+ });
537
+ for (const match of matches) {
538
+ if (configCount >= 5 || selected.size >= maxFiles) break;
539
+ selected.set(match, "config file");
540
+ configCount++;
541
+ }
542
+ }
543
+ const moduleBuildPatterns = [
544
+ // Gradle
545
+ "**/build.gradle.kts",
546
+ "**/build.gradle",
547
+ // Cargo workspace members
548
+ "**/Cargo.toml",
549
+ // Node workspaces
550
+ "**/package.json",
551
+ // Go sub-modules
552
+ "**/go.mod"
553
+ ];
554
+ let moduleBuildCount = 0;
555
+ for (const pattern of moduleBuildPatterns) {
556
+ const matches = await fg2(pattern, {
557
+ cwd: rootDir,
558
+ ignore: ignorePatterns,
559
+ deep: 4
560
+ });
561
+ const subMatches = matches.filter((m) => m.includes("/"));
562
+ for (const match of subMatches) {
563
+ if (moduleBuildCount >= 4 || selected.size >= maxFiles) break;
564
+ if (!selected.has(match)) {
565
+ selected.set(match, "module build file (reveals dependency graph)");
566
+ moduleBuildCount++;
567
+ }
568
+ }
569
+ if (moduleBuildCount >= 4) break;
570
+ }
571
+ let entryCount = 0;
572
+ for (const pattern of ENTRY_POINT_PATTERNS) {
573
+ if (entryCount >= 2) break;
574
+ const matches = await fg2(pattern, {
575
+ cwd: rootDir,
576
+ ignore: ignorePatterns,
577
+ deep: 5
578
+ });
579
+ for (const match of matches) {
580
+ if (entryCount >= 2 || selected.size >= maxFiles) break;
581
+ if (!selected.has(match)) {
582
+ selected.set(match, "entry point");
583
+ entryCount++;
584
+ }
585
+ }
586
+ }
587
+ try {
588
+ const { stdout } = await exec5(
589
+ "git",
590
+ ["log", "--since=3 months ago", "--format=", "--name-only"],
591
+ { cwd: rootDir, maxBuffer: 5e6 }
592
+ );
593
+ const fileCounts = /* @__PURE__ */ new Map();
594
+ for (const line of stdout.split("\n")) {
595
+ if (!line) continue;
596
+ if (line.includes("node_modules") || line.includes("/build/") || line.includes(".gradle") || line.includes("/generated/"))
597
+ continue;
598
+ const ext = path2.extname(line).slice(1);
599
+ if (!SOURCE_EXTENSIONS.includes(ext)) continue;
600
+ fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
601
+ }
602
+ const hotFiles = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
603
+ for (const [file, count] of hotFiles) {
604
+ if (selected.size >= maxFiles) break;
605
+ if (!selected.has(file)) {
606
+ selected.set(file, `frequently changed (${count} commits in 3 months)`);
607
+ }
608
+ }
609
+ } catch {
610
+ }
611
+ const seenCategories = /* @__PURE__ */ new Set();
612
+ let patternCount = 0;
613
+ for (const pattern of ARCHITECTURAL_PATTERNS) {
614
+ if (patternCount >= 8 || selected.size >= maxFiles) break;
615
+ if (seenCategories.has(pattern.category)) continue;
616
+ const matches = await fg2(pattern.glob, {
617
+ cwd: rootDir,
618
+ ignore: ignorePatterns
619
+ });
620
+ if (matches.length > 0) {
621
+ for (const match of matches) {
622
+ if (!selected.has(match)) {
623
+ selected.set(match, pattern.reason);
624
+ seenCategories.add(pattern.category);
625
+ patternCount++;
626
+ break;
627
+ }
628
+ }
629
+ }
630
+ }
631
+ for (const customGlob of projectConfig.patterns ?? []) {
632
+ if (selected.size >= maxFiles) break;
633
+ const matches = await fg2(customGlob, {
634
+ cwd: rootDir,
635
+ ignore: ignorePatterns
636
+ });
637
+ for (const match of matches) {
638
+ if (selected.size >= maxFiles) break;
639
+ if (!selected.has(match)) {
640
+ selected.set(match, "custom pattern (project config)");
641
+ break;
642
+ }
643
+ }
644
+ }
645
+ const testPatternGroups = [
646
+ // JS/TS tests
647
+ { patterns: ["**/*.test.*", "**/*.spec.*"], label: "JS/TS test" },
648
+ // JVM tests
649
+ { patterns: ["**/*Test.kt", "**/*Test.java"], label: "JVM test" },
650
+ // Python tests
651
+ { patterns: ["**/test_*.py", "**/*_test.py"], label: "Python test" },
652
+ // Go tests
653
+ { patterns: ["**/*_test.go"], label: "Go test" },
654
+ // Swift tests
655
+ { patterns: ["**/*Tests.swift", "**/*Test.swift"], label: "Swift test" },
656
+ // Rust tests
657
+ { patterns: ["**/*_test.rs"], label: "Rust test" }
658
+ ];
659
+ let testCount = 0;
660
+ for (const group of testPatternGroups) {
661
+ if (testCount >= 3 || selected.size >= maxFiles) break;
662
+ const testFiles = await fg2(group.patterns, {
663
+ cwd: rootDir,
664
+ ignore: ignorePatterns
665
+ });
666
+ if (testFiles.length > 0) {
667
+ for (const file of testFiles) {
668
+ if (!selected.has(file)) {
669
+ selected.set(file, `test example (${group.label})`);
670
+ testCount++;
671
+ break;
672
+ }
673
+ }
674
+ }
675
+ }
676
+ const sourceGlobs = SOURCE_EXTENSIONS.map((ext) => `**/*.${ext}`);
677
+ const allSourceFiles = await fg2(sourceGlobs, {
678
+ cwd: rootDir,
679
+ ignore: ignorePatterns
680
+ });
681
+ const dirRepresentatives = /* @__PURE__ */ new Map();
682
+ const boringFiles = /\.(gradle|gradle\.kts|json|toml|yaml|yml|xml|properties)$/;
683
+ for (const file of allSourceFiles) {
684
+ const topDir = file.split("/")[0];
685
+ if (!dirRepresentatives.has(topDir) && !boringFiles.test(file)) {
686
+ dirRepresentatives.set(topDir, file);
687
+ }
688
+ }
689
+ for (const [, file] of dirRepresentatives) {
690
+ if (selected.size >= maxFiles) break;
691
+ if (!selected.has(file)) {
692
+ selected.set(file, "directory representative");
693
+ }
694
+ }
695
+ const results = [];
696
+ for (const [filePath, reason] of selected) {
697
+ try {
698
+ const fullPath = path2.join(rootDir, filePath);
699
+ const stat = await fs3.stat(fullPath);
700
+ if (stat.size > 1e5) continue;
701
+ const content = await fs3.readFile(fullPath, "utf-8");
702
+ const lines = content.split("\n");
703
+ const preview = lines.slice(0, PREVIEW_LINES).join("\n");
704
+ results.push({
705
+ path: filePath,
706
+ preview,
707
+ totalLines: lines.length,
708
+ sizeBytes: stat.size,
709
+ reason
710
+ });
711
+ } catch {
712
+ }
713
+ }
714
+ return results;
715
+ }
716
+ async function readFullFile(rootDir, filePath) {
717
+ try {
718
+ const fullPath = path2.join(path2.resolve(rootDir), filePath);
719
+ if (!fullPath.startsWith(path2.resolve(rootDir))) return null;
720
+ const content = await fs3.readFile(fullPath, "utf-8");
721
+ return {
722
+ path: filePath,
723
+ content,
724
+ totalLines: content.split("\n").length
725
+ };
726
+ } catch {
727
+ return null;
728
+ }
729
+ }
730
+ var exec5, SOURCE_EXTENSIONS, CONFIG_FILES, ENTRY_POINT_PATTERNS, ARCHITECTURAL_PATTERNS, IGNORE_PATTERNS, PREVIEW_LINES;
731
+ var init_sampler = __esm({
732
+ "src/mcp/sampler.ts"() {
733
+ "use strict";
734
+ exec5 = promisify5(execFile5);
735
+ SOURCE_EXTENSIONS = [
736
+ "ts",
737
+ "tsx",
738
+ "js",
739
+ "jsx",
740
+ "mts",
741
+ "mjs",
742
+ "kt",
743
+ "kts",
744
+ "java",
745
+ "py",
746
+ "go",
747
+ "rs",
748
+ "swift",
749
+ "rb",
750
+ "cs",
751
+ "cpp",
752
+ "c",
753
+ "h",
754
+ "dart"
755
+ ];
756
+ CONFIG_FILES = [
757
+ // Build & project config
758
+ "package.json",
759
+ "tsconfig.json",
760
+ "build.gradle.kts",
761
+ "build.gradle",
762
+ "settings.gradle.kts",
763
+ "settings.gradle",
764
+ "Cargo.toml",
765
+ "go.mod",
766
+ "pyproject.toml",
767
+ "Gemfile",
768
+ "*.csproj",
769
+ // Version catalogs & dependency locks
770
+ "gradle/libs.versions.toml",
771
+ // Code quality & formatting
772
+ ".editorconfig",
773
+ ".eslintrc.*",
774
+ "eslint.config.*",
775
+ ".prettierrc",
776
+ "rustfmt.toml",
777
+ ".swiftlint.yml",
778
+ // CI/CD
779
+ ".github/workflows/*.yml",
780
+ ".gitlab-ci.yml",
781
+ "Jenkinsfile",
782
+ // Containerization
783
+ "Dockerfile",
784
+ "docker-compose.yml",
785
+ "docker-compose.yaml"
786
+ ];
787
+ ENTRY_POINT_PATTERNS = [
788
+ "src/main.*",
789
+ "src/index.*",
790
+ "src/app.*",
791
+ "main.*",
792
+ "index.*",
793
+ "app.*",
794
+ "App.*",
795
+ "**/Main.kt",
796
+ "**/Application.kt",
797
+ "**/main.py",
798
+ "**/main.go",
799
+ "**/main.rs",
800
+ "**/lib.rs",
801
+ "**/Program.cs"
802
+ ];
803
+ ARCHITECTURAL_PATTERNS = [
804
+ // State/data flow
805
+ { glob: "**/*ViewModel.*", category: "state", reason: "viewmodel (state management)" },
806
+ { glob: "**/*Store.*", category: "state", reason: "store (state management)" },
807
+ { glob: "**/*Reducer.*", category: "state", reason: "reducer (state management)" },
808
+ // Data layer — interface
809
+ { glob: "**/*Repository.*", category: "data-interface", reason: "repository interface (data layer contract)" },
810
+ { glob: "**/*Dao.*", category: "data-interface", reason: "DAO (data access)" },
811
+ { glob: "**/*DataSource.*", category: "data-interface", reason: "data source" },
812
+ // Data layer — implementation (where actual patterns live: mappers, retry, IO dispatchers)
813
+ { glob: "**/*RepositoryImpl.*", category: "data-impl", reason: "repository implementation (data layer patterns)" },
814
+ { glob: "**/*ServiceImpl.*", category: "data-impl", reason: "service implementation" },
815
+ { glob: "**/*Impl.*", category: "data-impl", reason: "implementation (concrete patterns)" },
816
+ // Data transformation
817
+ { glob: "**/*Mapper.*", category: "transform", reason: "mapper (data transformation)" },
818
+ { glob: "**/*Converter.*", category: "transform", reason: "converter (data transformation)" },
819
+ { glob: "**/*Adapter.*", category: "transform", reason: "adapter (interface adaptation)" },
820
+ // Dependency injection / wiring
821
+ { glob: "**/*Module.*", category: "di", reason: "module (DI/wiring)" },
822
+ { glob: "**/*Provider.*", category: "di", reason: "provider (DI/wiring)" },
823
+ { glob: "**/*Container.*", category: "di", reason: "container (DI/wiring)" },
824
+ { glob: "**/*Factory.*", category: "di", reason: "factory (object creation)" },
825
+ // API / network
826
+ { glob: "**/*Service.*", category: "api", reason: "service (business/API layer)" },
827
+ { glob: "**/*Client.*", category: "api", reason: "client (API/network layer)" },
828
+ { glob: "**/*Api.*", category: "api", reason: "API interface definition" },
829
+ // Interface contracts / protocols
830
+ { glob: "**/*Interface.*", category: "contract", reason: "interface definition" },
831
+ { glob: "**/*Protocol.*", category: "contract", reason: "protocol definition" },
832
+ { glob: "**/*Trait.*", category: "contract", reason: "trait definition" },
833
+ // Routing / navigation
834
+ { glob: "**/*Router.*", category: "routing", reason: "router (navigation/routing)" },
835
+ { glob: "**/*Route.*", category: "routing", reason: "route definition" },
836
+ { glob: "**/*NavHost.*", category: "routing", reason: "navigation host" },
837
+ { glob: "**/*Controller.*", category: "routing", reason: "controller (request handling)" },
838
+ { glob: "**/*Handler.*", category: "routing", reason: "handler (request handling)" },
839
+ // Middleware / interceptors
840
+ { glob: "**/*Middleware.*", category: "middleware", reason: "middleware (request pipeline)" },
841
+ { glob: "**/*Interceptor.*", category: "middleware", reason: "interceptor (cross-cutting)" },
842
+ { glob: "**/*Plugin.*", category: "middleware", reason: "plugin (extensibility)" },
843
+ // Models / types
844
+ { glob: "**/*Model.*", category: "model", reason: "model (domain types)" },
845
+ { glob: "**/*Entity.*", category: "model", reason: "entity (persistence types)" },
846
+ { glob: "**/*Dto.*", category: "model", reason: "DTO (data transfer types)" },
847
+ { glob: "**/*Schema.*", category: "model", reason: "schema (data validation)" },
848
+ // Use cases / commands
849
+ { glob: "**/*UseCase.*", category: "usecase", reason: "use case (business logic)" },
850
+ { glob: "**/*Interactor.*", category: "usecase", reason: "interactor (business logic)" },
851
+ { glob: "**/*Command.*", category: "usecase", reason: "command (CQRS pattern)" }
852
+ ];
853
+ IGNORE_PATTERNS = [
854
+ "**/node_modules/**",
855
+ "**/dist/**",
856
+ "**/build/**",
857
+ "**/.gradle/**",
858
+ "**/target/**",
859
+ "**/.git/**",
860
+ "**/vendor/**",
861
+ "**/__pycache__/**",
862
+ "**/venv/**",
863
+ "**/.venv/**",
864
+ "**/*.min.*",
865
+ "**/*.map",
866
+ "**/package-lock.json",
867
+ "**/yarn.lock",
868
+ "**/pnpm-lock.yaml",
869
+ "**/*.lock",
870
+ "**/*.generated.*",
871
+ "**/generated/**",
872
+ "**/R.java",
873
+ "**/BuildConfig.java"
874
+ ];
875
+ PREVIEW_LINES = 60;
876
+ }
877
+ });
878
+
879
+ // src/snapshot/prompt.ts
880
+ function buildSnapshotPrompt(files) {
881
+ const fileBlocks = files.map(
882
+ (f) => `=== ${f.path} ===
883
+ ${f.content.slice(0, 3e3)}${f.content.length > 3e3 ? "\n... (truncated)" : ""}`
884
+ ).join("\n\n");
885
+ return `Create a concept-to-files map for this codebase. Here are the key source files:
886
+
887
+ ${fileBlocks}`;
888
+ }
889
+ function buildIncrementalPrompt(files, existingSnapshot) {
890
+ const fileBlocks = files.map(
891
+ (f) => `=== ${f.path} ===
892
+ ${f.content.slice(0, 3e3)}${f.content.length > 3e3 ? "\n... (truncated)" : ""}`
893
+ ).join("\n\n");
894
+ return `Here is the existing concept map for this project:
895
+ ${JSON.stringify(existingSnapshot, null, 2)}
896
+
897
+ These files have been added or changed. Update the concept map to incorporate them. Return the FULL updated map (not just the changes).
898
+
899
+ Changed/new files:
900
+ ${fileBlocks}`;
901
+ }
902
+ var SNAPSHOT_SYSTEM_PROMPT;
903
+ var init_prompt = __esm({
904
+ "src/snapshot/prompt.ts"() {
905
+ "use strict";
906
+ 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.
907
+
908
+ Respond with ONLY a JSON object. No markdown, no explanation, no code fences. Just the raw JSON.
909
+
910
+ The JSON must have two keys: "features" and "flows".
911
+
912
+ "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").
913
+
914
+ "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?"
915
+
916
+ Example output:
917
+ {
918
+ "features": {
919
+ "user authentication": {
920
+ "description": "Login, signup, token refresh, and session management",
921
+ "files": ["src/services/AuthService.ts", "src/middleware/AuthMiddleware.ts", "src/models/User.ts", "src/routes/auth.ts"],
922
+ "tests": ["tests/auth.test.ts"]
923
+ },
924
+ "payment processing": {
925
+ "description": "Stripe integration for subscriptions and one-time payments",
926
+ "files": ["src/services/PaymentService.ts", "src/webhooks/stripe.ts", "src/models/Subscription.ts"],
927
+ "tests": ["tests/payment.test.ts"]
928
+ }
929
+ },
930
+ "flows": {
931
+ "user login": {
932
+ "description": "User submits credentials, gets JWT token",
933
+ "chain": ["src/routes/auth.ts", "src/services/AuthService.ts", "src/models/User.ts"]
934
+ },
935
+ "process payment": {
936
+ "description": "User initiates payment, Stripe charges card, webhook confirms",
937
+ "chain": ["src/routes/payment.ts", "src/services/PaymentService.ts", "src/webhooks/stripe.ts"]
938
+ }
939
+ }
940
+ }
941
+
942
+ Rules:
943
+ - Use the FULL relative file paths exactly as given in the input
944
+ - Group by what a human would naturally ask about, not by technical structure
945
+ - Each feature should have 2-8 files \u2014 not too granular, not too broad
946
+ - Flows should show the actual call chain order
947
+ - Include test files in the "tests" field when they exist
948
+ - Cover ALL the files you're given \u2014 don't skip any`;
949
+ }
950
+ });
951
+
952
+ // src/snapshot/snapshot.ts
953
+ var snapshot_exports = {};
954
+ __export(snapshot_exports, {
955
+ createSnapshot: () => createSnapshot,
956
+ getCurrentGitHash: () => getCurrentGitHash,
957
+ installHook: () => installHook,
958
+ loadSnapshot: () => loadSnapshot,
959
+ saveSnapshot: () => saveSnapshot,
960
+ updateSnapshot: () => updateSnapshot
961
+ });
962
+ import fs4 from "fs/promises";
963
+ import path3 from "path";
964
+ import { execFile as execFile6 } from "child_process";
965
+ import { promisify as promisify6 } from "util";
966
+ function snapshotDir(rootDir) {
967
+ return path3.join(rootDir, ".mason");
968
+ }
969
+ function snapshotPath(rootDir) {
970
+ return path3.join(snapshotDir(rootDir), "snapshot.json");
971
+ }
972
+ async function loadSnapshot(rootDir) {
973
+ try {
974
+ const raw = await fs4.readFile(snapshotPath(rootDir), "utf-8");
975
+ const parsed = JSON.parse(raw);
976
+ if (parsed.version !== 2) return null;
977
+ return parsed;
978
+ } catch {
979
+ return null;
980
+ }
981
+ }
982
+ async function saveSnapshot(rootDir, snapshot) {
983
+ await fs4.mkdir(snapshotDir(rootDir), { recursive: true });
984
+ await fs4.writeFile(
985
+ snapshotPath(rootDir),
986
+ JSON.stringify(snapshot, null, 2),
987
+ "utf-8"
988
+ );
989
+ }
990
+ async function getCurrentGitHash(rootDir) {
991
+ try {
992
+ const { stdout } = await exec6("git", ["rev-parse", "HEAD"], {
993
+ cwd: rootDir
994
+ });
995
+ return stdout.trim();
996
+ } catch {
997
+ return "unknown";
998
+ }
999
+ }
1000
+ function parseSnapshotResponse(raw) {
1001
+ let cleaned = raw.trim();
1002
+ if (cleaned.startsWith("```")) {
1003
+ cleaned = cleaned.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
1004
+ }
1005
+ try {
1006
+ const parsed = JSON.parse(cleaned);
1007
+ return {
1008
+ features: parsed.features ?? {},
1009
+ flows: parsed.flows ?? {}
1010
+ };
1011
+ } catch {
1012
+ const match = raw.match(/\{[\s\S]*\}/);
1013
+ if (match) {
1014
+ try {
1015
+ const parsed = JSON.parse(match[0]);
1016
+ return {
1017
+ features: parsed.features ?? {},
1018
+ flows: parsed.flows ?? {}
1019
+ };
1020
+ } catch {
1021
+ return { features: {}, flows: {} };
1022
+ }
1023
+ }
1024
+ return { features: {}, flows: {} };
1025
+ }
1026
+ }
1027
+ async function createSnapshot(rootDir, config) {
1028
+ const resolvedRoot = path3.resolve(rootDir);
1029
+ const sampled = await sampleFiles(resolvedRoot, 25);
1030
+ const filesWithContent = [];
1031
+ for (const sample of sampled) {
1032
+ const full = await readFullFile(resolvedRoot, sample.path);
1033
+ if (full) {
1034
+ filesWithContent.push({ path: full.path, content: full.content });
1035
+ }
1036
+ }
1037
+ const gitHash = await getCurrentGitHash(resolvedRoot);
1038
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1039
+ if (filesWithContent.length === 0) {
1040
+ return {
1041
+ version: 2,
1042
+ createdAt: now,
1043
+ updatedAt: now,
1044
+ gitHash,
1045
+ features: {},
1046
+ flows: {}
1047
+ };
1048
+ }
1049
+ const userMessage = buildSnapshotPrompt(filesWithContent);
1050
+ const result = await callLLM(config, userMessage, SNAPSHOT_SYSTEM_PROMPT);
1051
+ const resultText = typeof result === "string" ? result : result.type === "response" ? result.text : "";
1052
+ if (!resultText) {
1053
+ throw new Error(
1054
+ "No CLI or API key available for this provider. Use claude or ollama (no key needed), or provide an API key."
1055
+ );
1056
+ }
1057
+ const { features, flows } = parseSnapshotResponse(resultText);
1058
+ const snapshot = {
1059
+ version: 2,
1060
+ createdAt: now,
1061
+ updatedAt: now,
1062
+ gitHash,
1063
+ features,
1064
+ flows
1065
+ };
1066
+ await saveSnapshot(resolvedRoot, snapshot);
1067
+ return snapshot;
1068
+ }
1069
+ async function updateSnapshot(rootDir, config) {
1070
+ const resolvedRoot = path3.resolve(rootDir);
1071
+ const existing = await loadSnapshot(resolvedRoot);
1072
+ if (!existing) {
1073
+ const snapshot = await createSnapshot(rootDir, config);
1074
+ const featureCount = Object.keys(snapshot.features).length;
1075
+ const flowCount = Object.keys(snapshot.flows).length;
1076
+ return {
1077
+ status: "created",
1078
+ details: `New snapshot: ${featureCount} features, ${flowCount} flows`
1079
+ };
1080
+ }
1081
+ let changedFiles = [];
1082
+ try {
1083
+ const { stdout } = await exec6(
1084
+ "git",
1085
+ ["diff", "--name-only", existing.gitHash, "HEAD"],
1086
+ { cwd: resolvedRoot }
1087
+ );
1088
+ changedFiles = stdout.trim().split("\n").filter((f) => f.length > 0);
1089
+ } catch {
1090
+ const snapshot = await createSnapshot(rootDir, config);
1091
+ const featureCount = Object.keys(snapshot.features).length;
1092
+ return { status: "rebuilt", details: `${featureCount} features` };
1093
+ }
1094
+ if (changedFiles.length === 0) {
1095
+ return { status: "up-to-date", details: "No changes since last snapshot" };
1096
+ }
1097
+ const sampled = await sampleFiles(resolvedRoot, 30);
1098
+ const sampledPaths = new Set(sampled.map((s) => s.path));
1099
+ const snapshotFiles = /* @__PURE__ */ new Set();
1100
+ for (const feature of Object.values(existing.features)) {
1101
+ for (const f of feature.files) snapshotFiles.add(f);
1102
+ for (const t of feature.tests ?? []) snapshotFiles.add(t);
1103
+ }
1104
+ for (const flow of Object.values(existing.flows)) {
1105
+ for (const f of flow.chain) snapshotFiles.add(f);
1106
+ }
1107
+ const relevantChanges = changedFiles.filter(
1108
+ (f) => sampledPaths.has(f) || snapshotFiles.has(f)
1109
+ );
1110
+ if (relevantChanges.length === 0) {
1111
+ existing.gitHash = await getCurrentGitHash(resolvedRoot);
1112
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1113
+ await saveSnapshot(resolvedRoot, existing);
1114
+ return {
1115
+ status: "unchanged",
1116
+ details: `${changedFiles.length} files changed but none affect the concept map`
1117
+ };
1118
+ }
1119
+ const filesWithContent = [];
1120
+ for (const filePath of relevantChanges) {
1121
+ const full = await readFullFile(resolvedRoot, filePath);
1122
+ if (full) {
1123
+ filesWithContent.push({ path: full.path, content: full.content });
1124
+ }
1125
+ }
1126
+ if (filesWithContent.length === 0) {
1127
+ return { status: "unchanged", details: "Changed files could not be read" };
1128
+ }
1129
+ const userMessage = buildIncrementalPrompt(filesWithContent, {
1130
+ features: existing.features,
1131
+ flows: existing.flows
1132
+ });
1133
+ const result = await callLLM(config, userMessage, SNAPSHOT_SYSTEM_PROMPT);
1134
+ const resultText = typeof result === "string" ? result : result.type === "response" ? result.text : "";
1135
+ if (!resultText) {
1136
+ throw new Error("No CLI or API key available for this provider.");
1137
+ }
1138
+ const { features, flows } = parseSnapshotResponse(resultText);
1139
+ const gitHash = await getCurrentGitHash(resolvedRoot);
1140
+ existing.features = features;
1141
+ existing.flows = flows;
1142
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1143
+ existing.gitHash = gitHash;
1144
+ await saveSnapshot(resolvedRoot, existing);
1145
+ return {
1146
+ status: "updated",
1147
+ details: `${Object.keys(features).length} features, ${Object.keys(flows).length} flows (${relevantChanges.length} files changed)`
1148
+ };
1149
+ }
1150
+ async function installHook(rootDir) {
1151
+ const resolvedRoot = path3.resolve(rootDir);
1152
+ const hooksDir = path3.join(resolvedRoot, ".git", "hooks");
1153
+ try {
1154
+ await fs4.access(hooksDir);
1155
+ } catch {
1156
+ throw new Error("Not a git repository (no .git/hooks directory)");
1157
+ }
1158
+ const hookPath = path3.join(hooksDir, "post-commit");
1159
+ const hookContent = `#!/bin/sh
1160
+ # Mason: auto-update project snapshot after commit
1161
+ # Runs in background so it doesn't block your workflow
1162
+ mason snapshot-update "$(git rev-parse --show-toplevel)" &
1163
+ `;
1164
+ try {
1165
+ const existing = await fs4.readFile(hookPath, "utf-8");
1166
+ if (existing.includes("mason snapshot-update")) {
1167
+ return;
1168
+ }
1169
+ await fs4.appendFile(hookPath, "\n" + hookContent);
1170
+ } catch {
1171
+ await fs4.writeFile(hookPath, hookContent, { mode: 493 });
1172
+ }
1173
+ }
1174
+ var exec6;
1175
+ var init_snapshot = __esm({
1176
+ "src/snapshot/snapshot.ts"() {
1177
+ "use strict";
1178
+ init_sampler();
1179
+ init_providers();
1180
+ init_prompt();
1181
+ exec6 = promisify6(execFile6);
1182
+ }
1183
+ });
1184
+
1185
+ // src/mcp/tools.ts
1186
+ import fs5 from "fs/promises";
1187
+ import path4 from "path";
1188
+ import { execFile as execFile7 } from "child_process";
1189
+ import { promisify as promisify7 } from "util";
1190
+ import fg3 from "fast-glob";
1191
+ async function buildContext(dir) {
1192
+ return {
1193
+ rootDir: dir,
1194
+ gitAvailable: await isGitRepo(dir)
1195
+ };
1196
+ }
1197
+ async function analyzeProject(dir) {
1198
+ const rootDir = path4.resolve(dir);
1199
+ const context = await buildContext(rootDir);
1200
+ const results = await runAll(context);
1201
+ const projectSnapshot = await detectProjectSnapshot(rootDir);
1202
+ const output = {
1203
+ project: projectSnapshot,
1204
+ analyzers: results.map((r) => ({
1205
+ name: r.analyzer,
1206
+ durationMs: r.durationMs,
1207
+ findings: r.findings.map((f) => ({
1208
+ category: f.category,
1209
+ confidence: f.confidence,
1210
+ summary: f.summary,
1211
+ evidence: f.evidence,
1212
+ suggestedRule: f.ruleCandidate
1213
+ })),
1214
+ gaps: r.gaps.map((g) => ({
1215
+ question: g.question,
1216
+ context: g.context
1217
+ }))
1218
+ }))
1219
+ };
1220
+ return JSON.stringify(output, null, 2);
1221
+ }
1222
+ async function detectProjectSnapshot(rootDir) {
1223
+ const buildFiles = [
1224
+ "package.json",
1225
+ "tsconfig.json",
1226
+ "build.gradle.kts",
1227
+ "build.gradle",
1228
+ "settings.gradle.kts",
1229
+ "settings.gradle",
1230
+ "gradle/libs.versions.toml",
1231
+ "Cargo.toml",
1232
+ "go.mod",
1233
+ "go.sum",
1234
+ "pyproject.toml",
1235
+ "setup.py",
1236
+ "requirements.txt",
1237
+ "Pipfile",
1238
+ "Gemfile",
1239
+ "Package.swift",
1240
+ "Makefile",
1241
+ "CMakeLists.txt",
1242
+ "Dockerfile",
1243
+ "docker-compose.yml",
1244
+ "docker-compose.yaml",
1245
+ ".github/workflows",
1246
+ ".gitlab-ci.yml",
1247
+ "Jenkinsfile"
1248
+ ];
1249
+ const present = [];
1250
+ for (const file of buildFiles) {
1251
+ try {
1252
+ await fs5.access(path4.join(rootDir, file));
1253
+ present.push(file);
1254
+ } catch {
1255
+ }
1256
+ }
1257
+ const testDirs = [
1258
+ "test",
1259
+ "tests",
1260
+ "__tests__",
1261
+ "spec",
1262
+ "src/test",
1263
+ "src/tests",
1264
+ "**/src/test",
1265
+ "**/src/androidTest",
1266
+ "**/src/iosTest"
1267
+ ];
1268
+ const testInfo = {};
1269
+ for (const pattern of testDirs) {
1270
+ const files = await fg3(`${pattern}/**/*`, {
1271
+ cwd: rootDir,
1272
+ ignore: IGNORE,
1273
+ onlyFiles: true
1274
+ });
1275
+ if (files.length > 0) {
1276
+ testInfo[pattern] = files.length;
1277
+ }
1278
+ }
1279
+ const testFilePatterns = [
1280
+ { pattern: "**/*.test.*", label: "*.test.*" },
1281
+ { pattern: "**/*.spec.*", label: "*.spec.*" },
1282
+ { pattern: "**/*Test.kt", label: "*Test.kt" },
1283
+ { pattern: "**/*Test.java", label: "*Test.java" },
1284
+ { pattern: "**/test_*.py", label: "test_*.py" },
1285
+ { pattern: "**/*_test.go", label: "*_test.go" },
1286
+ { pattern: "**/*Tests.swift", label: "*Tests.swift" },
1287
+ { pattern: "**/*_test.rs", label: "*_test.rs" }
1288
+ ];
1289
+ for (const { pattern, label } of testFilePatterns) {
1290
+ const files = await fg3(pattern, { cwd: rootDir, ignore: IGNORE });
1291
+ if (files.length > 0) {
1292
+ testInfo[label] = files.length;
1293
+ }
1294
+ }
1295
+ const sourceFiles = await fg3("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
1296
+ cwd: rootDir,
1297
+ ignore: IGNORE
1298
+ });
1299
+ const fileCounts = {};
1300
+ for (const file of sourceFiles) {
1301
+ const ext = path4.extname(file).slice(1);
1302
+ fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
1303
+ }
1304
+ return {
1305
+ configFilesPresent: present,
1306
+ sourceFileCounts: fileCounts,
1307
+ totalSourceFiles: sourceFiles.length,
1308
+ testInfo: Object.keys(testInfo).length > 0 ? testInfo : void 0
1309
+ };
1310
+ }
1311
+ async function getCodeSamples(dir, count = 15) {
1312
+ const rootDir = path4.resolve(dir);
1313
+ const samples = await sampleFiles(rootDir, count);
1314
+ const output = {
1315
+ note: "These are previews (first ~60 lines). Use get_file_content to read the full file if needed.",
1316
+ files: samples.map((s) => ({
1317
+ path: s.path,
1318
+ reason: s.reason,
1319
+ totalLines: s.totalLines,
1320
+ sizeBytes: s.sizeBytes,
1321
+ preview: s.preview
1322
+ }))
1323
+ };
1324
+ return JSON.stringify(output, null, 2);
1325
+ }
1326
+ async function getFileContent(dir, filePath) {
1327
+ const rootDir = path4.resolve(dir);
1328
+ const result = await readFullFile(rootDir, filePath);
1329
+ if (!result) {
1330
+ return JSON.stringify({ error: `Could not read file: ${filePath}` });
1331
+ }
1332
+ return JSON.stringify(result, null, 2);
1333
+ }
1334
+ async function getProjectStructure(dir) {
1335
+ const rootDir = path4.resolve(dir);
1336
+ const allFiles = await fg3("**/*", {
1337
+ cwd: rootDir,
1338
+ ignore: IGNORE,
1339
+ onlyFiles: true
1340
+ });
1341
+ const dirInfo = /* @__PURE__ */ new Map();
1342
+ for (const file of allFiles) {
1343
+ const parts = file.split("/");
1344
+ for (let depth = 1; depth <= Math.min(parts.length, 2); depth++) {
1345
+ const dirPath = parts.slice(0, depth).join("/");
1346
+ if (!dirInfo.has(dirPath)) {
1347
+ dirInfo.set(dirPath, { fileCount: 0, extensions: /* @__PURE__ */ new Map() });
1348
+ }
1349
+ const info2 = dirInfo.get(dirPath);
1350
+ info2.fileCount++;
1351
+ const ext = path4.extname(file).slice(1);
1352
+ if (ext) {
1353
+ info2.extensions.set(ext, (info2.extensions.get(ext) ?? 0) + 1);
1354
+ }
1355
+ }
1356
+ }
1357
+ const directories = [...dirInfo.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([dirPath, info2]) => {
1358
+ const extensions = {};
1359
+ for (const [ext, count] of info2.extensions) {
1360
+ extensions[ext] = count;
1361
+ }
1362
+ return { path: dirPath, fileCount: info2.fileCount, extensions };
1363
+ });
1364
+ const topLevelFiles = allFiles.filter((f) => !f.includes("/"));
1365
+ const output = {
1366
+ totalFiles: allFiles.length,
1367
+ topLevelFiles,
1368
+ directories
1369
+ };
1370
+ return JSON.stringify(output, null, 2);
1371
+ }
1372
+ async function getTestMap(dir) {
1373
+ const rootDir = path4.resolve(dir);
1374
+ const testPatterns = [
1375
+ "**/*.test.*",
1376
+ "**/*.spec.*",
1377
+ "**/*Test.kt",
1378
+ "**/*Test.java",
1379
+ "**/*Tests.kt",
1380
+ "**/*Tests.java",
1381
+ "**/test_*.py",
1382
+ "**/*_test.py",
1383
+ "**/*_test.go",
1384
+ "**/*Tests.swift",
1385
+ "**/*Test.swift",
1386
+ "**/*_test.rs"
1387
+ ];
1388
+ const testFiles = await fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
1389
+ const sourceFiles = await fg3(
1390
+ "**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}",
1391
+ { cwd: rootDir, ignore: IGNORE }
1392
+ );
1393
+ const sourceByBaseName = /* @__PURE__ */ new Map();
1394
+ for (const file of sourceFiles) {
1395
+ if (testFiles.includes(file)) continue;
1396
+ const baseName = path4.basename(file).replace(/\.[^.]+$/, "");
1397
+ const existing = sourceByBaseName.get(baseName) ?? [];
1398
+ existing.push(file);
1399
+ sourceByBaseName.set(baseName, existing);
1400
+ }
1401
+ const pairs = [];
1402
+ const unmatched = [];
1403
+ for (const testFile of testFiles) {
1404
+ const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
1405
+ const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
1406
+ if (!sourceName) {
1407
+ unmatched.push(testFile);
1408
+ continue;
1409
+ }
1410
+ const candidates = sourceByBaseName.get(sourceName);
1411
+ if (candidates && candidates.length > 0) {
1412
+ const testDir = path4.dirname(testFile);
1413
+ const bestMatch = candidates.reduce((best, candidate) => {
1414
+ const candidateDir = path4.dirname(candidate);
1415
+ const bestDir = path4.dirname(best);
1416
+ const candidateOverlap = commonSegments(testDir, candidateDir);
1417
+ const bestOverlap = commonSegments(testDir, bestDir);
1418
+ return candidateOverlap > bestOverlap ? candidate : best;
1419
+ });
1420
+ pairs.push({
1421
+ test: testFile,
1422
+ source: bestMatch,
1423
+ confidence: candidates.length === 1 ? "exact" : "best-guess"
1424
+ });
1425
+ } else {
1426
+ unmatched.push(testFile);
1427
+ }
1428
+ }
1429
+ const output = {
1430
+ totalTestFiles: testFiles.length,
1431
+ paired: pairs,
1432
+ unmatched
1433
+ };
1434
+ return JSON.stringify(output, null, 2);
1435
+ }
1436
+ function commonSegments(pathA, pathB) {
1437
+ const segsA = pathA.split("/");
1438
+ const segsB = pathB.split("/");
1439
+ let count = 0;
1440
+ for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
1441
+ if (segsA[i] === segsB[i]) count++;
1442
+ else break;
1443
+ }
1444
+ return count;
1445
+ }
1446
+ async function getSnapshot(dir) {
1447
+ const rootDir = path4.resolve(dir);
1448
+ const snapshot = await loadSnapshot(rootDir);
1449
+ if (!snapshot) {
1450
+ return JSON.stringify({
1451
+ exists: false,
1452
+ message: "No concept map found. Run 'mason snapshot' to create one, or call save_snapshot with features and flows."
1453
+ });
1454
+ }
1455
+ const currentHash = await getCurrentGitHash(rootDir);
1456
+ const isStale = snapshot.gitHash !== currentHash && snapshot.gitHash !== "unknown";
1457
+ const output = {
1458
+ exists: true,
1459
+ createdAt: snapshot.createdAt,
1460
+ updatedAt: snapshot.updatedAt,
1461
+ featureCount: Object.keys(snapshot.features).length,
1462
+ flowCount: Object.keys(snapshot.flows).length,
1463
+ features: snapshot.features,
1464
+ flows: snapshot.flows,
1465
+ stale: isStale
1466
+ };
1467
+ if (isStale) {
1468
+ output.message = "Snapshot is behind HEAD. Some features/flows may reference changed files. Run 'mason snapshot-update' or call save_snapshot to refresh.";
1469
+ }
1470
+ return JSON.stringify(output, null, 2);
1471
+ }
1472
+ async function fullAnalysis(dir) {
1473
+ const rootDir = path4.resolve(dir);
1474
+ const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
1475
+ analyzeProject(dir),
1476
+ getProjectStructure(dir),
1477
+ getCodeSamples(dir, 25),
1478
+ getTestMap(dir),
1479
+ loadSnapshot(rootDir)
1480
+ ]);
1481
+ const output = {
1482
+ note: "Full project analysis. Code samples are previews (~60 lines). Use get_file_content to read any file in full.",
1483
+ analysis: JSON.parse(analysis),
1484
+ structure: JSON.parse(structure),
1485
+ codeSamples: JSON.parse(samples),
1486
+ testMap: JSON.parse(testMap)
1487
+ };
1488
+ if (snapshot) {
1489
+ output.conceptMap = {
1490
+ updatedAt: snapshot.updatedAt,
1491
+ features: snapshot.features,
1492
+ flows: snapshot.flows
1493
+ };
1494
+ 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.";
1495
+ }
1496
+ return JSON.stringify(output, null, 2);
1497
+ }
1498
+ async function saveSnapshotData(dir, features, flows) {
1499
+ const rootDir = path4.resolve(dir);
1500
+ const gitHash = await getCurrentGitHash(rootDir);
1501
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1502
+ const existing = await loadSnapshot(rootDir);
1503
+ if (existing) {
1504
+ existing.features = { ...existing.features, ...features };
1505
+ existing.flows = { ...existing.flows, ...flows };
1506
+ existing.updatedAt = now;
1507
+ existing.gitHash = gitHash;
1508
+ await saveSnapshot(rootDir, existing);
1509
+ return JSON.stringify({
1510
+ status: "updated",
1511
+ features: Object.keys(existing.features).length,
1512
+ flows: Object.keys(existing.flows).length
1513
+ });
1514
+ }
1515
+ const snapshot = {
1516
+ version: 2,
1517
+ createdAt: now,
1518
+ updatedAt: now,
1519
+ gitHash,
1520
+ features,
1521
+ flows
1522
+ };
1523
+ await saveSnapshot(rootDir, snapshot);
1524
+ return JSON.stringify({
1525
+ status: "created",
1526
+ features: Object.keys(features).length,
1527
+ flows: Object.keys(flows).length
1528
+ });
1529
+ }
1530
+ async function configureProject(dir, config) {
1531
+ const rootDir = path4.resolve(dir);
1532
+ const configDir = path4.join(rootDir, ".mason");
1533
+ const configPath = path4.join(configDir, "config.json");
1534
+ let existing = {};
1535
+ try {
1536
+ const raw = await fs5.readFile(configPath, "utf-8");
1537
+ existing = JSON.parse(raw);
1538
+ } catch {
1539
+ }
1540
+ if (config.patterns) existing.patterns = config.patterns;
1541
+ if (config.alwaysInclude) existing.alwaysInclude = config.alwaysInclude;
1542
+ if (config.ignore) existing.ignore = config.ignore;
1543
+ await fs5.mkdir(configDir, { recursive: true });
1544
+ await fs5.writeFile(configPath, JSON.stringify(existing, null, 2), "utf-8");
1545
+ return JSON.stringify({
1546
+ status: "saved",
1547
+ path: configPath,
1548
+ config: existing
1549
+ });
1550
+ }
1551
+ var exec7, IGNORE;
1552
+ var init_tools = __esm({
1553
+ "src/mcp/tools.ts"() {
1554
+ "use strict";
1555
+ init_analyzers();
1556
+ init_git();
1557
+ init_sampler();
1558
+ init_snapshot();
1559
+ exec7 = promisify7(execFile7);
1560
+ IGNORE = [
1561
+ "**/node_modules/**",
1562
+ "**/dist/**",
1563
+ "**/build/**",
1564
+ "**/.gradle/**",
1565
+ "**/target/**",
1566
+ "**/.git/**",
1567
+ "**/vendor/**",
1568
+ "**/__pycache__/**",
1569
+ "**/venv/**",
1570
+ "**/.venv/**",
1571
+ "**/*.min.*",
1572
+ "**/*.map"
1573
+ ];
1574
+ }
1575
+ });
1576
+
1577
+ // src/mcp/server.ts
1578
+ var server_exports = {};
1579
+ __export(server_exports, {
1580
+ createMcpServer: () => createMcpServer,
1581
+ startMcpServer: () => startMcpServer
1582
+ });
1583
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1584
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1585
+ import { z } from "zod";
1586
+ function createMcpServer() {
1587
+ const server = new McpServer(
1588
+ {
1589
+ name: "mason",
1590
+ version: "0.1.0"
1591
+ },
1592
+ {
1593
+ instructions: "Mason is a context engineering tool. Always call get_snapshot before using Explore agents, Glob, or Grep to understand the codebase. The snapshot is a concept map that maps features and flows to their implementing files \u2014 it eliminates the need to search. This applies to ANY question about architecture, features, flows, how things work, cross-feature interactions, or bug investigation. Workflow: 1) Call get_snapshot first. 2) If no snapshot, call full_analysis instead. 3) Use get_file_content to read the files the snapshot points to. 4) Call save_snapshot to persist your understanding for future sessions. 5) Call write_claude_md for documentation."
1594
+ }
1595
+ );
1596
+ server.tool(
1597
+ "full_analysis",
1598
+ "Run a complete project analysis in one call. Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source file mapping. This is the recommended starting point \u2014 call this first, then use get_file_content to read specific files in full.",
1599
+ {
1600
+ dir: z.string().describe("Absolute path to the project root directory")
1601
+ },
1602
+ async ({ dir }) => {
1603
+ const result = await fullAnalysis(dir);
1604
+ return {
1605
+ content: [{ type: "text", text: result }]
1606
+ };
1607
+ }
1608
+ );
1609
+ server.tool(
1610
+ "analyze_project",
1611
+ "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.",
1612
+ {
1613
+ dir: z.string().describe("Absolute path to the project root directory")
1614
+ },
1615
+ async ({ dir }) => {
1616
+ const result = await analyzeProject(dir);
1617
+ return {
1618
+ content: [{ type: "text", text: result }]
1619
+ };
1620
+ }
1621
+ );
1622
+ server.tool(
1623
+ "get_code_samples",
1624
+ "Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth. Use get_file_content to read the full content of any file that looks interesting.",
1625
+ {
1626
+ dir: z.string().describe("Absolute path to the project root directory"),
1627
+ count: z.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
1628
+ },
1629
+ async ({ dir, count }) => {
1630
+ const result = await getCodeSamples(dir, count);
1631
+ return {
1632
+ content: [{ type: "text", text: result }]
1633
+ };
1634
+ }
1635
+ );
1636
+ server.tool(
1637
+ "get_file_content",
1638
+ "Read the full content of a specific file. Use this after get_code_samples to drill into files you want to understand fully.",
1639
+ {
1640
+ dir: z.string().describe("Absolute path to the project root directory"),
1641
+ file_path: z.string().describe("Relative path to the file within the project (e.g., 'src/main.ts')")
1642
+ },
1643
+ async ({ dir, file_path }) => {
1644
+ const result = await getFileContent(dir, file_path);
1645
+ return {
1646
+ content: [{ type: "text", text: result }]
1647
+ };
1648
+ }
1649
+ );
1650
+ server.tool(
1651
+ "get_project_structure",
1652
+ "Get the directory structure of a project with file counts and extension breakdown per directory. Shows top-level files and annotated directory listing up to 2 levels deep. Useful for understanding project layout before diving into code.",
1653
+ {
1654
+ dir: z.string().describe("Absolute path to the project root directory")
1655
+ },
1656
+ async ({ dir }) => {
1657
+ const result = await getProjectStructure(dir);
1658
+ return {
1659
+ content: [{ type: "text", text: result }]
1660
+ };
1661
+ }
1662
+ );
1663
+ server.tool(
1664
+ "get_test_map",
1665
+ "Map test files to their corresponding source files by name matching. Shows which source files have tests and which don't. Useful for understanding test coverage patterns and test organization conventions.",
1666
+ {
1667
+ dir: z.string().describe("Absolute path to the project root directory")
1668
+ },
1669
+ async ({ dir }) => {
1670
+ const result = await getTestMap(dir);
1671
+ return {
1672
+ content: [{ type: "text", text: result }]
1673
+ };
1674
+ }
1675
+ );
1676
+ server.tool(
1677
+ "get_snapshot",
1678
+ "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.",
1679
+ {
1680
+ dir: z.string().describe("Absolute path to the project root directory")
1681
+ },
1682
+ async ({ dir }) => {
1683
+ const result = await getSnapshot(dir);
1684
+ return {
1685
+ content: [{ type: "text", text: result }]
1686
+ };
1687
+ }
1688
+ );
1689
+ server.tool(
1690
+ "save_snapshot",
1691
+ "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.",
1692
+ {
1693
+ dir: z.string().describe("Absolute path to the project root directory"),
1694
+ features: z.record(
1695
+ z.object({
1696
+ description: z.string().describe("One-line description of the feature"),
1697
+ files: z.array(z.string()).describe("File paths that implement this feature"),
1698
+ tests: z.array(z.string()).optional().describe("Test file paths for this feature")
1699
+ })
1700
+ ).describe("Map of feature names to their implementing files"),
1701
+ flows: z.record(
1702
+ z.object({
1703
+ description: z.string().describe("One-line description of the flow"),
1704
+ chain: z.array(z.string()).describe("Ordered list of file paths showing data/call flow")
1705
+ })
1706
+ ).describe("Map of flow names to ordered file chains")
1707
+ },
1708
+ async ({ dir, features, flows }) => {
1709
+ const result = await saveSnapshotData(dir, features, flows);
1710
+ return {
1711
+ content: [{ type: "text", text: result }]
1712
+ };
1713
+ }
1714
+ );
1715
+ server.tool(
1716
+ "configure_project",
1717
+ "Configure Mason for this project. Add custom file patterns to sample, files to always include, or paths to ignore. Saved to .mason/config.json. Use this when the default architectural patterns miss important files in the project.",
1718
+ {
1719
+ dir: z.string().describe("Absolute path to the project root directory"),
1720
+ patterns: z.array(z.string()).optional().describe("Custom glob patterns for architecturally important files (e.g., '**/*Gateway.*', '**/*Bloc.*')"),
1721
+ alwaysInclude: z.array(z.string()).optional().describe("Specific file paths to always include in samples (e.g., 'src/core/config.ts')"),
1722
+ ignore: z.array(z.string()).optional().describe("Additional glob patterns to ignore (e.g., '**/fixtures/**')")
1723
+ },
1724
+ async ({ dir, patterns, alwaysInclude, ignore }) => {
1725
+ const result = await configureProject(dir, {
1726
+ patterns,
1727
+ alwaysInclude,
1728
+ ignore
1729
+ });
1730
+ return {
1731
+ content: [{ type: "text", text: result }]
1732
+ };
1733
+ }
1734
+ );
1735
+ return server;
1736
+ }
1737
+ async function startMcpServer() {
1738
+ const server = createMcpServer();
1739
+ const transport = new StdioServerTransport();
1740
+ await server.connect(transport);
1741
+ }
1742
+ var init_server = __esm({
1743
+ "src/mcp/server.ts"() {
1744
+ "use strict";
1745
+ init_tools();
1746
+ }
1747
+ });
1748
+
1749
+ // src/cli.ts
1750
+ init_analyzers();
1751
+ init_git();
1752
+ init_config();
1753
+ init_providers();
1754
+ init_tools();
1755
+ import { Command } from "commander";
1756
+ import fs6 from "fs/promises";
1757
+ import path5 from "path";
1758
+ import ora from "ora";
1759
+ import chalk2 from "chalk";
1760
+
1761
+ // src/utils/logger.ts
1762
+ import chalk from "chalk";
1763
+ var verbose = false;
1764
+ function info(msg) {
1765
+ console.log(chalk.blue("\u2139"), msg);
1766
+ }
1767
+ function success(msg) {
1768
+ console.log(chalk.green("\u2714"), msg);
1769
+ }
1770
+ function error(msg) {
1771
+ console.error(chalk.red("\u2716"), msg);
1772
+ }
1773
+ function debug(msg) {
1774
+ if (verbose) {
1775
+ console.log(chalk.gray("\u22EF"), msg);
1776
+ }
1777
+ }
1778
+
1779
+ // src/cli.ts
1780
+ async function buildContext2(dir) {
1781
+ return {
1782
+ rootDir: dir,
1783
+ gitAvailable: await isGitRepo(dir)
1784
+ };
1785
+ }
1786
+ function printFindings(results) {
1787
+ for (const result of results) {
1788
+ if (result.findings.length === 0) {
1789
+ debug(`${result.analyzer}: no findings`);
1790
+ continue;
1791
+ }
1792
+ console.log(
1793
+ chalk2.bold(`
1794
+ \u{1F4CB} ${result.analyzer}`) + chalk2.gray(` (${result.durationMs}ms)`)
1795
+ );
1796
+ for (const finding of result.findings) {
1797
+ const conf = chalk2.gray(`[${Math.round(finding.confidence * 100)}%]`);
1798
+ console.log(` ${conf} ${finding.summary}`);
1799
+ for (const ev of finding.evidence) {
1800
+ console.log(chalk2.gray(` ${ev.filePath}: ${ev.detail}`));
1801
+ }
1802
+ }
1803
+ }
1804
+ }
1805
+ function extractMarkdown(raw) {
1806
+ const trimmed = raw.trim();
1807
+ if (trimmed.startsWith("# ")) return trimmed;
1808
+ const fenceMatch = trimmed.match(/```(?:markdown|md)?\n([\s\S]*?)```/);
1809
+ if (fenceMatch) return fenceMatch[1].trim();
1810
+ const headingIndex = trimmed.search(/^# /m);
1811
+ if (headingIndex >= 0) return trimmed.slice(headingIndex).trim();
1812
+ const subheadingIndex = trimmed.search(/^## /m);
1813
+ if (subheadingIndex >= 0) return trimmed.slice(subheadingIndex).trim();
1814
+ return trimmed;
1815
+ }
1816
+ function createCLI() {
1817
+ const program2 = new Command();
1818
+ program2.name("mason").description(
1819
+ "Context engineering CLI & MCP server \u2014 generates intelligent CLAUDE.md files"
1820
+ ).version("0.1.0");
1821
+ program2.command("setup").description("Register Mason as an MCP server with Claude Code").option("--scope <scope>", "Config scope: user or project", "user").action(async (opts) => {
1822
+ const { execFile: execFile8 } = await import("child_process");
1823
+ const { promisify: promisify8 } = await import("util");
1824
+ const exec8 = promisify8(execFile8);
1825
+ try {
1826
+ await exec8("claude", ["--version"]);
1827
+ } catch {
1828
+ error(
1829
+ "Claude Code CLI not found. Install it from https://claude.ai/code"
1830
+ );
1831
+ process.exit(1);
1832
+ }
1833
+ try {
1834
+ const args = [
1835
+ "mcp",
1836
+ "add",
1837
+ "mason",
1838
+ "--scope",
1839
+ opts.scope,
1840
+ "--",
1841
+ "npx",
1842
+ "mason-ai",
1843
+ "mcp"
1844
+ ];
1845
+ await exec8("claude", args);
1846
+ success("Mason registered with Claude Code.");
1847
+ info("Restart Claude Code to start using Mason's tools.");
1848
+ } catch (err) {
1849
+ error(
1850
+ `Failed to register: ${err instanceof Error ? err.message : String(err)}`
1851
+ );
1852
+ process.exit(1);
1853
+ }
1854
+ });
1855
+ 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(
1856
+ async (provider, apiKey, opts) => {
1857
+ const validProvider = validateProvider(provider);
1858
+ if (needsApiKey(validProvider) && !apiKey) {
1859
+ error(
1860
+ `API key is required for ${validProvider}. Usage: mason set-llm ${validProvider} <api-key>`
1861
+ );
1862
+ process.exit(1);
1863
+ }
1864
+ if (!apiKey && !needsApiKey(validProvider)) {
1865
+ const cli = await detectCLI(validProvider);
1866
+ if (!cli.available) {
1867
+ const hints = {
1868
+ claude: "Claude Code CLI not found. Install it from https://claude.ai/code, or provide an API key: mason set-llm claude <api-key>",
1869
+ 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>",
1870
+ ollama: "Ollama not found. Install it from https://ollama.ai"
1871
+ };
1872
+ error(hints[validProvider] ?? "CLI not found for this provider.");
1873
+ process.exit(1);
1874
+ }
1875
+ info(
1876
+ `Found ${validProvider} CLI (${cli.version ?? "installed"}). No API key needed.`
1877
+ );
1878
+ }
1879
+ const config = {
1880
+ provider: validProvider,
1881
+ apiKey,
1882
+ model: opts.model,
1883
+ ollamaHost: validProvider === "ollama" ? opts.ollamaHost : void 0
1884
+ };
1885
+ await saveConfig(config);
1886
+ const model = config.model ?? getDefaultModel(validProvider);
1887
+ success(
1888
+ `Configured ${validProvider} (model: ${model}). Run "mason generate" to create a CLAUDE.md.`
1889
+ );
1890
+ }
1891
+ );
1892
+ 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) => {
1893
+ const config = await loadConfig();
1894
+ if (!config) {
1895
+ error(
1896
+ 'No LLM configured. Run "mason set-llm <provider> <api-key>" first.'
1897
+ );
1898
+ process.exit(1);
1899
+ }
1900
+ const rootDir = path5.resolve(dir);
1901
+ const runConfig = opts.model ? { ...config, model: opts.model } : config;
1902
+ const spinner = ora("Analyzing codebase...").start();
1903
+ const analysisData = await fullAnalysis(rootDir);
1904
+ spinner.text = `Generating CLAUDE.md with ${runConfig.provider}...`;
1905
+ try {
1906
+ const result = await callLLM(
1907
+ runConfig,
1908
+ `Here is the full project analysis. Write a CLAUDE.md based on this data:
1909
+
1910
+ ${analysisData}`
1911
+ );
1912
+ spinner.stop();
1913
+ if (result.type === "prompt") {
1914
+ console.log(
1915
+ chalk2.bold("\nNo API key or CLI available. Copy this prompt into your LLM:\n")
1916
+ );
1917
+ console.log(chalk2.gray("\u2500".repeat(60)));
1918
+ console.log(result.text);
1919
+ console.log(chalk2.gray("\u2500".repeat(60)));
1920
+ console.log(
1921
+ chalk2.gray("\nPaste the LLM's response into CLAUDE.md manually.")
1922
+ );
1923
+ return;
1924
+ }
1925
+ const markdown = extractMarkdown(result.text);
1926
+ if (!markdown.trim()) {
1927
+ error("LLM returned empty response.");
1928
+ process.exit(1);
1929
+ }
1930
+ const claudeDir = path5.join(rootDir, ".claude");
1931
+ await fs6.mkdir(claudeDir, { recursive: true });
1932
+ const outPath = path5.join(claudeDir, "CLAUDE.md");
1933
+ await fs6.writeFile(outPath, markdown, "utf-8");
1934
+ success(`Generated ${outPath}`);
1935
+ } catch (err) {
1936
+ spinner.stop();
1937
+ error(
1938
+ `Failed to generate: ${err instanceof Error ? err.message : String(err)}`
1939
+ );
1940
+ process.exit(1);
1941
+ }
1942
+ });
1943
+ 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) => {
1944
+ const {
1945
+ createSnapshot: createSnapshot2,
1946
+ installHook: installHook2
1947
+ } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
1948
+ const rootDir = path5.resolve(dir);
1949
+ if (opts.installHook) {
1950
+ try {
1951
+ await installHook2(rootDir);
1952
+ success("Post-commit hook installed. Snapshot will auto-update on each commit.");
1953
+ } catch (err) {
1954
+ error(
1955
+ `Failed to install hook: ${err instanceof Error ? err.message : String(err)}`
1956
+ );
1957
+ }
1958
+ return;
1959
+ }
1960
+ const config = await loadConfig();
1961
+ if (!config) {
1962
+ error(
1963
+ 'No LLM configured. Run "mason set-llm <provider> <api-key>" first.'
1964
+ );
1965
+ process.exit(1);
1966
+ }
1967
+ const spinner = ora("Building project snapshot...").start();
1968
+ try {
1969
+ const snapshot = await createSnapshot2(rootDir, config);
1970
+ spinner.stop();
1971
+ const featureCount = Object.keys(snapshot.features).length;
1972
+ const flowCount = Object.keys(snapshot.flows).length;
1973
+ success(
1974
+ `Concept map created: ${featureCount} features, ${flowCount} flows \u2192 .mason/snapshot.json`
1975
+ );
1976
+ } catch (err) {
1977
+ spinner.stop();
1978
+ error(
1979
+ `Failed to create snapshot: ${err instanceof Error ? err.message : String(err)}`
1980
+ );
1981
+ process.exit(1);
1982
+ }
1983
+ });
1984
+ program2.command("snapshot-update").description("Incrementally update snapshot with recent changes").argument("[dir]", "Directory to update", ".").action(async (dir) => {
1985
+ const { updateSnapshot: updateSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
1986
+ const rootDir = path5.resolve(dir);
1987
+ const config = await loadConfig();
1988
+ if (!config) return;
1989
+ try {
1990
+ const result = await updateSnapshot2(rootDir, config);
1991
+ if (result.status === "up-to-date" || result.status === "unchanged") {
1992
+ return;
1993
+ }
1994
+ success(`Concept map ${result.status}: ${result.details}`);
1995
+ } catch {
1996
+ }
1997
+ });
1998
+ program2.command("analyze").description("Analyze the codebase and print findings").argument("[dir]", "Directory to analyze", ".").action(async (dir) => {
1999
+ const rootDir = path5.resolve(dir);
2000
+ const spinner = ora("Analyzing codebase...").start();
2001
+ const context = await buildContext2(rootDir);
2002
+ const results = await runAll(context);
2003
+ spinner.stop();
2004
+ printFindings(results);
2005
+ const totalFindings = results.reduce(
2006
+ (sum, r) => sum + r.findings.length,
2007
+ 0
2008
+ );
2009
+ console.log(
2010
+ chalk2.bold(`
2011
+ ${totalFindings} findings from ${results.length} analyzers`)
2012
+ );
2013
+ });
2014
+ program2.command("mcp").description("Start the MCP server (stdio transport)").action(async () => {
2015
+ const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
2016
+ await startMcpServer2();
2017
+ });
2018
+ return program2;
2019
+ }
2020
+
2021
+ // bin/mason.ts
2022
+ var program = createCLI();
2023
+ program.parse();
2024
+ //# sourceMappingURL=mason.js.map