@vibecheckai/cli 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +31 -38
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +68 -3
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +4 -2
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +5 -4
- package/bin/runners/lib/error-messages.js +1 -1
- package/bin/runners/lib/next-action.js +5 -5
- package/bin/runners/lib/report-output.js +6 -6
- package/bin/runners/lib/scan-output.js +4 -22
- package/mcp-server/package.json +1 -1
- package/mcp-server/tier-auth.js +105 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ vibecheck reality --url http://localhost:3000
|
|
|
89
89
|
|------|-------|----------|
|
|
90
90
|
| FREE | $0 | init --local, scan, ship (static), doctor, polish, report (HTML/MD), context, guard, checkpoint (basic) |
|
|
91
91
|
| STARTER | $39/mo | + init --connect, scan --autofix, report (SARIF/CSV), mcp, reality (basic) |
|
|
92
|
-
| PRO | $
|
|
92
|
+
| PRO | $49/mo | + prove, fix --apply, checkpoint (hallucination), reality (advanced), ai-test |
|
|
93
93
|
|
|
94
94
|
## What It Catches
|
|
95
95
|
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fake Success UI Rule
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Only flags egregious cases of UI showing success without any backing logic.
|
|
5
|
+
* This rule is now much more lenient to reduce false positives - most success
|
|
6
|
+
* messages are legitimate (form submissions, state updates, etc.)
|
|
7
|
+
*
|
|
8
|
+
* Only flags when:
|
|
9
|
+
* 1. Success message is in a "critical" domain (payments, auth)
|
|
10
|
+
* 2. AND there's zero HTTP/API activity in the entire file
|
|
11
|
+
* 3. AND no state management activity detected
|
|
5
12
|
*/
|
|
6
13
|
|
|
7
14
|
"use strict";
|
|
@@ -30,51 +37,37 @@ function evaluate({ claims, evidence, policy }) {
|
|
|
30
37
|
return null;
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
const claimFile = claim.pointer ? claim.pointer.split(":")[0] : "";
|
|
36
|
-
if (!claimFile) return true;
|
|
37
|
-
|
|
38
|
-
// Check if there's an HTTP call in the same file (within reasonable distance)
|
|
39
|
-
const claimLine = claim.pointer ? parseInt(claim.pointer.split(":")[1]?.split("-")[0] || "0", 10) : 0;
|
|
40
|
-
const hasNearbyHttpCall = claims.some(c => {
|
|
41
|
-
if (c.type !== CLAIM_TYPES.HTTP_CALL && c.type !== CLAIM_TYPES.ROUTE) return false;
|
|
42
|
-
const cFile = c.pointer ? c.pointer.split(":")[0] : "";
|
|
43
|
-
if (cFile !== claimFile) return false;
|
|
44
|
-
const cLine = c.pointer ? parseInt(c.pointer.split(":")[1]?.split("-")[0] || "0", 10) : 0;
|
|
45
|
-
// Within 50 lines is considered "nearby"
|
|
46
|
-
return Math.abs(cLine - claimLine) <= 50;
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// If there's a nearby HTTP call, this is likely checking the response, not showing UI
|
|
50
|
-
// Only flag if there's NO HTTP call nearby
|
|
51
|
-
return !hasNearbyHttpCall;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (realSuccessClaims.length === 0) {
|
|
55
|
-
return null; // All were false positives (likely API response checks)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Check if there's a corresponding HTTP call or side effect globally
|
|
40
|
+
// Check if there's ANY HTTP call, route reference, or side effect in the claims
|
|
41
|
+
// If yes, the file likely has real logic and success messages are legitimate
|
|
59
42
|
const hasHttpCall = claims.some(c =>
|
|
60
43
|
c.type === CLAIM_TYPES.HTTP_CALL || c.type === CLAIM_TYPES.ROUTE
|
|
61
44
|
);
|
|
62
45
|
|
|
63
46
|
const hasSideEffect = claims.some(c => c.type === CLAIM_TYPES.SIDE_EFFECT);
|
|
64
47
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
48
|
+
// If there's any API activity, allow all success messages
|
|
49
|
+
if (hasHttpCall || hasSideEffect) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if any success claim is in a critical domain
|
|
54
|
+
const criticalDomains = ruleConfig.block_if_domain || ["payments", "auth"];
|
|
55
|
+
const criticalSuccessClaims = successClaims.filter(claim => {
|
|
56
|
+
const domain = claim.domain || "general";
|
|
57
|
+
return criticalDomains.includes(domain);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Only flag if:
|
|
61
|
+
// 1. There's a success claim in a critical domain
|
|
62
|
+
// 2. AND there's zero API activity
|
|
63
|
+
// 3. AND we have multiple success claims (single success is likely legitimate)
|
|
64
|
+
if (criticalSuccessClaims.length > 0 && successClaims.length > 2) {
|
|
72
65
|
return {
|
|
73
66
|
rule: "fake_success_ui",
|
|
74
|
-
severity:
|
|
75
|
-
message: `
|
|
76
|
-
claimId: `claim_${claims.indexOf(
|
|
77
|
-
claim:
|
|
67
|
+
severity: ruleConfig.severity || "warn", // Default to warn, not block
|
|
68
|
+
message: `Multiple success messages (${successClaims.length}) in ${criticalSuccessClaims[0].domain} without API calls - verify logic exists`,
|
|
69
|
+
claimId: `claim_${claims.indexOf(criticalSuccessClaims[0])}`,
|
|
70
|
+
claim: criticalSuccessClaims[0]
|
|
78
71
|
};
|
|
79
72
|
}
|
|
80
73
|
|
|
@@ -12,6 +12,10 @@ const { CLAIM_TYPES } = require("../../claims/claim-types");
|
|
|
12
12
|
/**
|
|
13
13
|
* Common environment variables that are safe to use without explicit declaration.
|
|
14
14
|
* These are well-known Node.js, Next.js, and common deployment platform vars.
|
|
15
|
+
*
|
|
16
|
+
* NOTE: Be generous with this list - most env vars are set by the user's deployment
|
|
17
|
+
* environment and aren't hallucinations. We only want to flag truly phantom vars
|
|
18
|
+
* that the agent invented with no basis.
|
|
15
19
|
*/
|
|
16
20
|
const SAFE_ENV_VARS = new Set([
|
|
17
21
|
// Node.js core
|
|
@@ -19,6 +23,7 @@ const SAFE_ENV_VARS = new Set([
|
|
|
19
23
|
"NODE_OPTIONS",
|
|
20
24
|
"NODE_PATH",
|
|
21
25
|
"NODE_DEBUG",
|
|
26
|
+
"NODE_TLS_REJECT_UNAUTHORIZED",
|
|
22
27
|
|
|
23
28
|
// Next.js / React
|
|
24
29
|
"NEXT_PUBLIC_", // Prefix pattern
|
|
@@ -27,6 +32,7 @@ const SAFE_ENV_VARS = new Set([
|
|
|
27
32
|
"VERCEL_ENV",
|
|
28
33
|
"VERCEL_URL",
|
|
29
34
|
"NEXT_RUNTIME",
|
|
35
|
+
"ANALYZE",
|
|
30
36
|
|
|
31
37
|
// Common deployment
|
|
32
38
|
"PORT",
|
|
@@ -43,25 +49,70 @@ const SAFE_ENV_VARS = new Set([
|
|
|
43
49
|
"PWD",
|
|
44
50
|
"SHELL",
|
|
45
51
|
"TERM",
|
|
52
|
+
"TMPDIR",
|
|
53
|
+
"TEMP",
|
|
54
|
+
"TMP",
|
|
46
55
|
|
|
47
56
|
// Testing
|
|
48
57
|
"TEST",
|
|
49
58
|
"JEST_WORKER_ID",
|
|
50
59
|
"VITEST",
|
|
60
|
+
"PLAYWRIGHT_TEST_BASE_URL",
|
|
51
61
|
|
|
52
62
|
// Build tools
|
|
53
63
|
"npm_package_name",
|
|
54
64
|
"npm_package_version",
|
|
55
65
|
"npm_lifecycle_event",
|
|
66
|
+
|
|
67
|
+
// Database (common naming patterns)
|
|
68
|
+
"DATABASE_URL",
|
|
69
|
+
"DB_HOST",
|
|
70
|
+
"DB_PORT",
|
|
71
|
+
"DB_USER",
|
|
72
|
+
"DB_PASSWORD",
|
|
73
|
+
"DB_NAME",
|
|
74
|
+
"POSTGRES_URL",
|
|
75
|
+
"MYSQL_URL",
|
|
76
|
+
"REDIS_URL",
|
|
77
|
+
"MONGODB_URI",
|
|
78
|
+
|
|
79
|
+
// Auth (common naming patterns)
|
|
80
|
+
"JWT_SECRET",
|
|
81
|
+
"SESSION_SECRET",
|
|
82
|
+
"AUTH_SECRET",
|
|
83
|
+
"NEXTAUTH_SECRET",
|
|
84
|
+
"NEXTAUTH_URL",
|
|
85
|
+
|
|
86
|
+
// API keys (generic patterns)
|
|
87
|
+
"API_KEY",
|
|
88
|
+
"API_SECRET",
|
|
89
|
+
"SECRET_KEY",
|
|
90
|
+
|
|
91
|
+
// Email
|
|
92
|
+
"SMTP_HOST",
|
|
93
|
+
"SMTP_PORT",
|
|
94
|
+
"SMTP_USER",
|
|
95
|
+
"SMTP_PASSWORD",
|
|
96
|
+
"MAIL_HOST",
|
|
97
|
+
"MAIL_PORT",
|
|
98
|
+
"RESEND_API_KEY",
|
|
99
|
+
"SENDGRID_API_KEY",
|
|
100
|
+
|
|
101
|
+
// Payments
|
|
102
|
+
"STRIPE_SECRET_KEY",
|
|
103
|
+
"STRIPE_PUBLISHABLE_KEY",
|
|
104
|
+
"STRIPE_WEBHOOK_SECRET",
|
|
56
105
|
]);
|
|
57
106
|
|
|
58
107
|
/**
|
|
59
|
-
* Prefix patterns that are safe
|
|
108
|
+
* Prefix patterns that are safe - these are from known platforms/tools
|
|
60
109
|
*/
|
|
61
110
|
const SAFE_ENV_PREFIXES = [
|
|
62
111
|
"NEXT_PUBLIC_",
|
|
63
112
|
"REACT_APP_",
|
|
64
113
|
"VITE_",
|
|
114
|
+
"VUE_APP_",
|
|
115
|
+
"NUXT_",
|
|
65
116
|
"npm_",
|
|
66
117
|
"GITHUB_",
|
|
67
118
|
"CI_",
|
|
@@ -70,6 +121,18 @@ const SAFE_ENV_PREFIXES = [
|
|
|
70
121
|
"RENDER_",
|
|
71
122
|
"HEROKU_",
|
|
72
123
|
"AWS_",
|
|
124
|
+
"AZURE_",
|
|
125
|
+
"GOOGLE_",
|
|
126
|
+
"FIREBASE_",
|
|
127
|
+
"SUPABASE_",
|
|
128
|
+
"CLERK_",
|
|
129
|
+
"AUTH0_",
|
|
130
|
+
"SENTRY_",
|
|
131
|
+
"DATADOG_",
|
|
132
|
+
"NEW_RELIC_",
|
|
133
|
+
"OPENAI_",
|
|
134
|
+
"ANTHROPIC_",
|
|
135
|
+
"VIBECHECK_",
|
|
73
136
|
];
|
|
74
137
|
|
|
75
138
|
/**
|
|
@@ -143,10 +206,12 @@ function evaluate({ claims, evidence, policy }) {
|
|
|
143
206
|
};
|
|
144
207
|
}
|
|
145
208
|
|
|
209
|
+
// Default to "warn" - only block if explicitly configured
|
|
210
|
+
// Most env vars are real, just not in .env.example
|
|
146
211
|
return {
|
|
147
212
|
rule: "ghost_env",
|
|
148
|
-
severity: ruleConfig.severity || "
|
|
149
|
-
message: `
|
|
213
|
+
severity: ruleConfig.severity || "warn",
|
|
214
|
+
message: `Env var ${envVar} used but not found in .env.example`,
|
|
150
215
|
claimId: `claim_${i}`,
|
|
151
216
|
claim
|
|
152
217
|
};
|
|
@@ -170,10 +170,12 @@ function evaluate({ claims, evidence, policy }) {
|
|
|
170
170
|
continue;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
// Default to "warn" - only block if explicitly configured
|
|
174
|
+
// Route references are often correct, just not in truthpack yet
|
|
173
175
|
return {
|
|
174
176
|
rule: "ghost_route",
|
|
175
|
-
severity: ruleConfig.severity || "
|
|
176
|
-
message: `
|
|
177
|
+
severity: ruleConfig.severity || "warn",
|
|
178
|
+
message: `Route ${routePath} not found in truthpack - verify it exists`,
|
|
177
179
|
claimId: `claim_${i}`,
|
|
178
180
|
claim
|
|
179
181
|
};
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
const DEFAULT_THRESHOLDS = {
|
|
19
19
|
/**
|
|
20
20
|
* Score thresholds for automatic decisions
|
|
21
|
-
* Raised to
|
|
21
|
+
* Raised significantly to focus only on real issues (hallucinations, drift)
|
|
22
|
+
* Normal development changes should almost never trigger
|
|
22
23
|
*/
|
|
23
|
-
autoAllow:
|
|
24
|
-
requireConfirm:
|
|
25
|
-
autoBlock:
|
|
24
|
+
autoAllow: 50, // Auto-allow if score <= this (raised from 30)
|
|
25
|
+
requireConfirm: 85, // Require confirmation if score > this (raised from 70)
|
|
26
|
+
autoBlock: 120, // Auto-block if score >= this (raised from 100)
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Vector-specific thresholds
|
|
@@ -106,7 +106,7 @@ const ERROR_TEMPLATES = {
|
|
|
106
106
|
message: 'Invalid API key format',
|
|
107
107
|
suggestion: 'Check your API key format',
|
|
108
108
|
nextSteps: [
|
|
109
|
-
'API keys should start with
|
|
109
|
+
'API keys should start with grl_ (from dashboard)',
|
|
110
110
|
'Get a new key: https://app.vibecheckai.dev/settings/api-keys',
|
|
111
111
|
'Run: vibecheck login --key YOUR_KEY',
|
|
112
112
|
],
|
|
@@ -228,7 +228,7 @@ function computeNextAction(state) {
|
|
|
228
228
|
return {
|
|
229
229
|
action: "badge",
|
|
230
230
|
command: "vibecheck ship --badge",
|
|
231
|
-
dashboardLink: `${DASHBOARD_URL}/runs/${runId}
|
|
231
|
+
dashboardLink: runId ? `${DASHBOARD_URL}/runs/${runId}` : undefined,
|
|
232
232
|
why: "SHIP passed! Generate a verified badge for your README.",
|
|
233
233
|
timeEstimate: TIME_ESTIMATES.badge,
|
|
234
234
|
tier: "pro",
|
|
@@ -255,7 +255,7 @@ function computeNextAction(state) {
|
|
|
255
255
|
return {
|
|
256
256
|
action: "report",
|
|
257
257
|
command: "vibecheck report",
|
|
258
|
-
dashboardLink: `${DASHBOARD_URL}/runs/${runId}
|
|
258
|
+
dashboardLink: runId ? `${DASHBOARD_URL}/runs/${runId}` : undefined,
|
|
259
259
|
why: "Scan complete. Export report or upgrade to get verdict.",
|
|
260
260
|
timeEstimate: TIME_ESTIMATES.report,
|
|
261
261
|
tier: "free",
|
|
@@ -328,7 +328,7 @@ function getNextActionForCommand(completedCmd, result = {}, tier = "free") {
|
|
|
328
328
|
return {
|
|
329
329
|
action: "report",
|
|
330
330
|
command: "vibecheck report",
|
|
331
|
-
dashboardLink: `${DASHBOARD_URL}/runs/${runId}
|
|
331
|
+
dashboardLink: runId ? `${DASHBOARD_URL}/runs/${runId}` : undefined,
|
|
332
332
|
why: `Scan complete. Export report.`,
|
|
333
333
|
timeEstimate: TIME_ESTIMATES.report,
|
|
334
334
|
tier: "free",
|
|
@@ -344,7 +344,7 @@ function getNextActionForCommand(completedCmd, result = {}, tier = "free") {
|
|
|
344
344
|
return {
|
|
345
345
|
action: "badge",
|
|
346
346
|
command: "vibecheck ship --badge",
|
|
347
|
-
dashboardLink: `${DASHBOARD_URL}/runs/${runId}
|
|
347
|
+
dashboardLink: runId ? `${DASHBOARD_URL}/runs/${runId}` : undefined,
|
|
348
348
|
why: "SHIP passed! Generate a verified badge.",
|
|
349
349
|
timeEstimate: TIME_ESTIMATES.badge,
|
|
350
350
|
tier: "pro",
|
|
@@ -363,7 +363,7 @@ function getNextActionForCommand(completedCmd, result = {}, tier = "free") {
|
|
|
363
363
|
return {
|
|
364
364
|
action: "report",
|
|
365
365
|
command: "vibecheck report",
|
|
366
|
-
dashboardLink: `${DASHBOARD_URL}/runs/${runId}
|
|
366
|
+
dashboardLink: runId ? `${DASHBOARD_URL}/runs/${runId}` : undefined,
|
|
367
367
|
why: "Export detailed report.",
|
|
368
368
|
timeEstimate: TIME_ESTIMATES.report,
|
|
369
369
|
tier: "free",
|
|
@@ -52,12 +52,12 @@ const LOGO_VIBECHECK = `
|
|
|
52
52
|
|
|
53
53
|
// HERO: REPORTS (Blue/Cyan) - Fixed to say "REPORTS" plural
|
|
54
54
|
const LOGO_REPORTS = `
|
|
55
|
-
██████╗ ███████╗██████╗ ██████╗ ██████╗
|
|
56
|
-
|
|
57
|
-
██████╔╝█████╗ ██████╔╝██║
|
|
58
|
-
██╔══██╗██╔══╝ ██╔═══╝ ██║
|
|
59
|
-
██║ ██║███████╗██║ ╚██████╔╝██║
|
|
60
|
-
╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚═╝
|
|
55
|
+
██████╗ ███████╗██████╗ ██████╗ ██████╗ ████████╗███████╗
|
|
56
|
+
██╔══██╗██╔════╝██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝
|
|
57
|
+
██████╔╝█████╗ ██████╔╝██║ ██║██████╔╝ ██║ ███████╗
|
|
58
|
+
██╔══██╗██╔══╝ ██╔═══╝ ██║ ██║██╔══██╗ ██║ ╚════██║
|
|
59
|
+
██║ ██║███████╗██║ ╚██████╔╝██║ ██║ ██║ ███████║
|
|
60
|
+
╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
|
|
61
61
|
`;
|
|
62
62
|
|
|
63
63
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Enterprise Scan Output - V5 "Mission Control" Format
|
|
3
3
|
* Features:
|
|
4
|
-
* - External
|
|
4
|
+
* - External Vibecheck Header (ANSI Shadow)
|
|
5
5
|
* - Internal SCAN Block (Centered)
|
|
6
6
|
* - Split-pane layout (Vitals vs. Diagnostics)
|
|
7
7
|
* - Gradient Health Bars
|
|
@@ -281,6 +281,9 @@ function formatScanOutput(result, options = {}) {
|
|
|
281
281
|
const healthStatus = score > 80 ? 'OPTIMAL' : score > 50 ? 'STABLE' : 'CRITICAL';
|
|
282
282
|
const healthColor = score > 80 ? chalk.green : score > 50 ? chalk.yellow : chalk.red;
|
|
283
283
|
|
|
284
|
+
// Extract runId early to avoid duplicate declaration issues
|
|
285
|
+
const runId = options.runId || result.runId || result.verdict?.runId;
|
|
286
|
+
|
|
284
287
|
const duration = ((result.duration || result.timings?.total || 0) / 1000).toFixed(1);
|
|
285
288
|
const memUsage = Math.round(process.memoryUsage().heapUsed / 1024 / 1024 / 1024 * 100) / 100;
|
|
286
289
|
const projectPath = process.cwd();
|
|
@@ -449,27 +452,6 @@ function formatScanOutput(result, options = {}) {
|
|
|
449
452
|
lines.push(`${chalk.gray}${BOX.bottomLeft}${BOX.horizontal.repeat(WIDTH - 2)}${BOX.bottomRight}${chalk.reset}`);
|
|
450
453
|
|
|
451
454
|
// DASHBOARD LINK (outside frame)
|
|
452
|
-
const runId = result.runId || result.verdict?.runId;
|
|
453
|
-
if (runId) {
|
|
454
|
-
lines.push('');
|
|
455
|
-
lines.push(` ${chalk.dim}🔗 Dashboard:${chalk.reset} https://app.vibecheckai.dev/runs/${runId}`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// NEXT BEST ACTION (outside frame)
|
|
459
|
-
try {
|
|
460
|
-
const { formatNextActionOneLine, getNextActionForCommand } = require('./next-action');
|
|
461
|
-
const tier = options.tier || 'free';
|
|
462
|
-
const verdict = typeof result.verdict === 'object' ? result.verdict.verdict : result.verdict;
|
|
463
|
-
const nextAction = getNextActionForCommand('scan', { verdict, runId, findings }, tier);
|
|
464
|
-
lines.push('');
|
|
465
|
-
lines.push(` ${formatNextActionOneLine(nextAction)}`);
|
|
466
|
-
} catch (e) {
|
|
467
|
-
// Next action module might not be available, skip gracefully
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
// DASHBOARD LINK (outside frame)
|
|
472
|
-
const runId = result.runId || result.verdict?.runId;
|
|
473
455
|
if (runId) {
|
|
474
456
|
lines.push('');
|
|
475
457
|
lines.push(` ${chalk.dim}🔗 Dashboard:${chalk.reset} https://app.vibecheckai.dev/runs/${runId}`);
|
package/mcp-server/package.json
CHANGED
package/mcp-server/tier-auth.js
CHANGED
|
@@ -51,6 +51,7 @@ export const ERROR_CODES = {
|
|
|
51
51
|
INVALID_API_KEY: 'INVALID_API_KEY',
|
|
52
52
|
RATE_LIMITED: 'RATE_LIMITED',
|
|
53
53
|
OPTION_NOT_ENTITLED: 'OPTION_NOT_ENTITLED',
|
|
54
|
+
API_KEY_REQUIRED: 'API_KEY_REQUIRED',
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
// ============================================================================
|
|
@@ -237,10 +238,48 @@ export function createTierErrorEnvelope(code, message, extra = {}) {
|
|
|
237
238
|
};
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Create API_KEY_REQUIRED error envelope
|
|
243
|
+
* Used when a PRO tool is requested but no API key is provided
|
|
244
|
+
*/
|
|
245
|
+
export function apiKeyRequiredError(toolName) {
|
|
246
|
+
return createTierErrorEnvelope(ERROR_CODES.API_KEY_REQUIRED, `API key required for ${toolName}`, {
|
|
247
|
+
tool: toolName,
|
|
248
|
+
userAction: 'Add API Key',
|
|
249
|
+
retryable: true,
|
|
250
|
+
nextSteps: [
|
|
251
|
+
'Get your API key at https://vibecheckai.dev/dashboard',
|
|
252
|
+
'Run: vibecheck login',
|
|
253
|
+
'Or set VIBECHECK_API_KEY environment variable',
|
|
254
|
+
'Then pass apiKey in tool arguments or configure in ~/.vibecheck/credentials.json',
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
240
259
|
/**
|
|
241
260
|
* Create NOT_ENTITLED error envelope
|
|
242
261
|
*/
|
|
243
|
-
export function notEntitledError(toolName, currentTier = 'free', requiredTier = 'pro') {
|
|
262
|
+
export function notEntitledError(toolName, currentTier = 'free', requiredTier = 'pro', hasApiKey = true) {
|
|
263
|
+
// If no API key was provided, give more specific guidance
|
|
264
|
+
if (!hasApiKey) {
|
|
265
|
+
return createTierErrorEnvelope(ERROR_CODES.NOT_ENTITLED, `${toolName} requires PRO - add your API key`, {
|
|
266
|
+
tier: currentTier,
|
|
267
|
+
required: requiredTier,
|
|
268
|
+
tool: toolName,
|
|
269
|
+
userAction: 'Add API Key',
|
|
270
|
+
retryable: true,
|
|
271
|
+
nextSteps: [
|
|
272
|
+
`${toolName} requires PRO ($69/mo) subscription`,
|
|
273
|
+
'If you have PRO, add your API key:',
|
|
274
|
+
' - Run: vibecheck login',
|
|
275
|
+
' - Or pass apiKey in tool arguments',
|
|
276
|
+
' - Or set VIBECHECK_API_KEY environment variable',
|
|
277
|
+
'Get your API key at https://vibecheckai.dev/dashboard',
|
|
278
|
+
'Upgrade at https://vibecheckai.dev/pricing',
|
|
279
|
+
],
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
244
283
|
return createTierErrorEnvelope(ERROR_CODES.NOT_ENTITLED, `Requires ${requiredTier.toUpperCase()}`, {
|
|
245
284
|
tier: currentTier,
|
|
246
285
|
required: requiredTier,
|
|
@@ -326,6 +365,24 @@ export async function getTierFromApiKey(apiKey) {
|
|
|
326
365
|
}
|
|
327
366
|
}
|
|
328
367
|
|
|
368
|
+
// ============================================================================
|
|
369
|
+
// USER CONFIG
|
|
370
|
+
// ============================================================================
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Load user configuration from ~/.vibecheck/credentials.json
|
|
374
|
+
* @returns {Promise<{apiKey?: string, email?: string}|null>}
|
|
375
|
+
*/
|
|
376
|
+
async function loadUserConfig() {
|
|
377
|
+
try {
|
|
378
|
+
const configPath = path.join(os.homedir(), '.vibecheck', 'credentials.json');
|
|
379
|
+
const data = await fs.readFile(configPath, 'utf-8');
|
|
380
|
+
return JSON.parse(data);
|
|
381
|
+
} catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
329
386
|
// ============================================================================
|
|
330
387
|
// ACCESS CONTROL
|
|
331
388
|
// ============================================================================
|
|
@@ -410,6 +467,34 @@ export function canApproveAuthority(tier) {
|
|
|
410
467
|
return tier === 'pro';
|
|
411
468
|
}
|
|
412
469
|
|
|
470
|
+
/**
|
|
471
|
+
* Resolve API key from multiple sources
|
|
472
|
+
* Priority: 1. Passed directly 2. Environment variable 3. Credentials file
|
|
473
|
+
*
|
|
474
|
+
* @param {string} apiKey - API key passed directly (optional)
|
|
475
|
+
* @returns {Promise<{apiKey: string|null, source: string}>}
|
|
476
|
+
*/
|
|
477
|
+
export async function resolveApiKey(apiKey) {
|
|
478
|
+
// 1. Use directly provided API key
|
|
479
|
+
if (apiKey && typeof apiKey === 'string' && apiKey.length >= 10) {
|
|
480
|
+
return { apiKey, source: 'args' };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 2. Check environment variable
|
|
484
|
+
const envKey = process.env.VIBECHECK_API_KEY;
|
|
485
|
+
if (envKey && typeof envKey === 'string' && envKey.length >= 10) {
|
|
486
|
+
return { apiKey: envKey, source: 'env' };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 3. Check credentials file
|
|
490
|
+
const config = await loadUserConfig();
|
|
491
|
+
if (config?.apiKey && typeof config.apiKey === 'string' && config.apiKey.length >= 10) {
|
|
492
|
+
return { apiKey: config.apiKey, source: 'credentials' };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return { apiKey: null, source: 'none' };
|
|
496
|
+
}
|
|
497
|
+
|
|
413
498
|
/**
|
|
414
499
|
* Get MCP tool access with full ErrorEnvelope support
|
|
415
500
|
*
|
|
@@ -419,18 +504,30 @@ export function canApproveAuthority(tier) {
|
|
|
419
504
|
* @returns {Promise<{hasAccess: boolean, tier: string, error?: object}>}
|
|
420
505
|
*/
|
|
421
506
|
export async function getMcpToolAccess(toolName, apiKey, args = {}) {
|
|
422
|
-
|
|
507
|
+
// Resolve API key from multiple sources
|
|
508
|
+
const resolved = await resolveApiKey(apiKey);
|
|
509
|
+
const resolvedApiKey = resolved.apiKey;
|
|
510
|
+
const hasApiKey = resolvedApiKey !== null;
|
|
511
|
+
|
|
512
|
+
const tier = await getTierFromApiKey(resolvedApiKey);
|
|
423
513
|
|
|
424
514
|
// Check tool-level access
|
|
425
515
|
const hasToolAccess = canAccessTool(tier, toolName);
|
|
426
516
|
|
|
427
517
|
if (!hasToolAccess) {
|
|
518
|
+
// Check if this is a PRO tool and no API key was provided
|
|
519
|
+
const isPROTool = PRO_TOOLS.includes(toolName);
|
|
520
|
+
|
|
428
521
|
return {
|
|
429
522
|
hasAccess: false,
|
|
430
523
|
tier,
|
|
524
|
+
hasApiKey,
|
|
525
|
+
apiKeySource: resolved.source,
|
|
431
526
|
firewallMode: getFirewallMode(tier),
|
|
432
|
-
error: notEntitledError(toolName, tier, 'pro'),
|
|
433
|
-
reason:
|
|
527
|
+
error: notEntitledError(toolName, tier, 'pro', hasApiKey),
|
|
528
|
+
reason: hasApiKey
|
|
529
|
+
? `${toolName} requires Pro ($69/mo). Upgrade at https://vibecheckai.dev/pricing`
|
|
530
|
+
: `${toolName} requires Pro. Add your API key first: run 'vibecheck login' or pass apiKey in tool arguments.`,
|
|
434
531
|
};
|
|
435
532
|
}
|
|
436
533
|
|
|
@@ -440,6 +537,8 @@ export async function getMcpToolAccess(toolName, apiKey, args = {}) {
|
|
|
440
537
|
return {
|
|
441
538
|
hasAccess: false,
|
|
442
539
|
tier,
|
|
540
|
+
hasApiKey,
|
|
541
|
+
apiKeySource: resolved.source,
|
|
443
542
|
firewallMode: getFirewallMode(tier),
|
|
444
543
|
error: optionNotEntitledError(toolName, optionCheck.blockedOption, tier, optionCheck.required),
|
|
445
544
|
reason: `Option --${optionCheck.blockedOption} requires Pro`,
|
|
@@ -449,6 +548,8 @@ export async function getMcpToolAccess(toolName, apiKey, args = {}) {
|
|
|
449
548
|
return {
|
|
450
549
|
hasAccess: true,
|
|
451
550
|
tier,
|
|
551
|
+
hasApiKey,
|
|
552
|
+
apiKeySource: resolved.source,
|
|
452
553
|
firewallMode: getFirewallMode(tier),
|
|
453
554
|
reason: 'Access granted',
|
|
454
555
|
};
|
|
@@ -515,16 +616,6 @@ export async function checkTierGate(toolName, apiKey, args = {}) {
|
|
|
515
616
|
// USER INFO
|
|
516
617
|
// ============================================================================
|
|
517
618
|
|
|
518
|
-
async function loadUserConfig() {
|
|
519
|
-
try {
|
|
520
|
-
const configPath = path.join(os.homedir(), '.vibecheck', 'credentials.json');
|
|
521
|
-
const data = await fs.readFile(configPath, 'utf-8');
|
|
522
|
-
return JSON.parse(data);
|
|
523
|
-
} catch {
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
619
|
export async function getUserInfo() {
|
|
529
620
|
const config = await loadUserConfig();
|
|
530
621
|
|