@web-auto/camo 0.1.7 → 0.1.9

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
@@ -5,6 +5,17 @@
5
5
 
6
6
  A cross-platform command-line interface for Camoufox browser automation.
7
7
 
8
+ ## What Camo Provides
9
+
10
+ - Browser lifecycle management: start/stop/list sessions, idle cleanup, lock cleanup.
11
+ - Profile-first automation: persistent profile dirs, fingerprint support, remembered window size.
12
+ - Browser control primitives: navigation, tabs, viewport/window, mouse and keyboard actions.
13
+ - Devtools debugging helpers: open devtools, evaluate JS quickly, collect browser console logs.
14
+ - Session recorder: JSONL interaction capture (click/input/scroll/keyboard + page visits) with runtime toggle.
15
+ - Container subscription layer: selector registration, filter/list/watch in viewport.
16
+ - Autoscript runtime: validate/explain/run/resume/mock-run with snapshot and replay.
17
+ - Progress stream: local websocket daemon (`/events`) with tail/recent/emit commands.
18
+
8
19
  ## Installation
9
20
 
10
21
  ### npm (Recommended)
@@ -42,13 +53,72 @@ camo start worker-1 --headless --alias shard1 --idle-timeout 30m
42
53
  # Start with devtools (headful only)
43
54
  camo start worker-1 --devtools
44
55
 
56
+ # Evaluate JS (devtools-style input in page context)
57
+ camo devtools eval worker-1 "document.title"
58
+
59
+ # Read captured console entries
60
+ camo devtools logs worker-1 --levels error,warn --limit 50
61
+
62
+ # Start recording into JSONL (with in-page toggle)
63
+ camo record start worker-1 --name run-a --output ./logs/run-a.jsonl --overlay
64
+
45
65
  # Navigate
46
66
  camo goto https://www.xiaohongshu.com
47
67
 
48
68
  # Interact
69
+ camo highlight-mode on
70
+ camo click "#search-input" --highlight
71
+ camo type "#search-input" "hello world" --highlight
72
+ camo scroll --down --amount 500 --selector ".feed-list"
73
+ ```
74
+
75
+ ## Core Workflows
76
+
77
+ ### 1) Interactive browser session
78
+
79
+ ```bash
80
+ camo init
81
+ camo profile create myprofile
82
+ camo profile default myprofile
83
+ camo start --url https://example.com --alias main
49
84
  camo click "#search-input"
50
85
  camo type "#search-input" "hello world"
51
- camo scroll --down --amount 500
86
+ ```
87
+
88
+ ### 2) Headless worker with idle auto-stop
89
+
90
+ ```bash
91
+ camo start worker-1 --headless --alias shard1 --idle-timeout 30m
92
+ camo instances
93
+ camo stop idle
94
+ ```
95
+
96
+ ### 3) Devtools-style debugging
97
+
98
+ ```bash
99
+ camo start myprofile --devtools
100
+ camo devtools eval myprofile "document.title"
101
+ camo devtools eval myprofile "(console.error('check-error'), location.href)"
102
+ camo devtools logs myprofile --levels error,warn --limit 50
103
+ camo devtools clear myprofile
104
+ ```
105
+
106
+ ### 4) Run autoscript with live progress
107
+
108
+ ```bash
109
+ camo autoscript validate ./autoscripts/xhs.autoscript.json
110
+ camo autoscript run ./autoscripts/xhs.autoscript.json --profile myprofile \
111
+ --jsonl-file ./runs/xhs/run.jsonl \
112
+ --summary-file ./runs/xhs/summary.json
113
+ camo events tail --profile myprofile --mode autoscript
114
+ ```
115
+
116
+ ### 5) Record manual interactions as JSONL
117
+
118
+ ```bash
119
+ camo start myprofile --record --record-name xhs-debug --record-output ./logs/xhs-debug.jsonl --record-overlay
120
+ camo record status myprofile
121
+ camo record stop myprofile
52
122
  ```
53
123
 
54
124
  ## Commands
@@ -71,10 +141,17 @@ camo init list # List available OS and regions
71
141
  camo create fingerprint --os <os> --region <region>
72
142
  ```
73
143
 
144
+ ### Config
145
+
146
+ ```bash
147
+ camo config repo-root [path] # Get/set persisted webauto repo root
148
+ camo highlight-mode [status|on|off] # Global highlight mode for click/type/scroll
149
+ ```
150
+
74
151
  ### Browser Control
75
152
 
76
153
  ```bash
77
- camo start [profileId] [--url <url>] [--headless] [--devtools] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
154
+ camo start [profileId] [--url <url>] [--headless] [--devtools] [--record] [--record-name <name>] [--record-output <path>] [--record-overlay|--no-record-overlay] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
78
155
  camo stop [profileId]
79
156
  camo stop --id <instanceId>
80
157
  camo stop --alias <alias>
@@ -89,6 +166,7 @@ If no saved size exists, it defaults to near-fullscreen (full width, slight vert
89
166
  Use `--width/--height` to override and update the saved profile size.
90
167
  For headless sessions, default idle timeout is `30m` (auto-stop on inactivity). Use `--idle-timeout` (e.g. `45m`, `1800s`, `0`) to customize.
91
168
  Use `--devtools` to open browser developer tools in headed mode (cannot be combined with `--headless`).
169
+ Use `--record` to auto-enable JSONL recording at startup; `--record-name`, `--record-output`, and `--record-overlay` customize file naming/output and floating toggle UI.
92
170
 
93
171
  ### Lifecycle & Cleanup
94
172
 
@@ -100,7 +178,6 @@ camo cleanup all # Cleanup all active sessions
100
178
  camo cleanup locks # Cleanup stale lock files
101
179
  camo force-stop [profileId] # Force stop session (for stuck sessions)
102
180
  camo lock list # List active session locks
103
- camo recover [profileId] # Recover orphaned session
104
181
  ```
105
182
 
106
183
  ### Navigation
@@ -114,14 +191,42 @@ camo screenshot [profileId] [--output <file>] [--full]
114
191
  ### Interaction
115
192
 
116
193
  ```bash
117
- camo scroll [profileId] [--down|--up|--left|--right] [--amount <px>]
118
- camo click [profileId] <selector> # Click element by CSS selector
119
- camo type [profileId] <selector> <text> # Type text into element
194
+ camo scroll [profileId] [--down|--up|--left|--right] [--amount <px>] [--selector <css>] [--highlight|--no-highlight]
195
+ camo click [profileId] <selector> [--highlight|--no-highlight] # Click visible element by CSS selector
196
+ camo type [profileId] <selector> <text> [--highlight|--no-highlight] # Type into visible input element
120
197
  camo highlight [profileId] <selector> # Highlight element (red border, 2s)
121
198
  camo clear-highlight [profileId] # Clear all highlights
122
199
  camo viewport [profileId] --width <w> --height <h>
123
200
  ```
124
201
 
202
+ ### Devtools
203
+
204
+ ```bash
205
+ camo devtools logs [profileId] [--limit 120] [--since <unix_ms>] [--levels error,warn] [--clear]
206
+ camo devtools eval [profileId] <expression> [--profile <id>]
207
+ camo devtools clear [profileId]
208
+ ```
209
+
210
+ `devtools logs` reads entries from an injected in-page console collector.
211
+ Supported levels: `log`, `info`, `warn`, `error`, `debug`.
212
+
213
+ ### Recording
214
+
215
+ ```bash
216
+ camo record start [profileId] [--name <name>] [--output <file>] [--overlay|--no-overlay]
217
+ camo record stop [profileId] [--reason <text>]
218
+ camo record status [profileId]
219
+ ```
220
+
221
+ Recorder JSONL events include:
222
+ - `page.visit`
223
+ - `interaction.click`
224
+ - `interaction.keydown`
225
+ - `interaction.input`
226
+ - `interaction.wheel`
227
+ - `interaction.scroll`
228
+ - `recording.start|stop|toggled|runtime_ready`
229
+
125
230
  ### Pages
126
231
 
127
232
  ```bash
@@ -339,6 +444,7 @@ Condition types:
339
444
  - `WEBAUTO_CONTAINER_ROOT` - User container root override (default: `~/.webauto/container-lib`)
340
445
  - `CAMO_PROGRESS_EVENTS_FILE` - Optional progress event JSONL path override
341
446
  - `CAMO_PROGRESS_WS_HOST` / `CAMO_PROGRESS_WS_PORT` - Progress websocket daemon bind address (default: `127.0.0.1:7788`)
447
+ - `CAMO_DEFAULT_WINDOW_VERTICAL_RESERVE` - Reserved vertical pixels for default headful auto-size
342
448
 
343
449
  ## Session Persistence
344
450
 
@@ -346,7 +452,6 @@ Camo CLI persists session information locally:
346
452
 
347
453
  - Sessions are registered in `~/.webauto/sessions/`
348
454
  - On restart, `camo sessions` / `camo instances` shows live + orphaned sessions
349
- - Use `camo recover <profileId>` to reconnect or cleanup orphaned sessions
350
455
  - Stale sessions (>7 days) are automatically cleaned up
351
456
 
352
457
  ## Requirements
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web-auto/camo",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Camoufox Browser CLI - Cross-platform browser automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "check:file-size": "node scripts/check-file-size.mjs",
19
19
  "test": "node --test 'tests/**/*.test.mjs'",
20
20
  "test:coverage": "c8 --reporter=text --reporter=lcov node --test 'tests/**/*.test.mjs'",
21
+ "test:coverage:modes": "c8 --all --check-coverage --lines 90 --functions 90 --branches 90 --statements 90 --include src/commands/record.mjs --include src/commands/highlight-mode.mjs --include src/container/runtime-core/operations/selector-scripts.mjs node --test tests/unit/commands/record.test.mjs tests/unit/commands/highlight-mode.test.mjs tests/unit/container/selector-scripts.test.mjs tests/unit/commands/browser.test.mjs",
21
22
  "version:bump": "node scripts/bump-version.mjs",
22
23
  "install:global": "npm run build && npm install -g .",
23
24
  "uninstall:global": "npm uninstall -g @web-auto/camo",
@@ -86,43 +86,6 @@ export function buildCommentsHarvestScript(params = {}) {
86
86
  }
87
87
  return null;
88
88
  };
89
- const normalizeInlineText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
90
- const sanitizeAuthorText = (raw, commentText = '') => {
91
- const text = normalizeInlineText(raw);
92
- if (!text) return '';
93
- if (commentText && text === commentText) return '';
94
- if (text.length > 40) return '';
95
- if (/^(回复|展开|收起|查看更多|评论|赞|分享|发送)$/.test(text)) return '';
96
- return text;
97
- };
98
- const readAuthor = (item, commentText = '') => {
99
- const attrNames = ['data-user-name', 'data-username', 'data-user_nickname', 'data-nickname'];
100
- for (const attr of attrNames) {
101
- const value = sanitizeAuthorText(item.getAttribute?.(attr), commentText);
102
- if (value) return value;
103
- }
104
- const selectors = [
105
- '.comment-user .name',
106
- '.comment-user .username',
107
- '.comment-user .user-name',
108
- '.author .name',
109
- '.author',
110
- '.user-name',
111
- '.username',
112
- '.name',
113
- 'a[href*="/user/profile/"]',
114
- 'a[href*="/user/"]',
115
- ];
116
- for (const selector of selectors) {
117
- const node = item.querySelector(selector);
118
- if (!node) continue;
119
- const title = sanitizeAuthorText(node.getAttribute?.('title'), commentText);
120
- if (title) return title;
121
- const text = sanitizeAuthorText(node.textContent, commentText);
122
- if (text) return text;
123
- }
124
- return '';
125
- };
126
89
 
127
90
  const scroller = document.querySelector('.note-scroller')
128
91
  || document.querySelector('.comments-el')
@@ -142,8 +105,9 @@ export function buildCommentsHarvestScript(params = {}) {
142
105
  const nodes = Array.from(document.querySelectorAll('.comment-item, [class*="comment-item"]'));
143
106
  for (const item of nodes) {
144
107
  const textNode = item.querySelector('.content, .comment-content, p');
108
+ const authorNode = item.querySelector('.name, .author, .user-name, [class*="author"], [class*="name"]');
145
109
  const text = String((textNode && textNode.textContent) || '').trim();
146
- const author = readAuthor(item, text);
110
+ const author = String((authorNode && authorNode.textContent) || '').trim();
147
111
  if (!text) continue;
148
112
  const key = author + '::' + text;
149
113
  if (commentMap.has(key)) continue;
@@ -74,42 +74,6 @@ function buildCollectLikeTargetsScript() {
74
74
  }
75
75
  return '';
76
76
  };
77
- const sanitizeUserName = (raw, commentText = '') => {
78
- const text = String(raw || '').replace(/\\s+/g, ' ').trim();
79
- if (!text) return '';
80
- if (commentText && text === commentText) return '';
81
- if (text.length > 40) return '';
82
- if (/^(回复|展开|收起|查看更多|评论|赞|分享|发送)$/.test(text)) return '';
83
- return text;
84
- };
85
- const readUserName = (item, commentText = '') => {
86
- const attrNames = ['data-user-name', 'data-username', 'data-user_nickname', 'data-nickname'];
87
- for (const attr of attrNames) {
88
- const value = sanitizeUserName(item.getAttribute?.(attr), commentText);
89
- if (value) return value;
90
- }
91
- const selectors = [
92
- '.comment-user .name',
93
- '.comment-user .username',
94
- '.comment-user .user-name',
95
- '.author .name',
96
- '.author',
97
- '.user-name',
98
- '.username',
99
- '.name',
100
- 'a[href*="/user/profile/"]',
101
- 'a[href*="/user/"]',
102
- ];
103
- for (const selector of selectors) {
104
- const node = item.querySelector(selector);
105
- if (!node) continue;
106
- const title = sanitizeUserName(node.getAttribute?.('title'), commentText);
107
- if (title) return title;
108
- const text = sanitizeUserName(node.textContent, commentText);
109
- if (text) return text;
110
- }
111
- return '';
112
- };
113
77
  const readAttr = (item, attrNames) => {
114
78
  for (const attr of attrNames) {
115
79
  const value = String(item.getAttribute?.(attr) || '').trim();
@@ -117,15 +81,6 @@ function buildCollectLikeTargetsScript() {
117
81
  }
118
82
  return '';
119
83
  };
120
- const readUserId = (item) => {
121
- const value = readAttr(item, ['data-user-id', 'data-userid', 'data-user_id']);
122
- if (value) return value;
123
- const anchor = item.querySelector('a[href*="/user/profile/"], a[href*="/user/"]');
124
- const href = String(anchor?.getAttribute?.('href') || '').trim();
125
- if (!href) return '';
126
- const matched = href.match(/\\/user\\/(?:profile\\/)?([a-zA-Z0-9_-]+)/);
127
- return matched && matched[1] ? matched[1] : '';
128
- };
129
84
 
130
85
  const matchedSet = new Set(
131
86
  Array.isArray(state.matchedComments)
@@ -137,8 +92,8 @@ function buildCollectLikeTargetsScript() {
137
92
  const item = items[index];
138
93
  const text = readText(item, ['.content', '.comment-content', 'p']);
139
94
  if (!text) continue;
140
- const userName = readUserName(item, text);
141
- const userId = readUserId(item);
95
+ const userName = readText(item, ['.name', '.author', '.user-name', '.username', '[class*="author"]', '[class*="name"]']);
96
+ const userId = readAttr(item, ['data-user-id', 'data-userid', 'data-user_id']);
142
97
  const timestamp = readText(item, ['.date', '.time', '.timestamp', '[class*="time"]']);
143
98
  const likeControl = findLikeControl(item);
144
99
  rows.push({
package/src/cli.mjs CHANGED
@@ -13,6 +13,9 @@ import { handleSystemCommand } from './commands/system.mjs';
13
13
  import { handleContainerCommand } from './commands/container.mjs';
14
14
  import { handleAutoscriptCommand } from './commands/autoscript.mjs';
15
15
  import { handleEventsCommand } from './commands/events.mjs';
16
+ import { handleDevtoolsCommand } from './commands/devtools.mjs';
17
+ import { handleRecordCommand } from './commands/record.mjs';
18
+ import { handleHighlightModeCommand } from './commands/highlight-mode.mjs';
16
19
  import {
17
20
  handleStartCommand, handleStopCommand, handleStatusCommand,
18
21
  handleGotoCommand, handleBackCommand, handleScreenshotCommand,
@@ -64,6 +67,30 @@ function inferProfileId(cmd, args) {
64
67
  return positionals[0] || null;
65
68
  }
66
69
 
70
+ if (cmd === 'devtools') {
71
+ const sub = positionals[0] || null;
72
+ if (sub === 'eval' || sub === 'logs' || sub === 'clear') {
73
+ return positionals[1] || null;
74
+ }
75
+ }
76
+
77
+ if (cmd === 'record') {
78
+ const explicit = readFlagValue(args, ['--profile', '-p']);
79
+ if (explicit) return explicit;
80
+ const sub = positionals[0] || null;
81
+ const values = [];
82
+ for (let i = 2; i < args.length; i += 1) {
83
+ const token = args[i];
84
+ if (!token || String(token).startsWith('-')) continue;
85
+ const prev = args[i - 1];
86
+ if (prev && ['--name', '--output', '--reason'].includes(prev)) continue;
87
+ values.push(String(token));
88
+ }
89
+ if (sub === 'start' || sub === 'stop' || sub === 'status') {
90
+ return values[0] || null;
91
+ }
92
+ }
93
+
67
94
  if (cmd === 'autoscript' && positionals[0] === 'run') {
68
95
  return explicitProfile || null;
69
96
  }
@@ -192,6 +219,21 @@ async function main() {
192
219
  return;
193
220
  }
194
221
 
222
+ if (cmd === 'devtools') {
223
+ await runTrackedCommand(cmd, args, () => handleDevtoolsCommand(args));
224
+ return;
225
+ }
226
+
227
+ if (cmd === 'record') {
228
+ await runTrackedCommand(cmd, args, () => handleRecordCommand(args));
229
+ return;
230
+ }
231
+
232
+ if (cmd === 'highlight-mode') {
233
+ await runTrackedCommand(cmd, args, () => handleHighlightModeCommand(args));
234
+ return;
235
+ }
236
+
195
237
  // Lifecycle commands
196
238
  if (cmd === 'cleanup') {
197
239
  await runTrackedCommand(cmd, args, () => handleCleanupCommand(args));
@@ -237,7 +279,7 @@ async function main() {
237
279
  'start', 'stop', 'close', 'status', 'list', 'goto', 'navigate', 'back', 'screenshot',
238
280
  'new-page', 'close-page', 'switch-page', 'list-pages', 'shutdown',
239
281
  'scroll', 'click', 'type', 'highlight', 'clear-highlight', 'viewport',
240
- 'cookies', 'window', 'mouse', 'system', 'container', 'autoscript', 'events',
282
+ 'cookies', 'window', 'mouse', 'system', 'container', 'autoscript', 'events', 'devtools', 'record', 'highlight-mode',
241
283
  ]);
242
284
 
243
285
  if (!serviceCommands.has(cmd)) {
@@ -312,6 +354,12 @@ async function main() {
312
354
  case 'system':
313
355
  await handleSystemCommand(args);
314
356
  break;
357
+ case 'record':
358
+ await handleRecordCommand(args);
359
+ break;
360
+ case 'highlight-mode':
361
+ await handleHighlightModeCommand(args);
362
+ break;
315
363
  }
316
364
  });
317
365
  }
@@ -1,7 +1,9 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import {
3
4
  listProfiles,
4
5
  getDefaultProfile,
6
+ getHighlightMode,
5
7
  getProfileWindowSize,
6
8
  setProfileWindowSize,
7
9
  } from '../utils/config.mjs';
@@ -10,6 +12,7 @@ import { resolveProfileId, ensureUrlScheme, looksLikeUrlToken, getPositionals }
10
12
  import { acquireLock, releaseLock, cleanupStaleLocks } from '../lifecycle/lock.mjs';
11
13
  import {
12
14
  buildSelectorClickScript,
15
+ buildScrollTargetScript,
13
16
  buildSelectorTypeScript,
14
17
  } from '../container/runtime-core/operations/selector-scripts.mjs';
15
18
  import {
@@ -83,6 +86,12 @@ function validateAlias(alias) {
83
86
  return text.slice(0, 64);
84
87
  }
85
88
 
89
+ function resolveHighlightEnabled(args) {
90
+ if (args.includes('--highlight')) return true;
91
+ if (args.includes('--no-highlight')) return false;
92
+ return getHighlightMode();
93
+ }
94
+
86
95
  function formatDurationMs(ms) {
87
96
  const value = Number(ms);
88
97
  if (!Number.isFinite(value) || value <= 0) return 'disabled';
@@ -319,12 +328,27 @@ export async function handleStartCommand(args) {
319
328
  const idleTimeoutRaw = readFlagValue(args, ['--idle-timeout']);
320
329
  const parsedIdleTimeoutMs = parseDurationMs(idleTimeoutRaw, DEFAULT_HEADLESS_IDLE_TIMEOUT_MS);
321
330
  const wantsDevtools = args.includes('--devtools');
331
+ const wantsRecord = args.includes('--record');
332
+ const recordName = readFlagValue(args, ['--record-name']);
333
+ const recordOutputRaw = readFlagValue(args, ['--record-output']);
334
+ const recordOverlay = args.includes('--no-record-overlay')
335
+ ? false
336
+ : args.includes('--record-overlay')
337
+ ? true
338
+ : null;
322
339
  if (hasExplicitWidth !== hasExplicitHeight) {
323
- throw new Error('Usage: camo start [profileId] [--url <url>] [--headless] [--devtools] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]');
340
+ throw new Error('Usage: camo start [profileId] [--url <url>] [--headless] [--devtools] [--record] [--record-name <name>] [--record-output <path>] [--record-overlay|--no-record-overlay] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]');
324
341
  }
325
342
  if ((hasExplicitWidth && explicitWidth < START_WINDOW_MIN_WIDTH) || (hasExplicitHeight && explicitHeight < START_WINDOW_MIN_HEIGHT)) {
326
343
  throw new Error(`Window size too small. Minimum is ${START_WINDOW_MIN_WIDTH}x${START_WINDOW_MIN_HEIGHT}`);
327
344
  }
345
+ if (args.includes('--record-name') && !recordName) {
346
+ throw new Error('Usage: camo start [profileId] --record-name <name>');
347
+ }
348
+ if (args.includes('--record-output') && !recordOutputRaw) {
349
+ throw new Error('Usage: camo start [profileId] --record-output <path>');
350
+ }
351
+ const recordOutput = recordOutputRaw ? path.resolve(recordOutputRaw) : null;
328
352
  const hasExplicitWindowSize = hasExplicitWidth && hasExplicitHeight;
329
353
  const profileSet = new Set(listProfiles());
330
354
  let implicitUrl;
@@ -334,8 +358,9 @@ export async function handleStartCommand(args) {
334
358
  const arg = args[i];
335
359
  if (arg === '--url') { i++; continue; }
336
360
  if (arg === '--width' || arg === '--height') { i++; continue; }
337
- if (arg === '--alias' || arg === '--idle-timeout') { i++; continue; }
361
+ if (arg === '--alias' || arg === '--idle-timeout' || arg === '--record-name' || arg === '--record-output') { i++; continue; }
338
362
  if (arg === '--headless') continue;
363
+ if (arg === '--record' || arg === '--record-overlay' || arg === '--no-record-overlay') continue;
339
364
  if (arg.startsWith('--')) continue;
340
365
 
341
366
  if (looksLikeUrlToken(arg) && !profileSet.has(arg)) {
@@ -398,6 +423,14 @@ export async function handleStartCommand(args) {
398
423
  if (!existingHeadless && wantsDevtools) {
399
424
  payload.devtools = await requestDevtoolsOpen(profileId);
400
425
  }
426
+ if (wantsRecord) {
427
+ payload.recording = await callAPI('record:start', {
428
+ profileId,
429
+ ...(recordName ? { name: recordName } : {}),
430
+ ...(recordOutput ? { outputPath: recordOutput } : {}),
431
+ ...(recordOverlay !== null ? { overlay: recordOverlay } : {}),
432
+ });
433
+ }
401
434
  console.log(JSON.stringify(payload, null, 2));
402
435
  startSessionWatchdog(profileId);
403
436
  return;
@@ -423,6 +456,10 @@ export async function handleStartCommand(args) {
423
456
  url: targetUrl ? ensureUrlScheme(targetUrl) : undefined,
424
457
  headless,
425
458
  devtools: wantsDevtools,
459
+ ...(wantsRecord ? { record: true } : {}),
460
+ ...(recordName ? { recordName } : {}),
461
+ ...(recordOutput ? { recordOutput } : {}),
462
+ ...(recordOverlay !== null ? { recordOverlay } : {}),
426
463
  });
427
464
 
428
465
  if (result?.ok) {
@@ -729,32 +766,55 @@ export async function handleScrollCommand(args) {
729
766
  await ensureBrowserService();
730
767
  const directionFlags = new Set(['--up', '--down', '--left', '--right']);
731
768
  const isFlag = (arg) => arg?.startsWith('--');
769
+ const selectorIdx = args.indexOf('--selector');
770
+ const selector = selectorIdx >= 0 ? String(args[selectorIdx + 1] || '').trim() : null;
771
+ const highlight = resolveHighlightEnabled(args);
732
772
 
733
773
  let profileId = null;
734
774
  for (let i = 1; i < args.length; i++) {
735
775
  const arg = args[i];
736
776
  if (directionFlags.has(arg)) continue;
737
777
  if (arg === '--amount') { i++; continue; }
778
+ if (arg === '--selector') { i++; continue; }
779
+ if (arg === '--highlight' || arg === '--no-highlight') continue;
738
780
  if (isFlag(arg)) continue;
739
781
  profileId = arg;
740
782
  break;
741
783
  }
742
784
  if (!profileId) profileId = getDefaultProfile();
743
- if (!profileId) throw new Error('Usage: camo scroll [profileId] [--down|--up|--left|--right] [--amount <px>]');
785
+ if (!profileId) throw new Error('Usage: camo scroll [profileId] [--down|--up|--left|--right] [--amount <px>] [--selector <css>] [--highlight|--no-highlight]');
786
+ if (selectorIdx >= 0 && !selector) {
787
+ throw new Error('Usage: camo scroll [profileId] --selector <css>');
788
+ }
744
789
 
745
790
  const direction = args.includes('--up') ? 'up' : args.includes('--left') ? 'left' : args.includes('--right') ? 'right' : 'down';
746
791
  const amountIdx = args.indexOf('--amount');
747
792
  const amount = amountIdx >= 0 ? Number(args[amountIdx + 1]) || 300 : 300;
748
793
 
794
+ const target = await callAPI('evaluate', {
795
+ profileId,
796
+ script: buildScrollTargetScript({ selector, highlight }),
797
+ });
798
+ const centerX = Number(target?.result?.center?.x);
799
+ const centerY = Number(target?.result?.center?.y);
800
+ if (Number.isFinite(centerX) && Number.isFinite(centerY)) {
801
+ await callAPI('mouse:move', { profileId, x: centerX, y: centerY, steps: 2 });
802
+ }
803
+
749
804
  const deltaX = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
750
805
  const deltaY = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
751
806
  const result = await callAPI('mouse:wheel', { profileId, deltaX, deltaY });
752
- console.log(JSON.stringify(result, null, 2));
807
+ console.log(JSON.stringify({
808
+ ...result,
809
+ scrollTarget: target?.result || null,
810
+ highlight,
811
+ }, null, 2));
753
812
  }
754
813
 
755
814
  export async function handleClickCommand(args) {
756
815
  await ensureBrowserService();
757
816
  const positionals = getPositionals(args);
817
+ const highlight = resolveHighlightEnabled(args);
758
818
  let profileId;
759
819
  let selector;
760
820
 
@@ -766,12 +826,12 @@ export async function handleClickCommand(args) {
766
826
  selector = positionals[1];
767
827
  }
768
828
 
769
- if (!profileId) throw new Error('Usage: camo click [profileId] <selector>');
770
- if (!selector) throw new Error('Usage: camo click [profileId] <selector>');
829
+ if (!profileId) throw new Error('Usage: camo click [profileId] <selector> [--highlight|--no-highlight]');
830
+ if (!selector) throw new Error('Usage: camo click [profileId] <selector> [--highlight|--no-highlight]');
771
831
 
772
832
  const result = await callAPI('evaluate', {
773
833
  profileId,
774
- script: buildSelectorClickScript({ selector, highlight: false }),
834
+ script: buildSelectorClickScript({ selector, highlight }),
775
835
  });
776
836
  console.log(JSON.stringify(result, null, 2));
777
837
  }
@@ -779,6 +839,7 @@ export async function handleClickCommand(args) {
779
839
  export async function handleTypeCommand(args) {
780
840
  await ensureBrowserService();
781
841
  const positionals = getPositionals(args);
842
+ const highlight = resolveHighlightEnabled(args);
782
843
  let profileId;
783
844
  let selector;
784
845
  let text;
@@ -793,12 +854,12 @@ export async function handleTypeCommand(args) {
793
854
  text = positionals[2];
794
855
  }
795
856
 
796
- if (!profileId) throw new Error('Usage: camo type [profileId] <selector> <text>');
797
- if (!selector || text === undefined) throw new Error('Usage: camo type [profileId] <selector> <text>');
857
+ if (!profileId) throw new Error('Usage: camo type [profileId] <selector> <text> [--highlight|--no-highlight]');
858
+ if (!selector || text === undefined) throw new Error('Usage: camo type [profileId] <selector> <text> [--highlight|--no-highlight]');
798
859
 
799
860
  const result = await callAPI('evaluate', {
800
861
  profileId,
801
- script: buildSelectorTypeScript({ selector, highlight: false, text }),
862
+ script: buildSelectorTypeScript({ selector, highlight, text }),
802
863
  });
803
864
  console.log(JSON.stringify(result, null, 2));
804
865
  }