veil-browser 0.1.8 → 0.2.0
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/SKILL.md +275 -0
- package/dist/browser.d.ts +4 -4
- package/dist/browser.js +71 -25
- package/dist/index.js +366 -151
- package/package.json +3 -2
package/SKILL.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# veil — OpenClaw Stealth Browser Remote
|
|
2
|
+
|
|
3
|
+
## What is veil
|
|
4
|
+
|
|
5
|
+
veil is a **headless browser remote control** for OpenClaw. It runs a persistent stealth Chromium browser and exposes it through a clean CLI. You (OpenClaw) are the brain — veil is just the hands.
|
|
6
|
+
|
|
7
|
+
Every command outputs clean JSON: `{ ok: true, ... }` or `{ ok: false, error: "..." }`
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g veil-browser
|
|
13
|
+
npx playwright install chromium
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Core Mental Model
|
|
17
|
+
|
|
18
|
+
You chain veil commands like a human would:
|
|
19
|
+
1. Open the browser to the right platform (`veil open x`)
|
|
20
|
+
2. Read the page (`veil snapshot` or `veil read`)
|
|
21
|
+
3. Act on what you see (`veil click`, `veil type`)
|
|
22
|
+
4. Verify the result (`veil find`, `veil read`, `veil shot`)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Full Command Reference
|
|
27
|
+
|
|
28
|
+
### Session Setup (do once)
|
|
29
|
+
```bash
|
|
30
|
+
veil login x # Opens visible browser → log in → saves cookies
|
|
31
|
+
veil login linkedin
|
|
32
|
+
veil login reddit
|
|
33
|
+
veil sessions # List saved sessions
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Open a Session
|
|
37
|
+
```bash
|
|
38
|
+
veil open x # Restore X session, navigate to home feed
|
|
39
|
+
veil open linkedin # Restore LinkedIn session
|
|
40
|
+
veil open <platform> # Any saved platform
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Navigation
|
|
44
|
+
```bash
|
|
45
|
+
veil go https://x.com/home # Navigate to URL
|
|
46
|
+
veil go https://x.com --wait load # Wait for full load
|
|
47
|
+
veil url # Get current URL + title
|
|
48
|
+
veil back # Go back
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Reading the Page
|
|
52
|
+
```bash
|
|
53
|
+
veil snapshot # DOM/ARIA tree — use this to understand page structure
|
|
54
|
+
veil snapshot --max 4000 # Smaller snapshot for faster processing
|
|
55
|
+
veil read # Full page text (first 5000 chars)
|
|
56
|
+
veil read "[data-testid='tweetText']" # Text of first match
|
|
57
|
+
veil read "[data-testid='tweetText']" --all # All matches as array
|
|
58
|
+
veil read "a" --attr href # Read attribute
|
|
59
|
+
veil find "Sign in" # Check if text exists: { ok: true, found: true }
|
|
60
|
+
veil exists "[data-testid='like']" # Check if selector exists
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Clicking
|
|
64
|
+
```bash
|
|
65
|
+
veil click "[data-testid='like']" # Click first match
|
|
66
|
+
veil click "[data-testid='like']" --nth 2 # Click 3rd match (0-indexed)
|
|
67
|
+
veil click "[data-testid='like']" --force # Force click (bypass overlays)
|
|
68
|
+
veil click "[data-testid='reply']" --timeout 8000
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Typing
|
|
72
|
+
```bash
|
|
73
|
+
veil type "[data-testid='tweetTextarea_0']" "Hello world"
|
|
74
|
+
veil type "input[name='search']" "AI architecture" --clear # Clear first
|
|
75
|
+
veil type "[data-testid='tweetTextarea_0']" "text" --delay 60 # Slower, more human
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Keyboard
|
|
79
|
+
```bash
|
|
80
|
+
veil press Enter
|
|
81
|
+
veil press Tab
|
|
82
|
+
veil press Escape
|
|
83
|
+
veil press ArrowDown
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Scrolling
|
|
87
|
+
```bash
|
|
88
|
+
veil scroll down # Scroll 600px down
|
|
89
|
+
veil scroll up # Scroll 600px up
|
|
90
|
+
veil scroll down --amount 1200
|
|
91
|
+
veil scroll top # Jump to top of page
|
|
92
|
+
veil scroll bottom # Jump to bottom
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Timing
|
|
96
|
+
```bash
|
|
97
|
+
veil wait 500 # Wait 500ms
|
|
98
|
+
veil wait 2000 # Wait 2 seconds
|
|
99
|
+
veil wait-for "[data-testid='timeline']" # Wait until element appears
|
|
100
|
+
veil wait-for "[data-testid='timeline']" --timeout 15000
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Screenshots
|
|
104
|
+
```bash
|
|
105
|
+
veil shot # Screenshot to veil-<timestamp>.png
|
|
106
|
+
veil shot page.png # Custom filename
|
|
107
|
+
veil shot page.png --full # Full page
|
|
108
|
+
veil shot el.png --selector "[data-testid='tweet']" # Screenshot element
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### JavaScript Execution
|
|
112
|
+
```bash
|
|
113
|
+
veil eval "document.title"
|
|
114
|
+
veil eval "window.scrollY"
|
|
115
|
+
veil eval "document.querySelectorAll('article').length"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Close Browser
|
|
119
|
+
```bash
|
|
120
|
+
veil close # Close current browser
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Platform Selectors
|
|
126
|
+
|
|
127
|
+
### X (Twitter) — x.com
|
|
128
|
+
|
|
129
|
+
| Element | Selector |
|
|
130
|
+
|---------|----------|
|
|
131
|
+
| Tweet text area | `[data-testid="tweetTextarea_0"]` |
|
|
132
|
+
| Post/Tweet button | `[data-testid="tweetButtonInline"]` |
|
|
133
|
+
| Like button | `[data-testid="like"]` |
|
|
134
|
+
| Unlike (already liked) | `[data-testid="unlike"]` |
|
|
135
|
+
| Reply button | `[data-testid="reply"]` |
|
|
136
|
+
| Retweet button | `[data-testid="retweet"]` |
|
|
137
|
+
| Share/More options | `[aria-label="Share Tweet"]` |
|
|
138
|
+
| A tweet (article) | `article[data-testid="tweet"]` |
|
|
139
|
+
| First tweet | `article[data-testid="tweet"]:first-of-type` |
|
|
140
|
+
| Tweet text content | `[data-testid="tweetText"]` |
|
|
141
|
+
| Feed timeline | `[data-testid="primaryColumn"]` |
|
|
142
|
+
| Search box | `[data-testid="SearchBox_Search_Input"]` |
|
|
143
|
+
| Menu items | `[role="menuitem"]` |
|
|
144
|
+
| Follow button | `[data-testid="follow"]` |
|
|
145
|
+
|
|
146
|
+
### LinkedIn — linkedin.com
|
|
147
|
+
|
|
148
|
+
| Element | Selector |
|
|
149
|
+
|---------|----------|
|
|
150
|
+
| Like button | `button[aria-label*="Like"]` |
|
|
151
|
+
| Comment button | `button[aria-label*="Comment"]` |
|
|
152
|
+
| Share button | `button[aria-label*="Share"]` |
|
|
153
|
+
| Comment text area | `[contenteditable="true"][role="textbox"]` |
|
|
154
|
+
| Post button | `button.comments-comment-box__submit-button` |
|
|
155
|
+
| Feed posts | `.feed-shared-update-v2` |
|
|
156
|
+
|
|
157
|
+
### Reddit — reddit.com
|
|
158
|
+
|
|
159
|
+
| Element | Selector |
|
|
160
|
+
|---------|----------|
|
|
161
|
+
| Upvote button | `button[aria-label="upvote"]` |
|
|
162
|
+
| Comment link | `a[data-click-id="comments"]` |
|
|
163
|
+
| Comment box | `[contenteditable="true"]` |
|
|
164
|
+
| Submit comment | `button:has-text("Comment")` |
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Common Task Sequences
|
|
169
|
+
|
|
170
|
+
### Post a Tweet on X
|
|
171
|
+
```bash
|
|
172
|
+
veil open x
|
|
173
|
+
veil wait-for "[data-testid='primaryColumn']"
|
|
174
|
+
veil click "[data-testid='tweetTextarea_0']"
|
|
175
|
+
veil wait 300
|
|
176
|
+
veil type "[data-testid='tweetTextarea_0']" "Your tweet text here"
|
|
177
|
+
veil wait 500
|
|
178
|
+
veil click "[data-testid='tweetButtonInline']" --force
|
|
179
|
+
veil wait 1500
|
|
180
|
+
veil find "Your tweet text here"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Like First 5 Posts on X Feed
|
|
184
|
+
```bash
|
|
185
|
+
veil open x
|
|
186
|
+
veil wait-for "article[data-testid='tweet']"
|
|
187
|
+
veil click "[data-testid='like']" --nth 0
|
|
188
|
+
veil wait 800
|
|
189
|
+
veil click "[data-testid='like']" --nth 1
|
|
190
|
+
veil wait 800
|
|
191
|
+
veil click "[data-testid='like']" --nth 2
|
|
192
|
+
veil wait 800
|
|
193
|
+
veil click "[data-testid='like']" --nth 3
|
|
194
|
+
veil wait 800
|
|
195
|
+
veil click "[data-testid='like']" --nth 4
|
|
196
|
+
veil wait 800
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Reply to First Post on X Feed
|
|
200
|
+
```bash
|
|
201
|
+
veil open x
|
|
202
|
+
veil wait-for "article[data-testid='tweet']"
|
|
203
|
+
veil click "[data-testid='reply']" --nth 0 --force
|
|
204
|
+
veil wait 600
|
|
205
|
+
veil click "[data-testid='tweetTextarea_0']"
|
|
206
|
+
veil wait 200
|
|
207
|
+
veil type "[data-testid='tweetTextarea_0']" "Great post! This is exactly why AI needs structured reasoning."
|
|
208
|
+
veil wait 400
|
|
209
|
+
veil click "[data-testid='tweetButton']" --force
|
|
210
|
+
veil wait 1500
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Search X for AI Posts
|
|
214
|
+
```bash
|
|
215
|
+
veil open x
|
|
216
|
+
veil go https://x.com/search?q=AI+architecture&f=live
|
|
217
|
+
veil wait-for "article[data-testid='tweet']"
|
|
218
|
+
veil snapshot --max 3000
|
|
219
|
+
# Now you can see the results and decide which to interact with
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Read Feed Before Interacting
|
|
223
|
+
```bash
|
|
224
|
+
veil open x
|
|
225
|
+
veil wait-for "article[data-testid='tweet']"
|
|
226
|
+
veil read "[data-testid='tweetText']" --all
|
|
227
|
+
# Returns JSON array of tweet texts — you can decide which to reply to
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Error Handling
|
|
233
|
+
|
|
234
|
+
Every command returns JSON. Always check `ok`:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
result=$(veil click "[data-testid='like']")
|
|
238
|
+
# Check: echo $result | jq .ok
|
|
239
|
+
# If false: echo $result | jq .error
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Common errors and fixes:**
|
|
243
|
+
|
|
244
|
+
| Error | Fix |
|
|
245
|
+
|-------|-----|
|
|
246
|
+
| `No browser open` | Run `veil open x` first |
|
|
247
|
+
| `Timeout waiting for selector` | Page not loaded yet — add `veil wait 2000` before |
|
|
248
|
+
| `Element not found` | Use `veil snapshot` to inspect actual DOM, adjust selector |
|
|
249
|
+
| `Session not found` | Run `veil login x` to create session |
|
|
250
|
+
| Click fails (overlay) | Add `--force` flag to `veil click` |
|
|
251
|
+
|
|
252
|
+
**Debug workflow:**
|
|
253
|
+
```bash
|
|
254
|
+
veil shot debug.png # What does the page actually look like?
|
|
255
|
+
veil snapshot --max 5000 # What's in the DOM?
|
|
256
|
+
veil exists "[selector]" # Does the element even exist?
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Session Persistence
|
|
262
|
+
|
|
263
|
+
- Sessions stored in `~/.veil/sessions/<platform>.json` (encrypted cookies)
|
|
264
|
+
- Browser instance stays open within a single execution chain
|
|
265
|
+
- Each new `veil` command that needs the browser checks for existing session
|
|
266
|
+
- Use `veil close` to explicitly close the browser
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Notes
|
|
271
|
+
|
|
272
|
+
- All clicks on X use `--force` by default to bypass overlay interceptors
|
|
273
|
+
- Human-like delays are added automatically between interactions
|
|
274
|
+
- FlareSolverr auto-starts via Docker if Cloudflare challenges are detected
|
|
275
|
+
- veil outputs only JSON to stdout — safe to pipe and parse
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
export
|
|
1
|
+
import { Browser, BrowserContext, Page } from 'playwright';
|
|
2
|
+
export declare function ensureBrowser(opts?: {
|
|
3
3
|
headed?: boolean;
|
|
4
4
|
platform?: string;
|
|
5
|
-
}
|
|
6
|
-
export declare function getBrowser(opts?: LaunchOptions): Promise<{
|
|
5
|
+
}): Promise<{
|
|
7
6
|
browser: Browser;
|
|
8
7
|
context: BrowserContext;
|
|
9
8
|
page: Page;
|
|
10
9
|
}>;
|
|
10
|
+
export declare function getPage(): Promise<Page | null>;
|
|
11
11
|
export declare function closeBrowser(platform?: string): Promise<void>;
|
|
12
12
|
export declare function humanDelay(min?: number, max?: number): Promise<void>;
|
package/dist/browser.js
CHANGED
|
@@ -1,46 +1,92 @@
|
|
|
1
|
-
import { chromium } from 'playwright
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { loadSession } from './session.js';
|
|
6
|
+
const STATE_FILE = join(homedir(), '.veil', 'browser.json');
|
|
7
|
+
// Singleton references (in-process)
|
|
6
8
|
let _browser = null;
|
|
7
9
|
let _context = null;
|
|
8
10
|
let _page = null;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
let _platform = 'default';
|
|
12
|
+
export async function ensureBrowser(opts = {}) {
|
|
13
|
+
const platform = opts.platform ?? 'default';
|
|
14
|
+
if (_browser && _context && _page && !_page.isClosed()) {
|
|
15
|
+
return { browser: _browser, context: _context, page: _page };
|
|
16
|
+
}
|
|
17
|
+
// Launch browser with stealth args
|
|
18
|
+
const browser = await chromium.launch({
|
|
19
|
+
headless: !opts.headed,
|
|
13
20
|
args: [
|
|
14
|
-
'--no-sandbox',
|
|
15
21
|
'--disable-blink-features=AutomationControlled',
|
|
22
|
+
'--no-sandbox',
|
|
23
|
+
'--disable-setuid-sandbox',
|
|
16
24
|
'--disable-infobars',
|
|
25
|
+
'--disable-dev-shm-usage',
|
|
26
|
+
'--disable-accelerated-2d-canvas',
|
|
27
|
+
'--no-first-run',
|
|
28
|
+
'--no-zygote',
|
|
29
|
+
'--disable-gpu',
|
|
17
30
|
'--window-size=1280,800',
|
|
31
|
+
'--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
18
32
|
],
|
|
19
33
|
});
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
storageState: storageState ?? undefined,
|
|
34
|
+
// Create context with realistic settings
|
|
35
|
+
const context = await browser.newContext({
|
|
23
36
|
viewport: { width: 1280, height: 800 },
|
|
24
|
-
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
37
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
25
38
|
locale: 'en-US',
|
|
26
|
-
timezoneId: '
|
|
39
|
+
timezoneId: 'America/New_York',
|
|
40
|
+
permissions: ['notifications'],
|
|
41
|
+
extraHTTPHeaders: {
|
|
42
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
// Inject stealth scripts
|
|
46
|
+
await context.addInitScript(() => {
|
|
47
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
48
|
+
window.chrome = { runtime: {} };
|
|
49
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
50
|
+
get: () => [1, 2, 3, 4, 5],
|
|
51
|
+
});
|
|
52
|
+
Object.defineProperty(navigator, 'languages', {
|
|
53
|
+
get: () => ['en-US', 'en'],
|
|
54
|
+
});
|
|
27
55
|
});
|
|
28
|
-
|
|
29
|
-
|
|
56
|
+
// Restore session cookies if available
|
|
57
|
+
const session = await loadSession(platform);
|
|
58
|
+
if (session?.cookies && session.cookies.length > 0) {
|
|
59
|
+
await context.addCookies(session.cookies);
|
|
60
|
+
}
|
|
61
|
+
const page = await context.newPage();
|
|
62
|
+
_browser = browser;
|
|
63
|
+
_context = context;
|
|
64
|
+
_page = page;
|
|
65
|
+
_platform = platform;
|
|
66
|
+
// Save state for reference
|
|
67
|
+
await fs.mkdir(join(homedir(), '.veil'), { recursive: true });
|
|
68
|
+
await fs.writeFile(STATE_FILE, JSON.stringify({
|
|
69
|
+
platform,
|
|
70
|
+
pid: process.pid,
|
|
71
|
+
timestamp: Date.now(),
|
|
72
|
+
}), 'utf-8').catch(() => { });
|
|
73
|
+
return { browser, context, page };
|
|
74
|
+
}
|
|
75
|
+
export async function getPage() {
|
|
76
|
+
if (_page && !_page.isClosed())
|
|
77
|
+
return _page;
|
|
78
|
+
return null;
|
|
30
79
|
}
|
|
31
80
|
export async function closeBrowser(platform) {
|
|
32
|
-
if (_context && platform) {
|
|
33
|
-
const state = await _context.storageState();
|
|
34
|
-
await saveSession(platform, state);
|
|
35
|
-
}
|
|
36
81
|
if (_browser) {
|
|
37
|
-
await _browser.close();
|
|
82
|
+
await _browser.close().catch(() => { });
|
|
38
83
|
_browser = null;
|
|
39
84
|
_context = null;
|
|
40
85
|
_page = null;
|
|
41
86
|
}
|
|
87
|
+
await fs.rm(STATE_FILE).catch(() => { });
|
|
42
88
|
}
|
|
43
|
-
export function humanDelay(min =
|
|
44
|
-
const
|
|
45
|
-
return new Promise(
|
|
89
|
+
export function humanDelay(min = 500, max = 1200) {
|
|
90
|
+
const delay = Math.floor(Math.random() * (max - min) + min);
|
|
91
|
+
return new Promise(r => setTimeout(r, delay));
|
|
46
92
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,171 +1,386 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { navigateCommand } from './commands/navigate.js';
|
|
7
|
-
import { actCommand } from './commands/act.js';
|
|
8
|
-
import { extractCommand } from './commands/extract.js';
|
|
9
|
-
import { screenshotCommand } from './commands/screenshot.js';
|
|
10
|
-
import { sessionListCommand, logoutCommand } from './commands/session.js';
|
|
11
|
-
import { searchCommand } from './commands/search.js';
|
|
12
|
-
import { startMcpServer } from './mcp.js';
|
|
4
|
+
import { ensureBrowser, closeBrowser, getPage } from './browser.js';
|
|
5
|
+
import { saveSession } from './session.js';
|
|
13
6
|
const program = new Command();
|
|
14
7
|
program
|
|
15
8
|
.name('veil')
|
|
16
|
-
.description(
|
|
17
|
-
.version('0.
|
|
18
|
-
//
|
|
9
|
+
.description('🕶️ OpenClaw browser remote — stealth headless browser')
|
|
10
|
+
.version('0.2.0');
|
|
11
|
+
// ─── Session ──────────────────────────────────────────────────────────────────
|
|
19
12
|
program
|
|
20
13
|
.command('login <platform>')
|
|
21
|
-
.description('Open visible browser
|
|
22
|
-
.action(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
.
|
|
57
|
-
.option('
|
|
58
|
-
.option('--
|
|
59
|
-
.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
14
|
+
.description('Open visible browser to log in and save session (x, linkedin, reddit)')
|
|
15
|
+
.action(async (platform) => {
|
|
16
|
+
const platformUrls = {
|
|
17
|
+
x: 'https://x.com/login',
|
|
18
|
+
twitter: 'https://x.com/login',
|
|
19
|
+
linkedin: 'https://www.linkedin.com/login',
|
|
20
|
+
reddit: 'https://www.reddit.com/login',
|
|
21
|
+
bluesky: 'https://bsky.app',
|
|
22
|
+
};
|
|
23
|
+
const url = platformUrls[platform.toLowerCase()] ?? `https://${platform}`;
|
|
24
|
+
const { browser, context, page } = await ensureBrowser({ headed: true, platform });
|
|
25
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
26
|
+
console.log(chalk.cyan(`\n🔐 Log into ${platform} in the browser window.`));
|
|
27
|
+
console.log(chalk.gray(' Press Enter here when done.\n'));
|
|
28
|
+
await new Promise(res => process.stdin.once('data', () => res()));
|
|
29
|
+
await saveSession(platform, context);
|
|
30
|
+
console.log(chalk.green(`✅ Session saved for ${platform}`));
|
|
31
|
+
await browser.close();
|
|
32
|
+
});
|
|
33
|
+
program
|
|
34
|
+
.command('sessions')
|
|
35
|
+
.description('List saved sessions')
|
|
36
|
+
.action(async () => {
|
|
37
|
+
const { listSessions } = await import('./session.js');
|
|
38
|
+
const sessions = await listSessions();
|
|
39
|
+
if (sessions.length === 0) {
|
|
40
|
+
console.log(chalk.gray('No sessions saved. Run: veil login <platform>'));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
sessions.forEach(s => console.log(chalk.green(` ✓ ${s}`)));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// ─── Navigation ───────────────────────────────────────────────────────────────
|
|
47
|
+
program
|
|
48
|
+
.command('go <url>')
|
|
49
|
+
.description('Navigate to a URL')
|
|
50
|
+
.option('--platform <platform>', 'Platform for session restore', 'default')
|
|
51
|
+
.option('--wait <event>', 'Wait event: load|domcontentloaded|networkidle', 'domcontentloaded')
|
|
52
|
+
.option('--timeout <ms>', 'Timeout in ms', '30000')
|
|
53
|
+
.action(async (url, opts) => {
|
|
54
|
+
const { page } = await ensureBrowser({ platform: opts.platform });
|
|
55
|
+
try {
|
|
56
|
+
await page.goto(url, { waitUntil: opts.wait, timeout: parseInt(opts.timeout) });
|
|
57
|
+
const title = await page.title();
|
|
58
|
+
const finalUrl = page.url();
|
|
59
|
+
console.log(JSON.stringify({ ok: true, url: finalUrl, title }));
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
program
|
|
67
|
+
.command('url')
|
|
68
|
+
.description('Get current URL')
|
|
69
|
+
.action(async () => {
|
|
70
|
+
const page = await getPage();
|
|
71
|
+
if (!page) {
|
|
72
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser session open' }));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
console.log(JSON.stringify({ ok: true, url: page.url(), title: await page.title() }));
|
|
76
|
+
});
|
|
77
|
+
program
|
|
78
|
+
.command('back')
|
|
79
|
+
.description('Navigate back')
|
|
80
|
+
.action(async () => {
|
|
81
|
+
const page = await getPage();
|
|
82
|
+
if (!page) {
|
|
83
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser session open' }));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
await page.goBack();
|
|
87
|
+
console.log(JSON.stringify({ ok: true, url: page.url() }));
|
|
88
|
+
});
|
|
89
|
+
// ─── Interaction ──────────────────────────────────────────────────────────────
|
|
90
|
+
program
|
|
91
|
+
.command('click <selector>')
|
|
92
|
+
.description('Click an element by CSS selector or data-testid')
|
|
93
|
+
.option('--nth <n>', 'Which match (0-indexed)', '0')
|
|
94
|
+
.option('--force', 'Force click (bypass overlays)', false)
|
|
95
|
+
.option('--timeout <ms>', 'Timeout', '5000')
|
|
96
|
+
.action(async (selector, opts) => {
|
|
97
|
+
const page = await getPage();
|
|
98
|
+
if (!page) {
|
|
99
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser open. Run: veil go <url>' }));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const el = page.locator(selector).nth(parseInt(opts.nth));
|
|
104
|
+
await el.waitFor({ timeout: parseInt(opts.timeout) });
|
|
105
|
+
await el.click({ force: opts.force, timeout: parseInt(opts.timeout) });
|
|
106
|
+
console.log(JSON.stringify({ ok: true, selector, nth: opts.nth }));
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
console.log(JSON.stringify({ ok: false, error: err.message, selector }));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
program
|
|
114
|
+
.command('type <selector> <text>')
|
|
115
|
+
.description('Type text into an element')
|
|
116
|
+
.option('--clear', 'Clear field first', false)
|
|
117
|
+
.option('--delay <ms>', 'Delay between keystrokes in ms', '40')
|
|
118
|
+
.option('--nth <n>', 'Which match (0-indexed)', '0')
|
|
119
|
+
.action(async (selector, text, opts) => {
|
|
120
|
+
const page = await getPage();
|
|
121
|
+
if (!page) {
|
|
122
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser open' }));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const el = page.locator(selector).nth(parseInt(opts.nth));
|
|
127
|
+
await el.waitFor({ timeout: 5000 });
|
|
128
|
+
if (opts.clear)
|
|
129
|
+
await el.clear();
|
|
130
|
+
await el.click({ force: true });
|
|
131
|
+
await page.keyboard.type(text, { delay: parseInt(opts.delay) });
|
|
132
|
+
console.log(JSON.stringify({ ok: true, selector, typed: text }));
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
console.log(JSON.stringify({ ok: false, error: err.message, selector }));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
program
|
|
140
|
+
.command('press <key>')
|
|
141
|
+
.description('Press a keyboard key (Enter, Tab, Escape, ArrowDown...)')
|
|
142
|
+
.action(async (key) => {
|
|
143
|
+
const page = await getPage();
|
|
144
|
+
if (!page) {
|
|
145
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser open' }));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
await page.keyboard.press(key);
|
|
149
|
+
console.log(JSON.stringify({ ok: true, key }));
|
|
150
|
+
});
|
|
151
|
+
program
|
|
152
|
+
.command('scroll <direction>')
|
|
153
|
+
.description('Scroll page: up, down, top, bottom')
|
|
154
|
+
.option('--amount <px>', 'Pixels to scroll', '600')
|
|
155
|
+
.action(async (direction, opts) => {
|
|
156
|
+
const page = await getPage();
|
|
157
|
+
if (!page) {
|
|
158
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser open' }));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
const amount = parseInt(opts.amount);
|
|
162
|
+
const scrollMap = {
|
|
163
|
+
down: `window.scrollBy(0, ${amount})`,
|
|
164
|
+
up: `window.scrollBy(0, -${amount})`,
|
|
165
|
+
top: 'window.scrollTo(0, 0)',
|
|
166
|
+
bottom: 'window.scrollTo(0, document.body.scrollHeight)',
|
|
167
|
+
};
|
|
168
|
+
await page.evaluate(scrollMap[direction] ?? scrollMap.down);
|
|
169
|
+
console.log(JSON.stringify({ ok: true, direction, amount }));
|
|
170
|
+
});
|
|
171
|
+
program
|
|
172
|
+
.command('wait <ms>')
|
|
173
|
+
.description('Wait for N milliseconds')
|
|
174
|
+
.action(async (ms) => {
|
|
175
|
+
await new Promise(r => setTimeout(r, parseInt(ms)));
|
|
176
|
+
console.log(JSON.stringify({ ok: true, waited: parseInt(ms) }));
|
|
177
|
+
});
|
|
178
|
+
program
|
|
179
|
+
.command('wait-for <selector>')
|
|
180
|
+
.description('Wait until selector appears on the page')
|
|
181
|
+
.option('--timeout <ms>', 'Timeout', '10000')
|
|
182
|
+
.action(async (selector, opts) => {
|
|
183
|
+
const page = await getPage();
|
|
184
|
+
if (!page) {
|
|
185
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser open' }));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
84
188
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
console.log(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
189
|
+
await page.waitForSelector(selector, { timeout: parseInt(opts.timeout) });
|
|
190
|
+
console.log(JSON.stringify({ ok: true, selector }));
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.log(JSON.stringify({ ok: false, error: `Timeout waiting for: ${selector}` }));
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
// ─── Reading / Extraction ─────────────────────────────────────────────────────
|
|
198
|
+
program
|
|
199
|
+
.command('read [selector]')
|
|
200
|
+
.description('Read text from page or specific element')
|
|
201
|
+
.option('--all', 'Return all matches as array', false)
|
|
202
|
+
.option('--attr <attribute>', 'Read attribute instead of text (e.g. href, src)')
|
|
203
|
+
.action(async (selector, opts) => {
|
|
204
|
+
const page = await getPage();
|
|
205
|
+
if (!page) {
|
|
206
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser open' }));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
if (!selector) {
|
|
211
|
+
// Full page text
|
|
212
|
+
const text = await page.evaluate(() => document.body.innerText);
|
|
213
|
+
console.log(JSON.stringify({ ok: true, text: text.slice(0, 5000) }));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (opts.all) {
|
|
217
|
+
const items = await page.locator(selector).allTextContents();
|
|
218
|
+
console.log(JSON.stringify({ ok: true, items }));
|
|
219
|
+
}
|
|
220
|
+
else if (opts.attr) {
|
|
221
|
+
const val = await page.locator(selector).first().getAttribute(opts.attr);
|
|
222
|
+
console.log(JSON.stringify({ ok: true, value: val }));
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const text = await page.locator(selector).first().textContent();
|
|
226
|
+
console.log(JSON.stringify({ ok: true, text }));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
program
|
|
235
|
+
.command('snapshot')
|
|
236
|
+
.description('Get full page accessibility snapshot (ARIA tree) for reasoning')
|
|
237
|
+
.option('--max <chars>', 'Max chars to return', '8000')
|
|
238
|
+
.action(async (opts) => {
|
|
239
|
+
const page = await getPage();
|
|
240
|
+
if (!page) {
|
|
241
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser open' }));
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
// Use DOM snapshot instead of deprecated accessibility
|
|
245
|
+
const snapshot = await page.evaluate((max) => {
|
|
246
|
+
function nodeToObj(el, depth = 0) {
|
|
247
|
+
if (depth > 8)
|
|
248
|
+
return null;
|
|
249
|
+
const obj = {
|
|
250
|
+
tag: el.tagName?.toLowerCase(),
|
|
251
|
+
role: el.getAttribute('role'),
|
|
252
|
+
label: el.getAttribute('aria-label'),
|
|
253
|
+
testid: el.getAttribute('data-testid'),
|
|
254
|
+
text: el instanceof HTMLElement && !el.children.length ? el.innerText?.slice(0, 100) : undefined,
|
|
255
|
+
href: el instanceof HTMLAnchorElement ? el.href : undefined,
|
|
256
|
+
};
|
|
257
|
+
// Remove undefined keys
|
|
258
|
+
Object.keys(obj).forEach(k => obj[k] === undefined && delete obj[k]);
|
|
259
|
+
const children = Array.from(el.children)
|
|
260
|
+
.map(c => nodeToObj(c, depth + 1))
|
|
261
|
+
.filter(Boolean)
|
|
262
|
+
.slice(0, 10);
|
|
263
|
+
if (children.length)
|
|
264
|
+
obj.children = children;
|
|
265
|
+
return obj;
|
|
266
|
+
}
|
|
267
|
+
return JSON.stringify(nodeToObj(document.body), null, 2).slice(0, max);
|
|
268
|
+
}, parseInt(opts.max));
|
|
269
|
+
console.log(JSON.stringify({ ok: true, snapshot, url: page.url() }));
|
|
270
|
+
});
|
|
271
|
+
program
|
|
272
|
+
.command('find <text>')
|
|
273
|
+
.description('Check if text exists on the current page')
|
|
274
|
+
.action(async (text) => {
|
|
275
|
+
const page = await getPage();
|
|
276
|
+
if (!page) {
|
|
277
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser open' }));
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
const found = await page.getByText(text).first().isVisible().catch(() => false);
|
|
281
|
+
console.log(JSON.stringify({ ok: true, found, text }));
|
|
282
|
+
});
|
|
283
|
+
program
|
|
284
|
+
.command('exists <selector>')
|
|
285
|
+
.description('Check if a selector exists on the page')
|
|
286
|
+
.action(async (selector) => {
|
|
287
|
+
const page = await getPage();
|
|
288
|
+
if (!page) {
|
|
289
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser open' }));
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
const count = await page.locator(selector).count();
|
|
293
|
+
console.log(JSON.stringify({ ok: true, exists: count > 0, count, selector }));
|
|
294
|
+
});
|
|
295
|
+
// ─── Screenshots ──────────────────────────────────────────────────────────────
|
|
296
|
+
program
|
|
297
|
+
.command('shot [filename]')
|
|
298
|
+
.description('Take a screenshot')
|
|
299
|
+
.option('--selector <sel>', 'Screenshot specific element')
|
|
300
|
+
.option('--full', 'Full page screenshot', false)
|
|
301
|
+
.action(async (filename, opts) => {
|
|
302
|
+
const page = await getPage();
|
|
303
|
+
if (!page) {
|
|
304
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser open' }));
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
const path = filename ?? `veil-${Date.now()}.png`;
|
|
308
|
+
if (opts.selector) {
|
|
309
|
+
await page.locator(opts.selector).first().screenshot({ path });
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
await page.screenshot({ path, fullPage: opts.full });
|
|
313
|
+
}
|
|
314
|
+
console.log(JSON.stringify({ ok: true, path }));
|
|
315
|
+
});
|
|
316
|
+
// ─── Evaluate ─────────────────────────────────────────────────────────────────
|
|
317
|
+
program
|
|
318
|
+
.command('eval <script>')
|
|
319
|
+
.description('Run JavaScript in the browser and return result')
|
|
320
|
+
.action(async (script) => {
|
|
321
|
+
const page = await getPage();
|
|
322
|
+
if (!page) {
|
|
323
|
+
console.log(JSON.stringify({ ok: false, error: 'No browser open' }));
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const result = await page.evaluate(script);
|
|
328
|
+
console.log(JSON.stringify({ ok: true, result }));
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
// ─── Session management ───────────────────────────────────────────────────────
|
|
336
|
+
program
|
|
337
|
+
.command('open <platform>')
|
|
338
|
+
.description('Open a browser session using saved login cookies for a platform')
|
|
339
|
+
.option('--headed', 'Show browser window', false)
|
|
340
|
+
.action(async (platform, opts) => {
|
|
341
|
+
const { browser, context, page } = await ensureBrowser({ headed: opts.headed, platform });
|
|
342
|
+
const platformUrls = {
|
|
343
|
+
x: 'https://x.com/home',
|
|
344
|
+
twitter: 'https://x.com/home',
|
|
345
|
+
linkedin: 'https://www.linkedin.com/feed',
|
|
346
|
+
reddit: 'https://www.reddit.com',
|
|
347
|
+
bluesky: 'https://bsky.app',
|
|
139
348
|
};
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
.
|
|
151
|
-
.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// --- Status ---
|
|
349
|
+
const url = platformUrls[platform.toLowerCase()] ?? `https://${platform}`;
|
|
350
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
351
|
+
const title = await page.title();
|
|
352
|
+
console.log(JSON.stringify({ ok: true, platform, url: page.url(), title }));
|
|
353
|
+
});
|
|
354
|
+
program
|
|
355
|
+
.command('close')
|
|
356
|
+
.description('Close the current browser session')
|
|
357
|
+
.option('--platform <platform>', 'Platform to close', 'default')
|
|
358
|
+
.action(async (opts) => {
|
|
359
|
+
await closeBrowser(opts.platform);
|
|
360
|
+
console.log(JSON.stringify({ ok: true }));
|
|
361
|
+
});
|
|
362
|
+
// ─── Status ───────────────────────────────────────────────────────────────────
|
|
155
363
|
program
|
|
156
364
|
.command('status')
|
|
157
|
-
.description('Show veil status
|
|
365
|
+
.description('Show veil status')
|
|
158
366
|
.action(async () => {
|
|
159
367
|
const { listSessions } = await import('./session.js');
|
|
160
368
|
const { isFlareSolverrUp } = await import('./local-captcha.js');
|
|
161
369
|
const sessions = await listSessions();
|
|
162
370
|
const flare = await isFlareSolverrUp();
|
|
163
|
-
console.log(chalk.cyan('\n🕶️ veil v0.
|
|
164
|
-
console.log(` Sessions: ${sessions.length > 0 ? chalk.
|
|
165
|
-
console.log(` FlareSolverr: ${flare ? chalk.green('running') : chalk.gray('not running')}`);
|
|
166
|
-
console.log(
|
|
371
|
+
console.log(chalk.cyan('\n🕶️ veil v0.2.0 — OpenClaw Browser Remote\n'));
|
|
372
|
+
console.log(` Sessions: ${sessions.length > 0 ? chalk.green(sessions.join(', ')) : chalk.gray('none')}`);
|
|
373
|
+
console.log(` FlareSolverr: ${flare ? chalk.green('running') : chalk.gray('not running (auto-starts on use)')}`);
|
|
374
|
+
console.log('');
|
|
375
|
+
console.log(chalk.gray(' Quick reference:'));
|
|
376
|
+
console.log(chalk.gray(' veil login x # save X session'));
|
|
377
|
+
console.log(chalk.gray(' veil open x # restore X session'));
|
|
378
|
+
console.log(chalk.gray(' veil go <url> # navigate'));
|
|
379
|
+
console.log(chalk.gray(' veil snapshot # read current page'));
|
|
380
|
+
console.log(chalk.gray(' veil click <sel> # click element'));
|
|
381
|
+
console.log(chalk.gray(' veil type <sel> <text>'));
|
|
382
|
+
console.log(chalk.gray(' veil read [sel] # extract text'));
|
|
383
|
+
console.log(chalk.gray(' veil shot # screenshot'));
|
|
167
384
|
console.log('');
|
|
168
385
|
});
|
|
169
|
-
// Boot FlareSolverr in background on any command
|
|
170
|
-
veilStartup();
|
|
171
386
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "veil-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Stealth browser CLI for AI agents — bypass bot detection, persist sessions, local CAPTCHA solving, MCP server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"browser",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"dist/**/*",
|
|
34
|
-
"README.md"
|
|
34
|
+
"README.md",
|
|
35
|
+
"SKILL.md"
|
|
35
36
|
],
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"chalk": "^5.3.0",
|