@web-auto/camo 0.1.6 → 0.1.7

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
@@ -39,6 +39,9 @@ camo start --url https://example.com --alias main
39
39
  # Start headless worker (auto-kill after idle timeout)
40
40
  camo start worker-1 --headless --alias shard1 --idle-timeout 30m
41
41
 
42
+ # Start with devtools (headful only)
43
+ camo start worker-1 --devtools
44
+
42
45
  # Navigate
43
46
  camo goto https://www.xiaohongshu.com
44
47
 
@@ -71,7 +74,7 @@ camo create fingerprint --os <os> --region <region>
71
74
  ### Browser Control
72
75
 
73
76
  ```bash
74
- camo start [profileId] [--url <url>] [--headless] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
77
+ camo start [profileId] [--url <url>] [--headless] [--devtools] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
75
78
  camo stop [profileId]
76
79
  camo stop --id <instanceId>
77
80
  camo stop --alias <alias>
@@ -85,6 +88,7 @@ camo shutdown # Shutdown browser-service (all sessions)
85
88
  If no saved size exists, it defaults to near-fullscreen (full width, slight vertical reserve).
86
89
  Use `--width/--height` to override and update the saved profile size.
87
90
  For headless sessions, default idle timeout is `30m` (auto-stop on inactivity). Use `--idle-timeout` (e.g. `45m`, `1800s`, `0`) to customize.
91
+ Use `--devtools` to open browser developer tools in headed mode (cannot be combined with `--headless`).
88
92
 
89
93
  ### Lifecycle & Cleanup
90
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web-auto/camo",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Camoufox Browser CLI - Cross-platform browser automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -86,6 +86,43 @@ export function buildCommentsHarvestScript(params = {}) {
86
86
  }
87
87
  return null;
88
88
  };
89
+ const normalizeInlineText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
90
+ const sanitizeAuthorText = (raw, commentText = '') => {
91
+ const text = normalizeInlineText(raw);
92
+ if (!text) return '';
93
+ if (commentText && text === commentText) return '';
94
+ if (text.length > 40) return '';
95
+ if (/^(回复|展开|收起|查看更多|评论|赞|分享|发送)$/.test(text)) return '';
96
+ return text;
97
+ };
98
+ const readAuthor = (item, commentText = '') => {
99
+ const attrNames = ['data-user-name', 'data-username', 'data-user_nickname', 'data-nickname'];
100
+ for (const attr of attrNames) {
101
+ const value = sanitizeAuthorText(item.getAttribute?.(attr), commentText);
102
+ if (value) return value;
103
+ }
104
+ const selectors = [
105
+ '.comment-user .name',
106
+ '.comment-user .username',
107
+ '.comment-user .user-name',
108
+ '.author .name',
109
+ '.author',
110
+ '.user-name',
111
+ '.username',
112
+ '.name',
113
+ 'a[href*="/user/profile/"]',
114
+ 'a[href*="/user/"]',
115
+ ];
116
+ for (const selector of selectors) {
117
+ const node = item.querySelector(selector);
118
+ if (!node) continue;
119
+ const title = sanitizeAuthorText(node.getAttribute?.('title'), commentText);
120
+ if (title) return title;
121
+ const text = sanitizeAuthorText(node.textContent, commentText);
122
+ if (text) return text;
123
+ }
124
+ return '';
125
+ };
89
126
 
90
127
  const scroller = document.querySelector('.note-scroller')
91
128
  || document.querySelector('.comments-el')
@@ -105,9 +142,8 @@ export function buildCommentsHarvestScript(params = {}) {
105
142
  const nodes = Array.from(document.querySelectorAll('.comment-item, [class*="comment-item"]'));
106
143
  for (const item of nodes) {
107
144
  const textNode = item.querySelector('.content, .comment-content, p');
108
- const authorNode = item.querySelector('.name, .author, .user-name, [class*="author"], [class*="name"]');
109
145
  const text = String((textNode && textNode.textContent) || '').trim();
110
- const author = String((authorNode && authorNode.textContent) || '').trim();
146
+ const author = readAuthor(item, text);
111
147
  if (!text) continue;
112
148
  const key = author + '::' + text;
113
149
  if (commentMap.has(key)) continue;
@@ -74,6 +74,42 @@ function buildCollectLikeTargetsScript() {
74
74
  }
75
75
  return '';
76
76
  };
77
+ const sanitizeUserName = (raw, commentText = '') => {
78
+ const text = String(raw || '').replace(/\\s+/g, ' ').trim();
79
+ if (!text) return '';
80
+ if (commentText && text === commentText) return '';
81
+ if (text.length > 40) return '';
82
+ if (/^(回复|展开|收起|查看更多|评论|赞|分享|发送)$/.test(text)) return '';
83
+ return text;
84
+ };
85
+ const readUserName = (item, commentText = '') => {
86
+ const attrNames = ['data-user-name', 'data-username', 'data-user_nickname', 'data-nickname'];
87
+ for (const attr of attrNames) {
88
+ const value = sanitizeUserName(item.getAttribute?.(attr), commentText);
89
+ if (value) return value;
90
+ }
91
+ const selectors = [
92
+ '.comment-user .name',
93
+ '.comment-user .username',
94
+ '.comment-user .user-name',
95
+ '.author .name',
96
+ '.author',
97
+ '.user-name',
98
+ '.username',
99
+ '.name',
100
+ 'a[href*="/user/profile/"]',
101
+ 'a[href*="/user/"]',
102
+ ];
103
+ for (const selector of selectors) {
104
+ const node = item.querySelector(selector);
105
+ if (!node) continue;
106
+ const title = sanitizeUserName(node.getAttribute?.('title'), commentText);
107
+ if (title) return title;
108
+ const text = sanitizeUserName(node.textContent, commentText);
109
+ if (text) return text;
110
+ }
111
+ return '';
112
+ };
77
113
  const readAttr = (item, attrNames) => {
78
114
  for (const attr of attrNames) {
79
115
  const value = String(item.getAttribute?.(attr) || '').trim();
@@ -81,6 +117,15 @@ function buildCollectLikeTargetsScript() {
81
117
  }
82
118
  return '';
83
119
  };
120
+ const readUserId = (item) => {
121
+ const value = readAttr(item, ['data-user-id', 'data-userid', 'data-user_id']);
122
+ if (value) return value;
123
+ const anchor = item.querySelector('a[href*="/user/profile/"], a[href*="/user/"]');
124
+ const href = String(anchor?.getAttribute?.('href') || '').trim();
125
+ if (!href) return '';
126
+ const matched = href.match(/\\/user\\/(?:profile\\/)?([a-zA-Z0-9_-]+)/);
127
+ return matched && matched[1] ? matched[1] : '';
128
+ };
84
129
 
85
130
  const matchedSet = new Set(
86
131
  Array.isArray(state.matchedComments)
@@ -92,8 +137,8 @@ function buildCollectLikeTargetsScript() {
92
137
  const item = items[index];
93
138
  const text = readText(item, ['.content', '.comment-content', 'p']);
94
139
  if (!text) continue;
95
- const userName = readText(item, ['.name', '.author', '.user-name', '.username', '[class*="author"]', '[class*="name"]']);
96
- const userId = readAttr(item, ['data-user-id', 'data-userid', 'data-user_id']);
140
+ const userName = readUserName(item, text);
141
+ const userId = readUserId(item);
97
142
  const timestamp = readText(item, ['.date', '.time', '.timestamp', '[class*="time"]']);
98
143
  const likeControl = findLikeControl(item);
99
144
  rows.push({
@@ -8,6 +8,10 @@ import {
8
8
  import { callAPI, ensureCamoufox, ensureBrowserService, getSessionByProfile, checkBrowserService } from '../utils/browser-service.mjs';
9
9
  import { resolveProfileId, ensureUrlScheme, looksLikeUrlToken, getPositionals } from '../utils/args.mjs';
10
10
  import { acquireLock, releaseLock, cleanupStaleLocks } from '../lifecycle/lock.mjs';
11
+ import {
12
+ buildSelectorClickScript,
13
+ buildSelectorTypeScript,
14
+ } from '../container/runtime-core/operations/selector-scripts.mjs';
11
15
  import {
12
16
  registerSession,
13
17
  updateSession,
@@ -26,6 +30,9 @@ const START_WINDOW_MIN_HEIGHT = 700;
26
30
  const START_WINDOW_MAX_RESERVE = 240;
27
31
  const START_WINDOW_DEFAULT_RESERVE = 72;
28
32
  const DEFAULT_HEADLESS_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
33
+ const DEVTOOLS_SHORTCUTS = process.platform === 'darwin'
34
+ ? ['Meta+Alt+I', 'F12']
35
+ : ['F12', 'Control+Shift+I'];
29
36
 
30
37
  function sleep(ms) {
31
38
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -122,6 +129,67 @@ async function stopAndCleanupProfile(profileId, options = {}) {
122
129
  };
123
130
  }
124
131
 
132
+ async function probeViewportSize(profileId) {
133
+ try {
134
+ const payload = await callAPI('evaluate', {
135
+ profileId,
136
+ script: '(() => ({ width: Number(window.innerWidth || 0), height: Number(window.innerHeight || 0) }))()',
137
+ });
138
+ const size = payload?.result || payload?.data || payload || {};
139
+ const width = Number(size?.width || 0);
140
+ const height = Number(size?.height || 0);
141
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
142
+ return { width, height };
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+
148
+ export async function requestDevtoolsOpen(profileId, options = {}) {
149
+ const id = String(profileId || '').trim();
150
+ if (!id) {
151
+ return { ok: false, requested: false, reason: 'profile_required', shortcuts: [] };
152
+ }
153
+
154
+ const shortcuts = Array.isArray(options.shortcuts) && options.shortcuts.length
155
+ ? options.shortcuts.map((item) => String(item || '').trim()).filter(Boolean)
156
+ : DEVTOOLS_SHORTCUTS;
157
+ const settleMs = Math.max(0, parseNumber(options.settleMs, 180));
158
+ const before = await probeViewportSize(id);
159
+ const attempts = [];
160
+
161
+ for (const key of shortcuts) {
162
+ try {
163
+ await callAPI('keyboard:press', { profileId: id, key });
164
+ attempts.push({ key, ok: true });
165
+ if (settleMs > 0) {
166
+ // Allow browser UI animation to settle after shortcut.
167
+ // eslint-disable-next-line no-await-in-loop
168
+ await sleep(settleMs);
169
+ }
170
+ } catch (err) {
171
+ attempts.push({ key, ok: false, error: err?.message || String(err) });
172
+ }
173
+ }
174
+
175
+ const after = await probeViewportSize(id);
176
+ const beforeHeight = Number(before?.height || 0);
177
+ const afterHeight = Number(after?.height || 0);
178
+ const viewportReduced = beforeHeight > 0 && afterHeight > 0 && afterHeight < (beforeHeight - 100);
179
+ const successCount = attempts.filter((item) => item.ok).length;
180
+
181
+ return {
182
+ ok: successCount > 0,
183
+ requested: true,
184
+ shortcuts,
185
+ attempts,
186
+ before,
187
+ after,
188
+ verified: viewportReduced,
189
+ verification: viewportReduced ? 'viewport_height_reduced' : 'shortcut_dispatched',
190
+ };
191
+ }
192
+
125
193
  export function computeTargetViewportFromWindowMetrics(measured) {
126
194
  const innerWidth = Math.max(320, parseNumber(measured?.innerWidth, 0));
127
195
  const innerHeight = Math.max(240, parseNumber(measured?.innerHeight, 0));
@@ -250,8 +318,9 @@ export async function handleStartCommand(args) {
250
318
  const alias = validateAlias(readFlagValue(args, ['--alias']));
251
319
  const idleTimeoutRaw = readFlagValue(args, ['--idle-timeout']);
252
320
  const parsedIdleTimeoutMs = parseDurationMs(idleTimeoutRaw, DEFAULT_HEADLESS_IDLE_TIMEOUT_MS);
321
+ const wantsDevtools = args.includes('--devtools');
253
322
  if (hasExplicitWidth !== hasExplicitHeight) {
254
- throw new Error('Usage: camo start [profileId] [--url <url>] [--headless] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]');
323
+ throw new Error('Usage: camo start [profileId] [--url <url>] [--headless] [--devtools] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]');
255
324
  }
256
325
  if ((hasExplicitWidth && explicitWidth < START_WINDOW_MIN_WIDTH) || (hasExplicitHeight && explicitHeight < START_WINDOW_MIN_HEIGHT)) {
257
326
  throw new Error(`Window size too small. Minimum is ${START_WINDOW_MIN_WIDTH}x${START_WINDOW_MIN_HEIGHT}`);
@@ -308,7 +377,7 @@ export async function handleStartCommand(args) {
308
377
  alias: alias || null,
309
378
  });
310
379
  const idleState = computeIdleState(record);
311
- console.log(JSON.stringify({
380
+ const payload = {
312
381
  ok: true,
313
382
  sessionId: existing.session_id || existing.profileId,
314
383
  instanceId: record.instanceId,
@@ -323,7 +392,13 @@ export async function handleStartCommand(args) {
323
392
  byId: `camo stop --id ${record.instanceId}`,
324
393
  byAlias: record.alias ? `camo stop --alias ${record.alias}` : null,
325
394
  },
326
- }, null, 2));
395
+ };
396
+ const existingMode = String(existing?.mode || record?.mode || '').toLowerCase();
397
+ const existingHeadless = existing?.headless === true || existingMode.includes('headless');
398
+ if (!existingHeadless && wantsDevtools) {
399
+ payload.devtools = await requestDevtoolsOpen(profileId);
400
+ }
401
+ console.log(JSON.stringify(payload, null, 2));
327
402
  startSessionWatchdog(profileId);
328
403
  return;
329
404
  }
@@ -338,12 +413,16 @@ export async function handleStartCommand(args) {
338
413
  }
339
414
 
340
415
  const headless = args.includes('--headless');
416
+ if (wantsDevtools && headless) {
417
+ throw new Error('--devtools is not supported with --headless');
418
+ }
341
419
  const idleTimeoutMs = headless ? parsedIdleTimeoutMs : 0;
342
420
  const targetUrl = explicitUrl || implicitUrl;
343
421
  const result = await callAPI('start', {
344
422
  profileId,
345
423
  url: targetUrl ? ensureUrlScheme(targetUrl) : undefined,
346
424
  headless,
425
+ devtools: wantsDevtools,
347
426
  });
348
427
 
349
428
  if (result?.ok) {
@@ -416,6 +495,9 @@ export async function handleStartCommand(args) {
416
495
  Number.isFinite(measuredOuterHeight) ? measuredOuterHeight : windowTarget.height,
417
496
  );
418
497
  result.profileWindow = savedWindow?.window || null;
498
+ if (wantsDevtools) {
499
+ result.devtools = await requestDevtoolsOpen(profileId);
500
+ }
419
501
  }
420
502
  }
421
503
  console.log(JSON.stringify(result, null, 2));
@@ -426,6 +508,12 @@ export async function handleStopCommand(args) {
426
508
  const target = rawTarget.toLowerCase();
427
509
  const idTarget = readFlagValue(args, ['--id']);
428
510
  const aliasTarget = readFlagValue(args, ['--alias']);
511
+ if (args.includes('--id') && !idTarget) {
512
+ throw new Error('Usage: camo stop --id <instanceId>');
513
+ }
514
+ if (args.includes('--alias') && !aliasTarget) {
515
+ throw new Error('Usage: camo stop --alias <alias>');
516
+ }
429
517
  const stopIdle = target === 'idle' || args.includes('--idle');
430
518
  const stopAll = target === 'all';
431
519
  const serviceUp = await checkBrowserService();
@@ -465,11 +553,41 @@ export async function handleStopCommand(args) {
465
553
 
466
554
  if (stopIdle) {
467
555
  const now = Date.now();
468
- const idleTargets = listRegisteredSessions()
556
+ const registeredSessions = listRegisteredSessions();
557
+ let liveSessions = [];
558
+ if (serviceUp) {
559
+ try {
560
+ const status = await callAPI('getStatus', {});
561
+ liveSessions = Array.isArray(status?.sessions) ? status.sessions : [];
562
+ } catch {
563
+ // Ignore and fallback to local registry.
564
+ }
565
+ }
566
+ const regMap = new Map(
567
+ registeredSessions
568
+ .filter((item) => item && String(item?.status || '').trim() === 'active')
569
+ .map((item) => [String(item.profileId || '').trim(), item]),
570
+ );
571
+ const idleTargets = new Set(
572
+ registeredSessions
469
573
  .filter((item) => String(item?.status || '').trim() === 'active')
470
574
  .map((item) => ({ session: item, idle: computeIdleState(item, now) }))
471
575
  .filter((item) => item.idle.idle)
472
- .map((item) => item.session.profileId);
576
+ .map((item) => item.session.profileId),
577
+ );
578
+ let orphanLiveHeadlessCount = 0;
579
+ for (const live of liveSessions) {
580
+ const liveProfileId = String(live?.profileId || '').trim();
581
+ if (!liveProfileId) continue;
582
+ if (regMap.has(liveProfileId) || idleTargets.has(liveProfileId)) continue;
583
+ const mode = String(live?.mode || '').toLowerCase();
584
+ const liveHeadless = live?.headless === true || mode.includes('headless');
585
+ // Live but unregistered headless sessions are treated as idle-orphan targets.
586
+ if (liveHeadless) {
587
+ idleTargets.add(liveProfileId);
588
+ orphanLiveHeadlessCount += 1;
589
+ }
590
+ }
473
591
  const results = [];
474
592
  for (const profileId of idleTargets) {
475
593
  // eslint-disable-next-line no-await-in-loop
@@ -479,7 +597,8 @@ export async function handleStopCommand(args) {
479
597
  ok: true,
480
598
  mode: 'idle',
481
599
  serviceUp,
482
- targetCount: idleTargets.length,
600
+ targetCount: idleTargets.size,
601
+ orphanLiveHeadlessCount,
483
602
  closed: results.filter((item) => item.ok).length,
484
603
  failed: results.filter((item) => !item.ok).length,
485
604
  results,
@@ -652,14 +771,7 @@ export async function handleClickCommand(args) {
652
771
 
653
772
  const result = await callAPI('evaluate', {
654
773
  profileId,
655
- script: `(async () => {
656
- const el = document.querySelector(${JSON.stringify(selector)});
657
- if (!el) throw new Error('Element not found: ' + ${JSON.stringify(selector)});
658
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
659
- await new Promise(r => setTimeout(r, 200));
660
- el.click();
661
- return { clicked: true, selector: ${JSON.stringify(selector)} };
662
- })()`
774
+ script: buildSelectorClickScript({ selector, highlight: false }),
663
775
  });
664
776
  console.log(JSON.stringify(result, null, 2));
665
777
  }
@@ -686,18 +798,7 @@ export async function handleTypeCommand(args) {
686
798
 
687
799
  const result = await callAPI('evaluate', {
688
800
  profileId,
689
- script: `(async () => {
690
- const el = document.querySelector(${JSON.stringify(selector)});
691
- if (!el) throw new Error('Element not found: ' + ${JSON.stringify(selector)});
692
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
693
- await new Promise(r => setTimeout(r, 200));
694
- el.focus();
695
- el.value = '';
696
- el.value = ${JSON.stringify(text)};
697
- el.dispatchEvent(new Event('input', { bubbles: true }));
698
- el.dispatchEvent(new Event('change', { bubbles: true }));
699
- return { typed: true, selector: ${JSON.stringify(selector)}, length: ${text.length} };
700
- })()`
801
+ script: buildSelectorTypeScript({ selector, highlight: false, text }),
701
802
  });
702
803
  console.log(JSON.stringify(result, null, 2));
703
804
  }
@@ -33,7 +33,20 @@ export function buildSelectorClickScript({ selector, highlight }) {
33
33
  }
34
34
  el.scrollIntoView({ behavior: 'smooth', block: 'center' });
35
35
  await new Promise((r) => setTimeout(r, 150));
36
- el.click();
36
+ if (el instanceof HTMLElement) {
37
+ try { el.focus({ preventScroll: true }); } catch {}
38
+ const common = { bubbles: true, cancelable: true, view: window };
39
+ try {
40
+ if (typeof PointerEvent === 'function') {
41
+ el.dispatchEvent(new PointerEvent('pointerdown', { ...common, pointerType: 'mouse', button: 0 }));
42
+ el.dispatchEvent(new PointerEvent('pointerup', { ...common, pointerType: 'mouse', button: 0 }));
43
+ }
44
+ } catch {}
45
+ try { el.dispatchEvent(new MouseEvent('mousedown', { ...common, button: 0 })); } catch {}
46
+ try { el.dispatchEvent(new MouseEvent('mouseup', { ...common, button: 0 })); } catch {}
47
+ }
48
+ if (typeof el.click === 'function') el.click();
49
+ else el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window, button: 0 }));
37
50
  if (${highlightLiteral} && el instanceof HTMLElement) {
38
51
  setTimeout(() => { el.style.outline = restoreOutline; }, 260);
39
52
  }
@@ -56,9 +69,63 @@ export function buildSelectorTypeScript({ selector, highlight, text }) {
56
69
  }
57
70
  el.scrollIntoView({ behavior: 'smooth', block: 'center' });
58
71
  await new Promise((r) => setTimeout(r, 150));
59
- el.focus();
60
- el.value = ${textLiteral};
61
- el.dispatchEvent(new Event('input', { bubbles: true }));
72
+ if (el instanceof HTMLElement) {
73
+ try { el.focus({ preventScroll: true }); } catch {}
74
+ if (typeof el.click === 'function') el.click();
75
+ }
76
+ const value = ${textLiteral};
77
+ const fireInputEvent = (target, name, init) => {
78
+ try {
79
+ if (typeof InputEvent === 'function') {
80
+ target.dispatchEvent(new InputEvent(name, init));
81
+ return;
82
+ }
83
+ } catch {}
84
+ target.dispatchEvent(new Event(name, { bubbles: true, cancelable: init?.cancelable === true }));
85
+ };
86
+ const assignControlValue = (target, next) => {
87
+ if (target instanceof HTMLInputElement) {
88
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
89
+ if (setter) setter.call(target, next);
90
+ else target.value = next;
91
+ if (typeof target.setSelectionRange === 'function') {
92
+ const cursor = String(next).length;
93
+ try { target.setSelectionRange(cursor, cursor); } catch {}
94
+ }
95
+ return true;
96
+ }
97
+ if (target instanceof HTMLTextAreaElement) {
98
+ const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
99
+ if (setter) setter.call(target, next);
100
+ else target.value = next;
101
+ if (typeof target.setSelectionRange === 'function') {
102
+ const cursor = String(next).length;
103
+ try { target.setSelectionRange(cursor, cursor); } catch {}
104
+ }
105
+ return true;
106
+ }
107
+ return false;
108
+ };
109
+ const editableAssigned = assignControlValue(el, value);
110
+ if (!editableAssigned) {
111
+ if (el instanceof HTMLElement && el.isContentEditable) {
112
+ el.textContent = value;
113
+ } else {
114
+ throw new Error('Element not editable: ' + ${selectorLiteral});
115
+ }
116
+ }
117
+ fireInputEvent(el, 'beforeinput', {
118
+ bubbles: true,
119
+ cancelable: true,
120
+ data: value,
121
+ inputType: 'insertText',
122
+ });
123
+ fireInputEvent(el, 'input', {
124
+ bubbles: true,
125
+ cancelable: false,
126
+ data: value,
127
+ inputType: 'insertText',
128
+ });
62
129
  el.dispatchEvent(new Event('change', { bubbles: true }));
63
130
  if (${highlightLiteral} && el instanceof HTMLElement) {
64
131
  setTimeout(() => { el.style.outline = restoreOutline; }, 260);
@@ -24,7 +24,7 @@ CONFIG:
24
24
 
25
25
  BROWSER CONTROL:
26
26
  init Ensure camoufox + ensure browser-service daemon
27
- start [profileId] [--url <url>] [--headless] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
27
+ start [profileId] [--url <url>] [--headless] [--devtools] [--alias <name>] [--idle-timeout <duration>] [--width <w> --height <h>]
28
28
  stop [profileId]
29
29
  stop --id <instanceId> Stop by instance id
30
30
  stop --alias <alias> Stop by alias
@@ -97,6 +97,7 @@ EXAMPLES:
97
97
  camo profile default myprofile
98
98
  camo start --url https://example.com --alias main
99
99
  camo start worker-1 --headless --alias shard1 --idle-timeout 45m
100
+ camo start worker-1 --devtools
100
101
  camo start myprofile --width 1920 --height 1020
101
102
  camo stop --id inst_xxxxxxxx
102
103
  camo stop --alias shard1