@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
|
@@ -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;
|