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 +68 -16
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/hooks/hooks.json +10 -0
- package/plugin/plugins/claude-code-kanban/scripts/agent-spy.sh +17 -52
- package/public/app.js +221 -45
- package/public/index.html +6 -0
- package/public/style.css +160 -19
- package/server.js +193 -48
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
|
|
124
|
-
|
|
123
|
+
const safeText = text || '';
|
|
124
|
+
const truncated = safeText.length > USER_TEXT_MAX;
|
|
125
|
+
const msg = {
|
|
125
126
|
type: 'user',
|
|
126
|
-
text: truncated ?
|
|
127
|
-
fullText: truncated ?
|
|
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
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
.
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Tracks subagent lifecycle: one
|
|
3
|
-
# Layout: ~/.claude/.cck/agent-activity/{sessionId}/{agentId}.
|
|
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
|
-
#
|
|
47
|
-
[ "$TOOL_NAME" = "EnterPlanMode" ]
|
|
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.
|
|
74
|
+
FILE="$DIR/$AGENT_ID.jsonl"
|
|
74
75
|
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
75
|
-
|
|
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.
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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 "$
|
|
134
|
-
'{agentId: $id, type: $type,
|
|
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
|
-
|
|
110
|
+
>> "$FILE"
|
|
137
111
|
|
|
138
112
|
elif [ "$EVENT" = "TeammateIdle" ]; then
|
|
139
|
-
|
|
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
|