@omnicoreos/planka-mcp 0.2.0 → 0.3.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/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +26 -0
- package/.claude-plugin/plugin.json +45 -0
- package/.codex-plugin/mcp.json +16 -0
- package/.codex-plugin/plugin.json +27 -0
- package/.mcp.json +17 -0
- package/CHANGELOG.md +510 -0
- package/README.es.md +294 -55
- package/README.md +293 -55
- package/dist/cli/init.d.ts +101 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +481 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/client.d.ts +32 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +106 -32
- package/dist/client.js.map +1 -1
- package/dist/config/policy.d.ts +82 -0
- package/dist/config/policy.d.ts.map +1 -0
- package/dist/config/policy.js +226 -0
- package/dist/config/policy.js.map +1 -0
- package/dist/errors.d.ts +5 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +62 -5
- package/dist/errors.js.map +1 -1
- package/dist/identity.generated.d.ts +2 -1
- package/dist/identity.generated.d.ts.map +1 -1
- package/dist/identity.generated.js +2 -1
- package/dist/identity.generated.js.map +1 -1
- package/dist/index.js +85 -10
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +21 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +37 -0
- package/dist/instructions.js.map +1 -0
- package/dist/operations/actions.d.ts +654 -0
- package/dist/operations/actions.d.ts.map +1 -0
- package/dist/operations/actions.js +154 -0
- package/dist/operations/actions.js.map +1 -0
- package/dist/operations/archive.d.ts +28 -0
- package/dist/operations/archive.d.ts.map +1 -0
- package/dist/operations/archive.js +74 -0
- package/dist/operations/archive.js.map +1 -0
- package/dist/operations/attachments.d.ts +1 -1
- package/dist/operations/attachments.d.ts.map +1 -1
- package/dist/operations/attachments.js +3 -1
- package/dist/operations/attachments.js.map +1 -1
- package/dist/operations/board-id.d.ts +1 -1
- package/dist/operations/board-id.d.ts.map +1 -1
- package/dist/operations/board-id.js +13 -7
- package/dist/operations/board-id.js.map +1 -1
- package/dist/operations/boards.d.ts +96 -19
- package/dist/operations/boards.d.ts.map +1 -1
- package/dist/operations/boards.js +377 -93
- package/dist/operations/boards.js.map +1 -1
- package/dist/operations/card-brief.d.ts +91 -0
- package/dist/operations/card-brief.d.ts.map +1 -0
- package/dist/operations/card-brief.js +79 -0
- package/dist/operations/card-brief.js.map +1 -0
- package/dist/operations/cards.d.ts +34 -9
- package/dist/operations/cards.d.ts.map +1 -1
- package/dist/operations/cards.js +60 -14
- package/dist/operations/cards.js.map +1 -1
- package/dist/operations/comments.d.ts +61 -4
- package/dist/operations/comments.d.ts.map +1 -1
- package/dist/operations/comments.js +91 -8
- package/dist/operations/comments.js.map +1 -1
- package/dist/operations/duplicate.d.ts +16 -0
- package/dist/operations/duplicate.d.ts.map +1 -0
- package/dist/operations/duplicate.js +43 -0
- package/dist/operations/duplicate.js.map +1 -0
- package/dist/operations/labels.d.ts +1 -1
- package/dist/operations/labels.d.ts.map +1 -1
- package/dist/operations/labels.js +7 -4
- package/dist/operations/labels.js.map +1 -1
- package/dist/operations/lists.d.ts +63 -1
- package/dist/operations/lists.d.ts.map +1 -1
- package/dist/operations/lists.js +97 -2
- package/dist/operations/lists.js.map +1 -1
- package/dist/operations/members.d.ts +39 -0
- package/dist/operations/members.d.ts.map +1 -0
- package/dist/operations/members.js +107 -0
- package/dist/operations/members.js.map +1 -0
- package/dist/operations/projects.d.ts +16 -0
- package/dist/operations/projects.d.ts.map +1 -1
- package/dist/operations/projects.js +54 -9
- package/dist/operations/projects.js.map +1 -1
- package/dist/operations/tasks.d.ts +1 -1
- package/dist/operations/tasks.d.ts.map +1 -1
- package/dist/operations/tasks.js +5 -3
- package/dist/operations/tasks.js.map +1 -1
- package/dist/operations/users.d.ts +123 -0
- package/dist/operations/users.d.ts.map +1 -0
- package/dist/operations/users.js +180 -0
- package/dist/operations/users.js.map +1 -0
- package/dist/operations/verify.d.ts +84 -0
- package/dist/operations/verify.d.ts.map +1 -0
- package/dist/operations/verify.js +124 -0
- package/dist/operations/verify.js.map +1 -0
- package/dist/prompts.d.ts +48 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +155 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +38 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +127 -0
- package/dist/resources.js.map +1 -0
- package/dist/schemas/entities.d.ts +115 -24
- package/dist/schemas/entities.d.ts.map +1 -1
- package/dist/schemas/entities.js +48 -0
- package/dist/schemas/entities.js.map +1 -1
- package/dist/schemas/requests.d.ts +121 -46
- package/dist/schemas/requests.d.ts.map +1 -1
- package/dist/schemas/requests.js +57 -12
- package/dist/schemas/requests.js.map +1 -1
- package/dist/schemas/responses.d.ts +541 -186
- package/dist/schemas/responses.d.ts.map +1 -1
- package/dist/schemas/responses.js +13 -2
- package/dist/schemas/responses.js.map +1 -1
- package/dist/tools/activity.d.ts +150 -0
- package/dist/tools/activity.d.ts.map +1 -0
- package/dist/tools/activity.js +198 -0
- package/dist/tools/activity.js.map +1 -0
- package/dist/tools/annotations.d.ts +52 -0
- package/dist/tools/annotations.d.ts.map +1 -0
- package/dist/tools/annotations.js +214 -0
- package/dist/tools/annotations.js.map +1 -0
- package/dist/tools/attachments.d.ts +28 -4
- package/dist/tools/attachments.d.ts.map +1 -1
- package/dist/tools/attachments.js +53 -34
- package/dist/tools/attachments.js.map +1 -1
- package/dist/tools/card-ops.d.ts +232 -0
- package/dist/tools/card-ops.d.ts.map +1 -0
- package/dist/tools/card-ops.js +333 -0
- package/dist/tools/card-ops.js.map +1 -0
- package/dist/tools/cards.d.ts +90 -8
- package/dist/tools/cards.d.ts.map +1 -1
- package/dist/tools/cards.js +411 -128
- package/dist/tools/cards.js.map +1 -1
- package/dist/tools/comments.d.ts +226 -22
- package/dist/tools/comments.d.ts.map +1 -1
- package/dist/tools/comments.js +163 -134
- package/dist/tools/comments.js.map +1 -1
- package/dist/tools/dispatch.d.ts +47 -0
- package/dist/tools/dispatch.d.ts.map +1 -0
- package/dist/tools/dispatch.js +63 -0
- package/dist/tools/dispatch.js.map +1 -0
- package/dist/tools/guard.d.ts +9 -0
- package/dist/tools/guard.d.ts.map +1 -0
- package/dist/tools/guard.js +20 -0
- package/dist/tools/guard.js.map +1 -0
- package/dist/tools/index.d.ts +748 -450
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +136 -17
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/labels.d.ts +213 -18
- package/dist/tools/labels.d.ts.map +1 -1
- package/dist/tools/labels.js +218 -203
- package/dist/tools/labels.js.map +1 -1
- package/dist/tools/lists.d.ts +222 -15
- package/dist/tools/lists.d.ts.map +1 -1
- package/dist/tools/lists.js +175 -156
- package/dist/tools/lists.js.map +1 -1
- package/dist/tools/members.d.ts +128 -0
- package/dist/tools/members.d.ts.map +1 -0
- package/dist/tools/members.js +150 -0
- package/dist/tools/members.js.map +1 -0
- package/dist/tools/navigation.d.ts +22 -2
- package/dist/tools/navigation.d.ts.map +1 -1
- package/dist/tools/navigation.js +60 -15
- package/dist/tools/navigation.js.map +1 -1
- package/dist/tools/queries.d.ts +196 -166
- package/dist/tools/queries.d.ts.map +1 -1
- package/dist/tools/queries.js +125 -155
- package/dist/tools/queries.js.map +1 -1
- package/dist/tools/tasks.d.ts +26 -6
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +110 -55
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/users.d.ts +130 -0
- package/dist/tools/users.d.ts.map +1 -0
- package/dist/tools/users.js +165 -0
- package/dist/tools/users.js.map +1 -0
- package/docs/planka-2x-gotchas.md +121 -5
- package/docs/tools.md +771 -187
- package/docs/troubleshooting.md +137 -5
- package/hooks/hooks.json +15 -0
- package/hooks/preflight.mjs +100 -0
- package/package.json +6 -1
- package/scripts/setup.sh +8 -26
- package/scripts/sync-identity.mjs +55 -1
- package/server.json +87 -6
- package/tests/smoke/planka-smoke.mjs +512 -72
- package/workflow/skills/planka-close-card/SKILL.md +18 -5
- package/workflow/skills/planka-orchestrator/SKILL.md +36 -7
package/dist/tools/cards.js
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Card tools for PLANKA MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Every write here re-reads the card and reports what LANDED — never what was
|
|
5
|
+
* asked for. See src/operations/verify.ts.
|
|
3
6
|
*/
|
|
4
|
-
import { createCard,
|
|
7
|
+
import { createCard, getCardLight, getBoardNames, updateCard, moveCard, deleteCard, } from "../operations/cards.js";
|
|
8
|
+
import { getComments, COMMENTS_PAGE_SIZE } from "../operations/comments.js";
|
|
5
9
|
import { createTasks } from "../operations/tasks.js";
|
|
6
10
|
import { addLabelToCard } from "../operations/labels.js";
|
|
11
|
+
import { goneOn404, nothingToDo, verifiedDelete, verifiedWrite, writeResult, } from "../operations/verify.js";
|
|
7
12
|
import { PlankaError } from "../errors.js";
|
|
13
|
+
import { assertListWritable, getPolicy } from "../config/policy.js";
|
|
14
|
+
/**
|
|
15
|
+
* PLANKA_PROTECTED_LIST_IDS has to protect a column's CONTENT, not just the
|
|
16
|
+
* column: emptying it card by card (move, archive, delete) was the way around
|
|
17
|
+
* it. Costs one extra read, and only on deployments that configured the
|
|
18
|
+
* variable — with no protected list there is nothing to check.
|
|
19
|
+
*/
|
|
20
|
+
async function assertCardsListWritable(cardId, action) {
|
|
21
|
+
if (getPolicy().protectedListIds.length === 0)
|
|
22
|
+
return;
|
|
23
|
+
const before = await getCardLight({ cardId });
|
|
24
|
+
assertListWritable(before.card.listId, undefined, action);
|
|
25
|
+
}
|
|
26
|
+
/** Two date values are the same instant (or both empty). */
|
|
27
|
+
function sameDate(a, b) {
|
|
28
|
+
const na = a ?? null;
|
|
29
|
+
const nb = b ?? null;
|
|
30
|
+
if (na === nb)
|
|
31
|
+
return true;
|
|
32
|
+
if (na === null || nb === null)
|
|
33
|
+
return false;
|
|
34
|
+
const ta = Date.parse(na);
|
|
35
|
+
const tb = Date.parse(nb);
|
|
36
|
+
return Number.isNaN(ta) || Number.isNaN(tb) ? na === nb : ta === tb;
|
|
37
|
+
}
|
|
8
38
|
/**
|
|
9
39
|
* Tool: planka_create_card
|
|
10
40
|
* Create a new card on a board.
|
|
11
41
|
*/
|
|
12
42
|
export const createCardTool = {
|
|
13
43
|
name: "planka_create_card",
|
|
14
|
-
description: "Create a
|
|
44
|
+
description: "Create a card in a list, optionally with a checklist of tasks.",
|
|
15
45
|
inputSchema: {
|
|
16
46
|
type: "object",
|
|
17
47
|
properties: {
|
|
@@ -46,52 +76,103 @@ export const createCardTool = {
|
|
|
46
76
|
},
|
|
47
77
|
handler: async (params) => {
|
|
48
78
|
try {
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
79
|
+
const requestedTasks = params.tasks ?? [];
|
|
80
|
+
const requestedLabels = params.labelIds ?? [];
|
|
81
|
+
// 1. Create the card and prove it exists.
|
|
82
|
+
const created = await verifiedWrite({
|
|
83
|
+
write: () => createCard({
|
|
84
|
+
listId: params.listId,
|
|
85
|
+
name: params.name,
|
|
86
|
+
description: params.description,
|
|
87
|
+
dueDate: params.dueDate,
|
|
88
|
+
}),
|
|
89
|
+
reread: (card) => getCardLight({ cardId: card.id }),
|
|
90
|
+
// The re-read is by id, so comparing ids alone is true by construction.
|
|
91
|
+
// What has to hold is what the CALL asked for: name and column.
|
|
92
|
+
landed: (after, card) => after.card.id === card.id &&
|
|
93
|
+
after.card.name === params.name &&
|
|
94
|
+
after.card.listId === params.listId,
|
|
55
95
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
96
|
+
const card = created.result;
|
|
97
|
+
const partialFailure = {};
|
|
98
|
+
// 2. Tasks. A failure here must never cost the caller the card id.
|
|
99
|
+
if (requestedTasks.length > 0) {
|
|
100
|
+
try {
|
|
101
|
+
await createTasks({
|
|
102
|
+
cardId: card.id,
|
|
103
|
+
tasks: requestedTasks.map((name) => ({ name })),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
partialFailure.tasks =
|
|
108
|
+
error instanceof Error ? error.message : String(error);
|
|
109
|
+
}
|
|
62
110
|
}
|
|
63
|
-
//
|
|
64
|
-
let labelsAttached = 0;
|
|
111
|
+
// 3. Labels, one by one: a bad labelId is a 404 that must not sink the rest.
|
|
65
112
|
const labelErrors = [];
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
113
|
+
for (const labelId of requestedLabels) {
|
|
114
|
+
try {
|
|
115
|
+
await addLabelToCard({ cardId: card.id, labelId });
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
// A bad labelId and a 403 on the board are indistinguishable without
|
|
119
|
+
// the reason — and they need opposite reactions from the caller.
|
|
120
|
+
labelErrors.push({
|
|
121
|
+
labelId,
|
|
122
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 4. Read the card once more and report REALITY.
|
|
127
|
+
let after = created.after;
|
|
128
|
+
let readError = created.verificationError;
|
|
129
|
+
if (requestedTasks.length > 0 || requestedLabels.length > 0) {
|
|
130
|
+
try {
|
|
131
|
+
after = await getCardLight({ cardId: card.id });
|
|
132
|
+
readError = undefined;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
readError = `the card was created but could not be re-read: ${error instanceof Error ? error.message : String(error)}`;
|
|
76
136
|
}
|
|
77
137
|
}
|
|
138
|
+
const tasksOnCard = after
|
|
139
|
+
? requestedTasks.filter((name) => after.tasks.some((t) => t.name === name)).length
|
|
140
|
+
: 0;
|
|
141
|
+
const finalLabelIds = after
|
|
142
|
+
? after.cardLabels.map((cl) => cl.labelId)
|
|
143
|
+
: [];
|
|
144
|
+
const labelsAttached = after
|
|
145
|
+
? requestedLabels.filter((id) => finalLabelIds.includes(id)).length
|
|
146
|
+
: 0;
|
|
147
|
+
if (labelErrors.length > 0)
|
|
148
|
+
partialFailure.labelIds = labelErrors;
|
|
149
|
+
if (after && tasksOnCard < requestedTasks.length) {
|
|
150
|
+
partialFailure.tasksMissing = requestedTasks.filter((name) => !after.tasks.some((t) => t.name === name));
|
|
151
|
+
}
|
|
152
|
+
const allLabelsFailed = requestedLabels.length > 0 && labelsAttached === 0;
|
|
153
|
+
const ok = created.verified &&
|
|
154
|
+
!readError &&
|
|
155
|
+
Object.keys(partialFailure).length === 0 &&
|
|
156
|
+
!allLabelsFailed;
|
|
157
|
+
const body = {
|
|
158
|
+
success: ok,
|
|
159
|
+
verified: created.verified && !readError,
|
|
160
|
+
...(readError ? { verificationError: readError } : {}),
|
|
161
|
+
card: {
|
|
162
|
+
id: card.id,
|
|
163
|
+
name: after?.card.name ?? card.name,
|
|
164
|
+
listId: after?.card.listId ?? card.listId,
|
|
165
|
+
},
|
|
166
|
+
tasksRequested: requestedTasks.length,
|
|
167
|
+
tasksCreated: tasksOnCard,
|
|
168
|
+
labelsRequested: requestedLabels.length,
|
|
169
|
+
labelsAttached,
|
|
170
|
+
...(after ? { finalLabelIds } : {}),
|
|
171
|
+
...(Object.keys(partialFailure).length > 0 ? { partialFailure } : {}),
|
|
172
|
+
};
|
|
78
173
|
return {
|
|
79
|
-
content: [
|
|
80
|
-
|
|
81
|
-
type: "text",
|
|
82
|
-
text: JSON.stringify({
|
|
83
|
-
success: true,
|
|
84
|
-
card: {
|
|
85
|
-
id: card.id,
|
|
86
|
-
name: card.name,
|
|
87
|
-
listId: card.listId,
|
|
88
|
-
},
|
|
89
|
-
tasksCreated: params.tasks?.length || 0,
|
|
90
|
-
labelsAttached,
|
|
91
|
-
...(labelErrors.length > 0 && { labelErrors }),
|
|
92
|
-
}, null, 2),
|
|
93
|
-
},
|
|
94
|
-
],
|
|
174
|
+
content: [{ type: "text", text: JSON.stringify(body) }],
|
|
175
|
+
...(ok ? {} : { isError: true }),
|
|
95
176
|
};
|
|
96
177
|
}
|
|
97
178
|
catch (error) {
|
|
@@ -105,13 +186,104 @@ export const createCardTool = {
|
|
|
105
186
|
}
|
|
106
187
|
},
|
|
107
188
|
};
|
|
189
|
+
/** Default size of the description preview in the digest. */
|
|
190
|
+
const DEFAULT_DESCRIPTION_CHARS = 200;
|
|
191
|
+
/** Ceiling for `descriptionChars`: past this, ask for detail: "full". */
|
|
192
|
+
const MAX_DESCRIPTION_CHARS = 2000;
|
|
193
|
+
/**
|
|
194
|
+
* Card labels resolved to names, keeping the ids the write tools need.
|
|
195
|
+
*
|
|
196
|
+
* The card response carries only the cardId<->labelId pairs; the names come
|
|
197
|
+
* from the board (see getBoardNames), so `names` may legitimately be missing.
|
|
198
|
+
*/
|
|
199
|
+
function labelsOf(details, names) {
|
|
200
|
+
return details.cardLabels.map((cl) => ({
|
|
201
|
+
id: cl.labelId,
|
|
202
|
+
name: names?.get(cl.labelId) ??
|
|
203
|
+
details.labels.find((l) => l.id === cl.labelId)?.name ??
|
|
204
|
+
null,
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
/** Assignees, from cardMemberships joined with the included users. */
|
|
208
|
+
function membersOf(details) {
|
|
209
|
+
return details.cardMemberships.map((m) => ({
|
|
210
|
+
id: m.userId,
|
|
211
|
+
name: details.users.find((u) => u.id === m.userId)?.name ?? null,
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Tasks grouped by their checklist — never flattened.
|
|
216
|
+
*
|
|
217
|
+
* A card with "Backend 3/3" and "QA 0/4" says something a flat 3/7 does not,
|
|
218
|
+
* and Planka 2.x models exactly that (taskLists -> tasks by taskListId).
|
|
219
|
+
*/
|
|
220
|
+
function tasksOf(details, withItems) {
|
|
221
|
+
const lists = details.taskLists.map((tl) => {
|
|
222
|
+
const items = details.tasks.filter((t) => t.taskListId === tl.id);
|
|
223
|
+
return {
|
|
224
|
+
id: tl.id,
|
|
225
|
+
name: tl.name,
|
|
226
|
+
done: items.filter((t) => t.isCompleted).length,
|
|
227
|
+
total: items.length,
|
|
228
|
+
...(withItems
|
|
229
|
+
? {
|
|
230
|
+
items: items.map((t) => ({
|
|
231
|
+
id: t.id,
|
|
232
|
+
name: t.name,
|
|
233
|
+
isCompleted: t.isCompleted,
|
|
234
|
+
...(t.assigneeUserId
|
|
235
|
+
? {
|
|
236
|
+
assignee: details.users.find((u) => u.id === t.assigneeUserId)
|
|
237
|
+
?.name ?? t.assigneeUserId,
|
|
238
|
+
}
|
|
239
|
+
: {}),
|
|
240
|
+
})),
|
|
241
|
+
}
|
|
242
|
+
: {}),
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
// Tasks whose checklist did not come back must not vanish from the count.
|
|
246
|
+
const known = new Set(details.taskLists.map((tl) => tl.id));
|
|
247
|
+
const orphans = details.tasks.filter((t) => !known.has(t.taskListId));
|
|
248
|
+
if (orphans.length > 0) {
|
|
249
|
+
lists.push({
|
|
250
|
+
id: null,
|
|
251
|
+
name: "(ungrouped)",
|
|
252
|
+
done: orphans.filter((t) => t.isCompleted).length,
|
|
253
|
+
total: orphans.length,
|
|
254
|
+
...(withItems
|
|
255
|
+
? {
|
|
256
|
+
items: orphans.map((t) => ({
|
|
257
|
+
id: t.id,
|
|
258
|
+
name: t.name,
|
|
259
|
+
isCompleted: t.isCompleted,
|
|
260
|
+
})),
|
|
261
|
+
}
|
|
262
|
+
: {}),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
done: details.tasks.filter((t) => t.isCompleted).length,
|
|
267
|
+
total: details.tasks.length,
|
|
268
|
+
lists,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/** Custom field values resolved to their field name (full detail only). */
|
|
272
|
+
function customFieldsOf(details) {
|
|
273
|
+
return details.customFieldValues.map((v) => ({
|
|
274
|
+
name: details.customFields.find((f) => f.id === v.customFieldId)?.name ?? null,
|
|
275
|
+
content: v.content ?? null,
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
108
278
|
/**
|
|
109
279
|
* Tool: planka_get_card
|
|
110
|
-
*
|
|
280
|
+
* A digest by default; the full record only when asked for.
|
|
111
281
|
*/
|
|
112
282
|
export const getCardTool = {
|
|
113
283
|
name: "planka_get_card",
|
|
114
|
-
description: "
|
|
284
|
+
description: "One card as a digest: name, column, labels, members, checklist counts, attachment and comment counts, and the first 200 chars of the description. " +
|
|
285
|
+
'Comments are NOT included by default — pass withComments (a number, or "all") to read them. ' +
|
|
286
|
+
'detail:"full" adds the whole description, every task item, the attachment list and the custom fields.',
|
|
115
287
|
inputSchema: {
|
|
116
288
|
type: "object",
|
|
117
289
|
properties: {
|
|
@@ -119,52 +291,133 @@ export const getCardTool = {
|
|
|
119
291
|
type: "string",
|
|
120
292
|
description: "The card ID",
|
|
121
293
|
},
|
|
294
|
+
detail: {
|
|
295
|
+
type: "string",
|
|
296
|
+
enum: ["digest", "full"],
|
|
297
|
+
description: 'How much of the card to return. "digest" (default) is counts plus a description preview; "full" is the whole description, task items, attachments and custom fields.',
|
|
298
|
+
},
|
|
299
|
+
withComments: {
|
|
300
|
+
description: 'Comments to include: 0 (default, none), a number for the N most recent, or "all" to page through every comment.',
|
|
301
|
+
anyOf: [{ type: "number" }, { type: "string", enum: ["all"] }],
|
|
302
|
+
},
|
|
303
|
+
descriptionChars: {
|
|
304
|
+
type: "number",
|
|
305
|
+
description: 'Length of the description preview in the digest (default 200, max 2000). Ignored when detail is "full".',
|
|
306
|
+
},
|
|
122
307
|
},
|
|
123
308
|
required: ["cardId"],
|
|
124
309
|
},
|
|
125
310
|
handler: async (params) => {
|
|
126
311
|
try {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
312
|
+
const full = params.detail === "full";
|
|
313
|
+
const wantAll = params.withComments === "all";
|
|
314
|
+
const wantN = typeof params.withComments === "number"
|
|
315
|
+
? Math.max(0, Math.floor(params.withComments))
|
|
316
|
+
: 0;
|
|
317
|
+
const wantComments = wantAll || wantN > 0;
|
|
318
|
+
const [details, comments] = await Promise.all([
|
|
319
|
+
getCardLight({ cardId: params.cardId }),
|
|
320
|
+
wantComments
|
|
321
|
+
? getComments({
|
|
322
|
+
cardId: params.cardId,
|
|
323
|
+
...(wantAll
|
|
324
|
+
? { all: true }
|
|
325
|
+
: { limit: Math.min(wantN, COMMENTS_PAGE_SIZE) }),
|
|
326
|
+
order: "asc",
|
|
327
|
+
})
|
|
328
|
+
: Promise.resolve(null),
|
|
329
|
+
]);
|
|
330
|
+
// Names for the ids the card carries. Best effort: a board that cannot
|
|
331
|
+
// be read costs the names, never the card.
|
|
332
|
+
const names = await getBoardNames(details.card.boardId);
|
|
333
|
+
const listName = names?.lists.get(details.card.listId) ?? null;
|
|
334
|
+
const description = details.card.description ?? "";
|
|
335
|
+
const previewChars = Math.max(0, Math.min(params.descriptionChars ?? DEFAULT_DESCRIPTION_CHARS, MAX_DESCRIPTION_CHARS));
|
|
336
|
+
const preview = description.slice(0, previewChars);
|
|
337
|
+
const card = {
|
|
338
|
+
id: details.card.id,
|
|
339
|
+
name: details.card.name,
|
|
340
|
+
listId: details.card.listId,
|
|
341
|
+
// Always present, even as null: an omitted field cannot be told apart
|
|
342
|
+
// from "the board could not be read", and the model would assume the
|
|
343
|
+
// card has no column.
|
|
344
|
+
listName,
|
|
345
|
+
...(listName === null
|
|
346
|
+
? {
|
|
347
|
+
listNameUnavailable: names
|
|
348
|
+
? "the board does not list this column — it may be the hidden archive or trash"
|
|
349
|
+
: "the board could not be read, so column and label names were not resolved",
|
|
350
|
+
}
|
|
351
|
+
: {}),
|
|
352
|
+
boardId: details.card.boardId,
|
|
353
|
+
dueDate: details.card.dueDate ?? null,
|
|
354
|
+
isDueCompleted: details.card.isDueCompleted ??
|
|
355
|
+
details.card.isDueDateCompleted ??
|
|
356
|
+
null,
|
|
357
|
+
isClosed: details.card.isClosed ?? null,
|
|
358
|
+
createdAt: details.card.createdAt,
|
|
359
|
+
updatedAt: details.card.updatedAt ?? null,
|
|
360
|
+
};
|
|
361
|
+
if (full) {
|
|
362
|
+
card.description = details.card.description ?? null;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
card.descriptionPreview = preview;
|
|
366
|
+
card.descriptionChars = description.length;
|
|
367
|
+
if (description.length > preview.length) {
|
|
368
|
+
card.descriptionTruncated = true;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// Planka's own counter, not something this server derived.
|
|
372
|
+
const commentsTotal = details.card.commentsTotal ?? null;
|
|
373
|
+
const body = {
|
|
374
|
+
detail: full ? "full" : "digest",
|
|
375
|
+
card,
|
|
376
|
+
labels: labelsOf(details, names?.labels),
|
|
377
|
+
members: membersOf(details),
|
|
378
|
+
tasks: tasksOf(details, full),
|
|
379
|
+
attachmentsCount: details.attachments.length,
|
|
380
|
+
commentsTotal,
|
|
381
|
+
};
|
|
382
|
+
if (full) {
|
|
383
|
+
body.attachments = details.attachments.map((a) => ({
|
|
384
|
+
id: a.id,
|
|
385
|
+
name: a.name,
|
|
386
|
+
type: a.type ?? null,
|
|
387
|
+
mimeType: a.data?.mimeType ?? null,
|
|
388
|
+
sizeBytes: a.data?.size ?? null,
|
|
389
|
+
}));
|
|
390
|
+
body.customFields = customFieldsOf(details);
|
|
391
|
+
}
|
|
392
|
+
if (comments) {
|
|
393
|
+
body.comments = comments.comments.map((c) => ({
|
|
145
394
|
id: c.id,
|
|
146
395
|
text: c.text,
|
|
147
396
|
author: c.authorName,
|
|
148
397
|
createdAt: c.createdAt,
|
|
149
|
-
}))
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
398
|
+
}));
|
|
399
|
+
body.commentsReturned = comments.comments.length;
|
|
400
|
+
body.commentsOrder = "oldest-first";
|
|
401
|
+
if (comments.hasMore)
|
|
402
|
+
body.commentsHasMore = true;
|
|
403
|
+
if (comments.nextBeforeId)
|
|
404
|
+
body.nextBeforeId = comments.nextBeforeId;
|
|
405
|
+
if (comments.truncated)
|
|
406
|
+
body.commentsTruncated = true;
|
|
407
|
+
}
|
|
408
|
+
else if (commentsTotal !== 0) {
|
|
409
|
+
body.comments =
|
|
410
|
+
'not read — pass withComments (a number, or "all"), or call planka_get_comments';
|
|
411
|
+
}
|
|
412
|
+
if (!full) {
|
|
413
|
+
body.more =
|
|
414
|
+
'detail:"full" for the whole description, task items, attachments and custom fields';
|
|
415
|
+
}
|
|
163
416
|
return {
|
|
164
417
|
content: [
|
|
165
418
|
{
|
|
166
419
|
type: "text",
|
|
167
|
-
text: JSON.stringify(
|
|
420
|
+
text: JSON.stringify(body),
|
|
168
421
|
},
|
|
169
422
|
],
|
|
170
423
|
};
|
|
@@ -186,7 +439,7 @@ export const getCardTool = {
|
|
|
186
439
|
*/
|
|
187
440
|
export const updateCardTool = {
|
|
188
441
|
name: "planka_update_card",
|
|
189
|
-
description: "Update a card's
|
|
442
|
+
description: "Update a card's name, description, due date or completion. Null clears a field.",
|
|
190
443
|
inputSchema: {
|
|
191
444
|
type: "object",
|
|
192
445
|
properties: {
|
|
@@ -226,24 +479,41 @@ export const updateCardTool = {
|
|
|
226
479
|
patch.dueDate = updates.dueDate;
|
|
227
480
|
if (updates.isCompleted !== undefined)
|
|
228
481
|
patch.isCompleted = updates.isCompleted;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
482
|
+
if (Object.keys(patch).length === 1) {
|
|
483
|
+
return nothingToDo("name, description, dueDate, isCompleted");
|
|
484
|
+
}
|
|
485
|
+
const mismatches = (after) => {
|
|
486
|
+
const bad = [];
|
|
487
|
+
const c = after.card;
|
|
488
|
+
if (patch.name !== undefined && c.name !== patch.name)
|
|
489
|
+
bad.push("name");
|
|
490
|
+
if (patch.description !== undefined &&
|
|
491
|
+
(c.description ?? null) !== (patch.description ?? null))
|
|
492
|
+
bad.push("description");
|
|
493
|
+
if (patch.dueDate !== undefined && !sameDate(c.dueDate, patch.dueDate))
|
|
494
|
+
bad.push("dueDate");
|
|
495
|
+
if (patch.isCompleted !== undefined &&
|
|
496
|
+
c.isCompleted !== patch.isCompleted)
|
|
497
|
+
bad.push("isCompleted");
|
|
498
|
+
return bad;
|
|
246
499
|
};
|
|
500
|
+
const written = await verifiedWrite({
|
|
501
|
+
write: () => updateCard(patch),
|
|
502
|
+
reread: () => getCardLight({ cardId }),
|
|
503
|
+
landed: (after) => mismatches(after).length === 0,
|
|
504
|
+
});
|
|
505
|
+
const after = written.after;
|
|
506
|
+
const notLanded = after ? mismatches(after) : [];
|
|
507
|
+
return writeResult({
|
|
508
|
+
card: {
|
|
509
|
+
id: written.result.id,
|
|
510
|
+
name: after?.card.name ?? written.result.name,
|
|
511
|
+
description: after?.card.description ?? written.result.description,
|
|
512
|
+
dueDate: after?.card.dueDate ?? written.result.dueDate,
|
|
513
|
+
isCompleted: after?.card.isCompleted ?? written.result.isCompleted,
|
|
514
|
+
},
|
|
515
|
+
...(notLanded.length > 0 ? { notLanded } : {}),
|
|
516
|
+
}, written);
|
|
247
517
|
}
|
|
248
518
|
catch (error) {
|
|
249
519
|
if (error instanceof PlankaError) {
|
|
@@ -262,7 +532,7 @@ export const updateCardTool = {
|
|
|
262
532
|
*/
|
|
263
533
|
export const moveCardTool = {
|
|
264
534
|
name: "planka_move_card",
|
|
265
|
-
description: "Move a card to
|
|
535
|
+
description: "Move a card to another list, board or position — the workflow transition.",
|
|
266
536
|
inputSchema: {
|
|
267
537
|
type: "object",
|
|
268
538
|
properties: {
|
|
@@ -274,6 +544,10 @@ export const moveCardTool = {
|
|
|
274
544
|
type: "string",
|
|
275
545
|
description: "Target list ID",
|
|
276
546
|
},
|
|
547
|
+
boardId: {
|
|
548
|
+
type: "string",
|
|
549
|
+
description: "Target board ID — only when moving across boards (listId must belong to it)",
|
|
550
|
+
},
|
|
277
551
|
position: {
|
|
278
552
|
type: "number",
|
|
279
553
|
description: "Position in the list (lower = higher). Default: end of list",
|
|
@@ -283,26 +557,36 @@ export const moveCardTool = {
|
|
|
283
557
|
},
|
|
284
558
|
handler: async (params) => {
|
|
285
559
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
560
|
+
await assertCardsListWritable(params.cardId, "move cards out of");
|
|
561
|
+
const written = await verifiedWrite({
|
|
562
|
+
write: () => moveCard({
|
|
563
|
+
cardId: params.cardId,
|
|
564
|
+
listId: params.listId,
|
|
565
|
+
boardId: params.boardId,
|
|
566
|
+
position: params.position ?? 65536,
|
|
567
|
+
}),
|
|
568
|
+
reread: () => getCardLight({ cardId: params.cardId }),
|
|
569
|
+
landed: (after) => after.card.listId === params.listId &&
|
|
570
|
+
(params.boardId ? after.card.boardId === params.boardId : true),
|
|
290
571
|
});
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
572
|
+
const after = written.after;
|
|
573
|
+
return writeResult({
|
|
574
|
+
card: {
|
|
575
|
+
id: written.result.id,
|
|
576
|
+
name: after?.card.name ?? written.result.name,
|
|
577
|
+
listId: after?.card.listId ?? written.result.listId,
|
|
578
|
+
boardId: after?.card.boardId ?? written.result.boardId,
|
|
579
|
+
},
|
|
580
|
+
requestedListId: params.listId,
|
|
581
|
+
...(params.boardId ? { requestedBoardId: params.boardId } : {}),
|
|
582
|
+
...(after && after.card.listId !== params.listId
|
|
583
|
+
? {
|
|
584
|
+
notLanded: "the card is still in list " +
|
|
585
|
+
after.card.listId +
|
|
586
|
+
" — check that the listId belongs to the card's board",
|
|
587
|
+
}
|
|
588
|
+
: {}),
|
|
589
|
+
}, written);
|
|
306
590
|
}
|
|
307
591
|
catch (error) {
|
|
308
592
|
if (error instanceof PlankaError) {
|
|
@@ -321,7 +605,7 @@ export const moveCardTool = {
|
|
|
321
605
|
*/
|
|
322
606
|
export const deleteCardTool = {
|
|
323
607
|
name: "planka_delete_card",
|
|
324
|
-
description: "Permanently delete a card.
|
|
608
|
+
description: "Permanently delete a card with its tasks, comments and attachments. Cannot be undone; confirmed by re-reading.",
|
|
325
609
|
inputSchema: {
|
|
326
610
|
type: "object",
|
|
327
611
|
properties: {
|
|
@@ -334,18 +618,17 @@ export const deleteCardTool = {
|
|
|
334
618
|
},
|
|
335
619
|
handler: async (params) => {
|
|
336
620
|
try {
|
|
337
|
-
await
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
};
|
|
621
|
+
await assertCardsListWritable(params.cardId, "delete cards from");
|
|
622
|
+
const deleted = await verifiedDelete({
|
|
623
|
+
del: () => deleteCard({ cardId: params.cardId }),
|
|
624
|
+
confirmGone: () => goneOn404(() => getCardLight({ cardId: params.cardId })),
|
|
625
|
+
});
|
|
626
|
+
return writeResult({
|
|
627
|
+
cardId: params.cardId,
|
|
628
|
+
message: deleted.verified
|
|
629
|
+
? `Card ${params.cardId} deleted`
|
|
630
|
+
: `Card ${params.cardId} still answers a read after the delete`,
|
|
631
|
+
}, deleted);
|
|
349
632
|
}
|
|
350
633
|
catch (error) {
|
|
351
634
|
if (error instanceof PlankaError) {
|