@monoes/monobrowse 2.24.0 → 2.24.1

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 (55) hide show
  1. package/dist/src/__tests__/browse-concurrency.test.js +33 -3
  2. package/dist/src/__tests__/browse-concurrency.test.js.map +1 -1
  3. package/dist/src/__tests__/cdp-port.test.d.ts +2 -0
  4. package/dist/src/__tests__/cdp-port.test.d.ts.map +1 -0
  5. package/dist/src/__tests__/cdp-port.test.js +61 -0
  6. package/dist/src/__tests__/cdp-port.test.js.map +1 -0
  7. package/dist/src/__tests__/profile-dir.test.js +10 -1
  8. package/dist/src/__tests__/profile-dir.test.js.map +1 -1
  9. package/dist/src/browser/bridge.d.ts +16 -4
  10. package/dist/src/browser/bridge.d.ts.map +1 -1
  11. package/dist/src/browser/bridge.js +65 -12
  12. package/dist/src/browser/bridge.js.map +1 -1
  13. package/dist/src/browser/browser-launch.d.ts.map +1 -1
  14. package/dist/src/browser/browser-launch.js +4 -3
  15. package/dist/src/browser/browser-launch.js.map +1 -1
  16. package/dist/src/browser/cdp-port.d.ts +19 -0
  17. package/dist/src/browser/cdp-port.d.ts.map +1 -0
  18. package/dist/src/browser/cdp-port.js +26 -0
  19. package/dist/src/browser/cdp-port.js.map +1 -0
  20. package/dist/src/browser/cdp.d.ts.map +1 -1
  21. package/dist/src/browser/cdp.js +2 -1
  22. package/dist/src/browser/cdp.js.map +1 -1
  23. package/dist/src/browser/index.d.ts +1 -0
  24. package/dist/src/browser/index.d.ts.map +1 -1
  25. package/dist/src/browser/index.js +1 -0
  26. package/dist/src/browser/index.js.map +1 -1
  27. package/dist/src/browser/profile-dir.d.ts +8 -0
  28. package/dist/src/browser/profile-dir.d.ts.map +1 -1
  29. package/dist/src/browser/profile-dir.js +19 -0
  30. package/dist/src/browser/profile-dir.js.map +1 -1
  31. package/dist/src/cli/action.d.ts.map +1 -1
  32. package/dist/src/cli/action.js +3 -2
  33. package/dist/src/cli/action.js.map +1 -1
  34. package/dist/src/cli/commands-session.d.ts.map +1 -1
  35. package/dist/src/cli/commands-session.js +12 -5
  36. package/dist/src/cli/commands-session.js.map +1 -1
  37. package/dist/src/cli/platform.d.ts.map +1 -1
  38. package/dist/src/cli/platform.js +2 -1
  39. package/dist/src/cli/platform.js.map +1 -1
  40. package/dist/src/cli.js +0 -0
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +13 -8
  43. package/src/__tests__/browse-concurrency.test.ts +36 -3
  44. package/src/__tests__/cdp-port.test.ts +76 -0
  45. package/src/__tests__/profile-dir.test.ts +19 -0
  46. package/src/browser/bridge.ts +68 -17
  47. package/src/browser/browser-launch.ts +4 -2
  48. package/src/browser/cdp-port.ts +26 -0
  49. package/src/browser/cdp.ts +5 -1
  50. package/src/browser/index.ts +1 -0
  51. package/src/browser/profile-dir.ts +19 -0
  52. package/src/cli/action.ts +3 -2
  53. package/src/cli/commands-session.ts +12 -5
  54. package/src/cli/platform.ts +2 -1
  55. package/LICENSE +0 -191
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monobrowse",
3
- "version": "2.24.0",
3
+ "version": "2.24.1",
4
4
  "description": "Native browser automation via Chrome DevTools Protocol — the engine powering monomind browse",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,6 +13,10 @@
13
13
  "types": "./dist/src/index.d.ts",
14
14
  "import": "./dist/src/index.js"
15
15
  },
16
+ "./cdp-port": {
17
+ "types": "./dist/src/browser/cdp-port.d.ts",
18
+ "import": "./dist/src/browser/cdp-port.js"
19
+ },
16
20
  "./cli": {
17
21
  "types": "./dist/src/cli.d.ts",
18
22
  "import": "./dist/src/cli.js"
@@ -33,6 +37,13 @@
33
37
  "engines": {
34
38
  "node": ">=22.12.0"
35
39
  },
40
+ "scripts": {
41
+ "build": "tsc && node ../../../scripts/build-fs.mjs copy-into dist/src/browser/dashboard src/browser/dashboard/ui.html",
42
+ "test": "vitest run",
43
+ "test:coverage": "vitest run --coverage",
44
+ "prebuild": "node ../../../scripts/build-fs.mjs clean dist tsconfig.tsbuildinfo",
45
+ "prepublishOnly": "npm run build"
46
+ },
36
47
  "keywords": [
37
48
  "browser-automation",
38
49
  "cdp",
@@ -59,11 +70,5 @@
59
70
  "devDependencies": {
60
71
  "typescript": "^7.0.2",
61
72
  "vitest": "^4.1.4"
62
- },
63
- "scripts": {
64
- "build": "tsc && node ../../../scripts/build-fs.mjs copy-into dist/src/browser/dashboard src/browser/dashboard/ui.html",
65
- "test": "vitest run",
66
- "test:coverage": "vitest run --coverage",
67
- "prebuild": "node ../../../scripts/build-fs.mjs clean dist tsconfig.tsbuildinfo"
68
73
  }
69
- }
74
+ }
@@ -13,9 +13,9 @@
13
13
  */
14
14
 
15
15
  import { execFile, execFileSync, spawn } from 'node:child_process';
16
- import { existsSync } from 'node:fs';
16
+ import { existsSync, readFileSync } from 'node:fs';
17
17
  import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
18
- import { tmpdir } from 'node:os';
18
+ import { freemem, tmpdir } from 'node:os';
19
19
  import { dirname, join } from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
21
  import { afterEach, beforeAll, describe, expect, it } from 'vitest';
@@ -38,7 +38,40 @@ function chromeAvailable(): boolean {
38
38
  }
39
39
  }
40
40
 
41
- const runnable = existsSync(CLI) && chromeAvailable();
41
+ /**
42
+ * #651: under host memory pressure the kernel/Chrome aborts a launching Chrome
43
+ * (SIGABRT), which looks like a code failure. Real Chromes alive at once: the
44
+ * named-session test holds three (ref, build, and the bare command's own); the
45
+ * rest hold two. Each headless Chrome (browser + GPU/network/renderer
46
+ * processes) needs on the order of 400 MiB, rounded up to 512 MiB with
47
+ * headroom for the warm-up Chrome and the test runner itself.
48
+ */
49
+ const MAX_CONCURRENT_CHROMES = 3;
50
+ const BYTES_PER_CHROME = 512 * 1024 * 1024;
51
+ const REQUIRED_AVAILABLE_BYTES = MAX_CONCURRENT_CHROMES * BYTES_PER_CHROME;
52
+
53
+ /** Bytes the kernel reports as available without swapping (MemAvailable). */
54
+ function availableMemoryBytes(): number {
55
+ try {
56
+ const m = /^MemAvailable:\s+(\d+) kB/m.exec(readFileSync('/proc/meminfo', 'utf8'));
57
+ if (m) return Number(m[1]) * 1024;
58
+ } catch {
59
+ // not Linux: fall through to os.freemem()
60
+ }
61
+ return freemem();
62
+ }
63
+
64
+ const availableBytes = availableMemoryBytes();
65
+ const enoughMemory = availableBytes >= REQUIRED_AVAILABLE_BYTES;
66
+ const runnable = existsSync(CLI) && chromeAvailable() && enoughMemory;
67
+
68
+ if (existsSync(CLI) && !enoughMemory) {
69
+ const mib = (n: number) => Math.round(n / 1024 / 1024);
70
+ console.warn(
71
+ `SKIP browse-concurrency: ${mib(availableBytes)} MiB available, ` +
72
+ `${mib(REQUIRED_AVAILABLE_BYTES)} MiB needed for ${MAX_CONCURRENT_CHROMES} concurrent Chromes (#651)`,
73
+ );
74
+ }
42
75
 
43
76
  interface Run {
44
77
  code: number | null;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * #666: monomind's own Chrome must not default to 9222, which mono-agent's
3
+ * extension bridge owns. Covers the shared port constant and its env aliases,
4
+ * and the bridge's fall-through from a dead relay endpoint to a live one.
5
+ */
6
+ import type { AddressInfo } from 'node:net';
7
+ import { afterEach, describe, expect, it } from 'vitest';
8
+ import { WebSocketServer } from 'ws';
9
+ import { BridgeTransport } from '../browser/bridge.js';
10
+ import { CDP_PROBE_PORTS, DEFAULT_CDP_PORT, resolveCdpPort } from '../browser/cdp-port.js';
11
+
12
+ describe('resolveCdpPort', () => {
13
+ it('defaults to 9422, not the bridge port', () => {
14
+ expect(DEFAULT_CDP_PORT).toBe(9422);
15
+ expect(resolveCdpPort({})).toBe(9422);
16
+ });
17
+
18
+ it('prefers MONOBROWSE_CDP_PORT over the deprecated aliases', () => {
19
+ expect(
20
+ resolveCdpPort({
21
+ MONOBROWSE_CDP_PORT: '9500',
22
+ MONOBROWSE_PORT: '9501',
23
+ MONOMIND_CDP_PORT: '9502',
24
+ }),
25
+ ).toBe(9500);
26
+ expect(resolveCdpPort({ MONOBROWSE_PORT: '9501', MONOMIND_CDP_PORT: '9502' })).toBe(9501);
27
+ expect(resolveCdpPort({ MONOMIND_CDP_PORT: '9502' })).toBe(9502);
28
+ });
29
+
30
+ it('ignores a non-numeric value', () => {
31
+ expect(resolveCdpPort({ MONOBROWSE_CDP_PORT: 'nope' })).toBe(9422);
32
+ });
33
+
34
+ it('probes 9422 first and keeps 9222 so a user-launched Chrome is still found', () => {
35
+ expect(CDP_PROBE_PORTS).toEqual([9422, 9222, 9229]);
36
+ });
37
+ });
38
+
39
+ describe('BridgeTransport endpoint fall-through', () => {
40
+ let server: WebSocketServer | undefined;
41
+ const saved = process.env.MONOAGENT_BRIDGE_URL;
42
+
43
+ afterEach(async () => {
44
+ if (saved === undefined) delete process.env.MONOAGENT_BRIDGE_URL;
45
+ else process.env.MONOAGENT_BRIDGE_URL = saved;
46
+ if (server) await new Promise((r) => server?.close(r));
47
+ server = undefined;
48
+ });
49
+
50
+ it('moves past a refused endpoint to the next one', async () => {
51
+ server = new WebSocketServer({ port: 0, host: '127.0.0.1' });
52
+ await new Promise((r) => server?.once('listening', r));
53
+ const live = (server.address() as AddressInfo).port;
54
+ server.on('connection', (ws) => {
55
+ ws.on('message', (raw) => {
56
+ const env = JSON.parse(String(raw));
57
+ ws.send(JSON.stringify({ id: env.id, success: true, data: { tabId: 7 } }));
58
+ });
59
+ });
60
+ // Port 1 refuses connections; the live relay is second.
61
+ process.env.MONOAGENT_BRIDGE_URL = `ws://127.0.0.1:1/monoagent/cdp, ws://127.0.0.1:${live}/monoagent/cdp`;
62
+
63
+ const transport = new BridgeTransport({});
64
+ await transport.open({ message: () => {}, close: () => {}, error: () => {} });
65
+ expect(transport.attachedTabId).toBe(7);
66
+ transport.close();
67
+ });
68
+
69
+ it('rejects when every endpoint is down', async () => {
70
+ process.env.MONOAGENT_BRIDGE_URL = 'ws://127.0.0.1:1/monoagent/cdp';
71
+ const transport = new BridgeTransport({});
72
+ await expect(
73
+ transport.open({ message: () => {}, close: () => {}, error: () => {} }),
74
+ ).rejects.toThrow();
75
+ });
76
+ });
@@ -14,6 +14,7 @@ import { tmpdir } from 'node:os';
14
14
  import { join } from 'node:path';
15
15
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
16
16
  import {
17
+ chromeSpawnEnv,
17
18
  isOwnedProfileDir,
18
19
  isProfileDirInUse,
19
20
  profileDirOwnerPid,
@@ -194,3 +195,21 @@ describe('#395 isOwnedProfileDir / removeOwnedProfileDir', () => {
194
195
  15_000,
195
196
  );
196
197
  });
198
+
199
+ describe('chromeSpawnEnv (#663)', () => {
200
+ const env = { TMPDIR: '/x', KEEP: '1' };
201
+
202
+ it.skipIf(process.platform === 'win32')(
203
+ 'leaves the env alone when the singleton socket path fits',
204
+ () => {
205
+ expect(chromeSpawnEnv(env, '/tmp/mb-abcdef')).toBe(env);
206
+ },
207
+ );
208
+
209
+ it.skipIf(process.platform === 'win32')(
210
+ 'redirects TMPDIR to /tmp when it would overflow the socket path',
211
+ () => {
212
+ expect(chromeSpawnEnv(env, `/tmp/${'x'.repeat(80)}`)).toEqual({ TMPDIR: '/tmp', KEEP: '1' });
213
+ },
214
+ );
215
+ });
@@ -38,8 +38,18 @@ import { WebSocketTransport } from './transport.js';
38
38
  /** Header the Go relay authenticates with; mirrors internal/extension/token.go. */
39
39
  export const BRIDGE_TOKEN_HEADER = 'X-Monoagent-Extension-Token';
40
40
 
41
- /** Default relay endpoint; mirrors the extension server's default port. */
42
- export const DEFAULT_BRIDGE_URL = 'ws://127.0.0.1:9222/monoagent/cdp';
41
+ /**
42
+ * Relay endpoints tried in order: the extension server's default port, then
43
+ * the one mono-agent falls back to when 9222 is held. MONOAGENT_BRIDGE_URL
44
+ * (or the `url` option; comma-separated for several) overrides the list.
45
+ */
46
+ export const DEFAULT_BRIDGE_URLS: readonly string[] = [
47
+ 'ws://127.0.0.1:9222/monoagent/cdp',
48
+ 'ws://127.0.0.1:9323/monoagent/cdp',
49
+ ];
50
+
51
+ /** The first default relay endpoint. */
52
+ export const DEFAULT_BRIDGE_URL = DEFAULT_BRIDGE_URLS[0];
43
53
 
44
54
  /** How long the attach handshake may take before open() gives up. */
45
55
  const DEFAULT_ATTACH_TIMEOUT_MS = 15_000;
@@ -67,7 +77,7 @@ export interface BridgeTransportOptions {
67
77
  * to hand, only a browser with the user's session in it.
68
78
  */
69
79
  tabId?: number;
70
- /** Relay endpoint. Defaults to DEFAULT_BRIDGE_URL. */
80
+ /** Relay endpoint. Defaults to MONOAGENT_BRIDGE_URL, else DEFAULT_BRIDGE_URLS in order. */
71
81
  url?: string;
72
82
  /** Relay token, from ~/.monoagent (the Go side writes it). */
73
83
  token?: string;
@@ -114,7 +124,10 @@ interface OutboundCdp {
114
124
  export class BridgeTransport implements CdpTransport {
115
125
  /** 0 until the extension resolves the active tab (see open()). */
116
126
  private tabId: number;
117
- private readonly socket: CdpTransport;
127
+ private socket: CdpTransport | undefined;
128
+ /** Endpoints open() tries in order; empty when a socket was injected. */
129
+ private readonly urls: readonly string[];
130
+ private readonly headers: Record<string, string> | undefined;
118
131
  private readonly attachTimeoutMs: number;
119
132
  private handlers: CdpTransportHandlers | null = null;
120
133
  private nextEnvelopeId = 1;
@@ -124,24 +137,62 @@ export class BridgeTransport implements CdpTransport {
124
137
  constructor(opts: BridgeTransportOptions) {
125
138
  this.tabId = opts.tabId ?? 0;
126
139
  this.attachTimeoutMs = opts.attachTimeoutMs ?? DEFAULT_ATTACH_TIMEOUT_MS;
127
- this.socket =
128
- opts.socket ??
129
- new WebSocketTransport(
130
- opts.url ?? DEFAULT_BRIDGE_URL,
131
- opts.token ? { [BRIDGE_TOKEN_HEADER]: opts.token } : undefined,
132
- );
140
+ this.socket = opts.socket;
141
+ const pinned = (opts.url ?? process.env.MONOAGENT_BRIDGE_URL)
142
+ ?.split(',')
143
+ .map((u) => u.trim())
144
+ .filter(Boolean);
145
+ this.urls = opts.socket ? [] : pinned?.length ? pinned : DEFAULT_BRIDGE_URLS;
146
+ this.headers = opts.token ? { [BRIDGE_TOKEN_HEADER]: opts.token } : undefined;
147
+ }
148
+
149
+ private get sock(): CdpTransport {
150
+ if (!this.socket) throw new Error('extension bridge is not open');
151
+ return this.socket;
152
+ }
153
+
154
+ /** Open the relay socket, falling through the candidate endpoints. */
155
+ private async openSocket(handlers: CdpTransportHandlers): Promise<CdpTransport> {
156
+ const live: CdpTransportHandlers = {
157
+ message: handlers.message,
158
+ close: (err) => this.onGone(err ?? new Error('extension bridge closed')),
159
+ error: (err) => this.onGone(err),
160
+ };
161
+ if (this.socket) {
162
+ await this.socket.open(live);
163
+ return this.socket;
164
+ }
165
+ let last: unknown;
166
+ for (const url of this.urls) {
167
+ // A refused endpoint rejects open(); its error callback must not
168
+ // report the bridge gone while the next endpoint is still to try.
169
+ let connecting = true;
170
+ const socket = new WebSocketTransport(url, this.headers);
171
+ try {
172
+ await socket.open({
173
+ message: live.message,
174
+ close: (err) => !connecting && live.close(err),
175
+ error: (err) => !connecting && live.error(err),
176
+ });
177
+ connecting = false;
178
+ return socket;
179
+ } catch (err) {
180
+ last = err;
181
+ }
182
+ }
183
+ throw last instanceof Error ? last : new Error('extension bridge unreachable');
133
184
  }
134
185
 
135
186
  async open(handlers: CdpTransportHandlers): Promise<void> {
136
187
  this.handlers = handlers;
137
- await this.socket.open({
188
+ this.socket = await this.openSocket({
138
189
  message: (data) => this.onBridgeMessage(data),
139
- close: (err) => this.onGone(err ?? new Error('extension bridge closed')),
140
- error: (err) => this.onGone(err),
190
+ close: () => {},
191
+ error: () => {},
141
192
  });
142
193
  const reply = await this.command('cdp_attach', undefined, this.attachTimeoutMs);
143
194
  if (reply.success === false) {
144
- this.socket.close();
195
+ this.sock.close();
145
196
  const where = this.tabId ? `tab ${this.tabId}` : 'the active tab';
146
197
  throw new Error(`bridge attach to ${where} failed: ${reply.error ?? 'unknown'}`);
147
198
  }
@@ -149,7 +200,7 @@ export class BridgeTransport implements CdpTransport {
149
200
  // the only way to learn the id when we asked for "whatever is active".
150
201
  if (typeof reply.data?.tabId === 'number') this.tabId = reply.data.tabId;
151
202
  if (!this.tabId) {
152
- this.socket.close();
203
+ this.sock.close();
153
204
  throw new Error('bridge attach did not report a tab id');
154
205
  }
155
206
  }
@@ -199,7 +250,7 @@ export class BridgeTransport implements CdpTransport {
199
250
  // attached leaves Chrome's debugging banner on the user's tab. The
200
251
  // extension's idle sweep would eventually detach anyway.
201
252
  void this.command('cdp_detach').catch(() => {});
202
- this.socket.close();
253
+ this.socket?.close();
203
254
  this.failPending(new Error('extension bridge closed'));
204
255
  }
205
256
 
@@ -252,7 +303,7 @@ export class BridgeTransport implements CdpTransport {
252
303
  if (timer) clearTimeout(timer);
253
304
  resolve(reply);
254
305
  });
255
- this.socket.send(JSON.stringify(envelope)).catch((err) => {
306
+ this.sock.send(JSON.stringify(envelope)).catch((err) => {
256
307
  if (timer) clearTimeout(timer);
257
308
  this.pending.delete(id);
258
309
  reject(err);
@@ -16,14 +16,15 @@ import {
16
16
  } from './browser-discovery.js';
17
17
  import { reapIdleLaunchedBrowser } from './browser-lifecycle.js';
18
18
  import { launchedPids, launchedUserDataDirs, ownedUserDataDirPorts } from './browser-state.js';
19
+ import { DEFAULT_CDP_PORT } from './cdp-port.js';
19
20
  import {
21
+ chromeSpawnEnv,
20
22
  launchProfileDirPath,
21
23
  removeOwnedProfileDir,
22
24
  sweepStaleProfileDirs,
23
25
  } from './profile-dir.js';
24
26
  import type { BrowserConfig } from './types.js';
25
27
 
26
- const DEFAULT_PORT = 9222;
27
28
  const LAUNCH_TIMEOUT = 10_000;
28
29
  const POLL_INTERVAL = 200;
29
30
 
@@ -40,7 +41,7 @@ const LAUNCH_PORT_SCAN_TRIES = 10;
40
41
  class ChromeExitedEarlyError extends Error {}
41
42
 
42
43
  export async function launchBrowser(config: BrowserConfig = {}): Promise<number> {
43
- const rawPort = config.port ?? DEFAULT_PORT;
44
+ const rawPort = config.port ?? DEFAULT_CDP_PORT;
44
45
  // Port 0 means "let Chrome bind a free port and tell us which" — see
45
46
  // launchOnFreePort. Otherwise validate port is in a safe range for
46
47
  // localhost CDP debugging.
@@ -236,6 +237,7 @@ async function launchOnFreePort(config: BrowserConfig, port: number): Promise<nu
236
237
  const args = [...defaultArgs, ...callerArgs];
237
238
  const child = spawn(chromePath, args, {
238
239
  detached: true,
240
+ env: chromeSpawnEnv(process.env),
239
241
  // stderr carries the fixed-port ownership proof (see above). The pipe is
240
242
  // closed as soon as this launch settles, so a Chrome that outlives us
241
243
  // never blocks writing to it.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The CDP port monomind's own Chrome defaults to. Not 9222: mono-agent's
3
+ * extension bridge permanently owns that one and answers only /monoagent
4
+ * routes, so a connect to it never reaches Chrome (#666). 9422 sits outside
5
+ * the 922x/932x neighbourhood mono-agent uses (9222 bridge, 9232 test
6
+ * bridge, 9323 bridge fallback); launching still scans upward if it is busy.
7
+ */
8
+ export const DEFAULT_CDP_PORT = 9422;
9
+
10
+ /**
11
+ * Ports probed by `connect --auto-connect`. 9222 stays a probe so a user's
12
+ * own `--remote-debugging-port=9222` Chrome is still found.
13
+ */
14
+ export const CDP_PROBE_PORTS: readonly number[] = [DEFAULT_CDP_PORT, 9222, 9229];
15
+
16
+ /**
17
+ * The configured CDP port: MONOBROWSE_CDP_PORT, else the two older names
18
+ * (MONOBROWSE_PORT, MONOMIND_CDP_PORT; deprecated aliases), else the default.
19
+ */
20
+ export function resolveCdpPort(env: NodeJS.ProcessEnv = process.env): number {
21
+ for (const name of ['MONOBROWSE_CDP_PORT', 'MONOBROWSE_PORT', 'MONOMIND_CDP_PORT']) {
22
+ const port = Number.parseInt(env[name] ?? '', 10);
23
+ if (Number.isInteger(port) && port > 0) return port;
24
+ }
25
+ return DEFAULT_CDP_PORT;
26
+ }
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_CDP_PORT } from './cdp-port.js';
1
2
  import type { CdpTransport } from './transport.js';
2
3
  import { WebSocketTransport } from './transport.js';
3
4
  import type { CdpCommand, CdpResponse, CdpTarget } from './types.js';
@@ -233,7 +234,10 @@ export interface BrowserPage {
233
234
  close(): Promise<void>;
234
235
  }
235
236
 
236
- export async function createBrowserPage(url: string, port = 9222): Promise<BrowserPage> {
237
+ export async function createBrowserPage(
238
+ url: string,
239
+ port = DEFAULT_CDP_PORT,
240
+ ): Promise<BrowserPage> {
237
241
  const target = await fetchNewTarget(port, url);
238
242
  const wsDebuggerUrl = target.webSocketDebuggerUrl;
239
243
  if (!wsDebuggerUrl) {
@@ -4,6 +4,7 @@ export * from './batch.js';
4
4
  export * from './bridge.js';
5
5
  export * from './browser.js';
6
6
  export * from './cdp.js';
7
+ export * from './cdp-port.js';
7
8
  export * from './console-log.js';
8
9
  export * from './dialog.js';
9
10
  export * from './emulation.js';
@@ -38,6 +38,25 @@ const HOLDER_WAIT_MS = 2000;
38
38
  const POLL_MS = 50;
39
39
  const RM_RETRY_DELAY_MS = 250;
40
40
 
41
+ /** Longest unix socket path Chrome's singleton lock can bind (sun_path is 104
42
+ * bytes on macOS, 108 on Linux; both include the NUL). */
43
+ const MAX_SOCKET_PATH = 103;
44
+ /** `/org.chromium.Chromium.XXXXXX/SingletonSocket` — what Chrome appends to TMPDIR. */
45
+ const SINGLETON_SOCKET_SUFFIX_LEN = '/org.chromium.Chromium.XXXXXX/SingletonSocket'.length;
46
+
47
+ /**
48
+ * Environment to spawn Chrome with (#663). Chrome binds its singleton lock
49
+ * socket under $TMPDIR regardless of --user-data-dir, and aborts at startup
50
+ * (`FATAL: Socket path too long`, SIGABRT) once that path passes the unix
51
+ * socket limit — which a deep TMPDIR (CI work dirs, sandboxed runners) does.
52
+ * Only then is TMPDIR redirected to /tmp; otherwise the env is untouched.
53
+ */
54
+ export function chromeSpawnEnv(env: NodeJS.ProcessEnv, tmp = tmpdir()): NodeJS.ProcessEnv {
55
+ if (process.platform === 'win32') return env;
56
+ if (Buffer.byteLength(tmp) + SINGLETON_SOCKET_SUFFIX_LEN <= MAX_SOCKET_PATH) return env;
57
+ return { ...env, TMPDIR: '/tmp' };
58
+ }
59
+
41
60
  /** Name for a new default launch profile, directly under `root`. */
42
61
  export function launchProfileDirPath(port: number, id: string, root = tmpdir()): string {
43
62
  return join(root, `monomind-browser-${port}-${process.pid}-${id}`);
package/src/cli/action.ts CHANGED
@@ -4,6 +4,7 @@ import { existsSync } from 'node:fs';
4
4
  import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
5
5
  import { join } from 'node:path';
6
6
  import { Command } from 'commander';
7
+ import { resolveCdpPort } from '../browser/cdp-port.js';
7
8
  import type { ActionDef, StepDef } from '../index.js';
8
9
  import { type AnalyzerPage, analyzePageForAction } from '../index.js';
9
10
 
@@ -72,7 +73,7 @@ export function createActionCommand(): Command {
72
73
  | null;
73
74
  if (!createBrowserPage) {
74
75
  throw new Error(
75
- 'Browser CDP client not available. Ensure Chrome is running with --remote-debugging-port=9222',
76
+ 'Browser CDP client not available. Ensure Chrome is running with --remote-debugging-port=9422',
76
77
  );
77
78
  }
78
79
  const page = await createBrowserPage(opts.url);
@@ -119,7 +120,7 @@ export function createActionCommand(): Command {
119
120
  const { clickElement, fillElement, evaluateJs } = await import('../browser/actions.js');
120
121
  const { findBySelector } = await import('../browser/find.js');
121
122
  const { waitFor } = await import('../browser/wait.js');
122
- const port = parseInt(process.env.MONOBROWSE_PORT ?? '9222', 10);
123
+ const port = resolveCdpPort();
123
124
  const { client, sessionId } = await connectToTarget(port);
124
125
  const refs = new Map<string, import('../browser/types.js').ElementRef>();
125
126
 
@@ -8,6 +8,7 @@
8
8
  * differently: it must never kill a Chrome it did not launch.
9
9
  */
10
10
 
11
+ import { CDP_PROBE_PORTS, DEFAULT_CDP_PORT } from '../browser/cdp-port.js';
11
12
  import { detectAttentionNeeded, switchToHeaded } from './attention-wall.js';
12
13
  import { output } from './output.js';
13
14
  import {
@@ -299,22 +300,28 @@ export const closeCommand: Command = {
299
300
  export const connectCommand: Command = {
300
301
  name: 'connect',
301
302
  description:
302
- 'Connect to existing Chrome instance; later commands reuse this session (note: `open` without --port still launches on its own default). Usage: monomind browse connect [--port 9222] [--target <id>] [--auto-connect]',
303
+ 'Connect to existing Chrome instance; later commands reuse this session (note: `open` without --port still launches on its own default). Usage: monomind browse connect [--port 9422] [--target <id>] [--auto-connect]',
303
304
  options: [
304
- { name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
305
+ {
306
+ name: 'port',
307
+ short: 'p',
308
+ type: 'number',
309
+ description: 'CDP port',
310
+ default: DEFAULT_CDP_PORT,
311
+ },
305
312
  { name: 'target', type: 'string', description: 'Target ID to attach to' },
306
313
  {
307
314
  name: 'auto-connect',
308
315
  type: 'boolean',
309
- description: 'Auto-discover running Chrome on ports 9222 and 9229',
316
+ description: 'Auto-discover running Chrome on ports 9422, 9222 and 9229',
310
317
  default: false,
311
318
  },
312
319
  ],
313
320
  action: async (ctx: CommandContext): Promise<CommandResult> => {
314
- let port = (ctx.flags.port as number) ?? 9222;
321
+ let port = (ctx.flags.port as number) ?? DEFAULT_CDP_PORT;
315
322
 
316
323
  if (ctx.flags['auto-connect']) {
317
- const probePorts = [9222, 9229];
324
+ const probePorts = CDP_PROBE_PORTS;
318
325
  let found = false;
319
326
  for (const p of probePorts) {
320
327
  try {
@@ -8,9 +8,10 @@ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
8
8
  import { homedir } from 'node:os';
9
9
  import { join } from 'node:path';
10
10
  import { Command } from 'commander';
11
+ import { resolveCdpPort } from '../browser/cdp-port.js';
11
12
  import { getAdapter } from '../index.js';
12
13
 
13
- const CDP_PORT = Number(process.env.MONOMIND_CDP_PORT ?? 9222);
14
+ const CDP_PORT = resolveCdpPort();
14
15
 
15
16
  const SESSIONS_FILE = join(homedir(), '.monomind', 'sessions.json');
16
17