@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.
- package/README.md +60 -7
- package/dist/api-client.d.ts +251 -1
- package/dist/api-client.js +82 -3
- package/dist/handlers/blockers.js +9 -8
- package/dist/handlers/bodies-of-work.js +96 -63
- package/dist/handlers/connectors.d.ts +45 -0
- package/dist/handlers/connectors.js +183 -0
- package/dist/handlers/cost.d.ts +10 -0
- package/dist/handlers/cost.js +112 -50
- package/dist/handlers/decisions.js +32 -19
- package/dist/handlers/deployment.js +144 -122
- package/dist/handlers/discovery.d.ts +7 -0
- package/dist/handlers/discovery.js +96 -7
- package/dist/handlers/fallback.js +29 -23
- package/dist/handlers/file-checkouts.d.ts +20 -0
- package/dist/handlers/file-checkouts.js +133 -0
- package/dist/handlers/findings.d.ts +6 -0
- package/dist/handlers/findings.js +96 -40
- package/dist/handlers/git-issues.js +40 -36
- package/dist/handlers/ideas.js +49 -31
- package/dist/handlers/index.d.ts +3 -0
- package/dist/handlers/index.js +9 -0
- package/dist/handlers/milestones.js +39 -32
- package/dist/handlers/organizations.js +99 -91
- package/dist/handlers/progress.js +24 -13
- package/dist/handlers/project.js +68 -28
- package/dist/handlers/requests.js +18 -14
- package/dist/handlers/roles.d.ts +18 -0
- package/dist/handlers/roles.js +130 -0
- package/dist/handlers/session.js +58 -17
- package/dist/handlers/sprints.js +93 -81
- package/dist/handlers/tasks.d.ts +2 -0
- package/dist/handlers/tasks.js +189 -91
- package/dist/handlers/types.d.ts +64 -2
- package/dist/handlers/types.js +48 -1
- package/dist/handlers/validation.js +21 -17
- package/dist/index.js +7 -2716
- package/dist/token-tracking.d.ts +74 -0
- package/dist/token-tracking.js +122 -0
- package/dist/tools.js +685 -9
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +17 -0
- package/docs/TOOLS.md +2053 -0
- package/package.json +4 -1
- package/scripts/generate-docs.ts +212 -0
- package/src/api-client.test.ts +718 -0
- package/src/api-client.ts +320 -6
- package/src/handlers/__test-setup__.ts +16 -0
- package/src/handlers/blockers.test.ts +31 -19
- package/src/handlers/blockers.ts +9 -8
- package/src/handlers/bodies-of-work.test.ts +55 -32
- package/src/handlers/bodies-of-work.ts +115 -115
- package/src/handlers/connectors.test.ts +834 -0
- package/src/handlers/connectors.ts +229 -0
- package/src/handlers/cost.test.ts +34 -44
- package/src/handlers/cost.ts +136 -85
- package/src/handlers/decisions.test.ts +37 -27
- package/src/handlers/decisions.ts +35 -30
- package/src/handlers/deployment.ts +180 -208
- package/src/handlers/discovery.test.ts +4 -5
- package/src/handlers/discovery.ts +98 -8
- package/src/handlers/fallback.test.ts +26 -22
- package/src/handlers/fallback.ts +36 -33
- package/src/handlers/file-checkouts.test.ts +670 -0
- package/src/handlers/file-checkouts.ts +165 -0
- package/src/handlers/findings.test.ts +178 -19
- package/src/handlers/findings.ts +112 -74
- package/src/handlers/git-issues.test.ts +51 -43
- package/src/handlers/git-issues.ts +44 -84
- package/src/handlers/ideas.test.ts +28 -23
- package/src/handlers/ideas.ts +61 -59
- package/src/handlers/index.ts +9 -0
- package/src/handlers/milestones.test.ts +33 -28
- package/src/handlers/milestones.ts +52 -50
- package/src/handlers/organizations.test.ts +104 -83
- package/src/handlers/organizations.ts +117 -142
- package/src/handlers/progress.test.ts +20 -14
- package/src/handlers/progress.ts +26 -24
- package/src/handlers/project.test.ts +34 -27
- package/src/handlers/project.ts +95 -63
- package/src/handlers/requests.test.ts +27 -18
- package/src/handlers/requests.ts +21 -17
- package/src/handlers/roles.test.ts +303 -0
- package/src/handlers/roles.ts +208 -0
- package/src/handlers/session.test.ts +47 -0
- package/src/handlers/session.ts +71 -26
- package/src/handlers/sprints.test.ts +71 -50
- package/src/handlers/sprints.ts +113 -146
- package/src/handlers/tasks.test.ts +77 -15
- package/src/handlers/tasks.ts +231 -156
- package/src/handlers/tool-categories.test.ts +66 -0
- package/src/handlers/types.ts +81 -2
- package/src/handlers/validation.test.ts +78 -45
- package/src/handlers/validation.ts +23 -25
- package/src/index.ts +12 -2732
- package/src/token-tracking.test.ts +453 -0
- package/src/token-tracking.ts +164 -0
- package/src/tools.ts +685 -9
- package/src/utils.test.ts +2 -2
- package/src/utils.ts +17 -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
|
@@ -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
|
|
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
|
@@ -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,
|
|
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
|
-
}
|
|
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, {
|
|
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,
|
|
50
|
-
const { project_id } = args
|
|
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,
|
|
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
|
-
|
|
89
|
-
|
|
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,
|
|
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
|
-
|
|
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: '
|
|
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?:
|
|
141
|
-
alert_type?:
|
|
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
|
-
|
|
170
|
+
const apiClient = getApiClient();
|
|
171
|
+
const response = await apiClient.updateCostAlert(alert_id, updates);
|
|
172
|
+
|
|
173
|
+
if (!response.ok) {
|
|
150
174
|
return {
|
|
151
|
-
result: { error: '
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
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
|
|
173
|
-
const {
|
|
224
|
+
export const getBodyOfWorkCosts: Handler = async (args, _ctx) => {
|
|
225
|
+
const { body_of_work_id, project_id } = parseArgs(args, getBodyOfWorkCostsSchema);
|
|
174
226
|
|
|
175
|
-
if (!
|
|
227
|
+
if (!body_of_work_id && !project_id) {
|
|
176
228
|
return {
|
|
177
|
-
result: { error: '
|
|
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.
|
|
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
|
|
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
|
|
248
|
+
* Get sprint costs with velocity metrics
|
|
197
249
|
*/
|
|
198
|
-
export const
|
|
199
|
-
const {
|
|
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.
|
|
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
|
|
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
|
};
|