browser-pilot-cli 0.2.2 → 0.3.0-rc.1

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
@@ -28,6 +28,9 @@ a Chrome WebSocket and never display the Allow dialog.
28
28
  Use `bp connect --browser <id|product|channel>` for an explicit choice. Product
29
29
  embedders can pass the same selector to `browser-pilot bridge --stdio
30
30
  --browser <selector>`; otherwise selection follows a stable platform order.
31
+ One connection covers every live Chrome Profile on that endpoint. When several
32
+ Profiles are open, `bp connect` lists them without creating a Pilot window; use
33
+ `bp profiles` and `bp profile <index>`, or pass `--profile` to `bp open --new`.
31
34
 
32
35
  ### 2. Install the plugin for your agent
33
36
 
@@ -66,6 +69,7 @@ The agent will use `bp` commands automatically. Your real login sessions are pre
66
69
 
67
70
  - **No extension required** — Uses Chrome 144's native remote debugging toggle, not the Extension Debugger API
68
71
  - **Real login sessions** — Operates your actual browser profile. Cookies, extensions, logins all intact
72
+ - **Multi-Profile routing** — Lists user tabs across live Chrome Profiles and creates managed work only in a resolved Profile
69
73
  - **CLI-native** — Any agent with bash access can use it. No MCP protocol, no SDK integration needed
70
74
  - **Auto-snapshot** — Every action returns page state with numbered `[ref]` elements, so the agent always knows what's on screen
71
75
  - **Adaptive page views** — Bounded read, text search, DOM metadata, page geometry, and annotated screenshots let an agent choose the smallest useful representation
@@ -139,8 +143,9 @@ launching the bridge. Bulk cleanup remains limited to managed tabs, and user
139
143
  tabs remain open when a session ends.
140
144
 
141
145
  The `browser-pilot bridge --stdio` transport, Broker lifecycle, browser tool
142
- dispatch, event replay, protected Artifacts, and protocol 1.1 transport limit
143
- negotiation are implemented. Browser disconnect and explicit reconnect handling, scoped
146
+ dispatch, event replay, protected Artifacts, protocol 1.1 transport limit
147
+ negotiation, and protocol 1.2 Chrome Profile routing are implemented. Browser
148
+ disconnect and explicit reconnect handling, scoped
144
149
  download Artifacts, Workspace resource isolation, and typed watchdog events for
145
150
  stalled navigation, selected-frame detach, pending dialogs, and repeated
146
151
  browser-observable no-progress actions are also implemented. Bounded page
@@ -177,7 +182,7 @@ A project can pin Browser Pilot locally and run the same CLI without a global
177
182
  installation:
178
183
 
179
184
  ```bash
180
- npm install --save-exact browser-pilot-cli@0.2.2
185
+ npm install --save-exact browser-pilot-cli@0.3.0-rc.1
181
186
  npx --no-install browser-pilot tabs
182
187
  ```
183
188
 
@@ -205,6 +210,7 @@ an incompatible isolated Broker.
205
210
  | Command | Returns | Description |
206
211
  |---------|---------|-------------|
207
212
  | `bp open <url>` | snapshot | Navigate to URL |
213
+ | `bp open <url> --new --profile <selector>` | snapshot | Create managed work in one live Chrome Profile |
208
214
  | `bp snapshot` | snapshot | Get interactive elements |
209
215
  | `bp read [selector]` | text | Get bounded readable page/region content |
210
216
  | `bp search <text>` | matches | Find bounded visible text and nearby context |
@@ -243,8 +249,9 @@ dialogs are scoped to the compatibility Workspace and isolated from embedded
243
249
  Broker clients.
244
250
 
245
251
  Run `bp tabs` to list Pilot-managed tabs, their popups, and eligible tabs the
246
- user opened elsewhere in the same browser. `bp tab <n>` switches control to any
247
- listed tab.
252
+ user opened elsewhere across all live Profiles in the same browser endpoint.
253
+ Each JSON entry includes its opaque `profileContextId`; `bp tab <n>` switches
254
+ control to any listed tab.
248
255
 
249
256
  ### Network
250
257
 
@@ -263,8 +270,10 @@ listed tab.
263
270
 
264
271
  | Command | Description |
265
272
  |---------|-------------|
266
- | `bp connect` | Connect to Chrome, create pilot window |
273
+ | `bp connect` | Connect to Chrome; create a Pilot window immediately only when Profile routing is unambiguous |
267
274
  | `bp disconnect` | Close the CLI Pilot window; stop the daemon when no embedded client is live |
275
+ | `bp profiles` | List live Chrome Profile contexts and representative tabs |
276
+ | `bp profile <index\|id\|label\|name>` | Select a Profile for subsequent managed tabs |
268
277
  | `bp tabs` | List all controllable tabs in the current browser |
269
278
  | `bp tab <n>` | Switch to any listed managed or user tab |
270
279
  | `bp close` | Close the current tab (`--all` closes Pilot-managed tabs only) |
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { writeFileSync as writeFileSync2, readFileSync as readFileSync4, existsS
6
6
  import { resolve as resolvePath } from "path";
7
7
 
8
8
  // src/version.ts
9
- var BROWSER_PILOT_VERSION = "0.2.2";
9
+ var BROWSER_PILOT_VERSION = "0.3.0-rc.1";
10
10
 
11
11
  // src/protocol/errors.ts
12
12
  var ERROR_CODES = [
@@ -17,6 +17,9 @@ var ERROR_CODES = [
17
17
  "browser_not_authorized",
18
18
  "browser_disconnected",
19
19
  "broker_in_use",
20
+ "profile_selection_required",
21
+ "profile_context_stale",
22
+ "profile_context_unavailable",
20
23
  "workspace_not_found",
21
24
  "lease_expired",
22
25
  "target_not_owned",
@@ -1929,6 +1932,7 @@ function resultSchema(context, properties = {}, required = []) {
1929
1932
  }
1930
1933
  if (context === "target") {
1931
1934
  contextProperties.targetId = opaqueIdSchema;
1935
+ contextProperties.profileContextId = opaqueIdSchema;
1932
1936
  contextProperties.url = boundedUrl;
1933
1937
  contextRequired.push("targetId", "url");
1934
1938
  }
@@ -2008,6 +2012,27 @@ var dropdownChoiceSchema = {
2008
2012
  }, ["by", "value"])
2009
2013
  ]
2010
2014
  };
2015
+ var profileRepresentativeTabSchema = objectSchema({
2016
+ targetId: opaqueIdSchema,
2017
+ title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2018
+ url: boundedUrl
2019
+ }, ["targetId", "title", "url"]);
2020
+ var profileContextSchema = objectSchema({
2021
+ profileContextId: opaqueIdSchema,
2022
+ label: stringSchema({ minLength: 1, maxLength: 128 }),
2023
+ displayName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
2024
+ tabCount: integerSchema({ minimum: 0 }),
2025
+ eligibleTabCount: integerSchema({ minimum: 0 }),
2026
+ selected: booleanSchema(),
2027
+ representativeTabs: arraySchema(profileRepresentativeTabSchema, { maxItems: 3 })
2028
+ }, [
2029
+ "profileContextId",
2030
+ "label",
2031
+ "tabCount",
2032
+ "eligibleTabCount",
2033
+ "selected",
2034
+ "representativeTabs"
2035
+ ]);
2011
2036
  function tool(definition) {
2012
2037
  return definition;
2013
2038
  }
@@ -2067,6 +2092,40 @@ var TOOL_DEFINITIONS = [
2067
2092
  sensitivity: { input: [], output: ["browser_data"] },
2068
2093
  artifactKinds: []
2069
2094
  }),
2095
+ tool({
2096
+ name: "browser.profiles.list",
2097
+ title: "List browser Profiles",
2098
+ description: "Passively list live Profile contexts and bounded representative tabs.",
2099
+ context: "workspace",
2100
+ inputSchema: emptyInput,
2101
+ outputSchema: resultSchema("workspace", {
2102
+ profiles: arraySchema(profileContextSchema, { maxItems: 128 })
2103
+ }, ["profiles"]),
2104
+ requiredCapabilities: ["browser.control"],
2105
+ mutating: false,
2106
+ idempotency: "read_only",
2107
+ cancellation: "not_applicable",
2108
+ sensitivity: { input: [], output: ["browser_data"] },
2109
+ artifactKinds: []
2110
+ }),
2111
+ tool({
2112
+ name: "browser.profiles.select",
2113
+ title: "Select browser Profile",
2114
+ description: "Select one current Profile context for new managed targets in this Workspace.",
2115
+ context: "workspace",
2116
+ inputSchema: objectSchema({ profileContextId: opaqueIdSchema }, ["profileContextId"]),
2117
+ outputSchema: resultSchema("workspace", {
2118
+ profileContextId: opaqueIdSchema,
2119
+ label: stringSchema({ minLength: 1, maxLength: 128 }),
2120
+ displayName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data")
2121
+ }, ["profileContextId", "label"]),
2122
+ requiredCapabilities: ["browser.control"],
2123
+ mutating: true,
2124
+ idempotency: "idempotent",
2125
+ cancellation: "before_dispatch",
2126
+ sensitivity: { input: [], output: ["browser_data"] },
2127
+ artifactKinds: []
2128
+ }),
2070
2129
  tool({
2071
2130
  name: "browser.open",
2072
2131
  title: "Open URL",
@@ -2076,6 +2135,7 @@ var TOOL_DEFINITIONS = [
2076
2135
  url: boundedUrl,
2077
2136
  targetId: opaqueIdSchema,
2078
2137
  newTarget: booleanSchema(),
2138
+ profileContextId: opaqueIdSchema,
2079
2139
  observationLimit: integerSchema({ minimum: 1, maximum: OBSERVATION_V1_LIMITS.maxElements })
2080
2140
  }, ["url"]),
2081
2141
  outputSchema: observationOutput,
@@ -2481,6 +2541,7 @@ var TOOL_DEFINITIONS = [
2481
2541
  outputSchema: resultSchema("workspace", {
2482
2542
  targets: arraySchema(objectSchema({
2483
2543
  targetId: opaqueIdSchema,
2544
+ profileContextId: opaqueIdSchema,
2484
2545
  title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2485
2546
  url: boundedUrl,
2486
2547
  active: booleanSchema(),
@@ -3180,7 +3241,7 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3180
3241
  version: executableVersion,
3181
3242
  instanceId: "local:one-shot"
3182
3243
  },
3183
- protocol: { min: { major: 1, minor: 1 }, max: { major: 1, minor: 1 } },
3244
+ protocol: { min: { major: 1, minor: 1 }, max: { major: 1, minor: 2 } },
3184
3245
  requestedCapabilities: [...CAPABILITIES],
3185
3246
  launchMode: "one-shot"
3186
3247
  }), "initialize");
@@ -3229,6 +3290,24 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3229
3290
  }
3230
3291
  return result.targets;
3231
3292
  }
3293
+ async listProfiles() {
3294
+ const result = await this.callTool("browser.profiles.list");
3295
+ if (!Array.isArray(result.profiles)) {
3296
+ throw new BrowserPilotError("internal_error", "browser.profiles.list returned invalid Profiles");
3297
+ }
3298
+ return result.profiles;
3299
+ }
3300
+ async selectProfile(profileContextId) {
3301
+ const result = await this.callTool("browser.profiles.select", { profileContextId });
3302
+ const profiles = await this.listProfiles();
3303
+ const selected = profiles.find((profile) => profile.profileContextId === result.profileContextId);
3304
+ if (!selected) {
3305
+ throw new BrowserPilotError("profile_context_stale", "Selected Profile context is no longer available", {
3306
+ retryable: true
3307
+ });
3308
+ }
3309
+ return selected;
3310
+ }
3232
3311
  async ensureTarget() {
3233
3312
  const targets = await this.listTabs("all");
3234
3313
  const selected = targets.find((target) => target.active) ?? targets.find((target) => target.origin !== "user_tab");
@@ -3261,6 +3340,7 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3261
3340
  });
3262
3341
  return {
3263
3342
  targetId: opened.targetId,
3343
+ profileContextId: opened.profileContextId,
3264
3344
  title: String(opened.title ?? ""),
3265
3345
  url: String(opened.url),
3266
3346
  active: true,
@@ -3411,7 +3491,10 @@ var program = new Command();
3411
3491
  program.name("bp").description("Control your browser from the command line").version(BROWSER_PILOT_VERSION).option("--human", "force human-readable output (default when TTY)").addHelpText("after", `
3412
3492
  Workflow:
3413
3493
  bp connect # one-time setup (click Allow in Chrome)
3494
+ bp profiles # list live Chrome Profile contexts
3495
+ bp profile <index> # route new managed tabs to one Profile
3414
3496
  bp open <url> # navigate \u2014 returns snapshot with [ref] numbers
3497
+ bp open <url> --new --profile <id> # create managed work in one Profile
3415
3498
  bp click <ref> # interact \u2014 returns updated snapshot
3416
3499
  bp click --xy 400,300 # click at coordinates (canvas/maps)
3417
3500
  bp locate ".selector" # get element coordinates for click --xy
@@ -3498,7 +3581,8 @@ function emitObservation(result) {
3498
3581
  truncated,
3499
3582
  truncationReasons,
3500
3583
  ...Array.isArray(result.hints) ? { hints: result.hints } : {},
3501
- ...result.evidence && typeof result.evidence === "object" ? { evidence: result.evidence } : {}
3584
+ ...result.evidence && typeof result.evidence === "object" ? { evidence: result.evidence } : {},
3585
+ ...typeof result.profileContextId === "string" ? { profileContextId: result.profileContextId } : {}
3502
3586
  }));
3503
3587
  } else {
3504
3588
  const lines = [`[page] ${title} | ${url}`, ""];
@@ -3576,6 +3660,40 @@ async function requireCompatibility() {
3576
3660
  if (!client) throw new Error("Not connected");
3577
3661
  return client;
3578
3662
  }
3663
+ async function resolveCliProfile(client, selector) {
3664
+ const profiles = await client.listProfiles();
3665
+ const exactId = profiles.find((profile) => profile.profileContextId === selector);
3666
+ if (exactId) return exactId;
3667
+ if (/^\d+$/.test(selector)) {
3668
+ const index = Number(selector);
3669
+ if (Number.isSafeInteger(index) && profiles[index]) return profiles[index];
3670
+ throw invalidCliProfile(`Profile index out of range (0-${Math.max(0, profiles.length - 1)})`, selector);
3671
+ }
3672
+ const normalized = selector.trim().toLocaleLowerCase();
3673
+ const matches = profiles.filter((profile) => profile.label.toLocaleLowerCase() === normalized || profile.displayName?.toLocaleLowerCase() === normalized);
3674
+ if (matches.length === 1) return matches[0];
3675
+ if (matches.length > 1) {
3676
+ throw invalidCliProfile("Profile selector is ambiguous; use its index or Profile context ID", selector);
3677
+ }
3678
+ throw invalidCliProfile("Profile selector does not match a live Chrome Profile", selector);
3679
+ }
3680
+ function invalidCliProfile(message, selector) {
3681
+ return new BrowserPilotError("invalid_argument", message, {
3682
+ context: { field: "profile", selector }
3683
+ });
3684
+ }
3685
+ function cliProfile(profile, index) {
3686
+ return {
3687
+ index,
3688
+ profileContextId: profile.profileContextId,
3689
+ label: profile.label,
3690
+ ...profile.displayName ? { displayName: profile.displayName } : {},
3691
+ tabCount: profile.tabCount,
3692
+ eligibleTabCount: profile.eligibleTabCount,
3693
+ selected: profile.selected,
3694
+ representativeTabs: profile.representativeTabs
3695
+ };
3696
+ }
3579
3697
  function withCliTarget(operation) {
3580
3698
  return withCompatibilityTarget(BROWSER_PILOT_VERSION, operation);
3581
3699
  }
@@ -3611,7 +3729,7 @@ program.command("browsers").description("List supported local browsers and their
3611
3729
  return details.join("\n");
3612
3730
  }).join("\n\n"));
3613
3731
  }));
3614
- program.command("connect").description("Connect to Chrome and create pilot window").option("-b, --browser <name>", "browser to connect to").addHelpText("after", "\nExamples:\n bp connect\n bp connect --browser brave").action(action(async (opts) => {
3732
+ program.command("connect").description("Connect to Chrome and prepare unambiguous managed browsing").option("-b, --browser <name>", "browser to connect to").addHelpText("after", "\nExamples:\n bp connect\n bp connect --browser brave").action(action(async (opts) => {
3615
3733
  if (!useJson()) {
3616
3734
  console.log("Connecting to Chrome...");
3617
3735
  console.log(`If prompted, click "Allow" in Chrome's authorization dialog.
@@ -3619,10 +3737,20 @@ program.command("connect").description("Connect to Chrome and create pilot windo
3619
3737
  }
3620
3738
  const client = await connectCompatibility(BROWSER_PILOT_VERSION, opts.browser);
3621
3739
  await client.connectBrowser();
3622
- await client.ensureManagedTarget();
3740
+ const profiles = await client.listProfiles();
3741
+ if (profiles.length <= 1) await client.ensureManagedTarget();
3623
3742
  const browser = client.initialized.browsers.find((candidate) => candidate.state === "ready")?.product ?? "browser";
3743
+ if (profiles.length > 1) {
3744
+ const listed = profiles.map(cliProfile);
3745
+ emit(
3746
+ { ok: true, browser, profileSelectionRequired: true, profiles: listed },
3747
+ `\u2713 Connected to ${browser}
3748
+ Multiple Chrome Profiles are open. Run 'bp profiles', then 'bp profile <index>'.`
3749
+ );
3750
+ return;
3751
+ }
3624
3752
  emit(
3625
- { ok: true, browser },
3753
+ { ok: true, browser, profileSelectionRequired: false },
3626
3754
  `\u2713 Connected to ${browser}
3627
3755
  \u2713 Pilot window ready (daemon running in background)
3628
3756
 
@@ -3652,13 +3780,54 @@ program.command("disconnect").description("Release CLI browser state and stop an
3652
3780
  await shutdownCompatibility(BROWSER_PILOT_VERSION);
3653
3781
  emit({ ok: true }, "\u2713 Disconnected");
3654
3782
  }));
3655
- program.command("open <url>").description("Navigate to URL and return page snapshot").option("-n, --new", "open in new tab").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", "\nExamples:\n bp open https://github.com\n bp open github.com --new\n bp open https://example.com --limit 20").action(action(async (url, opts) => {
3783
+ program.command("profiles").description("List live Chrome Profile contexts").action(action(async () => {
3784
+ const profiles = (await (await requireCompatibility()).listProfiles()).map(cliProfile);
3785
+ if (useJson()) {
3786
+ console.log(JSON.stringify({ ok: true, profiles }));
3787
+ return;
3788
+ }
3789
+ if (profiles.length === 0) {
3790
+ console.log("No live Chrome Profile contexts found.");
3791
+ return;
3792
+ }
3793
+ for (const profile of profiles) {
3794
+ const name = profile.displayName ? ` (${profile.displayName})` : "";
3795
+ console.log(`${profile.selected ? "*" : " "} ${profile.index} ${profile.label}${name} ${profile.tabCount} tab(s)`);
3796
+ }
3797
+ }));
3798
+ program.command("profile <selector>").description("Select a Chrome Profile context for new managed tabs").action(action(async (selector) => {
3799
+ const client = await requireCompatibility();
3800
+ const profile = await resolveCliProfile(client, String(selector));
3801
+ const selected = await client.selectProfile(profile.profileContextId);
3802
+ emit(
3803
+ {
3804
+ ok: true,
3805
+ profileContextId: selected.profileContextId,
3806
+ label: selected.label,
3807
+ ...selected.displayName ? { displayName: selected.displayName } : {}
3808
+ },
3809
+ `\u2713 Selected ${selected.displayName ?? selected.label}`
3810
+ );
3811
+ }));
3812
+ program.command("open <url>").description("Navigate to URL and return page snapshot").option("-n, --new", "open in new tab").option("--profile <selector>", "Profile index, ID, label, or verified display name (requires --new)").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", "\nExamples:\n bp open https://github.com\n bp open github.com --new\n bp open https://example.com --new --profile 1\n bp open https://example.com --limit 20").action(action(async (url, opts) => {
3656
3813
  url = normalizeUrl(url);
3657
3814
  const limit = parseLimit(opts.limit);
3815
+ if (opts.profile && !opts.new) throw new Error("--profile requires --new");
3816
+ if (opts.new) {
3817
+ const client = await requireCompatibility();
3818
+ const profile = opts.profile ? await resolveCliProfile(client, String(opts.profile)) : void 0;
3819
+ emitObservation(await client.callTool("browser.open", {
3820
+ url,
3821
+ newTarget: true,
3822
+ ...profile ? { profileContextId: profile.profileContextId } : {},
3823
+ observationLimit: limit
3824
+ }));
3825
+ return;
3826
+ }
3658
3827
  await withCliTarget(async (client, target) => {
3659
3828
  const result = await client.callTool("browser.open", {
3660
3829
  url,
3661
- ...opts.new ? { newTarget: true } : { targetId: target.targetId },
3830
+ targetId: target.targetId,
3662
3831
  observationLimit: limit
3663
3832
  });
3664
3833
  emitObservation(result);