@schoolai/shipyard-mcp 0.4.1 → 0.4.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.
@@ -2,9 +2,11 @@
2
2
  import {
3
3
  GitHubAuthError,
4
4
  GitHubPRResponseSchema,
5
+ OriginPlatformValues,
5
6
  PLAN_INDEX_DOC_NAME,
6
7
  PlanStatusValues,
7
8
  TOOL_NAMES,
9
+ ThreadCommentSchema,
8
10
  YDOC_KEYS,
9
11
  addArtifact,
10
12
  addDeliverable,
@@ -15,19 +17,24 @@ import {
15
17
  createPlanUrlWithHistory,
16
18
  createPlanWebUrl,
17
19
  createUserResolver,
20
+ detectPlatform,
18
21
  extractDeliverables,
19
22
  extractTextFromCommentBody,
20
23
  formatDeliverablesForLLM,
21
24
  formatDiffCommentsForLLM,
22
25
  generateSessionToken,
23
26
  getArtifacts,
27
+ getClientInfo,
24
28
  getDeliverables,
29
+ getDisplayName,
25
30
  getGitHubUsername,
26
31
  getLinkedPRs,
27
32
  getLocalChanges,
33
+ getLocalDiffCommentById,
28
34
  getLocalDiffComments,
29
35
  getOctokit,
30
36
  getOrCreateDoc,
37
+ getPRReviewCommentById,
31
38
  getPRReviewComments,
32
39
  getPlanEvents,
33
40
  getPlanMetadata,
@@ -46,23 +53,29 @@ import {
46
53
  linkArtifactToDeliverable,
47
54
  linkPR,
48
55
  logPlanEvent,
56
+ parseCommentId,
49
57
  parseRepoString,
58
+ parseThreadId,
50
59
  parseThreads,
51
- registryConfig,
52
60
  releaseHubLock,
61
+ replyToLocalDiffComment,
62
+ replyToPRReviewComment,
53
63
  resetPlanToDraft,
64
+ setClientInfo,
54
65
  setPlanIndexEntry,
55
66
  setPlanMetadata,
56
67
  startRegistryServer,
68
+ toPlainObject,
57
69
  touchPlanIndexEntry,
58
70
  transitionPlanStatus,
59
71
  tryAcquireHubLock,
60
72
  uploadArtifact,
61
73
  webConfig
62
- } from "./chunk-6EWQC5VA.js";
74
+ } from "./chunk-6TW62VVK.js";
63
75
  import {
64
- logger
65
- } from "./chunk-HFZCBGQ3.js";
76
+ logger,
77
+ registryConfig
78
+ } from "./chunk-DLDKEWOB.js";
66
79
 
67
80
  // src/index.ts
68
81
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -81,7 +94,7 @@ import * as os from "os";
81
94
  import * as path from "path";
82
95
  import * as vm from "vm";
83
96
  import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";
84
- import { z as z13 } from "zod";
97
+ import { z as z15 } from "zod";
85
98
 
86
99
  // src/tools/add-artifact.ts
87
100
  import { writeFile as writeFile2 } from "fs/promises";
@@ -110,7 +123,6 @@ import { nanoid } from "nanoid";
110
123
 
111
124
  // src/local-artifacts.ts
112
125
  import { mkdir, readFile, rm, writeFile } from "fs/promises";
113
- import { homedir } from "os";
114
126
  import { join, resolve, sep } from "path";
115
127
  async function storeLocalArtifact(planId, filename, buffer) {
116
128
  const planDir = join(ARTIFACTS_DIR, planId);
@@ -138,7 +150,7 @@ async function deleteLocalArtifact(artifactId) {
138
150
  return false;
139
151
  }
140
152
  }
141
- var ARTIFACTS_DIR = join(homedir(), ".shipyard", "artifacts");
153
+ var ARTIFACTS_DIR = join(registryConfig.SHIPYARD_STATE_DIR, "artifacts");
142
154
 
143
155
  // src/tools/artifact-helpers.ts
144
156
  async function resolveArtifactContent(input) {
@@ -706,10 +718,9 @@ async function handleAutoComplete(doc, metadata, deliverables, actorName, taskId
706
718
  actorName,
707
719
  snapshotMessage: "Task completed - all deliverables fulfilled"
708
720
  });
709
- const { homedir: homedir2 } = await import("os");
710
721
  const { join: join3 } = await import("path");
711
722
  const { mkdir: mkdir2 } = await import("fs/promises");
712
- const snapshotsDir = join3(homedir2(), ".shipyard", "snapshots");
723
+ const snapshotsDir = join3(registryConfig.SHIPYARD_STATE_DIR, "snapshots");
713
724
  await mkdir2(snapshotsDir, { recursive: true });
714
725
  const snapshotFile = join3(snapshotsDir, `${taskId}.txt`);
715
726
  await writeFile2(snapshotFile, result.snapshotUrl, "utf-8");
@@ -958,7 +969,7 @@ import { ServerBlockNoteEditor as ServerBlockNoteEditor3 } from "@blocknote/serv
958
969
  import { nanoid as nanoid2 } from "nanoid";
959
970
  import open from "open";
960
971
  import { z as z4 } from "zod";
961
- var OriginPlatformEnum = z4.enum(["devin", "cursor", "windsurf", "aider", "unknown"]);
972
+ var OriginPlatformEnum = z4.enum(OriginPlatformValues);
962
973
  var CreateTaskInput = z4.object({
963
974
  title: z4.string().describe("Task title"),
964
975
  content: z4.string().describe("Task content (markdown)"),
@@ -986,8 +997,15 @@ function buildOriginMetadata(platform, sessionId, metadata) {
986
997
  generationId: typeof generationId === "string" ? generationId : void 0
987
998
  };
988
999
  }
989
- case "windsurf":
990
1000
  case "aider":
1001
+ case "browser":
1002
+ case "claude-code":
1003
+ case "cline":
1004
+ case "codex":
1005
+ case "continue":
1006
+ case "vscode":
1007
+ case "windsurf":
1008
+ case "zed":
991
1009
  case "unknown":
992
1010
  return { platform: "unknown", cwd: process.cwd() };
993
1011
  default: {
@@ -1064,7 +1082,7 @@ Bad deliverables (not provable):
1064
1082
  prNumber: { type: "number", description: "PR number. Required for artifact uploads." },
1065
1083
  originPlatform: {
1066
1084
  type: "string",
1067
- enum: ["devin", "cursor", "windsurf", "aider", "unknown"],
1085
+ enum: [...OriginPlatformValues],
1068
1086
  description: "Platform where this plan originated. Used for conversation export/import."
1069
1087
  },
1070
1088
  originSessionId: {
@@ -1688,11 +1706,10 @@ function formatThread(thread, number, selectedTextMaxLength, resolveUser) {
1688
1706
  const bodyText = extractTextFromCommentBody(comment.body);
1689
1707
  const authorName = resolveUser ? resolveUser(comment.userId) : comment.userId.slice(0, 8);
1690
1708
  if (idx === 0) {
1691
- output += `> **${authorName}:** ${bodyText}
1709
+ output += `[thread:${thread.id}] **${authorName}:** ${bodyText}
1692
1710
  `;
1693
1711
  } else {
1694
- output += `>
1695
- > **${authorName} (Reply):** ${bodyText}
1712
+ output += `[comment:${comment.id}] **${authorName} (Reply):** ${bodyText}
1696
1713
  `;
1697
1714
  }
1698
1715
  });
@@ -2076,11 +2093,307 @@ Use this token for add_artifact, read_task, link_pr, and other task operations.`
2076
2093
  }
2077
2094
  };
2078
2095
 
2079
- // src/tools/setup-review-notification.ts
2096
+ // src/tools/reply-to-diff-comment.ts
2080
2097
  import { z as z10 } from "zod";
2081
- var SetupReviewNotificationInput = z10.object({
2082
- taskId: z10.string().describe("Task ID to monitor"),
2083
- pollIntervalSeconds: z10.number().optional().default(30).describe("Polling interval in seconds (default: 30)")
2098
+ var ReplyToDiffCommentInput = z10.object({
2099
+ taskId: z10.string().describe("Plan ID"),
2100
+ sessionToken: z10.string().describe("Session token from create_plan"),
2101
+ commentId: z10.string().describe("Comment ID (PR or local diff comment)"),
2102
+ body: z10.string().describe("Reply text")
2103
+ });
2104
+ var replyToDiffCommentTool = {
2105
+ definition: {
2106
+ name: TOOL_NAMES.REPLY_TO_DIFF_COMMENT,
2107
+ description: `Reply to a PR review comment or local diff comment.
2108
+
2109
+ Allows AI to respond to code review feedback.
2110
+ Replies appear inline in the Changes tab with the original comment.
2111
+
2112
+ USAGE:
2113
+ - commentId: Comment ID from readDiffComments output (format: [pr:abc123] or [local:abc123])
2114
+ - body: Reply text (markdown supported)
2115
+
2116
+ The tool automatically detects whether the comment is a PR or local diff comment.
2117
+
2118
+ EXAMPLE:
2119
+ reply_to_diff_comment({
2120
+ taskId: "abc123",
2121
+ sessionToken: "token",
2122
+ commentId: "pr:xyz789",
2123
+ body: "Good point! I'll add that validation in the next commit."
2124
+ })`,
2125
+ inputSchema: {
2126
+ type: "object",
2127
+ properties: {
2128
+ taskId: { type: "string", description: "Plan ID" },
2129
+ sessionToken: { type: "string", description: "Session token from create_plan" },
2130
+ commentId: { type: "string", description: "Comment ID (PR or local)" },
2131
+ body: { type: "string", description: "Reply text" }
2132
+ },
2133
+ required: ["taskId", "sessionToken", "commentId", "body"]
2134
+ }
2135
+ },
2136
+ /**
2137
+ * Complexity of 20 is acceptable here because the function handles:
2138
+ * - Input validation and session verification
2139
+ * - Comment ID parsing with type detection
2140
+ * - Two distinct comment types (PR vs local) with separate error handling
2141
+ * - Success/error responses for each code path
2142
+ * Extracting helpers would fragment the error handling context.
2143
+ */
2144
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Tool handler needs unified error handling for two comment types
2145
+ handler: async (args) => {
2146
+ const input = ReplyToDiffCommentInput.parse(args);
2147
+ logger.info({ taskId: input.taskId, commentId: input.commentId }, "Replying to diff comment");
2148
+ const ydoc = await getOrCreateDoc(input.taskId);
2149
+ const metadata = getPlanMetadata(ydoc);
2150
+ if (!metadata) {
2151
+ return {
2152
+ content: [{ type: "text", text: `Plan "${input.taskId}" not found.` }],
2153
+ isError: true
2154
+ };
2155
+ }
2156
+ if (!metadata.sessionTokenHash || !verifySessionToken(input.sessionToken, metadata.sessionTokenHash)) {
2157
+ return {
2158
+ content: [{ type: "text", text: `Invalid session token for plan "${input.taskId}".` }],
2159
+ isError: true
2160
+ };
2161
+ }
2162
+ const actorName = await getGitHubUsername();
2163
+ const clientInfoName = getClientInfo();
2164
+ const { platform } = detectPlatform(clientInfoName);
2165
+ const agentDisplayName = getDisplayName(platform, actorName);
2166
+ const parsed = parseCommentId(input.commentId);
2167
+ logger.debug(
2168
+ { inputCommentId: input.commentId, parsedType: parsed.type, parsedId: parsed.id },
2169
+ "Parsed comment ID from input"
2170
+ );
2171
+ const prComment = parsed.type === "pr" || parsed.type === "unknown" ? getPRReviewCommentById(ydoc, parsed.id) : null;
2172
+ if (prComment) {
2173
+ try {
2174
+ const reply = replyToPRReviewComment(
2175
+ ydoc,
2176
+ parsed.id,
2177
+ input.body,
2178
+ agentDisplayName,
2179
+ actorName
2180
+ );
2181
+ logPlanEvent(ydoc, "comment_added", actorName, {
2182
+ commentId: reply.id,
2183
+ prNumber: prComment.prNumber
2184
+ });
2185
+ logger.info(
2186
+ { taskId: input.taskId, commentId: reply.id, parentCommentId: parsed.id },
2187
+ "PR review comment reply added"
2188
+ );
2189
+ return {
2190
+ content: [
2191
+ {
2192
+ type: "text",
2193
+ text: `Reply added to PR review comment!
2194
+
2195
+ Comment ID: ${reply.id}
2196
+ Parent Comment ID: ${parsed.id}
2197
+ PR: #${prComment.prNumber}
2198
+ File: ${prComment.path}:${prComment.line}
2199
+
2200
+ The reply will appear in the Changes tab inline with the original comment.`
2201
+ }
2202
+ ]
2203
+ };
2204
+ } catch (error) {
2205
+ logger.error({ error, commentId: parsed.id }, "Failed to reply to PR comment");
2206
+ const errorMessage = error instanceof Error ? error.message : String(error);
2207
+ return {
2208
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
2209
+ isError: true
2210
+ };
2211
+ }
2212
+ }
2213
+ const localComment = parsed.type === "local" || parsed.type === "unknown" ? getLocalDiffCommentById(ydoc, parsed.id) : null;
2214
+ if (localComment) {
2215
+ try {
2216
+ const reply = replyToLocalDiffComment(
2217
+ ydoc,
2218
+ parsed.id,
2219
+ input.body,
2220
+ agentDisplayName,
2221
+ actorName
2222
+ );
2223
+ logPlanEvent(ydoc, "comment_added", actorName, {
2224
+ commentId: reply.id
2225
+ });
2226
+ logger.info(
2227
+ { taskId: input.taskId, commentId: reply.id, parentCommentId: parsed.id },
2228
+ "Local diff comment reply added"
2229
+ );
2230
+ return {
2231
+ content: [
2232
+ {
2233
+ type: "text",
2234
+ text: `Reply added to local diff comment!
2235
+
2236
+ Comment ID: ${reply.id}
2237
+ Parent Comment ID: ${parsed.id}
2238
+ File: ${localComment.path}:${localComment.line}
2239
+
2240
+ The reply will appear in the Changes tab inline with the original comment.`
2241
+ }
2242
+ ]
2243
+ };
2244
+ } catch (error) {
2245
+ logger.error({ error, commentId: parsed.id }, "Failed to reply to local comment");
2246
+ const errorMessage = error instanceof Error ? error.message : String(error);
2247
+ return {
2248
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
2249
+ isError: true
2250
+ };
2251
+ }
2252
+ }
2253
+ return {
2254
+ content: [
2255
+ {
2256
+ type: "text",
2257
+ text: `Comment "${parsed.id}" not found in plan "${input.taskId}".${input.commentId !== parsed.id ? ` (parsed from input: "${input.commentId}")` : ""} Make sure the comment ID is correct and the comment exists.`
2258
+ }
2259
+ ],
2260
+ isError: true
2261
+ };
2262
+ }
2263
+ };
2264
+
2265
+ // src/tools/reply-to-thread-comment.ts
2266
+ import { nanoid as nanoid3 } from "nanoid";
2267
+ import { z as z11 } from "zod";
2268
+ var ReplyToThreadCommentInput = z11.object({
2269
+ taskId: z11.string().describe("Plan ID"),
2270
+ sessionToken: z11.string().describe("Session token from create_plan"),
2271
+ threadId: z11.string().describe("Thread ID to reply to"),
2272
+ body: z11.string().describe("Reply text")
2273
+ });
2274
+ var replyToThreadCommentTool = {
2275
+ definition: {
2276
+ name: TOOL_NAMES.REPLY_TO_THREAD_COMMENT,
2277
+ description: `Reply to a BlockNote inline thread comment.
2278
+
2279
+ Allows AI to respond to reviewer feedback on specific blocks.
2280
+ Replies appear in the comment thread sidebar.
2281
+
2282
+ USAGE:
2283
+ - threadId: Thread ID from read_plan output (format: [thread:abc123])
2284
+ - body: Reply text (plain text or BlockNote structured content)
2285
+
2286
+ EXAMPLE:
2287
+ reply_to_thread_comment({
2288
+ taskId: "abc123",
2289
+ sessionToken: "token",
2290
+ threadId: "thread-xyz",
2291
+ body: "Good point! I'll add that validation."
2292
+ })`,
2293
+ inputSchema: {
2294
+ type: "object",
2295
+ properties: {
2296
+ taskId: { type: "string", description: "Plan ID" },
2297
+ sessionToken: { type: "string", description: "Session token from create_plan" },
2298
+ threadId: { type: "string", description: "Thread ID to reply to" },
2299
+ body: { type: "string", description: "Reply text" }
2300
+ },
2301
+ required: ["taskId", "sessionToken", "threadId", "body"]
2302
+ }
2303
+ },
2304
+ handler: async (args) => {
2305
+ const input = ReplyToThreadCommentInput.parse(args);
2306
+ logger.info({ taskId: input.taskId, threadId: input.threadId }, "Replying to thread comment");
2307
+ const ydoc = await getOrCreateDoc(input.taskId);
2308
+ const metadata = getPlanMetadata(ydoc);
2309
+ if (!metadata) {
2310
+ return {
2311
+ content: [{ type: "text", text: `Plan "${input.taskId}" not found.` }],
2312
+ isError: true
2313
+ };
2314
+ }
2315
+ if (!metadata.sessionTokenHash || !verifySessionToken(input.sessionToken, metadata.sessionTokenHash)) {
2316
+ return {
2317
+ content: [{ type: "text", text: `Invalid session token for plan "${input.taskId}".` }],
2318
+ isError: true
2319
+ };
2320
+ }
2321
+ const actorName = await getGitHubUsername();
2322
+ const clientInfoName = getClientInfo();
2323
+ const { platform } = detectPlatform(clientInfoName);
2324
+ const agentDisplayName = getDisplayName(platform, actorName);
2325
+ const parsedThreadId = parseThreadId(input.threadId);
2326
+ logger.debug({ inputThreadId: input.threadId, parsedThreadId }, "Parsed thread ID from input");
2327
+ const reply = ThreadCommentSchema.parse({
2328
+ id: nanoid3(),
2329
+ userId: agentDisplayName,
2330
+ body: input.body,
2331
+ createdAt: Date.now()
2332
+ });
2333
+ let updateSucceeded = false;
2334
+ ydoc.transact(
2335
+ () => {
2336
+ const threadsMap = ydoc.getMap(YDOC_KEYS.THREADS);
2337
+ const threadDataRaw = threadsMap.get(parsedThreadId);
2338
+ if (!threadDataRaw || typeof threadDataRaw !== "object") {
2339
+ return;
2340
+ }
2341
+ const threadData = toPlainObject(threadDataRaw);
2342
+ if (!threadData) {
2343
+ return;
2344
+ }
2345
+ if (!("comments" in threadData)) {
2346
+ return;
2347
+ }
2348
+ const existingComments = Array.isArray(threadData.comments) ? threadData.comments : [];
2349
+ const updatedThread = {
2350
+ ...threadData,
2351
+ comments: [...existingComments, reply]
2352
+ };
2353
+ threadsMap.set(parsedThreadId, updatedThread);
2354
+ logPlanEvent(ydoc, "comment_added", actorName, {
2355
+ commentId: reply.id
2356
+ });
2357
+ updateSucceeded = true;
2358
+ },
2359
+ actorName ? { actor: actorName } : void 0
2360
+ );
2361
+ if (!updateSucceeded) {
2362
+ return {
2363
+ content: [
2364
+ {
2365
+ type: "text",
2366
+ text: `Thread "${parsedThreadId}" not found in plan "${input.taskId}".${input.threadId !== parsedThreadId ? ` (parsed from input: "${input.threadId}")` : ""}`
2367
+ }
2368
+ ],
2369
+ isError: true
2370
+ };
2371
+ }
2372
+ logger.info(
2373
+ { taskId: input.taskId, threadId: parsedThreadId, commentId: reply.id },
2374
+ "Thread reply added"
2375
+ );
2376
+ return {
2377
+ content: [
2378
+ {
2379
+ type: "text",
2380
+ text: `Reply added to thread!
2381
+
2382
+ Comment ID: ${reply.id}
2383
+ Thread ID: ${parsedThreadId}
2384
+
2385
+ The reply will appear in the comments panel when viewing this plan.`
2386
+ }
2387
+ ]
2388
+ };
2389
+ }
2390
+ };
2391
+
2392
+ // src/tools/setup-review-notification.ts
2393
+ import { z as z12 } from "zod";
2394
+ var SetupReviewNotificationInput = z12.object({
2395
+ taskId: z12.string().describe("Task ID to monitor"),
2396
+ pollIntervalSeconds: z12.number().optional().default(30).describe("Polling interval in seconds (default: 30)")
2084
2397
  });
2085
2398
  var setupReviewNotificationTool = {
2086
2399
  definition: {
@@ -2198,31 +2511,31 @@ The script:
2198
2511
 
2199
2512
  // src/tools/update-block-content.ts
2200
2513
  import { ServerBlockNoteEditor as ServerBlockNoteEditor5 } from "@blocknote/server-util";
2201
- import { z as z11 } from "zod";
2202
- var BlockOperationSchema = z11.discriminatedUnion("type", [
2203
- z11.object({
2204
- type: z11.literal("update"),
2205
- blockId: z11.string().describe("The block ID to update (from read_plan output)"),
2206
- content: z11.string().describe("New markdown content for this block")
2514
+ import { z as z13 } from "zod";
2515
+ var BlockOperationSchema = z13.discriminatedUnion("type", [
2516
+ z13.object({
2517
+ type: z13.literal("update"),
2518
+ blockId: z13.string().describe("The block ID to update (from read_plan output)"),
2519
+ content: z13.string().describe("New markdown content for this block")
2207
2520
  }),
2208
- z11.object({
2209
- type: z11.literal("insert"),
2210
- afterBlockId: z11.string().nullable().describe("Insert after this block ID (null = insert at beginning)"),
2211
- content: z11.string().describe("Markdown content to insert as new block(s)")
2521
+ z13.object({
2522
+ type: z13.literal("insert"),
2523
+ afterBlockId: z13.string().nullable().describe("Insert after this block ID (null = insert at beginning)"),
2524
+ content: z13.string().describe("Markdown content to insert as new block(s)")
2212
2525
  }),
2213
- z11.object({
2214
- type: z11.literal("delete"),
2215
- blockId: z11.string().describe("The block ID to delete")
2526
+ z13.object({
2527
+ type: z13.literal("delete"),
2528
+ blockId: z13.string().describe("The block ID to delete")
2216
2529
  }),
2217
- z11.object({
2218
- type: z11.literal("replace_all"),
2219
- content: z11.string().describe("Complete markdown content to replace the entire plan")
2530
+ z13.object({
2531
+ type: z13.literal("replace_all"),
2532
+ content: z13.string().describe("Complete markdown content to replace the entire plan")
2220
2533
  })
2221
2534
  ]);
2222
- var UpdateBlockContentInput = z11.object({
2223
- taskId: z11.string().describe("The task ID to modify"),
2224
- sessionToken: z11.string().describe("Session token from create_task"),
2225
- operations: z11.array(BlockOperationSchema).min(1).describe("Array of operations to perform atomically")
2535
+ var UpdateBlockContentInput = z13.object({
2536
+ taskId: z13.string().describe("The task ID to modify"),
2537
+ sessionToken: z13.string().describe("Session token from create_task"),
2538
+ operations: z13.array(BlockOperationSchema).min(1).describe("Array of operations to perform atomically")
2226
2539
  });
2227
2540
  var updateBlockContentTool = {
2228
2541
  definition: {
@@ -2483,15 +2796,15 @@ async function applyOperation(blocks, operation, editor) {
2483
2796
 
2484
2797
  // src/tools/update-task.ts
2485
2798
  import { ServerBlockNoteEditor as ServerBlockNoteEditor6 } from "@blocknote/server-util";
2486
- import { nanoid as nanoid3 } from "nanoid";
2487
- import { z as z12 } from "zod";
2799
+ import { nanoid as nanoid4 } from "nanoid";
2800
+ import { z as z14 } from "zod";
2488
2801
  function buildStatusTransition(targetStatus, actorName) {
2489
2802
  const now = Date.now();
2490
2803
  switch (targetStatus) {
2491
2804
  case "pending_review":
2492
2805
  return {
2493
2806
  status: "pending_review",
2494
- reviewRequestId: nanoid3()
2807
+ reviewRequestId: nanoid4()
2495
2808
  };
2496
2809
  case "changes_requested":
2497
2810
  return {
@@ -2517,12 +2830,12 @@ function buildStatusTransition(targetStatus, actorName) {
2517
2830
  return null;
2518
2831
  }
2519
2832
  }
2520
- var UpdateTaskInput = z12.object({
2521
- taskId: z12.string().describe("The task ID to update"),
2522
- sessionToken: z12.string().describe("Session token from create_task"),
2523
- title: z12.string().optional().describe("New title"),
2524
- status: z12.enum(["draft", "pending_review", "changes_requested", "in_progress", "completed"]).optional().describe("New status"),
2525
- tags: z12.array(z12.string()).optional().describe("Updated tags (replaces existing tags)")
2833
+ var UpdateTaskInput = z14.object({
2834
+ taskId: z14.string().describe("The task ID to update"),
2835
+ sessionToken: z14.string().describe("Session token from create_task"),
2836
+ title: z14.string().optional().describe("New title"),
2837
+ status: z14.enum(["draft", "pending_review", "changes_requested", "in_progress", "completed"]).optional().describe("New status"),
2838
+ tags: z14.array(z14.string()).optional().describe("Updated tags (replaces existing tags)")
2526
2839
  });
2527
2840
  var updateTaskTool = {
2528
2841
  definition: {
@@ -2685,8 +2998,8 @@ function getToolResultText(result) {
2685
2998
  return typeof text === "string" ? text : "";
2686
2999
  }
2687
3000
  async function serializeError(error) {
2688
- const { z: z14 } = await import("zod");
2689
- if (error instanceof z14.ZodError) {
3001
+ const { z: z16 } = await import("zod");
3002
+ if (error instanceof z16.ZodError) {
2690
3003
  const formattedIssues = error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
2691
3004
  const message2 = `Validation error:
2692
3005
  ${formattedIssues}`;
@@ -2720,7 +3033,7 @@ ${formattedIssues}`;
2720
3033
  }
2721
3034
  var BUNDLED_DOCS = `Execute TypeScript code that calls Shipyard APIs. Use this for multi-step workflows to reduce round-trips.
2722
3035
 
2723
- \u26A0\uFE0F IMPORTANT LIMITATION: Dynamic imports (\`await import()\`) are NOT supported in the VM execution context. Use only the pre-provided functions in the execution environment (createTask, readTask, readDiffComments, updateTask, addArtifact, completeTask, updateBlockContent, linkPR, requestUserInput, regenerateSessionToken, postUpdate). All necessary APIs are already available in the sandbox.
3036
+ \u26A0\uFE0F IMPORTANT LIMITATION: Dynamic imports (\`await import()\`) are NOT supported in the VM execution context. Use only the pre-provided functions in the execution environment (createTask, readTask, readDiffComments, updateTask, addArtifact, completeTask, updateBlockContent, linkPR, replyToThreadComment, replyToDiffComment, requestUserInput, regenerateSessionToken, postUpdate). All necessary APIs are already available in the sandbox.
2724
3037
 
2725
3038
  ## Available APIs
2726
3039
 
@@ -2909,6 +3222,56 @@ console.log('Linked:', pr.title, pr.status);
2909
3222
 
2910
3223
  ---
2911
3224
 
3225
+ ### replyToThreadComment(opts): Promise<string>
3226
+ Reply to a BlockNote inline thread comment.
3227
+
3228
+ Parameters:
3229
+ - taskId (string): Task ID
3230
+ - sessionToken (string): Session token
3231
+ - threadId (string): Thread ID from readTask output (format: [thread:abc123])
3232
+ - body (string): Reply text
3233
+
3234
+ Returns: Success message with comment ID and thread ID
3235
+
3236
+ Example:
3237
+ \`\`\`typescript
3238
+ const result = await replyToThreadComment({
3239
+ taskId,
3240
+ sessionToken,
3241
+ threadId: 'thread-xyz789',
3242
+ body: 'Good point! I\\'ll add that validation.'
3243
+ });
3244
+ console.log(result); // "Reply added to thread!..."
3245
+ \`\`\`
3246
+
3247
+ ---
3248
+
3249
+ ### replyToDiffComment(opts): Promise<string>
3250
+ Reply to a PR review comment or local diff comment.
3251
+
3252
+ Parameters:
3253
+ - taskId (string): Task ID
3254
+ - sessionToken (string): Session token
3255
+ - commentId (string): Comment ID from readDiffComments (format: [pr:abc123] or [local:abc123])
3256
+ - body (string): Reply text
3257
+
3258
+ Returns: Success message with reply comment ID and parent comment ID
3259
+
3260
+ Automatically detects whether the comment is a PR or local diff comment.
3261
+
3262
+ Example:
3263
+ \`\`\`typescript
3264
+ const result = await replyToDiffComment({
3265
+ taskId,
3266
+ sessionToken,
3267
+ commentId: 'pr:xyz789',
3268
+ body: 'Good catch! I\\'ll fix that in the next commit.'
3269
+ });
3270
+ console.log(result); // "Reply added to PR review comment!..."
3271
+ \`\`\`
3272
+
3273
+ ---
3274
+
2912
3275
  ### requestUserInput(opts): Promise<{ success, response?, status, reason? }>
2913
3276
  Request input from the user via browser modal.
2914
3277
 
@@ -3250,8 +3613,8 @@ const result = await addArtifact({
3250
3613
  return { taskId: task.taskId, snapshotUrl: result.snapshotUrl };
3251
3614
  \`\`\`
3252
3615
  `;
3253
- var ExecuteCodeInput = z13.object({
3254
- code: z13.string().describe("TypeScript code to execute")
3616
+ var ExecuteCodeInput = z15.object({
3617
+ code: z15.string().describe("TypeScript code to execute")
3255
3618
  });
3256
3619
  var scriptTracker = [];
3257
3620
  async function createTask(opts) {
@@ -3394,8 +3757,16 @@ async function setupReviewNotification(taskId, pollIntervalSeconds) {
3394
3757
  const script = scriptMatch?.[1] || "";
3395
3758
  return { script, fullResponse: text };
3396
3759
  }
3760
+ async function replyToThreadComment(opts) {
3761
+ const result = await replyToThreadCommentTool.handler(opts);
3762
+ return getToolResultText(result);
3763
+ }
3764
+ async function replyToDiffComment(opts) {
3765
+ const result = await replyToDiffCommentTool.handler(opts);
3766
+ return getToolResultText(result);
3767
+ }
3397
3768
  async function requestUserInput(opts) {
3398
- const { InputRequestManager } = await import("./input-request-manager-QT4IVCLS.js");
3769
+ const { InputRequestManager } = await import("./input-request-manager-FK4REBEZ.js");
3399
3770
  const ydoc = await getOrCreateDoc(PLAN_INDEX_DOC_NAME);
3400
3771
  const manager = new InputRequestManager();
3401
3772
  if ("questions" in opts && opts.questions) {
@@ -3614,6 +3985,8 @@ var executeCodeTool = {
3614
3985
  completeTask,
3615
3986
  updateBlockContent,
3616
3987
  linkPR: linkPR2,
3988
+ replyToThreadComment,
3989
+ replyToDiffComment,
3617
3990
  requestUserInput,
3618
3991
  regenerateSessionToken,
3619
3992
  postUpdate,
@@ -3722,6 +4095,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3722
4095
  tools: [executeCodeTool.definition]
3723
4096
  }));
3724
4097
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
4098
+ const clientInfo = server.getClientVersion();
4099
+ if (clientInfo?.name) {
4100
+ setClientInfo(clientInfo.name);
4101
+ logger.info(
4102
+ { clientName: clientInfo.name, clientVersion: clientInfo.version },
4103
+ "MCP client info captured from tool call"
4104
+ );
4105
+ }
3725
4106
  const { name, arguments: args } = request.params;
3726
4107
  if (name === TOOL_NAMES.EXECUTE_CODE) {
3727
4108
  return await executeCodeTool.handler(args ?? {});
@@ -5,10 +5,10 @@ import {
5
5
  createMultiQuestionInputRequest,
6
6
  getOrCreateDoc,
7
7
  logPlanEvent
8
- } from "./chunk-6EWQC5VA.js";
8
+ } from "./chunk-6TW62VVK.js";
9
9
  import {
10
10
  logger
11
- } from "./chunk-HFZCBGQ3.js";
11
+ } from "./chunk-DLDKEWOB.js";
12
12
 
13
13
  // src/services/input-request-manager.ts
14
14
  function formatDuration(totalSeconds) {