@vibescope/mcp-server 0.2.2 → 0.2.4
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/CHANGELOG.md +84 -0
- package/README.md +35 -20
- package/dist/api-client.d.ts +276 -8
- package/dist/api-client.js +128 -9
- package/dist/handlers/blockers.d.ts +11 -0
- package/dist/handlers/blockers.js +37 -2
- package/dist/handlers/bodies-of-work.d.ts +2 -0
- package/dist/handlers/bodies-of-work.js +30 -1
- package/dist/handlers/connectors.js +2 -2
- package/dist/handlers/decisions.d.ts +11 -0
- package/dist/handlers/decisions.js +37 -2
- package/dist/handlers/deployment.d.ts +6 -0
- package/dist/handlers/deployment.js +33 -5
- package/dist/handlers/discovery.js +27 -11
- package/dist/handlers/fallback.js +12 -6
- package/dist/handlers/file-checkouts.d.ts +1 -0
- package/dist/handlers/file-checkouts.js +17 -2
- package/dist/handlers/findings.d.ts +5 -0
- package/dist/handlers/findings.js +19 -2
- package/dist/handlers/git-issues.js +4 -2
- package/dist/handlers/ideas.d.ts +5 -0
- package/dist/handlers/ideas.js +19 -2
- package/dist/handlers/progress.js +2 -2
- package/dist/handlers/project.d.ts +1 -0
- package/dist/handlers/project.js +35 -2
- package/dist/handlers/requests.js +6 -3
- package/dist/handlers/roles.js +13 -2
- package/dist/handlers/session.d.ts +12 -0
- package/dist/handlers/session.js +288 -25
- package/dist/handlers/sprints.d.ts +2 -0
- package/dist/handlers/sprints.js +30 -1
- package/dist/handlers/tasks.d.ts +25 -2
- package/dist/handlers/tasks.js +228 -35
- package/dist/handlers/tool-docs.js +72 -5
- package/dist/templates/agent-guidelines.d.ts +18 -0
- package/dist/templates/agent-guidelines.js +207 -0
- package/dist/tools.js +478 -125
- package/dist/utils.d.ts +5 -2
- package/dist/utils.js +90 -51
- package/package.json +51 -46
- package/scripts/version-bump.ts +203 -0
- package/src/api-client.test.ts +8 -3
- package/src/api-client.ts +376 -13
- package/src/handlers/__test-setup__.ts +5 -0
- package/src/handlers/blockers.test.ts +76 -0
- package/src/handlers/blockers.ts +56 -2
- package/src/handlers/bodies-of-work.ts +59 -1
- package/src/handlers/connectors.ts +2 -2
- package/src/handlers/decisions.test.ts +71 -2
- package/src/handlers/decisions.ts +56 -2
- package/src/handlers/deployment.test.ts +81 -0
- package/src/handlers/deployment.ts +38 -5
- package/src/handlers/discovery.ts +27 -11
- package/src/handlers/fallback.test.ts +11 -10
- package/src/handlers/fallback.ts +14 -8
- package/src/handlers/file-checkouts.test.ts +83 -3
- package/src/handlers/file-checkouts.ts +22 -2
- package/src/handlers/findings.test.ts +2 -2
- package/src/handlers/findings.ts +38 -2
- package/src/handlers/git-issues.test.ts +2 -2
- package/src/handlers/git-issues.ts +4 -2
- package/src/handlers/ideas.test.ts +1 -1
- package/src/handlers/ideas.ts +34 -2
- package/src/handlers/progress.ts +2 -2
- package/src/handlers/project.ts +47 -2
- package/src/handlers/requests.test.ts +38 -7
- package/src/handlers/requests.ts +6 -3
- package/src/handlers/roles.test.ts +1 -1
- package/src/handlers/roles.ts +20 -2
- package/src/handlers/session.test.ts +303 -4
- package/src/handlers/session.ts +335 -28
- package/src/handlers/sprints.ts +61 -1
- package/src/handlers/tasks.test.ts +0 -73
- package/src/handlers/tasks.ts +269 -40
- package/src/handlers/tool-docs.ts +77 -5
- package/src/handlers/types.test.ts +259 -0
- package/src/templates/agent-guidelines.ts +210 -0
- package/src/tools.ts +479 -125
- package/src/utils.test.ts +7 -5
- package/src/utils.ts +95 -51
package/src/handlers/blockers.ts
CHANGED
|
@@ -29,10 +29,14 @@ const resolveBlockerSchema = {
|
|
|
29
29
|
resolution_note: { type: 'string' as const },
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
const getBlockerSchema = {
|
|
33
|
+
blocker_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
34
|
+
};
|
|
35
|
+
|
|
32
36
|
const getBlockersSchema = {
|
|
33
37
|
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
34
38
|
status: { type: 'string' as const, default: 'open', validate: createEnumValidator(VALID_BLOCKER_STATUSES) },
|
|
35
|
-
limit: { type: 'number' as const, default:
|
|
39
|
+
limit: { type: 'number' as const, default: 10 },
|
|
36
40
|
offset: { type: 'number' as const, default: 0 },
|
|
37
41
|
search_query: { type: 'string' as const },
|
|
38
42
|
};
|
|
@@ -41,6 +45,10 @@ const deleteBlockerSchema = {
|
|
|
41
45
|
blocker_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
42
46
|
};
|
|
43
47
|
|
|
48
|
+
const getBlockersStatsSchema = {
|
|
49
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
50
|
+
};
|
|
51
|
+
|
|
44
52
|
export const addBlocker: Handler = async (args, ctx) => {
|
|
45
53
|
const { project_id, description } = parseArgs(args, addBlockerSchema);
|
|
46
54
|
|
|
@@ -67,13 +75,39 @@ export const resolveBlocker: Handler = async (args, _ctx) => {
|
|
|
67
75
|
return success(response.data);
|
|
68
76
|
};
|
|
69
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Get a single blocker by ID.
|
|
80
|
+
* More token-efficient than get_blockers when you need details for a specific blocker.
|
|
81
|
+
*/
|
|
82
|
+
export const getBlocker: Handler = async (args, _ctx) => {
|
|
83
|
+
const { blocker_id } = parseArgs(args, getBlockerSchema);
|
|
84
|
+
|
|
85
|
+
const apiClient = getApiClient();
|
|
86
|
+
const response = await apiClient.proxy<{
|
|
87
|
+
blocker: {
|
|
88
|
+
id: string;
|
|
89
|
+
description: string;
|
|
90
|
+
status: string;
|
|
91
|
+
resolution_note?: string;
|
|
92
|
+
created_at: string;
|
|
93
|
+
resolved_at?: string;
|
|
94
|
+
};
|
|
95
|
+
}>('get_blocker', { blocker_id });
|
|
96
|
+
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
return error(response.error || 'Failed to get blocker');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return success(response.data);
|
|
102
|
+
};
|
|
103
|
+
|
|
70
104
|
export const getBlockers: Handler = async (args, _ctx) => {
|
|
71
105
|
const { project_id, status, limit, offset, search_query } = parseArgs(args, getBlockersSchema);
|
|
72
106
|
|
|
73
107
|
const apiClient = getApiClient();
|
|
74
108
|
const response = await apiClient.getBlockers(project_id, {
|
|
75
109
|
status,
|
|
76
|
-
limit,
|
|
110
|
+
limit: Math.min(limit ?? 10, 200),
|
|
77
111
|
offset,
|
|
78
112
|
search_query
|
|
79
113
|
});
|
|
@@ -98,12 +132,32 @@ export const deleteBlocker: Handler = async (args, _ctx) => {
|
|
|
98
132
|
return success(response.data);
|
|
99
133
|
};
|
|
100
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Get aggregate statistics about blockers for a project.
|
|
137
|
+
* Returns total count and breakdown by status without the actual blocker data.
|
|
138
|
+
* This is much more token-efficient than get_blockers for understanding the overall state.
|
|
139
|
+
*/
|
|
140
|
+
export const getBlockersStats: Handler = async (args, _ctx) => {
|
|
141
|
+
const { project_id } = parseArgs(args, getBlockersStatsSchema);
|
|
142
|
+
|
|
143
|
+
const apiClient = getApiClient();
|
|
144
|
+
const response = await apiClient.getBlockersStats(project_id);
|
|
145
|
+
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
return error(response.error || 'Failed to get blockers stats');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return success(response.data);
|
|
151
|
+
};
|
|
152
|
+
|
|
101
153
|
/**
|
|
102
154
|
* Blockers handlers registry
|
|
103
155
|
*/
|
|
104
156
|
export const blockerHandlers: HandlerRegistry = {
|
|
105
157
|
add_blocker: addBlocker,
|
|
106
158
|
resolve_blocker: resolveBlocker,
|
|
159
|
+
get_blocker: getBlocker,
|
|
107
160
|
get_blockers: getBlockers,
|
|
161
|
+
get_blockers_stats: getBlockersStats,
|
|
108
162
|
delete_blocker: deleteBlocker,
|
|
109
163
|
};
|
|
@@ -22,7 +22,7 @@ import type { Handler, HandlerRegistry } from './types.js';
|
|
|
22
22
|
import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
|
|
23
23
|
import { getApiClient } from '../api-client.js';
|
|
24
24
|
|
|
25
|
-
const BODY_OF_WORK_STATUSES = ['draft', 'active', 'completed', 'cancelled'] as const;
|
|
25
|
+
const BODY_OF_WORK_STATUSES = ['draft', 'active', 'completed', 'cancelled', 'archived'] as const;
|
|
26
26
|
const TASK_PHASES = ['pre', 'core', 'post'] as const;
|
|
27
27
|
const DEPLOY_ENVIRONMENTS = ['development', 'staging', 'production'] as const;
|
|
28
28
|
const VERSION_BUMPS = ['patch', 'minor', 'major'] as const;
|
|
@@ -205,6 +205,8 @@ export const getBodyOfWork: Handler = async (args, ctx) => {
|
|
|
205
205
|
description?: string;
|
|
206
206
|
status: string;
|
|
207
207
|
progress_percentage: number;
|
|
208
|
+
total_estimated_minutes?: number;
|
|
209
|
+
completed_estimated_minutes?: number;
|
|
208
210
|
};
|
|
209
211
|
// Full response includes tasks grouped by phase
|
|
210
212
|
tasks?: {
|
|
@@ -244,6 +246,8 @@ export const getBodiesOfWork: Handler = async (args, ctx) => {
|
|
|
244
246
|
description?: string;
|
|
245
247
|
status: string;
|
|
246
248
|
progress_percentage: number;
|
|
249
|
+
total_estimated_minutes?: number;
|
|
250
|
+
completed_estimated_minutes?: number;
|
|
247
251
|
task_counts: {
|
|
248
252
|
pre: { total: number; completed: number };
|
|
249
253
|
core: { total: number; completed: number };
|
|
@@ -449,6 +453,58 @@ export const getNextBodyOfWorkTask: Handler = async (args, ctx) => {
|
|
|
449
453
|
return { result: response.data };
|
|
450
454
|
};
|
|
451
455
|
|
|
456
|
+
// ============================================================================
|
|
457
|
+
// Archive / Unarchive Handlers
|
|
458
|
+
// ============================================================================
|
|
459
|
+
|
|
460
|
+
const archiveBodyOfWorkSchema = {
|
|
461
|
+
body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const unarchiveBodyOfWorkSchema = {
|
|
465
|
+
body_of_work_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
export const archiveBodyOfWork: Handler = async (args, ctx) => {
|
|
469
|
+
const { body_of_work_id } = parseArgs(args, archiveBodyOfWorkSchema);
|
|
470
|
+
|
|
471
|
+
const apiClient = getApiClient();
|
|
472
|
+
|
|
473
|
+
const response = await apiClient.proxy<{
|
|
474
|
+
success: boolean;
|
|
475
|
+
body_of_work_id: string;
|
|
476
|
+
title: string;
|
|
477
|
+
previous_status: string;
|
|
478
|
+
new_status: string;
|
|
479
|
+
}>('archive_body_of_work', { body_of_work_id });
|
|
480
|
+
|
|
481
|
+
if (!response.ok) {
|
|
482
|
+
return { result: { error: response.error || 'Failed to archive body of work' }, isError: true };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return { result: response.data };
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
export const unarchiveBodyOfWork: Handler = async (args, ctx) => {
|
|
489
|
+
const { body_of_work_id } = parseArgs(args, unarchiveBodyOfWorkSchema);
|
|
490
|
+
|
|
491
|
+
const apiClient = getApiClient();
|
|
492
|
+
|
|
493
|
+
const response = await apiClient.proxy<{
|
|
494
|
+
success: boolean;
|
|
495
|
+
body_of_work_id: string;
|
|
496
|
+
title: string;
|
|
497
|
+
previous_status: string;
|
|
498
|
+
new_status: string;
|
|
499
|
+
}>('unarchive_body_of_work', { body_of_work_id });
|
|
500
|
+
|
|
501
|
+
if (!response.ok) {
|
|
502
|
+
return { result: { error: response.error || 'Failed to unarchive body of work' }, isError: true };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return { result: response.data };
|
|
506
|
+
};
|
|
507
|
+
|
|
452
508
|
/**
|
|
453
509
|
* Bodies of Work handlers registry
|
|
454
510
|
*/
|
|
@@ -465,4 +521,6 @@ export const bodiesOfWorkHandlers: HandlerRegistry = {
|
|
|
465
521
|
remove_task_dependency: removeTaskDependency,
|
|
466
522
|
get_task_dependencies: getTaskDependencies,
|
|
467
523
|
get_next_body_of_work_task: getNextBodyOfWorkTask,
|
|
524
|
+
archive_body_of_work: archiveBodyOfWork,
|
|
525
|
+
unarchive_body_of_work: unarchiveBodyOfWork,
|
|
468
526
|
};
|
|
@@ -86,7 +86,7 @@ export const getConnectors: Handler = async (args, _ctx) => {
|
|
|
86
86
|
const response = await apiClient.getConnectors(project_id, {
|
|
87
87
|
type,
|
|
88
88
|
status,
|
|
89
|
-
limit,
|
|
89
|
+
limit: Math.min(limit ?? 50, 50),
|
|
90
90
|
offset
|
|
91
91
|
});
|
|
92
92
|
|
|
@@ -204,7 +204,7 @@ export const getConnectorEvents: Handler = async (args, _ctx) => {
|
|
|
204
204
|
connector_id,
|
|
205
205
|
project_id,
|
|
206
206
|
status,
|
|
207
|
-
limit,
|
|
207
|
+
limit: Math.min(limit ?? 50, 50),
|
|
208
208
|
offset
|
|
209
209
|
});
|
|
210
210
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { logDecision, getDecisions, deleteDecision } from './decisions.js';
|
|
2
|
+
import { logDecision, getDecisions, getDecisionsStats, deleteDecision } from './decisions.js';
|
|
3
3
|
import { ValidationError } from '../validators.js';
|
|
4
4
|
import { createMockContext } from './__test-utils__.js';
|
|
5
5
|
import { mockApiClient } from './__test-setup__.js';
|
|
@@ -215,7 +215,7 @@ describe('getDecisions', () => {
|
|
|
215
215
|
|
|
216
216
|
expect(mockApiClient.getDecisions).toHaveBeenCalledWith(
|
|
217
217
|
'123e4567-e89b-12d3-a456-426614174000',
|
|
218
|
-
{ limit:
|
|
218
|
+
{ limit: 10, offset: 0, search_query: undefined }
|
|
219
219
|
);
|
|
220
220
|
});
|
|
221
221
|
|
|
@@ -311,3 +311,72 @@ describe('deleteDecision', () => {
|
|
|
311
311
|
});
|
|
312
312
|
});
|
|
313
313
|
});
|
|
314
|
+
|
|
315
|
+
// ============================================================================
|
|
316
|
+
// getDecisionsStats Tests
|
|
317
|
+
// ============================================================================
|
|
318
|
+
|
|
319
|
+
describe('getDecisionsStats', () => {
|
|
320
|
+
beforeEach(() => vi.clearAllMocks());
|
|
321
|
+
|
|
322
|
+
it('should throw error for missing project_id', async () => {
|
|
323
|
+
const ctx = createMockContext();
|
|
324
|
+
|
|
325
|
+
await expect(getDecisionsStats({}, ctx)).rejects.toThrow(ValidationError);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
329
|
+
const ctx = createMockContext();
|
|
330
|
+
|
|
331
|
+
await expect(
|
|
332
|
+
getDecisionsStats({ project_id: 'invalid' }, ctx)
|
|
333
|
+
).rejects.toThrow(ValidationError);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should return decisions stats', async () => {
|
|
337
|
+
mockApiClient.getDecisionsStats.mockResolvedValue({
|
|
338
|
+
ok: true,
|
|
339
|
+
data: { total: 10 },
|
|
340
|
+
});
|
|
341
|
+
const ctx = createMockContext();
|
|
342
|
+
|
|
343
|
+
const result = await getDecisionsStats(
|
|
344
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
345
|
+
ctx
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(result.result).toMatchObject({ total: 10 });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should call API client getDecisionsStats', async () => {
|
|
352
|
+
mockApiClient.getDecisionsStats.mockResolvedValue({
|
|
353
|
+
ok: true,
|
|
354
|
+
data: { total: 0 },
|
|
355
|
+
});
|
|
356
|
+
const ctx = createMockContext();
|
|
357
|
+
|
|
358
|
+
await getDecisionsStats(
|
|
359
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
360
|
+
ctx
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
expect(mockApiClient.getDecisionsStats).toHaveBeenCalledWith(
|
|
364
|
+
'123e4567-e89b-12d3-a456-426614174000'
|
|
365
|
+
);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should return error when API call fails', async () => {
|
|
369
|
+
mockApiClient.getDecisionsStats.mockResolvedValue({
|
|
370
|
+
ok: false,
|
|
371
|
+
error: 'Query failed',
|
|
372
|
+
});
|
|
373
|
+
const ctx = createMockContext();
|
|
374
|
+
|
|
375
|
+
const result = await getDecisionsStats({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
|
|
376
|
+
|
|
377
|
+
expect(result.isError).toBe(true);
|
|
378
|
+
expect(result.result).toMatchObject({
|
|
379
|
+
error: 'Query failed',
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
@@ -22,9 +22,13 @@ const logDecisionSchema = {
|
|
|
22
22
|
alternatives_considered: { type: 'array' as const },
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
const getDecisionSchema = {
|
|
26
|
+
decision_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
27
|
+
};
|
|
28
|
+
|
|
25
29
|
const getDecisionsSchema = {
|
|
26
30
|
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
27
|
-
limit: { type: 'number' as const, default:
|
|
31
|
+
limit: { type: 'number' as const, default: 10 },
|
|
28
32
|
offset: { type: 'number' as const, default: 0 },
|
|
29
33
|
search_query: { type: 'string' as const },
|
|
30
34
|
};
|
|
@@ -33,6 +37,10 @@ const deleteDecisionSchema = {
|
|
|
33
37
|
decision_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
34
38
|
};
|
|
35
39
|
|
|
40
|
+
const getDecisionsStatsSchema = {
|
|
41
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
42
|
+
};
|
|
43
|
+
|
|
36
44
|
export const logDecision: Handler = async (args, ctx) => {
|
|
37
45
|
const { project_id, title, description, rationale, alternatives_considered } = parseArgs(args, logDecisionSchema);
|
|
38
46
|
|
|
@@ -53,13 +61,39 @@ export const logDecision: Handler = async (args, ctx) => {
|
|
|
53
61
|
return { result: { success: true, title, decision_id: response.data?.decision_id } };
|
|
54
62
|
};
|
|
55
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Get a single decision by ID.
|
|
66
|
+
* More token-efficient than get_decisions when you need details for a specific decision.
|
|
67
|
+
*/
|
|
68
|
+
export const getDecision: Handler = async (args, _ctx) => {
|
|
69
|
+
const { decision_id } = parseArgs(args, getDecisionSchema);
|
|
70
|
+
|
|
71
|
+
const apiClient = getApiClient();
|
|
72
|
+
const response = await apiClient.proxy<{
|
|
73
|
+
decision: {
|
|
74
|
+
id: string;
|
|
75
|
+
title: string;
|
|
76
|
+
description: string;
|
|
77
|
+
rationale?: string;
|
|
78
|
+
alternatives_considered?: string[];
|
|
79
|
+
created_at: string;
|
|
80
|
+
};
|
|
81
|
+
}>('get_decision', { decision_id });
|
|
82
|
+
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
return { result: { error: response.error || 'Failed to get decision' }, isError: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { result: response.data };
|
|
88
|
+
};
|
|
89
|
+
|
|
56
90
|
export const getDecisions: Handler = async (args, _ctx) => {
|
|
57
91
|
const { project_id, limit, offset, search_query } = parseArgs(args, getDecisionsSchema);
|
|
58
92
|
|
|
59
93
|
const apiClient = getApiClient();
|
|
60
94
|
|
|
61
95
|
const response = await apiClient.getDecisions(project_id, {
|
|
62
|
-
limit,
|
|
96
|
+
limit: Math.min(limit ?? 10, 200),
|
|
63
97
|
offset,
|
|
64
98
|
search_query
|
|
65
99
|
});
|
|
@@ -89,11 +123,31 @@ export const deleteDecision: Handler = async (args, _ctx) => {
|
|
|
89
123
|
return { result: { success: true } };
|
|
90
124
|
};
|
|
91
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Get aggregate statistics about decisions for a project.
|
|
128
|
+
* Returns total count without the actual decision data.
|
|
129
|
+
* This is more token-efficient than get_decisions for understanding overall state.
|
|
130
|
+
*/
|
|
131
|
+
export const getDecisionsStats: Handler = async (args, _ctx) => {
|
|
132
|
+
const { project_id } = parseArgs(args, getDecisionsStatsSchema);
|
|
133
|
+
|
|
134
|
+
const apiClient = getApiClient();
|
|
135
|
+
const response = await apiClient.getDecisionsStats(project_id);
|
|
136
|
+
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
return { result: { error: response.error || 'Failed to get decisions stats' }, isError: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { result: response.data };
|
|
142
|
+
};
|
|
143
|
+
|
|
92
144
|
/**
|
|
93
145
|
* Decisions handlers registry
|
|
94
146
|
*/
|
|
95
147
|
export const decisionHandlers: HandlerRegistry = {
|
|
96
148
|
log_decision: logDecision,
|
|
149
|
+
get_decision: getDecision,
|
|
97
150
|
get_decisions: getDecisions,
|
|
151
|
+
get_decisions_stats: getDecisionsStats,
|
|
98
152
|
delete_decision: deleteDecision,
|
|
99
153
|
};
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
cancelDeployment,
|
|
9
9
|
addDeploymentRequirement,
|
|
10
10
|
getDeploymentRequirements,
|
|
11
|
+
getDeploymentRequirementsStats,
|
|
11
12
|
} from './deployment.js';
|
|
12
13
|
import { ValidationError } from '../validators.js';
|
|
13
14
|
import { createMockContext } from './__test-utils__.js';
|
|
@@ -467,4 +468,84 @@ describe('getDeploymentRequirements', () => {
|
|
|
467
468
|
expect((result.result as { requirements: unknown[] }).requirements).toHaveLength(2);
|
|
468
469
|
expect(result.result).toHaveProperty('deployment_blocked', true);
|
|
469
470
|
});
|
|
471
|
+
|
|
472
|
+
it('should pass pagination params to API client', async () => {
|
|
473
|
+
mockApiClient.getDeploymentRequirements.mockResolvedValue({
|
|
474
|
+
ok: true,
|
|
475
|
+
data: { requirements: [], deployment_blocked: false },
|
|
476
|
+
});
|
|
477
|
+
const ctx = createMockContext();
|
|
478
|
+
|
|
479
|
+
await getDeploymentRequirements(
|
|
480
|
+
{ project_id: VALID_UUID, limit: 10, offset: 5 },
|
|
481
|
+
ctx
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
expect(mockApiClient.getDeploymentRequirements).toHaveBeenCalledWith(
|
|
485
|
+
VALID_UUID,
|
|
486
|
+
expect.objectContaining({ limit: 10, offset: 5 })
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// ============================================================================
|
|
492
|
+
// getDeploymentRequirementsStats Tests
|
|
493
|
+
// ============================================================================
|
|
494
|
+
|
|
495
|
+
describe('getDeploymentRequirementsStats', () => {
|
|
496
|
+
beforeEach(() => vi.clearAllMocks());
|
|
497
|
+
|
|
498
|
+
it('should throw error for missing project_id', async () => {
|
|
499
|
+
const ctx = createMockContext();
|
|
500
|
+
await expect(getDeploymentRequirementsStats({}, ctx)).rejects.toThrow(ValidationError);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
504
|
+
const ctx = createMockContext();
|
|
505
|
+
await expect(
|
|
506
|
+
getDeploymentRequirementsStats({ project_id: 'invalid' }, ctx)
|
|
507
|
+
).rejects.toThrow(ValidationError);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should return stats from API client', async () => {
|
|
511
|
+
const mockStats = {
|
|
512
|
+
total: 5,
|
|
513
|
+
by_status: { pending: 3, completed: 2 },
|
|
514
|
+
by_stage: { preparation: 2, deployment: 2, verification: 1 },
|
|
515
|
+
by_type: { migration: 2, env_var: 2, manual: 1 },
|
|
516
|
+
};
|
|
517
|
+
mockApiClient.getDeploymentRequirementsStats.mockResolvedValue({
|
|
518
|
+
ok: true,
|
|
519
|
+
data: mockStats,
|
|
520
|
+
});
|
|
521
|
+
const ctx = createMockContext();
|
|
522
|
+
|
|
523
|
+
const result = await getDeploymentRequirementsStats({ project_id: VALID_UUID }, ctx);
|
|
524
|
+
|
|
525
|
+
expect(result.result).toMatchObject(mockStats);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('should call API client with correct project_id', async () => {
|
|
529
|
+
mockApiClient.getDeploymentRequirementsStats.mockResolvedValue({
|
|
530
|
+
ok: true,
|
|
531
|
+
data: { total: 0, by_status: {}, by_stage: {}, by_type: {} },
|
|
532
|
+
});
|
|
533
|
+
const ctx = createMockContext();
|
|
534
|
+
|
|
535
|
+
await getDeploymentRequirementsStats({ project_id: VALID_UUID }, ctx);
|
|
536
|
+
|
|
537
|
+
expect(mockApiClient.getDeploymentRequirementsStats).toHaveBeenCalledWith(VALID_UUID);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('should handle API error', async () => {
|
|
541
|
+
mockApiClient.getDeploymentRequirementsStats.mockResolvedValue({
|
|
542
|
+
ok: false,
|
|
543
|
+
error: 'Database error',
|
|
544
|
+
});
|
|
545
|
+
const ctx = createMockContext();
|
|
546
|
+
|
|
547
|
+
const result = await getDeploymentRequirementsStats({ project_id: VALID_UUID }, ctx);
|
|
548
|
+
|
|
549
|
+
expect(result.result).toHaveProperty('error');
|
|
550
|
+
});
|
|
470
551
|
});
|
|
@@ -87,7 +87,7 @@ const addDeploymentRequirementSchema = {
|
|
|
87
87
|
file_path: { type: 'string' as const },
|
|
88
88
|
stage: { type: 'string' as const, default: 'preparation', validate: createEnumValidator(REQUIREMENT_STAGES) },
|
|
89
89
|
blocking: { type: 'boolean' as const, default: false },
|
|
90
|
-
recurring: { type: 'boolean' as const, default:
|
|
90
|
+
recurring: { type: 'boolean' as const, default: true },
|
|
91
91
|
};
|
|
92
92
|
|
|
93
93
|
const completeDeploymentRequirementSchema = {
|
|
@@ -98,6 +98,12 @@ const getDeploymentRequirementsSchema = {
|
|
|
98
98
|
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
99
99
|
status: { type: 'string' as const, default: 'pending', validate: createEnumValidator(REQUIREMENT_STATUSES) },
|
|
100
100
|
stage: { type: 'string' as const, validate: createEnumValidator([...REQUIREMENT_STAGES, 'all'] as const) },
|
|
101
|
+
limit: { type: 'number' as const, default: 50 },
|
|
102
|
+
offset: { type: 'number' as const, default: 0 },
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const getDeploymentRequirementsStatsSchema = {
|
|
106
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
101
107
|
};
|
|
102
108
|
|
|
103
109
|
const scheduleDeploymentSchema = {
|
|
@@ -115,6 +121,8 @@ const scheduleDeploymentSchema = {
|
|
|
115
121
|
const getScheduledDeploymentsSchema = {
|
|
116
122
|
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
117
123
|
include_disabled: { type: 'boolean' as const, default: false },
|
|
124
|
+
limit: { type: 'number' as const, default: 50 },
|
|
125
|
+
offset: { type: 'number' as const, default: 0 },
|
|
118
126
|
};
|
|
119
127
|
|
|
120
128
|
const updateScheduledDeploymentSchema = {
|
|
@@ -291,12 +299,14 @@ export const completeDeploymentRequirement: Handler = async (args, ctx) => {
|
|
|
291
299
|
};
|
|
292
300
|
|
|
293
301
|
export const getDeploymentRequirements: Handler = async (args, ctx) => {
|
|
294
|
-
const { project_id, status, stage } = parseArgs(args, getDeploymentRequirementsSchema);
|
|
302
|
+
const { project_id, status, stage, limit, offset } = parseArgs(args, getDeploymentRequirementsSchema);
|
|
295
303
|
|
|
296
304
|
const apiClient = getApiClient();
|
|
297
305
|
const response = await apiClient.getDeploymentRequirements(project_id, {
|
|
298
306
|
status: status as 'pending' | 'completed' | 'converted_to_task' | 'all',
|
|
299
|
-
stage: stage as 'preparation' | 'deployment' | 'verification' | 'all' | undefined
|
|
307
|
+
stage: stage as 'preparation' | 'deployment' | 'verification' | 'all' | undefined,
|
|
308
|
+
limit,
|
|
309
|
+
offset
|
|
300
310
|
});
|
|
301
311
|
|
|
302
312
|
if (!response.ok) {
|
|
@@ -306,6 +316,24 @@ export const getDeploymentRequirements: Handler = async (args, ctx) => {
|
|
|
306
316
|
return { result: response.data };
|
|
307
317
|
};
|
|
308
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Get aggregate statistics about deployment requirements for a project.
|
|
321
|
+
* Returns total count and breakdowns by status, stage, and type.
|
|
322
|
+
* More token-efficient than get_deployment_requirements when you just need to understand the overall state.
|
|
323
|
+
*/
|
|
324
|
+
export const getDeploymentRequirementsStats: Handler = async (args, ctx) => {
|
|
325
|
+
const { project_id } = parseArgs(args, getDeploymentRequirementsStatsSchema);
|
|
326
|
+
|
|
327
|
+
const apiClient = getApiClient();
|
|
328
|
+
const response = await apiClient.getDeploymentRequirementsStats(project_id);
|
|
329
|
+
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
return { result: { error: response.error || 'Failed to get deployment requirements stats' }, isError: true };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { result: response.data };
|
|
335
|
+
};
|
|
336
|
+
|
|
309
337
|
// ============================================================================
|
|
310
338
|
// Scheduled Deployments
|
|
311
339
|
// ============================================================================
|
|
@@ -366,10 +394,14 @@ export const scheduleDeployment: Handler = async (args, ctx) => {
|
|
|
366
394
|
};
|
|
367
395
|
|
|
368
396
|
export const getScheduledDeployments: Handler = async (args, ctx) => {
|
|
369
|
-
const { project_id, include_disabled } = parseArgs(args, getScheduledDeploymentsSchema);
|
|
397
|
+
const { project_id, include_disabled, limit, offset } = parseArgs(args, getScheduledDeploymentsSchema);
|
|
370
398
|
|
|
371
399
|
const apiClient = getApiClient();
|
|
372
|
-
const response = await apiClient.getScheduledDeployments(project_id,
|
|
400
|
+
const response = await apiClient.getScheduledDeployments(project_id, {
|
|
401
|
+
includeDisabled: include_disabled,
|
|
402
|
+
limit,
|
|
403
|
+
offset
|
|
404
|
+
});
|
|
373
405
|
|
|
374
406
|
if (!response.ok) {
|
|
375
407
|
return { result: { error: response.error || 'Failed to get scheduled deployments' }, isError: true };
|
|
@@ -498,6 +530,7 @@ export const deploymentHandlers: HandlerRegistry = {
|
|
|
498
530
|
add_deployment_requirement: addDeploymentRequirement,
|
|
499
531
|
complete_deployment_requirement: completeDeploymentRequirement,
|
|
500
532
|
get_deployment_requirements: getDeploymentRequirements,
|
|
533
|
+
get_deployment_requirements_stats: getDeploymentRequirementsStats,
|
|
501
534
|
// Scheduled deployments
|
|
502
535
|
schedule_deployment: scheduleDeployment,
|
|
503
536
|
get_scheduled_deployments: getScheduledDeployments,
|