@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 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> a Firefox fork with fingerprint spoofing at the C++ level.
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** `navigator.hardwareConcurrency`, WebGL renderers, AudioContext, screen geometry, WebRTC all spoofed before JavaScript ever sees them. No shims, no wrappers, no tells.
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** 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
- - **Search Macros** `@google_search`, `@youtube_search`, `@amazon_search`, and 10 more
37
- - **Deploy Anywhere** Docker, Fly.io, Railway
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`. Set `CAMOFOX_PORT` to override.
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) isolated cookies/storage
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) Firefox-based browser with C++ anti-detection
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) Open-source AI agent framework
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/?q=${encodeURIComponent(query || '')}`,
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.0.14",
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: PluginConfig;
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 port = api.config.port || 9377;
197
- const baseUrl = api.config.url || `http://localhost:${port}`;
198
- const autoStart = api.config.autoStart !== false; // default true
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 || "openclaw";
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 || "openclaw";
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 || "openclaw";
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 || "openclaw";
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 || "openclaw";
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 || "openclaw";
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 || "openclaw";
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 || "openclaw";
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 || "openclaw";
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)",