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/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 at http://localhost:5500
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 PI-001
38
+ kanban show 001
39
39
 
40
40
  # Move between columns (active | planned | icebox | done)
41
- kanban move PI-001 active
41
+ kanban move 001 active
42
42
 
43
- # Toggle subtask completion
44
- kanban toggle PI-001 0
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": "PI-001-my-feature",
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
- | `kanban_create` | Add new tasks |
113
- | `kanban_update` | Move, edit, toggle subtasks |
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 (default 5500) |
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(/^(PI-\d+[\w.-]*|BUG-\d+|CHORE-\d+)/);
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
- server.listen(port, 'localhost', () => {
336
- console.log(`\x1b[1;32m→ Kanban GUI: http://localhost:${port}\x1b[0m`);
337
- console.log(` Backlog: ${BACKLOG}`);
338
- console.log(' Ctrl+C żeby zamknąć');
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
- if (!cmd || cmd === 'serve') {
387
- const port = parseInt(args[1] || '5500', 10);
388
- await serveWeb(port);
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 === 'toggle' && args[1] && args[2]) {
452
- await cliToggle(args[1], parseInt(args[2], 10));
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);