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
@@ -4327,6 +4327,215 @@ var init_open = __esm({
4327
4327
  }
4328
4328
  });
4329
4329
 
4330
+ // src/shared/config.ts
4331
+ var fs = __toESM(require("fs"));
4332
+ var path = __toESM(require("path"));
4333
+
4334
+ // src/shared/types.ts
4335
+ var CARD_FORMAT_VERSION = 1;
4336
+ function getTitleFromContent(content) {
4337
+ const match = content.match(/^#\s+(.+)$/m);
4338
+ if (match)
4339
+ return match[1].trim();
4340
+ const firstLine = content.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
4341
+ return firstLine || "Untitled";
4342
+ }
4343
+ function generateSlug(title) {
4344
+ return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50) || "feature";
4345
+ }
4346
+ function generateFeatureFilename(id, title) {
4347
+ const slug = generateSlug(title);
4348
+ return `${id}-${slug}`;
4349
+ }
4350
+ function extractNumericId(filenameOrId) {
4351
+ const match = filenameOrId.match(/^(\d+)(?:-|$)/);
4352
+ return match ? parseInt(match[1], 10) : null;
4353
+ }
4354
+ var DEFAULT_COLUMNS = [
4355
+ { id: "backlog", name: "Backlog", color: "#6b7280" },
4356
+ { id: "todo", name: "To Do", color: "#3b82f6" },
4357
+ { id: "in-progress", name: "In Progress", color: "#f59e0b" },
4358
+ { id: "review", name: "Review", color: "#8b5cf6" },
4359
+ { id: "done", name: "Done", color: "#22c55e" }
4360
+ ];
4361
+ var DELETED_STATUS_ID = "deleted";
4362
+
4363
+ // src/shared/config.ts
4364
+ var DEFAULT_BOARD_CONFIG = {
4365
+ name: "Default",
4366
+ columns: [...DEFAULT_COLUMNS],
4367
+ nextCardId: 1,
4368
+ defaultStatus: "backlog",
4369
+ defaultPriority: "medium"
4370
+ };
4371
+ var DEFAULT_CONFIG = {
4372
+ version: 2,
4373
+ boards: {
4374
+ default: { ...DEFAULT_BOARD_CONFIG, columns: [...DEFAULT_COLUMNS] }
4375
+ },
4376
+ defaultBoard: "default",
4377
+ featuresDirectory: ".kanban",
4378
+ aiAgent: "claude",
4379
+ defaultPriority: "medium",
4380
+ defaultStatus: "backlog",
4381
+ showPriorityBadges: true,
4382
+ showAssignee: true,
4383
+ showDueDate: true,
4384
+ showLabels: true,
4385
+ showBuildWithAI: true,
4386
+ showFileName: false,
4387
+ compactMode: false,
4388
+ markdownEditorMode: false,
4389
+ showDeletedColumn: false,
4390
+ boardZoom: 100,
4391
+ cardZoom: 100,
4392
+ port: 3e3,
4393
+ labels: {}
4394
+ };
4395
+ var CONFIG_FILENAME = ".kanban.json";
4396
+ function configPath(workspaceRoot) {
4397
+ return path.join(workspaceRoot, CONFIG_FILENAME);
4398
+ }
4399
+ function migrateConfigV1ToV2(raw) {
4400
+ const v1Defaults = {
4401
+ featuresDirectory: ".kanban",
4402
+ defaultPriority: "medium",
4403
+ defaultStatus: "backlog",
4404
+ columns: [...DEFAULT_COLUMNS],
4405
+ aiAgent: "claude",
4406
+ nextCardId: 1,
4407
+ showPriorityBadges: true,
4408
+ showAssignee: true,
4409
+ showDueDate: true,
4410
+ showLabels: true,
4411
+ showBuildWithAI: true,
4412
+ showFileName: false,
4413
+ compactMode: false,
4414
+ markdownEditorMode: false
4415
+ };
4416
+ const v1 = { ...v1Defaults, ...raw };
4417
+ return {
4418
+ version: 2,
4419
+ boards: {
4420
+ default: {
4421
+ name: "Default",
4422
+ columns: v1.columns,
4423
+ nextCardId: v1.nextCardId,
4424
+ defaultStatus: v1.defaultStatus,
4425
+ defaultPriority: v1.defaultPriority
4426
+ }
4427
+ },
4428
+ defaultBoard: "default",
4429
+ featuresDirectory: v1.featuresDirectory,
4430
+ aiAgent: v1.aiAgent,
4431
+ defaultPriority: v1.defaultPriority,
4432
+ defaultStatus: v1.defaultStatus,
4433
+ showPriorityBadges: v1.showPriorityBadges,
4434
+ showAssignee: v1.showAssignee,
4435
+ showDueDate: v1.showDueDate,
4436
+ showLabels: v1.showLabels,
4437
+ showBuildWithAI: v1.showBuildWithAI,
4438
+ showFileName: v1.showFileName,
4439
+ compactMode: v1.compactMode,
4440
+ markdownEditorMode: v1.markdownEditorMode,
4441
+ showDeletedColumn: false,
4442
+ boardZoom: 100,
4443
+ cardZoom: 100,
4444
+ port: 3e3
4445
+ };
4446
+ }
4447
+ function readConfig(workspaceRoot) {
4448
+ const filePath = configPath(workspaceRoot);
4449
+ const defaults = { ...DEFAULT_CONFIG, boards: { default: { ...DEFAULT_BOARD_CONFIG, columns: [...DEFAULT_COLUMNS] } } };
4450
+ try {
4451
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
4452
+ if (!raw.version || raw.version === 1) {
4453
+ const v2 = migrateConfigV1ToV2(raw);
4454
+ writeConfig(workspaceRoot, v2);
4455
+ return v2;
4456
+ }
4457
+ const config = { ...defaults, ...raw };
4458
+ if (!config.boards || Object.keys(config.boards).length === 0) {
4459
+ config.boards = defaults.boards;
4460
+ }
4461
+ return config;
4462
+ } catch {
4463
+ return defaults;
4464
+ }
4465
+ }
4466
+ function writeConfig(workspaceRoot, config) {
4467
+ const filePath = configPath(workspaceRoot);
4468
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
4469
+ }
4470
+ function getBoardConfig(workspaceRoot, boardId) {
4471
+ const config = readConfig(workspaceRoot);
4472
+ const resolvedId = boardId || config.defaultBoard;
4473
+ const board = config.boards[resolvedId];
4474
+ if (!board) {
4475
+ throw new Error(`Board '${resolvedId}' not found`);
4476
+ }
4477
+ return board;
4478
+ }
4479
+ function allocateCardId(workspaceRoot, boardId) {
4480
+ const config = readConfig(workspaceRoot);
4481
+ const resolvedId = boardId || config.defaultBoard;
4482
+ const board = config.boards[resolvedId];
4483
+ if (!board) {
4484
+ throw new Error(`Board '${resolvedId}' not found`);
4485
+ }
4486
+ const id = board.nextCardId;
4487
+ board.nextCardId = id + 1;
4488
+ writeConfig(workspaceRoot, config);
4489
+ return id;
4490
+ }
4491
+ function syncCardIdCounter(workspaceRoot, boardId, existingIds) {
4492
+ if (existingIds.length === 0)
4493
+ return;
4494
+ const maxId = Math.max(...existingIds);
4495
+ const config = readConfig(workspaceRoot);
4496
+ const resolvedId = boardId || config.defaultBoard;
4497
+ const board = config.boards[resolvedId];
4498
+ if (!board)
4499
+ return;
4500
+ if (board.nextCardId <= maxId) {
4501
+ board.nextCardId = maxId + 1;
4502
+ writeConfig(workspaceRoot, config);
4503
+ }
4504
+ }
4505
+ function configToSettings(config) {
4506
+ return {
4507
+ showPriorityBadges: config.showPriorityBadges,
4508
+ showAssignee: config.showAssignee,
4509
+ showDueDate: config.showDueDate,
4510
+ showLabels: config.showLabels,
4511
+ showBuildWithAI: config.showBuildWithAI,
4512
+ showFileName: config.showFileName,
4513
+ compactMode: config.compactMode,
4514
+ markdownEditorMode: config.markdownEditorMode,
4515
+ showDeletedColumn: config.showDeletedColumn,
4516
+ defaultPriority: config.defaultPriority,
4517
+ defaultStatus: config.defaultStatus,
4518
+ boardZoom: config.boardZoom ?? 100,
4519
+ cardZoom: config.cardZoom ?? 100
4520
+ };
4521
+ }
4522
+ function settingsToConfig(config, settings) {
4523
+ return {
4524
+ ...config,
4525
+ showPriorityBadges: settings.showPriorityBadges,
4526
+ showAssignee: settings.showAssignee,
4527
+ showDueDate: settings.showDueDate,
4528
+ showLabels: settings.showLabels,
4529
+ showFileName: settings.showFileName,
4530
+ compactMode: settings.compactMode,
4531
+ showDeletedColumn: settings.showDeletedColumn,
4532
+ defaultPriority: settings.defaultPriority,
4533
+ defaultStatus: settings.defaultStatus,
4534
+ boardZoom: settings.boardZoom,
4535
+ cardZoom: settings.cardZoom
4536
+ };
4537
+ }
4538
+
4330
4539
  // src/standalone/server.ts
4331
4540
  var http2 = __toESM(require("http"));
4332
4541
  var fs5 = __toESM(require("fs"));
@@ -6030,34 +6239,6 @@ function watch(paths, options = {}) {
6030
6239
  }
6031
6240
  var esm_default = { watch, FSWatcher };
6032
6241
 
6033
- // src/shared/types.ts
6034
- function getTitleFromContent(content) {
6035
- const match = content.match(/^#\s+(.+)$/m);
6036
- if (match)
6037
- return match[1].trim();
6038
- const firstLine = content.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
6039
- return firstLine || "Untitled";
6040
- }
6041
- function generateSlug(title) {
6042
- return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50) || "feature";
6043
- }
6044
- function generateFeatureFilename(id, title) {
6045
- const slug = generateSlug(title);
6046
- return `${id}-${slug}`;
6047
- }
6048
- function extractNumericId(filenameOrId) {
6049
- const match = filenameOrId.match(/^(\d+)(?:-|$)/);
6050
- return match ? parseInt(match[1], 10) : null;
6051
- }
6052
- var DEFAULT_COLUMNS = [
6053
- { id: "backlog", name: "Backlog", color: "#6b7280" },
6054
- { id: "todo", name: "To Do", color: "#3b82f6" },
6055
- { id: "in-progress", name: "In Progress", color: "#f59e0b" },
6056
- { id: "review", name: "Review", color: "#8b5cf6" },
6057
- { id: "done", name: "Done", color: "#22c55e" }
6058
- ];
6059
- var DELETED_STATUS_ID = "deleted";
6060
-
6061
6242
  // src/sdk/KanbanSDK.ts
6062
6243
  var fs4 = __toESM(require("fs/promises"));
6063
6244
  var path5 = __toESM(require("path"));
@@ -6206,240 +6387,70 @@ function generateKeyBetween(a, b, digits = BASE_62_DIGITS) {
6206
6387
  if (ib2 === "A" + digits[0].repeat(26)) {
6207
6388
  return ib2 + midpoint("", fb2, digits);
6208
6389
  }
6209
- if (ib2 < b) {
6210
- return ib2;
6211
- }
6212
- const res = decrementInteger(ib2, digits);
6213
- if (res == null) {
6214
- throw new Error("cannot decrement any more");
6215
- }
6216
- return res;
6217
- }
6218
- if (b == null) {
6219
- const ia2 = getIntegerPart(a);
6220
- const fa2 = a.slice(ia2.length);
6221
- const i2 = incrementInteger(ia2, digits);
6222
- return i2 == null ? ia2 + midpoint(fa2, null, digits) : i2;
6223
- }
6224
- const ia = getIntegerPart(a);
6225
- const fa = a.slice(ia.length);
6226
- const ib = getIntegerPart(b);
6227
- const fb = b.slice(ib.length);
6228
- if (ia === ib) {
6229
- return ia + midpoint(fa, fb, digits);
6230
- }
6231
- const i = incrementInteger(ia, digits);
6232
- if (i == null) {
6233
- throw new Error("cannot increment any more");
6234
- }
6235
- if (i < b) {
6236
- return i;
6237
- }
6238
- return ia + midpoint(fa, null, digits);
6239
- }
6240
- function generateNKeysBetween(a, b, n, digits = BASE_62_DIGITS) {
6241
- if (n === 0) {
6242
- return [];
6243
- }
6244
- if (n === 1) {
6245
- return [generateKeyBetween(a, b, digits)];
6246
- }
6247
- if (b == null) {
6248
- let c2 = generateKeyBetween(a, b, digits);
6249
- const result = [c2];
6250
- for (let i = 0; i < n - 1; i++) {
6251
- c2 = generateKeyBetween(c2, b, digits);
6252
- result.push(c2);
6253
- }
6254
- return result;
6255
- }
6256
- if (a == null) {
6257
- let c2 = generateKeyBetween(a, b, digits);
6258
- const result = [c2];
6259
- for (let i = 0; i < n - 1; i++) {
6260
- c2 = generateKeyBetween(a, c2, digits);
6261
- result.push(c2);
6262
- }
6263
- result.reverse();
6264
- return result;
6265
- }
6266
- const mid = Math.floor(n / 2);
6267
- const c = generateKeyBetween(a, b, digits);
6268
- return [
6269
- ...generateNKeysBetween(a, c, mid, digits),
6270
- c,
6271
- ...generateNKeysBetween(c, b, n - mid - 1, digits)
6272
- ];
6273
- }
6274
-
6275
- // src/shared/config.ts
6276
- var fs = __toESM(require("fs"));
6277
- var path = __toESM(require("path"));
6278
- var DEFAULT_BOARD_CONFIG = {
6279
- name: "Default",
6280
- columns: [...DEFAULT_COLUMNS],
6281
- nextCardId: 1,
6282
- defaultStatus: "backlog",
6283
- defaultPriority: "medium"
6284
- };
6285
- var DEFAULT_CONFIG = {
6286
- version: 2,
6287
- boards: {
6288
- default: { ...DEFAULT_BOARD_CONFIG, columns: [...DEFAULT_COLUMNS] }
6289
- },
6290
- defaultBoard: "default",
6291
- featuresDirectory: ".kanban",
6292
- aiAgent: "claude",
6293
- defaultPriority: "medium",
6294
- defaultStatus: "backlog",
6295
- showPriorityBadges: true,
6296
- showAssignee: true,
6297
- showDueDate: true,
6298
- showLabels: true,
6299
- showBuildWithAI: true,
6300
- showFileName: false,
6301
- compactMode: false,
6302
- markdownEditorMode: false,
6303
- showDeletedColumn: false,
6304
- port: 3e3,
6305
- labels: {}
6306
- };
6307
- var CONFIG_FILENAME = ".kanban.json";
6308
- function configPath(workspaceRoot) {
6309
- return path.join(workspaceRoot, CONFIG_FILENAME);
6310
- }
6311
- function migrateConfigV1ToV2(raw) {
6312
- const v1Defaults = {
6313
- featuresDirectory: ".kanban",
6314
- defaultPriority: "medium",
6315
- defaultStatus: "backlog",
6316
- columns: [...DEFAULT_COLUMNS],
6317
- aiAgent: "claude",
6318
- nextCardId: 1,
6319
- showPriorityBadges: true,
6320
- showAssignee: true,
6321
- showDueDate: true,
6322
- showLabels: true,
6323
- showBuildWithAI: true,
6324
- showFileName: false,
6325
- compactMode: false,
6326
- markdownEditorMode: false
6327
- };
6328
- const v1 = { ...v1Defaults, ...raw };
6329
- return {
6330
- version: 2,
6331
- boards: {
6332
- default: {
6333
- name: "Default",
6334
- columns: v1.columns,
6335
- nextCardId: v1.nextCardId,
6336
- defaultStatus: v1.defaultStatus,
6337
- defaultPriority: v1.defaultPriority
6338
- }
6339
- },
6340
- defaultBoard: "default",
6341
- featuresDirectory: v1.featuresDirectory,
6342
- aiAgent: v1.aiAgent,
6343
- defaultPriority: v1.defaultPriority,
6344
- defaultStatus: v1.defaultStatus,
6345
- showPriorityBadges: v1.showPriorityBadges,
6346
- showAssignee: v1.showAssignee,
6347
- showDueDate: v1.showDueDate,
6348
- showLabels: v1.showLabels,
6349
- showBuildWithAI: v1.showBuildWithAI,
6350
- showFileName: v1.showFileName,
6351
- compactMode: v1.compactMode,
6352
- markdownEditorMode: v1.markdownEditorMode,
6353
- showDeletedColumn: false,
6354
- port: 3e3
6355
- };
6356
- }
6357
- function readConfig(workspaceRoot) {
6358
- const filePath = configPath(workspaceRoot);
6359
- const defaults = { ...DEFAULT_CONFIG, boards: { default: { ...DEFAULT_BOARD_CONFIG, columns: [...DEFAULT_COLUMNS] } } };
6360
- try {
6361
- const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
6362
- if (!raw.version || raw.version === 1) {
6363
- const v2 = migrateConfigV1ToV2(raw);
6364
- writeConfig(workspaceRoot, v2);
6365
- return v2;
6366
- }
6367
- const config = { ...defaults, ...raw };
6368
- if (!config.boards || Object.keys(config.boards).length === 0) {
6369
- config.boards = defaults.boards;
6390
+ if (ib2 < b) {
6391
+ return ib2;
6370
6392
  }
6371
- return config;
6372
- } catch {
6373
- return defaults;
6393
+ const res = decrementInteger(ib2, digits);
6394
+ if (res == null) {
6395
+ throw new Error("cannot decrement any more");
6396
+ }
6397
+ return res;
6374
6398
  }
6375
- }
6376
- function writeConfig(workspaceRoot, config) {
6377
- const filePath = configPath(workspaceRoot);
6378
- fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
6379
- }
6380
- function getBoardConfig(workspaceRoot, boardId) {
6381
- const config = readConfig(workspaceRoot);
6382
- const resolvedId = boardId || config.defaultBoard;
6383
- const board = config.boards[resolvedId];
6384
- if (!board) {
6385
- throw new Error(`Board '${resolvedId}' not found`);
6399
+ if (b == null) {
6400
+ const ia2 = getIntegerPart(a);
6401
+ const fa2 = a.slice(ia2.length);
6402
+ const i2 = incrementInteger(ia2, digits);
6403
+ return i2 == null ? ia2 + midpoint(fa2, null, digits) : i2;
6386
6404
  }
6387
- return board;
6388
- }
6389
- function allocateCardId(workspaceRoot, boardId) {
6390
- const config = readConfig(workspaceRoot);
6391
- const resolvedId = boardId || config.defaultBoard;
6392
- const board = config.boards[resolvedId];
6393
- if (!board) {
6394
- throw new Error(`Board '${resolvedId}' not found`);
6405
+ const ia = getIntegerPart(a);
6406
+ const fa = a.slice(ia.length);
6407
+ const ib = getIntegerPart(b);
6408
+ const fb = b.slice(ib.length);
6409
+ if (ia === ib) {
6410
+ return ia + midpoint(fa, fb, digits);
6395
6411
  }
6396
- const id = board.nextCardId;
6397
- board.nextCardId = id + 1;
6398
- writeConfig(workspaceRoot, config);
6399
- return id;
6400
- }
6401
- function syncCardIdCounter(workspaceRoot, boardId, existingIds) {
6402
- if (existingIds.length === 0)
6403
- return;
6404
- const maxId = Math.max(...existingIds);
6405
- const config = readConfig(workspaceRoot);
6406
- const resolvedId = boardId || config.defaultBoard;
6407
- const board = config.boards[resolvedId];
6408
- if (!board)
6409
- return;
6410
- if (board.nextCardId <= maxId) {
6411
- board.nextCardId = maxId + 1;
6412
- writeConfig(workspaceRoot, config);
6412
+ const i = incrementInteger(ia, digits);
6413
+ if (i == null) {
6414
+ throw new Error("cannot increment any more");
6413
6415
  }
6416
+ if (i < b) {
6417
+ return i;
6418
+ }
6419
+ return ia + midpoint(fa, null, digits);
6414
6420
  }
6415
- function configToSettings(config) {
6416
- return {
6417
- showPriorityBadges: config.showPriorityBadges,
6418
- showAssignee: config.showAssignee,
6419
- showDueDate: config.showDueDate,
6420
- showLabels: config.showLabels,
6421
- showBuildWithAI: config.showBuildWithAI,
6422
- showFileName: config.showFileName,
6423
- compactMode: config.compactMode,
6424
- markdownEditorMode: config.markdownEditorMode,
6425
- showDeletedColumn: config.showDeletedColumn,
6426
- defaultPriority: config.defaultPriority,
6427
- defaultStatus: config.defaultStatus
6428
- };
6429
- }
6430
- function settingsToConfig(config, settings) {
6431
- return {
6432
- ...config,
6433
- showPriorityBadges: settings.showPriorityBadges,
6434
- showAssignee: settings.showAssignee,
6435
- showDueDate: settings.showDueDate,
6436
- showLabels: settings.showLabels,
6437
- showFileName: settings.showFileName,
6438
- compactMode: settings.compactMode,
6439
- showDeletedColumn: settings.showDeletedColumn,
6440
- defaultPriority: settings.defaultPriority,
6441
- defaultStatus: settings.defaultStatus
6442
- };
6421
+ function generateNKeysBetween(a, b, n, digits = BASE_62_DIGITS) {
6422
+ if (n === 0) {
6423
+ return [];
6424
+ }
6425
+ if (n === 1) {
6426
+ return [generateKeyBetween(a, b, digits)];
6427
+ }
6428
+ if (b == null) {
6429
+ let c2 = generateKeyBetween(a, b, digits);
6430
+ const result = [c2];
6431
+ for (let i = 0; i < n - 1; i++) {
6432
+ c2 = generateKeyBetween(c2, b, digits);
6433
+ result.push(c2);
6434
+ }
6435
+ return result;
6436
+ }
6437
+ if (a == null) {
6438
+ let c2 = generateKeyBetween(a, b, digits);
6439
+ const result = [c2];
6440
+ for (let i = 0; i < n - 1; i++) {
6441
+ c2 = generateKeyBetween(a, c2, digits);
6442
+ result.push(c2);
6443
+ }
6444
+ result.reverse();
6445
+ return result;
6446
+ }
6447
+ const mid = Math.floor(n / 2);
6448
+ const c = generateKeyBetween(a, b, digits);
6449
+ return [
6450
+ ...generateNKeysBetween(a, c, mid, digits),
6451
+ c,
6452
+ ...generateNKeysBetween(c, b, n - mid - 1, digits)
6453
+ ];
6443
6454
  }
6444
6455
 
6445
6456
  // src/sdk/parser.ts
@@ -9098,6 +9109,7 @@ function renamed(from, to) {
9098
9109
  throw new Error("Function yaml." + from + " is removed in js-yaml 4. Use yaml." + to + " instead, which is now safe by default.");
9099
9110
  };
9100
9111
  }
9112
+ var JSON_SCHEMA = json;
9101
9113
  var load = loader.load;
9102
9114
  var loadAll = loader.loadAll;
9103
9115
  var dump = dumper.dump;
@@ -9136,48 +9148,26 @@ function parseFeatureFile(content, filePath) {
9136
9148
  return null;
9137
9149
  const frontmatter = frontmatterMatch[1];
9138
9150
  const rest = frontmatterMatch[2] || "";
9139
- const getValue = (key) => {
9140
- const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
9141
- if (!match)
9151
+ let parsed;
9152
+ try {
9153
+ const loaded = load(frontmatter, { schema: JSON_SCHEMA });
9154
+ if (!loaded || typeof loaded !== "object" || Array.isArray(loaded))
9155
+ return null;
9156
+ parsed = loaded;
9157
+ } catch {
9158
+ return null;
9159
+ }
9160
+ const str2 = (key) => {
9161
+ const val = parsed[key];
9162
+ if (val == null)
9142
9163
  return "";
9143
- const value = match[1].trim().replace(/^["']|["']$/g, "");
9144
- return value === "null" ? "" : value;
9164
+ return String(val);
9145
9165
  };
9146
- const getArrayValue = (key) => {
9147
- const match = frontmatter.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, "m"));
9148
- if (!match)
9166
+ const arr = (key) => {
9167
+ const val = parsed[key];
9168
+ if (!Array.isArray(val))
9149
9169
  return [];
9150
- return match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
9151
- };
9152
- const getMetadata = () => {
9153
- const lines = frontmatter.split("\n");
9154
- let metaStart = -1;
9155
- for (let j = 0; j < lines.length; j++) {
9156
- if (/^metadata:\s*$/.test(lines[j])) {
9157
- metaStart = j + 1;
9158
- break;
9159
- }
9160
- }
9161
- if (metaStart === -1)
9162
- return void 0;
9163
- const indentedLines = [];
9164
- for (let j = metaStart; j < lines.length; j++) {
9165
- if (/^\s/.test(lines[j]) || lines[j].trim() === "") {
9166
- indentedLines.push(lines[j]);
9167
- } else {
9168
- break;
9169
- }
9170
- }
9171
- if (indentedLines.length === 0)
9172
- return void 0;
9173
- try {
9174
- const parsed = load(indentedLines.join("\n"));
9175
- if (parsed && typeof parsed === "object")
9176
- return parsed;
9177
- return void 0;
9178
- } catch {
9179
- return void 0;
9180
- }
9170
+ return val.filter((v) => v != null).map(String);
9181
9171
  };
9182
9172
  const sections = rest.split(/\n---\n/);
9183
9173
  let body = sections[0] || "";
@@ -9198,53 +9188,52 @@ ${section}`;
9198
9188
  i += 1;
9199
9189
  }
9200
9190
  }
9201
- const meta = getMetadata();
9191
+ const actions = arr("actions");
9192
+ const rawMeta = parsed.metadata;
9193
+ const meta = rawMeta != null && typeof rawMeta === "object" && !Array.isArray(rawMeta) ? rawMeta : void 0;
9202
9194
  return {
9203
- id: getValue("id") || extractIdFromFilename(filePath),
9204
- status: getValue("status") || "backlog",
9205
- priority: getValue("priority") || "medium",
9206
- assignee: getValue("assignee") || null,
9207
- dueDate: getValue("dueDate") || null,
9208
- created: getValue("created") || (/* @__PURE__ */ new Date()).toISOString(),
9209
- modified: getValue("modified") || (/* @__PURE__ */ new Date()).toISOString(),
9210
- completedAt: getValue("completedAt") || null,
9211
- labels: getArrayValue("labels"),
9212
- attachments: getArrayValue("attachments"),
9195
+ version: typeof parsed.version === "number" ? parsed.version : parseInt(str2("version"), 10) || 0,
9196
+ id: str2("id") || extractIdFromFilename(filePath),
9197
+ status: str2("status") || "backlog",
9198
+ priority: str2("priority") || "medium",
9199
+ assignee: parsed.assignee != null ? String(parsed.assignee) : null,
9200
+ dueDate: parsed.dueDate != null ? String(parsed.dueDate) : null,
9201
+ created: str2("created") || (/* @__PURE__ */ new Date()).toISOString(),
9202
+ modified: str2("modified") || (/* @__PURE__ */ new Date()).toISOString(),
9203
+ completedAt: parsed.completedAt != null ? String(parsed.completedAt) : null,
9204
+ labels: arr("labels"),
9205
+ attachments: arr("attachments"),
9213
9206
  comments,
9214
- order: getValue("order") || "a0",
9207
+ order: str2("order") || "a0",
9215
9208
  content: body.trim(),
9216
9209
  ...meta ? { metadata: meta } : {},
9210
+ ...actions.length > 0 ? { actions } : {},
9217
9211
  filePath
9218
9212
  };
9219
9213
  }
9220
9214
  function serializeFeature(feature) {
9221
- const lines = [
9222
- "---",
9223
- `id: "${feature.id}"`,
9224
- `status: "${feature.status}"`,
9225
- `priority: "${feature.priority}"`,
9226
- `assignee: ${feature.assignee ? `"${feature.assignee}"` : "null"}`,
9227
- `dueDate: ${feature.dueDate ? `"${feature.dueDate}"` : "null"}`,
9228
- `created: "${feature.created}"`,
9229
- `modified: "${feature.modified}"`,
9230
- `completedAt: ${feature.completedAt ? `"${feature.completedAt}"` : "null"}`,
9231
- `labels: [${feature.labels.map((l) => `"${l}"`).join(", ")}]`,
9232
- `attachments: [${(feature.attachments || []).map((a) => `"${a}"`).join(", ")}]`,
9233
- `order: "${feature.order}"`
9234
- ];
9235
- if (feature.metadata && Object.keys(feature.metadata).length > 0) {
9236
- const metaYaml = dump(feature.metadata, { indent: 2, lineWidth: -1 });
9237
- lines.push("metadata:");
9238
- for (const line of metaYaml.trimEnd().split("\n")) {
9239
- lines.push(" " + line);
9240
- }
9241
- }
9242
- lines.push("---");
9243
- lines.push("");
9244
- const frontmatter = lines.join("\n");
9245
- let result = frontmatter + feature.content;
9246
- const comments = feature.comments || [];
9247
- for (const comment of comments) {
9215
+ const frontmatterObj = {
9216
+ version: feature.version ?? CARD_FORMAT_VERSION,
9217
+ id: feature.id,
9218
+ status: feature.status,
9219
+ priority: feature.priority,
9220
+ assignee: feature.assignee ?? null,
9221
+ dueDate: feature.dueDate ?? null,
9222
+ created: feature.created,
9223
+ modified: feature.modified,
9224
+ completedAt: feature.completedAt ?? null,
9225
+ labels: feature.labels,
9226
+ attachments: feature.attachments || [],
9227
+ order: feature.order,
9228
+ ...feature.actions?.length ? { actions: feature.actions } : {},
9229
+ ...feature.metadata && Object.keys(feature.metadata).length > 0 ? { metadata: feature.metadata } : {}
9230
+ };
9231
+ const yamlStr = dump(frontmatterObj, { lineWidth: -1, quotingType: '"', forceQuotes: true });
9232
+ let result = `---
9233
+ ${yamlStr}---
9234
+
9235
+ ${feature.content}`;
9236
+ for (const comment of feature.comments || []) {
9248
9237
  result += "\n\n---\n";
9249
9238
  result += `comment: true
9250
9239
  `;
@@ -9374,6 +9363,23 @@ async function migrateFileSystemToMultiBoard(featuresDir) {
9374
9363
  }
9375
9364
  }
9376
9365
 
9366
+ // src/sdk/metaUtils.ts
9367
+ function getNestedValue(obj, path8) {
9368
+ return path8.split(".").reduce((curr, key) => curr != null && typeof curr === "object" ? curr[key] : void 0, obj);
9369
+ }
9370
+ function matchesMetaFilter(metadata, filter) {
9371
+ if (!metadata)
9372
+ return false;
9373
+ for (const [path8, needle] of Object.entries(filter)) {
9374
+ const value = getNestedValue(metadata, path8);
9375
+ if (value == null)
9376
+ return false;
9377
+ if (!String(value).toLowerCase().includes(needle.toLowerCase()))
9378
+ return false;
9379
+ }
9380
+ return true;
9381
+ }
9382
+
9377
9383
  // src/sdk/KanbanSDK.ts
9378
9384
  var KanbanSDK = class {
9379
9385
  /**
@@ -9712,12 +9718,18 @@ var KanbanSDK = class {
9712
9718
  * - Migrates legacy integer ordering to fractional indexing
9713
9719
  * - Syncs the card ID counter with existing cards
9714
9720
  *
9715
- * Cards are returned sorted by their fractional order key.
9721
+ * By default cards are returned sorted by their fractional order key (board order).
9722
+ * Pass a {@link CardSortOption} to sort by creation or modification date instead.
9716
9723
  *
9717
9724
  * @param columns - Optional array of status/column IDs to filter by.
9718
9725
  * When provided, ensures those subdirectories exist on disk.
9719
9726
  * @param boardId - Optional board ID. Defaults to the workspace's default board.
9720
- * @returns A promise resolving to an array of {@link Feature} card objects, sorted by order.
9727
+ * @param metaFilter - Optional map of dot-notation metadata paths to required substrings.
9728
+ * Only cards whose metadata contains all specified values (case-insensitive substring match)
9729
+ * are returned.
9730
+ * @param sort - Optional sort order. One of `'created:asc'`, `'created:desc'`,
9731
+ * `'modified:asc'`, `'modified:desc'`. Defaults to fractional board order.
9732
+ * @returns A promise resolving to an array of {@link Feature} card objects.
9721
9733
  *
9722
9734
  * @example
9723
9735
  * ```ts
@@ -9726,9 +9738,15 @@ var KanbanSDK = class {
9726
9738
  *
9727
9739
  * // List only cards in 'todo' and 'in-progress' columns on the 'bugs' board
9728
9740
  * const filtered = await sdk.listCards(['todo', 'in-progress'], 'bugs')
9741
+ *
9742
+ * // List cards where metadata.sprint contains 'Q1' and metadata.links.jira contains 'PROJ'
9743
+ * const q1Jira = await sdk.listCards(undefined, undefined, { 'sprint': 'Q1', 'links.jira': 'PROJ' })
9744
+ *
9745
+ * // List all cards sorted by creation date, newest first
9746
+ * const newest = await sdk.listCards(undefined, undefined, undefined, 'created:desc')
9729
9747
  * ```
9730
9748
  */
9731
- async listCards(columns, boardId) {
9749
+ async listCards(columns, boardId, metaFilter, sort) {
9732
9750
  await this._ensureMigrated();
9733
9751
  const boardDir = this._boardDir(boardId);
9734
9752
  const resolvedBoardId = this._resolveBoardId(boardId);
@@ -9802,7 +9820,16 @@ var KanbanSDK = class {
9802
9820
  if (numericIds.length > 0) {
9803
9821
  syncCardIdCounter(this.workspaceRoot, resolvedBoardId, numericIds);
9804
9822
  }
9805
- return cards.sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0);
9823
+ const filtered = metaFilter && Object.keys(metaFilter).length > 0 ? cards.filter((c) => matchesMetaFilter(c.metadata, metaFilter)) : cards;
9824
+ if (sort) {
9825
+ const [field, dir2] = sort.split(":");
9826
+ return filtered.sort((a, b) => {
9827
+ const aVal = field === "created" ? a.created : a.modified;
9828
+ const bVal = field === "created" ? b.created : b.modified;
9829
+ return dir2 === "asc" ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
9830
+ });
9831
+ }
9832
+ return filtered.sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0);
9806
9833
  }
9807
9834
  /**
9808
9835
  * Retrieves a single card by its ID.
@@ -9875,6 +9902,7 @@ var KanbanSDK = class {
9875
9902
  const cardsInStatus = cards.filter((c) => c.status === status).sort((a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0);
9876
9903
  const lastOrder = cardsInStatus.length > 0 ? cardsInStatus[cardsInStatus.length - 1].order : null;
9877
9904
  const card = {
9905
+ version: CARD_FORMAT_VERSION,
9878
9906
  id: String(numericId),
9879
9907
  boardId: resolvedBoardId,
9880
9908
  status,
@@ -9890,6 +9918,7 @@ var KanbanSDK = class {
9890
9918
  order: generateKeyBetween(lastOrder, null),
9891
9919
  content: data.content,
9892
9920
  ...data.metadata && Object.keys(data.metadata).length > 0 ? { metadata: data.metadata } : {},
9921
+ ...data.actions && data.actions.length > 0 ? { actions: data.actions } : {},
9893
9922
  filePath: getFeatureFilePath(boardDir, status, filename)
9894
9923
  };
9895
9924
  await fs4.mkdir(path5.dirname(card.filePath), { recursive: true });
@@ -9949,6 +9978,54 @@ var KanbanSDK = class {
9949
9978
  this.emitEvent("task.updated", sanitizeFeature(card));
9950
9979
  return card;
9951
9980
  }
9981
+ /**
9982
+ * Triggers a named action for a card by POSTing to the global `actionWebhookUrl`
9983
+ * configured in `.kanban.json`.
9984
+ *
9985
+ * The payload sent to the webhook is:
9986
+ * ```json
9987
+ * { "action": "retry", "board": "default", "list": "in-progress", "card": { ...sanitizedCard } }
9988
+ * ```
9989
+ *
9990
+ * @param cardId - The ID of the card to trigger the action for.
9991
+ * @param action - The action name string (e.g. `'retry'`, `'sendEmail'`).
9992
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
9993
+ * @returns A promise resolving when the webhook responds with 2xx.
9994
+ * @throws {Error} If no `actionWebhookUrl` is configured in `.kanban.json`.
9995
+ * @throws {Error} If the card is not found.
9996
+ * @throws {Error} If the webhook responds with a non-2xx status.
9997
+ *
9998
+ * @example
9999
+ * ```ts
10000
+ * await sdk.triggerAction('42', 'retry')
10001
+ * await sdk.triggerAction('42', 'sendEmail', 'bugs')
10002
+ * ```
10003
+ */
10004
+ async triggerAction(cardId, action, boardId) {
10005
+ const config = readConfig(this.workspaceRoot);
10006
+ const { actionWebhookUrl } = config;
10007
+ if (!actionWebhookUrl) {
10008
+ throw new Error("No action webhook URL configured. Set actionWebhookUrl in .kanban.json");
10009
+ }
10010
+ const card = await this.getCard(cardId, boardId);
10011
+ if (!card)
10012
+ throw new Error(`Card not found: ${cardId}`);
10013
+ const resolvedBoardId = card.boardId || this._resolveBoardId(boardId);
10014
+ const payload = {
10015
+ action,
10016
+ board: resolvedBoardId,
10017
+ list: card.status,
10018
+ card: sanitizeFeature(card)
10019
+ };
10020
+ const response = await fetch(actionWebhookUrl, {
10021
+ method: "POST",
10022
+ headers: { "Content-Type": "application/json" },
10023
+ body: JSON.stringify(payload)
10024
+ });
10025
+ if (!response.ok) {
10026
+ throw new Error(`Action webhook responded with ${response.status}: ${response.statusText}`);
10027
+ }
10028
+ }
9952
10029
  /**
9953
10030
  * Moves a card to a different status column and/or position within that column.
9954
10031
  *
@@ -10149,25 +10226,28 @@ var KanbanSDK = class {
10149
10226
  writeConfig(this.workspaceRoot, config);
10150
10227
  }
10151
10228
  /**
10152
- * Removes a label definition from the workspace configuration.
10153
- *
10154
- * This only removes the color/group definition — cards that use this
10155
- * label keep their label strings. Those labels will render with default
10156
- * gray styling in the UI.
10229
+ * Removes a label definition from the workspace configuration and cascades
10230
+ * the deletion to all cards by removing the label from their `labels` array.
10157
10231
  *
10158
10232
  * @param name - The label name to remove.
10159
10233
  *
10160
10234
  * @example
10161
10235
  * ```ts
10162
- * sdk.deleteLabel('bug')
10236
+ * await sdk.deleteLabel('bug')
10163
10237
  * ```
10164
10238
  */
10165
- deleteLabel(name) {
10239
+ async deleteLabel(name) {
10166
10240
  const config = readConfig(this.workspaceRoot);
10167
10241
  if (config.labels) {
10168
10242
  delete config.labels[name];
10169
10243
  writeConfig(this.workspaceRoot, config);
10170
10244
  }
10245
+ const cards = await this.listCards();
10246
+ for (const card of cards) {
10247
+ if (card.labels.includes(name)) {
10248
+ await this.updateCard(card.id, { labels: card.labels.filter((l) => l !== name) });
10249
+ }
10250
+ }
10171
10251
  }
10172
10252
  /**
10173
10253
  * Renames a label in the configuration and cascades the change to all cards.
@@ -10592,6 +10672,57 @@ var KanbanSDK = class {
10592
10672
  this.emitEvent("column.deleted", removed);
10593
10673
  return board.columns;
10594
10674
  }
10675
+ /**
10676
+ * Moves all cards in the specified column to the `deleted` (soft-delete) column.
10677
+ *
10678
+ * This is a non-destructive operation — cards are moved to the reserved
10679
+ * `deleted` status and can be restored or permanently deleted later.
10680
+ * The column itself is not removed.
10681
+ *
10682
+ * @param columnId - The ID of the column whose cards should be moved to `deleted`.
10683
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
10684
+ * @returns A promise resolving to the number of cards that were moved.
10685
+ * @throws {Error} If the column is `'deleted'` (no-op protection).
10686
+ *
10687
+ * @example
10688
+ * ```ts
10689
+ * const moved = await sdk.cleanupColumn('blocked')
10690
+ * console.log(`Moved ${moved} cards to deleted`)
10691
+ * ```
10692
+ */
10693
+ async cleanupColumn(columnId, boardId) {
10694
+ if (columnId === DELETED_STATUS_ID)
10695
+ return 0;
10696
+ const cards = await this.listCards(void 0, boardId);
10697
+ const cardsToMove = cards.filter((c) => c.status === columnId);
10698
+ for (const card of cardsToMove) {
10699
+ await this.moveCard(card.id, DELETED_STATUS_ID, 0, boardId);
10700
+ }
10701
+ return cardsToMove.length;
10702
+ }
10703
+ /**
10704
+ * Permanently deletes all cards currently in the `deleted` column.
10705
+ *
10706
+ * This is equivalent to "empty trash". All soft-deleted cards are
10707
+ * removed from disk. This operation cannot be undone.
10708
+ *
10709
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
10710
+ * @returns A promise resolving to the number of cards that were permanently deleted.
10711
+ *
10712
+ * @example
10713
+ * ```ts
10714
+ * const count = await sdk.purgeDeletedCards()
10715
+ * console.log(`Permanently deleted ${count} cards`)
10716
+ * ```
10717
+ */
10718
+ async purgeDeletedCards(boardId) {
10719
+ const cards = await this.listCards(void 0, boardId);
10720
+ const deleted = cards.filter((c) => c.status === DELETED_STATUS_ID);
10721
+ for (const card of deleted) {
10722
+ await this.permanentlyDeleteCard(card.id, boardId);
10723
+ }
10724
+ return deleted.length;
10725
+ }
10595
10726
  /**
10596
10727
  * Reorders the columns of a board.
10597
10728
  *
@@ -10816,6 +10947,17 @@ function startServer(featuresDir, port2, webviewDir) {
10816
10947
  const sdk = new KanbanSDK(absoluteFeaturesDir, {
10817
10948
  onEvent: (event, data) => fireWebhooks(workspaceRoot, event, data)
10818
10949
  });
10950
+ const VALID_SORTS = ["created:asc", "created:desc", "modified:asc", "modified:desc"];
10951
+ function applySortParam(result, sortParam) {
10952
+ if (!sortParam || !VALID_SORTS.includes(sortParam))
10953
+ return result;
10954
+ const [field, dir2] = sortParam.split(":");
10955
+ return [...result].sort((a, b) => {
10956
+ const aVal = field === "created" ? a.created : a.modified;
10957
+ const bVal = field === "created" ? b.created : b.modified;
10958
+ return dir2 === "asc" ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
10959
+ });
10960
+ }
10819
10961
  function readBody(req) {
10820
10962
  return new Promise((resolve5, reject) => {
10821
10963
  const chunks = [];
@@ -10918,6 +11060,7 @@ function startServer(featuresDir, port2, webviewDir) {
10918
11060
  dueDate: data.dueDate,
10919
11061
  labels: data.labels,
10920
11062
  metadata: data.metadata,
11063
+ actions: data.actions,
10921
11064
  boardId: currentBoardId
10922
11065
  });
10923
11066
  await loadFeatures();
@@ -10986,10 +11129,7 @@ function startServer(featuresDir, port2, webviewDir) {
10986
11129
  }
10987
11130
  async function doPurgeDeletedCards() {
10988
11131
  try {
10989
- const deletedCards = features.filter((f) => f.status === "deleted");
10990
- for (const card of deletedCards) {
10991
- await sdk.permanentlyDeleteCard(card.id, currentBoardId);
10992
- }
11132
+ await sdk.purgeDeletedCards(currentBoardId);
10993
11133
  await loadFeatures();
10994
11134
  broadcast(buildInitMessage());
10995
11135
  return true;
@@ -11036,6 +11176,20 @@ function startServer(featuresDir, port2, webviewDir) {
11036
11176
  return { removed: false, error: String(err) };
11037
11177
  }
11038
11178
  }
11179
+ async function doCleanupColumn(columnId) {
11180
+ try {
11181
+ migrating = true;
11182
+ await sdk.cleanupColumn(columnId, currentBoardId);
11183
+ await loadFeatures();
11184
+ broadcast(buildInitMessage());
11185
+ return true;
11186
+ } catch (err) {
11187
+ console.error("Failed to cleanup column:", err);
11188
+ return false;
11189
+ } finally {
11190
+ migrating = false;
11191
+ }
11192
+ }
11039
11193
  function doSaveSettings(newSettings) {
11040
11194
  sdk.updateSettings(newSettings);
11041
11195
  broadcast(buildInitMessage());
@@ -11164,6 +11318,7 @@ function startServer(featuresDir, port2, webviewDir) {
11164
11318
  break;
11165
11319
  currentEditingFeatureId = featureId;
11166
11320
  const frontmatter = {
11321
+ version: feature.version ?? 0,
11167
11322
  id: feature.id,
11168
11323
  status: feature.status,
11169
11324
  priority: feature.priority,
@@ -11175,7 +11330,8 @@ function startServer(featuresDir, port2, webviewDir) {
11175
11330
  labels: feature.labels,
11176
11331
  attachments: feature.attachments,
11177
11332
  order: feature.order,
11178
- metadata: feature.metadata
11333
+ metadata: feature.metadata,
11334
+ actions: feature.actions
11179
11335
  };
11180
11336
  ws.send(JSON.stringify({ type: "featureContent", featureId: feature.id, content: feature.content, frontmatter, comments: feature.comments || [] }));
11181
11337
  break;
@@ -11191,7 +11347,8 @@ function startServer(featuresDir, port2, webviewDir) {
11191
11347
  assignee: fm.assignee,
11192
11348
  dueDate: fm.dueDate,
11193
11349
  labels: fm.labels,
11194
- attachments: fm.attachments
11350
+ attachments: fm.attachments,
11351
+ actions: fm.actions
11195
11352
  });
11196
11353
  break;
11197
11354
  }
@@ -11219,11 +11376,15 @@ function startServer(featuresDir, port2, webviewDir) {
11219
11376
  case "removeColumn":
11220
11377
  await doRemoveColumn(msg.columnId);
11221
11378
  break;
11379
+ case "cleanupColumn":
11380
+ await doCleanupColumn(msg.columnId);
11381
+ break;
11222
11382
  case "removeAttachment": {
11223
11383
  const featureId = msg.featureId;
11224
11384
  const feature = await doRemoveAttachment(featureId, msg.attachment);
11225
11385
  if (feature && currentEditingFeatureId === featureId) {
11226
11386
  const frontmatter = {
11387
+ version: feature.version ?? 0,
11227
11388
  id: feature.id,
11228
11389
  status: feature.status,
11229
11390
  priority: feature.priority,
@@ -11234,7 +11395,8 @@ function startServer(featuresDir, port2, webviewDir) {
11234
11395
  completedAt: feature.completedAt,
11235
11396
  labels: feature.labels,
11236
11397
  attachments: feature.attachments,
11237
- order: feature.order
11398
+ order: feature.order,
11399
+ actions: feature.actions
11238
11400
  };
11239
11401
  ws.send(JSON.stringify({ type: "featureContent", featureId: feature.id, content: feature.content, frontmatter, comments: feature.comments || [] }));
11240
11402
  }
@@ -11247,6 +11409,7 @@ function startServer(featuresDir, port2, webviewDir) {
11247
11409
  const feature = features.find((f) => f.id === msg.featureId);
11248
11410
  if (feature && currentEditingFeatureId === msg.featureId) {
11249
11411
  const frontmatter = {
11412
+ version: feature.version ?? 0,
11250
11413
  id: feature.id,
11251
11414
  status: feature.status,
11252
11415
  priority: feature.priority,
@@ -11257,7 +11420,8 @@ function startServer(featuresDir, port2, webviewDir) {
11257
11420
  completedAt: feature.completedAt,
11258
11421
  labels: feature.labels,
11259
11422
  attachments: feature.attachments,
11260
- order: feature.order
11423
+ order: feature.order,
11424
+ actions: feature.actions
11261
11425
  };
11262
11426
  ws.send(JSON.stringify({ type: "featureContent", featureId: feature.id, content: feature.content, frontmatter, comments: feature.comments || [] }));
11263
11427
  }
@@ -11270,6 +11434,7 @@ function startServer(featuresDir, port2, webviewDir) {
11270
11434
  const feature = features.find((f) => f.id === msg.featureId);
11271
11435
  if (feature && currentEditingFeatureId === msg.featureId) {
11272
11436
  const frontmatter = {
11437
+ version: feature.version ?? 0,
11273
11438
  id: feature.id,
11274
11439
  status: feature.status,
11275
11440
  priority: feature.priority,
@@ -11280,7 +11445,8 @@ function startServer(featuresDir, port2, webviewDir) {
11280
11445
  completedAt: feature.completedAt,
11281
11446
  labels: feature.labels,
11282
11447
  attachments: feature.attachments,
11283
- order: feature.order
11448
+ order: feature.order,
11449
+ actions: feature.actions
11284
11450
  };
11285
11451
  ws.send(JSON.stringify({ type: "featureContent", featureId: feature.id, content: feature.content, frontmatter, comments: feature.comments || [] }));
11286
11452
  }
@@ -11291,6 +11457,7 @@ function startServer(featuresDir, port2, webviewDir) {
11291
11457
  const feature = features.find((f) => f.id === msg.featureId);
11292
11458
  if (feature && currentEditingFeatureId === msg.featureId) {
11293
11459
  const frontmatter = {
11460
+ version: feature.version ?? 0,
11294
11461
  id: feature.id,
11295
11462
  status: feature.status,
11296
11463
  priority: feature.priority,
@@ -11301,7 +11468,8 @@ function startServer(featuresDir, port2, webviewDir) {
11301
11468
  completedAt: feature.completedAt,
11302
11469
  labels: feature.labels,
11303
11470
  attachments: feature.attachments,
11304
- order: feature.order
11471
+ order: feature.order,
11472
+ actions: feature.actions
11305
11473
  };
11306
11474
  ws.send(JSON.stringify({ type: "featureContent", featureId: feature.id, content: feature.content, frontmatter, comments: feature.comments || [] }));
11307
11475
  }
@@ -11359,13 +11527,26 @@ function startServer(featuresDir, port2, webviewDir) {
11359
11527
  }
11360
11528
  case "renameLabel": {
11361
11529
  await sdk.renameLabel(msg.oldName, msg.newName);
11530
+ await loadFeatures();
11362
11531
  broadcast({ type: "labelsUpdated", labels: sdk.getLabels() });
11363
11532
  broadcast(buildInitMessage());
11364
11533
  break;
11365
11534
  }
11366
11535
  case "deleteLabel": {
11367
- sdk.deleteLabel(msg.name);
11536
+ await sdk.deleteLabel(msg.name);
11537
+ await loadFeatures();
11368
11538
  broadcast({ type: "labelsUpdated", labels: sdk.getLabels() });
11539
+ broadcast(buildInitMessage());
11540
+ break;
11541
+ }
11542
+ case "triggerAction": {
11543
+ const { featureId, action, callbackKey } = msg;
11544
+ try {
11545
+ await sdk.triggerAction(featureId, action);
11546
+ ws.send(JSON.stringify({ type: "actionResult", callbackKey }));
11547
+ } catch (err) {
11548
+ ws.send(JSON.stringify({ type: "actionResult", callbackKey, error: String(err) }));
11549
+ }
11369
11550
  break;
11370
11551
  }
11371
11552
  case "openFile":
@@ -11486,6 +11667,14 @@ function startServer(featuresDir, port2, webviewDir) {
11486
11667
  const groupLabels = sdk.getLabelsInGroup(labelGroup);
11487
11668
  result = result.filter((f) => f.labels.some((l) => groupLabels.includes(l)));
11488
11669
  }
11670
+ const metaFilter = {};
11671
+ for (const [param, value] of url.searchParams.entries()) {
11672
+ if (param.startsWith("meta."))
11673
+ metaFilter[param.slice(5)] = value;
11674
+ }
11675
+ if (Object.keys(metaFilter).length > 0)
11676
+ result = result.filter((f) => matchesMetaFilter(f.metadata, metaFilter));
11677
+ result = applySortParam(result, url.searchParams.get("sort"));
11489
11678
  return jsonOk(res, result);
11490
11679
  } catch (err) {
11491
11680
  return jsonError(res, 400, String(err));
@@ -11507,6 +11696,7 @@ function startServer(featuresDir, port2, webviewDir) {
11507
11696
  dueDate: body.dueDate || null,
11508
11697
  labels: body.labels || [],
11509
11698
  metadata: body.metadata,
11699
+ actions: body.actions,
11510
11700
  boardId
11511
11701
  });
11512
11702
  return jsonOk(res, sanitizeFeature(feature), 201);
@@ -11552,6 +11742,21 @@ function startServer(featuresDir, port2, webviewDir) {
11552
11742
  return jsonError(res, 400, String(err));
11553
11743
  }
11554
11744
  }
11745
+ params = route("POST", "/api/boards/:boardId/tasks/:id/actions/:action");
11746
+ if (params) {
11747
+ try {
11748
+ const { boardId, id, action } = params;
11749
+ await sdk.triggerAction(id, action, boardId);
11750
+ res.writeHead(204);
11751
+ res.end();
11752
+ return;
11753
+ } catch (err) {
11754
+ const msg = String(err);
11755
+ if (msg.includes("Card not found"))
11756
+ return jsonError(res, 404, msg);
11757
+ return jsonError(res, 400, msg);
11758
+ }
11759
+ }
11555
11760
  params = route("DELETE", "/api/boards/:boardId/tasks/:id/permanent");
11556
11761
  if (params) {
11557
11762
  try {
@@ -11609,6 +11814,14 @@ function startServer(featuresDir, port2, webviewDir) {
11609
11814
  const groupLabels = sdk.getLabelsInGroup(labelGroup);
11610
11815
  result = result.filter((f) => f.labels.some((l) => groupLabels.includes(l)));
11611
11816
  }
11817
+ const metaFilter = {};
11818
+ for (const [param, value] of url.searchParams.entries()) {
11819
+ if (param.startsWith("meta."))
11820
+ metaFilter[param.slice(5)] = value;
11821
+ }
11822
+ if (Object.keys(metaFilter).length > 0)
11823
+ result = result.filter((f) => matchesMetaFilter(f.metadata, metaFilter));
11824
+ result = applySortParam(result, url.searchParams.get("sort"));
11612
11825
  return jsonOk(res, result);
11613
11826
  }
11614
11827
  params = route("POST", "/api/tasks");
@@ -11622,7 +11835,8 @@ function startServer(featuresDir, port2, webviewDir) {
11622
11835
  assignee: body.assignee || null,
11623
11836
  dueDate: body.dueDate || null,
11624
11837
  labels: body.labels || [],
11625
- metadata: body.metadata
11838
+ metadata: body.metadata,
11839
+ actions: body.actions
11626
11840
  };
11627
11841
  if (!data.content)
11628
11842
  return jsonError(res, 400, "content is required");
@@ -11670,6 +11884,21 @@ function startServer(featuresDir, port2, webviewDir) {
11670
11884
  return jsonError(res, 400, String(err));
11671
11885
  }
11672
11886
  }
11887
+ params = route("POST", "/api/tasks/:id/actions/:action");
11888
+ if (params) {
11889
+ try {
11890
+ const { id, action } = params;
11891
+ await sdk.triggerAction(id, action);
11892
+ res.writeHead(204);
11893
+ res.end();
11894
+ return;
11895
+ } catch (err) {
11896
+ const msg = String(err);
11897
+ if (msg.includes("Card not found"))
11898
+ return jsonError(res, 404, msg);
11899
+ return jsonError(res, 400, msg);
11900
+ }
11901
+ }
11673
11902
  params = route("DELETE", "/api/tasks/:id/permanent");
11674
11903
  if (params) {
11675
11904
  const { id } = params;
@@ -11919,6 +12148,9 @@ function startServer(featuresDir, port2, webviewDir) {
11919
12148
  if (!newName)
11920
12149
  return jsonError(res, 400, "newName is required");
11921
12150
  await sdk.renameLabel(name, newName);
12151
+ await loadFeatures();
12152
+ broadcast({ type: "labelsUpdated", labels: sdk.getLabels() });
12153
+ broadcast(buildInitMessage());
11922
12154
  return jsonOk(res, sdk.getLabels());
11923
12155
  } catch (err) {
11924
12156
  return jsonError(res, 400, String(err));
@@ -11928,7 +12160,10 @@ function startServer(featuresDir, port2, webviewDir) {
11928
12160
  if (params) {
11929
12161
  try {
11930
12162
  const name = decodeURIComponent(params.name);
11931
- sdk.deleteLabel(name);
12163
+ await sdk.deleteLabel(name);
12164
+ await loadFeatures();
12165
+ broadcast({ type: "labelsUpdated", labels: sdk.getLabels() });
12166
+ broadcast(buildInitMessage());
11932
12167
  return jsonOk(res, { success: true });
11933
12168
  } catch (err) {
11934
12169
  return jsonError(res, 400, String(err));
@@ -11954,6 +12189,7 @@ function startServer(featuresDir, port2, webviewDir) {
11954
12189
  const feature = features.find((f) => f.id === featureId);
11955
12190
  if (feature && currentEditingFeatureId === featureId) {
11956
12191
  const frontmatter = {
12192
+ version: feature.version ?? 0,
11957
12193
  id: feature.id,
11958
12194
  status: feature.status,
11959
12195
  priority: feature.priority,
@@ -11964,7 +12200,9 @@ function startServer(featuresDir, port2, webviewDir) {
11964
12200
  completedAt: feature.completedAt,
11965
12201
  labels: feature.labels,
11966
12202
  attachments: feature.attachments,
11967
- order: feature.order
12203
+ order: feature.order,
12204
+ metadata: feature.metadata,
12205
+ actions: feature.actions
11968
12206
  };
11969
12207
  broadcast({ type: "featureContent", featureId: feature.id, content: feature.content, frontmatter, comments: feature.comments || [] });
11970
12208
  }
@@ -12071,6 +12309,7 @@ function startServer(featuresDir, port2, webviewDir) {
12071
12309
  const currentContent = serializeFeature(editingFeature);
12072
12310
  if (currentContent !== lastWrittenContent) {
12073
12311
  const frontmatter = {
12312
+ version: editingFeature.version ?? 0,
12074
12313
  id: editingFeature.id,
12075
12314
  status: editingFeature.status,
12076
12315
  priority: editingFeature.priority,
@@ -12081,7 +12320,9 @@ function startServer(featuresDir, port2, webviewDir) {
12081
12320
  completedAt: editingFeature.completedAt,
12082
12321
  labels: editingFeature.labels,
12083
12322
  attachments: editingFeature.attachments,
12084
- order: editingFeature.order
12323
+ order: editingFeature.order,
12324
+ metadata: editingFeature.metadata,
12325
+ actions: editingFeature.actions
12085
12326
  };
12086
12327
  broadcast({ type: "featureContent", featureId: editingFeature.id, content: editingFeature.content, frontmatter, comments: editingFeature.comments || [] });
12087
12328
  }
@@ -12105,9 +12346,9 @@ function startServer(featuresDir, port2, webviewDir) {
12105
12346
  }
12106
12347
 
12107
12348
  // src/standalone/index.ts
12108
- function parseArgs(args) {
12349
+ function parseArgs(args, defaultPort) {
12109
12350
  let dir2 = ".kanban";
12110
- let port2 = 3e3;
12351
+ let port2 = defaultPort;
12111
12352
  let noBrowser2 = false;
12112
12353
  for (let i = 0; i < args.length; i++) {
12113
12354
  switch (args[i]) {
@@ -12129,7 +12370,7 @@ Usage: kanban-md [options]
12129
12370
 
12130
12371
  Options:
12131
12372
  -d, --dir <path> Features directory (default: .kanban)
12132
- -p, --port <number> Port to listen on (default: 3000)
12373
+ -p, --port <number> Port to listen on (default: .kanban.json port or 3000)
12133
12374
  --no-browser Don't open browser automatically
12134
12375
  -h, --help Show this help message
12135
12376
 
@@ -12145,7 +12386,8 @@ REST API available at http://localhost:<port>/api
12145
12386
  }
12146
12387
  return { dir: dir2, port: port2, noBrowser: noBrowser2 };
12147
12388
  }
12148
- var { dir, port, noBrowser } = parseArgs(process.argv.slice(2));
12389
+ var configPort = readConfig(process.cwd()).port;
12390
+ var { dir, port, noBrowser } = parseArgs(process.argv.slice(2), configPort);
12149
12391
  var server = startServer(dir, port);
12150
12392
  if (!noBrowser) {
12151
12393
  server.on("listening", async () => {