@mostrom/meeting-detector 1.0.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/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # Meeting Detector
2
+
3
+ Real-time meeting detection for macOS desktop apps using TCC (Transparency, Consent, and Control) logs.
4
+
5
+ ## Features
6
+
7
+ - 🎯 **App-agnostic**: Works with Zoom, Slack, Teams, Chrome, and any desktop meeting app
8
+ - ⚡ **Real-time**: Detects meeting start/stop events as they happen
9
+ - 🔍 **Process attribution**: Identifies which app is using camera/microphone with PID
10
+ - 🎛️ **Event-driven**: Clean Node.js API with TypeScript support
11
+ - 🚫 **Smart deduplication**: Prevents spam from multi-process apps like Teams
12
+ - 📱 **Front app correlation**: Shows which app is currently in focus
13
+
14
+ ## Installation
15
+
16
+ ### From npm (when published)
17
+ ```bash
18
+ npm install @mostrom/meeting-detector
19
+ ```
20
+
21
+ ### Local development
22
+ ```bash
23
+ git clone <repo-url>
24
+ cd meeting-detector
25
+ npm install
26
+ npm run build
27
+ ```
28
+
29
+ ### As a local dependency
30
+ ```bash
31
+ # In your project's package.json
32
+ {
33
+ "dependencies": {
34
+ "@mostrom/meeting-detector": "file:../path/to/meeting-detector"
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### Using npm link
40
+ ```bash
41
+ # In meeting-detector directory
42
+ npm link
43
+
44
+ # In your project directory
45
+ npm link @mostrom/meeting-detector
46
+ ```
47
+
48
+ ## Quick Start
49
+
50
+ ### Simple API
51
+ ```typescript
52
+ import { detector } from '@mostrom/meeting-detector';
53
+ import type { MeetingSignal } from '@mostrom/meeting-detector';
54
+
55
+ const meetingDetector = detector((signal: MeetingSignal) => {
56
+ console.log('Meeting event:', signal);
57
+ }, { debug: true });
58
+
59
+ // Graceful shutdown
60
+ process.on('SIGINT', () => {
61
+ meetingDetector.stop();
62
+ process.exit(0);
63
+ });
64
+ ```
65
+
66
+ ### Class API
67
+ ```typescript
68
+ import { MeetingDetector } from '@mostrom/meeting-detector';
69
+ import type { MeetingSignal } from '@mostrom/meeting-detector';
70
+
71
+ const detector = new MeetingDetector({ debug: true });
72
+
73
+ detector.onMeeting((signal: MeetingSignal) => {
74
+ console.log(`${signal.process} is using ${signal.service}`);
75
+ if (signal.verdict === 'requested') {
76
+ console.log('🔴 Meeting started');
77
+ }
78
+ });
79
+
80
+ detector.onError((error) => {
81
+ console.error('Detection error:', error);
82
+ });
83
+
84
+ detector.start();
85
+
86
+ // Later...
87
+ detector.stop();
88
+ ```
89
+
90
+ ## API Reference
91
+
92
+ ### `detector(callback, options?)`
93
+ Convenience function for simple usage.
94
+
95
+ ### `MeetingDetector` Class
96
+
97
+ #### Constructor
98
+ ```typescript
99
+ new MeetingDetector(options?: MeetingDetectorOptions)
100
+ ```
101
+
102
+ #### Methods
103
+ - `start(callback?)` - Start monitoring
104
+ - `stop()` - Stop monitoring
105
+ - `isRunning()` - Check if running
106
+ - `onMeeting(callback)` - Add meeting event listener
107
+ - `onError(callback)` - Add error event listener
108
+
109
+ #### Events
110
+ - `meeting` - Emitted when meeting state changes
111
+ - `error` - Emitted on errors
112
+ - `exit` - Emitted when process exits
113
+
114
+ ## Example Output
115
+
116
+ ```json
117
+ {
118
+ "event": "meeting_signal",
119
+ "timestamp": "2025-09-01T14:30:29Z",
120
+ "service": "microphone",
121
+ "verdict": "requested",
122
+ "process": "Microsoft Teams WebView Helper",
123
+ "pid": "7390",
124
+ "front_app": "MSTeams",
125
+ "camera_active": true
126
+ }
127
+ ```
128
+
129
+ ## TypeScript Types
130
+
131
+ ```typescript
132
+ interface MeetingSignal {
133
+ event: 'meeting_signal';
134
+ timestamp: string;
135
+ service: 'microphone' | 'camera' | '';
136
+ verdict: 'requested' | 'allowed' | 'denied' | '';
137
+ process: string;
138
+ pid: string;
139
+ front_app: string;
140
+ camera_active: boolean;
141
+ }
142
+
143
+ interface MeetingDetectorOptions {
144
+ scriptPath?: string; // Path to bash script (default: './meeting-detect.sh')
145
+ debug?: boolean; // Enable debug logging (default: false)
146
+ }
147
+ ```
148
+
149
+ ## Development
150
+
151
+ ```bash
152
+ # Run in development mode
153
+ npm run dev
154
+
155
+ # Build TypeScript
156
+ npm run build
157
+
158
+ # Watch for changes
159
+ npm run watch
160
+
161
+ # Prepare for publishing
162
+ npm run prepublishOnly
163
+ ```
164
+
165
+ ## Requirements
166
+
167
+ - macOS 10.14+ (uses TCC privacy logs)
168
+ - Node.js 14.0+
169
+ - TypeScript 4.5+ (for development)
170
+
171
+ ## How It Works
172
+
173
+ The detector runs a bash script that monitors macOS TCC (privacy) logs for microphone and camera access events. It uses:
174
+
175
+ 1. **TCC Log Streaming** - Monitors `com.apple.TCC` subsystem logs
176
+ 2. **Process Attribution** - Extracts PIDs from `target_token` fields
177
+ 3. **Smart Deduplication** - Prevents spam from multi-process apps
178
+ 4. **State Tracking** - Only emits when meaningful changes occur
179
+
180
+ ## License
181
+
182
+ MIT
@@ -0,0 +1,56 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { MeetingDetectorOptions, MeetingEventCallback, ErrorEventCallback } from './types.js';
3
+ export declare class MeetingDetector extends EventEmitter {
4
+ private process?;
5
+ private options;
6
+ private activeSessions;
7
+ constructor(options?: MeetingDetectorOptions);
8
+ /**
9
+ * Start monitoring for meeting signals
10
+ * @param callback Optional callback function for meeting events
11
+ */
12
+ start(callback?: MeetingEventCallback): void;
13
+ /**
14
+ * Stop monitoring
15
+ */
16
+ stop(): void;
17
+ /**
18
+ * Check if the detector is currently running
19
+ */
20
+ isRunning(): boolean;
21
+ /**
22
+ * Add a meeting event listener
23
+ */
24
+ onMeeting(callback: MeetingEventCallback): void;
25
+ /**
26
+ * Add an error event listener
27
+ */
28
+ onError(callback: ErrorEventCallback): void;
29
+ /**
30
+ * Comprehensive filtering to prevent false positives
31
+ * Filters out:
32
+ * - System processes (WebKit, SiriNCService, Chrome Helper, etc.)
33
+ * - Development tools (Electron, Terminal, Xcode)
34
+ * - Generic browser services (Chrome, Safari, Firefox)
35
+ * - Browser camera initialization (no window title + 'requested' verdict)
36
+ * - Google Meet signals without valid meeting URL patterns
37
+ */
38
+ private shouldIgnoreSignal;
39
+ /**
40
+ * Generate a unique session key based on the signal properties
41
+ */
42
+ private getSessionKey;
43
+ /**
44
+ * Check if this signal is a duplicate of an existing session
45
+ * Returns true if duplicate, false if new or expired session
46
+ */
47
+ private isDuplicateSession;
48
+ /**
49
+ * Clean up sessions that have expired
50
+ */
51
+ private cleanupExpiredSessions;
52
+ private parseSignal;
53
+ private transformAppName;
54
+ }
55
+ export declare function detector(callback: MeetingEventCallback, options?: MeetingDetectorOptions): MeetingDetector;
56
+ //# sourceMappingURL=detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAAiB,sBAAsB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAO7G,qBAAa,eAAgB,SAAQ,YAAY;IAC/C,OAAO,CAAC,OAAO,CAAC,CAAe;IAC/B,OAAO,CAAC,OAAO,CAAmC;IAClD,OAAO,CAAC,cAAc,CAAuC;gBAEjD,OAAO,GAAE,sBAA2B;IAgBhD;;;OAGG;IACI,KAAK,CAAC,QAAQ,CAAC,EAAE,oBAAoB,GAAG,IAAI;IAuEnD;;OAEG;IACI,IAAI,IAAI,IAAI;IAcnB;;OAEG;IACI,SAAS,IAAI,OAAO;IAI3B;;OAEG;IACI,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAItD;;OAEG;IACI,OAAO,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI;IAIlD;;;;;;;;OAQG;IACH,OAAO,CAAC,kBAAkB;IA0E1B;;OAEG;IACH,OAAO,CAAC,aAAa;IAIrB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IA2B1B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAkB9B,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,gBAAgB;CAqDzB;AAGD,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,eAAe,CAI1G"}
@@ -0,0 +1,339 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { EventEmitter } from 'node:events';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, join } from 'node:path';
5
+ export class MeetingDetector extends EventEmitter {
6
+ constructor(options = {}) {
7
+ super();
8
+ this.activeSessions = new Map();
9
+ // Get the absolute path to the script relative to this package
10
+ const defaultScriptPath = options.scriptPath || join(dirname(fileURLToPath(import.meta.url)), '../meeting-detect.sh');
11
+ this.options = {
12
+ scriptPath: defaultScriptPath,
13
+ debug: options.debug || false,
14
+ sessionDeduplicationMs: options.sessionDeduplicationMs || 60000
15
+ };
16
+ }
17
+ /**
18
+ * Start monitoring for meeting signals
19
+ * @param callback Optional callback function for meeting events
20
+ */
21
+ start(callback) {
22
+ if (this.process) {
23
+ throw new Error('Detector is already running');
24
+ }
25
+ if (callback) {
26
+ this.on('meeting', callback);
27
+ }
28
+ this.process = spawn('sh', [this.options.scriptPath], {
29
+ stdio: ['pipe', 'pipe', 'pipe']
30
+ });
31
+ this.process.stdout?.on('data', (data) => {
32
+ const lines = data.toString().trim().split('\n');
33
+ for (const line of lines) {
34
+ if (line.trim()) {
35
+ try {
36
+ const signal = this.parseSignal(line);
37
+ if (this.shouldIgnoreSignal(signal)) {
38
+ if (this.options.debug) {
39
+ console.log('[MeetingDetector] Ignoring signal:', signal);
40
+ }
41
+ continue;
42
+ }
43
+ if (this.isDuplicateSession(signal)) {
44
+ if (this.options.debug) {
45
+ console.log('[MeetingDetector] Skipping duplicate session:', signal);
46
+ }
47
+ continue;
48
+ }
49
+ if (this.options.debug) {
50
+ console.log('[MeetingDetector] Parsed signal:', signal);
51
+ }
52
+ this.emit('meeting', signal);
53
+ }
54
+ catch (error) {
55
+ if (this.options.debug) {
56
+ console.log('[MeetingDetector] Failed to parse line:', line);
57
+ }
58
+ this.emit('error', new Error(`Failed to parse signal: ${line}`));
59
+ }
60
+ }
61
+ }
62
+ });
63
+ this.process.stderr?.on('data', (data) => {
64
+ if (this.options.debug) {
65
+ console.log('[MeetingDetector] stderr:', data.toString());
66
+ }
67
+ });
68
+ this.process.on('error', (error) => {
69
+ this.emit('error', error);
70
+ });
71
+ this.process.on('exit', (code, signal) => {
72
+ if (this.options.debug) {
73
+ console.log(`[MeetingDetector] Process exited with code ${code}, signal ${signal}`);
74
+ }
75
+ this.process = undefined;
76
+ this.emit('exit', { code, signal });
77
+ });
78
+ if (this.options.debug) {
79
+ console.log('[MeetingDetector] Started monitoring');
80
+ }
81
+ }
82
+ /**
83
+ * Stop monitoring
84
+ */
85
+ stop() {
86
+ if (this.process) {
87
+ this.process.kill('SIGTERM');
88
+ this.process = undefined;
89
+ // Clear active sessions when stopping
90
+ this.activeSessions.clear();
91
+ if (this.options.debug) {
92
+ console.log('[MeetingDetector] Stopped monitoring');
93
+ }
94
+ }
95
+ }
96
+ /**
97
+ * Check if the detector is currently running
98
+ */
99
+ isRunning() {
100
+ return !!this.process;
101
+ }
102
+ /**
103
+ * Add a meeting event listener
104
+ */
105
+ onMeeting(callback) {
106
+ this.on('meeting', callback);
107
+ }
108
+ /**
109
+ * Add an error event listener
110
+ */
111
+ onError(callback) {
112
+ this.on('error', callback);
113
+ }
114
+ /**
115
+ * Comprehensive filtering to prevent false positives
116
+ * Filters out:
117
+ * - System processes (WebKit, SiriNCService, Chrome Helper, etc.)
118
+ * - Development tools (Electron, Terminal, Xcode)
119
+ * - Generic browser services (Chrome, Safari, Firefox)
120
+ * - Browser camera initialization (no window title + 'requested' verdict)
121
+ * - Google Meet signals without valid meeting URL patterns
122
+ */
123
+ shouldIgnoreSignal(signal) {
124
+ // System processes that should never trigger meeting detection
125
+ const systemProcessPatterns = [
126
+ 'sirinc', // SiriNCService
127
+ 'afplay', // macOS audio file player
128
+ 'systemsoundserver', // System sound effects
129
+ 'wavelink', // Audio routing software
130
+ 'granola helper', // Screen recording helper
131
+ 'webkit.gpu', // WebKit GPU process
132
+ 'webkit.networking', // WebKit networking
133
+ 'chrome helper', // Chrome helper processes (too generic)
134
+ 'electron helper', // Electron helper processes
135
+ ];
136
+ // Services/apps that are too generic or development-related
137
+ const genericServices = [
138
+ 'electron',
139
+ 'terminal',
140
+ 'granola',
141
+ 'finder',
142
+ 'xcode',
143
+ 'tips',
144
+ 'google chrome', // Generic Chrome service (not a specific meeting)
145
+ 'safari', // Generic Safari service
146
+ 'firefox', // Generic Firefox service
147
+ 'microsoft edge', // Generic Edge service
148
+ ];
149
+ const processName = signal.process?.toLowerCase() || '';
150
+ const serviceName = signal.service?.toLowerCase() || '';
151
+ // Filter by process name patterns (partial match for flexibility)
152
+ if (systemProcessPatterns.some(pattern => processName.includes(pattern))) {
153
+ return true;
154
+ }
155
+ // Filter by exact generic service names
156
+ if (genericServices.includes(serviceName)) {
157
+ return true;
158
+ }
159
+ // Camera initialization filter: if verdict is 'requested' and window_title is empty,
160
+ // it's likely just camera initialization, not an actual meeting
161
+ // This applies to ALL apps to prevent false positives during camera setup
162
+ if (signal.verdict === 'requested' && (!signal.window_title || signal.window_title.trim() === '')) {
163
+ // Exception: if camera is actively being used (not just requested), allow it through
164
+ // This helps distinguish between "requesting permission" vs "actively in a call"
165
+ if (!signal.camera_active) {
166
+ return true;
167
+ }
168
+ }
169
+ // For Google Meet specifically: require window title to contain meeting URL patterns
170
+ // This prevents false positives from just opening Chrome with camera permissions
171
+ if (serviceName === 'google meet') {
172
+ const windowTitle = signal.window_title || '';
173
+ // Valid Google Meet windows should contain:
174
+ // - meet.google.com URL
175
+ // - "Meet - " prefix in title
176
+ // - Valid meeting code pattern (e.g., abc-defg-hij)
177
+ const hasValidMeetTitle = windowTitle.includes('meet.google.com') ||
178
+ windowTitle.includes('Meet - ') ||
179
+ /[a-z]{3}-[a-z]{4}-[a-z]{3}/.test(windowTitle); // Meeting code pattern
180
+ if (!hasValidMeetTitle) {
181
+ return true;
182
+ }
183
+ }
184
+ return false;
185
+ }
186
+ /**
187
+ * Generate a unique session key based on the signal properties
188
+ */
189
+ getSessionKey(signal) {
190
+ return `${signal.pid}:${signal.service}:${signal.front_app}`;
191
+ }
192
+ /**
193
+ * Check if this signal is a duplicate of an existing session
194
+ * Returns true if duplicate, false if new or expired session
195
+ */
196
+ isDuplicateSession(signal) {
197
+ const sessionKey = this.getSessionKey(signal);
198
+ const now = Date.now();
199
+ const existingSession = this.activeSessions.get(sessionKey);
200
+ // Clean up expired sessions periodically
201
+ this.cleanupExpiredSessions();
202
+ if (existingSession) {
203
+ const timeSinceLastSeen = now - existingSession.lastSeen;
204
+ if (timeSinceLastSeen < this.options.sessionDeduplicationMs) {
205
+ // Update last seen time for existing session
206
+ existingSession.lastSeen = now;
207
+ return true; // This is a duplicate
208
+ }
209
+ }
210
+ // New session or expired session - track it
211
+ this.activeSessions.set(sessionKey, {
212
+ lastSeen: now,
213
+ signal
214
+ });
215
+ return false; // This is not a duplicate
216
+ }
217
+ /**
218
+ * Clean up sessions that have expired
219
+ */
220
+ cleanupExpiredSessions() {
221
+ const now = Date.now();
222
+ const expiredKeys = [];
223
+ for (const [key, session] of this.activeSessions.entries()) {
224
+ if (now - session.lastSeen > this.options.sessionDeduplicationMs) {
225
+ expiredKeys.push(key);
226
+ }
227
+ }
228
+ for (const key of expiredKeys) {
229
+ this.activeSessions.delete(key);
230
+ if (this.options.debug) {
231
+ console.log(`[MeetingDetector] Cleaned up expired session: ${key}`);
232
+ }
233
+ }
234
+ }
235
+ parseSignal(line) {
236
+ const signal = JSON.parse(line);
237
+ // Use transformed app name as service if the original service is a system service like 'microphone' or 'camera'
238
+ const originalService = signal.service || '';
239
+ const transformedService = this.transformAppName(signal.front_app, signal.process);
240
+ const finalService = (originalService === 'microphone' || originalService === 'camera' || !originalService)
241
+ ? transformedService
242
+ : originalService;
243
+ return {
244
+ event: signal.event,
245
+ timestamp: signal.timestamp,
246
+ service: finalService,
247
+ verdict: signal.verdict || '',
248
+ process: signal.process || '',
249
+ pid: signal.pid || '',
250
+ parent_pid: signal.parent_pid || '',
251
+ process_path: signal.process_path || '',
252
+ front_app: signal.front_app || '',
253
+ window_title: signal.window_title || '',
254
+ session_id: signal.session_id || '',
255
+ camera_active: signal.camera_active === 'true' || signal.camera_active === true
256
+ };
257
+ }
258
+ transformAppName(frontApp, process) {
259
+ const app = frontApp?.toLowerCase() || '';
260
+ const proc = process?.toLowerCase() || '';
261
+ if (app.includes('slack') || proc.includes('slack')) {
262
+ return 'Slack';
263
+ }
264
+ else if (app.includes('msteams') || proc.includes('microsoft teams') || proc.includes('teams')) {
265
+ return 'Microsoft Teams';
266
+ }
267
+ else if (app.includes('zoom') || proc.includes('zoom')) {
268
+ return 'Zoom';
269
+ }
270
+ else if (app.includes('webex') || proc.includes('webex') || proc.includes('cisco webex')) {
271
+ return 'Webex';
272
+ }
273
+ else if (app.includes('google meet') || proc.includes('google meet') || proc.includes('meet.google.com') || (app.includes('chrome') && proc.includes('chrome'))) {
274
+ return 'Google Meet';
275
+ }
276
+ else if (app.includes('skype') || proc.includes('skype')) {
277
+ return 'Skype';
278
+ }
279
+ else if (app.includes('discord') || proc.includes('discord')) {
280
+ return 'Discord';
281
+ }
282
+ else if (app.includes('facetime') || proc.includes('facetime')) {
283
+ return 'FaceTime';
284
+ }
285
+ else if (app.includes('gotomeeting') || proc.includes('gotomeeting') || proc.includes('goto meeting')) {
286
+ return 'GoToMeeting';
287
+ }
288
+ else if (app.includes('bluejeans') || proc.includes('bluejeans') || proc.includes('blue jeans')) {
289
+ return 'BlueJeans';
290
+ }
291
+ else if (app.includes('jitsi') || proc.includes('jitsi')) {
292
+ return 'Jitsi Meet';
293
+ }
294
+ else if (app.includes('whereby') || proc.includes('whereby')) {
295
+ return 'Whereby';
296
+ }
297
+ else if (app.includes('8x8') || proc.includes('8x8')) {
298
+ return '8x8';
299
+ }
300
+ else if (app.includes('ringcentral') || proc.includes('ringcentral') || proc.includes('ring central')) {
301
+ return 'RingCentral';
302
+ }
303
+ else if (app.includes('bigbluebutton') || proc.includes('bigbluebutton') || proc.includes('big blue button')) {
304
+ return 'BigBlueButton';
305
+ }
306
+ else if (app.includes('chime') || proc.includes('chime') || proc.includes('amazon chime')) {
307
+ return 'Amazon Chime';
308
+ }
309
+ else if (app.includes('hangouts') || proc.includes('hangouts') || proc.includes('google hangouts')) {
310
+ return 'Google Hangouts';
311
+ }
312
+ else if (app.includes('adobe connect') || proc.includes('adobe connect')) {
313
+ return 'Adobe Connect';
314
+ }
315
+ else if (app.includes('teamviewer') || proc.includes('teamviewer')) {
316
+ return 'TeamViewer';
317
+ }
318
+ else if (app.includes('anydesk') || proc.includes('anydesk')) {
319
+ return 'AnyDesk';
320
+ }
321
+ else if (app.includes('clickmeeting') || proc.includes('clickmeeting')) {
322
+ return 'ClickMeeting';
323
+ }
324
+ else if (app.includes('appear.in') || proc.includes('appear.in')) {
325
+ return 'Appear.in';
326
+ }
327
+ else {
328
+ // Fallback to front_app if no match
329
+ return frontApp || 'Meeting App';
330
+ }
331
+ }
332
+ }
333
+ // Convenience function for simple usage
334
+ export function detector(callback, options) {
335
+ const detector = new MeetingDetector(options);
336
+ detector.start(callback);
337
+ return detector;
338
+ }
339
+ //# sourceMappingURL=detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector.js","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { MeetingDetector, detector } from './detector.js';
2
+ export * from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC1D,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ import { detector } from './detector.js';
2
+ // Main exports
3
+ export { MeetingDetector, detector } from './detector.js';
4
+ export * from './types.js';
5
+ // Example usage (only when run directly)
6
+ if (import.meta.url === `file://${process.argv[1]}`) {
7
+ console.log('🔍 Starting meeting detector...');
8
+ const meetingDetector = detector((stateChange) => {
9
+ console.log('📱 Meeting signal:', stateChange);
10
+ }, { debug: true });
11
+ // Graceful shutdown
12
+ process.on('SIGINT', () => {
13
+ console.log('\n⏹️ Stopping meeting detector...');
14
+ meetingDetector.stop();
15
+ process.exit(0);
16
+ });
17
+ process.on('SIGTERM', () => {
18
+ meetingDetector.stop();
19
+ process.exit(0);
20
+ });
21
+ }
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,eAAe;AACf,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC1D,cAAc,YAAY,CAAC;AAE3B,yCAAyC;AACzC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,UAAU,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACpD,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;IAE/C,MAAM,eAAe,GAAG,QAAQ,CAAC,CAAC,WAA0B,EAAE,EAAE;QAC9D,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAC;IACjD,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAEpB,oBAAoB;IACpB,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;QAClD,eAAe,CAAC,IAAI,EAAE,CAAC;QACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,eAAe,CAAC,IAAI,EAAE,CAAC;QACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,39 @@
1
+ export interface MeetingSignal {
2
+ event: 'meeting_signal';
3
+ timestamp: string;
4
+ service: 'microphone' | 'camera' | '';
5
+ verdict: 'requested' | 'allowed' | 'denied' | '';
6
+ process: string;
7
+ pid: string;
8
+ parent_pid: string;
9
+ process_path: string;
10
+ front_app: string;
11
+ window_title: string;
12
+ session_id: string;
13
+ camera_active: boolean;
14
+ }
15
+ export interface ProcessExit {
16
+ code: number | null;
17
+ signal: NodeJS.Signals | null;
18
+ }
19
+ export interface MeetingDetectorOptions {
20
+ /**
21
+ * Path to the meeting-detect.sh script
22
+ * @default './meeting-detect.sh'
23
+ */
24
+ scriptPath?: string;
25
+ /**
26
+ * Enable debug logging
27
+ * @default false
28
+ */
29
+ debug?: boolean;
30
+ /**
31
+ * Session deduplication window in milliseconds
32
+ * Signals from the same session within this window will be ignored
33
+ * @default 60000 (60 seconds)
34
+ */
35
+ sessionDeduplicationMs?: number;
36
+ }
37
+ export type MeetingEventCallback = (signal: MeetingSignal) => void;
38
+ export type ErrorEventCallback = (error: Error) => void;
39
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,gBAAgB,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,YAAY,GAAG,QAAQ,GAAG,EAAE,CAAC;IACtC,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,EAAE,CAAC;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;AACnE,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,211 @@
1
+ # detect-meeting.sh
2
+ # Purpose: Heuristically detect when *any* desktop meeting app starts a meeting by
3
+ # watching for microphone/camera access grants in macOS Unified Logs (TCC)
4
+ # and correlating with the frontmost app at that moment. Prints compact JSON lines.
5
+ #
6
+ # Notes:
7
+ # - This is app-agnostic: Zoom, Slack Huddles, Meet (via Chrome), Teams, Webex, etc.
8
+ # - Triggers on first-time (or resumed) mic/camera usage events emitted by TCC.
9
+ # - Requires no special entitlements; may miss events if the app had continuous access
10
+ # without re-requesting. That’s why we also add a lightweight camera process probe.
11
+ #
12
+ # Usage: bash detect-meeting.sh
13
+ # Stop : Ctrl+C
14
+
15
+ set -euo pipefail
16
+
17
+ # --- util: print JSON safely ---
18
+ json() {
19
+ # args: key=value ...
20
+ # converts to {"key":"value", ...} with basic escaping
21
+ local kv out="{" first=1
22
+ for kv in "$@"; do
23
+ key="${kv%%=*}"; val="${kv#*=}"
24
+ # escape quotes and backslashes
25
+ val="${val//\\/\\\\}"; val="${val//\"/\\\"}"
26
+ if [[ $first -eq 0 ]]; then out+=", "; fi
27
+ out+="\"$key\":\"$val\""; first=0
28
+ done
29
+ out+="}"
30
+ printf '%s\n' "$out"
31
+ }
32
+
33
+ # --- util: get frontmost app name (best-effort) ---
34
+ front_app() {
35
+ /usr/bin/osascript -e 'tell application "System Events" to get name of first process whose frontmost is true' 2>/dev/null || echo ""
36
+ }
37
+
38
+ # --- util: get active window title ---
39
+ window_title() {
40
+ /usr/bin/osascript -e 'tell application "System Events" to get title of front window of first process whose frontmost is true' 2>/dev/null || echo ""
41
+ }
42
+
43
+ # --- util: get parent PID ---
44
+ parent_pid() {
45
+ local pid="$1"
46
+ if [[ -n "$pid" ]]; then
47
+ ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ' || echo ""
48
+ else
49
+ echo ""
50
+ fi
51
+ }
52
+
53
+ # --- util: get process path ---
54
+ process_path() {
55
+ local pid="$1"
56
+ if [[ -n "$pid" ]]; then
57
+ ps -o command= -p "$pid" 2>/dev/null | awk '{print $1}' || echo ""
58
+ else
59
+ echo ""
60
+ fi
61
+ }
62
+
63
+ # --- util: get session ID ---
64
+ session_id() {
65
+ who -m 2>/dev/null | awk '{print $2}' | head -1 || echo ""
66
+ }
67
+
68
+ # --- util: quick camera-in-use heuristic (VDCAssistant/AppleCameraAssistant presence) ---
69
+ camera_active() {
70
+ if pgrep -xq "VDCAssistant" || pgrep -xq "AppleCameraAssistant"; then
71
+ echo "true"
72
+ else
73
+ echo "false"
74
+ fi
75
+ }
76
+
77
+ # --- util: normalize app names to main app (reduces Teams/Chrome helper noise) ---
78
+ normalize_app() {
79
+ local process_name="$1"
80
+
81
+ # Microsoft Teams - all helpers normalize to "Microsoft Teams"
82
+ if [[ "$process_name" == *"Microsoft Teams"* ]]; then
83
+ echo "Microsoft Teams"
84
+ # Google Chrome - all helpers normalize to "Google Chrome"
85
+ elif [[ "$process_name" == *"Google Chrome"* ]] || [[ "$process_name" == *"Chrome Helper"* ]]; then
86
+ echo "Google Chrome"
87
+ # Slack - all helpers normalize to "Slack"
88
+ elif [[ "$process_name" == *"Slack"* ]]; then
89
+ echo "Slack"
90
+ # Default - return as-is
91
+ else
92
+ echo "$process_name"
93
+ fi
94
+ }
95
+
96
+ # --- state tracking variables to prevent duplicate logs ---
97
+ prev_camera_active=""
98
+ prev_front_app=""
99
+ prev_service=""
100
+ prev_verdict=""
101
+ prev_process=""
102
+ prev_pid=""
103
+ prev_main_app=""
104
+ last_log_time=0
105
+
106
+ # --- multi-line TCC parsing state ---
107
+ current_svc=""
108
+ current_pid=""
109
+ current_app=""
110
+ current_verdict=""
111
+
112
+ # --- stream TCC (privacy) log events for mic/camera access ---
113
+ # We look for kTCCServiceMicrophone / kTCCServiceCamera "Access Allowed"/"Auth Granted".
114
+ /usr/bin/log stream \
115
+ --style syslog \
116
+ --predicate 'subsystem == "com.apple.TCC" AND (eventMessage CONTAINS[c] "kTCCServiceMicrophone" OR eventMessage CONTAINS[c] "kTCCServiceCamera")' \
117
+ 2>/dev/null | \
118
+ while IFS= read -r line; do
119
+ # --- accumulate multi-line TCC log info ---
120
+
121
+ # Parse service type and save to current state
122
+ if [[ "$line" == *"kTCCServiceMicrophone"* ]]; then
123
+ current_svc="microphone"
124
+ elif [[ "$line" == *"kTCCServiceCamera"* ]]; then
125
+ current_svc="camera"
126
+ fi
127
+
128
+ # Parse verdict and save to current state
129
+ if [[ "$line" == *"Access Allowed"* ]] || [[ "$line" == *"Auth Granted"* ]] || [[ "$line" == *"Allow"* ]]; then
130
+ current_verdict="allowed"
131
+ elif [[ "$line" == *"Denied"* ]]; then
132
+ current_verdict="denied"
133
+ elif [[ "$line" == *"FORWARD"* ]]; then
134
+ current_verdict="requested"
135
+ fi
136
+
137
+ # Parse target PID - when we find this, we have enough info to emit
138
+ if [[ "$line" =~ target_token=\{pid:([0-9]+) ]]; then
139
+ current_pid="${BASH_REMATCH[1]}"
140
+ # Get process info from PID
141
+ if [[ -n "$current_pid" ]]; then
142
+ current_app_full=$(ps -p "$current_pid" -o comm= 2>/dev/null | tail -1)
143
+ # Extract just the app name from full path (e.g., "Google Chrome Helper" from long path)
144
+ current_app=$(basename "$current_app_full" 2>/dev/null || echo "$current_app_full")
145
+
146
+ # Get additional process details
147
+ current_parent_pid=$(parent_pid "$current_pid")
148
+ current_process_path=$(process_path "$current_pid")
149
+ fi
150
+
151
+ # --- we now have complete info, check if we should emit ---
152
+ if [[ -n "$current_svc" ]] && [[ -n "$current_pid" ]]; then
153
+ ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
154
+ fg_app="$(front_app)"
155
+ cam_now="$(camera_active)"
156
+ win_title="$(window_title)"
157
+ sess_id="$(session_id)"
158
+
159
+ # Normalize app name to reduce Teams/Chrome helper noise
160
+ normalized_app=$(normalize_app "$current_app")
161
+
162
+ # Get current timestamp for time-based deduplication
163
+ current_time=$(date +%s)
164
+
165
+ # Create state string focusing on actual meeting process, not front app switching
166
+ # Only track: camera status + service + normalized app (ignore front_app changes)
167
+ current_state="${cam_now}|${current_svc}|${normalized_app}"
168
+ previous_state="${prev_camera_active}|${prev_service}|${prev_main_app}"
169
+
170
+ # Calculate time since last log
171
+ time_diff=$((current_time - last_log_time))
172
+
173
+ # Log if there's a meaningful change OR enough time has passed (10 seconds cooldown)
174
+ # This allows new meetings while preventing Teams helper process spam
175
+ if [[ "$current_state" != "$previous_state" ]] || [[ $time_diff -ge 10 ]]; then
176
+ # State changed - emit JSON
177
+ json \
178
+ event="meeting_signal" \
179
+ timestamp="$ts" \
180
+ service="$current_svc" \
181
+ verdict="$current_verdict" \
182
+ process="$current_app" \
183
+ pid="$current_pid" \
184
+ parent_pid="$current_parent_pid" \
185
+ process_path="$current_process_path" \
186
+ front_app="$fg_app" \
187
+ window_title="$win_title" \
188
+ session_id="$sess_id" \
189
+ camera_active="$cam_now"
190
+
191
+ # Update previous state
192
+ prev_camera_active="$cam_now"
193
+ prev_front_app="$fg_app" # Still track for JSON output
194
+ prev_service="$current_svc"
195
+ prev_verdict="$current_verdict"
196
+ prev_process="$current_app"
197
+ prev_pid="$current_pid"
198
+ prev_main_app="$normalized_app"
199
+ last_log_time="$current_time"
200
+ fi
201
+
202
+ # Reset current state for next TCC entry
203
+ current_svc=""
204
+ current_pid=""
205
+ current_app=""
206
+ current_verdict=""
207
+ current_parent_pid=""
208
+ current_process_path=""
209
+ fi
210
+ fi
211
+ done
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@mostrom/meeting-detector",
3
+ "version": "1.0.0",
4
+ "description": "Real-time meeting detection for macOS desktop apps",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist/**/*",
16
+ "meeting-detect.sh",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "ts-node src/index.ts",
22
+ "start": "node dist/index.js",
23
+ "watch": "tsc -w",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": ["meeting", "detection", "macos", "tcc", "privacy"],
27
+ "author": "Kaiser White <kaise@mostrom.io>",
28
+ "license": "MIT",
29
+ "publishConfig": {
30
+ "access": "restricted"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/kaisewhite/meeting-detector.git"
35
+ },
36
+ "homepage": "https://github.com/kaisewhite/meeting-detector#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/kaisewhite/meeting-detector/issues"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.0.0",
42
+ "ts-node": "^10.9.0",
43
+ "typescript": "^5.0.0"
44
+ },
45
+ "dependencies": {},
46
+ "os": ["darwin"],
47
+ "engines": {
48
+ "node": ">=14.0.0"
49
+ }
50
+ }