@bubstack/moe-glass 0.1.0
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/README.md +29 -0
- package/agents/browser-user.md +105 -0
- package/dist/LICENSE +25 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22517 -0
- package/dist/index.js.map +1 -0
- package/dist/payload.d.ts +214 -0
- package/dist/payload.d.ts.map +1 -0
- package/dist/payload.js +325 -0
- package/dist/payload.js.map +1 -0
- package/package.json +59 -0
- package/skills/browsing/COMMANDLINE-USAGE.md +595 -0
- package/skills/browsing/EXAMPLES.md +717 -0
- package/skills/browsing/README.md +55 -0
- package/skills/browsing/SKILL.md +478 -0
- package/skills/browsing/chrome-ws +1021 -0
- package/skills/browsing/chrome-ws-lib.js +461 -0
- package/skills/browsing/host-override.js +98 -0
- package/skills/browsing/lib/browser-bridge.js +175 -0
- package/skills/browsing/lib/browser-session.js +137 -0
- package/skills/browsing/lib/capture.js +499 -0
- package/skills/browsing/lib/cdp-router.js +72 -0
- package/skills/browsing/lib/cdp-utils.js +18 -0
- package/skills/browsing/lib/chrome-launcher-helpers.js +374 -0
- package/skills/browsing/lib/chrome-process.js +464 -0
- package/skills/browsing/lib/console-logging.js +70 -0
- package/skills/browsing/lib/cookies.js +17 -0
- package/skills/browsing/lib/dialogs-render.js +154 -0
- package/skills/browsing/lib/dialogs-router.js +117 -0
- package/skills/browsing/lib/dialogs.js +254 -0
- package/skills/browsing/lib/element-selector.js +91 -0
- package/skills/browsing/lib/evaluation.js +85 -0
- package/skills/browsing/lib/extraction.js +55 -0
- package/skills/browsing/lib/file-upload.js +56 -0
- package/skills/browsing/lib/html-diff.js +122 -0
- package/skills/browsing/lib/key-definitions.js +149 -0
- package/skills/browsing/lib/keyboard-input.js +288 -0
- package/skills/browsing/lib/mouse.js +423 -0
- package/skills/browsing/lib/navigation.js +272 -0
- package/skills/browsing/lib/page-scripts/dom-summary.js +31 -0
- package/skills/browsing/lib/page-scripts/markdown.js +85 -0
- package/skills/browsing/lib/page-scripts/permission-shim.js +80 -0
- package/skills/browsing/lib/page-session.js +106 -0
- package/skills/browsing/lib/profile-lock.js +179 -0
- package/skills/browsing/lib/screenshot.js +171 -0
- package/skills/browsing/lib/select-option.js +99 -0
- package/skills/browsing/lib/session-state.js +66 -0
- package/skills/browsing/lib/tabs.js +144 -0
- package/skills/browsing/lib/viewport.js +103 -0
- package/skills/browsing/lib/websocket-client.js +162 -0
- package/skills/browsing/package.json +11 -0
- package/skills/browsing/test-chrome-args.js +81 -0
- package/skills/browsing/test-cookies.js +21 -0
- package/skills/browsing/test-e2e.sh +51 -0
- package/skills/browsing/test-extract.sh +17 -0
- package/skills/browsing/test-interact.sh +11 -0
- package/skills/browsing/test-navigate.sh +9 -0
- package/skills/browsing/test-raw.sh +8 -0
- package/skills/browsing/test-tabs.sh +15 -0
- package/skills/browsing/test-viewport.js +27 -0
- package/skills/browsing/test-wait.sh +9 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome WebSocket Library - Core CDP automation functions
|
|
3
|
+
* Used by both CLI and MCP server
|
|
4
|
+
*
|
|
5
|
+
* Fixes implemented:
|
|
6
|
+
* - JRV-130: Connection pooling for persistent focus
|
|
7
|
+
* - JRV-127: keyboard_press action for special keys
|
|
8
|
+
* - JRV-123: React-compatible input via Input.insertText
|
|
9
|
+
* - JRV-124: React-compatible click via Input.dispatchMouseEvent
|
|
10
|
+
* - JRV-125: Tab key handling (via keyboard_press)
|
|
11
|
+
* - JRV-126: Better eval return handling
|
|
12
|
+
* - JRV-128: SPA navigation support
|
|
13
|
+
* - JRV-129: Multi-element selector warnings
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const { getElementSelector } = require('./lib/element-selector');
|
|
18
|
+
const { KEY_DEFINITIONS } = require('./lib/key-definitions');
|
|
19
|
+
const { generateHtmlDiff } = require('./lib/html-diff');
|
|
20
|
+
const { createState } = require('./lib/session-state');
|
|
21
|
+
const { attachCookies } = require('./lib/cookies');
|
|
22
|
+
const { attachViewport } = require('./lib/viewport');
|
|
23
|
+
const { attachEvaluation } = require('./lib/evaluation');
|
|
24
|
+
const { attachMouse } = require('./lib/mouse');
|
|
25
|
+
const { attachChromeProcess } = require('./lib/chrome-process');
|
|
26
|
+
const { attachCapture } = require('./lib/capture');
|
|
27
|
+
const { attachNavigation } = require('./lib/navigation');
|
|
28
|
+
const { attachKeyboardInput } = require('./lib/keyboard-input');
|
|
29
|
+
const { attachExtraction } = require('./lib/extraction');
|
|
30
|
+
const { attachScreenshot } = require('./lib/screenshot');
|
|
31
|
+
const { attachTabs, createPageSessionResolver } = require('./lib/tabs');
|
|
32
|
+
const { createBrowserSession } = require('./lib/browser-session');
|
|
33
|
+
const { attachBrowserBridge } = require('./lib/browser-bridge');
|
|
34
|
+
const { attachFileUpload } = require('./lib/file-upload');
|
|
35
|
+
const { attachConsoleLogging } = require('./lib/console-logging');
|
|
36
|
+
const { attachSelectOption } = require('./lib/select-option');
|
|
37
|
+
const { attachDialogs, DialogRefusedError } = require('./lib/dialogs');
|
|
38
|
+
const { renderSyntheticArtifacts } = require('./lib/dialogs-render');
|
|
39
|
+
const {
|
|
40
|
+
getXdgCacheHome,
|
|
41
|
+
getChromeProfileDir,
|
|
42
|
+
getProfileMetaPath,
|
|
43
|
+
readProfileMeta,
|
|
44
|
+
writeProfileMeta,
|
|
45
|
+
clearProfileMeta,
|
|
46
|
+
findAvailablePort,
|
|
47
|
+
buildChromeArgs,
|
|
48
|
+
} = require('./lib/chrome-launcher-helpers');
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Session methods whose CDP work targets the page (tab) target.
|
|
52
|
+
* When a native browser dialog is open, these methods will wedge waiting for a
|
|
53
|
+
* CDP response that never arrives because the dialog blocks the JS runtime.
|
|
54
|
+
* The session-boundary wrapper below refuses them with a descriptive error
|
|
55
|
+
* rather than hanging until timeout.
|
|
56
|
+
*
|
|
57
|
+
* Browser-target methods (getTabs, newTab, closeTab, startChrome, …) are NOT
|
|
58
|
+
* listed here — they route through the browser target and work fine while a
|
|
59
|
+
* dialog is open.
|
|
60
|
+
*/
|
|
61
|
+
const PAGE_TARGET_SESSION_METHODS = new Set([
|
|
62
|
+
'navigate',
|
|
63
|
+
'back',
|
|
64
|
+
'forward',
|
|
65
|
+
'click',
|
|
66
|
+
'fill',
|
|
67
|
+
'selectOption',
|
|
68
|
+
'evaluate',
|
|
69
|
+
'extractText',
|
|
70
|
+
'getHtml',
|
|
71
|
+
'getAttribute',
|
|
72
|
+
'waitForElement',
|
|
73
|
+
'waitForText',
|
|
74
|
+
'screenshot',
|
|
75
|
+
'hover',
|
|
76
|
+
'drag',
|
|
77
|
+
'mouseMove',
|
|
78
|
+
'scroll',
|
|
79
|
+
'doubleClick',
|
|
80
|
+
'rightClick',
|
|
81
|
+
'humanType',
|
|
82
|
+
'fileUpload',
|
|
83
|
+
'keyboardPress',
|
|
84
|
+
'clickWithCapture',
|
|
85
|
+
'fillWithCapture',
|
|
86
|
+
'selectOptionWithCapture',
|
|
87
|
+
'evaluateWithCapture',
|
|
88
|
+
// captureActionWithDiff is intentionally excluded: it is a meta-wrapper whose
|
|
89
|
+
// second arg is an action-type string ('type', 'click', …), not a selector.
|
|
90
|
+
// The inner actions it wraps (humanType, click, hover, etc.) are individually
|
|
91
|
+
// listed above and each have their own dialog gating via
|
|
92
|
+
// withDialogAwarenessForSession in capture.js. Re-gating the wrapper at this
|
|
93
|
+
// boundary would cause it to refuse dialog::* selectors before the inner action
|
|
94
|
+
// ever sees them (scenario 10C basic-auth typing bug).
|
|
95
|
+
'setViewport',
|
|
96
|
+
'clearViewport',
|
|
97
|
+
'getViewport',
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build a fresh Chrome session — a state-bag scoped to a single Chrome target.
|
|
102
|
+
*
|
|
103
|
+
* Pre-factory, every consumer that required this file shared module-level
|
|
104
|
+
* state: the connection pool, console-message buffers, the chosen profile
|
|
105
|
+
* name, the launched Chrome process handle, the active CDP port, and the
|
|
106
|
+
* host-override config. Two consumers in the same process therefore drove a
|
|
107
|
+
* single Chrome — fine for the CLI and the MCP server (each owns its
|
|
108
|
+
* process), but a hazard for any caller that wants to drive multiple Chromes
|
|
109
|
+
* concurrently from one Node process (different ports, different profiles).
|
|
110
|
+
*
|
|
111
|
+
* `createSession({ host, port })` returns a fresh instance with private state
|
|
112
|
+
* and methods bound to that state. Two instances do not share a connection
|
|
113
|
+
* pool, console-message map, profile, Chrome process, or host-override —
|
|
114
|
+
* mutating one (e.g. setProfileName, startChrome) has no effect on the other.
|
|
115
|
+
* Pass `host`/`port` to seed the host-override; omit them to seed from the
|
|
116
|
+
* `CHROME_WS_HOST` / `CHROME_WS_PORT` env vars exactly as before.
|
|
117
|
+
*
|
|
118
|
+
* The returned object preserves the legacy module-level export shape — the
|
|
119
|
+
* one-line consumer migration is `require(...)` becomes
|
|
120
|
+
* `require(...).createSession()`.
|
|
121
|
+
*/
|
|
122
|
+
function createSession({ host, port, _testFakes } = {}) {
|
|
123
|
+
const state = createState({ host, port });
|
|
124
|
+
|
|
125
|
+
// =============================================================================
|
|
126
|
+
const dialogs = attachDialogs({ state });
|
|
127
|
+
|
|
128
|
+
const { chromeHttp, resolveWsUrl, getTabs, newTab, closeTab } = attachTabs({ state });
|
|
129
|
+
|
|
130
|
+
// Bridge primitives — single root WebSocket with flatten-mode page sessions.
|
|
131
|
+
// The browser-session is constructed immediately (lazy connect on first use).
|
|
132
|
+
// attachBrowserBridge issues Target.setDiscoverTargets which connects the root
|
|
133
|
+
// WS, so we defer it behind state.ensureBridge() (lazy).
|
|
134
|
+
const effectiveChromeHttp = (_testFakes && _testFakes.chromeHttp) ? _testFakes.chromeHttp : chromeHttp;
|
|
135
|
+
const browserSessionFactory = () => createBrowserSession({
|
|
136
|
+
host: state.hostOverride.getHost(),
|
|
137
|
+
port: state.hostOverride.getPort(),
|
|
138
|
+
rewriteWsUrl: state.rewriteWsUrl,
|
|
139
|
+
chromeHttp: effectiveChromeHttp,
|
|
140
|
+
WebSocketClient: _testFakes && _testFakes.WebSocketClient,
|
|
141
|
+
});
|
|
142
|
+
state.browserSession = browserSessionFactory();
|
|
143
|
+
|
|
144
|
+
let bridgePromise = null;
|
|
145
|
+
|
|
146
|
+
// Reset all bridge-layer state so the next ensureBridge() call re-attaches from
|
|
147
|
+
// scratch. Called by killChrome (explicit kill) and ensureBridge (stale detection).
|
|
148
|
+
// Does NOT call detach on cached pageSessions — the underlying WebSocket is
|
|
149
|
+
// already dead at call time, so detach would fail. Use resolver.release() per-tab
|
|
150
|
+
// before calling resetBridge if graceful cleanup is possible.
|
|
151
|
+
state.resetBridge = () => {
|
|
152
|
+
if (state.pageSessionResolver) {
|
|
153
|
+
state.pageSessionResolver.releaseAll();
|
|
154
|
+
}
|
|
155
|
+
state.pageSessionResolver = null;
|
|
156
|
+
state.browserBridge = null;
|
|
157
|
+
state.browserSession = browserSessionFactory();
|
|
158
|
+
bridgePromise = null;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
state.ensureBridge = () => {
|
|
162
|
+
// Detect stale bridge: if the cached browserSession is no longer connected,
|
|
163
|
+
// reset everything so we re-attach to the restarted Chrome process.
|
|
164
|
+
if (state.browserBridge && state.browserSession && !state.browserSession.isConnected()) {
|
|
165
|
+
state.resetBridge();
|
|
166
|
+
}
|
|
167
|
+
if (state.browserBridge) return Promise.resolve(state.browserBridge);
|
|
168
|
+
if (bridgePromise) return bridgePromise;
|
|
169
|
+
bridgePromise = (async () => {
|
|
170
|
+
const bridge = await attachBrowserBridge({
|
|
171
|
+
browser: state.browserSession,
|
|
172
|
+
host: state.hostOverride.getHost(),
|
|
173
|
+
port: state.hostOverride.getPort(),
|
|
174
|
+
rewriteWsUrl: state.rewriteWsUrl,
|
|
175
|
+
autoAttach: true,
|
|
176
|
+
onPageSession: async (ps) => {
|
|
177
|
+
// Install dialog shim before the paused target resumes.
|
|
178
|
+
// This gives popups, OAuth windows, and child frames dialog
|
|
179
|
+
// handling from their very first script.
|
|
180
|
+
try {
|
|
181
|
+
await dialogs.attachToPageSession(ps);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error('onPageSession dialog attach failed:', e);
|
|
184
|
+
}
|
|
185
|
+
// Prime the pageSession resolver cache so subsequent getPageSession(popup)
|
|
186
|
+
// calls return THIS session rather than issuing a duplicate Target.attachToTarget.
|
|
187
|
+
// The dialog is registered under THIS session's sessionId; agent commands
|
|
188
|
+
// must route through the same session to handle the dialog.
|
|
189
|
+
if (state.pageSessionResolver && ps.targetId) {
|
|
190
|
+
state.pageSessionResolver.prime(ps.targetId, ps);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
state.browserBridge = bridge;
|
|
195
|
+
state.pageSessionResolver = createPageSessionResolver({ bridge });
|
|
196
|
+
return bridge;
|
|
197
|
+
})();
|
|
198
|
+
// Clear bridgePromise on failure so the next call retries
|
|
199
|
+
bridgePromise.catch(() => { bridgePromise = null; });
|
|
200
|
+
return bridgePromise;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// getPageSession(tabIndexOrWsUrl) — shared resolver for pageSession-migrated libs.
|
|
204
|
+
// Accepts either a numeric tab index or a ws:// URL, lazy-boots the bridge, and
|
|
205
|
+
// returns a cached pageSession for the target. Reused by E2-E13 migration libs.
|
|
206
|
+
async function getPageSession(tabIndexOrWsUrl) {
|
|
207
|
+
await state.ensureBridge();
|
|
208
|
+
let tab;
|
|
209
|
+
if (typeof tabIndexOrWsUrl === 'number') {
|
|
210
|
+
const tabs = await getTabs();
|
|
211
|
+
tab = tabs[tabIndexOrWsUrl];
|
|
212
|
+
if (!tab) throw new Error(`No tab at index ${tabIndexOrWsUrl}`);
|
|
213
|
+
} else if (typeof tabIndexOrWsUrl === 'string') {
|
|
214
|
+
// Extract targetId from a ws URL like ws://host:port/devtools/page/<targetId>
|
|
215
|
+
const m = /\/devtools\/page\/([^/]+)$/.exec(tabIndexOrWsUrl);
|
|
216
|
+
if (!m) throw new Error(`Cannot extract targetId from: ${tabIndexOrWsUrl}`);
|
|
217
|
+
tab = { id: m[1] };
|
|
218
|
+
} else if (tabIndexOrWsUrl && tabIndexOrWsUrl.id) {
|
|
219
|
+
// Already a tab handle
|
|
220
|
+
tab = tabIndexOrWsUrl;
|
|
221
|
+
} else {
|
|
222
|
+
throw new Error('Unrecognized tabIndexOrWsUrl');
|
|
223
|
+
}
|
|
224
|
+
const ps = await state.pageSessionResolver(tab);
|
|
225
|
+
// Ensure dialog event listeners are wired up on the bridge session the first
|
|
226
|
+
// time a page session is obtained. This enables Page.javascriptDialogOpening
|
|
227
|
+
// events to arrive via the bridge path (stored under sessionId).
|
|
228
|
+
await dialogs.attachToPageSession(ps);
|
|
229
|
+
return ps;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const { click, hover, drag, mouseMove, scroll, doubleClick, rightClick } =
|
|
233
|
+
attachMouse({ getPageSession, dialogs });
|
|
234
|
+
|
|
235
|
+
const { keyboardPress, fill, humanType } =
|
|
236
|
+
attachKeyboardInput({ state, getPageSession, click, dialogs });
|
|
237
|
+
|
|
238
|
+
const { fileUpload } = attachFileUpload({ getPageSession });
|
|
239
|
+
|
|
240
|
+
const { selectOption } = attachSelectOption({ getPageSession });
|
|
241
|
+
|
|
242
|
+
const { evaluate } = attachEvaluation({ getPageSession });
|
|
243
|
+
|
|
244
|
+
// =============================================================================
|
|
245
|
+
|
|
246
|
+
const { extractText, getHtml, getAttribute } = attachExtraction({ getPageSession });
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
// getSessionDir is a lazy thunk: capture.js populates state.sessionDir via
|
|
250
|
+
// initializeSession(). We close over `state` so screenshot.js always reads
|
|
251
|
+
// the freshly-set value. If no capture has happened yet, we delegate to
|
|
252
|
+
// captureInitializer (set below after attachCapture) to create the dir.
|
|
253
|
+
// The ref itself must live before attachScreenshot and attachCapture, but the
|
|
254
|
+
// actual initializeSession function is injected after attachCapture runs.
|
|
255
|
+
const screenshotDirRef = { initializeSession: null };
|
|
256
|
+
|
|
257
|
+
const { screenshot } = attachScreenshot({
|
|
258
|
+
getPageSession,
|
|
259
|
+
state,
|
|
260
|
+
initializeSession: () => {
|
|
261
|
+
if (screenshotDirRef.initializeSession) return screenshotDirRef.initializeSession();
|
|
262
|
+
// Fallback if called before attachCapture (shouldn't happen in normal flow).
|
|
263
|
+
if (state.sessionDir) return state.sessionDir;
|
|
264
|
+
throw new Error('Session directory not yet initialized. Call an auto-capture action first.');
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const { startChrome, killChrome, showBrowser, hideBrowser, getBrowserMode, getChromePid, getActivePort, getProfileName, setProfileName } =
|
|
269
|
+
attachChromeProcess({ state, chromeHttp, getTabs, newTab });
|
|
270
|
+
|
|
271
|
+
const { enableConsoleLogging, getConsoleMessages, clearConsoleMessages } =
|
|
272
|
+
attachConsoleLogging({ state, getPageSession });
|
|
273
|
+
|
|
274
|
+
const {
|
|
275
|
+
initializeSession,
|
|
276
|
+
cleanupSession,
|
|
277
|
+
createCapturePrefix,
|
|
278
|
+
generateDomSummary,
|
|
279
|
+
getPageSize,
|
|
280
|
+
generateMarkdown,
|
|
281
|
+
capturePageArtifacts,
|
|
282
|
+
captureActionWithDiff,
|
|
283
|
+
clickWithCapture,
|
|
284
|
+
fillWithCapture,
|
|
285
|
+
selectOptionWithCapture,
|
|
286
|
+
evaluateWithCapture,
|
|
287
|
+
} = attachCapture({
|
|
288
|
+
state,
|
|
289
|
+
getPageSession,
|
|
290
|
+
getHtml,
|
|
291
|
+
screenshot,
|
|
292
|
+
actions: { click, fill, selectOption, evaluate },
|
|
293
|
+
dialogs,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Wire the forward reference so screenshot.js can call initializeSession.
|
|
297
|
+
screenshotDirRef.initializeSession = initializeSession;
|
|
298
|
+
|
|
299
|
+
const { navigate, waitForElement, waitForText, back, forward } =
|
|
300
|
+
attachNavigation({ state, getPageSession, capturePageArtifacts, evaluate });
|
|
301
|
+
|
|
302
|
+
const { setViewport, clearViewport, getViewport } = attachViewport({ getPageSession });
|
|
303
|
+
const { clearCookies } = attachCookies({ getPageSession });
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Session-boundary dialog gate
|
|
307
|
+
//
|
|
308
|
+
// Wraps every page-target method so that any call issued while a native dialog
|
|
309
|
+
// is open returns a structured refusal instead of hanging until a CDP timeout.
|
|
310
|
+
//
|
|
311
|
+
// Convention (mirrors all other page-target methods in this library):
|
|
312
|
+
// fn(tabIndexOrWsUrl, selectorOrArg, ...rest)
|
|
313
|
+
//
|
|
314
|
+
// If the second argument is a string beginning with "dialog::", it is a
|
|
315
|
+
// dialog-selector call (e.g. click("dialog::accept")) and must be allowed
|
|
316
|
+
// through so the existing internal routers in mouse.js and keyboard-input.js
|
|
317
|
+
// can handle it.
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
function wrapWithDialogGate(_name, fn) {
|
|
320
|
+
return async function dialogGated(tabIndexOrWsUrl, secondArg, ...rest) {
|
|
321
|
+
// Resolve the ws URL so we can look up dialog state.
|
|
322
|
+
// resolveWsUrl may throw (e.g., no Chrome running) — let it propagate
|
|
323
|
+
// naturally; that's not a dialog problem.
|
|
324
|
+
let wsUrl;
|
|
325
|
+
try {
|
|
326
|
+
wsUrl = await resolveWsUrl(tabIndexOrWsUrl);
|
|
327
|
+
} catch {
|
|
328
|
+
// Can't resolve the URL — delegate and let the method surface the error.
|
|
329
|
+
return fn(tabIndexOrWsUrl, secondArg, ...rest);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Look up dialog state keyed by sessionId (via targetId→sessionId map populated
|
|
333
|
+
// by attachToPageSession). dialogs.getOpen() handles both direct sessionId keys
|
|
334
|
+
// and wsUrl paths by extracting the targetId from the URL.
|
|
335
|
+
const open = dialogs.getOpen(wsUrl);
|
|
336
|
+
|
|
337
|
+
const isDialogSelector = typeof secondArg === 'string' && secondArg.startsWith('dialog::');
|
|
338
|
+
|
|
339
|
+
if (open && !isDialogSelector) {
|
|
340
|
+
throw new DialogRefusedError({ dialog: open, artifacts: renderSyntheticArtifacts(open) });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return fn(tabIndexOrWsUrl, secondArg, ...rest);
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Build the raw session object, then wrap page-target methods.
|
|
348
|
+
const rawSession = {
|
|
349
|
+
// State bag (exposed for bridge consumers and testing)
|
|
350
|
+
state,
|
|
351
|
+
|
|
352
|
+
// Internal helpers (exported for testing)
|
|
353
|
+
getElementSelector,
|
|
354
|
+
|
|
355
|
+
// Core browser actions (click/fill now use CDP events by default for React compatibility)
|
|
356
|
+
getTabs,
|
|
357
|
+
newTab,
|
|
358
|
+
closeTab,
|
|
359
|
+
navigate,
|
|
360
|
+
click, // Uses CDP mouse events, falls back to el.click()
|
|
361
|
+
fill, // Uses CDP insertText, falls back to el.value=
|
|
362
|
+
selectOption, // Warns if selector matches multiple elements
|
|
363
|
+
evaluate,
|
|
364
|
+
extractText,
|
|
365
|
+
getHtml,
|
|
366
|
+
getAttribute,
|
|
367
|
+
waitForElement,
|
|
368
|
+
waitForText,
|
|
369
|
+
back,
|
|
370
|
+
forward,
|
|
371
|
+
screenshot,
|
|
372
|
+
|
|
373
|
+
// Mouse actions (CDP-level, bypasses synthetic event restrictions)
|
|
374
|
+
hover, // Move mouse over element (CSS :hover, tooltips)
|
|
375
|
+
drag, // Drag-and-drop via native mouse event sequence
|
|
376
|
+
mouseMove, // Raw coordinate mouse movement
|
|
377
|
+
scroll, // Mouse wheel scrolling
|
|
378
|
+
doubleClick, // Double-click with dblclick event
|
|
379
|
+
rightClick, // Right-click with contextmenu event
|
|
380
|
+
|
|
381
|
+
// Human-like typing (individual keyDown/keyUp with realistic timing)
|
|
382
|
+
humanType,
|
|
383
|
+
|
|
384
|
+
// File upload (DOM.setFileInputFiles — can't be done via JS)
|
|
385
|
+
fileUpload,
|
|
386
|
+
|
|
387
|
+
// Keyboard support for special keys (Tab, Enter, Escape, Arrow keys, etc.)
|
|
388
|
+
keyboardPress,
|
|
389
|
+
KEY_DEFINITIONS,
|
|
390
|
+
|
|
391
|
+
// Chrome lifecycle
|
|
392
|
+
startChrome,
|
|
393
|
+
buildChromeArgs,
|
|
394
|
+
killChrome,
|
|
395
|
+
showBrowser,
|
|
396
|
+
hideBrowser,
|
|
397
|
+
getBrowserMode,
|
|
398
|
+
getChromePid,
|
|
399
|
+
|
|
400
|
+
// Profile management
|
|
401
|
+
getChromeProfileDir,
|
|
402
|
+
getProfileName,
|
|
403
|
+
setProfileName,
|
|
404
|
+
|
|
405
|
+
// Console logging
|
|
406
|
+
enableConsoleLogging,
|
|
407
|
+
getConsoleMessages,
|
|
408
|
+
clearConsoleMessages,
|
|
409
|
+
|
|
410
|
+
// Session management
|
|
411
|
+
getXdgCacheHome,
|
|
412
|
+
initializeSession,
|
|
413
|
+
cleanupSession,
|
|
414
|
+
createCapturePrefix,
|
|
415
|
+
|
|
416
|
+
// Auto-capture utilities
|
|
417
|
+
generateDomSummary,
|
|
418
|
+
getPageSize,
|
|
419
|
+
generateMarkdown,
|
|
420
|
+
capturePageArtifacts,
|
|
421
|
+
clickWithCapture,
|
|
422
|
+
fillWithCapture,
|
|
423
|
+
selectOptionWithCapture,
|
|
424
|
+
evaluateWithCapture,
|
|
425
|
+
|
|
426
|
+
// DOM diff capture (before/after with diff)
|
|
427
|
+
generateHtmlDiff,
|
|
428
|
+
captureActionWithDiff,
|
|
429
|
+
|
|
430
|
+
// Dynamic port allocation and per-profile meta.json
|
|
431
|
+
getActivePort,
|
|
432
|
+
findAvailablePort,
|
|
433
|
+
getProfileMetaPath,
|
|
434
|
+
readProfileMeta,
|
|
435
|
+
writeProfileMeta,
|
|
436
|
+
clearProfileMeta,
|
|
437
|
+
|
|
438
|
+
// Viewport/device emulation
|
|
439
|
+
setViewport,
|
|
440
|
+
clearViewport,
|
|
441
|
+
getViewport,
|
|
442
|
+
|
|
443
|
+
// Cookie management
|
|
444
|
+
clearCookies,
|
|
445
|
+
|
|
446
|
+
// Dialog awareness
|
|
447
|
+
dialogs,
|
|
448
|
+
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// Apply the session-boundary dialog gate to every page-target method.
|
|
452
|
+
for (const name of PAGE_TARGET_SESSION_METHODS) {
|
|
453
|
+
if (typeof rawSession[name] === 'function') {
|
|
454
|
+
rawSession[name] = wrapWithDialogGate(name, rawSession[name]);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return rawSession;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
module.exports = { createSession, PAGE_TARGET_SESSION_METHODS, DialogRefusedError };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const DEFAULT_PORT = 9222;
|
|
2
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a per-instance host-override configuration.
|
|
6
|
+
*
|
|
7
|
+
* The legacy module-level constants and `rewriteWsUrl` above are baked at
|
|
8
|
+
* module-load time and shared across every consumer that requires this
|
|
9
|
+
* file. That's fine for the single-Chrome use case — the CLI and the MCP
|
|
10
|
+
* server each own their process, so module-level state is effectively
|
|
11
|
+
* per-process. It breaks down when one process needs to drive several
|
|
12
|
+
* independent Chrome instances concurrently (different host/port pairs):
|
|
13
|
+
* the load-time constants can only describe one of them.
|
|
14
|
+
*
|
|
15
|
+
* `createOverride({ host, port })` returns a fresh state-bag with its own
|
|
16
|
+
* host/port/override-enabled flag, plus methods (`getHost`, `getPort`,
|
|
17
|
+
* `getBase`, `isOverrideEnabled`, `rewriteWsUrl`, `setDefaults`) bound to
|
|
18
|
+
* that state. Two instances do not share state — mutating one via
|
|
19
|
+
* `setDefaults()` does not affect the other. Callers that don't need
|
|
20
|
+
* per-instance isolation can keep using the module-level constants and
|
|
21
|
+
* `rewriteWsUrl` exactly as before; nothing about the legacy API has
|
|
22
|
+
* changed.
|
|
23
|
+
*
|
|
24
|
+
* Defaults: if both `host` and `port` are omitted, the instance seeds from
|
|
25
|
+
* the `CHROME_WS_HOST` / `CHROME_WS_PORT` env vars. If either argument is
|
|
26
|
+
* supplied, both are taken from the arguments (filling in defaults for the
|
|
27
|
+
* missing one) and the instance's `overrideEnabled` flag starts true —
|
|
28
|
+
* matching `setDefaults()` semantics.
|
|
29
|
+
*/
|
|
30
|
+
function createOverride({ host, port } = {}) {
|
|
31
|
+
let instanceHost;
|
|
32
|
+
let instancePort;
|
|
33
|
+
let instanceOverrideEnabled;
|
|
34
|
+
|
|
35
|
+
if (host !== undefined || port !== undefined) {
|
|
36
|
+
instanceHost = host !== undefined ? host : DEFAULT_HOST;
|
|
37
|
+
instancePort = port !== undefined ? port : DEFAULT_PORT;
|
|
38
|
+
instanceOverrideEnabled = true;
|
|
39
|
+
} else {
|
|
40
|
+
instanceHost = process.env.CHROME_WS_HOST || DEFAULT_HOST;
|
|
41
|
+
const parsed = parseInt(process.env.CHROME_WS_PORT || `${DEFAULT_PORT}`, 10);
|
|
42
|
+
instancePort = Number.isNaN(parsed) ? DEFAULT_PORT : parsed;
|
|
43
|
+
instanceOverrideEnabled =
|
|
44
|
+
process.env.CHROME_WS_HOST !== undefined || process.env.CHROME_WS_PORT !== undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setDefaults(nextHost, nextPort) {
|
|
48
|
+
instanceHost = nextHost;
|
|
49
|
+
instancePort = nextPort;
|
|
50
|
+
instanceOverrideEnabled = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getHost() {
|
|
54
|
+
return instanceHost;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getPort() {
|
|
58
|
+
return instancePort;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getBase() {
|
|
62
|
+
return `http://${instanceHost}:${instancePort}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isOverrideEnabled() {
|
|
66
|
+
return instanceOverrideEnabled;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function instanceRewriteWsUrl(originalUrl, overrideHost, overridePort) {
|
|
70
|
+
if (!originalUrl || typeof originalUrl !== 'string') {
|
|
71
|
+
return originalUrl;
|
|
72
|
+
}
|
|
73
|
+
if (!instanceOverrideEnabled) {
|
|
74
|
+
return originalUrl;
|
|
75
|
+
}
|
|
76
|
+
const useHost = overrideHost !== undefined ? overrideHost : instanceHost;
|
|
77
|
+
const usePort = overridePort !== undefined ? overridePort : instancePort;
|
|
78
|
+
try {
|
|
79
|
+
const url = new URL(originalUrl);
|
|
80
|
+
url.hostname = useHost;
|
|
81
|
+
url.port = `${usePort}`;
|
|
82
|
+
return url.toString();
|
|
83
|
+
} catch {
|
|
84
|
+
return originalUrl;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
setDefaults,
|
|
90
|
+
getHost,
|
|
91
|
+
getPort,
|
|
92
|
+
getBase,
|
|
93
|
+
isOverrideEnabled,
|
|
94
|
+
rewriteWsUrl: instanceRewriteWsUrl,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { createOverride };
|