@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 +182 -0
- package/dist/detector.d.ts +56 -0
- package/dist/detector.d.ts.map +1 -0
- package/dist/detector.js +339 -0
- package/dist/detector.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/meeting-detect.sh +211 -0
- package/package.json +50 -0
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"}
|
package/dist/detector.js
ADDED
|
@@ -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":"AAAA,OAAO,EAAE,KAAK,EAAgB,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAQ1C,MAAM,OAAO,eAAgB,SAAQ,YAAY;IAK/C,YAAY,UAAkC,EAAE;QAC9C,KAAK,EAAE,CAAC;QAHF,mBAAc,GAA6B,IAAI,GAAG,EAAE,CAAC;QAK3D,+DAA+D;QAC/D,MAAM,iBAAiB,GAAG,OAAO,CAAC,UAAU,IAAI,IAAI,CAClD,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EACvC,sBAAsB,CACvB,CAAC;QAEF,IAAI,CAAC,OAAO,GAAG;YACb,UAAU,EAAE,iBAAiB;YAC7B,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,KAAK;YAC7B,sBAAsB,EAAE,OAAO,CAAC,sBAAsB,IAAI,KAAK;SAChE,CAAC;IACJ,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,QAA+B;QAC1C,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;QACjD,CAAC;QAED,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAC/B,CAAC;QAED,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE;YACpD,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;SAChC,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAEjD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;oBAChB,IAAI,CAAC;wBACH,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;wBACtC,IAAI,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;4BACpC,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gCACvB,OAAO,CAAC,GAAG,CAAC,oCAAoC,EAAE,MAAM,CAAC,CAAC;4BAC5D,CAAC;4BACD,SAAS;wBACX,CAAC;wBAED,IAAI,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;4BACpC,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gCACvB,OAAO,CAAC,GAAG,CAAC,+CAA+C,EAAE,MAAM,CAAC,CAAC;4BACvE,CAAC;4BACD,SAAS;wBACX,CAAC;wBAED,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;4BACvB,OAAO,CAAC,GAAG,CAAC,kCAAkC,EAAE,MAAM,CAAC,CAAC;wBAC1D,CAAC;wBACD,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;oBAC/B,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;4BACvB,OAAO,CAAC,GAAG,CAAC,yCAAyC,EAAE,IAAI,CAAC,CAAC;wBAC/D,CAAC;wBACD,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,KAAK,CAAC,2BAA2B,IAAI,EAAE,CAAC,CAAC,CAAC;oBACnE,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YAC/C,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gBACvB,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACjC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;YACvC,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gBACvB,OAAO,CAAC,GAAG,CAAC,8CAA8C,IAAI,YAAY,MAAM,EAAE,CAAC,CAAC;YACtF,CAAC;YACD,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;YACzB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;QAEH,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACvB,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IAED;;OAEG;IACI,IAAI;QACT,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC7B,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;YAEzB,sCAAsC;YACtC,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;YAE5B,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gBACvB,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;YACtD,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACI,SAAS;QACd,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;IACxB,CAAC;IAED;;OAEG;IACI,SAAS,CAAC,QAA8B;QAC7C,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC/B,CAAC;IAED;;OAEG;IACI,OAAO,CAAC,QAA4B;QACzC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC7B,CAAC;IAED;;;;;;;;OAQG;IACK,kBAAkB,CAAC,MAAqB;QAC9C,+DAA+D;QAC/D,MAAM,qBAAqB,GAAG;YAC5B,QAAQ,EAAY,gBAAgB;YACpC,QAAQ,EAAY,0BAA0B;YAC9C,mBAAmB,EAAE,uBAAuB;YAC5C,UAAU,EAAU,yBAAyB;YAC7C,gBAAgB,EAAI,0BAA0B;YAC9C,YAAY,EAAQ,qBAAqB;YACzC,mBAAmB,EAAE,oBAAoB;YACzC,eAAe,EAAK,wCAAwC;YAC5D,iBAAiB,EAAG,4BAA4B;SACjD,CAAC;QAEF,4DAA4D;QAC5D,MAAM,eAAe,GAAG;YACtB,UAAU;YACV,UAAU;YACV,SAAS;YACT,QAAQ;YACR,OAAO;YACP,MAAM;YACN,eAAe,EAAI,kDAAkD;YACrE,QAAQ,EAAW,yBAAyB;YAC5C,SAAS,EAAU,0BAA0B;YAC7C,gBAAgB,EAAG,uBAAuB;SAC3C,CAAC;QAEF,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QACxD,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAExD,kEAAkE;QAClE,IAAI,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YACzE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,wCAAwC;QACxC,IAAI,eAAe,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;YAC1C,OAAO,IAAI,CAAC;QACd,CAAC;QAED,qFAAqF;QACrF,gEAAgE;QAChE,0EAA0E;QAC1E,IAAI,MAAM,CAAC,OAAO,KAAK,WAAW,IAAI,CAAC,CAAC,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;YAClG,qFAAqF;YACrF,iFAAiF;YACjF,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;gBAC1B,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAED,qFAAqF;QACrF,iFAAiF;QACjF,IAAI,WAAW,KAAK,aAAa,EAAE,CAAC;YAClC,MAAM,WAAW,GAAG,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC;YAE9C,4CAA4C;YAC5C,wBAAwB;YACxB,8BAA8B;YAC9B,oDAAoD;YACpD,MAAM,iBAAiB,GACrB,WAAW,CAAC,QAAQ,CAAC,iBAAiB,CAAC;gBACvC,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAC/B,4BAA4B,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,uBAAuB;YAEzE,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBACvB,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;OAEG;IACK,aAAa,CAAC,MAAqB;QACzC,OAAO,GAAG,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;IAC/D,CAAC;IAED;;;OAGG;IACK,kBAAkB,CAAC,MAAqB;QAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAE5D,yCAAyC;QACzC,IAAI,CAAC,sBAAsB,EAAE,CAAC;QAE9B,IAAI,eAAe,EAAE,CAAC;YACpB,MAAM,iBAAiB,GAAG,GAAG,GAAG,eAAe,CAAC,QAAQ,CAAC;YAEzD,IAAI,iBAAiB,GAAG,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE,CAAC;gBAC5D,6CAA6C;gBAC7C,eAAe,CAAC,QAAQ,GAAG,GAAG,CAAC;gBAC/B,OAAO,IAAI,CAAC,CAAC,sBAAsB;YACrC,CAAC;QACH,CAAC;QAED,4CAA4C;QAC5C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,EAAE;YAClC,QAAQ,EAAE,GAAG;YACb,MAAM;SACP,CAAC,CAAC;QAEH,OAAO,KAAK,CAAC,CAAC,0BAA0B;IAC1C,CAAC;IAED;;OAEG;IACK,sBAAsB;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,WAAW,GAAa,EAAE,CAAC;QAEjC,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,CAAC;YAC3D,IAAI,GAAG,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE,CAAC;gBACjE,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;QAED,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;YAC9B,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChC,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gBACvB,OAAO,CAAC,GAAG,CAAC,iDAAiD,GAAG,EAAE,CAAC,CAAC;YACtE,CAAC;QACH,CAAC;IACH,CAAC;IAEO,WAAW,CAAC,IAAY;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAwB,CAAC;QAEvD,gHAAgH;QAChH,MAAM,eAAe,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;QAC7C,MAAM,kBAAkB,GAAG,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QACnF,MAAM,YAAY,GAAG,CAAC,eAAe,KAAK,YAAY,IAAI,eAAe,KAAK,QAAQ,IAAI,CAAC,eAAe,CAAC;YACzG,CAAC,CAAC,kBAAkB;YACpB,CAAC,CAAC,eAAe,CAAC;QAEpB,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,OAAO,EAAE,YAAY;YACrB,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE;YAC7B,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE;YAC7B,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,EAAE;YACrB,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,EAAE;YACnC,YAAY,EAAE,MAAM,CAAC,YAAY,IAAI,EAAE;YACvC,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,EAAE;YACjC,YAAY,EAAE,MAAM,CAAC,YAAY,IAAI,EAAE;YACvC,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,EAAE;YACnC,aAAa,EAAE,MAAM,CAAC,aAAa,KAAK,MAAM,IAAI,MAAM,CAAC,aAAa,KAAK,IAAI;SAChF,CAAC;IACJ,CAAC;IAEO,gBAAgB,CAAC,QAAgB,EAAE,OAAe;QACxD,MAAM,GAAG,GAAG,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAE1C,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACpD,OAAO,OAAO,CAAC;QACjB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACjG,OAAO,iBAAiB,CAAC;QAC3B,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACzD,OAAO,MAAM,CAAC;QAChB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;YAC3F,OAAO,OAAO,CAAC;QACjB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;YAClK,OAAO,aAAa,CAAC;QACvB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3D,OAAO,OAAO,CAAC;QACjB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAC/D,OAAO,SAAS,CAAC;QACnB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YACjE,OAAO,UAAU,CAAC;QACpB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;YACxG,OAAO,aAAa,CAAC;QACvB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YAClG,OAAO,WAAW,CAAC;QACrB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3D,OAAO,YAAY,CAAC;QACtB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAC/D,OAAO,SAAS,CAAC;QACnB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACvD,OAAO,KAAK,CAAC;QACf,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;YACxG,OAAO,aAAa,CAAC;QACvB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC;YAC/G,OAAO,eAAe,CAAC;QACzB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;YAC5F,OAAO,cAAc,CAAC;QACxB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACrG,OAAO,iBAAiB,CAAC;QAC3B,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;YAC3E,OAAO,eAAe,CAAC;QACzB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YACrE,OAAO,YAAY,CAAC;QACtB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAC/D,OAAO,SAAS,CAAC;QACnB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;YACzE,OAAO,cAAc,CAAC;QACxB,CAAC;aAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;YACnE,OAAO,WAAW,CAAC;QACrB,CAAC;aAAM,CAAC;YACN,oCAAoC;YACpC,OAAO,QAAQ,IAAI,aAAa,CAAC;QACnC,CAAC;IACH,CAAC;CACF;AAED,wCAAwC;AACxC,MAAM,UAAU,QAAQ,CAAC,QAA8B,EAAE,OAAgC;IACvF,MAAM,QAAQ,GAAG,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC;IAC9C,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACzB,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|