@web-auto/camo 0.1.7 → 0.1.8

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
@@ -42,6 +42,12 @@ camo start worker-1 --headless --alias shard1 --idle-timeout 30m
42
42
  # Start with devtools (headful only)
43
43
  camo start worker-1 --devtools
44
44
 
45
+ # Evaluate JS (devtools-style input in page context)
46
+ camo devtools eval worker-1 "document.title"
47
+
48
+ # Read captured console entries
49
+ camo devtools logs worker-1 --levels error,warn --limit 50
50
+
45
51
  # Navigate
46
52
  camo goto https://www.xiaohongshu.com
47
53
 
@@ -122,6 +128,14 @@ camo clear-highlight [profileId] # Clear all highlights
122
128
  camo viewport [profileId] --width <w> --height <h>
123
129
  ```
124
130
 
131
+ ### Devtools
132
+
133
+ ```bash
134
+ camo devtools logs [profileId] [--limit 120] [--since <unix_ms>] [--levels error,warn] [--clear]
135
+ camo devtools eval [profileId] <expression> [--profile <id>]
136
+ camo devtools clear [profileId]
137
+ ```
138
+
125
139
  ### Pages
126
140
 
127
141
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@web-auto/camo",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Camoufox Browser CLI - Cross-platform browser automation",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -13,6 +13,7 @@ import { handleSystemCommand } from './commands/system.mjs';
13
13
  import { handleContainerCommand } from './commands/container.mjs';
14
14
  import { handleAutoscriptCommand } from './commands/autoscript.mjs';
15
15
  import { handleEventsCommand } from './commands/events.mjs';
16
+ import { handleDevtoolsCommand } from './commands/devtools.mjs';
16
17
  import {
17
18
  handleStartCommand, handleStopCommand, handleStatusCommand,
18
19
  handleGotoCommand, handleBackCommand, handleScreenshotCommand,
@@ -64,6 +65,13 @@ function inferProfileId(cmd, args) {
64
65
  return positionals[0] || null;
65
66
  }
66
67
 
68
+ if (cmd === 'devtools') {
69
+ const sub = positionals[0] || null;
70
+ if (sub === 'eval' || sub === 'logs' || sub === 'clear') {
71
+ return positionals[1] || null;
72
+ }
73
+ }
74
+
67
75
  if (cmd === 'autoscript' && positionals[0] === 'run') {
68
76
  return explicitProfile || null;
69
77
  }
@@ -192,6 +200,11 @@ async function main() {
192
200
  return;
193
201
  }
194
202
 
203
+ if (cmd === 'devtools') {
204
+ await runTrackedCommand(cmd, args, () => handleDevtoolsCommand(args));
205
+ return;
206
+ }
207
+
195
208
  // Lifecycle commands
196
209
  if (cmd === 'cleanup') {
197
210
  await runTrackedCommand(cmd, args, () => handleCleanupCommand(args));
@@ -237,7 +250,7 @@ async function main() {
237
250
  'start', 'stop', 'close', 'status', 'list', 'goto', 'navigate', 'back', 'screenshot',
238
251
  'new-page', 'close-page', 'switch-page', 'list-pages', 'shutdown',
239
252
  'scroll', 'click', 'type', 'highlight', 'clear-highlight', 'viewport',
240
- 'cookies', 'window', 'mouse', 'system', 'container', 'autoscript', 'events',
253
+ 'cookies', 'window', 'mouse', 'system', 'container', 'autoscript', 'events', 'devtools',
241
254
  ]);
242
255
 
243
256
  if (!serviceCommands.has(cmd)) {
@@ -0,0 +1,349 @@
1
+ import { getDefaultProfile, listProfiles } from '../utils/config.mjs';
2
+ import { callAPI } from '../utils/browser-service.mjs';
3
+
4
+ const DEFAULT_LIMIT = 120;
5
+ const MAX_LIMIT = 1000;
6
+
7
+ function parseNumber(value, fallback) {
8
+ const num = Number(value);
9
+ return Number.isFinite(num) ? num : fallback;
10
+ }
11
+
12
+ function clamp(value, min, max) {
13
+ return Math.min(Math.max(value, min), max);
14
+ }
15
+
16
+ function readFlagValue(args, names) {
17
+ for (let i = 0; i < args.length; i += 1) {
18
+ if (!names.includes(args[i])) continue;
19
+ const value = args[i + 1];
20
+ if (!value || String(value).startsWith('-')) return null;
21
+ return value;
22
+ }
23
+ return null;
24
+ }
25
+
26
+ function collectPositionals(args, startIndex = 2) {
27
+ const values = [];
28
+ for (let i = startIndex; i < args.length; i += 1) {
29
+ const token = args[i];
30
+ if (!token || String(token).startsWith('--')) {
31
+ continue;
32
+ }
33
+ const prev = args[i - 1];
34
+ if (prev && ['--profile', '-p', '--limit', '-n', '--levels', '--since'].includes(prev)) {
35
+ continue;
36
+ }
37
+ values.push(String(token));
38
+ }
39
+ return values;
40
+ }
41
+
42
+ function pickProfileAndExpression(args, subcommand) {
43
+ const explicitProfile = readFlagValue(args, ['--profile', '-p']);
44
+ const profileSet = new Set(listProfiles());
45
+ const positionals = collectPositionals(args, 2);
46
+
47
+ let profileId = explicitProfile || null;
48
+ let expression = null;
49
+
50
+ if (subcommand === 'eval') {
51
+ if (positionals.length === 0) {
52
+ return { profileId: profileId || getDefaultProfile(), expression: null };
53
+ }
54
+ if (!profileId && positionals.length >= 2) {
55
+ profileId = positionals[0];
56
+ expression = positionals.slice(1).join(' ').trim();
57
+ } else if (!profileId && profileSet.has(positionals[0])) {
58
+ profileId = positionals[0];
59
+ expression = positionals.slice(1).join(' ').trim();
60
+ } else {
61
+ expression = positionals.join(' ').trim();
62
+ }
63
+ return { profileId: profileId || getDefaultProfile(), expression };
64
+ }
65
+
66
+ if (positionals.length > 0) {
67
+ if (!profileId && (profileSet.has(positionals[0]) || subcommand === 'logs' || subcommand === 'clear')) {
68
+ profileId = positionals[0];
69
+ }
70
+ }
71
+ return { profileId: profileId || getDefaultProfile(), expression: null };
72
+ }
73
+
74
+ function buildConsoleInstallScript(maxEntries) {
75
+ return `(function installCamoDevtoolsConsoleCollector() {
76
+ const KEY = '__camo_console_collector_v1__';
77
+ const BUFFER_KEY = '__camo_console_buffer_v1__';
78
+ const MAX = ${Math.max(100, Math.floor(maxEntries || MAX_LIMIT))};
79
+ const now = () => Date.now();
80
+
81
+ const stringify = (value) => {
82
+ if (typeof value === 'string') return value;
83
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);
84
+ if (value === null) return 'null';
85
+ if (typeof value === 'undefined') return 'undefined';
86
+ if (typeof value === 'function') return '[function]';
87
+ if (typeof value === 'symbol') return String(value);
88
+ if (value instanceof Error) return value.stack || value.message || String(value);
89
+ try {
90
+ return JSON.stringify(value);
91
+ } catch {
92
+ return Object.prototype.toString.call(value);
93
+ }
94
+ };
95
+
96
+ const pushEntry = (level, args) => {
97
+ const target = window[BUFFER_KEY];
98
+ if (!Array.isArray(target)) return;
99
+ const text = Array.from(args || []).map(stringify).join(' ');
100
+ target.push({
101
+ ts: now(),
102
+ level,
103
+ text,
104
+ href: String(window.location?.href || ''),
105
+ });
106
+ if (target.length > MAX) {
107
+ target.splice(0, target.length - MAX);
108
+ }
109
+ };
110
+
111
+ if (!Array.isArray(window[BUFFER_KEY])) {
112
+ window[BUFFER_KEY] = [];
113
+ }
114
+
115
+ if (!window[KEY]) {
116
+ const levels = ['log', 'info', 'warn', 'error', 'debug'];
117
+ const originals = {};
118
+ for (const level of levels) {
119
+ const raw = typeof console[level] === 'function' ? console[level] : console.log;
120
+ originals[level] = raw.bind(console);
121
+ console[level] = (...args) => {
122
+ try {
123
+ pushEntry(level, args);
124
+ } catch {}
125
+ return originals[level](...args);
126
+ };
127
+ }
128
+
129
+ window.addEventListener('error', (event) => {
130
+ try {
131
+ const message = event?.message || 'window.error';
132
+ pushEntry('error', [message]);
133
+ } catch {}
134
+ });
135
+ window.addEventListener('unhandledrejection', (event) => {
136
+ try {
137
+ const reason = event?.reason instanceof Error
138
+ ? (event.reason.stack || event.reason.message)
139
+ : stringify(event?.reason);
140
+ pushEntry('error', ['unhandledrejection', reason]);
141
+ } catch {}
142
+ });
143
+
144
+ window[KEY] = { installedAt: now(), max: MAX };
145
+ }
146
+
147
+ return {
148
+ ok: true,
149
+ installed: true,
150
+ entries: Array.isArray(window[BUFFER_KEY]) ? window[BUFFER_KEY].length : 0,
151
+ max: MAX,
152
+ };
153
+ })();`;
154
+ }
155
+
156
+ function buildConsoleReadScript(options = {}) {
157
+ const limit = clamp(parseNumber(options.limit, DEFAULT_LIMIT), 1, MAX_LIMIT);
158
+ const sinceTs = Math.max(0, parseNumber(options.sinceTs, 0) || 0);
159
+ const clear = options.clear === true;
160
+ const levels = Array.isArray(options.levels)
161
+ ? options.levels.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)
162
+ : [];
163
+ const levelsLiteral = JSON.stringify(levels);
164
+
165
+ return `(function readCamoDevtoolsConsole() {
166
+ const BUFFER_KEY = '__camo_console_buffer_v1__';
167
+ const raw = Array.isArray(window[BUFFER_KEY]) ? window[BUFFER_KEY] : [];
168
+ const levelSet = new Set(${levelsLiteral});
169
+ const list = raw.filter((entry) => {
170
+ if (!entry || typeof entry !== 'object') return false;
171
+ const ts = Number(entry.ts || 0);
172
+ if (ts < ${sinceTs}) return false;
173
+ if (levelSet.size === 0) return true;
174
+ return levelSet.has(String(entry.level || '').toLowerCase());
175
+ });
176
+ const entries = list.slice(Math.max(0, list.length - ${limit}));
177
+ if (${clear ? 'true' : 'false'}) {
178
+ window[BUFFER_KEY] = [];
179
+ }
180
+ return {
181
+ ok: true,
182
+ total: raw.length,
183
+ returned: entries.length,
184
+ sinceTs: ${sinceTs},
185
+ levels: Array.from(levelSet),
186
+ cleared: ${clear ? 'true' : 'false'},
187
+ entries,
188
+ };
189
+ })();`;
190
+ }
191
+
192
+ function buildEvalScript(expression) {
193
+ return `(async function runCamoDevtoolsEval() {
194
+ const expr = ${JSON.stringify(expression || '')};
195
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
196
+ const resultPayload = { ok: true, mode: 'expression', value: null, valueType: null };
197
+
198
+ const toSerializable = (value, depth = 0, seen = new WeakSet()) => {
199
+ if (value === null) return null;
200
+ if (typeof value === 'undefined') return '[undefined]';
201
+ if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') return value;
202
+ if (typeof value === 'bigint') return value.toString();
203
+ if (typeof value === 'function') return '[function]';
204
+ if (typeof value === 'symbol') return value.toString();
205
+ if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack || null };
206
+ if (depth >= 3) return '[max-depth]';
207
+ if (Array.isArray(value)) return value.slice(0, 30).map((item) => toSerializable(item, depth + 1, seen));
208
+ if (typeof value === 'object') {
209
+ if (seen.has(value)) return '[circular]';
210
+ seen.add(value);
211
+ const out = {};
212
+ const keys = Object.keys(value).slice(0, 30);
213
+ for (const key of keys) {
214
+ out[key] = toSerializable(value[key], depth + 1, seen);
215
+ }
216
+ return out;
217
+ }
218
+ try {
219
+ return JSON.parse(JSON.stringify(value));
220
+ } catch {
221
+ return String(value);
222
+ }
223
+ };
224
+
225
+ try {
226
+ const fn = new AsyncFunction('return (' + expr + ')');
227
+ const value = await fn();
228
+ resultPayload.value = toSerializable(value);
229
+ resultPayload.valueType = typeof value;
230
+ return resultPayload;
231
+ } catch (exprError) {
232
+ try {
233
+ const fn = new AsyncFunction(expr);
234
+ const value = await fn();
235
+ resultPayload.mode = 'statement';
236
+ resultPayload.value = toSerializable(value);
237
+ resultPayload.valueType = typeof value;
238
+ return resultPayload;
239
+ } catch (statementError) {
240
+ return {
241
+ ok: false,
242
+ mode: 'statement',
243
+ error: {
244
+ message: statementError?.message || String(statementError),
245
+ stack: statementError?.stack || null,
246
+ expressionError: exprError?.message || String(exprError),
247
+ },
248
+ };
249
+ }
250
+ }
251
+ })();`;
252
+ }
253
+
254
+ async function ensureConsoleCollector(profileId, maxEntries = MAX_LIMIT) {
255
+ return callAPI('evaluate', {
256
+ profileId,
257
+ script: buildConsoleInstallScript(maxEntries),
258
+ });
259
+ }
260
+
261
+ async function handleLogs(args) {
262
+ const { profileId } = pickProfileAndExpression(args, 'logs');
263
+ if (!profileId) {
264
+ throw new Error('No default profile set. Run: camo profile default <profileId>');
265
+ }
266
+ const limit = clamp(parseNumber(readFlagValue(args, ['--limit', '-n']), DEFAULT_LIMIT), 1, MAX_LIMIT);
267
+ const sinceTs = Math.max(0, parseNumber(readFlagValue(args, ['--since']), 0) || 0);
268
+ const levelsRaw = readFlagValue(args, ['--levels', '--level']) || '';
269
+ const levels = levelsRaw
270
+ .split(',')
271
+ .map((item) => String(item || '').trim().toLowerCase())
272
+ .filter(Boolean);
273
+ const clear = args.includes('--clear');
274
+
275
+ const install = await ensureConsoleCollector(profileId, MAX_LIMIT);
276
+ const result = await callAPI('evaluate', {
277
+ profileId,
278
+ script: buildConsoleReadScript({ limit, sinceTs, levels, clear }),
279
+ });
280
+
281
+ console.log(JSON.stringify({
282
+ ok: true,
283
+ command: 'devtools.logs',
284
+ profileId,
285
+ collector: install?.result || install?.data || install || null,
286
+ result: result?.result || result?.data || result || null,
287
+ }, null, 2));
288
+ }
289
+
290
+ async function handleClear(args) {
291
+ const { profileId } = pickProfileAndExpression(args, 'clear');
292
+ if (!profileId) {
293
+ throw new Error('No default profile set. Run: camo profile default <profileId>');
294
+ }
295
+ await ensureConsoleCollector(profileId, MAX_LIMIT);
296
+ const result = await callAPI('evaluate', {
297
+ profileId,
298
+ script: buildConsoleReadScript({ limit: MAX_LIMIT, sinceTs: 0, clear: true }),
299
+ });
300
+ console.log(JSON.stringify({
301
+ ok: true,
302
+ command: 'devtools.clear',
303
+ profileId,
304
+ result: result?.result || result?.data || result || null,
305
+ }, null, 2));
306
+ }
307
+
308
+ async function handleEval(args) {
309
+ const { profileId, expression } = pickProfileAndExpression(args, 'eval');
310
+ if (!profileId) {
311
+ throw new Error('No default profile set. Run: camo profile default <profileId>');
312
+ }
313
+ if (!expression) {
314
+ throw new Error('Usage: camo devtools eval [profileId] <expression> [--profile <id>]');
315
+ }
316
+
317
+ await ensureConsoleCollector(profileId, MAX_LIMIT);
318
+ const result = await callAPI('evaluate', {
319
+ profileId,
320
+ script: buildEvalScript(expression),
321
+ });
322
+ console.log(JSON.stringify({
323
+ ok: true,
324
+ command: 'devtools.eval',
325
+ profileId,
326
+ expression,
327
+ result: result?.result || result?.data || result || null,
328
+ }, null, 2));
329
+ }
330
+
331
+ export async function handleDevtoolsCommand(args) {
332
+ const sub = String(args[1] || '').trim().toLowerCase();
333
+ switch (sub) {
334
+ case 'logs':
335
+ return handleLogs(args);
336
+ case 'clear':
337
+ return handleClear(args);
338
+ case 'eval':
339
+ return handleEval(args);
340
+ default:
341
+ console.log(`Usage: camo devtools <logs|eval|clear> [options]
342
+
343
+ Commands:
344
+ logs [profileId] [--limit 120] [--since <unix_ms>] [--levels error,warn] [--clear]
345
+ eval [profileId] <expression> [--profile <id>]
346
+ clear [profileId]
347
+ `);
348
+ }
349
+ }
@@ -63,6 +63,11 @@ PAGES:
63
63
  switch-page [profileId] <index>
64
64
  list-pages [profileId]
65
65
 
66
+ DEVTOOLS:
67
+ devtools logs [profileId] [--limit 120] [--since <unix_ms>] [--levels error,warn] [--clear]
68
+ devtools eval [profileId] <expression> [--profile <id>]
69
+ devtools clear [profileId]
70
+
66
71
  COOKIES:
67
72
  cookies get [profileId] Get all cookies for profile
68
73
  cookies save [profileId] --path <file> Save cookies to file
@@ -99,6 +104,8 @@ EXAMPLES:
99
104
  camo start worker-1 --headless --alias shard1 --idle-timeout 45m
100
105
  camo start worker-1 --devtools
101
106
  camo start myprofile --width 1920 --height 1020
107
+ camo devtools eval myprofile "document.title"
108
+ camo devtools logs myprofile --levels error,warn --limit 50
102
109
  camo stop --id inst_xxxxxxxx
103
110
  camo stop --alias shard1
104
111
  camo stop idle