monomind 1.10.40 → 1.10.41

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 (24) hide show
  1. package/package.json +1 -1
  2. package/packages/@monomind/cli/dist/src/browser/actions.js +114 -22
  3. package/packages/@monomind/cli/dist/src/browser/browser.d.ts +1 -0
  4. package/packages/@monomind/cli/dist/src/browser/browser.js +51 -24
  5. package/packages/@monomind/cli/dist/src/browser/cdp.js +20 -4
  6. package/packages/@monomind/cli/dist/src/browser/console-log.d.ts +1 -0
  7. package/packages/@monomind/cli/dist/src/browser/console-log.js +19 -3
  8. package/packages/@monomind/cli/dist/src/browser/dialog.js +1 -1
  9. package/packages/@monomind/cli/dist/src/browser/find.js +28 -8
  10. package/packages/@monomind/cli/dist/src/browser/har.d.ts +1 -0
  11. package/packages/@monomind/cli/dist/src/browser/har.js +7 -5
  12. package/packages/@monomind/cli/dist/src/browser/network.d.ts +7 -4
  13. package/packages/@monomind/cli/dist/src/browser/network.js +59 -22
  14. package/packages/@monomind/cli/dist/src/browser/profiler.js +13 -2
  15. package/packages/@monomind/cli/dist/src/browser/screenshot.js +3 -2
  16. package/packages/@monomind/cli/dist/src/browser/session.js +49 -12
  17. package/packages/@monomind/cli/dist/src/browser/snapshot.js +26 -14
  18. package/packages/@monomind/cli/dist/src/browser/storage.d.ts +1 -0
  19. package/packages/@monomind/cli/dist/src/browser/storage.js +3 -0
  20. package/packages/@monomind/cli/dist/src/browser/tabs.d.ts +2 -2
  21. package/packages/@monomind/cli/dist/src/browser/tabs.js +8 -5
  22. package/packages/@monomind/cli/dist/src/browser/wait.js +23 -13
  23. package/packages/@monomind/cli/dist/src/commands/browse.js +265 -32
  24. package/packages/@monomind/cli/package.json +1 -1
@@ -14,6 +14,7 @@ interface HarRequest {
14
14
  encodedSize: number;
15
15
  fromCache: boolean;
16
16
  responseBody?: string;
17
+ bodyEncoding?: 'base64';
17
18
  }
18
19
  export declare function startHarRecording(client: CdpClient, sessionId: string): Promise<void>;
19
20
  export declare function stopHarRecording(client: CdpClient, sessionId: string, outputPath?: string, captureResponseBodies?: boolean): Promise<string>;
@@ -60,22 +60,23 @@ export async function stopHarRecording(client, sessionId, outputPath, captureRes
60
60
  state.offReq();
61
61
  state.offResp();
62
62
  state.offFinished();
63
+ _sessions.delete(sessionId); // remove immediately after detaching listeners to prevent duplicate invocations
63
64
  // Optionally fetch response bodies
64
65
  if (captureResponseBodies) {
65
66
  for (const [reqId, entry] of state.requests.entries()) {
66
67
  try {
67
68
  const body = await client.send('Network.getResponseBody', { requestId: reqId }, sessionId);
68
- entry.responseBody = body.base64Encoded
69
- ? Buffer.from(body.body, 'base64').toString('utf8')
70
- : body.body;
71
- entry.size = entry.responseBody?.length ?? 0;
69
+ entry.responseBody = body.body;
70
+ entry.bodyEncoding = body.base64Encoded ? 'base64' : undefined;
71
+ entry.size = body.base64Encoded
72
+ ? Buffer.byteLength(body.body, 'base64')
73
+ : Buffer.byteLength(body.body, 'utf8');
72
74
  }
73
75
  catch {
74
76
  // body may not be available for cached/redirected responses
75
77
  }
76
78
  }
77
79
  }
78
- _sessions.delete(sessionId); // always runs — response body loop errors are caught per-entry
79
80
  const har = buildHar(Array.from(state.requests.values()), state.startTime);
80
81
  const path = outputPath ?? join(tmpdir(), `monomind-har-${Date.now()}.har`);
81
82
  await writeFile(path, JSON.stringify(har, null, 2));
@@ -123,6 +124,7 @@ function buildHar(entries, startTime) {
123
124
  size: e.size ?? -1,
124
125
  mimeType: e.mimeType ?? 'application/octet-stream',
125
126
  text: e.responseBody,
127
+ encoding: e.bodyEncoding,
126
128
  },
127
129
  redirectURL: '',
128
130
  headersSize: -1,
@@ -6,9 +6,7 @@ export declare function clearCookies(client: CdpClient, sessionId: string): Prom
6
6
  export declare function setExtraHeaders(client: CdpClient, sessionId: string, headers: Record<string, string>): Promise<void>;
7
7
  export declare function enableInterception(client: CdpClient, sessionId: string): Promise<void>;
8
8
  export declare function setupRoutes(client: CdpClient, sessionId: string, routes: NetworkRoute[]): Promise<void>;
9
- export declare function startRequestCapture(client: CdpClient, sessionId: string): void;
10
- export declare function stopRequestCapture(sessionId: string): void;
11
- export declare function getCapturedRequests(sessionId: string): {
9
+ type CapturedRequest = {
12
10
  id: string;
13
11
  url: string;
14
12
  method: string;
@@ -19,11 +17,16 @@ export declare function getCapturedRequests(sessionId: string): {
19
17
  startTime: number;
20
18
  endTime?: number;
21
19
  encodedSize?: number;
22
- }[];
20
+ };
21
+ export declare function startRequestCapture(client: CdpClient, sessionId: string): void;
22
+ export declare function stopRequestCapture(sessionId: string): void;
23
+ export declare function getCapturedRequests(sessionId: string): CapturedRequest[];
23
24
  export declare function clearCapturedRequests(sessionId: string): void;
24
25
  export declare function disableInterception(client: CdpClient, sessionId: string): Promise<void>;
26
+ export declare function teardownRouteInterception(sessionId: string): void;
25
27
  export declare function getLocalStorage(client: CdpClient, sessionId: string): Promise<Record<string, string>>;
26
28
  export declare function setLocalStorage(client: CdpClient, sessionId: string, data: Record<string, string>): Promise<void>;
27
29
  export declare function getSessionStorage(client: CdpClient, sessionId: string): Promise<Record<string, string>>;
28
30
  export declare function setSessionStorage(client: CdpClient, sessionId: string, data: Record<string, string>): Promise<void>;
31
+ export {};
29
32
  //# sourceMappingURL=network.d.ts.map
@@ -25,18 +25,19 @@ export async function setupRoutes(client, sessionId, routes) {
25
25
  _routeListeners.delete(sessionId);
26
26
  }
27
27
  if (routes.length === 0) {
28
- await client.send('Fetch.disable', {}, sessionId);
28
+ await client.send('Fetch.disable', {}, sessionId).catch(() => { });
29
29
  return;
30
30
  }
31
- await client.send('Fetch.enable', {
32
- patterns: routes.map((r) => ({ urlPattern: globToFetchPattern(r.pattern), requestStage: 'Request' })),
33
- }, sessionId);
31
+ // Pre-compile route regexes once — avoids creating new RegExp on every paused request
32
+ const compiledRoutes = routes.map((r) => ({ route: r, regex: globToRegex(r.pattern) }));
33
+ // Register listener BEFORE Fetch.enable to avoid race where Chrome emits
34
+ // Fetch.requestPaused before the handler is in place, leaving requests hung
34
35
  const off = client.on('Fetch.requestPaused', async (params, sid) => {
35
36
  if (sid !== sessionId)
36
37
  return;
37
38
  const { requestId, request } = params;
38
39
  try {
39
- const matchedRoute = routes.find((r) => globToRegex(r.pattern).test(request.url));
40
+ const matchedRoute = compiledRoutes.find((e) => e.regex.test(request.url))?.route;
40
41
  if (!matchedRoute) {
41
42
  await client.send('Fetch.continueRequest', { requestId }, sessionId);
42
43
  return;
@@ -65,25 +66,29 @@ export async function setupRoutes(client, sessionId, routes) {
65
66
  }
66
67
  });
67
68
  _routeListeners.set(sessionId, off);
69
+ await client.send('Fetch.enable', {
70
+ patterns: routes.map((r) => ({ urlPattern: globToFetchPattern(r.pattern), requestStage: 'Request' })),
71
+ }, sessionId);
68
72
  }
69
73
  const _capturedRequests = new Map();
70
74
  const _captureListeners = new Map();
71
75
  export function startRequestCapture(client, sessionId) {
72
- if (_captureListeners.has(sessionId))
73
- return;
74
- const list = [];
75
- _capturedRequests.set(sessionId, list);
76
+ stopRequestCapture(sessionId);
77
+ _capturedRequests.set(sessionId, new Map());
78
+ client.send('Network.enable', {}, sessionId).catch(() => { });
79
+ // Use indirection so clearCapturedRequests (which replaces the Map) affects live listeners
80
+ const idx = () => _capturedRequests.get(sessionId);
76
81
  const offReq = client.on('Network.requestWillBeSent', (params, sid) => {
77
82
  if (sid !== sessionId)
78
83
  return;
79
84
  const p = params;
80
- list.push({ id: p.requestId, url: p.request.url, method: p.request.method, requestHeaders: p.request.headers, startTime: p.timestamp * 1000 });
85
+ idx().set(p.requestId, { id: p.requestId, url: p.request.url, method: p.request.method, requestHeaders: p.request.headers, startTime: p.timestamp * 1000 });
81
86
  });
82
87
  const offResp = client.on('Network.responseReceived', (params, sid) => {
83
88
  if (sid !== sessionId)
84
89
  return;
85
90
  const p = params;
86
- const entry = list.find((r) => r.id === p.requestId);
91
+ const entry = idx().get(p.requestId);
87
92
  if (entry) {
88
93
  entry.status = p.response.status;
89
94
  entry.mimeType = p.response.mimeType;
@@ -95,13 +100,22 @@ export function startRequestCapture(client, sessionId) {
95
100
  if (sid !== sessionId)
96
101
  return;
97
102
  const p = params;
98
- const entry = list.find((r) => r.id === p.requestId);
103
+ const entry = idx().get(p.requestId);
99
104
  if (entry) {
100
105
  entry.encodedSize = p.encodedDataLength;
101
106
  entry.endTime = p.timestamp * 1000;
102
107
  }
103
108
  });
104
- _captureListeners.set(sessionId, [offReq, offResp, offFinished]);
109
+ const offFailed = client.on('Network.loadingFailed', (params, sid) => {
110
+ if (sid !== sessionId)
111
+ return;
112
+ const p = params;
113
+ const entry = idx().get(p.requestId);
114
+ if (entry) {
115
+ entry.endTime = p.timestamp * 1000;
116
+ }
117
+ });
118
+ _captureListeners.set(sessionId, [offReq, offResp, offFinished, offFailed]);
105
119
  }
106
120
  export function stopRequestCapture(sessionId) {
107
121
  const offs = _captureListeners.get(sessionId);
@@ -113,10 +127,10 @@ export function stopRequestCapture(sessionId) {
113
127
  _capturedRequests.delete(sessionId);
114
128
  }
115
129
  export function getCapturedRequests(sessionId) {
116
- return _capturedRequests.get(sessionId) ?? [];
130
+ return [...(_capturedRequests.get(sessionId)?.values() ?? [])];
117
131
  }
118
132
  export function clearCapturedRequests(sessionId) {
119
- _capturedRequests.set(sessionId, []);
133
+ _capturedRequests.set(sessionId, new Map());
120
134
  }
121
135
  export async function disableInterception(client, sessionId) {
122
136
  const prevOff = _routeListeners.get(sessionId);
@@ -126,11 +140,20 @@ export async function disableInterception(client, sessionId) {
126
140
  }
127
141
  await client.send('Fetch.disable', {}, sessionId);
128
142
  }
143
+ export function teardownRouteInterception(sessionId) {
144
+ const prevOff = _routeListeners.get(sessionId);
145
+ if (prevOff) {
146
+ prevOff();
147
+ _routeListeners.delete(sessionId);
148
+ }
149
+ }
129
150
  export async function getLocalStorage(client, sessionId) {
130
151
  const result = await client.send('Runtime.evaluate', {
131
152
  expression: 'JSON.stringify(Object.fromEntries(Object.entries(localStorage)))',
132
153
  returnByValue: true,
133
154
  }, sessionId);
155
+ if (result.exceptionDetails)
156
+ return {};
134
157
  try {
135
158
  return JSON.parse(result.result?.value ?? '{}');
136
159
  }
@@ -142,14 +165,20 @@ export async function setLocalStorage(client, sessionId, data) {
142
165
  const script = Object.entries(data)
143
166
  .map(([k, v]) => `localStorage.setItem(${JSON.stringify(k)}, ${JSON.stringify(v)})`)
144
167
  .join('; ');
145
- if (script)
146
- await client.send('Runtime.evaluate', { expression: script }, sessionId);
168
+ if (script) {
169
+ const result = await client.send('Runtime.evaluate', { expression: script, returnByValue: true }, sessionId);
170
+ if (result.exceptionDetails) {
171
+ throw new Error(`setLocalStorage failed: ${result.exceptionDetails.exception?.description ?? result.exceptionDetails.text}`);
172
+ }
173
+ }
147
174
  }
148
175
  export async function getSessionStorage(client, sessionId) {
149
176
  const result = await client.send('Runtime.evaluate', {
150
177
  expression: 'JSON.stringify(Object.fromEntries(Object.entries(sessionStorage)))',
151
178
  returnByValue: true,
152
179
  }, sessionId);
180
+ if (result.exceptionDetails)
181
+ return {};
153
182
  try {
154
183
  return JSON.parse(result.result?.value ?? '{}');
155
184
  }
@@ -161,14 +190,22 @@ export async function setSessionStorage(client, sessionId, data) {
161
190
  const script = Object.entries(data)
162
191
  .map(([k, v]) => `sessionStorage.setItem(${JSON.stringify(k)}, ${JSON.stringify(v)})`)
163
192
  .join('; ');
164
- if (script)
165
- await client.send('Runtime.evaluate', { expression: script }, sessionId);
193
+ if (script) {
194
+ const result = await client.send('Runtime.evaluate', { expression: script, returnByValue: true }, sessionId);
195
+ if (result.exceptionDetails) {
196
+ throw new Error(`setSessionStorage failed: ${result.exceptionDetails.exception?.description ?? result.exceptionDetails.text}`);
197
+ }
198
+ }
166
199
  }
167
200
  function globToRegex(pattern) {
201
+ // Chrome's Fetch urlPattern treats '*' as matching everything including '/'
202
+ // and '?' as any single character — match both behaviors here to avoid divergence
168
203
  const escaped = pattern
169
- .replace(/[-[\]{}()+?.,\\^$|#\s]/g, '\\$&')
170
- .replace(/\*\*/g, '.*')
171
- .replace(/\*/g, '[^/]*');
204
+ .replace(/[-[\]{}()+.,\\^$|#\s]/g, '\\$&')
205
+ .replace(/\*\*/g, '\x00')
206
+ .replace(/\*/g, '[^/]*')
207
+ .replace(/\x00/g, '.*')
208
+ .replace(/\?/g, '[^/]');
172
209
  return new RegExp(`^${escaped}$`);
173
210
  }
174
211
  function globToFetchPattern(pattern) {
@@ -2,6 +2,7 @@ import { writeFile } from 'fs/promises';
2
2
  import { join } from 'path';
3
3
  import { tmpdir } from 'os';
4
4
  const _sessions = new Set();
5
+ const _heapSessions = new Set();
5
6
  export async function startCpuProfile(client, sessionId, options = {}) {
6
7
  if (_sessions.has(sessionId)) {
7
8
  throw new Error('CPU profiler already running for this session');
@@ -34,6 +35,10 @@ export function isProfilingActive(sessionId) {
34
35
  return _sessions.has(sessionId);
35
36
  }
36
37
  export async function startHeapSnapshot(client, sessionId, outputPath) {
38
+ if (_heapSessions.has(sessionId)) {
39
+ throw new Error('Heap snapshot already in progress for this session');
40
+ }
41
+ _heapSessions.add(sessionId);
37
42
  await client.send('HeapProfiler.enable', {}, sessionId);
38
43
  const chunks = [];
39
44
  const off = client.on('HeapProfiler.addHeapSnapshotChunk', (params, sid) => {
@@ -43,22 +48,28 @@ export async function startHeapSnapshot(client, sessionId, outputPath) {
43
48
  });
44
49
  try {
45
50
  await new Promise((resolve, reject) => {
51
+ const timeoutHandle = setTimeout(() => {
52
+ off2();
53
+ reject(new Error('Timeout waiting for heap snapshot (120s)'));
54
+ }, 120_000);
46
55
  const off2 = client.on('HeapProfiler.reportHeapSnapshotProgress', (params, sid) => {
47
56
  if (sid !== sessionId)
48
57
  return;
49
58
  if (params.finished) {
59
+ clearTimeout(timeoutHandle);
50
60
  off2();
51
61
  resolve();
52
62
  }
53
63
  });
54
64
  client.send('HeapProfiler.takeHeapSnapshot', { reportProgress: true }, sessionId)
55
- .catch((err) => { off2(); reject(err); });
65
+ .catch((err) => { clearTimeout(timeoutHandle); off2(); reject(err); });
56
66
  });
57
67
  }
58
68
  finally {
59
69
  off();
70
+ _heapSessions.delete(sessionId);
71
+ await client.send('HeapProfiler.disable', {}, sessionId).catch(() => { });
60
72
  }
61
- await client.send('HeapProfiler.disable', {}, sessionId);
62
73
  const path = outputPath ?? join(tmpdir(), `monomind-heap-${Date.now()}.heapsnapshot`);
63
74
  await writeFile(path, chunks.join(''));
64
75
  return path;
@@ -1,5 +1,5 @@
1
- import { writeFile } from 'fs/promises';
2
- import { join } from 'path';
1
+ import { writeFile, mkdir } from 'fs/promises';
2
+ import { join, dirname } from 'path';
3
3
  import { tmpdir } from 'os';
4
4
  export async function captureScreenshot(client, sessionId, options = {}) {
5
5
  const format = options.format ?? 'png';
@@ -20,6 +20,7 @@ export async function captureScreenshot(client, sessionId, options = {}) {
20
20
  const data = result.data;
21
21
  const dataUrl = `data:image/${format};base64,${data}`;
22
22
  const outputPath = options.path ?? join(tmpdir(), `monomind-screenshot-${Date.now()}.${format}`);
23
+ await mkdir(dirname(outputPath), { recursive: true });
23
24
  await writeFile(outputPath, Buffer.from(data, 'base64'));
24
25
  return { path: outputPath, dataUrl };
25
26
  }
@@ -1,10 +1,23 @@
1
1
  import { readFile, writeFile, mkdir } from 'fs/promises';
2
- import { existsSync } from 'fs';
3
- import { join } from 'path';
2
+ import { join, dirname } from 'path';
4
3
  import { homedir } from 'os';
5
4
  import { getCookies, setCookies, getLocalStorage, setLocalStorage, getSessionStorage, setSessionStorage } from './network.js';
6
5
  const SESSION_DIR = join(homedir(), '.monomind', 'browser-sessions');
6
+ function validateSessionName(name) {
7
+ if (!name || /[/\\\x00]/.test(name) || name === '..' || name === '.' || name.startsWith('..')) {
8
+ throw new Error(`Invalid session name: ${JSON.stringify(name)}`);
9
+ }
10
+ }
11
+ function validateFilePath(filePath) {
12
+ if (!filePath || filePath.includes('\x00')) {
13
+ throw new Error(`Invalid file path: ${JSON.stringify(filePath)}`);
14
+ }
15
+ if (filePath.includes('/../') || filePath.startsWith('../') || filePath.endsWith('/..') || filePath.includes('\\')) {
16
+ throw new Error(`Invalid file path (traversal not allowed): ${JSON.stringify(filePath)}`);
17
+ }
18
+ }
7
19
  export async function saveSession(client, sessionId, targetId, name, url, title) {
20
+ validateSessionName(name);
8
21
  await mkdir(SESSION_DIR, { recursive: true });
9
22
  const cookies = await getCookies(client, sessionId);
10
23
  const localStorage = await getLocalStorage(client, sessionId);
@@ -15,19 +28,32 @@ export async function saveSession(client, sessionId, targetId, name, url, title)
15
28
  return filePath;
16
29
  }
17
30
  export async function loadSession(client, sessionId, name) {
31
+ validateSessionName(name);
18
32
  const filePath = join(SESSION_DIR, `${name}.json`);
19
- if (!existsSync(filePath))
20
- throw new Error(`Session not found: ${name}`);
21
- const raw = await readFile(filePath, 'utf8');
33
+ const raw = await readFile(filePath, 'utf8').catch((err) => {
34
+ if (err.code === 'ENOENT')
35
+ throw new Error(`Session not found: ${name}`);
36
+ throw err;
37
+ });
22
38
  const state = JSON.parse(raw);
39
+ if (!Array.isArray(state.cookies))
40
+ throw new Error(`Invalid session file: cookies is not an array`);
23
41
  await setCookies(client, sessionId, state.cookies);
24
- if (state.localStorage)
42
+ if (state.localStorage) {
43
+ if (typeof state.localStorage !== 'object' || Array.isArray(state.localStorage))
44
+ throw new Error('Invalid session file: localStorage is not a plain object');
25
45
  await setLocalStorage(client, sessionId, state.localStorage);
26
- if (state.sessionStorage)
46
+ }
47
+ if (state.sessionStorage) {
48
+ if (typeof state.sessionStorage !== 'object' || Array.isArray(state.sessionStorage))
49
+ throw new Error('Invalid session file: sessionStorage is not a plain object');
27
50
  await setSessionStorage(client, sessionId, state.sessionStorage);
51
+ }
28
52
  return state;
29
53
  }
30
54
  export async function saveStateFile(client, sessionId, targetId, filePath, url, title) {
55
+ validateFilePath(filePath);
56
+ await mkdir(dirname(filePath), { recursive: true });
31
57
  const cookies = await getCookies(client, sessionId);
32
58
  const localStorage = await getLocalStorage(client, sessionId);
33
59
  const sessionStorage = await getSessionStorage(client, sessionId);
@@ -35,20 +61,31 @@ export async function saveStateFile(client, sessionId, targetId, filePath, url,
35
61
  await writeFile(filePath, JSON.stringify(state, null, 2));
36
62
  }
37
63
  export async function loadStateFile(client, sessionId, filePath) {
64
+ validateFilePath(filePath);
38
65
  const raw = await readFile(filePath, 'utf8');
39
66
  const state = JSON.parse(raw);
67
+ if (!Array.isArray(state.cookies))
68
+ throw new Error(`Invalid state file: cookies is not an array`);
40
69
  await setCookies(client, sessionId, state.cookies);
41
- if (state.localStorage)
70
+ if (state.localStorage) {
71
+ if (typeof state.localStorage !== 'object' || Array.isArray(state.localStorage))
72
+ throw new Error('Invalid state file: localStorage is not a plain object');
42
73
  await setLocalStorage(client, sessionId, state.localStorage);
43
- if (state.sessionStorage)
74
+ }
75
+ if (state.sessionStorage) {
76
+ if (typeof state.sessionStorage !== 'object' || Array.isArray(state.sessionStorage))
77
+ throw new Error('Invalid state file: sessionStorage is not a plain object');
44
78
  await setSessionStorage(client, sessionId, state.sessionStorage);
79
+ }
45
80
  return state;
46
81
  }
47
82
  export async function listSessions() {
48
- if (!existsSync(SESSION_DIR))
49
- return [];
50
83
  const { readdir } = await import('fs/promises');
51
- const files = await readdir(SESSION_DIR);
84
+ const files = await readdir(SESSION_DIR).catch((err) => {
85
+ if (err.code === 'ENOENT')
86
+ return [];
87
+ throw err;
88
+ });
52
89
  return files.filter((f) => f.endsWith('.json')).map((f) => f.replace(/\.json$/, ''));
53
90
  }
54
91
  //# sourceMappingURL=session.js.map
@@ -8,7 +8,7 @@ export async function captureSnapshot(client, sessionId, options = {}) {
8
8
  const doc = await client.send('DOM.getDocument', {}, sessionId);
9
9
  const found = await client.send('DOM.querySelector', { nodeId: doc.root.nodeId, selector }, sessionId).catch(() => ({ nodeId: 0 }));
10
10
  if (found.nodeId) {
11
- const partial = await client.send('Accessibility.getPartialAXTree', { nodeId: found.nodeId, fetchRelatives: true }, sessionId).catch(async () => client.send('Accessibility.getFullAXTree', {}, sessionId));
11
+ const partial = await client.send('Accessibility.getPartialAXTree', { nodeId: found.nodeId, fetchRelatives: false }, sessionId).catch(async () => client.send('Accessibility.getFullAXTree', {}, sessionId));
12
12
  nodes = partial.nodes;
13
13
  }
14
14
  else {
@@ -29,14 +29,19 @@ export async function captureSnapshot(client, sessionId, options = {}) {
29
29
  let refCounter = 1;
30
30
  const lines = [];
31
31
  const processNode = (node, depth) => {
32
- if (node.ignored)
33
- return;
34
- if (maxDepth !== undefined && depth > maxDepth)
32
+ // Transparent/structural nodes don't consume depth budget — check before maxDepth
33
+ if (node.ignored) {
34
+ if (node.childIds) {
35
+ for (const childId of node.childIds) {
36
+ const child = nodeMap.get(childId);
37
+ if (child)
38
+ processNode(child, depth);
39
+ }
40
+ }
35
41
  return;
42
+ }
36
43
  const role = node.role?.value ?? 'generic';
37
- const name = node.name?.value ?? '';
38
- const description = node.description?.value;
39
- if (role === 'none' || role === 'generic' || role === 'InlineTextBox') {
44
+ if (role === 'none' || role === 'generic' || role === 'inlineTextBox') {
40
45
  if (node.childIds) {
41
46
  for (const childId of node.childIds) {
42
47
  const child = nodeMap.get(childId);
@@ -46,6 +51,11 @@ export async function captureSnapshot(client, sessionId, options = {}) {
46
51
  }
47
52
  return;
48
53
  }
54
+ // Only enforce maxDepth for real rendered roles
55
+ if (maxDepth !== undefined && depth > maxDepth)
56
+ return;
57
+ const name = node.name?.value ?? '';
58
+ const description = node.description?.value;
49
59
  const isInteractive = INTERACTIVE_ROLES.has(role.toLowerCase());
50
60
  if (interactiveOnly && !isInteractive) {
51
61
  if (node.childIds) {
@@ -91,7 +101,7 @@ export async function captureSnapshot(client, sessionId, options = {}) {
91
101
  const descStr = description && !compact ? ` (${description})` : '';
92
102
  const attrsStr = ` [${attrParts.join(', ')}]`;
93
103
  lines.push(`${indent}${role}${nameStr}${descStr}${attrsStr}`);
94
- if (!isInteractive && node.childIds) {
104
+ if (node.childIds) {
95
105
  for (const childId of node.childIds) {
96
106
  const child = nodeMap.get(childId);
97
107
  if (child)
@@ -99,9 +109,10 @@ export async function captureSnapshot(client, sessionId, options = {}) {
99
109
  }
100
110
  }
101
111
  };
102
- // Find root node
103
- const root = nodes.find((n) => !n.parentId);
104
- if (root)
112
+ // Find root nodes — for partial trees parentId may point outside the set; use filter to handle forests
113
+ const ids = new Set(nodes.map((n) => n.nodeId));
114
+ const roots = nodes.filter((n) => n.parentId === undefined || !ids.has(n.parentId));
115
+ for (const root of roots)
105
116
  processNode(root, 0);
106
117
  return { text: lines.join('\n'), refs, url, title };
107
118
  }
@@ -112,7 +123,8 @@ function extractProperties(node) {
112
123
  for (const prop of node.properties) {
113
124
  switch (prop.name) {
114
125
  case 'value':
115
- result.value = String(prop.value.value ?? '');
126
+ if (prop.value.value != null)
127
+ result.value = String(prop.value.value);
116
128
  break;
117
129
  case 'disabled':
118
130
  result.disabled = Boolean(prop.value.value);
@@ -156,8 +168,8 @@ export async function getElementBox(client, sessionId, ref) {
156
168
  const content = result.model?.content;
157
169
  if (!content || content.length < 8)
158
170
  return null;
159
- const x = (content[0] + content[2]) / 2;
160
- const y = (content[1] + content[5]) / 2;
171
+ const x = (content[0] + content[2] + content[4] + content[6]) / 4;
172
+ const y = (content[1] + content[3] + content[5] + content[7]) / 4;
161
173
  return { x, y, width: result.model.width, height: result.model.height };
162
174
  }
163
175
  catch {
@@ -6,6 +6,7 @@ export declare function clearLocalStorage(client: CdpClient, sessionId: string):
6
6
  export declare function getAllLocalStorage(client: CdpClient, sessionId: string): Promise<Record<string, string>>;
7
7
  export declare function getSessionStorageKey(client: CdpClient, sessionId: string, key: string): Promise<string | null>;
8
8
  export declare function setSessionStorageKey(client: CdpClient, sessionId: string, key: string, value: string): Promise<void>;
9
+ export declare function removeSessionStorageKey(client: CdpClient, sessionId: string, key: string): Promise<void>;
9
10
  export declare function clearSessionStorage(client: CdpClient, sessionId: string): Promise<void>;
10
11
  export declare function getAllSessionStorage(client: CdpClient, sessionId: string): Promise<Record<string, string>>;
11
12
  //# sourceMappingURL=storage.d.ts.map
@@ -28,6 +28,9 @@ export async function getSessionStorageKey(client, sessionId, key) {
28
28
  export async function setSessionStorageKey(client, sessionId, key, value) {
29
29
  await evaluateJs(client, sessionId, `sessionStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`);
30
30
  }
31
+ export async function removeSessionStorageKey(client, sessionId, key) {
32
+ await evaluateJs(client, sessionId, `sessionStorage.removeItem(${JSON.stringify(key)})`);
33
+ }
31
34
  export async function clearSessionStorage(client, sessionId) {
32
35
  await evaluateJs(client, sessionId, 'sessionStorage.clear()');
33
36
  }
@@ -2,7 +2,7 @@ import type { CdpClient } from './cdp.js';
2
2
  import type { CdpTarget } from './types.js';
3
3
  export declare function listTabs(port: number): Promise<CdpTarget[]>;
4
4
  export declare function newTab(port: number, url?: string): Promise<CdpTarget>;
5
- export declare function closeTab(client: CdpClient, sessionId: string, targetId: string): Promise<void>;
6
- export declare function activateTab(client: CdpClient, sessionId: string, targetId: string): Promise<string>;
5
+ export declare function closeTab(client: CdpClient, _sessionId: string, targetId: string): Promise<void>;
6
+ export declare function activateTab(client: CdpClient, oldSessionId: string, targetId: string): Promise<string>;
7
7
  export declare function switchToFrame(_client: CdpClient, _sessionId: string, _frameSelector: string): Promise<string | null>;
8
8
  //# sourceMappingURL=tabs.d.ts.map
@@ -6,12 +6,15 @@ export async function listTabs(port) {
6
6
  export async function newTab(port, url = 'about:blank') {
7
7
  return fetchNewTarget(port, url);
8
8
  }
9
- export async function closeTab(client, sessionId, targetId) {
10
- await client.send('Target.closeTarget', { targetId }, sessionId);
9
+ export async function closeTab(client, _sessionId, targetId) {
10
+ await client.send('Target.closeTarget', { targetId });
11
11
  }
12
- export async function activateTab(client, sessionId, targetId) {
13
- await client.send('Target.activateTarget', { targetId }, sessionId);
14
- const result = await client.send('Target.attachToTarget', { targetId, flatten: true }, sessionId);
12
+ export async function activateTab(client, oldSessionId, targetId) {
13
+ if (oldSessionId) {
14
+ await client.send('Target.detachFromTarget', { sessionId: oldSessionId }).catch(() => { });
15
+ }
16
+ await client.send('Target.activateTarget', { targetId });
17
+ const result = await client.send('Target.attachToTarget', { targetId, flatten: true });
15
18
  return result.sessionId;
16
19
  }
17
20
  export async function switchToFrame(_client, _sessionId, _frameSelector) {
@@ -32,7 +32,10 @@ async function waitForLoad(client, sessionId, condition, timeout) {
32
32
  const event = condition === 'load' ? 'Page.loadEventFired' : 'Page.domContentEventFired';
33
33
  const [eventPromise, cancelOnce] = client.onceWithOff(event, sessionId);
34
34
  let timedOut = false;
35
- const timeoutPromise = sleep(timeout).then(() => { timedOut = true; });
35
+ let timeoutHandle;
36
+ const timeoutPromise = new Promise((resolve) => {
37
+ timeoutHandle = setTimeout(() => { timedOut = true; resolve(); }, timeout);
38
+ });
36
39
  try {
37
40
  await Promise.race([eventPromise, timeoutPromise]);
38
41
  if (timedOut)
@@ -40,11 +43,13 @@ async function waitForLoad(client, sessionId, condition, timeout) {
40
43
  }
41
44
  finally {
42
45
  cancelOnce();
46
+ clearTimeout(timeoutHandle);
43
47
  }
44
48
  }
45
49
  async function waitForNetworkIdle(client, sessionId, idleMs, timeout) {
46
50
  return new Promise((resolve, reject) => {
47
51
  let pending = 0;
52
+ const inflight = new Set();
48
53
  let idleTimer = null;
49
54
  const cleanup = () => {
50
55
  if (idleTimer) {
@@ -55,6 +60,7 @@ async function waitForNetworkIdle(client, sessionId, idleMs, timeout) {
55
60
  offReq();
56
61
  offResp();
57
62
  offFail();
63
+ offCache();
58
64
  };
59
65
  const killTimer = setTimeout(() => {
60
66
  cleanup();
@@ -74,24 +80,28 @@ async function waitForNetworkIdle(client, sessionId, idleMs, timeout) {
74
80
  }
75
81
  }
76
82
  };
77
- const offReq = client.on('Network.requestWillBeSent', (_, sid) => {
78
- if (sid === sessionId) {
83
+ const offReq = client.on('Network.requestWillBeSent', (params, sid) => {
84
+ if (sid !== sessionId)
85
+ return;
86
+ const id = params.requestId;
87
+ if (!inflight.has(id)) {
88
+ inflight.add(id);
79
89
  pending++;
80
90
  check();
81
91
  }
82
92
  });
83
- const offResp = client.on('Network.loadingFinished', (_, sid) => {
84
- if (sid === sessionId) {
93
+ const decrement = (params, sid) => {
94
+ if (sid !== sessionId)
95
+ return;
96
+ const id = params.requestId;
97
+ if (inflight.delete(id)) {
85
98
  pending = Math.max(0, pending - 1);
86
99
  check();
87
100
  }
88
- });
89
- const offFail = client.on('Network.loadingFailed', (_, sid) => {
90
- if (sid === sessionId) {
91
- pending = Math.max(0, pending - 1);
92
- check();
93
- }
94
- });
101
+ };
102
+ const offResp = client.on('Network.loadingFinished', decrement);
103
+ const offFail = client.on('Network.loadingFailed', decrement);
104
+ const offCache = client.on('Network.requestServedFromCache', decrement);
95
105
  check();
96
106
  });
97
107
  }
@@ -107,7 +117,7 @@ async function waitForUrl(client, sessionId, pattern, deadline) {
107
117
  }
108
118
  async function waitForText(client, sessionId, text, deadline) {
109
119
  while (Date.now() < deadline) {
110
- const bodyText = await evaluateJs(client, sessionId, 'document.body.innerText');
120
+ const bodyText = await evaluateJs(client, sessionId, 'document.body?.innerText ?? ""');
111
121
  if (bodyText.includes(text))
112
122
  return;
113
123
  await sleep(POLL_INTERVAL);