@web-auto/camo 0.1.4 → 0.1.5

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/README.md CHANGED
@@ -68,12 +68,16 @@ camo create fingerprint --os <os> --region <region>
68
68
  ### Browser Control
69
69
 
70
70
  ```bash
71
- camo start [profileId] [--url <url>] [--headless]
71
+ camo start [profileId] [--url <url>] [--headless] [--width <w> --height <h>]
72
72
  camo stop [profileId]
73
73
  camo status [profileId]
74
74
  camo shutdown # Shutdown browser-service (all sessions)
75
75
  ```
76
76
 
77
+ `camo start` in headful mode now persists window size per profile and reuses that size on next start.
78
+ If no saved size exists, it defaults to near-fullscreen (full width, slight vertical reserve).
79
+ Use `--width/--height` to override and update the saved profile size.
80
+
77
81
  ### Lifecycle & Cleanup
78
82
 
79
83
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web-auto/camo",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Camoufox Browser CLI - Cross-platform browser automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,11 +1,145 @@
1
1
  import fs from 'node:fs';
2
- import { listProfiles, getDefaultProfile } from '../utils/config.mjs';
2
+ import {
3
+ listProfiles,
4
+ getDefaultProfile,
5
+ getProfileWindowSize,
6
+ setProfileWindowSize,
7
+ } from '../utils/config.mjs';
3
8
  import { callAPI, ensureCamoufox, ensureBrowserService, getSessionByProfile, checkBrowserService } from '../utils/browser-service.mjs';
4
9
  import { resolveProfileId, ensureUrlScheme, looksLikeUrlToken, getPositionals } from '../utils/args.mjs';
5
10
  import { acquireLock, releaseLock, isLocked, getLockInfo, cleanupStaleLocks } from '../lifecycle/lock.mjs';
6
11
  import { registerSession, updateSession, getSessionInfo, unregisterSession, listRegisteredSessions, markSessionClosed, cleanupStaleSessions, recoverSession } from '../lifecycle/session-registry.mjs';
7
12
  import { startSessionWatchdog, stopAllSessionWatchdogs, stopSessionWatchdog } from '../lifecycle/session-watchdog.mjs';
8
13
 
14
+ const START_WINDOW_MIN_WIDTH = 960;
15
+ const START_WINDOW_MIN_HEIGHT = 700;
16
+ const START_WINDOW_MAX_RESERVE = 240;
17
+ const START_WINDOW_DEFAULT_RESERVE = 72;
18
+
19
+ function sleep(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
22
+
23
+ function parseNumber(value, fallback = 0) {
24
+ const parsed = Number(value);
25
+ return Number.isFinite(parsed) ? parsed : fallback;
26
+ }
27
+
28
+ function clamp(value, min, max) {
29
+ return Math.min(Math.max(value, min), max);
30
+ }
31
+
32
+ export function computeTargetViewportFromWindowMetrics(measured) {
33
+ const innerWidth = Math.max(320, parseNumber(measured?.innerWidth, 0));
34
+ const innerHeight = Math.max(240, parseNumber(measured?.innerHeight, 0));
35
+ const outerWidth = Math.max(320, parseNumber(measured?.outerWidth, innerWidth));
36
+ const outerHeight = Math.max(240, parseNumber(measured?.outerHeight, innerHeight));
37
+
38
+ const rawDeltaW = Math.max(0, outerWidth - innerWidth);
39
+ const rawDeltaH = Math.max(0, outerHeight - innerHeight);
40
+ const frameW = rawDeltaW > 400 ? 16 : Math.min(rawDeltaW, 120);
41
+ const frameH = rawDeltaH > 400 ? 88 : Math.min(rawDeltaH, 180);
42
+
43
+ return {
44
+ width: Math.max(320, outerWidth - frameW),
45
+ height: Math.max(240, outerHeight - frameH),
46
+ frameW,
47
+ frameH,
48
+ innerWidth,
49
+ innerHeight,
50
+ outerWidth,
51
+ outerHeight,
52
+ };
53
+ }
54
+
55
+ export function computeStartWindowSize(metrics, options = {}) {
56
+ const display = metrics?.metrics || metrics || {};
57
+ const reserveFromEnv = parseNumber(process.env.CAMO_DEFAULT_WINDOW_VERTICAL_RESERVE, START_WINDOW_DEFAULT_RESERVE);
58
+ const reserve = clamp(
59
+ parseNumber(options.reservePx, reserveFromEnv),
60
+ 0,
61
+ START_WINDOW_MAX_RESERVE,
62
+ );
63
+
64
+ const workWidth = parseNumber(display.workWidth, 0);
65
+ const workHeight = parseNumber(display.workHeight, 0);
66
+ const width = parseNumber(display.width, 0);
67
+ const height = parseNumber(display.height, 0);
68
+ const baseW = Math.floor(workWidth > 0 ? workWidth : width);
69
+ const baseH = Math.floor(workHeight > 0 ? workHeight : height);
70
+
71
+ if (baseW <= 0 || baseH <= 0) {
72
+ return {
73
+ width: 1920,
74
+ height: 1000,
75
+ reservePx: reserve,
76
+ source: 'fallback',
77
+ };
78
+ }
79
+
80
+ return {
81
+ width: Math.max(START_WINDOW_MIN_WIDTH, baseW),
82
+ height: Math.max(START_WINDOW_MIN_HEIGHT, baseH - reserve),
83
+ reservePx: reserve,
84
+ source: workWidth > 0 || workHeight > 0 ? 'workArea' : 'screen',
85
+ };
86
+ }
87
+
88
+ async function probeWindowMetrics(profileId) {
89
+ const measured = await callAPI('evaluate', {
90
+ profileId,
91
+ script: '({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight })',
92
+ });
93
+ return measured?.result || {};
94
+ }
95
+
96
+ export async function syncWindowViewportAfterResize(profileId, width, height, options = {}) {
97
+ const settleMs = Math.max(40, parseNumber(options.settleMs, 120));
98
+ const attempts = Math.max(1, Math.min(8, Math.floor(parseNumber(options.attempts, 4))));
99
+ const tolerancePx = Math.max(0, parseNumber(options.tolerancePx, 3));
100
+
101
+ const windowResult = await callAPI('window:resize', { profileId, width, height });
102
+ await sleep(settleMs);
103
+
104
+ let measured = {};
105
+ let verified = {};
106
+ let viewport = null;
107
+ let matched = false;
108
+ let target = { width: 1280, height: 720, frameW: 16, frameH: 88 };
109
+
110
+ for (let i = 0; i < attempts; i += 1) {
111
+ measured = await probeWindowMetrics(profileId);
112
+ target = computeTargetViewportFromWindowMetrics(measured);
113
+ viewport = await callAPI('page:setViewport', {
114
+ profileId,
115
+ width: target.width,
116
+ height: target.height,
117
+ });
118
+ await sleep(settleMs);
119
+ verified = await probeWindowMetrics(profileId);
120
+ const dw = Math.abs(parseNumber(verified?.innerWidth, 0) - target.width);
121
+ const dh = Math.abs(parseNumber(verified?.innerHeight, 0) - target.height);
122
+ if (dw <= tolerancePx && dh <= tolerancePx) {
123
+ matched = true;
124
+ break;
125
+ }
126
+ }
127
+
128
+ return {
129
+ window: windowResult,
130
+ measured,
131
+ verified,
132
+ targetViewport: {
133
+ width: target.width,
134
+ height: target.height,
135
+ frameW: target.frameW,
136
+ frameH: target.frameH,
137
+ matched,
138
+ },
139
+ viewport,
140
+ };
141
+ }
142
+
9
143
  export async function handleStartCommand(args) {
10
144
  ensureCamoufox();
11
145
  await ensureBrowserService();
@@ -14,6 +148,19 @@ export async function handleStartCommand(args) {
14
148
 
15
149
  const urlIdx = args.indexOf('--url');
16
150
  const explicitUrl = urlIdx >= 0 ? args[urlIdx + 1] : undefined;
151
+ const widthIdx = args.indexOf('--width');
152
+ const heightIdx = args.indexOf('--height');
153
+ const explicitWidth = widthIdx >= 0 ? parseNumber(args[widthIdx + 1], NaN) : NaN;
154
+ const explicitHeight = heightIdx >= 0 ? parseNumber(args[heightIdx + 1], NaN) : NaN;
155
+ const hasExplicitWidth = Number.isFinite(explicitWidth);
156
+ const hasExplicitHeight = Number.isFinite(explicitHeight);
157
+ if (hasExplicitWidth !== hasExplicitHeight) {
158
+ throw new Error('Usage: camo start [profileId] [--url <url>] [--headless] [--width <w> --height <h>]');
159
+ }
160
+ if ((hasExplicitWidth && explicitWidth < START_WINDOW_MIN_WIDTH) || (hasExplicitHeight && explicitHeight < START_WINDOW_MIN_HEIGHT)) {
161
+ throw new Error(`Window size too small. Minimum is ${START_WINDOW_MIN_WIDTH}x${START_WINDOW_MIN_HEIGHT}`);
162
+ }
163
+ const hasExplicitWindowSize = hasExplicitWidth && hasExplicitHeight;
17
164
  const profileSet = new Set(listProfiles());
18
165
  let implicitUrl;
19
166
 
@@ -21,6 +168,7 @@ export async function handleStartCommand(args) {
21
168
  for (let i = 1; i < args.length; i++) {
22
169
  const arg = args[i];
23
170
  if (arg === '--url') { i++; continue; }
171
+ if (arg === '--width' || arg === '--height') { i++; continue; }
24
172
  if (arg === '--headless') continue;
25
173
  if (arg.startsWith('--')) continue;
26
174
 
@@ -87,6 +235,52 @@ export async function handleStartCommand(args) {
87
235
  headless,
88
236
  });
89
237
  startSessionWatchdog(profileId);
238
+
239
+ if (!headless) {
240
+ let windowTarget = null;
241
+ if (hasExplicitWindowSize) {
242
+ windowTarget = {
243
+ width: Math.floor(explicitWidth),
244
+ height: Math.floor(explicitHeight),
245
+ source: 'explicit',
246
+ };
247
+ } else {
248
+ const rememberedWindow = getProfileWindowSize(profileId);
249
+ if (rememberedWindow) {
250
+ windowTarget = {
251
+ width: rememberedWindow.width,
252
+ height: rememberedWindow.height,
253
+ source: 'profile',
254
+ updatedAt: rememberedWindow.updatedAt,
255
+ };
256
+ } else {
257
+ const display = await callAPI('system:display', {}).catch(() => null);
258
+ windowTarget = computeStartWindowSize(display);
259
+ }
260
+ }
261
+
262
+ result.startWindow = {
263
+ width: windowTarget.width,
264
+ height: windowTarget.height,
265
+ source: windowTarget.source,
266
+ };
267
+
268
+ const syncResult = await syncWindowViewportAfterResize(
269
+ profileId,
270
+ windowTarget.width,
271
+ windowTarget.height,
272
+ ).catch((err) => ({ error: err?.message || String(err) }));
273
+ result.windowSync = syncResult;
274
+
275
+ const measuredOuterWidth = Number(syncResult?.verified?.outerWidth);
276
+ const measuredOuterHeight = Number(syncResult?.verified?.outerHeight);
277
+ const savedWindow = setProfileWindowSize(
278
+ profileId,
279
+ Number.isFinite(measuredOuterWidth) ? measuredOuterWidth : windowTarget.width,
280
+ Number.isFinite(measuredOuterHeight) ? measuredOuterHeight : windowTarget.height,
281
+ );
282
+ result.profileWindow = savedWindow?.window || null;
283
+ }
90
284
  }
91
285
  console.log(JSON.stringify(result, null, 2));
92
286
  }
@@ -1,7 +1,38 @@
1
- import { getDefaultProfile } from '../utils/config.mjs';
1
+ import { getDefaultProfile, setProfileWindowSize } from '../utils/config.mjs';
2
2
  import { callAPI, ensureBrowserService } from '../utils/browser-service.mjs';
3
3
  import { getPositionals } from '../utils/args.mjs';
4
4
 
5
+ function sleep(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+
9
+ function computeTargetViewport(measured) {
10
+ const innerWidth = Math.max(320, Number(measured?.innerWidth || 0) || 0);
11
+ const innerHeight = Math.max(240, Number(measured?.innerHeight || 0) || 0);
12
+ const outerWidth = Math.max(320, Number(measured?.outerWidth || 0) || innerWidth);
13
+ const outerHeight = Math.max(240, Number(measured?.outerHeight || 0) || innerHeight);
14
+
15
+ const rawDeltaW = Math.max(0, outerWidth - innerWidth);
16
+ const rawDeltaH = Math.max(0, outerHeight - innerHeight);
17
+ const frameW = rawDeltaW > 400 ? 16 : Math.min(rawDeltaW, 120);
18
+ const frameH = rawDeltaH > 400 ? 88 : Math.min(rawDeltaH, 180);
19
+
20
+ return {
21
+ width: Math.max(320, outerWidth - frameW),
22
+ height: Math.max(240, outerHeight - frameH),
23
+ frameW,
24
+ frameH,
25
+ };
26
+ }
27
+
28
+ async function probeWindowMetrics(profileId) {
29
+ const measured = await callAPI('evaluate', {
30
+ profileId,
31
+ script: '({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight })',
32
+ });
33
+ return measured?.result || {};
34
+ }
35
+
5
36
  export async function handleWindowCommand(args) {
6
37
  await ensureBrowserService();
7
38
 
@@ -24,36 +55,66 @@ export async function handleWindowCommand(args) {
24
55
  const width = parseInt(args[widthIdx + 1]);
25
56
  const height = parseInt(args[heightIdx + 1]);
26
57
  const result = await callAPI('window:resize', { profileId, width, height });
27
- const measured = await callAPI('evaluate', {
28
- profileId,
29
- script: '({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight })',
30
- });
31
- const innerWidth = Math.max(320, Number(measured?.result?.innerWidth || 0) || 0);
32
- const innerHeight = Math.max(240, Number(measured?.result?.innerHeight || 0) || 0);
33
- const outerWidth = Math.max(320, Number(measured?.result?.outerWidth || 0) || innerWidth);
34
- const outerHeight = Math.max(240, Number(measured?.result?.outerHeight || 0) || innerHeight);
35
- const rawDeltaW = Math.max(0, outerWidth - innerWidth);
36
- const rawDeltaH = Math.max(0, outerHeight - innerHeight);
37
- const frameW = rawDeltaW > 400 ? 16 : Math.min(rawDeltaW, 120);
38
- const frameH = rawDeltaH > 400 ? 88 : Math.min(rawDeltaH, 180);
39
- const targetViewportWidth = Math.max(320, outerWidth - frameW);
40
- const targetViewportHeight = Math.max(240, outerHeight - frameH);
41
- const viewport = await callAPI('page:setViewport', {
58
+
59
+ const attempts = 4;
60
+ const settleMs = 120;
61
+ const tolerancePx = 3;
62
+ let measured = {};
63
+ let verified = {};
64
+ let viewport = null;
65
+ let targetViewportWidth = 1280;
66
+ let targetViewportHeight = 720;
67
+ let frameW = 16;
68
+ let frameH = 88;
69
+ let matched = false;
70
+
71
+ await sleep(settleMs);
72
+ for (let i = 0; i < attempts; i += 1) {
73
+ measured = await probeWindowMetrics(profileId);
74
+ const target = computeTargetViewport(measured);
75
+ targetViewportWidth = target.width;
76
+ targetViewportHeight = target.height;
77
+ frameW = target.frameW;
78
+ frameH = target.frameH;
79
+
80
+ viewport = await callAPI('page:setViewport', {
81
+ profileId,
82
+ width: targetViewportWidth,
83
+ height: targetViewportHeight,
84
+ });
85
+
86
+ await sleep(settleMs);
87
+ verified = await probeWindowMetrics(profileId);
88
+ const dw = Math.abs((Number(verified?.innerWidth || 0)) - targetViewportWidth);
89
+ const dh = Math.abs((Number(verified?.innerHeight || 0)) - targetViewportHeight);
90
+ if (dw <= tolerancePx && dh <= tolerancePx) {
91
+ matched = true;
92
+ break;
93
+ }
94
+ }
95
+
96
+ const measuredOuterWidth = Number(verified?.outerWidth);
97
+ const measuredOuterHeight = Number(verified?.outerHeight);
98
+ const savedWindow = setProfileWindowSize(
42
99
  profileId,
43
- width: targetViewportWidth,
44
- height: targetViewportHeight,
45
- });
100
+ Number.isFinite(measuredOuterWidth) ? measuredOuterWidth : width,
101
+ Number.isFinite(measuredOuterHeight) ? measuredOuterHeight : height,
102
+ );
103
+
46
104
  console.log(JSON.stringify({
47
105
  ok: true,
48
106
  profileId,
49
107
  window: result,
50
- measured: measured?.result || null,
108
+ measured: measured || null,
109
+ verified: verified || null,
51
110
  targetViewport: {
52
111
  width: targetViewportWidth,
53
112
  height: targetViewportHeight,
54
113
  frameW,
55
114
  frameH,
115
+ matched,
56
116
  },
117
+ profileWindow: savedWindow?.window || null,
57
118
  viewport,
58
119
  }, null, 2));
59
120
  } else {
@@ -63,6 +63,45 @@ function isBlankUrl(url) {
63
63
  return text === '' || text === 'about:blank' || text === 'about:blank#blocked';
64
64
  }
65
65
 
66
+ function computeTargetViewportFromWindow(measured) {
67
+ const innerWidth = Math.max(320, Number(measured?.innerWidth || 0) || 0);
68
+ const innerHeight = Math.max(240, Number(measured?.innerHeight || 0) || 0);
69
+ const outerWidth = Math.max(320, Number(measured?.outerWidth || 0) || innerWidth);
70
+ const outerHeight = Math.max(240, Number(measured?.outerHeight || 0) || innerHeight);
71
+ const rawDeltaW = Math.max(0, outerWidth - innerWidth);
72
+ const rawDeltaH = Math.max(0, outerHeight - innerHeight);
73
+ const frameW = rawDeltaW > 400 ? 16 : Math.min(rawDeltaW, 120);
74
+ const frameH = rawDeltaH > 400 ? 88 : Math.min(rawDeltaH, 180);
75
+ return {
76
+ width: Math.max(320, outerWidth - frameW),
77
+ height: Math.max(240, outerHeight - frameH),
78
+ innerWidth,
79
+ innerHeight,
80
+ };
81
+ }
82
+
83
+ async function probeWindowMetrics(profileId) {
84
+ const measured = await callAPI('evaluate', {
85
+ profileId,
86
+ script: '({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight })',
87
+ });
88
+ return measured?.result || {};
89
+ }
90
+
91
+ async function syncViewportWithWindow(profileId, tolerancePx = 3) {
92
+ const measured = await probeWindowMetrics(profileId);
93
+ const target = computeTargetViewportFromWindow(measured);
94
+ const dw = Math.abs(target.innerWidth - target.width);
95
+ const dh = Math.abs(target.innerHeight - target.height);
96
+ if (dw <= tolerancePx && dh <= tolerancePx) return false;
97
+ await callAPI('page:setViewport', {
98
+ profileId,
99
+ width: target.width,
100
+ height: target.height,
101
+ });
102
+ return true;
103
+ }
104
+
66
105
  function shouldExitMonitor(profileId) {
67
106
  const info = getSessionInfo(profileId);
68
107
  return !info || info.status !== 'active';
@@ -78,6 +117,8 @@ async function runMonitor(profileId, options = {}) {
78
117
  const intervalMs = Math.max(500, Number(options.intervalMs) || 1200);
79
118
  const emptyThreshold = Math.max(1, Number(options.emptyThreshold) || 2);
80
119
  const blankThreshold = Math.max(1, Number(options.blankThreshold) || 3);
120
+ const viewportSyncIntervalMs = Math.max(500, Number(options.viewportSyncIntervalMs) || 1500);
121
+ let lastViewportSyncAt = 0;
81
122
 
82
123
  let seenAnyPage = false;
83
124
  let seenNonBlankPage = false;
@@ -140,6 +181,12 @@ async function runMonitor(profileId, options = {}) {
140
181
  return;
141
182
  }
142
183
 
184
+ const now = Date.now();
185
+ if (pages.length > 0 && now - lastViewportSyncAt >= viewportSyncIntervalMs) {
186
+ lastViewportSyncAt = now;
187
+ await syncViewportWithWindow(profileId).catch(() => {});
188
+ }
189
+
143
190
  await sleep(intervalMs);
144
191
  }
145
192
  }
@@ -6,6 +6,7 @@ import os from 'node:os';
6
6
  export const CONFIG_DIR = path.join(os.homedir(), '.webauto');
7
7
  export const PROFILES_DIR = path.join(CONFIG_DIR, 'profiles');
8
8
  export const CONFIG_FILE = path.join(CONFIG_DIR, 'camo-cli.json');
9
+ export const PROFILE_META_FILE = 'camo-profile.json';
9
10
  export const BROWSER_SERVICE_URL = process.env.WEBAUTO_BROWSER_URL || 'http://127.0.0.1:7704';
10
11
 
11
12
  export function ensureDir(p) {
@@ -55,13 +56,13 @@ export function createProfile(profileId) {
55
56
  if (!isValidProfileId(profileId)) {
56
57
  throw new Error('Invalid profileId. Use only letters, numbers, dot, underscore, dash.');
57
58
  }
58
- const profileDir = path.join(PROFILES_DIR, profileId);
59
+ const profileDir = getProfileDir(profileId);
59
60
  if (fs.existsSync(profileDir)) throw new Error(`Profile already exists: ${profileId}`);
60
61
  ensureDir(profileDir);
61
62
  }
62
63
 
63
64
  export function deleteProfile(profileId) {
64
- const profileDir = path.join(PROFILES_DIR, profileId);
65
+ const profileDir = getProfileDir(profileId);
65
66
  if (!fs.existsSync(profileDir)) throw new Error(`Profile not found: ${profileId}`);
66
67
  fs.rmSync(profileDir, { recursive: true, force: true });
67
68
  }
@@ -82,6 +83,61 @@ export function getDefaultProfile() {
82
83
  return loadConfig().defaultProfile;
83
84
  }
84
85
 
86
+ export function getProfileDir(profileId) {
87
+ return path.join(PROFILES_DIR, String(profileId || '').trim());
88
+ }
89
+
90
+ export function getProfileMetaFile(profileId) {
91
+ return path.join(getProfileDir(profileId), PROFILE_META_FILE);
92
+ }
93
+
94
+ function loadProfileMeta(profileId) {
95
+ if (!isValidProfileId(profileId)) return {};
96
+ return readJson(getProfileMetaFile(profileId)) || {};
97
+ }
98
+
99
+ function saveProfileMeta(profileId, patch) {
100
+ if (!isValidProfileId(profileId)) return null;
101
+ const profileDir = getProfileDir(profileId);
102
+ ensureDir(profileDir);
103
+ const current = loadProfileMeta(profileId);
104
+ const next = {
105
+ ...current,
106
+ ...patch,
107
+ updatedAt: Date.now(),
108
+ };
109
+ writeJson(getProfileMetaFile(profileId), next);
110
+ return next;
111
+ }
112
+
113
+ export function getProfileWindowSize(profileId) {
114
+ const meta = loadProfileMeta(profileId);
115
+ const width = Number(meta?.window?.width);
116
+ const height = Number(meta?.window?.height);
117
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
118
+ if (width < 320 || height < 240) return null;
119
+ return {
120
+ width: Math.floor(width),
121
+ height: Math.floor(height),
122
+ updatedAt: Number(meta?.window?.updatedAt) || Number(meta?.updatedAt) || null,
123
+ };
124
+ }
125
+
126
+ export function setProfileWindowSize(profileId, width, height) {
127
+ const parsedWidth = Number(width);
128
+ const parsedHeight = Number(height);
129
+ if (!Number.isFinite(parsedWidth) || !Number.isFinite(parsedHeight)) return null;
130
+ if (parsedWidth < 320 || parsedHeight < 240) return null;
131
+ const now = Date.now();
132
+ return saveProfileMeta(profileId, {
133
+ window: {
134
+ width: Math.floor(parsedWidth),
135
+ height: Math.floor(parsedHeight),
136
+ updatedAt: now,
137
+ },
138
+ });
139
+ }
140
+
85
141
  const START_SCRIPT_REL = path.join('runtime', 'infra', 'utils', 'scripts', 'service', 'start-browser-service.mjs');
86
142
 
87
143
  export function hasStartScript(root) {
@@ -24,7 +24,7 @@ CONFIG:
24
24
 
25
25
  BROWSER CONTROL:
26
26
  init Ensure camoufox + ensure browser-service daemon
27
- start [profileId] [--url <url>] [--headless]
27
+ start [profileId] [--url <url>] [--headless] [--width <w> --height <h>]
28
28
  stop [profileId]
29
29
  status [profileId]
30
30
  list Alias of status
@@ -91,6 +91,7 @@ EXAMPLES:
91
91
  camo profile create myprofile
92
92
  camo profile default myprofile
93
93
  camo start --url https://example.com
94
+ camo start myprofile --width 1920 --height 1020
94
95
  camo goto https://www.xiaohongshu.com
95
96
  camo scroll --down --amount 500
96
97
  camo click "#search-input"