monomind 1.10.39 → 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.
- package/package.json +1 -1
- package/packages/@monomind/cli/dist/src/browser/actions.js +114 -22
- package/packages/@monomind/cli/dist/src/browser/batch.d.ts +0 -2
- package/packages/@monomind/cli/dist/src/browser/batch.js +1 -10
- package/packages/@monomind/cli/dist/src/browser/browser.d.ts +1 -0
- package/packages/@monomind/cli/dist/src/browser/browser.js +51 -24
- package/packages/@monomind/cli/dist/src/browser/cdp.js +21 -4
- package/packages/@monomind/cli/dist/src/browser/console-log.d.ts +1 -0
- package/packages/@monomind/cli/dist/src/browser/console-log.js +19 -3
- package/packages/@monomind/cli/dist/src/browser/dialog.js +5 -2
- package/packages/@monomind/cli/dist/src/browser/find.js +28 -8
- package/packages/@monomind/cli/dist/src/browser/har.d.ts +1 -0
- package/packages/@monomind/cli/dist/src/browser/har.js +7 -5
- package/packages/@monomind/cli/dist/src/browser/network.d.ts +7 -4
- package/packages/@monomind/cli/dist/src/browser/network.js +60 -23
- package/packages/@monomind/cli/dist/src/browser/profiler.js +13 -2
- package/packages/@monomind/cli/dist/src/browser/screenshot.js +4 -2
- package/packages/@monomind/cli/dist/src/browser/session.js +49 -12
- package/packages/@monomind/cli/dist/src/browser/snapshot.js +26 -14
- package/packages/@monomind/cli/dist/src/browser/storage.d.ts +1 -0
- package/packages/@monomind/cli/dist/src/browser/storage.js +3 -0
- package/packages/@monomind/cli/dist/src/browser/tabs.d.ts +3 -3
- package/packages/@monomind/cli/dist/src/browser/tabs.js +13 -13
- package/packages/@monomind/cli/dist/src/browser/trace.js +10 -4
- package/packages/@monomind/cli/dist/src/browser/wait.js +24 -14
- package/packages/@monomind/cli/dist/src/commands/browse.js +300 -34
- package/packages/@monomind/cli/package.json +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { evaluateJs } from './actions.js';
|
|
2
2
|
import { getObjectIdForRef } from './snapshot.js';
|
|
3
3
|
export async function findBySelector(client, sessionId, refs, selector, options = {}) {
|
|
4
|
+
if (options.nth !== undefined && options.nth < 1) {
|
|
5
|
+
throw new Error(`nth must be >= 1 (received ${options.nth})`);
|
|
6
|
+
}
|
|
4
7
|
try {
|
|
5
8
|
const doc = await client.send('DOM.getDocument', {}, sessionId);
|
|
6
9
|
let targetNodeId;
|
|
@@ -25,14 +28,22 @@ export async function findBySelector(client, sessionId, refs, selector, options
|
|
|
25
28
|
const existing = [...refs.values()].find((r) => r.backendDOMNodeId === backendDOMNodeId);
|
|
26
29
|
if (existing)
|
|
27
30
|
return existing;
|
|
28
|
-
// Synthetic ref for elements not represented in the AX tree
|
|
29
|
-
|
|
31
|
+
// Synthetic ref for elements not represented in the AX tree — insert into refs so it can be used in subsequent commands
|
|
32
|
+
const syntheticRef = { ref: `sel-${backendDOMNodeId}`, role: 'generic', name: selector, nodeId: targetNodeId, backendDOMNodeId };
|
|
33
|
+
refs.set(syntheticRef.ref, syntheticRef);
|
|
34
|
+
return syntheticRef;
|
|
30
35
|
}
|
|
31
|
-
catch {
|
|
36
|
+
catch (err) {
|
|
37
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
38
|
+
if (/invalid|illegal|SyntaxError/i.test(msg))
|
|
39
|
+
throw err;
|
|
32
40
|
return null;
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
43
|
export async function findByRole(client, sessionId, refs, role, options = {}) {
|
|
44
|
+
if (options.nth !== undefined && options.nth < 1) {
|
|
45
|
+
throw new Error(`nth must be >= 1 (received ${options.nth})`);
|
|
46
|
+
}
|
|
36
47
|
const lowerRole = role.toLowerCase();
|
|
37
48
|
const candidates = [...refs.values()].filter((r) => r.role.toLowerCase() === lowerRole);
|
|
38
49
|
let matches = candidates;
|
|
@@ -51,18 +62,24 @@ export async function findByRole(client, sessionId, refs, role, options = {}) {
|
|
|
51
62
|
return matches[0];
|
|
52
63
|
}
|
|
53
64
|
export async function findByText(client, sessionId, refs, text, options = {}) {
|
|
65
|
+
if (options.nth !== undefined && options.nth < 1) {
|
|
66
|
+
throw new Error(`nth must be >= 1 (received ${options.nth})`);
|
|
67
|
+
}
|
|
54
68
|
const lower = text.toLowerCase();
|
|
55
69
|
const candidates = [...refs.values()].filter((r) => options.exact ? r.name.toLowerCase() === lower : r.name.toLowerCase().includes(lower));
|
|
56
70
|
if (options.nth !== undefined)
|
|
57
71
|
return candidates[options.nth - 1] ?? null;
|
|
58
72
|
if (options.last)
|
|
59
|
-
return candidates[candidates.length - 1];
|
|
73
|
+
return candidates[candidates.length - 1] ?? null;
|
|
60
74
|
return candidates[0] ?? null;
|
|
61
75
|
}
|
|
62
76
|
export async function findByLabel(client, sessionId, refs, label, options = {}) {
|
|
63
77
|
return findByText(client, sessionId, refs, label, options);
|
|
64
78
|
}
|
|
65
79
|
export async function findByPlaceholder(client, sessionId, refs, placeholder, options = {}) {
|
|
80
|
+
if (options.nth !== undefined && options.nth < 1) {
|
|
81
|
+
throw new Error(`nth must be >= 1 (received ${options.nth})`);
|
|
82
|
+
}
|
|
66
83
|
const lower = placeholder.toLowerCase();
|
|
67
84
|
const candidates = [...refs.values()].filter((r) => {
|
|
68
85
|
const ph = (r.placeholder ?? '').toLowerCase();
|
|
@@ -71,14 +88,15 @@ export async function findByPlaceholder(client, sessionId, refs, placeholder, op
|
|
|
71
88
|
if (options.nth !== undefined)
|
|
72
89
|
return candidates[options.nth - 1] ?? null;
|
|
73
90
|
if (options.last)
|
|
74
|
-
return candidates[candidates.length - 1];
|
|
91
|
+
return candidates[candidates.length - 1] ?? null;
|
|
75
92
|
return candidates[0] ?? null;
|
|
76
93
|
}
|
|
77
94
|
export async function findByTestId(client, sessionId, testId) {
|
|
95
|
+
const escapedId = testId.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
78
96
|
const selectors = [
|
|
79
|
-
`[data-testid="${
|
|
80
|
-
`[data-test-id="${
|
|
81
|
-
`[data-test="${
|
|
97
|
+
`[data-testid="${escapedId}"]`,
|
|
98
|
+
`[data-test-id="${escapedId}"]`,
|
|
99
|
+
`[data-test="${escapedId}"]`,
|
|
82
100
|
];
|
|
83
101
|
for (const sel of selectors) {
|
|
84
102
|
const result = await evaluateJs(client, sessionId, `!!document.querySelector(${JSON.stringify(sel)})`);
|
|
@@ -126,6 +144,7 @@ export async function scrollIntoView(client, sessionId, ref) {
|
|
|
126
144
|
await client.send('Runtime.callFunctionOn', {
|
|
127
145
|
functionDeclaration: 'function() { this.scrollIntoView({ behavior: "smooth", block: "center" }); }',
|
|
128
146
|
objectId,
|
|
147
|
+
returnByValue: true,
|
|
129
148
|
}, sessionId);
|
|
130
149
|
}
|
|
131
150
|
export async function highlightElement(client, sessionId, ref) {
|
|
@@ -144,6 +163,7 @@ export async function highlightElement(client, sessionId, ref) {
|
|
|
144
163
|
}, 2000);
|
|
145
164
|
}`,
|
|
146
165
|
objectId,
|
|
166
|
+
returnByValue: true,
|
|
147
167
|
}, sessionId);
|
|
148
168
|
}
|
|
149
169
|
//# sourceMappingURL=find.js.map
|
|
@@ -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.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 =
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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,15 +190,23 @@ 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(/[-[\]{}()
|
|
170
|
-
.replace(/\*\*/g, '
|
|
171
|
-
.replace(/\*/g, '[^/]*')
|
|
172
|
-
|
|
204
|
+
.replace(/[-[\]{}()+.,\\^$|#\s]/g, '\\$&')
|
|
205
|
+
.replace(/\*\*/g, '\x00')
|
|
206
|
+
.replace(/\*/g, '[^/]*')
|
|
207
|
+
.replace(/\x00/g, '.*')
|
|
208
|
+
.replace(/\?/g, '[^/]');
|
|
209
|
+
return new RegExp(`^${escaped}$`);
|
|
173
210
|
}
|
|
174
211
|
function globToFetchPattern(pattern) {
|
|
175
212
|
return pattern.replace(/\*\*/g, '*');
|
|
@@ -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';
|
|
@@ -14,11 +14,13 @@ export async function captureScreenshot(client, sessionId, options = {}) {
|
|
|
14
14
|
}, sessionId);
|
|
15
15
|
const { w, h } = JSON.parse(dims.result?.value ?? '{"w":1280,"h":720}');
|
|
16
16
|
params.clip = { x: 0, y: 0, width: w, height: h, scale: 1 };
|
|
17
|
+
params.captureBeyondViewport = true;
|
|
17
18
|
}
|
|
18
19
|
const result = await client.send('Page.captureScreenshot', params, sessionId);
|
|
19
20
|
const data = result.data;
|
|
20
21
|
const dataUrl = `data:image/${format};base64,${data}`;
|
|
21
22
|
const outputPath = options.path ?? join(tmpdir(), `monomind-screenshot-${Date.now()}.${format}`);
|
|
23
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
22
24
|
await writeFile(outputPath, Buffer.from(data, 'base64'));
|
|
23
25
|
return { path: outputPath, dataUrl };
|
|
24
26
|
}
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
-
import {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
103
|
-
const
|
|
104
|
-
|
|
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
|
-
|
|
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]) /
|
|
160
|
-
const y = (content[1] + content[5]) /
|
|
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,
|
|
6
|
-
export declare function activateTab(client: CdpClient,
|
|
7
|
-
export declare function switchToFrame(
|
|
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
|
+
export declare function switchToFrame(_client: CdpClient, _sessionId: string, _frameSelector: string): Promise<string | null>;
|
|
8
8
|
//# sourceMappingURL=tabs.d.ts.map
|