@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
package/src/handlers/tasks.ts
CHANGED
|
@@ -18,16 +18,107 @@
|
|
|
18
18
|
|
|
19
19
|
import type { Handler, HandlerRegistry } from './types.js';
|
|
20
20
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
parseArgs,
|
|
22
|
+
uuidValidator,
|
|
23
|
+
taskStatusValidator,
|
|
24
|
+
priorityValidator,
|
|
25
|
+
progressValidator,
|
|
26
|
+
minutesValidator,
|
|
27
|
+
createEnumValidator,
|
|
27
28
|
ValidationError,
|
|
28
29
|
} from '../validators.js';
|
|
29
30
|
import { getApiClient } from '../api-client.js';
|
|
30
31
|
|
|
32
|
+
// Valid task types
|
|
33
|
+
const VALID_TASK_TYPES = [
|
|
34
|
+
'frontend', 'backend', 'database', 'feature', 'bugfix',
|
|
35
|
+
'design', 'mcp', 'testing', 'docs', 'infra', 'other'
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Argument Schemas
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
const getTasksSchema = {
|
|
43
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
44
|
+
status: { type: 'string' as const, validate: taskStatusValidator },
|
|
45
|
+
limit: { type: 'number' as const, default: 50 },
|
|
46
|
+
offset: { type: 'number' as const, default: 0 },
|
|
47
|
+
search_query: { type: 'string' as const },
|
|
48
|
+
include_subtasks: { type: 'boolean' as const, default: false },
|
|
49
|
+
include_metadata: { type: 'boolean' as const, default: false },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getNextTaskSchema = {
|
|
53
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const addTaskSchema = {
|
|
57
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
58
|
+
title: { type: 'string' as const, required: true as const },
|
|
59
|
+
description: { type: 'string' as const },
|
|
60
|
+
priority: { type: 'number' as const, default: 3, validate: priorityValidator },
|
|
61
|
+
estimated_minutes: { type: 'number' as const, validate: minutesValidator },
|
|
62
|
+
blocking: { type: 'boolean' as const, default: false },
|
|
63
|
+
task_type: { type: 'string' as const, validate: createEnumValidator(VALID_TASK_TYPES) },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const updateTaskSchema = {
|
|
67
|
+
task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
68
|
+
title: { type: 'string' as const },
|
|
69
|
+
description: { type: 'string' as const },
|
|
70
|
+
priority: { type: 'number' as const, validate: priorityValidator },
|
|
71
|
+
status: { type: 'string' as const, validate: taskStatusValidator },
|
|
72
|
+
progress_percentage: { type: 'number' as const, validate: progressValidator },
|
|
73
|
+
progress_note: { type: 'string' as const },
|
|
74
|
+
estimated_minutes: { type: 'number' as const, validate: minutesValidator },
|
|
75
|
+
git_branch: { type: 'string' as const },
|
|
76
|
+
worktree_path: { type: 'string' as const },
|
|
77
|
+
task_type: { type: 'string' as const, validate: createEnumValidator(VALID_TASK_TYPES) },
|
|
78
|
+
skip_worktree_requirement: { type: 'boolean' as const, default: false },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const completeTaskSchema = {
|
|
82
|
+
task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
83
|
+
summary: { type: 'string' as const },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const deleteTaskSchema = {
|
|
87
|
+
task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const addTaskReferenceSchema = {
|
|
91
|
+
task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
92
|
+
url: { type: 'string' as const, required: true as const },
|
|
93
|
+
label: { type: 'string' as const },
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const removeTaskReferenceSchema = {
|
|
97
|
+
task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
98
|
+
url: { type: 'string' as const, required: true as const },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const batchUpdateTasksSchema = {
|
|
102
|
+
updates: { type: 'array' as const, required: true as const },
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const batchCompleteTasksSchema = {
|
|
106
|
+
completions: { type: 'array' as const, required: true as const },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const addSubtaskSchema = {
|
|
110
|
+
parent_task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
111
|
+
title: { type: 'string' as const, required: true as const },
|
|
112
|
+
description: { type: 'string' as const },
|
|
113
|
+
priority: { type: 'number' as const, validate: priorityValidator },
|
|
114
|
+
estimated_minutes: { type: 'number' as const, validate: minutesValidator },
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const getSubtasksSchema = {
|
|
118
|
+
parent_task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
119
|
+
status: { type: 'string' as const, validate: taskStatusValidator },
|
|
120
|
+
};
|
|
121
|
+
|
|
31
122
|
// ============================================================================
|
|
32
123
|
// Git workflow helpers (used by complete_task response)
|
|
33
124
|
// ============================================================================
|
|
@@ -126,24 +217,12 @@ export function getValidationApprovedGitInstructions(
|
|
|
126
217
|
// ============================================================================
|
|
127
218
|
|
|
128
219
|
export const getTasks: Handler = async (args, ctx) => {
|
|
129
|
-
const { project_id, status, limit
|
|
130
|
-
project_id: string;
|
|
131
|
-
status?: string;
|
|
132
|
-
limit?: number;
|
|
133
|
-
offset?: number;
|
|
134
|
-
search_query?: string;
|
|
135
|
-
include_subtasks?: boolean;
|
|
136
|
-
include_metadata?: boolean; // When true, returns all task fields; when false (default), only id/title/priority/status
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
validateRequired(project_id, 'project_id');
|
|
140
|
-
validateUUID(project_id, 'project_id');
|
|
141
|
-
validateTaskStatus(status);
|
|
220
|
+
const { project_id, status, limit, offset, search_query, include_subtasks, include_metadata } = parseArgs(args, getTasksSchema);
|
|
142
221
|
|
|
143
222
|
const api = getApiClient();
|
|
144
223
|
const response = await api.getTasks(project_id, {
|
|
145
224
|
status,
|
|
146
|
-
limit: Math.min(limit, 200),
|
|
225
|
+
limit: Math.min(limit ?? 50, 200),
|
|
147
226
|
offset,
|
|
148
227
|
include_subtasks,
|
|
149
228
|
search_query,
|
|
@@ -151,7 +230,7 @@ export const getTasks: Handler = async (args, ctx) => {
|
|
|
151
230
|
});
|
|
152
231
|
|
|
153
232
|
if (!response.ok) {
|
|
154
|
-
|
|
233
|
+
return { result: { error: response.error || 'Failed to fetch tasks' }, isError: true };
|
|
155
234
|
}
|
|
156
235
|
|
|
157
236
|
return {
|
|
@@ -164,16 +243,13 @@ export const getTasks: Handler = async (args, ctx) => {
|
|
|
164
243
|
};
|
|
165
244
|
|
|
166
245
|
export const getNextTask: Handler = async (args, ctx) => {
|
|
167
|
-
const { project_id } = args
|
|
168
|
-
|
|
169
|
-
validateRequired(project_id, 'project_id');
|
|
170
|
-
validateUUID(project_id, 'project_id');
|
|
246
|
+
const { project_id } = parseArgs(args, getNextTaskSchema);
|
|
171
247
|
|
|
172
248
|
const api = getApiClient();
|
|
173
249
|
const response = await api.getNextTask(project_id, ctx.session.currentSessionId || undefined);
|
|
174
250
|
|
|
175
251
|
if (!response.ok) {
|
|
176
|
-
|
|
252
|
+
return { result: { error: response.error || 'Failed to get next task' }, isError: true };
|
|
177
253
|
}
|
|
178
254
|
|
|
179
255
|
const data = response.data;
|
|
@@ -211,21 +287,7 @@ export const getNextTask: Handler = async (args, ctx) => {
|
|
|
211
287
|
};
|
|
212
288
|
|
|
213
289
|
export const addTask: Handler = async (args, ctx) => {
|
|
214
|
-
const { project_id, title, description, priority
|
|
215
|
-
project_id: string;
|
|
216
|
-
title: string;
|
|
217
|
-
description?: string;
|
|
218
|
-
priority?: number;
|
|
219
|
-
estimated_minutes?: number;
|
|
220
|
-
blocking?: boolean;
|
|
221
|
-
task_type?: string;
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
validateRequired(project_id, 'project_id');
|
|
225
|
-
validateRequired(title, 'title');
|
|
226
|
-
validateUUID(project_id, 'project_id');
|
|
227
|
-
validatePriority(priority);
|
|
228
|
-
validateEstimatedMinutes(estimated_minutes);
|
|
290
|
+
const { project_id, title, description, priority, estimated_minutes, blocking, task_type } = parseArgs(args, addTaskSchema);
|
|
229
291
|
|
|
230
292
|
const api = getApiClient();
|
|
231
293
|
const response = await api.createTask(project_id, {
|
|
@@ -235,10 +297,11 @@ export const addTask: Handler = async (args, ctx) => {
|
|
|
235
297
|
estimated_minutes,
|
|
236
298
|
blocking,
|
|
237
299
|
session_id: ctx.session.currentSessionId || undefined,
|
|
300
|
+
task_type,
|
|
238
301
|
});
|
|
239
302
|
|
|
240
303
|
if (!response.ok) {
|
|
241
|
-
|
|
304
|
+
return { result: { error: response.error || 'Failed to add task' }, isError: true };
|
|
242
305
|
}
|
|
243
306
|
|
|
244
307
|
const data = response.data;
|
|
@@ -257,25 +320,25 @@ export const addTask: Handler = async (args, ctx) => {
|
|
|
257
320
|
};
|
|
258
321
|
|
|
259
322
|
export const updateTask: Handler = async (args, ctx) => {
|
|
260
|
-
const { task_id, progress_note,
|
|
261
|
-
|
|
262
|
-
title?: string;
|
|
263
|
-
description?: string;
|
|
264
|
-
priority?: number;
|
|
265
|
-
status?: string;
|
|
266
|
-
progress_percentage?: number;
|
|
267
|
-
progress_note?: string;
|
|
268
|
-
estimated_minutes?: number;
|
|
269
|
-
git_branch?: string;
|
|
270
|
-
task_type?: string;
|
|
271
|
-
};
|
|
323
|
+
const { task_id, title, description, priority, status, progress_percentage, progress_note, estimated_minutes, git_branch, worktree_path, task_type, skip_worktree_requirement } = parseArgs(args, updateTaskSchema);
|
|
324
|
+
const updates = { title, description, priority, status, progress_percentage, estimated_minutes, git_branch, worktree_path, task_type };
|
|
272
325
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
326
|
+
// Enforce worktree creation: require git_branch when marking task as in_progress
|
|
327
|
+
// This ensures multi-agent collaboration works properly with isolated worktrees
|
|
328
|
+
if (status === 'in_progress' && !git_branch && !skip_worktree_requirement) {
|
|
329
|
+
return {
|
|
330
|
+
result: {
|
|
331
|
+
error: 'worktree_required',
|
|
332
|
+
message: 'git_branch is required when marking a task as in_progress. Create a worktree first and provide the branch name.',
|
|
333
|
+
hint: 'Create a worktree with: git worktree add ../PROJECT-task-TASKID -b feature/TASKID-description BASE_BRANCH, then call update_task with both status and git_branch parameters.',
|
|
334
|
+
worktree_example: {
|
|
335
|
+
command: `git worktree add ../worktree-${task_id.substring(0, 8)} -b feature/${task_id.substring(0, 8)}-task develop`,
|
|
336
|
+
then: `update_task(task_id: "${task_id}", status: "in_progress", git_branch: "feature/${task_id.substring(0, 8)}-task")`,
|
|
337
|
+
},
|
|
338
|
+
skip_option: 'If this project does not use git branching (trunk-based or no git workflow), pass skip_worktree_requirement: true',
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
279
342
|
|
|
280
343
|
const api = getApiClient();
|
|
281
344
|
const response = await api.updateTask(task_id, {
|
|
@@ -310,7 +373,17 @@ export const updateTask: Handler = async (args, ctx) => {
|
|
|
310
373
|
},
|
|
311
374
|
};
|
|
312
375
|
}
|
|
313
|
-
|
|
376
|
+
if (response.error?.includes('branch_conflict')) {
|
|
377
|
+
return {
|
|
378
|
+
result: {
|
|
379
|
+
error: 'branch_conflict',
|
|
380
|
+
message: response.error,
|
|
381
|
+
conflicting_task_id: (response.data as { conflicting_task_id?: string })?.conflicting_task_id,
|
|
382
|
+
conflicting_task_title: (response.data as { conflicting_task_title?: string })?.conflicting_task_title,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return { result: { error: response.error || 'Failed to update task' }, isError: true };
|
|
314
387
|
}
|
|
315
388
|
|
|
316
389
|
// Build result - include git workflow info when transitioning to in_progress
|
|
@@ -327,23 +400,11 @@ export const updateTask: Handler = async (args, ctx) => {
|
|
|
327
400
|
result.next_step = data.next_step;
|
|
328
401
|
}
|
|
329
402
|
|
|
330
|
-
// Warn if transitioning to in_progress without git_branch
|
|
331
|
-
if (updates.status === 'in_progress' && !updates.git_branch) {
|
|
332
|
-
result.warning = 'git_branch not set. For multi-agent collaboration, set git_branch when marking in_progress to track your worktree.';
|
|
333
|
-
result.hint = 'Call update_task again with git_branch parameter after creating your worktree.';
|
|
334
|
-
}
|
|
335
|
-
|
|
336
403
|
return { result };
|
|
337
404
|
};
|
|
338
405
|
|
|
339
406
|
export const completeTask: Handler = async (args, ctx) => {
|
|
340
|
-
const { task_id, summary } = args
|
|
341
|
-
task_id: string;
|
|
342
|
-
summary?: string;
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
validateRequired(task_id, 'task_id');
|
|
346
|
-
validateUUID(task_id, 'task_id');
|
|
407
|
+
const { task_id, summary } = parseArgs(args, completeTaskSchema);
|
|
347
408
|
|
|
348
409
|
const api = getApiClient();
|
|
349
410
|
const response = await api.completeTask(task_id, {
|
|
@@ -352,12 +413,12 @@ export const completeTask: Handler = async (args, ctx) => {
|
|
|
352
413
|
});
|
|
353
414
|
|
|
354
415
|
if (!response.ok) {
|
|
355
|
-
|
|
416
|
+
return { result: { error: response.error || 'Failed to complete task' }, isError: true };
|
|
356
417
|
}
|
|
357
418
|
|
|
358
419
|
const data = response.data;
|
|
359
420
|
if (!data) {
|
|
360
|
-
|
|
421
|
+
return { result: { error: 'No response data from complete task' }, isError: true };
|
|
361
422
|
}
|
|
362
423
|
|
|
363
424
|
// Build result matching expected format
|
|
@@ -386,27 +447,20 @@ export const completeTask: Handler = async (args, ctx) => {
|
|
|
386
447
|
};
|
|
387
448
|
|
|
388
449
|
export const deleteTask: Handler = async (args, ctx) => {
|
|
389
|
-
const { task_id } = args
|
|
390
|
-
|
|
391
|
-
validateRequired(task_id, 'task_id');
|
|
392
|
-
validateUUID(task_id, 'task_id');
|
|
450
|
+
const { task_id } = parseArgs(args, deleteTaskSchema);
|
|
393
451
|
|
|
394
452
|
const api = getApiClient();
|
|
395
453
|
const response = await api.deleteTask(task_id);
|
|
396
454
|
|
|
397
455
|
if (!response.ok) {
|
|
398
|
-
|
|
456
|
+
return { result: { error: response.error || 'Failed to delete task' }, isError: true };
|
|
399
457
|
}
|
|
400
458
|
|
|
401
459
|
return { result: { success: true, deleted_id: task_id } };
|
|
402
460
|
};
|
|
403
461
|
|
|
404
462
|
export const addTaskReference: Handler = async (args, ctx) => {
|
|
405
|
-
const { task_id, url, label } = args
|
|
406
|
-
|
|
407
|
-
validateRequired(task_id, 'task_id');
|
|
408
|
-
validateUUID(task_id, 'task_id');
|
|
409
|
-
validateRequired(url, 'url');
|
|
463
|
+
const { task_id, url, label } = parseArgs(args, addTaskReferenceSchema);
|
|
410
464
|
|
|
411
465
|
const api = getApiClient();
|
|
412
466
|
const response = await api.addTaskReference(task_id, url, label);
|
|
@@ -415,7 +469,7 @@ export const addTaskReference: Handler = async (args, ctx) => {
|
|
|
415
469
|
if (response.error?.includes('already exists')) {
|
|
416
470
|
return { result: { success: false, error: 'Reference with this URL already exists' } };
|
|
417
471
|
}
|
|
418
|
-
|
|
472
|
+
return { result: { error: response.error || 'Failed to add reference' }, isError: true };
|
|
419
473
|
}
|
|
420
474
|
|
|
421
475
|
return {
|
|
@@ -427,11 +481,7 @@ export const addTaskReference: Handler = async (args, ctx) => {
|
|
|
427
481
|
};
|
|
428
482
|
|
|
429
483
|
export const removeTaskReference: Handler = async (args, ctx) => {
|
|
430
|
-
const { task_id, url } = args
|
|
431
|
-
|
|
432
|
-
validateRequired(task_id, 'task_id');
|
|
433
|
-
validateUUID(task_id, 'task_id');
|
|
434
|
-
validateRequired(url, 'url');
|
|
484
|
+
const { task_id, url } = parseArgs(args, removeTaskReferenceSchema);
|
|
435
485
|
|
|
436
486
|
const api = getApiClient();
|
|
437
487
|
const response = await api.removeTaskReference(task_id, url);
|
|
@@ -440,104 +490,92 @@ export const removeTaskReference: Handler = async (args, ctx) => {
|
|
|
440
490
|
if (response.error?.includes('not found')) {
|
|
441
491
|
return { result: { success: false, error: 'Reference with this URL not found' } };
|
|
442
492
|
}
|
|
443
|
-
|
|
493
|
+
return { result: { error: response.error || 'Failed to remove reference' }, isError: true };
|
|
444
494
|
}
|
|
445
495
|
|
|
446
496
|
return { result: { success: true } };
|
|
447
497
|
};
|
|
448
498
|
|
|
449
499
|
export const batchUpdateTasks: Handler = async (args, ctx) => {
|
|
450
|
-
const { updates } = args
|
|
451
|
-
updates: Array<{
|
|
452
|
-
task_id: string;
|
|
453
|
-
status?: string;
|
|
454
|
-
progress_percentage?: number;
|
|
455
|
-
progress_note?: string;
|
|
456
|
-
priority?: number;
|
|
457
|
-
}>;
|
|
458
|
-
};
|
|
500
|
+
const { updates } = parseArgs(args, batchUpdateTasksSchema);
|
|
459
501
|
|
|
460
|
-
|
|
502
|
+
const typedUpdates = updates as Array<{
|
|
503
|
+
task_id: string;
|
|
504
|
+
status?: string;
|
|
505
|
+
progress_percentage?: number;
|
|
506
|
+
progress_note?: string;
|
|
507
|
+
priority?: number;
|
|
508
|
+
}>;
|
|
509
|
+
|
|
510
|
+
if (!Array.isArray(typedUpdates) || typedUpdates.length === 0) {
|
|
461
511
|
throw new ValidationError('updates must be a non-empty array', {
|
|
462
512
|
field: 'updates',
|
|
463
513
|
hint: 'Provide an array of task updates with at least one item',
|
|
464
514
|
});
|
|
465
515
|
}
|
|
466
516
|
|
|
467
|
-
if (
|
|
517
|
+
if (typedUpdates.length > 50) {
|
|
468
518
|
throw new ValidationError('Too many updates. Maximum is 50 per batch.', {
|
|
469
519
|
field: 'updates',
|
|
470
520
|
hint: 'Split your updates into smaller batches',
|
|
471
521
|
});
|
|
472
522
|
}
|
|
473
523
|
|
|
474
|
-
//
|
|
475
|
-
for (const update of updates) {
|
|
476
|
-
validateRequired(update.task_id, 'task_id');
|
|
477
|
-
validateUUID(update.task_id, 'task_id');
|
|
478
|
-
validateTaskStatus(update.status);
|
|
479
|
-
validatePriority(update.priority);
|
|
480
|
-
validateProgressPercentage(update.progress_percentage);
|
|
481
|
-
}
|
|
482
|
-
|
|
524
|
+
// Individual item validation happens at API level
|
|
483
525
|
const api = getApiClient();
|
|
484
|
-
const response = await api.batchUpdateTasks(
|
|
526
|
+
const response = await api.batchUpdateTasks(typedUpdates);
|
|
485
527
|
|
|
486
528
|
if (!response.ok) {
|
|
487
|
-
|
|
529
|
+
return { result: { error: response.error || 'Failed to batch update tasks' }, isError: true };
|
|
488
530
|
}
|
|
489
531
|
|
|
490
532
|
return {
|
|
491
533
|
result: {
|
|
492
534
|
success: response.data?.success || false,
|
|
493
|
-
total:
|
|
535
|
+
total: typedUpdates.length,
|
|
494
536
|
succeeded: response.data?.updated_count || 0,
|
|
495
537
|
},
|
|
496
538
|
};
|
|
497
539
|
};
|
|
498
540
|
|
|
499
541
|
export const batchCompleteTasks: Handler = async (args, ctx) => {
|
|
500
|
-
const { completions } = args
|
|
501
|
-
completions: Array<{
|
|
502
|
-
task_id: string;
|
|
503
|
-
summary?: string;
|
|
504
|
-
}>;
|
|
505
|
-
};
|
|
542
|
+
const { completions } = parseArgs(args, batchCompleteTasksSchema);
|
|
506
543
|
|
|
507
|
-
|
|
544
|
+
const typedCompletions = completions as Array<{
|
|
545
|
+
task_id: string;
|
|
546
|
+
summary?: string;
|
|
547
|
+
}>;
|
|
548
|
+
|
|
549
|
+
if (!Array.isArray(typedCompletions) || typedCompletions.length === 0) {
|
|
508
550
|
throw new ValidationError('completions must be a non-empty array', {
|
|
509
551
|
field: 'completions',
|
|
510
552
|
hint: 'Provide an array of task completions with at least one item',
|
|
511
553
|
});
|
|
512
554
|
}
|
|
513
555
|
|
|
514
|
-
if (
|
|
556
|
+
if (typedCompletions.length > 50) {
|
|
515
557
|
throw new ValidationError('Too many completions. Maximum is 50 per batch.', {
|
|
516
558
|
field: 'completions',
|
|
517
559
|
hint: 'Split your completions into smaller batches',
|
|
518
560
|
});
|
|
519
561
|
}
|
|
520
562
|
|
|
521
|
-
//
|
|
522
|
-
for (const completion of completions) {
|
|
523
|
-
validateRequired(completion.task_id, 'task_id');
|
|
524
|
-
validateUUID(completion.task_id, 'task_id');
|
|
525
|
-
}
|
|
563
|
+
// Individual item validation happens at API level
|
|
526
564
|
|
|
527
565
|
const api = getApiClient();
|
|
528
|
-
const response = await api.batchCompleteTasks(
|
|
566
|
+
const response = await api.batchCompleteTasks(typedCompletions);
|
|
529
567
|
|
|
530
568
|
if (!response.ok) {
|
|
531
|
-
|
|
569
|
+
return { result: { error: response.error || 'Failed to batch complete tasks' }, isError: true };
|
|
532
570
|
}
|
|
533
571
|
|
|
534
572
|
const data = response.data;
|
|
535
573
|
return {
|
|
536
574
|
result: {
|
|
537
575
|
success: data?.success || false,
|
|
538
|
-
total:
|
|
576
|
+
total: typedCompletions.length,
|
|
539
577
|
succeeded: data?.completed_count || 0,
|
|
540
|
-
failed:
|
|
578
|
+
failed: typedCompletions.length - (data?.completed_count || 0),
|
|
541
579
|
next_task: data?.next_task,
|
|
542
580
|
},
|
|
543
581
|
};
|
|
@@ -548,19 +586,7 @@ export const batchCompleteTasks: Handler = async (args, ctx) => {
|
|
|
548
586
|
// ============================================================================
|
|
549
587
|
|
|
550
588
|
export const addSubtask: Handler = async (args, ctx) => {
|
|
551
|
-
const { parent_task_id, title, description, priority, estimated_minutes } = args
|
|
552
|
-
parent_task_id: string;
|
|
553
|
-
title: string;
|
|
554
|
-
description?: string;
|
|
555
|
-
priority?: number;
|
|
556
|
-
estimated_minutes?: number;
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
validateRequired(parent_task_id, 'parent_task_id');
|
|
560
|
-
validateUUID(parent_task_id, 'parent_task_id');
|
|
561
|
-
validateRequired(title, 'title');
|
|
562
|
-
if (priority !== undefined) validatePriority(priority);
|
|
563
|
-
if (estimated_minutes !== undefined) validateEstimatedMinutes(estimated_minutes);
|
|
589
|
+
const { parent_task_id, title, description, priority, estimated_minutes } = parseArgs(args, addSubtaskSchema);
|
|
564
590
|
|
|
565
591
|
const api = getApiClient();
|
|
566
592
|
const response = await api.addSubtask(parent_task_id, {
|
|
@@ -580,7 +606,7 @@ export const addSubtask: Handler = async (args, ctx) => {
|
|
|
580
606
|
},
|
|
581
607
|
};
|
|
582
608
|
}
|
|
583
|
-
|
|
609
|
+
return { result: { error: response.error || 'Failed to add subtask' }, isError: true };
|
|
584
610
|
}
|
|
585
611
|
|
|
586
612
|
return {
|
|
@@ -593,20 +619,13 @@ export const addSubtask: Handler = async (args, ctx) => {
|
|
|
593
619
|
};
|
|
594
620
|
|
|
595
621
|
export const getSubtasks: Handler = async (args, ctx) => {
|
|
596
|
-
const { parent_task_id, status } = args
|
|
597
|
-
parent_task_id: string;
|
|
598
|
-
status?: string;
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
validateRequired(parent_task_id, 'parent_task_id');
|
|
602
|
-
validateUUID(parent_task_id, 'parent_task_id');
|
|
603
|
-
if (status) validateTaskStatus(status);
|
|
622
|
+
const { parent_task_id, status } = parseArgs(args, getSubtasksSchema);
|
|
604
623
|
|
|
605
624
|
const api = getApiClient();
|
|
606
625
|
const response = await api.getSubtasks(parent_task_id, status);
|
|
607
626
|
|
|
608
627
|
if (!response.ok) {
|
|
609
|
-
|
|
628
|
+
return { result: { error: response.error || 'Failed to fetch subtasks' }, isError: true };
|
|
610
629
|
}
|
|
611
630
|
|
|
612
631
|
return {
|
|
@@ -621,6 +640,59 @@ export const getSubtasks: Handler = async (args, ctx) => {
|
|
|
621
640
|
};
|
|
622
641
|
};
|
|
623
642
|
|
|
643
|
+
// ============================================================================
|
|
644
|
+
// Worktree Cleanup Handlers
|
|
645
|
+
// ============================================================================
|
|
646
|
+
|
|
647
|
+
const getStaleWorktreesSchema = {
|
|
648
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const clearWorktreePathSchema = {
|
|
652
|
+
task_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
export const getStaleWorktrees: Handler = async (args, ctx) => {
|
|
656
|
+
const { project_id } = parseArgs(args, getStaleWorktreesSchema);
|
|
657
|
+
|
|
658
|
+
const api = getApiClient();
|
|
659
|
+
const response = await api.getStaleWorktrees(project_id);
|
|
660
|
+
|
|
661
|
+
if (!response.ok) {
|
|
662
|
+
return { result: { error: response.error || 'Failed to get stale worktrees' }, isError: true };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const data = response.data;
|
|
666
|
+
return {
|
|
667
|
+
result: {
|
|
668
|
+
project_id: data?.project_id,
|
|
669
|
+
project_name: data?.project_name,
|
|
670
|
+
stale_worktrees: data?.stale_worktrees || [],
|
|
671
|
+
count: data?.count || 0,
|
|
672
|
+
cleanup_instructions: data?.cleanup_instructions,
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
export const clearWorktreePath: Handler = async (args, ctx) => {
|
|
678
|
+
const { task_id } = parseArgs(args, clearWorktreePathSchema);
|
|
679
|
+
|
|
680
|
+
const api = getApiClient();
|
|
681
|
+
const response = await api.clearWorktreePath(task_id);
|
|
682
|
+
|
|
683
|
+
if (!response.ok) {
|
|
684
|
+
return { result: { error: response.error || 'Failed to clear worktree path' }, isError: true };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
result: {
|
|
689
|
+
success: true,
|
|
690
|
+
task_id,
|
|
691
|
+
message: 'Worktree path cleared. The worktree can now be safely removed if not already done.',
|
|
692
|
+
},
|
|
693
|
+
};
|
|
694
|
+
};
|
|
695
|
+
|
|
624
696
|
/**
|
|
625
697
|
* Task handlers registry
|
|
626
698
|
*/
|
|
@@ -638,4 +710,7 @@ export const taskHandlers: HandlerRegistry = {
|
|
|
638
710
|
// Subtask handlers
|
|
639
711
|
add_subtask: addSubtask,
|
|
640
712
|
get_subtasks: getSubtasks,
|
|
713
|
+
// Worktree cleanup handlers
|
|
714
|
+
get_stale_worktrees: getStaleWorktrees,
|
|
715
|
+
clear_worktree_path: clearWorktreePath,
|
|
641
716
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the documentation generator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { tools } from '../tools.js';
|
|
7
|
+
import { TOOL_CATEGORIES } from './discovery.js';
|
|
8
|
+
|
|
9
|
+
describe('Documentation Generator Prerequisites', () => {
|
|
10
|
+
it('should have tools defined', () => {
|
|
11
|
+
expect(tools).toBeDefined();
|
|
12
|
+
expect(Array.isArray(tools)).toBe(true);
|
|
13
|
+
expect(tools.length).toBeGreaterThan(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should have TOOL_CATEGORIES exported', () => {
|
|
17
|
+
expect(TOOL_CATEGORIES).toBeDefined();
|
|
18
|
+
expect(typeof TOOL_CATEGORIES).toBe('object');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should have all tools categorized', () => {
|
|
22
|
+
const categorizedTools = new Set<string>();
|
|
23
|
+
for (const category of Object.values(TOOL_CATEGORIES)) {
|
|
24
|
+
for (const tool of category.tools) {
|
|
25
|
+
categorizedTools.add(tool.name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const uncategorized = tools.filter((t) => !categorizedTools.has(t.name)).map((t) => t.name);
|
|
30
|
+
|
|
31
|
+
expect(uncategorized).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should have valid tool schemas', () => {
|
|
35
|
+
for (const tool of tools) {
|
|
36
|
+
expect(tool.name).toBeDefined();
|
|
37
|
+
expect(typeof tool.name).toBe('string');
|
|
38
|
+
expect(tool.description).toBeDefined();
|
|
39
|
+
expect(typeof tool.description).toBe('string');
|
|
40
|
+
expect(tool.inputSchema).toBeDefined();
|
|
41
|
+
expect(tool.inputSchema.type).toBe('object');
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should have category tools that exist in tools array', () => {
|
|
46
|
+
const toolNames = new Set(tools.map((t) => t.name));
|
|
47
|
+
|
|
48
|
+
for (const [categoryName, category] of Object.entries(TOOL_CATEGORIES)) {
|
|
49
|
+
for (const toolRef of category.tools) {
|
|
50
|
+
expect(toolNames.has(toolRef.name)).toBe(true);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should have unique tool names', () => {
|
|
56
|
+
const names = tools.map((t) => t.name);
|
|
57
|
+
const uniqueNames = new Set(names);
|
|
58
|
+
expect(names.length).toBe(uniqueNames.size);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should have unique category names', () => {
|
|
62
|
+
const names = Object.keys(TOOL_CATEGORIES);
|
|
63
|
+
const uniqueNames = new Set(names);
|
|
64
|
+
expect(names.length).toBe(uniqueNames.size);
|
|
65
|
+
});
|
|
66
|
+
});
|