@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.
Files changed (62) hide show
  1. package/README.md +29 -0
  2. package/agents/browser-user.md +105 -0
  3. package/dist/LICENSE +25 -0
  4. package/dist/index.d.ts +9 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +22517 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/payload.d.ts +214 -0
  9. package/dist/payload.d.ts.map +1 -0
  10. package/dist/payload.js +325 -0
  11. package/dist/payload.js.map +1 -0
  12. package/package.json +59 -0
  13. package/skills/browsing/COMMANDLINE-USAGE.md +595 -0
  14. package/skills/browsing/EXAMPLES.md +717 -0
  15. package/skills/browsing/README.md +55 -0
  16. package/skills/browsing/SKILL.md +478 -0
  17. package/skills/browsing/chrome-ws +1021 -0
  18. package/skills/browsing/chrome-ws-lib.js +461 -0
  19. package/skills/browsing/host-override.js +98 -0
  20. package/skills/browsing/lib/browser-bridge.js +175 -0
  21. package/skills/browsing/lib/browser-session.js +137 -0
  22. package/skills/browsing/lib/capture.js +499 -0
  23. package/skills/browsing/lib/cdp-router.js +72 -0
  24. package/skills/browsing/lib/cdp-utils.js +18 -0
  25. package/skills/browsing/lib/chrome-launcher-helpers.js +374 -0
  26. package/skills/browsing/lib/chrome-process.js +464 -0
  27. package/skills/browsing/lib/console-logging.js +70 -0
  28. package/skills/browsing/lib/cookies.js +17 -0
  29. package/skills/browsing/lib/dialogs-render.js +154 -0
  30. package/skills/browsing/lib/dialogs-router.js +117 -0
  31. package/skills/browsing/lib/dialogs.js +254 -0
  32. package/skills/browsing/lib/element-selector.js +91 -0
  33. package/skills/browsing/lib/evaluation.js +85 -0
  34. package/skills/browsing/lib/extraction.js +55 -0
  35. package/skills/browsing/lib/file-upload.js +56 -0
  36. package/skills/browsing/lib/html-diff.js +122 -0
  37. package/skills/browsing/lib/key-definitions.js +149 -0
  38. package/skills/browsing/lib/keyboard-input.js +288 -0
  39. package/skills/browsing/lib/mouse.js +423 -0
  40. package/skills/browsing/lib/navigation.js +272 -0
  41. package/skills/browsing/lib/page-scripts/dom-summary.js +31 -0
  42. package/skills/browsing/lib/page-scripts/markdown.js +85 -0
  43. package/skills/browsing/lib/page-scripts/permission-shim.js +80 -0
  44. package/skills/browsing/lib/page-session.js +106 -0
  45. package/skills/browsing/lib/profile-lock.js +179 -0
  46. package/skills/browsing/lib/screenshot.js +171 -0
  47. package/skills/browsing/lib/select-option.js +99 -0
  48. package/skills/browsing/lib/session-state.js +66 -0
  49. package/skills/browsing/lib/tabs.js +144 -0
  50. package/skills/browsing/lib/viewport.js +103 -0
  51. package/skills/browsing/lib/websocket-client.js +162 -0
  52. package/skills/browsing/package.json +11 -0
  53. package/skills/browsing/test-chrome-args.js +81 -0
  54. package/skills/browsing/test-cookies.js +21 -0
  55. package/skills/browsing/test-e2e.sh +51 -0
  56. package/skills/browsing/test-extract.sh +17 -0
  57. package/skills/browsing/test-interact.sh +11 -0
  58. package/skills/browsing/test-navigate.sh +9 -0
  59. package/skills/browsing/test-raw.sh +8 -0
  60. package/skills/browsing/test-tabs.sh +15 -0
  61. package/skills/browsing/test-viewport.js +27 -0
  62. 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 };