@web-auto/camo 0.1.2 → 0.1.4

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 (51) hide show
  1. package/README.md +137 -0
  2. package/package.json +7 -3
  3. package/scripts/check-file-size.mjs +80 -0
  4. package/scripts/file-size-policy.json +8 -0
  5. package/src/autoscript/action-providers/index.mjs +9 -0
  6. package/src/autoscript/action-providers/xhs/comments.mjs +412 -0
  7. package/src/autoscript/action-providers/xhs/common.mjs +77 -0
  8. package/src/autoscript/action-providers/xhs/detail.mjs +181 -0
  9. package/src/autoscript/action-providers/xhs/interaction.mjs +466 -0
  10. package/src/autoscript/action-providers/xhs/like-rules.mjs +57 -0
  11. package/src/autoscript/action-providers/xhs/persistence.mjs +167 -0
  12. package/src/autoscript/action-providers/xhs/search.mjs +174 -0
  13. package/src/autoscript/action-providers/xhs.mjs +133 -0
  14. package/src/autoscript/impact-engine.mjs +78 -0
  15. package/src/autoscript/runtime.mjs +1015 -0
  16. package/src/autoscript/schema.mjs +370 -0
  17. package/src/autoscript/xhs-unified-template.mjs +931 -0
  18. package/src/cli.mjs +190 -78
  19. package/src/commands/autoscript.mjs +1100 -0
  20. package/src/commands/browser.mjs +20 -4
  21. package/src/commands/container.mjs +401 -0
  22. package/src/commands/events.mjs +152 -0
  23. package/src/commands/lifecycle.mjs +17 -3
  24. package/src/commands/window.mjs +32 -1
  25. package/src/container/change-notifier.mjs +311 -0
  26. package/src/container/element-filter.mjs +143 -0
  27. package/src/container/index.mjs +3 -0
  28. package/src/container/runtime-core/checkpoint.mjs +195 -0
  29. package/src/container/runtime-core/index.mjs +21 -0
  30. package/src/container/runtime-core/operations/index.mjs +351 -0
  31. package/src/container/runtime-core/operations/selector-scripts.mjs +68 -0
  32. package/src/container/runtime-core/operations/tab-pool.mjs +544 -0
  33. package/src/container/runtime-core/operations/viewport.mjs +143 -0
  34. package/src/container/runtime-core/subscription.mjs +87 -0
  35. package/src/container/runtime-core/utils.mjs +94 -0
  36. package/src/container/runtime-core/validation.mjs +127 -0
  37. package/src/container/runtime-core.mjs +1 -0
  38. package/src/container/subscription-registry.mjs +459 -0
  39. package/src/core/actions.mjs +573 -0
  40. package/src/core/browser.mjs +270 -0
  41. package/src/core/index.mjs +53 -0
  42. package/src/core/utils.mjs +87 -0
  43. package/src/events/daemon-entry.mjs +33 -0
  44. package/src/events/daemon.mjs +80 -0
  45. package/src/events/progress-log.mjs +109 -0
  46. package/src/events/ws-server.mjs +239 -0
  47. package/src/lib/client.mjs +200 -0
  48. package/src/lifecycle/session-registry.mjs +8 -4
  49. package/src/lifecycle/session-watchdog.mjs +220 -0
  50. package/src/utils/browser-service.mjs +232 -9
  51. package/src/utils/help.mjs +28 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Core browser control module - Direct camoufox integration
3
+ * No external browser-service dependency
4
+ */
5
+ import { spawn, execSync } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+
10
+ const CONFIG_DIR = path.join(os.homedir(), '.webauto');
11
+ const PROFILES_DIR = path.join(CONFIG_DIR, 'profiles');
12
+
13
+ // Active browser instances registry (in-memory)
14
+ const activeBrowsers = new Map();
15
+
16
+ /**
17
+ * Detect camoufox executable path
18
+ */
19
+ export function detectCamoufoxPath() {
20
+ try {
21
+ const cmd = process.platform === 'win32' ? 'python -m camoufox path' : 'python3 -m camoufox path';
22
+ const out = execSync(cmd, {
23
+ encoding: 'utf8',
24
+ stdio: ['ignore', 'pipe', 'pipe'],
25
+ timeout: 30000,
26
+ });
27
+ const lines = out.trim().split(/\r?\n/);
28
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
29
+ const line = lines[i].trim();
30
+ if (line && (line.startsWith('/') || line.match(/^[A-Z]:\\/))) return line;
31
+ }
32
+ } catch {
33
+ return null;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Ensure camoufox is installed
40
+ */
41
+ export async function ensureCamoufox() {
42
+ const camoufoxPath = detectCamoufoxPath();
43
+ if (camoufoxPath) return camoufoxPath;
44
+
45
+ console.log('Camoufox not found. Installing...');
46
+ try {
47
+ execSync('npx --yes --package=camoufox camoufox fetch', { stdio: 'inherit' });
48
+ const newPath = detectCamoufoxPath();
49
+ if (!newPath) throw new Error('Camoufox install finished but executable was not detected');
50
+ console.log('Camoufox installed at:', newPath);
51
+ return newPath;
52
+ } catch (err) {
53
+ throw new Error(`Failed to install camoufox: ${err.message}`);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get profile directory
59
+ */
60
+ export function getProfileDir(profileId) {
61
+ return path.join(PROFILES_DIR, profileId);
62
+ }
63
+
64
+ /**
65
+ * Ensure profile exists
66
+ */
67
+ export function ensureProfile(profileId) {
68
+ const profileDir = getProfileDir(profileId);
69
+ if (!fs.existsSync(profileDir)) {
70
+ fs.mkdirSync(profileDir, { recursive: true });
71
+ }
72
+ return profileDir;
73
+ }
74
+
75
+ /**
76
+ * Check if browser is running for profile
77
+ */
78
+ export function isBrowserRunning(profileId) {
79
+ const browser = activeBrowsers.get(profileId);
80
+ if (!browser) return false;
81
+ return browser.process && !browser.process.killed;
82
+ }
83
+
84
+ /**
85
+ * Launch browser for profile
86
+ */
87
+ export async function launchBrowser(profileId, options = {}) {
88
+ if (isBrowserRunning(profileId)) {
89
+ throw new Error(`Browser already running for profile: ${profileId}`);
90
+ }
91
+
92
+ const camoufoxPath = await ensureCamoufox();
93
+ const profileDir = ensureProfile(profileId);
94
+
95
+ // Build launch arguments
96
+ const args = [
97
+ '-P', profileDir,
98
+ '--headless=false',
99
+ ];
100
+
101
+ if (options.url) {
102
+ args.push('-url', options.url);
103
+ }
104
+
105
+ // Launch camoufox
106
+ const browserProcess = spawn(camoufoxPath, args, {
107
+ detached: false,
108
+ stdio: ['ignore', 'pipe', 'pipe'],
109
+ });
110
+
111
+ const browser = {
112
+ profileId,
113
+ process: browserProcess,
114
+ profileDir,
115
+ startTime: Date.now(),
116
+ pages: [],
117
+ currentPage: 0,
118
+ wsEndpoint: null,
119
+ };
120
+
121
+ activeBrowsers.set(profileId, browser);
122
+
123
+ // Handle process exit
124
+ browserProcess.on('exit', (code) => {
125
+ activeBrowsers.delete(profileId);
126
+ });
127
+
128
+ // Wait for browser to be ready
129
+ await new Promise((resolve, reject) => {
130
+ const timeout = setTimeout(() => {
131
+ browserProcess.kill();
132
+ reject(new Error('Browser failed to start within timeout'));
133
+ }, 30000);
134
+
135
+ // Check for ready signal in stdout
136
+ browserProcess.stdout.on('data', (data) => {
137
+ const line = data.toString();
138
+ if (line.includes('Browser ready') || line.includes('Listening')) {
139
+ clearTimeout(timeout);
140
+ resolve();
141
+ }
142
+ });
143
+
144
+ // Also resolve after a short delay as fallback
145
+ setTimeout(() => {
146
+ clearTimeout(timeout);
147
+ resolve();
148
+ }, 5000);
149
+ });
150
+
151
+ return browser;
152
+ }
153
+
154
+ /**
155
+ * Stop browser for profile
156
+ */
157
+ export async function stopBrowser(profileId) {
158
+ const browser = activeBrowsers.get(profileId);
159
+ if (!browser) {
160
+ throw new Error(`No browser running for profile: ${profileId}`);
161
+ }
162
+
163
+ if (browser.process && !browser.process.killed) {
164
+ browser.process.kill('SIGTERM');
165
+ // Wait for graceful shutdown
166
+ await new Promise((resolve) => {
167
+ const timeout = setTimeout(() => {
168
+ if (browser.process && !browser.process.killed) {
169
+ browser.process.kill('SIGKILL');
170
+ }
171
+ resolve();
172
+ }, 5000);
173
+
174
+ browser.process.on('exit', () => {
175
+ clearTimeout(timeout);
176
+ resolve();
177
+ });
178
+ });
179
+ }
180
+
181
+ activeBrowsers.delete(profileId);
182
+ return { ok: true, profileId, stopped: true };
183
+ }
184
+
185
+ /**
186
+ * Get browser status
187
+ */
188
+ export function getBrowserStatus(profileId) {
189
+ if (profileId) {
190
+ const browser = activeBrowsers.get(profileId);
191
+ if (!browser) return null;
192
+ return {
193
+ profileId,
194
+ running: isBrowserRunning(profileId),
195
+ startTime: browser.startTime,
196
+ uptime: Date.now() - browser.startTime,
197
+ pages: browser.pages,
198
+ currentPage: browser.currentPage,
199
+ };
200
+ }
201
+
202
+ // Return all sessions
203
+ return Array.from(activeBrowsers.entries()).map(([id, b]) => ({
204
+ profileId: id,
205
+ running: isBrowserRunning(id),
206
+ startTime: b.startTime,
207
+ uptime: Date.now() - b.startTime,
208
+ pages: b.pages,
209
+ currentPage: b.currentPage,
210
+ }));
211
+ }
212
+
213
+ /**
214
+ * Get Playwright browser instance for profile
215
+ * Creates one if needed using camoufox-js
216
+ */
217
+ export async function getPlaywrightBrowser(profileId) {
218
+ const { chromium } = await import('playwright');
219
+
220
+ const browser = activeBrowsers.get(profileId);
221
+ if (!browser) {
222
+ throw new Error(`No browser session for profile: ${profileId}. Run 'camo start ${profileId}' first.`);
223
+ }
224
+
225
+ if (browser.pwBrowser) {
226
+ return browser.pwBrowser;
227
+ }
228
+
229
+ // Connect to camoufox using CDP
230
+ const pwBrowser = await chromium.connectOverCDP(browser.wsEndpoint || 'http://127.0.0.1:9222');
231
+ browser.pwBrowser = pwBrowser;
232
+ return pwBrowser;
233
+ }
234
+
235
+ /**
236
+ * Get current page for profile
237
+ */
238
+ export async function getCurrentPage(profileId) {
239
+ const browser = activeBrowsers.get(profileId);
240
+ if (!browser) {
241
+ throw new Error(`No browser session for profile: ${profileId}`);
242
+ }
243
+
244
+ if (browser.currentPage) {
245
+ return browser.currentPage;
246
+ }
247
+
248
+ // Get page from Playwright
249
+ const pwBrowser = await getPlaywrightBrowser(profileId);
250
+ const contexts = pwBrowser.contexts();
251
+ if (contexts.length === 0) {
252
+ throw new Error('No browser contexts available');
253
+ }
254
+ const pages = contexts[0].pages();
255
+ if (pages.length === 0) {
256
+ throw new Error('No pages available');
257
+ }
258
+
259
+ browser.currentPage = pages[pages.length - 1];
260
+ return browser.currentPage;
261
+ }
262
+
263
+
264
+ /**
265
+ * Get active browser (alias for registry lookup)
266
+ */
267
+ export function getActiveBrowser(profileId) {
268
+ return activeBrowsers.get(profileId) || null;
269
+ }
270
+
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Core module exports
3
+ */
4
+
5
+ export * from './browser.mjs';
6
+ export * from './actions.mjs';
7
+ export * from './utils.mjs';
8
+
9
+ // Re-export commonly used functions
10
+ export {
11
+ detectCamoufoxPath,
12
+ ensureCamoufox,
13
+ launchBrowser,
14
+ stopBrowser,
15
+ getBrowserStatus,
16
+ isBrowserRunning,
17
+ getPlaywrightBrowser,
18
+ getCurrentPage,
19
+ getActiveBrowser,
20
+ } from './browser.mjs';
21
+
22
+ export {
23
+ navigateTo,
24
+ goBack,
25
+ takeScreenshot,
26
+ scrollPage,
27
+ clickElement,
28
+ typeText,
29
+ pressKey,
30
+ highlightElement,
31
+ clearHighlights,
32
+ setViewport,
33
+ getPageInfo,
34
+ getDOMSnapshot,
35
+ queryElements,
36
+ evaluateJS,
37
+ createNewPage,
38
+ listPages,
39
+ switchPage,
40
+ closePage,
41
+ mouseMove,
42
+ mouseClick,
43
+ mouseWheel,
44
+ } from './actions.mjs';
45
+
46
+ export {
47
+ waitFor,
48
+ retry,
49
+ withTimeout,
50
+ ensureUrlScheme,
51
+ looksLikeUrlToken,
52
+ getPositionals,
53
+ } from './utils.mjs';
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Core utilities
3
+ */
4
+
5
+ /**
6
+ * Wait helper
7
+ */
8
+ export function waitFor(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
11
+
12
+ /**
13
+ * Retry with backoff
14
+ */
15
+ export async function retry(fn, options = {}) {
16
+ const maxAttempts = options.maxAttempts || 3;
17
+ const delay = options.delay || 1000;
18
+ const backoff = options.backoff || 2;
19
+
20
+ let lastError;
21
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
22
+ try {
23
+ return await fn();
24
+ } catch (err) {
25
+ lastError = err;
26
+ if (attempt < maxAttempts) {
27
+ await waitFor(delay * Math.pow(backoff, attempt - 1));
28
+ }
29
+ }
30
+ }
31
+
32
+ throw lastError;
33
+ }
34
+
35
+ /**
36
+ * Timeout wrapper
37
+ */
38
+ export async function withTimeout(promise, ms, message = 'Timeout') {
39
+ const timeout = new Promise((_, reject) => {
40
+ setTimeout(() => reject(new Error(message)), ms);
41
+ });
42
+ return Promise.race([promise, timeout]);
43
+ }
44
+
45
+ /**
46
+ * Format URL (ensure scheme)
47
+ */
48
+ export function ensureUrlScheme(url) {
49
+ if (!url) return url;
50
+ if (url.startsWith('http://') || url.startsWith('https://')) return url;
51
+ if (url.startsWith('localhost') || url.match(/^\\d+\\.\\d+/)) {
52
+ return `http://${url}`;
53
+ }
54
+ return `https://${url}`;
55
+ }
56
+
57
+ /**
58
+ * Looks like URL token
59
+ */
60
+ export function looksLikeUrlToken(token) {
61
+ if (!token || typeof token !== 'string') return false;
62
+ if (token.startsWith('http://') || token.startsWith('https://')) return true;
63
+ if (token.includes('.') && !token.includes(' ')) return true;
64
+ return false;
65
+ }
66
+
67
+ /**
68
+ * Get positional args (exclude flags)
69
+ */
70
+ export function getPositionals(args, excludeFlags = []) {
71
+ const result = [];
72
+ for (let i = 0; i < args.length; i++) {
73
+ const arg = args[i];
74
+ if (arg.startsWith('--')) {
75
+ if (!excludeFlags.includes(arg)) {
76
+ i++; // skip value
77
+ }
78
+ continue;
79
+ }
80
+ if (excludeFlags.includes(arg)) {
81
+ i++; // skip value
82
+ continue;
83
+ }
84
+ result.push(arg);
85
+ }
86
+ return result;
87
+ }
@@ -0,0 +1,33 @@
1
+ import { createProgressWsServer } from './ws-server.mjs';
2
+
3
+ function readFlagValue(args, names) {
4
+ for (let i = 0; i < args.length; i += 1) {
5
+ if (!names.includes(args[i])) continue;
6
+ const value = args[i + 1];
7
+ if (!value || String(value).startsWith('-')) return null;
8
+ return value;
9
+ }
10
+ return null;
11
+ }
12
+
13
+ async function main() {
14
+ const args = process.argv.slice(2);
15
+ const host = readFlagValue(args, ['--host']) || process.env.CAMO_PROGRESS_WS_HOST || '127.0.0.1';
16
+ const port = Math.max(1, Number(readFlagValue(args, ['--port']) || process.env.CAMO_PROGRESS_WS_PORT || 7788) || 7788);
17
+ const server = createProgressWsServer({ host, port });
18
+ await server.start();
19
+
20
+ const stop = async () => {
21
+ await server.stop();
22
+ process.exit(0);
23
+ };
24
+
25
+ process.on('SIGINT', stop);
26
+ process.on('SIGTERM', stop);
27
+ await new Promise(() => {});
28
+ }
29
+
30
+ main().catch(() => {
31
+ process.exit(1);
32
+ });
33
+
@@ -0,0 +1,80 @@
1
+ import path from 'node:path';
2
+ import { spawn } from 'node:child_process';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const DEFAULT_HOST = process.env.CAMO_PROGRESS_WS_HOST || '127.0.0.1';
6
+ const DEFAULT_PORT = Math.max(1, Number(process.env.CAMO_PROGRESS_WS_PORT || 7788) || 7788);
7
+ const DEFAULT_HEALTH_TIMEOUT_MS = 800;
8
+ const DEFAULT_START_TIMEOUT_MS = 4000;
9
+ const HEALTH_POLL_INTERVAL_MS = 140;
10
+
11
+ function sleep(ms) {
12
+ return new Promise((resolve) => setTimeout(resolve, ms));
13
+ }
14
+
15
+ export function resolveProgressWsConfig(options = {}) {
16
+ const host = String(options.host || DEFAULT_HOST).trim() || DEFAULT_HOST;
17
+ const port = Math.max(1, Number(options.port || DEFAULT_PORT) || DEFAULT_PORT);
18
+ return { host, port };
19
+ }
20
+
21
+ export function buildProgressHealthUrl(options = {}) {
22
+ const { host, port } = resolveProgressWsConfig(options);
23
+ return `http://${host}:${port}/health`;
24
+ }
25
+
26
+ export async function checkProgressEventDaemon(options = {}) {
27
+ const timeoutMs = Math.max(150, Number(options.timeoutMs || DEFAULT_HEALTH_TIMEOUT_MS) || DEFAULT_HEALTH_TIMEOUT_MS);
28
+ const healthUrl = buildProgressHealthUrl(options);
29
+ try {
30
+ const response = await fetch(healthUrl, { signal: AbortSignal.timeout(timeoutMs) });
31
+ if (!response.ok) return false;
32
+ const body = await response.json().catch(() => null);
33
+ return Boolean(body?.ok);
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function getDaemonEntryPath() {
40
+ const dir = path.dirname(fileURLToPath(import.meta.url));
41
+ return path.join(dir, 'daemon-entry.mjs');
42
+ }
43
+
44
+ function spawnProgressDaemon({ host, port }) {
45
+ const entry = getDaemonEntryPath();
46
+ const child = spawn(
47
+ process.execPath,
48
+ [entry, '--host', host, '--port', String(port)],
49
+ {
50
+ detached: true,
51
+ stdio: 'ignore',
52
+ env: {
53
+ ...process.env,
54
+ CAMO_PROGRESS_DAEMON: '1',
55
+ },
56
+ },
57
+ );
58
+ child.unref();
59
+ }
60
+
61
+ export async function ensureProgressEventDaemon(options = {}) {
62
+ const { host, port } = resolveProgressWsConfig(options);
63
+ const startTimeoutMs = Math.max(400, Number(options.startTimeoutMs || DEFAULT_START_TIMEOUT_MS) || DEFAULT_START_TIMEOUT_MS);
64
+ if (await checkProgressEventDaemon({ host, port })) {
65
+ return { ok: true, started: false, host, port };
66
+ }
67
+
68
+ spawnProgressDaemon({ host, port });
69
+
70
+ const deadline = Date.now() + startTimeoutMs;
71
+ while (Date.now() < deadline) {
72
+ if (await checkProgressEventDaemon({ host, port })) {
73
+ return { ok: true, started: true, host, port };
74
+ }
75
+ await sleep(HEALTH_POLL_INTERVAL_MS);
76
+ }
77
+
78
+ return { ok: false, started: true, host, port, error: 'progress_daemon_start_timeout' };
79
+ }
80
+
@@ -0,0 +1,109 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { CONFIG_DIR, ensureDir } from '../utils/config.mjs';
4
+
5
+ const DEFAULT_EVENTS_DIR = path.join(CONFIG_DIR, 'run', 'events');
6
+ const DEFAULT_EVENTS_FILE = path.join(DEFAULT_EVENTS_DIR, 'progress-events.jsonl');
7
+ const MAX_REPLAY_BYTES = Math.max(64 * 1024, Number(process.env.CAMO_PROGRESS_REPLAY_MAX_BYTES) || (2 * 1024 * 1024));
8
+
9
+ let localSeq = 0;
10
+
11
+ function resolveEventsFile() {
12
+ const raw = String(process.env.CAMO_PROGRESS_EVENTS_FILE || DEFAULT_EVENTS_FILE).trim();
13
+ return raw || DEFAULT_EVENTS_FILE;
14
+ }
15
+
16
+ function nextSeq() {
17
+ localSeq = (localSeq + 1) % 1_000_000_000;
18
+ return `${Date.now()}-${process.pid}-${localSeq}`;
19
+ }
20
+
21
+ function normalizePayload(payload) {
22
+ if (payload === undefined) return null;
23
+ if (payload === null) return null;
24
+ if (typeof payload === 'object') return payload;
25
+ return { value: payload };
26
+ }
27
+
28
+ export function getProgressEventsFile() {
29
+ return resolveEventsFile();
30
+ }
31
+
32
+ export function ensureProgressEventStore() {
33
+ const eventFile = resolveEventsFile();
34
+ ensureDir(path.dirname(eventFile));
35
+ if (!fs.existsSync(eventFile)) {
36
+ fs.writeFileSync(eventFile, '', 'utf8');
37
+ }
38
+ return eventFile;
39
+ }
40
+
41
+ export function buildProgressEvent({
42
+ ts = null,
43
+ seq = null,
44
+ source = 'camo',
45
+ mode = 'normal',
46
+ profileId = null,
47
+ runId = null,
48
+ event = 'unknown',
49
+ payload = null,
50
+ } = {}) {
51
+ return {
52
+ ts: ts || new Date().toISOString(),
53
+ seq: seq || nextSeq(),
54
+ source: String(source || 'camo'),
55
+ mode: String(mode || 'normal'),
56
+ profileId: profileId ? String(profileId) : null,
57
+ runId: runId ? String(runId) : null,
58
+ event: String(event || 'unknown'),
59
+ payload: normalizePayload(payload),
60
+ };
61
+ }
62
+
63
+ export function appendProgressEvent(input = {}) {
64
+ const eventFile = ensureProgressEventStore();
65
+ const event = buildProgressEvent(input);
66
+ fs.appendFileSync(eventFile, `${JSON.stringify(event)}\n`, 'utf8');
67
+ return event;
68
+ }
69
+
70
+ export function safeAppendProgressEvent(input = {}) {
71
+ try {
72
+ return appendProgressEvent(input);
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ export function readRecentProgressEvents(limit = 100) {
79
+ const eventFile = ensureProgressEventStore();
80
+ const maxItems = Math.max(0, Number(limit) || 0);
81
+ if (maxItems === 0) return [];
82
+ const stat = fs.statSync(eventFile);
83
+ if (stat.size <= 0) return [];
84
+
85
+ const start = Math.max(0, stat.size - MAX_REPLAY_BYTES);
86
+ const fd = fs.openSync(eventFile, 'r');
87
+ try {
88
+ const length = stat.size - start;
89
+ const buffer = Buffer.alloc(length);
90
+ fs.readSync(fd, buffer, 0, length, start);
91
+ const raw = buffer.toString('utf8');
92
+ return raw
93
+ .split('\n')
94
+ .map((line) => line.trim())
95
+ .filter(Boolean)
96
+ .slice(-maxItems)
97
+ .map((line) => {
98
+ try {
99
+ return JSON.parse(line);
100
+ } catch {
101
+ return null;
102
+ }
103
+ })
104
+ .filter(Boolean);
105
+ } finally {
106
+ fs.closeSync(fd);
107
+ }
108
+ }
109
+