@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,165 @@
|
|
|
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
|
+
* - is_file_available: Check if a file is available for checkout
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Handler, HandlerRegistry } from './types.js';
|
|
13
|
+
import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
|
|
14
|
+
import { getApiClient } from '../api-client.js';
|
|
15
|
+
|
|
16
|
+
const VALID_CHECKOUT_STATUSES = ['checked_out', 'checked_in', 'abandoned'] as const;
|
|
17
|
+
|
|
18
|
+
// Argument schemas for type-safe parsing
|
|
19
|
+
const checkoutFileSchema = {
|
|
20
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
21
|
+
file_path: { type: 'string' as const, required: true as const },
|
|
22
|
+
reason: { type: 'string' as const },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const checkinFileSchema = {
|
|
26
|
+
checkout_id: { type: 'string' as const, validate: uuidValidator },
|
|
27
|
+
project_id: { type: 'string' as const, validate: uuidValidator },
|
|
28
|
+
file_path: { type: 'string' as const },
|
|
29
|
+
summary: { type: 'string' as const },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const getFileCheckoutsSchema = {
|
|
33
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
34
|
+
status: { type: 'string' as const, validate: createEnumValidator(VALID_CHECKOUT_STATUSES) },
|
|
35
|
+
file_path: { type: 'string' as const },
|
|
36
|
+
limit: { type: 'number' as const, default: 50 },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const abandonCheckoutSchema = {
|
|
40
|
+
checkout_id: { type: 'string' as const, validate: uuidValidator },
|
|
41
|
+
project_id: { type: 'string' as const, validate: uuidValidator },
|
|
42
|
+
file_path: { type: 'string' as const },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const isFileAvailableSchema = {
|
|
46
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
47
|
+
file_path: { type: 'string' as const, required: true as const },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const checkoutFile: Handler = async (args, ctx) => {
|
|
51
|
+
const { project_id, file_path, reason } = parseArgs(args, checkoutFileSchema);
|
|
52
|
+
|
|
53
|
+
const apiClient = getApiClient();
|
|
54
|
+
const response = await apiClient.checkoutFile(project_id, file_path, reason, ctx.session.currentSessionId || undefined);
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
return { result: { error: response.error || 'Failed to checkout file' }, isError: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { result: response.data };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const checkinFile: Handler = async (args, ctx) => {
|
|
64
|
+
const { checkout_id, project_id, file_path, summary } = parseArgs(args, checkinFileSchema);
|
|
65
|
+
|
|
66
|
+
// Validate that either checkout_id or both project_id and file_path are provided
|
|
67
|
+
if (!checkout_id && (!project_id || !file_path)) {
|
|
68
|
+
return { result: { error: 'Either checkout_id or both project_id and file_path are required' }, isError: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const apiClient = getApiClient();
|
|
72
|
+
const response = await apiClient.checkinFile({
|
|
73
|
+
checkout_id,
|
|
74
|
+
project_id,
|
|
75
|
+
file_path,
|
|
76
|
+
summary
|
|
77
|
+
}, ctx.session.currentSessionId || undefined);
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
return { result: { error: response.error || 'Failed to checkin file' }, isError: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { result: response.data };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const getFileCheckouts: Handler = async (args, _ctx) => {
|
|
87
|
+
const { project_id, status, file_path, limit } = parseArgs(args, getFileCheckoutsSchema);
|
|
88
|
+
|
|
89
|
+
const apiClient = getApiClient();
|
|
90
|
+
const response = await apiClient.getFileCheckouts(project_id, {
|
|
91
|
+
status,
|
|
92
|
+
file_path,
|
|
93
|
+
limit
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
return { result: { error: response.error || 'Failed to get file checkouts' }, isError: true };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { result: response.data };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const abandonCheckout: Handler = async (args, _ctx) => {
|
|
104
|
+
const { checkout_id, project_id, file_path } = parseArgs(args, abandonCheckoutSchema);
|
|
105
|
+
|
|
106
|
+
// Validate that either checkout_id or both project_id and file_path are provided
|
|
107
|
+
if (!checkout_id && (!project_id || !file_path)) {
|
|
108
|
+
return { result: { error: 'Either checkout_id or both project_id and file_path are required' }, isError: true };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const apiClient = getApiClient();
|
|
112
|
+
const response = await apiClient.abandonCheckout({
|
|
113
|
+
checkout_id,
|
|
114
|
+
project_id,
|
|
115
|
+
file_path
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
return { result: { error: response.error || 'Failed to abandon checkout' }, isError: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { result: response.data };
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const isFileAvailable: Handler = async (args, _ctx) => {
|
|
126
|
+
const { project_id, file_path } = parseArgs(args, isFileAvailableSchema);
|
|
127
|
+
|
|
128
|
+
const apiClient = getApiClient();
|
|
129
|
+
const response = await apiClient.getFileCheckouts(project_id, {
|
|
130
|
+
status: 'checked_out',
|
|
131
|
+
file_path,
|
|
132
|
+
limit: 1
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
return { result: { error: response.error || 'Failed to check file availability' }, isError: true };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const checkouts = response.data?.checkouts || [];
|
|
140
|
+
const activeCheckout = checkouts.length > 0 ? checkouts[0] : null;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
result: {
|
|
144
|
+
available: !activeCheckout,
|
|
145
|
+
file_path,
|
|
146
|
+
checked_out_by: activeCheckout ? {
|
|
147
|
+
checkout_id: activeCheckout.id,
|
|
148
|
+
checked_out_by: activeCheckout.checked_out_by,
|
|
149
|
+
checked_out_at: activeCheckout.checked_out_at,
|
|
150
|
+
reason: activeCheckout.checkout_reason
|
|
151
|
+
} : null
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* File Checkouts handlers registry
|
|
158
|
+
*/
|
|
159
|
+
export const fileCheckoutHandlers: HandlerRegistry = {
|
|
160
|
+
checkout_file: checkoutFile,
|
|
161
|
+
checkin_file: checkinFile,
|
|
162
|
+
get_file_checkouts: getFileCheckouts,
|
|
163
|
+
abandon_checkout: abandonCheckout,
|
|
164
|
+
is_file_available: isFileAvailable,
|
|
165
|
+
};
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getFindingsStats,
|
|
6
6
|
updateFinding,
|
|
7
7
|
deleteFinding,
|
|
8
|
+
queryKnowledgeBase,
|
|
8
9
|
} from './findings.js';
|
|
9
10
|
import { ValidationError } from '../validators.js';
|
|
10
11
|
import { createMockContext } from './__test-utils__.js';
|
|
@@ -105,16 +106,17 @@ describe('addFinding', () => {
|
|
|
105
106
|
);
|
|
106
107
|
});
|
|
107
108
|
|
|
108
|
-
it('should
|
|
109
|
+
it('should return error when API call fails', async () => {
|
|
109
110
|
mockApiClient.addFinding.mockResolvedValue({
|
|
110
111
|
ok: false,
|
|
111
112
|
error: 'Insert failed',
|
|
112
113
|
});
|
|
113
114
|
const ctx = createMockContext();
|
|
114
115
|
|
|
115
|
-
await
|
|
116
|
-
|
|
117
|
-
).
|
|
116
|
+
const result = await addFinding({ project_id: VALID_UUID, title: 'Test' }, ctx);
|
|
117
|
+
|
|
118
|
+
expect(result.isError).toBe(true);
|
|
119
|
+
expect(result.result).toMatchObject({ error: 'Insert failed' });
|
|
118
120
|
});
|
|
119
121
|
});
|
|
120
122
|
|
|
@@ -258,16 +260,17 @@ describe('getFindings', () => {
|
|
|
258
260
|
);
|
|
259
261
|
});
|
|
260
262
|
|
|
261
|
-
it('should
|
|
263
|
+
it('should return error when API call fails', async () => {
|
|
262
264
|
mockApiClient.getFindings.mockResolvedValue({
|
|
263
265
|
ok: false,
|
|
264
266
|
error: 'Query failed',
|
|
265
267
|
});
|
|
266
268
|
const ctx = createMockContext();
|
|
267
269
|
|
|
268
|
-
await
|
|
269
|
-
|
|
270
|
-
).
|
|
270
|
+
const result = await getFindings({ project_id: VALID_UUID }, ctx);
|
|
271
|
+
|
|
272
|
+
expect(result.isError).toBe(true);
|
|
273
|
+
expect(result.result).toMatchObject({ error: 'Query failed' });
|
|
271
274
|
});
|
|
272
275
|
});
|
|
273
276
|
|
|
@@ -338,16 +341,17 @@ describe('updateFinding', () => {
|
|
|
338
341
|
);
|
|
339
342
|
});
|
|
340
343
|
|
|
341
|
-
it('should
|
|
344
|
+
it('should return error when API call fails', async () => {
|
|
342
345
|
mockApiClient.updateFinding.mockResolvedValue({
|
|
343
346
|
ok: false,
|
|
344
347
|
error: 'Update failed',
|
|
345
348
|
});
|
|
346
349
|
const ctx = createMockContext();
|
|
347
350
|
|
|
348
|
-
await
|
|
349
|
-
|
|
350
|
-
).
|
|
351
|
+
const result = await updateFinding({ finding_id: VALID_UUID, title: 'Test' }, ctx);
|
|
352
|
+
|
|
353
|
+
expect(result.isError).toBe(true);
|
|
354
|
+
expect(result.result).toMatchObject({ error: 'Update failed' });
|
|
351
355
|
});
|
|
352
356
|
});
|
|
353
357
|
|
|
@@ -396,16 +400,17 @@ describe('deleteFinding', () => {
|
|
|
396
400
|
expect(mockApiClient.deleteFinding).toHaveBeenCalledWith(VALID_UUID);
|
|
397
401
|
});
|
|
398
402
|
|
|
399
|
-
it('should
|
|
403
|
+
it('should return error when API call fails', async () => {
|
|
400
404
|
mockApiClient.deleteFinding.mockResolvedValue({
|
|
401
405
|
ok: false,
|
|
402
406
|
error: 'Delete failed',
|
|
403
407
|
});
|
|
404
408
|
const ctx = createMockContext();
|
|
405
409
|
|
|
406
|
-
await
|
|
407
|
-
|
|
408
|
-
).
|
|
410
|
+
const result = await deleteFinding({ finding_id: VALID_UUID }, ctx);
|
|
411
|
+
|
|
412
|
+
expect(result.isError).toBe(true);
|
|
413
|
+
expect(result.result).toMatchObject({ error: 'Delete failed' });
|
|
409
414
|
});
|
|
410
415
|
});
|
|
411
416
|
|
|
@@ -460,15 +465,169 @@ describe('getFindingsStats', () => {
|
|
|
460
465
|
expect(mockApiClient.getFindingsStats).toHaveBeenCalledWith(VALID_UUID);
|
|
461
466
|
});
|
|
462
467
|
|
|
463
|
-
it('should
|
|
468
|
+
it('should return error when API call fails', async () => {
|
|
464
469
|
mockApiClient.getFindingsStats.mockResolvedValue({
|
|
465
470
|
ok: false,
|
|
466
471
|
error: 'Query failed',
|
|
467
472
|
});
|
|
468
473
|
const ctx = createMockContext();
|
|
469
474
|
|
|
475
|
+
const result = await getFindingsStats({ project_id: VALID_UUID }, ctx);
|
|
476
|
+
|
|
477
|
+
expect(result.isError).toBe(true);
|
|
478
|
+
expect(result.result).toMatchObject({ error: 'Query failed' });
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// ============================================================================
|
|
483
|
+
// queryKnowledgeBase Tests
|
|
484
|
+
// ============================================================================
|
|
485
|
+
|
|
486
|
+
describe('queryKnowledgeBase', () => {
|
|
487
|
+
beforeEach(() => vi.clearAllMocks());
|
|
488
|
+
|
|
489
|
+
it('should throw error for missing project_id', async () => {
|
|
490
|
+
const ctx = createMockContext();
|
|
491
|
+
|
|
492
|
+
await expect(queryKnowledgeBase({}, ctx)).rejects.toThrow(ValidationError);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
496
|
+
const ctx = createMockContext();
|
|
497
|
+
|
|
470
498
|
await expect(
|
|
471
|
-
|
|
472
|
-
).rejects.toThrow(
|
|
499
|
+
queryKnowledgeBase({ project_id: 'invalid' }, ctx)
|
|
500
|
+
).rejects.toThrow(ValidationError);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('should throw error for invalid scope value', async () => {
|
|
504
|
+
const ctx = createMockContext();
|
|
505
|
+
|
|
506
|
+
await expect(
|
|
507
|
+
queryKnowledgeBase({ project_id: VALID_UUID, scope: 'invalid_scope' }, ctx)
|
|
508
|
+
).rejects.toThrow(ValidationError);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('should query with default parameters', async () => {
|
|
512
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
513
|
+
ok: true,
|
|
514
|
+
data: {
|
|
515
|
+
findings: [],
|
|
516
|
+
decisions: [],
|
|
517
|
+
completed_tasks: [],
|
|
518
|
+
resolved_blockers: [],
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
const ctx = createMockContext();
|
|
522
|
+
|
|
523
|
+
const result = await queryKnowledgeBase({ project_id: VALID_UUID }, ctx);
|
|
524
|
+
|
|
525
|
+
expect(result.result).toMatchObject({
|
|
526
|
+
findings: [],
|
|
527
|
+
decisions: [],
|
|
528
|
+
});
|
|
529
|
+
expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
|
|
530
|
+
VALID_UUID,
|
|
531
|
+
expect.objectContaining({
|
|
532
|
+
scope: 'summary',
|
|
533
|
+
limit: 5,
|
|
534
|
+
})
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('should pass scope parameter', async () => {
|
|
539
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
540
|
+
ok: true,
|
|
541
|
+
data: { findings: [] },
|
|
542
|
+
});
|
|
543
|
+
const ctx = createMockContext();
|
|
544
|
+
|
|
545
|
+
await queryKnowledgeBase({ project_id: VALID_UUID, scope: 'detailed' }, ctx);
|
|
546
|
+
|
|
547
|
+
expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
|
|
548
|
+
VALID_UUID,
|
|
549
|
+
expect.objectContaining({ scope: 'detailed' })
|
|
550
|
+
);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('should pass categories filter', async () => {
|
|
554
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
555
|
+
ok: true,
|
|
556
|
+
data: { findings: [], decisions: [] },
|
|
557
|
+
});
|
|
558
|
+
const ctx = createMockContext();
|
|
559
|
+
|
|
560
|
+
await queryKnowledgeBase({
|
|
561
|
+
project_id: VALID_UUID,
|
|
562
|
+
categories: ['findings', 'decisions']
|
|
563
|
+
}, ctx);
|
|
564
|
+
|
|
565
|
+
expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
|
|
566
|
+
VALID_UUID,
|
|
567
|
+
expect.objectContaining({
|
|
568
|
+
categories: ['findings', 'decisions']
|
|
569
|
+
})
|
|
570
|
+
);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('should cap limit at 20', async () => {
|
|
574
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
575
|
+
ok: true,
|
|
576
|
+
data: { findings: [] },
|
|
577
|
+
});
|
|
578
|
+
const ctx = createMockContext();
|
|
579
|
+
|
|
580
|
+
await queryKnowledgeBase({ project_id: VALID_UUID, limit: 100 }, ctx);
|
|
581
|
+
|
|
582
|
+
expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
|
|
583
|
+
VALID_UUID,
|
|
584
|
+
expect.objectContaining({ limit: 20 })
|
|
585
|
+
);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('should enforce minimum limit of 1', async () => {
|
|
589
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
590
|
+
ok: true,
|
|
591
|
+
data: { findings: [] },
|
|
592
|
+
});
|
|
593
|
+
const ctx = createMockContext();
|
|
594
|
+
|
|
595
|
+
await queryKnowledgeBase({ project_id: VALID_UUID, limit: -5 }, ctx);
|
|
596
|
+
|
|
597
|
+
expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
|
|
598
|
+
VALID_UUID,
|
|
599
|
+
expect.objectContaining({ limit: 1 })
|
|
600
|
+
);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('should pass search_query', async () => {
|
|
604
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
605
|
+
ok: true,
|
|
606
|
+
data: { findings: [] },
|
|
607
|
+
});
|
|
608
|
+
const ctx = createMockContext();
|
|
609
|
+
|
|
610
|
+
await queryKnowledgeBase({
|
|
611
|
+
project_id: VALID_UUID,
|
|
612
|
+
search_query: 'security'
|
|
613
|
+
}, ctx);
|
|
614
|
+
|
|
615
|
+
expect(mockApiClient.queryKnowledgeBase).toHaveBeenCalledWith(
|
|
616
|
+
VALID_UUID,
|
|
617
|
+
expect.objectContaining({ search_query: 'security' })
|
|
618
|
+
);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('should return error when API call fails', async () => {
|
|
622
|
+
mockApiClient.queryKnowledgeBase.mockResolvedValue({
|
|
623
|
+
ok: false,
|
|
624
|
+
error: 'Query failed',
|
|
625
|
+
});
|
|
626
|
+
const ctx = createMockContext();
|
|
627
|
+
|
|
628
|
+
const result = await queryKnowledgeBase({ project_id: VALID_UUID }, ctx);
|
|
629
|
+
|
|
630
|
+
expect(result.isError).toBe(true);
|
|
631
|
+
expect(result.result).toMatchObject({ error: 'Query failed' });
|
|
473
632
|
});
|
|
474
633
|
});
|
package/src/handlers/findings.ts
CHANGED
|
@@ -10,68 +10,97 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { Handler, HandlerRegistry } from './types.js';
|
|
13
|
-
import {
|
|
13
|
+
import { success, error } from './types.js';
|
|
14
|
+
import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
|
|
14
15
|
import { getApiClient } from '../api-client.js';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const VALID_FINDING_CATEGORIES = ['performance', 'security', 'code_quality', 'accessibility', 'documentation', 'architecture', 'testing', 'other'] as const;
|
|
18
|
+
const VALID_FINDING_SEVERITIES = ['info', 'low', 'medium', 'high', 'critical'] as const;
|
|
19
|
+
const VALID_FINDING_STATUSES = ['open', 'addressed', 'dismissed', 'wontfix'] as const;
|
|
20
|
+
|
|
21
|
+
type FindingCategory = typeof VALID_FINDING_CATEGORIES[number];
|
|
22
|
+
type FindingSeverity = typeof VALID_FINDING_SEVERITIES[number];
|
|
23
|
+
type FindingStatus = typeof VALID_FINDING_STATUSES[number];
|
|
24
|
+
|
|
25
|
+
// Argument schemas for type-safe parsing
|
|
26
|
+
const addFindingSchema = {
|
|
27
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
28
|
+
title: { type: 'string' as const, required: true as const },
|
|
29
|
+
description: { type: 'string' as const },
|
|
30
|
+
category: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_CATEGORIES) },
|
|
31
|
+
severity: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_SEVERITIES) },
|
|
32
|
+
file_path: { type: 'string' as const },
|
|
33
|
+
line_number: { type: 'number' as const },
|
|
34
|
+
related_task_id: { type: 'string' as const, validate: uuidValidator },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getFindingsSchema = {
|
|
38
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
39
|
+
category: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_CATEGORIES) },
|
|
40
|
+
severity: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_SEVERITIES) },
|
|
41
|
+
status: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_STATUSES) },
|
|
42
|
+
limit: { type: 'number' as const, default: 50 },
|
|
43
|
+
offset: { type: 'number' as const, default: 0 },
|
|
44
|
+
search_query: { type: 'string' as const },
|
|
45
|
+
summary_only: { type: 'boolean' as const, default: false },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const getFindingsStatsSchema = {
|
|
49
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const updateFindingSchema = {
|
|
53
|
+
finding_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
54
|
+
title: { type: 'string' as const },
|
|
55
|
+
description: { type: 'string' as const },
|
|
56
|
+
severity: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_SEVERITIES) },
|
|
57
|
+
status: { type: 'string' as const, validate: createEnumValidator(VALID_FINDING_STATUSES) },
|
|
58
|
+
resolution_note: { type: 'string' as const },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const deleteFindingSchema = {
|
|
62
|
+
finding_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const VALID_SCOPES = ['summary', 'detailed'] as const;
|
|
66
|
+
|
|
67
|
+
const queryKnowledgeBaseSchema = {
|
|
68
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
69
|
+
scope: { type: 'string' as const, default: 'summary', validate: createEnumValidator(VALID_SCOPES) },
|
|
70
|
+
categories: { type: 'array' as const },
|
|
71
|
+
limit: { type: 'number' as const, default: 5 },
|
|
72
|
+
search_query: { type: 'string' as const },
|
|
73
|
+
};
|
|
19
74
|
|
|
20
75
|
export const addFinding: Handler = async (args, ctx) => {
|
|
21
|
-
const { project_id,
|
|
22
|
-
project_id: string;
|
|
23
|
-
category?: FindingCategory;
|
|
24
|
-
title: string;
|
|
25
|
-
description?: string;
|
|
26
|
-
severity?: FindingSeverity;
|
|
27
|
-
file_path?: string;
|
|
28
|
-
line_number?: number;
|
|
29
|
-
related_task_id?: string;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
validateRequired(project_id, 'project_id');
|
|
33
|
-
validateUUID(project_id, 'project_id');
|
|
34
|
-
validateRequired(title, 'title');
|
|
35
|
-
if (related_task_id) validateUUID(related_task_id, 'related_task_id');
|
|
76
|
+
const { project_id, title, description, category, severity, file_path, line_number, related_task_id } = parseArgs(args, addFindingSchema);
|
|
36
77
|
|
|
37
78
|
const apiClient = getApiClient();
|
|
38
79
|
const response = await apiClient.addFinding(project_id, {
|
|
39
80
|
title,
|
|
40
81
|
description,
|
|
41
|
-
category,
|
|
42
|
-
severity,
|
|
82
|
+
category: category as FindingCategory | undefined,
|
|
83
|
+
severity: severity as FindingSeverity | undefined,
|
|
43
84
|
file_path,
|
|
44
85
|
line_number,
|
|
45
86
|
related_task_id
|
|
46
87
|
}, ctx.session.currentSessionId || undefined);
|
|
47
88
|
|
|
48
89
|
if (!response.ok) {
|
|
49
|
-
|
|
90
|
+
return error(response.error || 'Failed to add finding');
|
|
50
91
|
}
|
|
51
92
|
|
|
52
|
-
return
|
|
93
|
+
return success(response.data);
|
|
53
94
|
};
|
|
54
95
|
|
|
55
|
-
export const getFindings: Handler = async (args,
|
|
56
|
-
const { project_id, category, severity, status, limit
|
|
57
|
-
project_id: string;
|
|
58
|
-
category?: FindingCategory;
|
|
59
|
-
severity?: FindingSeverity;
|
|
60
|
-
status?: FindingStatus;
|
|
61
|
-
limit?: number;
|
|
62
|
-
offset?: number;
|
|
63
|
-
search_query?: string;
|
|
64
|
-
summary_only?: boolean;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
validateRequired(project_id, 'project_id');
|
|
68
|
-
validateUUID(project_id, 'project_id');
|
|
96
|
+
export const getFindings: Handler = async (args, _ctx) => {
|
|
97
|
+
const { project_id, category, severity, status, limit, offset, search_query, summary_only } = parseArgs(args, getFindingsSchema);
|
|
69
98
|
|
|
70
99
|
const apiClient = getApiClient();
|
|
71
100
|
const response = await apiClient.getFindings(project_id, {
|
|
72
|
-
category,
|
|
73
|
-
severity,
|
|
74
|
-
status,
|
|
101
|
+
category: category as FindingCategory | undefined,
|
|
102
|
+
severity: severity as FindingSeverity | undefined,
|
|
103
|
+
status: status as FindingStatus | undefined,
|
|
75
104
|
limit,
|
|
76
105
|
offset,
|
|
77
106
|
search_query,
|
|
@@ -79,10 +108,10 @@ export const getFindings: Handler = async (args, ctx) => {
|
|
|
79
108
|
});
|
|
80
109
|
|
|
81
110
|
if (!response.ok) {
|
|
82
|
-
|
|
111
|
+
return error(response.error || 'Failed to get findings');
|
|
83
112
|
}
|
|
84
113
|
|
|
85
|
-
return
|
|
114
|
+
return success(response.data);
|
|
86
115
|
};
|
|
87
116
|
|
|
88
117
|
/**
|
|
@@ -90,67 +119,75 @@ export const getFindings: Handler = async (args, ctx) => {
|
|
|
90
119
|
* Returns counts by category, severity, and status without the actual finding data.
|
|
91
120
|
* This is much more token-efficient than get_findings for understanding the overall state.
|
|
92
121
|
*/
|
|
93
|
-
export const getFindingsStats: Handler = async (args,
|
|
94
|
-
const { project_id } = args
|
|
95
|
-
project_id: string;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
validateRequired(project_id, 'project_id');
|
|
99
|
-
validateUUID(project_id, 'project_id');
|
|
122
|
+
export const getFindingsStats: Handler = async (args, _ctx) => {
|
|
123
|
+
const { project_id } = parseArgs(args, getFindingsStatsSchema);
|
|
100
124
|
|
|
101
125
|
const apiClient = getApiClient();
|
|
102
126
|
const response = await apiClient.getFindingsStats(project_id);
|
|
103
127
|
|
|
104
128
|
if (!response.ok) {
|
|
105
|
-
|
|
129
|
+
return error(response.error || 'Failed to get findings stats');
|
|
106
130
|
}
|
|
107
131
|
|
|
108
|
-
return
|
|
132
|
+
return success(response.data);
|
|
109
133
|
};
|
|
110
134
|
|
|
111
|
-
export const updateFinding: Handler = async (args,
|
|
112
|
-
const { finding_id,
|
|
113
|
-
finding_id: string;
|
|
114
|
-
status?: FindingStatus;
|
|
115
|
-
resolution_note?: string;
|
|
116
|
-
title?: string;
|
|
117
|
-
description?: string;
|
|
118
|
-
severity?: FindingSeverity;
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
validateRequired(finding_id, 'finding_id');
|
|
122
|
-
validateUUID(finding_id, 'finding_id');
|
|
135
|
+
export const updateFinding: Handler = async (args, _ctx) => {
|
|
136
|
+
const { finding_id, title, description, severity, status, resolution_note } = parseArgs(args, updateFindingSchema);
|
|
123
137
|
|
|
124
138
|
const apiClient = getApiClient();
|
|
125
139
|
const response = await apiClient.updateFinding(finding_id, {
|
|
126
140
|
title,
|
|
127
141
|
description,
|
|
128
|
-
severity,
|
|
129
|
-
status,
|
|
142
|
+
severity: severity as FindingSeverity | undefined,
|
|
143
|
+
status: status as FindingStatus | undefined,
|
|
130
144
|
resolution_note
|
|
131
145
|
});
|
|
132
146
|
|
|
133
147
|
if (!response.ok) {
|
|
134
|
-
|
|
148
|
+
return error(response.error || 'Failed to update finding');
|
|
135
149
|
}
|
|
136
150
|
|
|
137
|
-
return
|
|
151
|
+
return success(response.data);
|
|
138
152
|
};
|
|
139
153
|
|
|
140
|
-
export const deleteFinding: Handler = async (args,
|
|
141
|
-
const { finding_id } = args
|
|
142
|
-
|
|
143
|
-
validateRequired(finding_id, 'finding_id');
|
|
144
|
-
validateUUID(finding_id, 'finding_id');
|
|
154
|
+
export const deleteFinding: Handler = async (args, _ctx) => {
|
|
155
|
+
const { finding_id } = parseArgs(args, deleteFindingSchema);
|
|
145
156
|
|
|
146
157
|
const apiClient = getApiClient();
|
|
147
158
|
const response = await apiClient.deleteFinding(finding_id);
|
|
148
159
|
|
|
149
160
|
if (!response.ok) {
|
|
150
|
-
|
|
161
|
+
return error(response.error || 'Failed to delete finding');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return success(response.data);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Query aggregated project knowledge in a single call.
|
|
169
|
+
* Returns findings, Q&A, decisions, completed tasks, and resolved blockers.
|
|
170
|
+
* Use this instead of multiple separate tool calls to reduce token usage.
|
|
171
|
+
*/
|
|
172
|
+
export const queryKnowledgeBase: Handler = async (args, _ctx) => {
|
|
173
|
+
const { project_id, scope, categories, limit, search_query } = parseArgs(args, queryKnowledgeBaseSchema);
|
|
174
|
+
|
|
175
|
+
// Validate limit range
|
|
176
|
+
const effectiveLimit = Math.min(Math.max(1, limit ?? 5), 20);
|
|
177
|
+
|
|
178
|
+
const apiClient = getApiClient();
|
|
179
|
+
const response = await apiClient.queryKnowledgeBase(project_id, {
|
|
180
|
+
scope: scope as 'summary' | 'detailed' | undefined,
|
|
181
|
+
categories: categories as string[] | undefined,
|
|
182
|
+
limit: effectiveLimit,
|
|
183
|
+
search_query
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
return error(response.error || 'Failed to query knowledge base');
|
|
151
188
|
}
|
|
152
189
|
|
|
153
|
-
return
|
|
190
|
+
return success(response.data);
|
|
154
191
|
};
|
|
155
192
|
|
|
156
193
|
/**
|
|
@@ -162,4 +199,5 @@ export const findingHandlers: HandlerRegistry = {
|
|
|
162
199
|
get_findings_stats: getFindingsStats,
|
|
163
200
|
update_finding: updateFinding,
|
|
164
201
|
delete_finding: deleteFinding,
|
|
202
|
+
query_knowledge_base: queryKnowledgeBase,
|
|
165
203
|
};
|