kanbango 2.1.0 → 2.4.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/AGENTS.md +7 -7
- package/API.md +20 -12
- package/CHANGELOG.md +32 -0
- package/LLM_AGENTS.md +141 -175
- package/README.md +31 -14
- package/bin/kanban.js +101 -26
- package/index.html +162 -85
- package/index.js +2 -0
- package/kan2.md +76 -0
- package/kanban.js +263 -34
- package/mcp-server.js +389 -185
- package/package.json +1 -1
- package/plan.js +159 -0
- package/planv2.md +17 -7
- package/tests/run.js +1 -0
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ npx kanbango --help
|
|
|
25
25
|
# Initialize backlog directories
|
|
26
26
|
kanban init
|
|
27
27
|
|
|
28
|
-
# Start web GUI
|
|
28
|
+
# Start web GUI (stable project port; prints the real URL)
|
|
29
29
|
kanban serve
|
|
30
30
|
|
|
31
31
|
# List all tasks
|
|
@@ -35,13 +35,13 @@ kanban list --json
|
|
|
35
35
|
kanban add "My task" --col planned --epic "Phase 1"
|
|
36
36
|
|
|
37
37
|
# Show details
|
|
38
|
-
kanban show
|
|
38
|
+
kanban show 001
|
|
39
39
|
|
|
40
40
|
# Move between columns (active | planned | icebox | done)
|
|
41
|
-
kanban move
|
|
41
|
+
kanban move 001 active
|
|
42
42
|
|
|
43
|
-
#
|
|
44
|
-
kanban
|
|
43
|
+
# Update subtasks in one call
|
|
44
|
+
kanban update 001 '{"subtasks":[{"done":true,"text":"Research"},{"done":false,"text":"Implementation"}]}'
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
### Columns
|
|
@@ -59,14 +59,17 @@ Tasks are JSON files in `backlog/<column>/`:
|
|
|
59
59
|
|
|
60
60
|
```json
|
|
61
61
|
{
|
|
62
|
-
"id": "
|
|
62
|
+
"id": "001",
|
|
63
63
|
"title": "My Feature",
|
|
64
64
|
"column": "planned",
|
|
65
65
|
"epic_group": "Phase 1",
|
|
66
66
|
"created": "2026-07-08",
|
|
67
67
|
"description": "High-level context.",
|
|
68
68
|
"specs": "Technical details.",
|
|
69
|
+
"in_scope": ["What this task covers"],
|
|
70
|
+
"out_of_scope": ["What is explicitly excluded"],
|
|
69
71
|
"acceptance_criteria": ["Works as expected"],
|
|
72
|
+
"test_cases": ["Verify the happy path"],
|
|
70
73
|
"subtasks": [
|
|
71
74
|
{ "id": "st-1", "text": "First step", "done": false }
|
|
72
75
|
]
|
|
@@ -96,6 +99,24 @@ Add this to your MCP client config (`.mcp.json`, `opencode.json`, or Claude Desk
|
|
|
96
99
|
}
|
|
97
100
|
```
|
|
98
101
|
|
|
102
|
+
Auto-start the web GUI with MCP (opt-in):
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"kanbango": {
|
|
108
|
+
"command": "npx",
|
|
109
|
+
"args": ["kanbango", "mcp"],
|
|
110
|
+
"env": {
|
|
111
|
+
"KANBANGO_AUTO_GUI": "1"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Optional: pin the port with `KANBANGO_GUI_PORT` (e.g. `"5821"`). Without it, each project gets a stable port in `5510–5999` derived from the project path. The real URL is always available via `kanban_gui` → `status` (and written to `backlog/.kanbango-gui.json` while the GUI runs).
|
|
119
|
+
|
|
99
120
|
Or generate the config files automatically:
|
|
100
121
|
|
|
101
122
|
```bash
|
|
@@ -109,11 +130,8 @@ Once connected, your agent gets access to these tools:
|
|
|
109
130
|
| Tool | What it does |
|
|
110
131
|
|------|-------------|
|
|
111
132
|
| `kanban_read` | List tasks, filter by column/epic, show details |
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `kanban_gui_start` | Start web GUI from the agent |
|
|
115
|
-
| `kanban_gui_stop` | Stop web GUI |
|
|
116
|
-
| `kanban_gui_status` | Check if GUI is running |
|
|
133
|
+
| `kanban_manage` | Create, move, patch-update tasks |
|
|
134
|
+
| `kanban_gui` | Start, stop, or check web GUI status (returns the real URL/port) |
|
|
117
135
|
|
|
118
136
|
Your agent stays in sync with your real board — every change is persisted as JSON files.
|
|
119
137
|
|
|
@@ -122,12 +140,11 @@ Your agent stays in sync with your real board — every change is persisted as J
|
|
|
122
140
|
| Command | Description |
|
|
123
141
|
|---------|-------------|
|
|
124
142
|
| `kanban init` | Create backlog directory structure |
|
|
125
|
-
| `kanban serve [PORT]` | Start web GUI (
|
|
143
|
+
| `kanban serve [PORT]` | Start web GUI (stable project port, or PORT / KANBANGO_GUI_PORT) |
|
|
126
144
|
| `kanban list [--col <col>] [--json]` | List tasks |
|
|
127
145
|
| `kanban show <ID>` | Show task details |
|
|
128
146
|
| `kanban add <TITLE>` | Add a new task |
|
|
129
147
|
| `kanban move <ID> <COL>` | Move task |
|
|
130
|
-
| `kanban toggle <ID> <IDX>` | Toggle subtask |
|
|
131
148
|
| `kanban mcp-init` | Generate MCP config files |
|
|
132
149
|
|
|
133
150
|
## Web GUI
|
|
@@ -152,4 +169,4 @@ Node.js 16+
|
|
|
152
169
|
|
|
153
170
|
## License
|
|
154
171
|
|
|
155
|
-
MIT
|
|
172
|
+
MIT
|
package/bin/kanban.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const kanban = require('../kanban.js');
|
|
4
|
+
const plan = require('../plan.js');
|
|
4
5
|
const http = require('http');
|
|
5
6
|
const fs = require('fs');
|
|
6
7
|
const path = require('path');
|
|
@@ -9,7 +10,7 @@ const BACKLOG = path.join(process.cwd(), 'backlog');
|
|
|
9
10
|
const COLS = kanban.COLS;
|
|
10
11
|
|
|
11
12
|
function shortId(taskId) {
|
|
12
|
-
const match = taskId.match(/^(
|
|
13
|
+
const match = taskId.match(/^(?:[A-Z]+-)?(\d+)/);
|
|
13
14
|
return match ? match[1] : taskId;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -222,21 +223,6 @@ async function cliAdd(title, column, epicGroup) {
|
|
|
222
223
|
}
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
async function cliToggle(taskId, idx) {
|
|
226
|
-
const success = await kanban.doToggle(taskId, idx);
|
|
227
|
-
if (!success) {
|
|
228
|
-
console.error(`✗ Nie znaleziono: ${taskId} subtask ${idx}`);
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const task = await kanban.getTask(taskId);
|
|
233
|
-
if (idx < task.subtasks.length) {
|
|
234
|
-
const subtask = task.subtasks[idx];
|
|
235
|
-
const mark = subtask.done ? '✓' : '○';
|
|
236
|
-
console.log(` [${idx}] ${mark} ${subtask.text}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
226
|
async function serveWeb(port) {
|
|
241
227
|
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf-8');
|
|
242
228
|
|
|
@@ -262,7 +248,10 @@ async function serveWeb(port) {
|
|
|
262
248
|
const task = await kanban.doCreate(body.title || '', body.column || 'planned', body.epic_group || '—', {
|
|
263
249
|
description: body.description,
|
|
264
250
|
specs: body.specs,
|
|
251
|
+
in_scope: body.in_scope,
|
|
252
|
+
out_of_scope: body.out_of_scope,
|
|
265
253
|
acceptance_criteria: body.acceptance_criteria,
|
|
254
|
+
test_cases: body.test_cases,
|
|
266
255
|
subtasks: body.subtasks,
|
|
267
256
|
notes: body.notes
|
|
268
257
|
});
|
|
@@ -312,10 +301,12 @@ async function serveWeb(port) {
|
|
|
312
301
|
const patch = body.patch ? { ...body.patch } : {};
|
|
313
302
|
|
|
314
303
|
if (body.title !== undefined) patch.title = body.title;
|
|
315
|
-
if (body.tasks !== undefined) patch.subtasks = body.tasks;
|
|
316
304
|
if (body.description !== undefined) patch.description = body.description;
|
|
317
305
|
if (body.specs !== undefined) patch.specs = body.specs;
|
|
306
|
+
if (body.in_scope !== undefined) patch.in_scope = body.in_scope;
|
|
307
|
+
if (body.out_of_scope !== undefined) patch.out_of_scope = body.out_of_scope;
|
|
318
308
|
if (body.acceptance_criteria !== undefined) patch.acceptance_criteria = body.acceptance_criteria;
|
|
309
|
+
if (body.test_cases !== undefined) patch.test_cases = body.test_cases;
|
|
319
310
|
if (body.subtasks !== undefined) patch.subtasks = body.subtasks;
|
|
320
311
|
if (body.notes !== undefined) patch.notes = body.notes;
|
|
321
312
|
if (body.epic_group !== undefined) patch.epic_group = body.epic_group;
|
|
@@ -332,11 +323,66 @@ async function serveWeb(port) {
|
|
|
332
323
|
}
|
|
333
324
|
});
|
|
334
325
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
326
|
+
const MAX_ATTEMPTS = 10;
|
|
327
|
+
|
|
328
|
+
function listenOnce(server, port) {
|
|
329
|
+
return new Promise((resolve, reject) => {
|
|
330
|
+
function onError(err) {
|
|
331
|
+
server.removeListener('listening', onListening);
|
|
332
|
+
reject(err);
|
|
333
|
+
}
|
|
334
|
+
function onListening() {
|
|
335
|
+
server.removeListener('error', onError);
|
|
336
|
+
resolve();
|
|
337
|
+
}
|
|
338
|
+
server.once('error', onError);
|
|
339
|
+
server.once('listening', onListening);
|
|
340
|
+
server.listen(port, 'localhost');
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
345
|
+
try {
|
|
346
|
+
await listenOnce(server, port + attempt);
|
|
347
|
+
break;
|
|
348
|
+
} catch (err) {
|
|
349
|
+
if (err.code !== 'EADDRINUSE') throw err;
|
|
350
|
+
console.log(`Port ${port + attempt} zajęty, próbuję ${port + attempt + 1}…`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!server.listening) {
|
|
355
|
+
console.log(`Porty ${port}–${port + MAX_ATTEMPTS - 1} zajęte, próbuję losowy port…`);
|
|
356
|
+
await listenOnce(server, 0);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const actualPort = server.address().port;
|
|
360
|
+
const portInfo = await kanban.writeGuiPortFile({ port: actualPort, pid: process.pid });
|
|
361
|
+
|
|
362
|
+
async function cleanupGuiPortFile() {
|
|
363
|
+
try {
|
|
364
|
+
await kanban.clearGuiPortFile({ pid: process.pid });
|
|
365
|
+
} catch {
|
|
366
|
+
// best-effort cleanup
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
server.on('close', () => {
|
|
371
|
+
cleanupGuiPortFile();
|
|
339
372
|
});
|
|
373
|
+
|
|
374
|
+
process.once('SIGINT', async () => {
|
|
375
|
+
await cleanupGuiPortFile();
|
|
376
|
+
server.close(() => process.exit(0));
|
|
377
|
+
});
|
|
378
|
+
process.once('SIGTERM', async () => {
|
|
379
|
+
await cleanupGuiPortFile();
|
|
380
|
+
server.close(() => process.exit(0));
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
console.log(`\x1b[1;32m→ Kanban GUI: ${portInfo.url}\x1b[0m`);
|
|
384
|
+
console.log(` Backlog: ${BACKLOG}`);
|
|
385
|
+
console.log(' Ctrl+C żeby zamknąć');
|
|
340
386
|
}
|
|
341
387
|
|
|
342
388
|
function readBody(req) {
|
|
@@ -379,13 +425,42 @@ async function cliMigrate(dryRun) {
|
|
|
379
425
|
}
|
|
380
426
|
}
|
|
381
427
|
|
|
428
|
+
function parseJsonPayload(value) {
|
|
429
|
+
try {
|
|
430
|
+
return JSON.parse(value || '{}');
|
|
431
|
+
} catch (error) {
|
|
432
|
+
throw kanban.createKanbanError('INVALID_JSON', 'Payload is not valid JSON',
|
|
433
|
+
'Pass a JSON object after --json', { reason: error.message }, false, 400);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function cliPlan(action, payload) {
|
|
438
|
+
try {
|
|
439
|
+
const handlers = {
|
|
440
|
+
create: plan.create,
|
|
441
|
+
advance: plan.advance,
|
|
442
|
+
evidence: plan.evidence,
|
|
443
|
+
done: plan.done,
|
|
444
|
+
status: (input) => plan.status(input.task_id)
|
|
445
|
+
};
|
|
446
|
+
if (!handlers[action]) throw kanban.createKanbanError('INVALID_PLAN_ACTION', `Unknown plan action: ${action}`,
|
|
447
|
+
'Use create, advance, evidence, done, or status', { action }, false, 400);
|
|
448
|
+
console.log(JSON.stringify(await handlers[action](payload)));
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.log(JSON.stringify({ ok: false, task_id: payload.task_id || null, subtasks: [], error: {
|
|
451
|
+
code: error.code || 'INTERNAL_ERROR', message: error.message, hint: error.hint || '', details: error.details || {}
|
|
452
|
+
} }));
|
|
453
|
+
process.exitCode = 1;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
382
457
|
async function main() {
|
|
383
458
|
const args = process.argv.slice(2);
|
|
384
459
|
const cmd = args[0];
|
|
385
460
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
461
|
+
if (!cmd || cmd === 'serve') {
|
|
462
|
+
const port = kanban.resolvePreferredGuiPort(args[1]);
|
|
463
|
+
await serveWeb(port);
|
|
389
464
|
} else if (cmd === 'init') {
|
|
390
465
|
await cliInit();
|
|
391
466
|
} else if (cmd === 'mcp-init') {
|
|
@@ -448,8 +523,8 @@ async function main() {
|
|
|
448
523
|
}
|
|
449
524
|
|
|
450
525
|
await cliAdd(args[1], column, epicGroup);
|
|
451
|
-
} else if (cmd === '
|
|
452
|
-
await
|
|
526
|
+
} else if (cmd === 'plan' && args[1] && args[2] === '--json') {
|
|
527
|
+
await cliPlan(args[1], parseJsonPayload(args[3]));
|
|
453
528
|
} else {
|
|
454
529
|
console.error('Unknown command:', cmd);
|
|
455
530
|
process.exit(1);
|