kanban-lite 1.0.21 → 1.0.23

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 (44) hide show
  1. package/{CLAUDE.md → AGENTS.md} +13 -0
  2. package/CHANGELOG.md +68 -0
  3. package/README.md +10 -0
  4. package/dist/cli.js +168 -102
  5. package/dist/extension.js +178 -104
  6. package/dist/mcp-server.js +145 -95
  7. package/dist/sdk/index.cjs +126 -93
  8. package/dist/sdk/index.mjs +126 -93
  9. package/dist/sdk/sdk/KanbanSDK.d.ts +39 -7
  10. package/dist/sdk/shared/config.d.ts +4 -0
  11. package/dist/sdk/shared/types.d.ts +4 -0
  12. package/dist/standalone-webview/index.js +58 -58
  13. package/dist/standalone-webview/index.js.map +1 -1
  14. package/dist/standalone-webview/style.css +1 -1
  15. package/dist/standalone.js +606 -364
  16. package/dist/webview/index.js +57 -57
  17. package/dist/webview/index.js.map +1 -1
  18. package/dist/webview/style.css +1 -1
  19. package/docs/plans/2026-02-26-settings-tabs-design.md +40 -0
  20. package/docs/plans/2026-02-26-settings-tabs.md +166 -0
  21. package/docs/plans/2026-02-27-zoom-settings-design.md +82 -0
  22. package/docs/plans/2026-02-27-zoom-settings.md +395 -0
  23. package/docs/sdk.md +3 -6
  24. package/package.json +1 -1
  25. package/src/cli/index.ts +12 -2
  26. package/src/extension/KanbanPanel.ts +25 -5
  27. package/src/mcp-server/index.ts +20 -2
  28. package/src/sdk/KanbanSDK.ts +64 -7
  29. package/src/sdk/__tests__/KanbanSDK.test.ts +17 -1
  30. package/src/sdk/__tests__/metadata.test.ts +3 -1
  31. package/src/sdk/__tests__/multi-board.test.ts +2 -0
  32. package/src/sdk/parser.ts +50 -83
  33. package/src/shared/config.ts +14 -2
  34. package/src/shared/types.ts +4 -0
  35. package/src/standalone/__tests__/server.integration.test.ts +2 -2
  36. package/src/standalone/index.ts +7 -4
  37. package/src/standalone/server.ts +31 -6
  38. package/src/webview/App.tsx +42 -3
  39. package/src/webview/assets/main.css +31 -2
  40. package/src/webview/components/KanbanBoard.tsx +35 -3
  41. package/src/webview/components/KanbanColumn.tsx +40 -4
  42. package/src/webview/components/SettingsPanel.tsx +179 -77
  43. package/src/webview/components/Toolbar.tsx +127 -32
  44. package/src/webview/store/index.ts +26 -28
@@ -302,6 +302,8 @@ var DEFAULT_CONFIG = {
302
302
  compactMode: false,
303
303
  markdownEditorMode: false,
304
304
  showDeletedColumn: false,
305
+ boardZoom: 100,
306
+ cardZoom: 100,
305
307
  port: 3e3,
306
308
  labels: {}
307
309
  };
@@ -352,6 +354,8 @@ function migrateConfigV1ToV2(raw) {
352
354
  compactMode: v1.compactMode,
353
355
  markdownEditorMode: v1.markdownEditorMode,
354
356
  showDeletedColumn: false,
357
+ boardZoom: 100,
358
+ cardZoom: 100,
355
359
  port: 3e3
356
360
  };
357
361
  }
@@ -425,7 +429,9 @@ function configToSettings(config) {
425
429
  markdownEditorMode: config.markdownEditorMode,
426
430
  showDeletedColumn: config.showDeletedColumn,
427
431
  defaultPriority: config.defaultPriority,
428
- defaultStatus: config.defaultStatus
432
+ defaultStatus: config.defaultStatus,
433
+ boardZoom: config.boardZoom ?? 100,
434
+ cardZoom: config.cardZoom ?? 100
429
435
  };
430
436
  }
431
437
  function settingsToConfig(config, settings) {
@@ -439,7 +445,9 @@ function settingsToConfig(config, settings) {
439
445
  compactMode: settings.compactMode,
440
446
  showDeletedColumn: settings.showDeletedColumn,
441
447
  defaultPriority: settings.defaultPriority,
442
- defaultStatus: settings.defaultStatus
448
+ defaultStatus: settings.defaultStatus,
449
+ boardZoom: settings.boardZoom,
450
+ cardZoom: settings.cardZoom
443
451
  };
444
452
  }
445
453
 
@@ -3099,6 +3107,7 @@ function renamed(from, to) {
3099
3107
  throw new Error("Function yaml." + from + " is removed in js-yaml 4. Use yaml." + to + " instead, which is now safe by default.");
3100
3108
  };
3101
3109
  }
3110
+ var JSON_SCHEMA = json;
3102
3111
  var load = loader.load;
3103
3112
  var loadAll = loader.loadAll;
3104
3113
  var dump = dumper.dump;
@@ -3137,48 +3146,26 @@ function parseFeatureFile(content, filePath) {
3137
3146
  return null;
3138
3147
  const frontmatter = frontmatterMatch[1];
3139
3148
  const rest = frontmatterMatch[2] || "";
3140
- const getValue = (key) => {
3141
- const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
3142
- if (!match)
3149
+ let parsed;
3150
+ try {
3151
+ const loaded = load(frontmatter, { schema: JSON_SCHEMA });
3152
+ if (!loaded || typeof loaded !== "object" || Array.isArray(loaded))
3153
+ return null;
3154
+ parsed = loaded;
3155
+ } catch {
3156
+ return null;
3157
+ }
3158
+ const str2 = (key) => {
3159
+ const val = parsed[key];
3160
+ if (val == null)
3143
3161
  return "";
3144
- const value = match[1].trim().replace(/^["']|["']$/g, "");
3145
- return value === "null" ? "" : value;
3162
+ return String(val);
3146
3163
  };
3147
- const getArrayValue = (key) => {
3148
- const match = frontmatter.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, "m"));
3149
- if (!match)
3164
+ const arr = (key) => {
3165
+ const val = parsed[key];
3166
+ if (!Array.isArray(val))
3150
3167
  return [];
3151
- return match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
3152
- };
3153
- const getMetadata = () => {
3154
- const lines = frontmatter.split("\n");
3155
- let metaStart = -1;
3156
- for (let j = 0; j < lines.length; j++) {
3157
- if (/^metadata:\s*$/.test(lines[j])) {
3158
- metaStart = j + 1;
3159
- break;
3160
- }
3161
- }
3162
- if (metaStart === -1)
3163
- return void 0;
3164
- const indentedLines = [];
3165
- for (let j = metaStart; j < lines.length; j++) {
3166
- if (/^\s/.test(lines[j]) || lines[j].trim() === "") {
3167
- indentedLines.push(lines[j]);
3168
- } else {
3169
- break;
3170
- }
3171
- }
3172
- if (indentedLines.length === 0)
3173
- return void 0;
3174
- try {
3175
- const parsed = load(indentedLines.join("\n"));
3176
- if (parsed && typeof parsed === "object")
3177
- return parsed;
3178
- return void 0;
3179
- } catch {
3180
- return void 0;
3181
- }
3168
+ return val.filter((v) => v != null).map(String);
3182
3169
  };
3183
3170
  const sections = rest.split(/\n---\n/);
3184
3171
  let body = sections[0] || "";
@@ -3199,22 +3186,23 @@ ${section}`;
3199
3186
  i += 1;
3200
3187
  }
3201
3188
  }
3202
- const meta = getMetadata();
3203
- const actions = getArrayValue("actions");
3189
+ const actions = arr("actions");
3190
+ const rawMeta = parsed.metadata;
3191
+ const meta = rawMeta != null && typeof rawMeta === "object" && !Array.isArray(rawMeta) ? rawMeta : void 0;
3204
3192
  return {
3205
- version: parseInt(getValue("version"), 10) || 0,
3206
- id: getValue("id") || extractIdFromFilename(filePath),
3207
- status: getValue("status") || "backlog",
3208
- priority: getValue("priority") || "medium",
3209
- assignee: getValue("assignee") || null,
3210
- dueDate: getValue("dueDate") || null,
3211
- created: getValue("created") || (/* @__PURE__ */ new Date()).toISOString(),
3212
- modified: getValue("modified") || (/* @__PURE__ */ new Date()).toISOString(),
3213
- completedAt: getValue("completedAt") || null,
3214
- labels: getArrayValue("labels"),
3215
- attachments: getArrayValue("attachments"),
3193
+ version: typeof parsed.version === "number" ? parsed.version : parseInt(str2("version"), 10) || 0,
3194
+ id: str2("id") || extractIdFromFilename(filePath),
3195
+ status: str2("status") || "backlog",
3196
+ priority: str2("priority") || "medium",
3197
+ assignee: parsed.assignee != null ? String(parsed.assignee) : null,
3198
+ dueDate: parsed.dueDate != null ? String(parsed.dueDate) : null,
3199
+ created: str2("created") || (/* @__PURE__ */ new Date()).toISOString(),
3200
+ modified: str2("modified") || (/* @__PURE__ */ new Date()).toISOString(),
3201
+ completedAt: parsed.completedAt != null ? String(parsed.completedAt) : null,
3202
+ labels: arr("labels"),
3203
+ attachments: arr("attachments"),
3216
3204
  comments,
3217
- order: getValue("order") || "a0",
3205
+ order: str2("order") || "a0",
3218
3206
  content: body.trim(),
3219
3207
  ...meta ? { metadata: meta } : {},
3220
3208
  ...actions.length > 0 ? { actions } : {},
@@ -3222,37 +3210,28 @@ ${section}`;
3222
3210
  };
3223
3211
  }
3224
3212
  function serializeFeature(feature) {
3225
- const lines = [
3226
- "---",
3227
- `version: ${feature.version ?? CARD_FORMAT_VERSION}`,
3228
- `id: "${feature.id}"`,
3229
- `status: "${feature.status}"`,
3230
- `priority: "${feature.priority}"`,
3231
- `assignee: ${feature.assignee ? `"${feature.assignee}"` : "null"}`,
3232
- `dueDate: ${feature.dueDate ? `"${feature.dueDate}"` : "null"}`,
3233
- `created: "${feature.created}"`,
3234
- `modified: "${feature.modified}"`,
3235
- `completedAt: ${feature.completedAt ? `"${feature.completedAt}"` : "null"}`,
3236
- `labels: [${feature.labels.map((l) => `"${l}"`).join(", ")}]`,
3237
- `attachments: [${(feature.attachments || []).map((a) => `"${a}"`).join(", ")}]`,
3238
- `order: "${feature.order}"`
3239
- ];
3240
- if (feature.actions && feature.actions.length > 0) {
3241
- lines.push(`actions: [${feature.actions.map((a) => `"${a}"`).join(", ")}]`);
3242
- }
3243
- if (feature.metadata && Object.keys(feature.metadata).length > 0) {
3244
- const metaYaml = dump(feature.metadata, { indent: 2, lineWidth: -1 });
3245
- lines.push("metadata:");
3246
- for (const line of metaYaml.trimEnd().split("\n")) {
3247
- lines.push(" " + line);
3248
- }
3249
- }
3250
- lines.push("---");
3251
- lines.push("");
3252
- const frontmatter = lines.join("\n");
3253
- let result = frontmatter + feature.content;
3254
- const comments = feature.comments || [];
3255
- for (const comment of comments) {
3213
+ const frontmatterObj = {
3214
+ version: feature.version ?? CARD_FORMAT_VERSION,
3215
+ id: feature.id,
3216
+ status: feature.status,
3217
+ priority: feature.priority,
3218
+ assignee: feature.assignee ?? null,
3219
+ dueDate: feature.dueDate ?? null,
3220
+ created: feature.created,
3221
+ modified: feature.modified,
3222
+ completedAt: feature.completedAt ?? null,
3223
+ labels: feature.labels,
3224
+ attachments: feature.attachments || [],
3225
+ order: feature.order,
3226
+ ...feature.actions?.length ? { actions: feature.actions } : {},
3227
+ ...feature.metadata && Object.keys(feature.metadata).length > 0 ? { metadata: feature.metadata } : {}
3228
+ };
3229
+ const yamlStr = dump(frontmatterObj, { lineWidth: -1, quotingType: '"', forceQuotes: true });
3230
+ let result = `---
3231
+ ${yamlStr}---
3232
+
3233
+ ${feature.content}`;
3234
+ for (const comment of feature.comments || []) {
3256
3235
  result += "\n\n---\n";
3257
3236
  result += `comment: true
3258
3237
  `;
@@ -4245,25 +4224,28 @@ var KanbanSDK = class {
4245
4224
  writeConfig(this.workspaceRoot, config);
4246
4225
  }
4247
4226
  /**
4248
- * Removes a label definition from the workspace configuration.
4249
- *
4250
- * This only removes the color/group definition — cards that use this
4251
- * label keep their label strings. Those labels will render with default
4252
- * gray styling in the UI.
4227
+ * Removes a label definition from the workspace configuration and cascades
4228
+ * the deletion to all cards by removing the label from their `labels` array.
4253
4229
  *
4254
4230
  * @param name - The label name to remove.
4255
4231
  *
4256
4232
  * @example
4257
4233
  * ```ts
4258
- * sdk.deleteLabel('bug')
4234
+ * await sdk.deleteLabel('bug')
4259
4235
  * ```
4260
4236
  */
4261
- deleteLabel(name) {
4237
+ async deleteLabel(name) {
4262
4238
  const config = readConfig(this.workspaceRoot);
4263
4239
  if (config.labels) {
4264
4240
  delete config.labels[name];
4265
4241
  writeConfig(this.workspaceRoot, config);
4266
4242
  }
4243
+ const cards = await this.listCards();
4244
+ for (const card of cards) {
4245
+ if (card.labels.includes(name)) {
4246
+ await this.updateCard(card.id, { labels: card.labels.filter((l) => l !== name) });
4247
+ }
4248
+ }
4267
4249
  }
4268
4250
  /**
4269
4251
  * Renames a label in the configuration and cascades the change to all cards.
@@ -4688,6 +4670,57 @@ var KanbanSDK = class {
4688
4670
  this.emitEvent("column.deleted", removed);
4689
4671
  return board.columns;
4690
4672
  }
4673
+ /**
4674
+ * Moves all cards in the specified column to the `deleted` (soft-delete) column.
4675
+ *
4676
+ * This is a non-destructive operation — cards are moved to the reserved
4677
+ * `deleted` status and can be restored or permanently deleted later.
4678
+ * The column itself is not removed.
4679
+ *
4680
+ * @param columnId - The ID of the column whose cards should be moved to `deleted`.
4681
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
4682
+ * @returns A promise resolving to the number of cards that were moved.
4683
+ * @throws {Error} If the column is `'deleted'` (no-op protection).
4684
+ *
4685
+ * @example
4686
+ * ```ts
4687
+ * const moved = await sdk.cleanupColumn('blocked')
4688
+ * console.log(`Moved ${moved} cards to deleted`)
4689
+ * ```
4690
+ */
4691
+ async cleanupColumn(columnId, boardId) {
4692
+ if (columnId === DELETED_STATUS_ID)
4693
+ return 0;
4694
+ const cards = await this.listCards(void 0, boardId);
4695
+ const cardsToMove = cards.filter((c) => c.status === columnId);
4696
+ for (const card of cardsToMove) {
4697
+ await this.moveCard(card.id, DELETED_STATUS_ID, 0, boardId);
4698
+ }
4699
+ return cardsToMove.length;
4700
+ }
4701
+ /**
4702
+ * Permanently deletes all cards currently in the `deleted` column.
4703
+ *
4704
+ * This is equivalent to "empty trash". All soft-deleted cards are
4705
+ * removed from disk. This operation cannot be undone.
4706
+ *
4707
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
4708
+ * @returns A promise resolving to the number of cards that were permanently deleted.
4709
+ *
4710
+ * @example
4711
+ * ```ts
4712
+ * const count = await sdk.purgeDeletedCards()
4713
+ * console.log(`Permanently deleted ${count} cards`)
4714
+ * ```
4715
+ */
4716
+ async purgeDeletedCards(boardId) {
4717
+ const cards = await this.listCards(void 0, boardId);
4718
+ const deleted = cards.filter((c) => c.status === DELETED_STATUS_ID);
4719
+ for (const card of deleted) {
4720
+ await this.permanentlyDeleteCard(card.id, boardId);
4721
+ }
4722
+ return deleted.length;
4723
+ }
4691
4724
  /**
4692
4725
  * Reorders the columns of a board.
4693
4726
  *
@@ -5657,6 +5690,23 @@ async function main() {
5657
5690
  };
5658
5691
  }
5659
5692
  );
5693
+ server.tool(
5694
+ "cleanup_column",
5695
+ "Move all cards in a column to the deleted (soft-delete) column. The column itself is kept.",
5696
+ {
5697
+ boardId: import_zod.z.string().optional().describe("Board ID (uses default board if omitted)"),
5698
+ columnId: import_zod.z.string().describe("Column ID to clean up")
5699
+ },
5700
+ async ({ boardId, columnId }) => {
5701
+ const moved = await sdk.cleanupColumn(columnId, boardId);
5702
+ return {
5703
+ content: [{
5704
+ type: "text",
5705
+ text: `Moved ${moved} card${moved === 1 ? "" : "s"} from "${columnId}" to deleted`
5706
+ }]
5707
+ };
5708
+ }
5709
+ );
5660
5710
  server.tool("list_labels", "List all label definitions with colors and groups", {
5661
5711
  boardId: import_zod.z.string().optional().describe("Board ID")
5662
5712
  }, async () => {
@@ -5678,10 +5728,10 @@ async function main() {
5678
5728
  await sdk.renameLabel(oldName, newName);
5679
5729
  return { content: [{ type: "text", text: `Label "${oldName}" renamed to "${newName}"` }] };
5680
5730
  });
5681
- server.tool("delete_label", "Remove a label definition (cards keep the label text)", {
5731
+ server.tool("delete_label", "Remove a label definition and remove it from all cards", {
5682
5732
  name: import_zod.z.string().describe("Label name to remove")
5683
5733
  }, async ({ name }) => {
5684
- sdk.deleteLabel(name);
5734
+ await sdk.deleteLabel(name);
5685
5735
  return { content: [{ type: "text", text: `Label "${name}" definition removed` }] };
5686
5736
  });
5687
5737
  server.tool(
@@ -325,6 +325,8 @@ var DEFAULT_CONFIG = {
325
325
  compactMode: false,
326
326
  markdownEditorMode: false,
327
327
  showDeletedColumn: false,
328
+ boardZoom: 100,
329
+ cardZoom: 100,
328
330
  port: 3e3,
329
331
  labels: {}
330
332
  };
@@ -375,6 +377,8 @@ function migrateConfigV1ToV2(raw) {
375
377
  compactMode: v1.compactMode,
376
378
  markdownEditorMode: v1.markdownEditorMode,
377
379
  showDeletedColumn: false,
380
+ boardZoom: 100,
381
+ cardZoom: 100,
378
382
  port: 3e3
379
383
  };
380
384
  }
@@ -452,7 +456,9 @@ function configToSettings(config) {
452
456
  markdownEditorMode: config.markdownEditorMode,
453
457
  showDeletedColumn: config.showDeletedColumn,
454
458
  defaultPriority: config.defaultPriority,
455
- defaultStatus: config.defaultStatus
459
+ defaultStatus: config.defaultStatus,
460
+ boardZoom: config.boardZoom ?? 100,
461
+ cardZoom: config.cardZoom ?? 100
456
462
  };
457
463
  }
458
464
  function settingsToConfig(config, settings) {
@@ -466,7 +472,9 @@ function settingsToConfig(config, settings) {
466
472
  compactMode: settings.compactMode,
467
473
  showDeletedColumn: settings.showDeletedColumn,
468
474
  defaultPriority: settings.defaultPriority,
469
- defaultStatus: settings.defaultStatus
475
+ defaultStatus: settings.defaultStatus,
476
+ boardZoom: settings.boardZoom,
477
+ cardZoom: settings.cardZoom
470
478
  };
471
479
  }
472
480
 
@@ -3126,6 +3134,7 @@ function renamed(from, to) {
3126
3134
  throw new Error("Function yaml." + from + " is removed in js-yaml 4. Use yaml." + to + " instead, which is now safe by default.");
3127
3135
  };
3128
3136
  }
3137
+ var JSON_SCHEMA = json;
3129
3138
  var load = loader.load;
3130
3139
  var loadAll = loader.loadAll;
3131
3140
  var dump = dumper.dump;
@@ -3164,48 +3173,26 @@ function parseFeatureFile(content, filePath) {
3164
3173
  return null;
3165
3174
  const frontmatter = frontmatterMatch[1];
3166
3175
  const rest = frontmatterMatch[2] || "";
3167
- const getValue = (key) => {
3168
- const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
3169
- if (!match)
3176
+ let parsed;
3177
+ try {
3178
+ const loaded = load(frontmatter, { schema: JSON_SCHEMA });
3179
+ if (!loaded || typeof loaded !== "object" || Array.isArray(loaded))
3180
+ return null;
3181
+ parsed = loaded;
3182
+ } catch {
3183
+ return null;
3184
+ }
3185
+ const str2 = (key) => {
3186
+ const val = parsed[key];
3187
+ if (val == null)
3170
3188
  return "";
3171
- const value = match[1].trim().replace(/^["']|["']$/g, "");
3172
- return value === "null" ? "" : value;
3189
+ return String(val);
3173
3190
  };
3174
- const getArrayValue = (key) => {
3175
- const match = frontmatter.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, "m"));
3176
- if (!match)
3191
+ const arr = (key) => {
3192
+ const val = parsed[key];
3193
+ if (!Array.isArray(val))
3177
3194
  return [];
3178
- return match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
3179
- };
3180
- const getMetadata = () => {
3181
- const lines = frontmatter.split("\n");
3182
- let metaStart = -1;
3183
- for (let j = 0; j < lines.length; j++) {
3184
- if (/^metadata:\s*$/.test(lines[j])) {
3185
- metaStart = j + 1;
3186
- break;
3187
- }
3188
- }
3189
- if (metaStart === -1)
3190
- return void 0;
3191
- const indentedLines = [];
3192
- for (let j = metaStart; j < lines.length; j++) {
3193
- if (/^\s/.test(lines[j]) || lines[j].trim() === "") {
3194
- indentedLines.push(lines[j]);
3195
- } else {
3196
- break;
3197
- }
3198
- }
3199
- if (indentedLines.length === 0)
3200
- return void 0;
3201
- try {
3202
- const parsed = load(indentedLines.join("\n"));
3203
- if (parsed && typeof parsed === "object")
3204
- return parsed;
3205
- return void 0;
3206
- } catch {
3207
- return void 0;
3208
- }
3195
+ return val.filter((v) => v != null).map(String);
3209
3196
  };
3210
3197
  const sections = rest.split(/\n---\n/);
3211
3198
  let body = sections[0] || "";
@@ -3226,22 +3213,23 @@ ${section}`;
3226
3213
  i += 1;
3227
3214
  }
3228
3215
  }
3229
- const meta = getMetadata();
3230
- const actions = getArrayValue("actions");
3216
+ const actions = arr("actions");
3217
+ const rawMeta = parsed.metadata;
3218
+ const meta = rawMeta != null && typeof rawMeta === "object" && !Array.isArray(rawMeta) ? rawMeta : void 0;
3231
3219
  return {
3232
- version: parseInt(getValue("version"), 10) || 0,
3233
- id: getValue("id") || extractIdFromFilename(filePath),
3234
- status: getValue("status") || "backlog",
3235
- priority: getValue("priority") || "medium",
3236
- assignee: getValue("assignee") || null,
3237
- dueDate: getValue("dueDate") || null,
3238
- created: getValue("created") || (/* @__PURE__ */ new Date()).toISOString(),
3239
- modified: getValue("modified") || (/* @__PURE__ */ new Date()).toISOString(),
3240
- completedAt: getValue("completedAt") || null,
3241
- labels: getArrayValue("labels"),
3242
- attachments: getArrayValue("attachments"),
3220
+ version: typeof parsed.version === "number" ? parsed.version : parseInt(str2("version"), 10) || 0,
3221
+ id: str2("id") || extractIdFromFilename(filePath),
3222
+ status: str2("status") || "backlog",
3223
+ priority: str2("priority") || "medium",
3224
+ assignee: parsed.assignee != null ? String(parsed.assignee) : null,
3225
+ dueDate: parsed.dueDate != null ? String(parsed.dueDate) : null,
3226
+ created: str2("created") || (/* @__PURE__ */ new Date()).toISOString(),
3227
+ modified: str2("modified") || (/* @__PURE__ */ new Date()).toISOString(),
3228
+ completedAt: parsed.completedAt != null ? String(parsed.completedAt) : null,
3229
+ labels: arr("labels"),
3230
+ attachments: arr("attachments"),
3243
3231
  comments,
3244
- order: getValue("order") || "a0",
3232
+ order: str2("order") || "a0",
3245
3233
  content: body.trim(),
3246
3234
  ...meta ? { metadata: meta } : {},
3247
3235
  ...actions.length > 0 ? { actions } : {},
@@ -3249,37 +3237,28 @@ ${section}`;
3249
3237
  };
3250
3238
  }
3251
3239
  function serializeFeature(feature) {
3252
- const lines = [
3253
- "---",
3254
- `version: ${feature.version ?? CARD_FORMAT_VERSION}`,
3255
- `id: "${feature.id}"`,
3256
- `status: "${feature.status}"`,
3257
- `priority: "${feature.priority}"`,
3258
- `assignee: ${feature.assignee ? `"${feature.assignee}"` : "null"}`,
3259
- `dueDate: ${feature.dueDate ? `"${feature.dueDate}"` : "null"}`,
3260
- `created: "${feature.created}"`,
3261
- `modified: "${feature.modified}"`,
3262
- `completedAt: ${feature.completedAt ? `"${feature.completedAt}"` : "null"}`,
3263
- `labels: [${feature.labels.map((l) => `"${l}"`).join(", ")}]`,
3264
- `attachments: [${(feature.attachments || []).map((a) => `"${a}"`).join(", ")}]`,
3265
- `order: "${feature.order}"`
3266
- ];
3267
- if (feature.actions && feature.actions.length > 0) {
3268
- lines.push(`actions: [${feature.actions.map((a) => `"${a}"`).join(", ")}]`);
3269
- }
3270
- if (feature.metadata && Object.keys(feature.metadata).length > 0) {
3271
- const metaYaml = dump(feature.metadata, { indent: 2, lineWidth: -1 });
3272
- lines.push("metadata:");
3273
- for (const line of metaYaml.trimEnd().split("\n")) {
3274
- lines.push(" " + line);
3275
- }
3276
- }
3277
- lines.push("---");
3278
- lines.push("");
3279
- const frontmatter = lines.join("\n");
3280
- let result = frontmatter + feature.content;
3281
- const comments = feature.comments || [];
3282
- for (const comment of comments) {
3240
+ const frontmatterObj = {
3241
+ version: feature.version ?? CARD_FORMAT_VERSION,
3242
+ id: feature.id,
3243
+ status: feature.status,
3244
+ priority: feature.priority,
3245
+ assignee: feature.assignee ?? null,
3246
+ dueDate: feature.dueDate ?? null,
3247
+ created: feature.created,
3248
+ modified: feature.modified,
3249
+ completedAt: feature.completedAt ?? null,
3250
+ labels: feature.labels,
3251
+ attachments: feature.attachments || [],
3252
+ order: feature.order,
3253
+ ...feature.actions?.length ? { actions: feature.actions } : {},
3254
+ ...feature.metadata && Object.keys(feature.metadata).length > 0 ? { metadata: feature.metadata } : {}
3255
+ };
3256
+ const yamlStr = dump(frontmatterObj, { lineWidth: -1, quotingType: '"', forceQuotes: true });
3257
+ let result = `---
3258
+ ${yamlStr}---
3259
+
3260
+ ${feature.content}`;
3261
+ for (const comment of feature.comments || []) {
3283
3262
  result += "\n\n---\n";
3284
3263
  result += `comment: true
3285
3264
  `;
@@ -4272,25 +4251,28 @@ var KanbanSDK = class {
4272
4251
  writeConfig(this.workspaceRoot, config);
4273
4252
  }
4274
4253
  /**
4275
- * Removes a label definition from the workspace configuration.
4276
- *
4277
- * This only removes the color/group definition — cards that use this
4278
- * label keep their label strings. Those labels will render with default
4279
- * gray styling in the UI.
4254
+ * Removes a label definition from the workspace configuration and cascades
4255
+ * the deletion to all cards by removing the label from their `labels` array.
4280
4256
  *
4281
4257
  * @param name - The label name to remove.
4282
4258
  *
4283
4259
  * @example
4284
4260
  * ```ts
4285
- * sdk.deleteLabel('bug')
4261
+ * await sdk.deleteLabel('bug')
4286
4262
  * ```
4287
4263
  */
4288
- deleteLabel(name) {
4264
+ async deleteLabel(name) {
4289
4265
  const config = readConfig(this.workspaceRoot);
4290
4266
  if (config.labels) {
4291
4267
  delete config.labels[name];
4292
4268
  writeConfig(this.workspaceRoot, config);
4293
4269
  }
4270
+ const cards = await this.listCards();
4271
+ for (const card of cards) {
4272
+ if (card.labels.includes(name)) {
4273
+ await this.updateCard(card.id, { labels: card.labels.filter((l) => l !== name) });
4274
+ }
4275
+ }
4294
4276
  }
4295
4277
  /**
4296
4278
  * Renames a label in the configuration and cascades the change to all cards.
@@ -4715,6 +4697,57 @@ var KanbanSDK = class {
4715
4697
  this.emitEvent("column.deleted", removed);
4716
4698
  return board.columns;
4717
4699
  }
4700
+ /**
4701
+ * Moves all cards in the specified column to the `deleted` (soft-delete) column.
4702
+ *
4703
+ * This is a non-destructive operation — cards are moved to the reserved
4704
+ * `deleted` status and can be restored or permanently deleted later.
4705
+ * The column itself is not removed.
4706
+ *
4707
+ * @param columnId - The ID of the column whose cards should be moved to `deleted`.
4708
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
4709
+ * @returns A promise resolving to the number of cards that were moved.
4710
+ * @throws {Error} If the column is `'deleted'` (no-op protection).
4711
+ *
4712
+ * @example
4713
+ * ```ts
4714
+ * const moved = await sdk.cleanupColumn('blocked')
4715
+ * console.log(`Moved ${moved} cards to deleted`)
4716
+ * ```
4717
+ */
4718
+ async cleanupColumn(columnId, boardId) {
4719
+ if (columnId === DELETED_STATUS_ID)
4720
+ return 0;
4721
+ const cards = await this.listCards(void 0, boardId);
4722
+ const cardsToMove = cards.filter((c) => c.status === columnId);
4723
+ for (const card of cardsToMove) {
4724
+ await this.moveCard(card.id, DELETED_STATUS_ID, 0, boardId);
4725
+ }
4726
+ return cardsToMove.length;
4727
+ }
4728
+ /**
4729
+ * Permanently deletes all cards currently in the `deleted` column.
4730
+ *
4731
+ * This is equivalent to "empty trash". All soft-deleted cards are
4732
+ * removed from disk. This operation cannot be undone.
4733
+ *
4734
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
4735
+ * @returns A promise resolving to the number of cards that were permanently deleted.
4736
+ *
4737
+ * @example
4738
+ * ```ts
4739
+ * const count = await sdk.purgeDeletedCards()
4740
+ * console.log(`Permanently deleted ${count} cards`)
4741
+ * ```
4742
+ */
4743
+ async purgeDeletedCards(boardId) {
4744
+ const cards = await this.listCards(void 0, boardId);
4745
+ const deleted = cards.filter((c) => c.status === DELETED_STATUS_ID);
4746
+ for (const card of deleted) {
4747
+ await this.permanentlyDeleteCard(card.id, boardId);
4748
+ }
4749
+ return deleted.length;
4750
+ }
4718
4751
  /**
4719
4752
  * Reorders the columns of a board.
4720
4753
  *