dashcam 0.8.3 → 1.0.1-beta.10

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.
Files changed (55) hide show
  1. package/.dashcam/cli-config.json +3 -0
  2. package/.dashcam/recording.log +135 -0
  3. package/.dashcam/web-config.json +11 -0
  4. package/.github/RELEASE.md +59 -0
  5. package/.github/workflows/publish.yml +43 -0
  6. package/BACKWARD_COMPATIBILITY.md +177 -0
  7. package/LOG_TRACKING_GUIDE.md +225 -0
  8. package/README.md +709 -155
  9. package/bin/dashcam-background.js +177 -0
  10. package/bin/dashcam.cjs +8 -0
  11. package/bin/dashcam.js +696 -0
  12. package/bin/index.js +63 -0
  13. package/examples/execute-script.js +152 -0
  14. package/examples/simple-test.js +37 -0
  15. package/lib/applicationTracker.js +311 -0
  16. package/lib/auth.js +222 -0
  17. package/lib/binaries.js +21 -0
  18. package/lib/config.js +34 -0
  19. package/lib/extension-logs/helpers.js +182 -0
  20. package/lib/extension-logs/index.js +347 -0
  21. package/lib/extension-logs/manager.js +344 -0
  22. package/lib/ffmpeg.js +155 -0
  23. package/lib/logTracker.js +23 -0
  24. package/lib/logger.js +118 -0
  25. package/lib/logs/index.js +488 -0
  26. package/lib/permissions.js +85 -0
  27. package/lib/processManager.js +317 -0
  28. package/lib/recorder.js +690 -0
  29. package/lib/store.js +58 -0
  30. package/lib/tracking/FileTracker.js +105 -0
  31. package/lib/tracking/FileTrackerManager.js +62 -0
  32. package/lib/tracking/LogsTracker.js +161 -0
  33. package/lib/tracking/active-win.js +212 -0
  34. package/lib/tracking/icons/darwin.js +39 -0
  35. package/lib/tracking/icons/index.js +167 -0
  36. package/lib/tracking/icons/windows.js +27 -0
  37. package/lib/tracking/idle.js +82 -0
  38. package/lib/tracking.js +23 -0
  39. package/lib/uploader.js +456 -0
  40. package/lib/utilities/jsonl.js +77 -0
  41. package/lib/webLogsDaemon.js +234 -0
  42. package/lib/websocket/server.js +223 -0
  43. package/package.json +53 -21
  44. package/recording.log +814 -0
  45. package/sea-bundle.mjs +34595 -0
  46. package/test-page.html +15 -0
  47. package/test.log +1 -0
  48. package/test_run.log +48 -0
  49. package/test_workflow.sh +154 -0
  50. package/examples/crash-test.js +0 -11
  51. package/examples/github-issue.sh +0 -1
  52. package/examples/protocol.html +0 -22
  53. package/index.js +0 -177
  54. package/lib.js +0 -199
  55. package/recorder.js +0 -85
@@ -0,0 +1,488 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import { logger } from '../logger.js';
4
+ import { LogsTracker } from '../tracking/LogsTracker.js';
5
+ import { FileTrackerManager } from '../tracking/FileTrackerManager.js';
6
+ import { WebLogsDaemon, DAEMON_CONFIG_FILE } from '../webLogsDaemon.js';
7
+ import { WebLogsTracker } from '../extension-logs/index.js';
8
+ import { WebTrackerManager } from '../extension-logs/manager.js';
9
+ import { server } from '../websocket/server.js';
10
+ import { jsonl } from '../utilities/jsonl.js';
11
+ import { filterWebEvents } from '../extension-logs/helpers.js';
12
+ import fs from 'fs';
13
+
14
+ const CLI_CONFIG_FILE = path.join(process.cwd(), '.dashcam', 'cli-config.json');
15
+
16
+ // Simple trim function for CLI (adapted from desktop app)
17
+ async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipId) {
18
+ logger.info('Trimming logs', {
19
+ count: groupLogStatuses.length,
20
+ startMS,
21
+ endMS,
22
+ clientStartDate,
23
+ clientStartDateReadable: new Date(clientStartDate).toISOString()
24
+ });
25
+
26
+ const REPLAY_DIR = path.join(os.tmpdir(), 'dashcam', 'recordings');
27
+
28
+ // Filter out logs with no content
29
+ groupLogStatuses = groupLogStatuses.filter((status) => status.count);
30
+
31
+ let webHandled = false;
32
+
33
+ groupLogStatuses.forEach((status) => {
34
+ if (!status.fileLocation || !status.count) return;
35
+
36
+ try {
37
+ const parsed = path.parse(status.fileLocation);
38
+ const content = jsonl.read(status.fileLocation);
39
+
40
+ if (!content || !Array.isArray(content)) return;
41
+
42
+ let events = content;
43
+
44
+ // Convert events to relative time
45
+ let relativeEvents = events.map((event, index) => {
46
+ const originalTime = event.time;
47
+ event.time = parseInt(event.time + '') - startMS;
48
+ // Check if it's not already relative time
49
+ if (event.time > 1_000_000_000_000) {
50
+ // relative time = absolute time - clip start time
51
+ event.time = event.time - clientStartDate;
52
+ if (index === 0) {
53
+ // Log first event for debugging
54
+ logger.info('First event timestamp conversion', {
55
+ originalTime,
56
+ clientStartDate,
57
+ relativeTime: event.time,
58
+ relativeSeconds: (event.time / 1000).toFixed(2),
59
+ startMS,
60
+ line: event.line ? event.line.substring(0, 50) : 'N/A'
61
+ });
62
+ }
63
+ }
64
+ return event;
65
+ });
66
+
67
+ const duration = endMS - startMS;
68
+ let filteredEvents = relativeEvents;
69
+
70
+ if (status.type === 'application' || status.type === 'cli') {
71
+ // Filter events within the time range
72
+ filteredEvents = filteredEvents.filter((event) => {
73
+ return event.time >= 0 && event.time <= duration;
74
+ });
75
+
76
+ if (status.type === 'cli') {
77
+ // For CLI logs, keep the file paths in logFile field for UI display
78
+ // No need to remap to numeric indices
79
+
80
+ // Create items array showing unique files and their event counts
81
+ const fileCounts = {};
82
+ filteredEvents.forEach(event => {
83
+ fileCounts[event.logFile] = (fileCounts[event.logFile] || 0) + 1;
84
+ });
85
+
86
+ status.items = Object.entries(fileCounts).map(([filePath, count]) => ({
87
+ item: filePath,
88
+ count
89
+ }));
90
+ }
91
+ } else if (status.type === 'web' && !webHandled) {
92
+ logger.debug('Found web groupLog, handling all web groupLogs at once');
93
+ // We do this because weblogs have a single shared jsonl file
94
+ // shared between all web logs
95
+ filteredEvents = filterWebEvents(
96
+ filteredEvents,
97
+ groupLogStatuses.filter((status) => status.type === 'web'),
98
+ 0,
99
+ duration
100
+ );
101
+
102
+ webHandled = true;
103
+ } else if (status.type === 'web') {
104
+ // Skip processing for additional web logs - already handled
105
+ status.trimmedFileLocation = path.join(
106
+ REPLAY_DIR,
107
+ [clipId, parsed.base].join('_')
108
+ );
109
+ status.count = filteredEvents.length;
110
+ return;
111
+ }
112
+
113
+ logger.debug('Filtered events', {
114
+ source: events.length,
115
+ filtered: filteredEvents.length,
116
+ difference: events.length - filteredEvents.length,
117
+ });
118
+
119
+ status.count = filteredEvents.length;
120
+ status.trimmedFileLocation = jsonl.write(
121
+ REPLAY_DIR,
122
+ [clipId, parsed.base].join('_'),
123
+ filteredEvents
124
+ );
125
+ } catch (error) {
126
+ logger.error('Error trimming log file', { file: status.fileLocation, error });
127
+ }
128
+ });
129
+
130
+ // Handle shared web log file location
131
+ const firstWebLog = groupLogStatuses.find(
132
+ (status) => status.type === 'web' && status.trimmedFileLocation
133
+ );
134
+ if (firstWebLog) {
135
+ groupLogStatuses
136
+ .filter((status) => status.type === 'web')
137
+ .forEach(
138
+ (status) =>
139
+ (status.trimmedFileLocation = firstWebLog.trimmedFileLocation)
140
+ );
141
+ }
142
+
143
+ return groupLogStatuses;
144
+ }
145
+
146
+ class LogsTrackerManager {
147
+ constructor() {
148
+ this.instances = {};
149
+ this.cliConfig = {};
150
+ this.webLogsConfig = {};
151
+ this.fileTrackerManager = new FileTrackerManager();
152
+
153
+ // Load persisted configs
154
+ this.loadCliConfig();
155
+ this.loadWebConfig();
156
+
157
+ // Create the singleton watch-only tracker for CLI files
158
+ this.watchTracker = new LogsTracker({
159
+ config: this.cliConfig,
160
+ fileTrackerManager: this.fileTrackerManager
161
+ });
162
+ }
163
+
164
+ async ensureWebDaemonRunning() {
165
+ try {
166
+ await WebLogsDaemon.ensureDaemonRunning();
167
+ } catch (error) {
168
+ logger.error('Failed to ensure web daemon is running', { error });
169
+ }
170
+ }
171
+
172
+ loadWebConfig() {
173
+ try {
174
+ if (fs.existsSync(DAEMON_CONFIG_FILE)) {
175
+ const data = fs.readFileSync(DAEMON_CONFIG_FILE, 'utf8');
176
+ this.webLogsConfig = JSON.parse(data);
177
+ }
178
+ } catch (error) {
179
+ logger.error('Failed to load web config', { error });
180
+ }
181
+ }
182
+
183
+ saveWebConfig() {
184
+ try {
185
+ fs.writeFileSync(DAEMON_CONFIG_FILE, JSON.stringify(this.webLogsConfig, null, 2));
186
+ } catch (error) {
187
+ logger.error('Failed to save web config', { error });
188
+ }
189
+ }
190
+
191
+ loadCliConfig() {
192
+ try {
193
+ if (fs.existsSync(CLI_CONFIG_FILE)) {
194
+ const data = fs.readFileSync(CLI_CONFIG_FILE, 'utf8');
195
+ this.cliConfig = JSON.parse(data);
196
+ }
197
+ } catch (error) {
198
+ logger.error('Failed to load CLI config', { error });
199
+ }
200
+ }
201
+
202
+ saveCliConfig() {
203
+ try {
204
+ // Ensure directory exists
205
+ const dir = path.dirname(CLI_CONFIG_FILE);
206
+ if (!fs.existsSync(dir)) {
207
+ fs.mkdirSync(dir, { recursive: true });
208
+ }
209
+ fs.writeFileSync(CLI_CONFIG_FILE, JSON.stringify(this.cliConfig, null, 2));
210
+ } catch (error) {
211
+ logger.error('Failed to save CLI config', { error });
212
+ }
213
+ }
214
+
215
+ updateLogsConfig(config) {
216
+ // Update web logs config
217
+ const webConfigs = Array.isArray(config) ?
218
+ config.filter(app => app.type === 'web' && app.enabled === true)
219
+ .reduce((map, config) => {
220
+ map[config.id] = config;
221
+ return map;
222
+ }, {}) : {};
223
+
224
+ this.webLogsConfig = webConfigs;
225
+ this.saveWebConfig();
226
+
227
+ logger.info('Updated logs config', { webConfigs });
228
+ }
229
+
230
+ async addWebTracker(config) {
231
+ this.webLogsConfig[config.id] = config;
232
+ this.saveWebConfig();
233
+
234
+ // Ensure daemon is running and will pick up the new config
235
+ await this.ensureWebDaemonRunning();
236
+
237
+ logger.info(`Added web tracker: ${config.name}`, { patterns: config.patterns });
238
+ }
239
+
240
+ pushCliTrackedPath(filePath) {
241
+ if (!this.cliConfig[filePath]) {
242
+ this.cliConfig[filePath] = true;
243
+ this.watchTracker.updateConfig(this.cliConfig);
244
+ this.saveCliConfig();
245
+ logger.info(`Added CLI tracked path: ${filePath}`);
246
+ }
247
+ }
248
+
249
+ removeCliTrackedPath(filePath) {
250
+ if (this.cliConfig[filePath]) {
251
+ delete this.cliConfig[filePath];
252
+ this.watchTracker.updateConfig(this.cliConfig);
253
+ this.saveCliConfig();
254
+ logger.info(`Removed CLI tracked path: ${filePath}`);
255
+ }
256
+ }
257
+
258
+ // CLI interface methods
259
+ addCliLogFile(filePath) {
260
+ this.pushCliTrackedPath(filePath);
261
+ }
262
+
263
+ removeCliLogFile(filePath) {
264
+ this.removeCliTrackedPath(filePath);
265
+ }
266
+
267
+ addPipedLog(content) {
268
+ // Find active recording instance
269
+ const activeInstances = Object.values(this.instances);
270
+ if (activeInstances.length === 0) {
271
+ logger.warn('No active recording to add piped content to');
272
+ return;
273
+ }
274
+
275
+ // Add to the most recent active instance (last one)
276
+ const instance = activeInstances[activeInstances.length - 1];
277
+ const timestamp = Date.now();
278
+
279
+ // Write to CLI tracker's temp file
280
+ if (instance.trackers && instance.trackers.cli) {
281
+ const pipedLog = {
282
+ time: timestamp,
283
+ logFile: 'piped-input',
284
+ content: content
285
+ };
286
+
287
+ // Write to the CLI tracker's output file
288
+ try {
289
+ const outputFile = path.join(instance.directory, 'cli-logs.jsonl');
290
+ const logLine = JSON.stringify(pipedLog) + '\n';
291
+ fs.appendFileSync(outputFile, logLine);
292
+ logger.info('Added piped content to recording', {
293
+ recorderId: instance.recorderId,
294
+ contentLength: content.length
295
+ });
296
+ } catch (error) {
297
+ logger.error('Failed to write piped content', { error });
298
+ }
299
+ }
300
+ }
301
+
302
+ removeTracker(id) {
303
+ // Try removing from web trackers first
304
+ if (this.webLogsConfig[id]) {
305
+ delete this.webLogsConfig[id];
306
+ this.saveWebConfig();
307
+ logger.info(`Removed web tracker: ${id}`);
308
+ return;
309
+ }
310
+
311
+ // Check if it's a file tracker (format: file-1, file-2, etc.)
312
+ if (id.startsWith('file-')) {
313
+ const fileIndex = parseInt(id.split('-')[1]) - 1;
314
+ const cliFiles = Object.keys(this.cliConfig);
315
+ if (fileIndex >= 0 && fileIndex < cliFiles.length) {
316
+ const filePath = cliFiles[fileIndex];
317
+ this.removeCliTrackedPath(filePath);
318
+ logger.info(`Removed file tracker: ${filePath}`);
319
+ return;
320
+ }
321
+ }
322
+
323
+ logger.warn(`Tracker not found: ${id}`);
324
+ }
325
+
326
+ getStatus() {
327
+ // Load current web config
328
+ this.loadWebConfig();
329
+
330
+ const activeInstances = Object.keys(this.instances).length;
331
+ const cliFilesCount = Object.keys(this.cliConfig).length;
332
+ const webAppsCount = Object.keys(this.webLogsConfig).length;
333
+
334
+ // Get file tracker stats
335
+ const fileTrackerStats = Object.keys(this.cliConfig).map(filePath => {
336
+ const stats = this.fileTrackerManager.getStats(filePath);
337
+ return {
338
+ filePath,
339
+ count: stats.count
340
+ };
341
+ });
342
+
343
+ const totalEvents = fileTrackerStats.reduce((sum, stat) => sum + stat.count, 0);
344
+
345
+ return {
346
+ activeInstances,
347
+ cliFilesCount,
348
+ webAppsCount,
349
+ totalEvents,
350
+ fileTrackerStats,
351
+ cliFiles: Object.keys(this.cliConfig),
352
+ webApps: Object.values(this.webLogsConfig).map(config => ({
353
+ id: config.id,
354
+ name: config.name,
355
+ patterns: config.patterns
356
+ })),
357
+ webDaemonRunning: WebLogsDaemon.isDaemonRunning()
358
+ };
359
+ }
360
+
361
+ async startNew({ recorderId, screenId, directory }) {
362
+ logger.debug('LogsTrackerManager: Starting new logs tracker instance', { recorderId, screenId, directory });
363
+
364
+ const instanceKey = `${recorderId}_${screenId}`;
365
+
366
+ // Create recording tracker for CLI logs
367
+ const cliTracker = new LogsTracker({
368
+ directory,
369
+ config: { ...this.cliConfig }, // Copy current config
370
+ fileTrackerManager: this.fileTrackerManager,
371
+ });
372
+
373
+ // Start WebSocket server if not already running
374
+ if (!server.isListening.value) {
375
+ logger.debug('LogsTrackerManager: Starting WebSocket server...');
376
+ await server.start();
377
+ logger.info('LogsTrackerManager: WebSocket server started on port', { port: server.port });
378
+ } else {
379
+ logger.debug('LogsTrackerManager: WebSocket server already running on port', { port: server.port });
380
+ }
381
+
382
+ // Create a WebTrackerManager instance for this recording
383
+ logger.debug('LogsTrackerManager: Creating WebTrackerManager for recording...');
384
+ const webTrackerManager = new WebTrackerManager(server);
385
+
386
+ // Create recording tracker for web logs with directory to write events to file
387
+ logger.debug('LogsTrackerManager: Creating WebLogsTracker for recording...', {
388
+ directory,
389
+ webConfigCount: Object.keys(this.webLogsConfig).length
390
+ });
391
+ const webTracker = new WebLogsTracker({
392
+ config: { ...this.webLogsConfig }, // Copy current web config
393
+ webTrackerManager,
394
+ directory // This makes it NOT watch-only, so events will be written to file
395
+ });
396
+
397
+ this.instances[instanceKey] = {
398
+ recorderId,
399
+ screenId,
400
+ directory,
401
+ trackers: {
402
+ cli: cliTracker,
403
+ web: webTracker,
404
+ webTrackerManager // Store this so we can clean it up later
405
+ },
406
+ startTime: Date.now(),
407
+ endTime: undefined,
408
+ };
409
+
410
+ logger.info(`Started new logs tracker instance with web support`, {
411
+ recorderId,
412
+ screenId,
413
+ directory,
414
+ webConfigCount: Object.keys(this.webLogsConfig).length
415
+ });
416
+ return this.instances[instanceKey];
417
+ }
418
+
419
+ async stop({ recorderId, screenId }) {
420
+ const instanceKey = `${recorderId}_${screenId}`;
421
+ const instance = this.instances[instanceKey];
422
+
423
+ if (!instance) {
424
+ logger.warn(`No logs tracker instance found for ${instanceKey}`);
425
+ return [];
426
+ }
427
+
428
+ delete this.instances[instanceKey];
429
+
430
+ // Stop CLI tracker
431
+ const cliStatus = instance.trackers.cli.destroy();
432
+
433
+ // Stop web tracker if it exists
434
+ let webStatus = [];
435
+ if (instance.trackers.web) {
436
+ logger.debug('LogsTrackerManager: Stopping web tracker...', { recorderId, screenId });
437
+ webStatus = instance.trackers.web.destroy();
438
+ }
439
+
440
+ // Clean up WebTrackerManager if it exists
441
+ if (instance.trackers.webTrackerManager) {
442
+ logger.debug('LogsTrackerManager: Destroying WebTrackerManager...', { recorderId, screenId });
443
+ instance.trackers.webTrackerManager.destroy();
444
+ }
445
+
446
+ // Stop WebSocket server if no more recording instances are active
447
+ const remainingInstances = Object.keys(this.instances).length;
448
+ if (remainingInstances === 0 && server.isListening.value) {
449
+ logger.debug('LogsTrackerManager: No more recording instances, stopping WebSocket server...');
450
+ await server.stop();
451
+ logger.info('LogsTrackerManager: WebSocket server stopped');
452
+ } else {
453
+ logger.debug('LogsTrackerManager: WebSocket server kept running', { remainingInstances });
454
+ }
455
+
456
+ logger.info(`Stopped logs tracker instance with web support`, {
457
+ recorderId,
458
+ screenId,
459
+ cliStatusCount: cliStatus.length,
460
+ webStatusCount: webStatus.length
461
+ });
462
+
463
+ // Combine CLI and web statuses
464
+ return [...cliStatus, ...webStatus];
465
+ }
466
+
467
+ stopAll() {
468
+ logger.info('Stopping all logs tracker instances');
469
+ const results = [];
470
+ for (const instanceKey of Object.keys(this.instances)) {
471
+ const [recorderId, screenId] = instanceKey.split('_');
472
+ results.push(...this.stop({ recorderId, screenId }));
473
+ }
474
+ return results;
475
+ }
476
+
477
+ destroy() {
478
+ this.stopAll();
479
+ this.watchTracker.destroy();
480
+ this.fileTrackerManager.destroy();
481
+ // Note: Don't stop the web daemon here as it should persist
482
+ }
483
+ }
484
+
485
+ // Create singleton instance
486
+ const logsTrackerManager = new LogsTrackerManager();
487
+
488
+ export { trimLogs, logsTrackerManager };
@@ -0,0 +1,85 @@
1
+ import { logger } from './logger.js';
2
+ import { execa } from 'execa';
3
+ import os from 'os';
4
+
5
+ /**
6
+ * Check if the application has screen recording permissions on macOS
7
+ */
8
+ async function checkScreenRecordingPermission() {
9
+ const platform = os.platform();
10
+
11
+ if (platform !== 'darwin') {
12
+ // Not macOS, permissions check not needed
13
+ return { hasPermission: true, platform };
14
+ }
15
+
16
+ try {
17
+ // Try to capture a single frame to test permissions
18
+ // This is a quick test that will fail immediately if permissions are denied
19
+ const { stderr } = await execa('screencapture', ['-x', '-t', 'png', '/tmp/dashcam_permission_test.png'], {
20
+ timeout: 2000,
21
+ reject: false
22
+ });
23
+
24
+ // Clean up test file
25
+ try {
26
+ const fs = await import('fs');
27
+ if (fs.existsSync('/tmp/dashcam_permission_test.png')) {
28
+ fs.unlinkSync('/tmp/dashcam_permission_test.png');
29
+ }
30
+ } catch (e) {
31
+ // Ignore cleanup errors
32
+ }
33
+
34
+ const hasPermission = !stderr || !stderr.includes('not permitted');
35
+
36
+ return { hasPermission, platform: 'darwin' };
37
+ } catch (error) {
38
+ logger.debug('Permission check failed', { error: error.message });
39
+ return { hasPermission: false, platform: 'darwin', error: error.message };
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Display instructions for granting screen recording permissions
45
+ */
46
+ function showPermissionInstructions() {
47
+ const platform = os.platform();
48
+
49
+ if (platform !== 'darwin') {
50
+ return;
51
+ }
52
+
53
+ console.log('\n⚠️ Screen Recording Permission Required\n');
54
+ console.log('Dashcam needs screen recording permission to capture your screen.');
55
+ console.log('\nTo grant permission:');
56
+ console.log('1. Open System Settings (or System Preferences)');
57
+ console.log('2. Go to Privacy & Security > Screen Recording');
58
+ console.log('3. Click the lock icon and enter your password');
59
+ console.log('4. Enable screen recording for Terminal (or your terminal app)');
60
+ console.log(' - If using the standalone binary, you may need to add the binary itself');
61
+ console.log('5. Restart your terminal and try again\n');
62
+ console.log('Note: You may need to fully quit and restart your terminal application.\n');
63
+ }
64
+
65
+ /**
66
+ * Check permissions and show instructions if needed
67
+ */
68
+ async function ensurePermissions() {
69
+ const result = await checkScreenRecordingPermission();
70
+
71
+ if (!result.hasPermission) {
72
+ logger.warn('Screen recording permission not granted');
73
+ showPermissionInstructions();
74
+ return false;
75
+ }
76
+
77
+ logger.debug('Screen recording permission check passed');
78
+ return true;
79
+ }
80
+
81
+ export {
82
+ checkScreenRecordingPermission,
83
+ showPermissionInstructions,
84
+ ensurePermissions
85
+ };