@monoes/monobrowse 1.0.13 → 1.0.15
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/dist/src/__tests__/bridge-instruments.test.d.ts +2 -0
- package/dist/src/__tests__/bridge-instruments.test.d.ts.map +1 -0
- package/dist/src/__tests__/bridge-instruments.test.js +132 -0
- package/dist/src/__tests__/bridge-instruments.test.js.map +1 -0
- package/dist/src/__tests__/bridge-transport.test.d.ts +2 -0
- package/dist/src/__tests__/bridge-transport.test.d.ts.map +1 -0
- package/dist/src/__tests__/bridge-transport.test.js +287 -0
- package/dist/src/__tests__/bridge-transport.test.js.map +1 -0
- package/dist/src/__tests__/cdp-transport.test.d.ts +2 -0
- package/dist/src/__tests__/cdp-transport.test.d.ts.map +1 -0
- package/dist/src/__tests__/cdp-transport.test.js +167 -0
- package/dist/src/__tests__/cdp-transport.test.js.map +1 -0
- package/dist/src/browser/bridge.d.ts +119 -0
- package/dist/src/browser/bridge.d.ts.map +1 -0
- package/dist/src/browser/bridge.js +264 -0
- package/dist/src/browser/bridge.js.map +1 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +11 -0
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts +11 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +60 -56
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/index.d.ts +2 -0
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +2 -0
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/transport.d.ts +62 -0
- package/dist/src/browser/transport.d.ts.map +1 -0
- package/dist/src/browser/transport.js +65 -0
- package/dist/src/browser/transport.js.map +1 -0
- package/dist/src/cli/commander-adapter.d.ts +16 -0
- package/dist/src/cli/commander-adapter.d.ts.map +1 -0
- package/dist/src/cli/commander-adapter.js +68 -0
- package/dist/src/cli/commander-adapter.js.map +1 -0
- package/dist/src/cli/commands-context.d.ts +14 -0
- package/dist/src/cli/commands-context.d.ts.map +1 -0
- package/dist/src/cli/commands-context.js +351 -0
- package/dist/src/cli/commands-context.js.map +1 -0
- package/dist/src/cli/commands-data.d.ts +12 -0
- package/dist/src/cli/commands-data.d.ts.map +1 -0
- package/dist/src/cli/commands-data.js +246 -0
- package/dist/src/cli/commands-data.js.map +1 -0
- package/dist/src/cli/commands-element.d.ts +15 -0
- package/dist/src/cli/commands-element.d.ts.map +1 -0
- package/dist/src/cli/commands-element.js +262 -0
- package/dist/src/cli/commands-element.js.map +1 -0
- package/dist/src/cli/commands-files.d.ts +9 -0
- package/dist/src/cli/commands-files.d.ts.map +1 -0
- package/dist/src/cli/commands-files.js +207 -0
- package/dist/src/cli/commands-files.js.map +1 -0
- package/dist/src/cli/commands-input.d.ts +26 -0
- package/dist/src/cli/commands-input.d.ts.map +1 -0
- package/dist/src/cli/commands-input.js +399 -0
- package/dist/src/cli/commands-input.js.map +1 -0
- package/dist/src/cli/commands-navigate.d.ts +12 -0
- package/dist/src/cli/commands-navigate.d.ts.map +1 -0
- package/dist/src/cli/commands-navigate.js +389 -0
- package/dist/src/cli/commands-navigate.js.map +1 -0
- package/dist/src/cli/commands-page.d.ts +25 -0
- package/dist/src/cli/commands-page.d.ts.map +1 -0
- package/dist/src/cli/commands-page.js +377 -0
- package/dist/src/cli/commands-page.js.map +1 -0
- package/dist/src/cli/commands-report.d.ts +8 -0
- package/dist/src/cli/commands-report.d.ts.map +1 -0
- package/dist/src/cli/commands-report.js +256 -0
- package/dist/src/cli/commands-report.js.map +1 -0
- package/dist/src/cli/commands-script.d.ts +43 -0
- package/dist/src/cli/commands-script.d.ts.map +1 -0
- package/dist/src/cli/commands-script.js +168 -0
- package/dist/src/cli/commands-script.js.map +1 -0
- package/dist/src/cli/commands-session.d.ts +15 -0
- package/dist/src/cli/commands-session.d.ts.map +1 -0
- package/dist/src/cli/commands-session.js +357 -0
- package/dist/src/cli/commands-session.js.map +1 -0
- package/dist/src/cli/commands-trace.d.ts +12 -0
- package/dist/src/cli/commands-trace.d.ts.map +1 -0
- package/dist/src/cli/commands-trace.js +347 -0
- package/dist/src/cli/commands-trace.js.map +1 -0
- package/dist/src/cli/commands.d.ts +22 -42
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +42 -3648
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/session.d.ts +47 -0
- package/dist/src/cli/session.d.ts.map +1 -0
- package/dist/src/cli/session.js +343 -0
- package/dist/src/cli/session.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/__tests__/bridge-instruments.test.ts +180 -0
- package/src/__tests__/bridge-transport.test.ts +365 -0
- package/src/__tests__/cdp-transport.test.ts +198 -0
- package/src/browser/bridge.ts +354 -0
- package/src/browser/browser.ts +12 -0
- package/src/browser/cdp.ts +57 -52
- package/src/browser/index.ts +2 -0
- package/src/browser/transport.ts +111 -0
- package/src/cli/commander-adapter.ts +65 -0
- package/src/cli/commands-context.ts +367 -0
- package/src/cli/commands-data.ts +248 -0
- package/src/cli/commands-element.ts +305 -0
- package/src/cli/commands-files.ts +248 -0
- package/src/cli/commands-input.ts +434 -0
- package/src/cli/commands-navigate.ts +442 -0
- package/src/cli/commands-page.ts +420 -0
- package/src/cli/commands-report.ts +275 -0
- package/src/cli/commands-script.ts +183 -0
- package/src/cli/commands-session.ts +376 -0
- package/src/cli/commands-trace.ts +349 -0
- package/src/cli/commands.ts +95 -3943
- package/src/cli/session.ts +412 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browsing context — which page, frame or window the session currently acts
|
|
3
|
+
* on, and saving or restoring that context.
|
|
4
|
+
*
|
|
5
|
+
* `frame`/`tab`/`window` retarget the session (an OOPIF gets its own CDP
|
|
6
|
+
* sessionId, which is why the parent is saved for `frame main`). `state`
|
|
7
|
+
* persists cookies and storage so a later run can pick the context back up.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { output } from './output.js';
|
|
11
|
+
import { ensureConnected, getBrowser, print, session } from './session.js';
|
|
12
|
+
import type { Command, CommandContext, CommandResult } from './types.js';
|
|
13
|
+
|
|
14
|
+
export const stateCommand: Command = {
|
|
15
|
+
name: 'state',
|
|
16
|
+
description:
|
|
17
|
+
'Manage browser session state. Usage: monomind browse state save|load|list|rename|clean [name]',
|
|
18
|
+
options: [
|
|
19
|
+
{
|
|
20
|
+
name: 'older-than',
|
|
21
|
+
type: 'number',
|
|
22
|
+
description: 'For state clean: remove sessions older than N days',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
26
|
+
const browser = await getBrowser();
|
|
27
|
+
const action = ctx.args[0] as string;
|
|
28
|
+
if (!action) throw new Error('Usage: monomind browse state save|load|list [name]');
|
|
29
|
+
|
|
30
|
+
switch (action) {
|
|
31
|
+
case 'list': {
|
|
32
|
+
const sessions = await browser.listSessions();
|
|
33
|
+
if (sessions.length === 0) {
|
|
34
|
+
output.printInfo('No saved sessions');
|
|
35
|
+
} else {
|
|
36
|
+
output.printInfo('Saved sessions:');
|
|
37
|
+
for (const s of sessions) print(` ${s}`);
|
|
38
|
+
}
|
|
39
|
+
return { success: true, data: { sessions } };
|
|
40
|
+
}
|
|
41
|
+
case 'save': {
|
|
42
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
43
|
+
const target = ctx.args[1] as string;
|
|
44
|
+
if (!target) throw new Error('Usage: monomind browse state save <name-or-file>');
|
|
45
|
+
const url = await browser.getCurrentUrl(client, sessionId);
|
|
46
|
+
const title = await browser.getCurrentTitle(client, sessionId);
|
|
47
|
+
if (target.endsWith('.json')) {
|
|
48
|
+
await browser.saveStateFile(client, sessionId, session.targetId, target, url, title);
|
|
49
|
+
output.printSuccess(`State saved to ${target}`);
|
|
50
|
+
} else {
|
|
51
|
+
const path = await browser.saveSession(
|
|
52
|
+
client,
|
|
53
|
+
sessionId,
|
|
54
|
+
session.targetId,
|
|
55
|
+
target,
|
|
56
|
+
url,
|
|
57
|
+
title,
|
|
58
|
+
);
|
|
59
|
+
output.printSuccess(`Session "${target}" saved to ${path}`);
|
|
60
|
+
}
|
|
61
|
+
return { success: true };
|
|
62
|
+
}
|
|
63
|
+
case 'load': {
|
|
64
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
65
|
+
const target = ctx.args[1] as string;
|
|
66
|
+
if (!target) throw new Error('Usage: monomind browse state load <name-or-file>');
|
|
67
|
+
if (target.endsWith('.json')) {
|
|
68
|
+
await browser.loadStateFile(client, sessionId, target);
|
|
69
|
+
} else {
|
|
70
|
+
await browser.loadSession(client, sessionId, target);
|
|
71
|
+
}
|
|
72
|
+
output.printSuccess(`State loaded from ${target}`);
|
|
73
|
+
return { success: true };
|
|
74
|
+
}
|
|
75
|
+
case 'show': {
|
|
76
|
+
const { client: c, sessionId: sid } = await ensureConnected(session.port);
|
|
77
|
+
const url = await browser.getCurrentUrl(c, sid);
|
|
78
|
+
const title = await browser.getCurrentTitle(c, sid);
|
|
79
|
+
const cookies = await browser.getCookies(c, sid);
|
|
80
|
+
const ls = await browser.getAllLocalStorage(c, sid);
|
|
81
|
+
const info = {
|
|
82
|
+
url,
|
|
83
|
+
title,
|
|
84
|
+
cookies: cookies.length,
|
|
85
|
+
localStorage: Object.keys(ls).length,
|
|
86
|
+
refs: session.refs.size,
|
|
87
|
+
};
|
|
88
|
+
print(JSON.stringify(info, null, 2));
|
|
89
|
+
return { success: true, data: info };
|
|
90
|
+
}
|
|
91
|
+
case 'clear': {
|
|
92
|
+
const { client: c, sessionId: sid } = await ensureConnected(session.port);
|
|
93
|
+
await browser.clearCookies(c, sid);
|
|
94
|
+
await browser.clearLocalStorage(c, sid);
|
|
95
|
+
await browser.clearSessionStorage(c, sid);
|
|
96
|
+
session.refs = new Map();
|
|
97
|
+
await browser.clearRefCache();
|
|
98
|
+
output.printSuccess('Browser state cleared (cookies, localStorage, sessionStorage, refs)');
|
|
99
|
+
return { success: true };
|
|
100
|
+
}
|
|
101
|
+
case 'rename': {
|
|
102
|
+
const oldName = ctx.args[1] as string;
|
|
103
|
+
const newName = ctx.args[2] as string;
|
|
104
|
+
if (!oldName || !newName)
|
|
105
|
+
throw new Error('Usage: monomind browse state rename <old-name> <new-name>');
|
|
106
|
+
const sessions = await browser.listSessions();
|
|
107
|
+
if (!sessions.includes(oldName)) throw new Error(`Session not found: ${oldName}`);
|
|
108
|
+
// W1: validate newName to prevent path traversal
|
|
109
|
+
const { basename: basenameFn } = await import('node:path');
|
|
110
|
+
const safeName = basenameFn(newName);
|
|
111
|
+
if (safeName !== newName || safeName.startsWith('.') || safeName.includes('/')) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
'Invalid session name — must not contain path separators or start with "."',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const {
|
|
117
|
+
unlink: unlinkRename,
|
|
118
|
+
readFile,
|
|
119
|
+
writeFile,
|
|
120
|
+
mkdir: mkdirRename,
|
|
121
|
+
} = await import('node:fs/promises');
|
|
122
|
+
const { join: joinR } = await import('node:path');
|
|
123
|
+
const { homedir } = await import('node:os');
|
|
124
|
+
const sessionDir = joinR(homedir(), '.monomind', 'browser-sessions');
|
|
125
|
+
const oldPath = joinR(sessionDir, `${oldName}.json`);
|
|
126
|
+
const newPath = joinR(sessionDir, `${newName}.json`);
|
|
127
|
+
const data = JSON.parse(await readFile(oldPath, 'utf8'));
|
|
128
|
+
data.name = newName;
|
|
129
|
+
await mkdirRename(sessionDir, { recursive: true });
|
|
130
|
+
await writeFile(newPath, JSON.stringify(data, null, 2), 'utf8');
|
|
131
|
+
await unlinkRename(oldPath).catch(() => {}); // C1: delete old file (not rename to /dev/null)
|
|
132
|
+
output.printSuccess(`Session renamed: ${oldName} → ${newName}`);
|
|
133
|
+
return { success: true };
|
|
134
|
+
}
|
|
135
|
+
case 'clean': {
|
|
136
|
+
const days = (ctx.flags['older-than'] as number) ?? 7;
|
|
137
|
+
const { unlink, stat } = await import('node:fs/promises');
|
|
138
|
+
const { join: joinC } = await import('node:path');
|
|
139
|
+
const { homedir: homedirC } = await import('node:os');
|
|
140
|
+
const sessionDir = joinC(homedirC(), '.monomind', 'browser-sessions');
|
|
141
|
+
const sessions = await browser.listSessions();
|
|
142
|
+
const cutoff = Date.now() - days * 86400 * 1000;
|
|
143
|
+
let removed = 0;
|
|
144
|
+
for (const name of sessions) {
|
|
145
|
+
const p = joinC(sessionDir, `${name}.json`);
|
|
146
|
+
const s = await stat(p).catch(() => null);
|
|
147
|
+
if (s && s.mtimeMs < cutoff) {
|
|
148
|
+
await unlink(p).catch(() => {});
|
|
149
|
+
removed++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
output.printSuccess(`Cleaned ${removed} session(s) older than ${days} days`);
|
|
153
|
+
return { success: true, data: { removed } };
|
|
154
|
+
}
|
|
155
|
+
default:
|
|
156
|
+
throw new Error(`Unknown action: ${action}. Use: save|load|list|show|clear|rename|clean`);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const frameCommand: Command = {
|
|
162
|
+
name: 'frame',
|
|
163
|
+
description:
|
|
164
|
+
'Switch to iframe or back to main. Usage: monomind browse frame "#frame-id" | frame main',
|
|
165
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
166
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
167
|
+
const browser = await getBrowser();
|
|
168
|
+
const target = ctx.args[0] as string;
|
|
169
|
+
if (!target) throw new Error('Usage: monomind browse frame <selector>|main');
|
|
170
|
+
|
|
171
|
+
if (target === 'main') {
|
|
172
|
+
if (session.parentSessionId) {
|
|
173
|
+
// Detach from the OOPIF session (best-effort — it may already be gone)
|
|
174
|
+
await client
|
|
175
|
+
.send('Target.detachFromTarget', { sessionId: session.sessionId })
|
|
176
|
+
.catch(() => {});
|
|
177
|
+
session.sessionId = session.parentSessionId;
|
|
178
|
+
session.parentSessionId = '';
|
|
179
|
+
}
|
|
180
|
+
output.printSuccess('Switched to main frame');
|
|
181
|
+
} else {
|
|
182
|
+
const frameResult = await browser.switchToFrame(client, sessionId, target);
|
|
183
|
+
if (frameResult.sessionId) {
|
|
184
|
+
// Save the parent session so `frame main` can restore it
|
|
185
|
+
session.parentSessionId = session.sessionId;
|
|
186
|
+
session.sessionId = frameResult.sessionId;
|
|
187
|
+
// Enable CDP domains on the iframe's session so subsequent commands work
|
|
188
|
+
await browser.enableSessionDomains(client, session.sessionId);
|
|
189
|
+
}
|
|
190
|
+
output.printSuccess(`Switched to frame: ${frameResult.url ?? target}`);
|
|
191
|
+
}
|
|
192
|
+
return { success: true };
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const tabCommand: Command = {
|
|
197
|
+
name: 'tab',
|
|
198
|
+
description: 'Tab management. Usage: monomind browse tab list|new|close [url]',
|
|
199
|
+
options: [{ name: 'label', type: 'string', description: 'Label for new tab' }],
|
|
200
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
201
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
202
|
+
const browser = await getBrowser();
|
|
203
|
+
const action = ctx.args[0] as string;
|
|
204
|
+
|
|
205
|
+
switch (action ?? 'list') {
|
|
206
|
+
case 'list': {
|
|
207
|
+
const tabs = await browser.listTabs(session.port);
|
|
208
|
+
for (const t of tabs) print(` ${t.id}: ${t.title} (${t.url})`);
|
|
209
|
+
return { success: true, data: { tabs } };
|
|
210
|
+
}
|
|
211
|
+
case 'new': {
|
|
212
|
+
const url = ctx.args[1] as string | undefined;
|
|
213
|
+
const tab = await browser.newTab(session.port, url);
|
|
214
|
+
output.printSuccess(`New tab: ${tab.id} ${url ?? ''}`);
|
|
215
|
+
return { success: true, data: { tab } };
|
|
216
|
+
}
|
|
217
|
+
case 'close': {
|
|
218
|
+
const tabId = ctx.args[1] as string;
|
|
219
|
+
if (!tabId) throw new Error('Usage: monomind browse tab close <tabId>');
|
|
220
|
+
if (tabId === session.targetId) {
|
|
221
|
+
const sid = session.sessionId;
|
|
222
|
+
// Stop profiling before closing the tab so CDP commands still reach the live session
|
|
223
|
+
if (browser.getHarStatus(sid).recording) {
|
|
224
|
+
try {
|
|
225
|
+
await browser.stopHarRecording(client, sid);
|
|
226
|
+
} catch {
|
|
227
|
+
/* ignore */
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (browser.getTraceStatus(sid)) {
|
|
231
|
+
try {
|
|
232
|
+
await browser.stopTrace(client, sid);
|
|
233
|
+
} catch {
|
|
234
|
+
/* ignore */
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (browser.isProfilingActive(sid)) {
|
|
238
|
+
try {
|
|
239
|
+
await browser.stopCpuProfile(client, sid);
|
|
240
|
+
} catch {
|
|
241
|
+
/* ignore */
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
browser.teardownRouteInterception(sid);
|
|
245
|
+
browser.stopRequestCapture(sid);
|
|
246
|
+
browser.teardownDialogHandling(sid);
|
|
247
|
+
browser.teardownConsoleCapture(sid);
|
|
248
|
+
await browser.closeTab(client, sessionId, tabId);
|
|
249
|
+
client.close();
|
|
250
|
+
session.client = null;
|
|
251
|
+
session.sessionId = '';
|
|
252
|
+
session.parentSessionId = '';
|
|
253
|
+
session.targetId = '';
|
|
254
|
+
session.refs = new Map();
|
|
255
|
+
} else {
|
|
256
|
+
await browser.closeTab(client, sessionId, tabId);
|
|
257
|
+
}
|
|
258
|
+
output.printSuccess(`Closed tab: ${tabId}`);
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
default: {
|
|
262
|
+
// Attach to new tab FIRST — only tear down old session if that succeeds
|
|
263
|
+
const newSid = await browser.activateTab(client, sessionId, action);
|
|
264
|
+
const oldSid = session.sessionId;
|
|
265
|
+
if (browser.getHarStatus(oldSid).recording) {
|
|
266
|
+
try {
|
|
267
|
+
await browser.stopHarRecording(client, oldSid);
|
|
268
|
+
} catch {
|
|
269
|
+
/* ignore */
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (browser.getTraceStatus(oldSid)) {
|
|
273
|
+
try {
|
|
274
|
+
await browser.stopTrace(client, oldSid);
|
|
275
|
+
} catch {
|
|
276
|
+
/* ignore */
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (browser.isProfilingActive(oldSid)) {
|
|
280
|
+
try {
|
|
281
|
+
await browser.stopCpuProfile(client, oldSid);
|
|
282
|
+
} catch {
|
|
283
|
+
/* ignore */
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
await browser.disableInterception(client, oldSid).catch(() => {});
|
|
287
|
+
browser.stopRequestCapture(oldSid);
|
|
288
|
+
browser.teardownDialogHandling(oldSid);
|
|
289
|
+
browser.teardownConsoleCapture(oldSid);
|
|
290
|
+
session.sessionId = newSid;
|
|
291
|
+
session.targetId = action;
|
|
292
|
+
session.refs = new Map();
|
|
293
|
+
await browser.enableSessionDomains(client, session.sessionId);
|
|
294
|
+
output.printSuccess(`Switched to tab: ${action}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { success: true };
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export const windowCommand: Command = {
|
|
302
|
+
name: 'window',
|
|
303
|
+
description: 'Browser window management. Usage: monomind browse window new [url]',
|
|
304
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
305
|
+
const { client, sessionId: _sid } = await ensureConnected(session.port);
|
|
306
|
+
const browser = await getBrowser();
|
|
307
|
+
const action = ctx.args[0] as string;
|
|
308
|
+
|
|
309
|
+
if (!action || action === 'new') {
|
|
310
|
+
// Create isolated browser context (incognito-like) with a fresh page
|
|
311
|
+
const ctxResult = await client.send<{ browserContextId: string }>(
|
|
312
|
+
'Target.createBrowserContext',
|
|
313
|
+
{},
|
|
314
|
+
undefined,
|
|
315
|
+
);
|
|
316
|
+
const browserContextId = ctxResult.browserContextId;
|
|
317
|
+
const url = (ctx.args[1] as string) || 'about:blank';
|
|
318
|
+
const targetResult = await client.send<{ targetId: string }>(
|
|
319
|
+
'Target.createTarget',
|
|
320
|
+
{ url, browserContextId },
|
|
321
|
+
undefined,
|
|
322
|
+
);
|
|
323
|
+
const targetId = targetResult.targetId;
|
|
324
|
+
const attachResult = await client.send<{ sessionId: string }>(
|
|
325
|
+
'Target.attachToTarget',
|
|
326
|
+
{ targetId, flatten: true },
|
|
327
|
+
undefined,
|
|
328
|
+
);
|
|
329
|
+
const newSessionId = attachResult.sessionId;
|
|
330
|
+
// W3: fully tear down old session before switching
|
|
331
|
+
const oldSid = session.sessionId;
|
|
332
|
+
if (browser.getHarStatus(oldSid).recording) {
|
|
333
|
+
try {
|
|
334
|
+
await browser.stopHarRecording(client, oldSid);
|
|
335
|
+
} catch {
|
|
336
|
+
/* ignore */
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (browser.getTraceStatus(oldSid)) {
|
|
340
|
+
try {
|
|
341
|
+
await browser.stopTrace(client, oldSid);
|
|
342
|
+
} catch {
|
|
343
|
+
/* ignore */
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (browser.isProfilingActive(oldSid)) {
|
|
347
|
+
try {
|
|
348
|
+
await browser.stopCpuProfile(client, oldSid);
|
|
349
|
+
} catch {
|
|
350
|
+
/* ignore */
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
browser.teardownRouteInterception(oldSid);
|
|
354
|
+
browser.stopRequestCapture(oldSid);
|
|
355
|
+
browser.teardownDialogHandling(oldSid);
|
|
356
|
+
browser.teardownConsoleCapture(oldSid);
|
|
357
|
+
session.sessionId = newSessionId;
|
|
358
|
+
session.targetId = targetId;
|
|
359
|
+
session.refs = new Map();
|
|
360
|
+
await browser.enableSessionDomains(client, session.sessionId);
|
|
361
|
+
output.printSuccess(`Opened new window (isolated context): ${targetId} [${url}]`);
|
|
362
|
+
return { success: true, data: { targetId, browserContextId, sessionId: newSessionId } };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
throw new Error(`Unknown window action: ${action}. Use: new`);
|
|
366
|
+
},
|
|
367
|
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's own data and event surfaces — localStorage/sessionStorage,
|
|
3
|
+
* cookies, the clipboard, dialogs, and the captured console/error logs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { output } from './output.js';
|
|
7
|
+
import { ensureConnected, getBrowser, print, session } from './session.js';
|
|
8
|
+
import type { Command, CommandContext, CommandResult } from './types.js';
|
|
9
|
+
|
|
10
|
+
export const clipboardCommand: Command = {
|
|
11
|
+
name: 'clipboard',
|
|
12
|
+
description: 'Clipboard operations. Usage: monomind browse clipboard read|write|copy|paste',
|
|
13
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
14
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
15
|
+
const browser = await getBrowser();
|
|
16
|
+
const action = ctx.args[0] as string;
|
|
17
|
+
|
|
18
|
+
switch (action) {
|
|
19
|
+
case 'read': {
|
|
20
|
+
const text = await browser.readClipboard(client, sessionId);
|
|
21
|
+
print(text);
|
|
22
|
+
return { success: true, data: { text } };
|
|
23
|
+
}
|
|
24
|
+
case 'write': {
|
|
25
|
+
const text = ctx.args[1] as string;
|
|
26
|
+
if (!text) throw new Error('Usage: monomind browse clipboard write "text"');
|
|
27
|
+
await browser.writeClipboard(client, sessionId, text);
|
|
28
|
+
output.printSuccess('Clipboard written');
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case 'copy': {
|
|
32
|
+
const mod = process.platform === 'darwin' ? 4 : 2; // Meta/Cmd on macOS, Ctrl elsewhere
|
|
33
|
+
await browser.pressKeyCombo(client, sessionId, 'c', mod);
|
|
34
|
+
output.printSuccess('Copy sent');
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case 'paste': {
|
|
38
|
+
const mod = process.platform === 'darwin' ? 4 : 2;
|
|
39
|
+
await browser.pressKeyCombo(client, sessionId, 'v', mod);
|
|
40
|
+
output.printSuccess('Paste sent');
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
default:
|
|
44
|
+
throw new Error('Usage: monomind browse clipboard read|write|copy|paste');
|
|
45
|
+
}
|
|
46
|
+
return { success: true };
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const dialogCommand: Command = {
|
|
51
|
+
name: 'dialog',
|
|
52
|
+
description: 'Handle browser dialogs. Usage: monomind browse dialog accept|dismiss|status',
|
|
53
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
54
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
55
|
+
const browser = await getBrowser();
|
|
56
|
+
const action = ctx.args[0] as string;
|
|
57
|
+
|
|
58
|
+
switch (action) {
|
|
59
|
+
case 'accept': {
|
|
60
|
+
const text = ctx.args[1] as string | undefined;
|
|
61
|
+
await browser.acceptDialog(client, sessionId, text);
|
|
62
|
+
output.printSuccess('Dialog accepted');
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case 'dismiss':
|
|
66
|
+
await browser.dismissDialog(client, sessionId);
|
|
67
|
+
output.printSuccess('Dialog dismissed');
|
|
68
|
+
break;
|
|
69
|
+
case 'status': {
|
|
70
|
+
const info = browser.getDialogStatus(sessionId);
|
|
71
|
+
if (info) {
|
|
72
|
+
print(`Dialog open: type=${info.type} message="${info.message}"`);
|
|
73
|
+
} else {
|
|
74
|
+
print('No dialog open');
|
|
75
|
+
}
|
|
76
|
+
return { success: true, data: { dialog: info } };
|
|
77
|
+
}
|
|
78
|
+
default:
|
|
79
|
+
throw new Error('Usage: monomind browse dialog accept|dismiss|status');
|
|
80
|
+
}
|
|
81
|
+
return { success: true };
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const consoleLogCommand: Command = {
|
|
86
|
+
name: 'console',
|
|
87
|
+
description: 'View captured console messages. Usage: monomind browse console [--clear] [--json]',
|
|
88
|
+
options: [
|
|
89
|
+
{ name: 'clear', type: 'boolean', description: 'Clear console messages', default: false },
|
|
90
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
91
|
+
{ name: 'errors-only', type: 'boolean', description: 'Show only errors', default: false },
|
|
92
|
+
],
|
|
93
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
94
|
+
const browser = await getBrowser();
|
|
95
|
+
if (ctx.flags.clear) {
|
|
96
|
+
browser.clearConsoleMessages(session.sessionId);
|
|
97
|
+
output.printSuccess('Console cleared');
|
|
98
|
+
return { success: true };
|
|
99
|
+
}
|
|
100
|
+
const allMsgs = browser.getConsoleMessages(session.sessionId);
|
|
101
|
+
const msgs = ctx.flags['errors-only'] ? allMsgs.filter((m) => m.type === 'error') : allMsgs;
|
|
102
|
+
if (ctx.flags.json) {
|
|
103
|
+
print(JSON.stringify(msgs));
|
|
104
|
+
} else {
|
|
105
|
+
for (const m of msgs) {
|
|
106
|
+
const prefix = m.type === 'error' ? '[ERROR]' : m.type === 'warn' ? '[WARN]' : '[LOG]';
|
|
107
|
+
print(`${prefix} ${m.text}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { success: true, data: { messages: msgs } };
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const errorsCommand: Command = {
|
|
115
|
+
name: 'errors',
|
|
116
|
+
description: 'View page errors (uncaught JS exceptions). Usage: monomind browse errors [--clear]',
|
|
117
|
+
options: [
|
|
118
|
+
{ name: 'clear', type: 'boolean', description: 'Clear errors', default: false },
|
|
119
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
120
|
+
],
|
|
121
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
122
|
+
const browser = await getBrowser();
|
|
123
|
+
if (ctx.flags.clear) {
|
|
124
|
+
browser.clearPageErrors(session.sessionId);
|
|
125
|
+
output.printSuccess('Errors cleared');
|
|
126
|
+
return { success: true };
|
|
127
|
+
}
|
|
128
|
+
const errs = browser.getPageErrors(session.sessionId);
|
|
129
|
+
if (ctx.flags.json) {
|
|
130
|
+
print(JSON.stringify(errs));
|
|
131
|
+
} else if (errs.length === 0) {
|
|
132
|
+
output.printSuccess('No page errors');
|
|
133
|
+
} else {
|
|
134
|
+
for (const e of errs) print(`[ERROR] ${e.text} (${e.url}:${e.lineNumber})`);
|
|
135
|
+
}
|
|
136
|
+
return { success: true, data: { errors: errs } };
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const storageCommand: Command = {
|
|
141
|
+
name: 'storage',
|
|
142
|
+
description:
|
|
143
|
+
'localStorage/sessionStorage management. Usage: monomind browse storage local|session [key] [--set val] [--clear]',
|
|
144
|
+
options: [
|
|
145
|
+
{ name: 'set', type: 'string', description: 'Value to set for key' },
|
|
146
|
+
{ name: 'clear', type: 'boolean', description: 'Clear all storage', default: false },
|
|
147
|
+
{ name: 'remove', type: 'boolean', description: 'Remove a specific key', default: false },
|
|
148
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
149
|
+
],
|
|
150
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
151
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
152
|
+
const browser = await getBrowser();
|
|
153
|
+
const storageType = ctx.args[0] as string;
|
|
154
|
+
const key = ctx.args[1] as string | undefined;
|
|
155
|
+
|
|
156
|
+
if (!storageType) throw new Error('Usage: monomind browse storage local|session [key]');
|
|
157
|
+
|
|
158
|
+
const isLocal = storageType === 'local';
|
|
159
|
+
|
|
160
|
+
if (ctx.flags.clear) {
|
|
161
|
+
if (isLocal) await browser.clearLocalStorage(client, sessionId);
|
|
162
|
+
else await browser.clearSessionStorage(client, sessionId);
|
|
163
|
+
output.printSuccess(`${storageType}Storage cleared`);
|
|
164
|
+
return { success: true };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (key && ctx.flags.set !== undefined) {
|
|
168
|
+
if (isLocal)
|
|
169
|
+
await browser.setLocalStorageKey(client, sessionId, key, ctx.flags.set as string);
|
|
170
|
+
else await browser.setSessionStorageKey(client, sessionId, key, ctx.flags.set as string);
|
|
171
|
+
output.printSuccess(`Set ${key}`);
|
|
172
|
+
return { success: true };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (key && ctx.flags.remove) {
|
|
176
|
+
if (isLocal) await browser.removeLocalStorageKey(client, sessionId, key);
|
|
177
|
+
else await browser.removeSessionStorageKey(client, sessionId, key);
|
|
178
|
+
output.printSuccess(`Removed ${key}`);
|
|
179
|
+
return { success: true };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (key) {
|
|
183
|
+
const val = isLocal
|
|
184
|
+
? await browser.getLocalStorageKey(client, sessionId, key)
|
|
185
|
+
: await browser.getSessionStorageKey(client, sessionId, key);
|
|
186
|
+
if (ctx.flags.json) print(JSON.stringify({ data: val }));
|
|
187
|
+
else print(val ?? '(null)');
|
|
188
|
+
return { success: true, data: { value: val } };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const all = isLocal
|
|
192
|
+
? await browser.getAllLocalStorage(client, sessionId)
|
|
193
|
+
: await browser.getAllSessionStorage(client, sessionId);
|
|
194
|
+
if (ctx.flags.json) print(JSON.stringify(all));
|
|
195
|
+
else {
|
|
196
|
+
for (const [k, v] of Object.entries(all)) print(` ${k}: ${v}`);
|
|
197
|
+
}
|
|
198
|
+
return { success: true, data: { storage: all } };
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const cookiesCommand: Command = {
|
|
203
|
+
name: 'cookies',
|
|
204
|
+
description: 'Cookie management. Usage: monomind browse cookies [list|set|clear]',
|
|
205
|
+
options: [
|
|
206
|
+
{ name: 'name', type: 'string', description: 'Cookie name' },
|
|
207
|
+
{ name: 'value', type: 'string', description: 'Cookie value' },
|
|
208
|
+
{ name: 'domain', type: 'string', description: 'Cookie domain' },
|
|
209
|
+
{ name: 'curl', type: 'string', description: 'Import cookies from cURL dump file' },
|
|
210
|
+
],
|
|
211
|
+
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
212
|
+
const { client, sessionId } = await ensureConnected(session.port);
|
|
213
|
+
const browser = await getBrowser();
|
|
214
|
+
const action = (ctx.args[0] as string) ?? 'list';
|
|
215
|
+
|
|
216
|
+
switch (action) {
|
|
217
|
+
case 'list': {
|
|
218
|
+
const cookies = await browser.getCookies(client, sessionId);
|
|
219
|
+
print(JSON.stringify(cookies, null, 2));
|
|
220
|
+
return { success: true, data: { cookies } };
|
|
221
|
+
}
|
|
222
|
+
case 'set': {
|
|
223
|
+
// Support both: cookies set --name n --value v AND cookies set <name> <value>
|
|
224
|
+
const name = (ctx.flags.name as string) ?? (ctx.args[1] as string);
|
|
225
|
+
const value = (ctx.flags.value as string) ?? (ctx.args[2] as string);
|
|
226
|
+
if (!name || value === undefined) {
|
|
227
|
+
throw new Error('Usage: monomind browse cookies set <name> <value> [--domain <d>]');
|
|
228
|
+
}
|
|
229
|
+
await browser.setCookies(client, sessionId, [
|
|
230
|
+
{
|
|
231
|
+
name,
|
|
232
|
+
value,
|
|
233
|
+
domain: ctx.flags.domain as string,
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
output.printSuccess(`Cookie set: ${name}`);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 'clear':
|
|
240
|
+
await browser.clearCookies(client, sessionId);
|
|
241
|
+
output.printSuccess('Cookies cleared');
|
|
242
|
+
break;
|
|
243
|
+
default:
|
|
244
|
+
throw new Error('Usage: monomind browse cookies list|set|clear');
|
|
245
|
+
}
|
|
246
|
+
return { success: true };
|
|
247
|
+
},
|
|
248
|
+
};
|