kanban-lite 1.2.13 → 1.2.16
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 +226 -2
- package/dist/extension.js +143 -1
- package/dist/mcp-server.js +140 -1
- package/dist/sdk/index.cjs +91 -1
- package/dist/sdk/index.mjs +91 -1
- package/dist/sdk/sdk/KanbanSDK.d.ts +38 -0
- package/dist/sdk/sdk/modules/comments.d.ts +33 -0
- package/dist/sdk/shared/types.d.ts +21 -0
- package/dist/standalone-webview/index.js +14 -14
- package/dist/standalone-webview/index.js.map +1 -1
- package/dist/standalone-webview/style.css +1 -1
- package/dist/standalone.js +143 -1
- package/package.json +1 -1
- package/src/cli/index.ts +33 -1
- package/src/mcp-server/index.ts +52 -0
- package/src/sdk/KanbanSDK.ts +51 -1
- package/src/sdk/modules/comments.ts +91 -0
- package/src/shared/types.ts +9 -0
- package/src/standalone/broadcastService.ts +40 -0
- package/src/standalone/internal/routes/tasks.ts +45 -1
- package/src/webview/App.tsx +31 -0
- package/src/webview/assets/main.css +46 -0
- package/src/webview/components/CardEditor.tsx +1 -1
- package/src/webview/components/CommentsSection.tsx +28 -20
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
|
-
|
|
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
|
-
|
|
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 {
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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.",
|