@browserbridge/bbx 1.8.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.
- package/PRIVACY.md +29 -3
- package/README.md +10 -3
- package/SECURITY.md +12 -0
- package/package.json +7 -1
- package/packages/agent-client/src/cli-args.js +70 -6
- package/packages/agent-client/src/cli-batch.js +127 -59
- package/packages/agent-client/src/cli-helpers.js +8 -0
- package/packages/agent-client/src/cli-output.js +8 -1
- package/packages/agent-client/src/cli-setup-commands.js +3 -1
- package/packages/agent-client/src/cli.js +254 -26
- package/packages/agent-client/src/client.js +156 -3
- package/packages/agent-client/src/command-registry.js +14 -4
- package/packages/agent-client/src/config.js +195 -0
- package/packages/agent-client/src/mcp-config.js +8 -8
- package/packages/agent-client/src/npm-self-update.js +305 -0
- package/packages/agent-client/src/remotes.js +4 -2
- package/packages/agent-client/src/runtime.js +99 -7
- package/packages/agent-client/src/types.ts +38 -0
- package/packages/mcp-server/src/guidance.js +37 -6
- package/packages/mcp-server/src/handlers-capture.js +29 -2
- package/packages/mcp-server/src/handlers-dom.js +54 -3
- package/packages/mcp-server/src/handlers-page.js +333 -60
- package/packages/mcp-server/src/handlers-utils.js +141 -151
- package/packages/mcp-server/src/handlers.js +17 -4
- package/packages/mcp-server/src/server.js +269 -51
- package/packages/mcp-server/src/toolset.js +48 -0
- package/packages/native-host/src/artifact-store.js +353 -0
- package/packages/native-host/src/config.js +9 -1
- package/packages/native-host/src/daemon-logger.js +3 -1
- package/packages/native-host/src/daemon.js +457 -27
- package/packages/native-host/src/dom-baseline-owners.js +210 -0
- package/packages/native-host/src/native-host.js +38 -29
- package/packages/protocol/src/budget-presets.js +182 -0
- package/packages/protocol/src/capability-values.js +2 -0
- package/packages/protocol/src/defaults.js +28 -0
- package/packages/protocol/src/errors.js +48 -5
- package/packages/protocol/src/har.js +34 -0
- package/packages/protocol/src/incidental-sanitizer.js +197 -0
- package/packages/protocol/src/index.js +4 -0
- package/packages/protocol/src/json-lines.js +3 -2
- package/packages/protocol/src/protocol.js +316 -6
- package/packages/protocol/src/recovery-telemetry.js +302 -0
- package/packages/protocol/src/registry.js +226 -6
- package/packages/protocol/src/summary.js +116 -25
- package/packages/protocol/src/types.ts +490 -4
- package/skills/browser-bridge/SKILL.md +26 -8
- package/skills/browser-bridge/references/protocol.md +125 -15
- package/skills/browser-bridge/references/token-efficiency.md +17 -7
|
@@ -28,9 +28,14 @@ import {
|
|
|
28
28
|
getBridgeOperationTimeoutMs,
|
|
29
29
|
getProtocolVersion,
|
|
30
30
|
MAX_CLIENT_REQUEST_TIMEOUT_MS,
|
|
31
|
+
normalizeRecoveryTelemetrySummary,
|
|
32
|
+
RECOVERY_EVENT_KINDS,
|
|
33
|
+
sanitizeIncidentalPath,
|
|
34
|
+
sanitizeIncidentalText,
|
|
31
35
|
} from '../../protocol/src/index.js';
|
|
32
|
-
import { methodNeedsTab } from './cli-helpers.js';
|
|
36
|
+
import { isElementRef, methodNeedsTab } from './cli-helpers.js';
|
|
33
37
|
import { BridgeClient } from './client.js';
|
|
38
|
+
import { applyConfiguredAutoUpdate } from './config.js';
|
|
34
39
|
import { readRemoteConfig } from './remotes.js';
|
|
35
40
|
import { collectSetupStatus } from './setup-status.js';
|
|
36
41
|
|
|
@@ -101,7 +106,7 @@ export async function ensureClientConnected(client) {
|
|
|
101
106
|
* @param {BridgeClient} client
|
|
102
107
|
* @param {BridgeMethod} method
|
|
103
108
|
* @param {Record<string, unknown>} [params={}]
|
|
104
|
-
* @param {{ tabId?: number | null, source?: BridgeRequestSource, tokenBudget?: number | null }} [options]
|
|
109
|
+
* @param {{ tabId?: number | null, source?: BridgeRequestSource, tokenBudget?: number | null, automaticRetry?: 'mcp_second_attempt' }} [options]
|
|
105
110
|
* @returns {Promise<BridgeResponse>}
|
|
106
111
|
*/
|
|
107
112
|
export async function requestBridge(client, method, params = {}, options = {}) {
|
|
@@ -122,7 +127,7 @@ export async function requestBridge(client, method, params = {}, options = {}) {
|
|
|
122
127
|
method,
|
|
123
128
|
params,
|
|
124
129
|
tabId: methodNeedsTab(method) ? (options.tabId ?? null) : null,
|
|
125
|
-
meta: withRequestMeta(options.source, options.tokenBudget),
|
|
130
|
+
meta: withRequestMeta(options.source, options.tokenBudget, options.automaticRetry),
|
|
126
131
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
127
132
|
});
|
|
128
133
|
}
|
|
@@ -135,7 +140,7 @@ export async function requestBridge(client, method, params = {}, options = {}) {
|
|
|
135
140
|
* @returns {Promise<string>}
|
|
136
141
|
*/
|
|
137
142
|
export async function resolveRef(client, refOrSelector, tabId = null, source) {
|
|
138
|
-
if (refOrSelector
|
|
143
|
+
if (isElementRef(refOrSelector)) {
|
|
139
144
|
return refOrSelector;
|
|
140
145
|
}
|
|
141
146
|
|
|
@@ -164,9 +169,10 @@ export async function resolveRef(client, refOrSelector, tabId = null, source) {
|
|
|
164
169
|
/**
|
|
165
170
|
* @param {BridgeRequestSource | undefined} source
|
|
166
171
|
* @param {number | null | undefined} tokenBudget
|
|
172
|
+
* @param {'mcp_second_attempt' | undefined} automaticRetry
|
|
167
173
|
* @returns {BridgeMeta}
|
|
168
174
|
*/
|
|
169
|
-
function withRequestMeta(source, tokenBudget) {
|
|
175
|
+
function withRequestMeta(source, tokenBudget, automaticRetry) {
|
|
170
176
|
/** @type {BridgeMeta} */
|
|
171
177
|
const meta = {};
|
|
172
178
|
if (source) {
|
|
@@ -175,6 +181,9 @@ function withRequestMeta(source, tokenBudget) {
|
|
|
175
181
|
if (typeof tokenBudget === 'number' && Number.isFinite(tokenBudget)) {
|
|
176
182
|
meta.token_budget = tokenBudget;
|
|
177
183
|
}
|
|
184
|
+
if (automaticRetry === 'mcp_second_attempt') {
|
|
185
|
+
meta.automatic_retry = { attempt: 2, reason: 'retryable_error' };
|
|
186
|
+
}
|
|
178
187
|
return meta;
|
|
179
188
|
}
|
|
180
189
|
|
|
@@ -185,7 +194,7 @@ function withRequestMeta(source, tokenBudget) {
|
|
|
185
194
|
* @returns {Promise<T>}
|
|
186
195
|
*/
|
|
187
196
|
export async function withBridgeClient(callback, options) {
|
|
188
|
-
const client = new BridgeClient(options);
|
|
197
|
+
const client = new BridgeClient(await applyConfiguredAutoUpdate(options));
|
|
189
198
|
await ensureClientConnected(client);
|
|
190
199
|
try {
|
|
191
200
|
return await callback(client);
|
|
@@ -661,6 +670,30 @@ function summarizeMetrics(value) {
|
|
|
661
670
|
requestsProcessed: boundedCount(metrics.requestsProcessed),
|
|
662
671
|
requestsFailed: boundedCount(metrics.requestsFailed),
|
|
663
672
|
avgResponseTimeMs: boundedCount(metrics.avgResponseTimeMs),
|
|
673
|
+
recovery: normalizeRecoveryTelemetrySummary(metrics.recovery, 'daemon'),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* @param {Record<string, unknown> | null} health
|
|
679
|
+
* @param {import('./types.js').DoctorDaemonMetrics | null} metrics
|
|
680
|
+
* @returns {import('./types.js').DoctorRecoveryDiagnostics}
|
|
681
|
+
*/
|
|
682
|
+
function summarizeRecovery(health, metrics) {
|
|
683
|
+
const recovery = asRecord(health?.recovery);
|
|
684
|
+
const daemon = metrics?.recovery ?? normalizeRecoveryTelemetrySummary(recovery?.daemon, 'daemon');
|
|
685
|
+
const routedExtension = normalizeRecoveryTelemetrySummary(
|
|
686
|
+
recovery?.routedExtension,
|
|
687
|
+
'routedExtension'
|
|
688
|
+
);
|
|
689
|
+
return {
|
|
690
|
+
daemon,
|
|
691
|
+
routedExtension,
|
|
692
|
+
activeLoops: RECOVERY_EVENT_KINDS.filter(
|
|
693
|
+
(kind) =>
|
|
694
|
+
daemon?.events[kind].activeLoop === true ||
|
|
695
|
+
routedExtension?.events[kind].activeLoop === true
|
|
696
|
+
),
|
|
664
697
|
};
|
|
665
698
|
}
|
|
666
699
|
|
|
@@ -947,6 +980,7 @@ export async function getDoctorReport(options = {}) {
|
|
|
947
980
|
},
|
|
948
981
|
protocol: summarizeProtocol(null, false),
|
|
949
982
|
debugger: summarizeDebugger(null),
|
|
983
|
+
recovery: summarizeRecovery(null, null),
|
|
950
984
|
metrics: null,
|
|
951
985
|
recentEvents: [],
|
|
952
986
|
recentCauses: [],
|
|
@@ -1105,6 +1139,10 @@ export async function getDoctorReport(options = {}) {
|
|
|
1105
1139
|
),
|
|
1106
1140
|
];
|
|
1107
1141
|
report.debugger = summarizeDebugger(report.healthAvailable ? resolvedHealth : null);
|
|
1142
|
+
report.recovery = summarizeRecovery(
|
|
1143
|
+
report.healthAvailable ? resolvedHealth : null,
|
|
1144
|
+
report.metrics
|
|
1145
|
+
);
|
|
1108
1146
|
if (report.debugger.recentReason && !report.recentCauses.includes(report.debugger.recentReason)) {
|
|
1109
1147
|
report.recentCauses.push(report.debugger.recentReason);
|
|
1110
1148
|
}
|
|
@@ -1243,5 +1281,59 @@ export async function getDoctorReport(options = {}) {
|
|
|
1243
1281
|
);
|
|
1244
1282
|
}
|
|
1245
1283
|
|
|
1246
|
-
|
|
1284
|
+
for (const loop of report.recovery.activeLoops) {
|
|
1285
|
+
const issue = `${loop}_loop`;
|
|
1286
|
+
if (!report.issues.includes(issue)) report.issues.push(issue);
|
|
1287
|
+
if (loop === 'stale_ref_recovery') {
|
|
1288
|
+
report.nextSteps.push(
|
|
1289
|
+
'Stale-reference recovery is repeatedly failing. Re-query the target for a fresh elementRef and retry once without reusing old refs.'
|
|
1290
|
+
);
|
|
1291
|
+
} else if (
|
|
1292
|
+
loop === 'debugger_reattach' &&
|
|
1293
|
+
report.debugger.state !== 'conflict' &&
|
|
1294
|
+
!report.recentCauses.includes('debugger_conflict')
|
|
1295
|
+
) {
|
|
1296
|
+
report.nextSteps.push(
|
|
1297
|
+
'Debugger reattachment is repeatedly failing. Keep the tab open, close competing debuggers, and retry one debugger-backed operation.'
|
|
1298
|
+
);
|
|
1299
|
+
} else if (loop === 'content_script_reinjection') {
|
|
1300
|
+
report.nextSteps.push(
|
|
1301
|
+
'Content-script reinjection is repeatedly failing. Reload the Browser Bridge extension and target page, then enable the window again.'
|
|
1302
|
+
);
|
|
1303
|
+
} else if (loop === 'native_host_reconnect' && !authenticationFailed) {
|
|
1304
|
+
report.nextSteps.push(
|
|
1305
|
+
'Native-host reconnects are repeatedly failing. Run `bbx restart`; for remote TCP proxy use, verify the endpoint and rotate stale credentials with `bbx proxy enable --rotate-token`.'
|
|
1306
|
+
);
|
|
1307
|
+
} else if (loop === 'automatic_mcp_retry') {
|
|
1308
|
+
report.nextSteps.push(
|
|
1309
|
+
'Automatic MCP retries are repeatedly failing. Stop the retry loop, resolve the reported bridge readiness issue, then retry the original read once.'
|
|
1310
|
+
);
|
|
1311
|
+
} else if (loop === 'request_outcome') {
|
|
1312
|
+
report.nextSteps.push(
|
|
1313
|
+
'Browser requests are repeatedly failing. Inspect the first typed bridge error, narrow the request or target as directed, and retry once after correcting that specific failure.'
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
return {
|
|
1319
|
+
...report,
|
|
1320
|
+
manifestPath: sanitizeDoctorPath(report.manifestPath),
|
|
1321
|
+
daemonLogPath: sanitizeDoctorPath(report.daemonLogPath),
|
|
1322
|
+
unwritableBridgePaths: report.unwritableBridgePaths.map(sanitizeDoctorPath),
|
|
1323
|
+
nativeHostManifestIssues: report.nativeHostManifestIssues.map((issue) => ({
|
|
1324
|
+
...issue,
|
|
1325
|
+
manifestPath: sanitizeDoctorPath(issue.manifestPath),
|
|
1326
|
+
message: sanitizeIncidentalText(issue.message),
|
|
1327
|
+
})),
|
|
1328
|
+
browserManifests: report.browserManifests.map((browser) => ({
|
|
1329
|
+
...browser,
|
|
1330
|
+
manifestPath: sanitizeDoctorPath(browser.manifestPath),
|
|
1331
|
+
})),
|
|
1332
|
+
nextSteps: report.nextSteps.map((step) => sanitizeIncidentalText(step)),
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/** @param {string} value */
|
|
1337
|
+
function sanitizeDoctorPath(value) {
|
|
1338
|
+
return value ? sanitizeIncidentalPath(value) : '';
|
|
1247
1339
|
}
|
|
@@ -4,6 +4,8 @@ import type {
|
|
|
4
4
|
BridgeMethod,
|
|
5
5
|
BridgeRequestSource,
|
|
6
6
|
BridgeResponse,
|
|
7
|
+
RecoveryEventKind,
|
|
8
|
+
RecoveryTelemetrySummary,
|
|
7
9
|
ScreenshotResult,
|
|
8
10
|
SetupStatus,
|
|
9
11
|
} from '../../protocol/src/types.js';
|
|
@@ -30,6 +32,26 @@ export type McpClientName =
|
|
|
30
32
|
|
|
31
33
|
export type SupportedTarget = McpClientName;
|
|
32
34
|
|
|
35
|
+
export type AutoUpdatePolicy = 'off' | 'compatible';
|
|
36
|
+
|
|
37
|
+
export interface BrowserBridgeConfig {
|
|
38
|
+
autoUpdate: AutoUpdatePolicy;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NpmUpdateResult {
|
|
43
|
+
updated: boolean;
|
|
44
|
+
reason:
|
|
45
|
+
| 'updated'
|
|
46
|
+
| 'invalid_extension_version'
|
|
47
|
+
| 'invalid_installed_version'
|
|
48
|
+
| 'extension_not_newer'
|
|
49
|
+
| 'not_global_install'
|
|
50
|
+
| 'no_compatible_update';
|
|
51
|
+
previousVersion?: string;
|
|
52
|
+
version?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
export type Detector = () => boolean | Promise<boolean>;
|
|
34
56
|
|
|
35
57
|
export interface InstallAgentOptions {
|
|
@@ -51,6 +73,8 @@ export interface SetupStatusOptions {
|
|
|
51
73
|
|
|
52
74
|
export interface ProtocolHealthResult {
|
|
53
75
|
extensionConnected?: boolean;
|
|
76
|
+
extensionVersion?: string;
|
|
77
|
+
daemonVersion?: string;
|
|
54
78
|
supported_versions?: string[];
|
|
55
79
|
extension_supported_versions?: string[];
|
|
56
80
|
daemon_supported_versions?: string[];
|
|
@@ -91,6 +115,12 @@ export interface BridgeClientOptions {
|
|
|
91
115
|
checkProtocolOnConnect?: boolean;
|
|
92
116
|
restartDaemonOnVersionMismatch?: boolean;
|
|
93
117
|
restartDaemonFn?: typeof restartBridgeDaemon;
|
|
118
|
+
updateNpmOnCompatibleVersion?: boolean;
|
|
119
|
+
exitProcessOnNpmUpdate?: boolean;
|
|
120
|
+
updateCompatibleNpmPackageFn?: (options: {
|
|
121
|
+
extensionVersion: string;
|
|
122
|
+
supportedVersions: readonly string[];
|
|
123
|
+
}) => Promise<NpmUpdateResult>;
|
|
94
124
|
authToken?: string | null;
|
|
95
125
|
}
|
|
96
126
|
|
|
@@ -182,6 +212,13 @@ export interface DoctorDaemonMetrics {
|
|
|
182
212
|
requestsProcessed: number;
|
|
183
213
|
requestsFailed: number;
|
|
184
214
|
avgResponseTimeMs: number;
|
|
215
|
+
recovery: RecoveryTelemetrySummary | null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface DoctorRecoveryDiagnostics {
|
|
219
|
+
daemon: RecoveryTelemetrySummary | null;
|
|
220
|
+
routedExtension: RecoveryTelemetrySummary | null;
|
|
221
|
+
activeLoops: RecoveryEventKind[];
|
|
185
222
|
}
|
|
186
223
|
|
|
187
224
|
export interface DoctorRecentEvent {
|
|
@@ -246,6 +283,7 @@ export interface DoctorReport {
|
|
|
246
283
|
connections: DoctorConnectionDiagnostics;
|
|
247
284
|
protocol: DoctorProtocolDiagnostics;
|
|
248
285
|
debugger: DoctorDebuggerDiagnostics;
|
|
286
|
+
recovery: DoctorRecoveryDiagnostics;
|
|
249
287
|
metrics: DoctorDaemonMetrics | null;
|
|
250
288
|
recentEvents: DoctorRecentEvent[];
|
|
251
289
|
recentCauses: DoctorRecentEvent['cause'][];
|
|
@@ -1,17 +1,48 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Guidance lines shared by the progressive tool surface.
|
|
5
|
+
*
|
|
6
|
+
* @type {readonly string[]}
|
|
7
|
+
*/
|
|
8
|
+
const SHARED_INSTRUCTIONS = Object.freeze([
|
|
6
9
|
'Prefer Browser Bridge MCP tools over shelling out to bbx. Use bbx only for explicit CLI setup, doctor, logs, or raw debugging requests.',
|
|
7
10
|
'Start with browser_call method health.ping. If window access is disabled, call browser_call method access.request once, ask the user to click Enable in the Browser Bridge popup or side panel, then retry once.',
|
|
8
11
|
'Use structured reads first through browser_call: page.get_state, dom.query, page.get_text, page.extract_content, styles.get_computed, or layout.get_box_model. Prefer semantic extraction for articles and documentation. Use browser_batch only for parallel reads; run mutations sequentially and keep limits tight before widening.',
|
|
9
|
-
'Reuse elementRef values returned by DOM
|
|
12
|
+
'Reuse elementRef values returned by DOM reads. Use attribute allowlists for focused DOM reads.',
|
|
10
13
|
'Escalate to screenshot.capture_element, screenshot.capture_region, dom.get_accessibility_tree, page.evaluate, viewport.resize, or CDP only when structured reads cannot answer the question.',
|
|
11
14
|
'Use patch.apply_styles or patch.apply_dom for temporary experiments, and rollback patches before finishing unless the user asks to keep them.',
|
|
12
|
-
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Workflow guidance appended after the tool-selection rules.
|
|
19
|
+
*
|
|
20
|
+
* @type {readonly string[]}
|
|
21
|
+
*/
|
|
22
|
+
const WORKFLOW_INSTRUCTIONS = Object.freeze([
|
|
13
23
|
'',
|
|
14
24
|
'Page investigation: read page state, scoped DOM, and relevant page text first, batching independent reads. Find targets by text or role when selectors are unknown. Add styles, console, or network reads only when they directly answer the objective, and use screenshots or evaluation only when structured evidence is insufficient.',
|
|
15
25
|
'Layout debugging: locate the target, read only relevant computed properties and its box model, and inspect matched rules only when the cascade is unclear. Prototype the smallest fix with a reversible style patch, verify the result, check for new console errors, then rollback before editing source unless the user asks to keep the patch.',
|
|
16
26
|
'Flow verification: read initial page state, locate controls semantically, reuse elementRef values, interact through input tools, and wait for navigation or UI state changes. Verify the final DOM or page text plus console and network state when relevant. Do not create a new tab unless requested or required by the flow.',
|
|
17
|
-
]
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const SERVER_HEADER =
|
|
30
|
+
"Browser Bridge MCP inspects and interacts with the user's real Chrome tab through a compact tool set.";
|
|
31
|
+
|
|
32
|
+
const TOOL_RULES = Object.freeze([
|
|
33
|
+
'The common tools are available immediately. When a specialized typed tool is useful, call browser_toolset with its exact tool name; browser_call always reaches every bridge method without loading another tool.',
|
|
34
|
+
'Use browser_call method protocol.describe with method or group params to load unfamiliar signatures cheaply.',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build instructions for progressive tool discovery.
|
|
39
|
+
*
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function getMcpServerInstructions() {
|
|
43
|
+
return [SERVER_HEADER, ...TOOL_RULES, ...SHARED_INSTRUCTIONS, ...WORKFLOW_INSTRUCTIONS].join(
|
|
44
|
+
'\n'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const MCP_SERVER_INSTRUCTIONS = getMcpServerInstructions();
|
|
@@ -26,6 +26,8 @@ export const CAPTURE_ACTIONS = {
|
|
|
26
26
|
elementRef: r,
|
|
27
27
|
...(a.format ? { format: a.format } : {}),
|
|
28
28
|
...(typeof a.quality === 'number' ? { quality: a.quality } : {}),
|
|
29
|
+
delivery: a.delivery ?? 'auto',
|
|
30
|
+
...(typeof a.scale === 'number' ? { scale: a.scale } : {}),
|
|
29
31
|
}),
|
|
30
32
|
},
|
|
31
33
|
region: {
|
|
@@ -35,6 +37,8 @@ export const CAPTURE_ACTIONS = {
|
|
|
35
37
|
.../** @type {Record<string, unknown>} */ (a.rect || {}),
|
|
36
38
|
...(a.format ? { format: a.format } : {}),
|
|
37
39
|
...(typeof a.quality === 'number' ? { quality: a.quality } : {}),
|
|
40
|
+
delivery: a.delivery ?? 'auto',
|
|
41
|
+
...(typeof a.scale === 'number' ? { scale: a.scale } : {}),
|
|
38
42
|
}),
|
|
39
43
|
},
|
|
40
44
|
full_page: {
|
|
@@ -43,13 +47,15 @@ export const CAPTURE_ACTIONS = {
|
|
|
43
47
|
params: (a) => ({
|
|
44
48
|
...(a.format ? { format: a.format } : {}),
|
|
45
49
|
...(typeof a.quality === 'number' ? { quality: a.quality } : {}),
|
|
50
|
+
delivery: a.delivery ?? 'auto',
|
|
51
|
+
...(typeof a.scale === 'number' ? { scale: a.scale } : {}),
|
|
46
52
|
}),
|
|
47
53
|
},
|
|
48
54
|
cdp_document: { ref: false, method: 'cdp.get_document', params: () => ({}) },
|
|
49
55
|
cdp_dom_snapshot: {
|
|
50
56
|
ref: false,
|
|
51
57
|
method: 'cdp.get_dom_snapshot',
|
|
52
|
-
params: () => ({}),
|
|
58
|
+
params: (a) => ({ computedStyles: a.computedStyles }),
|
|
53
59
|
},
|
|
54
60
|
cdp_box_model: {
|
|
55
61
|
ref: false,
|
|
@@ -93,7 +99,7 @@ function isValidCaptureRegion(rect) {
|
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
/**
|
|
96
|
-
* @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, format?: 'png' | 'jpeg' | 'webp', quality?: number, nodeId?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
102
|
+
* @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, format?: 'png' | 'jpeg' | 'webp', quality?: number, delivery?: 'auto' | 'inline' | 'artifact', scale?: number, nodeId?: number, computedStyles?: string[], tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
97
103
|
* @returns {Promise<ToolResult>}
|
|
98
104
|
*/
|
|
99
105
|
export async function handleCaptureTool(args) {
|
|
@@ -141,6 +147,27 @@ export async function handleCaptureTool(args) {
|
|
|
141
147
|
*/
|
|
142
148
|
export function createScreenshotResult(response, method) {
|
|
143
149
|
const result = toRecord(response.result);
|
|
150
|
+
if (result.delivery === 'artifact') {
|
|
151
|
+
const artifact = toRecord(result.artifact);
|
|
152
|
+
if (typeof artifact.artifactId !== 'string' || typeof artifact.byteLength !== 'number') {
|
|
153
|
+
return summarizeToolError(new Error(`${method} returned invalid artifact metadata.`));
|
|
154
|
+
}
|
|
155
|
+
return createToolResult(
|
|
156
|
+
`Captured screenshot artifact (${artifact.byteLength} bytes, expires ${artifact.expiresAt ?? 'soon'}). Read it in chunks with the browser_artifact tool (action "read", paging offset until nextOffset is null), then delete it with browser_artifact action "delete" when done.`,
|
|
157
|
+
{
|
|
158
|
+
ok: true,
|
|
159
|
+
method,
|
|
160
|
+
delivery: 'artifact',
|
|
161
|
+
artifact,
|
|
162
|
+
mimeType: result.mimeType,
|
|
163
|
+
byteLength: result.byteLength,
|
|
164
|
+
dimensions: result.dimensions,
|
|
165
|
+
rect: boundedRect(result.rect),
|
|
166
|
+
complete: result.complete === true,
|
|
167
|
+
clipped: result.clipped === true,
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
}
|
|
144
171
|
if (typeof result.image !== 'string') {
|
|
145
172
|
return summarizeToolError(new Error(`${method} returned no image data.`));
|
|
146
173
|
}
|
|
@@ -15,6 +15,32 @@ import {
|
|
|
15
15
|
|
|
16
16
|
/** @type {Record<string, ToolAction>} */
|
|
17
17
|
export const DOM_ACTIONS = {
|
|
18
|
+
baseline_create: {
|
|
19
|
+
ref: false,
|
|
20
|
+
method: 'dom.baseline.create',
|
|
21
|
+
params: (a) => ({
|
|
22
|
+
selector: a.selector,
|
|
23
|
+
maxNodes: a.maxNodes,
|
|
24
|
+
maxDepth: a.maxDepth,
|
|
25
|
+
textBudget: a.textBudget,
|
|
26
|
+
attributeAllowlist: a.attributeAllowlist,
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
baseline_compare: {
|
|
30
|
+
ref: false,
|
|
31
|
+
method: 'dom.baseline.compare',
|
|
32
|
+
params: (a) => ({ baselineId: a.baselineId, maxChanges: a.maxChanges }),
|
|
33
|
+
},
|
|
34
|
+
baseline_describe: {
|
|
35
|
+
ref: false,
|
|
36
|
+
method: 'dom.baseline.describe',
|
|
37
|
+
params: (a) => ({ baselineId: a.baselineId }),
|
|
38
|
+
},
|
|
39
|
+
baseline_release: {
|
|
40
|
+
ref: false,
|
|
41
|
+
method: 'dom.baseline.release',
|
|
42
|
+
params: (a) => ({ baselineId: a.baselineId }),
|
|
43
|
+
},
|
|
18
44
|
query: {
|
|
19
45
|
ref: false,
|
|
20
46
|
method: 'dom.query',
|
|
@@ -96,7 +122,7 @@ export const DOM_ACTIONS = {
|
|
|
96
122
|
};
|
|
97
123
|
|
|
98
124
|
/**
|
|
99
|
-
* @param {{ action: string, selector?: string, elementRef?: string, withinRef?: string, maxNodes?: number, maxDepth?: number, compact?: boolean, interactiveOnly?: boolean, textBudget?: number, includeBbox?: boolean, attributeAllowlist?: string[], attributes?: string[], text?: string, exact?: boolean, maxResults?: number, role?: string, name?: string, state?: string, timeoutMs?: number, outer?: boolean, maxLength?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
125
|
+
* @param {{ action: string, baselineId?: string, maxChanges?: number, selector?: string, elementRef?: string, withinRef?: string, maxNodes?: number, maxDepth?: number, compact?: boolean, interactiveOnly?: boolean, textBudget?: number, includeBbox?: boolean, attributeAllowlist?: string[], attributes?: string[], text?: string, exact?: boolean, maxResults?: number, role?: string, name?: string, state?: string, timeoutMs?: number, outer?: boolean, maxLength?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
100
126
|
* @returns {Promise<ToolResult>}
|
|
101
127
|
*/
|
|
102
128
|
export async function handleDomTool(args) {
|
|
@@ -109,7 +135,17 @@ export async function handleDomTool(args) {
|
|
|
109
135
|
if (args.action === 'find_role' && !hasText(args.role)) {
|
|
110
136
|
return summarizeToolError('role is required for dom.find_by_role.');
|
|
111
137
|
}
|
|
112
|
-
if (
|
|
138
|
+
if (
|
|
139
|
+
['baseline_compare', 'baseline_describe', 'baseline_release'].includes(args.action) &&
|
|
140
|
+
!hasText(args.baselineId)
|
|
141
|
+
) {
|
|
142
|
+
return summarizeToolError('baselineId is required for this DOM baseline action.');
|
|
143
|
+
}
|
|
144
|
+
if (
|
|
145
|
+
args.action === 'query' ||
|
|
146
|
+
args.action === 'accessibility_tree' ||
|
|
147
|
+
args.action === 'baseline_create'
|
|
148
|
+
) {
|
|
113
149
|
const inferred =
|
|
114
150
|
args.action === 'accessibility_tree' ? 'normal' : inferBudgetFromSelector(args);
|
|
115
151
|
const withBudget = inferred ? { ...args, budgetPreset: args.budgetPreset ?? inferred } : args;
|
|
@@ -118,6 +154,18 @@ export async function handleDomTool(args) {
|
|
|
118
154
|
if (args.action === 'text') {
|
|
119
155
|
return dispatchToolAction(DOM_ACTIONS, applyTextBudgetPreset(args), 'DOM');
|
|
120
156
|
}
|
|
157
|
+
if (args.action === 'baseline_compare') {
|
|
158
|
+
const byPreset = { quick: 10, normal: 50, deep: 100 };
|
|
159
|
+
return dispatchToolAction(
|
|
160
|
+
DOM_ACTIONS,
|
|
161
|
+
{
|
|
162
|
+
...args,
|
|
163
|
+
maxChanges:
|
|
164
|
+
args.maxChanges ?? (args.budgetPreset ? byPreset[args.budgetPreset] : undefined),
|
|
165
|
+
},
|
|
166
|
+
'DOM'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
121
169
|
if (args.action === 'html') {
|
|
122
170
|
return dispatchToolAction(DOM_ACTIONS, applyHtmlBudgetPreset(args), 'DOM');
|
|
123
171
|
}
|
|
@@ -189,11 +237,14 @@ export const PATCH_ACTIONS = {
|
|
|
189
237
|
removeAttribute: 'remove_attribute',
|
|
190
238
|
addClass: 'add_class',
|
|
191
239
|
removeClass: 'remove_class',
|
|
240
|
+
toggleClass: 'toggle_class',
|
|
192
241
|
setTextContent: 'set_text',
|
|
193
242
|
};
|
|
194
243
|
const normalizedOperation = opMap[operation] || operation;
|
|
195
244
|
const value =
|
|
196
|
-
normalizedOperation === 'add_class' ||
|
|
245
|
+
normalizedOperation === 'add_class' ||
|
|
246
|
+
normalizedOperation === 'remove_class' ||
|
|
247
|
+
normalizedOperation === 'toggle_class'
|
|
197
248
|
? (a.value ?? a.name)
|
|
198
249
|
: a.value;
|
|
199
250
|
return {
|