@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.
- package/dist/api-client.d.ts +64 -1
- package/dist/api-client.js +34 -3
- package/dist/handlers/bodies-of-work.js +82 -49
- package/dist/handlers/cost.js +62 -54
- package/dist/handlers/decisions.js +29 -16
- package/dist/handlers/deployment.js +112 -106
- package/dist/handlers/discovery.js +35 -5
- package/dist/handlers/fallback.js +24 -19
- package/dist/handlers/file-checkouts.d.ts +18 -0
- package/dist/handlers/file-checkouts.js +101 -0
- package/dist/handlers/findings.d.ts +6 -0
- package/dist/handlers/findings.js +85 -30
- package/dist/handlers/git-issues.js +36 -32
- package/dist/handlers/ideas.js +44 -26
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/milestones.js +34 -27
- package/dist/handlers/organizations.js +86 -78
- package/dist/handlers/progress.js +22 -11
- package/dist/handlers/project.js +62 -22
- package/dist/handlers/requests.js +15 -11
- package/dist/handlers/roles.d.ts +18 -0
- package/dist/handlers/roles.js +130 -0
- package/dist/handlers/session.js +30 -8
- package/dist/handlers/sprints.js +76 -64
- package/dist/handlers/tasks.js +113 -73
- package/dist/handlers/validation.js +18 -14
- package/dist/tools.js +387 -0
- package/package.json +1 -1
- package/src/api-client.ts +89 -6
- package/src/handlers/__test-setup__.ts +7 -0
- package/src/handlers/bodies-of-work.ts +101 -101
- package/src/handlers/cost.test.ts +34 -44
- package/src/handlers/cost.ts +77 -92
- package/src/handlers/decisions.test.ts +3 -2
- package/src/handlers/decisions.ts +32 -27
- package/src/handlers/deployment.ts +142 -190
- package/src/handlers/discovery.test.ts +4 -5
- package/src/handlers/discovery.ts +37 -6
- package/src/handlers/fallback.ts +31 -29
- package/src/handlers/file-checkouts.test.ts +477 -0
- package/src/handlers/file-checkouts.ts +127 -0
- package/src/handlers/findings.test.ts +145 -0
- package/src/handlers/findings.ts +101 -64
- package/src/handlers/git-issues.ts +40 -80
- package/src/handlers/ideas.ts +56 -54
- package/src/handlers/index.ts +6 -0
- package/src/handlers/milestones.test.ts +1 -1
- package/src/handlers/milestones.ts +47 -45
- package/src/handlers/organizations.ts +104 -129
- package/src/handlers/progress.ts +24 -22
- package/src/handlers/project.ts +89 -57
- package/src/handlers/requests.ts +18 -14
- package/src/handlers/roles.test.ts +303 -0
- package/src/handlers/roles.ts +208 -0
- package/src/handlers/session.ts +39 -17
- package/src/handlers/sprints.ts +96 -129
- package/src/handlers/tasks.ts +144 -138
- package/src/handlers/validation.test.ts +1 -1
- package/src/handlers/validation.ts +20 -22
- package/src/tools.ts +387 -0
- package/dist/config/tool-categories.d.ts +0 -31
- package/dist/config/tool-categories.js +0 -253
- package/dist/knowledge.d.ts +0 -6
- package/dist/knowledge.js +0 -218
|
@@ -19,14 +19,96 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type { Handler, HandlerRegistry } from './types.js';
|
|
22
|
-
import {
|
|
22
|
+
import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
|
|
23
23
|
import { getApiClient } from '../api-client.js';
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const BODY_OF_WORK_STATUSES = ['draft', 'active', 'completed', 'cancelled'] as const;
|
|
26
|
+
const TASK_PHASES = ['pre', 'core', 'post'] as const;
|
|
27
|
+
const DEPLOY_ENVIRONMENTS = ['development', 'staging', 'production'] as const;
|
|
28
|
+
const VERSION_BUMPS = ['patch', 'minor', 'major'] as const;
|
|
29
|
+
const DEPLOY_TRIGGERS = ['all_completed', 'all_completed_validated'] as const;
|
|
30
|
+
|
|
31
|
+
type BodyOfWorkStatus = typeof BODY_OF_WORK_STATUSES[number];
|
|
32
|
+
type TaskPhase = typeof TASK_PHASES[number];
|
|
33
|
+
type DeployEnvironment = typeof DEPLOY_ENVIRONMENTS[number];
|
|
34
|
+
type VersionBump = typeof VERSION_BUMPS[number];
|
|
35
|
+
type DeployTrigger = typeof DEPLOY_TRIGGERS[number];
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Argument Schemas
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
const createBodyOfWorkSchema = {
|
|
42
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
43
|
+
title: { type: 'string' as const, required: true as const },
|
|
44
|
+
description: { type: 'string' as const },
|
|
45
|
+
auto_deploy_on_completion: { type: 'boolean' as const },
|
|
46
|
+
deploy_environment: { type: 'string' as const, validate: createEnumValidator(DEPLOY_ENVIRONMENTS) },
|
|
47
|
+
deploy_version_bump: { type: 'string' as const, validate: createEnumValidator(VERSION_BUMPS) },
|
|
48
|
+
deploy_trigger: { type: 'string' as const, validate: createEnumValidator(DEPLOY_TRIGGERS) },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const updateBodyOfWorkSchema = {
|
|
52
|
+
body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
53
|
+
title: { type: 'string' as const },
|
|
54
|
+
description: { type: 'string' as const },
|
|
55
|
+
auto_deploy_on_completion: { type: 'boolean' as const },
|
|
56
|
+
deploy_environment: { type: 'string' as const, validate: createEnumValidator(DEPLOY_ENVIRONMENTS) },
|
|
57
|
+
deploy_version_bump: { type: 'string' as const, validate: createEnumValidator(VERSION_BUMPS) },
|
|
58
|
+
deploy_trigger: { type: 'string' as const, validate: createEnumValidator(DEPLOY_TRIGGERS) },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const getBodyOfWorkSchema = {
|
|
62
|
+
body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
63
|
+
summary_only: { type: 'boolean' as const, default: false },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const getBodiesOfWorkSchema = {
|
|
67
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
68
|
+
status: { type: 'string' as const, validate: createEnumValidator(BODY_OF_WORK_STATUSES) },
|
|
69
|
+
limit: { type: 'number' as const, default: 50 },
|
|
70
|
+
offset: { type: 'number' as const, default: 0 },
|
|
71
|
+
search_query: { type: 'string' as const },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const deleteBodyOfWorkSchema = {
|
|
75
|
+
body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const addTaskToBodyOfWorkSchema = {
|
|
79
|
+
body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
80
|
+
task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
81
|
+
phase: { type: 'string' as const, validate: createEnumValidator(TASK_PHASES) },
|
|
82
|
+
order_index: { type: 'number' as const },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const removeTaskFromBodyOfWorkSchema = {
|
|
86
|
+
task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const activateBodyOfWorkSchema = {
|
|
90
|
+
body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const addTaskDependencySchema = {
|
|
94
|
+
body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
95
|
+
task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
96
|
+
depends_on_task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const removeTaskDependencySchema = {
|
|
100
|
+
task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
101
|
+
depends_on_task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const getTaskDependenciesSchema = {
|
|
105
|
+
body_of_work_id: { type: 'string' as const, validate: uuidValidator },
|
|
106
|
+
task_id: { type: 'string' as const, validate: uuidValidator },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const getNextBodyOfWorkTaskSchema = {
|
|
110
|
+
body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
111
|
+
};
|
|
30
112
|
|
|
31
113
|
export const createBodyOfWork: Handler = async (args, ctx) => {
|
|
32
114
|
const {
|
|
@@ -37,19 +119,7 @@ export const createBodyOfWork: Handler = async (args, ctx) => {
|
|
|
37
119
|
deploy_environment,
|
|
38
120
|
deploy_version_bump,
|
|
39
121
|
deploy_trigger,
|
|
40
|
-
} = args
|
|
41
|
-
project_id: string;
|
|
42
|
-
title: string;
|
|
43
|
-
description?: string;
|
|
44
|
-
auto_deploy_on_completion?: boolean;
|
|
45
|
-
deploy_environment?: DeployEnvironment;
|
|
46
|
-
deploy_version_bump?: VersionBump;
|
|
47
|
-
deploy_trigger?: DeployTrigger;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
validateRequired(project_id, 'project_id');
|
|
51
|
-
validateUUID(project_id, 'project_id');
|
|
52
|
-
validateRequired(title, 'title');
|
|
122
|
+
} = parseArgs(args, createBodyOfWorkSchema);
|
|
53
123
|
|
|
54
124
|
const { session } = ctx;
|
|
55
125
|
const apiClient = getApiClient();
|
|
@@ -95,18 +165,7 @@ export const updateBodyOfWork: Handler = async (args, ctx) => {
|
|
|
95
165
|
deploy_environment,
|
|
96
166
|
deploy_version_bump,
|
|
97
167
|
deploy_trigger,
|
|
98
|
-
} = args
|
|
99
|
-
body_of_work_id: string;
|
|
100
|
-
title?: string;
|
|
101
|
-
description?: string;
|
|
102
|
-
auto_deploy_on_completion?: boolean;
|
|
103
|
-
deploy_environment?: DeployEnvironment;
|
|
104
|
-
deploy_version_bump?: VersionBump;
|
|
105
|
-
deploy_trigger?: DeployTrigger;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
109
|
-
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
168
|
+
} = parseArgs(args, updateBodyOfWorkSchema);
|
|
110
169
|
|
|
111
170
|
// Check if any updates provided
|
|
112
171
|
if (title === undefined && description === undefined && auto_deploy_on_completion === undefined &&
|
|
@@ -134,10 +193,7 @@ export const updateBodyOfWork: Handler = async (args, ctx) => {
|
|
|
134
193
|
};
|
|
135
194
|
|
|
136
195
|
export const getBodyOfWork: Handler = async (args, ctx) => {
|
|
137
|
-
const { body_of_work_id, summary_only
|
|
138
|
-
|
|
139
|
-
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
140
|
-
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
196
|
+
const { body_of_work_id, summary_only } = parseArgs(args, getBodyOfWorkSchema);
|
|
141
197
|
|
|
142
198
|
const apiClient = getApiClient();
|
|
143
199
|
|
|
@@ -177,16 +233,7 @@ export const getBodyOfWork: Handler = async (args, ctx) => {
|
|
|
177
233
|
};
|
|
178
234
|
|
|
179
235
|
export const getBodiesOfWork: Handler = async (args, ctx) => {
|
|
180
|
-
const { project_id, status, limit
|
|
181
|
-
project_id: string;
|
|
182
|
-
status?: BodyOfWorkStatus;
|
|
183
|
-
limit?: number;
|
|
184
|
-
offset?: number;
|
|
185
|
-
search_query?: string;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
validateRequired(project_id, 'project_id');
|
|
189
|
-
validateUUID(project_id, 'project_id');
|
|
236
|
+
const { project_id, status, limit, offset, search_query } = parseArgs(args, getBodiesOfWorkSchema);
|
|
190
237
|
|
|
191
238
|
const apiClient = getApiClient();
|
|
192
239
|
|
|
@@ -208,7 +255,7 @@ export const getBodiesOfWork: Handler = async (args, ctx) => {
|
|
|
208
255
|
}>('get_bodies_of_work', {
|
|
209
256
|
project_id,
|
|
210
257
|
status,
|
|
211
|
-
limit: Math.min(limit, 100),
|
|
258
|
+
limit: Math.min(limit ?? 50, 100),
|
|
212
259
|
offset,
|
|
213
260
|
search_query
|
|
214
261
|
});
|
|
@@ -221,10 +268,7 @@ export const getBodiesOfWork: Handler = async (args, ctx) => {
|
|
|
221
268
|
};
|
|
222
269
|
|
|
223
270
|
export const deleteBodyOfWork: Handler = async (args, ctx) => {
|
|
224
|
-
const { body_of_work_id } = args
|
|
225
|
-
|
|
226
|
-
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
227
|
-
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
271
|
+
const { body_of_work_id } = parseArgs(args, deleteBodyOfWorkSchema);
|
|
228
272
|
|
|
229
273
|
const apiClient = getApiClient();
|
|
230
274
|
|
|
@@ -240,17 +284,7 @@ export const deleteBodyOfWork: Handler = async (args, ctx) => {
|
|
|
240
284
|
};
|
|
241
285
|
|
|
242
286
|
export const addTaskToBodyOfWork: Handler = async (args, ctx) => {
|
|
243
|
-
const { body_of_work_id, task_id, phase, order_index } = args
|
|
244
|
-
body_of_work_id: string;
|
|
245
|
-
task_id: string;
|
|
246
|
-
phase?: TaskPhase;
|
|
247
|
-
order_index?: number;
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
251
|
-
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
252
|
-
validateRequired(task_id, 'task_id');
|
|
253
|
-
validateUUID(task_id, 'task_id');
|
|
287
|
+
const { body_of_work_id, task_id, phase, order_index } = parseArgs(args, addTaskToBodyOfWorkSchema);
|
|
254
288
|
|
|
255
289
|
const apiClient = getApiClient();
|
|
256
290
|
|
|
@@ -275,10 +309,7 @@ export const addTaskToBodyOfWork: Handler = async (args, ctx) => {
|
|
|
275
309
|
};
|
|
276
310
|
|
|
277
311
|
export const removeTaskFromBodyOfWork: Handler = async (args, ctx) => {
|
|
278
|
-
const { task_id } = args
|
|
279
|
-
|
|
280
|
-
validateRequired(task_id, 'task_id');
|
|
281
|
-
validateUUID(task_id, 'task_id');
|
|
312
|
+
const { task_id } = parseArgs(args, removeTaskFromBodyOfWorkSchema);
|
|
282
313
|
|
|
283
314
|
const apiClient = getApiClient();
|
|
284
315
|
|
|
@@ -296,10 +327,7 @@ export const removeTaskFromBodyOfWork: Handler = async (args, ctx) => {
|
|
|
296
327
|
};
|
|
297
328
|
|
|
298
329
|
export const activateBodyOfWork: Handler = async (args, ctx) => {
|
|
299
|
-
const { body_of_work_id } = args
|
|
300
|
-
|
|
301
|
-
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
302
|
-
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
330
|
+
const { body_of_work_id } = parseArgs(args, activateBodyOfWorkSchema);
|
|
303
331
|
|
|
304
332
|
const apiClient = getApiClient();
|
|
305
333
|
|
|
@@ -319,18 +347,7 @@ export const activateBodyOfWork: Handler = async (args, ctx) => {
|
|
|
319
347
|
};
|
|
320
348
|
|
|
321
349
|
export const addTaskDependency: Handler = async (args, ctx) => {
|
|
322
|
-
const { body_of_work_id, task_id, depends_on_task_id } = args
|
|
323
|
-
body_of_work_id: string;
|
|
324
|
-
task_id: string;
|
|
325
|
-
depends_on_task_id: string;
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
329
|
-
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
330
|
-
validateRequired(task_id, 'task_id');
|
|
331
|
-
validateUUID(task_id, 'task_id');
|
|
332
|
-
validateRequired(depends_on_task_id, 'depends_on_task_id');
|
|
333
|
-
validateUUID(depends_on_task_id, 'depends_on_task_id');
|
|
350
|
+
const { body_of_work_id, task_id, depends_on_task_id } = parseArgs(args, addTaskDependencySchema);
|
|
334
351
|
|
|
335
352
|
if (task_id === depends_on_task_id) {
|
|
336
353
|
throw new Error('A task cannot depend on itself');
|
|
@@ -358,15 +375,7 @@ export const addTaskDependency: Handler = async (args, ctx) => {
|
|
|
358
375
|
};
|
|
359
376
|
|
|
360
377
|
export const removeTaskDependency: Handler = async (args, ctx) => {
|
|
361
|
-
const { task_id, depends_on_task_id } = args
|
|
362
|
-
task_id: string;
|
|
363
|
-
depends_on_task_id: string;
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
validateRequired(task_id, 'task_id');
|
|
367
|
-
validateUUID(task_id, 'task_id');
|
|
368
|
-
validateRequired(depends_on_task_id, 'depends_on_task_id');
|
|
369
|
-
validateUUID(depends_on_task_id, 'depends_on_task_id');
|
|
378
|
+
const { task_id, depends_on_task_id } = parseArgs(args, removeTaskDependencySchema);
|
|
370
379
|
|
|
371
380
|
const apiClient = getApiClient();
|
|
372
381
|
|
|
@@ -387,18 +396,12 @@ export const removeTaskDependency: Handler = async (args, ctx) => {
|
|
|
387
396
|
};
|
|
388
397
|
|
|
389
398
|
export const getTaskDependencies: Handler = async (args, ctx) => {
|
|
390
|
-
const { body_of_work_id, task_id } = args
|
|
391
|
-
body_of_work_id?: string;
|
|
392
|
-
task_id?: string;
|
|
393
|
-
};
|
|
399
|
+
const { body_of_work_id, task_id } = parseArgs(args, getTaskDependenciesSchema);
|
|
394
400
|
|
|
395
401
|
if (!body_of_work_id && !task_id) {
|
|
396
402
|
throw new Error('Either body_of_work_id or task_id is required');
|
|
397
403
|
}
|
|
398
404
|
|
|
399
|
-
if (body_of_work_id) validateUUID(body_of_work_id, 'body_of_work_id');
|
|
400
|
-
if (task_id) validateUUID(task_id, 'task_id');
|
|
401
|
-
|
|
402
405
|
const apiClient = getApiClient();
|
|
403
406
|
|
|
404
407
|
const response = await apiClient.proxy<{
|
|
@@ -421,10 +424,7 @@ export const getTaskDependencies: Handler = async (args, ctx) => {
|
|
|
421
424
|
};
|
|
422
425
|
|
|
423
426
|
export const getNextBodyOfWorkTask: Handler = async (args, ctx) => {
|
|
424
|
-
const { body_of_work_id } = args
|
|
425
|
-
|
|
426
|
-
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
427
|
-
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
427
|
+
const { body_of_work_id } = parseArgs(args, getNextBodyOfWorkTaskSchema);
|
|
428
428
|
|
|
429
429
|
const apiClient = getApiClient();
|
|
430
430
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from './cost.js';
|
|
14
14
|
import { createMockContext } from './__test-utils__.js';
|
|
15
15
|
import { mockApiClient } from './__test-setup__.js';
|
|
16
|
+
import { ValidationError } from '../validators.js';
|
|
16
17
|
|
|
17
18
|
const VALID_UUID = '123e4567-e89b-12d3-a456-426614174000';
|
|
18
19
|
|
|
@@ -26,13 +27,11 @@ describe('Cost Handlers', () => {
|
|
|
26
27
|
// ============================================================================
|
|
27
28
|
|
|
28
29
|
describe('getCostSummary', () => {
|
|
29
|
-
it('should
|
|
30
|
+
it('should throw ValidationError when project_id is missing', async () => {
|
|
30
31
|
const ctx = createMockContext();
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
expect(result.isError).toBe(true);
|
|
35
|
-
expect(result.result).toEqual({ error: 'project_id is required' });
|
|
33
|
+
await expect(getCostSummary({}, ctx)).rejects.toThrow(ValidationError);
|
|
34
|
+
await expect(getCostSummary({}, ctx)).rejects.toThrow('Missing required field: project_id');
|
|
36
35
|
});
|
|
37
36
|
|
|
38
37
|
it('should return daily cost summary with totals', async () => {
|
|
@@ -185,40 +184,37 @@ describe('Cost Handlers', () => {
|
|
|
185
184
|
// ============================================================================
|
|
186
185
|
|
|
187
186
|
describe('addCostAlert', () => {
|
|
188
|
-
it('should
|
|
187
|
+
it('should throw ValidationError when threshold_amount is missing', async () => {
|
|
189
188
|
const ctx = createMockContext();
|
|
190
189
|
|
|
191
|
-
|
|
192
|
-
{ threshold_period: 'daily' },
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
expect(result.result.error).toContain('threshold_amount must be a positive number');
|
|
190
|
+
await expect(
|
|
191
|
+
addCostAlert({ threshold_period: 'daily' }, ctx)
|
|
192
|
+
).rejects.toThrow(ValidationError);
|
|
193
|
+
await expect(
|
|
194
|
+
addCostAlert({ threshold_period: 'daily' }, ctx)
|
|
195
|
+
).rejects.toThrow('Missing required field: threshold_amount');
|
|
198
196
|
});
|
|
199
197
|
|
|
200
|
-
it('should
|
|
198
|
+
it('should throw ValidationError when threshold_amount is not positive', async () => {
|
|
201
199
|
const ctx = createMockContext();
|
|
202
200
|
|
|
203
|
-
|
|
204
|
-
{ threshold_amount: -5, threshold_period: 'daily' },
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
expect(result.result.error).toContain('threshold_amount must be a positive number');
|
|
201
|
+
await expect(
|
|
202
|
+
addCostAlert({ threshold_amount: -5, threshold_period: 'daily' }, ctx)
|
|
203
|
+
).rejects.toThrow(ValidationError);
|
|
204
|
+
await expect(
|
|
205
|
+
addCostAlert({ threshold_amount: -5, threshold_period: 'daily' }, ctx)
|
|
206
|
+
).rejects.toThrow('threshold_amount must be a positive number');
|
|
210
207
|
});
|
|
211
208
|
|
|
212
|
-
it('should
|
|
209
|
+
it('should throw ValidationError when threshold_period is invalid', async () => {
|
|
213
210
|
const ctx = createMockContext();
|
|
214
211
|
|
|
215
|
-
|
|
216
|
-
{ threshold_amount: 10, threshold_period: 'yearly' },
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
expect(result.result.error).toContain('threshold_period must be');
|
|
212
|
+
await expect(
|
|
213
|
+
addCostAlert({ threshold_amount: 10, threshold_period: 'yearly' }, ctx)
|
|
214
|
+
).rejects.toThrow(ValidationError);
|
|
215
|
+
await expect(
|
|
216
|
+
addCostAlert({ threshold_amount: 10, threshold_period: 'yearly' }, ctx)
|
|
217
|
+
).rejects.toThrow('Invalid threshold_period');
|
|
222
218
|
});
|
|
223
219
|
|
|
224
220
|
it('should create alert successfully', async () => {
|
|
@@ -291,13 +287,11 @@ describe('Cost Handlers', () => {
|
|
|
291
287
|
// ============================================================================
|
|
292
288
|
|
|
293
289
|
describe('updateCostAlert', () => {
|
|
294
|
-
it('should
|
|
290
|
+
it('should throw ValidationError when alert_id is missing', async () => {
|
|
295
291
|
const ctx = createMockContext();
|
|
296
292
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
expect(result.isError).toBe(true);
|
|
300
|
-
expect(result.result.error).toBe('alert_id is required');
|
|
293
|
+
await expect(updateCostAlert({}, ctx)).rejects.toThrow(ValidationError);
|
|
294
|
+
await expect(updateCostAlert({}, ctx)).rejects.toThrow('Missing required field: alert_id');
|
|
301
295
|
});
|
|
302
296
|
|
|
303
297
|
it('should return error when no updates provided', async () => {
|
|
@@ -367,13 +361,11 @@ describe('Cost Handlers', () => {
|
|
|
367
361
|
// ============================================================================
|
|
368
362
|
|
|
369
363
|
describe('deleteCostAlert', () => {
|
|
370
|
-
it('should
|
|
364
|
+
it('should throw ValidationError when alert_id is missing', async () => {
|
|
371
365
|
const ctx = createMockContext();
|
|
372
366
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
expect(result.isError).toBe(true);
|
|
376
|
-
expect(result.result.error).toBe('alert_id is required');
|
|
367
|
+
await expect(deleteCostAlert({}, ctx)).rejects.toThrow(ValidationError);
|
|
368
|
+
await expect(deleteCostAlert({}, ctx)).rejects.toThrow('Missing required field: alert_id');
|
|
377
369
|
});
|
|
378
370
|
|
|
379
371
|
it('should delete alert successfully', async () => {
|
|
@@ -408,13 +400,11 @@ describe('Cost Handlers', () => {
|
|
|
408
400
|
// ============================================================================
|
|
409
401
|
|
|
410
402
|
describe('getTaskCosts', () => {
|
|
411
|
-
it('should
|
|
403
|
+
it('should throw ValidationError when project_id is missing', async () => {
|
|
412
404
|
const ctx = createMockContext();
|
|
413
405
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
expect(result.isError).toBe(true);
|
|
417
|
-
expect(result.result.error).toBe('project_id is required');
|
|
406
|
+
await expect(getTaskCosts({}, ctx)).rejects.toThrow(ValidationError);
|
|
407
|
+
await expect(getTaskCosts({}, ctx)).rejects.toThrow('Missing required field: project_id');
|
|
418
408
|
});
|
|
419
409
|
|
|
420
410
|
it('should return task costs with total', async () => {
|
package/src/handlers/cost.ts
CHANGED
|
@@ -11,27 +11,68 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Handler, HandlerRegistry } from './types.js';
|
|
14
|
+
import { parseArgs, uuidValidator, createEnumValidator, ValidationError } from '../validators.js';
|
|
14
15
|
import { getApiClient } from '../api-client.js';
|
|
15
16
|
|
|
17
|
+
const VALID_PERIODS = ['daily', 'weekly', 'monthly'] as const;
|
|
18
|
+
const VALID_ALERT_TYPES = ['warning', 'critical'] as const;
|
|
19
|
+
|
|
20
|
+
type Period = typeof VALID_PERIODS[number];
|
|
21
|
+
type AlertType = typeof VALID_ALERT_TYPES[number];
|
|
22
|
+
|
|
23
|
+
// Argument schemas for type-safe parsing
|
|
24
|
+
const getCostSummarySchema = {
|
|
25
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
26
|
+
period: { type: 'string' as const, default: 'daily', validate: createEnumValidator(VALID_PERIODS) },
|
|
27
|
+
limit: { type: 'number' as const, default: 30 },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getCostAlertsSchema = {
|
|
31
|
+
project_id: { type: 'string' as const, validate: uuidValidator },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const addCostAlertSchema = {
|
|
35
|
+
project_id: { type: 'string' as const, validate: uuidValidator },
|
|
36
|
+
threshold_amount: { type: 'number' as const, required: true as const },
|
|
37
|
+
threshold_period: { type: 'string' as const, required: true as const, validate: createEnumValidator(VALID_PERIODS) },
|
|
38
|
+
alert_type: { type: 'string' as const, default: 'warning', validate: createEnumValidator(VALID_ALERT_TYPES) },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const updateCostAlertSchema = {
|
|
42
|
+
alert_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
43
|
+
threshold_amount: { type: 'number' as const },
|
|
44
|
+
threshold_period: { type: 'string' as const, validate: createEnumValidator(VALID_PERIODS) },
|
|
45
|
+
alert_type: { type: 'string' as const, validate: createEnumValidator(VALID_ALERT_TYPES) },
|
|
46
|
+
enabled: { type: 'boolean' as const },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const deleteCostAlertSchema = {
|
|
50
|
+
alert_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const getTaskCostsSchema = {
|
|
54
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
55
|
+
limit: { type: 'number' as const, default: 20 },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Custom validator for positive numbers
|
|
59
|
+
function validatePositiveNumber(value: number | undefined, fieldName: string): void {
|
|
60
|
+
if (value !== undefined && value <= 0) {
|
|
61
|
+
throw new ValidationError(`${fieldName} must be a positive number`, { field: fieldName });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
16
65
|
/**
|
|
17
66
|
* Get cost summary for a project (daily, weekly, or monthly)
|
|
18
67
|
*/
|
|
19
|
-
export const getCostSummary: Handler = async (args,
|
|
20
|
-
const { project_id, period
|
|
21
|
-
project_id: string;
|
|
22
|
-
period?: 'daily' | 'weekly' | 'monthly';
|
|
23
|
-
limit?: number;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
if (!project_id) {
|
|
27
|
-
return {
|
|
28
|
-
result: { error: 'project_id is required' },
|
|
29
|
-
isError: true,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
68
|
+
export const getCostSummary: Handler = async (args, _ctx) => {
|
|
69
|
+
const { project_id, period, limit } = parseArgs(args, getCostSummarySchema);
|
|
32
70
|
|
|
33
71
|
const apiClient = getApiClient();
|
|
34
|
-
const response = await apiClient.getCostSummary(project_id, {
|
|
72
|
+
const response = await apiClient.getCostSummary(project_id, {
|
|
73
|
+
period: period as Period,
|
|
74
|
+
limit
|
|
75
|
+
});
|
|
35
76
|
|
|
36
77
|
if (!response.ok) {
|
|
37
78
|
return {
|
|
@@ -46,8 +87,8 @@ export const getCostSummary: Handler = async (args, ctx) => {
|
|
|
46
87
|
/**
|
|
47
88
|
* Get cost alerts for the current user
|
|
48
89
|
*/
|
|
49
|
-
export const getCostAlerts: Handler = async (args,
|
|
50
|
-
const { project_id } = args
|
|
90
|
+
export const getCostAlerts: Handler = async (args, _ctx) => {
|
|
91
|
+
const { project_id } = parseArgs(args, getCostAlertsSchema);
|
|
51
92
|
|
|
52
93
|
const apiClient = getApiClient();
|
|
53
94
|
const response = await apiClient.getCostAlerts();
|
|
@@ -65,39 +106,18 @@ export const getCostAlerts: Handler = async (args, ctx) => {
|
|
|
65
106
|
/**
|
|
66
107
|
* Add a cost alert
|
|
67
108
|
*/
|
|
68
|
-
export const addCostAlert: Handler = async (args,
|
|
69
|
-
const {
|
|
70
|
-
project_id,
|
|
71
|
-
threshold_amount,
|
|
72
|
-
threshold_period,
|
|
73
|
-
alert_type = 'warning',
|
|
74
|
-
} = args as {
|
|
75
|
-
project_id?: string;
|
|
76
|
-
threshold_amount: number;
|
|
77
|
-
threshold_period: 'daily' | 'weekly' | 'monthly';
|
|
78
|
-
alert_type?: 'warning' | 'critical';
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
if (!threshold_amount || threshold_amount <= 0) {
|
|
82
|
-
return {
|
|
83
|
-
result: { error: 'threshold_amount must be a positive number' },
|
|
84
|
-
isError: true,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
109
|
+
export const addCostAlert: Handler = async (args, _ctx) => {
|
|
110
|
+
const { project_id, threshold_amount, threshold_period, alert_type } = parseArgs(args, addCostAlertSchema);
|
|
87
111
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
result: { error: 'threshold_period must be "daily", "weekly", or "monthly"' },
|
|
91
|
-
isError: true,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
112
|
+
// Additional validation for positive amount
|
|
113
|
+
validatePositiveNumber(threshold_amount, 'threshold_amount');
|
|
94
114
|
|
|
95
115
|
const apiClient = getApiClient();
|
|
96
116
|
const response = await apiClient.addCostAlert({
|
|
97
117
|
project_id,
|
|
98
|
-
threshold_amount
|
|
99
|
-
threshold_period,
|
|
100
|
-
alert_type
|
|
118
|
+
threshold_amount: threshold_amount!,
|
|
119
|
+
threshold_period: threshold_period as Period,
|
|
120
|
+
alert_type: alert_type as AlertType
|
|
101
121
|
});
|
|
102
122
|
|
|
103
123
|
if (!response.ok) {
|
|
@@ -113,46 +133,28 @@ export const addCostAlert: Handler = async (args, ctx) => {
|
|
|
113
133
|
/**
|
|
114
134
|
* Update a cost alert
|
|
115
135
|
*/
|
|
116
|
-
export const updateCostAlert: Handler = async (args,
|
|
117
|
-
const {
|
|
118
|
-
alert_id,
|
|
119
|
-
threshold_amount,
|
|
120
|
-
threshold_period,
|
|
121
|
-
alert_type,
|
|
122
|
-
enabled,
|
|
123
|
-
} = args as {
|
|
124
|
-
alert_id: string;
|
|
125
|
-
threshold_amount?: number;
|
|
126
|
-
threshold_period?: 'daily' | 'weekly' | 'monthly';
|
|
127
|
-
alert_type?: 'warning' | 'critical';
|
|
128
|
-
enabled?: boolean;
|
|
129
|
-
};
|
|
136
|
+
export const updateCostAlert: Handler = async (args, _ctx) => {
|
|
137
|
+
const { alert_id, threshold_amount, threshold_period, alert_type, enabled } = parseArgs(args, updateCostAlertSchema);
|
|
130
138
|
|
|
131
|
-
|
|
139
|
+
// Check that at least one update is provided
|
|
140
|
+
if (threshold_amount === undefined && threshold_period === undefined && alert_type === undefined && enabled === undefined) {
|
|
132
141
|
return {
|
|
133
|
-
result: { error: '
|
|
142
|
+
result: { error: 'No updates provided' },
|
|
134
143
|
isError: true,
|
|
135
144
|
};
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
const updates: {
|
|
139
148
|
threshold_amount?: number;
|
|
140
|
-
threshold_period?:
|
|
141
|
-
alert_type?:
|
|
149
|
+
threshold_period?: Period;
|
|
150
|
+
alert_type?: AlertType;
|
|
142
151
|
enabled?: boolean;
|
|
143
152
|
} = {};
|
|
144
153
|
if (threshold_amount !== undefined) updates.threshold_amount = threshold_amount;
|
|
145
|
-
if (threshold_period !== undefined) updates.threshold_period = threshold_period;
|
|
146
|
-
if (alert_type !== undefined) updates.alert_type = alert_type;
|
|
154
|
+
if (threshold_period !== undefined) updates.threshold_period = threshold_period as Period;
|
|
155
|
+
if (alert_type !== undefined) updates.alert_type = alert_type as AlertType;
|
|
147
156
|
if (enabled !== undefined) updates.enabled = enabled;
|
|
148
157
|
|
|
149
|
-
if (Object.keys(updates).length === 0) {
|
|
150
|
-
return {
|
|
151
|
-
result: { error: 'No updates provided' },
|
|
152
|
-
isError: true,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
158
|
const apiClient = getApiClient();
|
|
157
159
|
const response = await apiClient.updateCostAlert(alert_id, updates);
|
|
158
160
|
|
|
@@ -169,15 +171,8 @@ export const updateCostAlert: Handler = async (args, ctx) => {
|
|
|
169
171
|
/**
|
|
170
172
|
* Delete a cost alert
|
|
171
173
|
*/
|
|
172
|
-
export const deleteCostAlert: Handler = async (args,
|
|
173
|
-
const { alert_id } = args
|
|
174
|
-
|
|
175
|
-
if (!alert_id) {
|
|
176
|
-
return {
|
|
177
|
-
result: { error: 'alert_id is required' },
|
|
178
|
-
isError: true,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
174
|
+
export const deleteCostAlert: Handler = async (args, _ctx) => {
|
|
175
|
+
const { alert_id } = parseArgs(args, deleteCostAlertSchema);
|
|
181
176
|
|
|
182
177
|
const apiClient = getApiClient();
|
|
183
178
|
const response = await apiClient.deleteCostAlert(alert_id);
|
|
@@ -195,18 +190,8 @@ export const deleteCostAlert: Handler = async (args, ctx) => {
|
|
|
195
190
|
/**
|
|
196
191
|
* Get task costs for a project
|
|
197
192
|
*/
|
|
198
|
-
export const getTaskCosts: Handler = async (args,
|
|
199
|
-
const { project_id, limit
|
|
200
|
-
project_id: string;
|
|
201
|
-
limit?: number;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
if (!project_id) {
|
|
205
|
-
return {
|
|
206
|
-
result: { error: 'project_id is required' },
|
|
207
|
-
isError: true,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
193
|
+
export const getTaskCosts: Handler = async (args, _ctx) => {
|
|
194
|
+
const { project_id, limit } = parseArgs(args, getTaskCostsSchema);
|
|
210
195
|
|
|
211
196
|
const apiClient = getApiClient();
|
|
212
197
|
const response = await apiClient.getTaskCosts(project_id, limit);
|