claude-code-kanban 4.10.0 → 4.11.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/package.json +1 -1
- package/server.js +112 -12
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -74,6 +74,7 @@ const TASKS_DIR = path.join(CLAUDE_DIR, 'tasks');
|
|
|
74
74
|
const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
|
75
75
|
const TEAMS_DIR = path.join(CLAUDE_DIR, 'teams');
|
|
76
76
|
const PLANS_DIR = path.join(CLAUDE_DIR, 'plans');
|
|
77
|
+
const SESSIONS_DIR = path.join(CLAUDE_DIR, 'sessions');
|
|
77
78
|
const CCK_DIR = path.join(CLAUDE_DIR, '.cck');
|
|
78
79
|
const AGENT_ACTIVITY_DIR = path.join(CCK_DIR, 'agent-activity');
|
|
79
80
|
const CONTEXT_STATUS_DIR = path.join(CCK_DIR, 'context-status');
|
|
@@ -284,6 +285,73 @@ function isAutoSelfTeam(cfg) {
|
|
|
284
285
|
return namedSession && soleLead;
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
// Claude Code 2.1.x stores a session's tasks in its self-team list (tasks/session-<id>/).
|
|
289
|
+
// Usually `cfg.leadSessionId` is that session and already has a card. But a resumed /
|
|
290
|
+
// continued session keeps writing to the original team's list while running under a new
|
|
291
|
+
// session id, so `leadSessionId` points at the original (often a ghost with no card) and
|
|
292
|
+
// the tasks can't be matched to the live session by id. The on-disk bridge is the
|
|
293
|
+
// live-session registry (~/.claude/sessions/<pid>.json): the team's `createdAt` ≈ the
|
|
294
|
+
// owning session's `startedAt` (both written at boot) and they share a cwd. Match on that.
|
|
295
|
+
const SELF_TEAM_BOOT_WINDOW_MS = 60 * 1000;
|
|
296
|
+
let liveSessionsCache = null;
|
|
297
|
+
let lastLiveSessionsScan = 0;
|
|
298
|
+
const LIVE_SESSIONS_TTL = 5000;
|
|
299
|
+
|
|
300
|
+
function loadLiveSessions() {
|
|
301
|
+
const now = Date.now();
|
|
302
|
+
if (liveSessionsCache && now - lastLiveSessionsScan < LIVE_SESSIONS_TTL) return liveSessionsCache;
|
|
303
|
+
const sessions = [];
|
|
304
|
+
if (existsSync(SESSIONS_DIR)) {
|
|
305
|
+
try {
|
|
306
|
+
for (const file of readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'))) {
|
|
307
|
+
try {
|
|
308
|
+
const s = JSON.parse(readFileSync(path.join(SESSIONS_DIR, file), 'utf8'));
|
|
309
|
+
if (s?.sessionId && s.kind === 'interactive') {
|
|
310
|
+
sessions.push({ sessionId: s.sessionId, cwd: s.cwd || null, startedAt: s.startedAt || 0 });
|
|
311
|
+
}
|
|
312
|
+
} catch (_) { /* skip invalid */ }
|
|
313
|
+
}
|
|
314
|
+
} catch (_) { /* ignore */ }
|
|
315
|
+
}
|
|
316
|
+
liveSessionsCache = sessions;
|
|
317
|
+
lastLiveSessionsScan = now;
|
|
318
|
+
return sessions;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Given a self-team config, return the live interactive session id that owns it
|
|
322
|
+
// (same cwd, startedAt within the boot window of the team's createdAt), or null.
|
|
323
|
+
function resolveSelfTeamOwner(cfg) {
|
|
324
|
+
if (!cfg?.createdAt) return null;
|
|
325
|
+
const teamCwd = cfg.members?.[0]?.cwd;
|
|
326
|
+
if (!teamCwd) return null;
|
|
327
|
+
let best = null, bestDelta = Infinity;
|
|
328
|
+
for (const s of loadLiveSessions()) {
|
|
329
|
+
if (s.cwd !== teamCwd) continue;
|
|
330
|
+
const delta = Math.abs(s.startedAt - cfg.createdAt);
|
|
331
|
+
if (delta <= SELF_TEAM_BOOT_WINDOW_MS && delta < bestDelta) {
|
|
332
|
+
best = s.sessionId;
|
|
333
|
+
bestDelta = delta;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return best;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Attach a team-named task dir's counts to a session card, preferring it over an empty or
|
|
340
|
+
// smaller task dir (a team session can also have a near-empty UUID-named dir). Caller passes
|
|
341
|
+
// the already-computed counts.
|
|
342
|
+
function attachTeamTasks(card, teamTaskDir, teamName, counts) {
|
|
343
|
+
if (!card.tasksDir || counts.taskCount > (card.taskCount || 0)) {
|
|
344
|
+
Object.assign(card, {
|
|
345
|
+
taskCount: counts.taskCount,
|
|
346
|
+
completed: counts.completed,
|
|
347
|
+
inProgress: counts.inProgress,
|
|
348
|
+
pending: counts.pending,
|
|
349
|
+
tasksDir: teamTaskDir,
|
|
350
|
+
sharedTaskList: teamName,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
287
355
|
// SSE clients for live updates
|
|
288
356
|
const clients = new Set();
|
|
289
357
|
|
|
@@ -381,13 +449,18 @@ function getCustomTaskDir(sessionId) {
|
|
|
381
449
|
const dir = path.join(TASKS_DIR, taskListName);
|
|
382
450
|
if (existsSync(dir)) return dir;
|
|
383
451
|
}
|
|
384
|
-
// Check team-named task directory (teams store tasks under ~/.claude/tasks/<teamName>/)
|
|
452
|
+
// Check team-named task directory (teams store tasks under ~/.claude/tasks/<teamName>/).
|
|
453
|
+
// Match either the recorded leadSessionId, or — for 2.1.x self-teams whose lead is a
|
|
454
|
+
// team-lead agent id — the live interactive session that owns the team (see resolveSelfTeamOwner).
|
|
385
455
|
if (existsSync(TEAMS_DIR)) {
|
|
386
456
|
try {
|
|
387
457
|
for (const dir of readdirSync(TEAMS_DIR, { withFileTypes: true })) {
|
|
388
458
|
if (!dir.isDirectory()) continue;
|
|
389
459
|
const cfg = loadTeamConfig(dir.name);
|
|
390
|
-
if (cfg
|
|
460
|
+
if (!cfg) continue;
|
|
461
|
+
const owns = cfg.leadSessionId === sessionId
|
|
462
|
+
|| (isAutoSelfTeam(cfg) && resolveSelfTeamOwner(cfg) === sessionId);
|
|
463
|
+
if (owns) {
|
|
391
464
|
const teamTaskDir = path.join(TASKS_DIR, dir.name);
|
|
392
465
|
if (existsSync(teamTaskDir)) return teamTaskDir;
|
|
393
466
|
}
|
|
@@ -949,7 +1022,42 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
949
1022
|
// auto-created session-<uuid> self-team dir leaves a duplicate session card whose
|
|
950
1023
|
// id (session-<uuid>) resolves no messages, so switching to it shows a stale log.
|
|
951
1024
|
if (sessionsMap.has(dir.name) && dir.name !== leaderId) sessionsMap.delete(dir.name);
|
|
952
|
-
if (isAutoSelfTeam(cfg))
|
|
1025
|
+
if (isAutoSelfTeam(cfg)) {
|
|
1026
|
+
// Self-teams are normally noise with an empty team-named task dir. But 2.1.x stores a
|
|
1027
|
+
// session's tasks in the self-team list (tasks/session-<id>/), so when it's non-empty
|
|
1028
|
+
// the tasks would be silently orphaned. Recover them (gated on taskCount > 0 to keep
|
|
1029
|
+
// the empty-self-team noise case suppressed): attach to the owning card — the recorded
|
|
1030
|
+
// leadSessionId when it has one, else the live session continuing it (resolved from the
|
|
1031
|
+
// session registry), else a freshly-built fallback lead card.
|
|
1032
|
+
const teamTaskDir = path.join(TASKS_DIR, dir.name);
|
|
1033
|
+
if (!existsSync(teamTaskDir)) continue;
|
|
1034
|
+
const counts = getTaskCounts(teamTaskDir);
|
|
1035
|
+
if (counts.taskCount === 0) continue;
|
|
1036
|
+
|
|
1037
|
+
const ownerCard = sessionsMap.get(leaderId) || sessionsMap.get(resolveSelfTeamOwner(cfg));
|
|
1038
|
+
if (ownerCard) {
|
|
1039
|
+
attachTeamTasks(ownerCard, teamTaskDir, dir.name, counts);
|
|
1040
|
+
} else {
|
|
1041
|
+
const meta = metadata[leaderId] || {};
|
|
1042
|
+
const logStat = getSessionLogStat(meta);
|
|
1043
|
+
const logMtime = logStat.mtime;
|
|
1044
|
+
const logAge = logMtime ? Date.now() - logMtime : Infinity;
|
|
1045
|
+
const agentDir = path.join(AGENT_ACTIVITY_DIR, leaderId);
|
|
1046
|
+
const agentStatus = checkAgentStatus(agentDir, logAge > AGENT_STALE_MS, logMtime, false);
|
|
1047
|
+
const taskMtime = counts.newestTaskMtime ? counts.newestTaskMtime.getTime() : 0;
|
|
1048
|
+
const card = buildSessionObject(leaderId, meta, {
|
|
1049
|
+
_logStat: logStat,
|
|
1050
|
+
name: getSessionDisplayName(leaderId, meta) || cfg.name || dir.name,
|
|
1051
|
+
modifiedAt: new Date(Math.max(taskMtime, logMtime || 0)).toISOString(),
|
|
1052
|
+
hasActiveAgents: agentStatus.hasActive,
|
|
1053
|
+
hasRunningAgents: agentStatus.hasRunning,
|
|
1054
|
+
hasWaitingForUser: !!agentStatus.waitingForUser,
|
|
1055
|
+
});
|
|
1056
|
+
attachTeamTasks(card, teamTaskDir, dir.name, counts);
|
|
1057
|
+
sessionsMap.set(leaderId, card);
|
|
1058
|
+
}
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
953
1061
|
const existing = sessionsMap.get(leaderId);
|
|
954
1062
|
if (existing) {
|
|
955
1063
|
existing.isTeam = true;
|
|
@@ -963,15 +1071,7 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
963
1071
|
// holding the real tasks, the leader card otherwise shows 0/0.
|
|
964
1072
|
const teamTaskDir = path.join(TASKS_DIR, dir.name);
|
|
965
1073
|
if (existsSync(teamTaskDir)) {
|
|
966
|
-
|
|
967
|
-
if (!existing.tasksDir || counts.taskCount > (existing.taskCount || 0)) {
|
|
968
|
-
existing.taskCount = counts.taskCount;
|
|
969
|
-
existing.completed = counts.completed;
|
|
970
|
-
existing.inProgress = counts.inProgress;
|
|
971
|
-
existing.pending = counts.pending;
|
|
972
|
-
existing.tasksDir = teamTaskDir;
|
|
973
|
-
existing.sharedTaskList = dir.name;
|
|
974
|
-
}
|
|
1074
|
+
attachTeamTasks(existing, teamTaskDir, dir.name, getTaskCounts(teamTaskDir));
|
|
975
1075
|
}
|
|
976
1076
|
// Re-check agent status with isTeam=true
|
|
977
1077
|
const agentDir = path.join(AGENT_ACTIVITY_DIR, leaderId);
|