monomind 1.10.34 → 1.10.35
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/.claude/skills/monomind/.monomind/registry.json +6 -0
- package/package.json +1 -1
- package/packages/@monomind/cli/dist/src/browser/find.d.ts +1 -0
- package/packages/@monomind/cli/dist/src/browser/find.js +12 -0
- package/packages/@monomind/cli/dist/src/browser/har.d.ts +26 -0
- package/packages/@monomind/cli/dist/src/browser/har.js +130 -0
- package/packages/@monomind/cli/dist/src/browser/index.d.ts +5 -0
- package/packages/@monomind/cli/dist/src/browser/index.js +5 -0
- package/packages/@monomind/cli/dist/src/browser/network.d.ts +15 -0
- package/packages/@monomind/cli/dist/src/browser/network.js +54 -0
- package/packages/@monomind/cli/dist/src/browser/profiler.d.ts +10 -0
- package/packages/@monomind/cli/dist/src/browser/profiler.js +55 -0
- package/packages/@monomind/cli/dist/src/browser/record.d.ts +21 -0
- package/packages/@monomind/cli/dist/src/browser/record.js +48 -0
- package/packages/@monomind/cli/dist/src/browser/snapshot.js +23 -2
- package/packages/@monomind/cli/dist/src/browser/trace.d.ts +10 -0
- package/packages/@monomind/cli/dist/src/browser/trace.js +72 -0
- package/packages/@monomind/cli/dist/src/browser/types.d.ts +1 -0
- package/packages/@monomind/cli/dist/src/browser/vitals.d.ts +15 -0
- package/packages/@monomind/cli/dist/src/browser/vitals.js +116 -0
- package/packages/@monomind/cli/dist/src/commands/browse.js +415 -13
- package/packages/@monomind/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monomind",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.35",
|
|
4
4
|
"description": "Monomind - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -12,6 +12,7 @@ export declare function findBySelector(client: CdpClient, sessionId: string, sel
|
|
|
12
12
|
export declare function findByRole(client: CdpClient, sessionId: string, refs: Map<string, ElementRef>, role: string, options?: FindOptions): Promise<ElementRef | null>;
|
|
13
13
|
export declare function findByText(client: CdpClient, sessionId: string, refs: Map<string, ElementRef>, text: string, options?: FindOptions): Promise<ElementRef | null>;
|
|
14
14
|
export declare function findByLabel(client: CdpClient, sessionId: string, refs: Map<string, ElementRef>, label: string, options?: FindOptions): Promise<ElementRef | null>;
|
|
15
|
+
export declare function findByPlaceholder(client: CdpClient, sessionId: string, refs: Map<string, ElementRef>, placeholder: string, options?: FindOptions): Promise<ElementRef | null>;
|
|
15
16
|
export declare function findByTestId(client: CdpClient, sessionId: string, testId: string): Promise<string | null>;
|
|
16
17
|
export declare function isVisible(client: CdpClient, sessionId: string, ref: ElementRef): Promise<boolean>;
|
|
17
18
|
export declare function isEnabled(client: CdpClient, sessionId: string, ref: ElementRef): Promise<boolean>;
|
|
@@ -43,6 +43,18 @@ export async function findByText(client, sessionId, refs, text, options = {}) {
|
|
|
43
43
|
export async function findByLabel(client, sessionId, refs, label, options = {}) {
|
|
44
44
|
return findByText(client, sessionId, refs, label, options);
|
|
45
45
|
}
|
|
46
|
+
export async function findByPlaceholder(client, sessionId, refs, placeholder, options = {}) {
|
|
47
|
+
const lower = placeholder.toLowerCase();
|
|
48
|
+
const candidates = [...refs.values()].filter((r) => {
|
|
49
|
+
const desc = (r.description ?? '').toLowerCase();
|
|
50
|
+
return options.exact ? desc === lower : desc.includes(lower);
|
|
51
|
+
});
|
|
52
|
+
if (options.nth !== undefined)
|
|
53
|
+
return candidates[options.nth - 1] ?? null;
|
|
54
|
+
if (options.last)
|
|
55
|
+
return candidates[candidates.length - 1];
|
|
56
|
+
return candidates[0] ?? null;
|
|
57
|
+
}
|
|
46
58
|
export async function findByTestId(client, sessionId, testId) {
|
|
47
59
|
const selectors = [
|
|
48
60
|
`[data-testid="${testId}"]`,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
interface HarRequest {
|
|
3
|
+
id: string;
|
|
4
|
+
url: string;
|
|
5
|
+
method: string;
|
|
6
|
+
status: number;
|
|
7
|
+
statusText: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
requestHeaders: Record<string, string>;
|
|
10
|
+
responseHeaders: Record<string, string>;
|
|
11
|
+
startTime: number;
|
|
12
|
+
endTime: number;
|
|
13
|
+
size: number;
|
|
14
|
+
encodedSize: number;
|
|
15
|
+
fromCache: boolean;
|
|
16
|
+
responseBody?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function startHarRecording(client: CdpClient, sessionId: string): Promise<void>;
|
|
19
|
+
export declare function stopHarRecording(client: CdpClient, sessionId: string, outputPath?: string, captureResponseBodies?: boolean): Promise<string>;
|
|
20
|
+
export declare function getHarStatus(sessionId: string): {
|
|
21
|
+
recording: boolean;
|
|
22
|
+
requestCount: number;
|
|
23
|
+
};
|
|
24
|
+
export declare function getRequests(sessionId: string): Partial<HarRequest>[];
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=har.d.ts.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { writeFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
const _sessions = new Map();
|
|
5
|
+
export async function startHarRecording(client, sessionId) {
|
|
6
|
+
if (_sessions.has(sessionId))
|
|
7
|
+
throw new Error('HAR recording already in progress');
|
|
8
|
+
const requests = new Map();
|
|
9
|
+
const startTime = Date.now();
|
|
10
|
+
const offReq = client.on('Network.requestWillBeSent', (params, sid) => {
|
|
11
|
+
if (sid !== sessionId)
|
|
12
|
+
return;
|
|
13
|
+
const p = params;
|
|
14
|
+
requests.set(p.requestId, {
|
|
15
|
+
id: p.requestId,
|
|
16
|
+
url: p.request.url,
|
|
17
|
+
method: p.request.method,
|
|
18
|
+
requestHeaders: p.request.headers,
|
|
19
|
+
startTime: p.timestamp * 1000,
|
|
20
|
+
fromCache: false,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
const offResp = client.on('Network.responseReceived', (params, sid) => {
|
|
24
|
+
if (sid !== sessionId)
|
|
25
|
+
return;
|
|
26
|
+
const p = params;
|
|
27
|
+
const entry = requests.get(p.requestId);
|
|
28
|
+
if (entry) {
|
|
29
|
+
entry.status = p.response.status;
|
|
30
|
+
entry.statusText = p.response.statusText;
|
|
31
|
+
entry.mimeType = p.response.mimeType;
|
|
32
|
+
entry.responseHeaders = p.response.headers;
|
|
33
|
+
entry.fromCache = p.response.fromDiskCache || p.response.fromServiceWorker;
|
|
34
|
+
entry.endTime = p.timestamp * 1000;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
const offFinished = client.on('Network.loadingFinished', (params, sid) => {
|
|
38
|
+
if (sid !== sessionId)
|
|
39
|
+
return;
|
|
40
|
+
const p = params;
|
|
41
|
+
const entry = requests.get(p.requestId);
|
|
42
|
+
if (entry) {
|
|
43
|
+
entry.encodedSize = p.encodedDataLength;
|
|
44
|
+
entry.endTime = p.timestamp * 1000;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
_sessions.set(sessionId, { requests, offReq, offResp, offFinished, startTime });
|
|
48
|
+
}
|
|
49
|
+
export async function stopHarRecording(client, sessionId, outputPath, captureResponseBodies = false) {
|
|
50
|
+
const state = _sessions.get(sessionId);
|
|
51
|
+
if (!state)
|
|
52
|
+
throw new Error('No active HAR recording for this session');
|
|
53
|
+
state.offReq();
|
|
54
|
+
state.offResp();
|
|
55
|
+
state.offFinished();
|
|
56
|
+
// Optionally fetch response bodies
|
|
57
|
+
if (captureResponseBodies) {
|
|
58
|
+
for (const [reqId, entry] of state.requests.entries()) {
|
|
59
|
+
try {
|
|
60
|
+
const body = await client.send('Network.getResponseBody', { requestId: reqId }, sessionId);
|
|
61
|
+
entry.responseBody = body.base64Encoded
|
|
62
|
+
? Buffer.from(body.body, 'base64').toString('utf8')
|
|
63
|
+
: body.body;
|
|
64
|
+
entry.size = entry.responseBody.length;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// body may not be available for cached/redirected responses
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
_sessions.delete(sessionId);
|
|
72
|
+
const har = buildHar(Array.from(state.requests.values()), state.startTime);
|
|
73
|
+
const path = outputPath ?? join(tmpdir(), `monomind-har-${Date.now()}.har`);
|
|
74
|
+
await writeFile(path, JSON.stringify(har, null, 2));
|
|
75
|
+
return path;
|
|
76
|
+
}
|
|
77
|
+
export function getHarStatus(sessionId) {
|
|
78
|
+
const state = _sessions.get(sessionId);
|
|
79
|
+
return { recording: !!state, requestCount: state?.requests.size ?? 0 };
|
|
80
|
+
}
|
|
81
|
+
export function getRequests(sessionId) {
|
|
82
|
+
const state = _sessions.get(sessionId);
|
|
83
|
+
return state ? Array.from(state.requests.values()) : [];
|
|
84
|
+
}
|
|
85
|
+
function buildHar(entries, startTime) {
|
|
86
|
+
return {
|
|
87
|
+
log: {
|
|
88
|
+
version: '1.2',
|
|
89
|
+
creator: { name: 'monomind browse', version: '1.0.0' },
|
|
90
|
+
pages: [{
|
|
91
|
+
startedDateTime: new Date(startTime).toISOString(),
|
|
92
|
+
id: 'page_1',
|
|
93
|
+
title: '',
|
|
94
|
+
pageTimings: {},
|
|
95
|
+
}],
|
|
96
|
+
entries: entries.map((e) => ({
|
|
97
|
+
startedDateTime: new Date(e.startTime ?? startTime).toISOString(),
|
|
98
|
+
time: (e.endTime ?? e.startTime ?? startTime) - (e.startTime ?? startTime),
|
|
99
|
+
request: {
|
|
100
|
+
method: e.method ?? 'GET',
|
|
101
|
+
url: e.url ?? '',
|
|
102
|
+
httpVersion: 'HTTP/1.1',
|
|
103
|
+
cookies: [],
|
|
104
|
+
headers: Object.entries(e.requestHeaders ?? {}).map(([name, value]) => ({ name, value })),
|
|
105
|
+
queryString: [],
|
|
106
|
+
headersSize: -1,
|
|
107
|
+
bodySize: -1,
|
|
108
|
+
},
|
|
109
|
+
response: {
|
|
110
|
+
status: e.status ?? 0,
|
|
111
|
+
statusText: e.statusText ?? '',
|
|
112
|
+
httpVersion: 'HTTP/1.1',
|
|
113
|
+
cookies: [],
|
|
114
|
+
headers: Object.entries(e.responseHeaders ?? {}).map(([name, value]) => ({ name, value })),
|
|
115
|
+
content: {
|
|
116
|
+
size: e.size ?? -1,
|
|
117
|
+
mimeType: e.mimeType ?? 'application/octet-stream',
|
|
118
|
+
text: e.responseBody,
|
|
119
|
+
},
|
|
120
|
+
redirectURL: '',
|
|
121
|
+
headersSize: -1,
|
|
122
|
+
bodySize: e.encodedSize ?? -1,
|
|
123
|
+
},
|
|
124
|
+
cache: {},
|
|
125
|
+
timings: { send: 0, wait: 0, receive: 0 },
|
|
126
|
+
})),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=har.js.map
|
|
@@ -15,4 +15,9 @@ export * from './find.js';
|
|
|
15
15
|
export * from './pdf.js';
|
|
16
16
|
export * from './emulation.js';
|
|
17
17
|
export * from './batch.js';
|
|
18
|
+
export * from './record.js';
|
|
19
|
+
export * from './trace.js';
|
|
20
|
+
export * from './profiler.js';
|
|
21
|
+
export * from './vitals.js';
|
|
22
|
+
export * from './har.js';
|
|
18
23
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -15,4 +15,9 @@ export * from './find.js';
|
|
|
15
15
|
export * from './pdf.js';
|
|
16
16
|
export * from './emulation.js';
|
|
17
17
|
export * from './batch.js';
|
|
18
|
+
export * from './record.js';
|
|
19
|
+
export * from './trace.js';
|
|
20
|
+
export * from './profiler.js';
|
|
21
|
+
export * from './vitals.js';
|
|
22
|
+
export * from './har.js';
|
|
18
23
|
//# sourceMappingURL=index.js.map
|
|
@@ -6,6 +6,21 @@ 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): {
|
|
12
|
+
url: string;
|
|
13
|
+
method: string;
|
|
14
|
+
status?: number;
|
|
15
|
+
mimeType?: string;
|
|
16
|
+
requestHeaders?: Record<string, string>;
|
|
17
|
+
responseHeaders?: Record<string, string>;
|
|
18
|
+
startTime: number;
|
|
19
|
+
endTime?: number;
|
|
20
|
+
encodedSize?: number;
|
|
21
|
+
}[];
|
|
22
|
+
export declare function clearCapturedRequests(sessionId: string): void;
|
|
23
|
+
export declare function disableInterception(client: CdpClient, sessionId: string): Promise<void>;
|
|
9
24
|
export declare function getLocalStorage(client: CdpClient, sessionId: string): Promise<Record<string, string>>;
|
|
10
25
|
export declare function setLocalStorage(client: CdpClient, sessionId: string, data: Record<string, string>): Promise<void>;
|
|
11
26
|
//# sourceMappingURL=network.d.ts.map
|
|
@@ -50,6 +50,60 @@ export async function setupRoutes(client, sessionId, routes) {
|
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
+
const _capturedRequests = new Map();
|
|
54
|
+
const _captureListeners = new Map();
|
|
55
|
+
export function startRequestCapture(client, sessionId) {
|
|
56
|
+
if (_capturedRequests.has(sessionId))
|
|
57
|
+
return;
|
|
58
|
+
const list = [];
|
|
59
|
+
_capturedRequests.set(sessionId, list);
|
|
60
|
+
const offReq = client.on('Network.requestWillBeSent', (params, sid) => {
|
|
61
|
+
if (sid !== sessionId)
|
|
62
|
+
return;
|
|
63
|
+
const p = params;
|
|
64
|
+
list.push({ url: p.request.url, method: p.request.method, requestHeaders: p.request.headers, startTime: p.timestamp * 1000 });
|
|
65
|
+
});
|
|
66
|
+
const offResp = client.on('Network.responseReceived', (params, sid) => {
|
|
67
|
+
if (sid !== sessionId)
|
|
68
|
+
return;
|
|
69
|
+
const p = params;
|
|
70
|
+
const entry = list.find((r) => r.url === p.response.url && !r.status);
|
|
71
|
+
if (entry) {
|
|
72
|
+
entry.status = p.response.status;
|
|
73
|
+
entry.mimeType = p.response.mimeType;
|
|
74
|
+
entry.responseHeaders = p.response.headers;
|
|
75
|
+
entry.endTime = p.timestamp * 1000;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const offFinished = client.on('Network.loadingFinished', (params, sid) => {
|
|
79
|
+
if (sid !== sessionId)
|
|
80
|
+
return;
|
|
81
|
+
const p = params;
|
|
82
|
+
const entry = list[list.length - 1];
|
|
83
|
+
if (entry) {
|
|
84
|
+
entry.encodedSize = p.encodedDataLength;
|
|
85
|
+
entry.endTime = p.timestamp * 1000;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
_captureListeners.set(sessionId, [offReq, offResp, offFinished]);
|
|
89
|
+
}
|
|
90
|
+
export function stopRequestCapture(sessionId) {
|
|
91
|
+
const offs = _captureListeners.get(sessionId);
|
|
92
|
+
if (offs) {
|
|
93
|
+
for (const off of offs)
|
|
94
|
+
off();
|
|
95
|
+
_captureListeners.delete(sessionId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function getCapturedRequests(sessionId) {
|
|
99
|
+
return _capturedRequests.get(sessionId) ?? [];
|
|
100
|
+
}
|
|
101
|
+
export function clearCapturedRequests(sessionId) {
|
|
102
|
+
_capturedRequests.set(sessionId, []);
|
|
103
|
+
}
|
|
104
|
+
export async function disableInterception(client, sessionId) {
|
|
105
|
+
await client.send('Fetch.disable', {}, sessionId);
|
|
106
|
+
}
|
|
53
107
|
export async function getLocalStorage(client, sessionId) {
|
|
54
108
|
const result = await client.send('Runtime.evaluate', {
|
|
55
109
|
expression: 'JSON.stringify(Object.fromEntries(Object.entries(localStorage)))',
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
export interface ProfilerOptions {
|
|
3
|
+
path?: string;
|
|
4
|
+
samplingInterval?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function startCpuProfile(client: CdpClient, sessionId: string, options?: ProfilerOptions): Promise<void>;
|
|
7
|
+
export declare function stopCpuProfile(client: CdpClient, sessionId: string, outputPath?: string): Promise<string>;
|
|
8
|
+
export declare function isProfilingActive(sessionId: string): boolean;
|
|
9
|
+
export declare function startHeapSnapshot(client: CdpClient, sessionId: string, outputPath?: string): Promise<string>;
|
|
10
|
+
//# sourceMappingURL=profiler.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { writeFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
const _sessions = new Set();
|
|
5
|
+
export async function startCpuProfile(client, sessionId, options = {}) {
|
|
6
|
+
if (_sessions.has(sessionId)) {
|
|
7
|
+
throw new Error('CPU profiler already running for this session');
|
|
8
|
+
}
|
|
9
|
+
await client.send('Profiler.enable', {}, sessionId);
|
|
10
|
+
if (options.samplingInterval !== undefined) {
|
|
11
|
+
await client.send('Profiler.setSamplingInterval', { interval: options.samplingInterval }, sessionId);
|
|
12
|
+
}
|
|
13
|
+
await client.send('Profiler.start', {}, sessionId);
|
|
14
|
+
_sessions.add(sessionId);
|
|
15
|
+
}
|
|
16
|
+
export async function stopCpuProfile(client, sessionId, outputPath) {
|
|
17
|
+
if (!_sessions.has(sessionId)) {
|
|
18
|
+
throw new Error('No active CPU profiler for this session');
|
|
19
|
+
}
|
|
20
|
+
const result = await client.send('Profiler.stop', {}, sessionId);
|
|
21
|
+
await client.send('Profiler.disable', {}, sessionId);
|
|
22
|
+
_sessions.delete(sessionId);
|
|
23
|
+
const path = outputPath ?? join(tmpdir(), `monomind-profile-${Date.now()}.cpuprofile`);
|
|
24
|
+
await writeFile(path, JSON.stringify(result.profile));
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
export function isProfilingActive(sessionId) {
|
|
28
|
+
return _sessions.has(sessionId);
|
|
29
|
+
}
|
|
30
|
+
export async function startHeapSnapshot(client, sessionId, outputPath) {
|
|
31
|
+
await client.send('HeapProfiler.enable', {}, sessionId);
|
|
32
|
+
const chunks = [];
|
|
33
|
+
const off = client.on('HeapProfiler.addHeapSnapshotChunk', (params, sid) => {
|
|
34
|
+
if (sid !== sessionId)
|
|
35
|
+
return;
|
|
36
|
+
chunks.push(params.chunk);
|
|
37
|
+
});
|
|
38
|
+
await new Promise((resolve) => {
|
|
39
|
+
const off2 = client.on('HeapProfiler.reportHeapSnapshotProgress', (params, sid) => {
|
|
40
|
+
if (sid !== sessionId)
|
|
41
|
+
return;
|
|
42
|
+
if (params.finished) {
|
|
43
|
+
off2();
|
|
44
|
+
resolve();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
client.send('HeapProfiler.takeHeapSnapshot', { reportProgress: true }, sessionId).catch(() => { });
|
|
48
|
+
});
|
|
49
|
+
off();
|
|
50
|
+
await client.send('HeapProfiler.disable', {}, sessionId);
|
|
51
|
+
const path = outputPath ?? join(tmpdir(), `monomind-heap-${Date.now()}.heapsnapshot`);
|
|
52
|
+
await writeFile(path, chunks.join(''));
|
|
53
|
+
return path;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=profiler.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
export interface RecordOptions {
|
|
3
|
+
path?: string;
|
|
4
|
+
format?: 'jpeg' | 'png' | 'webp';
|
|
5
|
+
quality?: number;
|
|
6
|
+
everyNthFrame?: number;
|
|
7
|
+
maxWidth?: number;
|
|
8
|
+
maxHeight?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface RecordingState {
|
|
11
|
+
frames: string[];
|
|
12
|
+
offScreencast: (() => void) | null;
|
|
13
|
+
}
|
|
14
|
+
export declare function startRecording(client: CdpClient, sessionId: string, options?: RecordOptions): Promise<void>;
|
|
15
|
+
export declare function stopRecording(client: CdpClient, sessionId: string, outputPath?: string): Promise<string>;
|
|
16
|
+
export declare function getRecordingStatus(sessionId: string): {
|
|
17
|
+
recording: boolean;
|
|
18
|
+
frames: number;
|
|
19
|
+
};
|
|
20
|
+
export declare function saveFrameAsPng(client: CdpClient, sessionId: string, frameIndex: number, outputPath: string): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=record.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { writeFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
const _sessions = new Map();
|
|
5
|
+
export async function startRecording(client, sessionId, options = {}) {
|
|
6
|
+
if (_sessions.has(sessionId)) {
|
|
7
|
+
throw new Error('Recording already in progress for this session');
|
|
8
|
+
}
|
|
9
|
+
const state = { frames: [], offScreencast: null };
|
|
10
|
+
_sessions.set(sessionId, state);
|
|
11
|
+
state.offScreencast = client.on('Page.screencastFrame', async (params, sid) => {
|
|
12
|
+
if (sid !== sessionId)
|
|
13
|
+
return;
|
|
14
|
+
const { data, sessionId: frameSessionId } = params;
|
|
15
|
+
state.frames.push(data);
|
|
16
|
+
await client.send('Page.screencastFrameAck', { sessionId: frameSessionId }, sessionId).catch(() => { });
|
|
17
|
+
});
|
|
18
|
+
await client.send('Page.startScreencast', {
|
|
19
|
+
format: options.format ?? 'jpeg',
|
|
20
|
+
quality: options.quality ?? 80,
|
|
21
|
+
everyNthFrame: options.everyNthFrame ?? 1,
|
|
22
|
+
...(options.maxWidth ? { maxWidth: options.maxWidth } : {}),
|
|
23
|
+
...(options.maxHeight ? { maxHeight: options.maxHeight } : {}),
|
|
24
|
+
}, sessionId);
|
|
25
|
+
}
|
|
26
|
+
export async function stopRecording(client, sessionId, outputPath) {
|
|
27
|
+
const state = _sessions.get(sessionId);
|
|
28
|
+
if (!state)
|
|
29
|
+
throw new Error('No active recording for this session');
|
|
30
|
+
await client.send('Page.stopScreencast', {}, sessionId);
|
|
31
|
+
state.offScreencast?.();
|
|
32
|
+
_sessions.delete(sessionId);
|
|
33
|
+
const path = outputPath ?? join(tmpdir(), `monomind-screencast-${Date.now()}.frames.json`);
|
|
34
|
+
await writeFile(path, JSON.stringify({ frameCount: state.frames.length, frames: state.frames }));
|
|
35
|
+
return path;
|
|
36
|
+
}
|
|
37
|
+
export function getRecordingStatus(sessionId) {
|
|
38
|
+
const state = _sessions.get(sessionId);
|
|
39
|
+
return { recording: !!state, frames: state?.frames.length ?? 0 };
|
|
40
|
+
}
|
|
41
|
+
export async function saveFrameAsPng(client, sessionId, frameIndex, outputPath) {
|
|
42
|
+
const state = _sessions.get(sessionId);
|
|
43
|
+
if (!state || frameIndex >= state.frames.length) {
|
|
44
|
+
throw new Error(`Frame ${frameIndex} not available`);
|
|
45
|
+
}
|
|
46
|
+
await writeFile(outputPath, Buffer.from(state.frames[frameIndex], 'base64'));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=record.js.map
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { INTERACTIVE_ROLES } from './types.js';
|
|
2
2
|
import { getCurrentUrl, getCurrentTitle } from './browser.js';
|
|
3
3
|
export async function captureSnapshot(client, sessionId, options = {}) {
|
|
4
|
-
const { interactiveOnly = false, compact = false } = options;
|
|
5
|
-
|
|
4
|
+
const { interactiveOnly = false, compact = false, maxDepth, selector } = options;
|
|
5
|
+
// If a selector scope is requested, resolve it to a backendDOMNodeId and use getPartialAXTree
|
|
6
|
+
let nodes;
|
|
7
|
+
if (selector) {
|
|
8
|
+
const nodeResult = await client.send('Runtime.evaluate', { expression: `document.querySelector(${JSON.stringify(selector)})?.getAttribute('data-ax-node-id') ?? null`, returnByValue: true }, sessionId);
|
|
9
|
+
// Use DOM.querySelector to find the backend node
|
|
10
|
+
const doc = await client.send('DOM.getDocument', {}, sessionId);
|
|
11
|
+
const found = await client.send('DOM.querySelector', { nodeId: doc.root.nodeId, selector }, sessionId).catch(() => ({ nodeId: 0 }));
|
|
12
|
+
if (found.nodeId) {
|
|
13
|
+
const partial = await client.send('Accessibility.getPartialAXTree', { nodeId: found.nodeId, fetchRelatives: true }, sessionId).catch(async () => client.send('Accessibility.getFullAXTree', {}, sessionId));
|
|
14
|
+
nodes = partial.nodes;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const full = await client.send('Accessibility.getFullAXTree', {}, sessionId);
|
|
18
|
+
nodes = full.nodes;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const full = await client.send('Accessibility.getFullAXTree', {}, sessionId);
|
|
23
|
+
nodes = full.nodes;
|
|
24
|
+
}
|
|
6
25
|
const url = await getCurrentUrl(client, sessionId);
|
|
7
26
|
const title = await getCurrentTitle(client, sessionId);
|
|
8
27
|
const refs = new Map();
|
|
@@ -14,6 +33,8 @@ export async function captureSnapshot(client, sessionId, options = {}) {
|
|
|
14
33
|
const processNode = (node, depth) => {
|
|
15
34
|
if (node.ignored)
|
|
16
35
|
return;
|
|
36
|
+
if (maxDepth !== undefined && depth > maxDepth)
|
|
37
|
+
return;
|
|
17
38
|
const role = node.role?.value ?? 'generic';
|
|
18
39
|
const name = node.name?.value ?? '';
|
|
19
40
|
const description = node.description?.value;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
export interface TraceOptions {
|
|
3
|
+
path?: string;
|
|
4
|
+
categories?: string[];
|
|
5
|
+
screenshots?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function startTrace(client: CdpClient, sessionId: string, options?: TraceOptions): Promise<void>;
|
|
8
|
+
export declare function stopTrace(client: CdpClient, sessionId: string, outputPath?: string): Promise<string>;
|
|
9
|
+
export declare function getTraceStatus(sessionId: string): boolean;
|
|
10
|
+
//# sourceMappingURL=trace.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { writeFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
const DEFAULT_CATEGORIES = [
|
|
5
|
+
'-*',
|
|
6
|
+
'devtools.timeline',
|
|
7
|
+
'v8.execute',
|
|
8
|
+
'disabled-by-default-devtools.timeline',
|
|
9
|
+
'disabled-by-default-devtools.timeline.frame',
|
|
10
|
+
'toplevel',
|
|
11
|
+
'blink.console',
|
|
12
|
+
'blink.user_timing',
|
|
13
|
+
'latencyInfo',
|
|
14
|
+
'disabled-by-default-devtools.timeline.stack',
|
|
15
|
+
];
|
|
16
|
+
const _sessions = new Map();
|
|
17
|
+
export async function startTrace(client, sessionId, options = {}) {
|
|
18
|
+
if (_sessions.has(sessionId)) {
|
|
19
|
+
throw new Error('Trace already in progress for this session');
|
|
20
|
+
}
|
|
21
|
+
const events = [];
|
|
22
|
+
const offData = client.on('Tracing.dataCollected', (params, sid) => {
|
|
23
|
+
if (sid !== sessionId)
|
|
24
|
+
return;
|
|
25
|
+
const value = params.value;
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
events.push(...value);
|
|
28
|
+
});
|
|
29
|
+
const offComplete = client.on('Tracing.tracingComplete', (_params, sid) => {
|
|
30
|
+
if (sid !== sessionId)
|
|
31
|
+
return;
|
|
32
|
+
// Will be resolved by stopTrace
|
|
33
|
+
});
|
|
34
|
+
_sessions.set(sessionId, { events, offData, offComplete });
|
|
35
|
+
const cats = options.categories ?? DEFAULT_CATEGORIES;
|
|
36
|
+
if (options.screenshots)
|
|
37
|
+
cats.push('disabled-by-default-devtools.screenshot');
|
|
38
|
+
await client.send('Tracing.start', {
|
|
39
|
+
traceConfig: {
|
|
40
|
+
includedCategories: cats.filter((c) => !c.startsWith('-')),
|
|
41
|
+
excludedCategories: cats.filter((c) => c.startsWith('-')).map((c) => c.slice(1)),
|
|
42
|
+
},
|
|
43
|
+
}, sessionId);
|
|
44
|
+
}
|
|
45
|
+
export async function stopTrace(client, sessionId, outputPath) {
|
|
46
|
+
const state = _sessions.get(sessionId);
|
|
47
|
+
if (!state)
|
|
48
|
+
throw new Error('No active trace for this session');
|
|
49
|
+
await new Promise((resolve) => {
|
|
50
|
+
const off = client.on('Tracing.tracingComplete', (_params, sid) => {
|
|
51
|
+
if (sid !== sessionId)
|
|
52
|
+
return;
|
|
53
|
+
off();
|
|
54
|
+
resolve();
|
|
55
|
+
});
|
|
56
|
+
client.send('Tracing.end', {}, sessionId).catch(() => { });
|
|
57
|
+
});
|
|
58
|
+
state.offData();
|
|
59
|
+
state.offComplete();
|
|
60
|
+
_sessions.delete(sessionId);
|
|
61
|
+
const trace = {
|
|
62
|
+
traceEvents: state.events,
|
|
63
|
+
metadata: { 'clock-offset-since-epoch': Date.now() },
|
|
64
|
+
};
|
|
65
|
+
const path = outputPath ?? join(tmpdir(), `monomind-trace-${Date.now()}.json`);
|
|
66
|
+
await writeFile(path, JSON.stringify(trace));
|
|
67
|
+
return path;
|
|
68
|
+
}
|
|
69
|
+
export function getTraceStatus(sessionId) {
|
|
70
|
+
return _sessions.has(sessionId);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=trace.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
export interface WebVitals {
|
|
3
|
+
lcp?: number;
|
|
4
|
+
fcp?: number;
|
|
5
|
+
cls?: number;
|
|
6
|
+
ttfb?: number;
|
|
7
|
+
inp?: number;
|
|
8
|
+
domInteractive?: number;
|
|
9
|
+
domContentLoaded?: number;
|
|
10
|
+
loadTime?: number;
|
|
11
|
+
resources?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function collectVitals(client: CdpClient, sessionId: string, waitMs?: number): Promise<WebVitals>;
|
|
14
|
+
export declare function formatVitals(vitals: WebVitals): string;
|
|
15
|
+
//# sourceMappingURL=vitals.d.ts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { evaluateJs } from './actions.js';
|
|
2
|
+
export async function collectVitals(client, sessionId, waitMs = 2000) {
|
|
3
|
+
// Inject PerformanceObserver collectors and wait for data
|
|
4
|
+
const script = `
|
|
5
|
+
new Promise((resolve) => {
|
|
6
|
+
const vitals = {};
|
|
7
|
+
|
|
8
|
+
// Navigation timing
|
|
9
|
+
const nav = performance.getEntriesByType('navigation')[0];
|
|
10
|
+
if (nav) {
|
|
11
|
+
vitals.ttfb = nav.responseStart - nav.requestStart;
|
|
12
|
+
vitals.domInteractive = nav.domInteractive;
|
|
13
|
+
vitals.domContentLoaded = nav.domContentLoadedEventEnd;
|
|
14
|
+
vitals.loadTime = nav.loadEventEnd;
|
|
15
|
+
vitals.resources = performance.getEntriesByType('resource').length;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// FCP via paint entries
|
|
19
|
+
const paintEntries = performance.getEntriesByType('paint');
|
|
20
|
+
const fcp = paintEntries.find(e => e.name === 'first-contentful-paint');
|
|
21
|
+
if (fcp) vitals.fcp = fcp.startTime;
|
|
22
|
+
|
|
23
|
+
// LCP
|
|
24
|
+
let lcpValue = 0;
|
|
25
|
+
let lcpObs;
|
|
26
|
+
try {
|
|
27
|
+
lcpObs = new PerformanceObserver((list) => {
|
|
28
|
+
const entries = list.getEntries();
|
|
29
|
+
if (entries.length > 0) {
|
|
30
|
+
lcpValue = entries[entries.length - 1].startTime;
|
|
31
|
+
vitals.lcp = lcpValue;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
lcpObs.observe({ type: 'largest-contentful-paint', buffered: true });
|
|
35
|
+
} catch(e) {}
|
|
36
|
+
|
|
37
|
+
// CLS
|
|
38
|
+
let clsValue = 0;
|
|
39
|
+
let clsObs;
|
|
40
|
+
try {
|
|
41
|
+
clsObs = new PerformanceObserver((list) => {
|
|
42
|
+
for (const entry of list.getEntries()) {
|
|
43
|
+
if (!(entry as any).hadRecentInput) {
|
|
44
|
+
clsValue += (entry as any).value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
vitals.cls = clsValue;
|
|
48
|
+
});
|
|
49
|
+
clsObs.observe({ type: 'layout-shift', buffered: true });
|
|
50
|
+
} catch(e) {}
|
|
51
|
+
|
|
52
|
+
// INP
|
|
53
|
+
let inpValue = 0;
|
|
54
|
+
let inpObs;
|
|
55
|
+
try {
|
|
56
|
+
inpObs = new PerformanceObserver((list) => {
|
|
57
|
+
for (const entry of list.getEntries()) {
|
|
58
|
+
if ((entry as any).duration > inpValue) {
|
|
59
|
+
inpValue = (entry as any).duration;
|
|
60
|
+
vitals.inp = inpValue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
inpObs.observe({ type: 'event', buffered: true, durationThreshold: 40 });
|
|
65
|
+
} catch(e) {}
|
|
66
|
+
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
try { lcpObs?.disconnect(); } catch(e) {}
|
|
69
|
+
try { clsObs?.disconnect(); } catch(e) {}
|
|
70
|
+
try { inpObs?.disconnect(); } catch(e) {}
|
|
71
|
+
resolve(vitals);
|
|
72
|
+
}, ${waitMs});
|
|
73
|
+
})
|
|
74
|
+
`;
|
|
75
|
+
const result = await evaluateJs(client, sessionId, script);
|
|
76
|
+
return (result ?? {});
|
|
77
|
+
}
|
|
78
|
+
export function formatVitals(vitals) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
const ms = (v) => v !== undefined ? `${Math.round(v)}ms` : 'n/a';
|
|
81
|
+
const score = (metric, v) => {
|
|
82
|
+
if (v === undefined)
|
|
83
|
+
return '';
|
|
84
|
+
if (metric === 'lcp')
|
|
85
|
+
return v < 2500 ? ' ✓ good' : v < 4000 ? ' ~ needs improvement' : ' ✗ poor';
|
|
86
|
+
if (metric === 'fcp')
|
|
87
|
+
return v < 1800 ? ' ✓ good' : v < 3000 ? ' ~ needs improvement' : ' ✗ poor';
|
|
88
|
+
if (metric === 'cls')
|
|
89
|
+
return v < 0.1 ? ' ✓ good' : v < 0.25 ? ' ~ needs improvement' : ' ✗ poor';
|
|
90
|
+
if (metric === 'inp')
|
|
91
|
+
return v < 200 ? ' ✓ good' : v < 500 ? ' ~ needs improvement' : ' ✗ poor';
|
|
92
|
+
if (metric === 'ttfb')
|
|
93
|
+
return v < 800 ? ' ✓ good' : v < 1800 ? ' ~ needs improvement' : ' ✗ poor';
|
|
94
|
+
return '';
|
|
95
|
+
};
|
|
96
|
+
if (vitals.lcp !== undefined)
|
|
97
|
+
lines.push(` LCP (Largest Contentful Paint): ${ms(vitals.lcp)}${score('lcp', vitals.lcp)}`);
|
|
98
|
+
if (vitals.fcp !== undefined)
|
|
99
|
+
lines.push(` FCP (First Contentful Paint): ${ms(vitals.fcp)}${score('fcp', vitals.fcp)}`);
|
|
100
|
+
if (vitals.cls !== undefined)
|
|
101
|
+
lines.push(` CLS (Cumulative Layout Shift): ${vitals.cls?.toFixed(4)}${score('cls', vitals.cls)}`);
|
|
102
|
+
if (vitals.inp !== undefined)
|
|
103
|
+
lines.push(` INP (Interaction to Next Paint): ${ms(vitals.inp)}${score('inp', vitals.inp)}`);
|
|
104
|
+
if (vitals.ttfb !== undefined)
|
|
105
|
+
lines.push(` TTFB (Time to First Byte): ${ms(vitals.ttfb)}${score('ttfb', vitals.ttfb)}`);
|
|
106
|
+
if (vitals.domInteractive !== undefined)
|
|
107
|
+
lines.push(` DOM Interactive: ${ms(vitals.domInteractive)}`);
|
|
108
|
+
if (vitals.domContentLoaded !== undefined)
|
|
109
|
+
lines.push(` DOMContentLoaded: ${ms(vitals.domContentLoaded)}`);
|
|
110
|
+
if (vitals.loadTime !== undefined)
|
|
111
|
+
lines.push(` Load: ${ms(vitals.loadTime)}`);
|
|
112
|
+
if (vitals.resources !== undefined)
|
|
113
|
+
lines.push(` Resources loaded: ${vitals.resources}`);
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=vitals.js.map
|
|
@@ -71,6 +71,8 @@ const snapshotCommand = {
|
|
|
71
71
|
{ name: 'interactive', short: 'i', type: 'boolean', description: 'Interactive elements only (93% token reduction)', default: false },
|
|
72
72
|
{ name: 'compact', short: 'c', type: 'boolean', description: 'Compact output format', default: false },
|
|
73
73
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
74
|
+
{ name: 'depth', short: 'd', type: 'number', description: 'Max depth of AX tree to show' },
|
|
75
|
+
{ name: 'selector', short: 's', type: 'string', description: 'Scope snapshot to a CSS selector' },
|
|
74
76
|
],
|
|
75
77
|
action: async (ctx) => {
|
|
76
78
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -78,6 +80,8 @@ const snapshotCommand = {
|
|
|
78
80
|
const result = await browser.captureSnapshot(client, sessionId, {
|
|
79
81
|
interactiveOnly: ctx.flags.interactive,
|
|
80
82
|
compact: ctx.flags.compact,
|
|
83
|
+
maxDepth: ctx.flags.depth,
|
|
84
|
+
selector: ctx.flags.selector,
|
|
81
85
|
});
|
|
82
86
|
_refs = result.refs;
|
|
83
87
|
if (ctx.flags.json) {
|
|
@@ -158,8 +162,10 @@ const waitCommand = {
|
|
|
158
162
|
options: [
|
|
159
163
|
{ name: 'url', type: 'string', description: 'Wait for URL matching glob pattern' },
|
|
160
164
|
{ name: 'text', type: 'string', description: 'Wait for text to appear in page' },
|
|
165
|
+
{ name: 'not-text', type: 'string', description: 'Wait for text to disappear from page' },
|
|
161
166
|
{ name: 'selector', type: 'string', description: 'Wait for CSS selector to appear' },
|
|
162
167
|
{ name: 'load', type: 'string', description: 'Wait for load event: load|networkidle|domcontentloaded' },
|
|
168
|
+
{ name: 'fn', type: 'string', description: 'Wait until JS expression returns truthy' },
|
|
163
169
|
{ name: 'ms', type: 'number', description: 'Wait N milliseconds' },
|
|
164
170
|
{ name: 'timeout', short: 't', type: 'number', description: 'Timeout in ms', default: 30000 },
|
|
165
171
|
],
|
|
@@ -171,6 +177,36 @@ const waitCommand = {
|
|
|
171
177
|
output.printSuccess(`Waited ${ctx.flags.ms}ms`);
|
|
172
178
|
return { success: true };
|
|
173
179
|
}
|
|
180
|
+
if (ctx.flags.fn) {
|
|
181
|
+
const expr = ctx.flags.fn;
|
|
182
|
+
const timeout = ctx.flags.timeout ?? 30000;
|
|
183
|
+
const interval = 200;
|
|
184
|
+
const deadline = Date.now() + timeout;
|
|
185
|
+
while (Date.now() < deadline) {
|
|
186
|
+
const result = await browser.evaluateJs(client, sessionId, expr);
|
|
187
|
+
if (result) {
|
|
188
|
+
output.printSuccess('Wait function returned truthy');
|
|
189
|
+
return { success: true };
|
|
190
|
+
}
|
|
191
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
192
|
+
}
|
|
193
|
+
throw new Error(`Timeout waiting for --fn: ${expr}`);
|
|
194
|
+
}
|
|
195
|
+
if (ctx.flags['not-text']) {
|
|
196
|
+
const target = ctx.flags['not-text'];
|
|
197
|
+
const timeout = ctx.flags.timeout ?? 30000;
|
|
198
|
+
const interval = 200;
|
|
199
|
+
const deadline = Date.now() + timeout;
|
|
200
|
+
while (Date.now() < deadline) {
|
|
201
|
+
const text = await browser.evaluateJs(client, sessionId, 'document.body.innerText');
|
|
202
|
+
if (!text.includes(target)) {
|
|
203
|
+
output.printSuccess('Text disappeared');
|
|
204
|
+
return { success: true };
|
|
205
|
+
}
|
|
206
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
207
|
+
}
|
|
208
|
+
throw new Error(`Timeout waiting for text to disappear: "${target}"`);
|
|
209
|
+
}
|
|
174
210
|
await browser.waitFor(client, sessionId, {
|
|
175
211
|
url: ctx.flags.url,
|
|
176
212
|
text: ctx.flags.text,
|
|
@@ -211,7 +247,7 @@ const screenshotCommand = {
|
|
|
211
247
|
};
|
|
212
248
|
const getCommand = {
|
|
213
249
|
name: 'get',
|
|
214
|
-
description: 'Get page info. Usage: monomind browse get url|title|text|html [@ref]',
|
|
250
|
+
description: 'Get page info. Usage: monomind browse get url|title|text|html|value|attr|count|box|styles [@ref] [attrName]',
|
|
215
251
|
options: [
|
|
216
252
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
217
253
|
],
|
|
@@ -220,7 +256,7 @@ const getCommand = {
|
|
|
220
256
|
const browser = await getBrowser();
|
|
221
257
|
const what = ctx.args[0];
|
|
222
258
|
if (!what)
|
|
223
|
-
throw new Error('Usage: monomind browse get url|title|text|html');
|
|
259
|
+
throw new Error('Usage: monomind browse get url|title|text|html|value|attr|count|box|styles');
|
|
224
260
|
let value;
|
|
225
261
|
switch (what) {
|
|
226
262
|
case 'url':
|
|
@@ -254,14 +290,92 @@ const getCommand = {
|
|
|
254
290
|
case 'html':
|
|
255
291
|
value = (await browser.evaluateJs(client, sessionId, 'document.documentElement.outerHTML'));
|
|
256
292
|
break;
|
|
293
|
+
case 'value': {
|
|
294
|
+
const refArg = ctx.args[1];
|
|
295
|
+
if (!refArg)
|
|
296
|
+
throw new Error('Usage: monomind browse get value @ref');
|
|
297
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
298
|
+
const ref = _refs.get(refKey);
|
|
299
|
+
if (!ref)
|
|
300
|
+
throw new Error(`Ref @${refKey} not found`);
|
|
301
|
+
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
302
|
+
if (!objectId)
|
|
303
|
+
throw new Error('Element not in DOM');
|
|
304
|
+
const r = await client.send('Runtime.callFunctionOn', {
|
|
305
|
+
functionDeclaration: 'function() { return this.value ?? null; }',
|
|
306
|
+
objectId, returnByValue: true,
|
|
307
|
+
}, sessionId);
|
|
308
|
+
value = r.result?.value ?? null;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
case 'attr': {
|
|
312
|
+
const refArg = ctx.args[1];
|
|
313
|
+
const attrName = ctx.args[2];
|
|
314
|
+
if (!refArg || !attrName)
|
|
315
|
+
throw new Error('Usage: monomind browse get attr @ref <attrName>');
|
|
316
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
317
|
+
const ref = _refs.get(refKey);
|
|
318
|
+
if (!ref)
|
|
319
|
+
throw new Error(`Ref @${refKey} not found`);
|
|
320
|
+
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
321
|
+
if (!objectId)
|
|
322
|
+
throw new Error('Element not in DOM');
|
|
323
|
+
const r = await client.send('Runtime.callFunctionOn', {
|
|
324
|
+
functionDeclaration: `function() { return this.getAttribute(${JSON.stringify(attrName)}); }`,
|
|
325
|
+
objectId, returnByValue: true,
|
|
326
|
+
}, sessionId);
|
|
327
|
+
value = r.result?.value ?? null;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case 'count': {
|
|
331
|
+
const selector = ctx.args[1];
|
|
332
|
+
if (!selector)
|
|
333
|
+
throw new Error('Usage: monomind browse get count <cssSelector>');
|
|
334
|
+
value = await browser.evaluateJs(client, sessionId, `document.querySelectorAll(${JSON.stringify(selector)}).length`);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case 'box': {
|
|
338
|
+
const refArg = ctx.args[1];
|
|
339
|
+
if (!refArg)
|
|
340
|
+
throw new Error('Usage: monomind browse get box @ref');
|
|
341
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
342
|
+
const ref = _refs.get(refKey);
|
|
343
|
+
if (!ref)
|
|
344
|
+
throw new Error(`Ref @${refKey} not found`);
|
|
345
|
+
value = await browser.getElementBox(client, sessionId, ref);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case 'styles': {
|
|
349
|
+
const refArg = ctx.args[1];
|
|
350
|
+
if (!refArg)
|
|
351
|
+
throw new Error('Usage: monomind browse get styles @ref');
|
|
352
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
353
|
+
const ref = _refs.get(refKey);
|
|
354
|
+
if (!ref)
|
|
355
|
+
throw new Error(`Ref @${refKey} not found`);
|
|
356
|
+
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
357
|
+
if (!objectId)
|
|
358
|
+
throw new Error('Element not in DOM');
|
|
359
|
+
const r = await client.send('Runtime.callFunctionOn', {
|
|
360
|
+
functionDeclaration: 'function() { const s = window.getComputedStyle(this); return JSON.stringify(Object.fromEntries([...s].map(k => [k, s.getPropertyValue(k)]))); }',
|
|
361
|
+
objectId, returnByValue: true,
|
|
362
|
+
}, sessionId);
|
|
363
|
+
try {
|
|
364
|
+
value = JSON.parse(r.result?.value ?? '{}');
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
value = {};
|
|
368
|
+
}
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
257
371
|
default:
|
|
258
|
-
throw new Error(`Unknown: ${what}. Use: url|title|text|html`);
|
|
372
|
+
throw new Error(`Unknown: ${what}. Use: url|title|text|html|value|attr|count|box|styles`);
|
|
259
373
|
}
|
|
260
374
|
if (ctx.flags.json) {
|
|
261
375
|
print(JSON.stringify({ data: { [what]: value } }));
|
|
262
376
|
}
|
|
263
377
|
else {
|
|
264
|
-
print(value);
|
|
378
|
+
print(typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value ?? ''));
|
|
265
379
|
}
|
|
266
380
|
return { success: true, data: { [what]: value } };
|
|
267
381
|
},
|
|
@@ -329,10 +443,13 @@ const setCommand = {
|
|
|
329
443
|
case 'viewport': {
|
|
330
444
|
const width = parseInt(ctx.args[1], 10);
|
|
331
445
|
const height = parseInt(ctx.args[2], 10);
|
|
446
|
+
const dpr = parseFloat(ctx.args[3]) || undefined;
|
|
332
447
|
if (isNaN(width) || isNaN(height))
|
|
333
|
-
throw new Error('Usage: set viewport <width> <height>');
|
|
334
|
-
await
|
|
335
|
-
|
|
448
|
+
throw new Error('Usage: set viewport <width> <height> [dpr]');
|
|
449
|
+
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
450
|
+
width, height, deviceScaleFactor: dpr ?? 1, mobile: false,
|
|
451
|
+
}, sessionId);
|
|
452
|
+
output.printSuccess(`Viewport set to ${width}x${height}${dpr ? ` @${dpr}x` : ''}`);
|
|
336
453
|
break;
|
|
337
454
|
}
|
|
338
455
|
case 'device': {
|
|
@@ -442,8 +559,27 @@ const stateCommand = {
|
|
|
442
559
|
output.printSuccess(`State loaded from ${target}`);
|
|
443
560
|
return { success: true };
|
|
444
561
|
}
|
|
562
|
+
case 'show': {
|
|
563
|
+
const { client: c, sessionId: sid } = await ensureConnected(_port);
|
|
564
|
+
const url = await browser.getCurrentUrl(c, sid);
|
|
565
|
+
const title = await browser.getCurrentTitle(c, sid);
|
|
566
|
+
const cookies = await browser.getCookies(c, sid);
|
|
567
|
+
const ls = await browser.getAllLocalStorage(c, sid);
|
|
568
|
+
const info = { url, title, cookies: cookies.length, localStorage: Object.keys(ls).length, refs: _refs.size };
|
|
569
|
+
print(JSON.stringify(info, null, 2));
|
|
570
|
+
return { success: true, data: info };
|
|
571
|
+
}
|
|
572
|
+
case 'clear': {
|
|
573
|
+
const { client: c, sessionId: sid } = await ensureConnected(_port);
|
|
574
|
+
await browser.clearCookies(c, sid);
|
|
575
|
+
await browser.clearLocalStorage(c, sid);
|
|
576
|
+
await browser.clearSessionStorage(c, sid);
|
|
577
|
+
_refs = new Map();
|
|
578
|
+
output.printSuccess('Browser state cleared (cookies, localStorage, sessionStorage, refs)');
|
|
579
|
+
return { success: true };
|
|
580
|
+
}
|
|
445
581
|
default:
|
|
446
|
-
throw new Error(`Unknown action: ${action}. Use: save|load|list`);
|
|
582
|
+
throw new Error(`Unknown action: ${action}. Use: save|load|list|show|clear`);
|
|
447
583
|
}
|
|
448
584
|
},
|
|
449
585
|
};
|
|
@@ -456,13 +592,14 @@ const networkCommand = {
|
|
|
456
592
|
{ name: 'fulfill', type: 'string', description: 'JSON response body' },
|
|
457
593
|
{ name: 'status', type: 'number', description: 'HTTP status for fulfill', default: 200 },
|
|
458
594
|
{ name: 'headers', type: 'string', description: 'JSON headers object' },
|
|
595
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
459
596
|
],
|
|
460
597
|
action: async (ctx) => {
|
|
461
598
|
const { client, sessionId } = await ensureConnected(_port);
|
|
462
599
|
const browser = await getBrowser();
|
|
463
600
|
const action = ctx.args[0];
|
|
464
601
|
if (!action)
|
|
465
|
-
throw new Error('Usage: monomind browse network route|cookies|headers');
|
|
602
|
+
throw new Error('Usage: monomind browse network route|unroute|cookies|headers|requests');
|
|
466
603
|
switch (action) {
|
|
467
604
|
case 'route': {
|
|
468
605
|
const pattern = ctx.flags.pattern;
|
|
@@ -481,6 +618,10 @@ const networkCommand = {
|
|
|
481
618
|
output.printSuccess(`Network route set: ${pattern}`);
|
|
482
619
|
break;
|
|
483
620
|
}
|
|
621
|
+
case 'unroute':
|
|
622
|
+
await browser.disableInterception(client, sessionId);
|
|
623
|
+
output.printSuccess('Network interception disabled');
|
|
624
|
+
break;
|
|
484
625
|
case 'cookies': {
|
|
485
626
|
const cookies = await browser.getCookies(client, sessionId);
|
|
486
627
|
print(JSON.stringify(cookies, null, 2));
|
|
@@ -494,8 +635,38 @@ const networkCommand = {
|
|
|
494
635
|
output.printSuccess('Extra headers set');
|
|
495
636
|
break;
|
|
496
637
|
}
|
|
638
|
+
case 'capture': {
|
|
639
|
+
const subAction = ctx.args[1] ?? 'start';
|
|
640
|
+
if (subAction === 'start') {
|
|
641
|
+
browser.startRequestCapture(client, sessionId);
|
|
642
|
+
output.printSuccess('Request capture started');
|
|
643
|
+
}
|
|
644
|
+
else if (subAction === 'stop') {
|
|
645
|
+
browser.stopRequestCapture(sessionId);
|
|
646
|
+
output.printSuccess('Request capture stopped');
|
|
647
|
+
}
|
|
648
|
+
else if (subAction === 'clear') {
|
|
649
|
+
browser.clearCapturedRequests(sessionId);
|
|
650
|
+
output.printSuccess('Captured requests cleared');
|
|
651
|
+
}
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
case 'requests': {
|
|
655
|
+
const reqs = browser.getCapturedRequests(sessionId);
|
|
656
|
+
if (ctx.flags.json)
|
|
657
|
+
print(JSON.stringify({ data: reqs }));
|
|
658
|
+
else {
|
|
659
|
+
if (reqs.length === 0) {
|
|
660
|
+
output.printInfo('No captured requests. Run: network capture start');
|
|
661
|
+
}
|
|
662
|
+
else
|
|
663
|
+
for (const r of reqs)
|
|
664
|
+
print(` ${r.method ?? 'GET'} ${r.status ?? '-'} ${r.url}`);
|
|
665
|
+
}
|
|
666
|
+
return { success: true, data: { requests: reqs } };
|
|
667
|
+
}
|
|
497
668
|
default:
|
|
498
|
-
throw new Error(`Unknown: ${action}. Use: route|cookies|headers`);
|
|
669
|
+
throw new Error(`Unknown: ${action}. Use: route|unroute|cookies|headers|capture|requests`);
|
|
499
670
|
}
|
|
500
671
|
return { success: true };
|
|
501
672
|
},
|
|
@@ -1148,7 +1319,7 @@ const isCommand = {
|
|
|
1148
1319
|
};
|
|
1149
1320
|
const findCommand = {
|
|
1150
1321
|
name: 'find',
|
|
1151
|
-
description: 'Find elements by semantic locators. Usage: monomind browse find role|text|label|testid <value> [action]',
|
|
1322
|
+
description: 'Find elements by semantic locators. Usage: monomind browse find role|text|label|placeholder|testid|selector <value> [action]',
|
|
1152
1323
|
options: [
|
|
1153
1324
|
{ name: 'name', type: 'string', description: 'Filter by accessible name' },
|
|
1154
1325
|
{ name: 'exact', type: 'boolean', description: 'Require exact match', default: false },
|
|
@@ -1162,7 +1333,7 @@ const findCommand = {
|
|
|
1162
1333
|
const value = ctx.args[1];
|
|
1163
1334
|
const action = ctx.args[2];
|
|
1164
1335
|
if (!locator || !value)
|
|
1165
|
-
throw new Error('Usage: monomind browse find role|text|label|testid <value> [action]');
|
|
1336
|
+
throw new Error('Usage: monomind browse find role|text|label|placeholder|testid|selector <value> [action]');
|
|
1166
1337
|
const opts = {
|
|
1167
1338
|
name: ctx.flags.name,
|
|
1168
1339
|
exact: ctx.flags.exact,
|
|
@@ -1180,6 +1351,18 @@ const findCommand = {
|
|
|
1180
1351
|
case 'label':
|
|
1181
1352
|
ref = await browser.findByLabel(client, sessionId, _refs, value, opts);
|
|
1182
1353
|
break;
|
|
1354
|
+
case 'placeholder':
|
|
1355
|
+
ref = await browser.findByPlaceholder(client, sessionId, _refs, value, opts);
|
|
1356
|
+
break;
|
|
1357
|
+
case 'selector': {
|
|
1358
|
+
const html = await browser.findBySelector(client, sessionId, value, opts);
|
|
1359
|
+
if (!html) {
|
|
1360
|
+
output.printWarning(`selector not found: ${value}`);
|
|
1361
|
+
return { success: false };
|
|
1362
|
+
}
|
|
1363
|
+
output.printSuccess(`Found: ${value}`);
|
|
1364
|
+
return { success: true, data: { html } };
|
|
1365
|
+
}
|
|
1183
1366
|
case 'testid': {
|
|
1184
1367
|
const sel = await browser.findByTestId(client, sessionId, value);
|
|
1185
1368
|
if (!sel) {
|
|
@@ -1190,7 +1373,7 @@ const findCommand = {
|
|
|
1190
1373
|
return { success: true, data: { selector: sel } };
|
|
1191
1374
|
}
|
|
1192
1375
|
default:
|
|
1193
|
-
throw new Error(`Unknown locator: ${locator}. Use: role|text|label|testid`);
|
|
1376
|
+
throw new Error(`Unknown locator: ${locator}. Use: role|text|label|placeholder|testid|selector`);
|
|
1194
1377
|
}
|
|
1195
1378
|
if (!ref) {
|
|
1196
1379
|
output.printWarning(`No element found: ${locator}="${value}"`);
|
|
@@ -1361,6 +1544,218 @@ const removeinitscriptCommand = {
|
|
|
1361
1544
|
return { success: true };
|
|
1362
1545
|
},
|
|
1363
1546
|
};
|
|
1547
|
+
const connectCommand = {
|
|
1548
|
+
name: 'connect',
|
|
1549
|
+
description: 'Connect to existing Chrome instance. Usage: monomind browse connect [--port 9222] [--target <id>]',
|
|
1550
|
+
options: [
|
|
1551
|
+
{ name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
|
|
1552
|
+
{ name: 'target', type: 'string', description: 'Target ID to attach to' },
|
|
1553
|
+
],
|
|
1554
|
+
action: async (ctx) => {
|
|
1555
|
+
const port = ctx.flags.port ?? 9222;
|
|
1556
|
+
const browser = await getBrowser();
|
|
1557
|
+
const conn = await browser.connectToTarget(port, ctx.flags.target);
|
|
1558
|
+
_client = conn.client;
|
|
1559
|
+
_sessionId = conn.sessionId;
|
|
1560
|
+
_targetId = conn.target.id;
|
|
1561
|
+
_port = port;
|
|
1562
|
+
_refs = new Map();
|
|
1563
|
+
const url = await browser.getCurrentUrl(_client, _sessionId);
|
|
1564
|
+
const title = await browser.getCurrentTitle(_client, _sessionId);
|
|
1565
|
+
output.printSuccess(`Connected: ${title} (${url})`);
|
|
1566
|
+
return { success: true, data: { targetId: _targetId, url, title } };
|
|
1567
|
+
},
|
|
1568
|
+
};
|
|
1569
|
+
const recordCommand = {
|
|
1570
|
+
name: 'record',
|
|
1571
|
+
description: 'Screen recording. Usage: monomind browse record start|stop|status [path]',
|
|
1572
|
+
options: [
|
|
1573
|
+
{ name: 'format', type: 'string', description: 'jpeg|png', default: 'jpeg' },
|
|
1574
|
+
{ name: 'quality', type: 'number', description: 'Quality 0-100', default: 80 },
|
|
1575
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1576
|
+
],
|
|
1577
|
+
action: async (ctx) => {
|
|
1578
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1579
|
+
const browser = await getBrowser();
|
|
1580
|
+
const action = ctx.args[0];
|
|
1581
|
+
if (!action)
|
|
1582
|
+
throw new Error('Usage: monomind browse record start|stop|status');
|
|
1583
|
+
switch (action) {
|
|
1584
|
+
case 'start':
|
|
1585
|
+
await browser.startRecording(client, sessionId, {
|
|
1586
|
+
format: ctx.flags.format,
|
|
1587
|
+
quality: ctx.flags.quality,
|
|
1588
|
+
});
|
|
1589
|
+
output.printSuccess('Recording started');
|
|
1590
|
+
return { success: true };
|
|
1591
|
+
case 'stop': {
|
|
1592
|
+
const path = await browser.stopRecording(client, sessionId, ctx.args[1]);
|
|
1593
|
+
if (ctx.flags.json)
|
|
1594
|
+
print(JSON.stringify({ data: { path } }));
|
|
1595
|
+
else
|
|
1596
|
+
output.printSuccess(`Recording saved: ${path}`);
|
|
1597
|
+
return { success: true, data: { path } };
|
|
1598
|
+
}
|
|
1599
|
+
case 'status': {
|
|
1600
|
+
const status = browser.getRecordingStatus(sessionId);
|
|
1601
|
+
if (ctx.flags.json)
|
|
1602
|
+
print(JSON.stringify({ data: status }));
|
|
1603
|
+
else
|
|
1604
|
+
print(`Recording: ${status.recording} | Frames: ${status.frames}`);
|
|
1605
|
+
return { success: true, data: status };
|
|
1606
|
+
}
|
|
1607
|
+
default:
|
|
1608
|
+
throw new Error('Usage: monomind browse record start|stop|status [path]');
|
|
1609
|
+
}
|
|
1610
|
+
},
|
|
1611
|
+
};
|
|
1612
|
+
const traceCommand = {
|
|
1613
|
+
name: 'trace',
|
|
1614
|
+
description: 'CDP performance trace. Usage: monomind browse trace start|stop [path]',
|
|
1615
|
+
options: [
|
|
1616
|
+
{ name: 'screenshots', type: 'boolean', description: 'Include screenshots', default: false },
|
|
1617
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1618
|
+
],
|
|
1619
|
+
action: async (ctx) => {
|
|
1620
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1621
|
+
const browser = await getBrowser();
|
|
1622
|
+
const action = ctx.args[0];
|
|
1623
|
+
if (!action)
|
|
1624
|
+
throw new Error('Usage: monomind browse trace start|stop [path]');
|
|
1625
|
+
switch (action) {
|
|
1626
|
+
case 'start':
|
|
1627
|
+
await browser.startTrace(client, sessionId, { screenshots: ctx.flags.screenshots });
|
|
1628
|
+
output.printSuccess('Trace started');
|
|
1629
|
+
return { success: true };
|
|
1630
|
+
case 'stop': {
|
|
1631
|
+
const path = await browser.stopTrace(client, sessionId, ctx.args[1]);
|
|
1632
|
+
if (ctx.flags.json)
|
|
1633
|
+
print(JSON.stringify({ data: { path } }));
|
|
1634
|
+
else
|
|
1635
|
+
output.printSuccess(`Trace saved: ${path}`);
|
|
1636
|
+
return { success: true, data: { path } };
|
|
1637
|
+
}
|
|
1638
|
+
case 'status':
|
|
1639
|
+
print(browser.getTraceStatus(sessionId) ? 'Tracing active' : 'Not tracing');
|
|
1640
|
+
return { success: true };
|
|
1641
|
+
default:
|
|
1642
|
+
throw new Error('Usage: monomind browse trace start|stop|status [path]');
|
|
1643
|
+
}
|
|
1644
|
+
},
|
|
1645
|
+
};
|
|
1646
|
+
const profilerCommand = {
|
|
1647
|
+
name: 'profiler',
|
|
1648
|
+
description: 'CPU profiler. Usage: monomind browse profiler start|stop|heap [path]',
|
|
1649
|
+
options: [
|
|
1650
|
+
{ name: 'interval', type: 'number', description: 'Sampling interval µs', default: 1000 },
|
|
1651
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1652
|
+
],
|
|
1653
|
+
action: async (ctx) => {
|
|
1654
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1655
|
+
const browser = await getBrowser();
|
|
1656
|
+
const action = ctx.args[0];
|
|
1657
|
+
if (!action)
|
|
1658
|
+
throw new Error('Usage: monomind browse profiler start|stop|heap [path]');
|
|
1659
|
+
switch (action) {
|
|
1660
|
+
case 'start':
|
|
1661
|
+
await browser.startCpuProfile(client, sessionId, { samplingInterval: ctx.flags.interval });
|
|
1662
|
+
output.printSuccess('CPU profiler started');
|
|
1663
|
+
return { success: true };
|
|
1664
|
+
case 'stop': {
|
|
1665
|
+
const path = await browser.stopCpuProfile(client, sessionId, ctx.args[1]);
|
|
1666
|
+
if (ctx.flags.json)
|
|
1667
|
+
print(JSON.stringify({ data: { path } }));
|
|
1668
|
+
else
|
|
1669
|
+
output.printSuccess(`Profile saved: ${path}`);
|
|
1670
|
+
return { success: true, data: { path } };
|
|
1671
|
+
}
|
|
1672
|
+
case 'heap': {
|
|
1673
|
+
const path = await browser.startHeapSnapshot(client, sessionId, ctx.args[1]);
|
|
1674
|
+
if (ctx.flags.json)
|
|
1675
|
+
print(JSON.stringify({ data: { path } }));
|
|
1676
|
+
else
|
|
1677
|
+
output.printSuccess(`Heap snapshot saved: ${path}`);
|
|
1678
|
+
return { success: true, data: { path } };
|
|
1679
|
+
}
|
|
1680
|
+
default:
|
|
1681
|
+
throw new Error('Usage: monomind browse profiler start|stop|heap [path]');
|
|
1682
|
+
}
|
|
1683
|
+
},
|
|
1684
|
+
};
|
|
1685
|
+
const vitalsCommand = {
|
|
1686
|
+
name: 'vitals',
|
|
1687
|
+
description: 'Collect Core Web Vitals. Usage: monomind browse vitals [--wait 2000]',
|
|
1688
|
+
options: [
|
|
1689
|
+
{ name: 'wait', type: 'number', description: 'Wait ms for observers', default: 2000 },
|
|
1690
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1691
|
+
],
|
|
1692
|
+
action: async (ctx) => {
|
|
1693
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1694
|
+
const browser = await getBrowser();
|
|
1695
|
+
const vitals = await browser.collectVitals(client, sessionId, ctx.flags.wait);
|
|
1696
|
+
if (ctx.flags.json) {
|
|
1697
|
+
print(JSON.stringify({ data: vitals }));
|
|
1698
|
+
}
|
|
1699
|
+
else {
|
|
1700
|
+
print(browser.formatVitals(vitals));
|
|
1701
|
+
}
|
|
1702
|
+
return { success: true, data: vitals };
|
|
1703
|
+
},
|
|
1704
|
+
};
|
|
1705
|
+
const harCommand = {
|
|
1706
|
+
name: 'har',
|
|
1707
|
+
description: 'HAR network recording. Usage: monomind browse har start|stop|status [path]',
|
|
1708
|
+
options: [
|
|
1709
|
+
{ name: 'bodies', type: 'boolean', description: 'Capture response bodies', default: false },
|
|
1710
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1711
|
+
],
|
|
1712
|
+
action: async (ctx) => {
|
|
1713
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1714
|
+
const browser = await getBrowser();
|
|
1715
|
+
const action = ctx.args[0];
|
|
1716
|
+
if (!action)
|
|
1717
|
+
throw new Error('Usage: monomind browse har start|stop|status [path]');
|
|
1718
|
+
switch (action) {
|
|
1719
|
+
case 'start':
|
|
1720
|
+
await browser.startHarRecording(client, sessionId);
|
|
1721
|
+
output.printSuccess('HAR recording started');
|
|
1722
|
+
return { success: true };
|
|
1723
|
+
case 'stop': {
|
|
1724
|
+
const path = await browser.stopHarRecording(client, sessionId, ctx.args[1], ctx.flags.bodies);
|
|
1725
|
+
if (ctx.flags.json)
|
|
1726
|
+
print(JSON.stringify({ data: { path } }));
|
|
1727
|
+
else
|
|
1728
|
+
output.printSuccess(`HAR saved: ${path}`);
|
|
1729
|
+
return { success: true, data: { path } };
|
|
1730
|
+
}
|
|
1731
|
+
case 'status': {
|
|
1732
|
+
const status = browser.getHarStatus(sessionId);
|
|
1733
|
+
if (ctx.flags.json)
|
|
1734
|
+
print(JSON.stringify({ data: status }));
|
|
1735
|
+
else
|
|
1736
|
+
print(`Recording: ${status.recording} | Requests: ${status.requestCount}`);
|
|
1737
|
+
return { success: true, data: status };
|
|
1738
|
+
}
|
|
1739
|
+
default:
|
|
1740
|
+
throw new Error('Usage: monomind browse har start|stop|status [path]');
|
|
1741
|
+
}
|
|
1742
|
+
},
|
|
1743
|
+
};
|
|
1744
|
+
const resizeCommand = {
|
|
1745
|
+
name: 'resize',
|
|
1746
|
+
description: 'Resize browser window. Usage: monomind browse resize <width> <height>',
|
|
1747
|
+
action: async (ctx) => {
|
|
1748
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1749
|
+
const browser = await getBrowser();
|
|
1750
|
+
const width = parseInt(ctx.args[0], 10);
|
|
1751
|
+
const height = parseInt(ctx.args[1], 10);
|
|
1752
|
+
if (isNaN(width) || isNaN(height))
|
|
1753
|
+
throw new Error('Usage: monomind browse resize <width> <height>');
|
|
1754
|
+
await browser.setViewport(client, sessionId, width, height);
|
|
1755
|
+
output.printSuccess(`Resized to ${width}x${height}`);
|
|
1756
|
+
return { success: true, data: { width, height } };
|
|
1757
|
+
},
|
|
1758
|
+
};
|
|
1364
1759
|
// ---------------------------------------------------------------------------
|
|
1365
1760
|
// Root browse command
|
|
1366
1761
|
// ---------------------------------------------------------------------------
|
|
@@ -1412,6 +1807,13 @@ const browseCommand = {
|
|
|
1412
1807
|
batchCommand,
|
|
1413
1808
|
addinitscriptCommand,
|
|
1414
1809
|
removeinitscriptCommand,
|
|
1810
|
+
connectCommand,
|
|
1811
|
+
recordCommand,
|
|
1812
|
+
traceCommand,
|
|
1813
|
+
profilerCommand,
|
|
1814
|
+
vitalsCommand,
|
|
1815
|
+
harCommand,
|
|
1816
|
+
resizeCommand,
|
|
1415
1817
|
closeCommand,
|
|
1416
1818
|
],
|
|
1417
1819
|
options: [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monomindcli",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.35",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Monomind CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|