@browserbridge/bbx 1.7.8 → 1.9.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.
Files changed (44) hide show
  1. package/PRIVACY.md +57 -12
  2. package/README.md +19 -8
  3. package/SECURITY.md +12 -0
  4. package/package.json +15 -6
  5. package/packages/agent-client/src/cli-args.js +59 -0
  6. package/packages/agent-client/src/cli-batch.js +121 -58
  7. package/packages/agent-client/src/cli-output.js +8 -1
  8. package/packages/agent-client/src/cli.js +272 -70
  9. package/packages/agent-client/src/client.js +296 -8
  10. package/packages/agent-client/src/command-registry.js +64 -55
  11. package/packages/agent-client/src/config.js +195 -0
  12. package/packages/agent-client/src/content-extract.js +198 -0
  13. package/packages/agent-client/src/npm-self-update.js +305 -0
  14. package/packages/agent-client/src/remotes.js +4 -2
  15. package/packages/agent-client/src/runtime.js +864 -39
  16. package/packages/agent-client/src/types.ts +169 -6
  17. package/packages/mcp-server/src/guidance.js +1 -1
  18. package/packages/mcp-server/src/handlers-capture.js +69 -6
  19. package/packages/mcp-server/src/handlers-dom.js +61 -4
  20. package/packages/mcp-server/src/handlers-navigation.js +2 -1
  21. package/packages/mcp-server/src/handlers-page.js +211 -54
  22. package/packages/mcp-server/src/handlers-utils.js +101 -10
  23. package/packages/mcp-server/src/handlers.js +14 -7
  24. package/packages/mcp-server/src/server.js +176 -11
  25. package/packages/native-host/src/artifact-store.js +353 -0
  26. package/packages/native-host/src/config.js +9 -1
  27. package/packages/native-host/src/daemon-logger.js +3 -1
  28. package/packages/native-host/src/daemon.js +701 -33
  29. package/packages/native-host/src/native-host.js +28 -3
  30. package/packages/protocol/src/capability-values.js +2 -0
  31. package/packages/protocol/src/defaults.js +31 -0
  32. package/packages/protocol/src/errors.js +102 -6
  33. package/packages/protocol/src/incidental-sanitizer.js +197 -0
  34. package/packages/protocol/src/index.js +2 -0
  35. package/packages/protocol/src/protocol.js +612 -6
  36. package/packages/protocol/src/recovery-telemetry.js +302 -0
  37. package/packages/protocol/src/registry.js +206 -38
  38. package/packages/protocol/src/summary.js +203 -30
  39. package/packages/protocol/src/types.ts +721 -2
  40. package/skills/browser-bridge/SKILL.md +54 -19
  41. package/skills/browser-bridge/references/interaction.md +55 -11
  42. package/skills/browser-bridge/references/patch-workflow.md +2 -0
  43. package/skills/browser-bridge/references/protocol.md +265 -86
  44. package/skills/browser-bridge/references/token-efficiency.md +29 -13
@@ -0,0 +1,302 @@
1
+ // @ts-check
2
+
3
+ export const RECOVERY_WINDOW_MS = 5 * 60 * 1000;
4
+ export const RECOVERY_BUCKET_MS = 5 * 1000;
5
+ export const RECOVERY_LOOP_WINDOW_MS = 60 * 1000;
6
+ export const RECOVERY_LOOP_THRESHOLD = 3;
7
+ export const RECOVERY_MAX_GROUPS = 64;
8
+ export const RECOVERY_PUBLIC_COUNT_LIMIT = 10_000;
9
+
10
+ /** @type {readonly import('./types.js').RecoveryEventKind[]} */
11
+ export const RECOVERY_EVENT_KINDS = Object.freeze([
12
+ 'automatic_mcp_retry',
13
+ 'stale_ref_recovery',
14
+ 'debugger_reattach',
15
+ 'content_script_reinjection',
16
+ 'native_host_reconnect',
17
+ 'request_outcome',
18
+ ]);
19
+
20
+ const RECOVERY_EVENT_KIND_SET = new Set(RECOVERY_EVENT_KINDS);
21
+ const BUCKET_COUNT = RECOVERY_WINDOW_MS / RECOVERY_BUCKET_MS + 1;
22
+
23
+ /**
24
+ * Fixed-bucket, process-local recovery telemetry. Group names are used only to
25
+ * detect repeated failures and are never included in public snapshots.
26
+ */
27
+ export class RecoveryTelemetryCollector {
28
+ /** @param {{ now?: () => number }} [options] */
29
+ constructor({ now = Date.now } = {}) {
30
+ this.now = now;
31
+ /** @type {Array<{ start: number, events: Map<import('./types.js').RecoveryEventKind, { attempts: number, successes: number, failures: number, lastEventAt: number }> } | null>} */
32
+ this.buckets = Array.from({ length: BUCKET_COUNT }, () => null);
33
+ /** @type {Map<string, { failures: [number, number, number], next: number, count: number, touchedAt: number, kind: import('./types.js').RecoveryEventKind }>} */
34
+ this.groups = new Map();
35
+ }
36
+
37
+ /**
38
+ * @param {import('./types.js').RecoveryEventKind} kind
39
+ * @param {'success' | 'failure'} outcome
40
+ * @param {string} [group='default']
41
+ */
42
+ record(kind, outcome, group = 'default') {
43
+ if (!RECOVERY_EVENT_KIND_SET.has(kind)) return;
44
+ const now = normalizeNow(this.now());
45
+ const start = Math.floor(now / RECOVERY_BUCKET_MS) * RECOVERY_BUCKET_MS;
46
+ const index = Math.floor(start / RECOVERY_BUCKET_MS) % BUCKET_COUNT;
47
+ let bucket = this.buckets[index];
48
+ if (!bucket || bucket.start !== start) {
49
+ bucket = { start, events: new Map() };
50
+ this.buckets[index] = bucket;
51
+ }
52
+ const event = bucket.events.get(kind) ?? {
53
+ attempts: 0,
54
+ successes: 0,
55
+ failures: 0,
56
+ lastEventAt: 0,
57
+ };
58
+ event.attempts += 1;
59
+ event.successes += outcome === 'success' ? 1 : 0;
60
+ event.failures += outcome === 'failure' ? 1 : 0;
61
+ event.lastEventAt = now;
62
+ bucket.events.set(kind, event);
63
+
64
+ this.pruneGroups(now);
65
+ if (outcome !== 'failure') return;
66
+ const groupKey = `${kind}\u0000${group}`;
67
+ let state = this.groups.get(groupKey);
68
+ if (!state) {
69
+ while (this.groups.size >= RECOVERY_MAX_GROUPS) {
70
+ const oldest = [...this.groups].reduce((left, right) =>
71
+ left[1].touchedAt <= right[1].touchedAt ? left : right
72
+ );
73
+ this.groups.delete(oldest[0]);
74
+ }
75
+ state = {
76
+ failures: [0, 0, 0],
77
+ next: 0,
78
+ count: 0,
79
+ touchedAt: now,
80
+ kind,
81
+ };
82
+ this.groups.set(groupKey, state);
83
+ }
84
+ state.failures[state.next] = now;
85
+ state.next = (state.next + 1) % RECOVERY_LOOP_THRESHOLD;
86
+ state.count = Math.min(RECOVERY_LOOP_THRESHOLD, state.count + 1);
87
+ state.touchedAt = now;
88
+ }
89
+
90
+ /** @param {import('./types.js').RecoveryScope} scope */
91
+ snapshot(scope) {
92
+ const now = normalizeNow(this.now());
93
+ this.pruneGroups(now);
94
+ /** @type {Record<import('./types.js').RecoveryEventKind, import('./types.js').RecoveryEventSummary>} */
95
+ const events =
96
+ /** @type {Record<import('./types.js').RecoveryEventKind, import('./types.js').RecoveryEventSummary>} */ ({});
97
+ for (const kind of RECOVERY_EVENT_KINDS) {
98
+ let attempts = 0;
99
+ let successes = 0;
100
+ let failures = 0;
101
+ let lastEventAt = 0;
102
+ for (const bucket of this.buckets) {
103
+ if (
104
+ !bucket ||
105
+ bucket.start + RECOVERY_BUCKET_MS <= now - RECOVERY_WINDOW_MS ||
106
+ bucket.start > now
107
+ ) {
108
+ continue;
109
+ }
110
+ const event = bucket.events.get(kind);
111
+ if (!event) continue;
112
+ attempts += event.attempts;
113
+ successes += event.successes;
114
+ failures += event.failures;
115
+ lastEventAt = Math.max(lastEventAt, event.lastEventAt);
116
+ }
117
+ const activeLoop = [...this.groups.values()].some((group) => {
118
+ if (group.kind !== kind || group.count < RECOVERY_LOOP_THRESHOLD) return false;
119
+ return group.failures.every((at) => at >= now - RECOVERY_LOOP_WINDOW_MS && at <= now);
120
+ });
121
+ events[kind] = createPublicEventSummary(
122
+ attempts,
123
+ successes,
124
+ failures,
125
+ activeLoop,
126
+ lastEventAt
127
+ );
128
+ }
129
+ return {
130
+ scope,
131
+ windowMs: RECOVERY_WINDOW_MS,
132
+ bucketMs: RECOVERY_BUCKET_MS,
133
+ loopWindowMs: RECOVERY_LOOP_WINDOW_MS,
134
+ loopThreshold: RECOVERY_LOOP_THRESHOLD,
135
+ asOf: now,
136
+ activeLoop: RECOVERY_EVENT_KINDS.some((kind) => events[kind].activeLoop),
137
+ events,
138
+ };
139
+ }
140
+
141
+ /** @param {number} now */
142
+ pruneGroups(now) {
143
+ const cutoff = now - RECOVERY_LOOP_WINDOW_MS;
144
+ for (const [key, group] of this.groups) {
145
+ let recent = 0;
146
+ for (let index = 0; index < group.count; index += 1) {
147
+ const at = group.failures[index];
148
+ if (at >= cutoff && at <= now) recent += 1;
149
+ }
150
+ if (recent === 0) this.groups.delete(key);
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Strictly reconstruct a public summary from an untrusted transport value.
157
+ * @param {unknown} value
158
+ * @param {import('./types.js').RecoveryScope} expectedScope
159
+ * @returns {import('./types.js').RecoveryTelemetrySummary | null}
160
+ */
161
+ export function normalizeRecoveryTelemetrySummary(value, expectedScope) {
162
+ const source = asRecord(value);
163
+ if (!source || source.scope !== expectedScope) return null;
164
+ const asOf = strictTimestamp(source.asOf);
165
+ if (asOf === null) return null;
166
+ const eventsSource = asRecord(source.events);
167
+ if (!eventsSource) return null;
168
+ /** @type {Record<import('./types.js').RecoveryEventKind, import('./types.js').RecoveryEventSummary>} */
169
+ const events =
170
+ /** @type {Record<import('./types.js').RecoveryEventKind, import('./types.js').RecoveryEventSummary>} */ ({});
171
+ for (const kind of RECOVERY_EVENT_KINDS) {
172
+ const event = asRecord(eventsSource[kind]);
173
+ if (!event) return null;
174
+ const attempts = rawCount(event.attempts);
175
+ const successes = rawCount(event.successes);
176
+ const failures = rawCount(event.failures);
177
+ const pending = rawCount(event.pending);
178
+ const saturated = event.saturated;
179
+ if (
180
+ attempts === null ||
181
+ successes === null ||
182
+ failures === null ||
183
+ pending === null ||
184
+ typeof saturated !== 'boolean' ||
185
+ successes + failures + pending !== attempts ||
186
+ (saturated
187
+ ? attempts !== RECOVERY_PUBLIC_COUNT_LIMIT
188
+ : attempts > RECOVERY_PUBLIC_COUNT_LIMIT)
189
+ ) {
190
+ return null;
191
+ }
192
+ const lastEventAt = normalizeLastEventAt(event.lastEventAt, attempts, asOf);
193
+ if (lastEventAt === undefined) return null;
194
+ events[kind] = {
195
+ attempts,
196
+ successes,
197
+ failures,
198
+ pending,
199
+ saturated,
200
+ successRate: saturated || attempts === 0 ? null : roundRate(successes / attempts),
201
+ failureRate: saturated || attempts === 0 ? null : roundRate(failures / attempts),
202
+ activeLoop: event.activeLoop === true && failures >= RECOVERY_LOOP_THRESHOLD,
203
+ lastEventAt,
204
+ };
205
+ }
206
+ return {
207
+ scope: expectedScope,
208
+ windowMs: RECOVERY_WINDOW_MS,
209
+ bucketMs: RECOVERY_BUCKET_MS,
210
+ loopWindowMs: RECOVERY_LOOP_WINDOW_MS,
211
+ loopThreshold: RECOVERY_LOOP_THRESHOLD,
212
+ asOf,
213
+ activeLoop: RECOVERY_EVENT_KINDS.some((kind) => events[kind].activeLoop),
214
+ events,
215
+ };
216
+ }
217
+
218
+ /** @param {unknown} value */
219
+ function asRecord(value) {
220
+ return value && typeof value === 'object' && !Array.isArray(value)
221
+ ? /** @type {Record<string, unknown>} */ (value)
222
+ : null;
223
+ }
224
+
225
+ /** @param {unknown} value @returns {number | null} */
226
+ function rawCount(value) {
227
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
228
+ }
229
+
230
+ /** @param {unknown} value @returns {number | null} */
231
+ function strictTimestamp(value) {
232
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
233
+ }
234
+
235
+ /** @param {number} value */
236
+ function normalizeNow(value) {
237
+ return Number.isSafeInteger(value) && value >= 0 ? value : 0;
238
+ }
239
+
240
+ /**
241
+ * @param {unknown} value
242
+ * @param {number} attempts
243
+ * @param {number} asOf
244
+ * @returns {number | null | undefined}
245
+ */
246
+ function normalizeLastEventAt(value, attempts, asOf) {
247
+ if (attempts === 0) return value === null ? null : undefined;
248
+ const timestamp = strictTimestamp(value);
249
+ if (
250
+ timestamp === null ||
251
+ timestamp > asOf ||
252
+ timestamp < asOf - RECOVERY_WINDOW_MS - RECOVERY_BUCKET_MS
253
+ ) {
254
+ return undefined;
255
+ }
256
+ return timestamp;
257
+ }
258
+
259
+ /** @param {number} value */
260
+ function roundRate(value) {
261
+ return Math.round(value * 10_000) / 10_000;
262
+ }
263
+
264
+ /**
265
+ * @param {number} attempts
266
+ * @param {number} successes
267
+ * @param {number} failures
268
+ * @param {boolean} activeLoop
269
+ * @param {number} lastEventAt
270
+ * @returns {import('./types.js').RecoveryEventSummary}
271
+ */
272
+ function createPublicEventSummary(attempts, successes, failures, activeLoop, lastEventAt) {
273
+ if (attempts <= RECOVERY_PUBLIC_COUNT_LIMIT) {
274
+ return {
275
+ attempts,
276
+ successes,
277
+ failures,
278
+ pending: 0,
279
+ saturated: false,
280
+ successRate: attempts > 0 ? roundRate(successes / attempts) : null,
281
+ failureRate: attempts > 0 ? roundRate(failures / attempts) : null,
282
+ activeLoop,
283
+ lastEventAt: attempts > 0 ? lastEventAt : null,
284
+ };
285
+ }
286
+ const proportionalFailures = Math.floor((failures / attempts) * RECOVERY_PUBLIC_COUNT_LIMIT);
287
+ const cappedFailures = Math.min(
288
+ RECOVERY_PUBLIC_COUNT_LIMIT,
289
+ Math.max(activeLoop ? RECOVERY_LOOP_THRESHOLD : 0, proportionalFailures)
290
+ );
291
+ return {
292
+ attempts: RECOVERY_PUBLIC_COUNT_LIMIT,
293
+ successes: RECOVERY_PUBLIC_COUNT_LIMIT - cappedFailures,
294
+ failures: cappedFailures,
295
+ pending: 0,
296
+ saturated: true,
297
+ successRate: null,
298
+ failureRate: null,
299
+ activeLoop,
300
+ lastEventAt,
301
+ };
302
+ }