@vibecheckai/cli 3.9.0 → 4.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 (88) hide show
  1. package/README.md +1 -1
  2. package/bin/runners/context/generators/cursor-enhanced.js +99 -13
  3. package/bin/runners/lib/unified-cli-output.js +16 -0
  4. package/bin/runners/runCI.js +353 -0
  5. package/bin/runners/runCheckpoint.js +2 -2
  6. package/mcp-server/.eslintrc.json +24 -0
  7. package/mcp-server/README.md +425 -135
  8. package/mcp-server/SPEC.md +583 -0
  9. package/mcp-server/configs/README.md +172 -0
  10. package/mcp-server/configs/claude-desktop-pro.json +31 -0
  11. package/mcp-server/configs/claude-desktop-with-workspace.json +25 -0
  12. package/mcp-server/configs/claude-desktop.json +19 -0
  13. package/mcp-server/configs/cursor-mcp.json +21 -0
  14. package/mcp-server/configs/windsurf-mcp.json +17 -0
  15. package/mcp-server/mcp-config.example.json +9 -0
  16. package/mcp-server/package.json +49 -34
  17. package/mcp-server/src/cli.ts +185 -0
  18. package/mcp-server/src/index.ts +85 -0
  19. package/mcp-server/src/server.ts +1933 -0
  20. package/mcp-server/src/services/cache-service.ts +466 -0
  21. package/mcp-server/src/services/cli-service.ts +345 -0
  22. package/mcp-server/src/services/context-manager.ts +717 -0
  23. package/mcp-server/src/services/firewall-service.ts +662 -0
  24. package/mcp-server/src/services/git-service.ts +671 -0
  25. package/mcp-server/src/services/index.ts +52 -0
  26. package/mcp-server/src/services/prompt-builder-service.ts +1031 -0
  27. package/mcp-server/src/services/session-service.ts +550 -0
  28. package/mcp-server/src/services/tier-service.ts +470 -0
  29. package/mcp-server/src/types.ts +351 -0
  30. package/mcp-server/tsconfig.json +16 -27
  31. package/package.json +6 -6
  32. package/mcp-server/.guardrail/audit/audit.log.jsonl +0 -2
  33. package/mcp-server/.specs/architecture.mdc +0 -90
  34. package/mcp-server/.specs/security.mdc +0 -30
  35. package/mcp-server/HARDENING_SUMMARY.md +0 -299
  36. package/mcp-server/agent-checkpoint.js +0 -364
  37. package/mcp-server/agent-firewall-interceptor.js +0 -500
  38. package/mcp-server/architect-tools.js +0 -707
  39. package/mcp-server/audit-mcp.js +0 -206
  40. package/mcp-server/authority-tools.js +0 -569
  41. package/mcp-server/codebase-architect-tools.js +0 -838
  42. package/mcp-server/conductor/conflict-resolver.js +0 -588
  43. package/mcp-server/conductor/execution-planner.js +0 -544
  44. package/mcp-server/conductor/index.js +0 -377
  45. package/mcp-server/conductor/lock-manager.js +0 -615
  46. package/mcp-server/conductor/request-queue.js +0 -550
  47. package/mcp-server/conductor/session-manager.js +0 -500
  48. package/mcp-server/conductor/tools.js +0 -510
  49. package/mcp-server/consolidated-tools.js +0 -1170
  50. package/mcp-server/deprecation-middleware.js +0 -282
  51. package/mcp-server/handlers/index.ts +0 -15
  52. package/mcp-server/handlers/tool-handler.ts +0 -593
  53. package/mcp-server/hygiene-tools.js +0 -428
  54. package/mcp-server/index-v1.js +0 -698
  55. package/mcp-server/index.js +0 -2940
  56. package/mcp-server/intelligence-tools.js +0 -664
  57. package/mcp-server/intent-drift-tools.js +0 -873
  58. package/mcp-server/intent-firewall-interceptor.js +0 -529
  59. package/mcp-server/lib/api-client.cjs +0 -13
  60. package/mcp-server/lib/cache-wrapper.cjs +0 -383
  61. package/mcp-server/lib/error-envelope.js +0 -138
  62. package/mcp-server/lib/executor.ts +0 -499
  63. package/mcp-server/lib/index.ts +0 -29
  64. package/mcp-server/lib/logger.cjs +0 -30
  65. package/mcp-server/lib/rate-limiter.js +0 -166
  66. package/mcp-server/lib/sandbox.test.ts +0 -519
  67. package/mcp-server/lib/sandbox.ts +0 -395
  68. package/mcp-server/lib/types.ts +0 -267
  69. package/mcp-server/logger.js +0 -173
  70. package/mcp-server/manifest.json +0 -473
  71. package/mcp-server/mdc-generator.js +0 -298
  72. package/mcp-server/premium-tools.js +0 -1275
  73. package/mcp-server/proof-tools.js +0 -571
  74. package/mcp-server/registry/tool-registry.js +0 -586
  75. package/mcp-server/registry/tools.json +0 -619
  76. package/mcp-server/registry.test.ts +0 -340
  77. package/mcp-server/test-mcp.js +0 -108
  78. package/mcp-server/test-tools.js +0 -36
  79. package/mcp-server/tests/tier-gating.test.js +0 -297
  80. package/mcp-server/tier-auth.js +0 -767
  81. package/mcp-server/tools/index.js +0 -72
  82. package/mcp-server/tools-reorganized.ts +0 -244
  83. package/mcp-server/tools-v3.js +0 -1004
  84. package/mcp-server/truth-context.js +0 -622
  85. package/mcp-server/truth-firewall-tools.js +0 -2183
  86. package/mcp-server/vibecheck-2.0-tools.js +0 -761
  87. package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
  88. package/mcp-server/vibecheck-tools.js +0 -1075
@@ -1,2940 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * vibecheck MCP Server v2.1.0 - Hardened Production Build
5
- *
6
- * Curated Tools for AI Agents:
7
- * vibecheck.ctx - Build truthpack/context
8
- * vibecheck.scan - Static scan for issues
9
- * vibecheck.ship - Verdict with evidence
10
- * vibecheck.get_truthpack - Ground truth
11
- * vibecheck.validate_claim - Evidence-based claim validation
12
- * vibecheck.compile_context - Task-focused context
13
- * vibecheck.search_evidence - Evidence search
14
- * vibecheck.find_counterexamples - Falsification
15
- * vibecheck.check_invariants - Invariant checks
16
- *
17
- * Everything else is parameters on these tools.
18
- *
19
- * HARDENING FEATURES (v2.1.0):
20
- * - Input validation: URL, path, string, array, number sanitization
21
- * - Output sanitization: Redaction of secrets, truncation of large outputs
22
- * - Rate limiting: 120 calls/minute per server instance
23
- * - Path security: Traversal prevention, project root sandboxing
24
- * - Safe JSON parsing: Size limits, error handling
25
- * - Error handling: Consistent error codes, sanitized messages
26
- * - Resource safety: Bounded timeouts, memory limits
27
- * - Graceful degradation: Partial output on failures
28
- */
29
-
30
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
31
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
32
- import {
33
- CallToolRequestSchema,
34
- ListToolsRequestSchema,
35
- ListResourcesRequestSchema,
36
- ReadResourceRequestSchema,
37
- } from "@modelcontextprotocol/sdk/types.js";
38
-
39
- import fs from "fs/promises";
40
- import fsSync from "fs";
41
- import path from "path";
42
- import { fileURLToPath } from "url";
43
- import { execFile } from "child_process";
44
- import { promisify } from "util";
45
-
46
- // Import API client for dashboard integration (optional - may use CommonJS)
47
- import { createRequire } from "module";
48
- const require = createRequire(import.meta.url);
49
- const {
50
- createScan,
51
- updateScanProgress,
52
- submitScanResults,
53
- reportScanError,
54
- isApiAvailable
55
- } = require("./lib/api-client.cjs");
56
-
57
- const execFileAsync = promisify(execFile);
58
-
59
- const __filename = fileURLToPath(import.meta.url);
60
- const __dirname = path.dirname(__filename);
61
-
62
- // ============================================================================
63
- // CENTRALIZED CONFIGURATION
64
- // ============================================================================
65
- const CONFIG = {
66
- VERSION: "2.1.0",
67
- BIN_PATH: path.join(__dirname, "..", "bin", "vibecheck.js"),
68
- OUTPUT_DIR: ".vibecheck",
69
- ENV_DEFAULTS: { VIBECHECK_SKIP_AUTH: "1" },
70
- TIMEOUTS: {
71
- DEFAULT: 30000, // 30 seconds
72
- SCAN: 120000, // 2 minutes
73
- VERIFY: 180000, // 3 minutes
74
- REALITY: 300000, // 5 minutes
75
- PROVE: 600000, // 10 minutes
76
- AUTOPILOT: 300000, // 5 minutes
77
- },
78
- MAX_BUFFER: 10 * 1024 * 1024, // 10MB
79
- // Hardening limits
80
- LIMITS: {
81
- MAX_OUTPUT_LENGTH: 500000, // 500KB max output text
82
- MAX_PATH_LENGTH: 4096, // Max path string length
83
- MAX_URL_LENGTH: 2048, // Max URL length
84
- MAX_STRING_ARG: 10000, // Max string argument length
85
- MAX_ARRAY_ITEMS: 100, // Max array items in args
86
- RATE_LIMIT_WINDOW_MS: 60000, // 1 minute window
87
- RATE_LIMIT_MAX_CALLS: 120, // Max calls per window
88
- },
89
- // Sensitive patterns to redact from output
90
- SENSITIVE_PATTERNS: [
91
- /(?:sk_live_|sk_test_)[a-zA-Z0-9]{24,}/g, // Stripe keys
92
- /(?:AKIA|ASIA)[A-Z0-9]{16}/g, // AWS keys
93
- /ghp_[a-zA-Z0-9]{36}/g, // GitHub tokens
94
- /xox[baprs]-[0-9A-Za-z\-]{10,}/g, // Slack tokens
95
- /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g, // JWTs
96
- /(?:password|secret|token|apikey|api_key)["']?\s*[:=]\s*["'][^"']{8,}["']/gi, // Generic secrets
97
- ],
98
- };
99
-
100
- // ============================================================================
101
- // HARDENING: Input Validation & Sanitization Utilities
102
- // ============================================================================
103
-
104
- /**
105
- * Validate and sanitize a URL
106
- * @param {string} url - URL to validate
107
- * @returns {{ valid: boolean, url?: string, error?: string }}
108
- */
109
- function validateUrl(url) {
110
- if (!url || typeof url !== 'string') {
111
- return { valid: false, error: 'URL is required and must be a string' };
112
- }
113
-
114
- const trimmed = url.trim();
115
-
116
- if (trimmed.length > CONFIG.LIMITS.MAX_URL_LENGTH) {
117
- return { valid: false, error: `URL exceeds maximum length of ${CONFIG.LIMITS.MAX_URL_LENGTH} characters` };
118
- }
119
-
120
- try {
121
- const parsed = new URL(trimmed);
122
-
123
- // Only allow http/https protocols
124
- if (!['http:', 'https:'].includes(parsed.protocol)) {
125
- return { valid: false, error: 'URL must use http or https protocol' };
126
- }
127
-
128
- // Block localhost/internal IPs in production contexts (can be overridden)
129
- const hostname = parsed.hostname.toLowerCase();
130
- if (process.env.VIBECHECK_BLOCK_INTERNAL !== 'false') {
131
- // Allow localhost for development
132
- if (hostname === 'localhost' || hostname === '127.0.0.1') {
133
- // This is OK for local testing
134
- }
135
- }
136
-
137
- return { valid: true, url: trimmed };
138
- } catch {
139
- return { valid: false, error: 'Invalid URL format' };
140
- }
141
- }
142
-
143
- /**
144
- * Sanitize a file path to prevent path traversal.
145
- *
146
- * SECURITY FIX: Previous implementation used path.sep which differs between
147
- * Windows (\) and Unix (/). Attackers could bypass the check on Windows by
148
- * using forward slashes in paths. Now we normalize all separators before comparing.
149
- *
150
- * @param {string} inputPath - Path to sanitize
151
- * @param {string} projectRoot - Project root directory
152
- * @returns {{ valid: boolean, path?: string, error?: string }}
153
- */
154
- function sanitizePath(inputPath, projectRoot) {
155
- if (!inputPath || typeof inputPath !== 'string') {
156
- return { valid: false, error: 'Path is required and must be a string' };
157
- }
158
-
159
- if (inputPath.length > CONFIG.LIMITS.MAX_PATH_LENGTH) {
160
- return { valid: false, error: `Path exceeds maximum length of ${CONFIG.LIMITS.MAX_PATH_LENGTH} characters` };
161
- }
162
-
163
- // Reject null bytes which can truncate paths in some systems
164
- if (inputPath.includes('\0')) {
165
- return { valid: false, error: 'Path contains invalid characters (null byte)' };
166
- }
167
-
168
- try {
169
- const resolvedRoot = path.resolve(projectRoot);
170
- const resolvedPath = path.resolve(projectRoot, inputPath);
171
-
172
- // SECURITY: Normalize path separators for cross-platform comparison
173
- // On Windows, path.sep is '\' but paths can use '/' which would bypass the check
174
- const normalizedRoot = resolvedRoot.replace(/\\/g, '/').toLowerCase();
175
- const normalizedPath = resolvedPath.replace(/\\/g, '/').toLowerCase();
176
-
177
- // Ensure the resolved path is within or equal to the project root
178
- // Use normalized paths for comparison, add trailing slash to prevent
179
- // prefix attacks (e.g., /project-malicious matching /project)
180
- const rootWithSep = normalizedRoot.endsWith('/') ? normalizedRoot : normalizedRoot + '/';
181
-
182
- if (!normalizedPath.startsWith(rootWithSep) && normalizedPath !== normalizedRoot) {
183
- return { valid: false, error: 'Path traversal detected - path must be within project root' };
184
- }
185
-
186
- return { valid: true, path: resolvedPath };
187
- } catch (err) {
188
- return { valid: false, error: `Invalid path format: ${err.message}` };
189
- }
190
- }
191
-
192
- /**
193
- * Validate and sanitize string arguments
194
- * @param {*} value - Value to validate
195
- * @param {number} maxLength - Maximum allowed length
196
- * @returns {string}
197
- */
198
- function sanitizeString(value, maxLength = CONFIG.LIMITS.MAX_STRING_ARG) {
199
- if (value === null || value === undefined) {
200
- return '';
201
- }
202
-
203
- const str = String(value);
204
- return str.length > maxLength ? str.slice(0, maxLength) + '...[truncated]' : str;
205
- }
206
-
207
- /**
208
- * Validate array arguments
209
- * @param {*} arr - Array to validate
210
- * @param {number} maxItems - Maximum allowed items
211
- * @returns {any[]}
212
- */
213
- function sanitizeArray(arr, maxItems = CONFIG.LIMITS.MAX_ARRAY_ITEMS) {
214
- if (!Array.isArray(arr)) {
215
- return [];
216
- }
217
- return arr.slice(0, maxItems);
218
- }
219
-
220
- /**
221
- * Validate numeric arguments
222
- * @param {*} value - Value to validate
223
- * @param {number} min - Minimum value
224
- * @param {number} max - Maximum value
225
- * @param {number} defaultValue - Default if invalid
226
- * @returns {number}
227
- */
228
- function sanitizeNumber(value, min, max, defaultValue) {
229
- const num = Number(value);
230
- if (isNaN(num) || num < min || num > max) {
231
- return defaultValue;
232
- }
233
- return num;
234
- }
235
-
236
- /**
237
- * Redact sensitive information from output
238
- * @param {string} text - Text to redact
239
- * @returns {string}
240
- */
241
- function redactSensitive(text) {
242
- if (!text || typeof text !== 'string') {
243
- return text;
244
- }
245
-
246
- let result = text;
247
- for (const pattern of CONFIG.SENSITIVE_PATTERNS) {
248
- result = result.replace(pattern, '[REDACTED]');
249
- }
250
- return result;
251
- }
252
-
253
- /**
254
- * Truncate output to prevent memory issues
255
- * @param {string} text - Text to truncate
256
- * @param {number} maxLength - Maximum length
257
- * @returns {string}
258
- */
259
- function truncateOutput(text, maxLength = CONFIG.LIMITS.MAX_OUTPUT_LENGTH) {
260
- if (!text || typeof text !== 'string') {
261
- return '';
262
- }
263
-
264
- if (text.length <= maxLength) {
265
- return text;
266
- }
267
-
268
- const truncated = text.slice(0, maxLength);
269
- return truncated + `\n\n...[Output truncated at ${maxLength} characters]`;
270
- }
271
-
272
- // ============================================================================
273
- // HARDENING: Rate Limiting (Per-API-Key)
274
- //
275
- // SECURITY FIX: Previous implementation used a global rate limit, allowing
276
- // a single attacker to exhaust the rate limit budget for ALL users.
277
- // Now we rate limit per-API-key hash to isolate abuse.
278
- // ============================================================================
279
-
280
- const rateLimitState = {
281
- callsByKey: new Map(), // Map<keyHash, timestamp[]>
282
- globalCalls: [], // Fallback for unauthenticated requests
283
- cleanupInterval: null,
284
- maxKeys: 10000, // Prevent unbounded growth
285
- };
286
-
287
- /**
288
- * Hash API key for rate limit tracking (don't store raw keys)
289
- */
290
- function hashKeyForRateLimit(apiKey) {
291
- if (!apiKey) return '__anonymous__';
292
- const crypto = require('crypto');
293
- return crypto.createHash('sha256').update(apiKey).digest('hex').slice(0, 16);
294
- }
295
-
296
- /**
297
- * Cleanup expired rate limit entries
298
- */
299
- function cleanupRateLimitState() {
300
- const now = Date.now();
301
- const windowStart = now - CONFIG.LIMITS.RATE_LIMIT_WINDOW_MS;
302
-
303
- // Clean per-key entries
304
- for (const [keyHash, calls] of rateLimitState.callsByKey) {
305
- const validCalls = calls.filter(t => t > windowStart);
306
- if (validCalls.length === 0) {
307
- rateLimitState.callsByKey.delete(keyHash);
308
- } else {
309
- rateLimitState.callsByKey.set(keyHash, validCalls);
310
- }
311
- }
312
-
313
- // Clean global calls
314
- rateLimitState.globalCalls = rateLimitState.globalCalls.filter(t => t > windowStart);
315
-
316
- // Enforce max keys to prevent memory exhaustion
317
- if (rateLimitState.callsByKey.size > rateLimitState.maxKeys) {
318
- // Evict oldest keys (those with oldest last call)
319
- const entries = Array.from(rateLimitState.callsByKey.entries());
320
- entries.sort((a, b) => Math.max(...(a[1] || [0])) - Math.max(...(b[1] || [0])));
321
- const toDelete = entries.slice(0, rateLimitState.callsByKey.size - rateLimitState.maxKeys);
322
- for (const [key] of toDelete) {
323
- rateLimitState.callsByKey.delete(key);
324
- }
325
- }
326
- }
327
-
328
- /**
329
- * Check rate limit for a specific API key
330
- *
331
- * @param {string} apiKey - The API key (optional, falls back to global limit)
332
- * @returns {{ allowed: boolean, remaining: number, resetIn: number, keyHash?: string }}
333
- */
334
- function checkRateLimit(apiKey = null) {
335
- const now = Date.now();
336
- const windowStart = now - CONFIG.LIMITS.RATE_LIMIT_WINDOW_MS;
337
- const keyHash = hashKeyForRateLimit(apiKey);
338
-
339
- // Get or create call array for this key
340
- let calls = rateLimitState.callsByKey.get(keyHash);
341
- if (!calls) {
342
- calls = [];
343
- rateLimitState.callsByKey.set(keyHash, calls);
344
- }
345
-
346
- // Clean old entries for this key
347
- const validCalls = calls.filter(t => t > windowStart);
348
- rateLimitState.callsByKey.set(keyHash, validCalls);
349
-
350
- // Determine limit based on authentication
351
- // Anonymous gets stricter limit (10/min), authenticated gets full limit
352
- const maxCalls = apiKey ? CONFIG.LIMITS.RATE_LIMIT_MAX_CALLS : Math.min(10, CONFIG.LIMITS.RATE_LIMIT_MAX_CALLS);
353
- const remaining = maxCalls - validCalls.length;
354
-
355
- if (remaining <= 0) {
356
- const oldestCall = Math.min(...validCalls);
357
- const resetIn = Math.max(0, (oldestCall + CONFIG.LIMITS.RATE_LIMIT_WINDOW_MS) - now);
358
- return { allowed: false, remaining: 0, resetIn, keyHash };
359
- }
360
-
361
- // Record this call
362
- validCalls.push(now);
363
-
364
- // Periodic cleanup (every ~100 calls)
365
- if (Math.random() < 0.01) {
366
- cleanupRateLimitState();
367
- }
368
-
369
- return { allowed: true, remaining: remaining - 1, resetIn: CONFIG.LIMITS.RATE_LIMIT_WINDOW_MS, keyHash };
370
- }
371
-
372
- // ============================================================================
373
- // HARDENING: Circuit Breaker for API Integration
374
- // ============================================================================
375
-
376
- const circuitBreakerState = {
377
- failures: 0,
378
- lastFailureTime: 0,
379
- state: 'CLOSED', // CLOSED = normal, OPEN = failing, HALF_OPEN = testing
380
- failureThreshold: 5,
381
- resetTimeout: 60000, // 1 minute
382
- };
383
-
384
- /**
385
- * Check if API calls should be allowed
386
- * @returns {{ allowed: boolean, reason?: string }}
387
- */
388
- function checkCircuitBreaker() {
389
- const now = Date.now();
390
-
391
- // If circuit is open, check if we should attempt reset
392
- if (circuitBreakerState.state === 'OPEN') {
393
- if (now - circuitBreakerState.lastFailureTime >= circuitBreakerState.resetTimeout) {
394
- circuitBreakerState.state = 'HALF_OPEN';
395
- console.error('[MCP] Circuit breaker entering HALF_OPEN state - testing API');
396
- } else {
397
- return {
398
- allowed: false,
399
- reason: `Circuit breaker OPEN - API calls disabled for ${Math.ceil((circuitBreakerState.resetTimeout - (now - circuitBreakerState.lastFailureTime)) / 1000)}s`
400
- };
401
- }
402
- }
403
-
404
- return { allowed: true };
405
- }
406
-
407
- /**
408
- * Record API call result
409
- * @param {boolean} success - Whether the API call succeeded
410
- */
411
- function recordApiResult(success) {
412
- if (success) {
413
- // Reset on success
414
- if (circuitBreakerState.state === 'HALF_OPEN') {
415
- console.error('[MCP] Circuit breaker CLOSED - API recovered');
416
- }
417
- circuitBreakerState.failures = 0;
418
- circuitBreakerState.state = 'CLOSED';
419
- } else {
420
- circuitBreakerState.failures++;
421
- circuitBreakerState.lastFailureTime = Date.now();
422
-
423
- if (circuitBreakerState.failures >= circuitBreakerState.failureThreshold) {
424
- circuitBreakerState.state = 'OPEN';
425
- console.error(`[MCP] Circuit breaker OPEN after ${circuitBreakerState.failures} failures - disabling API calls`);
426
- }
427
- }
428
- }
429
-
430
- // ============================================================================
431
- // HARDENING: Safe JSON Parsing
432
- // ============================================================================
433
-
434
- /**
435
- * Safely parse JSON with size limits
436
- * @param {string} text - JSON string to parse
437
- * @param {number} maxSize - Maximum allowed size in bytes
438
- * @returns {{ success: boolean, data?: any, error?: string }}
439
- */
440
- function safeJsonParse(text, maxSize = 5 * 1024 * 1024) {
441
- if (!text || typeof text !== 'string') {
442
- return { success: false, error: 'Invalid input' };
443
- }
444
-
445
- if (text.length > maxSize) {
446
- return { success: false, error: `JSON exceeds maximum size of ${maxSize} bytes` };
447
- }
448
-
449
- try {
450
- const data = JSON.parse(text);
451
- return { success: true, data };
452
- } catch (e) {
453
- return { success: false, error: `JSON parse error: ${e.message}` };
454
- }
455
- }
456
-
457
- const VERSION = CONFIG.VERSION;
458
-
459
- // Import intelligence tools
460
- import {
461
- INTELLIGENCE_TOOLS,
462
- handleIntelligenceTool,
463
- } from "./intelligence-tools.js";
464
-
465
- // Import AI vibecheck tools
466
- import {
467
- VIBECHECK_TOOLS,
468
- handleVibecheckTool,
469
- } from "./vibecheck-tools.js";
470
-
471
- // Import agent checkpoint tools
472
- import {
473
- AGENT_CHECKPOINT_TOOLS,
474
- handleCheckpointTool,
475
- } from "./agent-checkpoint.js";
476
-
477
- // Import architect tools
478
- import {
479
- ARCHITECT_TOOLS,
480
- handleArchitectTool,
481
- } from "./architect-tools.js";
482
-
483
- // Import authority system tools
484
- import {
485
- AUTHORITY_TOOLS,
486
- handleAuthorityTool,
487
- } from "./authority-tools.js";
488
-
489
- // Import codebase architect tools
490
- import {
491
- CODEBASE_ARCHITECT_TOOLS,
492
- handleCodebaseArchitectTool,
493
- } from "./codebase-architect-tools.js";
494
-
495
- // Import vibecheck 2.0 tools
496
- import {
497
- VIBECHECK_2_TOOLS,
498
- handleVibecheck2Tool,
499
- } from "./vibecheck-2.0-tools.js";
500
-
501
- // Import intent drift tools
502
- import {
503
- intentDriftTools,
504
- } from "./intent-drift-tools.js";
505
-
506
- // Import audit trail for MCP
507
- import { emitToolInvoke, emitToolComplete } from "./audit-mcp.js";
508
-
509
- // Import MDC generator
510
- import { mdcGeneratorTool, handleMDCGeneration } from "./mdc-generator.js";
511
-
512
- // Import Truth Context tools (Evidence Pack / Truth Pack)
513
- import { TRUTH_CONTEXT_TOOLS, handleTruthContextTool } from "./truth-context.js";
514
-
515
- // Import Truth Firewall tools (Hallucination Stopper)
516
- import {
517
- TRUTH_FIREWALL_TOOLS,
518
- handleTruthFirewallTool,
519
- hasRecentClaimValidation,
520
- getContextAttribution,
521
- } from "./truth-firewall-tools.js";
522
-
523
- // Context attribution message
524
- const CONTEXT_ATTRIBUTION = "🧠 Context enhanced by vibecheck";
525
-
526
- // Import Consolidated Tools (15 focused tools - recommended surface)
527
- import { CONSOLIDATED_TOOLS, handleConsolidatedTool } from "./consolidated-tools.js";
528
-
529
- // Import v3 Tools (2-tier model: FREE tools for inspect & observe, PRO for fix/prove/enforce)
530
- import { MCP_TOOLS_V3, handleToolV3, TOOL_TIERS as V3_TOOL_TIERS } from "./tools-v3.js";
531
-
532
- // Import tier auth for entitlement checking
533
- import { getFeatureAccessStatus, getTierFromApiKey } from "./tier-auth.js";
534
-
535
- // Import new production modules
536
- const {
537
- validateToolInput,
538
- validateToolOutput,
539
- getToolTimeout,
540
- getToolTier,
541
- } = require('./registry/tool-registry.js');
542
- const {
543
- executeWithEnvelope,
544
- createErrorEnvelope,
545
- createSuccessEnvelope,
546
- } = require('./lib/error-envelope.js');
547
- const { rateLimiter } = require('./lib/rate-limiter.js');
548
-
549
- // Import Agent Firewall Interceptor - ENABLED BY DEFAULT
550
- // The Agent Firewall is the core gatekeeper that validates AI changes against reality
551
- import {
552
- AGENT_FIREWALL_TOOL,
553
- handleAgentFirewallIntercept,
554
- } from "./agent-firewall-interceptor.js";
555
-
556
- // Import Intent-Aware Firewall v2 - BLOCKING enforcement with intent alignment
557
- import {
558
- INTENT_FIREWALL_TOOL,
559
- handleIntentFirewallIntercept,
560
- INTENT_STATUS_TOOL,
561
- handleIntentStatus,
562
- } from "./intent-firewall-interceptor.js";
563
-
564
- // Import Conductor tools - Multi-Agent Coordination (Phase 2)
565
- import {
566
- CONDUCTOR_TOOLS,
567
- handleConductorToolCall,
568
- getConductorTools,
569
- } from "./conductor/tools.js";
570
-
571
- /**
572
- * TRUTH FIREWALL CONFIGURATION
573
- *
574
- * Tools that make assertions or change code MUST have recent claim validation.
575
- * Policy modes: strict (default for agents), balanced, permissive
576
- */
577
- const STRICT_GUARDRAIL_TOOLS = new Set([
578
- "vibecheck.scan",
579
- "vibecheck.ship",
580
- "vibecheck.ctx",
581
- "vibecheck.fix",
582
- "vibecheck.prove",
583
- "vibecheck.autopilot_apply",
584
- ]);
585
-
586
- // Tools that modify code or make assertions - require truth firewall
587
- const CODE_CHANGING_TOOLS = new Set([
588
- "vibecheck.fix",
589
- "vibecheck.autopilot_apply",
590
- "vibecheck.propose_patch",
591
- ]);
592
-
593
- // Policy thresholds (aligned with proof-context.js EVIDENCE_SCHEMA)
594
- const POLICY_THRESHOLDS = {
595
- strict: { minConfidence: 0.8, allowUnknown: false, requireValidation: true },
596
- balanced: { minConfidence: 0.6, allowUnknown: false, requireValidation: true },
597
- permissive: { minConfidence: 0.4, allowUnknown: true, requireValidation: false },
598
- };
599
-
600
- function getTruthPolicy(args) {
601
- const policy = args?.policy || "strict";
602
- return POLICY_THRESHOLDS[policy] ? policy : "strict";
603
- }
604
-
605
- function getPolicyConfig(policy) {
606
- return POLICY_THRESHOLDS[policy] || POLICY_THRESHOLDS.strict;
607
- }
608
-
609
- /**
610
- * Emit guardrail metric to audit log.
611
- *
612
- * SECURITY FIX: Previous implementation silently ignored all failures.
613
- * Now we log failures to stderr for security monitoring - an attacker
614
- * filling disk or manipulating permissions would have gone undetected.
615
- */
616
- async function emitGuardrailMetric(projectPath, metric) {
617
- try {
618
- const auditDir = path.join(projectPath, ".vibecheck", "audit");
619
- await fs.mkdir(auditDir, { recursive: true });
620
- const record = JSON.stringify({ ...metric, timestamp: new Date().toISOString() });
621
- await fs.appendFile(path.join(auditDir, "guardrail-metrics.jsonl"), `${record}\n`);
622
- } catch (err) {
623
- // SECURITY: Log failures - silent failure could hide attacks
624
- // (e.g., attacker fills disk to prevent audit logging)
625
- console.error(`[SECURITY] Guardrail metric write failed: ${err.message}`);
626
- console.error(`[SECURITY] Failed metric: ${JSON.stringify(metric)}`);
627
-
628
- // Attempt fallback to stderr-only logging for critical metrics
629
- if (metric.event === 'truth_firewall_block' || metric.event === 'security_violation') {
630
- console.error(`[SECURITY-CRITICAL] ${metric.event}: ${JSON.stringify(metric)}`);
631
- }
632
- }
633
- }
634
-
635
- /**
636
- * Check if a code-changing tool should be blocked due to missing validation.
637
- * Returns { blocked: boolean, reason?: string, suggestion?: string }
638
- */
639
- function checkTruthFirewallBlock(toolName, args, projectPath) {
640
- const policy = getTruthPolicy(args);
641
- const policyConfig = getPolicyConfig(policy);
642
-
643
- // Skip validation check if permissive mode and validation not required
644
- if (!policyConfig.requireValidation) {
645
- return { blocked: false };
646
- }
647
-
648
- // Check if this is a code-changing tool that requires validation
649
- if (!CODE_CHANGING_TOOLS.has(toolName) && !STRICT_GUARDRAIL_TOOLS.has(toolName)) {
650
- return { blocked: false };
651
- }
652
-
653
- // Check for recent claim validation
654
- if (!hasRecentClaimValidation(projectPath)) {
655
- return {
656
- blocked: true,
657
- reason: `Truth firewall requires claim validation before ${toolName}`,
658
- code: "TRUTH_FIREWALL_REQUIRED",
659
- suggestion: "Call vibecheck.validate_claim or vibecheck.get_truthpack before proceeding",
660
- nextSteps: [
661
- "Call vibecheck.get_truthpack with refresh=true for current evidence",
662
- "Call vibecheck.validate_claim for critical assumptions",
663
- `Re-run ${toolName} after validation`,
664
- ],
665
- };
666
- }
667
-
668
- return { blocked: false };
669
- }
670
-
671
- // ============================================================================
672
- // TOOL DEFINITIONS - Public Tools (Clean Product Surface)
673
- // ============================================================================
674
-
675
- // ============================================================================
676
- // TOOL REGISTRATION - V3 Tools Only (2-tier: FREE / PRO)
677
- // ============================================================================
678
- // V3 tools are the canonical tool surface with 2-tier model:
679
- // - FREE (10 tools): Inspect & Observe
680
- // - PRO (15 tools): Fix, Prove & Enforce (includes Authority, Conductor, Firewall)
681
-
682
- const TOOLS = [
683
- // V3 tools include all FREE and PRO tools
684
- // Authority, Conductor, and Agent Firewall are included in MCP_TOOLS_V3
685
- ...MCP_TOOLS_V3,
686
- ].filter(t => t !== null);
687
-
688
- // Legacy tool definitions removed - V3 is the only supported mode
689
- // All tools are now defined in tools-v3.js
690
-
691
-
692
-
693
- // ============================================================================
694
- // SERVER IMPLEMENTATION
695
- // ============================================================================
696
-
697
- class VibecheckMCP {
698
- constructor() {
699
- this.server = new Server(
700
- { name: "vibecheck", version: VERSION },
701
- { capabilities: { tools: {}, resources: {} } },
702
- );
703
- this.toolRegistry = this.buildToolRegistry();
704
- this.setupHandlers();
705
- }
706
-
707
- // ============================================================================
708
- // TOOL REGISTRY - Maps tool names to handlers for cleaner dispatch
709
- // HARDENING: Validates all handlers are functions
710
- // ============================================================================
711
- buildToolRegistry() {
712
- const registry = {};
713
-
714
- // Helper to safely add handler with validation
715
- const addHandler = (name, handler) => {
716
- if (typeof handler !== 'function') {
717
- console.error(`[MCP] Warning: Tool ${name} handler is not a function`);
718
- return;
719
- }
720
- registry[name] = handler;
721
- };
722
-
723
- // Agent Firewall - intercepts file writes (if available)
724
- if (handleAgentFirewallIntercept && typeof handleAgentFirewallIntercept === 'function') {
725
- addHandler("vibecheck_agent_firewall_intercept", handleAgentFirewallIntercept);
726
- }
727
-
728
- // Intent-Aware Firewall v2 - blocking enforcement with intent alignment
729
- if (handleIntentFirewallIntercept && typeof handleIntentFirewallIntercept === 'function') {
730
- addHandler("vibecheck_intent_firewall_intercept", handleIntentFirewallIntercept);
731
- }
732
- if (handleIntentStatus && typeof handleIntentStatus === 'function') {
733
- addHandler("vibecheck_intent_status", handleIntentStatus);
734
- }
735
-
736
- // Conductor - multi-agent coordination tools
737
- addHandler("vibecheck_conductor_register", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_register", args, projectPath));
738
- addHandler("vibecheck_conductor_acquire_lock", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_acquire_lock", args, projectPath));
739
- addHandler("vibecheck_conductor_release_lock", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_release_lock", args, projectPath));
740
- addHandler("vibecheck_conductor_propose", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_propose", args, projectPath));
741
- addHandler("vibecheck_conductor_status", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_status", args, projectPath));
742
- addHandler("vibecheck_conductor_terminate", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_terminate", args, projectPath));
743
-
744
- // Core CLI tools
745
- addHandler("vibecheck.ship", this.handleShip.bind(this));
746
- addHandler("vibecheck.scan", this.handleScan.bind(this));
747
- addHandler("vibecheck.verify", this.handleVerify.bind(this));
748
- addHandler("vibecheck.reality", this.handleReality.bind(this));
749
- addHandler("vibecheckai.dev-test", this.handleAITest.bind(this));
750
- addHandler("vibecheck.gate", this.handleGate.bind(this));
751
- addHandler("vibecheck.fix", this.handleFix.bind(this));
752
- addHandler("vibecheck.share", this.handleShare.bind(this));
753
- addHandler("vibecheck.ctx", this.handleCtx.bind(this));
754
- addHandler("vibecheck.prove", this.handleProve.bind(this));
755
- addHandler("vibecheck.proof", this.handleProof.bind(this));
756
- addHandler("vibecheck.validate", this.handleValidate.bind(this));
757
- addHandler("vibecheck.report", this.handleReport.bind(this));
758
- addHandler("vibecheck.status", this.handleStatus.bind(this));
759
- addHandler("vibecheck.autopilot", this.handleAutopilot.bind(this));
760
- addHandler("vibecheck.autopilot_plan", this.handleAutopilotPlan.bind(this));
761
- addHandler("vibecheck.autopilot_apply", this.handleAutopilotApply.bind(this));
762
- addHandler("vibecheck.badge", this.handleBadge.bind(this));
763
- addHandler("vibecheck.context", this.handleContext.bind(this));
764
-
765
- console.error(`[MCP] Tool registry built with ${Object.keys(registry).length} handlers`);
766
- return registry;
767
- }
768
-
769
- // ============================================================================
770
- // CLI RUNNER - Secure, async execution with array args (prevents injection)
771
- // ============================================================================
772
- async runCLI(command, args = [], cwd, options = {}) {
773
- const {
774
- env = {},
775
- timeout = CONFIG.TIMEOUTS.DEFAULT,
776
- skipAuth = true,
777
- } = options;
778
-
779
- // ========================================================================
780
- // HARDENING: Validate command
781
- // ========================================================================
782
- const sanitizedCommand = sanitizeString(command, 50);
783
- if (!sanitizedCommand || !/^[a-z0-9_-]+$/i.test(sanitizedCommand)) {
784
- throw new Error(`Invalid CLI command: ${sanitizedCommand}`);
785
- }
786
-
787
- // ========================================================================
788
- // HARDENING: Validate and sanitize arguments
789
- // ========================================================================
790
- const sanitizedArgs = sanitizeArray(args, 50).map(arg => {
791
- const str = String(arg);
792
- // Validate argument format (must be simple flags or values)
793
- if (str.length > 1000) {
794
- return str.slice(0, 1000);
795
- }
796
- return str;
797
- });
798
-
799
- // ========================================================================
800
- // HARDENING: Validate working directory
801
- // ========================================================================
802
- const resolvedCwd = path.resolve(cwd || process.cwd());
803
- if (!fsSync.existsSync(resolvedCwd)) {
804
- throw new Error(`Working directory does not exist: ${resolvedCwd}`);
805
- }
806
-
807
- // Build argument array - this prevents command injection
808
- const finalArgs = [CONFIG.BIN_PATH, sanitizedCommand, ...sanitizedArgs];
809
-
810
- // ========================================================================
811
- // HARDENING: Clean environment - don't leak sensitive vars
812
- // ========================================================================
813
- const safeEnv = { ...process.env };
814
- // Remove potentially sensitive env vars from being passed through
815
- const sensitiveEnvKeys = ['AWS_SECRET_ACCESS_KEY', 'STRIPE_SECRET_KEY', 'DATABASE_URL'];
816
- for (const key of sensitiveEnvKeys) {
817
- if (safeEnv[key] && !env[key]) {
818
- // Only remove if not explicitly set in options
819
- delete safeEnv[key];
820
- }
821
- }
822
-
823
- const execEnv = {
824
- ...safeEnv,
825
- ...CONFIG.ENV_DEFAULTS,
826
- ...(skipAuth ? { VIBECHECK_SKIP_AUTH: "1" } : {}),
827
- ...env,
828
- // Ensure Node.js doesn't prompt for anything
829
- NODE_NO_READLINE: "1",
830
- FORCE_COLOR: "0",
831
- };
832
-
833
- // ========================================================================
834
- // HARDENING: Bounded timeout
835
- // ========================================================================
836
- const boundedTimeout = sanitizeNumber(timeout, 1000, 900000, CONFIG.TIMEOUTS.DEFAULT);
837
-
838
- try {
839
- const { stdout, stderr } = await execFileAsync(process.execPath, finalArgs, {
840
- cwd: resolvedCwd,
841
- encoding: "utf8",
842
- maxBuffer: CONFIG.MAX_BUFFER,
843
- timeout: boundedTimeout,
844
- env: execEnv,
845
- // Don't inherit stdin - prevents hanging
846
- stdio: ['ignore', 'pipe', 'pipe'],
847
- });
848
-
849
- // Sanitize output before returning
850
- return {
851
- stdout: redactSensitive(truncateOutput(stdout || '')),
852
- stderr: redactSensitive(truncateOutput(stderr || '')),
853
- success: true
854
- };
855
- } catch (error) {
856
- // Attach partial output for graceful degradation (sanitized)
857
- error.partialOutput = redactSensitive(truncateOutput(error.stdout || ''));
858
- error.partialStderr = redactSensitive(truncateOutput(error.stderr || ''));
859
-
860
- // Add helpful error code for timeout
861
- if (error.killed && error.signal === 'SIGTERM') {
862
- error.code = 'TIMEOUT';
863
- error.message = `Command timed out after ${boundedTimeout}ms`;
864
- }
865
-
866
- throw error;
867
- }
868
- }
869
-
870
- // ============================================================================
871
- // HARDENED UTILITY HELPERS
872
- // ============================================================================
873
-
874
- /**
875
- * Strip ANSI escape codes from output with length validation
876
- * @param {string} str - String to strip
877
- * @returns {string}
878
- */
879
- stripAnsi(str) {
880
- if (!str || typeof str !== 'string') {
881
- return '';
882
- }
883
-
884
- // Truncate first to prevent DoS on very long strings
885
- const truncated = str.length > CONFIG.LIMITS.MAX_OUTPUT_LENGTH
886
- ? str.slice(0, CONFIG.LIMITS.MAX_OUTPUT_LENGTH)
887
- : str;
888
-
889
- return truncated.replace(/\x1b\[[0-9;]*m/g, "");
890
- }
891
-
892
- /**
893
- * Parse summary from disk with size limits and validation
894
- * @param {string} projectPath - Project root path
895
- * @returns {Promise<object|null>} Parsed summary or null
896
- */
897
- async parseSummaryFromDisk(projectPath) {
898
- const summaryPath = path.join(projectPath, CONFIG.OUTPUT_DIR, "summary.json");
899
-
900
- try {
901
- // Check file size first
902
- const stats = await fs.stat(summaryPath);
903
- if (stats.size > 5 * 1024 * 1024) { // 5MB limit
904
- console.error(`[MCP] Summary file too large: ${stats.size} bytes`);
905
- return null;
906
- }
907
-
908
- const content = await fs.readFile(summaryPath, "utf-8");
909
- const parsed = safeJsonParse(content);
910
-
911
- if (!parsed.success) {
912
- console.error(`[MCP] Invalid summary JSON: ${parsed.error}`);
913
- return null;
914
- }
915
-
916
- return parsed.data;
917
- } catch (err) {
918
- // Silent fail - this is for graceful degradation
919
- return null;
920
- }
921
- }
922
-
923
- /**
924
- * Format scan output from summary object with validation
925
- * @param {object} summary - Summary object
926
- * @param {string} projectPath - Project root path
927
- * @returns {string}
928
- */
929
- formatScanOutput(summary, projectPath) {
930
- if (!summary || typeof summary !== 'object') {
931
- return '## Error: Invalid summary data\n';
932
- }
933
-
934
- // Safely extract values with defaults
935
- const score = sanitizeNumber(summary.score, 0, 100, 0);
936
- const grade = sanitizeString(summary.grade, 10) || 'N/A';
937
- const canShip = Boolean(summary.canShip);
938
-
939
- let output = `## Score: ${score}/100 (${grade})\n\n`;
940
- output += `**Verdict:** ${canShip ? "✅ SHIP" : "🚫 NO-SHIP"}\n\n`;
941
-
942
- if (summary.counts && typeof summary.counts === 'object') {
943
- output += "### Checks\n\n";
944
- output += "| Category | Issues |\n|----------|--------|\n";
945
-
946
- // Limit to 50 categories to prevent output bloat
947
- const entries = Object.entries(summary.counts).slice(0, 50);
948
- for (const [key, count] of entries) {
949
- const safeKey = sanitizeString(key, 50);
950
- const safeCount = sanitizeNumber(count, 0, 999999, 0);
951
- const icon = safeCount === 0 ? "✅" : "⚠️";
952
- output += `| ${icon} ${safeKey} | ${safeCount} |\n`;
953
- }
954
- }
955
-
956
- output += `\n📄 **Report:** ${CONFIG.OUTPUT_DIR}/report.html\n`;
957
- return output;
958
- }
959
-
960
- /**
961
- * Safely read a file with size limits
962
- * @param {string} filePath - Path to file
963
- * @param {number} maxSize - Maximum file size in bytes
964
- * @returns {Promise<string|null>} File contents or null
965
- */
966
- async safeReadFile(filePath, maxSize = 10 * 1024 * 1024) {
967
- try {
968
- const stats = await fs.stat(filePath);
969
-
970
- if (stats.size > maxSize) {
971
- console.error(`[MCP] File too large: ${filePath} (${stats.size} bytes)`);
972
- return null;
973
- }
974
-
975
- const content = await fs.readFile(filePath, "utf-8");
976
- return content;
977
- } catch (err) {
978
- return null;
979
- }
980
- }
981
-
982
- setupHandlers() {
983
- // List tools
984
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
985
- tools: TOOLS,
986
- }));
987
-
988
- // Call tool - main dispatch handler
989
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
990
- const startTime = Date.now();
991
- let toolName = 'unknown';
992
- let projectPath = '.';
993
-
994
- try {
995
- // ====================================================================
996
- // HARDENING: Input extraction with validation
997
- // ====================================================================
998
- const params = request?.params;
999
- if (!params || typeof params !== 'object') {
1000
- return this.error('Invalid request: missing params', { code: 'INVALID_REQUEST' });
1001
- }
1002
-
1003
- toolName = sanitizeString(params.name, 100);
1004
- const args = params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
1005
-
1006
- // Validate tool name
1007
- if (!toolName || toolName.length < 2) {
1008
- return this.error('Invalid tool name', { code: 'INVALID_TOOL_NAME' });
1009
- }
1010
-
1011
- // ====================================================================
1012
- // HARDENING: Rate limiting (per-API-key)
1013
- // ====================================================================
1014
- const apiKey = args?.apiKey || null;
1015
- const rateCheck = checkRateLimit(apiKey);
1016
- if (!rateCheck.allowed) {
1017
- return this.error(`Rate limit exceeded. Try again in ${Math.ceil(rateCheck.resetIn / 1000)} seconds`, {
1018
- code: 'RATE_LIMIT_EXCEEDED',
1019
- suggestion: 'Reduce the frequency of tool calls',
1020
- nextSteps: [`Wait ${Math.ceil(rateCheck.resetIn / 1000)} seconds before retrying`],
1021
- });
1022
- }
1023
-
1024
- // ====================================================================
1025
- // HARDENING: Project path validation
1026
- // ====================================================================
1027
- const rawProjectPath = args?.projectPath || '.';
1028
- const pathValidation = sanitizePath(rawProjectPath, process.cwd());
1029
-
1030
- if (!pathValidation.valid) {
1031
- return this.error(pathValidation.error, {
1032
- code: 'INVALID_PATH',
1033
- suggestion: 'Provide a valid path within the current working directory',
1034
- });
1035
- }
1036
- projectPath = pathValidation.path;
1037
-
1038
- // Emit audit event for tool invocation start
1039
- // SECURITY: Include apiKey hash for audit trail (never log raw key)
1040
- try {
1041
- const crypto = require('crypto');
1042
- const apiKeyHash = apiKey
1043
- ? crypto.createHash('sha256').update(apiKey).digest('hex').slice(0, 16)
1044
- : 'anonymous';
1045
-
1046
- emitToolInvoke(toolName, args, "success", {
1047
- projectPath,
1048
- apiKeyHash,
1049
- rateLimit: rateCheck,
1050
- timestamp: new Date().toISOString(),
1051
- });
1052
- } catch {
1053
- // Audit logging should never break the tool
1054
- }
1055
-
1056
- // ====================================================================
1057
- // HARDENING: Truth firewall check with error handling
1058
- // ====================================================================
1059
- let firewallCheck = { blocked: false };
1060
- try {
1061
- firewallCheck = checkTruthFirewallBlock(toolName, args, projectPath);
1062
- } catch (firewallError) {
1063
- console.error(`[MCP] Firewall check error: ${firewallError.message}`);
1064
- // Continue - don't block on firewall errors
1065
- }
1066
-
1067
- if (firewallCheck.blocked) {
1068
- const policy = getTruthPolicy(args);
1069
- try {
1070
- await emitGuardrailMetric(projectPath, {
1071
- event: "truth_firewall_block",
1072
- tool: toolName,
1073
- policy,
1074
- reason: firewallCheck.code || "no_recent_claim_validation",
1075
- });
1076
- } catch {
1077
- // Metrics should never break the tool
1078
- }
1079
- return this.error(firewallCheck.reason, {
1080
- code: firewallCheck.code,
1081
- suggestion: firewallCheck.suggestion,
1082
- nextSteps: firewallCheck.nextSteps || [],
1083
- });
1084
- }
1085
-
1086
- // Handle v3 tools (2-tier: FREE/PRO aligned with CLI entitlements)
1087
- if (USE_V3_TOOLS && V3_TOOL_TIERS[toolName]) {
1088
- // ================================================================
1089
- // PRODUCTION HARDENING: Integrated validation, rate limiting, timeout
1090
- // ================================================================
1091
-
1092
- // 1. Get user tier from API key (never trust client-provided tier)
1093
- const { getMcpToolAccess } = await import('./tier-auth.js');
1094
- const access = await getMcpToolAccess(toolName, apiKey, args);
1095
- const userTier = access.tier || 'free';
1096
-
1097
- // 2. Check per-user rate limit
1098
- const userId = apiKey ? hashKeyForRateLimit(apiKey) : '__anonymous__';
1099
- const rateCheck = rateLimiter.check(userId, userTier);
1100
- if (!rateCheck.allowed) {
1101
- const errorEnvelope = createErrorEnvelope(
1102
- 'RATE_LIMITED',
1103
- `Rate limit exceeded. Try again in ${rateCheck.retryAfter} seconds`,
1104
- { retryAfter: rateCheck.retryAfter, limit: rateCheck.limit }
1105
- );
1106
- return this.error(errorEnvelope.error.message, {
1107
- code: errorEnvelope.error.code,
1108
- retryAfter: rateCheck.retryAfter,
1109
- });
1110
- }
1111
-
1112
- // 3. Validate input schema
1113
- const validation = validateToolInput(toolName, args);
1114
- if (!validation.valid) {
1115
- const errorEnvelope = createErrorEnvelope(
1116
- 'VALIDATION_ERROR',
1117
- 'Invalid tool input',
1118
- { errors: validation.errors }
1119
- );
1120
- return this.error(errorEnvelope.error.message, {
1121
- code: errorEnvelope.error.code,
1122
- errors: validation.errors,
1123
- });
1124
- }
1125
-
1126
- // 4. Check tier access (already done above, but ensure it's checked)
1127
- if (!access.hasAccess) {
1128
- return this.error(access.error?.message || 'Access denied', {
1129
- code: access.error?.code || 'TIER_REQUIRED',
1130
- tier: access.tier,
1131
- required: access.error?.required,
1132
- });
1133
- }
1134
-
1135
- // 5. Execute with timeout and cancellation support
1136
- const timeout = getToolTimeout(toolName);
1137
- const controller = new AbortController();
1138
-
1139
- // Register job for cancellation (if needed)
1140
- const jobId = `${toolName}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1141
- if (typeof global !== 'undefined') {
1142
- if (!global.activeJobs) global.activeJobs = new Map();
1143
- global.activeJobs.set(jobId, controller);
1144
- }
1145
-
1146
- try {
1147
- const result = await executeWithEnvelope(
1148
- toolName,
1149
- async (signal) => {
1150
- return await handleToolV3(toolName, args, {
1151
- tier: userTier,
1152
- signal, // Pass abort signal to tool handler
1153
- });
1154
- },
1155
- { timeout }
1156
- );
1157
-
1158
- // Cleanup job registration
1159
- if (typeof global !== 'undefined' && global.activeJobs) {
1160
- global.activeJobs.delete(jobId);
1161
- }
1162
-
1163
- // 6. Validate output schema (warn on mismatch, don't fail)
1164
- const outputValidation = validateToolOutput(toolName, result.data || result);
1165
- if (!outputValidation.valid) {
1166
- console.warn(`[MCP] Tool ${toolName} output validation warnings:`, outputValidation.errors);
1167
- }
1168
-
1169
- // 7. Return sanitized output
1170
- const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
1171
- return {
1172
- content: [{ type: "text", text: outputText }],
1173
- };
1174
- } catch (error) {
1175
- // Cleanup on error
1176
- if (typeof global !== 'undefined' && global.activeJobs) {
1177
- global.activeJobs.delete(jobId);
1178
- }
1179
- throw error;
1180
- }
1181
- }
1182
-
1183
- // 1. Check tool registry first (local CLI handlers)
1184
- if (this.toolRegistry[toolName]) {
1185
- return await this.toolRegistry[toolName](projectPath, args);
1186
- }
1187
-
1188
- // 2. Handle external module tools by prefix/pattern
1189
- if (toolName.startsWith("vibecheck.intelligence.")) {
1190
- return await handleIntelligenceTool(toolName, args, __dirname);
1191
- }
1192
-
1193
- // Handle AI vibecheck tools
1194
- if (["vibecheck.verify", "vibecheck.quality", "vibecheck.smells",
1195
- "vibecheck.hallucination", "vibecheck.breaking", "vibecheck.mdc",
1196
- "vibecheck.coverage"].includes(toolName)) {
1197
- const result = await handleVibecheckTool(toolName, args);
1198
- const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
1199
- return {
1200
- content: [{ type: "text", text: outputText }],
1201
- };
1202
- }
1203
-
1204
- // Handle agent checkpoint tools
1205
- if (["vibecheck_checkpoint", "vibecheck_set_strictness", "vibecheck_checkpoint_status"].includes(toolName)) {
1206
- return await handleCheckpointTool(toolName, args);
1207
- }
1208
-
1209
- // Handle architect tools
1210
- if (["vibecheck_architect_review", "vibecheck_architect_suggest",
1211
- "vibecheck_architect_patterns", "vibecheck_architect_set_strictness"].includes(toolName)) {
1212
- return await handleArchitectTool(toolName, args);
1213
- }
1214
-
1215
- // Handle authority system tools
1216
- if (["authority.classify", "authority.approve", "authority.list"].includes(toolName)) {
1217
- const result = await handleAuthorityTool(toolName, args, userTier || "free");
1218
- const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
1219
- return {
1220
- content: [{ type: "text", text: outputText }],
1221
- };
1222
- }
1223
-
1224
- // Handle codebase architect tools
1225
- if (["vibecheck_architect_context", "vibecheck_architect_guide",
1226
- "vibecheck_architect_validate", "vibecheck_architect_patterns",
1227
- "vibecheck_architect_dependencies"].includes(toolName)) {
1228
- return await handleCodebaseArchitectTool(toolName, args);
1229
- }
1230
-
1231
- // Handle vibecheck 2.0 tools
1232
- if (["checkpoint", "check", "ship", "fix", "status", "set_strictness"].includes(toolName)) {
1233
- return await handleVibecheck2Tool(toolName, args, __dirname);
1234
- }
1235
-
1236
- // Handle intent drift tools
1237
- if (toolName.startsWith("vibecheck_intent_")) {
1238
- const tool = intentDriftTools.find(t => t.name === toolName);
1239
- if (tool && tool.handler) {
1240
- const result = await tool.handler(args);
1241
- const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
1242
- return {
1243
- content: [{ type: "text", text: outputText }],
1244
- };
1245
- }
1246
- }
1247
-
1248
- // Handle MDC generator
1249
- if (toolName === "generate_mdc") {
1250
- return await handleMDCGeneration(args);
1251
- }
1252
-
1253
- // Handle Truth Context tools (Evidence Pack / Truth Pack)
1254
- if (["vibecheck.verify_claim", "vibecheck.evidence"].includes(toolName)) {
1255
- return await handleTruthContextTool(toolName, args);
1256
- }
1257
-
1258
- // Handle Truth Firewall tools (Hallucination Stopper)
1259
- if (["vibecheck.get_truthpack", "vibecheck.validate_claim", "vibecheck.compile_context",
1260
- "vibecheck.search_evidence", "vibecheck.find_counterexamples", "vibecheck.propose_patch",
1261
- "vibecheck.check_invariants", "vibecheck.add_assumption"].includes(toolName)) {
1262
- return await handleTruthFirewallTool(toolName, args, projectPath);
1263
- }
1264
-
1265
- return this.error(`Unknown tool: ${toolName}`, {
1266
- code: 'UNKNOWN_TOOL',
1267
- suggestion: 'Check the tool name and try again',
1268
- nextSteps: ['Use vibecheck.status to see available tools'],
1269
- });
1270
- } catch (err) {
1271
- // ====================================================================
1272
- // HARDENING: Enhanced error handling with sanitization
1273
- // ====================================================================
1274
- const durationMs = Date.now() - startTime;
1275
- const errorMessage = sanitizeString(err?.message || 'Unknown error', 500);
1276
-
1277
- // Emit audit event for tool error (safely)
1278
- try {
1279
- emitToolComplete(toolName, "error", {
1280
- errorMessage: redactSensitive(errorMessage),
1281
- durationMs,
1282
- });
1283
- } catch {
1284
- // Audit logging should never break the response
1285
- }
1286
-
1287
- // Log error details to stderr (not stdout - preserves MCP protocol)
1288
- console.error(`[MCP] Tool ${toolName} failed after ${durationMs}ms: ${errorMessage}`);
1289
-
1290
- return this.error(`${toolName} failed: ${redactSensitive(errorMessage)}`, {
1291
- code: err?.code || 'TOOL_ERROR',
1292
- suggestion: 'Check the error message and try again',
1293
- nextSteps: [
1294
- 'Verify the tool arguments are correct',
1295
- 'Check that the project path is valid',
1296
- 'Try running with simpler arguments first',
1297
- ],
1298
- });
1299
- }
1300
- });
1301
-
1302
- // Resources
1303
- this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
1304
- resources: [
1305
- {
1306
- uri: "vibecheck://config",
1307
- name: "vibecheck Config",
1308
- description: "Project vibecheck configuration",
1309
- mimeType: "application/json",
1310
- },
1311
- {
1312
- uri: "vibecheck://summary",
1313
- name: "Last Scan Summary",
1314
- description: "Most recent scan results and verdict",
1315
- mimeType: "application/json",
1316
- },
1317
- {
1318
- uri: "vibecheck://truthpack",
1319
- name: "Truth Pack",
1320
- description: "Ground truth: routes, env, auth, billing",
1321
- mimeType: "application/json",
1322
- },
1323
- {
1324
- uri: "vibecheck://missions",
1325
- name: "Fix Missions",
1326
- description: "Latest mission pack for AI-powered fixes",
1327
- mimeType: "application/json",
1328
- },
1329
- {
1330
- uri: "vibecheck://reality",
1331
- name: "Reality Results",
1332
- description: "Last runtime verification results",
1333
- mimeType: "application/json",
1334
- },
1335
- {
1336
- uri: "vibecheck://findings",
1337
- name: "All Findings",
1338
- description: "Complete list of detected issues",
1339
- mimeType: "application/json",
1340
- },
1341
- {
1342
- uri: "vibecheck://share",
1343
- name: "Share Pack",
1344
- description: "Latest fix missions share bundle for PR/review",
1345
- mimeType: "application/json",
1346
- },
1347
- {
1348
- uri: "vibecheck://prove",
1349
- name: "Prove Report",
1350
- description: "Last prove loop results (ctx → reality → ship → fix)",
1351
- mimeType: "application/json",
1352
- },
1353
- ],
1354
- }));
1355
-
1356
- this.server.setRequestHandler(
1357
- ReadResourceRequestSchema,
1358
- async (request) => {
1359
- // ====================================================================
1360
- // HARDENING: Resource request validation
1361
- // ====================================================================
1362
- const uri = sanitizeString(request?.params?.uri, 200);
1363
- if (!uri || !uri.startsWith('vibecheck://')) {
1364
- return { contents: [{ uri: uri || '', mimeType: "application/json", text: '{"error": "Invalid resource URI"}' }] };
1365
- }
1366
-
1367
- const projectPath = process.cwd();
1368
-
1369
- // Helper to safely read and return JSON resource
1370
- const safeReadResource = async (filePath, defaultMessage) => {
1371
- try {
1372
- const content = await fs.readFile(filePath, "utf-8");
1373
- // Validate JSON and sanitize
1374
- const parsed = safeJsonParse(content);
1375
- if (!parsed.success) {
1376
- return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ error: "Invalid JSON in resource file" }) }] };
1377
- }
1378
- // Redact any sensitive data and truncate
1379
- const sanitized = redactSensitive(truncateOutput(content, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
1380
- return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
1381
- } catch {
1382
- return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ message: defaultMessage }) }] };
1383
- }
1384
- };
1385
-
1386
- if (uri === "vibecheck://config") {
1387
- const configPath = path.join(projectPath, "vibecheck.config.json");
1388
- try {
1389
- const content = await fs.readFile(configPath, "utf-8");
1390
- // Redact sensitive config values
1391
- const sanitized = redactSensitive(truncateOutput(content, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
1392
- return {
1393
- contents: [{ uri, mimeType: "application/json", text: sanitized }],
1394
- };
1395
- } catch {
1396
- return {
1397
- contents: [{ uri, mimeType: "application/json", text: "{}" }],
1398
- };
1399
- }
1400
- }
1401
-
1402
- if (uri === "vibecheck://summary") {
1403
- const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
1404
- return await safeReadResource(summaryPath, "No scan found. Run vibecheck.scan first.");
1405
- }
1406
-
1407
- if (uri === "vibecheck://truthpack") {
1408
- const truthpackPath = path.join(projectPath, ".vibecheck", "truth", "truthpack.json");
1409
- return await safeReadResource(truthpackPath, "No truthpack. Run vibecheck.ctx first.");
1410
- }
1411
-
1412
- if (uri === "vibecheck://missions") {
1413
- const missionsDir = path.join(projectPath, ".vibecheck", "missions");
1414
- try {
1415
- // HARDENING: Validate directory read
1416
- const dirs = await fs.readdir(missionsDir);
1417
- const safeDirs = sanitizeArray(dirs, 100).filter(d => typeof d === 'string' && d.length > 0);
1418
- const latest = safeDirs.sort().reverse()[0];
1419
-
1420
- if (latest) {
1421
- const missionPath = path.join(missionsDir, latest, "missions.json");
1422
- return await safeReadResource(missionPath, "No missions found in latest directory.");
1423
- }
1424
- } catch (err) {
1425
- console.error(`[MCP] Error reading missions: ${err.message}`);
1426
- }
1427
- return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No missions. Run vibecheck.fix first."}' }] };
1428
- }
1429
-
1430
- if (uri === "vibecheck://reality") {
1431
- const realityPath = path.join(projectPath, ".vibecheck", "reality", "last_reality.json");
1432
- return await safeReadResource(realityPath, "No reality results. Run vibecheck verify first.");
1433
- }
1434
-
1435
- if (uri === "vibecheck://findings") {
1436
- const findingsPath = path.join(projectPath, ".vibecheck", "findings.json");
1437
-
1438
- // Try primary findings file
1439
- const content = await this.safeReadFile(findingsPath, 10 * 1024 * 1024);
1440
- if (content) {
1441
- const parsed = safeJsonParse(content);
1442
- if (parsed.success) {
1443
- const sanitized = redactSensitive(truncateOutput(content));
1444
- return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
1445
- }
1446
- }
1447
-
1448
- // HARDENING: Try summary.json as fallback with size limits
1449
- const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
1450
- const summaryContent = await this.safeReadFile(summaryPath, 10 * 1024 * 1024);
1451
-
1452
- if (summaryContent) {
1453
- const parsed = safeJsonParse(summaryContent);
1454
- if (parsed.success && parsed.data.findings) {
1455
- const findings = sanitizeArray(parsed.data.findings, 1000);
1456
- return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ findings }, null, 2) }] };
1457
- }
1458
- }
1459
-
1460
- return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No findings. Run vibecheck.scan first."}' }] };
1461
- }
1462
-
1463
- if (uri === "vibecheck://share") {
1464
- const missionsDir = path.join(projectPath, ".vibecheck", "missions");
1465
- try {
1466
- // HARDENING: Safe directory read
1467
- const dirs = await fs.readdir(missionsDir);
1468
- const safeDirs = sanitizeArray(dirs, 100).filter(d => typeof d === 'string' && d.length > 0);
1469
- const latest = safeDirs.sort().reverse()[0];
1470
-
1471
- if (latest) {
1472
- const sharePath = path.join(missionsDir, latest, "share", "share.json");
1473
-
1474
- // Try share.json first
1475
- const shareContent = await this.safeReadFile(sharePath, 10 * 1024 * 1024);
1476
- if (shareContent) {
1477
- const parsed = safeJsonParse(shareContent);
1478
- if (parsed.success) {
1479
- const sanitized = redactSensitive(truncateOutput(shareContent));
1480
- return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
1481
- }
1482
- }
1483
-
1484
- // HARDENING: Fallback to missions.json with safe read
1485
- const missionPath = path.join(missionsDir, latest, "missions.json");
1486
- return await safeReadResource(missionPath, "No share data available in latest mission.");
1487
- }
1488
- } catch (err) {
1489
- console.error(`[MCP] Error reading share pack: ${err.message}`);
1490
- }
1491
- return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No share pack. Run vibecheck.fix --share first."}' }] };
1492
- }
1493
-
1494
- if (uri === "vibecheck://prove") {
1495
- const provePath = path.join(projectPath, ".vibecheck", "prove", "last_prove.json");
1496
- return await safeReadResource(provePath, "No prove results. Run vibecheck.prove first.");
1497
- }
1498
-
1499
- return {
1500
- contents: [{
1501
- uri,
1502
- mimeType: "application/json",
1503
- text: JSON.stringify({ error: "Unknown resource URI" })
1504
- }]
1505
- };
1506
- },
1507
- );
1508
- }
1509
-
1510
- // ============================================================================
1511
- // HARDENED HELPERS
1512
- // ============================================================================
1513
-
1514
- /**
1515
- * Return a successful response with sanitization
1516
- * @param {string} text - Response text
1517
- * @param {boolean} includeAttribution - Include vibecheck attribution
1518
- * @returns {object} MCP response
1519
- */
1520
- success(text, includeAttribution = true) {
1521
- // Sanitize output: redact secrets and truncate
1522
- let sanitized = redactSensitive(sanitizeString(text, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
1523
-
1524
- const finalText = includeAttribution
1525
- ? `${sanitized}\n\n---\n_${CONTEXT_ATTRIBUTION}_`
1526
- : sanitized;
1527
-
1528
- return { content: [{ type: "text", text: finalText }] };
1529
- }
1530
-
1531
- /**
1532
- * Return an error response with sanitization
1533
- * @param {string} text - Error message
1534
- * @param {object} options - Additional options
1535
- * @returns {object} MCP error response
1536
- */
1537
- error(text, options = {}) {
1538
- const { code, suggestion, nextSteps = [] } = options;
1539
-
1540
- // Sanitize all text inputs
1541
- const sanitizedText = redactSensitive(sanitizeString(text, 1000));
1542
- const sanitizedSuggestion = suggestion ? redactSensitive(sanitizeString(suggestion, 500)) : null;
1543
- const sanitizedSteps = sanitizeArray(nextSteps, 10).map(s => sanitizeString(s, 200));
1544
-
1545
- let errorText = `❌ ${sanitizedText}`;
1546
-
1547
- if (code) {
1548
- errorText += `\n\n**Error Code:** \`${sanitizeString(code, 50)}\``;
1549
- }
1550
-
1551
- if (sanitizedSuggestion) {
1552
- errorText += `\n\n💡 **Suggestion:** ${sanitizedSuggestion}`;
1553
- }
1554
-
1555
- if (sanitizedSteps.length > 0) {
1556
- errorText += `\n\n**Next Steps:**\n`;
1557
- sanitizedSteps.forEach((step, i) => {
1558
- errorText += `${i + 1}. ${step}\n`;
1559
- });
1560
- }
1561
-
1562
- return { content: [{ type: "text", text: errorText }], isError: true };
1563
- }
1564
-
1565
- // Validate project path exists and is accessible (sync for simple validation)
1566
- validateProjectPath(projectPath) {
1567
- try {
1568
- const stats = fsSync.statSync(projectPath);
1569
- if (!stats.isDirectory()) {
1570
- return {
1571
- valid: false,
1572
- error: `Path is not a directory: ${projectPath}`,
1573
- suggestion: "Provide a directory path, not a file",
1574
- nextSteps: [
1575
- "Check the path you provided",
1576
- "Ensure it points to a project directory",
1577
- ],
1578
- };
1579
- }
1580
- return { valid: true };
1581
- } catch (e) {
1582
- return {
1583
- valid: false,
1584
- error: `Cannot access path: ${projectPath}`,
1585
- code: e.code || "PATH_ACCESS_ERROR",
1586
- suggestion: "Check that the path exists and you have read permissions",
1587
- nextSteps: [
1588
- "Verify the path is correct",
1589
- "Check file permissions",
1590
- "Ensure the directory exists",
1591
- ],
1592
- };
1593
- }
1594
- }
1595
-
1596
- // ============================================================================
1597
- // SCAN
1598
- // ============================================================================
1599
- async handleScan(projectPath, args) {
1600
- // Validate project path first
1601
- const validation = this.validateProjectPath(projectPath);
1602
- if (!validation.valid) {
1603
- return this.error(validation.error, {
1604
- code: validation.code || "INVALID_PATH",
1605
- suggestion: validation.suggestion,
1606
- nextSteps: validation.nextSteps || [],
1607
- });
1608
- }
1609
-
1610
- // HARDENING: Validate and sanitize profile
1611
- const validProfiles = ["quick", "full", "ship", "ci", "security", "compliance", "ai"];
1612
- const profile = validProfiles.includes(args?.profile) ? args?.profile : "quick";
1613
-
1614
- // HARDENING: Sanitize only array
1615
- const only = sanitizeArray(args?.only, 20).map(item => sanitizeString(item, 50));
1616
-
1617
- // Initialize API integration with timeout and circuit breaker
1618
- let apiScan = null;
1619
- let apiConnected = false;
1620
-
1621
- // HARDENING: Check circuit breaker before attempting API calls
1622
- const circuitCheck = checkCircuitBreaker();
1623
-
1624
- // Try to connect to API for dashboard integration
1625
- if (circuitCheck.allowed) {
1626
- try {
1627
- // HARDENING: Add timeout to API availability check
1628
- const apiCheckPromise = isApiAvailable();
1629
- const timeoutPromise = new Promise((_, reject) =>
1630
- setTimeout(() => reject(new Error('API check timeout')), 5000)
1631
- );
1632
-
1633
- apiConnected = await Promise.race([apiCheckPromise, timeoutPromise]);
1634
-
1635
- if (apiConnected) {
1636
- // Create scan record in dashboard
1637
- const createScanPromise = createScan({
1638
- localPath: sanitizeString(projectPath, 500),
1639
- branch: sanitizeString(args?.branch, 100) || 'main',
1640
- enableLLM: false,
1641
- });
1642
- const scanTimeoutPromise = new Promise((_, reject) =>
1643
- setTimeout(() => reject(new Error('Create scan timeout')), 10000)
1644
- );
1645
-
1646
- apiScan = await Promise.race([createScanPromise, scanTimeoutPromise]);
1647
- console.error(`[MCP] Connected to dashboard (Scan ID: ${apiScan.scanId})`);
1648
- recordApiResult(true); // Record success
1649
- }
1650
- } catch (err) {
1651
- // API connection is optional, continue without it
1652
- console.error(`[MCP] Dashboard integration unavailable: ${err.message}`);
1653
- recordApiResult(false); // Record failure
1654
- }
1655
- } else {
1656
- console.error(`[MCP] ${circuitCheck.reason}`);
1657
- }
1658
-
1659
- let output = "# 🔍 vibecheck Scan\n\n";
1660
- output += `**Profile:** ${profile}\n`;
1661
- output += `**Path:** ${projectPath}\n\n`;
1662
-
1663
- // Build CLI arguments array (secure - no injection possible)
1664
- const cliArgs = [`--profile=${profile}`, "--json"];
1665
- if (only.length > 0) {
1666
- cliArgs.push(`--only=${only.join(",")}`);
1667
- }
1668
-
1669
- try {
1670
- await this.runCLI("scan", cliArgs, projectPath, { timeout: CONFIG.TIMEOUTS.SCAN });
1671
-
1672
- // Read summary from disk
1673
- const summary = await this.parseSummaryFromDisk(projectPath);
1674
- if (summary) {
1675
- output += this.formatScanOutput(summary, projectPath);
1676
-
1677
- // Submit results to dashboard if connected
1678
- if (apiConnected && apiScan) {
1679
- try {
1680
- // HARDENING: Add timeout to result submission
1681
- const submitPromise = submitScanResults(apiScan.scanId, {
1682
- verdict: sanitizeString(summary.verdict, 50) || 'UNKNOWN',
1683
- score: sanitizeNumber(summary.score?.overall, 0, 100, 0),
1684
- findings: sanitizeArray(summary.findings, 1000) || [],
1685
- filesScanned: sanitizeNumber(summary.stats?.filesScanned, 0, 1000000, 0),
1686
- linesScanned: sanitizeNumber(summary.stats?.linesScanned, 0, 100000000, 0),
1687
- durationMs: sanitizeNumber(summary.timings?.total, 0, 3600000, 0),
1688
- metadata: {
1689
- profile,
1690
- source: 'mcp-server',
1691
- version: CONFIG.VERSION,
1692
- },
1693
- });
1694
- const submitTimeout = new Promise((_, reject) =>
1695
- setTimeout(() => reject(new Error('Submit timeout')), 10000)
1696
- );
1697
-
1698
- await Promise.race([submitPromise, submitTimeout]);
1699
- console.error(`[MCP] Results sent to dashboard`);
1700
- } catch (err) {
1701
- console.error(`[MCP] Failed to send results to dashboard: ${err.message}`);
1702
- }
1703
- }
1704
- }
1705
- output += "\n---\n_Context Enhanced by vibecheck AI_\n";
1706
- return this.success(output);
1707
- } catch (err) {
1708
- // Graceful degradation: check if scan completed but found issues (exit code 1)
1709
- const summary = await this.parseSummaryFromDisk(projectPath);
1710
- if (summary) {
1711
- output += this.formatScanOutput(summary, projectPath);
1712
-
1713
- // Submit results to dashboard if connected
1714
- if (apiConnected && apiScan) {
1715
- try {
1716
- // HARDENING: Add timeout to error case submission
1717
- const submitPromise = submitScanResults(apiScan.scanId, {
1718
- verdict: sanitizeString(summary.verdict, 50) || 'UNKNOWN',
1719
- score: sanitizeNumber(summary.score?.overall, 0, 100, 0),
1720
- findings: sanitizeArray(summary.findings, 1000) || [],
1721
- filesScanned: sanitizeNumber(summary.stats?.filesScanned, 0, 1000000, 0),
1722
- linesScanned: sanitizeNumber(summary.stats?.linesScanned, 0, 100000000, 0),
1723
- durationMs: sanitizeNumber(summary.timings?.total, 0, 3600000, 0),
1724
- metadata: {
1725
- profile,
1726
- source: 'mcp-server',
1727
- version: CONFIG.VERSION,
1728
- error: sanitizeString(err.message, 500),
1729
- },
1730
- });
1731
- const submitTimeout = new Promise((_, reject) =>
1732
- setTimeout(() => reject(new Error('Submit timeout')), 10000)
1733
- );
1734
-
1735
- await Promise.race([submitPromise, submitTimeout]);
1736
- console.error(`[MCP] Results sent to dashboard (with error)`);
1737
- } catch (apiErr) {
1738
- console.error(`[MCP] Failed to send results to dashboard: ${apiErr.message}`);
1739
- }
1740
- }
1741
- output += `\n⚠️ Scan completed with findings (exit code ${err.code || 1})\n`;
1742
- return this.success(output);
1743
- }
1744
-
1745
- // Report error to dashboard if connected
1746
- if (apiConnected && apiScan) {
1747
- try {
1748
- // HARDENING: Add timeout to error reporting
1749
- const reportPromise = reportScanError(apiScan.scanId, err);
1750
- const reportTimeout = new Promise((_, reject) =>
1751
- setTimeout(() => reject(new Error('Report timeout')), 10000)
1752
- );
1753
-
1754
- await Promise.race([reportPromise, reportTimeout]);
1755
- console.error(`[MCP] Error reported to dashboard`);
1756
- } catch (apiErr) {
1757
- console.error(`[MCP] Failed to report error to dashboard: ${apiErr.message}`);
1758
- }
1759
- }
1760
-
1761
- return this.error(`Scan failed: ${err.message}`, {
1762
- code: "SCAN_ERROR",
1763
- suggestion: "Check that the project path is valid and contains scanable code",
1764
- nextSteps: [
1765
- "Verify the project path is correct",
1766
- "Ensure you have read permissions",
1767
- "Check that required dependencies are installed",
1768
- "Try running: vibecheck scan --help",
1769
- ],
1770
- });
1771
- }
1772
- }
1773
-
1774
- // ============================================================================
1775
- // GATE - PRO tier required (CI/CD enforcement)
1776
- // ============================================================================
1777
- async handleGate(projectPath, args) {
1778
- // Check tier access (PRO tier required - aligned with CLI entitlements-v2.js)
1779
- const access = await getFeatureAccessStatus("vibecheck.gate", args?.apiKey, args);
1780
- if (!access.hasAccess) {
1781
- return {
1782
- content: [{
1783
- type: "text",
1784
- text: JSON.stringify({
1785
- ok: false,
1786
- error: access.error || {
1787
- code: 'NOT_ENTITLED',
1788
- message: 'Requires PRO',
1789
- userAction: 'Open billing',
1790
- retryable: false,
1791
- },
1792
- }, null, 2)
1793
- }],
1794
- isError: true
1795
- };
1796
- }
1797
-
1798
- const policy = args?.policy || "strict";
1799
-
1800
- let output = "# 🚦 vibecheck Gate\n\n";
1801
- output += `**Policy:** ${policy}\n\n`;
1802
-
1803
- // Build CLI arguments array (secure)
1804
- const cliArgs = [`--policy=${policy}`];
1805
- if (args?.sarif) cliArgs.push("--sarif");
1806
-
1807
- try {
1808
- await this.runCLI("gate", cliArgs, projectPath);
1809
-
1810
- output += "## ✅ GATE PASSED\n\n";
1811
- output += "All checks passed. Clear to merge.\n";
1812
- } catch (err) {
1813
- output += "## 🚫 GATE FAILED\n\n";
1814
- output += "Build blocked. Fix the issues and re-run.\n\n";
1815
- output += `Run \`vibecheck fix --plan\` to see recommended fixes.\n`;
1816
- }
1817
-
1818
- return this.success(output);
1819
- }
1820
-
1821
- // ============================================================================
1822
- // FIX MISSIONS v1 - PRO tier required (aligned with CLI entitlements-v2.js)
1823
- // ============================================================================
1824
- async handleFix(projectPath, args) {
1825
- // Check tier access - vibecheck.fix is PRO (all modes: plan, apply, autopilot)
1826
- const access = await getFeatureAccessStatus("vibecheck.fix", args?.apiKey, args);
1827
- if (!access.hasAccess) {
1828
- return {
1829
- content: [{
1830
- type: "text",
1831
- text: JSON.stringify({
1832
- ok: false,
1833
- error: access.error || {
1834
- code: 'NOT_ENTITLED',
1835
- message: 'Requires PRO',
1836
- userAction: 'Open billing',
1837
- retryable: false,
1838
- },
1839
- }, null, 2)
1840
- }],
1841
- isError: true
1842
- };
1843
- }
1844
-
1845
- const mode = args?.autopilot ? "Autopilot" :
1846
- args?.apply ? "Apply" :
1847
- args?.promptOnly ? "Prompt Only" :
1848
- args?.dryRun ? "Dry Run" : "Plan";
1849
-
1850
- let output = "# 🛠 vibecheck Fix Missions v1\n\n";
1851
- output += `**Mode:** ${mode}\n`;
1852
- output += `**Max Missions:** ${args?.maxMissions || 8}\n`;
1853
- if (args?.autopilot) output += `**Max Steps:** ${args?.maxSteps || 10}\n`;
1854
- if (args?.dryRun) output += `**Dry Run:** Yes (no changes will be made)\n`;
1855
- output += "\n";
1856
-
1857
- // Build CLI arguments array (secure)
1858
- const cliArgs = [];
1859
- if (args?.promptOnly) cliArgs.push("--prompt-only");
1860
- if (args?.apply) cliArgs.push("--apply");
1861
- if (args?.autopilot) cliArgs.push("--autopilot");
1862
- if (args?.share) cliArgs.push("--share");
1863
- if (args?.dryRun) cliArgs.push("--dry-run");
1864
- if (args?.maxMissions) cliArgs.push("--max-missions", String(args.maxMissions));
1865
- if (args?.maxSteps) cliArgs.push("--max-steps", String(args.maxSteps));
1866
-
1867
- try {
1868
- const result = await this.runCLI("fix", cliArgs, projectPath, {
1869
- timeout: CONFIG.TIMEOUTS.AUTOPILOT
1870
- });
1871
-
1872
- output += this.stripAnsi(result.stdout);
1873
-
1874
- // Read mission pack if available
1875
- const missionsDir = path.join(projectPath, CONFIG.OUTPUT_DIR, "missions");
1876
- try {
1877
- const dirs = await fs.readdir(missionsDir);
1878
- const latest = dirs.sort().reverse()[0];
1879
- if (latest) {
1880
- const missionPath = path.join(missionsDir, latest, "missions.json");
1881
- const missions = JSON.parse(await fs.readFile(missionPath, "utf-8"));
1882
-
1883
- output += "\n## Planned Missions\n\n";
1884
- output += "| # | Mission | Target Findings |\n|---|---------|----------------|\n";
1885
- for (let i = 0; i < missions.missions.length; i++) {
1886
- const m = missions.missions[i];
1887
- output += `| ${i + 1} | ${m.title} | ${m.targetFindingIds.length} |\n`;
1888
- }
1889
- output += `\n📁 **Mission Pack:** ${CONFIG.OUTPUT_DIR}/missions/${latest}\n`;
1890
- }
1891
- } catch {}
1892
- } catch (err) {
1893
- output += `\n⚠️ Fix error: ${err.message}\n`;
1894
- if (err.partialOutput) output += `\n${this.stripAnsi(err.partialOutput)}\n`;
1895
- }
1896
-
1897
- output += "\n---\n_Fix Missions v1 — Reality Firewall Protected_\n";
1898
- return this.success(output);
1899
- }
1900
-
1901
- // ============================================================================
1902
- // SHARE - Generate share bundle from fix missions
1903
- // ============================================================================
1904
- async handleShare(projectPath, args) {
1905
- let output = "# 📦 vibecheck Share Bundle\n\n";
1906
-
1907
- // Build CLI arguments array (secure)
1908
- const cliArgs = [];
1909
- if (args?.prComment) cliArgs.push("--pr-comment");
1910
- if (args?.out) cliArgs.push("--out", args.out);
1911
-
1912
- try {
1913
- const result = await this.runCLI("share", cliArgs, projectPath);
1914
-
1915
- output += this.stripAnsi(result.stdout);
1916
-
1917
- // Read share pack if available
1918
- const missionsDir = path.join(projectPath, CONFIG.OUTPUT_DIR, "missions");
1919
- try {
1920
- const dirs = await fs.readdir(missionsDir);
1921
- const latest = dirs.sort().reverse()[0];
1922
- if (latest) {
1923
- const sharePath = path.join(missionsDir, latest, "share", "share.json");
1924
- try {
1925
- const share = JSON.parse(await fs.readFile(sharePath, "utf-8"));
1926
-
1927
- output += "\n## Share Pack Summary\n\n";
1928
- output += `| Metric | Value |\n|--------|-------|\n`;
1929
- output += `| Mission Pack | ${latest} |\n`;
1930
- output += `| Total Steps | ${share.timeline?.length || 0} |\n`;
1931
- output += `| Final Verdict | ${share.finalVerdict || "Unknown"} |\n`;
1932
-
1933
- if (share.timeline?.length > 0) {
1934
- output += "\n## Timeline\n\n";
1935
- output += "| Step | Mission | Before | After |\n|------|---------|--------|-------|\n";
1936
- for (const t of share.timeline.slice(0, 10)) {
1937
- output += `| ${t.step} | ${t.missionTitle || t.missionId} | ${t.beforeVerdict} | ${t.afterVerdict} |\n`;
1938
- }
1939
- }
1940
-
1941
- output += `\n📁 **Share Pack:** ${CONFIG.OUTPUT_DIR}/missions/${latest}/share/\n`;
1942
- output += `📄 **PR Comment:** ${CONFIG.OUTPUT_DIR}/missions/${latest}/share/pr_comment.md\n`;
1943
- } catch {}
1944
- }
1945
- } catch {}
1946
- } catch (err) {
1947
- output += `\n⚠️ Share error: ${err.message}\n`;
1948
- if (err.partialOutput) output += `\n${this.stripAnsi(err.partialOutput)}\n`;
1949
- }
1950
-
1951
- output += "\n---\n_Fix Missions Share Bundle_\n";
1952
- return this.success(output);
1953
- }
1954
-
1955
- // ============================================================================
1956
- // CTX - Truth Pack Generator
1957
- // ============================================================================
1958
- async handleCtx(projectPath, args) {
1959
- let output = "# 📦 vibecheck Truth Pack\n\n";
1960
- output += `**Path:** ${projectPath}\n\n`;
1961
-
1962
- // Build CLI arguments array (secure)
1963
- const cliArgs = [];
1964
- if (args?.snapshot) cliArgs.push("--snapshot");
1965
- if (args?.json) cliArgs.push("--json");
1966
-
1967
- try {
1968
- const result = await this.runCLI("ctx", cliArgs, projectPath);
1969
-
1970
- if (args?.json) {
1971
- // Return raw JSON
1972
- return this.success(result.stdout);
1973
- }
1974
-
1975
- output += this.stripAnsi(result.stdout);
1976
-
1977
- // Read truthpack summary
1978
- const truthpackPath = path.join(projectPath, CONFIG.OUTPUT_DIR, "truth", "truthpack.json");
1979
- try {
1980
- const truthpack = JSON.parse(await fs.readFile(truthpackPath, "utf-8"));
1981
-
1982
- output += "\n## Truth Pack Contents\n\n";
1983
- output += `| Category | Count |\n|----------|-------|\n`;
1984
- output += `| Server Routes | ${truthpack.routes?.server?.length || 0} |\n`;
1985
- output += `| Client Refs | ${truthpack.routes?.clientRefs?.length || 0} |\n`;
1986
- output += `| Route Gaps | ${truthpack.routes?.gaps?.length || 0} |\n`;
1987
- output += `| Env Used | ${truthpack.env?.vars?.length || 0} |\n`;
1988
- output += `| Env Declared | ${truthpack.env?.declared?.length || 0} |\n`;
1989
- output += `| Auth Middleware | ${truthpack.auth?.nextMiddleware?.length || 0} |\n`;
1990
- output += `| Stripe Detected | ${truthpack.billing?.hasStripe ? "✅" : "❌"} |\n`;
1991
- output += `| Webhooks | ${truthpack.billing?.summary?.webhookHandlersFound || 0} |\n`;
1992
-
1993
- output += `\n📁 **Saved:** ${CONFIG.OUTPUT_DIR}/truth/truthpack.json\n`;
1994
- } catch {}
1995
- } catch (err) {
1996
- output += `\n⚠️ Truth pack error: ${err.message}\n`;
1997
- }
1998
-
1999
- output += "\n---\n_Ground Truth for AI Agents_\n";
2000
- return this.success(output);
2001
- }
2002
-
2003
- // ============================================================================
2004
- // PROVE - One Command Reality Proof (PRO tier required)
2005
- // ============================================================================
2006
- async handleProve(projectPath, args) {
2007
- // Check tier access (PRO tier required - aligned with CLI entitlements-v2.js)
2008
- const access = await getFeatureAccessStatus("vibecheck.prove", args?.apiKey, args);
2009
- if (!access.hasAccess) {
2010
- return {
2011
- content: [{
2012
- type: "text",
2013
- text: JSON.stringify({
2014
- ok: false,
2015
- error: access.error || {
2016
- code: 'NOT_ENTITLED',
2017
- message: 'Requires PRO',
2018
- userAction: 'Open billing',
2019
- retryable: false,
2020
- },
2021
- }, null, 2)
2022
- }],
2023
- isError: true
2024
- };
2025
- }
2026
-
2027
- let output = "# 🔬 vibecheck prove\n\n";
2028
- output += `**URL:** ${args?.url || "(static only)"}\n`;
2029
- output += `**Max Fix Rounds:** ${args?.maxFixRounds || 3}\n\n`;
2030
-
2031
- // Build CLI arguments array (secure)
2032
- const cliArgs = [];
2033
- if (args?.url) cliArgs.push("--url", args.url);
2034
- if (args?.auth) cliArgs.push("--auth", args.auth);
2035
- if (args?.storageState) cliArgs.push("--storage-state", args.storageState);
2036
- if (args?.skipReality) cliArgs.push("--skip-reality");
2037
- if (args?.skipFix) cliArgs.push("--skip-fix");
2038
- if (args?.maxFixRounds) cliArgs.push("--max-fix-rounds", String(args.maxFixRounds));
2039
-
2040
- try {
2041
- const result = await this.runCLI("prove", cliArgs, projectPath, {
2042
- timeout: CONFIG.TIMEOUTS.PROVE
2043
- });
2044
-
2045
- output += this.stripAnsi(result.stdout);
2046
-
2047
- // Read prove report
2048
- const provePath = path.join(projectPath, CONFIG.OUTPUT_DIR, "prove", "last_prove.json");
2049
- try {
2050
- const report = JSON.parse(await fs.readFile(provePath, "utf-8"));
2051
-
2052
- output += "\n## Timeline\n\n";
2053
- output += "| Step | Action | Status |\n|------|--------|--------|\n";
2054
- for (const t of report.timeline || []) {
2055
- output += `| ${t.step} | ${t.action} | ${t.status || t.verdict || "ok"} |\n`;
2056
- }
2057
-
2058
- output += `\n**Final Verdict:** ${report.finalVerdict}\n`;
2059
- output += `**Duration:** ${report.meta?.durationSeconds}s\n`;
2060
- } catch {}
2061
- } catch (err) {
2062
- output += `\n⚠️ Prove error: ${err.message}\n`;
2063
- if (err.partialOutput) output += `\n${this.stripAnsi(err.partialOutput)}\n`;
2064
- }
2065
-
2066
- output += "\n---\n_One command to make it real_\n";
2067
- return this.success(output);
2068
- }
2069
-
2070
- // ============================================================================
2071
- // PROOF
2072
- // ============================================================================
2073
- async handleProof(projectPath, args) {
2074
- const mode = args?.mode;
2075
-
2076
- if (!mode) {
2077
- return this.error("Mode required: 'mocks' or 'reality'");
2078
- }
2079
-
2080
- let output = `# 🎬 vibecheck Proof: ${mode.toUpperCase()}\n\n`;
2081
-
2082
- // Build CLI arguments array (secure)
2083
- const cliArgs = [mode];
2084
- if (mode === "reality" && args?.url) cliArgs.push(`--url=${args.url}`);
2085
- if (mode === "reality" && args?.flow) cliArgs.push(`--flow=${args.flow}`);
2086
-
2087
- try {
2088
- const result = await this.runCLI("proof", cliArgs, projectPath, {
2089
- timeout: CONFIG.TIMEOUTS.SCAN,
2090
- skipAuth: false
2091
- });
2092
-
2093
- output += result.stdout;
2094
- } catch (err) {
2095
- if (mode === "mocks") {
2096
- output += "## 🚫 MOCKPROOF: FAIL\n\n";
2097
- output += "Mock/demo code detected in production paths.\n";
2098
- } else {
2099
- output += "## 🚫 REALITY MODE: FAIL\n\n";
2100
- output += "Fake data or mock services detected at runtime.\n";
2101
- }
2102
- output += `\n${err.partialOutput || err.message}\n`;
2103
- }
2104
-
2105
- return this.success(output);
2106
- }
2107
-
2108
- // ============================================================================
2109
- // VALIDATE
2110
- // ============================================================================
2111
- async handleValidate(projectPath, args) {
2112
- const { code, intent } = args;
2113
- if (!code) return this.error("Code is required");
2114
-
2115
- let output = "# 🤖 AI Code Validation\n\n";
2116
- if (intent) output += `**Intent:** ${intent}\n\n`;
2117
-
2118
- try {
2119
- const {
2120
- runHallucinationCheck,
2121
- validateIntent,
2122
- validateQuality,
2123
- } = require(
2124
- path.join(__dirname, "..", "bin", "runners", "lib", "ai-bridge.js"),
2125
- );
2126
-
2127
- // 1. Hallucinations (checking against project deps + internal logic)
2128
- // Note: In MCP context, we might want to check the provided code specifically for imports.
2129
- // The bridge's runHallucinationCheck mostly checks package.json.
2130
- // But we can check imports in the 'code' snippet if we extract them.
2131
- // The bridge handles extractImports internally but runHallucinationCheck doesn't expose it directly for a string input.
2132
- // We will rely on package.json sanity check for now + static analysis of the snippet.
2133
-
2134
- const hallResult = await runHallucinationCheck(projectPath);
2135
-
2136
- // 2. Intent
2137
- let intentResult = { score: 100, issues: [] };
2138
- if (intent) {
2139
- intentResult = validateIntent(code, intent);
2140
- }
2141
-
2142
- // 3. Quality
2143
- const qualityResult = validateQuality(code);
2144
-
2145
- const allIssues = [
2146
- ...hallResult.issues,
2147
- ...intentResult.issues,
2148
- ...qualityResult.issues,
2149
- ];
2150
-
2151
- const score = Math.round(
2152
- (hallResult.score + intentResult.score + qualityResult.score) / 3,
2153
- );
2154
- const status = score >= 80 ? "✅ PASSED" : "⚠️ ISSUES FOUND";
2155
-
2156
- output += `**Status:** ${status} (${score}/100)\n\n`;
2157
-
2158
- if (allIssues.length > 0) {
2159
- output += "### Issues\n";
2160
- for (const issue of allIssues) {
2161
- const icon =
2162
- issue.severity === "critical"
2163
- ? "🔴"
2164
- : issue.severity === "high"
2165
- ? "🟠"
2166
- : "🟡";
2167
- output += `- ${icon} **[${issue.type}]** ${issue.message}\n`;
2168
- }
2169
- } else {
2170
- output += "✨ Code looks valid and safe.\n";
2171
- }
2172
-
2173
- return this.success(output);
2174
- } catch (err) {
2175
- return this.error(`Validation failed: ${err.message}`);
2176
- }
2177
- }
2178
-
2179
- // ============================================================================
2180
- // REPORT
2181
- // ============================================================================
2182
- async handleReport(projectPath, args) {
2183
- const type = args?.type || "summary";
2184
- const outputDir = path.join(projectPath, ".vibecheck");
2185
-
2186
- let output = "# 📄 vibecheck Report\n\n";
2187
-
2188
- try {
2189
- if (type === "summary") {
2190
- const summaryPath = path.join(outputDir, "summary.json");
2191
- const summary = JSON.parse(await fs.readFile(summaryPath, "utf-8"));
2192
-
2193
- output += `**Score:** ${summary.score}/100 (${summary.grade})\n`;
2194
- output += `**Verdict:** ${summary.canShip ? "✅ SHIP" : "🚫 NO-SHIP"}\n`;
2195
- output += `**Generated:** ${summary.timestamp}\n`;
2196
- } else if (type === "full") {
2197
- const reportPath = path.join(outputDir, "summary.md");
2198
- output += await fs.readFile(reportPath, "utf-8");
2199
- } else if (type === "sarif") {
2200
- const sarifPath = path.join(outputDir, "results.sarif");
2201
- const sarif = await fs.readFile(sarifPath, "utf-8");
2202
- output += "```json\n" + sarif.substring(0, 2000) + "\n```\n";
2203
- output += `\n📄 **Full SARIF:** ${sarifPath}\n`;
2204
- } else if (type === "html") {
2205
- output += `📄 **HTML Report:** ${path.join(outputDir, "report.html")}\n`;
2206
- output += "Open in browser to view the full report.\n";
2207
- }
2208
- } catch (err) {
2209
- output += `⚠️ No ${type} report found. Run \`vibecheck.scan\` first.\n`;
2210
- }
2211
-
2212
- return this.success(output);
2213
- }
2214
-
2215
- // ============================================================================
2216
- // SHIP - Verdict engine (PRO tier required - aligned with CLI entitlements-v2.js)
2217
- // ============================================================================
2218
- async handleShip(projectPath, args) {
2219
- // Check tier access (PRO tier required)
2220
- const access = await getFeatureAccessStatus("vibecheck.ship", args?.apiKey, args);
2221
- if (!access.hasAccess) {
2222
- return {
2223
- content: [{
2224
- type: "text",
2225
- text: JSON.stringify({
2226
- ok: false,
2227
- error: access.error || {
2228
- code: 'NOT_ENTITLED',
2229
- message: 'Requires PRO',
2230
- userAction: 'Open billing',
2231
- retryable: false,
2232
- },
2233
- }, null, 2)
2234
- }],
2235
- isError: true
2236
- };
2237
- }
2238
-
2239
- // HARDENING: Validate project path
2240
- const validation = this.validateProjectPath(projectPath);
2241
- if (!validation.valid) {
2242
- return this.error(validation.error, {
2243
- code: validation.code || "INVALID_PATH",
2244
- suggestion: validation.suggestion,
2245
- nextSteps: validation.nextSteps || [],
2246
- });
2247
- }
2248
-
2249
- let output = "# 🚀 vibecheck Ship\n\n";
2250
- output += `**Path:** ${projectPath}\n\n`;
2251
-
2252
- // Build CLI arguments array (secure)
2253
- const cliArgs = [];
2254
- if (args?.fix) cliArgs.push("--fix");
2255
-
2256
- try {
2257
- const result = await this.runCLI("ship", cliArgs, projectPath);
2258
-
2259
- output += this.stripAnsi(result.stdout);
2260
- } catch (err) {
2261
- output += `\n⚠️ Ship check failed: ${err.message}\n`;
2262
- if (err.partialOutput) output += `\n${this.stripAnsi(err.partialOutput)}\n`;
2263
- }
2264
-
2265
- output += "\n---\n_Context Enhanced by vibecheck AI_\n";
2266
- return this.success(output);
2267
- }
2268
-
2269
- // ============================================================================
2270
- // VERIFY - Runtime browser testing
2271
- // ============================================================================
2272
- async handleVerify(projectPath, args) {
2273
- // HARDENING: Validate URL
2274
- const urlValidation = validateUrl(args?.url);
2275
- if (!urlValidation.valid) {
2276
- return this.error(urlValidation.error, {
2277
- code: 'INVALID_URL',
2278
- suggestion: 'Provide a valid HTTP/HTTPS URL',
2279
- nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
2280
- });
2281
- }
2282
- const url = urlValidation.url;
2283
-
2284
- let output = "# 🧪 vibecheck Verify\n\n";
2285
- output += `**URL:** ${url}\n`;
2286
-
2287
- // HARDENING: Sanitize array inputs
2288
- const flows = sanitizeArray(args?.flows, 10);
2289
- if (flows.length) output += `**Flows:** ${flows.join(", ")}\n`;
2290
- if (args?.headed) output += `**Mode:** Headed (visible browser)\n`;
2291
- if (args?.record) output += `**Recording:** Enabled\n`;
2292
- output += "\n";
2293
-
2294
- // Build CLI arguments array (secure)
2295
- const cliArgs = ["--url", url];
2296
- // HARDENING: Sanitize auth - don't log full credentials
2297
- if (args?.auth && typeof args.auth === 'string') {
2298
- cliArgs.push("--auth", sanitizeString(args.auth, 200));
2299
- }
2300
- if (flows.length) cliArgs.push("--flows", flows.join(","));
2301
- if (args?.headed) cliArgs.push("--headed");
2302
- if (args?.record) cliArgs.push("--record");
2303
-
2304
- try {
2305
- const result = await this.runCLI("verify", cliArgs, projectPath, {
2306
- timeout: CONFIG.TIMEOUTS.VERIFY
2307
- });
2308
-
2309
- output += this.stripAnsi(result.stdout);
2310
-
2311
- // Try to read reality results
2312
- const realityPath = path.join(projectPath, CONFIG.OUTPUT_DIR, "reality", "last_reality.json");
2313
- try {
2314
- const reality = JSON.parse(await fs.readFile(realityPath, "utf-8"));
2315
-
2316
- output += "\n## Verification Summary\n\n";
2317
- output += `| Metric | Value |\n|--------|-------|\n`;
2318
- output += `| Pages Visited | ${reality.meta?.pagesVisited || 0} |\n`;
2319
- output += `| Buttons Clicked | ${reality.meta?.buttonsClicked || 0} |\n`;
2320
- output += `| Forms Tested | ${reality.meta?.formsTested || 0} |\n`;
2321
- output += `| Dead UI Found | ${reality.findings?.length || 0} |\n`;
2322
- output += `| Console Errors | ${reality.consoleErrors?.length || 0} |\n`;
2323
- output += `| Duration | ${reality.meta?.durationMs || 0}ms |\n`;
2324
-
2325
- if (reality.findings?.length > 0) {
2326
- output += "\n## Dead UI Detected\n\n";
2327
- for (const f of reality.findings.slice(0, 5)) {
2328
- output += `- **${f.title}** — ${f.reason}\n`;
2329
- }
2330
- if (reality.findings.length > 5) {
2331
- output += `\n_...and ${reality.findings.length - 5} more_\n`;
2332
- }
2333
- }
2334
-
2335
- output += `\n📁 **Full Report:** ${CONFIG.OUTPUT_DIR}/reality/last_reality.json\n`;
2336
- } catch {}
2337
- } catch (err) {
2338
- output += `\n⚠️ Verify failed: ${err.message}\n`;
2339
- if (err.partialOutput) output += `\n${this.stripAnsi(err.partialOutput)}\n`;
2340
- }
2341
-
2342
- output += "\n---\n_Runtime Verification by vibecheck_\n";
2343
- return this.success(output);
2344
- }
2345
-
2346
- // ============================================================================
2347
- // REALITY v2 - Two-Pass Auth Verification + Dead UI Crawler
2348
- // ============================================================================
2349
- async handleReality(projectPath, args) {
2350
- // HARDENING: Validate URL
2351
- const urlValidation = validateUrl(args?.url);
2352
- if (!urlValidation.valid) {
2353
- return this.error(urlValidation.error, {
2354
- code: 'INVALID_URL',
2355
- suggestion: 'Provide a valid HTTP/HTTPS URL',
2356
- nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
2357
- });
2358
- }
2359
- const url = urlValidation.url;
2360
-
2361
- let output = "# 🧪 vibecheck Reality v2\n\n";
2362
- output += `**URL:** ${url}\n`;
2363
- output += `**Two-Pass Auth:** ${args?.verifyAuth ? "Yes" : "No"}\n`;
2364
-
2365
- // HARDENING: Safely display auth info (mask password)
2366
- if (args?.auth && typeof args.auth === 'string') {
2367
- const authParts = args.auth.split(":");
2368
- const maskedAuth = authParts[0] ? `${authParts[0].slice(0, 20)}:***` : '***';
2369
- output += `**Auth:** ${maskedAuth}\n`;
2370
- }
2371
- if (args?.storageState) output += `**Storage State:** ${sanitizeString(args.storageState, 100)}\n`;
2372
- if (args?.headed) output += `**Mode:** Headed (visible browser)\n`;
2373
- if (args?.danger) output += `**Danger Mode:** Enabled (risky clicks allowed)\n`;
2374
- output += "\n";
2375
-
2376
- // Build CLI arguments array (secure)
2377
- const cliArgs = ["--url", url];
2378
- if (args?.auth && typeof args.auth === 'string') {
2379
- cliArgs.push("--auth", sanitizeString(args.auth, 200));
2380
- }
2381
- if (args?.verifyAuth) cliArgs.push("--verify-auth");
2382
-
2383
- // HARDENING: Validate path arguments
2384
- if (args?.storageState) {
2385
- const pathCheck = sanitizePath(args.storageState, projectPath);
2386
- if (pathCheck.valid) {
2387
- cliArgs.push("--storage-state", pathCheck.path);
2388
- }
2389
- }
2390
- if (args?.saveStorageState) {
2391
- const pathCheck = sanitizePath(args.saveStorageState, projectPath);
2392
- if (pathCheck.valid) {
2393
- cliArgs.push("--save-storage-state", pathCheck.path);
2394
- }
2395
- }
2396
- if (args?.truthpack) {
2397
- const pathCheck = sanitizePath(args.truthpack, projectPath);
2398
- if (pathCheck.valid) {
2399
- cliArgs.push("--truthpack", pathCheck.path);
2400
- }
2401
- }
2402
- if (args?.headed) cliArgs.push("--headed");
2403
-
2404
- // HARDENING: Bound numeric arguments
2405
- if (args?.maxPages) {
2406
- cliArgs.push("--max-pages", String(sanitizeNumber(args.maxPages, 1, 100, 18)));
2407
- }
2408
- if (args?.maxDepth) {
2409
- cliArgs.push("--max-depth", String(sanitizeNumber(args.maxDepth, 1, 10, 2)));
2410
- }
2411
- if (args?.danger) cliArgs.push("--danger");
2412
-
2413
- try {
2414
- const result = await this.runCLI("reality", cliArgs, projectPath, {
2415
- timeout: CONFIG.TIMEOUTS.REALITY
2416
- });
2417
-
2418
- output += this.stripAnsi(result.stdout);
2419
-
2420
- // Read reality results
2421
- const realityPath = path.join(projectPath, CONFIG.OUTPUT_DIR, "reality", "last_reality.json");
2422
- try {
2423
- const reality = JSON.parse(await fs.readFile(realityPath, "utf-8"));
2424
-
2425
- output += "\n## Reality Report\n\n";
2426
- output += `| Metric | Value |\n|--------|-------|\n`;
2427
- output += `| Anon Pages | ${reality.passes?.anon?.pagesVisited?.length || 0} |\n`;
2428
- if (reality.passes?.auth) {
2429
- output += `| Auth Pages | ${reality.passes?.auth?.pagesVisited?.length || 0} |\n`;
2430
- }
2431
- output += `| Findings | ${reality.findings?.length || 0} |\n`;
2432
- output += `| Console Errors | ${reality.consoleErrors?.length || 0} |\n`;
2433
- output += `| Network Errors | ${reality.networkErrors?.length || 0} |\n`;
2434
-
2435
- if (reality.coverage) {
2436
- output += `| UI Coverage | ${reality.coverage.percent}% (${reality.coverage.hit}/${reality.coverage.total}) |\n`;
2437
- }
2438
-
2439
- if (reality.meta?.protectedMatcherCount > 0) {
2440
- output += `| Auth Matchers | ${reality.meta.protectedMatcherCount} |\n`;
2441
- }
2442
-
2443
- // Show findings by category
2444
- const blocks = reality.findings?.filter(f => f.severity === "BLOCK") || [];
2445
- const warns = reality.findings?.filter(f => f.severity === "WARN") || [];
2446
-
2447
- if (blocks.length > 0) {
2448
- output += "\n### 🛑 BLOCK Findings\n\n";
2449
- for (const f of blocks.slice(0, 5)) {
2450
- output += `- **${f.title}**\n - ${f.reason}\n`;
2451
- if (f.screenshot) output += ` - Screenshot: ${f.screenshot}\n`;
2452
- }
2453
- if (blocks.length > 5) {
2454
- output += `\n_...and ${blocks.length - 5} more BLOCKs_\n`;
2455
- }
2456
- }
2457
-
2458
- if (warns.length > 0) {
2459
- output += "\n### ⚠️ WARN Findings\n\n";
2460
- for (const f of warns.slice(0, 3)) {
2461
- output += `- **${f.title}** — ${f.reason}\n`;
2462
- }
2463
- if (warns.length > 3) {
2464
- output += `\n_...and ${warns.length - 3} more WARNs_\n`;
2465
- }
2466
- }
2467
-
2468
- // Verdict
2469
- const verdict = blocks.length > 0 ? "🛑 BLOCK" : warns.length > 0 ? "⚠️ WARN" : "✅ CLEAN";
2470
- output += `\n## Verdict: ${verdict}\n`;
2471
-
2472
- output += `\n📁 **Full Report:** ${CONFIG.OUTPUT_DIR}/reality/last_reality.json\n`;
2473
-
2474
- if (reality.meta?.savedStorageState) {
2475
- output += `📁 **Saved Auth State:** ${reality.meta.savedStorageState}\n`;
2476
- }
2477
- } catch {}
2478
- } catch (err) {
2479
- output += `\n⚠️ Reality failed: ${err.message}\n`;
2480
- if (err.partialOutput) output += `\n${this.stripAnsi(err.partialOutput)}\n`;
2481
- }
2482
-
2483
- output += "\n---\n_Reality Mode v2 — Two-Pass Auth Verification_\n";
2484
- return this.success(output);
2485
- }
2486
-
2487
- // ============================================================================
2488
- // AI-TEST - AI Agent testing
2489
- // ============================================================================
2490
- async handleAITest(projectPath, args) {
2491
- // HARDENING: Validate URL
2492
- const urlValidation = validateUrl(args?.url);
2493
- if (!urlValidation.valid) {
2494
- return this.error(urlValidation.error, {
2495
- code: 'INVALID_URL',
2496
- suggestion: 'Provide a valid HTTP/HTTPS URL',
2497
- nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
2498
- });
2499
- }
2500
- const url = urlValidation.url;
2501
-
2502
- // HARDENING: Sanitize goal string
2503
- const goal = sanitizeString(args?.goal, 500) || "Test all features";
2504
-
2505
- let output = "# 🤖 vibecheck AI Agent\n\n";
2506
- output += `**URL:** ${url}\n`;
2507
- output += `**Goal:** ${goal}\n\n`;
2508
-
2509
- // Build CLI arguments array (secure)
2510
- const cliArgs = ["--url", url];
2511
- if (goal) cliArgs.push("--goal", goal);
2512
- if (args?.headed) cliArgs.push("--headed");
2513
-
2514
- try {
2515
- const result = await this.runCLI("ai-test", cliArgs, projectPath, {
2516
- timeout: CONFIG.TIMEOUTS.VERIFY
2517
- });
2518
-
2519
- output += this.stripAnsi(result.stdout);
2520
-
2521
- // Try to read fix prompts
2522
- const promptPath = path.join(projectPath, CONFIG.OUTPUT_DIR, "ai-agent", "fix-prompt.md");
2523
- try {
2524
- const prompts = await fs.readFile(promptPath, "utf-8");
2525
- output += "\n## Fix Prompts Generated\n\n";
2526
- output += prompts.substring(0, 2000);
2527
- if (prompts.length > 2000) output += "\n\n... (truncated)";
2528
- } catch {}
2529
- } catch (err) {
2530
- output += `\n⚠️ AI Agent failed: ${err.message}\n`;
2531
- }
2532
-
2533
- output += "\n---\n_Context Enhanced by vibecheck AI_\n";
2534
- return this.success(output);
2535
- }
2536
-
2537
- // ============================================================================
2538
- // AUTOPILOT - Continuous protection
2539
- // ============================================================================
2540
- async handleAutopilot(projectPath, args) {
2541
- const action = args?.action || "status";
2542
-
2543
- let output = "# 🤖 vibecheck Autopilot\n\n";
2544
- output += `**Action:** ${action}\n\n`;
2545
-
2546
- // Build CLI arguments array (secure - no injection possible)
2547
- const cliArgs = [action];
2548
- if (args?.slack) cliArgs.push("--slack", args.slack);
2549
- if (args?.email) cliArgs.push("--email", args.email);
2550
-
2551
- try {
2552
- const result = await this.runCLI("autopilot", cliArgs, projectPath);
2553
-
2554
- output += this.stripAnsi(result.stdout);
2555
- } catch (err) {
2556
- output += `\n⚠️ Autopilot failed: ${err.message}\n`;
2557
- }
2558
-
2559
- return this.success(output);
2560
- }
2561
-
2562
- // ============================================================================
2563
- // AUTOPILOT PLAN - Generate fix plan (PRO tier required)
2564
- // ============================================================================
2565
- async handleAutopilotPlan(projectPath, args) {
2566
- // Check tier access (PRO tier required - aligned with CLI entitlements-v2.js)
2567
- const access = await getFeatureAccessStatus("vibecheck.fix", args?.apiKey, args);
2568
- if (!access.hasAccess) {
2569
- return {
2570
- content: [{
2571
- type: "text",
2572
- text: JSON.stringify({
2573
- ok: false,
2574
- error: access.error || {
2575
- code: 'NOT_ENTITLED',
2576
- message: 'Requires PRO',
2577
- userAction: 'Open billing',
2578
- retryable: false,
2579
- },
2580
- }, null, 2)
2581
- }],
2582
- isError: true
2583
- };
2584
- }
2585
-
2586
- let output = "# 🤖 vibecheck Autopilot Plan\n\n";
2587
- output += `**Path:** ${projectPath}\n`;
2588
- output += `**Profile:** ${args?.profile || "ship"}\n\n`;
2589
-
2590
- try {
2591
- // Use the core autopilot runner directly
2592
- const corePath = path.join(__dirname, "..", "packages", "core", "dist", "index.js");
2593
- let runAutopilot;
2594
-
2595
- try {
2596
- const core = await import(corePath);
2597
- runAutopilot = core.runAutopilot;
2598
- } catch {
2599
- // Fallback to CLI - build secure args array
2600
- const cliArgs = [
2601
- "plan",
2602
- "--profile", args?.profile || "ship",
2603
- "--max-fixes", String(args?.maxFixes || 10),
2604
- "--json"
2605
- ];
2606
-
2607
- const result = await this.runCLI("autopilot", cliArgs, projectPath);
2608
-
2609
- const jsonResult = JSON.parse(result.stdout);
2610
- output += `## Scan Results\n\n`;
2611
- output += `- **Total findings:** ${jsonResult.totalFindings}\n`;
2612
- output += `- **Fixable:** ${jsonResult.fixableFindings}\n`;
2613
- output += `- **Estimated time:** ${jsonResult.estimatedDuration}\n\n`;
2614
-
2615
- output += `## Fix Packs\n\n`;
2616
- for (const pack of jsonResult.packs || []) {
2617
- const risk = pack.estimatedRisk === "high" ? "🔴" : pack.estimatedRisk === "medium" ? "🟡" : "🟢";
2618
- output += `### ${risk} ${pack.name}\n`;
2619
- output += `- Issues: ${pack.findings.length}\n`;
2620
- output += `- Files: ${pack.impactedFiles.join(", ")}\n\n`;
2621
- }
2622
-
2623
- output += `\n💡 Run \`vibecheck.autopilot_apply\` to apply these fixes.\n`;
2624
- return this.success(output);
2625
- }
2626
-
2627
- if (runAutopilot) {
2628
- const result = await runAutopilot({
2629
- projectPath,
2630
- mode: "plan",
2631
- profile: args?.profile || "ship",
2632
- maxFixes: args?.maxFixes || 10,
2633
- });
2634
-
2635
- output += `## Scan Results\n\n`;
2636
- output += `- **Total findings:** ${result.totalFindings}\n`;
2637
- output += `- **Fixable:** ${result.fixableFindings}\n`;
2638
- output += `- **Estimated time:** ${result.estimatedDuration}\n\n`;
2639
-
2640
- output += `## Fix Packs\n\n`;
2641
- for (const pack of result.packs) {
2642
- const risk = pack.estimatedRisk === "high" ? "🔴" : pack.estimatedRisk === "medium" ? "🟡" : "🟢";
2643
- output += `### ${risk} ${pack.name}\n`;
2644
- output += `- Issues: ${pack.findings.length}\n`;
2645
- output += `- Files: ${pack.impactedFiles.join(", ")}\n\n`;
2646
- }
2647
-
2648
- output += `\n💡 Run \`vibecheck.autopilot_apply\` to apply these fixes.\n`;
2649
- }
2650
- } catch (err) {
2651
- output += `\n❌ Error: ${err.message}\n`;
2652
- }
2653
-
2654
- return this.success(output);
2655
- }
2656
-
2657
- // ============================================================================
2658
- // AUTOPILOT APPLY - Apply fixes (PRO tier required)
2659
- // ============================================================================
2660
- async handleAutopilotApply(projectPath, args) {
2661
- // Check tier access (PRO tier required - aligned with CLI entitlements-v2.js)
2662
- const access = await getFeatureAccessStatus("vibecheck.fix", args?.apiKey, args);
2663
- if (!access.hasAccess) {
2664
- return {
2665
- content: [{
2666
- type: "text",
2667
- text: `🚫 UPGRADE REQUIRED\n\n${access.reason}\n\nCurrent tier: ${access.tier}\nUpgrade at: ${access.upgradeUrl}`
2668
- }],
2669
- isError: true
2670
- };
2671
- }
2672
-
2673
- let output = "# 🔧 vibecheck Autopilot Apply\n\n";
2674
- output += `**Path:** ${projectPath}\n`;
2675
- output += `**Profile:** ${args?.profile || "ship"}\n`;
2676
- output += `**Dry Run:** ${args?.dryRun ? "Yes" : "No"}\n\n`;
2677
-
2678
- // Build CLI arguments array (secure)
2679
- const cliArgs = [
2680
- "apply",
2681
- "--profile", args?.profile || "ship",
2682
- "--max-fixes", String(args?.maxFixes || 10),
2683
- "--json"
2684
- ];
2685
- if (args?.verify === false) cliArgs.push("--no-verify");
2686
- if (args?.dryRun) cliArgs.push("--dry-run");
2687
-
2688
- try {
2689
- const result = await this.runCLI("autopilot", cliArgs, projectPath, {
2690
- timeout: CONFIG.TIMEOUTS.AUTOPILOT,
2691
- });
2692
-
2693
- const jsonResult = JSON.parse(result.stdout);
2694
-
2695
- output += `## Results\n\n`;
2696
- output += `- **Packs attempted:** ${jsonResult.packsAttempted}\n`;
2697
- output += `- **Packs succeeded:** ${jsonResult.packsSucceeded}\n`;
2698
- output += `- **Packs failed:** ${jsonResult.packsFailed}\n`;
2699
- output += `- **Fixes applied:** ${jsonResult.appliedFixes?.filter(f => f.success).length || 0}\n`;
2700
- output += `- **Duration:** ${jsonResult.duration}ms\n\n`;
2701
-
2702
- if (jsonResult.verification) {
2703
- output += `## Verification\n\n`;
2704
- output += `- TypeScript: ${jsonResult.verification.typecheck?.passed ? "✅" : "❌"}\n`;
2705
- output += `- Build: ${jsonResult.verification.build?.passed ? "✅" : "⏭️"}\n`;
2706
- output += `- Overall: ${jsonResult.verification.passed ? "✅ PASSED" : "❌ FAILED"}\n\n`;
2707
- }
2708
-
2709
- output += `**Remaining findings:** ${jsonResult.remainingFindings}\n`;
2710
- output += `**New scan verdict:** ${jsonResult.newScanVerdict}\n`;
2711
- } catch (err) {
2712
- output += `\n❌ Error: ${err.message}\n`;
2713
- }
2714
-
2715
- return this.success(output);
2716
- }
2717
-
2718
- // ============================================================================
2719
- // BADGE - Generate ship badge (PRO tier required)
2720
- // ============================================================================
2721
- async handleBadge(projectPath, args) {
2722
- // Check tier access (PRO tier required - aligned with CLI entitlements-v2.js)
2723
- const access = await getFeatureAccessStatus("vibecheck.badge", args?.apiKey, args);
2724
- if (!access.hasAccess) {
2725
- return {
2726
- content: [{
2727
- type: "text",
2728
- text: JSON.stringify({
2729
- ok: false,
2730
- error: access.error || {
2731
- code: 'NOT_ENTITLED',
2732
- message: 'Requires PRO',
2733
- userAction: 'Open billing',
2734
- retryable: false,
2735
- },
2736
- }, null, 2)
2737
- }],
2738
- isError: true
2739
- };
2740
- }
2741
-
2742
- const format = args?.format || "svg";
2743
-
2744
- let output = "# 🏅 vibecheck Badge\n\n";
2745
-
2746
- // Build CLI arguments array (secure)
2747
- const cliArgs = ["--format", format];
2748
- if (args?.style) cliArgs.push("--style", args.style);
2749
-
2750
- try {
2751
- const result = await this.runCLI("badge", cliArgs, projectPath);
2752
-
2753
- output += this.stripAnsi(result.stdout);
2754
-
2755
- // Read the badge file
2756
- const badgePath = path.join(projectPath, CONFIG.OUTPUT_DIR, "badges", `badge.${format}`);
2757
- try {
2758
- const badge = await fs.readFile(badgePath, "utf-8");
2759
- if (format === "md") {
2760
- output += "\n**Markdown:**\n```\n" + badge + "\n```\n";
2761
- } else if (format === "html") {
2762
- output += "\n**HTML:**\n```html\n" + badge + "\n```\n";
2763
- } else {
2764
- output += `\n**Badge saved to:** ${badgePath}\n`;
2765
- }
2766
- } catch {}
2767
- } catch (err) {
2768
- output += `\n⚠️ Badge generation failed: ${err.message}\n`;
2769
- }
2770
-
2771
- return this.success(output);
2772
- }
2773
-
2774
- // ============================================================================
2775
- // CONTEXT - AI Rules Generator
2776
- // ============================================================================
2777
- async handleContext(projectPath, args) {
2778
- const platform = args?.platform || "all";
2779
-
2780
- let output = "# 🧠 vibecheck Context Generator\n\n";
2781
- output += `**Project:** ${path.basename(projectPath)}\n`;
2782
- output += `**Platform:** ${platform}\n\n`;
2783
-
2784
- // Build CLI arguments array (secure)
2785
- const cliArgs = [];
2786
- if (platform !== "all") cliArgs.push(`--platform=${platform}`);
2787
-
2788
- try {
2789
- await this.runCLI("context", cliArgs, projectPath, { skipAuth: false });
2790
-
2791
- output += "## ✅ Context Generated\n\n";
2792
- output += "Your AI coding assistants now have full project awareness.\n\n";
2793
-
2794
- output += "### Generated Files\n\n";
2795
-
2796
- if (platform === "all" || platform === "cursor") {
2797
- output += "**Cursor:**\n";
2798
- output += "- `.cursorrules` - Main rules file\n";
2799
- output += "- `.cursor/rules/*.mdc` - Modular rules\n\n";
2800
- }
2801
-
2802
- if (platform === "all" || platform === "windsurf") {
2803
- output += "**Windsurf:**\n";
2804
- output += "- `.windsurf/rules/*.md` - Cascade rules\n\n";
2805
- }
2806
-
2807
- if (platform === "all" || platform === "copilot") {
2808
- output += "**GitHub Copilot:**\n";
2809
- output += "- `.github/copilot-instructions.md`\n\n";
2810
- }
2811
-
2812
- output += "**Universal (MCP):**\n";
2813
- output += `- \`${CONFIG.OUTPUT_DIR}/context.json\` - Full context\n`;
2814
- output += `- \`${CONFIG.OUTPUT_DIR}/project-map.json\` - Project analysis\n\n`;
2815
-
2816
- output += "### What Your AI Now Knows\n\n";
2817
- output += "- Project architecture and structure\n";
2818
- output += "- API routes and endpoints\n";
2819
- output += "- Components and data models\n";
2820
- output += "- Coding conventions and patterns\n";
2821
- output += "- Dependencies and tech stack\n\n";
2822
-
2823
- output += "> **Tip:** Regenerate after major codebase changes with `vibecheck context`\n";
2824
- } catch (err) {
2825
- output += `\n⚠️ Context generation failed: ${err.message}\n`;
2826
- }
2827
-
2828
- output += "\n---\n_Context Enhanced by vibecheck AI_\n";
2829
- return this.success(output);
2830
- }
2831
-
2832
- // ============================================================================
2833
- // STATUS
2834
- // ============================================================================
2835
- async handleStatus(projectPath, args) {
2836
- let output = "# 📊 vibecheck Status\n\n";
2837
-
2838
- output += "## Server\n\n";
2839
- output += `- **Version:** ${VERSION}\n`;
2840
- output += `- **Node:** ${process.version}\n`;
2841
- output += `- **Platform:** ${process.platform}\n\n`;
2842
-
2843
- output += "## Project\n\n";
2844
- output += `- **Path:** ${projectPath}\n`;
2845
-
2846
- // Config
2847
- const configPaths = [
2848
- path.join(projectPath, "vibecheck.config.json"),
2849
- path.join(projectPath, ".vibecheckrc"),
2850
- ];
2851
- let hasConfig = false;
2852
- for (const p of configPaths) {
2853
- try {
2854
- await fs.access(p);
2855
- hasConfig = true;
2856
- output += `- **Config:** ✅ Found (${path.basename(p)})\n`;
2857
- break;
2858
- } catch {}
2859
- }
2860
- if (!hasConfig) {
2861
- output += "- **Config:** ⚠️ Not found (run `vibecheck init`)\n";
2862
- }
2863
-
2864
- // Last scan
2865
- const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
2866
- try {
2867
- const summary = JSON.parse(await fs.readFile(summaryPath, "utf-8"));
2868
- output += `- **Last Scan:** ${summary.timestamp}\n`;
2869
- output += `- **Last Score:** ${summary.score}/100 (${summary.grade})\n`;
2870
- output += `- **Last Verdict:** ${summary.canShip ? "✅ SHIP" : "🚫 NO-SHIP"}\n`;
2871
- } catch {
2872
- output += "- **Last Scan:** None\n";
2873
- }
2874
-
2875
- output += "\n## Available Tools\n\n";
2876
- output += "| Tool | Description |\n|------|-------------|\n";
2877
- for (const tool of TOOLS) {
2878
- output += `| \`${tool.name}\` | ${tool.description.split("—")[0].trim()} |\n`;
2879
- }
2880
-
2881
- output += "\n---\n_Vibecheck v" + VERSION + " — https://vibecheckai.dev_\n";
2882
-
2883
- return this.success(output);
2884
- }
2885
-
2886
- // ============================================================================
2887
- // RUN - with graceful shutdown handling
2888
- // ============================================================================
2889
- async run() {
2890
- const transport = new StdioServerTransport();
2891
-
2892
- // ========================================================================
2893
- // HARDENING: Graceful shutdown handling
2894
- // ========================================================================
2895
- const shutdown = async (signal) => {
2896
- console.error(`\n[MCP] Received ${signal}, shutting down gracefully...`);
2897
- try {
2898
- // Clear rate limit state to prevent memory leaks
2899
- rateLimitState.calls = [];
2900
-
2901
- // Close server connection
2902
- await this.server.close();
2903
- console.error('[MCP] Server closed successfully');
2904
- } catch (err) {
2905
- console.error(`[MCP] Error during shutdown: ${err.message}`);
2906
- }
2907
- process.exit(0);
2908
- };
2909
-
2910
- // Handle termination signals
2911
- process.on('SIGINT', () => shutdown('SIGINT'));
2912
- process.on('SIGTERM', () => shutdown('SIGTERM'));
2913
-
2914
- // Handle uncaught errors gracefully
2915
- process.on('uncaughtException', (err) => {
2916
- console.error(`[MCP] Uncaught exception: ${err.message}`);
2917
- console.error(err.stack);
2918
- // Don't exit - try to keep running
2919
- });
2920
-
2921
- process.on('unhandledRejection', (reason, promise) => {
2922
- console.error(`[MCP] Unhandled rejection at:`, promise);
2923
- console.error(`[MCP] Reason:`, reason);
2924
- // Don't exit - try to keep running
2925
- });
2926
-
2927
- await this.server.connect(transport);
2928
- console.error(`vibecheck MCP Server v${VERSION} running on stdio (hardened)`);
2929
- }
2930
- }
2931
-
2932
- // ============================================================================
2933
- // MAIN - with error handling
2934
- // ============================================================================
2935
- const server = new VibecheckMCP();
2936
- server.run().catch((err) => {
2937
- console.error(`[MCP] Fatal error starting server: ${err.message}`);
2938
- console.error(err.stack);
2939
- process.exit(1);
2940
- });