@jjlabsio/claude-crew 0.1.14 → 0.1.16
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/hud/index.mjs +214 -44
- package/package.json +1 -1
- package/scripts/setup-hud.mjs +0 -12
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"name": "claude-crew",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"description": "오케스트레이터 + PM, 플래너, 개발, QA, 마케팅 에이전트 팀으로 단일 제품의 개발과 마케팅을 통합 관리",
|
|
14
|
-
"version": "0.1.
|
|
14
|
+
"version": "0.1.16",
|
|
15
15
|
"author": {
|
|
16
16
|
"name": "Jaejin Song",
|
|
17
17
|
"email": "wowlxx28@gmail.com"
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"category": "workflow"
|
|
29
29
|
}
|
|
30
30
|
],
|
|
31
|
-
"version": "0.1.
|
|
31
|
+
"version": "0.1.16"
|
|
32
32
|
}
|
package/hud/index.mjs
CHANGED
|
@@ -187,11 +187,40 @@ function colorizeContext(pct) {
|
|
|
187
187
|
return `ctx:${color(`${pct}%`)}`;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Rate limits (5h / weekly)
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
function getRateLimits(stdin) {
|
|
194
|
+
const rl = stdin?.rate_limits;
|
|
195
|
+
if (!rl) return null;
|
|
196
|
+
const parse = (v) => {
|
|
197
|
+
if (v == null) return null;
|
|
198
|
+
const n = typeof v === 'number' ? v : parseFloat(v);
|
|
199
|
+
return isNaN(n) ? null : Math.round(Math.min(Math.max(n, 0), 100));
|
|
200
|
+
};
|
|
201
|
+
const fiveHour = parse(rl.five_hour?.used_percentage);
|
|
202
|
+
const sevenDay = parse(rl.seven_day?.used_percentage);
|
|
203
|
+
if (fiveHour == null && sevenDay == null) return null;
|
|
204
|
+
return { fiveHour, sevenDay };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function colorizeRateLimits(limits) {
|
|
208
|
+
if (!limits) return null;
|
|
209
|
+
const colorize = (pct) => {
|
|
210
|
+
const color = pct >= 85 ? red : pct >= 70 ? yellow : green;
|
|
211
|
+
return color(`${pct}%`);
|
|
212
|
+
};
|
|
213
|
+
const parts = [];
|
|
214
|
+
if (limits.fiveHour != null) parts.push(`5h:${colorize(limits.fiveHour)}`);
|
|
215
|
+
if (limits.sevenDay != null) parts.push(`weekly:${colorize(limits.sevenDay)}`);
|
|
216
|
+
return parts.join(' ');
|
|
217
|
+
}
|
|
218
|
+
|
|
190
219
|
// ---------------------------------------------------------------------------
|
|
191
220
|
// Transcript parsing (agents + skills)
|
|
192
221
|
// ---------------------------------------------------------------------------
|
|
193
222
|
function parseTranscript(transcriptPath) {
|
|
194
|
-
const result = { agents: [], lastSkill: null, sessionStart: null };
|
|
223
|
+
const result = { agents: [], todos: [], lastSkill: null, sessionStart: null };
|
|
195
224
|
if (!transcriptPath || !existsSync(transcriptPath)) return result;
|
|
196
225
|
|
|
197
226
|
const agentModels = loadAgentModels();
|
|
@@ -200,55 +229,120 @@ function parseTranscript(transcriptPath) {
|
|
|
200
229
|
const content = readFileSync(transcriptPath, 'utf-8');
|
|
201
230
|
const lines = content.split('\n').filter(Boolean);
|
|
202
231
|
|
|
203
|
-
// Map of tool_use_id -> agent info
|
|
204
232
|
const agentMap = new Map();
|
|
233
|
+
const latestTodos = [];
|
|
234
|
+
const taskIdToIndex = new Map();
|
|
205
235
|
let lastTimestamp = null;
|
|
206
236
|
|
|
207
237
|
for (const line of lines) {
|
|
208
238
|
let entry;
|
|
209
239
|
try { entry = JSON.parse(line); } catch { continue; }
|
|
210
240
|
|
|
211
|
-
// Track last known timestamp
|
|
212
241
|
if (entry.timestamp) {
|
|
213
242
|
lastTimestamp = entry.timestamp;
|
|
214
243
|
}
|
|
215
244
|
|
|
216
|
-
// Session start time
|
|
217
245
|
if (!result.sessionStart && entry.timestamp) {
|
|
218
246
|
result.sessionStart = new Date(entry.timestamp);
|
|
219
247
|
}
|
|
220
248
|
|
|
221
|
-
//
|
|
249
|
+
// Process tool_use blocks from assistant messages
|
|
222
250
|
if (entry.type === 'tool_use' || entry.type === 'assistant') {
|
|
223
|
-
const
|
|
224
|
-
if (Array.isArray(
|
|
225
|
-
for (const block of
|
|
226
|
-
if (block.type
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
251
|
+
const blocks = entry.message?.content;
|
|
252
|
+
if (Array.isArray(blocks)) {
|
|
253
|
+
for (const block of blocks) {
|
|
254
|
+
if (block.type !== 'tool_use') continue;
|
|
255
|
+
|
|
256
|
+
// Agent start
|
|
257
|
+
if (block.name === 'Agent' || block.name === 'proxy_Agent') {
|
|
258
|
+
const id = block.id;
|
|
259
|
+
if (id) {
|
|
260
|
+
const input = block.input || {};
|
|
261
|
+
const agentType = input.subagent_type || input.type || 'general';
|
|
262
|
+
const rawType = agentType.replace(/^claude-crew:/, '');
|
|
263
|
+
const model = input.model || agentModels[rawType] || null;
|
|
264
|
+
const description = input.description || input.prompt?.slice(0, 50) || '';
|
|
265
|
+
const ts = entry.timestamp || lastTimestamp;
|
|
266
|
+
agentMap.set(id, {
|
|
267
|
+
id,
|
|
268
|
+
type: agentType,
|
|
269
|
+
model,
|
|
270
|
+
description,
|
|
271
|
+
startTime: ts ? new Date(ts) : null,
|
|
272
|
+
status: 'running',
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Skill invocation
|
|
278
|
+
if (block.name === 'Skill' || block.name === 'proxy_Skill') {
|
|
279
|
+
const skillName = block.input?.skill || block.input?.name;
|
|
280
|
+
if (skillName) {
|
|
281
|
+
result.lastSkill = skillName;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// TodoWrite — full replacement of todo list
|
|
286
|
+
if (block.name === 'TodoWrite') {
|
|
287
|
+
const input = block.input || {};
|
|
288
|
+
if (input.todos && Array.isArray(input.todos)) {
|
|
289
|
+
const contentToTaskIds = new Map();
|
|
290
|
+
for (const [taskId, idx] of taskIdToIndex) {
|
|
291
|
+
if (idx < latestTodos.length) {
|
|
292
|
+
const c = latestTodos[idx].content;
|
|
293
|
+
const ids = contentToTaskIds.get(c) ?? [];
|
|
294
|
+
ids.push(taskId);
|
|
295
|
+
contentToTaskIds.set(c, ids);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
latestTodos.length = 0;
|
|
300
|
+
taskIdToIndex.clear();
|
|
301
|
+
latestTodos.push(...input.todos);
|
|
302
|
+
|
|
303
|
+
for (let i = 0; i < latestTodos.length; i++) {
|
|
304
|
+
const ids = contentToTaskIds.get(latestTodos[i].content);
|
|
305
|
+
if (ids) {
|
|
306
|
+
for (const taskId of ids) {
|
|
307
|
+
taskIdToIndex.set(taskId, i);
|
|
308
|
+
}
|
|
309
|
+
contentToTaskIds.delete(latestTodos[i].content);
|
|
310
|
+
}
|
|
245
311
|
}
|
|
246
312
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// TaskCreate — append a single task
|
|
316
|
+
if (block.name === 'TaskCreate') {
|
|
317
|
+
const input = block.input || {};
|
|
318
|
+
const subject = typeof input.subject === 'string' ? input.subject : '';
|
|
319
|
+
const description = typeof input.description === 'string' ? input.description : '';
|
|
320
|
+
const todoContent = subject || description || 'Untitled task';
|
|
321
|
+
const status = normalizeTaskStatus(input.status) ?? 'pending';
|
|
322
|
+
latestTodos.push({ content: todoContent, status });
|
|
323
|
+
|
|
324
|
+
const taskId = typeof input.taskId === 'string' || typeof input.taskId === 'number'
|
|
325
|
+
? String(input.taskId)
|
|
326
|
+
: block.id;
|
|
327
|
+
if (taskId) {
|
|
328
|
+
taskIdToIndex.set(taskId, latestTodos.length - 1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// TaskUpdate — update status/content of existing task
|
|
333
|
+
if (block.name === 'TaskUpdate') {
|
|
334
|
+
const input = block.input || {};
|
|
335
|
+
const index = resolveTaskIndex(input.taskId, taskIdToIndex, latestTodos);
|
|
336
|
+
if (index !== null) {
|
|
337
|
+
const status = normalizeTaskStatus(input.status);
|
|
338
|
+
if (status) {
|
|
339
|
+
latestTodos[index] = { ...latestTodos[index], status };
|
|
340
|
+
}
|
|
341
|
+
const subject = typeof input.subject === 'string' ? input.subject : '';
|
|
342
|
+
const description = typeof input.description === 'string' ? input.description : '';
|
|
343
|
+
const newContent = subject || description;
|
|
344
|
+
if (newContent) {
|
|
345
|
+
latestTodos[index] = { ...latestTodos[index], content: newContent };
|
|
252
346
|
}
|
|
253
347
|
}
|
|
254
348
|
}
|
|
@@ -261,17 +355,23 @@ function parseTranscript(transcriptPath) {
|
|
|
261
355
|
if (entry.type === 'tool_result') {
|
|
262
356
|
const toolUseId = entry.tool_use_id;
|
|
263
357
|
if (toolUseId && agentMap.has(toolUseId)) {
|
|
264
|
-
agentMap.get(toolUseId)
|
|
358
|
+
const agent = agentMap.get(toolUseId);
|
|
359
|
+
agent.status = 'completed';
|
|
360
|
+
const ts = entry.timestamp || lastTimestamp;
|
|
361
|
+
if (ts) agent.endTime = new Date(ts);
|
|
265
362
|
}
|
|
266
363
|
}
|
|
267
364
|
if (entry.type === 'user') {
|
|
268
|
-
const
|
|
269
|
-
if (Array.isArray(
|
|
270
|
-
for (const block of
|
|
365
|
+
const blocks = entry.message?.content;
|
|
366
|
+
if (Array.isArray(blocks)) {
|
|
367
|
+
for (const block of blocks) {
|
|
271
368
|
if (block.type === 'tool_result') {
|
|
272
369
|
const toolUseId = block.tool_use_id;
|
|
273
370
|
if (toolUseId && agentMap.has(toolUseId)) {
|
|
274
|
-
agentMap.get(toolUseId)
|
|
371
|
+
const agent = agentMap.get(toolUseId);
|
|
372
|
+
agent.status = 'completed';
|
|
373
|
+
const ts = entry.timestamp || lastTimestamp;
|
|
374
|
+
if (ts) agent.endTime = new Date(ts);
|
|
275
375
|
}
|
|
276
376
|
}
|
|
277
377
|
}
|
|
@@ -287,11 +387,46 @@ function parseTranscript(transcriptPath) {
|
|
|
287
387
|
if (a.startTime && (now - a.startTime.getTime()) > STALE_THRESHOLD_MS) return false;
|
|
288
388
|
return true;
|
|
289
389
|
});
|
|
390
|
+
result.todos = [...latestTodos];
|
|
290
391
|
} catch { /* ignore parse errors */ }
|
|
291
392
|
|
|
292
393
|
return result;
|
|
293
394
|
}
|
|
294
395
|
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// Todo helpers
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
function normalizeTaskStatus(status) {
|
|
400
|
+
if (typeof status !== 'string') return null;
|
|
401
|
+
switch (status) {
|
|
402
|
+
case 'pending':
|
|
403
|
+
case 'not_started':
|
|
404
|
+
return 'pending';
|
|
405
|
+
case 'in_progress':
|
|
406
|
+
case 'running':
|
|
407
|
+
return 'in_progress';
|
|
408
|
+
case 'completed':
|
|
409
|
+
case 'complete':
|
|
410
|
+
case 'done':
|
|
411
|
+
return 'completed';
|
|
412
|
+
default:
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function resolveTaskIndex(taskId, taskIdToIndex, latestTodos) {
|
|
418
|
+
if (typeof taskId === 'string' || typeof taskId === 'number') {
|
|
419
|
+
const key = String(taskId);
|
|
420
|
+
const mapped = taskIdToIndex.get(key);
|
|
421
|
+
if (typeof mapped === 'number') return mapped;
|
|
422
|
+
if (/^\d+$/.test(key)) {
|
|
423
|
+
const numericIndex = parseInt(key, 10) - 1;
|
|
424
|
+
if (numericIndex >= 0 && numericIndex < latestTodos.length) return numericIndex;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
295
430
|
// ---------------------------------------------------------------------------
|
|
296
431
|
// Agent model name (short)
|
|
297
432
|
// ---------------------------------------------------------------------------
|
|
@@ -307,14 +442,18 @@ function shortModelName(model) {
|
|
|
307
442
|
// ---------------------------------------------------------------------------
|
|
308
443
|
// Agent duration formatting
|
|
309
444
|
// ---------------------------------------------------------------------------
|
|
310
|
-
function formatAgentDuration(startTime) {
|
|
311
|
-
if (!startTime) return '
|
|
312
|
-
const ms = Date.
|
|
445
|
+
function formatAgentDuration(startTime, endTime) {
|
|
446
|
+
if (!startTime) return '';
|
|
447
|
+
const ms = (endTime ?? new Date()).getTime() - startTime.getTime();
|
|
448
|
+
if (ms < 1000) return '<1s';
|
|
313
449
|
const seconds = Math.floor(ms / 1000);
|
|
314
|
-
const minutes = Math.floor(seconds / 60);
|
|
315
|
-
if (seconds < 10) return '';
|
|
316
450
|
if (seconds < 60) return `${seconds}s`;
|
|
317
|
-
|
|
451
|
+
const minutes = Math.floor(seconds / 60);
|
|
452
|
+
const secs = seconds % 60;
|
|
453
|
+
if (minutes < 60) return `${minutes}m${secs}s`;
|
|
454
|
+
const hours = Math.floor(minutes / 60);
|
|
455
|
+
const mins = minutes % 60;
|
|
456
|
+
return `${hours}h${mins}m`;
|
|
318
457
|
}
|
|
319
458
|
|
|
320
459
|
// ---------------------------------------------------------------------------
|
|
@@ -337,7 +476,7 @@ function renderAgentsMultiLine(agents, maxLines = 5) {
|
|
|
337
476
|
const rawType = a.type.includes(':') ? a.type.split(':').pop() : a.type;
|
|
338
477
|
const name = rawType.padEnd(7);
|
|
339
478
|
const model = shortModelName(a.model).padEnd(8);
|
|
340
|
-
const duration = formatAgentDuration(a.startTime).padStart(
|
|
479
|
+
const duration = formatAgentDuration(a.startTime, a.endTime).padStart(6);
|
|
341
480
|
const desc = a.description.length > 40 ? a.description.slice(0, 37) + '...' : a.description;
|
|
342
481
|
|
|
343
482
|
detailLines.push(
|
|
@@ -353,6 +492,29 @@ function renderAgentsMultiLine(agents, maxLines = 5) {
|
|
|
353
492
|
return { headerPart, detailLines };
|
|
354
493
|
}
|
|
355
494
|
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
// Todo progress rendering
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
function renderTodosLine(todos) {
|
|
499
|
+
if (!todos || todos.length === 0) return null;
|
|
500
|
+
|
|
501
|
+
const inProgress = todos.find(t => t.status === 'in_progress');
|
|
502
|
+
const completed = todos.filter(t => t.status === 'completed').length;
|
|
503
|
+
const total = todos.length;
|
|
504
|
+
|
|
505
|
+
if (!inProgress) {
|
|
506
|
+
if (completed === total && total > 0) {
|
|
507
|
+
return `${green('\u2713')} all done ${dim(`(${completed}/${total})`)}`;
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const content = inProgress.content.length > 50
|
|
513
|
+
? inProgress.content.slice(0, 47) + '...'
|
|
514
|
+
: inProgress.content;
|
|
515
|
+
return `${yellow('\u25b8')} ${content} ${dim(`(${completed}/${total})`)}`;
|
|
516
|
+
}
|
|
517
|
+
|
|
356
518
|
// ---------------------------------------------------------------------------
|
|
357
519
|
// Session duration
|
|
358
520
|
// ---------------------------------------------------------------------------
|
|
@@ -441,11 +603,19 @@ async function main() {
|
|
|
441
603
|
|
|
442
604
|
midElements.push(colorizeSession(transcript.sessionStart));
|
|
443
605
|
|
|
606
|
+
const rateLimits = getRateLimits(stdin);
|
|
607
|
+
const rateLimitsStr = colorizeRateLimits(rateLimits);
|
|
608
|
+
if (rateLimitsStr) midElements.push(rateLimitsStr);
|
|
609
|
+
|
|
610
|
+
// --- Todos line ---
|
|
611
|
+
const todosLine = renderTodosLine(transcript.todos);
|
|
612
|
+
|
|
444
613
|
// --- Output ---
|
|
445
614
|
const outputLines = [];
|
|
446
615
|
outputLines.push(topElements.join(SEPARATOR));
|
|
447
616
|
outputLines.push(midElements.join(SEPARATOR));
|
|
448
617
|
outputLines.push(...detailLines);
|
|
618
|
+
if (todosLine) outputLines.push(todosLine);
|
|
449
619
|
|
|
450
620
|
console.log(outputLines.filter(Boolean).join('\n'));
|
|
451
621
|
}
|
package/package.json
CHANGED
package/scripts/setup-hud.mjs
CHANGED
|
@@ -69,18 +69,6 @@ async function main() {
|
|
|
69
69
|
writeFileSync(localSettingsPath, JSON.stringify(localSettings, null, 2));
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// --- Remove legacy global statusLine from ~/.claude/settings.json ---
|
|
73
|
-
const globalSettingsPath = join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'), 'settings.json');
|
|
74
|
-
if (existsSync(globalSettingsPath)) {
|
|
75
|
-
try {
|
|
76
|
-
const globalSettings = JSON.parse(readFileSync(globalSettingsPath, 'utf-8'));
|
|
77
|
-
if (globalSettings.statusLine) {
|
|
78
|
-
delete globalSettings.statusLine;
|
|
79
|
-
writeFileSync(globalSettingsPath, JSON.stringify(globalSettings, null, 2));
|
|
80
|
-
}
|
|
81
|
-
} catch { /* ignore */ }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
72
|
console.log(JSON.stringify({ continue: true }));
|
|
85
73
|
} catch (e) {
|
|
86
74
|
console.log(JSON.stringify({
|