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 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
- // 2. Hook script
56
- console.log(`\n Hook script: ${dim(HOOK_SCRIPT_DEST)}`);
57
- let hookInstalled = false;
58
- if (fs.existsSync(HOOK_SCRIPT_DEST)) {
59
- const existing = fs.readFileSync(HOOK_SCRIPT_DEST, 'utf8');
60
- const bundled = fs.readFileSync(HOOK_SCRIPT_SRC, 'utf8');
61
- if (existing === bundled) {
62
- console.log(` ${green('✓')} Up to date`);
63
- hookInstalled = true;
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(HOOK_SCRIPT_SRC, HOOK_SCRIPT_DEST);
68
- try { fs.chmodSync(HOOK_SCRIPT_DEST, 0o755); } catch {}
68
+ fs.copyFileSync(src, dest);
69
+ try { fs.chmodSync(dest, 0o755); } catch {}
69
70
  console.log(` ${green('✓')} Updated`);
70
- hookInstalled = true;
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(HOOK_SCRIPT_SRC, HOOK_SCRIPT_DEST);
79
- try { fs.chmodSync(HOOK_SCRIPT_DEST, 0o755); } catch {}
78
+ fs.copyFileSync(src, dest);
79
+ try { fs.chmodSync(dest, 0o755); } catch {}
80
80
  console.log(` ${green('✓')} Installed and set executable`);
81
- hookInstalled = true;
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 script
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.1",
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
  }