claude-code-monitor 1.0.3 → 1.1.0

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +83 -104
  3. package/dist/bin/ccm.js +51 -18
  4. package/dist/components/Dashboard.d.ts +6 -1
  5. package/dist/components/Dashboard.d.ts.map +1 -1
  6. package/dist/components/Dashboard.js +39 -5
  7. package/dist/components/SessionCard.d.ts.map +1 -1
  8. package/dist/components/SessionCard.js +2 -4
  9. package/dist/components/Spinner.js +1 -1
  10. package/dist/constants.d.ts +5 -2
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +5 -2
  13. package/dist/hook/handler.d.ts.map +1 -1
  14. package/dist/hook/handler.js +16 -15
  15. package/dist/hooks/useServer.d.ts +10 -0
  16. package/dist/hooks/useServer.d.ts.map +1 -0
  17. package/dist/hooks/useServer.js +39 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/server/index.d.ts +12 -0
  22. package/dist/server/index.d.ts.map +1 -0
  23. package/dist/server/index.js +309 -0
  24. package/dist/store/file-store.d.ts +5 -0
  25. package/dist/store/file-store.d.ts.map +1 -1
  26. package/dist/store/file-store.js +40 -7
  27. package/dist/types/index.d.ts +2 -0
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/dist/utils/applescript.d.ts +7 -0
  30. package/dist/utils/applescript.d.ts.map +1 -0
  31. package/dist/utils/applescript.js +18 -0
  32. package/dist/utils/focus.d.ts +0 -1
  33. package/dist/utils/focus.d.ts.map +1 -1
  34. package/dist/utils/focus.js +16 -22
  35. package/dist/utils/send-text.d.ts +40 -0
  36. package/dist/utils/send-text.d.ts.map +1 -0
  37. package/dist/utils/send-text.js +324 -0
  38. package/dist/utils/status.js +2 -2
  39. package/dist/utils/stdin.d.ts +6 -0
  40. package/dist/utils/stdin.d.ts.map +1 -0
  41. package/dist/utils/stdin.js +12 -0
  42. package/dist/utils/terminal-strategy.d.ts +18 -0
  43. package/dist/utils/terminal-strategy.d.ts.map +1 -0
  44. package/dist/utils/terminal-strategy.js +15 -0
  45. package/dist/utils/time.d.ts.map +1 -1
  46. package/dist/utils/time.js +1 -3
  47. package/dist/utils/transcript.d.ts +10 -0
  48. package/dist/utils/transcript.d.ts.map +1 -0
  49. package/dist/utils/transcript.js +45 -0
  50. package/dist/utils/tty-cache.d.ts +0 -5
  51. package/dist/utils/tty-cache.d.ts.map +1 -1
  52. package/dist/utils/tty-cache.js +0 -7
  53. package/package.json +6 -2
  54. package/public/index.html +1219 -0
@@ -1,4 +1,5 @@
1
- import { execFileSync } from 'node:child_process';
1
+ import { executeAppleScript } from './applescript.js';
2
+ import { executeWithTerminalFallback } from './terminal-strategy.js';
2
3
  /**
3
4
  * Sanitize a string for safe use in AppleScript.
4
5
  * Escapes backslashes, double quotes, and control characters to prevent injection.
@@ -12,25 +13,20 @@ export function sanitizeForAppleScript(str) {
12
13
  .replace(/\r/g, '\\r')
13
14
  .replace(/\t/g, '\\t');
14
15
  }
16
+ /**
17
+ * TTY path pattern for validation.
18
+ * Matches:
19
+ * - macOS: /dev/ttys000, /dev/tty000
20
+ * - Linux: /dev/pts/0
21
+ * @internal
22
+ */
23
+ const TTY_PATH_PATTERN = /^\/dev\/(ttys?\d+|pts\/\d+)$/;
15
24
  /**
16
25
  * Validate TTY path format.
17
- * Only allows paths like /dev/ttys000, /dev/pts/0, etc.
18
26
  * @internal
19
27
  */
20
28
  export function isValidTtyPath(tty) {
21
- return /^\/dev\/(ttys?\d+|pts\/\d+)$/.test(tty);
22
- }
23
- function executeAppleScript(script) {
24
- try {
25
- const result = execFileSync('osascript', ['-e', script], {
26
- encoding: 'utf-8',
27
- stdio: ['pipe', 'pipe', 'pipe'],
28
- }).trim();
29
- return result === 'true';
30
- }
31
- catch {
32
- return false;
33
- }
29
+ return TTY_PATH_PATTERN.test(tty);
34
30
  }
35
31
  function buildITerm2Script(tty) {
36
32
  const safeTty = sanitizeForAppleScript(tty);
@@ -96,13 +92,11 @@ export function focusSession(tty) {
96
92
  return false;
97
93
  if (!isValidTtyPath(tty))
98
94
  return false;
99
- // Try each terminal in order (use the first one that succeeds)
100
- const focusStrategies = [
101
- () => focusITerm2(tty),
102
- () => focusTerminalApp(tty),
103
- () => focusGhostty(),
104
- ];
105
- return focusStrategies.some((tryFocus) => tryFocus());
95
+ return executeWithTerminalFallback({
96
+ iTerm2: () => focusITerm2(tty),
97
+ terminalApp: () => focusTerminalApp(tty),
98
+ ghostty: () => focusGhostty(),
99
+ });
106
100
  }
107
101
  export function getSupportedTerminals() {
108
102
  return ['iTerm2', 'Terminal.app', 'Ghostty'];
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Validate text input for sending to terminal.
3
+ * @internal
4
+ */
5
+ export declare function validateTextInput(text: string): {
6
+ valid: boolean;
7
+ error?: string;
8
+ };
9
+ /**
10
+ * Send text to a terminal session and execute it (press Enter).
11
+ * Tries iTerm2, Terminal.app, and Ghostty in order.
12
+ *
13
+ * @param tty - The TTY path of the target terminal session
14
+ * @param text - The text to send to the terminal
15
+ * @returns true if text was sent successfully, false otherwise
16
+ *
17
+ * @remarks
18
+ * - This is macOS only (uses AppleScript)
19
+ * - For iTerm2 and Terminal.app, targets specific TTY
20
+ * - For Ghostty, sends to the active window (TTY targeting not supported)
21
+ * - System Events usage for Ghostty may require accessibility permissions
22
+ */
23
+ export declare function sendTextToTerminal(tty: string, text: string): {
24
+ success: boolean;
25
+ error?: string;
26
+ };
27
+ /**
28
+ * Send a single keystroke to a terminal session.
29
+ * Used for responding to permission prompts (y/n/a), Ctrl+C to abort, or Escape to cancel.
30
+ *
31
+ * @param tty - The TTY path of the target terminal session
32
+ * @param key - Single character key to send (y, n, a, 1-9, escape, etc.)
33
+ * @param useControl - If true, send with Control modifier (for Ctrl+C)
34
+ * @returns Result object with success status
35
+ */
36
+ export declare function sendKeystrokeToTerminal(tty: string, key: string, useControl?: boolean): {
37
+ success: boolean;
38
+ error?: string;
39
+ };
40
+ //# sourceMappingURL=send-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send-text.d.ts","sourceRoot":"","sources":["../../src/utils/send-text.ts"],"names":[],"mappings":"AAUA;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAUlF;AAoPD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GACX;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAuBtC;AA0BD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,UAAU,UAAQ,GACjB;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAuCtC"}
@@ -0,0 +1,324 @@
1
+ import { executeAppleScript } from './applescript.js';
2
+ import { isMacOS, isValidTtyPath, sanitizeForAppleScript } from './focus.js';
3
+ import { executeWithTerminalFallback } from './terminal-strategy.js';
4
+ /**
5
+ * Maximum text length allowed for sending to terminal.
6
+ * This is a security measure to prevent accidental or malicious large inputs.
7
+ */
8
+ const MAX_TEXT_LENGTH = 10000;
9
+ /**
10
+ * Validate text input for sending to terminal.
11
+ * @internal
12
+ */
13
+ export function validateTextInput(text) {
14
+ if (!text || text.trim().length === 0) {
15
+ return { valid: false, error: 'Text cannot be empty' };
16
+ }
17
+ if (text.length > MAX_TEXT_LENGTH) {
18
+ return { valid: false, error: `Text exceeds maximum length of ${MAX_TEXT_LENGTH} characters` };
19
+ }
20
+ return { valid: true };
21
+ }
22
+ /**
23
+ * Build AppleScript to send text to iTerm2 session by TTY.
24
+ * Uses clipboard and Cmd+V to paste text, then sends Enter key.
25
+ * This approach supports Unicode characters including Japanese.
26
+ */
27
+ function buildITerm2SendTextScript(tty, text) {
28
+ const safeTty = sanitizeForAppleScript(tty);
29
+ const safeText = sanitizeForAppleScript(text);
30
+ return `
31
+ set the clipboard to "${safeText}"
32
+ tell application "iTerm2"
33
+ repeat with aWindow in windows
34
+ repeat with aTab in tabs of aWindow
35
+ repeat with aSession in sessions of aTab
36
+ if tty of aSession is "${safeTty}" then
37
+ select aSession
38
+ select aTab
39
+ tell aWindow to select
40
+ activate
41
+ delay 0.3
42
+ tell application "System Events"
43
+ set frontmost of process "iTerm2" to true
44
+ delay 0.1
45
+ tell process "iTerm2"
46
+ keystroke "v" using command down
47
+ delay 0.1
48
+ keystroke return
49
+ end tell
50
+ end tell
51
+ return true
52
+ end if
53
+ end repeat
54
+ end repeat
55
+ end repeat
56
+ return false
57
+ end tell
58
+ `;
59
+ }
60
+ /**
61
+ * Build AppleScript to send text to Terminal.app by TTY.
62
+ * Uses clipboard and Cmd+V to paste text, then sends Enter key.
63
+ * This approach supports Unicode characters including Japanese.
64
+ */
65
+ function buildTerminalAppSendTextScript(tty, text) {
66
+ const safeTty = sanitizeForAppleScript(tty);
67
+ const safeText = sanitizeForAppleScript(text);
68
+ return `
69
+ set the clipboard to "${safeText}"
70
+ tell application "Terminal"
71
+ repeat with aWindow in windows
72
+ repeat with aTab in tabs of aWindow
73
+ if tty of aTab is "${safeTty}" then
74
+ set selected of aTab to true
75
+ set index of aWindow to 1
76
+ activate
77
+ delay 0.2
78
+ tell application "System Events"
79
+ tell process "Terminal"
80
+ keystroke "v" using command down
81
+ delay 0.1
82
+ keystroke return
83
+ end tell
84
+ end tell
85
+ return true
86
+ end if
87
+ end repeat
88
+ end repeat
89
+ return false
90
+ end tell
91
+ `;
92
+ }
93
+ /**
94
+ * Build AppleScript to send text to Ghostty via System Events.
95
+ * Ghostty doesn't support TTY-based targeting, so we send to the active window.
96
+ * Uses clipboard and Cmd+V to paste text, then sends Enter key.
97
+ * This approach supports Unicode characters including Japanese.
98
+ */
99
+ function buildGhosttySendTextScript(text) {
100
+ const safeText = sanitizeForAppleScript(text);
101
+ return `
102
+ set the clipboard to "${safeText}"
103
+ tell application "Ghostty"
104
+ activate
105
+ end tell
106
+ delay 0.6
107
+ tell application "System Events"
108
+ set frontmost of process "Ghostty" to true
109
+ delay 0.2
110
+ tell process "Ghostty"
111
+ keystroke "v" using command down
112
+ delay 0.2
113
+ keystroke return
114
+ end tell
115
+ end tell
116
+ return true
117
+ `;
118
+ }
119
+ function sendTextToITerm2(tty, text) {
120
+ return executeAppleScript(buildITerm2SendTextScript(tty, text));
121
+ }
122
+ function sendTextToTerminalApp(tty, text) {
123
+ return executeAppleScript(buildTerminalAppSendTextScript(tty, text));
124
+ }
125
+ function sendTextToGhostty(text) {
126
+ return executeAppleScript(buildGhosttySendTextScript(text));
127
+ }
128
+ // ============================================
129
+ // Direct Keystroke Functions (for permission prompts)
130
+ // ============================================
131
+ /**
132
+ * Build AppleScript to send a single keystroke to iTerm2 session by TTY.
133
+ * Used for permission prompts that expect single key input (y/n/a).
134
+ */
135
+ function buildITerm2KeystrokeScript(tty, key, useControl = false, useKeyCode) {
136
+ const safeTty = sanitizeForAppleScript(tty);
137
+ const safeKey = sanitizeForAppleScript(key);
138
+ const modifiers = useControl ? ' using control down' : '';
139
+ const keystrokeCmd = useKeyCode !== undefined ? `key code ${useKeyCode}` : `keystroke "${safeKey}"${modifiers}`;
140
+ return `
141
+ tell application "iTerm2"
142
+ repeat with aWindow in windows
143
+ repeat with aTab in tabs of aWindow
144
+ repeat with aSession in sessions of aTab
145
+ if tty of aSession is "${safeTty}" then
146
+ select aSession
147
+ select aTab
148
+ tell aWindow to select
149
+ activate
150
+ delay 0.2
151
+ tell application "System Events"
152
+ set frontmost of process "iTerm2" to true
153
+ delay 0.1
154
+ tell process "iTerm2"
155
+ ${keystrokeCmd}
156
+ end tell
157
+ end tell
158
+ return true
159
+ end if
160
+ end repeat
161
+ end repeat
162
+ end repeat
163
+ return false
164
+ end tell
165
+ `;
166
+ }
167
+ /**
168
+ * Build AppleScript to send a single keystroke to Terminal.app by TTY.
169
+ */
170
+ function buildTerminalAppKeystrokeScript(tty, key, useControl = false, useKeyCode) {
171
+ const safeTty = sanitizeForAppleScript(tty);
172
+ const safeKey = sanitizeForAppleScript(key);
173
+ const modifiers = useControl ? ' using control down' : '';
174
+ const keystrokeCmd = useKeyCode !== undefined ? `key code ${useKeyCode}` : `keystroke "${safeKey}"${modifiers}`;
175
+ return `
176
+ tell application "Terminal"
177
+ repeat with aWindow in windows
178
+ repeat with aTab in tabs of aWindow
179
+ if tty of aTab is "${safeTty}" then
180
+ set selected of aTab to true
181
+ set index of aWindow to 1
182
+ activate
183
+ delay 0.2
184
+ tell application "System Events"
185
+ tell process "Terminal"
186
+ ${keystrokeCmd}
187
+ end tell
188
+ end tell
189
+ return true
190
+ end if
191
+ end repeat
192
+ end repeat
193
+ return false
194
+ end tell
195
+ `;
196
+ }
197
+ /**
198
+ * Build AppleScript to send a single keystroke to Ghostty.
199
+ */
200
+ function buildGhosttyKeystrokeScript(key, useControl = false, useKeyCode) {
201
+ const safeKey = sanitizeForAppleScript(key);
202
+ const modifiers = useControl ? ' using control down' : '';
203
+ const keystrokeCmd = useKeyCode !== undefined ? `key code ${useKeyCode}` : `keystroke "${safeKey}"${modifiers}`;
204
+ return `
205
+ tell application "Ghostty"
206
+ activate
207
+ end tell
208
+ delay 0.4
209
+ tell application "System Events"
210
+ set frontmost of process "Ghostty" to true
211
+ delay 0.1
212
+ tell process "Ghostty"
213
+ ${keystrokeCmd}
214
+ end tell
215
+ end tell
216
+ return true
217
+ `;
218
+ }
219
+ function sendKeystrokeToITerm2(tty, key, useControl = false, useKeyCode) {
220
+ return executeAppleScript(buildITerm2KeystrokeScript(tty, key, useControl, useKeyCode));
221
+ }
222
+ function sendKeystrokeToTerminalApp(tty, key, useControl = false, useKeyCode) {
223
+ return executeAppleScript(buildTerminalAppKeystrokeScript(tty, key, useControl, useKeyCode));
224
+ }
225
+ function sendKeystrokeToGhostty(key, useControl = false, useKeyCode) {
226
+ return executeAppleScript(buildGhosttyKeystrokeScript(key, useControl, useKeyCode));
227
+ }
228
+ /**
229
+ * Send text to a terminal session and execute it (press Enter).
230
+ * Tries iTerm2, Terminal.app, and Ghostty in order.
231
+ *
232
+ * @param tty - The TTY path of the target terminal session
233
+ * @param text - The text to send to the terminal
234
+ * @returns true if text was sent successfully, false otherwise
235
+ *
236
+ * @remarks
237
+ * - This is macOS only (uses AppleScript)
238
+ * - For iTerm2 and Terminal.app, targets specific TTY
239
+ * - For Ghostty, sends to the active window (TTY targeting not supported)
240
+ * - System Events usage for Ghostty may require accessibility permissions
241
+ */
242
+ export function sendTextToTerminal(tty, text) {
243
+ if (!isMacOS()) {
244
+ return { success: false, error: 'This feature is only available on macOS' };
245
+ }
246
+ if (!isValidTtyPath(tty)) {
247
+ return { success: false, error: 'Invalid TTY path' };
248
+ }
249
+ const validation = validateTextInput(text);
250
+ if (!validation.valid) {
251
+ return { success: false, error: validation.error };
252
+ }
253
+ const success = executeWithTerminalFallback({
254
+ iTerm2: () => sendTextToITerm2(tty, text),
255
+ terminalApp: () => sendTextToTerminalApp(tty, text),
256
+ ghostty: () => sendTextToGhostty(text),
257
+ });
258
+ return success
259
+ ? { success: true }
260
+ : { success: false, error: 'Could not send text to any terminal' };
261
+ }
262
+ /**
263
+ * Allowed keys for permission prompt responses.
264
+ */
265
+ const ALLOWED_KEYS = new Set([
266
+ 'y',
267
+ 'n',
268
+ 'a',
269
+ '1',
270
+ '2',
271
+ '3',
272
+ '4',
273
+ '5',
274
+ '6',
275
+ '7',
276
+ '8',
277
+ '9',
278
+ 'escape',
279
+ ]);
280
+ /**
281
+ * macOS key code for Escape key.
282
+ */
283
+ const ESCAPE_KEY_CODE = 53;
284
+ /**
285
+ * Send a single keystroke to a terminal session.
286
+ * Used for responding to permission prompts (y/n/a), Ctrl+C to abort, or Escape to cancel.
287
+ *
288
+ * @param tty - The TTY path of the target terminal session
289
+ * @param key - Single character key to send (y, n, a, 1-9, escape, etc.)
290
+ * @param useControl - If true, send with Control modifier (for Ctrl+C)
291
+ * @returns Result object with success status
292
+ */
293
+ export function sendKeystrokeToTerminal(tty, key, useControl = false) {
294
+ if (!isMacOS()) {
295
+ return { success: false, error: 'This feature is only available on macOS' };
296
+ }
297
+ if (!isValidTtyPath(tty)) {
298
+ return { success: false, error: 'Invalid TTY path' };
299
+ }
300
+ const lowerKey = key.toLowerCase();
301
+ const isEscapeKey = lowerKey === 'escape';
302
+ // Validate key input (escape is special, others must be single character)
303
+ if (!isEscapeKey && (!key || key.length !== 1)) {
304
+ return { success: false, error: 'Key must be a single character or "escape"' };
305
+ }
306
+ // Only allow specific keys for security
307
+ if (!useControl && !ALLOWED_KEYS.has(lowerKey)) {
308
+ return { success: false, error: 'Invalid key. Allowed: y, n, a, 1-9, escape' };
309
+ }
310
+ // For Ctrl+C, only allow 'c'
311
+ if (useControl && lowerKey !== 'c') {
312
+ return { success: false, error: 'Only Ctrl+C is supported' };
313
+ }
314
+ // Determine if we need to use key code (for Escape key)
315
+ const useKeyCode = isEscapeKey ? ESCAPE_KEY_CODE : undefined;
316
+ const success = executeWithTerminalFallback({
317
+ iTerm2: () => sendKeystrokeToITerm2(tty, key, useControl, useKeyCode),
318
+ terminalApp: () => sendKeystrokeToTerminalApp(tty, key, useControl, useKeyCode),
319
+ ghostty: () => sendKeystrokeToGhostty(key, useControl, useKeyCode),
320
+ });
321
+ return success
322
+ ? { success: true }
323
+ : { success: false, error: 'Could not send keystroke to any terminal' };
324
+ }
@@ -1,10 +1,10 @@
1
1
  export function getStatusDisplay(status) {
2
2
  switch (status) {
3
3
  case 'running':
4
- return { symbol: '●', color: 'green', label: 'Running' };
4
+ return { symbol: '●', color: 'gray', label: 'Running' };
5
5
  case 'waiting_input':
6
6
  return { symbol: '◐', color: 'yellow', label: 'Waiting' };
7
7
  case 'stopped':
8
- return { symbol: '✓', color: 'cyan', label: 'Done' };
8
+ return { symbol: '✓', color: 'green', label: 'Done' };
9
9
  }
10
10
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Read all data from stdin and parse as JSON.
3
+ * @throws {SyntaxError} If the input is not valid JSON
4
+ */
5
+ export declare function readJsonFromStdin<T>(): Promise<T>;
6
+ //# sourceMappingURL=stdin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdin.d.ts","sourceRoot":"","sources":["../../src/utils/stdin.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAOvD"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Read all data from stdin and parse as JSON.
3
+ * @throws {SyntaxError} If the input is not valid JSON
4
+ */
5
+ export async function readJsonFromStdin() {
6
+ const chunks = [];
7
+ for await (const chunk of process.stdin) {
8
+ chunks.push(chunk);
9
+ }
10
+ const input = Buffer.concat(chunks).toString('utf-8');
11
+ return JSON.parse(input);
12
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Terminal operation strategies for macOS.
3
+ * Provides a unified interface to execute operations across different terminal apps.
4
+ */
5
+ export interface TerminalOperations {
6
+ iTerm2: () => boolean;
7
+ terminalApp: () => boolean;
8
+ ghostty: () => boolean;
9
+ }
10
+ /**
11
+ * Execute terminal operation with fallback strategy.
12
+ * Tries iTerm2 → Terminal.app → Ghostty in order, returning on first success.
13
+ *
14
+ * @param operations - Terminal-specific operation functions
15
+ * @returns true if any terminal operation succeeded, false otherwise
16
+ */
17
+ export declare function executeWithTerminalFallback(operations: TerminalOperations): boolean;
18
+ //# sourceMappingURL=terminal-strategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-strategy.d.ts","sourceRoot":"","sources":["../../src/utils/terminal-strategy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,OAAO,CAAC;IAC3B,OAAO,EAAE,MAAM,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAGnF"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Terminal operation strategies for macOS.
3
+ * Provides a unified interface to execute operations across different terminal apps.
4
+ */
5
+ /**
6
+ * Execute terminal operation with fallback strategy.
7
+ * Tries iTerm2 → Terminal.app → Ghostty in order, returning on first success.
8
+ *
9
+ * @param operations - Terminal-specific operation functions
10
+ * @returns true if any terminal operation succeeded, false otherwise
11
+ */
12
+ export function executeWithTerminalFallback(operations) {
13
+ const strategies = [operations.iTerm2, operations.terminalApp, operations.ghostty];
14
+ return strategies.some((op) => op());
15
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../../src/utils/time.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAc5D"}
1
+ {"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../../src/utils/time.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAa5D"}
@@ -13,7 +13,5 @@ export function formatRelativeTime(timestamp) {
13
13
  return `${hours}h ago`;
14
14
  if (minutes > 0)
15
15
  return `${minutes}m ago`;
16
- if (seconds >= 0)
17
- return `${seconds}s ago`;
18
- return 'now';
16
+ return `${seconds}s ago`;
19
17
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Build transcript file path from cwd and session_id.
3
+ * Claude Code stores transcripts at ~/.claude/projects/{encoded-cwd}/{session_id}.jsonl
4
+ */
5
+ export declare function buildTranscriptPath(cwd: string, sessionId: string): string;
6
+ /**
7
+ * Get the last assistant text message from a transcript file.
8
+ */
9
+ export declare function getLastAssistantMessage(transcriptPath: string): string | undefined;
10
+ //# sourceMappingURL=transcript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../src/utils/transcript.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAI1E;AAOD;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA+BlF"}
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Build transcript file path from cwd and session_id.
6
+ * Claude Code stores transcripts at ~/.claude/projects/{encoded-cwd}/{session_id}.jsonl
7
+ */
8
+ export function buildTranscriptPath(cwd, sessionId) {
9
+ // Encode cwd: replace / and . with - (including leading /)
10
+ const encodedCwd = cwd.replace(/[/.]/g, '-');
11
+ return join(homedir(), '.claude', 'projects', encodedCwd, `${sessionId}.jsonl`);
12
+ }
13
+ /**
14
+ * Get the last assistant text message from a transcript file.
15
+ */
16
+ export function getLastAssistantMessage(transcriptPath) {
17
+ if (!transcriptPath || !existsSync(transcriptPath)) {
18
+ return undefined;
19
+ }
20
+ try {
21
+ const content = readFileSync(transcriptPath, 'utf-8');
22
+ const lines = content.trim().split('\n').filter(Boolean);
23
+ // Read from end to find last text message
24
+ for (let i = lines.length - 1; i >= 0; i--) {
25
+ try {
26
+ const entry = JSON.parse(lines[i]);
27
+ if (entry.type === 'assistant' && entry.message?.content) {
28
+ const contentBlocks = entry.message.content;
29
+ for (const block of contentBlocks) {
30
+ if (block.type === 'text' && block.text) {
31
+ return block.text;
32
+ }
33
+ }
34
+ }
35
+ }
36
+ catch {
37
+ // Skip invalid JSON lines
38
+ }
39
+ }
40
+ }
41
+ catch {
42
+ // Ignore file read errors
43
+ }
44
+ return undefined;
45
+ }
@@ -4,9 +4,4 @@
4
4
  * @internal
5
5
  */
6
6
  export declare function isTtyAlive(tty: string | undefined): boolean;
7
- /**
8
- * Clear the TTY cache (useful for testing)
9
- * @internal
10
- */
11
- export declare function clearTtyCache(): void;
12
7
  //# sourceMappingURL=tty-cache.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tty-cache.d.ts","sourceRoot":"","sources":["../../src/utils/tty-cache.ts"],"names":[],"mappings":"AAwBA;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAsB3D;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAEpC"}
1
+ {"version":3,"file":"tty-cache.d.ts","sourceRoot":"","sources":["../../src/utils/tty-cache.ts"],"names":[],"mappings":"AAwBA;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAsB3D"}
@@ -44,10 +44,3 @@ export function isTtyAlive(tty) {
44
44
  evictOldestIfNeeded();
45
45
  return alive;
46
46
  }
47
- /**
48
- * Clear the TTY cache (useful for testing)
49
- * @internal
50
- */
51
- export function clearTtyCache() {
52
- ttyCache.clear();
53
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-monitor",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "CLI for monitoring multiple Claude Code sessions in real-time",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "dist",
19
+ "public",
19
20
  "README.md",
20
21
  "LICENSE",
21
22
  "CHANGELOG.md"
@@ -63,12 +64,15 @@
63
64
  "chokidar": "^4.0.3",
64
65
  "commander": "^13.1.0",
65
66
  "ink": "^5.2.0",
66
- "react": "^18.3.1"
67
+ "qrcode-terminal": "^0.12.0",
68
+ "react": "^18.3.1",
69
+ "ws": "^8.19.0"
67
70
  },
68
71
  "devDependencies": {
69
72
  "@biomejs/biome": "^2.3.11",
70
73
  "@types/node": "^22.15.30",
71
74
  "@types/react": "^18.3.12",
75
+ "@types/ws": "^8.18.1",
72
76
  "@vitest/coverage-v8": "^4.0.17",
73
77
  "tsx": "^4.20.3",
74
78
  "typescript": "^5.8.3",