@kaban-board/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +787 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command10 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/add.ts
|
|
7
|
+
import { KabanError } from "@kaban-board/core";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
|
|
10
|
+
// src/lib/context.ts
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { BoardService, createDb, TaskService } from "@kaban-board/core";
|
|
14
|
+
function getContext() {
|
|
15
|
+
const kabanDir = join(process.cwd(), ".kaban");
|
|
16
|
+
const dbPath = join(kabanDir, "board.db");
|
|
17
|
+
const configPath = join(kabanDir, "config.json");
|
|
18
|
+
if (!existsSync(dbPath)) {
|
|
19
|
+
console.error("Error: No board found. Run 'kaban init' first");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const db = createDb(dbPath);
|
|
23
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
24
|
+
const boardService = new BoardService(db);
|
|
25
|
+
const taskService = new TaskService(db, boardService);
|
|
26
|
+
return { db, config, boardService, taskService };
|
|
27
|
+
}
|
|
28
|
+
function getKabanPaths() {
|
|
29
|
+
const kabanDir = join(process.cwd(), ".kaban");
|
|
30
|
+
return {
|
|
31
|
+
kabanDir,
|
|
32
|
+
dbPath: join(kabanDir, "board.db"),
|
|
33
|
+
configPath: join(kabanDir, "config.json")
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function getAgent() {
|
|
37
|
+
return process.env.KABAN_AGENT ?? "user";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/lib/json-output.ts
|
|
41
|
+
function success(data) {
|
|
42
|
+
return { success: true, data };
|
|
43
|
+
}
|
|
44
|
+
function error(code, message) {
|
|
45
|
+
return { success: false, error: { code, message } };
|
|
46
|
+
}
|
|
47
|
+
function output(response) {
|
|
48
|
+
console.log(JSON.stringify(response, null, 2));
|
|
49
|
+
}
|
|
50
|
+
function outputSuccess(data) {
|
|
51
|
+
output(success(data));
|
|
52
|
+
}
|
|
53
|
+
function outputError(code, message) {
|
|
54
|
+
output(error(code, message));
|
|
55
|
+
process.exit(code);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/commands/add.ts
|
|
59
|
+
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) => {
|
|
60
|
+
const json = options.json;
|
|
61
|
+
try {
|
|
62
|
+
const { taskService, config } = getContext();
|
|
63
|
+
const agent = options.agent ?? getAgent();
|
|
64
|
+
const columnId = options.column ?? config.defaults.column;
|
|
65
|
+
const dependsOn = options.dependsOn ? options.dependsOn.split(",").map((s) => s.trim()) : [];
|
|
66
|
+
const task = await taskService.addTask({
|
|
67
|
+
title,
|
|
68
|
+
description: options.description,
|
|
69
|
+
columnId,
|
|
70
|
+
agent,
|
|
71
|
+
dependsOn
|
|
72
|
+
});
|
|
73
|
+
if (json) {
|
|
74
|
+
outputSuccess(task);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.log(`Created task [${task.id.slice(0, 8)}] "${task.title}"`);
|
|
78
|
+
console.log(` Column: ${task.columnId}`);
|
|
79
|
+
console.log(` Agent: ${task.createdBy}`);
|
|
80
|
+
} catch (error2) {
|
|
81
|
+
if (error2 instanceof KabanError) {
|
|
82
|
+
if (json)
|
|
83
|
+
outputError(error2.code, error2.message);
|
|
84
|
+
console.error(`Error: ${error2.message}`);
|
|
85
|
+
process.exit(error2.code);
|
|
86
|
+
}
|
|
87
|
+
throw error2;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// src/commands/done.ts
|
|
92
|
+
import { KabanError as KabanError2 } from "@kaban-board/core";
|
|
93
|
+
import { Command as Command2 } from "commander";
|
|
94
|
+
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) => {
|
|
95
|
+
const json = options.json;
|
|
96
|
+
try {
|
|
97
|
+
const { taskService, boardService } = getContext();
|
|
98
|
+
const tasks = await taskService.listTasks();
|
|
99
|
+
const task = tasks.find((t) => t.id.startsWith(id));
|
|
100
|
+
if (!task) {
|
|
101
|
+
if (json)
|
|
102
|
+
outputError(2, `Task '${id}' not found`);
|
|
103
|
+
console.error(`Error: Task '${id}' not found`);
|
|
104
|
+
process.exit(2);
|
|
105
|
+
}
|
|
106
|
+
const terminal = await boardService.getTerminalColumn();
|
|
107
|
+
if (!terminal) {
|
|
108
|
+
if (json)
|
|
109
|
+
outputError(1, "No terminal column configured");
|
|
110
|
+
console.error("Error: No terminal column configured");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const moved = await taskService.moveTask(task.id, terminal.id);
|
|
114
|
+
if (json) {
|
|
115
|
+
outputSuccess(moved);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
console.log(`Completed [${moved.id.slice(0, 8)}] "${moved.title}"`);
|
|
119
|
+
} catch (error2) {
|
|
120
|
+
if (error2 instanceof KabanError2) {
|
|
121
|
+
if (json)
|
|
122
|
+
outputError(error2.code, error2.message);
|
|
123
|
+
console.error(`Error: ${error2.message}`);
|
|
124
|
+
process.exit(error2.code);
|
|
125
|
+
}
|
|
126
|
+
throw error2;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
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";
|
|
133
|
+
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) => {
|
|
135
|
+
const { kabanDir, dbPath, configPath } = getKabanPaths();
|
|
136
|
+
if (existsSync2(dbPath)) {
|
|
137
|
+
console.error("Error: Board already exists in this directory");
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
mkdirSync(kabanDir, { recursive: true });
|
|
141
|
+
const config = {
|
|
142
|
+
...DEFAULT_CONFIG,
|
|
143
|
+
board: { name: options.name }
|
|
144
|
+
};
|
|
145
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
146
|
+
const db = createDb2(dbPath);
|
|
147
|
+
await initializeSchema(db);
|
|
148
|
+
const boardService = new BoardService2(db);
|
|
149
|
+
await boardService.initializeBoard(config);
|
|
150
|
+
console.log(`Initialized Kaban board: ${options.name}`);
|
|
151
|
+
console.log(` Database: ${dbPath}`);
|
|
152
|
+
console.log(` Config: ${configPath}`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// src/commands/list.ts
|
|
156
|
+
import { KabanError as KabanError3 } from "@kaban-board/core";
|
|
157
|
+
import { Command as Command4 } from "commander";
|
|
158
|
+
function sortTasks(tasks, sortBy, reverse) {
|
|
159
|
+
const sorted = [...tasks].sort((a, b) => {
|
|
160
|
+
switch (sortBy) {
|
|
161
|
+
case "name":
|
|
162
|
+
return a.title.localeCompare(b.title);
|
|
163
|
+
case "date":
|
|
164
|
+
return a.createdAt.getTime() - b.createdAt.getTime();
|
|
165
|
+
case "updated":
|
|
166
|
+
return a.updatedAt.getTime() - b.updatedAt.getTime();
|
|
167
|
+
default:
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
return reverse ? sorted.reverse() : sorted;
|
|
172
|
+
}
|
|
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) => {
|
|
174
|
+
const json = options.json;
|
|
175
|
+
try {
|
|
176
|
+
const { taskService, boardService } = getContext();
|
|
177
|
+
let tasks = await taskService.listTasks({
|
|
178
|
+
columnId: options.column,
|
|
179
|
+
agent: options.agent,
|
|
180
|
+
assignee: options.assignee,
|
|
181
|
+
blocked: options.blocked
|
|
182
|
+
});
|
|
183
|
+
if (options.sort) {
|
|
184
|
+
const validSorts = ["name", "date", "updated"];
|
|
185
|
+
if (!validSorts.includes(options.sort)) {
|
|
186
|
+
console.error(`Invalid sort field: ${options.sort}. Use: ${validSorts.join(", ")}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
tasks = sortTasks(tasks, options.sort, options.reverse ?? false);
|
|
190
|
+
}
|
|
191
|
+
if (json) {
|
|
192
|
+
outputSuccess(tasks);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (tasks.length === 0) {
|
|
196
|
+
console.log("No tasks found");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const columns = await boardService.getColumns();
|
|
200
|
+
const columnMap = new Map(columns.map((c) => [c.id, c]));
|
|
201
|
+
for (const task of tasks) {
|
|
202
|
+
const column = columnMap.get(task.columnId);
|
|
203
|
+
const blocked = task.blockedReason ? " [blocked]" : "";
|
|
204
|
+
const agent = task.createdBy !== "user" ? ` @${task.createdBy}` : "";
|
|
205
|
+
const assignee = task.assignedTo ? ` → ${task.assignedTo}` : "";
|
|
206
|
+
console.log(`[${task.id.slice(0, 8)}] ${task.title}${agent}${assignee}${blocked}`);
|
|
207
|
+
console.log(` ${column?.name ?? task.columnId}`);
|
|
208
|
+
if (task.description) {
|
|
209
|
+
const truncated = task.description.length > 60 ? `${task.description.slice(0, 60)}...` : task.description;
|
|
210
|
+
console.log(` ${truncated}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (error2) {
|
|
214
|
+
if (error2 instanceof KabanError3) {
|
|
215
|
+
if (json)
|
|
216
|
+
outputError(error2.code, error2.message);
|
|
217
|
+
console.error(`Error: ${error2.message}`);
|
|
218
|
+
process.exit(error2.code);
|
|
219
|
+
}
|
|
220
|
+
throw error2;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// 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";
|
|
227
|
+
import {
|
|
228
|
+
BoardService as BoardService3,
|
|
229
|
+
createDb as createDb3,
|
|
230
|
+
DEFAULT_CONFIG as DEFAULT_CONFIG2,
|
|
231
|
+
initializeSchema as initializeSchema2,
|
|
232
|
+
TaskService as TaskService2
|
|
233
|
+
} from "@kaban-board/core";
|
|
234
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
235
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
236
|
+
import {
|
|
237
|
+
CallToolRequestSchema,
|
|
238
|
+
ListResourcesRequestSchema,
|
|
239
|
+
ListToolsRequestSchema,
|
|
240
|
+
ReadResourceRequestSchema
|
|
241
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
242
|
+
import { Command as Command5 } from "commander";
|
|
243
|
+
function getKabanPaths2(basePath) {
|
|
244
|
+
const base = basePath ?? process.cwd();
|
|
245
|
+
const kabanDir = join2(base, ".kaban");
|
|
246
|
+
return {
|
|
247
|
+
kabanDir,
|
|
248
|
+
dbPath: join2(kabanDir, "board.db"),
|
|
249
|
+
configPath: join2(kabanDir, "config.json")
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function createContext(basePath) {
|
|
253
|
+
const { dbPath, configPath } = getKabanPaths2(basePath);
|
|
254
|
+
if (!existsSync3(dbPath)) {
|
|
255
|
+
throw new Error("No board found. Run 'kaban init' first");
|
|
256
|
+
}
|
|
257
|
+
const db = createDb3(dbPath);
|
|
258
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
259
|
+
const boardService = new BoardService3(db);
|
|
260
|
+
const taskService = new TaskService2(db, boardService);
|
|
261
|
+
return { db, config, boardService, taskService };
|
|
262
|
+
}
|
|
263
|
+
async function startMcpServer(workingDirectory) {
|
|
264
|
+
const server = new Server({ name: "kaban-mcp", version: "0.1.0" }, { capabilities: { tools: {}, resources: {} } });
|
|
265
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
266
|
+
tools: [
|
|
267
|
+
{
|
|
268
|
+
name: "kaban_init",
|
|
269
|
+
description: "Initialize a new Kaban board in the specified or current directory",
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: "object",
|
|
272
|
+
properties: {
|
|
273
|
+
name: { type: "string", description: "Board name (default: 'Kaban Board')" },
|
|
274
|
+
path: { type: "string", description: "Directory path (default: KABAN_PATH or cwd)" }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "kaban_add_task",
|
|
280
|
+
description: "Add a new task to the Kaban board",
|
|
281
|
+
inputSchema: {
|
|
282
|
+
type: "object",
|
|
283
|
+
properties: {
|
|
284
|
+
title: { type: "string", description: "Task title (1-200 chars)" },
|
|
285
|
+
description: { type: "string", description: "Task description" },
|
|
286
|
+
columnId: { type: "string", description: "Column ID (default: todo)" },
|
|
287
|
+
agent: { type: "string", description: "Agent name creating the task" },
|
|
288
|
+
dependsOn: {
|
|
289
|
+
type: "array",
|
|
290
|
+
items: { type: "string" },
|
|
291
|
+
description: "Task IDs this depends on"
|
|
292
|
+
},
|
|
293
|
+
files: {
|
|
294
|
+
type: "array",
|
|
295
|
+
items: { type: "string" },
|
|
296
|
+
description: "Associated file paths"
|
|
297
|
+
},
|
|
298
|
+
labels: { type: "array", items: { type: "string" }, description: "Task labels" }
|
|
299
|
+
},
|
|
300
|
+
required: ["title"]
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "kaban_get_task",
|
|
305
|
+
description: "Get a task by its ID",
|
|
306
|
+
inputSchema: {
|
|
307
|
+
type: "object",
|
|
308
|
+
properties: { id: { type: "string", description: "Task ID (ULID)" } },
|
|
309
|
+
required: ["id"]
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: "kaban_list_tasks",
|
|
314
|
+
description: "List tasks with optional filters",
|
|
315
|
+
inputSchema: {
|
|
316
|
+
type: "object",
|
|
317
|
+
properties: {
|
|
318
|
+
columnId: { type: "string", description: "Filter by column ID" },
|
|
319
|
+
agent: { type: "string", description: "Filter by agent name" },
|
|
320
|
+
blocked: { type: "boolean", description: "Show only blocked tasks" }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "kaban_move_task",
|
|
326
|
+
description: "Move a task to a different column",
|
|
327
|
+
inputSchema: {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {
|
|
330
|
+
id: { type: "string", description: "Task ID (ULID)" },
|
|
331
|
+
columnId: { type: "string", description: "Target column ID" },
|
|
332
|
+
force: { type: "boolean", description: "Force move even if WIP limit exceeded" }
|
|
333
|
+
},
|
|
334
|
+
required: ["id", "columnId"]
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: "kaban_update_task",
|
|
339
|
+
description: "Update a task's properties",
|
|
340
|
+
inputSchema: {
|
|
341
|
+
type: "object",
|
|
342
|
+
properties: {
|
|
343
|
+
id: { type: "string", description: "Task ID (ULID)" },
|
|
344
|
+
title: { type: "string", description: "New task title" },
|
|
345
|
+
description: { type: ["string", "null"], description: "New task description" },
|
|
346
|
+
assignedTo: { type: ["string", "null"], description: "Assigned agent name" },
|
|
347
|
+
files: {
|
|
348
|
+
type: "array",
|
|
349
|
+
items: { type: "string" },
|
|
350
|
+
description: "Associated file paths"
|
|
351
|
+
},
|
|
352
|
+
labels: { type: "array", items: { type: "string" }, description: "Task labels" },
|
|
353
|
+
expectedVersion: {
|
|
354
|
+
type: "number",
|
|
355
|
+
description: "Expected version for optimistic locking"
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
required: ["id"]
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: "kaban_delete_task",
|
|
363
|
+
description: "Delete a task",
|
|
364
|
+
inputSchema: {
|
|
365
|
+
type: "object",
|
|
366
|
+
properties: { id: { type: "string", description: "Task ID (ULID)" } },
|
|
367
|
+
required: ["id"]
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: "kaban_complete_task",
|
|
372
|
+
description: "Mark a task as completed (move to terminal column)",
|
|
373
|
+
inputSchema: {
|
|
374
|
+
type: "object",
|
|
375
|
+
properties: { id: { type: "string", description: "Task ID (ULID) or partial ID" } },
|
|
376
|
+
required: ["id"]
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: "kaban_status",
|
|
381
|
+
description: "Get board status summary",
|
|
382
|
+
inputSchema: { type: "object", properties: {} }
|
|
383
|
+
}
|
|
384
|
+
]
|
|
385
|
+
}));
|
|
386
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
387
|
+
const { name, arguments: args } = request.params;
|
|
388
|
+
try {
|
|
389
|
+
if (name === "kaban_init") {
|
|
390
|
+
const { name: boardName = "Kaban Board", path: basePath } = args ?? {};
|
|
391
|
+
const targetPath = basePath ?? workingDirectory;
|
|
392
|
+
const { kabanDir, dbPath, configPath } = getKabanPaths2(targetPath);
|
|
393
|
+
if (existsSync3(dbPath)) {
|
|
394
|
+
return {
|
|
395
|
+
content: [
|
|
396
|
+
{
|
|
397
|
+
type: "text",
|
|
398
|
+
text: JSON.stringify({ error: "Board already exists in this directory" })
|
|
399
|
+
}
|
|
400
|
+
],
|
|
401
|
+
isError: true
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
mkdirSync2(kabanDir, { recursive: true });
|
|
405
|
+
const config = {
|
|
406
|
+
...DEFAULT_CONFIG2,
|
|
407
|
+
board: { name: boardName }
|
|
408
|
+
};
|
|
409
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
410
|
+
const db = createDb3(dbPath);
|
|
411
|
+
await initializeSchema2(db);
|
|
412
|
+
const boardService2 = new BoardService3(db);
|
|
413
|
+
await boardService2.initializeBoard(config);
|
|
414
|
+
return {
|
|
415
|
+
content: [
|
|
416
|
+
{
|
|
417
|
+
type: "text",
|
|
418
|
+
text: JSON.stringify({
|
|
419
|
+
success: true,
|
|
420
|
+
board: boardName,
|
|
421
|
+
paths: { database: dbPath, config: configPath }
|
|
422
|
+
}, null, 2)
|
|
423
|
+
}
|
|
424
|
+
]
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const { taskService, boardService } = createContext(workingDirectory);
|
|
428
|
+
switch (name) {
|
|
429
|
+
case "kaban_add_task": {
|
|
430
|
+
const task = await taskService.addTask(args);
|
|
431
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
432
|
+
}
|
|
433
|
+
case "kaban_get_task": {
|
|
434
|
+
const task = await taskService.getTask(args.id);
|
|
435
|
+
if (!task)
|
|
436
|
+
return {
|
|
437
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Task not found" }) }]
|
|
438
|
+
};
|
|
439
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
440
|
+
}
|
|
441
|
+
case "kaban_list_tasks": {
|
|
442
|
+
const tasks = await taskService.listTasks(args);
|
|
443
|
+
return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
|
|
444
|
+
}
|
|
445
|
+
case "kaban_move_task": {
|
|
446
|
+
const { id, columnId, force } = args;
|
|
447
|
+
const task = await taskService.moveTask(id, columnId, { force });
|
|
448
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
449
|
+
}
|
|
450
|
+
case "kaban_update_task": {
|
|
451
|
+
const { id, expectedVersion, ...updates } = args;
|
|
452
|
+
const task = await taskService.updateTask(id, updates, expectedVersion);
|
|
453
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
454
|
+
}
|
|
455
|
+
case "kaban_delete_task": {
|
|
456
|
+
await taskService.deleteTask(args.id);
|
|
457
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true }) }] };
|
|
458
|
+
}
|
|
459
|
+
case "kaban_complete_task": {
|
|
460
|
+
const { id } = args;
|
|
461
|
+
const tasks = await taskService.listTasks();
|
|
462
|
+
const task = tasks.find((t) => t.id.startsWith(id));
|
|
463
|
+
if (!task)
|
|
464
|
+
return {
|
|
465
|
+
content: [
|
|
466
|
+
{ type: "text", text: JSON.stringify({ error: `Task '${id}' not found` }) }
|
|
467
|
+
]
|
|
468
|
+
};
|
|
469
|
+
const terminal = await boardService.getTerminalColumn();
|
|
470
|
+
if (!terminal)
|
|
471
|
+
return {
|
|
472
|
+
content: [
|
|
473
|
+
{ type: "text", text: JSON.stringify({ error: "No terminal column configured" }) }
|
|
474
|
+
]
|
|
475
|
+
};
|
|
476
|
+
const completed = await taskService.moveTask(task.id, terminal.id);
|
|
477
|
+
return { content: [{ type: "text", text: JSON.stringify(completed, null, 2) }] };
|
|
478
|
+
}
|
|
479
|
+
case "kaban_status": {
|
|
480
|
+
const board = await boardService.getBoard();
|
|
481
|
+
const columns = await boardService.getColumns();
|
|
482
|
+
const tasks = await taskService.listTasks();
|
|
483
|
+
const columnStats = columns.map((column) => ({
|
|
484
|
+
id: column.id,
|
|
485
|
+
name: column.name,
|
|
486
|
+
count: tasks.filter((t) => t.columnId === column.id).length,
|
|
487
|
+
wipLimit: column.wipLimit,
|
|
488
|
+
isTerminal: column.isTerminal
|
|
489
|
+
}));
|
|
490
|
+
return {
|
|
491
|
+
content: [
|
|
492
|
+
{
|
|
493
|
+
type: "text",
|
|
494
|
+
text: JSON.stringify({
|
|
495
|
+
board: { name: board?.name ?? "Kaban Board" },
|
|
496
|
+
columns: columnStats,
|
|
497
|
+
blockedCount: tasks.filter((t) => t.blockedReason).length,
|
|
498
|
+
totalTasks: tasks.length
|
|
499
|
+
}, null, 2)
|
|
500
|
+
}
|
|
501
|
+
]
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
default:
|
|
505
|
+
return {
|
|
506
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
|
|
507
|
+
isError: true
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
} catch (error2) {
|
|
511
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
512
|
+
return {
|
|
513
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
514
|
+
isError: true
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
519
|
+
resources: [
|
|
520
|
+
{
|
|
521
|
+
uri: "kaban://board/status",
|
|
522
|
+
name: "Board Status",
|
|
523
|
+
description: "Current Kaban board status and task summary",
|
|
524
|
+
mimeType: "application/json"
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
uri: "kaban://board/columns",
|
|
528
|
+
name: "Board Columns",
|
|
529
|
+
description: "Available columns on the Kaban board",
|
|
530
|
+
mimeType: "application/json"
|
|
531
|
+
}
|
|
532
|
+
]
|
|
533
|
+
}));
|
|
534
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
535
|
+
const { uri } = request.params;
|
|
536
|
+
try {
|
|
537
|
+
const { taskService, boardService } = createContext(workingDirectory);
|
|
538
|
+
if (uri === "kaban://board/status") {
|
|
539
|
+
const board = await boardService.getBoard();
|
|
540
|
+
const columns = await boardService.getColumns();
|
|
541
|
+
const tasks = await taskService.listTasks();
|
|
542
|
+
const columnStats = columns.map((column) => ({
|
|
543
|
+
id: column.id,
|
|
544
|
+
name: column.name,
|
|
545
|
+
count: tasks.filter((t) => t.columnId === column.id).length,
|
|
546
|
+
wipLimit: column.wipLimit,
|
|
547
|
+
isTerminal: column.isTerminal,
|
|
548
|
+
tasks: tasks.filter((t) => t.columnId === column.id).map((t) => ({
|
|
549
|
+
id: t.id,
|
|
550
|
+
title: t.title,
|
|
551
|
+
createdBy: t.createdBy,
|
|
552
|
+
blocked: !!t.blockedReason
|
|
553
|
+
}))
|
|
554
|
+
}));
|
|
555
|
+
return {
|
|
556
|
+
contents: [
|
|
557
|
+
{
|
|
558
|
+
uri,
|
|
559
|
+
mimeType: "application/json",
|
|
560
|
+
text: JSON.stringify({
|
|
561
|
+
board: { name: board?.name ?? "Kaban Board" },
|
|
562
|
+
columns: columnStats,
|
|
563
|
+
blockedCount: tasks.filter((t) => t.blockedReason).length,
|
|
564
|
+
totalTasks: tasks.length
|
|
565
|
+
}, null, 2)
|
|
566
|
+
}
|
|
567
|
+
]
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (uri === "kaban://board/columns") {
|
|
571
|
+
const columns = await boardService.getColumns();
|
|
572
|
+
return {
|
|
573
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(columns, null, 2) }]
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
if (uri.startsWith("kaban://tasks/")) {
|
|
577
|
+
const columnId = uri.replace("kaban://tasks/", "");
|
|
578
|
+
const tasks = await taskService.listTasks({ columnId });
|
|
579
|
+
return {
|
|
580
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(tasks, null, 2) }]
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (uri.startsWith("kaban://task/")) {
|
|
584
|
+
const id = uri.replace("kaban://task/", "");
|
|
585
|
+
const task = await taskService.getTask(id);
|
|
586
|
+
if (!task)
|
|
587
|
+
return {
|
|
588
|
+
contents: [
|
|
589
|
+
{
|
|
590
|
+
uri,
|
|
591
|
+
mimeType: "application/json",
|
|
592
|
+
text: JSON.stringify({ error: "Task not found" })
|
|
593
|
+
}
|
|
594
|
+
]
|
|
595
|
+
};
|
|
596
|
+
return {
|
|
597
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(task, null, 2) }]
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
contents: [
|
|
602
|
+
{
|
|
603
|
+
uri,
|
|
604
|
+
mimeType: "application/json",
|
|
605
|
+
text: JSON.stringify({ error: "Resource not found" })
|
|
606
|
+
}
|
|
607
|
+
]
|
|
608
|
+
};
|
|
609
|
+
} catch (error2) {
|
|
610
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
611
|
+
return {
|
|
612
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ error: message }) }]
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
const transport = new StdioServerTransport;
|
|
617
|
+
await server.connect(transport);
|
|
618
|
+
console.error("Kaban MCP server running on stdio");
|
|
619
|
+
}
|
|
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) => {
|
|
621
|
+
const workingDirectory = options.path ?? process.env.KABAN_PATH ?? process.cwd();
|
|
622
|
+
await startMcpServer(workingDirectory);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// src/commands/move.ts
|
|
626
|
+
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) => {
|
|
629
|
+
const json = options.json;
|
|
630
|
+
try {
|
|
631
|
+
const { taskService, boardService } = getContext();
|
|
632
|
+
const tasks = await taskService.listTasks();
|
|
633
|
+
const task = tasks.find((t) => t.id.startsWith(id));
|
|
634
|
+
if (!task) {
|
|
635
|
+
if (json)
|
|
636
|
+
outputError(2, `Task '${id}' not found`);
|
|
637
|
+
console.error(`Error: Task '${id}' not found`);
|
|
638
|
+
process.exit(2);
|
|
639
|
+
}
|
|
640
|
+
let targetColumn = column;
|
|
641
|
+
if (options.next) {
|
|
642
|
+
const columns = await boardService.getColumns();
|
|
643
|
+
const currentIdx = columns.findIndex((c) => c.id === task.columnId);
|
|
644
|
+
if (currentIdx < columns.length - 1) {
|
|
645
|
+
targetColumn = columns[currentIdx + 1].id;
|
|
646
|
+
} else {
|
|
647
|
+
if (json)
|
|
648
|
+
outputError(4, "Task is already in the last column");
|
|
649
|
+
console.error("Error: Task is already in the last column");
|
|
650
|
+
process.exit(4);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (!targetColumn) {
|
|
654
|
+
if (json)
|
|
655
|
+
outputError(4, "Specify a column or use --next");
|
|
656
|
+
console.error("Error: Specify a column or use --next");
|
|
657
|
+
process.exit(4);
|
|
658
|
+
}
|
|
659
|
+
const moved = await taskService.moveTask(task.id, targetColumn, {
|
|
660
|
+
force: options.force
|
|
661
|
+
});
|
|
662
|
+
if (json) {
|
|
663
|
+
outputSuccess(moved);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const col = await boardService.getColumn(moved.columnId);
|
|
667
|
+
console.log(`Moved [${moved.id.slice(0, 8)}] to ${col?.name}`);
|
|
668
|
+
} catch (error2) {
|
|
669
|
+
if (error2 instanceof KabanError4) {
|
|
670
|
+
if (json)
|
|
671
|
+
outputError(error2.code, error2.message);
|
|
672
|
+
console.error(`Error: ${error2.message}`);
|
|
673
|
+
process.exit(error2.code);
|
|
674
|
+
}
|
|
675
|
+
throw error2;
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// src/commands/schema.ts
|
|
680
|
+
import { jsonSchemas } from "@kaban-board/core";
|
|
681
|
+
import { Command as Command7 } from "commander";
|
|
682
|
+
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) => {
|
|
684
|
+
if (!name) {
|
|
685
|
+
console.log("Available schemas:");
|
|
686
|
+
for (const schemaName of availableSchemas) {
|
|
687
|
+
console.log(` - ${schemaName}`);
|
|
688
|
+
}
|
|
689
|
+
console.log(`
|
|
690
|
+
Usage: kaban schema <name>`);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const schema = jsonSchemas[name];
|
|
694
|
+
if (!schema) {
|
|
695
|
+
console.error(`Error: Unknown schema '${name}'`);
|
|
696
|
+
console.error(`Available: ${availableSchemas.join(", ")}`);
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
console.log(JSON.stringify(schema, null, 2));
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// src/commands/status.ts
|
|
703
|
+
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) => {
|
|
706
|
+
const json = options.json;
|
|
707
|
+
try {
|
|
708
|
+
const { taskService, boardService } = getContext();
|
|
709
|
+
const board = await boardService.getBoard();
|
|
710
|
+
const columns = await boardService.getColumns();
|
|
711
|
+
const tasks = await taskService.listTasks();
|
|
712
|
+
if (json) {
|
|
713
|
+
const columnStats = columns.map((column) => {
|
|
714
|
+
const columnTasks = tasks.filter((t) => t.columnId === column.id);
|
|
715
|
+
return {
|
|
716
|
+
id: column.id,
|
|
717
|
+
name: column.name,
|
|
718
|
+
count: columnTasks.length,
|
|
719
|
+
wipLimit: column.wipLimit,
|
|
720
|
+
isTerminal: column.isTerminal
|
|
721
|
+
};
|
|
722
|
+
});
|
|
723
|
+
outputSuccess({
|
|
724
|
+
board: { name: board?.name ?? "Kaban Board" },
|
|
725
|
+
columns: columnStats,
|
|
726
|
+
blockedCount: tasks.filter((t) => t.blockedReason).length,
|
|
727
|
+
totalTasks: tasks.length
|
|
728
|
+
});
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
console.log(`
|
|
732
|
+
${board?.name ?? "Kaban Board"}
|
|
733
|
+
`);
|
|
734
|
+
for (const column of columns) {
|
|
735
|
+
const columnTasks = tasks.filter((t) => t.columnId === column.id);
|
|
736
|
+
const count = columnTasks.length;
|
|
737
|
+
const limit = column.wipLimit ? `/${column.wipLimit}` : "";
|
|
738
|
+
const terminal = column.isTerminal ? " [done]" : "";
|
|
739
|
+
console.log(` ${column.name}: ${count}${limit}${terminal}`);
|
|
740
|
+
}
|
|
741
|
+
const blocked = tasks.filter((t) => t.blockedReason).length;
|
|
742
|
+
if (blocked > 0) {
|
|
743
|
+
console.log(`
|
|
744
|
+
${blocked} blocked task(s)`);
|
|
745
|
+
}
|
|
746
|
+
console.log();
|
|
747
|
+
} catch (error2) {
|
|
748
|
+
if (error2 instanceof KabanError5) {
|
|
749
|
+
if (json)
|
|
750
|
+
outputError(error2.code, error2.message);
|
|
751
|
+
console.error(`Error: ${error2.message}`);
|
|
752
|
+
process.exit(error2.code);
|
|
753
|
+
}
|
|
754
|
+
throw error2;
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// src/commands/tui.ts
|
|
759
|
+
import { spawn } from "node:child_process";
|
|
760
|
+
import { dirname, join as join3 } from "node:path";
|
|
761
|
+
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], {
|
|
767
|
+
stdio: "inherit",
|
|
768
|
+
cwd: process.cwd()
|
|
769
|
+
});
|
|
770
|
+
child.on("exit", (code) => {
|
|
771
|
+
process.exit(code ?? 0);
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// src/index.ts
|
|
776
|
+
var program = new Command10;
|
|
777
|
+
program.name("kaban").description("Terminal Kanban for AI Code Agents").version("0.1.0");
|
|
778
|
+
program.addCommand(initCommand);
|
|
779
|
+
program.addCommand(addCommand);
|
|
780
|
+
program.addCommand(listCommand);
|
|
781
|
+
program.addCommand(moveCommand);
|
|
782
|
+
program.addCommand(doneCommand);
|
|
783
|
+
program.addCommand(statusCommand);
|
|
784
|
+
program.addCommand(schemaCommand);
|
|
785
|
+
program.addCommand(mcpCommand);
|
|
786
|
+
program.addCommand(tuiCommand);
|
|
787
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kaban-board/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Terminal Kanban for AI Code Agents - CLI and MCP server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kaban": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node --packages external && chmod +x ./dist/index.js",
|
|
14
|
+
"dev": "bun run ./src/index.ts",
|
|
15
|
+
"test": "bun test",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@kaban-board/core": "workspace:*",
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
22
|
+
"commander": "^12.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/bun": "latest",
|
|
26
|
+
"typescript": "^5.0.0"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/beshkenadze/kaban.git",
|
|
34
|
+
"directory": "packages/cli"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://beshkenadze.github.io/kaban",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/beshkenadze/kaban/issues"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"keywords": [
|
|
42
|
+
"kanban",
|
|
43
|
+
"ai",
|
|
44
|
+
"agents",
|
|
45
|
+
"mcp",
|
|
46
|
+
"claude",
|
|
47
|
+
"terminal",
|
|
48
|
+
"cli",
|
|
49
|
+
"tui",
|
|
50
|
+
"task-management"
|
|
51
|
+
],
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
55
|
+
}
|