@allpepper/task-orchestrator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/src/db/client.ts +34 -0
  4. package/src/db/index.ts +1 -0
  5. package/src/db/migrate.ts +51 -0
  6. package/src/db/migrations/001_initial_schema.sql +160 -0
  7. package/src/domain/index.ts +1 -0
  8. package/src/domain/types.ts +225 -0
  9. package/src/index.ts +7 -0
  10. package/src/repos/base.ts +151 -0
  11. package/src/repos/dependencies.ts +356 -0
  12. package/src/repos/features.ts +507 -0
  13. package/src/repos/index.ts +4 -0
  14. package/src/repos/projects.ts +350 -0
  15. package/src/repos/sections.ts +505 -0
  16. package/src/repos/tags.example.ts +125 -0
  17. package/src/repos/tags.ts +175 -0
  18. package/src/repos/tasks.ts +581 -0
  19. package/src/repos/templates.ts +649 -0
  20. package/src/server.ts +121 -0
  21. package/src/services/index.ts +2 -0
  22. package/src/services/status-validator.ts +100 -0
  23. package/src/services/workflow.ts +104 -0
  24. package/src/tools/apply-template.ts +129 -0
  25. package/src/tools/get-blocked-tasks.ts +63 -0
  26. package/src/tools/get-next-status.ts +183 -0
  27. package/src/tools/get-next-task.ts +75 -0
  28. package/src/tools/get-tag-usage.ts +54 -0
  29. package/src/tools/index.ts +30 -0
  30. package/src/tools/list-tags.ts +56 -0
  31. package/src/tools/manage-container.ts +333 -0
  32. package/src/tools/manage-dependency.ts +198 -0
  33. package/src/tools/manage-sections.ts +388 -0
  34. package/src/tools/manage-template.ts +313 -0
  35. package/src/tools/query-container.ts +296 -0
  36. package/src/tools/query-dependencies.ts +68 -0
  37. package/src/tools/query-sections.ts +70 -0
  38. package/src/tools/query-templates.ts +137 -0
  39. package/src/tools/query-workflow-state.ts +198 -0
  40. package/src/tools/registry.ts +180 -0
  41. package/src/tools/rename-tag.ts +64 -0
  42. package/src/tools/setup-project.ts +189 -0
@@ -0,0 +1,296 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { createSuccessResponse, createErrorResponse, uuidSchema, optionalUuidSchema } from './registry';
4
+ import {
5
+ createProject,
6
+ getProject,
7
+ updateProject,
8
+ deleteProject,
9
+ searchProjects,
10
+ getProjectOverview,
11
+ } from '../repos/projects';
12
+ import {
13
+ createFeature,
14
+ getFeature,
15
+ updateFeature,
16
+ deleteFeature,
17
+ searchFeatures,
18
+ getFeatureOverview,
19
+ } from '../repos/features';
20
+ import {
21
+ createTask,
22
+ getTask,
23
+ updateTask,
24
+ deleteTask,
25
+ searchTasks,
26
+ setTaskStatus,
27
+ } from '../repos/tasks';
28
+ import { getSections } from '../repos/sections';
29
+ import type { TaskCounts } from '../repos/base';
30
+
31
+ /**
32
+ * Unified query_container tool - read operations for containers
33
+ *
34
+ * Operations:
35
+ * - get: Retrieve a single container by ID
36
+ * - search: Search containers with filters
37
+ * - overview: Get global overview (all) or scoped overview (by ID + hierarchy)
38
+ */
39
+ export function registerQueryContainerTool(server: McpServer): void {
40
+ server.tool(
41
+ 'query_container',
42
+ 'Unified read operations for containers (project, feature, task). Supports get, search, and overview operations.',
43
+ {
44
+ operation: z.enum(['get', 'search', 'overview']),
45
+ containerType: z.enum(['project', 'feature', 'task']),
46
+ id: optionalUuidSchema,
47
+ query: z.string().optional(),
48
+ status: z.string().optional(),
49
+ priority: z.string().optional(),
50
+ tags: z.string().optional(),
51
+ projectId: optionalUuidSchema,
52
+ featureId: optionalUuidSchema,
53
+ limit: z.number().int().optional().default(20),
54
+ offset: z.number().int().optional().default(0),
55
+ includeSections: z.boolean().optional().default(false),
56
+ },
57
+ async (params) => {
58
+ try {
59
+ const { operation, containerType, id } = params;
60
+
61
+ // ===== GET OPERATION =====
62
+ if (operation === 'get') {
63
+ if (!id) {
64
+ const response = createErrorResponse('id is required for get operation');
65
+ return {
66
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
67
+ };
68
+ }
69
+
70
+ // Dispatch based on container type
71
+ let result;
72
+ if (containerType === 'project') {
73
+ result = getProject(id);
74
+ } else if (containerType === 'feature') {
75
+ result = getFeature(id);
76
+ } else {
77
+ result = getTask(id);
78
+ }
79
+
80
+ if (!result.success) {
81
+ const response = createErrorResponse(result.error || 'Failed to get container');
82
+ return {
83
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
84
+ };
85
+ }
86
+
87
+ let data: any = { [containerType]: result.data };
88
+
89
+ // Include sections if requested
90
+ if (params.includeSections) {
91
+ const entityTypeMap = {
92
+ project: 'PROJECT',
93
+ feature: 'FEATURE',
94
+ task: 'TASK',
95
+ };
96
+ const sectionsResult = getSections(id, entityTypeMap[containerType]);
97
+ if (sectionsResult.success) {
98
+ data.sections = sectionsResult.data;
99
+ }
100
+ }
101
+
102
+ // Include task counts for features
103
+ if (containerType === 'feature') {
104
+ const overviewResult = getFeatureOverview(id);
105
+ if (overviewResult.success) {
106
+ data.taskCounts = overviewResult.data.taskCounts;
107
+ }
108
+ }
109
+
110
+ // Include task counts for projects
111
+ if (containerType === 'project') {
112
+ const overviewResult = getProjectOverview(id);
113
+ if (overviewResult.success) {
114
+ data.taskCounts = overviewResult.data.taskCounts;
115
+ }
116
+ }
117
+
118
+ const response = createSuccessResponse(`${containerType} retrieved successfully`, data);
119
+ return {
120
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
121
+ };
122
+ }
123
+
124
+ // ===== SEARCH OPERATION =====
125
+ if (operation === 'search') {
126
+ const searchParams = {
127
+ query: params.query,
128
+ status: params.status,
129
+ priority: params.priority,
130
+ tags: params.tags,
131
+ projectId: params.projectId,
132
+ featureId: params.featureId,
133
+ limit: params.limit ?? 20,
134
+ offset: params.offset ?? 0,
135
+ };
136
+
137
+ let result;
138
+ if (containerType === 'project') {
139
+ result = searchProjects(searchParams);
140
+ } else if (containerType === 'feature') {
141
+ result = searchFeatures(searchParams);
142
+ } else {
143
+ result = searchTasks(searchParams);
144
+ }
145
+
146
+ if (!result.success) {
147
+ const response = createErrorResponse(result.error || 'Search failed');
148
+ return {
149
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
150
+ };
151
+ }
152
+
153
+ const response = createSuccessResponse(
154
+ `Found ${result.data.length} ${containerType}(s)`,
155
+ {
156
+ items: result.data,
157
+ count: result.data.length,
158
+ limit: params.limit ?? 20,
159
+ offset: params.offset ?? 0,
160
+ }
161
+ );
162
+ return {
163
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
164
+ };
165
+ }
166
+
167
+ // ===== OVERVIEW OPERATION =====
168
+ if (operation === 'overview') {
169
+ // Global overview - search all with filters
170
+ if (!id) {
171
+ const searchParams = {
172
+ query: params.query,
173
+ status: params.status,
174
+ priority: params.priority,
175
+ tags: params.tags,
176
+ projectId: params.projectId,
177
+ featureId: params.featureId,
178
+ limit: params.limit ?? 20,
179
+ offset: params.offset ?? 0,
180
+ };
181
+
182
+ let result;
183
+ if (containerType === 'project') {
184
+ result = searchProjects(searchParams);
185
+ } else if (containerType === 'feature') {
186
+ result = searchFeatures(searchParams);
187
+ } else {
188
+ result = searchTasks(searchParams);
189
+ }
190
+
191
+ if (!result.success) {
192
+ const response = createErrorResponse(result.error || 'Overview failed');
193
+ return {
194
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
195
+ };
196
+ }
197
+
198
+ const response = createSuccessResponse(
199
+ `Global ${containerType} overview`,
200
+ {
201
+ items: result.data,
202
+ count: result.data.length,
203
+ limit: params.limit ?? 20,
204
+ offset: params.offset ?? 0,
205
+ }
206
+ );
207
+ return {
208
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
209
+ };
210
+ }
211
+
212
+ // Scoped overview - entity + hierarchy
213
+ if (containerType === 'project') {
214
+ const overviewResult = getProjectOverview(id);
215
+ if (!overviewResult.success) {
216
+ const response = createErrorResponse(
217
+ 'error' in overviewResult ? overviewResult.error : 'Failed to get project overview'
218
+ );
219
+ return {
220
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
221
+ };
222
+ }
223
+
224
+ // Get features for this project
225
+ const featuresResult = searchFeatures({ projectId: id, limit: 100 });
226
+ const features = featuresResult.success ? featuresResult.data : [];
227
+
228
+ const response = createSuccessResponse('Project overview retrieved', {
229
+ project: overviewResult.data.project,
230
+ taskCounts: overviewResult.data.taskCounts,
231
+ features,
232
+ });
233
+ return {
234
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
235
+ };
236
+ }
237
+
238
+ if (containerType === 'feature') {
239
+ const overviewResult = getFeatureOverview(id);
240
+ if (!overviewResult.success) {
241
+ const response = createErrorResponse(
242
+ 'error' in overviewResult ? overviewResult.error : 'Failed to get feature overview'
243
+ );
244
+ return {
245
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
246
+ };
247
+ }
248
+
249
+ // Get tasks for this feature
250
+ const tasksResult = searchTasks({ featureId: id, limit: 100 });
251
+ const tasks = tasksResult.success ? tasksResult.data : [];
252
+
253
+ const response = createSuccessResponse('Feature overview retrieved', {
254
+ feature: overviewResult.data.feature,
255
+ taskCounts: overviewResult.data.taskCounts,
256
+ tasks,
257
+ });
258
+ return {
259
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
260
+ };
261
+ }
262
+
263
+ if (containerType === 'task') {
264
+ const taskResult = getTask(id);
265
+ if (!taskResult.success) {
266
+ const response = createErrorResponse(
267
+ 'error' in taskResult ? taskResult.error : 'Failed to get task overview'
268
+ );
269
+ return {
270
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
271
+ };
272
+ }
273
+
274
+ // For tasks, just return the task (dependencies could be added in future)
275
+ const response = createSuccessResponse('Task overview retrieved', {
276
+ task: taskResult.data,
277
+ });
278
+ return {
279
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
280
+ };
281
+ }
282
+ }
283
+
284
+ const response = createErrorResponse('Invalid operation');
285
+ return {
286
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
287
+ };
288
+ } catch (error: any) {
289
+ const response = createErrorResponse(error.message || 'Internal error');
290
+ return {
291
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
292
+ };
293
+ }
294
+ }
295
+ );
296
+ }
@@ -0,0 +1,68 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { createSuccessResponse, createErrorResponse, uuidSchema } from './registry';
4
+ import { getDependencies } from '../repos/dependencies';
5
+
6
+ /**
7
+ * Register the query_dependencies MCP tool.
8
+ *
9
+ * Queries dependencies for a task with optional direction filtering.
10
+ *
11
+ * @param server - The MCP server instance to register the tool with
12
+ */
13
+ export function registerQueryDependenciesTool(server: McpServer): void {
14
+ server.tool(
15
+ 'query_dependencies',
16
+ 'Query dependencies for a task',
17
+ {
18
+ taskId: uuidSchema.describe('Task ID'),
19
+ direction: z
20
+ .enum(['dependencies', 'dependents', 'both'])
21
+ .optional()
22
+ .default('both')
23
+ .describe('Direction filter: dependencies, dependents, or both'),
24
+ },
25
+ async (params) => {
26
+ try {
27
+ const result = getDependencies(params.taskId, params.direction);
28
+
29
+ if (result.success === false) {
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text' as const,
34
+ text: JSON.stringify(
35
+ createErrorResponse(result.error, result.code),
36
+ null,
37
+ 2
38
+ ),
39
+ },
40
+ ],
41
+ };
42
+ }
43
+
44
+ return {
45
+ content: [
46
+ {
47
+ type: 'text' as const,
48
+ text: JSON.stringify(
49
+ createSuccessResponse('Dependencies retrieved', result.data),
50
+ null,
51
+ 2
52
+ ),
53
+ },
54
+ ],
55
+ };
56
+ } catch (error: any) {
57
+ return {
58
+ content: [
59
+ {
60
+ type: 'text' as const,
61
+ text: JSON.stringify(createErrorResponse(error.message), null, 2),
62
+ },
63
+ ],
64
+ };
65
+ }
66
+ }
67
+ );
68
+ }
@@ -0,0 +1,70 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { createSuccessResponse, createErrorResponse, uuidSchema } from './registry';
4
+ import { getSections } from '../repos/sections';
5
+
6
+ /**
7
+ * Register the query_sections MCP tool
8
+ *
9
+ * Retrieves sections for a given entity with optional filtering and content inclusion.
10
+ */
11
+ export function registerQuerySectionsTool(server: McpServer): void {
12
+ server.tool(
13
+ 'query_sections',
14
+ 'Retrieve sections for an entity (PROJECT, FEATURE, or TASK). Supports filtering by tags, section IDs, and optional content exclusion for token savings.',
15
+ {
16
+ entityType: z.enum(['PROJECT', 'FEATURE', 'TASK']).describe('The type of entity to query sections for'),
17
+ entityId: uuidSchema.describe('The UUID of the entity'),
18
+ includeContent: z.boolean().optional().default(true).describe('Set to false to exclude content field for token savings'),
19
+ tags: z.string().optional().describe('Comma-separated list of tags to filter sections'),
20
+ sectionIds: z.string().optional().describe('Comma-separated list of section IDs to filter')
21
+ },
22
+ async (params) => {
23
+ try {
24
+ // Parse sectionIds if provided
25
+ const sectionIds = params.sectionIds
26
+ ? params.sectionIds.split(',').map(id => id.trim().replace(/-/g, ''))
27
+ : undefined;
28
+
29
+ // Call getSections with parsed parameters
30
+ const result = getSections(params.entityId, params.entityType, {
31
+ includeContent: params.includeContent,
32
+ tags: params.tags,
33
+ sectionIds
34
+ });
35
+
36
+ // Handle result
37
+ if (!result.success) {
38
+ return {
39
+ content: [{
40
+ type: 'text' as const,
41
+ text: JSON.stringify(createErrorResponse(result.error), null, 2)
42
+ }]
43
+ };
44
+ }
45
+
46
+ return {
47
+ content: [{
48
+ type: 'text' as const,
49
+ text: JSON.stringify(
50
+ createSuccessResponse('Sections retrieved successfully', result.data),
51
+ null,
52
+ 2
53
+ )
54
+ }]
55
+ };
56
+ } catch (error: any) {
57
+ return {
58
+ content: [{
59
+ type: 'text' as const,
60
+ text: JSON.stringify(
61
+ createErrorResponse(error.message || 'Failed to query sections'),
62
+ null,
63
+ 2
64
+ )
65
+ }]
66
+ };
67
+ }
68
+ }
69
+ );
70
+ }
@@ -0,0 +1,137 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { createSuccessResponse, createErrorResponse, uuidSchema, optionalUuidSchema } from './registry';
4
+ import { getTemplate, listTemplates } from '../repos/templates';
5
+
6
+ /**
7
+ * Registers the `query_templates` MCP tool.
8
+ *
9
+ * Operations:
10
+ * - get: Retrieve a single template by ID
11
+ * - list: List templates with optional filters
12
+ */
13
+ export function registerQueryTemplatesTool(server: McpServer): void {
14
+ server.tool(
15
+ 'query_templates',
16
+ 'Query templates with operations: get (retrieve single template) or list (retrieve multiple templates with filters)',
17
+ {
18
+ operation: z.enum(['get', 'list']),
19
+ id: optionalUuidSchema,
20
+ includeSections: z.boolean().optional().default(false),
21
+ targetEntityType: z.string().optional(),
22
+ isBuiltIn: z.boolean().optional(),
23
+ isEnabled: z.boolean().optional(),
24
+ tags: z.string().optional()
25
+ },
26
+ async (params: any) => {
27
+ try {
28
+ const operation = params.operation;
29
+
30
+ if (operation === 'get') {
31
+ // Validate required parameters for get
32
+ if (!params.id) {
33
+ return {
34
+ content: [{
35
+ type: 'text' as const,
36
+ text: JSON.stringify(
37
+ createErrorResponse('Template ID is required for get operation'),
38
+ null,
39
+ 2
40
+ )
41
+ }]
42
+ };
43
+ }
44
+
45
+ const includeSections = params.includeSections ?? false;
46
+ const result = getTemplate(params.id, includeSections);
47
+
48
+ if (!result.success) {
49
+ return {
50
+ content: [{
51
+ type: 'text' as const,
52
+ text: JSON.stringify(
53
+ createErrorResponse(result.error || 'Failed to get template'),
54
+ null,
55
+ 2
56
+ )
57
+ }]
58
+ };
59
+ }
60
+
61
+ return {
62
+ content: [{
63
+ type: 'text' as const,
64
+ text: JSON.stringify(
65
+ createSuccessResponse('Template retrieved successfully', result.data),
66
+ null,
67
+ 2
68
+ )
69
+ }]
70
+ };
71
+ } else if (operation === 'list') {
72
+ // Build filter parameters
73
+ const filters = {
74
+ targetEntityType: params.targetEntityType,
75
+ isBuiltIn: params.isBuiltIn,
76
+ isEnabled: params.isEnabled,
77
+ tags: params.tags
78
+ };
79
+
80
+ const result = listTemplates(filters);
81
+
82
+ if (!result.success) {
83
+ return {
84
+ content: [{
85
+ type: 'text' as const,
86
+ text: JSON.stringify(
87
+ createErrorResponse(result.error || 'Failed to list templates'),
88
+ null,
89
+ 2
90
+ )
91
+ }]
92
+ };
93
+ }
94
+
95
+ return {
96
+ content: [{
97
+ type: 'text' as const,
98
+ text: JSON.stringify(
99
+ createSuccessResponse(
100
+ `Found ${result.data.length} template(s)`,
101
+ result.data
102
+ ),
103
+ null,
104
+ 2
105
+ )
106
+ }]
107
+ };
108
+ } else {
109
+ return {
110
+ content: [{
111
+ type: 'text' as const,
112
+ text: JSON.stringify(
113
+ createErrorResponse(`Invalid operation: ${operation}`),
114
+ null,
115
+ 2
116
+ )
117
+ }]
118
+ };
119
+ }
120
+ } catch (error: any) {
121
+ return {
122
+ content: [{
123
+ type: 'text' as const,
124
+ text: JSON.stringify(
125
+ createErrorResponse(
126
+ 'Internal error',
127
+ error.message || 'Unknown error occurred'
128
+ ),
129
+ null,
130
+ 2
131
+ )
132
+ }]
133
+ };
134
+ }
135
+ }
136
+ );
137
+ }