@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.
- package/README.md +1 -1
- package/bin/runners/context/generators/cursor-enhanced.js +99 -13
- package/bin/runners/lib/unified-cli-output.js +16 -0
- package/bin/runners/runCI.js +353 -0
- package/bin/runners/runCheckpoint.js +2 -2
- package/mcp-server/.eslintrc.json +24 -0
- package/mcp-server/README.md +425 -135
- package/mcp-server/SPEC.md +583 -0
- package/mcp-server/configs/README.md +172 -0
- package/mcp-server/configs/claude-desktop-pro.json +31 -0
- package/mcp-server/configs/claude-desktop-with-workspace.json +25 -0
- package/mcp-server/configs/claude-desktop.json +19 -0
- package/mcp-server/configs/cursor-mcp.json +21 -0
- package/mcp-server/configs/windsurf-mcp.json +17 -0
- package/mcp-server/mcp-config.example.json +9 -0
- package/mcp-server/package.json +49 -34
- package/mcp-server/src/cli.ts +185 -0
- package/mcp-server/src/index.ts +85 -0
- package/mcp-server/src/server.ts +1933 -0
- package/mcp-server/src/services/cache-service.ts +466 -0
- package/mcp-server/src/services/cli-service.ts +345 -0
- package/mcp-server/src/services/context-manager.ts +717 -0
- package/mcp-server/src/services/firewall-service.ts +662 -0
- package/mcp-server/src/services/git-service.ts +671 -0
- package/mcp-server/src/services/index.ts +52 -0
- package/mcp-server/src/services/prompt-builder-service.ts +1031 -0
- package/mcp-server/src/services/session-service.ts +550 -0
- package/mcp-server/src/services/tier-service.ts +470 -0
- package/mcp-server/src/types.ts +351 -0
- package/mcp-server/tsconfig.json +16 -27
- package/package.json +6 -6
- package/mcp-server/.guardrail/audit/audit.log.jsonl +0 -2
- package/mcp-server/.specs/architecture.mdc +0 -90
- package/mcp-server/.specs/security.mdc +0 -30
- package/mcp-server/HARDENING_SUMMARY.md +0 -299
- package/mcp-server/agent-checkpoint.js +0 -364
- package/mcp-server/agent-firewall-interceptor.js +0 -500
- package/mcp-server/architect-tools.js +0 -707
- package/mcp-server/audit-mcp.js +0 -206
- package/mcp-server/authority-tools.js +0 -569
- package/mcp-server/codebase-architect-tools.js +0 -838
- package/mcp-server/conductor/conflict-resolver.js +0 -588
- package/mcp-server/conductor/execution-planner.js +0 -544
- package/mcp-server/conductor/index.js +0 -377
- package/mcp-server/conductor/lock-manager.js +0 -615
- package/mcp-server/conductor/request-queue.js +0 -550
- package/mcp-server/conductor/session-manager.js +0 -500
- package/mcp-server/conductor/tools.js +0 -510
- package/mcp-server/consolidated-tools.js +0 -1170
- package/mcp-server/deprecation-middleware.js +0 -282
- package/mcp-server/handlers/index.ts +0 -15
- package/mcp-server/handlers/tool-handler.ts +0 -593
- package/mcp-server/hygiene-tools.js +0 -428
- package/mcp-server/index-v1.js +0 -698
- package/mcp-server/index.js +0 -2940
- package/mcp-server/intelligence-tools.js +0 -664
- package/mcp-server/intent-drift-tools.js +0 -873
- package/mcp-server/intent-firewall-interceptor.js +0 -529
- package/mcp-server/lib/api-client.cjs +0 -13
- package/mcp-server/lib/cache-wrapper.cjs +0 -383
- package/mcp-server/lib/error-envelope.js +0 -138
- package/mcp-server/lib/executor.ts +0 -499
- package/mcp-server/lib/index.ts +0 -29
- package/mcp-server/lib/logger.cjs +0 -30
- package/mcp-server/lib/rate-limiter.js +0 -166
- package/mcp-server/lib/sandbox.test.ts +0 -519
- package/mcp-server/lib/sandbox.ts +0 -395
- package/mcp-server/lib/types.ts +0 -267
- package/mcp-server/logger.js +0 -173
- package/mcp-server/manifest.json +0 -473
- package/mcp-server/mdc-generator.js +0 -298
- package/mcp-server/premium-tools.js +0 -1275
- package/mcp-server/proof-tools.js +0 -571
- package/mcp-server/registry/tool-registry.js +0 -586
- package/mcp-server/registry/tools.json +0 -619
- package/mcp-server/registry.test.ts +0 -340
- package/mcp-server/test-mcp.js +0 -108
- package/mcp-server/test-tools.js +0 -36
- package/mcp-server/tests/tier-gating.test.js +0 -297
- package/mcp-server/tier-auth.js +0 -767
- package/mcp-server/tools/index.js +0 -72
- package/mcp-server/tools-reorganized.ts +0 -244
- package/mcp-server/tools-v3.js +0 -1004
- package/mcp-server/truth-context.js +0 -622
- package/mcp-server/truth-firewall-tools.js +0 -2183
- package/mcp-server/vibecheck-2.0-tools.js +0 -761
- package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
- package/mcp-server/vibecheck-tools.js +0 -1075
package/mcp-server/index.js
DELETED
|
@@ -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
|
-
});
|