claude-code-kanban 1.19.0 → 2.0.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 -1
- package/lib/parsers.js +180 -24
- package/package.json +1 -1
- package/public/index.html +921 -172
- package/server.js +110 -18
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://www.npmjs.com/package/claude-code-kanban)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**[Live Demo & Docs](https://nikiforovall.blog/claude-code-kanban/)**
|
|
8
|
+
|
|
9
|
+
> Watch Claude Code work, in real time.
|
|
8
10
|
|
|
9
11
|

|
|
10
12
|
|
package/lib/parsers.js
CHANGED
|
@@ -112,11 +112,28 @@ function parseJsonlLine(line) {
|
|
|
112
112
|
return base;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
const TOOL_RESULT_MAX = 1500;
|
|
116
|
+
|
|
115
117
|
// Cache: jsonlPath -> { scannedUpTo, customTitle }
|
|
116
118
|
// Only re-scan the new bytes appended since last scan
|
|
117
119
|
const customTitleCache = new Map();
|
|
118
120
|
const CUSTOM_TITLE_SCAN_SIZE = 1048576; // 1MB max scan on first read
|
|
119
121
|
|
|
122
|
+
function extractCustomTitleFromText(text) {
|
|
123
|
+
if (!text.includes('"custom-title"')) return null;
|
|
124
|
+
const lines = text.split('\n');
|
|
125
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
126
|
+
if (!lines[i].includes('"custom-title"')) continue;
|
|
127
|
+
try {
|
|
128
|
+
const data = JSON.parse(lines[i]);
|
|
129
|
+
if (data.type === 'custom-title' && data.customTitle && !data.customTitle.startsWith('<')) {
|
|
130
|
+
return data.customTitle;
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
function readCustomTitle(jsonlPath, existingStat) {
|
|
121
138
|
try {
|
|
122
139
|
const stat = existingStat || statSync(jsonlPath);
|
|
@@ -127,27 +144,21 @@ function readCustomTitle(jsonlPath, existingStat) {
|
|
|
127
144
|
let customTitle = cached?.customTitle || null;
|
|
128
145
|
const fd = fs.openSync(jsonlPath, 'r');
|
|
129
146
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (data.type === 'custom-title' && data.customTitle) {
|
|
146
|
-
customTitle = data.customTitle;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
} catch (e) {}
|
|
150
|
-
}
|
|
147
|
+
if (cached) {
|
|
148
|
+
const len = stat.size - cached.scannedUpTo;
|
|
149
|
+
if (len > 0) {
|
|
150
|
+
const buf = Buffer.alloc(len);
|
|
151
|
+
fs.readSync(fd, buf, 0, len, cached.scannedUpTo);
|
|
152
|
+
customTitle = extractCustomTitleFromText(buf.toString('utf8')) || customTitle;
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
const CHUNK = CUSTOM_TITLE_SCAN_SIZE;
|
|
156
|
+
for (let offset = 0; offset < stat.size; offset += CHUNK) {
|
|
157
|
+
const len = Math.min(CHUNK, stat.size - offset);
|
|
158
|
+
const buf = Buffer.alloc(len);
|
|
159
|
+
fs.readSync(fd, buf, 0, len, offset);
|
|
160
|
+
const found = extractCustomTitleFromText(buf.toString('utf8'));
|
|
161
|
+
if (found) customTitle = found;
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
|
|
@@ -203,12 +214,28 @@ function readSessionInfoFromJsonl(jsonlPath) {
|
|
|
203
214
|
return result;
|
|
204
215
|
}
|
|
205
216
|
|
|
217
|
+
function getSystemMessageLabel(text) {
|
|
218
|
+
const taskMatch = text.match(/<summary>([^<]+)<\/summary>/);
|
|
219
|
+
if (taskMatch) return taskMatch[1].trim();
|
|
220
|
+
if (text.includes('<task-notification>')) {
|
|
221
|
+
const statusMatch = text.match(/<status>([^<]+)<\/status>/);
|
|
222
|
+
return statusMatch ? `Background task ${statusMatch[1]}` : 'Background task notification';
|
|
223
|
+
}
|
|
224
|
+
if (text.includes('<local-command-stdout>') && text.includes('Compacted')) return 'Compacted';
|
|
225
|
+
if (text.includes('<local-command-stdout>')) return 'Command output';
|
|
226
|
+
if (text.includes('<local-command-caveat>')) return 'System notification';
|
|
227
|
+
if (text.includes('.output completed') && text.includes('Background command')) return 'Background task completed';
|
|
228
|
+
if (text.startsWith('This session is being continued from a previous conversation')) return '__skip__';
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
206
232
|
function readRecentMessages(jsonlPath, limit = 10) {
|
|
207
233
|
let fd;
|
|
208
234
|
try {
|
|
209
235
|
const stat = statSync(jsonlPath);
|
|
210
236
|
fd = require('fs').openSync(jsonlPath, 'r');
|
|
211
237
|
const messages = [];
|
|
238
|
+
const toolResults = new Map();
|
|
212
239
|
let readSize = Math.min(65536, stat.size);
|
|
213
240
|
|
|
214
241
|
while (messages.length < limit && readSize <= stat.size) {
|
|
@@ -222,6 +249,7 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
222
249
|
const clean = firstNewline >= 0 ? text.substring(firstNewline + 1) : text;
|
|
223
250
|
|
|
224
251
|
messages.length = 0;
|
|
252
|
+
toolResults.clear();
|
|
225
253
|
for (const line of clean.split('\n')) {
|
|
226
254
|
if (!line.trim()) continue;
|
|
227
255
|
try {
|
|
@@ -261,16 +289,73 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
261
289
|
}
|
|
262
290
|
else if (inp.description) { detail = inp.description; fullDetail = inp.description; }
|
|
263
291
|
}
|
|
292
|
+
const params = {};
|
|
293
|
+
if (inp) {
|
|
294
|
+
if (block.name === 'Edit') {
|
|
295
|
+
if (inp.old_string) params.old_string = inp.old_string;
|
|
296
|
+
if (inp.new_string) params.new_string = inp.new_string;
|
|
297
|
+
if (inp.replace_all) params.replace_all = true;
|
|
298
|
+
} else if (block.name === 'Write') {
|
|
299
|
+
if (inp.content) params.content = inp.content.length > TOOL_RESULT_MAX ? inp.content.slice(0, TOOL_RESULT_MAX) + '\n... (truncated)' : inp.content;
|
|
300
|
+
} else if (block.name === 'Grep') {
|
|
301
|
+
if (inp.path) params.path = inp.path;
|
|
302
|
+
if (inp.glob) params.glob = inp.glob;
|
|
303
|
+
if (inp.type) params.type = inp.type;
|
|
304
|
+
if (inp.output_mode) params.output_mode = inp.output_mode;
|
|
305
|
+
if (inp['-i']) params.case_insensitive = true;
|
|
306
|
+
if (inp['-A']) params.after = inp['-A'];
|
|
307
|
+
if (inp['-B']) params.before = inp['-B'];
|
|
308
|
+
if (inp['-C'] || inp.context) params.context = inp['-C'] || inp.context;
|
|
309
|
+
if (inp.multiline) params.multiline = true;
|
|
310
|
+
if (inp.head_limit) params.head_limit = inp.head_limit;
|
|
311
|
+
} else if (block.name === 'Glob') {
|
|
312
|
+
if (inp.path) params.path = inp.path;
|
|
313
|
+
} else if (block.name === 'Bash') {
|
|
314
|
+
if (inp.timeout) params.timeout = inp.timeout;
|
|
315
|
+
if (inp.run_in_background) params.background = true;
|
|
316
|
+
} else if (block.name === 'Read') {
|
|
317
|
+
if (inp.offset) params.offset = inp.offset;
|
|
318
|
+
if (inp.limit) params.limit = inp.limit;
|
|
319
|
+
if (inp.pages) params.pages = inp.pages;
|
|
320
|
+
} else if (block.name === 'WebFetch') {
|
|
321
|
+
if (inp.prompt) params.prompt = inp.prompt;
|
|
322
|
+
} else if (block.name === 'WebSearch') {
|
|
323
|
+
if (inp.max_results) params.max_results = inp.max_results;
|
|
324
|
+
if (inp.allowed_domains) params.allowed_domains = inp.allowed_domains.join(', ');
|
|
325
|
+
if (inp.blocked_domains) params.blocked_domains = inp.blocked_domains.join(', ');
|
|
326
|
+
} else if (block.name === 'LSP') {
|
|
327
|
+
if (inp.operation) params.operation = inp.operation;
|
|
328
|
+
if (inp.filePath) params.filePath = inp.filePath;
|
|
329
|
+
if (inp.line != null) params.line = inp.line;
|
|
330
|
+
if (inp.character != null) params.character = inp.character;
|
|
331
|
+
} else if (block.name === 'ToolSearch') {
|
|
332
|
+
if (inp.max_results) params.max_results = inp.max_results;
|
|
333
|
+
} else if (block.name === 'TaskCreate') {
|
|
334
|
+
if (inp.description) params.description = inp.description;
|
|
335
|
+
} else if (block.name === 'TaskUpdate') {
|
|
336
|
+
if (inp.taskId) params.taskId = inp.taskId;
|
|
337
|
+
if (inp.status) params.status = inp.status;
|
|
338
|
+
} else if (block.name === 'NotebookEdit') {
|
|
339
|
+
if (inp.command) params.command = inp.command;
|
|
340
|
+
if (inp.cell_type) params.cell_type = inp.cell_type;
|
|
341
|
+
} else if (block.name === 'Agent') {
|
|
342
|
+
if (inp.mode) params.mode = inp.mode;
|
|
343
|
+
if (inp.model) params.model = inp.model;
|
|
344
|
+
if (inp.run_in_background) params.background = true;
|
|
345
|
+
if (inp.isolation) params.isolation = inp.isolation;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
264
348
|
const msg = {
|
|
265
349
|
type: 'tool_use',
|
|
266
350
|
tool: block.name,
|
|
267
351
|
detail,
|
|
268
352
|
fullDetail: fullDetail !== detail ? fullDetail : null,
|
|
269
353
|
description: inp?.description || null,
|
|
354
|
+
params: Object.keys(params).length > 0 ? params : null,
|
|
270
355
|
timestamp: obj.timestamp
|
|
271
356
|
};
|
|
357
|
+
if (block.id) msg.toolUseId = block.id;
|
|
272
358
|
if (block.name === 'Agent') {
|
|
273
|
-
if (block.id) msg.toolUseId = block.id;
|
|
274
359
|
if (inp) {
|
|
275
360
|
msg.agentType = inp.subagent_type || null;
|
|
276
361
|
if (inp.prompt) msg.agentPrompt = inp.prompt;
|
|
@@ -282,13 +367,33 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
282
367
|
} else if (obj.type === 'user' && obj.message?.role === 'user' && !obj.isMeta) {
|
|
283
368
|
if (typeof obj.message.content === 'string') {
|
|
284
369
|
const t = obj.message.content;
|
|
370
|
+
const sysLabel = getSystemMessageLabel(t);
|
|
371
|
+
if (sysLabel === '__skip__') continue;
|
|
285
372
|
const uTruncated = t.length > 500;
|
|
286
373
|
messages.push({
|
|
287
374
|
type: 'user',
|
|
288
375
|
text: uTruncated ? t.slice(0, 500) + '...' : t,
|
|
289
376
|
fullText: uTruncated ? t : null,
|
|
290
|
-
timestamp: obj.timestamp
|
|
377
|
+
timestamp: obj.timestamp,
|
|
378
|
+
...(sysLabel && { systemLabel: sysLabel })
|
|
291
379
|
});
|
|
380
|
+
} else if (Array.isArray(obj.message.content)) {
|
|
381
|
+
for (const block of obj.message.content) {
|
|
382
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
383
|
+
let resultText = '';
|
|
384
|
+
if (typeof block.content === 'string') {
|
|
385
|
+
resultText = block.content;
|
|
386
|
+
} else if (Array.isArray(block.content)) {
|
|
387
|
+
resultText = block.content
|
|
388
|
+
.filter(c => c.type === 'text' && c.text)
|
|
389
|
+
.map(c => c.text)
|
|
390
|
+
.join('\n');
|
|
391
|
+
}
|
|
392
|
+
if (resultText) {
|
|
393
|
+
toolResults.set(block.tool_use_id, resultText);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
292
397
|
}
|
|
293
398
|
}
|
|
294
399
|
} catch (e) { /* partial line */ }
|
|
@@ -298,8 +403,20 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
298
403
|
readSize *= 4;
|
|
299
404
|
}
|
|
300
405
|
|
|
406
|
+
// Attach tool results to their corresponding tool_use messages
|
|
407
|
+
for (const msg of messages) {
|
|
408
|
+
if (msg.type === 'tool_use' && msg.toolUseId && toolResults.has(msg.toolUseId)) {
|
|
409
|
+
const full = toolResults.get(msg.toolUseId);
|
|
410
|
+
const truncated = full.length > TOOL_RESULT_MAX;
|
|
411
|
+
msg.toolResult = truncated ? full.slice(0, TOOL_RESULT_MAX) + '\n... (truncated)' : full;
|
|
412
|
+
msg.toolResultTruncated = truncated;
|
|
413
|
+
if (truncated) msg.toolResultFull = full;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
301
417
|
require('fs').closeSync(fd);
|
|
302
418
|
fd = null;
|
|
419
|
+
messages.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
303
420
|
return messages.slice(-limit);
|
|
304
421
|
} catch (e) {
|
|
305
422
|
if (fd) try { require('fs').closeSync(fd); } catch (_) {}
|
|
@@ -343,6 +460,44 @@ function buildAgentProgressMap(jsonlPath) {
|
|
|
343
460
|
return map;
|
|
344
461
|
}
|
|
345
462
|
|
|
463
|
+
function readCompactSummaries(jsonlPath) {
|
|
464
|
+
const results = [];
|
|
465
|
+
try {
|
|
466
|
+
const subagentsDir = path.join(path.dirname(jsonlPath), path.basename(jsonlPath, '.jsonl'), 'subagents');
|
|
467
|
+
const files = readdirSync(subagentsDir).filter(f => f.startsWith('agent-acompact-') && f.endsWith('.jsonl'));
|
|
468
|
+
for (const file of files) {
|
|
469
|
+
const filePath = path.join(subagentsDir, file);
|
|
470
|
+
const content = readFileSync(filePath, 'utf8');
|
|
471
|
+
const lines = content.split('\n');
|
|
472
|
+
// Use last entry timestamp (closest to when compaction completed)
|
|
473
|
+
let lastTs;
|
|
474
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
475
|
+
if (!lines[i].trim()) continue;
|
|
476
|
+
try { lastTs = JSON.parse(lines[i]).timestamp; if (lastTs) break; } catch (_) {}
|
|
477
|
+
}
|
|
478
|
+
if (!lastTs) continue;
|
|
479
|
+
// Find the last assistant message with a <summary> tag
|
|
480
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
481
|
+
if (!lines[i].trim()) continue;
|
|
482
|
+
try {
|
|
483
|
+
const obj = JSON.parse(lines[i]);
|
|
484
|
+
if (obj.type !== 'assistant') continue;
|
|
485
|
+
const blocks = obj.message?.content;
|
|
486
|
+
if (!Array.isArray(blocks)) continue;
|
|
487
|
+
let found = false;
|
|
488
|
+
for (const b of blocks) {
|
|
489
|
+
if (b.type !== 'text' || !b.text) continue;
|
|
490
|
+
const match = b.text.match(/<summary>([\s\S]*?)(?:<\/summary>|$)/);
|
|
491
|
+
if (match) { results.push({ timestamp: lastTs, summary: match[1].trim() }); found = true; break; }
|
|
492
|
+
}
|
|
493
|
+
if (found) break;
|
|
494
|
+
} catch (_) {}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} catch (_) {}
|
|
498
|
+
return results.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
499
|
+
}
|
|
500
|
+
|
|
346
501
|
module.exports = {
|
|
347
502
|
parseTask,
|
|
348
503
|
parseAgent,
|
|
@@ -352,5 +507,6 @@ module.exports = {
|
|
|
352
507
|
parseJsonlLine,
|
|
353
508
|
readSessionInfoFromJsonl,
|
|
354
509
|
readRecentMessages,
|
|
355
|
-
buildAgentProgressMap
|
|
510
|
+
buildAgentProgressMap,
|
|
511
|
+
readCompactSummaries
|
|
356
512
|
};
|