browser-pilot-cli 0.3.0-rc.6 → 0.4.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/dist/cli.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command } from "commander";
4
+ import { Command, CommanderError } from "commander";
5
5
  import { writeFileSync as writeFileSync2, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
6
6
  import { resolve as resolvePath } from "path";
7
7
 
8
8
  // src/version.ts
9
- var BROWSER_PILOT_VERSION = "0.3.0-rc.6";
9
+ var BROWSER_PILOT_VERSION = "0.4.0";
10
10
 
11
11
  // src/protocol/errors.ts
12
12
  var ERROR_CODES = [
@@ -911,7 +911,7 @@ async function discoverBrowserCandidates(options = {}) {
911
911
  id: stableBrowserId(definition, os),
912
912
  product: definition.product,
913
913
  channel: definition.channel,
914
- profile: definition.dataDir,
914
+ userDataRoot: definition.dataDir,
915
915
  processState,
916
916
  remoteDebuggingState,
917
917
  authorizationState,
@@ -923,324 +923,6 @@ async function discoverBrowserCandidates(options = {}) {
923
923
  return results;
924
924
  }
925
925
 
926
- // src/page-scripts.ts
927
- var CLICK_TARGET_STATE_FUNCTION = `function() {
928
- const target = this;
929
- if (!target || target.nodeType !== 1 || !target.isConnected) {
930
- return {connected:false, kind:'other', focused:false};
931
- }
932
- const composedParent = node => {
933
- if (node && node.parentElement) return node.parentElement;
934
- const root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
935
- return root && root.host ? root.host : null;
936
- };
937
- const composedContains = (container, node) => {
938
- for (let current = node; current; current = composedParent(current)) {
939
- if (current === container) return true;
940
- }
941
- return false;
942
- };
943
- const isShadowHostOf = (node, possibleHost) => {
944
- let root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
945
- while (root && root.host) {
946
- if (root.host === possibleHost) return true;
947
- root = typeof root.host.getRootNode === 'function' ? root.host.getRootNode() : null;
948
- }
949
- return false;
950
- };
951
- const token = (value, mixed) => {
952
- const normalized = String(value || '').trim().toLowerCase();
953
- if (normalized === 'true') return true;
954
- if (normalized === 'false') return false;
955
- return mixed && normalized === 'mixed' ? 'mixed' : undefined;
956
- };
957
- const tag = String(target.tagName || '').toLowerCase();
958
- const role = String(target.getAttribute && target.getAttribute('role') || '').trim().toLowerCase();
959
- const inputType = tag === 'input' ? String(target.type || '').toLowerCase() : '';
960
- let kind = 'other';
961
- if (inputType === 'checkbox' || role === 'checkbox' || role === 'menuitemcheckbox') kind = 'checkbox';
962
- else if (inputType === 'radio' || role === 'radio' || role === 'menuitemradio') kind = 'radio';
963
- else if (role === 'switch') kind = 'switch';
964
- else if (tag === 'option' || role === 'option') kind = 'option';
965
- else if (tag === 'select' || role === 'listbox' || role === 'combobox') kind = 'select';
966
- else if (
967
- tag === 'button' || tag === 'a' || tag === 'input' || tag === 'textarea' ||
968
- role === 'button' || role === 'link' || role === 'tab' || role === 'menuitem'
969
- ) kind = 'control';
970
-
971
- const state = {connected:true, kind, focused:false};
972
- if ((kind === 'checkbox' || kind === 'radio') && tag === 'input') {
973
- state.checked = !!target.checked;
974
- } else if (kind === 'checkbox' || kind === 'radio' || kind === 'switch') {
975
- const checked = token(target.getAttribute('aria-checked'), true);
976
- if (checked !== undefined) state.checked = checked;
977
- }
978
- if (kind === 'option' && tag === 'option') state.selected = !!target.selected;
979
- else {
980
- const selected = token(target.getAttribute('aria-selected'), false);
981
- if (selected !== undefined) state.selected = selected === true;
982
- }
983
- const pressed = token(target.getAttribute('aria-pressed'), true);
984
- if (pressed !== undefined) state.pressed = pressed;
985
- const expanded = token(target.getAttribute('aria-expanded'), false);
986
- if (expanded !== undefined) state.expanded = expanded === true;
987
-
988
- let active = document.activeElement;
989
- for (let depth = 0; depth < 32 && active && active.shadowRoot && active.shadowRoot.activeElement; depth += 1) {
990
- active = active.shadowRoot.activeElement;
991
- }
992
- state.focused = active === target || composedContains(target, active) || isShadowHostOf(target, active);
993
- return state;
994
- }`;
995
- var GET_POINTER_TARGET_STATE = `function() {
996
- const blocked = (reason, obstruction) => ({
997
- status: 'blocked',
998
- reason,
999
- ...(obstruction ? {obstruction} : {}),
1000
- });
1001
- const composedParent = node => {
1002
- if (node && node.parentElement) return node.parentElement;
1003
- const root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
1004
- return root && root.host ? root.host : null;
1005
- };
1006
- const composedContains = (container, node) => {
1007
- for (let current = node; current; current = composedParent(current)) {
1008
- if (current === container) return true;
1009
- }
1010
- return false;
1011
- };
1012
- const isShadowHostOf = (node, possibleHost) => {
1013
- let root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
1014
- while (root && root.host) {
1015
- if (root.host === possibleHost) return true;
1016
- root = typeof root.host.getRootNode === 'function' ? root.host.getRootNode() : null;
1017
- }
1018
- return false;
1019
- };
1020
- const describes = node => {
1021
- if (!node || typeof node.tagName !== 'string') return undefined;
1022
- const tagName = node.tagName.toLowerCase().slice(0, 40);
1023
- const rawRole = typeof node.getAttribute === 'function' ? node.getAttribute('role') : null;
1024
- const role = typeof rawRole === 'string' ? rawRole.trim().slice(0, 40) : '';
1025
- return role ? {tagName, role} : {tagName};
1026
- };
1027
-
1028
- const target = this;
1029
- if (!target || target.nodeType !== 1 || !target.isConnected) return blocked('detached');
1030
- target.scrollIntoView({block:'center', inline:'center', behavior:'instant'});
1031
- if (!target.isConnected) return blocked('detached');
1032
-
1033
- const style = getComputedStyle(target);
1034
- if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') {
1035
- return blocked('no_layout');
1036
- }
1037
-
1038
- for (let current = target; current; current = composedParent(current)) {
1039
- const ariaDisabled = typeof current.getAttribute === 'function'
1040
- ? current.getAttribute('aria-disabled') : null;
1041
- const nativeDisabled = typeof current.matches === 'function' && current.matches(':disabled');
1042
- if (nativeDisabled || current.inert === true || String(ariaDisabled).trim().toLowerCase() === 'true') {
1043
- return blocked('disabled');
1044
- }
1045
- }
1046
-
1047
- const viewportWidth = Math.max(0, window.innerWidth || document.documentElement.clientWidth || 0);
1048
- const viewportHeight = Math.max(0, window.innerHeight || document.documentElement.clientHeight || 0);
1049
- const rectList = target.getClientRects();
1050
- const rects = [];
1051
- for (let index = 0; index < Math.min(rectList.length, 64); index += 1) {
1052
- const rect = rectList[index];
1053
- if (
1054
- Number.isFinite(rect.left) && Number.isFinite(rect.top) &&
1055
- Number.isFinite(rect.right) && Number.isFinite(rect.bottom) &&
1056
- rect.width > 0 && rect.height > 0
1057
- ) rects.push(rect);
1058
- }
1059
- if (rects.length === 0) return blocked('no_layout');
1060
-
1061
- const visibleRects = rects.map(rect => ({
1062
- left: Math.max(0, rect.left),
1063
- top: Math.max(0, rect.top),
1064
- right: Math.min(viewportWidth, rect.right),
1065
- bottom: Math.min(viewportHeight, rect.bottom),
1066
- })).filter(rect => rect.right > rect.left && rect.bottom > rect.top)
1067
- .sort((a, b) => ((b.right - b.left) * (b.bottom - b.top)) - ((a.right - a.left) * (a.bottom - a.top)))
1068
- .slice(0, 4);
1069
- if (visibleRects.length === 0) return blocked('outside_viewport');
1070
-
1071
- const labels = [];
1072
- if (target.labels && typeof target.labels.length === 'number') {
1073
- for (let index = 0; index < Math.min(target.labels.length, 32); index += 1) {
1074
- labels.push(target.labels[index]);
1075
- }
1076
- }
1077
- for (let current = target; current; current = composedParent(current)) {
1078
- if (String(current.tagName).toLowerCase() === 'label') labels.push(current);
1079
- }
1080
- const accepts = hit => {
1081
- if (!hit) return false;
1082
- if (hit === target || composedContains(target, hit) || isShadowHostOf(target, hit)) return true;
1083
- return labels.some(label => (
1084
- hit === label || composedContains(label, hit) || isShadowHostOf(label, hit)
1085
- ));
1086
- };
1087
-
1088
- let firstObstruction;
1089
- for (const rect of visibleRects) {
1090
- const width = rect.right - rect.left;
1091
- const height = rect.bottom - rect.top;
1092
- const points = [
1093
- [rect.left + width * 0.5, rect.top + height * 0.5],
1094
- [rect.left + width * 0.25, rect.top + height * 0.25],
1095
- [rect.left + width * 0.75, rect.top + height * 0.25],
1096
- [rect.left + width * 0.25, rect.top + height * 0.75],
1097
- [rect.left + width * 0.75, rect.top + height * 0.75],
1098
- ];
1099
- for (const [x, y] of points) {
1100
- const hit = document.elementsFromPoint(x, y)[0];
1101
- if (accepts(hit)) {
1102
- const readClickTargetState = ${CLICK_TARGET_STATE_FUNCTION};
1103
- return {status:'ready', x, y, targetState:readClickTargetState.call(target)};
1104
- }
1105
- if (!firstObstruction && hit) firstObstruction = describes(hit);
1106
- }
1107
- }
1108
- return blocked('obscured', firstObstruction);
1109
- }`;
1110
- var EDITABLE_STATE_FUNCTION = `function(target) {
1111
- const unsupported = (reason, inputType) => ({
1112
- kind:'unsupported', value:'', sensitive:false, editable:false,
1113
- ...(inputType ? {inputType} : {}), reason,
1114
- });
1115
- if (!target || target.nodeType !== 1 || !target.isConnected) return unsupported('detached');
1116
-
1117
- const composedParent = node => {
1118
- if (node && node.parentElement) return node.parentElement;
1119
- const root = node && typeof node.getRootNode === 'function' ? node.getRootNode() : null;
1120
- return root && root.host ? root.host : null;
1121
- };
1122
- const hasComposedState = (node, property, attribute) => {
1123
- for (let current = node; current; current = composedParent(current)) {
1124
- if (current[property] === true || current.hasAttribute?.(attribute)) return true;
1125
- }
1126
- return false;
1127
- };
1128
- const ariaTrue = (node, name) => String(node.getAttribute?.(name) || '').trim().toLowerCase() === 'true';
1129
- const blockedReason = node => {
1130
- if (hasComposedState(node, 'inert', 'inert')) return 'inert';
1131
- for (let current = node; current; current = composedParent(current)) {
1132
- if (current.matches?.(':disabled') || current.disabled === true || ariaTrue(current, 'aria-disabled')) {
1133
- return 'disabled';
1134
- }
1135
- }
1136
- for (let current = node; current; current = composedParent(current)) {
1137
- if (current.readOnly === true || ariaTrue(current, 'aria-readonly')) return 'readonly';
1138
- }
1139
- return null;
1140
- };
1141
- const contenteditableRoot = node => {
1142
- let root = node;
1143
- while (root.parentElement && root.parentElement.isContentEditable) root = root.parentElement;
1144
- return root;
1145
- };
1146
-
1147
- if (target instanceof HTMLInputElement) {
1148
- const inputType = String(target.type || 'text').toLowerCase();
1149
- const rangeTextTypes = new Set(['text', 'search', 'tel', 'url', 'password']);
1150
- const selectTextTypes = new Set(['email', 'number']);
1151
- const valueTypes = new Set(['date', 'time', 'datetime-local', 'month', 'week', 'color', 'range']);
1152
- const editMode = rangeTextTypes.has(inputType) || selectTextTypes.has(inputType)
1153
- ? 'text' : valueTypes.has(inputType) ? 'value' : null;
1154
- if (!editMode) return unsupported('unsupported_input_type', inputType);
1155
- const reason = blockedReason(target);
1156
- return {
1157
- kind:'input', value:String(target.value ?? ''), sensitive:inputType === 'password',
1158
- editable:!reason, inputType, editMode,
1159
- ...(editMode === 'text' ? {selectionMode:selectTextTypes.has(inputType) ? 'select' : 'range'} : {}),
1160
- ...(reason ? {reason} : {}),
1161
- };
1162
- }
1163
- if (target instanceof HTMLTextAreaElement) {
1164
- const reason = blockedReason(target);
1165
- return {
1166
- kind:'input', value:String(target.value ?? ''), sensitive:false,
1167
- editable:!reason, inputType:'textarea', editMode:'text', selectionMode:'range',
1168
- ...(reason ? {reason} : {}),
1169
- };
1170
- }
1171
- if (target.isContentEditable) {
1172
- const root = contenteditableRoot(target);
1173
- const reason = blockedReason(root);
1174
- return {
1175
- kind:'contenteditable', value:String(root.innerText ?? root.textContent ?? ''), sensitive:false,
1176
- editable:!reason, inputType:'contenteditable', editMode:'text', selectionMode:'range',
1177
- ...(reason ? {reason} : {}),
1178
- };
1179
- }
1180
- return unsupported('unsupported_element');
1181
- }`;
1182
- var PREPARE_EDITABLE_TARGET = `function(clear) {
1183
- const readState = ${EDITABLE_STATE_FUNCTION};
1184
- const state = readState(this);
1185
- if (!state.editable) return state;
1186
-
1187
- if (state.kind === 'input') {
1188
- this.focus();
1189
- const focusedState = readState(this);
1190
- if (!focusedState.editable) return focusedState;
1191
- if (focusedState.editMode === 'text') {
1192
- if (focusedState.selectionMode === 'range') {
1193
- const end = String(this.value ?? '').length;
1194
- this.setSelectionRange(clear ? 0 : end, end, 'none');
1195
- } else if (clear) {
1196
- this.select();
1197
- }
1198
- }
1199
- return focusedState;
1200
- }
1201
-
1202
- let root = this;
1203
- while (root.parentElement && root.parentElement.isContentEditable) root = root.parentElement;
1204
- root.focus();
1205
- const focusedState = readState(root);
1206
- if (!focusedState.editable) return focusedState;
1207
- const range = document.createRange();
1208
- range.selectNodeContents(root);
1209
- if (!clear) range.collapse(false);
1210
- const selection = window.getSelection();
1211
- if (!selection) return {...focusedState, editable:false, reason:'selection_unavailable'};
1212
- selection.removeAllRanges();
1213
- selection.addRange(range);
1214
- return focusedState;
1215
- }`;
1216
- var SET_VALUE_CONTROL = `function(value) {
1217
- const readState = ${EDITABLE_STATE_FUNCTION};
1218
- const state = readState(this);
1219
- if (!state.editable || state.kind !== 'input' || state.editMode !== 'value') return;
1220
- this.focus();
1221
- const beforeInput = new InputEvent('beforeinput', {
1222
- bubbles:true, composed:true, cancelable:true, inputType:'insertReplacementText', data:value,
1223
- });
1224
- if (!this.dispatchEvent(beforeInput)) return;
1225
- const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
1226
- if (setter) setter.call(this, value); else this.value = value;
1227
- this.dispatchEvent(new InputEvent('input', {
1228
- bubbles:true, composed:true, inputType:'insertReplacementText', data:value,
1229
- }));
1230
- }`;
1231
- var READ_EDITABLE_STATE = `function() {
1232
- const readState = ${EDITABLE_STATE_FUNCTION};
1233
- return readState(this);
1234
- }`;
1235
- var READ_ACTIVE_EDITABLE_STATE = `(() => {
1236
- const readState = ${EDITABLE_STATE_FUNCTION};
1237
- let active = document.activeElement;
1238
- while (active && active.shadowRoot && active.shadowRoot.activeElement) {
1239
- active = active.shadowRoot.activeElement;
1240
- }
1241
- return readState(active);
1242
- })()`;
1243
-
1244
926
  // src/runtime-layout.ts
1245
927
  import { fileURLToPath } from "url";
1246
928
  function isSelfContainedRuntime() {
@@ -1459,6 +1141,12 @@ var CAPABILITIES = [
1459
1141
  "cookies.read",
1460
1142
  "developer.eval"
1461
1143
  ];
1144
+ var PROFILE_IDENTITY_ERROR_CODES = [
1145
+ "profile_path_unavailable",
1146
+ "profile_path_unverified",
1147
+ "local_state_unavailable",
1148
+ "profile_metadata_missing"
1149
+ ];
1462
1150
  var OBSERVATION_TRUNCATION_REASONS = [
1463
1151
  "element_limit",
1464
1152
  "text_limit",
@@ -2020,6 +1708,12 @@ var profileRepresentativeTabSchema = objectSchema({
2020
1708
  var profileContextSchema = objectSchema({
2021
1709
  profileContextId: opaqueIdSchema,
2022
1710
  label: stringSchema({ minLength: 1, maxLength: 128 }),
1711
+ identityStatus: stringSchema({ enum: ["unidentified", "verified", "unavailable"] }),
1712
+ profileName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1713
+ accountName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1714
+ accountEmail: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1715
+ profileDirectory: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1716
+ identityErrorCode: stringSchema({ enum: [...PROFILE_IDENTITY_ERROR_CODES] }),
2023
1717
  displayName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
2024
1718
  tabCount: integerSchema({ minimum: 0 }),
2025
1719
  eligibleTabCount: integerSchema({ minimum: 0 }),
@@ -2048,6 +1742,7 @@ var TOOL_DEFINITIONS = [
2048
1742
  id: opaqueIdSchema,
2049
1743
  product: stringSchema({ minLength: 1, maxLength: 128 }),
2050
1744
  channel: stringSchema({ maxLength: 128 }),
1745
+ userDataRoot: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2051
1746
  profile: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2052
1747
  processState: stringSchema({ enum: ["running", "not_running", "unknown"] }),
2053
1748
  remoteDebuggingState: stringSchema({ enum: ["enabled", "disabled", "stale"] }),
@@ -2108,6 +1803,25 @@ var TOOL_DEFINITIONS = [
2108
1803
  sensitivity: { input: [], output: ["browser_data"] },
2109
1804
  artifactKinds: []
2110
1805
  }),
1806
+ tool({
1807
+ name: "browser.profiles.identify",
1808
+ title: "Identify browser Profiles",
1809
+ description: "Explicitly identify live Chrome Profiles using temporary visible chrome://version targets.",
1810
+ context: "workspace",
1811
+ inputSchema: objectSchema({
1812
+ profileContextId: opaqueIdSchema,
1813
+ refresh: booleanSchema()
1814
+ }),
1815
+ outputSchema: resultSchema("workspace", {
1816
+ profiles: arraySchema(profileContextSchema, { maxItems: 128 })
1817
+ }, ["profiles"]),
1818
+ requiredCapabilities: ["browser.control"],
1819
+ mutating: true,
1820
+ idempotency: "non_idempotent",
1821
+ cancellation: "best_effort",
1822
+ sensitivity: { input: [], output: ["browser_data"] },
1823
+ artifactKinds: []
1824
+ }),
2111
1825
  tool({
2112
1826
  name: "browser.profiles.select",
2113
1827
  title: "Select browser Profile",
@@ -2117,6 +1831,12 @@ var TOOL_DEFINITIONS = [
2117
1831
  outputSchema: resultSchema("workspace", {
2118
1832
  profileContextId: opaqueIdSchema,
2119
1833
  label: stringSchema({ minLength: 1, maxLength: 128 }),
1834
+ identityStatus: stringSchema({ enum: ["unidentified", "verified", "unavailable"] }),
1835
+ profileName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1836
+ accountName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1837
+ accountEmail: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1838
+ profileDirectory: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data"),
1839
+ identityErrorCode: stringSchema({ enum: [...PROFILE_IDENTITY_ERROR_CODES] }),
2120
1840
  displayName: sensitive(stringSchema({ minLength: 1, maxLength: 4096 }), "browser_data")
2121
1841
  }, ["profileContextId", "label"]),
2122
1842
  requiredCapabilities: ["browser.control"],
@@ -2545,10 +2265,11 @@ var TOOL_DEFINITIONS = [
2545
2265
  title: sensitive(stringSchema({ maxLength: 4096 }), "browser_data"),
2546
2266
  url: boundedUrl,
2547
2267
  active: booleanSchema(),
2268
+ selected: booleanSchema(),
2548
2269
  origin: stringSchema({ enum: ["managed", "managed_popup", "user_tab"] }),
2549
2270
  managedTabSetId: opaqueIdSchema,
2550
2271
  controlState: stringSchema({ enum: ["available", "controlled", "busy"] })
2551
- }, ["targetId", "title", "url", "active", "origin", "controlState"]))
2272
+ }, ["targetId", "title", "url", "origin", "controlState"]))
2552
2273
  }, ["targets"]),
2553
2274
  requiredCapabilities: ["browser.control"],
2554
2275
  mutating: false,
@@ -2560,7 +2281,7 @@ var TOOL_DEFINITIONS = [
2560
2281
  tool({
2561
2282
  name: "browser.tabs.switch",
2562
2283
  title: "Switch tab",
2563
- description: "Set a managed or user tab as the active controlled target for the current Lease.",
2284
+ description: "Select a managed or user tab as the controlled target for the current Lease.",
2564
2285
  context: "workspace",
2565
2286
  inputSchema: objectSchema({ targetId: opaqueIdSchema }, ["targetId"]),
2566
2287
  outputSchema: resultSchema("target"),
@@ -3207,10 +2928,23 @@ async function runStdioBridge(options) {
3207
2928
  }
3208
2929
 
3209
2930
  // src/compatibility-broker-client.ts
3210
- import { randomUUID as randomUUID6 } from "crypto";
2931
+ import { createHash as createHash4, randomUUID as randomUUID6 } from "crypto";
3211
2932
  var BRIDGE_SESSION_ID = "bridge:browser-pilot-cli";
3212
2933
  var CLIENT_KEY = "browser-pilot-cli";
3213
2934
  var LEASE_TTL_MS = 5 * 6e4;
2935
+ function compatibilityIdentity(clientKey) {
2936
+ if (clientKey === CLIENT_KEY) {
2937
+ return { bridgeSessionId: BRIDGE_SESSION_ID, instanceId: "local:one-shot" };
2938
+ }
2939
+ const digest = createHash4("sha256").update(clientKey).digest("base64url").slice(0, 24);
2940
+ return {
2941
+ bridgeSessionId: `bridge:browser-pilot-cli:${digest}`,
2942
+ instanceId: `local:one-shot:${digest}`
2943
+ };
2944
+ }
2945
+ function isSelected(target) {
2946
+ return target.selected ?? target.active === true;
2947
+ }
3214
2948
  function asRecord(value, label) {
3215
2949
  if (!value || typeof value !== "object" || Array.isArray(value)) {
3216
2950
  throw new BrowserPilotError("internal_error", `${label} returned an invalid result`);
@@ -3226,22 +2960,24 @@ function commandResult(value, method) {
3226
2960
  return asRecord(outcome.result, `${method} result`);
3227
2961
  }
3228
2962
  var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3229
- constructor(transport, initialized, workspace, lease) {
2963
+ constructor(transport, bridgeSessionId, initialized, workspace, lease) {
3230
2964
  this.transport = transport;
2965
+ this.bridgeSessionId = bridgeSessionId;
3231
2966
  this.initialized = initialized;
3232
2967
  this.workspace = workspace;
3233
2968
  this.lease = lease;
3234
2969
  }
3235
2970
  commandSequence = 0;
3236
- static async create(transport, executableVersion) {
3237
- const initialized = asRecord(await transport.brokerCall(BRIDGE_SESSION_ID, "initialize", {
2971
+ static async create(transport, executableVersion, clientKey = CLIENT_KEY) {
2972
+ const identity = compatibilityIdentity(clientKey);
2973
+ const initialized = asRecord(await transport.brokerCall(identity.bridgeSessionId, "initialize", {
3238
2974
  client: {
3239
2975
  id: "org.browser-pilot.cli",
3240
2976
  name: "Browser Pilot CLI",
3241
2977
  version: executableVersion,
3242
- instanceId: "local:one-shot"
2978
+ instanceId: identity.instanceId
3243
2979
  },
3244
- protocol: { min: { major: 1, minor: 1 }, max: { major: 1, minor: 2 } },
2980
+ protocol: { min: { major: 1, minor: 1 }, max: { major: 1, minor: 3 } },
3245
2981
  requestedCapabilities: [...CAPABILITIES],
3246
2982
  launchMode: "one-shot"
3247
2983
  }), "initialize");
@@ -3254,19 +2990,25 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3254
2990
  }
3255
2991
  });
3256
2992
  }
3257
- const created = asRecord(await transport.brokerCall(BRIDGE_SESSION_ID, "workspaces/create", {
3258
- clientKey: CLIENT_KEY
2993
+ const created = asRecord(await transport.brokerCall(identity.bridgeSessionId, "workspaces/create", {
2994
+ clientKey
3259
2995
  }), "workspaces/create");
3260
- const leased = asRecord(await transport.brokerCall(BRIDGE_SESSION_ID, "leases/create", {
2996
+ const leased = asRecord(await transport.brokerCall(identity.bridgeSessionId, "leases/create", {
3261
2997
  workspaceId: created.workspace.id,
3262
- clientKey: CLIENT_KEY,
2998
+ clientKey,
3263
2999
  ttlMs: LEASE_TTL_MS
3264
3000
  }), "leases/create");
3265
- return new _CompatibilityBrokerClient(transport, initialized, created.workspace, leased.lease);
3001
+ return new _CompatibilityBrokerClient(
3002
+ transport,
3003
+ identity.bridgeSessionId,
3004
+ initialized,
3005
+ created.workspace,
3006
+ leased.lease
3007
+ );
3266
3008
  }
3267
3009
  async callTool(name, args = {}, targetId) {
3268
3010
  this.commandSequence += 1;
3269
- return commandResult(await this.transport.brokerCall(BRIDGE_SESSION_ID, "tools/call", {
3011
+ return commandResult(await this.transport.brokerCall(this.bridgeSessionId, "tools/call", {
3270
3012
  name,
3271
3013
  arguments: args,
3272
3014
  workspaceId: this.workspace.id,
@@ -3304,6 +3046,16 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3304
3046
  }
3305
3047
  return result.profiles;
3306
3048
  }
3049
+ async identifyProfiles(profileContextId, refresh = false) {
3050
+ const result = await this.callTool("browser.profiles.identify", {
3051
+ ...profileContextId ? { profileContextId } : {},
3052
+ ...refresh ? { refresh: true } : {}
3053
+ });
3054
+ if (!Array.isArray(result.profiles)) {
3055
+ throw new BrowserPilotError("internal_error", "browser.profiles.identify returned invalid Profiles");
3056
+ }
3057
+ return result.profiles;
3058
+ }
3307
3059
  async selectProfile(profileContextId) {
3308
3060
  const result = await this.callTool("browser.profiles.select", { profileContextId });
3309
3061
  const profiles = await this.listProfiles();
@@ -3317,11 +3069,11 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3317
3069
  }
3318
3070
  async ensureTarget() {
3319
3071
  const targets = await this.listTabs("all");
3320
- const selected = targets.find((target) => target.active) ?? targets.find((target) => target.origin !== "user_tab");
3072
+ const selected = targets.find(isSelected) ?? targets.find((target) => target.origin !== "user_tab");
3321
3073
  if (selected) {
3322
- if (!selected.active) {
3074
+ if (!isSelected(selected)) {
3323
3075
  await this.callTool("browser.tabs.switch", { targetId: selected.targetId });
3324
- selected.active = true;
3076
+ selected.selected = true;
3325
3077
  }
3326
3078
  return selected;
3327
3079
  }
@@ -3329,11 +3081,11 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3329
3081
  }
3330
3082
  async ensureManagedTarget() {
3331
3083
  const targets = await this.listTabs("managed_only");
3332
- const selected = targets.find((target) => target.active) ?? targets[0];
3084
+ const selected = targets.find(isSelected) ?? targets[0];
3333
3085
  if (selected) {
3334
- if (!selected.active) {
3086
+ if (!isSelected(selected)) {
3335
3087
  await this.callTool("browser.tabs.switch", { targetId: selected.targetId });
3336
- selected.active = true;
3088
+ selected.selected = true;
3337
3089
  }
3338
3090
  return selected;
3339
3091
  }
@@ -3350,7 +3102,7 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3350
3102
  profileContextId: opened.profileContextId,
3351
3103
  title: String(opened.title ?? ""),
3352
3104
  url: String(opened.url),
3353
- active: true,
3105
+ selected: true,
3354
3106
  origin: "managed",
3355
3107
  controlState: "controlled"
3356
3108
  };
@@ -3363,7 +3115,7 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3363
3115
  return result.observationId;
3364
3116
  }
3365
3117
  async importArtifact(path, mimeType) {
3366
- const result = asRecord(await this.transport.brokerCall(BRIDGE_SESSION_ID, "artifacts/import", {
3118
+ const result = asRecord(await this.transport.brokerCall(this.bridgeSessionId, "artifacts/import", {
3367
3119
  workspaceId: this.workspace.id,
3368
3120
  leaseId: this.lease.id,
3369
3121
  path,
@@ -3372,7 +3124,7 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3372
3124
  return asRecord(result.artifact, "imported Artifact");
3373
3125
  }
3374
3126
  async exportArtifact(artifactId, path) {
3375
- await this.transport.brokerCall(BRIDGE_SESSION_ID, "artifacts/export", {
3127
+ await this.transport.brokerCall(this.bridgeSessionId, "artifacts/export", {
3376
3128
  workspaceId: this.workspace.id,
3377
3129
  leaseId: this.lease.id,
3378
3130
  artifactId,
@@ -3381,14 +3133,14 @@ var CompatibilityBrokerClient = class _CompatibilityBrokerClient {
3381
3133
  });
3382
3134
  }
3383
3135
  async releaseArtifact(artifactId) {
3384
- await this.transport.brokerCall(BRIDGE_SESSION_ID, "artifacts/release", {
3136
+ await this.transport.brokerCall(this.bridgeSessionId, "artifacts/release", {
3385
3137
  workspaceId: this.workspace.id,
3386
3138
  leaseId: this.lease.id,
3387
3139
  artifactId
3388
3140
  });
3389
3141
  }
3390
3142
  async releaseWorkspace() {
3391
- await this.transport.brokerCall(BRIDGE_SESSION_ID, "workspaces/release", {
3143
+ await this.transport.brokerCall(this.bridgeSessionId, "workspaces/release", {
3392
3144
  workspaceId: this.workspace.id
3393
3145
  });
3394
3146
  }
@@ -3427,29 +3179,29 @@ async function validateCompatibilityDaemon(client, executableVersion) {
3427
3179
  });
3428
3180
  }
3429
3181
  }
3430
- async function connectCompatibility(executableVersion, browserFilter) {
3182
+ async function connectCompatibility(executableVersion, browserFilter, clientKey = CLIENT_KEY) {
3431
3183
  const daemon = await connectDaemon(browserFilter);
3432
3184
  await validateCompatibilityDaemon(daemon, executableVersion);
3433
- return CompatibilityBrokerClient.create(daemon, executableVersion);
3185
+ return CompatibilityBrokerClient.create(daemon, executableVersion, clientKey);
3434
3186
  }
3435
- async function resumeCompatibility(executableVersion) {
3187
+ async function resumeCompatibility(executableVersion, clientKey = CLIENT_KEY) {
3436
3188
  if (!isDaemonRunning()) return null;
3437
3189
  const daemon = new DaemonClient();
3438
3190
  try {
3439
3191
  await validateCompatibilityDaemon(daemon, executableVersion);
3440
- return await CompatibilityBrokerClient.create(daemon, executableVersion);
3192
+ return await CompatibilityBrokerClient.create(daemon, executableVersion, clientKey);
3441
3193
  } catch (error) {
3442
3194
  if (error instanceof BrowserPilotError && error.code === "protocol_incompatible") throw error;
3443
3195
  return null;
3444
3196
  }
3445
3197
  }
3446
- async function withCompatibilityTarget(executableVersion, operation) {
3447
- const client = await resumeCompatibility(executableVersion);
3198
+ async function withCompatibilityTarget(executableVersion, operation, clientKey = CLIENT_KEY) {
3199
+ const client = await resumeCompatibility(executableVersion, clientKey);
3448
3200
  if (!client) throw new Error("Not connected");
3449
3201
  const target = await client.ensureTarget();
3450
3202
  return operation(client, target);
3451
3203
  }
3452
- async function shutdownCompatibility(executableVersion) {
3204
+ async function shutdownCompatibility(executableVersion, clientKey = CLIENT_KEY) {
3453
3205
  if (!isDaemonRunning()) return;
3454
3206
  const daemon = new DaemonClient();
3455
3207
  await validateDaemon(daemon);
@@ -3481,11 +3233,15 @@ async function shutdownCompatibility(executableVersion) {
3481
3233
  });
3482
3234
  }
3483
3235
  try {
3484
- const client = await CompatibilityBrokerClient.create(daemon, executableVersion);
3236
+ const client = await CompatibilityBrokerClient.create(daemon, executableVersion, clientKey);
3485
3237
  await client.releaseWorkspace();
3486
3238
  } catch (error) {
3487
3239
  if (!(error instanceof BrowserPilotError) || error.code !== "browser_not_found") throw error;
3488
3240
  }
3241
+ const afterRelease = await daemon.healthInfo();
3242
+ if ((afterRelease.clients?.embeddedConnections ?? 0) > 0 || (afterRelease.clients?.activeLeases ?? 0) > 0) {
3243
+ return;
3244
+ }
3489
3245
  await daemon.shutdown({
3490
3246
  brokerProcessIdentity: health.brokerProcessIdentity,
3491
3247
  executableVersion: requester.version,
@@ -3495,7 +3251,8 @@ async function shutdownCompatibility(executableVersion) {
3495
3251
 
3496
3252
  // src/cli.ts
3497
3253
  var program = new Command();
3498
- 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", `
3254
+ program.exitOverride();
3255
+ program.name("bp").description("Control your browser from the command line").version(BROWSER_PILOT_VERSION).option("--human", "force human-readable output (default when TTY)").option("--client-key <key>", "stable namespace for one-shot Agent state").addHelpText("after", `
3499
3256
  Workflow:
3500
3257
  bp connect # one-time setup (click Allow in Chrome)
3501
3258
  bp profiles # list live Chrome Profile contexts
@@ -3525,7 +3282,7 @@ Output:
3525
3282
  JSON by default when piped (for LLM/script use).
3526
3283
  Human-readable when run in a terminal (TTY). Force with --human.
3527
3284
  Actions return: {"ok":true, "title":"...", "url":"...", "elements":[...]}
3528
- Errors return: {"ok":false, "error":"...", "hint":"..."}
3285
+ Errors return: {"ok":false, "error":"...", "code":"...", "retryable":false}
3529
3286
 
3530
3287
  Canvas editors (Google Docs, Sheets, Figma):
3531
3288
  bp keyboard "text" --click ".editor" # click to focus, then type
@@ -3551,19 +3308,28 @@ Eval (escape hatch for operations without a dedicated command):
3551
3308
  echo 'complex js here' | bp eval # stdin for complex JS
3552
3309
  `);
3553
3310
  function useJson() {
3554
- if (program.opts().human) return false;
3311
+ if (process.argv.slice(2).includes("--human") || program.opts().human) return false;
3555
3312
  return !process.stdout.isTTY;
3556
3313
  }
3314
+ program.configureOutput({
3315
+ writeErr: (value) => {
3316
+ if (!useJson()) process.stderr.write(value);
3317
+ }
3318
+ });
3557
3319
  function emit(data, human) {
3558
3320
  if (useJson()) console.log(JSON.stringify(data));
3559
3321
  else if (human) console.log(human);
3560
3322
  }
3561
3323
  function fail(error, hint, details) {
3324
+ const stable = details ?? new BrowserPilotError("internal_error", error);
3562
3325
  if (useJson()) console.log(JSON.stringify({
3563
3326
  ok: false,
3564
3327
  error,
3328
+ code: stable.code,
3329
+ retryable: stable.retryable,
3565
3330
  ...hint ? { hint } : {},
3566
- ...details ? details.toData() : {}
3331
+ ...stable.context ? { context: stable.context } : {},
3332
+ ...stable.remediation ? { remediation: stable.remediation } : {}
3567
3333
  }));
3568
3334
  else console.error(`\u2717 ${error}${hint ? `
3569
3335
  hint: ${hint}` : ""}`);
@@ -3617,13 +3383,17 @@ function emitObservation(result) {
3617
3383
  function action(fn) {
3618
3384
  return (...args) => fn(...args).catch((err) => {
3619
3385
  const msg = err instanceof Error ? err.message : String(err);
3620
- if (err instanceof BrowserPilotError && err.code === "stale_ref") {
3621
- fail(msg, "Run 'bp snapshot' to refresh element refs.", err);
3386
+ const stable = err instanceof BrowserPilotError ? err : new BrowserPilotError("internal_error", msg, { cause: err });
3387
+ if (stable.code === "stale_ref") {
3388
+ fail(msg, "Run 'bp snapshot' to refresh element refs.", stable);
3622
3389
  }
3623
- if (msg.includes("not found") && msg.includes("Ref")) fail(msg, "Run 'bp snapshot' to refresh element refs.", err instanceof BrowserPilotError ? err : void 0);
3624
- if (msg.includes("Not connected")) fail(msg, "Run 'bp connect' first.", err instanceof BrowserPilotError ? err : void 0);
3625
- if (msg.includes("Page load timeout")) fail(msg, "Page may still be loading. Retry the command after a moment.", err instanceof BrowserPilotError ? err : void 0);
3626
- fail(msg, void 0, err instanceof BrowserPilotError ? err : void 0);
3390
+ if (stable.code === "browser_disconnected") {
3391
+ fail(msg, "Run 'bp connect' first.", stable);
3392
+ }
3393
+ if (stable.code === "unknown_outcome") {
3394
+ fail(msg, "Inspect the current tab state before deciding whether to retry.", stable);
3395
+ }
3396
+ fail(msg, void 0, stable);
3627
3397
  });
3628
3398
  }
3629
3399
  function normalizeUrl(url) {
@@ -3631,17 +3401,42 @@ function normalizeUrl(url) {
3631
3401
  return `https://${url}`;
3632
3402
  }
3633
3403
  function parseLimit(raw) {
3634
- const n = parseInt(raw, 10);
3635
- if (isNaN(n) || n < 1) throw new Error("--limit must be a positive number");
3404
+ const n = Number(raw);
3405
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(n) || n < 1) {
3406
+ throw invalidArgument("--limit must be a positive integer", "limit");
3407
+ }
3636
3408
  return n;
3637
3409
  }
3638
3410
  function parseRef(raw) {
3639
3411
  const ref = Number(raw);
3640
- if (!Number.isSafeInteger(ref) || ref < 1) {
3641
- throw new Error("Ref must be a positive integer");
3412
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(ref) || ref < 1) {
3413
+ throw invalidArgument("Ref must be a positive integer", "ref");
3642
3414
  }
3643
3415
  return ref;
3644
3416
  }
3417
+ function parseCoordinates(raw) {
3418
+ const parts = raw.split(",");
3419
+ if (parts.length !== 2 || parts.some((part) => part.trim() === "")) {
3420
+ throw invalidArgument("--xy must be x,y (e.g. --xy 400,300)", "xy");
3421
+ }
3422
+ const [x, y] = parts.map((part) => Number(part.trim()));
3423
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
3424
+ throw invalidArgument("--xy must be x,y (e.g. --xy 400,300)", "xy");
3425
+ }
3426
+ return { x, y };
3427
+ }
3428
+ function cliClientKey() {
3429
+ const value = program.opts().clientKey ?? process.env.BROWSER_PILOT_CLIENT_KEY;
3430
+ if (value === void 0) return "browser-pilot-cli";
3431
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(value)) {
3432
+ throw new BrowserPilotError(
3433
+ "invalid_argument",
3434
+ "Client key must be 3-128 characters using letters, digits, dot, underscore, colon, or hyphen",
3435
+ { context: { field: "clientKey" } }
3436
+ );
3437
+ }
3438
+ return value;
3439
+ }
3645
3440
  function requireString2(value, label) {
3646
3441
  if (typeof value !== "string") throw new BrowserPilotError("internal_error", `${label} is missing`);
3647
3442
  return value;
@@ -3659,12 +3454,21 @@ async function cliElementAddress(client, target, raw) {
3659
3454
  ref: parseRef(raw)
3660
3455
  };
3661
3456
  }
3662
- if (!raw.trim()) throw new Error("Element target must not be empty");
3457
+ if (!raw.trim()) throw invalidArgument("Element target must not be empty", "target");
3663
3458
  return { selector: raw };
3664
3459
  }
3665
3460
  async function requireCompatibility() {
3666
- const client = await resumeCompatibility(BROWSER_PILOT_VERSION);
3667
- if (!client) throw new Error("Not connected");
3461
+ const client = await resumeCompatibility(BROWSER_PILOT_VERSION, cliClientKey());
3462
+ if (!client) {
3463
+ throw new BrowserPilotError("browser_disconnected", "Browser Pilot is not connected", {
3464
+ retryable: true,
3465
+ remediation: {
3466
+ code: "connect_browser",
3467
+ message: "Run 'bp connect', complete Chrome authorization if prompted, then retry.",
3468
+ actionRequired: true
3469
+ }
3470
+ });
3471
+ }
3668
3472
  return client;
3669
3473
  }
3670
3474
  async function resolveCliProfile(client, selector) {
@@ -3673,11 +3477,11 @@ async function resolveCliProfile(client, selector) {
3673
3477
  if (exactId) return exactId;
3674
3478
  if (/^\d+$/.test(selector)) {
3675
3479
  const index = Number(selector);
3676
- if (Number.isSafeInteger(index) && profiles[index]) return profiles[index];
3677
- throw invalidCliProfile(`Profile index out of range (0-${Math.max(0, profiles.length - 1)})`, selector);
3480
+ if (Number.isSafeInteger(index) && index >= 1 && profiles[index - 1]) return profiles[index - 1];
3481
+ throw invalidCliProfile(`Profile index out of range (1-${profiles.length})`, selector);
3678
3482
  }
3679
3483
  const normalized = selector.trim().toLocaleLowerCase();
3680
- const matches = profiles.filter((profile) => profile.label.toLocaleLowerCase() === normalized || profile.displayName?.toLocaleLowerCase() === normalized);
3484
+ const matches = profiles.filter((profile) => profile.label.toLocaleLowerCase() === normalized || profile.displayName?.toLocaleLowerCase() === normalized || profile.profileName?.toLocaleLowerCase() === normalized || profile.accountName?.toLocaleLowerCase() === normalized || profile.accountEmail?.toLocaleLowerCase() === normalized);
3681
3485
  if (matches.length === 1) return matches[0];
3682
3486
  if (matches.length > 1) {
3683
3487
  throw invalidCliProfile("Profile selector is ambiguous; use its index or Profile context ID", selector);
@@ -3691,10 +3495,16 @@ function invalidCliProfile(message, selector) {
3691
3495
  }
3692
3496
  function cliProfile(profile, index) {
3693
3497
  return {
3694
- index,
3498
+ index: index + 1,
3695
3499
  profileContextId: profile.profileContextId,
3696
3500
  label: profile.label,
3697
3501
  ...profile.displayName ? { displayName: profile.displayName } : {},
3502
+ ...profile.identityStatus ? { identityStatus: profile.identityStatus } : {},
3503
+ ...profile.profileName ? { profileName: profile.profileName } : {},
3504
+ ...profile.accountName ? { accountName: profile.accountName } : {},
3505
+ ...profile.accountEmail ? { accountEmail: profile.accountEmail } : {},
3506
+ ...profile.profileDirectory ? { profileDirectory: profile.profileDirectory } : {},
3507
+ ...profile.identityErrorCode ? { identityErrorCode: profile.identityErrorCode } : {},
3698
3508
  tabCount: profile.tabCount,
3699
3509
  eligibleTabCount: profile.eligibleTabCount,
3700
3510
  selected: profile.selected,
@@ -3702,7 +3512,7 @@ function cliProfile(profile, index) {
3702
3512
  };
3703
3513
  }
3704
3514
  function withCliTarget(operation) {
3705
- return withCompatibilityTarget(BROWSER_PILOT_VERSION, operation);
3515
+ return withCompatibilityTarget(BROWSER_PILOT_VERSION, operation, cliClientKey());
3706
3516
  }
3707
3517
  function readStdin() {
3708
3518
  if (process.stdin.isTTY) return Promise.resolve("");
@@ -3716,7 +3526,7 @@ function readStdin() {
3716
3526
  }
3717
3527
  program.command("browsers").description("List supported local browsers and their setup state").option("-b, --browser <name>", "filter by browser ID, product, or channel").action(action(async (opts) => {
3718
3528
  const filter = typeof opts.browser === "string" ? opts.browser.toLowerCase() : void 0;
3719
- const client = await resumeCompatibility(BROWSER_PILOT_VERSION);
3529
+ const client = await resumeCompatibility(BROWSER_PILOT_VERSION, cliClientKey());
3720
3530
  const browsers = client ? await client.listBrowsers(filter) : (await discoverBrowserCandidates()).map((discovered) => discovered.candidate).filter((candidate) => !filter || [candidate.id, candidate.product, candidate.channel].some((value) => value?.toLowerCase().includes(filter)));
3721
3531
  if (useJson()) {
3722
3532
  console.log(JSON.stringify({ ok: true, browsers }));
@@ -3743,7 +3553,7 @@ program.command("connect").description("Connect to Chrome and prepare unambiguou
3743
3553
  console.log(`If prompted, click "Allow" in Chrome's authorization dialog.
3744
3554
  `);
3745
3555
  }
3746
- const client = await connectCompatibility(BROWSER_PILOT_VERSION, opts.browser);
3556
+ const client = await connectCompatibility(BROWSER_PILOT_VERSION, opts.browser, cliClientKey());
3747
3557
  await client.connectBrowser();
3748
3558
  const profiles = await client.listProfiles();
3749
3559
  if (profiles.length <= 1) await client.ensureManagedTarget();
@@ -3753,7 +3563,7 @@ program.command("connect").description("Connect to Chrome and prepare unambiguou
3753
3563
  emit(
3754
3564
  { ok: true, browser, profileSelectionRequired: true, profiles: listed },
3755
3565
  `\u2713 Connected to ${browser}
3756
- Multiple Chrome Profiles are open. Run 'bp profiles', then 'bp profile <index>'.`
3566
+ Multiple Chrome Profiles are open. Run 'bp profiles --identify', then 'bp profile <index>'.`
3757
3567
  );
3758
3568
  return;
3759
3569
  }
@@ -3785,11 +3595,13 @@ program.command("bridge").description("Run the Agent-neutral JSON-RPC bridge").o
3785
3595
  });
3786
3596
  });
3787
3597
  program.command("disconnect").description("Release CLI browser state and stop an otherwise unused daemon").action(action(async () => {
3788
- await shutdownCompatibility(BROWSER_PILOT_VERSION);
3598
+ await shutdownCompatibility(BROWSER_PILOT_VERSION, cliClientKey());
3789
3599
  emit({ ok: true }, "\u2713 Disconnected");
3790
3600
  }));
3791
- program.command("profiles").description("List live Chrome Profile contexts").action(action(async () => {
3792
- const profiles = (await (await requireCompatibility()).listProfiles()).map(cliProfile);
3601
+ program.command("profiles").description("List live Chrome Profile contexts").option("--identify", "explicitly identify Profiles using temporary visible Chrome pages").option("--refresh", "repeat Profile identification instead of using cached results").action(action(async (opts) => {
3602
+ const client = await requireCompatibility();
3603
+ const identified = opts.identify || opts.refresh ? await client.identifyProfiles(void 0, opts.refresh === true) : await client.listProfiles();
3604
+ const profiles = identified.map(cliProfile);
3793
3605
  if (useJson()) {
3794
3606
  console.log(JSON.stringify({ ok: true, profiles }));
3795
3607
  return;
@@ -3799,8 +3611,10 @@ program.command("profiles").description("List live Chrome Profile contexts").act
3799
3611
  return;
3800
3612
  }
3801
3613
  for (const profile of profiles) {
3802
- const name = profile.displayName ? ` (${profile.displayName})` : "";
3803
- console.log(`${profile.selected ? "*" : " "} ${profile.index} ${profile.label}${name} ${profile.tabCount} tab(s)`);
3614
+ const name = profile.profileName ?? profile.displayName;
3615
+ const account = profile.accountEmail ?? profile.accountName;
3616
+ const identity = name ? ` ${name}${account ? ` (${account})` : ""} [${profile.label}]` : ` ${profile.label}${profile.identityStatus === "unavailable" ? " (identity unavailable)" : ""}`;
3617
+ console.log(`${profile.selected ? "*" : " "} ${profile.index}${identity} ${profile.tabCount} tab(s)`);
3804
3618
  }
3805
3619
  }));
3806
3620
  program.command("profile <selector>").description("Select a Chrome Profile context for new managed tabs").action(action(async (selector) => {
@@ -3812,15 +3626,21 @@ program.command("profile <selector>").description("Select a Chrome Profile conte
3812
3626
  ok: true,
3813
3627
  profileContextId: selected.profileContextId,
3814
3628
  label: selected.label,
3815
- ...selected.displayName ? { displayName: selected.displayName } : {}
3629
+ ...selected.displayName ? { displayName: selected.displayName } : {},
3630
+ ...selected.identityStatus ? { identityStatus: selected.identityStatus } : {},
3631
+ ...selected.profileName ? { profileName: selected.profileName } : {},
3632
+ ...selected.accountName ? { accountName: selected.accountName } : {},
3633
+ ...selected.accountEmail ? { accountEmail: selected.accountEmail } : {},
3634
+ ...selected.profileDirectory ? { profileDirectory: selected.profileDirectory } : {},
3635
+ ...selected.identityErrorCode ? { identityErrorCode: selected.identityErrorCode } : {}
3816
3636
  },
3817
- `\u2713 Selected ${selected.displayName ?? selected.label}`
3637
+ `\u2713 Selected ${selected.profileName ?? selected.displayName ?? selected.label}`
3818
3638
  );
3819
3639
  }));
3820
- 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) => {
3640
+ 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, verified name, or email (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) => {
3821
3641
  url = normalizeUrl(url);
3822
3642
  const limit = parseLimit(opts.limit);
3823
- if (opts.profile && !opts.new) throw new Error("--profile requires --new");
3643
+ if (opts.profile && !opts.new) throw invalidArgument("--profile requires --new", "profile");
3824
3644
  if (opts.new) {
3825
3645
  const client = await requireCompatibility();
3826
3646
  const profile = opts.profile ? await resolveCliProfile(client, String(opts.profile)) : void 0;
@@ -3854,20 +3674,22 @@ Examples:
3854
3674
  bp click --xy 400,300 --double # double-click at coordinates
3855
3675
  bp click --xy 400,300 --right # right-click (context menu)
3856
3676
  bp click 3 --right # right-click element [3]`).action(action(async (ref, opts) => {
3857
- if (opts.double && opts.right) throw new Error("--double and --right are mutually exclusive");
3858
- if (!ref && !opts.xy) throw new Error("Provide a ref number or --xy coordinates");
3677
+ if (opts.double && opts.right) {
3678
+ throw invalidArgument("--double and --right are mutually exclusive", "button");
3679
+ }
3680
+ if (!ref && !opts.xy) throw invalidArgument("Provide a ref number or --xy coordinates", "target");
3681
+ if (ref && opts.xy) throw invalidArgument("Provide either a ref number or --xy, not both", "target");
3859
3682
  const limit = parseLimit(opts.limit);
3683
+ const coordinates = opts.xy ? parseCoordinates(opts.xy) : void 0;
3684
+ const parsedRef = ref ? parseRef(ref) : void 0;
3860
3685
  await withCliTarget(async (client, controlledTarget) => {
3861
3686
  let target;
3862
- if (opts.xy) {
3863
- const [xStr, yStr] = opts.xy.split(",");
3864
- const x = parseFloat(xStr), y = parseFloat(yStr);
3865
- if (isNaN(x) || isNaN(y)) throw new Error("--xy must be x,y (e.g. --xy 400,300)");
3866
- target = { x, y };
3687
+ if (coordinates) {
3688
+ target = coordinates;
3867
3689
  } else {
3868
3690
  target = {
3869
3691
  observationId: await client.latestObservation(controlledTarget.targetId),
3870
- ref: parseRef(ref)
3692
+ ref: parsedRef
3871
3693
  };
3872
3694
  }
3873
3695
  const result = await client.callTool("browser.click", {
@@ -3906,10 +3728,11 @@ Examples:
3906
3728
  }));
3907
3729
  program.command("type <ref> <text>").description("Type text into element and return page snapshot").option("-c, --clear", "clear field before typing").option("-s, --submit", "press Enter after typing").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", '\nExamples:\n bp type 2 "hello world"\n bp type 5 "query" --submit\n bp type 3 "new value" --clear').action(action(async (ref, text, opts) => {
3908
3730
  const limit = parseLimit(opts.limit);
3731
+ const parsedRef = parseRef(ref);
3909
3732
  await withCliTarget(async (client, target) => {
3910
3733
  const result = await client.callTool("browser.type", {
3911
3734
  observationId: await client.latestObservation(target.targetId),
3912
- ref: parseRef(ref),
3735
+ ref: parsedRef,
3913
3736
  text,
3914
3737
  ...opts.clear ? { clear: true } : {},
3915
3738
  ...opts.submit ? { submit: true } : {},
@@ -3934,8 +3757,10 @@ Examples:
3934
3757
  const limit = parseLimit(opts.limit);
3935
3758
  let delay2 = 0;
3936
3759
  if (opts.delay) {
3937
- delay2 = parseInt(opts.delay, 10);
3938
- if (isNaN(delay2) || delay2 < 0) throw new Error("--delay must be a non-negative number");
3760
+ delay2 = Number(opts.delay);
3761
+ if (!/^\d+$/.test(opts.delay) || !Number.isSafeInteger(delay2) || delay2 < 0) {
3762
+ throw invalidArgument("--delay must be a non-negative integer", "delay");
3763
+ }
3939
3764
  }
3940
3765
  await withCliTarget(async (client, target) => {
3941
3766
  const result = await client.callTool("browser.keyboard", {
@@ -3982,7 +3807,9 @@ Examples:
3982
3807
  echo 'complex js' | bp eval`).action(action(async (expression) => {
3983
3808
  if (!expression) {
3984
3809
  expression = await readStdin();
3985
- if (!expression) throw new Error("No expression. Pass as argument or pipe via stdin.");
3810
+ if (!expression) {
3811
+ throw invalidArgument("No expression. Pass as argument or pipe via stdin.", "expression");
3812
+ }
3986
3813
  }
3987
3814
  await withCliTarget(async (client, target) => {
3988
3815
  const result = await client.callTool("browser.eval", {
@@ -4014,8 +3841,10 @@ When to use which command:
4014
3841
  bp snapshot \u2192 list interactive elements (buttons, links, inputs)
4015
3842
  bp read \u2192 page text content (search results, articles)
4016
3843
  bp eval \u2192 custom extraction (structured data, attributes)`).action(action(async (selector, opts) => {
4017
- const limit = parseInt(opts.limit, 10);
4018
- if (isNaN(limit) || limit < 1) throw new Error("--limit must be a positive integer");
3844
+ const limit = Number(opts.limit);
3845
+ if (!/^\d+$/.test(opts.limit) || !Number.isSafeInteger(limit) || limit < 1) {
3846
+ throw invalidArgument("--limit must be a positive integer", "limit");
3847
+ }
4019
3848
  await withCliTarget(async (client, target) => {
4020
3849
  let data;
4021
3850
  try {
@@ -4096,15 +3925,25 @@ Examples:
4096
3925
  bp scroll down --selector ".results"
4097
3926
  bp scroll --ref 8 --to end
4098
3927
  bp scroll --to-text "Payment details"`).action(action(async (direction, opts) => {
4099
- if (opts.selector && opts.ref) throw new Error("--selector and --ref are mutually exclusive");
3928
+ if (opts.selector && opts.ref) {
3929
+ throw invalidArgument("--selector and --ref are mutually exclusive", "target");
3930
+ }
4100
3931
  const modes = [direction !== void 0, opts.to !== void 0, opts.toText !== void 0].filter(Boolean).length;
4101
- if (modes > 1) throw new Error("Use only one of direction, --to, or --to-text");
3932
+ if (modes > 1) throw invalidArgument("Use only one of direction, --to, or --to-text", "mode");
4102
3933
  const validDirections = /* @__PURE__ */ new Set(["up", "down", "left", "right"]);
4103
- if (direction !== void 0 && !validDirections.has(direction)) throw new Error("direction must be up, down, left, or right");
4104
- if (opts.to !== void 0 && !["start", "end"].includes(opts.to)) throw new Error("--to must be start or end");
4105
- if (!["pixels", "viewport"].includes(opts.unit)) throw new Error("--unit must be pixels or viewport");
3934
+ if (direction !== void 0 && !validDirections.has(direction)) {
3935
+ throw invalidArgument("direction must be up, down, left, or right", "direction");
3936
+ }
3937
+ if (opts.to !== void 0 && !["start", "end"].includes(opts.to)) {
3938
+ throw invalidArgument("--to must be start or end", "to");
3939
+ }
3940
+ if (!["pixels", "viewport"].includes(opts.unit)) {
3941
+ throw invalidArgument("--unit must be pixels or viewport", "unit");
3942
+ }
4106
3943
  const amount = opts.amount === void 0 ? void 0 : Number(opts.amount);
4107
- if (amount !== void 0 && (!Number.isFinite(amount) || amount <= 0)) throw new Error("--amount must be a positive number");
3944
+ if (amount !== void 0 && (!Number.isFinite(amount) || amount <= 0)) {
3945
+ throw invalidArgument("--amount must be a positive number", "amount");
3946
+ }
4108
3947
  const limit = parseLimit(opts.limit);
4109
3948
  await withCliTarget(async (client, target) => {
4110
3949
  const rawTarget = opts.selector ?? opts.ref;
@@ -4141,7 +3980,9 @@ program.command("dropdown <target>").description("List native or ARIA dropdown o
4141
3980
  });
4142
3981
  }));
4143
3982
  program.command("select <target> <option>").description("Select and verify a dropdown option").option("--by <mode>", "match by label, value, or index", "label").option("--contains", "use case-insensitive substring matching").option("-l, --limit <n>", "max elements in returned snapshot", "50").addHelpText("after", '\nExamples:\n bp select 4 "United States"\n bp select 4 us --by value\n bp select 4 3 --by index').action(action(async (rawTarget, rawOption, opts) => {
4144
- if (!["label", "value", "index"].includes(opts.by)) throw new Error("--by must be label, value, or index");
3983
+ if (!["label", "value", "index"].includes(opts.by)) {
3984
+ throw invalidArgument("--by must be label, value, or index", "by");
3985
+ }
4145
3986
  const choice = opts.by === "index" ? { by: "index", index: parseRef(rawOption) } : { by: opts.by, [opts.by]: rawOption, exact: opts.contains !== true };
4146
3987
  const limit = parseLimit(opts.limit);
4147
3988
  await withCliTarget(async (client, target) => {
@@ -4155,9 +3996,11 @@ program.command("select <target> <option>").description("Select and verify a dro
4155
3996
  }));
4156
3997
  program.command("upload <filepath>").description('Upload file (auto-finds <input type="file"> on the page)').option("--nth <n>", "which file input to use if multiple exist", "1").addHelpText("after", "\nAuto-detects file inputs on the page. No ref needed.\n\nExamples:\n bp upload ./photo.jpg\n bp upload /tmp/resume.pdf\n bp upload ./doc.pdf --nth 2 # if multiple file inputs").action(action(async (filepath, opts) => {
4157
3998
  const absPath = resolvePath(filepath);
4158
- if (!existsSync4(absPath)) throw new Error(`File not found: ${absPath}`);
4159
- const inputIndex = parseInt(opts.nth, 10);
4160
- if (isNaN(inputIndex) || inputIndex < 1) throw new Error("--nth must be a positive integer");
3999
+ if (!existsSync4(absPath)) throw invalidArgument(`File not found: ${absPath}`, "filepath");
4000
+ const inputIndex = Number(opts.nth);
4001
+ if (!/^\d+$/.test(opts.nth) || !Number.isSafeInteger(inputIndex) || inputIndex < 1) {
4002
+ throw invalidArgument("--nth must be a positive integer", "nth");
4003
+ }
4161
4004
  await withCliTarget(async (client, target) => {
4162
4005
  const artifact = await client.importArtifact(absPath);
4163
4006
  try {
@@ -4175,7 +4018,7 @@ program.command("upload <filepath>").description('Upload file (auto-finds <input
4175
4018
  }));
4176
4019
  program.command("screenshot [filename]").description("Capture screenshot").option("-f, --full", "capture full page").option("--selector <sel>", "capture specific element").option("--annotate [refs]", "draw Observation ref boxes; optionally comma-separated refs").addHelpText("after", '\nExamples:\n bp screenshot\n bp screenshot page.png\n bp screenshot --full\n bp screenshot --selector ".chart"\n bp screenshot page.png --annotate\n bp screenshot page.png --annotate 1,3,8').action(action(async (filename, opts) => {
4177
4020
  if (opts.annotate !== void 0 && (opts.full || opts.selector)) {
4178
- throw new Error("--annotate cannot be combined with --full or --selector");
4021
+ throw invalidArgument("--annotate cannot be combined with --full or --selector", "annotate");
4179
4022
  }
4180
4023
  await withCliTarget(async (client, target) => {
4181
4024
  let annotations;
@@ -4263,9 +4106,12 @@ program.command("frame [target]").description("List frames, or switch to a frame
4263
4106
  }
4264
4107
  }
4265
4108
  } else {
4266
- const idx = parseInt(target, 10);
4109
+ const idx = Number(target);
4267
4110
  if (!Number.isSafeInteger(idx) || idx < 0 || idx >= frames.length) {
4268
- throw new Error(`Frame index out of range (0-${Math.max(0, frames.length - 1)})`);
4111
+ throw invalidArgument(
4112
+ `Frame index out of range (0-${Math.max(0, frames.length - 1)})`,
4113
+ "target"
4114
+ );
4269
4115
  }
4270
4116
  const frame = frames[idx];
4271
4117
  await client.callTool("browser.frames.switch", idx === 0 ? { top: true } : { frameId: requireString2(frame.frameId, "frameId") }, controlledTarget.targetId);
@@ -4283,7 +4129,7 @@ program.command("auth [username] [password]").description("Set or clear HTTP Bas
4283
4129
  emit({ ok: true }, "\u2713 Auth credentials cleared");
4284
4130
  return;
4285
4131
  }
4286
- if (!password) throw new Error("Usage: bp auth <username> <password>");
4132
+ if (!password) throw invalidArgument("Usage: bp auth <username> <password>", "password");
4287
4133
  await client.callTool("browser.auth.set", { username, password });
4288
4134
  emit({ ok: true }, "\u2713 Auth credentials set (scoped to HTTP 401 challenges)");
4289
4135
  }));
@@ -4302,7 +4148,7 @@ program.command("dialogs").description("List pending JavaScript dialogs").action
4302
4148
  }));
4303
4149
  program.command("dialog <dialogId>").description("Accept or dismiss a pending JavaScript dialog").option("--accept", "accept the dialog").option("--dismiss", "dismiss the dialog").option("--prompt <text>", "text to submit to a prompt dialog").action(action(async (dialogId, opts) => {
4304
4150
  if (Boolean(opts.accept) === Boolean(opts.dismiss)) {
4305
- throw new Error("Choose exactly one of --accept or --dismiss");
4151
+ throw invalidArgument("Choose exactly one of --accept or --dismiss", "action");
4306
4152
  }
4307
4153
  const client = await requireCompatibility();
4308
4154
  const listed = await client.callTool("browser.dialogs.list");
@@ -4324,9 +4170,10 @@ program.command("dialog <dialogId>").description("Accept or dismiss a pending Ja
4324
4170
  }));
4325
4171
  program.command("tabs").description("List all controllable browser tabs").action(action(async () => {
4326
4172
  const targets = await (await requireCompatibility()).listTabs("all");
4327
- const tabs = targets.map(({ targetId: _targetId, managedTabSetId: _managedTabSetId, controlState, ...tab }, index) => ({
4328
- index,
4173
+ const tabs = targets.map(({ targetId: _targetId, managedTabSetId: _managedTabSetId, controlState, active, selected, ...tab }, index) => ({
4174
+ index: index + 1,
4329
4175
  ...tab,
4176
+ selected: selected ?? active === true,
4330
4177
  controlState
4331
4178
  }));
4332
4179
  if (useJson()) {
@@ -4334,20 +4181,20 @@ program.command("tabs").description("List all controllable browser tabs").action
4334
4181
  } else if (tabs.length === 0) {
4335
4182
  console.log("No controllable tabs open.");
4336
4183
  } else {
4337
- for (const t of tabs) console.log(`${t.active ? "*" : " "} ${t.index} ${t.url} ${t.title}`);
4184
+ for (const t of tabs) console.log(`${t.selected ? "*" : " "} ${t.index} ${t.url} ${t.title}`);
4338
4185
  }
4339
4186
  }));
4340
- program.command("tab <index>").description("Switch to tab by index").action(action(async (indexStr) => {
4187
+ program.command("tab <index>").description("Select a tab by one-based index").action(action(async (indexStr) => {
4341
4188
  const client = await requireCompatibility();
4342
- const index = parseInt(indexStr, 10);
4189
+ const index = Number(indexStr);
4343
4190
  const targets = await client.listTabs("all");
4344
- if (!Number.isSafeInteger(index) || index < 0 || index >= targets.length) {
4345
- throw new Error(`Tab index out of range (0-${Math.max(0, targets.length - 1)})`);
4191
+ if (!Number.isSafeInteger(index) || index < 1 || index > targets.length) {
4192
+ throw invalidArgument(`Tab index out of range (1-${targets.length})`, "index");
4346
4193
  }
4347
- await client.callTool("browser.tabs.switch", { targetId: targets[index].targetId });
4348
- emit({ ok: true, index }, `\u2713 Switched to tab ${index}`);
4194
+ await client.callTool("browser.tabs.switch", { targetId: targets[index - 1].targetId });
4195
+ emit({ ok: true, index }, `\u2713 Selected tab ${index}`);
4349
4196
  }));
4350
- program.command("close").description("Close current browser tab").option("-a, --all", "close all tabs in the current Pilot window").action(action(async (opts) => {
4197
+ program.command("close").description("Close current browser tab").option("-a, --all", "close all managed tabs in this Agent Workspace").action(action(async (opts) => {
4351
4198
  const client = await requireCompatibility();
4352
4199
  if (opts.all) {
4353
4200
  const managed = await client.listTabs("managed_only");
@@ -4377,7 +4224,7 @@ program.command("close").description("Close current browser tab").option("-a, --
4377
4224
  await client.callTool("browser.tabs.close", {}, target.targetId);
4378
4225
  const remainingTabs = await client.listTabs("all");
4379
4226
  if (remainingTabs.length > 0) {
4380
- if (!remainingTabs.some((tab) => tab.active)) {
4227
+ if (!remainingTabs.some((tab) => tab.selected ?? tab.active === true)) {
4381
4228
  const fallback = remainingTabs.find((tab) => tab.origin !== "user_tab");
4382
4229
  if (fallback) await client.callTool("browser.tabs.switch", { targetId: fallback.targetId });
4383
4230
  }
@@ -4404,7 +4251,7 @@ function cliNetworkRequest(request) {
4404
4251
  }
4405
4252
  async function findNetworkRequest(client, sequence) {
4406
4253
  if (!Number.isSafeInteger(sequence) || sequence < 1) {
4407
- throw new Error("Request ID must be a positive integer");
4254
+ throw invalidArgument("Request ID must be a positive integer", "id");
4408
4255
  }
4409
4256
  const listed = await client.callTool("browser.network.requests", {
4410
4257
  after: sequence - 1,
@@ -4428,14 +4275,18 @@ Examples:
4428
4275
  bp net rules # list active rules
4429
4276
  bp net remove --all # clear rules`).action(action(async (opts) => {
4430
4277
  const client = await requireCompatibility();
4431
- const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
4278
+ const limit = parseLimit(opts.limit ?? "20");
4279
+ const after = opts.after === void 0 ? void 0 : Number(opts.after);
4280
+ if (after !== void 0 && (!/^\d+$/.test(opts.after) || !Number.isSafeInteger(after))) {
4281
+ throw invalidArgument("--after must be a non-negative integer", "after");
4282
+ }
4432
4283
  const result = await client.callTool("browser.network.requests", {
4433
4284
  limit,
4434
4285
  ...opts.url ? { url: opts.url } : {},
4435
4286
  ...opts.method ? { method: opts.method } : {},
4436
4287
  ...opts.status ? { status: opts.status } : {},
4437
4288
  ...opts.type ? { type: String(opts.type).split(",").map((value) => value.trim()).filter(Boolean) } : {},
4438
- ...opts.after ? { after: parseInt(opts.after, 10) } : {}
4289
+ ...after !== void 0 ? { after } : {}
4439
4290
  });
4440
4291
  const requests = networkRequests(result).map(cliNetworkRequest);
4441
4292
  if (useJson()) {
@@ -4459,7 +4310,7 @@ Examples:
4459
4310
  }));
4460
4311
  netCmd.command("show <id>").description("Show full request/response details").option("--save <file>", "save response body to file").action(action(async (idStr, opts) => {
4461
4312
  const client = await requireCompatibility();
4462
- const id = parseInt(idStr, 10);
4313
+ const id = Number(idStr);
4463
4314
  const summary = await findNetworkRequest(client, id);
4464
4315
  const result = await client.callTool("browser.network.request", {
4465
4316
  requestId: requireString2(summary.requestId, "requestId"),
@@ -4468,7 +4319,13 @@ netCmd.command("show <id>").description("Show full request/response details").op
4468
4319
  const request = result.request && typeof result.request === "object" && !Array.isArray(result.request) ? result.request : {};
4469
4320
  const responseBody = typeof result.body === "string" ? result.body : void 0;
4470
4321
  if (opts.save) {
4471
- if (responseBody === void 0) throw new Error(`Response body for request #${id} is unavailable`);
4322
+ if (responseBody === void 0) {
4323
+ throw new BrowserPilotError(
4324
+ "action_not_verified",
4325
+ `Response body for request #${id} is unavailable`,
4326
+ { context: { sequence: id } }
4327
+ );
4328
+ }
4472
4329
  const outputPath = resolvePath(opts.save);
4473
4330
  const bytes = result.bodyEncoding === "base64" ? Buffer.from(responseBody, "base64") : Buffer.from(responseBody, "utf8");
4474
4331
  writeFileSync2(outputPath, bytes);
@@ -4497,15 +4354,18 @@ netCmd.command("block <pattern>").description("Block requests matching URL patte
4497
4354
  const rule = { id: result.ruleId, type: "block", pattern };
4498
4355
  emit({ ok: true, rule }, `Rule #${rule.id}: blocking "${pattern}"`);
4499
4356
  }));
4500
- netCmd.command("mock <pattern>").description("Mock responses for matching URLs").option("--body <json>", "response body").option("--file <path>", "read body from file").option("--status <code>", "HTTP status", "200").action(action(async (pattern, opts) => {
4357
+ netCmd.command("mock <pattern>").description("Mock responses for matching URLs").option("--body <text>", "response body text").option("--file <path>", "read body from file").option("--status <code>", "HTTP status", "200").action(action(async (pattern, opts) => {
4501
4358
  const client = await requireCompatibility();
4502
4359
  let body = opts.body || "";
4503
4360
  if (opts.file) {
4504
4361
  const filePath = resolvePath(opts.file);
4505
- if (!existsSync4(filePath)) throw new Error(`File not found: ${filePath}`);
4362
+ if (!existsSync4(filePath)) throw invalidArgument(`File not found: ${filePath}`, "file");
4506
4363
  body = readFileSync4(filePath, "utf-8");
4507
4364
  }
4508
- const status = parseInt(opts.status, 10);
4365
+ const status = Number(opts.status);
4366
+ if (!/^\d+$/.test(opts.status) || !Number.isSafeInteger(status) || status < 100 || status > 599) {
4367
+ throw invalidArgument("--status must be an HTTP status from 100 through 599", "status");
4368
+ }
4509
4369
  const result = await client.callTool("browser.network.rules.add", {
4510
4370
  type: "mock",
4511
4371
  pattern,
@@ -4552,10 +4412,32 @@ netCmd.command("remove [ruleId]").description("Remove interception rule(s)").opt
4552
4412
  } else if (ruleId) {
4553
4413
  await client.callTool("browser.network.rules.remove", { ruleId });
4554
4414
  emit({ ok: true }, `Rule #${ruleId} removed`);
4555
- } else throw new Error("Specify a rule ID or use --all");
4415
+ } else throw invalidArgument("Specify a rule ID or use --all", "ruleId");
4556
4416
  }));
4557
4417
  netCmd.command("clear").description("Clear captured request log").action(action(async () => {
4558
4418
  await (await requireCompatibility()).callTool("browser.network.clear");
4559
4419
  emit({ ok: true }, "Request log cleared");
4560
4420
  }));
4561
- program.parse();
4421
+ async function main() {
4422
+ try {
4423
+ await program.parseAsync();
4424
+ } catch (error) {
4425
+ if (error instanceof CommanderError) {
4426
+ if (error.exitCode === 0) {
4427
+ process.exitCode = 0;
4428
+ } else if (useJson()) {
4429
+ const message = error.message.replace(/^error:\s*/i, "");
4430
+ const stable = new BrowserPilotError("invalid_argument", message, {
4431
+ context: { parserCode: error.code }
4432
+ });
4433
+ fail(message, void 0, stable);
4434
+ } else {
4435
+ process.exitCode = error.exitCode;
4436
+ }
4437
+ } else {
4438
+ const message = error instanceof Error ? error.message : String(error);
4439
+ fail(message, void 0, error instanceof BrowserPilotError ? error : void 0);
4440
+ }
4441
+ }
4442
+ }
4443
+ void main();