@browserbridge/bbx 1.9.0 → 1.10.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.
@@ -0,0 +1,182 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Shared budget-preset merge helpers. Presets fill gaps in method params;
5
+ * explicit per-call params always win. Used by both the MCP server and the
6
+ * agent CLI so `budgetPreset`/`--preset` behave identically.
7
+ */
8
+
9
+ import {
10
+ DEFAULT_CONSOLE_LIMIT,
11
+ DEFAULT_HAR_LIMIT,
12
+ DEFAULT_LOG_TAIL_LIMIT,
13
+ DEFAULT_MAX_HTML_LENGTH,
14
+ DEFAULT_NETWORK_LIMIT,
15
+ DEFAULT_PAGE_TEXT_BUDGET,
16
+ getBudgetPreset,
17
+ isBudgetPresetName,
18
+ } from './defaults.js';
19
+
20
+ /**
21
+ * @param {unknown} value
22
+ * @returns {import('./defaults.js').BudgetPresetName | null}
23
+ */
24
+ export function getBudgetPresetName(value) {
25
+ return isBudgetPresetName(value) ? value : null;
26
+ }
27
+
28
+ /**
29
+ * @template {{ budgetPreset?: unknown, maxNodes?: unknown, maxDepth?: unknown, textBudget?: unknown }} T
30
+ * @param {T} args
31
+ * @returns {T}
32
+ */
33
+ export function applyTreeBudgetPreset(args) {
34
+ const presetName = getBudgetPresetName(args.budgetPreset);
35
+ if (!presetName) {
36
+ return args;
37
+ }
38
+ const preset = getBudgetPreset(presetName);
39
+ return /** @type {T} */ ({
40
+ ...args,
41
+ maxNodes: args.maxNodes ?? preset.maxNodes,
42
+ maxDepth: args.maxDepth ?? preset.maxDepth,
43
+ textBudget: args.textBudget ?? preset.textBudget,
44
+ });
45
+ }
46
+
47
+ /**
48
+ * @template {{ budgetPreset?: unknown, textBudget?: unknown }} T
49
+ * @param {T} args
50
+ * @returns {T}
51
+ */
52
+ export function applyTextBudgetPreset(args) {
53
+ const presetName = getBudgetPresetName(args.budgetPreset);
54
+ if (!presetName) {
55
+ return args;
56
+ }
57
+ const preset = getBudgetPreset(presetName);
58
+ return /** @type {T} */ ({
59
+ ...args,
60
+ textBudget: args.textBudget ?? preset.textBudget,
61
+ });
62
+ }
63
+
64
+ /**
65
+ * @template {{ budgetPreset?: unknown, textBudget?: unknown }} T
66
+ * @param {T} args
67
+ * @returns {T}
68
+ */
69
+ export function applyPageTextBudgetPreset(args) {
70
+ const presetName = getBudgetPresetName(args.budgetPreset);
71
+ if (!presetName) {
72
+ return args;
73
+ }
74
+ const textBudgetByPreset = {
75
+ quick: 2000,
76
+ normal: DEFAULT_PAGE_TEXT_BUDGET,
77
+ deep: DEFAULT_PAGE_TEXT_BUDGET * 2,
78
+ };
79
+ return /** @type {T} */ ({
80
+ ...args,
81
+ textBudget: args.textBudget ?? textBudgetByPreset[presetName],
82
+ });
83
+ }
84
+
85
+ /**
86
+ * @template {{ budgetPreset?: unknown, limit?: unknown }} T
87
+ * @param {T} args
88
+ * @param {{ quick: number, normal: number, deep: number }} defaults
89
+ * @returns {T}
90
+ */
91
+ export function applyLimitBudgetPreset(args, defaults) {
92
+ const presetName = getBudgetPresetName(args.budgetPreset);
93
+ if (!presetName) {
94
+ return args;
95
+ }
96
+ return /** @type {T} */ ({
97
+ ...args,
98
+ limit: args.limit ?? defaults[presetName],
99
+ });
100
+ }
101
+
102
+ /**
103
+ * @template {{ budgetPreset?: unknown, maxLength?: unknown }} T
104
+ * @param {T} args
105
+ * @returns {T}
106
+ */
107
+ export function applyHtmlBudgetPreset(args) {
108
+ const presetName = getBudgetPresetName(args.budgetPreset);
109
+ if (!presetName) {
110
+ return args;
111
+ }
112
+ const maxLengthByPreset = {
113
+ quick: 600,
114
+ normal: DEFAULT_MAX_HTML_LENGTH,
115
+ deep: 6000,
116
+ };
117
+ return /** @type {T} */ ({
118
+ ...args,
119
+ maxLength: args.maxLength ?? maxLengthByPreset[presetName],
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Apply a preset to method parameters before dispatch. Explicit method params
125
+ * always win over preset defaults.
126
+ *
127
+ * @param {import('./types.js').BridgeMethod} method
128
+ * @param {Record<string, unknown>} params
129
+ * @param {unknown} budgetPreset
130
+ * @returns {Record<string, unknown>}
131
+ */
132
+ export function applyMethodBudgetPreset(method, params, budgetPreset) {
133
+ const args = { ...params, budgetPreset };
134
+ /** @type {Record<string, unknown>} */
135
+ let normalized = args;
136
+ if (
137
+ method === 'dom.query' ||
138
+ method === 'dom.get_accessibility_tree' ||
139
+ method === 'dom.baseline.create'
140
+ ) {
141
+ normalized = applyTreeBudgetPreset(args);
142
+ } else if (method === 'dom.baseline.compare') {
143
+ const preset = getBudgetPresetName(budgetPreset);
144
+ const defaults = { quick: 10, normal: 50, deep: 100 };
145
+ normalized = {
146
+ ...args,
147
+ maxChanges: params.maxChanges ?? (preset ? defaults[preset] : undefined),
148
+ };
149
+ } else if (method === 'dom.get_text') {
150
+ normalized = applyTextBudgetPreset(args);
151
+ } else if (method === 'dom.get_html') {
152
+ normalized = applyHtmlBudgetPreset(args);
153
+ } else if (method === 'page.get_text' || method === 'page.extract_content') {
154
+ normalized = applyPageTextBudgetPreset(args);
155
+ } else if (method === 'page.get_console') {
156
+ normalized = applyLimitBudgetPreset(args, {
157
+ quick: 10,
158
+ normal: DEFAULT_CONSOLE_LIMIT,
159
+ deep: 100,
160
+ });
161
+ } else if (method === 'page.get_network') {
162
+ normalized = applyLimitBudgetPreset(args, {
163
+ quick: 10,
164
+ normal: DEFAULT_NETWORK_LIMIT,
165
+ deep: 100,
166
+ });
167
+ } else if (method === 'network.export_har') {
168
+ normalized = applyLimitBudgetPreset(args, {
169
+ quick: 20,
170
+ normal: DEFAULT_HAR_LIMIT,
171
+ deep: 100,
172
+ });
173
+ } else if (method === 'log.tail') {
174
+ normalized = applyLimitBudgetPreset(args, {
175
+ quick: 10,
176
+ normal: DEFAULT_LOG_TAIL_LIMIT,
177
+ deep: 100,
178
+ });
179
+ }
180
+ const { budgetPreset: _budgetPreset, ...methodParams } = normalized;
181
+ return methodParams;
182
+ }
@@ -0,0 +1,34 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Minimal structural validation for HAR 1.2 documents shared by the CLI and
5
+ * the MCP server. Only the envelope is validated (`log.version` and the
6
+ * `entries` array); deeper field checks are left to HAR consumers.
7
+ */
8
+
9
+ /**
10
+ * @param {unknown} value
11
+ * @returns {value is import('./types.js').HarLog}
12
+ */
13
+ export function isHarLog(value) {
14
+ return Boolean(
15
+ value &&
16
+ typeof value === 'object' &&
17
+ !Array.isArray(value) &&
18
+ Reflect.get(value, 'version') === '1.2' &&
19
+ Array.isArray(Reflect.get(value, 'entries'))
20
+ );
21
+ }
22
+
23
+ /**
24
+ * @param {unknown} value
25
+ * @returns {value is import('./types.js').HarDocument}
26
+ */
27
+ export function isHarDocument(value) {
28
+ return Boolean(
29
+ value &&
30
+ typeof value === 'object' &&
31
+ !Array.isArray(value) &&
32
+ isHarLog(Reflect.get(value, 'log'))
33
+ );
34
+ }
@@ -4,10 +4,12 @@
4
4
  * Public protocol barrel for shared bridge helpers, errors, capabilities, and
5
5
  * request/response normalization utilities.
6
6
  */
7
+ export * from './budget-presets.js';
7
8
  export * from './budget.js';
8
9
  export * from './capabilities.js';
9
10
  export * from './defaults.js';
10
11
  export * from './errors.js';
12
+ export * from './har.js';
11
13
  export * from './incidental-sanitizer.js';
12
14
  export * from './json-lines.js';
13
15
  export * from './payload-cost.js';
@@ -7,7 +7,7 @@ import { MAX_JSON_LINE_BYTES } from './defaults.js';
7
7
  *
8
8
  * @param {import('node:net').Socket} socket
9
9
  * @param {(message: unknown) => void} onMessage
10
- * @param {{ maxLineBytes?: number, onProtocolError?: (error: Error) => void }} [options]
10
+ * @param {{ maxLineBytes?: number, onProtocolError?: (error: Error) => void, onInvalidLine?: (error: Error) => void }} [options]
11
11
  * @returns {void}
12
12
  */
13
13
  export function parseJsonLines(socket, onMessage, options = {}) {
@@ -50,8 +50,9 @@ export function parseJsonLines(socket, onMessage, options = {}) {
50
50
  }
51
51
  try {
52
52
  onMessage(JSON.parse(line));
53
- } catch {
53
+ } catch (error) {
54
54
  // Malformed JSON line - skip it.
55
+ options.onInvalidLine?.(error instanceof Error ? error : new Error(String(error)));
55
56
  }
56
57
  }
57
58
  });
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import { CAPABILITIES } from './capability-values.js';
4
+ import { BridgeError, ERROR_CODES } from './errors.js';
4
5
 
5
6
  /**
6
7
  * @typedef {'trivial' | 'low' | 'moderate' | 'high'} BridgeMethodComplexity
@@ -97,6 +98,8 @@ const METHOD_POLICIES = Object.freeze({
97
98
  const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
98
99
  'access.request':
99
100
  'Request Browser Bridge access for the focused window. Do not repeat while access is already pending.',
101
+ 'protocol.describe':
102
+ 'Describe bridge method parameters and metadata by method or registry group.',
100
103
  'tabs.list': 'List tabs in the enabled window.',
101
104
  'tabs.create': 'Create a new tab in the enabled window.',
102
105
  'tabs.close': 'Close a tab in the enabled window.',
@@ -222,6 +225,13 @@ function createRegistryEntry(
222
225
  export const BRIDGE_METHOD_REGISTRY = Object.freeze({
223
226
  // system - trivial
224
227
  'access.request': createRegistryEntry('access.request', 'system', false, ['intent'], 'trivial'),
228
+ 'protocol.describe': createRegistryEntry(
229
+ 'protocol.describe',
230
+ 'system',
231
+ false,
232
+ ['method', 'group'],
233
+ 'trivial'
234
+ ),
225
235
  'skill.get_runtime_context': createRegistryEntry(
226
236
  'skill.get_runtime_context',
227
237
  'system',
@@ -888,6 +898,79 @@ export function createBridgeMethodGroups() {
888
898
  return groups;
889
899
  }
890
900
 
901
+ /**
902
+ * @typedef {{
903
+ * method: import('./types.js').BridgeMethod,
904
+ * group: string,
905
+ * tab: boolean,
906
+ * params: readonly string[],
907
+ * description: string,
908
+ * complexity: BridgeMethodComplexity
909
+ * }} BridgeMethodDescription
910
+ */
911
+
912
+ /**
913
+ * @param {import('./types.js').BridgeMethod} method
914
+ * @returns {BridgeMethodDescription}
915
+ */
916
+ function createBridgeMethodDescription(method) {
917
+ const entry = BRIDGE_METHOD_REGISTRY[method];
918
+ return {
919
+ method,
920
+ group: entry.group,
921
+ tab: entry.tab,
922
+ params: entry.params,
923
+ description: entry.description,
924
+ complexity: entry.complexity,
925
+ };
926
+ }
927
+
928
+ /**
929
+ * Return a compact registry index or detailed descriptions for one method or group.
930
+ *
931
+ * @param {{ method?: unknown, group?: unknown }} [options]
932
+ * @returns {{ groups: Array<{ group: string, count: number }> } | BridgeMethodDescription | { group: string, methods: BridgeMethodDescription[] }}
933
+ */
934
+ export function describeBridgeMethods(options = {}) {
935
+ const { method, group } = options;
936
+ if (method !== undefined && group !== undefined) {
937
+ throw new BridgeError(
938
+ ERROR_CODES.INVALID_REQUEST,
939
+ 'protocol.describe accepts either method or group, not both.'
940
+ );
941
+ }
942
+ if (method !== undefined) {
943
+ if (typeof method !== 'string' || !isBridgeMethod(method)) {
944
+ throw new BridgeError(
945
+ ERROR_CODES.INVALID_REQUEST,
946
+ `Unknown bridge method "${String(method)}".`
947
+ );
948
+ }
949
+ return createBridgeMethodDescription(method);
950
+ }
951
+
952
+ const groups = createBridgeMethodGroups();
953
+ if (group !== undefined) {
954
+ if (typeof group !== 'string' || !Object.hasOwn(groups, group)) {
955
+ throw new BridgeError(
956
+ ERROR_CODES.INVALID_REQUEST,
957
+ `Unknown bridge method group "${String(group)}".`
958
+ );
959
+ }
960
+ return {
961
+ group,
962
+ methods: groups[group].map(createBridgeMethodDescription),
963
+ };
964
+ }
965
+
966
+ return {
967
+ groups: Object.entries(groups).map(([groupName, methods]) => ({
968
+ group: groupName,
969
+ count: methods.length,
970
+ })),
971
+ };
972
+ }
973
+
891
974
  /**
892
975
  * @param {string} method
893
976
  * @returns {method is import('./types.js').BridgeMethod}
@@ -63,6 +63,7 @@ export type ErrorCode =
63
63
 
64
64
  export type BridgeMethod =
65
65
  | 'access.request'
66
+ | 'protocol.describe'
66
67
  | 'tabs.list'
67
68
  | 'tabs.create'
68
69
  | 'tabs.close'
@@ -14,6 +14,8 @@ Permission prompts are controlled by the host agent, not by Browser Bridge. In p
14
14
  Skill name: `browser-bridge` (also known as `bbx`). In GitHub Copilot, invoke as `/browser-bridge`. `bbx` is the CLI command used throughout this skill.
15
15
  When the runtime supports subagents, delegate bridge inspection to a smaller, lower-cost worker and return only concise findings to the parent.
16
16
  For open-ended investigation, start with structured reads (`page.get_state`, `dom.query`, `page.get_text`, `page.extract_content`, `styles.get_computed`, and `bbx batch` for CLI or `browser_batch` for MCP) and escalate to screenshots or debugger-backed methods only when structured evidence is insufficient. `browser_call` accepts one protocol method at a time; `batch` is not a valid `browser_call` method.
17
+ When an unfamiliar method is needed, run `bbx protocol describe <method|group>` before guessing parameters. In MCP mode, call `browser_call` with method `protocol.describe` and params `{ "method": "dom.query" }` or `{ "group": "inspect" }`.
18
+ MCP exposes common tools first. Load one specialized typed tool only when needed by calling `browser_toolset` with its exact name, for example `{ "tool": "browser_dom" }`; `browser_call` remains the universal fallback.
17
19
 
18
20
  ## CLI
19
21
 
@@ -32,6 +34,7 @@ bbx tab-create [url] # open a new tab (avoid unless necessary)
32
34
  bbx tab-close <tabId> # close a tab
33
35
  bbx tab-activate <tabId> # bring a tab to the foreground
34
36
  bbx skill # live runtime presets + limits
37
+ bbx protocol describe [method|group] # local method params or compact group index
35
38
  ```
36
39
 
37
40
  Use the globally installed `bbx` command or `node packages/agent-client/src/cli.js ...` for live readiness checks from this repository. Avoid `rtk npm exec -- bbx status` as a health signal: that path can restart/replace the daemon and report `Extension: disconnected` before Chrome reconnects. If it happens, wait a few seconds and verify with `bbx status`.
@@ -351,6 +354,7 @@ Every CLI shortcut command produces consistent `{ok, summary, evidence}` JSON. U
351
354
  - `network.export_har` requires that explicit capture to still be armed. Params are `limit` (1-200), sanitized-URL substring `urlPattern`, and `delivery` (`auto`, `inline`, `artifact`). Inline bounds remove whole oldest entries; export never changes capture state. `bbx har` uses the same client to download, length/SHA-256 verify, delete, validate, and atomically write artifacts locally.
352
355
  - `page.handle_dialog` mutations are not atomically bound to `expectedDialogId`; treat `commandDispatched` as dispatch evidence and verify follow-up state.
353
356
  - Raw screenshot calls default to inline base64. Use `delivery: "auto"` for size-aware artifact fallback, or explicit `inline` when MCP image content is required; prefer `bbx screenshot <ref> [outPath]` when one element is enough because the CLI downloads and verifies artifacts locally.
357
+ - Artifact delivery returns a descriptor (artifactId, sha256, byteLength), not bytes. From MCP, read chunks with the `browser_artifact` tool (`action: "read"`, paging `offset` until `nextOffset` is null, then verify the reassembled bytes against `sha256`) and free storage with `action: "delete"`. Each standalone CLI invocation has a new artifact owner, so use `bbx screenshot` / `bbx har` to capture, download, verify, and delete within one CLI process; use `delivery: "inline"` for raw CLI screenshot calls that need the bytes in their immediate result.
354
358
 
355
359
  ## Response Shapes
356
360
 
@@ -27,11 +27,16 @@ The table below includes the legacy capability bucket for each method so agents
27
27
  - `-` means the method is global/system-scoped and was never gated by a former capability bucket.
28
28
  - Capability names are descriptive coverage labels only. Browser Bridge access is window-scoped now; there are no capability-scoped sessions.
29
29
 
30
- ## All Methods (75)
30
+ Use `bbx protocol describe <method|group>` for a compact local signature lookup. In MCP mode,
31
+ call `browser_call` with method `protocol.describe` and params containing `method` or `group`;
32
+ omit both for the group index.
33
+
34
+ ## All Methods (76)
31
35
 
32
36
  | Method | Tab? | CDP? | Group | Capability | Notes |
33
37
  | ---------------------------------- | ---- | ---------- | ----------- | -------------------- | ------------------------------------------------------------------------------------------ |
34
38
  | `access.request` | No | - | system | `intent?` | Request window access with bounded operation context in the extension UI |
39
+ | `protocol.describe` | No | - | system | `-` | Local compact method/group parameters and metadata |
35
40
  | `tabs.list` | No | - | tabs | `-` | Discover tabs in the enabled window |
36
41
  | `tabs.create` | No | - | tabs | `tabs.manage` | Open a new tab; optional `url` and `active` |
37
42
  | `tabs.close` | No | - | tabs | `tabs.manage` | Close a tab by `tabId` |
@@ -115,6 +120,7 @@ stopped.
115
120
 
116
121
  ```bash
117
122
  bbx status | doctor | restart | logs | tabs | skill # no routed tab needed
123
+ bbx protocol describe [method|group] # local signature or compact group index
118
124
  bbx call <method> '{"key":"val"}' # generic RPC (routes to active tab in enabled window)
119
125
  bbx call --tab 123 <method> '{...}' # explicit tab target inside enabled window
120
126
  bbx batch '[{"method":"...","params":{}}]' # parallel calls
@@ -437,7 +443,7 @@ artifact, and atomically write the output on the CLI host.
437
443
 
438
444
  ### dom.get_accessibility_tree
439
445
 
440
- Retrieve a depth-limited accessibility tree via CDP. Full reads use `Accessibility.getFullAXTree`; an optional unique `selector` resolves through CDP DOM methods and uses `Accessibility.getPartialAXTree`, retaining only the selected subtree and required ancestor chain. Missing and ambiguous selectors return typed errors instead of selecting an arbitrary candidate. Nodes include `role`, `name`, `description`, `value`, state fields, `interactive`, `semanticInteractive`, `focusable`, `focusableAndEnabled`, `ignored`, and `childIds`. `interactive` is semantic/focusability metadata, not current pointer actionability.
446
+ Retrieve a depth-limited accessibility tree via CDP. Full reads use `Accessibility.getFullAXTree`; an optional unique `selector` resolves through CDP DOM methods, starts with `Accessibility.getPartialAXTree`, and incrementally fetches selected descendant layers with `Accessibility.getChildAXNodes`. Only the selected subtree and required ancestor chain are retained. Missing and ambiguous selectors return typed errors instead of selecting an arbitrary candidate. Nodes include `role`, `name`, `description`, `value`, state fields, `interactive`, `semanticInteractive`, `focusable`, `focusableAndEnabled`, `ignored`, and `childIds`. `interactive` is semantic/focusability metadata, not current pointer actionability.
441
447
 
442
448
  This is debugger-backed. Prefer `dom.find_by_role`, `dom.find_by_text`, and targeted `dom.query`/`dom.describe` first.
443
449
 
@@ -449,7 +455,7 @@ bbx call dom.get_accessibility_tree '{"maxNodes":100,"maxDepth":6,"interactiveOn
449
455
  bbx call dom.get_accessibility_tree '{"selector":"[role=dialog]","maxNodes":50,"compact":true}'
450
456
  ```
451
457
 
452
- Filtering occurs before `maxNodes`. Compact mode drops ignored, decorative, and empty nodes while reconnecting retained descendants; interactive-only keeps non-ignored semantic interactive roles. Because CDP applies `maxDepth` before Browser Bridge receives the tree, every result truthfully reports `truncated: true`, `partialTopology: true`, depth metadata, missing-child counts, and a continuation hint. No AX node is written into the page DOM or converted directly into an actionable element ref; use role/name with `dom.find_by_role` before input.
458
+ Filtering occurs before `maxNodes`. Compact mode drops ignored, decorative, and empty nodes while reconnecting retained descendants; interactive-only keeps non-ignored semantic interactive roles. Full reads ask CDP for the requested depth, while selector-scoped reads fetch descendant layers up to that same bound. Every result reports `truncated: true`, `partialTopology: true`, depth metadata, missing-child counts, and a continuation hint. No AX node is written into the page DOM or converted directly into an actionable element ref; use role/name with `dom.find_by_role` before input.
453
459
 
454
460
  ### viewport.resize
455
461
 
@@ -482,7 +488,7 @@ Capture a complete element, a tight viewport region, or a full document. All met
482
488
 
483
489
  Element capture uses full page-coordinate bounds with CDP capture beyond the viewport. If Browser Bridge cannot guarantee the entire element, or if an element/page exceeds Chrome capture limits, it returns a typed failure rather than a viewport-clipped image labeled complete. Use full-page capture only when element or tight region captures cannot express the issue.
484
490
 
485
- Raw calls default to inline base64 for compatibility. `auto` keeps small captures inline and returns a short-lived, daemon-owned opaque artifact handle for large captures; explicit `inline` preserves MCP image content when model vision is needed. Artifact handles expose no browser-host path and support owner-scoped bounded `artifact.read` and `artifact.delete` calls. Prefer `bbx screenshot [--format png|jpeg|webp] [--quality 0-100] <ref> [outPath]` when one element is enough; the CLI downloads, verifies, deletes, and atomically writes the artifact on the CLI host.
491
+ Raw calls default to inline base64 for compatibility. `auto` keeps small captures inline and returns a short-lived, daemon-owned opaque artifact handle for large captures; explicit `inline` preserves MCP image content when model vision is needed. Artifact handles expose no browser-host path and support owner-scoped bounded `artifact.read` and `artifact.delete` calls. Each standalone CLI invocation uses a new artifact owner, so raw CLI screenshot calls should keep inline delivery. Prefer `bbx screenshot [--format png|jpeg|webp] [--quality 0-100] <ref> [outPath]` when one element is enough; the CLI downloads, verifies, deletes, and atomically writes the artifact within one process.
486
492
 
487
493
  Artifact handles expire after five minutes and can be read or deleted only by
488
494
  the requesting client. The daemon creates a private artifact directory and
@@ -490,7 +496,7 @@ owner-only files on POSIX hosts, removes artifacts on expiry/teardown, and never
490
496
  returns its browser-host filesystem path.
491
497
 
492
498
  ```bash
493
- bbx call screenshot.capture_full_page '{"format":"jpeg","quality":75,"delivery":"auto","scale":0.75}'
499
+ bbx call screenshot.capture_full_page '{"format":"jpeg","quality":75,"delivery":"inline","scale":0.75}'
494
500
  ```
495
501
 
496
502
  ## Recovery Telemetry