@vibescope/mcp-server 0.2.0 → 0.2.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 (65) hide show
  1. package/dist/api-client.d.ts +64 -1
  2. package/dist/api-client.js +34 -3
  3. package/dist/handlers/bodies-of-work.js +82 -49
  4. package/dist/handlers/cost.js +62 -54
  5. package/dist/handlers/decisions.js +29 -16
  6. package/dist/handlers/deployment.js +112 -106
  7. package/dist/handlers/discovery.js +35 -5
  8. package/dist/handlers/fallback.js +24 -19
  9. package/dist/handlers/file-checkouts.d.ts +18 -0
  10. package/dist/handlers/file-checkouts.js +101 -0
  11. package/dist/handlers/findings.d.ts +6 -0
  12. package/dist/handlers/findings.js +85 -30
  13. package/dist/handlers/git-issues.js +36 -32
  14. package/dist/handlers/ideas.js +44 -26
  15. package/dist/handlers/index.d.ts +2 -0
  16. package/dist/handlers/index.js +6 -0
  17. package/dist/handlers/milestones.js +34 -27
  18. package/dist/handlers/organizations.js +86 -78
  19. package/dist/handlers/progress.js +22 -11
  20. package/dist/handlers/project.js +62 -22
  21. package/dist/handlers/requests.js +15 -11
  22. package/dist/handlers/roles.d.ts +18 -0
  23. package/dist/handlers/roles.js +130 -0
  24. package/dist/handlers/session.js +30 -8
  25. package/dist/handlers/sprints.js +76 -64
  26. package/dist/handlers/tasks.js +113 -73
  27. package/dist/handlers/validation.js +18 -14
  28. package/dist/tools.js +387 -0
  29. package/package.json +1 -1
  30. package/src/api-client.ts +89 -6
  31. package/src/handlers/__test-setup__.ts +7 -0
  32. package/src/handlers/bodies-of-work.ts +101 -101
  33. package/src/handlers/cost.test.ts +34 -44
  34. package/src/handlers/cost.ts +77 -92
  35. package/src/handlers/decisions.test.ts +3 -2
  36. package/src/handlers/decisions.ts +32 -27
  37. package/src/handlers/deployment.ts +142 -190
  38. package/src/handlers/discovery.test.ts +4 -5
  39. package/src/handlers/discovery.ts +37 -6
  40. package/src/handlers/fallback.ts +31 -29
  41. package/src/handlers/file-checkouts.test.ts +477 -0
  42. package/src/handlers/file-checkouts.ts +127 -0
  43. package/src/handlers/findings.test.ts +145 -0
  44. package/src/handlers/findings.ts +101 -64
  45. package/src/handlers/git-issues.ts +40 -80
  46. package/src/handlers/ideas.ts +56 -54
  47. package/src/handlers/index.ts +6 -0
  48. package/src/handlers/milestones.test.ts +1 -1
  49. package/src/handlers/milestones.ts +47 -45
  50. package/src/handlers/organizations.ts +104 -129
  51. package/src/handlers/progress.ts +24 -22
  52. package/src/handlers/project.ts +89 -57
  53. package/src/handlers/requests.ts +18 -14
  54. package/src/handlers/roles.test.ts +303 -0
  55. package/src/handlers/roles.ts +208 -0
  56. package/src/handlers/session.ts +39 -17
  57. package/src/handlers/sprints.ts +96 -129
  58. package/src/handlers/tasks.ts +144 -138
  59. package/src/handlers/validation.test.ts +1 -1
  60. package/src/handlers/validation.ts +20 -22
  61. package/src/tools.ts +387 -0
  62. package/dist/config/tool-categories.d.ts +0 -31
  63. package/dist/config/tool-categories.js +0 -253
  64. package/dist/knowledge.d.ts +0 -6
  65. package/dist/knowledge.js +0 -218
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import type { Handler, HandlerRegistry } from './types.js';
14
- import { validateRequired, validateUUID } from '../validators.js';
14
+ import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
15
15
  import { FALLBACK_ACTIVITIES } from '../utils.js';
16
16
  import { getApiClient } from '../api-client.js';
17
17
 
@@ -26,23 +26,38 @@ const VALID_ACTIVITIES = [
26
26
  'documentation_review',
27
27
  'dependency_audit',
28
28
  'validate_completed_tasks',
29
- ];
29
+ ] as const;
30
30
 
31
- export const startFallbackActivity: Handler = async (args, ctx) => {
32
- const { project_id, activity } = args as { project_id: string; activity: string };
31
+ type FallbackActivity = typeof VALID_ACTIVITIES[number];
33
32
 
34
- validateRequired(project_id, 'project_id');
35
- validateUUID(project_id, 'project_id');
36
- validateRequired(activity, 'activity');
33
+ // Argument schemas for type-safe parsing
34
+ const startFallbackActivitySchema = {
35
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
36
+ activity: { type: 'string' as const, required: true as const, validate: createEnumValidator(VALID_ACTIVITIES) },
37
+ };
37
38
 
38
- if (!VALID_ACTIVITIES.includes(activity)) {
39
- throw new Error(`Invalid activity. Must be one of: ${VALID_ACTIVITIES.join(', ')}`);
40
- }
39
+ const stopFallbackActivitySchema = {
40
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
41
+ summary: { type: 'string' as const },
42
+ };
43
+
44
+ const getActivityHistorySchema = {
45
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
46
+ activity_type: { type: 'string' as const },
47
+ limit: { type: 'number' as const, default: 50 },
48
+ };
49
+
50
+ const getActivitySchedulesSchema = {
51
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
52
+ };
53
+
54
+ export const startFallbackActivity: Handler = async (args, ctx) => {
55
+ const { project_id, activity } = parseArgs(args, startFallbackActivitySchema);
41
56
 
42
57
  const { session } = ctx;
43
58
  const apiClient = getApiClient();
44
59
 
45
- const response = await apiClient.startFallbackActivity(project_id, activity, session.currentSessionId || undefined);
60
+ const response = await apiClient.startFallbackActivity(project_id, activity as FallbackActivity, session.currentSessionId || undefined);
46
61
 
47
62
  if (!response.ok) {
48
63
  throw new Error(`Failed to start fallback activity: ${response.error}`);
@@ -75,10 +90,7 @@ export const startFallbackActivity: Handler = async (args, ctx) => {
75
90
  };
76
91
 
77
92
  export const stopFallbackActivity: Handler = async (args, ctx) => {
78
- const { project_id, summary } = args as { project_id: string; summary?: string };
79
-
80
- validateRequired(project_id, 'project_id');
81
- validateUUID(project_id, 'project_id');
93
+ const { project_id, summary } = parseArgs(args, stopFallbackActivitySchema);
82
94
 
83
95
  const { session } = ctx;
84
96
  const apiClient = getApiClient();
@@ -97,15 +109,8 @@ export const stopFallbackActivity: Handler = async (args, ctx) => {
97
109
  };
98
110
  };
99
111
 
100
- export const getActivityHistory: Handler = async (args, ctx) => {
101
- const { project_id, activity_type, limit = 50 } = args as {
102
- project_id: string;
103
- activity_type?: string;
104
- limit?: number;
105
- };
106
-
107
- validateRequired(project_id, 'project_id');
108
- validateUUID(project_id, 'project_id');
112
+ export const getActivityHistory: Handler = async (args, _ctx) => {
113
+ const { project_id, activity_type, limit } = parseArgs(args, getActivityHistorySchema);
109
114
 
110
115
  const apiClient = getApiClient();
111
116
 
@@ -138,11 +143,8 @@ export const getActivityHistory: Handler = async (args, ctx) => {
138
143
  };
139
144
  };
140
145
 
141
- export const getActivitySchedules: Handler = async (args, ctx) => {
142
- const { project_id } = args as { project_id: string };
143
-
144
- validateRequired(project_id, 'project_id');
145
- validateUUID(project_id, 'project_id');
146
+ export const getActivitySchedules: Handler = async (args, _ctx) => {
147
+ const { project_id } = parseArgs(args, getActivitySchedulesSchema);
146
148
 
147
149
  const apiClient = getApiClient();
148
150
 
@@ -0,0 +1,477 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ checkoutFile,
4
+ checkinFile,
5
+ getFileCheckouts,
6
+ abandonCheckout,
7
+ } from './file-checkouts.js';
8
+ import { ValidationError } from '../validators.js';
9
+ import { createMockContext, testUUID } from './__test-utils__.js';
10
+ import { mockApiClient } from './__test-setup__.js';
11
+
12
+ const VALID_UUID = testUUID();
13
+
14
+ // ============================================================================
15
+ // checkoutFile Tests
16
+ // ============================================================================
17
+
18
+ describe('checkoutFile', () => {
19
+ beforeEach(() => vi.clearAllMocks());
20
+
21
+ it('should throw error for missing project_id', async () => {
22
+ const ctx = createMockContext();
23
+
24
+ await expect(
25
+ checkoutFile({ file_path: '/src/index.ts' }, ctx)
26
+ ).rejects.toThrow(ValidationError);
27
+ });
28
+
29
+ it('should throw error for invalid project_id UUID', async () => {
30
+ const ctx = createMockContext();
31
+
32
+ await expect(
33
+ checkoutFile({ project_id: 'invalid', file_path: '/src/index.ts' }, ctx)
34
+ ).rejects.toThrow(ValidationError);
35
+ });
36
+
37
+ it('should throw error for missing file_path', async () => {
38
+ const ctx = createMockContext();
39
+
40
+ await expect(
41
+ checkoutFile({ project_id: VALID_UUID }, ctx)
42
+ ).rejects.toThrow(ValidationError);
43
+ });
44
+
45
+ it('should checkout file successfully', async () => {
46
+ mockApiClient.checkoutFile.mockResolvedValue({
47
+ ok: true,
48
+ data: { success: true, checkout_id: 'checkout-1', file_path: '/src/index.ts' },
49
+ });
50
+ const ctx = createMockContext();
51
+
52
+ const result = await checkoutFile(
53
+ {
54
+ project_id: VALID_UUID,
55
+ file_path: '/src/index.ts',
56
+ },
57
+ ctx
58
+ );
59
+
60
+ expect(result.result).toMatchObject({
61
+ success: true,
62
+ checkout_id: 'checkout-1',
63
+ });
64
+ });
65
+
66
+ it('should include reason in API call when provided', async () => {
67
+ mockApiClient.checkoutFile.mockResolvedValue({
68
+ ok: true,
69
+ data: { success: true, checkout_id: 'checkout-1', file_path: '/src/index.ts' },
70
+ });
71
+ const ctx = createMockContext({ sessionId: 'my-session' });
72
+
73
+ await checkoutFile(
74
+ {
75
+ project_id: VALID_UUID,
76
+ file_path: '/src/index.ts',
77
+ reason: 'Editing for feature X',
78
+ },
79
+ ctx
80
+ );
81
+
82
+ expect(mockApiClient.checkoutFile).toHaveBeenCalledWith(
83
+ VALID_UUID,
84
+ '/src/index.ts',
85
+ 'Editing for feature X',
86
+ 'my-session'
87
+ );
88
+ });
89
+
90
+ it('should throw error when API call fails', async () => {
91
+ mockApiClient.checkoutFile.mockResolvedValue({
92
+ ok: false,
93
+ error: 'File already checked out',
94
+ });
95
+ const ctx = createMockContext();
96
+
97
+ await expect(
98
+ checkoutFile({
99
+ project_id: VALID_UUID,
100
+ file_path: '/src/index.ts',
101
+ }, ctx)
102
+ ).rejects.toThrow('File already checked out');
103
+ });
104
+
105
+ it('should throw default error when API fails without message', async () => {
106
+ mockApiClient.checkoutFile.mockResolvedValue({
107
+ ok: false,
108
+ });
109
+ const ctx = createMockContext();
110
+
111
+ await expect(
112
+ checkoutFile({
113
+ project_id: VALID_UUID,
114
+ file_path: '/src/index.ts',
115
+ }, ctx)
116
+ ).rejects.toThrow('Failed to checkout file');
117
+ });
118
+ });
119
+
120
+ // ============================================================================
121
+ // checkinFile Tests
122
+ // ============================================================================
123
+
124
+ describe('checkinFile', () => {
125
+ beforeEach(() => vi.clearAllMocks());
126
+
127
+ it('should throw error when neither checkout_id nor project_id+file_path provided', async () => {
128
+ const ctx = createMockContext();
129
+
130
+ await expect(
131
+ checkinFile({}, ctx)
132
+ ).rejects.toThrow('Either checkout_id or both project_id and file_path are required');
133
+ });
134
+
135
+ it('should throw error when only project_id provided without file_path', async () => {
136
+ const ctx = createMockContext();
137
+
138
+ await expect(
139
+ checkinFile({ project_id: VALID_UUID }, ctx)
140
+ ).rejects.toThrow('Either checkout_id or both project_id and file_path are required');
141
+ });
142
+
143
+ it('should throw error when only file_path provided without project_id', async () => {
144
+ const ctx = createMockContext();
145
+
146
+ await expect(
147
+ checkinFile({ file_path: '/src/index.ts' }, ctx)
148
+ ).rejects.toThrow('Either checkout_id or both project_id and file_path are required');
149
+ });
150
+
151
+ it('should throw error for invalid checkout_id UUID', async () => {
152
+ const ctx = createMockContext();
153
+
154
+ await expect(
155
+ checkinFile({ checkout_id: 'invalid' }, ctx)
156
+ ).rejects.toThrow(ValidationError);
157
+ });
158
+
159
+ it('should checkin file successfully with checkout_id', async () => {
160
+ mockApiClient.checkinFile.mockResolvedValue({
161
+ ok: true,
162
+ data: { success: true },
163
+ });
164
+ const ctx = createMockContext();
165
+
166
+ const result = await checkinFile(
167
+ { checkout_id: VALID_UUID },
168
+ ctx
169
+ );
170
+
171
+ expect(result.result).toMatchObject({ success: true });
172
+ });
173
+
174
+ it('should checkin file successfully with project_id and file_path', async () => {
175
+ mockApiClient.checkinFile.mockResolvedValue({
176
+ ok: true,
177
+ data: { success: true },
178
+ });
179
+ const ctx = createMockContext();
180
+
181
+ const result = await checkinFile(
182
+ {
183
+ project_id: VALID_UUID,
184
+ file_path: '/src/index.ts',
185
+ },
186
+ ctx
187
+ );
188
+
189
+ expect(result.result).toMatchObject({ success: true });
190
+ });
191
+
192
+ it('should include summary in API call', async () => {
193
+ mockApiClient.checkinFile.mockResolvedValue({
194
+ ok: true,
195
+ data: { success: true },
196
+ });
197
+ const ctx = createMockContext({ sessionId: 'my-session' });
198
+
199
+ await checkinFile(
200
+ {
201
+ checkout_id: VALID_UUID,
202
+ summary: 'Added validation logic',
203
+ },
204
+ ctx
205
+ );
206
+
207
+ expect(mockApiClient.checkinFile).toHaveBeenCalledWith(
208
+ {
209
+ checkout_id: VALID_UUID,
210
+ project_id: undefined,
211
+ file_path: undefined,
212
+ summary: 'Added validation logic',
213
+ },
214
+ 'my-session'
215
+ );
216
+ });
217
+
218
+ it('should throw error when API call fails', async () => {
219
+ mockApiClient.checkinFile.mockResolvedValue({
220
+ ok: false,
221
+ error: 'Checkout not found',
222
+ });
223
+ const ctx = createMockContext();
224
+
225
+ await expect(
226
+ checkinFile({ checkout_id: VALID_UUID }, ctx)
227
+ ).rejects.toThrow('Checkout not found');
228
+ });
229
+ });
230
+
231
+ // ============================================================================
232
+ // getFileCheckouts Tests
233
+ // ============================================================================
234
+
235
+ describe('getFileCheckouts', () => {
236
+ beforeEach(() => vi.clearAllMocks());
237
+
238
+ it('should throw error for missing project_id', async () => {
239
+ const ctx = createMockContext();
240
+
241
+ await expect(
242
+ getFileCheckouts({}, ctx)
243
+ ).rejects.toThrow(ValidationError);
244
+ });
245
+
246
+ it('should throw error for invalid project_id UUID', async () => {
247
+ const ctx = createMockContext();
248
+
249
+ await expect(
250
+ getFileCheckouts({ project_id: 'invalid' }, ctx)
251
+ ).rejects.toThrow(ValidationError);
252
+ });
253
+
254
+ it('should throw error for invalid status', async () => {
255
+ const ctx = createMockContext();
256
+
257
+ await expect(
258
+ getFileCheckouts({ project_id: VALID_UUID, status: 'invalid_status' }, ctx)
259
+ ).rejects.toThrow(ValidationError);
260
+ });
261
+
262
+ it('should get file checkouts successfully', async () => {
263
+ mockApiClient.getFileCheckouts.mockResolvedValue({
264
+ ok: true,
265
+ data: {
266
+ checkouts: [
267
+ { id: 'checkout-1', file_path: '/src/index.ts', status: 'checked_out' },
268
+ ],
269
+ },
270
+ });
271
+ const ctx = createMockContext();
272
+
273
+ const result = await getFileCheckouts(
274
+ { project_id: VALID_UUID },
275
+ ctx
276
+ );
277
+
278
+ expect(result.result).toMatchObject({
279
+ checkouts: [
280
+ { id: 'checkout-1', file_path: '/src/index.ts', status: 'checked_out' },
281
+ ],
282
+ });
283
+ });
284
+
285
+ it('should filter by status', async () => {
286
+ mockApiClient.getFileCheckouts.mockResolvedValue({
287
+ ok: true,
288
+ data: { checkouts: [] },
289
+ });
290
+ const ctx = createMockContext();
291
+
292
+ await getFileCheckouts(
293
+ { project_id: VALID_UUID, status: 'checked_out' },
294
+ ctx
295
+ );
296
+
297
+ expect(mockApiClient.getFileCheckouts).toHaveBeenCalledWith(
298
+ VALID_UUID,
299
+ { status: 'checked_out', file_path: undefined, limit: 50 }
300
+ );
301
+ });
302
+
303
+ it('should filter by file_path', async () => {
304
+ mockApiClient.getFileCheckouts.mockResolvedValue({
305
+ ok: true,
306
+ data: { checkouts: [] },
307
+ });
308
+ const ctx = createMockContext();
309
+
310
+ await getFileCheckouts(
311
+ { project_id: VALID_UUID, file_path: '/src/utils.ts' },
312
+ ctx
313
+ );
314
+
315
+ expect(mockApiClient.getFileCheckouts).toHaveBeenCalledWith(
316
+ VALID_UUID,
317
+ { status: undefined, file_path: '/src/utils.ts', limit: 50 }
318
+ );
319
+ });
320
+
321
+ it('should use custom limit', async () => {
322
+ mockApiClient.getFileCheckouts.mockResolvedValue({
323
+ ok: true,
324
+ data: { checkouts: [] },
325
+ });
326
+ const ctx = createMockContext();
327
+
328
+ await getFileCheckouts(
329
+ { project_id: VALID_UUID, limit: 10 },
330
+ ctx
331
+ );
332
+
333
+ expect(mockApiClient.getFileCheckouts).toHaveBeenCalledWith(
334
+ VALID_UUID,
335
+ { status: undefined, file_path: undefined, limit: 10 }
336
+ );
337
+ });
338
+
339
+ it('should accept all valid status values', async () => {
340
+ mockApiClient.getFileCheckouts.mockResolvedValue({
341
+ ok: true,
342
+ data: { checkouts: [] },
343
+ });
344
+ const ctx = createMockContext();
345
+
346
+ for (const status of ['checked_out', 'checked_in', 'abandoned']) {
347
+ await getFileCheckouts(
348
+ { project_id: VALID_UUID, status },
349
+ ctx
350
+ );
351
+ }
352
+
353
+ expect(mockApiClient.getFileCheckouts).toHaveBeenCalledTimes(3);
354
+ });
355
+
356
+ it('should throw error when API call fails', async () => {
357
+ mockApiClient.getFileCheckouts.mockResolvedValue({
358
+ ok: false,
359
+ error: 'Database error',
360
+ });
361
+ const ctx = createMockContext();
362
+
363
+ await expect(
364
+ getFileCheckouts({ project_id: VALID_UUID }, ctx)
365
+ ).rejects.toThrow('Database error');
366
+ });
367
+ });
368
+
369
+ // ============================================================================
370
+ // abandonCheckout Tests
371
+ // ============================================================================
372
+
373
+ describe('abandonCheckout', () => {
374
+ beforeEach(() => vi.clearAllMocks());
375
+
376
+ it('should throw error when neither checkout_id nor project_id+file_path provided', async () => {
377
+ const ctx = createMockContext();
378
+
379
+ await expect(
380
+ abandonCheckout({}, ctx)
381
+ ).rejects.toThrow('Either checkout_id or both project_id and file_path are required');
382
+ });
383
+
384
+ it('should throw error when only project_id provided without file_path', async () => {
385
+ const ctx = createMockContext();
386
+
387
+ await expect(
388
+ abandonCheckout({ project_id: VALID_UUID }, ctx)
389
+ ).rejects.toThrow('Either checkout_id or both project_id and file_path are required');
390
+ });
391
+
392
+ it('should throw error for invalid checkout_id UUID', async () => {
393
+ const ctx = createMockContext();
394
+
395
+ await expect(
396
+ abandonCheckout({ checkout_id: 'invalid' }, ctx)
397
+ ).rejects.toThrow(ValidationError);
398
+ });
399
+
400
+ it('should abandon checkout successfully with checkout_id', async () => {
401
+ mockApiClient.abandonCheckout.mockResolvedValue({
402
+ ok: true,
403
+ data: { success: true },
404
+ });
405
+ const ctx = createMockContext();
406
+
407
+ const result = await abandonCheckout(
408
+ { checkout_id: VALID_UUID },
409
+ ctx
410
+ );
411
+
412
+ expect(result.result).toMatchObject({ success: true });
413
+ });
414
+
415
+ it('should abandon checkout successfully with project_id and file_path', async () => {
416
+ mockApiClient.abandonCheckout.mockResolvedValue({
417
+ ok: true,
418
+ data: { success: true },
419
+ });
420
+ const ctx = createMockContext();
421
+
422
+ const result = await abandonCheckout(
423
+ {
424
+ project_id: VALID_UUID,
425
+ file_path: '/src/index.ts',
426
+ },
427
+ ctx
428
+ );
429
+
430
+ expect(result.result).toMatchObject({ success: true });
431
+ });
432
+
433
+ it('should pass params correctly to API', async () => {
434
+ mockApiClient.abandonCheckout.mockResolvedValue({
435
+ ok: true,
436
+ data: { success: true },
437
+ });
438
+ const ctx = createMockContext();
439
+
440
+ await abandonCheckout(
441
+ {
442
+ project_id: VALID_UUID,
443
+ file_path: '/src/index.ts',
444
+ },
445
+ ctx
446
+ );
447
+
448
+ expect(mockApiClient.abandonCheckout).toHaveBeenCalledWith({
449
+ checkout_id: undefined,
450
+ project_id: VALID_UUID,
451
+ file_path: '/src/index.ts',
452
+ });
453
+ });
454
+
455
+ it('should throw error when API call fails', async () => {
456
+ mockApiClient.abandonCheckout.mockResolvedValue({
457
+ ok: false,
458
+ error: 'Checkout not found',
459
+ });
460
+ const ctx = createMockContext();
461
+
462
+ await expect(
463
+ abandonCheckout({ checkout_id: VALID_UUID }, ctx)
464
+ ).rejects.toThrow('Checkout not found');
465
+ });
466
+
467
+ it('should throw default error when API fails without message', async () => {
468
+ mockApiClient.abandonCheckout.mockResolvedValue({
469
+ ok: false,
470
+ });
471
+ const ctx = createMockContext();
472
+
473
+ await expect(
474
+ abandonCheckout({ checkout_id: VALID_UUID }, ctx)
475
+ ).rejects.toThrow('Failed to abandon checkout');
476
+ });
477
+ });
@@ -0,0 +1,127 @@
1
+ /**
2
+ * File Checkouts Handlers
3
+ *
4
+ * Handles file checkout/checkin for multi-agent coordination:
5
+ * - checkout_file: Check out a file before editing
6
+ * - checkin_file: Check in a file after editing
7
+ * - get_file_checkouts: Get active checkouts for a project
8
+ * - abandon_checkout: Force release a checkout
9
+ */
10
+
11
+ import type { Handler, HandlerRegistry } from './types.js';
12
+ import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
13
+ import { getApiClient } from '../api-client.js';
14
+
15
+ const VALID_CHECKOUT_STATUSES = ['checked_out', 'checked_in', 'abandoned'] as const;
16
+
17
+ // Argument schemas for type-safe parsing
18
+ const checkoutFileSchema = {
19
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
20
+ file_path: { type: 'string' as const, required: true as const },
21
+ reason: { type: 'string' as const },
22
+ };
23
+
24
+ const checkinFileSchema = {
25
+ checkout_id: { type: 'string' as const, validate: uuidValidator },
26
+ project_id: { type: 'string' as const, validate: uuidValidator },
27
+ file_path: { type: 'string' as const },
28
+ summary: { type: 'string' as const },
29
+ };
30
+
31
+ const getFileCheckoutsSchema = {
32
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
33
+ status: { type: 'string' as const, validate: createEnumValidator(VALID_CHECKOUT_STATUSES) },
34
+ file_path: { type: 'string' as const },
35
+ limit: { type: 'number' as const, default: 50 },
36
+ };
37
+
38
+ const abandonCheckoutSchema = {
39
+ checkout_id: { type: 'string' as const, validate: uuidValidator },
40
+ project_id: { type: 'string' as const, validate: uuidValidator },
41
+ file_path: { type: 'string' as const },
42
+ };
43
+
44
+ export const checkoutFile: Handler = async (args, ctx) => {
45
+ const { project_id, file_path, reason } = parseArgs(args, checkoutFileSchema);
46
+
47
+ const apiClient = getApiClient();
48
+ const response = await apiClient.checkoutFile(project_id, file_path, reason, ctx.session.currentSessionId || undefined);
49
+
50
+ if (!response.ok) {
51
+ throw new Error(response.error || 'Failed to checkout file');
52
+ }
53
+
54
+ return { result: response.data };
55
+ };
56
+
57
+ export const checkinFile: Handler = async (args, ctx) => {
58
+ const { checkout_id, project_id, file_path, summary } = parseArgs(args, checkinFileSchema);
59
+
60
+ // Validate that either checkout_id or both project_id and file_path are provided
61
+ if (!checkout_id && (!project_id || !file_path)) {
62
+ throw new Error('Either checkout_id or both project_id and file_path are required');
63
+ }
64
+
65
+ const apiClient = getApiClient();
66
+ const response = await apiClient.checkinFile({
67
+ checkout_id,
68
+ project_id,
69
+ file_path,
70
+ summary
71
+ }, ctx.session.currentSessionId || undefined);
72
+
73
+ if (!response.ok) {
74
+ throw new Error(response.error || 'Failed to checkin file');
75
+ }
76
+
77
+ return { result: response.data };
78
+ };
79
+
80
+ export const getFileCheckouts: Handler = async (args, _ctx) => {
81
+ const { project_id, status, file_path, limit } = parseArgs(args, getFileCheckoutsSchema);
82
+
83
+ const apiClient = getApiClient();
84
+ const response = await apiClient.getFileCheckouts(project_id, {
85
+ status,
86
+ file_path,
87
+ limit
88
+ });
89
+
90
+ if (!response.ok) {
91
+ throw new Error(response.error || 'Failed to get file checkouts');
92
+ }
93
+
94
+ return { result: response.data };
95
+ };
96
+
97
+ export const abandonCheckout: Handler = async (args, _ctx) => {
98
+ const { checkout_id, project_id, file_path } = parseArgs(args, abandonCheckoutSchema);
99
+
100
+ // Validate that either checkout_id or both project_id and file_path are provided
101
+ if (!checkout_id && (!project_id || !file_path)) {
102
+ throw new Error('Either checkout_id or both project_id and file_path are required');
103
+ }
104
+
105
+ const apiClient = getApiClient();
106
+ const response = await apiClient.abandonCheckout({
107
+ checkout_id,
108
+ project_id,
109
+ file_path
110
+ });
111
+
112
+ if (!response.ok) {
113
+ throw new Error(response.error || 'Failed to abandon checkout');
114
+ }
115
+
116
+ return { result: response.data };
117
+ };
118
+
119
+ /**
120
+ * File Checkouts handlers registry
121
+ */
122
+ export const fileCheckoutHandlers: HandlerRegistry = {
123
+ checkout_file: checkoutFile,
124
+ checkin_file: checkinFile,
125
+ get_file_checkouts: getFileCheckouts,
126
+ abandon_checkout: abandonCheckout,
127
+ };