@vibescope/mcp-server 0.0.1
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 +98 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +356 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +367 -0
- package/dist/handlers/__test-utils__.d.ts +72 -0
- package/dist/handlers/__test-utils__.js +176 -0
- package/dist/handlers/blockers.d.ts +18 -0
- package/dist/handlers/blockers.js +81 -0
- package/dist/handlers/bodies-of-work.d.ts +34 -0
- package/dist/handlers/bodies-of-work.js +614 -0
- package/dist/handlers/checkouts.d.ts +37 -0
- package/dist/handlers/checkouts.js +377 -0
- package/dist/handlers/cost.d.ts +39 -0
- package/dist/handlers/cost.js +247 -0
- package/dist/handlers/decisions.d.ts +16 -0
- package/dist/handlers/decisions.js +64 -0
- package/dist/handlers/deployment.d.ts +36 -0
- package/dist/handlers/deployment.js +1062 -0
- package/dist/handlers/discovery.d.ts +14 -0
- package/dist/handlers/discovery.js +870 -0
- package/dist/handlers/fallback.d.ts +18 -0
- package/dist/handlers/fallback.js +216 -0
- package/dist/handlers/findings.d.ts +18 -0
- package/dist/handlers/findings.js +110 -0
- package/dist/handlers/git-issues.d.ts +22 -0
- package/dist/handlers/git-issues.js +247 -0
- package/dist/handlers/ideas.d.ts +19 -0
- package/dist/handlers/ideas.js +188 -0
- package/dist/handlers/index.d.ts +29 -0
- package/dist/handlers/index.js +65 -0
- package/dist/handlers/knowledge-query.d.ts +22 -0
- package/dist/handlers/knowledge-query.js +253 -0
- package/dist/handlers/knowledge.d.ts +12 -0
- package/dist/handlers/knowledge.js +108 -0
- package/dist/handlers/milestones.d.ts +20 -0
- package/dist/handlers/milestones.js +179 -0
- package/dist/handlers/organizations.d.ts +36 -0
- package/dist/handlers/organizations.js +428 -0
- package/dist/handlers/progress.d.ts +14 -0
- package/dist/handlers/progress.js +149 -0
- package/dist/handlers/project.d.ts +20 -0
- package/dist/handlers/project.js +278 -0
- package/dist/handlers/requests.d.ts +16 -0
- package/dist/handlers/requests.js +131 -0
- package/dist/handlers/roles.d.ts +30 -0
- package/dist/handlers/roles.js +281 -0
- package/dist/handlers/session.d.ts +20 -0
- package/dist/handlers/session.js +791 -0
- package/dist/handlers/tasks.d.ts +52 -0
- package/dist/handlers/tasks.js +1111 -0
- package/dist/handlers/tasks.test.d.ts +1 -0
- package/dist/handlers/tasks.test.js +431 -0
- package/dist/handlers/types.d.ts +94 -0
- package/dist/handlers/types.js +1 -0
- package/dist/handlers/validation.d.ts +16 -0
- package/dist/handlers/validation.js +188 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2707 -0
- package/dist/knowledge.d.ts +6 -0
- package/dist/knowledge.js +121 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +2498 -0
- package/dist/utils.d.ts +149 -0
- package/dist/utils.js +317 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +532 -0
- package/dist/validators.d.ts +35 -0
- package/dist/validators.js +111 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +176 -0
- package/package.json +44 -0
- package/src/cli.test.ts +442 -0
- package/src/cli.ts +439 -0
- package/src/handlers/__test-utils__.ts +217 -0
- package/src/handlers/blockers.test.ts +390 -0
- package/src/handlers/blockers.ts +110 -0
- package/src/handlers/bodies-of-work.test.ts +1276 -0
- package/src/handlers/bodies-of-work.ts +783 -0
- package/src/handlers/cost.test.ts +436 -0
- package/src/handlers/cost.ts +322 -0
- package/src/handlers/decisions.test.ts +401 -0
- package/src/handlers/decisions.ts +86 -0
- package/src/handlers/deployment.test.ts +516 -0
- package/src/handlers/deployment.ts +1289 -0
- package/src/handlers/discovery.test.ts +254 -0
- package/src/handlers/discovery.ts +969 -0
- package/src/handlers/fallback.test.ts +687 -0
- package/src/handlers/fallback.ts +260 -0
- package/src/handlers/findings.test.ts +565 -0
- package/src/handlers/findings.ts +153 -0
- package/src/handlers/ideas.test.ts +753 -0
- package/src/handlers/ideas.ts +247 -0
- package/src/handlers/index.ts +69 -0
- package/src/handlers/milestones.test.ts +584 -0
- package/src/handlers/milestones.ts +217 -0
- package/src/handlers/organizations.test.ts +997 -0
- package/src/handlers/organizations.ts +550 -0
- package/src/handlers/progress.test.ts +369 -0
- package/src/handlers/progress.ts +188 -0
- package/src/handlers/project.test.ts +562 -0
- package/src/handlers/project.ts +352 -0
- package/src/handlers/requests.test.ts +531 -0
- package/src/handlers/requests.ts +150 -0
- package/src/handlers/session.test.ts +459 -0
- package/src/handlers/session.ts +912 -0
- package/src/handlers/tasks.test.ts +602 -0
- package/src/handlers/tasks.ts +1393 -0
- package/src/handlers/types.ts +88 -0
- package/src/handlers/validation.test.ts +880 -0
- package/src/handlers/validation.ts +223 -0
- package/src/index.ts +3205 -0
- package/src/knowledge.ts +132 -0
- package/src/tmpclaude-0078-cwd +1 -0
- package/src/tmpclaude-0ee1-cwd +1 -0
- package/src/tmpclaude-2dd5-cwd +1 -0
- package/src/tmpclaude-344c-cwd +1 -0
- package/src/tmpclaude-3860-cwd +1 -0
- package/src/tmpclaude-4b63-cwd +1 -0
- package/src/tmpclaude-5c73-cwd +1 -0
- package/src/tmpclaude-5ee3-cwd +1 -0
- package/src/tmpclaude-6795-cwd +1 -0
- package/src/tmpclaude-709e-cwd +1 -0
- package/src/tmpclaude-9839-cwd +1 -0
- package/src/tmpclaude-d829-cwd +1 -0
- package/src/tmpclaude-e072-cwd +1 -0
- package/src/tmpclaude-f6ee-cwd +1 -0
- package/src/utils.test.ts +681 -0
- package/src/utils.ts +375 -0
- package/src/validators.test.ts +223 -0
- package/src/validators.ts +122 -0
- package/tmpclaude-0439-cwd +1 -0
- package/tmpclaude-132f-cwd +1 -0
- package/tmpclaude-15bb-cwd +1 -0
- package/tmpclaude-165a-cwd +1 -0
- package/tmpclaude-1ba9-cwd +1 -0
- package/tmpclaude-21a3-cwd +1 -0
- package/tmpclaude-2a38-cwd +1 -0
- package/tmpclaude-2adf-cwd +1 -0
- package/tmpclaude-2f56-cwd +1 -0
- package/tmpclaude-3626-cwd +1 -0
- package/tmpclaude-3727-cwd +1 -0
- package/tmpclaude-40bc-cwd +1 -0
- package/tmpclaude-436f-cwd +1 -0
- package/tmpclaude-4783-cwd +1 -0
- package/tmpclaude-4b6d-cwd +1 -0
- package/tmpclaude-4ba4-cwd +1 -0
- package/tmpclaude-51e6-cwd +1 -0
- package/tmpclaude-5ecf-cwd +1 -0
- package/tmpclaude-6f97-cwd +1 -0
- package/tmpclaude-7fb2-cwd +1 -0
- package/tmpclaude-825c-cwd +1 -0
- package/tmpclaude-8baf-cwd +1 -0
- package/tmpclaude-8d9f-cwd +1 -0
- package/tmpclaude-975c-cwd +1 -0
- package/tmpclaude-9983-cwd +1 -0
- package/tmpclaude-a045-cwd +1 -0
- package/tmpclaude-ac4a-cwd +1 -0
- package/tmpclaude-b593-cwd +1 -0
- package/tmpclaude-b891-cwd +1 -0
- package/tmpclaude-c032-cwd +1 -0
- package/tmpclaude-cf43-cwd +1 -0
- package/tmpclaude-d040-cwd +1 -0
- package/tmpclaude-dcdd-cwd +1 -0
- package/tmpclaude-dcee-cwd +1 -0
- package/tmpclaude-e16b-cwd +1 -0
- package/tmpclaude-ecd2-cwd +1 -0
- package/tmpclaude-f48d-cwd +1 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +13 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vibescope CLI - Enforcement verification tool
|
|
5
|
+
*
|
|
6
|
+
* Used by Claude Code Stop hook to verify agent compliance with Vibescope tracking.
|
|
7
|
+
* Exit codes:
|
|
8
|
+
* 0 = Compliant (allow exit)
|
|
9
|
+
* 1 = Non-compliant (block exit, loop back)
|
|
10
|
+
* 2 = Error (allow exit with warning)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createClient } from '@supabase/supabase-js';
|
|
14
|
+
import { createHash } from 'crypto';
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { normalizeGitUrl } from './utils.js';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
export interface AuthContext {
|
|
23
|
+
userId: string;
|
|
24
|
+
apiKeyId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface VerificationResult {
|
|
28
|
+
status: 'compliant' | 'non_compliant' | 'no_session' | 'error';
|
|
29
|
+
reason: string;
|
|
30
|
+
continuation_prompt?: string;
|
|
31
|
+
details?: {
|
|
32
|
+
session_started: boolean;
|
|
33
|
+
project_id: string | null;
|
|
34
|
+
project_name: string | null;
|
|
35
|
+
git_url: string | null;
|
|
36
|
+
in_progress_tasks: number;
|
|
37
|
+
tasks_completed_this_session: number;
|
|
38
|
+
progress_logs_this_session: number;
|
|
39
|
+
blockers_logged_this_session: number;
|
|
40
|
+
session_duration_minutes: number | null;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface TaskInfo {
|
|
45
|
+
id: string;
|
|
46
|
+
title: string;
|
|
47
|
+
progress_percentage: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Configuration
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
const SUPABASE_URL = process.env.SUPABASE_URL || process.env.PUBLIC_SUPABASE_URL;
|
|
55
|
+
const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY;
|
|
56
|
+
const API_KEY = process.env.VIBESCOPE_API_KEY;
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Git URL Detection
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
export function detectGitUrl(): string | null {
|
|
63
|
+
try {
|
|
64
|
+
const url = execSync('git config --get remote.origin.url', {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
timeout: 5000,
|
|
67
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
68
|
+
}).trim();
|
|
69
|
+
|
|
70
|
+
// Normalize: remove .git suffix, convert SSH to HTTPS format
|
|
71
|
+
return normalizeGitUrl(url);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Authentication (reused from index.ts)
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
export function hashApiKey(key: string): string {
|
|
82
|
+
return createHash('sha256').update(key).digest('hex');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
export async function validateApiKey(
|
|
87
|
+
supabase: any,
|
|
88
|
+
apiKey: string
|
|
89
|
+
): Promise<AuthContext | null> {
|
|
90
|
+
const keyHash = hashApiKey(apiKey);
|
|
91
|
+
|
|
92
|
+
const { data, error } = await supabase
|
|
93
|
+
.from('api_keys')
|
|
94
|
+
.select('id, user_id')
|
|
95
|
+
.eq('key_hash', keyHash)
|
|
96
|
+
.single();
|
|
97
|
+
|
|
98
|
+
if (error || !data) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Cast to expected shape since we're using untyped client
|
|
103
|
+
const row = data as { id: string; user_id: string };
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
userId: row.user_id,
|
|
107
|
+
apiKeyId: row.id,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Verification Logic
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
export async function verify(
|
|
116
|
+
gitUrl?: string,
|
|
117
|
+
projectId?: string
|
|
118
|
+
): Promise<VerificationResult> {
|
|
119
|
+
// Check environment
|
|
120
|
+
if (!SUPABASE_URL || !SUPABASE_SERVICE_KEY) {
|
|
121
|
+
return {
|
|
122
|
+
status: 'error',
|
|
123
|
+
reason: 'Missing SUPABASE_URL or SUPABASE_SERVICE_KEY environment variables',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!API_KEY) {
|
|
128
|
+
return {
|
|
129
|
+
status: 'error',
|
|
130
|
+
reason: 'VIBESCOPE_API_KEY environment variable not set',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY);
|
|
135
|
+
|
|
136
|
+
// Validate API key
|
|
137
|
+
const auth = await validateApiKey(supabase, API_KEY);
|
|
138
|
+
if (!auth) {
|
|
139
|
+
return {
|
|
140
|
+
status: 'error',
|
|
141
|
+
reason: 'Invalid VIBESCOPE_API_KEY',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Auto-detect git URL if not provided
|
|
146
|
+
if (!gitUrl && !projectId) {
|
|
147
|
+
gitUrl = detectGitUrl() || undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Find project
|
|
151
|
+
let projectQuery = supabase
|
|
152
|
+
.from('projects')
|
|
153
|
+
.select('id, name, git_url')
|
|
154
|
+
.eq('user_id', auth.userId);
|
|
155
|
+
|
|
156
|
+
if (projectId) {
|
|
157
|
+
projectQuery = projectQuery.eq('id', projectId);
|
|
158
|
+
} else if (gitUrl) {
|
|
159
|
+
projectQuery = projectQuery.eq('git_url', gitUrl);
|
|
160
|
+
} else {
|
|
161
|
+
return {
|
|
162
|
+
status: 'no_session',
|
|
163
|
+
reason: 'Could not detect git URL and no project_id provided',
|
|
164
|
+
continuation_prompt:
|
|
165
|
+
'Could not detect which project you are working on. Please call start_work_session(git_url: "...") with your repository URL.',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { data: project, error: projectError } = await projectQuery.single();
|
|
170
|
+
|
|
171
|
+
if (projectError || !project) {
|
|
172
|
+
// Project not found - this is OK, might be an untracked repo
|
|
173
|
+
return {
|
|
174
|
+
status: 'compliant',
|
|
175
|
+
reason: `No Vibescope project found for ${gitUrl || projectId}. Untracked repository - exit allowed.`,
|
|
176
|
+
details: {
|
|
177
|
+
session_started: false,
|
|
178
|
+
project_id: null,
|
|
179
|
+
project_name: null,
|
|
180
|
+
git_url: gitUrl || null,
|
|
181
|
+
in_progress_tasks: 0,
|
|
182
|
+
tasks_completed_this_session: 0,
|
|
183
|
+
progress_logs_this_session: 0,
|
|
184
|
+
blockers_logged_this_session: 0,
|
|
185
|
+
session_duration_minutes: null,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check for agent session
|
|
191
|
+
const { data: session } = await supabase
|
|
192
|
+
.from('agent_sessions')
|
|
193
|
+
.select('id, last_synced_at, created_at')
|
|
194
|
+
.eq('api_key_id', auth.apiKeyId)
|
|
195
|
+
.eq('project_id', project.id)
|
|
196
|
+
.single();
|
|
197
|
+
|
|
198
|
+
if (!session) {
|
|
199
|
+
return {
|
|
200
|
+
status: 'non_compliant',
|
|
201
|
+
reason: 'No Vibescope session started for this project',
|
|
202
|
+
continuation_prompt: `[VIBESCOPE ENFORCEMENT] You have not started a Vibescope work session.
|
|
203
|
+
|
|
204
|
+
BEFORE you can exit, you MUST call:
|
|
205
|
+
start_work_session(git_url: "${project.git_url || project.id}")
|
|
206
|
+
|
|
207
|
+
This registers your session and gives you project context.`,
|
|
208
|
+
details: {
|
|
209
|
+
session_started: false,
|
|
210
|
+
project_id: project.id,
|
|
211
|
+
project_name: project.name,
|
|
212
|
+
git_url: project.git_url,
|
|
213
|
+
in_progress_tasks: 0,
|
|
214
|
+
tasks_completed_this_session: 0,
|
|
215
|
+
progress_logs_this_session: 0,
|
|
216
|
+
blockers_logged_this_session: 0,
|
|
217
|
+
session_duration_minutes: null,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const sessionStartTime = new Date(session.created_at);
|
|
223
|
+
const lastSyncTime = new Date(session.last_synced_at);
|
|
224
|
+
const now = new Date();
|
|
225
|
+
const sessionDurationMinutes = Math.round(
|
|
226
|
+
(now.getTime() - sessionStartTime.getTime()) / 60000
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Grace period: if session is less than 1 minute, allow exit
|
|
230
|
+
if (sessionDurationMinutes < 1) {
|
|
231
|
+
return {
|
|
232
|
+
status: 'compliant',
|
|
233
|
+
reason: 'Very short session (< 1 minute) - exit allowed',
|
|
234
|
+
details: {
|
|
235
|
+
session_started: true,
|
|
236
|
+
project_id: project.id,
|
|
237
|
+
project_name: project.name,
|
|
238
|
+
git_url: project.git_url,
|
|
239
|
+
in_progress_tasks: 0,
|
|
240
|
+
tasks_completed_this_session: 0,
|
|
241
|
+
progress_logs_this_session: 0,
|
|
242
|
+
blockers_logged_this_session: 0,
|
|
243
|
+
session_duration_minutes: sessionDurationMinutes,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check for in-progress tasks
|
|
249
|
+
const { data: inProgressTasks } = await supabase
|
|
250
|
+
.from('tasks')
|
|
251
|
+
.select('id, title, progress_percentage')
|
|
252
|
+
.eq('project_id', project.id)
|
|
253
|
+
.eq('status', 'in_progress');
|
|
254
|
+
|
|
255
|
+
const inProgressCount = inProgressTasks?.length || 0;
|
|
256
|
+
|
|
257
|
+
if (inProgressCount > 0) {
|
|
258
|
+
const taskList = (inProgressTasks as TaskInfo[])
|
|
259
|
+
.map((t) => ` - ${t.title} (${t.progress_percentage}% complete)`)
|
|
260
|
+
.join('\n');
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
status: 'non_compliant',
|
|
264
|
+
reason: `You have ${inProgressCount} task(s) still in_progress`,
|
|
265
|
+
continuation_prompt: `[VIBESCOPE ENFORCEMENT] You have ${inProgressCount} task(s) still marked as in_progress:
|
|
266
|
+
|
|
267
|
+
${taskList}
|
|
268
|
+
|
|
269
|
+
You MUST either:
|
|
270
|
+
1. Complete them: complete_task(task_id: "...", summary: "...")
|
|
271
|
+
2. Log why you're stopping: log_progress(project_id: "${project.id}", summary: "Stopping because...")`,
|
|
272
|
+
details: {
|
|
273
|
+
session_started: true,
|
|
274
|
+
project_id: project.id,
|
|
275
|
+
project_name: project.name,
|
|
276
|
+
git_url: project.git_url,
|
|
277
|
+
in_progress_tasks: inProgressCount,
|
|
278
|
+
tasks_completed_this_session: 0,
|
|
279
|
+
progress_logs_this_session: 0,
|
|
280
|
+
blockers_logged_this_session: 0,
|
|
281
|
+
session_duration_minutes: sessionDurationMinutes,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check for activity this session
|
|
287
|
+
const sessionStartIso = sessionStartTime.toISOString();
|
|
288
|
+
|
|
289
|
+
const [completedResult, progressResult, blockerResult] = await Promise.all([
|
|
290
|
+
// Tasks completed after session start
|
|
291
|
+
supabase
|
|
292
|
+
.from('tasks')
|
|
293
|
+
.select('id', { count: 'exact' })
|
|
294
|
+
.eq('project_id', project.id)
|
|
295
|
+
.eq('status', 'completed')
|
|
296
|
+
.gte('completed_at', sessionStartIso),
|
|
297
|
+
|
|
298
|
+
// Progress logs by agent after session start
|
|
299
|
+
supabase
|
|
300
|
+
.from('progress_logs')
|
|
301
|
+
.select('id', { count: 'exact' })
|
|
302
|
+
.eq('project_id', project.id)
|
|
303
|
+
.eq('created_by', 'agent')
|
|
304
|
+
.gte('created_at', sessionStartIso),
|
|
305
|
+
|
|
306
|
+
// Blockers logged by agent after session start
|
|
307
|
+
supabase
|
|
308
|
+
.from('blockers')
|
|
309
|
+
.select('id', { count: 'exact' })
|
|
310
|
+
.eq('project_id', project.id)
|
|
311
|
+
.eq('created_by', 'agent')
|
|
312
|
+
.gte('created_at', sessionStartIso),
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
const tasksCompleted = completedResult.count || 0;
|
|
316
|
+
const progressLogsCount = progressResult.count || 0;
|
|
317
|
+
const blockersLogged = blockerResult.count || 0;
|
|
318
|
+
|
|
319
|
+
// Check if any tracked work was done
|
|
320
|
+
const anyWorkTracked =
|
|
321
|
+
tasksCompleted > 0 || progressLogsCount > 0 || blockersLogged > 0;
|
|
322
|
+
|
|
323
|
+
if (!anyWorkTracked && sessionDurationMinutes >= 5) {
|
|
324
|
+
return {
|
|
325
|
+
status: 'non_compliant',
|
|
326
|
+
reason: `Session active for ${sessionDurationMinutes} minutes but no work was tracked`,
|
|
327
|
+
continuation_prompt: `[VIBESCOPE ENFORCEMENT] Your session has been active for ${sessionDurationMinutes} minutes but no work was tracked.
|
|
328
|
+
|
|
329
|
+
The human is watching the dashboard and will see an empty session.
|
|
330
|
+
|
|
331
|
+
You MUST either:
|
|
332
|
+
1. Pick a task and work on it: get_next_task(project_id: "${project.id}")
|
|
333
|
+
2. Log why you did nothing: log_progress(project_id: "${project.id}", summary: "No work done because...")`,
|
|
334
|
+
details: {
|
|
335
|
+
session_started: true,
|
|
336
|
+
project_id: project.id,
|
|
337
|
+
project_name: project.name,
|
|
338
|
+
git_url: project.git_url,
|
|
339
|
+
in_progress_tasks: 0,
|
|
340
|
+
tasks_completed_this_session: tasksCompleted,
|
|
341
|
+
progress_logs_this_session: progressLogsCount,
|
|
342
|
+
blockers_logged_this_session: blockersLogged,
|
|
343
|
+
session_duration_minutes: sessionDurationMinutes,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// All checks passed - compliant!
|
|
349
|
+
return {
|
|
350
|
+
status: 'compliant',
|
|
351
|
+
reason: 'All tracked work completed properly',
|
|
352
|
+
details: {
|
|
353
|
+
session_started: true,
|
|
354
|
+
project_id: project.id,
|
|
355
|
+
project_name: project.name,
|
|
356
|
+
git_url: project.git_url,
|
|
357
|
+
in_progress_tasks: 0,
|
|
358
|
+
tasks_completed_this_session: tasksCompleted,
|
|
359
|
+
progress_logs_this_session: progressLogsCount,
|
|
360
|
+
blockers_logged_this_session: blockersLogged,
|
|
361
|
+
session_duration_minutes: sessionDurationMinutes,
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ============================================================================
|
|
367
|
+
// CLI Entry Point
|
|
368
|
+
// ============================================================================
|
|
369
|
+
|
|
370
|
+
async function main() {
|
|
371
|
+
const args = process.argv.slice(2);
|
|
372
|
+
const command = args[0];
|
|
373
|
+
|
|
374
|
+
if (command === 'verify') {
|
|
375
|
+
// Parse --git-url and --project-id flags
|
|
376
|
+
let gitUrl: string | undefined;
|
|
377
|
+
let projectId: string | undefined;
|
|
378
|
+
|
|
379
|
+
for (let i = 1; i < args.length; i++) {
|
|
380
|
+
if (args[i] === '--git-url' && args[i + 1]) {
|
|
381
|
+
gitUrl = args[++i];
|
|
382
|
+
} else if (args[i] === '--project-id' && args[i + 1]) {
|
|
383
|
+
projectId = args[++i];
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const result = await verify(gitUrl, projectId);
|
|
388
|
+
console.log(JSON.stringify(result, null, 2));
|
|
389
|
+
|
|
390
|
+
// Exit codes: 0=compliant, 1=non-compliant, 2=error
|
|
391
|
+
if (result.status === 'compliant') {
|
|
392
|
+
process.exit(0);
|
|
393
|
+
} else if (result.status === 'error') {
|
|
394
|
+
process.exit(2);
|
|
395
|
+
} else {
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
} else if (command === 'help' || command === '--help' || command === '-h') {
|
|
399
|
+
console.log(`
|
|
400
|
+
Vibescope CLI - Enforcement verification tool
|
|
401
|
+
|
|
402
|
+
Usage:
|
|
403
|
+
vibescope-cli verify [options] Check Vibescope compliance before exit
|
|
404
|
+
|
|
405
|
+
Options:
|
|
406
|
+
--git-url <url> Git repository URL (auto-detected if not provided)
|
|
407
|
+
--project-id <id> Vibescope project UUID
|
|
408
|
+
|
|
409
|
+
Exit Codes:
|
|
410
|
+
0 Compliant - agent can exit
|
|
411
|
+
1 Non-compliant - agent should continue work
|
|
412
|
+
2 Error - allow exit with warning
|
|
413
|
+
|
|
414
|
+
Environment Variables:
|
|
415
|
+
VIBESCOPE_API_KEY Required - Your Vibescope API key
|
|
416
|
+
SUPABASE_URL Required - Supabase project URL
|
|
417
|
+
SUPABASE_SERVICE_KEY Required - Supabase service role key
|
|
418
|
+
`);
|
|
419
|
+
process.exit(0);
|
|
420
|
+
} else {
|
|
421
|
+
console.error('Usage: vibescope-cli verify [--git-url <url>] [--project-id <id>]');
|
|
422
|
+
console.error(' vibescope-cli --help');
|
|
423
|
+
process.exit(2);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Only run main when executed directly (not when imported for testing)
|
|
428
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]?.replace(/\\/g, '/')}`;
|
|
429
|
+
if (isMainModule || process.argv[1]?.endsWith('cli.js')) {
|
|
430
|
+
main().catch((err) => {
|
|
431
|
+
console.error(
|
|
432
|
+
JSON.stringify({
|
|
433
|
+
status: 'error',
|
|
434
|
+
reason: err instanceof Error ? err.message : 'Unknown error',
|
|
435
|
+
})
|
|
436
|
+
);
|
|
437
|
+
process.exit(2);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Test Utilities
|
|
3
|
+
*
|
|
4
|
+
* Common mock factories and test helpers used across handler tests.
|
|
5
|
+
* This eliminates ~85 lines of duplicate code per test file.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { vi } from 'vitest';
|
|
9
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
10
|
+
import type { HandlerContext, TokenUsage } from './types.js';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Mock Supabase Factory
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export interface MockSupabaseOverrides {
|
|
17
|
+
selectResult?: { data: unknown; error: unknown };
|
|
18
|
+
insertResult?: { data: unknown; error: unknown };
|
|
19
|
+
updateResult?: { data: unknown; error: unknown };
|
|
20
|
+
deleteResult?: { data: unknown; error: unknown };
|
|
21
|
+
sessionsResult?: { data: unknown; error: unknown };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a mock Supabase client for testing.
|
|
26
|
+
*
|
|
27
|
+
* The mock tracks which operation is being performed and returns
|
|
28
|
+
* the appropriate result from overrides.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const supabase = createMockSupabase({
|
|
33
|
+
* insertResult: { data: { id: 'new-id' }, error: null }
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function createMockSupabase(overrides: MockSupabaseOverrides = {}) {
|
|
38
|
+
const defaultResult = { data: null, error: null };
|
|
39
|
+
|
|
40
|
+
// Use an object to track state so it persists across all mock function calls
|
|
41
|
+
const state = {
|
|
42
|
+
currentOperation: 'select' as string,
|
|
43
|
+
currentTable: '' as string,
|
|
44
|
+
insertThenSelect: false,
|
|
45
|
+
updateCalled: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const mock = {
|
|
49
|
+
from: vi.fn((table: string) => {
|
|
50
|
+
state.currentTable = table;
|
|
51
|
+
// Reset state for new query chain
|
|
52
|
+
state.currentOperation = 'select';
|
|
53
|
+
state.insertThenSelect = false;
|
|
54
|
+
state.updateCalled = false;
|
|
55
|
+
return mock;
|
|
56
|
+
}),
|
|
57
|
+
select: vi.fn(() => {
|
|
58
|
+
if (state.currentOperation === 'insert') {
|
|
59
|
+
state.insertThenSelect = true;
|
|
60
|
+
} else if (!state.updateCalled) {
|
|
61
|
+
state.currentOperation = 'select';
|
|
62
|
+
state.insertThenSelect = false;
|
|
63
|
+
}
|
|
64
|
+
return mock;
|
|
65
|
+
}),
|
|
66
|
+
insert: vi.fn(() => {
|
|
67
|
+
state.currentOperation = 'insert';
|
|
68
|
+
state.insertThenSelect = false;
|
|
69
|
+
return mock;
|
|
70
|
+
}),
|
|
71
|
+
update: vi.fn(() => {
|
|
72
|
+
state.currentOperation = 'update';
|
|
73
|
+
state.updateCalled = true;
|
|
74
|
+
state.insertThenSelect = false;
|
|
75
|
+
return mock;
|
|
76
|
+
}),
|
|
77
|
+
delete: vi.fn(() => {
|
|
78
|
+
state.currentOperation = 'delete';
|
|
79
|
+
state.insertThenSelect = false;
|
|
80
|
+
return mock;
|
|
81
|
+
}),
|
|
82
|
+
eq: vi.fn().mockReturnThis(),
|
|
83
|
+
neq: vi.fn().mockReturnThis(),
|
|
84
|
+
in: vi.fn().mockReturnThis(),
|
|
85
|
+
is: vi.fn().mockReturnThis(),
|
|
86
|
+
not: vi.fn().mockReturnThis(),
|
|
87
|
+
or: vi.fn().mockReturnThis(),
|
|
88
|
+
gt: vi.fn().mockReturnThis(),
|
|
89
|
+
gte: vi.fn().mockReturnThis(),
|
|
90
|
+
lte: vi.fn().mockReturnThis(),
|
|
91
|
+
lt: vi.fn().mockReturnThis(),
|
|
92
|
+
order: vi.fn().mockReturnThis(),
|
|
93
|
+
limit: vi.fn().mockReturnThis(),
|
|
94
|
+
single: vi.fn(() => {
|
|
95
|
+
if (state.currentOperation === 'insert' || state.insertThenSelect) {
|
|
96
|
+
return Promise.resolve(overrides.insertResult ?? defaultResult);
|
|
97
|
+
}
|
|
98
|
+
if (state.updateCalled) {
|
|
99
|
+
return Promise.resolve(overrides.updateResult ?? defaultResult);
|
|
100
|
+
}
|
|
101
|
+
if (state.currentOperation === 'select') {
|
|
102
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult);
|
|
103
|
+
}
|
|
104
|
+
if (state.currentOperation === 'update') {
|
|
105
|
+
return Promise.resolve(overrides.updateResult ?? defaultResult);
|
|
106
|
+
}
|
|
107
|
+
return Promise.resolve(defaultResult);
|
|
108
|
+
}),
|
|
109
|
+
maybeSingle: vi.fn(() => {
|
|
110
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult);
|
|
111
|
+
}),
|
|
112
|
+
then: vi.fn((resolve: (value: unknown) => void) => {
|
|
113
|
+
// Handle special table cases
|
|
114
|
+
if (state.currentTable === 'agent_sessions' && overrides.sessionsResult) {
|
|
115
|
+
return Promise.resolve(overrides.sessionsResult).then(resolve);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (state.currentOperation === 'insert' || state.insertThenSelect) {
|
|
119
|
+
return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
|
|
120
|
+
}
|
|
121
|
+
if (state.updateCalled) {
|
|
122
|
+
return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
|
|
123
|
+
}
|
|
124
|
+
if (state.currentOperation === 'select') {
|
|
125
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
|
|
126
|
+
}
|
|
127
|
+
if (state.currentOperation === 'update') {
|
|
128
|
+
return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
|
|
129
|
+
}
|
|
130
|
+
if (state.currentOperation === 'delete') {
|
|
131
|
+
return Promise.resolve(overrides.deleteResult ?? defaultResult).then(resolve);
|
|
132
|
+
}
|
|
133
|
+
return Promise.resolve(defaultResult).then(resolve);
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return mock as unknown as SupabaseClient;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Mock Handler Context Factory
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
export interface MockContextOptions {
|
|
145
|
+
sessionId?: string | null;
|
|
146
|
+
userId?: string;
|
|
147
|
+
apiKeyId?: string;
|
|
148
|
+
instanceId?: string;
|
|
149
|
+
persona?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Create a mock HandlerContext for testing.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* const ctx = createMockContext(supabase, { sessionId: 'test-session' });
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function createMockContext(
|
|
161
|
+
supabase: SupabaseClient,
|
|
162
|
+
options: MockContextOptions = {}
|
|
163
|
+
): HandlerContext {
|
|
164
|
+
const defaultTokenUsage: TokenUsage = {
|
|
165
|
+
callCount: 5,
|
|
166
|
+
totalTokens: 2500,
|
|
167
|
+
byTool: {},
|
|
168
|
+
byModel: {},
|
|
169
|
+
currentModel: null,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const sessionId = 'sessionId' in options ? (options.sessionId ?? null) : 'session-123';
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
supabase,
|
|
176
|
+
auth: {
|
|
177
|
+
userId: options.userId ?? 'user-123',
|
|
178
|
+
apiKeyId: options.apiKeyId ?? 'api-key-123',
|
|
179
|
+
scope: 'personal' as const,
|
|
180
|
+
},
|
|
181
|
+
session: {
|
|
182
|
+
instanceId: options.instanceId ?? 'instance-abc',
|
|
183
|
+
currentSessionId: sessionId,
|
|
184
|
+
currentPersona: options.persona ?? 'Wave',
|
|
185
|
+
tokenUsage: defaultTokenUsage,
|
|
186
|
+
},
|
|
187
|
+
updateSession: vi.fn(),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// Test Data Generators
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generate a valid UUID for testing.
|
|
197
|
+
*/
|
|
198
|
+
export function testUUID(): string {
|
|
199
|
+
return '123e4567-e89b-12d3-a456-426614174000';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Generate a random-ish UUID for testing (deterministic based on seed).
|
|
204
|
+
*/
|
|
205
|
+
export function testUUIDSeeded(seed: number): string {
|
|
206
|
+
const hex = seed.toString(16).padStart(8, '0');
|
|
207
|
+
return `${hex}-e89b-12d3-a456-426614174000`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a mock timestamp for consistent testing.
|
|
212
|
+
*/
|
|
213
|
+
export function testTimestamp(offsetMinutes = 0): string {
|
|
214
|
+
const date = new Date('2025-01-14T12:00:00Z');
|
|
215
|
+
date.setMinutes(date.getMinutes() + offsetMinutes);
|
|
216
|
+
return date.toISOString();
|
|
217
|
+
}
|