@rallycry/conveyor-mcp 3.3.0 → 3.5.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.
- package/dist/{chunk-OAHSTMYM.js → chunk-Z566YAS7.js} +147 -73
- package/dist/chunk-Z566YAS7.js.map +1 -0
- package/dist/cli.js +443 -155
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/tunnel-cli.js +1 -1
- package/dist/tunnel.d.ts +60 -22
- package/package.json +3 -1
- package/dist/chunk-OAHSTMYM.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ConveyorConnection
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-Z566YAS7.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { createRequire } from "module";
|
|
@@ -9,29 +9,43 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
10
|
|
|
11
11
|
// src/tools/project.ts
|
|
12
|
+
import { z } from "zod";
|
|
12
13
|
function registerProjectTools(server2, conn2) {
|
|
13
14
|
server2.tool(
|
|
14
|
-
"
|
|
15
|
-
"
|
|
15
|
+
"list_projects",
|
|
16
|
+
"List Conveyor projects available to this MCP token's user. Use a returned project id as projectId on other tools when no default project is configured or when targeting a different project.",
|
|
16
17
|
{},
|
|
17
18
|
async () => {
|
|
18
|
-
const
|
|
19
|
+
const projects = await conn2.listProjects();
|
|
20
|
+
return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
server2.tool(
|
|
24
|
+
"get_project_summary",
|
|
25
|
+
"Get overall project status: task counts by status, active builds, repo info. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
26
|
+
{
|
|
27
|
+
projectId: z.string().optional().describe("Target Conveyor project ID")
|
|
28
|
+
},
|
|
29
|
+
async (params) => {
|
|
30
|
+
const summary = await conn2.getProjectSummary(params.projectId);
|
|
19
31
|
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
20
32
|
}
|
|
21
33
|
);
|
|
22
34
|
server2.tool(
|
|
23
35
|
"list_project_members",
|
|
24
|
-
"List project members with user ID, name, email, and access level \u2014 use to resolve a person's name or email to a user ID for task assignment or review",
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
"List project members with user ID, name, email, and access level \u2014 use to resolve a person's name or email to a user ID for task assignment or review. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
37
|
+
{
|
|
38
|
+
projectId: z.string().optional().describe("Target Conveyor project ID")
|
|
39
|
+
},
|
|
40
|
+
async (params) => {
|
|
41
|
+
const members = await conn2.listProjectMembers(params.projectId);
|
|
28
42
|
return { content: [{ type: "text", text: JSON.stringify(members, null, 2) }] };
|
|
29
43
|
}
|
|
30
44
|
);
|
|
31
45
|
}
|
|
32
46
|
|
|
33
47
|
// src/tools/tasks.ts
|
|
34
|
-
import { z } from "zod";
|
|
48
|
+
import { z as z2 } from "zod";
|
|
35
49
|
var CLI_EVENT_FORMATTERS = {
|
|
36
50
|
thinking: (data) => String(data.message ?? ""),
|
|
37
51
|
tool_use: (data) => `${data.tool}: ${String(data.input ?? "").slice(0, 1e3)}`,
|
|
@@ -69,12 +83,13 @@ var STATUS_ENUM = [
|
|
|
69
83
|
function registerListTasks(server2, conn2) {
|
|
70
84
|
server2.tool(
|
|
71
85
|
"list_tasks",
|
|
72
|
-
"List project tasks, optionally filtered by status or assignment (a specific assignee, or unassigned tasks). Returns summaries \u2014 plan omitted, description truncated; use get_task for full details.",
|
|
86
|
+
"List project tasks, optionally filtered by status or assignment (a specific assignee, or unassigned tasks). Pass projectId to target a specific project; otherwise the configured default project is used. Returns summaries \u2014 plan omitted, description truncated; use get_task for full details.",
|
|
73
87
|
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
89
|
+
status: z2.enum(STATUS_ENUM).optional().describe("Filter by task status"),
|
|
90
|
+
assigneeId: z2.string().optional().describe("Filter by assigned user ID"),
|
|
91
|
+
unassigned: z2.boolean().optional().describe("Only return tasks with no assignee (mutually exclusive with assigneeId)"),
|
|
92
|
+
limit: z2.number().optional().describe("Max tasks to return (default 50)")
|
|
78
93
|
},
|
|
79
94
|
async (params) => {
|
|
80
95
|
const tasks = await conn2.listTasks(params);
|
|
@@ -87,12 +102,27 @@ function registerListTasks(server2, conn2) {
|
|
|
87
102
|
function registerGetTask(server2, conn2) {
|
|
88
103
|
server2.tool(
|
|
89
104
|
"get_task",
|
|
90
|
-
"Get full task details including plan, chat history, PR info, subtasks, and build status",
|
|
105
|
+
"Get full task details including plan, chat history, PR info, subtasks, and build status. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
106
|
+
{
|
|
107
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
108
|
+
taskId: z2.string().describe("The task ID or slug (the value in a card URL, /cards/<slug>)")
|
|
109
|
+
},
|
|
110
|
+
async (params) => {
|
|
111
|
+
const task = await conn2.getTask(params.taskId, params.projectId);
|
|
112
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
function registerGetCardBySlug(server2, conn2) {
|
|
117
|
+
server2.tool(
|
|
118
|
+
"get_card_by_slug",
|
|
119
|
+
"Get full card details by the slug from a card URL (/cards/<slug>) instead of a task ID. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
91
120
|
{
|
|
92
|
-
|
|
121
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
122
|
+
slug: z2.string().describe("The card slug from a Conveyor card URL, e.g. 'ship-it'")
|
|
93
123
|
},
|
|
94
124
|
async (params) => {
|
|
95
|
-
const task = await conn2.
|
|
125
|
+
const task = await conn2.getCardBySlug(params.slug, params.projectId);
|
|
96
126
|
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
97
127
|
}
|
|
98
128
|
);
|
|
@@ -100,12 +130,13 @@ function registerGetTask(server2, conn2) {
|
|
|
100
130
|
function registerCreateTask(server2, conn2) {
|
|
101
131
|
server2.tool(
|
|
102
132
|
"create_task",
|
|
103
|
-
"Create a new task with title, description, and optional plan. Icon, story points, and agent assignment are auto-filled when a task is created in (or later moved to) a status beyond Planning \u2014 don't spend turns on them.",
|
|
133
|
+
"Create a new task with title, description, and optional plan. Pass projectId to target a specific project; otherwise the configured default project is used. Icon, story points, and agent assignment are auto-filled when a task is created in (or later moved to) a status beyond Planning \u2014 don't spend turns on them.",
|
|
104
134
|
{
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
136
|
+
title: z2.string().describe("Task title"),
|
|
137
|
+
description: z2.string().optional().describe("Task description"),
|
|
138
|
+
plan: z2.string().optional().describe("Task implementation plan (markdown)"),
|
|
139
|
+
status: z2.enum(["Planning", "Open"]).optional().describe("Initial status (default: Planning)")
|
|
109
140
|
},
|
|
110
141
|
async (params) => {
|
|
111
142
|
const task = await conn2.createTask(params);
|
|
@@ -118,14 +149,15 @@ function registerCreateTask(server2, conn2) {
|
|
|
118
149
|
function registerUpdateTask(server2, conn2) {
|
|
119
150
|
server2.tool(
|
|
120
151
|
"update_task",
|
|
121
|
-
"Update task fields: title, description, plan, status, or assignment. Moving a task beyond Planning auto-fills any missing icon, story points, and agent assignment \u2014 don't spend turns on them.",
|
|
152
|
+
"Update task fields: title, description, plan, status, or assignment. Pass projectId to target a specific project; otherwise the configured default project is used. Moving a task beyond Planning auto-fills any missing icon, story points, and agent assignment \u2014 don't spend turns on them.",
|
|
122
153
|
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
154
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
155
|
+
taskId: z2.string().describe("The task ID"),
|
|
156
|
+
title: z2.string().optional().describe("New title"),
|
|
157
|
+
description: z2.string().optional().describe("New description"),
|
|
158
|
+
plan: z2.string().optional().describe("New plan (markdown)"),
|
|
159
|
+
status: z2.enum(STATUS_ENUM).optional().describe("New status"),
|
|
160
|
+
assignedUserId: z2.string().nullable().optional().describe("User ID to assign, or null")
|
|
129
161
|
},
|
|
130
162
|
async (params) => {
|
|
131
163
|
const result = await conn2.updateTask(params);
|
|
@@ -138,25 +170,27 @@ function registerUpdateTask(server2, conn2) {
|
|
|
138
170
|
function registerChatTools(server2, conn2) {
|
|
139
171
|
server2.tool(
|
|
140
172
|
"read_task_chat",
|
|
141
|
-
"Read messages from a task's chat. For agent execution logs use get_task_logs.",
|
|
173
|
+
"Read messages from a task's chat. Pass projectId to target a specific project; otherwise the configured default project is used. For agent execution logs use get_task_logs.",
|
|
142
174
|
{
|
|
143
|
-
|
|
144
|
-
|
|
175
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
176
|
+
taskId: z2.string().describe("The task ID"),
|
|
177
|
+
limit: z2.number().optional().describe("Max messages to return (default 50)")
|
|
145
178
|
},
|
|
146
179
|
async (params) => {
|
|
147
|
-
const messages = await conn2.getTaskChat(params.taskId, params.limit);
|
|
180
|
+
const messages = await conn2.getTaskChat(params.taskId, params.limit, params.projectId);
|
|
148
181
|
return { content: [{ type: "text", text: JSON.stringify(messages, null, 2) }] };
|
|
149
182
|
}
|
|
150
183
|
);
|
|
151
184
|
server2.tool(
|
|
152
185
|
"post_to_chat",
|
|
153
|
-
"Post a message to a task's chat",
|
|
186
|
+
"Post a message to a task's chat. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
154
187
|
{
|
|
155
|
-
|
|
156
|
-
|
|
188
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
189
|
+
taskId: z2.string().describe("The task ID"),
|
|
190
|
+
content: z2.string().describe("Message content")
|
|
157
191
|
},
|
|
158
192
|
async (params) => {
|
|
159
|
-
await conn2.postToTaskChat(params.taskId, params.content);
|
|
193
|
+
await conn2.postToTaskChat(params.taskId, params.content, params.projectId);
|
|
160
194
|
return { content: [{ type: "text", text: "Message posted" }] };
|
|
161
195
|
}
|
|
162
196
|
);
|
|
@@ -164,17 +198,18 @@ function registerChatTools(server2, conn2) {
|
|
|
164
198
|
function registerGetTaskCli(server2, conn2) {
|
|
165
199
|
server2.tool(
|
|
166
200
|
"get_task_logs",
|
|
167
|
-
"Read CLI execution logs from a task. Returns agent reasoning, tool calls, setup output, and other execution events. For human chat use read_task_chat.",
|
|
201
|
+
"Read CLI execution logs from a task. Pass projectId to target a specific project; otherwise the configured default project is used. Returns agent reasoning, tool calls, setup output, and other execution events. For human chat use read_task_chat.",
|
|
168
202
|
{
|
|
169
|
-
|
|
170
|
-
|
|
203
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
204
|
+
taskId: z2.string().describe("The task ID or slug"),
|
|
205
|
+
source: z2.enum(["agent", "application"]).optional().describe(
|
|
171
206
|
"Filter by log source: 'agent' for reasoning/tool calls, 'application' for setup/dev-server output"
|
|
172
207
|
),
|
|
173
|
-
limit:
|
|
208
|
+
limit: z2.number().optional().describe("Max entries to return (default 50, max 500)")
|
|
174
209
|
},
|
|
175
|
-
async ({ taskId, source, limit }) => {
|
|
210
|
+
async ({ taskId, source, limit, projectId: projectId2 }) => {
|
|
176
211
|
const effectiveLimit = Math.min(limit ?? 50, 500);
|
|
177
|
-
const logs = await conn2.getTaskCli(taskId, effectiveLimit, source);
|
|
212
|
+
const logs = await conn2.getTaskCli(taskId, effectiveLimit, source, projectId2);
|
|
178
213
|
const formatted = logs.map((log) => {
|
|
179
214
|
return `[${log.timestamp}] [${log.type}] ${formatCliEventSummary(log.type, log.data)}`;
|
|
180
215
|
}).join("\n");
|
|
@@ -187,14 +222,15 @@ function registerGetTaskCli(server2, conn2) {
|
|
|
187
222
|
function registerSearchTasks(server2, conn2) {
|
|
188
223
|
server2.tool(
|
|
189
224
|
"search_tasks",
|
|
190
|
-
"Search tasks by tag name, text query, status, and/or assignment. Use tag names like 'agent-runner', not IDs. Returns summaries \u2014 plan omitted, description truncated; use get_task for full details.",
|
|
225
|
+
"Search tasks by tag name, text query, status, and/or assignment. Pass projectId to target a specific project; otherwise the configured default project is used. Use tag names like 'agent-runner', not IDs. Returns summaries \u2014 plan omitted, description truncated; use get_task for full details.",
|
|
191
226
|
{
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
227
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
228
|
+
tagNames: z2.array(z2.string()).optional().describe('Tag names to filter by (e.g., ["agent-runner", "chat"])'),
|
|
229
|
+
searchQuery: z2.string().optional().describe("Text search on title and description"),
|
|
230
|
+
statusFilters: z2.array(z2.enum(STATUS_ENUM)).optional().describe("Filter by one or more statuses"),
|
|
231
|
+
assigneeId: z2.string().optional().describe("Filter by assigned user ID"),
|
|
232
|
+
unassigned: z2.boolean().optional().describe("Only return tasks with no assignee (mutually exclusive with assigneeId)"),
|
|
233
|
+
limit: z2.number().optional().describe("Max results to return (default 20)")
|
|
198
234
|
},
|
|
199
235
|
async (params) => {
|
|
200
236
|
const tasks = await conn2.searchTasks(params);
|
|
@@ -207,10 +243,12 @@ function registerSearchTasks(server2, conn2) {
|
|
|
207
243
|
function registerListTags(server2, conn2) {
|
|
208
244
|
server2.tool(
|
|
209
245
|
"list_tags",
|
|
210
|
-
"List all project tags with their names, IDs, and colors",
|
|
211
|
-
{
|
|
212
|
-
|
|
213
|
-
|
|
246
|
+
"List all project tags with their names, IDs, and colors. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
247
|
+
{
|
|
248
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID")
|
|
249
|
+
},
|
|
250
|
+
async (params) => {
|
|
251
|
+
const tags = await conn2.listTags(params.projectId);
|
|
214
252
|
return { content: [{ type: "text", text: JSON.stringify(tags, null, 2) }] };
|
|
215
253
|
}
|
|
216
254
|
);
|
|
@@ -218,12 +256,13 @@ function registerListTags(server2, conn2) {
|
|
|
218
256
|
function registerReviewTools(server2, conn2) {
|
|
219
257
|
server2.tool(
|
|
220
258
|
"approve_task",
|
|
221
|
-
"Move a task forward in the review flow (ReviewPR -> ReviewDev, or -> Complete)",
|
|
259
|
+
"Move a task forward in the review flow (ReviewPR -> ReviewDev, or -> Complete). Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
222
260
|
{
|
|
223
|
-
|
|
261
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
262
|
+
taskId: z2.string().describe("The task ID")
|
|
224
263
|
},
|
|
225
264
|
async (params) => {
|
|
226
|
-
const result = await conn2.approveTask(params.taskId);
|
|
265
|
+
const result = await conn2.approveTask(params.taskId, params.projectId);
|
|
227
266
|
return {
|
|
228
267
|
content: [{ type: "text", text: `Task approved, new status: ${result.status}` }]
|
|
229
268
|
};
|
|
@@ -231,12 +270,13 @@ function registerReviewTools(server2, conn2) {
|
|
|
231
270
|
);
|
|
232
271
|
server2.tool(
|
|
233
272
|
"approve_and_merge_pr",
|
|
234
|
-
"Approve and merge a child task's pull request. Only succeeds if all CI/CD checks are passing. The child task must be in ReviewPR status with a PR.",
|
|
273
|
+
"Approve and merge a child task's pull request. Pass projectId to target a specific project; otherwise the configured default project is used. Only succeeds if all CI/CD checks are passing. The child task must be in ReviewPR status with a PR.",
|
|
235
274
|
{
|
|
236
|
-
|
|
275
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
276
|
+
childTaskId: z2.string().describe("The child task ID whose PR should be approved and merged")
|
|
237
277
|
},
|
|
238
278
|
async (params) => {
|
|
239
|
-
const result = await conn2.approveAndMergePR(params.childTaskId);
|
|
279
|
+
const result = await conn2.approveAndMergePR(params.childTaskId, params.projectId);
|
|
240
280
|
return {
|
|
241
281
|
content: [
|
|
242
282
|
{
|
|
@@ -249,13 +289,14 @@ function registerReviewTools(server2, conn2) {
|
|
|
249
289
|
);
|
|
250
290
|
server2.tool(
|
|
251
291
|
"request_changes",
|
|
252
|
-
"Post feedback and send task back to InProgress for more work",
|
|
292
|
+
"Post feedback and send task back to InProgress for more work. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
253
293
|
{
|
|
254
|
-
|
|
255
|
-
|
|
294
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
295
|
+
taskId: z2.string().describe("The task ID"),
|
|
296
|
+
feedback: z2.string().describe("Feedback message describing requested changes")
|
|
256
297
|
},
|
|
257
298
|
async (params) => {
|
|
258
|
-
await conn2.requestChanges(params.taskId, params.feedback);
|
|
299
|
+
await conn2.requestChanges(params.taskId, params.feedback, params.projectId);
|
|
259
300
|
return { content: [{ type: "text", text: "Changes requested, task moved to InProgress" }] };
|
|
260
301
|
}
|
|
261
302
|
);
|
|
@@ -267,10 +308,11 @@ function formatReviewerList(reviewers) {
|
|
|
267
308
|
function registerReviewerTools(server2, conn2) {
|
|
268
309
|
server2.tool(
|
|
269
310
|
"add_reviewer",
|
|
270
|
-
"Add a project member as a reviewer on a task. Idempotent \u2014 adding an existing reviewer is a no-op.",
|
|
311
|
+
"Add a project member as a reviewer on a task. Pass projectId to target a specific project; otherwise the configured default project is used. Idempotent \u2014 adding an existing reviewer is a no-op.",
|
|
271
312
|
{
|
|
272
|
-
|
|
273
|
-
|
|
313
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
314
|
+
taskId: z2.string().describe("The task ID or slug"),
|
|
315
|
+
userId: z2.string().describe("User ID of the reviewer (use list_project_members to resolve a name or email)")
|
|
274
316
|
},
|
|
275
317
|
async (params) => {
|
|
276
318
|
const result = await conn2.addReviewer(params);
|
|
@@ -286,10 +328,11 @@ function registerReviewerTools(server2, conn2) {
|
|
|
286
328
|
);
|
|
287
329
|
server2.tool(
|
|
288
330
|
"remove_reviewer",
|
|
289
|
-
"Remove a reviewer from a task. Idempotent \u2014 removing a non-reviewer is a no-op.",
|
|
331
|
+
"Remove a reviewer from a task. Pass projectId to target a specific project; otherwise the configured default project is used. Idempotent \u2014 removing a non-reviewer is a no-op.",
|
|
290
332
|
{
|
|
291
|
-
|
|
292
|
-
|
|
333
|
+
projectId: z2.string().optional().describe("Target Conveyor project ID"),
|
|
334
|
+
taskId: z2.string().describe("The task ID or slug"),
|
|
335
|
+
userId: z2.string().describe("User ID of the reviewer to remove")
|
|
293
336
|
},
|
|
294
337
|
async (params) => {
|
|
295
338
|
const result = await conn2.removeReviewer(params);
|
|
@@ -307,6 +350,7 @@ function registerReviewerTools(server2, conn2) {
|
|
|
307
350
|
function registerTaskTools(server2, conn2) {
|
|
308
351
|
registerListTasks(server2, conn2);
|
|
309
352
|
registerGetTask(server2, conn2);
|
|
353
|
+
registerGetCardBySlug(server2, conn2);
|
|
310
354
|
registerCreateTask(server2, conn2);
|
|
311
355
|
registerUpdateTask(server2, conn2);
|
|
312
356
|
registerChatTools(server2, conn2);
|
|
@@ -318,51 +362,55 @@ function registerTaskTools(server2, conn2) {
|
|
|
318
362
|
}
|
|
319
363
|
|
|
320
364
|
// src/tools/builds.ts
|
|
321
|
-
import { z as
|
|
365
|
+
import { z as z3 } from "zod";
|
|
322
366
|
function registerBuildTools(server2, conn2) {
|
|
323
367
|
server2.tool(
|
|
324
368
|
"start_task",
|
|
325
|
-
"Start a cloud build (codespace) for a task",
|
|
369
|
+
"Start a cloud build (codespace) for a task. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
326
370
|
{
|
|
327
|
-
|
|
371
|
+
projectId: z3.string().optional().describe("Target Conveyor project ID"),
|
|
372
|
+
taskId: z3.string().describe("The task ID")
|
|
328
373
|
},
|
|
329
374
|
async (params) => {
|
|
330
|
-
const result = await conn2.startBuild(params.taskId);
|
|
375
|
+
const result = await conn2.startBuild(params.taskId, params.projectId);
|
|
331
376
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
332
377
|
}
|
|
333
378
|
);
|
|
334
379
|
server2.tool(
|
|
335
380
|
"stop_task",
|
|
336
|
-
"Stop a running cloud build for a task",
|
|
381
|
+
"Stop a running cloud build for a task. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
337
382
|
{
|
|
338
|
-
|
|
383
|
+
projectId: z3.string().optional().describe("Target Conveyor project ID"),
|
|
384
|
+
taskId: z3.string().describe("The task ID")
|
|
339
385
|
},
|
|
340
386
|
async (params) => {
|
|
341
|
-
const result = await conn2.stopBuild(params.taskId);
|
|
387
|
+
const result = await conn2.stopBuild(params.taskId, params.projectId);
|
|
342
388
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
343
389
|
}
|
|
344
390
|
);
|
|
345
391
|
server2.tool(
|
|
346
392
|
"create_release",
|
|
347
|
-
"Create a release for the project \u2014 the same flow as the Release button in the web UI. Creates a release task with a release/YYYY.MM.N branch and a PR from the dev branch to the default branch. Omit taskIds to release ALL cards currently in Review (Dev); pass a subset to cherry-pick \u2014 a cloud build agent then cherry-picks those changes and resolves conflicts. Fails if a release is already in progress or no cards are in Review (Dev).",
|
|
393
|
+
"Create a release for the project \u2014 the same flow as the Release button in the web UI. Pass projectId to target a specific project; otherwise the configured default project is used. Creates a release task with a release/YYYY.MM.N branch and a PR from the dev branch to the default branch. Omit taskIds to release ALL cards currently in Review (Dev); pass a subset to cherry-pick \u2014 a cloud build agent then cherry-picks those changes and resolves conflicts. Fails if a release is already in progress or no cards are in Review (Dev).",
|
|
348
394
|
{
|
|
349
|
-
|
|
395
|
+
projectId: z3.string().optional().describe("Target Conveyor project ID"),
|
|
396
|
+
taskIds: z3.array(z3.string()).optional().describe(
|
|
350
397
|
"Task IDs in Review (Dev) to cherry-pick into the release. Omit to release all of them."
|
|
351
398
|
)
|
|
352
399
|
},
|
|
353
400
|
async (params) => {
|
|
354
|
-
const result = await conn2.createRelease(params.taskIds);
|
|
401
|
+
const result = await conn2.createRelease(params.taskIds, params.projectId);
|
|
355
402
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
356
403
|
}
|
|
357
404
|
);
|
|
358
405
|
server2.tool(
|
|
359
406
|
"get_build_status",
|
|
360
|
-
"Check codespace and agent status for a task",
|
|
407
|
+
"Check codespace and agent status for a task. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
361
408
|
{
|
|
362
|
-
|
|
409
|
+
projectId: z3.string().optional().describe("Target Conveyor project ID"),
|
|
410
|
+
taskId: z3.string().describe("The task ID")
|
|
363
411
|
},
|
|
364
412
|
async (params) => {
|
|
365
|
-
const status = await conn2.getBuildStatus(params.taskId);
|
|
413
|
+
const status = await conn2.getBuildStatus(params.taskId, params.projectId);
|
|
366
414
|
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
|
|
367
415
|
}
|
|
368
416
|
);
|
|
@@ -371,7 +419,7 @@ function registerBuildTools(server2, conn2) {
|
|
|
371
419
|
// src/tools/attachments.ts
|
|
372
420
|
import { readFile, stat } from "fs/promises";
|
|
373
421
|
import { basename, extname } from "path";
|
|
374
|
-
import { z as
|
|
422
|
+
import { z as z4 } from "zod";
|
|
375
423
|
var MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024;
|
|
376
424
|
var MIME_BY_EXT = {
|
|
377
425
|
".png": "image/png",
|
|
@@ -406,12 +454,13 @@ function inferMimeType(filePath, override) {
|
|
|
406
454
|
function registerListTaskFiles(server2, conn2) {
|
|
407
455
|
server2.tool(
|
|
408
456
|
"list_task_files",
|
|
409
|
-
"List all files attached to a task with metadata (no contents \u2014 fast and small). Use before fetching a specific file to see what is available and how large each is. For file contents use get_attachment.",
|
|
457
|
+
"List all files attached to a task with metadata (no contents \u2014 fast and small). Pass projectId to target a specific project; otherwise the configured default project is used. Use before fetching a specific file to see what is available and how large each is. For file contents use get_attachment.",
|
|
410
458
|
{
|
|
411
|
-
|
|
459
|
+
projectId: z4.string().optional().describe("Target Conveyor project ID"),
|
|
460
|
+
taskId: z4.string().describe("The task ID or slug")
|
|
412
461
|
},
|
|
413
462
|
async (params) => {
|
|
414
|
-
const files = await conn2.listTaskFiles(params.taskId);
|
|
463
|
+
const files = await conn2.listTaskFiles(params.taskId, params.projectId);
|
|
415
464
|
return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
|
|
416
465
|
}
|
|
417
466
|
);
|
|
@@ -448,17 +497,19 @@ function buildAttachmentContent(file) {
|
|
|
448
497
|
function registerGetAttachment(server2, conn2) {
|
|
449
498
|
server2.tool(
|
|
450
499
|
"get_attachment",
|
|
451
|
-
"Fetch one task file's content plus metadata by file ID (accepts task id or slug). Images are returned as viewable image blocks. Large text files (logs, JSON) are returned in pages \u2014 use `offset`/`maxBytes` to read more, or fetch `downloadUrl` for the whole file. Call list_task_files first to discover IDs and sizes.",
|
|
500
|
+
"Fetch one task file's content plus metadata by file ID (accepts task id or slug). Pass projectId to target a specific project; otherwise the configured default project is used. Images are returned as viewable image blocks. Large text files (logs, JSON) are returned in pages \u2014 use `offset`/`maxBytes` to read more, or fetch `downloadUrl` for the whole file. Call list_task_files first to discover IDs and sizes.",
|
|
452
501
|
{
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
502
|
+
projectId: z4.string().optional().describe("Target Conveyor project ID"),
|
|
503
|
+
taskId: z4.string().describe("The task ID or slug"),
|
|
504
|
+
fileId: z4.string().describe("The file ID to fetch"),
|
|
505
|
+
offset: z4.number().int().nonnegative().optional().describe("Byte offset into text content (paging). Default 0."),
|
|
506
|
+
maxBytes: z4.number().int().positive().optional().describe("Max bytes of text content to return from offset.")
|
|
457
507
|
},
|
|
458
508
|
async (params) => {
|
|
459
509
|
const file = await conn2.getAttachment(params.taskId, params.fileId, {
|
|
460
510
|
offset: params.offset,
|
|
461
|
-
maxBytes: params.maxBytes
|
|
511
|
+
maxBytes: params.maxBytes,
|
|
512
|
+
projectId: params.projectId
|
|
462
513
|
});
|
|
463
514
|
return { content: buildAttachmentContent(file) };
|
|
464
515
|
}
|
|
@@ -467,12 +518,13 @@ function registerGetAttachment(server2, conn2) {
|
|
|
467
518
|
function registerUploadAttachment(server2, conn2) {
|
|
468
519
|
server2.tool(
|
|
469
520
|
"upload_attachment",
|
|
470
|
-
"Upload a local file as a task attachment (any file type, up to 25MB). The file appears under the task's Files. Pass `comment` to also post it to the task chat in the same step.",
|
|
521
|
+
"Upload a local file as a task attachment (any file type, up to 25MB). Pass projectId to target a specific project; otherwise the configured default project is used. The file appears under the task's Files. Pass `comment` to also post it to the task chat in the same step.",
|
|
471
522
|
{
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
523
|
+
projectId: z4.string().optional().describe("Target Conveyor project ID"),
|
|
524
|
+
taskId: z4.string().describe("The task ID or slug"),
|
|
525
|
+
path: z4.string().describe("Absolute path to the local file to upload"),
|
|
526
|
+
comment: z4.string().optional().describe("When set, also posts the attachment to the task chat with this text"),
|
|
527
|
+
mimeType: z4.string().optional().describe("Override the mime type inferred from the file extension")
|
|
476
528
|
},
|
|
477
529
|
async (params) => {
|
|
478
530
|
const info = await stat(params.path).catch(() => null);
|
|
@@ -494,7 +546,8 @@ function registerUploadAttachment(server2, conn2) {
|
|
|
494
546
|
const { fileId, uploadUrl } = await conn2.requestFileUpload(params.taskId, {
|
|
495
547
|
fileName,
|
|
496
548
|
mimeType,
|
|
497
|
-
fileSize: info.size
|
|
549
|
+
fileSize: info.size,
|
|
550
|
+
projectId: params.projectId
|
|
498
551
|
});
|
|
499
552
|
const body = await readFile(params.path);
|
|
500
553
|
const res = await fetch(uploadUrl, {
|
|
@@ -512,7 +565,12 @@ function registerUploadAttachment(server2, conn2) {
|
|
|
512
565
|
]
|
|
513
566
|
};
|
|
514
567
|
}
|
|
515
|
-
const result = await conn2.confirmFileUpload(
|
|
568
|
+
const result = await conn2.confirmFileUpload(
|
|
569
|
+
params.taskId,
|
|
570
|
+
fileId,
|
|
571
|
+
params.comment,
|
|
572
|
+
params.projectId
|
|
573
|
+
);
|
|
516
574
|
const lines = [
|
|
517
575
|
`Uploaded ${result.fileName} (${info.size} bytes, ${mimeType}). File ID: ${result.fileId}`
|
|
518
576
|
];
|
|
@@ -529,17 +587,18 @@ function registerAttachmentTools(server2, conn2) {
|
|
|
529
587
|
}
|
|
530
588
|
|
|
531
589
|
// src/tools/pull-request.ts
|
|
532
|
-
import { z as
|
|
590
|
+
import { z as z5 } from "zod";
|
|
533
591
|
function registerPullRequestTools(server2, conn2) {
|
|
534
592
|
server2.tool(
|
|
535
593
|
"create_pull_request",
|
|
536
|
-
"Open a GitHub pull request for a task's existing branch (the branch must already be pushed to origin). Moves the task to ReviewPR. Returns the PR number and URL.",
|
|
594
|
+
"Open a GitHub pull request for a task's existing branch (the branch must already be pushed to origin). Pass projectId to target a specific project; otherwise the configured default project is used. Moves the task to ReviewPR. Returns the PR number and URL.",
|
|
537
595
|
{
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
596
|
+
projectId: z5.string().optional().describe("Target Conveyor project ID"),
|
|
597
|
+
taskId: z5.string().describe("The task ID whose branch should be opened as a PR"),
|
|
598
|
+
title: z5.string().describe("Pull request title"),
|
|
599
|
+
body: z5.string().describe("Pull request body (markdown)"),
|
|
600
|
+
head: z5.string().optional().describe("Source branch for the PR (defaults to the task's branch)"),
|
|
601
|
+
base: z5.string().optional().describe("Target branch for the PR (defaults to the repo default)")
|
|
543
602
|
},
|
|
544
603
|
async (params) => {
|
|
545
604
|
const result = await conn2.createPullRequest(params);
|
|
@@ -551,7 +610,7 @@ function registerPullRequestTools(server2, conn2) {
|
|
|
551
610
|
}
|
|
552
611
|
|
|
553
612
|
// src/tools/subtasks.ts
|
|
554
|
-
import { z as
|
|
613
|
+
import { z as z6 } from "zod";
|
|
555
614
|
var STATUS_ENUM2 = [
|
|
556
615
|
"Planning",
|
|
557
616
|
"Open",
|
|
@@ -566,14 +625,15 @@ var SP_DESCRIPTION = "Story point value (1=Common, 2=Magic, 3=Rare, 5=Unique)";
|
|
|
566
625
|
function registerCreateSubtask(server2, conn2) {
|
|
567
626
|
server2.tool(
|
|
568
627
|
"create_subtask",
|
|
569
|
-
"Create a subtask under a parent task. Subtasks break a larger task into independently buildable pieces.",
|
|
628
|
+
"Create a subtask under a parent task. Pass projectId to target a specific project; otherwise the configured default project is used. Subtasks break a larger task into independently buildable pieces.",
|
|
570
629
|
{
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
630
|
+
projectId: z6.string().optional().describe("Target Conveyor project ID"),
|
|
631
|
+
parentTaskId: z6.string().describe("The parent task ID"),
|
|
632
|
+
title: z6.string().describe("Subtask title"),
|
|
633
|
+
description: z6.string().optional().describe("Subtask description"),
|
|
634
|
+
plan: z6.string().optional().describe("Subtask implementation plan (markdown)"),
|
|
635
|
+
ordinal: z6.number().optional().describe("Ordering position among siblings"),
|
|
636
|
+
storyPointValue: z6.number().optional().describe(SP_DESCRIPTION)
|
|
577
637
|
},
|
|
578
638
|
async (params) => {
|
|
579
639
|
const subtask = await conn2.createSubtask(params);
|
|
@@ -586,15 +646,16 @@ function registerCreateSubtask(server2, conn2) {
|
|
|
586
646
|
function registerUpdateSubtask(server2, conn2) {
|
|
587
647
|
server2.tool(
|
|
588
648
|
"update_subtask",
|
|
589
|
-
"Update a subtask's fields: title, description, plan, status, ordering, or story points. Moving a subtask beyond Planning auto-fills missing story points and agent assignment \u2014 don't spend turns on them.",
|
|
649
|
+
"Update a subtask's fields: title, description, plan, status, ordering, or story points. Pass projectId to target a specific project; otherwise the configured default project is used. Moving a subtask beyond Planning auto-fills missing story points and agent assignment \u2014 don't spend turns on them.",
|
|
590
650
|
{
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
651
|
+
projectId: z6.string().optional().describe("Target Conveyor project ID"),
|
|
652
|
+
subtaskId: z6.string().describe("The subtask ID"),
|
|
653
|
+
title: z6.string().optional().describe("New title"),
|
|
654
|
+
description: z6.string().optional().describe("New description"),
|
|
655
|
+
plan: z6.string().optional().describe("New plan (markdown)"),
|
|
656
|
+
status: z6.enum(STATUS_ENUM2).optional().describe("New status"),
|
|
657
|
+
ordinal: z6.number().optional().describe("New ordering position among siblings"),
|
|
658
|
+
storyPointValue: z6.number().optional().describe(SP_DESCRIPTION)
|
|
598
659
|
},
|
|
599
660
|
async (params) => {
|
|
600
661
|
const result = await conn2.updateSubtask(params);
|
|
@@ -609,12 +670,13 @@ function registerUpdateSubtask(server2, conn2) {
|
|
|
609
670
|
function registerListSubtasks(server2, conn2) {
|
|
610
671
|
server2.tool(
|
|
611
672
|
"list_subtasks",
|
|
612
|
-
"List all subtasks of a parent task with their status and ordering.",
|
|
673
|
+
"List all subtasks of a parent task with their status and ordering. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
613
674
|
{
|
|
614
|
-
|
|
675
|
+
projectId: z6.string().optional().describe("Target Conveyor project ID"),
|
|
676
|
+
taskId: z6.string().describe("The parent task ID")
|
|
615
677
|
},
|
|
616
678
|
async (params) => {
|
|
617
|
-
const subtasks = await conn2.listSubtasks(params.taskId);
|
|
679
|
+
const subtasks = await conn2.listSubtasks(params.taskId, params.projectId);
|
|
618
680
|
return { content: [{ type: "text", text: JSON.stringify(subtasks, null, 2) }] };
|
|
619
681
|
}
|
|
620
682
|
);
|
|
@@ -622,12 +684,13 @@ function registerListSubtasks(server2, conn2) {
|
|
|
622
684
|
function registerDeleteSubtask(server2, conn2) {
|
|
623
685
|
server2.tool(
|
|
624
686
|
"delete_subtask",
|
|
625
|
-
"Delete a subtask by ID. This is permanent \u2014 use update_subtask to set status to Cancelled if you only want to close it.",
|
|
687
|
+
"Delete a subtask by ID. Pass projectId to target a specific project; otherwise the configured default project is used. This is permanent \u2014 use update_subtask to set status to Cancelled if you only want to close it.",
|
|
626
688
|
{
|
|
627
|
-
|
|
689
|
+
projectId: z6.string().optional().describe("Target Conveyor project ID"),
|
|
690
|
+
subtaskId: z6.string().describe("The subtask ID to delete")
|
|
628
691
|
},
|
|
629
692
|
async (params) => {
|
|
630
|
-
const result = await conn2.deleteSubtask(params.subtaskId);
|
|
693
|
+
const result = await conn2.deleteSubtask(params.subtaskId, params.projectId);
|
|
631
694
|
return {
|
|
632
695
|
content: [
|
|
633
696
|
{ type: "text", text: result.deleted ? "Subtask deleted" : "Subtask not deleted" }
|
|
@@ -644,16 +707,17 @@ function registerSubtaskTools(server2, conn2) {
|
|
|
644
707
|
}
|
|
645
708
|
|
|
646
709
|
// src/tools/dependencies.ts
|
|
647
|
-
import { z as
|
|
710
|
+
import { z as z7 } from "zod";
|
|
648
711
|
function registerGetDependencies(server2, conn2) {
|
|
649
712
|
server2.tool(
|
|
650
713
|
"get_dependencies",
|
|
651
|
-
"Get a task's dependencies and their met/unmet status (met = merged to dev). Use to confirm blockers merged, or see why a task cannot start. For task state use get_task.",
|
|
714
|
+
"Get a task's dependencies and their met/unmet status (met = merged to dev). Pass projectId to target a specific project; otherwise the configured default project is used. Use to confirm blockers merged, or see why a task cannot start. For task state use get_task.",
|
|
652
715
|
{
|
|
653
|
-
|
|
716
|
+
projectId: z7.string().optional().describe("Target Conveyor project ID"),
|
|
717
|
+
taskId: z7.string().describe("The task ID")
|
|
654
718
|
},
|
|
655
719
|
async (params) => {
|
|
656
|
-
const deps = await conn2.getDependencies(params.taskId);
|
|
720
|
+
const deps = await conn2.getDependencies(params.taskId, params.projectId);
|
|
657
721
|
return { content: [{ type: "text", text: JSON.stringify(deps, null, 2) }] };
|
|
658
722
|
}
|
|
659
723
|
);
|
|
@@ -661,10 +725,11 @@ function registerGetDependencies(server2, conn2) {
|
|
|
661
725
|
function registerAddDependency(server2, conn2) {
|
|
662
726
|
server2.tool(
|
|
663
727
|
"add_dependency",
|
|
664
|
-
"Add a blocking dependency \u2014 this task cannot start until the named task is merged to dev.",
|
|
728
|
+
"Add a blocking dependency \u2014 this task cannot start until the named task is merged to dev. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
665
729
|
{
|
|
666
|
-
|
|
667
|
-
|
|
730
|
+
projectId: z7.string().optional().describe("Target Conveyor project ID"),
|
|
731
|
+
taskId: z7.string().describe("The task ID that will be blocked"),
|
|
732
|
+
dependsOnSlugOrId: z7.string().describe("Slug or ID of the task this one depends on")
|
|
668
733
|
},
|
|
669
734
|
async (params) => {
|
|
670
735
|
await conn2.addDependency(params);
|
|
@@ -675,10 +740,11 @@ function registerAddDependency(server2, conn2) {
|
|
|
675
740
|
function registerRemoveDependency(server2, conn2) {
|
|
676
741
|
server2.tool(
|
|
677
742
|
"remove_dependency",
|
|
678
|
-
"Remove a previously added dependency from a task. The task is no longer blocked by the named task. Returns: confirmation string.",
|
|
743
|
+
"Remove a previously added dependency from a task. Pass projectId to target a specific project; otherwise the configured default project is used. The task is no longer blocked by the named task. Returns: confirmation string.",
|
|
679
744
|
{
|
|
680
|
-
|
|
681
|
-
|
|
745
|
+
projectId: z7.string().optional().describe("Target Conveyor project ID"),
|
|
746
|
+
taskId: z7.string().describe("The task ID to unblock"),
|
|
747
|
+
dependsOnSlugOrId: z7.string().describe("Slug or ID of the dependency to remove")
|
|
682
748
|
},
|
|
683
749
|
async (params) => {
|
|
684
750
|
await conn2.removeDependency(params);
|
|
@@ -693,15 +759,16 @@ function registerDependencyTools(server2, conn2) {
|
|
|
693
759
|
}
|
|
694
760
|
|
|
695
761
|
// src/tools/suggestions.ts
|
|
696
|
-
import { z as
|
|
762
|
+
import { z as z8 } from "zod";
|
|
697
763
|
function registerSuggestionTools(server2, conn2) {
|
|
698
764
|
server2.tool(
|
|
699
765
|
"create_suggestion",
|
|
700
|
-
"Suggest a feature, improvement, rule, or idea for the project. Duplicates are deduped and your upvote is recorded.",
|
|
766
|
+
"Suggest a feature, improvement, rule, or idea for the project. Pass projectId to target a specific project; otherwise the configured default project is used. Duplicates are deduped and your upvote is recorded.",
|
|
701
767
|
{
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
768
|
+
projectId: z8.string().optional().describe("Target Conveyor project ID"),
|
|
769
|
+
title: z8.string().describe("Suggestion title"),
|
|
770
|
+
description: z8.string().optional().describe("Suggestion details (markdown)"),
|
|
771
|
+
tagNames: z8.array(z8.string()).optional().describe('Tag names to categorize the suggestion (e.g., ["agent-runner"])')
|
|
705
772
|
},
|
|
706
773
|
async (params) => {
|
|
707
774
|
const result = await conn2.createSuggestion(params);
|
|
@@ -712,16 +779,17 @@ function registerSuggestionTools(server2, conn2) {
|
|
|
712
779
|
}
|
|
713
780
|
|
|
714
781
|
// src/tools/checklists.ts
|
|
715
|
-
import { z as
|
|
782
|
+
import { z as z9 } from "zod";
|
|
716
783
|
function registerListManualTests(server2, conn2) {
|
|
717
784
|
server2.tool(
|
|
718
785
|
"list_manual_tests",
|
|
719
|
-
"List the manual test checklist items for a task. Use to see what manual verification steps have already been recorded.",
|
|
786
|
+
"List the manual test checklist items for a task. Pass projectId to target a specific project; otherwise the configured default project is used. Use to see what manual verification steps have already been recorded.",
|
|
720
787
|
{
|
|
721
|
-
|
|
788
|
+
projectId: z9.string().optional().describe("Target Conveyor project ID"),
|
|
789
|
+
taskId: z9.string().describe("The task ID or slug (the value in a card URL, /cards/<slug>)")
|
|
722
790
|
},
|
|
723
791
|
async (params) => {
|
|
724
|
-
const items = await conn2.listManualTests(params.taskId);
|
|
792
|
+
const items = await conn2.listManualTests(params.taskId, params.projectId);
|
|
725
793
|
if (items.length === 0) {
|
|
726
794
|
return { content: [{ type: "text", text: "No manual tests recorded for this task." }] };
|
|
727
795
|
}
|
|
@@ -736,13 +804,14 @@ function registerListManualTests(server2, conn2) {
|
|
|
736
804
|
function registerSetManualTests(server2, conn2) {
|
|
737
805
|
server2.tool(
|
|
738
806
|
"set_manual_tests",
|
|
739
|
-
"Add manual test steps to a task's checklist. Existing items with the same title are automatically skipped (deduplication). Use to record specific manual verification steps that reviewers should follow when testing the task's PR.",
|
|
807
|
+
"Add manual test steps to a task's checklist. Pass projectId to target a specific project; otherwise the configured default project is used. Existing items with the same title are automatically skipped (deduplication). Use to record specific manual verification steps that reviewers should follow when testing the task's PR.",
|
|
740
808
|
{
|
|
741
|
-
|
|
742
|
-
|
|
809
|
+
projectId: z9.string().optional().describe("Target Conveyor project ID"),
|
|
810
|
+
taskId: z9.string().describe("The task ID or slug"),
|
|
811
|
+
items: z9.array(z9.object({ title: z9.string().min(1).describe("A concise, actionable test step") })).min(1).describe("List of manual test steps to add")
|
|
743
812
|
},
|
|
744
813
|
async (params) => {
|
|
745
|
-
const result = await conn2.setManualTests(params.taskId, params.items);
|
|
814
|
+
const result = await conn2.setManualTests(params.taskId, params.items, params.projectId);
|
|
746
815
|
const parts = [`Created ${result.created} manual test item(s).`];
|
|
747
816
|
if (result.skipped > 0) parts.push(`Skipped ${result.skipped} duplicate(s).`);
|
|
748
817
|
return { content: [{ type: "text", text: parts.join(" ") }] };
|
|
@@ -754,6 +823,224 @@ function registerChecklistTools(server2, conn2) {
|
|
|
754
823
|
registerSetManualTests(server2, conn2);
|
|
755
824
|
}
|
|
756
825
|
|
|
826
|
+
// src/tools/workspace.ts
|
|
827
|
+
import { z as z10 } from "zod";
|
|
828
|
+
|
|
829
|
+
// src/workspace-ssh-tunnel.ts
|
|
830
|
+
import net from "net";
|
|
831
|
+
import { once } from "events";
|
|
832
|
+
import { WebSocket } from "ws";
|
|
833
|
+
function withPort(url, port) {
|
|
834
|
+
const parsed = new URL(url);
|
|
835
|
+
parsed.searchParams.set("port", String(port));
|
|
836
|
+
return parsed.toString();
|
|
837
|
+
}
|
|
838
|
+
function toBuffer(data) {
|
|
839
|
+
if (Buffer.isBuffer(data)) return data;
|
|
840
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
841
|
+
return Buffer.concat(data);
|
|
842
|
+
}
|
|
843
|
+
async function startWorkspaceSshTunnel(options) {
|
|
844
|
+
const host = options.host ?? "127.0.0.1";
|
|
845
|
+
const server2 = net.createServer((client) => {
|
|
846
|
+
let ws = null;
|
|
847
|
+
let ready = false;
|
|
848
|
+
const pending = [];
|
|
849
|
+
client.on("data", (chunk) => {
|
|
850
|
+
if (ready && ws && ws.readyState === WebSocket.OPEN) ws.send(chunk);
|
|
851
|
+
else pending.push(chunk);
|
|
852
|
+
});
|
|
853
|
+
client.on("close", () => ws?.close());
|
|
854
|
+
client.on("error", () => ws?.close());
|
|
855
|
+
options.resolveTarget().then(({ tunnelUrl, attachToken }) => {
|
|
856
|
+
const socket = new WebSocket(withPort(tunnelUrl, options.remotePort), {
|
|
857
|
+
headers: { "x-workspace-attach-token": attachToken }
|
|
858
|
+
});
|
|
859
|
+
ws = socket;
|
|
860
|
+
socket.on("message", (data) => {
|
|
861
|
+
const chunk = toBuffer(data);
|
|
862
|
+
if (!ready) {
|
|
863
|
+
const text = chunk.toString("utf8");
|
|
864
|
+
if (text === JSON.stringify({ ok: true })) {
|
|
865
|
+
ready = true;
|
|
866
|
+
for (const buffered of pending.splice(0)) socket.send(buffered);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
client.destroy(new Error(`Workspace tunnel rejected connection: ${text}`));
|
|
870
|
+
socket.close();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
client.write(chunk);
|
|
874
|
+
});
|
|
875
|
+
socket.on("close", () => client.destroy());
|
|
876
|
+
socket.on("error", (err) => client.destroy(err));
|
|
877
|
+
}).catch((err) => {
|
|
878
|
+
client.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
const ac = new AbortController();
|
|
882
|
+
server2.listen(options.preferredLocalPort ?? 0, host);
|
|
883
|
+
try {
|
|
884
|
+
await Promise.race([
|
|
885
|
+
once(server2, "listening", { signal: ac.signal }),
|
|
886
|
+
once(server2, "error", { signal: ac.signal }).then(([err]) => {
|
|
887
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
888
|
+
})
|
|
889
|
+
]);
|
|
890
|
+
} catch (err) {
|
|
891
|
+
server2.close();
|
|
892
|
+
throw new Error(
|
|
893
|
+
`Failed to bind workspace SSH tunnel: ${err instanceof Error ? err.message : String(err)}`,
|
|
894
|
+
{ cause: err }
|
|
895
|
+
);
|
|
896
|
+
} finally {
|
|
897
|
+
ac.abort();
|
|
898
|
+
}
|
|
899
|
+
const address = server2.address();
|
|
900
|
+
if (!address || typeof address === "string") {
|
|
901
|
+
server2.close();
|
|
902
|
+
throw new Error("Failed to bind workspace SSH tunnel");
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
host,
|
|
906
|
+
port: address.port,
|
|
907
|
+
close: () => new Promise((resolve, reject) => {
|
|
908
|
+
server2.close((err) => {
|
|
909
|
+
if (err) reject(err);
|
|
910
|
+
else resolve();
|
|
911
|
+
});
|
|
912
|
+
})
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/tools/workspace.ts
|
|
917
|
+
var activeTunnels = /* @__PURE__ */ new Map();
|
|
918
|
+
function createTunnelId(taskId, port) {
|
|
919
|
+
return `${taskId}:${port}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
|
|
920
|
+
}
|
|
921
|
+
function localUrlFor(port, host, localPort) {
|
|
922
|
+
if (port === 2222) return void 0;
|
|
923
|
+
return `http://${host}:${localPort}`;
|
|
924
|
+
}
|
|
925
|
+
function registerAttachInfoTool(server2, conn2) {
|
|
926
|
+
server2.tool(
|
|
927
|
+
"workspace_attach_info",
|
|
928
|
+
"Return SSH/SFTP attach metadata for a running task Claudespace, plus hosted preview URLs and configured preview ports. Optionally installs an OpenSSH public key for this attach session.",
|
|
929
|
+
{
|
|
930
|
+
taskId: z10.string().describe("The task ID"),
|
|
931
|
+
sshPublicKey: z10.string().optional().describe("Optional OpenSSH public key to install into the workspace")
|
|
932
|
+
},
|
|
933
|
+
async ({ taskId, sshPublicKey }) => {
|
|
934
|
+
const info = await conn2.getWorkspaceAttachInfo(taskId, sshPublicKey);
|
|
935
|
+
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
function registerPreviewUrlsTool(server2, conn2) {
|
|
940
|
+
server2.tool(
|
|
941
|
+
"workspace_preview_urls",
|
|
942
|
+
"Return the hosted preview URLs and preview ports for a running task Claudespace. This mirrors the web UI preview link metadata.",
|
|
943
|
+
{
|
|
944
|
+
taskId: z10.string().describe("The task ID")
|
|
945
|
+
},
|
|
946
|
+
async ({ taskId }) => {
|
|
947
|
+
const info = await conn2.getWorkspaceAttachInfo(taskId);
|
|
948
|
+
return {
|
|
949
|
+
content: [
|
|
950
|
+
{
|
|
951
|
+
type: "text",
|
|
952
|
+
text: JSON.stringify(
|
|
953
|
+
{
|
|
954
|
+
taskId: info.taskId,
|
|
955
|
+
sessionId: info.sessionId,
|
|
956
|
+
previewPorts: info.previewPorts,
|
|
957
|
+
previewUrls: info.previewUrls
|
|
958
|
+
},
|
|
959
|
+
null,
|
|
960
|
+
2
|
|
961
|
+
)
|
|
962
|
+
}
|
|
963
|
+
]
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
function registerStartTunnelTool(server2, conn2, startTunnel) {
|
|
969
|
+
server2.tool(
|
|
970
|
+
"workspace_start_tunnel",
|
|
971
|
+
"Start a local loopback tunnel through the MCP server to a running task Claudespace port. Use port 2222 for SSH/SFTP, or one of previewPorts for app access.",
|
|
972
|
+
{
|
|
973
|
+
taskId: z10.string().describe("The task ID"),
|
|
974
|
+
port: z10.number().optional().describe("Remote Claudespace port. Defaults to SSH port 2222."),
|
|
975
|
+
preferredLocalPort: z10.number().optional().describe("Preferred local loopback port. If omitted, the OS chooses one."),
|
|
976
|
+
sshPublicKey: z10.string().optional().describe("Optional OpenSSH public key to install before opening the tunnel")
|
|
977
|
+
},
|
|
978
|
+
async ({ taskId, port, preferredLocalPort, sshPublicKey }) => {
|
|
979
|
+
const info = await conn2.getWorkspaceAttachInfo(taskId, sshPublicKey);
|
|
980
|
+
const remotePort = port ?? info.ssh?.remotePort ?? 2222;
|
|
981
|
+
const handle = await startTunnel({
|
|
982
|
+
remotePort,
|
|
983
|
+
preferredLocalPort,
|
|
984
|
+
// Re-mint per connection so the tunnel outlives the attach-token TTL.
|
|
985
|
+
// The key (if any) is already installed by the call above; the refetch
|
|
986
|
+
// only needs a fresh token, so it omits sshPublicKey.
|
|
987
|
+
resolveTarget: async () => {
|
|
988
|
+
const fresh = await conn2.getWorkspaceAttachInfo(taskId);
|
|
989
|
+
return { tunnelUrl: fresh.tunnelUrl, attachToken: fresh.attachToken };
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
const tunnelId = createTunnelId(taskId, remotePort);
|
|
993
|
+
activeTunnels.set(tunnelId, handle);
|
|
994
|
+
return {
|
|
995
|
+
content: [
|
|
996
|
+
{
|
|
997
|
+
type: "text",
|
|
998
|
+
text: JSON.stringify(
|
|
999
|
+
{
|
|
1000
|
+
tunnelId,
|
|
1001
|
+
taskId,
|
|
1002
|
+
remotePort,
|
|
1003
|
+
localHost: handle.host,
|
|
1004
|
+
localPort: handle.port,
|
|
1005
|
+
localUrl: localUrlFor(remotePort, handle.host, handle.port),
|
|
1006
|
+
sshCommand: remotePort === 2222 ? `ssh -p ${handle.port} ${info.ssh?.username ?? "conveyor"}@${handle.host}` : void 0,
|
|
1007
|
+
sftpCommand: remotePort === 2222 ? `sftp -P ${handle.port} ${info.ssh?.username ?? "conveyor"}@${handle.host}` : void 0,
|
|
1008
|
+
previewPorts: info.previewPorts,
|
|
1009
|
+
previewUrls: info.previewUrls,
|
|
1010
|
+
expiresAt: info.expiresAt
|
|
1011
|
+
},
|
|
1012
|
+
null,
|
|
1013
|
+
2
|
|
1014
|
+
)
|
|
1015
|
+
}
|
|
1016
|
+
]
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
function registerStopTunnelTool(server2) {
|
|
1022
|
+
server2.tool(
|
|
1023
|
+
"workspace_stop_tunnel",
|
|
1024
|
+
"Stop a local workspace tunnel previously opened by workspace_start_tunnel.",
|
|
1025
|
+
{
|
|
1026
|
+
tunnelId: z10.string().describe("Tunnel id returned by workspace_start_tunnel")
|
|
1027
|
+
},
|
|
1028
|
+
async ({ tunnelId }) => {
|
|
1029
|
+
const tunnel = activeTunnels.get(tunnelId);
|
|
1030
|
+
if (!tunnel) throw new Error("Workspace tunnel not found");
|
|
1031
|
+
await tunnel.close();
|
|
1032
|
+
activeTunnels.delete(tunnelId);
|
|
1033
|
+
return { content: [{ type: "text", text: "Workspace tunnel stopped" }] };
|
|
1034
|
+
}
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
function registerWorkspaceTools(server2, conn2, deps = {}) {
|
|
1038
|
+
registerAttachInfoTool(server2, conn2);
|
|
1039
|
+
registerPreviewUrlsTool(server2, conn2);
|
|
1040
|
+
registerStartTunnelTool(server2, conn2, deps.startTunnel ?? startWorkspaceSshTunnel);
|
|
1041
|
+
registerStopTunnelTool(server2);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
757
1044
|
// src/tools/index.ts
|
|
758
1045
|
function registerAllTools(server2, conn2) {
|
|
759
1046
|
registerProjectTools(server2, conn2);
|
|
@@ -765,16 +1052,17 @@ function registerAllTools(server2, conn2) {
|
|
|
765
1052
|
registerDependencyTools(server2, conn2);
|
|
766
1053
|
registerSuggestionTools(server2, conn2);
|
|
767
1054
|
registerChecklistTools(server2, conn2);
|
|
1055
|
+
registerWorkspaceTools(server2, conn2);
|
|
768
1056
|
}
|
|
769
1057
|
|
|
770
1058
|
// src/cli.ts
|
|
771
1059
|
var { version } = createRequire(import.meta.url)("../package.json");
|
|
772
1060
|
var apiUrl = process.env.CONVEYOR_API_URL;
|
|
773
|
-
var projectToken = process.env.CONVEYOR_PROJECT_TOKEN;
|
|
1061
|
+
var projectToken = process.env.CONVEYOR_USER_TOKEN ?? process.env.CONVEYOR_PROJECT_TOKEN;
|
|
774
1062
|
var projectId = process.env.CONVEYOR_PROJECT_ID;
|
|
775
|
-
if (!apiUrl || !projectToken
|
|
1063
|
+
if (!apiUrl || !projectToken) {
|
|
776
1064
|
process.stderr.write(
|
|
777
|
-
"Error: CONVEYOR_API_URL
|
|
1065
|
+
"Error: CONVEYOR_API_URL and CONVEYOR_USER_TOKEN or CONVEYOR_PROJECT_TOKEN environment variables are required. CONVEYOR_PROJECT_ID is optional and sets the default project.\n"
|
|
778
1066
|
);
|
|
779
1067
|
process.exit(1);
|
|
780
1068
|
}
|