claude-code-kanban 2.0.1 → 2.1.0-rc.2
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 +32 -0
- package/hooks/context-status.sh +18 -0
- package/install.js +36 -22
- package/lib/parsers.js +51 -0
- package/package.json +6 -3
- package/public/app.js +3979 -0
- package/public/index.html +267 -5979
- package/public/style.css +3062 -0
- package/public/sw.js +4 -3
- package/server.js +160 -9
package/README.md
CHANGED
|
@@ -87,6 +87,38 @@ Claude Code writes task files to `~/.claude/tasks/` and conversation logs to `~/
|
|
|
87
87
|
| `TeammateIdle` | Idle detection for team member agents |
|
|
88
88
|
| `PostToolUse` | Waiting-for-user detection (permission prompts, AskUserQuestion) |
|
|
89
89
|
|
|
90
|
+
## Context Window Monitoring
|
|
91
|
+
|
|
92
|
+
Track real-time context window usage for each Claude Code session directly in the dashboard sidebar and detail panel.
|
|
93
|
+
|
|
94
|
+
### Setup
|
|
95
|
+
|
|
96
|
+
The installer copies `context-status.sh` alongside the agent hooks:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npx claude-code-kanban --install
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Then add it to your statusline in `~/.claude/settings.json`:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"statusLine": {
|
|
107
|
+
"type": "command",
|
|
108
|
+
"command": "~/.claude/hooks/context-status.sh | npx -y ccstatusline@latest",
|
|
109
|
+
"padding": 0
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The script pipes through — your existing statusline still works. It just writes a snapshot to `~/.claude/context-status/{sessionId}.json` on each update.
|
|
115
|
+
|
|
116
|
+
### What you get
|
|
117
|
+
|
|
118
|
+
- **Sidebar bar** — compact context usage bar per session with color thresholds (green → yellow → orange → red) and a 200K token marker
|
|
119
|
+
- **Detail panel** — input/output token breakdown, cache read tokens, cost, duration, API time, lines added/removed, and model name
|
|
120
|
+
- Only shown for active or pinned sessions
|
|
121
|
+
|
|
90
122
|
## FAQ
|
|
91
123
|
|
|
92
124
|
**Does this control Claude?**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Statusline spy: writes raw context data for kanban dashboard, passes input through
|
|
3
|
+
# Layout: ~/.claude/context-status/{sessionId}.json
|
|
4
|
+
#
|
|
5
|
+
# Usage: pipe before your statusline command:
|
|
6
|
+
# "command": "~/.claude/hooks/context-status.sh | npx -y ccstatusline@latest"
|
|
7
|
+
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
|
|
10
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
|
|
11
|
+
if [ -n "$SESSION_ID" ]; then
|
|
12
|
+
DIR="$HOME/.claude/context-status"
|
|
13
|
+
mkdir -p "$DIR"
|
|
14
|
+
echo "$INPUT" > "$DIR/$SESSION_ID.json"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Pass through original input for downstream statusline
|
|
18
|
+
echo "$INPUT"
|
package/install.js
CHANGED
|
@@ -11,6 +11,8 @@ const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
|
|
|
11
11
|
const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
12
12
|
const HOOK_SCRIPT_DEST = path.join(HOOKS_DIR, 'agent-spy.sh');
|
|
13
13
|
const HOOK_SCRIPT_SRC = path.join(__dirname, 'hooks', 'agent-spy.sh');
|
|
14
|
+
const CTX_SCRIPT_DEST = path.join(HOOKS_DIR, 'context-status.sh');
|
|
15
|
+
const CTX_SCRIPT_SRC = path.join(__dirname, 'hooks', 'context-status.sh');
|
|
14
16
|
const AGENT_ACTIVITY_DIR = path.join(CLAUDE_DIR, 'agent-activity');
|
|
15
17
|
|
|
16
18
|
const HOOK_COMMAND = '~/.claude/hooks/agent-spy.sh';
|
|
@@ -52,36 +54,42 @@ async function runInstall() {
|
|
|
52
54
|
console.log(yellow('⚠ not found — hook script requires jq for JSON parsing'));
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
} else {
|
|
57
|
+
async function installScript(label, src, dest) {
|
|
58
|
+
console.log(`\n ${label}: ${dim(dest)}`);
|
|
59
|
+
if (fs.existsSync(dest)) {
|
|
60
|
+
const existing = fs.readFileSync(dest, 'utf8');
|
|
61
|
+
const bundled = fs.readFileSync(src, 'utf8');
|
|
62
|
+
if (existing === bundled) {
|
|
63
|
+
console.log(` ${green('✓')} Up to date`);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
65
66
|
if (await prompt(` Different version found. Update? [Y/n] `)) {
|
|
66
67
|
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
67
|
-
fs.copyFileSync(
|
|
68
|
-
try { fs.chmodSync(
|
|
68
|
+
fs.copyFileSync(src, dest);
|
|
69
|
+
try { fs.chmodSync(dest, 0o755); } catch {}
|
|
69
70
|
console.log(` ${green('✓')} Updated`);
|
|
70
|
-
|
|
71
|
-
} else {
|
|
72
|
-
console.log(` ${dim('Skipped')}`);
|
|
71
|
+
return true;
|
|
73
72
|
}
|
|
73
|
+
console.log(` ${dim('Skipped')}`);
|
|
74
|
+
return false;
|
|
74
75
|
}
|
|
75
|
-
} else {
|
|
76
76
|
if (await prompt(` Not found. Install? [Y/n] `)) {
|
|
77
77
|
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
78
|
-
fs.copyFileSync(
|
|
79
|
-
try { fs.chmodSync(
|
|
78
|
+
fs.copyFileSync(src, dest);
|
|
79
|
+
try { fs.chmodSync(dest, 0o755); } catch {}
|
|
80
80
|
console.log(` ${green('✓')} Installed and set executable`);
|
|
81
|
-
|
|
82
|
-
} else {
|
|
83
|
-
console.log(` ${dim('Skipped')}`);
|
|
81
|
+
return true;
|
|
84
82
|
}
|
|
83
|
+
console.log(` ${dim('Skipped')}`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. Hook scripts
|
|
88
|
+
const hookInstalled = await installScript('Hook script', HOOK_SCRIPT_SRC, HOOK_SCRIPT_DEST);
|
|
89
|
+
const ctxInstalled = await installScript('Context spy', CTX_SCRIPT_SRC, CTX_SCRIPT_DEST);
|
|
90
|
+
if (ctxInstalled) {
|
|
91
|
+
console.log(`\n ${yellow('To enable context tracking, pipe it before your statusline:')}`);
|
|
92
|
+
console.log(` ${dim('"statusLine": { "command": "~/.claude/hooks/context-status.sh | <your-statusline>" }')}`);
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
// 3. Settings.json
|
|
@@ -178,13 +186,19 @@ async function runUninstall() {
|
|
|
178
186
|
console.log(` Settings: ${dim('No settings.json found')}`);
|
|
179
187
|
}
|
|
180
188
|
|
|
181
|
-
// 2. Remove hook
|
|
189
|
+
// 2. Remove hook scripts
|
|
182
190
|
if (fs.existsSync(HOOK_SCRIPT_DEST)) {
|
|
183
191
|
fs.unlinkSync(HOOK_SCRIPT_DEST);
|
|
184
192
|
console.log(` Hook script: ${green('✓')} Removed`);
|
|
185
193
|
} else {
|
|
186
194
|
console.log(` Hook script: ${dim('Not found')}`);
|
|
187
195
|
}
|
|
196
|
+
if (fs.existsSync(CTX_SCRIPT_DEST)) {
|
|
197
|
+
fs.unlinkSync(CTX_SCRIPT_DEST);
|
|
198
|
+
console.log(` Context spy: ${green('✓')} Removed`);
|
|
199
|
+
} else {
|
|
200
|
+
console.log(` Context spy: ${dim('Not found')}`);
|
|
201
|
+
}
|
|
188
202
|
|
|
189
203
|
// 3. Optionally remove agent-activity data
|
|
190
204
|
if (fs.existsSync(AGENT_ACTIVITY_DIR)) {
|
package/lib/parsers.js
CHANGED
|
@@ -287,6 +287,11 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
287
287
|
return text;
|
|
288
288
|
}).join('\n\n');
|
|
289
289
|
}
|
|
290
|
+
else if (inp.plan) {
|
|
291
|
+
const titleMatch = inp.plan.match(/^#\s+(.+)/m);
|
|
292
|
+
detail = titleMatch ? titleMatch[1] : 'Plan';
|
|
293
|
+
fullDetail = detail;
|
|
294
|
+
}
|
|
290
295
|
else if (inp.description) { detail = inp.description; fullDetail = inp.description; }
|
|
291
296
|
}
|
|
292
297
|
const params = {};
|
|
@@ -343,6 +348,9 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
343
348
|
if (inp.model) params.model = inp.model;
|
|
344
349
|
if (inp.run_in_background) params.background = true;
|
|
345
350
|
if (inp.isolation) params.isolation = inp.isolation;
|
|
351
|
+
} else if (block.name === 'ExitPlanMode') {
|
|
352
|
+
if (inp.plan) params.plan = inp.plan;
|
|
353
|
+
if (inp.planFilePath) params.planFilePath = inp.planFilePath;
|
|
346
354
|
}
|
|
347
355
|
}
|
|
348
356
|
const msg = {
|
|
@@ -367,6 +375,49 @@ function readRecentMessages(jsonlPath, limit = 10) {
|
|
|
367
375
|
} else if (obj.type === 'user' && obj.message?.role === 'user' && !obj.isMeta) {
|
|
368
376
|
if (typeof obj.message.content === 'string') {
|
|
369
377
|
const t = obj.message.content;
|
|
378
|
+
const tmMatch = t.match(/<teammate-message\s+([^>]*)>([\s\S]*?)<\/teammate-message>/);
|
|
379
|
+
if (tmMatch) {
|
|
380
|
+
const attrs = tmMatch[1];
|
|
381
|
+
const body = tmMatch[2].trim();
|
|
382
|
+
const getAttr = (name) => (attrs.match(new RegExp(name + '="([^"]*)"')) || [])[1] || null;
|
|
383
|
+
const tid = getAttr('teammate_id');
|
|
384
|
+
const color = getAttr('color');
|
|
385
|
+
const summary = getAttr('summary');
|
|
386
|
+
let protocol = null;
|
|
387
|
+
try {
|
|
388
|
+
const j = JSON.parse(body);
|
|
389
|
+
if (j.type) protocol = j;
|
|
390
|
+
} catch (_) {}
|
|
391
|
+
const isIdle = protocol?.type === 'idle_notification';
|
|
392
|
+
const isProtocol = !!protocol;
|
|
393
|
+
let protocolLabel = null;
|
|
394
|
+
if (protocol) {
|
|
395
|
+
switch (protocol.type) {
|
|
396
|
+
case 'idle_notification': protocolLabel = protocol.idleReason || 'idle'; break;
|
|
397
|
+
case 'task_assignment': protocolLabel = `assigned #${protocol.taskId}: ${protocol.subject || ''}`; break;
|
|
398
|
+
case 'shutdown_request': protocolLabel = `shutdown: ${protocol.reason || 'requested'}`; break;
|
|
399
|
+
case 'shutdown_response': protocolLabel = protocol.approve ? 'shutdown approved' : `shutdown rejected: ${protocol.reason || ''}`; break;
|
|
400
|
+
case 'plan_approval_request': protocolLabel = 'plan approval requested'; break;
|
|
401
|
+
case 'plan_approval_response': protocolLabel = protocol.approve ? 'plan approved' : `plan rejected: ${protocol.feedback || ''}`; break;
|
|
402
|
+
default: protocolLabel = protocol.type; break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const truncated = !isProtocol && body.length > 500;
|
|
406
|
+
messages.push({
|
|
407
|
+
type: 'teammate',
|
|
408
|
+
teammateId: tid,
|
|
409
|
+
color,
|
|
410
|
+
summary,
|
|
411
|
+
isIdle,
|
|
412
|
+
isProtocol,
|
|
413
|
+
protocolType: protocol?.type || null,
|
|
414
|
+
protocolLabel,
|
|
415
|
+
text: isProtocol ? null : (truncated ? body.slice(0, 500) + '...' : body),
|
|
416
|
+
fullText: isProtocol ? null : (truncated ? body : null),
|
|
417
|
+
timestamp: obj.timestamp
|
|
418
|
+
});
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
370
421
|
const sysLabel = getSystemMessageLabel(t);
|
|
371
422
|
if (sysLabel === '__skip__') continue;
|
|
372
423
|
const uTruncated = t.length > 500;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-kanban",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.1.0-rc.2",
|
|
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": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"dev": "node server.js --open",
|
|
12
12
|
"test": "node --test test/contracts.test.js",
|
|
13
13
|
"test:hooks": "bash tests/test-agent-spy.sh",
|
|
14
|
-
"validate:schemas": "node test/validate-live-schemas.js"
|
|
14
|
+
"validate:schemas": "node test/validate-live-schemas.js",
|
|
15
|
+
"prepare": "husky"
|
|
15
16
|
},
|
|
16
17
|
"repository": {
|
|
17
18
|
"type": "git",
|
|
@@ -45,11 +46,13 @@
|
|
|
45
46
|
"server.js",
|
|
46
47
|
"install.js",
|
|
47
48
|
"hooks/agent-spy.sh",
|
|
49
|
+
"hooks/context-status.sh",
|
|
48
50
|
"lib/**/*",
|
|
49
51
|
"public/**/*"
|
|
50
52
|
],
|
|
51
53
|
"devDependencies": {
|
|
52
54
|
"ajv": "^8.18.0",
|
|
53
|
-
"ajv-formats": "^3.0.1"
|
|
55
|
+
"ajv-formats": "^3.0.1",
|
|
56
|
+
"husky": "^9.1.7"
|
|
54
57
|
}
|
|
55
58
|
}
|