kanban-lite 1.2.13 → 1.2.15

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/cli.js CHANGED
@@ -25092,6 +25092,56 @@ async function updateComment(ctx, { cardId, commentId, content, boardId }) {
25092
25092
  });
25093
25093
  return card;
25094
25094
  }
25095
+ async function streamComment(ctx, {
25096
+ cardId,
25097
+ author,
25098
+ boardId,
25099
+ stream,
25100
+ onStart,
25101
+ onChunk
25102
+ }) {
25103
+ if (!author?.trim())
25104
+ throw new Error("Comment author cannot be empty");
25105
+ const card = await ctx.getCard(cardId, boardId);
25106
+ if (!card)
25107
+ throw new Error(`Card not found: ${cardId}`);
25108
+ if (!card.comments)
25109
+ card.comments = [];
25110
+ const maxId = card.comments.reduce((max, c) => {
25111
+ const num = parseInt(c.id.replace("c", ""), 10);
25112
+ return Number.isNaN(num) ? max : Math.max(max, num);
25113
+ }, 0);
25114
+ const commentId = `c${maxId + 1}`;
25115
+ const created = (/* @__PURE__ */ new Date()).toISOString();
25116
+ onStart?.(commentId, author, created);
25117
+ let accumulated = "";
25118
+ for await (const chunk of stream) {
25119
+ accumulated += chunk;
25120
+ onChunk?.(commentId, chunk);
25121
+ }
25122
+ const comment = {
25123
+ id: commentId,
25124
+ author,
25125
+ created,
25126
+ content: accumulated
25127
+ };
25128
+ card.comments.push(comment);
25129
+ card.modified = (/* @__PURE__ */ new Date()).toISOString();
25130
+ await ctx._storage.writeCard(card);
25131
+ await appendActivityLog(ctx, {
25132
+ cardId: card.id,
25133
+ boardId: card.boardId || ctx._resolveBoardId(boardId),
25134
+ eventType: "comment.created",
25135
+ text: `Comment added by \`${author}\` (streamed)`,
25136
+ metadata: {
25137
+ commentId: comment.id,
25138
+ author,
25139
+ created: comment.created
25140
+ }
25141
+ }).catch(() => {
25142
+ });
25143
+ return card;
25144
+ }
25095
25145
  async function deleteComment(ctx, { cardId, commentId, boardId }) {
25096
25146
  const card = await ctx.getCard(cardId, boardId);
25097
25147
  if (!card)
@@ -27203,7 +27253,47 @@ var init_KanbanSDK = __esm({
27203
27253
  this._runAfterEvent("comment.deleted", { ...deletedComment, cardId: mergedInput.cardId }, void 0, card.boardId ?? this._resolveBoardId(mergedInput.boardId));
27204
27254
  return card;
27205
27255
  }
27206
- // --- Log management ---
27256
+ /**
27257
+ * Creates a comment on a card from a streaming text source, persisting it
27258
+ * once the stream is exhausted.
27259
+ *
27260
+ * This method is the streaming counterpart to {@link addComment}. It is
27261
+ * intended for use by AI agents that generate comment text incrementally
27262
+ * (e.g. an LLM `textStream`). The caller may supply `onStart` and `onChunk`
27263
+ * callbacks to fan live progress out to connected WebSocket viewers without
27264
+ * requiring intermediate disk writes.
27265
+ *
27266
+ * @param cardId - The ID of the card to comment on.
27267
+ * @param author - Display name of the streaming author.
27268
+ * @param stream - An `AsyncIterable<string>` that yields text chunks.
27269
+ * @param options.boardId - Optional board ID override.
27270
+ * @param options.onStart - Called once before iteration with the allocated
27271
+ * comment ID, author, and ISO timestamp.
27272
+ * @param options.onChunk - Called after each chunk with the comment ID and
27273
+ * the raw chunk string.
27274
+ * @returns A promise resolving to the updated {@link Card} once the stream
27275
+ * has been fully consumed and the comment has been persisted.
27276
+ * @throws {Error} If the card is not found.
27277
+ * @throws {Error} If `author` is empty.
27278
+ *
27279
+ * @example
27280
+ * ```ts
27281
+ * // Stream an AI SDK textStream as a comment
27282
+ * const { textStream } = await streamText({ model, prompt })
27283
+ * const card = await sdk.streamComment('42', 'ai-agent', textStream, {
27284
+ * onStart: (id, author, created) => broadcast({ type: 'commentStreamStart', cardId: '42', commentId: id, author, created }),
27285
+ * onChunk: (id, chunk) => broadcast({ type: 'commentChunk', cardId: '42', commentId: id, chunk }),
27286
+ * })
27287
+ * ```
27288
+ */
27289
+ async streamComment(cardId, author, stream, options) {
27290
+ const { boardId, onStart, onChunk } = options ?? {};
27291
+ const card = await streamComment(this, { cardId, author, boardId, stream, onStart, onChunk });
27292
+ const newComment = card.comments?.[card.comments.length - 1];
27293
+ if (newComment)
27294
+ this._runAfterEvent("comment.created", { ...newComment, cardId }, void 0, card.boardId ?? this._resolveBoardId(boardId));
27295
+ return card;
27296
+ }
27207
27297
  /**
27208
27298
  * Returns the absolute path to the log file for a card.
27209
27299
  *
@@ -43803,6 +43893,55 @@ async function main() {
43803
43893
  };
43804
43894
  }
43805
43895
  );
43896
+ server.tool(
43897
+ "stream_comment",
43898
+ "Add a comment to a kanban card from a streaming text source. Provide the full content string; it will be written via the streaming path so connected webview clients see it arrive incrementally.",
43899
+ {
43900
+ boardId: external_exports.string().optional().describe("Board ID (uses default board if omitted)"),
43901
+ cardId: external_exports.string().describe("Card ID (or partial ID)"),
43902
+ author: external_exports.string().describe("Comment author name"),
43903
+ content: external_exports.string().describe("Full comment text (supports markdown). The content is streamed word-by-word to connected viewers.")
43904
+ },
43905
+ async ({ boardId, cardId, author, content }) => {
43906
+ let resolvedId = cardId;
43907
+ const card = await sdk.getCard(cardId, boardId);
43908
+ if (!card) {
43909
+ const all = await sdk.listCards(void 0, boardId);
43910
+ const matches = all.filter((c) => c.id.includes(cardId));
43911
+ if (matches.length === 1) {
43912
+ resolvedId = matches[0].id;
43913
+ } else if (matches.length > 1) {
43914
+ return {
43915
+ content: [{ type: "text", text: `Multiple cards match "${cardId}": ${matches.map((m) => m.id).join(", ")}` }],
43916
+ isError: true
43917
+ };
43918
+ } else {
43919
+ return {
43920
+ content: [{ type: "text", text: `Card not found: ${cardId}` }],
43921
+ isError: true
43922
+ };
43923
+ }
43924
+ }
43925
+ async function* singleChunk() {
43926
+ yield content;
43927
+ }
43928
+ try {
43929
+ const updated = await runWithMcpAuth(() => sdk.streamComment(resolvedId, author, singleChunk(), { boardId }));
43930
+ const added = updated.comments?.[updated.comments.length - 1];
43931
+ return {
43932
+ content: [{
43933
+ type: "text",
43934
+ text: JSON.stringify(added, null, 2)
43935
+ }]
43936
+ };
43937
+ } catch (err) {
43938
+ return {
43939
+ content: [{ type: "text", text: String(err) }],
43940
+ isError: true
43941
+ };
43942
+ }
43943
+ }
43944
+ );
43806
43945
  server.tool(
43807
43946
  "update_comment",
43808
43947
  "Update the content of a comment on a kanban card.",
@@ -94976,6 +95115,15 @@ async function broadcastLogsUpdatedToEditingClients(ctx, cardId, logs) {
94976
95115
  client.send(json2);
94977
95116
  }
94978
95117
  }
95118
+ function broadcastCommentStreamStart(ctx, cardId, commentId, author, created) {
95119
+ broadcast(ctx, { type: "commentStreamStart", cardId, commentId, author, created });
95120
+ }
95121
+ function broadcastCommentChunk(ctx, cardId, commentId, chunk) {
95122
+ broadcast(ctx, { type: "commentChunk", cardId, commentId, chunk });
95123
+ }
95124
+ function broadcastCommentStreamDone(ctx, cardId, commentId) {
95125
+ broadcast(ctx, { type: "commentStreamDone", cardId, commentId });
95126
+ }
94979
95127
  var init_broadcastService = __esm({
94980
95128
  "src/standalone/broadcastService.ts"() {
94981
95129
  "use strict";
@@ -97002,6 +97150,49 @@ async function handleTaskRoutes(request) {
97002
97150
  }
97003
97151
  return true;
97004
97152
  }
97153
+ params = route("POST", "/api/tasks/:id/comments/stream");
97154
+ if (params) {
97155
+ const { id } = params;
97156
+ const author = (url.searchParams.get("author") ?? req.headers["x-comment-author"] ?? "").trim();
97157
+ if (!author) {
97158
+ jsonError(res, 400, "author query param is required");
97159
+ return true;
97160
+ }
97161
+ let commentId;
97162
+ try {
97163
+ async function* requestTextStream() {
97164
+ const decoder = new TextDecoder("utf-8");
97165
+ for await (const chunk of req) {
97166
+ yield decoder.decode(chunk, { stream: true });
97167
+ }
97168
+ }
97169
+ const card = await runWithRequestAuth(
97170
+ () => ctx.sdk.streamComment(id, author, requestTextStream(), {
97171
+ boardId: url.searchParams.get("boardId") ?? void 0,
97172
+ onStart: (cid, commentAuthor, created) => {
97173
+ commentId = cid;
97174
+ broadcastCommentStreamStart(ctx, id, cid, commentAuthor, created);
97175
+ },
97176
+ onChunk: (cid, chunk) => {
97177
+ broadcastCommentChunk(ctx, id, cid, chunk);
97178
+ }
97179
+ })
97180
+ );
97181
+ if (!card) {
97182
+ jsonError(res, 404, "Task not found");
97183
+ return true;
97184
+ }
97185
+ await loadCards(ctx);
97186
+ broadcast(ctx, buildInitMessage(ctx));
97187
+ if (commentId)
97188
+ broadcastCommentStreamDone(ctx, id, commentId);
97189
+ const comment = card.comments?.find((c) => c.id === commentId);
97190
+ jsonOk(res, comment ?? null, 201);
97191
+ } catch (err) {
97192
+ handleKnownError(err);
97193
+ }
97194
+ return true;
97195
+ }
97005
97196
  params = route("PUT", "/api/tasks/:id/comments/:commentId");
97006
97197
  if (params) {
97007
97198
  try {
@@ -99265,7 +99456,7 @@ async function resolveCardId(sdk, cardId, boardId) {
99265
99456
  async function cmdComment(sdk, positional, flags) {
99266
99457
  const subcommand = positional[0] || "list";
99267
99458
  const boardId = getBoardId(flags);
99268
- if (subcommand !== "list" && subcommand !== "add" && subcommand !== "edit" && subcommand !== "remove" && subcommand !== "rm") {
99459
+ if (subcommand !== "list" && subcommand !== "add" && subcommand !== "edit" && subcommand !== "remove" && subcommand !== "rm" && subcommand !== "stream") {
99269
99460
  const resolvedId = await resolveCardId(sdk, subcommand, boardId);
99270
99461
  const comments = await sdk.listComments(resolvedId, boardId);
99271
99462
  if (flags.json) {
@@ -99370,6 +99561,38 @@ async function cmdComment(sdk, positional, flags) {
99370
99561
  console.log(green(`Deleted comment ${commentId}`));
99371
99562
  break;
99372
99563
  }
99564
+ case "stream": {
99565
+ if (!cardId) {
99566
+ console.error(red("Usage: kl comment stream <card-id> --author <name>"));
99567
+ process.exit(1);
99568
+ }
99569
+ const author = typeof flags.author === "string" ? flags.author : "";
99570
+ if (!author) {
99571
+ console.error(red("Error: --author is required"));
99572
+ process.exit(1);
99573
+ }
99574
+ const resolvedId = await resolveCardId(sdk, cardId, boardId);
99575
+ async function* stdinStream() {
99576
+ process.stdin.setEncoding("utf8");
99577
+ for await (const chunk of process.stdin) {
99578
+ if (!flags.json)
99579
+ process.stderr.write(".");
99580
+ yield chunk;
99581
+ }
99582
+ }
99583
+ if (!flags.json)
99584
+ process.stderr.write("Streaming comment");
99585
+ const card = await sdk.streamComment(resolvedId, author, stdinStream(), { boardId });
99586
+ if (!flags.json)
99587
+ process.stderr.write("\n");
99588
+ const added = card.comments?.[card.comments.length - 1];
99589
+ if (flags.json) {
99590
+ console.log(JSON.stringify(added, null, 2));
99591
+ } else {
99592
+ console.log(green(`Streamed comment ${added?.id ?? "?"} to card ${resolvedId}`));
99593
+ }
99594
+ break;
99595
+ }
99373
99596
  }
99374
99597
  }
99375
99598
  async function cmdLog(sdk, positional, flags) {
@@ -99895,6 +100118,7 @@ ${bold("Attachment Commands:")}
99895
100118
  ${bold("Comment Commands:")}
99896
100119
  comment <id> List comments on a card
99897
100120
  comment add <id> Add a comment (--author, --body)
100121
+ comment stream <id> Stream a comment from stdin (--author)
99898
100122
  comment edit <id> <cid> Edit a comment (--body)
99899
100123
  comment remove <id> <cid> Remove a comment
99900
100124
 
package/dist/extension.js CHANGED
@@ -72588,6 +72588,56 @@ async function updateComment(ctx, { cardId, commentId, content, boardId }) {
72588
72588
  });
72589
72589
  return card;
72590
72590
  }
72591
+ async function streamComment(ctx, {
72592
+ cardId,
72593
+ author,
72594
+ boardId,
72595
+ stream,
72596
+ onStart,
72597
+ onChunk
72598
+ }) {
72599
+ if (!author?.trim())
72600
+ throw new Error("Comment author cannot be empty");
72601
+ const card = await ctx.getCard(cardId, boardId);
72602
+ if (!card)
72603
+ throw new Error(`Card not found: ${cardId}`);
72604
+ if (!card.comments)
72605
+ card.comments = [];
72606
+ const maxId = card.comments.reduce((max, c) => {
72607
+ const num = parseInt(c.id.replace("c", ""), 10);
72608
+ return Number.isNaN(num) ? max : Math.max(max, num);
72609
+ }, 0);
72610
+ const commentId = `c${maxId + 1}`;
72611
+ const created = (/* @__PURE__ */ new Date()).toISOString();
72612
+ onStart?.(commentId, author, created);
72613
+ let accumulated = "";
72614
+ for await (const chunk of stream) {
72615
+ accumulated += chunk;
72616
+ onChunk?.(commentId, chunk);
72617
+ }
72618
+ const comment = {
72619
+ id: commentId,
72620
+ author,
72621
+ created,
72622
+ content: accumulated
72623
+ };
72624
+ card.comments.push(comment);
72625
+ card.modified = (/* @__PURE__ */ new Date()).toISOString();
72626
+ await ctx._storage.writeCard(card);
72627
+ await appendActivityLog(ctx, {
72628
+ cardId: card.id,
72629
+ boardId: card.boardId || ctx._resolveBoardId(boardId),
72630
+ eventType: "comment.created",
72631
+ text: `Comment added by \`${author}\` (streamed)`,
72632
+ metadata: {
72633
+ commentId: comment.id,
72634
+ author,
72635
+ created: comment.created
72636
+ }
72637
+ }).catch(() => {
72638
+ });
72639
+ return card;
72640
+ }
72591
72641
  async function deleteComment(ctx, { cardId, commentId, boardId }) {
72592
72642
  const card = await ctx.getCard(cardId, boardId);
72593
72643
  if (!card)
@@ -74649,7 +74699,47 @@ var KanbanSDK = class _KanbanSDK {
74649
74699
  this._runAfterEvent("comment.deleted", { ...deletedComment, cardId: mergedInput.cardId }, void 0, card.boardId ?? this._resolveBoardId(mergedInput.boardId));
74650
74700
  return card;
74651
74701
  }
74652
- // --- Log management ---
74702
+ /**
74703
+ * Creates a comment on a card from a streaming text source, persisting it
74704
+ * once the stream is exhausted.
74705
+ *
74706
+ * This method is the streaming counterpart to {@link addComment}. It is
74707
+ * intended for use by AI agents that generate comment text incrementally
74708
+ * (e.g. an LLM `textStream`). The caller may supply `onStart` and `onChunk`
74709
+ * callbacks to fan live progress out to connected WebSocket viewers without
74710
+ * requiring intermediate disk writes.
74711
+ *
74712
+ * @param cardId - The ID of the card to comment on.
74713
+ * @param author - Display name of the streaming author.
74714
+ * @param stream - An `AsyncIterable<string>` that yields text chunks.
74715
+ * @param options.boardId - Optional board ID override.
74716
+ * @param options.onStart - Called once before iteration with the allocated
74717
+ * comment ID, author, and ISO timestamp.
74718
+ * @param options.onChunk - Called after each chunk with the comment ID and
74719
+ * the raw chunk string.
74720
+ * @returns A promise resolving to the updated {@link Card} once the stream
74721
+ * has been fully consumed and the comment has been persisted.
74722
+ * @throws {Error} If the card is not found.
74723
+ * @throws {Error} If `author` is empty.
74724
+ *
74725
+ * @example
74726
+ * ```ts
74727
+ * // Stream an AI SDK textStream as a comment
74728
+ * const { textStream } = await streamText({ model, prompt })
74729
+ * const card = await sdk.streamComment('42', 'ai-agent', textStream, {
74730
+ * onStart: (id, author, created) => broadcast({ type: 'commentStreamStart', cardId: '42', commentId: id, author, created }),
74731
+ * onChunk: (id, chunk) => broadcast({ type: 'commentChunk', cardId: '42', commentId: id, chunk }),
74732
+ * })
74733
+ * ```
74734
+ */
74735
+ async streamComment(cardId, author, stream, options) {
74736
+ const { boardId, onStart, onChunk } = options ?? {};
74737
+ const card = await streamComment(this, { cardId, author, boardId, stream, onStart, onChunk });
74738
+ const newComment = card.comments?.[card.comments.length - 1];
74739
+ if (newComment)
74740
+ this._runAfterEvent("comment.created", { ...newComment, cardId }, void 0, card.boardId ?? this._resolveBoardId(boardId));
74741
+ return card;
74742
+ }
74653
74743
  /**
74654
74744
  * Returns the absolute path to the log file for a card.
74655
74745
  *
@@ -78341,6 +78431,15 @@ async function broadcastLogsUpdatedToEditingClients(ctx, cardId, logs) {
78341
78431
  client.send(json2);
78342
78432
  }
78343
78433
  }
78434
+ function broadcastCommentStreamStart(ctx, cardId, commentId, author, created) {
78435
+ broadcast(ctx, { type: "commentStreamStart", cardId, commentId, author, created });
78436
+ }
78437
+ function broadcastCommentChunk(ctx, cardId, commentId, chunk) {
78438
+ broadcast(ctx, { type: "commentChunk", cardId, commentId, chunk });
78439
+ }
78440
+ function broadcastCommentStreamDone(ctx, cardId, commentId) {
78441
+ broadcast(ctx, { type: "commentStreamDone", cardId, commentId });
78442
+ }
78344
78443
 
78345
78444
  // src/standalone/watcherSetup.ts
78346
78445
  var fs10 = __toESM(require("fs"));
@@ -80282,6 +80381,49 @@ async function handleTaskRoutes(request) {
80282
80381
  }
80283
80382
  return true;
80284
80383
  }
80384
+ params = route("POST", "/api/tasks/:id/comments/stream");
80385
+ if (params) {
80386
+ const { id } = params;
80387
+ const author = (url.searchParams.get("author") ?? req.headers["x-comment-author"] ?? "").trim();
80388
+ if (!author) {
80389
+ jsonError(res, 400, "author query param is required");
80390
+ return true;
80391
+ }
80392
+ let commentId;
80393
+ try {
80394
+ async function* requestTextStream() {
80395
+ const decoder = new TextDecoder("utf-8");
80396
+ for await (const chunk of req) {
80397
+ yield decoder.decode(chunk, { stream: true });
80398
+ }
80399
+ }
80400
+ const card = await runWithRequestAuth(
80401
+ () => ctx.sdk.streamComment(id, author, requestTextStream(), {
80402
+ boardId: url.searchParams.get("boardId") ?? void 0,
80403
+ onStart: (cid, commentAuthor, created) => {
80404
+ commentId = cid;
80405
+ broadcastCommentStreamStart(ctx, id, cid, commentAuthor, created);
80406
+ },
80407
+ onChunk: (cid, chunk) => {
80408
+ broadcastCommentChunk(ctx, id, cid, chunk);
80409
+ }
80410
+ })
80411
+ );
80412
+ if (!card) {
80413
+ jsonError(res, 404, "Task not found");
80414
+ return true;
80415
+ }
80416
+ await loadCards(ctx);
80417
+ broadcast(ctx, buildInitMessage(ctx));
80418
+ if (commentId)
80419
+ broadcastCommentStreamDone(ctx, id, commentId);
80420
+ const comment = card.comments?.find((c) => c.id === commentId);
80421
+ jsonOk(res, comment ?? null, 201);
80422
+ } catch (err) {
80423
+ handleKnownError(err);
80424
+ }
80425
+ return true;
80426
+ }
80285
80427
  params = route("PUT", "/api/tasks/:id/comments/:commentId");
80286
80428
  if (params) {
80287
80429
  try {
@@ -24986,6 +24986,56 @@ async function updateComment(ctx, { cardId, commentId, content, boardId }) {
24986
24986
  });
24987
24987
  return card;
24988
24988
  }
24989
+ async function streamComment(ctx, {
24990
+ cardId,
24991
+ author,
24992
+ boardId,
24993
+ stream,
24994
+ onStart,
24995
+ onChunk
24996
+ }) {
24997
+ if (!author?.trim())
24998
+ throw new Error("Comment author cannot be empty");
24999
+ const card = await ctx.getCard(cardId, boardId);
25000
+ if (!card)
25001
+ throw new Error(`Card not found: ${cardId}`);
25002
+ if (!card.comments)
25003
+ card.comments = [];
25004
+ const maxId = card.comments.reduce((max, c) => {
25005
+ const num = parseInt(c.id.replace("c", ""), 10);
25006
+ return Number.isNaN(num) ? max : Math.max(max, num);
25007
+ }, 0);
25008
+ const commentId = `c${maxId + 1}`;
25009
+ const created = (/* @__PURE__ */ new Date()).toISOString();
25010
+ onStart?.(commentId, author, created);
25011
+ let accumulated = "";
25012
+ for await (const chunk of stream) {
25013
+ accumulated += chunk;
25014
+ onChunk?.(commentId, chunk);
25015
+ }
25016
+ const comment = {
25017
+ id: commentId,
25018
+ author,
25019
+ created,
25020
+ content: accumulated
25021
+ };
25022
+ card.comments.push(comment);
25023
+ card.modified = (/* @__PURE__ */ new Date()).toISOString();
25024
+ await ctx._storage.writeCard(card);
25025
+ await appendActivityLog(ctx, {
25026
+ cardId: card.id,
25027
+ boardId: card.boardId || ctx._resolveBoardId(boardId),
25028
+ eventType: "comment.created",
25029
+ text: `Comment added by \`${author}\` (streamed)`,
25030
+ metadata: {
25031
+ commentId: comment.id,
25032
+ author,
25033
+ created: comment.created
25034
+ }
25035
+ }).catch(() => {
25036
+ });
25037
+ return card;
25038
+ }
24989
25039
  async function deleteComment(ctx, { cardId, commentId, boardId }) {
24990
25040
  const card = await ctx.getCard(cardId, boardId);
24991
25041
  if (!card)
@@ -27047,7 +27097,47 @@ var KanbanSDK = class _KanbanSDK {
27047
27097
  this._runAfterEvent("comment.deleted", { ...deletedComment, cardId: mergedInput.cardId }, void 0, card.boardId ?? this._resolveBoardId(mergedInput.boardId));
27048
27098
  return card;
27049
27099
  }
27050
- // --- Log management ---
27100
+ /**
27101
+ * Creates a comment on a card from a streaming text source, persisting it
27102
+ * once the stream is exhausted.
27103
+ *
27104
+ * This method is the streaming counterpart to {@link addComment}. It is
27105
+ * intended for use by AI agents that generate comment text incrementally
27106
+ * (e.g. an LLM `textStream`). The caller may supply `onStart` and `onChunk`
27107
+ * callbacks to fan live progress out to connected WebSocket viewers without
27108
+ * requiring intermediate disk writes.
27109
+ *
27110
+ * @param cardId - The ID of the card to comment on.
27111
+ * @param author - Display name of the streaming author.
27112
+ * @param stream - An `AsyncIterable<string>` that yields text chunks.
27113
+ * @param options.boardId - Optional board ID override.
27114
+ * @param options.onStart - Called once before iteration with the allocated
27115
+ * comment ID, author, and ISO timestamp.
27116
+ * @param options.onChunk - Called after each chunk with the comment ID and
27117
+ * the raw chunk string.
27118
+ * @returns A promise resolving to the updated {@link Card} once the stream
27119
+ * has been fully consumed and the comment has been persisted.
27120
+ * @throws {Error} If the card is not found.
27121
+ * @throws {Error} If `author` is empty.
27122
+ *
27123
+ * @example
27124
+ * ```ts
27125
+ * // Stream an AI SDK textStream as a comment
27126
+ * const { textStream } = await streamText({ model, prompt })
27127
+ * const card = await sdk.streamComment('42', 'ai-agent', textStream, {
27128
+ * onStart: (id, author, created) => broadcast({ type: 'commentStreamStart', cardId: '42', commentId: id, author, created }),
27129
+ * onChunk: (id, chunk) => broadcast({ type: 'commentChunk', cardId: '42', commentId: id, chunk }),
27130
+ * })
27131
+ * ```
27132
+ */
27133
+ async streamComment(cardId, author, stream, options) {
27134
+ const { boardId, onStart, onChunk } = options ?? {};
27135
+ const card = await streamComment(this, { cardId, author, boardId, stream, onStart, onChunk });
27136
+ const newComment = card.comments?.[card.comments.length - 1];
27137
+ if (newComment)
27138
+ this._runAfterEvent("comment.created", { ...newComment, cardId }, void 0, card.boardId ?? this._resolveBoardId(boardId));
27139
+ return card;
27140
+ }
27051
27141
  /**
27052
27142
  * Returns the absolute path to the log file for a card.
27053
27143
  *
@@ -28564,6 +28654,55 @@ async function main() {
28564
28654
  };
28565
28655
  }
28566
28656
  );
28657
+ server.tool(
28658
+ "stream_comment",
28659
+ "Add a comment to a kanban card from a streaming text source. Provide the full content string; it will be written via the streaming path so connected webview clients see it arrive incrementally.",
28660
+ {
28661
+ boardId: import_zod.z.string().optional().describe("Board ID (uses default board if omitted)"),
28662
+ cardId: import_zod.z.string().describe("Card ID (or partial ID)"),
28663
+ author: import_zod.z.string().describe("Comment author name"),
28664
+ content: import_zod.z.string().describe("Full comment text (supports markdown). The content is streamed word-by-word to connected viewers.")
28665
+ },
28666
+ async ({ boardId, cardId, author, content }) => {
28667
+ let resolvedId = cardId;
28668
+ const card = await sdk.getCard(cardId, boardId);
28669
+ if (!card) {
28670
+ const all = await sdk.listCards(void 0, boardId);
28671
+ const matches = all.filter((c) => c.id.includes(cardId));
28672
+ if (matches.length === 1) {
28673
+ resolvedId = matches[0].id;
28674
+ } else if (matches.length > 1) {
28675
+ return {
28676
+ content: [{ type: "text", text: `Multiple cards match "${cardId}": ${matches.map((m) => m.id).join(", ")}` }],
28677
+ isError: true
28678
+ };
28679
+ } else {
28680
+ return {
28681
+ content: [{ type: "text", text: `Card not found: ${cardId}` }],
28682
+ isError: true
28683
+ };
28684
+ }
28685
+ }
28686
+ async function* singleChunk() {
28687
+ yield content;
28688
+ }
28689
+ try {
28690
+ const updated = await runWithMcpAuth(() => sdk.streamComment(resolvedId, author, singleChunk(), { boardId }));
28691
+ const added = updated.comments?.[updated.comments.length - 1];
28692
+ return {
28693
+ content: [{
28694
+ type: "text",
28695
+ text: JSON.stringify(added, null, 2)
28696
+ }]
28697
+ };
28698
+ } catch (err) {
28699
+ return {
28700
+ content: [{ type: "text", text: String(err) }],
28701
+ isError: true
28702
+ };
28703
+ }
28704
+ }
28705
+ );
28567
28706
  server.tool(
28568
28707
  "update_comment",
28569
28708
  "Update the content of a comment on a kanban card.",