@kaban-board/cli 0.1.3 → 0.2.4
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 +1038 -51
- package/dist/kaban-hook +14 -0
- package/package.json +6 -3
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";
|
|
@@ -12,7 +12,7 @@ import { Command } from "commander";
|
|
|
12
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { BoardService, createDb, TaskService } from "@kaban-board/core";
|
|
15
|
-
function getContext() {
|
|
15
|
+
async function getContext() {
|
|
16
16
|
const kabanDir = join(process.cwd(), ".kaban");
|
|
17
17
|
const dbPath = join(kabanDir, "board.db");
|
|
18
18
|
const configPath = join(kabanDir, "config.json");
|
|
@@ -20,7 +20,7 @@ function getContext() {
|
|
|
20
20
|
console.error("Error: No board found. Run 'kaban init' first");
|
|
21
21
|
process.exit(1);
|
|
22
22
|
}
|
|
23
|
-
const db = createDb(dbPath);
|
|
23
|
+
const db = await createDb(dbPath);
|
|
24
24
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
25
25
|
const boardService = new BoardService(db);
|
|
26
26
|
const taskService = new TaskService(db, boardService);
|
|
@@ -60,7 +60,7 @@ function outputError(code, message) {
|
|
|
60
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
61
|
const json = options.json;
|
|
62
62
|
try {
|
|
63
|
-
const { taskService, config } = getContext();
|
|
63
|
+
const { taskService, config } = await getContext();
|
|
64
64
|
const agent = options.agent ?? getAgent();
|
|
65
65
|
const columnId = options.column ?? config.defaults.column;
|
|
66
66
|
const dependsOn = options.dependsOn ? options.dependsOn.split(",").map((s) => s.trim()) : [];
|
|
@@ -95,7 +95,7 @@ import { Command as Command2 } from "commander";
|
|
|
95
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) => {
|
|
96
96
|
const json = options.json;
|
|
97
97
|
try {
|
|
98
|
-
const { taskService, boardService } = getContext();
|
|
98
|
+
const { taskService, boardService } = await getContext();
|
|
99
99
|
const tasks = await taskService.listTasks();
|
|
100
100
|
const task = tasks.find((t) => t.id.startsWith(id));
|
|
101
101
|
if (!task) {
|
|
@@ -128,23 +128,584 @@ var doneCommand = new Command2("done").description("Mark a task as done").argume
|
|
|
128
128
|
}
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
// src/commands/
|
|
132
|
-
import {
|
|
133
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
690
|
+
BoardService as BoardService2,
|
|
691
|
+
createDb as createDb2,
|
|
692
|
+
DEFAULT_CONFIG as DEFAULT_CONFIG2,
|
|
693
|
+
initializeSchema
|
|
694
|
+
} 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) => {
|
|
136
697
|
const { kabanDir, dbPath, configPath } = getKabanPaths();
|
|
137
|
-
if (
|
|
698
|
+
if (existsSync4(dbPath)) {
|
|
138
699
|
console.error("Error: Board already exists in this directory");
|
|
139
700
|
process.exit(1);
|
|
140
701
|
}
|
|
141
702
|
mkdirSync(kabanDir, { recursive: true });
|
|
142
703
|
const config = {
|
|
143
|
-
...
|
|
704
|
+
...DEFAULT_CONFIG2,
|
|
144
705
|
board: { name: options.name }
|
|
145
706
|
};
|
|
146
707
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
147
|
-
const db = createDb2(dbPath);
|
|
708
|
+
const db = await createDb2(dbPath);
|
|
148
709
|
await initializeSchema(db);
|
|
149
710
|
const boardService = new BoardService2(db);
|
|
150
711
|
await boardService.initializeBoard(config);
|
|
@@ -155,7 +716,7 @@ var initCommand = new Command3("init").description("Initialize a new Kaban board
|
|
|
155
716
|
|
|
156
717
|
// src/commands/list.ts
|
|
157
718
|
import { KabanError as KabanError3 } from "@kaban-board/core";
|
|
158
|
-
import { Command as
|
|
719
|
+
import { Command as Command5 } from "commander";
|
|
159
720
|
function sortTasks(tasks, sortBy, reverse) {
|
|
160
721
|
const sorted = [...tasks].sort((a, b) => {
|
|
161
722
|
switch (sortBy) {
|
|
@@ -171,10 +732,10 @@ function sortTasks(tasks, sortBy, reverse) {
|
|
|
171
732
|
});
|
|
172
733
|
return reverse ? sorted.reverse() : sorted;
|
|
173
734
|
}
|
|
174
|
-
var listCommand = new
|
|
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) => {
|
|
175
736
|
const json = options.json;
|
|
176
737
|
try {
|
|
177
|
-
const { taskService, boardService } = getContext();
|
|
738
|
+
const { taskService, boardService } = await getContext();
|
|
178
739
|
let tasks = await taskService.listTasks({
|
|
179
740
|
columnId: options.column,
|
|
180
741
|
agent: options.agent,
|
|
@@ -223,12 +784,12 @@ var listCommand = new Command4("list").description("List tasks").option("-c, --c
|
|
|
223
784
|
});
|
|
224
785
|
|
|
225
786
|
// src/commands/mcp.ts
|
|
226
|
-
import { existsSync as
|
|
227
|
-
import { join as
|
|
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";
|
|
228
789
|
import {
|
|
229
790
|
BoardService as BoardService3,
|
|
230
791
|
createDb as createDb3,
|
|
231
|
-
DEFAULT_CONFIG as
|
|
792
|
+
DEFAULT_CONFIG as DEFAULT_CONFIG3,
|
|
232
793
|
initializeSchema as initializeSchema2,
|
|
233
794
|
TaskService as TaskService2
|
|
234
795
|
} from "@kaban-board/core";
|
|
@@ -240,22 +801,22 @@ import {
|
|
|
240
801
|
ListToolsRequestSchema,
|
|
241
802
|
ReadResourceRequestSchema
|
|
242
803
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
243
|
-
import { Command as
|
|
804
|
+
import { Command as Command6 } from "commander";
|
|
244
805
|
function getKabanPaths2(basePath) {
|
|
245
806
|
const base = basePath ?? process.cwd();
|
|
246
|
-
const kabanDir =
|
|
807
|
+
const kabanDir = join4(base, ".kaban");
|
|
247
808
|
return {
|
|
248
809
|
kabanDir,
|
|
249
|
-
dbPath:
|
|
250
|
-
configPath:
|
|
810
|
+
dbPath: join4(kabanDir, "board.db"),
|
|
811
|
+
configPath: join4(kabanDir, "config.json")
|
|
251
812
|
};
|
|
252
813
|
}
|
|
253
|
-
function createContext(basePath) {
|
|
814
|
+
async function createContext(basePath) {
|
|
254
815
|
const { dbPath, configPath } = getKabanPaths2(basePath);
|
|
255
|
-
if (!
|
|
816
|
+
if (!existsSync5(dbPath)) {
|
|
256
817
|
throw new Error("No board found. Run 'kaban init' first");
|
|
257
818
|
}
|
|
258
|
-
const db = createDb3(dbPath);
|
|
819
|
+
const db = await createDb3(dbPath);
|
|
259
820
|
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
260
821
|
const boardService = new BoardService3(db);
|
|
261
822
|
const taskService = new TaskService2(db, boardService);
|
|
@@ -391,7 +952,7 @@ async function startMcpServer(workingDirectory) {
|
|
|
391
952
|
const { name: boardName = "Kaban Board", path: basePath } = args ?? {};
|
|
392
953
|
const targetPath = basePath ?? workingDirectory;
|
|
393
954
|
const { kabanDir, dbPath, configPath } = getKabanPaths2(targetPath);
|
|
394
|
-
if (
|
|
955
|
+
if (existsSync5(dbPath)) {
|
|
395
956
|
return {
|
|
396
957
|
content: [
|
|
397
958
|
{
|
|
@@ -404,11 +965,11 @@ async function startMcpServer(workingDirectory) {
|
|
|
404
965
|
}
|
|
405
966
|
mkdirSync2(kabanDir, { recursive: true });
|
|
406
967
|
const config = {
|
|
407
|
-
...
|
|
968
|
+
...DEFAULT_CONFIG3,
|
|
408
969
|
board: { name: boardName }
|
|
409
970
|
};
|
|
410
971
|
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
411
|
-
const db = createDb3(dbPath);
|
|
972
|
+
const db = await createDb3(dbPath);
|
|
412
973
|
await initializeSchema2(db);
|
|
413
974
|
const boardService2 = new BoardService3(db);
|
|
414
975
|
await boardService2.initializeBoard(config);
|
|
@@ -425,7 +986,7 @@ async function startMcpServer(workingDirectory) {
|
|
|
425
986
|
]
|
|
426
987
|
};
|
|
427
988
|
}
|
|
428
|
-
const { taskService, boardService } = createContext(workingDirectory);
|
|
989
|
+
const { taskService, boardService } = await createContext(workingDirectory);
|
|
429
990
|
switch (name) {
|
|
430
991
|
case "kaban_add_task": {
|
|
431
992
|
const task = await taskService.addTask(args);
|
|
@@ -535,7 +1096,7 @@ async function startMcpServer(workingDirectory) {
|
|
|
535
1096
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
536
1097
|
const { uri } = request.params;
|
|
537
1098
|
try {
|
|
538
|
-
const { taskService, boardService } = createContext(workingDirectory);
|
|
1099
|
+
const { taskService, boardService } = await createContext(workingDirectory);
|
|
539
1100
|
if (uri === "kaban://board/status") {
|
|
540
1101
|
const board = await boardService.getBoard();
|
|
541
1102
|
const columns = await boardService.getColumns();
|
|
@@ -618,18 +1179,18 @@ async function startMcpServer(workingDirectory) {
|
|
|
618
1179
|
await server.connect(transport);
|
|
619
1180
|
console.error("Kaban MCP server running on stdio");
|
|
620
1181
|
}
|
|
621
|
-
var mcpCommand = new
|
|
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) => {
|
|
622
1183
|
const workingDirectory = options.path ?? process.env.KABAN_PATH ?? process.cwd();
|
|
623
1184
|
await startMcpServer(workingDirectory);
|
|
624
1185
|
});
|
|
625
1186
|
|
|
626
1187
|
// src/commands/move.ts
|
|
627
1188
|
import { KabanError as KabanError4 } from "@kaban-board/core";
|
|
628
|
-
import { Command as
|
|
629
|
-
var moveCommand = new
|
|
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) => {
|
|
630
1191
|
const json = options.json;
|
|
631
1192
|
try {
|
|
632
|
-
const { taskService, boardService } = getContext();
|
|
1193
|
+
const { taskService, boardService } = await getContext();
|
|
633
1194
|
const tasks = await taskService.listTasks();
|
|
634
1195
|
const task = tasks.find((t) => t.id.startsWith(id));
|
|
635
1196
|
if (!task) {
|
|
@@ -679,9 +1240,9 @@ var moveCommand = new Command6("move").description("Move a task to a different c
|
|
|
679
1240
|
|
|
680
1241
|
// src/commands/schema.ts
|
|
681
1242
|
import { jsonSchemas } from "@kaban-board/core";
|
|
682
|
-
import { Command as
|
|
1243
|
+
import { Command as Command8 } from "commander";
|
|
683
1244
|
var availableSchemas = Object.keys(jsonSchemas);
|
|
684
|
-
var schemaCommand = new
|
|
1245
|
+
var schemaCommand = new Command8("schema").description("Output JSON schemas for AI agents").argument("[name]", "Schema name (omit to list available)").action((name) => {
|
|
685
1246
|
if (!name) {
|
|
686
1247
|
console.log("Available schemas:");
|
|
687
1248
|
for (const schemaName of availableSchemas) {
|
|
@@ -702,11 +1263,11 @@ Usage: kaban schema <name>`);
|
|
|
702
1263
|
|
|
703
1264
|
// src/commands/status.ts
|
|
704
1265
|
import { KabanError as KabanError5 } from "@kaban-board/core";
|
|
705
|
-
import { Command as
|
|
706
|
-
var
|
|
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) => {
|
|
707
1268
|
const json = options.json;
|
|
708
1269
|
try {
|
|
709
|
-
const { taskService, boardService } = getContext();
|
|
1270
|
+
const { taskService, boardService } = await getContext();
|
|
710
1271
|
const board = await boardService.getBoard();
|
|
711
1272
|
const columns = await boardService.getColumns();
|
|
712
1273
|
const tasks = await taskService.listTasks();
|
|
@@ -756,35 +1317,461 @@ var statusCommand = new Command8("status").description("Show board status summar
|
|
|
756
1317
|
}
|
|
757
1318
|
});
|
|
758
1319
|
|
|
1320
|
+
// src/commands/sync.ts
|
|
1321
|
+
import { Command as Command10 } from "commander";
|
|
1322
|
+
|
|
1323
|
+
// src/hook/constants.ts
|
|
1324
|
+
var STATUS_PRIORITY = {
|
|
1325
|
+
completed: 3,
|
|
1326
|
+
in_progress: 2,
|
|
1327
|
+
pending: 1,
|
|
1328
|
+
cancelled: 0
|
|
1329
|
+
};
|
|
1330
|
+
var STATUS_TO_COLUMN = {
|
|
1331
|
+
pending: "todo",
|
|
1332
|
+
in_progress: "in_progress",
|
|
1333
|
+
completed: "done",
|
|
1334
|
+
cancelled: "backlog"
|
|
1335
|
+
};
|
|
1336
|
+
var COLUMN_TO_STATUS = {
|
|
1337
|
+
backlog: "pending",
|
|
1338
|
+
todo: "pending",
|
|
1339
|
+
in_progress: "in_progress",
|
|
1340
|
+
review: "in_progress",
|
|
1341
|
+
done: "completed"
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
// src/hook/conflict-resolver.ts
|
|
1345
|
+
class ConflictResolver {
|
|
1346
|
+
strategy;
|
|
1347
|
+
constructor(strategy) {
|
|
1348
|
+
this.strategy = strategy;
|
|
1349
|
+
}
|
|
1350
|
+
resolve(todo, kabanTask) {
|
|
1351
|
+
const kabanStatus = this.columnToStatus(kabanTask.columnId);
|
|
1352
|
+
const todoColumn = STATUS_TO_COLUMN[todo.status];
|
|
1353
|
+
if (this.strategy === "todowrite_wins") {
|
|
1354
|
+
return {
|
|
1355
|
+
winner: "todo",
|
|
1356
|
+
targetColumn: todoColumn,
|
|
1357
|
+
reason: "todowrite_wins strategy"
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
if (this.strategy === "kaban_wins") {
|
|
1361
|
+
return {
|
|
1362
|
+
winner: "kaban",
|
|
1363
|
+
targetColumn: kabanTask.columnId,
|
|
1364
|
+
reason: "kaban_wins strategy"
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
if (todo.status === "completed") {
|
|
1368
|
+
return {
|
|
1369
|
+
winner: "todo",
|
|
1370
|
+
targetColumn: "done",
|
|
1371
|
+
reason: "completed status always wins (terminal state)"
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
if (kabanTask.columnId === "done") {
|
|
1375
|
+
return {
|
|
1376
|
+
winner: "kaban",
|
|
1377
|
+
targetColumn: "done",
|
|
1378
|
+
reason: "kaban task already completed (terminal state)"
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
const todoPriority = STATUS_PRIORITY[todo.status];
|
|
1382
|
+
const kabanPriority = STATUS_PRIORITY[kabanStatus];
|
|
1383
|
+
if (todoPriority > kabanPriority) {
|
|
1384
|
+
return {
|
|
1385
|
+
winner: "todo",
|
|
1386
|
+
targetColumn: todoColumn,
|
|
1387
|
+
reason: `todo status priority (${todoPriority}) > kaban (${kabanPriority})`
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
if (kabanPriority > todoPriority) {
|
|
1391
|
+
return {
|
|
1392
|
+
winner: "kaban",
|
|
1393
|
+
targetColumn: kabanTask.columnId,
|
|
1394
|
+
reason: `kaban status priority (${kabanPriority}) > todo (${todoPriority})`
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
return {
|
|
1398
|
+
winner: "todo",
|
|
1399
|
+
targetColumn: todoColumn,
|
|
1400
|
+
reason: "equal priority, todo wins (most recent)"
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
shouldSync(todo, cancelledPolicy) {
|
|
1404
|
+
if (todo.status === "cancelled") {
|
|
1405
|
+
return cancelledPolicy === "backlog";
|
|
1406
|
+
}
|
|
1407
|
+
return true;
|
|
1408
|
+
}
|
|
1409
|
+
columnToStatus(columnId) {
|
|
1410
|
+
return COLUMN_TO_STATUS[columnId] ?? "pending";
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/hook/kaban-client.ts
|
|
1415
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1416
|
+
|
|
1417
|
+
class KabanClient {
|
|
1418
|
+
cwd;
|
|
1419
|
+
constructor(cwd) {
|
|
1420
|
+
this.cwd = cwd;
|
|
1421
|
+
}
|
|
1422
|
+
async boardExists() {
|
|
1423
|
+
try {
|
|
1424
|
+
const result = await this.exec(["kaban", "status", "--json"]);
|
|
1425
|
+
return result.exitCode === 0;
|
|
1426
|
+
} catch {
|
|
1427
|
+
return false;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
async listTasks(columnId) {
|
|
1431
|
+
const args = ["kaban", "list", "--json"];
|
|
1432
|
+
if (columnId) {
|
|
1433
|
+
args.push("--column", columnId);
|
|
1434
|
+
}
|
|
1435
|
+
const result = await this.exec(args);
|
|
1436
|
+
if (result.exitCode !== 0) {
|
|
1437
|
+
return [];
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
const tasks = JSON.parse(result.stdout);
|
|
1441
|
+
return tasks.map((t) => ({
|
|
1442
|
+
id: t.id,
|
|
1443
|
+
title: t.title,
|
|
1444
|
+
columnId: t.columnId,
|
|
1445
|
+
description: t.description,
|
|
1446
|
+
labels: t.labels
|
|
1447
|
+
}));
|
|
1448
|
+
} catch {
|
|
1449
|
+
return [];
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
async getTaskById(id) {
|
|
1453
|
+
const result = await this.exec(["kaban", "get", id, "--json"]);
|
|
1454
|
+
if (result.exitCode !== 0) {
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
try {
|
|
1458
|
+
const task = JSON.parse(result.stdout);
|
|
1459
|
+
return {
|
|
1460
|
+
id: task.id,
|
|
1461
|
+
title: task.title,
|
|
1462
|
+
columnId: task.columnId,
|
|
1463
|
+
description: task.description,
|
|
1464
|
+
labels: task.labels
|
|
1465
|
+
};
|
|
1466
|
+
} catch {
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
async findTaskByTitle(title) {
|
|
1471
|
+
const tasks = await this.listTasks();
|
|
1472
|
+
return tasks.find((t) => t.title === title) ?? null;
|
|
1473
|
+
}
|
|
1474
|
+
async addTask(title, columnId = "todo") {
|
|
1475
|
+
const result = await this.exec(["kaban", "add", title, "--column", columnId, "--json"]);
|
|
1476
|
+
if (result.exitCode !== 0) {
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1479
|
+
try {
|
|
1480
|
+
const response = JSON.parse(result.stdout);
|
|
1481
|
+
return response.data?.id ?? response.id ?? null;
|
|
1482
|
+
} catch {
|
|
1483
|
+
const match = result.stdout.match(/id[":]*\s*["']?([A-Z0-9]+)/i);
|
|
1484
|
+
return match?.[1] ?? null;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
async moveTask(id, columnId) {
|
|
1488
|
+
const result = await this.exec(["kaban", "move", id, "--column", columnId]);
|
|
1489
|
+
return result.exitCode === 0;
|
|
1490
|
+
}
|
|
1491
|
+
async completeTask(id) {
|
|
1492
|
+
const result = await this.exec(["kaban", "done", id]);
|
|
1493
|
+
return result.exitCode === 0;
|
|
1494
|
+
}
|
|
1495
|
+
async getStatus() {
|
|
1496
|
+
const result = await this.exec(["kaban", "status", "--json"]);
|
|
1497
|
+
if (result.exitCode !== 0) {
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
return JSON.parse(result.stdout);
|
|
1502
|
+
} catch {
|
|
1503
|
+
return null;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
exec(args) {
|
|
1507
|
+
return new Promise((resolve2) => {
|
|
1508
|
+
const [cmd, ...cmdArgs] = args;
|
|
1509
|
+
const proc = spawn2(cmd, cmdArgs, { cwd: this.cwd });
|
|
1510
|
+
let stdout = "";
|
|
1511
|
+
let stderr = "";
|
|
1512
|
+
proc.stdout.on("data", (data) => {
|
|
1513
|
+
stdout += data.toString();
|
|
1514
|
+
});
|
|
1515
|
+
proc.stderr.on("data", (data) => {
|
|
1516
|
+
stderr += data.toString();
|
|
1517
|
+
});
|
|
1518
|
+
proc.on("close", (code) => {
|
|
1519
|
+
resolve2({ exitCode: code ?? 1, stdout, stderr });
|
|
1520
|
+
});
|
|
1521
|
+
proc.on("error", () => {
|
|
1522
|
+
resolve2({ exitCode: 1, stdout: "", stderr: "spawn error" });
|
|
1523
|
+
});
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// src/hook/sync-engine.ts
|
|
1529
|
+
class SyncEngine {
|
|
1530
|
+
kaban;
|
|
1531
|
+
resolver;
|
|
1532
|
+
config;
|
|
1533
|
+
constructor(cwd, config) {
|
|
1534
|
+
this.kaban = new KabanClient(cwd);
|
|
1535
|
+
this.resolver = new ConflictResolver(config.conflictStrategy);
|
|
1536
|
+
this.config = config;
|
|
1537
|
+
}
|
|
1538
|
+
async sync(todos) {
|
|
1539
|
+
const result = {
|
|
1540
|
+
success: true,
|
|
1541
|
+
created: 0,
|
|
1542
|
+
moved: 0,
|
|
1543
|
+
skipped: 0,
|
|
1544
|
+
errors: []
|
|
1545
|
+
};
|
|
1546
|
+
if (!await this.kaban.boardExists()) {
|
|
1547
|
+
result.skipped = todos.length;
|
|
1548
|
+
return result;
|
|
1549
|
+
}
|
|
1550
|
+
if (todos.length === 0) {
|
|
1551
|
+
return result;
|
|
1552
|
+
}
|
|
1553
|
+
const kabanTasks = await this.kaban.listTasks();
|
|
1554
|
+
const tasksByTitle = new Map(kabanTasks.map((t) => [t.title, t]));
|
|
1555
|
+
const tasksById = new Map(kabanTasks.map((t) => [t.id, t]));
|
|
1556
|
+
for (const todo of todos) {
|
|
1557
|
+
if (!this.resolver.shouldSync(todo, this.config.cancelledPolicy)) {
|
|
1558
|
+
result.skipped++;
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
try {
|
|
1562
|
+
const syncResult = await this.syncTodo(todo, tasksByTitle, tasksById);
|
|
1563
|
+
if (syncResult === "created")
|
|
1564
|
+
result.created++;
|
|
1565
|
+
else if (syncResult === "moved")
|
|
1566
|
+
result.moved++;
|
|
1567
|
+
else
|
|
1568
|
+
result.skipped++;
|
|
1569
|
+
} catch (error2) {
|
|
1570
|
+
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
1571
|
+
result.errors.push(`${this.truncateTitle(todo.content)}: ${errorMsg}`);
|
|
1572
|
+
result.success = false;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return result;
|
|
1576
|
+
}
|
|
1577
|
+
async syncTodo(todo, tasksByTitle, tasksById) {
|
|
1578
|
+
const normalizedTitle = this.truncateTitle(todo.content);
|
|
1579
|
+
const existing = tasksById.get(todo.id) ?? tasksByTitle.get(normalizedTitle) ?? tasksByTitle.get(todo.content);
|
|
1580
|
+
if (existing) {
|
|
1581
|
+
return this.handleExisting(todo, existing);
|
|
1582
|
+
}
|
|
1583
|
+
return this.handleNew(todo);
|
|
1584
|
+
}
|
|
1585
|
+
async handleExisting(todo, existing) {
|
|
1586
|
+
const resolution = this.resolver.resolve(todo, existing);
|
|
1587
|
+
if (resolution.winner === "kaban" || existing.columnId === resolution.targetColumn) {
|
|
1588
|
+
return "skipped";
|
|
1589
|
+
}
|
|
1590
|
+
if (resolution.targetColumn === "done") {
|
|
1591
|
+
const success2 = await this.kaban.completeTask(existing.id);
|
|
1592
|
+
if (!success2)
|
|
1593
|
+
throw new Error("failed to complete task");
|
|
1594
|
+
} else {
|
|
1595
|
+
const success2 = await this.kaban.moveTask(existing.id, resolution.targetColumn);
|
|
1596
|
+
if (!success2)
|
|
1597
|
+
throw new Error(`failed to move task to ${resolution.targetColumn}`);
|
|
1598
|
+
}
|
|
1599
|
+
return "moved";
|
|
1600
|
+
}
|
|
1601
|
+
async handleNew(todo) {
|
|
1602
|
+
const column = todo.status === "cancelled" ? "backlog" : STATUS_TO_COLUMN[todo.status];
|
|
1603
|
+
const title = this.truncateTitle(todo.content);
|
|
1604
|
+
const taskId = await this.kaban.addTask(title, column);
|
|
1605
|
+
if (!taskId) {
|
|
1606
|
+
throw new Error("failed to create task");
|
|
1607
|
+
}
|
|
1608
|
+
return "created";
|
|
1609
|
+
}
|
|
1610
|
+
truncateTitle(title) {
|
|
1611
|
+
if (title.length <= this.config.maxTitleLength)
|
|
1612
|
+
return title;
|
|
1613
|
+
return `${title.slice(0, this.config.maxTitleLength - 3)}...`;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// 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";
|
|
1622
|
+
|
|
1623
|
+
class SyncLogger {
|
|
1624
|
+
logPath;
|
|
1625
|
+
constructor(logPath) {
|
|
1626
|
+
this.logPath = this.expandPath(logPath);
|
|
1627
|
+
}
|
|
1628
|
+
async log(todosCount, result, durationMs) {
|
|
1629
|
+
const entry = {
|
|
1630
|
+
timestamp: new Date().toISOString(),
|
|
1631
|
+
todosCount,
|
|
1632
|
+
created: result.created,
|
|
1633
|
+
moved: result.moved,
|
|
1634
|
+
skipped: result.skipped,
|
|
1635
|
+
errors: result.errors,
|
|
1636
|
+
durationMs
|
|
1637
|
+
};
|
|
1638
|
+
await this.ensureDir();
|
|
1639
|
+
await appendFile(this.logPath, `${JSON.stringify(entry)}
|
|
1640
|
+
`);
|
|
1641
|
+
}
|
|
1642
|
+
async ensureDir() {
|
|
1643
|
+
const dir = dirname2(this.logPath);
|
|
1644
|
+
if (!existsSync6(dir)) {
|
|
1645
|
+
await mkdir2(dir, { recursive: true });
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
expandPath(path) {
|
|
1649
|
+
if (path.startsWith("~/")) {
|
|
1650
|
+
return path.replace("~", homedir3());
|
|
1651
|
+
}
|
|
1652
|
+
return path;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// 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) => {
|
|
1658
|
+
const startTime = performance.now();
|
|
1659
|
+
let input;
|
|
1660
|
+
try {
|
|
1661
|
+
const chunks = [];
|
|
1662
|
+
for await (const chunk of process.stdin) {
|
|
1663
|
+
chunks.push(chunk);
|
|
1664
|
+
}
|
|
1665
|
+
input = Buffer.concat(chunks).toString("utf-8");
|
|
1666
|
+
} catch {
|
|
1667
|
+
process.exit(0);
|
|
1668
|
+
}
|
|
1669
|
+
if (!input.trim()) {
|
|
1670
|
+
process.exit(0);
|
|
1671
|
+
}
|
|
1672
|
+
let parsed;
|
|
1673
|
+
try {
|
|
1674
|
+
parsed = JSON.parse(input);
|
|
1675
|
+
} catch {
|
|
1676
|
+
process.exit(0);
|
|
1677
|
+
}
|
|
1678
|
+
const validation = TodoWriteHookInputSchema.safeParse(parsed);
|
|
1679
|
+
if (!validation.success) {
|
|
1680
|
+
process.exit(0);
|
|
1681
|
+
}
|
|
1682
|
+
const hookInput = validation.data;
|
|
1683
|
+
const { cwd, tool_input } = hookInput;
|
|
1684
|
+
const { todos } = tool_input;
|
|
1685
|
+
if (todos.length === 0) {
|
|
1686
|
+
process.exit(0);
|
|
1687
|
+
}
|
|
1688
|
+
const config = { ...DEFAULT_CONFIG, logEnabled: options.log !== false };
|
|
1689
|
+
const engine = new SyncEngine(cwd, config);
|
|
1690
|
+
const logger = new SyncLogger(config.logPath);
|
|
1691
|
+
try {
|
|
1692
|
+
const result = await engine.sync(todos);
|
|
1693
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
1694
|
+
if (config.logEnabled) {
|
|
1695
|
+
await logger.log(todos.length, result, durationMs);
|
|
1696
|
+
}
|
|
1697
|
+
process.exit(result.success ? 0 : 1);
|
|
1698
|
+
} catch {
|
|
1699
|
+
process.exit(1);
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
|
|
759
1703
|
// src/commands/tui.ts
|
|
760
|
-
import { spawn } from "node:child_process";
|
|
761
|
-
import {
|
|
762
|
-
import {
|
|
763
|
-
import { Command as
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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";
|
|
1708
|
+
function findInPath(name) {
|
|
1709
|
+
const result = spawnSync("which", [name], { encoding: "utf-8" });
|
|
1710
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
1711
|
+
}
|
|
1712
|
+
function runBinary(path, args) {
|
|
1713
|
+
const child = spawn3(path, args, { stdio: "inherit", cwd: process.cwd() });
|
|
1714
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
1715
|
+
}
|
|
1716
|
+
function runBunx(bunPath, args) {
|
|
1717
|
+
let started = false;
|
|
1718
|
+
const child = spawn3(bunPath, ["x", "@kaban-board/tui", ...args], {
|
|
768
1719
|
stdio: "inherit",
|
|
769
1720
|
cwd: process.cwd()
|
|
770
1721
|
});
|
|
771
|
-
child.on("
|
|
772
|
-
|
|
1722
|
+
child.on("spawn", () => {
|
|
1723
|
+
started = true;
|
|
773
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 () => {
|
|
1748
|
+
const args = process.argv.slice(3);
|
|
1749
|
+
const siblingBinary = join5(dirname3(process.execPath), "kaban-tui");
|
|
1750
|
+
if (existsSync7(siblingBinary))
|
|
1751
|
+
return runBinary(siblingBinary, args);
|
|
1752
|
+
const pathBinary = findInPath("kaban-tui");
|
|
1753
|
+
if (pathBinary)
|
|
1754
|
+
return runBinary(pathBinary, args);
|
|
1755
|
+
const bunPath = findInPath("bun");
|
|
1756
|
+
if (bunPath)
|
|
1757
|
+
return runBunx(bunPath, args);
|
|
1758
|
+
showInstallError();
|
|
774
1759
|
});
|
|
775
1760
|
|
|
776
1761
|
// src/index.ts
|
|
777
1762
|
var require2 = createRequire(import.meta.url);
|
|
778
1763
|
var pkg = require2("../package.json");
|
|
779
|
-
var program = new
|
|
1764
|
+
var program = new Command12;
|
|
780
1765
|
program.name("kaban").description("Terminal Kanban for AI Code Agents").version(pkg.version);
|
|
781
1766
|
program.addCommand(initCommand);
|
|
782
1767
|
program.addCommand(addCommand);
|
|
783
1768
|
program.addCommand(listCommand);
|
|
784
1769
|
program.addCommand(moveCommand);
|
|
785
1770
|
program.addCommand(doneCommand);
|
|
786
|
-
program.addCommand(
|
|
1771
|
+
program.addCommand(statusCommand2);
|
|
787
1772
|
program.addCommand(schemaCommand);
|
|
788
1773
|
program.addCommand(mcpCommand);
|
|
789
1774
|
program.addCommand(tuiCommand);
|
|
1775
|
+
program.addCommand(hookCommand);
|
|
1776
|
+
program.addCommand(syncCommand);
|
|
790
1777
|
program.parse();
|
package/dist/kaban-hook
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.2.4",
|
|
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
|
+
"@clack/prompts": "^0.11.0",
|
|
20
21
|
"@kaban-board/core": "0.1.3",
|
|
21
22
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
22
|
-
"
|
|
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",
|