@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.
@@ -11,7 +11,7 @@
11
11
  "name": "claude-crew",
12
12
  "source": "./",
13
13
  "description": "오케스트레이터 + PM, 플래너, 개발, QA, 마케팅 에이전트 팀으로 단일 제품의 개발과 마케팅을 통합 관리",
14
- "version": "0.1.14",
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.14"
31
+ "version": "0.1.16"
32
32
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-crew",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "1인 SaaS 개발자를 위한 멀티 에이전트 오케스트레이션 — 개발, 마케팅, 일정을 한 대화에서 통합 관리",
5
5
  "author": {
6
6
  "name": "Jaejin Song",
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
- // Track agents
249
+ // Process tool_use blocks from assistant messages
222
250
  if (entry.type === 'tool_use' || entry.type === 'assistant') {
223
- const content = entry.message?.content;
224
- if (Array.isArray(content)) {
225
- for (const block of content) {
226
- if (block.type === 'tool_use') {
227
- // Agent start
228
- if (block.name === 'Agent' || block.name === 'proxy_Agent') {
229
- const id = block.id;
230
- if (id) {
231
- const input = block.input || {};
232
- const agentType = input.subagent_type || input.type || 'general';
233
- const rawType = agentType.replace(/^claude-crew:/, '');
234
- const model = input.model || agentModels[rawType] || null;
235
- const description = input.description || input.prompt?.slice(0, 50) || '';
236
- const ts = entry.timestamp || lastTimestamp;
237
- agentMap.set(id, {
238
- id,
239
- type: agentType,
240
- model,
241
- description,
242
- startTime: ts ? new Date(ts) : null,
243
- status: 'running',
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
- // Skill invocation
248
- if (block.name === 'Skill' || block.name === 'proxy_Skill') {
249
- const skillName = block.input?.skill || block.input?.name;
250
- if (skillName) {
251
- result.lastSkill = skillName;
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).status = 'completed';
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 content = entry.message?.content;
269
- if (Array.isArray(content)) {
270
- for (const block of content) {
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).status = 'completed';
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.now() - startTime.getTime();
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
- return `${minutes}m`;
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(4);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlabsio/claude-crew",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "1인 SaaS 개발자를 위한 멀티 에이전트 오케스트레이션 — 개발, 마케팅, 일정을 한 대화에서 통합 관리",
5
5
  "author": "Jaejin Song <wowlxx28@gmail.com>",
6
6
  "license": "MIT",
@@ -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({