@kaban-board/cli 0.1.2 → 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,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command10 } from "commander";
4
+ import { createRequire } from "node:module";
5
+ import { Command as Command12 } from "commander";
5
6
 
6
7
  // src/commands/add.ts
7
8
  import { KabanError } from "@kaban-board/core";
@@ -127,19 +128,575 @@ var doneCommand = new Command2("done").description("Mark a task as done").argume
127
128
  }
128
129
  });
129
130
 
130
- // src/commands/init.ts
131
- import { existsSync as existsSync2, mkdirSync, writeFileSync } from "node:fs";
132
- 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";
133
139
  import { Command as Command3 } from "commander";
134
- 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) => {
135
692
  const { kabanDir, dbPath, configPath } = getKabanPaths();
136
- if (existsSync2(dbPath)) {
693
+ if (existsSync4(dbPath)) {
137
694
  console.error("Error: Board already exists in this directory");
138
695
  process.exit(1);
139
696
  }
140
697
  mkdirSync(kabanDir, { recursive: true });
141
698
  const config = {
142
- ...DEFAULT_CONFIG,
699
+ ...DEFAULT_CONFIG2,
143
700
  board: { name: options.name }
144
701
  };
145
702
  writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -154,7 +711,7 @@ var initCommand = new Command3("init").description("Initialize a new Kaban board
154
711
 
155
712
  // src/commands/list.ts
156
713
  import { KabanError as KabanError3 } from "@kaban-board/core";
157
- import { Command as Command4 } from "commander";
714
+ import { Command as Command5 } from "commander";
158
715
  function sortTasks(tasks, sortBy, reverse) {
159
716
  const sorted = [...tasks].sort((a, b) => {
160
717
  switch (sortBy) {
@@ -170,7 +727,7 @@ function sortTasks(tasks, sortBy, reverse) {
170
727
  });
171
728
  return reverse ? sorted.reverse() : sorted;
172
729
  }
173
- 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) => {
174
731
  const json = options.json;
175
732
  try {
176
733
  const { taskService, boardService } = getContext();
@@ -222,12 +779,12 @@ var listCommand = new Command4("list").description("List tasks").option("-c, --c
222
779
  });
223
780
 
224
781
  // src/commands/mcp.ts
225
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
226
- 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";
227
784
  import {
228
785
  BoardService as BoardService3,
229
786
  createDb as createDb3,
230
- DEFAULT_CONFIG as DEFAULT_CONFIG2,
787
+ DEFAULT_CONFIG as DEFAULT_CONFIG3,
231
788
  initializeSchema as initializeSchema2,
232
789
  TaskService as TaskService2
233
790
  } from "@kaban-board/core";
@@ -239,19 +796,19 @@ import {
239
796
  ListToolsRequestSchema,
240
797
  ReadResourceRequestSchema
241
798
  } from "@modelcontextprotocol/sdk/types.js";
242
- import { Command as Command5 } from "commander";
799
+ import { Command as Command6 } from "commander";
243
800
  function getKabanPaths2(basePath) {
244
801
  const base = basePath ?? process.cwd();
245
- const kabanDir = join2(base, ".kaban");
802
+ const kabanDir = join4(base, ".kaban");
246
803
  return {
247
804
  kabanDir,
248
- dbPath: join2(kabanDir, "board.db"),
249
- configPath: join2(kabanDir, "config.json")
805
+ dbPath: join4(kabanDir, "board.db"),
806
+ configPath: join4(kabanDir, "config.json")
250
807
  };
251
808
  }
252
809
  function createContext(basePath) {
253
810
  const { dbPath, configPath } = getKabanPaths2(basePath);
254
- if (!existsSync3(dbPath)) {
811
+ if (!existsSync5(dbPath)) {
255
812
  throw new Error("No board found. Run 'kaban init' first");
256
813
  }
257
814
  const db = createDb3(dbPath);
@@ -390,7 +947,7 @@ async function startMcpServer(workingDirectory) {
390
947
  const { name: boardName = "Kaban Board", path: basePath } = args ?? {};
391
948
  const targetPath = basePath ?? workingDirectory;
392
949
  const { kabanDir, dbPath, configPath } = getKabanPaths2(targetPath);
393
- if (existsSync3(dbPath)) {
950
+ if (existsSync5(dbPath)) {
394
951
  return {
395
952
  content: [
396
953
  {
@@ -403,7 +960,7 @@ async function startMcpServer(workingDirectory) {
403
960
  }
404
961
  mkdirSync2(kabanDir, { recursive: true });
405
962
  const config = {
406
- ...DEFAULT_CONFIG2,
963
+ ...DEFAULT_CONFIG3,
407
964
  board: { name: boardName }
408
965
  };
409
966
  writeFileSync2(configPath, JSON.stringify(config, null, 2));
@@ -617,15 +1174,15 @@ async function startMcpServer(workingDirectory) {
617
1174
  await server.connect(transport);
618
1175
  console.error("Kaban MCP server running on stdio");
619
1176
  }
620
- 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) => {
621
1178
  const workingDirectory = options.path ?? process.env.KABAN_PATH ?? process.cwd();
622
1179
  await startMcpServer(workingDirectory);
623
1180
  });
624
1181
 
625
1182
  // src/commands/move.ts
626
1183
  import { KabanError as KabanError4 } from "@kaban-board/core";
627
- import { Command as Command6 } from "commander";
628
- 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) => {
629
1186
  const json = options.json;
630
1187
  try {
631
1188
  const { taskService, boardService } = getContext();
@@ -678,9 +1235,9 @@ var moveCommand = new Command6("move").description("Move a task to a different c
678
1235
 
679
1236
  // src/commands/schema.ts
680
1237
  import { jsonSchemas } from "@kaban-board/core";
681
- import { Command as Command7 } from "commander";
1238
+ import { Command as Command8 } from "commander";
682
1239
  var availableSchemas = Object.keys(jsonSchemas);
683
- 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) => {
684
1241
  if (!name) {
685
1242
  console.log("Available schemas:");
686
1243
  for (const schemaName of availableSchemas) {
@@ -701,8 +1258,8 @@ Usage: kaban schema <name>`);
701
1258
 
702
1259
  // src/commands/status.ts
703
1260
  import { KabanError as KabanError5 } from "@kaban-board/core";
704
- import { Command as Command8 } from "commander";
705
- 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) => {
706
1263
  const json = options.json;
707
1264
  try {
708
1265
  const { taskService, boardService } = getContext();
@@ -755,15 +1312,398 @@ var statusCommand = new Command8("status").description("Show board status summar
755
1312
  }
756
1313
  });
757
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
+
758
1698
  // src/commands/tui.ts
759
- import { spawn } from "node:child_process";
760
- 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";
761
1701
  import { fileURLToPath } from "node:url";
762
- import { Command as Command9 } from "commander";
763
- var __dirname2 = dirname(fileURLToPath(import.meta.url));
764
- var tuiCommand = new Command9("tui").description("Start interactive Terminal UI").action(async () => {
765
- const tuiEntry = join3(__dirname2, "../../../tui/src/index.ts");
766
- 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], {
767
1707
  stdio: "inherit",
768
1708
  cwd: process.cwd()
769
1709
  });
@@ -773,15 +1713,19 @@ var tuiCommand = new Command9("tui").description("Start interactive Terminal UI"
773
1713
  });
774
1714
 
775
1715
  // src/index.ts
776
- var program = new Command10;
777
- program.name("kaban").description("Terminal Kanban for AI Code Agents").version("0.1.0");
1716
+ var require2 = createRequire(import.meta.url);
1717
+ var pkg = require2("../package.json");
1718
+ var program = new Command12;
1719
+ program.name("kaban").description("Terminal Kanban for AI Code Agents").version(pkg.version);
778
1720
  program.addCommand(initCommand);
779
1721
  program.addCommand(addCommand);
780
1722
  program.addCommand(listCommand);
781
1723
  program.addCommand(moveCommand);
782
1724
  program.addCommand(doneCommand);
783
- program.addCommand(statusCommand);
1725
+ program.addCommand(statusCommand2);
784
1726
  program.addCommand(schemaCommand);
785
1727
  program.addCommand(mcpCommand);
786
1728
  program.addCommand(tuiCommand);
1729
+ program.addCommand(hookCommand);
1730
+ program.addCommand(syncCommand);
787
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.2",
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.0",
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",
@@ -38,6 +41,7 @@
38
41
  "url": "https://github.com/beshkenadze/kaban/issues"
39
42
  },
40
43
  "license": "MIT",
44
+ "author": "Aleksandr Beshkenadze <beshkenadze@gmail.com>",
41
45
  "keywords": [
42
46
  "kanban",
43
47
  "ai",