claude-code-monitor 1.1.0 → 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.
- package/README.md +19 -3
- package/dist/hooks/useServer.d.ts.map +1 -1
- package/dist/hooks/useServer.js +14 -7
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +18 -3
- package/dist/utils/focus.d.ts +1 -1
- package/dist/utils/focus.d.ts.map +1 -1
- package/dist/utils/focus.js +8 -6
- package/package.json +1 -1
- package/public/index.html +35 -5
package/README.md
CHANGED
|
@@ -65,9 +65,11 @@ On first run, it automatically sets up hooks and launches the monitor.
|
|
|
65
65
|
|
|
66
66
|
### Mobile Access
|
|
67
67
|
|
|
68
|
-
1. Press `h` to show QR code
|
|
68
|
+
1. Press `h` to show QR code (default port: 3456)
|
|
69
69
|
2. Scan with your smartphone (same Wi-Fi required)
|
|
70
70
|
|
|
71
|
+
> If port 3456 is in use, an available port is automatically selected.
|
|
72
|
+
|
|
71
73
|
---
|
|
72
74
|
|
|
73
75
|
## 📖 Usage
|
|
@@ -114,13 +116,15 @@ Monitor and control Claude Code sessions from your smartphone.
|
|
|
114
116
|
- Real-time session status via WebSocket
|
|
115
117
|
- View latest Claude messages
|
|
116
118
|
- Focus terminal sessions remotely
|
|
117
|
-
- Send text messages to terminal
|
|
119
|
+
- Send text messages to terminal (multi-line supported)
|
|
120
|
+
- Swipe-to-close gesture on modal
|
|
121
|
+
- Warning display for dangerous commands
|
|
118
122
|
|
|
119
123
|
### Security
|
|
120
124
|
|
|
121
125
|
> **Important**: Your smartphone and Mac must be on the **same Wi-Fi network**.
|
|
122
126
|
|
|
123
|
-
- **Token Authentication** -
|
|
127
|
+
- **Token Authentication** - A unique token is generated for authentication
|
|
124
128
|
- **Local Network Only** - Not accessible from the internet
|
|
125
129
|
- **Do not share the URL** - Treat it like a password
|
|
126
130
|
|
|
@@ -188,6 +192,18 @@ This is an unofficial community tool and is not affiliated with Anthropic.
|
|
|
188
192
|
|
|
189
193
|
---
|
|
190
194
|
|
|
195
|
+
## 🐛 Issues
|
|
196
|
+
|
|
197
|
+
Found a bug? [Open an issue](https://github.com/onikan27/claude-code-monitor/issues)
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 🤝 Contributing
|
|
202
|
+
|
|
203
|
+
Contributions are welcome! Please open an issue or submit a PR.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
191
207
|
## 📝 Changelog
|
|
192
208
|
|
|
193
209
|
See [CHANGELOG.md](./CHANGELOG.md) for details.
|
|
@@ -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,
|
|
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"}
|
package/dist/hooks/useServer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
17
|
+
const info = await createMobileServer(port);
|
|
16
18
|
if (isMounted) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 (
|
|
34
|
-
|
|
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":"
|
|
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"}
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|
package/dist/utils/focus.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sanitize a string for safe use in AppleScript.
|
|
3
|
-
* Escapes backslashes, double quotes,
|
|
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,
|
|
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"}
|
package/dist/utils/focus.js
CHANGED
|
@@ -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,
|
|
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
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
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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 ${
|
|
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>
|