@monoes/monomindcli 1.10.29 → 1.10.31
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/helpers/auto-memory-hook.mjs +39 -4
- package/.claude/helpers/handlers/adr-draft-handler.cjs +64 -0
- package/.claude/helpers/handlers/agent-start-handler.cjs +99 -0
- package/.claude/helpers/handlers/edit-handler.cjs +145 -0
- package/.claude/helpers/handlers/graph-status-handler.cjs +38 -0
- package/.claude/helpers/handlers/route-handler.cjs +393 -0
- package/.claude/helpers/handlers/session-handler.cjs +167 -0
- package/.claude/helpers/handlers/session-restore-handler.cjs +348 -0
- package/.claude/helpers/handlers/task-handler.cjs +329 -0
- package/.claude/helpers/hook-handler.cjs +120 -2431
- package/.claude/helpers/intelligence.cjs +21 -2
- package/.claude/helpers/learning-service.mjs +166 -8
- package/.claude/helpers/memory-palace.cjs +72 -12
- package/.claude/helpers/router.cjs +79 -5
- package/.claude/helpers/statusline.cjs +193 -399
- package/.claude/helpers/utils/micro-agents.cjs +338 -0
- package/.claude/helpers/utils/monograph.cjs +349 -0
- package/.claude/helpers/utils/telemetry.cjs +144 -0
- package/.claude/skills/agent-browser-testing/SKILL.md +3 -2
- package/.claude/skills/monomind/browse-agentcore.md +116 -0
- package/.claude/skills/monomind/browse-electron.md +189 -0
- package/.claude/skills/monomind/browse-qa.md +229 -0
- package/.claude/skills/monomind/browse-references/authentication.md +162 -0
- package/.claude/skills/monomind/browse-references/trust-boundaries.md +41 -0
- package/.claude/skills/monomind/browse-references/video-recording.md +84 -0
- package/.claude/skills/monomind/browse-slack.md +189 -0
- package/.claude/skills/monomind/browse-vercel.md +240 -0
- package/.claude/skills/monomind/browse.md +724 -0
- package/dist/src/browser/actions.d.ts +28 -0
- package/dist/src/browser/actions.d.ts.map +1 -0
- package/dist/src/browser/actions.js +292 -0
- package/dist/src/browser/actions.js.map +1 -0
- package/dist/src/browser/batch.d.ts +13 -0
- package/dist/src/browser/batch.d.ts.map +1 -0
- package/dist/src/browser/batch.js +11 -0
- package/dist/src/browser/batch.js.map +1 -0
- package/dist/src/browser/browser.d.ts +14 -0
- package/dist/src/browser/browser.d.ts.map +1 -0
- package/dist/src/browser/browser.js +198 -0
- package/dist/src/browser/browser.js.map +1 -0
- package/dist/src/browser/cdp.d.ts +17 -0
- package/dist/src/browser/cdp.d.ts.map +1 -0
- package/dist/src/browser/cdp.js +106 -0
- package/dist/src/browser/cdp.js.map +1 -0
- package/dist/src/browser/console-log.d.ts +22 -0
- package/dist/src/browser/console-log.d.ts.map +1 -0
- package/dist/src/browser/console-log.js +55 -0
- package/dist/src/browser/console-log.js.map +1 -0
- package/dist/src/browser/dialog.d.ts +11 -0
- package/dist/src/browser/dialog.d.ts.map +1 -0
- package/dist/src/browser/dialog.js +36 -0
- package/dist/src/browser/dialog.js.map +1 -0
- package/dist/src/browser/emulation.d.ts +15 -0
- package/dist/src/browser/emulation.d.ts.map +1 -0
- package/dist/src/browser/emulation.js +62 -0
- package/dist/src/browser/emulation.js.map +1 -0
- package/dist/src/browser/find.d.ts +21 -0
- package/dist/src/browser/find.d.ts.map +1 -0
- package/dist/src/browser/find.js +118 -0
- package/dist/src/browser/find.js.map +1 -0
- package/dist/src/browser/index.d.ts +18 -0
- package/dist/src/browser/index.d.ts.map +1 -0
- package/dist/src/browser/index.js +18 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/browser/network.d.ts +11 -0
- package/dist/src/browser/network.d.ts.map +1 -0
- package/dist/src/browser/network.js +81 -0
- package/dist/src/browser/network.js.map +1 -0
- package/dist/src/browser/pdf.d.ts +15 -0
- package/dist/src/browser/pdf.d.ts.map +1 -0
- package/dist/src/browser/pdf.js +27 -0
- package/dist/src/browser/pdf.js.map +1 -0
- package/dist/src/browser/screenshot.d.ts +15 -0
- package/dist/src/browser/screenshot.d.ts.map +1 -0
- package/dist/src/browser/screenshot.js +36 -0
- package/dist/src/browser/screenshot.js.map +1 -0
- package/dist/src/browser/session.d.ts +8 -0
- package/dist/src/browser/session.d.ts.map +1 -0
- package/dist/src/browser/session.js +50 -0
- package/dist/src/browser/session.js.map +1 -0
- package/dist/src/browser/snapshot.d.ts +12 -0
- package/dist/src/browser/snapshot.d.ts.map +1 -0
- package/dist/src/browser/snapshot.js +147 -0
- package/dist/src/browser/snapshot.js.map +1 -0
- package/dist/src/browser/storage.d.ts +11 -0
- package/dist/src/browser/storage.d.ts.map +1 -0
- package/dist/src/browser/storage.js +43 -0
- package/dist/src/browser/storage.js.map +1 -0
- package/dist/src/browser/tabs.d.ts +8 -0
- package/dist/src/browser/tabs.d.ts.map +1 -0
- package/dist/src/browser/tabs.js +25 -0
- package/dist/src/browser/tabs.js.map +1 -0
- package/dist/src/browser/types.d.ts +109 -0
- package/dist/src/browser/types.d.ts.map +1 -0
- package/dist/src/browser/types.js +16 -0
- package/dist/src/browser/types.js.map +1 -0
- package/dist/src/browser/wait.d.ts +4 -0
- package/dist/src/browser/wait.d.ts.map +1 -0
- package/dist/src/browser/wait.js +122 -0
- package/dist/src/browser/wait.js.map +1 -0
- package/dist/src/commands/browse.d.ts +8 -0
- package/dist/src/commands/browse.d.ts.map +1 -0
- package/dist/src/commands/browse.js +1494 -0
- package/dist/src/commands/browse.js.map +1 -0
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/ui/dashboard-v2.html +1857 -0
- package/dist/src/ui/server.mjs +71 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,1494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browse Command — Native browser automation via Chrome DevTools Protocol
|
|
3
|
+
* Provides ref-based element model and token-efficient accessibility snapshots
|
|
4
|
+
*/
|
|
5
|
+
import { output } from '../output.js';
|
|
6
|
+
// Runtime state (single session per CLI process)
|
|
7
|
+
let _client = null;
|
|
8
|
+
let _sessionId = '';
|
|
9
|
+
let _targetId = '';
|
|
10
|
+
let _port = 9222;
|
|
11
|
+
let _refs = new Map();
|
|
12
|
+
async function getBrowser() {
|
|
13
|
+
return import('../browser/index.js');
|
|
14
|
+
}
|
|
15
|
+
async function ensureConnected(port, targetId) {
|
|
16
|
+
const browser = await getBrowser();
|
|
17
|
+
if (!_client || !_client.isConnected()) {
|
|
18
|
+
_port = await browser.launchBrowser({ port, headless: false });
|
|
19
|
+
const conn = await browser.connectToTarget(_port, targetId);
|
|
20
|
+
_client = conn.client;
|
|
21
|
+
_sessionId = conn.sessionId;
|
|
22
|
+
_targetId = conn.target.id;
|
|
23
|
+
_refs = new Map();
|
|
24
|
+
}
|
|
25
|
+
return { client: _client, sessionId: _sessionId, targetId: _targetId };
|
|
26
|
+
}
|
|
27
|
+
function print(msg) {
|
|
28
|
+
process.stdout.write(msg + '\n');
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Sub-commands
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
const openCommand = {
|
|
34
|
+
name: 'open',
|
|
35
|
+
description: 'Open a URL in the browser. Usage: monomind browse open <url>',
|
|
36
|
+
options: [
|
|
37
|
+
{ name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
|
|
38
|
+
{ name: 'headless', type: 'boolean', description: 'Run in headless mode', default: false },
|
|
39
|
+
{ name: 'session', short: 's', type: 'string', description: 'Session name to restore' },
|
|
40
|
+
{ name: 'state', type: 'string', description: 'State file to load' },
|
|
41
|
+
],
|
|
42
|
+
action: async (ctx) => {
|
|
43
|
+
const url = ctx.args[0];
|
|
44
|
+
if (!url)
|
|
45
|
+
throw new Error('URL required. Usage: monomind browse open <url>');
|
|
46
|
+
const port = ctx.flags.port ?? 9222;
|
|
47
|
+
const browser = await getBrowser();
|
|
48
|
+
_port = await browser.launchBrowser({ port, headless: ctx.flags.headless });
|
|
49
|
+
const conn = await browser.connectToTarget(_port);
|
|
50
|
+
_client = conn.client;
|
|
51
|
+
_sessionId = conn.sessionId;
|
|
52
|
+
_targetId = conn.target.id;
|
|
53
|
+
_refs = new Map();
|
|
54
|
+
if (ctx.flags.state) {
|
|
55
|
+
await browser.loadStateFile(_client, _sessionId, ctx.flags.state);
|
|
56
|
+
}
|
|
57
|
+
else if (ctx.flags.session) {
|
|
58
|
+
await browser.loadSession(_client, _sessionId, ctx.flags.session);
|
|
59
|
+
}
|
|
60
|
+
await browser.openUrl(_client, _sessionId, url);
|
|
61
|
+
const currentUrl = await browser.getCurrentUrl(_client, _sessionId);
|
|
62
|
+
const title = await browser.getCurrentTitle(_client, _sessionId);
|
|
63
|
+
output.printSuccess(`Opened: ${title} (${currentUrl})`);
|
|
64
|
+
return { success: true, data: { url: currentUrl, title } };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const snapshotCommand = {
|
|
68
|
+
name: 'snapshot',
|
|
69
|
+
description: 'Capture accessibility snapshot with ref-based element handles (@e1, @e2, ...)',
|
|
70
|
+
options: [
|
|
71
|
+
{ name: 'interactive', short: 'i', type: 'boolean', description: 'Interactive elements only (93% token reduction)', default: false },
|
|
72
|
+
{ name: 'compact', short: 'c', type: 'boolean', description: 'Compact output format', default: false },
|
|
73
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
74
|
+
],
|
|
75
|
+
action: async (ctx) => {
|
|
76
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
77
|
+
const browser = await getBrowser();
|
|
78
|
+
const result = await browser.captureSnapshot(client, sessionId, {
|
|
79
|
+
interactiveOnly: ctx.flags.interactive,
|
|
80
|
+
compact: ctx.flags.compact,
|
|
81
|
+
});
|
|
82
|
+
_refs = result.refs;
|
|
83
|
+
if (ctx.flags.json) {
|
|
84
|
+
const refsObj = Object.fromEntries([...result.refs.entries()].map(([k, v]) => [k, v]));
|
|
85
|
+
print(JSON.stringify({ url: result.url, title: result.title, refs: refsObj, snapshot: result.text }));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
print(`[${result.title}] ${result.url}\n`);
|
|
89
|
+
print(result.text);
|
|
90
|
+
}
|
|
91
|
+
return { success: true, data: result };
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const clickCommand = {
|
|
95
|
+
name: 'click',
|
|
96
|
+
description: 'Click an element by ref (@e1) or coordinates. Usage: monomind browse click @e1',
|
|
97
|
+
options: [
|
|
98
|
+
{ name: 'right', type: 'boolean', description: 'Right-click', default: false },
|
|
99
|
+
{ name: 'double', type: 'boolean', description: 'Double-click', default: false },
|
|
100
|
+
{ name: 'x', type: 'number', description: 'X coordinate (for point click)' },
|
|
101
|
+
{ name: 'y', type: 'number', description: 'Y coordinate (for point click)' },
|
|
102
|
+
],
|
|
103
|
+
action: async (ctx) => {
|
|
104
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
105
|
+
const browser = await getBrowser();
|
|
106
|
+
const refArg = ctx.args[0];
|
|
107
|
+
if (!refArg && ctx.flags.x === undefined)
|
|
108
|
+
throw new Error('Ref (@e1) or --x/--y required');
|
|
109
|
+
if (ctx.flags.x !== undefined && ctx.flags.y !== undefined) {
|
|
110
|
+
await browser.clickPoint(client, sessionId, ctx.flags.x, ctx.flags.y);
|
|
111
|
+
output.printSuccess(`Clicked at (${ctx.flags.x}, ${ctx.flags.y})`);
|
|
112
|
+
return { success: true };
|
|
113
|
+
}
|
|
114
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
115
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
116
|
+
await browser.clickElement(client, sessionId, ref, {
|
|
117
|
+
button: ctx.flags.right ? 'right' : 'left',
|
|
118
|
+
clickCount: ctx.flags.double ? 2 : 1,
|
|
119
|
+
});
|
|
120
|
+
output.printSuccess(`Clicked: ${ref.role} "${ref.name}"`);
|
|
121
|
+
return { success: true };
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
const fillCommand = {
|
|
125
|
+
name: 'fill',
|
|
126
|
+
description: 'Fill an input element. Usage: monomind browse fill @e1 "text value"',
|
|
127
|
+
action: async (ctx) => {
|
|
128
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
129
|
+
const browser = await getBrowser();
|
|
130
|
+
const refArg = ctx.args[0];
|
|
131
|
+
const value = ctx.args[1];
|
|
132
|
+
if (!refArg || value === undefined)
|
|
133
|
+
throw new Error('Usage: monomind browse fill @e1 "value"');
|
|
134
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
135
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
136
|
+
await browser.fillElement(client, sessionId, ref, value);
|
|
137
|
+
output.printSuccess(`Filled: ${ref.role} "${ref.name}"`);
|
|
138
|
+
return { success: true };
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
const pressCommand = {
|
|
142
|
+
name: 'press',
|
|
143
|
+
description: 'Press a keyboard key. Usage: monomind browse press Enter',
|
|
144
|
+
action: async (ctx) => {
|
|
145
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
146
|
+
const browser = await getBrowser();
|
|
147
|
+
const key = ctx.args[0];
|
|
148
|
+
if (!key)
|
|
149
|
+
throw new Error('Key required. E.g.: monomind browse press Enter');
|
|
150
|
+
await browser.pressKey(client, sessionId, key);
|
|
151
|
+
output.printSuccess(`Pressed: ${key}`);
|
|
152
|
+
return { success: true };
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
const waitCommand = {
|
|
156
|
+
name: 'wait',
|
|
157
|
+
description: 'Wait for a condition to be met before proceeding',
|
|
158
|
+
options: [
|
|
159
|
+
{ name: 'url', type: 'string', description: 'Wait for URL matching glob pattern' },
|
|
160
|
+
{ name: 'text', type: 'string', description: 'Wait for text to appear in page' },
|
|
161
|
+
{ name: 'selector', type: 'string', description: 'Wait for CSS selector to appear' },
|
|
162
|
+
{ name: 'load', type: 'string', description: 'Wait for load event: load|networkidle|domcontentloaded' },
|
|
163
|
+
{ name: 'ms', type: 'number', description: 'Wait N milliseconds' },
|
|
164
|
+
{ name: 'timeout', short: 't', type: 'number', description: 'Timeout in ms', default: 30000 },
|
|
165
|
+
],
|
|
166
|
+
action: async (ctx) => {
|
|
167
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
168
|
+
const browser = await getBrowser();
|
|
169
|
+
if (ctx.flags.ms) {
|
|
170
|
+
await new Promise((r) => setTimeout(r, ctx.flags.ms));
|
|
171
|
+
output.printSuccess(`Waited ${ctx.flags.ms}ms`);
|
|
172
|
+
return { success: true };
|
|
173
|
+
}
|
|
174
|
+
await browser.waitFor(client, sessionId, {
|
|
175
|
+
url: ctx.flags.url,
|
|
176
|
+
text: ctx.flags.text,
|
|
177
|
+
selector: ctx.flags.selector,
|
|
178
|
+
load: ctx.flags.load,
|
|
179
|
+
timeout: ctx.flags.timeout,
|
|
180
|
+
});
|
|
181
|
+
output.printSuccess('Wait condition met');
|
|
182
|
+
return { success: true };
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
const screenshotCommand = {
|
|
186
|
+
name: 'screenshot',
|
|
187
|
+
description: 'Capture a screenshot. Usage: monomind browse screenshot [path]',
|
|
188
|
+
options: [
|
|
189
|
+
{ name: 'full', type: 'boolean', description: 'Full page screenshot', default: false },
|
|
190
|
+
{ name: 'format', type: 'string', description: 'Format: png|jpeg|webp', default: 'png' },
|
|
191
|
+
{ name: 'quality', type: 'number', description: 'Quality 0-100 for jpeg/webp', default: 80 },
|
|
192
|
+
{ name: 'json', type: 'boolean', description: 'Output JSON with path', default: false },
|
|
193
|
+
],
|
|
194
|
+
action: async (ctx) => {
|
|
195
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
196
|
+
const browser = await getBrowser();
|
|
197
|
+
const result = await browser.captureScreenshot(client, sessionId, {
|
|
198
|
+
path: ctx.args[0],
|
|
199
|
+
fullPage: ctx.flags.full,
|
|
200
|
+
format: ctx.flags.format,
|
|
201
|
+
quality: ctx.flags.quality,
|
|
202
|
+
});
|
|
203
|
+
if (ctx.flags.json) {
|
|
204
|
+
print(JSON.stringify({ data: { path: result.path } }));
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
output.printSuccess(`Screenshot saved: ${result.path}`);
|
|
208
|
+
}
|
|
209
|
+
return { success: true, data: result };
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
const getCommand = {
|
|
213
|
+
name: 'get',
|
|
214
|
+
description: 'Get page info. Usage: monomind browse get url|title|text|html [@ref]',
|
|
215
|
+
options: [
|
|
216
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
217
|
+
],
|
|
218
|
+
action: async (ctx) => {
|
|
219
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
220
|
+
const browser = await getBrowser();
|
|
221
|
+
const what = ctx.args[0];
|
|
222
|
+
if (!what)
|
|
223
|
+
throw new Error('Usage: monomind browse get url|title|text|html');
|
|
224
|
+
let value;
|
|
225
|
+
switch (what) {
|
|
226
|
+
case 'url':
|
|
227
|
+
value = await browser.getCurrentUrl(client, sessionId);
|
|
228
|
+
break;
|
|
229
|
+
case 'title':
|
|
230
|
+
value = await browser.getCurrentTitle(client, sessionId);
|
|
231
|
+
break;
|
|
232
|
+
case 'text': {
|
|
233
|
+
const refArg = ctx.args[1];
|
|
234
|
+
if (refArg) {
|
|
235
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
236
|
+
const ref = _refs.get(refKey);
|
|
237
|
+
if (!ref)
|
|
238
|
+
throw new Error(`Ref @${refKey} not found`);
|
|
239
|
+
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
240
|
+
if (!objectId)
|
|
241
|
+
throw new Error('Element not in DOM');
|
|
242
|
+
const result = await client.send('Runtime.callFunctionOn', {
|
|
243
|
+
functionDeclaration: 'function() { return this.innerText || this.textContent || ""; }',
|
|
244
|
+
objectId,
|
|
245
|
+
returnByValue: true,
|
|
246
|
+
}, sessionId);
|
|
247
|
+
value = result.result?.value ?? '';
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
value = (await browser.evaluateJs(client, sessionId, 'document.body.innerText'));
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case 'html':
|
|
255
|
+
value = (await browser.evaluateJs(client, sessionId, 'document.documentElement.outerHTML'));
|
|
256
|
+
break;
|
|
257
|
+
default:
|
|
258
|
+
throw new Error(`Unknown: ${what}. Use: url|title|text|html`);
|
|
259
|
+
}
|
|
260
|
+
if (ctx.flags.json) {
|
|
261
|
+
print(JSON.stringify({ data: { [what]: value } }));
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
print(value);
|
|
265
|
+
}
|
|
266
|
+
return { success: true, data: { [what]: value } };
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
const scrollCommand = {
|
|
270
|
+
name: 'scroll',
|
|
271
|
+
description: 'Scroll the page. Usage: monomind browse scroll up|down|left|right',
|
|
272
|
+
options: [
|
|
273
|
+
{ name: 'amount', short: 'a', type: 'number', description: 'Pixels to scroll', default: 300 },
|
|
274
|
+
{ name: 'ref', type: 'string', description: 'Element ref to scroll within' },
|
|
275
|
+
],
|
|
276
|
+
action: async (ctx) => {
|
|
277
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
278
|
+
const browser = await getBrowser();
|
|
279
|
+
const direction = ctx.args[0];
|
|
280
|
+
if (!direction)
|
|
281
|
+
throw new Error('Usage: monomind browse scroll up|down|left|right');
|
|
282
|
+
let ref;
|
|
283
|
+
if (ctx.flags.ref) {
|
|
284
|
+
const refKey = ctx.flags.ref.startsWith('@')
|
|
285
|
+
? ctx.flags.ref.slice(1)
|
|
286
|
+
: ctx.flags.ref;
|
|
287
|
+
ref = _refs.get(refKey);
|
|
288
|
+
}
|
|
289
|
+
await browser.scrollElement(client, sessionId, direction, ctx.flags.amount, ref);
|
|
290
|
+
output.printSuccess(`Scrolled ${direction}`);
|
|
291
|
+
return { success: true };
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
const navigateCommand = {
|
|
295
|
+
name: 'navigate',
|
|
296
|
+
description: 'Navigate browser history. Usage: monomind browse navigate back|forward|reload',
|
|
297
|
+
action: async (ctx) => {
|
|
298
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
299
|
+
const direction = ctx.args[0];
|
|
300
|
+
if (!direction)
|
|
301
|
+
throw new Error('Usage: monomind browse navigate back|forward|reload');
|
|
302
|
+
switch (direction) {
|
|
303
|
+
case 'back':
|
|
304
|
+
await client.send('Runtime.evaluate', { expression: 'history.back()' }, sessionId);
|
|
305
|
+
break;
|
|
306
|
+
case 'forward':
|
|
307
|
+
await client.send('Runtime.evaluate', { expression: 'history.forward()' }, sessionId);
|
|
308
|
+
break;
|
|
309
|
+
case 'reload':
|
|
310
|
+
await client.send('Page.reload', {}, sessionId);
|
|
311
|
+
break;
|
|
312
|
+
default:
|
|
313
|
+
throw new Error(`Unknown direction: ${direction}. Use: back|forward|reload`);
|
|
314
|
+
}
|
|
315
|
+
output.printSuccess(`Navigated: ${direction}`);
|
|
316
|
+
return { success: true };
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
const setCommand = {
|
|
320
|
+
name: 'set',
|
|
321
|
+
description: 'Configure browser settings. Usage: monomind browse set viewport|device|geo|offline|media|credentials|useragent <args>',
|
|
322
|
+
action: async (ctx) => {
|
|
323
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
324
|
+
const browser = await getBrowser();
|
|
325
|
+
const setting = ctx.args[0];
|
|
326
|
+
if (!setting)
|
|
327
|
+
throw new Error('Usage: monomind browse set viewport|device|geo|offline|media|credentials|useragent <args>');
|
|
328
|
+
switch (setting) {
|
|
329
|
+
case 'viewport': {
|
|
330
|
+
const width = parseInt(ctx.args[1], 10);
|
|
331
|
+
const height = parseInt(ctx.args[2], 10);
|
|
332
|
+
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}`);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case 'device': {
|
|
339
|
+
const deviceName = ctx.args[1];
|
|
340
|
+
if (!deviceName)
|
|
341
|
+
throw new Error(`Usage: set device <name>. Available: ${browser.listDevices().join(', ')}`);
|
|
342
|
+
await browser.emulateDevice(client, sessionId, deviceName);
|
|
343
|
+
output.printSuccess(`Emulating device: ${deviceName}`);
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case 'geo': {
|
|
347
|
+
const lat = parseFloat(ctx.args[1]);
|
|
348
|
+
const lon = parseFloat(ctx.args[2]);
|
|
349
|
+
const acc = parseFloat(ctx.args[3]) || 100;
|
|
350
|
+
if (isNaN(lat) || isNaN(lon))
|
|
351
|
+
throw new Error('Usage: set geo <latitude> <longitude> [accuracy]');
|
|
352
|
+
await browser.setGeolocation(client, sessionId, lat, lon, acc);
|
|
353
|
+
output.printSuccess(`Geolocation set: ${lat}, ${lon}`);
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case 'offline': {
|
|
357
|
+
const enabled = ctx.args[1] !== 'false';
|
|
358
|
+
await browser.setOfflineMode(client, sessionId, enabled);
|
|
359
|
+
output.printSuccess(`Offline mode: ${enabled}`);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case 'media': {
|
|
363
|
+
const scheme = ctx.args[1];
|
|
364
|
+
if (!scheme)
|
|
365
|
+
throw new Error('Usage: set media dark|light|no-preference');
|
|
366
|
+
await browser.setColorScheme(client, sessionId, scheme);
|
|
367
|
+
output.printSuccess(`Color scheme: ${scheme}`);
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case 'credentials': {
|
|
371
|
+
const username = ctx.args[1];
|
|
372
|
+
const password = ctx.args[2];
|
|
373
|
+
if (!username || !password)
|
|
374
|
+
throw new Error('Usage: set credentials <username> <password>');
|
|
375
|
+
await browser.setBasicAuth(client, sessionId, username, password);
|
|
376
|
+
output.printSuccess('Basic auth credentials set');
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case 'useragent': {
|
|
380
|
+
const ua = ctx.args[1];
|
|
381
|
+
if (!ua)
|
|
382
|
+
throw new Error('Usage: set useragent "<user-agent-string>"');
|
|
383
|
+
await browser.setUserAgent(client, sessionId, ua);
|
|
384
|
+
output.printSuccess('User agent set');
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
default:
|
|
388
|
+
throw new Error(`Unknown setting: ${setting}. Use: viewport|device|geo|offline|media|credentials|useragent`);
|
|
389
|
+
}
|
|
390
|
+
return { success: true };
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
const stateCommand = {
|
|
394
|
+
name: 'state',
|
|
395
|
+
description: 'Manage browser session state. Usage: monomind browse state save|load|list [name]',
|
|
396
|
+
action: async (ctx) => {
|
|
397
|
+
const browser = await getBrowser();
|
|
398
|
+
const action = ctx.args[0];
|
|
399
|
+
if (!action)
|
|
400
|
+
throw new Error('Usage: monomind browse state save|load|list [name]');
|
|
401
|
+
switch (action) {
|
|
402
|
+
case 'list': {
|
|
403
|
+
const sessions = await browser.listSessions();
|
|
404
|
+
if (sessions.length === 0) {
|
|
405
|
+
output.printInfo('No saved sessions');
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
output.printInfo('Saved sessions:');
|
|
409
|
+
for (const s of sessions)
|
|
410
|
+
print(` ${s}`);
|
|
411
|
+
}
|
|
412
|
+
return { success: true, data: { sessions } };
|
|
413
|
+
}
|
|
414
|
+
case 'save': {
|
|
415
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
416
|
+
const target = ctx.args[1];
|
|
417
|
+
if (!target)
|
|
418
|
+
throw new Error('Usage: monomind browse state save <name-or-file>');
|
|
419
|
+
const url = await browser.getCurrentUrl(client, sessionId);
|
|
420
|
+
const title = await browser.getCurrentTitle(client, sessionId);
|
|
421
|
+
if (target.endsWith('.json')) {
|
|
422
|
+
await browser.saveStateFile(client, sessionId, _targetId, target, url, title);
|
|
423
|
+
output.printSuccess(`State saved to ${target}`);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
const path = await browser.saveSession(client, sessionId, _targetId, target, url, title);
|
|
427
|
+
output.printSuccess(`Session "${target}" saved to ${path}`);
|
|
428
|
+
}
|
|
429
|
+
return { success: true };
|
|
430
|
+
}
|
|
431
|
+
case 'load': {
|
|
432
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
433
|
+
const target = ctx.args[1];
|
|
434
|
+
if (!target)
|
|
435
|
+
throw new Error('Usage: monomind browse state load <name-or-file>');
|
|
436
|
+
if (target.endsWith('.json')) {
|
|
437
|
+
await browser.loadStateFile(client, sessionId, target);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
await browser.loadSession(client, sessionId, target);
|
|
441
|
+
}
|
|
442
|
+
output.printSuccess(`State loaded from ${target}`);
|
|
443
|
+
return { success: true };
|
|
444
|
+
}
|
|
445
|
+
default:
|
|
446
|
+
throw new Error(`Unknown action: ${action}. Use: save|load|list`);
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
const networkCommand = {
|
|
451
|
+
name: 'network',
|
|
452
|
+
description: 'Network interception and cookie management',
|
|
453
|
+
options: [
|
|
454
|
+
{ name: 'pattern', type: 'string', description: 'URL pattern for route (glob)' },
|
|
455
|
+
{ name: 'abort', type: 'boolean', description: 'Abort matching requests' },
|
|
456
|
+
{ name: 'fulfill', type: 'string', description: 'JSON response body' },
|
|
457
|
+
{ name: 'status', type: 'number', description: 'HTTP status for fulfill', default: 200 },
|
|
458
|
+
{ name: 'headers', type: 'string', description: 'JSON headers object' },
|
|
459
|
+
],
|
|
460
|
+
action: async (ctx) => {
|
|
461
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
462
|
+
const browser = await getBrowser();
|
|
463
|
+
const action = ctx.args[0];
|
|
464
|
+
if (!action)
|
|
465
|
+
throw new Error('Usage: monomind browse network route|cookies|headers');
|
|
466
|
+
switch (action) {
|
|
467
|
+
case 'route': {
|
|
468
|
+
const pattern = ctx.flags.pattern;
|
|
469
|
+
if (!pattern)
|
|
470
|
+
throw new Error('--pattern required for network route');
|
|
471
|
+
const routes = [{
|
|
472
|
+
pattern,
|
|
473
|
+
action: ctx.flags.abort ? 'abort' : ctx.flags.fulfill ? 'fulfill' : 'continue',
|
|
474
|
+
response: ctx.flags.fulfill ? {
|
|
475
|
+
status: ctx.flags.status,
|
|
476
|
+
body: ctx.flags.fulfill,
|
|
477
|
+
headers: ctx.flags.headers ? JSON.parse(ctx.flags.headers) : {},
|
|
478
|
+
} : undefined,
|
|
479
|
+
}];
|
|
480
|
+
await browser.setupRoutes(client, sessionId, routes);
|
|
481
|
+
output.printSuccess(`Network route set: ${pattern}`);
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
case 'cookies': {
|
|
485
|
+
const cookies = await browser.getCookies(client, sessionId);
|
|
486
|
+
print(JSON.stringify(cookies, null, 2));
|
|
487
|
+
return { success: true, data: { cookies } };
|
|
488
|
+
}
|
|
489
|
+
case 'headers': {
|
|
490
|
+
const headers = ctx.flags.headers;
|
|
491
|
+
if (!headers)
|
|
492
|
+
throw new Error('--headers required (JSON string)');
|
|
493
|
+
await browser.setExtraHeaders(client, sessionId, JSON.parse(headers));
|
|
494
|
+
output.printSuccess('Extra headers set');
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
default:
|
|
498
|
+
throw new Error(`Unknown: ${action}. Use: route|cookies|headers`);
|
|
499
|
+
}
|
|
500
|
+
return { success: true };
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
const evalCommand = {
|
|
504
|
+
name: 'eval',
|
|
505
|
+
description: 'Evaluate JavaScript in page context. Usage: monomind browse eval "document.title"',
|
|
506
|
+
options: [
|
|
507
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
508
|
+
],
|
|
509
|
+
action: async (ctx) => {
|
|
510
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
511
|
+
const browser = await getBrowser();
|
|
512
|
+
const expr = ctx.args[0];
|
|
513
|
+
if (!expr)
|
|
514
|
+
throw new Error('Usage: monomind browse eval "<expression>"');
|
|
515
|
+
const result = await browser.evaluateJs(client, sessionId, expr);
|
|
516
|
+
if (ctx.flags.json) {
|
|
517
|
+
print(JSON.stringify({ data: result }));
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
print(String(result ?? ''));
|
|
521
|
+
}
|
|
522
|
+
return { success: true, data: { result } };
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
const closeCommand = {
|
|
526
|
+
name: 'close',
|
|
527
|
+
description: 'Close the active browser session',
|
|
528
|
+
action: async (_ctx) => {
|
|
529
|
+
if (_client) {
|
|
530
|
+
_client.close();
|
|
531
|
+
_client = null;
|
|
532
|
+
_sessionId = '';
|
|
533
|
+
_targetId = '';
|
|
534
|
+
_refs = new Map();
|
|
535
|
+
output.printSuccess('Browser session closed');
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
output.printInfo('No active browser session');
|
|
539
|
+
}
|
|
540
|
+
return { success: true };
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
// Additional subcommands
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
const dblclickCommand = {
|
|
547
|
+
name: 'dblclick',
|
|
548
|
+
description: 'Double-click an element. Usage: monomind browse dblclick @e1',
|
|
549
|
+
action: async (ctx) => {
|
|
550
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
551
|
+
const browser = await getBrowser();
|
|
552
|
+
const refArg = ctx.args[0];
|
|
553
|
+
if (!refArg)
|
|
554
|
+
throw new Error('Usage: monomind browse dblclick @e1');
|
|
555
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
556
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
557
|
+
await browser.clickElement(client, sessionId, ref, { clickCount: 2 });
|
|
558
|
+
output.printSuccess(`Double-clicked: ${ref.role} "${ref.name}"`);
|
|
559
|
+
return { success: true };
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
const focusCommand = {
|
|
563
|
+
name: 'focus',
|
|
564
|
+
description: 'Focus an element. Usage: monomind browse focus @e1',
|
|
565
|
+
action: async (ctx) => {
|
|
566
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
567
|
+
const browser = await getBrowser();
|
|
568
|
+
const refArg = ctx.args[0];
|
|
569
|
+
if (!refArg)
|
|
570
|
+
throw new Error('Usage: monomind browse focus @e1');
|
|
571
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
572
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
573
|
+
await browser.focusElement(client, sessionId, ref);
|
|
574
|
+
output.printSuccess(`Focused: ${ref.role} "${ref.name}"`);
|
|
575
|
+
return { success: true };
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
const typeCommand = {
|
|
579
|
+
name: 'type',
|
|
580
|
+
description: 'Type text into element (appends, does not clear). Usage: monomind browse type @e1 "text"',
|
|
581
|
+
action: async (ctx) => {
|
|
582
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
583
|
+
const browser = await getBrowser();
|
|
584
|
+
const refArg = ctx.args[0];
|
|
585
|
+
const value = ctx.args[1];
|
|
586
|
+
if (!refArg || value === undefined)
|
|
587
|
+
throw new Error('Usage: monomind browse type @e1 "value"');
|
|
588
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
589
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
590
|
+
await browser.typeIntoElement(client, sessionId, ref, value);
|
|
591
|
+
output.printSuccess(`Typed into: ${ref.role} "${ref.name}"`);
|
|
592
|
+
return { success: true };
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
const keyboardCommand = {
|
|
596
|
+
name: 'keyboard',
|
|
597
|
+
description: 'Keyboard commands. Usage: monomind browse keyboard type "text" | inserttext "text"',
|
|
598
|
+
action: async (ctx) => {
|
|
599
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
600
|
+
const browser = await getBrowser();
|
|
601
|
+
const action = ctx.args[0];
|
|
602
|
+
const text = ctx.args[1];
|
|
603
|
+
if (!action || !text)
|
|
604
|
+
throw new Error('Usage: monomind browse keyboard type|inserttext "text"');
|
|
605
|
+
await browser.typeText(client, sessionId, text);
|
|
606
|
+
output.printSuccess(`Keyboard ${action}: ${text.length} chars`);
|
|
607
|
+
return { success: true };
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
const keydownCommand = {
|
|
611
|
+
name: 'keydown',
|
|
612
|
+
description: 'Hold key down. Usage: monomind browse keydown Shift',
|
|
613
|
+
action: async (ctx) => {
|
|
614
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
615
|
+
const browser = await getBrowser();
|
|
616
|
+
const key = ctx.args[0];
|
|
617
|
+
if (!key)
|
|
618
|
+
throw new Error('Usage: monomind browse keydown <key>');
|
|
619
|
+
await browser.keyDown(client, sessionId, key);
|
|
620
|
+
output.printSuccess(`Key down: ${key}`);
|
|
621
|
+
return { success: true };
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
const keyupCommand = {
|
|
625
|
+
name: 'keyup',
|
|
626
|
+
description: 'Release held key. Usage: monomind browse keyup Shift',
|
|
627
|
+
action: async (ctx) => {
|
|
628
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
629
|
+
const browser = await getBrowser();
|
|
630
|
+
const key = ctx.args[0];
|
|
631
|
+
if (!key)
|
|
632
|
+
throw new Error('Usage: monomind browse keyup <key>');
|
|
633
|
+
await browser.keyUp(client, sessionId, key);
|
|
634
|
+
output.printSuccess(`Key up: ${key}`);
|
|
635
|
+
return { success: true };
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
const hoverCommand = {
|
|
639
|
+
name: 'hover',
|
|
640
|
+
description: 'Hover over an element. Usage: monomind browse hover @e1',
|
|
641
|
+
action: async (ctx) => {
|
|
642
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
643
|
+
const browser = await getBrowser();
|
|
644
|
+
const refArg = ctx.args[0];
|
|
645
|
+
if (!refArg)
|
|
646
|
+
throw new Error('Usage: monomind browse hover @e1');
|
|
647
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
648
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
649
|
+
await browser.hoverElement(client, sessionId, ref);
|
|
650
|
+
output.printSuccess(`Hovered: ${ref.role} "${ref.name}"`);
|
|
651
|
+
return { success: true };
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
const selectCommand = {
|
|
655
|
+
name: 'select',
|
|
656
|
+
description: 'Select a dropdown option. Usage: monomind browse select @e1 "Option text"',
|
|
657
|
+
action: async (ctx) => {
|
|
658
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
659
|
+
const browser = await getBrowser();
|
|
660
|
+
const refArg = ctx.args[0];
|
|
661
|
+
const value = ctx.args[1];
|
|
662
|
+
if (!refArg || !value)
|
|
663
|
+
throw new Error('Usage: monomind browse select @e1 "value"');
|
|
664
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
665
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
666
|
+
await browser.selectOption(client, sessionId, ref, value);
|
|
667
|
+
output.printSuccess(`Selected: "${value}"`);
|
|
668
|
+
return { success: true };
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
const checkCommand = {
|
|
672
|
+
name: 'check',
|
|
673
|
+
description: 'Check a checkbox. Usage: monomind browse check @e1',
|
|
674
|
+
action: async (ctx) => {
|
|
675
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
676
|
+
const browser = await getBrowser();
|
|
677
|
+
const refArg = ctx.args[0];
|
|
678
|
+
if (!refArg)
|
|
679
|
+
throw new Error('Usage: monomind browse check @e1');
|
|
680
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
681
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
682
|
+
await browser.checkElement(client, sessionId, ref, true);
|
|
683
|
+
output.printSuccess(`Checked: ${ref.role} "${ref.name}"`);
|
|
684
|
+
return { success: true };
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
const uncheckCommand = {
|
|
688
|
+
name: 'uncheck',
|
|
689
|
+
description: 'Uncheck a checkbox. Usage: monomind browse uncheck @e1',
|
|
690
|
+
action: async (ctx) => {
|
|
691
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
692
|
+
const browser = await getBrowser();
|
|
693
|
+
const refArg = ctx.args[0];
|
|
694
|
+
if (!refArg)
|
|
695
|
+
throw new Error('Usage: monomind browse uncheck @e1');
|
|
696
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
697
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
698
|
+
await browser.checkElement(client, sessionId, ref, false);
|
|
699
|
+
output.printSuccess(`Unchecked: ${ref.role} "${ref.name}"`);
|
|
700
|
+
return { success: true };
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
const scrollIntoViewCommand = {
|
|
704
|
+
name: 'scrollintoview',
|
|
705
|
+
description: 'Scroll element into view. Usage: monomind browse scrollintoview @e1',
|
|
706
|
+
action: async (ctx) => {
|
|
707
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
708
|
+
const browser = await getBrowser();
|
|
709
|
+
const refArg = ctx.args[0];
|
|
710
|
+
if (!refArg)
|
|
711
|
+
throw new Error('Usage: monomind browse scrollintoview @e1');
|
|
712
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
713
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
714
|
+
await browser.scrollIntoView(client, sessionId, ref);
|
|
715
|
+
output.printSuccess(`Scrolled into view: ${ref.role} "${ref.name}"`);
|
|
716
|
+
return { success: true };
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
const dragCommand = {
|
|
720
|
+
name: 'drag',
|
|
721
|
+
description: 'Drag element to another element. Usage: monomind browse drag @e1 @e2',
|
|
722
|
+
action: async (ctx) => {
|
|
723
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
724
|
+
const browser = await getBrowser();
|
|
725
|
+
const srcArg = ctx.args[0];
|
|
726
|
+
const tgtArg = ctx.args[1];
|
|
727
|
+
if (!srcArg || !tgtArg)
|
|
728
|
+
throw new Error('Usage: monomind browse drag @e1 @e2');
|
|
729
|
+
const srcKey = srcArg.startsWith('@') ? srcArg.slice(1) : srcArg;
|
|
730
|
+
const tgtKey = tgtArg.startsWith('@') ? tgtArg.slice(1) : tgtArg;
|
|
731
|
+
const src = await browser.resolveRef(client, sessionId, _refs, srcKey);
|
|
732
|
+
const tgt = await browser.resolveRef(client, sessionId, _refs, tgtKey);
|
|
733
|
+
await browser.dragAndDrop(client, sessionId, src, tgt);
|
|
734
|
+
output.printSuccess(`Dragged @${srcKey} to @${tgtKey}`);
|
|
735
|
+
return { success: true };
|
|
736
|
+
},
|
|
737
|
+
};
|
|
738
|
+
const uploadCommand = {
|
|
739
|
+
name: 'upload',
|
|
740
|
+
description: 'Upload files to a file input. Usage: monomind browse upload @e1 ./file.pdf',
|
|
741
|
+
action: async (ctx) => {
|
|
742
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
743
|
+
const browser = await getBrowser();
|
|
744
|
+
const refArg = ctx.args[0];
|
|
745
|
+
const files = ctx.args.slice(1);
|
|
746
|
+
if (!refArg || files.length === 0)
|
|
747
|
+
throw new Error('Usage: monomind browse upload @e1 <file1> [file2...]');
|
|
748
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
749
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
750
|
+
await browser.uploadFile(client, sessionId, ref, files);
|
|
751
|
+
output.printSuccess(`Uploaded ${files.length} file(s) to @${refKey}`);
|
|
752
|
+
return { success: true };
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
const mouseCommand = {
|
|
756
|
+
name: 'mouse',
|
|
757
|
+
description: 'Fine-grained mouse control. Usage: monomind browse mouse move|down|up|wheel <args>',
|
|
758
|
+
options: [
|
|
759
|
+
{ name: 'button', type: 'string', description: 'Button: left|right|middle', default: 'left' },
|
|
760
|
+
],
|
|
761
|
+
action: async (ctx) => {
|
|
762
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
763
|
+
const browser = await getBrowser();
|
|
764
|
+
const action = ctx.args[0];
|
|
765
|
+
switch (action) {
|
|
766
|
+
case 'move': {
|
|
767
|
+
const x = parseFloat(ctx.args[1]);
|
|
768
|
+
const y = parseFloat(ctx.args[2]);
|
|
769
|
+
await browser.mouseMove(client, sessionId, x, y);
|
|
770
|
+
output.printSuccess(`Mouse moved to (${x}, ${y})`);
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
case 'down': {
|
|
774
|
+
const x = parseFloat(ctx.args[1]) || 0;
|
|
775
|
+
const y = parseFloat(ctx.args[2]) || 0;
|
|
776
|
+
const button = ctx.flags.button ?? 'left';
|
|
777
|
+
await browser.mouseDown(client, sessionId, x, y, button);
|
|
778
|
+
output.printSuccess(`Mouse down at (${x}, ${y})`);
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
case 'up': {
|
|
782
|
+
const x = parseFloat(ctx.args[1]) || 0;
|
|
783
|
+
const y = parseFloat(ctx.args[2]) || 0;
|
|
784
|
+
const button = ctx.flags.button ?? 'left';
|
|
785
|
+
await browser.mouseUp(client, sessionId, x, y, button);
|
|
786
|
+
output.printSuccess(`Mouse up at (${x}, ${y})`);
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
case 'wheel': {
|
|
790
|
+
const x = parseFloat(ctx.args[1]) || 0;
|
|
791
|
+
const y = parseFloat(ctx.args[2]) || 0;
|
|
792
|
+
const dy = parseFloat(ctx.args[3]) || 0;
|
|
793
|
+
const dx = parseFloat(ctx.args[4]) || 0;
|
|
794
|
+
await browser.mouseWheel(client, sessionId, x, y, dy, dx);
|
|
795
|
+
output.printSuccess(`Mouse wheel (${dx}, ${dy})`);
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
default:
|
|
799
|
+
throw new Error('Usage: monomind browse mouse move|down|up|wheel <args>');
|
|
800
|
+
}
|
|
801
|
+
return { success: true };
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
const clipboardCommand = {
|
|
805
|
+
name: 'clipboard',
|
|
806
|
+
description: 'Clipboard operations. Usage: monomind browse clipboard read|write|copy|paste',
|
|
807
|
+
action: async (ctx) => {
|
|
808
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
809
|
+
const browser = await getBrowser();
|
|
810
|
+
const action = ctx.args[0];
|
|
811
|
+
switch (action) {
|
|
812
|
+
case 'read': {
|
|
813
|
+
const text = await browser.readClipboard(client, sessionId);
|
|
814
|
+
print(text);
|
|
815
|
+
return { success: true, data: { text } };
|
|
816
|
+
}
|
|
817
|
+
case 'write': {
|
|
818
|
+
const text = ctx.args[1];
|
|
819
|
+
if (!text)
|
|
820
|
+
throw new Error('Usage: monomind browse clipboard write "text"');
|
|
821
|
+
await browser.writeClipboard(client, sessionId, text);
|
|
822
|
+
output.printSuccess('Clipboard written');
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
case 'copy':
|
|
826
|
+
await browser.pressKey(client, sessionId, 'c');
|
|
827
|
+
output.printSuccess('Copy sent (Ctrl+C equivalent)');
|
|
828
|
+
break;
|
|
829
|
+
case 'paste':
|
|
830
|
+
await browser.pressKey(client, sessionId, 'v');
|
|
831
|
+
output.printSuccess('Paste sent (Ctrl+V equivalent)');
|
|
832
|
+
break;
|
|
833
|
+
default:
|
|
834
|
+
throw new Error('Usage: monomind browse clipboard read|write|copy|paste');
|
|
835
|
+
}
|
|
836
|
+
return { success: true };
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
const dialogCommand = {
|
|
840
|
+
name: 'dialog',
|
|
841
|
+
description: 'Handle browser dialogs. Usage: monomind browse dialog accept|dismiss|status',
|
|
842
|
+
action: async (ctx) => {
|
|
843
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
844
|
+
const browser = await getBrowser();
|
|
845
|
+
const action = ctx.args[0];
|
|
846
|
+
switch (action) {
|
|
847
|
+
case 'accept': {
|
|
848
|
+
const text = ctx.args[1];
|
|
849
|
+
await browser.acceptDialog(client, sessionId, text);
|
|
850
|
+
output.printSuccess('Dialog accepted');
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
case 'dismiss':
|
|
854
|
+
await browser.dismissDialog(client, sessionId);
|
|
855
|
+
output.printSuccess('Dialog dismissed');
|
|
856
|
+
break;
|
|
857
|
+
case 'status': {
|
|
858
|
+
const info = browser.getDialogStatus();
|
|
859
|
+
if (info) {
|
|
860
|
+
print(`Dialog open: type=${info.type} message="${info.message}"`);
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
print('No dialog open');
|
|
864
|
+
}
|
|
865
|
+
return { success: true, data: { dialog: info } };
|
|
866
|
+
}
|
|
867
|
+
default:
|
|
868
|
+
throw new Error('Usage: monomind browse dialog accept|dismiss|status');
|
|
869
|
+
}
|
|
870
|
+
return { success: true };
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
const frameCommand = {
|
|
874
|
+
name: 'frame',
|
|
875
|
+
description: 'Switch to iframe or back to main. Usage: monomind browse frame "#frame-id" | frame main',
|
|
876
|
+
action: async (ctx) => {
|
|
877
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
878
|
+
const browser = await getBrowser();
|
|
879
|
+
const target = ctx.args[0];
|
|
880
|
+
if (!target)
|
|
881
|
+
throw new Error('Usage: monomind browse frame <selector>|main');
|
|
882
|
+
if (target === 'main') {
|
|
883
|
+
output.printSuccess('Switched to main frame');
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
const frameSrc = await browser.switchToFrame(client, sessionId, target);
|
|
887
|
+
output.printSuccess(`Switched to frame: ${frameSrc ?? target}`);
|
|
888
|
+
}
|
|
889
|
+
return { success: true };
|
|
890
|
+
},
|
|
891
|
+
};
|
|
892
|
+
const tabCommand = {
|
|
893
|
+
name: 'tab',
|
|
894
|
+
description: 'Tab management. Usage: monomind browse tab list|new|close [url]',
|
|
895
|
+
options: [
|
|
896
|
+
{ name: 'label', type: 'string', description: 'Label for new tab' },
|
|
897
|
+
],
|
|
898
|
+
action: async (ctx) => {
|
|
899
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
900
|
+
const browser = await getBrowser();
|
|
901
|
+
const action = ctx.args[0];
|
|
902
|
+
switch (action ?? 'list') {
|
|
903
|
+
case 'list': {
|
|
904
|
+
const tabs = await browser.listTabs(_port);
|
|
905
|
+
for (const t of tabs)
|
|
906
|
+
print(` ${t.id}: ${t.title} (${t.url})`);
|
|
907
|
+
return { success: true, data: { tabs } };
|
|
908
|
+
}
|
|
909
|
+
case 'new': {
|
|
910
|
+
const url = ctx.args[1];
|
|
911
|
+
const tab = await browser.newTab(_port, url);
|
|
912
|
+
output.printSuccess(`New tab: ${tab.id} ${url ?? ''}`);
|
|
913
|
+
return { success: true, data: { tab } };
|
|
914
|
+
}
|
|
915
|
+
case 'close': {
|
|
916
|
+
const tabId = ctx.args[1];
|
|
917
|
+
if (!tabId)
|
|
918
|
+
throw new Error('Usage: monomind browse tab close <tabId>');
|
|
919
|
+
await browser.closeTab(client, sessionId, tabId);
|
|
920
|
+
output.printSuccess(`Closed tab: ${tabId}`);
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
default: {
|
|
924
|
+
// Try to treat arg as tab ID to switch to
|
|
925
|
+
await browser.activateTab(client, sessionId, action);
|
|
926
|
+
output.printSuccess(`Switched to tab: ${action}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return { success: true };
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
const consoleLogCommand = {
|
|
933
|
+
name: 'console',
|
|
934
|
+
description: 'View captured console messages. Usage: monomind browse console [--clear] [--json]',
|
|
935
|
+
options: [
|
|
936
|
+
{ name: 'clear', type: 'boolean', description: 'Clear console messages', default: false },
|
|
937
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
938
|
+
{ name: 'errors-only', type: 'boolean', description: 'Show only errors', default: false },
|
|
939
|
+
],
|
|
940
|
+
action: async (ctx) => {
|
|
941
|
+
const browser = await getBrowser();
|
|
942
|
+
if (ctx.flags.clear) {
|
|
943
|
+
browser.clearConsoleMessages();
|
|
944
|
+
output.printSuccess('Console cleared');
|
|
945
|
+
return { success: true };
|
|
946
|
+
}
|
|
947
|
+
const msgs = browser.getConsoleMessages();
|
|
948
|
+
if (ctx.flags.json) {
|
|
949
|
+
print(JSON.stringify(msgs));
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
for (const m of msgs) {
|
|
953
|
+
const prefix = m.type === 'error' ? '[ERROR]' : m.type === 'warn' ? '[WARN]' : '[LOG]';
|
|
954
|
+
print(`${prefix} ${m.text}`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return { success: true, data: { messages: msgs } };
|
|
958
|
+
},
|
|
959
|
+
};
|
|
960
|
+
const errorsCommand = {
|
|
961
|
+
name: 'errors',
|
|
962
|
+
description: 'View page errors (uncaught JS exceptions). Usage: monomind browse errors [--clear]',
|
|
963
|
+
options: [
|
|
964
|
+
{ name: 'clear', type: 'boolean', description: 'Clear errors', default: false },
|
|
965
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
966
|
+
],
|
|
967
|
+
action: async (ctx) => {
|
|
968
|
+
const browser = await getBrowser();
|
|
969
|
+
if (ctx.flags.clear) {
|
|
970
|
+
browser.clearPageErrors();
|
|
971
|
+
output.printSuccess('Errors cleared');
|
|
972
|
+
return { success: true };
|
|
973
|
+
}
|
|
974
|
+
const errs = browser.getPageErrors();
|
|
975
|
+
if (ctx.flags.json) {
|
|
976
|
+
print(JSON.stringify(errs));
|
|
977
|
+
}
|
|
978
|
+
else if (errs.length === 0) {
|
|
979
|
+
output.printSuccess('No page errors');
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
for (const e of errs)
|
|
983
|
+
print(`[ERROR] ${e.text} (${e.url}:${e.lineNumber})`);
|
|
984
|
+
}
|
|
985
|
+
return { success: true, data: { errors: errs } };
|
|
986
|
+
},
|
|
987
|
+
};
|
|
988
|
+
const storageCommand = {
|
|
989
|
+
name: 'storage',
|
|
990
|
+
description: 'localStorage/sessionStorage management. Usage: monomind browse storage local|session [key] [--set val] [--clear]',
|
|
991
|
+
options: [
|
|
992
|
+
{ name: 'set', type: 'string', description: 'Value to set for key' },
|
|
993
|
+
{ name: 'clear', type: 'boolean', description: 'Clear all storage', default: false },
|
|
994
|
+
{ name: 'remove', type: 'boolean', description: 'Remove a specific key', default: false },
|
|
995
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
996
|
+
],
|
|
997
|
+
action: async (ctx) => {
|
|
998
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
999
|
+
const browser = await getBrowser();
|
|
1000
|
+
const storageType = ctx.args[0];
|
|
1001
|
+
const key = ctx.args[1];
|
|
1002
|
+
if (!storageType)
|
|
1003
|
+
throw new Error('Usage: monomind browse storage local|session [key]');
|
|
1004
|
+
const isLocal = storageType === 'local';
|
|
1005
|
+
if (ctx.flags.clear) {
|
|
1006
|
+
if (isLocal)
|
|
1007
|
+
await browser.clearLocalStorage(client, sessionId);
|
|
1008
|
+
else
|
|
1009
|
+
await browser.clearSessionStorage(client, sessionId);
|
|
1010
|
+
output.printSuccess(`${storageType}Storage cleared`);
|
|
1011
|
+
return { success: true };
|
|
1012
|
+
}
|
|
1013
|
+
if (key && ctx.flags.set !== undefined) {
|
|
1014
|
+
if (isLocal)
|
|
1015
|
+
await browser.setLocalStorageKey(client, sessionId, key, ctx.flags.set);
|
|
1016
|
+
else
|
|
1017
|
+
await browser.setSessionStorageKey(client, sessionId, key, ctx.flags.set);
|
|
1018
|
+
output.printSuccess(`Set ${key}`);
|
|
1019
|
+
return { success: true };
|
|
1020
|
+
}
|
|
1021
|
+
if (key && ctx.flags.remove) {
|
|
1022
|
+
if (isLocal)
|
|
1023
|
+
await browser.removeLocalStorageKey(client, sessionId, key);
|
|
1024
|
+
output.printSuccess(`Removed ${key}`);
|
|
1025
|
+
return { success: true };
|
|
1026
|
+
}
|
|
1027
|
+
if (key) {
|
|
1028
|
+
const val = isLocal
|
|
1029
|
+
? await browser.getLocalStorageKey(client, sessionId, key)
|
|
1030
|
+
: await browser.getSessionStorageKey(client, sessionId, key);
|
|
1031
|
+
if (ctx.flags.json)
|
|
1032
|
+
print(JSON.stringify({ data: val }));
|
|
1033
|
+
else
|
|
1034
|
+
print(val ?? '(null)');
|
|
1035
|
+
return { success: true, data: { value: val } };
|
|
1036
|
+
}
|
|
1037
|
+
const all = isLocal
|
|
1038
|
+
? await browser.getAllLocalStorage(client, sessionId)
|
|
1039
|
+
: await browser.getAllSessionStorage(client, sessionId);
|
|
1040
|
+
if (ctx.flags.json)
|
|
1041
|
+
print(JSON.stringify(all));
|
|
1042
|
+
else {
|
|
1043
|
+
for (const [k, v] of Object.entries(all))
|
|
1044
|
+
print(` ${k}: ${v}`);
|
|
1045
|
+
}
|
|
1046
|
+
return { success: true, data: { storage: all } };
|
|
1047
|
+
},
|
|
1048
|
+
};
|
|
1049
|
+
const cookiesCommand = {
|
|
1050
|
+
name: 'cookies',
|
|
1051
|
+
description: 'Cookie management. Usage: monomind browse cookies [list|set|clear]',
|
|
1052
|
+
options: [
|
|
1053
|
+
{ name: 'name', type: 'string', description: 'Cookie name' },
|
|
1054
|
+
{ name: 'value', type: 'string', description: 'Cookie value' },
|
|
1055
|
+
{ name: 'domain', type: 'string', description: 'Cookie domain' },
|
|
1056
|
+
{ name: 'curl', type: 'string', description: 'Import cookies from cURL dump file' },
|
|
1057
|
+
],
|
|
1058
|
+
action: async (ctx) => {
|
|
1059
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1060
|
+
const browser = await getBrowser();
|
|
1061
|
+
const action = ctx.args[0] ?? 'list';
|
|
1062
|
+
switch (action) {
|
|
1063
|
+
case 'list': {
|
|
1064
|
+
const cookies = await browser.getCookies(client, sessionId);
|
|
1065
|
+
print(JSON.stringify(cookies, null, 2));
|
|
1066
|
+
return { success: true, data: { cookies } };
|
|
1067
|
+
}
|
|
1068
|
+
case 'set': {
|
|
1069
|
+
if (ctx.flags.name && ctx.flags.value) {
|
|
1070
|
+
await browser.setCookies(client, sessionId, [{
|
|
1071
|
+
name: ctx.flags.name,
|
|
1072
|
+
value: ctx.flags.value,
|
|
1073
|
+
domain: ctx.flags.domain,
|
|
1074
|
+
}]);
|
|
1075
|
+
output.printSuccess(`Cookie set: ${ctx.flags.name}`);
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
throw new Error('Usage: monomind browse cookies set --name <n> --value <v>');
|
|
1079
|
+
}
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
case 'clear':
|
|
1083
|
+
await browser.clearCookies(client, sessionId);
|
|
1084
|
+
output.printSuccess('Cookies cleared');
|
|
1085
|
+
break;
|
|
1086
|
+
default:
|
|
1087
|
+
throw new Error('Usage: monomind browse cookies list|set|clear');
|
|
1088
|
+
}
|
|
1089
|
+
return { success: true };
|
|
1090
|
+
},
|
|
1091
|
+
};
|
|
1092
|
+
const pdfCommand = {
|
|
1093
|
+
name: 'pdf',
|
|
1094
|
+
description: 'Save page as PDF. Usage: monomind browse pdf [path]',
|
|
1095
|
+
options: [
|
|
1096
|
+
{ name: 'landscape', type: 'boolean', description: 'Landscape orientation', default: false },
|
|
1097
|
+
{ name: 'background', type: 'boolean', description: 'Print background', default: true },
|
|
1098
|
+
],
|
|
1099
|
+
action: async (ctx) => {
|
|
1100
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1101
|
+
const browser = await getBrowser();
|
|
1102
|
+
const path = await browser.capturePdf(client, sessionId, {
|
|
1103
|
+
path: ctx.args[0],
|
|
1104
|
+
landscape: ctx.flags.landscape,
|
|
1105
|
+
printBackground: ctx.flags.background,
|
|
1106
|
+
});
|
|
1107
|
+
output.printSuccess(`PDF saved: ${path}`);
|
|
1108
|
+
return { success: true, data: { path } };
|
|
1109
|
+
},
|
|
1110
|
+
};
|
|
1111
|
+
const isCommand = {
|
|
1112
|
+
name: 'is',
|
|
1113
|
+
description: 'Check element state. Usage: monomind browse is visible|enabled|checked @e1',
|
|
1114
|
+
options: [
|
|
1115
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1116
|
+
],
|
|
1117
|
+
action: async (ctx) => {
|
|
1118
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1119
|
+
const browser = await getBrowser();
|
|
1120
|
+
const check = ctx.args[0];
|
|
1121
|
+
const refArg = ctx.args[1];
|
|
1122
|
+
if (!check || !refArg)
|
|
1123
|
+
throw new Error('Usage: monomind browse is visible|enabled|checked @e1');
|
|
1124
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
1125
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
1126
|
+
let result;
|
|
1127
|
+
switch (check) {
|
|
1128
|
+
case 'visible':
|
|
1129
|
+
result = await browser.isVisible(client, sessionId, ref);
|
|
1130
|
+
break;
|
|
1131
|
+
case 'enabled':
|
|
1132
|
+
result = await browser.isEnabled(client, sessionId, ref);
|
|
1133
|
+
break;
|
|
1134
|
+
case 'checked':
|
|
1135
|
+
result = await browser.isChecked(client, sessionId, ref);
|
|
1136
|
+
break;
|
|
1137
|
+
default: throw new Error(`Unknown check: ${check}. Use: visible|enabled|checked`);
|
|
1138
|
+
}
|
|
1139
|
+
if (ctx.flags.json) {
|
|
1140
|
+
print(JSON.stringify({ data: { [check]: result } }));
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
print(result ? 'true' : 'false');
|
|
1144
|
+
}
|
|
1145
|
+
return { success: true, data: { [check]: result } };
|
|
1146
|
+
},
|
|
1147
|
+
};
|
|
1148
|
+
const findCommand = {
|
|
1149
|
+
name: 'find',
|
|
1150
|
+
description: 'Find elements by semantic locators. Usage: monomind browse find role|text|label|testid <value> [action]',
|
|
1151
|
+
options: [
|
|
1152
|
+
{ name: 'name', type: 'string', description: 'Filter by accessible name' },
|
|
1153
|
+
{ name: 'exact', type: 'boolean', description: 'Require exact match', default: false },
|
|
1154
|
+
{ name: 'nth', type: 'number', description: 'Find nth match' },
|
|
1155
|
+
{ name: 'last', type: 'boolean', description: 'Find last match', default: false },
|
|
1156
|
+
],
|
|
1157
|
+
action: async (ctx) => {
|
|
1158
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1159
|
+
const browser = await getBrowser();
|
|
1160
|
+
const locator = ctx.args[0];
|
|
1161
|
+
const value = ctx.args[1];
|
|
1162
|
+
const action = ctx.args[2];
|
|
1163
|
+
if (!locator || !value)
|
|
1164
|
+
throw new Error('Usage: monomind browse find role|text|label|testid <value> [action]');
|
|
1165
|
+
const opts = {
|
|
1166
|
+
name: ctx.flags.name,
|
|
1167
|
+
exact: ctx.flags.exact,
|
|
1168
|
+
nth: ctx.flags.nth,
|
|
1169
|
+
last: ctx.flags.last,
|
|
1170
|
+
};
|
|
1171
|
+
let ref = null;
|
|
1172
|
+
switch (locator) {
|
|
1173
|
+
case 'role':
|
|
1174
|
+
ref = await browser.findByRole(client, sessionId, _refs, value, opts);
|
|
1175
|
+
break;
|
|
1176
|
+
case 'text':
|
|
1177
|
+
ref = await browser.findByText(client, sessionId, _refs, value, opts);
|
|
1178
|
+
break;
|
|
1179
|
+
case 'label':
|
|
1180
|
+
ref = await browser.findByLabel(client, sessionId, _refs, value, opts);
|
|
1181
|
+
break;
|
|
1182
|
+
case 'testid': {
|
|
1183
|
+
const sel = await browser.findByTestId(client, sessionId, value);
|
|
1184
|
+
if (!sel) {
|
|
1185
|
+
output.printWarning(`testid not found: ${value}`);
|
|
1186
|
+
return { success: false };
|
|
1187
|
+
}
|
|
1188
|
+
output.printSuccess(`Found testid selector: ${sel}`);
|
|
1189
|
+
return { success: true, data: { selector: sel } };
|
|
1190
|
+
}
|
|
1191
|
+
default:
|
|
1192
|
+
throw new Error(`Unknown locator: ${locator}. Use: role|text|label|testid`);
|
|
1193
|
+
}
|
|
1194
|
+
if (!ref) {
|
|
1195
|
+
output.printWarning(`No element found: ${locator}="${value}"`);
|
|
1196
|
+
return { success: false };
|
|
1197
|
+
}
|
|
1198
|
+
output.printSuccess(`Found: ${ref.role} "${ref.name}" [@${ref.ref}]`);
|
|
1199
|
+
if (action) {
|
|
1200
|
+
switch (action) {
|
|
1201
|
+
case 'click':
|
|
1202
|
+
await browser.clickElement(client, sessionId, ref);
|
|
1203
|
+
break;
|
|
1204
|
+
case 'fill': {
|
|
1205
|
+
const fillValue = ctx.args[3];
|
|
1206
|
+
await browser.fillElement(client, sessionId, ref, fillValue ?? '');
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
case 'type': {
|
|
1210
|
+
const typeValue = ctx.args[3];
|
|
1211
|
+
await browser.typeIntoElement(client, sessionId, ref, typeValue ?? '');
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
case 'hover':
|
|
1215
|
+
await browser.hoverElement(client, sessionId, ref);
|
|
1216
|
+
break;
|
|
1217
|
+
case 'focus':
|
|
1218
|
+
await browser.focusElement(client, sessionId, ref);
|
|
1219
|
+
break;
|
|
1220
|
+
case 'check':
|
|
1221
|
+
await browser.checkElement(client, sessionId, ref, true);
|
|
1222
|
+
break;
|
|
1223
|
+
case 'uncheck':
|
|
1224
|
+
await browser.checkElement(client, sessionId, ref, false);
|
|
1225
|
+
break;
|
|
1226
|
+
case 'text': {
|
|
1227
|
+
const objectId = await browser.getObjectIdForRef(client, sessionId, ref);
|
|
1228
|
+
if (objectId) {
|
|
1229
|
+
const r = await client.send('Runtime.callFunctionOn', {
|
|
1230
|
+
functionDeclaration: 'function() { return this.innerText || this.textContent || ""; }',
|
|
1231
|
+
objectId,
|
|
1232
|
+
returnByValue: true,
|
|
1233
|
+
}, sessionId);
|
|
1234
|
+
print(r.result?.value ?? '');
|
|
1235
|
+
}
|
|
1236
|
+
break;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return { success: true, data: { ref } };
|
|
1241
|
+
},
|
|
1242
|
+
};
|
|
1243
|
+
const highlightCommand = {
|
|
1244
|
+
name: 'highlight',
|
|
1245
|
+
description: 'Highlight an element for 2 seconds. Usage: monomind browse highlight @e1',
|
|
1246
|
+
action: async (ctx) => {
|
|
1247
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1248
|
+
const browser = await getBrowser();
|
|
1249
|
+
const refArg = ctx.args[0];
|
|
1250
|
+
if (!refArg)
|
|
1251
|
+
throw new Error('Usage: monomind browse highlight @e1');
|
|
1252
|
+
const refKey = refArg.startsWith('@') ? refArg.slice(1) : refArg;
|
|
1253
|
+
const ref = await browser.resolveRef(client, sessionId, _refs, refKey);
|
|
1254
|
+
await browser.highlightElement(client, sessionId, ref);
|
|
1255
|
+
output.printSuccess(`Highlighted: ${ref.role} "${ref.name}"`);
|
|
1256
|
+
return { success: true };
|
|
1257
|
+
},
|
|
1258
|
+
};
|
|
1259
|
+
const pushstateCommand = {
|
|
1260
|
+
name: 'pushstate',
|
|
1261
|
+
description: 'SPA navigation via pushState. Usage: monomind browse pushstate /path',
|
|
1262
|
+
action: async (ctx) => {
|
|
1263
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1264
|
+
const browser = await getBrowser();
|
|
1265
|
+
const url = ctx.args[0];
|
|
1266
|
+
if (!url)
|
|
1267
|
+
throw new Error('Usage: monomind browse pushstate <url>');
|
|
1268
|
+
await browser.pushState(client, sessionId, url);
|
|
1269
|
+
output.printSuccess(`pushState: ${url}`);
|
|
1270
|
+
return { success: true };
|
|
1271
|
+
},
|
|
1272
|
+
};
|
|
1273
|
+
const batchCommand = {
|
|
1274
|
+
name: 'batch',
|
|
1275
|
+
description: 'Execute multiple commands. Usage: monomind browse batch "open url" "snapshot -i" "click @e1"',
|
|
1276
|
+
options: [
|
|
1277
|
+
{ name: 'bail', type: 'boolean', description: 'Stop on first error', default: false },
|
|
1278
|
+
{ name: 'json', type: 'boolean', description: 'Input from JSON stdin', default: false },
|
|
1279
|
+
],
|
|
1280
|
+
action: async (ctx) => {
|
|
1281
|
+
const commands = ctx.args;
|
|
1282
|
+
if (commands.length === 0)
|
|
1283
|
+
throw new Error('Usage: monomind browse batch "cmd1" "cmd2" ...');
|
|
1284
|
+
const results = [];
|
|
1285
|
+
for (const cmdStr of commands) {
|
|
1286
|
+
const parts = cmdStr.trim().split(/\s+/);
|
|
1287
|
+
const subName = parts[0];
|
|
1288
|
+
const subArgs = parts.slice(1);
|
|
1289
|
+
const subCmd = browseCommand.subcommands?.find((s) => s.name === subName);
|
|
1290
|
+
if (!subCmd?.action) {
|
|
1291
|
+
const err = `Unknown command: ${subName}`;
|
|
1292
|
+
results.push({ command: cmdStr, success: false, error: err });
|
|
1293
|
+
if (ctx.flags.bail)
|
|
1294
|
+
break;
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
const parsedFlags = { _: [] };
|
|
1299
|
+
// Parse --flags from subArgs
|
|
1300
|
+
for (let i = 0; i < subArgs.length; i++) {
|
|
1301
|
+
if (subArgs[i].startsWith('--')) {
|
|
1302
|
+
const key = subArgs[i].slice(2);
|
|
1303
|
+
const next = subArgs[i + 1];
|
|
1304
|
+
if (next && !next.startsWith('--')) {
|
|
1305
|
+
parsedFlags[key] = next;
|
|
1306
|
+
i++;
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
parsedFlags[key] = true;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const fakeCtx = {
|
|
1314
|
+
args: subArgs.filter((a) => !a.startsWith('--')),
|
|
1315
|
+
flags: parsedFlags,
|
|
1316
|
+
cwd: ctx.cwd,
|
|
1317
|
+
interactive: false,
|
|
1318
|
+
};
|
|
1319
|
+
await subCmd.action(fakeCtx);
|
|
1320
|
+
results.push({ command: cmdStr, success: true });
|
|
1321
|
+
}
|
|
1322
|
+
catch (e) {
|
|
1323
|
+
const err = e instanceof Error ? e.message : String(e);
|
|
1324
|
+
results.push({ command: cmdStr, success: false, error: err });
|
|
1325
|
+
output.printWarning(`Batch error in "${cmdStr}": ${err}`);
|
|
1326
|
+
if (ctx.flags.bail)
|
|
1327
|
+
break;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
const failed = results.filter((r) => !r.success).length;
|
|
1331
|
+
output.printInfo(`Batch: ${results.length - failed}/${results.length} succeeded`);
|
|
1332
|
+
return { success: failed === 0, data: { results } };
|
|
1333
|
+
},
|
|
1334
|
+
};
|
|
1335
|
+
const addinitscriptCommand = {
|
|
1336
|
+
name: 'addinitscript',
|
|
1337
|
+
description: 'Add script to run before page navigation. Usage: monomind browse addinitscript "window.x=1"',
|
|
1338
|
+
action: async (ctx) => {
|
|
1339
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1340
|
+
const browser = await getBrowser();
|
|
1341
|
+
const script = ctx.args[0];
|
|
1342
|
+
if (!script)
|
|
1343
|
+
throw new Error('Usage: monomind browse addinitscript "<js>"');
|
|
1344
|
+
const id = await browser.addInitScript(client, sessionId, script);
|
|
1345
|
+
output.printSuccess(`Init script added: ${id}`);
|
|
1346
|
+
return { success: true, data: { identifier: id } };
|
|
1347
|
+
},
|
|
1348
|
+
};
|
|
1349
|
+
const removeinitscriptCommand = {
|
|
1350
|
+
name: 'removeinitscript',
|
|
1351
|
+
description: 'Remove a previously added init script. Usage: monomind browse removeinitscript <id>',
|
|
1352
|
+
action: async (ctx) => {
|
|
1353
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1354
|
+
const browser = await getBrowser();
|
|
1355
|
+
const id = ctx.args[0];
|
|
1356
|
+
if (!id)
|
|
1357
|
+
throw new Error('Usage: monomind browse removeinitscript <identifier>');
|
|
1358
|
+
await browser.removeInitScript(client, sessionId, id);
|
|
1359
|
+
output.printSuccess(`Init script removed: ${id}`);
|
|
1360
|
+
return { success: true };
|
|
1361
|
+
},
|
|
1362
|
+
};
|
|
1363
|
+
// ---------------------------------------------------------------------------
|
|
1364
|
+
// Root browse command
|
|
1365
|
+
// ---------------------------------------------------------------------------
|
|
1366
|
+
const browseCommand = {
|
|
1367
|
+
name: 'browse',
|
|
1368
|
+
description: 'Native browser automation via Chrome DevTools Protocol',
|
|
1369
|
+
subcommands: [
|
|
1370
|
+
openCommand,
|
|
1371
|
+
snapshotCommand,
|
|
1372
|
+
clickCommand,
|
|
1373
|
+
dblclickCommand,
|
|
1374
|
+
fillCommand,
|
|
1375
|
+
typeCommand,
|
|
1376
|
+
pressCommand,
|
|
1377
|
+
keyboardCommand,
|
|
1378
|
+
keydownCommand,
|
|
1379
|
+
keyupCommand,
|
|
1380
|
+
hoverCommand,
|
|
1381
|
+
focusCommand,
|
|
1382
|
+
selectCommand,
|
|
1383
|
+
checkCommand,
|
|
1384
|
+
uncheckCommand,
|
|
1385
|
+
scrollIntoViewCommand,
|
|
1386
|
+
dragCommand,
|
|
1387
|
+
uploadCommand,
|
|
1388
|
+
mouseCommand,
|
|
1389
|
+
clipboardCommand,
|
|
1390
|
+
waitCommand,
|
|
1391
|
+
screenshotCommand,
|
|
1392
|
+
getCommand,
|
|
1393
|
+
scrollCommand,
|
|
1394
|
+
navigateCommand,
|
|
1395
|
+
setCommand,
|
|
1396
|
+
stateCommand,
|
|
1397
|
+
networkCommand,
|
|
1398
|
+
evalCommand,
|
|
1399
|
+
dialogCommand,
|
|
1400
|
+
frameCommand,
|
|
1401
|
+
tabCommand,
|
|
1402
|
+
consoleLogCommand,
|
|
1403
|
+
errorsCommand,
|
|
1404
|
+
storageCommand,
|
|
1405
|
+
cookiesCommand,
|
|
1406
|
+
pdfCommand,
|
|
1407
|
+
isCommand,
|
|
1408
|
+
findCommand,
|
|
1409
|
+
highlightCommand,
|
|
1410
|
+
pushstateCommand,
|
|
1411
|
+
batchCommand,
|
|
1412
|
+
addinitscriptCommand,
|
|
1413
|
+
removeinitscriptCommand,
|
|
1414
|
+
closeCommand,
|
|
1415
|
+
],
|
|
1416
|
+
options: [
|
|
1417
|
+
{ name: 'port', short: 'p', type: 'number', description: 'CDP debug port', default: 9222 },
|
|
1418
|
+
{ name: 'session', short: 's', type: 'string', description: 'Named session to use' },
|
|
1419
|
+
],
|
|
1420
|
+
examples: [
|
|
1421
|
+
{ command: 'monomind browse open https://example.com', description: 'Open a URL' },
|
|
1422
|
+
{ command: 'monomind browse snapshot -i', description: 'Interactive-only snapshot (93% token reduction)' },
|
|
1423
|
+
{ command: 'monomind browse click @e3', description: 'Click element by ref' },
|
|
1424
|
+
{ command: 'monomind browse fill @e1 "user@example.com"', description: 'Fill an input' },
|
|
1425
|
+
{ command: 'monomind browse press Enter', description: 'Press Enter key' },
|
|
1426
|
+
{ command: 'monomind browse wait --url "**/dashboard"', description: 'Wait for URL pattern' },
|
|
1427
|
+
{ command: 'monomind browse wait --text "Success"', description: 'Wait for text' },
|
|
1428
|
+
{ command: 'monomind browse wait --load networkidle', description: 'Wait for network idle' },
|
|
1429
|
+
{ command: 'monomind browse screenshot ./output.png', description: 'Take screenshot' },
|
|
1430
|
+
{ command: 'monomind browse get url', description: 'Get current URL' },
|
|
1431
|
+
{ command: 'monomind browse scroll down', description: 'Scroll down 300px' },
|
|
1432
|
+
{ command: 'monomind browse set viewport 375 812', description: 'Set mobile viewport' },
|
|
1433
|
+
{ command: 'monomind browse state save my-session', description: 'Save session state' },
|
|
1434
|
+
{ command: 'monomind browse navigate back', description: 'Navigate back' },
|
|
1435
|
+
{ command: 'monomind browse eval "document.title"', description: 'Evaluate JavaScript' },
|
|
1436
|
+
{ command: 'monomind browse network route --pattern "https://api.*" --abort', description: 'Abort API calls' },
|
|
1437
|
+
{ command: 'monomind browse close', description: 'Close browser session' },
|
|
1438
|
+
],
|
|
1439
|
+
action: async (_ctx) => {
|
|
1440
|
+
output.printInfo('Native browser automation via Chrome DevTools Protocol.');
|
|
1441
|
+
output.printInfo('');
|
|
1442
|
+
output.printInfo('Usage: monomind browse <subcommand> [options]');
|
|
1443
|
+
output.printInfo('');
|
|
1444
|
+
output.printInfo('Subcommands:');
|
|
1445
|
+
output.printInfo(' open Open a URL');
|
|
1446
|
+
output.printInfo(' snapshot Capture accessibility snapshot with refs');
|
|
1447
|
+
output.printInfo(' click Click an element by ref');
|
|
1448
|
+
output.printInfo(' dblclick Double-click an element');
|
|
1449
|
+
output.printInfo(' fill Fill an input (clears first)');
|
|
1450
|
+
output.printInfo(' type Type into element (appends)');
|
|
1451
|
+
output.printInfo(' press Press a keyboard key');
|
|
1452
|
+
output.printInfo(' keyboard Insert text directly');
|
|
1453
|
+
output.printInfo(' keydown Hold a key down');
|
|
1454
|
+
output.printInfo(' keyup Release a held key');
|
|
1455
|
+
output.printInfo(' hover Hover over element');
|
|
1456
|
+
output.printInfo(' focus Focus an element');
|
|
1457
|
+
output.printInfo(' select Select a dropdown option');
|
|
1458
|
+
output.printInfo(' check Check a checkbox');
|
|
1459
|
+
output.printInfo(' uncheck Uncheck a checkbox');
|
|
1460
|
+
output.printInfo(' scrollintoview Scroll element into view');
|
|
1461
|
+
output.printInfo(' drag Drag element to another element');
|
|
1462
|
+
output.printInfo(' upload Upload file(s) to file input');
|
|
1463
|
+
output.printInfo(' mouse Fine-grained mouse control');
|
|
1464
|
+
output.printInfo(' clipboard Read/write clipboard');
|
|
1465
|
+
output.printInfo(' wait Wait for a condition');
|
|
1466
|
+
output.printInfo(' screenshot Take a screenshot');
|
|
1467
|
+
output.printInfo(' get Get page info (url, title, text, html)');
|
|
1468
|
+
output.printInfo(' scroll Scroll the page');
|
|
1469
|
+
output.printInfo(' navigate Navigate history (back/forward/reload)');
|
|
1470
|
+
output.printInfo(' set Configure viewport, device, user agent');
|
|
1471
|
+
output.printInfo(' state Save/load/list session state');
|
|
1472
|
+
output.printInfo(' network Network interception and cookies');
|
|
1473
|
+
output.printInfo(' eval Evaluate JavaScript');
|
|
1474
|
+
output.printInfo(' dialog Handle browser dialogs');
|
|
1475
|
+
output.printInfo(' frame Switch to iframe');
|
|
1476
|
+
output.printInfo(' tab Tab management');
|
|
1477
|
+
output.printInfo(' console View captured console messages');
|
|
1478
|
+
output.printInfo(' errors View page JS errors');
|
|
1479
|
+
output.printInfo(' storage localStorage/sessionStorage management');
|
|
1480
|
+
output.printInfo(' cookies Cookie management');
|
|
1481
|
+
output.printInfo(' pdf Save page as PDF');
|
|
1482
|
+
output.printInfo(' is Check element state (visible/enabled/checked)');
|
|
1483
|
+
output.printInfo(' find Find elements by semantic locators');
|
|
1484
|
+
output.printInfo(' highlight Highlight an element visually');
|
|
1485
|
+
output.printInfo(' pushstate SPA navigation via pushState');
|
|
1486
|
+
output.printInfo(' batch Execute multiple commands');
|
|
1487
|
+
output.printInfo(' addinitscript Add script to run before page navigation');
|
|
1488
|
+
output.printInfo(' removeinitscript Remove a previously added init script');
|
|
1489
|
+
output.printInfo(' close Close the browser session');
|
|
1490
|
+
return { success: true };
|
|
1491
|
+
},
|
|
1492
|
+
};
|
|
1493
|
+
export default browseCommand;
|
|
1494
|
+
//# sourceMappingURL=browse.js.map
|