@vibescope/mcp-server 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/api-client.d.ts +120 -2
- package/dist/api-client.js +51 -5
- package/dist/handlers/bodies-of-work.js +84 -50
- package/dist/handlers/cost.js +62 -54
- package/dist/handlers/decisions.js +29 -16
- package/dist/handlers/deployment.js +114 -107
- package/dist/handlers/discovery.d.ts +3 -0
- package/dist/handlers/discovery.js +55 -657
- package/dist/handlers/fallback.js +42 -28
- package/dist/handlers/file-checkouts.d.ts +18 -0
- package/dist/handlers/file-checkouts.js +101 -0
- package/dist/handlers/findings.d.ts +14 -1
- package/dist/handlers/findings.js +104 -28
- package/dist/handlers/git-issues.js +36 -32
- package/dist/handlers/ideas.js +44 -26
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/milestones.js +34 -27
- package/dist/handlers/organizations.js +86 -78
- package/dist/handlers/progress.js +22 -11
- package/dist/handlers/project.js +62 -22
- package/dist/handlers/requests.js +15 -11
- package/dist/handlers/roles.d.ts +18 -0
- package/dist/handlers/roles.js +130 -0
- package/dist/handlers/session.js +52 -15
- package/dist/handlers/sprints.js +78 -65
- package/dist/handlers/tasks.js +135 -74
- package/dist/handlers/tool-docs.d.ts +4 -3
- package/dist/handlers/tool-docs.js +252 -5
- package/dist/handlers/validation.js +30 -14
- package/dist/index.js +25 -7
- package/dist/tools.js +417 -4
- package/package.json +1 -1
- package/src/api-client.ts +161 -8
- package/src/handlers/__test-setup__.ts +12 -0
- package/src/handlers/bodies-of-work.ts +127 -111
- package/src/handlers/cost.test.ts +34 -44
- package/src/handlers/cost.ts +77 -92
- package/src/handlers/decisions.test.ts +3 -2
- package/src/handlers/decisions.ts +32 -27
- package/src/handlers/deployment.ts +144 -190
- package/src/handlers/discovery.test.ts +4 -5
- package/src/handlers/discovery.ts +60 -746
- package/src/handlers/fallback.test.ts +78 -0
- package/src/handlers/fallback.ts +51 -38
- package/src/handlers/file-checkouts.test.ts +477 -0
- package/src/handlers/file-checkouts.ts +127 -0
- package/src/handlers/findings.test.ts +274 -2
- package/src/handlers/findings.ts +123 -57
- package/src/handlers/git-issues.ts +40 -80
- package/src/handlers/ideas.ts +56 -54
- package/src/handlers/index.ts +6 -0
- package/src/handlers/milestones.test.ts +1 -1
- package/src/handlers/milestones.ts +47 -45
- package/src/handlers/organizations.ts +104 -129
- package/src/handlers/progress.ts +24 -22
- package/src/handlers/project.ts +89 -57
- package/src/handlers/requests.ts +18 -14
- package/src/handlers/roles.test.ts +303 -0
- package/src/handlers/roles.ts +208 -0
- package/src/handlers/session.test.ts +37 -2
- package/src/handlers/session.ts +64 -21
- package/src/handlers/sprints.ts +114 -134
- package/src/handlers/tasks.test.ts +61 -0
- package/src/handlers/tasks.ts +170 -139
- package/src/handlers/tool-docs.ts +1024 -0
- package/src/handlers/validation.test.ts +53 -1
- package/src/handlers/validation.ts +32 -21
- package/src/index.ts +25 -7
- package/src/tools.ts +417 -4
- 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
- package/src/knowledge.ts +0 -230
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Checkouts Handlers
|
|
3
|
+
*
|
|
4
|
+
* Handles file checkout/checkin for multi-agent coordination:
|
|
5
|
+
* - checkout_file: Check out a file before editing
|
|
6
|
+
* - checkin_file: Check in a file after editing
|
|
7
|
+
* - get_file_checkouts: Get active checkouts for a project
|
|
8
|
+
* - abandon_checkout: Force release a checkout
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Handler, HandlerRegistry } from './types.js';
|
|
12
|
+
import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
|
|
13
|
+
import { getApiClient } from '../api-client.js';
|
|
14
|
+
|
|
15
|
+
const VALID_CHECKOUT_STATUSES = ['checked_out', 'checked_in', 'abandoned'] as const;
|
|
16
|
+
|
|
17
|
+
// Argument schemas for type-safe parsing
|
|
18
|
+
const checkoutFileSchema = {
|
|
19
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
20
|
+
file_path: { type: 'string' as const, required: true as const },
|
|
21
|
+
reason: { type: 'string' as const },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const checkinFileSchema = {
|
|
25
|
+
checkout_id: { type: 'string' as const, validate: uuidValidator },
|
|
26
|
+
project_id: { type: 'string' as const, validate: uuidValidator },
|
|
27
|
+
file_path: { type: 'string' as const },
|
|
28
|
+
summary: { type: 'string' as const },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getFileCheckoutsSchema = {
|
|
32
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
33
|
+
status: { type: 'string' as const, validate: createEnumValidator(VALID_CHECKOUT_STATUSES) },
|
|
34
|
+
file_path: { type: 'string' as const },
|
|
35
|
+
limit: { type: 'number' as const, default: 50 },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const abandonCheckoutSchema = {
|
|
39
|
+
checkout_id: { type: 'string' as const, validate: uuidValidator },
|
|
40
|
+
project_id: { type: 'string' as const, validate: uuidValidator },
|
|
41
|
+
file_path: { type: 'string' as const },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const checkoutFile: Handler = async (args, ctx) => {
|
|
45
|
+
const { project_id, file_path, reason } = parseArgs(args, checkoutFileSchema);
|
|
46
|
+
|
|
47
|
+
const apiClient = getApiClient();
|
|
48
|
+
const response = await apiClient.checkoutFile(project_id, file_path, reason, ctx.session.currentSessionId || undefined);
|
|
49
|
+
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(response.error || 'Failed to checkout file');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { result: response.data };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const checkinFile: Handler = async (args, ctx) => {
|
|
58
|
+
const { checkout_id, project_id, file_path, summary } = parseArgs(args, checkinFileSchema);
|
|
59
|
+
|
|
60
|
+
// Validate that either checkout_id or both project_id and file_path are provided
|
|
61
|
+
if (!checkout_id && (!project_id || !file_path)) {
|
|
62
|
+
throw new Error('Either checkout_id or both project_id and file_path are required');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const apiClient = getApiClient();
|
|
66
|
+
const response = await apiClient.checkinFile({
|
|
67
|
+
checkout_id,
|
|
68
|
+
project_id,
|
|
69
|
+
file_path,
|
|
70
|
+
summary
|
|
71
|
+
}, ctx.session.currentSessionId || undefined);
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(response.error || 'Failed to checkin file');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { result: response.data };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const getFileCheckouts: Handler = async (args, _ctx) => {
|
|
81
|
+
const { project_id, status, file_path, limit } = parseArgs(args, getFileCheckoutsSchema);
|
|
82
|
+
|
|
83
|
+
const apiClient = getApiClient();
|
|
84
|
+
const response = await apiClient.getFileCheckouts(project_id, {
|
|
85
|
+
status,
|
|
86
|
+
file_path,
|
|
87
|
+
limit
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(response.error || 'Failed to get file checkouts');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { result: response.data };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const abandonCheckout: Handler = async (args, _ctx) => {
|
|
98
|
+
const { checkout_id, project_id, file_path } = parseArgs(args, abandonCheckoutSchema);
|
|
99
|
+
|
|
100
|
+
// Validate that either checkout_id or both project_id and file_path are provided
|
|
101
|
+
if (!checkout_id && (!project_id || !file_path)) {
|
|
102
|
+
throw new Error('Either checkout_id or both project_id and file_path are required');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const apiClient = getApiClient();
|
|
106
|
+
const response = await apiClient.abandonCheckout({
|
|
107
|
+
checkout_id,
|
|
108
|
+
project_id,
|
|
109
|
+
file_path
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new Error(response.error || 'Failed to abandon checkout');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { result: response.data };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* File Checkouts handlers registry
|
|
121
|
+
*/
|
|
122
|
+
export const fileCheckoutHandlers: HandlerRegistry = {
|
|
123
|
+
checkout_file: checkoutFile,
|
|
124
|
+
checkin_file: checkinFile,
|
|
125
|
+
get_file_checkouts: getFileCheckouts,
|
|
126
|
+
abandon_checkout: abandonCheckout,
|
|
127
|
+
};
|
|
@@ -2,8 +2,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
addFinding,
|
|
4
4
|
getFindings,
|
|
5
|
+
getFindingsStats,
|
|
5
6
|
updateFinding,
|
|
6
7
|
deleteFinding,
|
|
8
|
+
queryKnowledgeBase,
|
|
7
9
|
} from './findings.js';
|
|
8
10
|
import { ValidationError } from '../validators.js';
|
|
9
11
|
import { createMockContext } from './__test-utils__.js';
|
|
@@ -171,12 +173,74 @@ describe('getFindings', () => {
|
|
|
171
173
|
|
|
172
174
|
expect(mockApiClient.getFindings).toHaveBeenCalledWith(
|
|
173
175
|
VALID_UUID,
|
|
174
|
-
{
|
|
176
|
+
expect.objectContaining({
|
|
175
177
|
category: 'security',
|
|
176
178
|
severity: 'critical',
|
|
177
179
|
status: 'open',
|
|
178
180
|
limit: 10,
|
|
179
|
-
}
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should pass summary_only parameter to API client', async () => {
|
|
186
|
+
mockApiClient.getFindings.mockResolvedValue({
|
|
187
|
+
ok: true,
|
|
188
|
+
data: { findings: [], total_count: 0, has_more: false },
|
|
189
|
+
});
|
|
190
|
+
const ctx = createMockContext();
|
|
191
|
+
|
|
192
|
+
await getFindings({
|
|
193
|
+
project_id: VALID_UUID,
|
|
194
|
+
summary_only: true
|
|
195
|
+
}, ctx);
|
|
196
|
+
|
|
197
|
+
expect(mockApiClient.getFindings).toHaveBeenCalledWith(
|
|
198
|
+
VALID_UUID,
|
|
199
|
+
expect.objectContaining({
|
|
200
|
+
summary_only: true,
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should pass search_query parameter to API client', async () => {
|
|
206
|
+
mockApiClient.getFindings.mockResolvedValue({
|
|
207
|
+
ok: true,
|
|
208
|
+
data: { findings: [], total_count: 0, has_more: false },
|
|
209
|
+
});
|
|
210
|
+
const ctx = createMockContext();
|
|
211
|
+
|
|
212
|
+
await getFindings({
|
|
213
|
+
project_id: VALID_UUID,
|
|
214
|
+
search_query: 'security'
|
|
215
|
+
}, ctx);
|
|
216
|
+
|
|
217
|
+
expect(mockApiClient.getFindings).toHaveBeenCalledWith(
|
|
218
|
+
VALID_UUID,
|
|
219
|
+
expect.objectContaining({
|
|
220
|
+
search_query: 'security',
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should pass offset parameter to API client', async () => {
|
|
226
|
+
mockApiClient.getFindings.mockResolvedValue({
|
|
227
|
+
ok: true,
|
|
228
|
+
data: { findings: [], total_count: 100, has_more: true },
|
|
229
|
+
});
|
|
230
|
+
const ctx = createMockContext();
|
|
231
|
+
|
|
232
|
+
await getFindings({
|
|
233
|
+
project_id: VALID_UUID,
|
|
234
|
+
offset: 50,
|
|
235
|
+
limit: 25
|
|
236
|
+
}, ctx);
|
|
237
|
+
|
|
238
|
+
expect(mockApiClient.getFindings).toHaveBeenCalledWith(
|
|
239
|
+
VALID_UUID,
|
|
240
|
+
expect.objectContaining({
|
|
241
|
+
offset: 50,
|
|
242
|
+
limit: 25,
|
|
243
|
+
})
|
|
180
244
|
);
|
|
181
245
|
});
|
|
182
246
|
|
|
@@ -345,3 +409,211 @@ describe('deleteFinding', () => {
|
|
|
345
409
|
).rejects.toThrow('Delete failed');
|
|
346
410
|
});
|
|
347
411
|
});
|
|
412
|
+
|
|
413
|
+
// ============================================================================
|
|
414
|
+
// getFindingsStats Tests
|
|
415
|
+
// ============================================================================
|
|
416
|
+
|
|
417
|
+
describe('getFindingsStats', () => {
|
|
418
|
+
beforeEach(() => vi.clearAllMocks());
|
|
419
|
+
|
|
420
|
+
it('should throw error for missing project_id', async () => {
|
|
421
|
+
const ctx = createMockContext();
|
|
422
|
+
|
|
423
|
+
await expect(getFindingsStats({}, ctx)).rejects.toThrow(ValidationError);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
427
|
+
const ctx = createMockContext();
|
|
428
|
+
|
|
429
|
+
await expect(
|
|
430
|
+
getFindingsStats({ project_id: 'invalid' }, ctx)
|
|
431
|
+
).rejects.toThrow(ValidationError);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should return findings stats for project', async () => {
|
|
435
|
+
const mockStats = {
|
|
436
|
+
total: 10,
|
|
437
|
+
by_status: { open: 5, addressed: 3, dismissed: 2 },
|
|
438
|
+
by_severity: { critical: 1, high: 3, medium: 4, low: 2 },
|
|
439
|
+
by_category: { security: 3, performance: 4, code_quality: 3 },
|
|
440
|
+
};
|
|
441
|
+
mockApiClient.getFindingsStats.mockResolvedValue({
|
|
442
|
+
ok: true,
|
|
443
|
+
data: mockStats,
|
|
444
|
+
});
|
|
445
|
+
const ctx = createMockContext();
|
|
446
|
+
|
|
447
|
+
const result = await getFindingsStats({ project_id: VALID_UUID }, ctx);
|
|
448
|
+
|
|
449
|
+
expect(result.result).toMatchObject(mockStats);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should call API client getFindingsStats with project_id', async () => {
|
|
453
|
+
mockApiClient.getFindingsStats.mockResolvedValue({
|
|
454
|
+
ok: true,
|
|
455
|
+
data: { total: 0, by_status: {}, by_severity: {}, by_category: {} },
|
|
456
|
+
});
|
|
457
|
+
const ctx = createMockContext();
|
|
458
|
+
|
|
459
|
+
await getFindingsStats({ project_id: VALID_UUID }, ctx);
|
|
460
|
+
|
|
461
|
+
expect(mockApiClient.getFindingsStats).toHaveBeenCalledWith(VALID_UUID);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should throw error when API call fails', async () => {
|
|
465
|
+
mockApiClient.getFindingsStats.mockResolvedValue({
|
|
466
|
+
ok: false,
|
|
467
|
+
error: 'Query failed',
|
|
468
|
+
});
|
|
469
|
+
const ctx = createMockContext();
|
|
470
|
+
|
|
471
|
+
await expect(
|
|
472
|
+
getFindingsStats({ project_id: VALID_UUID }, ctx)
|
|
473
|
+
).rejects.toThrow('Query failed');
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// ============================================================================
|
|
478
|
+
// queryKnowledgeBase Tests
|
|
479
|
+
// ============================================================================
|
|
480
|
+
|
|
481
|
+
describe('queryKnowledgeBase', () => {
|
|
482
|
+
beforeEach(() => vi.clearAllMocks());
|
|
483
|
+
|
|
484
|
+
it('should throw error for missing project_id', async () => {
|
|
485
|
+
const ctx = createMockContext();
|
|
486
|
+
|
|
487
|
+
await expect(queryKnowledgeBase({}, ctx)).rejects.toThrow(ValidationError);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
491
|
+
const ctx = createMockContext();
|
|
492
|
+
|
|
493
|
+
await expect(
|
|
494
|
+
queryKnowledgeBase({ project_id: 'invalid' }, ctx)
|
|
495
|
+
).rejects.toThrow(ValidationError);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should query with default parameters', async () => {
|
|
499
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
500
|
+
ok: true,
|
|
501
|
+
data: {
|
|
502
|
+
findings: [],
|
|
503
|
+
decisions: [],
|
|
504
|
+
completed_tasks: [],
|
|
505
|
+
resolved_blockers: [],
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
const ctx = createMockContext();
|
|
509
|
+
|
|
510
|
+
const result = await queryKnowledgeBase({ project_id: VALID_UUID }, ctx);
|
|
511
|
+
|
|
512
|
+
expect(result.result).toMatchObject({
|
|
513
|
+
findings: [],
|
|
514
|
+
decisions: [],
|
|
515
|
+
});
|
|
516
|
+
expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
|
|
517
|
+
VALID_UUID,
|
|
518
|
+
expect.objectContaining({
|
|
519
|
+
scope: 'summary',
|
|
520
|
+
limit: 5,
|
|
521
|
+
})
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should pass scope parameter', async () => {
|
|
526
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
527
|
+
ok: true,
|
|
528
|
+
data: { findings: [] },
|
|
529
|
+
});
|
|
530
|
+
const ctx = createMockContext();
|
|
531
|
+
|
|
532
|
+
await queryKnowledgeBase({ project_id: VALID_UUID, scope: 'detailed' }, ctx);
|
|
533
|
+
|
|
534
|
+
expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
|
|
535
|
+
VALID_UUID,
|
|
536
|
+
expect.objectContaining({ scope: 'detailed' })
|
|
537
|
+
);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('should pass categories filter', async () => {
|
|
541
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
542
|
+
ok: true,
|
|
543
|
+
data: { findings: [], decisions: [] },
|
|
544
|
+
});
|
|
545
|
+
const ctx = createMockContext();
|
|
546
|
+
|
|
547
|
+
await queryKnowledgeBase({
|
|
548
|
+
project_id: VALID_UUID,
|
|
549
|
+
categories: ['findings', 'decisions']
|
|
550
|
+
}, ctx);
|
|
551
|
+
|
|
552
|
+
expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
|
|
553
|
+
VALID_UUID,
|
|
554
|
+
expect.objectContaining({
|
|
555
|
+
categories: ['findings', 'decisions']
|
|
556
|
+
})
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('should cap limit at 20', async () => {
|
|
561
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
562
|
+
ok: true,
|
|
563
|
+
data: { findings: [] },
|
|
564
|
+
});
|
|
565
|
+
const ctx = createMockContext();
|
|
566
|
+
|
|
567
|
+
await queryKnowledgeBase({ project_id: VALID_UUID, limit: 100 }, ctx);
|
|
568
|
+
|
|
569
|
+
expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
|
|
570
|
+
VALID_UUID,
|
|
571
|
+
expect.objectContaining({ limit: 20 })
|
|
572
|
+
);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('should enforce minimum limit of 1', async () => {
|
|
576
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
577
|
+
ok: true,
|
|
578
|
+
data: { findings: [] },
|
|
579
|
+
});
|
|
580
|
+
const ctx = createMockContext();
|
|
581
|
+
|
|
582
|
+
await queryKnowledgeBase({ project_id: VALID_UUID, limit: -5 }, ctx);
|
|
583
|
+
|
|
584
|
+
expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
|
|
585
|
+
VALID_UUID,
|
|
586
|
+
expect.objectContaining({ limit: 1 })
|
|
587
|
+
);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('should pass search_query', async () => {
|
|
591
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
592
|
+
ok: true,
|
|
593
|
+
data: { findings: [] },
|
|
594
|
+
});
|
|
595
|
+
const ctx = createMockContext();
|
|
596
|
+
|
|
597
|
+
await queryKnowledgeBase({
|
|
598
|
+
project_id: VALID_UUID,
|
|
599
|
+
search_query: 'security'
|
|
600
|
+
}, ctx);
|
|
601
|
+
|
|
602
|
+
expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
|
|
603
|
+
VALID_UUID,
|
|
604
|
+
expect.objectContaining({ search_query: 'security' })
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('should throw error when API call fails', async () => {
|
|
609
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
610
|
+
ok: false,
|
|
611
|
+
error: 'Query failed',
|
|
612
|
+
});
|
|
613
|
+
const ctx = createMockContext();
|
|
614
|
+
|
|
615
|
+
await expect(
|
|
616
|
+
queryKnowledgeBase({ project_id: VALID_UUID }, ctx)
|
|
617
|
+
).rejects.toThrow('Query failed');
|
|
618
|
+
});
|
|
619
|
+
});
|
package/src/handlers/findings.ts
CHANGED
|
@@ -3,42 +3,83 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles audit findings and knowledge base:
|
|
5
5
|
* - add_finding
|
|
6
|
-
* - get_findings
|
|
6
|
+
* - get_findings (supports summary_only for reduced tokens)
|
|
7
|
+
* - get_findings_stats (aggregate counts for minimal tokens)
|
|
7
8
|
* - update_finding
|
|
8
9
|
* - delete_finding
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import type { Handler, HandlerRegistry } from './types.js';
|
|
12
|
-
import {
|
|
13
|
+
import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
|
|
13
14
|
import { getApiClient } from '../api-client.js';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const VALID_FINDING_CATEGORIES = ['performance', 'security', 'code_quality', 'accessibility', 'documentation', 'architecture', 'testing', 'other'] as const;
|
|
17
|
+
const VALID_FINDING_SEVERITIES = ['info', 'low', 'medium', 'high', 'critical'] as const;
|
|
18
|
+
const VALID_FINDING_STATUSES = ['open', 'addressed', 'dismissed', 'wontfix'] as const;
|
|
19
|
+
|
|
20
|
+
type FindingCategory = typeof VALID_FINDING_CATEGORIES[number];
|
|
21
|
+
type FindingSeverity = typeof VALID_FINDING_SEVERITIES[number];
|
|
22
|
+
type FindingStatus = typeof VALID_FINDING_STATUSES[number];
|
|
23
|
+
|
|
24
|
+
// Argument schemas for type-safe parsing
|
|
25
|
+
const addFindingSchema = {
|
|
26
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
27
|
+
title: { type: 'string' as const, required: true as const },
|
|
28
|
+
description: { type: 'string' as const },
|
|
29
|
+
category: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_CATEGORIES) },
|
|
30
|
+
severity: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_SEVERITIES) },
|
|
31
|
+
file_path: { type: 'string' as const },
|
|
32
|
+
line_number: { type: 'number' as const },
|
|
33
|
+
related_task_id: { type: 'string' as const, validate: uuidValidator },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getFindingsSchema = {
|
|
37
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
38
|
+
category: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_CATEGORIES) },
|
|
39
|
+
severity: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_SEVERITIES) },
|
|
40
|
+
status: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_STATUSES) },
|
|
41
|
+
limit: { type: 'number' as const, default: 50 },
|
|
42
|
+
offset: { type: 'number' as const, default: 0 },
|
|
43
|
+
search_query: { type: 'string' as const },
|
|
44
|
+
summary_only: { type: 'boolean' as const, default: false },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const getFindingsStatsSchema = {
|
|
48
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const updateFindingSchema = {
|
|
52
|
+
finding_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
53
|
+
title: { type: 'string' as const },
|
|
54
|
+
description: { type: 'string' as const },
|
|
55
|
+
severity: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_SEVERITIES) },
|
|
56
|
+
status: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_STATUSES) },
|
|
57
|
+
resolution_note: { type: 'string' as const },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const deleteFindingSchema = {
|
|
61
|
+
finding_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const VALID_SCOPES = ['summary', 'detailed'] as const;
|
|
65
|
+
|
|
66
|
+
const queryKnowledgeBaseSchema = {
|
|
67
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
68
|
+
scope: { type: 'string' as const, default: 'summary', validate: createEnumValidator(VALID_SCOPES) },
|
|
69
|
+
categories: { type: 'array' as const },
|
|
70
|
+
limit: { type: 'number' as const, default: 5 },
|
|
71
|
+
search_query: { type: 'string' as const },
|
|
72
|
+
};
|
|
18
73
|
|
|
19
74
|
export const addFinding: Handler = async (args, ctx) => {
|
|
20
|
-
const { project_id,
|
|
21
|
-
project_id: string;
|
|
22
|
-
category?: FindingCategory;
|
|
23
|
-
title: string;
|
|
24
|
-
description?: string;
|
|
25
|
-
severity?: FindingSeverity;
|
|
26
|
-
file_path?: string;
|
|
27
|
-
line_number?: number;
|
|
28
|
-
related_task_id?: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
validateRequired(project_id, 'project_id');
|
|
32
|
-
validateUUID(project_id, 'project_id');
|
|
33
|
-
validateRequired(title, 'title');
|
|
34
|
-
if (related_task_id) validateUUID(related_task_id, 'related_task_id');
|
|
75
|
+
const { project_id, title, description, category, severity, file_path, line_number, related_task_id } = parseArgs(args, addFindingSchema);
|
|
35
76
|
|
|
36
77
|
const apiClient = getApiClient();
|
|
37
78
|
const response = await apiClient.addFinding(project_id, {
|
|
38
79
|
title,
|
|
39
80
|
description,
|
|
40
|
-
category,
|
|
41
|
-
severity,
|
|
81
|
+
category: category as FindingCategory | undefined,
|
|
82
|
+
severity: severity as FindingSeverity | undefined,
|
|
42
83
|
file_path,
|
|
43
84
|
line_number,
|
|
44
85
|
related_task_id
|
|
@@ -51,26 +92,18 @@ export const addFinding: Handler = async (args, ctx) => {
|
|
|
51
92
|
return { result: response.data };
|
|
52
93
|
};
|
|
53
94
|
|
|
54
|
-
export const getFindings: Handler = async (args,
|
|
55
|
-
const { project_id, category, severity, status, limit
|
|
56
|
-
project_id: string;
|
|
57
|
-
category?: FindingCategory;
|
|
58
|
-
severity?: FindingSeverity;
|
|
59
|
-
status?: FindingStatus;
|
|
60
|
-
limit?: number;
|
|
61
|
-
offset?: number;
|
|
62
|
-
search_query?: string;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
validateRequired(project_id, 'project_id');
|
|
66
|
-
validateUUID(project_id, 'project_id');
|
|
95
|
+
export const getFindings: Handler = async (args, _ctx) => {
|
|
96
|
+
const { project_id, category, severity, status, limit, offset, search_query, summary_only } = parseArgs(args, getFindingsSchema);
|
|
67
97
|
|
|
68
98
|
const apiClient = getApiClient();
|
|
69
99
|
const response = await apiClient.getFindings(project_id, {
|
|
70
|
-
category,
|
|
71
|
-
severity,
|
|
72
|
-
status,
|
|
73
|
-
limit
|
|
100
|
+
category: category as FindingCategory | undefined,
|
|
101
|
+
severity: severity as FindingSeverity | undefined,
|
|
102
|
+
status: status as FindingStatus | undefined,
|
|
103
|
+
limit,
|
|
104
|
+
offset,
|
|
105
|
+
search_query,
|
|
106
|
+
summary_only
|
|
74
107
|
});
|
|
75
108
|
|
|
76
109
|
if (!response.ok) {
|
|
@@ -80,25 +113,33 @@ export const getFindings: Handler = async (args, ctx) => {
|
|
|
80
113
|
return { result: response.data };
|
|
81
114
|
};
|
|
82
115
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
severity?: FindingSeverity;
|
|
91
|
-
};
|
|
116
|
+
/**
|
|
117
|
+
* Get aggregate statistics about findings for a project.
|
|
118
|
+
* Returns counts by category, severity, and status without the actual finding data.
|
|
119
|
+
* This is much more token-efficient than get_findings for understanding the overall state.
|
|
120
|
+
*/
|
|
121
|
+
export const getFindingsStats: Handler = async (args, _ctx) => {
|
|
122
|
+
const { project_id } = parseArgs(args, getFindingsStatsSchema);
|
|
92
123
|
|
|
93
|
-
|
|
94
|
-
|
|
124
|
+
const apiClient = getApiClient();
|
|
125
|
+
const response = await apiClient.getFindingsStats(project_id);
|
|
126
|
+
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error(response.error || 'Failed to get findings stats');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { result: response.data };
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const updateFinding: Handler = async (args, _ctx) => {
|
|
135
|
+
const { finding_id, title, description, severity, status, resolution_note } = parseArgs(args, updateFindingSchema);
|
|
95
136
|
|
|
96
137
|
const apiClient = getApiClient();
|
|
97
138
|
const response = await apiClient.updateFinding(finding_id, {
|
|
98
139
|
title,
|
|
99
140
|
description,
|
|
100
|
-
severity,
|
|
101
|
-
status,
|
|
141
|
+
severity: severity as FindingSeverity | undefined,
|
|
142
|
+
status: status as FindingStatus | undefined,
|
|
102
143
|
resolution_note
|
|
103
144
|
});
|
|
104
145
|
|
|
@@ -109,11 +150,8 @@ export const updateFinding: Handler = async (args, ctx) => {
|
|
|
109
150
|
return { result: response.data };
|
|
110
151
|
};
|
|
111
152
|
|
|
112
|
-
export const deleteFinding: Handler = async (args,
|
|
113
|
-
const { finding_id } = args
|
|
114
|
-
|
|
115
|
-
validateRequired(finding_id, 'finding_id');
|
|
116
|
-
validateUUID(finding_id, 'finding_id');
|
|
153
|
+
export const deleteFinding: Handler = async (args, _ctx) => {
|
|
154
|
+
const { finding_id } = parseArgs(args, deleteFindingSchema);
|
|
117
155
|
|
|
118
156
|
const apiClient = getApiClient();
|
|
119
157
|
const response = await apiClient.deleteFinding(finding_id);
|
|
@@ -125,12 +163,40 @@ export const deleteFinding: Handler = async (args, ctx) => {
|
|
|
125
163
|
return { result: response.data };
|
|
126
164
|
};
|
|
127
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Query aggregated project knowledge in a single call.
|
|
168
|
+
* Returns findings, Q&A, decisions, completed tasks, and resolved blockers.
|
|
169
|
+
* Use this instead of multiple separate tool calls to reduce token usage.
|
|
170
|
+
*/
|
|
171
|
+
export const queryKnowledgeBase: Handler = async (args, _ctx) => {
|
|
172
|
+
const { project_id, scope, categories, limit, search_query } = parseArgs(args, queryKnowledgeBaseSchema);
|
|
173
|
+
|
|
174
|
+
// Validate limit range
|
|
175
|
+
const effectiveLimit = Math.min(Math.max(1, limit ?? 5), 20);
|
|
176
|
+
|
|
177
|
+
const apiClient = getApiClient();
|
|
178
|
+
const response = await apiClient.queryKnowledgeBase(project_id, {
|
|
179
|
+
scope: scope as 'summary' | 'detailed' | undefined,
|
|
180
|
+
categories: categories as string[] | undefined,
|
|
181
|
+
limit: effectiveLimit,
|
|
182
|
+
search_query
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
throw new Error(response.error || 'Failed to query knowledge base');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { result: response.data };
|
|
190
|
+
};
|
|
191
|
+
|
|
128
192
|
/**
|
|
129
193
|
* Findings handlers registry
|
|
130
194
|
*/
|
|
131
195
|
export const findingHandlers: HandlerRegistry = {
|
|
132
196
|
add_finding: addFinding,
|
|
133
197
|
get_findings: getFindings,
|
|
198
|
+
get_findings_stats: getFindingsStats,
|
|
134
199
|
update_finding: updateFinding,
|
|
135
200
|
delete_finding: deleteFinding,
|
|
201
|
+
query_knowledge_base: queryKnowledgeBase,
|
|
136
202
|
};
|