@web-auto/camo 0.2.0 → 0.2.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 (114) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +586 -586
  3. package/bin/browser-service.mjs +11 -11
  4. package/bin/camo.mjs +22 -22
  5. package/package.json +48 -48
  6. package/scripts/build.mjs +19 -19
  7. package/scripts/bump-version.mjs +34 -34
  8. package/scripts/check-file-size.mjs +80 -80
  9. package/scripts/file-size-policy.json +12 -2
  10. package/scripts/install.mjs +76 -76
  11. package/scripts/release.sh +54 -54
  12. package/src/autoscript/action-providers/index.mjs +6 -6
  13. package/src/autoscript/impact-engine.mjs +78 -78
  14. package/src/autoscript/runtime.mjs +1017 -1017
  15. package/src/autoscript/schema.mjs +376 -376
  16. package/src/cli.mjs +405 -405
  17. package/src/commands/attach.mjs +141 -141
  18. package/src/commands/autoscript.mjs +1011 -1011
  19. package/src/commands/browser.mjs +1255 -1255
  20. package/src/commands/container.mjs +401 -401
  21. package/src/commands/cookies.mjs +69 -69
  22. package/src/commands/create.mjs +98 -98
  23. package/src/commands/devtools.mjs +349 -349
  24. package/src/commands/events.mjs +152 -152
  25. package/src/commands/highlight-mode.mjs +24 -24
  26. package/src/commands/init.mjs +68 -68
  27. package/src/commands/lifecycle.mjs +275 -275
  28. package/src/commands/mouse.mjs +45 -45
  29. package/src/commands/profile.mjs +46 -46
  30. package/src/commands/record.mjs +115 -115
  31. package/src/commands/system.mjs +14 -14
  32. package/src/commands/window.mjs +123 -123
  33. package/src/container/change-notifier.mjs +362 -362
  34. package/src/container/element-filter.mjs +143 -143
  35. package/src/container/index.mjs +3 -3
  36. package/src/container/runtime-core/checkpoint.mjs +209 -209
  37. package/src/container/runtime-core/index.mjs +21 -21
  38. package/src/container/runtime-core/operations/index.mjs +774 -774
  39. package/src/container/runtime-core/operations/selector-scripts.mjs +277 -277
  40. package/src/container/runtime-core/operations/tab-pool.mjs +746 -746
  41. package/src/container/runtime-core/operations/viewport.mjs +189 -189
  42. package/src/container/runtime-core/search.mjs +190 -190
  43. package/src/container/runtime-core/subscription.mjs +224 -224
  44. package/src/container/runtime-core/utils.mjs +94 -94
  45. package/src/container/runtime-core/validation.mjs +127 -127
  46. package/src/container/runtime-core.mjs +1 -1
  47. package/src/container/subscription-registry.mjs +459 -459
  48. package/src/core/actions.mjs +561 -561
  49. package/src/core/browser.mjs +266 -266
  50. package/src/core/index.mjs +52 -52
  51. package/src/core/utils.mjs +91 -91
  52. package/src/events/daemon-entry.mjs +33 -33
  53. package/src/events/daemon.mjs +80 -80
  54. package/src/events/progress-log.mjs +109 -109
  55. package/src/events/ws-server.mjs +239 -239
  56. package/src/lib/client.mjs +200 -200
  57. package/src/lifecycle/cleanup.mjs +83 -83
  58. package/src/lifecycle/lock.mjs +126 -126
  59. package/src/lifecycle/session-registry.mjs +279 -279
  60. package/src/lifecycle/session-view.mjs +76 -76
  61. package/src/lifecycle/session-watchdog.mjs +281 -281
  62. package/src/services/browser-service/index.js +671 -671
  63. package/src/services/browser-service/internal/BrowserSession.input.test.js +389 -389
  64. package/src/services/browser-service/internal/BrowserSession.js +325 -304
  65. package/src/services/browser-service/internal/ElementRegistry.js +60 -60
  66. package/src/services/browser-service/internal/ProfileLock.js +84 -84
  67. package/src/services/browser-service/internal/SessionManager.js +184 -184
  68. package/src/services/browser-service/internal/SessionManager.test.js +39 -39
  69. package/src/services/browser-service/internal/browser-session/cookies.js +144 -144
  70. package/src/services/browser-service/internal/browser-session/input-ops.js +222 -222
  71. package/src/services/browser-service/internal/browser-session/input-pipeline.js +144 -144
  72. package/src/services/browser-service/internal/browser-session/logging.js +46 -46
  73. package/src/services/browser-service/internal/browser-session/navigation.js +38 -38
  74. package/src/services/browser-service/internal/browser-session/page-hooks.js +442 -442
  75. package/src/services/browser-service/internal/browser-session/page-management.js +302 -302
  76. package/src/services/browser-service/internal/browser-session/page-management.test.js +148 -148
  77. package/src/services/browser-service/internal/browser-session/recording.js +198 -198
  78. package/src/services/browser-service/internal/browser-session/runtime-events.js +61 -61
  79. package/src/services/browser-service/internal/browser-session/session-core.js +84 -84
  80. package/src/services/browser-service/internal/browser-session/session-state.js +38 -38
  81. package/src/services/browser-service/internal/browser-session/types.js +14 -14
  82. package/src/services/browser-service/internal/browser-session/utils.js +95 -95
  83. package/src/services/browser-service/internal/browser-session/viewport-manager.js +46 -46
  84. package/src/services/browser-service/internal/browser-session/viewport.js +215 -215
  85. package/src/services/browser-service/internal/container-matcher.js +851 -851
  86. package/src/services/browser-service/internal/container-registry.js +182 -182
  87. package/src/services/browser-service/internal/engine-manager.js +259 -259
  88. package/src/services/browser-service/internal/fingerprint.js +203 -203
  89. package/src/services/browser-service/internal/heartbeat.js +137 -137
  90. package/src/services/browser-service/internal/logging.js +46 -46
  91. package/src/services/browser-service/internal/page-runtime/runtime.js +1317 -1317
  92. package/src/services/browser-service/internal/pageRuntime.js +28 -28
  93. package/src/services/browser-service/internal/runtimeInjector.js +31 -31
  94. package/src/services/browser-service/internal/service-process-logger.js +140 -140
  95. package/src/services/browser-service/internal/state-bus.js +45 -45
  96. package/src/services/browser-service/internal/storage-paths.js +42 -42
  97. package/src/services/browser-service/internal/ws-server.js +1194 -1194
  98. package/src/services/browser-service/internal/ws-server.test.js +58 -58
  99. package/src/services/browser-service/server.mjs +6 -6
  100. package/src/services/controller/cli-bridge.js +93 -93
  101. package/src/services/controller/container-index.js +50 -50
  102. package/src/services/controller/container-storage.js +36 -36
  103. package/src/services/controller/controller-actions.js +207 -207
  104. package/src/services/controller/controller.js +1138 -1138
  105. package/src/services/controller/selectors.js +54 -54
  106. package/src/services/controller/transport.js +125 -125
  107. package/src/utils/args.mjs +26 -26
  108. package/src/utils/browser-service.mjs +544 -544
  109. package/src/utils/command-log.mjs +64 -64
  110. package/src/utils/config.mjs +214 -214
  111. package/src/utils/fingerprint.mjs +181 -181
  112. package/src/utils/help.mjs +216 -216
  113. package/src/utils/js-policy.mjs +13 -13
  114. package/src/utils/ws-client.mjs +30 -30
@@ -1,91 +1,91 @@
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
- // Skip special browser URLs that don't need scheme
52
- if (url.startsWith('about:') || url.startsWith('chrome:') || url.startsWith('file:')) {
53
- return url;
54
- }
55
- if (url.startsWith('localhost') || url.match(/^\\d+\\.\\d+/)) {
56
- return `http://${url}`;
57
- }
58
- return `https://${url}`;
59
- }
60
-
61
- /**
62
- * Looks like URL token
63
- */
64
- export function looksLikeUrlToken(token) {
65
- if (!token || typeof token !== 'string') return false;
66
- if (token.startsWith('http://') || token.startsWith('https://')) return true;
67
- if (token.includes('.') && !token.includes(' ')) return true;
68
- return false;
69
- }
70
-
71
- /**
72
- * Get positional args (exclude flags)
73
- */
74
- export function getPositionals(args, excludeFlags = []) {
75
- const result = [];
76
- for (let i = 0; i < args.length; i++) {
77
- const arg = args[i];
78
- if (arg.startsWith('--')) {
79
- if (!excludeFlags.includes(arg)) {
80
- i++; // skip value
81
- }
82
- continue;
83
- }
84
- if (excludeFlags.includes(arg)) {
85
- i++; // skip value
86
- continue;
87
- }
88
- result.push(arg);
89
- }
90
- return result;
91
- }
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
+ // Skip special browser URLs that don't need scheme
52
+ if (url.startsWith('about:') || url.startsWith('chrome:') || url.startsWith('file:')) {
53
+ return url;
54
+ }
55
+ if (url.startsWith('localhost') || url.match(/^\\d+\\.\\d+/)) {
56
+ return `http://${url}`;
57
+ }
58
+ return `https://${url}`;
59
+ }
60
+
61
+ /**
62
+ * Looks like URL token
63
+ */
64
+ export function looksLikeUrlToken(token) {
65
+ if (!token || typeof token !== 'string') return false;
66
+ if (token.startsWith('http://') || token.startsWith('https://')) return true;
67
+ if (token.includes('.') && !token.includes(' ')) return true;
68
+ return false;
69
+ }
70
+
71
+ /**
72
+ * Get positional args (exclude flags)
73
+ */
74
+ export function getPositionals(args, excludeFlags = []) {
75
+ const result = [];
76
+ for (let i = 0; i < args.length; i++) {
77
+ const arg = args[i];
78
+ if (arg.startsWith('--')) {
79
+ if (!excludeFlags.includes(arg)) {
80
+ i++; // skip value
81
+ }
82
+ continue;
83
+ }
84
+ if (excludeFlags.includes(arg)) {
85
+ i++; // skip value
86
+ continue;
87
+ }
88
+ result.push(arg);
89
+ }
90
+ return result;
91
+ }
@@ -1,33 +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
-
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
+
@@ -1,80 +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
-
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
+
@@ -1,109 +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
-
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
+