@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 +112 -7
- package/package.json +2 -1
- package/src/autoscript/action-providers/xhs/comments.mjs +2 -38
- package/src/autoscript/action-providers/xhs/interaction.mjs +2 -47
- package/src/cli.mjs +49 -1
- package/src/commands/browser.mjs +71 -10
- package/src/commands/devtools.mjs +349 -0
- package/src/commands/highlight-mode.mjs +24 -0
- package/src/commands/record.mjs +115 -0
- package/src/container/runtime-core/operations/selector-scripts.mjs +193 -80
- package/src/utils/config.mjs +12 -0
- package/src/utils/help.mjs +25 -7
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
|
-
|
|
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>
|
|
119
|
-
camo type [profileId] <selector> <text>
|
|
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.
|
|
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 =
|
|
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 =
|
|
141
|
-
const userId =
|
|
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
|
}
|
package/src/commands/browser.mjs
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
862
|
+
script: buildSelectorTypeScript({ selector, highlight, text }),
|
|
802
863
|
});
|
|
803
864
|
console.log(JSON.stringify(result, null, 2));
|
|
804
865
|
}
|