monomind 1.10.34 → 1.10.36

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.
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "generatedAt": "2026-05-17T17:12:24.935Z",
4
+ "totalAgents": 0,
5
+ "agents": []
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monomind",
3
- "version": "1.10.34",
3
+ "version": "1.10.36",
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
- const { nodes } = await client.send('Accessibility.getFullAXTree', {}, sessionId);
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
@@ -41,6 +41,7 @@ export interface SnapshotOptions {
41
41
  interactiveOnly?: boolean;
42
42
  compact?: boolean;
43
43
  maxDepth?: number;
44
+ selector?: string;
44
45
  }
45
46
  export interface SnapshotResult {
46
47
  text: string;
@@ -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 browser.setViewport(client, sessionId, width, height);
335
- output.printSuccess(`Viewport set to ${width}x${height}`);
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.34",
3
+ "version": "1.10.36",
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",