local-kanban 1.6.3

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +381 -0
  2. package/LICENSE +21 -0
  3. package/README.md +144 -0
  4. package/README.ru.md +144 -0
  5. package/bin/kb +306 -0
  6. package/bin/local-kanban +217 -0
  7. package/ecosystem.example.cjs +25 -0
  8. package/package.json +52 -0
  9. package/public/app.js +18 -0
  10. package/public/boot-fallback.js +3 -0
  11. package/public/drawer-sync.js +14 -0
  12. package/public/favicon-180.png +0 -0
  13. package/public/favicon-32.png +0 -0
  14. package/public/favicon-64.png +0 -0
  15. package/public/favicon.ico +0 -0
  16. package/public/favicon.svg +6 -0
  17. package/public/icon-192.png +0 -0
  18. package/public/icon-512.png +0 -0
  19. package/public/icon-maskable-192.png +0 -0
  20. package/public/icon-maskable-512.png +0 -0
  21. package/public/index.html +138 -0
  22. package/public/js/board.js +698 -0
  23. package/public/js/chaos.js +247 -0
  24. package/public/js/core.js +821 -0
  25. package/public/js/dash.js +114 -0
  26. package/public/js/drawer.js +919 -0
  27. package/public/js/horizon.js +140 -0
  28. package/public/js/init.js +208 -0
  29. package/public/js/project.js +338 -0
  30. package/public/js/settings.js +905 -0
  31. package/public/js/sidebar.js +640 -0
  32. package/public/js/sse.js +386 -0
  33. package/public/manifest.webmanifest +32 -0
  34. package/public/offline.html +39 -0
  35. package/public/style.css +1247 -0
  36. package/public/sw.js +31 -0
  37. package/scripts/raycast/open-kanban.sh +18 -0
  38. package/scripts/update.js +43 -0
  39. package/skills/deploy/SKILL.md +70 -0
  40. package/skills/kanban/SKILL.md +217 -0
  41. package/src/backup.js +111 -0
  42. package/src/bus.js +10 -0
  43. package/src/config.js +40 -0
  44. package/src/db.js +321 -0
  45. package/src/error-handler.js +11 -0
  46. package/src/file-snapshot.js +22 -0
  47. package/src/origin-guard.js +32 -0
  48. package/src/pm2-services.js +31 -0
  49. package/src/query-params.js +17 -0
  50. package/src/repo-base.js +23 -0
  51. package/src/restore.js +153 -0
  52. package/src/routes/dashboard.js +132 -0
  53. package/src/routes/events.js +30 -0
  54. package/src/routes/horizons.js +66 -0
  55. package/src/routes/projects.js +532 -0
  56. package/src/routes/skills.js +116 -0
  57. package/src/routes/system.js +285 -0
  58. package/src/routes/tasks.js +477 -0
  59. package/src/schema.sql +135 -0
  60. package/src/server.js +100 -0
  61. package/src/skills.js +119 -0
  62. package/src/sync/github.js +265 -0
  63. package/src/sync/worker.js +136 -0
  64. package/src/task-shape.js +29 -0
package/public/sw.js ADDED
@@ -0,0 +1,31 @@
1
+ const OFFLINE_CACHE = 'kb-offline-v1';
2
+ const OFFLINE_URL = '/offline.html';
3
+
4
+ self.addEventListener('install', (e) => {
5
+ e.waitUntil(caches.open(OFFLINE_CACHE).then((c) => c.add(OFFLINE_URL)));
6
+ self.skipWaiting();
7
+ });
8
+
9
+ self.addEventListener('activate', (e) => {
10
+ e.waitUntil((async () => {
11
+ const keys = await caches.keys();
12
+ await Promise.all(keys.filter((k) => k !== OFFLINE_CACHE).map((k) => caches.delete(k)));
13
+ await self.clients.claim();
14
+ })());
15
+ });
16
+
17
+ self.addEventListener('fetch', (e) => {
18
+ const req = e.request;
19
+ if (req.mode !== 'navigate') return;
20
+ e.respondWith((async () => {
21
+ try {
22
+ return await fetch(req);
23
+ } catch {
24
+ return (await caches.match(OFFLINE_URL))
25
+ || new Response('<h1>No connection to the board</h1>', {
26
+ status: 503,
27
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
28
+ });
29
+ }
30
+ })());
31
+ });
@@ -0,0 +1,18 @@
1
+ #!/bin/bash
2
+
3
+ # A Raycast Script Command that opens the board. Written in English on purpose: this file ships in
4
+ # the package, and the @raycast lines below are not prose — Raycast parses them, and without
5
+ # @raycast.title it refuses to register the command at all.
6
+ # Install once, in the Raycast UI:
7
+ # 1. Raycast -> Settings -> Extensions -> Script Commands -> "Add Script Directory"
8
+ # -> pick this folder (.../local-kanban/scripts/raycast).
9
+ # 2. Find "Open Kanban" in Raycast -> Cmd+K -> "Configure Command" -> "Record Hotkey".
10
+ # After that the hotkey opens the board from any application.
11
+ # Running on a different port: change the address on the last line.
12
+ # @raycast.schemaVersion 1
13
+ # @raycast.title Open Kanban
14
+ # @raycast.mode silent
15
+ # @raycast.packageName local-kanban
16
+ # @raycast.icon 📋
17
+ # @raycast.description Open the local kanban board (localhost:3100)
18
+ open "http://localhost:3100"
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync, spawnSync } from 'node:child_process';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
7
+ const run = (cmd, args, opts = {}) =>
8
+ spawnSync(cmd, args, { cwd: ROOT, stdio: 'inherit', ...opts });
9
+ const out = (cmd, args) => {
10
+ try { return execFileSync(cmd, args, { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); }
11
+ catch { return null; }
12
+ };
13
+
14
+ const dirty = out('git', ['status', '--porcelain']);
15
+ if (dirty === null) { console.error('this is not a git repository — update with npm i -g local-kanban@latest'); process.exit(1); }
16
+ if (dirty) {
17
+ console.error('the working tree is not clean — commit or stash your changes first:\n' + dirty);
18
+ process.exit(1);
19
+ }
20
+
21
+ console.log('→ git pull');
22
+ if (run('git', ['pull', '--ff-only']).status !== 0) {
23
+ console.error('git pull failed (local history diverged from origin?) — sort it out by hand');
24
+ process.exit(1);
25
+ }
26
+
27
+ console.log('→ npm install');
28
+ if (run('npm', ['install', '--no-fund', '--no-audit']).status !== 0) process.exit(1);
29
+
30
+ const pm2List = out('pm2', ['jlist']);
31
+ let restarted = false;
32
+ if (pm2List) {
33
+ try {
34
+ const procs = JSON.parse(pm2List.slice(pm2List.indexOf('[')));
35
+ if (procs.some((p) => p.name === 'kanban')) {
36
+ console.log('→ pm2 restart kanban');
37
+ restarted = run('pm2', ['restart', 'kanban']).status === 0;
38
+ }
39
+ } catch { }
40
+ }
41
+ console.log(restarted
42
+ ? '✓ the board is updated and restarted'
43
+ : '✓ the code is updated — restart the server (pm2 restart kanban / local-kanban start)');
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: deploy
3
+ description: Generic deployment for any project on the kanban board — driven entirely by `kb info <proj>` (path, server, pm2, domain), nothing hardcoded. Triggers — "deploy <project>", "ship it", "update it on the server", and the deploy step of the kanban working loop. Flow: review changes → commit → push → pull on the server → install/build → restart → verify. A local project with no server takes the short branch: commit + restart the local pm2 process.
4
+ ---
5
+
6
+ # Generic deploy (driven by kb info)
7
+
8
+ This skill is not tied to any particular server: everything comes from the board's registry. Does
9
+ the project have its own flow? Then it has its own `deploy_skill` in `kb info` — read that
10
+ SKILL.md at `deploy_skill_path` and follow it instead of this one.
11
+
12
+ ## 0. The data
13
+
14
+ ```
15
+ kb info <proj> # path (local copy), server (ssh host), server_path (path on the server),
16
+ # pm2 (process names), domain (for verification)
17
+ ```
18
+ - SSH access uses the alias or address from `server` via `ssh <server>`: keys, users and ports live
19
+ in the user's `~/.ssh/config`. The board stores no secrets.
20
+ - No `server` → the project is local: steps 1–2, then straight to 5 (a local restart if it has pm2).
21
+
22
+ ## 1. Review the changes
23
+
24
+ - `git -C <path> status --porcelain` plus `git -C <path> diff --stat` — what has moved away from HEAD.
25
+ - Do not commit junk (temp files, .DS_Store, stray logs).
26
+ - If the working tree is clean and everything is already pushed, go straight to step 3.
27
+
28
+ ## 2. Commit + push
29
+
30
+ - Commit by meaning, with the task's issue number when the deployment is part of the kanban loop.
31
+ - `git -C <path> push origin <current branch>`.
32
+
33
+ ## 3. Update on the server
34
+
35
+ ```
36
+ ssh <server> "cd <server_path> && git pull --ff-only"
37
+ ```
38
+ - `git pull` failed because of local edits on the server → STOP and tell the human: the server has
39
+ drifted, and overwriting it silently (`reset --hard`) is not allowed. Inspect it with
40
+ `ssh <server> "cd <server_path> && git status --porcelain"`.
41
+
42
+ ## 4. Install / build (by project type — check package.json or the manifest)
43
+
44
+ - Node: `npm ci || npm install`; if there is a build script, `npm run build`.
45
+ - Python: `pip install -r requirements.txt` inside the project's venv.
46
+ - Static sites with no build step — skip.
47
+
48
+ ## 5. Restart
49
+
50
+ - pm2 processes from `kb info`: `ssh <server> "pm2 restart <names>"` (for a local project,
51
+ `pm2 restart <names>` without ssh).
52
+ - No pm2 → whatever the project uses (a systemd unit, docker compose). If it is unclear, ask the
53
+ human instead of guessing.
54
+
55
+ ## 6. Verify
56
+
57
+ - With a `domain`: `curl -sS -o /dev/null -w '%{http_code}' https://<domain>` (or http, whichever
58
+ the project uses) — expect 200/301.
59
+ - Check exactly what you changed: the specific page or endpoint, not just the home page.
60
+ - pm2: `kb svc <proj>` — processes online, restart counts not climbing.
61
+ - Verification failed → discuss the rollback with the human (git revert plus another deploy); never
62
+ roll back silently.
63
+
64
+ ## Rules
65
+
66
+ - Production commands (ssh, rsync, restart) run with the user's confirmation — that is expected,
67
+ do not work around it.
68
+ - The code lives in git: editing directly on the server is forbidden. Spotted drift? Report it,
69
+ do not overwrite it.
70
+ - One task, one deployment; related tasks that are ready together ship in a single run.
@@ -0,0 +1,217 @@
1
+ ---
2
+ name: kanban
3
+ description: Local kanban task board (the `kb` CLI, localhost:3100). Triggers — "what's on the board", "add a task", "work through project X", "do TIC-3", "take the next task", "accept TIC-3", "send TIC-3 back", "show the board", "block this task". Statuses and comments are mirrored to GitHub automatically (when sync is configured) — you never have to think about it.
4
+ ---
5
+
6
+ # Kanban (kb)
7
+
8
+ Every task lives in a local SQLite database behind the `kb` CLI. GitHub (issues + Projects v2) is a mirror; the sync is optional and runs in the background — ignore it.
9
+ Tasks are addressed by key: `TIC-3` (project prefix + number). Case does not matter.
10
+
11
+ Statuses: `backlog` → `todo` → `prep` → `doing` → `deploy` → `review` → `done` (+ `cancelled`). Blocked is a flag.
12
+ `prep` is where you study the task before writing code (reading CLAUDE.md, reading files). The work timer does NOT run there — only `doing` and `deploy` count as work. `kb take` puts a task in `prep`; the moment you start writing code, run `kb mv <key> doing`.
13
+
14
+ ## Commands
15
+
16
+ ```
17
+ kb take <proj> # take the next task from todo → prep; prints the description,
18
+ # the issue number (for commit messages), the last note from the
19
+ # human (if the task was sent back, act on it!) and the paths of
20
+ # attached screenshots as "📎 <path>" — OPEN those with Read,
21
+ # they are context for the task.
22
+ # started writing code → kb mv <key> doing.
23
+ kb review <key> "note" # leave a note and move to review. NOT atomic: the note is stored
24
+ # EVEN IF the move is rejected (backlog and todo cannot go straight
25
+ # to review — go through prep or doing first; prep→review is fine).
26
+ # Comments are not de-duplicated, so don't call this on a task that
27
+ # never went through doing.
28
+ kb p # projects: slug (PREFIX) b0 t2 pp0 d1 dp0 r1 ✓5 !1 (pp = prep)
29
+ kb ls <proj> [-s st] [-a] # tasks (without -a, backlog/done/cancelled are hidden)
30
+ kb show <key> # details + issue + latest comments
31
+ kb add <proj> "title" [-d "desc"] [-p high|med|low] [-s todo] # goes to backlog by default!
32
+ kb mv <key> todo|prep|doing|deploy|review|done|cancelled
33
+ kb done <key> · kb cancel <key> · kb pri <key> high|med|low|none
34
+ kb lbl <key> +bug -ui # labels: bug docs duplicate enhancement ui feature security noclaude ask …
35
+ # noclaude = done by hand (without Claude) → counted separately on the dashboard
36
+ # ask = ask clarifying questions and WAIT for an answer before starting
37
+ # (see Special cases)
38
+ # ⚠️ only labels from the palette (LABELS in github.js): kb lbl / kb add
39
+ # reject an unknown label immediately (exit code 2). A genuinely new
40
+ # label goes into the LABELS palette first, and into this file.
41
+ kb desc <key> "description" # set or overwrite a task description (used when grooming the backlog)
42
+ kb note <key> "text" # a standalone comment (rarely needed — kb review covers it)
43
+ kb link <key> <key> # link two tasks (the board draws a line) · kb unlink to unlink
44
+ kb block <key> "why" · kb unblock <key>
45
+ kb info <proj> # path, deploy_skill, server, pm2, domain
46
+ kb svc <proj> # live status of the project's services (local pm2 or your own PANEL_URL)
47
+ kb sync # state of the sync queue (or "sync is off" in local-only mode)
48
+ ```
49
+
50
+ ## A new project goes on the board FIRST (hard rule)
51
+
52
+ **The moment you create or start a new project — a new folder, site, bot, service — register it
53
+ on the board before writing any task or any code for it.** Otherwise the work happens off the
54
+ board: it has no task key and no deploy metadata. The board does NOT register projects on its own:
55
+ local folders under `LOCAL_ROOT` (`~/claude-projects`) appear in the sidebar with an "+ add" row,
56
+ but a project living anywhere else has to be registered by hand.
57
+
58
+ Register it through the API rather than raw SQL — the API emits an SSE event so the board updates
59
+ live, and the GitHub project is provisioned on the first task:
60
+
61
+ ```python
62
+ # python3, so quoting doesn't get in the way
63
+ import json, urllib.request
64
+ body = {
65
+ "slug": "myproj", "name": "myproj.com",
66
+ "path": "<path to the project's code>",
67
+ "deploy_skill": "deploy", # or empty if there is no deployment
68
+ "server": "<host>", "domain": "myproj.com",
69
+ "category": "<sidebar group>",
70
+ "description": "…",
71
+ }
72
+ req = urllib.request.Request("http://localhost:3100/api/projects",
73
+ data=json.dumps(body).encode(), method="POST",
74
+ headers={"Content-Type": "application/json"})
75
+ print(urllib.request.urlopen(req, timeout=10).read().decode()) # 201 + the object, including prefix
76
+ ```
77
+
78
+ Registry fields (PATCHABLE): `name, prefix, path, server_path, deploy_skill, server,
79
+ pm2_services, domain, category, description, pinned, archived`. `slug` and `name` are required;
80
+ `prefix` (the `PREFIX-N` task key) is generated for you. Edit later with
81
+ `PATCH /api/projects/:slug`. When a human registers a project by hand, their path is the
82
+ "+ Add project" wizard at the bottom of the sidebar; the API snippet above is yours.
83
+
84
+ **Verify it landed (do not skip):** `kb info <slug>` (path/deploy_skill/server/domain filled in),
85
+ `kb p | grep <slug>` (visible on the board), `kb sync` (the queue is not stuck). Only then create
86
+ tasks (`kb add`) and start working.
87
+
88
+ ## The working loop ("work through project X")
89
+
90
+ Claude takes a task as far as *review* and stops. Only the user moves anything to done/cancelled.
91
+
92
+ **When the prompt lists specific keys** (the 📋 button on the "To do" column produces them) those
93
+ are SPECIFIC tasks, not a queue. Take each one with `kb show <key>` (NOT `kb take`, which pulls
94
+ ONE next task from the project's `todo`) and **move it to prep right away: `kb mv <key> prep`** —
95
+ `kb show` does not change the status. If `kb show` printed `> 👤 Send back: …`, the task was
96
+ returned: open the `📎` screenshots with Read and act on that note FIRST. Then follow steps 2–6
97
+ for each. Use `kb take X` only when asked to "work through project X" without specific keys.
98
+
99
+ 1. `kb take X` — take a task (lands in prep; description and issue are already in the output).
100
+ "empty" → report it and stop. `> 👤 …` is a note from a send-back, act on it first.
101
+ `📎 <path>` — open the screenshot with Read.
102
+ 2. **HARD RULE — starting a task (never skip, even for a one-line change).** `kb info X` → go to
103
+ `path`. Before any code you MUST read: (a) the project's `CLAUDE.md` if it has one — **and if
104
+ it does not, find where the project keeps its architecture or research document (`README.md`,
105
+ `EXPERIMENTS.md`, `docs/`) and read that; apply the "doc in the same commit" rule to it**;
106
+ (b) the deploy skill at `deploy_skill_path` from `kb info`, if one is set. The matching commit
107
+ rule: **if you change code whose behaviour is documented in the project's `CLAUDE.md`, update
108
+ that section in the SAME commit.** When you edit a document with a script, gate the commit on
109
+ the script succeeding in one chain (`python … && git add … && git commit …`): a script that
110
+ failed on a missing anchor will not stop the next shell command, and the commit would go out
111
+ without the document. Started writing code → `kb mv X doing` (the timer starts).
112
+ 3. Reference the task's issue in commit messages: `git commit -m "... (#<N>)"` (or `owner/repo#<N>`
113
+ when you commit outside the issues repo) — N comes from the `take`/`show` output. The board
114
+ attaches commit stats to the task on the move to `review`: it looks for a commit that really
115
+ references the issue (`git log -1 --grep=#N`). **When sync is off** (the task has no issue
116
+ number) stats come from the last commit made while the task was in progress, so move the task
117
+ to `doing` BEFORE its commits. Commit with `git -C <path>` or after `cd <path>` — the shell's
118
+ cwd may be outside the repository. A project not under git simply skips the commit step; that
119
+ is normal. **Several tasks touching one file** → either one commit referencing both issues
120
+ (`… (#A, #B)`), or the "revert the hunk → commit → restore it" trick; with 3+ interleaved
121
+ changes, slice the full diff into per-issue patch files and apply them with `git apply`,
122
+ committing in between.
123
+ 4. If the project has a `deploy_skill`: `kb mv <key> deploy` → run the deployment with that skill →
124
+ step 5. Without a deploy_skill, go straight to step 5. Take the skill from `deploy_skill_path`
125
+ in `kb info` (if the skill is not in the available list, just read its SKILL.md at that path).
126
+ The generic `deploy` skill ships with the board (`skills/deploy/`) and works purely off
127
+ `kb info` data. Production commands (ssh/rsync) will ask for confirmation — that is expected.
128
+ 5. `kb review <key> "<two plain sentences: 1) what the problem was 2) how you solved it>"` → take
129
+ the next one.
130
+ 6. Stuck, or the decision is the human's → `kb block <key> "reason"` and move on.
131
+ 7. At the end, a one-line summary: how many in review, how many blocked.
132
+
133
+ ## Grooming the backlog
134
+
135
+ Triggered by a prompt like `Groom the backlog [of X] (grooming mode):` with a list of keys (the 📋
136
+ button on the "Backlog" column). This is NOT "do the tasks" — no code is written, no statuses move,
137
+ **every task stays in the backlog**. The goal is to declutter and prepare.
138
+
139
+ For each key: 1) `kb show` — title, description, comments; 2) no description → `kb desc <key> "…"`
140
+ (one or two sentences, invent nothing); 3) classify: `kb pri` by meaning (unclear — leave it) plus
141
+ `kb lbl`; 4) anything unclear → ask via `kb note`, do not guess; 5) do not change the status.
142
+ One line of summary at the end.
143
+
144
+ ## User commands for a task in review
145
+
146
+ - "accept TIC-3" → `kb done TIC-3`.
147
+ - "send TIC-3 back: <what's wrong>" → `kb unblock` if needed, `kb mv TIC-3 doing`, fix it, then the
148
+ loop again from step 4. **First REPRODUCE the exact scenario from the note** (curl or a browser,
149
+ following the real flow) and confirm the actual cause — only then fix it. A second attempt at a
150
+ returned task often cures a plausible but wrong cause.
151
+ - "cancel TIC-3" → `kb cancel TIC-3`.
152
+ - "do TIC-3" (a specific task in any status except done) → `kb show TIC-3` (the `👤` note comes
153
+ first; open `📎` screenshots with Read) and **`kb mv TIC-3 prep` right away**, then steps 2–6.
154
+
155
+ ## Special cases
156
+
157
+ - **The `ask` label on a task** (visible in `kb show`/`kb take` output) → this is a BLOCKING step:
158
+ study the task in `prep`, then ask your clarifying questions in chat — how you understood the
159
+ task, what you intend to change and where — EVEN IF everything looks obvious, and WAIT for an
160
+ answer. Do not move to `doing` or write code until the human replies.
161
+ - **You found an unrelated problem along the way** → do not fix it silently. Create a task
162
+ (`kb add <proj> "..." -d "..."`) and ask in chat: do it now, defer it, or is it for the human.
163
+ - **Related tasks solved together** → take both at once (both in doing), fix them in one pass and
164
+ link them with `kb link`.
165
+ - **A discussion or question task** (no code): study it in `prep`, put the full answer in
166
+ `kb note <key> "..."`, then `kb mv <key> review`. You write no code and do not move it to done.
167
+ - A "check whether the service is alive" task → `kb svc <proj>` (one line per service, no ssh).
168
+ - Work time is measured for you: doing/deploy runs the timer, review/done stops it.
169
+ - Verifying the result: text and config changes — curl/grep (cheap). Compiled projects — **a build**
170
+ is the cheap check (`cargo build`, `go build ./...`, `tsc --noEmit`, …). **For layout and visual
171
+ changes a screenshot IS the cheap check** — curl and grep never catch a broken layout: serve the
172
+ project from its root and screenshot the page with a headless browser (playwright), on desktop
173
+ (1280) and on mobile (390/360). Mobile "cut off" bugs are horizontal overflow — compare
174
+ `scrollWidth` with `clientWidth` and find elements whose `getBoundingClientRect().right >
175
+ clientWidth`, ignoring those inside an `overflow-x:auto` ancestor.
176
+ - **Driving the board's own UI in a browser** (drag & drop, drawer, SSE): use playwright directly,
177
+ `page.goto('http://localhost:3100', { waitUntil: 'domcontentloaded' })` — NOT `networkidle`, the
178
+ SSE connection never closes. Wait for `waitForSelector('#projects .proj')`. **The dashboard is
179
+ what opens on start**, so click a project in the sidebar first, then wait for `.col-body .card`.
180
+ Top-level functions are exposed on `window` (`openDrawer`, `getSetting`, …); `state` is not —
181
+ read state from the DOM. Test mutations on a throwaway task (`POST /api/tasks` → check →
182
+ `DELETE /api/tasks/<key>`); task numbers are not reused, which is normal. For copy buttons, grant
183
+ the clipboard permissions to the browser context or `readText()` silently returns empty. Call
184
+ actions that open a confirmation modal without returning the promise:
185
+ `page.evaluate((k) => { window.deleteTask(k); })` — returning it deadlocks; click the modal as a
186
+ separate step.
187
+
188
+ ## Token rules (hard)
189
+
190
+ - Never dump the whole board into the chat. One action, one line of `kb` output, no retelling.
191
+ - `kb show` only when you need the description in order to work.
192
+ - **Going into the database directly: it is `data/kanban.db`, NOT a `kanban.db` in the repo root.**
193
+ Gotcha: `sqlite3 <path>` SILENTLY creates an empty file when nothing is there, so a typo leaves a
194
+ junk 0-byte `.db` in a clean repository. Check the path before running it, and look at
195
+ `git status` after any sqlite session.
196
+ - Read task details with `kb show`/`kb take`, not `curl /api/tasks…|jq`: descriptions contain raw
197
+ newlines and a shell JSON parser chokes on them. If you truly need the API, save the response to
198
+ a file and parse it leniently (`python3 … strict=False`).
199
+ - **Bulk-create tasks or comments with a script** (`subprocess.run([kb,'add',…])` / `execFile`)
200
+ rather than assembling shell commands: backticks, quotes and newlines get mangled by the shell.
201
+ `kb add` prints the key as the first word of its output — take it and attach labels with `kb lbl`.
202
+ - **Bulk moves (`kb mv`/`kb review`) from a script must check `returncode`, not printed output**: a
203
+ rejected move exits non-zero, yet `kb review` still stores the comment. Verify with `kb p` at the
204
+ end that the counts add up.
205
+ - Removed an element from a page — verify with grep on **the text or markup**, not the class name
206
+ (the CSS rule of the same name stays behind).
207
+ - The review comment is **exactly two plain sentences, no jargon**: (1) what the problem was,
208
+ (2) how you solved it.
209
+ - The final chat message is one line per task: `TIC-3 review — summary board is ready`.
210
+ - `kb: board is not responding` → restart the board's server (`pm2 restart kanban` / `npm start`),
211
+ wait two seconds and retry once. **Exit codes are distinct: `1` — the board is down (a restart
212
+ and a retry make sense), `2` — the request was rejected (retrying is pointless, read the
213
+ message), `3` — `kb` itself broke (stack trace under `KB_DEBUG=1`), `4` — the board answered,
214
+ but with a failure of its own (5xx) or with something that is not JSON. Gate on the code, not on
215
+ the message text.** Code `4` is worth reading closely: it means the board falls over on this
216
+ request, or `KB_URL` points at something that is not the board at all.
217
+ - The web board is for the human: http://localhost:3100 — do not open it, do not scrape it.
package/src/backup.js ADDED
@@ -0,0 +1,111 @@
1
+ import { cpSync, mkdirSync, existsSync, readdirSync, unlinkSync, renameSync, rmSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { db, DATA_DIR, logError } from './db.js';
4
+
5
+ const DIR = join(DATA_DIR, 'backups');
6
+
7
+ const KEEP_DAILY = 7;
8
+ const KEEP_MANUAL = 5;
9
+ const DAILY_RE = /^kanban-\d{4}-\d{2}-\d{2}\.db$/;
10
+ const MANUAL_RE = /^kanban-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.db$/;
11
+
12
+ function backupFiles(re) {
13
+ if (!existsSync(DIR)) return [];
14
+ return readdirSync(DIR).filter((f) => re.test(f)).sort();
15
+ }
16
+
17
+ function rotate(re, keep) {
18
+ const files = backupFiles(re);
19
+ while (files.length > keep) unlinkSync(join(DIR, files.shift()));
20
+ }
21
+
22
+ let tmpSeq = 0;
23
+ let running = null;
24
+ async function snapshot(path) {
25
+ while (running) { try { await running; } catch { } }
26
+ const done = (async () => {
27
+ const tmp = `${path}.${process.pid}.${++tmpSeq}.part`;
28
+ rmSync(tmp, { force: true });
29
+ try {
30
+ await db.backup(tmp);
31
+ renameSync(tmp, path);
32
+ } catch (e) {
33
+ rmSync(tmp, { force: true });
34
+ throw e;
35
+ }
36
+ return path;
37
+ })();
38
+ running = done;
39
+ try { return await done; } finally { if (running === done) running = null; }
40
+ }
41
+
42
+ export const ATTACH_MIRROR = join(DIR, 'attachments');
43
+ function mirrorAttachments() {
44
+ const src = join(DATA_DIR, 'attachments');
45
+ if (!existsSync(src)) return;
46
+ cpSync(src, ATTACH_MIRROR, { recursive: true, force: false, errorOnExist: false });
47
+ }
48
+
49
+ export function listBackups() {
50
+ const rows = [
51
+ ...backupFiles(DAILY_RE).map((name) => ({ name, kind: 'daily' })),
52
+ ...backupFiles(MANUAL_RE).map((name) => ({ name, kind: 'manual' })),
53
+ ];
54
+ return rows
55
+ .map(({ name, kind }) => {
56
+ const s = statSync(join(DIR, name));
57
+ return { name, kind, size: s.size, mtime: s.mtime.toISOString() };
58
+ })
59
+ .sort((a, b) => b.mtime.localeCompare(a.mtime));
60
+ }
61
+
62
+ export function backupPath(name) {
63
+ const found = listBackups().find((b) => b.name === name);
64
+ return found ? join(DIR, found.name) : null;
65
+ }
66
+
67
+ export const backupState = { last_ok: null, last_error: null, last_error_at: null };
68
+
69
+ export async function backupNow() {
70
+ mkdirSync(DIR, { recursive: true });
71
+ const path = join(DIR, `kanban-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.db`);
72
+ await snapshot(path);
73
+ mirrorAttachments();
74
+ rotate(MANUAL_RE, KEEP_MANUAL);
75
+ backupState.last_ok = new Date().toISOString();
76
+ return path;
77
+ }
78
+
79
+ const BACKUP_EVERY_MS = 6 * 3600 * 1000;
80
+ const BACKUP_RETRY_MS = 10 * 60 * 1000;
81
+ export function startBackups() {
82
+ mkdirSync(DIR, { recursive: true });
83
+ let timer = null;
84
+ const again = (ms) => { clearTimeout(timer); timer = setTimeout(run, ms); timer.unref?.(); };
85
+ function run() {
86
+ const path = join(DIR, `kanban-${new Date().toISOString().slice(0, 10)}.db`);
87
+ if (existsSync(path)) {
88
+ mirrorAttachments();
89
+ backupState.last_ok = backupState.last_ok || statSync(path).mtime.toISOString();
90
+ again(BACKUP_EVERY_MS);
91
+ return;
92
+ }
93
+ snapshot(path)
94
+ .then(() => {
95
+ mirrorAttachments();
96
+ rotate(DAILY_RE, KEEP_DAILY);
97
+ backupState.last_ok = new Date().toISOString();
98
+ backupState.last_error = null;
99
+ console.log(`backup: ${path}`);
100
+ again(BACKUP_EVERY_MS);
101
+ })
102
+ .catch((e) => {
103
+ backupState.last_error = e.message;
104
+ backupState.last_error_at = new Date().toISOString();
105
+ logError('server', 'backup', `daily backup failed: ${e.message}`, e.stack);
106
+ console.error('backup failed:', e.message);
107
+ again(BACKUP_RETRY_MS);
108
+ });
109
+ }
110
+ run();
111
+ }
package/src/bus.js ADDED
@@ -0,0 +1,10 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { sseMax } from './config.js';
3
+
4
+ export const bus = new EventEmitter();
5
+ export const SSE_MAX = sseMax();
6
+ bus.setMaxListeners(SSE_MAX + 10);
7
+
8
+ export function emit(type, data) {
9
+ bus.emit('event', { type, data });
10
+ }
package/src/config.js ADDED
@@ -0,0 +1,40 @@
1
+ import { join, dirname, resolve, sep } from 'node:path';
2
+ import { homedir } from 'node:os';
3
+ import { existsSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ export const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
7
+
8
+ const packaged = ROOT.split(sep).includes('node_modules');
9
+ const defaultDataDir = () => {
10
+ if (existsSync(join(ROOT, 'data', 'kanban.db'))) return join(ROOT, 'data');
11
+ return packaged ? join(homedir(), '.local-kanban') : join(ROOT, 'data');
12
+ };
13
+
14
+ const env = (...names) => {
15
+ for (const n of names) {
16
+ const v = process.env[n];
17
+ if (v !== undefined && v !== '') return v;
18
+ }
19
+ return '';
20
+ };
21
+
22
+ export function expandPath(p) {
23
+ if (!p) return p;
24
+ const home = p === '~' || p.startsWith('~/') ? join(homedir(), p.slice(1)) : p;
25
+ return resolve(home);
26
+ }
27
+
28
+ export const DATA_DIR = expandPath(env('KB_DATA_DIR')) || defaultDataDir();
29
+ export const PORT = Number(env('PORT') || 3100);
30
+
31
+ export const localRoot = () => expandPath(env('KB_LOCAL_ROOT'))
32
+ || (env('KB_DATA_DIR') ? join(DATA_DIR, 'projects') : join(homedir(), 'claude-projects'));
33
+ export const panelUrl = () => env('KB_PANEL_URL', 'PANEL_URL');
34
+ export const panelInfo = () => env('KB_PANEL_INFO', 'PANEL_INFO');
35
+ export const skillsExtra = () => env('KB_SKILLS_EXTRA').split(':').filter(Boolean);
36
+ export const sseMax = () => Number(env('KB_SSE_MAX')) || 20;
37
+ export const allowedHosts = () => env('KB_ALLOWED_HOSTS').split(',').map((s) => s.trim()).filter(Boolean);
38
+ export const langOverride = () => env('KB_LANG');
39
+ export const ghOwnerEnv = () => env('KB_GH_OWNER');
40
+ export const ghRepoEnv = () => env('KB_GH_REPO');