@askjo/camofox-browser 1.0.14 → 1.1.1
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 +175 -14
- package/lib/macros.js +2 -1
- package/package.json +1 -1
- package/plugin.ts +142 -14
- package/server.js +337 -105
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<a href="https://hub.docker.com"><img src="https://img.shields.io/badge/docker-ready-blue" alt="Docker" /></a>
|
|
10
10
|
</p>
|
|
11
11
|
<p>
|
|
12
|
-
Standing on the mighty shoulders of <a href="https://camoufox.com">Camoufox</a>
|
|
12
|
+
Standing on the mighty shoulders of <a href="https://camoufox.com">Camoufox</a> - a Firefox fork with fingerprint spoofing at the C++ level.
|
|
13
13
|
<br/><br/>
|
|
14
14
|
The same engine behind <a href="https://askjo.ai">askjo.ai</a>'s web browsing.
|
|
15
15
|
</p>
|
|
@@ -23,18 +23,21 @@
|
|
|
23
23
|
|
|
24
24
|
AI agents need to browse the real web. Playwright gets blocked. Headless Chrome gets fingerprinted. Stealth plugins become the fingerprint.
|
|
25
25
|
|
|
26
|
-
Camoufox patches Firefox at the **C++ implementation level**
|
|
26
|
+
Camoufox patches Firefox at the **C++ implementation level** - `navigator.hardwareConcurrency`, WebGL renderers, AudioContext, screen geometry, WebRTC - all spoofed before JavaScript ever sees them. No shims, no wrappers, no tells.
|
|
27
27
|
|
|
28
28
|
This project wraps that engine in a REST API built for agents: accessibility snapshots instead of bloated HTML, stable element refs for clicking, and search macros for common sites.
|
|
29
29
|
|
|
30
30
|
## Features
|
|
31
31
|
|
|
32
|
-
- **C++ Anti-Detection**
|
|
33
|
-
- **Element Refs**
|
|
34
|
-
- **Token-Efficient**
|
|
35
|
-
- **Session Isolation**
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
32
|
+
- **C++ Anti-Detection** - bypasses Google, Cloudflare, and most bot detection
|
|
33
|
+
- **Element Refs** - stable `e1`, `e2`, `e3` identifiers for reliable interaction
|
|
34
|
+
- **Token-Efficient** - accessibility snapshots are ~90% smaller than raw HTML
|
|
35
|
+
- **Session Isolation** - separate cookies/storage per user
|
|
36
|
+
- **Cookie Import** - inject Netscape-format cookie files for authenticated browsing
|
|
37
|
+
- **Proxy + GeoIP** - route traffic through residential proxies with automatic locale/timezone
|
|
38
|
+
- **Structured Logging** - JSON log lines with request IDs for production observability
|
|
39
|
+
- **Search Macros** - `@google_search`, `@youtube_search`, `@amazon_search`, `@reddit_subreddit`, and 10 more
|
|
40
|
+
- **Deploy Anywhere** - Docker, Fly.io, Railway
|
|
38
41
|
|
|
39
42
|
## Quick Start
|
|
40
43
|
|
|
@@ -44,7 +47,7 @@ This project wraps that engine in a REST API built for agents: accessibility sna
|
|
|
44
47
|
openclaw plugins install @askjo/camofox-browser
|
|
45
48
|
```
|
|
46
49
|
|
|
47
|
-
**Tools:** `camofox_create_tab` · `camofox_snapshot` · `camofox_click` · `camofox_type` · `camofox_navigate` · `camofox_scroll` · `camofox_screenshot` · `camofox_close_tab` · `camofox_list_tabs`
|
|
50
|
+
**Tools:** `camofox_create_tab` · `camofox_snapshot` · `camofox_click` · `camofox_type` · `camofox_navigate` · `camofox_scroll` · `camofox_screenshot` · `camofox_close_tab` · `camofox_list_tabs` · `camofox_import_cookies`
|
|
48
51
|
|
|
49
52
|
### Standalone
|
|
50
53
|
|
|
@@ -55,7 +58,7 @@ npm install
|
|
|
55
58
|
npm start # downloads Camoufox on first run (~300MB)
|
|
56
59
|
```
|
|
57
60
|
|
|
58
|
-
Default port is `9377`.
|
|
61
|
+
Default port is `9377`. See [Environment Variables](#environment-variables) for all options.
|
|
59
62
|
|
|
60
63
|
### Docker
|
|
61
64
|
|
|
@@ -70,6 +73,141 @@ docker run -p 9377:9377 camofox-browser
|
|
|
70
73
|
|
|
71
74
|
## Usage
|
|
72
75
|
|
|
76
|
+
### Cookie Import
|
|
77
|
+
|
|
78
|
+
Import cookies from your browser into Camoufox to skip interactive login on sites like LinkedIn, Amazon, etc.
|
|
79
|
+
|
|
80
|
+
#### Setup
|
|
81
|
+
|
|
82
|
+
**1. Generate a secret key:**
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# macOS / Linux
|
|
86
|
+
openssl rand -hex 32
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**2. Set the environment variable before starting OpenClaw:**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export CAMOFOX_API_KEY="your-generated-key"
|
|
93
|
+
openclaw start
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The same key is used by both the plugin (to authenticate requests) and the server (to verify them). Both run from the same environment — set it once.
|
|
97
|
+
|
|
98
|
+
> **Why an env var?** The key is a secret. Plugin config in `openclaw.json` is stored in plaintext, so secrets don't belong there. Set `CAMOFOX_API_KEY` in your shell profile, systemd unit, Docker env, or Fly.io secrets.
|
|
99
|
+
|
|
100
|
+
> **Cookie import is disabled by default.** If `CAMOFOX_API_KEY` is not set, the server rejects all cookie requests with 403.
|
|
101
|
+
|
|
102
|
+
**3. Export cookies from your browser:**
|
|
103
|
+
|
|
104
|
+
Install a browser extension that exports Netscape-format cookie files (e.g., "cookies.txt" for Chrome/Firefox). Export the cookies for the site you want to authenticate.
|
|
105
|
+
|
|
106
|
+
**4. Place the cookie file:**
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
mkdir -p ~/.camofox/cookies
|
|
110
|
+
cp ~/Downloads/linkedin_cookies.txt ~/.camofox/cookies/linkedin.txt
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The default directory is `~/.camofox/cookies/`. Override with `CAMOFOX_COOKIES_DIR`.
|
|
114
|
+
|
|
115
|
+
**5. Ask your agent to import them:**
|
|
116
|
+
|
|
117
|
+
> Import my LinkedIn cookies from linkedin.txt
|
|
118
|
+
|
|
119
|
+
The agent calls `camofox_import_cookies` → reads the file → POSTs to the server with the Bearer token → cookies are injected into the browser session. Subsequent `camofox_create_tab` calls to linkedin.com will be authenticated.
|
|
120
|
+
|
|
121
|
+
#### How it works
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
~/.camofox/cookies/linkedin.txt (Netscape format, on disk)
|
|
125
|
+
│
|
|
126
|
+
▼
|
|
127
|
+
camofox_import_cookies tool (parses file, filters by domain)
|
|
128
|
+
│
|
|
129
|
+
▼ POST /sessions/:userId/cookies
|
|
130
|
+
│ Authorization: Bearer <CAMOFOX_API_KEY>
|
|
131
|
+
│ Body: { cookies: [Playwright cookie objects] }
|
|
132
|
+
▼
|
|
133
|
+
camofox server (validates, sanitizes, injects)
|
|
134
|
+
│
|
|
135
|
+
▼ context.addCookies(...)
|
|
136
|
+
│
|
|
137
|
+
Camoufox browser session (authenticated browsing)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- `cookiesPath` is resolved relative to the cookies directory — path traversal outside it is blocked
|
|
141
|
+
- Max 500 cookies per request, 5MB file size limit
|
|
142
|
+
- Cookie objects are sanitized to an allowlist of Playwright fields
|
|
143
|
+
|
|
144
|
+
#### Standalone server usage
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
curl -X POST http://localhost:9377/sessions/agent1/cookies \
|
|
148
|
+
-H 'Content-Type: application/json' \
|
|
149
|
+
-H 'Authorization: Bearer YOUR_CAMOFOX_API_KEY' \
|
|
150
|
+
-d '{"cookies":[{"name":"foo","value":"bar","domain":"example.com","path":"/","expires":-1,"httpOnly":false,"secure":false}]}'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### Docker / Fly.io
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
docker run -p 9377:9377 \
|
|
157
|
+
-e CAMOFOX_API_KEY="your-generated-key" \
|
|
158
|
+
-v ~/.camofox/cookies:/home/node/.camofox/cookies:ro \
|
|
159
|
+
camofox-browser
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
For Fly.io:
|
|
163
|
+
```bash
|
|
164
|
+
fly secrets set CAMOFOX_API_KEY="your-generated-key"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Proxy + GeoIP
|
|
168
|
+
|
|
169
|
+
Route all browser traffic through a proxy with automatic locale, timezone, and geolocation derived from the proxy's IP address via Camoufox's built-in GeoIP.
|
|
170
|
+
|
|
171
|
+
Set these environment variables before starting the server:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
export PROXY_HOST=166.88.179.132
|
|
175
|
+
export PROXY_PORT=46040
|
|
176
|
+
export PROXY_USERNAME=myuser
|
|
177
|
+
export PROXY_PASSWORD=mypass
|
|
178
|
+
npm start
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Or in Docker:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
docker run -p 9377:9377 \
|
|
185
|
+
-e PROXY_HOST=166.88.179.132 \
|
|
186
|
+
-e PROXY_PORT=46040 \
|
|
187
|
+
-e PROXY_USERNAME=myuser \
|
|
188
|
+
-e PROXY_PASSWORD=mypass \
|
|
189
|
+
camofox-browser
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
When a proxy is configured:
|
|
193
|
+
- All traffic routes through the proxy
|
|
194
|
+
- Camoufox's GeoIP automatically sets `locale`, `timezone`, and `geolocation` to match the proxy's exit IP
|
|
195
|
+
- Browser fingerprint (language, timezone, coordinates) is consistent with the proxy location
|
|
196
|
+
- Without a proxy, defaults to `en-US`, `America/Los_Angeles`, San Francisco coordinates
|
|
197
|
+
|
|
198
|
+
### Structured Logging
|
|
199
|
+
|
|
200
|
+
All log output is JSON (one object per line) for easy parsing by log aggregators:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{"ts":"2026-02-11T23:45:01.234Z","level":"info","msg":"req","reqId":"a1b2c3d4","method":"POST","path":"/tabs","userId":"agent1"}
|
|
204
|
+
{"ts":"2026-02-11T23:45:01.567Z","level":"info","msg":"res","reqId":"a1b2c3d4","status":200,"ms":333}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Health check requests (`/health`) are excluded from request logging to reduce noise.
|
|
208
|
+
|
|
209
|
+
### Basic Browsing
|
|
210
|
+
|
|
73
211
|
```bash
|
|
74
212
|
# Create a tab
|
|
75
213
|
curl -X POST http://localhost:9377/tabs \
|
|
@@ -134,15 +272,38 @@ curl -X POST http://localhost:9377/tabs/TAB_ID/navigate \
|
|
|
134
272
|
| `POST` | `/start` | Start browser engine |
|
|
135
273
|
| `POST` | `/stop` | Stop browser engine |
|
|
136
274
|
|
|
275
|
+
### Sessions
|
|
276
|
+
|
|
277
|
+
| Method | Endpoint | Description |
|
|
278
|
+
|--------|----------|-------------|
|
|
279
|
+
| `POST` | `/sessions/:userId/cookies` | Add cookies to a user session (Playwright cookie objects) |
|
|
280
|
+
|
|
137
281
|
## Search Macros
|
|
138
282
|
|
|
139
|
-
`@google_search` · `@youtube_search` · `@amazon_search` · `@reddit_search` · `@wikipedia_search` · `@twitter_search` · `@yelp_search` · `@spotify_search` · `@netflix_search` · `@linkedin_search` · `@instagram_search` · `@tiktok_search` · `@twitch_search`
|
|
283
|
+
`@google_search` · `@youtube_search` · `@amazon_search` · `@reddit_search` · `@reddit_subreddit` · `@wikipedia_search` · `@twitter_search` · `@yelp_search` · `@spotify_search` · `@netflix_search` · `@linkedin_search` · `@instagram_search` · `@tiktok_search` · `@twitch_search`
|
|
284
|
+
|
|
285
|
+
Reddit macros return JSON directly (no HTML parsing needed):
|
|
286
|
+
- `@reddit_search` - search all of Reddit, returns JSON with 25 results
|
|
287
|
+
- `@reddit_subreddit` - browse a subreddit (e.g., query `"programming"` → `/r/programming.json`)
|
|
288
|
+
|
|
289
|
+
## Environment Variables
|
|
290
|
+
|
|
291
|
+
| Variable | Description | Default |
|
|
292
|
+
|----------|-------------|---------|
|
|
293
|
+
| `CAMOFOX_PORT` | Server port | `9377` |
|
|
294
|
+
| `CAMOFOX_API_KEY` | Enable cookie import endpoint (disabled if unset) | - |
|
|
295
|
+
| `CAMOFOX_ADMIN_KEY` | Required for `POST /stop` | - |
|
|
296
|
+
| `CAMOFOX_COOKIES_DIR` | Directory for cookie files | `~/.camofox/cookies` |
|
|
297
|
+
| `PROXY_HOST` | Proxy hostname or IP | - |
|
|
298
|
+
| `PROXY_PORT` | Proxy port | - |
|
|
299
|
+
| `PROXY_USERNAME` | Proxy auth username | - |
|
|
300
|
+
| `PROXY_PASSWORD` | Proxy auth password | - |
|
|
140
301
|
|
|
141
302
|
## Architecture
|
|
142
303
|
|
|
143
304
|
```
|
|
144
305
|
Browser Instance (Camoufox)
|
|
145
|
-
└── User Session (BrowserContext)
|
|
306
|
+
└── User Session (BrowserContext) - isolated cookies/storage
|
|
146
307
|
├── Tab Group (sessionKey: "conv1")
|
|
147
308
|
│ ├── Tab (google.com)
|
|
148
309
|
│ └── Tab (github.com)
|
|
@@ -169,9 +330,9 @@ npm install @askjo/camofox-browser
|
|
|
169
330
|
|
|
170
331
|
## Credits
|
|
171
332
|
|
|
172
|
-
- [Camoufox](https://camoufox.com)
|
|
333
|
+
- [Camoufox](https://camoufox.com) - Firefox-based browser with C++ anti-detection
|
|
173
334
|
- [Donate to Camoufox's original creator daijro](https://camoufox.com/about/)
|
|
174
|
-
- [OpenClaw](https://openclaw.ai)
|
|
335
|
+
- [OpenClaw](https://openclaw.ai) - Open-source AI agent framework
|
|
175
336
|
|
|
176
337
|
## Crypto Scam Warning
|
|
177
338
|
|
package/lib/macros.js
CHANGED
|
@@ -2,7 +2,8 @@ const MACROS = {
|
|
|
2
2
|
'@google_search': (query) => `https://www.google.com/search?q=${encodeURIComponent(query || '')}`,
|
|
3
3
|
'@youtube_search': (query) => `https://www.youtube.com/results?search_query=${encodeURIComponent(query || '')}`,
|
|
4
4
|
'@amazon_search': (query) => `https://www.amazon.com/s?k=${encodeURIComponent(query || '')}`,
|
|
5
|
-
'@reddit_search': (query) => `https://www.reddit.com/search
|
|
5
|
+
'@reddit_search': (query) => `https://www.reddit.com/search.json?q=${encodeURIComponent(query || '')}&limit=25`,
|
|
6
|
+
'@reddit_subreddit': (query) => `https://www.reddit.com/r/${encodeURIComponent(query || 'all')}.json?limit=25`,
|
|
6
7
|
'@wikipedia_search': (query) => `https://en.wikipedia.org/wiki/Special:Search?search=${encodeURIComponent(query || '')}`,
|
|
7
8
|
'@twitter_search': (query) => `https://twitter.com/search?q=${encodeURIComponent(query || '')}`,
|
|
8
9
|
'@yelp_search': (query) => `https://www.yelp.com/search?find_desc=${encodeURIComponent(query || '')}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askjo/camofox-browser",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Headless browser automation server and OpenClaw plugin for AI agents - anti-detection, element refs, and session isolation",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"license": "MIT",
|
package/plugin.ts
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { spawn, ChildProcess } from "child_process";
|
|
9
|
-
import { join, dirname } from "path";
|
|
9
|
+
import { join, dirname, resolve, sep } from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
12
|
+
import { homedir } from "os";
|
|
11
13
|
|
|
12
14
|
// Get plugin directory - works in both ESM and CJS contexts
|
|
13
15
|
const getPluginDir = (): string => {
|
|
@@ -91,7 +93,8 @@ interface PluginApi {
|
|
|
91
93
|
name: string,
|
|
92
94
|
check: () => Promise<HealthCheckResult>
|
|
93
95
|
) => void;
|
|
94
|
-
config:
|
|
96
|
+
config: Record<string, unknown>;
|
|
97
|
+
pluginConfig?: PluginConfig;
|
|
95
98
|
log: {
|
|
96
99
|
info: (msg: string) => void;
|
|
97
100
|
error: (msg: string) => void;
|
|
@@ -113,6 +116,13 @@ async function startServer(
|
|
|
113
116
|
HOME: process.env.HOME,
|
|
114
117
|
NODE_ENV: process.env.NODE_ENV,
|
|
115
118
|
CAMOFOX_PORT: String(port),
|
|
119
|
+
CAMOFOX_ADMIN_KEY: process.env.CAMOFOX_ADMIN_KEY,
|
|
120
|
+
CAMOFOX_API_KEY: process.env.CAMOFOX_API_KEY,
|
|
121
|
+
CAMOFOX_COOKIES_DIR: process.env.CAMOFOX_COOKIES_DIR,
|
|
122
|
+
PROXY_HOST: process.env.PROXY_HOST,
|
|
123
|
+
PROXY_PORT: process.env.PROXY_PORT,
|
|
124
|
+
PROXY_USERNAME: process.env.PROXY_USERNAME,
|
|
125
|
+
PROXY_PASSWORD: process.env.PROXY_PASSWORD,
|
|
116
126
|
},
|
|
117
127
|
stdio: ["ignore", "pipe", "pipe"],
|
|
118
128
|
detached: false,
|
|
@@ -192,11 +202,57 @@ function toToolResult(data: unknown): ToolResult {
|
|
|
192
202
|
};
|
|
193
203
|
}
|
|
194
204
|
|
|
205
|
+
function parseNetscapeCookieFile(text: string) {
|
|
206
|
+
// Netscape cookie file format:
|
|
207
|
+
// domain \t includeSubdomains \t path \t secure \t expires \t name \t value
|
|
208
|
+
// HttpOnly cookies are prefixed with: #HttpOnly_
|
|
209
|
+
const cookies: Array<{
|
|
210
|
+
name: string;
|
|
211
|
+
value: string;
|
|
212
|
+
domain: string;
|
|
213
|
+
path: string;
|
|
214
|
+
expires: number;
|
|
215
|
+
httpOnly?: boolean;
|
|
216
|
+
secure?: boolean;
|
|
217
|
+
}> = [];
|
|
218
|
+
|
|
219
|
+
const cleaned = text.replace(/^\uFEFF/, '');
|
|
220
|
+
|
|
221
|
+
for (const rawLine of cleaned.split(/\r?\n/)) {
|
|
222
|
+
const line = rawLine.trim();
|
|
223
|
+
if (!line) continue;
|
|
224
|
+
if (line.startsWith('#') && !line.startsWith('#HttpOnly_')) continue;
|
|
225
|
+
|
|
226
|
+
let httpOnly = false;
|
|
227
|
+
let working = line;
|
|
228
|
+
if (working.startsWith('#HttpOnly_')) {
|
|
229
|
+
httpOnly = true;
|
|
230
|
+
working = working.replace(/^#HttpOnly_/, '');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const parts = working.split('\t');
|
|
234
|
+
if (parts.length < 7) continue;
|
|
235
|
+
|
|
236
|
+
const domain = parts[0];
|
|
237
|
+
const path = parts[2];
|
|
238
|
+
const secure = parts[3].toUpperCase() === 'TRUE';
|
|
239
|
+
const expires = Number(parts[4]);
|
|
240
|
+
const name = parts[5];
|
|
241
|
+
const value = parts.slice(6).join('\t');
|
|
242
|
+
|
|
243
|
+
cookies.push({ name, value, domain, path, expires, httpOnly, secure });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return cookies;
|
|
247
|
+
}
|
|
248
|
+
|
|
195
249
|
export default function register(api: PluginApi) {
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
250
|
+
const cfg = api.pluginConfig ?? (api.config as unknown as PluginConfig);
|
|
251
|
+
const port = cfg.port || 9377;
|
|
252
|
+
const baseUrl = cfg.url || `http://localhost:${port}`;
|
|
253
|
+
const autoStart = cfg.autoStart !== false; // default true
|
|
199
254
|
const pluginDir = getPluginDir();
|
|
255
|
+
const fallbackUserId = `camofox-${randomUUID()}`;
|
|
200
256
|
|
|
201
257
|
// Auto-start server if configured (default: true)
|
|
202
258
|
if (autoStart) {
|
|
@@ -227,7 +283,7 @@ export default function register(api: PluginApi) {
|
|
|
227
283
|
},
|
|
228
284
|
async execute(_id, params) {
|
|
229
285
|
const sessionKey = ctx.sessionKey || "default";
|
|
230
|
-
const userId = ctx.agentId ||
|
|
286
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
231
287
|
const result = await fetchApi(baseUrl, "/tabs", {
|
|
232
288
|
method: "POST",
|
|
233
289
|
body: JSON.stringify({ ...params, userId, sessionKey }),
|
|
@@ -249,7 +305,7 @@ export default function register(api: PluginApi) {
|
|
|
249
305
|
},
|
|
250
306
|
async execute(_id, params) {
|
|
251
307
|
const { tabId } = params as { tabId: string };
|
|
252
|
-
const userId = ctx.agentId ||
|
|
308
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
253
309
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}/snapshot?userId=${userId}`);
|
|
254
310
|
return toToolResult(result);
|
|
255
311
|
},
|
|
@@ -269,7 +325,7 @@ export default function register(api: PluginApi) {
|
|
|
269
325
|
},
|
|
270
326
|
async execute(_id, params) {
|
|
271
327
|
const { tabId, ...rest } = params as { tabId: string } & Record<string, unknown>;
|
|
272
|
-
const userId = ctx.agentId ||
|
|
328
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
273
329
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}/click`, {
|
|
274
330
|
method: "POST",
|
|
275
331
|
body: JSON.stringify({ ...rest, userId }),
|
|
@@ -294,7 +350,7 @@ export default function register(api: PluginApi) {
|
|
|
294
350
|
},
|
|
295
351
|
async execute(_id, params) {
|
|
296
352
|
const { tabId, ...rest } = params as { tabId: string } & Record<string, unknown>;
|
|
297
|
-
const userId = ctx.agentId ||
|
|
353
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
298
354
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}/type`, {
|
|
299
355
|
method: "POST",
|
|
300
356
|
body: JSON.stringify({ ...rest, userId }),
|
|
@@ -337,7 +393,7 @@ export default function register(api: PluginApi) {
|
|
|
337
393
|
},
|
|
338
394
|
async execute(_id, params) {
|
|
339
395
|
const { tabId, ...rest } = params as { tabId: string } & Record<string, unknown>;
|
|
340
|
-
const userId = ctx.agentId ||
|
|
396
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
341
397
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}/navigate`, {
|
|
342
398
|
method: "POST",
|
|
343
399
|
body: JSON.stringify({ ...rest, userId }),
|
|
@@ -360,7 +416,7 @@ export default function register(api: PluginApi) {
|
|
|
360
416
|
},
|
|
361
417
|
async execute(_id, params) {
|
|
362
418
|
const { tabId, ...rest } = params as { tabId: string } & Record<string, unknown>;
|
|
363
|
-
const userId = ctx.agentId ||
|
|
419
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
364
420
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}/scroll`, {
|
|
365
421
|
method: "POST",
|
|
366
422
|
body: JSON.stringify({ ...rest, userId }),
|
|
@@ -381,7 +437,7 @@ export default function register(api: PluginApi) {
|
|
|
381
437
|
},
|
|
382
438
|
async execute(_id, params) {
|
|
383
439
|
const { tabId } = params as { tabId: string };
|
|
384
|
-
const userId = ctx.agentId ||
|
|
440
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
385
441
|
const url = `${baseUrl}/tabs/${tabId}/screenshot?userId=${userId}`;
|
|
386
442
|
const res = await fetch(url);
|
|
387
443
|
if (!res.ok) {
|
|
@@ -414,7 +470,7 @@ export default function register(api: PluginApi) {
|
|
|
414
470
|
},
|
|
415
471
|
async execute(_id, params) {
|
|
416
472
|
const { tabId } = params as { tabId: string };
|
|
417
|
-
const userId = ctx.agentId ||
|
|
473
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
418
474
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}?userId=${userId}`, {
|
|
419
475
|
method: "DELETE",
|
|
420
476
|
});
|
|
@@ -431,12 +487,84 @@ export default function register(api: PluginApi) {
|
|
|
431
487
|
required: [],
|
|
432
488
|
},
|
|
433
489
|
async execute(_id, _params) {
|
|
434
|
-
const userId = ctx.agentId ||
|
|
490
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
435
491
|
const result = await fetchApi(baseUrl, `/tabs?userId=${userId}`);
|
|
436
492
|
return toToolResult(result);
|
|
437
493
|
},
|
|
438
494
|
}));
|
|
439
495
|
|
|
496
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
497
|
+
name: "camofox_import_cookies",
|
|
498
|
+
description:
|
|
499
|
+
"Import cookies into the current Camoufox user session (Netscape cookie file). Use to authenticate to sites like LinkedIn without interactive login.",
|
|
500
|
+
parameters: {
|
|
501
|
+
type: "object",
|
|
502
|
+
properties: {
|
|
503
|
+
cookiesPath: { type: "string", description: "Path to Netscape-format cookies.txt file" },
|
|
504
|
+
domainSuffix: {
|
|
505
|
+
type: "string",
|
|
506
|
+
description: "Only import cookies whose domain ends with this suffix",
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
required: ["cookiesPath"],
|
|
510
|
+
},
|
|
511
|
+
async execute(_id, params) {
|
|
512
|
+
const { cookiesPath, domainSuffix } = params as {
|
|
513
|
+
cookiesPath: string;
|
|
514
|
+
domainSuffix?: string;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
518
|
+
|
|
519
|
+
const fs = await import("fs/promises");
|
|
520
|
+
|
|
521
|
+
const cookiesDir = resolve(process.env.CAMOFOX_COOKIES_DIR || join(homedir(), ".camofox", "cookies"));
|
|
522
|
+
const resolved = resolve(cookiesDir, cookiesPath);
|
|
523
|
+
if (!resolved.startsWith(cookiesDir + sep)) {
|
|
524
|
+
throw new Error("cookiesPath must be a relative path within the cookies directory");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const stat = await fs.stat(resolved);
|
|
528
|
+
if (stat.size > 5 * 1024 * 1024) {
|
|
529
|
+
throw new Error("Cookie file too large (max 5MB)");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const text = await fs.readFile(resolved, "utf8");
|
|
533
|
+
let cookies = parseNetscapeCookieFile(text);
|
|
534
|
+
if (domainSuffix) {
|
|
535
|
+
cookies = cookies.filter((c) => c.domain.endsWith(domainSuffix));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Translate into Playwright cookie objects
|
|
539
|
+
const pwCookies = cookies.map((c) => ({
|
|
540
|
+
name: c.name,
|
|
541
|
+
value: c.value,
|
|
542
|
+
domain: c.domain,
|
|
543
|
+
path: c.path,
|
|
544
|
+
expires: c.expires,
|
|
545
|
+
httpOnly: !!c.httpOnly,
|
|
546
|
+
secure: !!c.secure,
|
|
547
|
+
}));
|
|
548
|
+
|
|
549
|
+
const apiKey = process.env.CAMOFOX_API_KEY;
|
|
550
|
+
if (!apiKey) {
|
|
551
|
+
throw new Error(
|
|
552
|
+
"CAMOFOX_API_KEY is not set. Cookie import is disabled unless you set CAMOFOX_API_KEY for both the server and the OpenClaw plugin environment."
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const result = await fetchApi(baseUrl, `/sessions/${encodeURIComponent(userId)}/cookies`, {
|
|
557
|
+
method: "POST",
|
|
558
|
+
headers: {
|
|
559
|
+
Authorization: `Bearer ${apiKey}`,
|
|
560
|
+
},
|
|
561
|
+
body: JSON.stringify({ cookies: pwCookies }),
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
return toToolResult({ imported: pwCookies.length, userId, result });
|
|
565
|
+
},
|
|
566
|
+
}));
|
|
567
|
+
|
|
440
568
|
api.registerCommand({
|
|
441
569
|
name: "camofox",
|
|
442
570
|
description: "Camoufox browser server control (status, start, stop)",
|