@askjo/camoufox-browser 1.0.11 → 1.0.12
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 +4 -4
- package/SKILL.md +1 -1
- package/package.json +1 -1
- package/plugin.ts +68 -59
- package/server-camoufox.js +11 -7
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ npm start # Default port 3000
|
|
|
82
82
|
```bash
|
|
83
83
|
curl -X POST http://localhost:3000/tabs \
|
|
84
84
|
-H "Content-Type: application/json" \
|
|
85
|
-
-d '{"userId": "user1", "
|
|
85
|
+
-d '{"userId": "user1", "sessionKey": "conv1", "url": "https://example.com"}'
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
### Get Page Snapshot (with element refs)
|
|
@@ -159,16 +159,16 @@ Navigate supports these macros for common sites:
|
|
|
159
159
|
```
|
|
160
160
|
Browser Instance
|
|
161
161
|
└── User Session (BrowserContext) - isolated cookies/storage
|
|
162
|
-
├── Tab Group (
|
|
162
|
+
├── Tab Group (sessionKey: "conv1") - conversation A
|
|
163
163
|
│ ├── Tab (google.com)
|
|
164
164
|
│ └── Tab (github.com)
|
|
165
|
-
└── Tab Group (
|
|
165
|
+
└── Tab Group (sessionKey: "conv2") - conversation B
|
|
166
166
|
└── Tab (amazon.com)
|
|
167
167
|
```
|
|
168
168
|
|
|
169
169
|
- One browser instance shared across users
|
|
170
170
|
- Separate BrowserContext per user (isolated cookies/storage)
|
|
171
|
-
- Tabs grouped by `
|
|
171
|
+
- Tabs grouped by `sessionKey` (conversation/task)
|
|
172
172
|
- 30-minute session timeout with automatic cleanup
|
|
173
173
|
|
|
174
174
|
## Running Locally
|
package/SKILL.md
CHANGED
|
@@ -55,7 +55,7 @@ Check health: `curl http://localhost:9377/health`
|
|
|
55
55
|
```bash
|
|
56
56
|
curl -X POST http://localhost:9377/tabs \
|
|
57
57
|
-H "Content-Type: application/json" \
|
|
58
|
-
-d '{"userId": "openclaw", "
|
|
58
|
+
-d '{"userId": "openclaw", "sessionKey": "task1", "url": "https://example.com"}'
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
Save the `tabId` from the response for subsequent requests.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askjo/camoufox-browser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Headless browser automation server and OpenClaw plugin for AI agents - anti-detection, element refs, and session isolation",
|
|
5
5
|
"main": "server-camoufox.js",
|
|
6
6
|
"license": "MIT",
|
package/plugin.ts
CHANGED
|
@@ -53,14 +53,25 @@ interface CliContext {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
interface ToolContext {
|
|
57
|
+
sessionKey?: string;
|
|
58
|
+
agentId?: string;
|
|
59
|
+
workspaceDir?: string;
|
|
60
|
+
sandboxed?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type ToolDefinition = {
|
|
64
|
+
name: string;
|
|
65
|
+
description: string;
|
|
66
|
+
parameters: object;
|
|
67
|
+
execute: (id: string, params: Record<string, unknown>) => Promise<ToolResult>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type ToolFactory = (ctx: ToolContext) => ToolDefinition | ToolDefinition[] | null | undefined;
|
|
71
|
+
|
|
56
72
|
interface PluginApi {
|
|
57
73
|
registerTool: (
|
|
58
|
-
tool:
|
|
59
|
-
name: string;
|
|
60
|
-
description: string;
|
|
61
|
-
parameters: object;
|
|
62
|
-
execute: (id: string, params: Record<string, unknown>) => Promise<ToolResult>;
|
|
63
|
-
},
|
|
74
|
+
tool: ToolDefinition | ToolFactory,
|
|
64
75
|
options?: { optional?: boolean }
|
|
65
76
|
) => void;
|
|
66
77
|
registerCommand: (cmd: {
|
|
@@ -198,29 +209,29 @@ export default function register(api: PluginApi) {
|
|
|
198
209
|
})();
|
|
199
210
|
}
|
|
200
211
|
|
|
201
|
-
api.registerTool({
|
|
212
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
202
213
|
name: "camoufox_create_tab",
|
|
203
214
|
description:
|
|
204
215
|
"PREFERRED: Create a new browser tab using Camoufox anti-detection browser. Use camoufox tools instead of Chrome/built-in browser - they bypass bot detection on Google, Amazon, LinkedIn, etc. Returns tabId for subsequent operations.",
|
|
205
216
|
parameters: {
|
|
206
217
|
type: "object",
|
|
207
218
|
properties: {
|
|
208
|
-
userId: { type: "string", description: "User identifier for session isolation" },
|
|
209
|
-
listItemId: { type: "string", description: "Conversation/task identifier for tab grouping" },
|
|
210
219
|
url: { type: "string", description: "Initial URL to navigate to" },
|
|
211
220
|
},
|
|
212
|
-
required: ["
|
|
221
|
+
required: ["url"],
|
|
213
222
|
},
|
|
214
223
|
async execute(_id, params) {
|
|
224
|
+
const sessionKey = ctx.sessionKey || "default";
|
|
225
|
+
const userId = ctx.agentId || "openclaw";
|
|
215
226
|
const result = await fetchApi(baseUrl, "/tabs", {
|
|
216
227
|
method: "POST",
|
|
217
|
-
body: JSON.stringify(params),
|
|
228
|
+
body: JSON.stringify({ ...params, userId, sessionKey }),
|
|
218
229
|
});
|
|
219
230
|
return toToolResult(result);
|
|
220
231
|
},
|
|
221
|
-
});
|
|
232
|
+
}));
|
|
222
233
|
|
|
223
|
-
api.registerTool({
|
|
234
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
224
235
|
name: "camoufox_snapshot",
|
|
225
236
|
description:
|
|
226
237
|
"Get accessibility snapshot of a Camoufox page with element refs (e1, e2, etc.) for interaction. Use with camoufox_create_tab.",
|
|
@@ -228,66 +239,66 @@ export default function register(api: PluginApi) {
|
|
|
228
239
|
type: "object",
|
|
229
240
|
properties: {
|
|
230
241
|
tabId: { type: "string", description: "Tab identifier" },
|
|
231
|
-
userId: { type: "string", description: "User identifier" },
|
|
232
242
|
},
|
|
233
|
-
required: ["tabId"
|
|
243
|
+
required: ["tabId"],
|
|
234
244
|
},
|
|
235
245
|
async execute(_id, params) {
|
|
236
|
-
const { tabId
|
|
246
|
+
const { tabId } = params as { tabId: string };
|
|
247
|
+
const userId = ctx.agentId || "openclaw";
|
|
237
248
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}/snapshot?userId=${userId}`);
|
|
238
249
|
return toToolResult(result);
|
|
239
250
|
},
|
|
240
|
-
});
|
|
251
|
+
}));
|
|
241
252
|
|
|
242
|
-
api.registerTool({
|
|
253
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
243
254
|
name: "camoufox_click",
|
|
244
255
|
description: "Click an element in a Camoufox tab by ref (e.g., e1) or CSS selector.",
|
|
245
256
|
parameters: {
|
|
246
257
|
type: "object",
|
|
247
258
|
properties: {
|
|
248
259
|
tabId: { type: "string", description: "Tab identifier" },
|
|
249
|
-
userId: { type: "string", description: "User identifier" },
|
|
250
260
|
ref: { type: "string", description: "Element ref from snapshot (e.g., e1)" },
|
|
251
261
|
selector: { type: "string", description: "CSS selector (alternative to ref)" },
|
|
252
262
|
},
|
|
253
|
-
required: ["tabId"
|
|
263
|
+
required: ["tabId"],
|
|
254
264
|
},
|
|
255
265
|
async execute(_id, params) {
|
|
256
|
-
const { tabId, ...
|
|
266
|
+
const { tabId, ...rest } = params as { tabId: string } & Record<string, unknown>;
|
|
267
|
+
const userId = ctx.agentId || "openclaw";
|
|
257
268
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}/click`, {
|
|
258
269
|
method: "POST",
|
|
259
|
-
body: JSON.stringify(
|
|
270
|
+
body: JSON.stringify({ ...rest, userId }),
|
|
260
271
|
});
|
|
261
272
|
return toToolResult(result);
|
|
262
273
|
},
|
|
263
|
-
});
|
|
274
|
+
}));
|
|
264
275
|
|
|
265
|
-
api.registerTool({
|
|
276
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
266
277
|
name: "camoufox_type",
|
|
267
278
|
description: "Type text into an element in a Camoufox tab.",
|
|
268
279
|
parameters: {
|
|
269
280
|
type: "object",
|
|
270
281
|
properties: {
|
|
271
282
|
tabId: { type: "string", description: "Tab identifier" },
|
|
272
|
-
userId: { type: "string", description: "User identifier" },
|
|
273
283
|
ref: { type: "string", description: "Element ref from snapshot (e.g., e2)" },
|
|
274
284
|
selector: { type: "string", description: "CSS selector (alternative to ref)" },
|
|
275
285
|
text: { type: "string", description: "Text to type" },
|
|
276
286
|
pressEnter: { type: "boolean", description: "Press Enter after typing" },
|
|
277
287
|
},
|
|
278
|
-
required: ["tabId", "
|
|
288
|
+
required: ["tabId", "text"],
|
|
279
289
|
},
|
|
280
290
|
async execute(_id, params) {
|
|
281
|
-
const { tabId, ...
|
|
291
|
+
const { tabId, ...rest } = params as { tabId: string } & Record<string, unknown>;
|
|
292
|
+
const userId = ctx.agentId || "openclaw";
|
|
282
293
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}/type`, {
|
|
283
294
|
method: "POST",
|
|
284
|
-
body: JSON.stringify(
|
|
295
|
+
body: JSON.stringify({ ...rest, userId }),
|
|
285
296
|
});
|
|
286
297
|
return toToolResult(result);
|
|
287
298
|
},
|
|
288
|
-
});
|
|
299
|
+
}));
|
|
289
300
|
|
|
290
|
-
api.registerTool({
|
|
301
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
291
302
|
name: "camoufox_navigate",
|
|
292
303
|
description:
|
|
293
304
|
"Navigate a Camoufox tab to a URL or use a search macro (@google_search, @youtube_search, etc.). Preferred over Chrome for sites with bot detection.",
|
|
@@ -295,7 +306,6 @@ export default function register(api: PluginApi) {
|
|
|
295
306
|
type: "object",
|
|
296
307
|
properties: {
|
|
297
308
|
tabId: { type: "string", description: "Tab identifier" },
|
|
298
|
-
userId: { type: "string", description: "User identifier" },
|
|
299
309
|
url: { type: "string", description: "URL to navigate to" },
|
|
300
310
|
macro: {
|
|
301
311
|
type: "string",
|
|
@@ -318,95 +328,94 @@ export default function register(api: PluginApi) {
|
|
|
318
328
|
},
|
|
319
329
|
query: { type: "string", description: "Search query (when using macro)" },
|
|
320
330
|
},
|
|
321
|
-
required: ["tabId"
|
|
331
|
+
required: ["tabId"],
|
|
322
332
|
},
|
|
323
333
|
async execute(_id, params) {
|
|
324
|
-
const { tabId, ...
|
|
334
|
+
const { tabId, ...rest } = params as { tabId: string } & Record<string, unknown>;
|
|
335
|
+
const userId = ctx.agentId || "openclaw";
|
|
325
336
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}/navigate`, {
|
|
326
337
|
method: "POST",
|
|
327
|
-
body: JSON.stringify(
|
|
338
|
+
body: JSON.stringify({ ...rest, userId }),
|
|
328
339
|
});
|
|
329
340
|
return toToolResult(result);
|
|
330
341
|
},
|
|
331
|
-
});
|
|
342
|
+
}));
|
|
332
343
|
|
|
333
|
-
api.registerTool({
|
|
344
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
334
345
|
name: "camoufox_scroll",
|
|
335
346
|
description: "Scroll a Camoufox page.",
|
|
336
347
|
parameters: {
|
|
337
348
|
type: "object",
|
|
338
349
|
properties: {
|
|
339
350
|
tabId: { type: "string", description: "Tab identifier" },
|
|
340
|
-
userId: { type: "string", description: "User identifier" },
|
|
341
351
|
direction: { type: "string", enum: ["up", "down", "left", "right"] },
|
|
342
352
|
amount: { type: "number", description: "Pixels to scroll" },
|
|
343
353
|
},
|
|
344
|
-
required: ["tabId", "
|
|
354
|
+
required: ["tabId", "direction"],
|
|
345
355
|
},
|
|
346
356
|
async execute(_id, params) {
|
|
347
|
-
const { tabId, ...
|
|
357
|
+
const { tabId, ...rest } = params as { tabId: string } & Record<string, unknown>;
|
|
358
|
+
const userId = ctx.agentId || "openclaw";
|
|
348
359
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}/scroll`, {
|
|
349
360
|
method: "POST",
|
|
350
|
-
body: JSON.stringify(
|
|
361
|
+
body: JSON.stringify({ ...rest, userId }),
|
|
351
362
|
});
|
|
352
363
|
return toToolResult(result);
|
|
353
364
|
},
|
|
354
|
-
});
|
|
365
|
+
}));
|
|
355
366
|
|
|
356
|
-
api.registerTool({
|
|
367
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
357
368
|
name: "camoufox_screenshot",
|
|
358
369
|
description: "Take a screenshot of a Camoufox page.",
|
|
359
370
|
parameters: {
|
|
360
371
|
type: "object",
|
|
361
372
|
properties: {
|
|
362
373
|
tabId: { type: "string", description: "Tab identifier" },
|
|
363
|
-
userId: { type: "string", description: "User identifier" },
|
|
364
374
|
},
|
|
365
|
-
required: ["tabId"
|
|
375
|
+
required: ["tabId"],
|
|
366
376
|
},
|
|
367
377
|
async execute(_id, params) {
|
|
368
|
-
const { tabId
|
|
378
|
+
const { tabId } = params as { tabId: string };
|
|
379
|
+
const userId = ctx.agentId || "openclaw";
|
|
369
380
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}/screenshot?userId=${userId}`);
|
|
370
381
|
return toToolResult(result);
|
|
371
382
|
},
|
|
372
|
-
});
|
|
383
|
+
}));
|
|
373
384
|
|
|
374
|
-
api.registerTool({
|
|
385
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
375
386
|
name: "camoufox_close_tab",
|
|
376
387
|
description: "Close a Camoufox browser tab.",
|
|
377
388
|
parameters: {
|
|
378
389
|
type: "object",
|
|
379
390
|
properties: {
|
|
380
391
|
tabId: { type: "string", description: "Tab identifier" },
|
|
381
|
-
userId: { type: "string", description: "User identifier" },
|
|
382
392
|
},
|
|
383
|
-
required: ["tabId"
|
|
393
|
+
required: ["tabId"],
|
|
384
394
|
},
|
|
385
395
|
async execute(_id, params) {
|
|
386
|
-
const { tabId
|
|
396
|
+
const { tabId } = params as { tabId: string };
|
|
397
|
+
const userId = ctx.agentId || "openclaw";
|
|
387
398
|
const result = await fetchApi(baseUrl, `/tabs/${tabId}?userId=${userId}`, {
|
|
388
399
|
method: "DELETE",
|
|
389
400
|
});
|
|
390
401
|
return toToolResult(result);
|
|
391
402
|
},
|
|
392
|
-
});
|
|
403
|
+
}));
|
|
393
404
|
|
|
394
|
-
api.registerTool({
|
|
405
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
395
406
|
name: "camoufox_list_tabs",
|
|
396
407
|
description: "List all open Camoufox tabs for a user.",
|
|
397
408
|
parameters: {
|
|
398
409
|
type: "object",
|
|
399
|
-
properties: {
|
|
400
|
-
|
|
401
|
-
},
|
|
402
|
-
required: ["userId"],
|
|
410
|
+
properties: {},
|
|
411
|
+
required: [],
|
|
403
412
|
},
|
|
404
|
-
async execute(_id,
|
|
405
|
-
const
|
|
413
|
+
async execute(_id, _params) {
|
|
414
|
+
const userId = ctx.agentId || "openclaw";
|
|
406
415
|
const result = await fetchApi(baseUrl, `/tabs?userId=${userId}`);
|
|
407
416
|
return toToolResult(result);
|
|
408
417
|
},
|
|
409
|
-
});
|
|
418
|
+
}));
|
|
410
419
|
|
|
411
420
|
api.registerCommand({
|
|
412
421
|
name: "camoufox",
|
package/server-camoufox.js
CHANGED
|
@@ -10,8 +10,9 @@ const app = express();
|
|
|
10
10
|
app.use(express.json({ limit: '5mb' }));
|
|
11
11
|
|
|
12
12
|
let browser = null;
|
|
13
|
-
// userId -> { context, tabGroups: Map<
|
|
13
|
+
// userId -> { context, tabGroups: Map<sessionKey, Map<tabId, TabState>>, lastAccess }
|
|
14
14
|
// TabState = { page, refs: Map<refId, {role, name, nth}>, visitedUrls: Set, toolCalls: number }
|
|
15
|
+
// Note: sessionKey was previously called listItemId - both are accepted for backward compatibility
|
|
15
16
|
const sessions = new Map();
|
|
16
17
|
|
|
17
18
|
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 min
|
|
@@ -364,13 +365,15 @@ app.get('/health', async (req, res) => {
|
|
|
364
365
|
// Create new tab
|
|
365
366
|
app.post('/tabs', async (req, res) => {
|
|
366
367
|
try {
|
|
367
|
-
const { userId, listItemId, url } = req.body;
|
|
368
|
-
|
|
369
|
-
|
|
368
|
+
const { userId, sessionKey, listItemId, url } = req.body;
|
|
369
|
+
// Accept both sessionKey (preferred) and listItemId (legacy) for backward compatibility
|
|
370
|
+
const resolvedSessionKey = sessionKey || listItemId;
|
|
371
|
+
if (!userId || !resolvedSessionKey) {
|
|
372
|
+
return res.status(400).json({ error: 'userId and sessionKey required' });
|
|
370
373
|
}
|
|
371
374
|
|
|
372
375
|
const session = await getSession(userId);
|
|
373
|
-
const group = getTabGroup(session,
|
|
376
|
+
const group = getTabGroup(session, resolvedSessionKey);
|
|
374
377
|
|
|
375
378
|
const page = await session.context.newPage();
|
|
376
379
|
const tabId = crypto.randomUUID();
|
|
@@ -382,7 +385,7 @@ app.post('/tabs', async (req, res) => {
|
|
|
382
385
|
tabState.visitedUrls.add(url);
|
|
383
386
|
}
|
|
384
387
|
|
|
385
|
-
console.log(`Tab ${tabId} created for user ${userId},
|
|
388
|
+
console.log(`Tab ${tabId} created for user ${userId}, session ${resolvedSessionKey}`);
|
|
386
389
|
res.json({ tabId, url: page.url() });
|
|
387
390
|
} catch (err) {
|
|
388
391
|
console.error('Create tab error:', err);
|
|
@@ -844,7 +847,8 @@ app.get('/tabs/:tabId/stats', async (req, res) => {
|
|
|
844
847
|
const { tabState, listItemId } = found;
|
|
845
848
|
res.json({
|
|
846
849
|
tabId: req.params.tabId,
|
|
847
|
-
listItemId,
|
|
850
|
+
sessionKey: listItemId,
|
|
851
|
+
listItemId, // Legacy compatibility
|
|
848
852
|
url: tabState.page.url(),
|
|
849
853
|
visitedUrls: Array.from(tabState.visitedUrls),
|
|
850
854
|
toolCalls: tabState.toolCalls,
|