@kaban-board/cli 0.2.5 → 0.2.8

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/dist/index.js CHANGED
@@ -1,16 +1,17 @@
1
1
  #!/usr/bin/env node
2
+ // @bun
2
3
 
3
4
  // src/index.ts
4
- import { createRequire } from "node:module";
5
- import { Command as Command12 } from "commander";
5
+ import { createRequire as createRequire2 } from "module";
6
+ import { Command as Command15 } from "commander";
6
7
 
7
8
  // src/commands/add.ts
8
9
  import { KabanError } from "@kaban-board/core";
9
10
  import { Command } from "commander";
10
11
 
11
12
  // src/lib/context.ts
12
- import { existsSync, readFileSync } from "node:fs";
13
- import { join } from "node:path";
13
+ import { existsSync, readFileSync } from "fs";
14
+ import { join } from "path";
14
15
  import { BoardService, createDb, TaskService } from "@kaban-board/core";
15
16
  async function getContext() {
16
17
  const kabanDir = join(process.cwd(), ".kaban");
@@ -57,20 +58,30 @@ function outputError(code, message) {
57
58
  }
58
59
 
59
60
  // src/commands/add.ts
60
- var addCommand = new Command("add").description("Add a new task").argument("<title>", "Task title").option("-c, --column <column>", "Column to add task to").option("-a, --agent <agent>", "Agent creating the task").option("-D, --description <text>", "Task description").option("-d, --depends-on <ids>", "Comma-separated task IDs this depends on").option("-j, --json", "Output as JSON").action(async (title, options) => {
61
+ var addCommand = new Command("add").description("Add a new task").argument("<title>", "Task title").option("-c, --column <column>", "Column to add task to").option("-a, --agent <agent>", "Agent creating the task").option("-D, --description <text>", "Task description").option("-d, --depends-on <ids>", "Comma-separated task IDs this depends on").option("-f, --force", "Force creation even if similar tasks exist").option("-j, --json", "Output as JSON").action(async (title, options) => {
61
62
  const json = options.json;
62
63
  try {
63
64
  const { taskService, config } = await getContext();
64
65
  const agent = options.agent ?? getAgent();
65
66
  const columnId = options.column ?? config.defaults.column;
66
67
  const dependsOn = options.dependsOn ? options.dependsOn.split(",").map((s) => s.trim()) : [];
67
- const task = await taskService.addTask({
68
+ const result = await taskService.addTaskChecked({
68
69
  title,
69
70
  description: options.description,
70
71
  columnId,
71
72
  agent,
72
73
  dependsOn
73
- });
74
+ }, { force: options.force });
75
+ if (result.rejected) {
76
+ if (json) {
77
+ outputError(409, result.rejectionReason ?? "Similar task exists");
78
+ } else {
79
+ console.error(`Error: ${result.rejectionReason}`);
80
+ console.error("Use --force to create anyway.");
81
+ }
82
+ process.exit(1);
83
+ }
84
+ const task = result.task;
74
85
  if (json) {
75
86
  outputSuccess(task);
76
87
  return;
@@ -78,6 +89,9 @@ var addCommand = new Command("add").description("Add a new task").argument("<tit
78
89
  console.log(`Created task [${task.id.slice(0, 8)}] "${task.title}"`);
79
90
  console.log(` Column: ${task.columnId}`);
80
91
  console.log(` Agent: ${task.createdBy}`);
92
+ if (result.similarTasks.length > 0) {
93
+ console.log(` Note: Found ${result.similarTasks.length} similar task(s)`);
94
+ }
81
95
  } catch (error2) {
82
96
  if (error2 instanceof KabanError) {
83
97
  if (json)
@@ -89,10 +103,237 @@ var addCommand = new Command("add").description("Add a new task").argument("<tit
89
103
  }
90
104
  });
91
105
 
92
- // src/commands/done.ts
106
+ // src/commands/archive.ts
93
107
  import { KabanError as KabanError2 } from "@kaban-board/core";
94
108
  import { Command as Command2 } from "commander";
95
- var doneCommand = new Command2("done").description("Mark a task as done").argument("<id>", "Task ID (can be partial)").option("-j, --json", "Output as JSON").action(async (id, options) => {
109
+ var archiveCommand = new Command2("archive").description("Archive completed tasks").option("-c, --column <column>", "Archive tasks from specific column").option("-o, --older-than <days>", "Archive tasks older than N days", parseInt).option("--all-columns", "Archive from all columns (requires --older-than)").option("--dry-run", "Show what would be archived without archiving").option("-j, --json", "Output as JSON").action(async (options) => {
110
+ const json = options.json;
111
+ try {
112
+ const { taskService, boardService } = await getContext();
113
+ if (!options.column && !options.allColumns) {
114
+ const terminal = await boardService.getTerminalColumn();
115
+ if (!terminal) {
116
+ if (json)
117
+ outputError(1, "No terminal column configured and no criteria specified");
118
+ console.error("Error: No terminal column configured and no criteria specified");
119
+ process.exit(1);
120
+ }
121
+ options.column = terminal.id;
122
+ }
123
+ if (options.allColumns && !options.olderThan) {
124
+ if (json)
125
+ outputError(1, "--all-columns requires --older-than");
126
+ console.error("Error: --all-columns requires --older-than");
127
+ process.exit(1);
128
+ }
129
+ const criteria = {};
130
+ if (options.column && !options.allColumns) {
131
+ criteria.status = options.column;
132
+ }
133
+ if (options.olderThan) {
134
+ const date = new Date;
135
+ date.setDate(date.getDate() - options.olderThan);
136
+ criteria.olderThan = date;
137
+ }
138
+ if (options.dryRun) {
139
+ const tasks = await taskService.listTasks({
140
+ columnId: criteria.status,
141
+ includeArchived: false
142
+ });
143
+ const olderThan = criteria.olderThan;
144
+ const filtered = olderThan ? tasks.filter((t) => t.createdAt < olderThan) : tasks;
145
+ if (json) {
146
+ outputSuccess({ dryRun: true, wouldArchive: filtered.length, tasks: filtered });
147
+ return;
148
+ }
149
+ console.log(`Would archive ${filtered.length} task(s):`);
150
+ for (const task of filtered) {
151
+ console.log(` [${task.id.slice(0, 8)}] ${task.title}`);
152
+ }
153
+ return;
154
+ }
155
+ const result = await taskService.archiveTasks(criteria);
156
+ if (json) {
157
+ outputSuccess(result);
158
+ return;
159
+ }
160
+ console.log(`Archived ${result.archivedCount} task(s)`);
161
+ } catch (error2) {
162
+ if (error2 instanceof KabanError2) {
163
+ if (json)
164
+ outputError(error2.code, error2.message);
165
+ console.error(`Error: ${error2.message}`);
166
+ process.exit(error2.code);
167
+ }
168
+ throw error2;
169
+ }
170
+ });
171
+ var restoreCommand = new Command2("restore").description("Restore a task from archive").argument("<id>", "Task ID to restore (can be partial)").option("-t, --to <column>", "Restore to specific column").option("-j, --json", "Output as JSON").action(async (id, options) => {
172
+ const json = options.json;
173
+ try {
174
+ const { taskService } = await getContext();
175
+ const archivedTasks = await taskService.listTasks({ includeArchived: true });
176
+ const task = archivedTasks.find((t) => t.id.startsWith(id) && t.archived);
177
+ if (!task) {
178
+ if (json)
179
+ outputError(2, `Archived task '${id}' not found`);
180
+ console.error(`Error: Archived task '${id}' not found`);
181
+ process.exit(2);
182
+ }
183
+ const restored = await taskService.restoreTask(task.id, options.to);
184
+ if (json) {
185
+ outputSuccess(restored);
186
+ return;
187
+ }
188
+ console.log(`Restored [${restored.id.slice(0, 8)}] "${restored.title}" to ${restored.columnId}`);
189
+ } catch (error2) {
190
+ if (error2 instanceof KabanError2) {
191
+ if (json)
192
+ outputError(error2.code, error2.message);
193
+ console.error(`Error: ${error2.message}`);
194
+ process.exit(error2.code);
195
+ }
196
+ throw error2;
197
+ }
198
+ });
199
+ var purgeCommand = new Command2("purge").description("Permanently delete archived tasks").option("--confirm", "Confirm deletion (required)").option("--dry-run", "Show what would be deleted without deleting").option("-j, --json", "Output as JSON").action(async (options) => {
200
+ const json = options.json;
201
+ try {
202
+ const { taskService } = await getContext();
203
+ if (options.dryRun) {
204
+ const archived = await taskService.listTasks({ includeArchived: true });
205
+ const archivedOnly = archived.filter((t) => t.archived);
206
+ if (json) {
207
+ outputSuccess({ dryRun: true, wouldDelete: archivedOnly.length, tasks: archivedOnly });
208
+ return;
209
+ }
210
+ console.log(`Would permanently delete ${archivedOnly.length} archived task(s):`);
211
+ for (const task of archivedOnly) {
212
+ console.log(` [${task.id.slice(0, 8)}] ${task.title}`);
213
+ }
214
+ return;
215
+ }
216
+ if (!options.confirm) {
217
+ if (json)
218
+ outputError(1, "Use --confirm to permanently delete archived tasks");
219
+ console.error("Error: Use --confirm to permanently delete archived tasks");
220
+ process.exit(1);
221
+ }
222
+ const result = await taskService.purgeArchive();
223
+ if (json) {
224
+ outputSuccess(result);
225
+ return;
226
+ }
227
+ console.log(`Permanently deleted ${result.deletedCount} archived task(s)`);
228
+ } catch (error2) {
229
+ if (error2 instanceof KabanError2) {
230
+ if (json)
231
+ outputError(error2.code, error2.message);
232
+ console.error(`Error: ${error2.message}`);
233
+ process.exit(error2.code);
234
+ }
235
+ throw error2;
236
+ }
237
+ });
238
+ var resetCommand = new Command2("reset").description("Delete ALL tasks (destructive)").option("--confirm", "Confirm deletion (required)").option("--dry-run", "Show what would be deleted without deleting").option("-j, --json", "Output as JSON").action(async (options) => {
239
+ const json = options.json;
240
+ try {
241
+ const { taskService } = await getContext();
242
+ if (options.dryRun) {
243
+ const allTasks = await taskService.listTasks({ includeArchived: true });
244
+ if (json) {
245
+ outputSuccess({ dryRun: true, wouldDelete: allTasks.length, tasks: allTasks });
246
+ return;
247
+ }
248
+ console.log(`Would permanently delete ALL ${allTasks.length} task(s):`);
249
+ for (const task of allTasks) {
250
+ const archived = task.archived ? " [archived]" : "";
251
+ console.log(` [${task.id.slice(0, 8)}] ${task.title}${archived}`);
252
+ }
253
+ return;
254
+ }
255
+ if (!options.confirm) {
256
+ if (json)
257
+ outputError(1, "Use --confirm to delete ALL tasks");
258
+ console.error("Error: Use --confirm to delete ALL tasks");
259
+ process.exit(1);
260
+ }
261
+ const result = await taskService.resetBoard();
262
+ if (json) {
263
+ outputSuccess(result);
264
+ return;
265
+ }
266
+ console.log(`Deleted ALL ${result.deletedCount} task(s)`);
267
+ } catch (error2) {
268
+ if (error2 instanceof KabanError2) {
269
+ if (json)
270
+ outputError(error2.code, error2.message);
271
+ console.error(`Error: ${error2.message}`);
272
+ process.exit(error2.code);
273
+ }
274
+ throw error2;
275
+ }
276
+ });
277
+
278
+ // src/commands/assign.ts
279
+ import { KabanError as KabanError3 } from "@kaban-board/core";
280
+ import { Command as Command3 } from "commander";
281
+ var assignCommand = new Command3("assign").description("Assign a task to an agent").argument("<id>", "Task ID (can be partial)").argument("[agent]", "Agent to assign (omit with --clear to unassign)").option("-c, --clear", "Unassign the task").option("-j, --json", "Output as JSON").action(async (id, agent, options) => {
282
+ const json = options.json;
283
+ try {
284
+ const { taskService } = await getContext();
285
+ if (options.clear && agent) {
286
+ if (json)
287
+ outputError(4, "Cannot use --clear with agent argument");
288
+ console.error("Error: Cannot use --clear with agent argument");
289
+ process.exit(4);
290
+ }
291
+ const tasks = await taskService.listTasks();
292
+ const task = tasks.find((t) => t.id.startsWith(id));
293
+ if (!task) {
294
+ if (json)
295
+ outputError(2, `Task '${id}' not found`);
296
+ console.error(`Error: Task '${id}' not found`);
297
+ process.exit(2);
298
+ }
299
+ const previousAssignee = task.assignedTo;
300
+ if (options.clear) {
301
+ const updated2 = await taskService.updateTask(task.id, { assignedTo: null });
302
+ if (json) {
303
+ outputSuccess(updated2);
304
+ return;
305
+ }
306
+ console.log(`Unassigned [${updated2.id.slice(0, 8)}] "${updated2.title}"`);
307
+ return;
308
+ }
309
+ if (!agent) {
310
+ if (json)
311
+ outputError(4, "Specify an agent or use --clear");
312
+ console.error("Error: Specify an agent or use --clear to unassign");
313
+ process.exit(4);
314
+ }
315
+ const updated = await taskService.updateTask(task.id, { assignedTo: agent });
316
+ if (json) {
317
+ outputSuccess(updated);
318
+ return;
319
+ }
320
+ const prevMsg = previousAssignee ? ` (was: ${previousAssignee})` : "";
321
+ console.log(`Assigned [${updated.id.slice(0, 8)}] "${updated.title}" to ${updated.assignedTo}${prevMsg}`);
322
+ } catch (error2) {
323
+ if (error2 instanceof KabanError3) {
324
+ if (json)
325
+ outputError(error2.code, error2.message);
326
+ console.error(`Error: ${error2.message}`);
327
+ process.exit(error2.code);
328
+ }
329
+ throw error2;
330
+ }
331
+ });
332
+
333
+ // src/commands/done.ts
334
+ import { KabanError as KabanError4 } from "@kaban-board/core";
335
+ import { Command as Command4 } from "commander";
336
+ var doneCommand = new Command4("done").description("Mark a task as done").argument("<id>", "Task ID (can be partial)").option("-j, --json", "Output as JSON").action(async (id, options) => {
96
337
  const json = options.json;
97
338
  try {
98
339
  const { taskService, boardService } = await getContext();
@@ -118,7 +359,7 @@ var doneCommand = new Command2("done").description("Mark a task as done").argume
118
359
  }
119
360
  console.log(`Completed [${moved.id.slice(0, 8)}] "${moved.title}"`);
120
361
  } catch (error2) {
121
- if (error2 instanceof KabanError2) {
362
+ if (error2 instanceof KabanError4) {
122
363
  if (json)
123
364
  outputError(error2.code, error2.message);
124
365
  console.error(`Error: ${error2.message}`);
@@ -129,20 +370,20 @@ var doneCommand = new Command2("done").description("Mark a task as done").argume
129
370
  });
130
371
 
131
372
  // src/commands/hook.ts
132
- import { spawn } from "node:child_process";
133
- import { existsSync as existsSync3, realpathSync } from "node:fs";
134
- import { chmod, copyFile as copyFile2, mkdir, readFile as readFile2, stat, unlink } from "node:fs/promises";
135
- import { homedir as homedir2 } from "node:os";
136
- import { dirname, join as join3 } from "node:path";
373
+ import { spawn } from "child_process";
374
+ import { existsSync as existsSync3, realpathSync } from "fs";
375
+ import { chmod, copyFile as copyFile2, mkdir, readFile as readFile2, stat, unlink } from "fs/promises";
376
+ import { homedir as homedir2 } from "os";
377
+ import { dirname, join as join3 } from "path";
137
378
  import * as p from "@clack/prompts";
138
379
  import chalk from "chalk";
139
- import { Command as Command3 } from "commander";
380
+ import { Command as Command5 } from "commander";
140
381
 
141
382
  // src/hook/settings-manager.ts
142
- import { existsSync as existsSync2 } from "node:fs";
143
- import { copyFile, readFile, writeFile } from "node:fs/promises";
144
- import { homedir } from "node:os";
145
- import { join as join2 } from "node:path";
383
+ import { existsSync as existsSync2 } from "fs";
384
+ import { copyFile, readFile, writeFile } from "fs/promises";
385
+ import { homedir } from "os";
386
+ import { join as join2 } from "path";
146
387
 
147
388
  // src/hook/schemas.ts
148
389
  import { z } from "zod";
@@ -381,7 +622,7 @@ async function checkDependencies() {
381
622
  function formatCheckResults(results) {
382
623
  const maxNameLen = Math.max(...results.map((r) => r.name.length));
383
624
  return results.map((r) => {
384
- const icon = r.ok ? chalk.green("") : chalk.red("");
625
+ const icon = r.ok ? chalk.green("\u2713") : chalk.red("\u2717");
385
626
  const name = r.name.padEnd(maxNameLen);
386
627
  const msg = r.ok ? chalk.dim(r.message) : chalk.red(r.message);
387
628
  return ` ${icon} ${name} ${msg}`;
@@ -485,7 +726,7 @@ function formatSize(bytes) {
485
726
  return `${bytes} B`;
486
727
  return `${Math.round(bytes / 1024)} KB`;
487
728
  }
488
- var installCommand = new Command3("install").description("Install TodoWrite sync hook for Claude Code").option("-y, --yes", "Skip confirmation").action(async (options) => {
729
+ var installCommand = new Command5("install").description("Install TodoWrite sync hook for Claude Code").option("-y, --yes", "Skip confirmation").action(async (options) => {
489
730
  p.intro(chalk.bgCyan.black(" kaban hook install "));
490
731
  const s = p.spinner();
491
732
  s.start("Checking dependencies...");
@@ -527,7 +768,7 @@ var installCommand = new Command3("install").description("Install TodoWrite sync
527
768
  summaryLines.push("");
528
769
  }
529
770
  if (!installResult.hookConfigured) {
530
- summaryLines.push(` ${chalk.yellow("")} Hook was already configured`);
771
+ summaryLines.push(` ${chalk.yellow("\u26A0")} Hook was already configured`);
531
772
  summaryLines.push("");
532
773
  }
533
774
  p.note(summaryLines.join(`
@@ -546,7 +787,7 @@ var installCommand = new Command3("install").description("Install TodoWrite sync
546
787
  process.exit(1);
547
788
  }
548
789
  });
549
- var uninstallCommand = new Command3("uninstall").description("Remove TodoWrite sync hook").option("-y, --yes", "Skip confirmation").option("--clean", "Also remove sync logs").action(async (options) => {
790
+ var uninstallCommand = new Command5("uninstall").description("Remove TodoWrite sync hook").option("-y, --yes", "Skip confirmation").option("--clean", "Also remove sync logs").action(async (options) => {
550
791
  p.intro(chalk.bgRed.white(" kaban hook uninstall "));
551
792
  const binaryExists = existsSync3(join3(HOOKS_DIR, HOOK_BINARY_NAME));
552
793
  const logExists = existsSync3(join3(HOOKS_DIR, LOG_FILE));
@@ -564,7 +805,7 @@ var uninstallCommand = new Command3("uninstall").description("Remove TodoWrite s
564
805
  process.exit(0);
565
806
  }
566
807
  const formatStatusLine = (exists, label) => {
567
- const icon = exists ? chalk.green("") : chalk.dim("");
808
+ const icon = exists ? chalk.green("\u2713") : chalk.dim("\u25CB");
568
809
  const text = exists ? "" : chalk.dim(" (not found)");
569
810
  return ` ${icon} ${label}${text}`;
570
811
  };
@@ -572,7 +813,7 @@ var uninstallCommand = new Command3("uninstall").description("Remove TodoWrite s
572
813
  if (!logExists)
573
814
  return formatStatusLine(false, "Sync log");
574
815
  const suffix = options.clean ? chalk.yellow(" (will be removed)") : chalk.dim(" (will be preserved)");
575
- return ` ${chalk.yellow("")} Sync log${suffix}`;
816
+ return ` ${chalk.yellow("\u25CB")} Sync log${suffix}`;
576
817
  };
577
818
  const statusLines = [
578
819
  formatStatusLine(binaryExists, "Hook binary"),
@@ -598,13 +839,13 @@ var uninstallCommand = new Command3("uninstall").description("Remove TodoWrite s
598
839
  s.stop("Removal complete");
599
840
  const summaryLines = [];
600
841
  if (result.hookRemoved)
601
- summaryLines.push(` ${chalk.green("")} Removed hook from settings.json`);
842
+ summaryLines.push(` ${chalk.green("\u2713")} Removed hook from settings.json`);
602
843
  if (result.binaryRemoved)
603
- summaryLines.push(` ${chalk.green("")} Removed ${HOOK_BINARY_NAME}`);
844
+ summaryLines.push(` ${chalk.green("\u2713")} Removed ${HOOK_BINARY_NAME}`);
604
845
  if (result.logRemoved)
605
- summaryLines.push(` ${chalk.green("")} Removed ${LOG_FILE}`);
846
+ summaryLines.push(` ${chalk.green("\u2713")} Removed ${LOG_FILE}`);
606
847
  if (logExists && !options.clean)
607
- summaryLines.push(` ${chalk.yellow("")} Preserved ${LOG_FILE} (use --clean to remove)`);
848
+ summaryLines.push(` ${chalk.yellow("\u25CB")} Preserved ${LOG_FILE} (use --clean to remove)`);
608
849
  if (summaryLines.length > 0) {
609
850
  p.note(summaryLines.join(`
610
851
  `), "Removed");
@@ -616,7 +857,7 @@ var uninstallCommand = new Command3("uninstall").description("Remove TodoWrite s
616
857
  process.exit(1);
617
858
  }
618
859
  });
619
- var statusCommand = new Command3("status").description("Check hook installation status").action(async () => {
860
+ var statusCommand = new Command5("status").description("Check hook installation status").action(async () => {
620
861
  p.intro(chalk.bgBlue.white(" kaban hook status "));
621
862
  const s = p.spinner();
622
863
  s.start("Checking status...");
@@ -652,7 +893,7 @@ var statusCommand = new Command3("status").description("Check hook installation
652
893
  ];
653
894
  const maxNameLen = Math.max(...results.map((r) => r.name.length));
654
895
  const statusLines = results.map((r) => {
655
- const icon = r.ok ? chalk.green("") : chalk.red("");
896
+ const icon = r.ok ? chalk.green("\u2713") : chalk.red("\u2717");
656
897
  const name = r.name.padEnd(maxNameLen);
657
898
  const detail = r.ok ? chalk.dim(r.detail) : chalk.red(r.detail);
658
899
  return ` ${icon} ${name} ${detail}`;
@@ -682,18 +923,18 @@ var statusCommand = new Command3("status").description("Check hook installation
682
923
  process.exit(1);
683
924
  }
684
925
  });
685
- var hookCommand = new Command3("hook").description("Manage TodoWrite sync hook for Claude Code").addCommand(installCommand).addCommand(uninstallCommand).addCommand(statusCommand);
926
+ var hookCommand = new Command5("hook").description("Manage TodoWrite sync hook for Claude Code").addCommand(installCommand).addCommand(uninstallCommand).addCommand(statusCommand);
686
927
 
687
928
  // src/commands/init.ts
688
- import { existsSync as existsSync4, mkdirSync, writeFileSync } from "node:fs";
929
+ import { existsSync as existsSync4, mkdirSync, writeFileSync } from "fs";
689
930
  import {
690
931
  BoardService as BoardService2,
691
932
  createDb as createDb2,
692
933
  DEFAULT_CONFIG as DEFAULT_CONFIG2,
693
934
  initializeSchema
694
935
  } from "@kaban-board/core";
695
- import { Command as Command4 } from "commander";
696
- var initCommand = new Command4("init").description("Initialize a new Kaban board in the current directory").option("-n, --name <name>", "Board name", "Kaban Board").action(async (options) => {
936
+ import { Command as Command6 } from "commander";
937
+ var initCommand = new Command6("init").description("Initialize a new Kaban board in the current directory").option("-n, --name <name>", "Board name", "Kaban Board").action(async (options) => {
697
938
  const { kabanDir, dbPath, configPath } = getKabanPaths();
698
939
  if (existsSync4(dbPath)) {
699
940
  console.error("Error: Board already exists in this directory");
@@ -715,8 +956,8 @@ var initCommand = new Command4("init").description("Initialize a new Kaban board
715
956
  });
716
957
 
717
958
  // src/commands/list.ts
718
- import { KabanError as KabanError3 } from "@kaban-board/core";
719
- import { Command as Command5 } from "commander";
959
+ import { KabanError as KabanError5 } from "@kaban-board/core";
960
+ import { Command as Command7 } from "commander";
720
961
  function sortTasks(tasks, sortBy, reverse) {
721
962
  const sorted = [...tasks].sort((a, b) => {
722
963
  switch (sortBy) {
@@ -732,7 +973,7 @@ function sortTasks(tasks, sortBy, reverse) {
732
973
  });
733
974
  return reverse ? sorted.reverse() : sorted;
734
975
  }
735
- var listCommand = new Command5("list").description("List tasks").option("-c, --column <column>", "Filter by column").option("-a, --agent <agent>", "Filter by creator agent").option("-u, --assignee <assignee>", "Filter by assigned agent").option("-b, --blocked", "Show only blocked tasks").option("-s, --sort <field>", "Sort by: name, date, updated").option("-r, --reverse", "Reverse sort order").option("-j, --json", "Output as JSON").action(async (options) => {
976
+ var listCommand = new Command7("list").description("List tasks").option("-c, --column <column>", "Filter by column").option("-a, --agent <agent>", "Filter by creator agent").option("-u, --assignee <assignee>", "Filter by assigned agent").option("-b, --blocked", "Show only blocked tasks").option("-s, --sort <field>", "Sort by: name, date, updated").option("-r, --reverse", "Reverse sort order").option("-j, --json", "Output as JSON").action(async (options) => {
736
977
  const json = options.json;
737
978
  try {
738
979
  const { taskService, boardService } = await getContext();
@@ -764,7 +1005,7 @@ var listCommand = new Command5("list").description("List tasks").option("-c, --c
764
1005
  const column = columnMap.get(task.columnId);
765
1006
  const blocked = task.blockedReason ? " [blocked]" : "";
766
1007
  const agent = task.createdBy !== "user" ? ` @${task.createdBy}` : "";
767
- const assignee = task.assignedTo ? ` ${task.assignedTo}` : "";
1008
+ const assignee = task.assignedTo ? ` \u2192 ${task.assignedTo}` : "";
768
1009
  console.log(`[${task.id.slice(0, 8)}] ${task.title}${agent}${assignee}${blocked}`);
769
1010
  console.log(` ${column?.name ?? task.columnId}`);
770
1011
  if (task.description) {
@@ -773,7 +1014,7 @@ var listCommand = new Command5("list").description("List tasks").option("-c, --c
773
1014
  }
774
1015
  }
775
1016
  } catch (error2) {
776
- if (error2 instanceof KabanError3) {
1017
+ if (error2 instanceof KabanError5) {
777
1018
  if (json)
778
1019
  outputError(error2.code, error2.message);
779
1020
  console.error(`Error: ${error2.message}`);
@@ -784,8 +1025,8 @@ var listCommand = new Command5("list").description("List tasks").option("-c, --c
784
1025
  });
785
1026
 
786
1027
  // src/commands/mcp.ts
787
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
788
- import { join as join4 } from "node:path";
1028
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1029
+ import { join as join4 } from "path";
789
1030
  import {
790
1031
  BoardService as BoardService3,
791
1032
  createDb as createDb3,
@@ -801,7 +1042,26 @@ import {
801
1042
  ListToolsRequestSchema,
802
1043
  ReadResourceRequestSchema
803
1044
  } from "@modelcontextprotocol/sdk/types.js";
804
- import { Command as Command6 } from "commander";
1045
+ import { Command as Command8 } from "commander";
1046
+ var mcpHelpers = {
1047
+ getParam(args, primary, alias) {
1048
+ if (!args)
1049
+ return;
1050
+ const primaryVal = args[primary];
1051
+ if (typeof primaryVal === "string" && primaryVal.trim())
1052
+ return primaryVal;
1053
+ const aliasVal = args[alias];
1054
+ if (typeof aliasVal === "string" && aliasVal.trim())
1055
+ return aliasVal;
1056
+ return;
1057
+ },
1058
+ errorResponse(message) {
1059
+ return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
1060
+ },
1061
+ jsonResponse(data) {
1062
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1063
+ }
1064
+ };
805
1065
  function getKabanPaths2(basePath) {
806
1066
  const base = basePath ?? process.cwd();
807
1067
  const kabanDir = join4(base, ".kaban");
@@ -867,8 +1127,10 @@ async function startMcpServer(workingDirectory) {
867
1127
  description: "Get a task by its ID",
868
1128
  inputSchema: {
869
1129
  type: "object",
870
- properties: { id: { type: "string", description: "Task ID (ULID)" } },
871
- required: ["id"]
1130
+ properties: {
1131
+ id: { type: "string", description: "Task ID (ULID)" },
1132
+ taskId: { type: "string", description: "Task ID (ULID) - alias for 'id'" }
1133
+ }
872
1134
  }
873
1135
  },
874
1136
  {
@@ -878,7 +1140,8 @@ async function startMcpServer(workingDirectory) {
878
1140
  type: "object",
879
1141
  properties: {
880
1142
  columnId: { type: "string", description: "Filter by column ID" },
881
- agent: { type: "string", description: "Filter by agent name" },
1143
+ agent: { type: "string", description: "Filter by creator agent name" },
1144
+ assignee: { type: "string", description: "Filter by assigned agent name" },
882
1145
  blocked: { type: "boolean", description: "Show only blocked tasks" }
883
1146
  }
884
1147
  }
@@ -890,10 +1153,11 @@ async function startMcpServer(workingDirectory) {
890
1153
  type: "object",
891
1154
  properties: {
892
1155
  id: { type: "string", description: "Task ID (ULID)" },
1156
+ taskId: { type: "string", description: "Task ID (ULID) - alias for 'id'" },
893
1157
  columnId: { type: "string", description: "Target column ID" },
1158
+ column: { type: "string", description: "Target column ID - alias for 'columnId'" },
894
1159
  force: { type: "boolean", description: "Force move even if WIP limit exceeded" }
895
- },
896
- required: ["id", "columnId"]
1160
+ }
897
1161
  }
898
1162
  },
899
1163
  {
@@ -903,6 +1167,7 @@ async function startMcpServer(workingDirectory) {
903
1167
  type: "object",
904
1168
  properties: {
905
1169
  id: { type: "string", description: "Task ID (ULID)" },
1170
+ taskId: { type: "string", description: "Task ID (ULID) - alias for 'id'" },
906
1171
  title: { type: "string", description: "New task title" },
907
1172
  description: { type: ["string", "null"], description: "New task description" },
908
1173
  assignedTo: { type: ["string", "null"], description: "Assigned agent name" },
@@ -916,8 +1181,7 @@ async function startMcpServer(workingDirectory) {
916
1181
  type: "number",
917
1182
  description: "Expected version for optimistic locking"
918
1183
  }
919
- },
920
- required: ["id"]
1184
+ }
921
1185
  }
922
1186
  },
923
1187
  {
@@ -925,8 +1189,10 @@ async function startMcpServer(workingDirectory) {
925
1189
  description: "Delete a task",
926
1190
  inputSchema: {
927
1191
  type: "object",
928
- properties: { id: { type: "string", description: "Task ID (ULID)" } },
929
- required: ["id"]
1192
+ properties: {
1193
+ id: { type: "string", description: "Task ID (ULID)" },
1194
+ taskId: { type: "string", description: "Task ID (ULID) - alias for 'id'" }
1195
+ }
930
1196
  }
931
1197
  },
932
1198
  {
@@ -934,17 +1200,144 @@ async function startMcpServer(workingDirectory) {
934
1200
  description: "Mark a task as completed (move to terminal column)",
935
1201
  inputSchema: {
936
1202
  type: "object",
937
- properties: { id: { type: "string", description: "Task ID (ULID) or partial ID" } },
938
- required: ["id"]
1203
+ properties: {
1204
+ id: { type: "string", description: "Task ID (ULID) or partial ID" },
1205
+ taskId: {
1206
+ type: "string",
1207
+ description: "Task ID (ULID) or partial ID - alias for 'id'"
1208
+ }
1209
+ }
939
1210
  }
940
1211
  },
941
1212
  {
942
1213
  name: "kaban_status",
943
1214
  description: "Get board status summary",
944
1215
  inputSchema: { type: "object", properties: {} }
1216
+ },
1217
+ {
1218
+ name: "kaban_archive_tasks",
1219
+ description: "Archive completed or stale tasks. By default archives from terminal columns only.",
1220
+ inputSchema: {
1221
+ type: "object",
1222
+ properties: {
1223
+ columnId: { type: "string", description: "Archive from this column only" },
1224
+ olderThanDays: { type: "number", description: "Archive tasks older than N days" },
1225
+ allColumns: { type: "boolean", description: "Archive from ALL columns" },
1226
+ dryRun: { type: "boolean", description: "Preview without archiving" }
1227
+ }
1228
+ }
1229
+ },
1230
+ {
1231
+ name: "kaban_search_archive",
1232
+ description: "Search archived tasks using full-text search",
1233
+ inputSchema: {
1234
+ type: "object",
1235
+ properties: {
1236
+ query: { type: "string", description: "Search query" },
1237
+ limit: { type: "number", description: "Max results (default: 50)" }
1238
+ },
1239
+ required: ["query"]
1240
+ }
1241
+ },
1242
+ {
1243
+ name: "kaban_restore_task",
1244
+ description: "Restore a task from archive",
1245
+ inputSchema: {
1246
+ type: "object",
1247
+ properties: {
1248
+ id: { type: "string", description: "Task ID to restore" },
1249
+ taskId: { type: "string", description: "Task ID - alias for id" },
1250
+ columnId: { type: "string", description: "Target column (default: todo)" }
1251
+ }
1252
+ }
1253
+ },
1254
+ {
1255
+ name: "kaban_purge_archive",
1256
+ description: "Permanently delete all archived tasks",
1257
+ inputSchema: {
1258
+ type: "object",
1259
+ properties: {
1260
+ confirm: { type: "boolean", description: "Must be true to confirm" },
1261
+ dryRun: { type: "boolean", description: "Preview without deleting" }
1262
+ },
1263
+ required: ["confirm"]
1264
+ }
1265
+ },
1266
+ {
1267
+ name: "kaban_reset_board",
1268
+ description: "Delete ALL tasks (active and archived)",
1269
+ inputSchema: {
1270
+ type: "object",
1271
+ properties: {
1272
+ confirm: { type: "boolean", description: "Must be true to confirm" },
1273
+ dryRun: { type: "boolean", description: "Preview without deleting" }
1274
+ },
1275
+ required: ["confirm"]
1276
+ }
1277
+ },
1278
+ {
1279
+ name: "kaban_archive_stats",
1280
+ description: "Get archive statistics",
1281
+ inputSchema: { type: "object", properties: {} }
1282
+ },
1283
+ {
1284
+ name: "kaban_add_dependency",
1285
+ description: "Add a dependency to a task (task cannot start until dependency is done)",
1286
+ inputSchema: {
1287
+ type: "object",
1288
+ properties: {
1289
+ taskId: { type: "string", description: "Task that depends on another" },
1290
+ id: { type: "string", description: "Task ID - alias for taskId" },
1291
+ dependsOnId: { type: "string", description: "Task that must be completed first" }
1292
+ },
1293
+ required: ["dependsOnId"]
1294
+ }
1295
+ },
1296
+ {
1297
+ name: "kaban_remove_dependency",
1298
+ description: "Remove a dependency from a task",
1299
+ inputSchema: {
1300
+ type: "object",
1301
+ properties: {
1302
+ taskId: { type: "string", description: "Task ID" },
1303
+ id: { type: "string", description: "Task ID - alias" },
1304
+ dependsOnId: { type: "string", description: "Dependency to remove" }
1305
+ },
1306
+ required: ["dependsOnId"]
1307
+ }
1308
+ },
1309
+ {
1310
+ name: "kaban_check_dependencies",
1311
+ description: "Check if task dependencies are resolved",
1312
+ inputSchema: {
1313
+ type: "object",
1314
+ properties: {
1315
+ taskId: { type: "string", description: "Task ID" },
1316
+ id: { type: "string", description: "Task ID - alias" }
1317
+ }
1318
+ }
1319
+ },
1320
+ {
1321
+ name: "kaban_add_task_checked",
1322
+ description: "Add task with duplicate detection. Rejects if very similar task exists.",
1323
+ inputSchema: {
1324
+ type: "object",
1325
+ properties: {
1326
+ title: { type: "string", description: "Task title" },
1327
+ description: { type: "string", description: "Task description" },
1328
+ columnId: { type: "string", description: "Target column (default: todo)" },
1329
+ createdBy: { type: "string", description: "Creator: user, claude, etc." },
1330
+ agent: { type: "string", description: "Creator (deprecated, use createdBy)" },
1331
+ assignedTo: { type: "string", description: "Assignee" },
1332
+ dependsOn: { type: "array", items: { type: "string" }, description: "Dependency IDs" },
1333
+ force: { type: "boolean", description: "Create even if similar exists" }
1334
+ },
1335
+ required: ["title"]
1336
+ }
945
1337
  }
946
1338
  ]
947
1339
  }));
1340
+ const { getParam, errorResponse, jsonResponse } = mcpHelpers;
948
1341
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
949
1342
  const { name, arguments: args } = request.params;
950
1343
  try {
@@ -953,15 +1346,7 @@ async function startMcpServer(workingDirectory) {
953
1346
  const targetPath = basePath ?? workingDirectory;
954
1347
  const { kabanDir, dbPath, configPath } = getKabanPaths2(targetPath);
955
1348
  if (existsSync5(dbPath)) {
956
- return {
957
- content: [
958
- {
959
- type: "text",
960
- text: JSON.stringify({ error: "Board already exists in this directory" })
961
- }
962
- ],
963
- isError: true
964
- };
1349
+ return errorResponse("Board already exists in this directory");
965
1350
  }
966
1351
  mkdirSync2(kabanDir, { recursive: true });
967
1352
  const config = {
@@ -973,70 +1358,77 @@ async function startMcpServer(workingDirectory) {
973
1358
  await initializeSchema2(db);
974
1359
  const boardService2 = new BoardService3(db);
975
1360
  await boardService2.initializeBoard(config);
976
- return {
977
- content: [
978
- {
979
- type: "text",
980
- text: JSON.stringify({
981
- success: true,
982
- board: boardName,
983
- paths: { database: dbPath, config: configPath }
984
- }, null, 2)
985
- }
986
- ]
987
- };
1361
+ return jsonResponse({
1362
+ success: true,
1363
+ board: boardName,
1364
+ paths: { database: dbPath, config: configPath }
1365
+ });
988
1366
  }
989
1367
  const { taskService, boardService } = await createContext(workingDirectory);
1368
+ const taskArgs = args;
1369
+ const taskId = getParam(taskArgs, "id", "taskId");
990
1370
  switch (name) {
991
1371
  case "kaban_add_task": {
1372
+ const addArgs = args;
1373
+ const title = addArgs?.title;
1374
+ if (typeof title !== "string" || !title.trim()) {
1375
+ return errorResponse("Title required (non-empty string)");
1376
+ }
992
1377
  const task = await taskService.addTask(args);
993
- return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1378
+ return jsonResponse(task);
994
1379
  }
995
1380
  case "kaban_get_task": {
996
- const task = await taskService.getTask(args.id);
1381
+ if (!taskId)
1382
+ return errorResponse("Task ID required (use 'id' or 'taskId')");
1383
+ const task = await taskService.getTask(taskId);
997
1384
  if (!task)
998
- return {
999
- content: [{ type: "text", text: JSON.stringify({ error: "Task not found" }) }]
1000
- };
1001
- return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1385
+ return errorResponse("Task not found");
1386
+ return jsonResponse(task);
1002
1387
  }
1003
1388
  case "kaban_list_tasks": {
1004
1389
  const tasks = await taskService.listTasks(args);
1005
- return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
1390
+ return jsonResponse(tasks);
1006
1391
  }
1007
1392
  case "kaban_move_task": {
1008
- const { id, columnId, force } = args;
1009
- const task = await taskService.moveTask(id, columnId, { force });
1010
- return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1393
+ if (!taskId)
1394
+ return errorResponse("Task ID required (use 'id' or 'taskId')");
1395
+ const targetColumn = getParam(taskArgs, "columnId", "column");
1396
+ if (!targetColumn)
1397
+ return errorResponse("Column ID required (use 'columnId' or 'column')");
1398
+ const { force } = args ?? {};
1399
+ const task = await taskService.moveTask(taskId, targetColumn, { force });
1400
+ return jsonResponse(task);
1011
1401
  }
1012
1402
  case "kaban_update_task": {
1013
- const { id, expectedVersion, ...updates } = args;
1014
- const task = await taskService.updateTask(id, updates, expectedVersion);
1015
- return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1403
+ if (!taskId)
1404
+ return errorResponse("Task ID required (use 'id' or 'taskId')");
1405
+ const {
1406
+ taskId: _t,
1407
+ id: _i,
1408
+ expectedVersion,
1409
+ ...updates
1410
+ } = args ?? {};
1411
+ const task = await taskService.updateTask(taskId, updates, expectedVersion);
1412
+ return jsonResponse(task);
1016
1413
  }
1017
1414
  case "kaban_delete_task": {
1018
- await taskService.deleteTask(args.id);
1019
- return { content: [{ type: "text", text: JSON.stringify({ success: true }) }] };
1415
+ if (!taskId)
1416
+ return errorResponse("Task ID required (use 'id' or 'taskId')");
1417
+ await taskService.deleteTask(taskId);
1418
+ return jsonResponse({ success: true });
1020
1419
  }
1021
1420
  case "kaban_complete_task": {
1022
- const { id } = args;
1421
+ if (!taskId)
1422
+ return errorResponse("Task ID required (use 'id' or 'taskId')");
1023
1423
  const tasks = await taskService.listTasks();
1024
- const task = tasks.find((t) => t.id.startsWith(id));
1424
+ const task = tasks.find((t) => t.id.startsWith(taskId));
1025
1425
  if (!task)
1026
- return {
1027
- content: [
1028
- { type: "text", text: JSON.stringify({ error: `Task '${id}' not found` }) }
1029
- ]
1030
- };
1426
+ return errorResponse(`Task '${taskId}' not found`);
1031
1427
  const terminal = await boardService.getTerminalColumn();
1032
1428
  if (!terminal)
1033
- return {
1034
- content: [
1035
- { type: "text", text: JSON.stringify({ error: "No terminal column configured" }) }
1036
- ]
1037
- };
1429
+ return errorResponse("No terminal column configured");
1038
1430
  const completed = await taskService.moveTask(task.id, terminal.id);
1039
- return { content: [{ type: "text", text: JSON.stringify(completed, null, 2) }] };
1431
+ return jsonResponse(completed);
1040
1432
  }
1041
1433
  case "kaban_status": {
1042
1434
  const board = await boardService.getBoard();
@@ -1049,25 +1441,162 @@ async function startMcpServer(workingDirectory) {
1049
1441
  wipLimit: column.wipLimit,
1050
1442
  isTerminal: column.isTerminal
1051
1443
  }));
1052
- return {
1053
- content: [
1054
- {
1055
- type: "text",
1056
- text: JSON.stringify({
1057
- board: { name: board?.name ?? "Kaban Board" },
1058
- columns: columnStats,
1059
- blockedCount: tasks.filter((t) => t.blockedReason).length,
1060
- totalTasks: tasks.length
1061
- }, null, 2)
1062
- }
1063
- ]
1064
- };
1444
+ return jsonResponse({
1445
+ board: { name: board?.name ?? "Kaban Board" },
1446
+ columns: columnStats,
1447
+ blockedCount: tasks.filter((t) => t.blockedReason).length,
1448
+ totalTasks: tasks.length
1449
+ });
1450
+ }
1451
+ case "kaban_archive_tasks": {
1452
+ const { columnId, olderThanDays, allColumns, dryRun } = args ?? {};
1453
+ const columns = await boardService.getColumns();
1454
+ const terminalColumns = columns.filter((c) => c.isTerminal);
1455
+ let targetColumnIds = [];
1456
+ if (columnId) {
1457
+ targetColumnIds = [columnId];
1458
+ } else if (allColumns) {
1459
+ targetColumnIds = columns.map((c) => c.id);
1460
+ } else {
1461
+ targetColumnIds = terminalColumns.map((c) => c.id);
1462
+ }
1463
+ if (targetColumnIds.length === 0) {
1464
+ return jsonResponse({
1465
+ archivedCount: 0,
1466
+ taskIds: [],
1467
+ message: "No columns to archive from"
1468
+ });
1469
+ }
1470
+ const allTasks = await taskService.listTasks();
1471
+ let tasksToArchive = allTasks.filter((t) => !t.archived && targetColumnIds.includes(t.columnId));
1472
+ if (olderThanDays !== undefined) {
1473
+ const cutoff = new Date;
1474
+ cutoff.setDate(cutoff.getDate() - olderThanDays);
1475
+ tasksToArchive = tasksToArchive.filter((t) => t.createdAt < cutoff);
1476
+ }
1477
+ if (tasksToArchive.length === 0) {
1478
+ return jsonResponse({
1479
+ archivedCount: 0,
1480
+ taskIds: [],
1481
+ message: "No matching tasks to archive"
1482
+ });
1483
+ }
1484
+ if (dryRun) {
1485
+ return jsonResponse({
1486
+ dryRun: true,
1487
+ wouldArchive: tasksToArchive.length,
1488
+ taskIds: tasksToArchive.map((t) => t.id),
1489
+ tasks: tasksToArchive.map((t) => ({
1490
+ id: t.id,
1491
+ title: t.title,
1492
+ columnId: t.columnId
1493
+ }))
1494
+ });
1495
+ }
1496
+ const result = await taskService.archiveTasks({
1497
+ taskIds: tasksToArchive.map((t) => t.id)
1498
+ });
1499
+ return jsonResponse(result);
1500
+ }
1501
+ case "kaban_search_archive": {
1502
+ const { query, limit } = args ?? {};
1503
+ if (!query)
1504
+ return errorResponse("Query required");
1505
+ const result = await taskService.searchArchive(query, { limit });
1506
+ return jsonResponse(result);
1507
+ }
1508
+ case "kaban_restore_task": {
1509
+ const id = getParam(taskArgs, "id", "taskId");
1510
+ if (!id)
1511
+ return errorResponse("Task ID required");
1512
+ const { columnId } = args ?? {};
1513
+ const task = await taskService.restoreTask(id, columnId);
1514
+ return jsonResponse(task);
1515
+ }
1516
+ case "kaban_purge_archive": {
1517
+ const { confirm: confirm2, dryRun } = args ?? {};
1518
+ if (!confirm2) {
1519
+ return errorResponse("Must set confirm: true to purge archive");
1520
+ }
1521
+ if (dryRun) {
1522
+ const result2 = await taskService.searchArchive("", { limit: 1000 });
1523
+ return jsonResponse({
1524
+ dryRun: true,
1525
+ wouldDelete: result2.total
1526
+ });
1527
+ }
1528
+ const result = await taskService.purgeArchive();
1529
+ return jsonResponse(result);
1530
+ }
1531
+ case "kaban_reset_board": {
1532
+ const { confirm: confirm2, dryRun } = args ?? {};
1533
+ if (!confirm2) {
1534
+ return errorResponse("Must set confirm: true to reset board");
1535
+ }
1536
+ if (dryRun) {
1537
+ const allTasks = await taskService.listTasks();
1538
+ const archivedResult = await taskService.searchArchive("", { limit: 1000 });
1539
+ return jsonResponse({
1540
+ dryRun: true,
1541
+ wouldDelete: allTasks.length + archivedResult.total,
1542
+ activeTasks: allTasks.length,
1543
+ archivedTasks: archivedResult.total
1544
+ });
1545
+ }
1546
+ const result = await taskService.resetBoard();
1547
+ return jsonResponse(result);
1548
+ }
1549
+ case "kaban_archive_stats": {
1550
+ const archivedResult = await taskService.searchArchive("", { limit: 1 });
1551
+ const allTasks = await taskService.listTasks();
1552
+ const columns = await boardService.getColumns();
1553
+ const terminalColumns = columns.filter((c) => c.isTerminal);
1554
+ const completedCount = allTasks.filter((t) => terminalColumns.some((c) => c.id === t.columnId)).length;
1555
+ return jsonResponse({
1556
+ archivedCount: archivedResult.total,
1557
+ activeTasks: allTasks.length,
1558
+ completedNotArchived: completedCount
1559
+ });
1560
+ }
1561
+ case "kaban_add_dependency": {
1562
+ const id = getParam(taskArgs, "taskId", "id");
1563
+ if (!id)
1564
+ return errorResponse("Task ID required");
1565
+ const { dependsOnId } = args ?? {};
1566
+ if (!dependsOnId)
1567
+ return errorResponse("dependsOnId required");
1568
+ const task = await taskService.addDependency(id, dependsOnId);
1569
+ return jsonResponse(task);
1570
+ }
1571
+ case "kaban_remove_dependency": {
1572
+ const id = getParam(taskArgs, "taskId", "id");
1573
+ if (!id)
1574
+ return errorResponse("Task ID required");
1575
+ const { dependsOnId } = args ?? {};
1576
+ if (!dependsOnId)
1577
+ return errorResponse("dependsOnId required");
1578
+ const task = await taskService.removeDependency(id, dependsOnId);
1579
+ return jsonResponse(task);
1580
+ }
1581
+ case "kaban_check_dependencies": {
1582
+ const id = getParam(taskArgs, "taskId", "id");
1583
+ if (!id)
1584
+ return errorResponse("Task ID required");
1585
+ const result = await taskService.validateDependencies(id);
1586
+ return jsonResponse(result);
1587
+ }
1588
+ case "kaban_add_task_checked": {
1589
+ const addCheckedArgs = args;
1590
+ const title = addCheckedArgs?.title;
1591
+ if (typeof title !== "string" || !title.trim()) {
1592
+ return errorResponse("Title required");
1593
+ }
1594
+ const { force, ...taskInput } = addCheckedArgs;
1595
+ const result = await taskService.addTaskChecked(taskInput, { force });
1596
+ return jsonResponse(result);
1065
1597
  }
1066
1598
  default:
1067
- return {
1068
- content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
1069
- isError: true
1070
- };
1599
+ return errorResponse(`Unknown tool: ${name}`);
1071
1600
  }
1072
1601
  } catch (error2) {
1073
1602
  const message = error2 instanceof Error ? error2.message : String(error2);
@@ -1179,15 +1708,15 @@ async function startMcpServer(workingDirectory) {
1179
1708
  await server.connect(transport);
1180
1709
  console.error("Kaban MCP server running on stdio");
1181
1710
  }
1182
- var mcpCommand = new Command6("mcp").description("Start MCP server for AI agent integration").option("-p, --path <path>", "Working directory for Kaban board").action(async (options) => {
1711
+ var mcpCommand = new Command8("mcp").description("Start MCP server for AI agent integration").option("-p, --path <path>", "Working directory for Kaban board").action(async (options) => {
1183
1712
  const workingDirectory = options.path ?? process.env.KABAN_PATH ?? process.cwd();
1184
1713
  await startMcpServer(workingDirectory);
1185
1714
  });
1186
1715
 
1187
1716
  // src/commands/move.ts
1188
- import { KabanError as KabanError4 } from "@kaban-board/core";
1189
- import { Command as Command7 } from "commander";
1190
- var moveCommand = new Command7("move").description("Move a task to a different column").argument("<id>", "Task ID (can be partial)").argument("[column]", "Target column").option("-n, --next", "Move to next column").option("-f, --force", "Force move even if WIP limit exceeded").option("-j, --json", "Output as JSON").action(async (id, column, options) => {
1717
+ import { KabanError as KabanError6 } from "@kaban-board/core";
1718
+ import { Command as Command9 } from "commander";
1719
+ var moveCommand = new Command9("move").description("Move a task to a different column").argument("<id>", "Task ID (can be partial)").argument("[column]", "Target column").option("-n, --next", "Move to next column").option("-f, --force", "Force move even if WIP limit exceeded").option("-A, --assign [agent]", "Assign task to agent (defaults to current agent)").option("-j, --json", "Output as JSON").action(async (id, column, options) => {
1191
1720
  const json = options.json;
1192
1721
  try {
1193
1722
  const { taskService, boardService } = await getContext();
@@ -1221,14 +1750,24 @@ var moveCommand = new Command7("move").description("Move a task to a different c
1221
1750
  const moved = await taskService.moveTask(task.id, targetColumn, {
1222
1751
  force: options.force
1223
1752
  });
1753
+ let assignAgent = null;
1754
+ if (options.assign !== undefined) {
1755
+ assignAgent = options.assign === true ? getAgent() : options.assign;
1756
+ await taskService.updateTask(moved.id, { assignedTo: assignAgent });
1757
+ }
1758
+ const finalTask = assignAgent ? await taskService.getTask(moved.id) : moved;
1224
1759
  if (json) {
1225
- outputSuccess(moved);
1760
+ outputSuccess(finalTask);
1226
1761
  return;
1227
1762
  }
1228
1763
  const col = await boardService.getColumn(moved.columnId);
1229
- console.log(`Moved [${moved.id.slice(0, 8)}] to ${col?.name}`);
1764
+ let msg = `Moved [${moved.id.slice(0, 8)}] to ${col?.name}`;
1765
+ if (assignAgent) {
1766
+ msg += ` (assigned to ${assignAgent})`;
1767
+ }
1768
+ console.log(msg);
1230
1769
  } catch (error2) {
1231
- if (error2 instanceof KabanError4) {
1770
+ if (error2 instanceof KabanError6) {
1232
1771
  if (json)
1233
1772
  outputError(error2.code, error2.message);
1234
1773
  console.error(`Error: ${error2.message}`);
@@ -1240,9 +1779,9 @@ var moveCommand = new Command7("move").description("Move a task to a different c
1240
1779
 
1241
1780
  // src/commands/schema.ts
1242
1781
  import { jsonSchemas } from "@kaban-board/core";
1243
- import { Command as Command8 } from "commander";
1782
+ import { Command as Command10 } from "commander";
1244
1783
  var availableSchemas = Object.keys(jsonSchemas);
1245
- var schemaCommand = new Command8("schema").description("Output JSON schemas for AI agents").argument("[name]", "Schema name (omit to list available)").action((name) => {
1784
+ var schemaCommand = new Command10("schema").description("Output JSON schemas for AI agents").argument("[name]", "Schema name (omit to list available)").action((name) => {
1246
1785
  if (!name) {
1247
1786
  console.log("Available schemas:");
1248
1787
  for (const schemaName of availableSchemas) {
@@ -1261,10 +1800,52 @@ Usage: kaban schema <name>`);
1261
1800
  console.log(JSON.stringify(schema, null, 2));
1262
1801
  });
1263
1802
 
1803
+ // src/commands/search.ts
1804
+ import { KabanError as KabanError7 } from "@kaban-board/core";
1805
+ import { Command as Command11 } from "commander";
1806
+ var searchCommand = new Command11("search").description("Search tasks").argument("<query>", "Search query").option("--archive", "Search in archive only").option("-l, --limit <n>", "Max results", parseInt, 50).option("-j, --json", "Output as JSON").action(async (query, options) => {
1807
+ const json = options.json;
1808
+ try {
1809
+ const { taskService } = await getContext();
1810
+ let tasks;
1811
+ if (options.archive) {
1812
+ const result = await taskService.searchArchive(query, {
1813
+ limit: options.limit
1814
+ });
1815
+ tasks = result.tasks;
1816
+ } else {
1817
+ const allTasks = await taskService.listTasks();
1818
+ const queryLower = query.toLowerCase();
1819
+ tasks = allTasks.filter((t) => t.title.toLowerCase().includes(queryLower) || t.description?.toLowerCase().includes(queryLower));
1820
+ }
1821
+ if (json) {
1822
+ outputSuccess(tasks);
1823
+ return;
1824
+ }
1825
+ if (tasks.length === 0) {
1826
+ console.log("No tasks found");
1827
+ return;
1828
+ }
1829
+ for (const task of tasks) {
1830
+ const archived = task.archived ? " [archived]" : "";
1831
+ console.log(`[${task.id.slice(0, 8)}] ${task.title}${archived}`);
1832
+ console.log(` Column: ${task.columnId} | By: ${task.createdBy}`);
1833
+ }
1834
+ } catch (error2) {
1835
+ if (error2 instanceof KabanError7) {
1836
+ if (json)
1837
+ outputError(error2.code, error2.message);
1838
+ console.error(`Error: ${error2.message}`);
1839
+ process.exit(error2.code);
1840
+ }
1841
+ throw error2;
1842
+ }
1843
+ });
1844
+
1264
1845
  // src/commands/status.ts
1265
- import { KabanError as KabanError5 } from "@kaban-board/core";
1266
- import { Command as Command9 } from "commander";
1267
- var statusCommand2 = new Command9("status").description("Show board status summary").option("-j, --json", "Output as JSON").action(async (options) => {
1846
+ import { KabanError as KabanError8 } from "@kaban-board/core";
1847
+ import { Command as Command12 } from "commander";
1848
+ var statusCommand2 = new Command12("status").description("Show board status summary").option("-j, --json", "Output as JSON").action(async (options) => {
1268
1849
  const json = options.json;
1269
1850
  try {
1270
1851
  const { taskService, boardService } = await getContext();
@@ -1307,7 +1888,7 @@ var statusCommand2 = new Command9("status").description("Show board status summa
1307
1888
  }
1308
1889
  console.log();
1309
1890
  } catch (error2) {
1310
- if (error2 instanceof KabanError5) {
1891
+ if (error2 instanceof KabanError8) {
1311
1892
  if (json)
1312
1893
  outputError(error2.code, error2.message);
1313
1894
  console.error(`Error: ${error2.message}`);
@@ -1318,7 +1899,7 @@ var statusCommand2 = new Command9("status").description("Show board status summa
1318
1899
  });
1319
1900
 
1320
1901
  // src/commands/sync.ts
1321
- import { Command as Command10 } from "commander";
1902
+ import { Command as Command13 } from "commander";
1322
1903
 
1323
1904
  // src/hook/constants.ts
1324
1905
  var STATUS_PRIORITY = {
@@ -1412,23 +1993,26 @@ class ConflictResolver {
1412
1993
  }
1413
1994
 
1414
1995
  // src/hook/kaban-client.ts
1415
- import { spawn as spawn2 } from "node:child_process";
1996
+ import { spawn as spawn2 } from "child_process";
1416
1997
 
1417
1998
  class KabanClient {
1418
1999
  cwd;
2000
+ kabanCmd;
1419
2001
  constructor(cwd) {
1420
2002
  this.cwd = cwd;
2003
+ const cliOverride = process.env.KABAN_CLI;
2004
+ this.kabanCmd = cliOverride ? cliOverride.split(" ") : ["kaban"];
1421
2005
  }
1422
2006
  async boardExists() {
1423
2007
  try {
1424
- const result = await this.exec(["kaban", "status", "--json"]);
2008
+ const result = await this.exec([...this.kabanCmd, "status", "--json"]);
1425
2009
  return result.exitCode === 0;
1426
2010
  } catch {
1427
2011
  return false;
1428
2012
  }
1429
2013
  }
1430
2014
  async listTasks(columnId) {
1431
- const args = ["kaban", "list", "--json"];
2015
+ const args = [...this.kabanCmd, "list", "--json"];
1432
2016
  if (columnId) {
1433
2017
  args.push("--column", columnId);
1434
2018
  }
@@ -1437,7 +2021,8 @@ class KabanClient {
1437
2021
  return [];
1438
2022
  }
1439
2023
  try {
1440
- const tasks = JSON.parse(result.stdout);
2024
+ const parsed = JSON.parse(result.stdout);
2025
+ const tasks = parsed.data ?? parsed;
1441
2026
  return tasks.map((t) => ({
1442
2027
  id: t.id,
1443
2028
  title: t.title,
@@ -1450,12 +2035,13 @@ class KabanClient {
1450
2035
  }
1451
2036
  }
1452
2037
  async getTaskById(id) {
1453
- const result = await this.exec(["kaban", "get", id, "--json"]);
2038
+ const result = await this.exec([...this.kabanCmd, "get", id, "--json"]);
1454
2039
  if (result.exitCode !== 0) {
1455
2040
  return null;
1456
2041
  }
1457
2042
  try {
1458
- const task = JSON.parse(result.stdout);
2043
+ const parsed = JSON.parse(result.stdout);
2044
+ const task = parsed.data ?? parsed;
1459
2045
  return {
1460
2046
  id: task.id,
1461
2047
  title: task.title,
@@ -1472,7 +2058,7 @@ class KabanClient {
1472
2058
  return tasks.find((t) => t.title === title) ?? null;
1473
2059
  }
1474
2060
  async addTask(title, columnId = "todo") {
1475
- const result = await this.exec(["kaban", "add", title, "--column", columnId, "--json"]);
2061
+ const result = await this.exec([...this.kabanCmd, "add", title, "--column", columnId, "--json"]);
1476
2062
  if (result.exitCode !== 0) {
1477
2063
  return null;
1478
2064
  }
@@ -1485,20 +2071,21 @@ class KabanClient {
1485
2071
  }
1486
2072
  }
1487
2073
  async moveTask(id, columnId) {
1488
- const result = await this.exec(["kaban", "move", id, "--column", columnId]);
2074
+ const result = await this.exec([...this.kabanCmd, "move", id, columnId]);
1489
2075
  return result.exitCode === 0;
1490
2076
  }
1491
2077
  async completeTask(id) {
1492
- const result = await this.exec(["kaban", "done", id]);
2078
+ const result = await this.exec([...this.kabanCmd, "done", id]);
1493
2079
  return result.exitCode === 0;
1494
2080
  }
1495
2081
  async getStatus() {
1496
- const result = await this.exec(["kaban", "status", "--json"]);
2082
+ const result = await this.exec([...this.kabanCmd, "status", "--json"]);
1497
2083
  if (result.exitCode !== 0) {
1498
2084
  return null;
1499
2085
  }
1500
2086
  try {
1501
- return JSON.parse(result.stdout);
2087
+ const parsed = JSON.parse(result.stdout);
2088
+ return parsed.data ?? parsed;
1502
2089
  } catch {
1503
2090
  return null;
1504
2091
  }
@@ -1615,10 +2202,10 @@ class SyncEngine {
1615
2202
  }
1616
2203
 
1617
2204
  // src/hook/sync-logger.ts
1618
- import { existsSync as existsSync6 } from "node:fs";
1619
- import { appendFile, mkdir as mkdir2 } from "node:fs/promises";
1620
- import { homedir as homedir3 } from "node:os";
1621
- import { dirname as dirname2 } from "node:path";
2205
+ import { existsSync as existsSync6 } from "fs";
2206
+ import { appendFile, mkdir as mkdir2 } from "fs/promises";
2207
+ import { homedir as homedir3 } from "os";
2208
+ import { dirname as dirname2 } from "path";
1622
2209
 
1623
2210
  class SyncLogger {
1624
2211
  logPath;
@@ -1654,7 +2241,7 @@ class SyncLogger {
1654
2241
  }
1655
2242
 
1656
2243
  // src/commands/sync.ts
1657
- var syncCommand = new Command10("sync").description("Sync TodoWrite input to Kaban board (reads from stdin)").option("--no-log", "Disable sync logging").action(async (options) => {
2244
+ var syncCommand = new Command13("sync").description("Sync TodoWrite input to Kaban board (reads from stdin)").option("--no-log", "Disable sync logging").action(async (options) => {
1658
2245
  const startTime = performance.now();
1659
2246
  let input;
1660
2247
  try {
@@ -1701,77 +2288,87 @@ var syncCommand = new Command10("sync").description("Sync TodoWrite input to Kab
1701
2288
  });
1702
2289
 
1703
2290
  // src/commands/tui.ts
1704
- import { spawn as spawn3, spawnSync } from "node:child_process";
1705
- import { existsSync as existsSync7 } from "node:fs";
1706
- import { dirname as dirname3, join as join5 } from "node:path";
1707
- import { Command as Command11 } from "commander";
2291
+ import { spawn as spawn3, spawnSync } from "child_process";
2292
+ import { existsSync as existsSync7 } from "fs";
2293
+ import { createRequire } from "module";
2294
+ import { dirname as dirname3, join as join5 } from "path";
2295
+ import { fileURLToPath } from "url";
2296
+ import { Command as Command14 } from "commander";
2297
+ var __dirname2 = dirname3(fileURLToPath(import.meta.url));
2298
+ var require2 = createRequire(import.meta.url);
2299
+ function hasBun() {
2300
+ try {
2301
+ const result = spawnSync("bun", ["--version"], { stdio: "ignore" });
2302
+ return result.status === 0;
2303
+ } catch {
2304
+ return false;
2305
+ }
2306
+ }
1708
2307
  function findInPath(name) {
1709
2308
  const result = spawnSync("which", [name], { encoding: "utf-8" });
1710
2309
  return result.status === 0 ? result.stdout.trim() : null;
1711
2310
  }
2311
+ function resolveTuiPackage() {
2312
+ try {
2313
+ return require2.resolve("@kaban-board/tui");
2314
+ } catch {
2315
+ return null;
2316
+ }
2317
+ }
1712
2318
  function runBinary(path, args) {
1713
2319
  const child = spawn3(path, args, { stdio: "inherit", cwd: process.cwd() });
1714
2320
  child.on("exit", (code) => process.exit(code ?? 0));
1715
2321
  }
1716
- function runBunx(bunPath, args) {
1717
- let started = false;
1718
- const child = spawn3(bunPath, ["x", "@kaban-board/tui", ...args], {
1719
- stdio: "inherit",
1720
- cwd: process.cwd()
1721
- });
1722
- child.on("spawn", () => {
1723
- started = true;
1724
- });
1725
- child.on("error", () => {
1726
- if (!started)
1727
- showInstallError();
1728
- });
1729
- child.on("exit", (code) => process.exit(code ?? 0));
1730
- return true;
1731
- }
1732
- function showInstallError() {
1733
- console.error(`
1734
- Error: kaban-tui not found
1735
-
1736
- The TUI requires Bun runtime. Install with one of:
1737
-
1738
- # Homebrew (recommended)
1739
- brew install beshkenadze/tap/kaban-tui
1740
-
1741
- # Or install Bun, then run via bunx
1742
- curl -fsSL https://bun.sh/install | bash
1743
- bunx @kaban-board/tui
1744
- `);
1745
- process.exit(1);
1746
- }
1747
- var tuiCommand = new Command11("tui").description("Start interactive Terminal UI (requires Bun)").action(async () => {
2322
+ var tuiCommand = new Command14("tui").description("Start interactive Terminal UI").action(async () => {
2323
+ const cwd = process.cwd();
1748
2324
  const args = process.argv.slice(3);
2325
+ const useBun = hasBun();
1749
2326
  const siblingBinary = join5(dirname3(process.execPath), "kaban-tui");
1750
2327
  if (existsSync7(siblingBinary))
1751
2328
  return runBinary(siblingBinary, args);
1752
2329
  const pathBinary = findInPath("kaban-tui");
1753
2330
  if (pathBinary)
1754
2331
  return runBinary(pathBinary, args);
1755
- const bunPath = findInPath("bun");
1756
- if (bunPath)
1757
- return runBunx(bunPath, args);
1758
- showInstallError();
2332
+ const tuiDevEntry = join5(__dirname2, "../../../tui/src/index.ts");
2333
+ if (existsSync7(tuiDevEntry) && useBun) {
2334
+ const child2 = spawn3("bun", ["run", tuiDevEntry], { stdio: "inherit", cwd });
2335
+ child2.on("exit", (code) => process.exit(code ?? 0));
2336
+ return;
2337
+ }
2338
+ if (!useBun) {
2339
+ console.error("TUI requires Bun. Install: curl -fsSL https://bun.sh/install | bash");
2340
+ process.exit(1);
2341
+ }
2342
+ const tuiEntry = resolveTuiPackage();
2343
+ if (tuiEntry) {
2344
+ const child2 = spawn3("bun", [tuiEntry, ...args], { stdio: "inherit", cwd });
2345
+ child2.on("exit", (code) => process.exit(code ?? 0));
2346
+ return;
2347
+ }
2348
+ const child = spawn3("bun", ["x", "@kaban-board/tui", ...args], { stdio: "inherit", cwd });
2349
+ child.on("exit", (code) => process.exit(code ?? 0));
1759
2350
  });
1760
2351
 
1761
2352
  // src/index.ts
1762
- var require2 = createRequire(import.meta.url);
1763
- var pkg = require2("../package.json");
1764
- var program = new Command12;
2353
+ var require3 = createRequire2(import.meta.url);
2354
+ var pkg = require3("../package.json");
2355
+ var program = new Command15;
1765
2356
  program.name("kaban").description("Terminal Kanban for AI Code Agents").version(pkg.version);
1766
2357
  program.addCommand(initCommand);
1767
2358
  program.addCommand(addCommand);
1768
2359
  program.addCommand(listCommand);
1769
2360
  program.addCommand(moveCommand);
2361
+ program.addCommand(assignCommand);
1770
2362
  program.addCommand(doneCommand);
1771
2363
  program.addCommand(statusCommand2);
1772
2364
  program.addCommand(schemaCommand);
2365
+ program.addCommand(searchCommand);
1773
2366
  program.addCommand(mcpCommand);
1774
2367
  program.addCommand(tuiCommand);
1775
2368
  program.addCommand(hookCommand);
1776
2369
  program.addCommand(syncCommand);
2370
+ program.addCommand(archiveCommand);
2371
+ program.addCommand(restoreCommand);
2372
+ program.addCommand(purgeCommand);
2373
+ program.addCommand(resetCommand);
1777
2374
  program.parse();