@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.
Files changed (195) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +26 -0
  3. package/.claude-plugin/plugin.json +45 -0
  4. package/.codex-plugin/mcp.json +16 -0
  5. package/.codex-plugin/plugin.json +27 -0
  6. package/.mcp.json +17 -0
  7. package/CHANGELOG.md +510 -0
  8. package/README.es.md +294 -55
  9. package/README.md +293 -55
  10. package/dist/cli/init.d.ts +101 -0
  11. package/dist/cli/init.d.ts.map +1 -0
  12. package/dist/cli/init.js +481 -0
  13. package/dist/cli/init.js.map +1 -0
  14. package/dist/client.d.ts +32 -4
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +106 -32
  17. package/dist/client.js.map +1 -1
  18. package/dist/config/policy.d.ts +82 -0
  19. package/dist/config/policy.d.ts.map +1 -0
  20. package/dist/config/policy.js +226 -0
  21. package/dist/config/policy.js.map +1 -0
  22. package/dist/errors.d.ts +5 -1
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js +62 -5
  25. package/dist/errors.js.map +1 -1
  26. package/dist/identity.generated.d.ts +2 -1
  27. package/dist/identity.generated.d.ts.map +1 -1
  28. package/dist/identity.generated.js +2 -1
  29. package/dist/identity.generated.js.map +1 -1
  30. package/dist/index.js +85 -10
  31. package/dist/index.js.map +1 -1
  32. package/dist/instructions.d.ts +21 -0
  33. package/dist/instructions.d.ts.map +1 -0
  34. package/dist/instructions.js +37 -0
  35. package/dist/instructions.js.map +1 -0
  36. package/dist/operations/actions.d.ts +654 -0
  37. package/dist/operations/actions.d.ts.map +1 -0
  38. package/dist/operations/actions.js +154 -0
  39. package/dist/operations/actions.js.map +1 -0
  40. package/dist/operations/archive.d.ts +28 -0
  41. package/dist/operations/archive.d.ts.map +1 -0
  42. package/dist/operations/archive.js +74 -0
  43. package/dist/operations/archive.js.map +1 -0
  44. package/dist/operations/attachments.d.ts +1 -1
  45. package/dist/operations/attachments.d.ts.map +1 -1
  46. package/dist/operations/attachments.js +3 -1
  47. package/dist/operations/attachments.js.map +1 -1
  48. package/dist/operations/board-id.d.ts +1 -1
  49. package/dist/operations/board-id.d.ts.map +1 -1
  50. package/dist/operations/board-id.js +13 -7
  51. package/dist/operations/board-id.js.map +1 -1
  52. package/dist/operations/boards.d.ts +96 -19
  53. package/dist/operations/boards.d.ts.map +1 -1
  54. package/dist/operations/boards.js +377 -93
  55. package/dist/operations/boards.js.map +1 -1
  56. package/dist/operations/card-brief.d.ts +91 -0
  57. package/dist/operations/card-brief.d.ts.map +1 -0
  58. package/dist/operations/card-brief.js +79 -0
  59. package/dist/operations/card-brief.js.map +1 -0
  60. package/dist/operations/cards.d.ts +34 -9
  61. package/dist/operations/cards.d.ts.map +1 -1
  62. package/dist/operations/cards.js +60 -14
  63. package/dist/operations/cards.js.map +1 -1
  64. package/dist/operations/comments.d.ts +61 -4
  65. package/dist/operations/comments.d.ts.map +1 -1
  66. package/dist/operations/comments.js +91 -8
  67. package/dist/operations/comments.js.map +1 -1
  68. package/dist/operations/duplicate.d.ts +16 -0
  69. package/dist/operations/duplicate.d.ts.map +1 -0
  70. package/dist/operations/duplicate.js +43 -0
  71. package/dist/operations/duplicate.js.map +1 -0
  72. package/dist/operations/labels.d.ts +1 -1
  73. package/dist/operations/labels.d.ts.map +1 -1
  74. package/dist/operations/labels.js +7 -4
  75. package/dist/operations/labels.js.map +1 -1
  76. package/dist/operations/lists.d.ts +63 -1
  77. package/dist/operations/lists.d.ts.map +1 -1
  78. package/dist/operations/lists.js +97 -2
  79. package/dist/operations/lists.js.map +1 -1
  80. package/dist/operations/members.d.ts +39 -0
  81. package/dist/operations/members.d.ts.map +1 -0
  82. package/dist/operations/members.js +107 -0
  83. package/dist/operations/members.js.map +1 -0
  84. package/dist/operations/projects.d.ts +16 -0
  85. package/dist/operations/projects.d.ts.map +1 -1
  86. package/dist/operations/projects.js +54 -9
  87. package/dist/operations/projects.js.map +1 -1
  88. package/dist/operations/tasks.d.ts +1 -1
  89. package/dist/operations/tasks.d.ts.map +1 -1
  90. package/dist/operations/tasks.js +5 -3
  91. package/dist/operations/tasks.js.map +1 -1
  92. package/dist/operations/users.d.ts +123 -0
  93. package/dist/operations/users.d.ts.map +1 -0
  94. package/dist/operations/users.js +180 -0
  95. package/dist/operations/users.js.map +1 -0
  96. package/dist/operations/verify.d.ts +84 -0
  97. package/dist/operations/verify.d.ts.map +1 -0
  98. package/dist/operations/verify.js +124 -0
  99. package/dist/operations/verify.js.map +1 -0
  100. package/dist/prompts.d.ts +48 -0
  101. package/dist/prompts.d.ts.map +1 -0
  102. package/dist/prompts.js +155 -0
  103. package/dist/prompts.js.map +1 -0
  104. package/dist/resources.d.ts +38 -0
  105. package/dist/resources.d.ts.map +1 -0
  106. package/dist/resources.js +127 -0
  107. package/dist/resources.js.map +1 -0
  108. package/dist/schemas/entities.d.ts +115 -24
  109. package/dist/schemas/entities.d.ts.map +1 -1
  110. package/dist/schemas/entities.js +48 -0
  111. package/dist/schemas/entities.js.map +1 -1
  112. package/dist/schemas/requests.d.ts +121 -46
  113. package/dist/schemas/requests.d.ts.map +1 -1
  114. package/dist/schemas/requests.js +57 -12
  115. package/dist/schemas/requests.js.map +1 -1
  116. package/dist/schemas/responses.d.ts +541 -186
  117. package/dist/schemas/responses.d.ts.map +1 -1
  118. package/dist/schemas/responses.js +13 -2
  119. package/dist/schemas/responses.js.map +1 -1
  120. package/dist/tools/activity.d.ts +150 -0
  121. package/dist/tools/activity.d.ts.map +1 -0
  122. package/dist/tools/activity.js +198 -0
  123. package/dist/tools/activity.js.map +1 -0
  124. package/dist/tools/annotations.d.ts +52 -0
  125. package/dist/tools/annotations.d.ts.map +1 -0
  126. package/dist/tools/annotations.js +214 -0
  127. package/dist/tools/annotations.js.map +1 -0
  128. package/dist/tools/attachments.d.ts +28 -4
  129. package/dist/tools/attachments.d.ts.map +1 -1
  130. package/dist/tools/attachments.js +53 -34
  131. package/dist/tools/attachments.js.map +1 -1
  132. package/dist/tools/card-ops.d.ts +232 -0
  133. package/dist/tools/card-ops.d.ts.map +1 -0
  134. package/dist/tools/card-ops.js +333 -0
  135. package/dist/tools/card-ops.js.map +1 -0
  136. package/dist/tools/cards.d.ts +90 -8
  137. package/dist/tools/cards.d.ts.map +1 -1
  138. package/dist/tools/cards.js +411 -128
  139. package/dist/tools/cards.js.map +1 -1
  140. package/dist/tools/comments.d.ts +226 -22
  141. package/dist/tools/comments.d.ts.map +1 -1
  142. package/dist/tools/comments.js +163 -134
  143. package/dist/tools/comments.js.map +1 -1
  144. package/dist/tools/dispatch.d.ts +47 -0
  145. package/dist/tools/dispatch.d.ts.map +1 -0
  146. package/dist/tools/dispatch.js +63 -0
  147. package/dist/tools/dispatch.js.map +1 -0
  148. package/dist/tools/guard.d.ts +9 -0
  149. package/dist/tools/guard.d.ts.map +1 -0
  150. package/dist/tools/guard.js +20 -0
  151. package/dist/tools/guard.js.map +1 -0
  152. package/dist/tools/index.d.ts +748 -450
  153. package/dist/tools/index.d.ts.map +1 -1
  154. package/dist/tools/index.js +136 -17
  155. package/dist/tools/index.js.map +1 -1
  156. package/dist/tools/labels.d.ts +213 -18
  157. package/dist/tools/labels.d.ts.map +1 -1
  158. package/dist/tools/labels.js +218 -203
  159. package/dist/tools/labels.js.map +1 -1
  160. package/dist/tools/lists.d.ts +222 -15
  161. package/dist/tools/lists.d.ts.map +1 -1
  162. package/dist/tools/lists.js +175 -156
  163. package/dist/tools/lists.js.map +1 -1
  164. package/dist/tools/members.d.ts +128 -0
  165. package/dist/tools/members.d.ts.map +1 -0
  166. package/dist/tools/members.js +150 -0
  167. package/dist/tools/members.js.map +1 -0
  168. package/dist/tools/navigation.d.ts +22 -2
  169. package/dist/tools/navigation.d.ts.map +1 -1
  170. package/dist/tools/navigation.js +60 -15
  171. package/dist/tools/navigation.js.map +1 -1
  172. package/dist/tools/queries.d.ts +196 -166
  173. package/dist/tools/queries.d.ts.map +1 -1
  174. package/dist/tools/queries.js +125 -155
  175. package/dist/tools/queries.js.map +1 -1
  176. package/dist/tools/tasks.d.ts +26 -6
  177. package/dist/tools/tasks.d.ts.map +1 -1
  178. package/dist/tools/tasks.js +110 -55
  179. package/dist/tools/tasks.js.map +1 -1
  180. package/dist/tools/users.d.ts +130 -0
  181. package/dist/tools/users.d.ts.map +1 -0
  182. package/dist/tools/users.js +165 -0
  183. package/dist/tools/users.js.map +1 -0
  184. package/docs/planka-2x-gotchas.md +121 -5
  185. package/docs/tools.md +771 -187
  186. package/docs/troubleshooting.md +137 -5
  187. package/hooks/hooks.json +15 -0
  188. package/hooks/preflight.mjs +100 -0
  189. package/package.json +6 -1
  190. package/scripts/setup.sh +8 -26
  191. package/scripts/sync-identity.mjs +55 -1
  192. package/server.json +87 -6
  193. package/tests/smoke/planka-smoke.mjs +512 -72
  194. package/workflow/skills/planka-close-card/SKILL.md +18 -5
  195. package/workflow/skills/planka-orchestrator/SKILL.md +36 -7
@@ -37,7 +37,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
37
37
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
38
38
  import { PlankaApi, boardEntities } from "../../scripts/lib/planka-api.mjs";
39
39
 
40
- const EXPECTED_TOOL_COUNT = 24;
40
+ const EXPECTED_TOOL_COUNT = 41;
41
41
 
42
42
  /** Prefix on every artifact this test creates; the initial sweep looks for it. */
43
43
  const PREFIX = "[smoke planka-mcp]";
@@ -287,15 +287,19 @@ async function run() {
287
287
  const boardText = JSON.stringify(mcpBoard);
288
288
 
289
289
  // --- labels the scratch card will carry -----------------------------------
290
- const labelA = await json("planka_manage_labels", {
291
- action: "create",
290
+ const labelA = await json("planka_create_label", {
292
291
  boardId,
293
292
  name: `${PREFIX} A`,
294
293
  color: "berry-red",
295
294
  });
296
295
  if (labelA?.label?.id) artifacts.labels.push(labelA.label.id);
297
- check("planka_manage_labels create (A)", Boolean(labelA?.label?.id), labelA);
296
+ check(
297
+ "planka_create_label (A) reports verified: true",
298
+ Boolean(labelA?.label?.id) && labelA.verified === true,
299
+ labelA
300
+ );
298
301
 
302
+ // The one deprecated-alias check for labels: the old name must still route.
299
303
  const labelB = await json("planka_manage_labels", {
300
304
  action: "create",
301
305
  boardId,
@@ -303,21 +307,24 @@ async function run() {
303
307
  color: "lagoon-blue",
304
308
  });
305
309
  if (labelB?.label?.id) artifacts.labels.push(labelB.label.id);
306
- check("planka_manage_labels create (B)", Boolean(labelB?.label?.id), labelB);
310
+ check(
311
+ "planka_manage_labels (DEPRECATED alias) still creates, verified: true",
312
+ Boolean(labelB?.label?.id) && labelB.verified === true,
313
+ labelB
314
+ );
307
315
 
308
316
  if (!labelA?.label?.id || !labelB?.label?.id) {
309
317
  throw new Error("Without labels there is no smoke — stopping here");
310
318
  }
311
319
 
312
- const renamed = await json("planka_manage_labels", {
313
- action: "update",
320
+ const renamed = await json("planka_update_label", {
314
321
  labelId: labelA.label.id,
315
322
  name: `${PREFIX} A2`,
316
323
  color: "pumpkin-orange",
317
324
  });
318
325
  check(
319
- "planka_manage_labels update renames and recolors",
320
- renamed?.label?.name === `${PREFIX} A2`,
326
+ "planka_update_label renames and recolors, verified: true",
327
+ renamed?.label?.name === `${PREFIX} A2` && renamed.verified === true,
321
328
  renamed
322
329
  );
323
330
 
@@ -332,8 +339,11 @@ async function run() {
332
339
  const cardId = created?.card?.id;
333
340
  if (cardId) artifacts.cards.push(cardId);
334
341
  check(
335
- "planka_create_card creates a card with 2 tasks and 1 label",
336
- Boolean(cardId) && created.tasksCreated === 2 && created.labelsAttached === 1,
342
+ "planka_create_card creates a card with 2 tasks and 1 label, verified: true",
343
+ Boolean(cardId) &&
344
+ created.verified === true &&
345
+ created.tasksCreated === 2 &&
346
+ created.labelsAttached === 1,
337
347
  created
338
348
  );
339
349
  if (!cardId) throw new Error("Without a card there is no smoke — stopping here");
@@ -345,44 +355,96 @@ async function run() {
345
355
  cardId
346
356
  );
347
357
 
348
- const details = await json("planka_get_card", { cardId });
358
+ // detail:"full" is what carries the task items and the whole description;
359
+ // the default digest returns counts and a 200-char preview.
360
+ const details = await json("planka_get_card", { cardId, detail: "full" });
349
361
  check(
350
362
  "planka_get_card returns the name, both tasks and the label",
351
363
  details?.card?.name === CARD_NAME &&
352
- details?.tasks?.length === 2 &&
364
+ details?.tasks?.total === 2 &&
365
+ details?.tasks?.lists?.[0]?.items?.length === 2 &&
353
366
  details?.labels?.length === 1,
354
367
  details
355
368
  );
356
- const taskId = details?.tasks?.[0]?.id;
369
+ const taskId = details?.tasks?.lists?.[0]?.items?.[0]?.id;
357
370
 
358
- const updated = await callTool("planka_update_card", {
371
+ const digest = await json("planka_get_card", { cardId });
372
+ check(
373
+ "the digest counts the tasks without listing them, and reads no comments",
374
+ digest?.detail === "digest" &&
375
+ digest?.tasks?.total === 2 &&
376
+ digest?.tasks?.lists?.[0]?.items === undefined &&
377
+ !Array.isArray(digest?.comments),
378
+ digest
379
+ );
380
+
381
+ const updated = await json("planka_update_card", {
359
382
  cardId,
360
383
  description: "description updated by the smoke test",
361
384
  });
362
- check("planka_update_card updates the description", !updated.isError, updated.text);
385
+ check(
386
+ "planka_update_card updates the description, verified: true",
387
+ updated?.verified === true,
388
+ updated
389
+ );
363
390
 
364
- const reread = await json("planka_get_card", { cardId });
391
+ const reread = await json("planka_get_card", { cardId, detail: "full" });
365
392
  check(
366
393
  "the update persisted (verified by re-reading)",
367
394
  reread?.card?.description === "description updated by the smoke test",
368
395
  reread?.card?.description
369
396
  );
370
397
 
371
- const taskUpdated = await callTool("planka_update_task", {
398
+ const taskUpdated = await json("planka_update_task", {
372
399
  taskId,
400
+ cardId,
373
401
  isCompleted: true,
374
402
  name: "first (done)",
375
403
  });
376
- check("planka_update_task completes and renames", !taskUpdated.isError, taskUpdated.text);
404
+ check(
405
+ "planka_update_task completes and renames, verified against the card",
406
+ taskUpdated?.verified === true && taskUpdated.task?.isCompleted === true,
407
+ taskUpdated
408
+ );
409
+
410
+ // A write that cannot be verified is NOT a success: no cardId means the task
411
+ // cannot be re-read, and the payload has to say so instead of claiming it.
412
+ const unverifiable = await callTool("planka_update_task", {
413
+ taskId,
414
+ name: "first (done)",
415
+ });
416
+ const unverifiableBody = (() => {
417
+ try {
418
+ return JSON.parse(unverifiable.text);
419
+ } catch {
420
+ return null;
421
+ }
422
+ })();
423
+ check(
424
+ "planka_update_task without cardId answers success:false, not a false green",
425
+ unverifiable.isError === true &&
426
+ unverifiableBody?.success === false &&
427
+ unverifiableBody?.verified === false &&
428
+ String(unverifiableBody?.verificationUnavailable ?? "").includes("cardId"),
429
+ unverifiableBody ?? unverifiable.text
430
+ );
377
431
 
378
- const taskAdded = await callTool("planka_create_tasks", {
432
+ const taskAdded = await json("planka_create_tasks", {
379
433
  cardId,
380
434
  tasks: ["third"],
381
435
  });
382
- check("planka_create_tasks appends a task", !taskAdded.isError, taskAdded.text);
436
+ check(
437
+ "planka_create_tasks appends a task, verified: true",
438
+ taskAdded?.verified === true && taskAdded.tasksCreated === 1,
439
+ taskAdded
440
+ );
383
441
 
384
- const taskDeleted = await callTool("planka_delete_task", { taskId });
385
- check("planka_delete_task removes the task", !taskDeleted.isError, taskDeleted.text);
442
+ const taskDeleted = await json("planka_delete_task", { taskId, cardId });
443
+ check(
444
+ "planka_delete_task removes the task, verified against the card",
445
+ taskDeleted?.verified === true,
446
+ taskDeleted
447
+ );
386
448
 
387
449
  // --- labels on the card ---------------------------------------------------
388
450
  const swapped = await json("planka_set_card_labels", {
@@ -418,7 +480,7 @@ async function run() {
418
480
  });
419
481
  check(
420
482
  "planka_list_cards with labelFilter finds the card",
421
- filtered?.cards?.some((card) => card.id === cardId),
483
+ filtered?.matches?.some((card) => card.id === cardId),
422
484
  filtered
423
485
  );
424
486
 
@@ -436,6 +498,16 @@ async function run() {
436
498
  allCards
437
499
  );
438
500
 
501
+ // The list show answers the whole column in one request: `truncated` is
502
+ // false by construction and `total` is the real size of the column.
503
+ check(
504
+ "planka_find_cards with listId reads the column whole, not a page of it",
505
+ allCards?.source === "list" &&
506
+ allCards?.truncated === false &&
507
+ allCards?.list?.id === list.id,
508
+ allCards
509
+ );
510
+
439
511
  const rawListCards = await api.get(`/api/lists/${list.id}/cards`);
440
512
  check(
441
513
  "the count matches the raw API (or exceeds one page, which is the point)",
@@ -443,10 +515,18 @@ async function run() {
443
515
  `mcp total=${totalInList} · raw first page=${rawListCards.items?.length}`
444
516
  );
445
517
 
518
+ // Cross-check against the list show, which carries every card of the column.
519
+ const rawListShow = await api.get(`/api/lists/${list.id}`);
520
+ check(
521
+ "planka_find_cards total equals the real size of the column (raw list show)",
522
+ totalInList === (rawListShow.included?.cards?.length ?? -1),
523
+ `mcp total=${totalInList} · raw list show=${rawListShow.included?.cards?.length}`
524
+ );
525
+
446
526
  check(
447
527
  "planka_list_cards omits descriptions by default",
448
- allCards?.cards?.every((card) => card.description === undefined),
449
- allCards?.cards?.[0]
528
+ allCards?.matches?.every((card) => card.description === undefined),
529
+ allCards?.matches?.[0]
450
530
  );
451
531
 
452
532
  const page = await json("planka_list_cards", {
@@ -474,7 +554,7 @@ async function run() {
474
554
  "planka_list_cards with an offset returns the next card, not the same one",
475
555
  next?.returned === 1 &&
476
556
  next.offset === 1 &&
477
- next.cards[0].id !== page.cards[0].id,
557
+ next.matches[0].id !== page.matches[0].id,
478
558
  next
479
559
  );
480
560
  } else {
@@ -495,6 +575,23 @@ async function run() {
495
575
  lists
496
576
  );
497
577
 
578
+ check(
579
+ "planka_board_summary warns instead of answering an empty cardsFrom",
580
+ await (async () => {
581
+ const typo = await json("planka_board_summary", {
582
+ boardId,
583
+ cardsFrom: [`${PREFIX} no such column`],
584
+ });
585
+ return (
586
+ Array.isArray(typo?.warnings) &&
587
+ typo.warnings.join(" ").includes(`${PREFIX} no such column`) &&
588
+ typo.warnings.join(" ").includes(list.name) &&
589
+ (typo.cards === undefined || typo.cards.length === 0)
590
+ );
591
+ })(),
592
+ "cardsFrom typo must come back as a warning, never as an empty column"
593
+ );
594
+
498
595
  check(
499
596
  "planka_list_lists is far cheaper than planka_get_board",
500
597
  JSON.stringify(lists).length * 4 < boardText.length,
@@ -554,10 +651,20 @@ async function run() {
554
651
  "planka_find_cards by label returns exactly the labelled card",
555
652
  byLabel?.total === 1 &&
556
653
  byLabel.matches[0].id === cardId &&
557
- byLabel.matches[0].list === list.name,
654
+ byLabel.matches[0].listId === list.id &&
655
+ byLabel.matches[0].listName === list.name,
558
656
  byLabel
559
657
  );
560
658
 
659
+ // The digest carries the label IDs, not only the names: set_card_labels takes
660
+ // ids, and a digest without them sent the caller back to board_summary.
661
+ check(
662
+ "planka_find_cards digest carries labelIds, so set_card_labels can run on it",
663
+ Array.isArray(byLabel?.matches?.[0]?.labelIds) &&
664
+ byLabel.matches[0].labelIds.includes(labelB.label.id),
665
+ byLabel?.matches?.[0]
666
+ );
667
+
561
668
  // The description was written by the update above and is NOT in the name:
562
669
  // if this matches, the text search really is reading descriptions.
563
670
  const byText = await json("planka_find_cards", {
@@ -578,7 +685,8 @@ async function run() {
578
685
  check(
579
686
  "planka_find_cards with listId stays inside that column",
580
687
  scoped?.matches?.length > 0 &&
581
- scoped.matches.every((card) => card.listId === list.id),
688
+ scoped.list?.id === list.id &&
689
+ scoped.source === "list",
582
690
  scoped
583
691
  );
584
692
 
@@ -589,9 +697,9 @@ async function run() {
589
697
  check(
590
698
  "planka_find_cards warns when the label does not exist and lists the ones that do",
591
699
  noLabel?.total === 0 &&
592
- typeof noLabel.warning === "string" &&
593
- noLabel.warning.includes(`${PREFIX} B`),
594
- noLabel?.warning
700
+ Array.isArray(noLabel.warnings) &&
701
+ noLabel.warnings.join(" ").includes(`${PREFIX} B`),
702
+ noLabel?.warnings
595
703
  );
596
704
 
597
705
  const noFilter = await callTool("planka_find_cards", { boardId });
@@ -601,12 +709,78 @@ async function run() {
601
709
  noFilter.text
602
710
  );
603
711
 
604
- const moved = await callTool("planka_move_card", {
712
+ const moved = await json("planka_move_card", {
605
713
  cardId,
606
714
  listId: list.id,
607
715
  position: 1,
608
716
  });
609
- check("planka_move_card repositions inside the list", !moved.isError, moved.text);
717
+ check(
718
+ "planka_move_card repositions inside the list, verified: true",
719
+ moved?.verified === true,
720
+ moved
721
+ );
722
+
723
+ // A move BETWEEN columns is the real use of the tool: a temporary column is
724
+ // created for it, the card goes there and comes back, and the raw API is the
725
+ // judge in both directions.
726
+ const moveTarget = await json("planka_create_list", {
727
+ boardId,
728
+ name: `${PREFIX} move target`,
729
+ });
730
+ if (moveTarget?.list?.id) artifacts.lists.push(moveTarget.list.id);
731
+ check(
732
+ "planka_create_list (move target) reports verified: true",
733
+ Boolean(moveTarget?.list?.id) && moveTarget.verified === true,
734
+ moveTarget
735
+ );
736
+
737
+ if (moveTarget?.list?.id) {
738
+ const movedAcross = await json("planka_move_card", {
739
+ cardId,
740
+ listId: moveTarget.list.id,
741
+ });
742
+ check(
743
+ "planka_move_card moves the card to ANOTHER column, verified: true",
744
+ movedAcross?.verified === true &&
745
+ movedAcross.card?.listId === moveTarget.list.id,
746
+ movedAcross
747
+ );
748
+
749
+ const rawAfterMove = await rawBoard();
750
+ check(
751
+ "the raw API confirms the card changed column",
752
+ rawAfterMove.cards.find((item) => item.id === cardId)?.listId ===
753
+ moveTarget.list.id,
754
+ rawAfterMove.cards.find((item) => item.id === cardId)?.listId
755
+ );
756
+
757
+ const movedBack = await json("planka_move_card", {
758
+ cardId,
759
+ listId: list.id,
760
+ });
761
+ check(
762
+ "planka_move_card brings it back to the original column, verified: true",
763
+ movedBack?.verified === true && movedBack.card?.listId === list.id,
764
+ movedBack
765
+ );
766
+
767
+ const rawAfterBack = await rawBoard();
768
+ check(
769
+ "the raw API confirms the card is back (and was NOT deleted with the column)",
770
+ rawAfterBack.cards.find((item) => item.id === cardId)?.listId === list.id,
771
+ rawAfterBack.cards.find((item) => item.id === cardId)?.listId
772
+ );
773
+
774
+ const droppedTarget = await json("planka_delete_list", {
775
+ listId: moveTarget.list.id,
776
+ });
777
+ if (droppedTarget?.verified === true) forget("lists", moveTarget.list.id);
778
+ check(
779
+ "planka_delete_list removes the temporary column, verified: true",
780
+ droppedTarget?.verified === true,
781
+ droppedTarget
782
+ );
783
+ }
610
784
 
611
785
  // --- comments: a 0 has to be a REAL 0 -------------------------------------
612
786
  const emptyMcp = await json("planka_get_comments", { cardId });
@@ -617,12 +791,16 @@ async function run() {
617
791
  `mcp=${emptyMcp?.commentCount} · raw=${emptyRaw.items?.length}`
618
792
  );
619
793
 
620
- const addedComment = await json("planka_add_comment", {
794
+ const addedComment = await json("planka_create_comment", {
621
795
  cardId,
622
796
  text: "smoke: original comment",
623
797
  });
624
798
  const commentId = addedComment?.comment?.id;
625
- check("planka_add_comment creates the comment", Boolean(commentId), addedComment);
799
+ check(
800
+ "planka_create_comment creates the comment, verified: true",
801
+ Boolean(commentId) && addedComment.verified === true,
802
+ addedComment
803
+ );
626
804
 
627
805
  const readMcp = await json("planka_get_comments", { cardId });
628
806
  check(
@@ -647,12 +825,15 @@ async function run() {
647
825
  readMcp?.comments?.[0]
648
826
  );
649
827
 
650
- const editedComment = await callTool("planka_manage_comment", {
651
- action: "update",
828
+ const editedComment = await json("planka_update_comment", {
652
829
  commentId,
653
830
  text: "smoke: edited comment",
654
831
  });
655
- check("planka_manage_comment update", !editedComment.isError, editedComment.text);
832
+ check(
833
+ "planka_update_comment, verified: true",
834
+ editedComment?.verified === true,
835
+ editedComment
836
+ );
656
837
 
657
838
  const afterEditMcp = await json("planka_get_comments", { cardId });
658
839
  const afterEditRaw = await api.getComments(cardId);
@@ -663,11 +844,12 @@ async function run() {
663
844
  `mcp=${afterEditMcp?.comments[0].text} · raw=${afterEditRaw.items[0]?.text}`
664
845
  );
665
846
 
666
- const deletedComment = await callTool("planka_manage_comment", {
667
- action: "delete",
668
- commentId,
669
- });
670
- check("planka_manage_comment delete", !deletedComment.isError, deletedComment.text);
847
+ const deletedComment = await json("planka_delete_comment", { commentId });
848
+ check(
849
+ "planka_delete_comment, verified: true",
850
+ deletedComment?.verified === true,
851
+ deletedComment
852
+ );
671
853
 
672
854
  const afterDeleteMcp = await json("planka_get_comments", { cardId });
673
855
  const afterDeleteRaw = await api.getComments(cardId);
@@ -677,6 +859,28 @@ async function run() {
677
859
  `mcp=${afterDeleteMcp?.commentCount} · raw=${afterDeleteRaw.items?.length}`
678
860
  );
679
861
 
862
+ // The two remaining deprecated aliases, on a throwaway comment: the old
863
+ // names must keep writing exactly like the tools that replaced them.
864
+ const aliasComment = await json("planka_add_comment", {
865
+ cardId,
866
+ text: "smoke: alias comment",
867
+ });
868
+ check(
869
+ "planka_add_comment (DEPRECATED alias) still creates, verified: true",
870
+ Boolean(aliasComment?.comment?.id) && aliasComment.verified === true,
871
+ aliasComment
872
+ );
873
+
874
+ const aliasDeleted = await json("planka_manage_comment", {
875
+ action: "delete",
876
+ commentId: aliasComment?.comment?.id,
877
+ });
878
+ check(
879
+ "planka_manage_comment (DEPRECATED alias) still deletes, verified: true",
880
+ aliasDeleted?.verified === true,
881
+ aliasDeleted
882
+ );
883
+
680
884
  // --- attachments: byte-for-byte roundtrip ---------------------------------
681
885
  workDir = mkdtempSync(join(tmpdir(), "planka-mcp-smoke-"));
682
886
  const filePath = join(workDir, "smoke-payload.txt");
@@ -726,14 +930,15 @@ async function run() {
726
930
  viewed.text
727
931
  );
728
932
 
729
- const deletedAttachment = await callTool("planka_delete_attachment", {
933
+ const deletedAttachment = await json("planka_delete_attachment", {
730
934
  attachmentId,
935
+ cardId,
731
936
  });
732
- if (!deletedAttachment.isError) forget("attachments", attachmentId);
937
+ if (deletedAttachment?.verified === true) forget("attachments", attachmentId);
733
938
  check(
734
- "planka_delete_attachment removes the attachment",
735
- !deletedAttachment.isError,
736
- deletedAttachment.text
939
+ "planka_delete_attachment removes the attachment, verified: true",
940
+ deletedAttachment?.verified === true,
941
+ deletedAttachment
737
942
  );
738
943
 
739
944
  const afterAttachmentDelete = await json("planka_get_attachments", { cardId });
@@ -744,42 +949,261 @@ async function run() {
744
949
  );
745
950
 
746
951
  // --- board lists ----------------------------------------------------------
747
- const tempList = await json("planka_manage_lists", {
748
- action: "create",
952
+ const tempList = await json("planka_create_list", {
749
953
  boardId,
750
954
  name: `${PREFIX} temporary list`,
751
955
  });
752
956
  if (tempList?.list?.id) artifacts.lists.push(tempList.list.id);
753
- check("planka_manage_lists create", Boolean(tempList?.list?.id), tempList);
957
+ check(
958
+ "planka_create_list, verified: true",
959
+ Boolean(tempList?.list?.id) && tempList.verified === true,
960
+ tempList
961
+ );
754
962
 
755
- const renamedList = await json("planka_manage_lists", {
756
- action: "update",
963
+ const renamedList = await json("planka_update_list", {
757
964
  listId: tempList.list.id,
758
965
  name: `${PREFIX} renamed list`,
759
966
  });
760
967
  check(
761
- "planka_manage_lists update renames",
762
- renamedList?.list?.name === `${PREFIX} renamed list`,
968
+ "planka_update_list renames, verified: true",
969
+ renamedList?.list?.name === `${PREFIX} renamed list` &&
970
+ renamedList.verified === true,
763
971
  renamedList
764
972
  );
765
973
 
766
- const deletedList = await callTool("planka_manage_lists", {
974
+ // A bulk move needs a column of its own, which is exactly what tempList is.
975
+ const strayCard = await json("planka_create_card", {
976
+ listId: tempList.list.id,
977
+ name: `${PREFIX} bulk move passenger`,
978
+ });
979
+ if (strayCard?.card?.id) artifacts.cards.push(strayCard.card.id);
980
+ check(
981
+ "a card was created in the temporary column for the bulk move",
982
+ Boolean(strayCard?.card?.id) && strayCard.verified === true,
983
+ strayCard
984
+ );
985
+
986
+ const bulkMoved = await json("planka_move_list_cards", {
987
+ fromListId: tempList.list.id,
988
+ toListId: list.id,
989
+ boardId,
990
+ });
991
+ check(
992
+ "planka_move_list_cards empties the column and the counts add up",
993
+ bulkMoved?.verified === true &&
994
+ bulkMoved?.moved === 1 &&
995
+ bulkMoved?.fromRemaining === 0,
996
+ bulkMoved
997
+ );
998
+
999
+ const rawAfterBulk = await api.get(`/api/lists/${tempList.list.id}`);
1000
+ check(
1001
+ "the raw API confirms the temporary column is empty",
1002
+ (rawAfterBulk?.included?.cards ?? []).length === 0,
1003
+ rawAfterBulk?.included?.cards?.length
1004
+ );
1005
+
1006
+ const bulkNoop = await json("planka_move_list_cards", {
1007
+ fromListId: tempList.list.id,
1008
+ toListId: list.id,
1009
+ });
1010
+ check(
1011
+ "planka_move_list_cards on an empty column moves nothing and says so",
1012
+ bulkNoop?.verified === true && bulkNoop?.moved === 0,
1013
+ bulkNoop
1014
+ );
1015
+
1016
+ // The one deprecated-alias check for lists.
1017
+ const deletedList = await json("planka_manage_lists", {
767
1018
  action: "delete",
768
1019
  listId: tempList.list.id,
769
1020
  });
770
- if (!deletedList.isError) forget("lists", tempList.list.id);
771
- check("planka_manage_lists delete", !deletedList.isError, deletedList.text);
1021
+ if (deletedList?.verified === true) forget("lists", tempList.list.id);
1022
+ check(
1023
+ "planka_manage_lists (DEPRECATED alias) still deletes, verified: true",
1024
+ deletedList?.verified === true,
1025
+ deletedList
1026
+ );
1027
+
1028
+ // --- identity -------------------------------------------------------------
1029
+ const who = await json("planka_whoami", { boardId });
1030
+ check(
1031
+ "planka_whoami names the account and the Planka version",
1032
+ Boolean(who?.user?.id) && typeof who?.plankaVersion === "string",
1033
+ who
1034
+ );
1035
+ check(
1036
+ "planka_whoami reports this session's access policy",
1037
+ who?.policy !== undefined && who?.authMode !== undefined,
1038
+ who?.policy
1039
+ );
1040
+
1041
+ const roster = await json("planka_list_users", { boardId });
1042
+ const agentUser = roster?.users?.find((user) => user.id === who?.user?.id);
1043
+ check(
1044
+ "planka_list_users lists the agent account itself",
1045
+ Boolean(agentUser) && ["instance", "board"].includes(roster?.source),
1046
+ roster
1047
+ );
1048
+ check(
1049
+ "planka_list_users hides emails unless asked",
1050
+ (roster?.users ?? []).every((user) => user.email === undefined),
1051
+ roster?.users?.[0]
1052
+ );
1053
+
1054
+ // --- card members ---------------------------------------------------------
1055
+ const assigned = await json("planka_set_card_members", {
1056
+ cardId,
1057
+ add: [who.user.id],
1058
+ });
1059
+ check(
1060
+ "planka_set_card_members assigns and re-reads the membership",
1061
+ assigned?.verified === true &&
1062
+ assigned?.finalMemberIds?.includes(who.user.id),
1063
+ assigned
1064
+ );
1065
+
1066
+ const rawAssigned = await api.getCard(cardId);
1067
+ check(
1068
+ "the raw API confirms the card membership",
1069
+ (rawAssigned?.included?.cardMemberships ?? []).some(
1070
+ (membership) => membership.userId === who.user.id
1071
+ ),
1072
+ rawAssigned?.included?.cardMemberships
1073
+ );
1074
+
1075
+ const unassigned = await json("planka_set_card_members", {
1076
+ cardId,
1077
+ remove: [who.user.id],
1078
+ });
1079
+ check(
1080
+ "planka_set_card_members unassigns and reports an empty membership",
1081
+ unassigned?.verified === true &&
1082
+ !unassigned?.finalMemberIds?.includes(who.user.id),
1083
+ unassigned
1084
+ );
1085
+
1086
+ const rawUnassigned = await api.getCard(cardId);
1087
+ check(
1088
+ "the raw API confirms the membership is gone",
1089
+ !(rawUnassigned?.included?.cardMemberships ?? []).some(
1090
+ (membership) => membership.userId === who.user.id
1091
+ ),
1092
+ rawUnassigned?.included?.cardMemberships
1093
+ );
1094
+
1095
+ // --- history: the card has been created, moved and assigned by now ---------
1096
+ const history = await json("planka_card_history", { cardId, limit: 10 });
1097
+ check(
1098
+ "planka_card_history returns the events this run generated",
1099
+ (history?.events ?? []).some((event) => event.type === "createCard") &&
1100
+ (history?.events ?? []).some((event) => event.type === "moveCard") &&
1101
+ (history?.events ?? []).some((event) => event.type === "addMemberToCard"),
1102
+ history
1103
+ );
1104
+ check(
1105
+ "planka_card_history summarises each event in one human line",
1106
+ (history?.events ?? []).every(
1107
+ (event) => typeof event.summary === "string" && event.summary.length > 0 && event.at
1108
+ ),
1109
+ history?.events?.[0]
1110
+ );
1111
+
1112
+ const activity = await json("planka_board_activity", { boardId, limit: 20 });
1113
+ const inActivity = (activity?.cards ?? []).find((entry) => entry.cardId === cardId);
1114
+ check(
1115
+ "planka_board_activity groups the scratch card's events under it",
1116
+ Boolean(inActivity) && (inActivity?.events ?? []).length > 0,
1117
+ inActivity
1118
+ );
1119
+
1120
+ // --- duplicate ------------------------------------------------------------
1121
+ const duplicate = await json("planka_duplicate_card", {
1122
+ cardId,
1123
+ name: `${PREFIX} duplicate`,
1124
+ });
1125
+ const duplicateId = duplicate?.card?.id ?? null;
1126
+ if (duplicateId) artifacts.cards.push(duplicateId);
1127
+ check(
1128
+ "planka_duplicate_card creates a verified copy",
1129
+ duplicate?.verified === true &&
1130
+ Boolean(duplicateId) &&
1131
+ duplicate?.card?.name === `${PREFIX} duplicate`,
1132
+ duplicate
1133
+ );
1134
+
1135
+ const rawDuplicate = duplicateId ? await api.getCard(duplicateId) : null;
1136
+ check(
1137
+ "the raw API confirms the copy exists in the same column",
1138
+ rawDuplicate?.item?.listId === duplicate?.card?.listId,
1139
+ rawDuplicate?.item
1140
+ );
1141
+
1142
+ // --- archive and back -----------------------------------------------------
1143
+ const beforeArchive = (await api.getCard(cardId))?.item?.listId;
1144
+ const archived = await json("planka_archive_card", { cardId });
1145
+ check(
1146
+ "planka_archive_card moves the card into the board's archive column",
1147
+ archived?.verified === true && archived?.archived === true,
1148
+ archived
1149
+ );
1150
+
1151
+ const rawArchived = await api.getCard(cardId);
1152
+ check(
1153
+ "the raw API confirms the card left its column",
1154
+ rawArchived?.item?.listId === archived?.archiveListId &&
1155
+ rawArchived?.item?.listId !== beforeArchive,
1156
+ rawArchived?.item
1157
+ );
1158
+
1159
+ const archivedAgain = await json("planka_archive_card", { cardId });
1160
+ check(
1161
+ "archiving an archived card says so instead of moving it again",
1162
+ archivedAgain?.alreadyArchived === true,
1163
+ archivedAgain
1164
+ );
1165
+
1166
+ const restored = await json("planka_archive_card", {
1167
+ cardId,
1168
+ restoreToListId: beforeArchive,
1169
+ });
1170
+ check(
1171
+ "planka_archive_card restores the card to the column it came from",
1172
+ restored?.verified === true && restored?.card?.listId === beforeArchive,
1173
+ restored
1174
+ );
1175
+
1176
+ const rawRestored = await api.getCard(cardId);
1177
+ check(
1178
+ "the raw API confirms the card is back in its column",
1179
+ rawRestored?.item?.listId === beforeArchive,
1180
+ rawRestored?.item
1181
+ );
772
1182
 
773
1183
  // --- actionable error paths ----------------------------------------------
774
- const errMissing = await callTool("planka_add_comment", { cardId });
1184
+ const errMissing = await callTool("planka_create_comment", { cardId });
775
1185
  check(
776
1186
  "ERROR: a missing required field names the tool and the field",
777
1187
  errMissing.isError &&
778
1188
  errMissing.text.includes("text") &&
779
- errMissing.text.includes("planka_add_comment"),
1189
+ errMissing.text.includes("planka_create_comment"),
780
1190
  errMissing.text
781
1191
  );
782
1192
 
1193
+ // The alias cannot declare per-action required fields, so it borrows the
1194
+ // target's. This is the check that the borrowing works.
1195
+ const errAliasMissing = await callTool("planka_manage_comment", {
1196
+ action: "update",
1197
+ commentId: "1",
1198
+ });
1199
+ check(
1200
+ "ERROR: manage_comment update without text points at planka_update_comment",
1201
+ errAliasMissing.isError &&
1202
+ errAliasMissing.text.includes("text") &&
1203
+ errAliasMissing.text.includes("planka_update_comment"),
1204
+ errAliasMissing.text
1205
+ );
1206
+
783
1207
  const errEmptyId = await callTool("planka_get_card", { cardId: "" });
784
1208
  check(
785
1209
  "ERROR: an empty id is rejected with an actionable message",
@@ -801,8 +1225,7 @@ async function run() {
801
1225
  errNoop.text
802
1226
  );
803
1227
 
804
- const errBadType = await callTool("planka_manage_lists", {
805
- action: "create",
1228
+ const errBadType = await callTool("planka_create_list", {
806
1229
  boardId,
807
1230
  name: `${PREFIX} should not exist`,
808
1231
  position: "not-a-number",
@@ -856,9 +1279,25 @@ async function run() {
856
1279
  );
857
1280
 
858
1281
  // --- teardown, verified ---------------------------------------------------
859
- const deletedCard = await callTool("planka_delete_card", { cardId });
860
- if (!deletedCard.isError) forget("cards", cardId);
861
- check("planka_delete_card deletes the card", !deletedCard.isError, deletedCard.text);
1282
+ // The copy and the bulk-move passenger are cards like any other: they have to
1283
+ // be gone before the "no artifacts left" check below can mean anything.
1284
+ for (const extraId of [duplicateId, strayCard?.card?.id].filter(Boolean)) {
1285
+ const deletedExtra = await json("planka_delete_card", { cardId: extraId });
1286
+ if (deletedExtra?.verified === true) forget("cards", extraId);
1287
+ check(
1288
+ `planka_delete_card removes the extra card ${extraId}, verified: true`,
1289
+ deletedExtra?.verified === true,
1290
+ deletedExtra
1291
+ );
1292
+ }
1293
+
1294
+ const deletedCard = await json("planka_delete_card", { cardId });
1295
+ if (deletedCard?.verified === true) forget("cards", cardId);
1296
+ check(
1297
+ "planka_delete_card deletes the card, verified: true",
1298
+ deletedCard?.verified === true,
1299
+ deletedCard
1300
+ );
862
1301
 
863
1302
  state = await rawBoard();
864
1303
  check(
@@ -868,12 +1307,13 @@ async function run() {
868
1307
  );
869
1308
 
870
1309
  for (const labelId of [labelA.label.id, labelB.label.id]) {
871
- const deletedLabel = await callTool("planka_manage_labels", {
872
- action: "delete",
873
- labelId,
874
- });
875
- if (!deletedLabel.isError) forget("labels", labelId);
876
- check(`planka_manage_labels delete (${labelId})`, !deletedLabel.isError, deletedLabel.text);
1310
+ const deletedLabel = await json("planka_delete_label", { labelId });
1311
+ if (deletedLabel?.verified === true) forget("labels", labelId);
1312
+ check(
1313
+ `planka_delete_label (${labelId}), verified: true`,
1314
+ deletedLabel?.verified === true,
1315
+ deletedLabel
1316
+ );
877
1317
  }
878
1318
 
879
1319
  state = await rawBoard();