claude-code-monitor 1.1.1 → 1.1.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"useServer.d.ts","sourceRoot":"","sources":["../../src/hooks/useServer.ts"],"names":[],"mappings":"AAGA,UAAU,eAAe;IACvB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAID,wBAAgB,SAAS,CAAC,IAAI,SAAe,GAAG,eAAe,CAuC9D"}
1
+ {"version":3,"file":"useServer.d.ts","sourceRoot":"","sources":["../../src/hooks/useServer.ts"],"names":[],"mappings":"AAGA,UAAU,eAAe;IACvB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAID,wBAAgB,SAAS,CAAC,IAAI,SAAe,GAAG,eAAe,CA6C9D"}
@@ -8,17 +8,24 @@ export function useServer(port = DEFAULT_PORT) {
8
8
  const [loading, setLoading] = useState(true);
9
9
  const [error, setError] = useState(null);
10
10
  useEffect(() => {
11
- let serverInfo = null;
11
+ // Use a ref-like object to track server across async boundaries
12
+ // This prevents race condition where cleanup runs before async completes
13
+ const serverRef = { current: null };
12
14
  let isMounted = true;
13
15
  async function startServer() {
14
16
  try {
15
- serverInfo = await createMobileServer(port);
17
+ const info = await createMobileServer(port);
16
18
  if (isMounted) {
17
- setUrl(serverInfo.url);
18
- setQrCode(serverInfo.qrCode);
19
- setActualPort(serverInfo.port);
19
+ serverRef.current = info;
20
+ setUrl(info.url);
21
+ setQrCode(info.qrCode);
22
+ setActualPort(info.port);
20
23
  setLoading(false);
21
24
  }
25
+ else {
26
+ // Component unmounted during async operation - stop server immediately
27
+ info.stop();
28
+ }
22
29
  }
23
30
  catch (err) {
24
31
  if (isMounted) {
@@ -30,8 +37,8 @@ export function useServer(port = DEFAULT_PORT) {
30
37
  startServer();
31
38
  return () => {
32
39
  isMounted = false;
33
- if (serverInfo) {
34
- serverInfo.stop();
40
+ if (serverRef.current) {
41
+ serverRef.current.stop();
35
42
  }
36
43
  };
37
44
  }, [port]);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAkOA,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAgB,UAAU,IAAI,MAAM,CAOnC;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM5D;AA0FD,wBAAsB,kBAAkB,CAAC,IAAI,SAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAoBjF;AAGD,wBAAsB,WAAW,CAAC,IAAI,SAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBpE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAyOA,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAgB,UAAU,IAAI,MAAM,CAOnC;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM5D;AAkGD,wBAAsB,kBAAkB,CAAC,IAAI,SAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAoBjF;AAGD,wBAAsB,WAAW,CAAC,IAAI,SAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BpE"}
@@ -21,6 +21,7 @@ function isPortAvailable(port) {
21
21
  return new Promise((resolve) => {
22
22
  const server = createNetServer();
23
23
  server.once('error', () => {
24
+ server.close(); // Ensure server is closed on error
24
25
  resolve(false);
25
26
  });
26
27
  server.once('listening', () => {
@@ -175,6 +176,11 @@ function setupWebSocketHandlers(wss, validToken) {
175
176
  }
176
177
  sendSessionsToClient(ws);
177
178
  ws.on('message', (data) => handleWebSocketMessage(ws, data));
179
+ // Handle client connection errors to prevent process crashes
180
+ ws.on('error', (error) => {
181
+ // Log error but don't crash - client disconnections are expected
182
+ console.error('WebSocket client error:', error.message);
183
+ });
178
184
  });
179
185
  }
180
186
  export function getLocalIP() {
@@ -260,9 +266,15 @@ function createServerComponents(token) {
260
266
  }
261
267
  /**
262
268
  * Stop all server components.
269
+ * Terminates all WebSocket clients before closing to prevent hanging.
263
270
  */
264
271
  function stopServerComponents({ watcher, wss, server }) {
265
- watcher.close();
272
+ // Close file watcher (async but we don't wait - acceptable for shutdown)
273
+ void watcher.close();
274
+ // Terminate all WebSocket clients before closing server
275
+ for (const client of wss.clients) {
276
+ client.terminate();
277
+ }
266
278
  wss.close();
267
279
  server.close();
268
280
  }
@@ -301,9 +313,12 @@ export async function startServer(port = DEFAULT_PORT) {
301
313
  qrcode.generate(url, { small: true });
302
314
  console.log('\n Press Ctrl+C to stop the server.\n');
303
315
  });
304
- process.on('SIGINT', () => {
316
+ // Graceful shutdown handler for both SIGINT (Ctrl+C) and SIGTERM (Docker/K8s)
317
+ const shutdown = () => {
305
318
  console.log('\n Shutting down...');
306
319
  stopServerComponents(components);
307
320
  process.exit(0);
308
- });
321
+ };
322
+ process.on('SIGINT', shutdown);
323
+ process.on('SIGTERM', shutdown);
309
324
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Sanitize a string for safe use in AppleScript.
3
- * Escapes backslashes, double quotes, and control characters to prevent injection.
3
+ * Escapes backslashes, double quotes, control characters, and AppleScript special chars.
4
4
  * @internal
5
5
  */
6
6
  export declare function sanitizeForAppleScript(str: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../../src/utils/focus.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO1D;AAWD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEnD;AAgED,wBAAgB,OAAO,IAAI,OAAO,CAEjC;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASjD;AAED,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAEhD"}
1
+ {"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../../src/utils/focus.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAS1D;AAWD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEnD;AAgED,wBAAgB,OAAO,IAAI,OAAO,CAEjC;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASjD;AAED,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAEhD"}
@@ -2,16 +2,18 @@ import { executeAppleScript } from './applescript.js';
2
2
  import { executeWithTerminalFallback } from './terminal-strategy.js';
3
3
  /**
4
4
  * Sanitize a string for safe use in AppleScript.
5
- * Escapes backslashes, double quotes, and control characters to prevent injection.
5
+ * Escapes backslashes, double quotes, control characters, and AppleScript special chars.
6
6
  * @internal
7
7
  */
8
8
  export function sanitizeForAppleScript(str) {
9
9
  return str
10
- .replace(/\\/g, '\\\\')
11
- .replace(/"/g, '\\"')
12
- .replace(/\n/g, '\\n')
13
- .replace(/\r/g, '\\r')
14
- .replace(/\t/g, '\\t');
10
+ .replace(/\\/g, '\\\\') // Backslash (must be first)
11
+ .replace(/"/g, '\\"') // Double quote
12
+ .replace(/\n/g, '\\n') // Newline
13
+ .replace(/\r/g, '\\r') // Carriage return
14
+ .replace(/\t/g, '\\t') // Tab
15
+ .replace(/\$/g, '\\$') // Dollar sign (variable reference in some contexts)
16
+ .replace(/`/g, '\\`'); // Backtick
15
17
  }
16
18
  /**
17
19
  * TTY path pattern for validation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-monitor",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "CLI for monitoring multiple Claude Code sessions in real-time",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/public/index.html CHANGED
@@ -890,6 +890,14 @@
890
890
  let pendingSend = null;
891
891
  let sessionsData = [];
892
892
 
893
+ // Event delegation for card clicks (safer than inline onclick)
894
+ $sessions.addEventListener('click', (e) => {
895
+ const card = e.target.closest('.card');
896
+ if (card && card.dataset.sessionId) {
897
+ openModal(card.dataset.sessionId);
898
+ }
899
+ });
900
+
893
901
  const STATUS = {
894
902
  running: 'Running',
895
903
  waiting_input: 'Waiting for Input',
@@ -960,6 +968,23 @@
960
968
  return clean.slice(0, maxLen) + '...';
961
969
  }
962
970
 
971
+ /**
972
+ * Escape HTML special characters to prevent XSS attacks.
973
+ */
974
+ function escapeHtml(text) {
975
+ if (!text) return '';
976
+ const div = document.createElement('div');
977
+ div.textContent = text;
978
+ return div.innerHTML;
979
+ }
980
+
981
+ /**
982
+ * Validate session ID format (alphanumeric, hyphens, underscores only).
983
+ */
984
+ function isValidSessionId(id) {
985
+ return /^[a-zA-Z0-9_-]+$/.test(id);
986
+ }
987
+
963
988
  function renderMarkdown(markdownText) {
964
989
  if (!markdownText) return '<span class="modal-message-empty">No messages yet</span>';
965
990
  if (typeof marked !== 'undefined') {
@@ -1118,13 +1143,18 @@
1118
1143
  }
1119
1144
 
1120
1145
  $sessions.innerHTML = list.map(session => {
1121
- const dirName = getDirName(session.cwd);
1122
- const shortPath = formatPath(session.cwd);
1123
- const statusLabel = STATUS[session.status];
1124
- const msgPreview = truncateMessage(session.lastMessage);
1146
+ // Skip sessions with invalid IDs to prevent XSS
1147
+ if (!isValidSessionId(session.session_id)) return '';
1148
+
1149
+ // Escape all user-provided content to prevent XSS
1150
+ const dirName = escapeHtml(getDirName(session.cwd));
1151
+ const shortPath = escapeHtml(formatPath(session.cwd));
1152
+ const statusLabel = escapeHtml(STATUS[session.status]);
1153
+ const msgPreview = escapeHtml(truncateMessage(session.lastMessage));
1154
+ const safeStatus = escapeHtml(session.status);
1125
1155
 
1126
1156
  return `
1127
- <div class="card ${session.status}" onclick="openModal('${session.session_id}')">
1157
+ <div class="card ${safeStatus}" data-session-id="${escapeHtml(session.session_id)}">
1128
1158
  <div class="card-status">
1129
1159
  <span class="card-status-dot"></span>
1130
1160
  <span>${statusLabel}</span>