@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,288 @@
|
|
|
1
|
+
const { KEY_DEFINITIONS, charToKeyDef } = require('./key-definitions');
|
|
2
|
+
const { getElementSelector } = require('./element-selector');
|
|
3
|
+
const { throwIfExceptionDetails } = require('./cdp-utils');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Keyboard and text-input actions: keyboardPress (named keys + modifiers),
|
|
7
|
+
* fill (smart text input with \t/\n handling), and humanType (realistic
|
|
8
|
+
* per-keystroke timing for bot-detection-resistant input).
|
|
9
|
+
*
|
|
10
|
+
* The headless/headed split inside humanType is load-bearing: in headed
|
|
11
|
+
* mode we send full keyDown/keyUp events so JS keyboard event handlers
|
|
12
|
+
* fire (and bot-detection sees them), but in headless mode rawKeyDown
|
|
13
|
+
* triggers Chrome browser shortcuts that navigate away from the page —
|
|
14
|
+
* so headless skips key events and relies on `Input.insertText` plus
|
|
15
|
+
* per-character timing for whatever realism it can offer.
|
|
16
|
+
*
|
|
17
|
+
* `attachKeyboardInput({ state, getPageSession, click, dialogs })`
|
|
18
|
+
* returns the bound API. `click` is the mouse-side click — humanType
|
|
19
|
+
* uses it to focus a target before typing.
|
|
20
|
+
*/
|
|
21
|
+
function attachKeyboardInput({ state, getPageSession, click, dialogs }) {
|
|
22
|
+
const { tryHandleDialogSelectorForSession } = require('./dialogs-router.js');
|
|
23
|
+
/**
|
|
24
|
+
* Press a named key (Tab, Enter, F1-F12, arrows, etc.) with optional
|
|
25
|
+
* modifiers. Sends both keyDown and keyUp; if the key has a `text`
|
|
26
|
+
* field (Tab → '\t', Enter → '\r'), it's included on keyDown so the
|
|
27
|
+
* browser fires the matching `input`/`keypress` events that form
|
|
28
|
+
* submission depends on.
|
|
29
|
+
*/
|
|
30
|
+
async function keyboardPress(tabIndexOrWsUrl, keyName, modifiers = {}) {
|
|
31
|
+
const ps = await getPageSession(tabIndexOrWsUrl);
|
|
32
|
+
|
|
33
|
+
let keyDef = KEY_DEFINITIONS[keyName];
|
|
34
|
+
if (!keyDef) {
|
|
35
|
+
if (keyName.length === 1) {
|
|
36
|
+
// Single printable character — build a key def from the char map.
|
|
37
|
+
const charDef = charToKeyDef(keyName);
|
|
38
|
+
keyDef = {
|
|
39
|
+
key: charDef.key,
|
|
40
|
+
code: charDef.code,
|
|
41
|
+
keyCode: charDef.keyCode,
|
|
42
|
+
text: charDef.text,
|
|
43
|
+
};
|
|
44
|
+
} else {
|
|
45
|
+
throw new Error(`Unknown key: ${keyName}. Supported keys: ${Object.keys(KEY_DEFINITIONS).join(', ')}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let modifierFlags = 0;
|
|
50
|
+
if (modifiers.alt) modifierFlags |= 1;
|
|
51
|
+
if (modifiers.ctrl) modifierFlags |= 2;
|
|
52
|
+
if (modifiers.meta) modifierFlags |= 4;
|
|
53
|
+
if (modifiers.shift) modifierFlags |= 8;
|
|
54
|
+
|
|
55
|
+
// When shift is held and the key has a single-character text value,
|
|
56
|
+
// send the uppercase/shifted form so CDP inserts the correct character.
|
|
57
|
+
let keyText = keyDef.text;
|
|
58
|
+
if (modifiers.shift && keyText && keyText.length === 1) {
|
|
59
|
+
const upper = keyText.toUpperCase();
|
|
60
|
+
if (upper !== keyText) {
|
|
61
|
+
// It's a letter — uppercase it.
|
|
62
|
+
keyText = upper;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await ps.send('Input.dispatchKeyEvent', {
|
|
67
|
+
type: 'keyDown',
|
|
68
|
+
key: keyDef.key,
|
|
69
|
+
code: keyDef.code,
|
|
70
|
+
windowsVirtualKeyCode: keyDef.keyCode,
|
|
71
|
+
nativeVirtualKeyCode: keyDef.keyCode,
|
|
72
|
+
modifiers: modifierFlags,
|
|
73
|
+
...(keyText && { text: keyText })
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await ps.send('Input.dispatchKeyEvent', {
|
|
77
|
+
type: 'keyUp',
|
|
78
|
+
key: keyDef.key,
|
|
79
|
+
code: keyDef.code,
|
|
80
|
+
windowsVirtualKeyCode: keyDef.keyCode,
|
|
81
|
+
nativeVirtualKeyCode: keyDef.keyCode,
|
|
82
|
+
modifiers: modifierFlags
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return { pressed: keyName, modifiers };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Smart text input. If `selector` is supplied, focuses the element
|
|
90
|
+
* (via JS focus to avoid mouse-click side effects). Then types the
|
|
91
|
+
* value, treating \t as Tab, \n as Enter (unless current focus is a
|
|
92
|
+
* <textarea>, in which case \n is inserted as a literal newline). Buffers
|
|
93
|
+
* runs of plain characters into single insertText calls — that batches
|
|
94
|
+
* fewer events and is faster than per-character.
|
|
95
|
+
*
|
|
96
|
+
* Special characters in `value`: \t = Tab, \n = Enter (or newline in textarea).
|
|
97
|
+
* Literal "\\t" / "\\n" in the input are also normalised — MCP payloads
|
|
98
|
+
* often arrive with the escapes un-evaluated.
|
|
99
|
+
*/
|
|
100
|
+
async function fill(tabIndexOrWsUrl, selector, value) {
|
|
101
|
+
const ps = await getPageSession(tabIndexOrWsUrl);
|
|
102
|
+
|
|
103
|
+
if (selector && selector.startsWith('dialog::') && dialogs) {
|
|
104
|
+
const dialogState = dialogs.getOpen(ps.sessionId);
|
|
105
|
+
const routed = await tryHandleDialogSelectorForSession({ selector, op: 'type', payload: value, state: dialogState, pageSession: ps });
|
|
106
|
+
if (routed.handled) {
|
|
107
|
+
if (routed.error) throw new Error(routed.error);
|
|
108
|
+
if (routed.clearDialog) dialogs.clear(ps.sessionId);
|
|
109
|
+
return routed.result;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (selector) {
|
|
114
|
+
const focusJs = `
|
|
115
|
+
(() => {
|
|
116
|
+
const el = ${getElementSelector(selector)};
|
|
117
|
+
if (!el) return { success: false, error: 'Element not found' };
|
|
118
|
+
el.focus();
|
|
119
|
+
return { success: true, focused: document.activeElement === el };
|
|
120
|
+
})()
|
|
121
|
+
`;
|
|
122
|
+
const focusResult = await ps.send('Runtime.evaluate', {
|
|
123
|
+
expression: focusJs,
|
|
124
|
+
returnByValue: true
|
|
125
|
+
});
|
|
126
|
+
throwIfExceptionDetails(focusResult);
|
|
127
|
+
if (!focusResult.result?.value?.success) {
|
|
128
|
+
throw new Error(focusResult.result?.value?.error || 'Failed to focus element');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Normalise literal escape sequences from MCP payloads.
|
|
133
|
+
const processedValue = value
|
|
134
|
+
.replace(/\\t/g, '\t')
|
|
135
|
+
.replace(/\\n/g, '\n');
|
|
136
|
+
|
|
137
|
+
const settle = (ms = 50) => new Promise(r => setTimeout(r, ms));
|
|
138
|
+
|
|
139
|
+
let buffer = '';
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < processedValue.length; i++) {
|
|
142
|
+
const char = processedValue[i];
|
|
143
|
+
|
|
144
|
+
if (char === '\t') {
|
|
145
|
+
if (buffer) {
|
|
146
|
+
await ps.send('Input.insertText', { text: buffer });
|
|
147
|
+
await settle();
|
|
148
|
+
buffer = '';
|
|
149
|
+
}
|
|
150
|
+
await keyboardPress(tabIndexOrWsUrl, 'Tab');
|
|
151
|
+
await settle();
|
|
152
|
+
} else if (char === '\n') {
|
|
153
|
+
if (buffer) {
|
|
154
|
+
await ps.send('Input.insertText', { text: buffer });
|
|
155
|
+
await settle();
|
|
156
|
+
buffer = '';
|
|
157
|
+
}
|
|
158
|
+
// Re-check focus — Tab may have shifted it to a different element type.
|
|
159
|
+
const currentFocus = await ps.send('Runtime.evaluate', {
|
|
160
|
+
expression: `({ isTextarea: document.activeElement?.tagName === 'TEXTAREA' })`,
|
|
161
|
+
returnByValue: true
|
|
162
|
+
});
|
|
163
|
+
throwIfExceptionDetails(currentFocus);
|
|
164
|
+
const currentlyInTextarea = currentFocus.result?.value?.isTextarea || false;
|
|
165
|
+
|
|
166
|
+
if (currentlyInTextarea) {
|
|
167
|
+
await ps.send('Input.insertText', { text: '\n' });
|
|
168
|
+
} else {
|
|
169
|
+
await keyboardPress(tabIndexOrWsUrl, 'Enter');
|
|
170
|
+
}
|
|
171
|
+
await settle();
|
|
172
|
+
} else {
|
|
173
|
+
buffer += char;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (buffer) {
|
|
178
|
+
await ps.send('Input.insertText', { text: buffer });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { typed: true, value };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Type text character-by-character with realistic per-keystroke timing.
|
|
186
|
+
* In headed mode, sends keyDown/keyUp around each insertText so JS
|
|
187
|
+
* keyboard events fire — important for bot-detection-resistant input.
|
|
188
|
+
* In headless mode, skips key events because rawKeyDown is interpreted
|
|
189
|
+
* as a browser shortcut and navigates away from the page; relies on
|
|
190
|
+
* insertText + per-character delay for whatever realism it can offer.
|
|
191
|
+
*
|
|
192
|
+
* @param {object} options
|
|
193
|
+
* @param {number} [options.delay=80] - Base delay between keystrokes (ms)
|
|
194
|
+
* @param {number} [options.jitter=80] - Random jitter range (ms) — total ~80–160ms/char
|
|
195
|
+
*/
|
|
196
|
+
async function humanType(tabIndexOrWsUrl, selector, text, options = {}) {
|
|
197
|
+
const ps = await getPageSession(tabIndexOrWsUrl);
|
|
198
|
+
|
|
199
|
+
if (selector && selector.startsWith('dialog::') && dialogs) {
|
|
200
|
+
const dialogState = dialogs.getOpen(ps.sessionId);
|
|
201
|
+
const routed = await tryHandleDialogSelectorForSession({ selector, op: 'type', payload: text, state: dialogState, pageSession: ps });
|
|
202
|
+
if (routed.handled) {
|
|
203
|
+
if (routed.error) throw new Error(routed.error);
|
|
204
|
+
if (routed.clearDialog) dialogs.clear(ps.sessionId);
|
|
205
|
+
return routed.result;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const delay = options.delay !== undefined ? options.delay : 80;
|
|
210
|
+
const jitter = options.jitter !== undefined ? options.jitter : 80;
|
|
211
|
+
|
|
212
|
+
if (selector) {
|
|
213
|
+
await click(tabIndexOrWsUrl, selector);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const char of text) {
|
|
217
|
+
const keyDef = charToKeyDef(char);
|
|
218
|
+
|
|
219
|
+
if (keyDef.special) {
|
|
220
|
+
// \n / \t — delegate to keyboardPress for the named-key path.
|
|
221
|
+
await keyboardPress(tabIndexOrWsUrl, keyDef.special);
|
|
222
|
+
} else {
|
|
223
|
+
const sendKeyEvents = !state.chromeHeadless;
|
|
224
|
+
const modifiers = keyDef.shift ? 8 : 0; // 8 = Shift
|
|
225
|
+
|
|
226
|
+
if (sendKeyEvents) {
|
|
227
|
+
if (keyDef.shift) {
|
|
228
|
+
await ps.send('Input.dispatchKeyEvent', {
|
|
229
|
+
type: 'keyDown',
|
|
230
|
+
key: 'Shift',
|
|
231
|
+
code: 'ShiftLeft',
|
|
232
|
+
windowsVirtualKeyCode: 16,
|
|
233
|
+
nativeVirtualKeyCode: 16,
|
|
234
|
+
modifiers
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await ps.send('Input.dispatchKeyEvent', {
|
|
239
|
+
type: 'rawKeyDown',
|
|
240
|
+
key: keyDef.key,
|
|
241
|
+
code: keyDef.code,
|
|
242
|
+
windowsVirtualKeyCode: keyDef.keyCode,
|
|
243
|
+
nativeVirtualKeyCode: keyDef.keyCode,
|
|
244
|
+
modifiers
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// insertText drives the character into the field reliably in both modes.
|
|
249
|
+
await ps.send('Input.insertText', {
|
|
250
|
+
text: keyDef.text
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (sendKeyEvents) {
|
|
254
|
+
await ps.send('Input.dispatchKeyEvent', {
|
|
255
|
+
type: 'keyUp',
|
|
256
|
+
key: keyDef.key,
|
|
257
|
+
code: keyDef.code,
|
|
258
|
+
windowsVirtualKeyCode: keyDef.keyCode,
|
|
259
|
+
nativeVirtualKeyCode: keyDef.keyCode,
|
|
260
|
+
modifiers
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (keyDef.shift) {
|
|
264
|
+
await ps.send('Input.dispatchKeyEvent', {
|
|
265
|
+
type: 'keyUp',
|
|
266
|
+
key: 'Shift',
|
|
267
|
+
code: 'ShiftLeft',
|
|
268
|
+
windowsVirtualKeyCode: 16,
|
|
269
|
+
nativeVirtualKeyCode: 16,
|
|
270
|
+
modifiers: 0
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (delay > 0 || jitter > 0) {
|
|
277
|
+
const wait = delay + Math.random() * jitter;
|
|
278
|
+
await new Promise(resolve => setTimeout(resolve, wait));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { typed: text, chars: text.length };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { keyboardPress, fill, humanType };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = { attachKeyboardInput };
|