claude-code-kanban 3.10.0 → 4.1.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/lib/parsers.js CHANGED
@@ -118,16 +118,23 @@ const TOOL_RESULT_MAX = 1500;
118
118
  const USER_TEXT_MAX = 500;
119
119
  const INTERRUPT_MARKER = '[Request interrupted by user]';
120
120
 
121
- function pushUserMessage(messages, text, timestamp, sysLabel) {
121
+ function pushUserMessage(messages, text, timestamp, sysLabel, extras) {
122
122
  if (sysLabel === '__skip__') return;
123
- const truncated = text.length > USER_TEXT_MAX;
124
- messages.push({
123
+ const safeText = text || '';
124
+ const truncated = safeText.length > USER_TEXT_MAX;
125
+ const msg = {
125
126
  type: 'user',
126
- text: truncated ? text.slice(0, USER_TEXT_MAX) + '...' : text,
127
- fullText: truncated ? text : null,
127
+ text: truncated ? safeText.slice(0, USER_TEXT_MAX) + '...' : safeText,
128
+ fullText: truncated ? safeText : null,
128
129
  timestamp,
129
130
  ...(sysLabel && { systemLabel: sysLabel })
130
- });
131
+ };
132
+ if (extras) {
133
+ if (extras.uuid) msg.uuid = extras.uuid;
134
+ if (extras.images && extras.images.length) msg.images = extras.images;
135
+ if (extras.toolResultRefs && extras.toolResultRefs.length) msg.toolResultRefs = extras.toolResultRefs;
136
+ }
137
+ messages.push(msg);
131
138
  }
132
139
 
133
140
  // Cache: jsonlPath -> { scannedUpTo, customTitle }
@@ -532,16 +539,19 @@ function readRecentMessages(jsonlPath, limit = 10) {
532
539
  }
533
540
  pushUserMessage(messages, t, obj.timestamp, getSystemMessageLabel(t));
534
541
  } else if (Array.isArray(obj.message.content)) {
535
- const joined = obj.message.content
536
- .filter(b => b.type === 'text' && typeof b.text === 'string' && b.text)
537
- .map(b => b.text)
538
- .join('\n')
539
- .trim();
540
- if (joined && joined !== INTERRUPT_MARKER) {
541
- pushUserMessage(messages, joined, obj.timestamp, getSystemMessageLabel(joined));
542
- }
543
- for (const block of obj.message.content) {
544
- if (block.type === 'tool_result' && block.tool_use_id) {
542
+ const texts = [];
543
+ const images = [];
544
+ const toolResultRefs = [];
545
+ obj.message.content.forEach((block, idx) => {
546
+ if (block.type === 'text' && typeof block.text === 'string' && block.text) {
547
+ texts.push(block.text);
548
+ } else if (block.type === 'image' && block.source && block.source.type === 'base64') {
549
+ images.push({
550
+ blockIndex: idx,
551
+ mediaType: block.source.media_type || 'image/png',
552
+ dataLen: typeof block.source.data === 'string' ? block.source.data.length : 0
553
+ });
554
+ } else if (block.type === 'tool_result' && block.tool_use_id) {
545
555
  let resultText = '';
546
556
  if (typeof block.content === 'string') {
547
557
  resultText = block.content;
@@ -554,7 +564,23 @@ function readRecentMessages(jsonlPath, limit = 10) {
554
564
  if (resultText) {
555
565
  toolResults.set(block.tool_use_id, resultText);
556
566
  }
567
+ toolResultRefs.push({
568
+ toolUseId: block.tool_use_id,
569
+ preview: resultText ? resultText.slice(0, 200) : ''
570
+ });
557
571
  }
572
+ });
573
+ const joined = texts.join('\n').trim();
574
+ const hasText = joined && joined !== INTERRUPT_MARKER;
575
+ const hasImages = images.length > 0;
576
+ if (hasText || hasImages) {
577
+ pushUserMessage(
578
+ messages,
579
+ joined,
580
+ obj.timestamp,
581
+ getSystemMessageLabel(joined),
582
+ { uuid: obj.uuid, images, toolResultRefs: hasText ? toolResultRefs : [] }
583
+ );
558
584
  }
559
585
  }
560
586
  }
@@ -620,6 +646,31 @@ function readFullToolResult(jsonlPath, toolUseId) {
620
646
  return null;
621
647
  }
622
648
 
649
+ function readUserImage(jsonlPath, msgUuid, blockIndex) {
650
+ if (!msgUuid || !jsonlPath) return null;
651
+ const idx = Number(blockIndex);
652
+ if (!Number.isInteger(idx) || idx < 0) return null;
653
+ try {
654
+ const content = readFileSync(jsonlPath, 'utf8');
655
+ const lines = content.split('\n');
656
+ for (const line of lines) {
657
+ if (!line || line.indexOf(msgUuid) === -1) continue;
658
+ try {
659
+ const obj = JSON.parse(line);
660
+ if (obj?.uuid !== msgUuid) continue;
661
+ if (!Array.isArray(obj?.message?.content)) continue;
662
+ const block = obj.message.content[idx];
663
+ if (!block || block.type !== 'image' || !block.source || block.source.type !== 'base64') return null;
664
+ return {
665
+ mediaType: block.source.media_type || 'image/png',
666
+ data: block.source.data
667
+ };
668
+ } catch (_) {}
669
+ }
670
+ } catch (_) {}
671
+ return null;
672
+ }
673
+
623
674
  function readMessagesPage(jsonlPath, limit = 10, beforeTimestamp = null) {
624
675
  const fetchLimit = limit + 1;
625
676
  const applyFilter = beforeTimestamp
@@ -913,6 +964,7 @@ module.exports = {
913
964
  readRecentMessages,
914
965
  readMessagesPage,
915
966
  readFullToolResult,
967
+ readUserImage,
916
968
  buildAgentProgressMap,
917
969
  buildSessionDigest,
918
970
  readCompactSummaries,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "3.10.0",
3
+ "version": "4.1.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": {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "1.1.5",
3
+ "version": "2.1.0",
4
4
  "description": "Agent activity tracking for claude-code-kanban dashboard"
5
5
  }
@@ -70,6 +70,16 @@
70
70
  "timeout": 5
71
71
  }
72
72
  ]
73
+ },
74
+ {
75
+ "matcher": "ExitPlanMode",
76
+ "hooks": [
77
+ {
78
+ "type": "command",
79
+ "command": "${CLAUDE_PLUGIN_ROOT}/scripts/agent-spy.sh",
80
+ "timeout": 5
81
+ }
82
+ ]
73
83
  }
74
84
  ],
75
85
  "PostToolUse": [
@@ -1,6 +1,7 @@
1
1
  #!/bin/bash
2
- # Tracks subagent lifecycle: one JSON file per agent, grouped by session
3
- # Layout: ~/.claude/.cck/agent-activity/{sessionId}/{agentId}.json
2
+ # Tracks subagent lifecycle: one append-only JSONL file per agent, grouped by session
3
+ # Layout: ~/.claude/.cck/agent-activity/{sessionId}/{agentId}.jsonl
4
+ # Each line is a lifecycle event (start | idle | stop). Server folds last-line-wins.
4
5
 
5
6
  INPUT=$(cat)
6
7
 
@@ -37,17 +38,17 @@ if [ "$EVENT" = "SessionStart" ]; then
37
38
  fi
38
39
 
39
40
  # PostToolUse / non-waiting PreToolUse: clear waiting state
40
- if [ "$EVENT" = "PostToolUse" ] || { [ "$EVENT" = "PreToolUse" ] && [ "$TOOL_NAME" != "AskUserQuestion" ]; }; then
41
+ if [ "$EVENT" = "PostToolUse" ] || { [ "$EVENT" = "PreToolUse" ] && [ "$TOOL_NAME" != "AskUserQuestion" ] && [ "$TOOL_NAME" != "ExitPlanMode" ]; }; then
41
42
  WFILE="$CCK_ACTIVITY/$SESSION_ID/_waiting.json"
42
43
  rm -f "$WFILE"
43
44
  [ "$EVENT" = "PostToolUse" ] && exit 0
44
45
  fi
45
46
 
46
- # Plan mode tools don't fire PostToolUse — skip to avoid stale markers
47
- [ "$TOOL_NAME" = "EnterPlanMode" ] || [ "$TOOL_NAME" = "ExitPlanMode" ] && exit 0
47
+ # EnterPlanMode has no waiting semantics — skip
48
+ [ "$TOOL_NAME" = "EnterPlanMode" ] && exit 0
48
49
 
49
50
  # Waiting-for-user events → write _waiting.json marker
50
- if [ "$EVENT" = "PermissionRequest" ] || { [ "$EVENT" = "PreToolUse" ] && [ "$TOOL_NAME" = "AskUserQuestion" ]; }; then
51
+ if [ "$EVENT" = "PermissionRequest" ] || { [ "$EVENT" = "PreToolUse" ] && { [ "$TOOL_NAME" = "AskUserQuestion" ] || [ "$TOOL_NAME" = "ExitPlanMode" ]; }; }; then
51
52
  DIR="$CCK_ACTIVITY/$SESSION_ID"
52
53
  mkdir -p "$DIR"
53
54
  KIND="permission"
@@ -70,23 +71,16 @@ if [ "$EVENT" = "TeammateIdle" ] && [ -z "$AGENT_ID" ] && [ -n "$TEAMMATE_NAME"
70
71
  [ ! -f "$MAP_FILE" ] && exit 0
71
72
  AGENT_ID=$(cat "$MAP_FILE")
72
73
  [ -z "$AGENT_ID" ] && exit 0
73
- FILE="$DIR/$AGENT_ID.json"
74
+ FILE="$DIR/$AGENT_ID.jsonl"
74
75
  TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
75
- STARTED_AT="$TS"
76
- if [ -f "$FILE" ]; then
77
- PREV_START=$(jq -r '.startedAt // ""' "$FILE" 2>/dev/null)
78
- [ -n "$PREV_START" ] && STARTED_AT="$PREV_START"
79
- fi
80
- cat > "$FILE" <<EOF
81
- {"agentId":"$AGENT_ID","type":"$TEAMMATE_NAME","status":"idle","startedAt":"$STARTED_AT","updatedAt":"$TS"}
82
- EOF
76
+ echo "{\"agentId\":\"$AGENT_ID\",\"type\":\"$TEAMMATE_NAME\",\"event\":\"idle\",\"status\":\"idle\",\"updatedAt\":\"$TS\"}" >> "$FILE"
83
77
  exit 0
84
78
  fi
85
79
 
86
80
  [ -z "$AGENT_ID" ] && exit 0
87
81
 
88
82
  DIR="$CCK_ACTIVITY/$SESSION_ID"
89
- FILE="$DIR/$AGENT_ID.json"
83
+ FILE="$DIR/$AGENT_ID.jsonl"
90
84
 
91
85
  # On Start: skip if no type (internal agents like AskUserQuestion)
92
86
  # On Stop/Idle: only skip if no existing file (never tracked)
@@ -102,48 +96,19 @@ mkdir -p "$DIR"
102
96
  TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
103
97
 
104
98
  if [ "$EVENT" = "SubagentStart" ]; then
105
- cat > "$FILE" <<EOF
106
- {"agentId":"$AGENT_ID","type":"$AGENT_TYPE_RAW","status":"active","startedAt":"$TS","updatedAt":"$TS"}
107
- EOF
108
- # Write name→id mapping for TeammateIdle resolution
109
- # Delete previous file only if not active (idle/stopped = teammate re-spawn, active = parallel subagent)
99
+ echo "{\"agentId\":\"$AGENT_ID\",\"type\":\"$AGENT_TYPE_RAW\",\"event\":\"start\",\"status\":\"active\",\"startedAt\":\"$TS\",\"updatedAt\":\"$TS\"}" >> "$FILE"
100
+ # Mapping always points at latest agent of this type (used by TeammateIdle resolution).
110
101
  if [ -n "$AGENT_TYPE_RAW" ]; then
111
- MAP_FILE="$DIR/_name-${AGENT_TYPE_RAW}.id"
112
- if [ -f "$MAP_FILE" ]; then
113
- OLD_ID=$(cat "$MAP_FILE")
114
- if [ -n "$OLD_ID" ] && [ "$OLD_ID" != "$AGENT_ID" ]; then
115
- OLD_FILE="$DIR/$OLD_ID.json"
116
- OLD_STATUS=""
117
- [ -f "$OLD_FILE" ] && OLD_STATUS=$(jq -r '.status // ""' "$OLD_FILE" 2>/dev/null)
118
- [ "$OLD_STATUS" != "active" ] && rm -f "$OLD_FILE"
119
- fi
120
- fi
121
- echo -n "$AGENT_ID" > "$MAP_FILE"
102
+ echo -n "$AGENT_ID" > "$DIR/_name-${AGENT_TYPE_RAW}.id"
122
103
  fi
123
104
 
124
105
  elif [ "$EVENT" = "SubagentStop" ]; then
125
- AGENT_TYPE="$AGENT_TYPE_RAW"
126
- STARTED_AT="$TS"
127
- if [ -f "$FILE" ]; then
128
- eval "$(jq -r '@sh "PREV_TYPE=\(.type // "unknown")", @sh "PREV_START=\(.startedAt // "")"' "$FILE")"
129
- [ -z "$AGENT_TYPE" ] && AGENT_TYPE="$PREV_TYPE"
130
- [ -n "$PREV_START" ] && STARTED_AT="$PREV_START"
131
- fi
132
106
  echo "$INPUT" | jq -c \
133
- --arg id "$AGENT_ID" --arg type "$AGENT_TYPE" --arg started "$STARTED_AT" --arg ts "$TS" \
134
- '{agentId: $id, type: $type, status: "stopped", startedAt: $started,
107
+ --arg id "$AGENT_ID" --arg type "$AGENT_TYPE_RAW" --arg ts "$TS" \
108
+ '{agentId: $id, type: $type, event: "stop", status: "stopped",
135
109
  lastMessage: (.last_assistant_message // ""), stoppedAt: $ts, updatedAt: $ts}' \
136
- > "$FILE"
110
+ >> "$FILE"
137
111
 
138
112
  elif [ "$EVENT" = "TeammateIdle" ]; then
139
- AGENT_TYPE="$AGENT_TYPE_RAW"
140
- STARTED_AT="$TS"
141
- if [ -f "$FILE" ]; then
142
- eval "$(jq -r '@sh "PREV_TYPE=\(.type // "unknown")", @sh "PREV_START=\(.startedAt // "")"' "$FILE")"
143
- [ -z "$AGENT_TYPE" ] && AGENT_TYPE="$PREV_TYPE"
144
- [ -n "$PREV_START" ] && STARTED_AT="$PREV_START"
145
- fi
146
- cat > "$FILE" <<EOF
147
- {"agentId":"$AGENT_ID","type":"$AGENT_TYPE","status":"idle","startedAt":"$STARTED_AT","updatedAt":"$TS"}
148
- EOF
113
+ echo "{\"agentId\":\"$AGENT_ID\",\"type\":\"$AGENT_TYPE_RAW\",\"event\":\"idle\",\"status\":\"idle\",\"updatedAt\":\"$TS\"}" >> "$FILE"
149
114
  fi