@kaban-board/cli 0.1.3 → 0.2.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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command10 } from "commander";
5
4
  import { createRequire } from "node:module";
5
+ import { Command as Command12 } from "commander";
6
6
 
7
7
  // src/commands/add.ts
8
8
  import { KabanError } from "@kaban-board/core";
@@ -128,19 +128,575 @@ var doneCommand = new Command2("done").description("Mark a task as done").argume
128
128
  }
129
129
  });
130
130
 
131
- // src/commands/init.ts
132
- import { existsSync as existsSync2, mkdirSync, writeFileSync } from "node:fs";
133
- import { BoardService as BoardService2, createDb as createDb2, DEFAULT_CONFIG, initializeSchema } from "@kaban-board/core";
131
+ // 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";
137
+ import * as p from "@clack/prompts";
138
+ import chalk from "chalk";
134
139
  import { Command as Command3 } from "commander";
135
- var initCommand = new Command3("init").description("Initialize a new Kaban board in the current directory").option("-n, --name <name>", "Board name", "Kaban Board").action(async (options) => {
140
+
141
+ // 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";
146
+
147
+ // src/hook/schemas.ts
148
+ import { z } from "zod";
149
+ var TodoStatusSchema = z.enum(["pending", "in_progress", "completed", "cancelled"]);
150
+ var TodoPrioritySchema = z.enum(["high", "medium", "low"]);
151
+ var TodoItemSchema = z.object({
152
+ id: z.string().min(1),
153
+ content: z.string().min(1).max(500),
154
+ status: TodoStatusSchema,
155
+ priority: TodoPrioritySchema
156
+ });
157
+ var TodoWriteInputSchema = z.object({
158
+ todos: z.array(TodoItemSchema)
159
+ });
160
+ var HookInputSchema = z.object({
161
+ session_id: z.string(),
162
+ transcript_path: z.string(),
163
+ cwd: z.string(),
164
+ permission_mode: z.string(),
165
+ hook_event_name: z.literal("PostToolUse"),
166
+ tool_name: z.string(),
167
+ tool_input: z.unknown(),
168
+ tool_response: z.unknown().optional(),
169
+ tool_use_id: z.string()
170
+ });
171
+ var TodoWriteHookInputSchema = HookInputSchema.extend({
172
+ tool_name: z.literal("TodoWrite"),
173
+ tool_input: TodoWriteInputSchema
174
+ });
175
+ var HookCommandSchema = z.object({
176
+ type: z.literal("command"),
177
+ command: z.string(),
178
+ timeout: z.number().optional()
179
+ });
180
+ var HookEntrySchema = z.object({
181
+ matcher: z.string(),
182
+ hooks: z.array(HookCommandSchema),
183
+ description: z.string().optional()
184
+ });
185
+ var HooksConfigSchema = z.object({
186
+ PostToolUse: z.array(HookEntrySchema).optional(),
187
+ PreToolUse: z.array(HookEntrySchema).optional(),
188
+ Notification: z.array(HookEntrySchema).optional(),
189
+ Stop: z.array(HookEntrySchema).optional()
190
+ });
191
+ var ClaudeSettingsSchema = z.object({
192
+ hooks: HooksConfigSchema.optional()
193
+ }).passthrough();
194
+ var TODOWRITE_HOOK_ENTRY = {
195
+ matcher: "TodoWrite",
196
+ hooks: [
197
+ {
198
+ type: "command",
199
+ command: "~/.claude/hooks/kaban-hook",
200
+ timeout: 10
201
+ }
202
+ ],
203
+ description: "Auto-sync TodoWrite changes to Kaban board"
204
+ };
205
+ var SyncConfigSchema = z.object({
206
+ conflictStrategy: z.enum(["todowrite_wins", "status_priority", "kaban_wins"]).default("status_priority"),
207
+ deletionPolicy: z.enum(["preserve", "archive", "delete"]).default("preserve"),
208
+ cancelledPolicy: z.enum(["skip", "backlog"]).default("skip"),
209
+ syncCooldownMs: z.number().min(0).max(5000).default(200),
210
+ maxTitleLength: z.number().min(50).max(1000).default(200),
211
+ logEnabled: z.boolean().default(true),
212
+ logPath: z.string().default("~/.claude/hooks/sync.log")
213
+ });
214
+ var DEFAULT_CONFIG = {
215
+ conflictStrategy: "status_priority",
216
+ deletionPolicy: "preserve",
217
+ cancelledPolicy: "skip",
218
+ syncCooldownMs: 200,
219
+ maxTitleLength: 200,
220
+ logEnabled: true,
221
+ logPath: "~/.claude/hooks/sync.log"
222
+ };
223
+
224
+ // src/hook/settings-manager.ts
225
+ var SETTINGS_PATH = join2(homedir(), ".claude", "settings.json");
226
+
227
+ class SettingsManager {
228
+ settingsPath;
229
+ constructor(settingsPath = SETTINGS_PATH) {
230
+ this.settingsPath = settingsPath;
231
+ }
232
+ async read() {
233
+ if (!existsSync2(this.settingsPath)) {
234
+ return {};
235
+ }
236
+ const content = await readFile(this.settingsPath, "utf-8");
237
+ const parsed = JSON.parse(content);
238
+ return ClaudeSettingsSchema.parse(parsed);
239
+ }
240
+ async write(settings) {
241
+ const content = JSON.stringify(settings, null, 2);
242
+ await writeFile(this.settingsPath, `${content}
243
+ `);
244
+ }
245
+ async backup() {
246
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
247
+ const backupPath = `${this.settingsPath}.backup-${timestamp}`;
248
+ await copyFile(this.settingsPath, backupPath);
249
+ return backupPath;
250
+ }
251
+ async addHook() {
252
+ const settings = await this.read();
253
+ if (this.hasHook(settings)) {
254
+ return { added: false };
255
+ }
256
+ let backupPath;
257
+ if (existsSync2(this.settingsPath)) {
258
+ backupPath = await this.backup();
259
+ }
260
+ if (!settings.hooks) {
261
+ settings.hooks = {};
262
+ }
263
+ if (!settings.hooks.PostToolUse) {
264
+ settings.hooks.PostToolUse = [];
265
+ }
266
+ settings.hooks.PostToolUse.push(TODOWRITE_HOOK_ENTRY);
267
+ await this.write(settings);
268
+ return { added: true, backupPath };
269
+ }
270
+ async removeHook() {
271
+ const settings = await this.read();
272
+ if (!this.hasHook(settings)) {
273
+ return { removed: false };
274
+ }
275
+ const hooks = settings.hooks;
276
+ if (hooks?.PostToolUse) {
277
+ hooks.PostToolUse = hooks.PostToolUse.filter((hook) => !this.isTodoWriteHook(hook));
278
+ if (hooks.PostToolUse.length === 0) {
279
+ delete hooks.PostToolUse;
280
+ }
281
+ if (Object.keys(hooks).length === 0) {
282
+ delete settings.hooks;
283
+ }
284
+ }
285
+ await this.write(settings);
286
+ return { removed: true };
287
+ }
288
+ hasHook(settings) {
289
+ const postToolUseHooks = settings.hooks?.PostToolUse;
290
+ if (!postToolUseHooks)
291
+ return false;
292
+ return postToolUseHooks.some((hook) => this.isTodoWriteHook(hook));
293
+ }
294
+ isTodoWriteHook(hook) {
295
+ return hook.matcher === "TodoWrite" && hook.hooks.some((h) => h.command.includes("kaban-hook"));
296
+ }
297
+ }
298
+
299
+ // src/commands/hook.ts
300
+ var HOOKS_DIR = join3(homedir2(), ".claude", "hooks");
301
+ var HOOK_BINARY_NAME = "kaban-hook";
302
+ var LOG_FILE = "sync.log";
303
+ async function checkKabanCli() {
304
+ return new Promise((resolve2) => {
305
+ try {
306
+ const proc = spawn("kaban", ["--version"]);
307
+ let stdout = "";
308
+ proc.stdout.on("data", (data) => {
309
+ stdout += data.toString();
310
+ });
311
+ proc.on("close", (code) => {
312
+ if (code === 0) {
313
+ resolve2({ ok: true, message: stdout.trim() });
314
+ } else {
315
+ resolve2({ ok: false, message: "not working" });
316
+ }
317
+ });
318
+ proc.on("error", () => {
319
+ resolve2({ ok: false, message: "not found in PATH" });
320
+ });
321
+ } catch {
322
+ resolve2({ ok: false, message: "not found in PATH" });
323
+ }
324
+ });
325
+ }
326
+ async function checkDependencies() {
327
+ const results = [];
328
+ const isBun = typeof Bun !== "undefined";
329
+ results.push({
330
+ name: "Runtime",
331
+ ok: true,
332
+ message: isBun ? `Bun v${Bun.version}` : `Node ${process.version}`
333
+ });
334
+ const claudeDir = join3(homedir2(), ".claude");
335
+ if (existsSync3(claudeDir)) {
336
+ results.push({
337
+ name: "Claude Code",
338
+ ok: true,
339
+ message: "~/.claude/ exists"
340
+ });
341
+ } else {
342
+ results.push({
343
+ name: "Claude Code",
344
+ ok: false,
345
+ message: "~/.claude/ not found - run Claude Code first"
346
+ });
347
+ }
348
+ const settingsPath = join3(claudeDir, "settings.json");
349
+ if (existsSync3(settingsPath)) {
350
+ try {
351
+ const content = await readFile2(settingsPath, "utf-8");
352
+ JSON.parse(content);
353
+ results.push({
354
+ name: "Settings",
355
+ ok: true,
356
+ message: "settings.json valid"
357
+ });
358
+ } catch {
359
+ results.push({
360
+ name: "Settings",
361
+ ok: false,
362
+ message: "settings.json is not valid JSON"
363
+ });
364
+ }
365
+ } else {
366
+ results.push({
367
+ name: "Settings",
368
+ ok: true,
369
+ message: "settings.json will be created"
370
+ });
371
+ }
372
+ const kabanInstalled = await checkKabanCli();
373
+ results.push({
374
+ name: "Kaban CLI",
375
+ ok: kabanInstalled.ok,
376
+ message: kabanInstalled.message
377
+ });
378
+ const allOk = results.every((r) => r.ok);
379
+ return { ok: allOk, results };
380
+ }
381
+ function formatCheckResults(results) {
382
+ const maxNameLen = Math.max(...results.map((r) => r.name.length));
383
+ return results.map((r) => {
384
+ const icon = r.ok ? chalk.green("✓") : chalk.red("✗");
385
+ const name = r.name.padEnd(maxNameLen);
386
+ const msg = r.ok ? chalk.dim(r.message) : chalk.red(r.message);
387
+ return ` ${icon} ${name} ${msg}`;
388
+ }).join(`
389
+ `);
390
+ }
391
+ async function installHook(spinner2) {
392
+ spinner2.message("Creating hooks directory...");
393
+ if (!existsSync3(HOOKS_DIR)) {
394
+ await mkdir(HOOKS_DIR, { recursive: true });
395
+ }
396
+ spinner2.message("Installing hook binary...");
397
+ const scriptPath = realpathSync(process.argv[1]);
398
+ const scriptDir = dirname(scriptPath);
399
+ const isDevMode = scriptPath.includes("/src/");
400
+ const distDir = isDevMode ? join3(scriptDir, "..", "dist") : scriptDir;
401
+ const sourceBinary = join3(distDir, HOOK_BINARY_NAME);
402
+ const targetBinary = join3(HOOKS_DIR, HOOK_BINARY_NAME);
403
+ if (!existsSync3(sourceBinary)) {
404
+ throw new Error(`Binary not found at ${sourceBinary}. Run 'bun run build' first.`);
405
+ }
406
+ await copyFile2(sourceBinary, targetBinary);
407
+ await chmod(targetBinary, 493);
408
+ spinner2.message("Configuring Claude Code settings...");
409
+ const settingsManager = new SettingsManager;
410
+ const result = await settingsManager.addHook();
411
+ const installResult = {
412
+ hookInstalled: true,
413
+ hookConfigured: result.added
414
+ };
415
+ if (result.backupPath) {
416
+ installResult.backupPath = result.backupPath;
417
+ }
418
+ return installResult;
419
+ }
420
+ async function uninstallHook(cleanLogs) {
421
+ const result = {
422
+ hookRemoved: false,
423
+ binaryRemoved: false,
424
+ logRemoved: false
425
+ };
426
+ const settingsManager = new SettingsManager;
427
+ const hookResult = await settingsManager.removeHook();
428
+ result.hookRemoved = hookResult.removed;
429
+ const binaryPath = join3(HOOKS_DIR, HOOK_BINARY_NAME);
430
+ if (existsSync3(binaryPath)) {
431
+ await unlink(binaryPath);
432
+ result.binaryRemoved = true;
433
+ }
434
+ const logPath = join3(HOOKS_DIR, LOG_FILE);
435
+ if (existsSync3(logPath) && cleanLogs) {
436
+ await unlink(logPath);
437
+ result.logRemoved = true;
438
+ }
439
+ return result;
440
+ }
441
+ async function getRecentActivity() {
442
+ const logPath = join3(HOOKS_DIR, LOG_FILE);
443
+ if (!existsSync3(logPath)) {
444
+ return null;
445
+ }
446
+ try {
447
+ const content = await readFile2(logPath, "utf-8");
448
+ const lines = content.trim().split(`
449
+ `).filter(Boolean);
450
+ const entries = lines.map((line) => JSON.parse(line));
451
+ const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
452
+ const recentEntries = entries.filter((e) => new Date(e.timestamp) > oneDayAgo);
453
+ const stats = recentEntries.reduce((acc, e) => ({
454
+ syncs: acc.syncs + 1,
455
+ created: acc.created + e.created,
456
+ moved: acc.moved + e.moved,
457
+ errors: acc.errors + e.errors.length
458
+ }), { syncs: 0, created: 0, moved: 0, errors: 0 });
459
+ const lastEntry = entries.at(-1);
460
+ const logStats = await stat(logPath);
461
+ const result = { ...stats };
462
+ if (lastEntry) {
463
+ result.lastSync = new Date(lastEntry.timestamp);
464
+ }
465
+ if (logStats.size > 0) {
466
+ result.logSize = logStats.size;
467
+ }
468
+ return result;
469
+ } catch {
470
+ return null;
471
+ }
472
+ }
473
+ function formatTimeAgo(date) {
474
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
475
+ if (seconds < 60)
476
+ return `${seconds} sec ago`;
477
+ if (seconds < 3600)
478
+ return `${Math.floor(seconds / 60)} min ago`;
479
+ if (seconds < 86400)
480
+ return `${Math.floor(seconds / 3600)} hours ago`;
481
+ return `${Math.floor(seconds / 86400)} days ago`;
482
+ }
483
+ function formatSize(bytes) {
484
+ if (bytes < 1024)
485
+ return `${bytes} B`;
486
+ return `${Math.round(bytes / 1024)} KB`;
487
+ }
488
+ var installCommand = new Command3("install").description("Install TodoWrite sync hook for Claude Code").option("-y, --yes", "Skip confirmation").action(async (options) => {
489
+ p.intro(chalk.bgCyan.black(" kaban hook install "));
490
+ const s = p.spinner();
491
+ s.start("Checking dependencies...");
492
+ const { ok, results } = await checkDependencies();
493
+ s.stop("Dependencies checked");
494
+ p.note(formatCheckResults(results), "Environment");
495
+ if (!ok) {
496
+ p.cancel("Dependency check failed. Please fix the issues above.");
497
+ process.exit(1);
498
+ }
499
+ if (!options.yes) {
500
+ const proceed = await p.confirm({
501
+ message: "Install TodoWrite-Kaban sync hook?",
502
+ initialValue: true
503
+ });
504
+ if (p.isCancel(proceed) || !proceed) {
505
+ p.cancel("Installation cancelled.");
506
+ process.exit(0);
507
+ }
508
+ }
509
+ s.start("Installing...");
510
+ try {
511
+ const installResult = await installHook(s);
512
+ s.stop("Installation complete");
513
+ const summaryLines = [
514
+ "",
515
+ ` ${chalk.cyan("Hook Binary")}`,
516
+ ` ${chalk.dim("Path:")} ~/.claude/hooks/${HOOK_BINARY_NAME}`,
517
+ "",
518
+ ` ${chalk.cyan("Hook Configuration")}`,
519
+ ` ${chalk.dim("Event:")} PostToolUse`,
520
+ ` ${chalk.dim("Matcher:")} TodoWrite`,
521
+ ` ${chalk.dim("Timeout:")} 10s`,
522
+ ""
523
+ ];
524
+ if (installResult.backupPath) {
525
+ summaryLines.push(` ${chalk.cyan("Backup")}`);
526
+ summaryLines.push(` ${chalk.dim(installResult.backupPath)}`);
527
+ summaryLines.push("");
528
+ }
529
+ if (!installResult.hookConfigured) {
530
+ summaryLines.push(` ${chalk.yellow("⚠")} Hook was already configured`);
531
+ summaryLines.push("");
532
+ }
533
+ p.note(summaryLines.join(`
534
+ `), "Installation Summary");
535
+ p.log.success("TodoWrite changes will now auto-sync to Kaban board!");
536
+ p.note([
537
+ ` ${chalk.cyan("Verify:")} kaban hook status`,
538
+ ` ${chalk.cyan("Logs:")} ~/.claude/hooks/sync.log`,
539
+ ` ${chalk.cyan("Remove:")} kaban hook uninstall`
540
+ ].join(`
541
+ `), "Next Steps");
542
+ p.outro(chalk.green("Done!"));
543
+ } catch (error2) {
544
+ s.stop("Installation failed");
545
+ p.cancel(error2 instanceof Error ? error2.message : "Unknown error");
546
+ process.exit(1);
547
+ }
548
+ });
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) => {
550
+ p.intro(chalk.bgRed.white(" kaban hook uninstall "));
551
+ const binaryExists = existsSync3(join3(HOOKS_DIR, HOOK_BINARY_NAME));
552
+ const logExists = existsSync3(join3(HOOKS_DIR, LOG_FILE));
553
+ const settingsManager = new SettingsManager;
554
+ let hookExists = false;
555
+ try {
556
+ const settings = await settingsManager.read();
557
+ hookExists = settingsManager.hasHook(settings);
558
+ } catch {
559
+ hookExists = false;
560
+ }
561
+ if (!binaryExists && !hookExists) {
562
+ p.log.warn("TodoWrite-Kaban sync hook is not installed.");
563
+ p.outro("Nothing to uninstall.");
564
+ process.exit(0);
565
+ }
566
+ const formatStatusLine = (exists, label) => {
567
+ const icon = exists ? chalk.green("✓") : chalk.dim("○");
568
+ const text = exists ? "" : chalk.dim(" (not found)");
569
+ return ` ${icon} ${label}${text}`;
570
+ };
571
+ const formatLogStatus = () => {
572
+ if (!logExists)
573
+ return formatStatusLine(false, "Sync log");
574
+ const suffix = options.clean ? chalk.yellow(" (will be removed)") : chalk.dim(" (will be preserved)");
575
+ return ` ${chalk.yellow("○")} Sync log${suffix}`;
576
+ };
577
+ const statusLines = [
578
+ formatStatusLine(binaryExists, "Hook binary"),
579
+ formatStatusLine(hookExists, "Settings configuration"),
580
+ formatLogStatus()
581
+ ];
582
+ p.note(statusLines.join(`
583
+ `), "Current Installation");
584
+ if (!options.yes) {
585
+ const confirmed = await p.confirm({
586
+ message: "Remove TodoWrite-Kaban sync?",
587
+ initialValue: false
588
+ });
589
+ if (p.isCancel(confirmed) || !confirmed) {
590
+ p.cancel("Uninstallation cancelled.");
591
+ process.exit(0);
592
+ }
593
+ }
594
+ const s = p.spinner();
595
+ s.start("Removing...");
596
+ try {
597
+ const result = await uninstallHook(options.clean);
598
+ s.stop("Removal complete");
599
+ const summaryLines = [];
600
+ if (result.hookRemoved)
601
+ summaryLines.push(` ${chalk.green("✓")} Removed hook from settings.json`);
602
+ if (result.binaryRemoved)
603
+ summaryLines.push(` ${chalk.green("✓")} Removed ${HOOK_BINARY_NAME}`);
604
+ if (result.logRemoved)
605
+ summaryLines.push(` ${chalk.green("✓")} Removed ${LOG_FILE}`);
606
+ if (logExists && !options.clean)
607
+ summaryLines.push(` ${chalk.yellow("○")} Preserved ${LOG_FILE} (use --clean to remove)`);
608
+ if (summaryLines.length > 0) {
609
+ p.note(summaryLines.join(`
610
+ `), "Removed");
611
+ }
612
+ p.outro(chalk.green("Done!"));
613
+ } catch (error2) {
614
+ s.stop("Removal failed");
615
+ p.cancel(error2 instanceof Error ? error2.message : "Unknown error");
616
+ process.exit(1);
617
+ }
618
+ });
619
+ var statusCommand = new Command3("status").description("Check hook installation status").action(async () => {
620
+ p.intro(chalk.bgBlue.white(" kaban hook status "));
621
+ const s = p.spinner();
622
+ s.start("Checking status...");
623
+ const binaryPath = join3(HOOKS_DIR, HOOK_BINARY_NAME);
624
+ const binaryExists = existsSync3(binaryPath);
625
+ const settingsManager = new SettingsManager;
626
+ let hookConfigured = false;
627
+ try {
628
+ const settings = await settingsManager.read();
629
+ hookConfigured = settingsManager.hasHook(settings);
630
+ } catch {
631
+ hookConfigured = false;
632
+ }
633
+ const kabanCheck = await checkKabanCli();
634
+ const activity = await getRecentActivity();
635
+ s.stop("Status checked");
636
+ const results = [
637
+ {
638
+ name: "Hook Binary",
639
+ ok: binaryExists,
640
+ detail: binaryExists ? binaryPath : "Not found"
641
+ },
642
+ {
643
+ name: "Settings Config",
644
+ ok: hookConfigured,
645
+ detail: hookConfigured ? "PostToolUse[TodoWrite] active" : "Hook not configured"
646
+ },
647
+ {
648
+ name: "Kaban CLI",
649
+ ok: kabanCheck.ok,
650
+ detail: kabanCheck.ok ? kabanCheck.message : "Not found in PATH"
651
+ }
652
+ ];
653
+ const maxNameLen = Math.max(...results.map((r) => r.name.length));
654
+ const statusLines = results.map((r) => {
655
+ const icon = r.ok ? chalk.green("✓") : chalk.red("✗");
656
+ const name = r.name.padEnd(maxNameLen);
657
+ const detail = r.ok ? chalk.dim(r.detail) : chalk.red(r.detail);
658
+ return ` ${icon} ${name} ${detail}`;
659
+ });
660
+ p.note(statusLines.join(`
661
+ `), "Installation Status");
662
+ if (activity) {
663
+ const activityLines = [
664
+ ` ${chalk.cyan("Syncs (24h):")} ${activity.syncs}`,
665
+ ` ${chalk.cyan("Tasks created:")} ${activity.created}`,
666
+ ` ${chalk.cyan("Tasks moved:")} ${activity.moved}`,
667
+ ` ${chalk.cyan("Errors:")} ${activity.errors > 0 ? chalk.red(activity.errors.toString()) : chalk.green("0")}`,
668
+ "",
669
+ activity.lastSync ? ` ${chalk.dim("Last sync:")} ${formatTimeAgo(activity.lastSync)}` : "",
670
+ activity.logSize ? ` ${chalk.dim("Log size:")} ${formatSize(activity.logSize)}` : ""
671
+ ].filter(Boolean);
672
+ p.note(activityLines.join(`
673
+ `), "Recent Activity");
674
+ } else {
675
+ p.log.info("No sync activity logged yet.");
676
+ }
677
+ const allOk = results.every((r) => r.ok);
678
+ if (allOk) {
679
+ p.outro(chalk.green("All systems operational!"));
680
+ } else {
681
+ p.outro(chalk.yellow("Some checks failed. Run 'kaban hook install' to fix."));
682
+ process.exit(1);
683
+ }
684
+ });
685
+ var hookCommand = new Command3("hook").description("Manage TodoWrite sync hook for Claude Code").addCommand(installCommand).addCommand(uninstallCommand).addCommand(statusCommand);
686
+
687
+ // src/commands/init.ts
688
+ import { existsSync as existsSync4, mkdirSync, writeFileSync } from "node:fs";
689
+ import { BoardService as BoardService2, createDb as createDb2, DEFAULT_CONFIG as DEFAULT_CONFIG2, initializeSchema } from "@kaban-board/core";
690
+ import { Command as Command4 } from "commander";
691
+ 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) => {
136
692
  const { kabanDir, dbPath, configPath } = getKabanPaths();
137
- if (existsSync2(dbPath)) {
693
+ if (existsSync4(dbPath)) {
138
694
  console.error("Error: Board already exists in this directory");
139
695
  process.exit(1);
140
696
  }
141
697
  mkdirSync(kabanDir, { recursive: true });
142
698
  const config = {
143
- ...DEFAULT_CONFIG,
699
+ ...DEFAULT_CONFIG2,
144
700
  board: { name: options.name }
145
701
  };
146
702
  writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -155,7 +711,7 @@ var initCommand = new Command3("init").description("Initialize a new Kaban board
155
711
 
156
712
  // src/commands/list.ts
157
713
  import { KabanError as KabanError3 } from "@kaban-board/core";
158
- import { Command as Command4 } from "commander";
714
+ import { Command as Command5 } from "commander";
159
715
  function sortTasks(tasks, sortBy, reverse) {
160
716
  const sorted = [...tasks].sort((a, b) => {
161
717
  switch (sortBy) {
@@ -171,7 +727,7 @@ function sortTasks(tasks, sortBy, reverse) {
171
727
  });
172
728
  return reverse ? sorted.reverse() : sorted;
173
729
  }
174
- var listCommand = new Command4("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) => {
730
+ 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) => {
175
731
  const json = options.json;
176
732
  try {
177
733
  const { taskService, boardService } = getContext();
@@ -223,12 +779,12 @@ var listCommand = new Command4("list").description("List tasks").option("-c, --c
223
779
  });
224
780
 
225
781
  // src/commands/mcp.ts
226
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
227
- import { join as join2 } from "node:path";
782
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
783
+ import { join as join4 } from "node:path";
228
784
  import {
229
785
  BoardService as BoardService3,
230
786
  createDb as createDb3,
231
- DEFAULT_CONFIG as DEFAULT_CONFIG2,
787
+ DEFAULT_CONFIG as DEFAULT_CONFIG3,
232
788
  initializeSchema as initializeSchema2,
233
789
  TaskService as TaskService2
234
790
  } from "@kaban-board/core";
@@ -240,19 +796,19 @@ import {
240
796
  ListToolsRequestSchema,
241
797
  ReadResourceRequestSchema
242
798
  } from "@modelcontextprotocol/sdk/types.js";
243
- import { Command as Command5 } from "commander";
799
+ import { Command as Command6 } from "commander";
244
800
  function getKabanPaths2(basePath) {
245
801
  const base = basePath ?? process.cwd();
246
- const kabanDir = join2(base, ".kaban");
802
+ const kabanDir = join4(base, ".kaban");
247
803
  return {
248
804
  kabanDir,
249
- dbPath: join2(kabanDir, "board.db"),
250
- configPath: join2(kabanDir, "config.json")
805
+ dbPath: join4(kabanDir, "board.db"),
806
+ configPath: join4(kabanDir, "config.json")
251
807
  };
252
808
  }
253
809
  function createContext(basePath) {
254
810
  const { dbPath, configPath } = getKabanPaths2(basePath);
255
- if (!existsSync3(dbPath)) {
811
+ if (!existsSync5(dbPath)) {
256
812
  throw new Error("No board found. Run 'kaban init' first");
257
813
  }
258
814
  const db = createDb3(dbPath);
@@ -391,7 +947,7 @@ async function startMcpServer(workingDirectory) {
391
947
  const { name: boardName = "Kaban Board", path: basePath } = args ?? {};
392
948
  const targetPath = basePath ?? workingDirectory;
393
949
  const { kabanDir, dbPath, configPath } = getKabanPaths2(targetPath);
394
- if (existsSync3(dbPath)) {
950
+ if (existsSync5(dbPath)) {
395
951
  return {
396
952
  content: [
397
953
  {
@@ -404,7 +960,7 @@ async function startMcpServer(workingDirectory) {
404
960
  }
405
961
  mkdirSync2(kabanDir, { recursive: true });
406
962
  const config = {
407
- ...DEFAULT_CONFIG2,
963
+ ...DEFAULT_CONFIG3,
408
964
  board: { name: boardName }
409
965
  };
410
966
  writeFileSync2(configPath, JSON.stringify(config, null, 2));
@@ -618,15 +1174,15 @@ async function startMcpServer(workingDirectory) {
618
1174
  await server.connect(transport);
619
1175
  console.error("Kaban MCP server running on stdio");
620
1176
  }
621
- var mcpCommand = new Command5("mcp").description("Start MCP server for AI agent integration").option("-p, --path <path>", "Working directory for Kaban board").action(async (options) => {
1177
+ 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) => {
622
1178
  const workingDirectory = options.path ?? process.env.KABAN_PATH ?? process.cwd();
623
1179
  await startMcpServer(workingDirectory);
624
1180
  });
625
1181
 
626
1182
  // src/commands/move.ts
627
1183
  import { KabanError as KabanError4 } from "@kaban-board/core";
628
- import { Command as Command6 } from "commander";
629
- var moveCommand = new Command6("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) => {
1184
+ import { Command as Command7 } from "commander";
1185
+ 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) => {
630
1186
  const json = options.json;
631
1187
  try {
632
1188
  const { taskService, boardService } = getContext();
@@ -679,9 +1235,9 @@ var moveCommand = new Command6("move").description("Move a task to a different c
679
1235
 
680
1236
  // src/commands/schema.ts
681
1237
  import { jsonSchemas } from "@kaban-board/core";
682
- import { Command as Command7 } from "commander";
1238
+ import { Command as Command8 } from "commander";
683
1239
  var availableSchemas = Object.keys(jsonSchemas);
684
- var schemaCommand = new Command7("schema").description("Output JSON schemas for AI agents").argument("[name]", "Schema name (omit to list available)").action((name) => {
1240
+ var schemaCommand = new Command8("schema").description("Output JSON schemas for AI agents").argument("[name]", "Schema name (omit to list available)").action((name) => {
685
1241
  if (!name) {
686
1242
  console.log("Available schemas:");
687
1243
  for (const schemaName of availableSchemas) {
@@ -702,8 +1258,8 @@ Usage: kaban schema <name>`);
702
1258
 
703
1259
  // src/commands/status.ts
704
1260
  import { KabanError as KabanError5 } from "@kaban-board/core";
705
- import { Command as Command8 } from "commander";
706
- var statusCommand = new Command8("status").description("Show board status summary").option("-j, --json", "Output as JSON").action(async (options) => {
1261
+ import { Command as Command9 } from "commander";
1262
+ var statusCommand2 = new Command9("status").description("Show board status summary").option("-j, --json", "Output as JSON").action(async (options) => {
707
1263
  const json = options.json;
708
1264
  try {
709
1265
  const { taskService, boardService } = getContext();
@@ -756,15 +1312,398 @@ var statusCommand = new Command8("status").description("Show board status summar
756
1312
  }
757
1313
  });
758
1314
 
1315
+ // src/commands/sync.ts
1316
+ import { Command as Command10 } from "commander";
1317
+
1318
+ // src/hook/constants.ts
1319
+ var STATUS_PRIORITY = {
1320
+ completed: 3,
1321
+ in_progress: 2,
1322
+ pending: 1,
1323
+ cancelled: 0
1324
+ };
1325
+ var STATUS_TO_COLUMN = {
1326
+ pending: "todo",
1327
+ in_progress: "in_progress",
1328
+ completed: "done",
1329
+ cancelled: "backlog"
1330
+ };
1331
+ var COLUMN_TO_STATUS = {
1332
+ backlog: "pending",
1333
+ todo: "pending",
1334
+ in_progress: "in_progress",
1335
+ review: "in_progress",
1336
+ done: "completed"
1337
+ };
1338
+
1339
+ // src/hook/conflict-resolver.ts
1340
+ class ConflictResolver {
1341
+ strategy;
1342
+ constructor(strategy) {
1343
+ this.strategy = strategy;
1344
+ }
1345
+ resolve(todo, kabanTask) {
1346
+ const kabanStatus = this.columnToStatus(kabanTask.columnId);
1347
+ const todoColumn = STATUS_TO_COLUMN[todo.status];
1348
+ if (this.strategy === "todowrite_wins") {
1349
+ return {
1350
+ winner: "todo",
1351
+ targetColumn: todoColumn,
1352
+ reason: "todowrite_wins strategy"
1353
+ };
1354
+ }
1355
+ if (this.strategy === "kaban_wins") {
1356
+ return {
1357
+ winner: "kaban",
1358
+ targetColumn: kabanTask.columnId,
1359
+ reason: "kaban_wins strategy"
1360
+ };
1361
+ }
1362
+ if (todo.status === "completed") {
1363
+ return {
1364
+ winner: "todo",
1365
+ targetColumn: "done",
1366
+ reason: "completed status always wins (terminal state)"
1367
+ };
1368
+ }
1369
+ if (kabanTask.columnId === "done") {
1370
+ return {
1371
+ winner: "kaban",
1372
+ targetColumn: "done",
1373
+ reason: "kaban task already completed (terminal state)"
1374
+ };
1375
+ }
1376
+ const todoPriority = STATUS_PRIORITY[todo.status];
1377
+ const kabanPriority = STATUS_PRIORITY[kabanStatus];
1378
+ if (todoPriority > kabanPriority) {
1379
+ return {
1380
+ winner: "todo",
1381
+ targetColumn: todoColumn,
1382
+ reason: `todo status priority (${todoPriority}) > kaban (${kabanPriority})`
1383
+ };
1384
+ }
1385
+ if (kabanPriority > todoPriority) {
1386
+ return {
1387
+ winner: "kaban",
1388
+ targetColumn: kabanTask.columnId,
1389
+ reason: `kaban status priority (${kabanPriority}) > todo (${todoPriority})`
1390
+ };
1391
+ }
1392
+ return {
1393
+ winner: "todo",
1394
+ targetColumn: todoColumn,
1395
+ reason: "equal priority, todo wins (most recent)"
1396
+ };
1397
+ }
1398
+ shouldSync(todo, cancelledPolicy) {
1399
+ if (todo.status === "cancelled") {
1400
+ return cancelledPolicy === "backlog";
1401
+ }
1402
+ return true;
1403
+ }
1404
+ columnToStatus(columnId) {
1405
+ return COLUMN_TO_STATUS[columnId] ?? "pending";
1406
+ }
1407
+ }
1408
+
1409
+ // src/hook/kaban-client.ts
1410
+ import { spawn as spawn2 } from "node:child_process";
1411
+
1412
+ class KabanClient {
1413
+ cwd;
1414
+ constructor(cwd) {
1415
+ this.cwd = cwd;
1416
+ }
1417
+ async boardExists() {
1418
+ try {
1419
+ const result = await this.exec(["kaban", "status", "--json"]);
1420
+ return result.exitCode === 0;
1421
+ } catch {
1422
+ return false;
1423
+ }
1424
+ }
1425
+ async listTasks(columnId) {
1426
+ const args = ["kaban", "list", "--json"];
1427
+ if (columnId) {
1428
+ args.push("--column", columnId);
1429
+ }
1430
+ const result = await this.exec(args);
1431
+ if (result.exitCode !== 0) {
1432
+ return [];
1433
+ }
1434
+ try {
1435
+ const tasks = JSON.parse(result.stdout);
1436
+ return tasks.map((t) => ({
1437
+ id: t.id,
1438
+ title: t.title,
1439
+ columnId: t.columnId,
1440
+ description: t.description,
1441
+ labels: t.labels
1442
+ }));
1443
+ } catch {
1444
+ return [];
1445
+ }
1446
+ }
1447
+ async getTaskById(id) {
1448
+ const result = await this.exec(["kaban", "get", id, "--json"]);
1449
+ if (result.exitCode !== 0) {
1450
+ return null;
1451
+ }
1452
+ try {
1453
+ const task = JSON.parse(result.stdout);
1454
+ return {
1455
+ id: task.id,
1456
+ title: task.title,
1457
+ columnId: task.columnId,
1458
+ description: task.description,
1459
+ labels: task.labels
1460
+ };
1461
+ } catch {
1462
+ return null;
1463
+ }
1464
+ }
1465
+ async findTaskByTitle(title) {
1466
+ const tasks = await this.listTasks();
1467
+ return tasks.find((t) => t.title === title) ?? null;
1468
+ }
1469
+ async addTask(title, columnId = "todo") {
1470
+ const result = await this.exec(["kaban", "add", title, "--column", columnId, "--json"]);
1471
+ if (result.exitCode !== 0) {
1472
+ return null;
1473
+ }
1474
+ try {
1475
+ const response = JSON.parse(result.stdout);
1476
+ return response.data?.id ?? response.id ?? null;
1477
+ } catch {
1478
+ const match = result.stdout.match(/id[":]*\s*["']?([A-Z0-9]+)/i);
1479
+ return match?.[1] ?? null;
1480
+ }
1481
+ }
1482
+ async moveTask(id, columnId) {
1483
+ const result = await this.exec(["kaban", "move", id, "--column", columnId]);
1484
+ return result.exitCode === 0;
1485
+ }
1486
+ async completeTask(id) {
1487
+ const result = await this.exec(["kaban", "done", id]);
1488
+ return result.exitCode === 0;
1489
+ }
1490
+ async getStatus() {
1491
+ const result = await this.exec(["kaban", "status", "--json"]);
1492
+ if (result.exitCode !== 0) {
1493
+ return null;
1494
+ }
1495
+ try {
1496
+ return JSON.parse(result.stdout);
1497
+ } catch {
1498
+ return null;
1499
+ }
1500
+ }
1501
+ exec(args) {
1502
+ return new Promise((resolve2) => {
1503
+ const [cmd, ...cmdArgs] = args;
1504
+ const proc = spawn2(cmd, cmdArgs, { cwd: this.cwd });
1505
+ let stdout = "";
1506
+ let stderr = "";
1507
+ proc.stdout.on("data", (data) => {
1508
+ stdout += data.toString();
1509
+ });
1510
+ proc.stderr.on("data", (data) => {
1511
+ stderr += data.toString();
1512
+ });
1513
+ proc.on("close", (code) => {
1514
+ resolve2({ exitCode: code ?? 1, stdout, stderr });
1515
+ });
1516
+ proc.on("error", () => {
1517
+ resolve2({ exitCode: 1, stdout: "", stderr: "spawn error" });
1518
+ });
1519
+ });
1520
+ }
1521
+ }
1522
+
1523
+ // src/hook/sync-engine.ts
1524
+ class SyncEngine {
1525
+ kaban;
1526
+ resolver;
1527
+ config;
1528
+ constructor(cwd, config) {
1529
+ this.kaban = new KabanClient(cwd);
1530
+ this.resolver = new ConflictResolver(config.conflictStrategy);
1531
+ this.config = config;
1532
+ }
1533
+ async sync(todos) {
1534
+ const result = {
1535
+ success: true,
1536
+ created: 0,
1537
+ moved: 0,
1538
+ skipped: 0,
1539
+ errors: []
1540
+ };
1541
+ if (!await this.kaban.boardExists()) {
1542
+ result.skipped = todos.length;
1543
+ return result;
1544
+ }
1545
+ if (todos.length === 0) {
1546
+ return result;
1547
+ }
1548
+ const kabanTasks = await this.kaban.listTasks();
1549
+ const tasksByTitle = new Map(kabanTasks.map((t) => [t.title, t]));
1550
+ const tasksById = new Map(kabanTasks.map((t) => [t.id, t]));
1551
+ for (const todo of todos) {
1552
+ if (!this.resolver.shouldSync(todo, this.config.cancelledPolicy)) {
1553
+ result.skipped++;
1554
+ continue;
1555
+ }
1556
+ try {
1557
+ const syncResult = await this.syncTodo(todo, tasksByTitle, tasksById);
1558
+ if (syncResult === "created")
1559
+ result.created++;
1560
+ else if (syncResult === "moved")
1561
+ result.moved++;
1562
+ else
1563
+ result.skipped++;
1564
+ } catch (error2) {
1565
+ const errorMsg = error2 instanceof Error ? error2.message : String(error2);
1566
+ result.errors.push(`${this.truncateTitle(todo.content)}: ${errorMsg}`);
1567
+ result.success = false;
1568
+ }
1569
+ }
1570
+ return result;
1571
+ }
1572
+ async syncTodo(todo, tasksByTitle, tasksById) {
1573
+ const normalizedTitle = this.truncateTitle(todo.content);
1574
+ const existing = tasksById.get(todo.id) ?? tasksByTitle.get(normalizedTitle) ?? tasksByTitle.get(todo.content);
1575
+ if (existing) {
1576
+ return this.handleExisting(todo, existing);
1577
+ }
1578
+ return this.handleNew(todo);
1579
+ }
1580
+ async handleExisting(todo, existing) {
1581
+ const resolution = this.resolver.resolve(todo, existing);
1582
+ if (resolution.winner === "kaban" || existing.columnId === resolution.targetColumn) {
1583
+ return "skipped";
1584
+ }
1585
+ if (resolution.targetColumn === "done") {
1586
+ const success2 = await this.kaban.completeTask(existing.id);
1587
+ if (!success2)
1588
+ throw new Error("failed to complete task");
1589
+ } else {
1590
+ const success2 = await this.kaban.moveTask(existing.id, resolution.targetColumn);
1591
+ if (!success2)
1592
+ throw new Error(`failed to move task to ${resolution.targetColumn}`);
1593
+ }
1594
+ return "moved";
1595
+ }
1596
+ async handleNew(todo) {
1597
+ const column = todo.status === "cancelled" ? "backlog" : STATUS_TO_COLUMN[todo.status];
1598
+ const title = this.truncateTitle(todo.content);
1599
+ const taskId = await this.kaban.addTask(title, column);
1600
+ if (!taskId) {
1601
+ throw new Error("failed to create task");
1602
+ }
1603
+ return "created";
1604
+ }
1605
+ truncateTitle(title) {
1606
+ if (title.length <= this.config.maxTitleLength)
1607
+ return title;
1608
+ return `${title.slice(0, this.config.maxTitleLength - 3)}...`;
1609
+ }
1610
+ }
1611
+
1612
+ // src/hook/sync-logger.ts
1613
+ import { existsSync as existsSync6 } from "node:fs";
1614
+ import { appendFile, mkdir as mkdir2 } from "node:fs/promises";
1615
+ import { homedir as homedir3 } from "node:os";
1616
+ import { dirname as dirname2 } from "node:path";
1617
+
1618
+ class SyncLogger {
1619
+ logPath;
1620
+ constructor(logPath) {
1621
+ this.logPath = this.expandPath(logPath);
1622
+ }
1623
+ async log(todosCount, result, durationMs) {
1624
+ const entry = {
1625
+ timestamp: new Date().toISOString(),
1626
+ todosCount,
1627
+ created: result.created,
1628
+ moved: result.moved,
1629
+ skipped: result.skipped,
1630
+ errors: result.errors,
1631
+ durationMs
1632
+ };
1633
+ await this.ensureDir();
1634
+ await appendFile(this.logPath, `${JSON.stringify(entry)}
1635
+ `);
1636
+ }
1637
+ async ensureDir() {
1638
+ const dir = dirname2(this.logPath);
1639
+ if (!existsSync6(dir)) {
1640
+ await mkdir2(dir, { recursive: true });
1641
+ }
1642
+ }
1643
+ expandPath(path) {
1644
+ if (path.startsWith("~/")) {
1645
+ return path.replace("~", homedir3());
1646
+ }
1647
+ return path;
1648
+ }
1649
+ }
1650
+
1651
+ // src/commands/sync.ts
1652
+ var syncCommand = new Command10("sync").description("Sync TodoWrite input to Kaban board (reads from stdin)").option("--no-log", "Disable sync logging").action(async (options) => {
1653
+ const startTime = performance.now();
1654
+ let input;
1655
+ try {
1656
+ const chunks = [];
1657
+ for await (const chunk of process.stdin) {
1658
+ chunks.push(chunk);
1659
+ }
1660
+ input = Buffer.concat(chunks).toString("utf-8");
1661
+ } catch {
1662
+ process.exit(0);
1663
+ }
1664
+ if (!input.trim()) {
1665
+ process.exit(0);
1666
+ }
1667
+ let parsed;
1668
+ try {
1669
+ parsed = JSON.parse(input);
1670
+ } catch {
1671
+ process.exit(0);
1672
+ }
1673
+ const validation = TodoWriteHookInputSchema.safeParse(parsed);
1674
+ if (!validation.success) {
1675
+ process.exit(0);
1676
+ }
1677
+ const hookInput = validation.data;
1678
+ const { cwd, tool_input } = hookInput;
1679
+ const { todos } = tool_input;
1680
+ if (todos.length === 0) {
1681
+ process.exit(0);
1682
+ }
1683
+ const config = { ...DEFAULT_CONFIG, logEnabled: options.log !== false };
1684
+ const engine = new SyncEngine(cwd, config);
1685
+ const logger = new SyncLogger(config.logPath);
1686
+ try {
1687
+ const result = await engine.sync(todos);
1688
+ const durationMs = Math.round(performance.now() - startTime);
1689
+ if (config.logEnabled) {
1690
+ await logger.log(todos.length, result, durationMs);
1691
+ }
1692
+ process.exit(result.success ? 0 : 1);
1693
+ } catch {
1694
+ process.exit(1);
1695
+ }
1696
+ });
1697
+
759
1698
  // src/commands/tui.ts
760
- import { spawn } from "node:child_process";
761
- import { dirname, join as join3 } from "node:path";
1699
+ import { spawn as spawn3 } from "node:child_process";
1700
+ import { dirname as dirname3, join as join5 } from "node:path";
762
1701
  import { fileURLToPath } from "node:url";
763
- import { Command as Command9 } from "commander";
764
- var __dirname2 = dirname(fileURLToPath(import.meta.url));
765
- var tuiCommand = new Command9("tui").description("Start interactive Terminal UI").action(async () => {
766
- const tuiEntry = join3(__dirname2, "../../../tui/src/index.ts");
767
- const child = spawn("bun", ["run", tuiEntry], {
1702
+ import { Command as Command11 } from "commander";
1703
+ var __dirname2 = dirname3(fileURLToPath(import.meta.url));
1704
+ var tuiCommand = new Command11("tui").description("Start interactive Terminal UI").action(async () => {
1705
+ const tuiEntry = join5(__dirname2, "../../../tui/src/index.ts");
1706
+ const child = spawn3("bun", ["run", tuiEntry], {
768
1707
  stdio: "inherit",
769
1708
  cwd: process.cwd()
770
1709
  });
@@ -776,15 +1715,17 @@ var tuiCommand = new Command9("tui").description("Start interactive Terminal UI"
776
1715
  // src/index.ts
777
1716
  var require2 = createRequire(import.meta.url);
778
1717
  var pkg = require2("../package.json");
779
- var program = new Command10;
1718
+ var program = new Command12;
780
1719
  program.name("kaban").description("Terminal Kanban for AI Code Agents").version(pkg.version);
781
1720
  program.addCommand(initCommand);
782
1721
  program.addCommand(addCommand);
783
1722
  program.addCommand(listCommand);
784
1723
  program.addCommand(moveCommand);
785
1724
  program.addCommand(doneCommand);
786
- program.addCommand(statusCommand);
1725
+ program.addCommand(statusCommand2);
787
1726
  program.addCommand(schemaCommand);
788
1727
  program.addCommand(mcpCommand);
789
1728
  program.addCommand(tuiCommand);
1729
+ program.addCommand(hookCommand);
1730
+ program.addCommand(syncCommand);
790
1731
  program.parse();
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hook-entry.ts
4
+ import { spawn } from "node:child_process";
5
+ var kaban = spawn("kaban", ["sync"], {
6
+ stdio: ["pipe", "inherit", "inherit"]
7
+ });
8
+ process.stdin.pipe(kaban.stdin);
9
+ kaban.on("close", (code) => {
10
+ process.exit(code ?? 0);
11
+ });
12
+ kaban.on("error", () => {
13
+ process.exit(0);
14
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaban-board/cli",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Terminal Kanban for AI Code Agents - CLI and MCP server",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,16 +10,19 @@
10
10
  "dist"
11
11
  ],
12
12
  "scripts": {
13
- "build": "bun build ./src/index.ts --outdir ./dist --target node --packages external && chmod +x ./dist/index.js",
13
+ "build": "bun build ./src/index.ts --outdir ./dist --target node --packages external && bun build ./src/hook-entry.ts --outfile ./dist/kaban-hook --target node && chmod +x ./dist/index.js ./dist/kaban-hook",
14
14
  "dev": "bun run ./src/index.ts",
15
15
  "test": "bun test",
16
16
  "typecheck": "tsc --noEmit",
17
17
  "prepublishOnly": "npm run build"
18
18
  },
19
19
  "dependencies": {
20
- "@kaban-board/core": "0.1.3",
20
+ "@clack/prompts": "^0.11.0",
21
+ "@kaban-board/core": "0.2.0",
21
22
  "@modelcontextprotocol/sdk": "^1.25.2",
22
- "commander": "^12.0.0"
23
+ "chalk": "^5.6.2",
24
+ "commander": "^12.0.0",
25
+ "zod": "^4.3.5"
23
26
  },
24
27
  "devDependencies": {
25
28
  "@types/bun": "latest",