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 +54 -32
- package/install.js +37 -7
- package/lib/parsers.js +100 -24
- package/package.json +1 -1
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +2 -2
- package/plugin/plugins/claude-code-kanban/skills/kanban/SKILL.md +26 -25
- package/public/app.js +352 -127
- package/public/index.html +4 -4
- package/public/style.css +364 -74
- package/server.js +344 -47
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1295
|
-
const
|
|
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
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
const
|
|
1309
|
-
|
|
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
|
-
}
|
|
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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
{
|
|
8
8
|
"name": "claude-code-kanban",
|
|
9
9
|
"source": "./plugins/claude-code-kanban",
|
|
10
|
-
"description": "
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
23
|
+
## Pin the current session
|
|
24
24
|
|
|
25
|
-
|
|
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
|
|
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}
|
|
45
|
+
claude-code-kanban preview-doc <path-to-file.md|.html> --session ${CLAUDE_SESSION_ID}
|
|
53
46
|
```
|
|
54
47
|
|
|
55
|
-
|
|
48
|
+
## Link a document to the session (no modal)
|
|
56
49
|
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
73
|
-
|
|
74
|
-
- **
|
|
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.
|