@jjlabsio/claude-crew 0.1.13 → 0.1.15

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.13",
14
+ "version": "0.1.15",
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.13"
31
+ "version": "0.1.15"
32
32
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-crew",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "1인 SaaS 개발자를 위한 멀티 에이전트 오케스트레이션 — 개발, 마케팅, 일정을 한 대화에서 통합 관리",
5
5
  "author": {
6
6
  "name": "Jaejin Song",
@@ -21,7 +21,11 @@
21
21
  "./agents/planner.md",
22
22
  "./agents/dev.md",
23
23
  "./agents/qa.md",
24
- "./agents/marketing.md"
24
+ "./agents/code-reviewer.md",
25
+ "./agents/techlead.md",
26
+ "./agents/researcher.md",
27
+ "./agents/explorer.md",
28
+ "./agents/plan-evaluator.md"
25
29
  ],
26
30
  "skills": [
27
31
  "./skills/"
package/hud/index.mjs CHANGED
@@ -13,6 +13,7 @@ import { execSync } from 'node:child_process';
13
13
  import { readFileSync, existsSync, readdirSync } from 'node:fs';
14
14
  import { join, dirname, basename } from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
+ import { homedir } from 'node:os';
16
17
 
17
18
  // ---------------------------------------------------------------------------
18
19
  // ANSI helpers
@@ -43,10 +44,37 @@ async function readStdin(timeoutMs = 1000) {
43
44
  });
44
45
  }
45
46
 
47
+ // ---------------------------------------------------------------------------
48
+ // Project installation info from installed_plugins.json
49
+ // ---------------------------------------------------------------------------
50
+ function getProjectInstallInfo(projectRoot) {
51
+ try {
52
+ const pluginsJsonPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
53
+ if (!existsSync(pluginsJsonPath)) return null;
54
+ const data = JSON.parse(readFileSync(pluginsJsonPath, 'utf-8'));
55
+ const crewEntries = data.plugins?.['claude-crew@claude-crew'] || [];
56
+ return crewEntries.find(e => e.projectPath === projectRoot) || null;
57
+ } catch { return null; }
58
+ }
59
+
46
60
  // ---------------------------------------------------------------------------
47
61
  // Version
48
62
  // ---------------------------------------------------------------------------
49
- function getVersion() {
63
+ function getVersion(installInfo) {
64
+ // Read from the project-specific install path
65
+ if (installInfo?.installPath) {
66
+ try {
67
+ const pkgPath = join(installInfo.installPath, 'package.json');
68
+ if (existsSync(pkgPath)) {
69
+ return JSON.parse(readFileSync(pkgPath, 'utf-8')).version || '0.0.0';
70
+ }
71
+ } catch { /* ignore */ }
72
+ }
73
+ // Fallback to version field from install record
74
+ if (installInfo?.version && installInfo.version !== 'unknown') {
75
+ return installInfo.version;
76
+ }
77
+ // Final fallback: own package.json (dev/local run)
50
78
  try {
51
79
  const __dirname = dirname(fileURLToPath(import.meta.url));
52
80
  const pkgPath = join(__dirname, '..', 'package.json');
@@ -159,6 +187,35 @@ function colorizeContext(pct) {
159
187
  return `ctx:${color(`${pct}%`)}`;
160
188
  }
161
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
+
162
219
  // ---------------------------------------------------------------------------
163
220
  // Transcript parsing (agents + skills)
164
221
  // ---------------------------------------------------------------------------
@@ -233,7 +290,10 @@ function parseTranscript(transcriptPath) {
233
290
  if (entry.type === 'tool_result') {
234
291
  const toolUseId = entry.tool_use_id;
235
292
  if (toolUseId && agentMap.has(toolUseId)) {
236
- agentMap.get(toolUseId).status = 'completed';
293
+ const agent = agentMap.get(toolUseId);
294
+ agent.status = 'completed';
295
+ const ts = entry.timestamp || lastTimestamp;
296
+ if (ts) agent.endTime = new Date(ts);
237
297
  }
238
298
  }
239
299
  if (entry.type === 'user') {
@@ -243,8 +303,10 @@ function parseTranscript(transcriptPath) {
243
303
  if (block.type === 'tool_result') {
244
304
  const toolUseId = block.tool_use_id;
245
305
  if (toolUseId && agentMap.has(toolUseId)) {
246
- agentMap.get(toolUseId).status = 'completed';
247
- }
306
+ const agent = agentMap.get(toolUseId);
307
+ agent.status = 'completed';
308
+ const ts = entry.timestamp || lastTimestamp;
309
+ if (ts) agent.endTime = new Date(ts);
248
310
  }
249
311
  }
250
312
  }
@@ -279,14 +341,18 @@ function shortModelName(model) {
279
341
  // ---------------------------------------------------------------------------
280
342
  // Agent duration formatting
281
343
  // ---------------------------------------------------------------------------
282
- function formatAgentDuration(startTime) {
283
- if (!startTime) return '?';
284
- const ms = Date.now() - startTime.getTime();
344
+ function formatAgentDuration(startTime, endTime) {
345
+ if (!startTime) return '';
346
+ const ms = (endTime ?? new Date()).getTime() - startTime.getTime();
347
+ if (ms < 1000) return '<1s';
285
348
  const seconds = Math.floor(ms / 1000);
286
- const minutes = Math.floor(seconds / 60);
287
- if (seconds < 10) return '';
288
349
  if (seconds < 60) return `${seconds}s`;
289
- return `${minutes}m`;
350
+ const minutes = Math.floor(seconds / 60);
351
+ const secs = seconds % 60;
352
+ if (minutes < 60) return `${minutes}m${secs}s`;
353
+ const hours = Math.floor(minutes / 60);
354
+ const mins = minutes % 60;
355
+ return `${hours}h${mins}m`;
290
356
  }
291
357
 
292
358
  // ---------------------------------------------------------------------------
@@ -309,7 +375,7 @@ function renderAgentsMultiLine(agents, maxLines = 5) {
309
375
  const rawType = a.type.includes(':') ? a.type.split(':').pop() : a.type;
310
376
  const name = rawType.padEnd(7);
311
377
  const model = shortModelName(a.model).padEnd(8);
312
- const duration = formatAgentDuration(a.startTime).padStart(4);
378
+ const duration = formatAgentDuration(a.startTime, a.endTime).padStart(6);
313
379
  const desc = a.description.length > 40 ? a.description.slice(0, 37) + '...' : a.description;
314
380
 
315
381
  detailLines.push(
@@ -364,7 +430,15 @@ async function main() {
364
430
  }
365
431
 
366
432
  const cwd = stdin.cwd || process.cwd();
367
- const version = getVersion();
433
+
434
+ // Find git project root for reliable matching against installed_plugins.json
435
+ const projectRoot = gitExec('git rev-parse --show-toplevel', cwd) || cwd;
436
+
437
+ // Only show HUD if claude-crew is installed in this project
438
+ const installInfo = getProjectInstallInfo(projectRoot);
439
+ if (!installInfo) return;
440
+
441
+ const version = getVersion(installInfo);
368
442
 
369
443
  // --- Top line ---
370
444
  const topElements = [];
@@ -405,6 +479,10 @@ async function main() {
405
479
 
406
480
  midElements.push(colorizeSession(transcript.sessionStart));
407
481
 
482
+ const rateLimits = getRateLimits(stdin);
483
+ const rateLimitsStr = colorizeRateLimits(rateLimits);
484
+ if (rateLimitsStr) midElements.push(rateLimitsStr);
485
+
408
486
  // --- Output ---
409
487
  const outputLines = [];
410
488
  outputLines.push(topElements.join(SEPARATOR));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlabsio/claude-crew",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "1인 SaaS 개발자를 위한 멀티 에이전트 오케스트레이션 — 개발, 마케팅, 일정을 한 대화에서 통합 관리",
5
5
  "author": "Jaejin Song <wowlxx28@gmail.com>",
6
6
  "license": "MIT",
@@ -2,12 +2,13 @@
2
2
  /**
3
3
  * CREW Session Start Hook
4
4
  *
5
- * Checks if statusLine is configured for CREW HUD.
6
- * If not, automatically sets it up.
7
- * Reads stdin JSON from Claude Code (SessionStart hook input).
5
+ * Writes statusLine to the project's .claude/settings.local.json so the HUD
6
+ * only appears in projects where claude-crew is installed.
7
+ * Also removes the legacy global statusLine from ~/.claude/settings.json.
8
8
  */
9
9
 
10
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { execSync } from 'node:child_process';
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
11
12
  import { join } from 'node:path';
12
13
  import { homedir } from 'node:os';
13
14
 
@@ -26,61 +27,67 @@ async function readStdin(timeoutMs = 3000) {
26
27
  });
27
28
  }
28
29
 
30
+ function gitExec(cmd, cwd) {
31
+ try {
32
+ return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
33
+ } catch { return null; }
34
+ }
35
+
29
36
  // ---------------------------------------------------------------------------
30
37
  // Main
31
38
  // ---------------------------------------------------------------------------
32
39
  async function main() {
33
- // Consume stdin (required by hook protocol)
34
- await readStdin();
40
+ const raw = await readStdin();
35
41
 
36
- const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
37
- const settingsPath = join(configDir, 'settings.json');
38
42
  const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
39
-
40
43
  if (!pluginRoot) {
41
- // Not running as a plugin — skip
42
44
  console.log(JSON.stringify({ continue: true }));
43
45
  return;
44
46
  }
45
47
 
48
+ let cwd = process.cwd();
49
+ if (raw) {
50
+ try { cwd = JSON.parse(raw).cwd || cwd; } catch { /* ignore */ }
51
+ }
52
+
53
+ // Use git toplevel as the reliable project root
54
+ const projectRoot = gitExec('git rev-parse --show-toplevel', cwd) || cwd;
55
+
46
56
  const hudCommand = `node "${pluginRoot}/hud/index.mjs"`;
57
+ const localSettingsPath = join(projectRoot, '.claude', 'settings.local.json');
47
58
 
48
59
  try {
49
- let settings = {};
50
- if (existsSync(settingsPath)) {
51
- settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
60
+ // --- Write statusLine to project-level settings.local.json ---
61
+ let localSettings = {};
62
+ if (existsSync(localSettingsPath)) {
63
+ try { localSettings = JSON.parse(readFileSync(localSettingsPath, 'utf-8')); } catch { /* ignore */ }
52
64
  }
53
65
 
54
- // Check if statusLine is already set to the *current* plugin path
55
- const currentCommand = settings.statusLine?.command || '';
56
- if (currentCommand === hudCommand) {
57
- // Already configured with this exact version
58
- console.log(JSON.stringify({ continue: true }));
59
- return;
66
+ if (localSettings.statusLine?.command !== hudCommand) {
67
+ localSettings.statusLine = { type: 'command', command: hudCommand };
68
+ mkdirSync(join(projectRoot, '.claude'), { recursive: true });
69
+ writeFileSync(localSettingsPath, JSON.stringify(localSettings, null, 2));
60
70
  }
61
71
 
62
- // Set statusLine to crew HUD
63
- settings.statusLine = {
64
- type: 'command',
65
- command: hudCommand,
66
- };
67
-
68
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
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
+ }
69
83
 
70
- console.log(JSON.stringify({
71
- continue: true,
72
- hookSpecificOutput: {
73
- hookEventName: 'SessionStart',
74
- additionalContext: 'CREW HUD가 자동 설정되었습니다. 다음 세션부터 statusline에 표시됩니다.',
75
- },
76
- }));
84
+ console.log(JSON.stringify({ continue: true }));
77
85
  } catch (e) {
78
- // Non-fatal — don't block session start
79
86
  console.log(JSON.stringify({
80
87
  continue: true,
81
88
  hookSpecificOutput: {
82
89
  hookEventName: 'SessionStart',
83
- additionalContext: `CREW HUD 자동 설정 실패: ${e.message}. /crew-setup을 수동 실행해주세요.`,
90
+ additionalContext: `CREW HUD 자동 설정 실패: ${e.message}`,
84
91
  },
85
92
  }));
86
93
  }