@browserbridge/bbx 1.7.8 → 1.8.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/PRIVACY.md +30 -11
- package/README.md +9 -5
- package/package.json +9 -6
- package/packages/agent-client/src/cli-args.js +24 -0
- package/packages/agent-client/src/cli.js +46 -52
- package/packages/agent-client/src/client.js +140 -5
- package/packages/agent-client/src/command-registry.js +56 -53
- package/packages/agent-client/src/content-extract.js +198 -0
- package/packages/agent-client/src/remotes.js +1 -1
- package/packages/agent-client/src/runtime.js +768 -35
- package/packages/agent-client/src/types.ts +131 -6
- package/packages/mcp-server/src/guidance.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +41 -5
- package/packages/mcp-server/src/handlers-dom.js +8 -2
- package/packages/mcp-server/src/handlers-navigation.js +2 -1
- package/packages/mcp-server/src/handlers-page.js +41 -4
- package/packages/mcp-server/src/handlers-utils.js +67 -7
- package/packages/mcp-server/src/handlers.js +4 -4
- package/packages/mcp-server/src/server.js +102 -6
- package/packages/native-host/src/daemon.js +158 -6
- package/packages/native-host/src/native-host.js +3 -1
- package/packages/protocol/src/defaults.js +3 -0
- package/packages/protocol/src/errors.js +54 -1
- package/packages/protocol/src/protocol.js +297 -1
- package/packages/protocol/src/registry.js +66 -35
- package/packages/protocol/src/summary.js +87 -5
- package/packages/protocol/src/types.ts +234 -0
- package/skills/browser-bridge/SKILL.md +34 -13
- package/skills/browser-bridge/references/interaction.md +55 -11
- package/skills/browser-bridge/references/patch-workflow.md +2 -0
- package/skills/browser-bridge/references/protocol.md +157 -82
- package/skills/browser-bridge/references/token-efficiency.md +19 -13
|
@@ -30,103 +30,106 @@ function createShortcutCommand(method, usage, build, options = {}) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Preserve explicit refs and pass selectors directly to methods that support
|
|
35
|
+
* atomic target resolution.
|
|
36
|
+
*
|
|
37
|
+
* @param {string | undefined} value
|
|
38
|
+
* @returns {{ elementRef: string } | { selector: string }}
|
|
39
|
+
*/
|
|
40
|
+
function createAtomicTarget(value) {
|
|
41
|
+
if (!value) throw new Error('A ref or selector is required.');
|
|
42
|
+
return value.startsWith('el_') ? { elementRef: value } : { selector: value };
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
/** @type {Record<string, ShortcutCommand>} */
|
|
34
46
|
export const SHORTCUT_COMMANDS = {
|
|
35
|
-
'access-request': createShortcutCommand('access.request', 'bbx access-request', () =>
|
|
47
|
+
'access-request': createShortcutCommand('access.request', 'bbx access-request [intent]', (r) =>
|
|
48
|
+
r[0] ? { intent: r[0] } : {}
|
|
49
|
+
),
|
|
36
50
|
'dom-query': createShortcutCommand('dom.query', 'bbx dom-query [selector]', (r) => ({
|
|
37
51
|
selector: r[0] || 'body',
|
|
38
52
|
})),
|
|
39
53
|
describe: createShortcutCommand(
|
|
40
54
|
'dom.describe',
|
|
41
55
|
'bbx describe <ref|selector>',
|
|
42
|
-
(
|
|
43
|
-
{
|
|
56
|
+
(r) => ({ target: createAtomicTarget(r[0]) }),
|
|
57
|
+
{ printMethod: 'dom.describe' }
|
|
44
58
|
),
|
|
45
59
|
text: createShortcutCommand(
|
|
46
60
|
'dom.get_text',
|
|
47
61
|
'bbx text <ref|selector> [budget]',
|
|
48
|
-
(r
|
|
49
|
-
|
|
62
|
+
(r) => ({
|
|
63
|
+
target: createAtomicTarget(r[0]),
|
|
50
64
|
textBudget: r[1] ? parseIntArg(r[1], 'budget') : undefined,
|
|
51
65
|
}),
|
|
52
|
-
{
|
|
66
|
+
{ printMethod: 'dom.get_text' }
|
|
53
67
|
),
|
|
54
68
|
styles: createShortcutCommand(
|
|
55
69
|
'styles.get_computed',
|
|
56
70
|
'bbx styles <ref|selector> [props]',
|
|
57
|
-
(r
|
|
58
|
-
{
|
|
71
|
+
(r) => ({ target: createAtomicTarget(r[0]), properties: parseCommaList(r[1]) }),
|
|
72
|
+
{ printMethod: 'styles.get_computed' }
|
|
59
73
|
),
|
|
60
74
|
box: createShortcutCommand(
|
|
61
75
|
'layout.get_box_model',
|
|
62
76
|
'bbx box <ref|selector>',
|
|
63
|
-
(
|
|
64
|
-
{
|
|
65
|
-
),
|
|
66
|
-
click: createShortcutCommand(
|
|
67
|
-
'input.click',
|
|
68
|
-
'bbx click <ref|selector> [button]',
|
|
69
|
-
(r, ref) => ({ target: { elementRef: ref }, button: r[1] }),
|
|
70
|
-
{ resolve: true }
|
|
71
|
-
),
|
|
72
|
-
focus: createShortcutCommand(
|
|
73
|
-
'input.focus',
|
|
74
|
-
'bbx focus <ref|selector>',
|
|
75
|
-
(_r, ref) => ({ target: { elementRef: ref } }),
|
|
76
|
-
{ resolve: true }
|
|
77
|
-
),
|
|
78
|
-
type: createShortcutCommand(
|
|
79
|
-
'input.type',
|
|
80
|
-
'bbx type <ref|selector> <text...>',
|
|
81
|
-
(r, ref) => ({ target: { elementRef: ref }, text: r.slice(1).join(' ') }),
|
|
82
|
-
{ resolve: true }
|
|
77
|
+
(r) => ({ target: createAtomicTarget(r[0]) }),
|
|
78
|
+
{ printMethod: 'layout.get_box_model' }
|
|
83
79
|
),
|
|
80
|
+
click: createShortcutCommand('input.click', 'bbx click <ref|selector> [button]', (r) => ({
|
|
81
|
+
target: createAtomicTarget(r[0]),
|
|
82
|
+
button: r[1],
|
|
83
|
+
})),
|
|
84
|
+
focus: createShortcutCommand('input.focus', 'bbx focus <ref|selector>', (r) => ({
|
|
85
|
+
target: createAtomicTarget(r[0]),
|
|
86
|
+
})),
|
|
87
|
+
type: createShortcutCommand('input.type', 'bbx type <ref|selector> <text...>', (r) => ({
|
|
88
|
+
target: createAtomicTarget(r[0]),
|
|
89
|
+
text: r.slice(1).join(' '),
|
|
90
|
+
})),
|
|
84
91
|
fill: createShortcutCommand(
|
|
85
92
|
'input.fill',
|
|
86
93
|
'bbx fill <ref|selector> <value>',
|
|
87
|
-
(r
|
|
88
|
-
target:
|
|
94
|
+
(r) => ({
|
|
95
|
+
target: createAtomicTarget(r[0]),
|
|
89
96
|
value: r.slice(1).join(' '),
|
|
90
97
|
mode: 'auto',
|
|
91
98
|
}),
|
|
92
99
|
{
|
|
93
|
-
resolve: true,
|
|
94
100
|
description: 'Set input/textarea value (React/Vue/Angular-safe, auto fallback to keystrokes)',
|
|
95
101
|
}
|
|
96
102
|
),
|
|
97
|
-
hover: createShortcutCommand(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
(_r, ref) => ({ target: { elementRef: ref } }),
|
|
101
|
-
{ resolve: true }
|
|
102
|
-
),
|
|
103
|
+
hover: createShortcutCommand('input.hover', 'bbx hover <ref|selector>', (r) => ({
|
|
104
|
+
target: createAtomicTarget(r[0]),
|
|
105
|
+
})),
|
|
103
106
|
html: createShortcutCommand(
|
|
104
107
|
'dom.get_html',
|
|
105
108
|
'bbx html <ref|selector> [maxLen]',
|
|
106
|
-
(r
|
|
107
|
-
|
|
109
|
+
(r) => ({
|
|
110
|
+
target: createAtomicTarget(r[0]),
|
|
108
111
|
maxLength: r[1] ? parseIntArg(r[1], 'maxLen') : undefined,
|
|
109
112
|
}),
|
|
110
|
-
{
|
|
113
|
+
{}
|
|
111
114
|
),
|
|
112
115
|
'patch-style': createShortcutCommand(
|
|
113
116
|
'patch.apply_styles',
|
|
114
117
|
'bbx patch-style <ref|sel> prop=val',
|
|
115
|
-
(r
|
|
116
|
-
target:
|
|
118
|
+
(r) => ({
|
|
119
|
+
target: createAtomicTarget(r[0]),
|
|
117
120
|
declarations: parsePropertyAssignments(r.slice(1)),
|
|
118
121
|
}),
|
|
119
|
-
{
|
|
122
|
+
{}
|
|
120
123
|
),
|
|
121
124
|
'patch-text': createShortcutCommand(
|
|
122
125
|
'patch.apply_dom',
|
|
123
126
|
'bbx patch-text <ref|sel> <text...>',
|
|
124
|
-
(r
|
|
125
|
-
target:
|
|
127
|
+
(r) => ({
|
|
128
|
+
target: createAtomicTarget(r[0]),
|
|
126
129
|
operation: 'set_text',
|
|
127
130
|
value: r.slice(1).join(' '),
|
|
128
131
|
}),
|
|
129
|
-
{
|
|
132
|
+
{ description: 'Apply a reversible DOM text patch' }
|
|
130
133
|
),
|
|
131
134
|
patches: createShortcutCommand('patch.list', 'bbx patches', () => ({}), {
|
|
132
135
|
printMethod: 'patch.list',
|
|
@@ -222,14 +225,14 @@ export const SHORTCUT_COMMANDS = {
|
|
|
222
225
|
attrs: createShortcutCommand(
|
|
223
226
|
'dom.get_attributes',
|
|
224
227
|
'bbx attrs <ref|selector> [attr1,...]',
|
|
225
|
-
(r
|
|
226
|
-
{
|
|
228
|
+
(r) => ({ target: createAtomicTarget(r[0]), attributes: parseCommaList(r[1]) }),
|
|
229
|
+
{ printMethod: 'dom.get_attributes' }
|
|
227
230
|
),
|
|
228
231
|
'matched-rules': createShortcutCommand(
|
|
229
232
|
'styles.get_matched_rules',
|
|
230
233
|
'bbx matched-rules <ref|selector>',
|
|
231
|
-
(
|
|
232
|
-
{
|
|
234
|
+
(r) => ({ target: createAtomicTarget(r[0]) }),
|
|
235
|
+
{ printMethod: 'styles.get_matched_rules' }
|
|
233
236
|
),
|
|
234
237
|
};
|
|
235
238
|
|
|
@@ -260,7 +263,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
260
263
|
'bbx install-skill [targets|all] [--global] [--project <path>] Install/update the managed Browser Bridge CLI skill',
|
|
261
264
|
'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf|agents',
|
|
262
265
|
'bbx status Check bridge connection',
|
|
263
|
-
'bbx doctor
|
|
266
|
+
'bbx doctor Consolidated local runtime, setup, protocol, and readiness diagnostics',
|
|
264
267
|
'bbx restart Restart the local daemon and running MCP servers',
|
|
265
268
|
'bbx proxy enable [--port 9223] [--bind-host 127.0.0.1] [--rotate-token] Enable loopback TCP proxy mode for SSH tunneling',
|
|
266
269
|
' Non-loopback binds require --unsafe-plaintext because raw TCP is unencrypted.',
|
|
@@ -270,7 +273,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
270
273
|
'bbx remote list List remote destinations',
|
|
271
274
|
'bbx remote test <name> Ping a remote destination',
|
|
272
275
|
'bbx remote remove <name> Remove a remote destination',
|
|
273
|
-
'bbx access-request
|
|
276
|
+
'bbx access-request [intent] Request access with inspect|interact|capture|navigate|debugger|general intent',
|
|
274
277
|
'bbx logs Recent bridge logs',
|
|
275
278
|
'bbx tabs List available tabs',
|
|
276
279
|
'bbx tab-create [url] Create a new tab',
|
|
@@ -372,7 +375,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
|
|
|
372
375
|
{
|
|
373
376
|
title: 'Capture',
|
|
374
377
|
lines: [
|
|
375
|
-
'bbx screenshot [--tab <tabId>] <ref|selector> [path]
|
|
378
|
+
'bbx screenshot [--tab <tabId>] [--format png|jpeg|webp] [--quality 0-100] <ref|selector> [path]',
|
|
376
379
|
],
|
|
377
380
|
},
|
|
378
381
|
]);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { Readability } from '@mozilla/readability';
|
|
4
|
+
import { parseHTML } from 'linkedom';
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('../../protocol/src/types.js').ExtractContentResult} ExtractContentResult */
|
|
7
|
+
/** @typedef {import('../../protocol/src/types.js').NormalizedExtractContentParams} ExtractContentParams */
|
|
8
|
+
|
|
9
|
+
const METADATA_LIMITS = Object.freeze({ title: 300, byline: 200, excerpt: 500, siteName: 200 });
|
|
10
|
+
const EXCLUDED_SELECTOR =
|
|
11
|
+
'script,style,noscript,template,input,textarea,select,option,button,[hidden],[aria-hidden="true"]';
|
|
12
|
+
|
|
13
|
+
/** @param {string | null | undefined} value @param {number} limit @returns {string | undefined} */
|
|
14
|
+
function boundedText(value, limit) {
|
|
15
|
+
const normalized = String(value ?? '')
|
|
16
|
+
.replace(/\s+/g, ' ')
|
|
17
|
+
.trim();
|
|
18
|
+
return normalized ? normalized.slice(0, limit) : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @param {ParentNode} root @returns {void} */
|
|
22
|
+
function sanitize(root) {
|
|
23
|
+
for (const element of root.querySelectorAll(EXCLUDED_SELECTOR)) element.remove();
|
|
24
|
+
for (const element of root.querySelectorAll('[style]')) {
|
|
25
|
+
const style = element.getAttribute('style')?.toLowerCase() ?? '';
|
|
26
|
+
if (/display\s*:\s*none|visibility\s*:\s*hidden/.test(style)) element.remove();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @param {Element} element @returns {number} */
|
|
31
|
+
function meaningfulLength(element) {
|
|
32
|
+
return (element.textContent ?? '').replace(/\s+/g, ' ').trim().length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {ParentNode} root @returns {Element | null} */
|
|
36
|
+
function largestSemanticRoot(root) {
|
|
37
|
+
let best = null;
|
|
38
|
+
let bestLength = 0;
|
|
39
|
+
const candidates = [];
|
|
40
|
+
if (
|
|
41
|
+
'matches' in root &&
|
|
42
|
+
typeof (/** @type {{ matches?: unknown }} */ (root).matches) === 'function' &&
|
|
43
|
+
/** @type {Element} */ (root).matches('article,main,[role="main"]')
|
|
44
|
+
) {
|
|
45
|
+
candidates.push(/** @type {Element} */ (root));
|
|
46
|
+
}
|
|
47
|
+
candidates.push(...root.querySelectorAll('article,main,[role="main"]'));
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
const length = meaningfulLength(candidate);
|
|
50
|
+
if (length > bestLength) {
|
|
51
|
+
best = candidate;
|
|
52
|
+
bestLength = length;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return best;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @param {Node} node @returns {string} */
|
|
59
|
+
function markdownForNode(node) {
|
|
60
|
+
if (node.nodeType === 3) return (node.textContent ?? '').replace(/\s+/g, ' ');
|
|
61
|
+
if (node.nodeType !== 1) return '';
|
|
62
|
+
const element = /** @type {Element} */ (node);
|
|
63
|
+
const tag = element.tagName.toLowerCase();
|
|
64
|
+
const children = () => Array.from(element.childNodes, markdownForNode).join('');
|
|
65
|
+
if (/^h[1-6]$/.test(tag)) return `\n${'#'.repeat(Number(tag[1]))} ${children().trim()}\n\n`;
|
|
66
|
+
if (['p', 'div', 'section', 'article', 'main'].includes(tag)) {
|
|
67
|
+
return `\n${children().trim()}\n\n`;
|
|
68
|
+
}
|
|
69
|
+
if (tag === 'br') return '\n';
|
|
70
|
+
if (tag === 'hr') return '\n---\n';
|
|
71
|
+
if (tag === 'strong' || tag === 'b') return `**${children().trim()}**`;
|
|
72
|
+
if (tag === 'em' || tag === 'i') return `*${children().trim()}*`;
|
|
73
|
+
if (tag === 'code' && element.parentElement?.tagName.toLowerCase() !== 'pre') {
|
|
74
|
+
return `\`${(element.textContent ?? '').replace(/`/g, '\\`')}\``;
|
|
75
|
+
}
|
|
76
|
+
if (tag === 'pre') return `\n\`\`\`\n${element.textContent ?? ''}\n\`\`\`\n\n`;
|
|
77
|
+
if (tag === 'blockquote') {
|
|
78
|
+
return `\n${children()
|
|
79
|
+
.trim()
|
|
80
|
+
.split('\n')
|
|
81
|
+
.map((line) => `> ${line}`)
|
|
82
|
+
.join('\n')}\n\n`;
|
|
83
|
+
}
|
|
84
|
+
if (tag === 'li') {
|
|
85
|
+
const marker =
|
|
86
|
+
element.parentElement?.tagName.toLowerCase() === 'ol'
|
|
87
|
+
? `${Array.from(element.parentElement.children).indexOf(element) + 1}.`
|
|
88
|
+
: '-';
|
|
89
|
+
return `\n${marker} ${children().trim()}`;
|
|
90
|
+
}
|
|
91
|
+
if (tag === 'ul' || tag === 'ol') return `${children()}\n\n`;
|
|
92
|
+
if (tag === 'a') {
|
|
93
|
+
const label = children().trim();
|
|
94
|
+
const href = element.getAttribute('href')?.trim();
|
|
95
|
+
return href && !/^(?:javascript|data):/i.test(href) ? `[${label}](${href})` : label;
|
|
96
|
+
}
|
|
97
|
+
return children();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @param {Element} root @param {'text' | 'markdown'} format @returns {string} */
|
|
101
|
+
function formatRoot(root, format) {
|
|
102
|
+
if (format === 'markdown') {
|
|
103
|
+
return markdownForNode(root)
|
|
104
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
105
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
106
|
+
.trim();
|
|
107
|
+
}
|
|
108
|
+
return (root.textContent ?? '')
|
|
109
|
+
.replace(/[ \t]+/g, ' ')
|
|
110
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
111
|
+
.trim();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @param {string} content @param {number} budget @param {'text' | 'markdown'} format */
|
|
115
|
+
function truncateContent(content, budget, format) {
|
|
116
|
+
if (content.length <= budget) return { content, truncated: false, omitted: 0 };
|
|
117
|
+
const boundary = Math.max(content.lastIndexOf('\n\n', budget), content.lastIndexOf(' ', budget));
|
|
118
|
+
const end = boundary >= Math.floor(budget * 0.7) ? boundary : budget;
|
|
119
|
+
let value = content.slice(0, end).trimEnd();
|
|
120
|
+
if (format === 'markdown' && (value.match(/```/g)?.length ?? 0) % 2 === 1) {
|
|
121
|
+
value = `${value.slice(0, Math.max(0, budget - 4)).trimEnd()}\n\`\`\``;
|
|
122
|
+
}
|
|
123
|
+
return { content: value, truncated: true, omitted: content.length - value.length };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Convert one bounded browser HTML snapshot into semantic content in Node.
|
|
128
|
+
* Raw HTML is consumed here and never included in the returned result.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} html
|
|
131
|
+
* @param {ExtractContentParams} params
|
|
132
|
+
* @param {{ title?: string, settlement?: ExtractContentResult['settlement'] }} [context]
|
|
133
|
+
* @returns {ExtractContentResult}
|
|
134
|
+
*/
|
|
135
|
+
export function extractContentFromHtml(html, params, context = {}) {
|
|
136
|
+
const escapedTitle = String(context.title ?? '').replace(/[&<>]/g, (character) => {
|
|
137
|
+
if (character === '&') return '&';
|
|
138
|
+
if (character === '<') return '<';
|
|
139
|
+
return '>';
|
|
140
|
+
});
|
|
141
|
+
const { document } = parseHTML(
|
|
142
|
+
`<!doctype html><html><head><title>${escapedTitle}</title></head><body>${html}</body></html>`
|
|
143
|
+
);
|
|
144
|
+
sanitize(document);
|
|
145
|
+
const semanticRoot = largestSemanticRoot(document.body);
|
|
146
|
+
const semanticLength = semanticRoot ? meaningfulLength(semanticRoot) : 0;
|
|
147
|
+
let readability = null;
|
|
148
|
+
try {
|
|
149
|
+
readability = new Readability(/** @type {Document} */ (document.cloneNode(true))).parse();
|
|
150
|
+
} catch {
|
|
151
|
+
readability = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @type {Element} */
|
|
155
|
+
let resultRoot;
|
|
156
|
+
/** @type {import('../../protocol/src/types.js').ExtractContentSource} */
|
|
157
|
+
let source;
|
|
158
|
+
if (readability && (readability.length ?? 0) >= Math.max(200, Math.floor(semanticLength * 0.2))) {
|
|
159
|
+
const template = document.createElement('template');
|
|
160
|
+
template.innerHTML = readability.content ?? '';
|
|
161
|
+
sanitize(template.content);
|
|
162
|
+
const wrapper = document.createElement('article');
|
|
163
|
+
wrapper.append(template.content.cloneNode(true));
|
|
164
|
+
resultRoot = wrapper;
|
|
165
|
+
source = 'readability';
|
|
166
|
+
} else if (semanticRoot && semanticLength > 0) {
|
|
167
|
+
resultRoot = semanticRoot;
|
|
168
|
+
source = 'semantic-root';
|
|
169
|
+
} else {
|
|
170
|
+
resultRoot = document.body;
|
|
171
|
+
source = 'body';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const formatted = formatRoot(resultRoot, params.format);
|
|
175
|
+
const bounded = truncateContent(formatted, params.textBudget, params.format);
|
|
176
|
+
const metadata = params.includeMetadata
|
|
177
|
+
? {
|
|
178
|
+
title: boundedText(readability?.title ?? context.title, METADATA_LIMITS.title),
|
|
179
|
+
byline: boundedText(readability?.byline, METADATA_LIMITS.byline),
|
|
180
|
+
excerpt: boundedText(readability?.excerpt, METADATA_LIMITS.excerpt),
|
|
181
|
+
siteName: boundedText(readability?.siteName, METADATA_LIMITS.siteName),
|
|
182
|
+
}
|
|
183
|
+
: {};
|
|
184
|
+
return {
|
|
185
|
+
format: params.format,
|
|
186
|
+
content: bounded.content,
|
|
187
|
+
...metadata,
|
|
188
|
+
source,
|
|
189
|
+
root: {
|
|
190
|
+
tag: resultRoot.tagName.toLowerCase(),
|
|
191
|
+
...(params.selector ? { selector: params.selector } : {}),
|
|
192
|
+
},
|
|
193
|
+
length: formatted.length,
|
|
194
|
+
truncated: bounded.truncated,
|
|
195
|
+
omitted: bounded.omitted,
|
|
196
|
+
...(context.settlement ? { settlement: context.settlement } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -287,7 +287,7 @@ function validateAuthToken(token) {
|
|
|
287
287
|
|
|
288
288
|
/**
|
|
289
289
|
* @param {string | null | undefined} destinationId
|
|
290
|
-
* @param {{ defaultTimeoutMs?: number }} [options={}]
|
|
290
|
+
* @param {{ defaultTimeoutMs?: number, checkProtocolOnConnect?: boolean }} [options={}]
|
|
291
291
|
* @returns {Promise<BridgeClient>}
|
|
292
292
|
*/
|
|
293
293
|
export async function createBridgeClientForDestination(destinationId, options = {}) {
|