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/README.md +90 -27
- package/dist/cli.js +320 -438
- package/dist/daemon.js +1021 -719
- package/docs/architecture/browser-pilot-platform-spec.md +50 -19
- package/docs/integration/stdio-bridge.md +40 -16
- package/docs/plans/profile-context-routing.md +34 -9
- package/docs/plans/universal-agent-integration.md +18 -5
- package/docs/plans/v0.3.0-stabilization.md +24 -7
- package/docs/releases/v0.2.0.md +53 -0
- package/docs/releases/v0.2.1.md +13 -0
- package/docs/releases/v0.2.2.md +18 -0
- package/docs/releases/v0.3.0.md +68 -0
- package/docs/releases/v0.4.0.md +62 -0
- package/package.json +7 -7
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.
|
|
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
|
-
|
|
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", "
|
|
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: "
|
|
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
|
|
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:
|
|
2978
|
+
instanceId: identity.instanceId
|
|
3243
2979
|
},
|
|
3244
|
-
protocol: { min: { major: 1, minor: 1 }, max: { major: 1, minor:
|
|
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(
|
|
3258
|
-
clientKey
|
|
2993
|
+
const created = asRecord(await transport.brokerCall(identity.bridgeSessionId, "workspaces/create", {
|
|
2994
|
+
clientKey
|
|
3259
2995
|
}), "workspaces/create");
|
|
3260
|
-
const leased = asRecord(await transport.brokerCall(
|
|
2996
|
+
const leased = asRecord(await transport.brokerCall(identity.bridgeSessionId, "leases/create", {
|
|
3261
2997
|
workspaceId: created.workspace.id,
|
|
3262
|
-
clientKey
|
|
2998
|
+
clientKey,
|
|
3263
2999
|
ttlMs: LEASE_TTL_MS
|
|
3264
3000
|
}), "leases/create");
|
|
3265
|
-
return new _CompatibilityBrokerClient(
|
|
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(
|
|
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(
|
|
3072
|
+
const selected = targets.find(isSelected) ?? targets.find((target) => target.origin !== "user_tab");
|
|
3321
3073
|
if (selected) {
|
|
3322
|
-
if (!selected
|
|
3074
|
+
if (!isSelected(selected)) {
|
|
3323
3075
|
await this.callTool("browser.tabs.switch", { targetId: selected.targetId });
|
|
3324
|
-
selected.
|
|
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(
|
|
3084
|
+
const selected = targets.find(isSelected) ?? targets[0];
|
|
3333
3085
|
if (selected) {
|
|
3334
|
-
if (!selected
|
|
3086
|
+
if (!isSelected(selected)) {
|
|
3335
3087
|
await this.callTool("browser.tabs.switch", { targetId: selected.targetId });
|
|
3336
|
-
selected.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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":"...", "
|
|
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
|
-
...
|
|
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
|
-
|
|
3621
|
-
|
|
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 (
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
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 =
|
|
3635
|
-
if (
|
|
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
|
|
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
|
|
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)
|
|
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 (
|
|
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
|
|
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.
|
|
3803
|
-
|
|
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,
|
|
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
|
|
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)
|
|
3858
|
-
|
|
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 (
|
|
3863
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
3938
|
-
if (
|
|
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)
|
|
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 =
|
|
4018
|
-
if (
|
|
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)
|
|
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
|
|
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))
|
|
4104
|
-
|
|
4105
|
-
|
|
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))
|
|
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))
|
|
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
|
|
4159
|
-
const inputIndex =
|
|
4160
|
-
if (
|
|
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
|
|
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 =
|
|
4109
|
+
const idx = Number(target);
|
|
4267
4110
|
if (!Number.isSafeInteger(idx) || idx < 0 || idx >= frames.length) {
|
|
4268
|
-
throw
|
|
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
|
|
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
|
|
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.
|
|
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("
|
|
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 =
|
|
4189
|
+
const index = Number(indexStr);
|
|
4343
4190
|
const targets = await client.listTabs("all");
|
|
4344
|
-
if (!Number.isSafeInteger(index) || index <
|
|
4345
|
-
throw
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
...
|
|
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 =
|
|
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)
|
|
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 <
|
|
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
|
|
4362
|
+
if (!existsSync4(filePath)) throw invalidArgument(`File not found: ${filePath}`, "file");
|
|
4506
4363
|
body = readFileSync4(filePath, "utf-8");
|
|
4507
4364
|
}
|
|
4508
|
-
const status =
|
|
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
|
|
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
|
-
|
|
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();
|