vibe-splain 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +886 -17
  2. package/package.json +15 -6
package/dist/index.js CHANGED
@@ -1,19 +1,888 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { installCommand } from './commands/install.js';
4
- import { serveCommand } from './commands/serve.js';
5
- const program = new Command();
6
- program
7
- .name('vibe-splain')
8
- .description('Architectural dossier engine for vibe-coded projects')
9
- .version('1.0.0');
10
- program
11
- .command('install')
12
- .description('Patch coding agent MCP config files to register vibe-splain')
13
- .action(installCommand);
14
- program
15
- .command('serve')
16
- .description('Start the MCP server (called by the coding agent, not by you)')
17
- .action(serveCommand);
2
+
3
+ // dist/index.js
4
+ import { Command } from "commander";
5
+
6
+ // dist/commands/install.js
7
+ import { readFile, writeFile } from "fs/promises";
8
+ import { existsSync } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ function expandPath(p) {
12
+ if (p.startsWith("~")) {
13
+ return join(homedir(), p.slice(1));
14
+ }
15
+ if (p.startsWith("%APPDATA%")) {
16
+ const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
17
+ return join(appData, p.slice("%APPDATA%".length));
18
+ }
19
+ return p;
20
+ }
21
+ var AGENT_CONFIGS = [
22
+ { name: "Claude Code", path: "~/.claude/claude_desktop_config.json", format: "claude" },
23
+ { name: "Claude Code (Windows)", path: "%APPDATA%/Claude/claude_desktop_config.json", format: "claude" },
24
+ { name: "Gemini CLI", path: "~/.gemini/settings.json", format: "gemini" },
25
+ { name: "Cursor", path: "~/.cursor/mcp.json", format: "cursor" },
26
+ { name: "Windsurf", path: "~/.codeium/windsurf/mcp_config.json", format: "cursor" }
27
+ ];
28
+ var MCP_ENTRY = {
29
+ command: "npx",
30
+ args: ["-y", "vibe-splain", "serve"]
31
+ };
32
+ async function installCommand() {
33
+ console.error("\n\u{1F527} VIBE-SPLAIN Installer\n");
34
+ let patchedCount = 0;
35
+ for (const agent of AGENT_CONFIGS) {
36
+ const resolvedPath = expandPath(agent.path);
37
+ if (!existsSync(resolvedPath)) {
38
+ continue;
39
+ }
40
+ console.error(` Found ${agent.name} config at ${resolvedPath}`);
41
+ try {
42
+ const raw = await readFile(resolvedPath, "utf8");
43
+ let config;
44
+ try {
45
+ config = JSON.parse(raw);
46
+ } catch {
47
+ console.error(` \u26A0 Could not parse JSON, skipping`);
48
+ continue;
49
+ }
50
+ if (config.mcpServers?.["vibe-splain"]) {
51
+ console.error(` \u2713 Already configured, skipping`);
52
+ patchedCount++;
53
+ continue;
54
+ }
55
+ if (!config.mcpServers) {
56
+ config.mcpServers = {};
57
+ }
58
+ config.mcpServers["vibe-splain"] = MCP_ENTRY;
59
+ await writeFile(resolvedPath, JSON.stringify(config, null, 2), "utf8");
60
+ console.error(` \u2705 Patched successfully`);
61
+ patchedCount++;
62
+ } catch (err) {
63
+ console.error(` \u274C Error patching: ${err}`);
64
+ }
65
+ }
66
+ if (patchedCount === 0) {
67
+ console.error(`
68
+ \u26A0\uFE0F No supported coding agent config found.`);
69
+ console.error(`Add this manually to your agent's MCP config:
70
+ `);
71
+ console.error(JSON.stringify({
72
+ mcpServers: {
73
+ "vibe-splain": MCP_ENTRY
74
+ }
75
+ }, null, 2));
76
+ process.exit(1);
77
+ } else {
78
+ console.error(`
79
+ \u2705 Patched ${patchedCount} agent config(s). Restart your coding agent to activate.
80
+ `);
81
+ }
82
+ }
83
+
84
+ // dist/mcp/server.js
85
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
86
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
87
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
88
+
89
+ // ../brain/dist/scanner.js
90
+ import Parser from "web-tree-sitter";
91
+ import { join as join3, dirname, relative, extname } from "path";
92
+ import { fileURLToPath } from "url";
93
+ import { createRequire } from "module";
94
+ import { readFile as readFile3, readdir } from "fs/promises";
95
+ import { existsSync as existsSync2 } from "fs";
96
+
97
+ // ../brain/dist/graph.js
98
+ import { join as join2 } from "path";
99
+ import { readFile as readFile2, writeFile as writeFile2, mkdir } from "fs/promises";
100
+ async function readGraph(projectRoot) {
101
+ const graphPath = join2(projectRoot, ".vibe-splainer", "graph.json");
102
+ try {
103
+ const raw = await readFile2(graphPath, "utf8");
104
+ return JSON.parse(raw);
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+ async function writeGraph(projectRoot, graph) {
110
+ const dir = join2(projectRoot, ".vibe-splainer");
111
+ await mkdir(dir, { recursive: true });
112
+ const graphPath = join2(dir, "graph.json");
113
+ await writeFile2(graphPath, JSON.stringify(graph, null, 2), "utf8");
114
+ }
115
+
116
+ // ../brain/dist/scanner.js
117
+ var __dirname = dirname(fileURLToPath(import.meta.url));
118
+ var require2 = createRequire(import.meta.url);
119
+ var parser = null;
120
+ async function initParser() {
121
+ if (parser)
122
+ return parser;
123
+ await Parser.init();
124
+ parser = new Parser();
125
+ let wasmPath;
126
+ try {
127
+ const wasmsDir = dirname(require2.resolve("tree-sitter-wasms/package.json"));
128
+ wasmPath = join3(wasmsDir, "out", "tree-sitter-typescript.wasm");
129
+ if (!existsSync2(wasmPath))
130
+ throw new Error("WASM not found in package");
131
+ } catch {
132
+ wasmPath = join3(__dirname, "../wasm", "tree-sitter-typescript.wasm");
133
+ }
134
+ const Lang = await Parser.Language.load(wasmPath);
135
+ parser.setLanguage(Lang);
136
+ return parser;
137
+ }
138
+ var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".next", ".vibe-splainer", ".git"]);
139
+ var EXCLUDE_PATTERNS = [/\.test\./, /\.spec\./, /\.config\./, /\.lock$/, /\.min\.js$/, /\.d\.ts$/];
140
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
141
+ var PILLAR_KEYWORDS = {
142
+ "Auth": ["passport", "jsonwebtoken", "bcrypt", "oauth", "session", "cookie-parser"],
143
+ "Database": ["prisma", "mongoose", "sequelize", "typeorm", "knex", "pg", "mysql2"],
144
+ "Payments": ["stripe", "paypal", "braintree", "plaid"],
145
+ "Routing": ["express.Router", "fastify", "koa-router", "next/router"],
146
+ "Queue": ["bull", "bullmq", "amqplib", "kafka", "redis"],
147
+ "Storage": ["aws-sdk", "s3", "multer", "cloudinary", "@google-cloud/storage"],
148
+ "Config": ["dotenv", "convict", "zod"]
149
+ };
150
+ async function collectFiles(dir, projectRoot) {
151
+ const files = [];
152
+ const entries = await readdir(dir, { withFileTypes: true });
153
+ for (const entry of entries) {
154
+ if (EXCLUDE_DIRS.has(entry.name))
155
+ continue;
156
+ const fullPath = join3(dir, entry.name);
157
+ if (entry.isDirectory()) {
158
+ const subFiles = await collectFiles(fullPath, projectRoot);
159
+ files.push(...subFiles);
160
+ } else if (entry.isFile()) {
161
+ const ext = extname(entry.name);
162
+ if (!SUPPORTED_EXTENSIONS.has(ext))
163
+ continue;
164
+ const relPath = relative(projectRoot, fullPath);
165
+ if (EXCLUDE_PATTERNS.some((p) => p.test(relPath)))
166
+ continue;
167
+ files.push(fullPath);
168
+ }
169
+ }
170
+ return files;
171
+ }
172
+ function detectPillars(source) {
173
+ const pillars = [];
174
+ for (const [pillar, keywords] of Object.entries(PILLAR_KEYWORDS)) {
175
+ for (const kw of keywords) {
176
+ if (source.includes(kw)) {
177
+ if (!pillars.includes(pillar))
178
+ pillars.push(pillar);
179
+ break;
180
+ }
181
+ }
182
+ }
183
+ return pillars;
184
+ }
185
+ function computeNestingDepth(node, depth = 0) {
186
+ let maxDepth = depth;
187
+ const nestingTypes = /* @__PURE__ */ new Set([
188
+ "function_declaration",
189
+ "function",
190
+ "arrow_function",
191
+ "method_definition",
192
+ "class_declaration",
193
+ "class",
194
+ "if_statement",
195
+ "for_statement",
196
+ "for_in_statement",
197
+ "while_statement",
198
+ "do_statement",
199
+ "switch_statement",
200
+ "try_statement",
201
+ "catch_clause"
202
+ ]);
203
+ for (const child of node.children) {
204
+ if (nestingTypes.has(child.type)) {
205
+ const childMax = computeNestingDepth(child, depth + 1);
206
+ maxDepth = Math.max(maxDepth, childMax);
207
+ } else {
208
+ const childMax = computeNestingDepth(child, depth);
209
+ maxDepth = Math.max(maxDepth, childMax);
210
+ }
211
+ }
212
+ return maxDepth;
213
+ }
214
+ function countMutations(node) {
215
+ let count = 0;
216
+ if (node.type === "assignment_expression" || node.type === "augmented_assignment_expression") {
217
+ const parent = node.parent;
218
+ if (!parent || parent.type !== "variable_declarator" || !parent.parent || parent.parent.type !== "lexical_declaration" || parent.parent.children[0]?.text !== "const") {
219
+ count++;
220
+ }
221
+ }
222
+ for (const child of node.children) {
223
+ count += countMutations(child);
224
+ }
225
+ return count;
226
+ }
227
+ function countImports(node) {
228
+ let count = 0;
229
+ for (const child of node.children) {
230
+ if (child.type === "import_statement" || child.type === "import_declaration") {
231
+ count++;
232
+ }
233
+ }
234
+ return count;
235
+ }
236
+ function extractImportPaths(source) {
237
+ const paths = [];
238
+ const importRegex = /(?:import|require)\s*\(?\s*['"]([^'"]+)['"]/g;
239
+ let match;
240
+ while ((match = importRegex.exec(source)) !== null) {
241
+ paths.push(match[1]);
242
+ }
243
+ return paths;
244
+ }
245
+ async function scanProject(projectRoot) {
246
+ const p = await initParser();
247
+ const files = await collectFiles(projectRoot, projectRoot);
248
+ const graph = { nodes: {}, edges: [] };
249
+ const fileImportMap = /* @__PURE__ */ new Map();
250
+ const reverseImportCount = /* @__PURE__ */ new Map();
251
+ for (const file of files) {
252
+ const source = await readFile3(file, "utf8");
253
+ const relPath = relative(projectRoot, file);
254
+ const importPaths = extractImportPaths(source);
255
+ fileImportMap.set(file, importPaths);
256
+ graph.nodes[relPath] = { imports: importPaths };
257
+ for (const imp of importPaths) {
258
+ if (imp.startsWith(".")) {
259
+ const resolvedDir = dirname(file);
260
+ const resolved = join3(resolvedDir, imp);
261
+ for (const ext of [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"]) {
262
+ const candidate = resolved.endsWith(ext) ? resolved : resolved + ext;
263
+ const relCandidate = relative(projectRoot, candidate);
264
+ reverseImportCount.set(relCandidate, (reverseImportCount.get(relCandidate) || 0) + 1);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ const allAnalyzed = [];
270
+ for (const file of files) {
271
+ const source = await readFile3(file, "utf8");
272
+ const relPath = relative(projectRoot, file);
273
+ const pillars = detectPillars(source);
274
+ let tree;
275
+ try {
276
+ tree = p.parse(source);
277
+ } catch {
278
+ continue;
279
+ }
280
+ const importCount = countImports(tree.rootNode);
281
+ const reverseCount = reverseImportCount.get(relPath) || 0;
282
+ const linkDensity = importCount + reverseCount;
283
+ const nestingDepth = computeNestingDepth(tree.rootNode);
284
+ const mutationCount = countMutations(tree.rootNode);
285
+ const cognitiveWeight = linkDensity * 2 + nestingDepth + mutationCount * 1.5;
286
+ const importPaths = fileImportMap.get(file) || [];
287
+ for (const imp of importPaths) {
288
+ graph.edges.push({ from: relPath, to: imp });
289
+ }
290
+ allAnalyzed.push({
291
+ path: file,
292
+ relativePath: relPath,
293
+ cognitiveWeight,
294
+ linkDensity,
295
+ nestingDepth,
296
+ mutationCount,
297
+ pillars
298
+ });
299
+ }
300
+ const highGravityFiles = allAnalyzed.filter((f) => f.cognitiveWeight >= 15).sort((a, b) => b.cognitiveWeight - a.cognitiveWeight);
301
+ const pillarMap = /* @__PURE__ */ new Map();
302
+ const untaggedFiles = [];
303
+ for (const file of highGravityFiles) {
304
+ if (file.pillars.length > 0) {
305
+ for (const pillar of file.pillars) {
306
+ if (!pillarMap.has(pillar))
307
+ pillarMap.set(pillar, []);
308
+ pillarMap.get(pillar).push(file);
309
+ }
310
+ } else {
311
+ untaggedFiles.push(file);
312
+ }
313
+ }
314
+ const dirGroups = /* @__PURE__ */ new Map();
315
+ for (const file of untaggedFiles) {
316
+ const dir = dirname(file.relativePath);
317
+ if (!dirGroups.has(dir))
318
+ dirGroups.set(dir, []);
319
+ dirGroups.get(dir).push(file);
320
+ }
321
+ const pillarGroups = [];
322
+ for (const [name, files2] of pillarMap) {
323
+ pillarGroups.push({ name, files: files2 });
324
+ }
325
+ for (const [name, files2] of dirGroups) {
326
+ pillarGroups.push({ name, files: files2 });
327
+ }
328
+ const wildCandidates = highGravityFiles.filter((f) => f.cognitiveWeight >= 25);
329
+ await writeGraph(projectRoot, graph);
330
+ const uiUrl = `file://${join3(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
331
+ return {
332
+ projectRoot,
333
+ totalFilesScanned: files.length,
334
+ highGravityFiles,
335
+ pillarGroups,
336
+ wildCandidates,
337
+ uiUrl,
338
+ graph
339
+ };
340
+ }
341
+
342
+ // ../brain/dist/dossier.js
343
+ import { Mutex } from "async-mutex";
344
+ import { join as join4, dirname as dirname2 } from "path";
345
+ import { fileURLToPath as fileURLToPath2 } from "url";
346
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
347
+ import { existsSync as existsSync3, cpSync } from "fs";
348
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
349
+ var dossierMutex = new Mutex();
350
+ async function readDossier(projectRoot) {
351
+ const dossierPath = join4(projectRoot, ".vibe-splainer", "dossier.json");
352
+ try {
353
+ const raw = await readFile4(dossierPath, "utf8");
354
+ return JSON.parse(raw);
355
+ } catch {
356
+ return null;
357
+ }
358
+ }
359
+ async function writeDossier(projectRoot, dossier) {
360
+ await dossierMutex.runExclusive(async () => {
361
+ const dir = join4(projectRoot, ".vibe-splainer");
362
+ await mkdir2(dir, { recursive: true });
363
+ const dossierPath = join4(dir, "dossier.json");
364
+ const tmp = dossierPath + ".tmp";
365
+ await writeFile3(tmp, JSON.stringify(dossier, null, 2), "utf8");
366
+ const { rename } = await import("fs/promises");
367
+ await rename(tmp, dossierPath);
368
+ await regenerateUI(projectRoot, dossier);
369
+ });
370
+ }
371
+ async function regenerateUI(projectRoot, dossier) {
372
+ const uiDir = join4(projectRoot, ".vibe-splainer", "ui");
373
+ await mkdir2(uiDir, { recursive: true });
374
+ const templateDir = join4(__dirname2, "../../cli/dist/ui");
375
+ if (!existsSync3(templateDir)) {
376
+ console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
377
+ return;
378
+ }
379
+ cpSync(templateDir, uiDir, { recursive: true });
380
+ let html = await readFile4(join4(templateDir, "index.html"), "utf8");
381
+ const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(dossier)};</script>`;
382
+ html = html.replace("</head>", `${injection}
383
+ </head>`);
384
+ await writeFile3(join4(uiDir, "index.html"), html, "utf8");
385
+ console.error("[vibe-splain] UI regenerated at", join4(uiDir, "index.html"));
386
+ }
387
+ function validateMermaidNodeCount(diagram) {
388
+ if (!diagram)
389
+ return true;
390
+ const nodePattern = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*[\[({|>]/gm;
391
+ const statePattern = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:/gm;
392
+ const nodes = /* @__PURE__ */ new Set();
393
+ for (const match of diagram.matchAll(nodePattern))
394
+ nodes.add(match[1]);
395
+ for (const match of diagram.matchAll(statePattern))
396
+ nodes.add(match[1]);
397
+ if (diagram.includes("[*]"))
398
+ nodes.add("[*]");
399
+ return nodes.size <= 7;
400
+ }
401
+
402
+ // ../brain/dist/watcher.js
403
+ import chokidar from "chokidar";
404
+ import { createHash } from "crypto";
405
+ import { readFile as readFile5 } from "fs/promises";
406
+ function startWatcher(projectRoot, watchedPaths) {
407
+ const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
408
+ ignoreInitial: true,
409
+ ignored: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.vibe-splainer/**"],
410
+ persistent: true
411
+ });
412
+ watcher.on("change", async (filepath) => {
413
+ try {
414
+ const dossier = await readDossier(projectRoot);
415
+ if (!dossier)
416
+ return;
417
+ const content = await readFile5(filepath, "utf8");
418
+ const newHash = createHash("sha256").update(content).digest("hex");
419
+ let mutated = false;
420
+ for (const pillar of dossier.pillars) {
421
+ for (const card of pillar.decisions) {
422
+ if (card.evidence.some((e) => e.file === filepath || filepath.endsWith(e.file))) {
423
+ if (card.lastScannedHash !== newHash) {
424
+ card.status = "stale";
425
+ if (!dossier.stalePaths.includes(filepath))
426
+ dossier.stalePaths.push(filepath);
427
+ mutated = true;
428
+ }
429
+ }
430
+ }
431
+ }
432
+ if (mutated)
433
+ await writeDossier(projectRoot, dossier);
434
+ } catch (err) {
435
+ console.error("[vibe-splain] Watcher error:", err);
436
+ }
437
+ });
438
+ console.error("[vibe-splain] File watcher started");
439
+ }
440
+
441
+ // dist/mcp/tools/scan_project.js
442
+ var scanProjectTool = {
443
+ name: "scan_project",
444
+ description: "Scans a codebase and returns its structural analysis. CALL THIS FIRST before any other tool. Returns High-Gravity files grouped by pillar, plus wildCandidates for unusual high-complexity files. After calling this tool, call get_file_context for each file in highGravityFiles, synthesize a narrative explaining WHY that code exists, then call write_decision_card to persist it. The uiUrl in the response is a file:// link \u2014 share it with the user so they can open the Dossier UI in their browser.",
445
+ inputSchema: {
446
+ type: "object",
447
+ properties: {
448
+ projectRoot: {
449
+ type: "string",
450
+ description: "Absolute path to the project root directory to scan"
451
+ }
452
+ },
453
+ required: ["projectRoot"]
454
+ }
455
+ };
456
+ async function handleScanProject(args) {
457
+ const projectRoot = args.projectRoot;
458
+ if (!projectRoot)
459
+ throw new Error("projectRoot is required");
460
+ console.error(`[vibe-splain] Scanning project: ${projectRoot}`);
461
+ const result = await scanProject(projectRoot);
462
+ const existingDossier = await readDossier(projectRoot);
463
+ const dossier = existingDossier || {
464
+ version: "1.0.0",
465
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
466
+ projectRoot,
467
+ pillars: [],
468
+ wildDiscoveries: [],
469
+ stalePaths: []
470
+ };
471
+ dossier.scannedAt = (/* @__PURE__ */ new Date()).toISOString();
472
+ for (const group of result.pillarGroups) {
473
+ const existingPillar = dossier.pillars.find((p) => p.name === group.name);
474
+ if (!existingPillar) {
475
+ dossier.pillars.push({ name: group.name, cardCount: 0, decisions: [] });
476
+ }
477
+ }
478
+ await writeDossier(projectRoot, dossier);
479
+ const watchPaths = result.highGravityFiles.map((f) => f.path);
480
+ startWatcher(projectRoot, watchPaths);
481
+ console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files scanned, ${result.highGravityFiles.length} high-gravity files found.`);
482
+ return {
483
+ projectRoot: result.projectRoot,
484
+ totalFilesScanned: result.totalFilesScanned,
485
+ highGravityFiles: result.highGravityFiles.map((f) => ({
486
+ relativePath: f.relativePath,
487
+ cognitiveWeight: f.cognitiveWeight,
488
+ pillars: f.pillars
489
+ })),
490
+ pillarGroups: result.pillarGroups.map((g) => ({
491
+ name: g.name,
492
+ fileCount: g.files.length,
493
+ files: g.files.map((f) => f.relativePath)
494
+ })),
495
+ wildCandidates: result.wildCandidates.map((f) => ({
496
+ relativePath: f.relativePath,
497
+ cognitiveWeight: f.cognitiveWeight
498
+ })),
499
+ uiUrl: result.uiUrl
500
+ };
501
+ }
502
+
503
+ // dist/mcp/tools/get_file_context.js
504
+ import { readFile as readFile6 } from "fs/promises";
505
+ import { join as join5, relative as relative2 } from "path";
506
+ var getFileContextTool = {
507
+ name: "get_file_context",
508
+ description: "Returns the full source code of a specific high-gravity file, its cognitive weight breakdown, and its import graph neighbors. Call this for each file you want to synthesize a Decision Card for. Use the source + neighbors to understand what the code does and WHY it was written that way.",
509
+ inputSchema: {
510
+ type: "object",
511
+ properties: {
512
+ projectRoot: {
513
+ type: "string",
514
+ description: "Absolute path to the project root"
515
+ },
516
+ filePath: {
517
+ type: "string",
518
+ description: "Relative or absolute path to the file"
519
+ }
520
+ },
521
+ required: ["projectRoot", "filePath"]
522
+ }
523
+ };
524
+ async function handleGetFileContext(args) {
525
+ const projectRoot = args.projectRoot;
526
+ const filePath = args.filePath;
527
+ if (!projectRoot || !filePath)
528
+ throw new Error("projectRoot and filePath are required");
529
+ const fullPath = filePath.startsWith("/") ? filePath : join5(projectRoot, filePath);
530
+ const relPath = relative2(projectRoot, fullPath);
531
+ const source = await readFile6(fullPath, "utf8");
532
+ const graph = await readGraph(projectRoot);
533
+ const neighbors = [];
534
+ if (graph) {
535
+ for (const edge of graph.edges) {
536
+ if (edge.from === relPath)
537
+ neighbors.push(edge.to);
538
+ if (edge.to === relPath || edge.to.endsWith(relPath))
539
+ neighbors.push(edge.from);
540
+ }
541
+ }
542
+ return {
543
+ filePath: relPath,
544
+ source,
545
+ lineCount: source.split("\n").length,
546
+ neighbors: [...new Set(neighbors)]
547
+ };
548
+ }
549
+
550
+ // dist/mcp/tools/write_decision_card.js
551
+ import { v4 as uuidv4 } from "uuid";
552
+ import { createHash as createHash2 } from "crypto";
553
+ import { readFile as readFile7 } from "fs/promises";
554
+ import { join as join6 } from "path";
555
+ var writeDecisionCardTool = {
556
+ name: "write_decision_card",
557
+ description: "Persists a Decision Card you have synthesized to the project's dossier. The narrative should be 3\u20135 sentences explaining WHY this code exists. Evidence must reference specific line ranges from the actual source. Diagrams are optional but use only stateDiagram-v2, flowchart TD, or linear A-->B-->C style, max 7 nodes. Will reject diagrams with more than 7 nodes.",
558
+ inputSchema: {
559
+ type: "object",
560
+ properties: {
561
+ projectRoot: {
562
+ type: "string",
563
+ description: "Absolute path to the project root"
564
+ },
565
+ pillar: {
566
+ type: "string",
567
+ description: "The pillar this card belongs to (e.g., Auth, Database, etc.)"
568
+ },
569
+ title: {
570
+ type: "string",
571
+ description: "Short title for the decision card"
572
+ },
573
+ narrative: {
574
+ type: "string",
575
+ description: "3-5 sentences explaining WHY this code exists"
576
+ },
577
+ evidence: {
578
+ type: "array",
579
+ items: {
580
+ type: "object",
581
+ properties: {
582
+ file: { type: "string", description: "Relative file path" },
583
+ startLine: { type: "number", description: "Start line number" },
584
+ endLine: { type: "number", description: "End line number" },
585
+ snippet: { type: "string", description: "Code snippet from the file" }
586
+ },
587
+ required: ["file", "startLine", "endLine", "snippet"]
588
+ },
589
+ description: "Array of evidence items referencing specific code"
590
+ },
591
+ diagram: {
592
+ type: "string",
593
+ description: "Optional Mermaid diagram (stateDiagram-v2, flowchart TD, or linear style). Max 7 nodes."
594
+ }
595
+ },
596
+ required: ["projectRoot", "pillar", "title", "narrative", "evidence"]
597
+ }
598
+ };
599
+ async function handleWriteDecisionCard(args) {
600
+ const projectRoot = args.projectRoot;
601
+ const pillar = args.pillar;
602
+ const title = args.title;
603
+ const narrative = args.narrative;
604
+ const evidence = args.evidence;
605
+ const diagram = args.diagram || null;
606
+ if (!projectRoot || !pillar || !title || !narrative || !evidence) {
607
+ throw new Error("projectRoot, pillar, title, narrative, and evidence are required");
608
+ }
609
+ if (diagram && !validateMermaidNodeCount(diagram)) {
610
+ throw new Error("Mermaid diagram exceeds maximum of 7 nodes. Simplify the diagram.");
611
+ }
612
+ let combinedContent = "";
613
+ for (const e of evidence) {
614
+ try {
615
+ const fullPath = join6(projectRoot, e.file);
616
+ const content = await readFile7(fullPath, "utf8");
617
+ combinedContent += content;
618
+ } catch {
619
+ combinedContent += e.snippet;
620
+ }
621
+ }
622
+ const hash = createHash2("sha256").update(combinedContent).digest("hex");
623
+ const card = {
624
+ id: uuidv4(),
625
+ pillar,
626
+ title,
627
+ narrative,
628
+ evidence,
629
+ diagram,
630
+ status: "fresh",
631
+ lastScannedHash: hash
632
+ };
633
+ let dossier = await readDossier(projectRoot);
634
+ if (!dossier) {
635
+ dossier = {
636
+ version: "1.0.0",
637
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
638
+ projectRoot,
639
+ pillars: [],
640
+ wildDiscoveries: [],
641
+ stalePaths: []
642
+ };
643
+ }
644
+ let existingPillar = dossier.pillars.find((p) => p.name === pillar);
645
+ if (!existingPillar) {
646
+ existingPillar = { name: pillar, cardCount: 0, decisions: [] };
647
+ dossier.pillars.push(existingPillar);
648
+ }
649
+ existingPillar.decisions.push(card);
650
+ existingPillar.cardCount = existingPillar.decisions.length;
651
+ await writeDossier(projectRoot, dossier);
652
+ console.error(`[vibe-splain] Decision card written: "${title}" in pillar "${pillar}"`);
653
+ return {
654
+ success: true,
655
+ cardId: card.id,
656
+ pillar,
657
+ title
658
+ };
659
+ }
660
+
661
+ // dist/mcp/tools/get_strategic_overview.js
662
+ var getStrategicOverviewTool = {
663
+ name: "get_strategic_overview",
664
+ description: "Returns the current state of the dossier without evidence snippets (to save tokens). Use this to see what has already been analyzed and what is stale. Check stalePaths to know which files need re-analysis.",
665
+ inputSchema: {
666
+ type: "object",
667
+ properties: {
668
+ projectRoot: {
669
+ type: "string",
670
+ description: "Absolute path to the project root"
671
+ }
672
+ },
673
+ required: ["projectRoot"]
674
+ }
675
+ };
676
+ async function handleGetStrategicOverview(args) {
677
+ const projectRoot = args.projectRoot;
678
+ if (!projectRoot)
679
+ throw new Error("projectRoot is required");
680
+ const dossier = await readDossier(projectRoot);
681
+ if (!dossier) {
682
+ return { error: "No dossier found. Run scan_project first." };
683
+ }
684
+ return {
685
+ version: dossier.version,
686
+ scannedAt: dossier.scannedAt,
687
+ projectRoot: dossier.projectRoot,
688
+ pillars: dossier.pillars.map((p) => ({
689
+ name: p.name,
690
+ cardCount: p.cardCount,
691
+ decisions: p.decisions.map((d) => ({
692
+ id: d.id,
693
+ title: d.title,
694
+ status: d.status,
695
+ pillar: d.pillar,
696
+ // Omit evidence snippets to save tokens
697
+ evidenceFileCount: d.evidence.length,
698
+ hasDiagram: !!d.diagram
699
+ }))
700
+ })),
701
+ wildDiscoveriesCount: dossier.wildDiscoveries.length,
702
+ stalePaths: dossier.stalePaths
703
+ };
704
+ }
705
+
706
+ // dist/mcp/tools/inspect_pillar.js
707
+ var inspectPillarTool = {
708
+ name: "inspect_pillar",
709
+ description: "Returns all Decision Cards for a specific pillar including full evidence snippets. Use when you need deep detail on a specific area of the codebase.",
710
+ inputSchema: {
711
+ type: "object",
712
+ properties: {
713
+ projectRoot: {
714
+ type: "string",
715
+ description: "Absolute path to the project root"
716
+ },
717
+ pillarName: {
718
+ type: "string",
719
+ description: "Name of the pillar to inspect"
720
+ }
721
+ },
722
+ required: ["projectRoot", "pillarName"]
723
+ }
724
+ };
725
+ async function handleInspectPillar(args) {
726
+ const projectRoot = args.projectRoot;
727
+ const pillarName = args.pillarName;
728
+ if (!projectRoot || !pillarName)
729
+ throw new Error("projectRoot and pillarName are required");
730
+ const dossier = await readDossier(projectRoot);
731
+ if (!dossier) {
732
+ return { error: "No dossier found. Run scan_project first." };
733
+ }
734
+ const pillar = dossier.pillars.find((p) => p.name === pillarName);
735
+ if (!pillar) {
736
+ return {
737
+ error: `Pillar "${pillarName}" not found. Available pillars: ${dossier.pillars.map((p) => p.name).join(", ")}`
738
+ };
739
+ }
740
+ return pillar;
741
+ }
742
+
743
+ // dist/mcp/tools/get_wild_discoveries.js
744
+ var getWildDiscoveriesTool = {
745
+ name: "get_wild_discoveries",
746
+ description: "Returns files with extremely high cognitive complexity (weight \u2265 25) that don't fit standard patterns. These are the most surprising and important parts of the codebase to understand.",
747
+ inputSchema: {
748
+ type: "object",
749
+ properties: {
750
+ projectRoot: {
751
+ type: "string",
752
+ description: "Absolute path to the project root"
753
+ }
754
+ },
755
+ required: ["projectRoot"]
756
+ }
757
+ };
758
+ async function handleGetWildDiscoveries(args) {
759
+ const projectRoot = args.projectRoot;
760
+ if (!projectRoot)
761
+ throw new Error("projectRoot is required");
762
+ const dossier = await readDossier(projectRoot);
763
+ if (!dossier) {
764
+ return { error: "No dossier found. Run scan_project first." };
765
+ }
766
+ return {
767
+ wildDiscoveries: dossier.wildDiscoveries,
768
+ count: dossier.wildDiscoveries.length
769
+ };
770
+ }
771
+
772
+ // dist/mcp/tools/mark_stale.js
773
+ var markStaleTool = {
774
+ name: "mark_stale",
775
+ description: "Manually marks Decision Cards as stale when you detect a file has changed. The file watcher does this automatically, but call this if you modify a file yourself during a session.",
776
+ inputSchema: {
777
+ type: "object",
778
+ properties: {
779
+ projectRoot: {
780
+ type: "string",
781
+ description: "Absolute path to the project root"
782
+ },
783
+ filePaths: {
784
+ type: "array",
785
+ items: { type: "string" },
786
+ description: "Array of relative file paths to mark as stale"
787
+ }
788
+ },
789
+ required: ["projectRoot", "filePaths"]
790
+ }
791
+ };
792
+ async function handleMarkStale(args) {
793
+ const projectRoot = args.projectRoot;
794
+ const filePaths = args.filePaths;
795
+ if (!projectRoot || !filePaths)
796
+ throw new Error("projectRoot and filePaths are required");
797
+ const dossier = await readDossier(projectRoot);
798
+ if (!dossier) {
799
+ return { error: "No dossier found. Run scan_project first." };
800
+ }
801
+ let staleCount = 0;
802
+ for (const filePath of filePaths) {
803
+ for (const pillar of dossier.pillars) {
804
+ for (const card of pillar.decisions) {
805
+ if (card.evidence.some((e) => e.file === filePath || filePath.endsWith(e.file))) {
806
+ card.status = "stale";
807
+ staleCount++;
808
+ }
809
+ }
810
+ }
811
+ if (!dossier.stalePaths.includes(filePath)) {
812
+ dossier.stalePaths.push(filePath);
813
+ }
814
+ }
815
+ await writeDossier(projectRoot, dossier);
816
+ return {
817
+ success: true,
818
+ staleCardsMarked: staleCount,
819
+ totalStalePaths: dossier.stalePaths.length
820
+ };
821
+ }
822
+
823
+ // dist/mcp/server.js
824
+ var ALL_TOOLS = [
825
+ scanProjectTool,
826
+ getFileContextTool,
827
+ writeDecisionCardTool,
828
+ getStrategicOverviewTool,
829
+ inspectPillarTool,
830
+ getWildDiscoveriesTool,
831
+ markStaleTool
832
+ ];
833
+ var TOOL_HANDLERS = {
834
+ scan_project: handleScanProject,
835
+ get_file_context: handleGetFileContext,
836
+ write_decision_card: handleWriteDecisionCard,
837
+ get_strategic_overview: handleGetStrategicOverview,
838
+ inspect_pillar: handleInspectPillar,
839
+ get_wild_discoveries: handleGetWildDiscoveries,
840
+ mark_stale: handleMarkStale
841
+ };
842
+ async function startMCPServer() {
843
+ await initParser();
844
+ console.error("[vibe-splain] Tree-Sitter parser initialized");
845
+ const server = new Server({ name: "vibe-splain", version: "1.0.0" }, { capabilities: { tools: {} } });
846
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
847
+ tools: ALL_TOOLS
848
+ }));
849
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
850
+ const { name, arguments: args } = request.params;
851
+ const handler = TOOL_HANDLERS[name];
852
+ if (!handler) {
853
+ return {
854
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
855
+ isError: true
856
+ };
857
+ }
858
+ try {
859
+ const result = await handler(args || {});
860
+ return {
861
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
862
+ };
863
+ } catch (err) {
864
+ const message = err instanceof Error ? err.message : String(err);
865
+ console.error(`[vibe-splain] Tool ${name} error:`, message);
866
+ return {
867
+ content: [{ type: "text", text: `Error: ${message}` }],
868
+ isError: true
869
+ };
870
+ }
871
+ });
872
+ const transport = new StdioServerTransport();
873
+ await server.connect(transport);
874
+ console.error("[vibe-splain] MCP server running on stdio");
875
+ }
876
+
877
+ // dist/commands/serve.js
878
+ async function serveCommand() {
879
+ console.error("[vibe-splain] Starting MCP server...");
880
+ await startMCPServer();
881
+ }
882
+
883
+ // dist/index.js
884
+ var program = new Command();
885
+ program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("1.0.0");
886
+ program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
887
+ program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").action(serveCommand);
18
888
  program.parse();
19
- //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-splain",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Architectural dossier engine for vibe-coded projects. Runs as an MCP server inside your coding agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,21 +29,30 @@
29
29
  "engines": {
30
30
  "node": ">=18"
31
31
  },
32
- "bin": { "vibe-splain": "./dist/index.js" },
32
+ "bin": {
33
+ "vibe-splain": "./dist/index.js"
34
+ },
33
35
  "scripts": {
34
- "build": "tsc"
36
+ "build": "tsc && node build.mjs"
35
37
  },
36
38
  "dependencies": {
37
- "@vibe-splain/brain": "*",
38
39
  "@modelcontextprotocol/sdk": "^1.0.0",
40
+ "async-mutex": "^0.5.0",
41
+ "chokidar": "^3.6.0",
39
42
  "commander": "^12.0.0",
40
43
  "fs-extra": "^11.0.0",
41
- "uuid": "^9.0.0"
44
+ "tree-sitter-wasms": "^0.1.11",
45
+ "uuid": "^9.0.0",
46
+ "web-tree-sitter": "^0.22.0"
42
47
  },
43
48
  "devDependencies": {
44
49
  "@types/fs-extra": "^11.0.0",
45
50
  "@types/uuid": "^9.0.0",
51
+ "esbuild": "^0.28.0",
46
52
  "typescript": "^5.4.0"
47
53
  },
48
- "files": ["dist/", "!dist/**/*.map"]
54
+ "files": [
55
+ "dist/",
56
+ "!dist/**/*.map"
57
+ ]
49
58
  }