@vibecheckai/cli 3.1.8 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/bin/registry.js +106 -116
  2. package/bin/runners/context/generators/mcp.js +18 -0
  3. package/bin/runners/context/index.js +72 -4
  4. package/bin/runners/context/proof-context.js +293 -1
  5. package/bin/runners/context/security-scanner.js +311 -73
  6. package/bin/runners/lib/analyzers.js +607 -20
  7. package/bin/runners/lib/detectors-v2.js +172 -15
  8. package/bin/runners/lib/entitlements-v2.js +48 -1
  9. package/bin/runners/lib/evidence-pack.js +678 -0
  10. package/bin/runners/lib/html-proof-report.js +913 -0
  11. package/bin/runners/lib/missions/plan.js +231 -41
  12. package/bin/runners/lib/missions/templates.js +125 -0
  13. package/bin/runners/lib/scan-output.js +492 -253
  14. package/bin/runners/lib/ship-output.js +901 -641
  15. package/bin/runners/runCheckpoint.js +44 -3
  16. package/bin/runners/runContext.d.ts +4 -0
  17. package/bin/runners/runDoctor.js +10 -2
  18. package/bin/runners/runFix.js +51 -341
  19. package/bin/runners/runInit.js +11 -0
  20. package/bin/runners/runPolish.d.ts +4 -0
  21. package/bin/runners/runPolish.js +608 -29
  22. package/bin/runners/runProve.js +210 -25
  23. package/bin/runners/runReality.js +846 -101
  24. package/bin/runners/runScan.js +238 -4
  25. package/bin/runners/runShip.js +19 -3
  26. package/bin/runners/runWatch.js +14 -1
  27. package/bin/vibecheck.js +32 -2
  28. package/mcp-server/consolidated-tools.js +408 -42
  29. package/mcp-server/index.js +152 -15
  30. package/mcp-server/proof-tools.js +571 -0
  31. package/mcp-server/tier-auth.js +22 -19
  32. package/mcp-server/tools-v3.js +744 -0
  33. package/mcp-server/truth-firewall-tools.js +190 -4
  34. package/package.json +3 -1
  35. package/bin/runners/runInstall.js +0 -281
  36. package/bin/runners/runLabs.js +0 -341
@@ -103,6 +103,14 @@ function parseArgs(args) {
103
103
  noBanner: globalFlags.noBanner || false,
104
104
  ci: globalFlags.ci || false,
105
105
  quiet: globalFlags.quiet || false,
106
+ // Allowlist subcommand
107
+ allowlist: null, // null = not using allowlist, or 'list' | 'add' | 'remove' | 'check'
108
+ allowlistId: null,
109
+ allowlistPattern: null,
110
+ allowlistReason: null,
111
+ allowlistScope: 'global',
112
+ allowlistFile: null,
113
+ allowlistExpires: null,
106
114
  };
107
115
 
108
116
  // Parse command-specific args from cleanArgs
@@ -118,6 +126,22 @@ function parseArgs(args) {
118
126
  else if (arg === '--no-save') opts.save = false;
119
127
  else if (arg === '--path' || arg === '-p') opts.path = cleanArgs[++i] || process.cwd();
120
128
  else if (arg.startsWith('--path=')) opts.path = arg.split('=')[1];
129
+ // Allowlist subcommand support
130
+ else if (arg === '--allowlist') {
131
+ const nextArg = cleanArgs[i + 1];
132
+ if (nextArg && !nextArg.startsWith('-')) {
133
+ opts.allowlist = nextArg;
134
+ i++;
135
+ } else {
136
+ opts.allowlist = 'list'; // Default to list
137
+ }
138
+ }
139
+ else if (arg === '--id' && opts.allowlist) opts.allowlistId = cleanArgs[++i];
140
+ else if (arg === '--pattern' && opts.allowlist) opts.allowlistPattern = cleanArgs[++i];
141
+ else if (arg === '--reason' && opts.allowlist) opts.allowlistReason = cleanArgs[++i];
142
+ else if (arg === '--scope' && opts.allowlist) opts.allowlistScope = cleanArgs[++i];
143
+ else if (arg === '--file' && opts.allowlist) opts.allowlistFile = cleanArgs[++i];
144
+ else if (arg === '--expires' && opts.allowlist) opts.allowlistExpires = cleanArgs[++i];
121
145
  else if (!arg.startsWith('-')) opts.path = path.resolve(arg);
122
146
  }
123
147
 
@@ -142,6 +166,17 @@ function printHelp(showBanner = true) {
142
166
  ${ansi.bold}Fix Mode:${ansi.reset}
143
167
  ${colors.accent}--autofix, -f${ansi.reset} Apply safe fixes + generate AI missions ${ansi.rgb(0, 200, 255)}[STARTER]${ansi.reset}
144
168
 
169
+ ${ansi.bold}Allowlist (suppress false positives):${ansi.reset}
170
+ ${colors.accent}--allowlist list${ansi.reset} Show all allowlist entries
171
+ ${colors.accent}--allowlist add${ansi.reset} Add entry to allowlist
172
+ ${ansi.dim}--id <finding-id>${ansi.reset} Finding ID to allowlist
173
+ ${ansi.dim}--pattern <regex>${ansi.reset} Pattern to match
174
+ ${ansi.dim}--reason <text>${ansi.reset} Reason (required)
175
+ ${ansi.dim}--scope <global|file|line>${ansi.reset} Scope (default: global)
176
+ ${ansi.dim}--expires <days>${ansi.reset} Auto-expire after N days
177
+ ${colors.accent}--allowlist remove --id <id>${ansi.reset} Remove entry from allowlist
178
+ ${colors.accent}--allowlist check --id <id>${ansi.reset} Check if finding is allowlisted
179
+
145
180
  ${ansi.bold}Options:${ansi.reset}
146
181
  ${colors.accent}--url, -u${ansi.reset} Base URL for reality testing (e.g., http://localhost:3000)
147
182
  ${colors.accent}--verbose, -v${ansi.reset} Show detailed progress
@@ -157,8 +192,11 @@ function printHelp(showBanner = true) {
157
192
  ${ansi.dim}# Scan + autofix with missions${ansi.reset}
158
193
  vibecheck scan --autofix
159
194
 
160
- ${ansi.dim}# CI/CD scan with manifest verification${ansi.reset}
161
- vibecheck scan --truth
195
+ ${ansi.dim}# Add false positive to allowlist${ansi.reset}
196
+ vibecheck scan --allowlist add --id R_DEAD_abc123 --reason "Known toggle"
197
+
198
+ ${ansi.dim}# List allowlist entries${ansi.reset}
199
+ vibecheck scan --allowlist list
162
200
 
163
201
  ${ansi.dim}# Full proof with Playwright${ansi.reset}
164
202
  vibecheck scan --reality --url http://localhost:3000
@@ -169,6 +207,172 @@ function printHelp(showBanner = true) {
169
207
  `);
170
208
  }
171
209
 
210
+ // ═══════════════════════════════════════════════════════════════════════════════
211
+ // ALLOWLIST MANAGEMENT (integrated from runAllowlist)
212
+ // ═══════════════════════════════════════════════════════════════════════════════
213
+
214
+ async function handleAllowlistCommand(opts) {
215
+ const root = opts.path || process.cwd();
216
+
217
+ // Load evidence-pack module for allowlist functions
218
+ let evidencePack;
219
+ try {
220
+ evidencePack = require("./lib/evidence-pack");
221
+ } catch (e) {
222
+ console.error(`${colors.error}✗${ansi.reset} Failed to load allowlist module: ${e.message}`);
223
+ return 1;
224
+ }
225
+
226
+ const action = opts.allowlist;
227
+
228
+ try {
229
+ switch (action) {
230
+ case "list": {
231
+ const allowlist = evidencePack.loadAllowlist(root);
232
+
233
+ if (opts.json) {
234
+ console.log(JSON.stringify(allowlist, null, 2));
235
+ return 0;
236
+ }
237
+
238
+ if (!opts.quiet) {
239
+ console.log(`\n 📋 ${ansi.bold}Allowlist Entries${ansi.reset}\n`);
240
+ }
241
+
242
+ if (!allowlist.entries || allowlist.entries.length === 0) {
243
+ console.log(` ${ansi.dim}No entries in allowlist.${ansi.reset}\n`);
244
+ console.log(` ${ansi.dim}Add entries with: vibecheck scan --allowlist add --id <id> --reason "..."${ansi.reset}\n`);
245
+ return 0;
246
+ }
247
+
248
+ for (const entry of allowlist.entries) {
249
+ const expireStr = entry.expiresAt
250
+ ? `${ansi.dim}expires ${new Date(entry.expiresAt).toLocaleDateString()}${ansi.reset}`
251
+ : '';
252
+ const scopeStr = entry.scope !== 'global'
253
+ ? `${colors.accent}[${entry.scope}]${ansi.reset} `
254
+ : '';
255
+
256
+ console.log(` ${colors.success}✓${ansi.reset} ${ansi.bold}${entry.id}${ansi.reset} ${scopeStr}${expireStr}`);
257
+
258
+ if (entry.findingId) {
259
+ console.log(` ${ansi.dim}Finding:${ansi.reset} ${entry.findingId}`);
260
+ }
261
+ if (entry.pattern) {
262
+ console.log(` ${ansi.dim}Pattern:${ansi.reset} ${entry.pattern}`);
263
+ }
264
+ console.log(` ${ansi.dim}Reason:${ansi.reset} ${entry.reason || 'No reason provided'}`);
265
+ console.log(` ${ansi.dim}Added:${ansi.reset} ${entry.addedAt} by ${entry.addedBy || 'user'}`);
266
+ console.log();
267
+ }
268
+
269
+ console.log(` ${ansi.dim}Total: ${allowlist.entries.length} entries${ansi.reset}\n`);
270
+ return 0;
271
+ }
272
+
273
+ case "add": {
274
+ if (!opts.allowlistId && !opts.allowlistPattern) {
275
+ console.error(`\n ${colors.error}✗${ansi.reset} Either --id or --pattern is required\n`);
276
+ return 1;
277
+ }
278
+
279
+ if (!opts.allowlistReason) {
280
+ console.error(`\n ${colors.error}✗${ansi.reset} --reason is required\n`);
281
+ return 1;
282
+ }
283
+
284
+ const entry = {
285
+ findingId: opts.allowlistId,
286
+ pattern: opts.allowlistPattern,
287
+ reason: opts.allowlistReason,
288
+ scope: opts.allowlistScope,
289
+ addedBy: 'cli'
290
+ };
291
+
292
+ if (opts.allowlistFile) entry.file = opts.allowlistFile;
293
+ if (opts.allowlistExpires) {
294
+ const days = parseInt(opts.allowlistExpires, 10);
295
+ entry.expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();
296
+ }
297
+
298
+ const added = evidencePack.addToAllowlist(root, entry);
299
+
300
+ if (opts.json) {
301
+ console.log(JSON.stringify({ added }, null, 2));
302
+ return 0;
303
+ }
304
+
305
+ console.log(`\n ${colors.success}+${ansi.reset} Added allowlist entry: ${ansi.bold}${added.id}${ansi.reset}\n`);
306
+ return 0;
307
+ }
308
+
309
+ case "remove": {
310
+ if (!opts.allowlistId) {
311
+ console.error(`\n ${colors.error}✗${ansi.reset} --id is required for remove\n`);
312
+ return 1;
313
+ }
314
+
315
+ const allowlist = evidencePack.loadAllowlist(root);
316
+ const before = allowlist.entries.length;
317
+ allowlist.entries = allowlist.entries.filter(e => e.id !== opts.allowlistId && e.findingId !== opts.allowlistId);
318
+ const removed = before - allowlist.entries.length;
319
+
320
+ if (removed > 0) {
321
+ evidencePack.saveAllowlist(root, allowlist);
322
+ }
323
+
324
+ if (opts.json) {
325
+ console.log(JSON.stringify({ removed, remaining: allowlist.entries.length }, null, 2));
326
+ return 0;
327
+ }
328
+
329
+ if (removed > 0) {
330
+ console.log(`\n ${colors.success}-${ansi.reset} Removed ${removed} entry from allowlist\n`);
331
+ } else {
332
+ console.log(`\n ${colors.warning}⚠${ansi.reset} Entry not found: ${opts.allowlistId}\n`);
333
+ }
334
+ return 0;
335
+ }
336
+
337
+ case "check": {
338
+ if (!opts.allowlistId) {
339
+ console.error(`\n ${colors.error}✗${ansi.reset} --id is required for check\n`);
340
+ return 1;
341
+ }
342
+
343
+ const allowlist = evidencePack.loadAllowlist(root);
344
+ const result = evidencePack.isAllowlisted({ id: opts.allowlistId }, allowlist);
345
+
346
+ if (opts.json) {
347
+ console.log(JSON.stringify(result, null, 2));
348
+ return result.allowed ? 0 : 1;
349
+ }
350
+
351
+ if (result.allowed) {
352
+ console.log(`\n ${colors.success}✓${ansi.reset} ${ansi.bold}Allowlisted${ansi.reset}`);
353
+ console.log(` ${ansi.dim}Reason:${ansi.reset} ${result.reason}`);
354
+ console.log(` ${ansi.dim}Entry:${ansi.reset} ${result.entry?.id}\n`);
355
+ } else {
356
+ console.log(`\n ${colors.warning}⚠${ansi.reset} ${ansi.bold}Not allowlisted${ansi.reset}\n`);
357
+ }
358
+ return result.allowed ? 0 : 1;
359
+ }
360
+
361
+ default:
362
+ console.error(`\n ${colors.error}✗${ansi.reset} Unknown allowlist action: ${action}\n`);
363
+ console.log(` ${ansi.dim}Available: list, add, remove, check${ansi.reset}\n`);
364
+ return 1;
365
+ }
366
+ } catch (error) {
367
+ if (opts.json) {
368
+ console.log(JSON.stringify({ error: error.message }, null, 2));
369
+ } else {
370
+ console.error(`\n ${colors.error}✗${ansi.reset} ${error.message}\n`);
371
+ }
372
+ return 1;
373
+ }
374
+ }
375
+
172
376
  // ═══════════════════════════════════════════════════════════════════════════════
173
377
  // AUTOFIX & MISSION GENERATION
174
378
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -453,6 +657,11 @@ async function runScan(args) {
453
657
  return 0;
454
658
  }
455
659
 
660
+ // Handle --allowlist subcommand
661
+ if (opts.allowlist) {
662
+ return await handleAllowlistCommand(opts);
663
+ }
664
+
456
665
  // Entitlement check (graceful offline handling)
457
666
  try {
458
667
  await enforceLimit('scans');
@@ -531,19 +740,44 @@ async function runScan(args) {
531
740
  // Fallback to JS-based scanner using truth.js and analyzers.js
532
741
  useFallbackScanner = true;
533
742
  const { buildTruthpack } = require('./lib/truth');
534
- const { findMissingRoutes, findEnvGaps, findFakeSuccess, findGhostAuth } = require('./lib/analyzers');
743
+ const {
744
+ findMissingRoutes,
745
+ findEnvGaps,
746
+ findFakeSuccess,
747
+ findGhostAuth,
748
+ findMockData,
749
+ findTodoFixme,
750
+ findConsoleLogs,
751
+ findHardcodedSecrets,
752
+ findDeadCode,
753
+ findDeprecatedApis,
754
+ findEmptyCatch,
755
+ findUnsafeRegex,
756
+ } = require('./lib/analyzers');
535
757
 
536
758
  scanRouteIntegrity = async function({ projectPath, layers, baseUrl, verbose }) {
537
759
  // Build truthpack for route analysis
538
760
  const truthpack = await buildTruthpack({ repoRoot: projectPath });
539
761
 
540
- // Run analyzers
762
+ // Run ALL analyzers for comprehensive scanning
541
763
  const findings = [];
764
+
765
+ // Core analyzers (route integrity, env, auth)
542
766
  findings.push(...findMissingRoutes(truthpack));
543
767
  findings.push(...findEnvGaps(truthpack));
544
768
  findings.push(...findFakeSuccess(projectPath));
545
769
  findings.push(...findGhostAuth(truthpack, projectPath));
546
770
 
771
+ // Code quality analyzers (MOCK, TODO, console.log, etc.)
772
+ findings.push(...findMockData(projectPath));
773
+ findings.push(...findTodoFixme(projectPath));
774
+ findings.push(...findConsoleLogs(projectPath));
775
+ findings.push(...findHardcodedSecrets(projectPath));
776
+ findings.push(...findDeadCode(projectPath));
777
+ findings.push(...findDeprecatedApis(projectPath));
778
+ findings.push(...findEmptyCatch(projectPath));
779
+ findings.push(...findUnsafeRegex(projectPath));
780
+
547
781
  // Convert to scan format matching TypeScript scanner output
548
782
  const shipBlockers = findings.map((f, i) => ({
549
783
  id: f.id || `finding-${i}`,
@@ -1044,23 +1044,39 @@ async function runShip(args, context = {}) {
1044
1044
  duration: Date.now() - executionStart,
1045
1045
  };
1046
1046
 
1047
+ // Get current tier for output formatting
1048
+ const currentTier = context?.authInfo?.access?.tier || getCurrentTier() || "free";
1049
+
1047
1050
  console.log(formatShipOutput(result, {
1048
1051
  verbose: opts.verbose,
1049
1052
  showFix: opts.fix,
1050
1053
  showBadge: opts.badge,
1051
1054
  outputDir,
1052
1055
  projectPath,
1056
+ tier: currentTier,
1057
+ isVerified: opts.withRuntime || false, // Reality testing = verified
1053
1058
  }));
1054
1059
 
1055
- // Badge file generation
1060
+ // Badge file generation (STARTER+ only)
1056
1061
  if (opts.badge) {
1057
- const { data: badgeData } = renderBadgeOutput(projectPath, verdict, results.score);
1062
+ const isVerified = opts.withRuntime && (currentTier === 'pro' || currentTier === 'compliance');
1063
+ const { data: badgeData } = renderBadgeOutput(projectPath, verdict, results.score, {
1064
+ tier: currentTier,
1065
+ isVerified
1066
+ });
1058
1067
 
1059
1068
  // Save badge info
1060
1069
  fs.mkdirSync(outputDir, { recursive: true });
1061
1070
  fs.writeFileSync(
1062
1071
  path.join(outputDir, 'badge.json'),
1063
- JSON.stringify({ ...badgeData, verdict, score: results.score, generatedAt: new Date().toISOString() }, null, 2)
1072
+ JSON.stringify({
1073
+ ...badgeData,
1074
+ verdict,
1075
+ score: results.score,
1076
+ tier: currentTier,
1077
+ isVerified,
1078
+ generatedAt: new Date().toISOString()
1079
+ }, null, 2)
1064
1080
  );
1065
1081
  }
1066
1082
 
@@ -203,7 +203,13 @@ function printTopFindings(findings, max = 5) {
203
203
  .filter(f => f.severity === "BLOCK" || f.severity === "WARN")
204
204
  .slice(0, max);
205
205
 
206
- if (!top.length) return;
206
+ if (!top.length) {
207
+ // Show success upsell for PRO
208
+ console.log(` ${colors.shipGreen}✓${c.reset} ${c.bold}No issues detected!${c.reset}`);
209
+ console.log(` ${c.dim}Want a verified badge? Try${c.reset} ${colors.cyan}vibecheck prove${c.reset} ${c.dim}(PRO)${c.reset}`);
210
+ console.log();
211
+ return;
212
+ }
207
213
 
208
214
  console.log(` ${c.bold}Top findings:${c.reset}`);
209
215
  for (const f of top) {
@@ -211,6 +217,13 @@ function printTopFindings(findings, max = 5) {
211
217
  const title = f.title?.length > 50 ? f.title.slice(0, 47) + '...' : f.title;
212
218
  console.log(` ${icon}${c.reset} ${title}`);
213
219
  }
220
+
221
+ // Show upsell based on findings
222
+ const blockers = findings.filter(f => f.severity === "BLOCK").length;
223
+ if (blockers > 0) {
224
+ console.log();
225
+ console.log(` ${colors.cyan}→${c.reset} ${c.dim}Fix these with${c.reset} ${colors.cyan}vibecheck fix${c.reset} ${c.dim}(STARTER)${c.reset}`);
226
+ }
214
227
  console.log();
215
228
  }
216
229
 
package/bin/vibecheck.js CHANGED
@@ -888,6 +888,14 @@ ${c.green}QUICK START - The 5-Step Journey${c.reset}
888
888
  4. ${c.bold}Prove${c.reset} ${c.cyan}vibecheck prove${c.reset} ${c.magenta}[PRO]${c.reset}
889
889
  5. ${c.bold}Ship${c.reset} ${c.cyan}vibecheck ship${c.reset}
890
890
 
891
+ ${c.bold}GLOBAL OPTIONS${c.reset}
892
+
893
+ ${c.cyan}--offline, --local${c.reset} Run in offline mode (no API, unlimited local scans)
894
+ ${c.cyan}--json${c.reset} Output as JSON
895
+ ${c.cyan}--quiet, -q${c.reset} Suppress output
896
+ ${c.cyan}--verbose${c.reset} Show detailed output
897
+ ${c.cyan}--path, -p <dir>${c.reset} Run in specified directory
898
+
891
899
  ${c.bold}SHELL COMPLETIONS${c.reset}
892
900
 
893
901
  ${c.cyan}vibecheck completion bash${c.reset} ${c.dim}# Add to ~/.bashrc${c.reset}
@@ -939,6 +947,7 @@ function parseGlobalFlags(rawArgs) {
939
947
  debug: false,
940
948
  strict: false,
941
949
  noBanner: false,
950
+ offline: false,
942
951
  path: process.cwd(),
943
952
  output: null,
944
953
  };
@@ -980,6 +989,10 @@ function parseGlobalFlags(rawArgs) {
980
989
  case "--no-banner":
981
990
  flags.noBanner = true;
982
991
  break;
992
+ case "--offline":
993
+ case "--local":
994
+ flags.offline = true;
995
+ break;
983
996
  case "--path":
984
997
  case "-p":
985
998
  flags.path = rawArgs[++i] || process.cwd();
@@ -1077,6 +1090,7 @@ async function main() {
1077
1090
  }
1078
1091
  if (globalFlags.verbose) cmdArgs.push("--verbose");
1079
1092
  if (globalFlags.strict) cmdArgs.push("--strict");
1093
+ if (globalFlags.offline) cmdArgs.push("--offline");
1080
1094
 
1081
1095
  // Unknown command
1082
1096
  if (!COMMANDS[cmd]) {
@@ -1103,8 +1117,13 @@ async function main() {
1103
1117
  const cmdDef = COMMANDS[cmd];
1104
1118
  let authInfo = { key: null };
1105
1119
 
1106
- // Auth check (unless skipAuth)
1107
- if (!cmdDef.skipAuth) {
1120
+ // Check for offline mode (via flag or env var)
1121
+ const isOffline = globalFlags.offline ||
1122
+ process.env.VIBECHECK_OFFLINE === '1' ||
1123
+ process.env.VIBECHECK_LOCAL === '1';
1124
+
1125
+ // Auth check (unless skipAuth or offline mode)
1126
+ if (!cmdDef.skipAuth && !isOffline) {
1108
1127
  const auth = getAuthModule();
1109
1128
  const { key } = auth.getApiKey();
1110
1129
  authInfo.key = key;
@@ -1136,6 +1155,17 @@ async function main() {
1136
1155
  }
1137
1156
 
1138
1157
  authInfo.access = access;
1158
+ } else if (isOffline) {
1159
+ // Offline mode - provide basic access info
1160
+ if (!config.quiet && !config.noBanner) {
1161
+ console.log(`${c.cyan}${sym.arrowRight} OFFLINE${c.reset} ${c.dim}mode - local scanning without API${c.reset}`);
1162
+ }
1163
+ authInfo.access = {
1164
+ allowed: true,
1165
+ tier: 'free',
1166
+ caps: { maxFiles: 100, maxDepth: 3 },
1167
+ offline: true
1168
+ };
1139
1169
  }
1140
1170
 
1141
1171
  // Update state