@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,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createCdpRouter } = require('./cdp-router');
|
|
4
|
+
const { attachPageSession, buildPageSessionFromAttached } = require('./page-session');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* attachBrowserBridge({browser, host, port, rewriteWsUrl}) — consumer-facing
|
|
8
|
+
* bridge over the browser-session.
|
|
9
|
+
*
|
|
10
|
+
* Exposes:
|
|
11
|
+
* targets.list() — synchronous snapshot of current targets
|
|
12
|
+
* targets.onCreated(handler) — register listener; returns unsub fn
|
|
13
|
+
* targets.onDestroyed(handler)
|
|
14
|
+
* targets.waitForNew(predicate, {timeoutMs})
|
|
15
|
+
* createBrowserContext({proxyServer?}) -> {browserContextId, createPage, dispose}
|
|
16
|
+
* attachPageSession(targetId) — page session over the browser-WS via flatten mode
|
|
17
|
+
*
|
|
18
|
+
* host/port/rewriteWsUrl are needed by createBrowserContext.createPage to construct
|
|
19
|
+
* per-page WS URLs for callers that still want one (the bridge itself never uses
|
|
20
|
+
* them — page sessions ride the browser-WS).
|
|
21
|
+
*/
|
|
22
|
+
async function attachBrowserBridge({ browser, host, port, rewriteWsUrl, autoAttach = false, onPageSession = null }) {
|
|
23
|
+
// The cdp-router sits between browser-session and bridge consumers.
|
|
24
|
+
// Page-session-tagged messages dispatch to the right session; root-session
|
|
25
|
+
// events (Target.*, etc.) fire root listeners. Command responses without
|
|
26
|
+
// sessionId stay correlated by browser-session.js's pendingRequests
|
|
27
|
+
// (single source of truth for root-session correlation).
|
|
28
|
+
const router = createCdpRouter({ browser });
|
|
29
|
+
|
|
30
|
+
const targetMap = new Map(); // targetId -> targetInfo
|
|
31
|
+
const onCreatedFns = new Set();
|
|
32
|
+
const onDestroyedFns = new Set();
|
|
33
|
+
|
|
34
|
+
router.getRootListeners().add((msg) => {
|
|
35
|
+
if (msg.method === 'Target.targetCreated') {
|
|
36
|
+
const t = msg.params.targetInfo;
|
|
37
|
+
targetMap.set(t.targetId, t);
|
|
38
|
+
for (const fn of onCreatedFns) {
|
|
39
|
+
try { fn(t); } catch (e) { console.error('targets onCreated handler threw:', e); }
|
|
40
|
+
}
|
|
41
|
+
} else if (msg.method === 'Target.targetInfoChanged') {
|
|
42
|
+
const t = msg.params.targetInfo;
|
|
43
|
+
targetMap.set(t.targetId, t);
|
|
44
|
+
} else if (msg.method === 'Target.targetDestroyed') {
|
|
45
|
+
const t = targetMap.get(msg.params.targetId);
|
|
46
|
+
targetMap.delete(msg.params.targetId);
|
|
47
|
+
if (t) {
|
|
48
|
+
for (const fn of onDestroyedFns) {
|
|
49
|
+
try { fn(t); } catch (e) { console.error('targets onDestroyed handler threw:', e); }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Handle auto-attached targets (popups, child frames, etc.) when autoAttach is on.
|
|
56
|
+
// Chrome emits this with an already-allocated sessionId — no Target.attachToTarget needed.
|
|
57
|
+
//
|
|
58
|
+
// Only act on targets that are paused (waitingForDebugger: true). Existing tabs
|
|
59
|
+
// that Chrome retrospectively reports via attachedToTarget (waitingForDebugger: false)
|
|
60
|
+
// are not paused and will be set up through the normal getPageSession/attachPageSession
|
|
61
|
+
// path. Installing a page session here for those targets would register the same
|
|
62
|
+
// sessionId twice in the cdp-router, causing duplicate-id protocol errors.
|
|
63
|
+
router.getRootListeners().add(async (msg) => {
|
|
64
|
+
if (msg.method !== 'Target.attachedToTarget') return;
|
|
65
|
+
const { sessionId, targetInfo, waitingForDebugger } = msg.params;
|
|
66
|
+
|
|
67
|
+
if (!waitingForDebugger) return;
|
|
68
|
+
|
|
69
|
+
const ps = buildPageSessionFromAttached({ browser, router, sessionId, targetId: targetInfo.targetId });
|
|
70
|
+
|
|
71
|
+
// Only run the onPageSession hook for page-type targets. Non-page targets
|
|
72
|
+
// (service_worker, background_page, etc.) don't support the Page CDP domain
|
|
73
|
+
// and would cause 'Page.enable' wasn't found errors in the hook.
|
|
74
|
+
if (onPageSession && targetInfo.type === 'page') {
|
|
75
|
+
try { await onPageSession(ps); }
|
|
76
|
+
catch (e) { console.error('onPageSession hook threw:', e); }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Resume the paused target AFTER the hook so any shims (e.g. dialog) are
|
|
80
|
+
// installed before the page's scripts run.
|
|
81
|
+
try { await ps.send('Runtime.runIfWaitingForDebugger', {}); }
|
|
82
|
+
catch (e) { console.error('Runtime.runIfWaitingForDebugger failed:', e); }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Subscribe — replays existing targets as targetCreated events.
|
|
86
|
+
await browser.send('Target.setDiscoverTargets', { discover: true });
|
|
87
|
+
|
|
88
|
+
if (autoAttach) {
|
|
89
|
+
await browser.send('Target.setAutoAttach', {
|
|
90
|
+
autoAttach: true,
|
|
91
|
+
waitForDebuggerOnStart: true,
|
|
92
|
+
flatten: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function list() { return Array.from(targetMap.values()); }
|
|
97
|
+
function onCreated(fn) { onCreatedFns.add(fn); return () => onCreatedFns.delete(fn); }
|
|
98
|
+
function onDestroyed(fn) { onDestroyedFns.add(fn); return () => onDestroyedFns.delete(fn); }
|
|
99
|
+
|
|
100
|
+
function waitForNew(predicate, { timeoutMs = 15000 } = {}) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
let unsub = null;
|
|
103
|
+
const timeout = setTimeout(() => {
|
|
104
|
+
if (unsub) unsub();
|
|
105
|
+
reject(new Error(`waitForNew: timed out after ${timeoutMs}ms`));
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
unsub = onCreated((t) => {
|
|
108
|
+
let match;
|
|
109
|
+
try { match = predicate(t); }
|
|
110
|
+
catch (e) {
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
if (unsub) unsub();
|
|
113
|
+
reject(e);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (match) {
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
if (unsub) unsub();
|
|
119
|
+
resolve(t);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* createBrowserContext({proxyServer?}) — creates a Chrome BrowserContext.
|
|
127
|
+
* Returns {browserContextId, createPage, dispose}.
|
|
128
|
+
*
|
|
129
|
+
* createPage(url) calls Target.createTarget({url, browserContextId}) and
|
|
130
|
+
* constructs a tab-shape-compatible page handle whose webSocketDebuggerUrl
|
|
131
|
+
* is run through rewriteWsUrl.
|
|
132
|
+
*
|
|
133
|
+
* dispose() is atomic — Chrome tears down cookies/storage/IDB/SW for the
|
|
134
|
+
* context in one call.
|
|
135
|
+
*/
|
|
136
|
+
async function createBrowserContext(opts = {}) {
|
|
137
|
+
const params = {};
|
|
138
|
+
if (opts.proxyServer) params.proxyServer = opts.proxyServer;
|
|
139
|
+
const { browserContextId } = await browser.send('Target.createBrowserContext', params);
|
|
140
|
+
let disposed = false;
|
|
141
|
+
|
|
142
|
+
async function createPage(url = 'about:blank') {
|
|
143
|
+
if (disposed) throw new Error('BrowserContext disposed');
|
|
144
|
+
const { targetId } = await browser.send('Target.createTarget', { url, browserContextId });
|
|
145
|
+
const rawWsUrl = `ws://${host}:${port}/devtools/page/${targetId}`;
|
|
146
|
+
return {
|
|
147
|
+
id: targetId, targetId,
|
|
148
|
+
webSocketDebuggerUrl: rewriteWsUrl(rawWsUrl, host, port),
|
|
149
|
+
type: 'page', url, browserContextId,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function dispose() {
|
|
154
|
+
if (disposed) return;
|
|
155
|
+
disposed = true;
|
|
156
|
+
try { await browser.send('Target.disposeBrowserContext', { browserContextId }); }
|
|
157
|
+
catch (e) { console.warn('BrowserContext.dispose() failed:', e && e.message); }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { browserContextId, createPage, dispose };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function attachPage(targetId) {
|
|
164
|
+
return attachPageSession({ browser, router }, targetId);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
targets: { list, onCreated, onDestroyed, waitForNew },
|
|
169
|
+
createBrowserContext,
|
|
170
|
+
attachPageSession: attachPage,
|
|
171
|
+
router,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { attachBrowserBridge };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { WebSocketClient: DefaultWebSocketClient } = require('./websocket-client');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* createBrowserSession({host, port, rewriteWsUrl, chromeHttp, WebSocketClient?}) -> bridge handle.
|
|
7
|
+
*
|
|
8
|
+
* Owns the one root WebSocket to /devtools/browser/<id>. Page-action commands ride
|
|
9
|
+
* per-page sessions (attached via Target.attachToTarget({flatten:true})) and envelope
|
|
10
|
+
* messages with a sessionId via sendRaw — the page session manages its own pendingRequests
|
|
11
|
+
* (in the cdp-router). browser-session correlates ROOT-session command responses only.
|
|
12
|
+
*
|
|
13
|
+
* Returned API:
|
|
14
|
+
* send(method, params?, {timeoutMs?}) -> Promise<result> // root command
|
|
15
|
+
* onEvent(handler) -> unsub fn
|
|
16
|
+
* close() -> Promise<void>
|
|
17
|
+
* isConnected() -> boolean
|
|
18
|
+
* sendRaw(json) -> void
|
|
19
|
+
*/
|
|
20
|
+
function createBrowserSession({ host, port, rewriteWsUrl, chromeHttp, WebSocketClient = DefaultWebSocketClient }) {
|
|
21
|
+
let ws = null;
|
|
22
|
+
const pendingRequests = new Map(); // id -> {resolve, reject, timeout}
|
|
23
|
+
let messageIdCounter = 1;
|
|
24
|
+
const eventListeners = new Set();
|
|
25
|
+
let connectPromise = null;
|
|
26
|
+
let closed = false;
|
|
27
|
+
|
|
28
|
+
async function ensureConnected() {
|
|
29
|
+
if (ws && ws.isConnected()) return;
|
|
30
|
+
if (connectPromise) { await connectPromise; return; }
|
|
31
|
+
connectPromise = (async () => {
|
|
32
|
+
try {
|
|
33
|
+
const versionInfo = await chromeHttp('/json/version');
|
|
34
|
+
if (!versionInfo || !versionInfo.webSocketDebuggerUrl) {
|
|
35
|
+
throw new Error('chromeHttp(/json/version) returned no webSocketDebuggerUrl');
|
|
36
|
+
}
|
|
37
|
+
const url = rewriteWsUrl(versionInfo.webSocketDebuggerUrl, host, port);
|
|
38
|
+
const next = new WebSocketClient(url);
|
|
39
|
+
next.on('message', (raw) => {
|
|
40
|
+
let data;
|
|
41
|
+
try { data = JSON.parse(raw); } catch (e) {
|
|
42
|
+
console.error('browser-session: bad JSON from CDP:', e);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Correlate ROOT-session command responses (id without sessionId). Page-session
|
|
46
|
+
// responses carry {id, result, sessionId} and fall through to event listeners
|
|
47
|
+
// for the cdp-router to dispatch.
|
|
48
|
+
if (data.id !== undefined && data.sessionId === undefined) {
|
|
49
|
+
const pending = pendingRequests.get(data.id);
|
|
50
|
+
if (pending) {
|
|
51
|
+
clearTimeout(pending.timeout);
|
|
52
|
+
pendingRequests.delete(data.id);
|
|
53
|
+
if (data.error) {
|
|
54
|
+
pending.reject(new Error(data.error.message || JSON.stringify(data.error)));
|
|
55
|
+
} else {
|
|
56
|
+
pending.resolve(data.result);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const fn of eventListeners) {
|
|
62
|
+
try { fn(data); } catch (e) { console.error('browser-session listener threw:', e); }
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
next.on('close', () => {
|
|
66
|
+
for (const [, p] of pendingRequests) {
|
|
67
|
+
clearTimeout(p.timeout);
|
|
68
|
+
p.reject(new Error('Browser session WS closed'));
|
|
69
|
+
}
|
|
70
|
+
pendingRequests.clear();
|
|
71
|
+
});
|
|
72
|
+
await next.connect();
|
|
73
|
+
// Assign ws only after a successful connect so concurrent callers that hit the
|
|
74
|
+
// `ws && ws.isConnected()` early-return don't see a half-initialized socket.
|
|
75
|
+
ws = next;
|
|
76
|
+
} catch (e) {
|
|
77
|
+
// Allow retry after a transient failure (network blip, Chrome not yet ready, etc.).
|
|
78
|
+
// We do NOT null on success — leaving the resolved promise in place makes subsequent
|
|
79
|
+
// ensureConnected() awaits a no-op.
|
|
80
|
+
connectPromise = null;
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
await connectPromise;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function send(method, params = {}, { timeoutMs = 10000 } = {}) {
|
|
88
|
+
if (closed) throw new Error('Browser session closed');
|
|
89
|
+
await ensureConnected();
|
|
90
|
+
if (closed) throw new Error('Browser session closed');
|
|
91
|
+
const id = messageIdCounter++;
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const timeout = setTimeout(() => {
|
|
94
|
+
pendingRequests.delete(id);
|
|
95
|
+
reject(new Error(`Browser session timeout: ${method}`));
|
|
96
|
+
}, timeoutMs);
|
|
97
|
+
pendingRequests.set(id, { resolve, reject, timeout });
|
|
98
|
+
try {
|
|
99
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
100
|
+
} catch (e) {
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
pendingRequests.delete(id);
|
|
103
|
+
reject(e);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function onEvent(handler) {
|
|
109
|
+
eventListeners.add(handler);
|
|
110
|
+
return () => eventListeners.delete(handler);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function close() {
|
|
114
|
+
closed = true;
|
|
115
|
+
if (ws) { ws.close(); ws = null; }
|
|
116
|
+
for (const [, p] of pendingRequests) {
|
|
117
|
+
clearTimeout(p.timeout);
|
|
118
|
+
p.reject(new Error('Browser session closed'));
|
|
119
|
+
}
|
|
120
|
+
pendingRequests.clear();
|
|
121
|
+
eventListeners.clear();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isConnected() { return ws !== null && ws.isConnected(); }
|
|
125
|
+
|
|
126
|
+
function sendRaw(json) {
|
|
127
|
+
if (closed) throw new Error('Browser session closed');
|
|
128
|
+
if (!ws || !ws.isConnected()) {
|
|
129
|
+
throw new Error('Browser WS not connected (call send() first to lazy-open)');
|
|
130
|
+
}
|
|
131
|
+
ws.send(json);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { send, onEvent, close, isConnected, sendRaw };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { createBrowserSession };
|