@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/.env.bak +4 -0
- package/.github/workflows/deploy.yml +21 -0
- package/AGENTS.md +153 -0
- package/Dockerfile.camoufox +59 -0
- package/LICENSE +21 -0
- package/README.md +234 -0
- package/SKILL.md +165 -0
- package/experimental/chromium/Dockerfile +35 -0
- package/experimental/chromium/README.md +47 -0
- package/experimental/chromium/run.sh +24 -0
- package/experimental/chromium/server.js +812 -0
- package/fly.toml +29 -0
- package/jest.config.js +41 -0
- package/lib/macros.js +30 -0
- package/openclaw.plugin.json +31 -0
- package/package.json +30 -0
- package/plugin.ts +312 -0
- package/run-camoufox.sh +37 -0
- package/server-camoufox.js +946 -0
- package/tests/e2e/concurrency.test.js +103 -0
- package/tests/e2e/formSubmission.test.js +129 -0
- package/tests/e2e/macroNavigation.test.js +92 -0
- package/tests/e2e/navigation.test.js +128 -0
- package/tests/e2e/scroll.test.js +81 -0
- package/tests/e2e/snapshotLinks.test.js +141 -0
- package/tests/e2e/tabLifecycle.test.js +149 -0
- package/tests/e2e/typingEnter.test.js +147 -0
- package/tests/helpers/client.js +222 -0
- package/tests/helpers/startJoBrowser.js +95 -0
- package/tests/helpers/testSite.js +238 -0
- package/tests/live/googleSearch.test.js +93 -0
- package/tests/live/macroExpansion.test.js +132 -0
- package/tests/unit/macros.test.js +123 -0
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
|
+
}
|
package/run-camoufox.sh
ADDED
|
@@ -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
|