claude-queue 1.0.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.
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+
8
+ // src/http.ts
9
+ var KANBAN_URL = process.env.KANBAN_SERVER_URL || "http://localhost:3333";
10
+ async function httpGet(url) {
11
+ const response = await fetch(`${KANBAN_URL}${url}`);
12
+ if (!response.ok) {
13
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
14
+ }
15
+ return response.json();
16
+ }
17
+ async function httpPost(url, body) {
18
+ const response = await fetch(`${KANBAN_URL}${url}`, {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify(body)
22
+ });
23
+ if (!response.ok) {
24
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
25
+ }
26
+ return response.json();
27
+ }
28
+ async function httpPatch(url, body) {
29
+ const response = await fetch(`${KANBAN_URL}${url}`, {
30
+ method: "PATCH",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify(body)
33
+ });
34
+ if (!response.ok) {
35
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
36
+ }
37
+ return response.json();
38
+ }
39
+
40
+ // src/handlers/watch.ts
41
+ async function handleWatch(args) {
42
+ const projectId = args?.projectId;
43
+ const project = await httpGet(`/api/projects/${projectId}?heartbeat=true`);
44
+ const pausedWarning = project.paused ? `
45
+
46
+ \u26A0\uFE0F WARNING: This project is currently PAUSED. Claude will not pick up new tasks until resumed. The user can click "Resume" in the task queue UI to allow task processing.` : "";
47
+ return {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: `Connected to project "${project.name}" (${project.id}).${pausedWarning}
52
+
53
+ You are now watching this task queue. Follow this loop:
54
+ 1. Call queue_get_tasks with status "ready" to find available tasks
55
+ 2. If found, call queue_claim_task to start working
56
+ 3. Update queue_update_activity periodically as you work
57
+ 4. If you need user input, call queue_set_blocked with your question
58
+ 5. Call queue_wait_for_reply to wait for user response
59
+ 6. When done, call queue_complete_task
60
+ 7. Auto-commit changes: git add -A && git commit -m "task: {title}"
61
+ 8. Repeat from step 1
62
+
63
+ If a task is deleted while you're working on it, run: git reset --hard HEAD
64
+ If queue_wait_for_reply returns { deleted: true }, discard changes and pick the next task.`
65
+ }
66
+ ]
67
+ };
68
+ }
69
+
70
+ // src/handlers/tasks.ts
71
+ async function handleGetTasks(args) {
72
+ const projectId = args?.projectId;
73
+ const status = args?.status;
74
+ const project = await httpGet(`/api/projects/${projectId}?heartbeat=true`);
75
+ if (project.paused) {
76
+ return {
77
+ content: [
78
+ {
79
+ type: "text",
80
+ text: JSON.stringify({
81
+ paused: true,
82
+ message: "Project is paused. Wait for user to resume before picking up new tasks."
83
+ })
84
+ }
85
+ ]
86
+ };
87
+ }
88
+ const url = status ? `/api/tasks/project/${projectId}?status=${status}` : `/api/tasks/project/${projectId}`;
89
+ const tasks = await httpGet(url);
90
+ return {
91
+ content: [
92
+ {
93
+ type: "text",
94
+ text: JSON.stringify(tasks, null, 2)
95
+ }
96
+ ]
97
+ };
98
+ }
99
+ async function handleClaimTask(args) {
100
+ const taskId = args?.taskId;
101
+ const starting_commit = args?.starting_commit;
102
+ const task = await httpPost(`/api/tasks/${taskId}/move`, {
103
+ status: "in_progress",
104
+ position: 0,
105
+ starting_commit
106
+ });
107
+ const [attachments, masterPrompt, projectPrompt] = await Promise.all([
108
+ httpGet(`/api/attachments/task/${taskId}`),
109
+ httpGet(`/api/prompts/master`),
110
+ httpGet(`/api/prompts/project/${task.project_id}`)
111
+ ]);
112
+ const imageAttachments = attachments.filter(
113
+ (a) => a.mime_type.startsWith("image/")
114
+ );
115
+ const content = [];
116
+ const hasPrompts = masterPrompt || projectPrompt;
117
+ if (hasPrompts) {
118
+ const promptParts = [];
119
+ if (masterPrompt) {
120
+ promptParts.push(`## Master Instructions
121
+ ${masterPrompt.content}`);
122
+ }
123
+ if (projectPrompt) {
124
+ promptParts.push(`## Project Instructions
125
+ ${projectPrompt.content}`);
126
+ }
127
+ content.push({
128
+ type: "text",
129
+ text: `# Custom Instructions
130
+
131
+ ${promptParts.join("\n\n")}`
132
+ });
133
+ }
134
+ content.push({
135
+ type: "text",
136
+ text: `Claimed task: "${task.title}" - now in progress (starting commit: ${starting_commit})`
137
+ });
138
+ if (imageAttachments.length > 0) {
139
+ const attachmentPaths = [];
140
+ for (const attachment of imageAttachments) {
141
+ const pathData = await httpGet(`/api/attachments/${attachment.id}/path`);
142
+ attachmentPaths.push(pathData.path);
143
+ }
144
+ content.push({
145
+ type: "text",
146
+ text: `
147
+
148
+ This task has ${imageAttachments.length} image attachment(s). Read these files to see the images:
149
+ ${attachmentPaths.map((p) => `- ${p}`).join("\n")}`
150
+ });
151
+ }
152
+ return { content };
153
+ }
154
+ async function handleCompleteTask(args) {
155
+ const taskId = args?.taskId;
156
+ const task = await httpPost(`/api/tasks/${taskId}/move`, {
157
+ status: "done",
158
+ position: 0
159
+ });
160
+ return {
161
+ content: [
162
+ {
163
+ type: "text",
164
+ text: `Task "${task.title}" completed and moved to Done!`
165
+ }
166
+ ]
167
+ };
168
+ }
169
+
170
+ // src/handlers/create-task.ts
171
+ async function handleCreateTask(args) {
172
+ const { projectId, title, description, status = "ready" } = args;
173
+ const task = await httpPost(`/api/tasks/project/${projectId}`, {
174
+ title,
175
+ description: description || null,
176
+ status
177
+ });
178
+ return {
179
+ content: [
180
+ {
181
+ type: "text",
182
+ text: `Created task "${task.title}" in ${status} column (id: ${task.id})`
183
+ }
184
+ ]
185
+ };
186
+ }
187
+
188
+ // src/handlers/activity.ts
189
+ async function handleUpdateActivity(args) {
190
+ const taskId = args?.taskId;
191
+ const activity = args?.activity;
192
+ await httpPatch(`/api/tasks/${taskId}`, { current_activity: activity });
193
+ return {
194
+ content: [
195
+ {
196
+ type: "text",
197
+ text: `Activity updated: ${activity}`
198
+ }
199
+ ]
200
+ };
201
+ }
202
+
203
+ // src/handlers/blocking.ts
204
+ async function handleSetBlocked(args) {
205
+ const taskId = args?.taskId;
206
+ const question = args?.question;
207
+ await httpPatch(`/api/tasks/${taskId}`, {
208
+ blocked: true,
209
+ current_activity: "Waiting for user input"
210
+ });
211
+ await httpPost(`/api/comments/task/${taskId}`, {
212
+ author: "claude",
213
+ content: question
214
+ });
215
+ return {
216
+ content: [
217
+ {
218
+ type: "text",
219
+ text: "Task marked as blocked. Question posted to user."
220
+ }
221
+ ]
222
+ };
223
+ }
224
+ async function handleWaitForReply(args) {
225
+ const taskId = args?.taskId;
226
+ try {
227
+ const response = await fetch(`${KANBAN_URL}/api/comments/task/${taskId}/wait-for-reply`);
228
+ if (response.status === 404) {
229
+ return {
230
+ content: [
231
+ {
232
+ type: "text",
233
+ text: JSON.stringify({ deleted: true })
234
+ }
235
+ ]
236
+ };
237
+ }
238
+ const data = await response.json();
239
+ if (data.deleted) {
240
+ return {
241
+ content: [
242
+ {
243
+ type: "text",
244
+ text: JSON.stringify({ deleted: true })
245
+ }
246
+ ]
247
+ };
248
+ }
249
+ if (data.timeout) {
250
+ return {
251
+ content: [
252
+ {
253
+ type: "text",
254
+ text: JSON.stringify({ timeout: true })
255
+ }
256
+ ]
257
+ };
258
+ }
259
+ await httpPatch(`/api/tasks/${taskId}`, { blocked: false });
260
+ await httpPatch(`/api/comments/task/${taskId}/mark-seen`, {});
261
+ return {
262
+ content: [
263
+ {
264
+ type: "text",
265
+ text: JSON.stringify({ reply: data.content })
266
+ }
267
+ ]
268
+ };
269
+ } catch (error) {
270
+ return {
271
+ content: [
272
+ {
273
+ type: "text",
274
+ text: JSON.stringify({ error: String(error) })
275
+ }
276
+ ]
277
+ };
278
+ }
279
+ }
280
+
281
+ // src/handlers/comments.ts
282
+ async function handleCheckComments(args) {
283
+ const taskId = args?.taskId;
284
+ const since = args?.since;
285
+ const url = since ? `/api/comments/task/${taskId}?since=${encodeURIComponent(since)}` : `/api/comments/task/${taskId}`;
286
+ const comments = await httpGet(url);
287
+ const userComments = comments.filter((c) => c.author === "user");
288
+ const unseenComments = userComments.filter((c) => !c.seen);
289
+ if (unseenComments.length > 0) {
290
+ await httpPatch(`/api/comments/task/${taskId}/mark-seen`, {});
291
+ await httpPatch(`/api/tasks/${taskId}`, { blocked: false });
292
+ }
293
+ if (userComments.length === 0) {
294
+ return {
295
+ content: [
296
+ {
297
+ type: "text",
298
+ text: JSON.stringify({ comments: [], hasNewComments: false })
299
+ }
300
+ ]
301
+ };
302
+ }
303
+ return {
304
+ content: [
305
+ {
306
+ type: "text",
307
+ text: JSON.stringify({
308
+ comments: userComments,
309
+ hasNewComments: unseenComments.length > 0,
310
+ latestTimestamp: userComments[userComments.length - 1].created_at
311
+ })
312
+ }
313
+ ]
314
+ };
315
+ }
316
+ async function handleAddComment(args) {
317
+ const taskId = args?.taskId;
318
+ const content = args?.content;
319
+ await httpPost(`/api/comments/task/${taskId}`, {
320
+ author: "claude",
321
+ content
322
+ });
323
+ return {
324
+ content: [
325
+ {
326
+ type: "text",
327
+ text: "Comment added"
328
+ }
329
+ ]
330
+ };
331
+ }
332
+
333
+ // src/handlers/list-projects.ts
334
+ async function handleListProjects() {
335
+ const projects = await httpGet("/api/projects");
336
+ if (projects.length === 0) {
337
+ return {
338
+ content: [
339
+ {
340
+ type: "text",
341
+ text: "No projects found. Create a project first by running `claude-queue` in a directory."
342
+ }
343
+ ]
344
+ };
345
+ }
346
+ const projectList = projects.map((p) => ` /queue ${p.id} # ${p.name} (${p.path})`).join("\n");
347
+ return {
348
+ content: [
349
+ {
350
+ type: "text",
351
+ text: `Available projects:
352
+
353
+ ${projectList}
354
+
355
+ Run one of the commands above to start working on that project's queue.`
356
+ }
357
+ ]
358
+ };
359
+ }
360
+
361
+ // src/index.ts
362
+ var server = new McpServer({
363
+ name: "claude-queue",
364
+ version: "1.0.0"
365
+ });
366
+ server.registerTool(
367
+ "queue_watch",
368
+ {
369
+ description: "Connect to a kanban board and start watching for tasks. Returns instructions for the watching loop.",
370
+ inputSchema: {
371
+ projectId: z.string().describe("Project ID (e.g., kbn-a3x9)")
372
+ }
373
+ },
374
+ async (args) => {
375
+ return await handleWatch(args);
376
+ }
377
+ );
378
+ server.registerTool(
379
+ "queue_get_tasks",
380
+ {
381
+ description: "Get tasks from the board, optionally filtered by status",
382
+ inputSchema: {
383
+ projectId: z.string().describe("Project ID"),
384
+ status: z.enum(["backlog", "ready", "in_progress", "done"]).optional().describe("Filter by status (optional)")
385
+ }
386
+ },
387
+ async (args) => {
388
+ return await handleGetTasks(args);
389
+ }
390
+ );
391
+ server.registerTool(
392
+ "queue_claim_task",
393
+ {
394
+ description: "Claim a ready task and move it to in_progress. Pass the current git commit hash as starting_commit - this will be used to reset changes if the task is cancelled.",
395
+ inputSchema: {
396
+ taskId: z.string().describe("Task ID to claim"),
397
+ starting_commit: z.string().describe("Current git commit hash (from `git rev-parse HEAD`). Used to reset changes if task is cancelled.")
398
+ }
399
+ },
400
+ async (args) => {
401
+ return await handleClaimTask(args);
402
+ }
403
+ );
404
+ server.registerTool(
405
+ "queue_create_task",
406
+ {
407
+ description: "Create a new task in a project. Used during planning mode to add tasks to the queue.",
408
+ inputSchema: {
409
+ projectId: z.string().describe("Project ID (e.g., kbn-a3x9)"),
410
+ title: z.string().describe("Task title"),
411
+ description: z.string().optional().describe("Task description (optional)"),
412
+ status: z.enum(["backlog", "ready"]).optional().describe("Task status: 'backlog' or 'ready' (default: 'ready')")
413
+ }
414
+ },
415
+ async (args) => {
416
+ return await handleCreateTask(args);
417
+ }
418
+ );
419
+ server.registerTool(
420
+ "queue_update_activity",
421
+ {
422
+ description: "Update what you're currently doing on a task",
423
+ inputSchema: {
424
+ taskId: z.string().describe("Task ID"),
425
+ activity: z.string().describe("Current activity (e.g., 'Reading auth module')")
426
+ }
427
+ },
428
+ async (args) => {
429
+ return await handleUpdateActivity(args);
430
+ }
431
+ );
432
+ server.registerTool(
433
+ "queue_set_blocked",
434
+ {
435
+ description: "Mark task as blocked and ask user a question",
436
+ inputSchema: {
437
+ taskId: z.string().describe("Task ID"),
438
+ question: z.string().describe("Question to ask the user")
439
+ }
440
+ },
441
+ async (args) => {
442
+ return await handleSetBlocked(args);
443
+ }
444
+ );
445
+ server.registerTool(
446
+ "queue_wait_for_reply",
447
+ {
448
+ description: "Poll until user replies to your question. Returns { reply: string } or { deleted: true } if task was deleted.",
449
+ inputSchema: {
450
+ taskId: z.string().describe("Task ID")
451
+ }
452
+ },
453
+ async (args) => {
454
+ return await handleWaitForReply(args);
455
+ }
456
+ );
457
+ server.registerTool(
458
+ "queue_complete_task",
459
+ {
460
+ description: "Mark task as complete and move to done",
461
+ inputSchema: {
462
+ taskId: z.string().describe("Task ID")
463
+ }
464
+ },
465
+ async (args) => {
466
+ return await handleCompleteTask(args);
467
+ }
468
+ );
469
+ server.registerTool(
470
+ "queue_check_comments",
471
+ {
472
+ description: "Check for new user comments on a task. Call this periodically while working to see if the user has left feedback or instructions.",
473
+ inputSchema: {
474
+ taskId: z.string().describe("Task ID"),
475
+ since: z.string().optional().describe("ISO timestamp to check for comments after (optional). If not provided, returns all user comments.")
476
+ }
477
+ },
478
+ async (args) => {
479
+ return await handleCheckComments(args);
480
+ }
481
+ );
482
+ server.registerTool(
483
+ "queue_add_comment",
484
+ {
485
+ description: "Add a comment to a task. Use this to leave summaries or notes.",
486
+ inputSchema: {
487
+ taskId: z.string().describe("Task ID"),
488
+ content: z.string().describe("Comment content")
489
+ }
490
+ },
491
+ async (args) => {
492
+ return await handleAddComment(args);
493
+ }
494
+ );
495
+ server.registerTool(
496
+ "queue_list_projects",
497
+ {
498
+ description: "List all available projects. Use this when no project ID is provided or when a project is not found.",
499
+ inputSchema: {}
500
+ },
501
+ async () => {
502
+ return await handleListProjects();
503
+ }
504
+ );
505
+ async function main() {
506
+ const transport = new StdioServerTransport();
507
+ await server.connect(transport);
508
+ }
509
+ main().catch(console.error);
package/dist/mcp.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const mcpPath = join(__dirname, "mcp", "index.js");
7
+
8
+ import(mcpPath);