@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.
Files changed (170) hide show
  1. package/README.md +98 -0
  2. package/dist/cli.d.ts +34 -0
  3. package/dist/cli.js +356 -0
  4. package/dist/cli.test.d.ts +1 -0
  5. package/dist/cli.test.js +367 -0
  6. package/dist/handlers/__test-utils__.d.ts +72 -0
  7. package/dist/handlers/__test-utils__.js +176 -0
  8. package/dist/handlers/blockers.d.ts +18 -0
  9. package/dist/handlers/blockers.js +81 -0
  10. package/dist/handlers/bodies-of-work.d.ts +34 -0
  11. package/dist/handlers/bodies-of-work.js +614 -0
  12. package/dist/handlers/checkouts.d.ts +37 -0
  13. package/dist/handlers/checkouts.js +377 -0
  14. package/dist/handlers/cost.d.ts +39 -0
  15. package/dist/handlers/cost.js +247 -0
  16. package/dist/handlers/decisions.d.ts +16 -0
  17. package/dist/handlers/decisions.js +64 -0
  18. package/dist/handlers/deployment.d.ts +36 -0
  19. package/dist/handlers/deployment.js +1062 -0
  20. package/dist/handlers/discovery.d.ts +14 -0
  21. package/dist/handlers/discovery.js +870 -0
  22. package/dist/handlers/fallback.d.ts +18 -0
  23. package/dist/handlers/fallback.js +216 -0
  24. package/dist/handlers/findings.d.ts +18 -0
  25. package/dist/handlers/findings.js +110 -0
  26. package/dist/handlers/git-issues.d.ts +22 -0
  27. package/dist/handlers/git-issues.js +247 -0
  28. package/dist/handlers/ideas.d.ts +19 -0
  29. package/dist/handlers/ideas.js +188 -0
  30. package/dist/handlers/index.d.ts +29 -0
  31. package/dist/handlers/index.js +65 -0
  32. package/dist/handlers/knowledge-query.d.ts +22 -0
  33. package/dist/handlers/knowledge-query.js +253 -0
  34. package/dist/handlers/knowledge.d.ts +12 -0
  35. package/dist/handlers/knowledge.js +108 -0
  36. package/dist/handlers/milestones.d.ts +20 -0
  37. package/dist/handlers/milestones.js +179 -0
  38. package/dist/handlers/organizations.d.ts +36 -0
  39. package/dist/handlers/organizations.js +428 -0
  40. package/dist/handlers/progress.d.ts +14 -0
  41. package/dist/handlers/progress.js +149 -0
  42. package/dist/handlers/project.d.ts +20 -0
  43. package/dist/handlers/project.js +278 -0
  44. package/dist/handlers/requests.d.ts +16 -0
  45. package/dist/handlers/requests.js +131 -0
  46. package/dist/handlers/roles.d.ts +30 -0
  47. package/dist/handlers/roles.js +281 -0
  48. package/dist/handlers/session.d.ts +20 -0
  49. package/dist/handlers/session.js +791 -0
  50. package/dist/handlers/tasks.d.ts +52 -0
  51. package/dist/handlers/tasks.js +1111 -0
  52. package/dist/handlers/tasks.test.d.ts +1 -0
  53. package/dist/handlers/tasks.test.js +431 -0
  54. package/dist/handlers/types.d.ts +94 -0
  55. package/dist/handlers/types.js +1 -0
  56. package/dist/handlers/validation.d.ts +16 -0
  57. package/dist/handlers/validation.js +188 -0
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.js +2707 -0
  60. package/dist/knowledge.d.ts +6 -0
  61. package/dist/knowledge.js +121 -0
  62. package/dist/tools.d.ts +2 -0
  63. package/dist/tools.js +2498 -0
  64. package/dist/utils.d.ts +149 -0
  65. package/dist/utils.js +317 -0
  66. package/dist/utils.test.d.ts +1 -0
  67. package/dist/utils.test.js +532 -0
  68. package/dist/validators.d.ts +35 -0
  69. package/dist/validators.js +111 -0
  70. package/dist/validators.test.d.ts +1 -0
  71. package/dist/validators.test.js +176 -0
  72. package/package.json +44 -0
  73. package/src/cli.test.ts +442 -0
  74. package/src/cli.ts +439 -0
  75. package/src/handlers/__test-utils__.ts +217 -0
  76. package/src/handlers/blockers.test.ts +390 -0
  77. package/src/handlers/blockers.ts +110 -0
  78. package/src/handlers/bodies-of-work.test.ts +1276 -0
  79. package/src/handlers/bodies-of-work.ts +783 -0
  80. package/src/handlers/cost.test.ts +436 -0
  81. package/src/handlers/cost.ts +322 -0
  82. package/src/handlers/decisions.test.ts +401 -0
  83. package/src/handlers/decisions.ts +86 -0
  84. package/src/handlers/deployment.test.ts +516 -0
  85. package/src/handlers/deployment.ts +1289 -0
  86. package/src/handlers/discovery.test.ts +254 -0
  87. package/src/handlers/discovery.ts +969 -0
  88. package/src/handlers/fallback.test.ts +687 -0
  89. package/src/handlers/fallback.ts +260 -0
  90. package/src/handlers/findings.test.ts +565 -0
  91. package/src/handlers/findings.ts +153 -0
  92. package/src/handlers/ideas.test.ts +753 -0
  93. package/src/handlers/ideas.ts +247 -0
  94. package/src/handlers/index.ts +69 -0
  95. package/src/handlers/milestones.test.ts +584 -0
  96. package/src/handlers/milestones.ts +217 -0
  97. package/src/handlers/organizations.test.ts +997 -0
  98. package/src/handlers/organizations.ts +550 -0
  99. package/src/handlers/progress.test.ts +369 -0
  100. package/src/handlers/progress.ts +188 -0
  101. package/src/handlers/project.test.ts +562 -0
  102. package/src/handlers/project.ts +352 -0
  103. package/src/handlers/requests.test.ts +531 -0
  104. package/src/handlers/requests.ts +150 -0
  105. package/src/handlers/session.test.ts +459 -0
  106. package/src/handlers/session.ts +912 -0
  107. package/src/handlers/tasks.test.ts +602 -0
  108. package/src/handlers/tasks.ts +1393 -0
  109. package/src/handlers/types.ts +88 -0
  110. package/src/handlers/validation.test.ts +880 -0
  111. package/src/handlers/validation.ts +223 -0
  112. package/src/index.ts +3205 -0
  113. package/src/knowledge.ts +132 -0
  114. package/src/tmpclaude-0078-cwd +1 -0
  115. package/src/tmpclaude-0ee1-cwd +1 -0
  116. package/src/tmpclaude-2dd5-cwd +1 -0
  117. package/src/tmpclaude-344c-cwd +1 -0
  118. package/src/tmpclaude-3860-cwd +1 -0
  119. package/src/tmpclaude-4b63-cwd +1 -0
  120. package/src/tmpclaude-5c73-cwd +1 -0
  121. package/src/tmpclaude-5ee3-cwd +1 -0
  122. package/src/tmpclaude-6795-cwd +1 -0
  123. package/src/tmpclaude-709e-cwd +1 -0
  124. package/src/tmpclaude-9839-cwd +1 -0
  125. package/src/tmpclaude-d829-cwd +1 -0
  126. package/src/tmpclaude-e072-cwd +1 -0
  127. package/src/tmpclaude-f6ee-cwd +1 -0
  128. package/src/utils.test.ts +681 -0
  129. package/src/utils.ts +375 -0
  130. package/src/validators.test.ts +223 -0
  131. package/src/validators.ts +122 -0
  132. package/tmpclaude-0439-cwd +1 -0
  133. package/tmpclaude-132f-cwd +1 -0
  134. package/tmpclaude-15bb-cwd +1 -0
  135. package/tmpclaude-165a-cwd +1 -0
  136. package/tmpclaude-1ba9-cwd +1 -0
  137. package/tmpclaude-21a3-cwd +1 -0
  138. package/tmpclaude-2a38-cwd +1 -0
  139. package/tmpclaude-2adf-cwd +1 -0
  140. package/tmpclaude-2f56-cwd +1 -0
  141. package/tmpclaude-3626-cwd +1 -0
  142. package/tmpclaude-3727-cwd +1 -0
  143. package/tmpclaude-40bc-cwd +1 -0
  144. package/tmpclaude-436f-cwd +1 -0
  145. package/tmpclaude-4783-cwd +1 -0
  146. package/tmpclaude-4b6d-cwd +1 -0
  147. package/tmpclaude-4ba4-cwd +1 -0
  148. package/tmpclaude-51e6-cwd +1 -0
  149. package/tmpclaude-5ecf-cwd +1 -0
  150. package/tmpclaude-6f97-cwd +1 -0
  151. package/tmpclaude-7fb2-cwd +1 -0
  152. package/tmpclaude-825c-cwd +1 -0
  153. package/tmpclaude-8baf-cwd +1 -0
  154. package/tmpclaude-8d9f-cwd +1 -0
  155. package/tmpclaude-975c-cwd +1 -0
  156. package/tmpclaude-9983-cwd +1 -0
  157. package/tmpclaude-a045-cwd +1 -0
  158. package/tmpclaude-ac4a-cwd +1 -0
  159. package/tmpclaude-b593-cwd +1 -0
  160. package/tmpclaude-b891-cwd +1 -0
  161. package/tmpclaude-c032-cwd +1 -0
  162. package/tmpclaude-cf43-cwd +1 -0
  163. package/tmpclaude-d040-cwd +1 -0
  164. package/tmpclaude-dcdd-cwd +1 -0
  165. package/tmpclaude-dcee-cwd +1 -0
  166. package/tmpclaude-e16b-cwd +1 -0
  167. package/tmpclaude-ecd2-cwd +1 -0
  168. package/tmpclaude-f48d-cwd +1 -0
  169. package/tsconfig.json +16 -0
  170. 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
+ }