@vibecheckai/cli 3.1.6 → 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 (56) hide show
  1. package/README.md +27 -32
  2. package/bin/registry.js +208 -343
  3. package/bin/runners/context/generators/mcp.js +18 -0
  4. package/bin/runners/context/index.js +72 -4
  5. package/bin/runners/context/proof-context.js +293 -1
  6. package/bin/runners/context/security-scanner.js +311 -73
  7. package/bin/runners/lib/analyzers.js +607 -20
  8. package/bin/runners/lib/detectors-v2.js +172 -15
  9. package/bin/runners/lib/entitlements-v2.js +48 -1
  10. package/bin/runners/lib/evidence-pack.js +678 -0
  11. package/bin/runners/lib/html-proof-report.js +913 -0
  12. package/bin/runners/lib/missions/plan.js +231 -41
  13. package/bin/runners/lib/missions/templates.js +125 -0
  14. package/bin/runners/lib/scan-output.js +492 -253
  15. package/bin/runners/lib/ship-output.js +901 -641
  16. package/bin/runners/runCheckpoint.js +44 -3
  17. package/bin/runners/runContext.d.ts +4 -0
  18. package/bin/runners/runContext.js +2 -3
  19. package/bin/runners/runDoctor.js +11 -4
  20. package/bin/runners/runFix.js +51 -341
  21. package/bin/runners/runInit.js +37 -20
  22. package/bin/runners/runPolish.d.ts +4 -0
  23. package/bin/runners/runPolish.js +608 -29
  24. package/bin/runners/runProve.js +210 -25
  25. package/bin/runners/runReality.js +861 -107
  26. package/bin/runners/runScan.js +238 -4
  27. package/bin/runners/runShip.js +19 -3
  28. package/bin/runners/runWatch.js +25 -5
  29. package/bin/vibecheck.js +35 -47
  30. package/mcp-server/consolidated-tools.js +408 -42
  31. package/mcp-server/index.js +152 -15
  32. package/mcp-server/package.json +1 -1
  33. package/mcp-server/proof-tools.js +571 -0
  34. package/mcp-server/tier-auth.js +22 -19
  35. package/mcp-server/tools-v3.js +744 -0
  36. package/mcp-server/truth-firewall-tools.js +190 -4
  37. package/package.json +3 -1
  38. package/bin/runners/runBadge.js +0 -916
  39. package/bin/runners/runContracts.js +0 -105
  40. package/bin/runners/runCtx.js +0 -680
  41. package/bin/runners/runCtxDiff.js +0 -301
  42. package/bin/runners/runCtxGuard.js +0 -176
  43. package/bin/runners/runCtxSync.js +0 -116
  44. package/bin/runners/runExport.js +0 -93
  45. package/bin/runners/runGraph.js +0 -454
  46. package/bin/runners/runInstall.js +0 -273
  47. package/bin/runners/runLabs.js +0 -341
  48. package/bin/runners/runLaunch.js +0 -181
  49. package/bin/runners/runPR.js +0 -255
  50. package/bin/runners/runPermissions.js +0 -310
  51. package/bin/runners/runPreflight.js +0 -580
  52. package/bin/runners/runReplay.js +0 -499
  53. package/bin/runners/runSecurity.js +0 -92
  54. package/bin/runners/runShare.js +0 -212
  55. package/bin/runners/runStatus.js +0 -102
  56. package/bin/runners/runVerify.js +0 -272
@@ -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
 
@@ -14,6 +14,7 @@
14
14
 
15
15
  const fs = require("fs");
16
16
  const path = require("path");
17
+ const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
17
18
  const { shipCore } = require("./runShip");
18
19
 
19
20
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -202,7 +203,13 @@ function printTopFindings(findings, max = 5) {
202
203
  .filter(f => f.severity === "BLOCK" || f.severity === "WARN")
203
204
  .slice(0, max);
204
205
 
205
- 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
+ }
206
213
 
207
214
  console.log(` ${c.bold}Top findings:${c.reset}`);
208
215
  for (const f of top) {
@@ -210,6 +217,13 @@ function printTopFindings(findings, max = 5) {
210
217
  const title = f.title?.length > 50 ? f.title.slice(0, 47) + '...' : f.title;
211
218
  console.log(` ${icon}${c.reset} ${title}`);
212
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
+ }
213
227
  console.log();
214
228
  }
215
229
 
@@ -273,8 +287,10 @@ function watchDirectory(dir, callback) {
273
287
  };
274
288
  }
275
289
 
276
- function printHelp() {
277
- console.log(BANNER_FULL);
290
+ function printHelp(opts = {}) {
291
+ if (shouldShowBanner(opts)) {
292
+ console.log(BANNER_FULL);
293
+ }
278
294
  console.log(`
279
295
  ${c.bold}Usage:${c.reset} vibecheck watch [options]
280
296
 
@@ -314,9 +330,12 @@ function printHelp() {
314
330
 
315
331
  async function runWatch(argsOrOpts = {}) {
316
332
  // Handle array args from CLI
333
+ let globalOpts = { noBanner: false, json: false, quiet: false, ci: false };
317
334
  if (Array.isArray(argsOrOpts)) {
318
- if (argsOrOpts.includes("--help") || argsOrOpts.includes("-h")) {
319
- printHelp();
335
+ const { flags } = parseGlobalFlags(argsOrOpts);
336
+ globalOpts = { ...globalOpts, ...flags };
337
+ if (globalOpts.help) {
338
+ printHelp(globalOpts);
320
339
  return 0;
321
340
  }
322
341
  const getArg = (flags) => {
@@ -331,6 +350,7 @@ async function runWatch(argsOrOpts = {}) {
331
350
  fastifyEntry: getArg(["--fastify-entry"]),
332
351
  debounceMs: parseInt(getArg(["--debounce"]) || "500", 10),
333
352
  clearScreen: !argsOrOpts.includes("--no-clear"),
353
+ ...globalOpts,
334
354
  };
335
355
  }
336
356
 
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,16 +1090,14 @@ 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]) {
1083
1097
  const suggestions = findSimilarCommands(cmd, ALL_COMMANDS);
1084
1098
  console.log(`\n${c.red}${sym.error}${c.reset} Unknown command: ${c.yellow}${cmd}${c.reset}`);
1085
1099
 
1086
- if (cmd === "replay" || cmd === "record") {
1087
- console.log(`\n${c.dim}replay is a PRO feature for session recording.${c.reset}`);
1088
- console.log(`${c.dim}Free alternative:${c.reset} ${c.cyan}vibecheck reality${c.reset} ${c.dim}(one-time runtime proof)${c.reset}`);
1089
- } else if (suggestions.length > 0) {
1100
+ if (suggestions.length > 0) {
1090
1101
  console.log(`\n${c.dim}Did you mean:${c.reset}`);
1091
1102
  suggestions.forEach((s) => {
1092
1103
  const actual = ALIAS_MAP[s] || s;
@@ -1106,8 +1117,13 @@ async function main() {
1106
1117
  const cmdDef = COMMANDS[cmd];
1107
1118
  let authInfo = { key: null };
1108
1119
 
1109
- // Auth check (unless skipAuth)
1110
- 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) {
1111
1127
  const auth = getAuthModule();
1112
1128
  const { key } = auth.getApiKey();
1113
1129
  authInfo.key = key;
@@ -1139,6 +1155,17 @@ async function main() {
1139
1155
  }
1140
1156
 
1141
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
+ };
1142
1169
  }
1143
1170
 
1144
1171
  // Update state
@@ -1173,47 +1200,8 @@ async function main() {
1173
1200
  runStart,
1174
1201
  };
1175
1202
 
1176
- // Execute command
1177
- switch (cmd) {
1178
- case "prove":
1179
- case "reality":
1180
- case "watch":
1181
- case "ship":
1182
- case "runtime":
1183
- case "export":
1184
- case "security":
1185
- case "install":
1186
- case "pr":
1187
- case "share":
1188
- exitCode = await runner(cmdArgs, context);
1189
- break;
1190
-
1191
- case "ctx":
1192
- case "truthpack":
1193
- if (cmdArgs[0] === "sync") {
1194
- const { runCtxSync } = require("./runners/runCtxSync");
1195
- exitCode = await runCtxSync({ ...context, fastifyEntry: getArgValue(cmdArgs, ["--fastify-entry"]) });
1196
- } else if (cmdArgs[0] === "guard") {
1197
- const { runCtxGuard } = require("./runners/runCtxGuard");
1198
- exitCode = await runCtxGuard.main(cmdArgs.slice(1));
1199
- } else if (cmdArgs[0] === "diff") {
1200
- const { main: ctxDiffMain } = require("./runners/runCtxDiff");
1201
- exitCode = await ctxDiffMain(cmdArgs.slice(1));
1202
- } else if (cmdArgs[0] === "search") {
1203
- const { runContext } = require("./runners/runContext");
1204
- exitCode = await runContext(["--search", ...cmdArgs.slice(1)]);
1205
- } else {
1206
- exitCode = await runner(cmdArgs, context);
1207
- }
1208
- break;
1209
-
1210
- case "status":
1211
- exitCode = await runner({ ...context, json: cmdArgs.includes("--json") });
1212
- break;
1213
-
1214
- default:
1215
- exitCode = await runner(cmdArgs);
1216
- }
1203
+ // Execute command - all commands use consistent runner signature
1204
+ exitCode = await runner(cmdArgs, context);
1217
1205
  } catch (error) {
1218
1206
  console.error(formatError(error, config));
1219
1207
  exitCode = 1;