@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 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", "listItemId": "conv1", "url": "https://example.com"}'
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 (listItemId: "conv1") - conversation A
162
+ ├── Tab Group (sessionKey: "conv1") - conversation A
163
163
  │ ├── Tab (google.com)
164
164
  │ └── Tab (github.com)
165
- └── Tab Group (listItemId: "conv2") - conversation B
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 `listItemId` (conversation/task)
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", "listItemId": "task1", "url": "https://example.com"}'
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.11",
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: ["userId", "url"],
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", "userId"],
243
+ required: ["tabId"],
234
244
  },
235
245
  async execute(_id, params) {
236
- const { tabId, userId } = params as { tabId: string; userId: string };
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", "userId"],
263
+ required: ["tabId"],
254
264
  },
255
265
  async execute(_id, params) {
256
- const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
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(body),
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", "userId", "text"],
288
+ required: ["tabId", "text"],
279
289
  },
280
290
  async execute(_id, params) {
281
- const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
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(body),
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", "userId"],
331
+ required: ["tabId"],
322
332
  },
323
333
  async execute(_id, params) {
324
- const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
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(body),
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", "userId", "direction"],
354
+ required: ["tabId", "direction"],
345
355
  },
346
356
  async execute(_id, params) {
347
- const { tabId, ...body } = params as { tabId: string } & Record<string, unknown>;
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(body),
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", "userId"],
375
+ required: ["tabId"],
366
376
  },
367
377
  async execute(_id, params) {
368
- const { tabId, userId } = params as { tabId: string; userId: string };
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", "userId"],
393
+ required: ["tabId"],
384
394
  },
385
395
  async execute(_id, params) {
386
- const { tabId, userId } = params as { tabId: string; userId: string };
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
- userId: { type: "string", description: "User identifier" },
401
- },
402
- required: ["userId"],
410
+ properties: {},
411
+ required: [],
403
412
  },
404
- async execute(_id, params) {
405
- const { userId } = params as { userId: string };
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",
@@ -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<listItemId, Map<tabId, TabState>>, lastAccess }
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
- if (!userId || !listItemId) {
369
- return res.status(400).json({ error: 'userId and listItemId required' });
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, listItemId);
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}, listItem ${listItemId}`);
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,