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.
- package/CHANGELOG.md +40 -0
- package/README.md +83 -104
- package/dist/bin/ccm.js +51 -18
- package/dist/components/Dashboard.d.ts +6 -1
- package/dist/components/Dashboard.d.ts.map +1 -1
- package/dist/components/Dashboard.js +39 -5
- package/dist/components/SessionCard.d.ts.map +1 -1
- package/dist/components/SessionCard.js +2 -4
- package/dist/components/Spinner.js +1 -1
- package/dist/constants.d.ts +5 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -2
- package/dist/hook/handler.d.ts.map +1 -1
- package/dist/hook/handler.js +16 -15
- package/dist/hooks/useServer.d.ts +10 -0
- package/dist/hooks/useServer.d.ts.map +1 -0
- package/dist/hooks/useServer.js +39 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +309 -0
- package/dist/store/file-store.d.ts +5 -0
- package/dist/store/file-store.d.ts.map +1 -1
- package/dist/store/file-store.js +40 -7
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/applescript.d.ts +7 -0
- package/dist/utils/applescript.d.ts.map +1 -0
- package/dist/utils/applescript.js +18 -0
- package/dist/utils/focus.d.ts +0 -1
- package/dist/utils/focus.d.ts.map +1 -1
- package/dist/utils/focus.js +16 -22
- package/dist/utils/send-text.d.ts +40 -0
- package/dist/utils/send-text.d.ts.map +1 -0
- package/dist/utils/send-text.js +324 -0
- package/dist/utils/status.js +2 -2
- package/dist/utils/stdin.d.ts +6 -0
- package/dist/utils/stdin.d.ts.map +1 -0
- package/dist/utils/stdin.js +12 -0
- package/dist/utils/terminal-strategy.d.ts +18 -0
- package/dist/utils/terminal-strategy.d.ts.map +1 -0
- package/dist/utils/terminal-strategy.js +15 -0
- package/dist/utils/time.d.ts.map +1 -1
- package/dist/utils/time.js +1 -3
- package/dist/utils/transcript.d.ts +10 -0
- package/dist/utils/transcript.d.ts.map +1 -0
- package/dist/utils/transcript.js +45 -0
- package/dist/utils/tty-cache.d.ts +0 -5
- package/dist/utils/tty-cache.d.ts.map +1 -1
- package/dist/utils/tty-cache.js +0 -7
- package/package.json +6 -2
- package/public/index.html +1219 -0
package/dist/hook/handler.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { flushPendingWrites, updateSession } from '../store/file-store.js';
|
|
2
|
+
import { readJsonFromStdin } from '../utils/stdin.js';
|
|
3
|
+
import { buildTranscriptPath } from '../utils/transcript.js';
|
|
2
4
|
// Allowed hook event names (whitelist)
|
|
3
5
|
/** @internal */
|
|
4
6
|
export const VALID_HOOK_EVENTS = new Set([
|
|
@@ -22,41 +24,40 @@ export async function handleHookEvent(eventName, tty) {
|
|
|
22
24
|
console.error(`Invalid event name: ${eventName}`);
|
|
23
25
|
process.exit(1);
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
const chunks = [];
|
|
27
|
-
for await (const chunk of process.stdin) {
|
|
28
|
-
chunks.push(chunk);
|
|
29
|
-
}
|
|
30
|
-
const inputJson = Buffer.concat(chunks).toString('utf-8');
|
|
31
|
-
let hookPayload;
|
|
27
|
+
let rawInput;
|
|
32
28
|
try {
|
|
33
|
-
|
|
29
|
+
rawInput = await readJsonFromStdin();
|
|
34
30
|
}
|
|
35
31
|
catch {
|
|
36
32
|
console.error('Invalid JSON input');
|
|
37
33
|
process.exit(1);
|
|
38
34
|
}
|
|
39
35
|
// Validate required fields
|
|
40
|
-
if (!isNonEmptyString(
|
|
36
|
+
if (!isNonEmptyString(rawInput.session_id)) {
|
|
41
37
|
console.error('Invalid or missing session_id');
|
|
42
38
|
process.exit(1);
|
|
43
39
|
}
|
|
44
40
|
// Validate optional fields if present
|
|
45
|
-
if (
|
|
41
|
+
if (rawInput.cwd !== undefined && typeof rawInput.cwd !== 'string') {
|
|
46
42
|
console.error('Invalid cwd: must be a string');
|
|
47
43
|
process.exit(1);
|
|
48
44
|
}
|
|
49
|
-
if (
|
|
50
|
-
typeof hookPayload.notification_type !== 'string') {
|
|
45
|
+
if (rawInput.notification_type !== undefined && typeof rawInput.notification_type !== 'string') {
|
|
51
46
|
console.error('Invalid notification_type: must be a string');
|
|
52
47
|
process.exit(1);
|
|
53
48
|
}
|
|
49
|
+
// Get transcript_path: use provided path or build from cwd and session_id
|
|
50
|
+
const cwd = rawInput.cwd || process.cwd();
|
|
51
|
+
const transcriptPath = typeof rawInput.transcript_path === 'string'
|
|
52
|
+
? rawInput.transcript_path
|
|
53
|
+
: buildTranscriptPath(cwd, rawInput.session_id);
|
|
54
54
|
const event = {
|
|
55
|
-
session_id:
|
|
56
|
-
cwd
|
|
55
|
+
session_id: rawInput.session_id,
|
|
56
|
+
cwd,
|
|
57
57
|
tty,
|
|
58
58
|
hook_event_name: eventName,
|
|
59
|
-
notification_type:
|
|
59
|
+
notification_type: rawInput.notification_type,
|
|
60
|
+
transcript_path: transcriptPath,
|
|
60
61
|
};
|
|
61
62
|
updateSession(event);
|
|
62
63
|
// Ensure data is written before process exits (hooks are short-lived processes)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface UseServerResult {
|
|
2
|
+
url: string | null;
|
|
3
|
+
qrCode: string | null;
|
|
4
|
+
port: number | null;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
error: Error | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function useServer(port?: number): UseServerResult;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=useServer.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { createMobileServer } from '../server/index.js';
|
|
3
|
+
const DEFAULT_PORT = 3456;
|
|
4
|
+
export function useServer(port = DEFAULT_PORT) {
|
|
5
|
+
const [url, setUrl] = useState(null);
|
|
6
|
+
const [qrCode, setQrCode] = useState(null);
|
|
7
|
+
const [actualPort, setActualPort] = useState(null);
|
|
8
|
+
const [loading, setLoading] = useState(true);
|
|
9
|
+
const [error, setError] = useState(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
let serverInfo = null;
|
|
12
|
+
let isMounted = true;
|
|
13
|
+
async function startServer() {
|
|
14
|
+
try {
|
|
15
|
+
serverInfo = await createMobileServer(port);
|
|
16
|
+
if (isMounted) {
|
|
17
|
+
setUrl(serverInfo.url);
|
|
18
|
+
setQrCode(serverInfo.qrCode);
|
|
19
|
+
setActualPort(serverInfo.port);
|
|
20
|
+
setLoading(false);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (isMounted) {
|
|
25
|
+
setError(err instanceof Error ? err : new Error('Failed to start server'));
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
startServer();
|
|
31
|
+
return () => {
|
|
32
|
+
isMounted = false;
|
|
33
|
+
if (serverInfo) {
|
|
34
|
+
serverInfo.stop();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}, [port]);
|
|
38
|
+
return { url, qrCode, port: actualPort, loading, error };
|
|
39
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { clearSessions, getSession, getSessions, getStorePath, } from './store/file-store.js';
|
|
2
2
|
export type { HookEvent, HookEventName, Session, SessionStatus, StoreData, } from './types/index.js';
|
|
3
3
|
export { focusSession, getSupportedTerminals, isMacOS } from './utils/focus.js';
|
|
4
|
+
export { sendTextToTerminal } from './utils/send-text.js';
|
|
4
5
|
export { getStatusDisplay } from './utils/status.js';
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EACb,UAAU,EACV,WAAW,EACX,YAAY,GACb,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,SAAS,EACT,aAAa,EACb,OAAO,EACP,aAAa,EACb,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EACb,UAAU,EACV,WAAW,EACX,YAAY,GACb,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,SAAS,EACT,aAAa,EACb,OAAO,EACP,aAAa,EACb,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
// Store functions
|
|
3
3
|
export { clearSessions, getSession, getSessions, getStorePath, } from './store/file-store.js';
|
|
4
4
|
export { focusSession, getSupportedTerminals, isMacOS } from './utils/focus.js';
|
|
5
|
+
export { sendTextToTerminal } from './utils/send-text.js';
|
|
5
6
|
// Utilities
|
|
6
7
|
export { getStatusDisplay } from './utils/status.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ServerInfo {
|
|
2
|
+
url: string;
|
|
3
|
+
qrCode: string;
|
|
4
|
+
token: string;
|
|
5
|
+
port: number;
|
|
6
|
+
stop: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function getLocalIP(): string;
|
|
9
|
+
export declare function generateQRCode(text: string): Promise<string>;
|
|
10
|
+
export declare function createMobileServer(port?: number): Promise<ServerInfo>;
|
|
11
|
+
export declare function startServer(port?: number): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { createServer as createNetServer } from 'node:net';
|
|
5
|
+
import { networkInterfaces } from 'node:os';
|
|
6
|
+
import { dirname, normalize, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import chokidar from 'chokidar';
|
|
9
|
+
import qrcode from 'qrcode-terminal';
|
|
10
|
+
import { WebSocketServer } from 'ws';
|
|
11
|
+
import { clearSessions, getSessions, getStorePath } from '../store/file-store.js';
|
|
12
|
+
import { focusSession } from '../utils/focus.js';
|
|
13
|
+
import { sendKeystrokeToTerminal, sendTextToTerminal } from '../utils/send-text.js';
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const DEFAULT_PORT = 3456;
|
|
16
|
+
const MAX_PORT_ATTEMPTS = 10;
|
|
17
|
+
/**
|
|
18
|
+
* Check if a port is available.
|
|
19
|
+
*/
|
|
20
|
+
function isPortAvailable(port) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const server = createNetServer();
|
|
23
|
+
server.once('error', () => {
|
|
24
|
+
resolve(false);
|
|
25
|
+
});
|
|
26
|
+
server.once('listening', () => {
|
|
27
|
+
server.close(() => {
|
|
28
|
+
resolve(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
server.listen(port, '0.0.0.0');
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Find an available port starting from the given port.
|
|
36
|
+
* Tries up to MAX_PORT_ATTEMPTS ports.
|
|
37
|
+
*/
|
|
38
|
+
async function findAvailablePort(startPort) {
|
|
39
|
+
for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {
|
|
40
|
+
const port = startPort + i;
|
|
41
|
+
if (await isPortAvailable(port)) {
|
|
42
|
+
return port;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`No available port found in range ${startPort}-${startPort + MAX_PORT_ATTEMPTS - 1}`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Generate a random authentication token.
|
|
49
|
+
*/
|
|
50
|
+
function generateAuthToken() {
|
|
51
|
+
return randomBytes(32).toString('hex');
|
|
52
|
+
}
|
|
53
|
+
// WebSocket.OPEN constant (avoid magic number)
|
|
54
|
+
const WEBSOCKET_OPEN = 1;
|
|
55
|
+
/**
|
|
56
|
+
* Find a session by session ID.
|
|
57
|
+
*/
|
|
58
|
+
function findSessionById(sessionId) {
|
|
59
|
+
const sessions = getSessions();
|
|
60
|
+
return sessions.find((s) => s.session_id === sessionId);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Handle focus command from WebSocket client.
|
|
64
|
+
*/
|
|
65
|
+
function handleFocusCommand(ws, sessionId) {
|
|
66
|
+
const session = findSessionById(sessionId);
|
|
67
|
+
if (!session?.tty) {
|
|
68
|
+
ws.send(JSON.stringify({
|
|
69
|
+
type: 'focusResult',
|
|
70
|
+
success: false,
|
|
71
|
+
error: 'Session not found or no TTY',
|
|
72
|
+
}));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const success = focusSession(session.tty);
|
|
76
|
+
ws.send(JSON.stringify({ type: 'focusResult', success }));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Handle sendText command from WebSocket client.
|
|
80
|
+
*/
|
|
81
|
+
function handleSendTextCommand(ws, sessionId, text) {
|
|
82
|
+
const session = findSessionById(sessionId);
|
|
83
|
+
if (!session?.tty) {
|
|
84
|
+
ws.send(JSON.stringify({ type: 'sendTextResult', success: false, error: 'Session not found' }));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const result = sendTextToTerminal(session.tty, text);
|
|
88
|
+
ws.send(JSON.stringify({ type: 'sendTextResult', ...result }));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Handle sendKeystroke command from WebSocket client.
|
|
92
|
+
* Used for permission prompt responses (y/n/a) and Ctrl+C.
|
|
93
|
+
*/
|
|
94
|
+
function handleSendKeystrokeCommand(ws, sessionId, key, useControl = false) {
|
|
95
|
+
const session = findSessionById(sessionId);
|
|
96
|
+
if (!session?.tty) {
|
|
97
|
+
ws.send(JSON.stringify({ type: 'sendKeystrokeResult', success: false, error: 'Session not found' }));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const result = sendKeystrokeToTerminal(session.tty, key, useControl);
|
|
101
|
+
ws.send(JSON.stringify({ type: 'sendKeystrokeResult', ...result }));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Handle clearSessions command from WebSocket client.
|
|
105
|
+
*/
|
|
106
|
+
function handleClearSessionsCommand(ws) {
|
|
107
|
+
try {
|
|
108
|
+
clearSessions();
|
|
109
|
+
ws.send(JSON.stringify({ type: 'clearSessionsResult', success: true }));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
ws.send(JSON.stringify({
|
|
113
|
+
type: 'clearSessionsResult',
|
|
114
|
+
success: false,
|
|
115
|
+
error: 'Failed to clear sessions',
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Handle incoming WebSocket message from client.
|
|
121
|
+
* Processes focus, sendText, and clearSessions commands.
|
|
122
|
+
*/
|
|
123
|
+
function handleWebSocketMessage(ws, data) {
|
|
124
|
+
let message;
|
|
125
|
+
try {
|
|
126
|
+
message = JSON.parse(data.toString());
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return; // Ignore invalid messages
|
|
130
|
+
}
|
|
131
|
+
if (message.type === 'focus' && message.sessionId) {
|
|
132
|
+
handleFocusCommand(ws, message.sessionId);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (message.type === 'sendText' && message.sessionId && message.text) {
|
|
136
|
+
handleSendTextCommand(ws, message.sessionId, message.text);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (message.type === 'sendKeystroke' && message.sessionId && message.key) {
|
|
140
|
+
handleSendKeystrokeCommand(ws, message.sessionId, message.key, message.useControl ?? false);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (message.type === 'clearSessions') {
|
|
144
|
+
handleClearSessionsCommand(ws);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Broadcast message to all connected WebSocket clients.
|
|
149
|
+
*/
|
|
150
|
+
function broadcastToClients(wss, message) {
|
|
151
|
+
const data = JSON.stringify(message);
|
|
152
|
+
for (const client of wss.clients) {
|
|
153
|
+
if (client.readyState === WEBSOCKET_OPEN) {
|
|
154
|
+
client.send(data);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Send current sessions to a WebSocket client.
|
|
160
|
+
*/
|
|
161
|
+
function sendSessionsToClient(ws) {
|
|
162
|
+
const sessions = getSessions();
|
|
163
|
+
ws.send(JSON.stringify({ type: 'sessions', data: sessions }));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Setup WebSocket connection handlers.
|
|
167
|
+
*/
|
|
168
|
+
function setupWebSocketHandlers(wss, validToken) {
|
|
169
|
+
wss.on('connection', (ws, req) => {
|
|
170
|
+
const url = new URL(req.url || '/', `ws://${req.headers.host}`);
|
|
171
|
+
const requestToken = url.searchParams.get('token');
|
|
172
|
+
if (requestToken !== validToken) {
|
|
173
|
+
ws.close(1008, 'Unauthorized');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
sendSessionsToClient(ws);
|
|
177
|
+
ws.on('message', (data) => handleWebSocketMessage(ws, data));
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
export function getLocalIP() {
|
|
181
|
+
const interfaces = networkInterfaces();
|
|
182
|
+
const allAddresses = Object.values(interfaces)
|
|
183
|
+
.flat()
|
|
184
|
+
.filter((info) => info != null);
|
|
185
|
+
const externalIPv4 = allAddresses.find((info) => info.family === 'IPv4' && !info.internal);
|
|
186
|
+
return externalIPv4?.address ?? 'localhost';
|
|
187
|
+
}
|
|
188
|
+
export function generateQRCode(text) {
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
qrcode.generate(text, { small: true }, (qrCode) => {
|
|
191
|
+
resolve(qrCode);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function getContentType(path) {
|
|
196
|
+
if (path.endsWith('.html'))
|
|
197
|
+
return 'text/html';
|
|
198
|
+
if (path.endsWith('.css'))
|
|
199
|
+
return 'text/css';
|
|
200
|
+
if (path.endsWith('.js'))
|
|
201
|
+
return 'application/javascript';
|
|
202
|
+
return 'text/plain';
|
|
203
|
+
}
|
|
204
|
+
function serveStatic(req, res, validToken) {
|
|
205
|
+
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
206
|
+
const requestToken = url.searchParams.get('token');
|
|
207
|
+
if (requestToken !== validToken) {
|
|
208
|
+
res.writeHead(401, { 'Content-Type': 'text/plain' });
|
|
209
|
+
res.end('Unauthorized - Invalid or missing token');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const publicDir = resolve(__dirname, '../../public');
|
|
213
|
+
const filePath = url.pathname === '/' ? '/index.html' : url.pathname;
|
|
214
|
+
// Prevent directory traversal
|
|
215
|
+
// Remove leading slashes and normalize to prevent absolute path injection
|
|
216
|
+
const safePath = normalize(filePath)
|
|
217
|
+
.replace(/^(\.\.(\/|\\|$))+/, '')
|
|
218
|
+
.replace(/^\/+/, '');
|
|
219
|
+
const fullPath = resolve(publicDir, safePath);
|
|
220
|
+
// Verify the resolved path is within publicDir
|
|
221
|
+
if (!fullPath.startsWith(publicDir)) {
|
|
222
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
223
|
+
res.end('Forbidden');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
228
|
+
res.writeHead(200, {
|
|
229
|
+
'Content-Type': getContentType(filePath),
|
|
230
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
231
|
+
});
|
|
232
|
+
res.end(content);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
res.writeHead(404);
|
|
236
|
+
res.end('Not Found');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Create server components (HTTP server, WebSocket server, file watcher).
|
|
241
|
+
* Shared by createMobileServer and startServer.
|
|
242
|
+
*/
|
|
243
|
+
function createServerComponents(token) {
|
|
244
|
+
const server = createServer((req, res) => serveStatic(req, res, token));
|
|
245
|
+
const wss = new WebSocketServer({ server });
|
|
246
|
+
setupWebSocketHandlers(wss, token);
|
|
247
|
+
const storePath = getStorePath();
|
|
248
|
+
const watcher = chokidar.watch(storePath, {
|
|
249
|
+
ignoreInitial: true,
|
|
250
|
+
awaitWriteFinish: {
|
|
251
|
+
stabilityThreshold: 100,
|
|
252
|
+
pollInterval: 50,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
watcher.on('change', () => {
|
|
256
|
+
const sessions = getSessions();
|
|
257
|
+
broadcastToClients(wss, { type: 'sessions', data: sessions });
|
|
258
|
+
});
|
|
259
|
+
return { server, wss, watcher };
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Stop all server components.
|
|
263
|
+
*/
|
|
264
|
+
function stopServerComponents({ watcher, wss, server }) {
|
|
265
|
+
watcher.close();
|
|
266
|
+
wss.close();
|
|
267
|
+
server.close();
|
|
268
|
+
}
|
|
269
|
+
export async function createMobileServer(port = DEFAULT_PORT) {
|
|
270
|
+
const actualPort = await findAvailablePort(port);
|
|
271
|
+
const localIP = getLocalIP();
|
|
272
|
+
const token = generateAuthToken();
|
|
273
|
+
const url = `http://${localIP}:${actualPort}?token=${token}`;
|
|
274
|
+
const qrCode = await generateQRCode(url);
|
|
275
|
+
const components = createServerComponents(token);
|
|
276
|
+
await new Promise((resolve) => {
|
|
277
|
+
components.server.listen(actualPort, '0.0.0.0', resolve);
|
|
278
|
+
});
|
|
279
|
+
return {
|
|
280
|
+
url,
|
|
281
|
+
qrCode,
|
|
282
|
+
token,
|
|
283
|
+
port: actualPort,
|
|
284
|
+
stop: () => stopServerComponents(components),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// CLI standalone mode
|
|
288
|
+
export async function startServer(port = DEFAULT_PORT) {
|
|
289
|
+
const actualPort = await findAvailablePort(port);
|
|
290
|
+
const localIP = getLocalIP();
|
|
291
|
+
const token = generateAuthToken();
|
|
292
|
+
const url = `http://${localIP}:${actualPort}?token=${token}`;
|
|
293
|
+
const components = createServerComponents(token);
|
|
294
|
+
components.server.listen(actualPort, '0.0.0.0', () => {
|
|
295
|
+
console.log('\n Claude Code Monitor - Mobile Web Interface\n');
|
|
296
|
+
console.log(` Server running at: ${url}\n`);
|
|
297
|
+
if (actualPort !== port) {
|
|
298
|
+
console.log(` (Port ${port} was in use, using ${actualPort} instead)\n`);
|
|
299
|
+
}
|
|
300
|
+
console.log(' Scan this QR code with your phone:\n');
|
|
301
|
+
qrcode.generate(url, { small: true });
|
|
302
|
+
console.log('\n Press Ctrl+C to stop the server.\n');
|
|
303
|
+
});
|
|
304
|
+
process.on('SIGINT', () => {
|
|
305
|
+
console.log('\n Shutting down...');
|
|
306
|
+
stopServerComponents(components);
|
|
307
|
+
process.exit(0);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { HookEvent, Session, SessionStatus, StoreData } from '../types/index.js';
|
|
2
2
|
export { isTtyAlive } from '../utils/tty-cache.js';
|
|
3
|
+
export interface Settings {
|
|
4
|
+
qrCodeVisible: boolean;
|
|
5
|
+
}
|
|
3
6
|
export declare function readStore(): StoreData;
|
|
4
7
|
export declare function writeStore(data: StoreData): void;
|
|
5
8
|
/** Immediately flush any pending writes (useful for testing and cleanup) */
|
|
@@ -18,4 +21,6 @@ export declare function getSession(sessionId: string, tty?: string): Session | u
|
|
|
18
21
|
export declare function removeSession(sessionId: string, tty?: string): void;
|
|
19
22
|
export declare function clearSessions(): void;
|
|
20
23
|
export declare function getStorePath(): string;
|
|
24
|
+
export declare function readSettings(): Settings;
|
|
25
|
+
export declare function writeSettings(settings: Settings): void;
|
|
21
26
|
//# sourceMappingURL=file-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../../src/store/file-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../../src/store/file-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAKtF,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAMnD,MAAM,WAAW,QAAQ;IACvB,aAAa,EAAE,OAAO,CAAC;CACxB;AAuBD,wBAAgB,SAAS,IAAI,SAAS,CAerC;AAoBD,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAQhD;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,IAAI,IAAI,CAKzC;AAED,qDAAqD;AACrD,wBAAgB,eAAe,IAAI,IAAI,CAMtC;AAED,gBAAgB;AAChB,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,gBAAgB;AAChB,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,GACV,IAAI,CAMN;AAED,gBAAgB;AAChB,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,aAAa,CAAC,EAAE,aAAa,GAAG,aAAa,CA8B9F;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAiCvD;AAED,wBAAgB,WAAW,IAAI,OAAO,EAAE,CAqBvC;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAI/E;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAKnE;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wBAAgB,YAAY,IAAI,QAAQ,CAYvC;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAUtD"}
|
package/dist/store/file-store.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { WRITE_DEBOUNCE_MS } from '../constants.js';
|
|
5
|
+
import { getLastAssistantMessage } from '../utils/transcript.js';
|
|
5
6
|
import { isTtyAlive } from '../utils/tty-cache.js';
|
|
6
7
|
// Re-export for backward compatibility
|
|
7
8
|
export { isTtyAlive } from '../utils/tty-cache.js';
|
|
8
9
|
const STORE_DIR = join(homedir(), '.claude-monitor');
|
|
9
10
|
const STORE_FILE = join(STORE_DIR, 'sessions.json');
|
|
11
|
+
const SETTINGS_FILE = join(STORE_DIR, 'settings.json');
|
|
12
|
+
const DEFAULT_SETTINGS = {
|
|
13
|
+
qrCodeVisible: false,
|
|
14
|
+
};
|
|
10
15
|
// In-memory cache for batched writes
|
|
11
16
|
let cachedStore = null;
|
|
12
17
|
let writeTimer = null;
|
|
@@ -22,7 +27,6 @@ function getEmptyStoreData() {
|
|
|
22
27
|
};
|
|
23
28
|
}
|
|
24
29
|
export function readStore() {
|
|
25
|
-
// Return cached data if available (for batched writes consistency)
|
|
26
30
|
if (cachedStore) {
|
|
27
31
|
return cachedStore;
|
|
28
32
|
}
|
|
@@ -129,6 +133,11 @@ export function updateSession(event) {
|
|
|
129
133
|
removeOldSessionsOnSameTty(store.sessions, event.session_id, event.tty);
|
|
130
134
|
}
|
|
131
135
|
const existing = store.sessions[key];
|
|
136
|
+
// Get latest assistant message from transcript
|
|
137
|
+
const assistantMessage = event.transcript_path
|
|
138
|
+
? getLastAssistantMessage(event.transcript_path)
|
|
139
|
+
: undefined;
|
|
140
|
+
const lastMessage = assistantMessage ?? existing?.lastMessage;
|
|
132
141
|
const session = {
|
|
133
142
|
session_id: event.session_id,
|
|
134
143
|
cwd: event.cwd,
|
|
@@ -136,6 +145,7 @@ export function updateSession(event) {
|
|
|
136
145
|
status: determineStatus(event, existing?.status),
|
|
137
146
|
created_at: existing?.created_at ?? now,
|
|
138
147
|
updated_at: now,
|
|
148
|
+
lastMessage,
|
|
139
149
|
};
|
|
140
150
|
store.sessions[key] = session;
|
|
141
151
|
writeStore(store);
|
|
@@ -143,14 +153,11 @@ export function updateSession(event) {
|
|
|
143
153
|
}
|
|
144
154
|
export function getSessions() {
|
|
145
155
|
const store = readStore();
|
|
146
|
-
const now = Date.now();
|
|
147
156
|
let hasChanges = false;
|
|
148
157
|
for (const [key, session] of Object.entries(store.sessions)) {
|
|
149
|
-
const lastUpdateMs = new Date(session.updated_at).getTime();
|
|
150
|
-
const isSessionActive = now - lastUpdateMs <= SESSION_TIMEOUT_MS;
|
|
151
158
|
const isTtyStillAlive = isTtyAlive(session.tty);
|
|
152
|
-
|
|
153
|
-
if (
|
|
159
|
+
// Only remove sessions when TTY no longer exists
|
|
160
|
+
if (!isTtyStillAlive) {
|
|
154
161
|
delete store.sessions[key];
|
|
155
162
|
hasChanges = true;
|
|
156
163
|
}
|
|
@@ -177,3 +184,29 @@ export function clearSessions() {
|
|
|
177
184
|
export function getStorePath() {
|
|
178
185
|
return STORE_FILE;
|
|
179
186
|
}
|
|
187
|
+
export function readSettings() {
|
|
188
|
+
ensureStoreDir();
|
|
189
|
+
if (!existsSync(SETTINGS_FILE)) {
|
|
190
|
+
return DEFAULT_SETTINGS;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const content = readFileSync(SETTINGS_FILE, 'utf-8');
|
|
194
|
+
const parsed = JSON.parse(content);
|
|
195
|
+
return { ...DEFAULT_SETTINGS, ...parsed };
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return DEFAULT_SETTINGS;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export function writeSettings(settings) {
|
|
202
|
+
ensureStoreDir();
|
|
203
|
+
try {
|
|
204
|
+
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), {
|
|
205
|
+
encoding: 'utf-8',
|
|
206
|
+
mode: 0o600,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Silently ignore write errors
|
|
211
|
+
}
|
|
212
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface HookEvent {
|
|
|
5
5
|
tty?: string;
|
|
6
6
|
hook_event_name: HookEventName;
|
|
7
7
|
notification_type?: string;
|
|
8
|
+
transcript_path?: string;
|
|
8
9
|
}
|
|
9
10
|
export type SessionStatus = 'running' | 'waiting_input' | 'stopped';
|
|
10
11
|
export interface Session {
|
|
@@ -14,6 +15,7 @@ export interface Session {
|
|
|
14
15
|
status: SessionStatus;
|
|
15
16
|
created_at: string;
|
|
16
17
|
updated_at: string;
|
|
18
|
+
lastMessage?: string;
|
|
17
19
|
}
|
|
18
20
|
export interface StoreData {
|
|
19
21
|
sessions: Record<string, Session>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,aAAa,GACrB,YAAY,GACZ,aAAa,GACb,cAAc,GACd,MAAM,GACN,kBAAkB,CAAC;AAGvB,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,aAAa,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,aAAa,GACrB,YAAY,GACZ,aAAa,GACb,cAAc,GACd,MAAM,GACN,kBAAkB,CAAC;AAGvB,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,aAAa,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAGD,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,CAAC;AAGpE,MAAM,WAAW,OAAO;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAGD,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;CACpB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execute an AppleScript and return whether it succeeded.
|
|
3
|
+
* @param script - AppleScript code to execute
|
|
4
|
+
* @returns true if the script returned "true", false otherwise
|
|
5
|
+
*/
|
|
6
|
+
export declare function executeAppleScript(script: string): boolean;
|
|
7
|
+
//# sourceMappingURL=applescript.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applescript.d.ts","sourceRoot":"","sources":["../../src/utils/applescript.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAU1D"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Execute an AppleScript and return whether it succeeded.
|
|
4
|
+
* @param script - AppleScript code to execute
|
|
5
|
+
* @returns true if the script returned "true", false otherwise
|
|
6
|
+
*/
|
|
7
|
+
export function executeAppleScript(script) {
|
|
8
|
+
try {
|
|
9
|
+
const result = execFileSync('osascript', ['-e', script], {
|
|
10
|
+
encoding: 'utf-8',
|
|
11
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
12
|
+
}).trim();
|
|
13
|
+
return result === 'true';
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/utils/focus.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../../src/utils/focus.ts"],"names":[],"mappings":"
|
|
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"}
|