@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
@@ -0,0 +1,377 @@
1
+ /**
2
+ * File Checkout Handlers
3
+ *
4
+ * Handles file checkout/checkin for multi-agent coordination:
5
+ * - checkout_file: Reserve a file for editing
6
+ * - checkin_file: Release a file after editing
7
+ * - get_file_checkouts: List active and recent checkouts
8
+ * - abandon_checkout: Force-release a checkout
9
+ */
10
+ import { validateRequired, validateUUID } from '../validators.js';
11
+ /**
12
+ * Verify the user owns or has access to the project
13
+ * This is needed because MCP server uses service_role which bypasses RLS
14
+ */
15
+ async function verifyProjectAccess(ctx, projectId) {
16
+ const { supabase, auth } = ctx;
17
+ // Check if user owns the project
18
+ const { data: ownedProject } = await supabase
19
+ .from('projects')
20
+ .select('id')
21
+ .eq('id', projectId)
22
+ .eq('user_id', auth.userId)
23
+ .single();
24
+ if (ownedProject)
25
+ return;
26
+ // Check if project is shared with user's organization (for org-scoped keys)
27
+ if (auth.scope === 'organization' && auth.organizationId) {
28
+ const { data: sharedProject } = await supabase
29
+ .from('project_shares')
30
+ .select('project_id')
31
+ .eq('project_id', projectId)
32
+ .eq('organization_id', auth.organizationId)
33
+ .single();
34
+ if (sharedProject)
35
+ return;
36
+ }
37
+ throw new Error('Project not found or access denied');
38
+ }
39
+ /**
40
+ * Checkout a file for editing
41
+ * Prevents other agents from editing the same file
42
+ */
43
+ export const checkoutFile = async (args, ctx) => {
44
+ const { project_id, file_path, reason } = args;
45
+ validateRequired(project_id, 'project_id');
46
+ validateUUID(project_id, 'project_id');
47
+ validateRequired(file_path, 'file_path');
48
+ // Verify user has access to this project
49
+ await verifyProjectAccess(ctx, project_id);
50
+ const { supabase, session } = ctx;
51
+ // Check if file is already checked out
52
+ const { data: existing } = await supabase
53
+ .from('file_checkouts')
54
+ .select('id, checked_out_by_session_id, checked_out_at')
55
+ .eq('project_id', project_id)
56
+ .eq('file_path', file_path)
57
+ .eq('status', 'checked_out')
58
+ .single();
59
+ if (existing) {
60
+ // Get session info for better error message
61
+ const { data: sessionInfo } = await supabase
62
+ .from('agent_sessions')
63
+ .select('persona, instance_id')
64
+ .eq('id', existing.checked_out_by_session_id)
65
+ .single();
66
+ const checkedOutBy = sessionInfo?.persona || sessionInfo?.instance_id?.slice(0, 8) || 'another agent';
67
+ const minutesAgo = Math.round((Date.now() - new Date(existing.checked_out_at).getTime()) / 60000);
68
+ return {
69
+ result: {
70
+ success: false,
71
+ error: 'File already checked out',
72
+ checked_out_by: checkedOutBy,
73
+ checked_out_minutes_ago: minutesAgo,
74
+ checkout_id: existing.id,
75
+ hint: 'Wait for the file to be checked in, or use abandon_checkout if the session is stale',
76
+ },
77
+ };
78
+ }
79
+ // Create the checkout
80
+ const { data, error } = await supabase
81
+ .from('file_checkouts')
82
+ .insert({
83
+ project_id,
84
+ file_path,
85
+ checked_out_by_session_id: session.currentSessionId,
86
+ checkout_reason: reason || null,
87
+ })
88
+ .select('id')
89
+ .single();
90
+ if (error) {
91
+ // Handle unique constraint violation (race condition)
92
+ if (error.code === '23505') {
93
+ return {
94
+ result: {
95
+ success: false,
96
+ error: 'File was just checked out by another agent',
97
+ hint: 'Retry after a moment or choose a different file',
98
+ },
99
+ };
100
+ }
101
+ throw new Error(`Failed to checkout file: ${error.message}`);
102
+ }
103
+ return {
104
+ result: {
105
+ success: true,
106
+ checkout_id: data.id,
107
+ file_path,
108
+ message: `File checked out successfully. Remember to checkin when done.`,
109
+ },
110
+ };
111
+ };
112
+ /**
113
+ * Checkin a file after editing
114
+ * Releases the file for other agents to edit
115
+ */
116
+ export const checkinFile = async (args, ctx) => {
117
+ const { checkout_id, file_path, project_id, summary } = args;
118
+ const { supabase, session } = ctx;
119
+ // Allow checkin by checkout_id OR by file_path + project_id
120
+ let checkoutId = checkout_id;
121
+ let verifiedProjectId;
122
+ if (!checkoutId) {
123
+ if (!file_path || !project_id) {
124
+ throw new Error('Either checkout_id or both file_path and project_id are required');
125
+ }
126
+ validateUUID(project_id, 'project_id');
127
+ // Verify user has access to this project
128
+ await verifyProjectAccess(ctx, project_id);
129
+ verifiedProjectId = project_id;
130
+ // Find the checkout by file path
131
+ const { data: checkout, error: findError } = await supabase
132
+ .from('file_checkouts')
133
+ .select('id')
134
+ .eq('project_id', project_id)
135
+ .eq('file_path', file_path)
136
+ .eq('status', 'checked_out')
137
+ .single();
138
+ if (findError || !checkout) {
139
+ return {
140
+ result: {
141
+ success: false,
142
+ error: 'No active checkout found for this file',
143
+ hint: 'The file may not be checked out or was already checked in',
144
+ },
145
+ };
146
+ }
147
+ checkoutId = checkout.id;
148
+ }
149
+ else {
150
+ validateUUID(checkoutId, 'checkout_id');
151
+ // Get checkout's project_id to verify access
152
+ const { data: checkout } = await supabase
153
+ .from('file_checkouts')
154
+ .select('project_id')
155
+ .eq('id', checkoutId)
156
+ .single();
157
+ if (!checkout) {
158
+ return {
159
+ result: {
160
+ success: false,
161
+ error: 'Checkout not found',
162
+ },
163
+ };
164
+ }
165
+ // Verify user has access to this project
166
+ await verifyProjectAccess(ctx, checkout.project_id);
167
+ verifiedProjectId = checkout.project_id;
168
+ }
169
+ // Perform the checkin
170
+ const { data, error } = await supabase
171
+ .from('file_checkouts')
172
+ .update({
173
+ status: 'checked_in',
174
+ checked_in_at: new Date().toISOString(),
175
+ checked_in_by_session_id: session.currentSessionId,
176
+ checkin_summary: summary || null,
177
+ updated_at: new Date().toISOString(),
178
+ })
179
+ .eq('id', checkoutId)
180
+ .eq('status', 'checked_out')
181
+ .select('file_path')
182
+ .single();
183
+ if (error || !data) {
184
+ return {
185
+ result: {
186
+ success: false,
187
+ error: 'Failed to checkin file',
188
+ hint: 'The checkout may have already been checked in or abandoned',
189
+ },
190
+ };
191
+ }
192
+ return {
193
+ result: {
194
+ success: true,
195
+ checkout_id: checkoutId,
196
+ file_path: data.file_path,
197
+ message: 'File checked in successfully',
198
+ },
199
+ };
200
+ };
201
+ /**
202
+ * Get file checkouts for a project
203
+ */
204
+ export const getFileCheckouts = async (args, ctx) => {
205
+ const { project_id, status, include_completed = false, limit = 50 } = args;
206
+ validateRequired(project_id, 'project_id');
207
+ validateUUID(project_id, 'project_id');
208
+ // Verify user has access to this project
209
+ await verifyProjectAccess(ctx, project_id);
210
+ const { supabase } = ctx;
211
+ let query = supabase
212
+ .from('file_checkouts')
213
+ .select(`
214
+ id,
215
+ file_path,
216
+ status,
217
+ checkout_reason,
218
+ checkin_summary,
219
+ checked_out_at,
220
+ checked_in_at,
221
+ checked_out_by_session_id,
222
+ checked_in_by_session_id
223
+ `)
224
+ .eq('project_id', project_id)
225
+ .order('checked_out_at', { ascending: false })
226
+ .limit(limit);
227
+ if (status) {
228
+ query = query.eq('status', status);
229
+ }
230
+ else if (!include_completed) {
231
+ query = query.eq('status', 'checked_out');
232
+ }
233
+ const { data, error } = await query;
234
+ if (error)
235
+ throw new Error(`Failed to fetch checkouts: ${error.message}`);
236
+ // Get session info for active checkouts
237
+ const sessionIds = [...new Set((data || [])
238
+ .map(c => c.checked_out_by_session_id)
239
+ .filter(Boolean))];
240
+ let sessionMap = {};
241
+ if (sessionIds.length > 0) {
242
+ const { data: sessions } = await supabase
243
+ .from('agent_sessions')
244
+ .select('id, persona, instance_id')
245
+ .in('id', sessionIds);
246
+ sessionMap = (sessions || []).reduce((acc, s) => {
247
+ acc[s.id] = { persona: s.persona, instance_id: s.instance_id };
248
+ return acc;
249
+ }, {});
250
+ }
251
+ const checkouts = (data || []).map(c => ({
252
+ ...c,
253
+ checked_out_by: c.checked_out_by_session_id
254
+ ? sessionMap[c.checked_out_by_session_id]?.persona ||
255
+ sessionMap[c.checked_out_by_session_id]?.instance_id?.slice(0, 8) ||
256
+ 'unknown'
257
+ : null,
258
+ }));
259
+ const activeCount = checkouts.filter(c => c.status === 'checked_out').length;
260
+ return {
261
+ result: {
262
+ checkouts,
263
+ active_count: activeCount,
264
+ total_count: checkouts.length,
265
+ },
266
+ };
267
+ };
268
+ /**
269
+ * Abandon a checkout (force release)
270
+ * Use when the original agent session died or is stuck
271
+ */
272
+ export const abandonCheckout = async (args, ctx) => {
273
+ const { checkout_id, reason } = args;
274
+ validateRequired(checkout_id, 'checkout_id');
275
+ validateUUID(checkout_id, 'checkout_id');
276
+ const { supabase, session } = ctx;
277
+ // First get the checkout to verify project access
278
+ const { data: checkout } = await supabase
279
+ .from('file_checkouts')
280
+ .select('project_id')
281
+ .eq('id', checkout_id)
282
+ .single();
283
+ if (!checkout) {
284
+ return {
285
+ result: {
286
+ success: false,
287
+ error: 'Checkout not found',
288
+ },
289
+ };
290
+ }
291
+ // Verify user has access to this project
292
+ await verifyProjectAccess(ctx, checkout.project_id);
293
+ const { data, error } = await supabase
294
+ .from('file_checkouts')
295
+ .update({
296
+ status: 'abandoned',
297
+ checkin_summary: reason || 'Checkout abandoned',
298
+ checked_in_at: new Date().toISOString(),
299
+ checked_in_by_session_id: session.currentSessionId,
300
+ updated_at: new Date().toISOString(),
301
+ })
302
+ .eq('id', checkout_id)
303
+ .eq('status', 'checked_out')
304
+ .select('file_path')
305
+ .single();
306
+ if (error || !data) {
307
+ return {
308
+ result: {
309
+ success: false,
310
+ error: 'Failed to abandon checkout',
311
+ hint: 'The checkout may have already been checked in or abandoned',
312
+ },
313
+ };
314
+ }
315
+ return {
316
+ result: {
317
+ success: true,
318
+ checkout_id,
319
+ file_path: data.file_path,
320
+ message: 'Checkout abandoned successfully. File is now available.',
321
+ },
322
+ };
323
+ };
324
+ /**
325
+ * Check if a file is available for checkout
326
+ */
327
+ export const isFileAvailable = async (args, ctx) => {
328
+ const { project_id, file_path } = args;
329
+ validateRequired(project_id, 'project_id');
330
+ validateUUID(project_id, 'project_id');
331
+ validateRequired(file_path, 'file_path');
332
+ // Verify user has access to this project
333
+ await verifyProjectAccess(ctx, project_id);
334
+ const { supabase } = ctx;
335
+ const { data } = await supabase
336
+ .from('file_checkouts')
337
+ .select('id, checked_out_by_session_id, checked_out_at')
338
+ .eq('project_id', project_id)
339
+ .eq('file_path', file_path)
340
+ .eq('status', 'checked_out')
341
+ .single();
342
+ if (!data) {
343
+ return {
344
+ result: {
345
+ available: true,
346
+ file_path,
347
+ },
348
+ };
349
+ }
350
+ // Get session info
351
+ const { data: sessionInfo } = await supabase
352
+ .from('agent_sessions')
353
+ .select('persona, instance_id')
354
+ .eq('id', data.checked_out_by_session_id)
355
+ .single();
356
+ const checkedOutBy = sessionInfo?.persona || sessionInfo?.instance_id?.slice(0, 8) || 'another agent';
357
+ const minutesAgo = Math.round((Date.now() - new Date(data.checked_out_at).getTime()) / 60000);
358
+ return {
359
+ result: {
360
+ available: false,
361
+ file_path,
362
+ checked_out_by: checkedOutBy,
363
+ checked_out_minutes_ago: minutesAgo,
364
+ checkout_id: data.id,
365
+ },
366
+ };
367
+ };
368
+ /**
369
+ * Checkout handlers registry
370
+ */
371
+ export const checkoutHandlers = {
372
+ checkout_file: checkoutFile,
373
+ checkin_file: checkinFile,
374
+ get_file_checkouts: getFileCheckouts,
375
+ abandon_checkout: abandonCheckout,
376
+ is_file_available: isFileAvailable,
377
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Cost Handlers
3
+ *
4
+ * Handles cost monitoring and alerts:
5
+ * - get_cost_summary
6
+ * - get_cost_alerts
7
+ * - add_cost_alert
8
+ * - update_cost_alert
9
+ * - delete_cost_alert
10
+ */
11
+ import type { Handler, HandlerRegistry } from './types.js';
12
+ /**
13
+ * Get cost summary for a project (daily, weekly, or monthly)
14
+ */
15
+ export declare const getCostSummary: Handler;
16
+ /**
17
+ * Get cost alerts for the current user
18
+ */
19
+ export declare const getCostAlerts: Handler;
20
+ /**
21
+ * Add a cost alert
22
+ */
23
+ export declare const addCostAlert: Handler;
24
+ /**
25
+ * Update a cost alert
26
+ */
27
+ export declare const updateCostAlert: Handler;
28
+ /**
29
+ * Delete a cost alert
30
+ */
31
+ export declare const deleteCostAlert: Handler;
32
+ /**
33
+ * Get task costs for a project
34
+ */
35
+ export declare const getTaskCosts: Handler;
36
+ /**
37
+ * Cost handlers registry
38
+ */
39
+ export declare const costHandlers: HandlerRegistry;
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Cost Handlers
3
+ *
4
+ * Handles cost monitoring and alerts:
5
+ * - get_cost_summary
6
+ * - get_cost_alerts
7
+ * - add_cost_alert
8
+ * - update_cost_alert
9
+ * - delete_cost_alert
10
+ */
11
+ /**
12
+ * Get cost summary for a project (daily, weekly, or monthly)
13
+ */
14
+ export const getCostSummary = async (args, ctx) => {
15
+ const { project_id, period = 'daily', limit = 30 } = args;
16
+ const { supabase } = ctx;
17
+ if (!project_id) {
18
+ return {
19
+ result: { error: 'project_id is required' },
20
+ isError: true,
21
+ };
22
+ }
23
+ // Select the appropriate view based on period
24
+ const viewName = `${period}_cost_summary`;
25
+ const { data, error } = await supabase
26
+ .from(viewName)
27
+ .select('*')
28
+ .eq('project_id', project_id)
29
+ .order(period === 'daily' ? 'date' : period === 'weekly' ? 'week_start' : 'month_start', { ascending: false })
30
+ .limit(limit);
31
+ if (error) {
32
+ return {
33
+ result: { error: `Failed to get cost summary: ${error.message}` },
34
+ isError: true,
35
+ };
36
+ }
37
+ // Calculate totals
38
+ const totals = (data || []).reduce((acc, row) => ({
39
+ sessions: acc.sessions + (row.session_count || 0),
40
+ tokens: acc.tokens + (row.total_tokens || 0),
41
+ calls: acc.calls + (row.total_calls || 0),
42
+ cost: acc.cost + parseFloat(row.estimated_cost_usd || '0'),
43
+ }), { sessions: 0, tokens: 0, calls: 0, cost: 0 });
44
+ return {
45
+ result: {
46
+ period,
47
+ project_id,
48
+ summary: data || [],
49
+ totals: {
50
+ ...totals,
51
+ cost: Math.round(totals.cost * 100) / 100,
52
+ },
53
+ },
54
+ };
55
+ };
56
+ /**
57
+ * Get cost alerts for the current user
58
+ */
59
+ export const getCostAlerts = async (args, ctx) => {
60
+ const { project_id } = args;
61
+ const { supabase, auth } = ctx;
62
+ let query = supabase
63
+ .from('cost_alerts')
64
+ .select('*')
65
+ .eq('user_id', auth.userId)
66
+ .order('threshold_amount', { ascending: true });
67
+ if (project_id) {
68
+ query = query.eq('project_id', project_id);
69
+ }
70
+ const { data, error } = await query;
71
+ if (error) {
72
+ return {
73
+ result: { error: `Failed to get cost alerts: ${error.message}` },
74
+ isError: true,
75
+ };
76
+ }
77
+ return {
78
+ result: {
79
+ alerts: data || [],
80
+ count: data?.length || 0,
81
+ },
82
+ };
83
+ };
84
+ /**
85
+ * Add a cost alert
86
+ */
87
+ export const addCostAlert = async (args, ctx) => {
88
+ const { project_id, threshold_amount, threshold_period, alert_type = 'warning', } = args;
89
+ const { supabase, auth } = ctx;
90
+ if (!threshold_amount || threshold_amount <= 0) {
91
+ return {
92
+ result: { error: 'threshold_amount must be a positive number' },
93
+ isError: true,
94
+ };
95
+ }
96
+ if (!threshold_period || !['daily', 'weekly', 'monthly'].includes(threshold_period)) {
97
+ return {
98
+ result: { error: 'threshold_period must be "daily", "weekly", or "monthly"' },
99
+ isError: true,
100
+ };
101
+ }
102
+ const { data, error } = await supabase
103
+ .from('cost_alerts')
104
+ .insert({
105
+ user_id: auth.userId,
106
+ project_id: project_id || null,
107
+ threshold_amount,
108
+ threshold_period,
109
+ alert_type,
110
+ })
111
+ .select()
112
+ .single();
113
+ if (error) {
114
+ return {
115
+ result: { error: `Failed to create cost alert: ${error.message}` },
116
+ isError: true,
117
+ };
118
+ }
119
+ return {
120
+ result: {
121
+ success: true,
122
+ alert: data,
123
+ message: `Alert created: ${alert_type} when ${threshold_period} cost exceeds $${threshold_amount}`,
124
+ },
125
+ };
126
+ };
127
+ /**
128
+ * Update a cost alert
129
+ */
130
+ export const updateCostAlert = async (args, ctx) => {
131
+ const { alert_id, threshold_amount, threshold_period, alert_type, enabled, } = args;
132
+ const { supabase, auth } = ctx;
133
+ if (!alert_id) {
134
+ return {
135
+ result: { error: 'alert_id is required' },
136
+ isError: true,
137
+ };
138
+ }
139
+ const updates = {};
140
+ if (threshold_amount !== undefined)
141
+ updates.threshold_amount = threshold_amount;
142
+ if (threshold_period !== undefined)
143
+ updates.threshold_period = threshold_period;
144
+ if (alert_type !== undefined)
145
+ updates.alert_type = alert_type;
146
+ if (enabled !== undefined)
147
+ updates.enabled = enabled;
148
+ if (Object.keys(updates).length === 0) {
149
+ return {
150
+ result: { error: 'No updates provided' },
151
+ isError: true,
152
+ };
153
+ }
154
+ const { data, error } = await supabase
155
+ .from('cost_alerts')
156
+ .update(updates)
157
+ .eq('id', alert_id)
158
+ .eq('user_id', auth.userId)
159
+ .select()
160
+ .single();
161
+ if (error) {
162
+ return {
163
+ result: { error: `Failed to update cost alert: ${error.message}` },
164
+ isError: true,
165
+ };
166
+ }
167
+ return {
168
+ result: {
169
+ success: true,
170
+ alert: data,
171
+ },
172
+ };
173
+ };
174
+ /**
175
+ * Delete a cost alert
176
+ */
177
+ export const deleteCostAlert = async (args, ctx) => {
178
+ const { alert_id } = args;
179
+ const { supabase, auth } = ctx;
180
+ if (!alert_id) {
181
+ return {
182
+ result: { error: 'alert_id is required' },
183
+ isError: true,
184
+ };
185
+ }
186
+ const { error } = await supabase
187
+ .from('cost_alerts')
188
+ .delete()
189
+ .eq('id', alert_id)
190
+ .eq('user_id', auth.userId);
191
+ if (error) {
192
+ return {
193
+ result: { error: `Failed to delete cost alert: ${error.message}` },
194
+ isError: true,
195
+ };
196
+ }
197
+ return {
198
+ result: {
199
+ success: true,
200
+ deleted_alert_id: alert_id,
201
+ },
202
+ };
203
+ };
204
+ /**
205
+ * Get task costs for a project
206
+ */
207
+ export const getTaskCosts = async (args, ctx) => {
208
+ const { project_id, limit = 20 } = args;
209
+ const { supabase } = ctx;
210
+ if (!project_id) {
211
+ return {
212
+ result: { error: 'project_id is required' },
213
+ isError: true,
214
+ };
215
+ }
216
+ const { data, error } = await supabase
217
+ .from('task_costs')
218
+ .select('*')
219
+ .eq('project_id', project_id)
220
+ .order('estimated_cost_usd', { ascending: false })
221
+ .limit(limit);
222
+ if (error) {
223
+ return {
224
+ result: { error: `Failed to get task costs: ${error.message}` },
225
+ isError: true,
226
+ };
227
+ }
228
+ const totalCost = (data || []).reduce((sum, task) => sum + parseFloat(task.estimated_cost_usd || '0'), 0);
229
+ return {
230
+ result: {
231
+ project_id,
232
+ tasks: data || [],
233
+ total_cost_usd: Math.round(totalCost * 100) / 100,
234
+ },
235
+ };
236
+ };
237
+ /**
238
+ * Cost handlers registry
239
+ */
240
+ export const costHandlers = {
241
+ get_cost_summary: getCostSummary,
242
+ get_cost_alerts: getCostAlerts,
243
+ add_cost_alert: addCostAlert,
244
+ update_cost_alert: updateCostAlert,
245
+ delete_cost_alert: deleteCostAlert,
246
+ get_task_costs: getTaskCosts,
247
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Decisions Handlers
3
+ *
4
+ * Handles architectural/technical decisions:
5
+ * - log_decision
6
+ * - get_decisions
7
+ * - delete_decision
8
+ */
9
+ import type { Handler, HandlerRegistry } from './types.js';
10
+ export declare const logDecision: Handler;
11
+ export declare const getDecisions: Handler;
12
+ export declare const deleteDecision: Handler;
13
+ /**
14
+ * Decisions handlers registry
15
+ */
16
+ export declare const decisionHandlers: HandlerRegistry;