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
@@ -41,6 +41,17 @@ npm test # Run tests (vitest)
41
41
 
42
42
  All three interfaces (API, CLI, MCP) support the same operations: cards CRUD, columns CRUD, settings get/update, webhooks CRUD, workspace info. When adding new functionality, add it to all three.
43
43
 
44
+ ## Implementation Order
45
+
46
+ Always follow this order when adding or modifying any feature:
47
+
48
+ 1. **SDK first** — implement the core logic in `src/sdk/KanbanSDK.ts` (or supporting SDK files) with full JSDoc comments before touching any interface layer.
49
+ 2. **API** — add or update the corresponding REST endpoint(s) in `src/standalone/server.ts`.
50
+ 3. **CLI** — add or update the command in `src/cli/index.ts`.
51
+ 4. **MCP** — add or update the tool definition in `src/mcp-server/index.ts`.
52
+
53
+ Never implement a feature directly in an interface layer without the SDK method existing first. The SDK is the single source of truth for all business logic.
54
+
44
55
  ## Data Storage
45
56
 
46
57
  - Cards: `.kanban/{status}/card-name-YYYY-MM-DD.md` (markdown with YAML frontmatter)
@@ -73,6 +84,8 @@ metadata:
73
84
 
74
85
  - **JSDoc:** Always update JSDoc comments when changing any logic, parameters, return types, or behavior of functions, methods, interfaces, or types. JSDoc is the source of truth for `docs/sdk.md`.
75
86
  - **Generated docs:** `docs/api.md`, `docs/sdk.md`, and `docs/webhooks.md` are autogenerated. Never edit them manually. Update the source (JSDoc for SDK, route metadata in `scripts/generate-api-docs.ts` for API, `scripts/generate-webhooks-docs.ts` for webhooks) and run `npm run docs` to regenerate.
87
+ - **README.md:** Always update `README.md` when adding or modifying user-facing features. Update the relevant Features section, configuration examples, CLI usage examples, REST API tables, and MCP tool table as appropriate.
88
+ - **CHANGELOG.md:** Always add an entry to `CHANGELOG.md` for every new feature, behaviour change, or bug fix. Place it under an `## [Unreleased]` section at the top (or the current in-progress version block). Follow the existing format: `### Added`, `### Changed`, `### Fixed` subsections with concise bullet points.
76
89
 
77
90
  ## Conventions
78
91
 
package/CHANGELOG.md CHANGED
@@ -5,6 +5,74 @@ All notable changes to the Kanban Lite extension will be documented in this file
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.1.0] - 2026-02-27
9
+
10
+ ### Added
11
+ - Board and card detail zoom settings with slider UI (75–150%) stored in `.kanban.json`
12
+ - Keyboard shortcuts for adjusting board/card zoom level (Ctrl/Cmd `+`/`-`)
13
+ - CSS custom properties (`--board-zoom`, `--card-zoom`) with `calc()` multipliers for smooth font scaling
14
+ - Smooth scrolling to the selected feature card in the kanban board
15
+ - Sorting options in the column context menu
16
+ - Default zoom level configuration for both board and card detail views
17
+
18
+ ## [2.0.0] - 2026-02-26
19
+
20
+ ### Added
21
+ - Per-card actions (named string labels) that trigger a global `actionWebhookUrl` via `POST` on demand
22
+ - Run Actions dropdown in the card editor and action input in CreateFeatureDialog
23
+ - `triggerAction` method in KanbanSDK with full support across REST API, WebSocket, MCP (`trigger_action` tool), and CLI (`--actions` flag)
24
+ - Comment editor component with Write / Preview tabs and a markdown formatting toolbar
25
+ - GitHub-style comment editing using the new CommentEditor in CommentsSection
26
+ - Settings panel split into three tabs: **General**, **Defaults**, and **Labels**
27
+ - `version` field on card frontmatter schema for format tracking
28
+ - Metadata filtering for card list/search operations across all interfaces
29
+ - Creation and modification date display with hover tooltips on FeatureCard and FeatureEditor
30
+ - Sort order filter for card queries
31
+
32
+ ### Fixed
33
+ - `version` field now included in all FeatureFrontmatter constructions in the server
34
+
35
+ ## [1.9.0] - 2026-02-25
36
+
37
+ ### Added
38
+ - Card metadata support — arbitrary key-value data stored as a native YAML block in frontmatter (`metadata` field)
39
+ - Metadata UI: key-count chip `{N}` on card grid and collapsible tree view in the card detail panel
40
+ - Label definitions with color picker in the Settings panel (create, rename, delete labels)
41
+ - Colored labels rendered on cards, in the editor, create dialog, and toolbar
42
+ - Label group filtering across SDK (`filterCardsByLabelGroup`), CLI (`--label-group`), REST API, and MCP tools
43
+ - SDK label management methods: `getLabels`, `setLabel`, `renameLabel`, `deleteLabel`
44
+ - Soft-delete support: hidden **Deleted** column with per-card restore or permanent delete
45
+ - Purge deleted cards functionality to permanently remove all soft-deleted cards
46
+ - `--metadata` flag for CLI `create` and `edit` commands (accepts JSON string)
47
+ - Metadata support in MCP `create_card` and `update_card` tools
48
+ - Metadata support in REST API create/update routes
49
+ - Workspace info section in the Settings panel showing project path and `.kanban.json` parameters
50
+ - `js-yaml` dependency for robust YAML metadata parsing
51
+
52
+ ### Fixed
53
+ - Comment parser no longer breaks on horizontal rules (`---`) inside comment blocks
54
+ - Blank lines in metadata YAML parsed correctly; scalar edge cases handled
55
+
56
+ ## [1.8.0] - 2026-02-24
57
+
58
+ ### Added
59
+ - Multi-board support: board selector dropdown to switch between boards and create new boards
60
+ - Card transfer between boards via a StatusDropdown with a nested board-and-column tree
61
+ - `transferCard` message type and `BoardInfo.columns` field in the extension/standalone protocol
62
+ - Webhooks system: CRUD operations (`create`, `get`, `update`, `delete`, `list`) stored in `.kanban-webhooks.json`
63
+ - Webhook event delivery on card create/update/delete/move with configurable `url`, `events`, and `secret`
64
+ - Webhook management commands in CLI and MCP server
65
+ - Comments functionality: add, edit, and delete comments on feature cards
66
+ - Markdown rendering for comment content
67
+ - Auto-generated SDK docs (`docs/sdk.md`) and REST API docs (`docs/api.md`) from JSDoc / route metadata
68
+ - `npm run docs` script to regenerate all documentation
69
+ - Theme toggle (light / dark) in the board toolbar
70
+ - Release scripts for versioning, changelog generation, and GitHub release creation
71
+
72
+ ### Fixed
73
+ - SDK export paths updated to support both CommonJS and ESM module formats
74
+ - SDK import paths corrected; server feature loading logic improved
75
+
8
76
  ## [1.7.0] - 2026-02-20
9
77
 
10
78
  ### Added
package/README.md CHANGED
@@ -44,10 +44,16 @@ kl add --title "My first task" --priority high
44
44
  - **Layout toggle**: Switch between horizontal and vertical board layouts
45
45
  - **Real-time updates**: WebSocket-powered live sync across clients
46
46
  - **Light & dark mode** support
47
+ - **Tabbed settings panel**: Settings organized into **General**, **Defaults**, and **Labels** tabs
48
+ - **Zoom controls**: Scale the board view and card detail panel independently between 75–150% via settings sliders or keyboard shortcuts
49
+ - **Column sorting**: Sort cards within a column by priority, due date, or creation date from the column menu
50
+ - **Smooth scroll to selection**: Board automatically scrolls to the selected card when switching features
47
51
  - **Keyboard shortcuts**:
48
52
  - `N` - Create new feature
49
53
  - `Esc` - Close dialogs
50
54
  - `Cmd/Ctrl + Enter` - Submit create dialog
55
+ - `Ctrl/Cmd + =` / `Ctrl/Cmd + -` - Zoom board view in / out
56
+ - `Ctrl/Cmd + Shift + =` / `Ctrl/Cmd + Shift + -` - Zoom card detail in / out
51
57
 
52
58
  ### Feature Cards
53
59
 
@@ -628,12 +634,16 @@ Board configuration is stored in `.kanban.json` at your project root. It support
628
634
  "showDueDate": true,
629
635
  "showLabels": true,
630
636
  "compactMode": false,
637
+ "boardZoom": 100,
638
+ "cardZoom": 100,
631
639
  "actionWebhookUrl": "https://example.com/kanban-actions"
632
640
  }
633
641
  ```
634
642
 
635
643
  Columns are fully customizable per board — add, remove, rename, or recolor them from the web UI, CLI, or REST API.
636
644
 
645
+ `boardZoom` and `cardZoom` set the default zoom percentage (75–150) for the board view and card detail panel respectively. They can also be adjusted live in the Settings panel or with `Ctrl/Cmd + =` / `Ctrl/Cmd + -` keyboard shortcuts.
646
+
637
647
  ## AI Agent Integration
638
648
  - **Claude Code**: Default, Plan, Auto-edit, and Full Auto modes
639
649
  - **Codex**: Suggest, Auto-edit, and Full Auto modes
package/dist/cli.js CHANGED
@@ -330,6 +330,8 @@ function migrateConfigV1ToV2(raw) {
330
330
  compactMode: v1.compactMode,
331
331
  markdownEditorMode: v1.markdownEditorMode,
332
332
  showDeletedColumn: false,
333
+ boardZoom: 100,
334
+ cardZoom: 100,
333
335
  port: 3e3
334
336
  };
335
337
  }
@@ -403,7 +405,9 @@ function configToSettings(config) {
403
405
  markdownEditorMode: config.markdownEditorMode,
404
406
  showDeletedColumn: config.showDeletedColumn,
405
407
  defaultPriority: config.defaultPriority,
406
- defaultStatus: config.defaultStatus
408
+ defaultStatus: config.defaultStatus,
409
+ boardZoom: config.boardZoom ?? 100,
410
+ cardZoom: config.cardZoom ?? 100
407
411
  };
408
412
  }
409
413
  function settingsToConfig(config, settings) {
@@ -417,7 +421,9 @@ function settingsToConfig(config, settings) {
417
421
  compactMode: settings.compactMode,
418
422
  showDeletedColumn: settings.showDeletedColumn,
419
423
  defaultPriority: settings.defaultPriority,
420
- defaultStatus: settings.defaultStatus
424
+ defaultStatus: settings.defaultStatus,
425
+ boardZoom: settings.boardZoom,
426
+ cardZoom: settings.cardZoom
421
427
  };
422
428
  }
423
429
  var fs, path, DEFAULT_BOARD_CONFIG, DEFAULT_CONFIG, CONFIG_FILENAME;
@@ -453,6 +459,8 @@ var init_config = __esm({
453
459
  compactMode: false,
454
460
  markdownEditorMode: false,
455
461
  showDeletedColumn: false,
462
+ boardZoom: 100,
463
+ cardZoom: 100,
456
464
  port: 3e3,
457
465
  labels: {}
458
466
  };
@@ -2714,7 +2722,7 @@ function renamed(from, to) {
2714
2722
  throw new Error("Function yaml." + from + " is removed in js-yaml 4. Use yaml." + to + " instead, which is now safe by default.");
2715
2723
  };
2716
2724
  }
2717
- var isNothing_1, isObject_1, toArray_1, repeat_1, isNegativeZero_1, extend_1, common, exception, snippet, TYPE_CONSTRUCTOR_OPTIONS, YAML_NODE_KINDS, type, schema, str, seq, map, failsafe, _null, bool, int, YAML_FLOAT_PATTERN, SCIENTIFIC_WITHOUT_DOT, float, json, core, YAML_DATE_REGEXP, YAML_TIMESTAMP_REGEXP, timestamp, merge, BASE64_MAP, binary, _hasOwnProperty$3, _toString$2, omap, _toString$1, pairs, _hasOwnProperty$2, set, _default, _hasOwnProperty$1, CONTEXT_FLOW_IN, CONTEXT_FLOW_OUT, CONTEXT_BLOCK_IN, CONTEXT_BLOCK_OUT, CHOMPING_CLIP, CHOMPING_STRIP, CHOMPING_KEEP, PATTERN_NON_PRINTABLE, PATTERN_NON_ASCII_LINE_BREAKS, PATTERN_FLOW_INDICATORS, PATTERN_TAG_HANDLE, PATTERN_TAG_URI, simpleEscapeCheck, simpleEscapeMap, i, directiveHandlers, loadAll_1, load_1, loader, _toString, _hasOwnProperty, CHAR_BOM, CHAR_TAB, CHAR_LINE_FEED, CHAR_CARRIAGE_RETURN, CHAR_SPACE, CHAR_EXCLAMATION, CHAR_DOUBLE_QUOTE, CHAR_SHARP, CHAR_PERCENT, CHAR_AMPERSAND, CHAR_SINGLE_QUOTE, CHAR_ASTERISK, CHAR_COMMA, CHAR_MINUS, CHAR_COLON, CHAR_EQUALS, CHAR_GREATER_THAN, CHAR_QUESTION, CHAR_COMMERCIAL_AT, CHAR_LEFT_SQUARE_BRACKET, CHAR_RIGHT_SQUARE_BRACKET, CHAR_GRAVE_ACCENT, CHAR_LEFT_CURLY_BRACKET, CHAR_VERTICAL_LINE, CHAR_RIGHT_CURLY_BRACKET, ESCAPE_SEQUENCES, DEPRECATED_BOOLEANS_SYNTAX, DEPRECATED_BASE60_SYNTAX, QUOTING_TYPE_SINGLE, QUOTING_TYPE_DOUBLE, STYLE_PLAIN, STYLE_SINGLE, STYLE_LITERAL, STYLE_FOLDED, STYLE_DOUBLE, dump_1, dumper, load, loadAll, dump, safeLoad, safeLoadAll, safeDump;
2725
+ var isNothing_1, isObject_1, toArray_1, repeat_1, isNegativeZero_1, extend_1, common, exception, snippet, TYPE_CONSTRUCTOR_OPTIONS, YAML_NODE_KINDS, type, schema, str, seq, map, failsafe, _null, bool, int, YAML_FLOAT_PATTERN, SCIENTIFIC_WITHOUT_DOT, float, json, core, YAML_DATE_REGEXP, YAML_TIMESTAMP_REGEXP, timestamp, merge, BASE64_MAP, binary, _hasOwnProperty$3, _toString$2, omap, _toString$1, pairs, _hasOwnProperty$2, set, _default, _hasOwnProperty$1, CONTEXT_FLOW_IN, CONTEXT_FLOW_OUT, CONTEXT_BLOCK_IN, CONTEXT_BLOCK_OUT, CHOMPING_CLIP, CHOMPING_STRIP, CHOMPING_KEEP, PATTERN_NON_PRINTABLE, PATTERN_NON_ASCII_LINE_BREAKS, PATTERN_FLOW_INDICATORS, PATTERN_TAG_HANDLE, PATTERN_TAG_URI, simpleEscapeCheck, simpleEscapeMap, i, directiveHandlers, loadAll_1, load_1, loader, _toString, _hasOwnProperty, CHAR_BOM, CHAR_TAB, CHAR_LINE_FEED, CHAR_CARRIAGE_RETURN, CHAR_SPACE, CHAR_EXCLAMATION, CHAR_DOUBLE_QUOTE, CHAR_SHARP, CHAR_PERCENT, CHAR_AMPERSAND, CHAR_SINGLE_QUOTE, CHAR_ASTERISK, CHAR_COMMA, CHAR_MINUS, CHAR_COLON, CHAR_EQUALS, CHAR_GREATER_THAN, CHAR_QUESTION, CHAR_COMMERCIAL_AT, CHAR_LEFT_SQUARE_BRACKET, CHAR_RIGHT_SQUARE_BRACKET, CHAR_GRAVE_ACCENT, CHAR_LEFT_CURLY_BRACKET, CHAR_VERTICAL_LINE, CHAR_RIGHT_CURLY_BRACKET, ESCAPE_SEQUENCES, DEPRECATED_BOOLEANS_SYNTAX, DEPRECATED_BASE60_SYNTAX, QUOTING_TYPE_SINGLE, QUOTING_TYPE_DOUBLE, STYLE_PLAIN, STYLE_SINGLE, STYLE_LITERAL, STYLE_FOLDED, STYLE_DOUBLE, dump_1, dumper, JSON_SCHEMA, load, loadAll, dump, safeLoad, safeLoadAll, safeDump;
2718
2726
  var init_js_yaml = __esm({
2719
2727
  "node_modules/js-yaml/dist/js-yaml.mjs"() {
2720
2728
  isNothing_1 = isNothing;
@@ -3115,6 +3123,7 @@ var init_js_yaml = __esm({
3115
3123
  dumper = {
3116
3124
  dump: dump_1
3117
3125
  };
3126
+ JSON_SCHEMA = json;
3118
3127
  load = loader.load;
3119
3128
  loadAll = loader.loadAll;
3120
3129
  dump = dumper.dump;
@@ -3155,48 +3164,26 @@ function parseFeatureFile(content, filePath) {
3155
3164
  return null;
3156
3165
  const frontmatter = frontmatterMatch[1];
3157
3166
  const rest = frontmatterMatch[2] || "";
3158
- const getValue = (key) => {
3159
- const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
3160
- if (!match)
3167
+ let parsed;
3168
+ try {
3169
+ const loaded = load(frontmatter, { schema: JSON_SCHEMA });
3170
+ if (!loaded || typeof loaded !== "object" || Array.isArray(loaded))
3171
+ return null;
3172
+ parsed = loaded;
3173
+ } catch {
3174
+ return null;
3175
+ }
3176
+ const str2 = (key) => {
3177
+ const val = parsed[key];
3178
+ if (val == null)
3161
3179
  return "";
3162
- const value = match[1].trim().replace(/^["']|["']$/g, "");
3163
- return value === "null" ? "" : value;
3180
+ return String(val);
3164
3181
  };
3165
- const getArrayValue = (key) => {
3166
- const match = frontmatter.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, "m"));
3167
- if (!match)
3182
+ const arr = (key) => {
3183
+ const val = parsed[key];
3184
+ if (!Array.isArray(val))
3168
3185
  return [];
3169
- return match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
3170
- };
3171
- const getMetadata = () => {
3172
- const lines = frontmatter.split("\n");
3173
- let metaStart = -1;
3174
- for (let j = 0; j < lines.length; j++) {
3175
- if (/^metadata:\s*$/.test(lines[j])) {
3176
- metaStart = j + 1;
3177
- break;
3178
- }
3179
- }
3180
- if (metaStart === -1)
3181
- return void 0;
3182
- const indentedLines = [];
3183
- for (let j = metaStart; j < lines.length; j++) {
3184
- if (/^\s/.test(lines[j]) || lines[j].trim() === "") {
3185
- indentedLines.push(lines[j]);
3186
- } else {
3187
- break;
3188
- }
3189
- }
3190
- if (indentedLines.length === 0)
3191
- return void 0;
3192
- try {
3193
- const parsed = load(indentedLines.join("\n"));
3194
- if (parsed && typeof parsed === "object")
3195
- return parsed;
3196
- return void 0;
3197
- } catch {
3198
- return void 0;
3199
- }
3186
+ return val.filter((v) => v != null).map(String);
3200
3187
  };
3201
3188
  const sections = rest.split(/\n---\n/);
3202
3189
  let body = sections[0] || "";
@@ -3217,22 +3204,23 @@ ${section}`;
3217
3204
  i += 1;
3218
3205
  }
3219
3206
  }
3220
- const meta = getMetadata();
3221
- const actions = getArrayValue("actions");
3207
+ const actions = arr("actions");
3208
+ const rawMeta = parsed.metadata;
3209
+ const meta = rawMeta != null && typeof rawMeta === "object" && !Array.isArray(rawMeta) ? rawMeta : void 0;
3222
3210
  return {
3223
- version: parseInt(getValue("version"), 10) || 0,
3224
- id: getValue("id") || extractIdFromFilename(filePath),
3225
- status: getValue("status") || "backlog",
3226
- priority: getValue("priority") || "medium",
3227
- assignee: getValue("assignee") || null,
3228
- dueDate: getValue("dueDate") || null,
3229
- created: getValue("created") || (/* @__PURE__ */ new Date()).toISOString(),
3230
- modified: getValue("modified") || (/* @__PURE__ */ new Date()).toISOString(),
3231
- completedAt: getValue("completedAt") || null,
3232
- labels: getArrayValue("labels"),
3233
- attachments: getArrayValue("attachments"),
3211
+ version: typeof parsed.version === "number" ? parsed.version : parseInt(str2("version"), 10) || 0,
3212
+ id: str2("id") || extractIdFromFilename(filePath),
3213
+ status: str2("status") || "backlog",
3214
+ priority: str2("priority") || "medium",
3215
+ assignee: parsed.assignee != null ? String(parsed.assignee) : null,
3216
+ dueDate: parsed.dueDate != null ? String(parsed.dueDate) : null,
3217
+ created: str2("created") || (/* @__PURE__ */ new Date()).toISOString(),
3218
+ modified: str2("modified") || (/* @__PURE__ */ new Date()).toISOString(),
3219
+ completedAt: parsed.completedAt != null ? String(parsed.completedAt) : null,
3220
+ labels: arr("labels"),
3221
+ attachments: arr("attachments"),
3234
3222
  comments,
3235
- order: getValue("order") || "a0",
3223
+ order: str2("order") || "a0",
3236
3224
  content: body.trim(),
3237
3225
  ...meta ? { metadata: meta } : {},
3238
3226
  ...actions.length > 0 ? { actions } : {},
@@ -3240,37 +3228,28 @@ ${section}`;
3240
3228
  };
3241
3229
  }
3242
3230
  function serializeFeature(feature) {
3243
- const lines = [
3244
- "---",
3245
- `version: ${feature.version ?? CARD_FORMAT_VERSION}`,
3246
- `id: "${feature.id}"`,
3247
- `status: "${feature.status}"`,
3248
- `priority: "${feature.priority}"`,
3249
- `assignee: ${feature.assignee ? `"${feature.assignee}"` : "null"}`,
3250
- `dueDate: ${feature.dueDate ? `"${feature.dueDate}"` : "null"}`,
3251
- `created: "${feature.created}"`,
3252
- `modified: "${feature.modified}"`,
3253
- `completedAt: ${feature.completedAt ? `"${feature.completedAt}"` : "null"}`,
3254
- `labels: [${feature.labels.map((l) => `"${l}"`).join(", ")}]`,
3255
- `attachments: [${(feature.attachments || []).map((a) => `"${a}"`).join(", ")}]`,
3256
- `order: "${feature.order}"`
3257
- ];
3258
- if (feature.actions && feature.actions.length > 0) {
3259
- lines.push(`actions: [${feature.actions.map((a) => `"${a}"`).join(", ")}]`);
3260
- }
3261
- if (feature.metadata && Object.keys(feature.metadata).length > 0) {
3262
- const metaYaml = dump(feature.metadata, { indent: 2, lineWidth: -1 });
3263
- lines.push("metadata:");
3264
- for (const line of metaYaml.trimEnd().split("\n")) {
3265
- lines.push(" " + line);
3266
- }
3267
- }
3268
- lines.push("---");
3269
- lines.push("");
3270
- const frontmatter = lines.join("\n");
3271
- let result = frontmatter + feature.content;
3272
- const comments = feature.comments || [];
3273
- for (const comment of comments) {
3231
+ const frontmatterObj = {
3232
+ version: feature.version ?? CARD_FORMAT_VERSION,
3233
+ id: feature.id,
3234
+ status: feature.status,
3235
+ priority: feature.priority,
3236
+ assignee: feature.assignee ?? null,
3237
+ dueDate: feature.dueDate ?? null,
3238
+ created: feature.created,
3239
+ modified: feature.modified,
3240
+ completedAt: feature.completedAt ?? null,
3241
+ labels: feature.labels,
3242
+ attachments: feature.attachments || [],
3243
+ order: feature.order,
3244
+ ...feature.actions?.length ? { actions: feature.actions } : {},
3245
+ ...feature.metadata && Object.keys(feature.metadata).length > 0 ? { metadata: feature.metadata } : {}
3246
+ };
3247
+ const yamlStr = dump(frontmatterObj, { lineWidth: -1, quotingType: '"', forceQuotes: true });
3248
+ let result = `---
3249
+ ${yamlStr}---
3250
+
3251
+ ${feature.content}`;
3252
+ for (const comment of feature.comments || []) {
3274
3253
  result += "\n\n---\n";
3275
3254
  result += `comment: true
3276
3255
  `;
@@ -4308,25 +4287,28 @@ var init_KanbanSDK = __esm({
4308
4287
  writeConfig(this.workspaceRoot, config);
4309
4288
  }
4310
4289
  /**
4311
- * Removes a label definition from the workspace configuration.
4312
- *
4313
- * This only removes the color/group definition — cards that use this
4314
- * label keep their label strings. Those labels will render with default
4315
- * gray styling in the UI.
4290
+ * Removes a label definition from the workspace configuration and cascades
4291
+ * the deletion to all cards by removing the label from their `labels` array.
4316
4292
  *
4317
4293
  * @param name - The label name to remove.
4318
4294
  *
4319
4295
  * @example
4320
4296
  * ```ts
4321
- * sdk.deleteLabel('bug')
4297
+ * await sdk.deleteLabel('bug')
4322
4298
  * ```
4323
4299
  */
4324
- deleteLabel(name) {
4300
+ async deleteLabel(name) {
4325
4301
  const config = readConfig(this.workspaceRoot);
4326
4302
  if (config.labels) {
4327
4303
  delete config.labels[name];
4328
4304
  writeConfig(this.workspaceRoot, config);
4329
4305
  }
4306
+ const cards = await this.listCards();
4307
+ for (const card of cards) {
4308
+ if (card.labels.includes(name)) {
4309
+ await this.updateCard(card.id, { labels: card.labels.filter((l) => l !== name) });
4310
+ }
4311
+ }
4330
4312
  }
4331
4313
  /**
4332
4314
  * Renames a label in the configuration and cascades the change to all cards.
@@ -4751,6 +4733,57 @@ var init_KanbanSDK = __esm({
4751
4733
  this.emitEvent("column.deleted", removed);
4752
4734
  return board.columns;
4753
4735
  }
4736
+ /**
4737
+ * Moves all cards in the specified column to the `deleted` (soft-delete) column.
4738
+ *
4739
+ * This is a non-destructive operation — cards are moved to the reserved
4740
+ * `deleted` status and can be restored or permanently deleted later.
4741
+ * The column itself is not removed.
4742
+ *
4743
+ * @param columnId - The ID of the column whose cards should be moved to `deleted`.
4744
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
4745
+ * @returns A promise resolving to the number of cards that were moved.
4746
+ * @throws {Error} If the column is `'deleted'` (no-op protection).
4747
+ *
4748
+ * @example
4749
+ * ```ts
4750
+ * const moved = await sdk.cleanupColumn('blocked')
4751
+ * console.log(`Moved ${moved} cards to deleted`)
4752
+ * ```
4753
+ */
4754
+ async cleanupColumn(columnId, boardId) {
4755
+ if (columnId === DELETED_STATUS_ID)
4756
+ return 0;
4757
+ const cards = await this.listCards(void 0, boardId);
4758
+ const cardsToMove = cards.filter((c) => c.status === columnId);
4759
+ for (const card of cardsToMove) {
4760
+ await this.moveCard(card.id, DELETED_STATUS_ID, 0, boardId);
4761
+ }
4762
+ return cardsToMove.length;
4763
+ }
4764
+ /**
4765
+ * Permanently deletes all cards currently in the `deleted` column.
4766
+ *
4767
+ * This is equivalent to "empty trash". All soft-deleted cards are
4768
+ * removed from disk. This operation cannot be undone.
4769
+ *
4770
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
4771
+ * @returns A promise resolving to the number of cards that were permanently deleted.
4772
+ *
4773
+ * @example
4774
+ * ```ts
4775
+ * const count = await sdk.purgeDeletedCards()
4776
+ * console.log(`Permanently deleted ${count} cards`)
4777
+ * ```
4778
+ */
4779
+ async purgeDeletedCards(boardId) {
4780
+ const cards = await this.listCards(void 0, boardId);
4781
+ const deleted = cards.filter((c) => c.status === DELETED_STATUS_ID);
4782
+ for (const card of deleted) {
4783
+ await this.permanentlyDeleteCard(card.id, boardId);
4784
+ }
4785
+ return deleted.length;
4786
+ }
4754
4787
  /**
4755
4788
  * Reorders the columns of a board.
4756
4789
  *
@@ -10587,10 +10620,7 @@ function startServer(featuresDir, port, webviewDir) {
10587
10620
  }
10588
10621
  async function doPurgeDeletedCards() {
10589
10622
  try {
10590
- const deletedCards = features.filter((f) => f.status === "deleted");
10591
- for (const card of deletedCards) {
10592
- await sdk.permanentlyDeleteCard(card.id, currentBoardId);
10593
- }
10623
+ await sdk.purgeDeletedCards(currentBoardId);
10594
10624
  await loadFeatures();
10595
10625
  broadcast(buildInitMessage());
10596
10626
  return true;
@@ -10637,6 +10667,20 @@ function startServer(featuresDir, port, webviewDir) {
10637
10667
  return { removed: false, error: String(err) };
10638
10668
  }
10639
10669
  }
10670
+ async function doCleanupColumn(columnId) {
10671
+ try {
10672
+ migrating = true;
10673
+ await sdk.cleanupColumn(columnId, currentBoardId);
10674
+ await loadFeatures();
10675
+ broadcast(buildInitMessage());
10676
+ return true;
10677
+ } catch (err) {
10678
+ console.error("Failed to cleanup column:", err);
10679
+ return false;
10680
+ } finally {
10681
+ migrating = false;
10682
+ }
10683
+ }
10640
10684
  function doSaveSettings(newSettings) {
10641
10685
  sdk.updateSettings(newSettings);
10642
10686
  broadcast(buildInitMessage());
@@ -10823,6 +10867,9 @@ function startServer(featuresDir, port, webviewDir) {
10823
10867
  case "removeColumn":
10824
10868
  await doRemoveColumn(msg.columnId);
10825
10869
  break;
10870
+ case "cleanupColumn":
10871
+ await doCleanupColumn(msg.columnId);
10872
+ break;
10826
10873
  case "removeAttachment": {
10827
10874
  const featureId = msg.featureId;
10828
10875
  const feature = await doRemoveAttachment(featureId, msg.attachment);
@@ -10971,13 +11018,16 @@ function startServer(featuresDir, port, webviewDir) {
10971
11018
  }
10972
11019
  case "renameLabel": {
10973
11020
  await sdk.renameLabel(msg.oldName, msg.newName);
11021
+ await loadFeatures();
10974
11022
  broadcast({ type: "labelsUpdated", labels: sdk.getLabels() });
10975
11023
  broadcast(buildInitMessage());
10976
11024
  break;
10977
11025
  }
10978
11026
  case "deleteLabel": {
10979
- sdk.deleteLabel(msg.name);
11027
+ await sdk.deleteLabel(msg.name);
11028
+ await loadFeatures();
10980
11029
  broadcast({ type: "labelsUpdated", labels: sdk.getLabels() });
11030
+ broadcast(buildInitMessage());
10981
11031
  break;
10982
11032
  }
10983
11033
  case "triggerAction": {
@@ -11589,6 +11639,9 @@ function startServer(featuresDir, port, webviewDir) {
11589
11639
  if (!newName)
11590
11640
  return jsonError(res, 400, "newName is required");
11591
11641
  await sdk.renameLabel(name, newName);
11642
+ await loadFeatures();
11643
+ broadcast({ type: "labelsUpdated", labels: sdk.getLabels() });
11644
+ broadcast(buildInitMessage());
11592
11645
  return jsonOk(res, sdk.getLabels());
11593
11646
  } catch (err) {
11594
11647
  return jsonError(res, 400, String(err));
@@ -11598,7 +11651,10 @@ function startServer(featuresDir, port, webviewDir) {
11598
11651
  if (params) {
11599
11652
  try {
11600
11653
  const name = decodeURIComponent(params.name);
11601
- sdk.deleteLabel(name);
11654
+ await sdk.deleteLabel(name);
11655
+ await loadFeatures();
11656
+ broadcast({ type: "labelsUpdated", labels: sdk.getLabels() });
11657
+ broadcast(buildInitMessage());
11602
11658
  return jsonOk(res, { success: true });
11603
11659
  } catch (err) {
11604
11660
  return jsonError(res, 400, String(err));
@@ -13142,9 +13198,19 @@ async function cmdColumns(sdk, positional, flags) {
13142
13198
  console.log(JSON.stringify(columns, null, 2));
13143
13199
  break;
13144
13200
  }
13201
+ case "cleanup": {
13202
+ const columnId = positional[1];
13203
+ if (!columnId) {
13204
+ console.error(red("Usage: kl columns cleanup <id>"));
13205
+ process.exit(1);
13206
+ }
13207
+ const moved = await sdk.cleanupColumn(columnId, boardId);
13208
+ console.log(green(`Moved ${moved} card${moved === 1 ? "" : "s"} from "${columnId}" to deleted`));
13209
+ break;
13210
+ }
13145
13211
  default:
13146
13212
  console.error(red(`Unknown columns subcommand: ${subcommand}`));
13147
- console.error("Available: list, add, update, remove");
13213
+ console.error("Available: list, add, update, remove, cleanup");
13148
13214
  process.exit(1);
13149
13215
  }
13150
13216
  }
@@ -13218,7 +13284,7 @@ async function cmdLabels(sdk, positional, flags) {
13218
13284
  console.error(red("Usage: kl labels delete <name>"));
13219
13285
  process.exit(1);
13220
13286
  }
13221
- sdk.deleteLabel(name);
13287
+ await sdk.deleteLabel(name);
13222
13288
  if (flags.json) {
13223
13289
  console.log(JSON.stringify({ deleted: name }, null, 2));
13224
13290
  } else {