@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,85 @@
1
+ /**
2
+ * Runtime.evaluate wrappers — three flavors, for three return-value
3
+ * shapes. The differences matter (see JRV-126):
4
+ *
5
+ * - `evaluate`: legacy `returnByValue: true`. Returns `result.result.value`.
6
+ * Loses type information for complex objects (DOM nodes come back as
7
+ * `[object Object]`-style descriptions). Awaits Promises.
8
+ *
9
+ * - `evaluateJson`: wraps the expression so the page-side code stringifies
10
+ * complex returns into a tagged shape (`{__type: 'Element', ...}`,
11
+ * `{__type: 'undefined'}`, etc.). Use when you want to inspect DOM
12
+ * nodes or distinguish undefined/null/error in the result.
13
+ *
14
+ * - `evaluateRaw`: `returnByValue: false`. Returns `result.result` (the
15
+ * full RemoteObject including `objectId`). For callers that need the
16
+ * raw CDP shape.
17
+ *
18
+ * `attachEvaluation({ getPageSession })` binds them to a session via the
19
+ * pageSession resolver.
20
+ */
21
+ const { throwIfExceptionDetails } = require('./cdp-utils');
22
+
23
+ function attachEvaluation({ getPageSession }) {
24
+ async function evaluate(tabIndexOrWsUrl, expression) {
25
+ const pageSession = await getPageSession(tabIndexOrWsUrl);
26
+ const result = await pageSession.send('Runtime.evaluate', {
27
+ expression,
28
+ returnByValue: true,
29
+ awaitPromise: true
30
+ });
31
+ throwIfExceptionDetails(result);
32
+ return result.result.value;
33
+ }
34
+
35
+ async function evaluateJson(tabIndexOrWsUrl, expression) {
36
+ const pageSession = await getPageSession(tabIndexOrWsUrl);
37
+
38
+ const wrappedExpression = `
39
+ (() => {
40
+ try {
41
+ const result = ${expression};
42
+ if (result === undefined) return { __type: 'undefined' };
43
+ if (result === null) return null;
44
+ if (result instanceof Element) {
45
+ return {
46
+ __type: 'Element',
47
+ tagName: result.tagName,
48
+ id: result.id,
49
+ className: result.className,
50
+ textContent: result.textContent?.slice(0, 100)
51
+ };
52
+ }
53
+ if (typeof result === 'function') {
54
+ return { __type: 'function', name: result.name || 'anonymous' };
55
+ }
56
+ return result;
57
+ } catch (e) {
58
+ return { __type: 'error', message: e.message };
59
+ }
60
+ })()
61
+ `;
62
+
63
+ const result = await pageSession.send('Runtime.evaluate', {
64
+ expression: wrappedExpression,
65
+ returnByValue: true,
66
+ awaitPromise: true
67
+ });
68
+ throwIfExceptionDetails(result);
69
+ return result.result.value;
70
+ }
71
+
72
+ async function evaluateRaw(tabIndexOrWsUrl, expression) {
73
+ const pageSession = await getPageSession(tabIndexOrWsUrl);
74
+ const result = await pageSession.send('Runtime.evaluate', {
75
+ expression,
76
+ returnByValue: false
77
+ });
78
+ throwIfExceptionDetails(result);
79
+ return result.result;
80
+ }
81
+
82
+ return { evaluate, evaluateJson, evaluateRaw };
83
+ }
84
+
85
+ module.exports = { attachEvaluation };
@@ -0,0 +1,55 @@
1
+ const { getElementSelector } = require('./element-selector');
2
+ const { throwIfExceptionDetails } = require('./cdp-utils');
3
+
4
+ /**
5
+ * Single-element extraction primitives — text content, HTML, attributes.
6
+ *
7
+ * Each is a thin wrapper around `Runtime.evaluate` that uses optional
8
+ * chaining to return `null`/`undefined` when the selector misses, so the
9
+ * caller doesn't have to distinguish "element not found" from "element
10
+ * found but empty." The page-content / DOM-summary / markdown extractors
11
+ * (the heavyweight ones used by auto-capture) live in `lib/capture.js`.
12
+ *
13
+ * `attachExtraction({ getPageSession })` returns the bound methods — no
14
+ * session state needed.
15
+ */
16
+ function attachExtraction({ getPageSession }) {
17
+ async function extractText(tabIndexOrWsUrl, selector) {
18
+ const ps = await getPageSession(tabIndexOrWsUrl);
19
+ const js = `${getElementSelector(selector)}?.textContent`;
20
+ const result = await ps.send('Runtime.evaluate', {
21
+ expression: js,
22
+ returnByValue: true
23
+ });
24
+ throwIfExceptionDetails(result);
25
+ return result.result.value;
26
+ }
27
+
28
+ async function getHtml(tabIndexOrWsUrl, selector = null) {
29
+ const ps = await getPageSession(tabIndexOrWsUrl);
30
+ const js = selector
31
+ ? `${getElementSelector(selector)}?.innerHTML`
32
+ : 'document.documentElement.outerHTML';
33
+ const result = await ps.send('Runtime.evaluate', {
34
+ expression: js,
35
+ returnByValue: true
36
+ });
37
+ throwIfExceptionDetails(result);
38
+ return result.result.value;
39
+ }
40
+
41
+ async function getAttribute(tabIndexOrWsUrl, selector, attrName) {
42
+ const ps = await getPageSession(tabIndexOrWsUrl);
43
+ const js = `${getElementSelector(selector)}?.getAttribute(${JSON.stringify(attrName)})`;
44
+ const result = await ps.send('Runtime.evaluate', {
45
+ expression: js,
46
+ returnByValue: true
47
+ });
48
+ throwIfExceptionDetails(result);
49
+ return result.result.value;
50
+ }
51
+
52
+ return { extractText, getHtml, getAttribute };
53
+ }
54
+
55
+ module.exports = { attachExtraction };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Programmatic file upload via `DOM.setFileInputFiles` — the only way to
3
+ * set files on an `<input type="file">` from outside the page, since JS
4
+ * security restrictions block synthetic file assignment.
5
+ *
6
+ * Resolves the input element via `DOM.querySelector` for CSS selectors
7
+ * or `DOM.performSearch` + `DOM.getSearchResults` for XPath, then attaches
8
+ * the absolute file paths to the input.
9
+ *
10
+ * `attachFileUpload({ getPageSession })` returns the bound action.
11
+ */
12
+ function attachFileUpload({ getPageSession }) {
13
+ async function fileUpload(tabIndexOrWsUrl, selector, filePaths) {
14
+ const pageSession = await getPageSession(tabIndexOrWsUrl);
15
+
16
+ const docResult = await pageSession.send('DOM.getDocument', {});
17
+ const rootNodeId = docResult.root.nodeId;
18
+
19
+ let nodeId;
20
+ if (selector.startsWith('/') || selector.startsWith('//')) {
21
+ const searchResult = await pageSession.send('DOM.performSearch', {
22
+ query: selector
23
+ });
24
+ if (searchResult.resultCount === 0) {
25
+ throw new Error(`File input not found: ${selector}`);
26
+ }
27
+ const nodesResult = await pageSession.send('DOM.getSearchResults', {
28
+ searchId: searchResult.searchId,
29
+ fromIndex: 0,
30
+ toIndex: 1
31
+ });
32
+ nodeId = nodesResult.nodeIds[0];
33
+ } else {
34
+ const queryResult = await pageSession.send('DOM.querySelector', {
35
+ nodeId: rootNodeId,
36
+ selector: selector
37
+ });
38
+ nodeId = queryResult.nodeId;
39
+ }
40
+
41
+ if (!nodeId) {
42
+ throw new Error(`File input not found: ${selector}`);
43
+ }
44
+
45
+ await pageSession.send('DOM.setFileInputFiles', {
46
+ files: filePaths,
47
+ nodeId: nodeId
48
+ });
49
+
50
+ return { uploaded: true, files: filePaths.length };
51
+ }
52
+
53
+ return { fileUpload };
54
+ }
55
+
56
+ module.exports = { attachFileUpload };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Line-based diff between two HTML strings using Myers' algorithm.
3
+ * Returns a human-readable summary with REMOVED and ADDED sections,
4
+ * capped at 50 lines per side with "and N more" footer. Used by
5
+ * capturePageArtifacts to attach a diff to the captured page state.
6
+ *
7
+ * Myers (not set-based) so reordered identical lines are correctly
8
+ * detected as a remove + add pair, not "no changes."
9
+ *
10
+ * Pure function. Hand-rolled — no npm dependency.
11
+ */
12
+
13
+ const MAX_LINES_PER_SIDE = 50;
14
+ const MAX_LINE_LENGTH = 200;
15
+
16
+ // Myers' O((N+M)D) shortest-edit-script. Returns an array of
17
+ // { type: 'eq'|'del'|'add', value: string } operations in order.
18
+ function myersDiff(a, b) {
19
+ const N = a.length;
20
+ const M = b.length;
21
+ const max = N + M;
22
+ const v = new Array(2 * max + 1);
23
+ const trace = [];
24
+
25
+ v[max + 1] = 0;
26
+ for (let d = 0; d <= max; d++) {
27
+ trace.push(v.slice());
28
+ for (let k = -d; k <= d; k += 2) {
29
+ let x;
30
+ if (k === -d || (k !== d && v[max + k - 1] < v[max + k + 1])) {
31
+ x = v[max + k + 1];
32
+ } else {
33
+ x = v[max + k - 1] + 1;
34
+ }
35
+ let y = x - k;
36
+ while (x < N && y < M && a[x] === b[y]) {
37
+ x++; y++;
38
+ }
39
+ v[max + k] = x;
40
+ if (x >= N && y >= M) {
41
+ // Backtrack through the trace to build the edit script.
42
+ return backtrack(trace, a, b, N, M, max);
43
+ }
44
+ }
45
+ }
46
+ return [];
47
+ }
48
+
49
+ function backtrack(trace, a, b, N, M, max) {
50
+ const ops = [];
51
+ let x = N;
52
+ let y = M;
53
+ for (let d = trace.length - 1; d > 0; d--) {
54
+ const v = trace[d];
55
+ const k = x - y;
56
+ let prevK;
57
+ if (k === -d || (k !== d && v[max + k - 1] < v[max + k + 1])) {
58
+ prevK = k + 1;
59
+ } else {
60
+ prevK = k - 1;
61
+ }
62
+ const prevX = v[max + prevK];
63
+ const prevY = prevX - prevK;
64
+ while (x > prevX && y > prevY) {
65
+ ops.push({ type: 'eq', value: a[x - 1] });
66
+ x--; y--;
67
+ }
68
+ if (d > 0) {
69
+ if (x === prevX) {
70
+ ops.push({ type: 'add', value: b[y - 1] });
71
+ y--;
72
+ } else {
73
+ ops.push({ type: 'del', value: a[x - 1] });
74
+ x--;
75
+ }
76
+ }
77
+ }
78
+ while (x > 0 && y > 0) {
79
+ ops.push({ type: 'eq', value: a[x - 1] });
80
+ x--; y--;
81
+ }
82
+ return ops.reverse();
83
+ }
84
+
85
+ function generateHtmlDiff(beforeHtml, afterHtml) {
86
+ const beforeLines = (beforeHtml || '').split('\n');
87
+ const afterLines = (afterHtml || '').split('\n');
88
+
89
+ const ops = myersDiff(beforeLines, afterLines);
90
+
91
+ const removed = ops.filter(o => o.type === 'del' && o.value.trim()).map(o => o.value);
92
+ const added = ops.filter(o => o.type === 'add' && o.value.trim()).map(o => o.value);
93
+
94
+ let diff = '';
95
+ if (removed.length > 0) {
96
+ diff += '=== REMOVED ===\n';
97
+ diff += removed.slice(0, MAX_LINES_PER_SIDE)
98
+ .map(l => '- ' + l.slice(0, MAX_LINE_LENGTH))
99
+ .join('\n');
100
+ if (removed.length > MAX_LINES_PER_SIDE) {
101
+ diff += `\n... and ${removed.length - MAX_LINES_PER_SIDE} more removed lines`;
102
+ }
103
+ diff += '\n\n';
104
+ }
105
+ if (added.length > 0) {
106
+ diff += '=== ADDED ===\n';
107
+ diff += added.slice(0, MAX_LINES_PER_SIDE)
108
+ .map(l => '+ ' + l.slice(0, MAX_LINE_LENGTH))
109
+ .join('\n');
110
+ if (added.length > MAX_LINES_PER_SIDE) {
111
+ diff += `\n... and ${added.length - MAX_LINES_PER_SIDE} more added lines`;
112
+ }
113
+ }
114
+
115
+ if (!diff) {
116
+ diff = '(no changes detected)';
117
+ }
118
+
119
+ return diff;
120
+ }
121
+
122
+ module.exports = { generateHtmlDiff };
@@ -0,0 +1,149 @@
1
+ /**
2
+ * CDP key definitions for keyboard.press support (JRV-127).
3
+ *
4
+ * Maps human-readable key names to the CDP `Input.dispatchKeyEvent` payload
5
+ * fields. Keys with a `text` property trigger native browser behaviors
6
+ * (form submit on Enter, focus advance on Tab) — keys without `text` are
7
+ * dispatched as `rawKeyDown`/`keyUp` only.
8
+ */
9
+
10
+ const KEY_DEFINITIONS = {
11
+ // Navigation keys - text property needed for Enter/Tab to trigger native behaviors
12
+ 'Tab': { key: 'Tab', code: 'Tab', keyCode: 9, text: '\t' },
13
+ 'Enter': { key: 'Enter', code: 'Enter', keyCode: 13, text: '\r' },
14
+ 'Escape': { key: 'Escape', code: 'Escape', keyCode: 27 },
15
+ 'Backspace': { key: 'Backspace', code: 'Backspace', keyCode: 8 },
16
+ 'Delete': { key: 'Delete', code: 'Delete', keyCode: 46 },
17
+ 'Space': { key: ' ', code: 'Space', keyCode: 32, text: ' ' },
18
+
19
+ // Arrow keys
20
+ 'ArrowUp': { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
21
+ 'ArrowDown': { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
22
+ 'ArrowLeft': { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
23
+ 'ArrowRight': { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
24
+
25
+ // Modifier keys
26
+ 'Shift': { key: 'Shift', code: 'ShiftLeft', keyCode: 16 },
27
+ 'Control': { key: 'Control', code: 'ControlLeft', keyCode: 17 },
28
+ 'Alt': { key: 'Alt', code: 'AltLeft', keyCode: 18 },
29
+ 'Meta': { key: 'Meta', code: 'MetaLeft', keyCode: 91 },
30
+
31
+ // Function keys
32
+ 'F1': { key: 'F1', code: 'F1', keyCode: 112 },
33
+ 'F2': { key: 'F2', code: 'F2', keyCode: 113 },
34
+ 'F3': { key: 'F3', code: 'F3', keyCode: 114 },
35
+ 'F4': { key: 'F4', code: 'F4', keyCode: 115 },
36
+ 'F5': { key: 'F5', code: 'F5', keyCode: 116 },
37
+ 'F6': { key: 'F6', code: 'F6', keyCode: 117 },
38
+ 'F7': { key: 'F7', code: 'F7', keyCode: 118 },
39
+ 'F8': { key: 'F8', code: 'F8', keyCode: 119 },
40
+ 'F9': { key: 'F9', code: 'F9', keyCode: 120 },
41
+ 'F10': { key: 'F10', code: 'F10', keyCode: 121 },
42
+ 'F11': { key: 'F11', code: 'F11', keyCode: 122 },
43
+ 'F12': { key: 'F12', code: 'F12', keyCode: 123 },
44
+
45
+ // Other
46
+ 'Home': { key: 'Home', code: 'Home', keyCode: 36 },
47
+ 'End': { key: 'End', code: 'End', keyCode: 35 },
48
+ 'PageUp': { key: 'PageUp', code: 'PageUp', keyCode: 33 },
49
+ 'PageDown': { key: 'PageDown', code: 'PageDown', keyCode: 34 },
50
+ 'Insert': { key: 'Insert', code: 'Insert', keyCode: 45 },
51
+ };
52
+
53
+ // Map shifted symbols to their unshifted base character — used by charToKeyDef
54
+ // to figure out the underlying physical key and that Shift must be held.
55
+ const SHIFT_SYMBOLS = {
56
+ '!': '1', '@': '2', '#': '3', '$': '4', '%': '5',
57
+ '^': '6', '&': '7', '*': '8', '(': '9', ')': '0',
58
+ '_': '-', '+': '=', '{': '[', '}': ']', '|': '\\',
59
+ ':': ';', '"': "'", '<': ',', '>': '.', '?': '/',
60
+ '~': '`',
61
+ };
62
+
63
+ // Punctuation key codes — physical key names for non-letter, non-digit keys.
64
+ const PUNCT_CODES = {
65
+ '-': 'Minus', '=': 'Equal', '[': 'BracketLeft', ']': 'BracketRight',
66
+ '\\': 'Backslash', ';': 'Semicolon', "'": 'Quote',
67
+ ',': 'Comma', '.': 'Period', '/': 'Slash', '`': 'Backquote',
68
+ };
69
+
70
+ /**
71
+ * Map a single character to its CDP key-event payload (key/code/keyCode/text/shift).
72
+ * Pure function — no Chrome session dependency. Returns `{ special: 'Enter'|'Tab' }`
73
+ * for characters that should be dispatched via the named-key path instead.
74
+ */
75
+ function charToKeyDef(char) {
76
+ if (char === '\n') return { special: 'Enter' };
77
+ if (char === '\t') return { special: 'Tab' };
78
+
79
+ if (char === ' ') {
80
+ return { key: ' ', code: 'Space', keyCode: 32, text: ' ', shift: false };
81
+ }
82
+
83
+ if (char >= 'A' && char <= 'Z') {
84
+ return {
85
+ key: char,
86
+ code: 'Key' + char,
87
+ keyCode: char.charCodeAt(0),
88
+ text: char,
89
+ shift: true
90
+ };
91
+ }
92
+
93
+ if (char >= 'a' && char <= 'z') {
94
+ return {
95
+ key: char,
96
+ code: 'Key' + char.toUpperCase(),
97
+ keyCode: char.toUpperCase().charCodeAt(0),
98
+ text: char,
99
+ shift: false
100
+ };
101
+ }
102
+
103
+ if (char >= '0' && char <= '9') {
104
+ return {
105
+ key: char,
106
+ code: 'Digit' + char,
107
+ keyCode: char.charCodeAt(0),
108
+ text: char,
109
+ shift: false
110
+ };
111
+ }
112
+
113
+ if (SHIFT_SYMBOLS[char]) {
114
+ const baseChar = SHIFT_SYMBOLS[char];
115
+ let code;
116
+ if (baseChar >= '0' && baseChar <= '9') {
117
+ code = 'Digit' + baseChar;
118
+ } else {
119
+ code = PUNCT_CODES[baseChar] || 'Unidentified';
120
+ }
121
+ return {
122
+ key: char,
123
+ code,
124
+ keyCode: baseChar.charCodeAt(0),
125
+ text: char,
126
+ shift: true
127
+ };
128
+ }
129
+
130
+ if (PUNCT_CODES[char]) {
131
+ return {
132
+ key: char,
133
+ code: PUNCT_CODES[char],
134
+ keyCode: char.charCodeAt(0),
135
+ text: char,
136
+ shift: false
137
+ };
138
+ }
139
+
140
+ return {
141
+ key: char,
142
+ code: 'Unidentified',
143
+ keyCode: char.charCodeAt(0),
144
+ text: char,
145
+ shift: false
146
+ };
147
+ }
148
+
149
+ module.exports = { KEY_DEFINITIONS, SHIFT_SYMBOLS, charToKeyDef };