claude-code-kanban 4.14.0 → 4.16.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/cli.js CHANGED
@@ -2,14 +2,23 @@ const path = require('path');
2
2
 
3
3
  // Help is auto-generated from this table — keep flags/usage in sync with `run` behavior.
4
4
  const COMMANDS = {
5
- preview: {
6
- summary: 'Open a markdown file in the preview modal on connected browser tabs',
7
- usage: 'claude-code-kanban preview <file.md> [--session <id>]',
5
+ 'preview-doc': {
6
+ summary: 'Open a markdown or HTML file in the preview modal on connected browser tabs',
7
+ usage: 'claude-code-kanban preview-doc <file.md|file.html> [--session <id>]',
8
8
  flags: {
9
9
  '--session <id>': 'Switch focused session in the browser (does not link the file)',
10
10
  },
11
11
  run: runPreviewCli,
12
12
  },
13
+ 'link-doc': {
14
+ summary: 'Link a file to a session in the sidebar without opening the preview modal',
15
+ usage: 'claude-code-kanban link-doc <file> --session <id> [--unlink]',
16
+ flags: {
17
+ '--session <id>': 'Session to link the file to (required unless $PREVIEW_SESSION is set)',
18
+ '--unlink': 'Remove the link instead of adding it',
19
+ },
20
+ run: runLinkDocCli,
21
+ },
13
22
  session: {
14
23
  summary: 'List or open Claude Code sessions',
15
24
  verbs: {
@@ -148,7 +157,8 @@ function printTopHelp() {
148
157
  console.log(' --port <n> Port to listen on (default 3541)');
149
158
  console.log(' --dir <path> Override Claude config dir (default ~/.claude)');
150
159
  console.log(' --open Open browser on start');
151
- console.log(' --install, --uninstall Install or remove the agent-spy hook');
160
+ console.log(' --install, --uninstall Install or remove the plugin, context spy, and statusline');
161
+ console.log(' --plugin-only With --install: refresh only the plugin, skip context spy and statusline');
152
162
  }
153
163
 
154
164
  function printNounHelp(noun) {
@@ -201,6 +211,19 @@ async function cliFetch(urlPath, init) {
201
211
  }
202
212
  }
203
213
 
214
+ // Every write verb posts JSON and reports failure the same way; `label` names the verb
215
+ // in the error line. Returns false when the server refused, so callers just return 1.
216
+ async function cliPostJson(urlPath, body, label) {
217
+ const res = await cliFetch(urlPath, {
218
+ method: 'POST',
219
+ headers: { 'Content-Type': 'application/json' },
220
+ body: JSON.stringify(body)
221
+ });
222
+ if (res.ok) return true;
223
+ console.error(`${label} failed (${res.status}): ${await res.text()}`);
224
+ return false;
225
+ }
226
+
204
227
  function reportCliError(e) {
205
228
  console.error(e.code === 'unreachable' ? e.message : (e.message || String(e)));
206
229
  }
@@ -208,26 +231,41 @@ function reportCliError(e) {
208
231
  async function runPreviewCli(args) {
209
232
  const filePathArg = args.find(a => !a.startsWith('--'));
210
233
  if (!filePathArg) {
211
- printLeafHelp('preview', COMMANDS.preview);
234
+ printLeafHelp('preview-doc', COMMANDS['preview-doc']);
212
235
  return 1;
213
236
  }
214
237
  const sessionId = getArgValue(args, 'session') || process.env.PREVIEW_SESSION || null;
215
238
  const abs = path.resolve(filePathArg);
216
239
  try {
217
- const res = await cliFetch('/api/preview', {
218
- method: 'POST',
219
- headers: { 'Content-Type': 'application/json' },
220
- body: JSON.stringify({ path: abs, sessionId })
221
- });
222
- if (!res.ok) {
223
- console.error(`Preview failed (${res.status}): ${await res.text()}`);
224
- return 1;
225
- }
240
+ if (!await cliPostJson('/api/preview', { path: abs, sessionId }, 'Preview')) return 1;
226
241
  console.log(`Preview opened: ${abs}${sessionId ? ` (session ${sessionId})` : ''}`);
227
242
  return 0;
228
243
  } catch (e) { reportCliError(e); return 1; }
229
244
  }
230
245
 
246
+ async function runLinkDocCli(args) {
247
+ const filePathArg = args.find(a => !a.startsWith('--'));
248
+ const sessionArg = getArgValue(args, 'session') || process.env.PREVIEW_SESSION || null;
249
+ if (!filePathArg) {
250
+ printLeafHelp('link-doc', COMMANDS['link-doc']);
251
+ return 1;
252
+ }
253
+ if (!sessionArg) {
254
+ console.error('--session is required: linked docs are stored per session.');
255
+ return 1;
256
+ }
257
+ const unlink = args.includes('--unlink');
258
+ // Resolved here because the browser keys linked docs by full id, so a prefix won't match.
259
+ const resolved = await resolveSessionByIdOrPrefix(sessionArg);
260
+ if (!resolved) return 1;
261
+ const abs = path.resolve(filePathArg);
262
+ try {
263
+ if (!await cliPostJson('/api/document/link', { path: abs, sessionId: resolved.id, unlink }, 'Link')) return 1;
264
+ console.log(`Document ${unlink ? 'unlinked from' : 'linked to'} session ${resolved.id.slice(0, 8)}: ${abs}`);
265
+ return 0;
266
+ } catch (e) { reportCliError(e); return 1; }
267
+ }
268
+
231
269
  // Mirror of `isSessionActive` in public/app.js — keep in sync (different runtimes, no shared module).
232
270
  function isSessionActive(s) {
233
271
  return s.hasRecentLog || s.inProgress > 0 || s.hasActiveAgents || s.hasWaitingForUser;
@@ -390,15 +428,7 @@ async function runSessionOpenCli(args) {
390
428
  const resolved = await resolveSessionByIdOrPrefix(idArg);
391
429
  if (!resolved) return 1;
392
430
  try {
393
- const res = await cliFetch('/api/session/open', {
394
- method: 'POST',
395
- headers: { 'Content-Type': 'application/json' },
396
- body: JSON.stringify({ id: resolved.id })
397
- });
398
- if (!res.ok) {
399
- console.error(`Open failed (${res.status}): ${await res.text()}`);
400
- return 1;
401
- }
431
+ if (!await cliPostJson('/api/session/open', { id: resolved.id }, 'Open')) return 1;
402
432
  console.log(`Session opened: ${resolved.id}${resolved.customTitle ? ` (${resolved.customTitle})` : ''}`);
403
433
  return 0;
404
434
  } catch (e) { reportCliError(e); return 1; }
@@ -414,15 +444,7 @@ async function runSessionPinCli(args) {
414
444
  const resolved = await resolveSessionByIdOrPrefix(idArg);
415
445
  if (!resolved) return 1;
416
446
  try {
417
- const res = await cliFetch('/api/session/pin', {
418
- method: 'POST',
419
- headers: { 'Content-Type': 'application/json' },
420
- body: JSON.stringify({ id: resolved.id, state })
421
- });
422
- if (!res.ok) {
423
- console.error(`Pin failed (${res.status}): ${await res.text()}`);
424
- return 1;
425
- }
447
+ if (!await cliPostJson('/api/session/pin', { id: resolved.id, state }, 'Pin')) return 1;
426
448
  const label = state === 'none' ? 'unpinned' : state;
427
449
  console.log(`Session ${label}: ${resolved.id}${resolved.customTitle ? ` (${resolved.customTitle})` : ''}`);
428
450
  return 0;
package/install.js CHANGED
@@ -49,6 +49,19 @@ function copyScript(src, dest) {
49
49
  try { fs.chmodSync(dest, 0o755); } catch {}
50
50
  }
51
51
 
52
+ // Deletes files but never directories: on Windows the running Claude Code process holds
53
+ // handles on the registered marketplace dirs, so removing one fails EPERM mid-clean and
54
+ // leaves the install gutted. Clearing files alone still drops anything stale, and the
55
+ // leftover empty dirs are harmless — copyDirSync writes straight back into them.
56
+ function clearFilesRecursive(dir) {
57
+ if (!fs.existsSync(dir)) return;
58
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
59
+ const target = path.join(dir, entry.name);
60
+ if (entry.isDirectory()) clearFilesRecursive(target);
61
+ else fs.rmSync(target, { force: true });
62
+ }
63
+ }
64
+
52
65
  function copyDirSync(src, dest) {
53
66
  fs.mkdirSync(dest, { recursive: true });
54
67
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
@@ -63,8 +76,9 @@ function copyDirSync(src, dest) {
63
76
  }
64
77
  }
65
78
 
66
- async function runInstall() {
67
- console.log(`\n ${bold('claude-code-kanban')} — Plugin & StatusLine installer\n`);
79
+ async function runInstall({ pluginOnly = false } = {}) {
80
+ console.log(`\n ${bold('claude-code-kanban')} — ${pluginOnly ? 'Plugin installer' : 'Plugin & StatusLine installer'}\n`);
81
+ let failed = false;
68
82
 
69
83
  // 1. Check prerequisites
70
84
  process.stdout.write(' Checking claude CLI... ');
@@ -87,14 +101,15 @@ async function runInstall() {
87
101
 
88
102
  // 2. Copy plugin to stable location & register marketplace
89
103
  console.log(`\n Plugin: ${dim(PLUGIN_DEST)}`);
90
- if (await prompt(` Install claude-code-kanban plugin? [Y/n] `)) {
104
+ if (pluginOnly || await prompt(` Install claude-code-kanban plugin? [Y/n] `)) {
91
105
  process.stdout.write(' Copying plugin to ~/.claude/.cck/plugin... ');
92
106
  try {
93
- if (fs.existsSync(PLUGIN_DEST)) fs.rmSync(PLUGIN_DEST, { recursive: true, force: true });
107
+ clearFilesRecursive(PLUGIN_DEST);
94
108
  copyDirSync(PLUGIN_SRC, PLUGIN_DEST);
95
109
  console.log(green('✓'));
96
110
  } catch (e) {
97
111
  console.log(red(`✗ ${e.message}`));
112
+ failed = true;
98
113
  }
99
114
 
100
115
  process.stdout.write(' Registering marketplace... ');
@@ -105,16 +120,27 @@ async function runInstall() {
105
120
  console.log(yellow(`⚠ ${mkt.error}`));
106
121
  }
107
122
 
123
+ // Claude caches the marketplace manifest, so a re-copied plugin stays invisible until refreshed
124
+ const upd = runCLI('claude plugin marketplace update claude-code-kanban');
125
+ if (!upd.ok) console.log(` ${yellow('⚠')} Marketplace refresh failed: ${upd.error}`);
126
+
108
127
  const inst = runCLI('claude plugin install claude-code-kanban@claude-code-kanban', ['already installed', 'already exists']);
109
128
  if (inst.ok) {
110
129
  console.log(` ${green('✓')} ${inst.idempotent ? 'Already installed' : 'Plugin installed'}`);
111
130
  } else {
112
131
  console.log(` ${red('✗')} Plugin install failed: ${inst.error}`);
132
+ failed = true;
113
133
  }
114
134
  } else {
115
135
  console.log(` ${dim('Skipped')}`);
116
136
  }
117
137
 
138
+ if (pluginOnly) {
139
+ console.log(`\n ${dim('Context spy and statusline skipped (--plugin-only).')}`);
140
+ printSummary(failed);
141
+ return;
142
+ }
143
+
118
144
  // 3. StatusLine setup (context-status.sh must be copied globally since statusLine is not plugin-scoped)
119
145
  console.log(`\n Context spy: ${dim(CTX_SCRIPT_DEST)}`);
120
146
  let ctxInstalled = false;
@@ -148,7 +174,7 @@ async function runInstall() {
148
174
  : {};
149
175
  } catch {
150
176
  console.log(` ${red('✗')} Malformed JSON in settings.json — skipping statusline config`);
151
- printSummary();
177
+ printSummary(true);
152
178
  return;
153
179
  }
154
180
 
@@ -179,10 +205,14 @@ async function runInstall() {
179
205
  }
180
206
  }
181
207
 
182
- printSummary();
208
+ printSummary(failed);
183
209
  }
184
210
 
185
- function printSummary() {
211
+ function printSummary(failed = false) {
212
+ if (failed) {
213
+ console.log(`\n ${red('Setup incomplete — see the errors above.')}\n`);
214
+ return;
215
+ }
186
216
  console.log(`\n ${green('Setup complete. Agent activity will appear in the Kanban dashboard.')}\n`);
187
217
  }
188
218
 
package/lib/parsers.js CHANGED
@@ -1291,33 +1291,31 @@ function findTerminatedTeammates(jsonlPath) {
1291
1291
 
1292
1292
  function extractPromptFromTranscript(jsonlPath) {
1293
1293
  const { openSync, readSync, closeSync } = fs;
1294
- const MAX_READ = 65536;
1295
- const CHUNK = 4096;
1294
+ // statSync (outside try) throws on a missing path — callers rely on that.
1295
+ const stat = statSync(jsonlPath);
1296
+ // Workflow-spawned subagent prompts embed full task context and routinely
1297
+ // exceed 64 KB, so read the whole first line up to a generous cap. One buffered
1298
+ // read (vs. per-chunk decode) also avoids corrupting multi-byte chars at chunk
1299
+ // boundaries for large prompts.
1300
+ const readSize = Math.min(2097152, stat.size);
1296
1301
  const fd = openSync(jsonlPath, 'r');
1297
1302
  try {
1298
- let accumulated = '';
1299
- const buf = Buffer.alloc(CHUNK);
1300
- while (accumulated.length < MAX_READ) {
1301
- const bytesRead = readSync(fd, buf, 0, CHUNK, null);
1302
- if (bytesRead === 0) break;
1303
- accumulated += buf.toString('utf8', 0, bytesRead);
1304
- const nlIdx = accumulated.indexOf('\n');
1305
- if (nlIdx === -1) continue;
1306
- const firstLine = accumulated.slice(0, nlIdx);
1307
- try {
1308
- const obj = JSON.parse(firstLine);
1309
- if (obj.type === 'user') {
1310
- const content = obj.message?.content;
1311
- if (typeof content === 'string') return content;
1312
- if (Array.isArray(content)) {
1313
- for (const b of content) {
1314
- if (b.type === 'text' && b.text) return b.text;
1315
- }
1316
- }
1303
+ const buf = Buffer.alloc(readSize);
1304
+ readSync(fd, buf, 0, readSize, 0);
1305
+ // Decode only the first line, not the whole (possibly large) read window.
1306
+ const nlByte = buf.indexOf(0x0a);
1307
+ const firstLine = (nlByte === -1 ? buf : buf.subarray(0, nlByte)).toString('utf8');
1308
+ const obj = JSON.parse(firstLine);
1309
+ if (obj.type === 'user') {
1310
+ const content = obj.message?.content;
1311
+ if (typeof content === 'string') return content;
1312
+ if (Array.isArray(content)) {
1313
+ for (const b of content) {
1314
+ if (b.type === 'text' && b.text) return b.text;
1317
1315
  }
1318
- } catch (_) {}
1319
- break;
1316
+ }
1320
1317
  }
1318
+ } catch (_) {
1321
1319
  } finally {
1322
1320
  closeSync(fd);
1323
1321
  }
@@ -1354,6 +1352,82 @@ function extractModelFromTranscript(jsonlPath) {
1354
1352
  return null;
1355
1353
  }
1356
1354
 
1355
+ // Full-file scan of a subagent transcript for roster stats: model, summed
1356
+ // output tokens, and first/last timestamps (for duration). COLD PATH ONLY —
1357
+ // used by the workflow run view on demand, never from the session-scan hot
1358
+ // path (a full read per agent would blow the /api/sessions budget).
1359
+ function extractTranscriptStats(jsonlPath) {
1360
+ let content;
1361
+ try { content = readFileSync(jsonlPath, 'utf8'); } catch (_) { return null; }
1362
+ let model = null;
1363
+ let outputTokens = 0;
1364
+ let firstTs = null;
1365
+ let lastTs = null;
1366
+ for (const line of content.split('\n')) {
1367
+ if (!line.trim()) continue;
1368
+ let obj;
1369
+ try { obj = JSON.parse(line); } catch (_) { continue; }
1370
+ if (obj.timestamp) {
1371
+ if (!firstTs) firstTs = obj.timestamp;
1372
+ lastTs = obj.timestamp;
1373
+ }
1374
+ const msg = obj.message;
1375
+ if (msg && typeof msg === 'object') {
1376
+ if (!model && msg.model) model = msg.model;
1377
+ const out = msg.usage?.output_tokens;
1378
+ if (typeof out === 'number') outputTokens += out;
1379
+ }
1380
+ }
1381
+ return { model, outputTokens, firstTs, lastTs };
1382
+ }
1383
+
1384
+ // Render a StructuredOutput tool input (arbitrary schema object) as markdown:
1385
+ // one section per field, prose kept verbatim, structured values as JSON blocks.
1386
+ function formatStructuredResult(input) {
1387
+ const parts = [];
1388
+ for (const [key, value] of Object.entries(input)) {
1389
+ if (value == null || value === '') continue;
1390
+ parts.push('### ' + key);
1391
+ parts.push(typeof value === 'string' ? value : '```json\n' + JSON.stringify(value, null, 2) + '\n```');
1392
+ }
1393
+ return parts.length ? parts.join('\n\n') : null;
1394
+ }
1395
+
1396
+ // Workflow-spawned subagents given a schema end their run on a forced
1397
+ // StructuredOutput tool call and never emit a text response — so their
1398
+ // lastMessage is empty. Tail-read the transcript, take the LAST StructuredOutput
1399
+ // tool_use, and format its input as the agent's result. Returns null when absent.
1400
+ function extractStructuredResultFromTranscript(jsonlPath) {
1401
+ const { openSync, readSync, closeSync } = fs;
1402
+ const stat = statSync(jsonlPath);
1403
+ const readSize = Math.min(1048576, stat.size);
1404
+ const start = stat.size - readSize;
1405
+ const fd = openSync(jsonlPath, 'r');
1406
+ try {
1407
+ const buf = Buffer.alloc(readSize);
1408
+ readSync(fd, buf, 0, readSize, start);
1409
+ const text = buf.toString('utf8');
1410
+ // Drop the leading partial line unless we read from the very start.
1411
+ const clean = start > 0 ? text.slice(text.indexOf('\n') + 1) : text;
1412
+ if (!clean.includes('"StructuredOutput"')) return null;
1413
+ // Scan from the end for the last StructuredOutput tool call.
1414
+ const lines = clean.split('\n');
1415
+ for (let i = lines.length - 1; i >= 0; i--) {
1416
+ if (!lines[i].includes('"StructuredOutput"')) continue;
1417
+ let obj;
1418
+ try { obj = JSON.parse(lines[i]); } catch (_) { continue; }
1419
+ const content = obj?.message?.content;
1420
+ if (!Array.isArray(content)) continue;
1421
+ const block = content.find(b => b?.type === 'tool_use' && b.name === 'StructuredOutput' && b.input && typeof b.input === 'object');
1422
+ if (block) return formatStructuredResult(block.input);
1423
+ }
1424
+ } catch (_) {
1425
+ } finally {
1426
+ closeSync(fd);
1427
+ }
1428
+ return null;
1429
+ }
1430
+
1357
1431
  // Incremental loop-tool scanner. JSONL is append-only, so we keep per-path
1358
1432
  // state and on each call read ONLY the bytes appended since scannedOffset.
1359
1433
  // Avoids the only full-file read that ran inside the /api/sessions hot path.
@@ -1492,5 +1566,7 @@ module.exports = {
1492
1566
  readCompactSummaries,
1493
1567
  findTerminatedTeammates,
1494
1568
  extractPromptFromTranscript,
1495
- extractModelFromTranscript
1569
+ extractModelFromTranscript,
1570
+ extractStructuredResultFromTranscript,
1571
+ extractTranscriptStats
1496
1572
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "4.14.0",
3
+ "version": "4.16.0",
4
4
  "description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -7,7 +7,7 @@
7
7
  {
8
8
  "name": "claude-code-kanban",
9
9
  "source": "./plugins/claude-code-kanban",
10
- "description": "Agent activity tracking for claude-code-kanban dashboard"
10
+ "description": "claude-code-kanban dashboard integration: agent activity tracking, context statusline, and a skill to drive the board from a session"
11
11
  }
12
12
  ]
13
13
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "2.2.1",
4
- "description": "Agent activity tracking for claude-code-kanban dashboard"
3
+ "version": "2.3.3",
4
+ "description": "claude-code-kanban dashboard integration: agent activity tracking, context statusline, and a skill to drive the board from a session"
5
5
  }
@@ -1,30 +1,28 @@
1
1
  ---
2
2
  name: kanban
3
- description: Drive the claude-code-kanban browser dashboard from this Claude session. Use this skill when the user mentions "kanban" together with "session" — e.g. "open this session in kanban", "show kanban", "focus current session in kanban", "pin/unpin a session in kanban", "preview this file in kanban", or asks to peek/view a kanban session.
4
- compatibility: Requires the `claude-code-kanban` CLI on PATH and the server running locally (default port 3541).
3
+ description: Drive the claude-code-kanban dashboard from this session — focus the current session in the browser, pin/unpin it in the sidebar, preview a markdown or HTML file, link a document to the session, or inspect session stats and messages. Use when the user mentions kanban or cck.
4
+ argument-hint: '[open|pin|unpin|preview|link] [target]'
5
5
  ---
6
6
 
7
7
  # Kanban Skill
8
8
 
9
- Drive the kanban from this Claude session. Every command corresponds to something the user could do in the kanban dashboard.
9
+ The current Claude session id is `${CLAUDE_SESSION_ID}` (substituted when this skill loads), so the user never needs to look it up.
10
10
 
11
- The current Claude session id is available as `${CLAUDE_SESSION_ID}` (substituted when this skill loads), so the user never needs to look it up.
11
+ When the user passes arguments, map them to the matching command below (`open` → `session open`, `pin`/`unpin`/`pins` → `session pin`/`--unpin`/`session pins`, `preview` → `preview-doc`, `link` → `link-doc`, `list`/`view`/`peek` → the read-only verbs); with no arguments, open the current session.
12
12
 
13
- NOTE, sometimes user prefers `npx claude-code-kanban` to `claude-code-kanban` — both work, as long as the CLI is available. Use npx as fallback or when user instructed explicitly.
13
+ Prefer the bare `claude-code-kanban` binary; fall back to `npx claude-code-kanban` when it is not on PATH, or when the user asks for npx explicitly.
14
14
 
15
15
  ## Open the current session in kanban
16
16
 
17
- Primary use case. Pins the active Claude session in the kanban sidebar and switches to the Active tab.
17
+ Primary use case. Pins the active session in the sidebar and switches to the Active tab.
18
18
 
19
19
  ```bash
20
20
  claude-code-kanban session open ${CLAUDE_SESSION_ID}
21
21
  ```
22
22
 
23
- Trigger phrases: "show this session in kanban", "focus current session", "open in kanban".
23
+ ## Pin the current session
24
24
 
25
- ## Pin the current session in kanban
26
-
27
- Pins the active Claude session in the sidebar so it stays visible regardless of filters. Three states: `pinned` (default), `sticky` (always at the top), or cleared via `--unpin`.
25
+ Pins the session so it stays visible regardless of filters. Three states: `pinned` (default), `sticky` (always at the top), or cleared with `--unpin`.
28
26
 
29
27
  ```bash
30
28
  claude-code-kanban session pin ${CLAUDE_SESSION_ID} # pin
@@ -32,43 +30,46 @@ claude-code-kanban session pin ${CLAUDE_SESSION_ID} --sticky # sticky at top
32
30
  claude-code-kanban session pin ${CLAUDE_SESSION_ID} --unpin # clear
33
31
  ```
34
32
 
35
- Trigger phrases: "pin this session", "pin in kanban", "make this session sticky", "unpin session".
36
-
37
33
  ## List pinned sessions
38
34
 
39
35
  ```bash
40
- claude-code-kanban session pins # all pinned/sticky sessions
36
+ claude-code-kanban session pins # all pinned/sticky
41
37
  claude-code-kanban session pins --sticky # sticky only
42
- claude-code-kanban session pins --json # JSON output
43
38
  ```
44
39
 
45
- Trigger phrases: "show pinned sessions", "what's pinned", "list pins".
46
-
47
40
  ## Preview a file in kanban
48
41
 
49
- Opens a markdown file in the preview modal:
42
+ Opens a markdown or standalone HTML file in the preview modal (HTML renders live in a sandboxed iframe, so sibling assets like `./style.css` do not load). Relative paths are fine — the server resolves to absolute.
50
43
 
51
44
  ```bash
52
- claude-code-kanban preview <path-to-file.md> --session ${CLAUDE_SESSION_ID} # prefer this one
45
+ claude-code-kanban preview-doc <path-to-file.md|.html> --session ${CLAUDE_SESSION_ID}
53
46
  ```
54
47
 
55
- Relative paths are fine — the server resolves to absolute.
48
+ ## Link a document to the session (no modal)
56
49
 
57
- ## Inspect sessions (read-only)
50
+ Same idea as `preview-doc`, but it only attaches the file to the session's linked docs in the sidebar — nothing pops up, so it is the safe choice while the user is working. Any extension is linkable.
51
+
52
+ ```bash
53
+ claude-code-kanban link-doc <path-to-file> --session ${CLAUDE_SESSION_ID} # link
54
+ claude-code-kanban link-doc <path-to-file> --session ${CLAUDE_SESSION_ID} --unlink # remove
55
+ ```
58
56
 
59
- When the user asks "what's going on in kanban?" or wants stats:
57
+ ## Inspect sessions (read-only)
60
58
 
61
59
  ```bash
62
60
  claude-code-kanban session list --active # recent active sessions
63
61
  claude-code-kanban session list --project <name> # filter by project
62
+ claude-code-kanban session list --days 0.5 --limit all # touched in last 12h, uncapped
64
63
  claude-code-kanban session view ${CLAUDE_SESSION_ID} # full stats for current session
65
- claude-code-kanban session peek ${CLAUDE_SESSION_ID} --limit 20 # last 20 messages
64
+ claude-code-kanban session peek ${CLAUDE_SESSION_ID} --limit 20 # last 20 messages (server caps at 50)
66
65
  ```
67
66
 
67
+ `session list` shows 10 rows by default and always includes pinned sessions, sticky first — `--no-pins` disables both.
68
+
68
69
  Add `--json` to any list-style verb for machine-readable output.
69
70
 
70
71
  ## Troubleshooting
71
72
 
72
- - **"Cannot reach cck server on port 3541"** → ask the user to start it: `claude-code-kanban` (or `npm start` in the cck repo).
73
- - **Different port** → set `PORT=<n>` env var when invoking the CLI.
74
- - **Ambiguous session prefix (HTTP 409)** → use the full id. With `${CLAUDE_SESSION_ID}` this won't happen.
73
+ `claude-code-kanban help <command>` prints the authoritative flags for any command — read it instead of guessing.
74
+
75
+ - **"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.