@vibescope/mcp-server 0.2.1 → 0.2.3

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 (93) hide show
  1. package/README.md +63 -38
  2. package/dist/api-client.d.ts +187 -0
  3. package/dist/api-client.js +53 -1
  4. package/dist/handlers/blockers.js +9 -8
  5. package/dist/handlers/bodies-of-work.js +14 -14
  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 +54 -0
  10. package/dist/handlers/decisions.js +3 -3
  11. package/dist/handlers/deployment.js +35 -19
  12. package/dist/handlers/discovery.d.ts +7 -0
  13. package/dist/handlers/discovery.js +61 -2
  14. package/dist/handlers/fallback.js +5 -4
  15. package/dist/handlers/file-checkouts.d.ts +2 -0
  16. package/dist/handlers/file-checkouts.js +38 -6
  17. package/dist/handlers/findings.js +13 -12
  18. package/dist/handlers/git-issues.js +4 -4
  19. package/dist/handlers/ideas.js +5 -5
  20. package/dist/handlers/index.d.ts +1 -0
  21. package/dist/handlers/index.js +3 -0
  22. package/dist/handlers/milestones.js +5 -5
  23. package/dist/handlers/organizations.js +13 -13
  24. package/dist/handlers/progress.js +2 -2
  25. package/dist/handlers/project.js +6 -6
  26. package/dist/handlers/requests.js +3 -3
  27. package/dist/handlers/session.js +28 -9
  28. package/dist/handlers/sprints.js +17 -17
  29. package/dist/handlers/tasks.d.ts +2 -0
  30. package/dist/handlers/tasks.js +78 -20
  31. package/dist/handlers/types.d.ts +64 -2
  32. package/dist/handlers/types.js +48 -1
  33. package/dist/handlers/validation.js +3 -3
  34. package/dist/index.js +7 -2716
  35. package/dist/token-tracking.d.ts +74 -0
  36. package/dist/token-tracking.js +122 -0
  37. package/dist/tools.js +298 -9
  38. package/dist/utils.d.ts +5 -0
  39. package/dist/utils.js +17 -0
  40. package/docs/TOOLS.md +2053 -0
  41. package/package.json +4 -1
  42. package/scripts/generate-docs.ts +212 -0
  43. package/src/api-client.test.ts +723 -0
  44. package/src/api-client.ts +236 -1
  45. package/src/handlers/__test-setup__.ts +9 -0
  46. package/src/handlers/blockers.test.ts +31 -19
  47. package/src/handlers/blockers.ts +9 -8
  48. package/src/handlers/bodies-of-work.test.ts +55 -32
  49. package/src/handlers/bodies-of-work.ts +14 -14
  50. package/src/handlers/connectors.test.ts +834 -0
  51. package/src/handlers/connectors.ts +229 -0
  52. package/src/handlers/cost.ts +66 -0
  53. package/src/handlers/decisions.test.ts +34 -25
  54. package/src/handlers/decisions.ts +3 -3
  55. package/src/handlers/deployment.ts +39 -19
  56. package/src/handlers/discovery.ts +61 -2
  57. package/src/handlers/fallback.test.ts +26 -22
  58. package/src/handlers/fallback.ts +5 -4
  59. package/src/handlers/file-checkouts.test.ts +242 -49
  60. package/src/handlers/file-checkouts.ts +44 -6
  61. package/src/handlers/findings.test.ts +38 -24
  62. package/src/handlers/findings.ts +13 -12
  63. package/src/handlers/git-issues.test.ts +51 -43
  64. package/src/handlers/git-issues.ts +4 -4
  65. package/src/handlers/ideas.test.ts +28 -23
  66. package/src/handlers/ideas.ts +5 -5
  67. package/src/handlers/index.ts +3 -0
  68. package/src/handlers/milestones.test.ts +33 -28
  69. package/src/handlers/milestones.ts +5 -5
  70. package/src/handlers/organizations.test.ts +104 -83
  71. package/src/handlers/organizations.ts +13 -13
  72. package/src/handlers/progress.test.ts +20 -14
  73. package/src/handlers/progress.ts +2 -2
  74. package/src/handlers/project.test.ts +34 -27
  75. package/src/handlers/project.ts +6 -6
  76. package/src/handlers/requests.test.ts +27 -18
  77. package/src/handlers/requests.ts +3 -3
  78. package/src/handlers/session.test.ts +47 -0
  79. package/src/handlers/session.ts +26 -9
  80. package/src/handlers/sprints.test.ts +71 -50
  81. package/src/handlers/sprints.ts +17 -17
  82. package/src/handlers/tasks.test.ts +77 -15
  83. package/src/handlers/tasks.ts +90 -21
  84. package/src/handlers/tool-categories.test.ts +66 -0
  85. package/src/handlers/types.ts +81 -2
  86. package/src/handlers/validation.test.ts +78 -45
  87. package/src/handlers/validation.ts +3 -3
  88. package/src/index.ts +12 -2732
  89. package/src/token-tracking.test.ts +453 -0
  90. package/src/token-tracking.ts +164 -0
  91. package/src/tools.ts +298 -9
  92. package/src/utils.test.ts +2 -2
  93. package/src/utils.ts +17 -0
@@ -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
+ };
@@ -8,6 +8,8 @@
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';
@@ -55,6 +57,16 @@ const getTaskCostsSchema = {
55
57
  limit: { type: 'number' as const, default: 20 },
56
58
  };
57
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
+
58
70
  // Custom validator for positive numbers
59
71
  function validatePositiveNumber(value: number | undefined, fieldName: string): void {
60
72
  if (value !== undefined && value <= 0) {
@@ -206,6 +218,58 @@ export const getTaskCosts: Handler = async (args, _ctx) => {
206
218
  return { result: response.data };
207
219
  };
208
220
 
221
+ /**
222
+ * Get body of work costs with phase breakdown
223
+ */
224
+ export const getBodyOfWorkCosts: Handler = async (args, _ctx) => {
225
+ const { body_of_work_id, project_id } = parseArgs(args, getBodyOfWorkCostsSchema);
226
+
227
+ if (!body_of_work_id && !project_id) {
228
+ return {
229
+ result: { error: 'Either body_of_work_id or project_id is required' },
230
+ isError: true,
231
+ };
232
+ }
233
+
234
+ const apiClient = getApiClient();
235
+ const response = await apiClient.getBodyOfWorkCosts({ body_of_work_id, project_id });
236
+
237
+ if (!response.ok) {
238
+ return {
239
+ result: { error: response.error || 'Failed to get body of work costs' },
240
+ isError: true,
241
+ };
242
+ }
243
+
244
+ return { result: response.data };
245
+ };
246
+
247
+ /**
248
+ * Get sprint costs with velocity metrics
249
+ */
250
+ export const getSprintCosts: Handler = async (args, _ctx) => {
251
+ const { sprint_id, project_id } = parseArgs(args, getSprintCostsSchema);
252
+
253
+ if (!sprint_id && !project_id) {
254
+ return {
255
+ result: { error: 'Either sprint_id or project_id is required' },
256
+ isError: true,
257
+ };
258
+ }
259
+
260
+ const apiClient = getApiClient();
261
+ const response = await apiClient.getSprintCosts({ sprint_id, project_id });
262
+
263
+ if (!response.ok) {
264
+ return {
265
+ result: { error: response.error || 'Failed to get sprint costs' },
266
+ isError: true,
267
+ };
268
+ }
269
+
270
+ return { result: response.data };
271
+ };
272
+
209
273
  /**
210
274
  * Cost handlers registry
211
275
  */
@@ -216,4 +280,6 @@ export const costHandlers: HandlerRegistry = {
216
280
  update_cost_alert: updateCostAlert,
217
281
  delete_cost_alert: deleteCostAlert,
218
282
  get_task_costs: getTaskCosts,
283
+ get_body_of_work_costs: getBodyOfWorkCosts,
284
+ get_sprint_costs: getSprintCosts,
219
285
  };
@@ -119,23 +119,26 @@ describe('logDecision', () => {
119
119
  );
120
120
  });
121
121
 
122
- it('should throw error when API call fails', async () => {
122
+ it('should return error when API call fails', async () => {
123
123
  mockApiClient.logDecision.mockResolvedValue({
124
124
  ok: false,
125
125
  error: 'Insert failed',
126
126
  });
127
127
  const ctx = createMockContext();
128
128
 
129
- await expect(
130
- logDecision(
131
- {
132
- project_id: '123e4567-e89b-12d3-a456-426614174000',
133
- title: 'Test',
134
- description: 'Test',
135
- },
136
- ctx
137
- )
138
- ).rejects.toThrow('Failed to log decision: Insert failed');
129
+ const result = await logDecision(
130
+ {
131
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
132
+ title: 'Test',
133
+ description: 'Test',
134
+ },
135
+ ctx
136
+ );
137
+
138
+ expect(result.isError).toBe(true);
139
+ expect(result.result).toMatchObject({
140
+ error: 'Insert failed',
141
+ });
139
142
  });
140
143
  });
141
144
 
@@ -216,19 +219,22 @@ describe('getDecisions', () => {
216
219
  );
217
220
  });
218
221
 
219
- it('should throw error when query fails', async () => {
222
+ it('should return error when query fails', async () => {
220
223
  mockApiClient.getDecisions.mockResolvedValue({
221
224
  ok: false,
222
225
  error: 'Query failed',
223
226
  });
224
227
  const ctx = createMockContext();
225
228
 
226
- await expect(
227
- getDecisions(
228
- { project_id: '123e4567-e89b-12d3-a456-426614174000' },
229
- ctx
230
- )
231
- ).rejects.toThrow('Failed to fetch decisions: Query failed');
229
+ const result = await getDecisions(
230
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
231
+ ctx
232
+ );
233
+
234
+ expect(result.isError).toBe(true);
235
+ expect(result.result).toMatchObject({
236
+ error: 'Query failed',
237
+ });
232
238
  });
233
239
  });
234
240
 
@@ -287,18 +293,21 @@ describe('deleteDecision', () => {
287
293
  );
288
294
  });
289
295
 
290
- it('should throw error when delete fails', async () => {
296
+ it('should return error when delete fails', async () => {
291
297
  mockApiClient.deleteDecision.mockResolvedValue({
292
298
  ok: false,
293
299
  error: 'Delete failed',
294
300
  });
295
301
  const ctx = createMockContext();
296
302
 
297
- await expect(
298
- deleteDecision(
299
- { decision_id: '123e4567-e89b-12d3-a456-426614174000' },
300
- ctx
301
- )
302
- ).rejects.toThrow('Failed to delete decision: Delete failed');
303
+ const result = await deleteDecision(
304
+ { decision_id: '123e4567-e89b-12d3-a456-426614174000' },
305
+ ctx
306
+ );
307
+
308
+ expect(result.isError).toBe(true);
309
+ expect(result.result).toMatchObject({
310
+ error: 'Delete failed',
311
+ });
303
312
  });
304
313
  });
@@ -47,7 +47,7 @@ export const logDecision: Handler = async (args, ctx) => {
47
47
  }, session.currentSessionId || undefined);
48
48
 
49
49
  if (!response.ok) {
50
- throw new Error(`Failed to log decision: ${response.error}`);
50
+ return { result: { error: response.error || 'Failed to log decision' }, isError: true };
51
51
  }
52
52
 
53
53
  return { result: { success: true, title, decision_id: response.data?.decision_id } };
@@ -65,7 +65,7 @@ export const getDecisions: Handler = async (args, _ctx) => {
65
65
  });
66
66
 
67
67
  if (!response.ok) {
68
- throw new Error(`Failed to fetch decisions: ${response.error}`);
68
+ return { result: { error: response.error || 'Failed to fetch decisions' }, isError: true };
69
69
  }
70
70
 
71
71
  return {
@@ -83,7 +83,7 @@ export const deleteDecision: Handler = async (args, _ctx) => {
83
83
  const response = await apiClient.deleteDecision(decision_id);
84
84
 
85
85
  if (!response.ok) {
86
- throw new Error(`Failed to delete decision: ${response.error}`);
86
+ return { result: { error: response.error || 'Failed to delete decision' }, isError: true };
87
87
  }
88
88
 
89
89
  return { result: { success: true } };
@@ -28,7 +28,7 @@ const VERSION_BUMPS = ['patch', 'minor', 'major'] as const;
28
28
  const REQUIREMENT_TYPES = ['migration', 'env_var', 'config', 'manual', 'breaking_change', 'agent_task'] as const;
29
29
  const REQUIREMENT_STAGES = ['preparation', 'deployment', 'verification'] as const;
30
30
  const REQUIREMENT_STATUSES = ['pending', 'completed', 'converted_to_task', 'all'] as const;
31
- const SCHEDULE_TYPES = ['once', 'daily', 'weekly', 'monthly'] as const;
31
+ const SCHEDULE_TYPES = ['once', 'hourly', 'daily', 'weekly', 'monthly'] as const;
32
32
 
33
33
  type Environment = typeof ENVIRONMENTS[number];
34
34
  type VersionBump = typeof VERSION_BUMPS[number];
@@ -107,6 +107,7 @@ const scheduleDeploymentSchema = {
107
107
  schedule_type: { type: 'string' as const, default: 'once', validate: createEnumValidator(SCHEDULE_TYPES) },
108
108
  scheduled_at: { type: 'string' as const, required: true as const },
109
109
  auto_trigger: { type: 'boolean' as const, default: true },
110
+ hours_interval: { type: 'number' as const, default: 1 },
110
111
  notes: { type: 'string' as const },
111
112
  git_ref: { type: 'string' as const },
112
113
  };
@@ -123,6 +124,7 @@ const updateScheduledDeploymentSchema = {
123
124
  schedule_type: { type: 'string' as const, validate: createEnumValidator(SCHEDULE_TYPES) },
124
125
  scheduled_at: { type: 'string' as const },
125
126
  auto_trigger: { type: 'boolean' as const },
127
+ hours_interval: { type: 'number' as const },
126
128
  enabled: { type: 'boolean' as const },
127
129
  notes: { type: 'string' as const },
128
130
  git_ref: { type: 'string' as const },
@@ -153,7 +155,7 @@ export const requestDeployment: Handler = async (args, ctx) => {
153
155
  });
154
156
 
155
157
  if (!response.ok) {
156
- throw new Error(response.error || 'Failed to request deployment');
158
+ return { result: { error: response.error || 'Failed to request deployment' }, isError: true };
157
159
  }
158
160
 
159
161
  return { result: response.data };
@@ -170,7 +172,7 @@ export const claimDeploymentValidation: Handler = async (args, ctx) => {
170
172
  );
171
173
 
172
174
  if (!response.ok) {
173
- throw new Error(response.error || 'Failed to claim deployment validation');
175
+ return { result: { error: response.error || 'Failed to claim deployment validation' }, isError: true };
174
176
  }
175
177
 
176
178
  return { result: response.data };
@@ -188,7 +190,7 @@ export const reportValidation: Handler = async (args, ctx) => {
188
190
  });
189
191
 
190
192
  if (!response.ok) {
191
- throw new Error(response.error || 'Failed to report validation');
193
+ return { result: { error: response.error || 'Failed to report validation' }, isError: true };
192
194
  }
193
195
 
194
196
  return { result: response.data };
@@ -201,7 +203,7 @@ export const checkDeploymentStatus: Handler = async (args, ctx) => {
201
203
  const response = await apiClient.checkDeploymentStatus(project_id);
202
204
 
203
205
  if (!response.ok) {
204
- throw new Error(response.error || 'Failed to check deployment status');
206
+ return { result: { error: response.error || 'Failed to check deployment status' }, isError: true };
205
207
  }
206
208
 
207
209
  return { result: response.data };
@@ -218,7 +220,7 @@ export const startDeployment: Handler = async (args, ctx) => {
218
220
  );
219
221
 
220
222
  if (!response.ok) {
221
- throw new Error(response.error || 'Failed to start deployment');
223
+ return { result: { error: response.error || 'Failed to start deployment' }, isError: true };
222
224
  }
223
225
 
224
226
  return { result: response.data };
@@ -235,7 +237,7 @@ export const completeDeployment: Handler = async (args, ctx) => {
235
237
  });
236
238
 
237
239
  if (!response.ok) {
238
- throw new Error(response.error || 'Failed to complete deployment');
240
+ return { result: { error: response.error || 'Failed to complete deployment' }, isError: true };
239
241
  }
240
242
 
241
243
  return { result: response.data };
@@ -248,7 +250,7 @@ export const cancelDeployment: Handler = async (args, ctx) => {
248
250
  const response = await apiClient.cancelDeployment(project_id, reason);
249
251
 
250
252
  if (!response.ok) {
251
- throw new Error(response.error || 'Failed to cancel deployment');
253
+ return { result: { error: response.error || 'Failed to cancel deployment' }, isError: true };
252
254
  }
253
255
 
254
256
  return { result: response.data };
@@ -269,7 +271,7 @@ export const addDeploymentRequirement: Handler = async (args, ctx) => {
269
271
  });
270
272
 
271
273
  if (!response.ok) {
272
- throw new Error(response.error || 'Failed to add deployment requirement');
274
+ return { result: { error: response.error || 'Failed to add deployment requirement' }, isError: true };
273
275
  }
274
276
 
275
277
  return { result: response.data };
@@ -282,7 +284,7 @@ export const completeDeploymentRequirement: Handler = async (args, ctx) => {
282
284
  const response = await apiClient.completeDeploymentRequirement(requirement_id);
283
285
 
284
286
  if (!response.ok) {
285
- throw new Error(response.error || 'Failed to complete deployment requirement');
287
+ return { result: { error: response.error || 'Failed to complete deployment requirement' }, isError: true };
286
288
  }
287
289
 
288
290
  return { result: response.data };
@@ -298,7 +300,7 @@ export const getDeploymentRequirements: Handler = async (args, ctx) => {
298
300
  });
299
301
 
300
302
  if (!response.ok) {
301
- throw new Error(response.error || 'Failed to get deployment requirements');
303
+ return { result: { error: response.error || 'Failed to get deployment requirements' }, isError: true };
302
304
  }
303
305
 
304
306
  return { result: response.data };
@@ -316,6 +318,7 @@ export const scheduleDeployment: Handler = async (args, ctx) => {
316
318
  schedule_type,
317
319
  scheduled_at,
318
320
  auto_trigger,
321
+ hours_interval,
319
322
  notes,
320
323
  git_ref,
321
324
  } = parseArgs(args, scheduleDeploymentSchema);
@@ -335,19 +338,28 @@ export const scheduleDeployment: Handler = async (args, ctx) => {
335
338
  });
336
339
  }
337
340
 
341
+ // Validate hours_interval for hourly schedule type (default is 1)
342
+ const hoursInterval = hours_interval ?? 1;
343
+ if (schedule_type === 'hourly' && (hoursInterval < 1 || hoursInterval > 24)) {
344
+ throw new ValidationError('hours_interval must be between 1 and 24', {
345
+ field: 'hours_interval',
346
+ });
347
+ }
348
+
338
349
  const apiClient = getApiClient();
339
350
  const response = await apiClient.scheduleDeployment(project_id, {
340
351
  environment: environment as 'development' | 'staging' | 'production',
341
352
  version_bump: version_bump as 'patch' | 'minor' | 'major',
342
- schedule_type: schedule_type as 'once' | 'daily' | 'weekly' | 'monthly',
353
+ schedule_type: schedule_type as 'once' | 'hourly' | 'daily' | 'weekly' | 'monthly',
343
354
  scheduled_at: scheduledDate.toISOString(),
344
355
  auto_trigger,
356
+ hours_interval: hoursInterval,
345
357
  notes,
346
358
  git_ref
347
359
  });
348
360
 
349
361
  if (!response.ok) {
350
- throw new Error(response.error || 'Failed to schedule deployment');
362
+ return { result: { error: response.error || 'Failed to schedule deployment' }, isError: true };
351
363
  }
352
364
 
353
365
  return { result: response.data };
@@ -360,7 +372,7 @@ export const getScheduledDeployments: Handler = async (args, ctx) => {
360
372
  const response = await apiClient.getScheduledDeployments(project_id, include_disabled);
361
373
 
362
374
  if (!response.ok) {
363
- throw new Error(response.error || 'Failed to get scheduled deployments');
375
+ return { result: { error: response.error || 'Failed to get scheduled deployments' }, isError: true };
364
376
  }
365
377
 
366
378
  return { result: response.data };
@@ -374,6 +386,7 @@ export const updateScheduledDeployment: Handler = async (args, ctx) => {
374
386
  schedule_type,
375
387
  scheduled_at,
376
388
  auto_trigger,
389
+ hours_interval,
377
390
  enabled,
378
391
  notes,
379
392
  git_ref,
@@ -394,6 +407,12 @@ export const updateScheduledDeployment: Handler = async (args, ctx) => {
394
407
  }
395
408
 
396
409
  if (auto_trigger !== undefined) updates.auto_trigger = auto_trigger;
410
+ if (hours_interval !== undefined) {
411
+ if (hours_interval < 1 || hours_interval > 24) {
412
+ throw new ValidationError('hours_interval must be between 1 and 24');
413
+ }
414
+ updates.hours_interval = hours_interval;
415
+ }
397
416
  if (enabled !== undefined) updates.enabled = enabled;
398
417
  if (notes !== undefined) updates.notes = notes;
399
418
  if (git_ref !== undefined) updates.git_ref = git_ref;
@@ -406,16 +425,17 @@ export const updateScheduledDeployment: Handler = async (args, ctx) => {
406
425
  const response = await apiClient.updateScheduledDeployment(schedule_id, updates as {
407
426
  environment?: 'development' | 'staging' | 'production';
408
427
  version_bump?: 'patch' | 'minor' | 'major';
409
- schedule_type?: 'once' | 'daily' | 'weekly' | 'monthly';
428
+ schedule_type?: 'once' | 'hourly' | 'daily' | 'weekly' | 'monthly';
410
429
  scheduled_at?: string;
411
430
  auto_trigger?: boolean;
431
+ hours_interval?: number;
412
432
  enabled?: boolean;
413
433
  notes?: string;
414
434
  git_ref?: string;
415
435
  });
416
436
 
417
437
  if (!response.ok) {
418
- throw new Error(response.error || 'Failed to update scheduled deployment');
438
+ return { result: { error: response.error || 'Failed to update scheduled deployment' }, isError: true };
419
439
  }
420
440
 
421
441
  return { result: response.data };
@@ -428,7 +448,7 @@ export const deleteScheduledDeployment: Handler = async (args, ctx) => {
428
448
  const response = await apiClient.deleteScheduledDeployment(schedule_id);
429
449
 
430
450
  if (!response.ok) {
431
- throw new Error(response.error || 'Failed to delete scheduled deployment');
451
+ return { result: { error: response.error || 'Failed to delete scheduled deployment' }, isError: true };
432
452
  }
433
453
 
434
454
  return { result: response.data };
@@ -445,7 +465,7 @@ export const triggerScheduledDeployment: Handler = async (args, ctx) => {
445
465
  );
446
466
 
447
467
  if (!response.ok) {
448
- throw new Error(response.error || 'Failed to trigger scheduled deployment');
468
+ return { result: { error: response.error || 'Failed to trigger scheduled deployment' }, isError: true };
449
469
  }
450
470
 
451
471
  return { result: response.data };
@@ -458,7 +478,7 @@ export const checkDueDeployments: Handler = async (args, ctx) => {
458
478
  const response = await apiClient.checkDueDeployments(project_id);
459
479
 
460
480
  if (!response.ok) {
461
- throw new Error(response.error || 'Failed to check due deployments');
481
+ return { result: { error: response.error || 'Failed to check due deployments' }, isError: true };
462
482
  }
463
483
 
464
484
  return { result: response.data };