@vibecheckai/cli 3.2.5 → 3.3.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 (197) hide show
  1. package/bin/.generated +25 -25
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/registry.js +192 -5
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
  6. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  7. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  8. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  9. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  10. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  11. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  12. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  13. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  14. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  15. package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
  16. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
  17. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
  18. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  19. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  20. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  21. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  22. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  23. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  24. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  25. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  26. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  27. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  28. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  29. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  30. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  31. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  32. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  33. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  34. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  35. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  36. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  37. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  38. package/bin/runners/lib/analyzers.js +81 -18
  39. package/bin/runners/lib/api-client.js +269 -0
  40. package/bin/runners/lib/auth-truth.js +193 -193
  41. package/bin/runners/lib/authority-badge.js +425 -0
  42. package/bin/runners/lib/backup.js +62 -62
  43. package/bin/runners/lib/billing.js +107 -107
  44. package/bin/runners/lib/claims.js +118 -118
  45. package/bin/runners/lib/cli-output.js +7 -1
  46. package/bin/runners/lib/cli-ui.js +540 -540
  47. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  48. package/bin/runners/lib/contracts/env-contract.js +181 -181
  49. package/bin/runners/lib/contracts/external-contract.js +206 -206
  50. package/bin/runners/lib/contracts/guard.js +168 -168
  51. package/bin/runners/lib/contracts/index.js +89 -89
  52. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  53. package/bin/runners/lib/contracts/route-contract.js +199 -199
  54. package/bin/runners/lib/contracts.js +804 -804
  55. package/bin/runners/lib/detect.js +89 -89
  56. package/bin/runners/lib/doctor/autofix.js +254 -254
  57. package/bin/runners/lib/doctor/index.js +37 -37
  58. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  59. package/bin/runners/lib/doctor/modules/index.js +46 -46
  60. package/bin/runners/lib/doctor/modules/network.js +250 -250
  61. package/bin/runners/lib/doctor/modules/project.js +312 -312
  62. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  63. package/bin/runners/lib/doctor/modules/security.js +348 -348
  64. package/bin/runners/lib/doctor/modules/system.js +213 -213
  65. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  66. package/bin/runners/lib/doctor/reporter.js +262 -262
  67. package/bin/runners/lib/doctor/service.js +262 -262
  68. package/bin/runners/lib/doctor/types.js +113 -113
  69. package/bin/runners/lib/doctor/ui.js +263 -263
  70. package/bin/runners/lib/doctor-v2.js +608 -608
  71. package/bin/runners/lib/drift.js +425 -425
  72. package/bin/runners/lib/enforcement.js +72 -72
  73. package/bin/runners/lib/enterprise-detect.js +603 -603
  74. package/bin/runners/lib/enterprise-init.js +942 -942
  75. package/bin/runners/lib/env-resolver.js +417 -417
  76. package/bin/runners/lib/env-template.js +66 -66
  77. package/bin/runners/lib/env.js +189 -189
  78. package/bin/runners/lib/error-handler.js +16 -9
  79. package/bin/runners/lib/exit-codes.js +275 -0
  80. package/bin/runners/lib/extractors/client-calls.js +990 -990
  81. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  82. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  83. package/bin/runners/lib/extractors/index.js +363 -363
  84. package/bin/runners/lib/extractors/next-routes.js +524 -524
  85. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  86. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  87. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  88. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  89. package/bin/runners/lib/findings-schema.js +281 -281
  90. package/bin/runners/lib/firewall-prompt.js +50 -50
  91. package/bin/runners/lib/global-flags.js +37 -0
  92. package/bin/runners/lib/graph/graph-builder.js +265 -265
  93. package/bin/runners/lib/graph/html-renderer.js +413 -413
  94. package/bin/runners/lib/graph/index.js +32 -32
  95. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  96. package/bin/runners/lib/graph/static-extractor.js +518 -518
  97. package/bin/runners/lib/help-formatter.js +413 -0
  98. package/bin/runners/lib/html-report.js +650 -650
  99. package/bin/runners/lib/llm.js +75 -75
  100. package/bin/runners/lib/logger.js +38 -0
  101. package/bin/runners/lib/meter.js +61 -61
  102. package/bin/runners/lib/missions/evidence.js +126 -126
  103. package/bin/runners/lib/patch.js +40 -40
  104. package/bin/runners/lib/permissions/auth-model.js +213 -213
  105. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  106. package/bin/runners/lib/permissions/index.js +45 -45
  107. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  108. package/bin/runners/lib/pkgjson.js +28 -28
  109. package/bin/runners/lib/policy.js +295 -295
  110. package/bin/runners/lib/preflight.js +142 -142
  111. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  112. package/bin/runners/lib/reality/index.js +318 -318
  113. package/bin/runners/lib/reality/request-hashing.js +416 -416
  114. package/bin/runners/lib/reality/request-mapper.js +453 -453
  115. package/bin/runners/lib/reality/safety-rails.js +463 -463
  116. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  117. package/bin/runners/lib/reality/toast-detector.js +393 -393
  118. package/bin/runners/lib/reality-findings.js +84 -84
  119. package/bin/runners/lib/receipts.js +179 -179
  120. package/bin/runners/lib/redact.js +29 -29
  121. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  122. package/bin/runners/lib/replay/index.js +263 -263
  123. package/bin/runners/lib/replay/player.js +348 -348
  124. package/bin/runners/lib/replay/recorder.js +331 -331
  125. package/bin/runners/lib/report.js +135 -135
  126. package/bin/runners/lib/route-detection.js +1140 -1140
  127. package/bin/runners/lib/sandbox/index.js +59 -59
  128. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  129. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  130. package/bin/runners/lib/sandbox/worktree.js +174 -174
  131. package/bin/runners/lib/schema-validator.js +350 -350
  132. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  133. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  134. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  135. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  136. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  137. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  138. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  139. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  140. package/bin/runners/lib/schemas/validator.js +438 -438
  141. package/bin/runners/lib/score-history.js +282 -282
  142. package/bin/runners/lib/share-pack.js +239 -239
  143. package/bin/runners/lib/snippets.js +67 -67
  144. package/bin/runners/lib/unified-cli-output.js +604 -0
  145. package/bin/runners/lib/upsell.js +658 -510
  146. package/bin/runners/lib/usage.js +153 -153
  147. package/bin/runners/lib/validate-patch.js +156 -156
  148. package/bin/runners/lib/verdict-engine.js +628 -628
  149. package/bin/runners/reality/engine.js +917 -917
  150. package/bin/runners/reality/flows.js +122 -122
  151. package/bin/runners/reality/report.js +378 -378
  152. package/bin/runners/reality/session.js +193 -193
  153. package/bin/runners/runAgent.d.ts +5 -0
  154. package/bin/runners/runApprove.js +1200 -0
  155. package/bin/runners/runAuth.js +324 -95
  156. package/bin/runners/runCheckpoint.js +39 -21
  157. package/bin/runners/runClassify.js +859 -0
  158. package/bin/runners/runContext.js +136 -24
  159. package/bin/runners/runDoctor.js +108 -68
  160. package/bin/runners/runFirewall.d.ts +5 -0
  161. package/bin/runners/runFirewallHook.d.ts +5 -0
  162. package/bin/runners/runFix.js +6 -5
  163. package/bin/runners/runGuard.js +262 -168
  164. package/bin/runners/runInit.js +3 -2
  165. package/bin/runners/runMcp.js +130 -52
  166. package/bin/runners/runPolish.js +43 -20
  167. package/bin/runners/runProve.js +1 -2
  168. package/bin/runners/runReport.js +3 -2
  169. package/bin/runners/runScan.js +145 -44
  170. package/bin/runners/runShip.js +3 -4
  171. package/bin/runners/runTruth.d.ts +5 -0
  172. package/bin/runners/runValidate.js +19 -2
  173. package/bin/runners/runWatch.js +104 -53
  174. package/bin/vibecheck.js +106 -19
  175. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  176. package/mcp-server/agent-firewall-interceptor.js +367 -31
  177. package/mcp-server/authority-tools.js +569 -0
  178. package/mcp-server/conductor/conflict-resolver.js +588 -0
  179. package/mcp-server/conductor/execution-planner.js +544 -0
  180. package/mcp-server/conductor/index.js +377 -0
  181. package/mcp-server/conductor/lock-manager.js +615 -0
  182. package/mcp-server/conductor/request-queue.js +550 -0
  183. package/mcp-server/conductor/session-manager.js +500 -0
  184. package/mcp-server/conductor/tools.js +510 -0
  185. package/mcp-server/index.js +1199 -208
  186. package/mcp-server/lib/api-client.cjs +305 -0
  187. package/mcp-server/lib/logger.cjs +30 -0
  188. package/mcp-server/logger.js +173 -0
  189. package/mcp-server/package.json +2 -2
  190. package/mcp-server/premium-tools.js +2 -2
  191. package/mcp-server/tier-auth.js +351 -136
  192. package/mcp-server/tools/index.js +72 -72
  193. package/mcp-server/truth-firewall-tools.js +145 -15
  194. package/mcp-server/vibecheck-tools.js +2 -2
  195. package/package.json +2 -3
  196. package/mcp-server/index.old.js +0 -4137
  197. package/mcp-server/package-lock.json +0 -165
@@ -1,510 +1,658 @@
1
- /**
2
- * Upsell Copy Module - Central copy generator for tier upgrades
3
- *
4
- * This is the SINGLE SOURCE OF TRUTH for all upsell/upgrade messaging.
5
- * All upgrade nudges flow through these functions.
6
- *
7
- * Rules:
8
- * - Blunt, confident, minimal tone
9
- * - No cringe language ("please upgrade")
10
- * - Every upsell includes a free path if one exists
11
- * - No web links besides https://vibecheckai.dev/pricing
12
- *
13
- * @module bin/runners/lib/upsell
14
- */
15
-
16
- "use strict";
17
-
18
- // ═══════════════════════════════════════════════════════════════════════════════
19
- // ANSI STYLING
20
- // ═══════════════════════════════════════════════════════════════════════════════
21
- const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
22
-
23
- const c = SUPPORTS_COLOR ? {
24
- reset: "\x1b[0m",
25
- bold: "\x1b[1m",
26
- dim: "\x1b[2m",
27
- red: "\x1b[31m",
28
- green: "\x1b[32m",
29
- yellow: "\x1b[33m",
30
- cyan: "\x1b[36m",
31
- magenta: "\x1b[35m",
32
- white: "\x1b[37m",
33
- gray: "\x1b[90m",
34
- } : {
35
- reset: "", bold: "", dim: "", red: "", green: "", yellow: "",
36
- cyan: "", magenta: "", white: "", gray: "",
37
- };
38
-
39
- const sym = {
40
- lock: "🔒",
41
- arrow: "→",
42
- warning: "⚠",
43
- check: "✓",
44
- cross: "✗",
45
- star: "★",
46
- rocket: "🚀",
47
- badge: "🏆",
48
- lightning: "⚡",
49
- };
50
-
51
- // ═══════════════════════════════════════════════════════════════════════════════
52
- // TIER CONFIGURATION
53
- // ═══════════════════════════════════════════════════════════════════════════════
54
- const TIER_LABELS = {
55
- free: "FREE",
56
- starter: "STARTER",
57
- pro: "PRO",
58
- complete: "COMPLETE",
59
- };
60
-
61
- const TIER_COLORS = {
62
- free: c.green,
63
- starter: c.cyan,
64
- pro: c.magenta,
65
- complete: c.yellow,
66
- };
67
-
68
- const PRICING_URL = "https://vibecheckai.dev";
69
-
70
- // ═══════════════════════════════════════════════════════════════════════════════
71
- // DENIAL COPY - Command-specific reasons and alternatives
72
- // ═══════════════════════════════════════════════════════════════════════════════
73
- const DENIAL_COPY = {
74
- ship: {
75
- feature: "release gate",
76
- why: "Full ship analysis with runtime verification",
77
- freeAlt: "vibecheck scan",
78
- freeAltDesc: "static analysis",
79
- },
80
- prove: {
81
- feature: "proof loop",
82
- why: "Complete ctx → reality → ship → fix cycle",
83
- freeAlt: "vibecheck ship",
84
- freeAltDesc: "static verdict",
85
- },
86
- permissions: {
87
- feature: "auth boundary / IDOR detection",
88
- why: "Deep authorization matrix and IDOR analysis",
89
- freeAlt: "vibecheck reality --verify-auth",
90
- freeAltDesc: "basic auth boundary check",
91
- },
92
- "fix.apply_patches": {
93
- feature: "patch generator / PR-ready diff",
94
- why: "Apply LLM-generated patches automatically",
95
- freeAlt: "vibecheck fix",
96
- freeAltDesc: "plan-only mode",
97
- },
98
- fix: {
99
- feature: "auto-fix with patches",
100
- why: "Generate and apply fixes to your codebase",
101
- freeAlt: "vibecheck fix --plan-only",
102
- freeAltDesc: "view fix plan without applying",
103
- },
104
- badge: {
105
- feature: "status artifact",
106
- why: "Verified ship badge for README",
107
- freeAlt: "vibecheck report",
108
- freeAltDesc: "HTML/MD report",
109
- },
110
- gate: {
111
- feature: "CI/CD gate",
112
- why: "Block deploys on verification failures",
113
- freeAlt: "vibecheck ship --ci",
114
- freeAltDesc: "exit codes for scripts",
115
- },
116
- pr: {
117
- feature: "PR comment generator",
118
- why: "Auto-generated PR comments with findings",
119
- freeAlt: "vibecheck report --format md",
120
- freeAltDesc: "markdown report",
121
- },
122
- launch: {
123
- feature: "pre-launch checklist",
124
- why: "Guided launch readiness wizard",
125
- freeAlt: "vibecheck ship",
126
- freeAltDesc: "verdict check",
127
- },
128
- mcp: {
129
- feature: "MCP server for AI IDEs",
130
- why: "Real-time AI IDE integration",
131
- freeAlt: "vibecheck ctx",
132
- freeAltDesc: "generate truthpack manually",
133
- },
134
- share: {
135
- feature: "share pack generator",
136
- why: "Shareable proof bundle for PRs/docs",
137
- freeAlt: "vibecheck report",
138
- freeAltDesc: "local report",
139
- },
140
- "ai-test": {
141
- feature: "autonomous test generation",
142
- why: "AI-generated test coverage",
143
- freeAlt: "vibecheck scan",
144
- freeAltDesc: "static analysis",
145
- },
146
- replay: {
147
- feature: "session replay",
148
- why: "Record and replay user sessions",
149
- freeAlt: "vibecheck reality",
150
- freeAltDesc: "one-time runtime proof",
151
- },
152
- graph: {
153
- feature: "reality proof graph",
154
- why: "Visual dependency and proof graph",
155
- freeAlt: "vibecheck ctx",
156
- freeAltDesc: "truthpack",
157
- },
158
- };
159
-
160
- // ═══════════════════════════════════════════════════════════════════════════════
161
- // CAPS COPY - Downgrade mode descriptions
162
- // ═══════════════════════════════════════════════════════════════════════════════
163
- const CAPS_COPY = {
164
- "reality.preview": {
165
- short: "5 pages, no auth boundary",
166
- full: "Preview mode: 5 pages max, 20 clicks, no auth boundary verification",
167
- upgradeBenefit: "Unlimited pages + full auth boundary testing",
168
- },
169
- "fix.plan_only": {
170
- short: "plan-only, no apply",
171
- full: "Plan mode: generates fix missions but cannot apply patches",
172
- upgradeBenefit: "Apply patches automatically with --apply",
173
- },
174
- "report.html_md": {
175
- short: "HTML/MD only",
176
- full: "Basic formats: HTML and Markdown reports only",
177
- upgradeBenefit: "SARIF, CSV, and compliance pack exports",
178
- },
179
- "ship.static": {
180
- short: "static-only",
181
- full: "Static analysis only, no runtime verification",
182
- upgradeBenefit: "Full runtime + static verification",
183
- },
184
- "mcp.help_only": {
185
- short: "help and print-config only",
186
- full: "MCP server limited to help and config commands",
187
- upgradeBenefit: "Full MCP server with all tools",
188
- },
189
- };
190
-
191
- // ═══════════════════════════════════════════════════════════════════════════════
192
- // formatDenied() - Hard denial message
193
- // ═══════════════════════════════════════════════════════════════════════════════
194
- /**
195
- * Format a denial message for a command that requires a higher tier.
196
- *
197
- * @param {string} cmd - The command that was denied
198
- * @param {object} opts - Options
199
- * @param {string} opts.currentTier - User's current tier
200
- * @param {string} opts.requiredTier - Required tier for the command
201
- * @param {string} [opts.reason] - Additional context
202
- * @param {string} [opts.suggestedNext] - Suggested next command
203
- * @param {string} [opts.freeAlternative] - Free alternative command
204
- * @returns {string} Formatted denial message
205
- */
206
- function formatDenied(cmd, opts = {}) {
207
- const { currentTier = "free", requiredTier = "pro" } = opts;
208
-
209
- const copy = DENIAL_COPY[cmd] || {
210
- feature: cmd,
211
- why: `${cmd} command`,
212
- freeAlt: null,
213
- freeAltDesc: null,
214
- };
215
-
216
- const reqColor = TIER_COLORS[requiredTier] || c.yellow;
217
- const reqLabel = TIER_LABELS[requiredTier] || requiredTier.toUpperCase();
218
- const curLabel = TIER_LABELS[currentTier] || currentTier.toUpperCase();
219
-
220
- let msg = `
221
- ${c.red}${c.bold}${sym.lock} LOCKED${c.reset}
222
-
223
- ${c.bold}What:${c.reset} ${c.yellow}${cmd}${c.reset} ${c.dim}(${copy.feature})${c.reset}
224
- ${c.bold}Why:${c.reset} ${copy.why}
225
- ${c.bold}Requires:${c.reset} ${reqColor}${reqLabel}${c.reset} plan
226
- ${c.bold}You have:${c.reset} ${c.dim}${curLabel}${c.reset}
227
- `;
228
-
229
- // Free alternative
230
- if (copy.freeAlt) {
231
- msg += `
232
- ${c.green}${sym.arrow} Free path:${c.reset} ${c.cyan}${copy.freeAlt}${c.reset} ${c.dim}(${copy.freeAltDesc})${c.reset}`;
233
- }
234
-
235
- // Upgrade CTA
236
- msg += `
237
- ${c.bold}${sym.arrow} Upgrade:${c.reset} ${PRICING_URL}
238
- `;
239
-
240
- return msg;
241
- }
242
-
243
- // ═══════════════════════════════════════════════════════════════════════════════
244
- // formatDowngrade() - Caps/downgrade notice
245
- // ═══════════════════════════════════════════════════════════════════════════════
246
- /**
247
- * Format a downgrade notice for a command running in capped mode.
248
- *
249
- * @param {string} cmd - The command being run
250
- * @param {object} opts - Options
251
- * @param {string} opts.currentTier - User's current tier
252
- * @param {string} opts.effectiveMode - The downgraded mode being used
253
- * @param {object} [opts.caps] - Specific caps applied
254
- * @returns {string} Formatted downgrade notice
255
- */
256
- function formatDowngrade(cmd, opts = {}) {
257
- const { currentTier = "free", effectiveMode, caps } = opts;
258
-
259
- const copy = CAPS_COPY[effectiveMode] || {
260
- short: effectiveMode || "limited mode",
261
- full: `Running in ${effectiveMode || "limited"} mode`,
262
- upgradeBenefit: "Full access",
263
- };
264
-
265
- const curLabel = TIER_LABELS[currentTier] || currentTier.toUpperCase();
266
-
267
- // Single line notice for start of run
268
- let shortNotice = `${c.yellow}${sym.warning}${c.reset} Running in ${c.yellow}${curLabel}${c.reset} mode: ${c.dim}${copy.short}${c.reset}`;
269
-
270
- return shortNotice;
271
- }
272
-
273
- /**
274
- * Format a cap-hit notice when user reaches a limit during execution.
275
- *
276
- * @param {string} cmd - The command being run
277
- * @param {object} opts - Options
278
- * @param {string} opts.limitType - Type of limit hit (e.g., "pages", "clicks")
279
- * @param {number} opts.limitValue - The limit value
280
- * @param {string} opts.upgradeTier - Tier to upgrade to
281
- * @returns {string} Formatted cap-hit message
282
- */
283
- function formatCapHit(cmd, opts = {}) {
284
- const { limitType = "limit", limitValue, upgradeTier = "pro" } = opts;
285
-
286
- const tierColor = TIER_COLORS[upgradeTier] || c.magenta;
287
- const tierLabel = TIER_LABELS[upgradeTier] || upgradeTier.toUpperCase();
288
-
289
- return `${c.yellow}${sym.warning}${c.reset} Hit FREE ${limitType} cap (${limitValue}). Upgrade to ${tierColor}${tierLabel}${c.reset} ${sym.arrow} ${PRICING_URL}`;
290
- }
291
-
292
- // ═══════════════════════════════════════════════════════════════════════════════
293
- // formatEarnedUpsell() - End-of-run upsell
294
- // ═══════════════════════════════════════════════════════════════════════════════
295
- /**
296
- * Format an earned upsell shown at end of run.
297
- *
298
- * @param {object} opts - Options
299
- * @param {string} opts.cmd - The command that was run
300
- * @param {string} opts.verdict - The verdict (SHIP/WARN/BLOCK)
301
- * @param {Array} [opts.topIssues] - Top 3 issues to show
302
- * @param {string} [opts.withheldArtifact] - Artifact that was withheld (e.g., "badge")
303
- * @param {string} [opts.upgradeTier] - Tier to suggest upgrading to
304
- * @param {string} [opts.why] - Why this upsell is relevant
305
- * @returns {string} Formatted earned upsell message
306
- */
307
- function formatEarnedUpsell(opts = {}) {
308
- const {
309
- cmd,
310
- verdict,
311
- topIssues = [],
312
- withheldArtifact,
313
- upgradeTier = "pro",
314
- why,
315
- currentTier = "free",
316
- suggestedCmd,
317
- } = opts;
318
-
319
- const tierColor = TIER_COLORS[upgradeTier] || c.magenta;
320
- const tierLabel = TIER_LABELS[upgradeTier] || upgradeTier.toUpperCase();
321
-
322
- let msg = "";
323
-
324
- // Badge withheld case
325
- if (withheldArtifact === "badge") {
326
- msg += `
327
- ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
328
- ${c.yellow}${sym.badge} Badge withheld${c.reset} ${c.dim}(verdict: ${verdict})${c.reset}
329
- `;
330
-
331
- if (topIssues.length > 0) {
332
- msg += `\n${c.bold}Top blockers:${c.reset}\n`;
333
- topIssues.slice(0, 3).forEach((issue, i) => {
334
- const sev = issue.severity === "BLOCK" ? c.red : c.yellow;
335
- msg += ` ${c.dim}${i + 1}.${c.reset} ${sev}${issue.severity}${c.reset} ${issue.message || issue.id || issue.type}\n`;
336
- });
337
- }
338
-
339
- if (suggestedCmd) {
340
- msg += `\n${c.green}${sym.arrow} Fix:${c.reset} ${c.cyan}${suggestedCmd}${c.reset}`;
341
- }
342
-
343
- msg += `\n${c.dim}Badge unlocks when verdict = SHIP${c.reset}\n`;
344
- return msg;
345
- }
346
-
347
- // Fix plan-only case
348
- if (cmd === "fix" && withheldArtifact === "apply") {
349
- msg += `
350
- ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
351
- ${c.yellow}${sym.lightning} Fix plan generated${c.reset}
352
-
353
- Patches ready but ${c.yellow}--apply${c.reset} requires ${tierColor}${tierLabel}${c.reset} plan.
354
-
355
- ${c.green}${sym.arrow} Review:${c.reset} .vibecheck/missions/
356
- ${c.bold}${sym.arrow} Apply:${c.reset} Upgrade ${sym.arrow} ${PRICING_URL}
357
- `;
358
- return msg;
359
- }
360
-
361
- // Reality cap hit case
362
- if (cmd === "reality" && why === "cap_hit") {
363
- msg += `
364
- ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
365
- ${c.yellow}${sym.warning} Preview complete${c.reset}
366
-
367
- Crawled ${topIssues.length || 5} pages (FREE limit).
368
- ${tierColor}${tierLabel}${c.reset} unlocks unlimited pages + auth boundary testing.
369
-
370
- ${c.bold}${sym.arrow} Upgrade:${c.reset} ${PRICING_URL}
371
- `;
372
- return msg;
373
- }
374
-
375
- // Generic earned upsell
376
- if (why) {
377
- msg += `
378
- ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
379
- ${c.cyan}${sym.star} ${why}${c.reset}
380
-
381
- ${tierColor}${tierLabel}${c.reset} unlocks this feature.
382
- ${c.bold}${sym.arrow} Upgrade:${c.reset} ${PRICING_URL}
383
- `;
384
- }
385
-
386
- return msg;
387
- }
388
-
389
- // ═══════════════════════════════════════════════════════════════════════════════
390
- // formatBadgeWithheld() - Specific badge denial
391
- // ═══════════════════════════════════════════════════════════════════════════════
392
- /**
393
- * Format badge withheld message with top issues.
394
- *
395
- * @param {string} verdict - Current verdict
396
- * @param {Array} findings - All findings
397
- * @param {string} currentTier - User's tier
398
- * @returns {string} Formatted message
399
- */
400
- function formatBadgeWithheld(verdict, findings = [], currentTier = "free") {
401
- const blockers = findings.filter(f => f.severity === "BLOCK").slice(0, 3);
402
- const topIssues = blockers.length > 0 ? blockers : findings.slice(0, 3);
403
-
404
- let msg = `
405
- ${c.yellow}${sym.badge} Badge withheld${c.reset}
406
-
407
- ${c.bold}Verdict:${c.reset} ${verdict === "BLOCK" ? c.red : c.yellow}${verdict}${c.reset}
408
- ${c.dim}Badge requires SHIP verdict${c.reset}
409
- `;
410
-
411
- if (topIssues.length > 0) {
412
- msg += `\n ${c.bold}Fix these first:${c.reset}\n`;
413
- topIssues.forEach((issue, i) => {
414
- const sev = issue.severity === "BLOCK" ? c.red : c.yellow;
415
- const label = issue.id || issue.type || issue.ruleId || "issue";
416
- const desc = issue.message || issue.description || "";
417
- msg += ` ${c.dim}${i + 1}.${c.reset} ${sev}[${issue.severity}]${c.reset} ${label}${desc ? `: ${c.dim}${desc.slice(0, 50)}${c.reset}` : ""}\n`;
418
- });
419
- }
420
-
421
- // Suggest fix command if available
422
- const canFix = currentTier !== "free";
423
- if (canFix) {
424
- msg += `\n ${c.green}${sym.arrow} Run:${c.reset} ${c.cyan}vibecheck fix${c.reset}\n`;
425
- } else {
426
- msg += `\n ${c.green}${sym.arrow} Run:${c.reset} ${c.cyan}vibecheck fix --plan-only${c.reset} ${c.dim}(view fix plan)${c.reset}\n`;
427
- }
428
-
429
- return msg;
430
- }
431
-
432
- // ═══════════════════════════════════════════════════════════════════════════════
433
- // formatNextSteps() - Smart suggestions
434
- // ═══════════════════════════════════════════════════════════════════════════════
435
- /**
436
- * Format next step suggestions based on current command and result.
437
- *
438
- * @param {string} cmd - Command just run
439
- * @param {string} verdict - Result/verdict
440
- * @param {string} currentTier - User's tier
441
- * @returns {string} Next step suggestion
442
- */
443
- function formatNextSteps(cmd, verdict, currentTier = "free") {
444
- const steps = [];
445
-
446
- switch (cmd) {
447
- case "scan":
448
- if (currentTier === "free") {
449
- steps.push({ cmd: "vibecheck ship", desc: "get verdict" });
450
- } else {
451
- steps.push({ cmd: "vibecheck ship", desc: "get verdict" });
452
- steps.push({ cmd: "vibecheck prove --url <url>", desc: "full proof loop" });
453
- }
454
- break;
455
-
456
- case "ship":
457
- if (verdict === "SHIP") {
458
- if (currentTier !== "free") {
459
- steps.push({ cmd: "vibecheck badge", desc: "generate badge" });
460
- }
461
- steps.push({ cmd: "vibecheck report", desc: "export report" });
462
- } else {
463
- if (currentTier === "free") {
464
- steps.push({ cmd: "vibecheck fix --plan-only", desc: "view fix plan" });
465
- } else {
466
- steps.push({ cmd: "vibecheck fix", desc: "auto-fix issues" });
467
- }
468
- }
469
- break;
470
-
471
- case "fix":
472
- steps.push({ cmd: "vibecheck ship", desc: "verify fixes" });
473
- break;
474
-
475
- case "reality":
476
- steps.push({ cmd: "vibecheck ship", desc: "get verdict with runtime proof" });
477
- break;
478
- }
479
-
480
- if (steps.length === 0) return "";
481
-
482
- let msg = `\n${c.dim}Next:${c.reset} `;
483
- msg += steps.map(s => `${c.cyan}${s.cmd}${c.reset} ${c.dim}(${s.desc})${c.reset}`).join(" or ");
484
-
485
- return msg;
486
- }
487
-
488
- // ═══════════════════════════════════════════════════════════════════════════════
489
- // EXPORTS
490
- // ═══════════════════════════════════════════════════════════════════════════════
491
- module.exports = {
492
- // Core formatters
493
- formatDenied,
494
- formatDowngrade,
495
- formatCapHit,
496
- formatEarnedUpsell,
497
- formatBadgeWithheld,
498
- formatNextSteps,
499
-
500
- // Copy data (for testing/docs)
501
- DENIAL_COPY,
502
- CAPS_COPY,
503
- TIER_LABELS,
504
- TIER_COLORS,
505
- PRICING_URL,
506
-
507
- // Styling (for consistent use)
508
- c,
509
- sym,
510
- };
1
+ /**
2
+ * Upsell Copy Module - Central copy generator for tier upgrades
3
+ *
4
+ * This is the SINGLE SOURCE OF TRUTH for all upsell/upgrade messaging.
5
+ * All upgrade nudges flow through these functions.
6
+ *
7
+ * Rules:
8
+ * - Blunt, confident, minimal tone
9
+ * - No cringe language ("please upgrade")
10
+ * - Every upsell includes a free path if one exists
11
+ * - No web links besides https://vibecheckai.dev/pricing
12
+ *
13
+ * @module bin/runners/lib/upsell
14
+ */
15
+
16
+ "use strict";
17
+
18
+ // ═══════════════════════════════════════════════════════════════════════════════
19
+ // ANSI STYLING
20
+ // ═══════════════════════════════════════════════════════════════════════════════
21
+ const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
22
+
23
+ const c = SUPPORTS_COLOR ? {
24
+ reset: "\x1b[0m",
25
+ bold: "\x1b[1m",
26
+ dim: "\x1b[2m",
27
+ red: "\x1b[31m",
28
+ green: "\x1b[32m",
29
+ yellow: "\x1b[33m",
30
+ cyan: "\x1b[36m",
31
+ magenta: "\x1b[35m",
32
+ white: "\x1b[37m",
33
+ gray: "\x1b[90m",
34
+ } : {
35
+ reset: "", bold: "", dim: "", red: "", green: "", yellow: "",
36
+ cyan: "", magenta: "", white: "", gray: "",
37
+ };
38
+
39
+ const sym = {
40
+ lock: "🔒",
41
+ arrow: "→",
42
+ warning: "⚠",
43
+ check: "✓",
44
+ cross: "✗",
45
+ star: "★",
46
+ rocket: "🚀",
47
+ badge: "🏆",
48
+ lightning: "⚡",
49
+ };
50
+
51
+ // ═══════════════════════════════════════════════════════════════════════════════
52
+ // TIER CONFIGURATION
53
+ // ═══════════════════════════════════════════════════════════════════════════════
54
+ const TIER_LABELS = {
55
+ free: "FREE",
56
+ starter: "STARTER",
57
+ pro: "PRO",
58
+ complete: "COMPLETE",
59
+ };
60
+
61
+ const TIER_COLORS = {
62
+ free: c.green,
63
+ starter: c.cyan,
64
+ pro: c.magenta,
65
+ complete: c.yellow,
66
+ };
67
+
68
+ const PRICING_URL = "https://vibecheckai.dev";
69
+
70
+ // ═══════════════════════════════════════════════════════════════════════════════
71
+ // DENIAL COPY - Command-specific reasons and alternatives
72
+ // ═══════════════════════════════════════════════════════════════════════════════
73
+ const DENIAL_COPY = {
74
+ ship: {
75
+ feature: "release gate",
76
+ why: "Full ship analysis with runtime verification",
77
+ freeAlt: "vibecheck scan",
78
+ freeAltDesc: "static analysis",
79
+ },
80
+ prove: {
81
+ feature: "proof loop",
82
+ why: "Complete ctx → reality → ship → fix cycle",
83
+ freeAlt: "vibecheck ship",
84
+ freeAltDesc: "static verdict",
85
+ },
86
+ permissions: {
87
+ feature: "auth boundary / IDOR detection",
88
+ why: "Deep authorization matrix and IDOR analysis",
89
+ freeAlt: "vibecheck reality --verify-auth",
90
+ freeAltDesc: "basic auth boundary check",
91
+ },
92
+ "fix.apply_patches": {
93
+ feature: "patch generator / PR-ready diff",
94
+ why: "Apply LLM-generated patches automatically",
95
+ freeAlt: "vibecheck fix",
96
+ freeAltDesc: "plan-only mode",
97
+ },
98
+ fix: {
99
+ feature: "auto-fix with patches",
100
+ why: "Generate and apply fixes to your codebase",
101
+ freeAlt: "vibecheck fix --plan-only",
102
+ freeAltDesc: "view fix plan without applying",
103
+ },
104
+ badge: {
105
+ feature: "status artifact",
106
+ why: "Verified ship badge for README",
107
+ freeAlt: "vibecheck report",
108
+ freeAltDesc: "HTML/MD report",
109
+ },
110
+ gate: {
111
+ feature: "CI/CD gate",
112
+ why: "Block deploys on verification failures",
113
+ freeAlt: "vibecheck ship --ci",
114
+ freeAltDesc: "exit codes for scripts",
115
+ },
116
+ pr: {
117
+ feature: "PR comment generator",
118
+ why: "Auto-generated PR comments with findings",
119
+ freeAlt: "vibecheck report --format md",
120
+ freeAltDesc: "markdown report",
121
+ },
122
+ launch: {
123
+ feature: "pre-launch checklist",
124
+ why: "Guided launch readiness wizard",
125
+ freeAlt: "vibecheck ship",
126
+ freeAltDesc: "verdict check",
127
+ },
128
+ mcp: {
129
+ feature: "MCP server for AI IDEs",
130
+ why: "Real-time AI IDE integration",
131
+ freeAlt: "vibecheck ctx",
132
+ freeAltDesc: "generate truthpack manually",
133
+ },
134
+ share: {
135
+ feature: "share pack generator",
136
+ why: "Shareable proof bundle for PRs/docs",
137
+ freeAlt: "vibecheck report",
138
+ freeAltDesc: "local report",
139
+ },
140
+ "ai-test": {
141
+ feature: "autonomous test generation",
142
+ why: "AI-generated test coverage",
143
+ freeAlt: "vibecheck scan",
144
+ freeAltDesc: "static analysis",
145
+ },
146
+ replay: {
147
+ feature: "session replay",
148
+ why: "Record and replay user sessions",
149
+ freeAlt: "vibecheck reality",
150
+ freeAltDesc: "one-time runtime proof",
151
+ },
152
+ graph: {
153
+ feature: "reality proof graph",
154
+ why: "Visual dependency and proof graph",
155
+ freeAlt: "vibecheck ctx",
156
+ freeAltDesc: "truthpack",
157
+ },
158
+ };
159
+
160
+ // ═══════════════════════════════════════════════════════════════════════════════
161
+ // CAPS COPY - Downgrade mode descriptions
162
+ // ═══════════════════════════════════════════════════════════════════════════════
163
+ const CAPS_COPY = {
164
+ "reality.preview": {
165
+ short: "5 pages, no auth boundary",
166
+ full: "Preview mode: 5 pages max, 20 clicks, no auth boundary verification",
167
+ upgradeBenefit: "Unlimited pages + full auth boundary testing",
168
+ },
169
+ "fix.plan_only": {
170
+ short: "plan-only, no apply",
171
+ full: "Plan mode: generates fix missions but cannot apply patches",
172
+ upgradeBenefit: "Apply patches automatically with --apply",
173
+ },
174
+ "report.html_md": {
175
+ short: "HTML/MD only",
176
+ full: "Basic formats: HTML and Markdown reports only",
177
+ upgradeBenefit: "SARIF, CSV, and compliance pack exports",
178
+ },
179
+ "ship.static": {
180
+ short: "static-only",
181
+ full: "Static analysis only, no runtime verification",
182
+ upgradeBenefit: "Full runtime + static verification",
183
+ },
184
+ "mcp.help_only": {
185
+ short: "help and print-config only",
186
+ full: "MCP server limited to help and config commands",
187
+ upgradeBenefit: "Full MCP server with all tools",
188
+ },
189
+ };
190
+
191
+ // ═══════════════════════════════════════════════════════════════════════════════
192
+ // formatDenied() - Hard denial message
193
+ // ═══════════════════════════════════════════════════════════════════════════════
194
+ /**
195
+ * Format a denial message for a command that requires a higher tier.
196
+ *
197
+ * @param {string} cmd - The command that was denied
198
+ * @param {object} opts - Options
199
+ * @param {string} opts.currentTier - User's current tier
200
+ * @param {string} opts.requiredTier - Required tier for the command
201
+ * @param {string} [opts.reason] - Additional context
202
+ * @param {string} [opts.suggestedNext] - Suggested next command
203
+ * @param {string} [opts.freeAlternative] - Free alternative command
204
+ * @returns {string} Formatted denial message
205
+ */
206
+ function formatDenied(cmd, opts = {}) {
207
+ const { currentTier = "free", requiredTier = "pro" } = opts;
208
+
209
+ const copy = DENIAL_COPY[cmd] || {
210
+ feature: cmd,
211
+ why: `${cmd} command`,
212
+ freeAlt: null,
213
+ freeAltDesc: null,
214
+ };
215
+
216
+ const reqColor = TIER_COLORS[requiredTier] || c.yellow;
217
+ const reqLabel = TIER_LABELS[requiredTier] || requiredTier.toUpperCase();
218
+ const curLabel = TIER_LABELS[currentTier] || currentTier.toUpperCase();
219
+
220
+ let msg = `
221
+ ${c.red}${c.bold}${sym.lock} LOCKED${c.reset}
222
+
223
+ ${c.bold}What:${c.reset} ${c.yellow}${cmd}${c.reset} ${c.dim}(${copy.feature})${c.reset}
224
+ ${c.bold}Why:${c.reset} ${copy.why}
225
+ ${c.bold}Requires:${c.reset} ${reqColor}${reqLabel}${c.reset} plan
226
+ ${c.bold}You have:${c.reset} ${c.dim}${curLabel}${c.reset}
227
+ `;
228
+
229
+ // Free alternative
230
+ if (copy.freeAlt) {
231
+ msg += `
232
+ ${c.green}${sym.arrow} Free path:${c.reset} ${c.cyan}${copy.freeAlt}${c.reset} ${c.dim}(${copy.freeAltDesc})${c.reset}`;
233
+ }
234
+
235
+ // Upgrade CTA
236
+ msg += `
237
+ ${c.bold}${sym.arrow} Upgrade:${c.reset} ${PRICING_URL}
238
+ `;
239
+
240
+ return msg;
241
+ }
242
+
243
+ // ═══════════════════════════════════════════════════════════════════════════════
244
+ // formatDowngrade() - Caps/downgrade notice
245
+ // ═══════════════════════════════════════════════════════════════════════════════
246
+ /**
247
+ * Format a downgrade notice for a command running in capped mode.
248
+ *
249
+ * @param {string} cmd - The command being run
250
+ * @param {object} opts - Options
251
+ * @param {string} opts.currentTier - User's current tier
252
+ * @param {string} opts.effectiveMode - The downgraded mode being used
253
+ * @param {object} [opts.caps] - Specific caps applied
254
+ * @returns {string} Formatted downgrade notice
255
+ */
256
+ function formatDowngrade(cmd, opts = {}) {
257
+ const { currentTier = "free", effectiveMode, caps } = opts;
258
+
259
+ const copy = CAPS_COPY[effectiveMode] || {
260
+ short: effectiveMode || "limited mode",
261
+ full: `Running in ${effectiveMode || "limited"} mode`,
262
+ upgradeBenefit: "Full access",
263
+ };
264
+
265
+ const curLabel = TIER_LABELS[currentTier] || currentTier.toUpperCase();
266
+
267
+ // Single line notice for start of run
268
+ let shortNotice = `${c.yellow}${sym.warning}${c.reset} Running in ${c.yellow}${curLabel}${c.reset} mode: ${c.dim}${copy.short}${c.reset}`;
269
+
270
+ return shortNotice;
271
+ }
272
+
273
+ /**
274
+ * Format a cap-hit notice when user reaches a limit during execution.
275
+ *
276
+ * @param {string} cmd - The command being run
277
+ * @param {object} opts - Options
278
+ * @param {string} opts.limitType - Type of limit hit (e.g., "pages", "clicks")
279
+ * @param {number} opts.limitValue - The limit value
280
+ * @param {string} opts.upgradeTier - Tier to upgrade to
281
+ * @returns {string} Formatted cap-hit message
282
+ */
283
+ function formatCapHit(cmd, opts = {}) {
284
+ const { limitType = "limit", limitValue, upgradeTier = "pro" } = opts;
285
+
286
+ const tierColor = TIER_COLORS[upgradeTier] || c.magenta;
287
+ const tierLabel = TIER_LABELS[upgradeTier] || upgradeTier.toUpperCase();
288
+
289
+ return `${c.yellow}${sym.warning}${c.reset} Hit FREE ${limitType} cap (${limitValue}). Upgrade to ${tierColor}${tierLabel}${c.reset} ${sym.arrow} ${PRICING_URL}`;
290
+ }
291
+
292
+ // ═══════════════════════════════════════════════════════════════════════════════
293
+ // formatEarnedUpsell() - End-of-run upsell
294
+ // ═══════════════════════════════════════════════════════════════════════════════
295
+ /**
296
+ * Format an earned upsell shown at end of run.
297
+ *
298
+ * @param {object} opts - Options
299
+ * @param {string} opts.cmd - The command that was run
300
+ * @param {string} opts.verdict - The verdict (SHIP/WARN/BLOCK)
301
+ * @param {Array} [opts.topIssues] - Top 3 issues to show
302
+ * @param {string} [opts.withheldArtifact] - Artifact that was withheld (e.g., "badge")
303
+ * @param {string} [opts.upgradeTier] - Tier to suggest upgrading to
304
+ * @param {string} [opts.why] - Why this upsell is relevant
305
+ * @returns {string} Formatted earned upsell message
306
+ */
307
+ function formatEarnedUpsell(opts = {}) {
308
+ const {
309
+ cmd,
310
+ verdict,
311
+ topIssues = [],
312
+ withheldArtifact,
313
+ upgradeTier = "pro",
314
+ why,
315
+ currentTier = "free",
316
+ suggestedCmd,
317
+ } = opts;
318
+
319
+ const tierColor = TIER_COLORS[upgradeTier] || c.magenta;
320
+ const tierLabel = TIER_LABELS[upgradeTier] || upgradeTier.toUpperCase();
321
+
322
+ let msg = "";
323
+
324
+ // Badge withheld case
325
+ if (withheldArtifact === "badge") {
326
+ msg += `
327
+ ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
328
+ ${c.yellow}${sym.badge} Badge withheld${c.reset} ${c.dim}(verdict: ${verdict})${c.reset}
329
+ `;
330
+
331
+ if (topIssues.length > 0) {
332
+ msg += `\n${c.bold}Top blockers:${c.reset}\n`;
333
+ topIssues.slice(0, 3).forEach((issue, i) => {
334
+ const sev = issue.severity === "BLOCK" ? c.red : c.yellow;
335
+ msg += ` ${c.dim}${i + 1}.${c.reset} ${sev}${issue.severity}${c.reset} ${issue.message || issue.id || issue.type}\n`;
336
+ });
337
+ }
338
+
339
+ if (suggestedCmd) {
340
+ msg += `\n${c.green}${sym.arrow} Fix:${c.reset} ${c.cyan}${suggestedCmd}${c.reset}`;
341
+ }
342
+
343
+ msg += `\n${c.dim}Badge unlocks when verdict = SHIP${c.reset}\n`;
344
+ return msg;
345
+ }
346
+
347
+ // Fix plan-only case
348
+ if (cmd === "fix" && withheldArtifact === "apply") {
349
+ msg += `
350
+ ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
351
+ ${c.yellow}${sym.lightning} Fix plan generated${c.reset}
352
+
353
+ Patches ready but ${c.yellow}--apply${c.reset} requires ${tierColor}${tierLabel}${c.reset} plan.
354
+
355
+ ${c.green}${sym.arrow} Review:${c.reset} .vibecheck/missions/
356
+ ${c.bold}${sym.arrow} Apply:${c.reset} Upgrade ${sym.arrow} ${PRICING_URL}
357
+ `;
358
+ return msg;
359
+ }
360
+
361
+ // Reality cap hit case
362
+ if (cmd === "reality" && why === "cap_hit") {
363
+ msg += `
364
+ ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
365
+ ${c.yellow}${sym.warning} Preview complete${c.reset}
366
+
367
+ Crawled ${topIssues.length || 5} pages (FREE limit).
368
+ ${tierColor}${tierLabel}${c.reset} unlocks unlimited pages + auth boundary testing.
369
+
370
+ ${c.bold}${sym.arrow} Upgrade:${c.reset} ${PRICING_URL}
371
+ `;
372
+ return msg;
373
+ }
374
+
375
+ // Generic earned upsell
376
+ if (why) {
377
+ msg += `
378
+ ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
379
+ ${c.cyan}${sym.star} ${why}${c.reset}
380
+
381
+ ${tierColor}${tierLabel}${c.reset} unlocks this feature.
382
+ ${c.bold}${sym.arrow} Upgrade:${c.reset} ${PRICING_URL}
383
+ `;
384
+ }
385
+
386
+ return msg;
387
+ }
388
+
389
+ // ═══════════════════════════════════════════════════════════════════════════════
390
+ // formatBadgeWithheld() - Specific badge denial
391
+ // ═══════════════════════════════════════════════════════════════════════════════
392
+ /**
393
+ * Format badge withheld message with top issues.
394
+ *
395
+ * @param {string} verdict - Current verdict
396
+ * @param {Array} findings - All findings
397
+ * @param {string} currentTier - User's tier
398
+ * @returns {string} Formatted message
399
+ */
400
+ function formatBadgeWithheld(verdict, findings = [], currentTier = "free") {
401
+ const blockers = findings.filter(f => f.severity === "BLOCK").slice(0, 3);
402
+ const topIssues = blockers.length > 0 ? blockers : findings.slice(0, 3);
403
+
404
+ let msg = `
405
+ ${c.yellow}${sym.badge} Badge withheld${c.reset}
406
+
407
+ ${c.bold}Verdict:${c.reset} ${verdict === "BLOCK" ? c.red : c.yellow}${verdict}${c.reset}
408
+ ${c.dim}Badge requires SHIP verdict${c.reset}
409
+ `;
410
+
411
+ if (topIssues.length > 0) {
412
+ msg += `\n ${c.bold}Fix these first:${c.reset}\n`;
413
+ topIssues.forEach((issue, i) => {
414
+ const sev = issue.severity === "BLOCK" ? c.red : c.yellow;
415
+ const label = issue.id || issue.type || issue.ruleId || "issue";
416
+ const desc = issue.message || issue.description || "";
417
+ msg += ` ${c.dim}${i + 1}.${c.reset} ${sev}[${issue.severity}]${c.reset} ${label}${desc ? `: ${c.dim}${desc.slice(0, 50)}${c.reset}` : ""}\n`;
418
+ });
419
+ }
420
+
421
+ // Suggest fix command if available
422
+ const canFix = currentTier !== "free";
423
+ if (canFix) {
424
+ msg += `\n ${c.green}${sym.arrow} Run:${c.reset} ${c.cyan}vibecheck fix${c.reset}\n`;
425
+ } else {
426
+ msg += `\n ${c.green}${sym.arrow} Run:${c.reset} ${c.cyan}vibecheck fix --plan-only${c.reset} ${c.dim}(view fix plan)${c.reset}\n`;
427
+ }
428
+
429
+ return msg;
430
+ }
431
+
432
+ // ═══════════════════════════════════════════════════════════════════════════════
433
+ // formatNextSteps() - Smart suggestions
434
+ // ═══════════════════════════════════════════════════════════════════════════════
435
+ /**
436
+ * Format next step suggestions based on current command and result.
437
+ *
438
+ * @param {string} cmd - Command just run
439
+ * @param {string} verdict - Result/verdict
440
+ * @param {string} currentTier - User's tier
441
+ * @returns {string} Next step suggestion
442
+ */
443
+ function formatNextSteps(cmd, verdict, currentTier = "free") {
444
+ const steps = [];
445
+
446
+ switch (cmd) {
447
+ case "scan":
448
+ if (currentTier === "free") {
449
+ steps.push({ cmd: "vibecheck ship", desc: "get verdict" });
450
+ } else {
451
+ steps.push({ cmd: "vibecheck ship", desc: "get verdict" });
452
+ steps.push({ cmd: "vibecheck prove --url <url>", desc: "full proof loop" });
453
+ }
454
+ break;
455
+
456
+ case "ship":
457
+ if (verdict === "SHIP") {
458
+ if (currentTier !== "free") {
459
+ steps.push({ cmd: "vibecheck badge", desc: "generate badge" });
460
+ }
461
+ steps.push({ cmd: "vibecheck report", desc: "export report" });
462
+ } else {
463
+ if (currentTier === "free") {
464
+ steps.push({ cmd: "vibecheck fix --plan-only", desc: "view fix plan" });
465
+ } else {
466
+ steps.push({ cmd: "vibecheck fix", desc: "auto-fix issues" });
467
+ }
468
+ }
469
+ break;
470
+
471
+ case "fix":
472
+ steps.push({ cmd: "vibecheck ship", desc: "verify fixes" });
473
+ break;
474
+
475
+ case "reality":
476
+ steps.push({ cmd: "vibecheck ship", desc: "get verdict with runtime proof" });
477
+ break;
478
+ }
479
+
480
+ if (steps.length === 0) return "";
481
+
482
+ let msg = `\n${c.dim}Next:${c.reset} `;
483
+ msg += steps.map(s => `${c.cyan}${s.cmd}${c.reset} ${c.dim}(${s.desc})${c.reset}`).join(" or ");
484
+
485
+ return msg;
486
+ }
487
+
488
+ // ═══════════════════════════════════════════════════════════════════════════════
489
+ // formatSoftUpsell() - Non-intrusive end-of-run upsell
490
+ // ═══════════════════════════════════════════════════════════════════════════════
491
+ /**
492
+ * Format a soft upsell shown at end of any command.
493
+ * Less intrusive than formatEarnedUpsell - single line.
494
+ *
495
+ * @param {string} cmd - The command that was run
496
+ * @param {object} opts - Options
497
+ * @param {string} opts.currentTier - User's current tier
498
+ * @param {string} opts.feature - Feature to upsell
499
+ * @param {string} opts.upgradeTier - Tier to suggest
500
+ * @returns {string} Formatted soft upsell (single line)
501
+ */
502
+ function formatSoftUpsell(cmd, opts = {}) {
503
+ const { currentTier = "free", feature, upgradeTier = "starter" } = opts;
504
+
505
+ // Don't show upsells to paid users for starter features
506
+ if (currentTier !== "free" && upgradeTier === "starter") return "";
507
+ if (currentTier === "pro" || currentTier === "complete") return "";
508
+
509
+ const tierColor = TIER_COLORS[upgradeTier] || c.cyan;
510
+ const tierLabel = TIER_LABELS[upgradeTier] || upgradeTier.toUpperCase();
511
+
512
+ // Command-specific soft upsells
513
+ const SOFT_UPSELLS = {
514
+ context: {
515
+ feature: "AI rule sync",
516
+ benefit: "sync rules across team + auto-update",
517
+ tier: "starter",
518
+ },
519
+ guard: {
520
+ feature: "advanced hallucination detection",
521
+ benefit: "deep claim verification + policy engine",
522
+ tier: "starter",
523
+ },
524
+ whoami: {
525
+ feature: "team dashboard",
526
+ benefit: "shared findings + team policies",
527
+ tier: "starter",
528
+ },
529
+ login: {
530
+ feature: "dashboard sync",
531
+ benefit: "findings sync to web dashboard",
532
+ tier: "starter",
533
+ },
534
+ logout: null, // No upsell on logout
535
+ validate: {
536
+ feature: "continuous validation",
537
+ benefit: "watch mode + CI integration",
538
+ tier: "starter",
539
+ },
540
+ truth: {
541
+ feature: "truthpack sync",
542
+ benefit: "team-shared truthpacks",
543
+ tier: "pro",
544
+ },
545
+ firewall: {
546
+ feature: "policy enforcement",
547
+ benefit: "custom rules + block patterns",
548
+ tier: "starter",
549
+ },
550
+ agent: {
551
+ feature: "agent firewall",
552
+ benefit: "real-time AI action blocking",
553
+ tier: "pro",
554
+ },
555
+ };
556
+
557
+ const upsellCopy = feature ? { feature, benefit: feature, tier: upgradeTier } : SOFT_UPSELLS[cmd];
558
+ if (!upsellCopy) return "";
559
+
560
+ const targetTier = upsellCopy.tier || upgradeTier;
561
+ const targetColor = TIER_COLORS[targetTier] || c.cyan;
562
+ const targetLabel = TIER_LABELS[targetTier] || targetTier.toUpperCase();
563
+
564
+ return `${c.dim}${sym.star} ${targetColor}${targetLabel}${c.reset}${c.dim}: ${upsellCopy.benefit} ${sym.arrow} ${PRICING_URL}${c.reset}`;
565
+ }
566
+
567
+ /**
568
+ * Format a workflow upsell - suggests next command with tier context.
569
+ *
570
+ * @param {string} completedCmd - Command just completed
571
+ * @param {string} currentTier - User's tier
572
+ * @returns {string} Formatted workflow suggestion with upsell
573
+ */
574
+ function formatWorkflowUpsell(completedCmd, currentTier = "free") {
575
+ const WORKFLOWS = {
576
+ scan: {
577
+ next: "ship",
578
+ freeCmd: "vibecheck ship",
579
+ paidCmd: "vibecheck ship --runtime",
580
+ paidBenefit: "includes runtime verification",
581
+ tier: "starter",
582
+ },
583
+ context: {
584
+ next: "scan",
585
+ freeCmd: "vibecheck scan",
586
+ paidCmd: "vibecheck scan --autofix",
587
+ paidBenefit: "auto-fix findings",
588
+ tier: "starter",
589
+ },
590
+ guard: {
591
+ next: "scan",
592
+ freeCmd: "vibecheck scan",
593
+ paidCmd: "vibecheck prove",
594
+ paidBenefit: "full proof loop",
595
+ tier: "pro",
596
+ },
597
+ doctor: {
598
+ next: "scan",
599
+ freeCmd: "vibecheck scan",
600
+ paidCmd: "vibecheck fix",
601
+ paidBenefit: "auto-fix issues found",
602
+ tier: "starter",
603
+ },
604
+ checkpoint: {
605
+ next: "fix",
606
+ freeCmd: "vibecheck fix --plan-only",
607
+ paidCmd: "vibecheck fix --apply",
608
+ paidBenefit: "apply patches automatically",
609
+ tier: "starter",
610
+ },
611
+ validate: {
612
+ next: "ship",
613
+ freeCmd: "vibecheck ship",
614
+ paidCmd: "vibecheck prove",
615
+ paidBenefit: "verified proof artifacts",
616
+ tier: "pro",
617
+ },
618
+ };
619
+
620
+ const workflow = WORKFLOWS[completedCmd];
621
+ if (!workflow) return "";
622
+
623
+ const isPaid = currentTier !== "free";
624
+ const tierColor = TIER_COLORS[workflow.tier] || c.cyan;
625
+ const tierLabel = TIER_LABELS[workflow.tier] || workflow.tier.toUpperCase();
626
+
627
+ if (isPaid) {
628
+ return `${c.dim}Next:${c.reset} ${c.cyan}${workflow.paidCmd}${c.reset}`;
629
+ }
630
+
631
+ return `${c.dim}Next:${c.reset} ${c.cyan}${workflow.freeCmd}${c.reset} ${c.dim}or${c.reset} ${tierColor}${tierLabel}${c.reset}${c.dim}: ${workflow.paidCmd} (${workflow.paidBenefit})${c.reset}`;
632
+ }
633
+
634
+ // ═══════════════════════════════════════════════════════════════════════════════
635
+ // EXPORTS
636
+ // ═══════════════════════════════════════════════════════════════════════════════
637
+ module.exports = {
638
+ // Core formatters
639
+ formatDenied,
640
+ formatDowngrade,
641
+ formatCapHit,
642
+ formatEarnedUpsell,
643
+ formatBadgeWithheld,
644
+ formatNextSteps,
645
+ formatSoftUpsell,
646
+ formatWorkflowUpsell,
647
+
648
+ // Copy data (for testing/docs)
649
+ DENIAL_COPY,
650
+ CAPS_COPY,
651
+ TIER_LABELS,
652
+ TIER_COLORS,
653
+ PRICING_URL,
654
+
655
+ // Styling (for consistent use)
656
+ c,
657
+ sym,
658
+ };