@vibescope/mcp-server 0.2.0 → 0.2.2

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 (104) hide show
  1. package/README.md +60 -7
  2. package/dist/api-client.d.ts +251 -1
  3. package/dist/api-client.js +82 -3
  4. package/dist/handlers/blockers.js +9 -8
  5. package/dist/handlers/bodies-of-work.js +96 -63
  6. package/dist/handlers/connectors.d.ts +45 -0
  7. package/dist/handlers/connectors.js +183 -0
  8. package/dist/handlers/cost.d.ts +10 -0
  9. package/dist/handlers/cost.js +112 -50
  10. package/dist/handlers/decisions.js +32 -19
  11. package/dist/handlers/deployment.js +144 -122
  12. package/dist/handlers/discovery.d.ts +7 -0
  13. package/dist/handlers/discovery.js +96 -7
  14. package/dist/handlers/fallback.js +29 -23
  15. package/dist/handlers/file-checkouts.d.ts +20 -0
  16. package/dist/handlers/file-checkouts.js +133 -0
  17. package/dist/handlers/findings.d.ts +6 -0
  18. package/dist/handlers/findings.js +96 -40
  19. package/dist/handlers/git-issues.js +40 -36
  20. package/dist/handlers/ideas.js +49 -31
  21. package/dist/handlers/index.d.ts +3 -0
  22. package/dist/handlers/index.js +9 -0
  23. package/dist/handlers/milestones.js +39 -32
  24. package/dist/handlers/organizations.js +99 -91
  25. package/dist/handlers/progress.js +24 -13
  26. package/dist/handlers/project.js +68 -28
  27. package/dist/handlers/requests.js +18 -14
  28. package/dist/handlers/roles.d.ts +18 -0
  29. package/dist/handlers/roles.js +130 -0
  30. package/dist/handlers/session.js +58 -17
  31. package/dist/handlers/sprints.js +93 -81
  32. package/dist/handlers/tasks.d.ts +2 -0
  33. package/dist/handlers/tasks.js +189 -91
  34. package/dist/handlers/types.d.ts +64 -2
  35. package/dist/handlers/types.js +48 -1
  36. package/dist/handlers/validation.js +21 -17
  37. package/dist/index.js +7 -2716
  38. package/dist/token-tracking.d.ts +74 -0
  39. package/dist/token-tracking.js +122 -0
  40. package/dist/tools.js +685 -9
  41. package/dist/utils.d.ts +5 -0
  42. package/dist/utils.js +17 -0
  43. package/docs/TOOLS.md +2053 -0
  44. package/package.json +4 -1
  45. package/scripts/generate-docs.ts +212 -0
  46. package/src/api-client.test.ts +718 -0
  47. package/src/api-client.ts +320 -6
  48. package/src/handlers/__test-setup__.ts +16 -0
  49. package/src/handlers/blockers.test.ts +31 -19
  50. package/src/handlers/blockers.ts +9 -8
  51. package/src/handlers/bodies-of-work.test.ts +55 -32
  52. package/src/handlers/bodies-of-work.ts +115 -115
  53. package/src/handlers/connectors.test.ts +834 -0
  54. package/src/handlers/connectors.ts +229 -0
  55. package/src/handlers/cost.test.ts +34 -44
  56. package/src/handlers/cost.ts +136 -85
  57. package/src/handlers/decisions.test.ts +37 -27
  58. package/src/handlers/decisions.ts +35 -30
  59. package/src/handlers/deployment.ts +180 -208
  60. package/src/handlers/discovery.test.ts +4 -5
  61. package/src/handlers/discovery.ts +98 -8
  62. package/src/handlers/fallback.test.ts +26 -22
  63. package/src/handlers/fallback.ts +36 -33
  64. package/src/handlers/file-checkouts.test.ts +670 -0
  65. package/src/handlers/file-checkouts.ts +165 -0
  66. package/src/handlers/findings.test.ts +178 -19
  67. package/src/handlers/findings.ts +112 -74
  68. package/src/handlers/git-issues.test.ts +51 -43
  69. package/src/handlers/git-issues.ts +44 -84
  70. package/src/handlers/ideas.test.ts +28 -23
  71. package/src/handlers/ideas.ts +61 -59
  72. package/src/handlers/index.ts +9 -0
  73. package/src/handlers/milestones.test.ts +33 -28
  74. package/src/handlers/milestones.ts +52 -50
  75. package/src/handlers/organizations.test.ts +104 -83
  76. package/src/handlers/organizations.ts +117 -142
  77. package/src/handlers/progress.test.ts +20 -14
  78. package/src/handlers/progress.ts +26 -24
  79. package/src/handlers/project.test.ts +34 -27
  80. package/src/handlers/project.ts +95 -63
  81. package/src/handlers/requests.test.ts +27 -18
  82. package/src/handlers/requests.ts +21 -17
  83. package/src/handlers/roles.test.ts +303 -0
  84. package/src/handlers/roles.ts +208 -0
  85. package/src/handlers/session.test.ts +47 -0
  86. package/src/handlers/session.ts +71 -26
  87. package/src/handlers/sprints.test.ts +71 -50
  88. package/src/handlers/sprints.ts +113 -146
  89. package/src/handlers/tasks.test.ts +77 -15
  90. package/src/handlers/tasks.ts +231 -156
  91. package/src/handlers/tool-categories.test.ts +66 -0
  92. package/src/handlers/types.ts +81 -2
  93. package/src/handlers/validation.test.ts +78 -45
  94. package/src/handlers/validation.ts +23 -25
  95. package/src/index.ts +12 -2732
  96. package/src/token-tracking.test.ts +453 -0
  97. package/src/token-tracking.ts +164 -0
  98. package/src/tools.ts +685 -9
  99. package/src/utils.test.ts +2 -2
  100. package/src/utils.ts +17 -0
  101. package/dist/config/tool-categories.d.ts +0 -31
  102. package/dist/config/tool-categories.js +0 -253
  103. package/dist/knowledge.d.ts +0 -6
  104. package/dist/knowledge.js +0 -218
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Connectors Handlers
3
+ *
4
+ * Handles external integration management:
5
+ * - get_connectors
6
+ * - get_connector
7
+ * - add_connector
8
+ * - update_connector
9
+ * - delete_connector
10
+ * - test_connector
11
+ * - get_connector_events
12
+ */
13
+
14
+ import type { Handler, HandlerRegistry } from './types.js';
15
+ import { success, error } from './types.js';
16
+ import {
17
+ parseArgs,
18
+ uuidValidator,
19
+ createEnumValidator,
20
+ } from '../validators.js';
21
+ import { getApiClient } from '../api-client.js';
22
+
23
+ // Valid connector types
24
+ const VALID_CONNECTOR_TYPES = ['webhook', 'slack', 'discord', 'github', 'custom'] as const;
25
+
26
+ // Valid connector statuses
27
+ const VALID_CONNECTOR_STATUSES = ['active', 'disabled'] as const;
28
+
29
+ // Valid event statuses
30
+ const VALID_EVENT_STATUSES = ['pending', 'sent', 'failed', 'retrying'] as const;
31
+
32
+ // Argument schemas for type-safe parsing
33
+ const getConnectorsSchema = {
34
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
35
+ type: { type: 'string' as const, validate: createEnumValidator(VALID_CONNECTOR_TYPES) },
36
+ status: { type: 'string' as const, validate: createEnumValidator(VALID_CONNECTOR_STATUSES) },
37
+ limit: { type: 'number' as const, default: 50 },
38
+ offset: { type: 'number' as const, default: 0 },
39
+ };
40
+
41
+ const getConnectorSchema = {
42
+ connector_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
43
+ };
44
+
45
+ const addConnectorSchema = {
46
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
47
+ name: { type: 'string' as const, required: true as const },
48
+ type: { type: 'string' as const, required: true as const, validate: createEnumValidator(VALID_CONNECTOR_TYPES) },
49
+ description: { type: 'string' as const },
50
+ config: { type: 'object' as const },
51
+ events: { type: 'object' as const },
52
+ };
53
+
54
+ const updateConnectorSchema = {
55
+ connector_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
56
+ name: { type: 'string' as const },
57
+ description: { type: 'string' as const },
58
+ config: { type: 'object' as const },
59
+ events: { type: 'object' as const },
60
+ status: { type: 'string' as const, validate: createEnumValidator(VALID_CONNECTOR_STATUSES) },
61
+ };
62
+
63
+ const deleteConnectorSchema = {
64
+ connector_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
65
+ };
66
+
67
+ const testConnectorSchema = {
68
+ connector_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
69
+ };
70
+
71
+ const getConnectorEventsSchema = {
72
+ connector_id: { type: 'string' as const, validate: uuidValidator },
73
+ project_id: { type: 'string' as const, validate: uuidValidator },
74
+ status: { type: 'string' as const, validate: createEnumValidator(VALID_EVENT_STATUSES) },
75
+ limit: { type: 'number' as const, default: 50 },
76
+ offset: { type: 'number' as const, default: 0 },
77
+ };
78
+
79
+ /**
80
+ * Get all connectors for a project
81
+ */
82
+ export const getConnectors: Handler = async (args, _ctx) => {
83
+ const { project_id, type, status, limit, offset } = parseArgs(args, getConnectorsSchema);
84
+
85
+ const apiClient = getApiClient();
86
+ const response = await apiClient.getConnectors(project_id, {
87
+ type,
88
+ status,
89
+ limit,
90
+ offset
91
+ });
92
+
93
+ if (!response.ok) {
94
+ return error(response.error || 'Failed to fetch connectors');
95
+ }
96
+
97
+ return success(response.data);
98
+ };
99
+
100
+ /**
101
+ * Get a single connector with full details
102
+ */
103
+ export const getConnector: Handler = async (args, _ctx) => {
104
+ const { connector_id } = parseArgs(args, getConnectorSchema);
105
+
106
+ const apiClient = getApiClient();
107
+ const response = await apiClient.getConnector(connector_id);
108
+
109
+ if (!response.ok) {
110
+ return error(response.error || 'Failed to fetch connector');
111
+ }
112
+
113
+ return success(response.data);
114
+ };
115
+
116
+ /**
117
+ * Add a new connector
118
+ */
119
+ export const addConnector: Handler = async (args, _ctx) => {
120
+ const { project_id, name, type, description, config, events } = parseArgs(args, addConnectorSchema);
121
+
122
+ const apiClient = getApiClient();
123
+ const response = await apiClient.addConnector(project_id, {
124
+ name,
125
+ type,
126
+ description,
127
+ config: config as Record<string, unknown> | undefined,
128
+ events: events as Record<string, boolean> | undefined
129
+ });
130
+
131
+ if (!response.ok) {
132
+ return error(response.error || 'Failed to create connector');
133
+ }
134
+
135
+ return success(response.data);
136
+ };
137
+
138
+ /**
139
+ * Update a connector
140
+ */
141
+ export const updateConnector: Handler = async (args, _ctx) => {
142
+ const { connector_id, name, description, config, events, status } = parseArgs(args, updateConnectorSchema);
143
+
144
+ const apiClient = getApiClient();
145
+ const response = await apiClient.updateConnector(connector_id, {
146
+ name,
147
+ description,
148
+ config: config as Record<string, unknown> | undefined,
149
+ events: events as Record<string, boolean> | undefined,
150
+ status
151
+ });
152
+
153
+ if (!response.ok) {
154
+ return error(response.error || 'Failed to update connector');
155
+ }
156
+
157
+ return success(response.data);
158
+ };
159
+
160
+ /**
161
+ * Delete a connector
162
+ */
163
+ export const deleteConnector: Handler = async (args, _ctx) => {
164
+ const { connector_id } = parseArgs(args, deleteConnectorSchema);
165
+
166
+ const apiClient = getApiClient();
167
+ const response = await apiClient.deleteConnector(connector_id);
168
+
169
+ if (!response.ok) {
170
+ return error(response.error || 'Failed to delete connector');
171
+ }
172
+
173
+ return success(response.data);
174
+ };
175
+
176
+ /**
177
+ * Test a connector by sending a test event
178
+ */
179
+ export const testConnector: Handler = async (args, _ctx) => {
180
+ const { connector_id } = parseArgs(args, testConnectorSchema);
181
+
182
+ const apiClient = getApiClient();
183
+ const response = await apiClient.testConnector(connector_id);
184
+
185
+ if (!response.ok) {
186
+ return error(response.error || 'Failed to test connector');
187
+ }
188
+
189
+ return success(response.data);
190
+ };
191
+
192
+ /**
193
+ * Get connector event history
194
+ */
195
+ export const getConnectorEvents: Handler = async (args, _ctx) => {
196
+ const { connector_id, project_id, status, limit, offset } = parseArgs(args, getConnectorEventsSchema);
197
+
198
+ if (!connector_id && !project_id) {
199
+ return error('Either connector_id or project_id is required');
200
+ }
201
+
202
+ const apiClient = getApiClient();
203
+ const response = await apiClient.getConnectorEvents({
204
+ connector_id,
205
+ project_id,
206
+ status,
207
+ limit,
208
+ offset
209
+ });
210
+
211
+ if (!response.ok) {
212
+ return error(response.error || 'Failed to fetch connector events');
213
+ }
214
+
215
+ return success(response.data);
216
+ };
217
+
218
+ /**
219
+ * Connectors handlers registry
220
+ */
221
+ export const connectorHandlers: HandlerRegistry = {
222
+ get_connectors: getConnectors,
223
+ get_connector: getConnector,
224
+ add_connector: addConnector,
225
+ update_connector: updateConnector,
226
+ delete_connector: deleteConnector,
227
+ test_connector: testConnector,
228
+ get_connector_events: getConnectorEvents,
229
+ };
@@ -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 return error when project_id is missing', async () => {
30
+ it('should throw ValidationError when project_id is missing', async () => {
30
31
  const ctx = createMockContext();
31
32
 
32
- const result = await getCostSummary({}, ctx);
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 return error when threshold_amount is missing', async () => {
187
+ it('should throw ValidationError when threshold_amount is missing', async () => {
189
188
  const ctx = createMockContext();
190
189
 
191
- const result = await addCostAlert(
192
- { threshold_period: 'daily' },
193
- ctx
194
- );
195
-
196
- expect(result.isError).toBe(true);
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 return error when threshold_amount is not positive', async () => {
198
+ it('should throw ValidationError when threshold_amount is not positive', async () => {
201
199
  const ctx = createMockContext();
202
200
 
203
- const result = await addCostAlert(
204
- { threshold_amount: -5, threshold_period: 'daily' },
205
- ctx
206
- );
207
-
208
- expect(result.isError).toBe(true);
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 return error when threshold_period is invalid', async () => {
209
+ it('should throw ValidationError when threshold_period is invalid', async () => {
213
210
  const ctx = createMockContext();
214
211
 
215
- const result = await addCostAlert(
216
- { threshold_amount: 10, threshold_period: 'yearly' },
217
- ctx
218
- );
219
-
220
- expect(result.isError).toBe(true);
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 return error when alert_id is missing', async () => {
290
+ it('should throw ValidationError when alert_id is missing', async () => {
295
291
  const ctx = createMockContext();
296
292
 
297
- const result = await updateCostAlert({}, ctx);
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 return error when alert_id is missing', async () => {
364
+ it('should throw ValidationError when alert_id is missing', async () => {
371
365
  const ctx = createMockContext();
372
366
 
373
- const result = await deleteCostAlert({}, ctx);
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 return error when project_id is missing', async () => {
403
+ it('should throw ValidationError when project_id is missing', async () => {
412
404
  const ctx = createMockContext();
413
405
 
414
- const result = await getTaskCosts({}, ctx);
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 () => {
@@ -8,30 +8,83 @@
8
8
  * - update_cost_alert
9
9
  * - delete_cost_alert
10
10
  * - get_task_costs
11
+ * - get_body_of_work_costs
12
+ * - get_sprint_costs
11
13
  */
12
14
 
13
15
  import type { Handler, HandlerRegistry } from './types.js';
16
+ import { parseArgs, uuidValidator, createEnumValidator, ValidationError } from '../validators.js';
14
17
  import { getApiClient } from '../api-client.js';
15
18
 
19
+ const VALID_PERIODS = ['daily', 'weekly', 'monthly'] as const;
20
+ const VALID_ALERT_TYPES = ['warning', 'critical'] as const;
21
+
22
+ type Period = typeof VALID_PERIODS[number];
23
+ type AlertType = typeof VALID_ALERT_TYPES[number];
24
+
25
+ // Argument schemas for type-safe parsing
26
+ const getCostSummarySchema = {
27
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
28
+ period: { type: 'string' as const, default: 'daily', validate: createEnumValidator(VALID_PERIODS) },
29
+ limit: { type: 'number' as const, default: 30 },
30
+ };
31
+
32
+ const getCostAlertsSchema = {
33
+ project_id: { type: 'string' as const, validate: uuidValidator },
34
+ };
35
+
36
+ const addCostAlertSchema = {
37
+ project_id: { type: 'string' as const, validate: uuidValidator },
38
+ threshold_amount: { type: 'number' as const, required: true as const },
39
+ threshold_period: { type: 'string' as const, required: true as const, validate: createEnumValidator(VALID_PERIODS) },
40
+ alert_type: { type: 'string' as const, default: 'warning', validate: createEnumValidator(VALID_ALERT_TYPES) },
41
+ };
42
+
43
+ const updateCostAlertSchema = {
44
+ alert_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
45
+ threshold_amount: { type: 'number' as const },
46
+ threshold_period: { type: 'string' as const, validate: createEnumValidator(VALID_PERIODS) },
47
+ alert_type: { type: 'string' as const, validate: createEnumValidator(VALID_ALERT_TYPES) },
48
+ enabled: { type: 'boolean' as const },
49
+ };
50
+
51
+ const deleteCostAlertSchema = {
52
+ alert_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
53
+ };
54
+
55
+ const getTaskCostsSchema = {
56
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
57
+ limit: { type: 'number' as const, default: 20 },
58
+ };
59
+
60
+ const getBodyOfWorkCostsSchema = {
61
+ body_of_work_id: { type: 'string' as const, validate: uuidValidator },
62
+ project_id: { type: 'string' as const, validate: uuidValidator },
63
+ };
64
+
65
+ const getSprintCostsSchema = {
66
+ sprint_id: { type: 'string' as const, validate: uuidValidator },
67
+ project_id: { type: 'string' as const, validate: uuidValidator },
68
+ };
69
+
70
+ // Custom validator for positive numbers
71
+ function validatePositiveNumber(value: number | undefined, fieldName: string): void {
72
+ if (value !== undefined && value <= 0) {
73
+ throw new ValidationError(`${fieldName} must be a positive number`, { field: fieldName });
74
+ }
75
+ }
76
+
16
77
  /**
17
78
  * Get cost summary for a project (daily, weekly, or monthly)
18
79
  */
19
- export const getCostSummary: Handler = async (args, ctx) => {
20
- const { project_id, period = 'daily', limit = 30 } = args as {
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
- }
80
+ export const getCostSummary: Handler = async (args, _ctx) => {
81
+ const { project_id, period, limit } = parseArgs(args, getCostSummarySchema);
32
82
 
33
83
  const apiClient = getApiClient();
34
- const response = await apiClient.getCostSummary(project_id, { period, limit });
84
+ const response = await apiClient.getCostSummary(project_id, {
85
+ period: period as Period,
86
+ limit
87
+ });
35
88
 
36
89
  if (!response.ok) {
37
90
  return {
@@ -46,8 +99,8 @@ export const getCostSummary: Handler = async (args, ctx) => {
46
99
  /**
47
100
  * Get cost alerts for the current user
48
101
  */
49
- export const getCostAlerts: Handler = async (args, ctx) => {
50
- const { project_id } = args as { project_id?: string };
102
+ export const getCostAlerts: Handler = async (args, _ctx) => {
103
+ const { project_id } = parseArgs(args, getCostAlertsSchema);
51
104
 
52
105
  const apiClient = getApiClient();
53
106
  const response = await apiClient.getCostAlerts();
@@ -65,39 +118,18 @@ export const getCostAlerts: Handler = async (args, ctx) => {
65
118
  /**
66
119
  * Add a cost alert
67
120
  */
68
- export const addCostAlert: Handler = async (args, ctx) => {
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
- }
121
+ export const addCostAlert: Handler = async (args, _ctx) => {
122
+ const { project_id, threshold_amount, threshold_period, alert_type } = parseArgs(args, addCostAlertSchema);
87
123
 
88
- if (!threshold_period || !['daily', 'weekly', 'monthly'].includes(threshold_period)) {
89
- return {
90
- result: { error: 'threshold_period must be "daily", "weekly", or "monthly"' },
91
- isError: true,
92
- };
93
- }
124
+ // Additional validation for positive amount
125
+ validatePositiveNumber(threshold_amount, 'threshold_amount');
94
126
 
95
127
  const apiClient = getApiClient();
96
128
  const response = await apiClient.addCostAlert({
97
129
  project_id,
98
- threshold_amount,
99
- threshold_period,
100
- alert_type
130
+ threshold_amount: threshold_amount!,
131
+ threshold_period: threshold_period as Period,
132
+ alert_type: alert_type as AlertType
101
133
  });
102
134
 
103
135
  if (!response.ok) {
@@ -113,52 +145,53 @@ export const addCostAlert: Handler = async (args, ctx) => {
113
145
  /**
114
146
  * Update a cost alert
115
147
  */
116
- export const updateCostAlert: Handler = async (args, ctx) => {
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
- };
148
+ export const updateCostAlert: Handler = async (args, _ctx) => {
149
+ const { alert_id, threshold_amount, threshold_period, alert_type, enabled } = parseArgs(args, updateCostAlertSchema);
130
150
 
131
- if (!alert_id) {
151
+ // Check that at least one update is provided
152
+ if (threshold_amount === undefined && threshold_period === undefined && alert_type === undefined && enabled === undefined) {
132
153
  return {
133
- result: { error: 'alert_id is required' },
154
+ result: { error: 'No updates provided' },
134
155
  isError: true,
135
156
  };
136
157
  }
137
158
 
138
159
  const updates: {
139
160
  threshold_amount?: number;
140
- threshold_period?: 'daily' | 'weekly' | 'monthly';
141
- alert_type?: 'warning' | 'critical';
161
+ threshold_period?: Period;
162
+ alert_type?: AlertType;
142
163
  enabled?: boolean;
143
164
  } = {};
144
165
  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;
166
+ if (threshold_period !== undefined) updates.threshold_period = threshold_period as Period;
167
+ if (alert_type !== undefined) updates.alert_type = alert_type as AlertType;
147
168
  if (enabled !== undefined) updates.enabled = enabled;
148
169
 
149
- if (Object.keys(updates).length === 0) {
170
+ const apiClient = getApiClient();
171
+ const response = await apiClient.updateCostAlert(alert_id, updates);
172
+
173
+ if (!response.ok) {
150
174
  return {
151
- result: { error: 'No updates provided' },
175
+ result: { error: response.error || 'Failed to update cost alert' },
152
176
  isError: true,
153
177
  };
154
178
  }
155
179
 
180
+ return { result: response.data };
181
+ };
182
+
183
+ /**
184
+ * Delete a cost alert
185
+ */
186
+ export const deleteCostAlert: Handler = async (args, _ctx) => {
187
+ const { alert_id } = parseArgs(args, deleteCostAlertSchema);
188
+
156
189
  const apiClient = getApiClient();
157
- const response = await apiClient.updateCostAlert(alert_id, updates);
190
+ const response = await apiClient.deleteCostAlert(alert_id);
158
191
 
159
192
  if (!response.ok) {
160
193
  return {
161
- result: { error: response.error || 'Failed to update cost alert' },
194
+ result: { error: response.error || 'Failed to delete cost alert' },
162
195
  isError: true,
163
196
  };
164
197
  }
@@ -167,24 +200,43 @@ export const updateCostAlert: Handler = async (args, ctx) => {
167
200
  };
168
201
 
169
202
  /**
170
- * Delete a cost alert
203
+ * Get task costs for a project
204
+ */
205
+ export const getTaskCosts: Handler = async (args, _ctx) => {
206
+ const { project_id, limit } = parseArgs(args, getTaskCostsSchema);
207
+
208
+ const apiClient = getApiClient();
209
+ const response = await apiClient.getTaskCosts(project_id, limit);
210
+
211
+ if (!response.ok) {
212
+ return {
213
+ result: { error: response.error || 'Failed to get task costs' },
214
+ isError: true,
215
+ };
216
+ }
217
+
218
+ return { result: response.data };
219
+ };
220
+
221
+ /**
222
+ * Get body of work costs with phase breakdown
171
223
  */
172
- export const deleteCostAlert: Handler = async (args, ctx) => {
173
- const { alert_id } = args as { alert_id: string };
224
+ export const getBodyOfWorkCosts: Handler = async (args, _ctx) => {
225
+ const { body_of_work_id, project_id } = parseArgs(args, getBodyOfWorkCostsSchema);
174
226
 
175
- if (!alert_id) {
227
+ if (!body_of_work_id && !project_id) {
176
228
  return {
177
- result: { error: 'alert_id is required' },
229
+ result: { error: 'Either body_of_work_id or project_id is required' },
178
230
  isError: true,
179
231
  };
180
232
  }
181
233
 
182
234
  const apiClient = getApiClient();
183
- const response = await apiClient.deleteCostAlert(alert_id);
235
+ const response = await apiClient.getBodyOfWorkCosts({ body_of_work_id, project_id });
184
236
 
185
237
  if (!response.ok) {
186
238
  return {
187
- result: { error: response.error || 'Failed to delete cost alert' },
239
+ result: { error: response.error || 'Failed to get body of work costs' },
188
240
  isError: true,
189
241
  };
190
242
  }
@@ -193,27 +245,24 @@ export const deleteCostAlert: Handler = async (args, ctx) => {
193
245
  };
194
246
 
195
247
  /**
196
- * Get task costs for a project
248
+ * Get sprint costs with velocity metrics
197
249
  */
198
- export const getTaskCosts: Handler = async (args, ctx) => {
199
- const { project_id, limit = 20 } = args as {
200
- project_id: string;
201
- limit?: number;
202
- };
250
+ export const getSprintCosts: Handler = async (args, _ctx) => {
251
+ const { sprint_id, project_id } = parseArgs(args, getSprintCostsSchema);
203
252
 
204
- if (!project_id) {
253
+ if (!sprint_id && !project_id) {
205
254
  return {
206
- result: { error: 'project_id is required' },
255
+ result: { error: 'Either sprint_id or project_id is required' },
207
256
  isError: true,
208
257
  };
209
258
  }
210
259
 
211
260
  const apiClient = getApiClient();
212
- const response = await apiClient.getTaskCosts(project_id, limit);
261
+ const response = await apiClient.getSprintCosts({ sprint_id, project_id });
213
262
 
214
263
  if (!response.ok) {
215
264
  return {
216
- result: { error: response.error || 'Failed to get task costs' },
265
+ result: { error: response.error || 'Failed to get sprint costs' },
217
266
  isError: true,
218
267
  };
219
268
  }
@@ -231,4 +280,6 @@ export const costHandlers: HandlerRegistry = {
231
280
  update_cost_alert: updateCostAlert,
232
281
  delete_cost_alert: deleteCostAlert,
233
282
  get_task_costs: getTaskCosts,
283
+ get_body_of_work_costs: getBodyOfWorkCosts,
284
+ get_sprint_costs: getSprintCosts,
234
285
  };