@askjo/camoufox-browser 1.0.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/fly.toml ADDED
@@ -0,0 +1,29 @@
1
+ app = "jo-camoufox-browser"
2
+ primary_region = "sjc"
3
+
4
+ [build]
5
+ dockerfile = "Dockerfile.camoufox"
6
+
7
+ # Internal-only service (Fly private network)
8
+ [[services]]
9
+ internal_port = 3000
10
+ protocol = "tcp"
11
+ auto_stop_machines = false
12
+ auto_start_machines = true
13
+ min_machines_running = 1
14
+
15
+ [[services.ports]]
16
+ port = 3000
17
+
18
+ [[services.http_checks]]
19
+ interval = 30000
20
+ timeout = 5000
21
+ path = "/health"
22
+
23
+ [[vm]]
24
+ memory = "4gb"
25
+ cpu_kind = "shared"
26
+ cpus = 2
27
+
28
+ [env]
29
+ NODE_ENV = "production"
package/jest.config.js ADDED
@@ -0,0 +1,41 @@
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ testTimeout: 60000, // 60 seconds per test
4
+
5
+ // Run tests sequentially to avoid resource conflicts
6
+ maxWorkers: 1,
7
+
8
+ // Test file patterns
9
+ testMatch: [
10
+ '**/tests/**/*.test.js'
11
+ ],
12
+
13
+ // Ignore patterns
14
+ testPathIgnorePatterns: [
15
+ '/node_modules/'
16
+ ],
17
+
18
+ // Setup and teardown
19
+ globalSetup: undefined,
20
+ globalTeardown: undefined,
21
+
22
+ // Verbose output
23
+ verbose: true,
24
+
25
+ // Fail fast on first error (useful for CI)
26
+ bail: process.env.CI ? 1 : 0,
27
+
28
+ // Coverage settings (optional)
29
+ collectCoverage: false,
30
+ coverageDirectory: 'coverage',
31
+ coveragePathIgnorePatterns: [
32
+ '/node_modules/',
33
+ '/tests/'
34
+ ],
35
+
36
+ // Reporter settings
37
+ reporters: [
38
+ 'default',
39
+ ...(process.env.CI ? [['jest-junit', { outputDirectory: 'test-results' }]] : [])
40
+ ]
41
+ };
package/lib/macros.js ADDED
@@ -0,0 +1,30 @@
1
+ const MACROS = {
2
+ '@google_search': (query) => `https://www.google.com/search?q=${encodeURIComponent(query || '')}`,
3
+ '@youtube_search': (query) => `https://www.youtube.com/results?search_query=${encodeURIComponent(query || '')}`,
4
+ '@amazon_search': (query) => `https://www.amazon.com/s?k=${encodeURIComponent(query || '')}`,
5
+ '@reddit_search': (query) => `https://www.reddit.com/search/?q=${encodeURIComponent(query || '')}`,
6
+ '@wikipedia_search': (query) => `https://en.wikipedia.org/wiki/Special:Search?search=${encodeURIComponent(query || '')}`,
7
+ '@twitter_search': (query) => `https://twitter.com/search?q=${encodeURIComponent(query || '')}`,
8
+ '@yelp_search': (query) => `https://www.yelp.com/search?find_desc=${encodeURIComponent(query || '')}`,
9
+ '@spotify_search': (query) => `https://open.spotify.com/search/${encodeURIComponent(query || '')}`,
10
+ '@netflix_search': (query) => `https://www.netflix.com/search?q=${encodeURIComponent(query || '')}`,
11
+ '@linkedin_search': (query) => `https://www.linkedin.com/search/results/all/?keywords=${encodeURIComponent(query || '')}`,
12
+ '@instagram_search': (query) => `https://www.instagram.com/explore/tags/${encodeURIComponent(query || '')}`,
13
+ '@tiktok_search': (query) => `https://www.tiktok.com/search?q=${encodeURIComponent(query || '')}`,
14
+ '@twitch_search': (query) => `https://www.twitch.tv/search?term=${encodeURIComponent(query || '')}`
15
+ };
16
+
17
+ function expandMacro(macro, query) {
18
+ const macroFn = MACROS[macro];
19
+ return macroFn ? macroFn(query) : null;
20
+ }
21
+
22
+ function getSupportedMacros() {
23
+ return Object.keys(MACROS);
24
+ }
25
+
26
+ module.exports = {
27
+ expandMacro,
28
+ getSupportedMacros,
29
+ MACROS
30
+ };
@@ -0,0 +1,31 @@
1
+ {
2
+ "id": "camoufox-browser",
3
+ "name": "Camoufox Browser",
4
+ "description": "Anti-detection browser automation for AI agents using Camoufox (Firefox-based)",
5
+ "version": "1.0.0",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "url": {
10
+ "type": "string",
11
+ "description": "Camoufox browser server URL",
12
+ "default": "http://localhost:9377"
13
+ },
14
+ "autoStart": {
15
+ "type": "boolean",
16
+ "description": "Auto-start the camoufox-browser server with the Gateway",
17
+ "default": false
18
+ }
19
+ },
20
+ "additionalProperties": false
21
+ },
22
+ "uiHints": {
23
+ "url": {
24
+ "label": "Server URL",
25
+ "placeholder": "http://localhost:9377"
26
+ },
27
+ "autoStart": {
28
+ "label": "Auto-start server"
29
+ }
30
+ }
31
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@askjo/camoufox-browser",
3
+ "version": "1.0.0",
4
+ "description": "Headless browser automation server for AI agents - REST API with anti-detection, element refs, and session isolation",
5
+ "main": "server-camoufox.js",
6
+ "openclaw": {
7
+ "extensions": ["plugin.ts"]
8
+ },
9
+ "scripts": {
10
+ "start": "node server-camoufox.js",
11
+ "start:chrome": "node server.js",
12
+ "test": "jest --runInBand --forceExit",
13
+ "test:e2e": "jest --runInBand --forceExit tests/e2e",
14
+ "test:live": "RUN_LIVE_TESTS=1 jest --runInBand --forceExit tests/live",
15
+ "test:debug": "DEBUG_SERVER=1 jest --runInBand --forceExit",
16
+ "postinstall": "npx camoufox-js fetch || true"
17
+ },
18
+ "dependencies": {
19
+ "camoufox-js": "^0.8.5",
20
+ "dotenv": "^17.2.3",
21
+ "express": "^4.18.2",
22
+ "playwright": "^1.50.0",
23
+ "playwright-core": "^1.58.0",
24
+ "playwright-extra": "^4.3.6",
25
+ "puppeteer-extra-plugin-stealth": "^2.11.2"
26
+ },
27
+ "devDependencies": {
28
+ "jest": "^29.7.0"
29
+ }
30
+ }
package/plugin.ts ADDED
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Camoufox Browser - OpenClaw Plugin
3
+ *
4
+ * Provides browser automation tools using the Camoufox anti-detection browser.
5
+ * Run the server separately: npm start (or ./run-camoufox.sh -p PORT)
6
+ */
7
+
8
+ import { spawn, ChildProcess } from "child_process";
9
+ import { join } from "path";
10
+
11
+ interface PluginConfig {
12
+ url?: string;
13
+ autoStart?: boolean;
14
+ }
15
+
16
+ interface PluginApi {
17
+ registerTool: (tool: {
18
+ name: string;
19
+ description: string;
20
+ parameters: object;
21
+ optional?: boolean;
22
+ handler: (params: Record<string, unknown>) => Promise<unknown>;
23
+ }) => void;
24
+ registerCommand: (cmd: {
25
+ name: string;
26
+ description: string;
27
+ handler: (args: string[]) => Promise<void>;
28
+ }) => void;
29
+ config: PluginConfig;
30
+ log: {
31
+ info: (msg: string) => void;
32
+ error: (msg: string) => void;
33
+ };
34
+ }
35
+
36
+ let serverProcess: ChildProcess | null = null;
37
+
38
+ async function fetchApi(
39
+ baseUrl: string,
40
+ path: string,
41
+ options: RequestInit = {}
42
+ ): Promise<unknown> {
43
+ const url = `${baseUrl}${path}`;
44
+ const res = await fetch(url, {
45
+ ...options,
46
+ headers: {
47
+ "Content-Type": "application/json",
48
+ ...options.headers,
49
+ },
50
+ });
51
+ if (!res.ok) {
52
+ const text = await res.text();
53
+ throw new Error(`${res.status}: ${text}`);
54
+ }
55
+ return res.json();
56
+ }
57
+
58
+ export default function register(api: PluginApi) {
59
+ const baseUrl = api.config.url || "http://localhost:9377";
60
+
61
+ api.registerTool({
62
+ name: "camoufox_create_tab",
63
+ description:
64
+ "Create a new browser tab. Returns tabId for subsequent operations.",
65
+ parameters: {
66
+ type: "object",
67
+ properties: {
68
+ userId: { type: "string", description: "User identifier for session isolation" },
69
+ listItemId: { type: "string", description: "Conversation/task identifier for tab grouping" },
70
+ url: { type: "string", description: "Initial URL to navigate to" },
71
+ },
72
+ required: ["userId", "url"],
73
+ },
74
+ optional: true,
75
+ handler: async (params) => {
76
+ return fetchApi(baseUrl, "/tabs", {
77
+ method: "POST",
78
+ body: JSON.stringify(params),
79
+ });
80
+ },
81
+ });
82
+
83
+ api.registerTool({
84
+ name: "camoufox_snapshot",
85
+ description:
86
+ "Get accessibility snapshot of a page with element refs (e1, e2, etc.) for interaction.",
87
+ parameters: {
88
+ type: "object",
89
+ properties: {
90
+ tabId: { type: "string", description: "Tab identifier" },
91
+ userId: { type: "string", description: "User identifier" },
92
+ },
93
+ required: ["tabId", "userId"],
94
+ },
95
+ optional: true,
96
+ handler: async (params) => {
97
+ const { tabId, userId } = params as { tabId: string; userId: string };
98
+ return fetchApi(baseUrl, `/tabs/${tabId}/snapshot?userId=${userId}`);
99
+ },
100
+ });
101
+
102
+ api.registerTool({
103
+ name: "camoufox_click",
104
+ description: "Click an element by ref (e.g., e1) or CSS selector.",
105
+ parameters: {
106
+ type: "object",
107
+ properties: {
108
+ tabId: { type: "string", description: "Tab identifier" },
109
+ userId: { type: "string", description: "User identifier" },
110
+ ref: { type: "string", description: "Element ref from snapshot (e.g., e1)" },
111
+ selector: { type: "string", description: "CSS selector (alternative to ref)" },
112
+ },
113
+ required: ["tabId", "userId"],
114
+ },
115
+ optional: true,
116
+ handler: async (params) => {
117
+ const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
118
+ return fetchApi(baseUrl, `/tabs/${tabId}/click`, {
119
+ method: "POST",
120
+ body: JSON.stringify(body),
121
+ });
122
+ },
123
+ });
124
+
125
+ api.registerTool({
126
+ name: "camoufox_type",
127
+ description: "Type text into an element.",
128
+ parameters: {
129
+ type: "object",
130
+ properties: {
131
+ tabId: { type: "string", description: "Tab identifier" },
132
+ userId: { type: "string", description: "User identifier" },
133
+ ref: { type: "string", description: "Element ref from snapshot (e.g., e2)" },
134
+ selector: { type: "string", description: "CSS selector (alternative to ref)" },
135
+ text: { type: "string", description: "Text to type" },
136
+ pressEnter: { type: "boolean", description: "Press Enter after typing" },
137
+ },
138
+ required: ["tabId", "userId", "text"],
139
+ },
140
+ optional: true,
141
+ handler: async (params) => {
142
+ const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
143
+ return fetchApi(baseUrl, `/tabs/${tabId}/type`, {
144
+ method: "POST",
145
+ body: JSON.stringify(body),
146
+ });
147
+ },
148
+ });
149
+
150
+ api.registerTool({
151
+ name: "camoufox_navigate",
152
+ description:
153
+ "Navigate to a URL or use a search macro (@google_search, @youtube_search, etc.).",
154
+ parameters: {
155
+ type: "object",
156
+ properties: {
157
+ tabId: { type: "string", description: "Tab identifier" },
158
+ userId: { type: "string", description: "User identifier" },
159
+ url: { type: "string", description: "URL to navigate to" },
160
+ macro: {
161
+ type: "string",
162
+ description: "Search macro (e.g., @google_search, @youtube_search)",
163
+ enum: [
164
+ "@google_search",
165
+ "@youtube_search",
166
+ "@amazon_search",
167
+ "@reddit_search",
168
+ "@wikipedia_search",
169
+ "@twitter_search",
170
+ "@yelp_search",
171
+ "@spotify_search",
172
+ "@netflix_search",
173
+ "@linkedin_search",
174
+ "@instagram_search",
175
+ "@tiktok_search",
176
+ "@twitch_search",
177
+ ],
178
+ },
179
+ query: { type: "string", description: "Search query (when using macro)" },
180
+ },
181
+ required: ["tabId", "userId"],
182
+ },
183
+ optional: true,
184
+ handler: async (params) => {
185
+ const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
186
+ return fetchApi(baseUrl, `/tabs/${tabId}/navigate`, {
187
+ method: "POST",
188
+ body: JSON.stringify(body),
189
+ });
190
+ },
191
+ });
192
+
193
+ api.registerTool({
194
+ name: "camoufox_scroll",
195
+ description: "Scroll the page.",
196
+ parameters: {
197
+ type: "object",
198
+ properties: {
199
+ tabId: { type: "string", description: "Tab identifier" },
200
+ userId: { type: "string", description: "User identifier" },
201
+ direction: { type: "string", enum: ["up", "down", "left", "right"] },
202
+ amount: { type: "number", description: "Pixels to scroll" },
203
+ },
204
+ required: ["tabId", "userId", "direction"],
205
+ },
206
+ optional: true,
207
+ handler: async (params) => {
208
+ const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
209
+ return fetchApi(baseUrl, `/tabs/${tabId}/scroll`, {
210
+ method: "POST",
211
+ body: JSON.stringify(body),
212
+ });
213
+ },
214
+ });
215
+
216
+ api.registerTool({
217
+ name: "camoufox_screenshot",
218
+ description: "Take a screenshot of the current page.",
219
+ parameters: {
220
+ type: "object",
221
+ properties: {
222
+ tabId: { type: "string", description: "Tab identifier" },
223
+ userId: { type: "string", description: "User identifier" },
224
+ },
225
+ required: ["tabId", "userId"],
226
+ },
227
+ optional: true,
228
+ handler: async (params) => {
229
+ const { tabId, userId } = params as { tabId: string; userId: string };
230
+ return fetchApi(baseUrl, `/tabs/${tabId}/screenshot?userId=${userId}`);
231
+ },
232
+ });
233
+
234
+ api.registerTool({
235
+ name: "camoufox_close_tab",
236
+ description: "Close a browser tab.",
237
+ parameters: {
238
+ type: "object",
239
+ properties: {
240
+ tabId: { type: "string", description: "Tab identifier" },
241
+ userId: { type: "string", description: "User identifier" },
242
+ },
243
+ required: ["tabId", "userId"],
244
+ },
245
+ optional: true,
246
+ handler: async (params) => {
247
+ const { tabId, userId } = params as { tabId: string; userId: string };
248
+ return fetchApi(baseUrl, `/tabs/${tabId}?userId=${userId}`, {
249
+ method: "DELETE",
250
+ });
251
+ },
252
+ });
253
+
254
+ api.registerTool({
255
+ name: "camoufox_list_tabs",
256
+ description: "List all open tabs for a user.",
257
+ parameters: {
258
+ type: "object",
259
+ properties: {
260
+ userId: { type: "string", description: "User identifier" },
261
+ },
262
+ required: ["userId"],
263
+ },
264
+ optional: true,
265
+ handler: async (params) => {
266
+ const { userId } = params as { userId: string };
267
+ return fetchApi(baseUrl, `/tabs?userId=${userId}`);
268
+ },
269
+ });
270
+
271
+ api.registerCommand({
272
+ name: "camoufox",
273
+ description: "Camoufox browser server control (status, start, stop)",
274
+ handler: async (args) => {
275
+ const subcommand = args[0] || "status";
276
+ switch (subcommand) {
277
+ case "status":
278
+ try {
279
+ const health = await fetchApi(baseUrl, "/health");
280
+ api.log.info(`Camoufox server at ${baseUrl}: ${JSON.stringify(health)}`);
281
+ } catch (e) {
282
+ api.log.error(`Camoufox server at ${baseUrl}: not reachable`);
283
+ }
284
+ break;
285
+ case "start":
286
+ if (serverProcess) {
287
+ api.log.info("Camoufox server already running");
288
+ return;
289
+ }
290
+ const pluginDir = __dirname;
291
+ serverProcess = spawn("npm", ["start"], {
292
+ cwd: pluginDir,
293
+ stdio: "inherit",
294
+ detached: true,
295
+ });
296
+ api.log.info(`Started camoufox-browser server (pid: ${serverProcess.pid})`);
297
+ break;
298
+ case "stop":
299
+ if (serverProcess) {
300
+ serverProcess.kill();
301
+ serverProcess = null;
302
+ api.log.info("Stopped camoufox-browser server");
303
+ } else {
304
+ api.log.info("No managed server process running");
305
+ }
306
+ break;
307
+ default:
308
+ api.log.error(`Unknown subcommand: ${subcommand}. Use: status, start, stop`);
309
+ }
310
+ },
311
+ });
312
+ }
@@ -0,0 +1,37 @@
1
+ #!/bin/bash
2
+ # Local development script for camoufox-browser
3
+ # Usage: ./run-camoufox.sh [-p port]
4
+ # Example: ./run-camoufox.sh -p 3001
5
+
6
+ PORT=3000
7
+ while getopts "p:" opt; do
8
+ case $opt in
9
+ p) PORT="$OPTARG" ;;
10
+ *) echo "Usage: $0 [-p port]"; exit 1 ;;
11
+ esac
12
+ done
13
+ export PORT
14
+
15
+ # Install deps if needed
16
+ if [ ! -d "node_modules" ]; then
17
+ echo "Installing dependencies..."
18
+ npm install
19
+ fi
20
+
21
+ # Check if camoufox browser is installed
22
+ if ! npx camoufox-js --version &> /dev/null 2>&1; then
23
+ echo "Fetching Camoufox browser..."
24
+ npx camoufox-js fetch
25
+ fi
26
+
27
+ # Install nodemon globally if not available
28
+ if ! command -v nodemon &> /dev/null; then
29
+ echo "Installing nodemon..."
30
+ npm install -g nodemon
31
+ fi
32
+
33
+ echo "Starting camoufox-browser on http://localhost:$PORT (with auto-reload)"
34
+ echo "Logs: /tmp/camoufox-browser.log"
35
+ nodemon --watch server-camoufox.js --exec "node server-camoufox.js" 2>&1 | while IFS= read -r line; do
36
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $line"
37
+ done | tee -a /tmp/camoufox-browser.log