@getvibeguard/cli 1.0.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.
Files changed (128) hide show
  1. package/README.md +166 -0
  2. package/dashboard/dist/assets/Tableau10-B-NsZVaP.js +1 -0
  3. package/dashboard/dist/assets/arc-CZ1oDkm-.js +1 -0
  4. package/dashboard/dist/assets/array-BKyUJesY.js +1 -0
  5. package/dashboard/dist/assets/blockDiagram-c4efeb88-BLITAZDl.js +118 -0
  6. package/dashboard/dist/assets/c4Diagram-c83219d4-DErNj891.js +10 -0
  7. package/dashboard/dist/assets/channel-sWBGOq9p.js +1 -0
  8. package/dashboard/dist/assets/classDiagram-beda092f-Bj8IB6RW.js +2 -0
  9. package/dashboard/dist/assets/classDiagram-v2-2358418a-BZtd2g5W.js +2 -0
  10. package/dashboard/dist/assets/clone-DtEyH3ya.js +1 -0
  11. package/dashboard/dist/assets/createText-1719965b-DkNrTk-5.js +7 -0
  12. package/dashboard/dist/assets/edges-96097737-CYxl0j3g.js +4 -0
  13. package/dashboard/dist/assets/erDiagram-0228fc6a-ClEp9Atg.js +51 -0
  14. package/dashboard/dist/assets/flowDb-c6c81e3f-ClDJCJwu.js +10 -0
  15. package/dashboard/dist/assets/flowDiagram-50d868cf-DYxwClWS.js +4 -0
  16. package/dashboard/dist/assets/flowDiagram-v2-4f6560a1-D7vEnO1T.js +1 -0
  17. package/dashboard/dist/assets/flowchart-elk-definition-6af322e1-C-wX1mNx.js +139 -0
  18. package/dashboard/dist/assets/ganttDiagram-a2739b55-DTeLavAj.js +257 -0
  19. package/dashboard/dist/assets/gitGraphDiagram-82fe8481-ClxNoYya.js +70 -0
  20. package/dashboard/dist/assets/graph-CT-F3Got.js +1 -0
  21. package/dashboard/dist/assets/index-5325376f-C-jTCYZA.js +1 -0
  22. package/dashboard/dist/assets/index-CvYvquQR.js +283 -0
  23. package/dashboard/dist/assets/index-n43poL1x.css +1 -0
  24. package/dashboard/dist/assets/infoDiagram-8eee0895-Zljudo5L.js +7 -0
  25. package/dashboard/dist/assets/init-Gi6I4Gst.js +1 -0
  26. package/dashboard/dist/assets/journeyDiagram-c64418c1-DXzTbuum.js +139 -0
  27. package/dashboard/dist/assets/katex-Cu_Erd72.js +261 -0
  28. package/dashboard/dist/assets/layout-CVO3EizT.js +1 -0
  29. package/dashboard/dist/assets/line-CIgln-0z.js +1 -0
  30. package/dashboard/dist/assets/linear-bmIUMQqg.js +1 -0
  31. package/dashboard/dist/assets/mindmap-definition-8da855dc-BDLTNZYk.js +425 -0
  32. package/dashboard/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  33. package/dashboard/dist/assets/path-CbwjOpE9.js +1 -0
  34. package/dashboard/dist/assets/pieDiagram-a8764435-DtcOKNPc.js +35 -0
  35. package/dashboard/dist/assets/quadrantDiagram-1e28029f-zoSI_Ltf.js +7 -0
  36. package/dashboard/dist/assets/requirementDiagram-08caed73-TsQZ9lTB.js +52 -0
  37. package/dashboard/dist/assets/sankeyDiagram-a04cb91d-DE5ciDwD.js +8 -0
  38. package/dashboard/dist/assets/sequenceDiagram-c5b8d532-DhabPb2n.js +122 -0
  39. package/dashboard/dist/assets/stateDiagram-1ecb1508-Bg2q_YNx.js +1 -0
  40. package/dashboard/dist/assets/stateDiagram-v2-c2b004d7-Bs5iRjYB.js +1 -0
  41. package/dashboard/dist/assets/styles-b4e223ce-DchmAmav.js +160 -0
  42. package/dashboard/dist/assets/styles-ca3715f6-Bu5zjaDx.js +207 -0
  43. package/dashboard/dist/assets/styles-d45a18b0-jCaD8baR.js +116 -0
  44. package/dashboard/dist/assets/svgDrawCommon-b86b1483-BrYVGY4c.js +1 -0
  45. package/dashboard/dist/assets/timeline-definition-faaaa080--sq0bTHe.js +61 -0
  46. package/dashboard/dist/assets/xychartDiagram-f5964ef8-ByvzN0uj.js +7 -0
  47. package/dashboard/dist/index.html +22 -0
  48. package/dist/cli.d.ts +4 -0
  49. package/dist/cli.d.ts.map +1 -0
  50. package/dist/cli.js +641 -0
  51. package/dist/cli.js.map +1 -0
  52. package/dist/commands/dashboard.d.ts +2 -0
  53. package/dist/commands/dashboard.d.ts.map +1 -0
  54. package/dist/commands/dashboard.js +184 -0
  55. package/dist/commands/dashboard.js.map +1 -0
  56. package/dist/commands/init.d.ts +5 -0
  57. package/dist/commands/init.d.ts.map +1 -0
  58. package/dist/commands/init.js +501 -0
  59. package/dist/commands/init.js.map +1 -0
  60. package/dist/index.d.ts +3 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +602 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/librarian/autofix.d.ts +34 -0
  65. package/dist/librarian/autofix.d.ts.map +1 -0
  66. package/dist/librarian/autofix.js +369 -0
  67. package/dist/librarian/autofix.js.map +1 -0
  68. package/dist/librarian/heartbeat.d.ts +38 -0
  69. package/dist/librarian/heartbeat.d.ts.map +1 -0
  70. package/dist/librarian/heartbeat.js +98 -0
  71. package/dist/librarian/heartbeat.js.map +1 -0
  72. package/dist/librarian/initializer.d.ts +41 -0
  73. package/dist/librarian/initializer.d.ts.map +1 -0
  74. package/dist/librarian/initializer.js +202 -0
  75. package/dist/librarian/initializer.js.map +1 -0
  76. package/dist/librarian/memory-manager.d.ts +37 -0
  77. package/dist/librarian/memory-manager.d.ts.map +1 -0
  78. package/dist/librarian/memory-manager.js +205 -0
  79. package/dist/librarian/memory-manager.js.map +1 -0
  80. package/dist/librarian/oracle.d.ts +51 -0
  81. package/dist/librarian/oracle.d.ts.map +1 -0
  82. package/dist/librarian/oracle.js +235 -0
  83. package/dist/librarian/oracle.js.map +1 -0
  84. package/dist/librarian/summarizer.d.ts +19 -0
  85. package/dist/librarian/summarizer.d.ts.map +1 -0
  86. package/dist/librarian/summarizer.js +175 -0
  87. package/dist/librarian/summarizer.js.map +1 -0
  88. package/dist/librarian/watcher.d.ts +51 -0
  89. package/dist/librarian/watcher.d.ts.map +1 -0
  90. package/dist/librarian/watcher.js +189 -0
  91. package/dist/librarian/watcher.js.map +1 -0
  92. package/dist/mcp-server.d.ts +10 -0
  93. package/dist/mcp-server.d.ts.map +1 -0
  94. package/dist/mcp-server.js +879 -0
  95. package/dist/mcp-server.js.map +1 -0
  96. package/dist/services/token-tracker.d.ts +58 -0
  97. package/dist/services/token-tracker.d.ts.map +1 -0
  98. package/dist/services/token-tracker.js +178 -0
  99. package/dist/services/token-tracker.js.map +1 -0
  100. package/dist/utils/chunker.d.ts +5 -0
  101. package/dist/utils/chunker.d.ts.map +1 -0
  102. package/dist/utils/chunker.js +121 -0
  103. package/dist/utils/chunker.js.map +1 -0
  104. package/dist/utils/config-manager.d.ts +65 -0
  105. package/dist/utils/config-manager.d.ts.map +1 -0
  106. package/dist/utils/config-manager.js +243 -0
  107. package/dist/utils/config-manager.js.map +1 -0
  108. package/dist/utils/config.d.ts +46 -0
  109. package/dist/utils/config.d.ts.map +1 -0
  110. package/dist/utils/config.js +171 -0
  111. package/dist/utils/config.js.map +1 -0
  112. package/dist/utils/diff-cleaner.d.ts +13 -0
  113. package/dist/utils/diff-cleaner.d.ts.map +1 -0
  114. package/dist/utils/diff-cleaner.js +80 -0
  115. package/dist/utils/diff-cleaner.js.map +1 -0
  116. package/dist/utils/git.d.ts +71 -0
  117. package/dist/utils/git.d.ts.map +1 -0
  118. package/dist/utils/git.js +285 -0
  119. package/dist/utils/git.js.map +1 -0
  120. package/dist/utils/llm.d.ts +30 -0
  121. package/dist/utils/llm.d.ts.map +1 -0
  122. package/dist/utils/llm.js +308 -0
  123. package/dist/utils/llm.js.map +1 -0
  124. package/dist/utils/pricing.d.ts +47 -0
  125. package/dist/utils/pricing.d.ts.map +1 -0
  126. package/dist/utils/pricing.js +171 -0
  127. package/dist/utils/pricing.js.map +1 -0
  128. package/package.json +45 -0
@@ -0,0 +1,879 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * VibeGuard MCP Server
4
+ *
5
+ * Exposes PROJECT_MEMORY.md as an MCP tool for Cursor to access project context.
6
+ * This allows Cursor to refresh its knowledge of the project's architecture,
7
+ * tech stack, and recent decisions.
8
+ */
9
+ // MCP SDK imports - use subpath imports that match the package exports
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
13
+ import { MemoryManager } from './librarian/memory-manager.js';
14
+ import { generateSummary } from './utils/llm.js';
15
+ import { getApiKey } from './utils/config.js';
16
+ import * as fs from 'fs/promises';
17
+ import * as path from 'path';
18
+ // Create server instance
19
+ const server = new McpServer({
20
+ name: 'vibeguard-mcp',
21
+ version: '1.0.0',
22
+ }, {
23
+ capabilities: {
24
+ tools: {},
25
+ },
26
+ });
27
+ // Initialize memory manager
28
+ const repoPath = process.cwd();
29
+ const memoryManager = new MemoryManager(repoPath);
30
+ /**
31
+ * Recursively find the latest modified time of any file in a directory
32
+ */
33
+ async function getLatestMTime(dir) {
34
+ let latestMTime = null;
35
+ async function walkDir(currentDir, depth = 0) {
36
+ if (depth > 10)
37
+ return; // Limit depth to prevent infinite loops
38
+ try {
39
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ const fullPath = path.join(currentDir, entry.name);
42
+ // Skip common ignore patterns
43
+ if (entry.name.startsWith('.') ||
44
+ entry.name === 'node_modules' ||
45
+ entry.name === 'dist' ||
46
+ entry.name === 'build') {
47
+ continue;
48
+ }
49
+ if (entry.isDirectory()) {
50
+ await walkDir(fullPath, depth + 1);
51
+ }
52
+ else if (entry.isFile()) {
53
+ try {
54
+ const stats = await fs.stat(fullPath);
55
+ if (!latestMTime || stats.mtime > latestMTime) {
56
+ latestMTime = stats.mtime;
57
+ }
58
+ }
59
+ catch (error) {
60
+ // Ignore permission errors or missing files
61
+ }
62
+ }
63
+ }
64
+ }
65
+ catch (error) {
66
+ // Ignore permission errors
67
+ }
68
+ }
69
+ try {
70
+ await fs.access(dir);
71
+ await walkDir(dir);
72
+ }
73
+ catch (error) {
74
+ return null;
75
+ }
76
+ return latestMTime;
77
+ }
78
+ /**
79
+ * Scan src/ directory and return file structure (for visualization)
80
+ */
81
+ async function scanSrcDirectory() {
82
+ const srcPath = path.join(repoPath, 'src');
83
+ const files = [];
84
+ async function walkDir(dir, prefix = '', depth = 0) {
85
+ if (depth > 10)
86
+ return;
87
+ try {
88
+ const entries = await fs.readdir(dir, { withFileTypes: true });
89
+ const filtered = entries.filter(entry => {
90
+ const name = entry.name;
91
+ return (!name.startsWith('.') &&
92
+ name !== 'node_modules' &&
93
+ name !== 'dist' &&
94
+ name !== 'build');
95
+ });
96
+ for (let i = 0; i < filtered.length; i++) {
97
+ const entry = filtered[i];
98
+ const isLast = i === filtered.length - 1;
99
+ const currentPrefix = isLast ? '└── ' : '├── ';
100
+ const nextPrefix = isLast ? ' ' : '│ ';
101
+ files.push(prefix + currentPrefix + entry.name);
102
+ if (entry.isDirectory()) {
103
+ const fullPath = path.join(dir, entry.name);
104
+ await walkDir(fullPath, prefix + nextPrefix, depth + 1);
105
+ }
106
+ }
107
+ }
108
+ catch (error) {
109
+ // Ignore permission errors
110
+ }
111
+ }
112
+ try {
113
+ await fs.access(srcPath);
114
+ await walkDir(srcPath);
115
+ }
116
+ catch (error) {
117
+ throw new Error('src/ directory not found. Make sure you are in the project root.');
118
+ }
119
+ return files.join('\n');
120
+ }
121
+ /**
122
+ * Validate Mermaid syntax - check for unclosed brackets
123
+ */
124
+ function validateMermaidSyntax(mermaidCode) {
125
+ const errors = [];
126
+ const mermaidMatch = mermaidCode.match(/```mermaid\n([\s\S]*?)```/);
127
+ if (!mermaidMatch) {
128
+ errors.push('No mermaid code block found');
129
+ return { valid: false, errors };
130
+ }
131
+ const code = mermaidMatch[1];
132
+ const bracketPairs = [
133
+ { open: '[', close: ']', name: 'square brackets' },
134
+ { open: '(', close: ')', name: 'parentheses' },
135
+ { open: '{', close: '}', name: 'curly braces' },
136
+ ];
137
+ for (const pair of bracketPairs) {
138
+ let depth = 0;
139
+ for (let i = 0; i < code.length; i++) {
140
+ if (code[i] === pair.open) {
141
+ depth++;
142
+ }
143
+ else if (code[i] === pair.close) {
144
+ depth--;
145
+ if (depth < 0) {
146
+ errors.push(`Unmatched closing ${pair.name} at position ${i}`);
147
+ return { valid: false, errors };
148
+ }
149
+ }
150
+ }
151
+ if (depth > 0) {
152
+ errors.push(`Unclosed ${pair.name}: ${depth} unclosed bracket(s)`);
153
+ }
154
+ }
155
+ const subgraphOpen = (code.match(/subgraph\s+[^\s]+\s*\[/g) || []).length;
156
+ const subgraphClose = (code.match(/end\s*$/gm) || []).length;
157
+ if (subgraphOpen !== subgraphClose) {
158
+ errors.push(`Unbalanced subgraphs: ${subgraphOpen} opened, ${subgraphClose} closed`);
159
+ }
160
+ return { valid: errors.length === 0, errors };
161
+ }
162
+ /**
163
+ * Generate architecture diagram programmatically
164
+ */
165
+ async function generateDiagram() {
166
+ try {
167
+ // Validate API key is available
168
+ await getApiKey();
169
+ }
170
+ catch (error) {
171
+ console.error('Cannot generate diagram: API key not available');
172
+ return;
173
+ }
174
+ const srcPath = path.join(repoPath, 'src');
175
+ try {
176
+ await fs.access(srcPath);
177
+ }
178
+ catch (error) {
179
+ console.error('Cannot generate diagram: src/ directory not found');
180
+ return;
181
+ }
182
+ // Scan src/ directory
183
+ const fileStructure = await scanSrcDirectory();
184
+ // Read PROJECT_MEMORY.md for context
185
+ let projectMemory = '';
186
+ try {
187
+ projectMemory = await memoryManager.readMemory();
188
+ }
189
+ catch (error) {
190
+ // Continue without context
191
+ }
192
+ // Build prompt for Gemini
193
+ const today = new Date().toLocaleDateString();
194
+ const prompt = `You are a Senior Software Architect & Mermaid.js Specialist. Your task is to generate a valid Mermaid.js flowchart (TD - Top Down) that visually represents the project's architecture.
195
+
196
+ # PROJECT STRUCTURE
197
+ \`\`\`
198
+ ${fileStructure}
199
+ \`\`\`
200
+
201
+ ${projectMemory ? `# PROJECT CONTEXT\n\`\`\`\n${projectMemory}\n\`\`\`\n\n` : ''}# REQUIREMENTS
202
+ 1. Use Mermaid.js Flowchart syntax (flowchart TD)
203
+ 2. Structure:
204
+ - Use subgraphs to group "Internal Logic" vs "External Services"
205
+ - Nodes should represent key components (CLI, MCP Server, Watcher, Gemini API, File System, etc.)
206
+ - Edges should have descriptive text (e.g., "Sends Diffs", "Returns Summary", "Updates Memory")
207
+ 3. Color Styling (REQUIRED):
208
+ - Add a classDef section at the end of the Mermaid code (before the closing code block)
209
+ - Define three classes with the following colors:
210
+ * ExternalServices: fill:#e1f5fe (Light Blue)
211
+ * InternalLogic: fill:#e8f5e9 (Light Green)
212
+ * PersistenceFiles: fill:#fff8e1 (Soft Amber)
213
+ - Apply classes to subgraphs or specific nodes:
214
+ * Apply "ExternalServices" class to the "External Services" subgraph
215
+ * Apply "InternalLogic" class to the "Internal Logic" subgraph
216
+ * Apply "PersistenceFiles" class to nodes representing File System, Git Repository, or any persistence layer
217
+ - Example classDef syntax:
218
+ \`\`\`
219
+ classDef ExternalServices fill:#e1f5fe,stroke:#01579b,stroke-width:2px
220
+ classDef InternalLogic fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
221
+ classDef PersistenceFiles fill:#fff8e1,stroke:#f57f17,stroke-width:2px
222
+ \`\`\`
223
+ - Apply classes using: \`class subgraphId ExternalServices\` or \`class nodeId PersistenceFiles\`
224
+ 4. Syntax Rules:
225
+ - All node IDs must be alphanumeric (use [ ] for labels with spaces)
226
+ - Avoid special characters like & or > inside labels unless escaped
227
+ - Ensure all brackets are properly closed
228
+ - All subgraphs must have matching "end" statements
229
+ 5. Output Format:
230
+ - Wrap the output in a standard Markdown code block: \`\`\`mermaid
231
+ - Include a brief H1 header "# VibeGuard Architecture Diagram" before the code block
232
+ - Add a short "Legend" section after the diagram explaining symbols and color scheme
233
+
234
+ # OUTPUT
235
+ Return ONLY the complete Markdown content for DIAGRAM.md, including:
236
+ - H1 header
237
+ - Mermaid code block with valid syntax
238
+ - Legend section
239
+
240
+ Do not include any explanations outside the Markdown format.`;
241
+ const systemPrompt = `You are an expert at creating Mermaid.js diagrams for software architecture. You always generate valid, well-structured diagrams that follow Mermaid syntax rules strictly.`;
242
+ try {
243
+ const diagramContent = await generateSummary(prompt, systemPrompt, {
244
+ thinkingLevel: 'pro',
245
+ maxTokens: 10000,
246
+ feature: 'Librarian',
247
+ });
248
+ // Validate Mermaid syntax
249
+ const validation = validateMermaidSyntax(diagramContent);
250
+ let finalContent = diagramContent;
251
+ if (!validation.valid) {
252
+ // Try to fix common issues and regenerate
253
+ const fixPrompt = `The previous Mermaid diagram had syntax errors: ${validation.errors.join(', ')}.
254
+
255
+ Please regenerate the diagram with these fixes:
256
+ ${diagramContent}
257
+
258
+ CRITICAL REQUIREMENTS:
259
+ 1. Ensure all brackets are closed and all subgraphs have matching "end" statements.
260
+ 2. MUST include color styling with classDef:
261
+ - classDef ExternalServices fill:#e1f5fe,stroke:#01579b,stroke-width:2px
262
+ - classDef InternalLogic fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
263
+ - classDef PersistenceFiles fill:#fff8e1,stroke:#f57f17,stroke-width:2px
264
+ 3. Apply classes to subgraphs: "External Services" subgraph gets ExternalServices class, "Internal Logic" subgraph gets InternalLogic class.
265
+ 4. Apply PersistenceFiles class to File System, Git Repository, or any persistence-related nodes.`;
266
+ const fixedContent = await generateSummary(fixPrompt, systemPrompt, {
267
+ thinkingLevel: 'pro',
268
+ maxTokens: 10000,
269
+ feature: 'Librarian',
270
+ });
271
+ const fixedValidation = validateMermaidSyntax(fixedContent);
272
+ if (fixedValidation.valid) {
273
+ finalContent = fixedContent;
274
+ }
275
+ }
276
+ // Save the diagram
277
+ const diagramPath = path.join(repoPath, 'DIAGRAM.md');
278
+ await fs.writeFile(diagramPath, finalContent, 'utf-8');
279
+ }
280
+ catch (error) {
281
+ console.error('Error generating diagram:', error.message || error);
282
+ }
283
+ }
284
+ /**
285
+ * Parse entries from the Recent Decisions section
286
+ * Returns an array of entry strings (each entry may span multiple lines)
287
+ */
288
+ function parseRecentDecisionsEntries(memoryContent) {
289
+ const lines = memoryContent.split('\n');
290
+ const entries = [];
291
+ let inRecentDecisions = false;
292
+ let currentEntry = [];
293
+ for (let i = 0; i < lines.length; i++) {
294
+ const line = lines[i];
295
+ const trimmedLine = line.trim();
296
+ // Check if we've found the Recent Decisions section
297
+ if (trimmedLine.match(/^##\s+Recent\s+Decisions/i)) {
298
+ inRecentDecisions = true;
299
+ continue;
300
+ }
301
+ // If we're in the section and hit another ## header, we're done
302
+ if (inRecentDecisions && trimmedLine.match(/^##\s+/)) {
303
+ // Save last entry if exists
304
+ if (currentEntry.length > 0) {
305
+ entries.push(currentEntry.join('\n').trim());
306
+ }
307
+ break;
308
+ }
309
+ // If we're in the section, collect entries
310
+ if (inRecentDecisions) {
311
+ // Detect entry start: lines starting with "- **" or "**" (bullet point format)
312
+ const isEntryStart = /^\s*[-*]\s+\*\*/.test(line) || /^\s*\*\*/.test(line);
313
+ if (isEntryStart) {
314
+ // Save previous entry if exists
315
+ if (currentEntry.length > 0) {
316
+ entries.push(currentEntry.join('\n').trim());
317
+ }
318
+ // Start new entry
319
+ currentEntry = [line];
320
+ }
321
+ else if (currentEntry.length > 0 || trimmedLine.length > 0) {
322
+ // Continue current entry (or start if first non-empty line after header)
323
+ if (trimmedLine.length > 0 || currentEntry.length > 0) {
324
+ currentEntry.push(line);
325
+ }
326
+ }
327
+ }
328
+ }
329
+ // Save last entry if exists
330
+ if (inRecentDecisions && currentEntry.length > 0) {
331
+ entries.push(currentEntry.join('\n').trim());
332
+ }
333
+ return entries;
334
+ }
335
+ /**
336
+ * Remove the oldest N entries from Recent Decisions section
337
+ * Returns the modified content
338
+ */
339
+ function removeOldestDecisions(memoryContent, countToRemove) {
340
+ const lines = memoryContent.split('\n');
341
+ const result = [];
342
+ let inRecentDecisions = false;
343
+ let entryCount = 0;
344
+ let skipCount = 0;
345
+ let currentEntry = [];
346
+ let shouldSkipCurrentEntry = false;
347
+ for (let i = 0; i < lines.length; i++) {
348
+ const line = lines[i];
349
+ const trimmedLine = line.trim();
350
+ // Check if we've found the Recent Decisions section
351
+ if (trimmedLine.match(/^##\s+Recent\s+Decisions/i)) {
352
+ inRecentDecisions = true;
353
+ result.push(line);
354
+ continue;
355
+ }
356
+ // If we're in the section and hit another ## header, we're done
357
+ if (inRecentDecisions && trimmedLine.match(/^##\s+/)) {
358
+ // Don't add skipped entry
359
+ result.push(line);
360
+ inRecentDecisions = false;
361
+ continue;
362
+ }
363
+ // If we're in the section, process entries
364
+ if (inRecentDecisions) {
365
+ const isEntryStart = /^\s*[-*]\s+\*\*/.test(line) || /^\s*\*\*/.test(line);
366
+ if (isEntryStart) {
367
+ // Process previous entry
368
+ if (currentEntry.length > 0) {
369
+ if (!shouldSkipCurrentEntry) {
370
+ result.push(...currentEntry);
371
+ }
372
+ currentEntry = [];
373
+ }
374
+ // Determine if we should skip this entry
375
+ entryCount++;
376
+ shouldSkipCurrentEntry = skipCount < countToRemove;
377
+ if (shouldSkipCurrentEntry) {
378
+ skipCount++;
379
+ }
380
+ else {
381
+ currentEntry.push(line);
382
+ }
383
+ }
384
+ else {
385
+ // Continue current entry
386
+ if (!shouldSkipCurrentEntry) {
387
+ currentEntry.push(line);
388
+ }
389
+ }
390
+ }
391
+ else {
392
+ // Not in Recent Decisions section, copy line as-is
393
+ result.push(line);
394
+ }
395
+ }
396
+ // Handle last entry if exists
397
+ if (inRecentDecisions && currentEntry.length > 0 && !shouldSkipCurrentEntry) {
398
+ result.push(...currentEntry);
399
+ }
400
+ return result.join('\n');
401
+ }
402
+ /**
403
+ * Perform memory compaction: summarize oldest 10 entries and move to Legacy Context
404
+ */
405
+ async function performMemoryCompaction(memoryContent) {
406
+ // Extract all Recent Decisions entries
407
+ const entries = parseRecentDecisionsEntries(memoryContent);
408
+ if (entries.length <= 10) {
409
+ return memoryContent; // Not enough entries to compact
410
+ }
411
+ // Get oldest 10 entries
412
+ const oldestEntries = entries.slice(0, 10);
413
+ const entriesText = oldestEntries.join('\n\n');
414
+ // Summarize using Gemini
415
+ const today = new Date().toLocaleDateString();
416
+ const summaryPrompt = `Summarize these 10 technical decisions into 3 concise bullet points for a section called 'Legacy Context'. Each bullet should capture the essential architectural or design decision, not individual file changes.
417
+
418
+ Decisions to summarize:
419
+ ${entriesText}
420
+
421
+ Requirements:
422
+ - Output exactly 3 bullet points
423
+ - Each bullet should start with "- "
424
+ - Focus on architectural intent and "why" decisions were made
425
+ - Keep each bullet concise (1-2 sentences max)
426
+ - Use today's date (${today}) for any date references
427
+
428
+ Output only the 3 bullet points, no additional text.`;
429
+ const systemPrompt = `You are an expert at summarizing technical decisions. You create concise, high-density summaries that preserve architectural intent.`;
430
+ try {
431
+ const summary = await generateSummary(summaryPrompt, systemPrompt, {
432
+ thinkingLevel: 'pro',
433
+ maxTokens: 10000,
434
+ feature: 'Librarian',
435
+ });
436
+ // Extract bullet points from summary (clean up any markdown formatting)
437
+ let legacyBullets = summary.trim();
438
+ // Ensure each line starts with "- " and is a bullet point
439
+ const bulletLines = legacyBullets
440
+ .split('\n')
441
+ .map(line => line.trim())
442
+ .filter(line => line.length > 0)
443
+ .map(line => {
444
+ // If line doesn't start with "- ", add it
445
+ if (!line.startsWith('- ')) {
446
+ // Remove any existing markdown list markers
447
+ line = line.replace(/^[-*]\s*/, '');
448
+ return `- ${line}`;
449
+ }
450
+ return line;
451
+ });
452
+ // Ensure we have exactly 3 bullets (take first 3 if more)
453
+ const finalBullets = bulletLines.slice(0, 3).join('\n');
454
+ // Remove oldest 10 entries from Recent Decisions
455
+ let updatedContent = removeOldestDecisions(memoryContent, 10);
456
+ // Append to Legacy Context section (or create it)
457
+ // Check if Legacy Context section exists
458
+ const hasLegacySection = /^##\s+Legacy\s+Context/i.test(updatedContent);
459
+ if (hasLegacySection) {
460
+ // First, write the content with removed entries
461
+ await memoryManager.writeMemory(updatedContent);
462
+ // Then append to existing Legacy Context section
463
+ await memoryManager.appendToSection('Legacy Context', finalBullets);
464
+ // Re-read to get final updated content
465
+ updatedContent = await memoryManager.readMemory();
466
+ }
467
+ else {
468
+ // Create new Legacy Context section
469
+ const lines = updatedContent.split('\n');
470
+ // Find a good place to insert (after Active Tech Debt or at the end)
471
+ let insertIndex = lines.length;
472
+ // Look for Active Tech Debt section
473
+ for (let i = 0; i < lines.length; i++) {
474
+ if (lines[i].match(/^##\s+Active\s+Tech\s+Debt/i)) {
475
+ // Find the end of this section
476
+ for (let j = i + 1; j < lines.length; j++) {
477
+ if (lines[j].match(/^##\s+/)) {
478
+ insertIndex = j;
479
+ break;
480
+ }
481
+ }
482
+ break;
483
+ }
484
+ }
485
+ // Insert Legacy Context section
486
+ const beforeSection = lines.slice(0, insertIndex).join('\n');
487
+ const afterSection = lines.slice(insertIndex).join('\n');
488
+ updatedContent = beforeSection +
489
+ (beforeSection.endsWith('\n') ? '' : '\n') +
490
+ '\n## Legacy Context\n\n' +
491
+ finalBullets + '\n' +
492
+ (afterSection.startsWith('\n') ? '' : '\n') +
493
+ afterSection;
494
+ // Write the updated content (with removed entries and new Legacy Context section)
495
+ await memoryManager.writeMemory(updatedContent);
496
+ updatedContent = await memoryManager.readMemory();
497
+ }
498
+ return updatedContent;
499
+ }
500
+ catch (error) {
501
+ // If summarization fails, log error but don't fail the update
502
+ console.error('Librarian: Error during memory compaction:', error.message || error);
503
+ return memoryContent; // Return original content if compaction fails
504
+ }
505
+ }
506
+ /**
507
+ * Parse the Pinned Files section from PROJECT_MEMORY.md
508
+ * Returns an array of file paths, or null if section doesn't exist
509
+ */
510
+ function parsePinnedFiles(memoryContent) {
511
+ const lines = memoryContent.split('\n');
512
+ let inPinnedSection = false;
513
+ const filePaths = [];
514
+ for (const line of lines) {
515
+ const trimmedLine = line.trim();
516
+ // Check if we've found the Pinned Files section
517
+ if (trimmedLine === '## Pinned Files') {
518
+ inPinnedSection = true;
519
+ continue;
520
+ }
521
+ // If we're in the section and hit another ## header, we're done
522
+ if (inPinnedSection && trimmedLine.startsWith('## ')) {
523
+ break;
524
+ }
525
+ // If we're in the section, collect non-empty lines as file paths
526
+ if (inPinnedSection) {
527
+ // Skip empty lines
528
+ if (trimmedLine.length === 0) {
529
+ continue;
530
+ }
531
+ // Extract path (remove list markers if present)
532
+ const filePath = trimmedLine.replace(/^[-*]\s+/, '').trim();
533
+ if (filePath.length > 0) {
534
+ filePaths.push(filePath);
535
+ }
536
+ }
537
+ }
538
+ return inPinnedSection ? filePaths : null;
539
+ }
540
+ // Register the read_project_memory tool using the underlying Server instance
541
+ server.server.setRequestHandler(ListToolsRequestSchema, async () => {
542
+ return {
543
+ tools: [
544
+ {
545
+ name: 'read_project_memory',
546
+ description: "Reads the current PROJECT_MEMORY.md file to refresh knowledge of the project's architecture, tech stack, and recent decisions. Returns the complete content of the memory file.",
547
+ inputSchema: {
548
+ type: 'object',
549
+ properties: {},
550
+ required: [],
551
+ },
552
+ },
553
+ {
554
+ name: 'update_project_memory',
555
+ description: "Updates the PROJECT_MEMORY.md file by appending content to a specific section. If the section exists, appends the new content under that header. If not, appends it to the end of the file. Uses atomic writes for safety.",
556
+ inputSchema: {
557
+ type: 'object',
558
+ properties: {
559
+ content: {
560
+ type: 'string',
561
+ description: 'The text content to add to the section.',
562
+ },
563
+ section: {
564
+ type: 'string',
565
+ description: 'The section name where to add the content (e.g., "Recent Decisions", "Active Tech Debt"). Will match section headers like "## Recent Decisions".',
566
+ },
567
+ },
568
+ required: ['content', 'section'],
569
+ },
570
+ },
571
+ {
572
+ name: 'get_core_context',
573
+ description: "Reads pinned files from PROJECT_MEMORY.md's '## Pinned Files' section and returns their contents in a structured Markdown format. If the section doesn't exist, it will be created with default files (PROJECT_MEMORY.md and package.json).",
574
+ inputSchema: {
575
+ type: 'object',
576
+ properties: {},
577
+ required: [],
578
+ },
579
+ },
580
+ ],
581
+ };
582
+ });
583
+ // Handle tool execution using the underlying Server instance
584
+ // Add explicit type to silence TS7006
585
+ server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
586
+ const { name, arguments: args } = request.params;
587
+ if (name === 'read_project_memory') {
588
+ try {
589
+ // Read the PROJECT_MEMORY.md file
590
+ const memoryContent = await memoryManager.readMemory();
591
+ if (!memoryContent || memoryContent.trim().length === 0) {
592
+ return {
593
+ content: [
594
+ {
595
+ type: 'text',
596
+ text: 'PROJECT_MEMORY.md is empty or does not exist. Run `vibeguard init` to create it.',
597
+ },
598
+ ],
599
+ };
600
+ }
601
+ return {
602
+ content: [
603
+ {
604
+ type: 'text',
605
+ text: memoryContent,
606
+ },
607
+ ],
608
+ };
609
+ }
610
+ catch (error) {
611
+ return {
612
+ content: [
613
+ {
614
+ type: 'text',
615
+ text: `Error reading PROJECT_MEMORY.md: ${error.message || error}`,
616
+ },
617
+ ],
618
+ isError: true,
619
+ };
620
+ }
621
+ }
622
+ if (name === 'update_project_memory') {
623
+ try {
624
+ // Extract content and section parameters
625
+ const content = args?.content;
626
+ const section = args?.section;
627
+ if (!content || typeof content !== 'string') {
628
+ return {
629
+ content: [
630
+ {
631
+ type: 'text',
632
+ text: 'Error: content parameter is required and must be a string.',
633
+ },
634
+ ],
635
+ isError: true,
636
+ };
637
+ }
638
+ if (!section || typeof section !== 'string') {
639
+ return {
640
+ content: [
641
+ {
642
+ type: 'text',
643
+ text: 'Error: section parameter is required and must be a string.',
644
+ },
645
+ ],
646
+ isError: true,
647
+ };
648
+ }
649
+ // Memory Compaction Check: If updating "Recent Decisions", check if compaction is needed
650
+ const normalizedSection = section.trim().toLowerCase();
651
+ if (normalizedSection === 'recent decisions' || normalizedSection === 'recent decisions (the "why")') {
652
+ // Read current memory to check entry count
653
+ const currentMemory = await memoryManager.readMemory();
654
+ const entries = parseRecentDecisionsEntries(currentMemory);
655
+ // If we have more than 15 entries, trigger compaction
656
+ if (entries.length > 15) {
657
+ console.error('Librarian: Compacting 10 old decisions into Legacy Context to save tokens.');
658
+ await performMemoryCompaction(currentMemory);
659
+ }
660
+ }
661
+ // Use MemoryManager.appendToSection() which handles:
662
+ // - Finding the section or appending to end
663
+ // - Atomic writes via write-file-atomic
664
+ // - Compression if needed
665
+ // - Auto-staging in Git
666
+ await memoryManager.appendToSection(section, content);
667
+ return {
668
+ content: [
669
+ {
670
+ type: 'text',
671
+ text: `Project Memory updated successfully. Content appended to section: ${section}`,
672
+ },
673
+ ],
674
+ };
675
+ }
676
+ catch (error) {
677
+ return {
678
+ content: [
679
+ {
680
+ type: 'text',
681
+ text: `Error updating PROJECT_MEMORY.md: ${error.message || error}`,
682
+ },
683
+ ],
684
+ isError: true,
685
+ };
686
+ }
687
+ }
688
+ if (name === 'get_core_context') {
689
+ try {
690
+ // Staleness Check: Compare src/ mtime with DIAGRAM.md mtime
691
+ const srcPath = path.join(repoPath, 'src');
692
+ const diagramPath = path.join(repoPath, 'DIAGRAM.md');
693
+ let isStale = false;
694
+ let stalenessWarning = '';
695
+ try {
696
+ // Get latest mtime from src/ directory
697
+ const srcLatestMTime = await getLatestMTime(srcPath);
698
+ // Get mtime of DIAGRAM.md if it exists
699
+ let diagramMTime = null;
700
+ try {
701
+ const diagramStats = await fs.stat(diagramPath);
702
+ diagramMTime = diagramStats.mtime;
703
+ }
704
+ catch (error) {
705
+ // DIAGRAM.md doesn't exist - consider it stale
706
+ if (srcLatestMTime) {
707
+ isStale = true;
708
+ stalenessWarning = 'CONTEXT_STALE: The architecture diagram is out of date.';
709
+ }
710
+ }
711
+ // Compare timestamps if both exist
712
+ if (srcLatestMTime && diagramMTime && srcLatestMTime > diagramMTime) {
713
+ isStale = true;
714
+ stalenessWarning = 'CONTEXT_STALE: The architecture diagram is out of date.';
715
+ }
716
+ // If stale, trigger visualization automatically
717
+ if (isStale) {
718
+ console.error('Context is stale. Regenerating DIAGRAM.md...');
719
+ await generateDiagram();
720
+ // Re-check after generation
721
+ try {
722
+ const newDiagramStats = await fs.stat(diagramPath);
723
+ const newSrcMTime = await getLatestMTime(srcPath);
724
+ if (newSrcMTime && newSrcMTime > newDiagramStats.mtime) {
725
+ // Still stale after generation (files changed during generation)
726
+ stalenessWarning = 'CONTEXT_STALE: The architecture diagram is out of date.';
727
+ }
728
+ else {
729
+ // Successfully updated
730
+ stalenessWarning = '';
731
+ }
732
+ }
733
+ catch (error) {
734
+ // Diagram generation may have failed, keep warning
735
+ }
736
+ }
737
+ }
738
+ catch (error) {
739
+ // If we can't check staleness, continue without warning
740
+ console.error('Error checking staleness:', error);
741
+ }
742
+ // Read PROJECT_MEMORY.md
743
+ let memoryContent = await memoryManager.readMemory();
744
+ if (!memoryContent || memoryContent.trim().length === 0) {
745
+ return {
746
+ content: [
747
+ {
748
+ type: 'text',
749
+ text: 'PROJECT_MEMORY.md is empty or does not exist. Run `vibeguard init` to create it.',
750
+ },
751
+ ],
752
+ isError: true,
753
+ };
754
+ }
755
+ // Parse pinned files section
756
+ let filePaths = parsePinnedFiles(memoryContent);
757
+ // If section doesn't exist, create it with defaults
758
+ if (filePaths === null) {
759
+ const defaultFiles = 'PROJECT_MEMORY.md\npackage.json';
760
+ await memoryManager.appendToSection('Pinned Files', defaultFiles);
761
+ // Re-read memory to get updated content
762
+ memoryContent = await memoryManager.readMemory();
763
+ filePaths = parsePinnedFiles(memoryContent);
764
+ // If still null after creation, use defaults directly
765
+ if (filePaths === null) {
766
+ filePaths = ['PROJECT_MEMORY.md', 'package.json'];
767
+ }
768
+ }
769
+ // If no files found, use defaults
770
+ if (filePaths.length === 0) {
771
+ filePaths = ['PROJECT_MEMORY.md', 'package.json'];
772
+ }
773
+ // Check if DIAGRAM.md exists and prepend it
774
+ let diagramContent = '';
775
+ try {
776
+ diagramContent = await fs.readFile(diagramPath, 'utf-8');
777
+ }
778
+ catch (error) {
779
+ // DIAGRAM.md doesn't exist - that's fine, continue without it
780
+ }
781
+ // Read pinned files and build output
782
+ const outputParts = ['# Pinned Project Context', ''];
783
+ // Add staleness warning if present
784
+ if (stalenessWarning) {
785
+ outputParts.push(`**⚠️ ${stalenessWarning}**`, '');
786
+ outputParts.push('');
787
+ }
788
+ // Prepend DIAGRAM.md if it exists
789
+ if (diagramContent && diagramContent.trim().length > 0) {
790
+ outputParts.push('## Architecture Diagram', '');
791
+ outputParts.push(diagramContent);
792
+ outputParts.push('');
793
+ outputParts.push('---');
794
+ outputParts.push('');
795
+ }
796
+ const errors = [];
797
+ for (const filePath of filePaths) {
798
+ try {
799
+ const fullPath = path.join(repoPath, filePath);
800
+ const fileContent = await fs.readFile(fullPath, 'utf-8');
801
+ outputParts.push(`## File: ${filePath}`);
802
+ outputParts.push('');
803
+ outputParts.push(fileContent);
804
+ outputParts.push('');
805
+ }
806
+ catch (error) {
807
+ // Handle file not found gracefully
808
+ if (error.code === 'ENOENT') {
809
+ errors.push(`File not found: ${filePath}`);
810
+ // Still add the section but with a note
811
+ outputParts.push(`## File: ${filePath}`);
812
+ outputParts.push('');
813
+ outputParts.push(`*File not found: ${filePath}*`);
814
+ outputParts.push('');
815
+ }
816
+ else {
817
+ errors.push(`Error reading ${filePath}: ${error.message || error}`);
818
+ }
819
+ }
820
+ }
821
+ // Add errors at the end if any
822
+ if (errors.length > 0) {
823
+ outputParts.push('---');
824
+ outputParts.push('');
825
+ outputParts.push('## Errors');
826
+ outputParts.push('');
827
+ for (const error of errors) {
828
+ outputParts.push(`- ${error}`);
829
+ }
830
+ }
831
+ const output = outputParts.join('\n');
832
+ return {
833
+ content: [
834
+ {
835
+ type: 'text',
836
+ text: output,
837
+ },
838
+ ],
839
+ };
840
+ }
841
+ catch (error) {
842
+ return {
843
+ content: [
844
+ {
845
+ type: 'text',
846
+ text: `Error getting core context: ${error.message || error}`,
847
+ },
848
+ ],
849
+ isError: true,
850
+ };
851
+ }
852
+ }
853
+ throw new Error(`Unknown tool: ${name}`);
854
+ });
855
+ // Start the server
856
+ async function main() {
857
+ const transport = new StdioServerTransport();
858
+ // Connect the server to the transport
859
+ await server.connect(transport);
860
+ // Log to stderr (stdout is used for MCP protocol)
861
+ console.error('VibeGuard MCP Server started');
862
+ console.error('Tools available: read_project_memory, update_project_memory, get_core_context');
863
+ console.error('Waiting for MCP client connections...');
864
+ }
865
+ // Handle errors
866
+ process.on('SIGINT', async () => {
867
+ console.error('\nShutting down MCP server...');
868
+ process.exit(0);
869
+ });
870
+ process.on('SIGTERM', async () => {
871
+ console.error('\nShutting down MCP server...');
872
+ process.exit(0);
873
+ });
874
+ // Start the server
875
+ main().catch((error) => {
876
+ console.error('Fatal error starting MCP server:', error);
877
+ process.exit(1);
878
+ });
879
+ //# sourceMappingURL=mcp-server.js.map