@browserbridge/bbx 1.2.0 → 1.3.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/README.md CHANGED
@@ -99,17 +99,18 @@ Managed installs support OpenAI Codex, Claude Code, Cursor, GitHub Copilot, Open
99
99
 
100
100
  ## Why Browser Bridge
101
101
 
102
- Most adjacent tools optimize for different goals. [Playwright](https://playwright.dev/) and headless automation stacks are excellent for deterministic tests and CI - but they start from a clean browser context by design. [Claude in Chrome](https://support.claude.com/en/articles/12012173-get-started-with-claude-in-chrome) is great for integrated Claude workflows, but is vendor-specific. Generic MCP browser servers offer broad control without the developer-focused depth.
102
+ Most adjacent tools optimize for different goals. [Playwright](https://playwright.dev/) and headless automation stacks are excellent for deterministic tests and CI - but they start from a clean browser context by design. [Claude in Chrome](https://support.claude.com/en/articles/12012173-get-started-with-claude-in-chrome) is great for integrated Claude workflows, and the [Codex extension](https://chromewebstore.google.com/detail/codex/hehggadaopoacecdllhhajmbjkdcmajg) is a great option if you use Codex, but both are vendor-specific. Generic MCP browser servers offer broad control without the developer-focused depth.
103
103
 
104
104
  Browser Bridge is optimized for the opposite starting point: **inspect the state that already exists** in a real tab - logged-in sessions, feature flags, seeded storage, SPA state - use structured reads to understand it, test a patch in place, then fix the source. It's open-source, agent-agnostic, and scoped to explicit tab sessions rather than ambient browser control.
105
105
 
106
106
  ## Setup
107
107
 
108
- 1. Install [Browser Bridge from the Chrome Web Store](https://chromewebstore.google.com/detail/browser-bridge/jjjkmmcdkpcgamlopogicbnnhdgebhie)
108
+ 1. Install [Browser Bridge from the Chrome Web Store](https://chromewebstore.google.com/detail/browser-bridge/jjjkmmcdkpcgamlopogicbnnhdgebhie) in Chrome or another Chromium-based browser
109
109
  2. `npm install -g @browserbridge/bbx` - installs the CLI and native host
110
- 3. In the extension side panel, install MCP or CLI (skill) for your agent of choice
111
- 4. Enable Browser Bridge for the Chrome window you want to inspect/control with the AI agent
112
- 5. Ask your agent to use Browser Bridge via MCP (`BB MCP` or `Browser Bridge MCP`), or invoke the `browser-bridge` / `$bbx` skill in CLI mode
110
+ 3. Run `bbx install`, or target a specific browser with `bbx install --browser edge`, `bbx install --browser brave`, `bbx install --browser chromium`, or `bbx install --browser arc`
111
+ 4. In the extension side panel, install MCP or CLI (skill) for your agent of choice
112
+ 5. Enable Browser Bridge for the browser window you want to inspect/control with the AI agent
113
+ 6. Ask your agent to use Browser Bridge via MCP (`BB MCP` or `Browser Bridge MCP`), or invoke the `browser-bridge` / `$bbx` skill in CLI mode
113
114
 
114
115
  ## How it works
115
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbridge/bbx",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -50,8 +50,8 @@ import { getDoctorReport, requestBridge, resolveRef } from './runtime.js';
50
50
  import { collectSetupStatus } from './setup-status.js';
51
51
  import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
52
52
 
53
- /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
54
- /** @typedef {{ image: string, rect: Record<string, unknown> }} ScreenshotResult */
53
+ /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
54
+ /** @typedef {import('./types.js').ScreenshotResult} ScreenshotResult */
55
55
 
56
56
  const REQUEST_SOURCE = 'cli';
57
57
  const TEST_TIMEOUT_ENV = 'BBX_CLIENT_REQUEST_TIMEOUT_MS';
@@ -139,8 +139,8 @@ if (command === 'install-skill') {
139
139
  projectPath: isGlobal ? os.homedir() : process.cwd(),
140
140
  ...getSetupStatusTestOverrides(),
141
141
  });
142
- /** @type {import('./install.js').SupportedTarget[]} */
143
- const detected = /** @type {import('./install.js').SupportedTarget[]} */ (
142
+ /** @type {import('./types.js').SupportedTarget[]} */
143
+ const detected = /** @type {import('./types.js').SupportedTarget[]} */ (
144
144
  setupStatus.skillTargets.filter((entry) => entry.detected).map((entry) => entry.key)
145
145
  );
146
146
  const installedManagedTargets = new Set(
@@ -149,7 +149,7 @@ if (command === 'install-skill') {
149
149
  .map((entry) => entry.key)
150
150
  );
151
151
  const installedManagedTargetList =
152
- /** @type {import('./install.js').SupportedTarget[]} */ ([...installedManagedTargets]);
152
+ /** @type {import('./types.js').SupportedTarget[]} */ ([...installedManagedTargets]);
153
153
 
154
154
  // Aliases like 'openai' and 'google' map to canonical targets and stay omitted.
155
155
  const items = SUPPORTED_TARGETS.map((t) => ({
@@ -167,19 +167,19 @@ if (command === 'install-skill') {
167
167
  items
168
168
  );
169
169
 
170
- /** @type {import('./install.js').SupportedTarget[]} */
170
+ /** @type {import('./types.js').SupportedTarget[]} */
171
171
  let targets;
172
172
  if (selected === null) {
173
173
  // Non-TTY: prefer managed installs, then detected targets (always includes 'agents').
174
174
  targets = installedManagedTargets.size > 0 ? installedManagedTargetList : detected;
175
175
  } else {
176
- targets = /** @type {import('./install.js').SupportedTarget[]} */ (selected);
176
+ targets = /** @type {import('./types.js').SupportedTarget[]} */ (selected);
177
177
  }
178
178
 
179
179
  const projectPath = isGlobal ? os.homedir() : process.cwd();
180
180
  if (selected !== null) {
181
181
  const deselectedTargets =
182
- /** @type {import('./install.js').SupportedTarget[]} */ (
182
+ /** @type {import('./types.js').SupportedTarget[]} */ (
183
183
  installedManagedTargetList.filter((target) => !targets.includes(target))
184
184
  );
185
185
  const removableTargets = await findInstalledManagedTargets({
@@ -241,7 +241,7 @@ if (command === 'install-mcp') {
241
241
 
242
242
  const clientArg = argsLeft[0];
243
243
 
244
- /** @type {import('./mcp-config.js').McpClientName[]} */
244
+ /** @type {import('./types.js').McpClientName[]} */
245
245
  let clients;
246
246
 
247
247
  if (!clientArg) {
@@ -252,14 +252,14 @@ if (command === 'install-mcp') {
252
252
  projectPath: process.cwd(),
253
253
  ...getSetupStatusTestOverrides(),
254
254
  });
255
- const detected = /** @type {import('./mcp-config.js').McpClientName[]} */ (
255
+ const detected = /** @type {import('./types.js').McpClientName[]} */ (
256
256
  setupStatus.mcpClients.filter((entry) => entry.detected).map((entry) => entry.key)
257
257
  );
258
258
  const configuredClients = new Set(
259
259
  setupStatus.mcpClients.filter((entry) => entry.configured).map((entry) => entry.key)
260
260
  );
261
261
  const configuredClientList =
262
- /** @type {import('./mcp-config.js').McpClientName[]} */ ([...configuredClients]);
262
+ /** @type {import('./types.js').McpClientName[]} */ ([...configuredClients]);
263
263
  const items = MCP_CLIENT_NAMES.map((c) => ({
264
264
  value: c,
265
265
  label: `${c.padEnd(10)} ${MCP_CLIENT_LABELS[c]}`,
@@ -284,12 +284,12 @@ if (command === 'install-mcp') {
284
284
  ? detected
285
285
  : [...MCP_CLIENT_NAMES];
286
286
  } else {
287
- clients = /** @type {import('./mcp-config.js').McpClientName[]} */ (selected);
287
+ clients = /** @type {import('./types.js').McpClientName[]} */ (selected);
288
288
  }
289
289
 
290
290
  if (selected !== null) {
291
291
  const deselectedClients =
292
- /** @type {import('./mcp-config.js').McpClientName[]} */ (
292
+ /** @type {import('./types.js').McpClientName[]} */ (
293
293
  configuredClientList.filter((clientName) => !clients.includes(clientName))
294
294
  );
295
295
  const removableClients = await findConfiguredMcpClients({
@@ -470,15 +470,20 @@ async function main() {
470
470
  }
471
471
 
472
472
  if (command === 'batch') {
473
- await ensureClientConnection();
474
473
  const input = rest[0];
475
474
  if (!input) {
476
475
  throw new Error('Usage: batch \'[{"method":"...","params":{...}}, ...]\'');
477
476
  }
478
- const calls = JSON.parse(input);
477
+ let calls;
478
+ try {
479
+ calls = JSON.parse(input);
480
+ } catch {
481
+ throw new Error('Invalid JSON syntax. Expected a JSON array of bridge calls.');
482
+ }
479
483
  if (!Array.isArray(calls)) {
480
484
  throw new Error('Batch input must be a JSON array.');
481
485
  }
486
+ await ensureClientConnection();
482
487
  const results = await Promise.all(
483
488
  calls.map(async (call) => {
484
489
  if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
@@ -847,10 +852,13 @@ async function uninstallBrowserBridge() {
847
852
  */
848
853
  async function parseCallCommand(args) {
849
854
  const parsed = extractTabFlag(args);
850
- const [first, second] = parsed.rest;
855
+ const [first, second, ...extra] = parsed.rest;
851
856
  if (!first) {
852
857
  throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
853
858
  }
859
+ if (extra.length > 0) {
860
+ throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
861
+ }
854
862
 
855
863
  if (first.includes('.')) {
856
864
  const method = /** @type {BridgeMethod} */ (first);
@@ -17,51 +17,13 @@ import {
17
17
  } from '../../native-host/src/config.js';
18
18
  import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
19
19
 
20
- /** @typedef {import('../../native-host/src/config.js').BridgeTransport} BridgeTransport */
21
-
22
- /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
23
- /** @typedef {import('../../protocol/src/types.js').BridgeMeta} BridgeMeta */
24
- /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
25
- /**
26
- * @typedef {{
27
- * extensionConnected?: boolean,
28
- * supported_versions?: string[],
29
- * daemon_supported_versions?: string[],
30
- * deprecated_since?: string,
31
- * migration_hint?: string
32
- * }} ProtocolHealthResult
33
- */
34
-
35
- /**
36
- * @typedef {{
37
- * type: 'registered',
38
- * role: 'agent' | 'extension',
39
- * clientId?: string
40
- * } | {
41
- * type: 'agent.response',
42
- * response: BridgeResponse
43
- * }} ClientMessage
44
- */
45
-
46
- /**
47
- * @typedef {{
48
- * resolve: (value: any) => void,
49
- * reject: (error: Error) => void,
50
- * timeoutId: NodeJS.Timeout
51
- * }} PendingRequest
52
- */
53
-
54
- /**
55
- * @typedef {{
56
- * transport?: BridgeTransport,
57
- * socketPath?: string,
58
- * clientId?: string,
59
- * defaultTimeoutMs?: number,
60
- * autoReconnect?: boolean,
61
- * restartDaemonOnVersionMismatch?: boolean,
62
- * restartDaemonFn?: typeof restartBridgeDaemon,
63
- * }} BridgeClientOptions
64
- */
20
+ /** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
21
+ /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
22
+ /** @typedef {import('./types.js').BridgeResponse} BridgeResponse */
23
+ /** @typedef {import('./types.js').BridgeClientOptions} BridgeClientOptions */
24
+ /** @typedef {import('./types.js').ClientMessage} ClientMessage */
25
+ /** @typedef {import('./types.js').PendingRequest} PendingRequest */
26
+ /** @typedef {import('./types.js').ProtocolHealthResult} ProtocolHealthResult */
65
27
 
66
28
  /**
67
29
  * @param {string} left
@@ -3,18 +3,8 @@
3
3
  import { BRIDGE_METHOD_REGISTRY, BRIDGE_METHODS } from '../../protocol/src/index.js';
4
4
  import { parseCommaList, parseIntArg, parsePropertyAssignments } from './cli-helpers.js';
5
5
 
6
- /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
7
-
8
- /**
9
- * @typedef {{
10
- * method: BridgeMethod,
11
- * resolve?: boolean,
12
- * printMethod?: string,
13
- * usage: string,
14
- * description: string,
15
- * build: (r: string[], ref?: string) => Record<string, unknown>
16
- * }} ShortcutCommand
17
- */
6
+ /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
7
+ /** @typedef {import('./types.js').ShortcutCommand} ShortcutCommand */
18
8
 
19
9
  /**
20
10
  * @param {BridgeMethod} method
@@ -247,7 +237,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
247
237
  {
248
238
  title: 'Setup',
249
239
  lines: [
250
- 'bbx install [--browser chrome|edge|brave|chromium] [extension-id] Install native messaging manifest',
240
+ 'bbx install [--browser chrome|edge|brave|chromium|arc] [extension-id] Install native messaging manifest',
251
241
  'bbx uninstall Remove native host manifests, Browser Bridge runtime files, and managed MCP/skill installs',
252
242
  'bbx install [--all] [--browser <name>] [extension-id] Install native host manifest (--all for all supported browsers)',
253
243
  'bbx install-skill [targets|all] [--global] [--project <path>] Install/update the managed Browser Bridge CLI skill',
@@ -4,9 +4,9 @@ import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
 
7
- /** @typedef {import('./mcp-config.js').McpClientName} McpClientName */
8
- /** @typedef {import('./install.js').SupportedTarget} SupportedTarget */
9
- /** @typedef {() => boolean | Promise<boolean>} Detector */
7
+ /** @typedef {import('./types.js').McpClientName} McpClientName */
8
+ /** @typedef {import('./types.js').SupportedTarget} SupportedTarget */
9
+ /** @typedef {import('./types.js').Detector} Detector */
10
10
 
11
11
  const home = os.homedir();
12
12
  const platform = process.platform;
@@ -41,9 +41,7 @@ const copilotBrowserBridgeNote = [
41
41
  '',
42
42
  ].join('\n');
43
43
 
44
- /**
45
- * @typedef {'codex' | 'claude' | 'cursor' | 'copilot' | 'opencode' | 'antigravity' | 'windsurf' | 'agents'} SupportedTarget
46
- */
44
+ /** @typedef {import('./types.js').SupportedTarget} SupportedTarget */
47
45
 
48
46
  /** @type {SupportedTarget[]} */
49
47
  export const SUPPORTED_TARGETS = [...supportedTargets];
@@ -68,9 +66,7 @@ export function isSupportedTarget(value) {
68
66
  return supportedTargets.includes(/** @type {SupportedTarget} */ (value));
69
67
  }
70
68
 
71
- /**
72
- * @typedef {{targets: SupportedTarget[], projectPath: string, global: boolean}} InstallAgentOptions
73
- */
69
+ /** @typedef {import('./types.js').InstallAgentOptions} InstallAgentOptions */
74
70
 
75
71
  /**
76
72
  * @param {string[]} args
@@ -202,7 +198,7 @@ async function rollbackInstalledSkillDirs(attempted) {
202
198
  /**
203
199
  * Write MCP config for the given clients.
204
200
  *
205
- * @param {import('./mcp-config.js').McpClientName[]} clients
201
+ * @param {import('./types.js').McpClientName[]} clients
206
202
  * @param {{
207
203
  * global: boolean,
208
204
  * projectPath: string,
@@ -5,9 +5,7 @@ import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
 
8
- /**
9
- * @typedef {'codex' | 'claude' | 'cursor' | 'copilot' | 'opencode' | 'antigravity' | 'windsurf' | 'agents'} McpClientName
10
- */
8
+ /** @typedef {import('./types.js').McpClientName} McpClientName */
11
9
 
12
10
  /** @type {McpClientName[]} */
13
11
  export const MCP_CLIENT_NAMES = [
@@ -12,39 +12,14 @@ import { resolveDefaultExtensionId } from '../../native-host/src/install-manifes
12
12
  import { methodNeedsTab } from './cli-helpers.js';
13
13
  import { BridgeClient } from './client.js';
14
14
 
15
- /** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
16
- /** @typedef {import('../../protocol/src/types.js').BridgeMeta} BridgeMeta */
17
- /** @typedef {import('../../protocol/src/types.js').BridgeRequestSource} BridgeRequestSource */
18
- /** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
15
+ /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
16
+ /** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
17
+ /** @typedef {import('./types.js').BridgeRequestSource} BridgeRequestSource */
18
+ /** @typedef {import('./types.js').BridgeResponse} BridgeResponse */
19
19
  /** @typedef {import('../../native-host/src/config.js').SupportedBrowser} SupportedBrowser */
20
-
21
- /**
22
- * @typedef {{
23
- * browser: string,
24
- * manifestPath: string,
25
- * installed: boolean
26
- * }} BrowserManifestStatus
27
- */
28
-
29
- /**
30
- * @typedef {{
31
- * manifestInstalled: boolean,
32
- * manifestPath: string,
33
- * allowedOrigins: string[],
34
- * defaultExtensionId: string | null,
35
- * defaultExtensionIdSource: string,
36
- * daemonReachable: boolean,
37
- * extensionConnected: boolean,
38
- * accessEnabled: boolean,
39
- * enabledWindowId: number | null,
40
- * routeTabId: number | null,
41
- * routeReady: boolean,
42
- * routeReason: string,
43
- * issues: string[],
44
- * nextSteps: string[],
45
- * browserManifests: BrowserManifestStatus[]
46
- * }} DoctorReport
47
- */
20
+ /** @typedef {import('./types.js').BrowserManifestStatus} BrowserManifestStatus */
21
+ /** @typedef {import('./types.js').DoctorReport} DoctorReport */
22
+ /** @typedef {import('./types.js').DoctorReportOptions} DoctorReportOptions */
48
23
 
49
24
  /**
50
25
  * @param {BridgeClient} client
@@ -178,15 +153,6 @@ export async function checkBrowserManifests() {
178
153
  );
179
154
  }
180
155
 
181
- /**
182
- * @typedef {{
183
- * loadManifest?: () => Promise<{allowed_origins?: string[]} | null>,
184
- * manifestPath?: string,
185
- * defaultExtensionIdInfo?: { extensionId: string | null, source: string },
186
- * bridgeClientRunner?: <T>(callback: (client: BridgeClient) => Promise<T>) => Promise<T>
187
- * }} DoctorReportOptions
188
- */
189
-
190
156
  /**
191
157
  * @param {DoctorReportOptions} [options={}]
192
158
  * @returns {Promise<DoctorReport>}
@@ -21,8 +21,8 @@ import {
21
21
  SUPPORTED_TARGETS,
22
22
  } from './install.js';
23
23
 
24
- /** @typedef {import('./mcp-config.js').McpClientName} McpClientName */
25
- /** @typedef {import('./install.js').SupportedTarget} SupportedTarget */
24
+ /** @typedef {import('./types.js').McpClientName} McpClientName */
25
+ /** @typedef {import('./types.js').SupportedTarget} SupportedTarget */
26
26
  /** @typedef {import('../../protocol/src/types.js').SetupStatus} SetupStatus */
27
27
  /** @typedef {import('../../protocol/src/types.js').McpClientStatus} McpClientStatus */
28
28
  /** @typedef {import('../../protocol/src/types.js').SkillTargetStatus} SkillTargetStatus */
@@ -52,17 +52,7 @@ const SKILL_TARGET_LABELS = {
52
52
  agents: 'Generic agents',
53
53
  };
54
54
 
55
- /**
56
- * @typedef {{
57
- * global?: boolean,
58
- * cwd?: string,
59
- * projectPath?: string,
60
- * mcpDetectors?: Record<string, () => boolean | Promise<boolean>>,
61
- * skillDetectors?: Record<string, () => boolean | Promise<boolean>>,
62
- * access?: (targetPath: string) => Promise<void>,
63
- * readFile?: (targetPath: string, encoding: BufferEncoding) => Promise<string>
64
- * }} SetupStatusOptions
65
- */
55
+ /** @typedef {import('./types.js').SetupStatusOptions} SetupStatusOptions */
66
56
 
67
57
  /**
68
58
  * Return Browser Bridge MCP and skill installation status for supported clients.
@@ -0,0 +1,131 @@
1
+ import type { BridgeTransport } from '../../native-host/src/config.js';
2
+ import type {
3
+ BridgeMeta,
4
+ BridgeMethod,
5
+ BridgeRequestSource,
6
+ BridgeResponse,
7
+ } from '../../protocol/src/types.js';
8
+ import type { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
9
+
10
+ export type { BridgeMeta, BridgeMethod, BridgeRequestSource, BridgeResponse, BridgeTransport };
11
+
12
+ export type McpClientName =
13
+ | 'codex'
14
+ | 'claude'
15
+ | 'cursor'
16
+ | 'copilot'
17
+ | 'opencode'
18
+ | 'antigravity'
19
+ | 'windsurf'
20
+ | 'agents';
21
+
22
+ export type SupportedTarget = McpClientName;
23
+
24
+ export type Detector = () => boolean | Promise<boolean>;
25
+
26
+ export interface InstallAgentOptions {
27
+ targets: SupportedTarget[];
28
+ projectPath: string;
29
+ global: boolean;
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ export interface SetupStatusOptions {
34
+ global?: boolean;
35
+ cwd?: string;
36
+ projectPath?: string;
37
+ mcpDetectors?: Record<string, Detector>;
38
+ skillDetectors?: Record<string, Detector>;
39
+ access?: (targetPath: string) => Promise<void>;
40
+ readFile?: (targetPath: string, encoding: BufferEncoding) => Promise<string>;
41
+ }
42
+
43
+ export interface ProtocolHealthResult {
44
+ extensionConnected?: boolean;
45
+ supported_versions?: string[];
46
+ daemon_supported_versions?: string[];
47
+ deprecated_since?: string;
48
+ migration_hint?: string;
49
+ }
50
+
51
+ export type ClientMessage =
52
+ | {
53
+ type: 'registered';
54
+ role: 'agent' | 'extension';
55
+ clientId?: string;
56
+ }
57
+ | {
58
+ type: 'agent.response';
59
+ response: BridgeResponse;
60
+ };
61
+
62
+ export interface PendingRequest {
63
+ resolve: (value: any) => void;
64
+ reject: (error: Error) => void;
65
+ timeoutId: NodeJS.Timeout;
66
+ }
67
+
68
+ export interface BridgeClientOptions {
69
+ transport?: BridgeTransport;
70
+ socketPath?: string;
71
+ clientId?: string;
72
+ defaultTimeoutMs?: number;
73
+ autoReconnect?: boolean;
74
+ restartDaemonOnVersionMismatch?: boolean;
75
+ restartDaemonFn?: typeof restartBridgeDaemon;
76
+ }
77
+
78
+ export interface ShortcutCommand {
79
+ method: BridgeMethod;
80
+ resolve?: boolean;
81
+ printMethod?: string;
82
+ usage: string;
83
+ description: string;
84
+ build: (r: string[], ref?: string) => Record<string, unknown>;
85
+ }
86
+
87
+ export interface BrowserManifestStatus {
88
+ browser: string;
89
+ manifestPath: string;
90
+ installed: boolean;
91
+ }
92
+
93
+ export interface DoctorReport {
94
+ manifestInstalled: boolean;
95
+ manifestPath: string;
96
+ allowedOrigins: string[];
97
+ defaultExtensionId: string | null;
98
+ defaultExtensionIdSource: string;
99
+ daemonReachable: boolean;
100
+ extensionConnected: boolean;
101
+ accessEnabled: boolean;
102
+ enabledWindowId: number | null;
103
+ routeTabId: number | null;
104
+ routeReady: boolean;
105
+ routeReason: string;
106
+ issues: string[];
107
+ nextSteps: string[];
108
+ browserManifests: BrowserManifestStatus[];
109
+ }
110
+
111
+ export interface DoctorReportOptions {
112
+ loadManifest?: () => Promise<{ allowed_origins?: string[] } | null>;
113
+ manifestPath?: string;
114
+ defaultExtensionIdInfo?: { extensionId: string | null; source: string };
115
+ bridgeClientRunner?: <T>(
116
+ callback: (client: { request: BridgeClientRequest }) => Promise<T>
117
+ ) => Promise<T>;
118
+ }
119
+
120
+ export type BridgeClientRequest = (options: {
121
+ method: BridgeMethod;
122
+ tabId?: number | null;
123
+ params?: Record<string, unknown>;
124
+ meta?: BridgeMeta;
125
+ timeoutMs?: number;
126
+ }) => Promise<BridgeResponse>;
127
+
128
+ export interface ScreenshotResult {
129
+ image: string;
130
+ rect: Record<string, unknown>;
131
+ }
@@ -40,22 +40,34 @@ export const CAPTURE_ACTIONS = {
40
40
  params: () => ({}),
41
41
  },
42
42
  cdp_box_model: {
43
- ref: true,
43
+ ref: false,
44
44
  method: 'cdp.get_box_model',
45
- params: (_, r) => ({ elementRef: r }),
45
+ params: (a) => ({ nodeId: a.nodeId }),
46
46
  },
47
47
  cdp_computed_styles: {
48
- ref: true,
48
+ ref: false,
49
49
  method: 'cdp.get_computed_styles_for_node',
50
- params: (_, r) => ({ elementRef: r }),
50
+ params: (a) => ({ nodeId: a.nodeId }),
51
51
  },
52
52
  };
53
53
 
54
+ /** @param {Record<string, unknown>} args */
55
+ function isCdpNodeCapture(args) {
56
+ return args.action === 'cdp_box_model' || args.action === 'cdp_computed_styles';
57
+ }
58
+
54
59
  /**
55
- * @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
60
+ * @param {{ action: string, elementRef?: string, selector?: string, rect?: Record<string, unknown>, nodeId?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
56
61
  * @returns {Promise<ToolResult>}
57
62
  */
58
63
  export async function handleCaptureTool(args) {
64
+ if (
65
+ isCdpNodeCapture(args) &&
66
+ (typeof args.nodeId !== 'number' || !Number.isFinite(args.nodeId))
67
+ ) {
68
+ return summarizeToolError('nodeId must be a finite number.');
69
+ }
70
+
59
71
  return dispatchToolAction(CAPTURE_ACTIONS, args, 'capture');
60
72
  }
61
73
 
@@ -148,6 +148,7 @@ export const PATCH_ACTIONS = {
148
148
  target: { elementRef: r },
149
149
  declarations: a.declarations,
150
150
  important: a.important,
151
+ patchId: a.patchId,
151
152
  verify: a.verify,
152
153
  }),
153
154
  },
@@ -160,16 +161,22 @@ export const PATCH_ACTIONS = {
160
161
  const opMap = {
161
162
  setAttribute: 'set_attribute',
162
163
  removeAttribute: 'remove_attribute',
163
- addClass: 'toggle_class',
164
- removeClass: 'toggle_class',
164
+ addClass: 'add_class',
165
+ removeClass: 'remove_class',
165
166
  setTextContent: 'set_text',
166
167
  setProperty: 'set_attribute',
167
168
  };
169
+ const normalizedOperation = opMap[operation] || operation;
170
+ const value =
171
+ normalizedOperation === 'add_class' || normalizedOperation === 'remove_class'
172
+ ? (a.value ?? a.name)
173
+ : a.value;
168
174
  return {
169
175
  target: { elementRef: r },
170
- operation: opMap[operation] || operation,
171
- value: a.value,
176
+ operation: normalizedOperation,
177
+ value,
172
178
  name: a.name,
179
+ patchId: a.patchId,
173
180
  verify: a.verify,
174
181
  };
175
182
  },
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  annotateBridgeSummary,
10
10
  applyLimitBudgetPreset,
11
- applyTextBudgetPreset,
11
+ applyPageTextBudgetPreset,
12
12
  bridgeMethodNeedsTab,
13
13
  callBridgeTool,
14
14
  createToolResult,
@@ -72,7 +72,7 @@ export const PAGE_ACTIONS = {
72
72
  export async function handlePageTool(args) {
73
73
  let normalizedArgs = args;
74
74
  if (args.action === 'text') {
75
- normalizedArgs = applyTextBudgetPreset(args);
75
+ normalizedArgs = applyPageTextBudgetPreset(args);
76
76
  } else if (args.action === 'console') {
77
77
  normalizedArgs = applyLimitBudgetPreset(args, {
78
78
  quick: 10,
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  bridgeMethodNeedsTab,
5
5
  DEFAULT_MAX_HTML_LENGTH,
6
+ DEFAULT_PAGE_TEXT_BUDGET,
6
7
  estimateJsonPayloadCost,
7
8
  getBudgetPreset,
8
9
  getErrorRecovery,
@@ -176,6 +177,27 @@ export function applyTextBudgetPreset(args) {
176
177
  });
177
178
  }
178
179
 
180
+ /**
181
+ * @template {{ budgetPreset?: unknown, textBudget?: unknown }} T
182
+ * @param {T} args
183
+ * @returns {T}
184
+ */
185
+ export function applyPageTextBudgetPreset(args) {
186
+ const presetName = getBudgetPresetName(args.budgetPreset);
187
+ if (!presetName) {
188
+ return args;
189
+ }
190
+ const textBudgetByPreset = {
191
+ quick: 2000,
192
+ normal: DEFAULT_PAGE_TEXT_BUDGET,
193
+ deep: DEFAULT_PAGE_TEXT_BUDGET * 2,
194
+ };
195
+ return /** @type {T} */ ({
196
+ ...args,
197
+ textBudget: args.textBudget ?? textBudgetByPreset[presetName],
198
+ });
199
+ }
200
+
179
201
  /**
180
202
  * @template {{ budgetPreset?: unknown, limit?: unknown }} T
181
203
  * @param {T} args