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/public/sw.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const CACHE_NAME = 'cc-kanban-
|
|
2
|
-
const PRECACHE_URLS = ['/'];
|
|
1
|
+
const CACHE_NAME = 'cc-kanban-v2';
|
|
2
|
+
const PRECACHE_URLS = ['/', '/style.css', '/app.js'];
|
|
3
3
|
|
|
4
4
|
let cachePromise = null;
|
|
5
5
|
function getCache() {
|
|
@@ -9,7 +9,8 @@ function getCache() {
|
|
|
9
9
|
|
|
10
10
|
function cacheResponse(request, response) {
|
|
11
11
|
if (response.ok) {
|
|
12
|
-
|
|
12
|
+
const clone = response.clone();
|
|
13
|
+
getCache().then(cache => cache.put(request, clone));
|
|
13
14
|
}
|
|
14
15
|
return response;
|
|
15
16
|
}
|
package/server.js
CHANGED
|
@@ -51,10 +51,11 @@ const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
|
|
51
51
|
const TEAMS_DIR = path.join(CLAUDE_DIR, 'teams');
|
|
52
52
|
const PLANS_DIR = path.join(CLAUDE_DIR, 'plans');
|
|
53
53
|
const AGENT_ACTIVITY_DIR = path.join(CLAUDE_DIR, 'agent-activity');
|
|
54
|
+
const CONTEXT_STATUS_DIR = path.join(CLAUDE_DIR, 'context-status');
|
|
54
55
|
|
|
55
56
|
const PERMISSION_TTL_MS = 1800000;
|
|
56
57
|
const AGENT_TTL_MS = 3600000;
|
|
57
|
-
const AGENT_STALE_MS =
|
|
58
|
+
const AGENT_STALE_MS = 900000;
|
|
58
59
|
|
|
59
60
|
const WAITING_RESOLVE_GRACE_MS = 15000;
|
|
60
61
|
|
|
@@ -73,8 +74,18 @@ function checkWaitingForUser(agentDir, logMtime) {
|
|
|
73
74
|
return null;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
function isGhostAgent(agent) {
|
|
78
|
+
if (agent.startedAt !== agent.updatedAt || agent.lastMessage) return false;
|
|
79
|
+
return (Date.now() - new Date(agent.startedAt).getTime()) >= AGENT_STALE_MS;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getContextStatus(sessionId, meta) {
|
|
83
|
+
return contextStatusCache.get(sessionId) || (meta?.teamLeaderId ? contextStatusCache.get(meta.teamLeaderId) : null) || null;
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
function isAgentFresh(agent) {
|
|
77
87
|
if (!agent.updatedAt) return true;
|
|
88
|
+
if (isGhostAgent(agent)) return false;
|
|
78
89
|
return (Date.now() - new Date(agent.updatedAt).getTime()) < AGENT_TTL_MS;
|
|
79
90
|
}
|
|
80
91
|
|
|
@@ -98,7 +109,6 @@ function checkAgentStatus(agentDir, stale, logMtime) {
|
|
|
98
109
|
const agent = JSON.parse(readFileSync(path.join(agentDir, file), 'utf8'));
|
|
99
110
|
if (isAgentFresh(agent)) {
|
|
100
111
|
if (agent.status === 'active') { result.hasActive = true; result.hasRunning = true; }
|
|
101
|
-
else if (agent.status === 'idle') { result.hasActive = true; }
|
|
102
112
|
}
|
|
103
113
|
if (result.hasRunning && result.hasActive) break;
|
|
104
114
|
} catch (e) { /* skip invalid */ }
|
|
@@ -171,6 +181,7 @@ const MAX_CACHE_ENTRIES = 200;
|
|
|
171
181
|
const progressMapCache = new Map();
|
|
172
182
|
const compactSummaryCache = new Map();
|
|
173
183
|
const taskCountsCache = new Map();
|
|
184
|
+
const contextStatusCache = new Map();
|
|
174
185
|
|
|
175
186
|
function evictStaleCache(cache) {
|
|
176
187
|
if (cache.size <= MAX_CACHE_ENTRIES) return;
|
|
@@ -200,7 +211,8 @@ function getTaskCounts(sessionPath) {
|
|
|
200
211
|
} catch (e) { /* skip invalid */ }
|
|
201
212
|
}
|
|
202
213
|
|
|
203
|
-
const
|
|
214
|
+
const taskCount = completed + inProgress + pending;
|
|
215
|
+
const result = { taskCount, completed, inProgress, pending, newestTaskMtime };
|
|
204
216
|
taskCountsCache.set(sessionPath, result);
|
|
205
217
|
return result;
|
|
206
218
|
}
|
|
@@ -387,6 +399,9 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
387
399
|
const limitParam = req.query.limit || '20';
|
|
388
400
|
const limit = limitParam === 'all' ? null : parseInt(limitParam, 10);
|
|
389
401
|
|
|
402
|
+
const pinnedParam = req.query.pinned;
|
|
403
|
+
const pinnedIds = pinnedParam ? new Set(pinnedParam.split(',').filter(Boolean)) : new Set();
|
|
404
|
+
|
|
390
405
|
const metadata = loadSessionMetadata();
|
|
391
406
|
const sessionsMap = new Map();
|
|
392
407
|
|
|
@@ -416,12 +431,12 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
416
431
|
}
|
|
417
432
|
|
|
418
433
|
const isTeam = isTeamSession(entry.name);
|
|
419
|
-
const
|
|
434
|
+
const teamConfig = isTeam ? loadTeamConfig(entry.name) : null;
|
|
435
|
+
const memberCount = teamConfig?.members?.length || 0;
|
|
420
436
|
const planInfo = getPlanInfo(meta.slug);
|
|
421
437
|
|
|
422
438
|
const resolvedAgentDir = (() => {
|
|
423
|
-
const
|
|
424
|
-
const rid = (tc && tc.leadSessionId) ? tc.leadSessionId : entry.name;
|
|
439
|
+
const rid = teamConfig?.leadSessionId || entry.name;
|
|
425
440
|
return path.join(AGENT_ACTIVITY_DIR, rid);
|
|
426
441
|
})();
|
|
427
442
|
const agentStatus = checkAgentStatus(resolvedAgentDir, stale, logMtime);
|
|
@@ -450,6 +465,7 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
450
465
|
jsonlPath: meta.jsonlPath || null,
|
|
451
466
|
tasksDir: sessionPath,
|
|
452
467
|
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
468
|
+
contextStatus: getContextStatus(entry.name, meta),
|
|
453
469
|
...planInfo
|
|
454
470
|
});
|
|
455
471
|
}
|
|
@@ -495,6 +511,7 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
495
511
|
jsonlPath: meta.jsonlPath || null,
|
|
496
512
|
tasksDir: null,
|
|
497
513
|
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
514
|
+
contextStatus: getContextStatus(sessionId, meta),
|
|
498
515
|
...planInfo
|
|
499
516
|
});
|
|
500
517
|
}
|
|
@@ -572,9 +589,16 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
572
589
|
}
|
|
573
590
|
}
|
|
574
591
|
|
|
592
|
+
// Backfill contextStatus for already-built sessions that are pinned
|
|
593
|
+
for (const pid of pinnedIds) {
|
|
594
|
+
const s = sessionsMap.get(pid);
|
|
595
|
+
if (s && !s.contextStatus) {
|
|
596
|
+
const meta = metadata[pid];
|
|
597
|
+
s.contextStatus = getContextStatus(pid, meta);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
575
601
|
// Ensure pinned sessions are in the map even if they weren't discovered
|
|
576
|
-
const pinnedParam = req.query.pinned;
|
|
577
|
-
const pinnedIds = pinnedParam ? new Set(pinnedParam.split(',').filter(Boolean)) : new Set();
|
|
578
602
|
for (const pid of pinnedIds) {
|
|
579
603
|
if (sessionsMap.has(pid)) continue;
|
|
580
604
|
const meta = metadata[pid];
|
|
@@ -604,6 +628,7 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
604
628
|
jsonlPath: meta.jsonlPath || null,
|
|
605
629
|
tasksDir: null,
|
|
606
630
|
projectDir: meta.jsonlPath ? path.dirname(meta.jsonlPath) : null,
|
|
631
|
+
contextStatus: getContextStatus(pid, meta),
|
|
607
632
|
...getPlanInfo(meta.slug)
|
|
608
633
|
});
|
|
609
634
|
}
|
|
@@ -774,6 +799,7 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
774
799
|
for (const file of files) {
|
|
775
800
|
try {
|
|
776
801
|
const agent = JSON.parse(readFileSync(path.join(agentDir, file), 'utf8'));
|
|
802
|
+
if (isGhostAgent(agent)) continue;
|
|
777
803
|
const agentStale = !sessionStale && agent.updatedAt && (Date.now() - new Date(agent.updatedAt).getTime()) > AGENT_STALE_MS;
|
|
778
804
|
if (!isAgentFresh(agent) || sessionStale || agentStale) {
|
|
779
805
|
if (agent.status === 'active' || agent.status === 'idle') {
|
|
@@ -811,7 +837,7 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
811
837
|
|
|
812
838
|
app.post('/api/sessions/:sessionId/agents/:agentId/stop', (req, res) => {
|
|
813
839
|
const sessionId = resolveSessionId(req.params.sessionId);
|
|
814
|
-
const agentId =
|
|
840
|
+
const agentId = sanitizeAgentId(req.params.agentId);
|
|
815
841
|
const agentFile = path.join(AGENT_ACTIVITY_DIR, sessionId, agentId + '.json');
|
|
816
842
|
if (!existsSync(agentFile)) return res.status(404).json({ error: 'Agent not found' });
|
|
817
843
|
try {
|
|
@@ -828,6 +854,83 @@ app.post('/api/sessions/:sessionId/agents/:agentId/stop', (req, res) => {
|
|
|
828
854
|
}
|
|
829
855
|
});
|
|
830
856
|
|
|
857
|
+
function sanitizeAgentId(raw) {
|
|
858
|
+
return path.basename(raw).replace(/[^a-zA-Z0-9_-]/g, '');
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function subagentJsonlPath(meta, agentId) {
|
|
862
|
+
return path.join(
|
|
863
|
+
path.dirname(meta.jsonlPath),
|
|
864
|
+
path.basename(meta.jsonlPath, '.jsonl'),
|
|
865
|
+
'subagents',
|
|
866
|
+
'agent-' + agentId + '.jsonl'
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
app.get('/api/sessions/:sessionId/agents/:agentId/messages', (req, res) => {
|
|
871
|
+
const sessionId = resolveSessionId(req.params.sessionId);
|
|
872
|
+
const agentId = sanitizeAgentId(req.params.agentId);
|
|
873
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 50, 100);
|
|
874
|
+
const metadata = loadSessionMetadata();
|
|
875
|
+
const meta = metadata[sessionId];
|
|
876
|
+
if (!meta?.jsonlPath) return res.json({ messages: [], agentId });
|
|
877
|
+
const subagentJsonl = subagentJsonlPath(meta, agentId);
|
|
878
|
+
if (!existsSync(subagentJsonl)) return res.json({ messages: [], agentId });
|
|
879
|
+
const messages = readRecentMessages(subagentJsonl, limit);
|
|
880
|
+
res.json({ messages, agentId });
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
app.get('/api/sessions/:sessionId/agents/:agentId/messages/stream', (req, res) => {
|
|
884
|
+
const sessionId = resolveSessionId(req.params.sessionId);
|
|
885
|
+
const agentId = sanitizeAgentId(req.params.agentId);
|
|
886
|
+
const metadata = loadSessionMetadata();
|
|
887
|
+
const meta = metadata[sessionId];
|
|
888
|
+
if (!meta?.jsonlPath) {
|
|
889
|
+
res.status(404).json({ error: 'Session not found' });
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const subagentJsonl = subagentJsonlPath(meta, agentId);
|
|
893
|
+
|
|
894
|
+
res.writeHead(200, {
|
|
895
|
+
'Content-Type': 'text/event-stream',
|
|
896
|
+
'Cache-Control': 'no-cache',
|
|
897
|
+
'Connection': 'keep-alive'
|
|
898
|
+
});
|
|
899
|
+
res.write('\n');
|
|
900
|
+
|
|
901
|
+
let lastSize = existsSync(subagentJsonl) ? statSync(subagentJsonl).size : 0;
|
|
902
|
+
|
|
903
|
+
const watcher = chokidar.watch(subagentJsonl, {
|
|
904
|
+
persistent: true,
|
|
905
|
+
ignoreInitial: true,
|
|
906
|
+
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
let closed = false;
|
|
910
|
+
const cleanup = () => { if (!closed) { closed = true; watcher.close(); } };
|
|
911
|
+
|
|
912
|
+
function emitMessages() {
|
|
913
|
+
const messages = readRecentMessages(subagentJsonl, 50);
|
|
914
|
+
lastSize = statSync(subagentJsonl).size;
|
|
915
|
+
res.write(`event: agent-log-update\ndata: ${JSON.stringify({ messages, agentId })}\n\n`);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
watcher.on('change', () => {
|
|
919
|
+
try {
|
|
920
|
+
if (statSync(subagentJsonl).size <= lastSize) return;
|
|
921
|
+
emitMessages();
|
|
922
|
+
} catch (_) {}
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
watcher.on('add', () => {
|
|
926
|
+
try { emitMessages(); } catch (_) {}
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
req.on('close', cleanup);
|
|
930
|
+
res.on('close', cleanup);
|
|
931
|
+
res.on('error', cleanup);
|
|
932
|
+
});
|
|
933
|
+
|
|
831
934
|
app.get('/api/sessions/:sessionId/messages', (req, res) => {
|
|
832
935
|
const limit = Math.min(parseInt(req.query.limit, 10) || 10, 50);
|
|
833
936
|
const metadata = loadSessionMetadata();
|
|
@@ -1052,6 +1155,10 @@ function broadcast(data) {
|
|
|
1052
1155
|
}
|
|
1053
1156
|
}
|
|
1054
1157
|
|
|
1158
|
+
app.get('/api/context-status', (req, res) => {
|
|
1159
|
+
res.json(Object.fromEntries(contextStatusCache));
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1055
1162
|
app.use('/api', (req, res) => {
|
|
1056
1163
|
res.status(404).json({ error: 'Not found' });
|
|
1057
1164
|
});
|
|
@@ -1181,6 +1288,48 @@ agentActivityWatcher.on('all', (event, filePath) => {
|
|
|
1181
1288
|
}
|
|
1182
1289
|
});
|
|
1183
1290
|
|
|
1291
|
+
// Watch context-status directory for statusline updates
|
|
1292
|
+
const contextStatusWatcher = chokidar.watch(CONTEXT_STATUS_DIR, {
|
|
1293
|
+
persistent: true,
|
|
1294
|
+
ignoreInitial: false,
|
|
1295
|
+
depth: 0
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
contextStatusWatcher.on('all', (event, filePath) => {
|
|
1299
|
+
if (!filePath.endsWith('.json')) return;
|
|
1300
|
+
const sessionId = path.basename(filePath, '.json');
|
|
1301
|
+
if (event === 'add' || event === 'change') {
|
|
1302
|
+
try {
|
|
1303
|
+
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
1304
|
+
contextStatusCache.set(sessionId, data);
|
|
1305
|
+
evictStaleCache(contextStatusCache);
|
|
1306
|
+
} catch (e) { /* ignore malformed */ }
|
|
1307
|
+
broadcast({ type: 'context-update', sessionId });
|
|
1308
|
+
} else if (event === 'unlink') {
|
|
1309
|
+
contextStatusCache.delete(sessionId);
|
|
1310
|
+
broadcast({ type: 'context-update', sessionId });
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
const CTX_CLEANUP_MAX_AGE_MS = 2 * 60 * 60 * 1000;
|
|
1315
|
+
async function cleanupContextStatus() {
|
|
1316
|
+
try {
|
|
1317
|
+
const entries = await fs.readdir(CONTEXT_STATUS_DIR);
|
|
1318
|
+
const now = Date.now();
|
|
1319
|
+
for (const f of entries) {
|
|
1320
|
+
if (!f.endsWith('.json')) continue;
|
|
1321
|
+
try {
|
|
1322
|
+
const fp = path.join(CONTEXT_STATUS_DIR, f);
|
|
1323
|
+
const st = statSync(fp);
|
|
1324
|
+
if (now - st.mtimeMs > CTX_CLEANUP_MAX_AGE_MS) {
|
|
1325
|
+
await fs.unlink(fp);
|
|
1326
|
+
contextStatusCache.delete(path.basename(f, '.json'));
|
|
1327
|
+
}
|
|
1328
|
+
} catch (e) { /* ignore */ }
|
|
1329
|
+
}
|
|
1330
|
+
} catch (e) { /* dir may not exist */ }
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1184
1333
|
// Cleanup agent-activity folders older than 2 days
|
|
1185
1334
|
const CLEANUP_MAX_AGE_MS = 2 * 24 * 60 * 60 * 1000;
|
|
1186
1335
|
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000;
|
|
@@ -1205,7 +1354,9 @@ async function cleanupAgentActivity() {
|
|
|
1205
1354
|
}
|
|
1206
1355
|
|
|
1207
1356
|
cleanupAgentActivity();
|
|
1357
|
+
cleanupContextStatus();
|
|
1208
1358
|
setInterval(cleanupAgentActivity, CLEANUP_INTERVAL_MS);
|
|
1359
|
+
setInterval(cleanupContextStatus, 30 * 60 * 1000);
|
|
1209
1360
|
|
|
1210
1361
|
const server = app.listen(PORT, () => {
|
|
1211
1362
|
const actualPort = server.address().port;
|