claude-code-kanban 4.28.0 → 4.30.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 +3 -2
- package/cli.js +383 -70
- package/install.js +7 -8
- package/lib/approvals.js +2 -2
- package/lib/contain.js +2 -2
- package/lib/dispatch-groups.js +28 -11
- package/lib/dispatch.js +1 -0
- package/lib/file-url.js +1 -1
- package/lib/inline-assets.js +2 -2
- package/lib/linked-docs.js +56 -0
- package/lib/net-guard.js +1 -1
- package/lib/open-editor.js +5 -3
- package/lib/parent-cache.js +58 -0
- package/lib/parsers.js +175 -168
- package/lib/priority.js +45 -0
- package/lib/session-cache.js +34 -0
- package/lib/session-events.js +2 -1
- package/lib/task-counts.js +90 -0
- package/lib/terminal.js +53 -4
- package/package.json +2 -1
- package/plugin/plugins/claude-code-kanban/scripts/postman.js +3 -0
- package/plugin/plugins/claude-code-kanban/skills/kanban/SKILL.md +16 -46
- package/public/app.js +563 -313
- package/public/index.html +46 -23
- package/public/project-match.js +27 -0
- package/public/style.css +306 -94
- package/public/sw.js +1 -1
- package/server.js +167 -74
- package/skill-guides/dispatch.md +5 -4
package/lib/priority.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Windows boosts the threads of the foreground window, and a process behind a hidden ConPTY never
|
|
4
|
+
// gets that, so the one the user types into competes on equal terms with every busy agent.
|
|
5
|
+
// Elsewhere a raise needs root, and a raised nice value would pass on to every child, so this is
|
|
6
|
+
// Windows only. There a child of an above-normal process starts at normal, so the tools it runs
|
|
7
|
+
// stay unboosted.
|
|
8
|
+
const { execFile } = require('node:child_process');
|
|
9
|
+
const os = require('node:os');
|
|
10
|
+
|
|
11
|
+
const { PRIORITY_ABOVE_NORMAL } = os.constants.priority;
|
|
12
|
+
|
|
13
|
+
function enabled(platform, env) {
|
|
14
|
+
return platform === 'win32' && env.CCK_PRIORITY_BOOST !== '0';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function raise(pid = 0, platform = process.platform, env = process.env) {
|
|
18
|
+
if (!enabled(platform, env)) return null;
|
|
19
|
+
try {
|
|
20
|
+
const before = os.getPriority(pid);
|
|
21
|
+
if (before <= PRIORITY_ABOVE_NORMAL) return null;
|
|
22
|
+
os.setPriority(pid, PRIORITY_ABOVE_NORMAL);
|
|
23
|
+
return before;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function restore(pid, before) {
|
|
30
|
+
try { os.setPriority(pid, before); } catch { /* the process is gone */ }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ConPTY's console host relays every byte between cck and the shell, and node-pty does not expose
|
|
34
|
+
// its pid. Under load a normal-priority host starves for seconds, until Windows' anti-starvation
|
|
35
|
+
// boost picks it up.
|
|
36
|
+
function raiseConsoleHosts(done) {
|
|
37
|
+
if (!enabled(process.platform, process.env)) return done?.();
|
|
38
|
+
const query = `(Get-CimInstance Win32_Process -Filter "ParentProcessId=${process.pid} AND (Name='conhost.exe' OR Name='OpenConsole.exe')").ProcessId`;
|
|
39
|
+
execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', query], { windowsHide: true, timeout: 30000 }, (err, stdout) => {
|
|
40
|
+
if (!err) for (const pid of stdout.split(/\s+/).filter(Boolean)) raise(Number(pid));
|
|
41
|
+
done?.();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { raise, restore, raiseConsoleHosts };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const { exportSessionCaches, importSessionCaches, sessionCachesDirty } = require('./parsers');
|
|
3
|
+
const { exportParentVerdicts, importParentVerdicts, parentVerdictsDirty } = require('./parent-cache');
|
|
4
|
+
const { exportTaskCounts, importTaskCounts, taskCountsDirty } = require('./task-counts');
|
|
5
|
+
|
|
6
|
+
// Bump when an entry's shape or meaning changes: a file with another version is ignored.
|
|
7
|
+
const VERSION = 2;
|
|
8
|
+
// A cache this large is not one cck wrote for a normal set of transcripts. Rebuild it cold.
|
|
9
|
+
const MAX_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
function loadSessionCache(file) {
|
|
12
|
+
try {
|
|
13
|
+
if (fs.statSync(file).size > MAX_BYTES) {
|
|
14
|
+
fs.rmSync(file, { force: true });
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
18
|
+
if (data?.version !== VERSION) return false;
|
|
19
|
+
importSessionCaches(data);
|
|
20
|
+
importParentVerdicts(data.parents);
|
|
21
|
+
importTaskCounts(data.taskCounts);
|
|
22
|
+
return true;
|
|
23
|
+
} catch (_) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// write(data) must replace the file atomically; server.js passes writeJsonAtomic.
|
|
29
|
+
function saveSessionCache(write) {
|
|
30
|
+
if (!sessionCachesDirty() && !parentVerdictsDirty() && !taskCountsDirty()) return;
|
|
31
|
+
write({ version: VERSION, ...exportSessionCaches(), parents: exportParentVerdicts(), taskCounts: exportTaskCounts() });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { loadSessionCache, saveSessionCache };
|
package/lib/session-events.js
CHANGED
|
@@ -25,6 +25,7 @@ const sessionEventBuckets = new Map();
|
|
|
25
25
|
// One cap for the whole line rather than one per field, because the description comes
|
|
26
26
|
// last: truncation eats its tail first and leaves the machine-readable head intact.
|
|
27
27
|
function sanitizeEventLine(line) {
|
|
28
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: strips control characters on purpose
|
|
28
29
|
return line.replace(/[\x00-\x1f\x7f]/g, ' ').trim().slice(0, 1500);
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -81,7 +82,7 @@ function handleSessionEvents(req, res) {
|
|
|
81
82
|
// bucket is still evicted from the map.
|
|
82
83
|
if (bucket && req.query.first === '1') drain(sessionId, bucket);
|
|
83
84
|
|
|
84
|
-
if (bucket
|
|
85
|
+
if (bucket?.queue.length) return res.json({ events: drain(sessionId, bucket) });
|
|
85
86
|
if (!wait) return res.json({ events: [] });
|
|
86
87
|
|
|
87
88
|
const pending = bucket || { queue: [], waiters: new Set() };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
// A task file's status is reused while its size, mtime and inode are unchanged. An mtime can be
|
|
5
|
+
// as coarse as 2 s (FAT), so a file read within that window of its last write could be written
|
|
6
|
+
// again with the same stamp and size. Such a file is not stored and is read again next time.
|
|
7
|
+
const RACY_MS = 3000;
|
|
8
|
+
const SKIP = 'skip';
|
|
9
|
+
const STATUSES = new Set(['completed', 'in_progress', 'pending', SKIP]);
|
|
10
|
+
|
|
11
|
+
const dirs = new Map();
|
|
12
|
+
let dirty = false;
|
|
13
|
+
|
|
14
|
+
function readStatus(taskPath) {
|
|
15
|
+
const text = fs.readFileSync(taskPath, 'utf8');
|
|
16
|
+
let task;
|
|
17
|
+
try {
|
|
18
|
+
task = JSON.parse(text);
|
|
19
|
+
} catch (_) {
|
|
20
|
+
return SKIP;
|
|
21
|
+
}
|
|
22
|
+
if (task == null || task.metadata?._internal) return SKIP;
|
|
23
|
+
if (task.status === 'completed' || task.status === 'in_progress') return task.status;
|
|
24
|
+
return 'pending';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function countTaskDir(dir) {
|
|
28
|
+
const prev = dirs.get(dir);
|
|
29
|
+
const next = new Map();
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const tally = { completed: 0, in_progress: 0, pending: 0 };
|
|
32
|
+
let newestTaskMtime = null;
|
|
33
|
+
|
|
34
|
+
for (const file of fs.readdirSync(dir).filter((f) => f.endsWith('.json'))) {
|
|
35
|
+
const taskPath = path.join(dir, file);
|
|
36
|
+
let st;
|
|
37
|
+
try {
|
|
38
|
+
st = fs.statSync(taskPath);
|
|
39
|
+
} catch (_) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const e = prev?.get(file);
|
|
43
|
+
let status;
|
|
44
|
+
if (e && e.size === st.size && e.mtimeMs === st.mtimeMs && e.ino === st.ino) {
|
|
45
|
+
status = e.status;
|
|
46
|
+
next.set(file, e);
|
|
47
|
+
} else {
|
|
48
|
+
try {
|
|
49
|
+
status = readStatus(taskPath);
|
|
50
|
+
} catch (_) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (now - st.mtimeMs >= RACY_MS) {
|
|
54
|
+
next.set(file, { size: st.size, mtimeMs: st.mtimeMs, ino: st.ino, status });
|
|
55
|
+
dirty = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (status === SKIP) continue;
|
|
59
|
+
tally[status]++;
|
|
60
|
+
if (!newestTaskMtime || st.mtime > newestTaskMtime) newestTaskMtime = st.mtime;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (prev && prev.size !== next.size) dirty = true;
|
|
64
|
+
dirs.set(dir, next);
|
|
65
|
+
return { completed: tally.completed, inProgress: tally.in_progress, pending: tally.pending, newestTaskMtime };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const taskCountsDirty = () => dirty;
|
|
69
|
+
|
|
70
|
+
function exportTaskCounts() {
|
|
71
|
+
dirty = false;
|
|
72
|
+
return [...dirs].filter(([dir]) => fs.existsSync(dir)).map(([dir, files]) => [dir, [...files]]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function importTaskCounts(entries) {
|
|
76
|
+
if (!Array.isArray(entries)) return;
|
|
77
|
+
const num = (x) => typeof x === 'number';
|
|
78
|
+
for (const pair of entries) {
|
|
79
|
+
if (typeof pair?.[0] !== 'string' || !Array.isArray(pair[1])) continue;
|
|
80
|
+
const files = new Map();
|
|
81
|
+
for (const item of pair[1]) {
|
|
82
|
+
const [file, e] = Array.isArray(item) ? item : [];
|
|
83
|
+
if (typeof file !== 'string' || !e || !num(e.size) || !num(e.mtimeMs) || !num(e.ino) || !STATUSES.has(e.status)) continue;
|
|
84
|
+
files.set(file, e);
|
|
85
|
+
}
|
|
86
|
+
dirs.set(pair[0], files);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { countTaskDir, taskCountsDirty, exportTaskCounts, importTaskCounts };
|
package/lib/terminal.js
CHANGED
|
@@ -13,6 +13,7 @@ const fs = require('node:fs');
|
|
|
13
13
|
const os = require('node:os');
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const { isContained } = require('./contain');
|
|
16
|
+
const priority = require('./priority');
|
|
16
17
|
|
|
17
18
|
const WS_PATH = '/api/terminal/ws';
|
|
18
19
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -32,6 +33,7 @@ const PROMPT_QUIET_MS = 400;
|
|
|
32
33
|
const HIGH_WATER = 128 * 1024;
|
|
33
34
|
const LOW_WATER = 16 * 1024;
|
|
34
35
|
const HELLO_TIMEOUT_MS = 5000;
|
|
36
|
+
const BOOST_TRIES = 30;
|
|
35
37
|
const MAX_PAYLOAD = 1024 * 1024;
|
|
36
38
|
// The hub hands its children PORT=0 and friends; a dev server started in the terminal
|
|
37
39
|
// would otherwise bind a random port, and a nested `claude` would think it runs inside one.
|
|
@@ -58,7 +60,7 @@ function readTerminalConfig({ argv = process.argv, env = process.env, getArgValu
|
|
|
58
60
|
return {
|
|
59
61
|
enabled: argv.includes('--enable-terminal') || cfg.enabled === true,
|
|
60
62
|
shell:
|
|
61
|
-
|
|
63
|
+
getArgValue?.('terminal-shell') ||
|
|
62
64
|
env.CCK_TERMINAL_SHELL ||
|
|
63
65
|
(typeof cfg.shell === 'string' && cfg.shell) ||
|
|
64
66
|
null,
|
|
@@ -149,6 +151,7 @@ function parseNewSpec(msg) {
|
|
|
149
151
|
if (typeof worktree !== 'boolean' && !(typeof worktree === 'string' && WORKTREE_RE.test(worktree))) return 'worktree name';
|
|
150
152
|
const model = msg.model || null;
|
|
151
153
|
if (model && !MODELS.has(model)) return 'model';
|
|
154
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: strips ESC so a prompt cannot carry terminal sequences
|
|
152
155
|
const prompt = typeof msg.prompt === 'string' ? msg.prompt.replace(/\x1b/g, '').trim() : '';
|
|
153
156
|
if (prompt.length > MAX_PROMPT) return 'prompt';
|
|
154
157
|
return { cwd: msg.cwd, name: name || null, worktree, model, prompt: prompt || null };
|
|
@@ -177,11 +180,15 @@ function screenText(term) {
|
|
|
177
180
|
|
|
178
181
|
// Claude Code keeps .claude.json beside ~/.claude when CLAUDE_CONFIG_DIR is unset but
|
|
179
182
|
// inside the dir when set, so the default dir must stay unset, not be spelled out.
|
|
180
|
-
|
|
183
|
+
// CCK_URL pins the CLI and the postman to this board: two boards on one config dir share
|
|
184
|
+
// one server.json, and the last one started owns it.
|
|
185
|
+
function ptyEnv({ claudeDir, isDefaultDir, noFlicker, cckUrl }) {
|
|
181
186
|
const env = { ...process.env };
|
|
182
187
|
for (const k of STRIP_ENV) delete env[k];
|
|
183
188
|
if (isDefaultDir) delete env.CLAUDE_CONFIG_DIR;
|
|
184
189
|
else env.CLAUDE_CONFIG_DIR = claudeDir;
|
|
190
|
+
if (cckUrl) env.CCK_URL = cckUrl;
|
|
191
|
+
else delete env.CCK_URL;
|
|
185
192
|
// xterm.js #5801: a clear inside a synchronized-output block jumps the viewport.
|
|
186
193
|
if (noFlicker) env.CLAUDE_CODE_NO_FLICKER = '1';
|
|
187
194
|
env.TERM = 'xterm-256color';
|
|
@@ -224,6 +231,7 @@ function refuse(socket, status, reason) {
|
|
|
224
231
|
*/
|
|
225
232
|
function createTerminalService(o) {
|
|
226
233
|
const { config, net } = o;
|
|
234
|
+
if (config.enabled) priority.raise();
|
|
227
235
|
const token = o.token || crypto.randomBytes(32).toString('hex');
|
|
228
236
|
const sessions = new Map();
|
|
229
237
|
let pty = null;
|
|
@@ -302,9 +310,46 @@ function createTerminalService(o) {
|
|
|
302
310
|
};
|
|
303
311
|
}
|
|
304
312
|
|
|
313
|
+
// The process a viewer types into: claude, found through its registry entry, or the shell.
|
|
314
|
+
function inputPid(s) {
|
|
315
|
+
if (s.mode === 'shell') return s.pty.pid;
|
|
316
|
+
if (s.claudePid || s.mode === 'pick') return s.claudePid;
|
|
317
|
+
const live = o.liveSessions();
|
|
318
|
+
const own = live.find((l) => l.pid && l.sessionId === s.id && l.startedAt >= s.startedAt - PICK_START_SLACK_MS);
|
|
319
|
+
if (own || s.mode !== 'fork') return own?.pid;
|
|
320
|
+
const claimed = new Set([...sessions.values()].map((x) => x.claudePid).filter(Boolean));
|
|
321
|
+
return findPickProcess(live, s, claimed);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// claude writes its registry entry a moment after it starts, so the pid is polled for.
|
|
325
|
+
function syncBoost(s) {
|
|
326
|
+
const want = s.sockets.size > 0 && !s.exited;
|
|
327
|
+
if (want && !s.boost) {
|
|
328
|
+
if (s.boostTimer) return;
|
|
329
|
+
let tries = 0;
|
|
330
|
+
const tryBoost = () => {
|
|
331
|
+
const pid = inputPid(s);
|
|
332
|
+
if (!pid && ++tries < BOOST_TRIES) return;
|
|
333
|
+
clearInterval(s.boostTimer);
|
|
334
|
+
s.boostTimer = null;
|
|
335
|
+
if (!pid) return;
|
|
336
|
+
if (s.mode !== 'shell') s.claudePid = pid;
|
|
337
|
+
const before = priority.raise(pid);
|
|
338
|
+
s.boost = { pid, before };
|
|
339
|
+
};
|
|
340
|
+
s.boostTimer = setInterval(tryBoost, PICK_POLL_MS);
|
|
341
|
+
tryBoost();
|
|
342
|
+
} else if (!want) {
|
|
343
|
+
clearInterval(s.boostTimer);
|
|
344
|
+
s.boostTimer = null;
|
|
345
|
+
if (s.boost && s.boost.before !== null && !s.exited) priority.restore(s.boost.pid, s.boost.before);
|
|
346
|
+
s.boost = null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
305
350
|
function spawnSession(id, mode, cwdCandidate, cols, rows, spec, extraEnv) {
|
|
306
351
|
const cwd = cwdCandidate && fs.existsSync(cwdCandidate) ? cwdCandidate : os.homedir();
|
|
307
|
-
const env = { ...ptyEnv({ claudeDir: o.claudeDir, isDefaultDir: o.isDefaultDir, noFlicker: config.noFlicker }), ...extraEnv };
|
|
352
|
+
const env = { ...ptyEnv({ claudeDir: o.claudeDir, isDefaultDir: o.isDefaultDir, noFlicker: config.noFlicker, cckUrl: o.serverUrl?.() }), ...extraEnv };
|
|
308
353
|
// Git Bash's login profile cds to $HOME unless this is set.
|
|
309
354
|
if (shellFamily(shell) === 'gitbash') env.CHERE_INVOKING = '1';
|
|
310
355
|
const proc = pty.spawn(shell, shellArgs(shell, claudeArgsFor(mode, id, spec)), {
|
|
@@ -335,6 +380,7 @@ function createTerminalService(o) {
|
|
|
335
380
|
proc.onExit(({ exitCode }) => {
|
|
336
381
|
s.exited = true;
|
|
337
382
|
clearInterval(s.pickTimer);
|
|
383
|
+
clearInterval(s.boostTimer);
|
|
338
384
|
if (sessions.get(s.id) === s) sessions.delete(s.id);
|
|
339
385
|
o.onExit?.(s.id);
|
|
340
386
|
if (!s.ended) o.onChange?.();
|
|
@@ -345,6 +391,7 @@ function createTerminalService(o) {
|
|
|
345
391
|
term.dispose();
|
|
346
392
|
});
|
|
347
393
|
sessions.set(id, s);
|
|
394
|
+
priority.raiseConsoleHosts();
|
|
348
395
|
o.onChange?.();
|
|
349
396
|
return s;
|
|
350
397
|
}
|
|
@@ -389,6 +436,7 @@ function createTerminalService(o) {
|
|
|
389
436
|
function attach(ws, s, attached) {
|
|
390
437
|
ws.unacked = 0;
|
|
391
438
|
s.sockets.add(ws);
|
|
439
|
+
syncBoost(s);
|
|
392
440
|
send(ws, { t: 'ready', attached });
|
|
393
441
|
const snapshot = s.serializer.serialize();
|
|
394
442
|
if (snapshot) sendOutput(ws, Buffer.from(snapshot, 'utf8'));
|
|
@@ -411,6 +459,7 @@ function createTerminalService(o) {
|
|
|
411
459
|
});
|
|
412
460
|
ws.on('close', () => {
|
|
413
461
|
s.sockets.delete(ws);
|
|
462
|
+
syncBoost(s);
|
|
414
463
|
if (!s.exited) updateFlow(s);
|
|
415
464
|
});
|
|
416
465
|
}
|
|
@@ -552,4 +601,4 @@ function createTerminalService(o) {
|
|
|
552
601
|
return { token, handleUpgrade, clientConfig, list, isRunning, end, authorized, startNew, shutdown, unavailableReason };
|
|
553
602
|
}
|
|
554
603
|
|
|
555
|
-
module.exports = { createTerminalService, readTerminalConfig, shellArgs, resolveShell, claudeArgsFor, parseNewSpec, findPickProcess, tokenMatches };
|
|
604
|
+
module.exports = { createTerminalService, readTerminalConfig, ptyEnv, shellArgs, resolveShell, claudeArgsFor, parseNewSpec, findPickProcess, tokenMatches };
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-kanban",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.30.0",
|
|
4
4
|
"description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
|
|
5
5
|
"main": "server.js",
|
|
6
|
+
"type": "commonjs",
|
|
6
7
|
"bin": {
|
|
7
8
|
"claude-code-kanban": "server.js"
|
|
8
9
|
},
|
|
@@ -38,7 +38,10 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
38
38
|
// Re-read every cycle rather than caching: it is how we follow the board across a
|
|
39
39
|
// restart onto a different port. A file left behind by a crashed server names a port
|
|
40
40
|
// something else may now hold, so trust it only while its pid is alive.
|
|
41
|
+
// CCK_URL comes from the board whose terminal started this session. That board owns the
|
|
42
|
+
// session's queue, and server.json may name another board on the same config dir.
|
|
41
43
|
function serverUrl() {
|
|
44
|
+
if (process.env.CCK_URL) return process.env.CCK_URL.replace(/\/+$/, '');
|
|
42
45
|
const { port, pid } = JSON.parse(fs.readFileSync(SERVER_INFO, 'utf8'));
|
|
43
46
|
if (pid) process.kill(pid, 0);
|
|
44
47
|
return `http://127.0.0.1:${port}`;
|
|
@@ -9,58 +9,28 @@ disable-model-invocation: true
|
|
|
9
9
|
|
|
10
10
|
This session id is `${CLAUDE_SESSION_ID}`, substituted when the skill loads.
|
|
11
11
|
|
|
12
|
-
An argument names the
|
|
12
|
+
An argument names the command that handles it; with no argument, open the current session. Prefer the bare `claude-code-kanban` binary, falling back to `npx claude-code-kanban` when it is off PATH or the user asks for npx.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
## `open` — the current session
|
|
17
|
-
|
|
18
|
-
Pins the session and switches the board to the Active tab.
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
claude-code-kanban session open ${CLAUDE_SESSION_ID}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## `pin` — keep the session visible
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
claude-code-kanban session pin ${CLAUDE_SESSION_ID} # pin
|
|
28
|
-
claude-code-kanban session pin ${CLAUDE_SESSION_ID} --sticky # always at the top
|
|
29
|
-
claude-code-kanban session pin ${CLAUDE_SESSION_ID} --unpin # clear
|
|
30
|
-
claude-code-kanban session pins # list pinned; --sticky narrows
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## `preview` — open a file in the modal
|
|
34
|
-
|
|
35
|
-
Markdown or standalone HTML. HTML renders in a sandboxed iframe, so sibling assets like `./style.css` do not load. Relative paths are fine — the server resolves them.
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
claude-code-kanban preview-doc <file.md|.html> --session ${CLAUDE_SESSION_ID}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## `link` — attach a doc without the modal
|
|
42
|
-
|
|
43
|
-
Adds the file to the session's linked docs in the sidebar. Any extension, and nothing pops up, so it is the safe choice while the user is working.
|
|
14
|
+
The CLI help is the reference, and it always matches the installed binary. Read it before you run a command, instead of guessing flags:
|
|
44
15
|
|
|
45
16
|
```bash
|
|
46
|
-
claude-code-kanban
|
|
47
|
-
claude-code-kanban
|
|
17
|
+
claude-code-kanban help # every command
|
|
18
|
+
claude-code-kanban help <command> # its subcommands
|
|
19
|
+
claude-code-kanban help <command> <subcommand> # flags and examples
|
|
48
20
|
```
|
|
49
21
|
|
|
50
|
-
|
|
22
|
+
| Argument | Command |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `open` (or none) | `session open ${CLAUDE_SESSION_ID}` |
|
|
25
|
+
| `pin` / `unpin` | `session pin ${CLAUDE_SESSION_ID}` (`--sticky`, `--unpin`) |
|
|
26
|
+
| `preview` | `preview-doc <file> --session ${CLAUDE_SESSION_ID}` — opens a modal on the user's screen |
|
|
27
|
+
| `link` | `link-doc <file> --session ${CLAUDE_SESSION_ID}` — no modal, so it is the safe choice while the user is working |
|
|
28
|
+
| `list` / `search` | `session list`, `session search <text>` |
|
|
29
|
+
| `view` / `peek` | `session view <id>`, `session peek <id>` |
|
|
30
|
+
| tasks, projects | `task list`, `project list` |
|
|
51
31
|
|
|
52
|
-
|
|
53
|
-
claude-code-kanban session list --active # recent active sessions
|
|
54
|
-
claude-code-kanban session list --project <name> # filter by project
|
|
55
|
-
claude-code-kanban session list --days 0.5 --limit all # touched in last 12h, uncapped
|
|
56
|
-
claude-code-kanban session view ${CLAUDE_SESSION_ID} # full stats for current session
|
|
57
|
-
claude-code-kanban session peek ${CLAUDE_SESSION_ID} --limit 20 # last 20 messages (server caps at 50)
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`session list` shows 10 rows and always includes pinned sessions, sticky first (`--no-pins` disables both). `--json` works on any list-style verb.
|
|
32
|
+
To be driven *by* the board instead — card moves arriving as instructions — the user types `/claude-code-kanban:kanban-follow`.
|
|
61
33
|
|
|
62
34
|
## Troubleshooting
|
|
63
35
|
|
|
64
|
-
`claude-code-kanban
|
|
65
|
-
|
|
66
|
-
- **"Cannot reach cck server…"** → the error names the port it tried. Ask the user to start the server with `claude-code-kanban`. If they run it elsewhere, set `PORT=<n>` when invoking the CLI.
|
|
36
|
+
- **"Cannot reach cck server…"** → the error names the config dir and the port it tried. Ask the user to start the server with `claude-code-kanban`. If they run it elsewhere, set `PORT=<n>` or `CCK_URL=<url>` when invoking the CLI.
|