@web-auto/camo 0.1.18 → 0.1.19

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.
Files changed (90) hide show
  1. package/README.md +18 -19
  2. package/bin/browser-service.mjs +11 -0
  3. package/package.json +7 -2
  4. package/scripts/install.mjs +3 -3
  5. package/src/cli.mjs +8 -5
  6. package/src/commands/attach.mjs +141 -0
  7. package/src/commands/browser.mjs +5 -16
  8. package/src/commands/mouse.mjs +2 -12
  9. package/src/container/runtime-core/operations/index.mjs +6 -15
  10. package/src/container/subscription-registry.mjs +6 -6
  11. package/src/core/actions.mjs +0 -12
  12. package/src/core/index.mjs +0 -1
  13. package/src/lifecycle/lock.mjs +7 -3
  14. package/src/services/browser-service/index.js +651 -0
  15. package/src/services/browser-service/index.js.map +1 -0
  16. package/src/services/browser-service/internal/BrowserSession.input.test.js +322 -0
  17. package/src/services/browser-service/internal/BrowserSession.input.test.js.map +1 -0
  18. package/src/services/browser-service/internal/BrowserSession.js +304 -0
  19. package/src/services/browser-service/internal/BrowserSession.js.map +1 -0
  20. package/src/services/browser-service/internal/ElementRegistry.js +61 -0
  21. package/src/services/browser-service/internal/ElementRegistry.js.map +1 -0
  22. package/src/services/browser-service/internal/ProfileLock.js +85 -0
  23. package/src/services/browser-service/internal/ProfileLock.js.map +1 -0
  24. package/src/services/browser-service/internal/SessionManager.js +184 -0
  25. package/src/services/browser-service/internal/SessionManager.js.map +1 -0
  26. package/src/services/browser-service/internal/SessionManager.test.js +40 -0
  27. package/src/services/browser-service/internal/SessionManager.test.js.map +1 -0
  28. package/src/services/browser-service/internal/browser-session/cookies.js +145 -0
  29. package/src/services/browser-service/internal/browser-session/cookies.js.map +1 -0
  30. package/src/services/browser-service/internal/browser-session/input-ops.js +127 -0
  31. package/src/services/browser-service/internal/browser-session/input-ops.js.map +1 -0
  32. package/src/services/browser-service/internal/browser-session/input-pipeline.js +133 -0
  33. package/src/services/browser-service/internal/browser-session/input-pipeline.js.map +1 -0
  34. package/src/services/browser-service/internal/browser-session/logging.js +46 -0
  35. package/src/services/browser-service/internal/browser-session/navigation.js +39 -0
  36. package/src/services/browser-service/internal/browser-session/navigation.js.map +1 -0
  37. package/src/services/browser-service/internal/browser-session/page-hooks.js +443 -0
  38. package/src/services/browser-service/internal/browser-session/page-hooks.js.map +1 -0
  39. package/src/services/browser-service/internal/browser-session/page-management.js +212 -0
  40. package/src/services/browser-service/internal/browser-session/page-management.js.map +1 -0
  41. package/src/services/browser-service/internal/browser-session/recording.js +199 -0
  42. package/src/services/browser-service/internal/browser-session/recording.js.map +1 -0
  43. package/src/services/browser-service/internal/browser-session/runtime-events.js +62 -0
  44. package/src/services/browser-service/internal/browser-session/runtime-events.js.map +1 -0
  45. package/src/services/browser-service/internal/browser-session/session-core.js +85 -0
  46. package/src/services/browser-service/internal/browser-session/session-core.js.map +1 -0
  47. package/src/services/browser-service/internal/browser-session/session-state.js +39 -0
  48. package/src/services/browser-service/internal/browser-session/session-state.js.map +1 -0
  49. package/src/services/browser-service/internal/browser-session/types.js +15 -0
  50. package/src/services/browser-service/internal/browser-session/types.js.map +1 -0
  51. package/src/services/browser-service/internal/browser-session/utils.js +69 -0
  52. package/src/services/browser-service/internal/browser-session/utils.js.map +1 -0
  53. package/src/services/browser-service/internal/browser-session/viewport-manager.js +47 -0
  54. package/src/services/browser-service/internal/browser-session/viewport-manager.js.map +1 -0
  55. package/src/services/browser-service/internal/browser-session/viewport.js +216 -0
  56. package/src/services/browser-service/internal/browser-session/viewport.js.map +1 -0
  57. package/src/services/browser-service/internal/container-matcher.js +852 -0
  58. package/src/services/browser-service/internal/container-matcher.js.map +1 -0
  59. package/src/services/browser-service/internal/container-registry.js +182 -0
  60. package/src/services/browser-service/internal/engine-manager.js +259 -0
  61. package/src/services/browser-service/internal/engine-manager.js.map +1 -0
  62. package/src/services/browser-service/internal/fingerprint.js +203 -0
  63. package/src/services/browser-service/internal/fingerprint.js.map +1 -0
  64. package/src/services/browser-service/internal/heartbeat.js +137 -0
  65. package/src/services/browser-service/internal/logging.js +46 -0
  66. package/src/services/browser-service/internal/pageRuntime.js +29 -0
  67. package/src/services/browser-service/internal/pageRuntime.js.map +1 -0
  68. package/src/services/browser-service/internal/runtimeInjector.js +30 -0
  69. package/src/services/browser-service/internal/runtimeInjector.js.map +1 -0
  70. package/src/services/browser-service/internal/service-process-logger.js +140 -0
  71. package/src/services/browser-service/internal/state-bus.js +46 -0
  72. package/src/services/browser-service/internal/state-bus.js.map +1 -0
  73. package/src/services/browser-service/internal/storage-paths.js +42 -0
  74. package/src/services/browser-service/internal/storage-paths.js.map +1 -0
  75. package/src/services/browser-service/internal/ws-server.js +1194 -0
  76. package/src/services/browser-service/internal/ws-server.js.map +1 -0
  77. package/src/services/browser-service/internal/ws-server.test.js +59 -0
  78. package/src/services/browser-service/internal/ws-server.test.js.map +1 -0
  79. package/src/services/browser-service/server.mjs +6 -0
  80. package/src/services/controller/cli-bridge.js +93 -0
  81. package/src/services/controller/container-index.js +50 -0
  82. package/src/services/controller/container-storage.js +36 -0
  83. package/src/services/controller/controller-actions.js +207 -0
  84. package/src/services/controller/controller.js +1138 -0
  85. package/src/services/controller/selectors.js +54 -0
  86. package/src/services/controller/transport.js +118 -0
  87. package/src/utils/browser-service.mjs +100 -125
  88. package/src/utils/config.mjs +22 -21
  89. package/src/utils/help.mjs +11 -9
  90. package/src/utils/ws-client.mjs +30 -0
@@ -0,0 +1,54 @@
1
+ export function normalizeSelectors(rawSelectors) {
2
+ const out = [];
3
+ const pushCss = (css, extra = {}) => {
4
+ if (typeof css !== 'string' || !css.trim()) return;
5
+ out.push({
6
+ css: css.trim(),
7
+ ...(typeof extra.variant === 'string' ? { variant: extra.variant } : {}),
8
+ ...(Number.isFinite(Number(extra.score)) ? { score: Number(extra.score) } : {}),
9
+ });
10
+ };
11
+
12
+ const splitLegacySelector = (rawSelector) => {
13
+ if (typeof rawSelector !== 'string') return [];
14
+ return rawSelector
15
+ .split(',')
16
+ .map((item) => item.trim())
17
+ .filter(Boolean);
18
+ };
19
+
20
+ if (Array.isArray(rawSelectors)) {
21
+ for (const item of rawSelectors) {
22
+ if (typeof item === 'string') {
23
+ for (const css of splitLegacySelector(item)) pushCss(css);
24
+ continue;
25
+ }
26
+ if (item && typeof item === 'object') {
27
+ if (typeof item.css === 'string') {
28
+ pushCss(item.css, item);
29
+ } else if (typeof item.selector === 'string') {
30
+ for (const css of splitLegacySelector(item.selector)) pushCss(css, item);
31
+ } else if (typeof item.id === 'string' && item.id) {
32
+ pushCss(`#${item.id}`, item);
33
+ } else if (Array.isArray(item.classes) && item.classes.length > 0) {
34
+ pushCss(`.${item.classes.filter(Boolean).join('.')}`, item);
35
+ }
36
+ }
37
+ }
38
+ } else if (typeof rawSelectors === 'string') {
39
+ for (const css of splitLegacySelector(rawSelectors)) pushCss(css);
40
+ } else if (rawSelectors && typeof rawSelectors === 'object') {
41
+ if (typeof rawSelectors.css === 'string') {
42
+ pushCss(rawSelectors.css, rawSelectors);
43
+ } else if (typeof rawSelectors.selector === 'string') {
44
+ for (const css of splitLegacySelector(rawSelectors.selector)) pushCss(css, rawSelectors);
45
+ }
46
+ }
47
+
48
+ const dedup = new Map();
49
+ for (const item of out) {
50
+ const key = `${item.css}::${item.variant || ''}::${item.score || ''}`;
51
+ if (!dedup.has(key)) dedup.set(key, item);
52
+ }
53
+ return Array.from(dedup.values());
54
+ }
@@ -0,0 +1,118 @@
1
+ import WebSocket from 'ws';
2
+
3
+ export function createTransport({ env = process.env, defaults = {}, debugLog = null } = {}) {
4
+ const getBrowserWsUrl = () => {
5
+ if (env.CAMO_WS_URL) return env.CAMO_WS_URL;
6
+ const host = env.CAMO_WS_HOST || defaults.wsHost || '127.0.0.1';
7
+ const port = Number(env.CAMO_WS_PORT || defaults.wsPort || 8765);
8
+ return `ws://${host}:${port}`;
9
+ };
10
+
11
+ const getBrowserHttpBase = () => {
12
+ if (env.CAMO_BROWSER_HTTP_BASE) return env.CAMO_BROWSER_HTTP_BASE.replace(/\/$/, '');
13
+ const host = env.CAMO_BROWSER_HTTP_HOST || defaults.httpHost || '127.0.0.1';
14
+ const port = Number(env.CAMO_BROWSER_HTTP_PORT || defaults.httpPort || 7704);
15
+ const protocol = env.CAMO_BROWSER_HTTP_PROTO || defaults.httpProtocol || 'http';
16
+ return `${protocol}://${host}:${port}`;
17
+ };
18
+
19
+ const browserServiceCommand = async (action, args, options = {}) => {
20
+ const timeoutMs = typeof options.timeoutMs === 'number' && options.timeoutMs > 0
21
+ ? options.timeoutMs
22
+ : 20000;
23
+ const profileId = (args?.profileId || args?.profile || args?.sessionId || '').toString();
24
+ debugLog?.('browserServiceCommand:start', { action, profileId, timeoutMs });
25
+ const res = await fetch(`${getBrowserHttpBase()}/command`, {
26
+ method: 'POST',
27
+ headers: { 'Content-Type': 'application/json' },
28
+ body: JSON.stringify({ action, args }),
29
+ signal: AbortSignal.timeout ? AbortSignal.timeout(timeoutMs) : undefined,
30
+ });
31
+
32
+ const raw = await res.text();
33
+ let data = {};
34
+ try {
35
+ data = raw ? JSON.parse(raw) : {};
36
+ } catch {
37
+ data = { raw };
38
+ }
39
+
40
+ if (!res.ok) {
41
+ debugLog?.('browserServiceCommand:http_err', { action, profileId, status: res.status, raw: raw?.slice?.(0, 200) });
42
+ throw new Error(data?.error || data?.body?.error || `browser-service command "${action}" HTTP ${res.status}`);
43
+ }
44
+ if (data && data.ok === false) {
45
+ debugLog?.('browserServiceCommand:ok_false', { action, profileId, error: data.error });
46
+ throw new Error(data.error || `browser-service command "${action}" failed`);
47
+ }
48
+ if (data && data.error) {
49
+ debugLog?.('browserServiceCommand:body_err', { action, profileId, error: data.error });
50
+ throw new Error(data.error);
51
+ }
52
+ debugLog?.('browserServiceCommand:ok', { action, profileId });
53
+ return data.body ?? data;
54
+ };
55
+
56
+ const sendWsCommand = (wsUrl, payload, timeoutMs = 15000) => new Promise((resolve, reject) => {
57
+ const socket = new WebSocket(wsUrl);
58
+ let settled = false;
59
+ const timeout = setTimeout(() => {
60
+ if (settled) return;
61
+ settled = true;
62
+ socket.terminate();
63
+ reject(new Error('WebSocket command timeout'));
64
+ }, timeoutMs);
65
+
66
+ const cleanup = () => {
67
+ clearTimeout(timeout);
68
+ socket.removeAllListeners();
69
+ };
70
+
71
+ socket.once('open', () => {
72
+ try {
73
+ socket.send(JSON.stringify(payload));
74
+ } catch (err) {
75
+ cleanup();
76
+ if (!settled) {
77
+ settled = true;
78
+ reject(err);
79
+ }
80
+ }
81
+ });
82
+
83
+ socket.once('message', (data) => {
84
+ cleanup();
85
+ if (settled) return;
86
+ settled = true;
87
+ try {
88
+ resolve(JSON.parse(data.toString('utf-8')));
89
+ } catch (err) {
90
+ reject(err);
91
+ } finally {
92
+ socket.close();
93
+ }
94
+ });
95
+
96
+ socket.once('error', (err) => {
97
+ cleanup();
98
+ if (settled) return;
99
+ settled = true;
100
+ reject(err);
101
+ });
102
+
103
+ socket.once('close', () => {
104
+ cleanup();
105
+ if (!settled) {
106
+ settled = true;
107
+ reject(new Error('WebSocket closed before response'));
108
+ }
109
+ });
110
+ });
111
+
112
+ return {
113
+ getBrowserWsUrl,
114
+ getBrowserHttpBase,
115
+ browserServiceCommand,
116
+ sendWsCommand,
117
+ };
118
+ }
@@ -9,7 +9,7 @@ import { BROWSER_SERVICE_URL, loadConfig, setRepoRoot } from './config.mjs';
9
9
  import { touchSessionActivity } from '../lifecycle/session-registry.mjs';
10
10
 
11
11
  const require = createRequire(import.meta.url);
12
- const DEFAULT_API_TIMEOUT_MS = 30000;
12
+ const DEFAULT_API_TIMEOUT_MS = 90000;
13
13
 
14
14
  function resolveApiTimeoutMs(options = {}) {
15
15
  const optionValue = Number(options?.timeoutMs);
@@ -23,6 +23,78 @@ function resolveApiTimeoutMs(options = {}) {
23
23
  return DEFAULT_API_TIMEOUT_MS;
24
24
  }
25
25
 
26
+ function resolveWsUrl() {
27
+ const cfg = loadConfig();
28
+ const explicit = String(process.env.CAMO_WS_URL || '').trim();
29
+ if (explicit) return explicit;
30
+ const host = String(process.env.CAMO_WS_HOST || '127.0.0.1').trim() || '127.0.0.1';
31
+ const port = Number(process.env.CAMO_WS_PORT || 8765) || 8765;
32
+ return `ws://${host}:${port}`;
33
+ }
34
+
35
+ async function openWs() {
36
+ if (typeof WebSocket !== 'function') {
37
+ throw new Error('Global WebSocket is unavailable in this Node runtime');
38
+ }
39
+ const wsUrl = resolveWsUrl();
40
+ return new Promise((resolve, reject) => {
41
+ const socket = new WebSocket(wsUrl);
42
+ const timer = setTimeout(() => {
43
+ try { socket.close(); } catch {}
44
+ reject(new Error(`WebSocket connect timeout: ${wsUrl}`));
45
+ }, 8000);
46
+ socket.addEventListener('open', () => {
47
+ clearTimeout(timer);
48
+ resolve(socket);
49
+ });
50
+ socket.addEventListener('error', (err) => {
51
+ clearTimeout(timer);
52
+ reject(new Error(`WebSocket connect failed: ${err?.message || String(err)}`));
53
+ });
54
+ });
55
+ }
56
+
57
+ export async function callWS(action, payload = {}, options = {}) {
58
+ const timeoutMs = resolveApiTimeoutMs(options);
59
+ const socket = await openWs();
60
+ const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
61
+ const sessionId = String(payload?.profileId || payload?.sessionId || payload?.profile || '').trim();
62
+ const message = {
63
+ type: 'command',
64
+ request_id: requestId,
65
+ session_id: sessionId,
66
+ data: { command_type: 'dev_command', action, parameters: payload },
67
+ };
68
+
69
+ return new Promise((resolve, reject) => {
70
+ const timer = setTimeout(() => {
71
+ try { socket.close(); } catch {}
72
+ reject(new Error(`browser-service ws timeout after ${timeoutMs}ms: ${action}`));
73
+ }, timeoutMs);
74
+
75
+ socket.addEventListener('message', (event) => {
76
+ try {
77
+ const data = typeof event.data === 'string' ? JSON.parse(event.data) : JSON.parse(String(event.data));
78
+ if (data?.type === 'response' && data.request_id === requestId) {
79
+ clearTimeout(timer);
80
+ try { socket.close(); } catch {}
81
+ resolve(data?.data ?? data);
82
+ }
83
+ } catch (err) {
84
+ clearTimeout(timer);
85
+ try { socket.close(); } catch {}
86
+ reject(err);
87
+ }
88
+ });
89
+
90
+ socket.send(JSON.stringify(message));
91
+ });
92
+ }
93
+
94
+ export function findRepoRootCandidate() {
95
+ return null;
96
+ }
97
+
26
98
  function isTimeoutError(error) {
27
99
  const name = String(error?.name || '').toLowerCase();
28
100
  const message = String(error?.message || '').toLowerCase();
@@ -342,64 +414,17 @@ export function ensureCamoufox() {
342
414
  console.log('Camoufox installed.');
343
415
  }
344
416
 
345
- const START_SCRIPT_REL = path.join('runtime', 'infra', 'utils', 'scripts', 'service', 'start-browser-service.mjs');
346
- const CONTROLLER_SERVER_REL = path.join('services', 'controller', 'src', 'server.mjs');
417
+ const CONTROLLER_SERVER_REL = path.join('src', 'services', 'browser-service', 'index.js');
347
418
 
348
- function hasStartScript(root) {
349
- if (!root) return false;
350
- return fs.existsSync(path.join(root, START_SCRIPT_REL));
351
- }
352
419
 
353
420
  function hasControllerServer(root) {
354
421
  if (!root) return false;
355
422
  return fs.existsSync(path.join(root, CONTROLLER_SERVER_REL));
356
423
  }
357
424
 
358
- function walkUpForRepoRoot(startDir) {
359
- if (!startDir) return null;
360
- let cursor = path.resolve(startDir);
361
- for (;;) {
362
- if (hasStartScript(cursor)) return cursor;
363
- const parent = path.dirname(cursor);
364
- if (parent === cursor) return null;
365
- cursor = parent;
366
- }
367
- }
368
425
 
369
- function scanCommonRepoRoots() {
370
- const home = os.homedir();
371
- const roots = [
372
- path.join(home, 'Documents', 'github'),
373
- path.join(home, 'github'),
374
- path.join(home, 'code'),
375
- path.join(home, 'projects'),
376
- path.join('/Volumes', 'extension', 'code'),
377
- path.join('C:', 'code'),
378
- path.join('D:', 'code'),
379
- path.join('C:', 'projects'),
380
- path.join('D:', 'projects'),
381
- path.join('C:', 'Users', os.userInfo().username, 'code'),
382
- path.join('C:', 'Users', os.userInfo().username, 'projects'),
383
- path.join('C:', 'Users', os.userInfo().username, 'Documents', 'github'),
384
- ].filter(Boolean);
385
426
 
386
- for (const root of roots) {
387
- if (!fs.existsSync(root)) continue;
388
- try {
389
- const entries = fs.readdirSync(root, { withFileTypes: true });
390
- for (const entry of entries) {
391
- if (!entry.isDirectory()) continue;
392
- if (!entry.name.toLowerCase().includes('webauto')) continue;
393
- const candidate = path.join(root, entry.name);
394
- if (hasStartScript(candidate)) return candidate;
395
- }
396
- } catch {
397
- // ignore scanning errors and continue
398
- }
399
- }
400
427
 
401
- return null;
402
- }
403
428
 
404
429
  function scanCommonInstallRoots() {
405
430
  const home = os.homedir();
@@ -416,75 +441,32 @@ function scanCommonInstallRoots() {
416
441
  ].filter(Boolean);
417
442
 
418
443
  for (const root of nodeModuleRoots) {
419
- const candidate = path.join(root, '@web-auto', 'webauto');
444
+ const candidate = path.join(root, '@web-auto', 'camo');
420
445
  if (hasControllerServer(candidate)) return candidate;
421
446
  }
422
447
  return null;
423
448
  }
424
449
 
425
- export function findRepoRootCandidate() {
426
- const cfg = loadConfig();
427
- const candidates = [
428
- process.env.WEBAUTO_REPO_ROOT,
429
- cfg.repoRoot,
430
- process.cwd(),
431
- path.join('/Volumes', 'extension', 'code', 'webauto'),
432
- path.join('/Volumes', 'extension', 'code', 'WebAuto'),
433
- path.join(os.homedir(), 'Documents', 'github', 'webauto'),
434
- path.join(os.homedir(), 'Documents', 'github', 'WebAuto'),
435
- path.join(os.homedir(), 'github', 'webauto'),
436
- path.join(os.homedir(), 'github', 'WebAuto'),
437
- path.join('C:', 'code', 'webauto'),
438
- path.join('C:', 'code', 'WebAuto'),
439
- path.join('C:', 'Users', os.userInfo().username, 'code', 'webauto'),
440
- path.join('C:', 'Users', os.userInfo().username, 'code', 'WebAuto'),
441
- ].filter(Boolean);
442
450
 
443
- for (const root of candidates) {
444
- if (hasStartScript(root)) {
445
- if (cfg.repoRoot !== root) {
446
- setRepoRoot(root);
447
- }
448
- return root;
449
- }
450
- }
451
451
 
452
- for (const startDir of [process.cwd()]) {
453
- const found = walkUpForRepoRoot(startDir);
454
- if (found) {
455
- if (cfg.repoRoot !== found) {
456
- setRepoRoot(found);
457
- }
458
- return found;
459
- }
460
- }
461
452
 
462
- const scanned = scanCommonRepoRoots();
463
- if (scanned) {
464
- if (cfg.repoRoot !== scanned) {
465
- setRepoRoot(scanned);
466
- }
467
- return scanned;
468
- }
469
453
 
470
- return null;
471
- }
472
454
 
473
455
  export function findInstallRootCandidate() {
474
456
  const cfg = loadConfig();
475
457
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
476
- const siblingInScopedNodeModules = path.resolve(currentDir, '..', '..', '..', 'webauto');
458
+ const siblingInScopedNodeModules = path.resolve(currentDir, '..', '..', '..', 'camo');
477
459
  const candidates = [
478
- process.env.WEBAUTO_INSTALL_DIR,
479
- process.env.WEBAUTO_PACKAGE_ROOT,
480
- process.env.WEBAUTO_REPO_ROOT,
460
+ process.env.CAMO_INSTALL_DIR,
461
+ process.env.CAMO_PACKAGE_ROOT,
462
+ process.env.CAMO_REPO_ROOT,
481
463
  cfg.repoRoot,
482
464
  siblingInScopedNodeModules,
483
465
  process.cwd(),
484
466
  ].filter(Boolean);
485
467
 
486
468
  try {
487
- const pkgPath = require.resolve('@web-auto/webauto/package.json');
469
+ const pkgPath = require.resolve('@web-auto/camo/package.json');
488
470
  candidates.push(path.dirname(pkgPath));
489
471
  } catch {
490
472
  // ignore resolution failures in npx-only environments
@@ -504,34 +486,27 @@ export function findInstallRootCandidate() {
504
486
  export async function ensureBrowserService() {
505
487
  if (await checkBrowserService()) return;
506
488
 
507
- const repoRoot = findRepoRootCandidate();
508
- if (repoRoot) {
509
- const scriptPath = path.join(repoRoot, START_SCRIPT_REL);
510
- console.log('Starting browser-service daemon...');
511
- execSync(`node "${scriptPath}"`, { stdio: 'inherit', cwd: repoRoot });
512
- } else {
513
- const installRoot = findInstallRootCandidate();
514
- if (!installRoot) {
515
- throw new Error(
516
- `Cannot locate browser-service launcher (${START_SCRIPT_REL} or ${CONTROLLER_SERVER_REL}). ` +
517
- 'Set WEBAUTO_INSTALL_DIR=<@web-auto/webauto install dir> or WEBAUTO_REPO_ROOT=<repo root>.',
518
- );
519
- }
520
- const scriptPath = path.join(installRoot, CONTROLLER_SERVER_REL);
521
- const env = {
522
- ...process.env,
523
- WEBAUTO_REPO_ROOT: String(process.env.WEBAUTO_REPO_ROOT || '').trim() || installRoot,
524
- };
525
- const child = spawn(process.execPath, [scriptPath], {
526
- cwd: installRoot,
527
- detached: true,
528
- stdio: 'ignore',
529
- windowsHide: true,
530
- env,
531
- });
532
- child.unref();
533
- console.log(`Starting browser-service daemon (packaged webauto, pid=${child.pid || 'unknown'})...`);
489
+ const installRoot = findInstallRootCandidate();
490
+ if (!installRoot) {
491
+ throw new Error(
492
+ `Cannot locate browser-service launcher (${CONTROLLER_SERVER_REL}). ` +
493
+ 'Ensure @web-auto/camo is installed or set CAMO_INSTALL_DIR.',
494
+ );
534
495
  }
496
+ const scriptPath = path.join(installRoot, CONTROLLER_SERVER_REL);
497
+ const env = {
498
+ ...process.env,
499
+ CAMO_REPO_ROOT: String(process.env.CAMO_REPO_ROOT || '').trim() || installRoot,
500
+ };
501
+ const child = spawn(process.execPath, [scriptPath], {
502
+ cwd: installRoot,
503
+ detached: true,
504
+ stdio: 'ignore',
505
+ windowsHide: true,
506
+ env,
507
+ });
508
+ child.unref();
509
+ console.log(`Starting browser-service daemon (pid=${child.pid || 'unknown'})...`);
535
510
 
536
511
  for (let i = 0; i < 20; i += 1) {
537
512
  await new Promise((r) => setTimeout(r, 400));
@@ -19,50 +19,58 @@ function normalizePathForPlatform(input, platform = process.platform) {
19
19
  return isWinPath ? pathApi.normalize(raw) : path.resolve(raw);
20
20
  }
21
21
 
22
- function normalizeLegacyWebautoRoot(input, platform = process.platform) {
22
+ function normalizeLegacyCamoRoot(input, platform = process.platform) {
23
23
  const pathApi = platform === 'win32' ? path.win32 : path;
24
24
  const resolved = normalizePathForPlatform(input, platform);
25
25
  const base = pathApi.basename(resolved).toLowerCase();
26
- if (base === '.webauto' || base === 'webauto') return resolved;
27
- return pathApi.join(resolved, '.webauto');
26
+ if (base === '.camo' || base === 'camo') return resolved;
27
+ return pathApi.join(resolved, '.camo');
28
28
  }
29
29
 
30
- export function resolveWebautoRoot(options = {}) {
30
+ export function resolveCamoRoot(options = {}) {
31
31
  const env = options.env || process.env;
32
32
  const platform = String(options.platform || process.platform);
33
33
  const pathApi = platform === 'win32' ? path.win32 : path;
34
34
  const homeDir = String(options.homeDir || os.homedir());
35
- const explicitDataRoot = String(env.WEBAUTO_DATA_ROOT || env.WEBAUTO_HOME || '').trim();
35
+ const explicitDataRoot = String(env.CAMO_DATA_ROOT || env.CAMO_HOME || '').trim();
36
36
  if (explicitDataRoot) return normalizePathForPlatform(explicitDataRoot, platform);
37
37
 
38
- const legacyRoot = String(env.WEBAUTO_ROOT || env.WEBAUTO_PORTABLE_ROOT || '').trim();
39
- if (legacyRoot) return normalizeLegacyWebautoRoot(legacyRoot, platform);
38
+ const legacyRoot = String(env.CAMO_ROOT || env.CAMO_PORTABLE_ROOT || '').trim();
39
+ if (legacyRoot) return normalizeLegacyCamoRoot(legacyRoot, platform);
40
40
 
41
41
  const dDriveExists = typeof options.hasDDrive === 'boolean'
42
42
  ? options.hasDDrive
43
43
  : hasDrive('D');
44
44
  if (platform === 'win32') {
45
- return dDriveExists ? 'D:\\webauto' : pathApi.join(homeDir, '.webauto');
45
+ return dDriveExists ? 'D:\\camo' : pathApi.join(homeDir, '.camo');
46
46
  }
47
- return pathApi.join(homeDir, '.webauto');
47
+ return pathApi.join(homeDir, '.camo');
48
+ }
49
+
50
+ // Backward-compatible export name; camo no longer uses legacy paths.
51
+ export function resolveLegacyRoot(options = {}) {
52
+ return resolveCamoRoot(options);
48
53
  }
49
54
 
50
55
  export function resolveProfilesDir(options = {}) {
51
56
  const env = options.env || process.env;
52
57
  const platform = String(options.platform || process.platform);
53
- const explicitProfileRoot = String(env.WEBAUTO_PROFILE_ROOT || '').trim();
58
+ const explicitProfileRoot = String(env.CAMO_PROFILE_ROOT || env.CAMO_PATHS_PROFILES || '').trim();
54
59
  if (explicitProfileRoot) {
55
60
  return normalizePathForPlatform(explicitProfileRoot, platform);
56
61
  }
57
62
  const pathApi = platform === 'win32' ? path.win32 : path;
58
- return pathApi.join(resolveWebautoRoot(options), 'profiles');
63
+ return pathApi.join(resolveCamoRoot(options), 'profiles');
59
64
  }
60
65
 
61
- export const CONFIG_DIR = resolveWebautoRoot();
66
+ export const CONFIG_DIR = resolveCamoRoot();
62
67
  export const PROFILES_DIR = resolveProfilesDir();
63
68
  export const CONFIG_FILE = path.join(CONFIG_DIR, 'camo-cli.json');
64
69
  export const PROFILE_META_FILE = 'camo-profile.json';
65
- export const BROWSER_SERVICE_URL = process.env.WEBAUTO_BROWSER_URL || 'http://127.0.0.1:7704';
70
+ export const BROWSER_SERVICE_URL = process.env.CAMO_BROWSER_URL
71
+ || process.env.CAMO_BROWSER_HTTP_URL
72
+ || process.env.CAMO_BROWSER_HOST
73
+ || 'http://127.0.0.1:7704';
66
74
 
67
75
  export function ensureDir(p) {
68
76
  if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
@@ -202,12 +210,5 @@ export function setProfileWindowSize(profileId, width, height) {
202
210
  height: Math.floor(parsedHeight),
203
211
  updatedAt: now,
204
212
  },
205
- });
206
- }
207
-
208
- const START_SCRIPT_REL = path.join('runtime', 'infra', 'utils', 'scripts', 'service', 'start-browser-service.mjs');
209
-
210
- export function hasStartScript(root) {
211
- if (!root) return false;
212
- return fs.existsSync(path.join(root, START_SCRIPT_REL));
213
+ });
213
214
  }
@@ -20,8 +20,9 @@ INITIALIZATION:
20
20
  create profile <profileId> Create a new profile
21
21
 
22
22
  CONFIG:
23
- config repo-root [path] Get or set persisted webauto repo root
23
+ config repo-root [path] Get or set persisted camo repo root
24
24
  highlight-mode [status|on|off] Global highlight mode for click/type/scroll (default: on)
25
+ attach <profileId|sessionId> Attach to session via WS; read JSON commands from stdin
25
26
 
26
27
  BROWSER CONTROL:
27
28
  init Ensure camoufox + ensure browser-service daemon
@@ -87,7 +88,6 @@ WINDOW:
87
88
  window resize [profileId] --width <w> --height <h> Resize browser window
88
89
 
89
90
  MOUSE:
90
- mouse move [profileId] --x <x> --y <y> [--steps <n>] Move mouse to coordinates
91
91
  mouse click [profileId] --x <x> --y <y> [--button left|right|middle] [--clicks <n>] [--delay <ms>] Click at coordinates
92
92
  mouse wheel [profileId] [--deltax <px>] [--deltay <px>] Scroll wheel
93
93
 
@@ -134,7 +134,6 @@ EXAMPLES:
134
134
  camo cookies auto start --interval 5000
135
135
  camo window move --x 100 --y 100
136
136
  camo window resize --width 1920 --height 1080
137
- camo mouse move --x 500 --y 300
138
137
  camo mouse click --x 500 --y 300 --button left
139
138
  camo mouse wheel --deltay -300
140
139
  camo system display
@@ -145,6 +144,7 @@ EXAMPLES:
145
144
  camo lock list
146
145
  camo unlock myprofile
147
146
  camo stop
147
+ echo '{"action":"stop","args":{"profileId":"myprofile"}}' | camo attach myprofile
148
148
 
149
149
  CONTAINER FILTER & SUBSCRIPTION:
150
150
  container init [--source <dir>] [--force] Initialize subscription dir + migrate container sets
@@ -172,12 +172,14 @@ PROGRESS EVENTS:
172
172
  (non-events commands auto-start daemon by default)
173
173
 
174
174
  ENV:
175
- WEBAUTO_BROWSER_URL Default: http://127.0.0.1:7704
176
- WEBAUTO_INSTALL_DIR Optional @web-auto/webauto install dir
177
- WEBAUTO_REPO_ROOT Optional webauto repo root (dev mode)
178
- WEBAUTO_DATA_ROOT / WEBAUTO_HOME Optional data root (Windows default D:\\webauto)
179
- WEBAUTO_PROFILE_ROOT Optional profile dir override
180
- WEBAUTO_ROOT Legacy data root (auto-appends .webauto if needed)
175
+ CAMO_BROWSER_URL Default: http://127.0.0.1:7704
176
+ CAMO_INSTALL_DIR Optional @web-auto/camo install dir
177
+ CAMO_REPO_ROOT Optional camo repo root (dev mode)
178
+ CAMO_DATA_ROOT / CAMO_HOME Optional data root (Windows default D:\\camo)
179
+ CAMO_PROFILE_ROOT Optional profile dir override
180
+ CAMO_ROOT Legacy data root (auto-appends .camo if needed)
181
+ CAMO_WS_URL Optional ws://host:port override
182
+ CAMO_WS_HOST / CAMO_WS_PORT WS host/port for browser-service
181
183
  CAMO_PROGRESS_EVENTS_FILE Optional path override for progress jsonl
182
184
  CAMO_PROGRESS_WS_HOST / CAMO_PROGRESS_WS_PORT Progress daemon host/port (defaults: 127.0.0.1:7788)
183
185
  `);
@@ -0,0 +1,30 @@
1
+ export function resolveWsUrl() {
2
+ const explicit = String(process.env.CAMO_WS_URL || '').trim();
3
+ if (explicit) return explicit;
4
+ const host = String(process.env.CAMO_WS_HOST || '127.0.0.1').trim() || '127.0.0.1';
5
+ const port = Number(process.env.CAMO_WS_PORT || 8765) || 8765;
6
+ return `ws://${host}:${port}`;
7
+ }
8
+
9
+ export async function ensureWsClient(wsUrl = resolveWsUrl()) {
10
+ if (typeof WebSocket !== 'function') {
11
+ throw new Error('Global WebSocket is unavailable in this Node runtime');
12
+ }
13
+ return new Promise((resolve, reject) => {
14
+ const socket = new WebSocket(wsUrl);
15
+ const timer = setTimeout(() => {
16
+ try { socket.close(); } catch {}
17
+ reject(new Error(`WebSocket connect timeout: ${wsUrl}`));
18
+ }, 8000);
19
+
20
+ socket.addEventListener('open', () => {
21
+ clearTimeout(timer);
22
+ resolve(socket);
23
+ });
24
+
25
+ socket.addEventListener('error', (err) => {
26
+ clearTimeout(timer);
27
+ reject(new Error(`WebSocket connect failed: ${err?.message || String(err)}`));
28
+ });
29
+ });
30
+ }