kanbango 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mcp-server.js CHANGED
@@ -293,14 +293,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
293
293
  properties: {
294
294
  operation: {
295
295
  type: 'string',
296
- enum: ['list', 'show', 'help'],
297
- description: 'list=scan board; show=one task (needs task_id); help=token playbook (no board I/O)',
296
+ enum: ['list', 'show', 'list_epics', 'show_epic', 'help'],
297
+ description: 'list/show=tasks; list_epics/show_epic=initiative containers; help=token playbook',
298
298
  default: 'list'
299
299
  },
300
300
  task_id: {
301
301
  type: 'string',
302
302
  description: "Required for show. Numeric id: '014' or '14'."
303
303
  },
304
+ epic_id: {
305
+ type: 'string',
306
+ description: "Required for show_epic. Epic id like 'E001'."
307
+ },
304
308
  col: {
305
309
  type: 'string',
306
310
  enum: COLS,
@@ -308,7 +312,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
308
312
  },
309
313
  epic: {
310
314
  type: 'string',
311
- description: 'Filter list by epic group'
315
+ description: 'Filter list by epic id or title'
312
316
  },
313
317
  view: {
314
318
  type: 'string',
@@ -332,12 +336,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
332
336
  properties: {
333
337
  action: {
334
338
  type: 'string',
335
- enum: ['create', 'move', 'update', 'plan_create', 'plan_advance', 'plan_evidence', 'plan_done', 'plan_status'],
336
- description: 'create|move|update daily; plan_* only for accepted multi-step work with tests'
339
+ enum: [
340
+ 'create',
341
+ 'move',
342
+ 'update',
343
+ 'epic_create',
344
+ 'epic_update',
345
+ 'plan_create',
346
+ 'plan_advance',
347
+ 'plan_evidence',
348
+ 'plan_done',
349
+ 'plan_status'
350
+ ],
351
+ description: 'create|move|update daily; epic_create|epic_update for containers; plan_* for multi-step work'
337
352
  },
338
353
  title: {
339
354
  type: 'string',
340
- description: 'Required for create and plan_create'
355
+ description: 'Required for create, plan_create, epic_create'
341
356
  },
342
357
  col: {
343
358
  type: 'string',
@@ -348,11 +363,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
348
363
  epic: {
349
364
  type: 'string',
350
365
  default: '—',
351
- description: 'Epic group (create/update/plan_create)'
366
+ description: 'Epic id or title (create/update/plan_create). Prefer E001.'
367
+ },
368
+ epic_id: {
369
+ type: 'string',
370
+ description: 'Required for epic_update; optional filter/link id'
352
371
  },
353
372
  description: {
354
373
  type: 'string',
355
- description: 'Why/context (recommended on create)'
374
+ description: 'Why/context (recommended on create / epic_create)'
375
+ },
376
+ goals: {
377
+ type: 'string',
378
+ description: 'Epic outcome one-liner (recommended on epic_create)'
356
379
  },
357
380
  specs: {
358
381
  type: 'string',
@@ -411,7 +434,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
411
434
  return: {
412
435
  type: 'string',
413
436
  enum: ['none', 'summary', 'full'],
414
- description: 'move/update response size. Prefer none. Default summary. create always returns full task once.'
437
+ description: 'move/update/epic_update response size. Prefer none. Default summary. create/epic_create return full once.'
415
438
  },
416
439
  index: {
417
440
  type: 'integer',
@@ -493,12 +516,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
493
516
  const readOptions = normalizeReadOptions(args, 'summary');
494
517
 
495
518
  if (operation === 'list') {
496
- let tasks = await kanban.allEpics();
519
+ await kanban.migrateEpicGroups();
520
+ let tasks = await kanban.allTasks();
497
521
  if (args.col) {
498
522
  tasks = tasks.filter((task) => task.column === args.col);
499
523
  }
500
- if (args.epic) {
501
- tasks = tasks.filter((task) => task.epic_group === args.epic);
524
+ if (args.epic || args.epic_id) {
525
+ const filter = args.epic_id || args.epic;
526
+ tasks = tasks.filter((task) => kanban.taskMatchesEpicFilter(task, filter));
502
527
  }
503
528
 
504
529
  result = tasks.map((task) => kanban.shapeTask(task, readOptions));
@@ -512,10 +537,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
512
537
  }
513
538
 
514
539
  result = kanban.shapeTask(await kanban.getTask(args.task_id), readOptions);
540
+ } else if (operation === 'list_epics') {
541
+ await kanban.migrateEpicGroups();
542
+ const tasks = await kanban.allTasks();
543
+ const epics = await kanban.listEpicEntities();
544
+ const epicView = args.view === 'full' || args.view === 'planning' || args.view === 'execution'
545
+ ? (args.view === 'execution' ? 'planning' : args.view)
546
+ : 'summary';
547
+ result = epics.map((epic) => kanban.shapeEpic(epic, tasks, {
548
+ view: Array.isArray(args.fields) && args.fields.length > 0 ? undefined : epicView,
549
+ fields: args.fields
550
+ }));
551
+ } else if (operation === 'show_epic') {
552
+ const epicRef = args.epic_id || args.epic;
553
+ if (!epicRef) {
554
+ throw invalidRequest(
555
+ "epic_id is required for 'show_epic' operation",
556
+ 'Provide an epic id like E001',
557
+ { operation }
558
+ );
559
+ }
560
+ await kanban.migrateEpicGroups();
561
+ const link = await kanban.resolveEpicRef(epicRef, { createIfMissing: false });
562
+ const epic = await kanban.getEpicEntity(link.epic_id);
563
+ const tasks = await kanban.allTasks();
564
+ result = kanban.shapeEpic(epic, tasks, {
565
+ view: args.view || 'full',
566
+ fields: args.fields
567
+ });
515
568
  } else {
516
569
  throw invalidRequest(
517
570
  `Unknown operation: ${operation}`,
518
- 'Use one of: list, show, help',
571
+ 'Use one of: list, show, list_epics, show_epic, help',
519
572
  { operation }
520
573
  );
521
574
  }
@@ -538,7 +591,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
538
591
  subtasks: args.subtasks,
539
592
  notes: args.notes
540
593
  };
541
- const created = await kanban.doCreate(args.title, args.col || 'planned', args.epic || '—', createPayload);
594
+ const epicRef = args.epic_id || args.epic || '—';
595
+ const created = await kanban.doCreate(args.title, args.col || 'planned', epicRef, createPayload);
542
596
  const shaped = kanban.shapeTask(created, { view: 'full' });
543
597
  const warnings = kanban.createFieldWarnings(createPayload);
544
598
  result = warnings.length > 0
@@ -547,6 +601,54 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
547
601
  break;
548
602
  }
549
603
 
604
+ case 'epic_create': {
605
+ const epicPayload = {
606
+ description: args.description,
607
+ goals: args.goals,
608
+ in_scope: args.in_scope,
609
+ out_of_scope: args.out_of_scope,
610
+ notes: args.notes
611
+ };
612
+ const createdEpic = await kanban.doCreateEpic(args.title, epicPayload);
613
+ const shapedEpic = kanban.shapeEpic(createdEpic, [], { view: 'full' });
614
+ const epicWarnings = kanban.createEpicFieldWarnings(epicPayload);
615
+ result = epicWarnings.length > 0
616
+ ? {
617
+ ...shapedEpic,
618
+ warnings: epicWarnings,
619
+ missing_recommended: kanban.missingRecommendedEpicCreateFields(epicPayload)
620
+ }
621
+ : shapedEpic;
622
+ break;
623
+ }
624
+
625
+ case 'epic_update': {
626
+ const epicId = args.epic_id || args.epic;
627
+ if (!epicId) {
628
+ throw invalidRequest(
629
+ "epic_id is required for 'epic_update'",
630
+ 'Provide an epic id like E001',
631
+ { action }
632
+ );
633
+ }
634
+ const epicPatch = args.patch ? { ...args.patch } : {};
635
+ if (args.title !== undefined) epicPatch.title = args.title;
636
+ if (args.description !== undefined) epicPatch.description = args.description;
637
+ if (args.goals !== undefined) epicPatch.goals = args.goals;
638
+ if (args.in_scope !== undefined) epicPatch.in_scope = args.in_scope;
639
+ if (args.out_of_scope !== undefined) epicPatch.out_of_scope = args.out_of_scope;
640
+ if (args.notes !== undefined) epicPatch.notes = args.notes;
641
+ const updatedEpic = await kanban.updateEpicEntity(epicId, epicPatch);
642
+ if (returnShape === 'none') {
643
+ result = { ok: true, epic_id: updatedEpic.id };
644
+ } else if (returnShape === 'summary') {
645
+ result = kanban.shapeEpic(updatedEpic, await kanban.allTasks(), { view: 'summary' });
646
+ } else {
647
+ result = kanban.shapeEpic(updatedEpic, await kanban.allTasks(), { view: 'full' });
648
+ }
649
+ break;
650
+ }
651
+
550
652
  case 'move': {
551
653
  if (!args.task_id) {
552
654
  throw invalidRequest(
@@ -585,7 +687,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
585
687
  if (args.test_cases !== undefined) patch.test_cases = args.test_cases;
586
688
  if (args.subtasks !== undefined) patch.subtasks = args.subtasks;
587
689
  if (args.notes !== undefined) patch.notes = args.notes;
588
- if (args.epic !== undefined) patch.epic_group = args.epic;
690
+ if (args.epic_id !== undefined) patch.epic_id = args.epic_id;
691
+ else if (args.epic !== undefined) patch.epic = args.epic;
589
692
  if (args.col !== undefined) patch.column = args.col;
590
693
  const updated = await kanban.updateTask(args.task_id, patch);
591
694
  result = formatTaskResult(updated, returnShape);
@@ -620,7 +723,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
620
723
  default:
621
724
  throw invalidRequest(
622
725
  `Unknown action: ${action}`,
623
- 'Use create, move, update, plan_create, plan_advance, plan_evidence, plan_done, or plan_status',
726
+ 'Use create, move, update, epic_create, epic_update, plan_create, plan_advance, plan_evidence, plan_done, or plan_status',
624
727
  { action }
625
728
  );
626
729
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kanbango",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "JSON-first local Kanban board with web GUI, CLI, and MCP server",
5
5
  "main": "index.js",
6
6
  "bin": {
package/tests/run.js CHANGED
@@ -20,3 +20,4 @@ runNode(path.join('tests', 'mcp-server.test.js'), [], 'MCP server test');
20
20
  runNode(path.join('tests', 'gui-port.test.js'), [], 'GUI port test');
21
21
  runNode(path.join('tests', 'plan-workflow.test.js'), [], 'Plan workflow test');
22
22
  runNode(path.join('tests', 'agent-playbook.test.js'), [], 'Agent playbook test');
23
+ runNode(path.join('tests', 'epics.test.js'), [], 'Epics test');