dashcam 0.8.3 → 1.0.1-beta.2

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/build.yml +103 -0
  6. package/.github/workflows/publish.yml +43 -0
  7. package/.github/workflows/release.yml +107 -0
  8. package/LOG_TRACKING_GUIDE.md +225 -0
  9. package/README.md +709 -155
  10. package/bin/dashcam.cjs +8 -0
  11. package/bin/dashcam.js +542 -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 +156 -0
  23. package/lib/logTracker.js +23 -0
  24. package/lib/logger.js +118 -0
  25. package/lib/logs/index.js +432 -0
  26. package/lib/permissions.js +85 -0
  27. package/lib/processManager.js +255 -0
  28. package/lib/recorder.js +675 -0
  29. package/lib/store.js +58 -0
  30. package/lib/tracking/FileTracker.js +98 -0
  31. package/lib/tracking/FileTrackerManager.js +62 -0
  32. package/lib/tracking/LogsTracker.js +147 -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 +449 -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 +50 -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 +80 -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
package/lib/ffmpeg.js ADDED
@@ -0,0 +1,156 @@
1
+ import { execa } from 'execa';
2
+ import { logger } from './logger.js';
3
+ import { getFfmpegPath, getFfprobePath } from './binaries.js';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import fs from 'fs';
7
+
8
+ /**
9
+ * Create a snapshot (PNG) from a video at a specific timestamp
10
+ */
11
+ export async function createSnapshot(inputVideoPath, outputSnapshotPath, snapshotTimeSeconds = 0) {
12
+ logger.debug('Creating snapshot', { inputVideoPath, outputSnapshotPath, snapshotTimeSeconds });
13
+
14
+ const ffmpegPath = await getFfmpegPath();
15
+
16
+ const command = [
17
+ '-ss', snapshotTimeSeconds,
18
+ '-i', inputVideoPath,
19
+ '-frames:v', '1',
20
+ '-vf', 'scale=640:-1:force_original_aspect_ratio=decrease:eval=frame',
21
+ '-pred', 'mixed',
22
+ '-compression_level', '100',
23
+ outputSnapshotPath,
24
+ '-y',
25
+ '-hide_banner'
26
+ ];
27
+
28
+ await execa(ffmpegPath, command);
29
+ }
30
+
31
+ /**
32
+ * Create an animated GIF from a video
33
+ */
34
+ export async function createGif(inputVideoPath, outputGifPath) {
35
+ logger.debug('Creating GIF', { inputVideoPath, outputGifPath });
36
+
37
+ const ffmpegPath = await getFfmpegPath();
38
+ const ffprobePath = await getFfprobePath();
39
+
40
+ // Function to check if video is ready
41
+ const isVideoReady = async () => {
42
+ try {
43
+ // Check if file exists and is not empty
44
+ if (!fs.existsSync(inputVideoPath) || fs.statSync(inputVideoPath).size === 0) {
45
+ return false;
46
+ }
47
+
48
+ // Try to read video info with ffprobe
49
+ const { exitCode } = await execa(ffprobePath, [
50
+ '-v', 'error',
51
+ '-select_streams', 'v:0',
52
+ '-show_entries', 'stream=codec_type',
53
+ '-of', 'default=noprint_wrappers=1:nokey=1',
54
+ inputVideoPath
55
+ ], { reject: false });
56
+
57
+ return exitCode === 0;
58
+ } catch (error) {
59
+ return false;
60
+ }
61
+ };
62
+
63
+ // Wait for up to 5 seconds for the video to be ready
64
+ for (let i = 0; i < 10; i++) {
65
+ if (await isVideoReady()) {
66
+ break;
67
+ }
68
+ await new Promise(resolve => setTimeout(resolve, 500));
69
+ }
70
+
71
+ // Final check
72
+ if (!await isVideoReady()) {
73
+ throw new Error('Video file is not ready or is corrupted');
74
+ }
75
+
76
+ const gifFps = 4;
77
+ const gifDuration = 10;
78
+ const gifFrames = Math.ceil(gifDuration * gifFps);
79
+
80
+ // Get video duration in seconds
81
+ const { stdout } = await execa(ffprobePath, [
82
+ '-v', 'error',
83
+ '-show_entries', 'format=duration',
84
+ '-of', 'default=noprint_wrappers=1:nokey=1',
85
+ inputVideoPath
86
+ ]);
87
+
88
+ const videoDuration = parseFloat(stdout);
89
+ const id = (Math.random() + 1).toString(36).substring(7);
90
+
91
+ // Handle NaN or invalid duration
92
+ if (!videoDuration || isNaN(videoDuration) || videoDuration <= 0) {
93
+ logger.warn('Video duration unavailable or invalid, using default sampling for GIF', {
94
+ duration: videoDuration,
95
+ stdout
96
+ });
97
+
98
+ // Fallback: Just sample the video at a fixed rate (e.g., 1 frame every 3 seconds)
99
+ const framesPath = path.join(os.tmpdir(), `frames_${id}_%04d.png`);
100
+ await execa(ffmpegPath, [
101
+ '-i', inputVideoPath,
102
+ '-vf', `fps=1/3`, // Sample 1 frame every 3 seconds
103
+ framesPath
104
+ ]);
105
+
106
+ // Create GIF from frames
107
+ await execa(ffmpegPath, [
108
+ '-framerate', `${gifFps}`,
109
+ '-i', framesPath,
110
+ '-loop', '0',
111
+ outputGifPath,
112
+ '-y',
113
+ '-hide_banner'
114
+ ]);
115
+
116
+ // Clean up temporary frame files
117
+ const framesToDelete = fs.readdirSync(os.tmpdir())
118
+ .filter(file => file.startsWith(`frames_${id}_`) && file.endsWith('.png'))
119
+ .map(file => path.join(os.tmpdir(), file));
120
+
121
+ for (const frame of framesToDelete) {
122
+ fs.unlinkSync(frame);
123
+ }
124
+
125
+ return;
126
+ }
127
+
128
+ const extractedFramesInterval = videoDuration / gifFrames;
129
+
130
+ // Extract frames
131
+ const framesPath = path.join(os.tmpdir(), `frames_${id}_%04d.png`);
132
+ await execa(ffmpegPath, [
133
+ '-i', inputVideoPath,
134
+ '-vf', `fps=1/${extractedFramesInterval}`,
135
+ framesPath
136
+ ]);
137
+
138
+ // Create GIF from frames
139
+ await execa(ffmpegPath, [
140
+ '-framerate', `${gifFps}`,
141
+ '-i', framesPath,
142
+ '-loop', '0',
143
+ outputGifPath,
144
+ '-y',
145
+ '-hide_banner'
146
+ ]);
147
+
148
+ // Clean up temporary frame files
149
+ const framesToDelete = fs.readdirSync(os.tmpdir())
150
+ .filter(file => file.startsWith(`frames_${id}_`) && file.endsWith('.png'))
151
+ .map(file => path.join(os.tmpdir(), file));
152
+
153
+ for (const frame of framesToDelete) {
154
+ fs.unlinkSync(frame);
155
+ }
156
+ }
@@ -0,0 +1,23 @@
1
+ import { LogsTracker } from './tracking/LogsTracker.js';
2
+ import { FileTrackerManager } from './tracking/FileTrackerManager.js';
3
+
4
+ // Create a shared file tracker manager for efficient resource usage
5
+ const fileTrackerManager = new FileTrackerManager();
6
+
7
+ // Create a singleton instance for watch-only mode (no directory)
8
+ const logTracker = new LogsTracker({
9
+ config: {},
10
+ fileTrackerManager
11
+ });
12
+
13
+ // Helper function to create a new tracker for recording (with directory)
14
+ export function createRecordingTracker(directory, config = {}) {
15
+ return new LogsTracker({
16
+ config,
17
+ directory,
18
+ fileTrackerManager
19
+ });
20
+ }
21
+
22
+ // Export the singleton for backwards compatibility
23
+ export { logTracker, fileTrackerManager };
package/lib/logger.js ADDED
@@ -0,0 +1,118 @@
1
+ import winston from 'winston';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ // Constants that match the desktop app
6
+ const DD_TOKEN = process.env.DD_TOKEN || 'pubfd7949e46d22d1e71fc8fa6d95ecc5f2';
7
+ const ENV = process.env.NODE_ENV || 'production';
8
+
9
+ // Configure HTTP transport for Datadog
10
+ const httpTransportOptions = {
11
+ host: 'http-intake.logs.datadoghq.com',
12
+ path: `/api/v2/logs?dd-api-key=${DD_TOKEN}`,
13
+ ssl: true,
14
+ level: 'silly'
15
+ };
16
+
17
+ // Metadata that matches the desktop app
18
+ const httpMeta = {
19
+ ddsource: 'nodejs',
20
+ service: 'dashcam-cli',
21
+ env: ENV,
22
+ os_type: os.type(),
23
+ os_release: os.release()
24
+ };
25
+
26
+ // Verbose level configuration
27
+ let isVerbose = false;
28
+
29
+ // Create custom format to include version and user info
30
+ const updateFormat = winston.format((info) => {
31
+ info.version = process.env.npm_package_version;
32
+ return info;
33
+ });
34
+
35
+ // Custom format for console output with more detail
36
+ const verboseConsoleFormat = winston.format.combine(
37
+ winston.format.timestamp({ format: 'HH:mm:ss.SSS' }),
38
+ winston.format.colorize(),
39
+ winston.format.printf(({ timestamp, level, message, ...meta }) => {
40
+ let output = `${timestamp} [${level}] ${message}`;
41
+
42
+ // Add metadata if present and in verbose mode
43
+ if (isVerbose && Object.keys(meta).length > 0) {
44
+ // Filter out internal winston metadata
45
+ const cleanMeta = Object.fromEntries(
46
+ Object.entries(meta).filter(([key]) =>
47
+ !['timestamp', 'level', 'message', 'ddsource', 'service', 'env', 'os_type', 'os_release', 'version'].includes(key)
48
+ )
49
+ );
50
+
51
+ if (Object.keys(cleanMeta).length > 0) {
52
+ output += `\n ${JSON.stringify(cleanMeta, null, 2).split('\n').join('\n ')}`;
53
+ }
54
+ }
55
+
56
+ return output;
57
+ })
58
+ );
59
+
60
+ // Function to set verbose mode
61
+ export function setVerbose(verbose = true) {
62
+ isVerbose = verbose;
63
+ // Update console transport level based on verbose mode
64
+ const consoleTransport = logger.transports.find(t => t.constructor.name === 'Console');
65
+ if (consoleTransport) {
66
+ consoleTransport.level = verbose ? 'silly' : (ENV === 'production' ? 'info' : 'debug');
67
+ }
68
+ }
69
+
70
+ // Create the logger instance
71
+ export const logger = winston.createLogger({
72
+ format: winston.format.combine(
73
+ updateFormat(),
74
+ winston.format.timestamp(),
75
+ winston.format.json()
76
+ ),
77
+ defaultMeta: {
78
+ ...httpMeta
79
+ },
80
+ transports: [
81
+ // Log to console in development with enhanced formatting
82
+ new winston.transports.Console({
83
+ format: verboseConsoleFormat,
84
+ level: ENV === 'production' ? 'info' : 'debug'
85
+ }),
86
+ // Log to files
87
+ new winston.transports.File({
88
+ filename: path.join(os.homedir(), '.dashcam', 'logs', 'error.log'),
89
+ level: 'error'
90
+ }),
91
+ new winston.transports.File({
92
+ filename: path.join(os.homedir(), '.dashcam', 'logs', 'combined.log'),
93
+ level: 'silly' // Capture all levels in file
94
+ }),
95
+ // Log debug info to separate file when verbose
96
+ new winston.transports.File({
97
+ filename: path.join(os.homedir(), '.dashcam', 'logs', 'debug.log'),
98
+ level: 'debug'
99
+ }),
100
+ // Send to Datadog in production
101
+ ...(ENV === 'production' ? [new winston.transports.Http(httpTransportOptions)] : [])
102
+ ]
103
+ });
104
+
105
+ // Add convenience methods for common logging patterns
106
+ logger.verbose = (message, meta) => logger.debug(`[VERBOSE] ${message}`, meta);
107
+ logger.trace = (message, meta) => logger.silly(`[TRACE] ${message}`, meta);
108
+
109
+ // Function to log function entry/exit for debugging
110
+ export function logFunctionCall(functionName, args = {}) {
111
+ if (isVerbose) {
112
+ logger.debug(`→ Entering ${functionName}`, { args });
113
+ return (result) => {
114
+ logger.debug(`← Exiting ${functionName}`, { result: typeof result });
115
+ };
116
+ }
117
+ return () => {}; // No-op if not verbose
118
+ }