claude-code-watcher 1.0.1 → 1.0.3
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 +25 -31
- package/hooks/session-tracker.mjs +11 -3
- package/package.json +3 -3
- package/src/commands/help.mjs +3 -5
- package/src/core/session.mjs +3 -12
- package/src/ui/format.mjs +15 -9
- package/src/ui/renderer.mjs +50 -22
package/README.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# claude-code-watcher (ccw)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<table>
|
|
4
|
+
<tr>
|
|
5
|
+
<td align="center">
|
|
6
|
+
<img src="./screenshot1.png" width="560"/><br/>
|
|
7
|
+
<sub>전체 뷰</sub>
|
|
8
|
+
</td>
|
|
9
|
+
<td align="center">
|
|
10
|
+
<img src="./screenshot2.png" width="220"/><br/>
|
|
11
|
+
<sub>컴팩트 뷰</sub>
|
|
12
|
+
</td>
|
|
13
|
+
</tr>
|
|
14
|
+
</table>
|
|
4
15
|
|
|
5
16
|
VS Code 터미널에서 실행 중인 Claude Code 세션을 실시간으로 모니터링하는 CLI 대시보드입니다.
|
|
6
17
|
|
|
@@ -9,10 +20,11 @@ VS Code 터미널에서 실행 중인 Claude Code 세션을 실시간으로 모
|
|
|
9
20
|
## 기능
|
|
10
21
|
|
|
11
22
|
- **실시간 세션 목록** — 열려 있는 모든 Claude Code 세션의 상태를 TTY 오름차순으로 표시
|
|
12
|
-
- **상태 추적** —
|
|
23
|
+
- **상태 추적** — `대기중` ⚪ / `작업중` 🔵 / `응답 요청` 🔴 상태 자동 전환
|
|
13
24
|
- **서브에이전트 추적** — Agent 툴 실행 중인 서브에이전트 수 표시
|
|
14
|
-
- **알림** — `Stop`(응답 완료) 또는 `Notification`(
|
|
25
|
+
- **알림** — `Stop`(응답 완료) 또는 `Notification`(응답 요청) 이벤트 발생 시 소리 알림 (사운드 커스터마이징 가능)
|
|
15
26
|
- **세션 상세보기** — 선택한 세션의 마지막 메시지, 경로, 타임스탬프 등 상세 정보
|
|
27
|
+
- **반응형 레이아웃** — 터미널 너비에 따라 자동으로 뷰 전환 (전체 → 컴팩트 → 상태 색상만)
|
|
16
28
|
- **프로세스 재시작** — `R` 키로 대시보드 자체를 재실행
|
|
17
29
|
|
|
18
30
|
## 요구사항
|
|
@@ -87,39 +99,21 @@ ccw /sound stop Blow
|
|
|
87
99
|
|
|
88
100
|
## 세션 상태
|
|
89
101
|
|
|
90
|
-
| 상태 |
|
|
102
|
+
| 상태 | 표시 | 설명 |
|
|
91
103
|
|------|------|------|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
| `stale` | 회색 | 10분 이상 업데이트 없음 |
|
|
96
|
-
| `error` | 빨강 | 오류 발생 |
|
|
104
|
+
| 대기중 | ⚪ 흰색 | 사용자 입력 대기 중 |
|
|
105
|
+
| 작업중 | 🔵 파랑 | Claude가 응답을 처리 중 |
|
|
106
|
+
| 응답 요청 | 🔴 빨강 | 사용자 권한 요청 등 알림 발생 |
|
|
97
107
|
|
|
98
|
-
##
|
|
108
|
+
## 반응형 레이아웃
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
터미널 너비에 따라 표시 방식이 자동으로 전환됩니다.
|
|
101
111
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
↓
|
|
107
|
-
~/.claude/dashboard/active/<sessionId>.json 업데이트
|
|
108
|
-
↓
|
|
109
|
-
ccw 대시보드가 파일 변경 감지 → 화면 갱신
|
|
110
|
-
```
|
|
112
|
+
| 너비 | 뷰 | 표시 내용 |
|
|
113
|
+
|------|----|-----------|
|
|
114
|
+
| 60 cols 이상 | 전체 대시보드 | 프로젝트명, 상태, 메시지, 시간 등 전체 정보 |
|
|
115
|
+
| 60 cols 미만 | 컴팩트 뷰 | `● 프로젝트명` (상태 색상 + 이름) |
|
|
111
116
|
|
|
112
|
-
등록되는 훅 이벤트:
|
|
113
|
-
|
|
114
|
-
| 이벤트 | 전환 상태 |
|
|
115
|
-
|--------|-----------|
|
|
116
|
-
| `SessionStart` | `waiting` |
|
|
117
|
-
| `UserPromptSubmit` | `working` |
|
|
118
|
-
| `PreToolUse` / `PostToolUse` | `notification` → `working` 복원 |
|
|
119
|
-
| `Stop` | `waiting` + 알림 |
|
|
120
|
-
| `Notification` | `notification` (working 중일 때만) |
|
|
121
|
-
| `SubagentStart` / `SubagentStop` | 서브에이전트 목록 관리 |
|
|
122
|
-
| `SessionEnd` | 세션 파일 삭제 |
|
|
123
117
|
|
|
124
118
|
## 알려진 제한사항
|
|
125
119
|
|
|
@@ -73,10 +73,18 @@ switch (hookEvent) {
|
|
|
73
73
|
break;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
case 'PreToolUse':
|
|
76
|
+
case 'PreToolUse': {
|
|
77
|
+
// PreToolUse means Claude is actively about to use a tool — always 'working'.
|
|
78
|
+
// This also covers the case where UserPromptSubmit didn't fire (hook failure, etc.).
|
|
79
|
+
const cur = existing.status ?? 'working';
|
|
80
|
+
status = 'working';
|
|
81
|
+
message = cur === 'waiting' ? 'Processing...' : (existing.message ?? 'Processing...');
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
case 'PostToolUse': {
|
|
78
|
-
//
|
|
79
|
-
//
|
|
86
|
+
// Only restore to 'working' from active states; preserve 'waiting' to avoid
|
|
87
|
+
// spurious transitions when a delayed PostToolUse arrives after Stop.
|
|
80
88
|
const cur = existing.status ?? 'working';
|
|
81
89
|
status = (cur === 'notification' || cur === 'working') ? 'working' : cur;
|
|
82
90
|
message = existing.message ?? 'Processing...';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-watcher",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Claude Code 세션 CLI 대시보드",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"author": "roy-jung",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "https://
|
|
28
|
+
"url": "https://github.com/roy-jung/claude-code-watcher"
|
|
29
29
|
},
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"scripts": {
|
|
32
32
|
"publish:internal": "npm publish --registry https://artifactory.navercorp.com/artifactory/api/npm/npm-naver/",
|
|
33
33
|
"publish:public": "npm publish --registry https://registry.npmjs.org"
|
|
34
34
|
}
|
|
35
|
-
}
|
|
35
|
+
}
|
package/src/commands/help.mjs
CHANGED
|
@@ -23,11 +23,9 @@ ${BOLD}Dashboard Keys:${RESET}
|
|
|
23
23
|
${color(FG.CYAN, 'q')} / ${color(FG.CYAN, 'Ctrl+C')} 종료
|
|
24
24
|
|
|
25
25
|
${BOLD}Status Colors:${RESET}
|
|
26
|
-
${color(FG.
|
|
27
|
-
${color(FG.
|
|
28
|
-
${color(FG.
|
|
29
|
-
${color(FG.BRIGHT_BLACK, 'stale')} 10분 이상 비활성
|
|
30
|
-
${color(FG.RED, 'error')} 오류 발생
|
|
26
|
+
${color(FG.BRIGHT_BLUE, 'working')} Claude가 응답을 처리 중
|
|
27
|
+
${color(FG.BRIGHT_WHITE, 'waiting')} 사용자 입력 대기 중
|
|
28
|
+
${color(FG.BRIGHT_RED, 'notification')} 알림 수신됨
|
|
31
29
|
|
|
32
30
|
${BOLD}Session Data:${RESET}
|
|
33
31
|
~/.claude/dashboard/active/<sessionId>.json
|
package/src/core/session.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export const STALE_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Parse raw JSON data into a SessionRecord.
|
|
5
3
|
* Returns null if the data is invalid.
|
|
@@ -40,7 +38,7 @@ export function parseSession(raw, filename) {
|
|
|
40
38
|
export function deriveSession(record, now = new Date()) {
|
|
41
39
|
const updatedAt = parseDate(record.updated) || now;
|
|
42
40
|
const startedAt = parseDate(record.startedAt) || now;
|
|
43
|
-
const status = classifyStatus(record.status
|
|
41
|
+
const status = classifyStatus(record.status);
|
|
44
42
|
|
|
45
43
|
return {
|
|
46
44
|
...record,
|
|
@@ -53,18 +51,11 @@ export function deriveSession(record, now = new Date()) {
|
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
/**
|
|
56
|
-
* Classify session status
|
|
54
|
+
* Classify session status.
|
|
57
55
|
* @param {string} status
|
|
58
|
-
* @param {Date} updatedAt
|
|
59
|
-
* @param {Date} now
|
|
60
56
|
* @returns {string}
|
|
61
57
|
*/
|
|
62
|
-
export function classifyStatus(status
|
|
63
|
-
if (status === 'error') return 'error';
|
|
64
|
-
|
|
65
|
-
const sinceMs = now - updatedAt;
|
|
66
|
-
if (sinceMs > STALE_THRESHOLD_MS && status !== 'working' && status !== 'notification') return 'stale';
|
|
67
|
-
|
|
58
|
+
export function classifyStatus(status) {
|
|
68
59
|
return status || 'waiting';
|
|
69
60
|
}
|
|
70
61
|
|
package/src/ui/format.mjs
CHANGED
|
@@ -61,24 +61,30 @@ export function formatTime(date) {
|
|
|
61
61
|
export function statusColor(status) {
|
|
62
62
|
switch (status) {
|
|
63
63
|
case 'working':
|
|
64
|
-
return FG.
|
|
64
|
+
return FG.BRIGHT_BLUE;
|
|
65
65
|
case 'waiting':
|
|
66
|
-
return FG.
|
|
67
|
-
case 'stale':
|
|
68
|
-
return FG.BRIGHT_BLACK;
|
|
66
|
+
return FG.BRIGHT_WHITE;
|
|
69
67
|
case 'notification':
|
|
70
|
-
return FG.
|
|
71
|
-
case 'error':
|
|
72
|
-
return FG.RED;
|
|
68
|
+
return FG.BRIGHT_RED;
|
|
73
69
|
default:
|
|
74
|
-
return FG.
|
|
70
|
+
return FG.BRIGHT_BLACK;
|
|
75
71
|
}
|
|
76
72
|
}
|
|
77
73
|
|
|
74
|
+
const STATUS_LABEL = {
|
|
75
|
+
working: '작업중',
|
|
76
|
+
waiting: '대기중',
|
|
77
|
+
notification: '응답 요청',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function statusLabel(status) {
|
|
81
|
+
return STATUS_LABEL[status] ?? status;
|
|
82
|
+
}
|
|
83
|
+
|
|
78
84
|
/**
|
|
79
85
|
* Return a colored status label string.
|
|
80
86
|
*/
|
|
81
87
|
export function formatStatus(status) {
|
|
82
|
-
return color(statusColor(status), status);
|
|
88
|
+
return color(statusColor(status), statusLabel(status));
|
|
83
89
|
}
|
|
84
90
|
|
package/src/ui/renderer.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
padEnd,
|
|
21
21
|
padStart,
|
|
22
22
|
statusColor,
|
|
23
|
+
statusLabel,
|
|
23
24
|
truncate,
|
|
24
25
|
} from './format.mjs';
|
|
25
26
|
|
|
@@ -224,28 +225,56 @@ export class Renderer {
|
|
|
224
225
|
const lines = [];
|
|
225
226
|
|
|
226
227
|
if (cols < MIN_WIDTH) {
|
|
227
|
-
lines
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
this.#
|
|
233
|
-
|
|
228
|
+
this.#renderCompact(lines, cols, rows);
|
|
229
|
+
} else {
|
|
230
|
+
// Use cols-1 so the box never touches the terminal's last column,
|
|
231
|
+
// avoiding auto-wrap artifacts in terminals like Ghostty.
|
|
232
|
+
const w = cols - 1;
|
|
233
|
+
if (this.#view === 'detail' && this.#sessions.length > 0) {
|
|
234
|
+
this.#renderDetail(lines, w, rows);
|
|
235
|
+
} else {
|
|
236
|
+
this.#renderList(lines, w, rows);
|
|
237
|
+
}
|
|
234
238
|
}
|
|
239
|
+
this.#flush(lines, rows);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Compact view: project name + colored status indicator (COMPACT_WIDTH <= cols < MIN_WIDTH). */
|
|
243
|
+
#renderCompact(lines, cols, rows) {
|
|
244
|
+
const w = cols - 1; // avoid auto-wrap like full view
|
|
245
|
+
const inner = w - 2; // between │ and │
|
|
235
246
|
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
const
|
|
247
|
+
// Top border with title: ╭─ Claude Code Watcher ───╮
|
|
248
|
+
const titleStr = '─ Claude Code Watcher ';
|
|
249
|
+
const rightDashes = Math.max(0, inner - titleStr.length);
|
|
250
|
+
lines.push(color(FG.WHITE, `╭${titleStr}${'─'.repeat(rightDashes)}╮`));
|
|
239
251
|
|
|
240
|
-
if (this.#
|
|
241
|
-
|
|
252
|
+
if (this.#sessions.length === 0) {
|
|
253
|
+
lines.push(
|
|
254
|
+
`${color(FG.WHITE, '│')}${padEnd(color(FG.BRIGHT_BLACK, ' ○'), inner)}${color(FG.WHITE, '│')}`,
|
|
255
|
+
);
|
|
242
256
|
} else {
|
|
243
|
-
|
|
257
|
+
const nameW = inner - 3; // ' ● ' = 3
|
|
258
|
+
for (const session of this.#sessions) {
|
|
259
|
+
const dot = color(statusColor(session.status), '●');
|
|
260
|
+
const name = color(statusColor(session.status), padEnd(truncate(session.displayName, nameW), nameW));
|
|
261
|
+
lines.push(
|
|
262
|
+
`${color(FG.WHITE, '│')} ${dot} ${name}${color(FG.WHITE, '│')}`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
244
265
|
}
|
|
266
|
+
|
|
267
|
+
lines.push(color(FG.WHITE, `╰${'─'.repeat(inner)}╯`));
|
|
268
|
+
|
|
269
|
+
const credit = color(FG.BRIGHT_BLACK, '@roy-jung');
|
|
270
|
+
lines.push(
|
|
271
|
+
' '.repeat(Math.max(0, w - visibleLength('@roy-jung'))) + credit,
|
|
272
|
+
);
|
|
273
|
+
|
|
245
274
|
this.#flush(lines, rows);
|
|
246
275
|
}
|
|
247
276
|
|
|
248
|
-
|
|
277
|
+
#renderHeader(lines, cols, now) {
|
|
249
278
|
const count = this.#sessions.length;
|
|
250
279
|
const inner = cols - 4;
|
|
251
280
|
const dot = count > 0 ? color(FG.CYAN, '●') : color(FG.BRIGHT_BLACK, '○');
|
|
@@ -304,7 +333,9 @@ export class Renderer {
|
|
|
304
333
|
color(isSelected ? FG.CYAN : FG.BRIGHT_BLACK, numStr.padStart(NUM_W)),
|
|
305
334
|
NUM_W,
|
|
306
335
|
);
|
|
307
|
-
const
|
|
336
|
+
const rc = str => color(statusColor(session.status), str);
|
|
337
|
+
|
|
338
|
+
const proj = rc(padEnd(truncate(session.displayName, projW), projW));
|
|
308
339
|
const subagents = this.#store.getSubagents(session.sessionId);
|
|
309
340
|
const activeSubCount = subagents.filter(
|
|
310
341
|
s => s.status === 'working',
|
|
@@ -313,7 +344,7 @@ export class Renderer {
|
|
|
313
344
|
if (activeSubCount > 0) {
|
|
314
345
|
const badge = color(FG.BRIGHT_BLACK, `[${activeSubCount}]`);
|
|
315
346
|
const badgeW = 1 + String(activeSubCount).length + 2; // ' [N]'
|
|
316
|
-
const label = truncate(session.status, STAT_W - badgeW);
|
|
347
|
+
const label = truncate(statusLabel(session.status), STAT_W - badgeW);
|
|
317
348
|
stat = padEnd(
|
|
318
349
|
`${coloredStatus(session.status, label)} ${badge}`,
|
|
319
350
|
STAT_W,
|
|
@@ -321,14 +352,11 @@ export class Renderer {
|
|
|
321
352
|
} else {
|
|
322
353
|
stat = padEnd(formatStatus(session.status), STAT_W);
|
|
323
354
|
}
|
|
324
|
-
const msg = padEnd(
|
|
355
|
+
const msg = rc(padEnd(
|
|
325
356
|
truncate(session.message || session.lastResponse || '', msgW),
|
|
326
357
|
msgW,
|
|
327
|
-
);
|
|
328
|
-
const time =
|
|
329
|
-
FG.BRIGHT_BLACK,
|
|
330
|
-
padEnd(session.sinceLabel || '', TIME_W),
|
|
331
|
-
);
|
|
358
|
+
));
|
|
359
|
+
const time = rc(padEnd(session.sinceLabel || '', TIME_W));
|
|
332
360
|
|
|
333
361
|
const rowLine = buildDataLine(sel, num, proj, stat, msg, time);
|
|
334
362
|
lines.push(
|